From ace099afd7c606a448d6ff47f04f1de115a8d9fa Mon Sep 17 00:00:00 2001 From: Roo Code Date: Tue, 12 May 2026 12:43:43 +0000 Subject: [PATCH 1/4] docs: add Phase 6 architectural design for background task visibility Addresses Issue #12330 Phase 6 planning discussion. Covers: - Conversation replay (6a) - Tab/panel switching (6b) - Real-time progress streaming (6c) Includes feasibility analysis, priority recommendations, and detailed design with message types, component structure, and testing strategy. --- .../phase-6-background-task-visibility.md | 277 ++++++++++++++++++ 1 file changed, 277 insertions(+) create mode 100644 docs/architecture/phase-6-background-task-visibility.md diff --git a/docs/architecture/phase-6-background-task-visibility.md b/docs/architecture/phase-6-background-task-visibility.md new file mode 100644 index 0000000000..d9e4ca13fe --- /dev/null +++ b/docs/architecture/phase-6-background-task-visibility.md @@ -0,0 +1,277 @@ +# Phase 6: Background Task Visibility and Interaction + +> Architectural design document for Issue #12330 +> Phase 6 of "Support parallel execution of specialized agents and improve context handoff between modes" + +## 1. Context + +Phase 5 (Background Tasks Panel UI) is complete. This document proposes the scope, priority, and architecture for Phase 6, which focuses on enabling better visibility and interaction with background tasks. + +## 2. Current Architecture + +### Task Lifecycle + +`ClineProvider` maintains a `clineStack: Task[]` (LIFO). Only the top-of-stack task is "current" -- all state updates, webview messages, and user interactions route through `getCurrentTask()`. + +``` +ClineProvider +├── clineStack: Task[] # LIFO stack, sequential execution +├── taskHistoryStore # Per-task file persistence +├── getCurrentTask() # Returns top of stack +├── addClineToStack(task) # Push new task +└── removeClineFromStack() # Pop completed task +``` + +### Task Persistence + +| Layer | File | Purpose | +|-------|------|---------| +| Messages | `taskMessages.ts` | Save/load `ClineMessage[]` per task | +| API History | `apiMessages.ts` | Save/load API conversation history | +| History Items | `TaskHistoryStore.ts` | Per-task metadata files with in-memory cache | +| Metadata | `taskMetadata.ts` | Task metadata helpers | + +### Webview Communication + +The extension sends typed `ExtensionMessage` objects to the webview. Key message types: + +- `state` -- Full state snapshot (includes `clineMessages`, `currentTaskId`) +- `taskHistoryUpdated` -- Full history list refresh +- `taskHistoryItemUpdated` -- Single history item update + +Currently, `postStateToWebviewWithoutTaskHistory()` sends state for only the current task. There is no mechanism to send updates for background tasks. + +### Subtask Support + +Parent-child relationships exist via `parentTaskId` and `childIds` on `HistoryItem`. The `new_task` tool creates subtasks that push onto the stack. When a subtask completes, it pops and returns control to the parent. + +## 3. Agreed Scope for Phase 6 + +**In scope (Items 1-3):** +1. Full conversation replay for completed background tasks +2. Tab switching / multi-task view +3. Real-time progress streaming for active background tasks + +**Deferred to Phase 7 (Items 4-5):** +4. Write-capable background tasks + basic file locking +5. Persistent background task history across sessions + +## 4. Feasibility Analysis + +### Item 1: Full Conversation Replay + +**Complexity: Medium | Risk: Low** + +`readTaskMessages(taskId, globalStoragePath)` already loads the full `ClineMessage[]` array from disk for any task. The existing `ChatView` component renders these messages. The main work is creating a read-only wrapper that: + +- Accepts a `taskId` prop instead of reading from global state +- Loads messages on mount via a new webview message +- Hides input controls (chat box, approval buttons) +- Renders tool calls, outputs, and assistant responses in the same format + +**Why it's low risk:** No changes to task execution, persistence, or the foreground task flow. Purely additive UI + a new message handler. + +### Item 2: Tab Switching / Multi-task View + +**Complexity: Medium-High | Risk: Medium** + +The webview already has a tab system in `App.tsx` (`tab === "history"`, `tab === "settings"`, `tab === "chat"`). Adding a background tasks view requires: + +- A new tab or panel within the chat view +- A list of active/completed background tasks with status indicators +- Navigation to open a task's replay view or live view +- State management to track which background task is currently being viewed + +**Key challenge:** The webview currently receives state for only one task. Viewing a background task must not disrupt the foreground task's state. This requires either: + - (a) A separate message channel for background task data, or + - (b) A secondary state context in the webview that can hold background task data alongside the primary task state + +Option (a) is cleaner and avoids polluting the existing state management. + +### Item 3: Real-time Progress Streaming + +**Complexity: High | Risk: Medium-High** + +Currently, `Task.ts` calls `provider.postStateToWebviewWithoutTaskHistory()` to update the UI. This method sends the full state for the current task only. For background tasks to stream progress: + +1. `Task.ts` must emit incremental updates even when it is not the "current" task +2. A new message type (`backgroundTaskProgress`) must carry task-scoped updates +3. The webview must handle concurrent update streams without degrading performance +4. Throttling/batching is needed to prevent excessive re-renders + +**Why it's harder:** Requires changes to the core task execution loop (`Task.ts`), not just additive UI. The task currently assumes it IS the visible task when posting updates. + +## 5. Recommended Priority Order + +``` +Phase 6a: Conversation Replay (Foundation -- standalone value) + │ + ▼ +Phase 6b: Tab/Panel Switching (Navigation framework, depends on 6a) + │ + ▼ +Phase 6c: Real-time Progress Streaming (Highest complexity, builds on 6b) +``` + +Each sub-phase is independently shippable and testable. + +## 6. Detailed Design + +### 6a. Conversation Replay + +#### New Message Types + +```typescript +// Webview → Extension +interface RequestBackgroundTaskMessages { + type: "requestBackgroundTaskMessages" + taskId: string +} + +// Extension → Webview +interface BackgroundTaskMessages { + type: "backgroundTaskMessages" + taskId: string + messages: ClineMessage[] +} +``` + +#### Extension Handler (webviewMessageHandler.ts) + +```typescript +case "requestBackgroundTaskMessages": { + const taskId = message.taskId + const globalStoragePath = provider.contextProxy.globalStorageUri.fsPath + const messages = await readTaskMessages(taskId, globalStoragePath) + provider.postMessageToWebview({ + type: "backgroundTaskMessages", + taskId, + messages: messages ?? [], + }) + break +} +``` + +#### Webview Component + +``` +BackgroundTaskReplayView +├── Props: { taskId: string, onClose: () => void } +├── State: messages (ClineMessage[]), loading (boolean) +├── On mount: sends requestBackgroundTaskMessages +├── On message: receives backgroundTaskMessages, filters by taskId +├── Renders: read-only message list (reuses ChatRow components) +└── No input controls, no approval buttons +``` + +### 6b. Tab/Panel Switching + +#### UI Structure + +``` +App.tsx +├── tab === "chat" → ChatView (foreground task) +├── tab === "history" → HistoryView +├── tab === "settings" → SettingsView +└── tab === "bgTask" → BackgroundTaskView + ├── BackgroundTasksList (sidebar/panel) + │ ├── Active tasks with status badges + │ └── Completed tasks + └── BackgroundTaskReplayView (from 6a) OR BackgroundTaskLiveView (from 6c) +``` + +#### State Management + +```typescript +// New webview state (in App.tsx or dedicated context) +interface BackgroundTaskViewState { + selectedTaskId: string | null + viewMode: "replay" | "live" +} +``` + +#### Navigation Flow + +1. User clicks background task icon/tab +2. App switches to `tab === "bgTask"` +3. BackgroundTasksList shows available tasks +4. User clicks a task → sets `selectedTaskId` +5. If task is completed → opens BackgroundTaskReplayView +6. If task is active → opens BackgroundTaskLiveView (Phase 6c) + +### 6c. Real-time Progress Streaming + +#### New Message Types + +```typescript +// Extension → Webview (incremental updates) +interface BackgroundTaskProgress { + type: "backgroundTaskProgress" + taskId: string + update: BackgroundTaskUpdate +} + +interface BackgroundTaskUpdate { + kind: "tool_call" | "tool_result" | "assistant_text" | "status_change" | "error" + timestamp: number + data: any // Typed per kind +} +``` + +#### Task.ts Changes + +Add a method that emits progress regardless of whether the task is "current": + +```typescript +// In Task.ts +private emitBackgroundProgress(update: BackgroundTaskUpdate) { + const provider = this.providerRef.deref() + if (!provider) return + + // Only emit background updates when this task is NOT the current task + if (provider.getCurrentTask()?.taskId === this.taskId) return + + provider.postMessageToWebview({ + type: "backgroundTaskProgress", + taskId: this.taskId, + update, + }) +} +``` + +#### Throttling Strategy + +- Batch updates in 200ms windows +- Cap at 10 updates per batch per task +- Drop older updates if buffer exceeds threshold +- Priority: status_change > error > tool_result > tool_call > assistant_text + +#### Webview: BackgroundTaskLiveView + +``` +BackgroundTaskLiveView +├── Props: { taskId: string } +├── State: updates (BackgroundTaskUpdate[]), status +├── Subscribes to backgroundTaskProgress messages filtered by taskId +├── Renders: streaming list of tool calls and results +├── Auto-scrolls to latest update +└── Shows task status badge (running, paused, completed, errored) +``` + +## 7. Testing Strategy + +| Area | Test Type | Key Scenarios | +|------|-----------|---------------| +| Message handler | Unit (vitest) | Request/response for task messages, missing task, corrupt data | +| BackgroundTaskReplayView | Component (vitest + RTL) | Loading state, message rendering, empty state | +| Tab switching | Component (vitest + RTL) | Tab navigation, state preservation, back to foreground | +| Progress streaming | Unit (vitest) | Throttling, batching, concurrent tasks | +| Integration | E2E (if feasible) | Full flow: start bg task → view progress → replay after completion | + +## 8. Open Questions + +1. **Should the background task list be a sidebar panel or a tab?** A sidebar panel (like the existing Phase 5 panel) keeps the foreground task visible. A tab replaces the view entirely but is simpler. + +2. **Message size limits for replay:** Completed tasks can have thousands of messages. Should we paginate or lazy-load? Initial recommendation: load all at once (same as current ChatView behavior), optimize if performance becomes an issue. + +3. **Progress streaming granularity:** Should we stream every tool call parameter, or just tool names + status? Start with names + status, add detail incrementally. From 62b556e99eb3c5b7f4109ba3fb5fea7f3531c241 Mon Sep 17 00:00:00 2001 From: Roo Code Date: Tue, 12 May 2026 13:04:00 +0000 Subject: [PATCH 2/4] docs: refine 6c to MVP scope, expand open questions for discussion --- .../phase-6-background-task-visibility.md | 107 ++++++++++++++++-- 1 file changed, 95 insertions(+), 12 deletions(-) diff --git a/docs/architecture/phase-6-background-task-visibility.md b/docs/architecture/phase-6-background-task-visibility.md index d9e4ca13fe..a0732de4cd 100644 --- a/docs/architecture/phase-6-background-task-visibility.md +++ b/docs/architecture/phase-6-background-task-visibility.md @@ -199,7 +199,21 @@ interface BackgroundTaskViewState { 5. If task is completed → opens BackgroundTaskReplayView 6. If task is active → opens BackgroundTaskLiveView (Phase 6c) -### 6c. Real-time Progress Streaming +### 6c. Real-time Progress Streaming (Minimal Viable Version) + +> **Design principle:** Keep Phase 6c tightly scoped to avoid expanding the phase. Ship the simplest useful version first; richer detail can be added incrementally in later phases. + +#### MVP Scope + +The minimal viable version streams only: +- **Tool name + status** (started / completed / errored) -- not full parameters or output +- **Last N updates** (rolling window of ~20 items) -- older entries are discarded client-side +- **Status changes** (running, paused, completed, errored) + +What is explicitly **out of scope** for the MVP: +- Full tool call parameters or output payloads +- Assistant text streaming +- Persistent storage of streamed updates (replay from disk covers completed tasks) #### New Message Types @@ -212,12 +226,16 @@ interface BackgroundTaskProgress { } interface BackgroundTaskUpdate { - kind: "tool_call" | "tool_result" | "assistant_text" | "status_change" | "error" + kind: "tool_call" | "tool_result" | "status_change" | "error" timestamp: number - data: any // Typed per kind + toolName?: string // e.g. "read_file", "execute_command" + status?: string // e.g. "started", "completed", "errored" + errorMessage?: string // Only for kind === "error" } ``` +Note: `assistant_text` is excluded from the MVP. The update interface uses typed optional fields instead of `data: any` to keep the contract narrow and safe. + #### Task.ts Changes Add a method that emits progress regardless of whether the task is "current": @@ -239,25 +257,29 @@ private emitBackgroundProgress(update: BackgroundTaskUpdate) { } ``` +The hook points in Task.ts should be minimal -- emit at tool call start and tool call end only. Avoid adding hooks inside the LLM streaming loop for the MVP. + #### Throttling Strategy -- Batch updates in 200ms windows -- Cap at 10 updates per batch per task -- Drop older updates if buffer exceeds threshold -- Priority: status_change > error > tool_result > tool_call > assistant_text +- Batch updates in 500ms windows (conservative default; can be tuned down later) +- Cap at 5 updates per batch per task +- Drop older updates if buffer exceeds threshold (keep last N = 20) +- Priority ordering: status_change > error > tool_result > tool_call #### Webview: BackgroundTaskLiveView ``` BackgroundTaskLiveView ├── Props: { taskId: string } -├── State: updates (BackgroundTaskUpdate[]), status +├── State: updates (BackgroundTaskUpdate[], capped at last 20), status ├── Subscribes to backgroundTaskProgress messages filtered by taskId -├── Renders: streaming list of tool calls and results +├── Renders: compact list of recent tool calls with status icons ├── Auto-scrolls to latest update └── Shows task status badge (running, paused, completed, errored) ``` +The live view intentionally shows a compact summary, not a full chat transcript. Users who want full detail can wait for the task to complete and use the replay view (6a). + ## 7. Testing Strategy | Area | Test Type | Key Scenarios | @@ -270,8 +292,69 @@ BackgroundTaskLiveView ## 8. Open Questions -1. **Should the background task list be a sidebar panel or a tab?** A sidebar panel (like the existing Phase 5 panel) keeps the foreground task visible. A tab replaces the view entirely but is simpler. +The following questions need alignment before implementation begins. They are grouped by area and ordered by impact. + +### UI Layout + +1. **Should the background task list be a sidebar panel or a tab?** + + | Option | Pros | Cons | + |--------|------|------| + | **Sidebar panel** (like Phase 5 panel) | Foreground task stays visible; quick glance at background status without context-switching | More complex layout; may feel cramped in narrow viewports | + | **Full tab** (`tab === "bgTask"`) | Simpler implementation; full width for task details | Replaces the current view entirely; user loses sight of foreground task | + | **Hybrid** (collapsible sidebar that can expand to full view) | Best of both worlds | Highest implementation effort | + + **Recommendation:** Start with a full tab for simplicity. If user feedback indicates they need to monitor background tasks while interacting with the foreground task, add a sidebar mode in a follow-up. + + **Decision needed:** Which option should we ship first? + +2. **Where does the "background tasks" entry point live?** + + Options: + - A new icon in the existing tab bar (alongside chat, history, settings) + - A badge/button on the status area of the chat view + - An entry in the history view with a filter for background tasks + + **Decision needed:** Which placement feels most discoverable without adding clutter? + +3. **Should the replay view share the ChatView component or be a separate component?** + + Reusing `ChatView` with a read-only prop reduces duplication but may introduce coupling. A dedicated `BackgroundTaskReplayView` is more isolated but duplicates rendering logic. + + **Recommendation:** Create a thin wrapper around `ChatRow` components rather than reusing the full `ChatView`. This avoids inheriting input controls, scroll management, and approval button logic that don't apply. + +### Progress Streaming Granularity + +4. **What level of detail should the MVP stream?** + + | Level | What's shown | Bandwidth / perf cost | + |-------|-------------|----------------------| + | **Minimal** (recommended for MVP) | Tool name + status (started/completed/errored) | Very low | + | **Medium** | Tool name + truncated first argument (e.g., file path) | Low | + | **Full** | Complete tool parameters + output | High -- requires careful truncation | + + **Recommendation:** Ship with minimal level. The tool name and status provide enough signal to know "what the background task is doing right now" without performance risk. Medium level can be added as a fast follow if users want more context. + + **Decision needed:** Is the minimal level sufficient, or should we target medium from the start? + +5. **Should streaming updates be opt-in?** + + If multiple background tasks are running, streaming all of them simultaneously could be noisy. Options: + - Stream all tasks by default, throttle aggressively + - Only stream updates for the currently-viewed background task + - Let users toggle streaming per task + + **Recommendation:** Only stream updates for the currently-viewed background task (the one selected in the background task list). This keeps the implementation simple and avoids unnecessary message traffic. + + **Decision needed:** Confirm this approach or choose an alternative. + +6. **How should errors in background tasks be surfaced?** + + When a background task hits an error, the user may not notice if they're focused on the foreground task. Options: + - Badge/notification on the background tasks tab icon + - Toast notification + - Both -2. **Message size limits for replay:** Completed tasks can have thousands of messages. Should we paginate or lazy-load? Initial recommendation: load all at once (same as current ChatView behavior), optimize if performance becomes an issue. + **Recommendation:** Badge on the tab icon (low disruption). Toast notifications can be added later if users miss errors. -3. **Progress streaming granularity:** Should we stream every tool call parameter, or just tool names + status? Start with names + status, add detail incrementally. + **Decision needed:** Is a badge sufficient, or do we need more prominent notification? From 2c0073197055e1469b22601bb484870fc1d3cb93 Mon Sep 17 00:00:00 2001 From: Roo Code Date: Tue, 12 May 2026 13:10:02 +0000 Subject: [PATCH 3/4] docs: incorporate confirmed decisions from design review into Phase 6 document --- .../phase-6-background-task-visibility.md | 77 ++++++------------- 1 file changed, 22 insertions(+), 55 deletions(-) diff --git a/docs/architecture/phase-6-background-task-visibility.md b/docs/architecture/phase-6-background-task-visibility.md index a0732de4cd..7f9ecb420b 100644 --- a/docs/architecture/phase-6-background-task-visibility.md +++ b/docs/architecture/phase-6-background-task-visibility.md @@ -168,14 +168,16 @@ BackgroundTaskReplayView #### UI Structure +A new icon is added to the existing tab bar (alongside chat, history, settings) as the entry point. The background task view occupies the full tab area. + ``` App.tsx ├── tab === "chat" → ChatView (foreground task) ├── tab === "history" → HistoryView ├── tab === "settings" → SettingsView └── tab === "bgTask" → BackgroundTaskView - ├── BackgroundTasksList (sidebar/panel) - │ ├── Active tasks with status badges + ├── BackgroundTasksList (task list with status badges + error badge on tab icon) + │ ├── Active tasks │ └── Completed tasks └── BackgroundTaskReplayView (from 6a) OR BackgroundTaskLiveView (from 6c) ``` @@ -192,7 +194,7 @@ interface BackgroundTaskViewState { #### Navigation Flow -1. User clicks background task icon/tab +1. User clicks background tasks icon in the tab bar 2. App switches to `tab === "bgTask"` 3. BackgroundTasksList shows available tasks 4. User clicks a task → sets `selectedTaskId` @@ -280,6 +282,8 @@ BackgroundTaskLiveView The live view intentionally shows a compact summary, not a full chat transcript. Users who want full detail can wait for the task to complete and use the replay view (6a). +> **Confirmed:** Streaming is scoped to the currently selected background task only. The extension should not emit `backgroundTaskProgress` messages for tasks the user is not viewing. This keeps message traffic low and the implementation simple. + ## 7. Testing Strategy | Area | Test Type | Key Scenarios | @@ -290,71 +294,34 @@ The live view intentionally shows a compact summary, not a full chat transcript. | Progress streaming | Unit (vitest) | Throttling, batching, concurrent tasks | | Integration | E2E (if feasible) | Full flow: start bg task → view progress → replay after completion | -## 8. Open Questions +## 8. Confirmed Decisions -The following questions need alignment before implementation begins. They are grouped by area and ordered by impact. +The following decisions were confirmed during design review and should guide implementation. ### UI Layout -1. **Should the background task list be a sidebar panel or a tab?** - - | Option | Pros | Cons | - |--------|------|------| - | **Sidebar panel** (like Phase 5 panel) | Foreground task stays visible; quick glance at background status without context-switching | More complex layout; may feel cramped in narrow viewports | - | **Full tab** (`tab === "bgTask"`) | Simpler implementation; full width for task details | Replaces the current view entirely; user loses sight of foreground task | - | **Hybrid** (collapsible sidebar that can expand to full view) | Best of both worlds | Highest implementation effort | - - **Recommendation:** Start with a full tab for simplicity. If user feedback indicates they need to monitor background tasks while interacting with the foreground task, add a sidebar mode in a follow-up. - - **Decision needed:** Which option should we ship first? - -2. **Where does the "background tasks" entry point live?** - - Options: - - A new icon in the existing tab bar (alongside chat, history, settings) - - A badge/button on the status area of the chat view - - An entry in the history view with a filter for background tasks - - **Decision needed:** Which placement feels most discoverable without adding clutter? - -3. **Should the replay view share the ChatView component or be a separate component?** - - Reusing `ChatView` with a read-only prop reduces duplication but may introduce coupling. A dedicated `BackgroundTaskReplayView` is more isolated but duplicates rendering logic. - - **Recommendation:** Create a thin wrapper around `ChatRow` components rather than reusing the full `ChatView`. This avoids inheriting input controls, scroll management, and approval button logic that don't apply. - -### Progress Streaming Granularity +1. **Background task view layout: Full tab** (`tab === "bgTask"`) -4. **What level of detail should the MVP stream?** + Start with a full tab for simplicity in Phase 6. A sidebar/hybrid mode may be considered later based on user feedback. - | Level | What's shown | Bandwidth / perf cost | - |-------|-------------|----------------------| - | **Minimal** (recommended for MVP) | Tool name + status (started/completed/errored) | Very low | - | **Medium** | Tool name + truncated first argument (e.g., file path) | Low | - | **Full** | Complete tool parameters + output | High -- requires careful truncation | +2. **Entry point placement: New tab bar icon** - **Recommendation:** Ship with minimal level. The tool name and status provide enough signal to know "what the background task is doing right now" without performance risk. Medium level can be added as a fast follow if users want more context. + Add a new icon in the existing tab bar (alongside chat, history, settings). This is the most discoverable location without cluttering the chat view. - **Decision needed:** Is the minimal level sufficient, or should we target medium from the start? +3. **Replay view implementation: Thin wrapper around ChatRow components** -5. **Should streaming updates be opt-in?** + Create a dedicated `BackgroundTaskReplayView` that wraps `ChatRow` components directly rather than reusing the full `ChatView`. This avoids inheriting input controls, scroll management, and approval button logic that don't apply to read-only replay. - If multiple background tasks are running, streaming all of them simultaneously could be noisy. Options: - - Stream all tasks by default, throttle aggressively - - Only stream updates for the currently-viewed background task - - Let users toggle streaming per task +### Progress Streaming (6c) - **Recommendation:** Only stream updates for the currently-viewed background task (the one selected in the background task list). This keeps the implementation simple and avoids unnecessary message traffic. +4. **Streaming granularity: Minimal level** - **Decision needed:** Confirm this approach or choose an alternative. + Stream tool name + status only (started/completed/errored). This provides enough signal to know what the background task is doing without performance risk. Truncated arguments (medium level) can be added in a follow-up if users need more context. -6. **How should errors in background tasks be surfaced?** +5. **Streaming scope: Currently selected task only** - When a background task hits an error, the user may not notice if they're focused on the foreground task. Options: - - Badge/notification on the background tasks tab icon - - Toast notification - - Both + Only stream updates for the background task the user is currently viewing. This avoids unnecessary message traffic and keeps the implementation simple. - **Recommendation:** Badge on the tab icon (low disruption). Toast notifications can be added later if users miss errors. +6. **Error surfacing: Badge on the background tasks tab icon** - **Decision needed:** Is a badge sufficient, or do we need more prominent notification? + Display a badge on the tab icon when a background task encounters an error. Toast notifications can be added later if users miss errors. From 770a2209ead8e6a120423f8845499461c3d01ab8 Mon Sep 17 00:00:00 2001 From: Roo Code Date: Tue, 12 May 2026 13:22:27 +0000 Subject: [PATCH 4/4] feat: implement Phase 6a conversation replay for background tasks - Add requestBackgroundTaskMessages/backgroundTaskMessages message types - Add extension handler to load task messages from disk via readTaskMessages - Create BackgroundTaskReplayView component (read-only ChatRow wrapper) - Wire up bgTaskReplay tab in App.tsx with switchTab support - Add extension handler tests (3 tests) - Add component tests (7 tests) --- packages/types/src/vscode-extension-host.ts | 7 +- ...sageHandler.backgroundTaskMessages.spec.ts | 83 ++++++++++ src/core/webview/webviewMessageHandler.ts | 15 +- webview-ui/src/App.tsx | 17 +- .../chat/BackgroundTaskReplayView.tsx | 139 +++++++++++++++++ .../BackgroundTaskReplayView.spec.tsx | 147 ++++++++++++++++++ 6 files changed, 405 insertions(+), 3 deletions(-) create mode 100644 src/core/webview/__tests__/webviewMessageHandler.backgroundTaskMessages.spec.ts create mode 100644 webview-ui/src/components/chat/BackgroundTaskReplayView.tsx create mode 100644 webview-ui/src/components/chat/__tests__/BackgroundTaskReplayView.spec.tsx diff --git a/packages/types/src/vscode-extension-host.ts b/packages/types/src/vscode-extension-host.ts index a4ef802efb..0a0f738004 100644 --- a/packages/types/src/vscode-extension-host.ts +++ b/packages/types/src/vscode-extension-host.ts @@ -92,6 +92,7 @@ export interface ExtensionMessage { | "folderSelected" | "skills" | "fileContent" + | "backgroundTaskMessages" text?: string /** For fileContent: { path, content, error? } */ fileContent?: { path: string; content: string | null; error?: string } @@ -160,6 +161,8 @@ export interface ExtensionMessage { tools?: SerializedCustomToolDefinition[] // For customToolsResult skills?: SkillMetadata[] // For skills response modes?: { slug: string; name: string }[] // For modes response + backgroundTaskMessages?: ClineMessage[] // For backgroundTaskMessages: loaded messages for a background task replay + backgroundTaskId?: string // For backgroundTaskMessages: the task ID these messages belong to aggregatedCosts?: { // For taskWithAggregatedCosts response totalCost: number @@ -514,6 +517,8 @@ export interface WebviewMessage { | "createWorktreeInclude" | "checkoutBranch" | "browseForWorktreePath" + // Background task replay messages + | "requestBackgroundTaskMessages" // Skills messages | "requestSkills" | "createSkill" @@ -524,7 +529,7 @@ export interface WebviewMessage { text?: string taskId?: string editedMessageContent?: string - tab?: "settings" | "history" | "mcp" | "modes" | "chat" + tab?: "settings" | "history" | "mcp" | "modes" | "chat" | "bgTaskReplay" disabled?: boolean context?: string dataUri?: string diff --git a/src/core/webview/__tests__/webviewMessageHandler.backgroundTaskMessages.spec.ts b/src/core/webview/__tests__/webviewMessageHandler.backgroundTaskMessages.spec.ts new file mode 100644 index 0000000000..d9e68b79f6 --- /dev/null +++ b/src/core/webview/__tests__/webviewMessageHandler.backgroundTaskMessages.spec.ts @@ -0,0 +1,83 @@ +// npx vitest run core/webview/__tests__/webviewMessageHandler.backgroundTaskMessages.spec.ts + +import { webviewMessageHandler } from "../webviewMessageHandler" +import type { ClineProvider } from "../ClineProvider" + +vi.mock("../../task-persistence", () => ({ + saveTaskMessages: vi.fn(), + readTaskMessages: vi.fn(), +})) + +import { readTaskMessages } from "../../task-persistence" + +const mockPostMessageToWebview = vi.fn() + +const mockClineProvider = { + contextProxy: { + globalStorageUri: { fsPath: "/mock/global/storage" }, + getValue: vi.fn(), + setValue: vi.fn(), + }, + postMessageToWebview: mockPostMessageToWebview, + getStateToPostToWebview: vi.fn().mockResolvedValue({}), +} as unknown as ClineProvider + +describe("webviewMessageHandler - requestBackgroundTaskMessages", () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it("loads task messages from disk and posts them to the webview", async () => { + const mockMessages = [ + { ts: 1000, type: "say", say: "text", text: "Hello" }, + { ts: 2000, type: "say", say: "text", text: "World" }, + ] + vi.mocked(readTaskMessages).mockResolvedValue(mockMessages as any) + + await webviewMessageHandler(mockClineProvider, { + type: "requestBackgroundTaskMessages", + text: "task-123", + }) + + expect(readTaskMessages).toHaveBeenCalledWith({ + taskId: "task-123", + globalStoragePath: "/mock/global/storage", + }) + + expect(mockPostMessageToWebview).toHaveBeenCalledWith({ + type: "backgroundTaskMessages", + backgroundTaskId: "task-123", + backgroundTaskMessages: mockMessages, + }) + }) + + it("returns empty array when task has no messages", async () => { + vi.mocked(readTaskMessages).mockResolvedValue([]) + + await webviewMessageHandler(mockClineProvider, { + type: "requestBackgroundTaskMessages", + text: "task-empty", + }) + + expect(readTaskMessages).toHaveBeenCalledWith({ + taskId: "task-empty", + globalStoragePath: "/mock/global/storage", + }) + + expect(mockPostMessageToWebview).toHaveBeenCalledWith({ + type: "backgroundTaskMessages", + backgroundTaskId: "task-empty", + backgroundTaskMessages: [], + }) + }) + + it("does nothing when taskId is not provided", async () => { + await webviewMessageHandler(mockClineProvider, { + type: "requestBackgroundTaskMessages", + // no text/taskId provided + }) + + expect(readTaskMessages).not.toHaveBeenCalled() + expect(mockPostMessageToWebview).not.toHaveBeenCalled() + }) +}) diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index fac7ed10d5..999cb005cb 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -22,7 +22,7 @@ import { import { customToolRegistry } from "@roo-code/core" import { type ApiMessage } from "../task-persistence/apiMessages" -import { saveTaskMessages } from "../task-persistence" +import { saveTaskMessages, readTaskMessages } from "../task-persistence" import { ClineProvider } from "./ClineProvider" import { handleCheckpointRestoreOperation } from "./checkpointRestoreHandler" @@ -767,6 +767,19 @@ export const webviewMessageHandler = async (provider: ClineProvider, message: We case "showTaskWithId": provider.showTaskWithId(message.text!) break + case "requestBackgroundTaskMessages": { + const taskId = message.text + if (taskId) { + const globalStoragePath = provider.contextProxy.globalStorageUri.fsPath + const messages = await readTaskMessages({ taskId, globalStoragePath }) + await provider.postMessageToWebview({ + type: "backgroundTaskMessages", + backgroundTaskId: taskId, + backgroundTaskMessages: messages, + }) + } + break + } case "condenseTaskContextRequest": provider.condenseTaskContext(message.text!) break diff --git a/webview-ui/src/App.tsx b/webview-ui/src/App.tsx index 99847dc8eb..94735a5516 100644 --- a/webview-ui/src/App.tsx +++ b/webview-ui/src/App.tsx @@ -12,6 +12,7 @@ import ChatView, { ChatViewRef } from "./components/chat/ChatView" import HistoryView from "./components/history/HistoryView" import SettingsView, { SettingsViewRef } from "./components/settings/SettingsView" import WelcomeView from "./components/welcome/WelcomeViewProvider" +import BackgroundTaskReplayView from "./components/chat/BackgroundTaskReplayView" import { CheckpointRestoreDialog } from "./components/chat/CheckpointRestoreDialog" import { DeleteMessageDialog, EditMessageDialog } from "./components/chat/MessageModificationConfirmationDialog" import ErrorBoundary from "./components/ErrorBoundary" @@ -19,7 +20,7 @@ import { useAddNonInteractiveClickListener } from "./components/ui/hooks/useNonI import { TooltipProvider } from "./components/ui/tooltip" import { STANDARD_TOOLTIP_DELAY } from "./components/ui/standard-tooltip" -type Tab = "settings" | "history" | "chat" +type Tab = "settings" | "history" | "chat" | "bgTaskReplay" interface DeleteMessageDialogState { isOpen: boolean @@ -50,6 +51,7 @@ const App = () => { const [showAnnouncement, setShowAnnouncement] = useState(false) const [tab, setTab] = useState("chat") + const [replayTaskId, setReplayTaskId] = useState(null) const [deleteMessageDialogState, setDeleteMessageDialogState] = useState({ isOpen: false, @@ -88,6 +90,10 @@ const App = () => { // Handle switchTab action with tab parameter if (message.action === "switchTab" && message.tab) { const targetTab = message.tab as Tab + // If switching to bgTaskReplay, extract taskId from values + if (targetTab === "bgTaskReplay" && message.values?.taskId) { + setReplayTaskId(message.values.taskId as string) + } switchTab(targetTab) // Extract targetSection from values if provided const targetSection = message.values?.section as string | undefined @@ -174,6 +180,15 @@ const App = () => { ) : ( <> + {tab === "bgTaskReplay" && replayTaskId && ( + { + setReplayTaskId(null) + switchTab("chat") + }} + /> + )} {tab === "history" && switchTab("chat")} />} {tab === "settings" && ( setTab("chat")} targetSection={currentSection} /> diff --git a/webview-ui/src/components/chat/BackgroundTaskReplayView.tsx b/webview-ui/src/components/chat/BackgroundTaskReplayView.tsx new file mode 100644 index 0000000000..625956bf53 --- /dev/null +++ b/webview-ui/src/components/chat/BackgroundTaskReplayView.tsx @@ -0,0 +1,139 @@ +import { memo, useCallback, useEffect, useRef, useState } from "react" +import { useEvent } from "react-use" +import { ArrowLeft, Loader2 } from "lucide-react" + +import type { ClineMessage, ExtensionMessage } from "@roo-code/types" + +import { vscode } from "@src/utils/vscode" + +import ChatRow from "./ChatRow" + +export interface BackgroundTaskReplayViewProps { + taskId: string + onClose: () => void +} + +/** + * A read-only view that displays the full message history of a background task. + * This is a thin wrapper around ChatRow components -- it loads messages from disk + * via the extension and renders them without any input controls or approval buttons. + */ +const BackgroundTaskReplayView = memo(({ taskId, onClose }: BackgroundTaskReplayViewProps) => { + const [messages, setMessages] = useState([]) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + const [expandedMessages, setExpandedMessages] = useState>(new Set()) + const scrollContainerRef = useRef(null) + + // Request messages from the extension on mount + useEffect(() => { + setLoading(true) + setError(null) + vscode.postMessage({ type: "requestBackgroundTaskMessages", text: taskId }) + }, [taskId]) + + // Listen for the response + const handleMessage = useCallback( + (event: MessageEvent) => { + const message: ExtensionMessage = event.data + if (message.type === "backgroundTaskMessages" && message.backgroundTaskId === taskId) { + setMessages(message.backgroundTaskMessages ?? []) + setLoading(false) + } + }, + [taskId], + ) + + useEvent("message", handleMessage) + + const handleToggleExpand = useCallback((ts: number) => { + setExpandedMessages((prev) => { + const next = new Set(prev) + if (next.has(ts)) { + next.delete(ts) + } else { + next.add(ts) + } + return next + }) + }, []) + + if (loading) { + return ( +
+ +

