{activeTaskId ? (
-
+
) : (
No task selected
diff --git a/apps/web/components/task/task-chat-panel.tsx b/apps/web/components/task/task-chat-panel.tsx
index 0fe686e9bd..80a977113d 100644
--- a/apps/web/components/task/task-chat-panel.tsx
+++ b/apps/web/components/task/task-chat-panel.tsx
@@ -3,7 +3,7 @@
import { FormEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { flushSync } from 'react-dom';
import { useVirtualizer } from '@tanstack/react-virtual';
-import { IconBrain, IconCpu, IconListCheck, IconLoader2, IconPaperclip } from '@tabler/icons-react';
+import { IconBrain, IconListCheck, IconLoader2, IconPaperclip } from '@tabler/icons-react';
import { Button } from '@kandev/ui/button';
import {
DropdownMenu,
@@ -11,7 +11,7 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
} from '@kandev/ui/dropdown-menu';
-import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@kandev/ui/select';
+
import { Tooltip, TooltipContent, TooltipTrigger } from '@kandev/ui/tooltip';
import { cn } from '@/lib/utils';
import { useAppStore } from '@/components/state-provider';
@@ -31,25 +31,20 @@ import { TokenUsageDisplay } from '@/components/task/chat/token-usage-display';
import { TodoSummary } from '@/components/task/chat/todo-summary';
import { SessionsDropdown } from '@/components/task/sessions-dropdown';
import { useSessionContextWindow } from '@/hooks/use-session-context-window';
-
-type AgentOption = {
- id: string;
- label: string;
-};
+import { ModelSelector } from '@/components/task/model-selector';
type TaskChatPanelProps = {
- agents: AgentOption[];
+ /** @deprecated No longer used - model selection is now handled by ModelSelector */
+ agents?: { id: string; label: string }[];
onSend?: (message: string) => void;
sessionId?: string | null;
};
export function TaskChatPanel({
- agents,
onSend,
sessionId = null,
}: TaskChatPanelProps) {
const [messageInput, setMessageInput] = useState('');
- const [selectedAgent, setSelectedAgent] = useState(agents[0]?.id ?? '');
const [planModeEnabled, setPlanModeEnabled] = useState(false);
const [isSending, setIsSending] = useState(false);
const messagesContainerRef = useRef(null);
@@ -84,6 +79,12 @@ export function TaskChatPanel({
const sessionModel = sessionProfile?.model ?? null;
+ // Get pending model state for this session
+ const pendingModels = useAppStore((state) => state.pendingModel.bySessionId);
+ const clearPendingModel = useAppStore((state) => state.clearPendingModel);
+ const setActiveModel = useAppStore((state) => state.setActiveModel);
+ const pendingModel = resolvedSessionId ? pendingModels[resolvedSessionId] : null;
+
// Fetch context window usage for this session
const contextWindow = useSessionContextWindow(resolvedSessionId);
@@ -248,12 +249,27 @@ export function TaskChatPanel({
}
const client = getWebSocketClient();
if (!client) return;
+
+ // Include pending model in the request if one is selected
+ const modelToSend = pendingModel && pendingModel !== sessionModel ? pendingModel : undefined;
+
await client.request(
'message.add',
- { task_id: taskId, session_id: resolvedSessionId, content: message },
+ {
+ task_id: taskId,
+ session_id: resolvedSessionId,
+ content: message,
+ ...(modelToSend && { model: modelToSend }),
+ },
10000
);
- }, [resolvedSessionId, taskId]);
+
+ // Move pending model to active model after sending (it's now the model in use)
+ if (modelToSend && resolvedSessionId) {
+ setActiveModel(resolvedSessionId, modelToSend);
+ clearPendingModel(resolvedSessionId);
+ }
+ }, [resolvedSessionId, taskId, pendingModel, sessionModel, setActiveModel, clearPendingModel]);
// Cancels the current agent turn without terminating the agent process,
// allowing the user to interrupt and send a new prompt.
@@ -341,17 +357,10 @@ export function TaskChatPanel({
)}
- {/* Session info - shows agent state and model */}
+ {/* Session info - shows agent state */}
{session?.state && (
- {/* Model indicator */}
- {sessionModel && (
-
-
- {sessionModel}
-
- )}
)}
@@ -372,18 +381,7 @@ export function TaskChatPanel({
/>
-
+
diff --git a/apps/web/components/task/task-layout.tsx b/apps/web/components/task/task-layout.tsx
index bf3208b437..c8cbbcc36d 100644
--- a/apps/web/components/task/task-layout.tsx
+++ b/apps/web/components/task/task-layout.tsx
@@ -14,11 +14,13 @@ const DEFAULT_HORIZONTAL_LAYOUT: [number, number, number] = [18, 57, 25];
type TaskLayoutProps = {
workspaceId: string | null;
boardId: string | null;
+ sessionId?: string | null;
};
export const TaskLayout = memo(function TaskLayout({
workspaceId,
boardId,
+ sessionId = null,
}: TaskLayoutProps) {
const [horizontalLayout, setHorizontalLayout] = useState<[number, number, number]>(
getLocalStorage('task-layout-horizontal-v2', DEFAULT_HORIZONTAL_LAYOUT)
@@ -64,6 +66,7 @@ export const TaskLayout = memo(function TaskLayout({
openFileRequest={openFileRequest}
onDiffPathHandled={() => setSelectedDiffPath(null)}
onFileOpenHandled={() => setOpenFileRequest(null)}
+ sessionId={sessionId}
/>
diff --git a/apps/web/lib/state/store.ts b/apps/web/lib/state/store.ts
index 9aae81a379..bbdc3a2f1b 100644
--- a/apps/web/lib/state/store.ts
+++ b/apps/web/lib/state/store.ts
@@ -259,6 +259,14 @@ export type SessionAgentctlState = {
itemsBySessionId: Record;
};
+export type PendingModelState = {
+ bySessionId: Record;
+};
+
+export type ActiveModelState = {
+ bySessionId: Record;
+};
+
export type AppState = {
kanban: KanbanState;
workspaces: WorkspaceState;
@@ -290,6 +298,8 @@ export type AppState = {
sessionAgentctl: SessionAgentctlState;
worktrees: WorktreesState;
sessionWorktreesBySessionId: SessionWorktreesState;
+ pendingModel: PendingModelState;
+ activeModel: ActiveModelState;
hydrate: (state: Partial) => void;
setActiveWorkspace: (workspaceId: string | null) => void;
setWorkspaces: (workspaces: WorkspaceState['items']) => void;
@@ -352,6 +362,9 @@ export type AppState = {
clearGitStatus: (sessionId: string) => void;
setContextWindow: (sessionId: string, contextWindow: ContextWindowEntry) => void;
bumpAgentProfilesVersion: () => void;
+ setPendingModel: (sessionId: string, modelId: string) => void;
+ clearPendingModel: (sessionId: string) => void;
+ setActiveModel: (sessionId: string, modelId: string) => void;
};
export type AppStore = ReturnType;
@@ -402,6 +415,8 @@ const defaultState: AppState = {
sessionAgentctl: { itemsBySessionId: {} },
worktrees: { items: {} },
sessionWorktreesBySessionId: { itemsBySessionId: {} },
+ pendingModel: { bySessionId: {} },
+ activeModel: { bySessionId: {} },
hydrate: () => undefined,
setActiveWorkspace: () => undefined,
setWorkspaces: () => undefined,
@@ -450,6 +465,9 @@ const defaultState: AppState = {
clearGitStatus: () => undefined,
setContextWindow: () => undefined,
bumpAgentProfilesVersion: () => undefined,
+ setPendingModel: () => undefined,
+ clearPendingModel: () => undefined,
+ setActiveModel: () => undefined,
};
function mergeInitialState(
@@ -504,6 +522,9 @@ function mergeInitialState(
| 'clearGitStatus'
| 'setContextWindow'
| 'bumpAgentProfilesVersion'
+ | 'setPendingModel'
+ | 'clearPendingModel'
+ | 'setActiveModel'
> {
if (!initialState) return defaultState;
return {
@@ -543,6 +564,8 @@ function mergeInitialState(
...defaultState.sessionWorktreesBySessionId,
...initialState.sessionWorktreesBySessionId,
},
+ pendingModel: { ...defaultState.pendingModel, ...initialState.pendingModel },
+ activeModel: { ...defaultState.activeModel, ...initialState.activeModel },
};
}
@@ -842,7 +865,11 @@ export function createAppStore(initialState?: Partial) {
}),
setTaskSession: (session) =>
set((draft) => {
- draft.taskSessions.items[session.id] = session;
+ // Merge with existing session data to preserve fields like agent_profile_id
+ const existingSession = draft.taskSessions.items[session.id];
+ draft.taskSessions.items[session.id] = existingSession
+ ? { ...existingSession, ...session }
+ : session;
if (session.worktree_id) {
draft.worktrees.items[session.worktree_id] = {
id: session.worktree_id,
@@ -909,6 +936,18 @@ export function createAppStore(initialState?: Partial) {
set((draft) => {
draft.agentProfiles.version += 1;
}),
+ setPendingModel: (sessionId, modelId) =>
+ set((draft) => {
+ draft.pendingModel.bySessionId[sessionId] = modelId;
+ }),
+ clearPendingModel: (sessionId) =>
+ set((draft) => {
+ delete draft.pendingModel.bySessionId[sessionId];
+ }),
+ setActiveModel: (sessionId, modelId) =>
+ set((draft) => {
+ draft.activeModel.bySessionId[sessionId] = modelId;
+ }),
}))
);
}
diff --git a/apps/web/lib/types/backend.ts b/apps/web/lib/types/backend.ts
index 92c88d0ae7..4a07c8e6f4 100644
--- a/apps/web/lib/types/backend.ts
+++ b/apps/web/lib/types/backend.ts
@@ -155,6 +155,7 @@ export type TaskSessionStateChangedPayload = {
session_id: string;
old_state?: string;
new_state: string;
+ agent_profile_id?: string;
metadata?: Record;
};
diff --git a/apps/web/lib/ws/handlers/agent-session.ts b/apps/web/lib/ws/handlers/agent-session.ts
index a4d23cd0f5..2e9426c284 100644
--- a/apps/web/lib/ws/handlers/agent-session.ts
+++ b/apps/web/lib/ws/handlers/agent-session.ts
@@ -18,13 +18,15 @@ export function registerTaskSessionHandlers(store: StoreApi): WsHandle
if (sessionId) {
const existingSession = store.getState().taskSessions.items[sessionId];
if (existingSession) {
- // Update existing session
+ // Update existing session - preserve all existing fields
store.getState().setTaskSession({
...existingSession,
state: newState,
});
} else {
// Create partial session object
+ // Include agent_profile_id from the payload if provided
+ const agentProfileId = payload.agent_profile_id;
store.getState().setTaskSession({
id: sessionId,
task_id: taskId,
@@ -32,6 +34,7 @@ export function registerTaskSessionHandlers(store: StoreApi): WsHandle
progress: 0,
started_at: '',
updated_at: '',
+ ...(agentProfileId ? { agent_profile_id: agentProfileId } : {}),
});
}
@@ -39,6 +42,7 @@ export function registerTaskSessionHandlers(store: StoreApi): WsHandle
if (sessionsByTask) {
const hasSession = sessionsByTask.some((session) => session.id === sessionId);
if (!hasSession) {
+ const agentProfileId = payload.agent_profile_id;
store.getState().setTaskSessionsForTask(taskId, [...sessionsByTask, {
id: sessionId,
task_id: taskId,
@@ -46,6 +50,7 @@ export function registerTaskSessionHandlers(store: StoreApi): WsHandle
progress: 0,
started_at: '',
updated_at: '',
+ ...(agentProfileId ? { agent_profile_id: agentProfileId } : {}),
}]);
} else {
const nextSessions = sessionsByTask.map((session) =>
diff --git a/docs/design/dynamic-model-switching.md b/docs/design/dynamic-model-switching.md
new file mode 100644
index 0000000000..6ac1f1edde
--- /dev/null
+++ b/docs/design/dynamic-model-switching.md
@@ -0,0 +1,478 @@
+# Dynamic Model Switching Feature Analysis
+
+> **Status**: Design Document
+> **Created**: 2026-01-21
+> **Scope**: Mid-session model switching within the Kandev application
+
+## Executive Summary
+
+This document analyzes the implementation of dynamic model switching, allowing users to change the AI model mid-session without starting a new session. The feature requires UI changes to expose model selection, backend logic to detect model changes and restart agents, and careful handling of session continuity.
+
+## Current Architecture Overview
+
+### Session Lifecycle (Current Flow)
+
+```
+User clicks "Start Session"
+ → Frontend sends orchestrator.start with agentProfileId
+ → Orchestrator.StartTask → Executor.ExecuteWithProfile
+ → Lifecycle Manager resolves profile → gets model from AgentProfileInfo
+ → CommandBuilder.BuildCommand adds --model flag
+ → Agent process started with fixed model
+ → ACP session initialized
+ → User sends prompts via session/prompt
+```
+
+**Key Insight**: Model is determined at session start via `AgentProfileInfo.Model` and baked into the agent command. There's no current mechanism to change the model mid-session.
+
+### Relevant Components
+
+| Component | Path | Role |
+|-----------|------|------|
+| TaskChatPanel | `apps/web/components/task/task-chat-panel.tsx` | Chat UI, displays current model |
+| SessionsDropdown | `apps/web/components/task/sessions-dropdown.tsx` | Session switcher |
+| Orchestrator Service | `apps/backend/internal/orchestrator/service.go` | PromptTask, StartTask, ResumeTaskSession |
+| Executor | `apps/backend/internal/orchestrator/executor/executor.go` | Agent launch, prompt forwarding |
+| Lifecycle Manager | `apps/backend/internal/agent/lifecycle/manager.go` | Agent process management |
+| CommandBuilder | `apps/backend/internal/agent/lifecycle/command.go` | Builds CLI args including --model |
+| ProfileResolver | `apps/backend/internal/agent/lifecycle/profile_resolver.go` | Resolves profile → model |
+| SessionManager | `apps/backend/internal/agent/lifecycle/session.go` | ACP session init/prompt |
+| TaskSession Model | `apps/backend/internal/task/models/models.go` | Session persistence |
+| Message Service | `apps/backend/internal/task/service/service.go` | CreateMessage |
+
+---
+
+## Requirements Breakdown
+
+### UI Requirements
+
+1. **Model Selector in Session View**: Replace existing agent selector with model dropdown
+2. **Deferred Application**: Selection does NOT take effect immediately
+3. **Apply on Prompt**: Selected model applied when user sends next prompt
+4. **Persistent State**: All subsequent prompts use new model until changed again
+
+### Backend Requirements
+
+1. **Model Change Detection**: Compare current model vs requested model on prompt
+2. **System Message Insertion**: "Model switching to [model name]" message
+3. **Agent Restart**: Stop current agent, restart with new `--model` flag
+4. **Session Resumption**: Attempt to resume ACP session after restart (if supported)
+
+---
+
+## Technical Approach
+
+### Phase 1: Frontend Model Selector
+
+#### 1.1 Store State for Pending Model
+
+Add to the Zustand store a per-session "pending model" that tracks what model the user wants to switch to.
+
+**Files to modify:**
+- `apps/web/lib/state/store.ts` - Add `pendingModelBySessionId: Record`
+- `apps/web/lib/types/http.ts` - Add model-related types if needed
+
+#### 1.2 Model Selector Component
+
+Create a model dropdown in the chat panel that:
+- Shows available models for the current agent type
+- Shows current model (from session's agent profile)
+- Allows selection of a different model
+- Stores selection in `pendingModelBySessionId[sessionId]`
+
+**Files to modify:**
+- `apps/web/components/task/task-chat-panel.tsx` - Add ModelSelector component
+- May create new component: `apps/web/components/task/model-selector.tsx`
+
+**Data source for available models:**
+- From `AvailableAgent.model_config.available_models` in the agent discovery store
+- Need to resolve which agent the session is using via `session.agent_profile_id`
+
+#### 1.3 Prompt Submission with Model Override
+
+When user submits a prompt, check if `pendingModelBySessionId[sessionId]` differs from current model:
+- If different: include `model` field in WebSocket prompt payload
+- Clear pending model after submission (backend will handle the switch)
+
+**WebSocket payload change:**
+
+```typescript
+// Current
+{ action: "orchestrator.prompt", payload: { taskId, sessionId, content } }
+
+// Proposed
+{ action: "orchestrator.prompt", payload: { taskId, sessionId, content, model?: string } }
+```
+
+**Files to modify:**
+- `apps/web/lib/ws/actions.ts` - Add `model` to prompt action payload
+- `apps/web/components/task/task-chat-panel.tsx` - Include model in prompt submission
+
+---
+
+### Phase 2: Backend Model Change Detection
+
+#### 2.1 Extend Prompt Handling
+
+Modify the orchestrator's `PromptTask` to accept an optional model parameter.
+
+**Files to modify:**
+- `apps/backend/internal/orchestrator/service.go` - `PromptTask` method
+- `apps/backend/internal/ws/handlers/orchestrator.go` - Parse model from WS payload
+
+**Logic flow:**
+```
+PromptTask(taskId, sessionId, content, model)
+ → If model != currentExecution.Model:
+ → Insert system message "Switching to {model}..."
+ → Stop current agent (graceful shutdown)
+ → Update session's effective model
+ → Restart agent with new --model flag
+ → Resume or reinitialize ACP session
+ → Forward prompt to agent
+```
+
+#### 2.2 Executor Model Switch
+
+The executor needs a new method to handle model switching:
+
+**Files to modify:**
+- `apps/backend/internal/orchestrator/executor/executor.go`
+ - Add `SwitchModel(sessionId, newModel)` method
+ - Coordinates stop → rebuild → restart → resume
+
+**Implementation sketch:**
+```go
+func (e *Executor) SwitchModel(ctx context.Context, sessionId, newModel string) error {
+ exec, ok := e.running[sessionId]
+ if !ok {
+ return ErrSessionNotFound
+ }
+
+ // 1. Stop current agent
+ e.lifecycleManager.StopAgent(ctx, exec.AgentID)
+
+ // 2. Update profile with new model
+ newProfileInfo := exec.ProfileInfo
+ newProfileInfo.Model = newModel
+
+ // 3. Restart agent with new command
+ newAgentID, err := e.lifecycleManager.Launch(ctx, LaunchOpts{
+ Model: newModel,
+ // ... other opts from exec
+ })
+
+ // 4. Attempt session resume
+ e.lifecycleManager.InitializeSession(ctx, newAgentID, InitOpts{
+ IsResume: true,
+ ResumeToken: exec.ResumeToken,
+ })
+
+ return nil
+}
+```
+
+#### 2.3 System Message for Model Switch
+
+Insert a visible message in the chat indicating the model change.
+
+**Files to modify:**
+- `apps/backend/internal/task/service/service.go` - Use `CreateMessage` with type `status`
+
+**Message format:**
+```json
+{
+ "type": "status",
+ "content": "Switching model to Claude Opus 4.5...",
+ "metadata": {
+ "status_type": "model_switch",
+ "from_model": "sonnet4.5",
+ "to_model": "opus4.5"
+ }
+}
+```
+
+---
+
+### Phase 3: Session Resumption After Restart
+
+#### 3.1 Agent-Specific Resumption
+
+Different agents handle resumption differently (from `agents.json`):
+
+| Agent | `resume_via_acp` | `resume_flag` | `can_recover` |
+|-------|------------------|---------------|---------------|
+| auggie-agent | false | `--resume` | false |
+| codex-agent | true | (none) | true |
+
+**For Codex-style agents (resume_via_acp: true):**
+- Use ACP `session/load` with the stored `ResumeToken`
+- Session history is preserved by the agent
+
+**For Auggie-style agents (resume_via_acp: false, can_recover: false):**
+- Cannot truly resume; must reinitialize with fresh ACP session
+- **Decision**: Accept context loss, display system message: "Model switched. A new session was started."
+
+#### 3.2 Handling Resume Failures
+
+**Files to modify:**
+- `apps/backend/internal/agent/lifecycle/session.go` - `InitializeSession` error handling
+
+**Fallback strategy:**
+1. Attempt resume with stored token
+2. If resume fails → start new session
+3. Emit warning message to user: "Session could not be fully resumed after model switch"
+
+
+---
+
+## Integration Points
+
+### WebSocket Integration
+
+| Layer | Change Required |
+|-------|-----------------|
+| Client (`apps/web/lib/ws/client.ts`) | No changes |
+| Actions (`apps/web/lib/ws/actions.ts`) | Add `model` field to prompt action |
+| Handler (`apps/backend/internal/ws/handlers/orchestrator.go`) | Parse `model` from payload |
+
+**Wire format example:**
+```json
+{
+ "id": "uuid",
+ "type": "request",
+ "action": "orchestrator.prompt",
+ "payload": {
+ "task_id": "task-uuid",
+ "session_id": "session-uuid",
+ "content": "User message",
+ "model": "opus4.5"
+ }
+}
+```
+
+### Store Integration
+
+**New store slice additions:**
+
+```typescript
+// In store.ts
+pendingModel: {
+ bySessionId: Record, // sessionId → pending model id
+ setPendingModel: (sessionId: string, modelId: string) => void,
+ clearPendingModel: (sessionId: string) => void,
+ getPendingModel: (sessionId: string) => string | null,
+}
+```
+
+**Store → Component data flow:**
+```
+ModelSelector reads: settingsAgents (for available models), session.agent_profile_id
+ModelSelector writes: pendingModel.bySessionId[sessionId]
+TaskChatPanel reads: pendingModel.bySessionId[sessionId] on prompt submit
+```
+
+### Database Integration
+
+**Decision: No schema change (runtime-only)**
+- Model switch is runtime-only; session's `agent_profile_id` remains unchanged
+- On backend restart, session resumes with original profile model
+- No migration required
+
+---
+
+## Challenges and Edge Cases
+
+### 1. Agent Busy During Model Switch Request
+
+**Scenario**: User selects new model while agent is processing a previous prompt.
+
+**Solution options:**
+- **Queue the switch**: Store pending model, apply after current response completes
+- **Block selection**: Disable model selector while agent is busy
+- **Immediate abort**: Cancel current response, switch model (destructive)
+
+**Recommendation**: Queue the switch; apply on next user-initiated prompt.
+
+### 2. Rapid Model Changes
+
+**Scenario**: User changes model multiple times before sending a prompt.
+
+**Solution**: Only the final selection matters; each change overwrites `pendingModelBySessionId[sessionId]`.
+
+### 3. Resume Failure After Switch
+
+**Scenario**: Agent restarts with new model but ACP session/load fails.
+
+**Handling:**
+1. Log error for debugging
+2. Fall back to new session initialization
+3. Show user message: "Model switched successfully but session context could not be fully restored"
+
+### 4. Different Agent Types
+
+**Scenario**: User tries to switch to a model not supported by current agent type.
+
+**Prevention**: Model selector only shows models from `agent.model_config.available_models` for the session's agent type.
+
+### 5. Session State Persistence
+
+**Scenario**: Backend restarts mid-session; what model should resume use?
+
+**Behavior**: Uses `agent_profile_snapshot.model` from session creation (original profile model). Runtime model switches are not persisted.
+
+### 6. Model Availability Changes
+
+**Scenario**: A model is removed from agent configuration while sessions are active.
+
+**Handling**: Validate model against current `available_models` before sending to backend. If invalid, fall back to session's original model.
+
+---
+
+## Alternative Approaches
+
+### Alternative A: Runtime Model Switching via ACP
+
+**Concept**: Send a hypothetical `session/switch_model` ACP call without restarting agent.
+
+**Pros:**
+- No process restart; faster switch
+- Seamless context preservation
+
+**Cons:**
+- Not supported by current agent implementations
+- Would require agent CLI changes upstream
+- Different agents would need different implementations
+
+**Verdict**: Not viable without significant upstream work.
+
+### Alternative B: Model as Separate Session Concept
+
+**Concept**: Treat each model switch as a "sub-session" within the same UI session.
+
+**Pros:**
+- Clean separation of concerns
+- Easy to track model history
+
+**Cons:**
+- Complex UI implications
+- Breaks current session model significantly
+- Overhead for simple model changes
+
+**Verdict**: Over-engineering for the requirement.
+
+### Alternative C: Pre-select Model Before Session Start Only
+
+**Concept**: Allow model selection only at session creation, not mid-session.
+
+**Pros:**
+- Simplest implementation
+- No restart complexity
+
+**Cons:**
+- Doesn't meet the stated requirement
+- Poor UX for iterative work
+
+**Verdict**: Does not satisfy user requirements.
+
+---
+
+## Implementation Phases
+
+### Phase 1: Frontend Model Selector (Est. 2-3 days)
+
+**Goal**: Model dropdown appears in UI; selection stored locally.
+
+**Files to modify:**
+1. `apps/web/lib/state/store.ts` - Add pendingModel slice
+2. `apps/web/components/task/task-chat-panel.tsx` - Add ModelSelector
+3. Create `apps/web/components/task/model-selector.tsx` - New component
+
+**Deliverable**: User can select a model, but it has no effect yet.
+
+### Phase 2: Backend Prompt Extension (Est. 2-3 days)
+
+**Goal**: Prompts can include model; backend detects change.
+
+**Files to modify:**
+1. `apps/web/lib/ws/actions.ts` - Add model to prompt payload
+2. `apps/backend/internal/ws/handlers/orchestrator.go` - Parse model
+3. `apps/backend/internal/orchestrator/service.go` - PromptTask accepts model
+
+**Deliverable**: Model parameter flows from UI to orchestrator.
+
+### Phase 3: Agent Restart Logic (Est. 3-4 days)
+
+**Goal**: Backend restarts agent with new model when detected.
+
+**Files to modify:**
+1. `apps/backend/internal/orchestrator/executor/executor.go` - SwitchModel method
+2. `apps/backend/internal/agent/lifecycle/manager.go` - Ensure clean restart
+3. `apps/backend/internal/task/service/service.go` - Insert status message
+
+**Deliverable**: Model switch triggers agent restart; status message appears.
+
+### Phase 4: Session Resumption (Est. 2-3 days)
+
+**Goal**: Resume ACP session after agent restart (where supported).
+
+**Files to modify:**
+1. `apps/backend/internal/agent/lifecycle/session.go` - Resume logic
+2. `apps/backend/internal/orchestrator/executor/executor.go` - Handle resume failures
+
+**Deliverable**: Codex-style agents resume context; Auggie-style gracefully degrade.
+
+### Phase 5: Testing & Edge Cases (Est. 2-3 days)
+
+**Goal**: Verify all scenarios work correctly.
+
+**Test scenarios:**
+- Model switch while agent idle
+- Model switch while agent busy (queued)
+- Rapid model changes
+- Resume failure fallback
+- Invalid model selection
+- Backend restart mid-session
+
+---
+
+## Summary of Files to Modify
+
+| File | Changes |
+|------|---------|
+| `apps/web/lib/state/store.ts` | Add pendingModel slice |
+| `apps/web/components/task/task-chat-panel.tsx` | Integrate ModelSelector, pass model on prompt |
+| `apps/web/components/task/model-selector.tsx` | New component |
+| `apps/web/lib/ws/actions.ts` | Add model field to prompt action |
+| `apps/backend/internal/ws/handlers/orchestrator.go` | Parse model from WS payload |
+| `apps/backend/internal/orchestrator/service.go` | Extend PromptTask signature |
+| `apps/backend/internal/orchestrator/executor/executor.go` | Add SwitchModel method |
+| `apps/backend/internal/agent/lifecycle/manager.go` | Ensure clean restart support |
+| `apps/backend/internal/agent/lifecycle/session.go` | Handle resume after restart |
+| `apps/backend/internal/task/service/service.go` | Create model switch status message |
+
+---
+
+## Estimated Total Effort
+
+| Phase | Estimate |
+|-------|----------|
+| Phase 1: Frontend Model Selector | 2-3 days |
+| Phase 2: Backend Prompt Extension | 2-3 days |
+| Phase 3: Agent Restart Logic | 3-4 days |
+| Phase 4: Session Resumption | 2-3 days |
+| Phase 5: Testing & Edge Cases | 2-3 days |
+| **Total** | **11-16 days** |
+
+---
+
+## Resolved Decisions
+
+| Decision | Resolution |
+|----------|------------|
+| Database schema | Runtime-only, no schema changes |
+| Non-recoverable agents (Auggie) | Accept context loss, show "new session started" message |
+| Agent busy during switch | Not an issue - model applied on next user prompt |
+| UI placement | Replace existing agent selector with model selector |
+| Confirmation dialog | None required - seamless switch |
+