Skip to content

Commit b6faccc

Browse files
authored
fix(canvas): address thread feed review findings
Generated-By: PostHog Code Task-Id: 87a79e23-20c4-440b-818b-28154924ffbc
1 parent f2e8cad commit b6faccc

8 files changed

Lines changed: 363 additions & 161 deletions

File tree

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { describe, expect, it } from "vitest";
2+
import { taskFeedRunStatus } from "./channelFeed";
3+
4+
describe("taskFeedRunStatus", () => {
5+
it.each(["queued", "in_progress"] as const)(
6+
"preserves a cloud %s status even when local session activity has settled",
7+
(status) => {
8+
expect(
9+
taskFeedRunStatus({
10+
status,
11+
environment: "cloud",
12+
}),
13+
).toBe(status);
14+
},
15+
);
16+
17+
it("hides an unreliable non-terminal local status", () => {
18+
expect(
19+
taskFeedRunStatus({ status: "queued", environment: "local" }),
20+
).toBeNull();
21+
});
22+
23+
it("keeps a terminal local status", () => {
24+
expect(
25+
taskFeedRunStatus({ status: "completed", environment: "local" }),
26+
).toBe("completed");
27+
});
28+
});
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import type { TaskRunStatus } from "@posthog/shared/domain-types";
2+
import { isTerminalStatus } from "@posthog/shared/domain-types";
3+
4+
const PERMANENT_CHANNEL_FEED_FAILURES = new Set([401, 403, 404]);
5+
6+
export function shouldPollChannelFeed(error: unknown): boolean {
7+
if (!error || typeof error !== "object" || !("status" in error)) return true;
8+
const status = (error as { status?: unknown }).status;
9+
return (
10+
typeof status !== "number" || !PERMANENT_CHANNEL_FEED_FAILURES.has(status)
11+
);
12+
}
13+
14+
export function taskFeedRunStatus({
15+
status,
16+
environment,
17+
}: {
18+
status: TaskRunStatus | null | undefined;
19+
environment: string | null | undefined;
20+
}): TaskRunStatus | null {
21+
if (!status) return null;
22+
return environment === "cloud" || isTerminalStatus(status) ? status : null;
23+
}

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

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
GitBranchIcon,
55
RobotIcon,
66
} from "@phosphor-icons/react";
7+
import { taskFeedRunStatus } from "@posthog/core/canvas/channelFeed";
78
import {
89
Avatar,
910
AvatarFallback,
@@ -37,7 +38,6 @@ import {
3738
} from "@posthog/quill";
3839
import { formatRelativeTimeShort, getLocalDayDiff } from "@posthog/shared";
3940
import type { Task, TaskRunStatus } from "@posthog/shared/domain-types";
40-
import { isTerminalStatus } from "@posthog/shared/domain-types";
4141
import { getUserInitials } from "@posthog/ui/features/auth/userInitials";
4242
import { TaskTabIcon } from "@posthog/ui/features/browser-tabs/TaskTabIcon";
4343
import { mentionChipClass } from "@posthog/ui/features/canvas/components/MentionText";
@@ -46,7 +46,6 @@ import { useChannelTaskData } from "@posthog/ui/features/canvas/hooks/useChannel
4646
import { useTaskThread } from "@posthog/ui/features/canvas/hooks/useTaskThread";
4747
import { shouldOpenTaskCardInline } from "@posthog/ui/features/canvas/taskCardNavigation";
4848
import { userDisplayName } from "@posthog/ui/features/canvas/utils/userDisplay";
49-
import { useSessionSelector } from "@posthog/ui/features/sessions/useSession";
5049
import {
5150
type SidebarPrState,
5251
useTaskPrStatus,
@@ -163,21 +162,14 @@ interface TaskStatusDisplay {
163162
// deliberate end state we should not soften with a PR.
164163
function useTaskStatusDisplay(task: Task): TaskStatusDisplay {
165164
const data = useChannelTaskData(task);
166-
const agentSettledAfterRun = useSessionSelector(
167-
task.id,
168-
(session) =>
169-
!!session &&
170-
(session.events?.length ?? 0) > 0 &&
171-
!session.isPromptPending &&
172-
(session.pendingPermissions?.size ?? 0) === 0,
173-
);
174165
const { prState } = useTaskPrStatus({
175166
id: task.id,
176167
cloudPrUrl: data?.cloudPrUrl ?? null,
177168
taskRunEnvironment: data?.taskRunEnvironment ?? null,
178169
});
179170
const status = data?.taskRunStatus ?? task.latest_run?.status;
180171
const environment = data?.taskRunEnvironment ?? task.latest_run?.environment;
172+
const displayStatus = taskFeedRunStatus({ status, environment });
181173
// `prState` is resolved async from git/`gh` and is routinely null for cloud
182174
// tasks (the details fetch hasn't landed, or there's no cached row). But the
183175
// PR URL itself is a hard signal a PR exists — the card's "PR" link keys off
@@ -209,10 +201,8 @@ function useTaskStatusDisplay(task: Task): TaskStatusDisplay {
209201
base = null;
210202
} else if (!status) {
211203
base = <Badge>Draft</Badge>;
212-
} else if (environment === "cloud" || isTerminalStatus(status)) {
213-
const effectiveStatus =
214-
agentSettledAfterRun && !isTerminalStatus(status) ? "completed" : status;
215-
base = statusBadge(effectiveStatus);
204+
} else if (displayStatus) {
205+
base = statusBadge(displayStatus);
216206
} else {
217207
// Local, non-terminal: the run status is unreliable (the backend row stays
218208
// "queued" while the agent runs on the creator's machine), so we render no

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

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,37 @@
1+
import type { ConversationItem } from "@posthog/ui/features/sessions/components/buildConversationItems";
12
import { render, screen } from "@testing-library/react";
23
import { describe, expect, it } from "vitest";
34
import { AgentStatusLine, UserPromptRow } from "./ThreadPanel";
5+
import { agentTurns } from "./threadAgentTurns";
6+
7+
describe("agentTurns", () => {
8+
it("accumulates every text chunk in one agent turn", () => {
9+
const items = [
10+
{
11+
type: "session_update",
12+
id: "first",
13+
timestamp: 10,
14+
update: {
15+
sessionUpdate: "agent_message_chunk",
16+
content: { type: "text", text: "Hello" },
17+
},
18+
},
19+
{
20+
type: "session_update",
21+
id: "second",
22+
timestamp: 20,
23+
update: {
24+
sessionUpdate: "agent_message_chunk",
25+
content: { type: "text", text: " there" },
26+
},
27+
},
28+
] as ConversationItem[];
29+
30+
expect(agentTurns(items)).toEqual([
31+
{ id: "first", text: "Hello there", timestamp: 10 },
32+
]);
33+
});
34+
});
435

536
describe("AgentStatusLine", () => {
637
it("renders working status outside the conversation timeline", () => {

0 commit comments

Comments
 (0)