fix(cli): preserve v2 mini interfaces
This commit is contained in:
parent
31b8231195
commit
262add0801
22 changed files with 352 additions and 1707 deletions
3
bun.lock
3
bun.lock
|
|
@ -98,7 +98,7 @@
|
||||||
},
|
},
|
||||||
"packages/cli": {
|
"packages/cli": {
|
||||||
"name": "@opencode-ai/cli",
|
"name": "@opencode-ai/cli",
|
||||||
"version": "1.17.20",
|
"version": "1.17.18",
|
||||||
"bin": {
|
"bin": {
|
||||||
"opencode2": "./bin/opencode2.cjs",
|
"opencode2": "./bin/opencode2.cjs",
|
||||||
},
|
},
|
||||||
|
|
@ -108,7 +108,6 @@
|
||||||
"@opencode-ai/core": "workspace:*",
|
"@opencode-ai/core": "workspace:*",
|
||||||
"@opencode-ai/plugin": "workspace:*",
|
"@opencode-ai/plugin": "workspace:*",
|
||||||
"@opencode-ai/schema": "workspace:*",
|
"@opencode-ai/schema": "workspace:*",
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
|
||||||
"@opencode-ai/server": "workspace:*",
|
"@opencode-ai/server": "workspace:*",
|
||||||
"@opencode-ai/tui": "workspace:*",
|
"@opencode-ai/tui": "workspace:*",
|
||||||
"@opentui/core": "catalog:",
|
"@opentui/core": "catalog:",
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"name": "@opencode-ai/cli",
|
"name": "@opencode-ai/cli",
|
||||||
"version": "1.17.20",
|
"version": "1.17.18",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|
@ -36,7 +36,6 @@
|
||||||
"@opencode-ai/core": "workspace:*",
|
"@opencode-ai/core": "workspace:*",
|
||||||
"@opencode-ai/plugin": "workspace:*",
|
"@opencode-ai/plugin": "workspace:*",
|
||||||
"@opencode-ai/schema": "workspace:*",
|
"@opencode-ai/schema": "workspace:*",
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
|
||||||
"@opencode-ai/server": "workspace:*",
|
"@opencode-ai/server": "workspace:*",
|
||||||
"@opencode-ai/tui": "workspace:*",
|
"@opencode-ai/tui": "workspace:*",
|
||||||
"@opentui/core": "catalog:",
|
"@opentui/core": "catalog:",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { EOL } from "os"
|
import { EOL } from "os"
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"
|
import { OpenCode } from "@opencode-ai/client"
|
||||||
import { Commands } from "../../commands"
|
import { Commands } from "../../commands"
|
||||||
import { Runtime } from "../../../framework/runtime"
|
import { Runtime } from "../../../framework/runtime"
|
||||||
import { Service } from "@opencode-ai/client/effect"
|
import { Service } from "@opencode-ai/client/effect"
|
||||||
|
|
@ -12,11 +12,11 @@ export default Runtime.handler(
|
||||||
const options = yield* ServiceConfig.options()
|
const options = yield* ServiceConfig.options()
|
||||||
const found = yield* Service.discover(options)
|
const found = yield* Service.discover(options)
|
||||||
const endpoint = found ?? (yield* Service.start(options))
|
const endpoint = found ?? (yield* Service.start(options))
|
||||||
const client = createOpencodeClient({ baseUrl: endpoint.url, headers: Service.headers(endpoint) })
|
const client = OpenCode.make({ baseUrl: endpoint.url, headers: Service.headers(endpoint) })
|
||||||
const response = yield* Effect.promise(() => client.v2.agent.list({ location: { directory: process.cwd() } }))
|
const response = yield* Effect.promise(() => client.agent.list({ location: { directory: process.cwd() } }))
|
||||||
process.stdout.write(
|
process.stdout.write(
|
||||||
JSON.stringify(
|
JSON.stringify(
|
||||||
response.data?.data.toSorted((a, b) => a.id.localeCompare(b.id)),
|
response.data.toSorted((a, b) => a.id.localeCompare(b.id)),
|
||||||
null,
|
null,
|
||||||
2,
|
2,
|
||||||
) + EOL,
|
) + EOL,
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import { EOL } from "node:os"
|
import { EOL } from "node:os"
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import {
|
import {
|
||||||
createOpencodeClient,
|
OpenCode,
|
||||||
type IntegrationAttemptStatus,
|
type IntegrationAttemptStatus,
|
||||||
type IntegrationOAuthMethod,
|
type IntegrationOAuthMethod,
|
||||||
type OpencodeClient,
|
type OpenCodeClient,
|
||||||
} from "@opencode-ai/sdk/v2/client"
|
} from "@opencode-ai/client"
|
||||||
import { Commands } from "../../commands"
|
import { Commands } from "../../commands"
|
||||||
import { Runtime } from "../../../framework/runtime"
|
import { Runtime } from "../../../framework/runtime"
|
||||||
import { Service } from "@opencode-ai/client/effect"
|
import { Service } from "@opencode-ai/client/effect"
|
||||||
|
|
@ -20,7 +20,7 @@ export default Runtime.handler(
|
||||||
const options = yield* ServiceConfig.options()
|
const options = yield* ServiceConfig.options()
|
||||||
const found = yield* Service.discover(options)
|
const found = yield* Service.discover(options)
|
||||||
const endpoint = found ?? (yield* Service.start(options))
|
const endpoint = found ?? (yield* Service.start(options))
|
||||||
const client = createOpencodeClient({ baseUrl: endpoint.url, headers: Service.headers(endpoint) })
|
const client = OpenCode.make({ baseUrl: endpoint.url, headers: Service.headers(endpoint) })
|
||||||
|
|
||||||
const integration = yield* resolveIntegration(client, input.name, location)
|
const integration = yield* resolveIntegration(client, input.name, location)
|
||||||
if (!integration)
|
if (!integration)
|
||||||
|
|
@ -32,10 +32,9 @@ export default Runtime.handler(
|
||||||
return yield* Effect.fail(new Error(`MCP server "${input.name}" is not an OAuth-capable remote server`))
|
return yield* Effect.fail(new Error(`MCP server "${input.name}" is not an OAuth-capable remote server`))
|
||||||
|
|
||||||
const started = yield* Effect.promise(() =>
|
const started = yield* Effect.promise(() =>
|
||||||
client.v2.integration.connect.oauth({ integrationID: integration.id, methodID: method.id, inputs: {}, location }),
|
client.integration.connect.oauth({ integrationID: integration.id, methodID: method.id, inputs: {}, location }),
|
||||||
)
|
)
|
||||||
const attempt = started.data?.data
|
const attempt = started.data
|
||||||
if (!attempt) return yield* Effect.fail(new Error(started.error?.message ?? "Failed to start OAuth attempt"))
|
|
||||||
if (attempt.mode === "code")
|
if (attempt.mode === "code")
|
||||||
return yield* Effect.fail(new Error("This server requires manual code entry, which the CLI does not support"))
|
return yield* Effect.fail(new Error("This server requires manual code entry, which the CLI does not support"))
|
||||||
|
|
||||||
|
|
@ -52,13 +51,14 @@ export default Runtime.handler(
|
||||||
)
|
)
|
||||||
|
|
||||||
const poll = (
|
const poll = (
|
||||||
client: OpencodeClient,
|
client: OpenCodeClient,
|
||||||
attemptID: string,
|
attemptID: string,
|
||||||
): Effect.Effect<Exclude<IntegrationAttemptStatus, { status: "pending" }>> =>
|
): Effect.Effect<Exclude<IntegrationAttemptStatus, { status: "pending" }>> =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const response = yield* Effect.promise(() => client.v2.integration.attempt.status({ attemptID, location }))
|
const status = yield* Effect.promise(() => client.integration.attempt.status({ attemptID, location })).pipe(
|
||||||
const status = response.data?.data
|
Effect.map((result) => result.data),
|
||||||
if (!status || status.status === "pending") {
|
)
|
||||||
|
if (status.status === "pending") {
|
||||||
yield* Effect.sleep("1 second")
|
yield* Effect.sleep("1 second")
|
||||||
return yield* poll(client, attemptID)
|
return yield* poll(client, attemptID)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { EOL } from "node:os"
|
import { EOL } from "node:os"
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { createOpencodeClient, type McpServer } from "@opencode-ai/sdk/v2/client"
|
import { OpenCode, type McpServer } from "@opencode-ai/client"
|
||||||
import { Commands } from "../../commands"
|
import { Commands } from "../../commands"
|
||||||
import { Runtime } from "../../../framework/runtime"
|
import { Runtime } from "../../../framework/runtime"
|
||||||
import { Service } from "@opencode-ai/client/effect"
|
import { Service } from "@opencode-ai/client/effect"
|
||||||
|
|
@ -12,9 +12,9 @@ export default Runtime.handler(
|
||||||
const options = yield* ServiceConfig.options()
|
const options = yield* ServiceConfig.options()
|
||||||
const found = yield* Service.discover(options)
|
const found = yield* Service.discover(options)
|
||||||
const endpoint = found ?? (yield* Service.start(options))
|
const endpoint = found ?? (yield* Service.start(options))
|
||||||
const client = createOpencodeClient({ baseUrl: endpoint.url, headers: Service.headers(endpoint) })
|
const client = OpenCode.make({ baseUrl: endpoint.url, headers: Service.headers(endpoint) })
|
||||||
const response = yield* Effect.promise(() => client.v2.mcp.list({ location: { directory: process.cwd() } }))
|
const response = yield* Effect.promise(() => client.mcp.list({ location: { directory: process.cwd() } }))
|
||||||
const servers = (response.data?.data ?? []).toSorted((a, b) => a.name.localeCompare(b.name))
|
const servers = response.data.toSorted((a, b) => a.name.localeCompare(b.name))
|
||||||
if (servers.length === 0) {
|
if (servers.length === 0) {
|
||||||
process.stdout.write("No MCP servers configured" + EOL)
|
process.stdout.write("No MCP servers configured" + EOL)
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { EOL } from "node:os"
|
import { EOL } from "node:os"
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"
|
import { OpenCode } from "@opencode-ai/client"
|
||||||
import { Commands } from "../../commands"
|
import { Commands } from "../../commands"
|
||||||
import { Runtime } from "../../../framework/runtime"
|
import { Runtime } from "../../../framework/runtime"
|
||||||
import { Service } from "@opencode-ai/client/effect"
|
import { Service } from "@opencode-ai/client/effect"
|
||||||
|
|
@ -15,7 +15,7 @@ export default Runtime.handler(
|
||||||
const options = yield* ServiceConfig.options()
|
const options = yield* ServiceConfig.options()
|
||||||
const found = yield* Service.discover(options)
|
const found = yield* Service.discover(options)
|
||||||
const endpoint = found ?? (yield* Service.start(options))
|
const endpoint = found ?? (yield* Service.start(options))
|
||||||
const client = createOpencodeClient({ baseUrl: endpoint.url, headers: Service.headers(endpoint) })
|
const client = OpenCode.make({ baseUrl: endpoint.url, headers: Service.headers(endpoint) })
|
||||||
|
|
||||||
const integration = yield* resolveIntegration(client, input.name, location)
|
const integration = yield* resolveIntegration(client, input.name, location)
|
||||||
if (!integration) {
|
if (!integration) {
|
||||||
|
|
@ -31,7 +31,7 @@ export default Runtime.handler(
|
||||||
|
|
||||||
yield* Effect.forEach(
|
yield* Effect.forEach(
|
||||||
credentials,
|
credentials,
|
||||||
(connection) => Effect.promise(() => client.v2.credential.remove({ credentialID: connection.id, location })),
|
(connection) => Effect.promise(() => client.credential.remove({ credentialID: connection.id, location })),
|
||||||
{ discard: true },
|
{ discard: true },
|
||||||
)
|
)
|
||||||
process.stdout.write(`Removed OAuth credentials for ${input.name}` + EOL)
|
process.stdout.write(`Removed OAuth credentials for ${input.name}` + EOL)
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,18 @@
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import type { OpencodeClient } from "@opencode-ai/sdk/v2/client"
|
import type { OpenCodeClient } from "@opencode-ai/client"
|
||||||
|
|
||||||
// Resolve through the MCP-owned integrationID rather than matching integration names: the shared
|
// Resolve through the MCP-owned integrationID rather than matching integration names: the shared
|
||||||
// integration registry also holds provider/plugin integrations, whose names could collide with a server.
|
// integration registry also holds provider/plugin integrations, whose names could collide with a server.
|
||||||
// Fails when the server is unknown; returns undefined when the server has no integration (e.g. a local
|
// Fails when the server is unknown; returns undefined when the server has no integration (e.g. a local
|
||||||
// or anonymous server), leaving that case for the caller to interpret.
|
// or anonymous server), leaving that case for the caller to interpret.
|
||||||
export const resolveIntegration = (client: OpencodeClient, name: string, location: { directory: string }) =>
|
export const resolveIntegration = (client: OpenCodeClient, name: string, location: { directory: string }) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const servers = yield* Effect.promise(() => client.v2.mcp.list({ location }))
|
const servers = yield* Effect.promise(() => client.mcp.list({ location }))
|
||||||
const server = (servers.data?.data ?? []).find((entry) => entry.name === name)
|
const server = servers.data.find((entry) => entry.name === name)
|
||||||
if (!server) return yield* Effect.fail(new Error(`MCP server not found: ${name}`))
|
if (!server) return yield* Effect.fail(new Error(`MCP server not found: ${name}`))
|
||||||
const integrationID = server.integrationID
|
const integrationID = server.integrationID
|
||||||
if (!integrationID) return undefined
|
if (!integrationID) return undefined
|
||||||
const found = yield* Effect.promise(() => client.v2.integration.get({ integrationID, location }))
|
return yield* Effect.promise(() => client.integration.get({ integrationID, location })).pipe(
|
||||||
return found.data?.data
|
Effect.map((result) => result.data ?? undefined),
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
// Demo mode for testing direct interactive mode without a real SDK.
|
// Demo mode for testing direct interactive mode without a real SDK.
|
||||||
//
|
//
|
||||||
// Enabled with `--demo`. Intercepts prompt submissions and generates synthetic
|
// Enabled with `--demo`. Intercepts prompt submissions and drives the same
|
||||||
// SDK events that feed through the real reducer and footer pipeline. This
|
// presentation commits and footer actions as the live transport. This
|
||||||
// lets you test scrollback formatting, permission UI, question UI, and tool
|
// lets you test scrollback formatting, permission UI, question UI, and tool
|
||||||
// snapshots without making actual model calls. Pass a demo slash command as
|
// snapshots without making actual model calls. Pass a demo slash command as
|
||||||
// the initial interactive message to trigger a preview immediately.
|
// the initial interactive message to trigger a preview immediately.
|
||||||
|
|
@ -15,10 +15,18 @@
|
||||||
// Demo mode also handles permission and question replies locally, completing
|
// Demo mode also handles permission and question replies locally, completing
|
||||||
// or failing the synthetic tool parts as appropriate.
|
// or failing the synthetic tool parts as appropriate.
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import type { Event, ToolPart } from "@opencode-ai/sdk/v2"
|
import type { PermissionV2Request, QuestionV2Request } from "@opencode-ai/client/promise"
|
||||||
import { createSessionData, reduceSessionData, type SessionData } from "./session-data"
|
|
||||||
import { writeSessionOutput } from "./stream"
|
import { writeSessionOutput } from "./stream"
|
||||||
import type { FooterApi, PermissionReply, QuestionReject, QuestionReply, RunPrompt, StreamCommit } from "./types"
|
import { toolCommit } from "./stream-v2.subagent"
|
||||||
|
import type {
|
||||||
|
FooterApi,
|
||||||
|
MiniToolPart,
|
||||||
|
PermissionReply,
|
||||||
|
QuestionReject,
|
||||||
|
QuestionReply,
|
||||||
|
RunPrompt,
|
||||||
|
StreamCommit,
|
||||||
|
} from "./types"
|
||||||
|
|
||||||
const KINDS = [
|
const KINDS = [
|
||||||
"markdown",
|
"markdown",
|
||||||
|
|
@ -124,7 +132,7 @@ type Permit = {
|
||||||
ref: Ref
|
ref: Ref
|
||||||
permission: string
|
permission: string
|
||||||
patterns: string[]
|
patterns: string[]
|
||||||
metadata?: Record<string, unknown>
|
metadata?: PermissionV2Request["metadata"]
|
||||||
always: string[]
|
always: string[]
|
||||||
done: Perm["done"]
|
done: Perm["done"]
|
||||||
}
|
}
|
||||||
|
|
@ -132,9 +140,7 @@ type Permit = {
|
||||||
type State = {
|
type State = {
|
||||||
id: string
|
id: string
|
||||||
thinking: boolean
|
thinking: boolean
|
||||||
data: SessionData
|
|
||||||
footer: FooterApi
|
footer: FooterApi
|
||||||
limits: () => Record<string, number>
|
|
||||||
msg: number
|
msg: number
|
||||||
part: number
|
part: number
|
||||||
call: number
|
call: number
|
||||||
|
|
@ -142,12 +148,12 @@ type State = {
|
||||||
ask: number
|
ask: number
|
||||||
perms: Map<string, Perm>
|
perms: Map<string, Perm>
|
||||||
asks: Map<string, Ask>
|
asks: Map<string, Ask>
|
||||||
|
started: Set<string>
|
||||||
}
|
}
|
||||||
|
|
||||||
type Input = {
|
type Input = {
|
||||||
sessionID: string
|
sessionID: string
|
||||||
thinking: boolean
|
thinking: boolean
|
||||||
limits: () => Record<string, number>
|
|
||||||
footer: FooterApi
|
footer: FooterApi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -255,185 +261,69 @@ function take(state: State, key: "msg" | "part" | "call" | "perm" | "ask", prefi
|
||||||
return `demo_${prefix}_${state[key]}`
|
return `demo_${prefix}_${state[key]}`
|
||||||
}
|
}
|
||||||
|
|
||||||
function feed(state: State, event: Event): void {
|
function present(state: State, commits: StreamCommit[], view?: QuestionV2Request | PermissionV2Request): void {
|
||||||
const out = reduceSessionData({
|
|
||||||
data: state.data,
|
|
||||||
event,
|
|
||||||
sessionID: state.id,
|
|
||||||
thinking: state.thinking,
|
|
||||||
limits: state.limits(),
|
|
||||||
})
|
|
||||||
state.data = out.data
|
|
||||||
writeSessionOutput(
|
writeSessionOutput(
|
||||||
|
{ footer: state.footer },
|
||||||
{
|
{
|
||||||
footer: state.footer,
|
commits,
|
||||||
|
footer: view
|
||||||
|
? {
|
||||||
|
view: "action" in view ? { type: "permission", request: view } : { type: "question", request: view },
|
||||||
|
patch: { status: "action" in view ? "awaiting permission" : "awaiting answer" },
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
},
|
},
|
||||||
out,
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearBlocker(state: State): void {
|
||||||
|
writeSessionOutput(
|
||||||
|
{ footer: state.footer },
|
||||||
|
{ commits: [], footer: { view: { type: "prompt" }, patch: { status: "" } } },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function open(state: State): string {
|
function open(state: State): string {
|
||||||
const id = take(state, "msg", "msg")
|
return take(state, "msg", "msg")
|
||||||
feed(state, {
|
|
||||||
type: "message.updated",
|
|
||||||
properties: {
|
|
||||||
sessionID: state.id,
|
|
||||||
info: {
|
|
||||||
id,
|
|
||||||
sessionID: state.id,
|
|
||||||
role: "assistant",
|
|
||||||
time: {
|
|
||||||
created: Date.now(),
|
|
||||||
},
|
|
||||||
parentID: `user_${id}`,
|
|
||||||
modelID: "demo",
|
|
||||||
providerID: "demo",
|
|
||||||
mode: "demo",
|
|
||||||
agent: "demo",
|
|
||||||
path: {
|
|
||||||
cwd: process.cwd(),
|
|
||||||
root: process.cwd(),
|
|
||||||
},
|
|
||||||
cost: 0.001,
|
|
||||||
tokens: {
|
|
||||||
input: 120,
|
|
||||||
output: 320,
|
|
||||||
reasoning: 80,
|
|
||||||
cache: {
|
|
||||||
read: 0,
|
|
||||||
write: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} as Event)
|
|
||||||
return id
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function emitText(state: State, body: string, signal?: AbortSignal): Promise<void> {
|
async function emitText(state: State, body: string, signal?: AbortSignal): Promise<void> {
|
||||||
const msg = open(state)
|
const msg = open(state)
|
||||||
const part = take(state, "part", "part")
|
const part = take(state, "part", "part")
|
||||||
const start = Date.now()
|
|
||||||
|
|
||||||
feed(state, {
|
|
||||||
type: "message.part.updated",
|
|
||||||
properties: {
|
|
||||||
sessionID: state.id,
|
|
||||||
time: Date.now(),
|
|
||||||
part: {
|
|
||||||
id: part,
|
|
||||||
sessionID: state.id,
|
|
||||||
messageID: msg,
|
|
||||||
type: "text",
|
|
||||||
text: "",
|
|
||||||
time: {
|
|
||||||
start,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} as Event)
|
|
||||||
|
|
||||||
let next = ""
|
|
||||||
for (const item of split(body)) {
|
for (const item of split(body)) {
|
||||||
if (signal?.aborted) {
|
if (signal?.aborted) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
next += item
|
present(state, [{ kind: "assistant", source: "assistant", text: item, phase: "progress", messageID: msg, partID: part }])
|
||||||
feed(state, {
|
|
||||||
type: "message.part.delta",
|
|
||||||
properties: {
|
|
||||||
sessionID: state.id,
|
|
||||||
messageID: msg,
|
|
||||||
partID: part,
|
|
||||||
field: "text",
|
|
||||||
delta: item,
|
|
||||||
},
|
|
||||||
} as Event)
|
|
||||||
await wait(45, signal)
|
await wait(45, signal)
|
||||||
}
|
}
|
||||||
|
|
||||||
feed(state, {
|
|
||||||
type: "message.part.updated",
|
|
||||||
properties: {
|
|
||||||
sessionID: state.id,
|
|
||||||
time: Date.now(),
|
|
||||||
part: {
|
|
||||||
id: part,
|
|
||||||
sessionID: state.id,
|
|
||||||
messageID: msg,
|
|
||||||
type: "text",
|
|
||||||
text: next,
|
|
||||||
time: {
|
|
||||||
start,
|
|
||||||
end: Date.now(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} as Event)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function emitReasoning(state: State, body: string, signal?: AbortSignal): Promise<void> {
|
async function emitReasoning(state: State, body: string, signal?: AbortSignal): Promise<void> {
|
||||||
const msg = open(state)
|
const msg = open(state)
|
||||||
const part = take(state, "part", "part")
|
const part = take(state, "part", "part")
|
||||||
const start = Date.now()
|
let first = true
|
||||||
|
|
||||||
feed(state, {
|
|
||||||
type: "message.part.updated",
|
|
||||||
properties: {
|
|
||||||
sessionID: state.id,
|
|
||||||
time: Date.now(),
|
|
||||||
part: {
|
|
||||||
id: part,
|
|
||||||
sessionID: state.id,
|
|
||||||
messageID: msg,
|
|
||||||
type: "reasoning",
|
|
||||||
text: "",
|
|
||||||
time: {
|
|
||||||
start,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} as Event)
|
|
||||||
|
|
||||||
let next = ""
|
|
||||||
for (const item of split(body)) {
|
for (const item of split(body)) {
|
||||||
if (signal?.aborted) {
|
if (signal?.aborted) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
next += item
|
if (state.thinking) {
|
||||||
feed(state, {
|
present(state, [
|
||||||
type: "message.part.delta",
|
{
|
||||||
properties: {
|
kind: "reasoning",
|
||||||
sessionID: state.id,
|
source: "reasoning",
|
||||||
messageID: msg,
|
text: first ? `Thinking: ${item.replace(/\[REDACTED\]/g, "")}` : item.replace(/\[REDACTED\]/g, ""),
|
||||||
partID: part,
|
phase: "progress",
|
||||||
field: "text",
|
messageID: msg,
|
||||||
delta: item,
|
partID: part,
|
||||||
},
|
},
|
||||||
} as Event)
|
])
|
||||||
|
first = false
|
||||||
|
}
|
||||||
await wait(45, signal)
|
await wait(45, signal)
|
||||||
}
|
}
|
||||||
|
|
||||||
feed(state, {
|
|
||||||
type: "message.part.updated",
|
|
||||||
properties: {
|
|
||||||
sessionID: state.id,
|
|
||||||
time: Date.now(),
|
|
||||||
part: {
|
|
||||||
id: part,
|
|
||||||
sessionID: state.id,
|
|
||||||
messageID: msg,
|
|
||||||
type: "reasoning",
|
|
||||||
text: next,
|
|
||||||
time: {
|
|
||||||
start,
|
|
||||||
end: Date.now(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} as Event)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function make(state: State, tool: string, input: Record<string, unknown>): Ref {
|
function make(state: State, tool: string, input: Record<string, unknown>): Ref {
|
||||||
|
|
@ -448,29 +338,23 @@ function make(state: State, tool: string, input: Record<string, unknown>): Ref {
|
||||||
}
|
}
|
||||||
|
|
||||||
function startTool(state: State, ref: Ref, metadata: Record<string, unknown> = {}): void {
|
function startTool(state: State, ref: Ref, metadata: Record<string, unknown> = {}): void {
|
||||||
feed(state, {
|
state.started.add(ref.part)
|
||||||
type: "message.part.updated",
|
present(
|
||||||
properties: {
|
state,
|
||||||
sessionID: state.id,
|
[
|
||||||
time: Date.now(),
|
toolCommit(
|
||||||
part: {
|
{
|
||||||
id: ref.part,
|
id: ref.part,
|
||||||
sessionID: state.id,
|
sessionID: state.id,
|
||||||
messageID: ref.msg,
|
messageID: ref.msg,
|
||||||
type: "tool",
|
callID: ref.call,
|
||||||
callID: ref.call,
|
tool: ref.tool,
|
||||||
tool: ref.tool,
|
state: { status: "running", input: ref.input, metadata, time: { start: ref.start } },
|
||||||
state: {
|
|
||||||
status: "running",
|
|
||||||
input: ref.input,
|
|
||||||
metadata,
|
|
||||||
time: {
|
|
||||||
start: ref.start,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
"start",
|
||||||
},
|
),
|
||||||
} as Event)
|
],
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function askPermission(state: State, item: Permit): void {
|
function askPermission(state: State, item: Permit): void {
|
||||||
|
|
@ -482,21 +366,15 @@ function askPermission(state: State, item: Permit): void {
|
||||||
done: item.done,
|
done: item.done,
|
||||||
})
|
})
|
||||||
|
|
||||||
feed(state, {
|
present(state, [], {
|
||||||
type: "permission.asked",
|
id,
|
||||||
properties: {
|
sessionID: state.id,
|
||||||
id,
|
action: item.permission,
|
||||||
sessionID: state.id,
|
resources: item.patterns,
|
||||||
permission: item.permission,
|
metadata: item.metadata ?? {},
|
||||||
patterns: item.patterns,
|
save: item.always,
|
||||||
metadata: item.metadata ?? {},
|
source: { type: "tool", messageID: item.ref.msg, callID: item.ref.call },
|
||||||
always: item.always,
|
})
|
||||||
tool: {
|
|
||||||
messageID: item.ref.msg,
|
|
||||||
callID: item.ref.call,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} as Event)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function doneTool(
|
function doneTool(
|
||||||
|
|
@ -508,77 +386,53 @@ function doneTool(
|
||||||
metadata?: Record<string, unknown>
|
metadata?: Record<string, unknown>
|
||||||
},
|
},
|
||||||
): void {
|
): void {
|
||||||
feed(state, {
|
if (!state.started.has(ref.part)) startTool(state, ref)
|
||||||
type: "message.part.updated",
|
const part: MiniToolPart = {
|
||||||
properties: {
|
id: ref.part,
|
||||||
sessionID: state.id,
|
sessionID: state.id,
|
||||||
time: Date.now(),
|
messageID: ref.msg,
|
||||||
part: {
|
callID: ref.call,
|
||||||
id: ref.part,
|
tool: ref.tool,
|
||||||
sessionID: state.id,
|
state: {
|
||||||
messageID: ref.msg,
|
status: "completed",
|
||||||
type: "tool",
|
input: ref.input,
|
||||||
callID: ref.call,
|
output: output.output,
|
||||||
tool: ref.tool,
|
title: output.title,
|
||||||
state: {
|
metadata: output.metadata ?? {},
|
||||||
status: "completed",
|
time: { start: ref.start, end: Date.now() },
|
||||||
input: ref.input,
|
|
||||||
output: output.output,
|
|
||||||
title: output.title,
|
|
||||||
metadata: output.metadata ?? {},
|
|
||||||
time: {
|
|
||||||
start: ref.start,
|
|
||||||
end: Date.now(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
} as Event)
|
}
|
||||||
|
present(state, [toolCommit(part, output.output ? "progress" : "final")])
|
||||||
}
|
}
|
||||||
|
|
||||||
function failTool(state: State, ref: Ref, error: string): void {
|
function failTool(state: State, ref: Ref, error: string): void {
|
||||||
feed(state, {
|
if (!state.started.has(ref.part)) startTool(state, ref)
|
||||||
type: "message.part.updated",
|
present(
|
||||||
properties: {
|
state,
|
||||||
sessionID: state.id,
|
[
|
||||||
time: Date.now(),
|
toolCommit(
|
||||||
part: {
|
{
|
||||||
id: ref.part,
|
id: ref.part,
|
||||||
sessionID: state.id,
|
sessionID: state.id,
|
||||||
messageID: ref.msg,
|
messageID: ref.msg,
|
||||||
type: "tool",
|
callID: ref.call,
|
||||||
callID: ref.call,
|
tool: ref.tool,
|
||||||
tool: ref.tool,
|
state: {
|
||||||
state: {
|
status: "error",
|
||||||
status: "error",
|
input: ref.input,
|
||||||
input: ref.input,
|
error,
|
||||||
error,
|
metadata: {},
|
||||||
metadata: {},
|
time: { start: ref.start, end: Date.now() },
|
||||||
time: {
|
|
||||||
start: ref.start,
|
|
||||||
end: Date.now(),
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
"final",
|
||||||
},
|
),
|
||||||
} as Event)
|
],
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function emitError(state: State, text: string): void {
|
function emitError(state: State, text: string): void {
|
||||||
const event = {
|
present(state, [{ kind: "error", source: "system", text, phase: "start" }])
|
||||||
id: `session.error:${state.id}:${Date.now()}`,
|
|
||||||
type: "session.error",
|
|
||||||
properties: {
|
|
||||||
sessionID: state.id,
|
|
||||||
error: {
|
|
||||||
name: "UnknownError",
|
|
||||||
data: {
|
|
||||||
message: text,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} satisfies Event
|
|
||||||
feed(state, event)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function emitBash(state: State, signal?: AbortSignal): Promise<void> {
|
async function emitBash(state: State, signal?: AbortSignal): Promise<void> {
|
||||||
|
|
@ -685,7 +539,7 @@ function emitTask(state: State): void {
|
||||||
start: Date.now(),
|
start: Date.now(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} satisfies ToolPart
|
} satisfies MiniToolPart
|
||||||
showSubagent(state, {
|
showSubagent(state, {
|
||||||
sessionID: "sub_demo_1",
|
sessionID: "sub_demo_1",
|
||||||
partID: ref.part,
|
partID: ref.part,
|
||||||
|
|
@ -979,18 +833,12 @@ function emitQuestion(state: State, kind: QuestionKind = "multi"): void {
|
||||||
const id = take(state, "ask", "ask")
|
const id = take(state, "ask", "ask")
|
||||||
state.asks.set(id, { ref })
|
state.asks.set(id, { ref })
|
||||||
|
|
||||||
feed(state, {
|
present(state, [], {
|
||||||
type: "question.asked",
|
id,
|
||||||
properties: {
|
sessionID: state.id,
|
||||||
id,
|
questions,
|
||||||
sessionID: state.id,
|
tool: { messageID: ref.msg, callID: ref.call },
|
||||||
questions,
|
})
|
||||||
tool: {
|
|
||||||
messageID: ref.msg,
|
|
||||||
callID: ref.call,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} as Event)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function emitFmt(state: State, kind: string, body: string, signal?: AbortSignal): Promise<boolean> {
|
async function emitFmt(state: State, kind: string, body: string, signal?: AbortSignal): Promise<boolean> {
|
||||||
|
|
@ -1089,9 +937,7 @@ export function createRunDemo(input: Input) {
|
||||||
const state: State = {
|
const state: State = {
|
||||||
id: input.sessionID,
|
id: input.sessionID,
|
||||||
thinking: input.thinking,
|
thinking: input.thinking,
|
||||||
data: createSessionData(),
|
|
||||||
footer: input.footer,
|
footer: input.footer,
|
||||||
limits: input.limits,
|
|
||||||
msg: 0,
|
msg: 0,
|
||||||
part: 0,
|
part: 0,
|
||||||
call: 0,
|
call: 0,
|
||||||
|
|
@ -1099,6 +945,7 @@ export function createRunDemo(input: Input) {
|
||||||
ask: 0,
|
ask: 0,
|
||||||
perms: new Map(),
|
perms: new Map(),
|
||||||
asks: new Map(),
|
asks: new Map(),
|
||||||
|
started: new Set(),
|
||||||
}
|
}
|
||||||
|
|
||||||
const start = async (): Promise<void> => {
|
const start = async (): Promise<void> => {
|
||||||
|
|
@ -1166,16 +1013,7 @@ export function createRunDemo(input: Input) {
|
||||||
}
|
}
|
||||||
|
|
||||||
state.perms.delete(input.requestID)
|
state.perms.delete(input.requestID)
|
||||||
const event = {
|
clearBlocker(state)
|
||||||
id: `permission.replied:${input.requestID}:${Date.now()}`,
|
|
||||||
type: "permission.replied",
|
|
||||||
properties: {
|
|
||||||
sessionID: state.id,
|
|
||||||
requestID: input.requestID,
|
|
||||||
reply: input.reply,
|
|
||||||
},
|
|
||||||
} satisfies Event
|
|
||||||
feed(state, event)
|
|
||||||
|
|
||||||
if (input.reply === "reject") {
|
if (input.reply === "reject") {
|
||||||
failTool(state, item.ref, input.message || "permission rejected")
|
failTool(state, item.ref, input.message || "permission rejected")
|
||||||
|
|
@ -1193,16 +1031,7 @@ export function createRunDemo(input: Input) {
|
||||||
}
|
}
|
||||||
|
|
||||||
state.asks.delete(input.requestID)
|
state.asks.delete(input.requestID)
|
||||||
const event = {
|
clearBlocker(state)
|
||||||
id: `question.replied:${input.requestID}:${Date.now()}`,
|
|
||||||
type: "question.replied",
|
|
||||||
properties: {
|
|
||||||
sessionID: state.id,
|
|
||||||
requestID: input.requestID,
|
|
||||||
answers: input.answers,
|
|
||||||
},
|
|
||||||
} satisfies Event
|
|
||||||
feed(state, event)
|
|
||||||
doneTool(state, ask.ref, {
|
doneTool(state, ask.ref, {
|
||||||
title: "question",
|
title: "question",
|
||||||
output: "",
|
output: "",
|
||||||
|
|
@ -1220,13 +1049,7 @@ export function createRunDemo(input: Input) {
|
||||||
}
|
}
|
||||||
|
|
||||||
state.asks.delete(input.requestID)
|
state.asks.delete(input.requestID)
|
||||||
feed(state, {
|
clearBlocker(state)
|
||||||
type: "question.rejected",
|
|
||||||
properties: {
|
|
||||||
sessionID: state.id,
|
|
||||||
requestID: input.requestID,
|
|
||||||
},
|
|
||||||
} as Event)
|
|
||||||
failTool(state, ask.ref, "question rejected")
|
failTool(state, ask.ref, "question rejected")
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
import type { TextareaRenderable } from "@opentui/core"
|
import type { TextareaRenderable } from "@opentui/core"
|
||||||
import { useKeyboard, useTerminalDimensions } from "@opentui/solid"
|
import { useKeyboard, useTerminalDimensions } from "@opentui/solid"
|
||||||
import { For, Match, Show, Switch, createEffect, createMemo, createSignal } from "solid-js"
|
import { For, Match, Show, Switch, createEffect, createMemo, createSignal } from "solid-js"
|
||||||
import type { PermissionRequest } from "@opencode-ai/sdk/v2"
|
import type { PermissionV2Request } from "@opencode-ai/client/promise"
|
||||||
import {
|
import {
|
||||||
createPermissionBodyState,
|
createPermissionBodyState,
|
||||||
permissionAlwaysLines,
|
permissionAlwaysLines,
|
||||||
|
|
@ -130,7 +130,7 @@ export function RejectField(props: {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RunPermissionBody(props: {
|
export function RunPermissionBody(props: {
|
||||||
request: PermissionRequest
|
request: PermissionV2Request
|
||||||
theme: RunFooterTheme
|
theme: RunFooterTheme
|
||||||
block: RunBlockTheme
|
block: RunBlockTheme
|
||||||
diffStyle?: RunDiffStyle
|
diffStyle?: RunDiffStyle
|
||||||
|
|
@ -142,7 +142,7 @@ export function RunPermissionBody(props: {
|
||||||
const ft = createMemo(() => toolFiletype(info().file))
|
const ft = createMemo(() => toolFiletype(info().file))
|
||||||
const narrow = createMemo(() => footerWidthPolicy(dims().width).dialog.narrow)
|
const narrow = createMemo(() => footerWidthPolicy(dims().width).dialog.narrow)
|
||||||
const opts = createMemo(() =>
|
const opts = createMemo(() =>
|
||||||
permissionOptions(state().stage).filter((option) => option !== "always" || props.request.always.length > 0),
|
permissionOptions(state().stage).filter((option) => option !== "always" || (props.request.save?.length ?? 0) > 0),
|
||||||
)
|
)
|
||||||
const busy = createMemo(() => state().submitting)
|
const busy = createMemo(() => state().submitting)
|
||||||
const title = createMemo(() => {
|
const title = createMemo(() => {
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
import type { TextareaRenderable } from "@opentui/core"
|
import type { TextareaRenderable } from "@opentui/core"
|
||||||
import { useKeyboard, useTerminalDimensions } from "@opentui/solid"
|
import { useKeyboard, useTerminalDimensions } from "@opentui/solid"
|
||||||
import { For, Show, createEffect, createMemo, createSignal } from "solid-js"
|
import { For, Show, createEffect, createMemo, createSignal } from "solid-js"
|
||||||
import type { QuestionRequest } from "@opencode-ai/sdk/v2"
|
import type { QuestionV2Request } from "@opencode-ai/client/promise"
|
||||||
import {
|
import {
|
||||||
createQuestionBodyState,
|
createQuestionBodyState,
|
||||||
questionConfirm,
|
questionConfirm,
|
||||||
|
|
@ -45,7 +45,7 @@ import type { RunFooterTheme } from "./theme"
|
||||||
import type { QuestionReject, QuestionReply } from "./types"
|
import type { QuestionReject, QuestionReply } from "./types"
|
||||||
|
|
||||||
export function RunQuestionBody(props: {
|
export function RunQuestionBody(props: {
|
||||||
request: QuestionRequest
|
request: QuestionV2Request
|
||||||
theme: RunFooterTheme
|
theme: RunFooterTheme
|
||||||
onReply: (input: QuestionReply) => void | Promise<void>
|
onReply: (input: QuestionReply) => void | Promise<void>
|
||||||
onReject: (input: QuestionReject) => void | Promise<void>
|
onReject: (input: QuestionReject) => void | Promise<void>
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import type { EventSubscribeOutput, OpenCodeClient } from "@opencode-ai/client/promise"
|
import type { EventSubscribeOutput, OpenCodeClient } from "@opencode-ai/client/promise"
|
||||||
import type { ReasoningPart, StepFinishPart, StepStartPart, TextPart, ToolPart } from "@opencode-ai/sdk/v2"
|
|
||||||
import { SessionMessage } from "@opencode-ai/schema/session-message"
|
import { SessionMessage } from "@opencode-ai/schema/session-message"
|
||||||
import { EOL } from "node:os"
|
import { EOL } from "node:os"
|
||||||
import { UI } from "./ui"
|
import { UI } from "./ui"
|
||||||
|
import type { MiniToolPart } from "./types"
|
||||||
|
|
||||||
type Model = {
|
type Model = {
|
||||||
providerID: string
|
providerID: string
|
||||||
|
|
@ -28,8 +28,8 @@ type Input = {
|
||||||
auto: boolean
|
auto: boolean
|
||||||
/** True when the client is attached to a shared server rather than an exclusive in-process one. */
|
/** True when the client is attached to a shared server rather than an exclusive in-process one. */
|
||||||
attached: boolean
|
attached: boolean
|
||||||
renderTool: (part: ToolPart) => Promise<void>
|
renderTool: (part: MiniToolPart) => Promise<void>
|
||||||
renderToolError: (part: ToolPart) => Promise<void>
|
renderToolError: (part: MiniToolPart) => Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
type StartedPart = {
|
type StartedPart = {
|
||||||
|
|
@ -77,7 +77,7 @@ export async function runNonInteractivePrompt(input: Input) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
const writeText = (part: TextPart, timestamp: number) => {
|
const writeText = (part: { text: string; [key: string]: unknown }, timestamp: number) => {
|
||||||
if (emit("text", timestamp, { part })) return
|
if (emit("text", timestamp, { part })) return
|
||||||
const text = part.text.trim()
|
const text = part.text.trim()
|
||||||
if (!text) return
|
if (!text) return
|
||||||
|
|
@ -169,7 +169,7 @@ export async function runNonInteractivePrompt(input: Input) {
|
||||||
if (!promoted) continue
|
if (!promoted) continue
|
||||||
|
|
||||||
if (event.type === "session.step.started") {
|
if (event.type === "session.step.started") {
|
||||||
const part: StepStartPart = {
|
const part = {
|
||||||
id: partID(event.id),
|
id: partID(event.id),
|
||||||
sessionID: input.sessionID,
|
sessionID: input.sessionID,
|
||||||
messageID: event.data.assistantMessageID,
|
messageID: event.data.assistantMessageID,
|
||||||
|
|
@ -191,7 +191,7 @@ export async function runNonInteractivePrompt(input: Input) {
|
||||||
if (event.type === "session.text.ended") {
|
if (event.type === "session.text.ended") {
|
||||||
const started = starts.get("text")
|
const started = starts.get("text")
|
||||||
starts.delete("text")
|
starts.delete("text")
|
||||||
const part: TextPart = {
|
const part = {
|
||||||
id: started?.id ?? partID(event.id),
|
id: started?.id ?? partID(event.id),
|
||||||
sessionID: input.sessionID,
|
sessionID: input.sessionID,
|
||||||
messageID: event.data.assistantMessageID,
|
messageID: event.data.assistantMessageID,
|
||||||
|
|
@ -210,7 +210,7 @@ export async function runNonInteractivePrompt(input: Input) {
|
||||||
if (event.type === "session.reasoning.ended" && input.thinking) {
|
if (event.type === "session.reasoning.ended" && input.thinking) {
|
||||||
const started = starts.get("reasoning")
|
const started = starts.get("reasoning")
|
||||||
starts.delete("reasoning")
|
starts.delete("reasoning")
|
||||||
const part: ReasoningPart = {
|
const part = {
|
||||||
id: started?.id ?? partID(event.id),
|
id: started?.id ?? partID(event.id),
|
||||||
sessionID: input.sessionID,
|
sessionID: input.sessionID,
|
||||||
messageID: event.data.assistantMessageID,
|
messageID: event.data.assistantMessageID,
|
||||||
|
|
@ -263,7 +263,7 @@ export async function runNonInteractivePrompt(input: Input) {
|
||||||
}
|
}
|
||||||
if (event.type === "session.tool.success") {
|
if (event.type === "session.tool.success") {
|
||||||
const current = tools.get(event.data.callID) ?? fallbackTool(event)
|
const current = tools.get(event.data.callID) ?? fallbackTool(event)
|
||||||
const part: ToolPart = {
|
const part: MiniToolPart = {
|
||||||
id: current.id,
|
id: current.id,
|
||||||
sessionID: input.sessionID,
|
sessionID: input.sessionID,
|
||||||
messageID: event.data.assistantMessageID,
|
messageID: event.data.assistantMessageID,
|
||||||
|
|
@ -296,7 +296,7 @@ export async function runNonInteractivePrompt(input: Input) {
|
||||||
if (event.type === "session.tool.failed") {
|
if (event.type === "session.tool.failed") {
|
||||||
const current = tools.get(event.data.callID) ?? fallbackTool(event)
|
const current = tools.get(event.data.callID) ?? fallbackTool(event)
|
||||||
const error = event.data.error.message
|
const error = event.data.error.message
|
||||||
const part: ToolPart = {
|
const part: MiniToolPart = {
|
||||||
id: current.id,
|
id: current.id,
|
||||||
sessionID: input.sessionID,
|
sessionID: input.sessionID,
|
||||||
messageID: event.data.assistantMessageID,
|
messageID: event.data.assistantMessageID,
|
||||||
|
|
@ -325,7 +325,7 @@ export async function runNonInteractivePrompt(input: Input) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.type === "session.step.ended") {
|
if (event.type === "session.step.ended") {
|
||||||
const part: StepFinishPart = {
|
const part = {
|
||||||
id: partID(event.id),
|
id: partID(event.id),
|
||||||
sessionID: input.sessionID,
|
sessionID: input.sessionID,
|
||||||
messageID: event.data.assistantMessageID,
|
messageID: event.data.assistantMessageID,
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@
|
||||||
//
|
//
|
||||||
// permissionInfo() extracts display info (icon, title, lines, diff) from
|
// permissionInfo() extracts display info (icon, title, lines, diff) from
|
||||||
// the request, delegating to tool.ts for tool-specific formatting.
|
// the request, delegating to tool.ts for tool-specific formatting.
|
||||||
import type { PermissionRequest } from "@opencode-ai/sdk/v2"
|
import type { PermissionV2Request } from "@opencode-ai/client/promise"
|
||||||
import type { PermissionReply } from "./types"
|
import type { PermissionReply } from "./types"
|
||||||
import { toolPath, toolPermissionInfo } from "./tool"
|
import { toolPath, toolPermissionInfo } from "./tool"
|
||||||
|
|
||||||
|
|
@ -55,7 +55,7 @@ function text(v: unknown): string {
|
||||||
return typeof v === "string" ? v : ""
|
return typeof v === "string" ? v : ""
|
||||||
}
|
}
|
||||||
|
|
||||||
function data(request: PermissionRequest): Dict {
|
function data(request: PermissionV2Request): Dict {
|
||||||
const meta = dict(request.metadata)
|
const meta = dict(request.metadata)
|
||||||
return {
|
return {
|
||||||
...meta,
|
...meta,
|
||||||
|
|
@ -63,8 +63,8 @@ function data(request: PermissionRequest): Dict {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function patterns(request: PermissionRequest): string[] {
|
function patterns(request: PermissionV2Request): string[] {
|
||||||
return request.patterns.filter((item): item is string => typeof item === "string")
|
return request.resources.filter((item): item is string => typeof item === "string")
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createPermissionBodyState(requestID: string): PermissionBodyState {
|
export function createPermissionBodyState(requestID: string): PermissionBodyState {
|
||||||
|
|
@ -89,15 +89,15 @@ export function permissionOptions(stage: PermissionStage): PermissionOption[] {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
export function permissionInfo(request: PermissionRequest): PermissionInfo {
|
export function permissionInfo(request: PermissionV2Request): PermissionInfo {
|
||||||
const pats = patterns(request)
|
const pats = patterns(request)
|
||||||
const input = data(request)
|
const input = data(request)
|
||||||
const info = toolPermissionInfo(request.permission, input, dict(request.metadata), pats)
|
const info = toolPermissionInfo(request.action, input, dict(request.metadata), pats)
|
||||||
if (info) {
|
if (info) {
|
||||||
return info
|
return info
|
||||||
}
|
}
|
||||||
|
|
||||||
if (request.permission === "external_directory") {
|
if (request.action === "external_directory") {
|
||||||
const meta = dict(request.metadata)
|
const meta = dict(request.metadata)
|
||||||
const raw = text(meta.parentDir) || text(meta.filepath) || pats[0] || ""
|
const raw = text(meta.parentDir) || text(meta.filepath) || pats[0] || ""
|
||||||
const dir = raw.includes("*") ? raw.slice(0, raw.indexOf("*")).replace(/[\\/]+$/, "") : raw
|
const dir = raw.includes("*") ? raw.slice(0, raw.indexOf("*")).replace(/[\\/]+$/, "") : raw
|
||||||
|
|
@ -108,7 +108,7 @@ export function permissionInfo(request: PermissionRequest): PermissionInfo {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (request.permission === "doom_loop") {
|
if (request.action === "doom_loop") {
|
||||||
return {
|
return {
|
||||||
icon: "⟳",
|
icon: "⟳",
|
||||||
title: "Continue after repeated failures",
|
title: "Continue after repeated failures",
|
||||||
|
|
@ -118,19 +118,20 @@ export function permissionInfo(request: PermissionRequest): PermissionInfo {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
icon: "⚙",
|
icon: "⚙",
|
||||||
title: `Call tool ${request.permission}`,
|
title: `Call tool ${request.action}`,
|
||||||
lines: [`Tool: ${request.permission}`],
|
lines: [`Tool: ${request.action}`],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function permissionAlwaysLines(request: PermissionRequest): string[] {
|
export function permissionAlwaysLines(request: PermissionV2Request): string[] {
|
||||||
if (request.always.length === 1 && request.always[0] === "*") {
|
const save = request.save ?? []
|
||||||
return [`This will allow ${request.permission} until OpenCode is restarted.`]
|
if (save.length === 1 && save[0] === "*") {
|
||||||
|
return [`This will allow ${request.action} until OpenCode is restarted.`]
|
||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
"This will allow the following patterns until OpenCode is restarted.",
|
"This will allow the following patterns until OpenCode is restarted.",
|
||||||
...request.always.map((item) => `- ${item}`),
|
...save.map((item) => `- ${item}`),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@
|
||||||
//
|
//
|
||||||
// Custom answers: if a question has custom=true, an extra "Type your own
|
// Custom answers: if a question has custom=true, an extra "Type your own
|
||||||
// answer" option appears. Selecting it enters editing mode with a text field.
|
// answer" option appears. Selecting it enters editing mode with a text field.
|
||||||
import type { QuestionInfo, QuestionRequest } from "@opencode-ai/sdk/v2"
|
import type { QuestionV2Info, QuestionV2Request } from "@opencode-ai/client/promise"
|
||||||
import type { QuestionReject, QuestionReply } from "./types"
|
import type { QuestionReject, QuestionReply } from "./types"
|
||||||
|
|
||||||
export type QuestionBodyState = {
|
export type QuestionBodyState = {
|
||||||
|
|
@ -51,23 +51,23 @@ export function questionSync(state: QuestionBodyState, requestID: string): Quest
|
||||||
return createQuestionBodyState(requestID)
|
return createQuestionBodyState(requestID)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function questionSingle(request: QuestionRequest): boolean {
|
export function questionSingle(request: QuestionV2Request): boolean {
|
||||||
return request.questions.length === 1 && request.questions[0]?.multiple !== true
|
return request.questions.length === 1 && request.questions[0]?.multiple !== true
|
||||||
}
|
}
|
||||||
|
|
||||||
export function questionTabs(request: QuestionRequest): number {
|
export function questionTabs(request: QuestionV2Request): number {
|
||||||
return questionSingle(request) ? 1 : request.questions.length + 1
|
return questionSingle(request) ? 1 : request.questions.length + 1
|
||||||
}
|
}
|
||||||
|
|
||||||
export function questionConfirm(request: QuestionRequest, state: QuestionBodyState): boolean {
|
export function questionConfirm(request: QuestionV2Request, state: QuestionBodyState): boolean {
|
||||||
return !questionSingle(request) && state.tab === request.questions.length
|
return !questionSingle(request) && state.tab === request.questions.length
|
||||||
}
|
}
|
||||||
|
|
||||||
export function questionInfo(request: QuestionRequest, state: QuestionBodyState): QuestionInfo | undefined {
|
export function questionInfo(request: QuestionV2Request, state: QuestionBodyState): QuestionV2Info | undefined {
|
||||||
return request.questions[state.tab]
|
return request.questions[state.tab]
|
||||||
}
|
}
|
||||||
|
|
||||||
export function questionCustom(request: QuestionRequest, state: QuestionBodyState): boolean {
|
export function questionCustom(request: QuestionV2Request, state: QuestionBodyState): boolean {
|
||||||
return questionInfo(request, state)?.custom !== false
|
return questionInfo(request, state)?.custom !== false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -84,7 +84,7 @@ export function questionPicked(state: QuestionBodyState): boolean {
|
||||||
return state.answers[state.tab]?.includes(value) ?? false
|
return state.answers[state.tab]?.includes(value) ?? false
|
||||||
}
|
}
|
||||||
|
|
||||||
export function questionOther(request: QuestionRequest, state: QuestionBodyState): boolean {
|
export function questionOther(request: QuestionV2Request, state: QuestionBodyState): boolean {
|
||||||
const info = questionInfo(request, state)
|
const info = questionInfo(request, state)
|
||||||
if (!info || info.custom === false) {
|
if (!info || info.custom === false) {
|
||||||
return false
|
return false
|
||||||
|
|
@ -93,7 +93,7 @@ export function questionOther(request: QuestionRequest, state: QuestionBodyState
|
||||||
return state.selected === info.options.length
|
return state.selected === info.options.length
|
||||||
}
|
}
|
||||||
|
|
||||||
export function questionTotal(request: QuestionRequest, state: QuestionBodyState): number {
|
export function questionTotal(request: QuestionV2Request, state: QuestionBodyState): number {
|
||||||
const info = questionInfo(request, state)
|
const info = questionInfo(request, state)
|
||||||
if (!info) {
|
if (!info) {
|
||||||
return 0
|
return 0
|
||||||
|
|
@ -156,7 +156,7 @@ export function questionStoreCustom(state: QuestionBodyState, tab: number, text:
|
||||||
|
|
||||||
function questionPick(
|
function questionPick(
|
||||||
state: QuestionBodyState,
|
state: QuestionBodyState,
|
||||||
request: QuestionRequest,
|
request: QuestionV2Request,
|
||||||
answer: string,
|
answer: string,
|
||||||
custom = false,
|
custom = false,
|
||||||
): QuestionStep {
|
): QuestionStep {
|
||||||
|
|
@ -204,7 +204,7 @@ function questionToggle(state: QuestionBodyState, answer: string): QuestionBodyS
|
||||||
return storeAnswers(state, state.tab, list)
|
return storeAnswers(state, state.tab, list)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function questionMove(state: QuestionBodyState, request: QuestionRequest, dir: -1 | 1): QuestionBodyState {
|
export function questionMove(state: QuestionBodyState, request: QuestionV2Request, dir: -1 | 1): QuestionBodyState {
|
||||||
const total = questionTotal(request, state)
|
const total = questionTotal(request, state)
|
||||||
if (total === 0) {
|
if (total === 0) {
|
||||||
return state
|
return state
|
||||||
|
|
@ -216,7 +216,7 @@ export function questionMove(state: QuestionBodyState, request: QuestionRequest,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function questionSelect(state: QuestionBodyState, request: QuestionRequest): QuestionStep {
|
export function questionSelect(state: QuestionBodyState, request: QuestionV2Request): QuestionStep {
|
||||||
const info = questionInfo(request, state)
|
const info = questionInfo(request, state)
|
||||||
if (!info) {
|
if (!info) {
|
||||||
return { state }
|
return { state }
|
||||||
|
|
@ -255,7 +255,7 @@ export function questionSelect(state: QuestionBodyState, request: QuestionReques
|
||||||
return questionPick(state, request, option.label)
|
return questionPick(state, request, option.label)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function questionSave(state: QuestionBodyState, request: QuestionRequest): QuestionStep {
|
export function questionSave(state: QuestionBodyState, request: QuestionV2Request): QuestionStep {
|
||||||
const info = questionInfo(request, state)
|
const info = questionInfo(request, state)
|
||||||
if (!info) {
|
if (!info) {
|
||||||
return { state }
|
return { state }
|
||||||
|
|
@ -305,20 +305,20 @@ export function questionSave(state: QuestionBodyState, request: QuestionRequest)
|
||||||
return questionPick(state, request, value, true)
|
return questionPick(state, request, value, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function questionSubmit(request: QuestionRequest, state: QuestionBodyState): QuestionReply {
|
export function questionSubmit(request: QuestionV2Request, state: QuestionBodyState): QuestionReply {
|
||||||
return {
|
return {
|
||||||
requestID: request.id,
|
requestID: request.id,
|
||||||
answers: questionAnswers(state, request.questions.length),
|
answers: questionAnswers(state, request.questions.length),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function questionReject(request: QuestionRequest): QuestionReject {
|
export function questionReject(request: QuestionV2Request): QuestionReject {
|
||||||
return {
|
return {
|
||||||
requestID: request.id,
|
requestID: request.id,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function questionHint(request: QuestionRequest, state: QuestionBodyState): string {
|
export function questionHint(request: QuestionV2Request, state: QuestionBodyState): string {
|
||||||
if (state.submitting) {
|
if (state.submitting) {
|
||||||
return "Waiting for question event..."
|
return "Waiting for question event..."
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,13 @@ import { Service } from "@opencode-ai/client/effect"
|
||||||
import { OpenCode, type OpenCodeClient } from "@opencode-ai/client/promise"
|
import { OpenCode, type OpenCodeClient } from "@opencode-ai/client/promise"
|
||||||
import { FSUtil } from "@opencode-ai/core/fs-util"
|
import { FSUtil } from "@opencode-ai/core/fs-util"
|
||||||
import { Model } from "@opencode-ai/schema/model"
|
import { Model } from "@opencode-ai/schema/model"
|
||||||
import type { ToolPart } from "@opencode-ai/sdk/v2"
|
|
||||||
import { open } from "node:fs/promises"
|
import { open } from "node:fs/promises"
|
||||||
import path from "node:path"
|
import path from "node:path"
|
||||||
import { Server } from "../services/server"
|
import { Server } from "../services/server"
|
||||||
import { loadRunAgents, waitForCatalogReady } from "./catalog.shared"
|
import { loadRunAgents, waitForCatalogReady } from "./catalog.shared"
|
||||||
import { runNonInteractivePrompt } from "./noninteractive"
|
import { runNonInteractivePrompt } from "./noninteractive"
|
||||||
import { toolInlineInfo } from "./tool"
|
import { toolInlineInfo } from "./tool"
|
||||||
|
import type { MiniToolPart } from "./types"
|
||||||
import { UI } from "./ui"
|
import { UI } from "./ui"
|
||||||
|
|
||||||
export type RunCommandInput = {
|
export type RunCommandInput = {
|
||||||
|
|
@ -224,7 +224,7 @@ function isBinaryContent(bytes: Uint8Array) {
|
||||||
return bytes.reduce((count, byte) => count + Number(byte < 9 || (byte > 13 && byte < 32)), 0) / bytes.length > 0.3
|
return bytes.reduce((count, byte) => count + Number(byte < 9 || (byte > 13 && byte < 32)), 0) / bytes.length > 0.3
|
||||||
}
|
}
|
||||||
|
|
||||||
async function renderTool(part: ToolPart) {
|
async function renderTool(part: MiniToolPart) {
|
||||||
const info = toolInlineInfo(part)
|
const info = toolInlineInfo(part)
|
||||||
if (info.mode === "block") {
|
if (info.mode === "block") {
|
||||||
UI.empty()
|
UI.empty()
|
||||||
|
|
@ -240,7 +240,7 @@ async function renderTool(part: ToolPart) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function renderToolError(part: ToolPart) {
|
async function renderToolError(part: MiniToolPart) {
|
||||||
const info = toolInlineInfo(part)
|
const info = toolInlineInfo(part)
|
||||||
UI.println(UI.Style.TEXT_NORMAL + "✗", UI.Style.TEXT_NORMAL + `${info.title} failed`)
|
UI.println(UI.Style.TEXT_NORMAL + "✗", UI.Style.TEXT_NORMAL + `${info.title} failed`)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -547,7 +547,6 @@ async function runInteractiveRuntime(input: RunRuntimeInput, deps: RunRuntimeDep
|
||||||
footer,
|
footer,
|
||||||
sessionID: state.sessionID,
|
sessionID: state.sessionID,
|
||||||
thinking: input.thinking,
|
thinking: input.thinking,
|
||||||
limits: () => state.limits,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,15 +1,10 @@
|
||||||
// Session message extraction and prompt history.
|
import type { SessionMessageInfo, SessionMessageUser } from "@opencode-ai/client/promise"
|
||||||
//
|
|
||||||
// Fetches session messages from the SDK and extracts user turn text for
|
|
||||||
// the prompt history ring. Also finds the most recently used variant for
|
|
||||||
// the current model so the footer can pre-select it.
|
|
||||||
import { promptCopy, promptSame } from "./prompt.shared"
|
import { promptCopy, promptSame } from "./prompt.shared"
|
||||||
import type { Message, Part } from "@opencode-ai/sdk/v2"
|
|
||||||
import type { RunInput, RunPrompt } from "./types"
|
import type { RunInput, RunPrompt } from "./types"
|
||||||
|
|
||||||
const LIMIT = 200
|
const LIMIT = 200
|
||||||
|
|
||||||
export type SessionMessages = Array<{ info: Message; parts: Part[] }>
|
export type SessionMessages = SessionMessageInfo[]
|
||||||
|
|
||||||
type Turn = {
|
type Turn = {
|
||||||
prompt: RunPrompt
|
prompt: RunPrompt
|
||||||
|
|
@ -25,133 +20,42 @@ export type RunSession = {
|
||||||
variant?: string
|
variant?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
function fileName(url: string, filename?: string) {
|
function messagePrompt(message: SessionMessageUser): RunPrompt {
|
||||||
if (filename) {
|
|
||||||
return filename
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const next = new URL(url)
|
|
||||||
if (next.protocol !== "file:") {
|
|
||||||
return url
|
|
||||||
}
|
|
||||||
|
|
||||||
const name = next.pathname.split("/").at(-1)
|
|
||||||
if (name) {
|
|
||||||
return decodeURIComponent(name)
|
|
||||||
}
|
|
||||||
} catch {}
|
|
||||||
|
|
||||||
return url
|
|
||||||
}
|
|
||||||
|
|
||||||
function fileSource(
|
|
||||||
part: Extract<SessionMessages[number]["parts"][number], { type: "file" }>,
|
|
||||||
text: { start: number; end: number; value: string },
|
|
||||||
) {
|
|
||||||
if (part.source) {
|
|
||||||
return {
|
|
||||||
...structuredClone(part.source),
|
|
||||||
text,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: "file" as const,
|
text: message.text,
|
||||||
path: part.filename ?? part.url,
|
parts: [
|
||||||
text,
|
...(message.files ?? []).map((file) => ({
|
||||||
}
|
type: "file" as const,
|
||||||
}
|
url: file.source.type === "uri" ? file.source.uri : `data:${file.mime};base64,${file.data}`,
|
||||||
|
mime: file.mime,
|
||||||
export function messagePrompt(msg: SessionMessages[number]): RunPrompt {
|
filename: file.name,
|
||||||
const parts: RunPrompt["parts"] = []
|
source: file.mention
|
||||||
let text = msg.parts
|
? {
|
||||||
.filter((part): part is Extract<SessionMessages[number]["parts"][number], { type: "text" }> => {
|
type: "file",
|
||||||
return part.type === "text" && !part.synthetic
|
path: file.name ?? (file.source.type === "uri" ? file.source.uri : "inline attachment"),
|
||||||
})
|
text: { start: file.mention.start, end: file.mention.end, value: file.mention.text },
|
||||||
.map((part) => part.text)
|
}
|
||||||
.join("")
|
: undefined,
|
||||||
let cursor = Bun.stringWidth(text)
|
})),
|
||||||
const used: Array<{ start: number; end: number }> = []
|
...(message.agents ?? []).map((agent) => ({
|
||||||
|
type: "agent" as const,
|
||||||
const take = (value: string): { start: number; end: number; value: string } | undefined => {
|
name: agent.name,
|
||||||
let from = 0
|
source: agent.mention
|
||||||
while (true) {
|
? { start: agent.mention.start, end: agent.mention.end, value: agent.mention.text }
|
||||||
const idx = text.indexOf(value, from)
|
: undefined,
|
||||||
if (idx === -1) {
|
})),
|
||||||
return undefined
|
],
|
||||||
}
|
|
||||||
|
|
||||||
const start = Bun.stringWidth(text.slice(0, idx))
|
|
||||||
const end = start + Bun.stringWidth(value)
|
|
||||||
if (!used.some((item) => item.start < end && start < item.end)) {
|
|
||||||
return { start, end, value }
|
|
||||||
}
|
|
||||||
|
|
||||||
from = idx + value.length
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const add = (value: string) => {
|
|
||||||
const gap = text ? " " : ""
|
|
||||||
const start = cursor + Bun.stringWidth(gap)
|
|
||||||
text += gap + value
|
|
||||||
const end = start + Bun.stringWidth(value)
|
|
||||||
cursor = end
|
|
||||||
return { start, end, value }
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const part of msg.parts) {
|
|
||||||
if (part.type === "file") {
|
|
||||||
const next = part.source?.text ? structuredClone(part.source.text) : take("@" + fileName(part.url, part.filename))
|
|
||||||
const span = next ?? add("@" + fileName(part.url, part.filename))
|
|
||||||
used.push({ start: span.start, end: span.end })
|
|
||||||
parts.push({
|
|
||||||
type: "file",
|
|
||||||
mime: part.mime,
|
|
||||||
filename: part.filename,
|
|
||||||
url: part.url,
|
|
||||||
source: fileSource(part, span),
|
|
||||||
})
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if (part.type !== "agent") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const span = part.source ? structuredClone(part.source) : (take("@" + part.name) ?? add("@" + part.name))
|
|
||||||
used.push({ start: span.start, end: span.end })
|
|
||||||
parts.push({
|
|
||||||
type: "agent",
|
|
||||||
name: part.name,
|
|
||||||
source: span,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return { text, parts }
|
|
||||||
}
|
|
||||||
|
|
||||||
function turn(msg: SessionMessages[number]): Turn | undefined {
|
|
||||||
if (msg.info.role !== "user") {
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
prompt: messagePrompt(msg),
|
|
||||||
provider: msg.info.model.providerID,
|
|
||||||
model: msg.info.model.modelID,
|
|
||||||
variant: msg.info.model.variant,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createSession(messages: SessionMessages): RunSession {
|
export function createSession(messages: SessionMessages): RunSession {
|
||||||
return {
|
return {
|
||||||
first: messages.length === 0,
|
first: messages.length === 0,
|
||||||
turns: messages.flatMap((msg) => {
|
turns: messages.flatMap((message) =>
|
||||||
const item = turn(msg)
|
message.type === "user"
|
||||||
return item ? [item] : []
|
? [{ prompt: messagePrompt(message), provider: undefined, model: undefined, variant: undefined }]
|
||||||
}),
|
: [],
|
||||||
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -164,89 +68,34 @@ export async function resolveCurrentSession(
|
||||||
sdk.message.list({ sessionID, limit, order: "desc" }),
|
sdk.message.list({ sessionID, limit, order: "desc" }),
|
||||||
sdk.session.get({ sessionID }),
|
sdk.session.get({ sessionID }),
|
||||||
])
|
])
|
||||||
const messages = response.data.toReversed()
|
const current = createSession(response.data.toReversed())
|
||||||
return {
|
return {
|
||||||
first: messages.length === 0,
|
...current,
|
||||||
turns: messages.flatMap((message) => {
|
turns: current.turns.map((turn) => ({
|
||||||
if (message.type !== "user") return []
|
...turn,
|
||||||
return [
|
provider: session.model?.providerID,
|
||||||
{
|
model: session.model?.id,
|
||||||
prompt: {
|
variant: session.model?.variant,
|
||||||
text: message.text,
|
})),
|
||||||
parts: [
|
|
||||||
...(message.files ?? []).map((file) => ({
|
|
||||||
type: "file" as const,
|
|
||||||
url: file.source.type === "uri" ? file.source.uri : `data:${file.mime};base64,${file.data}`,
|
|
||||||
mime: file.mime,
|
|
||||||
filename: file.name,
|
|
||||||
source: file.mention
|
|
||||||
? {
|
|
||||||
type: "file" as const,
|
|
||||||
path: file.name ?? (file.source.type === "uri" ? file.source.uri : "inline attachment"),
|
|
||||||
text: { start: file.mention.start, end: file.mention.end, value: file.mention.text },
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
})),
|
|
||||||
...(message.agents ?? []).map((agent) => ({
|
|
||||||
type: "agent" as const,
|
|
||||||
name: agent.name,
|
|
||||||
source: agent.mention
|
|
||||||
? { start: agent.mention.start, end: agent.mention.end, value: agent.mention.text }
|
|
||||||
: undefined,
|
|
||||||
})),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
provider: session.model?.providerID,
|
|
||||||
model: session.model?.id,
|
|
||||||
variant: session.model?.variant,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}),
|
|
||||||
...(session.model && {
|
...(session.model && {
|
||||||
model: {
|
model: { providerID: session.model.providerID, modelID: session.model.id },
|
||||||
providerID: session.model.providerID,
|
|
||||||
modelID: session.model.id,
|
|
||||||
},
|
|
||||||
variant: session.model.variant,
|
variant: session.model.variant,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function sessionHistory(session: RunSession, limit = LIMIT): RunPrompt[] {
|
export function sessionHistory(session: RunSession, limit = LIMIT): RunPrompt[] {
|
||||||
const out: RunPrompt[] = []
|
return session.turns
|
||||||
|
.map((turn) => turn.prompt)
|
||||||
for (const turn of session.turns) {
|
.filter((prompt) => prompt.text.trim())
|
||||||
if (!turn.prompt.text.trim()) {
|
.filter((prompt, index, prompts) => index === 0 || !promptSame(prompts[index - 1], prompt))
|
||||||
continue
|
.map(promptCopy)
|
||||||
}
|
.slice(-limit)
|
||||||
|
|
||||||
if (out[out.length - 1] && promptSame(out[out.length - 1], turn.prompt)) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
out.push(promptCopy(turn.prompt))
|
|
||||||
}
|
|
||||||
|
|
||||||
return out.slice(-limit)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function sessionVariant(session: RunSession, model: RunInput["model"]): string | undefined {
|
export function sessionVariant(session: RunSession, model: RunInput["model"]): string | undefined {
|
||||||
if (!model) {
|
if (!model) return
|
||||||
return undefined
|
if (session.model?.providerID === model.providerID && session.model.modelID === model.modelID) return session.variant
|
||||||
}
|
|
||||||
|
|
||||||
if (session.model?.providerID === model.providerID && session.model.modelID === model.modelID) {
|
return session.turns.findLast((turn) => turn.provider === model.providerID && turn.model === model.modelID)?.variant
|
||||||
return session.variant
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let idx = session.turns.length - 1; idx >= 0; idx -= 1) {
|
|
||||||
const turn = session.turns[idx]
|
|
||||||
if (turn.provider !== model.providerID || turn.model !== model.modelID) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
return turn.variant
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,10 +15,14 @@
|
||||||
// Per-child interruption uses `v2.session.interrupt(childID)`. Per-child
|
// Per-child interruption uses `v2.session.interrupt(childID)`. Per-child
|
||||||
// backgrounding is intentionally absent: subagent jobs block the parent
|
// backgrounding is intentionally absent: subagent jobs block the parent
|
||||||
// session, so only whole-session `v2.session.background(parentID)` exists.
|
// session, so only whole-session `v2.session.background(parentID)` exists.
|
||||||
import type { EventSubscribeOutput, OpenCodeClient } from "@opencode-ai/client/promise"
|
import type {
|
||||||
import type { SessionMessageAssistantTool, SessionMessageInfo, ToolPart } from "@opencode-ai/sdk/v2"
|
EventSubscribeOutput,
|
||||||
|
OpenCodeClient,
|
||||||
|
SessionMessageAssistantTool,
|
||||||
|
SessionMessageInfo,
|
||||||
|
} from "@opencode-ai/client/promise"
|
||||||
import { Locale } from "@opencode-ai/tui/util/locale"
|
import { Locale } from "@opencode-ai/tui/util/locale"
|
||||||
import type { FooterSubagentDetail, FooterSubagentState, FooterSubagentTab, StreamCommit } from "./types"
|
import type { FooterSubagentDetail, FooterSubagentState, FooterSubagentTab, MiniToolPart, StreamCommit } from "./types"
|
||||||
|
|
||||||
const CHILD_MESSAGE_LIMIT = 80
|
const CHILD_MESSAGE_LIMIT = 80
|
||||||
const CHILD_FRAME_LIMIT = 80
|
const CHILD_FRAME_LIMIT = 80
|
||||||
|
|
@ -32,11 +36,11 @@ export function outputText(content: ReadonlyArray<{ type: string; text?: string
|
||||||
return content.flatMap((item) => (item.type === "text" && item.text ? [item.text] : [])).join("\n")
|
return content.flatMap((item) => (item.type === "text" && item.text ? [item.text] : [])).join("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
export function legacyTool(input: {
|
export function miniTool(input: {
|
||||||
sessionID: string
|
sessionID: string
|
||||||
messageID: string
|
messageID: string
|
||||||
tool: SessionMessageAssistantTool
|
tool: SessionMessageAssistantTool
|
||||||
}): ToolPart {
|
}): MiniToolPart {
|
||||||
const tool = input.tool
|
const tool = input.tool
|
||||||
const providerCall =
|
const providerCall =
|
||||||
tool.executed === undefined && tool.providerState === undefined
|
tool.executed === undefined && tool.providerState === undefined
|
||||||
|
|
@ -109,7 +113,7 @@ export function legacyTool(input: {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toolCommit(part: ToolPart, phase: "start" | "progress" | "final"): StreamCommit {
|
export function toolCommit(part: MiniToolPart, phase: "start" | "progress" | "final"): StreamCommit {
|
||||||
const status = part.state.status
|
const status = part.state.status
|
||||||
const text =
|
const text =
|
||||||
status === "running"
|
status === "running"
|
||||||
|
|
@ -310,7 +314,7 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
|
||||||
}
|
}
|
||||||
|
|
||||||
const childTool = (child: ChildState, item: SessionMessageAssistantTool, messageID: string) => {
|
const childTool = (child: ChildState, item: SessionMessageAssistantTool, messageID: string) => {
|
||||||
const part = legacyTool({
|
const part = miniTool({
|
||||||
sessionID: child.sessionID,
|
sessionID: child.sessionID,
|
||||||
messageID,
|
messageID,
|
||||||
tool: item,
|
tool: item,
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,15 @@
|
||||||
import type { EventSubscribeOutput, OpenCodeClient } from "@opencode-ai/client/promise"
|
|
||||||
import type {
|
import type {
|
||||||
PermissionRequest,
|
EventSubscribeOutput,
|
||||||
QuestionRequest,
|
OpenCodeClient,
|
||||||
SessionMessageInfo,
|
PermissionV2Request,
|
||||||
|
QuestionV2Request,
|
||||||
SessionMessageAssistantTool,
|
SessionMessageAssistantTool,
|
||||||
} from "@opencode-ai/sdk/v2"
|
SessionMessageInfo,
|
||||||
|
} from "@opencode-ai/client/promise"
|
||||||
import { Event } from "@opencode-ai/schema/event"
|
import { Event } from "@opencode-ai/schema/event"
|
||||||
import { blockerStatus, pickBlockerView } from "./session-data"
|
import { blockerStatus, pickBlockerView } from "./session-data"
|
||||||
import { writeSessionOutput } from "./stream"
|
import { writeSessionOutput } from "./stream"
|
||||||
import { createSubagentTracker, legacyTool, toolCommit } from "./stream-v2.subagent"
|
import { createSubagentTracker, miniTool, toolCommit } from "./stream-v2.subagent"
|
||||||
import type {
|
import type {
|
||||||
FooterApi,
|
FooterApi,
|
||||||
FooterView,
|
FooterView,
|
||||||
|
|
@ -87,8 +88,6 @@ type ShellWait = {
|
||||||
}
|
}
|
||||||
|
|
||||||
type RunV2Event = EventSubscribeOutput
|
type RunV2Event = EventSubscribeOutput
|
||||||
type PermissionV2Request = Extract<RunV2Event, { type: "permission.v2.asked" }>["data"]
|
|
||||||
type QuestionV2Request = Extract<RunV2Event, { type: "question.v2.asked" }>["data"]
|
|
||||||
type PromptFilePart = Extract<RunPromptPart, { type: "file" }>
|
type PromptFilePart = Extract<RunPromptPart, { type: "file" }>
|
||||||
|
|
||||||
type ToolState = {
|
type ToolState = {
|
||||||
|
|
@ -101,8 +100,8 @@ type ToolState = {
|
||||||
}
|
}
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
permissions: PermissionRequest[]
|
permissions: PermissionV2Request[]
|
||||||
questions: QuestionRequest[]
|
questions: QuestionV2Request[]
|
||||||
view: FooterView
|
view: FooterView
|
||||||
messageIDs: Set<string>
|
messageIDs: Set<string>
|
||||||
text: Map<string, string>
|
text: Map<string, string>
|
||||||
|
|
@ -138,27 +137,6 @@ export function formatUnknownError(error: unknown): string {
|
||||||
return "unknown error"
|
return "unknown error"
|
||||||
}
|
}
|
||||||
|
|
||||||
function permission(request: PermissionV2Request): PermissionRequest {
|
|
||||||
return {
|
|
||||||
id: request.id,
|
|
||||||
sessionID: request.sessionID,
|
|
||||||
permission: request.action,
|
|
||||||
patterns: [...request.resources],
|
|
||||||
metadata: request.metadata ?? {},
|
|
||||||
always: [...(request.save ?? [])],
|
|
||||||
tool: request.source?.type === "tool" ? request.source : undefined,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function question(request: QuestionV2Request): QuestionRequest {
|
|
||||||
return {
|
|
||||||
id: request.id,
|
|
||||||
sessionID: request.sessionID,
|
|
||||||
questions: request.questions.map((item) => ({ ...item, options: item.options.map((option) => ({ ...option })) })),
|
|
||||||
tool: request.tool,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function sessionID(event: RunV2Event) {
|
function sessionID(event: RunV2Event) {
|
||||||
return "sessionID" in event.data && typeof event.data.sessionID === "string" ? event.data.sessionID : undefined
|
return "sessionID" in event.data && typeof event.data.sessionID === "string" ? event.data.sessionID : undefined
|
||||||
}
|
}
|
||||||
|
|
@ -229,8 +207,7 @@ function streamPartKey(messageID: string, partID: string) {
|
||||||
return `${messageID}\u0000${partID}`
|
return `${messageID}\u0000${partID}`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Matches the commit shapes the legacy session-data reducer produced for direct
|
// Direct shell calls use one "start" commit rendering `$ command` and one "progress"
|
||||||
// shell calls: one "start" commit rendering `$ command` and one "progress"
|
|
||||||
// commit rendering the merged output (see toolEntryBody in tool.ts).
|
// commit rendering the merged output (see toolEntryBody in tool.ts).
|
||||||
function shellCommit(
|
function shellCommit(
|
||||||
callID: string,
|
callID: string,
|
||||||
|
|
@ -384,7 +361,7 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderTool = (messageID: string, item: SessionMessageAssistantTool) => {
|
const renderTool = (messageID: string, item: SessionMessageAssistantTool) => {
|
||||||
const part = legacyTool({
|
const part = miniTool({
|
||||||
sessionID: input.sessionID,
|
sessionID: input.sessionID,
|
||||||
messageID,
|
messageID,
|
||||||
tool: item,
|
tool: item,
|
||||||
|
|
@ -536,8 +513,8 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
|
||||||
])
|
])
|
||||||
const projected = structuredClone(messages.data).toReversed() as SessionMessageInfo[]
|
const projected = structuredClone(messages.data).toReversed() as SessionMessageInfo[]
|
||||||
for (const message of projected) renderMessage(message, next.render, next.reuseVisibleWait)
|
for (const message of projected) renderMessage(message, next.render, next.reuseVisibleWait)
|
||||||
state.permissions = permissions.map(permission)
|
state.permissions = permissions
|
||||||
state.questions = questions.map(question)
|
state.questions = questions
|
||||||
syncBlockers()
|
syncBlockers()
|
||||||
await subagents.hydrate({ messages: [...projected], active })
|
await subagents.hydrate({ messages: [...projected], active })
|
||||||
const running = input.sessionID in active
|
const running = input.sessionID in active
|
||||||
|
|
@ -770,7 +747,7 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (event.type === "permission.v2.asked") {
|
if (event.type === "permission.v2.asked") {
|
||||||
if (!state.permissions.some((item) => item.id === event.data.id)) state.permissions.push(permission(event.data))
|
if (!state.permissions.some((item) => item.id === event.data.id)) state.permissions.push(event.data)
|
||||||
syncBlockers()
|
syncBlockers()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -780,7 +757,7 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (event.type === "question.v2.asked") {
|
if (event.type === "question.v2.asked") {
|
||||||
if (!state.questions.some((item) => item.id === event.data.id)) state.questions.push(question(event.data))
|
if (!state.questions.some((item) => item.id === event.data.id)) state.questions.push(event.data)
|
||||||
syncBlockers()
|
syncBlockers()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
// Thin bridge between reducer output and the footer API.
|
// Thin bridge between transport output and the footer API.
|
||||||
//
|
//
|
||||||
// The reducers produce StreamCommit[] and an optional FooterOutput (patch +
|
// Transports produce StreamCommit[] and an optional FooterOutput (patch +
|
||||||
// view + subagent state). This module forwards them to footer.append() and
|
// view + subagent state). This module forwards them to footer.append() and
|
||||||
// footer.event() respectively, adding trace writes along the way. It also
|
// footer.event() respectively, adding trace writes along the way. It also
|
||||||
// defaults status updates to phase "running" if the caller didn't set a
|
// defaults status updates to phase "running" if the caller didn't set a
|
||||||
// phase -- a convenience so reducer code doesn't have to repeat that.
|
// phase -- a convenience so transport code doesn't have to repeat that.
|
||||||
import type { FooterApi, FooterOutput, FooterPatch, FooterSubagentState, StreamCommit } from "./types"
|
import type { FooterApi, FooterOutput, FooterPatch, FooterSubagentState, StreamCommit } from "./types"
|
||||||
|
|
||||||
type Trace = {
|
type Trace = {
|
||||||
|
|
@ -103,9 +103,9 @@ export function traceSubagentState(state: FooterSubagentState) {
|
||||||
permissions: state.permissions.map((item) => ({
|
permissions: state.permissions.map((item) => ({
|
||||||
id: item.id,
|
id: item.id,
|
||||||
sessionID: item.sessionID,
|
sessionID: item.sessionID,
|
||||||
permission: item.permission,
|
action: item.action,
|
||||||
patterns: item.patterns,
|
resources: item.resources,
|
||||||
tool: item.tool,
|
source: item.source,
|
||||||
metadata: item.metadata
|
metadata: item.metadata
|
||||||
? {
|
? {
|
||||||
keys: Object.keys(item.metadata),
|
keys: Object.keys(item.metadata),
|
||||||
|
|
@ -137,7 +137,7 @@ export function traceFooterOutput(footer?: FooterOutput) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Forwards reducer output to the footer: commits go to scrollback, patches update the status bar.
|
// Forwards transport output to the footer: commits go to scrollback, patches update the status bar.
|
||||||
export function writeSessionOutput(input: OutputInput, out: StreamOutput): void {
|
export function writeSessionOutput(input: OutputInput, out: StreamOutput): void {
|
||||||
for (const commit of out.commits) {
|
for (const commit of out.commits) {
|
||||||
input.trace?.write("ui.commit", commit)
|
input.trace?.write("ui.commit", commit)
|
||||||
|
|
|
||||||
|
|
@ -15,10 +15,9 @@
|
||||||
import os from "os"
|
import os from "os"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import stripAnsi from "strip-ansi"
|
import stripAnsi from "strip-ansi"
|
||||||
import type { ToolPart } from "@opencode-ai/sdk/v2"
|
|
||||||
import { LANGUAGE_EXTENSIONS } from "@opencode-ai/tui/util/filetype"
|
import { LANGUAGE_EXTENSIONS } from "@opencode-ai/tui/util/filetype"
|
||||||
import { Locale } from "@opencode-ai/tui/util/locale"
|
import { Locale } from "@opencode-ai/tui/util/locale"
|
||||||
import type { RunEntryBody, StreamCommit, ToolSnapshot } from "./types"
|
import type { MiniToolPart, RunEntryBody, StreamCommit, ToolSnapshot } from "./types"
|
||||||
|
|
||||||
export type ToolView = {
|
export type ToolView = {
|
||||||
output: boolean
|
output: boolean
|
||||||
|
|
@ -1177,7 +1176,7 @@ function rule(name?: string): AnyToolRule | undefined {
|
||||||
return TOOL_RULES[name]
|
return TOOL_RULES[name]
|
||||||
}
|
}
|
||||||
|
|
||||||
function frame(part: ToolPart): ToolFrame {
|
function frame(part: MiniToolPart): ToolFrame {
|
||||||
const state = dict(part.state)
|
const state = dict(part.state)
|
||||||
return {
|
return {
|
||||||
raw: "",
|
raw: "",
|
||||||
|
|
@ -1231,7 +1230,7 @@ export function toolStructuredFinal(commit: StreamCommit): boolean {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toolInlineInfo(part: ToolPart): ToolInline {
|
export function toolInlineInfo(part: MiniToolPart): ToolInline {
|
||||||
const ctx = frame(part)
|
const ctx = frame(part)
|
||||||
const draw = rule(ctx.name)?.run
|
const draw = rule(ctx.name)?.run
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -7,12 +7,16 @@
|
||||||
//
|
//
|
||||||
// Data flow through the system:
|
// Data flow through the system:
|
||||||
//
|
//
|
||||||
// SDK events → session-data reducer → StreamCommit[] + FooterOutput
|
// V2 events / demo actions → StreamCommit[] + FooterOutput
|
||||||
// → stream.ts bridges to footer API
|
// → stream.ts bridges to footer API
|
||||||
// → footer.ts queues commits and patches the footer view
|
// → footer.ts queues commits and patches the footer view
|
||||||
// → OpenTUI split-footer renderer writes to terminal
|
// → OpenTUI split-footer renderer writes to terminal
|
||||||
import type { OpenCodeClient, ReferenceListOutput } from "@opencode-ai/client/promise"
|
import type {
|
||||||
import type { FilePart, PermissionRequest, QuestionRequest, ToolPart } from "@opencode-ai/sdk/v2"
|
OpenCodeClient,
|
||||||
|
PermissionV2Request,
|
||||||
|
QuestionV2Request,
|
||||||
|
ReferenceListOutput,
|
||||||
|
} from "@opencode-ai/client/promise"
|
||||||
import type { TuiConfig } from "@opencode-ai/tui/config/v1"
|
import type { TuiConfig } from "@opencode-ai/tui/config/v1"
|
||||||
|
|
||||||
export type RunFilePart = {
|
export type RunFilePart = {
|
||||||
|
|
@ -30,7 +34,11 @@ export type RunPromptPart =
|
||||||
url: string
|
url: string
|
||||||
filename?: string
|
filename?: string
|
||||||
mime?: string
|
mime?: string
|
||||||
source?: FilePart["source"]
|
source?: {
|
||||||
|
type: string
|
||||||
|
text: { start: number; end: number; value: string }
|
||||||
|
[key: string]: unknown
|
||||||
|
}
|
||||||
}
|
}
|
||||||
| { type: "agent"; name: string; source?: { start: number; end: number; value: string } }
|
| { type: "agent"; name: string; source?: { start: number; end: number; value: string } }
|
||||||
|
|
||||||
|
|
@ -210,6 +218,41 @@ export type ToolQuestionSnapshot = {
|
||||||
|
|
||||||
export type ToolSnapshot = ToolCodeSnapshot | ToolDiffSnapshot | ToolTaskSnapshot | ToolQuestionSnapshot
|
export type ToolSnapshot = ToolCodeSnapshot | ToolDiffSnapshot | ToolTaskSnapshot | ToolQuestionSnapshot
|
||||||
|
|
||||||
|
export type MiniToolState =
|
||||||
|
| { status: "pending"; input: Record<string, unknown>; raw?: string }
|
||||||
|
| {
|
||||||
|
status: "running"
|
||||||
|
input: Record<string, unknown>
|
||||||
|
title?: string
|
||||||
|
metadata?: Record<string, unknown>
|
||||||
|
time: { start: number }
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
status: "completed"
|
||||||
|
input: Record<string, unknown>
|
||||||
|
output: string
|
||||||
|
title?: string
|
||||||
|
metadata?: Record<string, unknown>
|
||||||
|
time: { start: number; end: number }
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
status: "error"
|
||||||
|
input: Record<string, unknown>
|
||||||
|
error: string
|
||||||
|
metadata?: Record<string, unknown>
|
||||||
|
time: { start: number; end: number }
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MiniToolPart = {
|
||||||
|
id: string
|
||||||
|
sessionID: string
|
||||||
|
messageID: string
|
||||||
|
type?: "tool"
|
||||||
|
callID: string
|
||||||
|
tool: string
|
||||||
|
state: MiniToolState
|
||||||
|
}
|
||||||
|
|
||||||
export type EntryLayout = "inline" | "block"
|
export type EntryLayout = "inline" | "block"
|
||||||
|
|
||||||
export type RunEntryBody =
|
export type RunEntryBody =
|
||||||
|
|
@ -220,13 +263,13 @@ export type RunEntryBody =
|
||||||
| { type: "structured"; snapshot: ToolSnapshot }
|
| { type: "structured"; snapshot: ToolSnapshot }
|
||||||
|
|
||||||
// Which interactive surface the footer is showing. Only one view is active at
|
// Which interactive surface the footer is showing. Only one view is active at
|
||||||
// a time. The reducer drives transitions: when a permission arrives the view
|
// a time. The transport drives transitions: when a permission arrives the view
|
||||||
// switches to "permission", and when the permission resolves it falls back to
|
// switches to "permission", and when the permission resolves it falls back to
|
||||||
// "prompt".
|
// "prompt".
|
||||||
export type FooterView =
|
export type FooterView =
|
||||||
| { type: "prompt" }
|
| { type: "prompt" }
|
||||||
| { type: "permission"; request: PermissionRequest }
|
| { type: "permission"; request: PermissionV2Request }
|
||||||
| { type: "question"; request: QuestionRequest }
|
| { type: "question"; request: QuestionV2Request }
|
||||||
|
|
||||||
export type FooterPromptRoute =
|
export type FooterPromptRoute =
|
||||||
| { type: "composer" }
|
| { type: "composer" }
|
||||||
|
|
@ -259,11 +302,11 @@ export type FooterSubagentDetail = {
|
||||||
export type FooterSubagentState = {
|
export type FooterSubagentState = {
|
||||||
tabs: FooterSubagentTab[]
|
tabs: FooterSubagentTab[]
|
||||||
details: Record<string, FooterSubagentDetail>
|
details: Record<string, FooterSubagentDetail>
|
||||||
permissions: PermissionRequest[]
|
permissions: PermissionV2Request[]
|
||||||
questions: QuestionRequest[]
|
questions: QuestionV2Request[]
|
||||||
}
|
}
|
||||||
|
|
||||||
// The reducer emits this alongside scrollback commits so the footer can update in the same frame.
|
// The transport emits this alongside scrollback commits so the footer can update in the same frame.
|
||||||
export type FooterOutput = {
|
export type FooterOutput = {
|
||||||
patch?: FooterPatch
|
patch?: FooterPatch
|
||||||
view?: FooterView
|
view?: FooterView
|
||||||
|
|
@ -357,8 +400,8 @@ export type StreamSource = "assistant" | "reasoning" | "tool" | "system"
|
||||||
|
|
||||||
export type StreamToolState = "running" | "completed" | "error"
|
export type StreamToolState = "running" | "completed" | "error"
|
||||||
|
|
||||||
// A single append-only commit to scrollback. The session-data reducer produces
|
// A single append-only commit to scrollback. The transport produces these from
|
||||||
// these from SDK events, and RunFooter.append() queues them for the next
|
// V2 events, and RunFooter.append() queues them for the next
|
||||||
// microtask flush. Once flushed, they become immutable terminal scrollback
|
// microtask flush. Once flushed, they become immutable terminal scrollback
|
||||||
// rows -- they cannot be rewritten.
|
// rows -- they cannot be rewritten.
|
||||||
export type StreamCommit = {
|
export type StreamCommit = {
|
||||||
|
|
@ -370,7 +413,7 @@ export type StreamCommit = {
|
||||||
messageID?: string
|
messageID?: string
|
||||||
partID?: string
|
partID?: string
|
||||||
tool?: string
|
tool?: string
|
||||||
part?: ToolPart
|
part?: MiniToolPart
|
||||||
interrupted?: boolean
|
interrupted?: boolean
|
||||||
toolState?: StreamToolState
|
toolState?: StreamToolState
|
||||||
toolError?: string
|
toolError?: string
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue