Skip to content

feat: mesh collaboration via iroh and loro#38

Open
adhi-thirumala wants to merge 47 commits into
mainfrom
fable-collab
Open

feat: mesh collaboration via iroh and loro#38
adhi-thirumala wants to merge 47 commits into
mainfrom
fable-collab

Conversation

@adhi-thirumala

@adhi-thirumala adhi-thirumala commented Jun 13, 2026

Copy link
Copy Markdown
Collaborator
  • created plan.md
  • plan v2
  • add p2p collaboration
  • add collaboration presence status
  • finished off first pass plan implemnentation
  • add collaboration presence status
  • finished off first pass plan implemnentation

Summary by CodeRabbit

  • New Features

    • P2P collaborative editing with invite tickets, session join/leave flows, and real-time syncing
    • Live presence roster, external cursors, and deterministic peer colors
    • Direct transport + gossip networking with anti-entropy catch-up and missing-asset pulls (with loading placeholders)
  • UI/UX Enhancements

    • Collaborate top bar/dropdowns, Share/Join dialog, and collaboration status badge/pill
    • Leave-session prompts integrated into window/panel close flows
    • More consistent selection-change updates across mouse/keyboard/IME and undo/redo
  • Bug Fixes

    • Protocol-version mismatch notices; refined image placeholder rendering and stable asset hashing
  • Tests & Documentation

    • Added convergence, anti-entropy, and swarm loopback tests; updated collaboration docs and QA checklists

@coderabbitai

coderabbitai Bot commented Jun 13, 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

This PR adds a new collaboration crate, session/runtime plumbing, editor collaboration patching, workspace collaboration UI, and supporting tests/docs.

Changes

P2P collaboration

Layer / File(s) Summary
Bootstrap and public surface
CLAUDE.md, Cargo.toml, crates/flowstate-collab/Cargo.toml, crates/flowstate/src/{lib.rs,commands.rs,commands/keymap.rs,rich_text_element/mod.rs}, crates/flowstate/src/collab/{mod.rs,share_dialog.rs,share_dialog_view.rs,status.rs,shutdown.rs}, crates/flowstate/src/workspace/workspace/{mod.rs,render_documents.rs,render_status.rs,render_top_bar.rs,top_bar.rs,traits.rs,keybindings.rs,documents.rs,window.rs}, crates/flowstate/src/workspace/icons.rs, crates/gpui-flowtext/Cargo.toml, crates/gpui-flowtext/src/{collaboration.rs,rich_text/tests/mod.rs}, helpers/docs/collab_qa.md, plan.md, fix.md
Adds the crate/module surface, dependencies, command identifiers, collaboration dialog/status UI, workspace command routing, close handling, and planning/QA/test wiring.
Schema, bindings, and projection
crates/flowstate-collab/src/{ids.rs,lib.rs,binding.rs,local_apply.rs,projection.rs,schema.rs,patch_apply.rs,remote_apply.rs,self_check.rs,ticket.rs}, crates/flowstate-collab/tests/{local_apply.rs,projection.rs,translation.rs,remote_apply.rs,convergence.rs}
Defines collaboration identifiers, schema and projection helpers, binding state, local and remote document translation, patch replay, self-check hashing, ticket encoding, and the related translation/projection/convergence tests.
Network runtime, swarm, and direct transport
Cargo.toml, crates/flowstate-collab/src/net/{mod.rs,runtime.rs,swarm.rs,direct.rs,anti_entropy.rs,blobs.rs}, crates/flowstate-collab/tests/{anti_entropy.rs,swarm_loopback.rs}
Adds the gossip/direct transport stack, runtime startup, anti-entropy, blob outbox handling, and network loopback/anti-entropy tests.
Session runtime and workspace flow completion
crates/flowstate/src/collab/{asset_transfer.rs,manager.rs,mod.rs,notify.rs,pump.rs,presence_view.rs,session.rs,session_io.rs,session_presence.rs,session_timers.rs}, crates/flowstate/src/workspace/workspace/{collab.rs,collab_prompts.rs,documents.rs,render_status.rs}, crates/gpui-flowtext/src/{document/blocks.rs,persistence/{records.rs,validation.rs},rich_text/element.rs,rich_text/editor/{block_insertion.rs,caret_movement.rs,collab_apply.rs,commands.rs,edit_pipeline.rs,lifecycle.rs,media.rs,mod.rs,mouse.rs,movement_core.rs,object_assets.rs,object_selection.rs,paste.rs,platform.rs,render_blocks.rs,search_highlights.rs,serialization.rs,tables.rs,table_equation_editing.rs},rich_text/tests/{collab_capture.rs,edit_layout.rs,persistence_blocks.rs}}
Connects sessions to networking, presence, asset pulls, recovery/self-check timers, editor capture and patch application, IME and selection handling, placeholder image behavior, and collaboration-aware document/window teardown.

Possibly related PRs

Poem

🐇 I hopped through schemas, tracks, and streams,
And stitched together many dreamful teams.
With tickets, tabs, and burrowed light,
The shared text blooms through day and night.
A carrot-toast to every peer in sight 🥕

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fable-collab

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

@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: 18

Caution

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

⚠️ Outside diff range comments (2)
crates/gpui-flowtext/src/rich_text/editor/mouse.rs (1)

278-304: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Capture the drag/drop canonical span from the pre-edit document and include the source-side deletion.

The ReplaceParagraphSpan here is built after the delete+insert has already run, so its before payload is reading post-move content. It also only spans the drop-side paragraph range, which misses the source-side deletion when the drag crosses paragraphs. That means the collaboration op no longer matches the local edit and can replay incorrectly on peers.

🤖 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 `@crates/gpui-flowtext/src/rich_text/editor/mouse.rs` around lines 278 - 304,
Build the `ReplaceParagraphSpan` canonical op from the pre-edit state in
`mouse.rs` so it reflects the drag/drop before any mutation is applied; capture
the source-side deleted paragraph span as part of the same canonical operation,
not just the inserted drop range. Update the
`EditRecord`/`mark_document_changed_with_ops` path around `canonical_operations`
so the payload uses the pre-delete document contents and includes both source
and destination paragraph ranges when the drag crosses paragraphs.
crates/gpui-flowtext/src/rich_text/editor/object_selection.rs (1)

492-500: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't emit a fake BlockId(0) for deleted-block collaboration ops.

The deleted block id is resolved after the block/id vectors have already been mutated, and a miss is silently rewritten to BlockId(0). In collaboration mode that can delete the wrong remote block instead of describing the local edit. Capture the block id before removal, and if it is unavailable, fall back to a broader op like ReplaceDocument rather than fabricating an id.

Also applies to: 513-513

🤖 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 `@crates/gpui-flowtext/src/rich_text/editor/object_selection.rs` around lines
492 - 500, The block identifier is being resolved after the block and id vectors
have been mutated, causing the lookup to fail and fall back to a fabricated
BlockId(0), which can delete the wrong remote block in collaboration mode.
Capture the block identifier by calling self.identity_map.block_id(block_ix)
BEFORE removing the block from the vectors using blocks.remove() and
remove_block_ids(). Use the captured block id in the CanonicalOperation for
DeleteBlock. If the block id cannot be captured (when the lookup fails), replace
the DeleteBlock operation with a broader operation like ReplaceDocument instead
of using a fake BlockId(0). Apply this same fix to all locations where blocks
are deleted and canonical operations are created.
🧹 Nitpick comments (11)
crates/flowstate-collab/src/proto_direct.rs (1)

31-38: ⚡ Quick win

Simplify redundant try_from after length check.

