Skip to content

fix(canvas): stop local generation stranding the canvas on "Generating"#2882

Draft
raquelmsmith wants to merge 2 commits into
mainfrom
posthog-code/fix-canvas-stuck-generating-local
Draft

fix(canvas): stop local generation stranding the canvas on "Generating"#2882
raquelmsmith wants to merge 2 commits into
mainfrom
posthog-code/fix-canvas-stuck-generating-local

Conversation

@raquelmsmith

Copy link
Copy Markdown
Member

Problem

A freeform canvas generated by a local task could get stuck showing "Generating" forever — the "canvas keeps timing out" experience.

FreeformCanvasView derives its running state differently per environment:

  • cloud falls back to the persisted run record and stops once latest_run.status (or the session cloudStatus) is terminal.
  • local relied only on the live ACP session being connecting/connected.

So if a local session went stale or the agent stalled without cleanly disconnecting, running stayed true, isGenerating never cleared, and the canvas was pinned on the "Generating" empty state with no escape.

Changes

  • Extract the run-status derivation into a pure, unit-tested helper (canvasGenerationStatus.ts).
  • Give the local path the same terminal-run guard cloud already has: a terminal run record (completed / failed / cancelled) always ends running, so a stuck/stale session can no longer strand the canvas. Cloud behavior is unchanged.

How did you test this?

I'm an agent (PostHog Slack app). I did not drive the desktop app manually. I added canvasGenerationStatus.test.ts (11 cases, cloud + local, incl. the regression: a terminal run record must win over a stale connected session) — vitest run passes locally. @posthog/ui typecheck shows no errors in the changed files, and Biome check passes on them.

Agent context

Investigated from a Slack thread: a user's self-driving canvas "errored out, then kept timing out." Traced his session — the canvas generation ran as a local repo-less task (workspaceMode: "local"), and the only frontend defect that matches an indefinite "Generating" state is the cloud/local asymmetry in the run-status derivation. The canvas's own runtime error renders in the about:srcdoc sandbox and isn't captured in our product analytics, so this targets the most likely stuck-state mechanism rather than a specific captured stack trace — worth a sanity check from whoever owns the freeform canvas.


Created with PostHog Code from a Slack thread

The freeform canvas derived its "is generating" state differently for cloud and local runs. Cloud falls back to the persisted run record and stops once the run status is terminal, but local relied solely on the live ACP session being connecting/connected. If that session went stale or the agent stalled without a clean disconnect, `running` stayed true and the canvas was pinned on "Generating" indefinitely — the "canvas keeps timing out" symptom.

Extract the run-status derivation into a pure, tested helper and give the local path the same terminal-run guard cloud already has: a terminal run record (completed/failed/cancelled) always ends "running", so a stuck session can no longer strand the canvas.

Generated-By: PostHog Code
Task-Id: 8085fa09-5b8c-475d-930e-582a5bc920b3
@github-actions

github-actions Bot commented Jun 23, 2026

Copy link
Copy Markdown

React Doctor found no issues in the changed files. 🎉

Reviewed by React Doctor for commit 7e76c6d.

@greptile-apps

greptile-apps Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Reviews (1): Last reviewed commit: "fix(canvas): stop local generation stran..." | Re-trigger Greptile

if (genTaskLoading) return true;

if (latestRun?.environment === "cloud") {
const cloudStatus = session?.cloudStatus ?? latestRun?.status ?? null;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Inside the latestRun?.environment === "cloud" branch, TypeScript narrows latestRun to be non-undefined (the optional-chain comparison can only be true when latestRun is defined). The ?. and ?? null are therefore superfluous — latestRun.status is always a TaskRunStatus here, and isTerminalStatus already handles null/undefined the same way.

Suggested change
const cloudStatus = session?.cloudStatus ?? latestRun?.status ?? null;
const cloudStatus = session?.cloudStatus ?? latestRun.status;

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines +40 to +106

it.each<[string, Run, Session | undefined, boolean]>([
["in_progress, no session", run("cloud", "in_progress"), undefined, true],
[
"session cloudStatus terminal overrides run record",
run("cloud", "in_progress"),
session("connected", "completed"),
false,
],
["run record terminal", run("cloud", "failed"), undefined, false],
])("cloud: %s", (_label, latestRun, sess, expected) => {
expect(
isCanvasGenerationRunning({
genTaskId: "t1",
genTaskLoading: false,
latestRun,
session: sess,
}),
).toBe(expected);
});

it.each<[string, Run, Session | undefined, boolean]>([
[
"session connected",
run("local", "in_progress"),
session("connected"),
true,
],
[
"session connecting",
run("local", "in_progress"),
session("connecting"),
true,
],
["no live session", run("local", "in_progress"), undefined, false],
[
"session disconnected",
run("local", "in_progress"),
session("disconnected"),
false,
],
// The regression: a terminal run record must stop "running" even if the
// live session is still (stale) reporting connected — otherwise the canvas
// is stranded on "Generating" forever.
[
"terminal run wins over stale connected session",
run("local", "completed"),
session("connected"),
false,
],
[
"failed run wins over stale connected session",
run("local", "failed"),
session("connected"),
false,
],
])("local: %s", (_label, latestRun, sess, expected) => {
expect(
isCanvasGenerationRunning({
genTaskId: "t1",
genTaskLoading: false,
latestRun,
session: sess,
}),
).toBe(expected);
});
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Missing test branch for a loaded task that has no latest_run yet (latestRun: undefined) combined with an active local session. After loading, if genTask.latest_run is absent (e.g., a task whose first run hasn't been created yet), the code falls through to the local path and the session check determines the result. The behaviour is correct — isTerminalStatus(undefined) returns false, so a connected session returns true — but it's an untested code path that could silently regress if the fallthrough logic changes.

…d local path

Address PR review:
- Drop superfluous optional-chain/`?? null` in the cloud branch where
  `latestRun` is already narrowed to defined.
- Add a test for a loaded task with no run record yet plus a connected
  local session, exercising the previously untested fallthrough.

Generated-By: PostHog Code
Task-Id: 670cc6fb-860d-439a-a93b-97e9a82bd5aa
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant