From 01c8bf20f94f80fb63f15a4856b76cde60f9bcd5 Mon Sep 17 00:00:00 2001 From: Luke Parker <10430890+Hona@users.noreply.github.com> Date: Mon, 17 Aug 2026 09:20:51 +1000 Subject: [PATCH] fix(app): correct background subagent status (#42944) --- .../session-timeline-notices.spec.ts | 34 ++++++++++++++++--- .../src/components/message-part.tsx | 14 ++++++-- 2 files changed, 40 insertions(+), 8 deletions(-) diff --git a/packages/app/e2e/regression/session-timeline-notices.spec.ts b/packages/app/e2e/regression/session-timeline-notices.spec.ts index 7ba29a4d8b..23b55eb7b7 100644 --- a/packages/app/e2e/regression/session-timeline-notices.spec.ts +++ b/packages/app/e2e/regression/session-timeline-notices.spec.ts @@ -1,10 +1,15 @@ import { expect, test } from "@playwright/test" import type { SessionMessageAssistant, SessionMessageInfo } from "@opencode-ai/client/promise" -import { session, sessionID, setupTimeline } from "../performance/timeline-stability/fixture" +import { event, session, sessionID, setupTimeline } from "../performance/timeline-stability/fixture" const user = { id: "msg_user", type: "user", text: "Run it", time: { created: 1 } } satisfies SessionMessageInfo -const assistant = (completed: boolean, tool = false, childID?: string): SessionMessageAssistant => ({ +const assistant = ( + completed: boolean, + tool = false, + childID?: string, + background = false, +): SessionMessageAssistant => ({ id: "msg_assistant", type: "assistant", agent: "build", @@ -15,7 +20,11 @@ const assistant = (completed: boolean, tool = false, childID?: string): SessionM type: "tool", id: "call_subagent", name: "subagent", - state: { status: "running", input: {}, metadata: childID ? { sessionID: childID } : {} }, + state: { + status: "running", + input: { description: "Inspect code", ...(background ? { background: true } : {}) }, + metadata: { status: "running", ...(childID ? { sessionID: childID } : {}) }, + }, time: { created: 2 }, }, ] @@ -66,7 +75,10 @@ test("renders current protocol notices in CLI order", async ({ page }) => { test("moves blocking work to the background with Ctrl+B", async ({ page }) => { await setupTimeline(page, { currentMessages: [user, assistant(false, true)] }) - await expect(page.locator('[data-component="task-tool-card"]')).toBeVisible() + const card = page.locator('[data-component="task-tool-card"]') + await expect(card).toBeVisible() + await expect(card).toContainText("Inspect code") + await expect(card).not.toContainText("(background)") await expect(page.getByText("Called `subagent`", { exact: false })).toHaveCount(0) await expect(page.locator('[data-component="background-tool-control"]')).toHaveCount(0) await expect(page.locator('[data-action="session-background-toggle"]')).toContainText("Move 1 subagent to background") @@ -79,6 +91,11 @@ test("moves blocking work to the background with Ctrl+B", async ({ page }) => { await request }) +test("waits for completion before labeling requested background work", async ({ page }) => { + await setupTimeline(page, { currentMessages: [user, assistant(false, true, undefined, true)] }) + await expect(page.locator('[data-component="task-tool-card"]')).not.toContainText("(background)") +}) + test("navigates from a running subagent card and hides background controls in the child", async ({ page }) => { const childID = "ses_running_child" await setupTimeline(page, { @@ -107,7 +124,7 @@ test("shows a badge for active background work", async ({ page }) => { test("separates blocking and already-backgrounded work into two rows", async ({ page }) => { const backgroundID = "ses_background_existing" const blockingID = "ses_background_blocking" - await setupTimeline(page, { + const timeline = await setupTimeline(page, { currentMessages: [ user, { @@ -177,9 +194,16 @@ test("separates blocking and already-backgrounded work into two rows", async ({ }) const dock = page.locator('[data-component="session-background-dock"]') + const backgroundCard = page.locator('[data-timeline-part-id="call_backgrounded"]') await expect(dock).toContainText("Move 1 subagent to background") await expect(dock.getByText("Running 1 shell and 1 subagent in background", { exact: true })).toBeVisible() + await expect(backgroundCard).toContainText("Background task (background)") + await expect(backgroundCard.locator('[data-component="session-progress-indicator-v2"]')).toBeVisible() await expect( page.locator('[data-timeline-part-id="call_shell_backgrounded"] [data-component="text-shimmer"]'), ).toHaveAttribute("data-active", "true") + + await timeline.send(event("session.status", { sessionID: backgroundID, status: { type: "idle" } })) + await expect(backgroundCard.locator('[data-component="session-progress-indicator-v2"]')).toHaveCount(0) + await expect(backgroundCard).toContainText("Background task (background)") }) diff --git a/packages/session-ui/src/components/message-part.tsx b/packages/session-ui/src/components/message-part.tsx index 502d640b69..657f417bda 100644 --- a/packages/session-ui/src/components/message-part.tsx +++ b/packages/session-ui/src/components/message-part.tsx @@ -2000,17 +2000,25 @@ ToolRegistry.register({ const title = createMemo(() => agent().name ?? i18n.t("ui.tool.agent.default")) const tone = createMemo(() => agent().color) const v2Tone = createMemo(() => agent().v2Color) + const background = createMemo(() => { + if (props.tool === "task") return props.metadata.background === true + return props.status === "completed" && props.metadata.status === "running" + }) const subtitle = createMemo(() => { const value = typeof props.input.description === "string" && props.input.description ? props.input.description : childSessionId() if (!value) return value - if (props.input.background === true || props.metadata.background === true || props.metadata.status === "running") - return `${value} (background)` + if (background()) return `${value} (background)` return value }) - const running = createMemo(() => props.status === "pending" || props.status === "running") + const running = createMemo(() => { + if (props.status === "pending" || props.status === "running") return true + const id = childSessionId() + if (!id) return false + return (data.store.session_status[id]?.type ?? "idle") !== "idle" + }) const href = createMemo(() => sessionLink(childSessionId(), data.sessionHref)) const clickable = createMemo(() => !!(childSessionId() && (data.navigateToSession || href())))