Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
a05ca70
fix(collab): keep snapshot compression off network workers
cxnmai Aug 19, 2026
182c14a
fix(collab): build joined snapshots off UI thread
cxnmai Aug 19, 2026
464e5cc
fix(collab): avoid eager history warmup on join
cxnmai Aug 19, 2026
d05cc28
refactor: migrate GPUI timers to executors
cxnmai Aug 20, 2026
6bd2a16
fix: update flowtext for pinned GPUI
cxnmai Aug 20, 2026
a724598
build: pin WASM-capable GPUI cohort
cxnmai Aug 20, 2026
639f91c
refactor: migrate dock panels to GPUI base
cxnmai Aug 20, 2026
00a39c3
fix: adapt Flowstate to pinned GPUI APIs
cxnmai Aug 20, 2026
44c63b3
fix: preserve document save errors after GPUI update
cxnmai Aug 20, 2026
0e28304
refactor: update panel save tasks for GPUI
cxnmai Aug 20, 2026
5eb990b
feat: make document core WASM-compatible
cxnmai Aug 20, 2026
f0fd949
feat: add GPUI browser target
cxnmai Aug 20, 2026
bf83e58
fix: use WASM-safe Flowtext render clock
cxnmai Aug 20, 2026
474b943
feat: launch real workspace on wasm
cxnmai Aug 20, 2026
ae11e18
feat: add browser-safe app settings backend
cxnmai Aug 20, 2026
0de2aca
fix: gate native ribbon services on wasm
cxnmai Aug 20, 2026
3f1a8ce
refactor: isolate native workspace services
cxnmai Aug 20, 2026
02f620d
feat: open real workspace document in browser
cxnmai Aug 20, 2026
1b3c6cd
feat: add in-memory web workspace actions
cxnmai Aug 20, 2026
705bdd5
fix: gate collaboration status on wasm
cxnmai Aug 20, 2026
e59666d
fix: render document chrome on wasm
cxnmai Aug 20, 2026
0a0b813
fix: preserve workspace shell on wasm
cxnmai Aug 20, 2026
e85a010
fix: disable native sharing controls on wasm
cxnmai Aug 20, 2026
d392a43
fix: preserve shared workspace state on wasm
cxnmai Aug 20, 2026
4723516
fix: render web workspace status bar
cxnmai Aug 20, 2026
7735dc7
fix: share tab shortcut labels across targets
cxnmai Aug 20, 2026
af5f1e3
fix: retain browser content pane geometry
cxnmai Aug 20, 2026
612f4ac
fix: preserve outline header on wasm
cxnmai Aug 20, 2026
2fbc821
fix: match browser theme and bundle icons
cxnmai Aug 20, 2026
7a721b3
fix: use current document theme on web
cxnmai Aug 20, 2026
22d06f1
fix: resolve web icon URLs against origin
cxnmai Aug 20, 2026
cf8e28c
feat: open real DB8 documents in browser
cxnmai Aug 20, 2026
3fe5582
fix: attach browser document write authority
cxnmai Aug 20, 2026
2f968be
fix: use browser-safe CRDT clocks
cxnmai Aug 20, 2026
dc89301
fix: use browser-safe document timestamps
cxnmai Aug 20, 2026
62bbae4
fix: bundle browser document fonts
cxnmai Aug 20, 2026
34f2917
fix: restore browser editor and settings surfaces
cxnmai Aug 20, 2026
8676247
fix: route browser delete keys to editor
cxnmai Aug 20, 2026
3ad8471
fix: consume editor deletion actions
cxnmai Aug 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
3,289 changes: 2,226 additions & 1,063 deletions Cargo.lock

Large diffs are not rendered by default.

33 changes: 31 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
cargo-features = ["profile-rustflags"]

[workspace]
members = ["crates/*"]
members = [
"crates/*",
"vendor/gpui-base",
"vendor/gpui-component",
"vendor/gpui-component-assets",
"vendor/gpui-component-macros",
]
default-members = ["crates/flowstate"]
resolver = "3"

