- Rust 62.4%
- JavaScript 23.7%
- HTML 8.1%
- CSS 5.8%
|
All checks were successful
lab release / release (push) Successful in 14m52s
Consolidate the canvas editor's floating bubble (selection) menu and
in-table toolbar into the single fixed #canvas-toolbar, and add a
font-size control.
- Inline-format controls (bold/italic/.../link) and the 11 table actions
now live in the top bar; the table group is greyed/disabled unless the
cursor is in a table, so the button set never changes and the toolbar
stays a constant height (no editor-content jump entering/leaving tables).
- Remove the two floating-menu IIFEs (~120 lines) and their CSS; net -64
lines. Toolbar buttons keep selection via a single mousedown handler.
- Add font-size: export FontSize from @tiptap/extension-text-style in the
vendored bundle; Size/12-32px dropdown -> setFontSize.
Fix editor failing to load: exporting FontSize pulled a newer text-style
that moved @tiptap/extension-collaboration onto the @tiptap/y-tiptap fork
for its ySyncPlugin, while the bundle still used the deprecated
@tiptap/extension-collaboration-cursor@3.0.0 (resolves ySyncPluginKey from
upstream y-prosemirror -- a different key). The cursor plugin init read
getState()===undefined and threw "reading 'doc'", aborting editor
construction. Replace it with the official successor
@tiptap/extension-collaboration-caret (shares the y-tiptap key); update
remote-caret CSS (.collaboration-cursor__* -> .collaboration-carets__*).
Pin the whole @tiptap family to exact 3.22.3 (deps + overrides) so the
vendored bundle is deterministic and won't drift on rebuild, and restore
scripts/vendor-bundle/build.sh (referenced by README but never committed).
Verified in-browser against the running daemon: editor loads, toolbar
fixed at 71px in/out of tables, table group toggles enabled/greyed,
setFontSize('24px') applies, floating menus gone.
Closes #92.
|
||
|---|---|---|
| .cargo | ||
| .forgejo/workflows | ||
| crates | ||
| docs | ||
| scripts | ||
| .gitignore | ||
| Cargo.lock | ||
| Cargo.toml | ||
| LICENSE | ||
| PURPOSE.md | ||
| README.md | ||
| rust-toolchain.toml | ||
hero_collab
A markdown-centric team collaboration platform built as a first-class Hero OS service. Channels and threads, real-time chat with reactions, @mentions, file attachments, full-text search, collaborative canvases (CRDT via Yjs/yrs), and voice huddles via LiveKit.
Service model: hero_collab_server exposes JSON-RPC 2.0 over a Unix
domain socket (~/hero/var/sockets/hero_collab/rpc.sock), oschema-first
(generated from oschema/ via the openrpc_server! macro), with a
Server-Sent Events firehose at /events for live updates.
hero_collab_web (built from the hero_collab_web crate) serves
the HTML/JS web UI and proxies browser-originated RPC/WS
(~/hero/var/sockets/hero_collab/web.sock). Both are run by hero_proc
via their declarative service.toml manifests, typically behind
hero_proxy → hero_router for identity injection and TLS.
Quick start (local development)
Use the nushell service script (preferred):
service collab start --update --reset # build, wipe DB, seed 4 test users (dev mode)
service collab start --update # build + start, keep existing DB
service collab start # start without rebuild
service collab stop # stop all collab processes
service collab status # show running status
The --reset flag wipes ~/hero/var/data/hero_collab/collab.db* and
attachment files, then seeds a canonical 4-user fixture (Alice=id 1,
Bob=2, Carol=3, Dave=4) + one "General" workspace + one #general
channel with all 4 users as members. Alice is the channel admin.
Then open http://localhost:9988/hero_collab/web/ — that's
hero_router's port, which forwards to hero_collab_web's socket.
The user picker appears; pick any of the 4 seeded users. Open
another tab (or incognito) and pick a different user to simulate
multi-user chat.
--reset is destructive — intended for iterative dev where starting
from a known clean slate is the point. Do not run it against a DB you
care about.
Prerequisites
- Rust (edition 2024) —
rustup update stable - SQLite 3 (bundled via
rusqlite, so system SQLite isn't strictly required) - A running
hero_proc+hero_router+ optionalhero_proxy - Optional: a LiveKit server for huddles — provided by the
hero_livekitservice
Auth modes
| Mode | Behavior |
|---|---|
proxy (default) |
Expect X-Hero-User / X-Hero-Context from hero_proxy. Reject unauthenticated RPCs. |
dev |
User-picker fallback, no headers required. Logs a prominent warning at startup. Never ship to production. |
Set the mode via hero_collab_server --auth-mode=<dev\|proxy> (the
flag survives the hero_proc spawn boundary), or COLLAB_AUTH_MODE for
direct-binary launches / tests (clap's env fallback; ignored under
hero_proc's clean-env supervision). The web binary reads
COLLAB_AUTH_MODE the same way.
Architecture (4 crates)
crates/
├── hero_collab_server/ JSON-RPC service on rpc.sock (oschema-first, SQLite-backed)
├── hero_collab_web/ axum HTTP + WS relay serving the web UI on web.sock (binary: hero_collab_web)
├── hero_collab_sdk/ Auto-generated typed Rust client (via openrpc_client!)
└── hero_collab_examples/ Runnable examples + the integration test suite
Data layout:
~/hero/var/data/hero_collab/collab.db— SQLite DB (WAL mode)~/hero/var/data/hero_collab/files/— attachment blobs, namespaced by workspace then attachment id~/hero/var/logs/core/...— structured logs viahero_proc's aggregator
Wire protocol: JSON-RPC 2.0 over HTTP/1.1 over Unix Domain Socket, served
on the canonical hero_sockets surface (/api/ping, /api/{domain}/rpc,
/api/{domain}/openrpc.json, /events SSE). The schema in oschema/ is
the source of truth: the openrpc_server! macro generates the typed trait
- Input/Output types + the serving entry, emits the spec to
crates/hero_collab_server/openrpc/, andhero_collab_sdkis generated from that spec viaopenrpc_client!. ~110 methods, stable error codes persrc/rpc_error.rs.
Real-time: handlers fan out typed events to the /events SSE firehose
(fanout::dispatch); hero_collab_web consumes the firehose and de-muxes
per recipient to each browser's /ws/user/{id} WebSocket.
Key features
- Workspaces / channels / DMs — Slack topology with public + private channels and direct-message kinds.
- Messages — send, edit, delete (soft), pin, full-text search (SQLite FTS5), attachments with per-user ownership, and threaded replies.
- Reactions — atomic
message_toggle_reactreturning{action: "added"|"removed"}; no reactions on tombstoned messages. - @mentions — parsed on send, delivered via the SSE firehose
(
mention.created) and surfaced as OS-level browser notifications when the tab is backgrounded. - Canvases — Yjs/yrs CRDT-backed collaborative docs with a Tiptap editor; multi-client real-time sync over binary WS; role-gated (owner, editor, viewer).
- Voice huddles — LiveKit SFU integration; JWTs signed by
livekit.rs. Thehero_livekitservice provides the SFU. - @-mention identity — caller identity comes from
HeroRequestContext(X-Hero-User/X-Hero-Context); auser_idparam only ever names the object of an action, never the caller. - Observability —
rpc.dispatchtracing on every call; counters viasystem_metrics;/healthshows active WS connection count.
Testing
# Unit tests (rate_limit, rpc_error, validation, fanout, activity, …)
cargo test -p hero_collab_server --bin hero_collab_server
# Integration tests (spawn a real server per test, ephemeral sockets,
# exercise the /events SSE firehose)
cargo test -p hero_collab_server --test integration
# Full workspace
cargo test --workspace
Current baseline: 52 unit + 61 integration tests passing.
Operations
docs/BACKUP.md— online DB backup + restore; retention hints.- Metrics to watch:
/healthonweb.sock:active_ws_connections.system_metricsRPC:rpc_calls_total,rpc_errors_total,avg_latency_ms,workspaces,users,channels,messages.- Log stream: grep
rpc.dispatchfor per-call timing + error codes;task=huddle_reaper/task=attachment_cleanupfor background tasks;trace_id=to correlate user-reported issues with sanitized-32603 Internalresponses.
Browser support
Minimum: Chrome/Edge 100+, Firefox 100+, Safari 15.4+.
See crates/hero_collab_web/BROWSER_SUPPORT.md for the full API
coverage rationale.
Contributing
- Commit messages: one header line describing the change + a body explaining why.
- New handlers return
RpcResult<Value>, notanyhow::Result— seesrc/rpc_error.rsfor the typed-error contract. - Input validation uses the typed newtype pattern (
Name,Email,ChannelName,MessageContent, …) fromsrc/validation.rs; handlers callparse_input::<T>(params)to deserialize + validate. - Run
cargo testbefore committing; run the browser smoke when touchingchat-app.jsorcanvas-app.js.
License
See LICENSE.