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

This commit is contained in:
Brendan Allan 2026-07-25 16:07:13 +08:00 committed by GitHub
commit 0a6637e17a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
33 changed files with 268 additions and 191 deletions

View file

@ -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=="],

View file

@ -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({ body: JSON.stringify(
location: { directory }, new URL(route.request().url()).searchParams.get("mode") === "branch"
data: ? [diff("src/alpha.ts"), diff("src/beta.ts")]
new URL(route.request().url()).searchParams.get("mode") === "branch" : [diff("src/alpha.ts"), diff("src/gamma.ts")],
? [diff("src/alpha.ts"), diff("src/beta.ts")] ),
: [diff("src/alpha.ts"), diff("src/gamma.ts")],
}),
}), }),
) )
await page.addInitScript( await page.addInitScript(

View file

@ -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 }, branch: "review-pane-performance",
data: { branch: "review-pane-performance", defaultBranch: "dev" }, 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({ body: JSON.stringify(
location: { directory: scope ?? directory, project: { id: projectID, directory } }, url.searchParams.get("mode") === "branch"
data: ? detail
url.searchParams.get("mode") === "branch" ? branchDiffs
? detail .filter((diff) => diff.file.startsWith("src/branch/d00027/"))
? branchDiffs .map((diff) => fileDiff(diff.file, diff.additions, true, detailVersion))
.filter((diff) => diff.file.startsWith("src/branch/d00027/")) : branchDiffs
.map((diff) => fileDiff(diff.file, diff.additions, true, detailVersion)) : Array.from({ length: 7 }, (_, index) => fileDiff(`src/git-${index}.ts`, 1)),
: 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.pathname === "/vcs/diff" &&
url.searchParams.get("location[directory]")?.replaceAll("\\", "/").endsWith("/src/branch/d00027") === true 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.pathname === "/vcs/diff" &&
url.searchParams.get("location[directory]")?.replaceAll("\\", "/").endsWith("/src/branch/d00027") === true url.searchParams.get("directory")?.replaceAll("\\", "/").endsWith("/src/branch/d00027") === true
) )
}) })
sessionStatus[sessionID] = { type: "idle" } sessionStatus[sessionID] = { type: "idle" }

View file

@ -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)

View file

@ -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:*",

View file

@ -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 || "")

View file

@ -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 () => { async (): Promise<Path | undefined> => {
return sdk.api.path.get().catch(() => undefined) if ((await sdk.protocol) !== "v1") return
return sdk.client.path
.get()
.then((result) => result.data)
.catch(() => undefined)
}, },
{ initialValue: undefined }, { initialValue: undefined },
) )

View file

@ -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({ if ((await serverCtx().sdk.protocol) !== "v1") return
projectID: props.project.id, const project = await serverCtx()
name, .sdk.client.project.update({
icon: { color: store.color || "", override: store.iconOverride || "" }, projectID: props.project.id,
commands: { start }, 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)),
) )

View file

@ -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]

View file

@ -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[] },
) )

View file

@ -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[] },
) )

View file

@ -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() // return sdk()
.api.pty.connectToken({ // .api.pty.connectToken({
ptyID: id, // ptyID: id,
location: { directory }, // location: { directory },
"x-opencode-ticket": "1", // "x-opencode-ticket": "1",
}) // })
.then((result) => result.data.ticket) // .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
})
} }
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

View file

