Skip to content

feat(canvas): collapse channels by default, hash icon, and starred channels#2683

Draft
raquelmsmith wants to merge 4 commits into
mainfrom
posthog-code/canvas-collapsed-starred-channels
Draft

feat(canvas): collapse channels by default, hash icon, and starred channels#2683
raquelmsmith wants to merge 4 commits into
mainfrom
posthog-code/canvas-collapsed-starred-channels

Conversation

@raquelmsmith

@raquelmsmith raquelmsmith commented Jun 15, 2026

Copy link
Copy Markdown
Member

What

Reworks the Channels sidebar channel list:

  1. Channels start collapsed on load. Expansion is session-only — opening the list always begins fully collapsed (previously each channel's open/closed state was persisted in localStorage).
  2. # icon + caret-on-hover. Each channel shows a # by default; hovering the row swaps it for the expand/collapse caret. The header moved from quill's folder Collapsible to a Radix Collapsible so the trigger contents are controllable (same pattern as SidebarSection).
  3. Starred channels (user-specific), Slack-style. Starred channels appear in a Starred group pinned above the rest. Star/unstar via the hover star button on each row or the channel menu. Deleting a channel clears its star.

Storage: backend per-user shortcuts

Stars are persisted in the PostHog backend as per-user desktop file-system shortcuts — so they're user-specific and sync across devices (no PostHog/posthog change required; the capability already exists).

  • New api-client methods hit /api/projects/{teamId}/desktop_file_system_shortcut/ (the DesktopFileSystemShortcutViewSet, scoped to the desktop surface and the requesting user).
  • A channel is a folder, so its star is a folder-typed shortcut whose ref is the channel's raw file-system path — mirroring how PostHog's own project tree stars folders (projectTreeDataLogic). Channel now carries that raw path.
  • useChannelStars lists the user's shortcuts and maps ref → shortcut id; useChannelStarToggle stars (POST) / unstars (DELETE) with optimistic cache updates so the sidebar re-sorts instantly. Deleting a channel removes its shortcut too, so a later same-named channel doesn't inherit a stale star.

This replaces the earlier device-local approach (a zustand store), which has been removed.

Testing

  • pnpm typecheck (whole repo) — clean (22/22)
  • biome check on changed files — clean
  • Unit tests: useChannelStars.test.tsx (ref-mapping, star, unstar) + existing useChannels tests — passing

Draft for local testing.

Note

I haven't yet driven the running Electron app to visually confirm the hover/caret behavior, the Starred section, and a real star/unstar round-trip against the backend. Happy to do that and attach screenshots if you'd like a visual check before merging.

🤖 Generated with Claude Code

…annels

Reworks the channel list in the Channels sidebar:

- Channels always start collapsed on load. Expansion is now session-only
  state instead of being persisted per-channel in localStorage, so opening
  the list always begins fully collapsed.
- Each channel shows a `#` icon by default; hovering the row swaps it for
  the expand/collapse caret (the SidebarSection pattern). This moves the
  header off quill's `folder` Collapsible onto a Radix Collapsible so the
  trigger contents are fully controllable.
- Adds user-specific starred channels, Slack-style. Starred channels surface
  in a "Starred" group pinned above the rest of the list. Star/unstar via the
  hover star button on each row or the channel "…" menu. Starred state lives
  in a persisted zustand UI store (canvas-starred-channels) via the host
  storage backend — consistent with the rest of the canvas view state, and
  per-device. Deleting a channel also clears its star.

Note on storage: stars are persisted client-side per device rather than on
the PostHog user record. This keeps the change self-contained in this repo
(no PostHog/posthog backend change) and matches how channel UI state was
already handled. If we want cross-device sync later, the store is the single
seam to swap for a user-settings field.

Generated-By: PostHog Code
Task-Id: 79b30083-d838-4e23-8c62-09fc18c48bba
@github-actions

github-actions Bot commented Jun 15, 2026

Copy link
Copy Markdown

React Doctor found no issues in the changed files. 🎉

Reviewed by React Doctor for commit 3f86704.

@greptile-apps

greptile-apps Bot commented Jun 15, 2026

Copy link
Copy Markdown
Contributor
Prompt To Fix All With AI
Fix the following 3 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 3
packages/ui/src/features/canvas/components/ChannelsList.tsx:517-519
Dangling "Channels" label when all channels are starred. The `<ChannelGroupLabel>Channels</ChannelGroupLabel>` is rendered unconditionally whenever any channel is starred, but if every channel is starred then `otherChannels` is empty and the label floats below the Starred group with nothing underneath it.

```suggestion
            {otherChannels.length > 0 && <ChannelGroupLabel>Channels</ChannelGroupLabel>}
          </>
        )}
```

### Issue 2 of 3
packages/ui/src/features/canvas/starredChannelsStore.ts:11
`isStarred` is never used as a selector in any component — every callsite in `ChannelsList.tsx` inlines `s.starredIds.includes(channel.id)` directly (three separate sites). The method only appears in test assertions via `getState()`. Either replace all three inline `.includes()` usages in the component with `s.isStarred(channelId)`, or drop `isStarred` from the store and keep the inline form — as written, it's a superfluous export that duplicates logic that already lives at the call sites.

### Issue 3 of 3
packages/ui/src/features/canvas/starredChannelsStore.test.ts:8-44
House rules prefer parameterised tests. The toggle cases (star → unstarred; unstar → starred; order preserved) share the same setup/assertion shape and could be driven by a `it.each` table, making it easy to add future edge cases (e.g. starring an already-starred id, unstarring from an empty list) without duplicating boilerplate.

Reviews (1): Last reviewed commit: "feat(canvas): collapse channels by defau..." | Re-trigger Greptile

Comment on lines +517 to +519
<ChannelGroupLabel>Channels</ChannelGroupLabel>
</>
)}

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 Dangling "Channels" label when all channels are starred. The <ChannelGroupLabel>Channels</ChannelGroupLabel> is rendered unconditionally whenever any channel is starred, but if every channel is starred then otherChannels is empty and the label floats below the Starred group with nothing underneath it.

