story-kit: merge 290_story_show_agent_output_stream_in_expanded_work_item_detail_panel
This commit is contained in:
@@ -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");
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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={{
|
||||||
|
|||||||
@@ -420,75 +420,96 @@ export function WorkItemDetailPanel({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Agent Logs section */}
|
{/* Agent Logs section */}
|
||||||
<div
|
{!agentInfo && (
|
||||||
data-testid={
|
|
||||||
agentInfo ? "agent-logs-section" : "placeholder-agent-logs"
|
|
||||||
}
|
|
||||||
style={{
|
|
||||||
border: "1px solid #2a2a2a",
|
|
||||||
borderRadius: "8px",
|
|
||||||
padding: "10px 12px",
|
|
||||||
background: "#161616",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
|
data-testid="placeholder-agent-logs"
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
border: "1px solid #2a2a2a",
|
||||||
alignItems: "center",
|
borderRadius: "8px",
|
||||||
justifyContent: "space-between",
|
padding: "10px 12px",
|
||||||
marginBottom: agentInfo ? "6px" : "4px",
|
background: "#161616",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
fontSize: "0.8em",
|
fontSize: "0.8em",
|
||||||
color: agentInfo ? "#888" : "#555",
|
color: "#555",
|
||||||
|
marginBottom: "4px",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Agent Logs
|
Agent Logs
|
||||||
</div>
|
</div>
|
||||||
{agentInfo && agentStatus && (
|
|
||||||
<div
|
|
||||||
data-testid="agent-status-badge"
|
|
||||||
style={{
|
|
||||||
fontSize: "0.7em",
|
|
||||||
color: STATUS_COLORS[agentStatus],
|
|
||||||
fontWeight: 600,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{agentInfo.agent_name} — {agentStatus}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{agentInfo && agentLog.length > 0 ? (
|
|
||||||
<div
|
|
||||||
data-testid="agent-log-output"
|
|
||||||
style={{
|
|
||||||
fontSize: "0.75em",
|
|
||||||
fontFamily: "monospace",
|
|
||||||
color: "#ccc",
|
|
||||||
whiteSpace: "pre-wrap",
|
|
||||||
wordBreak: "break-word",
|
|
||||||
lineHeight: "1.5",
|
|
||||||
maxHeight: "200px",
|
|
||||||
overflowY: "auto",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{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" }}>
|
<div style={{ fontSize: "0.75em", color: "#444" }}>
|
||||||
Coming soon
|
Coming soon
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
</div>
|
)}
|
||||||
|
{agentInfo && (
|
||||||
|
<div
|
||||||
|
data-testid="agent-logs-section"
|
||||||
|
style={{
|
||||||
|
border: "1px solid #2a2a2a",
|
||||||
|
borderRadius: "8px",
|
||||||
|
padding: "10px 12px",
|
||||||
|
background: "#161616",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
marginBottom: "6px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontWeight: 600,
|
||||||
|
fontSize: "0.8em",
|
||||||
|
color: "#888",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Agent Logs
|
||||||
|
</div>
|
||||||
|
{agentStatus && (
|
||||||
|
<div
|
||||||
|
data-testid="agent-status-badge"
|
||||||
|
style={{
|
||||||
|
fontSize: "0.7em",
|
||||||
|
color: STATUS_COLORS[agentStatus],
|
||||||
|
fontWeight: 600,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{agentInfo.agent_name} — {agentStatus}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{agentLog.length > 0 ? (
|
||||||
|
<div
|
||||||
|
data-testid="agent-log-output"
|
||||||
|
style={{
|
||||||
|
fontSize: "0.75em",
|
||||||
|
fontFamily: "monospace",
|
||||||
|
color: "#ccc",
|
||||||
|
whiteSpace: "pre-wrap",
|
||||||
|
wordBreak: "break-word",
|
||||||
|
lineHeight: "1.5",
|
||||||
|
maxHeight: "200px",
|
||||||
|
overflowY: "auto",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{agentLog.join("")}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div style={{ fontSize: "0.75em", color: "#444" }}>
|
||||||
|
{agentStatus === "running" || agentStatus === "pending"
|
||||||
|
? "Waiting for output..."
|
||||||
|
: "No output."}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Placeholder sections for future content */}
|
{/* Placeholder sections for future content */}
|
||||||
{(
|
{(
|
||||||
|
|||||||
Reference in New Issue
Block a user