Skip to content

Sheets: a board is several canvases now - #33

Merged
boubou666 merged 5 commits into
mainfrom
sheets
Aug 8, 2026
Merged

boubou666 merged 5 commits into
mainfrom
sheets

Conversation

@boubou666

@boubou666 boubou666 commented Aug 8, 2026 •

Copy link
Copy Markdown
Owner

First of three. This one is the model, the stored format, the tab strip, and the op addressing that has to ship with them. Sheet management crossing live to other people on the board is PR 2; duplicating a whole board is PR 3.

A board was one infinite surface, which is enough room until a project has three subjects — and then they either sprawl into each other or get split across boards that have nothing to do with one another. Sheets are the tabbed answer every tool with this problem arrives at. Anyone who can open the board gets all of them: sheets organise a board, they do not restrict it.

Only the active sheet is a live Store

The rest are documents held beside it, in src/core/sheets.js. That is the whole design, and what it buys is that store is the same object for the life of the app — the renderer, the input layer, the selection and the format bar never learn that sheets exist, and an inactive sheet costs no listeners and no rendering, only correct content.

Switching is Store#checkpoint() on the way out and Store#restore() on the way in, both new and both small, so the undo stack travels with the sheet. Undoing on the canvas you are looking at is what ctrl+Z means; walking back into edits on a canvas nobody is showing is not.

Ops from another client for a sheet that is not on screen are applied through a throwaway Store — so there is exactly one implementation of what an op means, mergeOrder included, rather than a second one that would drift from it.

The stored document keeps a board where a board has always been

{ "v": 2,
  "sheets": [ { "id": …, "name": "Discovery", "order": […], "objects": […] }, … ],
  "order":   […],   // a copy of sheets[0]
  "objects": […] }  // a copy of sheets[0]

The first sheet is written twice on purpose:

  • No migration and no policy changes. isBoard() in platform/storage.js and the boards_doc_shape constraint from 20260804101204_boards.sql both still pass. "Board access is sheet access" needs no enforcement because there is nothing new to enforce.
  • A stale tab is not a data-loss event. A document those checks fail is read as a board that is not there, which app.hydrate() answers by seeding a starter board and autosaving it over the top. An old client reading a v2 board instead finds a real one.

It is insurance, not a guarantee: an old client that saves still writes a v1 document and drops the other sheets. There is no version of this that fixes a client already running, and it is worth knowing before this deploys.

Reading is total — a v1 board, or junk, is one sheet named Sheet 1. Every board that exists upgrades on load and nothing is migrated in place.

Ops name the sheet they were made on

Shipping the model without this would mean two people on one board with different sheets open writing into each other's canvas, so it is here rather than in PR 2. The payload gains sheet; the receiver routes through sheets.applyRemote. A payload with no sheet is an older client — or a board with one canvas — and means the sheet on screen.

Verified by breaking the routing and watching exactly the two covering tests fail.

The strip

Along the bottom left, from the same tokens as the toolbar and the format bar. Click a tab to switch, double-click to rename, + to add, ⋯ on the active tab for Rename / Duplicate / Delete. Delete confirms through the existing in-page dialog and is absent on the last sheet — a board with no sheets has no canvas.

The menu is positioned fixed from the button's box rather than hung off the tab: the strip scrolls, and a scroll container clips what its children draw outside it, which for a menu that opens upwards is all of it. That cost me a test run to find.

Two calls made while building

  • Nothing gates who may add or remove a sheet. Nothing else on this canvas gates editing either — a viewer can drag cards all day and the write is refused at the database. A role check here would be the only one in the app; sheets follow the document's rule instead. (The plan said viewers would get a read-only strip; that did not survive contact with the code, which has no client-side notion of a role.)
  • A switch clears the selection — it names objects on the sheet being left, and a selection you cannot see is a delete key pointed off screen — and each sheet's camera is remembered in memory only. A camera is where you are standing; storing it would broadcast it to everyone on the board and make a save out of scrolling.

