story-kit: merge 290_story_show_agent_output_stream_in_expanded_work_item_detail_panel

This commit is contained in:
Dave
2026-03-18 15:28:08 +00:00
parent 68bf179407
commit 83ccfece81
3 changed files with 129 additions and 205 deletions

View File

@@ -213,7 +213,7 @@ describe("RosterBadge availability state", () => {
}); });
}); });
describe("Thinking traces hidden from agent stream UI", () => { describe("Agent output not shown in sidebar (story 290)", () => {
beforeAll(() => { beforeAll(() => {
Element.prototype.scrollIntoView = vi.fn(); Element.prototype.scrollIntoView = vi.fn();
}); });
@@ -224,7 +224,51 @@ describe("Thinking traces hidden from agent stream UI", () => {
mockedSubscribeAgentStream.mockReturnValue(() => {}); mockedSubscribeAgentStream.mockReturnValue(() => {});
}); });
// AC1: thinking block is never rendered even when thinking events arrive // AC1: output events do not appear in the agents sidebar
it("does not render agent output when output event arrives", async () => {
let emitEvent: ((e: AgentEvent) => void) | null = null;
mockedSubscribeAgentStream.mockImplementation(
(_storyId, _agentName, onEvent) => {
emitEvent = onEvent;
return () => {};
},
);
const agentList: AgentInfo[] = [
{
story_id: "290_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);
const { container } = render(<AgentPanel />);
await screen.findByTestId("roster-badge-coder-1");
await act(async () => {
emitEvent?.({
type: "output",
story_id: "290_output",
agent_name: "coder-1",
text: "doing some work...",
});
});
// No output elements in the sidebar
expect(
container.querySelector('[data-testid^="agent-output-"]'),
).not.toBeInTheDocument();
expect(
container.querySelector('[data-testid^="agent-stream-"]'),
).not.toBeInTheDocument();
});
// AC1: thinking events do not appear in the agents sidebar
it("does not render thinking block 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(
@@ -236,7 +280,7 @@ describe("Thinking traces hidden from agent stream UI", () => {
const agentList: AgentInfo[] = [ const agentList: AgentInfo[] = [
{ {
story_id: "218_thinking", story_id: "290_thinking",
agent_name: "coder-1", agent_name: "coder-1",
status: "running", status: "running",
session_id: null, session_id: null,
@@ -253,109 +297,16 @@ describe("Thinking traces hidden from agent stream UI", () => {
await act(async () => { await act(async () => {
emitEvent?.({ emitEvent?.({
type: "thinking", type: "thinking",
story_id: "218_thinking", story_id: "290_thinking",
agent_name: "coder-1", agent_name: "coder-1",
text: "Let me consider the problem carefully...", text: "Let me consider the problem carefully...",
}); });
}); });
// AC1: thinking block must not be present // No thinking block or output in sidebar
expect(screen.queryByTestId("thinking-block")).not.toBeInTheDocument(); expect(screen.queryByTestId("thinking-block")).not.toBeInTheDocument();
}); expect(
screen.queryByText("Let me consider the problem carefully..."),
// AC2: after thinking events, only regular output is rendered ).not.toBeInTheDocument();
it("renders regular output but not thinking block when both arrive", async () => {
let emitEvent: ((e: AgentEvent) => void) | null = null;
mockedSubscribeAgentStream.mockImplementation(
(_storyId, _agentName, onEvent) => {
emitEvent = onEvent;
return () => {};
},
);
const agentList: AgentInfo[] = [
{
story_id: "218_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");
// Thinking event — must be ignored visually
await act(async () => {
emitEvent?.({
type: "thinking",
story_id: "218_output",
agent_name: "coder-1",
text: "thinking deeply",
});
});
// AC3: output event still renders correctly (no regression)
await act(async () => {
emitEvent?.({
type: "output",
story_id: "218_output",
agent_name: "coder-1",
text: "Here is the result.",
});
});
// AC1: no thinking block
expect(screen.queryByTestId("thinking-block")).not.toBeInTheDocument();
// AC2+AC3: output area renders the text but NOT thinking text
const outputArea = screen.getByTestId("agent-output-coder-1");
expect(outputArea).toBeInTheDocument();
expect(outputArea.textContent).toContain("Here is the result.");
expect(outputArea.textContent).not.toContain("thinking deeply");
});
// 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) => {
emitEvent = onEvent;
return () => {};
},
);
const agentList: AgentInfo[] = [
{
story_id: "218_noThink",
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");
await act(async () => {
emitEvent?.({
type: "output",
story_id: "218_noThink",
agent_name: "coder-1",
text: "plain output line",
});
});
expect(screen.queryByTestId("thinking-block")).not.toBeInTheDocument();
const outputArea = screen.getByTestId("agent-output-coder-1");
expect(outputArea.textContent).toContain("plain output line");
}); });
}); });

View File

@@ -13,7 +13,6 @@ const { useCallback, useEffect, useRef, useState } = React;
interface AgentState { interface AgentState {
agentName: string; agentName: string;
status: AgentStatusValue; status: AgentStatusValue;
log: string[];
sessionId: string | null; sessionId: string | null;
worktreePath: string | null; worktreePath: string | null;
baseBranch: string | null; baseBranch: string | null;
@@ -120,7 +119,6 @@ export function AgentPanel({
const current = prev[key] ?? { const current = prev[key] ?? {
agentName, agentName,
status: "pending" as AgentStatusValue, status: "pending" as AgentStatusValue,
log: [],
sessionId: null, sessionId: null,
worktreePath: null, worktreePath: null,
baseBranch: null, baseBranch: null,
@@ -144,14 +142,6 @@ export function AgentPanel({
}, },
}; };
} }
case "output":
return {
...prev,
[key]: {
...current,
log: [...current.log, event.text ?? ""],
},
};
case "done": case "done":
return { return {
...prev, ...prev,
@@ -168,17 +158,12 @@ export function AgentPanel({
[key]: { [key]: {
...current, ...current,
status: "failed", status: "failed",
log: [
...current.log,
`[ERROR] ${event.message ?? "Unknown error"}`,
],
terminalAt: current.terminalAt ?? Date.now(), terminalAt: current.terminalAt ?? Date.now(),
}, },
}; };
case "thinking":
// Thinking traces are internal model state — never display them.
return prev;
default: default:
// output, thinking, and other events are not displayed in the sidebar.
// Agent output streams appear in the work item detail panel instead.
return prev; return prev;
} }
}); });
@@ -204,7 +189,6 @@ export function AgentPanel({
agentMap[key] = { agentMap[key] = {
agentName: a.agent_name, agentName: a.agent_name,
status: a.status, status: a.status,
log: [],
sessionId: a.session_id, sessionId: a.session_id,
worktreePath: a.worktree_path, worktreePath: a.worktree_path,
baseBranch: a.base_branch, baseBranch: a.base_branch,
@@ -261,9 +245,6 @@ export function AgentPanel({
} }
}; };
// Agents that have streaming content to show
const activeAgents = Object.values(agents).filter((a) => a.log.length > 0);
return ( return (
<div <div
style={{ style={{
@@ -420,35 +401,6 @@ export function AgentPanel({
</div> </div>
)} )}
{/* Per-agent streaming output */}
{activeAgents.map((agent) => (
<div
key={`stream-${agent.agentName}`}
data-testid={`agent-stream-${agent.agentName}`}
style={{
display: "flex",
flexDirection: "column",
gap: "4px",
}}
>
{agent.log.length > 0 && (
<div
data-testid={`agent-output-${agent.agentName}`}
style={{
fontSize: "0.8em",
fontFamily: "monospace",
color: "#ccc",
whiteSpace: "pre-wrap",
wordBreak: "break-word",
lineHeight: "1.5",
}}
>
{agent.log.join("")}
</div>
)}
</div>
))}
{actionError && ( {actionError && (
<div <div
style={{ style={{

View File

@@ -420,10 +420,34 @@ export function WorkItemDetailPanel({
}} }}
> >
{/* Agent Logs section */} {/* Agent Logs section */}
{!agentInfo && (
<div <div
data-testid={ data-testid="placeholder-agent-logs"
agentInfo ? "agent-logs-section" : "placeholder-agent-logs" style={{
} border: "1px solid #2a2a2a",
borderRadius: "8px",
padding: "10px 12px",
background: "#161616",
}}
>
<div
style={{
fontWeight: 600,
fontSize: "0.8em",
color: "#555",
marginBottom: "4px",
}}
>
Agent Logs
</div>
<div style={{ fontSize: "0.75em", color: "#444" }}>
Coming soon
</div>
</div>
)}
{agentInfo && (
<div
data-testid="agent-logs-section"
style={{ style={{
border: "1px solid #2a2a2a", border: "1px solid #2a2a2a",
borderRadius: "8px", borderRadius: "8px",
@@ -436,19 +460,19 @@ export function WorkItemDetailPanel({
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
justifyContent: "space-between", justifyContent: "space-between",
marginBottom: agentInfo ? "6px" : "4px", marginBottom: "6px",
}} }}
> >
<div <div
style={{ style={{
fontWeight: 600, fontWeight: 600,
fontSize: "0.8em", fontSize: "0.8em",
color: agentInfo ? "#888" : "#555", color: "#888",
}} }}
> >
Agent Logs Agent Logs
</div> </div>
{agentInfo && agentStatus && ( {agentStatus && (
<div <div
data-testid="agent-status-badge" data-testid="agent-status-badge"
style={{ style={{
@@ -461,7 +485,7 @@ export function WorkItemDetailPanel({
</div> </div>
)} )}
</div> </div>
{agentInfo && agentLog.length > 0 ? ( {agentLog.length > 0 ? (
<div <div
data-testid="agent-log-output" data-testid="agent-log-output"
style={{ style={{
@@ -477,18 +501,15 @@ export function WorkItemDetailPanel({
> >
{agentLog.join("")} {agentLog.join("")}
</div> </div>
) : agentInfo ? ( ) : (
<div style={{ fontSize: "0.75em", color: "#444" }}> <div style={{ fontSize: "0.75em", color: "#444" }}>
{agentStatus === "running" || agentStatus === "pending" {agentStatus === "running" || agentStatus === "pending"
? "Waiting for output..." ? "Waiting for output..."
: "No output."} : "No output."}
</div> </div>
) : (
<div style={{ fontSize: "0.75em", color: "#444" }}>
Coming soon
</div>
)} )}
</div> </div>
)}
{/* Placeholder sections for future content */} {/* Placeholder sections for future content */}
{( {(