Skip to content

feat(canvas): M6 — in-place live editing - #833

Merged
h4yfans merged 14 commits into
mainfrom
spatial-canvas-m6-live-editing
Jul 22, 2026
Merged

feat(canvas): M6 — in-place live editing#833
h4yfans merged 14 commits into
mainfrom
spatial-canvas-m6-live-editing

Conversation

@h4yfans

@h4yfans h4yfans commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator

M6 — In-place live editing (spatial canvas)

Last core milestone before default-on (M7). Double-clicking an idle card promotes it to a single active state that mounts a full live editor; click-away/Escape returns it to the idle preview and unmounts the editor. Renderer-only, over the existing M2 hybrid card system (Excalidraw rectangle + pointer-events:none DOM overlay).

Source of truth: docs/superpowers/specs/2026-07-21-spatial-canvas-m6-live-editing-design.md (+ master spec 2026-07-17-spatial-canvas-design.md). Plan: docs/superpowers/plans/2026-07-21-spatial-canvas-m6-live-editing.md.

What's in it

  • Active-card state machinecanvas-active.ts (angle-aware hit-test, reducer, tool-gate, active-pin); double-click→activate, click-away (no swallow) / Escape (swallowed) / tool-select → deactivate; keydown/keyup contained so Cmd/Ctrl+Z reaches the editor, not Excalidraw.
  • Note editor<EmbeddedNoteEditor> reuses <ContentArea>, bound to the note's Y.Doc via a new ref-counted, noteId-keyed shared-doc registry (yjs-doc-registry.ts + use-yjs-collaboration.ts refactor) so a note open in a tab and active on a card share one doc (no echo/dupe). Single-consumer behaviour is byte-identical to before; ownership handoff is reactive.
  • Task editor — slim field subset (title/status/priority/due/description) reusing the drawer's standalone field components; writes route through the shared updateTask mapper.
  • Event editor — extracted <CalendarEventForm> from the event popover (popover becomes a thin wrapper; no caller change); card owns the draft, saves via calendarService.updateEvent.
  • Perf/virtualization — one active heavy editor at a time; off-screen cards stay unmounted (E2E-gated).
  • The ↗ redirect stays distinct from editing (double-click edits in place; ↗ opens the item in a tab) — no cross-fire.