Tests

631 passing, line coverage 96.0%. core/sheets.js and SheetTabs.jsx at 100%.

  • test/node/sheets.test.js — the v1 upgrade, junk read as an empty sheet, repeated ids replaced, add/rename/duplicate/delete, the last-sheet refusal, per-sheet undo, remote ops landing on an inactive sheet without clearing its history, and the shape of what gets stored.
  • test/node/sync-hydration.test.js — a new block for addressing: an op names its sheet going out, lands on that sheet arriving, falls back to the one on screen when unnamed, and keeps its sheet across the held-ops replay.
  • test/browser/sheets.test.js — the strip end to end, including a reload, the stored shape, and a pre-sheets board opening as one sheet.

🤖 Generated with Claude Code

https://claude.ai/code/session_01GJ9tdqJPUktg2XR3M7Ncqr

Summary by CodeRabbit

  • New Features

    • Added support for multiple sheets within a board.
    • Switch between sheets using tabs, with options to add, rename, duplicate, and delete sheets.
    • Added responsive tab navigation and sheet action menus.
  • Improvements

    • Preserved each sheet’s content, camera position, and undo history when switching.
    • Switching sheets clears selection and restores the destination view.
    • Autosave and synchronization preserve the complete multi-sheet board.
    • Existing single-sheet boards continue to load correctly, with collaborative changes applied to the correct sheet.

A board was one infinite surface, which is enough room until a project
has three subjects — and then they either sprawl into each other or get
split across boards that have nothing to do with one another. Sheets are
the tabbed answer every tool with this problem arrives at: one board,
several named canvases, a strip along the bottom to move between them.
Anyone who can open the board gets all of them. Sheets organise a board;
they do not restrict it.

**Only the active sheet is a live `Store`.** The rest are documents held
beside it. That is the whole design, and what it buys is that `store` is
the same object for the life of the app: the renderer, the input layer,
the selection and the format bar never learn that sheets exist, and an
inactive sheet costs no listeners and no rendering — only correct
content, which is all anyone can see of it. Switching is `checkpoint` on
the way out and `restore` on the way in, so undo travels with the sheet.
Undoing on the canvas you are looking at is what ctrl+Z means; walking
back into edits on a canvas nobody is showing is not.

**The stored document keeps a board where a board has always been.** A
sheeted board writes its first sheet twice — once in `sheets`, once at
the top level. `isBoard` and the database's `boards_doc_shape` check both
still pass, so there is no migration and no policy to change; and a tab
still running the previous build finds a real board rather than a
document it reads as absent, which it would answer by seeding a starter
board over the top. Insurance, not a guarantee: that client saving still
writes v1 back and drops the other sheets.

**Ops say which sheet they were made on.** Without that, two people on
one board with different sheets open write into each other's canvas —
so this ships with the addressing rather than after it. A payload with no
sheet is an older client, and means the canvas it is looking at.

Sheet *management* does not cross to other clients yet; that is the next
change. Nothing gates who may add or remove one, deliberately: nothing
else on this canvas gates editing either — a viewer can drag a card all
day and the write is refused at the database. Sheets follow the document's
rule rather than inventing one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GJ9tdqJPUktg2XR3M7Ncqr
@coderabbitai

coderabbitai Bot commented Aug 8, 2026 •

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR adds multi-sheet board documents with tabs, per-sheet objects and undo history, versioned persistence, legacy migration, camera handling, autosave integration, and sheet-aware synchronization.

Changes

Multi-sheet board flow

