Skip to content

Commit 56f1157

Browse files
adamleithpclaude
andcommitted
feat(canvas): experimental thread/feed/inbox tweaks
- Remove the "Create PR"/"Draft PR" ship actions from the open thread's agent row (the status chip still reflects PR/ship state). - Feed task card now agrees with the thread: a settled live session downgrades a stale non-terminal cloud run status to "Ready" instead of a lingering "In progress". - Add a placeholder "Inbox" tab to the channel tab strip with a live pipeline-report count badge (no destination yet; clicking is a no-op). - Minor spacing/mention-link tweaks in the feed and mention rendering. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 3676950 commit 56f1157

5 files changed

Lines changed: 49 additions & 56 deletions

File tree

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

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ import { useChannelTaskData } from "@posthog/ui/features/canvas/hooks/useChannel
6060
import { useTaskThread } from "@posthog/ui/features/canvas/hooks/useTaskThread";
6161
import { useThreadPanelStore } from "@posthog/ui/features/canvas/stores/threadPanelStore";
6262
import { userDisplayName } from "@posthog/ui/features/canvas/utils/userDisplay";
63+
import { useSessionSelector } from "@posthog/ui/features/sessions/useSession";
6364
import {
6465
type SidebarPrState,
6566
useTaskPrStatus,
@@ -176,6 +177,20 @@ interface TaskStatusDisplay {
176177
// deliberate end state we should not soften with a PR.
177178
function useTaskStatusDisplay(task: Task): TaskStatusDisplay {
178179
const data = useChannelTaskData(task);
180+
// A cloud run's status lingers on "in_progress" after the agent yields its
181+
// turn — the same staleness the thread's "Ready to ship" sidesteps by reading
182+
// the live session instead. Once the local session has produced output and
183+
// settled (not generating, not blocked on a permission), trust that the agent
184+
// has finished so the card and the thread agree. Gated on real activity so a
185+
// not-yet-started task (empty session) isn't mislabeled "Ready".
186+
const agentSettledAfterRun = useSessionSelector(
187+
task.id,
188+
(s) =>
189+
!!s &&
190+
(s.events?.length ?? 0) > 0 &&
191+
!s.isPromptPending &&
192+
(s.pendingPermissions?.size ?? 0) === 0,
193+
);
179194
const { prState } = useTaskPrStatus({
180195
id: task.id,
181196
cloudPrUrl: data?.cloudPrUrl ?? null,
@@ -215,7 +230,12 @@ function useTaskStatusDisplay(task: Task): TaskStatusDisplay {
215230
} else if (!status) {
216231
base = <Badge>Draft</Badge>;
217232
} else if (environment === "cloud" || isTerminalStatus(status)) {
218-
base = statusBadge(status);
233+
// Prefer the settled live session over a stale non-terminal cloud status, so
234+
// a finished run reads "Ready" instead of a lingering "In progress". Never
235+
// softens a terminal status (a failed run stays failed).
236+
const effective =
237+
agentSettledAfterRun && !isTerminalStatus(status) ? "completed" : status;
238+
base = statusBadge(effective);
219239
} else {
220240
// Local, non-terminal: the run status is unreliable (the backend row stays
221241
// "queued" while the agent runs on the creator's machine), so we render no
@@ -403,7 +423,7 @@ const FeedItem = memo(function FeedItem({
403423
onOpenThread: (task: Task) => void;
404424
}) {
405425
return (
406-
<ThreadItem className="rounded-none py-1 pr-8 hover:bg-fill-hover/50">
426+
<ThreadItem className="rounded-none py-1 pr-8 pl-0 hover:bg-fill-hover/50">
407427
<ThreadItemGutter>
408428
<Avatar>
409429
<AvatarFallback>

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

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
import { Button, cn } from "@posthog/quill";
1+
import { INBOX_PIPELINE_STATUS_FILTER } from "@posthog/core/inbox/reportFiltering";
2+
import { Badge, Button, cn } from "@posthog/quill";
23
import { CHANNEL_SECTIONS } from "@posthog/ui/features/canvas/channelSections";
34
import { ChannelPinnedMenu } from "@posthog/ui/features/canvas/components/ChannelPinnedMenu";
5+
import { useInboxReports } from "@posthog/ui/features/inbox/hooks/useInboxReports";
46
import { Link, useRouterState } from "@tanstack/react-router";
57

68
const TABS = CHANNEL_SECTIONS.map((s) => ({
@@ -13,9 +15,27 @@ const TABS = CHANNEL_SECTIONS.map((s) => ({
1315
// codebase's convention) rather than Link's activeProps.
1416
export function ChannelTabs({ channelId }: { channelId: string }) {
1517
const pathname = useRouterState({ select: (s) => s.location.pathname });
18+
// Live report count for the Inbox tab's badge — pipeline reports still needing
19+
// attention (the same status set the real Inbox badges), not every report ever
20+
// (which includes resolved/suppressed history). A cheap `limit: 1` count query
21+
// returns the real total without pulling the list. The tab itself is a
22+
// placeholder (no destination yet) — clicking is a no-op.
23+
const { data: inbox } = useInboxReports({
24+
status: INBOX_PIPELINE_STATUS_FILTER,
25+
limit: 1,
26+
});
27+
const inboxCount = inbox?.count ?? 0;
1628

1729
return (
1830
<nav className="flex items-center gap-px">
31+
<Button type="button" variant="default" size="sm">
32+
Inbox
33+
{inboxCount > 0 && (
34+
<Badge variant="info" className="ms-0.5">
35+
{inboxCount}
36+
</Badge>
37+
)}
38+
</Button>
1939
{TABS.map((tab) => {
2040
const href = tab.to.replace("$channelId", channelId);
2141
const active = pathname === href;

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -633,7 +633,7 @@ export function ChannelsList() {
633633
open={starredOpen}
634634
onOpenChange={setStarredOpen}
635635
>
636-
<div className="pl-2">
636+
<div>
637637
{starred.map((channel) => (
638638
<ChannelSection key={channel.id} channel={channel} />
639639
))}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ export function MentionText({
102102
}, [content, markdownLinks]);
103103
const selfEmail = currentUserEmail?.toLowerCase();
104104
const router = useRouter();
105-
const linkClass = "bg-info/50 px-0.5 rounded-xs";
105+
const linkClass = "bg-info/50 px-0.5 rounded-xs hover:bg-info/80";
106106
return (
107107
<Text size="1" className={className}>
108108
{segments.map(({ segment, key }) => {

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

Lines changed: 4 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import {
22
ArrowSquareOutIcon,
33
CaretRightIcon,
44
DotsThreeIcon,
5-
GitPullRequestIcon,
65
PaperPlaneRightIcon,
76
RobotIcon,
87
TrashIcon,
@@ -235,25 +234,17 @@ function AgentStatusChip({ status }: { status: AgentStatus }) {
235234

236235
// One agent turn as its own thread item — a full avatar/"Agent" header row with
237236
// a timestamp, the turn's final message, and (on the current turn) the live
238-
// status chip plus ship actions. The live turn's bubble streams; settled turns
239-
// render static and stay put as new turns arrive below.
237+
// status chip. The live turn's bubble streams; settled turns render static and
238+
// stay put as new turns arrive below.
240239
function AgentTurnRow({
241240
message,
242241
status,
243242
streaming,
244-
showActions,
245-
hasPr,
246-
onCreatePr,
247-
onDraftPr,
248243
}: {
249244
message?: AgentMessage;
250245
/** The live status chip; only the current turn passes one. */
251246
status?: AgentStatus;
252247
streaming: boolean;
253-
showActions: boolean;
254-
hasPr: boolean;
255-
onCreatePr: () => void;
256-
onDraftPr: () => void;
257248
}) {
258249
return (
259250
<ThreadItem>
@@ -283,17 +274,6 @@ function AgentTurnRow({
283274
</div>
284275
</ThreadItemBody>
285276
)}
286-
{showActions && !hasPr && (
287-
<div className="pbs-1 flex flex-wrap items-center gap-1.5">
288-
<Button variant="primary" size="sm" onClick={onCreatePr}>
289-
<GitPullRequestIcon size={13} />
290-
Create PR
291-
</Button>
292-
<Button variant="outline" size="sm" onClick={onDraftPr}>
293-
Draft PR
294-
</Button>
295-
</div>
296-
)}
297277
</ThreadItemContent>
298278
</ThreadItem>
299279
);
@@ -411,7 +391,7 @@ function ThreadConversation({
411391
useSessionConnection({ taskId, task, session, repoPath, isCloud });
412392
const { items } = useConversationItems(events, isPromptPending);
413393
const pendingPermissions = usePendingPermissionsForTask(taskId);
414-
const { handleSendPrompt } = useSessionCallbacks({
394+
useSessionCallbacks({
415395
taskId,
416396
task,
417397
session,
@@ -455,22 +435,6 @@ function ThreadConversation({
455435
prUrl,
456436
]);
457437

458-
// Shipping a PR from a channel thread means asking the agent to open it — the
459-
// uniform path that works for cloud and local runs alike (the agent owns the
460-
// branch/commit). Re-engages the session, so the status flips back to active.
461-
const askAgentToOpenPr = (draftPr: boolean) => {
462-
void handleSendPrompt(
463-
draftPr
464-
? "Create a draft pull request with these changes."
465-
: "Create a pull request with these changes.",
466-
);
467-
toast.success(
468-
draftPr
469-
? "Asked the agent to open a draft PR"
470-
: "Asked the agent to open a PR",
471-
);
472-
};
473-
474438
// User prompts, human thread messages, and agent turns woven into one
475439
// chronological list. Ties keep insertion order (prompts, then humans, then
476440
// agents); an agent turn with no timestamp yet (just started, still streaming)
@@ -661,10 +625,6 @@ function ThreadConversation({
661625
row.message.id === lastAgentId &&
662626
agentStatus?.phase === "active"
663627
}
664-
showActions={false}
665-
hasPr={!!prUrl}
666-
onCreatePr={() => askAgentToOpenPr(false)}
667-
onDraftPr={() => askAgentToOpenPr(true)}
668628
/>
669629
),
670630
)}
@@ -674,14 +634,7 @@ function ThreadConversation({
674634
PR" action above it, out of view. Once a PR exists the agent's
675635
"Done" message already reflects it, so the footer is dropped. */}
676636
{agentStatus && !(agentStatus.phase === "complete" && !!prUrl) && (
677-
<AgentTurnRow
678-
status={agentStatus}
679-
streaming={false}
680-
showActions={agentStatus.phase === "complete"}
681-
hasPr={!!prUrl}
682-
onCreatePr={() => askAgentToOpenPr(false)}
683-
onDraftPr={() => askAgentToOpenPr(true)}
684-
/>
637+
<AgentTurnRow status={agentStatus} streaming={false} />
685638
)}
686639
</ThreadItemGroup>
687640
)}

0 commit comments

Comments
 (0)