diff --git a/apps/web/components/task/chat/messages/tool-subagent-message.test.tsx b/apps/web/components/task/chat/messages/tool-subagent-message.test.tsx new file mode 100644 index 0000000000..6fdc265fe6 --- /dev/null +++ b/apps/web/components/task/chat/messages/tool-subagent-message.test.tsx @@ -0,0 +1,128 @@ +import { describe, it, expect, afterEach } from "vitest"; +import { render, screen, fireEvent, cleanup } from "@testing-library/react"; +import { ToolSubagentMessage } from "@/components/task/chat/messages/tool-subagent-message"; +import type { Message } from "@/lib/types/http"; + +// Guards the subagent card's expand/collapse behavior. Regression coverage for +// "silent" subagents (a result_text with no child tool calls) rendering +// permanently expanded: auto-expand must key on the running state alone, not on +// result_text, so completed cards collapse to their header + metadata row like +// subagents that stream child tool calls. + +function subagentMessage(opts: { + status: string; + result_text?: string; + description?: string; +}): Message { + return { + id: "m1", + content: "Subagent", + metadata: { + status: opts.status, + normalized: { + kind: "subagent_task", + subagent_task: { + description: opts.description ?? "Summarize the repo", + subagent_type: "general-purpose", + status: opts.status, + result_text: opts.result_text, + }, + }, + }, + } as unknown as Message; +} + +const noopRenderChild = () => null; +const SILENT_RESULT = "Found 42 files across 7 packages."; +const RESULT_TEXT = "subagent-result-text"; + +afterEach(cleanup); + +describe("ToolSubagentMessage expansion", () => { + it("collapses a completed subagent with child tool calls to its header", () => { + const child = { id: "c1", content: "sleep 30", metadata: {} } as unknown as Message; + render( +
sleep 30
} + />, + ); + expect(screen.queryByTestId("child-body")).toBeNull(); + }); + + it("collapses a completed silent subagent (result_text, no children)", () => { + render( + , + ); + // Completed and no longer running -> collapsed: result text stays hidden + // until the user opens the card. The metadata row remains visible. + expect(screen.queryByTestId(RESULT_TEXT)).toBeNull(); + expect(screen.getByTestId("subagent-type")).toBeTruthy(); + }); + + it("reveals the silent subagent's result text when expanded", () => { + render( + , + ); + fireEvent.click(screen.getByRole("button")); + expect(screen.getByTestId(RESULT_TEXT).textContent).toContain(SILENT_RESULT); + }); + + it("auto-expands while the subagent is running, then auto-collapses on completion", () => { + const running = subagentMessage({ status: "running" }); + const { rerender } = render( + , + ); + // Running -> auto-expanded working indicator is shown. + expect(screen.queryByText("Subagent working...")).not.toBeNull(); + + rerender( + , + ); + // Completed without a manual override -> auto-collapsed. + expect(screen.queryByTestId(RESULT_TEXT)).toBeNull(); + }); + + it("keeps a manual collapse after the subagent completes", () => { + const { rerender } = render( + , + ); + // User collapses the running card. + fireEvent.click(screen.getByRole("button")); + expect(screen.queryByText("Subagent working...")).toBeNull(); + + // Completion emits result_text; the manual collapse must survive (it used + // to be wiped, forcing the card back open). + rerender( + , + ); + expect(screen.queryByTestId(RESULT_TEXT)).toBeNull(); + }); +}); diff --git a/apps/web/components/task/chat/messages/tool-subagent-message.tsx b/apps/web/components/task/chat/messages/tool-subagent-message.tsx index fdfe761538..ead4eaecd4 100644 --- a/apps/web/components/task/chat/messages/tool-subagent-message.tsx +++ b/apps/web/components/task/chat/messages/tool-subagent-message.tsx @@ -182,23 +182,13 @@ export const ToolSubagentMessage = memo(function ToolSubagentMessage({ // Track manual override state - null means "use auto behavior" const [manualExpandState, setManualExpandState] = useState(null); - // Auto behavior: expand if active or if we have a result text to surface - // (silent Auggie-style subagents have no child messages, so we want the - // final summary visible without an extra click). - const autoExpanded = isActive || Boolean(resultText); - - // Reset the manual override the first time a result_text arrives so a card - // the user collapsed while the subagent was running auto-opens to show the - // summary. "Adjust state during render" pattern (preferred over useEffect): - // https://react.dev/learn/you-might-not-need-an-effect — only fires on the - // empty -> non-empty transition; later user collapses persist. - const [prevResultText, setPrevResultText] = useState(resultText); - if (resultText !== prevResultText) { - setPrevResultText(resultText); - if (resultText && !prevResultText) { - setManualExpandState(null); - } - } + // Auto behavior: expand only while the subagent is active. On completion the + // card auto-collapses to its header + metadata row, matching subagents that + // stream child tool calls; the result text is one click away. Previously + // auto-expand was also keyed on result_text, which left silent (Auggie-style) + // subagents — the ones with no child messages — permanently expanded because + // result_text never clears. + const autoExpanded = isActive; // Derive expanded state: manual override takes precedence, otherwise use auto const isExpanded = manualExpandState ?? autoExpanded;