Line 33 already ensures payload.len() <= MAX_FRAME_LEN (2 MiB), which is well below u32::MAX. The try_from on line 34 can never fail, making the ? unreachable.

♻️ Suggested simplification
   let payload = postcard::to_stdvec(&(PROTOCOL_VERSION, value))?;
   ensure!(payload.len() <= MAX_FRAME_LEN, "direct frame exceeds {MAX_FRAME_LEN} bytes");
-  let len = u32::try_from(payload.len())?;
+  let len = payload.len() as u32;
   let mut frame = len.to_le_bytes().to_vec();
🤖 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 `@crates/flowstate-collab/src/proto_direct.rs` around lines 31 - 38, Simplify
encode_frame in proto_direct.rs by removing the redundant
u32::try_from(payload.len())? conversion after the MAX_FRAME_LEN check; since
payload.len() is already bounded below u32::MAX, compute the length directly as
a u32 and keep the rest of the frame assembly unchanged.
crates/flowstate-collab/src/net/swarm.rs (1)

137-162: ⚡ Quick win

Consider structured logging instead of eprintln! for production.

Line 149 uses eprintln! to report gossip decode failures. For production deployments, replace this with a proper logging framework (e.g., tracing::warn!) to enable configurable log levels and structured output.

🤖 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 `@crates/flowstate-collab/src/net/swarm.rs` around lines 137 - 162, Replace the
eprintln! call in the handle_event function's error handling branch (where
proto_gossip::decode fails) with a structured logging call using tracing::warn!.
This will allow production deployments to configure log levels and get
structured output instead of relying on stderr output, enabling better
observability and log aggregation.
crates/flowstate/src/collab/session.rs (1)

592-595: ⚡ Quick win

Extract undo manager configuration to named constants or configuration.

The hardcoded values merge_interval(500) and max_undo_steps(300) are magic numbers. Consider extracting them to named constants or making them configurable for better maintainability.

♻️ Proposed refactor with named constants
+const COLLAB_UNDO_MERGE_INTERVAL_MS: u64 = 500;
+const COLLAB_MAX_UNDO_STEPS: usize = 300;
+
 fn attach_undo_manager(&mut self) {
   if self.undo_manager.is_some() {
     return;
   }
   let Some(doc) = &self.doc else {
     return;
   };
   let mut undo_manager = UndoManager::new(doc);
-  undo_manager.set_merge_interval(500);
-  undo_manager.set_max_undo_steps(300);
+  undo_manager.set_merge_interval(COLLAB_UNDO_MERGE_INTERVAL_MS);
+  undo_manager.set_max_undo_steps(COLLAB_MAX_UNDO_STEPS);
   self.undo_manager = Some(undo_manager);
 }
🤖 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 `@crates/flowstate/src/collab/session.rs` around lines 592 - 595, The
UndoManager setup in session.rs uses magic numbers for merge_interval and
max_undo_steps; extract the 500 and 300 values into named constants or a
session/undo configuration near UndoManager::new, set_merge_interval, and
set_max_undo_steps so the defaults are easy to find and adjust.
crates/flowstate-collab/src/ticket.rs (1)

46-48: 💤 Low value

Consider handling serialization failure gracefully instead of panicking.

While postcard serialization of this simple struct is unlikely to fail, using expect() introduces a potential panic point. Consider returning an error or documenting why this can never fail.

♻️ Alternative approach returning an error

If the Ticket trait allows it, consider changing the signature to return Result:

fn encode_bytes(&self) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
  Ok(postcard::to_stdvec(self)?)
}

However, if the trait requires Vec<u8>, the current approach with a clear panic message is acceptable.

🤖 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 `@crates/flowstate-collab/src/ticket.rs` around lines 46 - 48, The encode_bytes
method in the Ticket trait implementation uses expect() which can panic on
serialization failure. First, check the Ticket trait definition to see if the
return type can be changed from Vec<u8> to a Result type. If the trait signature
is flexible, update encode_bytes to return Result<Vec<u8>, Box<dyn
std::error::Error>> and use the ? operator instead of expect(). If the trait
signature is fixed and requires Vec<u8>, then keep the current approach but add
a doc comment above the method explaining why postcard serialization of
SessionTicket cannot fail and the panic is justified as a last resort safeguard.
crates/flowstate/src/collab/session_presence.rs (1)

9-11: 💤 Low value

Consider using a structured logging framework instead of eprintln!.

The code uses eprintln! for error messages (lines 10, 28). For better observability and consistency, consider using a structured logging framework like tracing or log. This would allow filtering, formatting, and routing of log messages in production.

Also applies to: 27-29

🤖 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 `@crates/flowstate/src/collab/session_presence.rs` around lines 9 - 11, The
code currently uses eprintln! macro for logging error messages when presence
updates fail. Replace the eprintln! calls with a structured logging framework
such as tracing or log to enable better observability, filtering, and
production-grade log management. Update all occurrences where eprintln! is used
for the flowstate collab presence update error message (in the error handling
block for presence.apply() and any similar error logging) to use the appropriate
logging macro from your chosen framework instead, ensuring consistent structured
logging throughout the module.
crates/flowstate-collab/src/proto_gossip.rs (2)

7-7: Move DIRECT_ALPN to proto_direct.rs module.

The DIRECT_ALPN constant is used exclusively in direct protocol code (net/direct.rs and net/runtime.rs) and semantically belongs with other direct protocol constants. Currently it's defined in proto_gossip.rs alongside PROTOCOL_VERSION and GOSSIP_INLINE_LIMIT, but it has no relation to the gossip protocol. Move it to proto_direct.rs where it should be grouped with MAX_FRAME_LEN and MAX_PAYLOAD_CHUNK_LEN for better code organization.

🤖 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 `@crates/flowstate-collab/src/proto_gossip.rs` at line 7, Move the DIRECT_ALPN
constant out of proto_gossip.rs and into proto_direct.rs so it lives with the
other direct-protocol constants like MAX_FRAME_LEN and MAX_PAYLOAD_CHUNK_LEN.
Update any direct-protocol code that references DIRECT_ALPN, especially
net/direct.rs and net/runtime.rs, to import it from proto_direct.rs instead of
proto_gossip.rs, and leave proto_gossip.rs containing only gossip-related
constants such as PROTOCOL_VERSION and GOSSIP_INLINE_LIMIT.

30-32: Consider optimizing encoded_len to avoid allocating the full message buffer.

The function calls encode(msg)? which allocates and serializes the entire message using postcard just to measure its length. Since there's only one call site and the message size is bounded by GOSSIP_INLINE_LIMIT (2KB), the practical impact is minimal. However, if this function is called frequently in future code paths, consider implementing a size-counting approach or accepting the allocation as acceptable for this use case.

🤖 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 `@crates/flowstate-collab/src/proto_gossip.rs` around lines 30 - 32, The
encoded_len function allocates the full message buffer by calling encode(msg)?
just to measure its length, which is inefficient even though the practical
impact is minimal with the current 2KB size bound. Either implement a
size-counting approach that avoids the full buffer allocation (such as using a
custom serializer that only counts bytes without storing them), or if accepting
the allocation as reasonable, add a comment in the function explaining why the
current approach is acceptable given the bounded message size and single call
site.
crates/flowstate/src/collab/share_dialog.rs (1)

146-151: 💤 Low value

Consider logging clipboard write failures.

The let _ = pattern silently discards any error from window_handle.update(). While this is likely intentional (the window may have closed while the async ticket-minting task was in flight, and clipboard copying is best-effort), logging the failure would help debugging clipboard issues.

