From 6e1b273f5306c730a75334d0f6ebf8497855c4b9 Mon Sep 17 00:00:00 2001 From: Adam Leith Date: Thu, 16 Jul 2026 13:12:02 +0100 Subject: [PATCH 1/5] channel rename + large new button --- .../features/browser-tabs/BrowserTabStrip.tsx | 2 +- .../canvas/components/ActivityView.tsx | 2 +- .../components/ChannelContextPanel.test.tsx | 2 +- .../canvas/components/ChannelContextPanel.tsx | 2 +- .../canvas/components/ChannelHeader.tsx | 2 +- .../canvas/components/ChannelIntro.tsx | 11 +- .../canvas/components/ChannelsFab.tsx | 72 ++++++++ .../canvas/components/ChannelsList.tsx | 174 +++++++++++++----- .../canvas/components/ChannelsSidebar.tsx | 10 +- .../canvas/components/CreateChannelModal.tsx | 6 +- .../canvas/components/RenameChannelModal.tsx | 4 +- .../canvas/components/WebsiteChannelHome.tsx | 2 +- .../canvas/components/WebsiteContext.tsx | 4 +- .../ui/src/features/canvas/contextPrompt.ts | 4 +- .../features/canvas/hooks/useDashboards.ts | 21 ++- .../ui/src/features/command/CommandMenu.tsx | 4 +- .../sidebar/components/SidebarNavSection.tsx | 2 +- .../task-detail/components/TaskInput.tsx | 6 +- 18 files changed, 246 insertions(+), 84 deletions(-) create mode 100644 packages/ui/src/features/canvas/components/ChannelsFab.tsx diff --git a/packages/ui/src/features/browser-tabs/BrowserTabStrip.tsx b/packages/ui/src/features/browser-tabs/BrowserTabStrip.tsx index bd52aef08e..3baed5c3b4 100644 --- a/packages/ui/src/features/browser-tabs/BrowserTabStrip.tsx +++ b/packages/ui/src/features/browser-tabs/BrowserTabStrip.tsx @@ -525,7 +525,7 @@ export function BrowserTabStrip() { const meta = channelSectionFor(section); return { id: t.id, - label: meta?.label ?? channel ?? "Context", + label: meta?.label ?? channel ?? "Channel", icon: , channelName: channel, // No section meta → the channel's index page. diff --git a/packages/ui/src/features/canvas/components/ActivityView.tsx b/packages/ui/src/features/canvas/components/ActivityView.tsx index e3323ae71c..4261f1959a 100644 --- a/packages/ui/src/features/canvas/components/ActivityView.tsx +++ b/packages/ui/src/features/canvas/components/ActivityView.tsx @@ -176,7 +176,7 @@ export function ActivityView() { Activity - Mentions of you across contexts. + Mentions of you across channels.
{isLoading && items.length === 0 ? ( diff --git a/packages/ui/src/features/canvas/components/ChannelContextPanel.test.tsx b/packages/ui/src/features/canvas/components/ChannelContextPanel.test.tsx index b9d1b7e40e..a5ad8a984b 100644 --- a/packages/ui/src/features/canvas/components/ChannelContextPanel.test.tsx +++ b/packages/ui/src/features/canvas/components/ChannelContextPanel.test.tsx @@ -63,7 +63,7 @@ describe("ChannelContextPanel", () => { const user = userEvent.setup(); const onClose = renderPanel(); await user.click( - screen.getByRole("button", { name: "Close context panel" }), + screen.getByRole("button", { name: "Close CONTEXT.md panel" }), ); expect(onClose).toHaveBeenCalledTimes(1); }); diff --git a/packages/ui/src/features/canvas/components/ChannelContextPanel.tsx b/packages/ui/src/features/canvas/components/ChannelContextPanel.tsx index b5ad42ac86..2d4d21d754 100644 --- a/packages/ui/src/features/canvas/components/ChannelContextPanel.tsx +++ b/packages/ui/src/features/canvas/components/ChannelContextPanel.tsx @@ -35,7 +35,7 @@ export function ChannelContextPanel({ diff --git a/packages/ui/src/features/canvas/components/ChannelIntro.tsx b/packages/ui/src/features/canvas/components/ChannelIntro.tsx index 33919338f7..c39d3d4e1e 100644 --- a/packages/ui/src/features/canvas/components/ChannelIntro.tsx +++ b/packages/ui/src/features/canvas/components/ChannelIntro.tsx @@ -55,9 +55,9 @@ export function ChannelIntro({ @{userDisplayName(creator ?? null)} {" "} - created this context {creationDatePhrase(channel.created_at)}. This + created this channel {creationDatePhrase(channel.created_at)}. This is the very beginning of the{" "} - {channelName} context. + {channelName} channel. )}
@@ -70,7 +70,7 @@ export function ChannelIntro({ Created context.md - Used in all sessions within this context + Used in all sessions within this channel @@ -125,9 +125,10 @@ export function ChannelIntro({ - Learn more about contexts + Learn more about channels - Context is a group of tasks that are related to a specific topic. + A channel is a group of tasks that are related to a specific + topic. diff --git a/packages/ui/src/features/canvas/components/ChannelsFab.tsx b/packages/ui/src/features/canvas/components/ChannelsFab.tsx new file mode 100644 index 0000000000..2b5d618985 --- /dev/null +++ b/packages/ui/src/features/canvas/components/ChannelsFab.tsx @@ -0,0 +1,72 @@ +import { FileTextIcon, PlusIcon } from "@phosphor-icons/react"; +import { + Button, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@posthog/quill"; +import { CreateChannelModal } from "@posthog/ui/features/canvas/components/CreateChannelModal"; +import { openTaskInput } from "@posthog/ui/router/useOpenTask"; +import { useRouterState } from "@tanstack/react-router"; +import { SquircleDashed } from "lucide-react"; +import { useState } from "react"; + +// The create affordance for the Channels space, floated over the bottom-right +// of the channel list. It owns the create-channel modal (the list itself has no +// other entry point) and opens its menu upward, since it sits at the bottom. +export function ChannelsFab() { + const [modalOpen, setModalOpen] = useState(false); + // New task has no /website mirror yet, so it jumps back to Code unless we're + // already in the Channels space — same rule as the nav's New task row. + const inChannels = useRouterState({ + select: (s) => s.location.pathname.startsWith("/website"), + }); + + return ( + <> + + + + + + } + /> + } + /> + + Create something new + + + + setModalOpen(true)}> + + New channel + + + openTaskInput(inChannels ? { space: "website" } : undefined) + } + > + + New task + + + + + + + ); +} diff --git a/packages/ui/src/features/canvas/components/ChannelsList.tsx b/packages/ui/src/features/canvas/components/ChannelsList.tsx index a228e5dca1..7f48a064ee 100644 --- a/packages/ui/src/features/canvas/components/ChannelsList.tsx +++ b/packages/ui/src/features/canvas/components/ChannelsList.tsx @@ -37,7 +37,6 @@ import { TooltipTrigger, } from "@posthog/quill"; import { ANALYTICS_EVENTS } from "@posthog/shared/analytics-events"; -import { CreateChannelModal } from "@posthog/ui/features/canvas/components/CreateChannelModal"; import { RenameChannelModal } from "@posthog/ui/features/canvas/components/RenameChannelModal"; import { trackAndCreateCanvas } from "@posthog/ui/features/canvas/createCanvasAnalytics"; import { @@ -138,7 +137,7 @@ function useChannelActions(channel: Channel): { channel_id: channel.id, success: false, }); - toast.error("Couldn't delete context", { + toast.error("Couldn't delete channel", { description: error instanceof Error ? error.message : String(error), }); return false; @@ -148,7 +147,7 @@ function useChannelActions(channel: Channel): { const actions: ChannelActionItem[] = [ { key: "star", - label: isStarred ? "Unstar context" : "Star context", + label: isStarred ? "Unstar channel" : "Star channel", icon: , onSelect: () => { track(ANALYTICS_EVENTS.CHANNEL_ACTION, { @@ -167,14 +166,14 @@ function useChannelActions(channel: Channel): { }, { key: "rename", - label: "Rename context…", + label: "Rename channel…", icon: , separatorBefore: true, onSelect: () => setRenameOpen(true), }, { key: "delete", - label: "Delete context…", + label: "Delete channel…", icon: , variant: "destructive", onSelect: () => setConfirmDeleteOpen(true), @@ -441,17 +440,17 @@ function ChannelSection({ channel }: { channel: Channel }) { Delete {channel.name}? - This permanently deletes the context and can’t be undone. + This permanently deletes the channel and can’t be undone.
  • - The context and its{" "} + The channel and its{" "} CONTEXT.md are deleted.
  • - Every canvas saved in this context is permanently deleted. + Every canvas saved in this channel is permanently deleted.
  • - Filed tasks are removed from the context, but the tasks + Filed tasks are removed from the channel, but the tasks themselves are not deleted.
@@ -470,7 +469,7 @@ function ChannelSection({ channel }: { channel: Channel }) { }) } > - Delete context + Delete channel @@ -490,54 +489,139 @@ function PersonalChannelRow() { const { createChannel, isCreating } = useChannelMutations(); // Listing backend channels lazily provisions the personal channel server-side. useTaskChannels(); + // The "+" dropdown (New task / New canvas), mirroring a shared channel row. + const [newMenuOpen, setNewMenuOpen] = useState(false); const meFolder = channels.find((c) => c.name === PERSONAL_CHANNEL_NAME); + const createAndOpenCanvas = useCreateAndOpenDashboard(meFolder?.id); const isActive = !!meFolder && (pathname === `/website/${meFolder.id}` || pathname.startsWith(`/website/${meFolder.id}/`)); - const open = async () => { + // The "me" folder is created on first use, so every action resolves the id + // rather than closing over it — the row is actionable before it exists. + const ensureFolderId = async (): Promise => { try { - const folder = meFolder ?? (await createChannel(PERSONAL_CHANNEL_NAME)); - void navigate({ - to: "/website/$channelId", - params: { channelId: folder.id }, - }); + return (meFolder ?? (await createChannel(PERSONAL_CHANNEL_NAME))).id; } catch (error) { toast.error("Couldn't open me", { description: error instanceof Error ? error.message : String(error), }); + return undefined; } }; + const open = async () => { + const channelId = await ensureFolderId(); + if (!channelId) return; + void navigate({ to: "/website/$channelId", params: { channelId } }); + }; + + const newTask = async () => { + const channelId = await ensureFolderId(); + if (!channelId) return; + track(ANALYTICS_EVENTS.CHANNEL_ACTION, { + action_type: "new_task_open", + surface: "sidebar", + channel_id: channelId, + }); + void navigate({ to: "/website/$channelId/new", params: { channelId } }); + }; + + const newCanvas = async () => { + const channelId = await ensureFolderId(); + if (!channelId) return; + trackAndCreateCanvas( + channelId, + undefined, + "sidebar", + () => void createAndOpenCanvas({ channelId }), + ); + }; + return ( - + + +
+ + + + + + } + /> + } + /> + New… + + + void newTask()}> + + New task + + void newCanvas()}> + + New canvas + + + +
+
); } // The channel list — the Channels space sidebar body. The private "#me" // channel is pinned at the top; starred channels surface in their own section // so the ones you use most stay in reach; the rest sit under a "Channels" -// label with the "New" channel button. +// label. Creating anything goes through the floating ChannelsFab, mounted by +// the sidebar outside this scroll region. export function ChannelsList() { const { channels: allChannels, isLoading } = useChannels(); const { starredRefToShortcutId } = useChannelStars(); - const [modalOpen, setModalOpen] = useState(false); // The "me" folder renders as the pinned personal row, not a shared channel. const channels = allChannels.filter((c) => c.name !== PERSONAL_CHANNEL_NAME); @@ -561,7 +645,9 @@ export function ChannelsList() { // One shared provider groups every row tooltip so that once one shows, // moving to the next row reveals its tooltip instantly (no re-delay). - + {/* Bottom padding clears the floating create button (ChannelsFab), so the + last channel stays reachable at full scroll. */} + {starred.length > 0 && ( @@ -581,25 +667,15 @@ export function ChannelsList() { )} 0 && "mt-3")}> - - - - Contexts - - + + + Channels {!isLoading && channels.length === 0 && ( - No contexts yet. Create one to get started. + No channels yet. Create one to get started. )} @@ -609,8 +685,6 @@ export function ChannelsList() { ))} - - ); } diff --git a/packages/ui/src/features/canvas/components/ChannelsSidebar.tsx b/packages/ui/src/features/canvas/components/ChannelsSidebar.tsx index 111ff227f5..1ed590fa55 100644 --- a/packages/ui/src/features/canvas/components/ChannelsSidebar.tsx +++ b/packages/ui/src/features/canvas/components/ChannelsSidebar.tsx @@ -2,6 +2,7 @@ import { ArchiveIcon } from "@phosphor-icons/react"; import { Separator } from "@posthog/quill"; import { PROJECT_BLUEBIRD_FLAG } from "@posthog/shared"; import { useArchivedTaskIds } from "@posthog/ui/features/archive/useArchivedTaskIds"; +import { ChannelsFab } from "@posthog/ui/features/canvas/components/ChannelsFab"; import { ChannelsList } from "@posthog/ui/features/canvas/components/ChannelsList"; import { useChannelsSidebarStore } from "@posthog/ui/features/canvas/components/channelsSidebarStore"; import { useFeatureFlag } from "@posthog/ui/features/feature-flags/useFeatureFlag"; @@ -110,8 +111,13 @@ export function ChannelsSidebar() { {bodyChannelsEnabled ? ( <> - - + {/* The fab is a sibling of the scroll region, not a child, so it + stays pinned to the bottom-right instead of scrolling away. */} + + + + + ) : ( diff --git a/packages/ui/src/features/canvas/components/CreateChannelModal.tsx b/packages/ui/src/features/canvas/components/CreateChannelModal.tsx index d2c61fc89b..3f4e79e5b9 100644 --- a/packages/ui/src/features/canvas/components/CreateChannelModal.tsx +++ b/packages/ui/src/features/canvas/components/CreateChannelModal.tsx @@ -107,7 +107,7 @@ export function CreateChannelModal({ surface: "sidebar", success: false, }); - toast.error("Couldn't create context", { + toast.error("Couldn't create channel", { description: error instanceof Error ? error.message : String(error), }); return; @@ -183,7 +183,7 @@ export function CreateChannelModal({ Create your context.md ) : ( - Create a context + Create a channel )} @@ -234,7 +234,7 @@ export function CreateChannelModal({ {needsDescription && ( - What's this context about? + What's this channel about?