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

@ -18,11 +18,13 @@ description: |
- The only OpenCode client dependency is the experimental `@opencode-ai/client` protocol. Server and UI stay on the same reviewed release; the selected runtime CLI is independently updated and validated through service/API compatibility rather than an exact version gate. Current public `@opencode-ai/sdk` docs describe a different contract.
- Do not use `@opencode-ai/sdk`, `@opencode-ai/sdk/v2/client`, or `createOpencodeClient()`; follow installed `@opencode-ai/client` declarations.
- There is no `packages/opencode-plugin/`. Do not restore plugin tools, plugin routes, or plugin packaging.
- The server owns one shared OpenCode service through `OpenCodeSharedService` and its lease-locked discovery, launcher, process-proof, and authenticated-stop lifecycle. Proven host shutdown delegates to native `Service.stop`; WSL uses native authenticated health stop to avoid the client's cross-namespace PID fallback. Workspaces are native OpenCode `Location`/directory scopes, not separate OpenCode processes.
- The server uses the selected host or WSL CLI's official `service status`, `service start`, and `service get password` lifecycle to connect to one externally owned global OpenCode daemon. It owns no private port/database/registration/PID and never stops the daemon on backend shutdown. WSL requires Windows localhost forwarding and uses no cross-namespace PID operations.
- The UI uses generated Promise clients from `OpenCode.make()` through the CodeNomad proxy.
- OpenCode owns session APIs, session Shell (`client.session.shell`), session instructions (`client.session.instructions.entry`), location-scoped background Shells, and interactive PTYs. The Status panel lists `client.shell.*` records, refreshes on Shell events/reconnect, displays native metadata, and supports ownership-checked removal. Interactive `client.pty.*` terminals remain separate.
- CodeNomad owns workspace lifecycle, directory authorization, Git status/diff/stage/unstage/commit, Yolo persistence/auto-replies, and `/api/events`.
- V2 service startup forces `OPENCODE_DB` to `~/.local/share/opencode2/opencode.db`; never share the V1 database with V2.
- CodeNomad owns explicit Stop Workspace eviction, directory authorization, Git status/diff/stage/unstage/commit, Yolo persistence/auto-replies, and `/api/events`. Tab/window close only detaches local UI and never evicts.
- OpenCode owns the global daemon's standard state and database. Allowed configured environment variables apply only to `service start` for a missing daemon; an existing daemon is unchanged, and `OPENCODE_DB`/`XDG_STATE_HOME` ownership settings are ignored.
- Native desktop identity is channel plus config profile: one singleton process/backend per profile, multiple UUID windows, second-launch focus by default, and `--new-window` for another window. Stable/dev/non-default profiles isolate native state; OpenCode sessions/messages are shared while tabs/drafts/views are per-window.
- Client-state V3 is a per-window envelope over the V2 content-addressed partition graph with atomic publication/migration, ownership-fenced writes, and conservative post-commit GC. Native SideCar/browser previews are sandboxed without same-origin access; DOM comment inspection is web-only.
## Package Map
@ -59,7 +61,7 @@ description: |
| Avoid | Use |
|---|---|
| Public `@opencode-ai/sdk` examples | Installed experimental `@opencode-ai/client` declarations |
| One `opencode serve` per workspace | One CodeNomad-managed shared service |
| One `opencode serve` per workspace | One externally owned global daemon through the official CLI lifecycle |
| Per-worktree clients/processes | Root proxy client plus native location/directory inputs |
| Reintroducing `packages/opencode-plugin` or server plugin/background-process paths | Native session Shell/instructions, background `shell.*`, and separate interactive `pty.*` management |
| OpenCode APIs for stage/commit/Yolo policy | CodeNomad routes and managers |

View file

@ -10,7 +10,11 @@ Electron/Tauri -> CodeNomad Fastify server -> one shared OpenCode service
SolidJS UI <- /api/events <- event bridge
```
The server uses `packages/server/src/workspaces/opencode-service.ts` for a lease-locked discovery, launcher, process-proof, and authenticated-stop lifecycle. Transferable lease proof binds the registration and endpoint credentials to the daemon PID/process-start identity, host or WSL namespace, and launch signature. Proven host shutdown delegates to native `Service.stop`; WSL uses native authenticated health stop to avoid the client's cross-namespace PID fallback. `WorkspaceManager` validates each selected directory with `client.location.get()` and stores its `LocationRef`; a workspace is a logical location owner, not an OpenCode child process.
The server uses `packages/server/src/workspaces/opencode-service.ts` and the selected host or WSL CLI's official `service status`, `service start`, and `service get password` lifecycle to connect to one externally owned global daemon. It owns no private port/database/registration/PID and never stops the daemon on backend shutdown. WSL requires Windows localhost forwarding and performs no cross-namespace PID operations. `WorkspaceManager` validates each selected directory with `client.location.get()` and stores its `LocationRef`; explicit Stop Workspace evicts that location, while tab/window close only detaches local UI.
Native desktop identity is channel plus config profile: one singleton process/backend per profile, multiple UUID windows, MRU focus on second launch, and `--new-window` for another window. Stable/dev/non-default profiles isolate native/browser/client state. OpenCode sessions/messages remain shared; tabs, drafts, views, and restore membership are per-window.
Client-state V3 is a per-window envelope over the V2 content-addressed partition graph. Electron and Tauri prepare immutable partitions before atomically publishing the root, fence migration and writes on current ownership, and collect only unreferenced partitions after publication. Native SideCar/browser previews omit same-origin sandbox permission, making DOM comment inspection web-only.
## Boundaries
@ -36,7 +40,7 @@ Session Shell remains separate from background Shell and PTY management. The Sta
OpenCode location/workspace identity is upstream state. CodeNomad persists only its own preferences and policy metadata, including Yolo state.
OpenCode V2 forces `OPENCODE_DB` to `~/.local/share/opencode2/opencode.db`; V1 and V2 databases must remain separate.
OpenCode owns the global daemon's standard state and database. Allowed configured environment variables apply only when CodeNomad starts a missing daemon; an existing daemon is unchanged, and legacy `OPENCODE_DB`/`XDG_STATE_HOME` ownership variables are ignored.
## Entry Points

View file

@ -6,6 +6,14 @@ CodeNomad supports two desktop platforms:
- **Electron** (primary, mature)
- **Tauri** (emerging, Rust-based)
## Process, Window, And State Model
- Identity scope is update channel plus config profile. Each scope has one native singleton process and one CodeNomad backend; stable, dev, and non-default config profiles isolate native/browser/client state.
- A second launch focuses the MRU window unless `--new-window` requests another UUID-backed local window.
- OpenCode sessions/messages stay in the shared global daemon. Tabs, drafts, views, restore membership, and native bounds are per-window.
- Client-state V3 is a per-window envelope over the V2 SHA-256 content-addressed partition graph. Prepare immutable partitions, fence migration and writes on current ownership/renderer authority, atomically publish the root, then conservatively remove only partitions unreferenced by all windows.
- Native SideCar/browser previews are sandboxed without `allow-same-origin`; DOM comment inspection is web-only.
## Electron
### Directory Structure

View file

@ -4,10 +4,10 @@
1. UI posts a folder to `/api/workspaces`.
2. `WorkspaceManager` resolves the binary launch spec and calls the single `OpenCodeSharedService`.
3. The CodeNomad adapter discovers or launches one shared `opencode serve --service` endpoint under a lifecycle lock and records transferable registration, endpoint, launch-signature, PID, process-start, and namespace proof.
3. The adapter runs the selected host or WSL CLI's official `service status`, `service start`, and `service get password` lifecycle and validates the authenticated loopback endpoint. WSL requires Windows localhost forwarding and stays inside the Linux namespace.
4. `client.location.get` validates the directory and returns native location/workspace identity.
5. CodeNomad publishes workspace events on `/api/events` and exposes `/workspaces/:id/instance` as the authorized native API proxy.
6. Final-owner deletion queues location eviction. Proven final shared-service shutdown flushes queued evictions and sends an authenticated stop only if no live CodeNomad peer remains and the exact daemon identity still matches.
6. Explicit Stop Workspace evicts the location/resources and removes the logical workspace. Ordinary tab/window close only detaches local UI; backend shutdown clears connection state. Neither close nor shutdown stops the daemon.
## Prompt, Shell, Instructions, And PTYs

View file

@ -5,7 +5,7 @@
CodeNomad keeps the experimental `@opencode-ai/client` protocol aligned in `packages/server/package.json` and `packages/ui/package.json`. The runtime CLI is independently updated and startup validates service health and API compatibility without requiring that exact dependency version. This is distinct from the current public `@opencode-ai/sdk` documentation.
- Promise client: `import { OpenCode } from "@opencode-ai/client"`
- Service lifecycle: `import { Service } from "@opencode-ai/client/service"`
- Service authentication headers: `import { Service } from "@opencode-ai/client/service"`
- Client construction: `OpenCode.make({ baseUrl, headers?, fetch? })`
- Declarations: `node_modules/@opencode-ai/client/dist/promise/`
@ -15,7 +15,7 @@ Do not import `@opencode-ai/sdk`; its wrapper shapes, `{ data, error }` conventi
| Area | Calls | CodeNomad caller |
|---|---|---|
| Service | `Service.discover/headers`; custom launch and authenticated stop | `packages/server/src/workspaces/opencode-service.ts` |
| Service | CLI `service status/start/get password`; `Service.headers` for authenticated health/API calls | `packages/server/src/workspaces/{host,wsl,opencode-cli,opencode}-service.ts` |
| Location | `client.location.get`, `client.debug.location.evict` | shared service wrapper |
| Events | `client.event.subscribe()` | `packages/server/src/workspaces/instance-events.ts` |
| Sessions | `list/get/create/fork/remove/rename/prompt/command/shell/interrupt` | UI session stores |

View file

@ -12,14 +12,14 @@
- A CodeNomad workspace must validate through `client.location.get` before becoming ready.
- Directory-bearing proxy input is untrusted and must resolve to the workspace root or one of its Git worktrees.
- Session ID alone is insufficient: the proxy fetches the session and verifies `session.location.directory`.
- Queue eviction after the final logical owner is deleted; flush it only during proven final shared-service shutdown.
- Explicit Stop Workspace evicts the native location/resources and removes CodeNomad's logical workspace. Ordinary tab/window close only detaches local UI and never evicts.
## Shared Lifecycle
- There is one shared service, client and upstream event subscription. CodeNomad keeps lease and process-identity proof around lifecycle operations, delegates proven host shutdown to native `Service.stop`, and uses native authenticated health stop for WSL.
- A workspace stop removes location ownership; it does not stop a dedicated OpenCode process.
- Transferable proof records registration/credentials, daemon PID and process-start identity, host/WSL namespace, and launch signature. Shutdown stops only after no live peer remains and the proof still identifies the exact daemon.
- V2 forces `OPENCODE_DB` to `~/.local/share/opencode2/opencode.db`; V1/V2 schemas must not share a database.
- There is one externally owned global service, one server client and one upstream event subscription. CodeNomad uses official host/WSL CLI status/start/password commands, owns no private service state or PID, and never stops the daemon on backend shutdown.
- WSL requires Windows localhost forwarding, executes lifecycle commands inside Linux, and never uses cross-namespace PID operations.
- A workspace stop evicts its location; it does not stop a dedicated process or the global daemon.
- OpenCode owns standard state/database. Allowed configured environment variables apply only when starting a missing daemon; existing daemons are unchanged, and `OPENCODE_DB`/`XDG_STATE_HOME` are ignored.
- The native event stream is volatile. Reconnect must reconcile authoritative state; use current `session.*`, `filesystem.changed`, and `config.updated` names rather than obsolete event aliases.
## Ownership Matrix
@ -29,7 +29,7 @@
| Session/message/Shell/instructions | OpenCode native API; session Shell remains separate from background Shell and PTY management |
| Background Shell list/metadata/output/remove | Location-scoped OpenCode native API through CodeNomad ownership checks; Status UI refreshes on Shell events/reconnect |
| Interactive PTYs | Separate native `pty.*` API |
| Service discovery/start/stop | CodeNomad hardened adapter using selected OpenCode primitives |
| Service status/start/password | CodeNomad adapter using the selected host or WSL CLI; daemon stop remains external |
| Workspace and directory authorization | CodeNomad |
| Git status/diff and mutations | CodeNomad |
| Yolo policy/persistence/auto-reply | CodeNomad |

View file

@ -2,15 +2,13 @@
## Shared Service
`WorkspaceManager` owns one `OpenCodeSharedService`. Production discovers an existing endpoint or launches one with CodeNomad's detached launcher. The wrapper creates one server-side Promise client, performs health checks, invalidates failed connections, and calls native `Service.stop` for a proven host daemon. WSL uses native authenticated health stop so no Windows PID fallback can run.
`WorkspaceManager` owns one `OpenCodeSharedService`. Production runs the selected host or WSL CLI's official `service status`, `service start`, and `service get password` lifecycle, validates the authenticated loopback endpoint, creates one Promise client, and invalidates failed connections. It owns no private port/database/registration/PID and never stops the daemon on backend shutdown. WSL requires Windows localhost forwarding and performs no cross-namespace PID operations.
Lifecycle leases serialize processes and carry transferable proof: registration and endpoint credentials, daemon PID/process-start identity, host/WSL namespace, and launch signature. A peer can inherit proof, but only the final verified process may send the authenticated stop and wait for that daemon to exit.
V2 forces `OPENCODE_DB` to `~/.local/share/opencode2/opencode.db`. Never point V1 and V2 at the same database. The configured/inherited environment is part of the launch signature and applies when the service starts/restarts.
OpenCode owns standard state/database. Allowed configured environment variables apply only to `service start` for a missing daemon; existing daemons are unchanged, and `OPENCODE_DB`/`XDG_STATE_HOME` ownership variables are ignored.
## Locations And Directories
Workspace creation calls `client.location.get({ location: { directory } })` and records the returned directory/workspace ID. Final-owner deletion queues `client.debug.location.evict`; proven final shared-service shutdown flushes it after excluding live peers.
Workspace creation calls `client.location.get({ location: { directory } })` and records the returned directory/workspace ID. Explicit Stop Workspace calls `client.debug.location.evict` before removing the logical workspace. Ordinary tab/window close only detaches local UI and never evicts.
The instance proxy is method/path allowlisted, rejects unowned paths, `directory`, `location.directory`, and `location[directory]` values, and verifies session location before forwarding. Keep this check at the server trust boundary; new upstream routes require explicit review.

View file

@ -10,10 +10,11 @@
## OpenCode Service
- Use `OpenCodeSharedService` in `packages/server/src/workspaces/opencode-service.ts`.
- Keep one CodeNomad-managed shared-service lifecycle and one event subscription for all workspaces. Production lifecycle is custom and process-proofed; do not replace it with direct `Service.ensure`/`Service.stop`.
- Keep one shared-service adapter and one event subscription for all workspaces. Use the selected host or WSL CLI's official status/start/password lifecycle, own no private service state/PID, and never stop the externally owned global daemon on backend shutdown.
- Model workspaces with native `LocationRef`/directories in `packages/server/src/workspaces/manager.ts`.
- Never spawn or stop OpenCode per workspace and never add plugin installation/packaging.
- Force V2 `OPENCODE_DB` to `~/.local/share/opencode2/opencode.db` and never share the V1 database with V2.
- Explicit Stop Workspace evicts the location; ordinary UI close never calls workspace deletion. WSL requires localhost forwarding and no cross-namespace PID operations.
- Leave global service state/database ownership to OpenCode. Pass allowed environment only when starting a missing daemon; leave an existing daemon unchanged and ignore `OPENCODE_DB`/`XDG_STATE_HOME`.
## Trust Boundaries