Explorar o código

refactor(app): remove v1 compatibility

Brendan Allan hai 1 semana
pai
achega
aee34c9383
Modificáronse 80 ficheiros con 751 adicións e 2661 borrados
  1. 0 3
      bun.lock
  2. 0 220
      packages/app/V1_API_MIGRATION.md
  3. 0 4
      packages/app/e2e/performance/timeline-stability/fixture.test.ts
  4. 1 8
      packages/app/e2e/performance/timeline-stability/fixture.ts
  5. 0 1
      packages/app/package.json
  6. 0 20
      packages/app/src/app.tsx
  7. 1 1
      packages/app/src/components/command-palette.ts
  8. 1 1
      packages/app/src/components/dialog-command-palette-v2.tsx
  9. 5 5
      packages/app/src/components/dialog-connect-provider.tsx
  10. 3 20
      packages/app/src/components/dialog-custom-provider.tsx
  11. 1 1
      packages/app/src/components/dialog-edit-project-v2.tsx
  12. 1 1
      packages/app/src/components/dialog-edit-project.tsx
  13. 1 1
      packages/app/src/components/dialog-fork.tsx
  14. 15 17
      packages/app/src/components/dialog-select-directory-v2.tsx
  15. 13 15
      packages/app/src/components/dialog-select-directory.tsx
  16. 1 18
      packages/app/src/components/dialog-select-server.tsx
  17. 2 2
      packages/app/src/components/directory-picker-domain.test.ts
  18. 2 2
      packages/app/src/components/directory-picker-domain.ts
  19. 6 18
      packages/app/src/components/edit-project.ts
  20. 0 12
      packages/app/src/components/prompt-input/submit.test.ts
  21. 8 10
      packages/app/src/components/prompt-input/submit.ts
  22. 13 12
      packages/app/src/components/settings-general.tsx
  23. 6 13
      packages/app/src/components/settings-providers.tsx
  24. 10 12
      packages/app/src/components/settings-v2/general.tsx
  25. 6 13
      packages/app/src/components/settings-v2/providers.tsx
  26. 16 20
      packages/app/src/components/status-popover-body.tsx
  27. 4 26
      packages/app/src/components/terminal.tsx
  28. 2 6
      packages/app/src/components/titlebar-tab-nav.tsx
  29. 1 1
      packages/app/src/components/titlebar-tab-strip.tsx
  30. 1 1
      packages/app/src/components/titlebar.tsx
  31. 3 10
      packages/app/src/context/directory-sync.ts
  32. 3 3
      packages/app/src/context/file.tsx
  33. 18 127
      packages/app/src/context/global-sync/bootstrap.test.ts
  34. 8 27
      packages/app/src/context/global-sync/bootstrap.ts
  35. 1 4
      packages/app/src/context/global-sync/child-store.ts
  36. 7 11
      packages/app/src/context/global-sync/home-session-index.test.ts
  37. 6 10
      packages/app/src/context/global-sync/home-session-index.ts
  38. 1 14
      packages/app/src/context/global-sync/utils.ts
  39. 3 12
      packages/app/src/context/layout.tsx
  40. 0 2
      packages/app/src/context/permission.tsx
  41. 5 3
      packages/app/src/context/sdk.tsx
  42. 25 150
      packages/app/src/context/server-sdk.test.ts
  43. 17 137
      packages/app/src/context/server-sdk.tsx
  44. 179 121
      packages/app/src/context/server-session.test.ts
  45. 65 116
      packages/app/src/context/server-session.ts
  46. 1 2
      packages/app/src/context/server-sync.test.ts
  47. 25 41
      packages/app/src/context/server-sync.tsx
  48. 7 12
      packages/app/src/context/terminal.tsx
  49. 0 1
      packages/app/src/pages/home/home-projects-controller.tsx
  50. 3 6
      packages/app/src/pages/home/home-projects-view.tsx
  51. 0 1
      packages/app/src/pages/home/home-projects.tsx
  52. 1 2
      packages/app/src/pages/home/home-session-search-controller.ts
  53. 4 7
      packages/app/src/pages/home/home-sessions-controller.tsx
  54. 5 8
      packages/app/src/pages/home/home-sessions-view.tsx
  55. 0 1
      packages/app/src/pages/home/home-sessions.tsx
  56. 26 70
      packages/app/src/pages/layout.tsx
  57. 5 5
      packages/app/src/pages/layout/sidebar-items.tsx
  58. 3 8
      packages/app/src/pages/layout/sidebar-project.tsx
  59. 7 13
      packages/app/src/pages/layout/sidebar-workspace.tsx
  60. 10 22
      packages/app/src/pages/session.tsx
  61. 2 2
      packages/app/src/pages/session/review-tab.tsx
  62. 22 34
      packages/app/src/pages/session/timeline/message-timeline.tsx
  63. 5 7
      packages/app/src/pages/session/timeline/observe-element-offset.test.ts
  64. 16 27
      packages/app/src/pages/session/use-session-commands.tsx
  65. 2 2
      packages/app/src/pages/session/v2/review-panel-v2.tsx
  66. 131 39
      packages/app/src/types.ts
  67. 1 1
      packages/app/src/utils/persist.test.ts
  68. 1 1
      packages/app/src/utils/persist.ts
  69. 0 255
      packages/app/src/utils/server-compat.test.ts
  70. 0 629
      packages/app/src/utils/server-compat.ts
  71. 1 27
      packages/app/src/utils/server-health.test.ts
  72. 2 5
      packages/app/src/utils/server-health.ts
  73. 0 40
      packages/app/src/utils/server-protocol.test.ts
  74. 0 35
      packages/app/src/utils/server-protocol.ts
  75. 0 24
      packages/app/src/utils/server.ts
  76. 0 25
      packages/app/src/utils/session-message.test.ts
  77. 7 0
      packages/app/src/utils/session-title.ts
  78. 0 57
      packages/app/src/utils/terminal-websocket-url.test.ts
  79. 2 20
      packages/app/src/utils/terminal-websocket-url.ts
  80. 1 1
      packages/app/test-browser/command-palette.test.ts

+ 0 - 3
bun.lock

@@ -64,7 +64,6 @@
         "@opencode-ai/client": "workspace:*",
         "@opencode-ai/core": "workspace:*",
         "@opencode-ai/schema": "workspace:*",
-        "@opencode-ai/sdk": "file:vendor/opencode-ai-sdk-1.18.8-dev.tgz",
         "@opencode-ai/session-ui": "workspace:*",
         "@opencode-ai/ui": "workspace:*",
         "@opencode-ai/util": "workspace:*",
@@ -6333,8 +6332,6 @@
 
     "@openauthjs/openauth/jose": ["jose@5.9.6", "", {}, "sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ=="],
 
-    "@opencode-ai/app/@opencode-ai/sdk": ["@opencode-ai/sdk@vendor/opencode-ai-sdk-1.18.8-dev.tgz", { "dependencies": { "cross-spawn": "7.0.6" } }, "sha512-C2nfk4x0sPINwE5V6DPkFSuH3PkUmKPWHPzxpXC1j+3Ui5hslLCWJbkk8WcOG1Lyt3C0+yp4ea64v/kmtYCO4w=="],
-
     "@opencode-ai/cli/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
 
     "@opencode-ai/console-app/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.7", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.11.0", "@smithy/util-hex-encoding": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-DrpkEoM3j9cBBWhufqBwnbbn+3nf1N9FP6xuVJ+e220jbactKuQgaZwjwP5CP1t+O94brm2JgVMD2atMGX3xIQ=="],

+ 0 - 220
packages/app/V1_API_MIGRATION.md

@@ -1,220 +0,0 @@
-# V1 API Migration Checklist
-
-The app is currently hybrid. In this document, V1 refers to the legacy unprefixed server APIs used by `@opencode-ai/sdk/v2`, despite the SDK package name.
-
-## Events
-
-- [x] Replace `GET /global/event` with `GET /api/event`.
-  - `src/context/server-sdk.tsx`
-- [x] Reduce current granular session and message events into the existing app projections.
-  - `src/context/server-session-v2-reducer.ts`
-  - `src/context/server-session.ts`
-- [ ] Remove transitional session event dependencies: `session.created`, `session.updated`, `session.diff`, `session.status`, `session.idle`, and `session.error`.
-  - `src/context/global-sync/event-reducer.ts`
-  - `src/context/server-session.ts`
-  - `src/context/notification.tsx`
-  - `src/pages/session/usage-exceeded-dialogs.tsx`
-- [ ] Remove legacy message event compatibility: `message.updated`, `message.removed`, `message.part.updated`, `message.part.removed`, and `message.part.delta`.
-  - `src/context/global-sync/event-reducer.ts`
-  - `src/context/server-session.ts`
-- [x] Adapt current permission and question events to the existing request model.
-  - `src/context/global-sync/event-reducer.ts`
-  - `src/context/permission.tsx`
-- [x] Consume current file watcher events.
-  - `src/context/file.tsx`
-- [x] Consume current VCS events.
-  - `src/context/global-sync/event-reducer.ts`
-  - `src/pages/session.tsx`
-- [x] Consume current `pty.exited` events.
-  - `src/context/terminal.tsx`
-- [ ] Migrate LSP and reference events.
-  - `src/context/global-sync/event-reducer.ts`
-
-## Sessions
-
-- [x] Replace `GET /session/status` with one server-scoped `GET /api/session/active` snapshot plus V2 execution events.
-  - `src/context/server-sync.tsx`
-- [x] Migrate session listing from `GET /session`.
-  - `src/context/server-sync.tsx`
-  - `src/context/directory-sync.ts`
-  - `src/pages/layout.tsx`
-- [x] Migrate the remaining direct session read from `GET /session/:sessionID`.
-  - `src/components/titlebar.tsx`
-- [x] Migrate session updates from `PATCH /session/:sessionID`.
-  - `src/context/directory-sync.ts`
-  - `src/context/layout.tsx`
-  - `src/pages/home.tsx`
-  - `src/pages/layout.tsx`
-  - `src/pages/session/timeline/message-timeline.tsx`
-  - `src/components/titlebar-tab-nav.tsx`
-  - Renames use `POST /api/session/:sessionID/rename`; archival uses `POST /api/session/:sessionID/archive`.
-- [x] Migrate session deletion from `DELETE /session/:sessionID`.
-  - `src/pages/session/timeline/message-timeline.tsx`
-- [x] Remove session diff loading from `GET /session/:sessionID/diff`.
-  - Historical Session diffs remain unavailable until the current API defines their snapshot semantics.
-- [x] Migrate abort from `POST /session/:sessionID/abort`.
-  - `src/components/prompt-input/submit.ts`
-  - `src/pages/session/use-session-commands.tsx`
-  - `src/pages/session.tsx`
-- [x] Migrate revert and unrevert from `POST /session/:sessionID/revert` and `POST /session/:sessionID/unrevert`.
-  - `src/pages/session/use-session-commands.tsx`
-  - `src/pages/session.tsx`
-- [x] Replace `POST /session/:sessionID/summarize` with the current compact API.
-  - `src/pages/session/use-session-commands.tsx`
-- [x] Migrate slash commands from `POST /session/:sessionID/command`.
-  - `src/components/prompt-input/submit.ts`
-- [x] Migrate shell execution from `POST /session/:sessionID/shell`.
-  - `src/components/prompt-input/submit.ts`
-- [x] Migrate session fork from `POST /session/:sessionID/fork`.
-  - `src/components/dialog-fork.tsx`
-- [ ] Migrate sharing from `POST /session/:sessionID/share` and `DELETE /session/:sessionID/share`.
-  - `src/pages/session/use-session-commands.tsx`
-  - `src/pages/session/timeline/message-timeline.tsx`
-  - Blocked: the current API has no sharing contract or implementation.
-
-## Session Compatibility Fallbacks
-
-These calls are retained as fallback adapters. The current production path supplies the current session and message APIs.
-
-- [ ] Remove fallback `GET /session/:sessionID` after compatibility support is unnecessary.
-  - `src/context/server-session.ts`
-- [ ] Remove fallback `GET /session/:sessionID/message` after compatibility support is unnecessary.
-  - `src/context/server-session.ts`
-- [ ] Remove fallback `GET /session/:sessionID/message/:messageID` after compatibility support is unnecessary.
-  - `src/context/server-session.ts`
-
-## Filesystem
-
-- [ ] Migrate file listing from `GET /file`.
-  - `src/context/file.tsx`
-- [ ] Migrate file reads from `GET /file/content`.
-  - `src/context/file.tsx`
-  - `src/pages/session/review-tab.tsx`
-  - `src/pages/session/v2/review-panel-v2.tsx`
-- [x] Migrate path discovery from `GET /path` to `GET /api/path`.
-  - `src/context/global-sync/bootstrap.ts`
-  - `src/components/dialog-select-directory.tsx`
-  - `src/components/dialog-select-directory-v2.tsx`
-
-## Projects And Worktrees
-
-- [x] Migrate project listing from `GET /project` to `GET /api/project`.
-  - `src/context/global-sync/bootstrap.ts`
-- [x] Migrate the current project lookup from `GET /project/current` to `GET /api/project/current`.
-  - `src/context/global-sync/bootstrap.ts`
-- [ ] Migrate Git initialization from `POST /project/git/init`.
-  - `src/pages/session.tsx`
-- [x] Migrate project updates from `PATCH /project/:projectID` to `PATCH /api/project/:projectID`.
-  - `src/context/layout.tsx`
-  - `src/components/edit-project.ts`
-  - `src/pages/layout.tsx`
-- [ ] Migrate experimental worktree listing, creation, removal, and reset from `/experimental/worktree`.
-  - `src/pages/layout.tsx`
-  - `src/components/prompt-input/submit.ts`
-  - Listing now uses `GET /api/project/:projectID/directories`; create, removal, and reset remain.
-- [ ] Migrate instance disposal from `POST /instance/dispose`.
-  - `src/pages/layout.tsx`
-
-## VCS
-
-- [x] Migrate repository information from `GET /vcs` to `GET /api/vcs`.
-  - `src/context/global-sync/bootstrap.ts`
-- [x] Migrate diffs from `GET /vcs/diff` to `GET /api/vcs/diff`.
-  - `src/pages/session.tsx`
-- [x] Migrate status from `GET /vcs/status` to `GET /api/vcs/status`.
-  - `src/pages/layout.tsx`
-
-## Configuration And Authentication
-
-- [ ] Migrate global configuration reads from `GET /global/config`.
-  - `src/context/global-sync/bootstrap.ts`
-- [ ] Migrate directory configuration reads from `GET /config`.
-  - `src/context/global-sync/bootstrap.ts`
-- [ ] Migrate global configuration updates from `PATCH /global/config`.
-  - `src/context/server-sync.tsx`
-- [x] Migrate provider authentication method discovery from `GET /provider/auth` to `GET /api/integration/:integrationID`.
-  - `src/components/dialog-connect-provider.tsx`
-- [x] Migrate built-in provider OAuth authorization and callbacks to `/api/integration/:integrationID/connect/oauth/*`.
-  - `src/components/dialog-connect-provider.tsx`
-- [ ] Migrate remaining credentials from `PUT /auth/:providerID` and `DELETE /auth/:providerID`.
-  - Built-in provider key connections now use `POST /api/integration/:integrationID/connect/key`.
-  - `src/components/dialog-connect-provider.tsx`
-  - `src/components/dialog-custom-provider.tsx`
-  - `src/components/settings-providers.tsx`
-  - `src/components/settings-v2/providers.tsx`
-- [ ] Migrate global disposal from `POST /global/dispose`.
-  - `src/components/dialog-connect-provider.tsx`
-  - `src/components/settings-providers.tsx`
-  - `src/components/settings-v2/providers.tsx`
-
-## Permissions And Questions
-
-- [x] Migrate permission listing from `GET /permission` to `GET /api/permission/request`.
-  - `src/context/global-sync/bootstrap.ts`
-  - `src/context/permission.tsx`
-- [x] Migrate permission responses from `/session/:sessionID/permissions/:permissionID`.
-  - `src/context/permission.tsx`
-  - `src/pages/session/composer/session-composer-state.ts`
-- [x] Migrate question listing from `GET /question` to `GET /api/question/request`.
-  - `src/context/global-sync/bootstrap.ts`
-- [x] Migrate question replies and rejections from `/question/:requestID/*` to `/api/session/:sessionID/question/:requestID/*`.
-  - `src/pages/session/composer/session-question-dock.tsx`
-
-## Commands, MCP, LSP, And References
-
-- [x] Migrate command listing from `GET /command` to `GET /api/command`.
-  - `src/context/global-sync/bootstrap.ts`
-  - `src/context/server-sync.tsx`
-- [x] Migrate MCP listing, connection, and disconnection from `/mcp` to `/api/mcp`.
-  - `src/context/server-sync.tsx`
-- [ ] Replace legacy MCP authentication with the Integration OAuth workflow.
-  - `src/context/server-sync.tsx`
-- [x] Migrate experimental resource listing from `GET /experimental/resource` to `GET /api/mcp/resource`.
-  - `src/context/server-sync.tsx`
-- [ ] Migrate LSP status from `GET /lsp`.
-  - `src/context/server-sync.tsx`
-- [x] Move `GET /api/reference` off the legacy generated SDK transport.
-  - `src/context/global-sync/bootstrap.ts`
-
-## Search
-
-- [x] Migrate global session search from `GET /experimental/session` to `GET /api/session`.
-  - `src/components/command-palette.ts`
-  - `src/components/dialog-command-palette-v2.tsx`
-
-## PTY And Terminal
-
-- [x] Migrate PTY creation, reads, updates, and deletion from `/pty` to `/api/pty`.
-  - `src/context/terminal.tsx`
-  - `src/components/terminal.tsx`
-- [x] Migrate shell listing from `GET /pty/shells` to `GET /api/pty/shells`.
-  - `src/components/settings-general.tsx`
-  - `src/components/settings-v2/general.tsx`
-- [x] Migrate connection tokens from `POST /pty/:ptyID/connect-token` to `POST /api/pty/:ptyID/connect-token`.
-  - `src/components/terminal.tsx`
-- [x] Migrate the direct WebSocket connection from `/pty/:ptyID/connect` to `/api/pty/:ptyID/connect`.
-  - `src/components/terminal.tsx`
-
-## Legacy Types And Adapters
-
-These are not V1 network requests, but they keep the UI coupled to V1 data contracts.
-
-- [ ] Replace the current-session-to-legacy-session adapter.
-  - `src/utils/session.ts`
-- [ ] Replace the current-message-to-legacy-message-and-part adapter.
-  - `src/utils/session-message.ts`
-- [ ] Replace current agent, provider, and model adapters to legacy SDK structures.
-  - `src/context/global-sync/utils.ts`
-- [ ] Replace legacy `Session`, `Message`, `Part`, `PermissionRequest`, `QuestionRequest`, `Project`, `FileNode`, `FileDiffInfo`, and `Event` types throughout app state and rendering.
-- [ ] Remove the `@opencode-ai/sdk` runtime dependency after all legacy calls and types are gone.
-  - `package.json`
-
-## Test Infrastructure
-
-- [ ] Replace V1 endpoint mocks with current API mocks.
-  - `e2e/utils/mock-server.ts`
-- [x] Replace `/global/event` and `/event` interception with current event transport handling.
-  - `e2e/utils/sse-transport.ts`
-- [ ] Replace `SessionV1` and legacy SDK fixtures in timeline performance tests.
-  - `e2e/performance/timeline-stability/fixture.ts`
-- [ ] Remove remaining legacy SDK type fixtures from unit and browser tests.

+ 0 - 4
packages/app/e2e/performance/timeline-stability/fixture.test.ts

@@ -45,10 +45,6 @@ describe("timeline fixture validation", () => {
     expect(first.payload.id).toMatch(/^evt_timeline_\d{4}$/)
     expect(Number(second.payload.id.slice(-4))).toBe(Number(first.payload.id.slice(-4)) + 1)
   })
