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(() => {
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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 */}
|
||||
{(
|
||||
|
||||
Reference in New Issue
Block a user