Skip to content

feat(collaboration): portable snapshots and navigable map comments - #1753

Merged
giswqs merged 7 commits into
mainfrom
feat/collaboration-snapshots-comments
Aug 7, 2026
Merged

feat(collaboration): portable snapshots and navigable map comments#1753
giswqs merged 7 commits into
mainfrom
feat/collaboration-snapshots-comments

Conversation

@giswqs

@giswqs giswqs commented Aug 7, 2026

Copy link
Copy Markdown
Member

Summary

  • Collaboration snapshots are now self-contained: control-managed vector data is materialized and the local-file reloadable flag is cleared, so a guest no longer joins to an empty map. A stale async snapshot is discarded if a newer broadcast starts, and the host seeds a snapshot when the relay has none (previously a project loaded before starting collaboration stayed invisible until the host edited something).
  • Plugin activation and settings stay participant-local. Applying a peer's activePluginIds deactivated local controls and let their teardown remove the native layers they own; plugin-created layers still travel as portable GeoJSON. Comments are likewise excluded from projectChanged because the comment-mutation protocol already syncs them.
  • Embedded GeoJSON outgrew the old limits, so the snapshot cap moves from 1 MB to 10 MB (configurable per deployment via COLLAB_MAX_SNAPSHOT_BYTES) and the Cloudflare relay stores the project in a SQLite row instead of a Durable Object key/value entry, with a fallback read for sessions created before that table existed.
  • Guests apply the host's current view on join, follow-host defaults on for joiners, and the session Copy button copies a joinable ?collab= URL rather than a bare code.
  • Comments are reachable from the map: clicking a pin reveals, highlights and scrolls to its card, the panel ships collapsed on the Style rail for discoverability, comments are persisted with the project, and C places a new comment from the command palette. Marker hover scaling moved to a child element so it no longer fights MapLibre's translate transform.

Test plan

  • npm run build
  • npm run test:frontend (5473 pass, 0 fail)
  • npm run test:worker
  • pre-commit run --files <changed paths>
  • Host a session in one browser, join from another with the copied URL, and confirm local GeoJSON and external-plugin layers render for the guest without the host editing first
  • Confirm activating a plugin control on one client does not deactivate it on the other, and that adding a comment does not revert peer layer state
  • Join a session created before this change and confirm the legacy stored snapshot still loads
  • Share a project large enough to exceed 1 MB and confirm it is accepted
  • Confirm the guest lands on the host's viewport immediately after joining
  • Click a map comment pin and confirm the matching card is revealed, highlighted and scrolled into view, including for a resolved comment
  • Press C and confirm the comment placement tool activates
  • Save and reopen a project and confirm comments survive the round trip

Summary by CodeRabbit

  • New Features

    • Added synchronized comment selection between the map and Comments panel, including filtering, scrolling, and visual highlighting.
    • Added toolbar and keyboard shortcuts for creating and posting comments.
    • Comments are enabled by default and collapsed in the side rail.
    • Collaboration now shares portable local vector data and supports host viewport following.
    • Session sharing copies a complete collaboration URL.
    • Increased supported collaboration snapshot size to 10 MB.
  • Bug Fixes

    • Improved marker hover behavior without affecting map positioning.
    • Project exports now retain comments.
    • Collaboration preserves local map and plugin settings during updates.

A guest could join a session and see an empty map: snapshots kept local
file references and control-managed vector data the collaborator cannot
read, so shared layers now embed their features and drop the reloadable
flag. Guests also arrive at the host's viewport instead of waiting for
the host's next move, and the session Copy button yields a joinable URL.

Comments become reachable from the map: clicking a pin reveals, highlights
and scrolls to its card, the panel ships collapsed on the Style rail so it
is discoverable, comments are saved with the project, and "C" places a new
one from the command palette.
Copilot AI lite review requested due to automatic review settings August 7, 2026 00:58

Copilot AI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

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 change updates comment selection and activation, panel registration, portable asynchronous collaboration snapshots, local project-state merging, separate comment synchronization, and configurable SQLite-backed relay storage.