Layer / File(s) Summary
Document state and serialization
src/core/sheets.js, src/core/store.js, test/node/sheets.test.js
Adds normalized version-2 documents, legacy migration, serialization, per-sheet checkpoints, restoration, lifecycle operations, remote routing, and tests.
Sheet lifecycle and app integration
src/app.js, src/components/BoardCanvas.jsx
Adds sheet initialization, commands, camera restoration, selection clearing, document loading, autosave wiring, synchronization wiring, and public app exposure.
Sheet tab interface
src/components/SheetTabs.jsx, src/styles/canvas.css, test/browser/sheets.test.js
Adds accessible tabs, menus, creation, renaming, duplication, deletion, responsive styling, and browser coverage.
Persistence and synchronization integration
src/core/autosave.js, src/platform/sync.js, test/node/autosave.test.js, test/node/lifecycle.test.js, test/node/sync.test.js, test/node/sync-hydration.test.js
Autosave serializes the complete document. Synchronization batches and routes operations by sheet, including deferred hydration and legacy sheet-less messages.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant SheetTabs
  participant createSheets
  participant Store
  participant createBoardSync
  participant Repository
  User->>SheetTabs: select or edit a sheet
  SheetTabs->>createSheets: invoke sheet command
  createSheets->>Store: checkpoint or restore sheet state
  Store->>createBoardSync: emit sheet operation
  createBoardSync->>createBoardSync: route operation by sheet ID
  createSheets->>Repository: serialize complete document
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 70.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: adding multiple canvases, or sheets, within a board.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch sheets

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 8

🧹 Nitpick comments (5)
src/core/sheets.js (1)

304-309: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Avoid cloning the undo stacks on every save.

stateOf returns store.checkpoint() for the active entry. checkpoint runs structuredClone over past and future. toJSON discards both. Autosave calls toJSON on every write, so each save deep-clones a history that holds full object copies for every recorded add and del.

Read the active sheet through store.toJSON() here instead.

♻️ Proposed refactor
     toJSON() {
       return writeDocument(entries.map((entry) => {
-        const { order, objects } = stateOf(entry);
+        const { order, objects } = entry.id === activeId ? store.toJSON() : entry.state;
         return { id: entry.id, name: entry.name, order, objects };
       }));
     },
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/sheets.js` around lines 304 - 309, Update toJSON() to serialize the
active sheet using store.toJSON() instead of stateOf(entry), avoiding checkpoint
cloning of undo history. Preserve the returned id, name, order, and objects
fields while excluding past and future from the saved document.
src/app.js (1)

98-128: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

cameras keeps an entry for every removed sheet.

showingSheet writes the outgoing camera at Line 111 before it runs change(). When change() is sheets.remove(id) and the removed sheet was on screen, the entry written for that id stays in the map. The id can never become active again, so the entry is never read and never replaced.

Delete the camera for a sheet that is no longer in sheets after the change.

♻️ Proposed refactor
     const result = change();
     if (sheets.activeId === from) return result;
 
+    // The sheet just left may have been removed rather than switched away
+    // from, and a camera for a sheet that cannot come back is never read.
+    if (!sheets.has(from)) cameras.delete(from);
     selection.clear();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app.js` around lines 98 - 128, Update showingSheet so that after change()
completes, it removes the camera entry for the previous active sheet when that
sheet no longer exists in sheets. Preserve the existing camera restoration and
viewport behavior for sheets that remain available.
test/browser/sheets.test.js (1)

236-237: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Also assert that the duplicated objects match the first sheet.

Line 236 checks only that stored.objects is an array. Line 237 compares stored.order with stored.sheets[0].order. The top-level compatibility copy could carry the wrong objects and still pass. Add the matching check on objects.

💚 Proposed addition
     assert.ok(Array.isArray(stored.order) && Array.isArray(stored.objects), 'no board at the top level');
     assert.deepEqual(stored.order, stored.sheets[0].order, 'the top level is not the first sheet');
