story-kit: start 218_story_hide_thinking_traces_from_agents_panel

This commit is contained in:
Dave
2026-02-27 09:52:02 +00:00
parent 6ca4ecc52e
commit 19f661e8e3
3 changed files with 48 additions and 166 deletions

View File

@@ -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

View File

@@ -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(() => { beforeAll(() => {
Element.prototype.scrollIntoView = vi.fn(); Element.prototype.scrollIntoView = vi.fn();
}); });
@@ -224,8 +224,8 @@ describe("Thinking trace block in agent stream UI", () => {
mockedSubscribeAgentStream.mockReturnValue(() => {}); mockedSubscribeAgentStream.mockReturnValue(() => {});
}); });
// AC1+AC2: thinking block renders with fixed max-height and is visually distinct // AC1: thinking block is never rendered even when thinking events arrive
it("renders thinking block with max-height 96px when thinking event arrives", async () => { it("does not render thinking block when thinking event arrives", async () => {
let emitEvent: ((e: AgentEvent) => void) | null = null; let emitEvent: ((e: AgentEvent) => void) | null = null;
mockedSubscribeAgentStream.mockImplementation( mockedSubscribeAgentStream.mockImplementation(
(_storyId, _agentName, onEvent) => { (_storyId, _agentName, onEvent) => {
@@ -236,7 +236,7 @@ describe("Thinking trace block in agent stream UI", () => {
const agentList: AgentInfo[] = [ const agentList: AgentInfo[] = [
{ {
story_id: "160_thinking", story_id: "218_thinking",
agent_name: "coder-1", agent_name: "coder-1",
status: "running", status: "running",
session_id: null, session_id: null,
@@ -248,37 +248,23 @@ describe("Thinking trace block in agent stream UI", () => {
mockedAgents.listAgents.mockResolvedValue(agentList); mockedAgents.listAgents.mockResolvedValue(agentList);
render(<AgentPanel />); render(<AgentPanel />);
// Wait for the subscription to be set up
await screen.findByTestId("roster-badge-coder-1"); await screen.findByTestId("roster-badge-coder-1");
// Fire a thinking event
await act(async () => { await act(async () => {
emitEvent?.({ emitEvent?.({
type: "thinking", type: "thinking",
story_id: "160_thinking", story_id: "218_thinking",
agent_name: "coder-1", agent_name: "coder-1",
text: "Let me consider the problem carefully...", text: "Let me consider the problem carefully...",
}); });
}); });
const block = screen.getByTestId("thinking-block"); // AC1: thinking block must not be present
expect(block).toBeInTheDocument(); expect(screen.queryByTestId("thinking-block")).not.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...",
);
}); });
// AC3: thinking block renders the "thinking" label // AC2: after thinking events, only regular output is rendered
it("shows a thinking label in the block header", async () => { it("renders regular output but not thinking block when both arrive", async () => {
let emitEvent: ((e: AgentEvent) => void) | null = null; let emitEvent: ((e: AgentEvent) => void) | null = null;
mockedSubscribeAgentStream.mockImplementation( mockedSubscribeAgentStream.mockImplementation(
(_storyId, _agentName, onEvent) => { (_storyId, _agentName, onEvent) => {
@@ -289,7 +275,7 @@ describe("Thinking trace block in agent stream UI", () => {
const agentList: AgentInfo[] = [ const agentList: AgentInfo[] = [
{ {
story_id: "160_label", story_id: "218_output",
agent_name: "coder-1", agent_name: "coder-1",
status: "running", status: "running",
session_id: null, session_id: null,
@@ -303,80 +289,37 @@ describe("Thinking trace block in agent stream UI", () => {
render(<AgentPanel />); render(<AgentPanel />);
await screen.findByTestId("roster-badge-coder-1"); await screen.findByTestId("roster-badge-coder-1");
// Thinking event — must be ignored visually
await act(async () => { await act(async () => {
emitEvent?.({ emitEvent?.({
type: "thinking", type: "thinking",
story_id: "160_label", story_id: "218_output",
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(<AgentPanel />);
await screen.findByTestId("roster-badge-coder-1");
// First: thinking event
await act(async () => {
emitEvent?.({
type: "thinking",
story_id: "160_output",
agent_name: "coder-1", agent_name: "coder-1",
text: "thinking deeply", text: "thinking deeply",
}); });
}); });
// Then: text output event // AC3: output event still renders correctly (no regression)
await act(async () => { await act(async () => {
emitEvent?.({ emitEvent?.({
type: "output", type: "output",
story_id: "160_output", story_id: "218_output",
agent_name: "coder-1", agent_name: "coder-1",
text: "Here is the result.", 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"); 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).toBeInTheDocument();
expect(outputArea.textContent).toContain("Here is the result."); expect(outputArea.textContent).toContain("Here is the result.");
expect(thinkingBlock.contains(outputArea)).toBe(false);
}); });
// AC5: thinking block remains visible when text starts // AC3: output-only event stream (no thinking) still works
it("keeps thinking block visible after output arrives", async () => { it("renders output event text without a thinking block", async () => {
let emitEvent: ((e: AgentEvent) => void) | null = null; let emitEvent: ((e: AgentEvent) => void) | null = null;
mockedSubscribeAgentStream.mockImplementation( mockedSubscribeAgentStream.mockImplementation(
(_storyId, _agentName, onEvent) => { (_storyId, _agentName, onEvent) => {
@@ -387,7 +330,7 @@ describe("Thinking trace block in agent stream UI", () => {
const agentList: AgentInfo[] = [ const agentList: AgentInfo[] = [
{ {
story_id: "160_persist", story_id: "218_noThink",
agent_name: "coder-1", agent_name: "coder-1",
status: "running", status: "running",
session_id: null, session_id: null,
@@ -401,25 +344,17 @@ describe("Thinking trace block in agent stream UI", () => {
render(<AgentPanel />); render(<AgentPanel />);
await screen.findByTestId("roster-badge-coder-1"); 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 () => { await act(async () => {
emitEvent?.({ emitEvent?.({
type: "output", type: "output",
story_id: "160_persist", story_id: "218_noThink",
agent_name: "coder-1", agent_name: "coder-1",
text: "final answer", text: "plain output line",
}); });
}); });
// Thinking block still in the DOM after output arrives expect(screen.queryByTestId("thinking-block")).not.toBeInTheDocument();
expect(screen.getByTestId("thinking-block")).toBeInTheDocument(); const outputArea = screen.getByTestId("agent-output-coder-1");
expect(outputArea.textContent).toContain("plain output line");
}); });
}); });

View File

@@ -14,66 +14,12 @@ interface AgentState {
agentName: string; agentName: string;
status: AgentStatusValue; status: AgentStatusValue;
log: string[]; 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; sessionId: string | null;
worktreePath: string | null; worktreePath: string | null;
baseBranch: string | null; baseBranch: string | null;
terminalAt: number | 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<HTMLDivElement>(null);
// Auto-scroll to bottom whenever text grows
useEffect(() => {
const el = scrollRef.current;
if (el) {
el.scrollTop = el.scrollHeight;
}
}, [text]);
return (
<div
data-testid="thinking-block"
ref={scrollRef}
style={{
maxHeight: "96px",
overflowY: "auto",
background: "#161b22",
border: "1px solid #2d333b",
borderRadius: "6px",
padding: "6px 10px",
fontSize: "0.78em",
fontFamily: "monospace",
color: "#6e7681",
fontStyle: "italic",
whiteSpace: "pre-wrap",
wordBreak: "break-word",
lineHeight: "1.4",
}}
>
<span
style={{
display: "block",
fontSize: "0.8em",
color: "#444c56",
marginBottom: "4px",
fontStyle: "normal",
letterSpacing: "0.04em",
textTransform: "uppercase",
}}
>
thinking
</span>
{text}
</div>
);
}
const formatTimestamp = (value: Date | null): string => { const formatTimestamp = (value: Date | null): string => {
if (!value) return ""; if (!value) return "";
return value.toLocaleTimeString([], { return value.toLocaleTimeString([], {
@@ -175,8 +121,6 @@ export function AgentPanel({
agentName, agentName,
status: "pending" as AgentStatusValue, status: "pending" as AgentStatusValue,
log: [], log: [],
thinking: "",
thinkingDone: false,
sessionId: null, sessionId: null,
worktreePath: null, worktreePath: null,
baseBranch: null, baseBranch: null,
@@ -200,23 +144,12 @@ export function AgentPanel({
}, },
}; };
} }
case "thinking":
return {
...prev,
[key]: {
...current,
thinking: current.thinking + (event.text ?? ""),
},
};
case "output": case "output":
return { return {
...prev, ...prev,
[key]: { [key]: {
...current, ...current,
log: [...current.log, event.text ?? ""], log: [...current.log, event.text ?? ""],
// Receiving text output signals thinking phase is over
thinkingDone:
current.thinking.length > 0 ? true : current.thinkingDone,
}, },
}; };
case "done": case "done":
@@ -269,8 +202,6 @@ export function AgentPanel({
agentName: a.agent_name, agentName: a.agent_name,
status: a.status, status: a.status,
log: [], log: [],
thinking: "",
thinkingDone: false,
sessionId: a.session_id, sessionId: a.session_id,
worktreePath: a.worktree_path, worktreePath: a.worktree_path,
baseBranch: a.base_branch, baseBranch: a.base_branch,
@@ -327,10 +258,8 @@ export function AgentPanel({
} }
}; };
// Agents that have streaming content to show (thinking or log) // Agents that have streaming content to show
const activeAgents = Object.values(agents).filter( const activeAgents = Object.values(agents).filter((a) => a.log.length > 0);
(a) => a.thinking.length > 0 || a.log.length > 0,
);
return ( return (
<div <div
@@ -488,7 +417,7 @@ export function AgentPanel({
</div> </div>
)} )}
{/* Per-agent streaming output: thinking trace + regular text */} {/* Per-agent streaming output */}
{activeAgents.map((agent) => ( {activeAgents.map((agent) => (
<div <div
key={`stream-${agent.agentName}`} key={`stream-${agent.agentName}`}
@@ -499,7 +428,6 @@ export function AgentPanel({
gap: "4px", gap: "4px",
}} }}
> >
{agent.thinking.length > 0 && <ThinkingBlock text={agent.thinking} />}
{agent.log.length > 0 && ( {agent.log.length > 0 && (
<div <div
data-testid={`agent-output-${agent.agentName}`} data-testid={`agent-output-${agent.agentName}`}