From 1ee5fb282916644c9e12d7865aa288a2cf84831d Mon Sep 17 00:00:00 2001 From: Raquel Smith Date: Tue, 30 Jun 2026 09:27:57 -0700 Subject: [PATCH 1/5] feat(channels): add recents + pinned to channel home MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reorder the channel home page so the prompt box sits above the starter suggestions, and add a two-column glance below them: recent tasks on the left (5 most recently active, with the shared task status icons and an "All tasks →" link to the Recents tab) and pinned artifacts on the right (5 most recently pinned canvases). Typing in the prompt box fades out the suggestions and lists so the composer has focus. Generated-By: PostHog Code Task-Id: b1d3004b-eca2-40c5-b30a-7d8db3f22573 --- .../canvas/components/ChannelHomeComposer.tsx | 15 +- .../canvas/components/WebsiteChannelHome.tsx | 262 ++++++++++++++++-- 2 files changed, 251 insertions(+), 26 deletions(-) diff --git a/packages/ui/src/features/canvas/components/ChannelHomeComposer.tsx b/packages/ui/src/features/canvas/components/ChannelHomeComposer.tsx index d46596a043..069129a9e0 100644 --- a/packages/ui/src/features/canvas/components/ChannelHomeComposer.tsx +++ b/packages/ui/src/features/canvas/components/ChannelHomeComposer.tsx @@ -35,6 +35,9 @@ interface ChannelHomeComposerProps { channelName?: string; /** Channel CONTEXT.md, attached to the created task as background. */ channelContext?: string; + /** Fires as the editor goes empty ⇄ non-empty, so the home page can fade out + * its suggestions / lists while the user is typing. */ + onEmptyChange?: (isEmpty: boolean) => void; onTaskCreated: (task: Task) => void; } @@ -48,7 +51,7 @@ export const ChannelHomeComposer = forwardRef< ChannelHomeComposerHandle, ChannelHomeComposerProps >(function ChannelHomeComposer( - { channelId, channelName, channelContext, onTaskCreated }, + { channelId, channelName, channelContext, onEmptyChange, onTaskCreated }, ref, ) { const sessionId = `channel-home:${channelId}`; @@ -56,6 +59,14 @@ export const ChannelHomeComposer = forwardRef< const [editorIsEmpty, setEditorIsEmpty] = useState(true); const { isOnline } = useConnectivity(); + const handleEmptyChange = useCallback( + (isEmpty: boolean) => { + setEditorIsEmpty(isEmpty); + onEmptyChange?.(isEmpty); + }, + [onEmptyChange], + ); + const { lastUsedAdapter, setLastUsedAdapter, @@ -225,7 +236,7 @@ export const ChannelHomeComposer = forwardRef< /> ) } - onEmptyChange={setEditorIsEmpty} + onEmptyChange={handleEmptyChange} onSubmitClick={handleSubmit} onSubmit={() => { if (canSubmit) handleSubmit(); diff --git a/packages/ui/src/features/canvas/components/WebsiteChannelHome.tsx b/packages/ui/src/features/canvas/components/WebsiteChannelHome.tsx index 1bf83a6942..0000ab3977 100644 --- a/packages/ui/src/features/canvas/components/WebsiteChannelHome.tsx +++ b/packages/ui/src/features/canvas/components/WebsiteChannelHome.tsx @@ -1,26 +1,43 @@ +import { ArrowRightIcon, CaretRightIcon } from "@phosphor-icons/react"; +import type { DashboardSummary } from "@posthog/core/canvas/dashboardSchemas"; +import { cn } from "@posthog/quill"; +import { formatRelativeTimeShort } from "@posthog/shared"; import { ANALYTICS_EVENTS } from "@posthog/shared/analytics-events"; import type { Task } from "@posthog/shared/domain-types"; +import { useArchivedTaskIds } from "@posthog/ui/features/archive/useArchivedTaskIds"; +import { TaskTabIcon } from "@posthog/ui/features/browser-tabs/TaskTabIcon"; import { CHANNEL_TASK_SUGGESTIONS } from "@posthog/ui/features/canvas/channelTaskSuggestions"; import { ChannelHeader } from "@posthog/ui/features/canvas/components/ChannelHeader"; import { ChannelHomeComposer, type ChannelHomeComposerHandle, } from "@posthog/ui/features/canvas/components/ChannelHomeComposer"; +import { iconForTemplate } from "@posthog/ui/features/canvas/components/canvasTemplateIcon"; import { useChannels } from "@posthog/ui/features/canvas/hooks/useChannels"; -import { useChannelTaskMutations } from "@posthog/ui/features/canvas/hooks/useChannelTasks"; +import { + useChannelTaskMutations, + useChannelTasks, +} from "@posthog/ui/features/canvas/hooks/useChannelTasks"; +import { useDashboards } from "@posthog/ui/features/canvas/hooks/useDashboards"; import { useFolderInstructions } from "@posthog/ui/features/canvas/hooks/useFolderInstructions"; import { SuggestedPromptCard } from "@posthog/ui/features/task-detail/components/SuggestedPromptCard"; import { taskDetailQuery } from "@posthog/ui/features/tasks/queries"; +import { useTasks } from "@posthog/ui/features/tasks/useTasks"; import { useSetHeaderContent } from "@posthog/ui/hooks/useSetHeaderContent"; import { toast } from "@posthog/ui/primitives/toast"; import { track } from "@posthog/ui/shell/analytics"; import { Text } from "@radix-ui/themes"; import { useQueryClient } from "@tanstack/react-query"; -import { useNavigate } from "@tanstack/react-router"; -import { useCallback, useMemo, useRef } from "react"; +import { Link, useNavigate } from "@tanstack/react-router"; +import { useCallback, useMemo, useRef, useState } from "react"; + +const RECENT_TASK_LIMIT = 5; +const PINNED_ARTIFACT_LIMIT = 5; -// A channel's homepage: a heading and a composer that files new tasks into the -// channel. The channel's tasks + canvases live behind the "History" tab. +// A channel's homepage: a heading, the composer that files new tasks into the +// channel, the starter-prompt suggestions, and a two-column glance at the +// channel's recent tasks and pinned artifacts. The full lists live behind the +// "Recents" and "Artifacts" tabs. export function WebsiteChannelHome({ channelId }: { channelId: string }) { const navigate = useNavigate(); const queryClient = useQueryClient(); @@ -36,6 +53,9 @@ export function WebsiteChannelHome({ channelId }: { channelId: string }) { ); const composerRef = useRef(null); + // While the user is typing, dim the suggestions + lists so the focus stays on + // the prompt box. + const [composerEmpty, setComposerEmpty] = useState(true); const handleSuggestionSelect = useCallback( (prompt: string, mode?: string) => { @@ -79,7 +99,7 @@ export function WebsiteChannelHome({ channelId }: { channelId: string }) { return (
-
+

What can I do for you today? @@ -89,32 +109,226 @@ export function WebsiteChannelHome({ channelId }: { channelId: string }) {

- {/* Starter prompts, always shown directly above the box. */} -
- - Suggestions - -
- {CHANNEL_TASK_SUGGESTIONS.map((suggestion) => ( - - handleSuggestionSelect(suggestion.prompt, suggestion.mode) - } - /> - ))} -
-
- + + {/* Suggestions + recent/pinned glance. All fade out together while the + user is typing so the prompt box has the floor. */} +
+
+ + Suggestions + +
+ {CHANNEL_TASK_SUGGESTIONS.map((suggestion) => ( + + handleSuggestionSelect(suggestion.prompt, suggestion.mode) + } + /> + ))} +
+
+ +
+ + +
+
); } + +// Left column: the channel's most recently active tasks, with the shared task +// status icons. "All tasks" jumps to the Recents tab. +function RecentTasksColumn({ channelId }: { channelId: string }) { + const navigate = useNavigate(); + const { tasks: filedTasks } = useChannelTasks(channelId); + const { data: tasks } = useTasks(); + const archivedTaskIds = useArchivedTaskIds(); + + const recentTasks = useMemo(() => { + const taskById = new Map(tasks?.map((t) => [t.id, t]) ?? []); + return filedTasks + .flatMap((f) => { + const task = taskById.get(f.taskId); + if (!task || archivedTaskIds.has(f.taskId)) return []; + return [{ task, ts: Date.parse(task.updated_at) || 0 }]; + }) + .sort((a, b) => b.ts - a.ts) + .slice(0, RECENT_TASK_LIMIT); + }, [filedTasks, tasks, archivedTaskIds]); + + const openTask = useCallback( + (taskId: string) => { + track(ANALYTICS_EVENTS.CHANNEL_ACTION, { + action_type: "open_task", + surface: "channel_home", + channel_id: channelId, + task_id: taskId, + }); + void navigate({ + to: "/website/$channelId/tasks/$taskId", + params: { channelId, taskId }, + }); + }, + [channelId, navigate], + ); + + return ( + + All tasks + + + } + empty={recentTasks.length === 0 ? "No tasks yet" : undefined} + > + {recentTasks.map(({ task, ts }) => ( + } + title={task.title || "Untitled task"} + subtitle={formatRelativeTimeShort(ts)} + onClick={() => openTask(task.id)} + /> + ))} + + ); +} + +// Right column: the channel's pinned canvases, most recently pinned first. No +// dedicated page yet, so this is the only surface for them. +function PinnedArtifactsColumn({ channelId }: { channelId: string }) { + const navigate = useNavigate(); + const { dashboards } = useDashboards(channelId); + + const pinned = useMemo( + () => + dashboards + .filter((d: DashboardSummary) => d.pinnedAt != null) + .sort((a, b) => (b.pinnedAt ?? 0) - (a.pinnedAt ?? 0)) + .slice(0, PINNED_ARTIFACT_LIMIT), + [dashboards], + ); + + const openCanvas = useCallback( + (dashboardId: string) => { + track(ANALYTICS_EVENTS.CHANNEL_ACTION, { + action_type: "open_artifact", + surface: "channel_home", + channel_id: channelId, + }); + void navigate({ + to: "/website/$channelId/dashboards/$dashboardId", + params: { channelId, dashboardId }, + }); + }, + [channelId, navigate], + ); + + return ( + + {pinned.map((d) => ( + openCanvas(d.id)} + /> + ))} + + ); +} + +function ColumnShell({ + title, + action, + empty, + children, +}: { + title: string; + action?: React.ReactNode; + empty?: string; + children: React.ReactNode; +}) { + return ( +
+
+ + {title} + + {action} +
+ {empty ? ( + {empty} + ) : ( +
{children}
+ )} +
+ ); +} + +function ListRow({ + icon, + title, + subtitle, + onClick, +}: { + icon: React.ReactNode; + title: string; + subtitle: string; + onClick: () => void; +}) { + return ( + + ); +} From c195f7c1599195c47d047b44811ad1628251a175 Mon Sep 17 00:00:00 2001 From: Raquel Smith Date: Tue, 30 Jun 2026 09:49:42 -0700 Subject: [PATCH 2/5] fix(channels): address review feedback on channel home - Gate the "No tasks yet" / "No pinned artifacts yet" empty states on the underlying queries' loading flags so they don't flash before data arrives. - Include `dashboard_id` on the open_artifact analytics event (add the optional field to ChannelActionProperties). - Mark the faded suggestions/lists container `inert` while typing so its links/buttons leave the keyboard tab order, complementing aria-hidden. Generated-By: PostHog Code Task-Id: b1d3004b-eca2-40c5-b30a-7d8db3f22573 --- packages/shared/src/analytics-events.ts | 2 ++ .../canvas/components/WebsiteChannelHome.tsx | 21 ++++++++++++++----- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/packages/shared/src/analytics-events.ts b/packages/shared/src/analytics-events.ts index b174c15a1f..cc3a311c0b 100644 --- a/packages/shared/src/analytics-events.ts +++ b/packages/shared/src/analytics-events.ts @@ -827,6 +827,8 @@ export interface ChannelActionProperties { channel_id?: string; /** For file/unfile/archive/open task actions. */ task_id?: string; + /** For open_artifact when the artifact is a canvas. */ + dashboard_id?: string; /** For file_task: destination channel when different from `channel_id`. */ target_channel_id?: string; /** For nav_click: which destination ("home"|"inbox"|"canvas"|"agents"|"files"|"settings"). */ diff --git a/packages/ui/src/features/canvas/components/WebsiteChannelHome.tsx b/packages/ui/src/features/canvas/components/WebsiteChannelHome.tsx index 0000ab3977..036cc582c0 100644 --- a/packages/ui/src/features/canvas/components/WebsiteChannelHome.tsx +++ b/packages/ui/src/features/canvas/components/WebsiteChannelHome.tsx @@ -126,6 +126,7 @@ export function WebsiteChannelHome({ channelId }: { channelId: string }) { !composerEmpty && "pointer-events-none opacity-0", )} aria-hidden={!composerEmpty} + inert={!composerEmpty || undefined} >
@@ -158,8 +159,9 @@ export function WebsiteChannelHome({ channelId }: { channelId: string }) { // status icons. "All tasks" jumps to the Recents tab. function RecentTasksColumn({ channelId }: { channelId: string }) { const navigate = useNavigate(); - const { tasks: filedTasks } = useChannelTasks(channelId); - const { data: tasks } = useTasks(); + const { tasks: filedTasks, isLoading: filedLoading } = + useChannelTasks(channelId); + const { data: tasks, isLoading: tasksLoading } = useTasks(); const archivedTaskIds = useArchivedTaskIds(); const recentTasks = useMemo(() => { @@ -203,7 +205,11 @@ function RecentTasksColumn({ channelId }: { channelId: string }) { } - empty={recentTasks.length === 0 ? "No tasks yet" : undefined} + empty={ + !filedLoading && !tasksLoading && recentTasks.length === 0 + ? "No tasks yet" + : undefined + } > {recentTasks.map(({ task, ts }) => ( @@ -239,6 +245,7 @@ function PinnedArtifactsColumn({ channelId }: { channelId: string }) { action_type: "open_artifact", surface: "channel_home", channel_id: channelId, + dashboard_id: dashboardId, }); void navigate({ to: "/website/$channelId/dashboards/$dashboardId", @@ -251,7 +258,11 @@ function PinnedArtifactsColumn({ channelId }: { channelId: string }) { return ( {pinned.map((d) => ( Date: Tue, 30 Jun 2026 11:43:17 -0700 Subject: [PATCH 3/5] feat(channels): category chips + context indicator on channel home Declutter the channel home: collapse the suggestions grid into four centered category chips (Code, Analysis, Debug, Canvas) below the prompt box. Clicking a chip crossfades the chips + recents out and an action list in, with each action showing its icon, title, and a muted inline description, plus a back button to return to the chips. The recents and pinned columns hide alongside the chips while a category is open. Also restore the channel CONTEXT.md indicator that was lost in the home redesign: when a channel has a context doc, the composer now shows the "Using: #channel CONTEXT.md" chip (with dismiss) it had on the new-task page. Generated-By: PostHog Code Task-Id: b9cd812b-a600-4565-87d0-f5b9f195ea14 --- packages/shared/src/analytics-events.ts | 4 + .../features/canvas/channelTaskSuggestions.ts | 228 +++++++++++------ .../canvas/components/ChannelHomeComposer.tsx | 41 +++- .../canvas/components/WebsiteChannelHome.tsx | 230 ++++++++++++++++-- 4 files changed, 406 insertions(+), 97 deletions(-) diff --git a/packages/shared/src/analytics-events.ts b/packages/shared/src/analytics-events.ts index cc3a311c0b..821df4057c 100644 --- a/packages/shared/src/analytics-events.ts +++ b/packages/shared/src/analytics-events.ts @@ -809,6 +809,7 @@ export type ChannelActionType = | "unstar" | "edit_context_open" | "new_task_open" + | "select_suggestion_category" | "new_task_suggestion" | "view_context" | "view_history" @@ -833,6 +834,9 @@ export interface ChannelActionProperties { target_channel_id?: string; /** For nav_click: which destination ("home"|"inbox"|"canvas"|"agents"|"files"|"settings"). */ nav_target?: string; + /** For select_suggestion_category / new_task_suggestion: the suggestion + * category id (e.g. "code", "analysis", "debug", "canvas"). */ + category?: string; /** For new_task_suggestion: the starter-prompt card label. */ suggestion_label?: string; /** Whether the underlying mutation resolved successfully. */ diff --git a/packages/ui/src/features/canvas/channelTaskSuggestions.ts b/packages/ui/src/features/canvas/channelTaskSuggestions.ts index 5529b1447e..6cf905dd92 100644 --- a/packages/ui/src/features/canvas/channelTaskSuggestions.ts +++ b/packages/ui/src/features/canvas/channelTaskSuggestions.ts @@ -2,92 +2,182 @@ import { Bug, ChartBar, ChartLine, + ChartPieSlice, ChatCircleText, + Code, Cube, CurrencyDollar, Flask, + type Icon, + SquaresFour, + TrendDown, + WarningOctagon, Wrench, } from "@phosphor-icons/react"; import type { SuggestedPrompt } from "@posthog/ui/features/task-detail/components/SuggestedPromptCard"; -// Starter prompts shown as cards on the channels (project-bluebird) new-task -// screen. Clicking a card drops its `prompt` into the composer, ready to -// edit/send. Each prompt ends with a "User input:" block of fill-in lines the -// user completes before sending. Channels-only — the /code new-task screen -// keeps its discovery suggestions. Card styling mirrors SuggestedTaskCard -// (icon badge + title + description); the icon/color follow the same -// `var(---N)` token scheme. -export const CHANNEL_TASK_SUGGESTIONS: SuggestedPrompt[] = [ +// Starter prompts for the channels (project-bluebird) new-task surfaces. Picking +// one drops its `prompt` into the composer, ready to edit/send. Each prompt ends +// with a "User input:" block of fill-in lines the user completes before sending. +// Channels-only — the /code new-task screen keeps its discovery suggestions. +// +// On the channel home these are grouped behind the category chips below; the +// flat CHANNEL_TASK_SUGGESTIONS list (derived at the bottom) still feeds the +// new-task screen's card grid. +export interface SuggestionCategory { + id: string; + /** Chip label shown on the channel home. */ + label: string; + icon: Icon; + /** Radix color token base (`var(---N)`) for the chip + row icons. */ + color: string; + suggestions: SuggestedPrompt[]; +} + +export const CHANNEL_SUGGESTION_CATEGORIES: SuggestionCategory[] = [ { - label: "Debug a user issue", - description: "Trace a specific user's events, replays, and errors", - icon: Bug, - color: "red", - mode: "auto", - prompt: - "Help me debug an issue a specific user is hitting. Pull their recent events, session replays, and errors, then figure out what went wrong.\n\n\nUser input:\n- Describe the user issue:\n- User identifier (distinct ID, email address, etc):", + id: "code", + label: "Code", + icon: Code, + color: "orange", + suggestions: [ + { + label: "Fix a bug", + description: "Track down and fix a problem in the code", + icon: Wrench, + color: "orange", + mode: "plan", + prompt: + "Help me fix a bug — track down the root cause in the code and implement a fix. Open a PR if appropriate.\n\n\nUser input:\n- Describe the bug / what's going wrong:\n- Steps to reproduce (optional):\n- Where it happens (file, page, area — optional):", + }, + { + label: "Build a new feature", + description: "Design and implement something new", + icon: Cube, + color: "teal", + mode: "plan", + prompt: + "Help me build a new feature — propose an approach, then implement it. Open a PR if appropriate.\n\n\nUser input:\n- Describe the feature you want:\n- Any constraints or requirements (optional):", + }, + ], }, { - label: "Run a feature analysis", - description: "Adoption, engagement, and retention of a feature", + id: "analysis", + label: "Analysis", icon: ChartLine, color: "blue", - mode: "auto", - prompt: - "Analyze how a feature is performing — adoption, engagement, and retention of users who use it vs. those who don't.\n\n\nUser input:\n- Feature to analyze:\n- Time period (optional):", + suggestions: [ + { + label: "Run a feature analysis", + description: "Adoption, engagement, and retention of a feature", + icon: ChartLine, + color: "blue", + mode: "auto", + prompt: + "Analyze how a feature is performing — adoption, engagement, and retention of users who use it vs. those who don't.\n\n\nUser input:\n- Feature to analyze:\n- Time period (optional):", + }, + { + label: "Understand revenue patterns", + description: "Trends over time, by plan, and by cohort", + icon: CurrencyDollar, + color: "green", + mode: "auto", + prompt: + "Analyze our revenue trends — break it down over time, by plan, and by cohort, and call out notable changes and likely drivers.\n\n\nUser input:\n- What revenue question are you trying to answer:\n- Time period (optional):", + }, + { + label: "Summarize product usage", + description: "Top events, active users, and key funnels", + icon: ChartBar, + color: "violet", + mode: "auto", + prompt: + "Summarize how our product is being used — top events, active users, key funnels, and notable trends.\n\n\nUser input:\n- Product area or feature to focus on (optional):\n- Time period (optional):", + }, + { + label: "Interpret experiment results", + description: "Significance and what to do next", + icon: Flask, + color: "purple", + mode: "auto", + prompt: + "Interpret the results of an experiment — explain what the metrics show, whether it's significant, and what to do next.\n\n\nUser input:\n- Experiment name or key:\n- What decision are you trying to make (optional):", + }, + { + label: "Summarize user & agent feedback", + description: "Common themes across recent feedback", + icon: ChatCircleText, + color: "amber", + mode: "auto", + prompt: + "Summarize recent user and support/agent feedback — surface the common themes, complaints, and requests.\n\n\nUser input:\n- Feedback source or topic to focus on:\n- Time period (optional):", + }, + ], }, { - label: "Understand revenue patterns", - description: "Trends over time, by plan, and by cohort", - icon: CurrencyDollar, - color: "green", - mode: "auto", - prompt: - "Analyze our revenue trends — break it down over time, by plan, and by cohort, and call out notable changes and likely drivers.\n\n\nUser input:\n- What revenue question are you trying to answer:\n- Time period (optional):", + id: "debug", + label: "Debug", + icon: Bug, + color: "red", + suggestions: [ + { + label: "Debug a user issue", + description: "Trace a specific user's events, replays, and errors", + icon: Bug, + color: "red", + mode: "auto", + prompt: + "Help me debug an issue a specific user is hitting. Pull their recent events, session replays, and errors, then figure out what went wrong.\n\n\nUser input:\n- Describe the user issue:\n- User identifier (distinct ID, email address, etc):", + }, + { + label: "Investigate an error", + description: "Root cause, frequency, and who it affects", + icon: WarningOctagon, + color: "red", + mode: "auto", + prompt: + "Investigate an error or exception — find the root cause, how often it happens, and which users it affects.\n\n\nUser input:\n- Error message or issue:\n- Where you're seeing it (optional):", + }, + { + label: "Diagnose a metric change", + description: "Why a metric dropped or spiked", + icon: TrendDown, + color: "amber", + mode: "auto", + prompt: + "Figure out why a metric dropped or spiked — break it down by segment and surface the likely causes.\n\n\nUser input:\n- Which metric changed:\n- When you noticed it (optional):", + }, + ], }, { - label: "Summarize product usage", - description: "Top events, active users, and key funnels", - icon: ChartBar, + id: "canvas", + label: "Canvas", + icon: SquaresFour, color: "violet", - mode: "auto", - prompt: - "Summarize how our product is being used — top events, active users, key funnels, and notable trends.\n\n\nUser input:\n- Product area or feature to focus on (optional):\n- Time period (optional):", - }, - { - label: "Summarize user & agent feedback", - description: "Common themes across recent feedback", - icon: ChatCircleText, - color: "amber", - mode: "auto", - prompt: - "Summarize recent user and support/agent feedback — surface the common themes, complaints, and requests.\n\n\nUser input:\n- Feedback source or topic to focus on:\n- Time period (optional):", - }, - { - label: "Interpret experiment results", - description: "Significance and what to do next", - icon: Flask, - color: "purple", - mode: "auto", - prompt: - "Interpret the results of an experiment — explain what the metrics show, whether it's significant, and what to do next.\n\n\nUser input:\n- Experiment name or key:\n- What decision are you trying to make (optional):", - }, - { - label: "Fix a bug", - description: "Track down and fix a problem in the code", - icon: Wrench, - color: "orange", - mode: "plan", - prompt: - "Help me fix a bug — track down the root cause in the code and implement a fix. Open a PR if appropriate.\n\n\nUser input:\n- Describe the bug / what's going wrong:\n- Steps to reproduce (optional):\n- Where it happens (file, page, area — optional):", - }, - { - label: "Build a new feature", - description: "Design and implement something new", - icon: Cube, - color: "teal", - mode: "plan", - prompt: - "Help me build a new feature — propose an approach, then implement it. Open a PR if appropriate.\n\n\nUser input:\n- Describe the feature you want:\n- Any constraints or requirements (optional):", + suggestions: [ + { + label: "Build a dashboard", + description: "Lay out the key metrics on a canvas", + icon: SquaresFour, + color: "violet", + mode: "auto", + prompt: + "Build a dashboard canvas that brings together the metrics that matter for this work.\n\n\nUser input:\n- What should the dashboard cover:\n- Key metrics or breakdowns to include (optional):", + }, + { + label: "Visualize a metric", + description: "Chart a single metric with the right breakdowns", + icon: ChartPieSlice, + color: "purple", + mode: "auto", + prompt: + "Create a canvas that visualizes a single metric over time, with the breakdowns that make it useful.\n\n\nUser input:\n- Metric to visualize:\n- Breakdowns or filters (optional):", + }, + ], }, ]; + +// Flat list for the new-task screen's card grid (WebsiteNewTask), which shows +// every starter prompt at once rather than grouping them behind chips. +export const CHANNEL_TASK_SUGGESTIONS: SuggestedPrompt[] = + CHANNEL_SUGGESTION_CATEGORIES.flatMap((category) => category.suggestions); diff --git a/packages/ui/src/features/canvas/components/ChannelHomeComposer.tsx b/packages/ui/src/features/canvas/components/ChannelHomeComposer.tsx index 069129a9e0..c5c91789c1 100644 --- a/packages/ui/src/features/canvas/components/ChannelHomeComposer.tsx +++ b/packages/ui/src/features/canvas/components/ChannelHomeComposer.tsx @@ -1,8 +1,11 @@ +import { FileText, X } from "@phosphor-icons/react"; import { isValidConfigValue } from "@posthog/core/task-detail/configOptions"; import type { Task } from "@posthog/shared/domain-types"; +import { Tooltip } from "@radix-ui/themes"; import { forwardRef, useCallback, + useEffect, useImperativeHandle, useRef, useState, @@ -59,6 +62,20 @@ export const ChannelHomeComposer = forwardRef< const [editorIsEmpty, setEditorIsEmpty] = useState(true); const { isOnline } = useConnectivity(); + // The channel CONTEXT.md is attached to new tasks by default; the chip below + // the prompt surfaces that it's included and lets the user drop it from this + // task. Re-include whenever the source context changes (e.g. the doc loads or + // the channel switches) so a dismissal doesn't stick. Mirrors TaskInput. + const [channelContextDismissed, setChannelContextDismissed] = useState(false); + const lastChannelContextRef = useRef(channelContext); + useEffect(() => { + if (lastChannelContextRef.current !== channelContext) { + lastChannelContextRef.current = channelContext; + setChannelContextDismissed(false); + } + }, [channelContext]); + const includeChannelContext = !!channelContext && !channelContextDismissed; + const handleEmptyChange = useCallback( (isEmpty: boolean) => { setEditorIsEmpty(isEmpty); @@ -136,7 +153,7 @@ export const ChannelHomeComposer = forwardRef< model: currentModel, reasoningLevel: currentReasoningLevel, allowNoRepo: true, - channelContext, + channelContext: includeChannelContext ? channelContext : undefined, channelName, onTaskCreated, }); @@ -242,6 +259,28 @@ export const ChannelHomeComposer = forwardRef< if (canSubmit) handleSubmit(); }} /> + + {includeChannelContext && ( +
+ Using: + + + + {channelName ? `#${channelName} ` : ""}CONTEXT.md + + + + + +
+ )}
); }); diff --git a/packages/ui/src/features/canvas/components/WebsiteChannelHome.tsx b/packages/ui/src/features/canvas/components/WebsiteChannelHome.tsx index 036cc582c0..9f223cf9db 100644 --- a/packages/ui/src/features/canvas/components/WebsiteChannelHome.tsx +++ b/packages/ui/src/features/canvas/components/WebsiteChannelHome.tsx @@ -1,4 +1,8 @@ -import { ArrowRightIcon, CaretRightIcon } from "@phosphor-icons/react"; +import { + ArrowLeftIcon, + ArrowRightIcon, + CaretRightIcon, +} from "@phosphor-icons/react"; import type { DashboardSummary } from "@posthog/core/canvas/dashboardSchemas"; import { cn } from "@posthog/quill"; import { formatRelativeTimeShort } from "@posthog/shared"; @@ -6,7 +10,10 @@ import { ANALYTICS_EVENTS } from "@posthog/shared/analytics-events"; import type { Task } from "@posthog/shared/domain-types"; import { useArchivedTaskIds } from "@posthog/ui/features/archive/useArchivedTaskIds"; import { TaskTabIcon } from "@posthog/ui/features/browser-tabs/TaskTabIcon"; -import { CHANNEL_TASK_SUGGESTIONS } from "@posthog/ui/features/canvas/channelTaskSuggestions"; +import { + CHANNEL_SUGGESTION_CATEGORIES, + type SuggestionCategory, +} from "@posthog/ui/features/canvas/channelTaskSuggestions"; import { ChannelHeader } from "@posthog/ui/features/canvas/components/ChannelHeader"; import { ChannelHomeComposer, @@ -20,16 +27,16 @@ import { } from "@posthog/ui/features/canvas/hooks/useChannelTasks"; import { useDashboards } from "@posthog/ui/features/canvas/hooks/useDashboards"; import { useFolderInstructions } from "@posthog/ui/features/canvas/hooks/useFolderInstructions"; -import { SuggestedPromptCard } from "@posthog/ui/features/task-detail/components/SuggestedPromptCard"; +import type { SuggestedPrompt } from "@posthog/ui/features/task-detail/components/SuggestedPromptCard"; import { taskDetailQuery } from "@posthog/ui/features/tasks/queries"; import { useTasks } from "@posthog/ui/features/tasks/useTasks"; import { useSetHeaderContent } from "@posthog/ui/hooks/useSetHeaderContent"; import { toast } from "@posthog/ui/primitives/toast"; import { track } from "@posthog/ui/shell/analytics"; -import { Text } from "@radix-ui/themes"; +import { Flex, Text } from "@radix-ui/themes"; import { useQueryClient } from "@tanstack/react-query"; import { Link, useNavigate } from "@tanstack/react-router"; -import { useCallback, useMemo, useRef, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; const RECENT_TASK_LIMIT = 5; const PINNED_ARTIFACT_LIMIT = 5; @@ -57,11 +64,50 @@ export function WebsiteChannelHome({ channelId }: { channelId: string }) { // the prompt box. const [composerEmpty, setComposerEmpty] = useState(true); - const handleSuggestionSelect = useCallback( - (prompt: string, mode?: string) => { - composerRef.current?.applySuggestion(prompt, mode); + // Which category chip is expanded into its action list. `displayedCategoryId` + // lags `activeCategoryId` so the detail box keeps its contents through the + // fade-out when collapsing back to the chips. + const [activeCategoryId, setActiveCategoryId] = useState(null); + const [displayedCategoryId, setDisplayedCategoryId] = useState( + null, + ); + useEffect(() => { + if (activeCategoryId) { + setDisplayedCategoryId(activeCategoryId); + return; + } + const id = setTimeout(() => setDisplayedCategoryId(null), 200); + return () => clearTimeout(id); + }, [activeCategoryId]); + const displayedCategory = CHANNEL_SUGGESTION_CATEGORIES.find( + (c) => c.id === displayedCategoryId, + ); + + const selectCategory = useCallback( + (category: SuggestionCategory) => { + setActiveCategoryId(category.id); + track(ANALYTICS_EVENTS.CHANNEL_ACTION, { + action_type: "select_suggestion_category", + surface: "channel_home", + channel_id: channelId, + category: category.id, + }); + }, + [channelId], + ); + + const applySuggestion = useCallback( + (suggestion: SuggestedPrompt, categoryId: string) => { + track(ANALYTICS_EVENTS.CHANNEL_ACTION, { + action_type: "new_task_suggestion", + surface: "channel_home", + channel_id: channelId, + category: categoryId, + suggestion_label: suggestion.label, + }); + composerRef.current?.applySuggestion(suggestion.prompt, suggestion.mode); }, - [], + [channelId], ); const onTaskCreated = useCallback( @@ -122,39 +168,169 @@ export function WebsiteChannelHome({ channelId }: { channelId: string }) { user is typing so the prompt box has the floor. */}
-
- - Suggestions - -
- {CHANNEL_TASK_SUGGESTIONS.map((suggestion) => ( - - handleSuggestionSelect(suggestion.prompt, suggestion.mode) + {/* The chips + recents and the expanded category box share this + space and crossfade: the chips view stays in flow (holding the + height) while the detail box overlays it. */} +
+
+
+ {CHANNEL_SUGGESTION_CATEGORIES.map((category) => ( + selectCategory(category)} + /> + ))} +
+ +
+ + +
+
+ +
+ {displayedCategory ? ( + setActiveCategoryId(null)} + onSelect={(suggestion) => + applySuggestion(suggestion, displayedCategory.id) } /> - ))} + ) : null}
- -
- - -
); } +// A category pill below the prompt box. Clicking it expands the category's +// action list in place of the chips + recents. +function CategoryChip({ + category, + onClick, +}: { + category: SuggestionCategory; + onClick: () => void; +}) { + const Icon = category.icon; + return ( + + ); +} + +// The expanded list for a category: a back button to collapse to the chips, +// then one row per suggested action. +function CategorySuggestions({ + category, + onBack, + onSelect, +}: { + category: SuggestionCategory; + onBack: () => void; + onSelect: (suggestion: SuggestedPrompt) => void; +}) { + return ( +
+
+ + + {category.label} + +
+ {category.suggestions.map((suggestion) => ( + onSelect(suggestion)} + /> + ))} +
+ ); +} + +// A single suggested action: icon badge, title, then the description as muted +// text alongside it. +function SuggestionRow({ + suggestion, + onSelect, +}: { + suggestion: SuggestedPrompt; + onSelect: () => void; +}) { + const Icon = suggestion.icon; + return ( + + ); +} + // Left column: the channel's most recently active tasks, with the shared task // status icons. "All tasks" jumps to the Recents tab. function RecentTasksColumn({ channelId }: { channelId: string }) { From c08eb6a6fbbb3a25901f7f2214255a0026f41618 Mon Sep 17 00:00:00 2001 From: Raquel Smith Date: Wed, 1 Jul 2026 01:13:18 -0700 Subject: [PATCH 4/5] fix(channels): attach context indicator to prompt box Match the channel-home CONTEXT.md chip to the TaskInput style: attach it to the bottom of the prompt box with a slight underlap (negative top margin, no top border, bottom-only rounding, inset horizontal margin) instead of floating it below as a detached pill. Generated-By: PostHog Code Task-Id: b9cd812b-a600-4565-87d0-f5b9f195ea14 --- .../ui/src/features/canvas/components/ChannelHomeComposer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/src/features/canvas/components/ChannelHomeComposer.tsx b/packages/ui/src/features/canvas/components/ChannelHomeComposer.tsx index c5c91789c1..28d2f945c0 100644 --- a/packages/ui/src/features/canvas/components/ChannelHomeComposer.tsx +++ b/packages/ui/src/features/canvas/components/ChannelHomeComposer.tsx @@ -261,7 +261,7 @@ export const ChannelHomeComposer = forwardRef< /> {includeChannelContext && ( -
+
Using: From b765563e5ab0957f4bb2c3d5b8788d8f855e6dc3 Mon Sep 17 00:00:00 2001 From: Raquel Smith Date: Wed, 1 Jul 2026 01:45:17 -0700 Subject: [PATCH 5/5] feat(channels): category chips as a menu bar Turn the channel-home category chips into a quill Menubar: each chip is a menu trigger whose popup lists that category's suggested actions, with hover-to-switch between menus. Anchoring each popup to the whole bar (not its trigger) makes Base UI's --anchor-width the bar width, so the popup fills the bar. The bar now stays put while a menu is open; only the recents below fade out. Chips get a bit more vertical margin. Drops the previous in-place expand/back-button + crossfade in favor of the menu popups' built-in fade. Generated-By: PostHog Code Task-Id: b9cd812b-a600-4565-87d0-f5b9f195ea14 --- .../canvas/components/WebsiteChannelHome.tsx | 241 ++++++++---------- 1 file changed, 102 insertions(+), 139 deletions(-) diff --git a/packages/ui/src/features/canvas/components/WebsiteChannelHome.tsx b/packages/ui/src/features/canvas/components/WebsiteChannelHome.tsx index 9f223cf9db..13ee9aaef3 100644 --- a/packages/ui/src/features/canvas/components/WebsiteChannelHome.tsx +++ b/packages/ui/src/features/canvas/components/WebsiteChannelHome.tsx @@ -1,10 +1,13 @@ -import { - ArrowLeftIcon, - ArrowRightIcon, - CaretRightIcon, -} from "@phosphor-icons/react"; +import { ArrowRightIcon, CaretRightIcon } from "@phosphor-icons/react"; import type { DashboardSummary } from "@posthog/core/canvas/dashboardSchemas"; -import { cn } from "@posthog/quill"; +import { + cn, + Menubar, + MenubarContent, + MenubarItem, + MenubarMenu, + MenubarTrigger, +} from "@posthog/quill"; import { formatRelativeTimeShort } from "@posthog/shared"; import { ANALYTICS_EVENTS } from "@posthog/shared/analytics-events"; import type { Task } from "@posthog/shared/domain-types"; @@ -36,7 +39,7 @@ import { track } from "@posthog/ui/shell/analytics"; import { Flex, Text } from "@radix-ui/themes"; import { useQueryClient } from "@tanstack/react-query"; import { Link, useNavigate } from "@tanstack/react-router"; -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useCallback, useMemo, useRef, useState } from "react"; const RECENT_TASK_LIMIT = 5; const PINNED_ARTIFACT_LIMIT = 5; @@ -64,34 +67,27 @@ export function WebsiteChannelHome({ channelId }: { channelId: string }) { // the prompt box. const [composerEmpty, setComposerEmpty] = useState(true); - // Which category chip is expanded into its action list. `displayedCategoryId` - // lags `activeCategoryId` so the detail box keeps its contents through the - // fade-out when collapsing back to the chips. - const [activeCategoryId, setActiveCategoryId] = useState(null); - const [displayedCategoryId, setDisplayedCategoryId] = useState( - null, - ); - useEffect(() => { - if (activeCategoryId) { - setDisplayedCategoryId(activeCategoryId); - return; - } - const id = setTimeout(() => setDisplayedCategoryId(null), 200); - return () => clearTimeout(id); - }, [activeCategoryId]); - const displayedCategory = CHANNEL_SUGGESTION_CATEGORIES.find( - (c) => c.id === displayedCategoryId, - ); + // Anchor for the category menus: pointing each popup at the whole menu bar + // (rather than its own trigger) makes Base UI's --anchor-width the bar width, + // so the popup fills the bar. + const menuBarRef = useRef(null); - const selectCategory = useCallback( - (category: SuggestionCategory) => { - setActiveCategoryId(category.id); - track(ANALYTICS_EVENTS.CHANNEL_ACTION, { - action_type: "select_suggestion_category", - surface: "channel_home", - channel_id: channelId, - category: category.id, - }); + // Which category menu is open, if any. The menu bar stays put while a menu is + // open, but the recents below fade out so the options have the floor. + const [openCategoryId, setOpenCategoryId] = useState(null); + const handleCategoryOpenChange = useCallback( + (category: SuggestionCategory, open: boolean) => { + setOpenCategoryId((prev) => + open ? category.id : prev === category.id ? null : prev, + ); + if (open) { + track(ANALYTICS_EVENTS.CHANNEL_ACTION, { + action_type: "select_suggestion_category", + surface: "channel_home", + channel_id: channelId, + category: category.id, + }); + } }, [channelId], ); @@ -164,8 +160,8 @@ export function WebsiteChannelHome({ channelId }: { channelId: string }) { onTaskCreated={onTaskCreated} /> - {/* Suggestions + recent/pinned glance. All fade out together while the - user is typing so the prompt box has the floor. */} + {/* Category menu bar + recent/pinned glance. Everything fades out while + the user is typing so the prompt box has the floor. */}
- {/* The chips + recents and the expanded category box share this - space and crossfade: the chips view stays in flow (holding the - height) while the detail box overlays it. */} -
-
-
- {CHANNEL_SUGGESTION_CATEGORIES.map((category) => ( - selectCategory(category)} - /> - ))} -
- -
- - -
-
- -
- {displayedCategory ? ( - setActiveCategoryId(null)} + {/* The bar is the anchor for every category popup: anchoring to it + (not the trigger) makes each popup fill the bar's width. */} +
+ + {CHANNEL_SUGGESTION_CATEGORIES.map((category) => ( + + handleCategoryOpenChange(category, open) + } onSelect={(suggestion) => - applySuggestion(suggestion, displayedCategory.id) + applySuggestion(suggestion, category.id) } /> - ) : null} -
+ ))} + +
+ + {/* Recents fade out while a category menu is open. */} +
+ +
@@ -229,70 +208,55 @@ export function WebsiteChannelHome({ channelId }: { channelId: string }) { ); } -// A category pill below the prompt box. Clicking it expands the category's -// action list in place of the chips + recents. -function CategoryChip({ - category, - onClick, -}: { - category: SuggestionCategory; - onClick: () => void; -}) { - const Icon = category.icon; - return ( - - ); -} - -// The expanded list for a category: a back button to collapse to the chips, -// then one row per suggested action. -function CategorySuggestions({ +// One category in the menu bar: a chip-styled trigger, and a popup listing the +// category's suggested actions. The popup is anchored to the whole bar so it +// fills the bar's width (via Base UI's --anchor-width). +function CategoryMenu({ category, - onBack, + anchor, + onOpenChange, onSelect, }: { category: SuggestionCategory; - onBack: () => void; + anchor: React.RefObject; + onOpenChange: (open: boolean) => void; onSelect: (suggestion: SuggestedPrompt) => void; }) { + const Icon = category.icon; return ( -
-
- - - {category.label} - -
- {category.suggestions.map((suggestion) => ( - onSelect(suggestion)} - /> - ))} -
+ + + + {category.label} + + + {category.suggestions.map((suggestion) => ( + onSelect(suggestion)} + /> + ))} + + ); } -// A single suggested action: icon badge, title, then the description as muted -// text alongside it. +// A single suggested action row: icon badge, title, then the description as +// muted text alongside it. Selecting it drops the prompt into the composer. function SuggestionRow({ suggestion, onSelect, @@ -302,10 +266,9 @@ function SuggestionRow({ }) { const Icon = suggestion.icon; return ( - + ); }