Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 712d4fb800 | |||
| 4d2875912d |
@@ -40,9 +40,11 @@
|
||||
"@dnd-kit/helpers": "0.5.0",
|
||||
"@dnd-kit/solid": "0.5.0",
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/client": "workspace:*",
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
"@opencode-ai/schema": "workspace:*",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@opencode-ai/sdk-v1": "https://registry.npmjs.org/@opencode-ai/sdk/-/sdk-1.17.18.tgz",
|
||||
"@opencode-ai/session-ui": "workspace:*",
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
"@pierre/trees": "1.0.0-beta.4",
|
||||
@@ -2218,6 +2220,8 @@
|
||||
|
||||
"@opencode-ai/sdk-next": ["@opencode-ai/sdk-next@workspace:packages/sdk-next"],
|
||||
|
||||
"@opencode-ai/sdk-v1": ["@opencode-ai/sdk@https://registry.npmjs.org/@opencode-ai/sdk/-/sdk-1.17.18.tgz", { "dependencies": { "cross-spawn": "7.0.6" } }, "sha512-c/C9PhY8PrbcxDY+JIYtOZsrmMD0KzoVvxq+RGUrZ6LQp57SuVBbT4lfwA2G8Se5RNC1N5JtYjiuaXeECnF2SQ=="],
|
||||
|
||||
"@opencode-ai/server": ["@opencode-ai/server@workspace:packages/server"],
|
||||
|
||||
"@opencode-ai/session-ui": ["@opencode-ai/session-ui@workspace:packages/session-ui"],
|
||||
|
||||
@@ -53,9 +53,11 @@
|
||||
"@dnd-kit/helpers": "0.5.0",
|
||||
"@dnd-kit/solid": "0.5.0",
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/client": "workspace:*",
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
"@opencode-ai/schema": "workspace:*",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@opencode-ai/sdk-v1": "https://registry.npmjs.org/@opencode-ai/sdk/-/sdk-1.17.18.tgz",
|
||||
"@opencode-ai/session-ui": "workspace:*",
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
"@pierre/trees": "1.0.0-beta.4",
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { createOpencodeClient } from "@opencode-ai/sdk-v1/v2/client"
|
||||
import { createV1Backend } from "./backend-v1"
|
||||
|
||||
function setup(respond: (request: Request) => Response | Promise<Response>) {
|
||||
const requests: Request[] = []
|
||||
const fetch = Object.assign(
|
||||
async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const request = input instanceof Request ? input : new Request(input, init)
|
||||
requests.push(request)
|
||||
return respond(request)
|
||||
},
|
||||
{ preconnect: globalThis.fetch.preconnect },
|
||||
) satisfies typeof globalThis.fetch
|
||||
return {
|
||||
requests,
|
||||
backend: createV1Backend(createOpencodeClient({ baseUrl: "http://localhost", fetch })),
|
||||
}
|
||||
}
|
||||
|
||||
function json(data: unknown, headers?: HeadersInit) {
|
||||
return new Response(JSON.stringify(data), {
|
||||
headers: { "content-type": "application/json", ...Object.fromEntries(new Headers(headers)) },
|
||||
})
|
||||
}
|
||||
|
||||
const session = {
|
||||
id: "ses_1",
|
||||
slug: "one",
|
||||
projectID: "project",
|
||||
directory: "/repo",
|
||||
title: "Session",
|
||||
version: "1",
|
||||
time: { created: 1, updated: 2 },
|
||||
}
|
||||
|
||||
describe("createV1Backend", () => {
|
||||
test("normalizes session pagination and location", async () => {
|
||||
const setupResult = setup(() => json([session], { "x-next-cursor": "456" }))
|
||||
|
||||
const result = await setupResult.backend.common.sessions.list({
|
||||
location: { directory: "/repo", workspaceID: "workspace" },
|
||||
roots: true,
|
||||
limit: 10,
|
||||
cursor: "123",
|
||||
})
|
||||
|
||||
expect(result).toEqual({
|
||||
items: [
|
||||
{
|
||||
id: "ses_1",
|
||||
parentID: undefined,
|
||||
projectID: "project",
|
||||
location: { directory: "/repo", workspaceID: undefined },
|
||||
title: "Session",
|
||||
cost: 0,
|
||||
tokens: undefined,
|
||||
time: { created: 1, updated: 2 },
|
||||
share: undefined,
|
||||
revert: undefined,
|
||||
},
|
||||
],
|
||||
next: "456",
|
||||
})
|
||||
const url = new URL(setupResult.requests[0].url)
|
||||
expect(url.pathname).toBe("/experimental/session")
|
||||
expect(url.searchParams.get("directory")).toBe("/repo")
|
||||
expect(url.searchParams.get("workspace")).toBe("workspace")
|
||||
expect(url.searchParams.get("roots")).toBe("true")
|
||||
expect(url.searchParams.get("cursor")).toBe("123")
|
||||
})
|
||||
|
||||
test("converts normalized prompts to legacy parts", async () => {
|
||||
const setupResult = setup(() => new Response(null, { status: 204 }))
|
||||
|
||||
await setupResult.backend.common.sessions.prompt({
|
||||
sessionID: "ses_1",
|
||||
id: "msg_1",
|
||||
text: "hello",
|
||||
selection: {
|
||||
agent: "build",
|
||||
model: { id: "model", providerID: "provider", variant: "high" },
|
||||
},
|
||||
files: [{ uri: "data:text/plain;base64,aGk=", name: "hi.txt", mime: "text/plain" }],
|
||||
agents: [{ name: "explore", text: "@explore", start: 6, end: 14 }],
|
||||
})
|
||||
|
||||
const request = setupResult.requests[0]
|
||||
expect(new URL(request.url).pathname).toBe("/session/ses_1/prompt_async")
|
||||
expect(await request.json()).toEqual({
|
||||
messageID: "msg_1",
|
||||
model: { providerID: "provider", modelID: "model" },
|
||||
agent: "build",
|
||||
variant: "high",
|
||||
parts: [
|
||||
{ type: "text", text: "hello" },
|
||||
{ type: "file", mime: "text/plain", filename: "hi.txt", url: "data:text/plain;base64,aGk=" },
|
||||
{ type: "agent", name: "explore", source: { value: "@explore", start: 6, end: 14 } },
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
test("combines mixed file search and decodes binary content", async () => {
|
||||
const setupResult = setup((request) => {
|
||||
const url = new URL(request.url)
|
||||
if (url.pathname === "/find/file") {
|
||||
return json(url.searchParams.get("type") === "file" ? ["a.txt", "shared"] : ["dir", "shared"])
|
||||
}
|
||||
return json({ type: "binary", content: "AAEC", encoding: "base64", mimeType: "application/octet-stream" })
|
||||
})
|
||||
|
||||
const found = await setupResult.backend.common.files.find({ query: "a" })
|
||||
const content = await setupResult.backend.common.files.read({ path: "a.bin" })
|
||||
|
||||
expect(found).toEqual([
|
||||
{ path: "a.txt", type: "file" },
|
||||
{ path: "shared", type: "directory" },
|
||||
{ path: "dir", type: "directory" },
|
||||
])
|
||||
expect([...content.bytes]).toEqual([0, 1, 2])
|
||||
expect(content.kind).toBe("binary")
|
||||
expect(content.mimeType).toBe("application/octet-stream")
|
||||
})
|
||||
|
||||
test("merges global config updates with untouched fields", async () => {
|
||||
const bodies: unknown[] = []
|
||||
const setupResult = setup(async (request) => {
|
||||
if (request.method === "GET") return json({ autoupdate: true, model: "old", disabled_providers: ["one"] })
|
||||
bodies.push(await request.json())
|
||||
return json({})
|
||||
})
|
||||
|
||||
await setupResult.backend.capabilities.configuration?.updateGlobal({
|
||||
model: "new",
|
||||
disabledProviders: ["two"],
|
||||
})
|
||||
|
||||
expect(bodies).toEqual([{ autoupdate: true, model: "new", disabled_providers: ["two"] }])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,992 @@
|
||||
import type {
|
||||
Config,
|
||||
Event,
|
||||
GlobalEvent,
|
||||
Message,
|
||||
Model,
|
||||
OpencodeClient,
|
||||
Part,
|
||||
Project,
|
||||
Provider,
|
||||
Session,
|
||||
} from "@opencode-ai/sdk-v1/v2/client"
|
||||
import type {
|
||||
AppAgent,
|
||||
AppClient,
|
||||
AppCommand,
|
||||
AppConfig,
|
||||
AppEvent,
|
||||
AppEventEnvelope,
|
||||
AppFileDiff,
|
||||
AppModel,
|
||||
AppPermissionRequest,
|
||||
AppProject,
|
||||
AppProvider,
|
||||
AppQuestionRequest,
|
||||
AppReference,
|
||||
AppSession,
|
||||
CommandInput,
|
||||
DecoratedFileContent,
|
||||
FileContent,
|
||||
FileEntry,
|
||||
LocationInput,
|
||||
LocationRef,
|
||||
Page,
|
||||
PromptFile,
|
||||
PromptInput,
|
||||
ProviderCatalog,
|
||||
RequestOptions,
|
||||
SessionActivity,
|
||||
TimelineContent,
|
||||
TimelineItem,
|
||||
ToolState,
|
||||
} from "./backend"
|
||||
|
||||
type CachedMessage = {
|
||||
info: Message
|
||||
parts: Part[]
|
||||
}
|
||||
|
||||
export function createV1Backend(client: OpencodeClient, defaultLocation?: LocationRef): AppClient {
|
||||
const messages = new Map<string, CachedMessage>()
|
||||
|
||||
const options = (input?: RequestOptions) => ({ signal: input?.signal, throwOnError: true as const })
|
||||
const location = (input?: LocationInput) => legacyLocation(input?.location ?? defaultLocation)
|
||||
const cache = (input: CachedMessage) => {
|
||||
messages.set(input.info.id, input)
|
||||
return toTimelineItem(input.info, input.parts)
|
||||
}
|
||||
|
||||
const loadMessage = async (input: { sessionID: string; messageID: string }, request?: RequestOptions) => {
|
||||
const cached = messages.get(input.messageID)
|
||||
if (cached) return cached
|
||||
const result = await client.session.message({ ...input, ...legacyLocation(defaultLocation) }, options(request))
|
||||
messages.set(input.messageID, result.data)
|
||||
return result.data
|
||||
}
|
||||
|
||||
return {
|
||||
version: "v1",
|
||||
common: {
|
||||
health: {
|
||||
get: async (request) => {
|
||||
const result = await client.global.health(options(request))
|
||||
return result.data
|
||||
},
|
||||
},
|
||||
projects: {
|
||||
list: async (request) => {
|
||||
const result = await client.project.list(undefined, options(request))
|
||||
return result.data.map(toProject)
|
||||
},
|
||||
current: async (input, request) => {
|
||||
const params = location(input)
|
||||
const [project, path] = await Promise.all([
|
||||
client.project.current(params, options(request)),
|
||||
client.path.get(params, options(request)),
|
||||
])
|
||||
return { id: project.data.id, directory: path.data.directory }
|
||||
},
|
||||
},
|
||||
catalog: {
|
||||
providers: async (input, request) => {
|
||||
const result = await client.provider.list(location(input), options(request))
|
||||
return toProviderCatalog(result.data)
|
||||
},
|
||||
agents: async (input, request) => {
|
||||
const result = await client.app.agents(location(input), options(request))
|
||||
return normalizeAgents(result.data).map(toAgent)
|
||||
},
|
||||
},
|
||||
commands: {
|
||||
list: async (input, request) => {
|
||||
const result = await client.command.list(location(input), options(request))
|
||||
return result.data.map(toCommand)
|
||||
},
|
||||
},
|
||||
references: {
|
||||
list: async (input, request) => {
|
||||
const result = await client.v2.reference.list(
|
||||
{ location: apiLocation(input?.location ?? defaultLocation) },
|
||||
options(request),
|
||||
)
|
||||
return result.data.data.map(toReference)
|
||||
},
|
||||
},
|
||||
sessions: {
|
||||
list: async (input, request) => {
|
||||
const cursor = input?.cursor === undefined ? undefined : Number(input.cursor)
|
||||
if (cursor !== undefined && !Number.isFinite(cursor))
|
||||
throw new Error(`Invalid session cursor: ${input?.cursor}`)
|
||||
const result = await client.experimental.session.list(
|
||||
{
|
||||
...location(input),
|
||||
roots: input?.roots,
|
||||
limit: input?.limit,
|
||||
search: input?.search,
|
||||
cursor,
|
||||
},
|
||||
options(request),
|
||||
)
|
||||
return {
|
||||
items: result.data.map(toSession),
|
||||
next: result.response.headers.get("x-next-cursor") ?? undefined,
|
||||
}
|
||||
},
|
||||
create: async (input, request) => {
|
||||
const result = await client.session.create(
|
||||
{
|
||||
...location(input),
|
||||
agent: input?.agent,
|
||||
model: input?.model,
|
||||
},
|
||||
options(request),
|
||||
)
|
||||
return toSession(result.data)
|
||||
},
|
||||
get: async (input, request) => {
|
||||
const result = await client.session.get({ sessionID: input.sessionID, ...location(input) }, options(request))
|
||||
return toSession(result.data)
|
||||
},
|
||||
remove: async (input, request) => {
|
||||
await client.session.delete({ sessionID: input.sessionID, ...location(input) }, options(request))
|
||||
},
|
||||
fork: async (input, request) => {
|
||||
const result = await client.session.fork(
|
||||
{ sessionID: input.sessionID, messageID: input.messageID, ...location(input) },
|
||||
options(request),
|
||||
)
|
||||
return toSession(result.data)
|
||||
},
|
||||
rename: async (input, request) => {
|
||||
await client.session.update(
|
||||
{ sessionID: input.sessionID, title: input.title, ...location(input) },
|
||||
options(request),
|
||||
)
|
||||
},
|
||||
interrupt: async (input, request) => {
|
||||
await client.session.abort({ sessionID: input.sessionID, ...location(input) }, options(request))
|
||||
},
|
||||
activity: async (input, request) => {
|
||||
const result = await client.session.status(location(input), options(request))
|
||||
return Object.fromEntries(
|
||||
Object.entries(result.data).flatMap(([sessionID, status]) => {
|
||||
const activity = toActivity(status)
|
||||
return activity ? [[sessionID, activity] as const] : []
|
||||
}),
|
||||
)
|
||||
},
|
||||
history: async (input, request) => {
|
||||
const result = await client.session.messages(
|
||||
{
|
||||
sessionID: input.sessionID,
|
||||
limit: input.limit,
|
||||
before: input.cursor,
|
||||
...legacyLocation(defaultLocation),
|
||||
},
|
||||
options(request),
|
||||
)
|
||||
return {
|
||||
items: result.data.map(cache),
|
||||
next: result.response.headers.get("x-next-cursor") ?? undefined,
|
||||
}
|
||||
},
|
||||
message: async (input, request) => cache(await loadMessage(input, request)),
|
||||
prompt: async (input, request) => {
|
||||
await client.session.promptAsync(
|
||||
{
|
||||
sessionID: input.sessionID,
|
||||
messageID: input.id,
|
||||
agent: input.selection?.agent,
|
||||
model: input.selection?.model && {
|
||||
providerID: input.selection.model.providerID,
|
||||
modelID: input.selection.model.id,
|
||||
},
|
||||
variant: input.selection?.model?.variant,
|
||||
parts: toPromptParts(input),
|
||||
...legacyLocation(defaultLocation),
|
||||
},
|
||||
options(request),
|
||||
)
|
||||
},
|
||||
command: async (input, request) => {
|
||||
await client.session.command(
|
||||
{
|
||||
sessionID: input.sessionID,
|
||||
messageID: input.id,
|
||||
command: input.command,
|
||||
arguments: input.arguments ?? "",
|
||||
agent: input.agent,
|
||||
model: input.model && `${input.model.providerID}/${input.model.id}`,
|
||||
variant: input.model?.variant,
|
||||
parts: input.files?.map(toFilePart),
|
||||
...legacyLocation(defaultLocation),
|
||||
},
|
||||
options(request),
|
||||
)
|
||||
},
|
||||
},
|
||||
files: {
|
||||
list: async (input, request) => {
|
||||
const result = await client.file.list({ path: input.path ?? "", ...location(input) }, options(request))
|
||||
return result.data.map((item) => ({ path: item.path, type: item.type }))
|
||||
},
|
||||
find: async (input, request) => {
|
||||
const find = async (type: FileEntry["type"]) => {
|
||||
const result = await client.find.files(
|
||||
{ query: input.query, type, limit: input.limit, ...location(input) },
|
||||
options(request),
|
||||
)
|
||||
return result.data.map((path) => ({ path, type }))
|
||||
}
|
||||
if (input.type) return find(input.type)
|
||||
const result = await Promise.all([find("file"), find("directory")])
|
||||
return [...new Map(result.flat().map((item) => [item.path, item])).values()]
|
||||
},
|
||||
read: async (input, request) => {
|
||||
const result = await client.file.read({ path: input.path, ...location(input) }, options(request))
|
||||
return toFileContent(result.data)
|
||||
},
|
||||
},
|
||||
permissions: {
|
||||
pending: async (input, request) => {
|
||||
const result = await client.permission.list(location(input), options(request))
|
||||
return result.data.map(toPermission)
|
||||
},
|
||||
reply: async (input, request) => {
|
||||
await client.permission.reply(
|
||||
{
|
||||
requestID: input.requestID,
|
||||
reply: input.reply,
|
||||
message: input.message,
|
||||
...legacyLocation(defaultLocation),
|
||||
},
|
||||
options(request),
|
||||
)
|
||||
},
|
||||
},
|
||||
questions: {
|
||||
pending: async (input, request) => {
|
||||
const result = await client.question.list(location(input), options(request))
|
||||
return result.data.map(toQuestion)
|
||||
},
|
||||
reply: async (input, request) => {
|
||||
await client.question.reply(
|
||||
{
|
||||
requestID: input.requestID,
|
||||
answers: input.answers.map((answer) => [...answer]),
|
||||
...legacyLocation(defaultLocation),
|
||||
},
|
||||
options(request),
|
||||
)
|
||||
},
|
||||
reject: async (input, request) => {
|
||||
await client.question.reject(
|
||||
{ requestID: input.requestID, ...legacyLocation(defaultLocation) },
|
||||
options(request),
|
||||
)
|
||||
},
|
||||
},
|
||||
vcs: {
|
||||
status: async (input, request) => {
|
||||
const result = await client.vcs.status(location(input), options(request))
|
||||
return result.data
|
||||
},
|
||||
diff: async (input, request) => {
|
||||
const result = await client.vcs.diff(
|
||||
{
|
||||
mode: input.mode === "working" ? "git" : "branch",
|
||||
context: input.context,
|
||||
...location(input),
|
||||
},
|
||||
options(request),
|
||||
)
|
||||
return result.data
|
||||
},
|
||||
},
|
||||
mcp: {
|
||||
list: async (input, request) => {
|
||||
const result = await client.mcp.status(location(input), options(request))
|
||||
return Object.entries(result.data).map(([name, status]) => ({ name, status }))
|
||||
},
|
||||
resources: async (input, request) => {
|
||||
const result = await client.experimental.resource.list(location(input), options(request))
|
||||
return {
|
||||
resources: Object.values(result.data).map((item) => ({
|
||||
server: item.client,
|
||||
name: item.name,
|
||||
uri: item.uri,
|
||||
description: item.description,
|
||||
mimeType: item.mimeType,
|
||||
})),
|
||||
templates: [],
|
||||
}
|
||||
},
|
||||
},
|
||||
pty: {
|
||||
list: async (input, request) => {
|
||||
const result = await client.pty.list(location(input), options(request))
|
||||
return result.data.map(toPty)
|
||||
},
|
||||
create: async (input, request) => {
|
||||
const result = await client.pty.create(
|
||||
{
|
||||
title: input.title,
|
||||
command: input.command,
|
||||
args: input.args ? [...input.args] : undefined,
|
||||
cwd: input.cwd,
|
||||
env: input.env ? { ...input.env } : undefined,
|
||||
...location(input),
|
||||
},
|
||||
options(request),
|
||||
)
|
||||
return toPty(result.data)
|
||||
},
|
||||
get: async (input, request) => {
|
||||
const result = await client.pty.get({ ptyID: input.ptyID, ...location(input) }, options(request))
|
||||
return toPty(result.data)
|
||||
},
|
||||
update: async (input, request) => {
|
||||
const result = await client.pty.update(
|
||||
{
|
||||
ptyID: input.ptyID,
|
||||
title: input.title,
|
||||
size: input.size,
|
||||
...location(input),
|
||||
},
|
||||
options(request),
|
||||
)
|
||||
return toPty(result.data)
|
||||
},
|
||||
remove: async (input, request) => {
|
||||
await client.pty.remove({ ptyID: input.ptyID, ...location(input) }, options(request))
|
||||
},
|
||||
},
|
||||
events: {
|
||||
subscribe: (request) => ({
|
||||
async *[Symbol.asyncIterator]() {
|
||||
const result = await client.global.event(options(request))
|
||||
for await (const input of result.stream) {
|
||||
const event = await toEvent(input, messages, loadMessage)
|
||||
yield {
|
||||
location:
|
||||
input.directory === "global"
|
||||
? undefined
|
||||
: { directory: input.directory, workspaceID: input.workspace },
|
||||
event,
|
||||
} satisfies AppEventEnvelope
|
||||
}
|
||||
},
|
||||
}),
|
||||
},
|
||||
disposeLocation: async (input, request) => {
|
||||
await client.instance.dispose(location(input), options(request))
|
||||
},
|
||||
},
|
||||
capabilities: {
|
||||
configuration: {
|
||||
getGlobal: async (request) => {
|
||||
const result = await client.global.config.get(options(request))
|
||||
return toConfig(result.data)
|
||||
},
|
||||
updateGlobal: async (config, request) => {
|
||||
const current = await client.global.config.get(options(request))
|
||||
await client.global.config.update({ config: { ...current.data, ...fromConfig(config) } }, options(request))
|
||||
},
|
||||
get: async (input, request) => {
|
||||
const result = await client.config.get(location(input), options(request))
|
||||
return toConfig(result.data)
|
||||
},
|
||||
},
|
||||
providerAuthV1: {
|
||||
methods: async (input, request) => {
|
||||
const result = await client.provider.auth(location(input), options(request))
|
||||
return result.data
|
||||
},
|
||||
authorize: async (input, request) => {
|
||||
const result = await client.provider.oauth.authorize(
|
||||
{
|
||||
providerID: input.providerID,
|
||||
method: input.method,
|
||||
inputs: input.values ? { ...input.values } : undefined,
|
||||
...location(input),
|
||||
},
|
||||
options(request),
|
||||
)
|
||||
return result.data
|
||||
},
|
||||
callback: async (input, request) => {
|
||||
await client.provider.oauth.callback(
|
||||
{
|
||||
providerID: input.providerID,
|
||||
method: input.method,
|
||||
code: input.code,
|
||||
...location(input),
|
||||
},
|
||||
options(request),
|
||||
)
|
||||
},
|
||||
setApiKey: async (input, request) => {
|
||||
await client.auth.set(
|
||||
{
|
||||
providerID: input.providerID,
|
||||
auth: { type: "api", key: input.key, metadata: input.metadata && { ...input.metadata } },
|
||||
},
|
||||
options(request),
|
||||
)
|
||||
},
|
||||
remove: async (input, request) => {
|
||||
await client.auth.remove(input, options(request))
|
||||
},
|
||||
},
|
||||
projectEditing: {
|
||||
update: async (input, request) => {
|
||||
const result = await client.project.update(
|
||||
{
|
||||
projectID: input.projectID,
|
||||
name: input.name,
|
||||
icon: input.icon,
|
||||
commands: input.commands,
|
||||
...location(input),
|
||||
},
|
||||
options(request),
|
||||
)
|
||||
return toProject(result.data)
|
||||
},
|
||||
initGit: async (input, request) => {
|
||||
const result = await client.project.initGit(location(input), options(request))
|
||||
return toProject(result.data)
|
||||
},
|
||||
},
|
||||
worktreesV1: {
|
||||
list: async (input, request) => {
|
||||
const result = await client.worktree.list(location(input), options(request))
|
||||
return result.data
|
||||
},
|
||||
create: async (input, request) => {
|
||||
const result = await client.worktree.create(location(input), options(request))
|
||||
return { directory: result.data.directory, branch: result.data.branch }
|
||||
},
|
||||
remove: async (input, request) => {
|
||||
await client.worktree.remove(
|
||||
{ ...location(input), worktreeRemoveInput: { directory: input.directory } },
|
||||
options(request),
|
||||
)
|
||||
},
|
||||
reset: async (input, request) => {
|
||||
await client.worktree.reset(
|
||||
{ ...location(input), worktreeResetInput: { directory: input.directory } },
|
||||
options(request),
|
||||
)
|
||||
},
|
||||
},
|
||||
sessionExtrasV1: {
|
||||
archive: async (sessionID, archivedAt, request) => {
|
||||
await client.session.update(
|
||||
{ sessionID, time: { archived: archivedAt }, ...legacyLocation(defaultLocation) },
|
||||
options(request),
|
||||
)
|
||||
},
|
||||
share: async (sessionID, request) => {
|
||||
const result = await client.session.share({ sessionID, ...legacyLocation(defaultLocation) }, options(request))
|
||||
if (!result.data.share) throw new Error(`Session ${sessionID} was shared without a URL`)
|
||||
return result.data.share.url
|
||||
},
|
||||
unshare: async (sessionID, request) => {
|
||||
await client.session.unshare({ sessionID, ...legacyLocation(defaultLocation) }, options(request))
|
||||
},
|
||||
diff: async (sessionID, request) => {
|
||||
const result = await client.session.diff({ sessionID, ...legacyLocation(defaultLocation) }, options(request))
|
||||
return result.data.flatMap((item) => (item.file ? [{ ...item, file: item.file }] : []))
|
||||
},
|
||||
todos: async (sessionID, request) => {
|
||||
const result = await client.session.todo({ sessionID, ...legacyLocation(defaultLocation) }, options(request))
|
||||
return result.data.map((item) => ({ content: item.content, status: item.status }))
|
||||
},
|
||||
summarize: async (sessionID, model, request) => {
|
||||
await client.session.summarize(
|
||||
{
|
||||
sessionID,
|
||||
providerID: model.providerID,
|
||||
modelID: model.id,
|
||||
...legacyLocation(defaultLocation),
|
||||
},
|
||||
options(request),
|
||||
)
|
||||
},
|
||||
revert: async (sessionID, messageID, request) => {
|
||||
await client.session.revert({ sessionID, messageID, ...legacyLocation(defaultLocation) }, options(request))
|
||||
},
|
||||
clearRevert: async (sessionID, request) => {
|
||||
await client.session.unrevert({ sessionID, ...legacyLocation(defaultLocation) }, options(request))
|
||||
},
|
||||
shell: async (input, request) => {
|
||||
await client.session.shell(
|
||||
{
|
||||
sessionID: input.sessionID,
|
||||
messageID: input.id,
|
||||
command: input.command,
|
||||
agent: input.agent,
|
||||
model: input.model && { providerID: input.model.providerID, modelID: input.model.id },
|
||||
...location(input),
|
||||
},
|
||||
options(request),
|
||||
)
|
||||
},
|
||||
},
|
||||
lsp: {
|
||||
status: async (input, request) => {
|
||||
const result = await client.lsp.status(location(input), options(request))
|
||||
return result.data.map((item) => ({ id: item.id, name: item.name, status: item.status }))
|
||||
},
|
||||
},
|
||||
mcpControl: {
|
||||
connect: async (input, request) => {
|
||||
await client.mcp.connect({ name: input.name, ...location(input) }, options(request))
|
||||
},
|
||||
disconnect: async (input, request) => {
|
||||
await client.mcp.disconnect({ name: input.name, ...location(input) }, options(request))
|
||||
},
|
||||
authenticate: async (input, request) => {
|
||||
await client.mcp.auth.authenticate({ name: input.name, ...location(input) }, options(request))
|
||||
},
|
||||
},
|
||||
pathInfo: {
|
||||
get: async (input, request) => {
|
||||
const result = await client.path.get(location(input), options(request))
|
||||
return result.data
|
||||
},
|
||||
},
|
||||
vcsInfo: {
|
||||
get: async (input, request) => {
|
||||
const result = await client.vcs.get(location(input), options(request))
|
||||
return { branch: result.data.branch, defaultBranch: result.data.default_branch }
|
||||
},
|
||||
},
|
||||
decoratedFiles: {
|
||||
read: async (input, request) => {
|
||||
const result = await client.file.read({ path: input.path, ...location(input) }, options(request))
|
||||
return toDecoratedFile(result.data)
|
||||
},
|
||||
},
|
||||
ptyTransport: {
|
||||
connectToken: async (input, request) => {
|
||||
const result = await client.pty.connectToken({ ptyID: input.ptyID, ...location(input) }, options(request))
|
||||
return { ticket: result.data.ticket }
|
||||
},
|
||||
},
|
||||
shellDiscovery: {
|
||||
list: async (input, request) => {
|
||||
const result = await client.pty.shells(location(input), options(request))
|
||||
return result.data
|
||||
},
|
||||
},
|
||||
runtimeV1: {
|
||||
disposeAll: async (request) => {
|
||||
await client.global.dispose(options(request))
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function legacyLocation(input?: LocationRef) {
|
||||
return {
|
||||
directory: input?.directory,
|
||||
workspace: input?.workspaceID,
|
||||
}
|
||||
}
|
||||
|
||||
function apiLocation(input?: LocationRef) {
|
||||
if (!input) return
|
||||
return {
|
||||
directory: input.directory,
|
||||
workspace: input.workspaceID,
|
||||
}
|
||||
}
|
||||
|
||||
function toProject(input: Project): AppProject {
|
||||
return {
|
||||
id: input.id,
|
||||
worktree: input.worktree,
|
||||
name: input.name,
|
||||
icon: input.icon,
|
||||
commands: input.commands,
|
||||
sandboxes: input.sandboxes,
|
||||
}
|
||||
}
|
||||
|
||||
function toSession(input: Session): AppSession {
|
||||
return {
|
||||
id: input.id,
|
||||
parentID: input.parentID,
|
||||
projectID: input.projectID,
|
||||
location: { directory: input.directory, workspaceID: input.workspaceID },
|
||||
title: input.title,
|
||||
cost: input.cost ?? 0,
|
||||
tokens: input.tokens,
|
||||
time: input.time,
|
||||
share: input.share,
|
||||
revert: input.revert && { messageID: input.revert.messageID },
|
||||
}
|
||||
}
|
||||
|
||||
function toModel(input: Model): AppModel {
|
||||
return {
|
||||
id: input.id,
|
||||
providerID: input.providerID,
|
||||
name: input.name,
|
||||
family: input.family,
|
||||
releaseDate: input.release_date,
|
||||
cost: {
|
||||
input: input.cost.input,
|
||||
output: input.cost.output,
|
||||
cacheRead: input.cost.cache.read,
|
||||
cacheWrite: input.cost.cache.write,
|
||||
},
|
||||
capabilities: {
|
||||
reasoning: input.capabilities.reasoning,
|
||||
input: input.capabilities.input,
|
||||
},
|
||||
limit: input.limit,
|
||||
variants: input.variants,
|
||||
}
|
||||
}
|
||||
|
||||
function toProvider(input: Provider): AppProvider {
|
||||
return {
|
||||
id: input.id,
|
||||
name: input.name,
|
||||
models: Object.fromEntries(
|
||||
Object.entries(input.models).flatMap(([id, model]) =>
|
||||
model.status === "deprecated" ? [] : [[id, toModel(model)]],
|
||||
),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
function toProviderCatalog(input: {
|
||||
all: Provider[]
|
||||
connected: string[]
|
||||
default: Record<string, string>
|
||||
}): ProviderCatalog {
|
||||
return {
|
||||
providers: new Map(input.all.map((provider) => [provider.id, toProvider(provider)])),
|
||||
connected: input.connected,
|
||||
defaults: input.default,
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeAgents(input: unknown) {
|
||||
const valid = (item: unknown): item is import("@opencode-ai/sdk-v1/v2/client").Agent => {
|
||||
if (!item || typeof item !== "object") return false
|
||||
if (!("name" in item) || typeof item.name !== "string") return false
|
||||
if (!("mode" in item)) return false
|
||||
return item.mode === "subagent" || item.mode === "primary" || item.mode === "all"
|
||||
}
|
||||
if (Array.isArray(input)) return input.filter(valid)
|
||||
if (valid(input)) return [input]
|
||||
if (!input || typeof input !== "object") return []
|
||||
return Object.values(input).filter(valid)
|
||||
}
|
||||
|
||||
function toAgent(input: import("@opencode-ai/sdk-v1/v2/client").Agent): AppAgent {
|
||||
return {
|
||||
id: input.name,
|
||||
name: input.name,
|
||||
description: input.description,
|
||||
mode: input.mode,
|
||||
hidden: input.hidden ?? false,
|
||||
color: input.color,
|
||||
model: input.model && {
|
||||
id: input.model.modelID,
|
||||
providerID: input.model.providerID,
|
||||
variant: input.variant,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function toCommand(input: import("@opencode-ai/sdk-v1/v2/client").Command): AppCommand {
|
||||
return { name: input.name, description: input.description, source: input.source }
|
||||
}
|
||||
|
||||
function toReference(input: import("@opencode-ai/sdk-v1/v2/client").ReferenceInfo): AppReference {
|
||||
return input
|
||||
}
|
||||
|
||||
function toActivity(input: import("@opencode-ai/sdk-v1/v2/client").SessionStatus): SessionActivity | undefined {
|
||||
if (input.type === "idle") return
|
||||
if (input.type === "busy") return { type: "running" }
|
||||
return input
|
||||
}
|
||||
|
||||
function toTimelineItem(info: Message, parts: readonly Part[]): TimelineItem {
|
||||
if (info.role === "user") {
|
||||
return {
|
||||
type: "user",
|
||||
id: info.id,
|
||||
sessionID: info.sessionID,
|
||||
created: info.time.created,
|
||||
content: parts.flatMap(toTimelineContent),
|
||||
agent: info.agent,
|
||||
model: {
|
||||
id: info.model.modelID,
|
||||
providerID: info.model.providerID,
|
||||
variant: info.model.variant,
|
||||
},
|
||||
raw: { info, parts },
|
||||
}
|
||||
}
|
||||
return {
|
||||
type: "assistant",
|
||||
id: info.id,
|
||||
sessionID: info.sessionID,
|
||||
parentID: info.parentID,
|
||||
created: info.time.created,
|
||||
completed: info.time.completed,
|
||||
content: parts.flatMap(toTimelineContent),
|
||||
agent: info.agent,
|
||||
model: { id: info.modelID, providerID: info.providerID, variant: info.variant },
|
||||
tokens: info.tokens,
|
||||
error: info.error,
|
||||
raw: { info, parts },
|
||||
}
|
||||
}
|
||||
|
||||
function toTimelineContent(input: Part): TimelineContent[] {
|
||||
if (input.type === "text")
|
||||
return [
|
||||
{
|
||||
type: input.type,
|
||||
id: input.id,
|
||||
text: input.text,
|
||||
synthetic: input.synthetic,
|
||||
ignored: input.ignored,
|
||||
metadata: input.metadata,
|
||||
},
|
||||
]
|
||||
if (input.type === "reasoning") return [{ type: input.type, id: input.id, text: input.text }]
|
||||
if (input.type === "file")
|
||||
return [
|
||||
{
|
||||
type: input.type,
|
||||
id: input.id,
|
||||
uri: input.url,
|
||||
name: input.filename,
|
||||
mime: input.mime,
|
||||
source: input.source && {
|
||||
...input.source,
|
||||
text: {
|
||||
text: input.source.text.value,
|
||||
start: input.source.text.start,
|
||||
end: input.source.text.end,
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
if (input.type === "agent")
|
||||
return [
|
||||
{
|
||||
type: input.type,
|
||||
id: input.id,
|
||||
name: input.name,
|
||||
source: input.source && { text: input.source.value, start: input.source.start, end: input.source.end },
|
||||
},
|
||||
]
|
||||
if (input.type === "tool")
|
||||
return [{ type: input.type, id: input.id, callID: input.callID, tool: input.tool, state: toToolState(input.state) }]
|
||||
return []
|
||||
}
|
||||
|
||||
function toToolState(input: import("@opencode-ai/sdk-v1/v2/client").ToolState): ToolState {
|
||||
if (input.status === "pending") return { status: input.status, input: input.input, raw: input.raw }
|
||||
if (input.status === "running")
|
||||
return { status: input.status, input: input.input, title: input.title, metadata: input.metadata }
|
||||
if (input.status === "completed")
|
||||
return {
|
||||
status: input.status,
|
||||
input: input.input,
|
||||
output: input.output,
|
||||
title: input.title,
|
||||
metadata: input.metadata,
|
||||
}
|
||||
return { status: input.status, input: input.input, error: input.error, metadata: input.metadata }
|
||||
}
|
||||
|
||||
function toFilePart(input: PromptFile) {
|
||||
return {
|
||||
type: "file" as const,
|
||||
mime: input.mime ?? "text/plain",
|
||||
filename: input.name,
|
||||
url: input.uri,
|
||||
source: input.source?.path
|
||||
? {
|
||||
type: "file" as const,
|
||||
path: input.source.path,
|
||||
text: { value: input.source.text, start: input.source.start, end: input.source.end },
|
||||
}
|
||||
: undefined,
|
||||
}
|
||||
}
|
||||
|
||||
function toPromptParts(input: PromptInput) {
|
||||
return [
|
||||
{ type: "text" as const, text: input.text },
|
||||
...(input.files?.map(toFilePart) ?? []),
|
||||
...(input.agents?.map((agent) => ({
|
||||
type: "agent" as const,
|
||||
name: agent.name,
|
||||
source:
|
||||
agent.text !== undefined && agent.start !== undefined && agent.end !== undefined
|
||||
? { value: agent.text, start: agent.start, end: agent.end }
|
||||
: undefined,
|
||||
})) ?? []),
|
||||
]
|
||||
}
|
||||
|
||||
function toFileContent(input: import("@opencode-ai/sdk-v1/v2/client").FileContent): FileContent {
|
||||
if (input.encoding !== "base64") {
|
||||
return { bytes: new TextEncoder().encode(input.content), kind: input.type, mimeType: input.mimeType }
|
||||
}
|
||||
return {
|
||||
bytes: Uint8Array.from(atob(input.content), (character) => character.charCodeAt(0)),
|
||||
kind: input.type,
|
||||
mimeType: input.mimeType,
|
||||
}
|
||||
}
|
||||
|
||||
function toDecoratedFile(input: import("@opencode-ai/sdk-v1/v2/client").FileContent): DecoratedFileContent {
|
||||
return {
|
||||
type: input.type,
|
||||
content: input.content,
|
||||
diff: input.diff,
|
||||
encoding: input.encoding,
|
||||
mimeType: input.mimeType,
|
||||
patch: input.patch && { hunks: input.patch.hunks.map((hunk) => ({ lines: hunk.lines })) },
|
||||
}
|
||||
}
|
||||
|
||||
function toPermission(input: import("@opencode-ai/sdk-v1/v2/client").PermissionRequest): AppPermissionRequest {
|
||||
return {
|
||||
id: input.id,
|
||||
sessionID: input.sessionID,
|
||||
action: input.permission,
|
||||
resources: input.patterns,
|
||||
metadata: input.metadata,
|
||||
}
|
||||
}
|
||||
|
||||
function toQuestion(input: import("@opencode-ai/sdk-v1/v2/client").QuestionRequest): AppQuestionRequest {
|
||||
return {
|
||||
id: input.id,
|
||||
sessionID: input.sessionID,
|
||||
questions: input.questions,
|
||||
}
|
||||
}
|
||||
|
||||
function toPty(input: import("@opencode-ai/sdk-v1/v2/client").Pty) {
|
||||
return { id: input.id, title: input.title }
|
||||
}
|
||||
|
||||
async function toEvent(
|
||||
envelope: GlobalEvent,
|
||||
messages: Map<string, CachedMessage>,
|
||||
loadMessage: (input: { sessionID: string; messageID: string }) => Promise<CachedMessage>,
|
||||
): Promise<AppEvent> {
|
||||
const input = envelope.payload as Event
|
||||
if (input.type === "server.connected") return { type: input.type }
|
||||
if (input.type === "global.disposed") return { type: "server.disposed" }
|
||||
if (input.type === "server.instance.disposed")
|
||||
return { type: "server.disposed", location: { directory: input.properties.directory } }
|
||||
if (input.type === "project.updated") return { type: input.type, project: toProject(input.properties) }
|
||||
if (input.type === "session.created" || input.type === "session.updated")
|
||||
return { type: input.type, session: toSession(input.properties.info) }
|
||||
if (input.type === "session.deleted") return { type: input.type, sessionID: input.properties.sessionID }
|
||||
if (input.type === "session.status") {
|
||||
const activity = toActivity(input.properties.status)
|
||||
if (activity) return { type: "session.activity", sessionID: input.properties.sessionID, activity }
|
||||
return { type: "unknown", raw: input }
|
||||
}
|
||||
if (input.type === "session.error")
|
||||
return { type: input.type, sessionID: input.properties.sessionID, error: input.properties.error }
|
||||
if (input.type === "message.updated") {
|
||||
const cached = messages.get(input.properties.info.id)
|
||||
const value = { info: input.properties.info, parts: cached?.parts ?? [] }
|
||||
messages.set(input.properties.info.id, value)
|
||||
return { type: "timeline.updated", item: toTimelineItem(value.info, value.parts) }
|
||||
}
|
||||
if (input.type === "message.part.updated") {
|
||||
const messageID = input.properties.part.messageID
|
||||
const value = await loadMessage({ sessionID: input.properties.sessionID, messageID })
|
||||
const parts = [...value.parts.filter((part) => part.id !== input.properties.part.id), input.properties.part]
|
||||
const next = { info: value.info, parts }
|
||||
messages.set(messageID, next)
|
||||
return { type: "timeline.updated", item: toTimelineItem(next.info, next.parts) }
|
||||
}
|
||||
if (input.type === "message.removed") {
|
||||
messages.delete(input.properties.messageID)
|
||||
return { type: "timeline.removed", sessionID: input.properties.sessionID, itemID: input.properties.messageID }
|
||||
}
|
||||
if (input.type === "message.part.removed") {
|
||||
const value = await loadMessage({ sessionID: input.properties.sessionID, messageID: input.properties.messageID })
|
||||
const next = { info: value.info, parts: value.parts.filter((part) => part.id !== input.properties.partID) }
|
||||
messages.set(input.properties.messageID, next)
|
||||
return { type: "timeline.updated", item: toTimelineItem(next.info, next.parts) }
|
||||
}
|
||||
if (input.type === "permission.asked")
|
||||
return { type: "permission.requested", request: toPermission(input.properties) }
|
||||
if (input.type === "permission.v2.asked")
|
||||
return {
|
||||
type: "permission.requested",
|
||||
request: {
|
||||
id: input.properties.id,
|
||||
sessionID: input.properties.sessionID,
|
||||
action: input.properties.action,
|
||||
resources: input.properties.resources,
|
||||
metadata: input.properties.metadata,
|
||||
},
|
||||
}
|
||||
if (input.type === "permission.replied" || input.type === "permission.v2.replied")
|
||||
return {
|
||||
type: "permission.replied",
|
||||
sessionID: input.properties.sessionID,
|
||||
requestID: input.properties.requestID,
|
||||
}
|
||||
if (input.type === "question.asked" || input.type === "question.v2.asked")
|
||||
return { type: "question.requested", request: toQuestion(input.properties) }
|
||||
if (input.type === "question.replied" || input.type === "question.v2.replied")
|
||||
return { type: "question.replied", sessionID: input.properties.sessionID, requestID: input.properties.requestID }
|
||||
if (input.type === "question.rejected" || input.type === "question.v2.rejected")
|
||||
return { type: "question.rejected", sessionID: input.properties.sessionID, requestID: input.properties.requestID }
|
||||
if (input.type === "file.watcher.updated")
|
||||
return { type: "file.changed", path: input.properties.file, change: input.properties.event }
|
||||
if (input.type === "vcs.branch.updated") return { type: input.type, branch: input.properties.branch }
|
||||
if (input.type === "pty.exited") return { type: input.type, ptyID: input.properties.id }
|
||||
return { type: "unknown", raw: input }
|
||||
}
|
||||
|
||||
function toConfig(input: Config): AppConfig {
|
||||
return {
|
||||
shell: input.shell,
|
||||
model: input.model,
|
||||
share: input.share,
|
||||
plugin: input.plugin,
|
||||
disabledProviders: input.disabled_providers,
|
||||
provider: input.provider,
|
||||
permission: input.permission,
|
||||
}
|
||||
}
|
||||
|
||||
function fromConfig(input: AppConfig): Config {
|
||||
return {
|
||||
shell: input.shell,
|
||||
model: input.model,
|
||||
share: input.share,
|
||||
plugin: input.plugin?.map((plugin): NonNullable<Config["plugin"]>[number] =>
|
||||
typeof plugin === "string" ? plugin : [plugin[0], { ...plugin[1] }],
|
||||
),
|
||||
disabled_providers: input.disabledProviders ? [...input.disabledProviders] : undefined,
|
||||
provider: input.provider as Config["provider"],
|
||||
permission: input.permission as Config["permission"],
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { OpenCode } from "@opencode-ai/client"
|
||||
import { createV2Backend } from "./backend-v2"
|
||||
|
||||
function setup(respond: (request: Request) => Response | Promise<Response>) {
|
||||
const requests: Request[] = []
|
||||
const fetch = Object.assign(
|
||||
async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const request = input instanceof Request ? input : new Request(input, init)
|
||||
requests.push(request)
|
||||
return respond(request)
|
||||
},
|
||||
{ preconnect: globalThis.fetch.preconnect },
|
||||
) satisfies typeof globalThis.fetch
|
||||
return {
|
||||
requests,
|
||||
backend: createV2Backend(OpenCode.make({ baseUrl: "http://localhost", fetch }), {
|
||||
directory: "/default",
|
||||
workspaceID: "default-workspace",
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
function json(data: unknown) {
|
||||
return new Response(JSON.stringify(data), { headers: { "content-type": "application/json" } })
|
||||
}
|
||||
|
||||
const session = {
|
||||
id: "ses_1",
|
||||
projectID: "project",
|
||||
cost: 1.5,
|
||||
tokens: { input: 1, output: 2, reasoning: 3, cache: { read: 4, write: 5 } },
|
||||
time: { created: 10, updated: 20 },
|
||||
title: "Session",
|
||||
location: { directory: "/repo", workspaceID: "workspace" },
|
||||
}
|
||||
|
||||
describe("createV2Backend", () => {
|
||||
test("normalizes session pages and preserves location precedence", async () => {
|
||||
const setupResult = setup(() => json({ data: [session], cursor: { previous: "before", next: "after" } }))
|
||||
|
||||
const result = await setupResult.backend.common.sessions.list({
|
||||
location: { directory: "/explicit", workspaceID: "explicit-workspace" },
|
||||
roots: true,
|
||||
limit: 10,
|
||||
cursor: "cursor",
|
||||
})
|
||||
|
||||
expect(result).toEqual({
|
||||
items: [
|
||||
{
|
||||
id: "ses_1",
|
||||
parentID: undefined,
|
||||
projectID: "project",
|
||||
location: { directory: "/repo", workspaceID: "workspace" },
|
||||
title: "Session",
|
||||
cost: 1.5,
|
||||
tokens: { input: 1, output: 2, reasoning: 3, cache: { read: 4, write: 5 } },
|
||||
time: { created: 10, updated: 20 },
|
||||
revert: undefined,
|
||||
},
|
||||
],
|
||||
previous: "before",
|
||||
next: "after",
|
||||
})
|
||||
const url = new URL(setupResult.requests[0].url)
|
||||
expect(url.pathname).toBe("/api/session")
|
||||
expect(url.searchParams.get("directory")).toBe("/explicit")
|
||||
expect(url.searchParams.get("workspace")).toBe("explicit-workspace")
|
||||
expect(url.searchParams.get("parentID")).toBe("null")
|
||||
expect(url.searchParams.get("cursor")).toBe("cursor")
|
||||
})
|
||||
|
||||
test("uses the default location and maps binary file responses", async () => {
|
||||
const setupResult = setup(() => new Response(Uint8Array.from([0, 1, 2])))
|
||||
|
||||
const result = await setupResult.backend.common.files.read({ path: "dir/a.bin" })
|
||||
|
||||
expect([...result.bytes]).toEqual([0, 1, 2])
|
||||
const url = new URL(setupResult.requests[0].url)
|
||||
expect(url.pathname).toBe("/api/fs/read/dir/a.bin")
|
||||
expect(url.searchParams.get("location[directory]")).toBe("/default")
|
||||
expect(url.searchParams.get("location[workspace]")).toBe("default-workspace")
|
||||
})
|
||||
|
||||
test("switches prompt selection before admission", async () => {
|
||||
const setupResult = setup((request) =>
|
||||
request.url.endsWith("/prompt")
|
||||
? json({
|
||||
data: {
|
||||
admittedSeq: 1,
|
||||
id: "msg_1",
|
||||
sessionID: "ses_1",
|
||||
timeCreated: 1,
|
||||
type: "user",
|
||||
data: { text: "hello" },
|
||||
delivery: "steer",
|
||||
},
|
||||
})
|
||||
: new Response(null, { status: 204 }),
|
||||
)
|
||||
|
||||
await setupResult.backend.common.sessions.prompt({
|
||||
sessionID: "ses_1",
|
||||
id: "msg_1",
|
||||
text: "hello",
|
||||
selection: { agent: "build", model: { id: "model", providerID: "provider", variant: "high" } },
|
||||
files: [{ uri: "data:text/plain;base64,aGk=", name: "hi.txt", source: { text: "hi", start: 0, end: 2 } }],
|
||||
})
|
||||
|
||||
expect(setupResult.requests.map((item) => new URL(item.url).pathname)).toEqual([
|
||||
"/api/session/ses_1/agent",
|
||||
"/api/session/ses_1/model",
|
||||
"/api/session/ses_1/prompt",
|
||||
])
|
||||
expect(await setupResult.requests[2].json()).toEqual({
|
||||
id: "msg_1",
|
||||
text: "hello",
|
||||
files: [
|
||||
{
|
||||
uri: "data:text/plain;base64,aGk=",
|
||||
name: "hi.txt",
|
||||
mention: { start: 0, end: 2, text: "hi" },
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
test("requests file staging when selected revert files are present", async () => {
|
||||
const setupResult = setup(() => json({ data: { messageID: "msg_1", files: [] } }))
|
||||
|
||||
await setupResult.backend.capabilities.sessionExtrasV2?.stageRevert({
|
||||
sessionID: "ses_1",
|
||||
messageID: "msg_1",
|
||||
files: ["a.txt"],
|
||||
})
|
||||
|
||||
expect(await setupResult.requests[0].json()).toEqual({ messageID: "msg_1", files: true })
|
||||
})
|
||||
|
||||
test("normalizes retry events before message projection refresh", async () => {
|
||||
const setupResult = setup(() =>
|
||||
new Response(
|
||||
`data: ${JSON.stringify({
|
||||
id: "evt_retry",
|
||||
created: 1,
|
||||
type: "session.retry.scheduled",
|
||||
durable: { aggregateID: "ses_1", seq: 3, version: 1 },
|
||||
location: { directory: "/repo" },
|
||||
data: {
|
||||
sessionID: "ses_1",
|
||||
assistantMessageID: "msg_1",
|
||||
attempt: 2,
|
||||
at: 1234,
|
||||
error: { type: "rate_limit", message: "try again" },
|
||||
},
|
||||
})}\n\n`,
|
||||
{ headers: { "content-type": "text/event-stream" } },
|
||||
),
|
||||
)
|
||||
|
||||
const result = await setupResult.backend.common.events.subscribe()[Symbol.asyncIterator]().next()
|
||||
|
||||
expect(result.value).toEqual({
|
||||
location: { directory: "/repo" },
|
||||
event: {
|
||||
type: "session.activity",
|
||||
sessionID: "ses_1",
|
||||
activity: { type: "retry", attempt: 2, message: "try again", next: 1234 },
|
||||
},
|
||||
})
|
||||
expect(setupResult.requests).toHaveLength(1)
|
||||
})
|
||||
|
||||
test("normalizes durable session log events through the event mapper", async () => {
|
||||
const setupResult = setup(() =>
|
||||
new Response(
|
||||
`data: ${JSON.stringify({
|
||||
id: "evt_started",
|
||||
created: 1,
|
||||
type: "session.execution.started",
|
||||
durable: { aggregateID: "ses_1", seq: 7, version: 1 },
|
||||
data: { sessionID: "ses_1" },
|
||||
})}\n\n`,
|
||||
{ headers: { "content-type": "text/event-stream" } },
|
||||
),
|
||||
)
|
||||
const capability = setupResult.backend.capabilities.sessionExtrasV2
|
||||
if (!capability) throw new Error("Missing V2 session capability")
|
||||
|
||||
const result = await capability.log({ sessionID: "ses_1" })[Symbol.asyncIterator]().next()
|
||||
|
||||
expect(result.value).toEqual({
|
||||
sequence: 7,
|
||||
event: { type: "session.activity", sessionID: "ses_1", activity: { type: "running" } },
|
||||
})
|
||||
expect(new URL(setupResult.requests[0].url).pathname).toBe("/api/experimental/session/ses_1/log")
|
||||
})
|
||||
|
||||
test("refreshes message projections for streamed V2 fragments", async () => {
|
||||
const setupResult = setup((request) => {
|
||||
if (new URL(request.url).pathname === "/api/event") {
|
||||
return new Response(
|
||||
`data: ${JSON.stringify({
|
||||
id: "evt_1",
|
||||
created: 1,
|
||||
type: "session.text.delta",
|
||||
location: { directory: "/repo" },
|
||||
data: { sessionID: "ses_1", assistantMessageID: "msg_1", ordinal: 0, delta: "hi" },
|
||||
})}\n\n`,
|
||||
{ headers: { "content-type": "text/event-stream" } },
|
||||
)
|
||||
}
|
||||
return json({
|
||||
data: {
|
||||
id: "msg_1",
|
||||
type: "assistant",
|
||||
time: { created: 1 },
|
||||
agent: "build",
|
||||
model: { id: "model", providerID: "provider" },
|
||||
content: [{ type: "text", text: "hello" }],
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
const result = await setupResult.backend.common.events.subscribe()[Symbol.asyncIterator]().next()
|
||||
|
||||
expect(result.value).toEqual({
|
||||
location: { directory: "/repo" },
|
||||
event: {
|
||||
type: "timeline.updated",
|
||||
item: {
|
||||
type: "assistant",
|
||||
id: "msg_1",
|
||||
sessionID: "ses_1",
|
||||
created: 1,
|
||||
completed: undefined,
|
||||
content: [{ type: "text", id: "msg_1:text:0", text: "hello" }],
|
||||
agent: "build",
|
||||
model: { id: "model", providerID: "provider" },
|
||||
tokens: undefined,
|
||||
error: undefined,
|
||||
raw: {
|
||||
id: "msg_1",
|
||||
type: "assistant",
|
||||
time: { created: 1 },
|
||||
agent: "build",
|
||||
model: { id: "model", providerID: "provider" },
|
||||
content: [{ type: "text", text: "hello" }],
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
expect(new URL(setupResult.requests[1].url).pathname).toBe("/api/session/ses_1/message/msg_1")
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,970 @@
|
||||
import type {
|
||||
AgentInfo,
|
||||
FormCreateInput,
|
||||
FormInfo,
|
||||
IntegrationInfo,
|
||||
McpServer,
|
||||
ModelInfo,
|
||||
OpenCodeClient,
|
||||
OpenCodeEvent,
|
||||
PermissionV2Request,
|
||||
Project,
|
||||
QuestionV2Request,
|
||||
SessionInfo,
|
||||
SessionMessageAssistant,
|
||||
SessionMessageInfo,
|
||||
SessionPendingInfo,
|
||||
ShellInfo1,
|
||||
} from "@opencode-ai/client"
|
||||
import type {
|
||||
AppAgent,
|
||||
AppClient,
|
||||
AppEvent,
|
||||
AppFileDiff,
|
||||
AppMcpServer,
|
||||
AppModel,
|
||||
AppPermissionRequest,
|
||||
AppProject,
|
||||
AppQuestionRequest,
|
||||
AppSession,
|
||||
FormField,
|
||||
IntegrationAttempt,
|
||||
IntegrationAttemptStatus,
|
||||
IntegrationConnection,
|
||||
IntegrationMethod,
|
||||
LocationInput,
|
||||
LocationRef,
|
||||
PendingSessionInput,
|
||||
PromptFile,
|
||||
RequestOptions,
|
||||
SessionActivity,
|
||||
SessionLogItem,
|
||||
ShellProcess,
|
||||
TimelineContent,
|
||||
TimelineItem,
|
||||
} from "./backend"
|
||||
|
||||
export function createV2Backend(client: OpenCodeClient, defaultLocation?: LocationRef): AppClient {
|
||||
const request = (input?: RequestOptions) => ({ signal: input?.signal })
|
||||
const location = (input?: LocationInput) => toLocation(input?.location ?? defaultLocation)
|
||||
|
||||
const loadSession = async (sessionID: string, options?: RequestOptions) =>
|
||||
toSession(await client.session.get({ sessionID }, request(options)))
|
||||
const loadMessage = async (sessionID: string, messageID: string, options?: RequestOptions) =>
|
||||
toTimelineItem(await client.session.message({ sessionID, messageID }, request(options)), sessionID)
|
||||
|
||||
return {
|
||||
version: "v2",
|
||||
common: {
|
||||
health: { get: (options) => client.health.get(request(options)) },
|
||||
projects: {
|
||||
list: async (options) => (await client.project.list(request(options))).map(toProject),
|
||||
current: (input, options) => client.project.current({ location: location(input) }, request(options)),
|
||||
},
|
||||
catalog: {
|
||||
providers: async (input, options) => {
|
||||
const params = { location: location(input) }
|
||||
const [providers, models, selected] = await Promise.all([
|
||||
client.provider.list(params, request(options)),
|
||||
client.model.list(params, request(options)),
|
||||
client.model.default(params, request(options)),
|
||||
])
|
||||
return {
|
||||
providers: new Map(
|
||||
providers.data.map((provider) => [
|
||||
provider.id,
|
||||
{
|
||||
id: provider.id,
|
||||
name: provider.name,
|
||||
models: Object.fromEntries(
|
||||
models.data
|
||||
.filter((model) => model.providerID === provider.id && model.status !== "deprecated")
|
||||
.map((model) => [model.id, toModel(model)]),
|
||||
),
|
||||
},
|
||||
]),
|
||||
),
|
||||
connected: providers.data.filter((provider) => !provider.disabled).map((provider) => provider.id),
|
||||
defaults: selected.data ? { [selected.data.providerID]: selected.data.id } : {},
|
||||
}
|
||||
},
|
||||
agents: async (input, options) =>
|
||||
(await client.agent.list({ location: location(input) }, request(options))).data.map(toAgent),
|
||||
},
|
||||
commands: {
|
||||
list: async (input, options) =>
|
||||
(await client.command.list({ location: location(input) }, request(options))).data.map((item) => ({
|
||||
name: item.name,
|
||||
description: item.description,
|
||||
})),
|
||||
},
|
||||
references: {
|
||||
list: async (input, options) =>
|
||||
(await client.reference.list({ location: location(input) }, request(options))).data.map((item) => ({
|
||||
name: item.name,
|
||||
path: item.path,
|
||||
description: item.description,
|
||||
hidden: item.hidden,
|
||||
source: item.source,
|
||||
})),
|
||||
},
|
||||
sessions: {
|
||||
list: async (input, options) => {
|
||||
const result = await client.session.list(
|
||||
{
|
||||
workspace: input?.location?.workspaceID ?? defaultLocation?.workspaceID,
|
||||
directory: input?.location?.directory ?? defaultLocation?.directory,
|
||||
parentID: input?.roots ? null : undefined,
|
||||
limit: input?.limit,
|
||||
search: input?.search,
|
||||
cursor: input?.cursor,
|
||||
},
|
||||
request(options),
|
||||
)
|
||||
return {
|
||||
items: result.data.map(toSession),
|
||||
previous: result.cursor.previous ?? undefined,
|
||||
next: result.cursor.next ?? undefined,
|
||||
}
|
||||
},
|
||||
create: async (input, options) =>
|
||||
toSession(
|
||||
await client.session.create(
|
||||
{ location: input?.location ?? defaultLocation, agent: input?.agent, model: input?.model },
|
||||
request(options),
|
||||
),
|
||||
),
|
||||
get: (input, options) => loadSession(input.sessionID, options),
|
||||
remove: async (input, options) => {
|
||||
await client.session.remove({ sessionID: input.sessionID }, request(options))
|
||||
},
|
||||
fork: async (input, options) =>
|
||||
toSession(await client.session.fork({ sessionID: input.sessionID, messageID: input.messageID }, request(options))),
|
||||
rename: async (input, options) => {
|
||||
await client.session.rename({ sessionID: input.sessionID, title: input.title }, request(options))
|
||||
},
|
||||
interrupt: async (input, options) => {
|
||||
await client.session.interrupt({ sessionID: input.sessionID }, request(options))
|
||||
},
|
||||
activity: async (_input, options) => {
|
||||
const result = await client.session.active(request(options))
|
||||
return Object.fromEntries(Object.keys(result).map((sessionID) => [sessionID, { type: "running" } as const]))
|
||||
},
|
||||
history: async (input, options) => {
|
||||
const result = await client.message.list(
|
||||
{ sessionID: input.sessionID, limit: input.limit, cursor: input.cursor },
|
||||
request(options),
|
||||
)
|
||||
return {
|
||||
items: result.data.map((item) => toTimelineItem(item, input.sessionID)),
|
||||
previous: result.cursor.previous ?? undefined,
|
||||
next: result.cursor.next ?? undefined,
|
||||
}
|
||||
},
|
||||
message: (input, options) => loadMessage(input.sessionID, input.messageID, options),
|
||||
prompt: async (input, options) => {
|
||||
if (input.selection?.agent)
|
||||
await client.session.switchAgent(
|
||||
{ sessionID: input.sessionID, agent: input.selection.agent },
|
||||
request(options),
|
||||
)
|
||||
if (input.selection?.model)
|
||||
await client.session.switchModel(
|
||||
{ sessionID: input.sessionID, model: input.selection.model },
|
||||
request(options),
|
||||
)
|
||||
await client.session.prompt(
|
||||
{
|
||||
sessionID: input.sessionID,
|
||||
id: input.id,
|
||||
text: input.text,
|
||||
files: input.files?.map(toPromptFile),
|
||||
agents: input.agents?.map((agent) => ({
|
||||
name: agent.name,
|
||||
mention:
|
||||
agent.start === undefined || agent.end === undefined || agent.text === undefined
|
||||
? undefined
|
||||
: { start: agent.start, end: agent.end, text: agent.text },
|
||||
})),
|
||||
delivery: input.delivery,
|
||||
},
|
||||
request(options),
|
||||
)
|
||||
},
|
||||
command: async (input, options) => {
|
||||
await client.session.command(
|
||||
{
|
||||
sessionID: input.sessionID,
|
||||
id: input.id,
|
||||
command: input.command,
|
||||
arguments: input.arguments,
|
||||
agent: input.agent,
|
||||
model: input.model,
|
||||
files: input.files?.map(toPromptFile),
|
||||
delivery: input.delivery,
|
||||
},
|
||||
request(options),
|
||||
)
|
||||
},
|
||||
},
|
||||
files: {
|
||||
list: async (input, options) =>
|
||||
(await client.file.list({ location: location(input), path: input.path }, request(options))).data,
|
||||
find: async (input, options) =>
|
||||
(
|
||||
await client.file.find(
|
||||
{ location: location(input), query: input.query, type: input.type, limit: input.limit },
|
||||
request(options),
|
||||
)
|
||||
).data,
|
||||
read: async (input, options) => ({
|
||||
bytes: await client.file.read({ location: location(input), path: input.path }, request(options)),
|
||||
}),
|
||||
},
|
||||
permissions: {
|
||||
pending: async (input, options) =>
|
||||
(await client.permission.request.list({ location: location(input) }, request(options))).data.map(toPermission),
|
||||
reply: async (input, options) => {
|
||||
await client.permission.reply(input, request(options))
|
||||
},
|
||||
},
|
||||
questions: {
|
||||
pending: async (input, options) =>
|
||||
(await client.question.request.list({ location: location(input) }, request(options))).data.map(toQuestion),
|
||||
reply: async (input, options) => {
|
||||
await client.question.reply({ ...input, answers: input.answers.map((answer) => [...answer]) }, request(options))
|
||||
},
|
||||
reject: async (input, options) => {
|
||||
await client.question.reject(input, request(options))
|
||||
},
|
||||
},
|
||||
vcs: {
|
||||
status: async (input, options) =>
|
||||
(await client.vcs.status({ location: location(input) }, request(options))).data,
|
||||
diff: async (input, options) =>
|
||||
(await client.vcs.diff({ ...input, location: location(input) }, request(options))).data.map(toFileDiff),
|
||||
},
|
||||
mcp: {
|
||||
list: async (input, options) =>
|
||||
(await client.mcp.list({ location: location(input) }, request(options))).data.map(toMcpServer),
|
||||
resources: async (input, options) =>
|
||||
(await client.mcp.resource.catalog({ location: location(input) }, request(options))).data,
|
||||
},
|
||||
pty: {
|
||||
list: async (input, options) =>
|
||||
(await client.pty.list({ location: location(input) }, request(options))).data.map(toPty),
|
||||
create: async (input, options) =>
|
||||
toPty(
|
||||
(
|
||||
await client.pty.create(
|
||||
{
|
||||
location: location(input),
|
||||
title: input.title,
|
||||
command: input.command,
|
||||
args: input.args,
|
||||
cwd: input.cwd,
|
||||
env: input.env,
|
||||
},
|
||||
request(options),
|
||||
)
|
||||
).data,
|
||||
),
|
||||
get: async (input, options) =>
|
||||
toPty((await client.pty.get({ ptyID: input.ptyID, location: location(input) }, request(options))).data),
|
||||
update: async (input, options) =>
|
||||
toPty(
|
||||
(
|
||||
await client.pty.update(
|
||||
{ ptyID: input.ptyID, location: location(input), title: input.title, size: input.size },
|
||||
request(options),
|
||||
)
|
||||
).data,
|
||||
),
|
||||
remove: async (input, options) => {
|
||||
await client.pty.remove({ ptyID: input.ptyID, location: location(input) }, request(options))
|
||||
},
|
||||
},
|
||||
events: {
|
||||
subscribe: (options) => ({
|
||||
async *[Symbol.asyncIterator]() {
|
||||
for await (const input of client.event.subscribe(request(options))) {
|
||||
yield {
|
||||
location: input.location,
|
||||
event: await toEvent(input, loadSession, loadMessage, options),
|
||||
}
|
||||
}
|
||||
},
|
||||
}),
|
||||
},
|
||||
disposeLocation: async (input, options) => {
|
||||
await client.debug.location.evict({ location: location(input) }, request(options))
|
||||
},
|
||||
},
|
||||
capabilities: {
|
||||
integrationsV2: {
|
||||
list: async (input, options) =>
|
||||
(await client.integration.list({ location: location(input) }, request(options))).data.map(toIntegration),
|
||||
get: async (input, options) => {
|
||||
const result = await client.integration.get(
|
||||
{ integrationID: input.integrationID, location: location(input) },
|
||||
request(options),
|
||||
)
|
||||
return result.data ? toIntegration(result.data) : null
|
||||
},
|
||||
connectKey: async (input, options) => {
|
||||
await client.integration.connect.key({ ...input, location: location(input) }, request(options))
|
||||
},
|
||||
connectOauth: async (input, options) =>
|
||||
toIntegrationAttempt(
|
||||
(
|
||||
await client.integration.connect.oauth(
|
||||
{
|
||||
integrationID: input.integrationID,
|
||||
methodID: input.methodID,
|
||||
inputs: input.values,
|
||||
label: input.label,
|
||||
location: location(input),
|
||||
},
|
||||
request(options),
|
||||
)
|
||||
).data,
|
||||
),
|
||||
attemptStatus: async (input, options) =>
|
||||
toAttemptStatus(
|
||||
(
|
||||
await client.integration.attempt.status(
|
||||
{ attemptID: input.attemptID, location: location(input) },
|
||||
request(options),
|
||||
)
|
||||
).data,
|
||||
),
|
||||
completeAttempt: async (input, options) => {
|
||||
await client.integration.attempt.complete({ ...input, location: location(input) }, request(options))
|
||||
},
|
||||
cancelAttempt: async (input, options) => {
|
||||
await client.integration.attempt.cancel({ ...input, location: location(input) }, request(options))
|
||||
},
|
||||
renameCredential: async (input, options) => {
|
||||
await client.credential.update({ ...input, location: location(input) }, request(options))
|
||||
},
|
||||
removeCredential: async (input, options) => {
|
||||
await client.credential.remove({ ...input, location: location(input) }, request(options))
|
||||
},
|
||||
},
|
||||
sessionExtrasV2: {
|
||||
switchAgent: async (input, options) => {
|
||||
await client.session.switchAgent(input, request(options))
|
||||
},
|
||||
switchModel: async (input, options) => {
|
||||
await client.session.switchModel(input, request(options))
|
||||
},
|
||||
move: async (input, options) => {
|
||||
await client.session.move(
|
||||
{ sessionID: input.sessionID, destination: { directory: input.directory }, moveChanges: input.moveChanges },
|
||||
request(options),
|
||||
)
|
||||
},
|
||||
skill: async (input, options) => {
|
||||
await client.session.skill(input, request(options))
|
||||
},
|
||||
synthetic: async (input, options) =>
|
||||
toPending(
|
||||
await client.session.synthetic(
|
||||
{ ...input, metadata: input.metadata && toClientRecord(input.metadata) },
|
||||
request(options),
|
||||
),
|
||||
),
|
||||
shell: async (input, options) => {
|
||||
await client.session.shell(input, request(options))
|
||||
},
|
||||
compact: async (input, options) => toPending(await client.session.compact(input, request(options))),
|
||||
wait: async (input, options) => {
|
||||
await client.session.wait(input, request(options))
|
||||
},
|
||||
context: async (input, options) =>
|
||||
(await client.session.context(input, request(options))).map((item) => toTimelineItem(item, input.sessionID)),
|
||||
pending: async (input, options) =>
|
||||
(await client.session.pending.list(input, request(options))).map(toPending),
|
||||
instructionEntries: (input, options) => client.session.instructions.entry.list(input, request(options)),
|
||||
putInstructionEntry: async (input, options) => {
|
||||
await client.session.instructions.entry.put({ ...input, value: toClientJson(input.value) }, request(options))
|
||||
},
|
||||
removeInstructionEntry: async (input, options) => {
|
||||
await client.session.instructions.entry.remove(input, request(options))
|
||||
},
|
||||
log: (input, options) => ({
|
||||
async *[Symbol.asyncIterator]() {
|
||||
for await (const item of client.session.log(input, request(options))) {
|
||||
yield {
|
||||
sequence: "durable" in item ? item.durable.seq : (item.seq ?? 0),
|
||||
event: await toLogEvent(item, loadSession, loadMessage, options),
|
||||
} satisfies SessionLogItem
|
||||
}
|
||||
},
|
||||
}),
|
||||
background: async (input, options) => {
|
||||
await client.session.background(input, request(options))
|
||||
},
|
||||
stageRevert: async (input, options) => {
|
||||
const result = await client.session.revert.stage(
|
||||
{ sessionID: input.sessionID, messageID: input.messageID, files: input.files?.length ? true : undefined },
|
||||
request(options),
|
||||
)
|
||||
return { messageID: result.messageID }
|
||||
},
|
||||
clearRevert: async (input, options) => {
|
||||
await client.session.revert.clear(input, request(options))
|
||||
},
|
||||
commitRevert: async (input, options) => {
|
||||
await client.session.revert.commit(input, request(options))
|
||||
},
|
||||
},
|
||||
projectCopiesV2: {
|
||||
directories: (input, options) =>
|
||||
client.project.directories({ projectID: input.projectID, location: location(input) }, request(options)),
|
||||
create: (input, options) => client.projectCopy.create({ ...input, location: location(input) }, request(options)),
|
||||
remove: async (input, options) => {
|
||||
await client.projectCopy.remove({ ...input, location: location(input) }, request(options))
|
||||
},
|
||||
refresh: async (input, options) => {
|
||||
await client.projectCopy.refresh({ ...input, location: location(input) }, request(options))
|
||||
},
|
||||
},
|
||||
formsV2: {
|
||||
pending: async (input, options) =>
|
||||
(await client.form.request.list({ location: location(input) }, request(options))).data.map(toForm),
|
||||
list: async (input, options) => (await client.form.list(input, request(options))).map(toForm),
|
||||
create: async (input, options) => {
|
||||
if (input.fields.length === 0) throw new Error("A form requires at least one field")
|
||||
const fields: FormCreateInput["fields"] = [
|
||||
fromFormField(input.fields[0]),
|
||||
...input.fields.slice(1).map(fromFormField),
|
||||
]
|
||||
return toForm(
|
||||
await client.form.create(
|
||||
{ ...input, metadata: input.metadata && toClientRecord(input.metadata), fields },
|
||||
request(options),
|
||||
),
|
||||
)
|
||||
},
|
||||
get: async (input, options) => toForm(await client.form.get(input, request(options))),
|
||||
state: (input, options) => client.form.state(input, request(options)),
|
||||
reply: async (input, options) => {
|
||||
await client.form.reply({ ...input, answer: { ...input.answer } }, request(options))
|
||||
},
|
||||
cancel: async (input, options) => {
|
||||
await client.form.cancel(input, request(options))
|
||||
},
|
||||
},
|
||||
savedPermissionsV2: {
|
||||
list: (input, options) => client.permission.saved.list(input, request(options)),
|
||||
remove: async (input, options) => {
|
||||
await client.permission.saved.remove(input, request(options))
|
||||
},
|
||||
},
|
||||
shellsV2: {
|
||||
list: async (input, options) =>
|
||||
(await client.shell.list({ location: location(input) }, request(options))).data.map(toShell),
|
||||
create: async (input, options) =>
|
||||
toShell(
|
||||
(
|
||||
await client.shell.create(
|
||||
{
|
||||
...input,
|
||||
metadata: input.metadata && toClientRecord(input.metadata),
|
||||
location: location(input),
|
||||
},
|
||||
request(options),
|
||||
)
|
||||
).data,
|
||||
),
|
||||
get: async (input, options) =>
|
||||
toShell((await client.shell.get({ id: input.id, location: location(input) }, request(options))).data),
|
||||
setTimeout: async (input, options) =>
|
||||
toShell(
|
||||
(
|
||||
await client.shell.timeout(
|
||||
{ id: input.id, timeout: input.timeout, location: location(input) },
|
||||
request(options),
|
||||
)
|
||||
).data,
|
||||
),
|
||||
output: async (input, options) =>
|
||||
(
|
||||
await client.shell.output(
|
||||
{ id: input.id, cursor: input.cursor, limit: input.limit, location: location(input) },
|
||||
request(options),
|
||||
)
|
||||
).data,
|
||||
remove: async (input, options) => {
|
||||
await client.shell.remove({ id: input.id, location: location(input) }, request(options))
|
||||
},
|
||||
},
|
||||
discoveryV2: {
|
||||
server: (options) => client.server.get(request(options)),
|
||||
location: (input, options) => client.location.get({ location: location(input) }, request(options)),
|
||||
plugins: async (input, options) =>
|
||||
(await client.plugin.list({ location: location(input) }, request(options))).data,
|
||||
skills: async (input, options) => {
|
||||
const result = await client.skill.list({ location: location(input) }, request(options))
|
||||
return result.data.map((item) => ({
|
||||
...item,
|
||||
location: { directory: item.location },
|
||||
}))
|
||||
},
|
||||
models: async (input, options) =>
|
||||
(await client.model.list({ location: location(input) }, request(options))).data.map(toModel),
|
||||
defaultModel: async (input, options) => {
|
||||
const result = await client.model.default({ location: location(input) }, request(options))
|
||||
return result.data ? toModel(result.data) : null
|
||||
},
|
||||
generateText: async (input, options) =>
|
||||
(await client.generate.text({ ...input, location: location(input) }, request(options))).text,
|
||||
loadedLocations: (options) => client.debug.location.list(request(options)),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function toLocation(input?: LocationRef) {
|
||||
if (!input) return
|
||||
return { directory: input.directory, workspace: input.workspaceID }
|
||||
}
|
||||
|
||||
function toProject(input: Project): AppProject {
|
||||
return {
|
||||
id: input.id,
|
||||
worktree: input.worktree,
|
||||
name: input.name,
|
||||
icon: input.icon,
|
||||
commands: input.commands,
|
||||
sandboxes: input.sandboxes,
|
||||
}
|
||||
}
|
||||
|
||||
function toSession(input: SessionInfo): AppSession {
|
||||
return {
|
||||
id: input.id,
|
||||
parentID: input.parentID,
|
||||
projectID: input.projectID,
|
||||
location: input.location,
|
||||
title: input.title,
|
||||
cost: input.cost,
|
||||
tokens: input.tokens,
|
||||
time: input.time,
|
||||
revert: input.revert && { messageID: input.revert.messageID },
|
||||
}
|
||||
}
|
||||
|
||||
function toModel(input: ModelInfo): AppModel {
|
||||
const cost = input.cost[0]
|
||||
return {
|
||||
id: input.id,
|
||||
providerID: input.providerID,
|
||||
name: input.name,
|
||||
family: input.family,
|
||||
releaseDate: new Date(input.time.released).toISOString().slice(0, 10),
|
||||
cost: cost && { input: cost.input, output: cost.output, cacheRead: cost.cache.read, cacheWrite: cost.cache.write },
|
||||
capabilities: {
|
||||
reasoning: input.capabilities.output.includes("reasoning"),
|
||||
input: {
|
||||
text: input.capabilities.input.includes("text"),
|
||||
image: input.capabilities.input.includes("image"),
|
||||
audio: input.capabilities.input.includes("audio"),
|
||||
video: input.capabilities.input.includes("video"),
|
||||
pdf: input.capabilities.input.includes("pdf"),
|
||||
},
|
||||
},
|
||||
limit: { context: input.limit.context, output: input.limit.output },
|
||||
variants: Object.fromEntries(input.variants.map((variant) => [variant.id, variant])),
|
||||
}
|
||||
}
|
||||
|
||||
function toAgent(input: AgentInfo): AppAgent {
|
||||
return {
|
||||
id: input.id,
|
||||
name: input.name,
|
||||
description: input.description,
|
||||
mode: input.mode,
|
||||
hidden: input.hidden,
|
||||
color: input.color,
|
||||
model: input.model,
|
||||
}
|
||||
}
|
||||
|
||||
function toTimelineItem(input: SessionMessageInfo, sessionID: string): TimelineItem {
|
||||
if (input.type === "user") {
|
||||
return {
|
||||
type: "user",
|
||||
id: input.id,
|
||||
sessionID,
|
||||
created: input.time.created,
|
||||
content: [
|
||||
{ type: "text", id: `${input.id}:text`, text: input.text },
|
||||
...(input.files?.map((file, index) => ({
|
||||
type: "file" as const,
|
||||
id: `${input.id}:file:${index}`,
|
||||
uri: file.source.type === "uri" ? file.source.uri : `data:${file.mime};base64,${file.data}`,
|
||||
name: file.name,
|
||||
})) ?? []),
|
||||
...(input.agents?.map((agent, index) => ({
|
||||
type: "agent" as const,
|
||||
id: `${input.id}:agent:${index}`,
|
||||
name: agent.name,
|
||||
source: agent.mention,
|
||||
})) ?? []),
|
||||
],
|
||||
raw: input,
|
||||
}
|
||||
}
|
||||
if (input.type === "assistant") return toAssistant(input, sessionID)
|
||||
const type =
|
||||
input.type === "agent-switched"
|
||||
? "agent-switch"
|
||||
: input.type === "model-switched"
|
||||
? "model-switch"
|
||||
: input.type
|
||||
return { type, id: input.id, sessionID, created: input.time.created, raw: input }
|
||||
}
|
||||
|
||||
function toAssistant(input: SessionMessageAssistant, sessionID: string): TimelineItem {
|
||||
return {
|
||||
type: "assistant",
|
||||
id: input.id,
|
||||
sessionID,
|
||||
created: input.time.created,
|
||||
completed: input.time.completed,
|
||||
content: input.content.flatMap((item, index): TimelineContent[] => {
|
||||
if (item.type === "text") return [{ type: "text", id: `${input.id}:text:${index}`, text: item.text }]
|
||||
if (item.type === "reasoning")
|
||||
return [{ type: "reasoning", id: `${input.id}:reasoning:${index}`, text: item.text }]
|
||||
if (item.state.status === "streaming")
|
||||
return [
|
||||
{
|
||||
type: "tool",
|
||||
id: item.id,
|
||||
tool: item.name,
|
||||
state: { status: "pending", input: {}, raw: item.state.input },
|
||||
},
|
||||
]
|
||||
if (item.state.status === "running")
|
||||
return [
|
||||
{
|
||||
type: "tool",
|
||||
id: item.id,
|
||||
tool: item.name,
|
||||
state: { status: "running", input: item.state.input, metadata: item.state.structured },
|
||||
},
|
||||
]
|
||||
if (item.state.status === "error")
|
||||
return [
|
||||
{
|
||||
type: "tool",
|
||||
id: item.id,
|
||||
tool: item.name,
|
||||
state: {
|
||||
status: "error",
|
||||
input: item.state.input,
|
||||
error: item.state.error.message,
|
||||
metadata: item.state.structured,
|
||||
},
|
||||
},
|
||||
]
|
||||
return [
|
||||
{
|
||||
type: "tool",
|
||||
id: item.id,
|
||||
tool: item.name,
|
||||
state: {
|
||||
status: "completed",
|
||||
input: item.state.input,
|
||||
output: item.state.content
|
||||
.map((content) => (content.type === "text" ? content.text : content.uri))
|
||||
.join("\n"),
|
||||
metadata: item.state.structured,
|
||||
},
|
||||
},
|
||||
]
|
||||
}),
|
||||
agent: input.agent,
|
||||
model: input.model,
|
||||
tokens: input.tokens,
|
||||
error: input.error,
|
||||
raw: input,
|
||||
}
|
||||
}
|
||||
|
||||
function toPromptFile(input: PromptFile) {
|
||||
return {
|
||||
uri: input.uri,
|
||||
name: input.name,
|
||||
mention: input.source && { start: input.source.start, end: input.source.end, text: input.source.text },
|
||||
}
|
||||
}
|
||||
|
||||
function toPermission(input: PermissionV2Request): AppPermissionRequest {
|
||||
return {
|
||||
id: input.id,
|
||||
sessionID: input.sessionID,
|
||||
action: input.action,
|
||||
resources: input.resources,
|
||||
metadata: input.metadata,
|
||||
}
|
||||
}
|
||||
|
||||
function toQuestion(input: QuestionV2Request): AppQuestionRequest {
|
||||
return { id: input.id, sessionID: input.sessionID, questions: input.questions }
|
||||
}
|
||||
|
||||
function toFileDiff(input: { file: string; patch: string; additions: number; deletions: number; status: "added" | "deleted" | "modified" }): AppFileDiff {
|
||||
return input
|
||||
}
|
||||
|
||||
function toMcpServer(input: McpServer): AppMcpServer {
|
||||
return { name: input.name, status: input.status, integrationID: input.integrationID }
|
||||
}
|
||||
|
||||
function toPty(input: { id: string; title: string }) {
|
||||
return { id: input.id, title: input.title }
|
||||
}
|
||||
|
||||
function toIntegration(input: IntegrationInfo) {
|
||||
return {
|
||||
id: input.id,
|
||||
name: input.name,
|
||||
methods: input.methods.map((method): IntegrationMethod => {
|
||||
if (method.type === "oauth") return method
|
||||
if (method.type === "key") return { type: "key", label: method.label ?? "API key" }
|
||||
return { type: "environment", label: method.names.join(", ") }
|
||||
}),
|
||||
connections: input.connections.map((connection): IntegrationConnection =>
|
||||
connection.type === "credential"
|
||||
? { id: connection.id, label: connection.label }
|
||||
: { id: connection.name, label: connection.name },
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
function toIntegrationAttempt(input: {
|
||||
attemptID: string
|
||||
url: string
|
||||
instructions: string
|
||||
mode: "auto" | "code"
|
||||
time: { created: number | string; expires: number | string }
|
||||
}): IntegrationAttempt {
|
||||
return {
|
||||
...input,
|
||||
time: { created: Number(input.time.created), expires: Number(input.time.expires) },
|
||||
}
|
||||
}
|
||||
|
||||
function toAttemptStatus(input: import("@opencode-ai/client").IntegrationAttemptStatus): IntegrationAttemptStatus {
|
||||
if (input.status === "failed") return { status: input.status, error: input.message }
|
||||
return { status: input.status }
|
||||
}
|
||||
|
||||
function toPending(input: SessionPendingInfo): PendingSessionInput {
|
||||
return {
|
||||
id: input.id,
|
||||
sessionID: input.sessionID,
|
||||
sequence: input.admittedSeq,
|
||||
created: input.timeCreated,
|
||||
delivery: "delivery" in input ? input.delivery : undefined,
|
||||
raw: input,
|
||||
}
|
||||
}
|
||||
|
||||
function toForm(input: FormInfo) {
|
||||
return {
|
||||
id: input.id,
|
||||
sessionID: input.sessionID,
|
||||
title: input.title,
|
||||
metadata: input.metadata,
|
||||
fields: input.fields.map((field): FormField => {
|
||||
if (field.type === "external")
|
||||
return {
|
||||
type: field.type,
|
||||
key: field.key,
|
||||
label: field.title ?? field.key,
|
||||
url: field.url,
|
||||
description: field.description,
|
||||
}
|
||||
if (field.type === "multiselect")
|
||||
return {
|
||||
type: field.type,
|
||||
key: field.key,
|
||||
label: field.title ?? field.key,
|
||||
options: field.options.map((option) => option.value),
|
||||
required: field.required,
|
||||
description: field.description,
|
||||
}
|
||||
return {
|
||||
type: field.type,
|
||||
key: field.key,
|
||||
label: field.title ?? field.key,
|
||||
required: field.required,
|
||||
description: field.description,
|
||||
}
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
function fromFormField(input: FormField): FormCreateInput["fields"][number] {
|
||||
if (input.type === "external")
|
||||
return { type: input.type, key: input.key, title: input.label, url: input.url, description: input.description }
|
||||
if (input.type === "multiselect")
|
||||
return {
|
||||
type: input.type,
|
||||
key: input.key,
|
||||
title: input.label,
|
||||
options: input.options.map((option) => ({ value: option, label: option })),
|
||||
required: input.required,
|
||||
description: input.description,
|
||||
}
|
||||
return {
|
||||
type: input.type,
|
||||
key: input.key,
|
||||
title: input.label,
|
||||
required: input.type === "boolean" ? undefined : input.required,
|
||||
description: input.description,
|
||||
}
|
||||
}
|
||||
|
||||
function toClientRecord(input: Readonly<Record<string, import("./backend").JsonValue>>) {
|
||||
return Object.fromEntries(Object.entries(input).map(([key, value]) => [key, toClientJson(value)]))
|
||||
}
|
||||
|
||||
function toClientJson(input: import("./backend").JsonValue): import("@opencode-ai/client").JsonValue {
|
||||
if (isJsonArray(input)) return input.map(toClientJson)
|
||||
if (input && typeof input === "object") return toClientRecord(input)
|
||||
return input
|
||||
}
|
||||
|
||||
function isJsonArray(input: import("./backend").JsonValue): input is readonly import("./backend").JsonValue[] {
|
||||
return Array.isArray(input)
|
||||
}
|
||||
|
||||
function toShell(input: ShellInfo1): ShellProcess {
|
||||
return {
|
||||
id: input.id,
|
||||
command: input.command,
|
||||
cwd: input.cwd,
|
||||
status: input.status === "running" ? "running" : "exited",
|
||||
created: input.time.started,
|
||||
exitCode: input.exit,
|
||||
metadata: input.metadata,
|
||||
}
|
||||
}
|
||||
|
||||
async function toEvent(
|
||||
input: OpenCodeEvent,
|
||||
loadSession: (sessionID: string, options?: RequestOptions) => Promise<AppSession>,
|
||||
loadMessage: (sessionID: string, messageID: string, options?: RequestOptions) => Promise<TimelineItem>,
|
||||
options?: RequestOptions,
|
||||
): Promise<AppEvent> {
|
||||
if (input.type === "server.connected") return { type: input.type }
|
||||
if (input.type === "session.deleted") return { type: input.type, sessionID: input.data.sessionID }
|
||||
if (input.type === "session.error") return { type: input.type, sessionID: input.data.sessionID, error: input.data.error }
|
||||
if (input.type === "session.status") {
|
||||
const activity = toActivity(input.data.status)
|
||||
return activity
|
||||
? { type: "session.activity", sessionID: input.data.sessionID, activity }
|
||||
: { type: "unknown", raw: input }
|
||||
}
|
||||
if (input.type === "session.execution.started")
|
||||
return { type: "session.activity", sessionID: input.data.sessionID, activity: { type: "running" } }
|
||||
if (input.type === "session.retry.scheduled")
|
||||
return {
|
||||
type: "session.activity",
|
||||
sessionID: input.data.sessionID,
|
||||
activity: {
|
||||
type: "retry",
|
||||
attempt: input.data.attempt,
|
||||
message: input.data.error.message,
|
||||
next: input.data.at,
|
||||
},
|
||||
}
|
||||
if (input.type === "session.execution.failed")
|
||||
return { type: "session.error", sessionID: input.data.sessionID, error: input.data.error }
|
||||
if (input.type === "session.idle") return { type: "unknown", raw: input }
|
||||
if (input.type === "permission.v2.asked") return { type: "permission.requested", request: toPermission(input.data) }
|
||||
if (input.type === "permission.v2.replied")
|
||||
return { type: "permission.replied", sessionID: input.data.sessionID, requestID: input.data.requestID }
|
||||
if (input.type === "question.v2.asked") return { type: "question.requested", request: toQuestion(input.data) }
|
||||
if (input.type === "question.v2.replied" || input.type === "question.v2.rejected")
|
||||
return {
|
||||
type: input.type === "question.v2.replied" ? "question.replied" : "question.rejected",
|
||||
sessionID: input.data.sessionID,
|
||||
requestID: input.data.requestID,
|
||||
}
|
||||
if (input.type === "filesystem.changed")
|
||||
return { type: "file.changed", path: input.data.file, change: input.data.event }
|
||||
if (input.type === "vcs.branch.updated") return { type: input.type, branch: input.data.branch }
|
||||
if (input.type === "pty.exited") return { type: input.type, ptyID: input.data.id }
|
||||
if (input.type === "message.removed")
|
||||
return { type: "timeline.removed", sessionID: input.data.sessionID, itemID: input.data.messageID }
|
||||
if (input.type === "message.updated")
|
||||
return {
|
||||
type: "timeline.updated",
|
||||
item: await loadMessage(input.data.sessionID, input.data.info.id, options),
|
||||
}
|
||||
if (input.type === "message.part.updated")
|
||||
return {
|
||||
type: "timeline.updated",
|
||||
item: await loadMessage(input.data.sessionID, input.data.part.messageID, options),
|
||||
}
|
||||
|
||||
const sessionID = eventSessionID(input)
|
||||
const messageID = eventMessageID(input)
|
||||
if (sessionID && messageID)
|
||||
return { type: "timeline.updated", item: await loadMessage(sessionID, messageID, options) }
|
||||
if (sessionID && isSessionProjectionEvent(input.type)) {
|
||||
const session = await loadSession(sessionID, options)
|
||||
return { type: input.type === "session.created" ? "session.created" : "session.updated", session }
|
||||
}
|
||||
return { type: "unknown", raw: input }
|
||||
}
|
||||
|
||||
function toActivity(input: import("@opencode-ai/client").SessionStatus): SessionActivity | undefined {
|
||||
if (input.type === "idle") return
|
||||
if (input.type === "busy") return { type: "running" }
|
||||
return input
|
||||
}
|
||||
|
||||
function eventSessionID(input: OpenCodeEvent) {
|
||||
if (!("data" in input) || typeof input.data !== "object" || input.data === null || !("sessionID" in input.data)) return
|
||||
return typeof input.data.sessionID === "string" ? input.data.sessionID : undefined
|
||||
}
|
||||
|
||||
function eventMessageID(input: OpenCodeEvent) {
|
||||
if (!("data" in input) || typeof input.data !== "object" || input.data === null) return
|
||||
if ("assistantMessageID" in input.data && typeof input.data.assistantMessageID === "string")
|
||||
return input.data.assistantMessageID
|
||||
if ("messageID" in input.data && typeof input.data.messageID === "string") return input.data.messageID
|
||||
if (input.type === "session.input.promoted") return input.data.inputID
|
||||
}
|
||||
|
||||
function isSessionProjectionEvent(type: string) {
|
||||
return [
|
||||
"session.created",
|
||||
"session.updated",
|
||||
"session.agent.selected",
|
||||
"session.model.selected",
|
||||
"session.moved",
|
||||
"session.renamed",
|
||||
"session.usage.updated",
|
||||
"session.revert.staged",
|
||||
"session.revert.cleared",
|
||||
"session.revert.committed",
|
||||
].includes(type)
|
||||
}
|
||||
|
||||
async function toLogEvent(
|
||||
input: import("@opencode-ai/client").SessionLogOutput,
|
||||
loadSession: (sessionID: string, options?: RequestOptions) => Promise<AppSession>,
|
||||
loadMessage: (sessionID: string, messageID: string, options?: RequestOptions) => Promise<TimelineItem>,
|
||||
options?: RequestOptions,
|
||||
): Promise<AppEvent> {
|
||||
if (input.type === "log.synced") return { type: "unknown", raw: input }
|
||||
return toEvent(input, loadSession, loadMessage, options)
|
||||
}
|
||||
@@ -0,0 +1,1190 @@
|
||||
export type JsonValue = null | boolean | number | string | readonly JsonValue[] | { readonly [key: string]: JsonValue }
|
||||
|
||||
export type RequestOptions = {
|
||||
readonly signal?: AbortSignal
|
||||
}
|
||||
|
||||
export type LocationRef = {
|
||||
readonly directory: string
|
||||
readonly workspaceID?: string
|
||||
}
|
||||
|
||||
export type LocationInput = {
|
||||
readonly location?: LocationRef
|
||||
}
|
||||
|
||||
export type ModelRef = {
|
||||
readonly id: string
|
||||
readonly providerID: string
|
||||
readonly variant?: string
|
||||
}
|
||||
|
||||
export type Page<T> = {
|
||||
readonly items: readonly T[]
|
||||
readonly previous?: string
|
||||
readonly next?: string
|
||||
}
|
||||
|
||||
export type Health = {
|
||||
readonly healthy: boolean
|
||||
readonly version?: string
|
||||
readonly pid?: number
|
||||
}
|
||||
|
||||
export type AppProject = {
|
||||
readonly id: string
|
||||
readonly worktree: string
|
||||
readonly name?: string
|
||||
readonly icon?: {
|
||||
readonly url?: string
|
||||
readonly override?: string
|
||||
readonly color?: string
|
||||
}
|
||||
readonly commands?: {
|
||||
readonly start?: string
|
||||
}
|
||||
readonly sandboxes: readonly string[]
|
||||
}
|
||||
|
||||
export type CurrentProject = {
|
||||
readonly id: string
|
||||
readonly directory: string
|
||||
}
|
||||
|
||||
export type AppModel = {
|
||||
readonly id: string
|
||||
readonly providerID: string
|
||||
readonly name: string
|
||||
readonly family?: string
|
||||
readonly releaseDate?: string
|
||||
readonly cost?: {
|
||||
readonly input: number
|
||||
readonly output?: number
|
||||
readonly cacheRead?: number
|
||||
readonly cacheWrite?: number
|
||||
}
|
||||
readonly capabilities: {
|
||||
readonly reasoning: boolean
|
||||
readonly input: {
|
||||
readonly text: boolean
|
||||
readonly image: boolean
|
||||
readonly audio: boolean
|
||||
readonly video: boolean
|
||||
readonly pdf: boolean
|
||||
}
|
||||
}
|
||||
readonly limit: {
|
||||
readonly context: number
|
||||
readonly output?: number
|
||||
}
|
||||
readonly variants?: Readonly<Record<string, unknown>>
|
||||
}
|
||||
|
||||
export type AppProvider = {
|
||||
readonly id: string
|
||||
readonly name: string
|
||||
readonly models: Readonly<Record<string, AppModel>>
|
||||
}
|
||||
|
||||
export type ProviderCatalog = {
|
||||
readonly providers: ReadonlyMap<string, AppProvider>
|
||||
readonly connected: readonly string[]
|
||||
readonly defaults: Readonly<Record<string, string>>
|
||||
}
|
||||
|
||||
export type AppAgent = {
|
||||
readonly id: string
|
||||
readonly name: string
|
||||
readonly description?: string
|
||||
readonly mode: "subagent" | "primary" | "all"
|
||||
readonly hidden: boolean
|
||||
readonly color?: string
|
||||
readonly model?: ModelRef
|
||||
}
|
||||
|
||||
export type AppCommand = {
|
||||
readonly name: string
|
||||
readonly description?: string
|
||||
readonly source?: "command" | "mcp" | "skill"
|
||||
}
|
||||
|
||||
export type AppReference = {
|
||||
readonly name: string
|
||||
readonly path: string
|
||||
readonly description?: string
|
||||
readonly hidden?: boolean
|
||||
readonly source:
|
||||
| {
|
||||
readonly type: "local"
|
||||
readonly path: string
|
||||
}
|
||||
| {
|
||||
readonly type: "git"
|
||||
readonly repository: string
|
||||
readonly branch?: string
|
||||
}
|
||||
}
|
||||
|
||||
export type TokenUsage = {
|
||||
readonly input: number
|
||||
readonly output: number
|
||||
readonly reasoning: number
|
||||
readonly cache: {
|
||||
readonly read: number
|
||||
readonly write: number
|
||||
}
|
||||
}
|
||||
|
||||
export type AppSession = {
|
||||
readonly id: string
|
||||
readonly parentID?: string
|
||||
readonly projectID: string
|
||||
readonly location: LocationRef
|
||||
readonly title: string
|
||||
readonly cost: number
|
||||
readonly tokens?: TokenUsage
|
||||
readonly time: {
|
||||
readonly created: number
|
||||
readonly updated?: number
|
||||
readonly archived?: number
|
||||
}
|
||||
readonly share?: {
|
||||
readonly url: string
|
||||
}
|
||||
readonly revert?: {
|
||||
readonly messageID: string
|
||||
}
|
||||
}
|
||||
|
||||
export type SessionActivity =
|
||||
| { readonly type: "running" }
|
||||
| {
|
||||
readonly type: "retry"
|
||||
readonly attempt: number
|
||||
readonly message: string
|
||||
readonly next: number
|
||||
readonly action?: {
|
||||
readonly reason: string
|
||||
readonly provider: string
|
||||
readonly title: string
|
||||
readonly message: string
|
||||
readonly label: string
|
||||
readonly link?: string
|
||||
}
|
||||
}
|
||||
|
||||
export type SessionListInput = LocationInput & {
|
||||
readonly roots?: boolean
|
||||
readonly limit?: number
|
||||
readonly search?: string
|
||||
readonly cursor?: string
|
||||
}
|
||||
|
||||
export type SourceText = {
|
||||
readonly text: string
|
||||
readonly start: number
|
||||
readonly end: number
|
||||
}
|
||||
|
||||
export type FileSource =
|
||||
| {
|
||||
readonly type: "file" | "symbol"
|
||||
readonly path: string
|
||||
readonly name?: string
|
||||
readonly kind?: number
|
||||
readonly text: SourceText
|
||||
}
|
||||
| {
|
||||
readonly type: "resource"
|
||||
readonly clientName: string
|
||||
readonly uri: string
|
||||
readonly text: SourceText
|
||||
}
|
||||
|
||||
export type ToolState =
|
||||
| {
|
||||
readonly status: "pending"
|
||||
readonly input: Readonly<Record<string, unknown>>
|
||||
readonly raw?: string
|
||||
}
|
||||
| {
|
||||
readonly status: "running"
|
||||
readonly input: Readonly<Record<string, unknown>>
|
||||
readonly title?: string
|
||||
readonly metadata?: Readonly<Record<string, unknown>>
|
||||
}
|
||||
| {
|
||||
readonly status: "completed"
|
||||
readonly input: Readonly<Record<string, unknown>>
|
||||
readonly output: string
|
||||
readonly title?: string
|
||||
readonly metadata?: Readonly<Record<string, unknown>>
|
||||
}
|
||||
| {
|
||||
readonly status: "error"
|
||||
readonly input: Readonly<Record<string, unknown>>
|
||||
readonly error: string
|
||||
readonly metadata?: Readonly<Record<string, unknown>>
|
||||
}
|
||||
|
||||
export type TimelineContent =
|
||||
| {
|
||||
readonly type: "text"
|
||||
readonly id: string
|
||||
readonly text: string
|
||||
readonly synthetic?: boolean
|
||||
readonly ignored?: boolean
|
||||
readonly metadata?: Readonly<Record<string, unknown>>
|
||||
}
|
||||
| {
|
||||
readonly type: "reasoning"
|
||||
readonly id: string
|
||||
readonly text: string
|
||||
}
|
||||
| {
|
||||
readonly type: "file"
|
||||
readonly id: string
|
||||
readonly uri: string
|
||||
readonly name?: string
|
||||
readonly mime?: string
|
||||
readonly source?: FileSource
|
||||
}
|
||||
| {
|
||||
readonly type: "agent"
|
||||
readonly id: string
|
||||
readonly name: string
|
||||
readonly source?: SourceText
|
||||
}
|
||||
| {
|
||||
readonly type: "tool"
|
||||
readonly id: string
|
||||
readonly callID?: string
|
||||
readonly tool: string
|
||||
readonly state: ToolState
|
||||
}
|
||||
|
||||
export type TimelineItem =
|
||||
| {
|
||||
readonly type: "user"
|
||||
readonly id: string
|
||||
readonly sessionID: string
|
||||
readonly created: number
|
||||
readonly content: readonly TimelineContent[]
|
||||
readonly agent?: string
|
||||
readonly model?: ModelRef
|
||||
readonly raw?: unknown
|
||||
}
|
||||
| {
|
||||
readonly type: "assistant"
|
||||
readonly id: string
|
||||
readonly sessionID: string
|
||||
readonly parentID?: string
|
||||
readonly created: number
|
||||
readonly completed?: number
|
||||
readonly content: readonly TimelineContent[]
|
||||
readonly agent?: string
|
||||
readonly model?: ModelRef
|
||||
readonly tokens?: TokenUsage
|
||||
readonly error?: unknown
|
||||
readonly raw?: unknown
|
||||
}
|
||||
| {
|
||||
readonly type: "agent-switch" | "model-switch" | "synthetic" | "system" | "skill" | "shell" | "compaction"
|
||||
readonly id: string
|
||||
readonly sessionID: string
|
||||
readonly created: number
|
||||
readonly raw?: unknown
|
||||
}
|
||||
|
||||
export type PromptFile = {
|
||||
readonly uri: string
|
||||
readonly name?: string
|
||||
readonly mime?: string
|
||||
readonly source?: SourceText & {
|
||||
readonly path?: string
|
||||
}
|
||||
}
|
||||
|
||||
export type PromptAgentMention = {
|
||||
readonly name: string
|
||||
readonly start?: number
|
||||
readonly end?: number
|
||||
readonly text?: string
|
||||
}
|
||||
|
||||
export type PromptInput = {
|
||||
readonly sessionID: string
|
||||
readonly id: string
|
||||
readonly text: string
|
||||
readonly files?: readonly PromptFile[]
|
||||
readonly agents?: readonly PromptAgentMention[]
|
||||
readonly selection?: {
|
||||
readonly agent?: string
|
||||
readonly model?: ModelRef
|
||||
}
|
||||
readonly delivery?: "steer" | "queue"
|
||||
}
|
||||
|
||||
export type CommandInput = {
|
||||
readonly sessionID: string
|
||||
readonly id?: string
|
||||
readonly command: string
|
||||
readonly arguments?: string
|
||||
readonly agent?: string
|
||||
readonly model?: ModelRef
|
||||
readonly files?: readonly PromptFile[]
|
||||
readonly delivery?: "steer" | "queue"
|
||||
}
|
||||
|
||||
export type FileEntry = {
|
||||
readonly path: string
|
||||
readonly type: "file" | "directory"
|
||||
}
|
||||
|
||||
export type FileContent = {
|
||||
readonly bytes: Uint8Array
|
||||
readonly kind?: "text" | "binary"
|
||||
readonly mimeType?: string
|
||||
}
|
||||
|
||||
export type AppFileDiff = {
|
||||
readonly file: string
|
||||
readonly patch?: string
|
||||
readonly additions: number
|
||||
readonly deletions: number
|
||||
readonly status?: "added" | "deleted" | "modified"
|
||||
}
|
||||
|
||||
export type AppPermissionRequest = {
|
||||
readonly id: string
|
||||
readonly sessionID: string
|
||||
readonly action: string
|
||||
readonly resources: readonly string[]
|
||||
readonly metadata?: Readonly<Record<string, unknown>>
|
||||
}
|
||||
|
||||
export type AppQuestion = {
|
||||
readonly question: string
|
||||
readonly header?: string
|
||||
readonly options: readonly {
|
||||
readonly label: string
|
||||
readonly description?: string
|
||||
}[]
|
||||
readonly multiple?: boolean
|
||||
readonly custom?: boolean
|
||||
}
|
||||
|
||||
export type AppQuestionRequest = {
|
||||
readonly id: string
|
||||
readonly sessionID: string
|
||||
readonly questions: readonly AppQuestion[]
|
||||
}
|
||||
|
||||
export type AppMcpStatus =
|
||||
| { readonly status: "connected" }
|
||||
| { readonly status: "pending" }
|
||||
| { readonly status: "disabled" }
|
||||
| { readonly status: "needs_auth" }
|
||||
| { readonly status: "failed"; readonly error: string }
|
||||
| { readonly status: "needs_client_registration"; readonly error: string }
|
||||
|
||||
export type AppMcpServer = {
|
||||
readonly name: string
|
||||
readonly status: AppMcpStatus
|
||||
readonly integrationID?: string
|
||||
}
|
||||
|
||||
export type AppMcpResource = {
|
||||
readonly server: string
|
||||
readonly name: string
|
||||
readonly uri: string
|
||||
readonly description?: string
|
||||
readonly mimeType?: string
|
||||
}
|
||||
|
||||
export type AppMcpResourceTemplate = {
|
||||
readonly server: string
|
||||
readonly name: string
|
||||
readonly uriTemplate: string
|
||||
readonly description?: string
|
||||
readonly mimeType?: string
|
||||
}
|
||||
|
||||
export type AppPty = {
|
||||
readonly id: string
|
||||
readonly title: string
|
||||
}
|
||||
|
||||
export type AppEventEnvelope = {
|
||||
readonly location?: LocationRef
|
||||
readonly event: AppEvent
|
||||
}
|
||||
|
||||
export type AppEvent =
|
||||
| { readonly type: "server.connected" }
|
||||
| { readonly type: "server.disposed"; readonly location?: LocationRef }
|
||||
| { readonly type: "project.updated"; readonly project: AppProject }
|
||||
| { readonly type: "session.created"; readonly session: AppSession }
|
||||
| { readonly type: "session.updated"; readonly session: AppSession }
|
||||
| { readonly type: "session.deleted"; readonly sessionID: string }
|
||||
| { readonly type: "session.activity"; readonly sessionID: string; readonly activity: SessionActivity }
|
||||
| { readonly type: "session.error"; readonly sessionID?: string; readonly error?: unknown }
|
||||
| { readonly type: "timeline.updated"; readonly item: TimelineItem }
|
||||
| { readonly type: "timeline.removed"; readonly sessionID: string; readonly itemID: string }
|
||||
| { readonly type: "permission.requested"; readonly request: AppPermissionRequest }
|
||||
| { readonly type: "permission.replied"; readonly sessionID: string; readonly requestID: string }
|
||||
| { readonly type: "question.requested"; readonly request: AppQuestionRequest }
|
||||
| { readonly type: "question.replied" | "question.rejected"; readonly sessionID: string; readonly requestID: string }
|
||||
| { readonly type: "file.changed"; readonly path: string; readonly change: "add" | "change" | "unlink" }
|
||||
| { readonly type: "vcs.branch.updated"; readonly branch?: string }
|
||||
| { readonly type: "pty.exited"; readonly ptyID: string }
|
||||
| { readonly type: "unknown"; readonly raw: unknown }
|
||||
|
||||
export interface HealthApi {
|
||||
get(options?: RequestOptions): Promise<Health>
|
||||
}
|
||||
|
||||
export interface ProjectApi {
|
||||
list(options?: RequestOptions): Promise<readonly AppProject[]>
|
||||
current(input?: LocationInput, options?: RequestOptions): Promise<CurrentProject>
|
||||
}
|
||||
|
||||
export interface CatalogApi {
|
||||
providers(input?: LocationInput, options?: RequestOptions): Promise<ProviderCatalog>
|
||||
agents(input?: LocationInput, options?: RequestOptions): Promise<readonly AppAgent[]>
|
||||
}
|
||||
|
||||
export interface CommandApi {
|
||||
list(input?: LocationInput, options?: RequestOptions): Promise<readonly AppCommand[]>
|
||||
}
|
||||
|
||||
export interface ReferenceApi {
|
||||
list(input?: LocationInput, options?: RequestOptions): Promise<readonly AppReference[]>
|
||||
}
|
||||
|
||||
export interface SessionApi {
|
||||
list(input?: SessionListInput, options?: RequestOptions): Promise<Page<AppSession>>
|
||||
create(
|
||||
input?: LocationInput & { readonly agent?: string; readonly model?: ModelRef },
|
||||
options?: RequestOptions,
|
||||
): Promise<AppSession>
|
||||
get(input: LocationInput & { readonly sessionID: string }, options?: RequestOptions): Promise<AppSession>
|
||||
remove(input: LocationInput & { readonly sessionID: string }, options?: RequestOptions): Promise<void>
|
||||
fork(
|
||||
input: LocationInput & { readonly sessionID: string; readonly messageID?: string },
|
||||
options?: RequestOptions,
|
||||
): Promise<AppSession>
|
||||
rename(
|
||||
input: LocationInput & { readonly sessionID: string; readonly title: string },
|
||||
options?: RequestOptions,
|
||||
): Promise<void>
|
||||
interrupt(input: LocationInput & { readonly sessionID: string }, options?: RequestOptions): Promise<void>
|
||||
activity(input?: LocationInput, options?: RequestOptions): Promise<Readonly<Record<string, SessionActivity>>>
|
||||
history(
|
||||
input: { readonly sessionID: string; readonly limit?: number; readonly cursor?: string },
|
||||
options?: RequestOptions,
|
||||
): Promise<Page<TimelineItem>>
|
||||
message(
|
||||
input: { readonly sessionID: string; readonly messageID: string },
|
||||
options?: RequestOptions,
|
||||
): Promise<TimelineItem>
|
||||
prompt(input: PromptInput, options?: RequestOptions): Promise<void>
|
||||
command(input: CommandInput, options?: RequestOptions): Promise<void>
|
||||
}
|
||||
|
||||
export interface FileApi {
|
||||
list(input: LocationInput & { readonly path?: string }, options?: RequestOptions): Promise<readonly FileEntry[]>
|
||||
find(
|
||||
input: LocationInput & {
|
||||
readonly query: string
|
||||
readonly type?: "file" | "directory"
|
||||
readonly limit?: number
|
||||
},
|
||||
options?: RequestOptions,
|
||||
): Promise<readonly FileEntry[]>
|
||||
read(input: LocationInput & { readonly path: string }, options?: RequestOptions): Promise<FileContent>
|
||||
}
|
||||
|
||||
export interface PermissionApi {
|
||||
pending(input?: LocationInput, options?: RequestOptions): Promise<readonly AppPermissionRequest[]>
|
||||
reply(
|
||||
input: {
|
||||
readonly sessionID: string
|
||||
readonly requestID: string
|
||||
readonly reply: "once" | "always" | "reject"
|
||||
readonly message?: string
|
||||
},
|
||||
options?: RequestOptions,
|
||||
): Promise<void>
|
||||
}
|
||||
|
||||
export interface QuestionApi {
|
||||
pending(input?: LocationInput, options?: RequestOptions): Promise<readonly AppQuestionRequest[]>
|
||||
reply(
|
||||
input: {
|
||||
readonly sessionID: string
|
||||
readonly requestID: string
|
||||
readonly answers: readonly (readonly string[])[]
|
||||
},
|
||||
options?: RequestOptions,
|
||||
): Promise<void>
|
||||
reject(input: { readonly sessionID: string; readonly requestID: string }, options?: RequestOptions): Promise<void>
|
||||
}
|
||||
|
||||
export interface VcsApi {
|
||||
status(
|
||||
input?: LocationInput,
|
||||
options?: RequestOptions,
|
||||
): Promise<
|
||||
readonly {
|
||||
readonly file: string
|
||||
readonly additions: number
|
||||
readonly deletions: number
|
||||
readonly status: "added" | "deleted" | "modified"
|
||||
}[]
|
||||
>
|
||||
diff(
|
||||
input: LocationInput & { readonly mode: "working" | "branch"; readonly context?: number },
|
||||
options?: RequestOptions,
|
||||
): Promise<readonly AppFileDiff[]>
|
||||
}
|
||||
|
||||
export interface McpApi {
|
||||
list(input?: LocationInput, options?: RequestOptions): Promise<readonly AppMcpServer[]>
|
||||
resources(
|
||||
input?: LocationInput,
|
||||
options?: RequestOptions,
|
||||
): Promise<{
|
||||
readonly resources: readonly AppMcpResource[]
|
||||
readonly templates: readonly AppMcpResourceTemplate[]
|
||||
}>
|
||||
}
|
||||
|
||||
export interface PtyApi {
|
||||
list(input?: LocationInput, options?: RequestOptions): Promise<readonly AppPty[]>
|
||||
create(
|
||||
input: LocationInput & {
|
||||
readonly title?: string
|
||||
readonly command?: string
|
||||
readonly args?: readonly string[]
|
||||
readonly cwd?: string
|
||||
readonly env?: Readonly<Record<string, string>>
|
||||
},
|
||||
options?: RequestOptions,
|
||||
): Promise<AppPty>
|
||||
get(input: LocationInput & { readonly ptyID: string }, options?: RequestOptions): Promise<AppPty>
|
||||
update(
|
||||
input: LocationInput & {
|
||||
readonly ptyID: string
|
||||
readonly title?: string
|
||||
readonly size?: { readonly rows: number; readonly cols: number }
|
||||
},
|
||||
options?: RequestOptions,
|
||||
): Promise<AppPty>
|
||||
remove(input: LocationInput & { readonly ptyID: string }, options?: RequestOptions): Promise<void>
|
||||
}
|
||||
|
||||
export interface EventApi {
|
||||
subscribe(options?: RequestOptions): AsyncIterable<AppEventEnvelope>
|
||||
}
|
||||
|
||||
export interface CommonClient {
|
||||
readonly health: HealthApi
|
||||
readonly projects: ProjectApi
|
||||
readonly catalog: CatalogApi
|
||||
readonly commands: CommandApi
|
||||
readonly references: ReferenceApi
|
||||
readonly sessions: SessionApi
|
||||
readonly files: FileApi
|
||||
readonly permissions: PermissionApi
|
||||
readonly questions: QuestionApi
|
||||
readonly vcs: VcsApi
|
||||
readonly mcp: McpApi
|
||||
readonly pty: PtyApi
|
||||
readonly events: EventApi
|
||||
disposeLocation(input: LocationInput, options?: RequestOptions): Promise<void>
|
||||
}
|
||||
|
||||
export type AppProviderConfig = {
|
||||
readonly npm?: string
|
||||
readonly name?: string
|
||||
readonly env?: readonly string[]
|
||||
readonly options?: {
|
||||
readonly baseURL?: string
|
||||
readonly headers?: Readonly<Record<string, string>>
|
||||
readonly [key: string]: unknown
|
||||
}
|
||||
readonly models?: Readonly<Record<string, { readonly name?: string }>>
|
||||
}
|
||||
|
||||
export type AppConfig = {
|
||||
readonly shell?: string
|
||||
readonly model?: string
|
||||
readonly share?: "manual" | "auto" | "disabled"
|
||||
readonly plugin?: readonly (string | readonly [string, Readonly<Record<string, unknown>>])[]
|
||||
readonly disabledProviders?: readonly string[]
|
||||
readonly provider?: Readonly<Record<string, AppProviderConfig>>
|
||||
readonly permission?: unknown
|
||||
}
|
||||
|
||||
export interface ConfigurationCapability {
|
||||
getGlobal(options?: RequestOptions): Promise<AppConfig>
|
||||
updateGlobal(config: AppConfig, options?: RequestOptions): Promise<void>
|
||||
get(input?: LocationInput, options?: RequestOptions): Promise<AppConfig>
|
||||
}
|
||||
|
||||
export type ProviderAuthPrompt =
|
||||
| {
|
||||
readonly type: "text"
|
||||
readonly key: string
|
||||
readonly message: string
|
||||
readonly placeholder?: string
|
||||
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
|
||||
}
|
||||
| {
|
||||
readonly type: "select"
|
||||
readonly key: string
|
||||
readonly message: string
|
||||
readonly options: readonly { readonly label: string; readonly value: string; readonly hint?: string }[]
|
||||
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
|
||||
}
|
||||
|
||||
export type ProviderAuthMethod = {
|
||||
readonly type: "oauth" | "api"
|
||||
readonly label: string
|
||||
readonly prompts?: readonly ProviderAuthPrompt[]
|
||||
}
|
||||
|
||||
export type ProviderAuthorization = {
|
||||
readonly url: string
|
||||
readonly method: "auto" | "code"
|
||||
readonly instructions: string
|
||||
}
|
||||
|
||||
export interface ProviderAuthV1Capability {
|
||||
methods(
|
||||
input?: LocationInput,
|
||||
options?: RequestOptions,
|
||||
): Promise<Readonly<Record<string, readonly ProviderAuthMethod[]>>>
|
||||
authorize(
|
||||
input: LocationInput & {
|
||||
readonly providerID: string
|
||||
readonly method: number
|
||||
readonly values?: Readonly<Record<string, string>>
|
||||
},
|
||||
options?: RequestOptions,
|
||||
): Promise<ProviderAuthorization>
|
||||
callback(
|
||||
input: LocationInput & { readonly providerID: string; readonly method: number; readonly code?: string },
|
||||
options?: RequestOptions,
|
||||
): Promise<void>
|
||||
setApiKey(
|
||||
input: {
|
||||
readonly providerID: string
|
||||
readonly key: string
|
||||
readonly metadata?: Readonly<Record<string, string>>
|
||||
},
|
||||
options?: RequestOptions,
|
||||
): Promise<void>
|
||||
remove(input: { readonly providerID: string }, options?: RequestOptions): Promise<void>
|
||||
}
|
||||
|
||||
export interface ProjectEditingCapability {
|
||||
update(
|
||||
input: LocationInput & {
|
||||
readonly projectID: string
|
||||
readonly name?: string
|
||||
readonly icon?: { readonly override?: string; readonly color?: string }
|
||||
readonly commands?: { readonly start?: string }
|
||||
},
|
||||
options?: RequestOptions,
|
||||
): Promise<AppProject>
|
||||
initGit(input?: LocationInput, options?: RequestOptions): Promise<AppProject>
|
||||
}
|
||||
|
||||
export type AppWorktree = {
|
||||
readonly directory: string
|
||||
readonly branch?: string
|
||||
}
|
||||
|
||||
export interface WorktreesV1Capability {
|
||||
list(input: LocationInput, options?: RequestOptions): Promise<readonly string[]>
|
||||
create(input: LocationInput, options?: RequestOptions): Promise<AppWorktree>
|
||||
remove(input: LocationInput & { readonly directory: string }, options?: RequestOptions): Promise<void>
|
||||
reset(input: LocationInput & { readonly directory: string }, options?: RequestOptions): Promise<void>
|
||||
}
|
||||
|
||||
export type AppTodo = {
|
||||
readonly id?: string
|
||||
readonly content: string
|
||||
readonly status: "pending" | "in_progress" | "completed" | "cancelled" | (string & {})
|
||||
}
|
||||
|
||||
export type LegacySessionShellInput = LocationInput & {
|
||||
readonly sessionID: string
|
||||
readonly id?: string
|
||||
readonly command: string
|
||||
readonly agent: string
|
||||
readonly model?: ModelRef
|
||||
}
|
||||
|
||||
export interface SessionExtrasV1Capability {
|
||||
archive(sessionID: string, archivedAt: number, options?: RequestOptions): Promise<void>
|
||||
share(sessionID: string, options?: RequestOptions): Promise<string>
|
||||
unshare(sessionID: string, options?: RequestOptions): Promise<void>
|
||||
diff(sessionID: string, options?: RequestOptions): Promise<readonly AppFileDiff[]>
|
||||
todos(sessionID: string, options?: RequestOptions): Promise<readonly AppTodo[]>
|
||||
summarize(sessionID: string, model: ModelRef, options?: RequestOptions): Promise<void>
|
||||
revert(sessionID: string, messageID: string, options?: RequestOptions): Promise<void>
|
||||
clearRevert(sessionID: string, options?: RequestOptions): Promise<void>
|
||||
shell(input: LegacySessionShellInput, options?: RequestOptions): Promise<void>
|
||||
}
|
||||
|
||||
export type AppLspStatus = {
|
||||
readonly id: string
|
||||
readonly name: string
|
||||
readonly status: "connected" | "error"
|
||||
}
|
||||
|
||||
export interface LspCapability {
|
||||
status(input?: LocationInput, options?: RequestOptions): Promise<readonly AppLspStatus[]>
|
||||
}
|
||||
|
||||
export interface McpControlCapability {
|
||||
connect(input: LocationInput & { readonly name: string }, options?: RequestOptions): Promise<void>
|
||||
disconnect(input: LocationInput & { readonly name: string }, options?: RequestOptions): Promise<void>
|
||||
authenticate(input: LocationInput & { readonly name: string }, options?: RequestOptions): Promise<void>
|
||||
}
|
||||
|
||||
export type AppPathInfo = {
|
||||
readonly home: string
|
||||
readonly directory: string
|
||||
readonly state?: string
|
||||
readonly config?: string
|
||||
readonly worktree?: string
|
||||
}
|
||||
|
||||
export interface PathInfoCapability {
|
||||
get(input?: LocationInput, options?: RequestOptions): Promise<AppPathInfo>
|
||||
}
|
||||
|
||||
export type AppVcsInfo = {
|
||||
readonly branch?: string
|
||||
readonly defaultBranch?: string
|
||||
}
|
||||
|
||||
export interface VcsInfoCapability {
|
||||
get(input?: LocationInput, options?: RequestOptions): Promise<AppVcsInfo>
|
||||
}
|
||||
|
||||
export type DecoratedFileContent = {
|
||||
readonly type: "text" | "binary"
|
||||
readonly content: string
|
||||
readonly diff?: string
|
||||
readonly encoding?: "base64"
|
||||
readonly mimeType?: string
|
||||
readonly patch?: {
|
||||
readonly hunks: readonly { readonly lines: readonly string[] }[]
|
||||
}
|
||||
}
|
||||
|
||||
export interface DecoratedFileCapability {
|
||||
read(input: LocationInput & { readonly path: string }, options?: RequestOptions): Promise<DecoratedFileContent>
|
||||
}
|
||||
|
||||
export type PtyTicket = {
|
||||
readonly ticket: string
|
||||
}
|
||||
|
||||
export interface PtyTransportCapability {
|
||||
connectToken(input: LocationInput & { readonly ptyID: string }, options?: RequestOptions): Promise<PtyTicket>
|
||||
}
|
||||
|
||||
export type ShellOption = {
|
||||
readonly name: string
|
||||
readonly path: string
|
||||
readonly acceptable: boolean
|
||||
}
|
||||
|
||||
export interface ShellDiscoveryCapability {
|
||||
list(input?: LocationInput, options?: RequestOptions): Promise<readonly ShellOption[]>
|
||||
}
|
||||
|
||||
export interface RuntimeV1Capability {
|
||||
disposeAll(options?: RequestOptions): Promise<void>
|
||||
}
|
||||
|
||||
export type IntegrationMethod =
|
||||
| {
|
||||
readonly type: "oauth"
|
||||
readonly id: string
|
||||
readonly label: string
|
||||
readonly prompts?: readonly ProviderAuthPrompt[]
|
||||
}
|
||||
| {
|
||||
readonly type: "key"
|
||||
readonly label: string
|
||||
}
|
||||
| {
|
||||
readonly type: "environment"
|
||||
readonly label: string
|
||||
}
|
||||
|
||||
export type IntegrationConnection = {
|
||||
readonly id: string
|
||||
readonly label: string
|
||||
}
|
||||
|
||||
export type IntegrationInfo = {
|
||||
readonly id: string
|
||||
readonly name: string
|
||||
readonly methods: readonly IntegrationMethod[]
|
||||
readonly connections: readonly IntegrationConnection[]
|
||||
}
|
||||
|
||||
export type IntegrationAttempt = {
|
||||
readonly attemptID: string
|
||||
readonly url: string
|
||||
readonly instructions: string
|
||||
readonly mode: "auto" | "code"
|
||||
readonly time: {
|
||||
readonly created: number
|
||||
readonly expires: number
|
||||
}
|
||||
}
|
||||
|
||||
export type IntegrationAttemptStatus =
|
||||
| { readonly status: "pending" }
|
||||
| { readonly status: "complete" }
|
||||
| { readonly status: "failed"; readonly error?: string }
|
||||
| { readonly status: "expired" }
|
||||
|
||||
export interface IntegrationsV2Capability {
|
||||
list(input?: LocationInput, options?: RequestOptions): Promise<readonly IntegrationInfo[]>
|
||||
get(
|
||||
input: LocationInput & { readonly integrationID: string },
|
||||
options?: RequestOptions,
|
||||
): Promise<IntegrationInfo | null>
|
||||
connectKey(
|
||||
input: LocationInput & { readonly integrationID: string; readonly key: string; readonly label?: string },
|
||||
options?: RequestOptions,
|
||||
): Promise<void>
|
||||
connectOauth(
|
||||
input: LocationInput & {
|
||||
readonly integrationID: string
|
||||
readonly methodID: string
|
||||
readonly values: Readonly<Record<string, string>>
|
||||
readonly label?: string
|
||||
},
|
||||
options?: RequestOptions,
|
||||
): Promise<IntegrationAttempt>
|
||||
attemptStatus(
|
||||
input: LocationInput & { readonly attemptID: string },
|
||||
options?: RequestOptions,
|
||||
): Promise<IntegrationAttemptStatus>
|
||||
completeAttempt(
|
||||
input: LocationInput & { readonly attemptID: string; readonly code?: string },
|
||||
options?: RequestOptions,
|
||||
): Promise<void>
|
||||
cancelAttempt(input: LocationInput & { readonly attemptID: string }, options?: RequestOptions): Promise<void>
|
||||
renameCredential(
|
||||
input: LocationInput & { readonly credentialID: string; readonly label: string },
|
||||
options?: RequestOptions,
|
||||
): Promise<void>
|
||||
removeCredential(input: LocationInput & { readonly credentialID: string }, options?: RequestOptions): Promise<void>
|
||||
}
|
||||
|
||||
export type PendingSessionInput = {
|
||||
readonly id: string
|
||||
readonly sessionID: string
|
||||
readonly sequence: number
|
||||
readonly created: number
|
||||
readonly delivery?: "steer" | "queue"
|
||||
readonly raw: unknown
|
||||
}
|
||||
|
||||
export type SessionLogItem = {
|
||||
readonly sequence: number
|
||||
readonly event: AppEvent | { readonly type: "unknown"; readonly raw: unknown }
|
||||
}
|
||||
|
||||
export type InstructionEntry = {
|
||||
readonly key: string
|
||||
readonly value: JsonValue
|
||||
}
|
||||
|
||||
export interface SessionExtrasV2Capability {
|
||||
switchAgent(input: { readonly sessionID: string; readonly agent: string }, options?: RequestOptions): Promise<void>
|
||||
switchModel(input: { readonly sessionID: string; readonly model: ModelRef }, options?: RequestOptions): Promise<void>
|
||||
move(
|
||||
input: { readonly sessionID: string; readonly directory: string; readonly moveChanges?: boolean },
|
||||
options?: RequestOptions,
|
||||
): Promise<void>
|
||||
skill(
|
||||
input: { readonly sessionID: string; readonly id?: string; readonly skill: string; readonly resume?: boolean },
|
||||
options?: RequestOptions,
|
||||
): Promise<void>
|
||||
synthetic(
|
||||
input: {
|
||||
readonly sessionID: string
|
||||
readonly id?: string
|
||||
readonly text: string
|
||||
readonly description?: string
|
||||
readonly metadata?: Readonly<Record<string, JsonValue>>
|
||||
readonly delivery?: "steer" | "queue"
|
||||
readonly resume?: boolean
|
||||
},
|
||||
options?: RequestOptions,
|
||||
): Promise<PendingSessionInput>
|
||||
shell(
|
||||
input: { readonly sessionID: string; readonly id?: string; readonly command: string },
|
||||
options?: RequestOptions,
|
||||
): Promise<void>
|
||||
compact(
|
||||
input: { readonly sessionID: string; readonly id?: string },
|
||||
options?: RequestOptions,
|
||||
): Promise<PendingSessionInput>
|
||||
wait(input: { readonly sessionID: string }, options?: RequestOptions): Promise<void>
|
||||
context(input: { readonly sessionID: string }, options?: RequestOptions): Promise<readonly TimelineItem[]>
|
||||
pending(input: { readonly sessionID: string }, options?: RequestOptions): Promise<readonly PendingSessionInput[]>
|
||||
instructionEntries(
|
||||
input: { readonly sessionID: string },
|
||||
options?: RequestOptions,
|
||||
): Promise<readonly InstructionEntry[]>
|
||||
putInstructionEntry(
|
||||
input: { readonly sessionID: string; readonly key: string; readonly value: JsonValue },
|
||||
options?: RequestOptions,
|
||||
): Promise<void>
|
||||
removeInstructionEntry(
|
||||
input: { readonly sessionID: string; readonly key: string },
|
||||
options?: RequestOptions,
|
||||
): Promise<void>
|
||||
log(
|
||||
input: { readonly sessionID: string; readonly after?: number; readonly follow?: boolean },
|
||||
options?: RequestOptions,
|
||||
): AsyncIterable<SessionLogItem>
|
||||
background(input: { readonly sessionID: string }, options?: RequestOptions): Promise<void>
|
||||
stageRevert(
|
||||
input: { readonly sessionID: string; readonly messageID: string; readonly files?: readonly string[] },
|
||||
options?: RequestOptions,
|
||||
): Promise<{ readonly messageID: string }>
|
||||
clearRevert(input: { readonly sessionID: string }, options?: RequestOptions): Promise<void>
|
||||
commitRevert(input: { readonly sessionID: string }, options?: RequestOptions): Promise<void>
|
||||
}
|
||||
|
||||
export type ProjectDirectory = {
|
||||
readonly directory: string
|
||||
readonly strategy?: string
|
||||
}
|
||||
|
||||
export interface ProjectCopiesV2Capability {
|
||||
directories(
|
||||
input: LocationInput & { readonly projectID: string },
|
||||
options?: RequestOptions,
|
||||
): Promise<readonly ProjectDirectory[]>
|
||||
create(
|
||||
input: LocationInput & {
|
||||
readonly projectID: string
|
||||
readonly strategy: string
|
||||
readonly directory: string
|
||||
readonly name?: string
|
||||
},
|
||||
options?: RequestOptions,
|
||||
): Promise<{ readonly directory: string }>
|
||||
remove(
|
||||
input: LocationInput & { readonly projectID: string; readonly directory: string; readonly force: boolean },
|
||||
options?: RequestOptions,
|
||||
): Promise<void>
|
||||
refresh(input: LocationInput & { readonly projectID: string }, options?: RequestOptions): Promise<void>
|
||||
}
|
||||
|
||||
export type FormField =
|
||||
| {
|
||||
readonly type: "string"
|
||||
readonly key: string
|
||||
readonly label: string
|
||||
readonly required?: boolean
|
||||
readonly description?: string
|
||||
}
|
||||
| {
|
||||
readonly type: "number" | "integer"
|
||||
readonly key: string
|
||||
readonly label: string
|
||||
readonly required?: boolean
|
||||
readonly description?: string
|
||||
}
|
||||
| {
|
||||
readonly type: "boolean"
|
||||
readonly key: string
|
||||
readonly label: string
|
||||
readonly description?: string
|
||||
}
|
||||
| {
|
||||
readonly type: "multiselect"
|
||||
readonly key: string
|
||||
readonly label: string
|
||||
readonly options: readonly string[]
|
||||
readonly required?: boolean
|
||||
readonly description?: string
|
||||
}
|
||||
| {
|
||||
readonly type: "external"
|
||||
readonly key: string
|
||||
readonly label: string
|
||||
readonly url: string
|
||||
readonly description?: string
|
||||
}
|
||||
|
||||
export type FormInfo = {
|
||||
readonly id: string
|
||||
readonly sessionID: string
|
||||
readonly title: string
|
||||
readonly metadata?: Readonly<Record<string, JsonValue>>
|
||||
readonly fields: readonly FormField[]
|
||||
}
|
||||
|
||||
export type FormAnswer = Readonly<Record<string, string | number | boolean | readonly string[]>>
|
||||
|
||||
export type FormState =
|
||||
| { readonly status: "pending" }
|
||||
| { readonly status: "answered"; readonly answer: FormAnswer }
|
||||
| { readonly status: "cancelled" }
|
||||
|
||||
export interface FormsV2Capability {
|
||||
pending(input?: LocationInput, options?: RequestOptions): Promise<readonly FormInfo[]>
|
||||
list(input: { readonly sessionID: string }, options?: RequestOptions): Promise<readonly FormInfo[]>
|
||||
create(
|
||||
input: {
|
||||
readonly sessionID: string
|
||||
readonly id?: string
|
||||
readonly title: string
|
||||
readonly metadata?: Readonly<Record<string, JsonValue>>
|
||||
readonly fields: readonly FormField[]
|
||||
},
|
||||
options?: RequestOptions,
|
||||
): Promise<FormInfo>
|
||||
get(input: { readonly sessionID: string; readonly formID: string }, options?: RequestOptions): Promise<FormInfo>
|
||||
state(input: { readonly sessionID: string; readonly formID: string }, options?: RequestOptions): Promise<FormState>
|
||||
reply(
|
||||
input: { readonly sessionID: string; readonly formID: string; readonly answer: FormAnswer },
|
||||
options?: RequestOptions,
|
||||
): Promise<void>
|
||||
cancel(input: { readonly sessionID: string; readonly formID: string }, options?: RequestOptions): Promise<void>
|
||||
}
|
||||
|
||||
export type SavedPermission = {
|
||||
readonly id: string
|
||||
readonly projectID: string
|
||||
readonly action: string
|
||||
readonly resource: string
|
||||
}
|
||||
|
||||
export interface SavedPermissionsV2Capability {
|
||||
list(input?: { readonly projectID?: string }, options?: RequestOptions): Promise<readonly SavedPermission[]>
|
||||
remove(input: { readonly id: string }, options?: RequestOptions): Promise<void>
|
||||
}
|
||||
|
||||
export type ShellProcess = {
|
||||
readonly id: string
|
||||
readonly command: string
|
||||
readonly cwd: string
|
||||
readonly status: "running" | "exited"
|
||||
readonly created: number
|
||||
readonly exitCode?: number
|
||||
readonly metadata?: Readonly<Record<string, JsonValue>>
|
||||
}
|
||||
|
||||
export type ShellOutput = {
|
||||
readonly output: string
|
||||
readonly cursor: number
|
||||
readonly size: number
|
||||
readonly truncated: boolean
|
||||
}
|
||||
|
||||
export interface ShellsV2Capability {
|
||||
list(input?: LocationInput, options?: RequestOptions): Promise<readonly ShellProcess[]>
|
||||
create(
|
||||
input: LocationInput & {
|
||||
readonly command: string
|
||||
readonly cwd?: string
|
||||
readonly timeout: number
|
||||
readonly metadata?: Readonly<Record<string, JsonValue>>
|
||||
},
|
||||
options?: RequestOptions,
|
||||
): Promise<ShellProcess>
|
||||
get(input: LocationInput & { readonly id: string }, options?: RequestOptions): Promise<ShellProcess>
|
||||
setTimeout(
|
||||
input: LocationInput & { readonly id: string; readonly timeout: number },
|
||||
options?: RequestOptions,
|
||||
): Promise<ShellProcess>
|
||||
output(
|
||||
input: LocationInput & { readonly id: string; readonly cursor?: number; readonly limit?: number },
|
||||
options?: RequestOptions,
|
||||
): Promise<ShellOutput>
|
||||
remove(input: LocationInput & { readonly id: string }, options?: RequestOptions): Promise<void>
|
||||
}
|
||||
|
||||
export type ServerInfo = {
|
||||
readonly urls: readonly string[]
|
||||
}
|
||||
|
||||
export type LocationInfo = LocationRef & {
|
||||
readonly project: CurrentProject
|
||||
}
|
||||
|
||||
export type PluginInfo = {
|
||||
readonly id: string
|
||||
}
|
||||
|
||||
export type SkillInfo = {
|
||||
readonly id: string
|
||||
readonly name: string
|
||||
readonly description?: string
|
||||
readonly slash?: boolean
|
||||
readonly autoinvoke?: boolean
|
||||
readonly location: LocationRef
|
||||
readonly content: string
|
||||
}
|
||||
|
||||
export interface DiscoveryV2Capability {
|
||||
server(options?: RequestOptions): Promise<ServerInfo>
|
||||
location(input?: LocationInput, options?: RequestOptions): Promise<LocationInfo>
|
||||
plugins(input?: LocationInput, options?: RequestOptions): Promise<readonly PluginInfo[]>
|
||||
skills(input?: LocationInput, options?: RequestOptions): Promise<readonly SkillInfo[]>
|
||||
models(input?: LocationInput, options?: RequestOptions): Promise<readonly AppModel[]>
|
||||
defaultModel(input?: LocationInput, options?: RequestOptions): Promise<AppModel | null>
|
||||
generateText(
|
||||
input: LocationInput & { readonly prompt: string; readonly model?: ModelRef },
|
||||
options?: RequestOptions,
|
||||
): Promise<string>
|
||||
loadedLocations(options?: RequestOptions): Promise<readonly LocationRef[]>
|
||||
}
|
||||
|
||||
export interface Capabilities {
|
||||
readonly configuration?: ConfigurationCapability
|
||||
readonly providerAuthV1?: ProviderAuthV1Capability
|
||||
readonly integrationsV2?: IntegrationsV2Capability
|
||||
readonly projectEditing?: ProjectEditingCapability
|
||||
readonly worktreesV1?: WorktreesV1Capability
|
||||
readonly projectCopiesV2?: ProjectCopiesV2Capability
|
||||
readonly sessionExtrasV1?: SessionExtrasV1Capability
|
||||
readonly sessionExtrasV2?: SessionExtrasV2Capability
|
||||
readonly lsp?: LspCapability
|
||||
readonly mcpControl?: McpControlCapability
|
||||
readonly pathInfo?: PathInfoCapability
|
||||
readonly vcsInfo?: VcsInfoCapability
|
||||
readonly decoratedFiles?: DecoratedFileCapability
|
||||
readonly ptyTransport?: PtyTransportCapability
|
||||
readonly shellDiscovery?: ShellDiscoveryCapability
|
||||
readonly shellsV2?: ShellsV2Capability
|
||||
readonly formsV2?: FormsV2Capability
|
||||
readonly savedPermissionsV2?: SavedPermissionsV2Capability
|
||||
readonly discoveryV2?: DiscoveryV2Capability
|
||||
readonly runtimeV1?: RuntimeV1Capability
|
||||
}
|
||||
|
||||
export interface AppClient {
|
||||
readonly version: "v1" | "v2"
|
||||
readonly common: CommonClient
|
||||
readonly capabilities: Capabilities
|
||||
}
|
||||
Reference in New Issue
Block a user