feat(canvas): handle outside file changes and editor crashes

Merged
GN
3d ago
+2668-202

Summary

  • Edits made outside Termul and a crashed editor must never silently lose work.
  • Part 7 of 10 in a stacked series. Review and merge bottom-up; each PR targets the one below it. Canvas mode v3 (figcraft engine). Spec: spec-openpencil-canvas-mode; architecture spine 2026-10-16 (AD-17 to AD-31).

Related Issue

No issue. Split from #1369 so each part stays under 50 files.

Type of Change

  • feat: new feature

What Changed

  • A clean canvas reloads an outside change; a dirty one offers Keep mine / Take disk / Save a copy. Termul's own writes are recognized by content hash.
  • A crash or repeated timeouts remount from the last save and show what was lost.
  • Converges the lifecycle stories on one open path, one close path and one mount record.

Scope

  • E2-5 plus the merge of the E2 wave.

How It Was Tested

  • bun run typecheck on this branch
  • Each part is a checkpoint that was built and tested when it was made (cargo test for canvas and host_mcp, vitest, real runs in headless Edge). The bottom and top of the stack were rebuilt and re-tested on current dev.

Checklist

  • PR title follows the conventional commit format
  • Explained why no issue exists
  • Added or updated tests
  • Read AGENTS.md
  • A human reviewed the complete diff before submission

🤖 Generated with Claude Code


Summary by cubic

Edits made outside Termul and a crashed editor must never silently lose work.

A canvas document changed on disk now either reloads silently (when the editor has no unsaved work) or shows a conflict with Keep mine / Take disk / Save a copy choices. Termul identifies its own writes by content hash. A crashed editor is remounted from the last saved bytes and reports how many unsaved changes were lost.

  • Autosave stops during a conflict; writes are checked against the last known file so unseen changes are never overwritten.
  • Lease-loss frames now carry a reason (takenOver or evicted) so an evicted renderer drops its iframe immediately.
  • Open and close inputs are converged into one request shape on both transports, with crashTimeouts sent from the new canvasCrashTimeouts setting.