Part of #358 (Epic 3: TaskScheduler + Subtask Fan-out).
Depends on: Story 3.2b (Fan-out). Story 3.2d (webview-side guard) depends on this and must follow.
Context
The webview is fundamentally single-task. messageUpdated events have no taskId field (Task.ts, line 1048), and ChatView renders whatever arrives without verifying it belongs to currentTaskId. If two tasks post to the webview simultaneously — which becomes possible when maxConcurrency > 1 via Story 3.2b — messages from a background task corrupt the displayed task's chat.
The primary fix lives in the extension host: non-focused tasks simply do not post. VS Code's WebviewView is a single instance per view ID (no multi-pane option), so the extension host must enforce focus gating. This story implements the extension-side half. Story 3.2d implements the webview-side defense-in-depth (rejection guard + per-task seq migration).
Developer Notes
In src/core/task/Task.ts:
- Add
taskId field to messageUpdated events: provider?.postMessageToWebview({ type: "messageUpdated", clineMessage: message, taskId: this.taskId }).
- Gate
postStateToWebviewWithoutTaskHistory() and postMessageToWebview({ type: "messageUpdated" }): only post if this.taskId === this.provider.taskRegistry.current?.taskId. Non-focused tasks accumulate messages in their own clineMessages array silently; the webview receives them when the user navigates to that task.
In src/core/webview/ClineProvider.ts:
- Add
runningTaskIds: string[] to the state shape returned by getStateToPostToWebview(), populated from taskRegistry.getRunning().map(t => t.taskId).
- When
showTaskWithId is handled and the target task is still running in TaskRegistry: call taskRegistry.setCurrent(taskId) (focus switch) and push the target task's current clineMessages to the webview. Do NOT pop/destroy the previous task — it continues running in the background.
In packages/types/src/vscode-extension-host.ts:
- Add
taskId?: string to ExtensionMessage for messageUpdated type.
- Add
clineMessagesTaskId?: string stub alongside clineMessagesSeq in state messages (consumed by Story 3.2d).
Files: src/core/task/Task.ts, src/core/webview/ClineProvider.ts, packages/types/src/vscode-extension-host.ts
Tests (src/core/task/__tests__/TaskWebviewGuard.spec.ts):
- Non-focused task:
postStateToWebviewWithoutTaskHistory() does NOT call postMessageToWebview. Assert via spy.
- Focused task:
postStateToWebviewWithoutTaskHistory() calls postMessageToWebview normally.
messageUpdated includes taskId field matching the emitting task.
- Focus switch via
taskRegistry.setCurrent(newTaskId): subsequent posts from old task are suppressed; new task's posts go through.
runningTaskIds in state reflects all running tasks from TaskRegistry.
Acceptance Criteria
messageUpdated events include taskId — grep -n "messageUpdated" src/core/task/Task.ts shows taskId in every call.
- Non-focused tasks do not post to the webview — verified by spy-based test.
runningTaskIds is present in webview state and reflects TaskRegistry.getRunning().
- All existing single-task tests pass unchanged — the guard is transparent when only one task runs.
Part of #358 (Epic 3: TaskScheduler + Subtask Fan-out).
Depends on: Story 3.2b (Fan-out). Story 3.2d (webview-side guard) depends on this and must follow.
Context
The webview is fundamentally single-task.
messageUpdatedevents have notaskIdfield (Task.ts, line 1048), andChatViewrenders whatever arrives without verifying it belongs tocurrentTaskId. If two tasks post to the webview simultaneously — which becomes possible whenmaxConcurrency > 1via Story 3.2b — messages from a background task corrupt the displayed task's chat.The primary fix lives in the extension host: non-focused tasks simply do not post. VS Code's
WebviewViewis a single instance per view ID (no multi-pane option), so the extension host must enforce focus gating. This story implements the extension-side half. Story 3.2d implements the webview-side defense-in-depth (rejection guard + per-task seq migration).Developer Notes
In
src/core/task/Task.ts:taskIdfield tomessageUpdatedevents:provider?.postMessageToWebview({ type: "messageUpdated", clineMessage: message, taskId: this.taskId }).postStateToWebviewWithoutTaskHistory()andpostMessageToWebview({ type: "messageUpdated" }): only post ifthis.taskId === this.provider.taskRegistry.current?.taskId. Non-focused tasks accumulate messages in their ownclineMessagesarray silently; the webview receives them when the user navigates to that task.In
src/core/webview/ClineProvider.ts:runningTaskIds: string[]to the state shape returned bygetStateToPostToWebview(), populated fromtaskRegistry.getRunning().map(t => t.taskId).showTaskWithIdis handled and the target task is still running inTaskRegistry: calltaskRegistry.setCurrent(taskId)(focus switch) and push the target task's currentclineMessagesto the webview. Do NOT pop/destroy the previous task — it continues running in the background.In
packages/types/src/vscode-extension-host.ts:taskId?: stringtoExtensionMessageformessageUpdatedtype.clineMessagesTaskId?: stringstub alongsideclineMessagesSeqin state messages (consumed by Story 3.2d).Files:
src/core/task/Task.ts,src/core/webview/ClineProvider.ts,packages/types/src/vscode-extension-host.tsTests (
src/core/task/__tests__/TaskWebviewGuard.spec.ts):postStateToWebviewWithoutTaskHistory()does NOT callpostMessageToWebview. Assert via spy.postStateToWebviewWithoutTaskHistory()callspostMessageToWebviewnormally.messageUpdatedincludestaskIdfield matching the emitting task.taskRegistry.setCurrent(newTaskId): subsequent posts from old task are suppressed; new task's posts go through.runningTaskIdsin state reflects all running tasks fromTaskRegistry.Acceptance Criteria
messageUpdatedevents includetaskId—grep -n "messageUpdated" src/core/task/Task.tsshowstaskIdin every call.runningTaskIdsis present in webview state and reflectsTaskRegistry.getRunning().