Expand Down Expand Up @@ -31,9 +37,26 @@ flowstate-collab = { version = "0.1.0", path = "crates/flowstate-collab" }
flowstate-flow = { version = "0.1.0", path = "crates/flowstate-flow" }
flowstate-tub = { version = "0.1.0", path = "crates/flowstate-tub" }
gpui-flowtext = { version = "0.1.0", path = "crates/gpui-flowtext" }
gpui = "0.2.2"
gpui = { git = "https://github.com/zed-industries/zed.git", rev = "e0931d5a9dbf4f781b336fdf448739e74a2ac0b5", default-features = false }
gpui_platform = { git = "https://github.com/zed-industries/zed.git", rev = "e0931d5a9dbf4f781b336fdf448739e74a2ac0b5" }
gpui-component = "0.5.1"
gpui-component-assets = "0.5.1"
gpui-base = { path = "vendor/gpui-base" }
gpui-component-macros = { path = "vendor/gpui-component-macros" }
gpui_macros = { git = "https://github.com/zed-industries/zed.git", rev = "e0931d5a9dbf4f781b336fdf448739e74a2ac0b5" }
instant = { version = "0.1", features = ["wasm-bindgen"] }
web-time = "1.1"
log = "0.4"
lsp-types = { version = "0.97.0", features = ["proposed"] }
raw-window-handle = "0.6.2"
ropey = { version = "=2.0.0-beta.1", features = ["metric_chars", "metric_lines_lf", "metric_utf16"] }
rust-i18n = "4.2.0"
schemars = "1"
serde_repr = "0.1"
smallvec = "1"
smol = "2"
sum-tree = { version = "0.2.0", package = "zed-sum-tree" }
syntect = { version = "5.3.0", default-features = false, features = ["parsing", "default-syntaxes", "regex-fancy"] }
gpui-symbols = { version = "0.6.1", features = ["component"] }
hotpath = "0.16.1"
ignore = "0.4.25"
Expand Down Expand Up @@ -76,8 +99,10 @@ typst-svg = "0.14"
unicode-segmentation = "1"
url = "2.5"
uuid = { version = "1.23.1", features = ["serde", "v4"] }
windows = { version = "0.58.0", features = ["Wdk", "Wdk_System", "Wdk_System_SystemServices"] }
zip = { version = "0.6.6", default-features = false, features = ["deflate"] }
zstd = "0.13.3"
zed-font-kit = { git = "https://github.com/zed-industries/font-kit", rev = "94b0f28166665e8fd2f53ff6d268a14955c82269", package = "zed-font-kit", features = ["loader-freetype-default", "source-fontconfig-dlopen"] }

[patch.crates-io]
# §perf (bimodal-undo fix): vendored to add `BTree::next_leaf_matching`, a
Expand All @@ -87,7 +112,11 @@ zstd = "0.13.3"
# rebuild retreats a large concurrent branch (the 80-195s dirty-history undo
# regime). One added method; re-apply on generic-btree upgrades.
generic-btree = { path = "vendor/generic-btree" }
gpui = { git = "https://github.com/zed-industries/zed.git", rev = "e0931d5a9dbf4f781b336fdf448739e74a2ac0b5" }
gpui-component = { path = "vendor/gpui-component" }
gpui-base = { path = "vendor/gpui-base" }
gpui-component-assets = { path = "vendor/gpui-component-assets" }
gpui-component-macros = { path = "vendor/gpui-component-macros" }
# §perf: vendored to add `LoroDoc::query_text_id_positions`, a single-pass batch
# resolver for many text cursor ids. Upstream Loro 1.13 only offers per-id cursor
# resolution (`get_cursor_pos`), which is O(elements) each; resolving every
Expand Down
20 changes: 12 additions & 8 deletions crates/flowstate-collab/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,30 +6,34 @@ publish.workspace = true