Changes

Comments and collaboration

Layer / File(s) Summary
Comment selection and highlighting
apps/geolibre-desktop/src/components/comments/CommentMapOverlay.tsx, apps/geolibre-desktop/src/components/comments/CommentThread.tsx, apps/geolibre-desktop/src/components/comments/CommentsPanel.tsx, apps/geolibre-desktop/src/components/layout/DesktopShell.tsx
Map-selected comments open the panel, select the matching card, switch filters, scroll into view, and apply selected styling. Hover transforms apply to a child marker element.
Comment activation and panel registration
apps/geolibre-desktop/src/components/layout/TopToolbar.tsx, apps/geolibre-desktop/src/components/layout/DesktopShell.tsx, apps/geolibre-desktop/src/hooks/useRegisterCommentsPanel.ts, apps/geolibre-desktop/src/components/comments/AddCommentDialog.tsx, apps/geolibre-desktop/src/components/comments/CommentsPanel.tsx, apps/geolibre-desktop/src/components/layout/SettingsDialog.tsx, apps/geolibre-desktop/src/i18n/locales/en.json
The toolbar adds the add.comment command and C shortcut. Comment posting supports Command/Ctrl+Enter. The Comments panel registers as enabled and collapsed on the configured rail. Session copying uses a collaboration URL.
Portable collaboration snapshots
apps/geolibre-desktop/src/lib/collaboration-layers.ts, apps/geolibre-desktop/src/lib/build-project-snapshot.ts, apps/geolibre-desktop/src/hooks/useProjectFileActions.ts, tests/collaboration-snapshot.test.ts
Snapshots embed materialized local vectors, remove reload metadata, preserve comments, reuse unchanged layers, and cover these cases with tests.
Asynchronous collaboration synchronization
apps/geolibre-desktop/src/hooks/useCollaboration.ts, apps/geolibre-desktop/src/lib/collaboration-project.ts, apps/geolibre-desktop/src/lib/project-broadcast-changed.ts, tests/collaboration-project.test.ts, tests/collaboration-project-changed.test.ts
Snapshot sending is asynchronous and rejects stale results. Inbound projects preserve local view and plugin state. Comments and plugin settings no longer trigger project snapshot broadcasts. Hosts seed empty sessions, and guests follow the host on join.
Relay snapshot storage and limits
workers/collab/src/session.ts, packages/collab-core/src/session.ts, docs/collaboration.md
The default snapshot limit increases to 10 MB. Relay storage uses SQLite with legacy KV fallback and migration. Deployments can configure the limit with COLLAB_MAX_SNAPSHOT_BYTES.

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

Sequence Diagram(s)

sequenceDiagram
  participant CommentMapOverlay
  participant DesktopShell
  participant CommentsPanel
  participant CommentThread
  CommentMapOverlay->>DesktopShell: select comment ID
  DesktopShell->>CommentsPanel: pass selectedCommentId
  CommentsPanel->>CommentsPanel: reveal filter and scroll card
  CommentsPanel->>CommentThread: pass selected state
Loading
sequenceDiagram
  participant useCollaboration
  participant buildCollaborationSnapshot
  participant CollaborationRelay
  participant LocalProject
  useCollaboration->>buildCollaborationSnapshot: build asynchronous snapshot
  buildCollaborationSnapshot->>CollaborationRelay: send portable redacted snapshot
  CollaborationRelay-->>useCollaboration: return inbound project
  useCollaboration->>LocalProject: merge local view and plugin state
Loading

Possibly related PRs

Suggested reviewers: harshshinde0

Poem

A rabbit taps C; comments appear,
Map pins stay fixed as cards draw near.
Local vectors hop into snapshots bright,
Hosts share views with guests in flight.
Stale requests fade from sight.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the two main changes: portable collaboration snapshots and navigable map comments.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/collaboration-snapshots-comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

🔍 Cloudflare PR preview

Item Value
Site https://a60b50a9.geolibre-preview.pages.dev
Demo app https://a60b50a9.geolibre-preview.pages.dev/demo/
Commit 97cdc7b

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 3