+    assert.deepEqual(stored.objects, stored.sheets[0].objects, 'the top level objects are not the first sheet\'s');
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/browser/sheets.test.js` around lines 236 - 237, Extend the assertions in
the relevant browser sheet persistence test to compare top-level stored.objects
with stored.sheets[0].objects, alongside the existing stored.order comparison,
so the duplicated compatibility data is verified against the first sheet.
src/styles/canvas.css (1)

761-762: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use a theme token for the danger colour.

Every other colour in this block reads from a variable (--panel, --line, --ink, --muted, --accent). #d83a52 is hardcoded. The comment above states that deleting reads red "here as everywhere else", which suggests a token already exists. A fixed red also risks low contrast against a dark --panel.

#!/bin/bash
# Description: Find existing danger colour tokens and other uses of this hex.
rg -n --glob '*.css' -- '`#d83a52`|--danger|--red|\.danger\b'
rg -n --glob '*.css' -C 2 -- '--accent\s*:'
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/styles/canvas.css` around lines 761 - 762, Replace the hardcoded `#d83a52`
values in the .sheet-menu button.danger color and hover background with the
existing danger theme token, preserving the current hover opacity behavior and
matching the token used elsewhere for danger styling.
src/components/SheetTabs.jsx (1)

80-93: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Align the tablist structure with the ARIA tabs pattern.

Keep only sheet tabs inside role="tablist". Move .sheet-more and .sheet-add to a separate control group, and remove the wrapper div from the tablist hierarchy. Add arrow-key navigation with roving tabindex.

Expose the canvas as a role="tabpanel" linked to the selected tab with aria-controls and aria-labelledby.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/SheetTabs.jsx` around lines 80 - 93, Update the SheetTabs
component so role="tablist" contains only sheet tab elements, moving .sheet-more
and .sheet-add into a separate control group and removing the extra tablist
wrapper. Implement roving tabindex with arrow-key navigation among tabs, and add
matching tab/panel ARIA linkage: each tab should use aria-controls for the
canvas panel, while the canvas should be role="tabpanel" with aria-labelledby
referencing the selected tab.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/app.js`:
- Around line 314-318: Update the saved-board initialization around
sheets.load(saved.board) so outbound synchronization remains queued until
hydration completes. Ensure createBoardSync does not broadcast the temporary
sheets.activeId, then release and stamp the queued batch with the loaded sheet
ID after sheets.load finishes; keep heldUntil limited to inbound operations.

In `@src/components/SheetTabs.jsx`:
- Around line 57-63: Update the rename flow around the ask.prompt call to trim
the returned name before calling commands.renameSheet, and only rename when the
trimmed value is non-empty. Preserve the existing null/cancel behavior and pass
the trimmed value to commands.renameSheet.
- Around line 125-129: Update the menu dismissal effect to close the open menu
on both window resize and the sheet strip’s scroll events, in addition to its
existing dismissal triggers. Reuse the strip ref and existing menu state/setter
symbols in the effect, and ensure both listeners are registered and cleaned up
so the fixed-position menu cannot retain stale coordinates.

In `@src/core/sheets.js`:
- Around line 65-72: Update the sheet-mapping logic in readDocument to track ids
already assigned while iterating through sheets, generating a new id when an id
is missing, empty, or already used; preserve unique valid ids and record every
final id before processing the next sheet. Correct the duplicate-id fixture in
the relevant sheets test so it verifies both sheets receive distinct ids.

In `@src/platform/sync.js`:
- Around line 145-151: Update the store.onOps queueing flow to capture
sheets.activeId when each local operation batch is created, rather than reading
it during the delayed flush. Queue batches with their captured sheet IDs and
have the broadcast send path use each batch’s stored sheet; add coverage for
switching sheets before a delayed flush and verify operations retain their
original sheet.

In `@src/styles/canvas.css`:
- Around line 677-682: Update the .sheet-tab rule to prevent flex shrinking by
applying the same non-shrinking flex behavior used by .sheet-add, while
preserving the existing width cap and scrolling behavior of the .sheet-tabs
container.