[dependencies]
anyhow = { workspace = true }
async-channel = { workspace = true }
base64 = { workspace = true }
blake3 = "1.8.5"
flowstate-document.workspace = true
flowstate-fidelity.workspace = true
gpui-flowtext = { workspace = true }
hotpath.workspace = true
instant = { workspace = true }
loro = { workspace = true }
postcard = { workspace = true }
rustc-hash = { workspace = true }
serde = { workspace = true }
tracing = { workspace = true }
uuid = { workspace = true }
web-time = { workspace = true }

[target.'cfg(not(target_family = "wasm"))'.dependencies]
async-channel = { workspace = true }
base64 = { workspace = true }
iroh = { workspace = true }
iroh-gossip = { workspace = true }
iroh-tickets = { workspace = true }
loro = { workspace = true }
n0-future = { workspace = true }
postcard = { workspace = true }
rand = { workspace = true }
reqwest = { workspace = true }
rustc-hash = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
sha2 = { workspace = true }
tokio = { workspace = true }
tracing = { workspace = true }
twox-hash = { workspace = true }
url = { workspace = true }
uuid = { workspace = true }
zstd.workspace = true

[features]
Expand Down
48 changes: 24 additions & 24 deletions crates/flowstate-collab/src/crdt_runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1272,18 +1272,18 @@ impl CrdtRuntime {
static OPEN_PROBE: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
let probe = *OPEN_PROBE.get_or_init(|| std::env::var_os("FLOWSTATE_OPEN_PROBE").is_some());
let path = path.as_ref();
let t0 = std::time::Instant::now();
let t0 = instant::Instant::now();
let package = DocumentPackage::read(path).with_context(|| format!("reading Flowstate package {}", path.display()))?;
let t_read = t0.elapsed();
let t1 = std::time::Instant::now();
let t1 = instant::Instant::now();
let projection = package
.current_projection_document()
.context("reading frontier-matched package projection cache")?;
let t_cache = t1.elapsed();
let cache_hit = projection.is_some();
// `read` just validated the package; the plain `load_loro_doc` would run
// a SECOND full validate (re-hashing every segment + asset) on every open.
let t2 = std::time::Instant::now();
let t2 = instant::Instant::now();
// §act-twelve A12.1.3 (default ON): decode the shallow accelerator chunk
// instead of the full history when the package carries one. Every
// anomaly — no chunk, stale chunk, a segment the shallow state cannot
Expand All @@ -1309,7 +1309,7 @@ impl CrdtRuntime {
.context("loading Loro document from package")?,
};
let t_doc = t2.elapsed();
let t3 = std::time::Instant::now();
let t3 = instant::Instant::now();
let mut runtime = Self::from_doc_with_projection(doc, Some(package), Some(path.to_path_buf()), projection)?;
runtime.package_journal_prepared = true;
if probe {
Expand Down Expand Up @@ -1371,7 +1371,7 @@ impl CrdtRuntime {
// call is idempotent, so re-configuring an already-configured doc is a no-op.
static OPEN_PROBE2: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
let open_probe = *OPEN_PROBE2.get_or_init(|| std::env::var_os("FLOWSTATE_OPEN_PROBE").is_some());
let probe_t0 = std::time::Instant::now();
let probe_t0 = instant::Instant::now();
flowstate_document::loro_schema::configure_text_styles(&doc);
let frontier_before_startup_metadata = doc.state_frontiers().encode();
let projection_content_repaired = if repair_paragraph_style_marks {
Expand All @@ -1387,7 +1387,7 @@ impl CrdtRuntime {
// arms below skip projection, so they contribute none.
let mut startup_defects: Vec<ProjectionDefect> = Vec::new();
let probe_t_repair = probe_t0.elapsed();
let probe_t1 = std::time::Instant::now();
let probe_t1 = instant::Instant::now();
let mut projection = match projection {
Some(projection) if projection.frontier == current_frontier => projection,
Some(mut projection) if !projection_content_repaired && projection.frontier == frontier_before_startup_metadata => {
Expand Down Expand Up @@ -3104,7 +3104,7 @@ impl CrdtRuntime {
// ---- Rematerialize the region under the shared law + splice. -----------
static DERIVE_DEBUG: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
let derive_debug = *DERIVE_DEBUG.get_or_init(|| std::env::var_os("FLOWSTATE_DERIVE_DEBUG").is_some());
let materialize_start = std::time::Instant::now();
let materialize_start = instant::Instant::now();
let region = flowstate_document::materialize_body_region(&self.doc, region_sentinel, region_end, &paragraph_map, &pblock_map, &object_map)
.map_err(|_| "region-materialize-failed")?;
if derive_debug {
Expand Down Expand Up @@ -3161,7 +3161,7 @@ impl CrdtRuntime {
let fidelity_before_vv = fidelity::enabled().then(|| self.doc.state_vv());
static IMPORT_PROBE: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
let import_probe = *IMPORT_PROBE.get_or_init(|| std::env::var_os("FLOWSTATE_IMPORT_PROBE").is_some());
let probe_t = std::time::Instant::now();
let probe_t = instant::Instant::now();
let mut statuses = Vec::with_capacity(chunks.len());
let mut below_root_chunks: Vec<&[u8]> = Vec::new();
for bytes in chunks {
Expand Down Expand Up @@ -3202,7 +3202,7 @@ impl CrdtRuntime {
// through hull-disjoint remote changes and only clears when the import
// actually threatens the recorded coordinates.
let probe_loro = probe_t.elapsed();
let probe_t = std::time::Instant::now();
let probe_t = instant::Instant::now();
let import_applied_changes = frontier_after != from_frontier.encode();
// §22: when an import is missing dependencies, surface the pending version
// range so the UI session can trigger immediate update pull/anti-entropy
Expand Down Expand Up @@ -3238,12 +3238,12 @@ impl CrdtRuntime {
};
let drained = self.merge_subscription_invalidation(&mut invalidation);
let probe_drain = probe_t.elapsed();
let probe_t2 = std::time::Instant::now();
let probe_t2 = instant::Instant::now();
if import_applied_changes {
self.reconcile_recorded_inverse_after_remote(&invalidation, &drained);
}
let probe_reconcile = probe_t2.elapsed();
let probe_t2 = std::time::Instant::now();
let probe_t2 = instant::Instant::now();
self.derive_body_projection_events(invalidation, &drained, "remote_import", last)?;
if import_probe {
eprintln!(
Expand Down Expand Up @@ -3289,14 +3289,14 @@ impl CrdtRuntime {
// one-directional-sync failure. Log and keep the merge in memory instead;
// the segment persistence self-heals (re-snapshots) in `persist_update_segment`.
// One revision sync + one segment persist covers the WHOLE chunk.
let probe_tail = std::time::Instant::now();
let probe_tail = instant::Instant::now();
if let Some(package) = &mut self.package
&& let Err(error) = package.sync_revisions_from_loro(&self.doc)
{
tracing::error!(%error, "syncing revisions after remote import failed; kept the merged update in memory");
}
let probe_revisions = probe_tail.elapsed();
let probe_tail = std::time::Instant::now();
let probe_tail = instant::Instant::now();
if let Err(error) = self.persist_update_from_last_frontier() {
tracing::error!(%error, "persisting merged remote update failed; kept the merge in memory (durability degraded until the next successful save)");
fidelity::event(FidelityClass::Persistence, "remote-persist-failed", || format!("{error:#}"));
Expand Down Expand Up @@ -3951,7 +3951,7 @@ impl CrdtRuntime {
let projection_was_current = self.projection.frontier == from_frontier.encode();
static BEGIN_PROBE: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
let begin_probe = *BEGIN_PROBE.get_or_init(|| std::env::var_os("FLOWSTATE_CHECKPOINT_PROBE").is_some());
let probe_t = std::time::Instant::now();
let probe_t = instant::Instant::now();
flowstate_document::touch_document_metadata(&self.doc).map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?;
flowstate_document::record_revision(&self.doc, revision_id, revision_frontier, title, "Explicit save", self.author_user_id)
.map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?;
Expand All @@ -3961,12 +3961,12 @@ impl CrdtRuntime {
let mut revision_invalidation = ProjectionInvalidation::default();
self.merge_subscription_invalidation(&mut revision_invalidation);
let probe_commits = probe_t.elapsed();
let probe_t = std::time::Instant::now();
let probe_t = instant::Instant::now();
let update = self
.local_update_bytes(&from_vv)
.map_err(|error| io::Error::other(error.to_string()))?;
let probe_export = probe_t.elapsed();
let probe_t = std::time::Instant::now();
let probe_t = instant::Instant::now();
let mut events = Vec::new();
if !update.is_empty() {
let event_update = update.clone();
Expand Down Expand Up @@ -7199,8 +7199,8 @@ fn write_comment_message(
}

fn unix_time_secs() -> i64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
web_time::SystemTime::now()
.duration_since(web_time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs()
.try_into()
Expand Down Expand Up @@ -7583,7 +7583,7 @@ impl CheckpointJob {
static CHECKPOINT_PROBE: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
let probe = *CHECKPOINT_PROBE.get_or_init(|| std::env::var_os("FLOWSTATE_CHECKPOINT_PROBE").is_some());
let result = (|| -> io::Result<bool> {
let probe_t = std::time::Instant::now();
let probe_t = instant::Instant::now();
// §A13.4.4: reconstruct the checkpoint doc from the package when no
// fork was supplied (shallow chunk fast path, full decode fallback) —
// and verify it landed on the manifest tip before deriving anything.
Expand All @@ -7604,22 +7604,22 @@ impl CheckpointJob {
},
};
let probe_derive = probe_t.elapsed();
let probe_t = std::time::Instant::now();
let probe_t = instant::Instant::now();
self
.package
.replace_assets_from_document(&self.projection)?;
let probe_assets = probe_t.elapsed();
// §act-five P9: ONE materialization feeds both the projection cache and the
// search units (was two full `document_from_loro` walks per checkpoint).
let probe_t = std::time::Instant::now();
let probe_t = instant::Instant::now();
// §A13.4.1: the job carries the runtime's maintained projection —
// build the cache payload from IT when frontier-current (from-Loro
// walk only on mismatch).
self
.package
.rebuild_caches_from_projection(&fork, &self.projection)?;
let probe_caches = probe_t.elapsed();
let probe_t = std::time::Instant::now();
let probe_t = instant::Instant::now();
// §act-twelve A12.4.1 incremental persistence: routine revision
// checkpoints (explicit save + debounced autosave) skip the
// full-history snapshot export while the update-segment chain is
Expand All @@ -7639,7 +7639,7 @@ impl CheckpointJob {
self.package.refresh_shallow_snapshot(&fork);
}
let probe_compact = probe_t.elapsed();
let probe_t = std::time::Instant::now();
let probe_t = instant::Instant::now();
if self.record_revision {
self.package.create_named_revision_at_with_id(
&fork,
Expand All @@ -7653,7 +7653,7 @@ impl CheckpointJob {
}
let probe_revision = probe_t.elapsed();
if let Some(path) = &self.write_path {
let probe_t = std::time::Instant::now();
let probe_t = instant::Instant::now();
self.package.write(path)?;
if probe {
eprintln!(
Expand Down
32 changes: 9 additions & 23 deletions crates/flowstate-collab/src/doc_io.rs
Original file line number Diff line number Diff line change
Expand Up @@ -431,12 +431,6 @@ fn io_loop(core: &Arc<WriteGate<CrdtRuntime>>, receiver: &Receiver<IoRequest>) {
// session — part of the receiving peer's 13.1 GB/run import churn.
let mut unfreed_import_bytes: usize = 0;
const IMPORT_CACHE_FREE_BYTES: usize = 4 * 1024 * 1024;
// §perf-heaven T8.18 (first cold import): warm the retained import calculator's
// trackers ONCE when this doc first shows collaboration activity, so the first
// remote import reuses the built index rather than paying the cold tracker
// build synchronously on the receive path. Gated so a single-user doc never
// pays for it.
let mut import_calc_warmed = false;
loop {
let request = if let Some(request) = deferred.pop() {
request
Expand Down Expand Up @@ -536,23 +530,15 @@ fn io_loop(core: &Arc<WriteGate<CrdtRuntime>>, receiver: &Receiver<IoRequest>) {
}
},
IoRequest::PumpPublish { reply } => {
let result = core
.lock(GateHolder::ExportUpdates)
.map(|mut guard| {
// §perf-heaven T8.18: the first publish marks this doc as an ACTIVE
// collaboration participant. Warm the retained import calculator now
// (off the receive path) so the FIRST remote import reuses the
// id_to_cursor index instead of building it cold. Safe: a stale warmed
// tracker is rebuilt by `start_tracking`'s validity guard, so warming
// can only save work, never change a computed diff.
if !import_calc_warmed {
guard.doc().inner().warm_import_diff_calculator();
import_calc_warmed = true;
}
guard.take_pending_publish()
})
.map_err(|poisoned| anyhow::anyhow!(poisoned));
send_reply(&reply, result);
// Never prebuild Loro's full-history import index here. On a newly
// joined history-heavy document that "warmup" can monopolize this
// service thread for minutes while holding the write gate, making the
// window unable to process editor/layout work. Import builds only the
// state it actually needs when remote updates arrive.
send_reply(
&reply,
gate_call(core, GateHolder::ExportUpdates, |runtime| Ok(runtime.take_pending_publish())),
);
},
IoRequest::ProjectionSnapshot { reply } => send_reply(
&reply,
Expand Down
20 changes: 20 additions & 0 deletions crates/flowstate-collab/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,43 @@
//! runtime, transport protocol types, presence, and networking state.
//! Application/UI integration lives in `crates/flowstate/src/collab`.

#[cfg(not(target_family = "wasm"))]
pub mod admission;
#[cfg(not(target_family = "wasm"))]
pub mod bluetooth;
pub mod crdt_runtime;
#[cfg(not(target_family = "wasm"))]
pub mod discovery;
#[cfg(not(target_family = "wasm"))]
pub mod doc_io;
#[cfg(not(target_family = "wasm"))]
pub mod dropbox;
#[cfg(not(target_family = "wasm"))]
pub mod identity;
#[cfg(not(target_family = "wasm"))]
pub mod ids;
pub mod local_write;
#[cfg(not(target_family = "wasm"))]
pub mod net;
#[cfg(not(target_family = "wasm"))]
pub mod presence;
#[cfg(target_family = "wasm")]
#[path = "presence_core.rs"]
pub mod presence;
#[cfg(not(target_family = "wasm"))]
pub mod proto_direct;
#[cfg(not(target_family = "wasm"))]
pub mod proto_gossip;
#[cfg(not(target_family = "wasm"))]
pub mod self_check;
#[cfg(not(target_family = "wasm"))]
pub mod ticket;

#[cfg(not(target_family = "wasm"))]
pub use admission::SessionAdmission;
#[cfg(not(target_family = "wasm"))]
pub use ids::{BlobId, SessionId};
#[cfg(not(target_family = "wasm"))]
pub use proto_direct::DIRECT_ALPN;
#[cfg(not(target_family = "wasm"))]
pub use proto_gossip::PROTOCOL_VERSION;
Loading
Loading