Optional: log clipboard errors
       if let Ok(text) = &encoded
         && let Some(window_handle) = copy_to_clipboard
       {
         let text = text.clone();
-        let _ = window_handle.update(cx, |_, _, cx| cx.write_to_clipboard(ClipboardItem::new_string(text)));
+        if let Err(e) = window_handle.update(cx, |_, _, cx| cx.write_to_clipboard(ClipboardItem::new_string(text))) {
+          eprintln!("Failed to copy invite to clipboard: {e}");
+        }
       }
🤖 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 `@crates/flowstate/src/collab/share_dialog.rs` around lines 146 - 151, The
`window_handle.update()` call that writes to the clipboard is silently
discarding any errors with the `let _ =` pattern. Replace this pattern by
capturing the Result returned from the update call and logging any failures
using an appropriate logger (such as error or warn level). Keep the operation as
best-effort by not propagating or panicking on the error, but ensure that
clipboard write failures are logged to help with debugging clipboard-related
issues.
crates/flowstate-collab/src/binding.rs (1)

148-167: ⚡ Quick win

Remove unnecessary clone in rebuild_indexes.

Line 153 clones each BindingRow before passing it to index_row, but index_row only needs a reference. This adds O(n) allocations every time indexes are rebuilt (after insert/remove/move operations).

♻️ Proposed fix
   pub fn rebuild_indexes(&mut self) {
     self.by_paragraph.clear();
     self.by_block.clear();
     self.by_container.clear();
     for ix in 0..self.rows.len() {
-      let row = self.rows[ix].clone();
-      self.index_row(ix, &row);
+      self.index_row(ix, &self.rows[ix]);
     }
   }
🤖 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 `@crates/flowstate-collab/src/binding.rs` around lines 148 - 167, In the
rebuild_indexes method, remove the unnecessary clone operation on line 153 where
each BindingRow is being cloned. Since the index_row method only requires a
reference to the BindingRow (as seen in its signature), pass a direct reference
to self.rows[ix] to index_row instead of creating a clone first. This eliminates
wasteful O(n) allocations that occur every time indexes are rebuilt after
insert/remove/move operations.
crates/flowstate-collab/tests/convergence.rs (1)

14-55: ⚡ Quick win

Consider adding convergence tests for structural blocks.

The current convergence test comprehensively validates paragraph text and style convergence, but doesn't test structural block operations (insert/move/replace/delete of images, equations, tables). Adding convergence tests for these operations would increase confidence in the collaboration implementation.

🤖 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 `@crates/flowstate-collab/tests/convergence.rs` around lines 14 - 55, The test
`two_peers_converge_with_reordered_and_duplicated_update_imports` currently
validates convergence for paragraph text and style operations but does not cover
structural block operations. Add new convergence test functions that exercise
insert, move, replace, and delete operations on structural blocks (such as
images, equations, and tables) between two peers with reordered and duplicated
imports, following the same test pattern used for the existing paragraph-focused
test to ensure these operations also converge correctly.
crates/gpui-flowtext/src/rich_text/editor/lifecycle.rs (1)

265-268: ⚡ Quick win

Method name misleading: clears all undo/redo history, not just collaboration history.

The method clear_collab_history clears both undo_stack and redo_stack entirely, which affects all editor history, not just collaboration-specific history. Consider renaming to clear_undo_redo_stacks or clear_all_history to accurately reflect the scope of the operation.

🤖 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 `@crates/gpui-flowtext/src/rich_text/editor/lifecycle.rs` around lines 265 -
268, The method `clear_collab_history` has a misleading name since it clears
both the `undo_stack` and `redo_stack` entirely, affecting all editor history
rather than just collaboration-specific history. Rename this method to
`clear_undo_redo_stacks` or `clear_all_history` to accurately reflect that it
clears all undo and redo history, not just collaboration-related history. Be
sure to update all call sites that reference this method name.
🤖 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 `@crates/flowstate-collab/src/ids.rs`:
- Around line 18-20: `SessionId::new` currently uses `rand::random()`, but
session IDs need a stronger entropy source; update the constructor in
`crates/flowstate-collab/src/ids.rs` to generate the ID with `rand::rngs::OsRng`
(or `SysRng`) instead of `rand::random()`, and keep the change localized to the
`SessionId`/ID generation path so all session IDs come from a cryptographically
secure source.

In `@crates/flowstate-collab/src/local_apply.rs`:
- Around line 148-164: In local_apply.rs inside the split/row insertion logic,
validate the new row metadata first before mutating the source LoroText: fetch
the inserted block id and version (the lookups around block_ids.get(insert_ix)
and blocks.get(insert_ix)) before calling delete_utf8 on the original paragraph,
and only truncate text once those checks succeed so a failed split cannot leave
the document partially edited.

In `@crates/flowstate-collab/src/net/blobs.rs`:
- Around line 45-57: In `BlobStore::insert_with_id`, reinserting an existing
`BlobId` currently leaves the old payload in `entries`, and `BlobStore::get`
returns that stale first match. Update `insert_with_id` to remove or overwrite
any existing entry with the same `BlobId` before pushing the new bytes, while
keeping `total_bytes` consistent with the replacement logic.

In `@crates/flowstate-collab/src/net/direct.rs`:
- Around line 138-146: The semaphore permit is being acquired after reading the
request frame, which allows unbounded task spawning and request buffering before
the concurrency limit is checked. Move the
self.permits.clone().try_acquire_owned() check to occur before the read_frame
call in handle_stream() to ensure the concurrency limit gates network work. This
issue applies at two locations: the primary site in handle_stream (lines
138-146) and a sibling location (lines 152-160). Additionally, consider gating
the permit acquisition even earlier, before tokio::spawn invokes handle_stream
in the accept() method, to limit task creation itself.

In `@crates/flowstate-collab/src/net/runtime.rs`:
- Around line 18-40: `start()` leaves `RUNTIME` pointing at dead channels after
`net_main()` shuts down, so future calls reuse a stopped bridge; change
`RuntimeBridge`/`start()` so a failed or exited runtime can be detected and
replaced with a fresh `spawn_runtime()` result instead of returning the cached
pair. Also update the `direct.rs` endpoint cache (the `OnceLock` around the
client endpoint) so it is invalidated or refreshed when the runtime restarts,
otherwise the new bridge will still use a stale endpoint.
- Around line 101-107: Contain per-command failures inside net_main so a bad
join/publish does not exit the runtime or skip responses. In
NetCommand::CreateSession, catch SwarmHandle::spawn errors locally, reply to the
caller with the failure, and keep the loop running; in the publish path around
handle.publish(payload).await, handle the error locally instead of propagating
it. Apply the same error containment at
crates/flowstate-collab/src/net/runtime.rs:101-107,
crates/flowstate-collab/src/net/runtime.rs:116-120, and
crates/flowstate-collab/src/net/runtime.rs:128-130, using the existing net_main
/ replace_swarm / handle.publish flow as the anchor points.

In `@crates/flowstate-collab/src/projection.rs`:
- Around line 169-175: `verify_lineage` in
`crates/flowstate-collab/src/projection.rs` only validates `META_SESSION`, so
add a `META_SCHEMA` check against `SCHEMA_VERSION` before returning success.
Update the `verify_lineage` path to fail fast with an error when the stored
schema is missing or does not match the current version, alongside the existing
session check, so snapshot projection never reaches `document_from_loro` with an
incompatible layout.

