diff --git a/src/features/background-agent/background-task-notification-template.test.ts b/src/features/background-agent/background-task-notification-template.test.ts
new file mode 100644
index 000000000..5555e909a
--- /dev/null
+++ b/src/features/background-agent/background-task-notification-template.test.ts
@@ -0,0 +1,130 @@
+import { describe, expect, test } from "bun:test"
+import { buildBackgroundTaskNotificationText } from "./background-task-notification-template"
+
+describe("buildBackgroundTaskNotificationText", () => {
+ describe("#given one task still running after a completed task notification", () => {
+ test("#when building the partial notification #then it preserves the existing completed-task format", () => {
+ // given
+ const notification = buildBackgroundTaskNotificationText({
+ task: {
+ id: "task-1",
+ description: "Index repo",
+ status: "completed",
+ },
+ duration: "42s",
+ statusText: "COMPLETED",
+ allComplete: false,
+ remainingCount: 1,
+ completedTasks: [],
+ })
+
+ // when
+ const expectedNotification = `
+[BACKGROUND TASK COMPLETED]
+**ID:** \`task-1\`
+**Description:** Index repo
+**Duration:** 42s
+
+**1 task still in progress.** You WILL be notified when ALL complete.
+Do NOT poll - continue productive work.
+
+Use \`background_output(task_id="task-1")\` to retrieve this result when ready.
+`
+
+ // then
+ expect(notification).toBe(expectedNotification)
+ })
+ })
+
+ describe("#given one task still running after a failed task notification", () => {
+ test("#when building the partial notification #then it preserves the existing failure format", () => {
+ // given
+ const notification = buildBackgroundTaskNotificationText({
+ task: {
+ id: "task-2",
+ description: "Summarize logs",
+ status: "error",
+ error: "Timed out",
+ },
+ duration: "3m 4s",
+ statusText: "ERROR",
+ allComplete: false,
+ remainingCount: 2,
+ completedTasks: [],
+ })
+
+ // when
+ const expectedNotification = `
+[BACKGROUND TASK ERROR]
+**ID:** \`task-2\`
+**Description:** Summarize logs
+**Duration:** 3m 4s
+**Error:** Timed out
+
+**2 tasks still in progress.** You WILL be notified when ALL complete.
+**ACTION REQUIRED:** This task failed. Check the error and decide whether to retry, cancel remaining tasks, or continue.
+
+Use \`background_output(task_id="task-2")\` to retrieve this result when ready.
+`
+
+ // then
+ expect(notification).toBe(expectedNotification)
+ })
+ })
+
+ describe("#given all sibling tasks completed with mixed outcomes", () => {
+ test("#when building the final notification #then it preserves the existing summary format", () => {
+ // given
+ const notification = buildBackgroundTaskNotificationText({
+ task: {
+ id: "task-3",
+ description: "Fallback task",
+ status: "error",
+ error: "Denied",
+ },
+ duration: "10s",
+ statusText: "ERROR",
+ allComplete: true,
+ remainingCount: 0,
+ completedTasks: [
+ {
+ id: "task-1",
+ description: "Index repo",
+ status: "completed",
+ },
+ {
+ id: "task-2",
+ description: "Summarize logs",
+ status: "cancelled",
+ error: "User aborted",
+ },
+ {
+ id: "task-3",
+ description: "Fallback task",
+ status: "error",
+ error: "Denied",
+ },
+ ],
+ })
+
+ // when
+ const expectedNotification = `
+[ALL BACKGROUND TASKS FINISHED - 2 FAILED]
+
+**Completed:**
+- \`task-1\`: Index repo
+
+**Failed:**
+- \`task-2\`: Summarize logs [CANCELLED] - User aborted
+- \`task-3\`: Fallback task [ERROR] - Denied
+
+Use \`background_output(task_id="")\` to retrieve each result.
+
+**ACTION REQUIRED:** 2 task(s) failed. Check errors above and decide whether to retry or proceed.
+`
+
+ // then
+ expect(notification).toBe(expectedNotification)
+ })
+ })
+})
diff --git a/src/features/background-agent/background-task-notification-template.ts b/src/features/background-agent/background-task-notification-template.ts
index e2e74cc78..240efe9c0 100644
--- a/src/features/background-agent/background-task-notification-template.ts
+++ b/src/features/background-agent/background-task-notification-template.ts
@@ -1,14 +1,21 @@
-import type { BackgroundTask } from "./types"
+import type { BackgroundTaskStatus } from "./types"
export type BackgroundTaskNotificationStatus = "COMPLETED" | "CANCELLED" | "INTERRUPTED" | "ERROR"
+export interface BackgroundTaskNotificationTask {
+ id: string
+ description: string
+ status: BackgroundTaskStatus
+ error?: string
+}
+
export function buildBackgroundTaskNotificationText(input: {
- task: BackgroundTask
+ task: BackgroundTaskNotificationTask
duration: string
statusText: BackgroundTaskNotificationStatus
allComplete: boolean
remainingCount: number
- completedTasks: BackgroundTask[]
+ completedTasks: BackgroundTaskNotificationTask[]
}): string {
const { task, duration, statusText, allComplete, remainingCount, completedTasks } = input
@@ -50,14 +57,12 @@ Use \`background_output(task_id="")\` to retrieve each result.${hasFailures
`
}
- const agentInfo = task.category ? `${task.agent} (${task.category})` : task.agent
const isFailure = statusText !== "COMPLETED"
return `
[BACKGROUND TASK ${statusText}]
**ID:** \`${task.id}\`
**Description:** ${task.description}
-**Agent:** ${agentInfo}
**Duration:** ${duration}${errorInfo}
**${remainingCount} task${remainingCount === 1 ? "" : "s"} still in progress.** You WILL be notified when ALL complete.
diff --git a/src/features/background-agent/manager.ts b/src/features/background-agent/manager.ts
index efbed503f..621700030 100644
--- a/src/features/background-agent/manager.ts
+++ b/src/features/background-agent/manager.ts
@@ -34,6 +34,10 @@ import {
import { subagentSessions } from "../claude-code-session-state"
import { getTaskToastManager } from "../task-toast-manager"
import { formatDuration } from "./duration-formatter"
+import {
+ buildBackgroundTaskNotificationText,
+ type BackgroundTaskNotificationTask,
+} from "./background-task-notification-template"
import {
isAbortedSessionError,
extractErrorName,
@@ -151,7 +155,7 @@ export class BackgroundManager {
private queuesByKey: Map = new Map()
private processingKeys: Set = new Set()
private completionTimers: Map> = new Map()
- private completedTaskSummaries: Map> = new Map()
+ private completedTaskSummaries: Map = new Map()
private idleDeferralTimers: Map> = new Map()
private notificationQueueByParent: Map> = new Map()
private rootDescendantCounts: Map
@@ -1632,56 +1636,14 @@ export class BackgroundManager {
: task.status === "error"
? "ERROR"
: "CANCELLED"
- const errorInfo = task.error ? `\n**Error:** ${task.error}` : ""
-
- let notification: string
- if (allComplete) {
- const succeededTasks = completedTasks.filter(t => t.status === "completed")
- const failedTasks = completedTasks.filter(t => t.status !== "completed")
-
- const succeededText = succeededTasks.length > 0
- ? succeededTasks.map(t => `- \`${t.id}\`: ${t.description}`).join("\n")
- : ""
- const failedText = failedTasks.length > 0
- ? failedTasks.map(t => `- \`${t.id}\`: ${t.description} [${t.status.toUpperCase()}]${t.error ? ` - ${t.error}` : ""}`).join("\n")
- : ""
-
- const hasFailures = failedTasks.length > 0
- const header = hasFailures
- ? `[ALL BACKGROUND TASKS FINISHED - ${failedTasks.length} FAILED]`
- : "[ALL BACKGROUND TASKS COMPLETE]"
-
- let body = ""
- if (succeededText) {
- body += `**Completed:**\n${succeededText}\n`
- }
- if (failedText) {
- body += `\n**Failed:**\n${failedText}\n`
- }
- if (!body) {
- body = `- \`${task.id}\`: ${task.description} [${task.status.toUpperCase()}]${task.error ? ` - ${task.error}` : ""}\n`
- }
-
- notification = `
-${header}
-
-${body.trim()}
-
-Use \`background_output(task_id="")\` to retrieve each result.${hasFailures ? `\n\n**ACTION REQUIRED:** ${failedTasks.length} task(s) failed. Check errors above and decide whether to retry or proceed.` : ""}
-`
- } else {
- notification = `
-[BACKGROUND TASK ${statusText}]
-**ID:** \`${task.id}\`
-**Description:** ${task.description}
-**Duration:** ${duration}${errorInfo}
-
-**${remainingCount} task${remainingCount === 1 ? "" : "s"} still in progress.** You WILL be notified when ALL complete.
-${statusText === "COMPLETED" ? "Do NOT poll - continue productive work." : "**ACTION REQUIRED:** This task failed. Check the error and decide whether to retry, cancel remaining tasks, or continue."}
-
-Use \`background_output(task_id="${task.id}")\` to retrieve this result when ready.
-`
- }
+ const notification = buildBackgroundTaskNotificationText({
+ task,
+ duration,
+ statusText,
+ allComplete,
+ remainingCount,
+ completedTasks,
+ })
let agent: string | undefined = task.parentAgent
let model: { providerID: string; modelID: string } | undefined