In `@test/browser/sheets.test.js`:
- Around line 76-80: Replace the immediate absence assertion at
test/browser/sheets.test.js:76-80 with page.waitFor waiting until
document.querySelector('[data-id="${first}"]') === null. Before reading
app.selection.size at test/browser/sheets.test.js:117-118, add page.waitFor for
the second tab’s appearance; both sites must wait on the UI condition produced
by the preceding click rather than relying on timing.
- Around line 242-250: Navigate away from the board route before seeding the
legacy v1 document in this test, matching the existing beforeEach setup. Insert
the root-route navigation before the localStorage.setItem call so pending saves
from app.store.load cannot overwrite the seeded data before the subsequent
page.goto.

---

Nitpick comments:
In `@src/app.js`:
- Around line 98-128: Update showingSheet so that after change() completes, it
removes the camera entry for the previous active sheet when that sheet no longer
exists in sheets. Preserve the existing camera restoration and viewport behavior
for sheets that remain available.

In `@src/components/SheetTabs.jsx`:
- Around line 80-93: Update the SheetTabs component so role="tablist" contains
only sheet tab elements, moving .sheet-more and .sheet-add into a separate
control group and removing the extra tablist wrapper. Implement roving tabindex
with arrow-key navigation among tabs, and add matching tab/panel ARIA linkage:
each tab should use aria-controls for the canvas panel, while the canvas should
be role="tabpanel" with aria-labelledby referencing the selected tab.

In `@src/core/sheets.js`:
- Around line 304-309: Update toJSON() to serialize the active sheet using
store.toJSON() instead of stateOf(entry), avoiding checkpoint cloning of undo
history. Preserve the returned id, name, order, and objects fields while
excluding past and future from the saved document.

In `@src/styles/canvas.css`:
- Around line 761-762: Replace the hardcoded `#d83a52` values in the .sheet-menu
button.danger color and hover background with the existing danger theme token,
preserving the current hover opacity behavior and matching the token used
elsewhere for danger styling.

In `@test/browser/sheets.test.js`:
- Around line 236-237: Extend the assertions in the relevant browser sheet
persistence test to compare top-level stored.objects with
stored.sheets[0].objects, alongside the existing stored.order comparison, so the
duplicated compatibility data is verified against the first sheet.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 2d0d0d40-92af-420f-8910-8286b9ccf645

📥 Commits

Reviewing files that changed from the base of the PR and between 318d07b and 37e653b.

📒 Files selected for processing (14)
  • src/app.js
  • src/components/BoardCanvas.jsx
  • src/components/SheetTabs.jsx
  • src/core/autosave.js
  • src/core/sheets.js
  • src/core/store.js
  • src/platform/sync.js
  • src/styles/canvas.css
  • test/browser/sheets.test.js
  • test/node/autosave.test.js
  • test/node/lifecycle.test.js
  • test/node/sheets.test.js
  • test/node/sync-hydration.test.js
  • test/node/sync.test.js

Comment thread src/app.js
Comment thread src/components/SheetTabs.jsx
Comment thread src/components/SheetTabs.jsx
Comment thread src/core/sheets.js
Comment thread src/platform/sync.js Outdated
Comment thread src/styles/canvas.css
Comment thread test/browser/sheets.test.js Outdated
Comment thread test/browser/sheets.test.js
Janacek and others added 3 commits August 8, 2026 18:42
CI caught what a machine with no Docker could not: every cross-client
test failed, because each client minted its own id for the same first
sheet and then addressed its ops to it. The receiver had no such sheet
and dropped them. That was not the test harness being unrealistic — it
was every board that exists, since the first sheet is not created by
anybody. It is what a board from before sheets *becomes* when it is read,
on each client separately, and two of them have to arrive at the same
answer without being told.

So it is a constant. Every other sheet is made by somebody, on a client,
at a moment, and gets a generated id that travels with the sheet itself.

