Skip to content
This repository was archived by the owner on Aug 6, 2026. It is now read-only.

Commit c9c7332

Browse files
authored
refactor(canvas): simplify-pass cleanups on the client remodel
Restore the canvas context editor's save path (it lost its writer in the remodel): the editor now buffers locally and commits through the saveContext mutation, and the freeform store shrinks to runtime-error + version-browse state only. Scope revert/reset/sync invalidations to the one canvas via a shared helper; poll builds at the record cadence until a build actually starts; request explicit page sizes on paginated lists. Collapse the duplicated channels query onto useTaskChannels, find the personal channel by type everywhere, and collapse ChannelTaskRecord's duplicate id/taskId. Extract the version-navigation arithmetic into a tested pure helper, replace the currentCode sentinel with a real isEdit flag, seed the home canvas from the shared contract constants, and delete dead code (orphaned freeform version schemas, unreferenced channel client methods, the pruned canvas-contracts validators, a stray local redis dump). Generated-By: PostHog Code Task-Id: b548ad79-52a8-496c-a064-6dfc4c61b688
1 parent 03c6952 commit c9c7332

39 files changed

Lines changed: 474 additions & 733 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,3 +88,4 @@ apps/mobile/ROADMAP.md
8888
# to the PostHog Visual Review product; the committed baseline is the signed
8989
# hash manifest apps/code/snapshots.yml.
9090
apps/code/.storybook/__snapshots__/
91+
dump.rdb

dump.rdb

-14.2 KB
Binary file not shown.

packages/api-client/src/posthog-client.ts

Lines changed: 0 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -2094,20 +2094,6 @@ export class PostHogAPIClient {
20942094
return (await response.json()) as TaskChannel;
20952095
}
20962096

