Skip to content
This repository was archived by the owner on May 15, 2026. It is now read-only.

Commit 15f4be4

Browse files
committed
feat: Phase 5 - Background Tasks Panel UI for parallel task visibility
Adds a collapsible Background Tasks Panel to the chat sidebar that shows active and recently completed background tasks. This builds on the Phase 4 BackgroundTaskRunner to give users visibility into background work. Key changes: - BackgroundTaskStatusInfo type for exposing task status to the webview - BackgroundTaskRunner tracks completed tasks with result summaries - BackgroundTaskRunner.getTasksStatus() returns combined active + completed - BackgroundTaskRunner.onStateChanged callback for UI refresh - backgroundTasks field added to ExtensionState and getStateToPostToWebview - cancelBackgroundTask webview message handler - postBackgroundTasksToWebview() for lightweight status-only updates - BackgroundTasksPanel React component with collapsible panel, cancel buttons, active count badge, and result summaries - 31 backend tests (BackgroundTaskRunner) + 7 UI tests (panel component)
1 parent 4b2b91f commit 15f4be4

8 files changed

Lines changed: 646 additions & 11 deletions

File tree

packages/types/src/vscode-extension-host.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,9 @@ export type ExtensionState = Pick<
337337
openAiCodexIsAuthenticated?: boolean
338338
debug?: boolean
339339

