Skip to content

feat(canvas): build and securely deliver cloud artifacts - #3835

Open
k11kirky wants to merge 27 commits into
mainfrom
posthog-code/canvas-build-pipeline-phase-6
Open

feat(canvas): build and securely deliver cloud artifacts#3835
k11kirky wants to merge 27 commits into
mainfrom
posthog-code/canvas-build-pipeline-phase-6

Conversation

@k11kirky

@k11kirky k11kirky commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Problem

Canvases rode on the desktop "file system": the record was an untyped meta blob (code, version snapshots, pointers) merged read-modify-write, and every channel feature did two lookups — the file-system folder plus a backend channel resolved by name. The build pipeline's output was also invisible in practice: the builds poll couldn't start itself for agent-driven builds, every poll tick reloaded the iframe, and build failures hid in an edit-mode-only tooltip. The backend remodel (PostHog/posthog#73874) retires that surface; this PR moves the client onto it and fixes the build-lifecycle UX.

Changes

  • New data layer: canvases are typed rows on the canvases API (ProjectApiClient + rewritten DashboardsService); source lives in server-side versions (source/versions/revert), the rendered output is the published build's artifact. The freeform store shrinks to view state; undo/redo browse server versions and revert republishes (and rebuilds).
  • One channel identity: the backend channel UUID keys the sidebar, stars, CONTEXT.md instructions, feeds, task filing (now a PATCH of the task's channel), routes, and deep links. The name-matching bridge (useBackendChannel) and the file-system client are deleted. The home canvas is an is_home row seeded via a real publish; its board reads system.canvases/system.tasks.
  • Build lifecycle UX: builds poll while a generation runs and refresh when it finishes; the artifact URL is pinned per build (no iframe reloads on poll ticks, with signed-URL-expiry recovery); queued/building states show elapsed time; failures surface in both view and edit modes with diagnostics and an "Ask agent to fix" affordance; the completion toast verifies the build before announcing success.
  • Tidy-up: agent prompts use the canvas-*/channel-* tools; the unused local-preview pipeline (canvas-preview, the workspace-server canvas builder, its divergent contract) is deleted, with the platform manifest vendored from the server's single source of truth for the whitelist contract test; assertCanvasCapability moves to @posthog/core. Net −2k lines.

Pairs with PostHog/posthog#73874 — land the backend first, this same day (the old routes are deleted server-side).

Canvas-mode submits no longer pre-create a canvas: the channel composer used to mint an "Untitled canvas" before starting the generation task. It now just starts the task and the agent resolves the target itself — building on a matching existing canvas in the channel or creating a descriptively-named one (the building-canvases skill's resolve step). While a generation runs on a not-yet-renderable canvas, the run's chat panel opens by default, in view mode too. Cloud “Generating” state now ends with the agent's turn (via the session's prompt-pending signal) instead of persisting through the sandbox's follow-up linger window until the inactivity timeout.

Heads-up for existing sessions: the server grows a canvas scope, and OAuth grants narrowed to an app's scope ceiling enumerate their scopes at grant time (refresh never widens them) — so pre-existing sessions would 403 on every canvases call. OAUTH_SCOPE_VERSION is bumped so those sessions re-auth once and pick the scope up.

Rebased onto the code-spaces-layout work: the new spaces sidebar (search-driven Autocomplete list, hotkey slots, activity hover popover) is adapted to this PR's data layer — the personal channel is found by channel_type instead of lazily provisioned client-side, starring uses the per-user starred flag, unread state keys by channel id, and the activity feed's name→id channel bridge is removed in favor of the channel id the items now carry.

How did you test this?

Automated: full monorepo typecheck; Biome clean; check-host-boundaries (no new violations); 174 core canvas tests + 252 UI canvas tests + 681 shared tests, all updated to the new behavior (service/API mapping, home-canvas seeding incl. declared capabilities and TSX-valid seed, build lifecycle normalization, whitelist↔manifest contract, hooks over the new channel endpoints, store/view-state suites). Not done: no manual end-to-end run against a deployed backend from this session — the generate → build → artifact flow should get one manual pass in the dev stack before the pair merges.


Created with PostHog Code

@github-actions

github-actions Bot commented Jul 27, 2026

Copy link
Copy Markdown

React Doctor found 4 issues in 1 file · 4 warnings.

4 warnings

src/features/canvas/freeform/FreeformCanvasView.tsx

Reviewed by React Doctor for commit 06f431f.

@k11kirky
k11kirky force-pushed the posthog-code/canvas-build-pipeline-phase-6 branch from 7c3e20f to af2a629 Compare July 28, 2026 11:23
@k11kirky k11kirky changed the title feat(canvas): support binary assets and module workers feat(canvas): build and securely deliver cloud artifacts Jul 28, 2026
@k11kirky
k11kirky changed the base branch from posthog-code/canvas-build-pipeline-phase-5 to main July 28, 2026 11:26
@trunk-io

trunk-io Bot commented Jul 28, 2026

Copy link
Copy Markdown

Merging to main in this repository is managed by Trunk.

  • To merge this pull request, check the box to the left or comment /trunk merge below.

After your PR is submitted to the merge queue, this comment will be automatically updated with its status. If the PR fails, failure details will also be posted here

@greptile-apps

greptile-apps Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Too many files changed for review. (104 files found, 100 file limit)

Bypass the limit by tagging @greptile-apps to review.

@k11kirky
k11kirky force-pushed the posthog-code/canvas-build-pipeline-phase-6 branch from 0d9ed53 to c9c7332 Compare July 28, 2026 20:09
Comment thread packages/ui/src/features/canvas/freeform/BuiltCanvas.tsx Outdated
@veria-ai

veria-ai Bot commented Jul 28, 2026

Copy link
Copy Markdown

PR overview

All previously flagged issues have been addressed. No open security concerns remain on this pull request.

Security review

No open security issues remain on this pull request.

Fixed/addressed: 2 · PR risk: 0/10

k11kirky added a commit to PostHog/posthog that referenced this pull request Jul 28, 2026
…cates

Channel-composer generation tasks no longer arrive with a pre-created canvas id (PostHog/code#3835), so the building-canvases skill's resolve step now spells out the flow: list the channel's canvases, build on a clearly-matching one and say so in the reply, and only otherwise create — with a short descriptive name, never "Untitled canvas".

Generated-By: PostHog Code
Task-Id: b548ad79-52a8-496c-a064-6dfc4c61b688
posthog Bot added a commit to PostHog/posthog that referenced this pull request Jul 29, 2026
…cates

Channel-composer generation tasks no longer arrive with a pre-created canvas id (PostHog/code#3835), so the building-canvases skill's resolve step now spells out the flow: list the channel's canvases, build on a clearly-matching one and say so in the reply, and only otherwise create — with a short descriptive name, never "Untitled canvas".

Generated-By: PostHog Code
Task-Id: b548ad79-52a8-496c-a064-6dfc4c61b688
posthog Bot added a commit to PostHog/posthog that referenced this pull request Jul 29, 2026
…cates

Channel-composer generation tasks no longer arrive with a pre-created canvas id (PostHog/code#3835), so the building-canvases skill's resolve step now spells out the flow: list the channel's canvases, build on a clearly-matching one and say so in the reply, and only otherwise create — with a short descriptive name, never "Untitled canvas".

Generated-By: PostHog Code
Task-Id: b548ad79-52a8-496c-a064-6dfc4c61b688
@posthog

posthog Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

🤖 CI You Later — automated babysit pass. CI is fully green on ba30647; the branch conflicts with main.

I did not push anything. Resolving this needs a rebase + force-push (GitHub can't auto-merge, so the signed-merge API refuses), and rewriting your commits is outside what I'm allowed to do unattended. So here is the resolution I derived and verified locally instead.

The conflict

One commit on main collides: bf01d21a6 fix(channels): keep foreign canvases out of me (#3888). It makes personal-channel (#me) ownership decided solely by the creator uuidauthorUuid/createdByUuid threaded through, display names demoted to display-only, rows with no creator failing closed. It threaded that uuid through the file-system data layer that this PR deletes.

5 files conflict. channelItems.ts, channelItems.test.ts and ChannelItemRow.test.tsx auto-merge cleanly and carry main's fix as-is.

Resolution

  • dashboardSchemas.ts — take this PR's side wholesale (main's hunk only touches the deleted dashboardFileMetaSchema/dashboardSummarySchema), then add createdByUuid to dashboardRecordSchema.
  • dashboardsService.ts — take this PR's side for both hunks (main edited the deleted FS toRecord), then map the uuid in the new API toRecord. ApiCanvas.created_by already carries uuid, so nothing new is needed from the backend.
  • useChannelItems.tsxdrop both sides of the import conflict: userDisplayName is unused after main's change, and useBackendChannel/PERSONAL_CHANNEL_NAME are deleted by this PR. The hook body auto-merges to the uuid-only version.
  • useChannelItems.test.tsx — keep this PR's channel() helper and main's widened canvas(id, createdBy?, createdByUuid?) signature.
  • dashboardsService.test.tsmain's two new ownership tests survive the auto-merge but call the deleted dashboardRow/fakeFs helpers. I ported them onto this PR's apiCanvas/fakeApi fake rather than dropping them; assertions unchanged.
Patch for the non-mechanical parts (apply after resolving as above)
diff --git a/packages/core/src/canvas/dashboardSchemas.ts b/packages/core/src/canvas/dashboardSchemas.ts
index 8ccabb805..4a18c7e36 100644
--- a/packages/core/src/canvas/dashboardSchemas.ts
+++ b/packages/core/src/canvas/dashboardSchemas.ts
@@ -15,8 +15,13 @@ export const dashboardRecordSchema = z.object({
   // Id of the task currently generating this canvas (freeform gen runs as a
   // dedicated task, like CONTEXT.md). null/absent = no generation in flight.
   generationTaskId: z.string().nullish(),
-  // Display name of the creator (from the backend's created_by user).
+  // Display name of the creator (from the backend's created_by user). Display
+  // only — a name is not an identity; ownership uses createdByUuid.
   createdBy: z.string().optional(),
+  // Stable uuid of the creator (from the backend's created_by user). This, not
+  // the display name, decides personal-channel ownership. Absent for rows the
+  // API returns without a creator (e.g. once the creator is deleted).
+  createdByUuid: z.string().optional(),
   createdAt: z.number(),
   updatedAt: z.number(),
   // Epoch ms the canvas was pinned to its channel; absent = not pinned.
diff --git a/packages/core/src/canvas/dashboardsService.test.ts b/packages/core/src/canvas/dashboardsService.test.ts
index 007b74bd4..f60e25919 100644
--- a/packages/core/src/canvas/dashboardsService.test.ts
+++ b/packages/core/src/canvas/dashboardsService.test.ts
@@ -75,6 +75,30 @@ describe("DashboardsService.list", () => {
     });
     expect(rows[0].createdAt).toBe(Date.parse("2026-07-01T00:00:00Z"));
   });
+
+  it("maps the backend creator uuid onto records", async () => {
+    const { api } = fakeApi({
+      "canvases/?channel=chan-1": [
+        apiCanvas({ created_by: { uuid: "creator-uuid" } }),
+      ],
+    });
+
+    const [result] = await new DashboardsService(api).list("chan-1");
+
+    expect(result.createdByUuid).toBe("creator-uuid");
+  });
+
+  // The backend sends `created_by: null` once the creator is deleted. Leaving
+  // the uuid undefined is what makes such a row fail closed out of #me.
+  it("leaves the creator uuid undefined when the row has no creator", async () => {
+    const { api } = fakeApi({
+      "canvases/?channel=chan-1": [apiCanvas({ created_by: null })],
+    });
+
+    const [result] = await new DashboardsService(api).list("chan-1");
+
+    expect(result.createdByUuid).toBeUndefined();
+  });
 });
 
 describe("DashboardsService.ensureHomeCanvas", () => {
diff --git a/packages/core/src/canvas/dashboardsService.ts b/packages/core/src/canvas/dashboardsService.ts
index 650ce8d3c..facb66046 100644
--- a/packages/core/src/canvas/dashboardsService.ts
+++ b/packages/core/src/canvas/dashboardsService.ts
@@ -51,6 +51,7 @@ interface ApiCanvas {
   current_version_id: string | null;
   published_build_id: string | null;
   created_by?: {
+    uuid: string;
     first_name?: string | null;
     last_name?: string | null;
     email?: string | null;
@@ -92,6 +93,9 @@ function toRecord(api: ApiCanvas): DashboardRecord {
     context: api.context ?? "",
     generationTaskId: api.generation_task_id,
     createdBy: creatorLabel(api.created_by),
+    // Undefined when the API returns no creator, which is what makes such a row
+    // fail closed out of the personal channel (see isOwnedBy in channelItems).
+    createdByUuid: api.created_by?.uuid,
     createdAt: toEpoch(api.created_at) ?? 0,
     updatedAt: toEpoch(api.updated_at) ?? 0,
     pinnedAt: toEpoch(api.pinned_at),

Verified on the resolved tree: pnpm typecheck clean (24/24 packages) · biome check packages/core packages/ui clean · check-host-boundaries no new violations · 2603 core tests and 2327 UI tests pass (incl. 182 core + 289 UI canvas tests).

Still needs you

  1. The rebase itself — I can't force-push over your commits.
  2. @veria-ai's High finding on BuiltCanvas.tsx:58 (iframe bridge survives self-navigation) — left alone deliberately. A MessageChannel handshake or teardown-on-navigate changes the host↔artifact protocol on both sides, and this PR ships multi-page artifacts with navigate intents, so "tear down on navigation" isn't obviously safe. Your call, not mine. Note the wildcard postMessage target can't be tightened here — sandbox="allow-scripts" without allow-same-origin gives the frame an opaque origin.

I'll re-check on the next pass and won't repeat this comment.

@k11kirky
k11kirky force-pushed the posthog-code/canvas-build-pipeline-phase-6 branch from 6387168 to f31828d Compare July 29, 2026 11:40
k11kirky added a commit to PostHog/posthog that referenced this pull request Jul 29, 2026
…cates

Channel-composer generation tasks no longer arrive with a pre-created canvas id (PostHog/code#3835), so the building-canvases skill's resolve step now spells out the flow: list the channel's canvases, build on a clearly-matching one and say so in the reply, and only otherwise create — with a short descriptive name, never "Untitled canvas".

Generated-By: PostHog Code
Task-Id: b548ad79-52a8-496c-a064-6dfc4c61b688
@k11kirky
k11kirky force-pushed the posthog-code/canvas-build-pipeline-phase-6 branch from eb4e2f8 to 73fd317 Compare July 29, 2026 12:36
Comment thread packages/core/src/canvas/generationPrompt.ts Outdated
@posthog

posthog Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

👋 Visual changes detected for this PR.

Review and approve in PostHog Visual Review

If these changes are unexpected, they may be caused by a flaky test or a broken snapshot on master. Don't approve — rerun the job or wait for a fix.

k11kirky added a commit to PostHog/posthog that referenced this pull request Jul 29, 2026
…cates

Channel-composer generation tasks no longer arrive with a pre-created canvas id (PostHog/code#3835), so the building-canvases skill's resolve step now spells out the flow: list the channel's canvases, build on a clearly-matching one and say so in the reply, and only otherwise create — with a short descriptive name, never "Untitled canvas".

Generated-By: PostHog Code
Task-Id: b548ad79-52a8-496c-a064-6dfc4c61b688
k11kirky added a commit to PostHog/posthog that referenced this pull request Jul 29, 2026
…cates

Channel-composer generation tasks no longer arrive with a pre-created canvas id (PostHog/code#3835), so the building-canvases skill's resolve step now spells out the flow: list the channel's canvases, build on a clearly-matching one and say so in the reply, and only otherwise create — with a short descriptive name, never "Untitled canvas".

Generated-By: PostHog Code
Task-Id: b548ad79-52a8-496c-a064-6dfc4c61b688
k11kirky and others added 21 commits July 31, 2026 20:07
The `quality` CI job (`biome ci .`) failed with two format errors on lines
this branch added: a wrapped `expect(...).not.toContain(...)` call in
prompt-builder.test.ts and the collapsed-panel JSX guard in
FreeformCanvasView.tsx. Reformatted with `biome format --write` so
`biome ci` exits clean; no behaviour change.

Generated-By: PostHog Code
Task-Id: 4928140a-2ab8-4fb3-86c1-e02d25f60cca
…ffect

react-doctor blocked CI with a `no-adjust-state-on-prop-change` error at
CanvasBuildStatus.tsx: the effect called `setNow(Date.now())` to restart the
clock when a new build became active, so the first commit after a build change
briefly rendered the previous build's elapsed time.

The `now` state was only ever read to compute the label, so drop it: the
interval now just bumps a tick to force a re-render and the label is computed
from `Date.now()` during render. This removes the duplicated state (one of the
two remedies react-doctor suggests) rather than adding a `prev`-prop
comparison, and it also means the label can never be stale.

react-doctor and `biome ci` are clean; @posthog/ui typecheck plus the
@posthog/ui (2323) and @posthog/core (2595) unit suites pass.

Generated-By: PostHog Code
Task-Id: 4928140a-2ab8-4fb3-86c1-e02d25f60cca
Generated-By: PostHog Code
Task-Id: 53286c7c-92b5-42a3-8e73-7f45d68382c8
Generated-By: PostHog Code
Task-Id: 53286c7c-92b5-42a3-8e73-7f45d68382c8
Generated-By: PostHog Code
Task-Id: 53286c7c-92b5-42a3-8e73-7f45d68382c8
Generated-By: PostHog Code
Task-Id: 53286c7c-92b5-42a3-8e73-7f45d68382c8
8 updated
Run: 556c77f3-ca53-4b8c-b076-ce14c09c6b1b

Co-authored-by: k11kirky <4571455+k11kirky@users.noreply.github.com>
Generated-By: PostHog Code
Task-Id: 45da9f78-5b15-4cfe-a8a5-9afcbd6b4755
Generated-By: PostHog Code
Task-Id: 45da9f78-5b15-4cfe-a8a5-9afcbd6b4755
Generated-By: PostHog Code
Task-Id: 45da9f78-5b15-4cfe-a8a5-9afcbd6b4755
Generated-By: PostHog Code
Task-Id: 45da9f78-5b15-4cfe-a8a5-9afcbd6b4755
Generated-By: PostHog Code
Task-Id: 45da9f78-5b15-4cfe-a8a5-9afcbd6b4755
Generated-By: PostHog Code
Task-Id: 45da9f78-5b15-4cfe-a8a5-9afcbd6b4755
Generated-By: PostHog Code
Task-Id: 45da9f78-5b15-4cfe-a8a5-9afcbd6b4755
Generated-By: PostHog Code
Task-Id: 45da9f78-5b15-4cfe-a8a5-9afcbd6b4755
Generated-By: PostHog Code
Task-Id: 45da9f78-5b15-4cfe-a8a5-9afcbd6b4755
Generated-By: PostHog Code
Task-Id: 45da9f78-5b15-4cfe-a8a5-9afcbd6b4755
Generated-By: PostHog Code
Task-Id: 45da9f78-5b15-4cfe-a8a5-9afcbd6b4755
Reuse: use escapeXmlAttr from @posthog/shared for prompt metadata; derive
platform pins (react version, canvasSdkVersion) from CANVAS_PLATFORM_MANIFEST;
route the dashboards-grid delete through deleteCanvasWithUndo.

Simplification: single build-record mapping shared by toBuildRecord and
tryToBuildRecord; de-duplicate the ActivityView feed body across layouts;
drop the freeformChatStore LRU/mount bookkeeping now that per-thread state
is tiny; inline the one-line canvasSourcePresentation helpers; delete the
orphaned desktop file-system client methods.

Efficiency: stop re-validating build records in polled getBuilds; bound
canvas data payloads without a per-request TextEncoder; keep onDataRequest
referentially stable across builds polls; resolve the home canvas from the
query cache before hitting ensureHomeCanvas.

Altitude: extract a shared canvas host message router used by BuiltCanvas
and FreeformCanvas (both paths now get the concurrency/size/timeout guards);
move capability gating onto BuiltCanvas via a capabilities prop; extract the
pinned-artifact lifecycle into usePinnedArtifact.

Generated-By: PostHog Code
Task-Id: aea6c5f4-02ae-448b-b4dc-f10f581ad597
@charlesvien
charlesvien force-pushed the posthog-code/canvas-build-pipeline-phase-6 branch from ea7a3c6 to 06f431f Compare August 1, 2026 03:35
charlesvien pushed a commit to PostHog/posthog that referenced this pull request Aug 1, 2026
…cates

Channel-composer generation tasks no longer arrive with a pre-created canvas id (PostHog/code#3835), so the building-canvases skill's resolve step now spells out the flow: list the channel's canvases, build on a clearly-matching one and say so in the reply, and only otherwise create — with a short descriptive name, never "Untitled canvas".

Generated-By: PostHog Code
Task-Id: b548ad79-52a8-496c-a064-6dfc4c61b688
k11kirky added a commit to PostHog/posthog that referenced this pull request Aug 1, 2026
…cates

Channel-composer generation tasks no longer arrive with a pre-created canvas id (PostHog/code#3835), so the building-canvases skill's resolve step now spells out the flow: list the channel's canvases, build on a clearly-matching one and say so in the reply, and only otherwise create — with a short descriptive name, never "Untitled canvas".

Generated-By: PostHog Code
Task-Id: b548ad79-52a8-496c-a064-6dfc4c61b688
k11kirky added a commit to PostHog/posthog that referenced this pull request Aug 3, 2026
…cates

Channel-composer generation tasks no longer arrive with a pre-created canvas id (PostHog/code#3835), so the building-canvases skill's resolve step now spells out the flow: list the channel's canvases, build on a clearly-matching one and say so in the reply, and only otherwise create — with a short descriptive name, never "Untitled canvas".

Generated-By: PostHog Code
Task-Id: b548ad79-52a8-496c-a064-6dfc4c61b688
@charlesvien

Copy link
Copy Markdown
Member

Hey @k11kirky, we've migrated this repo into the PostHog/posthog monorepo, so this PR needs to be recreated there.

Easiest path: check out the monorepo and run the /porting-code-prs skill with your agent pointed at this PR. It has the migration guide and path remappings baked in so the agent doesn't have to figure them out. You don't strictly need it though, just asking your agent to port the PR works fine too.

Shout if you get stuck.

@charlesvien charlesvien added the stale No recent changes to PR label Aug 3, 2026
k11kirky added a commit to PostHog/posthog that referenced this pull request Aug 3, 2026
…cates

Channel-composer generation tasks no longer arrive with a pre-created canvas id (PostHog/code#3835), so the building-canvases skill's resolve step now spells out the flow: list the channel's canvases, build on a clearly-matching one and say so in the reply, and only otherwise create — with a short descriptive name, never "Untitled canvas".

Generated-By: PostHog Code
Task-Id: b548ad79-52a8-496c-a064-6dfc4c61b688
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

stale No recent changes to PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants