Create a fresh logical workspace for every same-folder launch while preserving shared native location ownership, independent restore cancellation, bounded creation, and final-owner eviction. Treat aborted background session inventory as cancellation instead of a fatal sidebar error. Reset Virtua's index measurements when a 200-message resident page is replaced so bottom pinning targets the actual transcript end without remounting the follow controller. Refresh the V2 migration and comparison notes for the current session, worktree, provider quota, Shell, and packaging behavior. Coverage includes full server and focused UI matrices, server/UI typechecks, the UI production build, and regression tests for each failure.
14 KiB
OpenCode V2 Migration
Summary
This branch replaces CodeNomad's OpenCode V1 SDK, custom plugin, and per-workspace runtime architecture with the experimental native OpenCode V2 client and one shared OpenCode service. It intentionally provides no V1 runtime fallback.
The work grew beyond an SDK swap. It also introduces location-based ownership, native Forms and Shell resources, project-wide session pagination, reconnect reconciliation, bounded virtualized timelines, multi-window desktop state, and a content-addressed restore format.
Server and UI pin the same reviewed @opencode-ai/client beta build. The selected opencode2 CLI is managed independently: CodeNomad's updater resolves the latest published CLI beta and installs that concrete version, while startup accepts another compatible CLI after authenticated health and API validation instead of enforcing an exact version.
The incremental comparison with official OpenCode Desktop V2, including closed findings and remaining gaps, is recorded in DESKTOP_V2_COMPARISON.md.
Native V2 Adoption
- Use native locations and
SessionInfo.locationas the authority for workspace, session, file, event, Shell, PTY, and Git worktree ownership. - Use native APIs for projects, sessions, messages, prompts, commands, models, agents, providers, MCP, permissions, Forms, files, VCS, instructions, Shells, and PTYs.
- Use native session lifecycle and output events, including
session.created,session.renamed,session.moved,session.status,session.idle,session.execution.*,session.compaction.*,session.text.*,session.reasoning.*, andsession.tool.*. - Use
@opencode-ai/client/solidcreateDatafor live message, tool, permission, and Form projection while preserving REST-loaded history and optimistic local sends. - Replace the legacy Question request lifecycle with native Forms. Question tool output rendering and reviewed upstream compatibility routes remain where applicable.
- Replace shell-mode prompts with native
session.shell. - Replace CodeNomad background processes with native
shell.*resources. The Status UI lists, displays bounded output for, and removes Shells; create/output/timeout routes remain available through the ownership-checked proxy. Interactivepty.*terminals remain separate. - Store voice-mode instructions with
session.instructions.entryand synchronize them before prompts, commands, and session Shell calls.
Shared Service Model
- OpenCode V2 explicitly confirms that the intended architecture is one shared process for all workspaces and clients; workload slowdowns must be profiled and fixed within that topology rather than worked around with private servers.
- Replace one OpenCode runtime per workspace with one externally owned global service in the selected host or WSL environment.
- Discover or start it through the selected CLI's official
service status,service start, andservice get passwordcommands. - Accept only bounded, authenticated loopback health endpoints and pin one service identity while connected.
- Use OpenCode's standard service registration, state, and database. CodeNomad owns no private daemon port, database, registration, or PID.
- Pass configured startup environment variables and
NODE_EXTRA_CA_CERTSonly when starting a missing service. Strip legacyOPENCODE_DBandXDG_STATE_HOMEoverrides rather than taking ownership of OpenCode storage. - Never stop the global daemon during CodeNomad shutdown. Backend shutdown clears only CodeNomad's cached connection and logical workspace state.
- Run the Linux CLI inside the selected WSL distribution and require Windows localhost forwarding. No cross-namespace PID fallback or process signaling remains.
Workspace, Location, and Tab Model
- A CodeNomad workspace is now a logical UUID-backed instance over a native OpenCode location, not an OpenCode process.
- A normal folder launch always creates a new logical instance and tab, even when the same or canonically equivalent directory is already open.
- The explicit Open action selects an existing instance instead of creating another one.
- Duplicate-folder instances share the same daemon and native location but keep independent logical IDs, tabs, selection, drafts, and view state.
- The workspace catalog is shared by the backend. Tab membership, order, active selection, SideCars, drafts, attachments, and view state are local to each native window.
- Closing a tab or window detaches only local UI state. Stop Workspace deletes the selected logical instance and evicts the native location only after its final logical owner is removed.
- Restore matches duplicate-folder tabs by normalized-path occurrence rather than collapsing them into one instance.
- Owned Git worktrees are resolved server-side and participate in the same location, request, and event-routing rules as the root directory.
Sessions, Streaming, and Reconciliation
- Query a complete project-scoped session inventory across root and worktree subpaths without one request per parent; native
globalprojects remain scoped to the selected workspace directory. - Follow native
cursor.nextvalues for session and message pagination. The proxy decodes session cursors only to validate embedded directory/project scope, strips competing selectors, and forwards the original cursor unchanged. - Hydrate only missing ancestor chains with
session.getand fetch active status for later session pages. - Load message history lazily into a replace-in-place 200-message resident window. Older, newer, oldest, and latest navigation swaps authoritative pages without accumulating the transcript, while delayed REST responses cannot overwrite newer event state.
- Route location-scoped events to every owning logical workspace and resolve locationless session, permission, Form, Shell, and PTY events through native ownership.
- Use one upstream event subscription and browser
EventSourcefor web, Electron, and Tauri. - Treat events as volatile projections, not durable history. Internal stream generations and browser reconnects trigger targeted authoritative refreshes for workspaces, sessions, active state, pending permissions/Forms, loaded messages, catalogs, and invalidated file/config state.
- Preserve the Solid projection controller across reconnects and merge live records into REST history rather than clearing usable state.
UI and Memory Optimizations
- Virtualize session lists and message timelines with
virtuato bound mounted DOM for large histories. - Preserve user-controlled scroll position, bottom-follow intent, oversized streaming hold points, and anchor-based restore across live updates.
- Keep native cursors authoritative; do not infer completion from page length.
- Bound instance logs and validate restore-state counts, IDs, paths, snapshots, string budgets, partition sizes, and graph sizes.
- Reconcile only affected resources after native events or reconnects instead of periodically reloading full message history.
- Keep optimistic prompts visible before native admission and replace temporary parts with authoritative native parts without duplicating output.
- Reduce every native compaction delta in order while keeping intermediate chunks off the renderer path; project accumulated content at the next boundary and refresh immediately when compaction starts, ends, or fails.
Forms, Permissions, and Providers
- Merge pending permissions and Forms into one ordered interruption UI while preserving their separate native reply/cancel APIs.
- Reconcile pending requests from the root, active catalog, known session, worktree, and queued Form locations after reconnect and during bounded liveness checks while sessions or prompts are active.
- Treat successful permission and Form replies or cancellations as local authority so stale events and partial scans cannot resurrect settled interruptions; failed mutations trigger authoritative reconciliation.
- Carry location for global Forms through the proxy without inventing a synthetic session.
- Support native provider API-key, OAuth, command, and interactive Form authentication, including dynamic required fields and custom choices.
- Expose read-only quota usage for the supported provider registry, including xAI, Claude, Command Code, CrofAI, DeepSeek, and NeuralWatt, without returning, refreshing, or mutating provider credentials.
- Keep Yolo policy server-owned: persist enabled session families, inherit policy across descendants, deduplicate duplicate-instance delivery by permission ID, retry within a fixed bound, and synchronize state to every window.
Worktree Safety
Before deleting a Git worktree, CodeNomad now:
- Resolves and fences the canonical physical worktree identity across nested paths, aliases, junctions, symlinks, and WSL paths.
- Rejects new session mutations and drains already admitted mutations through their upstream response, failing closed if the bounded drain cannot complete.
- Resolves the native project and inventories every session with native cursors.
- Selects sessions whose native location belongs to the worktree.
- Refuses deletion while affected sessions are active.
- Moves affected sessions to the root location.
- Re-inventories until the moves are authoritative.
- Removes the Git worktree inside the same rollback boundary.
- Restores moved sessions if verification or deletion fails.
Git status, diff, stage, unstage, commit, worktree creation, and worktree removal remain CodeNomad server operations where V2 does not provide equivalent transactional behavior.
Proxy and Security Boundaries
- Expose only reviewed method/path pairs; new upstream APIs are unavailable until explicitly allowlisted.
- Verify workspace ownership for native locations, sessions, projects, cursors, Shell/PTY CWDs, imported session locations, and prompt file URIs before forwarding.
- Reject encoded path traversal, foreign locations/projects, forged cursors, and browser-supplied workspace selectors.
- Translate host/WSL paths only after ownership validation.
- Strip CodeNomad cookies, browser authorization, forwarding headers, and incoming
x-opencode-*headers; inject shared-service authentication server-side. - Block upstream cookies and authentication challenges and avoid logging unredacted secret-bearing request bodies.
- Sandbox native SideCar/browser previews without
allow-same-origin; native hosts do not inspect embedded cross-origin DOM.
Desktop and Restore Restructuring
- Run one native singleton and one CodeNomad backend per channel/config profile. Stable, development, and non-default config identities use isolated singleton, browser-storage, backend, and client-state scopes.
- Focus the most-recent local window on a second launch unless
--new-windowis supplied. - Give each Electron or Tauri window a UUID and independent tab/restore record while sharing the backend and global OpenCode data.
- Persist one record per window in a V3 envelope over a V2 content-addressed partition graph.
- Split workspace/session documents and chunk attachments so unrelated state does not rewrite one monolithic snapshot.
- Validate hashes, canonical JSON, allowed fields, graph references, and size/count limits. A corrupt leaf can be discarded while valid sibling state survives.
- Prepare immutable partitions before atomically publishing the root; serialize and fence writes against ownership loss, renderer-token mismatch, shutdown, and migration races.
- Coordinate Electron/Tauri ownership with participant markers, process-start identity, stale-owner recovery, and verified release.
- Copy legacy Electron/Tauri client state non-destructively on first migration and refuse to overwrite unsupported future formats.
- Store the stable/default cross-host state under
~/.codenomad/client-state/v2; development and non-default profiles use derived profile-specific locations. - Restore every persisted UUID window, exact active tab/session selection, drafts, attachments, expansion, scroll/follow state, idle markers, interrupted generations, bounds, and zoom.
- Fence late workspace creation and cleanup so cancelled restore requests cannot leak or delete the wrong logical instance.
- Build Electron and Tauri server resources reproducibly from the integrity-pinned root workspace lock for the requested OS/CPU target; no independent server lockfile or prebuild dependency repair remains.
Removed Legacy Architecture
The migration deletes rather than maintains these superseded systems:
- The complete
packages/opencode-pluginpackage, its packaging script, desktop resources, setup hooks, environment plumbing, and plugin README. - Plugin POST/SSE channels, handlers, voice synchronization routes, and the custom plugin-to-CodeNomad event bridge.
- Per-workspace OpenCode runtime processes, loopback servers, clients, authentication, binary selection, launch cleanup, process identity, process-tree signaling, and runtime tests.
- The
.codenomad/worktreeMap.jsonmapping layer and UI-side OpenCode workspace/worktree-client matching. - The custom background-process manager, persistence, HTTP routes, and UI store; native Shell listing, bounded output display, and removal replace them.
- Legacy Question queues, request event handling, state, components, and tests, replaced by native Forms.
- The V1 message/delta buffer and periodic full-history event reload strategy, replaced by native events plus authoritative reconciliation.
- The duplicate Rust-native Tauri SSE transport, including batching, coalescing, cookie forwarding, pong handling, reconnect code, commands, managed state, and tests.
- The desktop native-event adapter made unnecessary by the shared browser
EventSourcepath. - Message/part deletion controls and compatibility companions for operations not offered by the V2 beta protocol.
- The server and UI dependency on
@opencode-ai/sdkand the runtime V1 compatibility path.
Review Notes
- The generated V2 client remains experimental. Review its installed declarations and release notes whenever the pinned client advances; public
@opencode-ai/sdkexamples are not authoritative for this branch. - Upgrade references: OpenCode releases, OpenCode V2 documentation, and
node_modules/@opencode-ai/client/dist/promise/. - This branch intentionally has no OpenCode V1 fallback or private OpenCode database.