feat(app): add versioned backend interface

This commit is contained in:
Brendan Allan
2026-07-13 22:37:59 +08:00
parent 2987c30238
commit 4d314f6e04
5 changed files with 2328 additions and 0 deletions
+4
View File
@@ -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"],
+2
View File
@@ -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",
+140
View File
@@ -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"] }])
})
})
+992
View File
@@ -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"],
}
}
+1190
View File
@@ -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
}