2097-
async getTaskChannel(id: string): Promise<TaskChannel> {
2098-
const teamId = await this.getTeamId();
2099-
const urlPath = `/api/projects/${teamId}/task_channels/${encodeURIComponent(id)}/`;
2100-
const response = await this.api.fetcher.fetch({
2101-
method: "get",
2102-
url: new URL(`${this.api.baseUrl}${urlPath}`),
2103-
path: urlPath,
2104-
});
2105-
if (!response.ok) {
2106-
throw new Error(`Failed to fetch task channel: ${response.statusText}`);
2107-
}
2108-
return (await response.json()) as TaskChannel;
2109-
}
2110-
21112097
// Rename a channel. The server normalizes the name (lowercase-dashed).
21122098
async renameTaskChannel(id: string, name: string): Promise<TaskChannel> {
21132099
const teamId = await this.getTeamId();
@@ -2262,49 +2248,6 @@ export class PostHogAPIClient {
22622248
return all;
22632249
}
22642250

2265-
// The task currently generating this channel's CONTEXT.md, shared across the
2266-
// project so any user sees an in-progress generation (instead of fragile
2267-
// local state). Returns null when nothing is generating.
2268-
async getChannelGenerationTask(channelId: string): Promise<string | null> {
2269-
const teamId = await this.getTeamId();
2270-
const urlPath = `/api/projects/${teamId}/task_channels/${encodeURIComponent(channelId)}/context_generation/`;
2271-
const response = await this.api.fetcher.fetch({
2272-
method: "get",
2273-
url: new URL(`${this.api.baseUrl}${urlPath}`),
2274-
path: urlPath,
2275-
});
2276-
if (response.status === 404) return null;
2277-
if (!response.ok) {
2278-
throw new Error(
2279-
`Failed to fetch channel generation task: ${response.statusText}`,
2280-
);
2281-
}
2282-
const data = (await response.json()) as { task_id?: string | null };
2283-
return data.task_id ?? null;
2284-
}
2285-
2286-
// Record (or clear, with null) the task generating this channel's CONTEXT.md.
2287-
async setChannelGenerationTask(
2288-
channelId: string,
2289-
taskId: string | null,
2290-
): Promise<void> {
2291-
const teamId = await this.getTeamId();
2292-
const urlPath = `/api/projects/${teamId}/task_channels/${encodeURIComponent(channelId)}/context_generation/`;
2293-
const response = await this.api.fetcher.fetch({
2294-
method: "put",
2295-
url: new URL(`${this.api.baseUrl}${urlPath}`),
2296-
path: urlPath,
2297-
overrides: {
2298-
body: JSON.stringify({ task_id: taskId }),
2299-
},
2300-
});
2301-
if (!response.ok && response.status !== 404) {
2302-
throw new Error(
2303-
`Failed to set channel generation task: ${response.statusText}`,
2304-
);
2305-
}
2306-
}
2307-
23082251
// A channel's system-announcement feed (context created, CONTEXT.md being
23092252
// built), chronological. Durable + team-visible, rendered alongside task cards.
23102253
async getChannelFeed(channelId: string): Promise<ChannelFeedMessage[]> {

packages/core/src/canvas/channelTaskSchemas.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { z } from "zod";
22

3+
// A filing is the task's `channel` field on the tasks API, so the task id IS
4+
// the record's identity — there is no separate row id.
35
export const channelTaskRecordSchema = z.object({
4-
id: z.string(),
56
channelId: z.string(),
67
taskId: z.string(),
78
createdAt: z.number(),
@@ -17,4 +18,4 @@ export const fileChannelTaskInput = z.object({
1718
taskId: z.string().min(1),
1819
});
1920

20-
export const channelTaskIdInput = z.object({ id: z.string().min(1) });
21+
export const unfileChannelTaskInput = z.object({ taskId: z.string().min(1) });

packages/core/src/canvas/channelTasksService.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,9 @@ export class ChannelTasksService {
2323
const rows = await this.api.listPaginated<ApiTask>(
2424
`tasks/?channel=${encodeURIComponent(channelId)}`,
2525
"list channel tasks",
26+
{ limit: 200 },
2627
);
2728
return rows.map((task) => ({
28-
id: task.id,
2929
channelId,
3030
taskId: task.id,
3131
createdAt: Date.parse(task.created_at) || 0,
@@ -38,7 +38,6 @@ export class ChannelTasksService {
3838
}): Promise<ChannelTaskRecord> {
3939
const task = await this.setChannel(input.taskId, input.channelId);
4040
return {
41-
id: task.id,
4241
channelId: input.channelId,
4342
taskId: task.id,
4443
createdAt: Date.parse(task.created_at) || 0,

packages/core/src/canvas/dashboardsService.ts

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
import {
2+
CANVAS_COMPONENT_PATH,
3+
CANVAS_ENTRY_HTML,
4+
CANVAS_SOURCE_SCHEMA_VERSION,
5+
} from "@posthog/shared";
16
import { inject, injectable } from "inversify";
27
import {
38
type CanvasBuildActionInput,
@@ -19,7 +24,7 @@ import { PROJECT_API_CLIENT, type ProjectApiClient } from "./projectApiClient";
1924
const HOME_CANVAS_NAME = "Home";
2025

2126
// The entry shell for a client-authored single-file project (the home canvas
22-
// seed): the runtime mounts the default export of src/canvas.tsx.
27+
// seed): the runtime mounts the default export of the canvas component file.
2328
const SINGLE_FILE_INDEX_HTML = `<!doctype html>
2429
<html>
2530
<head>
@@ -28,7 +33,7 @@ const SINGLE_FILE_INDEX_HTML = `<!doctype html>
2833
</head>
2934
<body>
3035
<div id="root"></div>
31-
<script type="module" src="/src/canvas.tsx"></script>
36+
<script type="module" src="/${CANVAS_COMPONENT_PATH}"></script>
3237
</body>
3338
</html>
3439
`;
@@ -127,6 +132,7 @@ export class DashboardsService {
127132
const rows = await this.api.listPaginated<ApiCanvas>(
128133
`canvases/?channel=${encodeURIComponent(channelId)}`,
129134
"list canvases",
135+
{ limit: 200 },
130136
);
131137
return rows.map(toRecord);
132138
}
@@ -326,6 +332,7 @@ export class DashboardsService {
326332
const rows = await this.api.listPaginated<ApiCanvas>(
327333
`canvases/?channel=${encodeURIComponent(channelId)}&is_home=true`,
328334
"find home canvas",
335+
{ limit: 200 },
329336
);
330337
return rows.length ? toRecord(rows[0]) : null;
331338
}
@@ -338,12 +345,12 @@ export class DashboardsService {
338345
channelId: string,
339346
): Promise<DashboardRecord> {
340347
const project = {
341-
schemaVersion: 1,
348+
schemaVersion: CANVAS_SOURCE_SCHEMA_VERSION,
342349
files: {
343-
"index.html": SINGLE_FILE_INDEX_HTML,
344-
"src/canvas.tsx": buildHomeCanvasCode(channelId, record.id),
350+
[CANVAS_ENTRY_HTML]: SINGLE_FILE_INDEX_HTML,
351+
[CANVAS_COMPONENT_PATH]: buildHomeCanvasCode(channelId, record.id),
345352
},
346-
entryHtml: "index.html",
353+
entryHtml: CANVAS_ENTRY_HTML,
347354
dependencies: { react: "19.0.0" },
348355
canvasSdkVersion: "0.1.0",
349356
capabilities: {
@@ -407,7 +414,7 @@ function sql(v: string): string {
407414
return "'" + String(v).replace(/'/g, "''") + "'";
408415
}
409416
410-
type Row = { id: string; title: string; ref: string | null; createdAt: string };
417+
type Row = { id: string; title: string; createdAt: string };
411418
412419
// Paginated reader for the channel's canvases or tasks, newest first.
413420
function useChannelRows(kind: "dashboard" | "task") {
@@ -434,7 +441,6 @@ function useChannelRows(kind: "dashboard" | "task") {
434441
const batch: Row[] = ((res && res.results) || []).map((r: any[]) => ({
435442
id: String(r[0]),
436443
title: String(r[1]),
437-
ref: kind === "task" ? String(r[0]) : null,
438444
createdAt: String(r[2]),
439445
}));
440446
offsetRef.current += batch.length;
@@ -665,10 +671,7 @@ function TasksSection() {
665671
key={r.id}
666672
title={r.title}
667673
meta={r.createdAt.slice(0, 10)}
668-
// A task row's file-system id is NOT the task id; the task id is the
669-
// row's ref (ChannelTasksService files it as ref=taskId). Only rows
670-
// with a ref are navigable.
671-
onClick={r.ref ? () => ph.navigate?.toTask(r.ref as string) : undefined}
674+
onClick={() => ph.navigate?.toTask(r.id)}
672675
/>
673676
))}
674677
</Section>

packages/core/src/canvas/freeformSchemas.ts

Lines changed: 0 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -5,44 +5,6 @@ import { z } from "zod";
55
// generation path can resolve the right system prompt.
66
export const FREEFORM_TEMPLATE_ID = "freeform";
77

8-
// A single point in a freeform canvas's edit history. Every agent turn appends
9-
// one full-file snapshot (Q7: full-file rewrite); the user can revert to any of
10-
// them and the `currentVersionId` pointer is what publishes. We keep whole-file
11-
// snapshots rather than diffs because canvases are small and a snapshot can
12-
// never fail to reconstruct.
13-
export const freeformVersionSchema = z.object({
14-
id: z.string(),
15-
// The complete single-file React source for this version.
16-
code: z.string(),
17-
// The author-written context (markdown) passed to the agent, as it stood for
18-
// this version. Snapshotted so reverting restores the context too. Absent on
19-
// versions saved before the Context tab existed.
20-
context: z.string().optional(),
21-
// The user prompt that produced this version (absent for the seed/empty one,
22-
// and for a version created by a context-only edit).
23-
prompt: z.string().optional(),
24-
// Epoch ms the version was created.
25-
createdAt: z.number(),
26-
});
27-
export type FreeformVersion = z.infer<typeof freeformVersionSchema>;
28-
29-
// The freeform-specific payload that rides in a canvas's file-system `meta` blob.
30-
export const freeformCanvasSchema = z.object({
31-
// The currently-rendered source (mirrors the version pointed to by
32-
// currentVersionId; duplicated so the renderer needs only this field).
33-
code: z.string(),
34-
// Full, ordered edit history (oldest first). Always contains >= 1 entry once
35-
// the agent has produced anything.
36-
versions: z.array(freeformVersionSchema).default([]),
37-
// Which version is live. Undo/redo moves this pointer; a new agent turn
38-
// truncates any "redo" tail (Q8: linear-discard) and appends.
39-
currentVersionId: z.string().optional(),
40-
// The live author-written context (markdown), mirrors the version pointed to by
41-
// currentVersionId. Prepended to every agent turn so the build is anchored to it.
42-
context: z.string().default(""),
43-
});
44-
export type FreeformCanvas = z.infer<typeof freeformCanvasSchema>;
45-
468
// ---------------------------------------------------------------------------
479
// Canvas data avenue: the host-side query the postMessage `ph.query` shim calls.
4810
// Routed through PostHog's cached query runner (the same avenue insights use, so

packages/core/src/canvas/projectApiClient.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,21 @@ export class ProjectApiClient {
4040
return (await res.json()) as T;
4141
}
4242

43-
/** List a DRF-paginated collection, walking `next` links until exhausted. */
44-
async listPaginated<T>(path: string, errorLabel: string): Promise<T[]> {
43+
/**
44+
* List a DRF-paginated collection, walking `next` links until exhausted.
45+
* `options.limit` sets the page size on the first request (subsequent pages
46+
* follow the server's `next` links, which carry the limit forward).
47+
*/
48+
async listPaginated<T>(
49+
path: string,
50+
errorLabel: string,
51+
options?: { limit?: number },
52+
): Promise<T[]> {
4553
const all: T[] = [];
46-
let suffix = path;
54+
let suffix =
55+
options?.limit != null
56+
? `${path}${path.includes("?") ? "&" : "?"}limit=${options.limit}`
57+
: path;
4758
for (let i = 0; i < MAX_PAGES; i++) {
4859
const page = await this.json<{ next: string | null; results: T[] }>(
4960
suffix,

packages/host-router/src/routers/channel-tasks.router.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import {
2-
channelTaskIdInput,
32
channelTaskRecordSchema,
43
fileChannelTaskInput,
54
listChannelTasksInput,
5+
unfileChannelTaskInput,
66
} from "@posthog/core/canvas/channelTaskSchemas";
77
import { CHANNEL_TASKS_SERVICE } from "@posthog/core/canvas/identifiers";
88
import type { IChannelTasksService } from "@posthog/core/canvas/services";
@@ -27,10 +27,10 @@ export const channelTasksRouter = router({
2727
.file(input),
2828
),
2929
unfile: publicProcedure
30-
.input(channelTaskIdInput)
30+
.input(unfileChannelTaskInput)
3131
.mutation(({ ctx, input }) =>
3232
ctx.container
3333
.get<IChannelTasksService>(CHANNEL_TASKS_SERVICE)
34-
.unfile(input.id),
34+
.unfile(input.taskId),
3535
),
3636
});

0 commit comments

Comments
 (0)