In `@crates/flowstate-collab/src/remote_apply.rs`:
- Around line 34-37: In remote_apply.rs, the Diff::Map handling in
remote_apply::apply should emit only one object-replacement patch per map diff
instead of calling apply_map_diff once per updated key; update the logic so
ReplaceObjectBlock is generated once per affected row even when replace_block_at
changes KIND, DATA, and REV together. Use the apply_map_diff path and its
related map-diff dispatch to dedupe by row/target before applying patches, and
ensure the same fix covers the other map-diff site referenced by the comment.

In `@crates/flowstate/src/collab/asset_transfer.rs`:
- Around line 142-147: In record_from_bytes, replace the use of DefaultHasher
with a stable, deterministic wire hash for cross-peer verification so identical
bytes always produce the same content_hash across platforms and Rust versions.
Update the content_hash computation to use a portable algorithm such as SHA-256
or xxHash, and keep the existing byte_len and content_hash validation logic in
AssetRecord unchanged except for comparing against the new stable hash value.

In `@crates/flowstate/src/collab/mod.rs`:
- Around line 119-146: The `session_by_panel` mapping is being inserted on line
137 before two fallible operations execute: the `try_send` call for
RegisterDirectHandler and the `establish_joined_peer` call. If either operation
fails, the function returns an error but leaves the mapping in place, causing
subsequent attach attempts to incorrectly think the panel is already attached.
Move the `self.session_by_panel.insert(panel_id, session_id)` statement to after
the `Self::establish_joined_peer(session, commands, cx)?` call succeeds,
ensuring the mapping is only recorded when the entire attachment operation
completes successfully.
- Around line 53-86: The `start_session_for_panel` function registers a session
at line 75 via `self.register_session(entity.clone(), cx)`, but if either the
`RegisterDirectHandler` command send (line 77) or the `CreateSession` command
send (line 82) fails, the function returns an error without cleaning up the
registered session. This creates a resource leak where the session ID remains
registered but the network layer never received the commands. To fix this, when
either `try_send` call fails, detach the session from its context and unregister
it before returning the error. Reference the correct cleanup pattern used in the
`join_session` method at lines 106-110, which properly calls detach and
unregister before returning errors, and apply the same approach to both error
paths in `start_session_for_panel`.

In `@crates/flowstate/src/collab/presence_view.rs`:
- Around line 33-39: The directory creation in collaboration_recovery_path
currently ignores failures from create_dir_all, so update this function to
handle that error instead of discarding it. Either change
collaboration_recovery_path to return a Result<PathBuf> and propagate the
std::fs::create_dir_all error to callers, or at minimum log/return a failure
when the temp recovery directory cannot be created; keep the rest of the
path-building logic with SessionId and sanitized_recovery_title unchanged.

In `@crates/flowstate/src/collab/session_timers.rs`:
- Around line 238-249: When `rebuild_from_projection` rebuilds the document
after a self-check by calling `replace_document_from_collaboration()`, any
deferred remote patches stored in `session_io.rs` become stale since they
describe edits already present in the rebuilt projection. These stale patches
would then be incorrectly re-applied on the next flush. Clear the
`pending_remote_patches` from `session_io.rs` within the
`rebuild_from_projection` method after the document has been rebuilt to prevent
this re-application of already-synchronized edits.

In `@crates/flowstate/src/collab/session.rs`:
- Around line 131-132: Switch the session request channels in `Session` setup
from unbounded to bounded to apply backpressure and avoid memory growth; update
the `direct_tx/direct_rx` and `undo_tx/undo_rx` creation in `collab/session.rs`
to use `async_channel::bounded` with a justified capacity, and make sure the
downstream direct request pump and undo request pump code paths still handle
send/receive behavior correctly. If keeping unbounded is intentional, document
that decision near the channel setup.

In `@crates/flowstate/src/workspace/workspace/collab.rs`:
- Around line 204-208: In join_collaboration_from_clipboard’s synchronous error
path around crate::collab::join_session, don’t just log and return None; surface
a user-visible error prompt before exiting the branch. Update the error handling
in workspace/collab.rs so the failure is reported through the existing UI/error
notification mechanism used by this flow, while still returning None after the
prompt is shown.

In `@crates/flowstate/src/workspace/workspace/top_bar.rs`:
- Around line 109-143: Update collaboration_top_bar_button in
workspace/top_bar.rs so the “Leave Shared Session” menu item is disabled unless
the active document actually has an attached collaboration session, not just
when has_document is true. Use the same workspace/session state used by
confirm_leave_collaboration_on_active_document to compute the enabled flag, and
pass that into file_menu_item for the “Leave Shared Session” entry so local tabs
don’t show a no-op action.

In `@crates/flowstate/src/workspace/workspace/window.rs`:
- Around line 84-87: `window.rs` is shutting down the global collaboration
runtime too early, which breaks other open windows; change the
`window_handle.update` close path so
`workspace.leave_all_collaboration_sessions(cx)` only affects the current
workspace and `crate::collab::shutdown(cx)` is deferred until the last
workspace/window is gone or behind a shared reference count. Also audit the
collaboration close/leave flow in `collab_prompts.rs` so it does not trigger a
global shutdown from a single workspace path, and route any such teardown
through the same last-window guard.

In `@crates/gpui-flowtext/src/rich_text/editor/block_insertion.rs`:
- Around line 79-80: Update prepare_block_insertion_index and
insert_ordered_block_fragment_after_caret so the placeholder caret paragraph is
only removed when the insertion does not include InputBlock::Paragraph; preserve
the lone empty paragraph for paragraph-block inserts to keep
self.document.paragraphs and self.document.blocks aligned. Use the existing
block-rebuild logic in insert_ordered_block_fragment_after_caret and the caret
cleanup in prepare_block_insertion_index to gate the removal instead of
unconditionally stripping the paragraph.

---

Outside diff comments:
In `@crates/gpui-flowtext/src/rich_text/editor/mouse.rs`:
- Around line 278-304: Build the `ReplaceParagraphSpan` canonical op from the
pre-edit state in `mouse.rs` so it reflects the drag/drop before any mutation is
applied; capture the source-side deleted paragraph span as part of the same
canonical operation, not just the inserted drop range. Update the
`EditRecord`/`mark_document_changed_with_ops` path around `canonical_operations`
so the payload uses the pre-delete document contents and includes both source
and destination paragraph ranges when the drag crosses paragraphs.

In `@crates/gpui-flowtext/src/rich_text/editor/object_selection.rs`:
- Around line 492-500: The block identifier is being resolved after the block
and id vectors have been mutated, causing the lookup to fail and fall back to a
fabricated BlockId(0), which can delete the wrong remote block in collaboration
mode. Capture the block identifier by calling
self.identity_map.block_id(block_ix) BEFORE removing the block from the vectors
using blocks.remove() and remove_block_ids(). Use the captured block id in the
CanonicalOperation for DeleteBlock. If the block id cannot be captured (when the
lookup fails), replace the DeleteBlock operation with a broader operation like
ReplaceDocument instead of using a fake BlockId(0). Apply this same fix to all
locations where blocks are deleted and canonical operations are created.

---

Nitpick comments:
In `@crates/flowstate-collab/src/binding.rs`:
- Around line 148-167: In the rebuild_indexes method, remove the unnecessary
clone operation on line 153 where each BindingRow is being cloned. Since the
index_row method only requires a reference to the BindingRow (as seen in its
signature), pass a direct reference to self.rows[ix] to index_row instead of
creating a clone first. This eliminates wasteful O(n) allocations that occur
every time indexes are rebuilt after insert/remove/move operations.