340+
/** Background tasks status for the UI panel */
341+
backgroundTasks?: BackgroundTaskStatusInfo[]
342+
340343
/**
341344
* Monotonically increasing sequence number for clineMessages state pushes.
342345
* When present, the frontend should only apply clineMessages from a state push
@@ -346,6 +349,21 @@ export type ExtensionState = Pick<
346349
clineMessagesSeq?: number
347350
}
348351

352+
/**
353+
* Status of a background task as exposed to the webview UI.
354+
*/
355+
export interface BackgroundTaskStatusInfo {
356+
taskId: string
357+
parentTaskId: string
358+
status: "running" | "completed" | "cancelled" | "timed_out" | "error"
359+
startedAt: number
360+
completedAt?: number
361+
/** Short summary of the result (from attempt_completion) */
362+
resultSummary?: string
363+
/** The mode slug the background task was running in */
364+
mode?: string
365+
}
366+
349367
export interface Command {
350368
name: string
351369
source: "global" | "project" | "built-in"
@@ -514,6 +532,8 @@ export interface WebviewMessage {
514532
| "createWorktreeInclude"
515533
| "checkoutBranch"
516534
| "browseForWorktreePath"
535+
// Background task messages
536+
| "cancelBackgroundTask"
517537
// Skills messages
518538
| "requestSkills"
519539
| "createSkill"

src/core/task/BackgroundTaskRunner.ts

Lines changed: 135 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
* This is Phase 4 of the parallel execution roadmap: Background Read-Only Concurrency.
1111
*/
1212

13+
import { BackgroundTaskStatusInfo } from "@roo-code/types"
14+
1315
import { Task, TaskOptions } from "./Task"
1416

1517
/** Read-only tools that background tasks are allowed to use. */
@@ -46,11 +48,27 @@ export interface BackgroundTaskRunnerCallbacks {
4648
onTaskError?: (taskId: string, parentTaskId: string, error: Error) => void
4749
}
4850

51+
/** Maximum number of recently completed tasks to keep for UI display. */
52+
const MAX_COMPLETED_TASKS = 10
53+
54+
export interface CompletedBackgroundTaskInfo {
55+
taskId: string
56+
parentTaskId: string
57+
status: "completed" | "cancelled" | "timed_out" | "error"
58+
startedAt: number
59+
completedAt: number
60+
resultSummary?: string
61+
mode?: string
62+
}
63+
4964
export class BackgroundTaskRunner {
5065
private backgroundTasks: Map<string, BackgroundTaskInfo> = new Map()
66+
private completedTasks: CompletedBackgroundTaskInfo[] = []
5167
private maxConcurrentTasks: number
5268
private taskTimeoutMs: number
5369
private callbacks: BackgroundTaskRunnerCallbacks
70+
/** Called whenever the set of active/completed tasks changes, so the UI can be refreshed. */
71+
public onStateChanged?: () => void
5472

5573
constructor(
5674
maxConcurrentTasks: number = DEFAULT_MAX_BACKGROUND_TASKS,
@@ -108,12 +126,14 @@ export class BackgroundTaskRunner {
108126
`[BackgroundTaskRunner] Registered background task ${task.taskId} ` +
109127
`(parent: ${parentTaskId}, active: ${this.backgroundTasks.size}/${this.maxConcurrentTasks})`,
110128
)
129+
130+
this.notifyStateChanged()
111131
}
112132

113133
/**
114134
* Called when a background task completes. Cleans up tracking state.
115135
*/
116-
onTaskCompleted(taskId: string): BackgroundTaskInfo | undefined {
136+
onTaskCompleted(taskId: string, resultSummary?: string): BackgroundTaskInfo | undefined {
117137
const info = this.backgroundTasks.get(taskId)
118138

119139
if (!info) {
@@ -123,11 +143,22 @@ export class BackgroundTaskRunner {
123143
clearTimeout(info.timeoutHandle)
124144
this.backgroundTasks.delete(taskId)
125145

146+
this.addCompletedTask({
147+
taskId,
148+
parentTaskId: info.parentTaskId,
149+
status: "completed",
150+
startedAt: info.startedAt,
151+
completedAt: Date.now(),
152+
resultSummary,
153+
})
154+
126155
console.log(
127156
`[BackgroundTaskRunner] Background task ${taskId} completed ` +
128157
`(active: ${this.backgroundTasks.size}/${this.maxConcurrentTasks})`,
129158
)
130159

160+
this.notifyStateChanged()
161+
131162
return info
132163
}
133164

@@ -174,9 +205,12 @@ export class BackgroundTaskRunner {
174205

175206
clearTimeout(info.timeoutHandle)
176207

208+
let status: CompletedBackgroundTaskInfo["status"] = "cancelled"
209+
177210
try {
178211
await info.task.abortTask(true)
179212
} catch (error) {
213+
status = "error"
180214
const err = error instanceof Error ? error : new Error(String(error))
181215
console.error(`[BackgroundTaskRunner] Error aborting background task ${taskId}: ${err.message}`)
182216
try {
@@ -188,10 +222,20 @@ export class BackgroundTaskRunner {
188222

189223
this.backgroundTasks.delete(taskId)
190224

225+
this.addCompletedTask({
226+
taskId,
227+
parentTaskId: info.parentTaskId,
228+
status,
229+
startedAt: info.startedAt,
230+
completedAt: Date.now(),
231+
})
232+
191233
console.log(
192234
`[BackgroundTaskRunner] Cancelled background task ${taskId} ` +
193235
`(active: ${this.backgroundTasks.size}/${this.maxConcurrentTasks})`,
194236
)
237+
238+
this.notifyStateChanged()
195239
}
196240

197241
/**
@@ -205,12 +249,79 @@ export class BackgroundTaskRunner {
205249
}
206250
}
207251

252+
/**
253+
* Returns the combined status of all active and recently completed background tasks
254+
* for display in the webview UI.
255+
*/
256+
getTasksStatus(): BackgroundTaskStatusInfo[] {
257+
const activeTasks: BackgroundTaskStatusInfo[] = []
258+
259+
for (const [taskId, info] of this.backgroundTasks) {
260+
activeTasks.push({
261+
taskId,
262+
parentTaskId: info.parentTaskId,
263+
status: "running",
264+
startedAt: info.startedAt,
265+
})
266+
}
267+
268+
const completedStatuses: BackgroundTaskStatusInfo[] = this.completedTasks.map((ct) => ({
269+
taskId: ct.taskId,
270+
parentTaskId: ct.parentTaskId,
271+
status: ct.status,
272+
startedAt: ct.startedAt,
273+
completedAt: ct.completedAt,
274+
resultSummary: ct.resultSummary,
275+
mode: ct.mode,
276+
}))
277+
278+
return [...activeTasks, ...completedStatuses]
279+
}
280+
281+
/**
282+
* Returns the list of recently completed tasks (for testing and direct access).
283+
*/
284+
getCompletedTasks(): readonly CompletedBackgroundTaskInfo[] {
285+
return this.completedTasks
286+
}
287+
288+
/**
289+
* Clears completed tasks from the buffer.
290+
*/
291+
clearCompletedTasks(): void {
292+
this.completedTasks = []
293+
this.notifyStateChanged()
294+
}
295+
296+
/**
297+
* Add a completed task to the buffer, evicting the oldest if at capacity.
298+
*/
299+
private addCompletedTask(info: CompletedBackgroundTaskInfo): void {
300+
this.completedTasks.push(info)
301+
302+
if (this.completedTasks.length > MAX_COMPLETED_TASKS) {
303+
this.completedTasks = this.completedTasks.slice(-MAX_COMPLETED_TASKS)
304+
}
305+
}
306+
307+
/**
308+
* Notify the owner that background task state has changed.
309+
*/
310+
private notifyStateChanged(): void {
311+
try {
312+
this.onStateChanged?.()
313+
} catch {
314+
// Callback errors must not break internal logic.
315+
}
316+
}
317+
208318
/**
209319
* Handle timeout of a background task.
210320
*/
211321
private async timeoutTask(taskId: string): Promise<void> {
212322
const info = this.backgroundTasks.get(taskId)
213323
const parentTaskId = info?.parentTaskId ?? "unknown"
324+
const startedAt = info?.startedAt ?? Date.now()
214325

215326
console.warn(`[BackgroundTaskRunner] Background task ${taskId} timed out after ${this.taskTimeoutMs}ms`)
216327

@@ -220,6 +331,28 @@ export class BackgroundTaskRunner {
220331
// Callback errors must not break cleanup.
221332
}
222333

223-
await this.cancelTask(taskId)
334+
// Record as timed_out before cancelling (cancelTask will record as cancelled otherwise)
335+
clearTimeout(info?.timeoutHandle)
336+
if (info) {
337+
try {
338+
await info.task.abortTask(true)
339+
} catch (error) {
340+
const err = error instanceof Error ? error : new Error(String(error))
341+
console.error(`[BackgroundTaskRunner] Error aborting timed-out task ${taskId}: ${err.message}`)
342+
}
343+
this.backgroundTasks.delete(taskId)
344+
345+
this.addCompletedTask({
346+
taskId,
347+
parentTaskId,
348+
status: "timed_out",
349+
startedAt,
350+
completedAt: Date.now(),
351+
})
352+
353+
this.notifyStateChanged()
354+
} else {
355+
await this.cancelTask(taskId)
356+
}
224357
}
225358
}

0 commit comments

Comments
 (0)