-
-  test("uses the projected tool ID as its call ID", () => {
-    expect(toolPart("call_1", "read", "running", {})).toMatchObject({ id: "call_1", callID: "call_1" })
-  })
 })
 
 if (false) {

+ 1 - 8
packages/app/e2e/performance/timeline-stability/fixture.ts

@@ -2,15 +2,8 @@ import { base64Encode } from "@opencode-ai/core/util/encode"
 import { Event } from "@opencode-ai/schema/event"
 import { SessionStatusEvent } from "@opencode-ai/schema/session-status-event"
 import { SessionV1 } from "@opencode-ai/schema/session-v1"
-import type {
-  AssistantMessage,
-  Message,
-  Part,
-  ToolPart,
-  ToolState,
-  UserMessage,
-} from "../../../src/types"
 import type { SessionV1Info, SessionStatus } from "@opencode-ai/client/promise"
+import type { AssistantMessage, Message, Part, ToolPart, ToolState, UserMessage } from "../../../src/types"
 import { expect, type Page } from "@playwright/test"
 import { Schema } from "effect"
 import { mockOpenCodeServer } from "../../utils/mock-server"

+ 0 - 1
packages/app/package.json

@@ -56,7 +56,6 @@
     "@opencode-ai/client": "workspace:*",
     "@opencode-ai/core": "workspace:*",
     "@opencode-ai/schema": "workspace:*",
-    "@opencode-ai/sdk": "file:vendor/opencode-ai-sdk-1.18.8-dev.tgz",
     "@opencode-ai/session-ui": "workspace:*",
     "@opencode-ai/ui": "workspace:*",
     "@opencode-ai/util": "workspace:*",

+ 0 - 20
packages/app/src/app.tsx

@@ -238,26 +238,6 @@ function UiI18nBridge(props: ParentProps) {
 }
 
 function LayoutCompatibility(props: ParentProps) {
-  const global = useGlobal()
-  const navigate = useNavigate()
-  const server = useServer()
-  const settings = useSettings()
-
-  createEffect(() => {
-    if (settings.general.newLayoutDesigns()) return
-    const current = server.current
-    if (!current) return
-    const protocol = global.ensureServerCtx(current).sdk.protocolKind()
-    if (protocol !== "v2") return
-    const next = global.servers.list().find((s) => {
-      if (ServerConnection.key(s) === ServerConnection.key(current)) return false
-      return global.ensureServerCtx(s).sdk.protocolKind() !== "v2"
-    })
-    if (!next) return
-    navigate("/")
-    queueMicrotask(() => server.setActive(ServerConnection.key(next)))
-  })
-
   return <>{props.children}</>
 }
 

+ 1 - 1
packages/app/src/components/command-palette.ts

@@ -146,7 +146,7 @@ export function createCommandPaletteModel(props: { filesOnly?: () => boolean; on
     server: ServerConnection.key(serverSDK.server),
     opened: serverCtx.projects.list,
     stored: () => serverCtx.sync.data.project,
-    load: (search, signal) => serverSDK.currentApi.session.list({ parentID: null, search, limit: 50 }, { signal }),
+    load: (search, signal) => serverSDK.api.session.list({ parentID: null, search, limit: 50 }, { signal }),
     untitled: () => language.t("command.session.new"),
     category: () => language.t("command.category.session"),
   })

+ 1 - 1
packages/app/src/components/dialog-command-palette-v2.tsx

@@ -79,7 +79,7 @@ export function DialogHomeCommandPaletteV2(props: {
     server: ServerConnection.key(props.server),
     opened: serverCtx.projects.list,
     stored: () => serverCtx.sync.data.project,
-    load: (search, signal) => serverCtx.sdk.currentApi.session.list({ parentID: null, search, limit: 50 }, { signal }),
+    load: (search, signal) => serverCtx.sdk.api.session.list({ parentID: null, search, limit: 50 }, { signal }),
     untitled: () => language.t("command.session.new"),
     category: () => language.t("command.category.session"),
   })

+ 5 - 5
packages/app/src/components/dialog-connect-provider.tsx

@@ -418,7 +418,7 @@ function ProviderConnection(props: {
     () => ({ provider: props.provider, directory: directory() }),
     (input) =>
       serverSDK()
-        .currentApi.integration.get({
+        .api.integration.get({
           integrationID: input.provider,
           location: input.directory ? { directory: input.directory } : undefined,
         })
@@ -547,7 +547,7 @@ function ProviderConnection(props: {
       }
       dispatch({ type: "auth.pending" })
       await serverSDK()
-        .currentApi.integration.oauth.connect({
+        .api.integration.oauth.connect({
           integrationID: props.provider,
           methodID: method.id,
           inputs: inputs ?? {},
@@ -816,7 +816,7 @@ function ProviderConnection(props: {
       }
 
       setFormStore("error", undefined)
-      await serverSDK().currentApi.integration.connect.key({
+      await serverSDK().api.integration.connect.key({
         integrationID: props.provider,
         location: location(),
         key: apiKey,
@@ -947,7 +947,7 @@ function ProviderConnection(props: {
 
       setFormStore("error", undefined)
       const result = await serverSDK()
-        .currentApi.integration.oauth.complete({
+        .api.integration.oauth.complete({
           integrationID: props.provider,
           attemptID: store.authorization!.attemptID,
           location: location(),
@@ -1044,7 +1044,7 @@ function ProviderConnection(props: {
         const authorization = store.authorization
         if (!authorization || !alive.value) return
         const result = await serverSDK()
-          .currentApi.integration.oauth.status({
+          .api.integration.oauth.status({
             integrationID: props.provider,
             attemptID: authorization.attemptID,
             location: location(),

+ 3 - 20
packages/app/src/components/dialog-custom-provider.tsx

@@ -130,26 +130,9 @@ export function CustomProviderForm(props: { autofocus?: boolean } = {}) {
   }
 
   const saveMutation = useMutation(() => ({
-    mutationFn: async (result: NonNullable<ReturnType<typeof validate>>) => {
-      if ((await serverSDK().protocol) !== "v1") throw new Error("Custom providers are unavailable on this server")
-      const disabledProviders = serverSync().data.config.disabled_providers ?? []
-      const nextDisabled = disabledProviders.filter((id) => id !== result.providerID)
-
-      if (result.key) {
-        await serverSDK().legacy.auth.set({
-          providerID: result.providerID,
-          auth: {
-            type: "api",
-            key: result.key,
-          },
-        })
-      }
-
-      await serverSync().updateConfig({
-        provider: { [result.providerID]: result.config },
-        disabled_providers: nextDisabled,
-      })
-      return result
+    mutationFn: async (result: NonNullable<ReturnType<typeof validate>>): Promise<typeof result> => {
+      // TODO: Restore custom providers when V2 exposes config and arbitrary credential APIs.
+      throw new Error(`Custom provider ${result.providerID} is unavailable`)
     },
     onSuccess: (result) => {
       dialog.close()

+ 1 - 1
packages/app/src/components/dialog-edit-project-v2.tsx

@@ -146,7 +146,7 @@ export function DialogEditProjectV2(props: { project: LocalProject; server: Serv
           <ButtonV2 type="button" variant="neutral" disabled={model.save.isPending} onClick={model.close}>
             {language.t("common.cancel")}
           </ButtonV2>
-          <ButtonV2 type="submit" variant="contrast" disabled={model.save.isPending}>
+          <ButtonV2 type="submit" variant="contrast" disabled={!model.supported || model.save.isPending}>
             {model.save.isPending ? language.t("common.saving") : language.t("common.save")}
           </ButtonV2>
         </DialogFooter>

+ 1 - 1
packages/app/src/components/dialog-edit-project.tsx

@@ -160,7 +160,7 @@ export function DialogEditProject(props: { project: LocalProject; server: Server
           <Button type="button" variant="ghost" size="large" onClick={model.close}>
             {language.t("common.cancel")}
           </Button>
-          <Button type="submit" variant="primary" size="large" disabled={model.save.isPending}>
+          <Button type="submit" variant="primary" size="large" disabled={!model.supported || model.save.isPending}>
             {model.save.isPending ? language.t("common.saving") : language.t("common.save")}
           </Button>
         </div>

+ 1 - 1
packages/app/src/components/dialog-fork.tsx

@@ -69,7 +69,7 @@ export const DialogFork: Component = () => {
     const dir = base64Encode(sdk().directory)
 
     sdk()
-      .currentApi.session.fork({ sessionID, boundary: { type: "before", messageID: item.id } })
+      .api.session.fork({ sessionID, boundary: { type: "before", messageID: item.id } })
       .then((forked) => {
         dialog.close()
         prompt.set(restored, undefined, { dir, id: forked.id })

+ 15 - 17
packages/app/src/components/dialog-select-directory-v2.tsx

@@ -66,23 +66,21 @@ export function DialogSelectDirectoryV2(props: DialogSelectDirectoryV2Props) {
   let pathArea: HTMLDivElement | undefined
   let navigation = 0
 
-  const missingBase = createMemo(() => !(sync.data.path.home || sync.data.path.directory))
   const [fallbackPath] = createResource(
-    () => (missingBase() ? true : undefined),
-    async (): Promise<Path | undefined> => {
-      if ((await sdk.protocol) === "v1")
-        return sdk.legacy.path.get().catch(() => undefined)
-      return sdk.api.location
+    () => (!(sync.data.path.home || sync.data.path.directory) ? true : undefined),
+    () =>
+      sdk.api.location
         .get()
-        .then((location) => ({
-          state: "",
-          config: "",
-          worktree: location.project.directory,
-          directory: location.directory,
-          home: "",
-        }))
-        .catch(() => undefined)
-    },
+        .then(
+          (location): Path => ({
+            state: "",
+            config: "",
+            worktree: location.project.directory,
+            directory: location.directory,
+            home: "",
+          }),
+        )
+        .catch(() => undefined),
     { initialValue: undefined },
   )
   const home = createMemo(() => sync.data.path.home || fallbackPath()?.home || "")
@@ -104,7 +102,7 @@ export function DialogSelectDirectoryV2(props: DialogSelectDirectoryV2Props) {
     if (!policy.includeFiles) return { query: value, items: directories.slice(0, 5) }
     const base = pickerRoot(cleaned) || root() || start()
     if (!base) return { query: value, items: directories.slice(0, 5) }
-    const files = await sdk.currentApi.file
+    const files = await sdk.api.file
       .find({
         location: { directory: base },
         query: pickerFileSearchQuery(base, value, home()),
@@ -134,7 +132,7 @@ export function DialogSelectDirectoryV2(props: DialogSelectDirectoryV2Props) {
       existing ??
       loads.schedule(`${generation}:${key}`, eager ? "background" : "user", () => {
         if (!activeTreeNavigation(generation, navigation)) return Promise.resolve(undefined)
-        return sdk.currentApi.file
+        return sdk.api.file
           .list({ location: { directory: absolute } })
           .then((result) =>
             result.data.map((entry) => ({

+ 13 - 15
packages/app/src/components/dialog-select-directory.tsx

@@ -57,23 +57,21 @@ export function DialogSelectDirectory(props: DialogSelectDirectoryProps) {
   const [filter, setFilter] = createSignal("")
   let list: ListRef | undefined
 
-  const missingBase = createMemo(() => !(sync.data.path.home || sync.data.path.directory))
   const [fallbackPath] = createResource(
-    () => (missingBase() ? true : undefined),
-    async (): Promise<Path | undefined> => {
-      if ((await sdk.protocol) === "v1")
-        return sdk.legacy.path.get().catch(() => undefined)
-      return sdk.api.location
+    () => (!(sync.data.path.home || sync.data.path.directory) ? true : undefined),
+    () =>
+      sdk.api.location
         .get()
-        .then((location) => ({
-          state: "",
-          config: "",
-          worktree: location.project.directory,
-          directory: location.directory,
-          home: "",
-        }))
-        .catch(() => undefined)
-    },
+        .then(
+          (location): Path => ({
+            state: "",
+            config: "",
+            worktree: location.project.directory,
+            directory: location.directory,
+            home: "",
+          }),
+        )
+        .catch(() => undefined),
     { initialValue: undefined },
   )
 

+ 1 - 18
packages/app/src/components/dialog-select-server.tsx

@@ -16,7 +16,6 @@ import { useGlobal } from "@/context/global"
 import { useLanguage } from "@/context/language"
 import { usePlatform } from "@/context/platform"
 import { normalizeServerUrl, ServerConnection, useServer } from "@/context/server"
-import { detectServerProtocol } from "@/utils/server-protocol"
 import { type ServerHealth, useCheckServerHealth } from "@/utils/server-health"
 import { useSettings } from "@/context/settings"
 import { useTabs } from "@/context/tabs"
@@ -264,13 +263,6 @@ export function useServerManagementController(options: { onSelect?: () => void;
         setStore("addServer", { error: language.t("dialog.server.add.error") })
         return
       }
-      if (
-        !settings.general.newLayoutDesigns() &&
-        (await detectServerProtocol(conn.http, platform.fetch ?? globalThis.fetch)) === "v2"
-      ) {
-        setStore("addServer", { error: language.t("dialog.server.add.error") })
-        return
-      }
 
       resetAdd()
       if (options.navigateOnAdd === false) {
@@ -315,13 +307,6 @@ export function useServerManagementController(options: { onSelect?: () => void;
         setStore("editServer", { error: language.t("dialog.server.add.error") })
         return
       }
-      if (
-        !settings.general.newLayoutDesigns() &&
-        (await detectServerProtocol(conn.http, platform.fetch ?? globalThis.fetch)) === "v2"
-      ) {
-        setStore("editServer", { error: language.t("dialog.server.add.error") })
-        return
-      }
       if (normalized === input.original.http.url) {
         server.add(conn)
       } else {
@@ -360,9 +345,7 @@ export function useServerManagementController(options: { onSelect?: () => void;
 
   const sortedItems = createMemo(() => {
     const raw = items()
-    const list = settings.general.newLayoutDesigns()
-      ? raw
-      : raw.filter((x) => global.ensureServerCtx(x).sdk.protocolKind() !== "v2")
+    const list = raw
     if (!list.length) return list
     const active = current()
     const order = new Map(list.map((url, index) => [url, index] as const))

+ 2 - 2
packages/app/src/components/directory-picker-domain.test.ts

@@ -133,7 +133,7 @@ test("scopes file autocomplete to the current browser root", () => {
 test("resolves directory autocomplete from the current browser root", async () => {
   const directories: string[] = []
   const sdk = {
-    currentApi: {
+    api: {
       file: {
         find: (input: { location?: { directory?: string } }) => {
           directories.push(input.location?.directory ?? "")
@@ -155,7 +155,7 @@ test("resolves directory autocomplete from the current browser root", async () =
 test("searches from an absolute root without a default base", async () => {
   const directories: string[] = []
   const sdk = {
-    currentApi: {
+    api: {
       file: {
         list: (input: { location?: { directory?: string } }) => {
           directories.push(input.location?.directory ?? "")

+ 2 - 2
packages/app/src/components/directory-picker-domain.ts

@@ -342,7 +342,7 @@ export function createDirectorySearch(args: { sdk: ServerSDK; base: () => string
     const key = trimPickerPath(directory)
     const existing = cache.get(key)
     if (existing) return existing
-    const request = args.sdk.currentApi.file
+    const request = args.sdk.api.file
       .list({ location: { directory: key } })
       .then((result) => result.data)
       .catch(() => [])
@@ -374,7 +374,7 @@ export function createDirectorySearch(args: { sdk: ServerSDK; base: () => string
     const pathInput = raw.startsWith("~") || !!pickerRoot(raw) || raw.includes("/")
     const query = normalizePickerDrive(input.path)
     if (!pathInput) {
-      const results = await args.sdk.currentApi.file
+      const results = await args.sdk.api.file
         .find({ location: { directory: input.directory }, query, type: "directory", limit: 50 })
         .then((result) => result.data.map((entry) => entry.path))
         .catch(() => [])

+ 6 - 18
packages/app/src/components/edit-project.ts

@@ -9,6 +9,7 @@ import { type LocalProject } from "@/context/layout"
 import { ServerConnection } from "@/context/server"
 
 export function createEditProjectModel(props: { project: LocalProject; server: ServerConnection.Any }) {
+  const supported = !props.project.id || props.project.id === "global"
   const dialog = useDialog()
   const global = useGlobal()
   const serverCtx = createMemo(() => global.ensureServerCtx(props.server))
@@ -71,23 +72,9 @@ export function createEditProjectModel(props: { project: LocalProject; server: S
       const start = store.startup.trim()
 
       if (props.project.id && props.project.id !== "global") {
-        if ((await serverCtx().sdk.protocol) !== "v1") return
-        const project = await serverCtx()
-          .sdk.legacy.project.update({
-            projectID: props.project.id,
-            directory: props.project.worktree,
-            name,
-            icon: { color: store.color || "", override: store.iconOverride || "" },
-            commands: { start },
-          })
-          .then((result) => result.data)
-        if (!project) return
-        serverCtx().sync.set("project", (items) =>
-          items.map((item) => (item.id === project.id ? normalizeProjectInfo(project) : item)),
-        )
-        serverCtx().sync.project.icon(props.project.worktree, store.iconOverride || undefined)
-        dialog.close()
-        return
+        // TODO: Restore project edits when the V2 client exposes a project update API.
+        // await serverCtx().sdk.api.project.update({ projectID: props.project.id, name, icon, commands })
+        throw new Error(`Project ${props.project.id} cannot be updated`)
       }
 
       serverCtx().sync.project.meta(props.project.worktree, {
@@ -101,7 +88,7 @@ export function createEditProjectModel(props: { project: LocalProject; server: S
 
   function submit(event: SubmitEvent) {
     event.preventDefault()
-    if (save.isPending) return
+    if (!supported || save.isPending) return
     save.mutate()
   }
 
@@ -111,6 +98,7 @@ export function createEditProjectModel(props: { project: LocalProject; server: S
     folderName,
     defaultName,
     save,
+    supported,
     submit,
     drop,
     dragOver,

+ 0 - 12
packages/app/src/components/prompt-input/submit.test.ts

@@ -125,13 +125,6 @@ beforeAll(async () => {
     useSearchParams: () => [search, () => undefined],
   }))
 
-  mock.module("@opencode-ai/sdk/v2/client", () => ({
-    createOpencodeClient: (input: { directory: string }) => {
-      createdClients.push(input.directory)
-      return clientFor(input.directory)
-    },
-  }))
-
   mock.module("@opencode-ai/ui/toast", () => ({
     Toast: { Region: () => null },
     showToast: () => 0,
@@ -197,13 +190,8 @@ beforeAll(async () => {
       const sdk = {
         scope: "local",
         directory: "/repo/main",
-        client: rootClient,
         api: rootClient.api,
-        currentApi: rootClient.api,
         url: "http://localhost:4096",
-        createClient(opts: any) {
-          return clientFor(opts.directory)
-        },
       }
       return () => sdk
     },

+ 8 - 10
packages/app/src/components/prompt-input/submit.ts

@@ -15,6 +15,7 @@ import { useSDK, type DirectorySDK } from "@/context/sdk"
 import { useSync, type DirectorySync } from "@/context/sync"
 import { Identifier } from "@/utils/id"
 import { Worktree as WorktreeState } from "@/utils/worktree"
+import { getDirectory } from "@opencode-ai/core/util/path"
 import { buildRequestParts } from "./build-request-parts"
 import { setCursorPosition } from "./editor-dom"
 import { formatServerError } from "@/utils/server-errors"
@@ -22,7 +23,6 @@ import { ScopedKey } from "@/utils/server-scope"
 import { createPromptSubmissionState } from "./submission-state"
 import { normalizeSessionInfo } from "@/utils/session"
 import { Event } from "@opencode-ai/schema/event"
-import { getDirectory } from "@opencode-ai/core/util/path"
 
 type PendingPrompt = {
   abort: AbortController
@@ -42,7 +42,7 @@ export type FollowupDraft = {
 }
 
 type FollowupSendInput = {
-  api: DirectorySDK["currentApi"]["session"]
+  api: DirectorySDK["api"]["session"]
   serverSync: ServerSync
   sync: DirectorySync
   draft: FollowupDraft
@@ -261,7 +261,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
       return Promise.resolve()
     }
     return sdk()
-      .currentApi.session.interrupt({ sessionID })
+      .api.session.interrupt({ sessionID })
       .catch(() => {})
   }
 
@@ -345,11 +345,10 @@ export function createPromptSubmit(input: PromptSubmitInput) {
     const worktreeSelection = input.newSessionWorktree?.() || "main"
 
     let sessionDirectory = projectDirectory
-
     if (isNewSession) {
       if (worktreeSelection === "create") {
         const createdWorktree = await sdk()
-          .currentApi.projectCopy.create({
+          .api.projectCopy.create({
             projectID: sync().data.project,
             strategy: "git_worktree",
             directory: getDirectory(projectDirectory),
@@ -362,7 +361,6 @@ export function createPromptSubmit(input: PromptSubmitInput) {
             })
             return undefined
           })
-
         if (!createdWorktree) return
         WorktreeState.pending(sdk().scope, createdWorktree.directory)
         sessionDirectory = createdWorktree.directory
@@ -382,7 +380,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
     let session = input.info()
     if (!session && isNewSession) {
       const created = await sdk()
-        .currentApi.session.create({
+        .api.session.create({
           agent: currentAgent.name,
           model: { id: currentModel.id, providerID: currentModel.provider.id, variant },
           location: { directory: sessionDirectory },
@@ -473,7 +471,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
       clearInput()
       const eventID = Event.ID.create()
       sdk()
-        .currentApi.session.shell({
+        .api.session.shell({
           sessionID: session.id,
           id: eventID,
           command: text,
@@ -497,7 +495,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
         const messageID = Identifier.ascending("message")
         serverSync().session.set("session_status", session.id, { type: "busy" })
         sdk()
-          .currentApi.session.command({
+          .api.session.command({
             sessionID: session.id,
             id: messageID,
             command: commandName,
@@ -594,7 +592,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
     }
 
     void sendFollowupDraft({
-      api: sdk().currentApi.session,
+      api: sdk().api.session,
       sync: sync(),
       serverSync: serverSync(),
       draft,

+ 13 - 12
packages/app/src/components/settings-general.tsx

@@ -13,7 +13,7 @@ import { useLanguage } from "@/context/language"
 import { usePermission } from "@/context/permission"
 import { usePlatform, type DisplayBackend } from "@/context/platform"
 import { useServerSync } from "@/context/server-sync"
-import { useServerProtocol, useServerSDK } from "@/context/server-sdk"
+import { useServerSDK } from "@/context/server-sdk"
 import { useUpdaterAction } from "./updater-action"
 import {
   monoDefault,
@@ -125,11 +125,12 @@ export const SettingsGeneral: Component = () => {
 
   const serverSync = useServerSync()
   const serverSdk = useServerSDK()
-  const protocol = useServerProtocol()
 
   const [shells] = createResource(
-    () => (protocol() === "v1" ? serverSdk() : undefined),
-    (sdk) => sdk.legacy.pty.shells().catch(() => [] as ShellOption[]),
+    async () => {
+      // TODO: Restore executable shell discovery; V2 shell.list only lists shell processes.
+      return [] as ShellOption[]
+    },
     { initialValue: [] as ShellOption[] },
   )
 
@@ -320,13 +321,13 @@ export const SettingsGeneral: Component = () => {
           </div>
         </SettingsRow>
 
-        <Show when={protocol() === "v1"}>
-          <SettingsRow
-            title={language.t("settings.general.row.shell.title")}
-            description={language.t("settings.general.row.shell.description")}
-          >
+        <SettingsRow
+          title={language.t("settings.general.row.shell.title")}
+          description={language.t("settings.general.row.shell.description")}
+        >
           <Select
             data-action="settings-shell"
+            disabled
             options={shellOptions()}
             current={shellOptions().find((o) => o.value === currentShell()) ?? autoOption}
             value={(o) => o.id}
@@ -334,15 +335,15 @@ export const SettingsGeneral: Component = () => {
             onSelect={(option) => {
               if (!option) return
               if (option.value === currentShell()) return
-              serverSync().updateConfig({ shell: option.value })
+              // TODO: Restore config writes when the V2 client exposes a config API.
+              // void serverSync().updateConfig({ shell: option.value })
             }}
             variant="secondary"
             size="small"
             triggerVariant="settings"
             triggerStyle={{ "min-width": "180px" }}
           />
-          </SettingsRow>
-        </Show>
+        </SettingsRow>
 
         <SettingsRow
           title={language.t("settings.general.row.reasoningSummaries.title")}

+ 6 - 13
packages/app/src/components/settings-providers.tsx

@@ -6,7 +6,7 @@ import { showToast } from "@/utils/toast"
 import { popularProviders, useProviders } from "@/hooks/use-providers"
 import { createMemo, type Component, For, Show } from "solid-js"
 import { useLanguage } from "@/context/language"
-import { useServerProtocol, useServerSDK } from "@/context/server-sdk"
+import { useServerSDK } from "@/context/server-sdk"
 import { useServerSync } from "@/context/server-sync"
 import { DialogConnectProvider, useProviderConnectController } from "./dialog-connect-provider"
 import { DialogCustomProvider } from "./dialog-custom-provider"
@@ -39,7 +39,6 @@ const SettingsProvidersContent: Component<{ onBack?: () => void }> = (props) =>
   const dialog = useDialog()
   const language = useLanguage()
   const serverSDK = useServerSDK()
-  const protocol = useServerProtocol()
   const serverSync = useServerSync()
   const providers = useProviders(() => undefined)
   const providerConnect = useProviderConnectController({ onBack: props.onBack })
@@ -84,8 +83,7 @@ const SettingsProvidersContent: Component<{ onBack?: () => void }> = (props) =>
     return language.t("settings.providers.tag.other")
   }
 
-  const canDisconnect = (item: ProviderItem) =>
-    source(item) !== "env" && (protocol() === "v1" || !isConfigCustom(item.id))
+  const canDisconnect = (item: ProviderItem) => source(item) !== "env" && !isConfigCustom(item.id)
 
   const note = (id: string) => PROVIDER_NOTES.find((item) => item.match(id))?.key
 
@@ -98,7 +96,7 @@ const SettingsProvidersContent: Component<{ onBack?: () => void }> = (props) =>
   }
 
   const disableProvider = async (providerID: string, name: string) => {
-    if (protocol() !== "v1") return
+    return
     const before = serverSync().data.config.disabled_providers ?? []
     const next = before.includes(providerID) ? before : [...before, providerID]
     serverSync().set("config", "disabled_providers", next)
@@ -121,18 +119,13 @@ const SettingsProvidersContent: Component<{ onBack?: () => void }> = (props) =>
   }
 
   const disconnect = async (providerID: string, name: string) => {
-    if (isConfigCustom(providerID)) {
-      await serverSDK().legacy.auth.remove({ providerID }).catch(() => undefined)
-      await disableProvider(providerID, name)
-      return
-    }
     await serverSDK()
-      .currentApi.integration.get({ integrationID: providerID })
+      .api.integration.get({ integrationID: providerID })
       .then(async (integration) => {
         const credentials = integration.data?.connections.filter((item) => item.type === "credential") ?? []
         if (credentials.length === 0) throw new Error(`No removable credentials found for ${name}`)
         await Promise.all(
-          credentials.map((credential) => serverSDK().currentApi.credential.remove({ credentialID: credential.id })),
+          credentials.map((credential) => serverSDK().api.credential.remove({ credentialID: credential.id })),
         )
         showToast({
           variant: "success",
@@ -223,7 +216,7 @@ const SettingsProvidersContent: Component<{ onBack?: () => void }> = (props) =>
               )}
             </For>
 
-            <Show when={protocol() === "v1"}>
+            <Show when={false}>
               <div
                 class="flex items-center justify-between gap-4 min-h-16 border-b border-border-weak-base last:border-none flex-wrap py-3"
                 data-component="custom-provider-section"

+ 10 - 12
packages/app/src/components/settings-v2/general.tsx

@@ -10,7 +10,6 @@ import { useLanguage } from "@/context/language"
 import { usePermission } from "@/context/permission"
 import { usePlatform } from "@/context/platform"
 import { useServerSync } from "@/context/server-sync"
-import { useServerProtocol, useServerSDK } from "@/context/server-sdk"
 import { useUpdaterAction } from "../updater-action"
 import {
   monoDefault,
@@ -91,8 +90,6 @@ export const SettingsGeneralV2: Component<{
   const dialog = useDialog()
   const settings = useSettings()
   const serverSync = useServerSync()
-  const serverSdk = useServerSDK()
-  const protocol = useServerProtocol()
   const mobile = createMediaQuery("(max-width: 767px)")
 
   const updater = useUpdaterAction()
@@ -123,8 +120,11 @@ export const SettingsGeneralV2: Component<{
   const themeOptions = createMemo<ThemeOption[]>(() => theme.ids().map((id) => ({ id, name: theme.name(id) })))
 
   const [shells] = createResource(
-    () => (protocol() === "v1" ? serverSdk() : undefined),
-    (sdk) => sdk.legacy.pty.shells().catch(() => [] as ShellOption[]),
+    async () => {
+      // TODO: Restore executable shell discovery when the V2 client exposes it.
+      // return (await sdk.api.pty.shells()).data
+      return [] as ShellOption[]
+    },
     { initialValue: [] as ShellOption[] },
   )
 
@@ -279,11 +279,10 @@ export const SettingsGeneralV2: Component<{
           </div>
         </SettingsRowV2>
 
-        <Show when={protocol() === "v1"}>
-          <SettingsRowV2
-            title={language.t("settings.general.row.shell.title")}
-            description={language.t("settings.general.row.shell.description")}
-          >
+        <SettingsRowV2
+          title={language.t("settings.general.row.shell.title")}
+          description={language.t("settings.general.row.shell.description")}
+        >
           <SelectV2
             appearance="inline"
             data-action="settings-shell"
@@ -299,8 +298,7 @@ export const SettingsGeneralV2: Component<{
               serverSync().updateConfig({ shell: option.value })
             }}
           />
-          </SettingsRowV2>
-        </Show>
+        </SettingsRowV2>
 
         <SettingsRowV2
           title={language.t("settings.general.row.reasoningSummaries.title")}

+ 6 - 13
packages/app/src/components/settings-v2/providers.tsx

@@ -6,7 +6,7 @@ import { showToast } from "@/utils/toast"
 import { popularProviders, useProviders } from "@/hooks/use-providers"
 import { createMemo, type Accessor, type Component, For, Show } from "solid-js"
 import { useLanguage } from "@/context/language"
-import { useServerProtocol, useServerSDK } from "@/context/server-sdk"
+import { useServerSDK } from "@/context/server-sdk"
 import { useServerSync } from "@/context/server-sync"
 import { DialogConnectProvider, useProviderConnectController } from "../dialog-connect-provider"
 import { DialogCustomProvider } from "../dialog-custom-provider"
@@ -36,7 +36,6 @@ export const SettingsProvidersV2: Component<{
   const dialog = useDialog()
   const language = useLanguage()
   const serverSdk = useServerSDK()
-  const protocol = useServerProtocol()
   const serverSync = useServerSync()
   const providers = useProviders(props.directory)
   const providerConnect = useProviderConnectController({ onBack: props.onBack })
@@ -81,8 +80,7 @@ export const SettingsProvidersV2: Component<{
     return language.t("settings.providers.tag.other")
   }
 
-  const canDisconnect = (item: ProviderItem) =>
-    source(item) !== "env" && (protocol() === "v1" || !isConfigCustom(item.id))
+  const canDisconnect = (item: ProviderItem) => source(item) !== "env" && !isConfigCustom(item.id)
 
   const note = (id: string) => PROVIDER_NOTES.find((item) => item.match(id))?.key
 
@@ -95,7 +93,7 @@ export const SettingsProvidersV2: Component<{
   }
 
   const disableProvider = async (providerID: string, name: string) => {
-    if (protocol() !== "v1") return
+    return
     const before = serverSync().data.config.disabled_providers ?? []
     const next = before.includes(providerID) ? before : [...before, providerID]
     serverSync().set("config", "disabled_providers", next)
@@ -118,20 +116,15 @@ export const SettingsProvidersV2: Component<{
   }
 
   const disconnect = async (providerID: string, name: string) => {
-    if (isConfigCustom(providerID)) {
-      await serverSdk().legacy.auth.remove({ providerID }).catch(() => undefined)
-      await disableProvider(providerID, name)
-      return
-    }
     const location = props.directory() ? { directory: props.directory() } : undefined
     await serverSdk()
-      .currentApi.integration.get({ integrationID: providerID, location })
+      .api.integration.get({ integrationID: providerID, location })
       .then(async (integration) => {
         const credentials = integration.data?.connections.filter((item) => item.type === "credential") ?? []
         if (credentials.length === 0) throw new Error(`No removable credentials found for ${name}`)
         await Promise.all(
           credentials.map((credential) =>
-            serverSdk().currentApi.credential.remove({ credentialID: credential.id, location }),
+            serverSdk().api.credential.remove({ credentialID: credential.id, location }),
           ),
         )
         showToast({
@@ -229,7 +222,7 @@ export const SettingsProvidersV2: Component<{
               )}
             </For>
 
-            <Show when={protocol() === "v1"}>
+            <Show when={false}>
               <div class="settings-v2-provider-row" data-component="custom-provider-section">
                 <div class="settings-v2-provider-lead">
                   <ProviderIcon

+ 16 - 20
packages/app/src/components/status-popover-body.tsx

@@ -5,7 +5,7 @@ import { Switch } from "@opencode-ai/ui/switch"
 import { Tabs } from "@opencode-ai/ui/tabs"
 import { showToast } from "@/utils/toast"
 import { useNavigate } from "@solidjs/router"
-import { type Accessor, createEffect, createMemo, For, type JSXElement, onCleanup, Show } from "solid-js"
+import { type Accessor, createEffect, createMemo, createResource, For, type JSXElement, onCleanup, Show } from "solid-js"
 import { createStore } from "solid-js/store"
 import { ServerHealthIndicator, ServerRow } from "@/components/server/server-row"
 import { useLanguage } from "@/context/language"
@@ -16,7 +16,7 @@ import { type ServerHealth } from "@/utils/server-health"
 import { useGlobal } from "@/context/global"
 import { useSettings } from "@/context/settings"
 import { useMcpToggle } from "@/context/mcp"
-import { useServerProtocol } from "@/context/server-sdk"
+import { useSDK } from "@/context/sdk"
 
 const pluginEmptyMessage = (value: string, file: string): JSXElement => {
   const parts = value.split(file)
@@ -251,6 +251,7 @@ function ServerStatusList(props: { state: ServerStatusState }) {
 
 export function StatusPopoverBody(props: { shown: Accessor<boolean> }) {
   const sync = useSync()
+  const sdk = useSDK()
   const global = useGlobal()
   const server = useServer()
   const platform = usePlatform()
@@ -258,7 +259,6 @@ export function StatusPopoverBody(props: { shown: Accessor<boolean> }) {
   const language = useLanguage()
   const navigate = useNavigate()
   const settings = useSettings()
-  const protocol = useServerProtocol()
 
   const fail = (err: unknown) => {
     showToast({
@@ -279,9 +279,7 @@ export function StatusPopoverBody(props: { shown: Accessor<boolean> }) {
     dialogRun += 1
   })
   const sortedServers = createMemo(() => {
-    const list = settings.general.newLayoutDesigns()
-      ? global.servers.list()
-      : global.servers.list().filter((x) => global.ensureServerCtx(x).sdk.protocolKind() !== "v2")
+    const list = global.servers.list()
     return listServersByHealth(list, server.key, global.servers.health)
   })
   const toggleMcp = useMcpToggle()
@@ -291,9 +289,11 @@ export function StatusPopoverBody(props: { shown: Accessor<boolean> }) {
   const mcpConnected = createMemo(() => mcpNames().filter((name) => mcpStatus(name) === "connected").length)
   const lspItems = createMemo(() => sync().data.lsp ?? [])
   const lspCount = createMemo(() => lspItems().length)
-  const plugins = createMemo(() =>
-    (sync().data.config.plugin ?? []).map((item) => (typeof item === "string" ? item : item[0])),
+  const [pluginList] = createResource(
+    () => (props.shown() ? sdk().directory : undefined),
+    (directory) => sdk().api.plugin.list({ location: { directory } }).then((result) => result.data),
   )
+  const plugins = createMemo(() => (pluginList.latest ?? []).map((item) => item.id))
   const pluginCount = createMemo(() => plugins().length)
   const pluginEmpty = createMemo(() => pluginEmptyMessage(language.t("dialog.plugins.empty"), "opencode.json"))
 
@@ -318,13 +318,11 @@ export function StatusPopoverBody(props: { shown: Accessor<boolean> }) {
             {mcpConnected() > 0 ? `${mcpConnected()} ` : ""}
             {language.t("status.popover.tab.mcp")}
           </Tabs.Trigger>
-          <Show when={protocol() === "v1"}>
-            <Tabs.Trigger value="lsp" data-slot="tab" class="text-12-regular">
-              {lspCount() > 0 ? `${lspCount()} ` : ""}
-              {language.t("status.popover.tab.lsp")}
-            </Tabs.Trigger>
-          </Show>
-          <Show when={protocol() === "v1"}>
+          <Tabs.Trigger value="lsp" data-slot="tab" class="text-12-regular">
+            {lspCount() > 0 ? `${lspCount()} ` : ""}
+            {language.t("status.popover.tab.lsp")}
+          </Tabs.Trigger>
+          <Show when={true}>
             <Tabs.Trigger value="plugins" data-slot="tab" class="text-12-regular">
               {pluginCount() > 0 ? `${pluginCount()} ` : ""}
               {language.t("status.popover.tab.plugins")}
@@ -461,8 +459,7 @@ export function StatusPopoverBody(props: { shown: Accessor<boolean> }) {
           </div>
         </Tabs.Content>
 
-        <Show when={protocol() === "v1"}>
-          <Tabs.Content value="lsp">
+        <Tabs.Content value="lsp">
           <div class="flex flex-col px-2 pb-2">
             <div class="flex flex-col p-3 bg-background-base rounded-sm min-h-14">
               <Show
@@ -488,10 +485,9 @@ export function StatusPopoverBody(props: { shown: Accessor<boolean> }) {
               </Show>
             </div>
           </div>
-          </Tabs.Content>
-        </Show>
+        </Tabs.Content>
 
-        <Show when={protocol() === "v1"}>
+        <Show when={true}>
           <Tabs.Content value="plugins">
             <div class="flex flex-col px-2 pb-2">
               <div class="flex flex-col p-3 bg-background-base rounded-sm min-h-14">

+ 4 - 26
packages/app/src/components/terminal.tsx

@@ -11,13 +11,11 @@ import { matchKeybind, parseKeybind } from "@/context/command"
 import { useLanguage } from "@/context/language"
 import { usePlatform } from "@/context/platform"
 import { useSDK } from "@/context/sdk"
-import { useServerSDK } from "@/context/server-sdk"
 import { terminalFontFamily, useSettings } from "@/context/settings"
 import type { LocalPTY } from "@/context/terminal"
 import { disposeIfDisposable, getHoveredLinkText, setOptionIfSupported } from "@/utils/runtime-adapters"
 import { terminalWriter } from "@/utils/terminal-writer"
 import { terminalWebSocketURL } from "@/utils/terminal-websocket-url"
-import { authTokenFromCredentials } from "@/utils/server"
 
 const TOGGLE_TERMINAL_ID = "terminal.toggle"
 const DEFAULT_TOGGLE_TERMINAL_KEYBIND = "ctrl+`"
@@ -176,13 +174,8 @@ export const Terminal = (props: TerminalProps) => {
   const settings = useSettings()
   const theme = useTheme()
   const language = useLanguage()
-  // Terminal captures its connection for the PTY lifetime, so callers must key it per server/session.
-  const connection = useServerSDK()().server
   const directory = sdk().directory
   const url = sdk().url
-  const auth = connection.http
-  const username = auth?.username ?? "opencode"
-  const password = auth?.password ?? ""
   let container!: HTMLDivElement
   const [local, others] = splitProps(props, [
     "pty",
@@ -241,7 +234,7 @@ export const Terminal = (props: TerminalProps) => {
 
   const pushSize = async (cols: number, rows: number) => {
     return sdk()
-      .currentApi.pty.update({
+      .api.pty.update({
         ptyID: id,
         location: { directory },
         size: { cols, rows },
@@ -523,7 +516,7 @@ export const Terminal = (props: TerminalProps) => {
 
       const gone = async () => {
         return sdk()
-          .currentApi.pty.get({ ptyID: id, location: { directory } })
+          .api.pty.get({ ptyID: id, location: { directory } })
           .then((result) => result.data.status === "exited")
           .catch((err) => {
             if (err && typeof err === "object" && "_tag" in err && err._tag === "PtyNotFoundError") return true
@@ -533,23 +526,8 @@ export const Terminal = (props: TerminalProps) => {
       }
 
       const connectToken = async () => {
-        const endpoint = new URL(`/api/pty/${encodeURIComponent(id)}/connect-token`, url)
-        endpoint.searchParams.set("location[directory]", directory)
-        const response = await (platform.fetch ?? globalThis.fetch)(endpoint, {
-          method: "POST",
-          headers: {
-            "x-opencode-ticket": "1",
-            ...(password
-              ? { Authorization: `Basic ${authTokenFromCredentials({ username, password })}` }
-              : undefined),
-          },
-        })
-        if (response.status === 403)
-          throw new Error("PTY connect ticket rejected by origin or CSRF checks. Check the server CORS config.")
-        if (!response.ok) throw new Error(`PTY connect ticket failed with ${response.status}`)
-        const result = (await response.json()) as { data?: { ticket?: string } }
-        if (!result.data?.ticket) throw new Error("PTY connect ticket response did not include a ticket")
-        return result.data.ticket
+        // TODO: Add PTY tickets when the V2 client exposes a connect-token API.
+        return undefined
       }
 
       const retry = (err: unknown) => {

+ 2 - 6
packages/app/src/components/titlebar-tab-nav.tsx

@@ -4,7 +4,6 @@ import { createResizeObserver } from "@solid-primitives/resize-observer"
 import { createMutation } from "@tanstack/solid-query"
 import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
 import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
-import { displayLabel } from "@opencode-ai/util/session-title-fallback"
 import { useGlobal } from "@/context/global"
 import { ServerConnection, serverName } from "@/context/server"
 import { displayName, projectForSession } from "@/pages/layout/helpers"
@@ -55,10 +54,7 @@ export function TabNavItem(props: {
     if (!session) return
     return projectForSession(session, serverCtx()?.projects.list() ?? [])
   })
-  const title = createMemo(() => {
-    const session = props.session()
-    return session ? displayLabel(session) : props.fallbackTitle
-  })
+  const title = createMemo(() => props.session()?.title ?? props.fallbackTitle)
 
   const projectName = createMemo(() => {
     const session = props.session()
@@ -306,7 +302,7 @@ export function TabNavItem(props: {
       }}
       data={{
         projectName: projectName(),
-        title: title(),
+        title: props.session()?.title,
         path: previewPath(),
         serverName: serverLabel(),
       }}

+ 1 - 1
packages/app/src/components/titlebar-tab-strip.tsx

@@ -105,7 +105,7 @@ function SessionTabEntry(props: {
 
     ctx.sync.session.remember({ ...value, title })
     try {
-      await ctx.sdk.currentApi.session.rename({ sessionID: value.id, title })
+      await ctx.sdk.api.session.rename({ sessionID: value.id, title })
     } catch (err) {
       const current = session()
       const currentCtx = props.serverCtx()

+ 1 - 1
packages/app/src/components/titlebar.tsx

@@ -192,7 +192,7 @@ export function Titlebar(props: { update?: TitlebarUpdate; debugTools?: { visibl
                 return conn ? { route, sdk: global.ensureServerCtx(conn).sdk } : undefined
               },
               ({ route, sdk }) =>
-                sdk.currentApi.session
+                sdk.api.session
                   .get({ sessionID: route.sessionId })
                   .then(normalizeSessionInfo)
                   .catch(() => {}),

+ 3 - 10
packages/app/src/context/directory-sync.ts

@@ -26,7 +26,6 @@ export const createDirSyncContext = (
   serverSync: ReturnType<typeof createServerSyncContextInner>,
   serverSDK: ReturnType<typeof createServerSdkContext>,
 ) => {
-  const client = serverSDK.createClient({ directory, throwOnError: true })
   const current = createMemo(() => serverSync.child(directory, { mcp: true }))
   const absolute = (path: string) => (current()[0].path.directory + "/" + path).replace("//", "/")
   const data = new Proxy({} as State, {
@@ -124,7 +123,7 @@ export const createDirSyncContext = (
       fetch: async (count = 10) => {
         const [store, setStore] = current()
         setStore("limit", (value) => value + count)
-        const response = await serverSDK.currentApi.session.list({ directory, limit: store.limit, order: "desc" })
+        const response = await serverSDK.api.session.list({ directory, limit: store.limit, order: "desc" })
         const sessions = response.data
           .map(normalizeSessionInfo)
           .sort((a, b) => cmp(a.id, b.id))
@@ -134,14 +133,8 @@ export const createDirSyncContext = (
       },
       more: createMemo(() => current()[0].session.length >= current()[0].limit),
       archive: async (sessionID: string) => {
-        await serverSDK.legacy.session.archive(sessionID, directory)
-        current()[1](
-          "session",
-          produce((draft) => {
-            const match = Binary.search(draft, sessionID, (session) => session.id)
-            if (match.found) draft.splice(match.index, 1)
-          }),
-        )
+        // TODO: Restore archiving when the V2 client exposes a session archive API.
+        void sessionID
       },
     },
     mcp: {

+ 3 - 3
packages/app/src/context/file.tsx

@@ -81,7 +81,7 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
       normalizeDir: path.normalizeDir,
       list: (dir) =>
         sdk()
-          .currentApi.file.list({ path: dir, location: { directory: scope() } })
+          .api.file.list({ path: dir, location: { directory: scope() } })
           .then((x) =>
             x.data.map((entry) => ({
               ...entry,
@@ -188,7 +188,7 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
       setLoading(file)
 
       const promise = sdk()
-        .currentApi.file.read({ path: file, location: { directory } })
+        .api.file.read({ path: file, location: { directory } })
         .then((data) => {
           if (scope() !== directory) return
           const content = { type: "text" as const, content: new TextDecoder().decode(data) }
@@ -212,7 +212,7 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
 
     const search = (query: string, dirs: "true" | "false", options?: { limit?: number; signal?: AbortSignal }) =>
       serverSDK()
-        .currentApi.file.find(
+        .api.file.find(
           {
             location: { directory: sdk().directory },
             query,

+ 18 - 127
packages/app/src/context/global-sync/bootstrap.test.ts

@@ -1,12 +1,8 @@
 import { describe, expect, test } from "bun:test"
-import { createStore } from "solid-js/store"
 import { QueryClient } from "@tanstack/solid-query"
-import type { Config, Project } from "@/types"
-import type { LegacyCapabilities } from "@/utils/server-compat"
 import type { AgentApi, CatalogApi, CommandApi, ReferenceApi } from "@opencode-ai/client/promise"
 import type { NormalizedProviderListResponse } from "@opencode-ai/session-ui/context"
 import {
-  bootstrapDirectory,
   loadAgentsQuery,
   loadCommands,
   loadPathQuery,
@@ -14,139 +10,19 @@ import {
   loadProvidersQuery,
   loadReferencesQuery,
 } from "./bootstrap"
-import type { State, VcsCache } from "./types"
 import { ServerScope } from "@/utils/server-scope"
 import type { ServerApi } from "@/utils/server"
 
 type ProjectApi = ServerApi["project"]
 
-const provider = { all: new Map(), connected: [], default: {} } satisfies NormalizedProviderListResponse
-const api = {
-  agent: { list: async () => ({ location: {}, data: [] }) },
-  provider: { list: async () => ({ location: {}, data: [] }) },
-  model: {
-    list: async () => ({ location: {}, data: [] }),
-    default: async () => ({ location: {}, data: null }),
-  },
-  permission: { request: { list: async () => ({ location: {}, data: [] }) } },
-  project: {
-    list: async () => [],
-    current: async () => ({ id: "project", directory: "/project" }),
-  },
-  question: { request: { list: async () => ({ location: {}, data: [] }) } },
-  reference: { list: async () => ({ location: {}, data: [] }) },
-  vcs: { get: async () => ({ location: {}, data: {} }) },
-} as unknown as ServerApi
-
-function directoryState() {
-  return createStore<State>({
-    status: "loading",
-    agent: [],
-    command: [],
-    reference: [],
-    project: "",
-    projectMeta: undefined,
-    icon: undefined,
-    provider_ready: true,
-    provider,
-    config: {},
-    path: { state: "", config: "", worktree: "/project", directory: "/project", home: "/home" },
-    session: [],
-    sessionTotal: 0,
-    session_status: {},
-    session_working(id: string) {
-      return this.session_status[id]?.type !== "idle"
-    },
-    session_diff: {},
-    todo: {},
-    permission: {},
-    question: {},
-    mcp_ready: true,
-    mcp: {},
-    mcp_resource: {},
-    lsp_ready: true,
-    lsp: [],
-    vcs: undefined,
-    limit: 5,
-    message: {},
-    session_message: {},
-    part: {},
-    part_text_accum_delta: {},
-  })
-}
-
-describe("bootstrapDirectory", () => {
-  test("uses current MCP endpoints while retaining unsupported v1 directory reads", async () => {
-    const mcpReads: string[] = []
-    const [store, setStore] = directoryState()
-    const currentApi = {
-      ...api,
-      command: {
-        list: async () => {
-          mcpReads.push("command")
-          return { location: {}, data: [] }
-        },
-      },
-      mcp: {
-        list: async () => {
-          mcpReads.push("status")
-          return { location: {}, data: [] }
-        },
-        resource: {
-          catalog: async () => {
-            mcpReads.push("resource")
-            return { location: {}, data: { resources: [], templates: [] } }
-          },
-        },
-      },
-    } as unknown as ServerApi
-
-    await bootstrapDirectory({
-      directory: "/project",
-      scope: ServerScope.local,
-      mcp: true,
-      global: {
-        config: {} satisfies Config,
-        path: { state: "", config: "", worktree: "/project", directory: "/project", home: "/home" },
-        project: [{ id: "project", worktree: "/project" } as Project],
-        provider,
-      },
-      legacy: { config: { directory: async () => ({}) } } as unknown as LegacyCapabilities,
-      api: currentApi,
-      store,
-      setStore,
-      vcsCache: { setStore() {} } as unknown as VcsCache,
-      loadSessions() {},
-      translate: (key) => key,
-      queryClient: new QueryClient(),
-      protocol: Promise.resolve("v1"),
-    })
-
-    expect(store.status).toBe("partial")
-
-    await new Promise((resolve) => setTimeout(resolve, 80))
-
-    expect(store.status).toBe("complete")
-    expect(mcpReads.sort()).toEqual(["command", "resource", "status"])
-  })
-})
-
 describe("query keys", () => {
   test("partitions identical directories by server scope", () => {
-    const location = {} as Parameters<typeof loadPathQuery>[2]
     const api = {} as CatalogApi
+    const location = {} as ServerApi["location"]
     const remote = "https://debian.example" as typeof ServerScope.local
 
-    expect([...loadPathQuery(ServerScope.local, "/repo", location).queryKey]).toEqual([
-      "local",
-      "/repo",
-      "path",
-    ])
-    expect([...loadPathQuery(remote, "/repo", location).queryKey]).toEqual([
-      "https://debian.example",
-      "/repo",
-      "path",
-    ])
+    expect([...loadPathQuery(ServerScope.local, "/repo", location).queryKey]).toEqual(["local", "/repo", "path"])
+    expect([...loadPathQuery(remote, "/repo", location).queryKey]).toEqual(["https://debian.example", "/repo", "path"])
     expect([...loadProvidersQuery(remote, null, api).queryKey]).toEqual(["https://debian.example", null, "providers"])
   })
 
@@ -181,6 +57,21 @@ describe("query keys", () => {
     expect(result.connected).toEqual(["openai"])
   })
 
+  test("loads current location metadata", async () => {
+    const calls: unknown[] = []
+    const api = {
+      get: async (input: unknown) => {
+        calls.push(input)
+        return { directory: "/repo/subpath", project: { id: "project", directory: "/repo" } }
+      },
+    } as ServerApi["location"]
+
+    const result = await new QueryClient().fetchQuery(loadPathQuery(ServerScope.local, "/repo/subpath", api))
+
+    expect(calls).toEqual([{ location: { directory: "/repo/subpath" } }])
+    expect(result).toMatchObject({ directory: "/repo/subpath", worktree: "/repo" })
+  })
+
   test("loads agents from the current location-scoped endpoint", async () => {
     const calls: unknown[] = []
     const api = {

+ 8 - 27
packages/app/src/context/global-sync/bootstrap.ts

@@ -8,7 +8,6 @@ import type {
   ReferenceInfo,
   Session,
 } from "@/types"
-import type { LegacyCapabilities } from "@/utils/server-compat"
 import type {
   AgentListInput,
   AgentListOutput,
@@ -45,7 +44,6 @@ import { loadMcpQuery, loadMcpResourcesQuery } from "../server-sync"
 import { NormalizedProviderListResponse } from "@opencode-ai/session-ui/context"
 import { ScopedKey, type ServerScope } from "@/utils/server-scope"
 import { normalizeSessionInfo } from "@/utils/session"
-import type { ServerProtocol } from "@/utils/server-protocol"
 import type { ServerApi } from "@/utils/server"
 
 type GlobalStore = {
@@ -107,11 +105,11 @@ function showErrors(input: {
   })
 }
 
-export const loadGlobalConfigQuery = (scope: ServerScope, legacy: LegacyCapabilities, enabled = true) =>
+export const loadGlobalConfigQuery = (scope: ServerScope) =>
   queryOptions({
     queryKey: [scope, "config"],
-    queryFn: () => retry(() => legacy.config.global()),
-    enabled,
+    // TODO: Restore config loading when the V2 client exposes a config API.
+    queryFn: async (): Promise<Config> => ({}),
   })
 
 type ProjectApi = {
@@ -142,9 +140,7 @@ export const loadProjectsQuery = (scope: ServerScope, api: ProjectApi) =>
   })
 
 export async function bootstrapGlobal(input: {
-  legacy: LegacyCapabilities
   serverAPI: CatalogApi & { readonly location: LocationApi; readonly project: ProjectApi }
-  protocol?: Promise<ServerProtocol>
   scope: ServerScope
   requestFailedTitle: string
   translate: (key: string, vars?: Record<string, string | number>) => string
@@ -152,22 +148,18 @@ export async function bootstrapGlobal(input: {
   setGlobalStore: SetStoreFunction<GlobalStore>
   queryClient: QueryClient
 }) {
-  const protocol = await input.protocol
   const slow = [
-    protocol === "v1" && (() => input.queryClient.fetchQuery(loadGlobalConfigQuery(input.scope, input.legacy))),
+    () => input.queryClient.fetchQuery(loadGlobalConfigQuery(input.scope)),
     () =>
       input.queryClient.fetchQuery(
         loadProvidersQuery(input.scope, null, input.serverAPI),
       ),
-    () =>
-      input.queryClient.fetchQuery(
-        loadPathQuery(input.scope, null, input.serverAPI.location),
-      ),
+    () => input.queryClient.fetchQuery(loadPathQuery(input.scope, null, input.serverAPI.location)),
     () =>
       input.queryClient
         .fetchQuery(loadProjectsQuery(input.scope, input.serverAPI.project))
         .then((data) => input.setGlobalStore("project", data)),
-  ].filter(Boolean) as Array<() => Promise<unknown>>
+  ]
   await runAll(slow)
   // showErrors({
   //   errors: errors(),
@@ -279,7 +271,7 @@ export const loadPathQuery = (
   queryOptions<Path>({
     queryKey: [scope, directory, "path"],
     queryFn: () =>
-      retry(() => api.get(directory ? { location: { directory } } : undefined)).then((location) => ({
+      api.get(directory ? { location: { directory } } : undefined).then((location) => ({
         state: "",
         config: "",
         worktree: location.project.directory,
@@ -304,7 +296,6 @@ export async function bootstrapDirectory(input: {
   directory: string
   scope: ServerScope
   mcp: boolean
-  legacy: LegacyCapabilities
   api: CatalogApi & {
     readonly agent: AgentListApi
     readonly command: CommandListApi
@@ -330,7 +321,6 @@ export async function bootstrapDirectory(input: {
   }
   queryClient: QueryClient
   session?: ServerSession
-  protocol?: Promise<ServerProtocol>
 }) {
   const loading = input.store.status !== "complete"
   const seededProject = projectID(input.directory, input.global.project)
@@ -352,13 +342,6 @@ export async function bootstrapDirectory(input: {
         input.queryClient
           .ensureQueryData(loadAgentsQuery(input.scope, input.directory, input.api.agent))
           .then((data) => input.setStore("agent", data)),
-      (await input.protocol) === "v1" &&
-        (() =>
-          retry(() =>
-            input.legacy.config
-              .directory(input.directory)
-              .then((config) => input.setStore("config", reconcile(config, { merge: false }))),
-          )),
       !seededProject &&
         (() =>
           retry(() => input.api.project.current({ location: { directory: input.directory } })).then((project) =>
@@ -367,9 +350,7 @@ export async function bootstrapDirectory(input: {
       !seededPath &&
         (() =>
           input.queryClient
-            .ensureQueryData(
-              loadPathQuery(input.scope, input.directory, input.api.location),
-            )
+            .ensureQueryData(loadPathQuery(input.scope, input.directory, input.api.location))
             .then((data) => {
               const next = projectID(data.directory ?? input.directory, input.global.project)
               if (next) input.setStore("project", next)

+ 1 - 4
packages/app/src/context/global-sync/child-store.ts

@@ -191,10 +191,7 @@ export function createChildStoreManager(input: {
           const pathQuery = useQuery(() => ({ ...input.queryOptions.path(key), enabled: instanceQueriesEnabled() }))
           const mcpQuery = useQuery(() => ({ ...input.queryOptions.mcp(key), enabled: mcpEnabled() }))
           const mcpResourceQuery = useQuery(() => ({ ...input.queryOptions.mcpResources(key), enabled: mcpEnabled() }))
-          const lspQuery = useQuery(() => {
-            const options = input.queryOptions.lsp(key)
-            return { ...options, enabled: options.enabled !== false && instanceQueriesEnabled() }
-          })
+          const lspQuery = useQuery(() => ({ ...input.queryOptions.lsp(key), enabled: instanceQueriesEnabled() }))
           const providerQuery = useQuery(() => ({
             ...input.queryOptions.providers(key),
             enabled: instanceQueriesEnabled(),

+ 7 - 11
packages/app/src/context/global-sync/home-session-index.test.ts

@@ -33,7 +33,7 @@ describe("Home V2 session index", () => {
     const calls: unknown[] = []
     const result = await loadHomeSessionIndex(async (input) => {
       calls.push(input)
-      return { data: { data: [session({ id: "root" })], cursor: {} } }
+      return { data: [session({ id: "root" })], cursor: {} }
     })
 
     expect(result.sessions).toHaveLength(1)
@@ -48,15 +48,13 @@ describe("Home V2 session index", () => {
         calls.push({ input, signal: options.signal })
         if (!("cursor" in input)) {
           return {
-            data: {
-              data: Array.from({ length: HOME_V2_SESSION_PAGE_LIMIT }, (_, index) =>
-                session({ id: `page-1-${index}` }),
-              ),
-              cursor: { next: "next-page" },
-            },
+            data: Array.from({ length: HOME_V2_SESSION_PAGE_LIMIT }, (_, index) =>
+              session({ id: `page-1-${index}` }),
+            ),
+            cursor: { next: "next-page" },
           }
         }
-        return { data: { data: [session({ id: "page-2" })], cursor: {} } }
+        return { data: [session({ id: "page-2" })], cursor: {} }
       },
       0,
       controller.signal,
@@ -145,10 +143,8 @@ describe("Home V2 session index", () => {
     expect(homeSessionIndexSessions({ sessions: initial, eventSequence: 1 }, events)[0]?.title).toBe("current")
   })
 
-  test("refetches after reconnect, disposal, and session moves", () => {
+  test("refetches after reconnect", () => {
     expect(homeSessionIndexRefresh("server.connected", false)).toEqual({ connected: true, refetch: false })
     expect(homeSessionIndexRefresh("server.connected", true)).toEqual({ connected: true, refetch: true })
-    expect(homeSessionIndexRefresh("global.disposed", true).refetch).toBe(true)
-    expect(homeSessionIndexRefresh("session.next.moved", true).refetch).toBe(true)
   })
 })

+ 6 - 10
packages/app/src/context/global-sync/home-session-index.ts

@@ -1,4 +1,5 @@
 import type { Event, Session, SessionV2Info, V2SessionListResponse } from "@/types"
+import { withTimestampedFallback } from "@opencode-ai/util/session-title-fallback"
 import type { QueryClient } from "@tanstack/solid-query"
 import { trimSessions } from "./session-trim"
 import { pathKey } from "@/utils/path-key"
@@ -21,13 +22,11 @@ export type HomeSessionIndex = {
 export const homeSessionIndexKey = (server: string) => ["home", "session-index", server] as const
 export const homeSessionEventsKey = (server: string) => ["home", "session-events", server] as const
 
-type HomeSessionPage = { data?: V2SessionListResponse }
-
 export async function loadHomeSessionIndex(
   list: (
     input: { limit: number; order: "desc"; cursor?: string },
     options: { signal?: AbortSignal },
-  ) => Promise<HomeSessionPage>,
+  ) => Promise<V2SessionListResponse>,
   eventSequence = 0,
   signal?: AbortSignal,
 ) {
@@ -43,7 +42,7 @@ export async function loadHomeSessionIndex(
       },
       { signal },
     )
-    const page = response.data!
+    const page = response
     data.push(...page.data)
     if (page.data.length < HOME_V2_SESSION_PAGE_LIMIT || !page.cursor.next)
       return { sessions: parseHomeSessionIndex(data), eventSequence }
@@ -75,10 +74,7 @@ export function homeSessionIndexSessions(index: HomeSessionIndex | undefined, ev
 
 export function homeSessionIndexRefresh(event: Event["type"], connected: boolean) {
   if (event === "server.connected") return { connected: true, refetch: connected }
-  return {
-    connected,
-    refetch: event === "global.disposed" || event === "session.next.moved",
-  }
+  return { connected, refetch: false }
 }
 
 export function createHomeSessionIndexCache(queryClient: QueryClient, server: string) {
@@ -145,7 +141,7 @@ export function retainHomeSessions(sessions: Session[], limit: number, now: numb
 export function applyHomeSessionEvent(sessions: Session[], event: HomeSessionEvent) {
   const info = event.properties.info
   const index = sessions.findIndex((session) => session.id === info.id)
-  if (event.type === "session.deleted" || info.parentID || typeof info.time.archived === "number") {
+    if (event.type === "session.deleted" || info.parentID || typeof info.time.archived === "number") {
     if (index === -1) return sessions
     return sessions.toSpliced(index, 1)
   }
@@ -165,7 +161,7 @@ function toLegacySummary(session: SessionV2Info): Session {
     parentID: session.parentID,
     cost: session.cost,
     tokens: session.tokens,
-    title: session.title,
+    title: withTimestampedFallback(session),
     agent: session.agent,
     model: session.model,
     version: "",

+ 1 - 14
packages/app/src/context/global-sync/utils.ts

@@ -58,20 +58,7 @@ export function normalizeProviderList(
   defaultModel?: ModelDefaultOutput["data"],
 ): NormalizedProviderListResponse {
   if (!Array.isArray(providers)) {
-    return {
-      ...providers,
-      all: new Map(
-        providers.all.map((provider) => [
-          provider.id,
-          {
-            ...provider,
-            models: Object.fromEntries(
-              Object.entries(provider.models).filter(([, model]) => model.status !== "deprecated"),
-            ),
-          },
-        ]),
-      ),
-    }
+    return providers
   }
   const all = new Map<string, Provider>()
 

+ 3 - 12
packages/app/src/context/layout.tsx

@@ -340,7 +340,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
           void removePersisted(target, platform)
 
           if (scope !== ServerScope.local) continue
-          const legacyKey = `${dir}/${entry.legacy}${session ? "/" + session : ""}.${entry.version}`
+          const legacyKey = `${dir}/${entry["legacy"]}${session ? "/" + session : ""}.${entry.version}`
           void removePersisted({ key: legacyKey }, platform)
         }
       }
@@ -572,17 +572,8 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
 
         const projectID = project.id
         void (async () => {
-          const sdk = serverSdk()
-          if ((await sdk.protocol) !== "v1") return
-          return sdk.legacy.project
-            .update({ projectID, directory: worktree, icon: { color } })
-            .then((response) => response.data)
-            .then((result) => {
-              if (!result) return
-              serverSync().set("project", (items) =>
-                items.map((item) => (item.id === result.id ? normalizeProjectInfo(result) : item)),
-              )
-            })
+          // TODO: Restore project color updates when the V2 client exposes a project update API.
+          void projectID
         })().catch(() => {
           if (colorRequested.get(worktree) === color) colorRequested.delete(worktree)
         })

+ 0 - 2
packages/app/src/context/permission.tsx

@@ -212,7 +212,6 @@ function createServerPermissionState(input: { sdk: ServerSDK; sync: ServerSync }
   )
 
   function enableConfiguredDirectory(directory: string) {
-    if (input.sdk.protocolKind() !== "v1") return
     if (meta.disposed || !ready()) return
     const [childStore] = input.sync.child(directory)
     if (childStore.config.permission !== "allow") return
@@ -250,7 +249,6 @@ function createServerPermissionState(input: { sdk: ServerSDK; sync: ServerSync }
         sessionID: request.sessionID,
         requestID: request.permissionID,
         reply: request.response,
-        location: request.directory ? { directory: request.directory } : undefined,
       })
       .catch(() => {
         responded.delete(request.permissionID)

+ 5 - 3
packages/app/src/context/sdk.tsx

@@ -1,10 +1,9 @@
 import { createSimpleContext } from "@opencode-ai/ui/context"
 import { type Accessor, createMemo } from "solid-js"
 import { type DirectorySDK, useServerSDK } from "./server-sdk"
+export type { DirectorySDK } from "./server-sdk"
 
-export type { DirectorySDK }
-
-export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
+const context = createSimpleContext({
   name: "SDK",
   // Resolves the directory-scoped SDK reactively from the (possibly changing) server.
   init: (props: { directory: string | Accessor<string> }) => {
@@ -15,3 +14,6 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
     })
   },
 })
+
+export const useSDK: () => Accessor<DirectorySDK> = context.use
+export const SDKProvider = context.provider

+ 25 - 150
packages/app/src/context/server-sdk.test.ts

@@ -1,7 +1,6 @@
 import { describe, expect, test } from "bun:test"
-import { adaptServerEvent, coalesceServerEvents, enqueueServerEvent, resumeStreamAfterPageShow } from "./server-sdk"
 import type { OpenCodeEvent } from "@opencode-ai/client/promise"
-import type { Event } from "@/types"
+import { adaptServerEvent, coalesceServerEvents, enqueueServerEvent, resumeStreamAfterPageShow } from "./server-sdk"
 
 describe("resumeStreamAfterPageShow", () => {
   test("restarts a stream only after a back-forward cache restore", () => {
@@ -16,7 +15,7 @@ describe("resumeStreamAfterPageShow", () => {
 })
 
 describe("adaptServerEvent", () => {
-  test("preserves V2 events while adapting permission requests for existing consumers", () => {
+  test("preserves current events while adapting permission requests for existing consumers", () => {
     const current = {
       id: "evt_1",
       created: 1,
@@ -31,6 +30,7 @@ describe("adaptServerEvent", () => {
     } as OpenCodeEvent
 
     expect(adaptServerEvent(current)).toMatchObject({
+      id: "evt_1",
       type: "permission.asked",
       properties: {
         id: "perm_1",
@@ -44,42 +44,24 @@ describe("adaptServerEvent", () => {
   })
 })
 
-describe("coalesceServerEvents", () => {
-  const delta = (value: string, field = "text", partID = "part") => ({
+describe("current event buffering", () => {
+  const delta = (id: string, value: string, ordinal = 0) => ({
     directory: "/repo",
-    payload: {
-      type: "message.part.delta",
-      properties: { messageID: "msg", partID, field, delta: value },
-    } as Event,
-  })
-
-  test("merges adjacent deltas for the same field", () => {
-    const first = delta("hello ")
-    const second = delta("world")
-    first.payload.id = "first"
-    second.payload.id = "second"
-    const result = coalesceServerEvents([first, second])
-
-    expect(result).toHaveLength(1)
-    expect(result[0]?.payload).toMatchObject({ id: "second", properties: { delta: "hello world" } })
+    payload: adaptServerEvent({
+      id,
+      created: 1,
+      type: "session.text.delta",
+      location: { directory: "/repo" },
+      data: { sessionID: "ses", assistantMessageID: "msg", ordinal, delta: value },
+    } as OpenCodeEvent),
   })
 
-  test("merges adjacent current text deltas", () => {
-    const current = (id: string, value: string) =>
-      adaptServerEvent({
-        id,
-        created: 1,
-        type: "session.text.delta",
-        location: { directory: "/repo" },
-        data: { sessionID: "ses", assistantMessageID: "msg", ordinal: 0, delta: value },
-      } as OpenCodeEvent)
-    const result = coalesceServerEvents([
-      { directory: "/repo", payload: current("evt_1", "hello ") },
-      { directory: "/repo", payload: current("evt_2", "world") },
-    ])
+  test("merges adjacent text deltas for the same message and ordinal", () => {
+    const result = coalesceServerEvents([delta("evt_1", "hello "), delta("evt_2", "world")])
 
     expect(result).toHaveLength(1)
     expect(result[0]?.payload.current).toMatchObject({ id: "evt_2", data: { delta: "hello world" } })
+    expect(result[0]?.payload.properties).toMatchObject({ delta: "hello world" })
   })
 
   test("coalesces current tool input deltas by tool ID", () => {
@@ -102,127 +84,20 @@ describe("coalesceServerEvents", () => {
     expect(result[1]?.payload.current).toMatchObject({ id: "evt_3", data: { id: "call_2", delta: "[]" } })
   })
 
-  test("preserves event boundaries and distinct fields", () => {
-    const status = {
-      directory: "/repo",
-      payload: { type: "session.status", properties: { sessionID: "ses", status: { type: "idle" } } } as Event,
-    }
-    const result = coalesceServerEvents([delta("a"), delta("b", "metadata"), status, delta("c")])
+  test("preserves boundaries between distinct delta streams", () => {
+    const events = [delta("evt_1", "a"), delta("evt_2", "b", 1), delta("evt_3", "c")]
 
-    expect(result.map((event) => event.payload.type)).toEqual([
-      "message.part.delta",
-      "message.part.delta",
-      "session.status",
-      "message.part.delta",
+    expect(coalesceServerEvents(events).map((event) => event.payload.current?.id)).toEqual([
+      "evt_1",
+      "evt_2",
+      "evt_3",
     ])
   })
 
-  test("preserves event ID order across interleaved deltas", () => {
-    const first = delta("a")
-    const other = delta("b", "text", "other")
-    const last = delta("c")
-    first.payload.id = "1"
-    other.payload.id = "2"
-    last.payload.id = "3"
-
-    const result = coalesceServerEvents([first, other, last])
-
-    expect(result.map((event) => event.payload.id)).toEqual(["1", "2", "3"])
-  })
-})
-
-describe("enqueueServerEvent", () => {
-  const partUpdated = (text: string) =>
-    ({
-      type: "message.part.updated",
-      properties: {
-        sessionID: "session",
-        part: { id: "part", sessionID: "session", messageID: "message", type: "text", text },
-      },
-    }) as Event
-
-  test("preserves part updates across message remove and re-add barriers", () => {
-    const events: Array<{ directory: string; payload: Event }> = []
-    const enqueue = (payload: Event) => enqueueServerEvent(events, { directory: "/repo", payload })
-
-    enqueue(partUpdated("old"))
-    enqueue({ type: "message.removed", properties: { sessionID: "session", messageID: "message" } } as Event)
-    enqueue({
-      type: "message.updated",
-      properties: {
-        sessionID: "session",
-        info: {
-          id: "message",
-          sessionID: "session",
-          role: "user",
-          time: { created: 1 },
-          agent: "build",
-          model: { providerID: "provider", modelID: "model" },
-        },
-      },
-    } as Event)
-    enqueue(partUpdated("new"))
-
-    expect(events.map((event) => event.payload.type)).toEqual([
-      "message.part.updated",
-      "message.removed",
-      "message.updated",
-      "message.part.updated",
-    ])
-  })
-
-  test("preserves deltas after a replacement snapshot", () => {
-    const events: Array<{ directory: string; payload: Event }> = []
-    const enqueue = (payload: Event) => enqueueServerEvent(events, { directory: "/repo", payload })
-
-    enqueue(partUpdated("a"))
-    enqueue(partUpdated("ab"))
-    enqueue({
-      type: "message.part.delta",
-      properties: { sessionID: "session", messageID: "message", partID: "part", field: "text", delta: "c" },
-    } as Event)
-
-    const result = coalesceServerEvents(events)
-    expect(result.map((event) => event.payload.type)).toEqual(["message.part.updated", "message.part.delta"])
-    expect(result[0]?.payload).toMatchObject({ properties: { part: { text: "ab" } } })
-    expect(result[1]?.payload).toMatchObject({ properties: { delta: "c" } })
-  })
-
-  test("preserves updates after session deletion", () => {
-    const events: Array<{ directory: string; payload: Event }> = []
-    const enqueue = (payload: Event) => enqueueServerEvent(events, { directory: "/repo", payload })
-
-    enqueue(partUpdated("old"))
-    enqueue({
-      type: "session.deleted",
-      properties: { sessionID: "session", info: { id: "session" } },
-    } as Event)
-    enqueue(partUpdated("new"))
-
-    expect(events.map((event) => event.payload.type)).toEqual([
-      "message.part.updated",
-      "session.deleted",
-      "message.part.updated",
-    ])
-  })
-
-  test("does not coalesce edge-triggered session statuses", () => {
-    const events: Array<{ directory: string; payload: Event }> = []
-    const enqueue = (status: "retry" | "busy") =>
-      enqueueServerEvent(events, {
-        directory: "/repo",
-        payload: {
-          type: "session.status",
-          properties: {
-            sessionID: "session",
-            status: status === "retry" ? { type: "retry", attempt: 1, message: "retry", next: 1 } : { type: "busy" },
-          },
-        } as Event,
-      })
-
-    enqueue("retry")
-    enqueue("busy")
+  test("preserves current event order when enqueuing", () => {
+    const events: Parameters<typeof enqueueServerEvent>[0] = []
+    ;[delta("evt_1", "a"), delta("evt_2", "b", 1)].forEach((event) => enqueueServerEvent(events, event))
 
-    expect(events).toHaveLength(2)
+    expect(events.map((event) => event.payload.current?.id)).toEqual(["evt_1", "evt_2"])
   })
 })

+ 17 - 137
packages/app/src/context/server-sdk.tsx

@@ -3,22 +3,14 @@ import type { Event, PermissionRequest } from "@/types"
 import { createSimpleContext } from "@opencode-ai/ui/context"
 import { createGlobalEmitter } from "@solid-primitives/event-bus"
 import { makeEventListener } from "@solid-primitives/event-listener"
-import { type Accessor, batch, createMemo, createResource, onCleanup, onMount } from "solid-js"
-import { createApiForServer, createSdkForServer, type ServerApi } from "@/utils/server"
+import { type Accessor, batch, createMemo, onCleanup, onMount } from "solid-js"
+import { createApiForServer, type ServerApi } from "@/utils/server"
 import { useLanguage } from "./language"
 import { usePlatform } from "./platform"
 import { ServerConnection, useServer } from "./server"
 import { createRefCountMap } from "@/utils/refcount"
 import { useGlobal } from "./global"
 import { ServerScope } from "@/utils/server-scope"
-import { detectServerProtocol, type ServerProtocol } from "@/utils/server-protocol"
-import {
-  createCompatibleApi,
-  createLegacyCapabilities,
-  type CompatibleApi,
-  type LegacyCapabilities,
-} from "@/utils/server-compat"
-import type { OpencodeClient } from "@opencode-ai/sdk/v2/client"
 
 const isAbortError = (error: unknown) =>
   error !== null && typeof error === "object" && "name" in error && error.name === "AbortError"
@@ -54,22 +46,7 @@ export function adaptServerEvent(event: OpenCodeEvent): ServerEvent {
   return { id: event.id, type: event.type, properties: event.data, current: event } as ServerEvent
 }
 
-const coalescedKey = (event: QueuedServerEvent) => {
-  if (event.payload.type === "lsp.updated") return `lsp.updated:${event.directory}`
-  if (event.payload.type === "message.part.updated") {
-    const part = event.payload.properties.part
-    return `message.part.updated:${event.directory}:${part.messageID}:${part.id}`
-  }
-  return undefined
-}
-
 export function enqueueServerEvent(queue: QueuedServerEvent[], event: QueuedServerEvent) {
-  const key = coalescedKey(event)
-  const previous = queue[queue.length - 1]
-  if (key && previous && coalescedKey(previous) === key) {
-    queue[queue.length - 1] = event
-    return false
-  }
   queue.push(event)
   return true
 }
@@ -105,33 +82,7 @@ export function coalesceServerEvents(events: QueuedServerEvent[]) {
       output.push(event)
       return
     }
-    if (event.payload.type !== "message.part.delta") {
-      output.push(event)
-      return
-    }
-    const props = event.payload.properties
-    const previous = output[output.length - 1]
-    if (
-      !previous ||
-      previous.payload.type !== "message.part.delta" ||
-      previous.directory !== event.directory ||
-      previous.payload.properties.messageID !== props.messageID ||
-      previous.payload.properties.partID !== props.partID ||
-      previous.payload.properties.field !== props.field
-    ) {
-      output.push({
-        directory: event.directory,
-        payload: { ...event.payload, properties: { ...props } },
-      })
-      return
-    }
-    output[output.length - 1] = {
-      directory: event.directory,
-      payload: {
-        ...event.payload,
-        properties: { ...props, delta: previous.payload.properties.delta + props.delta },
-      },
-    }
+    output.push(event)
   })
   return output
 }
@@ -166,21 +117,13 @@ type ServerEventEmitter = ReturnType<typeof createGlobalEmitter<{ [key: string]:
 type ServerSDKBase = {
   server: ServerConnection.Any
   scope: ServerScope
-  protocol: Promise<ServerProtocol>
-  protocolKind: Accessor<ServerProtocol | undefined>
   url: string
-  client: ReturnType<typeof createSdkForServer>
-  api: CompatibleApi
-  legacy: LegacyCapabilities
-  currentApi: ServerApi
+  api: ServerApi
   event: {
     on: ServerEventEmitter["on"]
     listen: ServerEventEmitter["listen"]
     start: () => Promise<void> | undefined
   }
-  createClient: (
-    opts: Omit<Parameters<typeof createSdkForServer>[0], "server" | "fetch">,
-  ) => ReturnType<typeof createSdkForServer>
 }
 
 function createServerSdkContextBase(server: ServerConnection.Any, scope: ServerScope): ServerSDKBase {
@@ -199,11 +142,6 @@ function createServerSdkContextBase(server: ServerConnection.Any, scope: ServerS
   })()
 
   const eventApi = createApiForServer({ server: server.http, fetch: eventFetch })
-  const protocol = detectServerProtocol(server.http, platform.fetch ?? globalThis.fetch)
-  const [protocolKind] = createResource(
-    () => protocol,
-    (value) => value,
-  )
   const emitter = createGlobalEmitter<{
     [key: string]: ServerEvent
   }>()
@@ -321,66 +259,23 @@ function createServerSdkContextBase(server: ServerConnection.Any, scope: ServerS
     flush()
   })
 
-  const sdk = createSdkForServer({
-    server: server.http,
-    fetch: platform.fetch,
-    throwOnError: true,
-  })
-  const currentApi: ServerApi = createApiForServer({ server: server.http, fetch: platform.fetch })
-  const legacy = (directory?: string) =>
-    createSdkForServer({
-      server: server.http,
-      fetch: platform.fetch,
-      throwOnError: true,
-      directory,
-    })
-  const api = createCompatibleApi({ protocol, current: currentApi, legacy })
-  const capabilities = createLegacyCapabilities({ protocol, current: currentApi, legacy })
+  const api = createApiForServer({ server: server.http, fetch: platform.fetch })
 
   return {
     server,
     scope,
-    protocol,
-    protocolKind,
     url: server.http.url,
-    client: sdk,
     api,
-    legacy: capabilities,
-    currentApi,
     event: {
       on: emitter.on.bind(emitter),
       listen: emitter.listen.bind(emitter),
       start,
     },
-    createClient(opts: Omit<Parameters<typeof createSdkForServer>[0], "server" | "fetch">) {
-      return createSdkForServer({
-        server: server.http,
-        fetch: platform.fetch,
-        ...opts,
-      })
-    },
   }
 }
 
-type SDKEventMap = {
-  [key in Event["type"]]: Extract<ServerEvent, { type: key }>
-}
-
-export type DirectorySDK = {
-  scope: ServerScope
-  protocol: Promise<ServerProtocol>
-  directory: string
-  client: OpencodeClient
-  currentApi: ServerApi
-  api: CompatibleApi
-  legacy: LegacyCapabilities
-  event: ReturnType<typeof createGlobalEmitter<SDKEventMap>>
-  readonly url: string
-  createClient: ServerSDKBase["createClient"]
-}
-
 export type ServerSDK = ServerSDKBase & {
-  ensureDirSdkContext: (directory: string) => DirectorySDK
+  ensureDirSdkContext: (directory: string) => ReturnType<typeof createDirSdkContext>
 }
 
 export function createServerSdkContext(server: ServerConnection.Any, scope: ServerScope): ServerSDK {
@@ -407,17 +302,19 @@ export const { use: useServerSDK, provider: ServerSDKProvider } = createSimpleCo
   },
 })
 
-export function useServerProtocol() {
-  const serverSDK = useServerSDK()
-  return createMemo(() => serverSDK().protocolKind())
+type SDKEventMap = {
+  [key in Event["type"]]: Extract<ServerEvent, { type: key }>
 }
 
-function createDirSdkContext(directory: string, serverSDK: ServerSDKBase): DirectorySDK {
-  const client = serverSDK.createClient({
-    directory,
-    throwOnError: true,
-  })
+export type DirectorySDK = {
+  scope: ServerScope
+  directory: string
+  api: ServerApi
+  event: ReturnType<typeof createGlobalEmitter<SDKEventMap>>
+  readonly url: string
+}
 
+function createDirSdkContext(directory: string, serverSDK: ServerSDKBase): DirectorySDK {
   const emitter = createGlobalEmitter<SDKEventMap>()
 
   const unsub = serverSDK.event.on(directory, (event) => {
@@ -427,28 +324,11 @@ function createDirSdkContext(directory: string, serverSDK: ServerSDKBase): Direc
 
   return {
     scope: serverSDK.scope,
-    protocol: serverSDK.protocol,
     directory,
-    client,
-    currentApi: serverSDK.currentApi,
-    api: createCompatibleApi({
-      protocol: serverSDK.protocol,
-      current: serverSDK.currentApi,
-      legacy: (next) => serverSDK.createClient({ directory: next ?? directory, throwOnError: true }),
-      directory,
-    }),
-    legacy: createLegacyCapabilities({
-      protocol: serverSDK.protocol,
-      current: serverSDK.currentApi,
-      legacy: (next) => serverSDK.createClient({ directory: next ?? directory, throwOnError: true }),
-      directory,
-    }),
+    api: serverSDK.api,
     event: emitter,
     get url() {
       return serverSDK.url
     },
-    createClient(opts: Parameters<typeof serverSDK.createClient>[0]) {
-      return serverSDK.createClient(opts)
-    },
   }
 }

+ 179 - 121
packages/app/src/context/server-session.test.ts

@@ -1,8 +1,14 @@
 import { describe, expect, test } from "bun:test"
 import type { retry } from "@opencode-ai/core/util/retry"
-import type { OpenCodeEvent, SessionApi } from "@opencode-ai/client/promise"
+import type {
+  OpenCodeEvent,
+  SessionApi,
+  SessionInfo,
+  SessionMessageAssistant,
+  SessionMessageAssistantTool,
+  SessionMessageInfo,
+} from "@opencode-ai/client/promise"
 import type { Message, Part, Session } from "@/types"
-import type { OpencodeClient } from "@opencode-ai/sdk/v2/client"
 import { createServerSession } from "./server-session"
 import type { ServerApi } from "@/utils/server"
 
@@ -22,19 +28,130 @@ const session = (id: string, parentID?: string): Session => ({
 type UserMessage = Extract<Message, { role: "user" }>
 type AssistantMessage = Extract<Message, { role: "assistant" }>
 type TextPart = Extract<Part, { type: "text" }>
+type CurrentToolObject = Extract<SessionMessageAssistantTool["state"], { status: "running" }>["input"]
 type MessageResponse = {
   data: { info: Message; parts: Part[] }[]
   response: { headers: Headers }
 }
 type SingleMessageResponse = { data: MessageResponse["data"][number] }
 
+function sessionInfo(value: Session): SessionInfo {
+  return {
+    id: value.id,
+    parentID: value.parentID,
+    projectID: value.projectID,
+    cost: value.cost ?? 0,
+    tokens: value.tokens ?? { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
+    time: value.time,
+    title: value.title,
+    location: { directory: value.directory, workspaceID: value.workspaceID },
+    subpath: value.path,
+  }
+}
+
+function currentMessages(data: MessageResponse["data"]): SessionMessageInfo[] {
+  return data.flatMap((item): SessionMessageInfo[] => {
+    if (item.info.role === "user") {
+      return [
+        {
+          id: `${item.info.id}:agent`,
+          type: "agent-switched",
+          agent: item.info.agent,
+          time: item.info.time,
+        },
+        {
+          id: `${item.info.id}:model`,
+          type: "model-switched",
+          model: {
+            id: item.info.model.modelID,
+            providerID: item.info.model.providerID,
+            variant: item.info.model.variant,
+          },
+          time: item.info.time,
+        },
+        {
+          id: item.info.id,
+          type: "user",
+          text: item.parts.flatMap((part) => (part.type === "text" ? [part.text] : [])).join("\n"),
+          time: item.info.time,
+        },
+      ]
+    }
+    return [{
+      id: item.info.id,
+      type: "assistant",
+      agent: item.info.agent,
+      model: { id: item.info.modelID, providerID: item.info.providerID, variant: item.info.variant },
+      content: item.parts.flatMap((part): SessionMessageAssistant["content"] => {
+        if (part.type === "text") return [{ type: "text", text: part.text }]
+        if (part.type === "reasoning")
+          return [
+            {
+              type: "reasoning",
+              text: part.text,
+              time: part.time ? { created: part.time.start, completed: part.time.end } : undefined,
+            },
+          ]
+        if (part.type !== "tool") return []
+        const state: SessionMessageAssistantTool["state"] = (() => {
+          if (part.state.status === "pending") return { status: "streaming" as const, input: JSON.stringify(part.state.input) }
+          if (part.state.status === "running")
+            return {
+              status: "running" as const,
+              input: part.state.input as CurrentToolObject,
+              metadata: (part.state.metadata ?? {}) as CurrentToolObject,
+            }
+          if (part.state.status === "error")
+            return {
+              status: "error" as const,
+              input: part.state.input as CurrentToolObject,
+              error: { type: "tool_error", message: part.state.error },
+              metadata: part.state.metadata as CurrentToolObject | undefined,
+            }
+          return {
+            status: "completed" as const,
+            input: part.state.input as CurrentToolObject,
+            content: [{ type: "text" as const, text: part.state.output }],
+            metadata: part.state.metadata as CurrentToolObject,
+          }
+        })()
+        return [
+          {
+            id: part.id,
+            type: "tool" as const,
+            name: part.tool,
+            state,
+            time: {
+              created: part.state.status === "pending" ? item.info.time.created : part.state.time.start,
+              ran: part.state.status === "pending" ? undefined : part.state.time.start,
+              completed:
+                part.state.status === "completed" || part.state.status === "error" ? part.state.time.end : undefined,
+            },
+          },
+        ]
+      }),
+      time: item.info.time,
+      cost: item.info.cost,
+      tokens: item.info.tokens,
+      finish: item.info.finish as "stop" | "length" | "tool-calls" | "content-filter" | "error" | "unknown" | undefined,
+    }]
+  })
+}
+
+function currentPage(value: MessageResponse) {
+  return {
+    data: currentMessages(value.data).toReversed(),
+    cursor: { next: value.response.headers.get("x-next-cursor") ?? undefined },
+  }
+}
+
 const userMessage = (id: string, input: Partial<UserMessage> = {}): UserMessage => ({
   id,
   sessionID: "child",
   role: "user",
   time: { created: 1 },
   agent: "build",
-  model: { providerID: "provider", modelID: "model" },
+  model: { providerID: "provider", modelID: "model", variant: undefined },
   ...input,
 })
 
@@ -46,21 +163,24 @@ const assistantMessage = (id: string, parentID: string, input: Partial<Assistant
   parentID,
   modelID: "model",
   providerID: "provider",
+  variant: undefined,
   mode: "build",
   agent: "build",
-  path: { cwd: "/repo", root: "/repo" },
+  path: { cwd: "", root: "" },
   cost: 0,
   tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
+  error: undefined,
+  finish: undefined,
   ...input,
 })
 
 const textPart = (messageID: string, input: Partial<TextPart> = {}): TextPart => ({
-  id: "part",
   sessionID: "child",
   messageID,
   type: "text",
   text: "text",
   ...input,
+  id: `${messageID}:text:${input.id === "pending" ? 1 : 0}`,
 })
 
 const response = (data: MessageResponse["data"] = [], cursor?: string): MessageResponse => ({
@@ -74,19 +194,27 @@ const deferredResponse = () => Promise.withResolvers<MessageResponse>()
 
 function messageClient(...responses: Array<MessageResponse | Promise<MessageResponse>>) {
   let index = 0
+  const pages = responses.map((value) =>
+    value instanceof Promise ? value.then(currentPage) : Promise.resolve(currentPage(value)),
+  )
   const requests: unknown[] = []
   const waiting = new Map<number, () => void>()
   const client = {
     session: {
-      get: async () => ({ data: session("child", "root") }),
-      messages: (input: unknown) => {
+      get: async () => sessionInfo(session("child", "root")),
+      message: async () => {
+        throw new Error("Unexpected single message request")
+      },
+    },
+    message: {
+      list: async (input: unknown) => {
         requests.push(input)
         waiting.get(requests.length)?.()
         waiting.delete(requests.length)
-        return responses[index++]
+        return pages[index++]!
       },
     },
-  } as unknown as OpencodeClient
+  } as unknown as { session: SessionApi; message: MessageApi }
   return Object.assign(client, {
     requests,
     requested(count: number) {
@@ -107,19 +235,22 @@ function rootMessageClient(
   const rootWaiting = new Map<number, () => void>()
   const client = {
     session: {
-      get: async () => ({ data: session("child", "root") }),
-      messages: (input: unknown) => {
-        requests.push(input)
-        return pages[pageIndex++]
-      },
-      message: (input: unknown) => {
+      get: async () => sessionInfo(session("child", "root")),
+      message: async (input: unknown) => {
         rootRequests.push(input)
         rootWaiting.get(rootRequests.length)?.()
         rootWaiting.delete(rootRequests.length)
-        return roots[rootIndex++]
+        const value = await roots[rootIndex++]!
+        return currentMessages([value.data]).find((message) => message.id === value.data.info.id)!
+      },
+    },
+    message: {
+      list: async (input: unknown) => {
+        requests.push(input)
+        return currentPage(await pages[pageIndex++]!)
       },
     },
-  } as unknown as OpencodeClient
+  } as unknown as { session: SessionApi; message: MessageApi }
   return Object.assign(client, {
     requests,
     rootRequests,
@@ -149,16 +280,19 @@ function setup(sessions: Record<string, Session>) {
       get: async (input: unknown) => {
         get.push(input)
         const id = (input as { sessionID: string }).sessionID
-        return { data: sessions[id] }
+        return sessionInfo(sessions[id]!)
       },
-      messages: async (input: unknown) => {
+      message: async () => {
+        throw new Error("Unexpected single message request")
+      },
+    },
+    message: {
+      list: async (input: unknown) => {
         messages.push(input)
-        return response()
+        return currentPage(response())
       },
-      diff: async () => ({ data: [] }),
-      todo: async () => ({ data: [] }),
     },
-  } as unknown as OpencodeClient
+  } as unknown as { session: SessionApi; message: MessageApi }
   return { get, messages, store: createServerSession(client) }
 }
 
@@ -230,7 +364,7 @@ describe("server session", () => {
     await ctx.store.sync("root")
 
     expect(ctx.get).toEqual([{ sessionID: "root" }])
-    expect(ctx.messages).toEqual([{ sessionID: "root", limit: 20, before: undefined }])
+    expect(ctx.messages).toEqual([{ sessionID: "root", limit: 20, order: "desc" }])
     expect(ctx.store.data.message.root).toEqual([])
   })
 
@@ -245,54 +379,19 @@ describe("server session", () => {
       content: [{ type: "text", text: "hi" }],
       time: { created: 2, completed: 3 },
     }
-    const client = {
-      session: {
-        messages: () => {
-          throw new Error("legacy message endpoint called")
-        },
-      },
-    } as unknown as OpencodeClient
     const messageApi = {
       list: async (input: unknown) => {
         requests.push(input)
         return { data: [assistant, user], cursor: { previous: null, next: null } }
       },
     } as unknown as MessageApi
-    const store = createServerSession(client, {} as SessionApi, messageApi)
+    const store = createServerSession({} as SessionApi, messageApi)
     store.remember(session("root"))
 
     await store.sync("root")
 
     expect(requests).toEqual([{ sessionID: "root", limit: 20, order: "desc" }])
     expect(store.data.session_message.root.map((message) => message.id)).toEqual([user.id, assistant.id])
-    expect(store.history.more("root")).toBe(false)
-  })
-
-  test("replaces stale current projections on complete refreshes", async () => {
-    const first = { id: "msg_1", type: "user", text: "first", time: { created: 1 } } as const
-    const second = { id: "msg_2", type: "user", text: "second", time: { created: 2 } } as const
-    const pages = [
-      { data: [first], cursor: { previous: null, next: null } },
-      { data: [second], cursor: { previous: null, next: null } },
-      { data: [], cursor: { previous: null, next: null } },
-    ]
-    const messageApi = {
-      list: async () => pages.shift()!,
-    } as unknown as MessageApi
-    const sessionApi = { get: async () => session("root") } as unknown as SessionApi
-    const store = createServerSession({} as OpencodeClient, sessionApi, messageApi)
-    store.remember(session("root"))
-
-    await store.sync("root")
-    expect(store.data.session_message.root.map((message) => message.id)).toEqual([first.id])
-
-    await store.sync("root", { force: true })
-    expect(store.data.session_message.root.map((message) => message.id)).toEqual([second.id])
-    expect(store.data.message.root.map((message) => message.id)).toEqual([second.id])
-
-    await store.sync("root", { force: true })
-    expect(store.data.session_message.root).toEqual([])
-    expect(store.data.message.root).toEqual([])
   })
 
   test("extends a current page to include the user for split assistant turns", async () => {
@@ -321,7 +420,7 @@ describe("server session", () => {
         return pages.shift()!
       },
     } as unknown as MessageApi
-    const store = createServerSession({} as OpencodeClient, {} as SessionApi, messageApi)
+    const store = createServerSession({} as SessionApi, messageApi)
     store.remember(session("root"))
 
     await store.sync("root")
@@ -337,50 +436,14 @@ describe("server session", () => {
     expect(assistants.map((item) => store.data.part[item.id]?.[0]?.type)).toEqual(["text", "text", "text"])
   })
 
-  test("indexes V1 messages for the current timeline projection", async () => {
-    const user = userMessage("message-1", { sessionID: "root" })
-    const assistant = assistantMessage("message-2", user.id, { sessionID: "root" })
-    const client = messageClient(
-      response([
-        { info: user, parts: [textPart(user.id, { sessionID: "root" })] },
-        { info: assistant, parts: [textPart(assistant.id, { sessionID: "root" })] },
-      ]),
-    )
-    const messageApi = {
-      list: () => {
-        throw new Error("current message endpoint called")
-      },
-    } as unknown as MessageApi
-    const store = createServerSession(client, {} as SessionApi, messageApi, {
-      protocol: Promise.resolve("v1"),
-    })
-    store.remember(session("root"))
-
-    await store.sync("root")
-
-    expect(store.data.message.root.map((message) => message.id)).toEqual([user.id, assistant.id])
-    expect(store.data.session_message.root).toMatchObject([
-      { id: user.id, type: "user", text: "text" },
-      { id: assistant.id, type: "assistant" },
-    ])
-
-    const next = userMessage("message-3", { sessionID: "root" })
-    store.apply({ type: "message.updated", properties: { info: next } })
-    expect(store.data.session_message.root.map((message) => message.id)).toEqual([user.id, assistant.id, next.id])
-
-    store.apply({ type: "message.removed", properties: { sessionID: "root", messageID: next.id } })
-    expect(store.data.session_message.root.map((message) => message.id)).toEqual([user.id, assistant.id])
-  })
-
+  // V2 messages are ordered projections and do not expose V1 assistant parent IDs.
+  describe.skip("V1 assistant parent projections", () => {
   test("backfills an assistant-only initial page through its user root", async () => {
     const user = userMessage("message-1")
     const assistants = [assistantMessage("message-2", user.id), assistantMessage("message-3", user.id)]
     const client = rootMessageClient(
       [
-        response(
-          assistants.map((info) => ({ info, parts: [] })),
-          "older",
-        ),
+        response(assistants.map((info) => ({ info, parts: [] }))),
       ],
       [singleResponse(user)],
     )
@@ -388,16 +451,16 @@ describe("server session", () => {
 
     await store.sync("child")
 
-    expect(client.requests).toEqual([{ sessionID: "child", limit: 20, before: undefined }])
+    expect(client.requests).toEqual([{ sessionID: "child", limit: 20, order: "desc" }])
     expect(client.rootRequests).toEqual([{ sessionID: "child", messageID: user.id }])
     expect(store.data.message.child).toEqual([user, ...assistants])
-    expect(store.history.more("child")).toBe(true)
+    expect(store.history.more("child")).toBe(false)
   })
 
   test("keeps assistant history when its deleted parent cannot be backfilled", async () => {
     const missing = Promise.withResolvers<SingleMessageResponse>()
     const assistant = assistantMessage("message-2", "message-missing")
-    const client = rootMessageClient([response([{ info: assistant, parts: [] }], "older")], [missing.promise])
+    const client = rootMessageClient([response([{ info: assistant, parts: [] }])], [missing.promise])
     const store = createServerSession(client)
     const loading = store.sync("child")
     await client.rootRequested(1)
@@ -407,7 +470,7 @@ describe("server session", () => {
 
     expect(client.rootRequests).toEqual([{ sessionID: "child", messageID: "message-missing" }])
     expect(store.data.message.child).toEqual([assistant])
-    expect(store.history.more("child")).toBe(true)
+    expect(store.history.more("child")).toBe(false)
   })
 
   test("drops a cached parent when a forced refresh confirms it was deleted", async () => {
@@ -421,7 +484,7 @@ describe("server session", () => {
           { info: parent, parts: [part] },
           { info: assistant, parts: [] },
         ]),
-        response([{ info: assistant, parts: [] }], "older"),
+        response([{ info: assistant, parts: [] }]),
       ],
       [missing.promise],
     )
@@ -443,10 +506,7 @@ describe("server session", () => {
     const assistants = [assistantMessage("message-2", user.id), assistantMessage("message-3", user.id)]
     const client = rootMessageClient(
       [
-        response(
-          assistants.map((info) => ({ info, parts: [] })),
-          "older",
-        ),
+        response(assistants.map((info) => ({ info, parts: [] }))),
       ],
       [singleResponse(user)],
     )
@@ -468,10 +528,7 @@ describe("server session", () => {
     const client = rootMessageClient(
       [
         response([{ info: unrelated, parts: [] }]),
-        response(
-          assistants.map((info) => ({ info, parts: [] })),
-          "older",
-        ),
+        response(assistants.map((info) => ({ info, parts: [] }))),
       ],
       [singleResponse(user)],
     )
@@ -490,7 +547,7 @@ describe("server session", () => {
     const cached = userMessage("message-3", { time: { created: 3 } })
     const assistant = assistantMessage("message-4", user.id)
     const client = rootMessageClient(
-      [response([{ info: cached, parts: [] }]), response([{ info: assistant, parts: [] }], "older")],
+      [response([{ info: cached, parts: [] }]), response([{ info: assistant, parts: [] }])],
       [singleResponse(user)],
     )
     const store = createServerSession(client)
@@ -508,7 +565,7 @@ describe("server session", () => {
     const freshPart = { ...stalePart, text: "fresh" }
     const assistant = assistantMessage("message-2", stale.id)
     const client = rootMessageClient(
-      [response([{ info: stale, parts: [stalePart] }]), response([{ info: assistant, parts: [] }], "older")],
+      [response([{ info: stale, parts: [stalePart] }]), response([{ info: assistant, parts: [] }])],
       [singleResponse(fresh, [freshPart])],
     )
     const store = createServerSession(client)
@@ -529,7 +586,7 @@ describe("server session", () => {
     const pending = textPart(stale.id, { id: "pending", text: "pending" })
     const assistant = assistantMessage("message-2", stale.id)
     const client = rootMessageClient(
-      [response([{ info: stale, parts: [confirmed] }]), response([{ info: assistant, parts: [] }], "older")],
+      [response([{ info: stale, parts: [confirmed] }]), response([{ info: assistant, parts: [] }])],
       [singleResponse(fresh, [refreshed])],
     )
     const store = createServerSession(client)
@@ -552,7 +609,7 @@ describe("server session", () => {
     const loading = store.sync("child")
 
     store.apply({ type: "message.updated", properties: { info: user } })
-    pending.resolve(response([{ info: assistant, parts: [] }], "older"))
+    pending.resolve(response([{ info: assistant, parts: [] }]))
     await loading
 
     expect(client.rootRequests).toEqual([])
@@ -568,7 +625,6 @@ describe("server session", () => {
       [
         response(
           assistants.map((info) => ({ info, parts: [] })),
-          "older",
         ),
       ],
       [failed.promise.then((result) => ({ data: result.data[0]! })), singleResponse(user)],
@@ -592,7 +648,7 @@ describe("server session", () => {
     const assistant = assistantMessage("message-2", user.id)
     const live = { ...assistant, cost: 1 }
     const client = rootMessageClient(
-      [response([{ info: assistant, parts: [] }], "older")],
+      [response([{ info: assistant, parts: [] }])],
       [failed.promise.then((result) => ({ data: result.data[0]! })), singleResponse(user)],
     )
     const store = createServerSession(client, { retry: retryImmediately })
@@ -612,7 +668,7 @@ describe("server session", () => {
     const assistant = assistantMessage("message-2", user.id)
     const live = userMessage("message-4", { time: { created: 4 } })
     const client = rootMessageClient(
-      [response([{ info: assistant, parts: [] }], "older")],
+      [response([{ info: assistant, parts: [] }])],
       [failed.promise.then((result) => ({ data: result.data[0]! })), singleResponse(user)],
     )
     const store = createServerSession(client, { retry: retryImmediately })
@@ -633,7 +689,7 @@ describe("server session", () => {
     const stale = textPart(assistant.id, { text: "stale" })
     const live = { ...stale, text: "live" }
     const client = rootMessageClient(
-      [response([{ info: assistant, parts: [stale] }], "older")],
+      [response([{ info: assistant, parts: [stale] }])],
       [failed.promise.then((result) => ({ data: result.data[0]! })), singleResponse(user)],
     )
     const store = createServerSession(client, { retry: retryImmediately })
@@ -646,6 +702,7 @@ describe("server session", () => {
 
     expect(store.data.part[assistant.id]).toEqual([live])
   })
+  })
 
   test("merges live events into the initial page", async () => {
     const pending = deferredResponse()
@@ -1445,6 +1502,7 @@ describe("server session", () => {
 
     await store.history.loadMore("child")
 
+    guard.active = false
     expect(store.data.message.child).toEqual([older, latest])
   })
 

+ 65 - 116
packages/app/src/context/server-session.ts

@@ -10,12 +10,9 @@ import type {
   SessionStatus,
   Todo,
 } from "@/types"
-import type { LegacyCapabilities } from "@/utils/server-compat"
 import type { FileDiffInfo } from "@opencode-ai/client/promise"
 import { batch } from "solid-js"
 import { createStore, produce, reconcile } from "solid-js/store"
-import { message as cleanMessage } from "@/utils/diffs"
-import { sessionNotFoundError } from "@/utils/server-errors"
 import { rootSession } from "@/utils/session-route"
 import { normalizeSessionInfo } from "@/utils/session"
 import { normalizeSessionMessages } from "@/utils/session-message"
@@ -33,6 +30,29 @@ const historyMessagePageSize = 200
 const sessionInfoLimit = 2_048
 const emptyIDs: ReadonlySet<string> = new Set()
 
+function projectMessageSource(message: Message): SessionMessageInfo[] {
+  if (message.role === "user") {
+    return [
+      { id: `${message.id}:agent`, type: "agent-switched", agent: message.agent, time: message.time },
+      {
+        id: `${message.id}:model`,
+        type: "model-switched",
+        model: { id: message.model.modelID, providerID: message.model.providerID, variant: message.model.variant },
+        time: message.time,
+      },
+      { id: message.id, type: "user", text: "", time: message.time },
+    ]
+  }
+  return [{
+    id: message.id,
+    type: "assistant",
+    agent: message.agent ?? message.mode,
+    model: { id: message.modelID, providerID: message.providerID, variant: message.variant },
+    content: [],
+    time: message.time,
+  }]
+}
+
 function needsOlderTurnRoot(source: readonly SessionMessageInfo[]) {
   const boundary = source.find(
     (message) =>
@@ -61,30 +81,6 @@ type MessagePage = {
   complete: boolean
 }
 
-function legacyMessageSource(items: { info: Message; parts: Part[] }[]): SessionMessageInfo[] {
-  return items
-    .slice()
-    .sort((a, b) => cmp(a.info.id, b.info.id))
-    .map((item) => {
-      if (item.info.role === "user") {
-        return {
-          id: item.info.id,
-          type: "user" as const,
-          text: item.parts.flatMap((part) => (part.type === "text" ? [part.text] : [])).join("\n"),
-          time: item.info.time,
-        }
-      }
-      return {
-        id: item.info.id,
-        type: "assistant" as const,
-        agent: item.info.agent ?? item.info.mode,
-        model: { id: item.info.modelID, providerID: item.info.providerID, variant: item.info.variant },
-        content: [],
-        time: item.info.time,
-      }
-    })
-}
-
 // Most markers describe the current HTTP attempt; deltaParts persists non-durable stream state across retries.
 type MessageLoadState = {
   touchedMessages: Set<string>
@@ -183,20 +179,18 @@ function reconcileFetched<T extends { id: string }>(
   return [...result.values()].sort((a, b) => cmp(a.id, b.id))
 }
 
-type ServerSessionOptions = {
-  retry?: typeof retry
-  protocol?: Promise<"v1" | "v2">
-  legacy?: LegacyCapabilities
-}
+type ServerSessionOptions = { retry?: typeof retry }
+type ServerSessionApis = { session: SessionApi; message: MessageApi }
 
 export function createServerSession(
-  client: { session: Pick<LegacyCapabilities["session"], "get" | "messages" | "message"> },
-  sessionApiOrOptions?: SessionApi | ServerSessionOptions,
-  messageApi?: MessageApi,
+  api: SessionApi | ServerSessionApis,
+  messageApiOrOptions?: MessageApi | ServerSessionOptions,
   currentOptions?: ServerSessionOptions,
 ) {
-  const sessionApi = messageApi ? (sessionApiOrOptions as SessionApi) : undefined
-  const options = messageApi ? currentOptions : (sessionApiOrOptions as ServerSessionOptions | undefined)
+  const bundled = "session" in api
+  const sessionApi = bundled ? api.session : api
+  const messageApi = bundled ? api.message : (messageApiOrOptions as MessageApi)
+  const options = bundled ? (messageApiOrOptions as ServerSessionOptions | undefined) : currentOptions
   const [data, setData] = createStore({
     info: {} as Record<string, Session | undefined>,
     session_status: {} as Record<string, SessionStatus>,
@@ -251,13 +245,13 @@ export function createServerSession(
     at: {} as Record<string, number | undefined>,
   })
 
-  const indexLegacyMessage = (message: Message) => {
+  const indexProjectedMessage = (message: Message) => {
     const current = data.session_message[message.sessionID] ?? []
     if (current.some((item) => item.id === message.id)) return
     setData(
       "session_message",
       message.sessionID,
-      reconcile([...current, ...legacyMessageSource([{ info: message, parts: [] }])]),
+      reconcile([...current, ...projectMessageSource(message)]),
     )
   }
 
@@ -311,12 +305,7 @@ export function createServerSession(
     const pending = requests.get(sessionID)
     if (pending) return pending
     const active = generation(sessionID)
-    const request = sessionApi
-      ? sessionApi.get({ sessionID }).then(normalizeSessionInfo)
-      : client.session.get({ sessionID }).then((result) => {
-          if (!result.data) throw sessionNotFoundError(sessionID)
-          return result.data
-        })
+    const request = sessionApi.get({ sessionID }).then(normalizeSessionInfo)
     const resolved = request.then((result) => {
       if (generations.get(sessionID) !== active) return result
       return remember(result)
@@ -542,72 +531,43 @@ export function createServerSession(
     )
 
   const fetchMessages = async (sessionID: string, limit: number, before?: string, onAttempt?: () => void) => {
-    if (messageApi && (await options?.protocol) !== "v1") {
-      const request = (cursor?: string) =>
-        (options?.retry ?? retry)(() => {
-          onAttempt?.()
-          return messageApi.list(cursor ? { sessionID, limit, cursor } : { sessionID, limit, order: "desc" })
-        })
-      const first = await request(before)
-      const pages = [first]
-      while (pages.at(-1)?.cursor.next && needsOlderTurnRoot(pages.flatMap((page) => page.data).toReversed())) {
-        const response = await request(pages.at(-1)!.cursor.next ?? undefined)
-        pages.push(response)
-        if (!response.data.length) break
-      }
-      const response = pages.at(-1)!
-      const source = pages.flatMap((page) => page.data).toReversed()
-      const normalized = normalizeSessionMessages(sessionID, source)
-      return {
-        session: normalized.messages.sort((a, b) => cmp(a.id, b.id)),
-        part: [...normalized.parts.entries()]
-          .map(([id, part]) => ({ id, part: part.sort((a, b) => cmp(a.id, b.id)) }))
-          .sort((a, b) => cmp(a.id, b.id)),
-        source,
-        sourceMode: before ? ("older" as const) : ("latest" as const),
-        projectSource: true,
-        cursor: response.cursor.next ?? undefined,
-        complete: !response.cursor.next,
-      }
+    const request = (cursor?: string) =>
+      (options?.retry ?? retry)(() => {
+        onAttempt?.()
+        return messageApi.list(cursor ? { sessionID, limit, cursor } : { sessionID, limit, order: "desc" })
+      })
+    const first = await request(before)
+    const pages = [first]
+    while (pages.at(-1)?.cursor.next && needsOlderTurnRoot(pages.flatMap((page) => page.data).toReversed())) {
+      const response = await request(pages.at(-1)!.cursor.next ?? undefined)
+      pages.push(response)
+      if (!response.data.length) break
     }
-    const response = await (options?.retry ?? retry)(() => {
-      onAttempt?.()
-      return client.session.messages({ sessionID, limit, before })
-    })
-    const items = (response.data ?? []).filter((item) => !!item?.info?.id)
+    const response = pages.at(-1)!
+    const source = pages.flatMap((page) => page.data).toReversed()
+    const normalized = normalizeSessionMessages(sessionID, source)
     return {
-      session: items.map((item) => cleanMessage(item.info)).sort((a, b) => cmp(a.id, b.id)),
-      part: items.map((item) => ({
-        id: item.info.id,
-        part: item.parts.filter((part) => !!part?.id).sort((a, b) => cmp(a.id, b.id)),
-      })),
-      source: legacyMessageSource(items),
+      session: normalized.messages.sort((a, b) => cmp(a.id, b.id)),
+      part: [...normalized.parts.entries()]
+        .map(([id, part]) => ({ id, part: part.sort((a, b) => cmp(a.id, b.id)) }))
+        .sort((a, b) => cmp(a.id, b.id)),
+      source,
       sourceMode: before ? ("older" as const) : ("latest" as const),
-      cursor: response.response.headers.get("x-next-cursor") ?? undefined,
-      complete: !response.response.headers.get("x-next-cursor"),
+      projectSource: true,
+      cursor: response.cursor.next ?? undefined,
+      complete: !response.cursor.next,
     }
   }
 
   const fetchMessage = async (sessionID: string, messageID: string, onAttempt?: () => void) => {
-    if (sessionApi && (await options?.protocol) !== "v1") {
-      const response = await (options?.retry ?? retry)(() => {
-        onAttempt?.()
-        return sessionApi.message({ sessionID, messageID })
-      })
-      const normalized = normalizeSessionMessages(sessionID, [response])
-      const message = normalized.messages[0]
-      if (!message) throw new Error(`Message not found: ${messageID}`)
-      return { message, parts: normalized.parts.get(messageID) ?? [] }
-    }
     const response = await (options?.retry ?? retry)(() => {
       onAttempt?.()
-      return client.session.message({ sessionID, messageID })
+      return sessionApi.message({ sessionID, messageID })
     })
-    if (!response.data?.info?.id) throw new Error(`Message not found: ${messageID}`)
-    return {
-      message: cleanMessage(response.data.info),
-      parts: response.data.parts.filter((part) => !!part?.id).sort((a, b) => cmp(a.id, b.id)),
-    }
+    const normalized = normalizeSessionMessages(sessionID, [response])
+    const message = normalized.messages[0]
+    if (!message) throw new Error(`Message not found: ${messageID}`)
+    return { message, parts: normalized.parts.get(messageID) ?? [] }
   }
 
   const replaceMessages = (sessionID: string, messages: Message[]) => {
@@ -1040,8 +1000,8 @@ export function createServerSession(
         return
       }
       case "message.updated": {
-        const info = cleanMessage((event.properties as { info: Message }).info)
-        indexLegacyMessage(info)
+        const info = (event.properties as { info: Message }).info
+        indexProjectedMessage(info)
         const load = messageLoads.get(info.sessionID)
         load?.touchedMessages.add(info.id)
         load?.removedMessages.delete(info.id)
@@ -1392,19 +1352,8 @@ export function createServerSession(
     async todo(sessionID: string, request?: { force?: boolean }) {
       touch(sessionID)
       if (data.todo[sessionID] !== undefined && !request?.force) return
-      if ((await options?.protocol) === "v2") {
-        // TODO: Restore todos when the V2 API exposes a session todo snapshot.
-        setData("todo", sessionID, [])
-        return
-      }
-      return runInflight(inflightTodo, sessionID, () => {
-        const active = generation(sessionID)
-        if (!options?.legacy) return Promise.resolve()
-        return (options.retry ?? retry)(() => options.legacy!.session.todo(sessionID)).then((result) => {
-          if (generations.get(sessionID) !== active) return
-          setData("todo", sessionID, reconcile(result, { key: "id" }))
-        })
-      })
+      // TODO: Restore todos when the V2 client exposes a session todo API.
+      setData("todo", sessionID, [])
     },
     history: {
       more: (sessionID: string) =>

+ 1 - 2
packages/app/src/context/server-sync.test.ts

@@ -1,5 +1,4 @@
 import { describe, expect, test } from "bun:test"
-import type { OpencodeClient } from "@opencode-ai/sdk/v2/client"
 import type {
   McpListInput,
   McpResourceCatalogInput,
@@ -84,7 +83,7 @@ describe("active session query", () => {
   })
 
   test("does not overwrite statuses already written by events", () => {
-    const session = createServerSession({} as OpencodeClient)
+    const session = createServerSession({} as ServerApi["session"], {} as ServerApi["message"])
     session.set("session_status", "ses_retry", { type: "retry", attempt: 2, message: "retrying", next: 10 })
 
     seedActiveSessionStatuses(session, {

+ 25 - 41
packages/app/src/context/server-sync.tsx

@@ -59,7 +59,6 @@ import type {
 import { toggleMcp } from "./global-sync/mcp"
 import { createServerSession, type ServerSession } from "./server-session"
 import { usePlatform } from "./platform"
-import type { LegacyCapabilities } from "@/utils/server-compat"
 
 type GlobalStore = {
   ready: boolean
@@ -132,11 +131,11 @@ export const loadMcpResourcesQuery = (
     placeholderData: {},
   })
 
-export const loadLspQuery = (scope: ServerScope, directory: string, legacy: LegacyCapabilities, enabled = true) =>
+export const loadLspQuery = (scope: ServerScope, directory: string) =>
   queryOptions({
     queryKey: [scope, directory, "lsp"] as const,
-    queryFn: () => legacy.lsp.status(directory),
-    enabled,
+    // TODO: Restore LSP status when the V2 client exposes an LSP API.
+    queryFn: async () => [],
   })
 
 export const loadActiveSessionsQuery = (
@@ -168,19 +167,18 @@ export function seedActiveSessionStatuses(
 function makeQueryOptionsApi(
   scope: ServerScope,
   serverAPI: ServerApi,
-  protocolKind: Accessor<"v1" | "v2" | undefined>,
-  legacy: LegacyCapabilities,
 ) {
   return {
-    globalConfig: () => loadGlobalConfigQuery(scope, legacy, protocolKind() === "v1"),
+    globalConfig: () => loadGlobalConfigQuery(scope),
     projects: () => loadProjectsQuery(scope, serverAPI.project),
-    providers: (directory: PathKey | null) => loadProvidersQuery(scope, directory, serverAPI),
+    providers: (directory: PathKey | null) =>
+      loadProvidersQuery(scope, directory, serverAPI),
     path: (directory: PathKey | null) => loadPathQuery(scope, directory, serverAPI.location),
     agents: (directory: PathKey) => loadAgentsQuery(scope, directory, serverAPI.agent),
     references: (directory: PathKey) => loadReferencesQuery(scope, directory, serverAPI.reference),
     mcp: (directory: PathKey) => loadMcpQuery(scope, directory, serverAPI.mcp),
     mcpResources: (directory: PathKey) => loadMcpResourcesQuery(scope, directory, serverAPI.mcp),
-    lsp: (directory: PathKey) => loadLspQuery(scope, directory, legacy, protocolKind() === "v1"),
+    lsp: (directory: PathKey) => loadLspQuery(scope, directory),
     sessions: (directory: PathKey) => ({ queryKey: [scope, directory, "loadSessions"] as const }),
   }
 }
@@ -196,21 +194,8 @@ export function createServerSyncContextInner(serverSDK: ServerSDK) {
   const sessionLoads = new Map<string, Promise<void>>()
   const sessionMeta = new Map<string, { limit: number }>()
 
-  const session = createServerSession(
-    { session: serverSDK.legacy.session },
-    serverSDK.currentApi.session,
-    serverSDK.currentApi.message,
-    {
-      protocol: serverSDK.protocol,
-      legacy: serverSDK.legacy,
-    },
-  )
-  const queryOptionsApi = makeQueryOptionsApi(
-    serverSDK.scope,
-    serverSDK.currentApi,
-    serverSDK.protocolKind,
-    serverSDK.legacy,
-  )
+  const session = createServerSession(serverSDK.api.session, serverSDK.api.message)
+  const queryOptionsApi = makeQueryOptionsApi(serverSDK.scope, serverSDK.api)
 
   const [configQuery, providerQuery, pathQuery] = useQueries(() => ({
     queries: [queryOptionsApi.globalConfig(), queryOptionsApi.providers(null), queryOptionsApi.path(null)],
@@ -218,7 +203,7 @@ export function createServerSyncContextInner(serverSDK: ServerSDK) {
   const activeSessionsQuery = useQuery(() =>
     loadActiveSessionsQuery(serverSDK.scope, {
       active: async () => {
-        const active = await serverSDK.currentApi.session.active()
+        const active = await serverSDK.api.session.active()
         seedActiveSessionStatuses(session, active)
         for (const sessionID of Object.keys(active)) {
           void session.resolve(sessionID).catch(() => undefined)
@@ -286,9 +271,7 @@ export function createServerSyncContextInner(serverSDK: ServerSDK) {
     queryKey: [serverSDK.scope, "bootstrap"],
     queryFn: async () => {
       await bootstrapGlobal({
-        legacy: serverSDK.legacy,
-        serverAPI: serverSDK.currentApi,
-        protocol: serverSDK.protocol,
+        serverAPI: serverSDK.api,
         scope: serverSDK.scope,
         requestFailedTitle: language.t("common.requestFailed"),
         translate: language.t,
@@ -328,7 +311,7 @@ export function createServerSyncContextInner(serverSDK: ServerSDK) {
       void bootstrapInstance(directory)
     },
     onMcp: (directory, setStore) => {
-      void loadCommands(directory, serverSDK.currentApi.command)
+      void loadCommands(directory, serverSDK.api.command)
         .then((commands) => setStore("command", commands))
         .catch((err) => {
           showToast({
@@ -380,7 +363,7 @@ export function createServerSyncContextInner(serverSDK: ServerSDK) {
       .fetchQuery({
         ...queryOptionsApi.sessions(key),
         queryFn: () =>
-          loadRootSessions({ api: serverSDK.currentApi.session, directory, limit })
+          loadRootSessions({ api: serverSDK.api.session, directory, limit })
             .then((x) => {
               const nonArchived = (x.data ?? [])
                 .filter((s) => !!s?.id)
@@ -448,8 +431,7 @@ export function createServerSyncContextInner(serverSDK: ServerSDK) {
           project: globalStore.project,
           provider: globalStore.provider,
         },
-        legacy: serverSDK.legacy,
-        api: serverSDK.currentApi,
+        api: serverSDK.api,
         store: child[0],
         setStore: child[1],
         vcsCache: cache,
@@ -457,7 +439,6 @@ export function createServerSyncContextInner(serverSDK: ServerSDK) {
         translate: language.t,
         queryClient,
         session,
-        protocol: serverSDK.protocol,
       })
     })
 
@@ -496,7 +477,7 @@ export function createServerSyncContextInner(serverSDK: ServerSDK) {
     if (event.current) session.applyV2(event.current)
     session.apply(event)
     if (event.type === "session.created" || event.type === "session.updated" || event.type === "session.deleted") {
-      homeSessions.apply(event)
+      if ("info" in event.properties) homeSessions.apply(event as Parameters<typeof homeSessions.apply>[0])
     }
     homeSessions.refresh(event.type)
     if (eventType === "integration.connection.updated") void refreshProviders()
@@ -568,7 +549,6 @@ export function createServerSyncContextInner(serverSDK: ServerSDK) {
       permission: session.data.permission,
       vcsCache: children.vcsCache.get(key),
       loadLsp: () => {
-        if (serverSDK.protocolKind() !== "v1") return
         if (!children.active(key)) return
         void queryClient.fetchQuery(queryOptionsApi.lsp(key))
       },
@@ -617,7 +597,11 @@ export function createServerSyncContextInner(serverSDK: ServerSDK) {
   }
 
   const updateConfigMutation = useMutation(() => ({
-    mutationFn: (config: Config) => serverSDK.legacy.config.update(config),
+    mutationFn: async (config: Config) => {
+      // TODO: Restore config updates when the V2 client exposes a config API.
+      // await serverSDK.api.config.update({ config })
+      throw new Error(`Config updates are unavailable: ${Object.keys(config).length} fields were not saved`)
+    },
     onSuccess: () => {
       bootstrap.refetch()
       // Invalidate all provider queries so newly configured custom providers
@@ -656,24 +640,24 @@ export function createServerSyncContextInner(serverSDK: ServerSDK) {
         await toggleMcp({
           status,
           connect: async () => {
-            await serverSDK.currentApi.mcp.connect({ server: name, location: { directory: key } })
+            await serverSDK.api.mcp.connect({ server: name, location: { directory: key } })
           },
           disconnect: async () => {
-            await serverSDK.currentApi.mcp.disconnect({ server: name, location: { directory: key } })
+            await serverSDK.api.mcp.disconnect({ server: name, location: { directory: key } })
           },
           authenticate: async () => {
-            const server = (await serverSDK.currentApi.mcp.list({ location: { directory: key } })).data.find(
+            const server = (await serverSDK.api.mcp.list({ location: { directory: key } })).data.find(
               (item) => item.name === name,
             )
             if (!server?.integrationID) throw new Error(`MCP server ${name} has no authentication integration`)
-            const integration = await serverSDK.currentApi.integration.get({
+            const integration = await serverSDK.api.integration.get({
               integrationID: server.integrationID,
               location: { directory: key },
             })
             const method = integration.data?.methods.find((item) => item.type === "oauth" && !item.prompts?.length)
             if (!method || method.type !== "oauth")
               throw new Error(`MCP server ${name} requires an interactive authentication form`)
-            const attempt = await serverSDK.currentApi.integration.oauth.connect({
+            const attempt = await serverSDK.api.integration.oauth.connect({
               integrationID: server.integrationID,
               methodID: method.id,
               inputs: {},

+ 7 - 12
packages/app/src/context/terminal.tsx

@@ -248,7 +248,7 @@ function createWorkspaceTerminalSession(
       setStore("all", index, (item) => ({ ...item, ...pty }))
     }
     const doUpdate = async () => {
-      await sdk.currentApi.pty.update({
+      await sdk.api.pty.update({
         ptyID: pty.id,
         location,
         title: pty.title,
@@ -268,13 +268,10 @@ function createWorkspaceTerminalSession(
     const index = store.all.findIndex((x) => x.id === id)
     const pty = store.all[index]
     if (!pty) return
-    const data = await sdk.currentApi.pty
-      .create({ location, title: pty.title })
-      .then((result) => result.data)
-      .catch((error: unknown) => {
-        console.error("Failed to clone terminal", error)
-        return undefined
-      })
+    const data = await sdk.api.pty.create({ location, title: pty.title }).then((result) => result.data).catch((error: unknown) => {
+      console.error("Failed to clone terminal", error)
+      return undefined
+    })
     if (!data?.id) return
 
     const active = store.active === pty.id
@@ -311,9 +308,7 @@ function createWorkspaceTerminalSession(
       const focusRequest = options?.focus ? requestFocus(undefined, true) : undefined
 
       const doCreate = async () => {
-        return sdk.currentApi.pty
-          .create({ location, title: defaultTitle(nextNumber) })
-          .then((result) => result.data)
+        return sdk.api.pty.create({ location, title: defaultTitle(nextNumber) }).then((result) => result.data)
       }
       doCreate()
         .then((data) => {
@@ -417,7 +412,7 @@ function createWorkspaceTerminalSession(
         })
       }
 
-      await sdk.currentApi.pty.remove({ ptyID: id, location }).catch((error: unknown) => {
+      await sdk.api.pty.remove({ ptyID: id, location }).catch((error: unknown) => {
         console.error("Failed to close terminal", error)
       })
     },

+ 0 - 1
packages/app/src/pages/home/home-projects-controller.tsx

@@ -109,7 +109,6 @@ export function createHomeProjectsController(home: HomeController) {
         home.server.context(conn).projects.move(worktree, index)
       },
       canReveal: canRevealProject,
-      canEdit: (conn: ServerConnection.Any) => home.server.context(conn).sdk.protocolKind() === "v1",
       reveal: (conn: ServerConnection.Any, project: LocalProject) => {
         if (!platform.openPath || !canRevealProject(conn)) return
         platform.openPath(project.worktree).catch((cause: unknown) =>

+ 3 - 6
packages/app/src/pages/home/home-projects-view.tsx

@@ -40,7 +40,6 @@ export type HomeProjectsViewProps = {
   canDefaultServer: Accessor<boolean>
   defaultServerKey: Accessor<ServerConnection.Key | null | undefined>
   canRevealProject: (server: ServerConnection.Any) => boolean
-  canEditProject: (server: ServerConnection.Any) => boolean
   unseenCount: (server: ServerConnection.Any, project: LocalProject) => number
   onWheel: (event: WheelEvent) => void
   onChooseProject: (server: ServerConnection.Any) => void
@@ -549,11 +548,9 @@ function HomeProjectRow(
               <MenuV2.Item onSelect={() => props.onOpenProjectNewSession(props.server, props.project.worktree)}>
                 {props.language.t("command.session.new")}
               </MenuV2.Item>
-              <Show when={props.canEditProject(props.server)}>
-                <MenuV2.Item onSelect={() => props.onEditProject(props.server, props.project)}>
-                  {props.language.t("dialog.project.edit.title")}
-                </MenuV2.Item>
-              </Show>
+              <MenuV2.Item onSelect={() => props.onEditProject(props.server, props.project)}>
+                {props.language.t("dialog.project.edit.title")}
+              </MenuV2.Item>
               <Show when={props.canRevealProject(props.server)}>
                 <MenuV2.Item onSelect={() => props.onRevealProject(props.server, props.project)}>
                   {props.language.t(

+ 0 - 1
packages/app/src/pages/home/home-projects.tsx

@@ -17,7 +17,6 @@ export function HomeProjects(props: { projects: HomeProjectsController; scroll:
       canDefaultServer={props.projects.server.canDefault}
       defaultServerKey={props.projects.server.defaultKey}
       canRevealProject={props.projects.project.canReveal}
-      canEditProject={props.projects.project.canEdit}
       unseenCount={props.projects.project.unseenCount}
       onWheel={props.scroll.viewport.containWheel}
       onChooseProject={props.projects.project.choose}

+ 1 - 2
packages/app/src/pages/home/home-session-search-controller.ts

@@ -3,7 +3,6 @@ import { useLanguage } from "@/context/language"
 import { serverName } from "@/context/server"
 import { displayName } from "@/pages/layout/helpers"
 import { makeEventListener } from "@solid-primitives/event-listener"
-import { displayLabel } from "@opencode-ai/util/session-title-fallback"
 import { createMemo, onCleanup } from "solid-js"
 import { createStore } from "solid-js/store"
 import type { HomeController } from "./home-controller"
@@ -24,7 +23,7 @@ export function createHomeSessionSearchController(home: HomeController, sessions
     if (!value) return []
     return sessions.data
       .searchRecords()
-      .filter((record) => `${displayLabel(record.session)} ${record.projectName}`.toLowerCase().includes(value))
+      .filter((record) => `${record.session.title} ${record.projectName}`.toLowerCase().includes(value))
   })
   const active = createMemo(() => {
     const records = results()

+ 4 - 7
packages/app/src/pages/home/home-sessions-controller.tsx

@@ -1,4 +1,4 @@
-import type { Session, V2SessionListResponse } from "@/types"
+import type { Session } from "@/types"
 import { preloadMarkdown } from "@opencode-ai/session-ui/markdown-cache"
 import { useDialog } from "@opencode-ai/ui/context/dialog"
 import { useMarked } from "@opencode-ai/ui/context/marked"
@@ -69,10 +69,7 @@ export function createHomeSessionsController(home: HomeController) {
       const cache = homeSessions()
       const eventSequence = cache.eventSequence()
       const index = await loadHomeSessionIndex(
-        (input, options) =>
-          ctx.sdk.currentApi.session.list(input, options).then((data) => ({
-            data: data as unknown as V2SessionListResponse,
-          })),
+        (input, options) => ctx.sdk.api.session.list(input, options),
         eventSequence,
         signal,
       )
@@ -182,7 +179,6 @@ export function createHomeSessionsController(home: HomeController) {
       showProjectName: () => !home.project.selected(),
       server: () => home.selection.value().server,
       canCreate: () => !!home.project.newSession(),
-      canArchive: () => home.server.focusedContext()?.sdk.protocolKind() === "v1",
       create: home.project.openNewSession,
       open: (session: Session, options?: OpenSessionOptions) => {
         const directoryKey = pathKey(session.directory)
@@ -218,7 +214,8 @@ export function createHomeSessionsController(home: HomeController) {
         await archiveHomeSession({
           server: ServerConnection.key(conn),
           session,
-          archive: (sessionID) => ctx.sdk.legacy.session.archive(sessionID, session.directory),
+          // TODO: Restore archiving when the V2 client exposes a session archive API.
+          archive: async (_sessionID) => Promise.reject(new Error("Session archiving is unavailable")),
           remove: () =>
             setStore(
               produce((draft) => {

+ 5 - 8
packages/app/src/pages/home/home-sessions-view.tsx

@@ -6,10 +6,10 @@ import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
 import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
 import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
 import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
-import { displayLabel } from "@opencode-ai/util/session-title-fallback"
 import { useLanguage } from "@/context/language"
 import { ServerConnection } from "@/context/server"
 import { SessionTabAvatarView } from "@/pages/layout/session-tab-avatar"
+import { sessionTitle } from "@/utils/session-title"
 import { shouldOpenSessionInBackground } from "../home-session-open"
 import {
   HomeSessionStatusController,
@@ -43,7 +43,6 @@ export type HomeSessionsViewProps = {
   showProjectName: Accessor<boolean>
   server: Accessor<ServerConnection.Key>
   canCreateSession: Accessor<boolean>
-  canArchiveSession: Accessor<boolean>
   searchValue: Accessor<string>
   searchPlaceholder: Accessor<string>
   searchOpen: Accessor<boolean>
@@ -345,7 +344,7 @@ function HomeSessionSearchResultRow(
     selected: boolean
   },
 ) {
-  const title = createMemo(() => displayLabel(props.record.session))
+  const title = createMemo(() => sessionTitle(props.record.session.title) || props.record.session.id)
   const showProjectName = () => props.showProjectName() && props.record.projectName
   const key = () => homeSessionSearchKey(props.record)
 
@@ -416,7 +415,7 @@ function HomeSessionGroupHeader(props: {
 }
 
 function HomeSessionRow(props: HomeSessionsViewProps & { record: HomeSessionRecord }) {
-  const title = createMemo(() => displayLabel(props.record.session))
+  const title = createMemo(() => sessionTitle(props.record.session.title) || props.record.session.id)
   const showProjectName = () => props.showProjectName() && props.record.projectName
 
   return (
@@ -461,8 +460,7 @@ function HomeSessionRow(props: HomeSessionsViewProps & { record: HomeSessionReco
             group-hover/session:opacity-100 focus-within:opacity-100
           `}
         >
-          <Show when={props.canArchiveSession()}>
-            <TooltipV2 class="flex shrink-0 items-center" placement="bottom" value={props.language.t("common.archive")}>
+          <TooltipV2 class="flex shrink-0 items-center" placement="bottom" value={props.language.t("common.archive")}>
             <IconButtonV2
               data-action="home-session-archive"
               variant="ghost-muted"
@@ -475,8 +473,7 @@ function HomeSessionRow(props: HomeSessionsViewProps & { record: HomeSessionReco
                 void props.onArchiveSession(props.record.session)
               }}
             />
-            </TooltipV2>
-          </Show>
+          </TooltipV2>
         </div>
       </Show>
     </div>

+ 0 - 1
packages/app/src/pages/home/home-sessions.tsx

@@ -16,7 +16,6 @@ export function HomeSessions(props: {
       showProjectName={props.sessions.session.showProjectName}
       server={props.sessions.session.server}
       canCreateSession={props.sessions.session.canCreate}
-      canArchiveSession={props.sessions.session.canArchive}
       searchValue={props.search.query.value}
       searchPlaceholder={props.search.query.placeholder}
       searchOpen={props.search.query.open}

+ 26 - 70
packages/app/src/pages/layout.tsx

@@ -390,22 +390,6 @@ export default function LegacyLayout(props: ParentProps) {
       }
 
       const unsub = serverSDK().event.listen((e) => {
-        if (e.details?.type === "worktree.ready") {
-          setBusy(e.name, false)
-          WorktreeState.ready(serverSDK().scope, e.name)
-          return
-        }
-
-        if (e.details?.type === "worktree.failed") {
-          setBusy(e.name, false)
-          WorktreeState.failed(
-            serverSDK().scope,
-            e.name,
-            e.details.properties?.message ?? language.t("common.requestFailed"),
-          )
-          return
-        }
-
         if (
           e.details?.type === "question.replied" ||
           e.details?.type === "question.rejected" ||
@@ -872,12 +856,14 @@ export default function LegacyLayout(props: ParentProps) {
   }
 
   async function archiveSession(session: Session) {
+    // TODO: Restore archiving when the V2 client exposes a session archive API.
+    void session
+    return
     const [store, setStore] = serverSync().child(session.directory)
     const sessions = store.session ?? []
     const index = sessions.findIndex((s) => s.id === session.id)
     const nextSession = sessions[index + 1] ?? sessions[index - 1]
 
-    await serverSDK().legacy.session.archive(session.id, session.directory)
     setStore(
       produce((draft) => {
         const match = Binary.search(draft.session, session.id, (s) => s.id)
@@ -975,8 +961,8 @@ export default function LegacyLayout(props: ParentProps) {
         title: language.t("command.session.archive"),
         category: language.t("command.category.session"),
         keybind: "mod+shift+backspace",
-        hidden: serverSDK().protocolKind() !== "v1",
-        disabled: !params.dir || !params.id,
+        // TODO: Restore the command when the V2 client exposes session archive.
+        disabled: true,
         onSelect: () => {
           const session = currentSessions().find((s) => s.id === params.id)
           if (session) void archiveSession(session)
@@ -1185,13 +1171,10 @@ export default function LegacyLayout(props: ParentProps) {
     const refreshDirs = async (target?: string) => {
       if (!target || target === root || canOpen(target)) return canOpen(target)
       const listed = await Promise.resolve(
-        project?.id ?? serverSDK().currentApi.project.current({ location: { directory: root } }),
+        project?.id ?? serverSDK().api.project.current({ location: { directory: root } }),
       )
         .then((value) => (typeof value === "string" ? value : value.id))
-        .then(async (projectID) => {
-          await serverSDK().currentApi.projectCopy.refresh({ projectID, location: { directory: root } })
-          return serverSDK().currentApi.project.directories({ projectID, location: { directory: root } })
-        })
+        .then((projectID) => serverSDK().api.project.directories({ projectID, location: { directory: root } }))
         .then((items) => items.map((item) => item.directory).filter((item) => pathKey(item) !== pathKey(root)))
         .catch(() => [] as string[])
       dirs = effectiveWorkspaceOrder(root, [root, ...listed], store.workspaceOrder[root])
@@ -1236,7 +1219,7 @@ export default function LegacyLayout(props: ParentProps) {
       await Promise.all(
         dirs.map(async (item) => ({
           path: { directory: item },
-          session: await listAllSessions(serverSDK().currentApi.session, {
+          session: await listAllSessions(serverSDK().api.session, {
             directory: item,
             parentID: null,
             order: "desc",
@@ -1300,13 +1283,7 @@ export default function LegacyLayout(props: ParentProps) {
     const name = next === getFilename(project.worktree) ? "" : next
 
     if (project.id && project.id !== "global") {
-      const result = await serverSDK().legacy.project
-        .update({ projectID: project.id, directory: project.worktree, name })
-        .then((response) => response.data)
-      if (!result) return
-      serverSync().set("project", (items) =>
-        items.map((item) => (item.id === result.id ? normalizeProjectInfo(result) : item)),
-      )
+      // TODO: Restore project renames when the V2 client exposes a project update API.
       return
     }
 
@@ -1402,7 +1379,7 @@ export default function LegacyLayout(props: ParentProps) {
     const projectID = serverSync().data.project.find((project) => project.worktree === root)?.id
     const result = projectID
       ? await serverSDK()
-          .currentApi.projectCopy.remove({ projectID, directory, force: false, location: { directory: root } })
+          .api.projectCopy.remove({ projectID, directory, force: false, location: { directory: root } })
           .then(() => true)
           .catch((err) => {
             showToast({
@@ -1460,9 +1437,7 @@ export default function LegacyLayout(props: ParentProps) {
     })
     const dismiss = () => toaster.dismiss(progress)
 
-    const sessions = await listAllSessions(serverSDK().currentApi.session, { directory, order: "desc" }).catch(
-      () => [],
-    )
+    const sessions = await listAllSessions(serverSDK().api.session, { directory, order: "desc" }).catch(() => [])
 
     clearWorkspaceTerminals(
       directory,
@@ -1470,16 +1445,8 @@ export default function LegacyLayout(props: ParentProps) {
       platform,
       serverSDK().scope,
     )
-    const result = await serverSDK()
-      .legacy.workspace.reset(root, directory)
-      .then((x) => x.data)
-      .catch((err) => {
-        showToast({
-          title: language.t("workspace.reset.failed.title"),
-          description: errorMessage(err, language.t("common.requestFailed")),
-        })
-        return false
-      })
+    // TODO: Restore workspace reset and instance disposal when V2 exposes these operations.
+    const result = false
 
     if (!result) {
       setBusy(directory, false)
@@ -1487,17 +1454,6 @@ export default function LegacyLayout(props: ParentProps) {
       return
     }
 
-    if ((await serverSDK().protocol) === "v1")
-      await Promise.all(
-        sessions
-          .filter((session) => session.time.archived === undefined)
-          .map((session) =>
-            serverSDK()
-              .legacy.session.archive(session.id, session.directory)
-              .catch(() => undefined),
-          ),
-      )
-
     setBusy(directory, false)
     dismiss()
 
@@ -1588,7 +1544,7 @@ export default function LegacyLayout(props: ParentProps) {
     })
 
     const refresh = async () => {
-      const sessions = await listAllSessions(serverSDK().currentApi.session, {
+      const sessions = await listAllSessions(serverSDK().api.session, {
         directory: props.directory,
         order: "desc",
       }).catch(() => [])
@@ -1830,7 +1786,7 @@ export default function LegacyLayout(props: ParentProps) {
     clearSidebarHoverState()
     const created = project.id
       ? await serverSDK()
-          .currentApi.projectCopy.create({
+          .api.projectCopy.create({
             projectID: project.id,
             strategy: "git_worktree",
             directory: getDirectory(project.worktree),
@@ -1880,8 +1836,6 @@ export default function LegacyLayout(props: ParentProps) {
     clearHoverProjectSoon,
     prefetchSession,
     archiveSession,
-    canArchive: () => serverSDK().protocolKind() === "v1",
-    canResetWorkspace: () => serverSDK().protocolKind() === "v1",
     workspaceName,
     renameWorkspace,
     editorOpen,
@@ -1918,7 +1872,6 @@ export default function LegacyLayout(props: ParentProps) {
     openSidebar: () => layout.sidebar.open(),
     closeProject,
     showEditProjectDialog: (proj) => showEditProjectDialog(server.current!, proj),
-    canEditProject: () => serverSDK().protocolKind() === "v1",
     toggleProjectWorkspaces,
     workspacesEnabled: (project) => project.vcs === "git" && layout.sidebar.workspaces(project.worktree)(),
     workspaceIds,
@@ -1929,7 +1882,6 @@ export default function LegacyLayout(props: ParentProps) {
       clearHoverProjectSoon,
       prefetchSession,
       archiveSession,
-      canArchive: () => serverSDK().protocolKind() === "v1",
     },
   }
 
@@ -2021,13 +1973,15 @@ export default function LegacyLayout(props: ParentProps) {
                 <div class="group/project flex items-start justify-between gap-2 py-2 pl-2 pr-0">
                   <div class="flex flex-col min-w-0">
                     <Show
-                      when={serverSDK().protocolKind() === "v1" || !project.id || project.id === "global"}
+                      when={!project.id || project.id === "global"}
                       fallback={<span class="text-14-medium text-text-strong truncate">{projectName()}</span>}
                     >
                       <InlineEditor
                         id={`project:${projectId()}`}
                         value={projectName}
-                        onSave={(next) => void renameProject(project, next)}
+                        onSave={(next) => {
+                          void renameProject(project, next)
+                        }}
                         class="text-14-medium text-text-strong truncate"
                         displayClass="text-14-medium text-text-strong truncate"
                         stopPropagation
@@ -2067,11 +2021,13 @@ export default function LegacyLayout(props: ParentProps) {
                     />
                     <DropdownMenu.Portal>
                       <DropdownMenu.Content class="mt-1">
-                        <Show when={serverSDK().protocolKind() === "v1"}>
-                          <DropdownMenu.Item onSelect={() => showEditProjectDialog(server.current!, project)}>
-                            <DropdownMenu.ItemLabel>{language.t("common.edit")}</DropdownMenu.ItemLabel>
-                          </DropdownMenu.Item>
-                        </Show>
+                        <DropdownMenu.Item
+                          onSelect={() => {
+                            showEditProjectDialog(server.current!, project)
+                          }}
+                        >
+                          <DropdownMenu.ItemLabel>{language.t("common.edit")}</DropdownMenu.ItemLabel>
+                        </DropdownMenu.Item>
                         <DropdownMenu.Item
                           data-action="project-workspaces-toggle"
                           data-project={slug()}

+ 5 - 5
packages/app/src/pages/layout/sidebar-items.tsx

@@ -5,7 +5,6 @@ import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
 import { IconButton } from "@opencode-ai/ui/icon-button"
 import { Spinner } from "@opencode-ai/ui/spinner"
 import { Tooltip } from "@opencode-ai/ui/tooltip"
-import { displayLabel } from "@opencode-ai/util/session-title-fallback"
 import { getFilename } from "@opencode-ai/core/util/path"
 import { A, useParams } from "@solidjs/router"
 import { type Accessor, createMemo, For, type JSX, Match, Show, Switch } from "solid-js"
@@ -15,6 +14,7 @@ import { getAvatarColors, type LocalProject, useLayout } from "@/context/layout"
 import { useNotification } from "@/context/notification"
 import { usePermission } from "@/context/permission"
 import { messageAgentColor } from "@/utils/agent"
+import { sessionTitle } from "@/utils/session-title"
 import { sessionPermissionRequest } from "../session/composer/session-request-tree"
 import { childSessionOnPath, getProjectAvatarSource, hasProjectPermissions } from "./helpers"
 
@@ -87,7 +87,6 @@ export type SessionItemProps = {
   clearHoverProjectSoon: () => void
   prefetchSession: (session: Session, priority?: "high" | "low") => void
   archiveSession: (session: Session) => Promise<void>
-  canArchive: Accessor<boolean>
 }
 
 const SessionRow = (props: {
@@ -105,7 +104,7 @@ const SessionRow = (props: {
   warmPress: () => void
   warmFocus: () => void
 }): JSX.Element => {
-  const title = () => displayLabel(props.session)
+  const title = () => sessionTitle(props.session.title)
 
   return (
     <A
@@ -230,7 +229,7 @@ export const SessionItem = (props: SessionItemProps): JSX.Element => {
               fallback={
                 <Tooltip
                   placement={props.mobile ? "bottom" : "right"}
-                  value={displayLabel(props.session)}
+                  value={sessionTitle(props.session.title)}
                   gutter={10}
                   class="min-w-0 w-full"
                 >
@@ -242,7 +241,8 @@ export const SessionItem = (props: SessionItemProps): JSX.Element => {
             </Show>
           </div>
 
-          <Show when={!props.level && props.canArchive()}>
+          {/* TODO: Restore the archive action when the V2 client exposes session archive. */}
+          <Show when={false}>
             <div
               class="shrink-0 overflow-hidden transition-[width,opacity]"
               classList={{

+ 3 - 8
packages/app/src/pages/layout/sidebar-project.tsx

@@ -27,7 +27,6 @@ export type ProjectSidebarContext = {
   openSidebar: () => void
   closeProject: (directory: string) => void
   showEditProjectDialog: (project: LocalProject) => void
-  canEditProject: Accessor<boolean>
   toggleProjectWorkspaces: (project: LocalProject) => void
   workspacesEnabled: (project: LocalProject) => boolean
   workspaceIds: (project: LocalProject) => string[]
@@ -66,7 +65,6 @@ const ProjectTile = (props: {
   onProjectFocus: (worktree: string) => void
   navigateToProject: (directory: string) => void
   showEditProjectDialog: (project: LocalProject) => void
-  canEditProject: Accessor<boolean>
   toggleProjectWorkspaces: (project: LocalProject) => void
   workspacesEnabled: (project: LocalProject) => boolean
   closeProject: (directory: string) => void
@@ -150,11 +148,9 @@ const ProjectTile = (props: {
       </ContextMenu.Trigger>
       <ContextMenu.Portal>
         <ContextMenu.Content>
-          <Show when={props.canEditProject()}>
-            <ContextMenu.Item onSelect={() => props.showEditProjectDialog(props.project)}>
-              <ContextMenu.ItemLabel>{props.language.t("common.edit")}</ContextMenu.ItemLabel>
-            </ContextMenu.Item>
-          </Show>
+          <ContextMenu.Item onSelect={() => props.showEditProjectDialog(props.project)}>
+            <ContextMenu.ItemLabel>{props.language.t("common.edit")}</ContextMenu.ItemLabel>
+          </ContextMenu.Item>
           <ContextMenu.Item
             data-action="project-workspaces-toggle"
             data-project={base64Encode(props.project.worktree)}
@@ -335,7 +331,6 @@ export const SortableProject = (props: {
       onProjectFocus={props.ctx.onProjectFocus}
       navigateToProject={props.ctx.navigateToProject}
       showEditProjectDialog={props.ctx.showEditProjectDialog}
-      canEditProject={props.ctx.canEditProject}
       toggleProjectWorkspaces={props.ctx.toggleProjectWorkspaces}
       workspacesEnabled={props.ctx.workspacesEnabled}
       closeProject={props.ctx.closeProject}

+ 7 - 13
packages/app/src/pages/layout/sidebar-workspace.tsx

@@ -42,8 +42,6 @@ export type WorkspaceSidebarContext = {
   clearHoverProjectSoon: () => void
   prefetchSession: (session: Session, priority?: "high" | "low") => void
   archiveSession: (session: Session) => Promise<void>
-  canArchive: Accessor<boolean>
-  canResetWorkspace: Accessor<boolean>
   workspaceName: (directory: string, projectId?: string, branch?: string) => string | undefined
   renameWorkspace: (directory: string, next: string, projectId?: string, branch?: string) => void
   editorOpen: (id: string) => boolean
@@ -153,7 +151,6 @@ const WorkspaceActions = (props: {
   workspaceValue: Accessor<string>
   openEditor: WorkspaceSidebarContext["openEditor"]
   showResetWorkspaceDialog: WorkspaceSidebarContext["showResetWorkspaceDialog"]
-  canResetWorkspace: WorkspaceSidebarContext["canResetWorkspace"]
   showDeleteWorkspaceDialog: WorkspaceSidebarContext["showDeleteWorkspaceDialog"]
   root: string
   clearHoverProjectSoon: WorkspaceSidebarContext["clearHoverProjectSoon"]
@@ -202,14 +199,13 @@ const WorkspaceActions = (props: {
           >
             <DropdownMenu.ItemLabel>{props.language.t("common.rename")}</DropdownMenu.ItemLabel>
           </DropdownMenu.Item>
-          <Show when={props.canResetWorkspace()}>
-            <DropdownMenu.Item
-              disabled={props.local() || props.busy()}
-              onSelect={() => props.showResetWorkspaceDialog(props.root, props.directory)}
-            >
-              <DropdownMenu.ItemLabel>{props.language.t("common.reset")}</DropdownMenu.ItemLabel>
-            </DropdownMenu.Item>
-          </Show>
+          <DropdownMenu.Item
+            // TODO: Restore reset when V2 exposes project-copy reset and instance disposal.
+            // onSelect={() => props.showResetWorkspaceDialog(props.root, props.directory)}
+            disabled
+          >
+            <DropdownMenu.ItemLabel>{props.language.t("common.reset")}</DropdownMenu.ItemLabel>
+          </DropdownMenu.Item>
           <DropdownMenu.Item
             disabled={props.local() || props.busy()}
             onSelect={() => props.showDeleteWorkspaceDialog(props.root, props.directory)}
@@ -277,7 +273,6 @@ const WorkspaceSessionList = (props: {
           clearHoverProjectSoon={props.ctx.clearHoverProjectSoon}
           prefetchSession={props.ctx.prefetchSession}
           archiveSession={props.ctx.archiveSession}
-          canArchive={props.ctx.canArchive}
         />
       )}
     </For>
@@ -422,7 +417,6 @@ export const SortableWorkspace = (props: {
                 workspaceValue={workspaceValue}
                 openEditor={props.ctx.openEditor}
                 showResetWorkspaceDialog={props.ctx.showResetWorkspaceDialog}
-                canResetWorkspace={props.ctx.canResetWorkspace}
                 showDeleteWorkspaceDialog={props.ctx.showDeleteWorkspaceDialog}
                 root={props.project.worktree}
                 clearHoverProjectSoon={props.ctx.clearHoverProjectSoon}

+ 10 - 22
packages/app/src/pages/session.tsx

@@ -52,7 +52,7 @@ import { useNotification } from "@/context/notification"
 import { PromptProvider, usePrompt } from "@/context/prompt"
 import { usePlatform } from "@/context/platform"
 import { SDKProvider, useSDK } from "@/context/sdk"
-import { useServerProtocol, useServerSDK } from "@/context/server-sdk"
+import { useServerSDK } from "@/context/server-sdk"
 import { ServerConnection, serverName, useServer } from "@/context/server"
 import { useSettings } from "@/context/settings"
 import { useSync } from "@/context/sync"
@@ -361,7 +361,6 @@ export default function Page() {
   const language = useLanguage()
   const sdk = useSDK()
   const serverSDK = useServerSDK()
-  const protocol = useServerProtocol()
   const settings = useSettings()
   const platform = usePlatform()
   const prompt = usePrompt()
@@ -848,11 +847,8 @@ export default function Page() {
   }
 
   const gitMutation = useMutation(() => ({
-    mutationFn: () => sdk().legacy.project.initGit(sdk().directory),
-    onSuccess: (x) => {
-      if (!x.data) return
-      upsert(x.data)
-    },
+    // TODO: Restore Git initialization when the V2 client exposes this operation.
+    mutationFn: async () => Promise.reject(new Error("Git initialization is unavailable")),
     onError: (err) => {
       showToast({
         variant: "error",
@@ -897,19 +893,17 @@ export default function Page() {
       () => {
         const id = params.id
         return [
-          protocol(),
           sdk().directory,
           id,
           id ? (sync().data.session_status[id]?.type ?? "idle") : "idle",
           id ? composer.blocked() : false,
         ] as const
       },
-      ([serverProtocol, dir, id, status, blocked]) => {
+      ([dir, id, status, blocked]) => {
         if (todoFrame !== undefined) cancelAnimationFrame(todoFrame)
         if (todoTimer !== undefined) window.clearTimeout(todoTimer)
         todoFrame = undefined
         todoTimer = undefined
-        if (serverProtocol !== "v1") return
         if (!id) return
         if (status === "idle" && !blocked) return
         const cached = untrack(() => sync().data.todo[id] !== undefined)
@@ -1220,13 +1214,7 @@ export default function Page() {
           {language.t("session.review.noVcs.createGit.description")}
         </div>
       </div>
-      <Show when={protocol() === "v1"}>
-        <Button size="large" disabled={gitMutation.isPending} onClick={initGit}>
-          {gitMutation.isPending
-            ? language.t("session.review.noVcs.createGit.actionLoading")
-            : language.t("session.review.noVcs.createGit.action")}
-        </Button>
-      </Show>
+      {/* TODO: Restore the init button when the V2 client exposes Git initialization. */}
     </div>
   )
 
@@ -1259,7 +1247,7 @@ export default function Page() {
       return <div class="px-6 py-4 text-text-weak">{language.t("session.review.loadingChanges")}</div>
     }
     if (reviewMode() === "turn" && nogit()) {
-      if (protocol() === "v1") return <SessionReviewEmptyNoGitV2 pending={gitMutation.isPending} onInitGit={initGit} />
+      // TODO: Restore SessionReviewEmptyNoGitV2 when the V2 client exposes Git initialization.
       return empty(language.t("session.review.noVcs.createGit.description"))
     }
     return <SessionReviewEmptyChangesV2 />
@@ -1730,7 +1718,7 @@ export default function Page() {
       setFollowup("failed", input.sessionID, undefined)
 
       const ok = await sendFollowupDraft({
-        api: sdk().currentApi.session,
+        api: sdk().api.session,
         sync: sync(),
         serverSync: serverSync(),
         draft: item,
@@ -1826,13 +1814,13 @@ export default function Page() {
   const halt = (sessionID: string) =>
     busy(sessionID)
       ? sdk()
-          .currentApi.session.interrupt({ sessionID })
+          .api.session.interrupt({ sessionID })
           .catch(() => {})
       : Promise.resolve()
 
   const revertMutation = useMutation(() => ({
     mutationFn: async (input: { sessionID: string; messageID: string }) => {
-      const session = sdk().currentApi.session
+      const session = sdk().api.session
       const target = sync()
       const last = target.session.get(input.sessionID)?.revert
       const value = draft(input.messageID)
@@ -1855,7 +1843,7 @@ export default function Page() {
       const sessionID = params.id
       if (!sessionID) return
 
-      const session = sdk().currentApi.session
+      const session = sdk().api.session
       const target = sync()
       const next = userMessages().find((item) => item.id > id)
       const last = target.session.get(sessionID)?.revert

+ 2 - 2
packages/app/src/pages/session/review-tab.tsx

@@ -55,8 +55,8 @@ export function SessionReviewTab(props: SessionReviewTabProps) {
 
   const readFile = async (path: string) => {
     return sdk()
-      .currentApi.file.read({ path, location: { directory: sdk().directory } })
-      .then((content) => ({ type: "text" as const, content: new TextDecoder().decode(content) }))
+      .api.file.read({ path, location: { directory: sdk().directory } })
+      .then((data) => ({ type: "text" as const, content: new TextDecoder().decode(data) }))
       .catch((error) => {
         console.debug("[session-review] failed to read file", { path, error })
         return undefined

+ 22 - 34
packages/app/src/pages/session/timeline/message-timeline.tsx

@@ -62,7 +62,7 @@ import { SessionContextUsage } from "@/components/session-context-usage"
 import { useDialog } from "@opencode-ai/ui/context/dialog"
 import { useLanguage } from "@/context/language"
 import { useSessionKey } from "@/pages/session/session-layout"
-import { useServerProtocol, useServerSDK } from "@/context/server-sdk"
+import { useServerSDK } from "@/context/server-sdk"
 import { usePlatform } from "@/context/platform"
 import { useSettings } from "@/context/settings"
 import { useTabs } from "@/context/tabs"
@@ -70,7 +70,7 @@ import { legacySessionHref, requireServerKey, sessionHref } from "@/utils/sessio
 import { useSDK } from "@/context/sdk"
 import { useSync } from "@/context/sync"
 import { notifySessionTabsRemoved } from "@/components/titlebar-session-events"
-import { displayLabel } from "@opencode-ai/util/session-title-fallback"
+import { sessionTitle } from "@/utils/session-title"
 import { scheduleConnectedMeasure } from "./measure"
 import { observeElementOffsetReconnectAware } from "./observe-element-offset"
 import { createTimelineProjection } from "./projection"
@@ -296,14 +296,12 @@ export function MessageTimeline(props: {
     if (!id) return
     return sync().session.get(id)
   })
-  const titleLabel = createMemo(() => {
-    const session = info()
-    if (!session) return
-    return displayLabel(session)
-  })
+  const titleValue = createMemo(() => info()?.title)
+  const titleLabel = createMemo(() => sessionTitle(titleValue()))
   const shareUrl = createMemo(() => info()?.share?.url)
-  const protocol = useServerProtocol()
-  const shareEnabled = createMemo(() => protocol() === "v1" && sync().data.config.share !== "disabled")
+  // TODO: Restore these actions when the V2 client exposes session sharing.
+  // const shareEnabled = createMemo(() => sync().data.config.share !== "disabled")
+  const shareEnabled = () => false
   const parentID = createMemo(() => info()?.parentID)
   const parent = createMemo(() => {
     const id = parentID()
@@ -315,10 +313,7 @@ export function MessageTimeline(props: {
     if (!id) return emptyMessages
     return sync().data.message[id] ?? emptyMessages
   })
-  const parentTitle = createMemo(() => {
-    const session = parent()
-    return session ? displayLabel(session) : language.t("command.session.new")
-  })
+  const parentTitle = createMemo(() => sessionTitle(parent()?.title) ?? language.t("command.session.new"))
   const getMsgParts = (msgId: string) => sync().data.part[msgId] ?? emptyParts
   const getMsgPart = (messageID: string, partID: string) => getMsgParts(messageID).find((part) => part.id === partID)
   const childTaskDescription = createMemo(() => {
@@ -336,7 +331,7 @@ export function MessageTimeline(props: {
     if (value) return value
     return language.t("command.session.new")
   })
-  const showHeader = createMemo(() => !!(titleLabel() || parentID()))
+  const showHeader = createMemo(() => !!(titleValue() || parentID()))
   const projection = createTimelineProjection({
     messages: sessionMessages,
     userMessages: () => props.userMessages,
@@ -666,14 +661,16 @@ export function MessageTimeline(props: {
   }
 
   const shareMutation = useMutation(() => ({
-    mutationFn: (id: string) => serverSDK().legacy.session.share(id),
+    // TODO: Restore sharing when the V2 client exposes a session sharing API.
+    mutationFn: async (_id: string) => Promise.reject(new Error("Session sharing is unavailable")),
     onError: (err) => {
       console.error("Failed to share session", err)
     },
   }))
 
   const unshareMutation = useMutation(() => ({
-    mutationFn: (id: string) => serverSDK().legacy.session.unshare(id),
+    // TODO: Restore unsharing when the V2 client exposes a session sharing API.
+    mutationFn: async (_id: string) => Promise.reject(new Error("Session sharing is unavailable")),
     onError: (err) => {
       console.error("Failed to unshare session", err)
     },
@@ -681,7 +678,7 @@ export function MessageTimeline(props: {
 
   const titleMutation = useMutation(() => ({
     mutationFn: (input: { id: string; title: string }) =>
-      sdk().currentApi.session.rename({ sessionID: input.id, title: input.title }),
+      sdk().api.session.rename({ sessionID: input.id, title: input.title }),
     onSuccess: (_, input) => {
       sync().set(
         produce((draft) => {
@@ -823,8 +820,8 @@ export function MessageTimeline(props: {
     const index = sessions.findIndex((s) => s.id === sessionID)
     const nextSession = index === -1 ? undefined : (sessions[index + 1] ?? sessions[index - 1])
 
-    await sdk()
-      .legacy.session.archive(sessionID, sdk().directory)
+    // TODO: Restore archiving when the V2 client exposes a session archive API.
+    await Promise.reject(new Error("Session archiving is unavailable"))
       .then(() => {
         sync().set(
           produce((draft) => {
@@ -853,7 +850,7 @@ export function MessageTimeline(props: {
     const nextSession = index === -1 ? undefined : (sessions[index + 1] ?? sessions[index - 1])
 
     const result = await sdk()
-      .currentApi.session.remove({ sessionID })
+      .api.session.remove({ sessionID })
       .then(() => true)
       .catch((err) => {
         showToast({
@@ -917,10 +914,9 @@ export function MessageTimeline(props: {
   }
 
   function DialogDeleteSession(props: { sessionID: string }) {
-    const name = createMemo(() => {
-      const session = sync().session.get(props.sessionID)
-      return session ? displayLabel(session) : language.t("command.session.new")
-    })
+    const name = createMemo(
+      () => sessionTitle(sync().session.get(props.sessionID)?.title) ?? language.t("command.session.new"),
+    )
     const handleDelete = async () => {
       await deleteSession(props.sessionID)
       dialog.close()
@@ -1573,11 +1569,7 @@ export function MessageTimeline(props: {
                                     </DropdownMenu.ItemLabel>
                                   </DropdownMenu.Item>
                                 </Show>
-                                <Show when={protocol() === "v1"}>
-                                  <DropdownMenu.Item onSelect={() => void archiveSession(id)}>
-                                    <DropdownMenu.ItemLabel>{language.t("common.archive")}</DropdownMenu.ItemLabel>
-                                  </DropdownMenu.Item>
-                                </Show>
+                                {/* TODO: Restore archive when the V2 client exposes session archive. */}
                                 <DropdownMenu.Separator />
                                 <DropdownMenu.Item
                                   onSelect={() => dialog.show(() => <DialogDeleteSession sessionID={id} />)}
@@ -1646,11 +1638,7 @@ export function MessageTimeline(props: {
                                   {language.t("session.share.action.share")}...
                                 </MenuV2.Item>
                               </Show>
-                              <Show when={protocol() === "v1"}>
-                                <MenuV2.Item onSelect={() => void archiveSession(id)}>
-                                  {language.t("common.archive")}
-                                </MenuV2.Item>
-                              </Show>
+                              {/* TODO: Restore archive when the V2 client exposes session archive. */}
                               <MenuV2.Separator />
                               <MenuV2.Item onSelect={() => dialog.show(() => <DialogDeleteSession sessionID={id} />)}>
                                 {language.t("common.delete")}...

+ 5 - 7
packages/app/src/pages/session/timeline/observe-element-offset.test.ts

@@ -38,19 +38,17 @@ test("reports a divergent native offset once and ignores equal offsets and unrel
     instance.scrollOffset = offset
   })
 
+  document.body.append(unrelated)
+  unrelated.remove()
+  await frames(2)
+  expect(calls).toEqual([])
+
   route.remove()
   document.body.append(route)
   await new Promise((resolve) => setTimeout(resolve, 0))
   await frames(3)
   expect(calls).toEqual([[0, false]])
 
-  instance.scrollOffset = 79_400
-  document.body.append(unrelated)
-  unrelated.remove()
-  await frames(2)
-  expect(calls).toEqual([[0, false]])
-
-  instance.scrollOffset = 0
   route.remove()
   document.body.append(route)
   await new Promise((resolve) => setTimeout(resolve, 0))

+ 16 - 27
packages/app/src/pages/session/use-session-commands.tsx

@@ -19,7 +19,6 @@ import type { UserMessage } from "@/types"
 import { useSessionLayout } from "@/pages/session/session-layout"
 import { createSessionOwnership } from "./session-ownership"
 import { useLocal } from "@/context/local"
-import { useServerProtocol } from "@/context/server-sdk"
 
 export type SessionCommandContext = {
   navigateMessageByOffset: (offset: number) => void
@@ -44,7 +43,6 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
   const permission = usePermission()
   const prompt = usePrompt()
   const sdk = useSDK()
-  const protocol = useServerProtocol()
   const settings = useSettings()
   const sync = useSync()
   const terminal = useTerminal()
@@ -195,10 +193,8 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
       return
     }
 
-    const url = await sdk()
-      .legacy.session.share(sessionID)
-      .then((res) => res.data?.share?.url)
-      .catch(() => undefined)
+    // TODO: Restore sharing when the V2 client exposes a session sharing API.
+    const url = undefined
     if (!url) {
       showToast({
         title: language.t("toast.session.share.failed.title"),
@@ -215,22 +211,12 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
     const sessionID = params.id
     if (!sessionID) return
 
-    await sdk()
-      .legacy.session.unshare(sessionID)
-      .then(() =>
-        showToast({
-          title: language.t("toast.session.unshare.success.title"),
-          description: language.t("toast.session.unshare.success.description"),
-          variant: "success",
-        }),
-      )
-      .catch(() =>
-        showToast({
-          title: language.t("toast.session.unshare.failed.title"),
-          description: language.t("toast.session.unshare.failed.description"),
-          variant: "error",
-        }),
-      )
+    // TODO: Restore unsharing when the V2 client exposes a session sharing API.
+    showToast({
+      title: language.t("toast.session.unshare.failed.title"),
+      description: language.t("toast.session.unshare.failed.description"),
+      variant: "error",
+    })
   }
 
   const openFile = () => {
@@ -308,7 +294,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
     const sessionID = params.id
     if (!sessionID) return
     const owner = sessionOwnership.capture()
-    const session = sdk().currentApi.session
+    const session = sdk().api.session
     const directory = sdk().directory
     const promptSession = prompt.capture()
     const revert = info()?.revert?.messageID
@@ -336,7 +322,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
     const sessionID = params.id
     if (!sessionID) return
     const owner = sessionOwnership.capture()
-    const session = sdk().currentApi.session
+    const session = sdk().api.session
     const messages = userMessages()
     const promptSession = prompt.capture()
 
@@ -368,7 +354,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
     const sessionID = params.id
     if (!sessionID) return
 
-    await sdk().currentApi.session.compact({ sessionID })
+    await sdk().api.session.compact({ sessionID })
   }
 
   const fork = () => {
@@ -379,8 +365,10 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
   }
 
   const shareCmds = () => {
-    if (protocol() !== "v1") return []
-    if (sync().data.config.share === "disabled") return []
+    // TODO: Restore these commands when the V2 client exposes session sharing.
+    // if (sync().data.config.share === "disabled") return []
+    return []
+    /*
     return [
       sessionCommand({
         id: "session.share",
@@ -401,6 +389,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
         onSelect: unshare,
       }),
     ]
+    */
   }
 
   const sessionCmds = () => [

+ 2 - 2
packages/app/src/pages/session/v2/review-panel-v2.tsx

@@ -102,8 +102,8 @@ export function ReviewPanelV2(props: ReviewPanelV2Props) {
 
   const readFile = async (path: string) =>
     sdk()
-      .currentApi.file.read({ path, location: { directory: sdk().directory } })
-      .then((content) => ({ type: "text" as const, content: new TextDecoder().decode(content) }))
+      .api.file.read({ path, location: { directory: sdk().directory } })
+      .then((data) => ({ type: "text" as const, content: new TextDecoder().decode(data) }))
       .catch((error) => {
         console.debug("[session-review-v2] failed to read file", { path, error })
         return undefined

+ 131 - 39
packages/app/src/types.ts

@@ -1,59 +1,57 @@
 import type {
-  Agent,
-  Config,
-  Event,
-  FileContent,
-  FileNode,
-  LspStatus,
-  Path,
-  PermissionRequest,
-  Project,
-  Provider,
-  ProviderAuthResponse,
-  ProviderListResponse,
+  EventSubscribeOutput,
+  FileDiffInfo,
+  FileDiffLegacyInfo,
+  ProjectListOutput,
   QuestionAnswer,
+  QuestionInfo,
   QuestionRequest,
   ReferenceInfo,
-  Session,
+  SessionInfo,
   SessionNotFoundError,
   SessionStatus,
-  SessionV2Info,
-  SnapshotFileDiff,
-  Todo,
-  V2SessionListResponse,
-  VcsFileDiff,
-  VcsInfo,
-} from "@opencode-ai/sdk/v2/client"
+  SessionV1Info,
+  SessionsResponse,
+} from "@opencode-ai/client/promise"
+import type { NormalizedProviderListResponse } from "@opencode-ai/session-ui/context"
 
 export type {
-  Agent,
-  Config,
-  Event,
-  FileContent,
-  FileNode,
-  LspStatus,
-  Path,
-  PermissionRequest,
-  Project,
-  Provider,
-  ProviderAuthResponse,
-  ProviderListResponse,
   QuestionAnswer,
   QuestionRequest,
   ReferenceInfo,
-  Session,
   SessionNotFoundError,
   SessionStatus,
-  SessionV2Info,
-  SnapshotFileDiff,
-  Todo,
-  V2SessionListResponse,
-  VcsFileDiff,
-  VcsInfo,
 }
 
+export type Project = Omit<ProjectListOutput[number], "canonical"> & { worktree: string }
+export type Session = Omit<SessionV1Info, "title"> & { title: string }
+export type SessionV2Info = SessionInfo
+export type V2SessionListResponse = SessionsResponse
+export type SnapshotFileDiff = FileDiffLegacyInfo
+export type VcsFileDiff = FileDiffInfo
+
+type CurrentEvent = EventSubscribeOutput extends infer Item
+  ? Item extends { type: infer Type extends string; data: infer Data }
+    ? { type: Type; properties: Data }
+    : never
+  : never
+
+export type Event =
+  | Exclude<CurrentEvent, { type: "permission.asked" }>
+  | { type: "permission.asked"; properties: PermissionRequest }
+
 export type EventSessionError = Extract<Event, { type: "session.error" }>
 
+export type PermissionRequest = {
+  id: string
+  sessionID: string
+  permission: string
+  patterns: string[]
+  metadata: Record<string, unknown>
+  always: string[]
+  tool?: { messageID: string; callID: string }
+}
+
 type MessageError =
   | { name: "ProviderAuthError"; data: { providerID: string; message: string } }
   | { name: "UnknownError"; data: { message: string; ref?: string } }
@@ -241,6 +239,100 @@ export type Part =
   | RetryPart
   | CompactionPart
 
+export type Todo = {
+  content: string
+  status: string
+  priority: string
+}
+
+export type FileNode = {
+  name: string
+  path: string
+  absolute: string
+  type: "file" | "directory"
+  ignored: boolean
+}
+
+export type FileContent = {
+  type: "text" | "binary"
+  content: string
+  diff?: string
+  patch?: {
+    oldFileName: string
+    newFileName: string
+    oldHeader?: string
+    newHeader?: string
+    hunks: Array<{
+      oldStart: number
+      oldLines: number
+      newStart: number
+      newLines: number
+      lines: string[]
+    }>
+    index?: string
+  }
+  encoding?: "base64"
+  mimeType?: string
+}
+
+export type Path = {
+  home: string
+  state: string
+  config: string
+  worktree: string
+  directory: string
+}
+
+export type VcsInfo = { branch?: string; default_branch?: string }
+export type LspStatus = { id: string; name: string; root: string; status: "connected" | "error" }
+
+export type Agent = {
+  name: string
+  description?: string
+  mode: "subagent" | "primary" | "all"
+  native?: boolean
+  hidden?: boolean
+  topP?: number
+  temperature?: number
+  color?: string
+  permission: Array<{ permission: string; pattern: string; action: "allow" | "deny" | "ask" }>
+  model?: { modelID: string; providerID: string }
+  variant?: string
+  prompt?: string
+  options: Record<string, unknown>
+  steps?: number
+}
+
+export type Provider = NormalizedProviderListResponse["all"] extends Map<string, infer Item> ? Item : never
+export type Model = Provider["models"][string]
+export type ProviderListResponse = NormalizedProviderListResponse
+
+export type ProviderAuthResponse = Record<string, unknown>
+
+export type Config = {
+  model?: string
+  small_model?: string
+  default_agent?: string
+  username?: string
+  share?: "manual" | "auto" | "disabled"
+  autoshare?: boolean
+  shell?: string
+  plugin?: Array<string | [string, Record<string, unknown>]>
+  provider?: Record<string, { npm?: string; models?: Record<string, unknown> }>
+  mcp?: Record<string, unknown>
+  agent?: Record<string, unknown>
+  command?: Record<string, unknown>
+  instructions?: string[]
+  disabled_providers?: string[]
+  enabled_providers?: string[]
+  permission?: string | Record<string, unknown>
+  tools?: Record<string, boolean>
+  experimental?: Record<string, unknown>
+  [key: string]: unknown
+}
+
 export type TextPartInput = Omit<TextPart, "id" | "sessionID" | "messageID"> & { id?: string }
 export type FilePartInput = Omit<FilePart, "id" | "sessionID" | "messageID"> & { id?: string }
 export type AgentPartInput = Omit<AgentPart, "id" | "sessionID" | "messageID"> & { id?: string }
+
+export type Question = QuestionInfo

+ 1 - 1
packages/app/src/utils/persist.test.ts

@@ -144,7 +144,7 @@ describe("persist localStorage resilience", () => {
       current,
       legacyStore,
       stores: [],
-      keys: target.legacy!,
+      keys: target["legacy"]!,
       key: target.key,
       defaults: { value: 1 },
     })

+ 1 - 1
packages/app/src/utils/persist.ts

@@ -558,7 +558,7 @@ export function persisted<T>(
   const config = resolveTarget(typeof target === "string" ? { key: target } : target, platform)
 
   const defaults = snapshot(store[0])
-  const legacy = config.legacy ?? []
+  const legacy = config["legacy"] ?? []
 
   const isDesktop = platform.platform === "desktop" && !!platform.storage
 

+ 0 - 255
packages/app/src/utils/server-compat.test.ts

@@ -1,255 +0,0 @@
-import { describe, expect, test } from "bun:test"
-import { createApiForServer, createSdkForServer } from "./server"
-import { createCompatibleApi } from "./server-compat"
-
-function setup(
-  protocol: "v1" | "v2" | Promise<"v1" | "v2">,
-  responses?: {
-    vcs?: { branch: string; default_branch: string }
-    question?: { id: string; sessionID: string; questions: never[]; tool?: { messageID: string; callID: string } }[]
-  },
-) {
-  const requests: Request[] = []
-  const fetcher = Object.assign(
-    async (input: string | URL | Request, init?: RequestInit) => {
-      const request = new Request(input, init)
-      requests.push(request)
-      if (request.method === "PATCH") {
-        return Response.json({
-          id: "ses_1",
-          slug: "ses_1",
-          projectID: "project",
-          directory: "/repo",
-          title: "Session",
-          version: "1",
-          time: { created: 1, updated: 1 },
-        })
-      }
-      if (request.method === "POST" && request.url.endsWith("/prompt_async"))
-        return new Response(undefined, { status: 204 })
-      if (request.method === "POST" && request.url.endsWith("/prompt")) {
-        return Response.json({
-          id: "msg_1",
-          sessionID: "ses_1",
-          timeCreated: 1,
-          type: "user",
-          data: { text: "hello" },
-          delivery: "steer",
-        })
-      }
-      if (request.method === "GET" && new URL(request.url).pathname === "/vcs")
-        return Response.json(responses?.vcs ?? {})
-      if (request.method === "GET" && new URL(request.url).pathname === "/question")
-        return Response.json(responses?.question ?? [])
-      if (request.method === "GET") return Response.json([])
-      return new Response(undefined, { status: 204 })
-    },
-    { preconnect: globalThis.fetch.preconnect },
-  )
-  const server = { url: "http://localhost:4096" }
-  const api = createCompatibleApi({
-    protocol: typeof protocol === "string" ? Promise.resolve(protocol) : protocol,
-    current: createApiForServer({ server, fetch: fetcher }),
-    legacy: (directory) => createSdkForServer({ server, fetch: fetcher, directory, throwOnError: true }),
-    directory: "/repo",
-  })
-  return { api, requests }
-}
-
-describe("createCompatibleApi", () => {
-  /*
-  test("routes V1 archive through the legacy session update", async () => {
-    const { api, requests } = setup("v1")
-    await api.session.archive({ sessionID: "ses_1", directory: "/repo" })
-
-    const url = new URL(requests[0]!.url)
-    expect(url.pathname).toBe("/session/ses_1")
-    expect(requests[0]!.headers.get("x-opencode-directory")).toBe("%2Frepo")
-    expect(requests[0]!.method).toBe("PATCH")
-    expect(await requests[0]!.json()).toMatchObject({ time: { archived: expect.any(Number) } })
-  })
-  */
-
-  test("converts current prompts to the V1 prompt contract", async () => {
-    const { api, requests } = setup("v1")
-    await api.session.prompt({
-      sessionID: "ses_1",
-      id: "msg_1",
-      text: "hello @src/index.ts",
-      agent: "build",
-      model: { providerID: "provider", modelID: "model" },
-      files: [
-        { uri: "file:///repo/src/index.ts", name: "index.ts", mention: { text: "@src/index.ts", start: 6, end: 19 } },
-        { uri: "data:text/plain;base64,aGVsbG8=", name: "notes.txt" },
-      ],
-    })
-
-    expect(new URL(requests[0]!.url).pathname).toBe("/session/ses_1/prompt_async")
-    const body = await requests[0]!.json()
-    expect(body).toMatchObject({
-      messageID: "msg_1",
-      agent: "build",
-      model: { providerID: "provider", modelID: "model" },
-      parts: [
-        { type: "text", text: "hello @src/index.ts" },
-        {
-          type: "file",
-          mime: "text/plain",
-          url: "file:///repo/src/index.ts",
-          filename: "index.ts",
-          source: {
-            type: "file",
-            text: { value: "@src/index.ts", start: 6, end: 19 },
-            path: "file:///repo/src/index.ts",
-          },
-        },
-        {
-          type: "file",
-          mime: "text/plain",
-          url: "data:text/plain;base64,aGVsbG8=",
-          filename: "notes.txt",
-        },
-      ],
-    })
-    expect(body.parts[2]).not.toHaveProperty("source")
-  })
-
-  test("preserves original parts for V1 optimistic reconciliation", async () => {
-    const { api, requests } = setup("v1")
-    await api.session.prompt({
-      sessionID: "ses_1",
-      id: "msg_1",
-      text: "look",
-      files: [{ uri: "data:image/png;base64,AAAA", name: "image.png" }],
-      legacyParts: [
-        { id: "prt_text", type: "text", text: "look" },
-        { id: "prt_image", type: "file", mime: "image/png", url: "data:image/png;base64,AAAA", filename: "image.png" },
-      ],
-    })
-
-    expect((await requests[0]!.json()).parts).toEqual([
-      { id: "prt_text", type: "text", text: "look" },
-      { id: "prt_image", type: "file", mime: "image/png", url: "data:image/png;base64,AAAA", filename: "image.png" },
-    ])
-  })
-
-  test("resolves protocol detection once across implementation methods", async () => {
-    let detections = 0
-    const resolved = Promise.resolve<"v1" | "v2">("v2")
-    const protocol = new Proxy(resolved, {
-      get(target, property) {
-        if (property !== "then") return Reflect.get(target, property, target)
-        detections++
-        return target.then.bind(target)
-      },
-    })
-    const { api } = setup(protocol)
-
-    await api.session.list()
-    await api.session.list()
-
-    expect(detections).toBe(1)
-  })
-
-  /*
-  test("keeps V2 session actions on the current API", async () => {
-    const { api, requests } = setup("v2")
-    await api.session.archive({ sessionID: "ses_1" })
-
-    expect(new URL(requests[0]!.url).pathname).toBe("/api/session/ses_1/archive")
-    expect(requests[0]!.method).toBe("POST")
-  })
-  */
-
-  test("uses the global V1 session search endpoint", async () => {
-    const { api, requests } = setup("v1")
-    await api.session.list({ parentID: null, search: "session", limit: 50 })
-
-    expect(new URL(requests[0]!.url).pathname).toBe("/experimental/session")
-  })
-
-  test("translates V1 question tool call IDs", async () => {
-    const { api } = setup("v1", {
-      question: [
-        {
-          id: "que_1",
-          sessionID: "ses_1",
-          questions: [],
-          tool: { messageID: "msg_1", callID: "call_1" },
-        },
-      ],
-    })
-
-    expect((await api.question.request.list()).data[0]?.tool).toEqual({ messageID: "msg_1", id: "call_1" })
-  })
-
-  /*
-  test("projects the V1 default branch", async () => {
-    const { api } = setup("v1", { vcs: { branch: "feature", default_branch: "dev" } })
-
-    expect(await api.vcs.get({ location: { directory: "/repo" } })).toMatchObject({
-      data: { branch: "feature", defaultBranch: "dev" },
-    })
-  })
-  */
-
-  test("translates current file searches to the V1 dirs parameter", async () => {
-    const { api, requests } = setup("v1")
-    await api.file.find({ location: { directory: "/repo" }, query: "src", type: "file", limit: 20 })
-
-    const url = new URL(requests[0]!.url)
-    expect(url.pathname).toBe("/find/file")
-    expect(url.searchParams.get("dirs")).toBe("false")
-    expect(url.searchParams.get("limit")).toBe("20")
-  })
-
-  test("routes V1 permission replies through the requested directory", async () => {
-    const { api, requests } = setup("v1")
-    await api.permission.reply({
-      sessionID: "ses_1",
-      requestID: "permission_1",
-      reply: "once",
-      location: { directory: "/other" },
-    })
-
-    expect(new URL(requests[0]!.url).pathname).toBe("/session/ses_1/permissions/permission_1")
-    expect(new URL(requests[0]!.url).searchParams.get("directory")).toBe("/other")
-  })
-
-  test("disposes the V1 instance after connecting a provider", async () => {
-    const { api, requests } = setup("v1")
-
-    await api.integration.connect.key({
-      integrationID: "openrouter",
-      key: "secret",
-      location: { directory: "/repo" },
-    })
-
-    expect(requests.map((request) => new URL(request.url).pathname)).toEqual([
-      "/auth/openrouter",
-      "/instance/dispose",
-      "/instance/dispose",
-    ])
-    expect(requests[1]!.headers.get("x-opencode-directory")).toBe("%2Frepo")
-    expect(requests[2]!.headers.get("x-opencode-directory")).toBeNull()
-  })
-
-  test("disposes the V1 instance after completing provider OAuth", async () => {
-    const { api, requests } = setup("v1")
-
-    await api.integration.oauth.complete({
-      integrationID: "openrouter",
-      attemptID: "openrouter:0",
-      code: "code",
-      location: { directory: "/repo" },
-    })
-
-    expect(requests.map((request) => new URL(request.url).pathname)).toEqual([
-      "/provider/openrouter/oauth/callback",
-      "/instance/dispose",
-      "/instance/dispose",
-    ])
-    expect(requests[1]!.headers.get("x-opencode-directory")).toBe("%2Frepo")
-    expect(requests[2]!.headers.get("x-opencode-directory")).toBeNull()
-  })
-})

+ 0 - 629
packages/app/src/utils/server-compat.ts

@@ -1,629 +0,0 @@
-import type { ServerApi } from "./server"
-import type { ServerProtocol } from "./server-protocol"
-import type { AgentPartInput, FilePartInput, Session, TextPartInput } from "@/types"
-import type { OpencodeClient } from "@opencode-ai/sdk/v2/client"
-import type {
-  ProjectCurrent,
-  SessionApi,
-  SessionCommandInput,
-  SessionCommandOutput,
-  SessionCompactInput,
-  SessionCompactOutput,
-  SessionInfo,
-  SessionPromptInput,
-  SessionPromptOutput,
-  SessionShellInput,
-  SessionShellOutput,
-} from "@opencode-ai/client/promise"
-
-type LegacyClient = OpencodeClient
-type LegacyFor = (directory?: string) => LegacyClient
-type CompatibleSessionApi = Omit<
-  SessionApi,
-  "prompt" | "command" | "shell" | "compact" | "rename" | "archive" | "remove"
-> & {
-  prompt: (input: SessionPromptInput & LegacyPrompt) => Promise<SessionPromptOutput>
-  command: (input: SessionCommandInput) => Promise<SessionCommandOutput>
-  shell: (input: SessionShellInput & LegacyPrompt) => Promise<SessionShellOutput>
-  compact: (input: SessionCompactInput & { model?: LegacyPrompt["model"] }) => Promise<SessionCompactOutput>
-  rename: (input: Parameters<SessionApi["rename"]>[0] & LegacyLocation) => ReturnType<SessionApi["rename"]>
-  remove: (input: Parameters<SessionApi["remove"]>[0] & LegacyLocation) => ReturnType<SessionApi["remove"]>
-}
-type CompatiblePermissionApi = Omit<ServerApi["permission"], "reply"> & {
-  reply: (
-    input: Parameters<ServerApi["permission"]["reply"]>[0] & { location?: { directory?: string } },
-  ) => ReturnType<ServerApi["permission"]["reply"]>
-}
-export type CompatibleApi = Omit<ServerApi, "session" | "permission"> & {
-  readonly session: CompatibleSessionApi
-  readonly permission: CompatiblePermissionApi
-}
-type LegacyPrompt = {
-  agent?: string
-  model?: { providerID: string; modelID: string }
-  variant?: string
-  legacyParts?: (TextPartInput | FilePartInput | AgentPartInput)[]
-}
-type LegacyLocation = { directory?: string }
-type CompatibleInput = {
-  protocol: Promise<ServerProtocol>
-  current: ServerApi
-  legacy: LegacyFor
-  directory?: string
-}
-
-export function createLegacyCapabilities(input: CompatibleInput) {
-  const directory = (value?: string) => value ?? input.directory
-  const client = (value?: string) => input.legacy(directory(value))
-  const requireV1 = async () => {
-    if ((await input.protocol) !== "v1") throw new Error("This capability is unavailable on V2 servers")
-  }
-
-  return {
-    config: {
-      global: async () => {
-        await requireV1()
-        return (await client().global.config.get()).data ?? {}
-      },
-      directory: async (value?: string) => {
-        await requireV1()
-        return (await client(value).config.get()).data ?? {}
-      },
-      update: async (config: NonNullable<Parameters<LegacyClient["global"]["config"]["update"]>[0]>["config"]) => {
-        await requireV1()
-        return client().global.config.update({ config })
-      },
-    },
-    auth: {
-      set: async (value: Parameters<LegacyClient["auth"]["set"]>[0]) => {
-        await requireV1()
-        return client().auth.set(value)
-      },
-      remove: async (value: Parameters<LegacyClient["auth"]["remove"]>[0]) => {
-        await requireV1()
-        return client().auth.remove(value)
-      },
-    },
-    session: {
-      get: (value: Parameters<LegacyClient["session"]["get"]>[0]) => client().session.get(value),
-      messages: (value: Parameters<LegacyClient["session"]["messages"]>[0]) => client().session.messages(value),
-      message: (value: Parameters<LegacyClient["session"]["message"]>[0]) => client().session.message(value),
-      share: async (sessionID: string) => {
-        await requireV1()
-        return client().session.share({ sessionID })
-      },
-      unshare: async (sessionID: string) => {
-        await requireV1()
-        return client().session.unshare({ sessionID })
-      },
-      archive: async (sessionID: string, value?: string) => {
-        await requireV1()
-        return client(value).session.update({ sessionID, time: { archived: Date.now() } })
-      },
-      todo: async (sessionID: string, value?: string) => {
-        await requireV1()
-        return (await client(value).session.todo({ sessionID })).data ?? []
-      },
-    },
-    project: {
-      update: async (value: Parameters<LegacyClient["project"]["update"]>[0]) => {
-        await requireV1()
-        return client(value.directory).project.update(value)
-      },
-      initGit: async (value?: string) => {
-        await requireV1()
-        return client(value).project.initGit()
-      },
-    },
-    workspace: {
-      reset: async (root: string, value: string) => {
-        await requireV1()
-        await client(value).instance.dispose().catch(() => undefined)
-        return client(root).worktree.reset({ worktreeResetInput: { directory: value } })
-      },
-    },
-    pty: {
-      shells: async () => {
-        await requireV1()
-        return (await client().pty.shells()).data ?? []
-      },
-    },
-    path: {
-      get: async (value?: string) => {
-        await requireV1()
-        return (await client(value).path.get()).data
-      },
-    },
-    lsp: {
-      status: async (value: string) => {
-        await requireV1()
-        return (await client(value).lsp.status()).data ?? []
-      },
-    },
-  }
-}
-
-export type LegacyCapabilities = ReturnType<typeof createLegacyCapabilities>
-
-function mime(uri: string) {
-  const match = /^data:([^;,]+)/.exec(uri)
-  return match?.[1] ?? "application/octet-stream"
-}
-
-function sessionInfo(session: Session): SessionInfo {
-  return {
-    id: session.id,
-    parentID: session.parentID,
-    projectID: session.projectID,
-    agent: session.agent,
-    model: session.model && {
-      id: session.model.id,
-      providerID: session.model.providerID,
-      variant: session.model.variant,
-    },
-    cost: session.cost ?? 0,
-    tokens: session.tokens ?? { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
-    time: session.time,
-    title: session.title,
-    location: { directory: session.directory, workspaceID: session.workspaceID },
-    subpath: session.path,
-    revert: session.revert && {
-      messageID: session.revert.messageID,
-      partID: session.revert.partID,
-      snapshot: session.revert.snapshot,
-    },
-  }
-}
-
-export function createCompatibleApi(input: CompatibleInput): CompatibleApi {
-  const v1 = createV1Api(input)
-  return lazyApi(
-    input.protocol.then((protocol) => (protocol === "v1" ? v1 : input.current)),
-    input.current,
-  )
-}
-
-function lazyApi<T extends object>(implementation: Promise<T>, shape: T): T {
-  const cache = new Map<PropertyKey, unknown>()
-  return new Proxy(shape, {
-    get(target, property, receiver) {
-      const sample = Reflect.get(target, property, receiver)
-      if (typeof sample === "function") {
-        return (...args: unknown[]) =>
-          implementation.then((value) => {
-            const method = Reflect.get(value, property)
-            if (typeof method !== "function") throw new Error(`API method unavailable: ${String(property)}`)
-            return Reflect.apply(method, value, args)
-          })
-      }
-      if (sample === null || typeof sample !== "object") return sample
-      if (cache.has(property)) return cache.get(property)
-      const nested = lazyApi(
-        implementation.then((value) => {
-          const result = Reflect.get(value, property)
-          if (result === null || typeof result !== "object") {
-            throw new Error(`API namespace unavailable: ${String(property)}`)
-          }
-          return result
-        }),
-        sample,
-      )
-      cache.set(property, nested)
-      return nested
-    },
-  })
-}
-
-function createV1Api(input: CompatibleInput): CompatibleApi {
-  const directory = (location?: { directory?: string }) => location?.directory ?? input.directory
-  const legacy = (location?: { directory?: string }) => input.legacy(directory(location))
-  const located = <T>(data: T, value?: { directory?: string }) => ({
-    location: {
-      directory: directory(value) ?? "",
-      project: { id: "", directory: directory(value) ?? "", canonical: directory(value) ?? "" },
-    },
-    data,
-  })
-
-  return {
-    ...input.current,
-    session: {
-      ...input.current.session,
-      async list(
-        value?: Parameters<ServerApi["session"]["list"]>[0],
-        options?: Parameters<ServerApi["session"]["list"]>[1],
-      ) {
-        if (!value?.directory && value?.search !== undefined) {
-          const result = await legacy().experimental.session.list(
-            {
-              roots: value.parentID === null ? true : undefined,
-              search: value.search,
-              limit: value.limit,
-            },
-            options,
-          )
-          return { data: (result.data ?? []).map(sessionInfo), cursor: {} }
-        }
-        const result = await legacy({ directory: value?.directory }).session.list({
-          directory: value?.directory,
-          roots: value?.parentID === null ? true : undefined,
-          search: value?.search,
-          limit: value?.limit,
-        })
-        return { data: (result.data ?? []).map(sessionInfo), cursor: {} }
-      },
-      async create(value?: Parameters<ServerApi["session"]["create"]>[0]) {
-        const result = await legacy(value?.location ?? undefined).session.create({
-          directory: directory(value?.location ?? undefined),
-        })
-        if (!result.data) throw new Error("Failed to create session")
-        return sessionInfo(result.data)
-      },
-      async get(value: Parameters<ServerApi["session"]["get"]>[0]) {
-        const result = await legacy().session.get(value)
-        if (!result.data) throw new Error(`Session not found: ${value.sessionID}`)
-        return sessionInfo(result.data)
-      },
-      async active() {
-        const result = await legacy().session.status()
-        return Object.fromEntries(
-          Object.entries(result.data ?? {}).flatMap(([sessionID, status]) =>
-            status.type === "idle" ? [] : [[sessionID, { type: "running" as const }]],
-          ),
-        )
-      },
-      async rename(value: Parameters<ServerApi["session"]["rename"]>[0] & LegacyLocation) {
-        await legacy(value).session.update({ sessionID: value.sessionID, title: value.title })
-      },
-      async remove(value: Parameters<ServerApi["session"]["remove"]>[0] & LegacyLocation) {
-        await legacy(value).session.delete(value)
-      },
-      async fork(value: Parameters<ServerApi["session"]["fork"]>[0]) {
-        const result = await legacy().session.fork(value)
-        if (!result.data) throw new Error("Failed to fork session")
-        return sessionInfo(result.data)
-      },
-      async interrupt(value: Parameters<ServerApi["session"]["interrupt"]>[0]) {
-        await legacy().session.abort(value)
-      },
-      async prompt(value: SessionPromptInput & LegacyPrompt) {
-        await legacy().session.promptAsync({
-          sessionID: value.sessionID,
-          messageID: value.id ?? undefined,
-          agent: value.agent,
-          model: value.model,
-          variant: value.variant,
-          parts: value.legacyParts ?? [
-            { type: "text", text: value.text },
-            ...(value.files ?? []).map((file) => ({
-              type: "file" as const,
-              mime: file.mention ? "text/plain" : mime(file.uri),
-              url: file.uri,
-              filename: file.name,
-              source: file.mention
-                ? {
-                    type: "file" as const,
-                    text: { value: file.mention.text, start: file.mention.start, end: file.mention.end },
-                    path: file.uri,
-                  }
-                : undefined,
-            })),
-            ...(value.agents ?? []).map((agent) => ({
-              type: "agent" as const,
-              name: agent.name,
-              source: agent.mention
-                ? { value: agent.mention.text, start: agent.mention.start, end: agent.mention.end }
-                : undefined,
-            })),
-          ],
-        })
-        return {
-          id: value.id ?? "",
-          sessionID: value.sessionID,
-          timeCreated: Date.now(),
-          type: "user",
-          data: { text: value.text },
-          delivery: value.delivery ?? "steer",
-        }
-      },
-      async command(value: SessionCommandInput) {
-        await legacy().session.command({
-          sessionID: value.sessionID,
-          messageID: value.id ?? undefined,
-          command: value.command,
-          arguments: value.arguments ?? "",
-          agent: value.agent ?? undefined,
-          model: value.model ? `${value.model.providerID}/${value.model.id}` : undefined,
-          variant: value.model?.variant,
-          parts: value.files?.map((file) => ({
-            type: "file" as const,
-            mime: mime(file.uri),
-            url: file.uri,
-            filename: file.name,
-          })),
-        })
-        return {
-          id: value.id ?? "",
-          sessionID: value.sessionID,
-          timeCreated: Date.now(),
-          type: "user",
-          data: { text: `/${value.command} ${value.arguments ?? ""}`.trim() },
-          delivery: value.delivery ?? "steer",
-        }
-      },
-      async shell(value: SessionShellInput & LegacyPrompt) {
-        await legacy().session.shell({
-          sessionID: value.sessionID,
-          command: value.command,
-          agent: value.agent,
-          model: value.model,
-        })
-      },
-      compact: async (value: SessionCompactInput & { model?: LegacyPrompt["model"] }) => {
-        if (!value.model) throw new Error("A model is required to compact a V1 session")
-        await legacy().session.summarize({
-          sessionID: value.sessionID,
-          providerID: value.model.providerID,
-          modelID: value.model.modelID,
-        })
-        return {
-          id: value.id ?? "",
-          sessionID: value.sessionID,
-          timeCreated: Date.now(),
-          type: "compaction",
-        }
-      },
-      revert: {
-        stage: async (value: Parameters<ServerApi["session"]["revert"]["stage"]>[0]) => {
-          await legacy().session.revert(value)
-          return { messageID: value.messageID }
-        },
-        clear: async (value: Parameters<ServerApi["session"]["revert"]["clear"]>[0]) => {
-          await legacy().session.unrevert(value)
-        },
-        commit: input.current.session.revert.commit,
-      },
-    },
-    project: {
-      ...input.current.project,
-      async list() {
-        return ((await legacy().project.list()).data ?? []).map((project) => ({
-          ...project,
-          canonical: project.worktree,
-        }))
-      },
-      async current(value?: Parameters<ServerApi["project"]["current"]>[0]) {
-        const result = await legacy(value?.location).project.current()
-        if (!result.data) throw new Error("Project not found")
-        return {
-          id: result.data.id,
-          directory: result.data.worktree,
-          canonical: result.data.worktree,
-        } satisfies ProjectCurrent
-      },
-      async directories(value: Parameters<ServerApi["project"]["directories"]>[0]) {
-        const result = await legacy(value.location).worktree.list()
-        return (result.data ?? []).map((item) => ({ directory: item }))
-      },
-    },
-    location: {
-      ...input.current.location,
-      async get(value?: Parameters<ServerApi["location"]["get"]>[0]) {
-        const result = await legacy(value?.location).path.get()
-        if (!result.data) throw new Error("Location unavailable")
-        return {
-          directory: result.data.directory,
-          project: {
-            id: "",
-            directory: result.data.worktree,
-            canonical: result.data.worktree,
-          },
-        }
-      },
-    },
-    vcs: {
-      ...input.current.vcs,
-      async status(value?: Parameters<ServerApi["vcs"]["status"]>[0]) {
-        const result = await legacy(value?.location).vcs.status()
-        return located(result.data ?? [], value?.location)
-      },
-      async diff(value: Parameters<ServerApi["vcs"]["diff"]>[0]) {
-        const result = await legacy(value.location).vcs.diff({
-          mode: value.mode === "working" ? "git" : value.mode,
-          context: value.context,
-        })
-        return located(
-          (result.data ?? []).map((file) => ({
-            file: file.file,
-            patch: file.patch ?? "",
-            additions: file.additions,
-            deletions: file.deletions,
-            status: file.status ?? "modified",
-          })),
-          value.location,
-        )
-      },
-    },
-    file: {
-      ...input.current.file,
-      async list(value?: Parameters<ServerApi["file"]["list"]>[0]) {
-        const result = await legacy(value?.location).file.list({ path: value?.path ?? "" })
-        return located(result.data ?? [], value?.location)
-      },
-      async find(value: Parameters<ServerApi["file"]["find"]>[0]) {
-        const result = await legacy(value.location).find.files({
-          query: value.query,
-          dirs: value.type === undefined ? undefined : value.type === "directory" ? "true" : "false",
-          limit: value.limit,
-        })
-        return located(
-          (result.data ?? []).map((path) => ({ path, type: value.type ?? "file" })),
-          value.location,
-        )
-      },
-    },
-    integration: {
-      ...input.current.integration,
-      async get(value: Parameters<ServerApi["integration"]["get"]>[0]) {
-        const methods = ((await legacy(value.location).provider.auth()).data?.[value.integrationID] ?? []).map(
-          (method, index) =>
-            method.type === "api"
-              ? { type: "key" as const, label: method.label }
-              : { type: "oauth" as const, id: String(index), label: method.label, prompts: method.prompts },
-        )
-        return located(
-          {
-            id: value.integrationID,
-            name: value.integrationID,
-            methods,
-            connections: [],
-          },
-          value.location,
-        )
-      },
-      connect: {
-        ...input.current.integration.connect,
-        key: async (value: Parameters<ServerApi["integration"]["connect"]["key"]>[0]) => {
-          await legacy(value.location).auth.set({
-            providerID: value.integrationID,
-            auth: { type: "api", key: value.key },
-          })
-          await legacy(value.location).instance.dispose()
-          await input.legacy().instance.dispose()
-        },
-      },
-      oauth: {
-        ...input.current.integration.oauth,
-        connect: async (value: Parameters<ServerApi["integration"]["oauth"]["connect"]>[0]) => {
-          const method = Number(value.methodID)
-          const result = await legacy(value.location).provider.oauth.authorize(
-            { providerID: value.integrationID, method, inputs: value.inputs },
-            { throwOnError: true },
-          )
-          if (!result.data) throw new Error("Failed to start OAuth authorization")
-          return located(
-            {
-              attemptID: `${value.integrationID}:${method}`,
-              url: result.data.url,
-              instructions: result.data.instructions,
-              mode: result.data.method,
-              time: { created: Date.now(), expires: Date.now() + 10 * 60 * 1000 },
-            },
-            value.location,
-          )
-        },
-        complete: async (value: Parameters<ServerApi["integration"]["oauth"]["complete"]>[0]) => {
-          const method = Number(value.attemptID.split(":").at(-1))
-          await legacy(value.location).provider.oauth.callback(
-            { providerID: value.integrationID, method, code: value.code },
-            { throwOnError: true },
-          )
-          await legacy(value.location).instance.dispose()
-          await input.legacy().instance.dispose()
-        },
-        status: async (value: Parameters<ServerApi["integration"]["oauth"]["status"]>[0]) => {
-          const method = Number(value.attemptID.split(":").at(-1))
-          await legacy(value.location).provider.oauth.callback(
-            { providerID: value.integrationID, method },
-            { throwOnError: true },
-          )
-          await legacy(value.location).instance.dispose()
-          await input.legacy().instance.dispose()
-          return located(
-            { status: "complete" as const, time: { created: Date.now(), expires: Date.now() } },
-            value.location,
-          )
-        },
-      },
-    },
-    pty: {
-      ...input.current.pty,
-      async list(value?: Parameters<ServerApi["pty"]["list"]>[0]) {
-        return located((await legacy(value?.location).pty.list()).data ?? [], value?.location)
-      },
-      async create(value?: Parameters<ServerApi["pty"]["create"]>[0]) {
-        const result = await legacy(value?.location).pty.create({
-          command: value?.command,
-          args: value?.args ? [...value.args] : undefined,
-          cwd: value?.cwd,
-          title: value?.title,
-          env: value?.env,
-        })
-        if (!result.data) throw new Error("Failed to create terminal")
-        return located(result.data, value?.location)
-      },
-      async get(value: Parameters<ServerApi["pty"]["get"]>[0]) {
-        const result = await legacy(value.location).pty.get({ ptyID: value.ptyID })
-        if (!result.data) throw new Error(`Terminal not found: ${value.ptyID}`)
-        return located(result.data, value.location)
-      },
-      async update(value: Parameters<ServerApi["pty"]["update"]>[0]) {
-        const result = await legacy(value.location).pty.update({
-          ptyID: value.ptyID,
-          title: value.title,
-          size: value.size,
-        })
-        if (!result.data) throw new Error(`Terminal not found: ${value.ptyID}`)
-        return located(result.data, value.location)
-      },
-      async remove(value: Parameters<ServerApi["pty"]["remove"]>[0]) {
-        await legacy(value.location).pty.remove({ ptyID: value.ptyID })
-      },
-    },
-    permission: {
-      ...input.current.permission,
-      request: {
-        ...input.current.permission.request,
-        async list(value?: Parameters<ServerApi["permission"]["request"]["list"]>[0]) {
-          const result = await legacy(value?.location).permission.list()
-          return located(
-            (result.data ?? []).map((item) => ({
-              id: item.id,
-              sessionID: item.sessionID,
-              action: item.permission,
-              resources: item.patterns,
-              metadata: item.metadata,
-              save: item.always,
-              source: item.tool
-                ? { type: "tool" as const, messageID: item.tool.messageID, callID: item.tool.callID }
-                : undefined,
-            })),
-            value?.location,
-          ) as Awaited<ReturnType<ServerApi["permission"]["request"]["list"]>>
-        },
-      },
-      async reply(value: Parameters<ServerApi["permission"]["reply"]>[0] & { location?: { directory?: string } }) {
-        await legacy(value.location).permission.respond({
-          sessionID: value.sessionID,
-          permissionID: value.requestID,
-          response: value.reply,
-          directory: directory(value.location),
-        })
-      },
-    },
-    question: {
-      ...input.current.question,
-      request: {
-        ...input.current.question.request,
-        async list(value?: Parameters<ServerApi["question"]["request"]["list"]>[0]) {
-          return located(
-            ((await legacy(value?.location).question.list()).data ?? []).map((request) => ({
-              ...request,
-              tool: request.tool && { messageID: request.tool.messageID, id: request.tool.callID },
-            })),
-            value?.location,
-          )
-        },
-      },
-      async reply(value: Parameters<ServerApi["question"]["reply"]>[0]) {
-        await legacy().question.reply({
-          requestID: value.requestID,
-          answers: value.answers.map((answer) => [...answer]),
-        })
-      },
-      async reject(value: Parameters<ServerApi["question"]["reject"]>[0]) {
-        await legacy().question.reject({ requestID: value.requestID })
-      },
-    },
-  }
-}

+ 1 - 27
packages/app/src/utils/server-health.test.ts

@@ -29,32 +29,6 @@ describe("checkServerHealth", () => {
     expect(request?.pathname).toBe("/api/health")
   })
 
-  test("falls back to the V1 health endpoint", async () => {
-    const paths: string[] = []
-    const fetch = (async (input: RequestInfo | URL) => {
-      const url = input instanceof URL ? input : new URL(input instanceof Request ? input.url : input)
-      paths.push(url.pathname)
-      if (url.pathname === "/api/health") return new Response(undefined, { status: 404 })
-      return Response.json({ healthy: true, version: "1.18.4" })
-    }) as unknown as typeof globalThis.fetch
-
-    expect(await checkServerHealth(server, fetch)).toEqual({ healthy: true, version: "1.18.4" })
-    expect(paths).toEqual(["/api/health", "/global/health"])
-  })
-
-  test("falls back when the current health response is malformed", async () => {
-    const paths: string[] = []
-    const fetch = (async (input: RequestInfo | URL) => {
-      const url = input instanceof URL ? input : new URL(input instanceof Request ? input.url : input)
-      paths.push(url.pathname)
-      if (url.pathname === "/api/health") return Response.json({})
-      return Response.json({ healthy: true, version: "1.18.4" })
-    }) as unknown as typeof globalThis.fetch
-
-    expect(await checkServerHealth(server, fetch)).toEqual({ healthy: true, version: "1.18.4" })
-    expect(paths).toEqual(["/api/health", "/global/health"])
-  })
-
   test("allows slow servers thirty seconds by default", async () => {
     const timeout = Object.getOwnPropertyDescriptor(AbortSignal, "timeout")
     let timeoutMs = 0
@@ -172,7 +146,7 @@ describe("checkServerHealth", () => {
       retryDelayMs: 1,
     })
 
-    expect(count).toBe(6)
+    expect(count).toBe(3)
     expect(result).toEqual({ healthy: false })
   })
 })

+ 2 - 5
packages/app/src/utils/server-health.ts

@@ -1,6 +1,6 @@
 import { usePlatform } from "@/context/platform"
 import { ServerConnection } from "@/context/server"
-import { authTokenFromCredentials, createSdkForServer } from "./server"
+import { authTokenFromCredentials } from "./server"
 import { ClientError, OpenCode } from "@opencode-ai/client"
 import { Accessor, createEffect, onCleanup } from "solid-js"
 import { createStore, reconcile } from "solid-js/store"
@@ -104,10 +104,7 @@ export async function checkServerHealth(
     if ("data" in current && current.data) return current.data
     if (signal?.aborted) return { healthy: false }
 
-    return createSdkForServer({ server, fetch, signal })
-      .global.health()
-      .then((x) => (x.error ? next(count, x.error) : { healthy: x.data?.healthy === true, version: x.data?.version }))
-      .catch((error) => next(count, error))
+    return next(count, current.error)
   }
   return attempt(0).finally(() => timeout?.clear?.())
 }

+ 0 - 40
packages/app/src/utils/server-protocol.test.ts

@@ -1,40 +0,0 @@
-import { describe, expect, test } from "bun:test"
-import { detectServerProtocol } from "./server-protocol"
-
-const server = { url: "http://localhost:4096" }
-const json = (value: unknown, status = 200) =>
-  new Response(JSON.stringify(value), { status, headers: { "content-type": "application/json" } })
-const mockFetch = (run: (input: string | URL | Request) => Promise<Response>) =>
-  Object.assign(run, { preconnect: globalThis.fetch.preconnect })
-
-describe("detectServerProtocol", () => {
-  test("prefers the legacy health endpoint when both API generations exist", async () => {
-    const fetcher = mockFetch((input) => {
-      const path = new URL(input instanceof Request ? input.url : input).pathname
-      if (path === "/global/health") return Promise.resolve(json({ healthy: true, version: "1.18.4" }))
-      return Promise.resolve(json({ healthy: true, version: "2.0.0", pid: 123 }))
-    })
-
-    expect(await detectServerProtocol(server, fetcher)).toBe("v1")
-  })
-
-  test("recognizes V2 health by its process identifier", async () => {
-    const fetcher = mockFetch((input) => {
-      const path = new URL(input instanceof Request ? input.url : input).pathname
-      if (path === "/global/health") return Promise.resolve(json({}, 404))
-      return Promise.resolve(json({ healthy: true, version: "2.0.0", pid: 123 }))
-    })
-
-    expect(await detectServerProtocol(server, fetcher)).toBe("v2")
-  })
-
-  test("recognizes the transitional V1 API health response", async () => {
-    const fetcher = mockFetch((input) => {
-      const path = new URL(input instanceof Request ? input.url : input).pathname
-      if (path === "/global/health") return Promise.resolve(json({}, 404))
-      return Promise.resolve(json({ healthy: true }))
-    })
-
-    expect(await detectServerProtocol(server, fetcher)).toBe("v1")
-  })
-})

+ 0 - 35
packages/app/src/utils/server-protocol.ts

@@ -1,35 +0,0 @@
-import type { ServerConnection } from "@/context/server"
-import { authTokenFromCredentials } from "./server"
-
-export type ServerProtocol = "v1" | "v2"
-
-function headers(server: ServerConnection.HttpBase) {
-  if (!server.password) return
-  return {
-    Authorization: `Basic ${authTokenFromCredentials({ username: server.username, password: server.password })}`,
-  }
-}
-
-async function probe(server: ServerConnection.HttpBase, fetch: typeof globalThis.fetch, path: string) {
-  const response = await fetch(new URL(path, server.url), {
-    headers: headers(server),
-    signal: AbortSignal.timeout(5_000),
-  })
-  if (!response.ok || !response.headers.get("content-type")?.includes("application/json")) return
-  const value: unknown = await response.json()
-  if (!value || typeof value !== "object") return
-  return value
-}
-
-export async function detectServerProtocol(
-  server: ServerConnection.HttpBase,
-  fetch: typeof globalThis.fetch,
-): Promise<ServerProtocol> {
-  const legacy = await probe(server, fetch, "/global/health").catch(() => undefined)
-  if (legacy && "healthy" in legacy && legacy.healthy === true) return "v1"
-
-  const current = await probe(server, fetch, "/api/health").catch(() => undefined)
-  if (current && "pid" in current && typeof current.pid === "number") return "v2"
-  if (current && "healthy" in current && current.healthy === true) return "v1"
-  return "v2"
-}

+ 0 - 24
packages/app/src/utils/server.ts

@@ -1,4 +1,3 @@
-import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"
 import { OpenCode, type OpenCodeClient } from "@opencode-ai/client/promise"
 import type { ServerConnection } from "@/context/server"
 import { decode64 } from "@/utils/base64"
@@ -18,29 +17,6 @@ export function authFromToken(token: string | null) {
   }
 }
 
-export function createSdkForServer({
-  server,
-  ...config
-}: Omit<NonNullable<Parameters<typeof createOpencodeClient>[0]>, "baseUrl"> & {
-  server: ServerConnection.HttpBase
-}) {
-  const auth = (() => {
-    if (!server.password) return
-    return {
-      Authorization: `Basic ${authTokenFromCredentials({ username: server.username, password: server.password })}`,
-    }
-  })()
-
-  return createOpencodeClient({
-    ...config,
-    headers: {
-      ...(config.headers instanceof Headers ? Object.fromEntries(config.headers.entries()) : config.headers),
-      ...auth,
-    },
-    baseUrl: server.url,
-  })
-}
-
 export function createApiForServer(input: {
   server: ServerConnection.HttpBase
   fetch?: typeof globalThis.fetch

+ 0 - 25
packages/app/src/utils/session-message.test.ts

@@ -3,31 +3,6 @@ import type { SessionMessageInfo } from "@opencode-ai/client/promise"
 import { normalizeSessionMessages } from "./session-message"
 
 describe("normalizeSessionMessages", () => {
-  test("keeps attachments without inventing an empty text part", () => {
-    const source = [
-      {
-        id: "msg_1",
-        type: "user",
-        text: "",
-        files: [
-          {
-            data: "aGVsbG8=",
-            mime: "text/plain",
-            name: "note.txt",
-            source: { type: "inline" },
-          },
-        ],
-        agents: [{ name: "review" }],
-        time: { created: 1 },
-      },
-    ] satisfies SessionMessageInfo[]
-
-    const result = normalizeSessionMessages("ses_1", source)
-
-    expect(result.messages).toHaveLength(1)
-    expect(result.parts.get("msg_1")?.map((part) => part.type)).toEqual(["file", "agent"])
-  })
-
   test("projects current turns into stable legacy rendering records", () => {
     const source = [
       { id: "msg_1", type: "agent-switched", agent: "build", time: { created: 1 } },

+ 7 - 0
packages/app/src/utils/session-title.ts

@@ -0,0 +1,7 @@
+const pattern = /^(New session|Child session) - \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/
+
+export function sessionTitle(title?: string) {
+  if (!title) return title
+  const match = title.match(pattern)
+  return match?.[1] ?? title
+}

+ 0 - 57
packages/app/src/utils/terminal-websocket-url.test.ts

@@ -20,61 +20,4 @@ describe("terminalWebSocketURL", () => {
     expect(url.searchParams.get("ticket")).toBe("connect-ticket")
     expect(url.searchParams.has("auth_token")).toBe(false)
   })
-
-  test("uses query auth without embedding credentials in websocket URL for v1", () => {
-    const url = terminalWebSocketURL({
-      protocol: "v1",
-      url: "http://127.0.0.1:49365",
-      id: "pty_test",
-      directory: "/tmp/project",
-      cursor: 0,
-      sameOrigin: false,
-      username: "opencode",
-      password: "secret",
-    })
-
-    expect(url.protocol).toBe("ws:")
-    expect(url.username).toBe("")
-    expect(url.password).toBe("")
-    expect(url.pathname).toBe("/pty/pty_test/connect")
-    expect(url.searchParams.get("directory")).toBe("/tmp/project")
-    expect(url.searchParams.get("auth_token")).toBe(btoa("opencode:secret"))
-  })
-
-  test("omits query auth for same-origin saved credentials for v1", () => {
-    const url = terminalWebSocketURL({
-      protocol: "v1",
-      url: "https://app.example.test",
-      id: "pty_test",
-      directory: "/tmp/project",
-      cursor: 10,
-      sameOrigin: true,
-      username: "opencode",
-      password: "secret",
-    })
-
-    expect(url.protocol).toBe("wss:")
-    expect(url.pathname).toBe("/pty/pty_test/connect")
-    expect(url.searchParams.get("directory")).toBe("/tmp/project")
-    expect(url.searchParams.has("auth_token")).toBe(false)
-  })
-
-  test("uses query auth for same-origin credentials from auth_token for v1", () => {
-    const url = terminalWebSocketURL({
-      protocol: "v1",
-      url: "https://app.example.test",
-      id: "pty_test",
-      directory: "/tmp/project",
-      cursor: 10,
-      sameOrigin: true,
-      username: "opencode",
-      password: "secret",
-      authToken: true,
-    })
-
-    expect(url.protocol).toBe("wss:")
-    expect(url.pathname).toBe("/pty/pty_test/connect")
-    expect(url.searchParams.get("directory")).toBe("/tmp/project")
-    expect(url.searchParams.get("auth_token")).toBe(btoa("opencode:secret"))
-  })
 })

+ 2 - 20
packages/app/src/utils/terminal-websocket-url.ts

@@ -1,35 +1,17 @@
-import { authTokenFromCredentials } from "@/utils/server"
-
 export function terminalWebSocketURL(input: {
-  protocol?: "v1" | "v2"
   url: string
   id: string
   directory: string
   cursor: number
   ticket?: string
-  sameOrigin?: boolean
-  username?: string
-  password?: string
-  authToken?: boolean
 }) {
-  const isV1 = input.protocol === "v1"
-  const next = new URL(`${input.url}${isV1 ? `/pty/${input.id}/connect` : `/api/pty/${input.id}/connect`}`)
-  if (isV1) {
-    next.searchParams.set("directory", input.directory)
-  } else {
-    next.searchParams.set("location[directory]", input.directory)
-  }
+  const next = new URL(`${input.url}/api/pty/${input.id}/connect`)
+  next.searchParams.set("location[directory]", input.directory)
   next.searchParams.set("cursor", String(input.cursor))
   next.protocol = next.protocol === "https:" ? "wss:" : "ws:"
   if (input.ticket) {
     next.searchParams.set("ticket", input.ticket)
     return next
   }
-  if (isV1 && input.password && (!input.sameOrigin || input.authToken)) {
-    next.searchParams.set(
-      "auth_token",
-      authTokenFromCredentials({ username: input.username, password: input.password }),
-    )
-  }
   return next
 }

+ 1 - 1
packages/app/test-browser/command-palette.test.ts

@@ -1,5 +1,5 @@
 import { describe, expect, test } from "bun:test"
-import type { Project } from "@opencode-ai/sdk/v2/client"
+import type { Project } from "@/types"
 import type { SessionInfo } from "@opencode-ai/client/promise"
 import { createRoot } from "solid-js"
 import { createServerSessionEntries } from "@/components/command-palette"