In `@crates/flowstate-collab/src/net/swarm.rs`:
- Around line 137-162: Replace the eprintln! call in the handle_event function's
error handling branch (where proto_gossip::decode fails) with a structured
logging call using tracing::warn!. This will allow production deployments to
configure log levels and get structured output instead of relying on stderr
output, enabling better observability and log aggregation.

In `@crates/flowstate-collab/src/proto_direct.rs`:
- Around line 31-38: Simplify encode_frame in proto_direct.rs by removing the
redundant u32::try_from(payload.len())? conversion after the MAX_FRAME_LEN
check; since payload.len() is already bounded below u32::MAX, compute the length
directly as a u32 and keep the rest of the frame assembly unchanged.

In `@crates/flowstate-collab/src/proto_gossip.rs`:
- Line 7: Move the DIRECT_ALPN constant out of proto_gossip.rs and into
proto_direct.rs so it lives with the other direct-protocol constants like
MAX_FRAME_LEN and MAX_PAYLOAD_CHUNK_LEN. Update any direct-protocol code that
references DIRECT_ALPN, especially net/direct.rs and net/runtime.rs, to import
it from proto_direct.rs instead of proto_gossip.rs, and leave proto_gossip.rs
containing only gossip-related constants such as PROTOCOL_VERSION and
GOSSIP_INLINE_LIMIT.
- Around line 30-32: The encoded_len function allocates the full message buffer
by calling encode(msg)? just to measure its length, which is inefficient even
though the practical impact is minimal with the current 2KB size bound. Either
implement a size-counting approach that avoids the full buffer allocation (such
as using a custom serializer that only counts bytes without storing them), or if
accepting the allocation as reasonable, add a comment in the function explaining
why the current approach is acceptable given the bounded message size and single
call site.

In `@crates/flowstate-collab/src/ticket.rs`:
- Around line 46-48: The encode_bytes method in the Ticket trait implementation
uses expect() which can panic on serialization failure. First, check the Ticket
trait definition to see if the return type can be changed from Vec<u8> to a
Result type. If the trait signature is flexible, update encode_bytes to return
Result<Vec<u8>, Box<dyn std::error::Error>> and use the ? operator instead of
expect(). If the trait signature is fixed and requires Vec<u8>, then keep the
current approach but add a doc comment above the method explaining why postcard
serialization of SessionTicket cannot fail and the panic is justified as a last
resort safeguard.

In `@crates/flowstate-collab/tests/convergence.rs`:
- Around line 14-55: The test
`two_peers_converge_with_reordered_and_duplicated_update_imports` currently
validates convergence for paragraph text and style operations but does not cover
structural block operations. Add new convergence test functions that exercise
insert, move, replace, and delete operations on structural blocks (such as
images, equations, and tables) between two peers with reordered and duplicated
imports, following the same test pattern used for the existing paragraph-focused
test to ensure these operations also converge correctly.

In `@crates/flowstate/src/collab/session_presence.rs`:
- Around line 9-11: The code currently uses eprintln! macro for logging error
messages when presence updates fail. Replace the eprintln! calls with a
structured logging framework such as tracing or log to enable better
observability, filtering, and production-grade log management. Update all
occurrences where eprintln! is used for the flowstate collab presence update
error message (in the error handling block for presence.apply() and any similar
error logging) to use the appropriate logging macro from your chosen framework
instead, ensuring consistent structured logging throughout the module.

In `@crates/flowstate/src/collab/session.rs`:
- Around line 592-595: The UndoManager setup in session.rs uses magic numbers
for merge_interval and max_undo_steps; extract the 500 and 300 values into named
constants or a session/undo configuration near UndoManager::new,
set_merge_interval, and set_max_undo_steps so the defaults are easy to find and
adjust.

In `@crates/flowstate/src/collab/share_dialog.rs`:
- Around line 146-151: The `window_handle.update()` call that writes to the
clipboard is silently discarding any errors with the `let _ =` pattern. Replace
this pattern by capturing the Result returned from the update call and logging
any failures using an appropriate logger (such as error or warn level). Keep the
operation as best-effort by not propagating or panicking on the error, but
ensure that clipboard write failures are logged to help with debugging
clipboard-related issues.

In `@crates/gpui-flowtext/src/rich_text/editor/lifecycle.rs`:
- Around line 265-268: The method `clear_collab_history` has a misleading name
since it clears both the `undo_stack` and `redo_stack` entirely, affecting all
editor history rather than just collaboration-specific history. Rename this
method to `clear_undo_redo_stacks` or `clear_all_history` to accurately reflect
that it clears all undo and redo history, not just collaboration-related
history. Be sure to update all call sites that reference this method name.
🪄 Autofix (Beta)

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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0a0195cb-c104-4a46-a26a-5ff0efdfaa62

📥 Commits

Reviewing files that changed from the base of the PR and between cd5330b and 2b91dae.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (77)
  • CLAUDE.md
  • Cargo.toml
  • crates/flowstate-collab/Cargo.toml
  • crates/flowstate-collab/src/binding.rs
  • crates/flowstate-collab/src/ids.rs
  • crates/flowstate-collab/src/lib.rs
  • crates/flowstate-collab/src/local_apply.rs
  • crates/flowstate-collab/src/net/anti_entropy.rs
  • crates/flowstate-collab/src/net/blobs.rs
  • crates/flowstate-collab/src/net/direct.rs
  • crates/flowstate-collab/src/net/mod.rs
  • crates/flowstate-collab/src/net/runtime.rs
  • crates/flowstate-collab/src/net/swarm.rs
  • crates/flowstate-collab/src/presence.rs
  • crates/flowstate-collab/src/projection.rs
  • crates/flowstate-collab/src/proto_direct.rs
  • crates/flowstate-collab/src/proto_gossip.rs
  • crates/flowstate-collab/src/remote_apply.rs
  • crates/flowstate-collab/src/schema.rs
  • crates/flowstate-collab/src/self_check.rs
  • crates/flowstate-collab/src/ticket.rs
  • crates/flowstate-collab/tests/anti_entropy.rs
  • crates/flowstate-collab/tests/convergence.rs
  • crates/flowstate-collab/tests/local_apply.rs
  • crates/flowstate-collab/tests/projection.rs
  • crates/flowstate-collab/tests/translation.rs
  • crates/flowstate/Cargo.toml
  • crates/flowstate/src/collab/asset_transfer.rs
  • crates/flowstate/src/collab/mod.rs
  • crates/flowstate/src/collab/presence_view.rs
  • crates/flowstate/src/collab/session.rs
  • crates/flowstate/src/collab/session_io.rs
  • crates/flowstate/src/collab/session_presence.rs
  • crates/flowstate/src/collab/session_timers.rs
  • crates/flowstate/src/collab/share_dialog.rs
  • crates/flowstate/src/collab/shutdown.rs
  • crates/flowstate/src/collab/status.rs
  • crates/flowstate/src/commands.rs
  • crates/flowstate/src/commands/keymap.rs
  • crates/flowstate/src/lib.rs
  • crates/flowstate/src/rich_text_element/mod.rs
  • crates/flowstate/src/workspace/workspace/collab.rs
  • crates/flowstate/src/workspace/workspace/collab_prompts.rs
  • crates/flowstate/src/workspace/workspace/documents.rs
  • crates/flowstate/src/workspace/workspace/keybindings.rs
  • crates/flowstate/src/workspace/workspace/mod.rs
  • crates/flowstate/src/workspace/workspace/render_documents.rs
  • crates/flowstate/src/workspace/workspace/render_status.rs
  • crates/flowstate/src/workspace/workspace/render_top_bar.rs
  • crates/flowstate/src/workspace/workspace/top_bar.rs
  • crates/flowstate/src/workspace/workspace/traits.rs
  • crates/flowstate/src/workspace/workspace/window.rs
  • crates/gpui-flowtext/Cargo.toml
  • crates/gpui-flowtext/src/collaboration.rs
  • crates/gpui-flowtext/src/document/blocks.rs
  • crates/gpui-flowtext/src/rich_text/editor/block_insertion.rs
  • crates/gpui-flowtext/src/rich_text/editor/caret_movement.rs
  • crates/gpui-flowtext/src/rich_text/editor/collab_apply.rs
  • crates/gpui-flowtext/src/rich_text/editor/commands.rs
  • crates/gpui-flowtext/src/rich_text/editor/edit_pipeline.rs
  • crates/gpui-flowtext/src/rich_text/editor/lifecycle.rs
  • crates/gpui-flowtext/src/rich_text/editor/media.rs
  • crates/gpui-flowtext/src/rich_text/editor/mod.rs
  • crates/gpui-flowtext/src/rich_text/editor/mouse.rs
  • crates/gpui-flowtext/src/rich_text/editor/movement_core.rs
  • crates/gpui-flowtext/src/rich_text/editor/object_assets.rs
  • crates/gpui-flowtext/src/rich_text/editor/object_selection.rs
  • crates/gpui-flowtext/src/rich_text/editor/paste.rs
  • crates/gpui-flowtext/src/rich_text/editor/render_blocks.rs
  • crates/gpui-flowtext/src/rich_text/editor/serialization.rs
  • crates/gpui-flowtext/src/rich_text/editor/table_equation_editing.rs
  • crates/gpui-flowtext/src/rich_text/editor/tables.rs
  • crates/gpui-flowtext/src/rich_text/layout/block_layout.rs
  • crates/gpui-flowtext/src/rich_text/tests/collab_capture.rs
  • crates/gpui-flowtext/src/rich_text/tests/mod.rs
  • helpers/docs/collab_qa.md
  • plan.md

