diff --git a/.story_kit/work/2_current/218_story_hide_thinking_traces_from_agents_panel.md b/.story_kit/work/2_current/218_story_hide_thinking_traces_from_agents_panel.md new file mode 100644 index 0000000..2a26a5b --- /dev/null +++ b/.story_kit/work/2_current/218_story_hide_thinking_traces_from_agents_panel.md @@ -0,0 +1,19 @@ +--- +name: "Hide thinking traces from Agents panel" +--- + +# Story 218: Hide thinking traces from Agents panel + +## User Story + +As a user, I don't want to see Claude's internal thinking traces in the Agents panel, so that the panel only shows actionable output. + +## Acceptance Criteria + +- [ ] AgentPanel does not render thinking traces (ThinkingBlock component removed or thinking events ignored) +- [ ] Agent output log only shows regular text output, status changes, and errors +- [ ] No regression in agent output streaming (text_delta events still display correctly) + +## Out of Scope + +- TBD diff --git a/frontend/src/components/AgentPanel.test.tsx b/frontend/src/components/AgentPanel.test.tsx index 282c5c4..ce79daa 100644 --- a/frontend/src/components/AgentPanel.test.tsx +++ b/frontend/src/components/AgentPanel.test.tsx @@ -213,7 +213,7 @@ describe("RosterBadge availability state", () => { }); }); -describe("Thinking trace block in agent stream UI", () => { +describe("Thinking traces hidden from agent stream UI", () => { beforeAll(() => { Element.prototype.scrollIntoView = vi.fn(); }); @@ -224,8 +224,8 @@ describe("Thinking trace block in agent stream UI", () => { mockedSubscribeAgentStream.mockReturnValue(() => {}); }); - // AC1+AC2: thinking block renders with fixed max-height and is visually distinct - it("renders thinking block with max-height 96px when thinking event arrives", async () => { + // AC1: thinking block is never rendered even when thinking events arrive + it("does not render thinking block when thinking event arrives", async () => { let emitEvent: ((e: AgentEvent) => void) | null = null; mockedSubscribeAgentStream.mockImplementation( (_storyId, _agentName, onEvent) => { @@ -236,7 +236,7 @@ describe("Thinking trace block in agent stream UI", () => { const agentList: AgentInfo[] = [ { - story_id: "160_thinking", + story_id: "218_thinking", agent_name: "coder-1", status: "running", session_id: null, @@ -248,37 +248,23 @@ describe("Thinking trace block in agent stream UI", () => { mockedAgents.listAgents.mockResolvedValue(agentList); render(); - - // Wait for the subscription to be set up await screen.findByTestId("roster-badge-coder-1"); - // Fire a thinking event await act(async () => { emitEvent?.({ type: "thinking", - story_id: "160_thinking", + story_id: "218_thinking", agent_name: "coder-1", text: "Let me consider the problem carefully...", }); }); - const block = screen.getByTestId("thinking-block"); - expect(block).toBeInTheDocument(); - // AC2: fixed max-height - expect(block.style.maxHeight).toBe("96px"); - // AC2: overflow scrolling - expect(block.style.overflowY).toBe("auto"); - // AC1: visually distinct — italic monospace font - expect(block.style.fontStyle).toBe("italic"); - expect(block.style.fontFamily).toBe("monospace"); - // Contains the thinking text - expect(block.textContent).toContain( - "Let me consider the problem carefully...", - ); + // AC1: thinking block must not be present + expect(screen.queryByTestId("thinking-block")).not.toBeInTheDocument(); }); - // AC3: thinking block renders the "thinking" label - it("shows a thinking label in the block header", async () => { + // AC2: after thinking events, only regular output is rendered + it("renders regular output but not thinking block when both arrive", async () => { let emitEvent: ((e: AgentEvent) => void) | null = null; mockedSubscribeAgentStream.mockImplementation( (_storyId, _agentName, onEvent) => { @@ -289,7 +275,7 @@ describe("Thinking trace block in agent stream UI", () => { const agentList: AgentInfo[] = [ { - story_id: "160_label", + story_id: "218_output", agent_name: "coder-1", status: "running", session_id: null, @@ -303,80 +289,37 @@ describe("Thinking trace block in agent stream UI", () => { render(); await screen.findByTestId("roster-badge-coder-1"); + // Thinking event — must be ignored visually await act(async () => { emitEvent?.({ type: "thinking", - story_id: "160_label", - agent_name: "coder-1", - text: "thinking...", - }); - }); - - const block = screen.getByTestId("thinking-block"); - expect(block.textContent).toContain("thinking"); - }); - - // AC4: regular text output renders outside the thinking container - it("renders regular output outside the thinking block", async () => { - let emitEvent: ((e: AgentEvent) => void) | null = null; - mockedSubscribeAgentStream.mockImplementation( - (_storyId, _agentName, onEvent) => { - emitEvent = onEvent; - return () => {}; - }, - ); - - const agentList: AgentInfo[] = [ - { - story_id: "160_output", - agent_name: "coder-1", - status: "running", - session_id: null, - worktree_path: "/tmp/wt", - base_branch: "master", - log_session_id: null, - }, - ]; - mockedAgents.listAgents.mockResolvedValue(agentList); - - render(); - await screen.findByTestId("roster-badge-coder-1"); - - // First: thinking event - await act(async () => { - emitEvent?.({ - type: "thinking", - story_id: "160_output", + story_id: "218_output", agent_name: "coder-1", text: "thinking deeply", }); }); - // Then: text output event + // AC3: output event still renders correctly (no regression) await act(async () => { emitEvent?.({ type: "output", - story_id: "160_output", + story_id: "218_output", agent_name: "coder-1", text: "Here is the result.", }); }); - const thinkingBlock = screen.getByTestId("thinking-block"); + // AC1: no thinking block + expect(screen.queryByTestId("thinking-block")).not.toBeInTheDocument(); + + // AC2+AC3: output area renders the text const outputArea = screen.getByTestId("agent-output-coder-1"); - - // Thinking still visible - expect(thinkingBlock).toBeInTheDocument(); - expect(thinkingBlock.textContent).toContain("thinking deeply"); - - // Output renders in a separate element, not inside the thinking block expect(outputArea).toBeInTheDocument(); expect(outputArea.textContent).toContain("Here is the result."); - expect(thinkingBlock.contains(outputArea)).toBe(false); }); - // AC5: thinking block remains visible when text starts - it("keeps thinking block visible after output arrives", async () => { + // AC3: output-only event stream (no thinking) still works + it("renders output event text without a thinking block", async () => { let emitEvent: ((e: AgentEvent) => void) | null = null; mockedSubscribeAgentStream.mockImplementation( (_storyId, _agentName, onEvent) => { @@ -387,7 +330,7 @@ describe("Thinking trace block in agent stream UI", () => { const agentList: AgentInfo[] = [ { - story_id: "160_persist", + story_id: "218_noThink", agent_name: "coder-1", status: "running", session_id: null, @@ -401,25 +344,17 @@ describe("Thinking trace block in agent stream UI", () => { render(); await screen.findByTestId("roster-badge-coder-1"); - await act(async () => { - emitEvent?.({ - type: "thinking", - story_id: "160_persist", - agent_name: "coder-1", - text: "my thoughts", - }); - }); - await act(async () => { emitEvent?.({ type: "output", - story_id: "160_persist", + story_id: "218_noThink", agent_name: "coder-1", - text: "final answer", + text: "plain output line", }); }); - // Thinking block still in the DOM after output arrives - expect(screen.getByTestId("thinking-block")).toBeInTheDocument(); + expect(screen.queryByTestId("thinking-block")).not.toBeInTheDocument(); + const outputArea = screen.getByTestId("agent-output-coder-1"); + expect(outputArea.textContent).toContain("plain output line"); }); }); diff --git a/frontend/src/components/AgentPanel.tsx b/frontend/src/components/AgentPanel.tsx index 29fe437..2a99f10 100644 --- a/frontend/src/components/AgentPanel.tsx +++ b/frontend/src/components/AgentPanel.tsx @@ -14,66 +14,12 @@ interface AgentState { agentName: string; status: AgentStatusValue; log: string[]; - /** Accumulated thinking text for the current turn. */ - thinking: string; - /** True once regular output has been received after thinking started. */ - thinkingDone: boolean; sessionId: string | null; worktreePath: string | null; baseBranch: string | null; terminalAt: number | null; } -/** Fixed-height thinking trace block that auto-scrolls to bottom as text arrives. */ -function ThinkingBlock({ text }: { text: string }) { - const scrollRef = useRef(null); - - // Auto-scroll to bottom whenever text grows - useEffect(() => { - const el = scrollRef.current; - if (el) { - el.scrollTop = el.scrollHeight; - } - }, [text]); - - return ( -
- - thinking - - {text} -
- ); -} - const formatTimestamp = (value: Date | null): string => { if (!value) return ""; return value.toLocaleTimeString([], { @@ -175,8 +121,6 @@ export function AgentPanel({ agentName, status: "pending" as AgentStatusValue, log: [], - thinking: "", - thinkingDone: false, sessionId: null, worktreePath: null, baseBranch: null, @@ -200,23 +144,12 @@ export function AgentPanel({ }, }; } - case "thinking": - return { - ...prev, - [key]: { - ...current, - thinking: current.thinking + (event.text ?? ""), - }, - }; case "output": return { ...prev, [key]: { ...current, log: [...current.log, event.text ?? ""], - // Receiving text output signals thinking phase is over - thinkingDone: - current.thinking.length > 0 ? true : current.thinkingDone, }, }; case "done": @@ -269,8 +202,6 @@ export function AgentPanel({ agentName: a.agent_name, status: a.status, log: [], - thinking: "", - thinkingDone: false, sessionId: a.session_id, worktreePath: a.worktree_path, baseBranch: a.base_branch, @@ -327,10 +258,8 @@ export function AgentPanel({ } }; - // Agents that have streaming content to show (thinking or log) - const activeAgents = Object.values(agents).filter( - (a) => a.thinking.length > 0 || a.log.length > 0, - ); + // Agents that have streaming content to show + const activeAgents = Object.values(agents).filter((a) => a.log.length > 0); return (
)} - {/* Per-agent streaming output: thinking trace + regular text */} + {/* Per-agent streaming output */} {activeAgents.map((agent) => (
- {agent.thinking.length > 0 && } {agent.log.length > 0 && (