Skip to content

feat(desktop): add a pinned canvas tab#221

Open
framethespace wants to merge 2 commits intoOpenCoworkAI:mainfrom
framethespace:codex/canvas-context-tab
Open

feat(desktop): add a pinned canvas tab#221
framethespace wants to merge 2 commits intoOpenCoworkAI:mainfrom
framethespace:codex/canvas-context-tab

Conversation

@framethespace
Copy link
Copy Markdown

@framethespace framethespace commented Apr 23, 2026

Summary

This adds a pinned Canvas tab to the desktop workspace so people can sketch rough layouts, drop in references, and send that visual context back with the next prompt instead of starting every iteration from a blank text box. The aim is to make the design loop feel a little more natural and collaborative while still keeping the output code-native and local-first.

Compatibility / upgradeability / no bloat / elegance: all green.

Type of change

  • Bug fix
  • New feature
  • Documentation
  • Refactor (no behavior change)
  • Build / CI / tooling
  • Breaking change

Linked issue

N/A

Checklist

  • I read docs/VISION.md, docs/PRINCIPLES.md, and CLAUDE.md before starting
  • Commits are signed with DCO (git commit -s)
  • pnpm lint && pnpm typecheck && pnpm test passes locally
  • Added/updated tests for the change
  • Added a changeset (pnpm changeset) if user-visible
  • Updated docs if behavior changed

Dependency additions (if any)

@excalidraw/excalidraw@^0.18.1

Screenshots / recordings (UI changes)

Canvas tab screenshot

@github-actions github-actions Bot added docs Documentation area:desktop apps/desktop (Electron shell, renderer) area:build Turbo/Vite/Biome/tsconfig toolchain labels Apr 23, 2026
Signed-off-by: framethespace <68256458+framethespace@users.noreply.github.com>
Signed-off-by: framethespace <68256458+framethespace@users.noreply.github.com>
@framethespace framethespace force-pushed the codex/canvas-context-tab branch from 2fea064 to edf2dc7 Compare April 23, 2026 19:16
Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

Findings

  • [Blocker] Canvas state is written to disk without an app-level schema wrapper, which violates the repo rule that everything persisted on disk must carry a schemaVersion. scene.excalidraw.json is written as raw Excalidraw JSON and imports.json is written as a bare array, so any future migration of canvas metadata becomes guesswork instead of an explicit versioned upgrade path. Evidence apps/desktop/src/main/canvas-ipc.ts:107, apps/desktop/src/main/canvas-ipc.ts:108, apps/desktop/src/renderer/src/store.ts:614
    Suggested fix:

    const payload = {
      schemaVersion: 1,
      sceneJson,
      importedFiles,
      lastGeneratedCanvasRevision,
    };
    await writeFile(canvasStatePath(designId), JSON.stringify(payload, null, 2), 'utf8');
  • [Blocker] Canvas load/save/export failures are silently downgraded to empty state or empty attachments, so the user can continue with a prompt that claims to include canvas context while nothing was actually attached. That breaks the project's “no silent fallbacks” rule and makes data loss hard to diagnose. Evidence apps/desktop/src/main/canvas-ipc.ts:83, apps/desktop/src/renderer/src/store.ts:2868, apps/desktop/src/renderer/src/store.ts:2912, apps/desktop/src/renderer/src/store.ts:2946, apps/desktop/src/renderer/src/store.ts:1682
    Suggested fix:

    try {
      return await window.codesign.canvas.writeContextFiles({ designId, files: artifacts });
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err);
      get().reportableErrorToast({
        code: 'CANVAS_CONTEXT_EXPORT_FAILED',
        scope: 'canvas',
        title: tr('canvas.contextExportFailed'),
        description: message,
      });
      throw new Error(`Failed to export canvas context: ${message}`);
    }
  • [Major] The “only resend when changed” check is reset on every design load, so a saved-but-unchanged canvas is treated as dirty after any app restart or design switch. loadCanvasStateForCurrentDesign() restores canvasRevision to 1 and lastGeneratedCanvasRevision to 0, and sendPrompt() uses that pair to decide whether to reattach the canvas. That means unchanged canvas context is resent indefinitely across sessions, which contradicts the feature contract and adds avoidable tokens/cost. Evidence apps/desktop/src/renderer/src/store.ts:1621, apps/desktop/src/renderer/src/store.ts:1623, apps/desktop/src/renderer/src/store.ts:2865, apps/desktop/src/renderer/src/store.ts:2866
    Suggested fix:

    interface PersistedCanvasStateV1 {
      schemaVersion: 1;
      sceneJson: string | null;
      importedFiles: LocalInputFile[];
      lastGeneratedCanvasRevision: number;
    }
    
    set({
      canvasRevision: hasSavedCanvas ? saved.lastGeneratedCanvasRevision : 0,
      lastGeneratedCanvasRevision: saved.lastGeneratedCanvasRevision,
    });

Summary

  • Review mode: initial. Three issues found: two hard-constraint regressions (schemaVersion missing on new on-disk canvas state, silent fallbacks on canvas failures) and one behavioral regression where unchanged canvas context is resent after reload/switch.

Testing

  • Not run (automation: pnpm is not available in this runner). Missing coverage: no test exercises the reload/switch path for lastGeneratedCanvasRevision persistence; current canvas tests only cover same-session behavior in apps/desktop/src/renderer/src/store.test.ts:261.

open-codesign Bot

const importedFiles = parseImportedFiles(record['importedFiles']);
await mkdir(canvasStateDir(designId), { recursive: true });
await Promise.all([
writeFile(canvasScenePath(designId), sceneJson ?? '', 'utf8'),
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.

[Blocker] This writes raw Excalidraw JSON / a bare imports array directly to disk with no app-level schemaVersion. The repo rule is explicit that persisted disk formats must be versioned, and parseCanvasScene() currently assumes this raw shape forever.

Suggested fix:

const payload = {
  schemaVersion: 1,
  sceneJson,
  importedFiles,
  lastGeneratedCanvasRevision,
};
await writeFile(canvasStatePath(designId), JSON.stringify(payload, null, 2), 'utf8');

files: artifacts,
});
} catch (err) {
console.warn('[open-codesign] buildCanvasContextFiles failed:', err);
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.

[Blocker] Returning [] here turns a real export failure into a silent no-op. sendPrompt() keeps going and can still show the canvas badge, so the user gets no signal that their canvas context was dropped.

Suggested fix:

} catch (err) {
  const message = err instanceof Error ? err.message : String(err);
  get().reportableErrorToast({
    code: 'CANVAS_CONTEXT_EXPORT_FAILED',
    scope: 'canvas',
    title: tr('canvas.contextExportFailed'),
    description: message,
  });
  throw new Error(`Failed to export canvas context: ${message}`);
}

canvasImportedFiles: nextImportedFiles,
canvasSceneLoaded: true,
canvasSeed: s.canvasSeed + 1,
canvasRevision: hasSavedCanvas ? 1 : 0,
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.

[Major] This resets dirty tracking after every reload/switch: any non-empty saved canvas comes back as canvasRevision = 1 and lastGeneratedCanvasRevision = 0, so sendPrompt() reattaches unchanged canvas context forever across sessions.

Suggested fix:

set({
  canvasRevision: hasSavedCanvas ? saved.lastGeneratedCanvasRevision : 0,
  lastGeneratedCanvasRevision: saved.lastGeneratedCanvasRevision,
});

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:build Turbo/Vite/Biome/tsconfig toolchain area:desktop apps/desktop (Electron shell, renderer) docs Documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant