story-kit: start 218_story_hide_thinking_traces_from_agents_panel
This commit is contained in:
@@ -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(<AgentPanel />);
|
||||
|
||||
// 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(<AgentPanel />);
|
||||
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(<AgentPanel />);
|
||||
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(<AgentPanel />);
|
||||
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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<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 => {
|
||||
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 (
|
||||
<div
|
||||
@@ -488,7 +417,7 @@ export function AgentPanel({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Per-agent streaming output: thinking trace + regular text */}
|
||||
{/* Per-agent streaming output */}
|
||||
{activeAgents.map((agent) => (
|
||||
<div
|
||||
key={`stream-${agent.agentName}`}
|
||||
@@ -499,7 +428,6 @@ export function AgentPanel({
|
||||
gap: "4px",
|
||||
}}
|
||||
>
|
||||
{agent.thinking.length > 0 && <ThinkingBlock text={agent.thinking} />}
|
||||
{agent.log.length > 0 && (
|
||||
<div
|
||||
data-testid={`agent-output-${agent.agentName}`}
|
||||
|
||||
Reference in New Issue
Block a user