Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 5 additions & 6 deletions apps/web/src/agentsPanelStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -22,7 +23,6 @@ export interface AgentsPanelSource {
environmentId: EnvironmentId;
threadId: ThreadId;
subagents: ReadonlyArray<SubagentProgressItem>;
backgroundRuns: ReadonlyArray<ThreadBackgroundRunItem>;
/** 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<string, ThreadBackgroundRunItem>;
Expand All @@ -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;
}

Expand Down
4 changes: 0 additions & 4 deletions apps/web/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3367,30 +3367,26 @@ export default function ChatView(props: ChatViewProps) {
environmentId,
threadId: activeThreadId,
subagents: subagentProgress?.items ?? EMPTY_SUBAGENT_ITEMS,
backgroundRuns,
subagentRuns: promotedSubagentRuns,
history: subagentHistory,
workEntries: workLogEntries,
providerLabel: activeProviderDriver,
turnInFlight: activeTurnInProgress,
hydrated: threadDetailHydrated,
threadCwd: gitCwd,
onToggleBackgroundRunTerminal: toggleBackgroundRunTerminal,
onStopBackgroundRun: stopBackgroundRun,
});
}, [
activeProviderDriver,
activeThreadId,
activeTurnInProgress,
backgroundRuns,
environmentId,
gitCwd,
promotedSubagentRuns,
stopBackgroundRun,
subagentHistory,
subagentProgress?.items,
threadDetailHydrated,
toggleBackgroundRunTerminal,
workLogEntries,
]);
useEffect(() => () => publishAgentsPanelSource(null), []);
Expand Down
80 changes: 7 additions & 73 deletions apps/web/src/components/chat/AgentsPanel.browser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,19 +83,14 @@ const TERMINAL_RUN: ThreadBackgroundRunItem = {
command: "vp run dev:desktop",
};

function renderPanel(
props: Partial<Parameters<typeof AgentsPanel>[0]> = {},
onToggleBackgroundRunTerminal = vi.fn(),
) {
function renderPanel(props: Partial<Parameters<typeof AgentsPanel>[0]> = {}) {
return render(
<main style={{ boxSizing: "border-box", height: 640, width: 400 }}>
<AgentsPanel
environmentId={ENVIRONMENT_ID}
threadId={THREAD_ID}
subagents={[]}
backgroundRuns={[]}
providerLabel="codex"
onToggleBackgroundRunTerminal={onToggleBackgroundRunTerminal}
onStopBackgroundRun={vi.fn()}
{...props}
/>
Expand Down Expand Up @@ -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();
}
Expand Down Expand Up @@ -406,7 +352,6 @@ describe("AgentsPanel", () => {
},
}),
]}
backgroundRuns={[]}
history={[
buildHistoryEntry({
item: buildSubagent({
Expand All @@ -425,7 +370,6 @@ describe("AgentsPanel", () => {
]}
providerLabel="codex"
embedded
onToggleBackgroundRunTerminal={vi.fn()}
onStopBackgroundRun={vi.fn()}
/>
</main>,
Expand Down Expand Up @@ -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",
});

Expand All @@ -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();
}
Expand All @@ -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()}
/>
</ChatRightPanel>
Expand Down Expand Up @@ -923,7 +859,7 @@ describe("AgentsPanel", () => {
workingTreeFileCount: 0,
reviewableTurnCount: 0,
diffHasExplicitTarget: false,
agents: { subagents: [], backgroundRuns: [], history: [] },
agents: { subagents: [], history: [] },
})}
onSelectTab={onSelectTab}
onCloseTab={vi.fn()}
Expand Down Expand Up @@ -991,7 +927,6 @@ describe("AgentsPanel", () => {
diffHasExplicitTarget: false,
agents: {
subagents: [buildSubagent({ label: "Router sweep" })],
backgroundRuns: [],
history: [
buildHistoryEntry({
item: buildSubagent({
Expand Down Expand Up @@ -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()}
Expand Down Expand Up @@ -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()}
Expand Down Expand Up @@ -1162,7 +1097,6 @@ describe("AgentsPanel", () => {
diffHasExplicitTarget: false,
agents: {
subagents: [buildSubagent({ label: "Router sweep" })],
backgroundRuns: [],
history: [],
},
})}
Expand Down
72 changes: 14 additions & 58 deletions apps/web/src/components/chat/AgentsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ export interface AgentsPanelProps {
environmentId: EnvironmentId;
threadId: ThreadId;
subagents: ReadonlyArray<SubagentProgressItem>;
backgroundRuns: ReadonlyArray<ThreadBackgroundRunItem>;
/** 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. */
Expand All @@ -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;
}
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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}
Expand Down Expand Up @@ -300,23 +293,21 @@ export const AgentsPanel = memo(function AgentsPanel({
environmentId,
threadId,
subagents,
backgroundRuns,
subagentRuns,
history,
workEntries = EMPTY_WORK_ENTRIES,
providerLabel,
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(
Expand All @@ -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],
Expand Down Expand Up @@ -515,31 +496,6 @@ export const AgentsPanel = memo(function AgentsPanel({
</ul>
</>
) : 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 ? (
<>
<SectionLabel
className="px-3 py-1.5"
tick={false}
data-agents-panel-commands="true"
>
Commands
</SectionLabel>
<ul className="divide-y divide-border/40 border-t border-border/40">
{view.commands.map((branch) => (
<BranchRow
key={branch.key}
branch={branch}
variant="flat"
providerGlyph={providerGlyph}
onSelect={handleSelect}
onStop={handleStop}
/>
))}
</ul>
</>
) : null}
</div>
)}
</div>
Expand Down
Loading
Loading