Jelajahi Sumber

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

Brendan Allan 1 bulan lalu
induk
melakukan
0a6637e17a
33 mengubah file dengan 268 tambahan dan 191 penghapusan
  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. TEMPAT SAMPAH
      packages/app/vendor/opencode-ai-client-1.17.13-v2.tgz
  32. TEMPAT SAMPAH
      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/helpers": "0.5.0",
         "@dnd-kit/solid": "0.5.0",
         "@dnd-kit/solid": "0.5.0",
         "@kobalte/core": "catalog:",
         "@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/core": "workspace:*",
         "@opencode-ai/schema": "workspace:*",
         "@opencode-ai/schema": "workspace:*",
         "@opencode-ai/sdk": "workspace:*",
         "@opencode-ai/sdk": "workspace:*",
@@ -799,7 +799,7 @@
       "version": "1.18.5",
       "version": "1.18.5",
       "dependencies": {
       "dependencies": {
         "@kobalte/core": "catalog:",
         "@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/core": "workspace:*",
         "@opencode-ai/sdk": "workspace:*",
         "@opencode-ai/sdk": "workspace:*",
         "@opencode-ai/ui": "workspace:*",
         "@opencode-ai/ui": "workspace:*",
@@ -6030,7 +6030,7 @@
 
 
     "@openauthjs/openauth/jose": ["jose@5.9.6", "", {}, "sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ=="],
     "@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=="],
     "@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/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=="],
     "@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) {
 async function setup(page: Page) {
   await mockOpenCodeServer(page, {
   await mockOpenCodeServer(page, {
-    protocol: "v2",
+    protocol: "v1",
     directory,
     directory,
     project: {
     project: {
       id: projectID,
       id: projectID,
@@ -93,20 +93,18 @@ async function setup(page: Page) {
     route.fulfill({
     route.fulfill({
       status: 200,
       status: 200,
       contentType: "application/json",
       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) =>
   await page.route("**/vcs/diff**", (route) =>
     route.fulfill({
     route.fulfill({
       status: 200,
       status: 200,
       contentType: "application/json",
       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(
   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
   let detailFailures = 1
   await page.setViewportSize({ width: 1400, height: 900 })
   await page.setViewportSize({ width: 1400, height: 900 })
   await mockOpenCodeServer(page, {
   await mockOpenCodeServer(page, {
-    protocol: "v2",
+    protocol: "v1",
     directory,
     directory,
     project: {
     project: {
       id: projectID,
       id: projectID,
@@ -67,33 +67,31 @@ test("keeps the review tree and terminal sized when both panels are open", async
       status: 200,
       status: 200,
       contentType: "application/json",
       contentType: "application/json",
       body: JSON.stringify({
       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 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")
     const detail = scope?.endsWith("/src/branch/d00027")
     if (detail && detailFailures-- > 0) return route.fulfill({ status: 500, body: "retry detail" })
     if (detail && detailFailures-- > 0) return route.fulfill({ status: 500, body: "retry detail" })
     return route.fulfill({
     return route.fulfill({
       status: 200,
       status: 200,
       contentType: "application/json",
       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({
     route.fulfill({
       status: 200,
       status: 200,
       contentType: "application/json",
       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({
     route.fulfill({
       status: 200,
       status: 200,
       contentType: "application/json",
       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({
     route.fulfill({
       status: 200,
       status: 200,
       contentType: "application/json",
       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(() => {
   await page.addInitScript(() => {
     localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } }))
     localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } }))
     localStorage.setItem(
     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 lazyDiff = page.waitForRequest((request) => {
     const url = new URL(request.url())
     const url = new URL(request.url())
     return (
     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()
   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 refreshedDiff = page.waitForRequest((request) => {
     const url = new URL(request.url())
     const url = new URL(request.url())
     return (
     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" }
   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]!)
   const connection = new URL(connections[0]!)
   expect(connection.pathname).toBe(`/api/pty/${ptyID}/connect`)
   expect(connection.pathname).toBe(`/api/pty/${ptyID}/connect`)
   expect(connection.searchParams.get("location[directory]")).toBe(directory)
   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 writeProbe(page)
 
 
   await switchTab(page, titleB)
   await switchTab(page, titleB)

+ 1 - 1
packages/app/package.json

@@ -53,7 +53,7 @@
     "@dnd-kit/helpers": "0.5.0",
     "@dnd-kit/helpers": "0.5.0",
     "@dnd-kit/solid": "0.5.0",
     "@dnd-kit/solid": "0.5.0",
     "@kobalte/core": "catalog:",
     "@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/core": "workspace:*",
     "@opencode-ai/schema": "workspace:*",
     "@opencode-ai/schema": "workspace:*",
     "@opencode-ai/sdk": "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 { useGlobal } from "@/context/global"
 import { useLanguage } from "@/context/language"
 import { useLanguage } from "@/context/language"
 import { ServerConnection } from "@/context/server"
 import { ServerConnection } from "@/context/server"
+import type { Path } from "@opencode-ai/sdk/v2/client"
 import {
 import {
   absoluteTreePath,
   absoluteTreePath,
   activeTreeNavigation,
   activeTreeNavigation,
@@ -68,7 +69,13 @@ export function DialogSelectDirectoryV2(props: DialogSelectDirectoryV2Props) {
   const missingBase = createMemo(() => !(sync.data.path.home || sync.data.path.directory))
   const missingBase = createMemo(() => !(sync.data.path.home || sync.data.path.directory))
   const [fallbackPath] = createResource(
   const [fallbackPath] = createResource(
     () => (missingBase() ? true : undefined),
     () => (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 },
     { initialValue: undefined },
   )
   )
   const home = createMemo(() => sync.data.path.home || fallbackPath()?.home || "")
   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 { ServerConnection } from "@/context/server"
 import { useGlobal } from "@/context/global"
 import { useGlobal } from "@/context/global"
 import { cleanPickerInput, createDirectorySearch, displayPickerPath } from "./directory-picker-domain"
 import { cleanPickerInput, createDirectorySearch, displayPickerPath } from "./directory-picker-domain"
+import type { Path } from "@opencode-ai/sdk/v2/client"
 
 
 interface DialogSelectDirectoryProps {
 interface DialogSelectDirectoryProps {
   title?: string
   title?: string
@@ -59,8 +60,12 @@ export function DialogSelectDirectory(props: DialogSelectDirectoryProps) {
   const missingBase = createMemo(() => !(sync.data.path.home || sync.data.path.directory))
   const missingBase = createMemo(() => !(sync.data.path.home || sync.data.path.directory))
   const [fallbackPath] = createResource(
   const [fallbackPath] = createResource(
     () => (missingBase() ? true : undefined),
     () => (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 },
     { 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()
       const start = store.startup.trim()
 
 
       if (props.project.id && props.project.id !== "global") {
       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) =>
         serverCtx().sync.set("project", (items) =>
           items.map((item) => (item.id === project.id ? normalizeProjectInfo(project) : item)),
           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,
       title: cmd.name,
       description: cmd.description,
       description: cmd.description,
       type: "custom" as const,
       type: "custom" as const,
-      source: cmd.source,
+      // source: cmd.source,
     }))
     }))
 
 
     return [...custom, ...builtin]
     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") {
       if ((await sdk.protocol) === "v1") {
         return (await sdk.client.pty.shells()).data ?? []
         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[] },
     { 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") {
       if ((await sdk.protocol) === "v1") {
         return (await sdk.client.pty.shells()).data ?? []
         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[] },
     { 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 rejected by origin or CSRF checks. Check the server CORS config.")
           throw new Error(`PTY connect ticket failed with ${result.response.status}`)
           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) => {
       const retry = (err: unknown) => {
@@ -616,7 +610,7 @@ export const Terminal = (props: TerminalProps) => {
           return undefined
           return undefined
         })
         })
         const protocol = await sdk().protocol
         const protocol = await sdk().protocol
-        if (protocol === "v2" && !ticket) return
+        // if (protocol === "v2" && !ticket) return
         if (once.value) return
         if (once.value) return
         if (disposed) 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),
       more: createMemo(() => current()[0].session.length >= current()[0].limit),
       archive: async (sessionID: string) => {
       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](
         current()[1](
           "session",
           "session",
           produce((draft) => {
           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 { createStore } from "solid-js/store"
 import { QueryClient } from "@tanstack/solid-query"
 import { QueryClient } from "@tanstack/solid-query"
 import type { Config, OpencodeClient, Project } from "@opencode-ai/sdk/v2/client"
 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 type { NormalizedProviderListResponse } from "@opencode-ai/session-ui/context"
 import {
 import {
   bootstrapDirectory,
   bootstrapDirectory,
@@ -17,6 +17,8 @@ import type { State, VcsCache } from "./types"
 import { ServerScope } from "@/utils/server-scope"
 import { ServerScope } from "@/utils/server-scope"
 import type { ServerApi } from "@/utils/server"
 import type { ServerApi } from "@/utils/server"
 
 
+type ProjectApi = ServerApi["project"]
+
 const provider = { all: new Map(), connected: [], default: {} } satisfies NormalizedProviderListResponse
 const provider = { all: new Map(), connected: [], default: {} } satisfies NormalizedProviderListResponse
 const api = {
 const api = {
   agent: { list: async () => ({ location: {}, data: [] }) },
   agent: { list: async () => ({ location: {}, data: [] }) },
@@ -200,7 +202,7 @@ describe("query keys", () => {
         calls.push(input)
         calls.push(input)
         return {
         return {
           location: {},
           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
     } as unknown as CommandApi
@@ -208,7 +210,7 @@ describe("query keys", () => {
     const result = await loadCommands("/repo", api)
     const result = await loadCommands("/repo", api)
 
 
     expect(calls).toEqual([{ location: { directory: "/repo" } }])
     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 () => {
   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,
   CommandInfo,
   CommandListInput,
   CommandListInput,
   CommandListOutput,
   CommandListOutput,
-  McpApi,
-  PathGetInput,
-  PathGetOutput,
-  PermissionApi,
   ProjectCurrentInput,
   ProjectCurrentInput,
   ProjectCurrentOutput,
   ProjectCurrentOutput,
   ProjectListOutput,
   ProjectListOutput,
-  QuestionApi,
   ReferenceListInput,
   ReferenceListInput,
   ReferenceListOutput,
   ReferenceListOutput,
   SessionApi,
   SessionApi,
-  VcsApi,
 } from "@opencode-ai/client/promise"
 } from "@opencode-ai/client/promise"
 import { showToast } from "@/utils/toast"
 import { showToast } from "@/utils/toast"
 import { getFilename } from "@opencode-ai/core/util/path"
 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 { ScopedKey, type ServerScope } from "@/utils/server-scope"
 import { normalizeSessionInfo } from "@/utils/session"
 import { normalizeSessionInfo } from "@/utils/session"
 import type { ServerProtocol } from "@/utils/server-protocol"
 import type { ServerProtocol } from "@/utils/server-protocol"
+import type { ServerApi } from "@/utils/server"
 
 
 type GlobalStore = {
 type GlobalStore = {
   ready: boolean
   ready: boolean
@@ -121,9 +116,10 @@ type ProjectApi = {
   readonly current: (input?: ProjectCurrentInput) => Promise<ProjectCurrentOutput>
   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) =>
 export const loadProjectsQuery = (scope: ServerScope, api: ProjectApi) =>
   queryOptions({
   queryOptions({
@@ -143,7 +139,7 @@ export const loadProjectsQuery = (scope: ServerScope, api: ProjectApi) =>
 
 
 export async function bootstrapGlobal(input: {
 export async function bootstrapGlobal(input: {
   serverSDK: OpencodeClient
   serverSDK: OpencodeClient
-  serverAPI: CatalogApi & { readonly path: PathApi; readonly project: ProjectApi }
+  serverAPI: CatalogApi & { readonly project: ProjectApi }
   protocol?: Promise<ServerProtocol>
   protocol?: Promise<ServerProtocol>
   scope: ServerScope
   scope: ServerScope
   requestFailedTitle: string
   requestFailedTitle: string
@@ -158,7 +154,7 @@ export async function bootstrapGlobal(input: {
       input.queryClient.fetchQuery(
       input.queryClient.fetchQuery(
         loadProvidersQuery(input.scope, null, input.serverAPI, input.serverSDK, input.protocol),
         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
       input.queryClient
         .fetchQuery(loadProjectsQuery(input.scope, input.serverAPI.project))
         .fetchQuery(loadProjectsQuery(input.scope, input.serverAPI.project))
@@ -289,17 +285,26 @@ export const loadCommands = (
           agent: command.agent,
           agent: command.agent,
           model: providerID && id ? { providerID, id } : undefined,
           model: providerID && id ? { providerID, id } : undefined,
           subtask: command.subtask,
           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)
     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>({
   queryOptions<Path>({
     queryKey: [scope, directory, "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 = (
 export const loadReferencesQuery = (
@@ -328,7 +333,6 @@ export async function bootstrapDirectory(input: {
     readonly agent: AgentListApi
     readonly agent: AgentListApi
     readonly command: CommandListApi
     readonly command: CommandListApi
     readonly mcp: McpApi
     readonly mcp: McpApi
-    readonly path: PathApi
     readonly permission: PermissionApi
     readonly permission: PermissionApi
     readonly project: ProjectApi
     readonly project: ProjectApi
     readonly question: QuestionApi
     readonly question: QuestionApi
@@ -408,19 +412,20 @@ export async function bootstrapDirectory(input: {
       !seededPath &&
       !seededPath &&
         (() =>
         (() =>
           input.queryClient
           input.queryClient
-            .ensureQueryData(loadPathQuery(input.scope, input.directory, input.api.path))
+            .ensureQueryData(loadPathQuery(input.scope, input.directory, input.sdk, input.protocol))
             .then((data) => {
             .then((data) => {
               const next = projectID(data.directory ?? input.directory, input.global.project)
               const next = projectID(data.directory ?? input.directory, input.global.project)
               if (next) input.setStore("project", next)
               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)
             input.setStore("vcs", next)
             if (next) input.vcsCache.setStore("value", next)
             if (next) input.vcsCache.setStore("value", next)
-          }),
-        ),
+          })
+        }),
       input.mcp &&
       input.mcp &&
         (() =>
         (() =>
           loadCommands(input.directory, input.api.command, input.sdk, input.protocol).then((commands) =>
           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
       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": {
     case "session.moved": {
       const properties = event.properties as {
       const properties = event.properties as {
         sessionID: string
         sessionID: string

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

@@ -570,16 +570,22 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
           continue
           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",
         sessionID: "ses_1",
         assistantMessageID: "msg_assistant",
         assistantMessageID: "msg_assistant",
         callID: "call_1",
         callID: "call_1",
-        structured: {},
+        metadata: {},
         content: [{ type: "text", text: "done" }],
         content: [{ type: "text", text: "done" }],
         executed: true,
         executed: true,
       },
       },

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

@@ -269,13 +269,18 @@ export function createV2SessionReducer() {
           ...tool,
           ...tool,
           executed: event.data.executed,
           executed: event.data.executed,
           providerState: event.data.state,
           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 },
           time: { ...tool.time, ran: event.created },
         }))
         }))
       case "session.tool.progress":
       case "session.tool.progress":
         return updateTool(source, event.data.assistantMessageID, event.data.callID, sessionID, (tool) =>
         return updateTool(source, event.data.assistantMessageID, event.data.callID, sessionID, (tool) =>
           tool.state.status === "running"
           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,
             : tool,
         )
         )
       case "session.tool.success":
       case "session.tool.success":
@@ -288,9 +293,10 @@ export function createV2SessionReducer() {
             state: {
             state: {
               status: "completed",
               status: "completed",
               input: tool.state.input,
               input: tool.state.input,
-              structured: event.data.structured,
+              // structured: event.data.structured,
+              metadata: event.data.metadata,
               content: event.data.content,
               content: event.data.content,
-              result: event.data.result,
+              // result: event.data.result,
             },
             },
             time: { ...tool.time, completed: event.created },
             time: { ...tool.time, completed: event.created },
           }
           }
@@ -305,10 +311,11 @@ export function createV2SessionReducer() {
             state: {
             state: {
               status: "error",
               status: "error",
               input: typeof tool.state.input === "string" ? {} : tool.state.input,
               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,
               error: event.data.error,
-              result: event.data.result,
+              // result: event.data.result,
             },
             },
             time: { ...tool.time, completed: event.created },
             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 { describe, expect, test } from "bun:test"
 import type { retry } from "@opencode-ai/core/util/retry"
 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 type { Message, OpencodeClient, Part, Session } from "@opencode-ai/sdk/v2/client"
 import { createServerSession } from "./server-session"
 import { createServerSession } from "./server-session"
+import type { ServerApi } from "@/utils/server"
+
+type MessageApi = ServerApi["message"]
 
 
 const session = (id: string, parentID?: string): Session => ({
 const session = (id: string, parentID?: string): Session => ({
   id,
   id,

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

@@ -1,6 +1,6 @@
 import { Binary } from "@opencode-ai/core/util/binary"
 import { Binary } from "@opencode-ai/core/util/binary"
 import { retry } from "@opencode-ai/core/util/retry"
 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 {
 import type {
   Message,
   Message,
   OpencodeClient,
   OpencodeClient,
@@ -21,6 +21,9 @@ import { normalizeSessionInfo } from "@/utils/session"
 import { normalizeSessionMessages } from "@/utils/session-message"
 import { normalizeSessionMessages } from "@/utils/session-message"
 import { dropSessionCaches, pickSessionCacheEvictions, SESSION_CACHE_LIMIT } from "./global-sync/session-cache"
 import { dropSessionCaches, pickSessionCacheEvictions, SESSION_CACHE_LIMIT } from "./global-sync/session-cache"
 import { createV2SessionReducer, type V2SessionReduction } from "./server-session-v2-reducer"
 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 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)
 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)
     if (event.type === "session.usage.updated" && info)
       remember({ ...info, cost: event.data.cost, tokens: event.data.tokens })
       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.started") setData("session_status", sessionID, { type: "busy" })
     if (
     if (
       event.type === "session.execution.succeeded" ||
       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 { describe, expect, test } from "bun:test"
 import type { OpencodeClient } from "@opencode-ai/sdk/v2/client"
 import type { OpencodeClient } from "@opencode-ai/sdk/v2/client"
 import type {
 import type {
-  McpApi,
   McpListInput,
   McpListInput,
   McpResourceCatalogInput,
   McpResourceCatalogInput,
   SessionApi,
   SessionApi,
@@ -14,6 +13,9 @@ import { estimateRootSessionTotal, loadRootSessions } from "./global-sync/sessio
 import { loadActiveSessionsQuery, loadMcpQuery, loadMcpResourcesQuery, seedActiveSessionStatuses } from "./server-sync"
 import { loadActiveSessionsQuery, loadMcpQuery, loadMcpResourcesQuery, seedActiveSessionStatuses } from "./server-sync"
 import { ServerScope } from "@/utils/server-scope"
 import { ServerScope } from "@/utils/server-scope"
 import { createServerSession } from "./server-session"
 import { createServerSession } from "./server-session"
+import type { ServerApi } from "@/utils/server"
+
+type McpApi = ServerApi["mcp"]
 
 
 describe("MCP queries", () => {
 describe("MCP queries", () => {
   test("loads current servers for the requested location", async () => {
   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),
     projects: () => loadProjectsQuery(scope, serverAPI.project),
     providers: (directory: PathKey | null) =>
     providers: (directory: PathKey | null) =>
       loadProvidersQuery(scope, directory, serverAPI, directory ? sdkFor(directory) : serverSDK(), protocol),
       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),
     agents: (directory: PathKey) => loadAgentsQuery(scope, directory, serverAPI.agent, sdkFor(directory), protocol),
     references: (directory: PathKey) =>
     references: (directory: PathKey) =>
       loadReferencesQuery(scope, directory, serverAPI.reference, sdkFor(directory), protocol),
       loadReferencesQuery(scope, directory, serverAPI.reference, sdkFor(directory), protocol),
@@ -581,7 +582,7 @@ export function createServerSyncContextInner(serverSDK: ServerSDK) {
     children.mark(key)
     children.mark(key)
     if (
     if (
       event.current?.type === "session.moved" ||
       event.current?.type === "session.moved" ||
-      event.current?.type === "session.archived" ||
+      // event.current?.type === "session.archived" ||
       event.current?.type === "session.forked" ||
       event.current?.type === "session.forked" ||
       eventType === "command.updated" ||
       eventType === "command.updated" ||
       eventType === "config.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()
         const ctx = home.server.focusedContext()
         if (!conn || !ctx) return
         if (!conn || !ctx) return
         const [, setStore] = ctx.sync.child(session.directory)
         const [, setStore] = ctx.sync.child(session.directory)
+        if ((await ctx.sdk.protocol) !== "v1") return
         await archiveHomeSession({
         await archiveHomeSession({
           server: ServerConnection.key(conn),
           server: ServerConnection.key(conn),
           session,
           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: () =>
           remove: () =>
             setStore(
             setStore(
               produce((draft) => {
               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) {
   async function archiveSession(session: Session) {
+    if ((await serverSDK().protocol) !== "v1") return
     const [store, setStore] = serverSync().child(session.directory)
     const [store, setStore] = serverSync().child(session.directory)
     const sessions = store.session ?? []
     const sessions = store.session ?? []
     const index = sessions.findIndex((s) => s.id === session.id)
     const index = sessions.findIndex((s) => s.id === session.id)
     const nextSession = sessions[index + 1] ?? sessions[index - 1]
     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(
     setStore(
       produce((draft) => {
       produce((draft) => {
         const match = Binary.search(draft.session, session.id, (s) => s.id)
         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
     const name = next === getFilename(project.worktree) ? "" : next
 
 
     if (project.id && project.id !== "global") {
     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) =>
       serverSync().set("project", (items) =>
         items.map((item) => (item.id === result.id ? normalizeProjectInfo(result) : item)),
         items.map((item) => (item.id === result.id ? normalizeProjectInfo(result) : item)),
       )
       )
@@ -1479,15 +1490,20 @@ export default function LegacyLayout(props: ParentProps) {
       return
       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)
     setBusy(directory, false)
     dismiss()
     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 archiveSession = async (sessionID: string) => {
     const session = sync().session.get(sessionID)
     const session = sync().session.get(sessionID)
     if (!session) return
     if (!session) return
+    if ((await sdk().protocol) !== "v1") return
 
 
     const sessions = sync().data.session ?? []
     const sessions = sync().data.session ?? []
     const index = sessions.findIndex((s) => s.id === sessionID)
     const index = sessions.findIndex((s) => s.id === sessionID)
     const nextSession = index === -1 ? undefined : (sessions[index + 1] ?? sessions[index - 1])
     const nextSession = index === -1 ? undefined : (sessions[index + 1] ?? sessions[index - 1])
 
 
     await sdk()
     await sdk()
-      .api.session.archive({ sessionID })
+      .client.session.update({ sessionID, directory: sdk().directory, time: { archived: Date.now() } })
       .then(() => {
       .then(() => {
         sync().set(
         sync().set(
           produce((draft) => {
           produce((draft) => {

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

@@ -53,6 +53,7 @@ function setup(
 }
 }
 
 
 describe("createCompatibleApi", () => {
 describe("createCompatibleApi", () => {
+  /*
   test("routes V1 archive through the legacy session update", async () => {
   test("routes V1 archive through the legacy session update", async () => {
     const { api, requests } = setup("v1")
     const { api, requests } = setup("v1")
     await api.session.archive({ sessionID: "ses_1", directory: "/repo" })
     await api.session.archive({ sessionID: "ses_1", directory: "/repo" })
@@ -63,6 +64,7 @@ describe("createCompatibleApi", () => {
     expect(requests[0]!.method).toBe("PATCH")
     expect(requests[0]!.method).toBe("PATCH")
     expect(await requests[0]!.json()).toMatchObject({ time: { archived: expect.any(Number) } })
     expect(await requests[0]!.json()).toMatchObject({ time: { archived: expect.any(Number) } })
   })
   })
+  */
 
 
   test("converts current prompts to the V1 prompt contract", async () => {
   test("converts current prompts to the V1 prompt contract", async () => {
     const { api, requests } = setup("v1")
     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 () => {
   test("resolves protocol detection once across implementation methods", async () => {
     let detections = 0
     let detections = 0
     const resolved = Promise.resolve<"v1" | "v2">("v2")
     const resolved = Promise.resolve<"v1" | "v2">("v2")
@@ -147,12 +141,22 @@ describe("createCompatibleApi", () => {
     })
     })
     const { api } = setup(protocol)
     const { api } = setup(protocol)
 
 
-    await api.session.archive({ sessionID: "ses_1" })
+    await api.session.list()
     await api.session.list()
     await api.session.list()
 
 
     expect(detections).toBe(1)
     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 () => {
   test("uses the global V1 session search endpoint", async () => {
     const { api, requests } = setup("v1")
     const { api, requests } = setup("v1")
     await api.session.list({ parentID: null, search: "session", limit: 50 })
     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")
     expect(new URL(requests[0]!.url).pathname).toBe("/experimental/session")
   })
   })
 
 
+  /*
   test("projects the V1 default branch", async () => {
   test("projects the V1 default branch", async () => {
     const { api } = setup("v1", { vcs: { branch: "feature", default_branch: "dev" } })
     const { api } = setup("v1", { vcs: { branch: "feature", default_branch: "dev" } })
 
 
@@ -167,6 +172,7 @@ describe("createCompatibleApi", () => {
       data: { branch: "feature", defaultBranch: "dev" },
       data: { branch: "feature", defaultBranch: "dev" },
     })
     })
   })
   })
+  */
 
 
   test("translates current file searches to the V1 dirs parameter", async () => {
   test("translates current file searches to the V1 dirs parameter", async () => {
     const { api, requests } = setup("v1")
     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>
   shell: (input: SessionShellInput & LegacyPrompt) => Promise<SessionShellOutput>
   compact: (input: SessionCompactInput & { model?: LegacyPrompt["model"] }) => Promise<SessionCompactOutput>
   compact: (input: SessionCompactInput & { model?: LegacyPrompt["model"] }) => Promise<SessionCompactOutput>
   rename: (input: Parameters<SessionApi["rename"]>[0] & LegacyLocation) => ReturnType<SessionApi["rename"]>
   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"]>
   remove: (input: Parameters<SessionApi["remove"]>[0] & LegacyLocation) => ReturnType<SessionApi["remove"]>
 }
 }
 type CompatiblePermissionApi = Omit<ServerApi["permission"], "reply"> & {
 type CompatiblePermissionApi = Omit<ServerApi["permission"], "reply"> & {
@@ -183,9 +183,9 @@ function createV1Api(input: CompatibleInput): CompatibleApi {
       async rename(value: Parameters<ServerApi["session"]["rename"]>[0] & LegacyLocation) {
       async rename(value: Parameters<ServerApi["session"]["rename"]>[0] & LegacyLocation) {
         await legacy(value).session.update({ sessionID: value.sessionID, title: value.title })
         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) {
       async remove(value: Parameters<ServerApi["session"]["remove"]>[0] & LegacyLocation) {
         await legacy(value).session.delete(value)
         await legacy(value).session.delete(value)
       },
       },
@@ -308,34 +308,34 @@ function createV1Api(input: CompatibleInput): CompatibleApi {
         if (!result.data) throw new Error("Project not found")
         if (!result.data) throw new Error("Project not found")
         return { id: result.data.id, directory: result.data.worktree } satisfies ProjectCurrent
         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]) {
       async directories(value: Parameters<ServerApi["project"]["directories"]>[0]) {
         const result = await legacy(value.location).worktree.list()
         const result = await legacy(value.location).worktree.list()
         return (result.data ?? []).map((item) => ({ directory: item }))
         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: {
     vcs: {
       ...input.current.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]) {
       async status(value?: Parameters<ServerApi["vcs"]["status"]>[0]) {
         const result = await legacy(value?.location).vcs.status()
         const result = await legacy(value?.location).vcs.status()
         return located(result.data ?? [], value?.location)
         return located(result.data ?? [], value?.location)
@@ -451,9 +451,9 @@ function createV1Api(input: CompatibleInput): CompatibleApi {
     },
     },
     pty: {
     pty: {
       ...input.current.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]) {
       async list(value?: Parameters<ServerApi["pty"]["list"]>[0]) {
         return located((await legacy(value?.location).pty.list()).data ?? [], value?.location)
         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]) {
       async remove(value: Parameters<ServerApi["pty"]["remove"]>[0]) {
         await legacy(value.location).pty.remove({ ptyID: value.ptyID })
         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: {
     permission: {
       ...input.current.permission,
       ...input.current.permission,

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

@@ -49,7 +49,7 @@ describe("normalizeSessionMessages", () => {
             state: {
             state: {
               status: "completed",
               status: "completed",
               input: { filePath: "note.txt" },
               input: { filePath: "note.txt" },
-              structured: { title: "note.txt" },
+              metadata: { title: "note.txt" },
               content: [{ type: "text", text: "hello" }],
               content: [{ type: "text", text: "hello" }],
             },
             },
             time: { created: 5, ran: 6, completed: 7 },
             time: { created: 5, ran: 6, completed: 7 },
@@ -170,7 +170,7 @@ describe("normalizeSessionMessages", () => {
               status: "completed",
               status: "completed",
               input: { path: "/repo/README.md", oldString: "old", newString: "new" },
               input: { path: "/repo/README.md", oldString: "old", newString: "new" },
               content: [{ type: "text", text: "Edited file successfully" }],
               content: [{ type: "text", text: "Edited file successfully" }],
-              structured: {
+              metadata: {
                 files: [
                 files: [
                   {
                   {
                     file: "README.md",
                     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 {
       return {
         status: "running" as const,
         status: "running" as const,
         input: normalizeToolInput(tool.name, tool.state.input),
         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 },
         time: { start },
       }
       }
     }
     }
@@ -313,7 +314,8 @@ function toolPart(sessionID: string, messageID: string, tool: SessionMessageAssi
         status: "error" as const,
         status: "error" as const,
         input: normalizeToolInput(tool.name, tool.state.input),
         input: normalizeToolInput(tool.name, tool.state.input),
         error: tool.state.error.message,
         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 },
         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),
       input: normalizeToolInput(tool.name, tool.state.input),
       output: tool.state.content.flatMap((item) => (item.type === "text" ? [item.text] : [])).join("\n"),
       output: tool.state.content.flatMap((item) => (item.type === "text" ? [item.text] : [])).join("\n"),
       title: tool.name,
       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 },
       time: { start, end: tool.time.completed ?? start },
       attachments: attachments.length ? attachments : undefined,
       attachments: attachments.length ? attachments : undefined,
     }
     }

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


TEMPAT SAMPAH
packages/app/vendor/opencode-ai-client-1.17.13.tgz


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

@@ -39,7 +39,7 @@
   },
   },
   "dependencies": {
   "dependencies": {
     "@kobalte/core": "catalog:",
     "@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/core": "workspace:*",
     "@opencode-ai/sdk": "workspace:*",
     "@opencode-ai/sdk": "workspace:*",
     "@opencode-ai/ui": "workspace:*",
     "@opencode-ai/ui": "workspace:*",