Skip to content

Commit eb76666

Browse files
authored
fix(canvas): keep agent status out of thread history
Generated-By: PostHog Code Task-Id: 87a79e23-20c4-440b-818b-28154924ffbc
1 parent 2f661a1 commit eb76666

4 files changed

Lines changed: 36 additions & 68 deletions

File tree

packages/core/src/canvas/threadTimeline.test.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -139,14 +139,9 @@ describe("deriveThreadAgentStatus", () => {
139139
expected: { phase: "active", label: "Working…" },
140140
},
141141
{
142-
name: "reports shipped work",
143-
input: { hasActivity: true, hasPullRequest: true },
144-
expected: { phase: "complete", label: "Shipped" },
145-
},
146-
{
147-
name: "reports settled work without a pull request as done",
142+
name: "returns no status after work settles",
148143
input: { hasActivity: true },
149-
expected: { phase: "complete", label: "Done" },
144+
expected: null,
150145
},
151146
])("$name", ({ input, expected }) => {
152147
expect(deriveThreadAgentStatus(input)).toEqual(expected);

packages/core/src/canvas/threadTimeline.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ export function buildThreadTimeline<T>({
7373
].sort((left, right) => left.timestamp - right.timestamp);
7474
}
7575

76-
export type ThreadAgentPhase = "active" | "needs_input" | "complete" | "error";
76+
export type ThreadAgentPhase = "active" | "needs_input" | "error";
7777

7878
export interface ThreadAgentStatus {
7979
phase: ThreadAgentPhase;
@@ -109,7 +109,6 @@ export function deriveThreadAgentStatus({
109109
pendingPermissionCount = 0,
110110
isPromptPending = false,
111111
isInitializing = false,
112-
hasPullRequest = false,
113112
}: {
114113
hasActivity?: boolean;
115114
hasError?: boolean;
@@ -118,7 +117,6 @@ export function deriveThreadAgentStatus({
118117
pendingPermissionCount?: number;
119118
isPromptPending?: boolean;
120119
isInitializing?: boolean;
121-
hasPullRequest?: boolean;
122120
}): ThreadAgentStatus | null {
123121
if (!hasActivity) return null;
124122
if (hasError || cloudStatus === "failed") {
@@ -130,10 +128,7 @@ export function deriveThreadAgentStatus({
130128
if (isPromptPending || isInitializing) {
131129
return { phase: "active", label: "Working…" };
132130
}
133-
return {
134-
phase: "complete",
135-
label: hasPullRequest ? "Shipped" : "Done",
136-
};
131+
return null;
137132
}
138133

139134
export function shouldSuspendThreadSession({

packages/ui/src/features/canvas/components/ThreadPanel.test.tsx

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,16 @@
11
import { render, screen } from "@testing-library/react";
22
import { describe, expect, it } from "vitest";
3-
import { AgentTurnRow, UserPromptRow } from "./ThreadPanel";
3+
import { AgentStatusLine, UserPromptRow } from "./ThreadPanel";
44

5-
describe("AgentTurnRow", () => {
6-
it.each([
7-
{ phase: "active" as const, label: "Working…" },
8-
{ phase: "complete" as const, label: "Done" },
9-
])("renders $label in the message area", (statusValue) => {
10-
render(<AgentTurnRow status={statusValue} streaming={false} />);
5+
describe("AgentStatusLine", () => {
6+
it("renders working status outside the conversation timeline", () => {
7+
render(<AgentStatusLine status={{ phase: "active", label: "Working…" }} />);
118

12-
const author = screen.getByText("Agent");
13-
const status = screen.getByText(statusValue.label);
9+
const status = screen.getByText("Working…");
1410

15-
expect(author.parentElement).not.toContainElement(status);
16-
expect(author.closest("article")).toContainElement(status);
17-
expect(status.closest('[data-slot="thread-item-body"]')).not.toBeNull();
11+
expect(status.closest("article")).toBeNull();
12+
expect(status.closest('[data-slot="thread-item-body"]')).toBeNull();
13+
expect(status.closest("output")).not.toBeNull();
1814
});
1915
});
2016

packages/ui/src/features/canvas/components/ThreadPanel.tsx

Lines changed: 24 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -204,31 +204,27 @@ function agentPrompts(items: ConversationItem[]): ThreadAgentMessage[] {
204204
return prompts;
205205
}
206206

207-
function AgentStatusChip({ status }: { status: ThreadAgentStatus }) {
208-
switch (status.phase) {
209-
case "active":
210-
return (
211-
<Badge variant="info">
212-
<Spinner className="size-3" />
213-
{status.label}
214-
</Badge>
215-
);
216-
case "needs_input":
217-
return <Badge variant="warning">{status.label}</Badge>;
218-
case "error":
219-
return <Badge variant="destructive">{status.label}</Badge>;
220-
default:
221-
return <Badge variant="success">{status.label}</Badge>;
222-
}
207+
export function AgentStatusLine({ status }: { status: ThreadAgentStatus }) {
208+
return (
209+
<output
210+
aria-live="polite"
211+
className="flex items-center gap-1.5 px-3 py-1.5 text-muted-foreground text-xs"
212+
>
213+
{status.phase === "active" ? (
214+
<Spinner className="size-3" />
215+
) : (
216+
<RobotIcon size={12} />
217+
)}
218+
<span>{status.label}</span>
219+
</output>
220+
);
223221
}
224222

225223
export function AgentTurnRow({
226224
message,
227-
status,
228225
streaming,
229226
}: {
230-
message?: ThreadAgentMessage;
231-
status?: ThreadAgentStatus;
227+
message: ThreadAgentMessage;
232228
streaming: boolean;
233229
}) {
234230
return (
@@ -243,25 +239,19 @@ export function AgentTurnRow({
243239
<ThreadItemContent>
244240
<ThreadItemHeader>
245241
<ThreadItemAuthor>Agent</ThreadItemAuthor>
246-
{message?.timestamp !== undefined && (
242+
{message.timestamp !== undefined && (
247243
<ThreadTimestamp
248244
dateTime={new Date(message.timestamp).toISOString()}
249245
/>
250246
)}
251247
</ThreadItemHeader>
252-
{(message?.text || status) && (
248+
{message.text && (
253249
<ThreadItemBody>
254250
<div className="rounded-md border border-border bg-muted px-2 py-1.5">
255-
{message?.text &&
256-
(streaming ? (
257-
<ChatStreamingMarkdown content={message.text} />
258-
) : (
259-
<ChatMarkdown content={message.text} />
260-
))}
261-
{status && (
262-
<div className={message?.text ? "mt-2" : undefined}>
263-
<AgentStatusChip status={status} />
264-
</div>
251+
{streaming ? (
252+
<ChatStreamingMarkdown content={message.text} />
253+
) : (
254+
<ChatMarkdown content={message.text} />
265255
)}
266256
</div>
267257
</ThreadItemBody>
@@ -370,11 +360,6 @@ function ThreadConversation({
370360
});
371361
const { items } = useConversationItems(events, isPromptPending);
372362
const pendingPermissions = usePendingPermissionsForTask(taskId);
373-
const prUrl =
374-
typeof task.latest_run?.output?.pr_url === "string"
375-
? task.latest_run.output.pr_url
376-
: undefined;
377-
378363
const agentMsgs = useMemo(() => agentTurns(items), [items]);
379364
const promptMsgs = useMemo(() => agentPrompts(items), [items]);
380365

@@ -388,7 +373,6 @@ function ThreadConversation({
388373
pendingPermissionCount: pendingPermissions.size,
389374
isPromptPending,
390375
isInitializing,
391-
hasPullRequest: !!prUrl,
392376
}),
393377
[
394378
events.length,
@@ -399,7 +383,6 @@ function ThreadConversation({
399383
pendingPermissions.size,
400384
isPromptPending,
401385
isInitializing,
402-
prUrl,
403386
],
404387
);
405388

@@ -498,7 +481,7 @@ function ThreadConversation({
498481
});
499482
};
500483

501-
const isEmpty = timeline.length === 0 && !agentStatus;
484+
const isEmpty = timeline.length === 0;
502485
const isReady = !isInitializing && !isLoading;
503486

504487
return (
@@ -595,13 +578,12 @@ function ThreadConversation({
595578
/>
596579
),
597580
)}
598-
{agentStatus && !(agentStatus.phase === "complete" && !!prUrl) && (
599-
<AgentTurnRow status={agentStatus} streaming={false} />
600-
)}
601581
</ThreadItemGroup>
602582
)}
603583
</div>
604584

585+
{agentStatus && <AgentStatusLine status={agentStatus} />}
586+
605587
<div className="border-border border-t p-2">
606588
<MentionComposer
607589
value={draft}

0 commit comments

Comments
 (0)