🤖 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 `@apps/geolibre-desktop/src/components/comments/CommentsPanel.tsx`:
- Around line 343-345: Replace the literal tooltip in the session-copy control
using the component’s react-i18next t() function, add the corresponding English
source entry to src/i18n/locales/en.json, and preserve the existing “Copy
session URL” text as the translation value.

In `@apps/geolibre-desktop/src/hooks/useCollaboration.ts`:
- Around line 81-87: Update sendSnapshot to catch rejections from
buildCollaborationSnapshot, ignoring failures from stale requests identified by
snapshotRequestRef.current. For the active request, preserve the existing
edit/pause checks and report the construction failure through the collaboration
state error mechanism instead of allowing an unhandled rejection.

In `@apps/geolibre-desktop/src/lib/build-project-snapshot.ts`:
- Around line 73-79: Update the snapshot-building flow around state,
materializeEmbeddableVectorLayers, and prepareCollaborationLayers to capture the
layer array once and verify the store still contains that same revision after
materialization. If the layers changed during the await, retry or discard the
build rather than combining materialized data with newer layers; ensure the
resulting snapshot always uses one consistent layer revision.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: ab64987f-4875-4c53-b9f3-46d2a799d4b3

📥 Commits

Reviewing files that changed from the base of the PR and between ebeea83 and 5f70285.

📒 Files selected for processing (12)
  • apps/geolibre-desktop/src/components/comments/CommentMapOverlay.tsx
  • apps/geolibre-desktop/src/components/comments/CommentThread.tsx
  • apps/geolibre-desktop/src/components/comments/CommentsPanel.tsx
  • apps/geolibre-desktop/src/components/layout/DesktopShell.tsx
  • apps/geolibre-desktop/src/components/layout/SettingsDialog.tsx
  • apps/geolibre-desktop/src/components/layout/TopToolbar.tsx
  • apps/geolibre-desktop/src/hooks/useCollaboration.ts
  • apps/geolibre-desktop/src/hooks/useProjectFileActions.ts
  • apps/geolibre-desktop/src/hooks/useRegisterCommentsPanel.ts
  • apps/geolibre-desktop/src/lib/build-project-snapshot.ts
  • apps/geolibre-desktop/src/lib/collaboration-layers.ts
  • tests/collaboration-snapshot.test.ts
💤 Files with no reviewable changes (1)
  • apps/geolibre-desktop/src/components/layout/SettingsDialog.tsx

Comment thread apps/geolibre-desktop/src/components/comments/CommentsPanel.tsx Outdated
Comment thread apps/geolibre-desktop/src/hooks/useCollaboration.ts
Comment thread apps/geolibre-desktop/src/lib/build-project-snapshot.ts Outdated
Comment thread apps/geolibre-desktop/src/lib/build-project-snapshot.ts Outdated
Comment thread apps/geolibre-desktop/src/lib/build-project-snapshot.ts Outdated
Comment thread apps/geolibre-desktop/src/hooks/useCollaboration.ts Outdated
Comment thread apps/geolibre-desktop/src/components/layout/SettingsDialog.tsx
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

All four inline comments posted successfully.

Code review

Bugs

  • buildCollaborationSnapshot computes materialized from the state.layers captured before its awaits, but builds layers from a fresh useAppStore.getState().layers read afterward. A desktop path-backed vector layer added during that window won't have a materialized entry, yet prepareCollaborationLayers still strips its localFileReloadable flag — sending collaborators a layer that looks portable but has no embedded data. Medium confidence (apps/geolibre-desktop/src/lib/build-project-snapshot.ts:77-78).
  • SettingsDialog.tsx's toggleCommentsPanel dropped the collapseRightPanel(COMMENTS_PANEL_ID) call that its sibling toggleBrowserPanel still has, so re-enabling "Show Comments Panel" from Settings now fully expands the panel over the map instead of leaving it collapsed like Browser and like Comments' own default-mount behavior. Medium confidence (apps/geolibre-desktop/src/components/layout/SettingsDialog.tsx:429-435).

