Skip to content

feat(code): canvas — experimental Channels space with gen-UI dashboards (project-bluebird)#2522

Merged
adamleithp merged 10 commits into
mainfrom
feat/canvas
Jun 10, 2026
Merged

feat(code): canvas — experimental Channels space with gen-UI dashboards (project-bluebird)#2522
adamleithp merged 10 commits into
mainfrom
feat/canvas

Conversation

@adamleithp

@adamleithp adamleithp commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

⚠️ Experimental

Everything new here is gated behind the project-bluebird feature 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, /website and /inbox redirect to /code, and the Code chrome is byte-for-byte as it is on main. So this is safe to land and iterate on behind the flag.

The app rail is the split point between the two UXs:

  • Code → the existing task/sessions app, exactly as today (the only space prod users see).
  • Channels → the new experimental space (channels, gen-UI dashboards, channel tasks).

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

Route What it does
/ Redirects to /code (no standalone home).
/code, /code/* The existing app — task list, sessions, inbox, settings. Unchanged.
/website Channels space index — lists the project's channels.
/website/$channelId A channel's dashboards grid (cards with live previews).
/website/$channelId/dashboards/$dashboardId A single dashboard — view, or edit with the gen-UI canvas + chat.
/website/$channelId/new New task scoped to the channel.
/website/$channelId/tasks/$taskId A channel-scoped task.
/website/$channelId/settings Channel settings.

The 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 folder row on the project's desktop_file_system surface. 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-render system prompt describing a fixed component catalog (Page, Grid, Card, Stat, Table, BarList, Heading, etc.). The agent:

  1. Calls PostHog MCP tools to fetch real project data (never fabricated).
  2. Streams back prose + json-render JSONL patches, which the main process assembles into a Spec (a root + flat element map) and forwards to the renderer to render live.
  3. For every metric it fills, it also records the exact HogQL that produced it under 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 same desktop_file_system surface:

  • Name = the row's last path segment = the canvas h1. Renaming PATCHes the row's path.
  • Spec + bookkeeping (channel id, timestamps) ride in the row's free-form meta JSON blob (typed/documented as DashboardFileMeta).
  • CRUD + refresh go through DashboardsService (main), which talks to the backend via authenticatedFetch. meta.spec is 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.md for the breadcrumb / naming / storage conventions.

image image image image image

Behind project-bluebird; no-op for users until we flip the flag.

@greptile-apps

greptile-apps Bot commented Jun 8, 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
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

Comment thread packages/ui/src/features/canvas/components/WebsiteLayout.tsx
Comment thread packages/ui/src/features/canvas/components/ChannelsList.tsx
@adamleithp adamleithp marked this pull request as ready for review June 8, 2026 10:53
@adamleithp

Copy link
Copy Markdown
Contributor Author

Addressed the review comments in 97a5b53:

  1. Silent save/fork errorsonSave now .catches and onFork is wrapped in try/catch, both surfacing a toast.
  2. Orphaned dashboards on channel delete — the channel's dashboards are now deleted (best-effort) before the channel folder, so our custom dashboard FS rows can't be left behind if the backend doesn't cascade them.
  3. N+1 preview fetchesdashboards.list now includes the spec (already loaded from the FS row's meta), so the grid renders previews from the single list call instead of one dashboards.get per card.

adamleithp and others added 7 commits June 10, 2026 11:17
Stage 1 of landing feat/canvas on the post-#2442 (package-split) main:

- canvas-gen, dashboards, dashboard-query services kept in apps/code/src/main/
  services, imports rewired to the new homes (@posthog/core/auth, @posthog/shared
  for TypedEventEmitter+AcpMessage, @posthog/workspace-server agent service+token).
- DI: tokens + strongly-typed MainBindings + container singleton binds.
- trpc: canvasGen + dashboards routers registered in the root router.
- Re-add @json-render/core dependency (dropped on reset).

Extend @posthog/workspace-server AgentService with systemPromptOverride (replaces
the default coding prompt for constrained surfaces) and disallowedTools (passed to
the Claude SDK), which the canvas generator needs to inject its json-render
catalog prompt and sandbox the agent to PostHog-MCP-only. The new shared agent API
had dropped both.

Code app + workspace-server + agent packages all typecheck clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ge graph

Stage 2-3 of landing feat/canvas on post-#2442 main. Everything typechecks
across all packages (22/22 turbo tasks).

Renderer (→ packages/ui/src/features/canvas):
- 27 files relocated; imports rewired to @posthog/ui/* + @posthog/* aliases.
- Stores/subscriptions get the host tRPC client via resolveService(HOST_TRPC_CLIENT)
  (non-React DI accessor); hooks use useHostTRPC.
- Website routes → packages/ui/src/router/routes/website*, route tree regenerated.

tRPC visibility fix (the renderer types against HostRouter):
- Canvas schemas + DI tokens + service interfaces → @posthog/core/canvas.
- canvas-gen + dashboards routers → packages/host-router, registered in hostRouter.
- Service classes stay in apps/code/main, bound to the core tokens in the container.

Channels API: add desktop_file_system methods (get/create/rename/delete) to
@posthog/api-client's PostHogAPIClient (the routes aren't in the generated OpenAPI).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Stage 4 — shell integration, gated by project-bluebird:
- Add PROJECT_BLUEBIRD_FLAG to @posthog/shared.
- Port the AppNav rail (Code ↔ Channels) into the canvas feature.
- __root renders the rail when the flag is on, and gives /website its own
  chrome-free Channels space (the channel sidebar + content come from
  WebsiteLayout). Code space gains the rail so users can switch.

Full repo typechecks (22/22 turbo tasks).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
WebsiteLayout only renders the content outlet; the channel list column lives in __root's channels-space branch (as before the port).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Web-readiness alignment with the package split:
- DashboardsService + DashboardQueryService → packages/core/src/canvas (they
  only need AuthService + fetch, so any host can run them). They inject the core
  AUTH_SERVICE token + ROOT_LOGGER and are bound via canvasCoreModule.
- CanvasGenService stays in apps/code/main: it bridges the agent runtime
  (@posthog/workspace-server) AND auth (@posthog/core), which can't be combined
  in a lower package without a dependency cycle (core → workspace-client →
  workspace-server → core). The app is the only layer that can depend on both.

Full repo typechecks (22/22 turbo tasks).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
adamleithp and others added 3 commits June 10, 2026 12:57
- Host boundary: move CanvasGenService out of apps/code (which must stay a thin
  Electron host — no @Injectable business services) into @posthog/host-router,
  the host-side package that already owns the canvas routers and depends on both
  @posthog/core (auth/schemas) and @posthog/workspace-server (agent), so no
  dependency cycle. The renderer imports host-router type-only, so no node code
  enters the bundle. Bound to CANVAS_GEN_SERVICE in the app container as before.
- react-doctor: replace two "adjust state on prop change" effects (reset-on-open
  in CreateChannelModal, countdown reset in DashboardRefreshControl) with the
  inline prev-prop comparison pattern — no stale-for-one-commit flash.

Full repo typechecks (22/22); host-boundary check + react-doctor both clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
# Conflicts:
#	packages/ui/src/router/routeTree.gen.ts
The /website routes stay registered regardless of project-bluebird, so a stale URL or restored session could strand a flag-off user in the channel layout rendered inside the Code chrome. Redirect them to Code once flags resolve, matching the pre-canvas behaviour exactly when the flag is off.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@adamleithp adamleithp merged commit 88dce58 into main Jun 10, 2026
18 of 19 checks passed
@adamleithp adamleithp deleted the feat/canvas branch June 10, 2026 12:42
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.

2 participants