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.
6.8 KiB
Technical Implementation
OpenCode Dependency
Server and UI use the same reviewed experimental @opencode-ai/client release. Import the generated Promise client from @opencode-ai/client. The runtime CLI is independently updated and startup validates service health and API compatibility without requiring the client package's exact version string. Every upgrade must review OpenCode release notes, current documentation, installed declarations, and proxy/API parity.
Do not add @opencode-ai/sdk, old { data, error } SDK wrappers, createOpencodeClient(), or a packages/opencode-plugin package. Verify method signatures in node_modules/@opencode-ai/client/dist/promise/.
Server Integration
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.
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:
await client.location.get({ location: { directory } })
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
packages/ui/src/lib/sdk-manager.ts constructs clients with OpenCode.make() at /workspaces/:id/instance/. Use getRootClient(instanceId) from packages/ui/src/stores/opencode-client.ts; pass native directory/location inputs when required.
Session actions use native APIs directly:
await client.session.prompt({ sessionID, text, files })
await client.session.shell({ sessionID, command })
await client.session.instructions.entry.put({ sessionID, key, value })
Shell mode and conversation instructions are upstream features and remain separate from native background Shells and interactive V2 PTYs. None requires a CodeNomad plugin.
Native background Shells are location-scoped and listed in the Status panel. packages/ui/src/stores/shell-store.ts refreshes the list on native Shell events and reconnect, exposes native metadata, and supports removal. The proxy verifies Shell cwd ownership before ID-scoped operations and forwards native output cursors unchanged. Interactive pty.* terminals remain separate.
Routing And Security
- CodeNomad operations:
packages/ui/src/lib/api-client.ts->/api/*. - OpenCode operations: generated client ->
/workspaces/:id/instance/api/*. - Browser events:
GET /api/events; heartbeat response:POST /api/client-connections/pong. - The proxy exposes only an explicit method/path allowlist, checks client-provided directories and prompt files, defaults safe requests to the workspace location, and verifies session ownership before forwarding. New OpenCode routes are unavailable until reviewed and allowlisted.
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-*.
Yolo also remains CodeNomad-owned. AutoAcceptManager persists policy state, observes native permission events, replies with client.permission.reply, and publishes yolo.stateChanged/yolo.autoAccepted over /api/events.
Events
InstanceEventBridge consumes the one shared client.event.subscribe() iterable. It maps location-scoped events to workspace IDs and publishes instance.event through the CodeNomad EventBus. This stream is volatile: reconnection does not replay a guaranteed history, so UI stores refetch sessions and pending requests and other consumers must re-read authoritative file/config state.
Use current protocol names. Session events include session.created, session.renamed, session.moved, session.status, session.idle, session.execution.*, session.compaction.*, session.text.*, session.reasoning.*, and session.tool.*; background-process refresh events include shell.created, shell.exited, and shell.deleted; file and config invalidations are filesystem.changed and config.updated.
Current Structure
packages/server/src/
server/routes/ CodeNomad /api routes
workspaces/manager.ts workspace/location ownership
workspaces/opencode-service.ts
workspaces/instance-events.ts
workspaces/git-status.ts
workspaces/git-mutations.ts
permissions/ Yolo and permission policy
packages/ui/src/
lib/api-client.ts CodeNomad API and /api/events
lib/sdk-manager.ts native OpenCode Promise clients
stores/opencode-client.ts root client authority
stores/session-api.ts session queries/lifecycle
stores/session-actions.ts prompt, Shell, instructions
stores/shell-store.ts location-scoped native background Shell state/actions
Deleted packages/opencode-plugin, server plugin/background-process, and per-workspace runtime files are not architectural extension points and must not be restored.
Validation
- Run root typecheck or the relevant server/UI workspace typecheck.
- Run focused tests for service lifecycle, instance proxy, event bridge, Git mutations, or Yolo when changing those boundaries.
- Update server API types and UI consumers together.