Performance

  • sendSnapshot re-runs buildCollaborationSnapshot on every debounced project change, which re-materializes (DuckDB query) every embeddable control-managed vector layer in the project regardless of which field actually changed — no caching keyed by layer data revision, so an unrelated edit repeatedly re-embeds and re-broadcasts the same heavy GeoJSON. Medium-low confidence (apps/geolibre-desktop/src/hooks/useCollaboration.ts:84).

Quality

  • buildCollaborationSnapshot's "portable" rebuild branch duplicates the same ~20-field projectFromStore call already present in buildProjectSnapshot and in useProjectFileActions.ts's buildCurrentProject; this PR remembered to add comments to all three, but future field additions could silently miss one. Low confidence (apps/geolibre-desktop/src/lib/build-project-snapshot.ts:85-109).

Security

  • Nothing found. CommentMapOverlay's pin rendering continues to set author color via style.backgroundColor rather than interpolating into markup, and the new CommentsPanel.handleCopySessionUrl mirrors an already-shipped pattern in CollaborateDialog.tsx (full-URL + ?collab= param), so it isn't a new exposure.

CLAUDE.md

  • No violations spotted: no new external tile/map hosts, no touched WASM-version mirrors, i18n strings use t(), and logical Tailwind utilities (ms-1) are used for the new keyboard-shortcut badge.

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

🔍 GitHub Pages PR preview

Item Value
Site Deploy failed. See the job log.
Demo app Unavailable
Commit 97cdc7b

A guest joining a brand new session saw nothing until the host happened to
edit: the store subscription only observes changes made after attach, so a
project loaded before starting collaboration was never broadcast. The host
now seeds a snapshot when the relay has none.

Plugin activation and settings are participant-local. Applying a peer's
activePluginIds deactivated local controls and let their teardown remove
the native layers they own, so inbound snapshots keep the local plugin
state; plugin-created layers still travel as portable GeoJSON. Comments
stay out of projectChanged because the comment-mutation protocol already
syncs them, and answering one with a whole-project snapshot let a comment
overwrite unrelated layer state.

Embedding that portable GeoJSON outgrew both the 1 MB snapshot cap (now
10 MB, configurable per deployment via COLLAB_MAX_SNAPSHOT_BYTES) and the
Durable Object 2 MiB key/value entry limit, so the Cloudflare relay stores
the project in a SQLite row and falls back to the legacy key for sessions
created before it.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (1)
apps/geolibre-desktop/src/hooks/useCollaboration.ts (1)

146-162: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Send the initial host presence after join.

attach() calls bindPresence() before it sends join. bindPresence() immediately sends a presence frame. The relay rejects that frame because the socket has no join attachment yet.

As a result, message.presence has no host view during welcome. Lines 158-162 then leave the guest at its local viewport until the host moves.

Proposed fix
-    const map = mapControllerRef.current?.getMap() ?? null;
-    const detachMap = map ? bindPresence(map, conn) : () => {};
-
     conn.send({
       type: "join",
       clientId: selfIdRef.current ?? crypto.randomUUID(),
       displayName,
       color,
       hostToken,
     });
+
+    const map = mapControllerRef.current?.getMap() ?? null;
+    const detachMap = map ? bindPresence(map, conn) : () => {};
🤖 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 `@apps/geolibre-desktop/src/hooks/useCollaboration.ts` around lines 146 - 162,
The collaboration flow must send the host’s initial presence only after the
guest has joined and the relay attachment exists. Update the join/welcome
handling around attach(), bindPresence(), and the message.role === "guest"
host-view application so the host presence is sent after join rather than during
premature bindPresence(), allowing message.presence to contain the host view
immediately.
🤖 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 `@tests/collaboration-project.test.ts`:
- Around line 10-14: Update both fixtures in tests/collaboration-project.test.ts
(lines 10-14) and tests/collaboration-project-changed.test.ts (lines 88-91) to
include the required ProjectPluginState fields mapControlPositions and
activePluginIds; add settings to the second fixture as well, using empty values
where those tests do not depend on them.