Pinned three ways: the id is the same wherever a document is read, ops
addressed by one client's reading land on another's, and — the one that
would have caught this — the two-client suite that only CI can run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GJ9tdqJPUktg2XR3M7Ncqr
Three from review, two of them places where the comment claimed something
the code did not do.

**The sheet was read at send time.** The throttle holds ops for up to
50ms after the gesture that made them, and switching sheets in that
window is one click — so a drag on one sheet could go out addressed to
the sheet the person moved to, and land there on everyone else's screen.
The comment said switching "is not something you can do in the middle of
the gesture", which is true and beside the point: the window is after the
gesture, not during it. The queue holds batches now, each stamped when
its ops arrive.

**Ops made before the board loaded were addressed to a sheet nobody
else has.** Until the load lands, the sheet on screen is the empty one
this client started with. Those sends are held until there is a board to
name — the same holding inbound ops already had, for the same reason from
the other end.

**A repeated sheet id survived being read.** The comment said missing *or
repeated* ids were replaced; the code only replaced missing ones, and the
test named for the case had no duplicate in its fixture. Two sheets with
one id is worse than it sounds: everything here finds a sheet by first
match, so the second is unreachable, takes the first's remote ops, and is
written back for next time.

Also: the tab menu closes when the strip scrolls under it, tabs no longer
shrink to ellipses instead of scrolling, and three test sites wait for
what they assert rather than reading straight after a click.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GJ9tdqJPUktg2XR3M7Ncqr
The scroll half of this landed last commit; the resize half did not, and
the finding named both. A fixed menu placed from a button's box floats
away from it whenever anything moves the button without a pointer going
down — a wheel over the strip, or the window being resized.

The scroll case has a test that fails without the listener. Resize does
not: driving a real window resize through CDP for a listener whose whole
body is `setMenu(null)` costs more than it pins.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GJ9tdqJPUktg2XR3M7Ncqr
@boubou666

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 8, 2026 •

Copy link
Copy Markdown
⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@boubou666

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Aug 8, 2026 •

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/app.js (1)

314-332: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Preserve sheet changes made during hydration.

sheets.load(saved.board) replaces every pre-hydration sheet. The early buffer contains only raw Store operations, so sheet additions, renames, duplicates, removals, and selections are lost.

If a user adds or selects a sheet before loading completes, its object operations are also replayed onto the loaded first sheet because early does not retain a sheet ID.

Buffer sheet lifecycle actions and operation sheet addresses until loading completes, then replay them against the loaded document. Alternatively, disable sheet-changing commands until hydration completes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app.js` around lines 314 - 332, Preserve sheet changes made before
hydration completes: extend the early-action buffering around
sheets.load(saved.board) to retain sheet lifecycle actions and each operation’s
sheet ID, then replay them against the loaded document in order before applying
remaining early operations. Ensure additions, renames, duplicates, removals, and
selections target their original sheets rather than the loaded first sheet.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/core/store.js`:
- Around line 249-264: Update Store.restore to represent state restoration as a
single non-broadcast transaction routed through apply(ops), including a correct
inverse for the replaced objects, order, past, and future state. Preserve the
existing restore notification behavior and ensure restore does not trigger
persistence or broadcasting; keep load using restore with empty history.

In `@src/platform/sync.js`:
- Around line 242-254: In src/platform/sync.js lines 242-254, update release to
replay every pending local batch with sheet:null into the hydrated active sheet
through a non-outbound path before calling flush(), while preserving batch
ordering and existing stopped handling. In test/node/sync-hydration.test.js
lines 228-248, add or update coverage so the initiating store retains the early
operation after hydration and release.

---

Outside diff comments:
In `@src/app.js`:
- Around line 314-332: Preserve sheet changes made before hydration completes:
extend the early-action buffering around sheets.load(saved.board) to retain
sheet lifecycle actions and each operation’s sheet ID, then replay them against
the loaded document in order before applying remaining early operations. Ensure
additions, renames, duplicates, removals, and selections target their original
sheets rather than the loaded first sheet.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 54f9b1f6-714b-40a1-bdf7-d5b81b0ccd88