Loading task messages...

+
+ ) + } + + if (error) { + return ( +
+

{error}

+ +
+ ) + } + + return ( +
+ {/* Header bar */} +
+ + + Task replay (read-only) · {messages.length} messages + +
+ + {/* Message list */} +
+ {messages.length === 0 ? ( +
+

+ No messages found for this task. +

+
+ ) : ( + messages.map((msg, index) => ( + {}} + /> + )) + )} +
+
+ ) +}) + +BackgroundTaskReplayView.displayName = "BackgroundTaskReplayView" + +export default BackgroundTaskReplayView diff --git a/webview-ui/src/components/chat/__tests__/BackgroundTaskReplayView.spec.tsx b/webview-ui/src/components/chat/__tests__/BackgroundTaskReplayView.spec.tsx new file mode 100644 index 0000000000..0e9b2a19f4 --- /dev/null +++ b/webview-ui/src/components/chat/__tests__/BackgroundTaskReplayView.spec.tsx @@ -0,0 +1,147 @@ +// pnpm --filter @roo-code/vscode-webview test src/components/chat/__tests__/BackgroundTaskReplayView.spec.tsx + +import React from "react" +import { render, screen, act, waitFor } from "@/utils/test-utils" + +import { vscode } from "@src/utils/vscode" + +import BackgroundTaskReplayView from "../BackgroundTaskReplayView" + +// Mock vscode API +vi.mock("@src/utils/vscode", () => ({ + vscode: { + postMessage: vi.fn(), + }, +})) + +// Mock ChatRow to avoid pulling in heavy dependencies +vi.mock("../ChatRow", () => ({ + default: function MockChatRow({ message }: { message: { ts: number; text?: string } }) { + return
{message.text ?? "message"}
+ }, +})) + +// Mock use-sound +vi.mock("use-sound", () => ({ + default: vi.fn().mockImplementation(() => [vi.fn()]), +})) + +// Mock ExtensionStateContext +vi.mock("@src/context/ExtensionStateContext", () => ({ + useExtensionState: vi.fn().mockReturnValue({ + clineMessages: [], + mcpServers: [], + mode: "code", + apiConfiguration: {}, + currentTaskItem: null, + }), + ExtensionStateContextProvider: ({ children }: { children: React.ReactNode }) => <>{children}, +})) + +function simulateBackgroundTaskMessages(taskId: string, messages: any[]) { + const event = new MessageEvent("message", { + data: { + type: "backgroundTaskMessages", + backgroundTaskId: taskId, + backgroundTaskMessages: messages, + }, + }) + window.dispatchEvent(event) +} + +describe("BackgroundTaskReplayView", () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it("requests messages on mount", () => { + render() + + expect(vscode.postMessage).toHaveBeenCalledWith({ + type: "requestBackgroundTaskMessages", + text: "task-123", + }) + }) + + it("shows loading state initially", () => { + render() + + expect(screen.getByTestId("replay-loading")).toBeTruthy() + }) + + it("renders messages when received from extension", async () => { + render() + + act(() => { + simulateBackgroundTaskMessages("task-123", [ + { ts: 1000, type: "say", say: "text", text: "Hello" }, + { ts: 2000, type: "say", say: "text", text: "World" }, + ]) + }) + + await waitFor(() => { + const rows = screen.getAllByTestId("chat-row") + expect(rows).toHaveLength(2) + }) + }) + + it("shows empty state when task has no messages", async () => { + render() + + act(() => { + simulateBackgroundTaskMessages("task-empty", []) + }) + + await waitFor(() => { + expect(screen.getByTestId("replay-empty-state")).toBeTruthy() + }) + }) + + it("calls onClose when back button is clicked", async () => { + const onClose = vi.fn() + render() + + act(() => { + simulateBackgroundTaskMessages("task-123", [{ ts: 1000, type: "say", say: "text", text: "Hello" }]) + }) + + await waitFor(() => { + expect(screen.getByTestId("replay-back-button")).toBeTruthy() + }) + + act(() => { + screen.getByTestId("replay-back-button").click() + }) + + expect(onClose).toHaveBeenCalled() + }) + + it("ignores messages for a different task ID", async () => { + render() + + act(() => { + simulateBackgroundTaskMessages("task-different", [ + { ts: 1000, type: "say", say: "text", text: "Wrong task" }, + ]) + }) + + // Should still show loading since the task ID didn't match + expect(screen.getByTestId("replay-loading")).toBeTruthy() + }) + + it("shows message count in header after loading", async () => { + render() + + act(() => { + simulateBackgroundTaskMessages("task-123", [ + { ts: 1000, type: "say", say: "text", text: "Msg 1" }, + { ts: 2000, type: "say", say: "text", text: "Msg 2" }, + { ts: 3000, type: "say", say: "text", text: "Msg 3" }, + ]) + }) + + await waitFor(() => { + expect(screen.getByText(/3 messages/)).toBeTruthy() + }) + }) +})