---

Outside diff comments:
In `@apps/geolibre-desktop/src/hooks/useCollaboration.ts`:
- Around line 146-162: The collaboration flow must send the host’s initial
presence only after the guest has joined and the relay attachment exists. Update
the join/welcome handling around attach(), bindPresence(), and the message.role
=== "guest" host-view application so the host presence is sent after join rather
than during premature bindPresence(), allowing message.presence to contain the
host view immediately.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 9dd24599-6413-4e0c-99cf-29d0ab10d514

📥 Commits

Reviewing files that changed from the base of the PR and between 5f70285 and 38a378f.

📒 Files selected for processing (8)
  • apps/geolibre-desktop/src/hooks/useCollaboration.ts
  • apps/geolibre-desktop/src/lib/collaboration-project.ts
  • apps/geolibre-desktop/src/lib/project-broadcast-changed.ts
  • docs/collaboration.md
  • packages/collab-core/src/session.ts
  • tests/collaboration-project-changed.test.ts
  • tests/collaboration-project.test.ts
  • workers/collab/src/session.ts

Comment thread tests/collaboration-project.test.ts
giswqs added 2 commits August 6, 2026 21:48
- buildCollaborationSnapshot: read the layer array once and feed it to both
  materialization and prepareCollaborationLayers, so a layer added during the
  await can no longer lose localFileReloadable without gaining features.
- buildProjectSnapshot takes an optional layers override, removing the second
  copy of the ~20-field projectFromStore call a new project field could miss.
- sendSnapshot catches a failed snapshot build and reports it through
  collaboration state instead of leaving an unhandled rejection.
- SettingsDialog: restore collapseRightPanel when re-enabling the Comments
  panel, matching the Browser toggle and the panel's own mount state.
- Localize the session copy tooltip via the existing collaborate.copyLink key.
- Complete the ProjectPluginState fixtures in the collaboration tests: both
  were missing mapControlPositions, and the projectChanged fixture was also
  missing activePluginIds and settings.

@coderabbitai coderabbitai Bot left a comment

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.

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 `@apps/geolibre-desktop/src/hooks/useCollaboration.ts`:
- Around line 93-95: Update the error-setting block for the current snapshot
request to set collaborate.shareFailed only when !syncPausedRef.current and
canEdit() are both true, preserving the size-specific relay error after
synchronization is paused.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: d7e271b6-f5ad-4b3d-9024-213f0e512100

📥 Commits

Reviewing files that changed from the base of the PR and between 38a378f and eb95447.

📒 Files selected for processing (5)
  • apps/geolibre-desktop/src/components/comments/CommentsPanel.tsx
  • apps/geolibre-desktop/src/components/layout/SettingsDialog.tsx
  • apps/geolibre-desktop/src/hooks/useCollaboration.ts
  • apps/geolibre-desktop/src/i18n/locales/en.json
  • apps/geolibre-desktop/src/lib/build-project-snapshot.ts

Comment thread apps/geolibre-desktop/src/hooks/useCollaboration.ts Outdated
Comment thread apps/geolibre-desktop/src/lib/build-project-snapshot.ts Outdated
// portable and arrives empty. A layer added after this read is simply absent
// from this snapshot; the store change that added it schedules the next one.
const source = useAppStore.getState().layers;
const materialized = await materializeEmbeddableVectorLayers(source);

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.

Every debounced snapshot re-materializes all embeddable local vector layers, even when the change that triggered the broadcast is unrelated.

