From 7cd0d3c20a6f8e37594046d9d0c1d470f057afb1 Mon Sep 17 00:00:00 2001 From: Raquel Smith Date: Fri, 26 Jun 2026 11:13:28 -0700 Subject: [PATCH 1/3] fix(canvas): always run canvas generation as a cloud run Freeform canvas generation hard-coded `workspaceMode: "local"`, so every canvas/dashboard build kicked off a local agent run instead of a cloud run. Switch it to `workspaceMode: "cloud"` so generation always runs server-side, regardless of the user's last-used workspace mode or whether the app is open. The repo-less cloud path is already supported by the task creation saga (it provisions a cloud workspace and starts the run without a repo), and the agent still publishes results via the `desktop-file-system-canvas-partial-update` MCP tool. Generated-By: PostHog Code Task-Id: 36b53fe0-c4f6-473b-b514-21e50941cf52 --- .../canvas/hooks/useGenerateFreeformCanvas.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/ui/src/features/canvas/hooks/useGenerateFreeformCanvas.ts b/packages/ui/src/features/canvas/hooks/useGenerateFreeformCanvas.ts index f72745184..61dfd8721 100644 --- a/packages/ui/src/features/canvas/hooks/useGenerateFreeformCanvas.ts +++ b/packages/ui/src/features/canvas/hooks/useGenerateFreeformCanvas.ts @@ -24,9 +24,9 @@ import { useCallback, useState } from "react"; // so every client's canvas view tracks the in-flight run. Canvas generation // reads PostHog data via the MCP rather than repo code, so no repo is selected // up front — the agent attaches one lazily only if it decides it needs one. The -// task runs in a per-task scratch dir (local) and the agent publishes the result -// via the `desktop-file-system-canvas-partial-update` MCP tool — mirrors -// useGenerateContext. +// task runs as a cloud run (not a local agent) so generation proceeds +// server-side regardless of which client kicked it off, and the agent publishes +// the result via the `desktop-file-system-canvas-partial-update` MCP tool. export function useGenerateFreeformCanvas(args: { dashboardId: string; channelId: string; @@ -73,7 +73,10 @@ export function useGenerateFreeformCanvas(args: { taskDescription: `Generate canvas "${name}"`, // Unattended generation: run in auto mode so it doesn't stall on edit-approval prompts. executionMode: "auto" as const, - workspaceMode: "local", + // Always a cloud run — canvas generation should never tie up (or + // depend on) the local machine. Hard-coded, not the sticky + // last-used workspace mode, so it's unaffected by prior local tasks. + workspaceMode: "cloud", allowNoRepo: true, channelContext, channelName, @@ -98,9 +101,8 @@ export function useGenerateFreeformCanvas(args: { useCanvasGenerationTrackerStore .getState() .track({ taskId: task.id, dashboardId, channelId, name }); - // Repo-less tasks create no workspace row, so the usual workspace.create - // invalidation never fires — refresh the cache so the task view resolves - // its scratch cwd instead of showing the repo-picker prompt. + // 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(), }); From fb272cbadc69e4a9301cb4dbadc0dd6acc3a323f Mon Sep 17 00:00:00 2001 From: Raquel Smith Date: Fri, 26 Jun 2026 11:13:30 -0700 Subject: [PATCH 2/3] fix(canvas): always run CONTEXT.md generation as a cloud run CONTEXT.md generation hard-coded `workspaceMode: "local"`, same as canvas generation, so it kicked off a local agent run instead of a cloud run. Switch it to `workspaceMode: "cloud"` so generation always runs server-side, regardless of the user's last-used workspace mode or whether the app is open. The repo-less cloud path is already supported by the task creation saga. Generated-By: PostHog Code Task-Id: 36b53fe0-c4f6-473b-b514-21e50941cf52 --- .../canvas/hooks/useGenerateContext.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/packages/ui/src/features/canvas/hooks/useGenerateContext.ts b/packages/ui/src/features/canvas/hooks/useGenerateContext.ts index c2c77e70f..866af9bc9 100644 --- a/packages/ui/src/features/canvas/hooks/useGenerateContext.ts +++ b/packages/ui/src/features/canvas/hooks/useGenerateContext.ts @@ -14,10 +14,10 @@ import { useCallback, useState } from "react"; // Kicks off CONTEXT.md generation as a repo-less task. The user no longer picks // a folder/repo up front — the agent decides at runtime whether it needs one and -// asks the user to clarify if it can't find the right one. The task runs in a -// per-task scratch dir (local), is filed to the channel, and recorded -// server-side as the channel's generation task so every user's CONTEXT.md view -// can track it. +// asks the user to clarify if it can't find the right one. The task runs as a +// cloud run (not a local agent) so generation proceeds server-side regardless of +// which client kicked it off, is filed to the channel, and recorded server-side +// as the channel's generation task so every user's CONTEXT.md view can track it. export function useGenerateContext(channelId: string, channelName: string) { const taskService = useService(TASK_SERVICE); const trpc = useHostTRPC(); @@ -34,7 +34,10 @@ export function useGenerateContext(channelId: string, channelName: string) { { content: buildContextGenerationPrompt({ channelName, channelId }), taskDescription: `Generate CONTEXT.md for #${channelName}`, - workspaceMode: "local", + // Always a cloud run — generation should never tie up (or depend on) + // the local machine. Hard-coded, not the sticky last-used workspace + // mode, so it's unaffected by prior local tasks. + workspaceMode: "cloud", allowNoRepo: true, }, (output) => invalidateTasks(output.task), @@ -52,9 +55,8 @@ export function useGenerateContext(channelId: string, channelName: string) { // are best-effort: a failure here shouldn't undo a started task. void fileTask(channelId, task.id, task.title).catch(() => {}); void setGenerationTask(task.id).catch(() => {}); - // Repo-less tasks create no workspace row, so the usual workspace.create - // invalidation never fires — refresh the cache so the task view resolves - // its scratch cwd instead of showing the repo-picker prompt. + // 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(), }); From 4e949cb132779594581d4d44d85fdda96e5ae570 Mon Sep 17 00:00:00 2001 From: Raquel Smith Date: Fri, 26 Jun 2026 11:29:27 -0700 Subject: [PATCH 3/3] feat(canvas): dev-only local/cloud picker for canvas + CONTEXT.md generation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Canvas and CONTEXT.md generation always run as cloud runs, but cloud runs only reflect merged code — so a local build of these features can't be tested before merging. Add a dev-only (import.meta.env.DEV) workspace-mode picker to both generation screens so the run can be pointed at a local agent during development. Production is unchanged: the picker is hidden and both hooks default to cloud. The hooks now take an optional workspaceMode (default "cloud"); the UIs reuse the existing WorkspaceModeSelect (restricted to local/cloud) and label it with a tooltip noting it's dev-only. Generated-By: PostHog Code Task-Id: 36b53fe0-c4f6-473b-b514-21e50941cf52 --- .../canvas/components/WebsiteContext.tsx | 50 ++++++++-- .../canvas/freeform/FreeformGenerateBar.tsx | 27 ++++++ .../canvas/hooks/useGenerateContext.ts | 93 ++++++++++--------- .../canvas/hooks/useGenerateFreeformCanvas.ts | 14 ++- 4 files changed, 126 insertions(+), 58 deletions(-) diff --git a/packages/ui/src/features/canvas/components/WebsiteContext.tsx b/packages/ui/src/features/canvas/components/WebsiteContext.tsx index 48863659f..88f418ccb 100644 --- a/packages/ui/src/features/canvas/components/WebsiteContext.tsx +++ b/packages/ui/src/features/canvas/components/WebsiteContext.tsx @@ -13,6 +13,9 @@ import { EmptyMedia, EmptyTitle, Button as QuillButton, + Tooltip, + TooltipContent, + TooltipTrigger, } from "@posthog/quill"; import { ANALYTICS_EVENTS } from "@posthog/shared/analytics-events"; import { isTerminalStatus } from "@posthog/shared/domain-types"; @@ -30,6 +33,10 @@ import { import { useGenerateContext } from "@posthog/ui/features/canvas/hooks/useGenerateContext"; import { MarkdownRenderer } from "@posthog/ui/features/editor/components/MarkdownRenderer"; import { useSessionForTask } from "@posthog/ui/features/sessions/useSession"; +import { + type WorkspaceMode, + WorkspaceModeSelect, +} from "@posthog/ui/features/task-detail/components/WorkspaceModeSelect"; import { taskDetailQuery } from "@posthog/ui/features/tasks/queries"; import { useSetHeaderContent } from "@posthog/ui/hooks/useSetHeaderContent"; import { track } from "@posthog/ui/shell/analytics"; @@ -469,24 +476,47 @@ function GenerateWithAgent({ }) { const { generate, isStarting } = useGenerateContext(channelId, channelName); + // Generation always runs in the cloud, except the dev-only picker below lets a + // local build of these features be tested before it's merged to the cloud env. + const [workspaceMode, setWorkspaceMode] = useState("cloud"); + const onGenerate = () => { track(ANALYTICS_EVENTS.CONTEXT_ACTION, { action_type: "generate_started", channel_id: channelId, }); - void generate(); + void generate(workspaceMode); }; return ( - - {isStarting ? : } - {regenerate ? "Generate again" : "Generate with agent"} - + + + {isStarting ? : } + {regenerate ? "Generate again" : "Generate with agent"} + + {/* Dev-only: pick local vs cloud so a local build can be tested pre-merge. */} + {import.meta.env.DEV && ( + + }> + + + + Dev mode only — generation always runs in the cloud in production. + + + )} + ); } diff --git a/packages/ui/src/features/canvas/freeform/FreeformGenerateBar.tsx b/packages/ui/src/features/canvas/freeform/FreeformGenerateBar.tsx index c99424b23..48ddc6075 100644 --- a/packages/ui/src/features/canvas/freeform/FreeformGenerateBar.tsx +++ b/packages/ui/src/features/canvas/freeform/FreeformGenerateBar.tsx @@ -1,6 +1,11 @@ +import { Tooltip, TooltipContent, TooltipTrigger } from "@posthog/quill"; import { useGenerateFreeformCanvas } from "@posthog/ui/features/canvas/hooks/useGenerateFreeformCanvas"; import { PromptInput } from "@posthog/ui/features/message-editor/components/PromptInput"; import type { EditorHandle } from "@posthog/ui/features/message-editor/types"; +import { + type WorkspaceMode, + WorkspaceModeSelect, +} from "@posthog/ui/features/task-detail/components/WorkspaceModeSelect"; import { forwardRef, useState } from "react"; // Composer that kicks off freeform canvas generation as a dedicated task: the @@ -55,6 +60,10 @@ export const FreeformGenerateBar = forwardRef< const isEdit = !!currentCode?.trim(); const [useStarter, setUseStarter] = useState(true); + // Generation always runs in the cloud, except the dev-only picker below lets a + // local build of these features be tested before it's merged to the cloud env. + const [workspaceMode, setWorkspaceMode] = useState("cloud"); + const run = async (text: string) => { const instruction = text.trim(); if (!instruction) return; @@ -62,6 +71,7 @@ export const FreeformGenerateBar = forwardRef< instruction, currentCode, useStarter: !isEdit && useStarter, + workspaceMode, }); if (taskId) onStarted?.(taskId); }; @@ -92,6 +102,23 @@ export const FreeformGenerateBar = forwardRef< scratch) )} + {/* Dev-only: pick local vs cloud so a local build can be tested pre-merge. */} + {import.meta.env.DEV && ( + + }> + + + + Dev mode only — generation always runs in the cloud in production. + + + )} ); }); diff --git a/packages/ui/src/features/canvas/hooks/useGenerateContext.ts b/packages/ui/src/features/canvas/hooks/useGenerateContext.ts index 866af9bc9..99d3af456 100644 --- a/packages/ui/src/features/canvas/hooks/useGenerateContext.ts +++ b/packages/ui/src/features/canvas/hooks/useGenerateContext.ts @@ -4,6 +4,7 @@ import { } from "@posthog/core/task-detail/taskService"; import { useService } from "@posthog/di/react"; import { useHostTRPC } from "@posthog/host-router/react"; +import type { WorkspaceMode } from "@posthog/shared"; import { buildContextGenerationPrompt } from "@posthog/ui/features/canvas/contextPrompt"; import { useChannelTaskMutations } from "@posthog/ui/features/canvas/hooks/useChannelTasks"; import { useFolderGenerationTaskMutation } from "@posthog/ui/features/canvas/hooks/useFolderGenerationTask"; @@ -27,53 +28,57 @@ export function useGenerateContext(channelId: string, channelName: string) { const { set: setGenerationTask } = useFolderGenerationTaskMutation(channelId); const [isStarting, setIsStarting] = useState(false); - const generate = useCallback(async (): Promise => { - setIsStarting(true); - try { - const result = await taskService.createTask( - { - content: buildContextGenerationPrompt({ channelName, channelId }), - taskDescription: `Generate CONTEXT.md for #${channelName}`, - // Always a cloud run — generation should never tie up (or depend on) - // the local machine. Hard-coded, not the sticky last-used workspace - // mode, so it's unaffected by prior local tasks. - workspaceMode: "cloud", - allowNoRepo: true, - }, - (output) => invalidateTasks(output.task), - ); + const generate = useCallback( + async (workspaceMode: WorkspaceMode = "cloud"): Promise => { + setIsStarting(true); + try { + const result = await taskService.createTask( + { + content: buildContextGenerationPrompt({ channelName, channelId }), + taskDescription: `Generate CONTEXT.md for #${channelName}`, + // Defaults to a cloud run — generation should never tie up (or + // depend on) the local machine, and it's never the sticky last-used + // workspace mode. The dev-only picker can override to "local" to + // test a local build of these features before merging. + workspaceMode, + allowNoRepo: true, + }, + (output) => invalidateTasks(output.task), + ); - if (!result.success) { - toast.error("Couldn't start CONTEXT.md generation", { - description: result.error, + if (!result.success) { + toast.error("Couldn't start CONTEXT.md generation", { + description: result.error, + }); + return null; + } + + const task = result.data.task; + // File into the channel + record as the (shared) generation task. Both + // are best-effort: a failure here shouldn't undo a started task. + void fileTask(channelId, task.id, task.title).catch(() => {}); + void setGenerationTask(task.id).catch(() => {}); + // 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(), }); - return null; + return task.id; + } finally { + setIsStarting(false); } - - const task = result.data.task; - // File into the channel + record as the (shared) generation task. Both - // are best-effort: a failure here shouldn't undo a started task. - void fileTask(channelId, task.id, task.title).catch(() => {}); - void setGenerationTask(task.id).catch(() => {}); - // 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(), - }); - return task.id; - } finally { - setIsStarting(false); - } - }, [ - taskService, - trpc, - queryClient, - invalidateTasks, - fileTask, - setGenerationTask, - channelId, - channelName, - ]); + }, + [ + taskService, + trpc, + queryClient, + invalidateTasks, + fileTask, + setGenerationTask, + channelId, + channelName, + ], + ); return { generate, isStarting }; } diff --git a/packages/ui/src/features/canvas/hooks/useGenerateFreeformCanvas.ts b/packages/ui/src/features/canvas/hooks/useGenerateFreeformCanvas.ts index 61dfd8721..338abb4df 100644 --- a/packages/ui/src/features/canvas/hooks/useGenerateFreeformCanvas.ts +++ b/packages/ui/src/features/canvas/hooks/useGenerateFreeformCanvas.ts @@ -6,6 +6,7 @@ import { } from "@posthog/core/task-detail/taskService"; import { useService } from "@posthog/di/react"; import { useHostTRPC } from "@posthog/host-router/react"; +import type { WorkspaceMode } from "@posthog/shared"; import { buildFreeformGenerationPrompt } from "@posthog/ui/features/canvas/freeformPrompt"; import { useChannelTaskMutations } from "@posthog/ui/features/canvas/hooks/useChannelTasks"; import { @@ -56,6 +57,10 @@ export function useGenerateFreeformCanvas(args: { currentCode?: string; // Default on (opt out in the bar): seed the starter scaffold on first build. useStarter?: boolean; + // Dev-only override (the bar exposes a local/cloud picker in dev so a + // local build of these features can be tested before merging). Production + // always runs in the cloud — see the default below. + workspaceMode?: WorkspaceMode; }): Promise => { setIsStarting(true); try { @@ -73,10 +78,11 @@ export function useGenerateFreeformCanvas(args: { taskDescription: `Generate canvas "${name}"`, // Unattended generation: run in auto mode so it doesn't stall on edit-approval prompts. executionMode: "auto" as const, - // Always a cloud run — canvas generation should never tie up (or - // depend on) the local machine. Hard-coded, not the sticky - // last-used workspace mode, so it's unaffected by prior local tasks. - workspaceMode: "cloud", + // Defaults to a cloud run — canvas generation should never tie up + // (or depend on) the local machine, and it's never the sticky + // last-used workspace mode. The dev-only picker can override to + // "local" to test a local build of these features before merging. + workspaceMode: opts.workspaceMode ?? "cloud", allowNoRepo: true, channelContext, channelName,