Skip to content

feat(desktop): canvas version history, restore, and conflict-checked save - #6775

Closed
wpfleger96 wants to merge 3 commits into
mainfrom
hayt/canvas-history-desktop
Closed

feat(desktop): canvas version history, restore, and conflict-checked save#6775
wpfleger96 wants to merge 3 commits into
mainfrom
hayt/canvas-history-desktop

Conversation

@wpfleger96

@wpfleger96 wpfleger96 commented Aug 25, 2026

Copy link
Copy Markdown
Member

Adds channel canvas version history, restore, and conflict-checked saves to the desktop app. Every canvas write is a regular signed kind:40100 event the relay retains, so the full revision stream is already queryable — this exposes it and guards writes with optimistic concurrency.

What changed

  • Head event id through the API. getCanvas now surfaces the loaded revision as eventId on CanvasResponse (the Rust command already returned it; the TS layer dropped it).
  • Conflict-checked save. set_canvas / setCanvas take an optional expectedRevision, and the editor sends the head id it snapshotted at edit-start (not the live head — a background refetch can move it mid-edit). When the head moved, the relay rejects and the editor renders a distinct "canvas changed — reload" message instead of a raw error. canvasConflict.ts recognizes all three frozen reject strings as reload-required conflicts: conflict: canvas changed since it was loaded, conflict: canvas revision does not exist, and conflict: canvas write does not supersede the current head.
  • Create-race sentinel. The first save of a new canvas sends expected-revision: "none" rather than omitting the tag, so a concurrent first creation is rejected as a conflict instead of silently overwritten.
  • Writer discipline. set_canvas signs created_at = max(now, asserted_head.created_at + 1) so an accepted tagged write always sorts strictly ahead of the head it asserts (created_at DESC, id ASC), keeping the accepted write the visible head even under same-second or behind-clock writers.
  • History panel with pagination. A new get_canvas_history Tauri command pages the kind:40100 stream newest-first via a composite (until, before_id) cursor that mirrors the relay read order, so ties on a second never skip or repeat a revision. The canvas view gains a History toggle listing each revision's author, timestamp, a line diff against the current content, a "Load older" control, and — for editors — a Restore action.
  • Restore never mutates history. Restore publishes a new head carrying the selected revision's content under the same expected-revision guard, so a concurrent edit surfaces the same conflict state as a normal save.

Canvas API and types moved into dedicated tauriCanvas.ts / canvasTypes.ts modules, and the canvas query/mutation hooks into canvasHooks.ts (all re-exported from the existing barrels) to keep the capped tauri.ts, types.ts, and hooks.ts under the desktop file-size ratchet.

expectedRevision is optional end-to-end, so every flow except the conflict path works against current main; the relay-side precondition, none sentinel, head-advancement guarantee, and idempotent-replay exception ship in the crates PR, #6780. PR2 merges after PR1.

Related: #6780 (relay + SDK + CLI)

Originating discussion: buzz://message?channel=326d56bc-c96c-4af0-86a1-5e804cd1b467&id=d63c63f432a49166b25a27e48bec69b9767b6c0f148c383baa12da574b383415

@wpfleger96

Copy link
Copy Markdown
Member Author

the crates-side PR is up: #6780 (relay expected-revision precondition + SDK + CLI). merges before this one.

…save

Expose the canvas head event id through the TS API and send it as the
optimistic-concurrency `expected-revision` on every save. When a
concurrent edit moved the head, the relay reject surfaces as a distinct
"canvas changed — reload" state instead of a generic error.

Add a get_canvas_history command over the retained kind:40100 stream and
a history panel with author, timestamp, a line diff against the current
content, and a Restore action. Restore publishes a new head carrying the
selected revision's content under the same conflict guard — it never
mutates or deletes history.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
@wpfleger96
wpfleger96 force-pushed the hayt/canvas-history-desktop branch from 4441474 to 7b3aa19 Compare August 25, 2026 16:15
Hayt and others added 2 commits August 25, 2026 13:33
Gate canvas existence on the persisted revision id, not content
truthiness: an empty-string canvas is a valid kind:40100 revision (and
restore can republish one), so keying existence, the Create/Edit label,
and the History section on content hid retained history for an existing
empty canvas.

Bound get_canvas_history's page size to the relay read maximum. A
request above 1,000 is silently clamped by the relay, which made
`events.len() == page_size` false and nulled the cursor even when older
revisions remained, stranding them behind an unreachable page. Reject
outside 1..=1000.

Reset the shared restore mutation on revision selection change so a
failed restore's error can no longer render under a different row.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…esktop

* origin/main:
  Add database pressure observability (#6700)
  revert fixed mention highlight (#6716)
  highlight search terms in results and messages (#6702)
  fix(desktop): make lightbox zoom controls interactive (#6710)
  Support community deletion in versioned media buckets (#6738)
  Fix TipTap editor mount race (#6779)
  feat(buzz-agent): gate LLM tool calls on session/request_permission (#5712)
  Add staging dev relay image workflow (#6709)

Signed-off-by: Hayt <9e1c23a3fd83f61da34420e4e88ff1b16e45cafcc0cd9019eb07d4ecfa8ca9b0@buzz.block.builderlab.xyz>
@wpfleger96

Copy link
Copy Markdown
Member Author

Consolidated into #6780, which now covers the full canvas version-history feature (relay/DB/SDK/CLI + Desktop history UI).

@wpfleger96 wpfleger96 closed this Aug 25, 2026
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