feat(code): canvas — experimental Channels space with gen-UI dashboards (project-bluebird)#2522
Open
adamleithp wants to merge 47 commits into
Open
feat(code): canvas — experimental Channels space with gen-UI dashboards (project-bluebird)#2522adamleithp wants to merge 47 commits into
adamleithp wants to merge 47 commits into
Conversation
Wrap the app in a left app rail (square Quill icon-lg buttons) switching between a new Home space (/ hello-world scene + its own sidenav) and the existing Code app (/code). Rail reserves macOS traffic-light space and is a titlebar drag region. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Give the Home sidenav Quill folder collapsibles with a placeholder nav (Features, Resources). The Features > Website item opens a new blank /website canvas route. Centralize Home-space detection in canvas/spaces.ts so the whole space shares its chrome. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add @json-render/core + @json-render/react. The /website canvas now renders an agent-built, data-driven UI on the left with a chat panel hugging the right. - Main CanvasGenService reuses AgentService (PostHog MCP auto-enabled) to run an ephemeral __preview__ session per thread with a json-render system prompt and bypassPermissions, forwards ACP session updates through @json-render/core's mixed-stream parser to assemble the Spec, and streams typed events over tRPC. - AgentService gains systemPromptOverride to replace the coding-agent prompt for non-coding surfaces (keeps only project scoping). - Renderer: shared json-render catalog (Page/Grid/Card/Stat/Table/BarList/…) + Radix registry, a thin canvasChatStore, a scoped subscription registrar, and the chat/composer UI. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The renderer could OOM/crash while streaming a generated spec: partial or malformed mid-stream frames were rendered and could recurse infinitely. - Main only emits a spec once its root element actually exists (no partial frames). - Wrap CanvasRenderer in an ErrorBoundary keyed on the spec, so a bad frame is caught and rendering recovers on the next valid frame. - Only render when isNonEmptySpec(spec). - Make the canvas subscription idempotent per thread (guards StrictMode double-mounts from stacking IPC listeners). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add an Inbox rail item (above Code) that opens a full-screen /inbox route reusing InboxView, without the code app chrome (header/sidebar/space-switcher). The existing /code/inbox route is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a numeric Quill destructive badge to the Inbox rail button, mirroring the code sidebar's actionable-report count. Extract the count into a shared useInboxSignalCount hook that reuses the sidebar's query cache (no extra polling). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Expand the Website space with a breadcrumb topbar and sub-navigation: - /website is now a layout (breadcrumb topbar: Website > <page>) with children: index (canvas), new (TaskInput reused via onTaskCreated), settings (placeholder), and tasks/$taskId (TaskDetail reused). - New tasks created from /website/new route into the Website space at /website/tasks/$id (not /code) and are tracked in a persisted websiteTasksStore. - HomeSidebar gains a Website section: Canvas, New task, Settings, plus the list of created tasks to return to. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Render HomeSidebar nav items as Quill Button (variant default, size sm, full-width left-aligned), expressing active via data-selected. Active logic unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the Website "Canvas" entry with "Dashboards": /website redirects to the default dashboard at /website/dashboards/$dashboardId, which renders mock website-data dashboards (traffic, acquisition, engagement, conversion, performance). The breadcrumb second crumb is a Quill combobox to switch the active dashboard by name (Website > [dashboard]). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add an Edit toggle (Quill outline button, ml-auto, data-selected) to the dashboards breadcrumb bar. When active, the dashboard swaps its tiles for the gen-UI canvas + side chat input for that dashboard. Make the canvas chat store multi-thread (one thread per dashboard) so each dashboard keeps its own chat and generated canvas. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Disable base-ui combobox filtering (filter={null}) so the dashboard dropdown
always lists every dashboard instead of collapsing to the selected one.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the mock dashboards with real, file-backed ones: a main DashboardsService stores each dashboard as JSON (a json-render spec) under <appData>/dashboards. - Dashboards list, combobox, and sidebar count now come from the saved files. - A dashboard renders its saved spec read-only; Edit drops into the gen-UI canvas + chat for that dashboard's thread. - Topbar Save (enabled when the generated spec differs from saved) persists it; Save as fork copies the current spec into a new dashboard. - Empty state + sidebar "New dashboard" create a blank dashboard and open it in edit mode. Saved dashboards survive a refresh. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wrap create+navigate in try/catch with a toast + log so a failed New dashboard action (e.g. backend not reachable) no longer silently does nothing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Clicking Save now opens a Quill dialog to confirm/enter the dashboard name before persisting. The dashboard name in the breadcrumb (edit mode) is also inline-editable: hovering shows a faint border, clicking swaps to a Quill input sized to match the text (no layout shift), committing on Enter/blur. Save is disabled while renaming and re-enabled on blur. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Revert to the dropdown switcher in edit mode and a direct Save (no name dialog, no click-to-rename input). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a Quill button group (refresh | gear) to the dashboard topbar. Refresh
refetches the dashboard data; the gear dropdown selects Static (manual refresh)
or a Polling interval (10s, 10min). While polling, the main button counts down
("Refreshing in XX"). Polling pauses in edit mode so the data stays put.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tching Wrap the Polling label and interval items in DropdownMenuGroup so the Menu.GroupLabel has its required Menu.Group context (fixes the MenuGroupRootContext error). Spin the refresh icon (motion-safe:animate-spin) while the dashboard data is fetching. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The canvas nav rail and Home/Website/Inbox spaces mount in one place (__root.tsx). Gate that mount on a new project-bluebird flag: when off, the app is the pre-canvas code-only shell. Stranded users on a canvas-only path (cold-boot restore, stale deep link) are sent to /code once flags resolve, so flagged users aren't bounced off /website during the load window. - New PROJECT_BLUEBIRD_FLAG constant; defaults on in dev so local canvas work isn't hidden behind a flag PostHog doesn't serve locally. - New useFeatureFlagsLoaded hook to defer the redirect until a flag value is trustworthy rather than acting on the false-before-load default. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- /website index is now a 3-wide card grid of live dashboard previews
(CanvasRenderer scaled to a thumbnail) instead of redirecting to the first
dashboard. Clicking a card opens the full dashboard.
- Each card has a hover-revealed "..." menu (outline) with a destructive
Delete, rendered as a sibling of the card link so it never navigates.
- New dashboards.delete endpoint + deleteDashboard mutation (ENOENT treated as
success).
- HomeSidebar nav items now actually highlight the active route: Quill's Button
doesn't style data-selected, so consume it via a Tailwind utility (mirrors
SidebarItem) and gate the attr on `active || undefined`.
- Dashboard breadcrumbs are now Website > Dashboards [> {name}], with the
middle crumb linking back to the index.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Editing a saved dashboard rendered WebsiteCanvas, which only shows the chat thread's spec. A freshly opened board has no thread yet, so the canvas fell back to EMPTY_THREAD (spec: null) and looked wiped. Seed the thread from the saved dashboard spec when entering edit, via a new ensureSpec action that only hydrates an empty thread (never clobbers a live stream or in-session edits). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ettings) Replace the placeholder Website/Features/Resources nav with server-backed channels (desktop file-system folders). Each channel gets its own dashboards (channel-scoped, file-backed), tasks, and settings, routed under /website/$channelId. Adds channel create/delete, a Slack-style create modal, orphan-dashboard migration into the first channel, and breadcrumb/nav polish. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Edit mode: - Inline-edit static text (titles/labels) and drag-and-drop reorder via a key-aware EditRenderer over shared presentational bodies; an editable.ts "interpreter" gates which props are editable (literals only); data values show a locked "from query" hint. Cancel discards unsaved edits. Refreshable data: - Each Stat's HogQL is stored in spec.state.queries; a new DashboardQueryService runs them via the PostHog query endpoint and dashboards.refresh patches fresh values into the file (whole-board + per-card via ViewRenderer). Stat numbers are formatted at render. Hardening: - Canvas agent runs with a disallowedTools denylist (no file/shell/network) so bypassPermissions can't write files or run commands. - Refresh is hidden in edit mode so a Save can't clobber refreshed values. Plus CANVAS_MVP.md progress update. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Remove the Slack-like nav rail and Home/Inbox top-level spaces. The code sidebar gains a [ Tasks | Channels ] tab bar (below the command center, gated behind project-bluebird): Tasks is the existing list; Channels renders the channel list (extracted to ChannelsList). Selecting a channel opens its dashboards under /website/$channelId, now rendered inside the code chrome. - Delete CanvasNav, HomeSidebar, spaces.ts, the top-level /inbox route. - __root reverts to the code-only chrome; / redirects to /code. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When project-bluebird is on, the Tasks/Channels TabsList now sits outside the scroll container so it stays put while the list scrolls, and the gap below Command Center is tightened. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…stem The canvas harness now always emits a top-level h1 Heading as the dashboard title, and that h1 is the dashboard's name: saving syncs it to the file name. Dashboards move from the local JSON store to the PostHog desktop file system — each dashboard is a `dashboard`-typed row nested under its channel folder, with the json-render spec in `meta.spec`. Renaming the h1 PATCHes the row's path, so names stay in sync with the backend (the same surface that owns channel names). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Breadcrumbs (channel / Dashboards / name) and the dashboard controls now render in the window title bar via the header store instead of a second standalone bar, reclaiming ~40px of vertical space in the channel space. Adds the Dashboards crumb, muted-foreground link color, and consistent vertical alignment. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…AGENTS.md A dashboard's own name is its H1, so the breadcrumb stops at "Dashboards" (still a link back to the grid) instead of repeating the name. Restores right padding on the header content row. Documents the breadcrumb + naming + storage patterns in a canvas-feature AGENTS.md — notably: a page is not its own crumb, its H1 is. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A single dashboard shows "#channel / Dashboards" (Dashboards links back to the grid); the dashboards grid itself shows only "#channel" since its own h1 is the title. A page is never a crumb of itself. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Removes the adoptOrphans no-op (main service, tRPC procedure, renderer hook, and its ChannelsList call) left over from the local-file store. Documents the desktop file-system row's `meta` payload as a typed `DashboardFileMeta` zod schema, and notes the last-write-wins tradeoff on `meta.spec` at the refresh write + in AGENTS.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tabs Brings back the Slack-like app rail (gated behind project-bluebird) switching three top-level spaces: Code (the task app, untouched), Inbox (full-screen), and Channels (the website space with its channel-list sidebar + dashboards). Adds the top-level /inbox route and the rail/space branching in __root, with a redirect guard sending stranded rail-only paths to /code when the flag is off. Reverts the code sidebar changes: the Tasks/Channels tab bar is gone, the sidebar is the plain task list again. The Channels space gets its own chrome, so WebsiteLayout renders its own breadcrumb bar instead of the global header store. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The channel-list sidebar started flush at the top while the outlet's breadcrumb bar pushed its column down. Add a matching h-10 "Channels" bar above the list so both columns start on the same line, like /code's header sits above the sidebar. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replaces the small top "+" icon button with a full-width outline "New channel" button pinned to the bottom of the channels nav; the channel list scrolls above it. ChannelsList now owns its own scroll so the footer stays put. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drops the dnd-kit sortable/drag-handle reordering from the dashboard edit mode and the moveChild store action. Inline text editing and the hover frame stay; the drag affordance is gone. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The rail is now Code + Channels. Drops the Inbox rail item (and its signal-count badge) and the now-dead top-level /inbox route + space branch in __root. The in-code inbox (/code/inbox via the code sidebar) is unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…t menu - Breadcrumb: a faded # icon before the channel name. - Channel "..." menu: fit-content width, plus a new "Rename channel" item that opens a modal (PATCHes the desktop FS folder path via a new renameDesktopFileSystemChannel client method + renameChannel mutation). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ChannelGridLink renders a quill Button (default variant) that navigates, so the channel / Dashboards crumbs get the standard button hover instead of a bare link. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Top bar now holds only the breadcrumbs. A second bar below carries a (dead) Filter button on the left and the dashboard controls (refresh/edit/save) on the right. The leaf crumb is now a disabled quill button so it matches the clickable crumbs' shape instead of being bare text. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Top bar: breadcrumbs + primary actions on the right — New dashboard on the grid, Edit/Cancel/Save/Save-as-fork on a dashboard. - Toolbar below: a (now outline) Filter on the left, just Refresh on the right. - Drop the in-page "Dashboards" h1; it's now the current (disabled) breadcrumb on the grid. New-dashboard header row removed from the grid page. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The toolbar (Filter + refresh) now renders only on the dashboards grid and a single dashboard, not on the channel's tasks/settings views. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds a nested "Sessions" folder collapsible inside each channel (same styling + child indent) holding New task, the channel's task list, and five dummy status filters (Backlog/Todo/Needs Review/Done/Cancelled) with status icons. NavButton gains an optional leading icon. Settings moves below the group. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Contributor
Prompt To Fix All With AIFix the following 3 code review issues. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 3
apps/code/src/renderer/features/canvas/components/WebsiteLayout.tsx:82-100
Both `onSave` and `onFork` silently discard errors. `onSave` wraps `saveDashboard` (which calls `mutateAsync`) in `void`, so any server error is dropped with no user feedback. `onFork` is `async` with `await createDashboard` but no `try/catch`; since React doesn't handle rejected Promises from click handlers, a network or server failure becomes an unhandled rejection and the user sees nothing.
```suggestion
const onSave = () => {
if (!dirty) return;
// The h1 title is the dashboard's name: sync it to the file on every save so
// renaming the canvas title renames the saved dashboard.
saveDashboard(dashboardId, liveSpec, dashboardTitleFromSpec(liveSpec)).catch(
(error) => {
toast.error("Couldn't save dashboard", {
description: error instanceof Error ? error.message : String(error),
});
},
);
};
const onFork = async () => {
if (!hasSpec) return;
try {
const title =
dashboardTitleFromSpec(liveSpec) ?? dashboard?.name ?? "Dashboard";
const name = `${title} (fork)`;
const record = await createDashboard(channelId, name, liveSpec);
setEditing(record.id, true);
void navigate({
to: "/website/$channelId/dashboards/$dashboardId",
params: { channelId, dashboardId: record.id },
});
} catch (error) {
toast.error("Couldn't fork dashboard", {
description: error instanceof Error ? error.message : String(error),
});
}
};
```
### Issue 2 of 3
apps/code/src/renderer/features/canvas/components/ChannelsList.tsx:144-156
**Orphaned dashboards on channel deletion**
`deleteChannel` removes the channel folder from the backend, but dashboards stored as child `desktop_file_system` entries (with `meta.channelId === channel.id`) are not cleaned up. If the backend doesn't cascade-delete children, those dashboard rows become orphaned — invisible in any channel list but still consuming storage and accumulating over time. Consider fetching and deleting child dashboards before (or concurrently with) deleting the folder, or at least documenting a backend expectation that DELETE cascades.
### Issue 3 of 3
apps/code/src/renderer/features/canvas/components/WebsiteDashboardsIndex.tsx:101-112
**N+1 IPC/API calls for dashboard preview thumbnails**
Each `DashboardCard` calls `useDashboard(summary.id)` to load the full spec for its preview. For a channel with N dashboards, opening the grid fires N+1 round trips (1 `dashboards.list` in main → N individual `dashboards.get` calls). The `staleTime: 5_000` helps on repeat views but the initial load is unbounded. Consider either including the spec in the list endpoint response, or adding a bulk-fetch procedure so the grid can be populated in a single call.
Reviews (1): Last reviewed commit: "feat(canvas): rename the channel "Sessio..." | Re-trigger Greptile |
- Surface save/fork failures with a toast instead of swallowing the rejected promise (onSave .catch, onFork try/catch). - Delete a channel's dashboards before the channel folder so our custom "dashboard" FS rows can't be orphaned if the backend doesn't cascade them. - Include the spec in dashboards.list (already loaded from the FS row's meta) so the grid renders previews without an N+1 of dashboards.get per card. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Contributor
Author
|
Addressed the review comments in 97a5b53:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Everything new here is gated behind the
project-bluebirdfeature flag (on by default in dev builds only). With the flag off — every production user today — the app is the existing code-only shell, untouched: no nav rail,/websiteand/inboxredirect to/code, and the Code chrome is byte-for-byte as it is onmain. So this is safe to land and iterate on behind the flag.The app rail is the split point between the two UXs:
What this adds
A Slack-like vertical app nav rail (
AppNav) that switches between top-level "spaces", plus a whole new Channels space built on the PostHog desktop file system API.New routes
//code(no standalone home)./code,/code/*/website/website/$channelId/website/$channelId/dashboards/$dashboardId/website/$channelId/new/website/$channelId/tasks/$taskId/website/$channelId/settingsThe Channels space has its own chrome (rail + a persistent channel-list sidebar + its own breadcrumb/toolbar bars) rather than the Code header/sidebar.
Channels = desktop file-system folders
A channel is a top-level
folderrow on the project'sdesktop_file_systemsurface. Create / rename / delete go straight through the REST API, so channel names live on the backend and sync across clients.Dashboards & gen UI
A dashboard is a small, live, data-driven view of the current PostHog project — built conversationally rather than hand-configured.
How the gen UI works. Each dashboard has a chat thread. A PostHog agent (reusing the existing agent + PostHog MCP server) runs with a
json-rendersystem prompt describing a fixed component catalog (Page, Grid, Card, Stat, Table, BarList, Heading, etc.). The agent:json-renderJSONL patches, which the main process assembles into a Spec (a root + flat element map) and forwards to the renderer to render live.state.queries, so the dashboard can be refreshed (or polled) later by re-running those queries and patching the values back in.The dashboard always opens with a top-level h1 Heading — that h1 is the dashboard's name. Editing it renames the dashboard. Inline text editing is supported in edit mode (drag-and-drop reordering was removed).
How it's backed by the file-system API. Dashboards are not local files — each one is a
dashboard-typed row nested under its channel folder on the samedesktop_file_systemsurface:path.metaJSON blob (typed/documented asDashboardFileMeta).DashboardsService(main), which talks to the backend viaauthenticatedFetch.meta.specis currently last-write-wins (no versioning) — fine for now, revisit if multi-client editing becomes real.This keeps dashboards and their names in sync with the backend — the same surface that owns channel names.
See
apps/code/src/renderer/features/canvas/AGENTS.mdfor the breadcrumb / naming / storage conventions.Behind
project-bluebird; no-op for users until we flip the flag.