Comment thread crates/flowstate-collab/src/ids.rs
Comment thread crates/flowstate-collab/src/local_apply.rs Outdated
Comment thread crates/flowstate-collab/src/net/blobs.rs Outdated
Comment thread crates/flowstate-collab/src/net/direct.rs Outdated
Comment thread crates/flowstate-collab/src/net/runtime.rs
Comment thread crates/flowstate/src/collab/session.rs Outdated
Comment thread crates/flowstate/src/workspace/workspace/collab.rs
Comment thread crates/flowstate/src/workspace/workspace/top_bar.rs Outdated
Comment thread crates/flowstate/src/workspace/workspace/window.rs
Comment thread crates/gpui-flowtext/src/rich_text/editor/block_insertion.rs Outdated

@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: 2

🤖 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 `@fix.md`:
- Around line 298-300: The markdown heading hierarchy in fix.md has a gap
between the level-2 heading WAVE 3 and the level-4 heading V1, which violates
the MD001 rule for contiguous heading levels. Change the V1 heading from four
hash marks (####) to three hash marks (###) to maintain proper heading hierarchy
immediately under WAVE 3.
- Around line 21-40: The markdown code fence for the WAVE plan block is missing
a language tag, which violates MD040 and causes inconsistent rendering. Add the
language tag "text" immediately after the opening triple backticks on line 21
(before the newline), so the fence opens with ```text instead of just ```.
🪄 Autofix (Beta)

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: defaults

Review profile: CHILL

Plan: Pro

Run ID: ab6dd76a-be26-40dd-b429-f2aed429750f

📥 Commits

Reviewing files that changed from the base of the PR and between 3a8bc26 and 2dd3249.

📒 Files selected for processing (1)
  • fix.md

Comment thread fix.md Outdated
Comment thread fix.md Outdated

@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 `@crates/flowstate-collab/src/net/runtime.rs`:
- Around line 212-224: The attach_session call on the direct_state object
executes before SwarmHandle::spawn which can fail, leaving a stale attached
session if spawn errors. Move the direct_state.attach_session(session) call to
execute only after the SwarmHandle::spawn completes successfully and
replace_swarm finishes. Apply this same fix at both locations where this pattern
occurs (the one shown in the diff and the second occurrence mentioned in the
comment).
🪄 Autofix (Beta)

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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4a34d9d2-bd30-4286-899d-533a2c1ec439

📥 Commits

Reviewing files that changed from the base of the PR and between 2dd3249 and 0f065e7.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (69)
  • Cargo.toml
  • crates/flowstate-collab/Cargo.toml
  • crates/flowstate-collab/src/binding.rs
  • crates/flowstate-collab/src/ids.rs
  • crates/flowstate-collab/src/lib.rs
  • crates/flowstate-collab/src/local_apply.rs
  • crates/flowstate-collab/src/net/blobs.rs
  • crates/flowstate-collab/src/net/direct.rs
  • crates/flowstate-collab/src/net/mod.rs
  • crates/flowstate-collab/src/net/runtime.rs
  • crates/flowstate-collab/src/net/swarm.rs
  • crates/flowstate-collab/src/patch_apply.rs
  • crates/flowstate-collab/src/presence.rs
  • crates/flowstate-collab/src/projection.rs
  • crates/flowstate-collab/src/proto_direct.rs
  • crates/flowstate-collab/src/proto_gossip.rs
  • crates/flowstate-collab/src/remote_apply.rs
  • crates/flowstate-collab/src/schema.rs
  • crates/flowstate-collab/src/self_check.rs
  • crates/flowstate-collab/src/ticket.rs
  • crates/flowstate-collab/tests/convergence.rs
  • crates/flowstate-collab/tests/remote_apply.rs
  • crates/flowstate-collab/tests/swarm_loopback.rs
  • crates/flowstate-collab/tests/translation.rs
  • crates/flowstate/Cargo.toml
  • crates/flowstate/src/collab/asset_transfer.rs
  • crates/flowstate/src/collab/manager.rs
  • crates/flowstate/src/collab/mod.rs
  • crates/flowstate/src/collab/notify.rs
  • crates/flowstate/src/collab/presence_view.rs
  • crates/flowstate/src/collab/pump.rs
  • crates/flowstate/src/collab/session.rs
  • crates/flowstate/src/collab/session_io.rs
  • crates/flowstate/src/collab/session_presence.rs
  • crates/flowstate/src/collab/session_timers.rs
  • crates/flowstate/src/collab/share_dialog.rs
  • crates/flowstate/src/collab/share_dialog_view.rs
  • crates/flowstate/src/collab/shutdown.rs
  • crates/flowstate/src/collab/status.rs
  • crates/flowstate/src/workspace/icons.rs
  • crates/flowstate/src/workspace/workspace/collab.rs
  • crates/flowstate/src/workspace/workspace/collab_prompts.rs
  • crates/flowstate/src/workspace/workspace/documents.rs
  • crates/flowstate/src/workspace/workspace/mod.rs
  • crates/flowstate/src/workspace/workspace/render_status.rs
  • crates/flowstate/src/workspace/workspace/render_top_bar.rs
  • crates/flowstate/src/workspace/workspace/top_bar.rs
  • crates/flowstate/src/workspace/workspace/traits.rs
  • crates/flowstate/src/workspace/workspace/window.rs
  • crates/gpui-flowtext/src/document/blocks.rs
  • crates/gpui-flowtext/src/persistence/records.rs
  • crates/gpui-flowtext/src/persistence/validation.rs
  • crates/gpui-flowtext/src/rich_text/editor/block_insertion.rs
  • crates/gpui-flowtext/src/rich_text/editor/collab_apply.rs
  • crates/gpui-flowtext/src/rich_text/editor/commands.rs
  • crates/gpui-flowtext/src/rich_text/editor/hit_testing.rs
  • crates/gpui-flowtext/src/rich_text/editor/lifecycle.rs
  • crates/gpui-flowtext/src/rich_text/editor/mod.rs
  • crates/gpui-flowtext/src/rich_text/editor/mouse.rs
  • crates/gpui-flowtext/src/rich_text/editor/object_assets.rs
  • crates/gpui-flowtext/src/rich_text/editor/object_selection.rs
  • crates/gpui-flowtext/src/rich_text/editor/platform.rs
  • crates/gpui-flowtext/src/rich_text/editor/search_highlights.rs
  • crates/gpui-flowtext/src/rich_text/element.rs
  • crates/gpui-flowtext/src/rich_text/tests/collab_capture.rs
  • crates/gpui-flowtext/src/rich_text/tests/edit_layout.rs
  • crates/gpui-flowtext/src/rich_text/tests/persistence_blocks.rs
  • helpers/docs/collab_qa.md
  • helpers/docs/collaboration.md
💤 Files with no reviewable changes (4)
  • crates/flowstate/src/collab/shutdown.rs
  • crates/flowstate/src/workspace/workspace/window.rs
  • crates/gpui-flowtext/src/rich_text/tests/edit_layout.rs
  • crates/flowstate/src/workspace/workspace/collab_prompts.rs
✅ Files skipped from review due to trivial changes (2)
  • helpers/docs/collaboration.md
  • helpers/docs/collab_qa.md
🚧 Files skipped from review as they are similar to previous changes (26)
  • crates/flowstate/Cargo.toml
  • crates/flowstate-collab/src/self_check.rs
  • crates/flowstate-collab/Cargo.toml
  • crates/flowstate-collab/src/proto_gossip.rs
  • crates/flowstate/src/workspace/workspace/render_top_bar.rs
  • crates/flowstate/src/collab/status.rs
  • crates/flowstate-collab/src/binding.rs
  • Cargo.toml
  • crates/gpui-flowtext/src/rich_text/editor/object_selection.rs
  • crates/gpui-flowtext/src/rich_text/editor/lifecycle.rs
  • crates/gpui-flowtext/src/rich_text/editor/mod.rs
  • crates/flowstate/src/collab/asset_transfer.rs
  • crates/flowstate-collab/src/ticket.rs
  • crates/flowstate-collab/src/ids.rs
  • crates/flowstate-collab/src/lib.rs
  • crates/flowstate-collab/src/proto_direct.rs
  • crates/flowstate-collab/src/presence.rs
  • crates/flowstate/src/workspace/workspace/documents.rs
  • crates/flowstate-collab/src/net/mod.rs
  • crates/gpui-flowtext/src/rich_text/editor/collab_apply.rs
  • crates/flowstate-collab/src/local_apply.rs
  • crates/gpui-flowtext/src/rich_text/editor/block_insertion.rs
  • crates/flowstate/src/workspace/workspace/mod.rs
  • crates/flowstate-collab/src/remote_apply.rs
  • crates/flowstate/src/workspace/workspace/collab.rs
  • crates/flowstate/src/collab/session_io.rs

Comment thread crates/flowstate-collab/src/net/runtime.rs
Implemented collaboration-only tracing with non-blocking file logging.
What changed:
- Added tracing, tracing-subscriber, and tracing-appender.
- Added crates/flowstate/src/logging.rs using
tracing_appender::non_blocking, so log writing runs on its own worker
thread.
- Logging is initialized from main and the WorkerGuard is held for app
lifetime.
- Added structured collaboration logs for:
- session create, join, attach, leave, shutdown
- gossip subscribe/publish/receive/lag/neighbors
- direct snapshot/update/blob/asset pulls and serves
- text sync import/export, local edit capture, remote patches
- presence publish/apply and peer counts
- anti-entropy digest decisions and recovery pulls
- asset transfer and validation
- offline/reconnect/recovery/self-check drift
- relevant join/share workspace errors
Defaults:
- Logs go to flowstate_data_dir()/logs as daily rolling flowstate.log...
files.
- Override with FLOWSTATE_LOG_DIR=/path/to/logs.
- Default filter is collaboration-only:
- flowstate::collab=trace
- flowstate::workspace::workspace=trace
- flowstate_collab=trace
- Override with FLOWSTATE_LOG or RUST_LOG.
Validation:
- cargo clippy -p flowstate -p flowstate-collab passes.
- git diff --check passes.

@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 `@crates/flowstate/src/collab/session_io.rs`:
- Around line 105-110: The malformed digest payload handling in the
VersionVector::decode section is currently returning an Err that propagates up
to the caller in session.rs at line 724, causing fatal session detachment.
Instead of returning Err when the decode fails, handle the malformed digest
gracefully by logging the warning (already done) and returning a success value
that indicates the digest was invalid or should be skipped, preventing the
session detachment. This treats the malformed digest as invalid input to be
ignored rather than as a fatal local error.
- Around line 24-35: The pending_remote_updates buffer in the session attachment
code path has no size constraints and will grow unbounded when updates are
received before the session is fully attached. Add a check before pushing bytes
to self.pending_remote_updates to enforce a maximum cap on either the number of
pending updates or total byte count. If the cap is exceeded, either drop the
update, reject it with an error, or implement appropriate backpressure to
prevent memory exhaustion from bursty or malicious traffic during session join.

In `@crates/flowstate/src/logging.rs`:
- Line 9: The logging configuration uses daily log rotation but does not
implement any cleanup or retention policy for old log files, causing unbounded
disk growth over time. Configure log file retention and cleanup in the logger
setup (around the DEFAULT_LOG_FILTER constant and logger initialization at lines
26-27) by adding a retention policy that automatically deletes old log files
after a configurable period (e.g., 7-30 days) or limits the total number of
retained log files. This ensures disk space is managed responsibly while still
maintaining useful logs for debugging.
🪄 Autofix (Beta)

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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3c954e8d-3dd0-483a-a2a1-7ed353499dcf

📥 Commits

Reviewing files that changed from the base of the PR and between 0f065e7 and c8375bd.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (20)
  • Cargo.toml
  • crates/flowstate-collab/Cargo.toml
  • crates/flowstate-collab/src/net/anti_entropy.rs
  • crates/flowstate-collab/src/net/direct.rs
  • crates/flowstate-collab/src/net/mod.rs
  • crates/flowstate-collab/src/net/runtime.rs
  • crates/flowstate-collab/src/net/swarm.rs
  • crates/flowstate-collab/src/proto_gossip.rs
  • crates/flowstate/Cargo.toml
  • crates/flowstate/src/collab/asset_transfer.rs
  • crates/flowstate/src/collab/manager.rs
  • crates/flowstate/src/collab/pump.rs
  • crates/flowstate/src/collab/session.rs
  • crates/flowstate/src/collab/session_io.rs
  • crates/flowstate/src/collab/session_presence.rs
  • crates/flowstate/src/collab/session_timers.rs
  • crates/flowstate/src/lib.rs
  • crates/flowstate/src/logging.rs
  • crates/flowstate/src/main.rs
  • crates/flowstate/src/workspace/workspace/collab.rs
✅ Files skipped from review due to trivial changes (1)
  • crates/flowstate/src/lib.rs
🚧 Files skipped from review as they are similar to previous changes (12)
  • crates/flowstate-collab/src/proto_gossip.rs
  • Cargo.toml
  • crates/flowstate-collab/src/net/mod.rs
  • crates/flowstate-collab/src/net/runtime.rs
  • crates/flowstate/src/collab/asset_transfer.rs
  • crates/flowstate-collab/src/net/swarm.rs
  • crates/flowstate-collab/src/net/direct.rs
  • crates/flowstate/src/workspace/workspace/collab.rs
  • crates/flowstate/src/collab/pump.rs
  • crates/flowstate/src/collab/session_timers.rs
  • crates/flowstate/src/collab/manager.rs
  • crates/flowstate/src/collab/session_presence.rs

Comment on lines +24 to +35
if self.doc.is_none() || self.binding.is_none() || self.editor.is_none() {
tracing::debug!(
session = %self.session,
bytes = bytes.len(),
queued_updates = self.pending_remote_updates.len() + 1,
has_doc = self.doc.is_some(),
has_binding = self.binding.is_some(),
has_editor = self.editor.is_some(),
"queueing remote collaboration update until session is attached",
);
self.pending_remote_updates.push(bytes.to_vec());
return Ok(());

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.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Bound pre-attach remote update buffering to avoid memory blowups.

When the session is not fully attached, every inbound update is appended to pending_remote_updates with no byte/count cap. Under bursty or malicious traffic during join, this can grow without bound and exhaust memory before attach completes.

💡 Suggested direction
+// Example guardrails (tune values):
+const MAX_PENDING_REMOTE_UPDATES: usize = 512;
+const MAX_PENDING_REMOTE_UPDATE_BYTES: usize = 8 * 1024 * 1024;

 pub fn import_update_bytes(&mut self, bytes: &[u8], cx: &mut Context<Self>) -> Result<()> {
   if self.doc.is_none() || self.binding.is_none() || self.editor.is_none() {
+    let queued_bytes: usize = self.pending_remote_updates.iter().map(Vec::len).sum();
+    if self.pending_remote_updates.len() >= MAX_PENDING_REMOTE_UPDATES
+      || queued_bytes.saturating_add(bytes.len()) > MAX_PENDING_REMOTE_UPDATE_BYTES
+    {
+      tracing::warn!(session = %self.session, queued = self.pending_remote_updates.len(), queued_bytes, incoming = bytes.len(), "remote update queue limit exceeded during attach/join");
+      // choose one policy: drop oldest/newest, request resync, or fail join explicitly
+      return Ok(());
+    }
     self.pending_remote_updates.push(bytes.to_vec());
     return Ok(());
   }
🤖 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 `@crates/flowstate/src/collab/session_io.rs` around lines 24 - 35, The
pending_remote_updates buffer in the session attachment code path has no size
constraints and will grow unbounded when updates are received before the session
is fully attached. Add a check before pushing bytes to
self.pending_remote_updates to enforce a maximum cap on either the number of
pending updates or total byte count. If the cap is exceeded, either drop the
update, reject it with an error, or implement appropriate backpressure to
prevent memory exhaustion from bursty or malicious traffic during session join.

Comment thread crates/flowstate/src/collab/session_io.rs
Comment thread crates/flowstate/src/logging.rs Outdated
cxnmai and others added 10 commits June 16, 2026 19:33
Eliminates the whole-document rewrite/reprojection on every non-trivial edit
while preserving the single-root LoroText design and CRDT convergence.

- T1: maintained paragraph->byte Fenwick offset index in DocBinding
  (new body_index.rs); range helpers no longer stringify the whole body.
- T2: incremental ReplaceParagraphSpan — span-scoped minimal body splice,
  mark carry-over, targeted block-row reconcile; replace_document fallback.
- T4: delta-driven remote reconcile — single-paragraph fast path via the
  Loro TextDelta + slice_delta; falls back to full reprojection on mark
  changes / newline changes / multi-region edits.
- T6-T9: editor keystroke-path incrementalization (skip section rebuild on
  content edits, in-place block update, O(log n)+shift offset update).
- T10: O(1) DocumentIdentityMap::paragraph_index via FxHashMap.
- T12: remove dead pre-CRDT op-replay path + postcard wire format.
- T15: cheap self-check via cached version-vector/projection hash.
- T14: mark plan.md/fix.md superseded by FIX_LORO_ROOT.md.

Adds a deterministic convergence regression and tests/perf_smoke.rs proving
edit-proportional local deltas and O(1) remote paragraph reconcile. T3/T5
(dead-in-prod/optional) and T11/T13 (breaking/higher-risk) deferred.
- logging: default to `error` only; `FLOWSTATE_LOG_LEVEL` (error|warn|info|
  debug|trace) raises Flowstate's own crates, `FLOWSTATE_LOG`/`RUST_LOG` give
  full EnvFilter control, `FLOWSTATE_LOG_STDOUT` also mirrors logs to stdout.
- README: document the logging environment variables and precedence.
- clippy: add missing trailing `;` in session_io asset-serve warn arm
  (semicolon_if_nothing_returned); `cargo clippy --workspace --all-targets`
  is now clean.
adamhumphrey44 and others added 22 commits June 19, 2026 21:08
remove_subtree was called but never defined in b459052 (broken build);
implement it cycle-safely.

Clippy/build fixes: collapsible-if and bool-to-int (flowstate-document),
renamed fn param + unused AssetId import (flowstate-collab),
Iterator::last -> next_back and box large DocumentRuntimeSource variant
(flowstate), private EditRecord visibility + pt->px test helper
(gpui-flowtext).

flowstate-flow: log corrupt .fl0 loads via tracing instead of silently
discarding them.

collab: after an external save/checkpoint records a named revision into
Loro, refresh the session version-vector and publish an anti-entropy
digest so peers converge on the new revision op promptly.
Implements every outstanding item from adjustmentplan.md surfaced by a full
section-by-section audit, verified by cargo test (190 pass) + cargo clippy
(0 errors/0 warnings in-tree).

flowstate-document:
- §11 section page structure (page size/margins/columns/orientation/numbering
  + header/footer flows) in schema, import (read+write), and projection.
- §19 integrity index chunk; §27 schema-migration records (infra).
- §28 container-id resolution used in the projector (not just stored).
- §29 native LoroMap::clear/LoroMovableList::clear.
- §31 AssetMap image dimensions; §15 register_user helper.

gpui-flowtext:
- §16 real affinity/gravity: EditorSelection carries+consumes affinity and
  VisualGravity drives caret placement at soft-wrap seams (no longer derived).
- §11 DocumentSection.page payload.

flowstate-collab:
- §16 affinity sourced from the editor selection (endpoint_intent removed).
- §22 missing_dependency_request wired into pending-import tracing.
- §23 permanent subscription now filters by origin, trigger, per-event frontier
  and a runtime epoch.
- §24 full projection index set (paragraph/block/table/style/section/asset/
  search/cursor-cache) materialized and used for O(1) lookups + invalidation.
- §28 container-id fast-path resolution; §29 native clear; §15 set_author_identity.

flowstate-docx:
- §26 structured DOCX import: tables, images (OPC media), and equations
  (OMML->LaTeX via new interpreter/omml.rs + interpreter/structured.rs).
- §26 export embeds JPEG as well as PNG.

flowstate:
- §11/§15 wiring: stable local user identity in app settings, threaded through
  the runtime actor (SetAuthorIdentity) so revisions carry durable authorship;
  collaborating saves refresh anti-entropy.

Also fixes a redo-selection regression (removed an over-aggressive
clear_pending_undo_selection) and updates the segment-count test for
Loro-native revision recording.
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