From 75f04da244dea4d1564c561c96b71dc91d442f27 Mon Sep 17 00:00:00 2001 From: Peter Kirkham Date: Fri, 10 Jul 2026 22:09:15 +0100 Subject: [PATCH] fix(canvas): name the canvas before creating its generation task MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Since the composer's Canvas mode creates the dashboard as "Untitled canvas" right at submit, every generation task was titled `Generate canvas "Untitled canvas"` — and that placeholder also leaked into the backend task description (shown as the optimistic first-message bubble on cloud runs) and the generation prompt's header. Auto-naming now runs before task creation, concurrently with model resolution, so the task title, description, prompt, channel filing, and completion toast all carry the real name from the start. Generated-By: PostHog Code Task-Id: ba8c30b5-6388-4d27-995d-e665d51138dd --- .../canvas/hooks/useGenerateFreeformCanvas.ts | 44 +++++++++---------- .../stores/canvasGenerationTrackerStore.ts | 14 ------ 2 files changed, 22 insertions(+), 36 deletions(-) diff --git a/packages/ui/src/features/canvas/hooks/useGenerateFreeformCanvas.ts b/packages/ui/src/features/canvas/hooks/useGenerateFreeformCanvas.ts index 3ae5404d7b..6b630e83a2 100644 --- a/packages/ui/src/features/canvas/hooks/useGenerateFreeformCanvas.ts +++ b/packages/ui/src/features/canvas/hooks/useGenerateFreeformCanvas.ts @@ -115,6 +115,17 @@ export function useGenerateFreeformCanvas(args: { } = opts; setIsStarting(true); try { + // Auto-name a still-unnamed canvas from its generation prompt BEFORE + // creating the task: the name feeds the task title, the backend task + // description (which doubles as the optimistic first-message bubble on + // cloud runs), and the generation prompt's header — naming afterwards + // would leave all three reading `Untitled canvas`. Runs concurrently + // with model resolution below, so the common path adds no latency. + // Best-effort: on failure the placeholder name is kept, as before. + const namePromise = isPlaceholderCanvasName(name) + ? titleGenerator.generateCanvasName(instruction).catch(() => null) + : Promise.resolve(null); + // A cloud run requires an explicit adapter + model (the API rejects a // cloud runtime without a model). Resolve the caller's pick — or the // adapter's server default when none — the same way the inbox one-click @@ -137,18 +148,26 @@ export function useGenerateFreeformCanvas(args: { } } + const generatedName = (await namePromise)?.trim(); + const canvasName = generatedName || name; + if (generatedName) { + // Rename fire-and-forget: a failure shouldn't block the run, and the + // task carries the real name regardless. + void renameDashboard(dashboardId, canvasName).catch(() => {}); + } + const result = await taskService.createTask( { content: buildFreeformGenerationPrompt({ dashboardId, - name, + name: canvasName, channelName, templateId, instruction, currentCode, useStarter, }), - taskDescription: `Generate canvas "${name}"`, + taskDescription: `Generate canvas "${canvasName}"`, // Unattended generation: run in auto mode so it doesn't stall on edit-approval prompts. executionMode: "auto" as const, workspaceMode, @@ -180,31 +199,12 @@ export function useGenerateFreeformCanvas(args: { // finishes, even after the user navigates to another canvas. useCanvasGenerationTrackerStore .getState() - .track({ taskId: task.id, dashboardId, channelId, name }); + .track({ taskId: task.id, dashboardId, channelId, name: canvasName }); // Refresh the workspace cache so the new cloud workspace row appears and // the task view resolves the cloud run instead of the repo-picker prompt. void queryClient.invalidateQueries({ queryKey: trpc.workspace.getAll.queryKey(), }); - // Auto-name a still-unnamed canvas from its generation prompt, using the - // same helper model that names tasks. Best-effort: a failure (or a user - // who already named the canvas) leaves the existing title untouched. - if (isPlaceholderCanvasName(name)) { - void titleGenerator - .generateCanvasName(instruction) - .then(async (generated) => { - const title = generated?.trim(); - if (title) { - await renameDashboard(dashboardId, title); - // Keep the tracked generation's name in sync so its completion - // toast reads the real title, not "Untitled canvas". - useCanvasGenerationTrackerStore - .getState() - .updateName(task.id, title); - } - }) - .catch(() => {}); - } return task.id; } finally { setIsStarting(false); diff --git a/packages/ui/src/features/canvas/stores/canvasGenerationTrackerStore.ts b/packages/ui/src/features/canvas/stores/canvasGenerationTrackerStore.ts index 62b6780c1d..df59717e27 100644 --- a/packages/ui/src/features/canvas/stores/canvasGenerationTrackerStore.ts +++ b/packages/ui/src/features/canvas/stores/canvasGenerationTrackerStore.ts @@ -16,9 +16,6 @@ interface CanvasGenerationTrackerState { // Keyed by taskId. tracked: Record; track: (entry: TrackedCanvasGeneration) => void; - // Keep the display name fresh when a freshly-created canvas is auto-renamed - // from its prompt after generation has already started. - updateName: (taskId: string, name: string) => void; untrack: (taskId: string) => void; } @@ -27,17 +24,6 @@ export const useCanvasGenerationTrackerStore = tracked: {}, track: (entry) => set((s) => ({ tracked: { ...s.tracked, [entry.taskId]: entry } })), - updateName: (taskId, name) => - set((s) => - s.tracked[taskId] - ? { - tracked: { - ...s.tracked, - [taskId]: { ...s.tracked[taskId], name }, - }, - } - : s, - ), untrack: (taskId) => set((s) => { if (!s.tracked[taskId]) return s;