Part of #358 (Epic 3: TaskScheduler + Subtask Fan-out).
Depends on: Story 3.2c (Extension-side task-scoping guard — must land first).
Context
Story 3.2c gates posting at the extension host — non-focused tasks don't post. This story adds defense-in-depth on the webview side: the frontend actively rejects messages tagged with a non-current taskId, and migrates clineMessagesSeq from a single global counter to a per-task Map<string, number>.
Without the seq migration a focus switch mid-flight causes a correctness bug: the newly-focused task's first state push carries its own lower seq value, which the webview rejects because the old task's higher global counter is still in place — silently discarding the first update after every task switch.
Also adds a "N tasks running" visual indicator in ChatView so users know background tasks are active.
Developer Notes
In src/core/webview/ClineProvider.ts:
- Migrate
clineMessagesSeq from number to Map<string, number> keyed by taskId. Each task increments its own seq counter. On focus switch, the new task's seq starts from its own accumulated value, not from the global counter.
- Emit
clineMessagesTaskId: string alongside clineMessagesSeq in state messages, populated from taskRegistry.current?.taskId.
In webview-ui/src/context/ExtensionStateContext.tsx:
- In
handleMessage for messageUpdated: reject if message.taskId !== currentState.currentTaskId. This prevents a background task's message from corrupting the displayed task's chat.
- In
mergeExtensionState: reject clineMessages if clineMessagesTaskId !== currentTaskId (cross-task state push from a task that lost focus mid-flight).
- Update seq comparison to use per-task seq: only compare seq values when
clineMessagesTaskId matches.
In webview-ui/src/components/chat/ChatView.tsx (or a small new component):
- If
runningTaskIds.length > 1: show a small indicator (e.g., "2 tasks running" pill/badge near the TaskHeader). Clicking it navigates to task history where SubtaskRow already handles navigation.
- No other UI changes needed — existing subtask tree and "Back to parent task" button handle task switching.
Files: src/core/webview/ClineProvider.ts, webview-ui/src/context/ExtensionStateContext.tsx, webview-ui/src/components/chat/ChatView.tsx
Tests (webview-ui/src/__tests__/ExtensionStateContext.spec.ts or similar):
messageUpdated with taskId !== currentTaskId → message is rejected, clineMessages unchanged.
messageUpdated with taskId === currentTaskId → message is applied normally.
- State message with
clineMessagesTaskId !== currentTaskId → clineMessages not updated, other state fields still merged.
- Per-task seq: state with matching
taskId and higher seq → applied. State with matching taskId and lower seq → rejected.
Acceptance Criteria
- Webview rejects
messageUpdated from non-current tasks — no message corruption when two tasks run concurrently.
- Per-task
clineMessagesSeq prevents cross-task stale overwrites after a focus switch.
- "N tasks running" indicator is visible in
ChatView when runningTaskIds.length > 1.
- All existing single-task tests pass unchanged — per-task seq is transparent when only one task runs.
Part of #358 (Epic 3: TaskScheduler + Subtask Fan-out).
Depends on: Story 3.2c (Extension-side task-scoping guard — must land first).
Context
Story 3.2c gates posting at the extension host — non-focused tasks don't post. This story adds defense-in-depth on the webview side: the frontend actively rejects messages tagged with a non-current
taskId, and migratesclineMessagesSeqfrom a single global counter to a per-taskMap<string, number>.Without the seq migration a focus switch mid-flight causes a correctness bug: the newly-focused task's first state push carries its own lower seq value, which the webview rejects because the old task's higher global counter is still in place — silently discarding the first update after every task switch.
Also adds a "N tasks running" visual indicator in
ChatViewso users know background tasks are active.Developer Notes
In
src/core/webview/ClineProvider.ts:clineMessagesSeqfromnumbertoMap<string, number>keyed bytaskId. Each task increments its own seq counter. On focus switch, the new task's seq starts from its own accumulated value, not from the global counter.clineMessagesTaskId: stringalongsideclineMessagesSeqin state messages, populated fromtaskRegistry.current?.taskId.In
webview-ui/src/context/ExtensionStateContext.tsx:handleMessageformessageUpdated: reject ifmessage.taskId !== currentState.currentTaskId. This prevents a background task's message from corrupting the displayed task's chat.mergeExtensionState: rejectclineMessagesifclineMessagesTaskId !== currentTaskId(cross-task state push from a task that lost focus mid-flight).clineMessagesTaskIdmatches.In
webview-ui/src/components/chat/ChatView.tsx(or a small new component):runningTaskIds.length > 1: show a small indicator (e.g.,"2 tasks running"pill/badge near the TaskHeader). Clicking it navigates to task history whereSubtaskRowalready handles navigation.Files:
src/core/webview/ClineProvider.ts,webview-ui/src/context/ExtensionStateContext.tsx,webview-ui/src/components/chat/ChatView.tsxTests (
webview-ui/src/__tests__/ExtensionStateContext.spec.tsor similar):messageUpdatedwithtaskId !== currentTaskId→ message is rejected,clineMessagesunchanged.messageUpdatedwithtaskId === currentTaskId→ message is applied normally.clineMessagesTaskId !== currentTaskId→clineMessagesnot updated, other state fields still merged.taskIdand higher seq → applied. State with matchingtaskIdand lower seq → rejected.Acceptance Criteria
messageUpdatedfrom non-current tasks — no message corruption when two tasks run concurrently.clineMessagesSeqprevents cross-task stale overwrites after a focus switch.ChatViewwhenrunningTaskIds.length > 1.