Notable finds

  • Data-loss fix: tab-switch/close serialized a torn-down Excalidraw as an empty scene, wiping cards + ink on reopen. serialize() now returns null when the wrapper is disconnected (skips the destructive write; debounce/beforeunload/quit-flush unaffected). Covered by a new restore-regression E2E.
  • R17 is authenticated-only: Yjs collaboration engages only when synced. Unauthenticated users (and the E2E vault) edit via non-collaborative BlockNote + markdown-save (mirrors note.tsx, gated so there's no double-write). The shared registry is validated by unit tests; E2E [T019] Create migration runner #19 is an honest unauthenticated no-duplicate test.

Backward compatibility

  • spatialCanvas flag stays default-OFF (untouched). No sync / contract / crypto / DB / migration change. Renderer editing over existing entities.

Testing

Deferred to M7 (rollout) — need sign-off

  • Docs: docs:impact --strict reports missing-docs; docs are an M7 deliverable per master spec §13. Pushed with MEMRY_DOCS_IMPACT_SKIP=1 (intentionally non-docs).
  • Unauthenticated split-view co-edit: with the flag ON + unauthenticated + the same note visible in a tab and an active card (split view), the two non-collaborative editors can clobber body edits and double-run task auto-conversion. Documented in design §12; must be guarded before default-on.

Follow-ups (non-blocking)

  • Event card shows a stuck "Loading…" if getEvent rejects (Escape still deactivates).
  • toCreatePayload/createDraftFromItem are hand-copied into the event editor — extract to shared date-utils in a future pass.

Copilot AI review requested due to automatic review settings July 22, 2026 04:05
@vercel

vercel Bot commented Jul 22, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

2 Skipped Deployments
Project Deployment Actions Updated (UTC)
memry-docs Ignored Ignored Preview Jul 22, 2026 11:43am
memrynote-landing Ignored Ignored Preview Jul 22, 2026 11:43am

@github-actions github-actions Bot added documentation Improvements or additions to documentation enhancement New feature or request test labels Jul 22, 2026
@github-actions

github-actions Bot commented Jul 22, 2026

Copy link
Copy Markdown

React Doctor found no new issues. 🎉

Reviewed by React Doctor for commit 200c176.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Implements M6 “in-place live editing” for spatial-canvas cards: a single active card can mount a full live editor (note/task/event), with dblclick-to-activate and click-away/Escape/tool-select to deactivate, plus a renderer-side Yjs doc registry to safely support same-note multi-mount within one window.

Changes:

  • Add active-card state machine (hit-test/reducer/pinning) and wire it into the canvas overlay to mount exactly one active editor at a time.
  • Introduce a ref-counted, noteId-keyed shared Yjs doc registry and route useYjsCollaboration through it; gate ContentArea side effects to a single owner.
  • Add embedded note/task/event editors for active cards, extract <CalendarEventForm>, fix Excalidraw unmount “restore wipe”, and add E2E/unit coverage for the new flows.

Reviewed changes

Copilot reviewed 25 out of 25 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
docs/superpowers/specs/2026-07-21-spatial-canvas-m6-live-editing-design.md M6 design doc for active editing + shared-doc approach.
docs/superpowers/plans/2026-07-21-spatial-canvas-m6-live-editing.md Implementation plan detailing tasks and test strategy.
apps/desktop/tests/e2e/canvas-editing.e2e.ts E2E coverage for activate/deactivate, note/task/event editing, restore regression, virtualization.
apps/desktop/src/renderer/src/sync/yjs-doc-registry.ts Ref-counted registry to share one Yjs entry per noteId within a renderer window.
apps/desktop/src/renderer/src/sync/yjs-doc-registry.test.ts Unit tests for refcounting and side-effect ownership promotion.
apps/desktop/src/renderer/src/sync/use-yjs-collaboration.ts Refactor hook to acquire/release shared doc entries and expose side-effect ownership.
apps/desktop/src/renderer/src/sync/use-yjs-collaboration.registry.test.ts Minimal test ensuring registry wiring exports exist.
apps/desktop/src/renderer/src/pages/canvas/embedded-note-editor.tsx Embedded note editor for active cards with non-collab markdown-save fallback.
apps/desktop/src/renderer/src/pages/canvas/embedded-note-editor.test.tsx Unit tests for debounce + save-registry integration.
apps/desktop/src/renderer/src/pages/canvas/canvas-task-editor.tsx Slim in-place task editor reusing existing task field components + shared update mapper.
apps/desktop/src/renderer/src/pages/canvas/canvas-task-editor.test.tsx Unit tests for task field wiring + debounced description persistence.
apps/desktop/src/renderer/src/pages/canvas/canvas-event-editor.tsx In-place event editor that loads a draft and saves via calendar service.
apps/desktop/src/renderer/src/pages/canvas/canvas-event-editor.test.tsx Unit tests for draft conversion and save/dismiss wiring.
apps/desktop/src/renderer/src/pages/canvas/canvas-editor.tsx Guard serialize() to prevent persisting empty scene on Excalidraw teardown.
apps/desktop/src/renderer/src/pages/canvas/canvas-card.tsx Add data-canvas-redirect marker to keep ↗ distinct from dblclick editing.
apps/desktop/src/renderer/src/pages/canvas/canvas-card-overlay.tsx Active-card wiring (dblclick activate, click-away/tool deactivate, pin active for virtualization).
apps/desktop/src/renderer/src/pages/canvas/canvas-card-overlay.test.tsx Update tests to assert activation/deactivation behavior (not redirect).
apps/desktop/src/renderer/src/pages/canvas/canvas-card-active.tsx Active-card container: keyboard containment + editor switch (note/task/event).
apps/desktop/src/renderer/src/pages/canvas/canvas-card-active.test.tsx Unit tests for active container attrs, focus, and Escape handling.
apps/desktop/src/renderer/src/pages/canvas/canvas-active.ts Pure active-state utilities (hitTest/reducer/tool gate/pin).
apps/desktop/src/renderer/src/pages/canvas/canvas-active.test.ts Unit tests for active-state utilities.
apps/desktop/src/renderer/src/components/note/content-area/types.ts Add runSideEffects? prop to support single-owner side effects.
apps/desktop/src/renderer/src/components/note/content-area/ContentArea.tsx Gate task auto-conversion behind runSideEffects + wire ownership from Yjs hook.
apps/desktop/src/renderer/src/components/calendar/calendar-event-popover.tsx Replace inline form body with extracted <CalendarEventForm>.
apps/desktop/src/renderer/src/components/calendar/calendar-event-form.tsx New extracted controlled calendar event form component.
Comments suppressed due to low confidence (1)

apps/desktop/src/renderer/src/sync/use-yjs-collaboration.ts:194

  • When collaboration is disabled (no noteId / enabled=false / noteId mismatch), the hook still returns the previous note’s isRemoteUpdateRef via activeState.isRemoteUpdateRef. That ref can point at a destroyed doc and can incorrectly flag remote updates for the current (disabled) consumer. Return a dummy ref whenever the hook is in the DISABLED_STATE (and ideally derive the disabled boolean once to avoid divergence).
  const state =
    !noteId ||
    !enabled ||
    activeState.noteId !== noteId ||
    (!activeState.fallback && !activeState.provider?.isSynced)

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +55 to +72
useEffect(() => {
let cancelled = false
calendarService
.getEvent(eventId)
.then((event) => {
if (cancelled || !event) return
setDraft(toDraft(event))
})
.catch((error: unknown) => {
log.error('Failed to load calendar event', {
eventId,
error: extractErrorMessage(error)
})
})
return () => {
cancelled = true
}
}, [eventId])
Copilot AI review requested due to automatic review settings July 22, 2026 11:42
@h4yfans
h4yfans force-pushed the spatial-canvas-m6-live-editing branch from da11641 to 200c176 Compare July 22, 2026 11:42

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 26 out of 26 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (2)

apps/desktop/src/renderer/src/sync/use-yjs-collaboration.ts:194

  • When collaboration is disabled (no noteId / enabled=false / noteId mismatch), the hook still returns activeState.isRemoteUpdateRef, which can point at a previous note’s doc. That can leak a stale remote-update flag into callers and makes the return value inconsistent with the state (which correctly falls back to DISABLED_STATE).
  const state =
    !noteId ||
    !enabled ||
    activeState.noteId !== noteId ||
    (!activeState.fallback && !activeState.provider?.isSynced)

apps/desktop/src/renderer/src/pages/canvas/canvas-event-editor.tsx:68

  • If calendarService.getEvent(eventId) rejects, the component logs and keeps draft as null, leaving the card stuck in the “Loading…” state indefinitely (no retry, no error, no way to dismiss besides click-away).
  useEffect(() => {
    let cancelled = false
    calendarService
      .getEvent(eventId)
      .then((event) => {
        if (cancelled || !event) return
        setDraft(toDraft(event))
      })
      .catch((error: unknown) => {
        log.error('Failed to load calendar event', {
          eventId,
          error: extractErrorMessage(error)
        })
      })

Comment on lines +51 to +56
try {
await notesService.update({ id: noteId, content: pending })
lastSavedContentRef.current = pending
} catch (err) {
log.error('Failed to save note', { noteId, error: err })
}
@h4yfans
h4yfans marked this pull request as ready for review July 22, 2026 11:51
@h4yfans
h4yfans merged commit 166e75d into main Jul 22, 2026
20 checks passed
@h4yfans
h4yfans deleted the spatial-canvas-m6-live-editing branch July 22, 2026 11:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation enhancement New feature or request test

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants