feat(v2): unify global service and desktop windows

Use the official host and WSL OpenCode service lifecycle instead of private daemon ownership. Workspace deletion now evicts only the selected location while shared sessions, agents, messages, and executions remain available to other windows.

Add profile-scoped singleton multi-window support for Electron and Tauri, isolate local window UI state in a durable partition graph, and preserve migration, fencing, bounded persistence, and cross-host ownership semantics. New Window and New Instance are exposed together in the native Window menu.

Harden renderer authority, remote profiles, SSE identity, idle attention, git-status concurrency, SideCar sandboxing, shutdown, generated Tauri ACLs, documentation, and CI coverage.

Validated with server, UI, Electron, and Tauri test matrices; TypeScript checks; cargo fmt; production Electron/server/Tauri builds; diff checks; and a packaged Windows smoke covering singleton focus, --new-window, one shared backend, menu placement, and slot hash verification.
This commit is contained in:
Pascal André 2026-08-20 08:41:27 +02:00
parent 5484f9c990
commit b5f3fc6e82
No known key found for this signature in database
155 changed files with 10981 additions and 4867 deletions

View file

@ -94,10 +94,11 @@ dev-docs/ Development documentation
### 2. Shared Service Management
- CodeNomad server discovers or launches one service through its hardened lifecycle; proven host shutdown delegates to native `Service.stop`, while WSL uses native authenticated health stop
- CodeNomad uses the official host or WSL CLI lifecycle to connect to one externally owned global service
- Workspace folders become validated native locations
- UI traffic stays behind the CodeNomad proxy
- Shutdown transfers proof to a live peer or stops only the exact proven daemon when no peer remains
- Shutdown clears only CodeNomad's in-memory connection state and never stops the daemon
- Explicit Stop Workspace evicts its location; tab/window close only detaches local UI
### 3. One Shared Service, Location-Scoped Clients
@ -165,9 +166,11 @@ dev-docs/ Development documentation
## Current OpenCode Baseline
- Experimental protocol client: server and UI use the same reviewed version; the runtime `opencode2` CLI is independently updated and checked through service/API compatibility, not an exact version gate
- Service: one shared endpoint managed by CodeNomad's lease-locked process-proof lifecycle
- Service: one externally owned global endpoint with a CodeNomad-pinned host or WSL identity
- Workspaces: native locations/directories
- Database: V2 always uses `~/.local/share/opencode2/opencode.db`, separate from V1
- Database: OpenCode's global default; configured startup environment applies only when CodeNomad starts a missing daemon
- Desktop: one singleton process/backend per channel/config profile, multiple UUID windows, and isolated stable/dev/non-default native state
- Restore: V3 per-window envelope over a V2 content-addressed partition graph; OpenCode sessions/messages are shared while tabs/drafts/views are local
- Events: volatile native stream with authoritative reconnect reconciliation
- Proxy: explicit method/path allowlist; upstream additions are not automatic
- Shell mode and instructions: native session APIs, separate from background Shell and PTY management

View file

@ -15,19 +15,26 @@ There is no `@opencode-ai/sdk` integration and no `packages/opencode-plugin` pac
## Shared Service And Locations
`packages/server/src/workspaces/opencode-service.ts` uses native discovery and headers while retaining a custom launcher that serializes lifecycle changes with cross-process leases, records the registration and authenticated endpoint, proves daemon and CodeNomad PIDs with process-start identity in the host or WSL namespace, and binds that proof to a launch command/environment hash. Live peer leases can inherit that proof; only the final verified CodeNomad process may call `Service.stop`. WSL daemons use the same authenticated graceful-stop request instead because the published fallback signals PIDs in the caller's namespace.
`packages/server/src/workspaces/opencode-service.ts` runs the selected host or WSL CLI's official `service status`, `service start`, and `service get password` lifecycle, validates the authenticated loopback endpoint, and pins that identity while active. It connects to one externally owned global daemon and never stops it on backend shutdown. CodeNomad owns no private daemon port, database, registration, or PID.
The V2 service always uses `~/.local/share/opencode2/opencode.db`. V1 and V2 must use separate databases because their schemas are incompatible.
OpenCode owns the daemon's standard state and database. Configured allowed environment variables and `NODE_EXTRA_CA_CERTS` apply only if CodeNomad starts a missing daemon; an existing daemon is unchanged, and legacy `OPENCODE_DB`/`XDG_STATE_HOME` ownership settings are ignored. WSL support requires Windows localhost forwarding and executes the Linux CLI lifecycle inside the selected distribution without cross-namespace PID operations.
`packages/server/src/workspaces/manager.ts` treats selected folders as native OpenCode locations:
1. Validate the directory with `client.location.get`.
2. Store the returned `LocationRef` and publish the logical workspace.
3. Reuse the shared service for every additional directory.
4. Queue eviction after the final logical owner is deleted.
5. Flush queued evictions only during proven final shared-service shutdown, then stop only the exact daemon covered by transferable CodeNomad process proof.
4. On explicit **Stop Workspace**, evict the location and its resources from the global service, then remove CodeNomad's logical workspace.
Workspaces are not OpenCode processes and do not own ports or PIDs.
Workspaces are not OpenCode processes and do not own ports or PIDs. Closing an ordinary tab or native window only detaches local UI state and never evicts the location.
## Native Profiles, Windows, And Client State
Electron and Tauri run one native singleton process and one CodeNomad backend per channel/config profile. A second launch focuses the most-recent window by default; `--new-window` creates another UUID-backed window. Stable, dev, and non-default config profiles isolate singleton identity, backend/browser storage, and client state.
OpenCode sessions and messages remain shared through the global daemon. Window membership, tabs, drafts, view state, and native bounds are local to each UUID window. Client-state V3 is a per-window envelope over the V2 content-addressed partition graph: immutable partitions are prepared before atomic root publication, writes and migrations are fenced by current ownership, and garbage collection runs after publication while retaining every partition referenced by any window.
Native SideCar/browser previews use a sandbox without `allow-same-origin`, so they cannot inspect the embedded DOM. DOM comment inspection is available only in the web client.
## API Boundaries