@ -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) => {

View file

@ -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 () => {

View file

@ -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 = { type McpApi = ServerApi["mcp"]
readonly get: (input?: PathGetInput) => Promise<PathGetOutput> 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(() => retry(async () => {
input.api.vcs.get({ location: { directory: input.directory } }).then((result) => { if ((await input.protocol) !== "v1") return
const next = { branch: result.data.branch, default_branch: result.data.defaultBranch } 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) =>

View file

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

View file

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

View file

@ -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,
}, },

View file

@ -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 : {}, // structured: tool.state.status === "running" ? tool.state.structured : {},
content: tool.state.status === "running" ? tool.state.content : [], 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 },
} }

View file

@ -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,

View file

@ -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 (event.type === "session.archived") {
if (info) remember({ ...info, time: { ...info.time, archived: event.created, updated: event.created } }) // if (info) remember({ ...info, time: { ...info.time, archived: event.created, updated: event.created } })
evict([sessionID]) // 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" ||

View file

@ -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 () => {

View file

@ -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" ||

View file

@ -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) => {

View file

@ -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( if ((await serverSDK().protocol) === "v1")
sessions await Promise.all(
.filter((session) => session.time.archived === undefined) sessions
.map((session) => .filter((session) => session.time.archived === undefined)
serverSDK() .map((session) =>
.api.session.archive({ sessionID: session.id, directory: session.directory }) serverSDK()
.catch(() => undefined), .client.session.update({
), sessionID: session.id,
) directory: session.directory,
time: { archived: Date.now() },
})
.catch(() => undefined),
),
)
setBusy(directory, false) setBusy(directory, false)
dismiss() dismiss()

View file

@ -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) => {

View file

@ -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")

View file

@ -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) { // async archive(value: Parameters<ServerApi["session"]["archive"]>[0] & LegacyLocation) {
await legacy(value).session.update({ sessionID: value.sessionID, time: { archived: Date.now() } }) // 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]) { // async update(value: Parameters<ServerApi["project"]["update"]>[0]) {
const project = (await legacy().project.list()).data?.find((item) => item.id === value.projectID) // const project = (await legacy().project.list()).data?.find((item) => item.id === value.projectID)
const result = await legacy({ directory: project?.worktree }).project.update({ // const result = await legacy({ directory: project?.worktree }).project.update({
...value, // ...value,
directory: project?.worktree, // directory: project?.worktree,
}) // })
if (!result.data) throw new Error(`Project not found: ${value.projectID}`) // if (!result.data) throw new Error(`Project not found: ${value.projectID}`)
return result.data as Project // 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: { // path: {
...input.current.path, // ...input.current.path,
async get(value?: Parameters<ServerApi["path"]["get"]>[0]) { // async get(value?: Parameters<ServerApi["path"]["get"]>[0]) {
const result = await legacy(value?.location).path.get() // const result = await legacy(value?.location).path.get()
if (!result.data) throw new Error("Path unavailable") // if (!result.data) throw new Error("Path unavailable")
return result.data // return result.data
}, // },
}, // },
vcs: { vcs: {
...input.current.vcs, ...input.current.vcs,
async get(value?: Parameters<ServerApi["vcs"]["get"]>[0]) { // async get(value?: Parameters<ServerApi["vcs"]["get"]>[0]) {
const result = await legacy(value?.location).vcs.get() // const result = await legacy(value?.location).vcs.get()
return located({ branch: result.data?.branch, defaultBranch: result.data?.default_branch }, value?.location) // 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]) { // async shells(value?: Parameters<ServerApi["pty"]["shells"]>[0]) {
return located((await legacy(value?.location).pty.shells()).data ?? [], value?.location) // 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]) { // async connectToken(value: Parameters<ServerApi["pty"]["connectToken"]>[0]) {
const result = await legacy(value.location).pty.connectToken({ ptyID: value.ptyID }) // const result = await legacy(value.location).pty.connectToken({ ptyID: value.ptyID })
if (!result.data) throw new Error(`Failed to connect terminal: ${value.ptyID}`) // if (!result.data) throw new Error(`Failed to connect terminal: ${value.ptyID}`)
return located(result.data, value.location) // return located(result.data, value.location)
}, // },
}, },
permission: { permission: {
...input.current.permission, ...input.current.permission,

View file

@ -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",

View file

@ -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,
} }

Binary file not shown.

View file

@ -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:*",