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..a5a9d2f6 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({ @@ -492,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); }); }); @@ -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} />