Skip to content

Commit 6f243a0

Browse files
adamleithpclaudek11kirky
authored
feat(channels): collapsible sidebar groups, # icons, two-step create, context→channel rename (#3510)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Peter Kirkham <k11kirky@gmail.com>
1 parent 9beff6e commit 6f243a0

22 files changed

Lines changed: 731 additions & 256 deletions

packages/ui/src/features/browser-tabs/BrowserTabStrip.tsx

Lines changed: 5 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {
22
BrainIcon,
3+
HashIcon,
34
HouseIcon,
45
PlugsConnectedIcon,
56
RobotIcon,
@@ -24,16 +25,15 @@ import {
2425
} from "@posthog/shared";
2526
import { channelSectionFor } from "@posthog/ui/features/canvas/channelSections";
2627
import { iconForTemplate } from "@posthog/ui/features/canvas/components/canvasTemplateIcon";
28+
import { ensurePersonalChannel } from "@posthog/ui/features/canvas/ensurePersonalChannel";
2729
import {
28-
type Channel,
2930
useChannelMutations,
3031
useChannels,
3132
} from "@posthog/ui/features/canvas/hooks/useChannels";
3233
import {
3334
useDashboard,
3435
useDashboards,
3536
} from "@posthog/ui/features/canvas/hooks/useDashboards";
36-
import { PERSONAL_CHANNEL_NAME } from "@posthog/ui/features/canvas/hooks/useTaskChannels";
3737
import { SHORTCUTS } from "@posthog/ui/features/command/keyboard-shortcuts";
3838
import { useFeatureFlag } from "@posthog/ui/features/feature-flags/useFeatureFlag";
3939
import { usePanelLayoutStore } from "@posthog/ui/features/panels/panelLayoutStore";
@@ -50,7 +50,6 @@ import {
5050
useRouter,
5151
useRouterState,
5252
} from "@tanstack/react-router";
53-
import { SquircleDashed } from "lucide-react";
5453
import { type ReactNode, useEffect, useMemo } from "react";
5554
import { useHotkeys } from "react-hotkeys-hook";
5655
import {
@@ -87,13 +86,6 @@ declare module "@tanstack/history" {
8786
const canvasInfo = new Map<string, { name: string; templateId: string }>();
8887
const taskInfo = new Map<string, string>();
8988

90-
// Dedupe concurrent #me provisioning. The folder-creation endpoint isn't
91-
// server-side idempotent, and the new-tab path is on Cmd+T (trivially
92-
// double-fired/held) — so two landings racing before the first create's cache
93-
// update lands could each create a "me" folder. One in-flight create is shared
94-
// across callers until it settles.
95-
let personalChannelInFlight: Promise<Channel> | null = null;
96-
9789
/** Bounded insert (most-recent kept) so the caches don't grow unbounded over a
9890
* long session. */
9991
const MAX_CACHE_ENTRIES = 200;
@@ -525,8 +517,8 @@ export function BrowserTabStrip() {
525517
const meta = channelSectionFor(section);
526518
return {
527519
id: t.id,
528-
label: meta?.label ?? channel ?? "Context",
529-
icon: <SquircleDashed size={14} />,
520+
label: meta?.label ?? channel ?? "Channel",
521+
icon: <HashIcon size={14} />,
530522
channelName: channel,
531523
// No section meta → the channel's index page.
532524
isChannelHome: !meta,
@@ -739,16 +731,7 @@ export function BrowserTabStrip() {
739731
// row uses); fall back to the new-task screen if it can't be created.
740732
void (async () => {
741733
try {
742-
const existing = channels.find((c) => c.name === PERSONAL_CHANNEL_NAME);
743-
if (!existing && !personalChannelInFlight) {
744-
personalChannelInFlight = createChannel(
745-
PERSONAL_CHANNEL_NAME,
746-
).finally(() => {
747-
personalChannelInFlight = null;
748-
});
749-
}
750-
const folder = existing ?? (await personalChannelInFlight);
751-
if (!folder) return;
734+
const folder = await ensurePersonalChannel(channels, createChannel);
752735
navigate({
753736
to: "/website/$channelId",
754737
params: { channelId: folder.id },

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ export function ActivityView() {
176176
Activity
177177
</Text>
178178
<Text size="2" className="block text-muted-foreground">
179-
Mentions of you across contexts.
179+
Mentions of you across channels.
180180
</Text>
181181
<div className="mt-4">
182182
{isLoading && items.length === 0 ? (

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

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { HashIcon } from "@phosphor-icons/react";
12
import {
23
Button,
34
Tooltip,
@@ -7,7 +8,6 @@ import {
78
import { HeaderTitleEditor } from "@posthog/ui/features/task-detail/HeaderTitleEditor";
89
import { Flex, Text } from "@radix-ui/themes";
910
import { useNavigate } from "@tanstack/react-router";
10-
import { SquircleDashed } from "lucide-react";
1111
import { type ReactNode, useState } from "react";
1212

1313
interface ChannelBreadcrumbProps {
@@ -48,10 +48,7 @@ export function ChannelBreadcrumb({
4848

4949
const channelSegment = (
5050
<>
51-
<SquircleDashed
52-
size={12}
53-
className="mt-px shrink-0 text-muted-foreground/80"
54-
/>
51+
<HashIcon size={12} className="mt-px shrink-0 text-muted-foreground/80" />
5552
<Text
5653
className="min-w-0 truncate whitespace-nowrap font-medium text-[13px]"
5754
title={channelName}

packages/ui/src/features/canvas/components/ChannelContextPanel.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ describe("ChannelContextPanel", () => {
6363
const user = userEvent.setup();
6464
const onClose = renderPanel();
6565
await user.click(
66-
screen.getByRole("button", { name: "Close context panel" }),
66+
screen.getByRole("button", { name: "Close CONTEXT.md panel" }),
6767
);
6868
expect(onClose).toHaveBeenCalledTimes(1);
6969
});

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export function ChannelContextPanel({
3535
<button
3636
type="button"
3737
onClick={onClose}
38-
aria-label="Close context panel"
38+
aria-label="Close CONTEXT.md panel"
3939
className="flex size-6 shrink-0 items-center justify-center rounded text-gray-10 hover:bg-gray-4 hover:text-gray-12"
4040
>
4141
<X size={14} />

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

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1+
import { HashIcon } from "@phosphor-icons/react";
12
import { Button, cn } from "@posthog/quill";
23
import { ChannelTabs } from "@posthog/ui/features/canvas/components/ChannelTabs";
34
import { useChannels } from "@posthog/ui/features/canvas/hooks/useChannels";
45
import { Text } from "@radix-ui/themes";
56
import { useNavigate, useRouterState } from "@tanstack/react-router";
6-
import { SquircleDashed } from "lucide-react";
77

88
// The shared channel header: a clickable "# channel" that doubles as the Home
99
// item — it routes to the channel home (`/website/$channelId`, like the sidebar
@@ -29,12 +29,9 @@ export function ChannelHeader({ channelId }: { channelId: string }) {
2929
size="sm"
3030
className={cn("min-w-0", isHome ? "bg-fill-selected" : "")}
3131
>
32-
<SquircleDashed
33-
size={20}
34-
className="shrink-0 text-muted-foreground/80"
35-
/>
32+
<HashIcon size={20} className="shrink-0 text-muted-foreground/80" />
3633
<Text className="min-w-0 truncate font-medium" title={channelName}>
37-
{channelName ?? "Context"}
34+
{channelName ?? "Channel"}
3835
</Text>
3936
</Button>
4037
<ChannelTabs channelId={channelId} />

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

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,9 @@ export function ChannelIntro({
5555
<span className={mentionChipClass}>
5656
@{userDisplayName(creator ?? null)}
5757
</span>{" "}
58-
created this context {creationDatePhrase(channel.created_at)}. This
58+
created this channel {creationDatePhrase(channel.created_at)}. This
5959
is the very beginning of the{" "}
60-
<Text weight="bold">{channelName}</Text> context.
60+
<Text weight="bold">{channelName}</Text> channel.
6161
</Text>
6262
)}
6363
</div>
@@ -70,7 +70,7 @@ export function ChannelIntro({
7070
<ItemContent className="self-start">
7171
<ItemTitle>Created context.md</ItemTitle>
7272
<ItemDescription className="text-xs">
73-
Used in all sessions within this context
73+
Used in all sessions within this channel
7474
</ItemDescription>
7575
</ItemContent>
7676
</Item>
@@ -125,9 +125,10 @@ export function ChannelIntro({
125125
<Info size={18} />
126126
</ItemMedia>
127127
<ItemContent className="self-start">
128-
<ItemTitle>Learn more about contexts</ItemTitle>
128+
<ItemTitle>Learn more about channels</ItemTitle>
129129
<ItemDescription className="text-xs">
130-
Context is a group of tasks that are related to a specific topic.
130+
A channel is a group of tasks that are related to a specific
131+
topic.
131132
</ItemDescription>
132133
</ItemContent>
133134
</Item>
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { FileTextIcon, HashIcon, PlusIcon } from "@phosphor-icons/react";
2+
import {
3+
Button,
4+
DropdownMenu,
5+
DropdownMenuContent,
6+
DropdownMenuItem,
7+
DropdownMenuTrigger,
8+
Tooltip,
9+
TooltipContent,
10+
TooltipTrigger,
11+
} from "@posthog/quill";
12+
import { CreateChannelModal } from "@posthog/ui/features/canvas/components/CreateChannelModal";
13+
import { openTaskInput } from "@posthog/ui/router/useOpenTask";
14+
import { useRouterState } from "@tanstack/react-router";
15+
import { useState } from "react";
16+
17+
// The create affordance for the Channels space, floated over the bottom-right
18+
// of the channel list. It owns the create-channel modal (the list itself has no
19+
// other entry point) and opens its menu upward, since it sits at the bottom.
20+
export function ChannelsFab() {
21+
const [modalOpen, setModalOpen] = useState(false);
22+
// New task has no /website mirror yet, so it jumps back to Code unless we're
23+
// already in the Channels space — same rule as the nav's New task row.
24+
const inChannels = useRouterState({
25+
select: (s) => s.location.pathname.startsWith("/website"),
26+
});
27+
28+
return (
29+
<>
30+
<DropdownMenu>
31+
<Tooltip>
32+
<TooltipTrigger
33+
render={
34+
<DropdownMenuTrigger
35+
render={
36+
<Button
37+
variant="primary"
38+
size="icon-lg"
39+
aria-label="Create"
40+
className="absolute right-3 bottom-3 z-10 rounded-full shadow-lg"
41+
>
42+
<PlusIcon size={20} weight="bold" />
43+
</Button>
44+
}
45+
/>
46+
}
47+
/>
48+
<TooltipContent side="top" align="center">
49+
Create something new
50+
</TooltipContent>
51+
</Tooltip>
52+
<DropdownMenuContent align="center" side="top" sideOffset={6}>
53+
<DropdownMenuItem onClick={() => setModalOpen(true)}>
54+
<HashIcon size={14} className="text-gray-9" />
55+
New channel
56+
</DropdownMenuItem>
57+
<DropdownMenuItem
58+
onClick={() =>
59+
openTaskInput(inChannels ? { space: "website" } : undefined)
60+
}
61+
>
62+
<FileTextIcon size={14} className="text-gray-9" />
63+
New task
64+
</DropdownMenuItem>
65+
</DropdownMenuContent>
66+
</DropdownMenu>
67+
68+
<CreateChannelModal open={modalOpen} onOpenChange={setModalOpen} />
69+
</>
70+
);
71+
}

0 commit comments

Comments
 (0)