📥 Commits

Reviewing files that changed from the base of the PR and between 318d07b and 4033705.

📒 Files selected for processing (14)
  • src/app.js
  • src/components/BoardCanvas.jsx
  • src/components/SheetTabs.jsx
  • src/core/autosave.js
  • src/core/sheets.js
  • src/core/store.js
  • src/platform/sync.js
  • src/styles/canvas.css
  • test/browser/sheets.test.js
  • test/node/autosave.test.js
  • test/node/lifecycle.test.js
  • test/node/sheets.test.js
  • test/node/sync-hydration.test.js
  • test/node/sync.test.js

Comment thread src/core/store.js
Comment thread src/platform/sync.js
The test stopped one step short of what `hydrate()` does — it loaded the
board and released the buffer, but left out the replay of the edits made
while the board was in flight. Read literally it said those edits are
lost, and review read it exactly that way.

They are not: `app.js` replays them onto the loaded sheet as REMOTE, and
the queued batch goes out addressed to that same sheet. The test now does
both halves and asserts both — the sender keeps the edit, and it crosses
once rather than twice.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GJ9tdqJPUktg2XR3M7Ncqr

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test/node/sync-hydration.test.js`:
- Around line 252-253: Replace await settle() in the hydration replay test with
the hydration completion promise or explicit replay-ready signal, then call
scheduler.flushTimers() only after hydration replay has completed; remove the
fixed event-loop delay.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 20f5a93c-0e03-44ed-bba9-0c3f283c5817

📥 Commits

Reviewing files that changed from the base of the PR and between 4033705 and 9672f64.

📒 Files selected for processing (1)
  • test/node/sync-hydration.test.js

Comment thread test/node/sync-hydration.test.js
@boubou666
boubou666 merged commit 4f85a59 into main Aug 8, 2026
4 of 5 checks passed
@boubou666
boubou666 deleted the sheets branch August 8, 2026 20:02
boubou666 added a commit that referenced this pull request Aug 9, 2026
Select something, open the menu on the tab you are on, and pick another sheet.
The objects go there — with what their envelopes hold and the arrows between
them, which is the set copying already takes — and you arrive with them,
selected, where the camera is looking.

Until now the only way to get work from one canvas to another was copy, switch,
paste, switch back, delete. Sheets have been in the app since #33 and this is
the hole in them.

**A move is a copy and a delete, made in that order on two sheets.** That is not
a compromise, it is the only shape that fits: a sheet's history travels with the
sheet, which is what makes undo mean "undo what happened on the canvas I am
looking at", so a change spanning two sheets would have to live in one stack and
be invisible from the other. This way each sheet holds its own half — undo where
they arrived takes them away, undo where they were puts them back — and going
with them is what makes that legible rather than strange.

It also means nothing new had to cross the wire. Every op is made while its own
sheet is the one on screen, so they are addressed and broadcast exactly as any
other edit is; writing into a document nobody is looking at would have needed a
word in the sheet vocabulary and a second path through sync, for a feature that
does not need one.

The offer lives in the tab menu of the sheet you are on, one entry per other
sheet, named — that menu is the only one a tab has, and "move selection to
Later" says which sheet without asking anyone to work out what "there" meant. It
is absent with nothing selected, since then it is an offer to move nothing.

`deleteSelected` grew a sibling on the way: `remove(ids)` takes objects and the
arrows that pointed at them, and the selection-shaped version is now one line on
top of it — the move needs the same rule for a set that is not the selection.

1059 tests, 97.2% lines.


Claude-Session: https://claude.ai/code/session_01PTizw7U35rC2MPquwr4Fv5

Co-authored-by: boubou666 <thomas.epitech@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

2 participants