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(() => {
Element.prototype.scrollIntoView = vi.fn();
});
@@ -224,7 +224,51 @@ describe("Thinking traces hidden from agent stream UI", () => {
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 () => {
let emitEvent: ((e: AgentEvent) => void) | null = null;
mockedSubscribeAgentStream.mockImplementation(
@@ -236,7 +280,7 @@ describe("Thinking traces hidden from agent stream UI", () => {
const agentList: AgentInfo[] = [
{
story_id: "218_thinking",
story_id: "290_thinking",
agent_name: "coder-1",
status: "running",
session_id: null,
@@ -253,109 +297,16 @@ describe("Thinking traces hidden from agent stream UI", () => {
await act(async () => {
emitEvent?.({
type: "thinking",
story_id: "218_thinking",
story_id: "290_thinking",
agent_name: "coder-1",
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();
});
// 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) => {
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");
expect(
screen.queryByText("Let me consider the problem carefully..."),
).not.toBeInTheDocument();
});
});

View File

@@ -13,7 +13,6 @@ const { useCallback, useEffect, useRef, useState } = React;
interface AgentState {
agentName: string;
status: AgentStatusValue;
log: string[];
sessionId: string | null;
worktreePath: string | null;
baseBranch: string | null;
@@ -120,7 +119,6 @@ export function AgentPanel({
const current = prev[key] ?? {
agentName,
status: "pending" as AgentStatusValue,
log: [],
sessionId: null,
worktreePath: null,
baseBranch: null,
@@ -144,14 +142,6 @@ export function AgentPanel({
},
};
}
case "output":
return {
...prev,
[key]: {
...current,
log: [...current.log, event.text ?? ""],
},
};
case "done":
return {
...prev,
@@ -168,17 +158,12 @@ export function AgentPanel({
[key]: {
...current,
status: "failed",
log: [
...current.log,
`[ERROR] ${event.message ?? "Unknown error"}`,
],
terminalAt: current.terminalAt ?? Date.now(),
},
};
case "thinking":
// Thinking traces are internal model state — never display them.
return prev;
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;
}
});
@@ -204,7 +189,6 @@ export function AgentPanel({
agentMap[key] = {
agentName: a.agent_name,
status: a.status,
log: [],
sessionId: a.session_id,
worktreePath: a.worktree_path,
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 (
<div
style={{
@@ -420,35 +401,6 @@ export function AgentPanel({
</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 && (
<div
style={{

View File

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