Suggested change
<ChannelGroupLabel>Channels</ChannelGroupLabel>
</>
)}
{otherChannels.length > 0 && <ChannelGroupLabel>Channels</ChannelGroupLabel>}
</>
)}
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/ui/src/features/canvas/components/ChannelsList.tsx
Line: 517-519

Comment:
Dangling "Channels" label when all channels are starred. The `<ChannelGroupLabel>Channels</ChannelGroupLabel>` is rendered unconditionally whenever any channel is starred, but if every channel is starred then `otherChannels` is empty and the label floats below the Starred group with nothing underneath it.

```suggestion
            {otherChannels.length > 0 && <ChannelGroupLabel>Channels</ChannelGroupLabel>}
          </>
        )}
```

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 3f86704 — the Channels section header now only renders when otherChannels.length > 0, so it no longer dangles when every channel is starred. (The code moved from ChannelGroupLabel to a collapsible ChannelGroup.)

// across projects, so a flat list needs no per-project scoping.
interface StarredChannelsStore {
starredIds: string[];
isStarred: (channelId: string) => boolean;

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.

P2 isStarred is never used as a selector in any component — every callsite in ChannelsList.tsx inlines s.starredIds.includes(channel.id) directly (three separate sites). The method only appears in test assertions via getState(). Either replace all three inline .includes() usages in the component with s.isStarred(channelId), or drop isStarred from the store and keep the inline form — as written, it's a superfluous export that duplicates logic that already lives at the call sites.

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/ui/src/features/canvas/starredChannelsStore.ts
Line: 11

Comment:
`isStarred` is never used as a selector in any component — every callsite in `ChannelsList.tsx` inlines `s.starredIds.includes(channel.id)` directly (three separate sites). The method only appears in test assertions via `getState()`. Either replace all three inline `.includes()` usages in the component with `s.isStarred(channelId)`, or drop `isStarred` from the store and keep the inline form — as written, it's a superfluous export that duplicates logic that already lives at the call sites.

How can I resolve this? If you propose a fix, please make it concise.

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!

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Obsolete — starredChannelsStore.ts was removed in the file-system shortcuts refactor (d27c908). Starred state now lives in useChannelStars (backed by desktop FS shortcuts); there is no isStarred selector duplicating .includes() anymore.

Comment on lines +8 to +44

it("stars a channel that is not yet starred", () => {
useStarredChannelsStore.getState().toggle("c1");

expect(useStarredChannelsStore.getState().starredIds).toEqual(["c1"]);
expect(useStarredChannelsStore.getState().isStarred("c1")).toBe(true);
});

it("toggles an already-starred channel back off", () => {
const { toggle } = useStarredChannelsStore.getState();
toggle("c1");
toggle("c1");

expect(useStarredChannelsStore.getState().starredIds).toEqual([]);
expect(useStarredChannelsStore.getState().isStarred("c1")).toBe(false);
});

it("preserves star order as channels are added", () => {
const { toggle } = useStarredChannelsStore.getState();
toggle("c1");
toggle("c2");

expect(useStarredChannelsStore.getState().starredIds).toEqual(["c1", "c2"]);
});

it("unstar removes a channel and is a no-op when absent", () => {
const { toggle, unstar } = useStarredChannelsStore.getState();
toggle("c1");
toggle("c2");

unstar("c1");
expect(useStarredChannelsStore.getState().starredIds).toEqual(["c2"]);

// Removing a channel that isn't starred leaves the list untouched.
unstar("missing");
expect(useStarredChannelsStore.getState().starredIds).toEqual(["c2"]);
});

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.

P2 House rules prefer parameterised tests. The toggle cases (star → unstarred; unstar → starred; order preserved) share the same setup/assertion shape and could be driven by a it.each table, making it easy to add future edge cases (e.g. starring an already-starred id, unstarring from an empty list) without duplicating boilerplate.

Context Used: Do not attempt to comment on incorrect alphabetica... (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/ui/src/features/canvas/starredChannelsStore.test.ts
Line: 8-44

Comment:
House rules prefer parameterised tests. The toggle cases (star → unstarred; unstar → starred; order preserved) share the same setup/assertion shape and could be driven by a `it.each` table, making it easy to add future edge cases (e.g. starring an already-starred id, unstarring from an empty list) without duplicating boilerplate.

**Context Used:** Do not attempt to comment on incorrect alphabetica... ([source](https://app.greptile.com/review/custom-context?memory=instruction-0))

How can I resolve this? If you propose a fix, please make it concise.

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!

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Obsolete — starredChannelsStore.test.ts was removed in the file-system shortcuts refactor (d27c908). The store it tested no longer exists.

…ortcuts

Replaces the device-local zustand store for starred channels with the
PostHog backend's per-user desktop file-system shortcuts, so stars are
user-specific and sync across devices.

- api-client: add desktop_file_system_shortcut methods (list/create/delete)
  on the "desktop" surface, hand-written like the existing desktop_file_system
  channel methods (these routes aren't in the generated OpenAPI client).
- A channel is a folder, so its star is a folder-typed shortcut whose `ref`
  is the channel's raw file-system path — matching how PostHog's own project
  tree stars folders. Channel now carries that raw `path` for the `ref`.
- useChannelStars / useChannelStarToggle: query the user's shortcuts and
  map ref → shortcut id, then star (POST) / unstar (DELETE) with optimistic
  cache updates so the sidebar re-sorts instantly. Deleting a channel also
  removes its shortcut so a later same-named channel doesn't inherit a stale
  star.
- Remove starredChannelsStore and its test; add useChannelStars tests.

The collapse-by-default behavior and the #/caret-on-hover header from the
previous commit are unchanged.

Generated-By: PostHog Code
Task-Id: 79b30083-d838-4e23-8c62-09fc18c48bba
Make the channel sidebar section headers first-class collapsible groups:
sentence-case labels at the channel-row font size, a leading star/hash icon
that swaps to a caret on hover, and an always-visible Starred section with an
empty-state prompt. Channel rows are indented under their section.

Move starring into the channel "..." menu (drop the inline star button) and
add an "Edit CONTEXT.md" item there, removing the CONTEXT.md row from the list.
Stop highlighting expanded channels persistently — they highlight on hover only.

Generated-By: PostHog Code
Task-Id: 14f68ad5-6084-4693-8d06-00d6f4c32319
Addresses Greptile P1: the "Channels" section header rendered even when
otherChannels was empty (all channels starred), leaving a dangling header.

Generated-By: PostHog Code
Task-Id: 14f68ad5-6084-4693-8d06-00d6f4c32319
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant