Просмотр исходного кода

chore(app): vendor v2 client snapshot (#38818)

Brendan Allan 1 месяц назад
Родитель
Сommit
0a6637e17a
33 измененных файлов с 268 добавлено и 191 удалено
  1. 4 4
      bun.lock
  2. 7 9
      packages/app/e2e/regression/review-state-persistence.spec.ts
  3. 22 24
      packages/app/e2e/regression/review-terminal-stacked.spec.ts
  4. 1 1
      packages/app/e2e/regression/terminal-tab-switch.spec.ts
  5. 1 1
      packages/app/package.json
  6. 8 1
      packages/app/src/components/dialog-select-directory-v2.tsx
  7. 7 2
      packages/app/src/components/dialog-select-directory.tsx
  8. 17 6
      packages/app/src/components/edit-project.ts
  9. 1 1
      packages/app/src/components/prompt-input.tsx
  10. 2 1
      packages/app/src/components/settings-general.tsx
  11. 2 1
      packages/app/src/components/settings-v2/general.tsx
  12. 8 14
      packages/app/src/components/terminal.tsx
  13. 2 1
      packages/app/src/context/directory-sync.ts
  14. 5 3
      packages/app/src/context/global-sync/bootstrap.test.ts
  15. 26 21
      packages/app/src/context/global-sync/bootstrap.ts
  16. 13 13
      packages/app/src/context/global-sync/event-reducer.ts
  17. 16 10
      packages/app/src/context/layout.tsx
  18. 1 1
      packages/app/src/context/server-session-v2-reducer.test.ts
  19. 14 7
      packages/app/src/context/server-session-v2-reducer.ts
  20. 4 1
      packages/app/src/context/server-session.test.ts
  21. 8 5
      packages/app/src/context/server-session.ts
  22. 3 1
      packages/app/src/context/server-sync.test.ts
  23. 3 2
      packages/app/src/context/server-sync.tsx
  24. 7 1
      packages/app/src/pages/home/home-sessions-controller.tsx
  25. 27 11
      packages/app/src/pages/layout.tsx
  26. 2 1
      packages/app/src/pages/session/timeline/message-timeline.tsx
  27. 15 9
      packages/app/src/utils/server-compat.test.ts
  28. 33 33
      packages/app/src/utils/server-compat.ts
  29. 2 2
      packages/app/src/utils/session-message.test.ts
  30. 6 3
      packages/app/src/utils/session-message.ts
  31. BIN
      packages/app/vendor/opencode-ai-client-1.17.13-v2.tgz
  32. BIN
      packages/app/vendor/opencode-ai-client-1.17.13.tgz
  33. 1 1
      packages/session-ui/package.json

+ 4 - 4
bun.lock

@@ -37,7 +37,7 @@
         "@dnd-kit/helpers": "0.5.0",
         "@dnd-kit/solid": "0.5.0",
         "@kobalte/core": "catalog:",
-        "@opencode-ai/client": "file:vendor/opencode-ai-client-1.17.13.tgz",
+        "@opencode-ai/client": "file:vendor/opencode-ai-client-1.17.13-v2.tgz",
         "@opencode-ai/core": "workspace:*",
         "@opencode-ai/schema": "workspace:*",
         "@opencode-ai/sdk": "workspace:*",
@@ -799,7 +799,7 @@
       "version": "1.18.5",
       "dependencies": {
         "@kobalte/core": "catalog:",
-        "@opencode-ai/client": "file:../app/vendor/opencode-ai-client-1.17.13.tgz",
+        "@opencode-ai/client": "file:../app/vendor/opencode-ai-client-1.17.13-v2.tgz",
         "@opencode-ai/core": "workspace:*",
         "@opencode-ai/sdk": "workspace:*",
         "@opencode-ai/ui": "workspace:*",
@@ -6030,7 +6030,7 @@
 
     "@openauthjs/openauth/jose": ["jose@5.9.6", "", {}, "sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ=="],
 
-    "@opencode-ai/app/@opencode-ai/client": ["@opencode-ai/client@vendor/opencode-ai-client-1.17.13.tgz", {}, "sha512-1cYJikTCrXNhnS2qQ3P3rtdbGqhvJKksswG0amJNqgaeUfz3xDlKEDx+YoIosT7Cqk/AtO18jtip0lYL3TVdHQ=="],
+    "@opencode-ai/app/@opencode-ai/client": ["@opencode-ai/client@vendor/opencode-ai-client-1.17.13-v2.tgz", {}, "sha512-332kgNifvpQOF9e3UA+pIa5xPrMhLaQkUiNiO+meS0Ba9HjSE6hfsWnEojMkD0DPSLqPP6rCF1dDoF7U0Y0OCQ=="],
 
     "@opencode-ai/core/@ai-sdk/openai": ["@ai-sdk/openai@3.0.84", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@ai-sdk/provider-utils": "4.0.38" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cmgbeJL0bbY0yTJH4/AdmP5E7MjWRL9G8UdhIi0JlV/So03o82ORJofW8OzwCZPTORVQblFbpZXYGDcUd9NdUQ=="],
 
@@ -6050,7 +6050,7 @@
 
     "@opencode-ai/script/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],
 
-    "@opencode-ai/session-ui/@opencode-ai/client": ["@opencode-ai/client@../app/vendor/opencode-ai-client-1.17.13.tgz", {}, "sha512-1cYJikTCrXNhnS2qQ3P3rtdbGqhvJKksswG0amJNqgaeUfz3xDlKEDx+YoIosT7Cqk/AtO18jtip0lYL3TVdHQ=="],
+    "@opencode-ai/session-ui/@opencode-ai/client": ["@opencode-ai/client@../app/vendor/opencode-ai-client-1.17.13-v2.tgz", {}, "sha512-332kgNifvpQOF9e3UA+pIa5xPrMhLaQkUiNiO+meS0Ba9HjSE6hfsWnEojMkD0DPSLqPP6rCF1dDoF7U0Y0OCQ=="],
 
     "@opencode-ai/session-ui/@solid-primitives/resize-observer": ["@solid-primitives/resize-observer@2.1.3", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.3", "@solid-primitives/rootless": "^1.5.2", "@solid-primitives/static-store": "^0.1.2", "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-zBLje5E06TgOg93S7rGPldmhDnouNGhvfZVKOp+oG2XU8snA+GoCSSCz1M+jpNAg5Ek2EakU5UVQqL152WmdXQ=="],
 

+ 7 - 9
packages/app/e2e/regression/review-state-persistence.spec.ts

@@ -65,7 +65,7 @@ async function switchSession(page: Page, title: string) {
 
 async function setup(page: Page) {
   await mockOpenCodeServer(page, {
-    protocol: "v2",
+    protocol: "v1",
     directory,
     project: {
       id: projectID,
@@ -93,20 +93,18 @@ async function setup(page: Page) {
     route.fulfill({
       status: 200,
       contentType: "application/json",
-      body: JSON.stringify({ location: { directory }, data: { branch: "feature", defaultBranch: "dev" } }),
+      body: JSON.stringify({ branch: "feature", default_branch: "dev" }),
     }),
   )
   await page.route("**/vcs/diff**", (route) =>
     route.fulfill({
       status: 200,
       contentType: "application/json",
-      body: JSON.stringify({
-        location: { directory },
-        data:
-          new URL(route.request().url()).searchParams.get("mode") === "branch"
-            ? [diff("src/alpha.ts"), diff("src/beta.ts")]
-            : [diff("src/alpha.ts"), diff("src/gamma.ts")],
-      }),
+      body: JSON.stringify(
+        new URL(route.request().url()).searchParams.get("mode") === "branch"
+          ? [diff("src/alpha.ts"), diff("src/beta.ts")]
+          : [diff("src/alpha.ts"), diff("src/gamma.ts")],
+      ),
     }),
   )
   await page.addInitScript(

+ 22 - 24
packages/app/e2e/regression/review-terminal-stacked.spec.ts

@@ -25,7 +25,7 @@ test("keeps the review tree and terminal sized when both panels are open", async
   let detailFailures = 1
   await page.setViewportSize({ width: 1400, height: 900 })
   await mockOpenCodeServer(page, {
-    protocol: "v2",
+    protocol: "v1",
     directory,
     project: {
       id: projectID,
@@ -67,33 +67,31 @@ test("keeps the review tree and terminal sized when both panels are open", async
       status: 200,
       contentType: "application/json",
       body: JSON.stringify({
-        location: { directory },
-        data: { branch: "review-pane-performance", defaultBranch: "dev" },
+        branch: "review-pane-performance",
+        default_branch: "dev",
       }),
     }),
   )
-  await page.route("**/api/vcs/diff**", (route) => {
+  await page.route("**/vcs/diff**", (route) => {
     const url = new URL(route.request().url())
-    const scope = url.searchParams.get("location[directory]")?.replaceAll("\\", "/")
+    const scope = url.searchParams.get("directory")?.replaceAll("\\", "/")
     const detail = scope?.endsWith("/src/branch/d00027")
     if (detail && detailFailures-- > 0) return route.fulfill({ status: 500, body: "retry detail" })
     return route.fulfill({
       status: 200,
       contentType: "application/json",
-      body: JSON.stringify({
-        location: { directory: scope ?? directory, project: { id: projectID, directory } },
-        data:
-          url.searchParams.get("mode") === "branch"
-            ? detail
-              ? branchDiffs
-                  .filter((diff) => diff.file.startsWith("src/branch/d00027/"))
-                  .map((diff) => fileDiff(diff.file, diff.additions, true, detailVersion))
-              : branchDiffs
-            : Array.from({ length: 7 }, (_, index) => fileDiff(`src/git-${index}.ts`, 1)),
-      }),
+      body: JSON.stringify(
+        url.searchParams.get("mode") === "branch"
+          ? detail
+            ? branchDiffs
+                .filter((diff) => diff.file.startsWith("src/branch/d00027/"))
+                .map((diff) => fileDiff(diff.file, diff.additions, true, detailVersion))
+            : branchDiffs
+          : Array.from({ length: 7 }, (_, index) => fileDiff(`src/git-${index}.ts`, 1)),
+      ),
     })
   })
-  await page.route("**/api/pty*", (route) =>
+  await page.route("**/pty*", (route) =>
     route.fulfill({
       status: 200,
       contentType: "application/json",
@@ -111,7 +109,7 @@ test("keeps the review tree and terminal sized when both panels are open", async
       }),
     }),
   )
-  await page.route("**/api/pty/pty_review_terminal*", (route) =>
+  await page.route("**/pty/pty_review_terminal*", (route) =>
     route.fulfill({
       status: 200,
       contentType: "application/json",
@@ -129,7 +127,7 @@ test("keeps the review tree and terminal sized when both panels are open", async
       }),
     }),
   )
-  await page.route("**/api/pty/pty_review_terminal/connect-token*", (route) =>
+  await page.route("**/pty/pty_review_terminal/connect-token*", (route) =>
     route.fulfill({
       status: 200,
       contentType: "application/json",
@@ -139,7 +137,7 @@ test("keeps the review tree and terminal sized when both panels are open", async
       }),
     }),
   )
-  await page.routeWebSocket("**/api/pty/pty_review_terminal/connect", () => undefined)
+  await page.routeWebSocket("**/pty/pty_review_terminal/connect", () => undefined)
   await page.addInitScript(() => {
     localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } }))
     localStorage.setItem(
@@ -177,8 +175,8 @@ test("keeps the review tree and terminal sized when both panels are open", async
   const lazyDiff = page.waitForRequest((request) => {
     const url = new URL(request.url())
     return (
-      url.pathname === "/api/vcs/diff" &&
-      url.searchParams.get("location[directory]")?.replaceAll("\\", "/").endsWith("/src/branch/d00027") === true
+      url.pathname === "/vcs/diff" &&
+      url.searchParams.get("directory")?.replaceAll("\\", "/").endsWith("/src/branch/d00027") === true
     )
   })
   await lastFile.click()
@@ -192,8 +190,8 @@ test("keeps the review tree and terminal sized when both panels are open", async
   const refreshedDiff = page.waitForRequest((request) => {
     const url = new URL(request.url())
     return (
-      url.pathname === "/api/vcs/diff" &&
-      url.searchParams.get("location[directory]")?.replaceAll("\\", "/").endsWith("/src/branch/d00027") === true
+      url.pathname === "/vcs/diff" &&
+      url.searchParams.get("directory")?.replaceAll("\\", "/").endsWith("/src/branch/d00027") === true
     )
   })
   sessionStatus[sessionID] = { type: "idle" }

+ 1 - 1
packages/app/e2e/regression/terminal-tab-switch.spec.ts

@@ -32,7 +32,7 @@ test("keeps the terminal session alive when switching session tabs in a workspac
   const connection = new URL(connections[0]!)
   expect(connection.pathname).toBe(`/api/pty/${ptyID}/connect`)
   expect(connection.searchParams.get("location[directory]")).toBe(directory)
-  expect(connection.searchParams.get("ticket")).toBe("e2e-ticket")
+  expect(connection.searchParams.get("ticket")).toBeNull()
   await writeProbe(page)
 
   await switchTab(page, titleB)

+ 1 - 1
packages/app/package.json

@@ -53,7 +53,7 @@
     "@dnd-kit/helpers": "0.5.0",
     "@dnd-kit/solid": "0.5.0",
     "@kobalte/core": "catalog:",
-    "@opencode-ai/client": "file:vendor/opencode-ai-client-1.17.13.tgz",
+    "@opencode-ai/client": "file:vendor/opencode-ai-client-1.17.13-v2.tgz",
     "@opencode-ai/core": "workspace:*",
     "@opencode-ai/schema": "workspace:*",
     "@opencode-ai/sdk": "workspace:*",

+ 8 - 1
packages/app/src/components/dialog-select-directory-v2.tsx

@@ -8,6 +8,7 @@ import { createEffect, createMemo, createResource, createSignal, For, onCleanup,
 import { useGlobal } from "@/context/global"
 import { useLanguage } from "@/context/language"
 import { ServerConnection } from "@/context/server"
+import type { Path } from "@opencode-ai/sdk/v2/client"
 import {
   absoluteTreePath,
   activeTreeNavigation,
@@ -68,7 +69,13 @@ export function DialogSelectDirectoryV2(props: DialogSelectDirectoryV2Props) {
   const missingBase = createMemo(() => !(sync.data.path.home || sync.data.path.directory))
   const [fallbackPath] = createResource(
     () => (missingBase() ? true : undefined),
-    () => sdk.api.path.get().catch(() => undefined),
+    async (): Promise<Path | undefined> => {
+      if ((await sdk.protocol) !== "v1") return
+      return sdk.client.path
+        .get()
+        .then((result) => result.data)
+        .catch(() => undefined)
+    },
     { initialValue: undefined },
   )
   const home = createMemo(() => sync.data.path.home || fallbackPath()?.home || "")

+ 7 - 2
packages/app/src/components/dialog-select-directory.tsx

@@ -9,6 +9,7 @@ import { useLanguage } from "@/context/language"
 import { ServerConnection } from "@/context/server"
 import { useGlobal } from "@/context/global"
 import { cleanPickerInput, createDirectorySearch, displayPickerPath } from "./directory-picker-domain"
+import type { Path } from "@opencode-ai/sdk/v2/client"
 
 interface DialogSelectDirectoryProps {
   title?: string
@@ -59,8 +60,12 @@ export function DialogSelectDirectory(props: DialogSelectDirectoryProps) {
   const missingBase = createMemo(() => !(sync.data.path.home || sync.data.path.directory))
   const [fallbackPath] = createResource(
     () => (missingBase() ? true : undefined),
-    async () => {
-      return sdk.api.path.get().catch(() => undefined)
+    async (): Promise<Path | undefined> => {
+      if ((await sdk.protocol) !== "v1") return
+      return sdk.client.path
+        .get()
+        .then((result) => result.data)
+        .catch(() => undefined)
     },
     { initialValue: undefined },
   )

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

@@ -71,12 +71,23 @@ export function createEditProjectModel(props: { project: LocalProject; server: S
       const start = store.startup.trim()
 
       if (props.project.id && props.project.id !== "global") {
-        const project = await serverCtx().sdk.api.project.update({
-          projectID: props.project.id,
-          name,
-          icon: { color: store.color || "", override: store.iconOverride || "" },
-          commands: { start },
-        })
+        if ((await serverCtx().sdk.protocol) !== "v1") return
+        const project = await serverCtx()
+          .sdk.client.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
+        // const project = await serverCtx().sdk.api.project.update({
+        //   projectID: props.project.id,
+        //   name,
+        //   icon: { color: store.color || "", override: store.iconOverride || "" },
+        //   commands: { start },
+        // })
         serverCtx().sync.set("project", (items) =>
           items.map((item) => (item.id === project.id ? normalizeProjectInfo(project) : item)),
         )

+ 1 - 1
packages/app/src/components/prompt-input.tsx

@@ -709,7 +709,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
       title: cmd.name,
       description: cmd.description,
       type: "custom" as const,
-      source: cmd.source,
+      // source: cmd.source,
     }))
 
     return [...custom, ...builtin]

+ 2 - 1
packages/app/src/components/settings-general.tsx

@@ -132,7 +132,8 @@ export const SettingsGeneral: Component = () => {
       if ((await sdk.protocol) === "v1") {
         return (await sdk.client.pty.shells()).data ?? []
       }
-      return (await sdk.api.pty.shells()).data
+      // return (await sdk.api.pty.shells()).data
+      return [] as ShellOption[]
     },
     { initialValue: [] as ShellOption[] },
   )

+ 2 - 1
packages/app/src/components/settings-v2/general.tsx

@@ -127,7 +127,8 @@ export const SettingsGeneralV2: Component<{
       if ((await sdk.protocol) === "v1") {
         return (await sdk.client.pty.shells()).data ?? []
       }
-      return (await sdk.api.pty.shells()).data
+      // return (await sdk.api.pty.shells()).data
+      return [] as ShellOption[]
     },
     { initialValue: [] as ShellOption[] },
   )

+ 8 - 14
packages/app/src/components/terminal.tsx

@@ -573,19 +573,13 @@ export const Terminal = (props: TerminalProps) => {
             throw new Error("PTY connect ticket rejected by origin or CSRF checks. Check the server CORS config.")
           throw new Error(`PTY connect ticket failed with ${result.response.status}`)
         }
-        return sdk()
-          .api.pty.connectToken({
-            ptyID: id,
-            location: { directory },
-            "x-opencode-ticket": "1",
-          })
-          .then((result) => result.data.ticket)
-          .catch((err: unknown) => {
-            if (err && typeof err === "object" && "_tag" in err && err._tag === "ForbiddenError") {
-              throw new Error("PTY connect ticket rejected by origin or CSRF checks. Check the server CORS config.")
-            }
-            throw err
-          })
+        // return sdk()
+        //   .api.pty.connectToken({
+        //     ptyID: id,
+        //     location: { directory },
+        //     "x-opencode-ticket": "1",
+        //   })
+        //   .then((result) => result.data.ticket)
       }
 
       const retry = (err: unknown) => {
@@ -616,7 +610,7 @@ export const Terminal = (props: TerminalProps) => {
           return undefined
         })
         const protocol = await sdk().protocol
-        if (protocol === "v2" && !ticket) return
+        // if (protocol === "v2" && !ticket) return
         if (once.value) return
         if (disposed) return
 

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

@@ -134,7 +134,8 @@ export const createDirSyncContext = (
       },
       more: createMemo(() => current()[0].session.length >= current()[0].limit),
       archive: async (sessionID: string) => {
-        await serverSDK.api.session.archive({ sessionID, directory })
+        if ((await serverSDK.protocol) !== "v1") return
+        await serverSDK.client.session.update({ sessionID, directory, time: { archived: Date.now() } })
         current()[1](
           "session",
           produce((draft) => {

+ 5 - 3
packages/app/src/context/global-sync/bootstrap.test.ts

@@ -2,7 +2,7 @@ import { describe, expect, test } from "bun:test"
 import { createStore } from "solid-js/store"
 import { QueryClient } from "@tanstack/solid-query"
 import type { Config, OpencodeClient, Project } from "@opencode-ai/sdk/v2/client"
-import type { AgentApi, CatalogApi, CommandApi, ProjectApi, ReferenceApi } from "@opencode-ai/client/promise"
+import type { AgentApi, CatalogApi, CommandApi, ReferenceApi } from "@opencode-ai/client/promise"
 import type { NormalizedProviderListResponse } from "@opencode-ai/session-ui/context"
 import {
   bootstrapDirectory,
@@ -17,6 +17,8 @@ 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: [] }) },
@@ -200,7 +202,7 @@ describe("query keys", () => {
         calls.push(input)
         return {
           location: {},
-          data: [{ name: "review", template: "Review files", source: "command" as const }],
+          data: [{ name: "review", template: "Review files" /* source: "command" as const */ }],
         }
       },
     } as unknown as CommandApi
@@ -208,7 +210,7 @@ describe("query keys", () => {
     const result = await loadCommands("/repo", api)
 
     expect(calls).toEqual([{ location: { directory: "/repo" } }])
-    expect(result).toEqual([{ name: "review", template: "Review files", source: "command" }])
+    expect(result).toEqual([{ name: "review", template: "Review files" /* source: "command" */ }])
   })
 
   test("loads projects from the current endpoint", async () => {

+ 26 - 21
packages/app/src/context/global-sync/bootstrap.ts

@@ -16,18 +16,12 @@ import type {
   CommandInfo,
   CommandListInput,
   CommandListOutput,
-  McpApi,
-  PathGetInput,
-  PathGetOutput,
-  PermissionApi,
   ProjectCurrentInput,
   ProjectCurrentOutput,
   ProjectListOutput,
-  QuestionApi,
   ReferenceListInput,
   ReferenceListOutput,
   SessionApi,
-  VcsApi,
 } from "@opencode-ai/client/promise"
 import { showToast } from "@/utils/toast"
 import { getFilename } from "@opencode-ai/core/util/path"
@@ -50,6 +44,7 @@ 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 = {
   ready: boolean
@@ -121,9 +116,10 @@ type ProjectApi = {
   readonly current: (input?: ProjectCurrentInput) => Promise<ProjectCurrentOutput>
 }
 
-type PathApi = {
-  readonly get: (input?: PathGetInput) => Promise<PathGetOutput>
-}
+type McpApi = ServerApi["mcp"]
+type PermissionApi = ServerApi["permission"]
+type QuestionApi = ServerApi["question"]
+type VcsApi = ServerApi["vcs"]
 
 export const loadProjectsQuery = (scope: ServerScope, api: ProjectApi) =>
   queryOptions({
@@ -143,7 +139,7 @@ export const loadProjectsQuery = (scope: ServerScope, api: ProjectApi) =>
 
 export async function bootstrapGlobal(input: {
   serverSDK: OpencodeClient
-  serverAPI: CatalogApi & { readonly path: PathApi; readonly project: ProjectApi }
+  serverAPI: CatalogApi & { readonly project: ProjectApi }
   protocol?: Promise<ServerProtocol>
   scope: ServerScope
   requestFailedTitle: string
@@ -158,7 +154,7 @@ export async function bootstrapGlobal(input: {
       input.queryClient.fetchQuery(
         loadProvidersQuery(input.scope, null, input.serverAPI, input.serverSDK, input.protocol),
       ),
-    () => input.queryClient.fetchQuery(loadPathQuery(input.scope, null, input.serverAPI.path)),
+    () => input.queryClient.fetchQuery(loadPathQuery(input.scope, null, input.serverSDK, input.protocol)),
     () =>
       input.queryClient
         .fetchQuery(loadProjectsQuery(input.scope, input.serverAPI.project))
@@ -289,17 +285,26 @@ export const loadCommands = (
           agent: command.agent,
           model: providerID && id ? { providerID, id } : undefined,
           subtask: command.subtask,
-          source: command.source === "skill" ? undefined : command.source,
+          // source: command.source === "skill" ? undefined : command.source,
         }
       })
     }
     return api.list({ location: { directory } }).then((result) => result.data)
   })
 
-export const loadPathQuery = (scope: ServerScope, directory: string | null, api: PathApi) =>
+export const loadPathQuery = (
+  scope: ServerScope,
+  directory: string | null,
+  sdk: OpencodeClient,
+  protocol?: Promise<ServerProtocol>,
+) =>
   queryOptions<Path>({
     queryKey: [scope, directory, "path"],
-    queryFn: () => retry(() => api.get(directory ? { location: { directory } } : undefined)),
+    queryFn: async () => {
+      if ((await protocol) !== "v1")
+        return { state: "", config: "", worktree: "", directory: directory ?? "", home: "" }
+      return retry(() => sdk.path.get({ directory: directory ?? undefined }).then((result) => result.data!))
+    },
   })
 
 export const loadReferencesQuery = (
@@ -328,7 +333,6 @@ export async function bootstrapDirectory(input: {
     readonly agent: AgentListApi
     readonly command: CommandListApi
     readonly mcp: McpApi
-    readonly path: PathApi
     readonly permission: PermissionApi
     readonly project: ProjectApi
     readonly question: QuestionApi
@@ -408,19 +412,20 @@ export async function bootstrapDirectory(input: {
       !seededPath &&
         (() =>
           input.queryClient
-            .ensureQueryData(loadPathQuery(input.scope, input.directory, input.api.path))
+            .ensureQueryData(loadPathQuery(input.scope, input.directory, input.sdk, input.protocol))
             .then((data) => {
               const next = projectID(data.directory ?? input.directory, input.global.project)
               if (next) input.setStore("project", next)
             })),
       () =>
-        retry(() =>
-          input.api.vcs.get({ location: { directory: input.directory } }).then((result) => {
-            const next = { branch: result.data.branch, default_branch: result.data.defaultBranch }
+        retry(async () => {
+          if ((await input.protocol) !== "v1") return
+          return input.sdk.vcs.get().then((result) => {
+            const next = { branch: result.data?.branch, default_branch: result.data?.default_branch }
             input.setStore("vcs", next)
             if (next) input.vcsCache.setStore("value", next)
-          }),
-        ),
+          })
+        }),
       input.mcp &&
         (() =>
           loadCommands(input.directory, input.api.command, input.sdk, input.protocol).then((commands) =>

+ 13 - 13
packages/app/src/context/global-sync/event-reducer.ts

@@ -211,19 +211,19 @@ export function applyDirectoryEvent(input: {
       }))
       break
     }
-    case "session.archived": {
-      const properties = event.properties as { sessionID: string }
-      const result = Binary.search(input.store.session, properties.sessionID, (session) => session.id)
-      if (!result.found) break
-      const info = input.store.session[result.index]
-      input.setStore(
-        "session",
-        produce((draft) => void draft.splice(result.index, 1)),
-      )
-      cleanupSessionCaches(input.setStore, properties.sessionID)
-      if (!info?.parentID) input.setStore("sessionTotal", (value) => Math.max(0, value - 1))
-      break
-    }
+    // case "session.archived": {
+    //   const properties = event.properties as { sessionID: string }
+    //   const result = Binary.search(input.store.session, properties.sessionID, (session) => session.id)
+    //   if (!result.found) break
+    //   const info = input.store.session[result.index]
+    //   input.setStore(
+    //     "session",
+    //     produce((draft) => void draft.splice(result.index, 1)),
+    //   )
+    //   cleanupSessionCaches(input.setStore, properties.sessionID)
+    //   if (!info?.parentID) input.setStore("sessionTotal", (value) => Math.max(0, value - 1))
+    //   break
+    // }
     case "session.moved": {
       const properties = event.properties as {
         sessionID: string

+ 16 - 10
packages/app/src/context/layout.tsx

@@ -570,16 +570,22 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
           continue
         }
 
-        void serverSdk()
-          .api.project.update({ projectID: project.id, icon: { color } })
-          .then((result) =>
-            serverSync().set("project", (items) =>
-              items.map((item) => (item.id === result.id ? normalizeProjectInfo(result) : item)),
-            ),
-          )
-          .catch(() => {
-            if (colorRequested.get(worktree) === color) colorRequested.delete(worktree)
-          })
+        const projectID = project.id
+        void (async () => {
+          const sdk = serverSdk()
+          if ((await sdk.protocol) !== "v1") return
+          return sdk.client.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)),
+              )
+            })
+        })().catch(() => {
+          if (colorRequested.get(worktree) === color) colorRequested.delete(worktree)
+        })
       }
     })
 

+ 1 - 1
packages/app/src/context/server-session-v2-reducer.test.ts

@@ -114,7 +114,7 @@ describe("v2 session reducer", () => {
         sessionID: "ses_1",
         assistantMessageID: "msg_assistant",
         callID: "call_1",
-        structured: {},
+        metadata: {},
         content: [{ type: "text", text: "done" }],
         executed: true,
       },

+ 14 - 7
packages/app/src/context/server-session-v2-reducer.ts

@@ -269,13 +269,18 @@ export function createV2SessionReducer() {
           ...tool,
           executed: event.data.executed,
           providerState: event.data.state,
-          state: { status: "running", input: event.data.input, structured: {}, content: [] },
+          // structured: {}, content: []
+          state: { status: "running", input: event.data.input, metadata: {} },
           time: { ...tool.time, ran: event.created },
         }))
       case "session.tool.progress":
         return updateTool(source, event.data.assistantMessageID, event.data.callID, sessionID, (tool) =>
           tool.state.status === "running"
-            ? { ...tool, state: { ...tool.state, structured: event.data.structured, content: event.data.content } }
+            ? {
+                ...tool,
+                // state: { ...tool.state, structured: event.data.structured, content: event.data.content },
+                state: { ...tool.state, metadata: event.data.metadata },
+              }
             : tool,
         )
       case "session.tool.success":
@@ -288,9 +293,10 @@ export function createV2SessionReducer() {
             state: {
               status: "completed",
               input: tool.state.input,
-              structured: event.data.structured,
+              // structured: event.data.structured,
+              metadata: event.data.metadata,
               content: event.data.content,
-              result: event.data.result,
+              // result: event.data.result,
             },
             time: { ...tool.time, completed: event.created },
           }
@@ -305,10 +311,11 @@ export function createV2SessionReducer() {
             state: {
               status: "error",
               input: typeof tool.state.input === "string" ? {} : tool.state.input,
-              structured: tool.state.status === "running" ? tool.state.structured : {},
-              content: tool.state.status === "running" ? tool.state.content : [],
+              // structured: tool.state.status === "running" ? tool.state.structured : {},
+              metadata: event.data.metadata ?? (tool.state.status === "running" ? tool.state.metadata : {}),
+              content: event.data.content,
               error: event.data.error,
-              result: event.data.result,
+              // result: event.data.result,
             },
             time: { ...tool.time, completed: event.created },
           }

+ 4 - 1
packages/app/src/context/server-session.test.ts

@@ -1,8 +1,11 @@
 import { describe, expect, test } from "bun:test"
 import type { retry } from "@opencode-ai/core/util/retry"
-import type { MessageApi, OpenCodeEvent, SessionApi } from "@opencode-ai/client/promise"
+import type { OpenCodeEvent, SessionApi } from "@opencode-ai/client/promise"
 import type { Message, OpencodeClient, Part, Session } from "@opencode-ai/sdk/v2/client"
 import { createServerSession } from "./server-session"
+import type { ServerApi } from "@/utils/server"
+
+type MessageApi = ServerApi["message"]
 
 const session = (id: string, parentID?: string): Session => ({
   id,

+ 8 - 5
packages/app/src/context/server-session.ts

@@ -1,6 +1,6 @@
 import { Binary } from "@opencode-ai/core/util/binary"
 import { retry } from "@opencode-ai/core/util/retry"
-import type { MessageApi, OpenCodeEvent, SessionApi, SessionMessageInfo } from "@opencode-ai/client/promise"
+import type { OpenCodeEvent, SessionApi, SessionMessageInfo } from "@opencode-ai/client/promise"
 import type {
   Message,
   OpencodeClient,
@@ -21,6 +21,9 @@ import { normalizeSessionInfo } from "@/utils/session"
 import { normalizeSessionMessages } from "@/utils/session-message"
 import { dropSessionCaches, pickSessionCacheEvictions, SESSION_CACHE_LIMIT } from "./global-sync/session-cache"
 import { createV2SessionReducer, type V2SessionReduction } from "./server-session-v2-reducer"
+import type { ServerApi } from "@/utils/server"
+
+type MessageApi = ServerApi["message"]
 
 const cmp = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0)
 const cmpMessage = (a: Message, b: Message) => a.time.created - b.time.created || cmp(a.id, b.id)
@@ -954,10 +957,10 @@ export function createServerSession(
       })
     if (event.type === "session.usage.updated" && info)
       remember({ ...info, cost: event.data.cost, tokens: event.data.tokens })
-    if (event.type === "session.archived") {
-      if (info) remember({ ...info, time: { ...info.time, archived: event.created, updated: event.created } })
-      evict([sessionID])
-    }
+    // if (event.type === "session.archived") {
+    //   if (info) remember({ ...info, time: { ...info.time, archived: event.created, updated: event.created } })
+    //   evict([sessionID])
+    // }
     if (event.type === "session.execution.started") setData("session_status", sessionID, { type: "busy" })
     if (
       event.type === "session.execution.succeeded" ||

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

@@ -1,7 +1,6 @@
 import { describe, expect, test } from "bun:test"
 import type { OpencodeClient } from "@opencode-ai/sdk/v2/client"
 import type {
-  McpApi,
   McpListInput,
   McpResourceCatalogInput,
   SessionApi,
@@ -14,6 +13,9 @@ import { estimateRootSessionTotal, loadRootSessions } from "./global-sync/sessio
 import { loadActiveSessionsQuery, loadMcpQuery, loadMcpResourcesQuery, seedActiveSessionStatuses } from "./server-sync"
 import { ServerScope } from "@/utils/server-scope"
 import { createServerSession } from "./server-session"
+import type { ServerApi } from "@/utils/server"
+
+type McpApi = ServerApi["mcp"]
 
 describe("MCP queries", () => {
   test("loads current servers for the requested location", async () => {

+ 3 - 2
packages/app/src/context/server-sync.tsx

@@ -188,7 +188,8 @@ function makeQueryOptionsApi(
     projects: () => loadProjectsQuery(scope, serverAPI.project),
     providers: (directory: PathKey | null) =>
       loadProvidersQuery(scope, directory, serverAPI, directory ? sdkFor(directory) : serverSDK(), protocol),
-    path: (directory: PathKey | null) => loadPathQuery(scope, directory, serverAPI.path),
+    path: (directory: PathKey | null) =>
+      loadPathQuery(scope, directory, directory ? sdkFor(directory) : serverSDK(), protocol),
     agents: (directory: PathKey) => loadAgentsQuery(scope, directory, serverAPI.agent, sdkFor(directory), protocol),
     references: (directory: PathKey) =>
       loadReferencesQuery(scope, directory, serverAPI.reference, sdkFor(directory), protocol),
@@ -581,7 +582,7 @@ export function createServerSyncContextInner(serverSDK: ServerSDK) {
     children.mark(key)
     if (
       event.current?.type === "session.moved" ||
-      event.current?.type === "session.archived" ||
+      // event.current?.type === "session.archived" ||
       event.current?.type === "session.forked" ||
       eventType === "command.updated" ||
       eventType === "config.updated" ||

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

@@ -211,10 +211,16 @@ export function createHomeSessionsController(home: HomeController) {
         const ctx = home.server.focusedContext()
         if (!conn || !ctx) return
         const [, setStore] = ctx.sync.child(session.directory)
+        if ((await ctx.sdk.protocol) !== "v1") return
         await archiveHomeSession({
           server: ServerConnection.key(conn),
           session,
-          archive: (sessionID) => ctx.sdk.api.session.archive({ sessionID, directory: session.directory }),
+          archive: (sessionID) =>
+            ctx.sdk.client.session.update({
+              sessionID,
+              directory: session.directory,
+              time: { archived: Date.now() },
+            }),
           remove: () =>
             setStore(
               produce((draft) => {

+ 27 - 11
packages/app/src/pages/layout.tsx

@@ -872,12 +872,17 @@ export default function LegacyLayout(props: ParentProps) {
   }
 
   async function archiveSession(session: Session) {
+    if ((await serverSDK().protocol) !== "v1") 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().api.session.archive({ sessionID: session.id, directory: session.directory })
+    await serverSDK().client.session.update({
+      sessionID: session.id,
+      directory: session.directory,
+      time: { archived: Date.now() },
+    })
     setStore(
       produce((draft) => {
         const match = Binary.search(draft.session, session.id, (s) => s.id)
@@ -1296,7 +1301,13 @@ export default function LegacyLayout(props: ParentProps) {
     const name = next === getFilename(project.worktree) ? "" : next
 
     if (project.id && project.id !== "global") {
-      const result = await serverSDK().api.project.update({ projectID: project.id, name })
+      const sdk = serverSDK()
+      if ((await sdk.protocol) !== "v1") return
+      const result = await sdk.client.project
+        .update({ projectID: project.id, directory: project.worktree, name })
+        .then((response) => response.data)
+      if (!result) return
+      // const result = await serverSDK().api.project.update({ projectID: project.id, name })
       serverSync().set("project", (items) =>
         items.map((item) => (item.id === result.id ? normalizeProjectInfo(result) : item)),
       )
@@ -1479,15 +1490,20 @@ export default function LegacyLayout(props: ParentProps) {
       return
     }
 
-    await Promise.all(
-      sessions
-        .filter((session) => session.time.archived === undefined)
-        .map((session) =>
-          serverSDK()
-            .api.session.archive({ sessionID: session.id, directory: session.directory })
-            .catch(() => undefined),
-        ),
-    )
+    if ((await serverSDK().protocol) === "v1")
+      await Promise.all(
+        sessions
+          .filter((session) => session.time.archived === undefined)
+          .map((session) =>
+            serverSDK()
+              .client.session.update({
+                sessionID: session.id,
+                directory: session.directory,
+                time: { archived: Date.now() },
+              })
+              .catch(() => undefined),
+          ),
+      )
 
     setBusy(directory, false)
     dismiss()

+ 2 - 1
packages/app/src/pages/session/timeline/message-timeline.tsx

@@ -812,13 +812,14 @@ export function MessageTimeline(props: {
   const archiveSession = async (sessionID: string) => {
     const session = sync().session.get(sessionID)
     if (!session) return
+    if ((await sdk().protocol) !== "v1") return
 
     const sessions = sync().data.session ?? []
     const index = sessions.findIndex((s) => s.id === sessionID)
     const nextSession = index === -1 ? undefined : (sessions[index + 1] ?? sessions[index - 1])
 
     await sdk()
-      .api.session.archive({ sessionID })
+      .client.session.update({ sessionID, directory: sdk().directory, time: { archived: Date.now() } })
       .then(() => {
         sync().set(
           produce((draft) => {

+ 15 - 9
packages/app/src/utils/server-compat.test.ts

@@ -53,6 +53,7 @@ function setup(
 }
 
 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" })
@@ -63,6 +64,7 @@ describe("createCompatibleApi", () => {
     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")
@@ -127,14 +129,6 @@ describe("createCompatibleApi", () => {
     ])
   })
 
-  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("resolves protocol detection once across implementation methods", async () => {
     let detections = 0
     const resolved = Promise.resolve<"v1" | "v2">("v2")
@@ -147,12 +141,22 @@ describe("createCompatibleApi", () => {
     })
     const { api } = setup(protocol)
 
-    await api.session.archive({ sessionID: "ses_1" })
+    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 })
@@ -160,6 +164,7 @@ describe("createCompatibleApi", () => {
     expect(new URL(requests[0]!.url).pathname).toBe("/experimental/session")
   })
 
+  /*
   test("projects the V1 default branch", async () => {
     const { api } = setup("v1", { vcs: { branch: "feature", default_branch: "dev" } })
 
@@ -167,6 +172,7 @@ describe("createCompatibleApi", () => {
       data: { branch: "feature", defaultBranch: "dev" },
     })
   })
+  */
 
   test("translates current file searches to the V1 dirs parameter", async () => {
     const { api, requests } = setup("v1")

+ 33 - 33
packages/app/src/utils/server-compat.ts

@@ -27,7 +27,7 @@ type CompatibleSessionApi = Omit<
   shell: (input: SessionShellInput & LegacyPrompt) => Promise<SessionShellOutput>
   compact: (input: SessionCompactInput & { model?: LegacyPrompt["model"] }) => Promise<SessionCompactOutput>
   rename: (input: Parameters<SessionApi["rename"]>[0] & LegacyLocation) => ReturnType<SessionApi["rename"]>
-  archive: (input: Parameters<SessionApi["archive"]>[0] & LegacyLocation) => ReturnType<SessionApi["archive"]>
+  // archive: (input: Parameters<SessionApi["archive"]>[0] & LegacyLocation) => ReturnType<SessionApi["archive"]>
   remove: (input: Parameters<SessionApi["remove"]>[0] & LegacyLocation) => ReturnType<SessionApi["remove"]>
 }
 type CompatiblePermissionApi = Omit<ServerApi["permission"], "reply"> & {
@@ -183,9 +183,9 @@ function createV1Api(input: CompatibleInput): CompatibleApi {
       async rename(value: Parameters<ServerApi["session"]["rename"]>[0] & LegacyLocation) {
         await legacy(value).session.update({ sessionID: value.sessionID, title: value.title })
       },
-      async archive(value: Parameters<ServerApi["session"]["archive"]>[0] & LegacyLocation) {
-        await legacy(value).session.update({ sessionID: value.sessionID, time: { archived: Date.now() } })
-      },
+      // async archive(value: Parameters<ServerApi["session"]["archive"]>[0] & LegacyLocation) {
+      //   await legacy(value).session.update({ sessionID: value.sessionID, time: { archived: Date.now() } })
+      // },
       async remove(value: Parameters<ServerApi["session"]["remove"]>[0] & LegacyLocation) {
         await legacy(value).session.delete(value)
       },
@@ -308,34 +308,34 @@ function createV1Api(input: CompatibleInput): CompatibleApi {
         if (!result.data) throw new Error("Project not found")
         return { id: result.data.id, directory: result.data.worktree } satisfies ProjectCurrent
       },
-      async update(value: Parameters<ServerApi["project"]["update"]>[0]) {
-        const project = (await legacy().project.list()).data?.find((item) => item.id === value.projectID)
-        const result = await legacy({ directory: project?.worktree }).project.update({
-          ...value,
-          directory: project?.worktree,
-        })
-        if (!result.data) throw new Error(`Project not found: ${value.projectID}`)
-        return result.data as Project
-      },
+      // async update(value: Parameters<ServerApi["project"]["update"]>[0]) {
+      //   const project = (await legacy().project.list()).data?.find((item) => item.id === value.projectID)
+      //   const result = await legacy({ directory: project?.worktree }).project.update({
+      //     ...value,
+      //     directory: project?.worktree,
+      //   })
+      //   if (!result.data) throw new Error(`Project not found: ${value.projectID}`)
+      //   return result.data as Project
+      // },
       async directories(value: Parameters<ServerApi["project"]["directories"]>[0]) {
         const result = await legacy(value.location).worktree.list()
         return (result.data ?? []).map((item) => ({ directory: item }))
       },
     },
-    path: {
-      ...input.current.path,
-      async get(value?: Parameters<ServerApi["path"]["get"]>[0]) {
-        const result = await legacy(value?.location).path.get()
-        if (!result.data) throw new Error("Path unavailable")
-        return result.data
-      },
-    },
+    // path: {
+    //   ...input.current.path,
+    //   async get(value?: Parameters<ServerApi["path"]["get"]>[0]) {
+    //     const result = await legacy(value?.location).path.get()
+    //     if (!result.data) throw new Error("Path unavailable")
+    //     return result.data
+    //   },
+    // },
     vcs: {
       ...input.current.vcs,
-      async get(value?: Parameters<ServerApi["vcs"]["get"]>[0]) {
-        const result = await legacy(value?.location).vcs.get()
-        return located({ branch: result.data?.branch, defaultBranch: result.data?.default_branch }, value?.location)
-      },
+      // async get(value?: Parameters<ServerApi["vcs"]["get"]>[0]) {
+      //   const result = await legacy(value?.location).vcs.get()
+      //   return located({ branch: result.data?.branch, defaultBranch: result.data?.default_branch }, value?.location)
+      // },
       async status(value?: Parameters<ServerApi["vcs"]["status"]>[0]) {
         const result = await legacy(value?.location).vcs.status()
         return located(result.data ?? [], value?.location)
@@ -451,9 +451,9 @@ function createV1Api(input: CompatibleInput): CompatibleApi {
     },
     pty: {
       ...input.current.pty,
-      async shells(value?: Parameters<ServerApi["pty"]["shells"]>[0]) {
-        return located((await legacy(value?.location).pty.shells()).data ?? [], value?.location)
-      },
+      // async shells(value?: Parameters<ServerApi["pty"]["shells"]>[0]) {
+      //   return located((await legacy(value?.location).pty.shells()).data ?? [], value?.location)
+      // },
       async list(value?: Parameters<ServerApi["pty"]["list"]>[0]) {
         return located((await legacy(value?.location).pty.list()).data ?? [], value?.location)
       },
@@ -485,11 +485,11 @@ function createV1Api(input: CompatibleInput): CompatibleApi {
       async remove(value: Parameters<ServerApi["pty"]["remove"]>[0]) {
         await legacy(value.location).pty.remove({ ptyID: value.ptyID })
       },
-      async connectToken(value: Parameters<ServerApi["pty"]["connectToken"]>[0]) {
-        const result = await legacy(value.location).pty.connectToken({ ptyID: value.ptyID })
-        if (!result.data) throw new Error(`Failed to connect terminal: ${value.ptyID}`)
-        return located(result.data, value.location)
-      },
+      // async connectToken(value: Parameters<ServerApi["pty"]["connectToken"]>[0]) {
+      //   const result = await legacy(value.location).pty.connectToken({ ptyID: value.ptyID })
+      //   if (!result.data) throw new Error(`Failed to connect terminal: ${value.ptyID}`)
+      //   return located(result.data, value.location)
+      // },
     },
     permission: {
       ...input.current.permission,

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

@@ -49,7 +49,7 @@ describe("normalizeSessionMessages", () => {
             state: {
               status: "completed",
               input: { filePath: "note.txt" },
-              structured: { title: "note.txt" },
+              metadata: { title: "note.txt" },
               content: [{ type: "text", text: "hello" }],
             },
             time: { created: 5, ran: 6, completed: 7 },
@@ -170,7 +170,7 @@ describe("normalizeSessionMessages", () => {
               status: "completed",
               input: { path: "/repo/README.md", oldString: "old", newString: "new" },
               content: [{ type: "text", text: "Edited file successfully" }],
-              structured: {
+              metadata: {
                 files: [
                   {
                     file: "README.md",

+ 6 - 3
packages/app/src/utils/session-message.ts

@@ -304,7 +304,8 @@ function toolPart(sessionID: string, messageID: string, tool: SessionMessageAssi
       return {
         status: "running" as const,
         input: normalizeToolInput(tool.name, tool.state.input),
-        metadata: normalizeToolMetadata(tool.name, tool.state.structured),
+        // metadata: normalizeToolMetadata(tool.name, tool.state.structured),
+        metadata: normalizeToolMetadata(tool.name, tool.state.metadata ?? {}),
         time: { start },
       }
     }
@@ -313,7 +314,8 @@ function toolPart(sessionID: string, messageID: string, tool: SessionMessageAssi
         status: "error" as const,
         input: normalizeToolInput(tool.name, tool.state.input),
         error: tool.state.error.message,
-        metadata: normalizeToolMetadata(tool.name, tool.state.structured),
+        // metadata: normalizeToolMetadata(tool.name, tool.state.structured),
+        metadata: normalizeToolMetadata(tool.name, tool.state.metadata ?? {}),
         time: { start, end: tool.time.completed ?? start },
       }
     }
@@ -337,7 +339,8 @@ function toolPart(sessionID: string, messageID: string, tool: SessionMessageAssi
       input: normalizeToolInput(tool.name, tool.state.input),
       output: tool.state.content.flatMap((item) => (item.type === "text" ? [item.text] : [])).join("\n"),
       title: tool.name,
-      metadata: normalizeToolMetadata(tool.name, tool.state.structured),
+      // metadata: normalizeToolMetadata(tool.name, tool.state.structured),
+      metadata: normalizeToolMetadata(tool.name, tool.state.metadata ?? {}),
       time: { start, end: tool.time.completed ?? start },
       attachments: attachments.length ? attachments : undefined,
     }

BIN
packages/app/vendor/opencode-ai-client-1.17.13-v2.tgz


BIN
packages/app/vendor/opencode-ai-client-1.17.13.tgz


+ 1 - 1
packages/session-ui/package.json

@@ -39,7 +39,7 @@
   },
   "dependencies": {
     "@kobalte/core": "catalog:",
-    "@opencode-ai/client": "file:../app/vendor/opencode-ai-client-1.17.13.tgz",
+    "@opencode-ai/client": "file:../app/vendor/opencode-ai-client-1.17.13-v2.tgz",
     "@opencode-ai/core": "workspace:*",
     "@opencode-ai/sdk": "workspace:*",
     "@opencode-ai/ui": "workspace:*",