From ad352c11e77403a5cbe61595d4fc39a91413a288 Mon Sep 17 00:00:00 2001 From: Badcuban <108198679+badcuban@users.noreply.github.com> Date: Sun, 16 Aug 2026 20:59:15 -0400 Subject: [PATCH 1/2] Keep background commands out of the Agents tab entirely The tab briefly listed non-agent background commands in their own section below the agents, but the header's activity chip already gives every background task a richer surface - its command, terminal toggle and stop button - so the section was a duplicate with a worse address. The Agents tab now holds only agents: the run-branch machinery (AgentRunBranch, runBranch, agentInitiatedRuns, the provenance tag helpers) is deleted, the panel and its store stop carrying backgroundRuns and the terminal toggle, and the launcher row reports "No agents yet" instead of counting commands. Promoted codex exec runs are unaffected: they are agents and keep their rows and stop handles. --- apps/web/src/agentsPanelStore.ts | 11 +- apps/web/src/components/ChatView.tsx | 4 - .../components/chat/AgentsPanel.browser.tsx | 80 +----------- apps/web/src/components/chat/AgentsPanel.tsx | 72 ++--------- .../components/chat/agentsPanel.logic.test.ts | 102 ++------------- .../src/components/chat/agentsPanel.logic.ts | 120 ++---------------- .../chat/rightPanelLauncherState.test.ts | 17 +-- .../chat/rightPanelLauncherState.ts | 9 +- .../routes/_chat.$environmentId.$threadId.tsx | 6 - 9 files changed, 57 insertions(+), 364 deletions(-) diff --git a/apps/web/src/agentsPanelStore.ts b/apps/web/src/agentsPanelStore.ts index ef292984..e57cf8a4 100644 --- a/apps/web/src/agentsPanelStore.ts +++ b/apps/web/src/agentsPanelStore.ts @@ -2,10 +2,11 @@ * Bridge between the chat view, which knows what the current turn is doing, * and the route, which owns the right-panel slot the agents panel renders in. * - * The panel needs live subagent progress, background runs and the terminal - * toggle — all of which are chat-view state — but it mounts as a sibling of - * the chat column, next to source control. ChatView publishes here; the route - * reads. Same shape as the file viewer's store, for the same reason. + * The panel needs live subagent progress and the agents' stop handles — all + * chat-view state — but it mounts as a sibling of the chat column, next to + * source control. ChatView publishes here; the route reads. Same shape as the + * file viewer's store, for the same reason. Background command runs are not + * part of this source: the header's activity chip is their surface. */ import { create } from "zustand"; @@ -22,7 +23,6 @@ export interface AgentsPanelSource { environmentId: EnvironmentId; threadId: ThreadId; subagents: ReadonlyArray; - backgroundRuns: ReadonlyArray; /** Runs the panel lists as subagents rather than as runs, keyed by the tool * call that launched them. They carry the stop handle those agent rows use. */ subagentRuns: ReadonlyMap; @@ -46,7 +46,6 @@ export interface AgentsPanelSource { * unhydrated publish as observed idleness. */ hydrated: boolean; threadCwd: string | null; - onToggleBackgroundRunTerminal: (terminalId: string) => void; onStopBackgroundRun: (run: ThreadBackgroundRunItem) => void; } diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index ea108696..23802b0f 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -3367,7 +3367,6 @@ export default function ChatView(props: ChatViewProps) { environmentId, threadId: activeThreadId, subagents: subagentProgress?.items ?? EMPTY_SUBAGENT_ITEMS, - backgroundRuns, subagentRuns: promotedSubagentRuns, history: subagentHistory, workEntries: workLogEntries, @@ -3375,14 +3374,12 @@ export default function ChatView(props: ChatViewProps) { turnInFlight: activeTurnInProgress, hydrated: threadDetailHydrated, threadCwd: gitCwd, - onToggleBackgroundRunTerminal: toggleBackgroundRunTerminal, onStopBackgroundRun: stopBackgroundRun, }); }, [ activeProviderDriver, activeThreadId, activeTurnInProgress, - backgroundRuns, environmentId, gitCwd, promotedSubagentRuns, @@ -3390,7 +3387,6 @@ export default function ChatView(props: ChatViewProps) { subagentHistory, subagentProgress?.items, threadDetailHydrated, - toggleBackgroundRunTerminal, workLogEntries, ]); useEffect(() => () => publishAgentsPanelSource(null), []); diff --git a/apps/web/src/components/chat/AgentsPanel.browser.tsx b/apps/web/src/components/chat/AgentsPanel.browser.tsx index ae3c7526..9a10c473 100644 --- a/apps/web/src/components/chat/AgentsPanel.browser.tsx +++ b/apps/web/src/components/chat/AgentsPanel.browser.tsx @@ -83,19 +83,14 @@ const TERMINAL_RUN: ThreadBackgroundRunItem = { command: "vp run dev:desktop", }; -function renderPanel( - props: Partial[0]> = {}, - onToggleBackgroundRunTerminal = vi.fn(), -) { +function renderPanel(props: Partial[0]> = {}) { return render(
@@ -144,69 +139,20 @@ describe("AgentsPanel", () => { statusLabel: "Done", }), ], - // The user's own terminal rides along and must not appear: only the - // provider's run draws a branch. - backgroundRuns: [PROVIDER_RUN, TERMINAL_RUN], }); try { await expect.element(page.getByText("Router sweep")).toBeVisible(); const branches = [...document.querySelectorAll("[data-agent-branch='true']")]; - // Agents in attention order first; the provider's command run draws its - // branch in the Commands section after them. + // Agents in attention order; background command runs never draw a + // branch here — the header's activity chip is their surface. expect(branches.map((branch) => branch.getAttribute("data-agent-branch-status"))).toEqual([ "running", "waiting", "failed", "completed", - "running", ]); - expect(branches.map((branch) => branch.getAttribute("data-agent-branch-kind"))).toContain( - "run", - ); - - // A run is transcript-less, so it says where it came from instead. - const tags = [...document.querySelectorAll("[data-agent-branch-tag='true']")]; - expect(tags.map((tag) => tag.textContent)).toEqual(["codex · provider"]); - } finally { - await mounted.unmount(); - } - }); - - it("tags a detected run with the provider that reported it", async () => { - const mounted = await renderPanel({ - backgroundRuns: [ - { - ...TERMINAL_RUN, - id: "detected:1", - source: "detected", - terminalId: null, - label: "Dev server", - port: 5173, - }, - ], - }); - - try { - await expect.element(page.getByText("codex · detected")).toBeVisible(); - } finally { - await mounted.unmount(); - } - }); - - it("toggles the terminal when a run branch is pressed instead of drilling in", async () => { - const onToggleBackgroundRunTerminal = vi.fn(); - const mounted = await renderPanel( - { backgroundRuns: [PROVIDER_RUN] }, - onToggleBackgroundRunTerminal, - ); - - try { - await page.getByRole("button", { name: "Open Dev server terminal" }).click(); - expect(onToggleBackgroundRunTerminal).toHaveBeenCalledWith("default"); - // Still the tree: a run never replaces the panel with a transcript. - expect(document.querySelector("[data-agents-panel='tree']")).not.toBeNull(); } finally { await mounted.unmount(); } @@ -406,7 +352,6 @@ describe("AgentsPanel", () => { }, }), ]} - backgroundRuns={[]} history={[ buildHistoryEntry({ item: buildSubagent({ @@ -425,7 +370,6 @@ describe("AgentsPanel", () => { ]} providerLabel="codex" embedded - onToggleBackgroundRunTerminal={vi.fn()} onStopBackgroundRun={vi.fn()} />
, @@ -784,10 +728,9 @@ describe("AgentsPanel", () => { } }); - it("marks a spawned agent with the thread provider's glyph but leaves runs their tag", async () => { + it("marks a spawned agent with the thread provider's glyph", async () => { const mounted = await renderPanel({ subagents: [buildSubagent({ label: "Router sweep" })], - backgroundRuns: [PROVIDER_RUN], providerLabel: "claudeAgent", }); @@ -798,12 +741,7 @@ describe("AgentsPanel", () => { const subagentRow = rows.find( (row) => row.getAttribute("data-agent-branch-kind") === "subagent", ); - const runRow = rows.find((row) => row.getAttribute("data-agent-branch-kind") === "run"); expect(subagentRow?.querySelector("[data-agent-branch-provider='true'] svg")).not.toBeNull(); - expect(runRow?.querySelector("[data-agent-branch-provider='true']")).toBeNull(); - expect(runRow?.querySelector("[data-agent-branch-tag='true']")?.textContent).toBe( - "claudeagent · provider", - ); } finally { await mounted.unmount(); } @@ -824,10 +762,8 @@ describe("AgentsPanel", () => { environmentId={ENVIRONMENT_ID} threadId={THREAD_ID} subagents={[buildSubagent({ label: "Router sweep" })]} - backgroundRuns={[]} providerLabel="codex" embedded - onToggleBackgroundRunTerminal={vi.fn()} onStopBackgroundRun={vi.fn()} /> @@ -923,7 +859,7 @@ describe("AgentsPanel", () => { workingTreeFileCount: 0, reviewableTurnCount: 0, diffHasExplicitTarget: false, - agents: { subagents: [], backgroundRuns: [], history: [] }, + agents: { subagents: [], history: [] }, })} onSelectTab={onSelectTab} onCloseTab={vi.fn()} @@ -991,7 +927,6 @@ describe("AgentsPanel", () => { diffHasExplicitTarget: false, agents: { subagents: [buildSubagent({ label: "Router sweep" })], - backgroundRuns: [], history: [ buildHistoryEntry({ item: buildSubagent({ @@ -1040,7 +975,7 @@ describe("AgentsPanel", () => { workingTreeFileCount: 0, reviewableTurnCount: 0, diffHasExplicitTarget: true, - agents: { subagents: [], backgroundRuns: [], history: [] }, + agents: { subagents: [], history: [] }, })} onSelectTab={vi.fn()} onCloseTab={vi.fn()} @@ -1077,7 +1012,7 @@ describe("AgentsPanel", () => { workingTreeFileCount: 0, reviewableTurnCount: 6, diffHasExplicitTarget: false, - agents: { subagents: [], backgroundRuns: [], history: [] }, + agents: { subagents: [], history: [] }, })} onSelectTab={vi.fn()} onCloseTab={vi.fn()} @@ -1162,7 +1097,6 @@ describe("AgentsPanel", () => { diffHasExplicitTarget: false, agents: { subagents: [buildSubagent({ label: "Router sweep" })], - backgroundRuns: [], history: [], }, })} diff --git a/apps/web/src/components/chat/AgentsPanel.tsx b/apps/web/src/components/chat/AgentsPanel.tsx index 3e996542..11c97f8e 100644 --- a/apps/web/src/components/chat/AgentsPanel.tsx +++ b/apps/web/src/components/chat/AgentsPanel.tsx @@ -31,7 +31,6 @@ export interface AgentsPanelProps { environmentId: EnvironmentId; threadId: ThreadId; subagents: ReadonlyArray; - backgroundRuns: ReadonlyArray; /** Background runs already listed above as subagents, keyed by the tool call * that launched them. They are not rendered as rows; they only give the * matching agent row a stop handle. */ @@ -53,7 +52,6 @@ export interface AgentsPanelProps { /** Set when the panel renders inside the sidebar's tab strip, which already * carries the window chrome, the panel's name and its dismissal. */ embedded?: boolean; - onToggleBackgroundRunTerminal: (terminalId: string) => void; onStopBackgroundRun: (run: ThreadBackgroundRunItem) => void; onClose?: (() => void) | undefined; } @@ -139,17 +137,13 @@ function BranchRow({ onSelect: (branch: AgentBranch) => void; onStop: (branch: AgentBranch) => void; }) { - const interactive = - branch.kind === "subagent" ? branch.transcriptAvailable : branch.terminalId !== null; + const interactive = branch.transcriptAvailable; const meta = branch.meta.join(" · "); - const ariaLabel = - branch.kind === "subagent" - ? `Open ${branch.name} transcript` - : `${branch.terminalVisible ? "Close" : "Open"} ${branch.name} terminal`; + const ariaLabel = `Open ${branch.name} transcript`; - // An agent the provider launched as a background shell command borrows the - // run rows' stop arm — same control, same placement, same behavior. - const canStop = branch.kind === "run" ? branch.run.canStop : branch.stoppableRun !== null; + // An agent the provider launched as a background shell command borrows a + // stop arm from the run it is — the same control the activity chip offers. + const canStop = branch.stoppableRun !== null; const flat = variant === "flat"; // Flat rows carry no trunk to hang a status dot off, and a filed-away agent // that simply finished has nothing to say with one. Anything else does. @@ -271,7 +265,6 @@ function BranchRow({ className={cn(rowClassName, "transition-colors hover:bg-foreground/[0.03] focus-ring")} aria-label={ariaLabel} title={rowTitle} - aria-pressed={branch.kind === "run" ? branch.terminalVisible : undefined} onClick={() => onSelect(branch)} > {body} @@ -300,7 +293,6 @@ export const AgentsPanel = memo(function AgentsPanel({ environmentId, threadId, subagents, - backgroundRuns, subagentRuns, history, workEntries = EMPTY_WORK_ENTRIES, @@ -308,15 +300,14 @@ export const AgentsPanel = memo(function AgentsPanel({ turnInFlight = false, threadCwd, embedded = false, - onToggleBackgroundRunTerminal, onStopBackgroundRun, onClose, }: AgentsPanelProps) { const selectedAgentId = useSelectedAgentId(); const view = useMemo( - () => buildAgentsPanelView({ subagents, backgroundRuns, subagentRuns, history, providerLabel }), - [backgroundRuns, history, providerLabel, subagentRuns, subagents], + () => buildAgentsPanelView({ subagents, subagentRuns, history }), + [history, subagentRuns, subagents], ); const headerMeta = useMemo(() => formatAgentsHeaderMeta({ subagents }), [subagents]); const headerSummary = useMemo( @@ -342,26 +333,16 @@ export const AgentsPanel = memo(function AgentsPanel({ [selectedSubagentThreadId, workEntries], ); - const handleSelect = useCallback( - (branch: AgentBranch) => { - if (branch.kind === "run") { - if (branch.terminalId) { - onToggleBackgroundRunTerminal(branch.terminalId); - } - return; - } - if (branch.item.agentThreadId) { - selectAgentsPanelAgent(branch.item.agentThreadId); - } - }, - [onToggleBackgroundRunTerminal], - ); + const handleSelect = useCallback((branch: AgentBranch) => { + if (branch.item.agentThreadId) { + selectAgentsPanelAgent(branch.item.agentThreadId); + } + }, []); const handleStop = useCallback( (branch: AgentBranch) => { - const run = branch.kind === "run" ? branch.run : branch.stoppableRun; - if (run) { - onStopBackgroundRun(run); + if (branch.stoppableRun) { + onStopBackgroundRun(branch.stoppableRun); } }, [onStopBackgroundRun], @@ -515,31 +496,6 @@ export const AgentsPanel = memo(function AgentsPanel({ ) : null} - {/* Background commands are not agents; they keep their rows (and - stop handles) below the agents instead of crowding them out. */} - {view.commands.length > 0 ? ( - <> - - Commands - -
    - {view.commands.map((branch) => ( - - ))} -
- - ) : null} )} diff --git a/apps/web/src/components/chat/agentsPanel.logic.test.ts b/apps/web/src/components/chat/agentsPanel.logic.test.ts index 0c80156d..6791d27d 100644 --- a/apps/web/src/components/chat/agentsPanel.logic.test.ts +++ b/apps/web/src/components/chat/agentsPanel.logic.test.ts @@ -92,7 +92,6 @@ describe("buildAgentBranches", () => { createdAt: "2026-08-11T10:03:00.000Z", }), ], - backgroundRuns: [], }); expect(branches.map((branch) => branch.key)).toEqual([ @@ -117,7 +116,6 @@ describe("buildAgentBranches", () => { }), buildSubagent({ id: "native", spawnCallId: "tool-native" }), ], - backgroundRuns: [], subagentRuns: new Map([ ["tool-codex-exec", run], // A settled agent's process is already gone, so its row never offers @@ -145,7 +143,6 @@ describe("buildAgentBranches", () => { buildSubagent({ id: "starting", status: "starting" }), buildSubagent({ id: "interrupted", status: "interrupted" }), ], - backgroundRuns: [], }); expect(branches.map((branch) => `${branch.key}:${branch.status}`)).toEqual([ @@ -181,7 +178,6 @@ describe("buildAgentBranches", () => { }, }), ], - backgroundRuns: [], }); expect(running?.output).toBe("Running the test suite"); @@ -192,69 +188,18 @@ describe("buildAgentBranches", () => { it("falls back to the agent's streamed prose when the provider reports no step", () => { const [branch] = buildAgentBranches({ subagents: [buildSubagent({ liveBody: " Reading the route \n files " })], - backgroundRuns: [], }); expect(branch?.output).toBe("Reading the route files"); }); - it("marks a run with its provenance and the terminal it toggles", () => { - const branches = buildAgentBranches({ - subagents: [], - backgroundRuns: [ - buildRun({ id: "detected-run" }), - buildRun({ - id: "provider-run", - source: "provider", - providerKind: "command", - terminalId: "terminal-a", - terminalVisible: true, - label: "Dev server task", - }), - ], - providerLabel: "codex", - }); - - expect(branches.map((branch) => branch.tag)).toEqual(["codex · detected", "codex · provider"]); - const providerBranch = branches.find((branch) => branch.key === "run:provider-run"); - expect(providerBranch?.kind === "run" && providerBranch.terminalId).toBe("terminal-a"); - expect(providerBranch?.kind === "run" && providerBranch.terminalVisible).toBe(true); - }); - - it("leaves the user's own terminals out: a hand-run shell is not orchestration", () => { - const branches = buildAgentBranches({ - subagents: [], - backgroundRuns: [ - buildRun({ id: "detected-run" }), - buildRun({ - id: "terminal-run", - source: "terminal", - terminalId: "terminal-a", - label: "vp run dev:desktop", - }), - ], - providerLabel: "codex", - }); - - expect(branches.map((branch) => branch.key)).toEqual(["run:detected-run"]); - }); - - it("names a run's served URL as its latest output", () => { - const [branch] = buildAgentBranches({ - subagents: [], - backgroundRuns: [buildRun({ urls: ["http://localhost:5173"] })], - }); - - expect(branch?.output).toBe("http://localhost:5173"); - }); - - it("keeps subagents and runs in one ordering, agents ahead of untimed runs", () => { + it("never draws background command runs: the activity chip is their surface", () => { const branches = buildAgentBranches({ subagents: [buildSubagent({ id: "agent", status: "running" })], - backgroundRuns: [buildRun({ id: "run" })], }); - expect(branches.map((branch) => branch.key)).toEqual(["subagent:agent", "run:run"]); + expect(branches.map((branch) => branch.key)).toEqual(["subagent:agent"]); + expect(branches.every((branch) => branch.kind === "subagent")).toBe(true); }); }); @@ -267,7 +212,6 @@ describe("buildAgentsPanelView", () => { it("keeps finished agents listed after the live items empty", () => { const view = buildAgentsPanelView({ subagents: [], - backgroundRuns: [], history: [ historyOf( buildSubagent({ @@ -304,32 +248,12 @@ describe("buildAgentsPanelView", () => { expect(view.earlier[0]?.time).toBe("12m ago"); }); - it("keeps background commands out of the agent list, in their own section", () => { - const view = buildAgentsPanelView({ - subagents: [buildSubagent({ id: "live", agentThreadId: "agent-live", status: "running" })], - backgroundRuns: [ - buildRun({ - id: "provider:task-watch", - source: "provider", - label: "Watch PR 162 CI until settled", - port: null, - }), - ], - }); - - expect(view.current.map((branch) => branch.kind)).toEqual(["subagent"]); - expect(view.commands.map((branch) => branch.kind)).toEqual(["run"]); - expect(view.hasAny).toBe(true); - }); - - it("counts a command-only thread as having content", () => { + it("holds only agents: a thread with nothing but commands running is empty", () => { const view = buildAgentsPanelView({ subagents: [], - backgroundRuns: [buildRun({ id: "provider:task-1", source: "provider" })], }); expect(view.current).toEqual([]); - expect(view.commands).toHaveLength(1); - expect(view.hasAny).toBe(true); + expect(view.hasAny).toBe(false); }); describe("formatAgentsPanelSummary", () => { @@ -348,7 +272,6 @@ describe("buildAgentsPanelView", () => { statusLabel: "Needs approval", }), ], - backgroundRuns: [], history: [ historyOf( buildSubagent({ id: "old", agentThreadId: "old", status: "completed" }), @@ -362,19 +285,16 @@ describe("buildAgentsPanelView", () => { ); }); - it("counts a background run as a run rather than as an agent", () => { + it("never mentions background runs: they are not agents", () => { const view = viewOf({ subagents: [buildSubagent({ id: "a", agentThreadId: "a", status: "running" })], - backgroundRuns: [buildRun({ id: "detected:default" })], }); - expect(formatAgentsPanelSummary(view, "claude")).toBe("Claude · 1 running · 1 run"); + expect(formatAgentsPanelSummary(view, "claude")).toBe("Claude · 1 running"); }); it("has nothing to say about a thread that has never run an agent", () => { - expect( - formatAgentsPanelSummary(viewOf({ subagents: [], backgroundRuns: [] }), "codex"), - ).toBeNull(); + expect(formatAgentsPanelSummary(viewOf({ subagents: [] }), "codex")).toBeNull(); }); }); @@ -383,7 +303,6 @@ describe("buildAgentsPanelView", () => { // model, effort and age rather than dropping to just an age. const view = buildAgentsPanelView({ subagents: [], - backgroundRuns: [], history: [ historyOf( buildSubagent({ @@ -423,7 +342,6 @@ describe("buildAgentsPanelView", () => { createdAt: "2026-08-11T10:11:24.000Z", }), ], - backgroundRuns: [], nowMs: Date.parse("2026-08-11T10:12:00.000Z"), }); @@ -433,7 +351,6 @@ describe("buildAgentsPanelView", () => { it("orders the history newest first", () => { const view = buildAgentsPanelView({ subagents: [], - backgroundRuns: [], history: [ historyOf( buildSubagent({ @@ -460,7 +377,6 @@ describe("buildAgentsPanelView", () => { it("lets the live record win over its own history counterpart", () => { const view = buildAgentsPanelView({ subagents: [buildSubagent({ id: "agent", agentThreadId: "agent-thread", status: "running" })], - backgroundRuns: [], history: [ historyOf( buildSubagent({ id: "agent", agentThreadId: "agent-thread", status: "completed" }), @@ -475,7 +391,6 @@ describe("buildAgentsPanelView", () => { it("renders a failed history record as a failure rather than a quiet completion", () => { const view = buildAgentsPanelView({ subagents: [], - backgroundRuns: [], history: [ historyOf( buildSubagent({ @@ -500,7 +415,6 @@ describe("findAgentsPanelSubagent", () => { it("resolves an agent that exists only in the thread's history", () => { const view = buildAgentsPanelView({ subagents: [], - backgroundRuns: [], history: [ { item: buildSubagent({ id: "old", agentThreadId: "agent-old", status: "completed" }), diff --git a/apps/web/src/components/chat/agentsPanel.logic.ts b/apps/web/src/components/chat/agentsPanel.logic.ts index 85b49e4c..f3d924da 100644 --- a/apps/web/src/components/chat/agentsPanel.logic.ts +++ b/apps/web/src/components/chat/agentsPanel.logic.ts @@ -16,9 +16,6 @@ import { pluralize } from "../../lib/utils"; import { formatElapsedDurationLabel, formatRelativeTimeLabel } from "../../timestampFormat"; import { formatSubagentMetaParts, formatSubagentDuration } from "./subagentMeta"; import { - backgroundRunCommandText, - backgroundRunMetaItems, - backgroundRunSourceLabel, deriveSubagentDisplayDetails, normalizeSubagentInlineText, type ThreadBackgroundRunItem, @@ -66,16 +63,10 @@ export interface AgentSubagentBranch extends AgentBranchBase { readonly stoppableRun: ThreadBackgroundRunItem | null; } -export interface AgentRunBranch extends AgentBranchBase { - readonly kind: "run"; - readonly depth: 0; - readonly run: ThreadBackgroundRunItem; - /** Clicking the branch toggles this terminal; null runs are not clickable. */ - readonly terminalId: string | null; - readonly terminalVisible: boolean; -} - -export type AgentBranch = AgentSubagentBranch | AgentRunBranch; +/** Background command runs are not agents and never appear in this panel — + * the header's activity chip is their surface (rows, terminal toggle, stop). + * The alias survives so branch consumers keep reading as "any branch". */ +export type AgentBranch = AgentSubagentBranch; export interface TurnAgentSummarySegment { readonly id: string; @@ -116,39 +107,6 @@ export function isLiveAgentBranchStatus(status: AgentBranchStatus): boolean { return status === "running" || status === "waiting"; } -/** - * A background run only appears while it is live, so every run branch reads as - * running. Provider-managed runs that the provider has parked read as waiting. - */ -function backgroundRunBranchStatus(run: ThreadBackgroundRunItem): AgentBranchStatus { - return /\b(waiting|blocked|paused)\b/iu.test(run.statusLabel) ? "waiting" : "running"; -} - -/** - * The runs the panel — and every indicator that advertises it — counts: work an - * agent started, or that detection attributed to one. A terminal the user - * opened themselves is the thread's own shell, not the turn's orchestration; - * the terminal strip and the header's activity popover are its surfaces, and - * counting it here made a hand-run dev server read as an agent. - */ -function agentInitiatedRuns( - runs: ReadonlyArray, -): ReadonlyArray { - return runs.filter((run) => run.source !== "terminal"); -} - -/** `codex · detected`. The provider is dropped when it is not known. */ -function backgroundRunTag( - run: ThreadBackgroundRunItem, - providerLabel: string | null | undefined, -): string { - if (run.source === "terminal") { - return "terminal"; - } - const provider = providerLabel?.trim().toLowerCase(); - return provider ? `${provider} · ${run.source}` : run.source; -} - /** * `gpt-5.6-sol · high`: who is doing the work, as the provider recorded it. The * model is the raw slug rather than a catalog display name, because that is what @@ -204,34 +162,6 @@ function subagentBranch( }; } -function runBranch( - run: ThreadBackgroundRunItem, - providerLabel: string | null | undefined, -): AgentRunBranch { - const task = backgroundRunCommandText(run); - const detail = run.detail?.trim() || null; - // A served URL is the most useful thing a run has said; its detail line is - // the fallback when it is not just the command restated. - const primaryUrl = run.urls[0] ?? null; - const output = primaryUrl ?? (detail !== null && detail !== task ? detail : null); - return { - kind: "run", - key: `run:${run.id}`, - status: backgroundRunBranchStatus(run), - name: run.label, - statusLabel: run.statusLabel, - meta: [backgroundRunSourceLabel(run), ...backgroundRunMetaItems(run)], - time: null, - task, - output, - tag: backgroundRunTag(run, providerLabel), - depth: 0, - run, - terminalId: run.terminalId, - terminalVisible: run.terminalVisible === true, - }; -} - /** * Running first, then waiting, then failed, then completed — the order in * which a branch is likely to need attention. Within a group, oldest first; @@ -240,23 +170,15 @@ function runBranch( */ export function buildAgentBranches(input: { readonly subagents: ReadonlyArray; - readonly backgroundRuns: ReadonlyArray; /** Background runs that are already listed as subagents, keyed by the tool * call that launched them. Lends each matching row its stop handle. */ readonly subagentRuns?: ReadonlyMap | undefined; - readonly providerLabel?: string | null | undefined; readonly nowMs?: number | undefined; }): ReadonlyArray { - const branches: Array<{ branch: AgentBranch; startedAtMs: number | null }> = [ - ...input.subagents.map((item) => ({ - branch: subagentBranch(item, input.nowMs, input.subagentRuns) as AgentBranch, - startedAtMs: parseTimestamp(item.createdAt), - })), - ...agentInitiatedRuns(input.backgroundRuns).map((run) => ({ - branch: runBranch(run, input.providerLabel) as AgentBranch, - startedAtMs: null, - })), - ]; + const branches = input.subagents.map((item) => ({ + branch: subagentBranch(item, input.nowMs, input.subagentRuns), + startedAtMs: parseTimestamp(item.createdAt), + })); return branches .toSorted((left, right) => { @@ -329,12 +251,9 @@ export interface AgentsPanelView { readonly current: ReadonlyArray; /** Agents from earlier in the thread, newest first. */ readonly earlier: ReadonlyArray; - /** Non-agent background work — command tasks, detected processes. A CI - * watcher loop is not an agent, and a tab called Agents putting commands - * above the agents read as exactly that; they keep their rows (and stop - * buttons) in their own section below instead. */ - readonly commands: ReadonlyArray; - /** False only when the thread has never run an agent at all. */ + /** False only when the thread has never run an agent at all. Background + * command runs do not appear in this panel at all — the header's activity + * chip is their surface. */ readonly hasAny: boolean; } @@ -343,10 +262,6 @@ export interface AgentsPanelView { * these are, how many are working, how many want something, and how much of the * list is already finished. The row otherwise carried a pulsing dot and a * duration with nothing between them naming what was pulsing. - * - * Terminal and detected runs are counted as runs rather than folded into the - * agent count — a dev server on the tree is not an agent, and saying so would - * inflate the only number here anyone would act on. */ export function formatAgentsPanelSummary( view: AgentsPanelView, @@ -361,13 +276,11 @@ export function formatAgentsPanelSummary( ); const runningCount = subagents.filter((branch) => branch.status === "running").length; const waitingCount = subagents.filter((branch) => branch.status === "waiting").length; - const runCount = view.commands.length; const parts = [ providerDisplayLabel(providerLabel), runningCount > 0 ? `${runningCount} running` : null, waitingCount > 0 ? `${waitingCount} needs you` : null, - runCount > 0 ? pluralize(runCount, "run") : null, view.earlier.length > 0 ? `${view.earlier.length} earlier` : null, ].filter((part): part is string => part !== null); @@ -392,17 +305,11 @@ function providerDisplayLabel(providerLabel: string | null | undefined): string */ export function buildAgentsPanelView(input: { readonly subagents: ReadonlyArray; - readonly backgroundRuns: ReadonlyArray; readonly subagentRuns?: ReadonlyMap | undefined; readonly history?: ReadonlyArray | undefined; - readonly providerLabel?: string | null | undefined; readonly nowMs?: number | undefined; }): AgentsPanelView { - const branches = buildAgentBranches(input); - const current = branches.filter( - (branch): branch is AgentSubagentBranch => branch.kind === "subagent", - ); - const commands = branches.filter((branch): branch is AgentRunBranch => branch.kind === "run"); + const current = buildAgentBranches(input); const currentAgentKeys = new Set(input.subagents.map(subagentIdentity)); const earlier = (input.history ?? []) .filter((entry) => !currentAgentKeys.has(subagentIdentity(entry.item))) @@ -415,8 +322,7 @@ export function buildAgentsPanelView(input: { return { current, earlier, - commands, - hasAny: current.length > 0 || earlier.length > 0 || commands.length > 0, + hasAny: current.length > 0 || earlier.length > 0, }; } diff --git a/apps/web/src/components/chat/rightPanelLauncherState.test.ts b/apps/web/src/components/chat/rightPanelLauncherState.test.ts index 3c7621cb..237aa4db 100644 --- a/apps/web/src/components/chat/rightPanelLauncherState.test.ts +++ b/apps/web/src/components/chat/rightPanelLauncherState.test.ts @@ -50,7 +50,7 @@ function buildRun(overrides: Partial = {}): ThreadBackg }; } -const NO_AGENTS = { subagents: [], backgroundRuns: [], history: [] } as const; +const NO_AGENTS = { subagents: [], history: [] } as const; /** A clean thread with no committed turn diffs behind it. */ const EMPTY_THREAD = { reviewableTurnCount: 0, agents: NO_AGENTS } as const; @@ -210,7 +210,6 @@ describe("buildRightPanelLauncherStates", () => { diffHasExplicitTarget: false, agents: { subagents: [], - backgroundRuns: [], history: [ { item: buildSubagent({ id: "a", status: "completed" }), resultBody: null }, { @@ -240,21 +239,20 @@ describe("buildRightPanelLauncherStates", () => { workingTreeFileCount: null, reviewableTurnCount: 0, diffHasExplicitTarget: false, - // The background run is a command, not an agent; it neither inflates - // the agent count nor reads as one running. - agents: { subagents: [buildSubagent()], backgroundRuns: [buildRun()], history }, + agents: { subagents: [buildSubagent()], history }, }).agents, ).toEqual({ description: "1 of 2 agents running.", empty: false }); - // A thread with only background commands is not empty, but has no agents. + // Background commands live in the header's activity chip; a thread with + // nothing but commands running has no agents to report here. expect( buildRightPanelLauncherStates({ workingTreeFileCount: null, reviewableTurnCount: 0, diffHasExplicitTarget: false, - agents: { subagents: [], backgroundRuns: [buildRun()], history: [] }, + agents: { subagents: [], history: [] }, }).agents, - ).toEqual({ description: "1 background command.", empty: false }); + ).toEqual({ description: "No agents yet.", empty: true }); expect( buildRightPanelLauncherStates({ @@ -263,7 +261,6 @@ describe("buildRightPanelLauncherStates", () => { diffHasExplicitTarget: false, agents: { subagents: [buildSubagent({ status: "waiting", statusLabel: "Needs approval" })], - backgroundRuns: [], history, }, }).agents, @@ -275,7 +272,7 @@ describe("buildRightPanelLauncherStates", () => { workingTreeFileCount: null, reviewableTurnCount: 0, diffHasExplicitTarget: false, - agents: { subagents: [buildSubagent()], backgroundRuns: [], history: [] }, + agents: { subagents: [buildSubagent()], history: [] }, }).agents, ).toEqual({ description: "1 agent running.", empty: false }); }); diff --git a/apps/web/src/components/chat/rightPanelLauncherState.ts b/apps/web/src/components/chat/rightPanelLauncherState.ts index fb41d21e..3e5f1d4f 100644 --- a/apps/web/src/components/chat/rightPanelLauncherState.ts +++ b/apps/web/src/components/chat/rightPanelLauncherState.ts @@ -40,7 +40,6 @@ export type RightPanelLauncherStates = Readonly< /** The agent state the panel already receives, read only for its counts. */ export interface RightPanelLauncherAgentsInput { readonly subagents: ReadonlyArray; - readonly backgroundRuns: ReadonlyArray; readonly history: ReadonlyArray; } @@ -180,11 +179,9 @@ function agentsState(agents: RightPanelLauncherAgentsInput | null): RightPanelSu const view = buildAgentsPanelView(agents); const total = view.current.length + view.earlier.length; if (total === 0) { - // Commands are not agents, but a surface with live rows on it is not - // empty either. - return view.commands.length > 0 - ? { description: `${pluralize(view.commands.length, "background command")}.`, empty: false } - : { description: "No agents yet.", empty: true }; + // Background commands live in the header's activity chip, not here, so + // they have no bearing on whether this surface is empty. + return { description: "No agents yet.", empty: true }; } const live = summarizeLiveAgents(agents); const waitingCount = live?.waitingCount ?? 0; diff --git a/apps/web/src/routes/_chat.$environmentId.$threadId.tsx b/apps/web/src/routes/_chat.$environmentId.$threadId.tsx index fd169a81..886033a1 100644 --- a/apps/web/src/routes/_chat.$environmentId.$threadId.tsx +++ b/apps/web/src/routes/_chat.$environmentId.$threadId.tsx @@ -68,9 +68,7 @@ import { // (or on a thread with nothing running at all), so the agents tab renders from // these until a source for this thread arrives. const EMPTY_SUBAGENTS = [] as const; -const EMPTY_BACKGROUND_RUNS = [] as const; const EMPTY_SUBAGENT_HISTORY = [] as const; -const noopToggleTerminal = () => {}; const noopStopRun = () => {}; const DiffPanel = lazy(() => import("../components/DiffPanel")); @@ -471,7 +469,6 @@ function ChatThreadRouteView() { environmentId={threadRef.environmentId} threadId={threadRef.threadId} subagents={agentsSource?.subagents ?? EMPTY_SUBAGENTS} - backgroundRuns={agentsSource?.backgroundRuns ?? EMPTY_BACKGROUND_RUNS} subagentRuns={agentsSource?.subagentRuns} history={agentsSource?.history ?? EMPTY_SUBAGENT_HISTORY} workEntries={agentsSource?.workEntries} @@ -479,9 +476,6 @@ function ChatThreadRouteView() { turnInFlight={agentsSource?.turnInFlight ?? false} threadCwd={agentsSource?.threadCwd} embedded - onToggleBackgroundRunTerminal={ - agentsSource?.onToggleBackgroundRunTerminal ?? noopToggleTerminal - } onStopBackgroundRun={agentsSource?.onStopBackgroundRun ?? noopStopRun} /> From 2b50b0449e4746e21256c84c65aec7443b85d64f Mon Sep 17 00:00:00 2001 From: Badcuban <108198679+badcuban@users.noreply.github.com> Date: Sun, 16 Aug 2026 21:23:37 -0400 Subject: [PATCH 2/2] Drop the removed backgroundRuns input from the panel view test --- apps/web/src/components/chat/agentsPanel.logic.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/components/chat/agentsPanel.logic.test.ts b/apps/web/src/components/chat/agentsPanel.logic.test.ts index 6791d27d..a5a9d2f6 100644 --- a/apps/web/src/components/chat/agentsPanel.logic.test.ts +++ b/apps/web/src/components/chat/agentsPanel.logic.test.ts @@ -407,7 +407,7 @@ describe("buildAgentsPanelView", () => { }); it("reports nothing at all only for a thread that has never run an agent", () => { - expect(buildAgentsPanelView({ subagents: [], backgroundRuns: [] }).hasAny).toBe(false); + expect(buildAgentsPanelView({ subagents: [] }).hasAny).toBe(false); }); });