Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
cd97a1f
feat(code): add Slack-like canvas nav rail with Home and Code spaces
adamleithp Jun 4, 2026
93969b4
feat(code): add Home space canvas nav and blank /website canvas
adamleithp Jun 4, 2026
8044626
feat(code): generative UI canvas with PostHog data agent on /website
adamleithp Jun 4, 2026
fe4a7e6
fix(code): harden canvas rendering against mid-stream crashes
adamleithp Jun 4, 2026
471d5eb
feat(code): add top-level Inbox space in canvas nav
adamleithp Jun 4, 2026
13db7e6
feat(code): show inbox count badge on canvas nav Inbox item
adamleithp Jun 4, 2026
5d55e0b
feat(code): add Website space sub-nav, new task, and task views
adamleithp Jun 4, 2026
64a8c12
refactor(code): use Quill buttons for Home sidebar nav items
adamleithp Jun 4, 2026
94168b8
feat(code): website dashboards with breadcrumb dashboard picker
adamleithp Jun 4, 2026
9a1813b
feat(code): per-dashboard edit mode with gen-UI chat
adamleithp Jun 4, 2026
b942bc2
style(code): indent Website sub-nav items
adamleithp Jun 4, 2026
8b9a8a6
fix(code): always show all dashboards in the picker
adamleithp Jun 4, 2026
2b34d6d
feat(code): show dashboard count badge on Dashboards nav item
adamleithp Jun 4, 2026
bb08d78
feat(code): file-backed json-render dashboards with save/fork
adamleithp Jun 4, 2026
ba0a838
fix(code): surface dashboard creation errors instead of silent no-op
adamleithp Jun 4, 2026
40fdc4a
feat(code): name dashboards via save dialog or inline rename
adamleithp Jun 4, 2026
ee36f4e
revert(code): drop dashboard name dialog + inline rename
adamleithp Jun 4, 2026
4f49ff0
feat(code): dashboard refresh + polling control
adamleithp Jun 4, 2026
32de67d
fix(code): valid menu group for polling label + spin refresh while fe…
adamleithp Jun 4, 2026
8e63626
docs(code): add canvas/dashboards progress + MVP gaps
adamleithp Jun 4, 2026
209207f
feat(code): gate canvas feature behind project-bluebird flag
adamleithp Jun 4, 2026
8fb2664
feat(code): dashboard grid index with previews, delete, and nav polish
adamleithp Jun 4, 2026
18389ce
fix(code): seed canvas thread with saved spec on edit
adamleithp Jun 4, 2026
a60a364
feat(code): channel-scoped Home space (channels, dashboards, tasks, s…
adamleithp Jun 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 95 additions & 0 deletions CANVAS_MVP.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# Canvas / Dashboards — Progress & MVP gaps

Branch: `feat/canvas`. Generative-UI dashboards built from real PostHog data,
wrapped in a Slack-like multi-space shell.

## What's built

### Shell / navigation

- **Canvas nav rail** (`features/canvas/components/CanvasNav.tsx`) — Slack-like
left rail of square Quill buttons: **Home**, **Inbox**, **Code**. Reserves
macOS traffic-light space; draggable titlebar region.
- **Inbox** — top-level `/inbox` renders `InboxView` full-screen (no code
chrome); rail button shows a live count badge.
- **Home space** (`/`) — own `HomeSidebar` with Quill folder collapsibles and
Quill nav buttons. Website section: Dashboards (with count badge), New
dashboard, New task, Settings, and the list of created tasks.
- Root layout (`routes/__root.tsx`) branches: settings (full-screen),
inbox (full-screen), home space (rail + HomeSidebar), code (existing chrome).
Home-space detection centralized in `features/canvas/spaces.ts`.

### Website space (`/website/*`, layout in `WebsiteLayout.tsx`)

- Breadcrumb topbar: `Website > [crumb]` + right-aligned controls.
- **New task** — reuses `/code`'s `TaskInput` via its `onTaskCreated` seam;
created tasks route to `/website/tasks/$id` (tracked in
`websiteTasksStore`, persisted) and render with the reused `TaskDetail`.
- **Settings** — inert placeholder.

### Dashboards (file-backed json-render)

- **Main `DashboardsService`** (`main/services/dashboards/`) — each dashboard is
a JSON file (`{id, name, spec, createdAt, updatedAt}`) under
`<appData>/dashboards/`. tRPC `dashboards.list | get | create | update`.
- Dashboard route renders the saved json-render **spec read-only** via
`CanvasRenderer` (ErrorBoundary-guarded). Empty state when no spec.
- **Combobox switcher** in the breadcrumb (filtering disabled so all show).
- **Edit mode** (per-dashboard toggle) swaps the view for the **gen-UI canvas +
chat** for that dashboard's thread.
- **Save** (enabled only when the live spec differs from saved) writes the spec;
**Save as fork** copies the current spec into a new dashboard; **New
dashboard** / empty-state create a blank one and open it in edit mode.
- **Refresh + polling control** (`DashboardRefreshControl.tsx`) — Quill button
group `Refresh | ⚙`. Gear dropdown: Static / Polling (10s, 10min). Polling
counts down in the button ("Refreshing in XX"), pauses in edit mode, and the
icon spins (`motion-safe:animate-spin`) while fetching.

### Gen-UI engine

- `@json-render/core` + `@json-render/react`. Shared catalog
(`genui/catalog.ts`: Page/Grid/Card/Heading/Text/Stat/Table/BarList/Badge/
Divider) → `CANVAS_SYSTEM_PROMPT`. Radix registry in `genui/registry.tsx`.
- **Main `CanvasGenService`** reuses `AgentService` (PostHog MCP auto-enabled)
via a new `systemPromptOverride`, runs an ephemeral `__preview__` session per
thread with `bypassPermissions`, forwards ACP updates through a mixed-stream
parser to assemble the spec, and streams typed events over a tRPC
subscription. Multi-thread (one per dashboard).
- Renderer: thin multi-thread `canvasChatStore`, scoped subscription registrar,
`CanvasChat` panel.

## What's left for a real MVP

1. **Live data (biggest gap).** Dashboards store *static* specs — the agent
bakes numbers in at generation time. Refresh/polling currently just re-reads
the same file, so it's a visual no-op for saved dashboards. Real MVP needs
one of:
- re-run the agent on refresh to regenerate against fresh PostHog data, or
- json-render **data bindings** + a data-fetch layer the spec references
(preferred — cheap refresh, no re-generation).
The refresh/polling UI is already wired for whichever path.
2. **Verify the gen-UI agent end-to-end, live.** Not yet confirmed against a
real authed project: that the agent reliably emits valid json-render JSONL,
that PostHog MCP tools auto-approve under `bypassPermissions`, that the
prose/JSONL split is robust, and that it doesn't flood. May need
system-prompt tuning or gating the Claude Code file/bash tools off.
3. **Dashboard lifecycle.** No delete. Rename was reverted (dropdown only) —
add back if needed. Editing starts from a blank canvas rather than seeding
from the saved spec, so iterating on an existing dashboard restarts.
4. **Website task detail** lacks the code `HeaderRow` actions (branch selector,
handoff, skill buttons) since that chrome is intentionally absent — add a
website-space task toolbar if those are needed.
5. **Persistence niceties.** Polling choice is per-mount local state (resets on
reload); canvas chat threads aren't persisted (lost on reload). Dashboard
storage path isn't surfaced/configurable.
6. **Tests.** None yet for `DashboardsService`, `CanvasGenService`,
`canvasChatStore`, or the refresh control.
7. **States & polish.** Loading/error states for the dashboards list and gen-UI
stream; optional minimum spin duration so instant local refreshes still read
as deliberate.

## Dev caveat

Main-process changes (new services/routers: `dashboards`, `canvas-gen`,
`AgentService` edits) require a **full dev restart** — renderer HMR won't load
them. Symptom when stale: `No "mutation"-procedure on path "dashboards.create"`.
2 changes: 2 additions & 0 deletions apps/code/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,8 @@
"@dnd-kit/react": "^0.1.21",
"@fontsource-variable/inter": "^5.2.8",
"@joplin/turndown-plugin-gfm": "^1.0.67",
"@json-render/core": "^0.19.0",
"@json-render/react": "^0.19.0",
"@lezer/common": "^1.5.1",
"@lezer/highlight": "^1.2.3",
"@modelcontextprotocol/ext-apps": "^1.1.2",
Expand Down
4 changes: 4 additions & 0 deletions apps/code/src/main/di/container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,11 @@ import { AppLifecycleService } from "../services/app-lifecycle/service";
import { ArchiveService } from "../services/archive/service";
import { AuthService } from "../services/auth/service";
import { AuthProxyService } from "../services/auth-proxy/service";
import { CanvasGenService } from "../services/canvas-gen/service";
import { CloudTaskService } from "../services/cloud-task/service";
import { ConnectivityService } from "../services/connectivity/service";
import { ContextMenuService } from "../services/context-menu/service";
import { DashboardsService } from "../services/dashboards/service";
import { DeepLinkService } from "../services/deep-link/service";
import { EnrichmentService } from "../services/enrichment/service";
import { EnvironmentService } from "../services/environment/service";
Expand Down Expand Up @@ -114,6 +116,8 @@ container.bind(MAIN_TOKENS.ArchiveService).to(ArchiveService);
container.bind(MAIN_TOKENS.SuspensionService).to(SuspensionService);
container.bind(MAIN_TOKENS.AppLifecycleService).to(AppLifecycleService);
container.bind(MAIN_TOKENS.CloudTaskService).to(CloudTaskService);
container.bind(MAIN_TOKENS.CanvasGenService).to(CanvasGenService);
container.bind(MAIN_TOKENS.DashboardsService).to(DashboardsService);
container.bind(MAIN_TOKENS.ConnectivityService).to(ConnectivityService);
container.bind(MAIN_TOKENS.ContextMenuService).to(ContextMenuService);
container.bind(MAIN_TOKENS.DeepLinkService).to(DeepLinkService);
Expand Down
2 changes: 2 additions & 0 deletions apps/code/src/main/di/tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ export const MAIN_TOKENS = Object.freeze({
SuspensionService: Symbol.for("Main.SuspensionService"),
AppLifecycleService: Symbol.for("Main.AppLifecycleService"),
CloudTaskService: Symbol.for("Main.CloudTaskService"),
CanvasGenService: Symbol.for("Main.CanvasGenService"),
DashboardsService: Symbol.for("Main.DashboardsService"),
ConnectivityService: Symbol.for("Main.ConnectivityService"),
ContextMenuService: Symbol.for("Main.ContextMenuService"),

Expand Down
6 changes: 6 additions & 0 deletions apps/code/src/main/services/agent/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,12 @@ export const startSessionInput = z.object({
effort: effortLevelSchema.optional(),
model: z.string().optional(),
jsonSchema: z.record(z.string(), z.unknown()).nullish(),
/**
* When set, fully replaces the built system prompt (attribution / PR / branch
* conventions) with this text, keeping only the PostHog project-scoping line.
* Used by non-coding agent surfaces (e.g. the canvas generation agent).
*/
systemPromptOverride: z.string().optional(),
});

export type StartSessionInput = z.infer<typeof startSessionInput>;
Expand Down
20 changes: 19 additions & 1 deletion apps/code/src/main/services/agent/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,8 @@ interface SessionConfig {
model?: string;
/** JSON Schema for structured task output — when set, the agent gets a create_output tool */
jsonSchema?: Record<string, unknown> | null;
/** When set, replaces the default system prompt (keeps only project scoping) */
systemPromptOverride?: string;
}

interface ManagedSession {
Expand Down Expand Up @@ -474,10 +476,20 @@ export class AgentService extends TypedEventEmitter<AgentServiceEvents> {
taskId: string,
customInstructions?: string,
additionalDirectories?: string[],
systemPromptOverride?: string,
): {
append: string;
} {
let prompt = `PostHog context: use project ${credentials.projectId} on ${credentials.apiHost}. When using PostHog MCP tools, operate only on this project.`;
const projectContext = `PostHog context: use project ${credentials.projectId} on ${credentials.apiHost}. When using PostHog MCP tools, operate only on this project.`;

// Override mode: non-coding surfaces (e.g. canvas generation) get only the
// project-scoping line plus their own instructions — the attribution / PR /
// branch conventions below are irrelevant and would mislead the agent.
if (systemPromptOverride) {
return { append: `${projectContext}\n\n${systemPromptOverride}` };
}

let prompt = projectContext;

prompt += `

Expand Down Expand Up @@ -565,6 +577,7 @@ When creating pull requests, add the following footer at the end of the PR descr
effort,
model,
jsonSchema,
systemPromptOverride,
} = config;

// Preview config doesn't need a real repo — use a temp directory
Expand Down Expand Up @@ -625,6 +638,7 @@ When creating pull requests, add the following footer at the end of the PR descr
taskId,
customInstructions,
additionalDirectories,
systemPromptOverride,
);

const acpConnection = await agent.run(taskId, taskRunId, {
Expand Down Expand Up @@ -1546,6 +1560,10 @@ For git operations while detached:
effort: "effort" in params ? params.effort : undefined,
model: "model" in params ? params.model : undefined,
jsonSchema: "jsonSchema" in params ? params.jsonSchema : undefined,
systemPromptOverride:
"systemPromptOverride" in params
? params.systemPromptOverride
: undefined,
};
}

Expand Down
48 changes: 48 additions & 0 deletions apps/code/src/main/services/canvas-gen/schemas.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { z } from "zod";

// Input for generating / extending a canvas from a chat prompt.
export const canvasGenerateInput = z.object({
threadId: z.string().min(1),
prompt: z.string().min(1),
/**
* The json-render system prompt describing the component catalog. Computed in
* the renderer from the shared catalog and applied once when the ephemeral
* agent session for this thread is created.
*/
systemPrompt: z.string().min(1),
model: z.string().optional(),
});
export type CanvasGenerateInput = z.infer<typeof canvasGenerateInput>;

export const canvasThreadInput = z.object({ threadId: z.string().min(1) });
export type CanvasThreadInput = z.infer<typeof canvasThreadInput>;

// Events streamed to the renderer as the agent responds. `spec` carries the
// full assembled json-render Spec snapshot after each applied JSONL patch.
export const canvasStreamEventSchema = z.discriminatedUnion("type", [
z.object({ type: z.literal("started") }),
z.object({ type: z.literal("prose"), text: z.string() }),
z.object({
type: z.literal("spec"),
spec: z.record(z.string(), z.unknown()),
}),
z.object({
type: z.literal("tool"),
toolName: z.string(),
status: z.string(),
}),
z.object({ type: z.literal("done") }),
z.object({ type: z.literal("error"), message: z.string() }),
]);
export type CanvasStreamEvent = z.infer<typeof canvasStreamEventSchema>;

export const CanvasGenEvent = { Event: "canvas-event" } as const;

export interface CanvasGenEventPayload {
threadId: string;
event: CanvasStreamEvent;
}

export interface CanvasGenEvents {
[CanvasGenEvent.Event]: CanvasGenEventPayload;
}
Loading
Loading