diff --git a/packages/api-client/src/posthog-client.ts b/packages/api-client/src/posthog-client.ts index 18a2aa6da6..4ac94adb1b 100644 --- a/packages/api-client/src/posthog-client.ts +++ b/packages/api-client/src/posthog-client.ts @@ -52,6 +52,8 @@ import type { ActionabilityJudgmentArtefact, AvailableSuggestedReviewer, AvailableSuggestedReviewersResponse, + ChannelFeedMessage, + ChannelFeedMessageEvent, CodeReferenceArtefact, CommitArtefact, CommitDiffResponse, @@ -2467,6 +2469,56 @@ export class PostHogAPIClient { return (await response.json()) as TaskChannel; } + // A channel's system-announcement feed (context created, CONTEXT.md being + // built), chronological. Durable + team-visible, rendered alongside task cards. + async getChannelFeed(channelId: string): Promise { + const teamId = await this.getTeamId(); + const urlPath = `/api/projects/${teamId}/task_channels/${channelId}/feed/`; + const response = await this.api.fetcher.fetch({ + method: "get", + url: new URL(`${this.api.baseUrl}${urlPath}`), + path: urlPath, + }); + if (!response.ok) { + throw new Error(`Failed to fetch channel feed: ${response.statusText}`); + } + return (await response.json()) as ChannelFeedMessage[]; + } + + // Post a system announcement into a channel's feed. The row is authored by the + // system; the server records the requester as `author` for "Adam …" rendering. + async postChannelFeedMessage( + channelId: string, + input: { + event: ChannelFeedMessageEvent; + payload?: Record; + // Optional explicit timestamp (ISO) so a burst of announcements orders + // deterministically instead of racing on server insert time. + createdAt?: string; + }, + ): Promise { + const teamId = await this.getTeamId(); + const urlPath = `/api/projects/${teamId}/task_channels/${channelId}/feed/`; + const response = await this.api.fetcher.fetch({ + method: "post", + url: new URL(`${this.api.baseUrl}${urlPath}`), + path: urlPath, + overrides: { + body: JSON.stringify({ + event: input.event, + payload: input.payload ?? {}, + ...(input.createdAt ? { created_at: input.createdAt } : {}), + }), + }, + }); + if (!response.ok) { + throw new Error( + `Failed to post channel feed message: ${response.statusText}`, + ); + } + return (await response.json()) as ChannelFeedMessage; + } + // Mentions of the current user across task threads, newest first. async getTaskMentions(options?: { since?: string }): Promise { const teamId = await this.getTeamId(); diff --git a/packages/shared/src/domain-types.ts b/packages/shared/src/domain-types.ts index 358fa23864..5e46d7f947 100644 --- a/packages/shared/src/domain-types.ts +++ b/packages/shared/src/domain-types.ts @@ -79,6 +79,27 @@ export interface TaskChannel { created_by?: UserBasic | null; } +/** Lifecycle events a client may post into a channel's feed. */ +export type ChannelFeedMessageEvent = "context_md_building"; + +/** + * A durable, team-visible "PostHog agent" announcement in a channel's feed — + * rendered alongside task cards (e.g. "Adam created this context"). `author` is + * the user whose action produced the row; `author_kind` says who authored it. + * `payload` carries structured event data (e.g. `{ context_name }`) so rendering + * survives renames. + */ +export interface ChannelFeedMessage { + id: string; + channel: string; + author?: UserBasic | null; + author_kind: "human" | "system" | "agent"; + event: ChannelFeedMessageEvent | string; + payload: Record; + content: string; + created_at: string; +} + /** * One human message in a task's thread. Thread messages never reach the agent * unless the task author forwards one, which stamps the forwarded_* fields. diff --git a/packages/ui/src/features/browser-tabs/BrowserTabStrip.tsx b/packages/ui/src/features/browser-tabs/BrowserTabStrip.tsx index 8b42382a4c..d9c0a55844 100644 --- a/packages/ui/src/features/browser-tabs/BrowserTabStrip.tsx +++ b/packages/ui/src/features/browser-tabs/BrowserTabStrip.tsx @@ -1,6 +1,5 @@ import { BrainIcon, - HashIcon, HouseIcon, PlugsConnectedIcon, RobotIcon, @@ -50,6 +49,7 @@ import { useRouter, useRouterState, } from "@tanstack/react-router"; +import { SquircleDashed } from "lucide-react"; import { type ReactNode, useEffect, useMemo } from "react"; import { useHotkeys } from "react-hotkeys-hook"; import { @@ -519,8 +519,8 @@ export function BrowserTabStrip() { const meta = channelSectionFor(section); return { id: t.id, - label: meta?.label ?? channel ?? "Channel", - icon: , + label: meta?.label ?? channel ?? "Context", + icon: , channelName: channel, // No section meta → the channel's index page. isChannelHome: !meta, diff --git a/packages/ui/src/features/browser-tabs/TabStrip.tsx b/packages/ui/src/features/browser-tabs/TabStrip.tsx index 0d389d8d6b..718ad8045c 100644 --- a/packages/ui/src/features/browser-tabs/TabStrip.tsx +++ b/packages/ui/src/features/browser-tabs/TabStrip.tsx @@ -231,7 +231,7 @@ function SortableTabPill({ repeat the channel-home name already shown above. */} {tab.channelName ? (
- #{tab.channelName} + {tab.channelName} {tab.isChannelHome ? " / home" : null}
) : null} diff --git a/packages/ui/src/features/canvas/components/ActivityView.tsx b/packages/ui/src/features/canvas/components/ActivityView.tsx index 905360c757..e3323ae71c 100644 --- a/packages/ui/src/features/canvas/components/ActivityView.tsx +++ b/packages/ui/src/features/canvas/components/ActivityView.tsx @@ -89,7 +89,7 @@ function ActivityRow({ <> {" in "} - #{item.channelName} + {item.channelName} )} @@ -176,7 +176,7 @@ export function ActivityView() { Activity - Mentions of you across channel threads. + Mentions of you across contexts.
{isLoading && items.length === 0 ? ( diff --git a/packages/ui/src/features/canvas/components/ChannelBreadcrumb.tsx b/packages/ui/src/features/canvas/components/ChannelBreadcrumb.tsx index 9bd0adaa1f..86bd34fbfa 100644 --- a/packages/ui/src/features/canvas/components/ChannelBreadcrumb.tsx +++ b/packages/ui/src/features/canvas/components/ChannelBreadcrumb.tsx @@ -1,4 +1,3 @@ -import { HashIcon } from "@phosphor-icons/react"; import { Button, Tooltip, @@ -8,6 +7,7 @@ import { import { HeaderTitleEditor } from "@posthog/ui/features/task-detail/HeaderTitleEditor"; import { Flex, Text } from "@radix-ui/themes"; import { useNavigate } from "@tanstack/react-router"; +import { SquircleDashed } from "lucide-react"; import { type ReactNode, useState } from "react"; interface ChannelBreadcrumbProps { @@ -48,7 +48,10 @@ export function ChannelBreadcrumb({ const channelSegment = ( <> - + { it.each([ { channelName: "project-bluebird", - expectedTitle: "#project-bluebird CONTEXT.md", + expectedTitle: "project-bluebird CONTEXT.md", }, { channelName: undefined, expectedTitle: "CONTEXT.md" }, ])( diff --git a/packages/ui/src/features/canvas/components/ChannelContextPanel.tsx b/packages/ui/src/features/canvas/components/ChannelContextPanel.tsx index 7b6397e0a8..b5ad42ac86 100644 --- a/packages/ui/src/features/canvas/components/ChannelContextPanel.tsx +++ b/packages/ui/src/features/canvas/components/ChannelContextPanel.tsx @@ -29,7 +29,7 @@ export function ChannelContextPanel({ weight="medium" className="min-w-0 truncate text-gray-12" > - {channelName ? `#${channelName} ` : ""}CONTEXT.md + {channelName ? `${channelName} ` : ""}CONTEXT.md diff --git a/packages/ui/src/features/canvas/components/ChannelsList.tsx b/packages/ui/src/features/canvas/components/ChannelsList.tsx index d6bc3e269e..a228e5dca1 100644 --- a/packages/ui/src/features/canvas/components/ChannelsList.tsx +++ b/packages/ui/src/features/canvas/components/ChannelsList.tsx @@ -2,7 +2,6 @@ import { ChartBarIcon, DotsThreeIcon, FileTextIcon, - HashIcon, LinkIcon, LockSimpleIcon, PencilSimpleIcon, @@ -60,6 +59,7 @@ import { toast } from "@posthog/ui/primitives/toast"; import { track } from "@posthog/ui/shell/analytics"; import { Box, Flex, Text } from "@radix-ui/themes"; import { useNavigate, useRouterState } from "@tanstack/react-router"; +import { SquircleDashed } from "lucide-react"; import { Fragment, type ReactNode, useEffect, useRef, useState } from "react"; import { hostClient } from "../hostClient"; @@ -138,7 +138,7 @@ function useChannelActions(channel: Channel): { channel_id: channel.id, success: false, }); - toast.error("Couldn't delete channel", { + toast.error("Couldn't delete context", { description: error instanceof Error ? error.message : String(error), }); return false; @@ -148,7 +148,7 @@ function useChannelActions(channel: Channel): { const actions: ChannelActionItem[] = [ { key: "star", - label: isStarred ? "Unstar channel" : "Star channel", + label: isStarred ? "Unstar context" : "Star context", icon: , onSelect: () => { track(ANALYTICS_EVENTS.CHANNEL_ACTION, { @@ -167,14 +167,14 @@ function useChannelActions(channel: Channel): { }, { key: "rename", - label: "Rename channel…", + label: "Rename context…", icon: , separatorBefore: true, onSelect: () => setRenameOpen(true), }, { key: "delete", - label: "Delete channel…", + label: "Delete context…", icon: , variant: "destructive", onSelect: () => setConfirmDeleteOpen(true), @@ -334,7 +334,7 @@ function ChannelSection({ channel }: { channel: Channel }) { }} className="w-full min-w-0 justify-start gap-2 data-selected:bg-fill-selected data-selected:text-gray-12" > - + - Delete #{channel.name}? + Delete {channel.name}? - This permanently deletes the channel and can’t be undone. + This permanently deletes the context and can’t be undone.
  • - The channel and its{" "} + The context and its{" "} CONTEXT.md are deleted.
  • - Every canvas saved in this channel is permanently deleted. + Every canvas saved in this context is permanently deleted.
  • - Filed tasks are removed from the channel, but the tasks + Filed tasks are removed from the context, but the tasks themselves are not deleted.
@@ -470,7 +470,7 @@ function ChannelSection({ channel }: { channel: Channel }) { }) } > - Delete channel + Delete context
@@ -505,7 +505,7 @@ function PersonalChannelRow() { params: { channelId: folder.id }, }); } catch (error) { - toast.error("Couldn't open #me", { + toast.error("Couldn't open me", { description: error instanceof Error ? error.message : String(error), }); } @@ -521,7 +521,7 @@ function PersonalChannelRow() { onClick={() => void open()} className="w-full min-w-0 justify-start gap-2 data-selected:bg-fill-selected data-selected:text-gray-12" > - + {PERSONAL_CHANNEL_NAME} @@ -583,8 +583,8 @@ export function ChannelsList() { 0 && "mt-3")}> - - Channels + + Contexts