* chore: unify product name display to OpenDesign repo-wide Rename the user-facing product name from "Open Design" to "OpenDesign" across documentation, web/desktop UI strings, i18n locales, prompts, plugin/skill/design-system content, changelogs, and specs, matching the landing-page rename already in flight. Deliberately excluded so installed-app identity stays untouched: - packages/release PRODUCT_NAME and everything derived from it (app bundle names, installer/registry identity, updater feeds) - tools/pack, tools/release, apps/packaged, landing page (PR #6983) - mocks/ recorded traces and LICENSE - test fixtures and comments that encode real on-disk install paths (Open Design.app, Program Files\Open Design, AppData paths) Validated with pnpm guard, pnpm typecheck, and targeted web/desktop/host test runs over the renamed strings. * fix(web): keep two-letter author monogram for CamelCase brand names authorInitials split words on whitespace only, so the renamed single-token author name "OpenDesign" produced a one-letter monogram. Treat lower-to-upper CamelCase boundaries as word breaks so the avatar fallback stays "OD". * fix(release): restore spaced Windows source artifact names in win.ps1 tools/pack still derives builder outputs from the unchanged PRODUCT_NAME ("Open Design"), so the asset script must look for the spaced filenames. Matches the mac/mac-intel/linux asset scripts, which were already kept on the original names. --------- Co-authored-by: Joey <236967869+joeylee12629-star@users.noreply.github.com>
8.2 KiB
Collab realtime — current implementation and hop 1 design history
Status: implemented on main. Owner: workspace-team.
Implemented state
Collaboration invalidations now travel over two implemented SSE hops:
- Hop 1 (vela → daemon): the daemon opens an exact-workspace cloud-hub
subscription at
/api/v1/collab/events. It verifies that thereadyevent names the requested workspace, uses heartbeats to monitor stream health, reconnects after interruptions, and performs source-gap catch-up. - Hop 2 (daemon → web): the daemon publishes thin invalidations at
/api/projects/:id/eventsand/api/workspace/events; web clients re-fetch the affected state rather than receiving full resource payloads. - Polling floor: polling remains as a slower recovery floor so missed or unavailable push events still converge.
Historical design context — July 15 proposal
The remainder of this document preserves the original July 15 design snapshot and rationale. Its proposed endpoint, event catalogue, status statements, and sequencing are historical context, not the current wire contract.
Why this exists
OpenDesign is local-first: every user runs their own daemon on their own machine, and vela (cloud) is the shared collaboration backend. Any cross-user change (user A comments on a shared project; user B should see it) travels two hops:
User A edits
│ A's web → A's daemon → vela (write path, unchanged)
▼
① vela (cloud) ─────────▶ B's daemon ← cross-user / cross-machine
│ today: B's daemon POLLS vela
▼
② B's daemon ───────────▶ B's web ← same machine
│ today: B's web POLLS B's daemon
Hop ② (daemon→web SSE) is being implemented separately (branch
feat/collab-sse). It removes the same-machine polling flood and is the local
delivery layer. But it does not change cross-user latency: hop ② still
depends on hop ① delivering the change to the daemon, and hop ① is poll-only
today. Cross-user latency is therefore floored at the daemon's vela-poll
interval (~5s for comments; on-demand for members/context).
This document specifies hop ①: a push channel from vela to the daemon, so a change made by one user reaches other members' daemons in near-real-time, replacing the daemon's polling of vela.
Historical implementation snapshot (July 15)
apps/daemon/src/collab/collab-cloud-service.tsruns an internalsetIntervalpoll (~5s) that callsclient.pullComments(...). This is the only self-driven daemon poll.- Members / workspace context / billing are fetched via the vela CLI on demand (web-request-driven, with short-lived caches).
- There is no push/subscribe path between daemon and vela. The only
subscribein the collab layer iscollab-publish-watcher, which watches local disk files (chokidar), not vela.
So hop ① is genuinely new backend surface.
Goal
vela exposes a push channel the daemon subscribes to, delivering thin invalidation events ("something of type X changed") for the teams/projects the subscribing user is a member of. The daemon reacts by re-pulling the affected resource (it already owns the pull code) and forwarding a hop-② event to its web client.
Non-goals:
- Not changing the write path (daemon→vela writes stay via the existing CLI/HTTP).
- Not pushing full payloads. Events are thin signals; the daemon re-pulls. This keeps payloads tiny, avoids server-side fan-out of large bodies, and makes a missed event during a disconnect harmless (a reconnect snapshot covers it).
Event catalogue (what vela must emit)
Scope: per team (workspace) and per shared project the subscriber can see.
Workspace-scoped:
team-projects-changed— a project was shared / unshared / created / deleted.members-changed— a member joined / left / changed role.workspace-context-changed— seat count / entitlement / context changed.billing-changed— subscription / seat billing changed.
Project-scoped (carry projectId):
comment-changed— a comment was added / edited / deleted / resolved.presence-changed— a member went online / away / offline on this project.project-metadata-changed— name / settings changed.resource-changed— published files / resources changed.
Thin event shape (proposed):
{
"type": "comment-changed",
"teamId": "team_...",
"projectId": "proj_...", // present for project-scoped events
"at": "2026-07-15T14:30:00.000Z",
"eventId": "evt_...", // optional, monotonic per team — enables Last-Event-ID
"actorId": "user_..." // optional, lets the daemon suppress echo of its own writes
}
No resource body. The daemon maps type → re-pull call.
Transport (vela to choose; SSE recommended)
Options, in preference order:
- SSE —
GET /v1/teams/{teamId}/events(text/event-stream), one-way server→daemon, HTTP/1.1-friendly, native auto-reconnect, nativeLast-Event-ID. Matches the thin-event model. Recommended. - WebSocket — bidirectional, which we do not need (writes already go via the existing path). More infra for no benefit here.
- Long-poll — acceptable fallback if SSE infra is hard; higher latency and more request churn.
Recommendation: SSE per team, multiplexing all event types over one connection.
Subscription model
- The daemon opens one SSE per authenticated team (not one per project), multiplexing every event type. This keeps the daemon's connection count to vela at one-per-team.
- Auth: reuse the vela session/token the daemon already holds for CLI calls. The endpoint authenticates the subscriber and scopes events to teams/projects that subscriber is a member of. The daemon must never receive events for projects the user cannot see.
- Server-side filtering: vela emits to a subscriber only the events for its visible teams/projects.
Catch-up / reconnect
- On (re)connect the daemon re-pulls current snapshots for its active resources (comments for open projects, member/context for the workspace). It already has this code. So a disconnect gap is closed by the snapshot — no server-side event buffer is required for correctness.
Last-Event-IDis optional: if some resource is too expensive to re-pull wholesale, vela may support replay-since-id for that type. Not required for v1.- Heartbeat: vela should emit a periodic
:pingcomment (~15s) so the daemon can detect a dead connection and reconnect.
Daemon-side changes (our side, once vela ships this)
- Add a vela-events subscriber that opens the team SSE using the daemon's vela session.
- On each event: re-pull the affected resource, then emit the corresponding
hop-② thin event to the web sinks (workspace sink / project
activeProjectEventSinks). - Keep the existing polls as a fallback floor: while the vela SSE is connected, pause/slow them; while disconnected, resume them. This is the same poll-as-floor pattern hop ② uses, and it means a daemon that cannot reach the vela SSE (old build, network) degrades to today's behavior with no regression.
- Suppress echo of the daemon's own writes using
actorIdwhere useful.
Scale / open questions for vela backend
- Can vela expose a per-team SSE endpoint authenticated by the existing daemon session token?
- What change granularity can vela emit? Does it have a change-feed / can it hook the write path to publish these events, or would it need its own internal poll?
- Connection budget: many member daemons subscribe per team simultaneously. Is a long-lived SSE per daemon acceptable at vela's scale, or is long-poll / a shared fan-out layer preferred?
- Presence specifically: presence is high-churn. Is a coalesced/debounced
presence-changed(e.g. at most one per project per N seconds) acceptable to keep event volume down?
Sequencing
- Hop ② (daemon↔web SSE) ships first, independent of this — it fixes the local
flood and is the delivery layer this feeds. It is gated on the packaged
net.fetchproxy fix (SSE is buffered by the old undici proxy). - Hop ① (this doc) turns cross-user collab real-time once vela ships the push channel and the daemon subscriber replaces the polls.