View file

@ -8,11 +8,9 @@ Do not add `@opencode-ai/sdk`, old `{ data, error }` SDK wrappers, `createOpenco
## Server Integration
`OpenCodeSharedService` is the sole service adapter. Production uses `Service.discover` and `Service.headers`, then a custom launcher with lease and process-identity proof. Proven host shutdown delegates to native `Service.stop`; WSL uses native authenticated health stop to avoid the client's Windows PID fallback.
`OpenCodeSharedService` is the sole service adapter. Host and WSL paths both execute the selected CLI's official `service status`, `service start`, and `service get password` commands, validate the authenticated loopback endpoint, pin its identity while active, and never stop the externally owned global daemon on backend shutdown.
Startup and shutdown are serialized by filesystem leases. Each CodeNomad process proves its own PID/start identity and launch signature; service proof contains the registration contents, endpoint credentials, daemon PID/start identity, and host/WSL namespace. On exit, an owner transfers that proof to an elected live peer and releases its lease; a replacement can also inherit matching proof from a stale peer under the lifecycle lock. The final process stops only after all peers are proven stale/absent and the registration, endpoint, process identity, and launch signature still match; uncertainty retains the lease and leaks safely rather than signaling a PID.
The V2 service database is fixed at `~/.local/share/opencode2/opencode.db`; V1 and V2 schemas must never share a database. The complete environment is part of the launch signature and takes effect on service start/restart, not on an already-running daemon.
OpenCode owns the daemon's standard state, database, and registration; CodeNomad has no private port, database, registration, or daemon PID. Allowed configured environment variables and the current `NODE_EXTRA_CA_CERTS` are passed only to `service start` for a missing daemon. Existing daemons are unchanged, and `OPENCODE_DB`/`XDG_STATE_HOME` are ignored. WSL requires Windows localhost forwarding and runs this lifecycle inside Linux without Windows PID operations.
Workspace creation passes a native location:
@ -20,7 +18,13 @@ Workspace creation passes a native location:
await client.location.get({ location: { directory } })
```
`WorkspaceManager` records the returned directory/workspace ID. After the final logical owner is removed, eviction is queued and is sent only during proven final shared-service shutdown, after cross-process peer and daemon identity checks.
`WorkspaceManager` records the returned directory/workspace ID. Explicit **Stop Workspace** evicts that location/resources and removes the logical workspace without stopping the global daemon. Ordinary tab/window close only detaches local UI state and does not call the delete/eviction path.
## Native Windows And Restore State
Each channel/config profile has one native singleton process and one backend. A second launch focuses the MRU window unless `--new-window` requests another UUID window; stable, dev, and non-default config profiles use isolated native/browser/client-state scopes.
OpenCode sessions/messages are shared service data. Each window separately persists tab membership, drafts, view state, and native bounds in the client-state V3 envelope. Snapshot V2 is a SHA-256 content-addressed partition graph. Electron and Tauri prepare immutable partitions, fence migration/root replacement on current ownership and renderer authority, atomically publish the envelope, then conservatively sweep partitions no window references.
## UI Integration
@ -47,6 +51,8 @@ Native background Shells are location-scoped and listed in the Status panel. `pa
Never trust a browser-supplied worktree path. Resolve workspace/worktree ownership server-side.
Native SideCar/browser preview iframes are sandboxed without `allow-same-origin`; DOM comment inspection is therefore web-only.
## CodeNomad-Owned Mutations
Git status/diff and mutations remain CodeNomad APIs. Stage, unstage and commit execute validated Git commands in `packages/server/src/workspaces/git-mutations.ts`; the UI calls `/api/workspaces/:id/worktrees/:slug/git-*`.