Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 36 additions & 78 deletions packages/ui/src/features/canvas/components/ChannelFeedView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import {
ChatCircleIcon,
GitBranchIcon,
RobotIcon,
UserIcon,
} from "@phosphor-icons/react";
import {
Avatar,
Expand Down Expand Up @@ -40,13 +39,10 @@ import type { Task, TaskRunStatus } from "@posthog/shared/domain-types";
import { isTerminalStatus } from "@posthog/shared/domain-types";
import { getUserInitials } from "@posthog/ui/features/auth/userInitials";
import { TaskTabIcon } from "@posthog/ui/features/browser-tabs/TaskTabIcon";
import { mentionChipClass } from "@posthog/ui/features/canvas/components/MentionText";
import { useChannelTaskData } from "@posthog/ui/features/canvas/hooks/useChannelTaskData";
import { useTaskThread } from "@posthog/ui/features/canvas/hooks/useTaskThread";
import { userDisplayName } from "@posthog/ui/features/canvas/utils/userDisplay";
import { xmlToPlainText } from "@posthog/ui/features/message-editor/content";
import { CollapsibleMessageContent } from "@posthog/ui/features/sessions/components/session-update/CollapsibleMessageContent";
import { extractChannelContext } from "@posthog/ui/features/sessions/components/session-update/channelContext";
import { getOriginProductMeta } from "@posthog/ui/features/sidebar/components/items/TaskIcon";
import {
type SidebarPrState,
useTaskPrStatus,
Expand Down Expand Up @@ -198,9 +194,8 @@ function useTaskStatusDisplay(task: Task): TaskStatusDisplay {
base = statusBadge(status);
} else {
// Local, non-terminal: the run status is unreliable (the backend row stays
// "queued" while the agent runs on the creator's machine), and the
// environment already shows in the card's meta row ("· Local"), so we
// render no status badge here rather than a redundant "Local" pill.
// "queued" while the agent runs on the creator's machine), so we render no
// status badge rather than a misleading one.
base = null;
}

Expand Down Expand Up @@ -242,59 +237,21 @@ function TaskStatusBadge({ display }: { display: TaskStatusDisplay }) {
);
}

// The prompt as the user typed it: drop the channel CONTEXT.md block the saga
// prepended and flatten the editor XML back to plain text.
function promptText(task: Task): string {
const raw =
extractChannelContext(task.description)?.stripped ?? task.description;
try {
return xmlToPlainText(raw).trim() || task.title;
} catch {
return raw.trim() || task.title;
}
}

// The card's context line, mirroring the storybook feed: who/what kicked the
// task off ("Requested by @Ann" for humans, the origin product otherwise).
function TaskCardOrigin({ task }: { task: Task }) {
const isUserCreated = task.origin_product === "user_created";
const label = isUserCreated
? `Requested by @${userDisplayName(task.created_by)}`
: (getOriginProductMeta(task.origin_product)?.label ?? task.origin_product);
return (
<span className="inline-flex min-w-0 items-center gap-1.5 text-muted-foreground text-xs">
{isUserCreated ? <UserIcon size={12} /> : <RobotIcon size={12} />}
<span className="truncate">{label}</span>
</span>
);
}

// The task the message kicked off, as a card everyone in the channel sees:
// origin + status up top, bold title, then run metadata.
// bold title + status up top, then run metadata.
function TaskCard({ task, onOpen }: { task: Task; onOpen: () => void }) {
const statusDisplay = useTaskStatusDisplay(task);
const prUrl =
typeof task.latest_run?.output?.pr_url === "string"
? task.latest_run.output.pr_url
: undefined;
// The repository renders separately with its icon; `meta` is the plain-text
// remainder of the row.
const environment = task.latest_run?.environment;
const meta = [
task.slug || null,
task.latest_run?.stage ?? null,
environment === "cloud"
? "Cloud"
: environment === "local"
? "Local"
: null,
].filter(Boolean) as string[];
const stage = task.latest_run?.stage;

return (
<Card
size="sm"
className={cn(
"mt-1.5 w-full max-w-[820px] cursor-pointer rounded-sm py-0 transition-none hover:bg-fill-hover",
"mt-1.5 w-full cursor-pointer rounded-sm py-0 transition-none hover:bg-fill-hover",
statusDisplay.isMerged
? "border-transparent bg-(--purple-a2) shadow-[0_0_0_1px_var(--purple-8)] hover:bg-(--purple-a3) dark:bg-(--purple-a1) dark:hover:bg-(--purple-a2)"
: "hover:border-border-primary",
Expand All @@ -303,29 +260,28 @@ function TaskCard({ task, onOpen }: { task: Task; onOpen: () => void }) {
>
<CardContent className="flex flex-col gap-1 py-2.5">
<div className="flex items-center justify-between gap-2">
<TaskCardOrigin task={task} />
<div className="flex min-w-0 items-center gap-1.5">
{/* Same live status icon as the code side nav, so the card and the
nav never disagree (generating spinner, needs-permission, cloud
status colors, PR state). */}
<TaskTabIcon task={task} size={14} />
<Text size="2" weight="medium" className="line-clamp-2">
{task.title || "Untitled task"}
</Text>
</div>
<TaskStatusBadge display={statusDisplay} />
</div>
<div className="flex min-w-0 items-center gap-1.5">
{/* Same live status icon as the code side nav, so the card and the
nav never disagree (generating spinner, needs-permission, cloud
status colors, PR state). */}
<TaskTabIcon task={task} size={14} />
<Text size="2" weight="medium" className="line-clamp-2">
{task.title || "Untitled task"}
</Text>
</div>
{(meta.length > 0 || task.repository || prUrl) && (
{(stage || task.repository || prUrl) && (
<div className="flex min-w-0 items-center gap-3">
{task.repository && (
<span className="inline-flex items-center gap-1 text-muted-foreground text-xs">
<GitBranchIcon size={12} />
{task.repository}
</span>
)}
Comment on lines 277 to 281

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Local Runtime Indicator Disappears

When a local run is non-terminal, useTaskStatusDisplay() returns no badge because the backend can keep the run at queued while it is running on the creator's machine. This change also removes the Local meta text, so a newly started local task with no stage, repository, or PR renders with no status or runtime cue in the channel feed.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

{meta.length > 0 && (
{stage && (
<Text size="1" className="truncate text-muted-foreground">
{meta.join(" · ")}
{stage}
</Text>
)}
{prUrl && (
Expand Down Expand Up @@ -422,40 +378,42 @@ const FeedItem = memo(function FeedItem({
onOpenTask: (task: Task) => void;
onOpenThread: (task: Task) => void;
}) {
const prompt = useMemo(() => promptText(task), [task]);
const isAgent = !task.created_by || task.origin_product !== "user_created";

return (
<ThreadItem className="rounded-none py-4 pr-8 hover:bg-fill-hover/50">
<ThreadItemGutter>
<Avatar>
<AvatarFallback>
{isAgent && !task.created_by ? (
<RobotIcon size={16} />
) : (
getUserInitials(task.created_by)
)}
<RobotIcon size={16} />
</AvatarFallback>
</Avatar>
</ThreadItemGutter>

<ThreadItemContent className="min-w-0">
<ThreadItemHeader>
<ThreadItemAuthor>
{task.created_by ? userDisplayName(task.created_by) : "Agent"}
</ThreadItemAuthor>
{isAgent && <Badge variant="info">Agent</Badge>}
<ThreadItemAuthor>PostHog</ThreadItemAuthor>
<Badge variant="info">Agent</Badge>
<ThreadItemTimestamp
dateTime={new Date(task.created_at).toISOString()}
>
{formatRelativeTimeShort(task.created_at)}
</ThreadItemTimestamp>
</ThreadItemHeader>

<ThreadItemBody>
<CollapsibleMessageContent contentClassName="wrap-break-word whitespace-pre-wrap">
{prompt}
</CollapsibleMessageContent>
<ThreadItemBody className="wrap-break-word">
{/* Only attribute channel-started tasks: other origins (Slack,
automations) carry a created_by who didn't start it here. */}
{task.origin_product === "user_created" && task.created_by ? (
<>
{/* Mention-styled but rendered inert: the starter shouldn't be
notified about their own task. */}
<span className={mentionChipClass}>
@{userDisplayName(task.created_by)}
</span>{" "}
started a new task
</>
) : (
"A new task was started"
)}
</ThreadItemBody>

<TaskCard task={task} onOpen={() => onOpenTask(task)} />
Expand Down
14 changes: 10 additions & 4 deletions packages/ui/src/features/canvas/components/MentionText.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ type RenderSegment =
| { type: "link"; text: string; href: string }
| { type: "mention"; name: string; email: string };

// The plain (not-the-viewer) mention chip look, also used by surfaces that
// render a mention-styled name without real mention semantics (e.g. the
// channel feed's "started a new task" row).
export const mentionChipClass =
"rounded px-0.5 font-medium text-[var(--accent-11)]";

/**
* Thread message content with inline mention tokens rendered as highlighted
* `@Name` chips (a mention of the viewer gets the stronger treatment) and
Expand Down Expand Up @@ -52,11 +58,11 @@ export function MentionText({
return (
<span
key={key}
className={`rounded px-0.5 font-medium ${
className={
selfEmail && segment.email.toLowerCase() === selfEmail
? "bg-[var(--accent-a4)] text-[var(--accent-12)]"
: "text-[var(--accent-11)]"
}`}
? "rounded bg-[var(--accent-a4)] px-0.5 font-medium text-[var(--accent-12)]"
: mentionChipClass
}
title={segment.email}
>
@{segment.name}
Expand Down
Loading