materializeEmbeddableVectorLayers re-queries DuckDB (control.getLayerGeoJSON) for every embeddable local/control-managed vector layer on every call to buildCollaborationSnapshot. Since the prepared/portable layers computed here are never written back into the store (useAppStore's layers stays the original, non-portable version), the next store change — even something as unrelated as basemapOpacity or legend — schedules another sendSnapshot() that redoes the full materialization from scratch for every such layer.

With the snapshot cap now raised from 1 MB to 10 MB (packages/collab-core/src/session.ts), this could mean repeatedly re-serializing multi-MB feature collections from DuckDB on every minor, unrelated project edit while a session with embedded local data is active.

Confidence: medium-high — clear from the code structure; actual impact depends on how large/frequent the embedded local datasets are in practice.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Not changing this, leaving the thread open for a maintainer call (same concern as the earlier thread on useCollaboration.ts).

The cost is real but a cache here trades a correctness property for it. materializeEmbeddableVectorLayers only touches layers matching isEmbeddableLocalVectorLayer, so a project without local/control-managed vector layers already pays nothing. When such layers do exist, the control owns their features and can mutate them without writing to the store, so there is no store-visible revision to key a memo on. Keying on the layers array identity would mean an unrelated edit broadcasts the features as they were at the last layer-list change rather than as they are now — silently shipping stale geometry, which is worse than the redundant read.

The fix worth having is a data revision counter on the vector control, which getLayerGeoJSON callers (save and export, not just collaboration) could all use. That belongs in its own change.

Posting a comment meant leaving the keyboard for the mouse, and the form
offered no hint that a shortcut existed. The button now carries the
platform-correct label and aria-keyshortcuts alongside the handler.
Comment thread packages/collab-core/src/session.ts
Comment thread apps/geolibre-desktop/src/components/layout/TopToolbar.tsx

@coderabbitai coderabbitai Bot left a comment

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.

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 `@apps/geolibre-desktop/src/components/comments/AddCommentDialog.tsx`:
- Around line 15-17: Update the POST_COMMENT_SHORTCUT definition to explicitly
set shift: false, ensuring matchesShortcut accepts only the advertised unshifted
Ctrl+Enter or Meta+Enter combination and rejects Shift-modified variants.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: dc3c8106-ebe0-4895-8fe1-739130ea0f03

📥 Commits

Reviewing files that changed from the base of the PR and between a6cae3d and f92c487.

📒 Files selected for processing (2)
  • apps/geolibre-desktop/src/components/comments/AddCommentDialog.tsx
  • apps/geolibre-desktop/src/i18n/locales/en.json

Comment thread apps/geolibre-desktop/src/components/comments/AddCommentDialog.tsx Outdated
giswqs added 2 commits August 6, 2026 22:05
- Store a hosted snapshot in chunks across SQLite rows. A Durable Object caps
  a SQLite string at 2 MB just as it caps a key/value entry, so the one-row
  table did not actually admit the 10 MB the cap now allows.
- buildCollaborationSnapshot re-reads and re-materializes (bounded) when an
  edit lands mid-read, so layerGroups and selectedLayerId can no longer name a
  layer the broadcast does not carry.
- sendSnapshot reports a build failure only while the request may still
  broadcast, so it cannot overwrite a relay error that paused sync.
- Correct the snapshot-cap comments and docs: 32 MiB is the received-message
  ceiling, and note the separate 2 MB storage bound.
- Update the viewer-mode shortcut comment, which no longer held once
  add.comment started carrying a shortcut.
- Set shift: false on POST_COMMENT_SHORTCUT. An omitted shift means "ignored"
  in matchesShortcut, so the dialog also posted on Ctrl/Cmd+Shift+Enter, a
  chord the button never advertises.
@giswqs
giswqs merged commit ca6a5fc into main Aug 7, 2026
26 checks passed
@giswqs
giswqs deleted the feat/collaboration-snapshots-comments branch August 7, 2026 02:12
this.ctx.storage.sql.exec(
"INSERT INTO collab_snapshot_chunks (seq, value) VALUES (?, ?)",
seq,
snapshot.slice(offset, offset + SNAPSHOT_CHUNK_CHARS),

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.

Bug (medium confidence): snapshot.slice(offset, offset + SNAPSHOT_CHUNK_CHARS) splits the JSON string on raw UTF-16 code-unit boundaries. If a chunk boundary happens to fall between the two halves of a surrogate pair (e.g. an emoji in a comment author name, layer name, or an embedded GeoJSON property — plausible now that whole vector datasets get embedded), each half becomes a lone surrogate in its own chunk. Storing that as SQLite TEXT typically forces UTF-8 re-encoding of each chunk independently, which replaces a lone surrogate with U+FFFD — silently corrupting that character when the chunks are rejoined on read (readSqlSnapshot's .join("")). This only bites snapshots over 256KB (i.e. once chunking actually kicks in), so it'd be easy to miss in normal testing.

A boundary-safe split (e.g. nudging the slice point to avoid landing on a high surrogate) would avoid this.

Comment on lines +402 to +403
await this.writeSnapshot(JSON.stringify(project));
await this.ctx.storage.put("rev", rev);

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.

Reliability (low-medium confidence): this replaces what used to be a single atomic storage.put({ snapshot, rev }) with two separately-awaited operations — writeSnapshot (which itself ends in await this.ctx.storage.delete("snapshot")) followed by await this.ctx.storage.put("rev", rev). If the Durable Object is evicted or throws between these awaited steps, the stored snapshot content and the rev counter can end up out of sync (new content persisted with a stale rev, or vice versa). The within-writeSnapshot chunk delete+insert itself is fine (no await between those calls, so Cloudflare's SQLite storage coalesces them into one transaction), but the boundary between writeSnapshot and the rev put is a real gap. Worth wrapping both in this.ctx.storage.transaction() (or an explicit storage.sql transaction) if that guarantee still matters here.

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Code review

Bugs

  • workers/collab/src/session.ts:154writeSnapshot's chunk slice() cuts the JSON string on raw UTF-16 code-unit boundaries. If a boundary lands inside a surrogate pair (an emoji anywhere in the payload — plausible once whole vector datasets get embedded), the two chunks each carry a lone surrogate, which can get silently mangled to U+FFFD on UTF-8 storage and rejoin. Only affects snapshots over 256KB. Medium confidence.
  • workers/collab/src/session.ts:402-403handleSnapshot used to persist snapshot and rev in one atomic storage.put({...}); now it's writeSnapshot() (ending in its own await storage.delete("snapshot")) followed by a separate await storage.put("rev", rev). A crash/eviction between those awaits can leave content and rev out of sync. Low-medium confidence, narrow window.

Security

  • Nothing found. Color/text values into the DOM (CommentMapOverlay) continue to go through safe DOM APIs rather than markup interpolation; SQL chunk inserts are parameterized; the new COLLAB_MAX_SNAPSHOT_BYTES env value is validated with Number.isSafeInteger before use.

Performance

  • readSnapshot()/writeSnapshot() both call ensureSnapshotTable() (a CREATE TABLE IF NOT EXISTS) on every invocation, including every snapshot read on join. Cheap individually but avoidable per-message overhead. Low confidence/severity, not flagged inline.

Quality

  • The new chunked-storage logic in workers/collab/src/session.ts (table creation, chunking, legacy-key fallback) has no unit test coverage — npm run test:worker only typechecks geolibre-collab-worker, it doesn't run tests against it. This extends an already-untested file with meaningfully more logic. Not blocking (pre-existing gap in that worker's test setup), noted for awareness rather than as an inline comment.
  • I initially suspected a dedup mismatch between buildCollaborationSnapshot (used to build outgoing snapshots) and buildProjectEgressSnapshot (used to reset lastContentRef after applying an inbound one) in useCollaboration.ts, since the former re-materializes control-managed vector layers and the latter doesn't. On closer tracing this turned out to be a non-issue: the materialized embeddedGeoJSON is already baked into the applied store state by the time either path reads it, so both pipelines converge on the same serialized content. No finding there.

CLAUDE.md

  • No violations found. New user-facing strings go through t()/en.json; the RTL-safe ms-/me- utilities are used for the new kbd hints; the Tauri CSP allowlist and other mirrored constants (MAX_VECTOR_PMTILES_ZOOM, etc.) are untouched by this PR and don't need updates here.

@mnv74ftr5s-blip mnv74ftr5s-blip 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.

Me

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.

3 participants