Repository navigation
[Story 3.2d] Webview-side task-scoping guard + per-task seq #371
Description
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.
VS Code recommends passing serializable snapshots from the extension host rather than maintaining competing frontend truth. React's own docs flag redundant/duplicated state as a common bug source, and warn that state preservation/reset is tied to component tree position — task identity must be explicit (via currentTaskId) rather than inferred from component position. This story enforces both: the webview derives display state from the extension snapshot, not from its own internal tracking.
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
clineMessagesSeqfromnumber(currently line 204) toMap<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. This is transparent when only one task runs — the single-task seq behavior is identical (only the map key changes). - Emit
clineMessagesTaskId: stringalongsideclineMessagesSeqin state messages, populated fromtaskRegistry.current?.taskId.
In webview-ui/src/context/ExtensionStateContext.tsx:
- In
handleMessageformessageUpdated: reject ifmessage.taskId !== currentState.currentTaskId. This prevents a background task's message from corrupting the displayed task's chat. - In
mergeExtensionState(currently line 172 for the existing global seq guard): rejectclineMessagesifclineMessagesTaskId !== 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 whenclineMessagesTaskIdmatchescurrentTaskId.
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 whereSubtaskRowalready 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):
messageUpdatedwithtaskId !== currentTaskId→ message is rejected,clineMessagesunchanged.messageUpdatedwithtaskId === currentTaskId→ message is applied normally.- State message with
clineMessagesTaskId !== currentTaskId→clineMessagesnot updated, other state fields still merged. - Per-task seq: state with matching
taskIdand higher seq → applied. State with matchingtaskIdand lower seq → rejected. - Focus switch from task A to task B where B has a lower sequence than A → B's first state push is accepted because sequence comparison is per-task.
- Single-task: per-task seq is transparent — all existing seq guard tests pass unchanged.
Acceptance Criteria
- Webview rejects
messageUpdatedfrom non-current tasks — no message corruption when two tasks run concurrently. - Per-task
clineMessagesSeqprevents cross-task stale overwrites after a focus switch. - "N tasks running" indicator is visible in
ChatViewwhenrunningTaskIds.length > 1. - All existing single-task tests pass unchanged — per-task seq is transparent when only one task runs.
Out of Scope
- Story [Story 3.2b] Fan-out: parent stays active while child runs #369 owns parent/child execution-context isolation and delegation rollback safety.
- Story [Story 3.2c] Extension-side task-scoping guard #370 owns extension-host post suppression and live-task focus switching.
- Story [Story 3.3] Child completion callback and parent wakeup #372 owns child completion callback and parent wakeup.
Activity