diff --git a/docs/design/app-v2-api-migration.md b/docs/design/app-v2-api-migration.md new file mode 100644 index 0000000000..309b62663e --- /dev/null +++ b/docs/design/app-v2-api-migration.md @@ -0,0 +1,428 @@ +# V2 Desktop and Web API Migration + +## Decision + +Desktop and web should move from the legacy `@opencode-ai/sdk` transport to +`@opencode-ai/client` and the standalone V2 server. Metadata, lifecycle, +location, integration, and project-copy state should use V2 models directly. + +The existing app timeline should remain on its V1 `Message` and `Part` shape +during this migration. A narrow app-side compatibility projection should map +V2 session messages and events into that shape. This is not runtime V1 support: +the app will call only V2 endpoints, and the standalone server will expose only +V2 APIs. The compatibility boundary exists solely to avoid rewriting the mature +timeline and message-part UI at the same time as the transport migration. + +The migration is larger than changing HTTP paths. The important contract +changes are: + +- A prompt is durable asynchronous admission, not a request that returns a + completed assistant message. +- Session messages are V2 projections on the wire. The app adapter projects + them into a legacy message plus parts without treating that shape as durable + server state. +- The global event stream carries `{ type, data, location }`, not + `{ directory, payload: { type, properties } }`. +- Location is explicit on requests and session placement is durable. +- Most mutations return `204`; read responses use `{ data }`, pagination uses + `{ data, cursor }`, and location reads use `{ location, data }`. +- Provider authentication is integration and credential management. +- Revert is a staged operation. + +This document compares the 125 legacy endpoints in `packages/opencode` with the +105 endpoints assembled by `packages/protocol/src/api.ts`. The standalone V2 +server mounts the latter through `packages/server/src/routes.ts`. + +## Current Architecture + +The browser and desktop renderer both use `packages/app`. The app currently: + +- creates `@opencode-ai/sdk/v2/client` clients in + `packages/app/src/utils/server.ts`; +- creates implicit directory-scoped clients in + `packages/app/src/context/server-sdk.tsx`; +- consumes the legacy `/global/event` envelope; +- stores legacy `Session`, `Message`, and `Part` values in + `packages/app/src/context/server-session.ts` and global sync; +- submits with `/session/:id/prompt_async` and observes legacy message-part + events. + +The `v2` import path in `@opencode-ai/sdk` is the generated client for the V1 +server API. It is not the client for the V2 architecture. The V2 Promise client +is exported by `@opencode-ai/client` from `packages/client/src/promise`. + +Desktop embeds the same app but starts the V1 backend: + +- `packages/desktop/electron.vite.config.ts` bundles + `packages/opencode/dist/node/node.js`; +- `packages/desktop/src/main/sidecar.ts` calls legacy `Server.listen`; +- `packages/desktop/src/main/server.ts` probes `/global/health`. + +The V2 process entry is `packages/server/src/process.ts`. It requires a +password, exposes `/api/health`, and returns an address plus a shutdown effect. + +## Complete Endpoint Comparison + +The tables group every public endpoint by domain. `Direct` means the app can +adopt the V2 operation after request and response shape changes. `Redesign` +means the user workflow exists but its semantics or state model changed. +`Gap` means V2 has no corresponding operation and a product or protocol +decision is required. + +### Server, Configuration, and Discovery + +| V1 endpoints | V2 endpoints | Result | +| --- | --- | --- | +| `GET /global/health` | `GET /api/health` | Direct. V2 adds `pid`. | +| `POST /global/dispose`, `POST /instance/dispose` | `POST /api/service/stop` | Redesign. V2 stops a managed instance by `instanceID`; it does not expose location disposal as an app command. | +| `POST /global/upgrade` | None | Gap. Desktop updater should own application upgrades; remote server upgrade needs a separate decision. | +| `GET/PATCH /global/config`, `GET/PATCH /config` | None | Gap. Configuration editing is used throughout settings and provider UI. Add a V2 config group or remove those controls. | +| `GET /path` | `GET /api/location` | Redesign. V2 returns placement identity, not host config/state/home paths. | +| None | `GET /api/server` | New. Returns advertised server URLs. | +| None | `GET/DELETE /api/debug/location` | New diagnostics; not an app bootstrap dependency. | +| `POST /log` | None | Gap. Client telemetry should use its own transport rather than a compatibility route. | + +### Catalog, Providers, and Integrations + +| V1 endpoints | V2 endpoints | Result | +| --- | --- | --- | +| `GET /agent` | `GET /api/agent` | Direct. | +| `GET /command` | `GET /api/command` | Direct. | +| `GET /skill` | `GET /api/skill` | Direct. | +| None | `GET /api/plugin`, `GET /api/reference` | New catalogs. | +| `GET /provider`, `GET /config/providers` | `GET /api/provider`, `GET /api/provider/:providerID`, `GET /api/model`, `GET /api/model/default` | Redesign around separate provider and model catalogs. | +| `GET /provider/auth`, `POST /provider/:id/oauth/authorize`, `POST /provider/:id/oauth/callback`, `PUT/DELETE /auth/:id` | Integration and credential routes below | Redesign. Do not carry legacy auth calls forward. | +| None | `GET /api/integration`, `GET /api/integration/:id` | New integration catalog and connection state. | +| None | `POST /api/experimental/integration/wellknown` | New discovery operation. | +| None | `POST /api/integration/:id/connect/key` | Replaces direct API-key storage. | +| None | `POST /api/integration/:id/connect/oauth`, `GET/DELETE /api/integration/:id/connect/oauth/:attemptID`, `POST /api/integration/:id/connect/oauth/:attemptID/complete` | Replaces legacy OAuth with explicit attempts. | +| None | `POST /api/integration/:id/connect/command`, `GET/DELETE /api/integration/:id/connect/command/:attemptID` | New command-based connection flow. | +| None | `PATCH/DELETE /api/credential/:credentialID` | New credential lifecycle. | +| None | `POST /api/generate` | New stateless generation endpoint. | + +### Projects, Copies, Workspaces, and Sync + +| V1 endpoints | V2 endpoints | Result | +| --- | --- | --- | +| `GET /project`, `GET /project/current`, `GET /project/:id/directories` | Same operations under `/api` | Direct, with V2 response shapes and explicit location. | +| `PATCH /project/:id` | None | Gap. The app edits project name and icon. Add project metadata mutation if this remains a server-owned feature. | +| `POST /project/git/init` | None | Gap. Add a VCS initialization operation or remove the action. | +| `POST /experimental/project/:id/copy/generate-name` | None | Gap. Name generation can be client-side or a separate generation request. | +| `GET/POST/DELETE /experimental/worktree`, `POST /experimental/worktree/reset` | Project-copy routes below | Redesign around project copies and strategies. | +| None | `POST/DELETE /experimental/project/:id/copy`, `POST /experimental/project/:id/copy/refresh` | New project-copy model. These are currently missing the `/api` prefix and must be fixed before cutover. | +| All seven `/experimental/workspace*` endpoints | None | Gap by design unless remote workspace placement remains a product requirement. | +| All four `/sync/*` endpoints | Native V2 durable storage and location | Remove. These are V1 synchronization internals, not client API equivalents. | +| `POST /experimental/control-plane/move-session` | `POST /api/session/:id/move` | Direct at the session API level. | + +### Sessions and Messages + +| V1 endpoints | V2 endpoints | Result | +| --- | --- | --- | +| `GET /session` | `GET /api/session` | Direct with cursor pagination and different filters. | +| `GET /session/status` | `GET /api/session/active` | Redesign. V2 reports process-local active drains, not a status for every session. Idle state comes from projection and events. | +| `GET /session/:id` | `GET /api/session/:id` | Direct. | +| `GET /session/:id/children` | `GET /api/session?parentID=...` | Direct through list filtering. | +| `POST /session` | `POST /api/session` | Direct; pass durable location, agent, and model. | +| `DELETE /session/:id` | `DELETE /api/session/:id` | Direct; V2 also removes children. | +| `PATCH /session/:id` | `POST /api/session/:id/rename`, `/agent`, `/model`, `/move` | Redesign. Metadata, permission, and archive mutation have no equivalent. | +| `POST /session/:id/fork` | `POST /api/session/:id/fork` | Direct. | +| `POST /session/:id/abort` | `POST /api/session/:id/interrupt` | Redesign. Interrupt is an idle-safe, process-local operation returning `204`. | +| `POST /session/:id/init` | None | Gap. Remove AGENTS generation or add it as a named V2 command/workflow. | +| `POST/DELETE /session/:id/share` | None | Gap. Sharing needs a V2 design rather than retaining V1 session mutation. | +| `POST /session/:id/summarize` | `POST /api/session/:id/compact` | Redesign around durable compaction. | +| `POST /session/:id/message`, `POST /session/:id/prompt_async` | `POST /api/session/:id/prompt` | Redesign. V2 returns admitted pending input; completion is observed asynchronously. | +| `POST /session/:id/command` | `POST /api/session/:id/command` | Redesign request fields and delivery semantics. | +| `POST /session/:id/shell` | `POST /api/session/:id/shell` | Redesign. V2 emits shell lifecycle events and returns `204`. | +| None | `POST /api/session/:id/skill`, `/synthetic`, `/background`, `/wait` | New session operations. | +| `POST /session/:id/revert`, `/unrevert` | `POST /api/session/:id/revert/stage`, `/clear`, `/commit` | Redesign the undo UI around staged revert. | +| `GET /session/:id/message` | `GET /api/session/:id/message` | Redesign around `SessionMessage.Info[]` and body cursors. | +| `GET /session/:id/message/:messageID` | Same operation under `/api` | Direct after adopting V2 message types. | +| `DELETE /session/:id/message/:messageID`, `DELETE/PATCH .../part/:partID` | None | Remove. V2 projections are not directly mutable transcript storage. | +| `GET /session/:id/diff` | `GET /api/vcs/diff` only | Gap. Decide whether the review UI needs a session-scoped derived diff endpoint. | +| None | `GET /api/session/:id/context`, `/pending` | New projection and inbox reads. | +| None | `GET/PUT/DELETE /api/session/:id/instructions/entries[/key]` | New durable instruction entries. | +| None | `GET /api/experimental/session/:id/log` | New durable event log for diagnostics/recovery views. | + +### Permission, Question, and Form + +| V1 endpoints | V2 endpoints | Result | +| --- | --- | --- | +| `GET /permission` | `GET /api/permission/request` | Direct for cross-location bootstrap. | +| Deprecated `POST /session/:id/permissions/:permissionID` and `POST /permission/:requestID/reply` | `POST /api/session/:id/permission/:requestID/reply` | Redesign around current replies and required session identity. | +| None | `POST/GET /api/session/:id/permission`, `GET .../:requestID` | New session-scoped request API. | +| None | `GET /api/permission/saved`, `DELETE /api/permission/saved/:id` | New saved decisions. | +| `GET /question`, `POST /question/:id/reply`, `POST /question/:id/reject` | `GET /api/question/request`, `GET /api/session/:id/question`, session-scoped reply/reject | Direct after adding session identity and new types. | +| None | `GET /api/form/request`, all six `/api/session/:id/form*` operations | New structured interaction surface. | + +### Filesystem and VCS + +| V1 endpoints | V2 endpoints | Result | +| --- | --- | --- | +| `GET /file` | `GET /api/fs/list` | Direct with new entry types. | +| `GET /file/content` | `GET /api/fs/read/*` | Redesign. V2 returns bytes; the app must decode text and model binary/media content. | +| `GET /find/file` | `GET /api/fs/find` | Direct with a unified entry result. | +| `GET /find`, `GET /find/symbol` | None | Gap. Add text/symbol search only if required by current UI. | +| `GET /file/status` | `GET /api/vcs/status` | Direct. | +| `GET /vcs/status`, `GET /vcs/diff` | Same operations under `/api` | Direct. | +| `GET /vcs`, `GET /vcs/diff/raw`, `POST /vcs/apply` | None | Gap. Branch display, raw patch, and apply actions need explicit V2 endpoints or removal. | +| `GET /lsp`, `GET /formatter` | None | Gap. Status UI must be removed or supported by new read models. | + +### PTY and Shell + +| V1 endpoints | V2 endpoints | Result | +| --- | --- | --- | +| `GET/POST /pty`, `GET/PUT/DELETE /pty/:id` | Same operations under `/api` | Direct with explicit location and location envelopes. | +| `POST /pty/:id/connect-token`, `GET /pty/:id/connect` | Same operations under `/api` | Direct. Require ticket flow and remove legacy query-auth fallback. | +| `GET /pty/shells` | None | Gap. Use a client default shell or add server shell discovery. | +| None | `GET/POST /api/shell`, `GET /api/shell/:id`, `PATCH .../timeout`, `GET .../output`, `DELETE .../:id` | New background noninteractive shell API. | + +### MCP, Events, and Experimental Controls + +| V1 endpoints | V2 endpoints | Result | +| --- | --- | --- | +| `GET /mcp` | `GET /api/mcp` | Redesign around server catalog values rather than a status map. | +| `GET /experimental/resource` | `GET /api/mcp/resource` | Direct with a location envelope. | +| The seven V1 MCP add/connect/disconnect/auth endpoints | None | Redesign through integrations. Add MCP-specific mutations only for behavior integrations cannot represent. | +| `GET /global/event`, `GET /event` | `GET /api/event` | Redesign event routing and reducers. V2 intentionally uses one global cross-location stream. | +| All twelve `/tui/*` endpoints | None | Remove from desktop/web scope. They are TUI remote-control APIs. | +| `/experimental/capabilities`, `/console*`, `/tool*`, global experimental session/background endpoints | Session background and catalog APIs where applicable | Remove or redesign per feature. These are not general app bootstrap contracts. | + +## Required Protocol Work Before Cutover + +The app cannot reach feature parity against the current V2 surface. Resolve +these items before switching production desktop/web: + +1. Add V2 configuration read/update operations, or deliberately remove server + configuration editing from the app. +2. Decide whether project metadata mutation and git initialization remain app + features; add narrow V2 operations if they do. +3. Decide archive semantics. Do not encode archive as a hidden compatibility + field on session rename. +4. Decide whether sharing is part of V2. Remove the UI until a V2 sharing model + exists. +5. Decide whether session-derived diffs are required. VCS working-tree diff is + not equivalent to a session range diff. +6. Decide whether branch, LSP, formatter, text-search, symbol-search, and shell + discovery status remain visible in the app. +7. Move project-copy routes from `/experimental/project/...` to + `/api/experimental/project/...`. They are currently the only V2 group outside + the V2 namespace and are normalized as legacy routes by the combined server. + +Missing features should be removed or implemented as native V2 contracts. They +should not be routed through `packages/opencode`, which is V1 reference code. + +## Application Design + +### Client and Location + +Replace `@opencode-ai/sdk` for runtime calls with `@opencode-ai/client` in +`packages/app`. `createSdkForServer` should construct the generated Promise +client with the existing Basic authorization and platform fetch behavior. Keep +the V1 session types as a type-only compatibility dependency while the existing +timeline consumes them. + +Keep one server client. Replace implicit directory clients with a small +application context containing: + +```ts +{ + client: OpenCodeClient + location: Location.Ref +} +``` + +Every location-scoped call should pass that location. Session-scoped calls use +the session's durable placement and should not infer placement from the active +route after creation. + +### Event Stream + +Subscribe with `client.event.subscribe()`. Route an event to location state by +`event.location?.directory`, with a separate global channel when location is +absent. Preserve the current bounded UI batching and reconnect behavior, but +remove conversion to legacy `payload.properties`. + +Metadata reducers should consume V2 definitions directly. Transcript events +should go through the timeline adapter, which updates its V1-shaped projection +from session step, text, reasoning, tool, input, retry, compaction, and +execution events. Do not manufacture legacy `message.*` events and feed them +back through the old reducer; apply the V2 event once at the compatibility +boundary and replace only the affected projected message and parts. + +The live stream is lossy across disconnects. On reconnect, reload active +session projections and request lists rather than assuming event replay. The +durable experimental session log is useful for diagnostics but should not be a +hidden replacement for projection reads. + +### Session Store and Timeline + +Keep the legacy normalized `Message` and `Part` stores and existing timeline +components. Add the compatibility boundary at +`packages/app/src/context/v2/session-timeline-adapter.ts`. Paging should call the +V2 message endpoint with body cursors, then replace the adapter from the returned +`SessionMessage.Info[]` projection. + +The adapter should expose a complete projection for one session: + +```ts +type LegacyTimelineProjection = { + messages: Message[] + parts: Record + status: SessionStatus + notices: SessionMessageInfo[] +} + +type TimelineAdapter = { + replace(messages: readonly SessionMessageInfo[]): LegacyTimelineProjection + apply(event: OpenCodeEvent): LegacyTimelineProjection + sourceMessageID(legacyID: string): string | undefined + legacyMessageID(sourceID: string): string | undefined + reset(): void +} +``` + +Use `packages/core/src/session/message-updater.ts` as the authoritative event +fold and `packages/tui/src/context/data.tsx` as the browser-side live-update +reference. There is currently no reusable V2-to-V1 converter. + +Projection rules: + +- Preserve V2 user and assistant IDs where doing so does not violate app + ordering assumptions. Map user text, files, and agents to the corresponding + V1 parts. +- Map assistant text and reasoning content to V1 text and reasoning parts. V2 + content has no part ID, so derive a stable part ID from source message ID and + absolute content index. +- Map V2 tool `streaming`, `running`, `completed`, and `error` states to V1 + `pending`, `running`, `completed`, and `error`. Preserve the V2 tool ID as the + V1 `callID`; derive the V1 part ID independently. +- Serialize textual tool content into V1 output and map file content to + attachments. Preserve structured values and V2-only state under compatibility + metadata only when an existing component needs it. +- Map V2 retry state to a V1 retry part and retry session status. Map assistant + errors to the closest V1 error type while retaining the source error type. +- Map V2 compaction to a synthetic V1 user message with a compaction part so the + existing divider remains intact. +- Keep agent/model switches as adapter selection state. Do not fabricate false + user turns for them. +- Keep synthetic, system, skill, and shell messages in `notices` until a narrow + display mapping is defined. Fabricating user messages for them would corrupt + assistant parent and turn semantics. + +The conversion is intentionally lossy for historical path, parent identity, +structured tool result values, and some provider error details. These fields do +not have V2 equivalents. Defaults should be isolated in the adapter and tested, +not spread through timeline components. + +V2 projection order is authoritative and is not guaranteed to match message ID +order. Queued input may be promoted after a later steer, and compaction may +reuse an admitted ID. `server-session.ts` currently uses sorted-ID binary +searches, so it must preserve array order independently of IDs. If a generated +legacy ID is needed, retain a bidirectional source-ID map for fork, revert, deep +links, and optimistic reconciliation. + +With this boundary, most files under +`packages/app/src/pages/session/timeline/` and the legacy message-part renderers +can remain unchanged. Necessary changes are concentrated in: + +- `packages/app/src/context/server-session.ts`; +- `packages/app/src/context/global-sync/event-reducer.ts`; +- `packages/app/src/context/global-sync/types.ts` for V2 session metadata while + retaining V1 transcript types; +- `packages/app/src/context/server-sdk.tsx`; +- `packages/app/src/context/sync.tsx`. + +### Prompt and Execution + +Build V2 prompt input as text plus file and agent attachments. Map the existing +steer/queue UI directly to `delivery`. Use `session.prompt` for admission and +observe execution through projection/events. Use `session.interrupt`, staged +revert operations, and `session.compact` for their corresponding controls. + +The app must not wait for prompt HTTP completion to obtain the assistant +message. An optional `session.wait` is suitable for explicit blocking workflows, +not the interactive composer. + +### Integrations and Requests + +Rebuild provider settings around integration connection methods and credential +records. Migrate permission and question bootstrap to cross-location request +lists, while replies use session-scoped endpoints. Add form handling as a new +request-dock type rather than forcing forms into question types. + +### Files and Terminals + +Decode `fs.read` bytes according to the UI use case and preserve binary/media +detection in one application boundary. Move terminal HTTP and WebSocket paths +to `/api/pty`, pass nested location query values, and require connect tickets. + +## Desktop Design + +Desktop should bundle a V2 entry that runs `ServerProcess.start`, not the V1 +`Server.listen`. The sidecar needs a narrow Promise-facing wrapper that: + +1. builds and runs the Effect server layer in the worker; +2. reports ready only after `ServerProcess.start` returns its bound address; +3. retains the running fiber/scope for shutdown; +4. passes the existing password, loopback hostname, selected port, and desktop + origin CORS policy; +5. preserves the existing sidecar worker isolation and crash reporting. + +Then update desktop health checks to `/api/health`. The renderer remains a +normal authenticated HTTP client and does not need a new preload API. + +Likely files: + +- `packages/desktop/electron.vite.config.ts`; +- `packages/desktop/src/main/env.d.ts`; +- `packages/desktop/src/main/sidecar.ts`; +- `packages/desktop/src/main/server.ts`; +- `packages/desktop/package.json`. + +The standalone server currently receives CORS through its route construction, +so the wrapper must verify `oc://renderer` is accepted before replacing the V1 +sidecar. + +## Migration Sequence + +1. Resolve the protocol gaps required for the retained product scope, fix the + project-copy namespace, and regenerate `packages/client`. +2. Add V2 client construction, explicit location context, `/api/health`, and the + native event subscription behind a development-only entry point. +3. Add the V2-to-V1 timeline adapter, switch transcript paging and live events + to V2, and retain the existing timeline renderers. Record the required + production timeline benchmark baseline before this step and compare it + afterward. +4. Migrate prompt, command, shell, interrupt, compact, fork, revert, permission, + question, and form workflows. +5. Migrate filesystem, VCS, PTY, projects, project copies, catalogs, integrations, + and credentials. +6. Switch desktop packaging and lifecycle to the V2 server process. +7. Remove runtime `@opencode-ai/sdk` calls, V1 event handling, directory-scoped + SDK clients, fallback endpoint probes, and unsupported V1-only UI. Retain + only the type-level V1 transcript compatibility surface used by the adapter. +8. Remove transitional V1 event definitions from the V2 public event manifest + after all current consumers stop depending on them, then regenerate clients. + +This order isolates the deliberate V1 timeline shape behind one V2 projection +boundary. Transcript and event conversion is the highest-risk work and should +land before the broad set of mechanical endpoint migrations. + +## Verification + +Each migration slice should verify: + +- generated Promise client request and response types; +- app typecheck and focused state/timeline tests; +- event reconnect followed by projection reconciliation; +- simultaneous sessions in different locations; +- prompt steer, queue, interrupt, retry, tool, permission, question, and form + flows; +- PTY ticket connection and reconnect; +- desktop worker startup, authenticated `/api/health`, graceful shutdown, and + crash recovery; +- browser and desktop behavior on both mobile-sized and desktop viewports. + +Run package checks from their package directories. Public Protocol or Server +`HttpApi` changes require `bun run generate` from `packages/client`; generated +files must not be edited directly.