refactor(cli): remove legacy sdk dependency
This commit is contained in:
parent
ce2e301e24
commit
358d4746a9
22 changed files with 350 additions and 1705 deletions
1
bun.lock
1
bun.lock
|
|
@ -108,7 +108,6 @@
|
|||
"@opencode-ai/core": "workspace:*",
|
||||
"@opencode-ai/plugin": "workspace:*",
|
||||
"@opencode-ai/schema": "workspace:*",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@opencode-ai/server": "workspace:*",
|
||||
"@opencode-ai/tui": "workspace:*",
|
||||
"@opentui/core": "catalog:",
|
||||
|
|
|
|||
|
|
@ -36,7 +36,6 @@
|
|||
"@opencode-ai/core": "workspace:*",
|
||||
"@opencode-ai/plugin": "workspace:*",
|
||||
"@opencode-ai/schema": "workspace:*",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@opencode-ai/server": "workspace:*",
|
||||
"@opencode-ai/tui": "workspace:*",
|
||||
"@opentui/core": "catalog:",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { EOL } from "os"
|
||||
import { Effect } from "effect"
|
||||
import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"
|
||||
import { OpenCode } from "@opencode-ai/client"
|
||||
import { Commands } from "../../commands"
|
||||
import { Runtime } from "../../../framework/runtime"
|
||||
import { Service } from "@opencode-ai/client/effect"
|
||||
|
|
@ -12,11 +12,11 @@ export default Runtime.handler(
|
|||
const options = yield* ServiceConfig.options()
|
||||
const found = yield* Service.discover(options)
|
||||
const endpoint = found ?? (yield* Service.start(options))
|
||||
const client = createOpencodeClient({ baseUrl: endpoint.url, headers: Service.headers(endpoint) })
|
||||
const response = yield* Effect.promise(() => client.v2.agent.list({ location: { directory: process.cwd() } }))
|
||||
const client = OpenCode.make({ baseUrl: endpoint.url, headers: Service.headers(endpoint) })
|
||||
const response = yield* Effect.promise(() => client.agent.list({ location: { directory: process.cwd() } }))
|
||||
process.stdout.write(
|
||||
JSON.stringify(
|
||||
response.data?.data.toSorted((a, b) => a.id.localeCompare(b.id)),
|
||||
response.data.toSorted((a, b) => a.id.localeCompare(b.id)),
|
||||
null,
|
||||
2,
|
||||
) + EOL,
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import { EOL } from "node:os"
|
||||
import { Effect } from "effect"
|
||||
import {
|
||||
createOpencodeClient,
|
||||
OpenCode,
|
||||
type IntegrationAttemptStatus,
|
||||
type IntegrationOAuthMethod,
|
||||
type OpencodeClient,
|
||||
} from "@opencode-ai/sdk/v2/client"
|
||||
type OpenCodeClient,
|
||||
} from "@opencode-ai/client"
|
||||
import { Commands } from "../../commands"
|
||||
import { Runtime } from "../../../framework/runtime"
|
||||
import { Service } from "@opencode-ai/client/effect"
|
||||
|
|
@ -20,7 +20,7 @@ export default Runtime.handler(
|
|||
const options = yield* ServiceConfig.options()
|
||||
const found = yield* Service.discover(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)
|
||||
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`))
|
||||
|
||||
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
|
||||
if (!attempt) return yield* Effect.fail(new Error(started.error?.message ?? "Failed to start OAuth attempt"))
|
||||
const attempt = started.data
|
||||
if (attempt.mode === "code")
|
||||
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 = (
|
||||
client: OpencodeClient,
|
||||
client: OpenCodeClient,
|
||||
attemptID: string,
|
||||
): Effect.Effect<Exclude<IntegrationAttemptStatus, { status: "pending" }>> =>
|
||||
Effect.gen(function* () {
|
||||
const response = yield* Effect.promise(() => client.v2.integration.attempt.status({ attemptID, location }))
|
||||
const status = response.data?.data
|
||||
if (!status || status.status === "pending") {
|
||||
const status = yield* Effect.promise(() => client.integration.attempt.status({ attemptID, location })).pipe(
|
||||
Effect.map((result) => result.data),
|
||||
)
|
||||
if (status.status === "pending") {
|
||||
yield* Effect.sleep("1 second")
|
||||
return yield* poll(client, attemptID)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { EOL } from "node:os"
|
||||
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 { Runtime } from "../../../framework/runtime"
|
||||
import { Service } from "@opencode-ai/client/effect"
|
||||
|
|
@ -12,9 +12,9 @@ export default Runtime.handler(
|
|||
const options = yield* ServiceConfig.options()
|
||||
const found = yield* Service.discover(options)
|
||||
const endpoint = found ?? (yield* Service.start(options))
|
||||
const client = createOpencodeClient({ baseUrl: endpoint.url, headers: Service.headers(endpoint) })
|
||||
const response = yield* Effect.promise(() => client.v2.mcp.list({ location: { directory: process.cwd() } }))
|
||||
const servers = (response.data?.data ?? []).toSorted((a, b) => a.name.localeCompare(b.name))
|
||||
const client = OpenCode.make({ baseUrl: endpoint.url, headers: Service.headers(endpoint) })
|
||||
const response = yield* Effect.promise(() => client.mcp.list({ location: { directory: process.cwd() } }))
|
||||
const servers = response.data.toSorted((a, b) => a.name.localeCompare(b.name))
|
||||
if (servers.length === 0) {
|
||||
process.stdout.write("No MCP servers configured" + EOL)
|
||||
return
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { EOL } from "node:os"
|
||||
import { Effect } from "effect"
|
||||
import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"
|
||||
import { OpenCode } from "@opencode-ai/client"
|
||||
import { Commands } from "../../commands"
|
||||
import { Runtime } from "../../../framework/runtime"
|
||||
import { Service } from "@opencode-ai/client/effect"
|
||||
|
|
@ -15,7 +15,7 @@ export default Runtime.handler(
|
|||
const options = yield* ServiceConfig.options()
|
||||
const found = yield* Service.discover(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)
|
||||
if (!integration) {
|
||||
|
|
@ -31,7 +31,7 @@ export default Runtime.handler(
|
|||
|
||||
yield* Effect.forEach(
|
||||
credentials,
|
||||
(connection) => Effect.promise(() => client.v2.credential.remove({ credentialID: connection.id, location })),
|
||||
(connection) => Effect.promise(() => client.credential.remove({ credentialID: connection.id, location })),
|
||||
{ discard: true },
|
||||
)
|
||||
process.stdout.write(`Removed OAuth credentials for ${input.name}` + EOL)
|
||||
|
|
|
|||
|
|
@ -1,17 +1,18 @@
|
|||
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
|
||||
// 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
|
||||
// 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* () {
|
||||
const servers = yield* Effect.promise(() => client.v2.mcp.list({ location }))
|
||||
const server = (servers.data?.data ?? []).find((entry) => entry.name === name)
|
||||
const servers = yield* Effect.promise(() => client.mcp.list({ location }))
|
||||
const server = servers.data.find((entry) => entry.name === name)
|
||||
if (!server) return yield* Effect.fail(new Error(`MCP server not found: ${name}`))
|
||||
const integrationID = server.integrationID
|
||||
if (!integrationID) return undefined
|
||||
const found = yield* Effect.promise(() => client.v2.integration.get({ integrationID, location }))
|
||||
return found.data?.data
|
||||
return yield* Effect.promise(() => client.integration.get({ integrationID, location })).pipe(
|
||||
Effect.map((result) => result.data ?? undefined),
|
||||
)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// Demo mode for testing direct interactive mode without a real SDK.
|
||||
//
|
||||
// Enabled with `--demo`. Intercepts prompt submissions and generates synthetic
|
||||
// SDK events that feed through the real reducer and footer pipeline. This
|
||||
// Enabled with `--demo`. Intercepts prompt submissions and drives the same
|
||||
// presentation commits and footer actions as the live transport. This
|
||||
// lets you test scrollback formatting, permission UI, question UI, and tool
|
||||
// snapshots without making actual model calls. Pass a demo slash command as
|
||||
// the initial interactive message to trigger a preview immediately.
|
||||
|
|
@ -15,10 +15,18 @@
|
|||
// Demo mode also handles permission and question replies locally, completing
|
||||
// or failing the synthetic tool parts as appropriate.
|
||||
import path from "path"
|
||||
import type { Event, ToolPart } from "@opencode-ai/sdk/v2"
|
||||
import { createSessionData, reduceSessionData, type SessionData } from "./session-data"
|
||||
import type { PermissionV2Request, QuestionV2Request } from "@opencode-ai/client/promise"
|
||||
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 = [
|
||||
"markdown",
|
||||
|
|
@ -124,7 +132,7 @@ type Permit = {
|
|||
ref: Ref
|
||||
permission: string
|
||||
patterns: string[]
|
||||
metadata?: Record<string, unknown>
|
||||
metadata?: PermissionV2Request["metadata"]
|
||||
always: string[]
|
||||
done: Perm["done"]
|
||||
}
|
||||
|
|
@ -132,9 +140,7 @@ type Permit = {
|
|||
type State = {
|
||||
id: string
|
||||
thinking: boolean
|
||||
data: SessionData
|
||||
footer: FooterApi
|
||||
limits: () => Record<string, number>
|
||||
msg: number
|
||||
part: number
|
||||
call: number
|
||||
|
|
@ -142,12 +148,12 @@ type State = {
|
|||
ask: number
|
||||
perms: Map<string, Perm>
|
||||
asks: Map<string, Ask>
|
||||
started: Set<string>
|
||||
}
|
||||
|
||||
type Input = {
|
||||
sessionID: string
|
||||
thinking: boolean
|
||||
limits: () => Record<string, number>
|
||||
footer: FooterApi
|
||||
}
|
||||
|
||||
|
|
@ -255,185 +261,69 @@ function take(state: State, key: "msg" | "part" | "call" | "perm" | "ask", prefi
|
|||
return `demo_${prefix}_${state[key]}`
|
||||
}
|
||||
|
||||
function feed(state: State, event: Event): void {
|
||||
const out = reduceSessionData({
|
||||
data: state.data,
|
||||
event,
|
||||
sessionID: state.id,
|
||||
thinking: state.thinking,
|
||||
limits: state.limits(),
|
||||
})
|
||||
state.data = out.data
|
||||
function present(state: State, commits: StreamCommit[], view?: QuestionV2Request | PermissionV2Request): void {
|
||||
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 {
|
||||
const id = 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
|
||||
return take(state, "msg", "msg")
|
||||
}
|
||||
|
||||
async function emitText(state: State, body: string, signal?: AbortSignal): Promise<void> {
|
||||
const msg = open(state)
|
||||
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)) {
|
||||
if (signal?.aborted) {
|
||||
return
|
||||
}
|
||||
|
||||
next += item
|
||||
feed(state, {
|
||||
type: "message.part.delta",
|
||||
properties: {
|
||||
sessionID: state.id,
|
||||
messageID: msg,
|
||||
partID: part,
|
||||
field: "text",
|
||||
delta: item,
|
||||
},
|
||||
} as Event)
|
||||
present(state, [{ kind: "assistant", source: "assistant", text: item, phase: "progress", messageID: msg, partID: part }])
|
||||
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> {
|
||||
const msg = open(state)
|
||||
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: "reasoning",
|
||||
text: "",
|
||||
time: {
|
||||
start,
|
||||
},
|
||||
},
|
||||
},
|
||||
} as Event)
|
||||
|
||||
let next = ""
|
||||
let first = true
|
||||
for (const item of split(body)) {
|
||||
if (signal?.aborted) {
|
||||
return
|
||||
}
|
||||
|
||||
next += item
|
||||
feed(state, {
|
||||
type: "message.part.delta",
|
||||
properties: {
|
||||
sessionID: state.id,
|
||||
messageID: msg,
|
||||
partID: part,
|
||||
field: "text",
|
||||
delta: item,
|
||||
},
|
||||
} as Event)
|
||||
if (state.thinking) {
|
||||
present(state, [
|
||||
{
|
||||
kind: "reasoning",
|
||||
source: "reasoning",
|
||||
text: first ? `Thinking: ${item.replace(/\[REDACTED\]/g, "")}` : item.replace(/\[REDACTED\]/g, ""),
|
||||
phase: "progress",
|
||||
messageID: msg,
|
||||
partID: part,
|
||||
},
|
||||
])
|
||||
first = false
|
||||
}
|
||||
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 {
|
||||
|
|
@ -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 {
|
||||
feed(state, {
|
||||
type: "message.part.updated",
|
||||
properties: {
|
||||
sessionID: state.id,
|
||||
time: Date.now(),
|
||||
part: {
|
||||
id: ref.part,
|
||||
sessionID: state.id,
|
||||
messageID: ref.msg,
|
||||
type: "tool",
|
||||
callID: ref.call,
|
||||
tool: ref.tool,
|
||||
state: {
|
||||
status: "running",
|
||||
input: ref.input,
|
||||
metadata,
|
||||
time: {
|
||||
start: ref.start,
|
||||
},
|
||||
state.started.add(ref.part)
|
||||
present(
|
||||
state,
|
||||
[
|
||||
toolCommit(
|
||||
{
|
||||
id: ref.part,
|
||||
sessionID: state.id,
|
||||
messageID: ref.msg,
|
||||
callID: ref.call,
|
||||
tool: ref.tool,
|
||||
state: { status: "running", input: ref.input, metadata, time: { start: ref.start } },
|
||||
},
|
||||
},
|
||||
},
|
||||
} as Event)
|
||||
"start",
|
||||
),
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
function askPermission(state: State, item: Permit): void {
|
||||
|
|
@ -482,21 +366,15 @@ function askPermission(state: State, item: Permit): void {
|
|||
done: item.done,
|
||||
})
|
||||
|
||||
feed(state, {
|
||||
type: "permission.asked",
|
||||
properties: {
|
||||
id,
|
||||
sessionID: state.id,
|
||||
permission: item.permission,
|
||||
patterns: item.patterns,
|
||||
metadata: item.metadata ?? {},
|
||||
always: item.always,
|
||||
tool: {
|
||||
messageID: item.ref.msg,
|
||||
callID: item.ref.call,
|
||||
},
|
||||
},
|
||||
} as Event)
|
||||
present(state, [], {
|
||||
id,
|
||||
sessionID: state.id,
|
||||
action: item.permission,
|
||||
resources: item.patterns,
|
||||
metadata: item.metadata ?? {},
|
||||
save: item.always,
|
||||
source: { type: "tool", messageID: item.ref.msg, callID: item.ref.call },
|
||||
})
|
||||
}
|
||||
|
||||
function doneTool(
|
||||
|
|
@ -508,77 +386,53 @@ function doneTool(
|
|||
metadata?: Record<string, unknown>
|
||||
},
|
||||
): void {
|
||||
feed(state, {
|
||||
type: "message.part.updated",
|
||||
properties: {
|
||||
sessionID: state.id,
|
||||
time: Date.now(),
|
||||
part: {
|
||||
id: ref.part,
|
||||
sessionID: state.id,
|
||||
messageID: ref.msg,
|
||||
type: "tool",
|
||||
callID: ref.call,
|
||||
tool: ref.tool,
|
||||
state: {
|
||||
status: "completed",
|
||||
input: ref.input,
|
||||
output: output.output,
|
||||
title: output.title,
|
||||
metadata: output.metadata ?? {},
|
||||
time: {
|
||||
start: ref.start,
|
||||
end: Date.now(),
|
||||
},
|
||||
},
|
||||
},
|
||||
if (!state.started.has(ref.part)) startTool(state, ref)
|
||||
const part: MiniToolPart = {
|
||||
id: ref.part,
|
||||
sessionID: state.id,
|
||||
messageID: ref.msg,
|
||||
callID: ref.call,
|
||||
tool: ref.tool,
|
||||
state: {
|
||||
status: "completed",
|
||||
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 {
|
||||
feed(state, {
|
||||
type: "message.part.updated",
|
||||
properties: {
|
||||
sessionID: state.id,
|
||||
time: Date.now(),
|
||||
part: {
|
||||
id: ref.part,
|
||||
sessionID: state.id,
|
||||
messageID: ref.msg,
|
||||
type: "tool",
|
||||
callID: ref.call,
|
||||
tool: ref.tool,
|
||||
state: {
|
||||
status: "error",
|
||||
input: ref.input,
|
||||
error,
|
||||
metadata: {},
|
||||
time: {
|
||||
start: ref.start,
|
||||
end: Date.now(),
|
||||
if (!state.started.has(ref.part)) startTool(state, ref)
|
||||
present(
|
||||
state,
|
||||
[
|
||||
toolCommit(
|
||||
{
|
||||
id: ref.part,
|
||||
sessionID: state.id,
|
||||
messageID: ref.msg,
|
||||
callID: ref.call,
|
||||
tool: ref.tool,
|
||||
state: {
|
||||
status: "error",
|
||||
input: ref.input,
|
||||
error,
|
||||
metadata: {},
|
||||
time: { start: ref.start, end: Date.now() },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as Event)
|
||||
"final",
|
||||
),
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
function emitError(state: State, text: string): void {
|
||||
const event = {
|
||||
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)
|
||||
present(state, [{ kind: "error", source: "system", text, phase: "start" }])
|
||||
}
|
||||
|
||||
async function emitBash(state: State, signal?: AbortSignal): Promise<void> {
|
||||
|
|
@ -685,7 +539,7 @@ function emitTask(state: State): void {
|
|||
start: Date.now(),
|
||||
},
|
||||
},
|
||||
} satisfies ToolPart
|
||||
} satisfies MiniToolPart
|
||||
showSubagent(state, {
|
||||
sessionID: "sub_demo_1",
|
||||
partID: ref.part,
|
||||
|
|
@ -979,18 +833,12 @@ function emitQuestion(state: State, kind: QuestionKind = "multi"): void {
|
|||
const id = take(state, "ask", "ask")
|
||||
state.asks.set(id, { ref })
|
||||
|
||||
feed(state, {
|
||||
type: "question.asked",
|
||||
properties: {
|
||||
id,
|
||||
sessionID: state.id,
|
||||
questions,
|
||||
tool: {
|
||||
messageID: ref.msg,
|
||||
callID: ref.call,
|
||||
},
|
||||
},
|
||||
} as Event)
|
||||
present(state, [], {
|
||||
id,
|
||||
sessionID: state.id,
|
||||
questions,
|
||||
tool: { messageID: ref.msg, callID: ref.call },
|
||||
})
|
||||
}
|
||||
|
||||
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 = {
|
||||
id: input.sessionID,
|
||||
thinking: input.thinking,
|
||||
data: createSessionData(),
|
||||
footer: input.footer,
|
||||
limits: input.limits,
|
||||
msg: 0,
|
||||
part: 0,
|
||||
call: 0,
|
||||
|
|
@ -1099,6 +945,7 @@ export function createRunDemo(input: Input) {
|
|||
ask: 0,
|
||||
perms: new Map(),
|
||||
asks: new Map(),
|
||||
started: new Set(),
|
||||
}
|
||||
|
||||
const start = async (): Promise<void> => {
|
||||
|
|
@ -1166,16 +1013,7 @@ export function createRunDemo(input: Input) {
|
|||
}
|
||||
|
||||
state.perms.delete(input.requestID)
|
||||
const event = {
|
||||
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)
|
||||
clearBlocker(state)
|
||||
|
||||
if (input.reply === "reject") {
|
||||
failTool(state, item.ref, input.message || "permission rejected")
|
||||
|
|
@ -1193,16 +1031,7 @@ export function createRunDemo(input: Input) {
|
|||
}
|
||||
|
||||
state.asks.delete(input.requestID)
|
||||
const event = {
|
||||
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)
|
||||
clearBlocker(state)
|
||||
doneTool(state, ask.ref, {
|
||||
title: "question",
|
||||
output: "",
|
||||
|
|
@ -1220,13 +1049,7 @@ export function createRunDemo(input: Input) {
|
|||
}
|
||||
|
||||
state.asks.delete(input.requestID)
|
||||
feed(state, {
|
||||
type: "question.rejected",
|
||||
properties: {
|
||||
sessionID: state.id,
|
||||
requestID: input.requestID,
|
||||
},
|
||||
} as Event)
|
||||
clearBlocker(state)
|
||||
failTool(state, ask.ref, "question rejected")
|
||||
return true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
import type { TextareaRenderable } from "@opentui/core"
|
||||
import { useKeyboard, useTerminalDimensions } from "@opentui/solid"
|
||||
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 {
|
||||
createPermissionBodyState,
|
||||
permissionAlwaysLines,
|
||||
|
|
@ -130,7 +130,7 @@ export function RejectField(props: {
|
|||
}
|
||||
|
||||
export function RunPermissionBody(props: {
|
||||
request: PermissionRequest
|
||||
request: PermissionV2Request
|
||||
theme: RunFooterTheme
|
||||
block: RunBlockTheme
|
||||
diffStyle?: RunDiffStyle
|
||||
|
|
@ -142,7 +142,7 @@ export function RunPermissionBody(props: {
|
|||
const ft = createMemo(() => toolFiletype(info().file))
|
||||
const narrow = createMemo(() => footerWidthPolicy(dims().width).dialog.narrow)
|
||||
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 title = createMemo(() => {
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
import type { TextareaRenderable } from "@opentui/core"
|
||||
import { useKeyboard, useTerminalDimensions } from "@opentui/solid"
|
||||
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 {
|
||||
createQuestionBodyState,
|
||||
questionConfirm,
|
||||
|
|
@ -45,7 +45,7 @@ import type { RunFooterTheme } from "./theme"
|
|||
import type { QuestionReject, QuestionReply } from "./types"
|
||||
|
||||
export function RunQuestionBody(props: {
|
||||
request: QuestionRequest
|
||||
request: QuestionV2Request
|
||||
theme: RunFooterTheme
|
||||
onReply: (input: QuestionReply) => void | Promise<void>
|
||||
onReject: (input: QuestionReject) => void | Promise<void>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
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 { EOL } from "node:os"
|
||||
import { UI } from "./ui"
|
||||
import type { MiniToolPart } from "./types"
|
||||
|
||||
type Model = {
|
||||
providerID: string
|
||||
|
|
@ -28,8 +28,8 @@ type Input = {
|
|||
auto: boolean
|
||||
/** True when the client is attached to a shared server rather than an exclusive in-process one. */
|
||||
attached: boolean
|
||||
renderTool: (part: ToolPart) => Promise<void>
|
||||
renderToolError: (part: ToolPart) => Promise<void>
|
||||
renderTool: (part: MiniToolPart) => Promise<void>
|
||||
renderToolError: (part: MiniToolPart) => Promise<void>
|
||||
}
|
||||
|
||||
type StartedPart = {
|
||||
|
|
@ -77,7 +77,7 @@ export async function runNonInteractivePrompt(input: Input) {
|
|||
return true
|
||||
}
|
||||
|
||||
const writeText = (part: TextPart, timestamp: number) => {
|
||||
const writeText = (part: { text: string; [key: string]: unknown }, timestamp: number) => {
|
||||
if (emit("text", timestamp, { part })) return
|
||||
const text = part.text.trim()
|
||||
if (!text) return
|
||||
|
|
@ -169,7 +169,7 @@ export async function runNonInteractivePrompt(input: Input) {
|
|||
if (!promoted) continue
|
||||
|
||||
if (event.type === "session.step.started") {
|
||||
const part: StepStartPart = {
|
||||
const part = {
|
||||
id: partID(event.id),
|
||||
sessionID: input.sessionID,
|
||||
messageID: event.data.assistantMessageID,
|
||||
|
|
@ -191,7 +191,7 @@ export async function runNonInteractivePrompt(input: Input) {
|
|||
if (event.type === "session.text.ended") {
|
||||
const started = starts.get("text")
|
||||
starts.delete("text")
|
||||
const part: TextPart = {
|
||||
const part = {
|
||||
id: started?.id ?? partID(event.id),
|
||||
sessionID: input.sessionID,
|
||||
messageID: event.data.assistantMessageID,
|
||||
|
|
@ -210,7 +210,7 @@ export async function runNonInteractivePrompt(input: Input) {
|
|||
if (event.type === "session.reasoning.ended" && input.thinking) {
|
||||
const started = starts.get("reasoning")
|
||||
starts.delete("reasoning")
|
||||
const part: ReasoningPart = {
|
||||
const part = {
|
||||
id: started?.id ?? partID(event.id),
|
||||
sessionID: input.sessionID,
|
||||
messageID: event.data.assistantMessageID,
|
||||
|
|
@ -263,7 +263,7 @@ export async function runNonInteractivePrompt(input: Input) {
|
|||
}
|
||||
if (event.type === "session.tool.success") {
|
||||
const current = tools.get(event.data.callID) ?? fallbackTool(event)
|
||||
const part: ToolPart = {
|
||||
const part: MiniToolPart = {
|
||||
id: current.id,
|
||||
sessionID: input.sessionID,
|
||||
messageID: event.data.assistantMessageID,
|
||||
|
|
@ -296,7 +296,7 @@ export async function runNonInteractivePrompt(input: Input) {
|
|||
if (event.type === "session.tool.failed") {
|
||||
const current = tools.get(event.data.callID) ?? fallbackTool(event)
|
||||
const error = event.data.error.message
|
||||
const part: ToolPart = {
|
||||
const part: MiniToolPart = {
|
||||
id: current.id,
|
||||
sessionID: input.sessionID,
|
||||
messageID: event.data.assistantMessageID,
|
||||
|
|
@ -325,7 +325,7 @@ export async function runNonInteractivePrompt(input: Input) {
|
|||
}
|
||||
|
||||
if (event.type === "session.step.ended") {
|
||||
const part: StepFinishPart = {
|
||||
const part = {
|
||||
id: partID(event.id),
|
||||
sessionID: input.sessionID,
|
||||
messageID: event.data.assistantMessageID,
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
//
|
||||
// permissionInfo() extracts display info (icon, title, lines, diff) from
|
||||
// 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 { toolPath, toolPermissionInfo } from "./tool"
|
||||
|
||||
|
|
@ -55,7 +55,7 @@ function text(v: unknown): string {
|
|||
return typeof v === "string" ? v : ""
|
||||
}
|
||||
|
||||
function data(request: PermissionRequest): Dict {
|
||||
function data(request: PermissionV2Request): Dict {
|
||||
const meta = dict(request.metadata)
|
||||
return {
|
||||
...meta,
|
||||
|
|
@ -63,8 +63,8 @@ function data(request: PermissionRequest): Dict {
|
|||
}
|
||||
}
|
||||
|
||||
function patterns(request: PermissionRequest): string[] {
|
||||
return request.patterns.filter((item): item is string => typeof item === "string")
|
||||
function patterns(request: PermissionV2Request): string[] {
|
||||
return request.resources.filter((item): item is string => typeof item === "string")
|
||||
}
|
||||
|
||||
export function createPermissionBodyState(requestID: string): PermissionBodyState {
|
||||
|
|
@ -89,15 +89,15 @@ export function permissionOptions(stage: PermissionStage): PermissionOption[] {
|
|||
return []
|
||||
}
|
||||
|
||||
export function permissionInfo(request: PermissionRequest): PermissionInfo {
|
||||
export function permissionInfo(request: PermissionV2Request): PermissionInfo {
|
||||
const pats = patterns(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) {
|
||||
return info
|
||||
}
|
||||
|
||||
if (request.permission === "external_directory") {
|
||||
if (request.action === "external_directory") {
|
||||
const meta = dict(request.metadata)
|
||||
const raw = text(meta.parentDir) || text(meta.filepath) || pats[0] || ""
|
||||
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 {
|
||||
icon: "⟳",
|
||||
title: "Continue after repeated failures",
|
||||
|
|
@ -118,19 +118,20 @@ export function permissionInfo(request: PermissionRequest): PermissionInfo {
|
|||
|
||||
return {
|
||||
icon: "⚙",
|
||||
title: `Call tool ${request.permission}`,
|
||||
lines: [`Tool: ${request.permission}`],
|
||||
title: `Call tool ${request.action}`,
|
||||
lines: [`Tool: ${request.action}`],
|
||||
}
|
||||
}
|
||||
|
||||
export function permissionAlwaysLines(request: PermissionRequest): string[] {
|
||||
if (request.always.length === 1 && request.always[0] === "*") {
|
||||
return [`This will allow ${request.permission} until OpenCode is restarted.`]
|
||||
export function permissionAlwaysLines(request: PermissionV2Request): string[] {
|
||||
const save = request.save ?? []
|
||||
if (save.length === 1 && save[0] === "*") {
|
||||
return [`This will allow ${request.action} until OpenCode is restarted.`]
|
||||
}
|
||||
|
||||
return [
|
||||
"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
|
||||
// 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"
|
||||
|
||||
export type QuestionBodyState = {
|
||||
|
|
@ -51,23 +51,23 @@ export function questionSync(state: QuestionBodyState, requestID: string): Quest
|
|||
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
|
||||
}
|
||||
|
||||
export function questionTabs(request: QuestionRequest): number {
|
||||
export function questionTabs(request: QuestionV2Request): number {
|
||||
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
|
||||
}
|
||||
|
||||
export function questionInfo(request: QuestionRequest, state: QuestionBodyState): QuestionInfo | undefined {
|
||||
export function questionInfo(request: QuestionV2Request, state: QuestionBodyState): QuestionV2Info | undefined {
|
||||
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
|
||||
}
|
||||
|
||||
|
|
@ -84,7 +84,7 @@ export function questionPicked(state: QuestionBodyState): boolean {
|
|||
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)
|
||||
if (!info || info.custom === false) {
|
||||
return false
|
||||
|
|
@ -93,7 +93,7 @@ export function questionOther(request: QuestionRequest, state: QuestionBodyState
|
|||
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)
|
||||
if (!info) {
|
||||
return 0
|
||||
|
|
@ -156,7 +156,7 @@ export function questionStoreCustom(state: QuestionBodyState, tab: number, text:
|
|||
|
||||
function questionPick(
|
||||
state: QuestionBodyState,
|
||||
request: QuestionRequest,
|
||||
request: QuestionV2Request,
|
||||
answer: string,
|
||||
custom = false,
|
||||
): QuestionStep {
|
||||
|
|
@ -204,7 +204,7 @@ function questionToggle(state: QuestionBodyState, answer: string): QuestionBodyS
|
|||
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)
|
||||
if (total === 0) {
|
||||
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)
|
||||
if (!info) {
|
||||
return { state }
|
||||
|
|
@ -255,7 +255,7 @@ export function questionSelect(state: QuestionBodyState, request: QuestionReques
|
|||
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)
|
||||
if (!info) {
|
||||
return { state }
|
||||
|
|
@ -305,20 +305,20 @@ export function questionSave(state: QuestionBodyState, request: QuestionRequest)
|
|||
return questionPick(state, request, value, true)
|
||||
}
|
||||
|
||||
export function questionSubmit(request: QuestionRequest, state: QuestionBodyState): QuestionReply {
|
||||
export function questionSubmit(request: QuestionV2Request, state: QuestionBodyState): QuestionReply {
|
||||
return {
|
||||
requestID: request.id,
|
||||
answers: questionAnswers(state, request.questions.length),
|
||||
}
|
||||
}
|
||||
|
||||
export function questionReject(request: QuestionRequest): QuestionReject {
|
||||
export function questionReject(request: QuestionV2Request): QuestionReject {
|
||||
return {
|
||||
requestID: request.id,
|
||||
}
|
||||
}
|
||||
|
||||
export function questionHint(request: QuestionRequest, state: QuestionBodyState): string {
|
||||
export function questionHint(request: QuestionV2Request, state: QuestionBodyState): string {
|
||||
if (state.submitting) {
|
||||
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 { FSUtil } from "@opencode-ai/core/fs-util"
|
||||
import { Model } from "@opencode-ai/schema/model"
|
||||
import type { ToolPart } from "@opencode-ai/sdk/v2"
|
||||
import { open } from "node:fs/promises"
|
||||
import path from "node:path"
|
||||
import { Server } from "../services/server"
|
||||
import { loadRunAgents, waitForCatalogReady } from "./catalog.shared"
|
||||
import { runNonInteractivePrompt } from "./noninteractive"
|
||||
import { toolInlineInfo } from "./tool"
|
||||
import type { MiniToolPart } from "./types"
|
||||
import { UI } from "./ui"
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
async function renderTool(part: ToolPart) {
|
||||
async function renderTool(part: MiniToolPart) {
|
||||
const info = toolInlineInfo(part)
|
||||
if (info.mode === "block") {
|
||||
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)
|
||||
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,
|
||||
sessionID: state.sessionID,
|
||||
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.
|
||||
//
|
||||
// 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 type { SessionMessageInfo, SessionMessageUser } from "@opencode-ai/client/promise"
|
||||
import { promptCopy, promptSame } from "./prompt.shared"
|
||||
import type { Message, Part } from "@opencode-ai/sdk/v2"
|
||||
import type { RunInput, RunPrompt } from "./types"
|
||||
|
||||
const LIMIT = 200
|
||||
|
||||
export type SessionMessages = Array<{ info: Message; parts: Part[] }>
|
||||
export type SessionMessages = SessionMessageInfo[]
|
||||
|
||||
type Turn = {
|
||||
prompt: RunPrompt
|
||||
|
|
@ -25,133 +20,42 @@ export type RunSession = {
|
|||
variant?: string
|
||||
}
|
||||
|
||||
function fileName(url: string, filename?: string) {
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
function messagePrompt(message: SessionMessageUser): RunPrompt {
|
||||
return {
|
||||
type: "file" as const,
|
||||
path: part.filename ?? part.url,
|
||||
text,
|
||||
}
|
||||
}
|
||||
|
||||
export function messagePrompt(msg: SessionMessages[number]): RunPrompt {
|
||||
const parts: RunPrompt["parts"] = []
|
||||
let text = msg.parts
|
||||
.filter((part): part is Extract<SessionMessages[number]["parts"][number], { type: "text" }> => {
|
||||
return part.type === "text" && !part.synthetic
|
||||
})
|
||||
.map((part) => part.text)
|
||||
.join("")
|
||||
let cursor = Bun.stringWidth(text)
|
||||
const used: Array<{ start: number; end: number }> = []
|
||||
|
||||
const take = (value: string): { start: number; end: number; value: string } | undefined => {
|
||||
let from = 0
|
||||
while (true) {
|
||||
const idx = text.indexOf(value, from)
|
||||
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,
|
||||
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",
|
||||
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,
|
||||
})),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
export function createSession(messages: SessionMessages): RunSession {
|
||||
return {
|
||||
first: messages.length === 0,
|
||||
turns: messages.flatMap((msg) => {
|
||||
const item = turn(msg)
|
||||
return item ? [item] : []
|
||||
}),
|
||||
turns: messages.flatMap((message) =>
|
||||
message.type === "user"
|
||||
? [{ 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.session.get({ sessionID }),
|
||||
])
|
||||
const messages = response.data.toReversed()
|
||||
const current = createSession(response.data.toReversed())
|
||||
return {
|
||||
first: messages.length === 0,
|
||||
turns: messages.flatMap((message) => {
|
||||
if (message.type !== "user") return []
|
||||
return [
|
||||
{
|
||||
prompt: {
|
||||
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,
|
||||
},
|
||||
]
|
||||
}),
|
||||
...current,
|
||||
turns: current.turns.map((turn) => ({
|
||||
...turn,
|
||||
provider: session.model?.providerID,
|
||||
model: session.model?.id,
|
||||
variant: session.model?.variant,
|
||||
})),
|
||||
...(session.model && {
|
||||
model: {
|
||||
providerID: session.model.providerID,
|
||||
modelID: session.model.id,
|
||||
},
|
||||
model: { providerID: session.model.providerID, modelID: session.model.id },
|
||||
variant: session.model.variant,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
export function sessionHistory(session: RunSession, limit = LIMIT): RunPrompt[] {
|
||||
const out: RunPrompt[] = []
|
||||
|
||||
for (const turn of session.turns) {
|
||||
if (!turn.prompt.text.trim()) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (out[out.length - 1] && promptSame(out[out.length - 1], turn.prompt)) {
|
||||
continue
|
||||
}
|
||||
|
||||
out.push(promptCopy(turn.prompt))
|
||||
}
|
||||
|
||||
return out.slice(-limit)
|
||||
return session.turns
|
||||
.map((turn) => turn.prompt)
|
||||
.filter((prompt) => prompt.text.trim())
|
||||
.filter((prompt, index, prompts) => index === 0 || !promptSame(prompts[index - 1], prompt))
|
||||
.map(promptCopy)
|
||||
.slice(-limit)
|
||||
}
|
||||
|
||||
export function sessionVariant(session: RunSession, model: RunInput["model"]): string | undefined {
|
||||
if (!model) {
|
||||
return undefined
|
||||
}
|
||||
if (!model) return
|
||||
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.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
|
||||
return session.turns.findLast((turn) => turn.provider === model.providerID && turn.model === model.modelID)?.variant
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,10 +15,14 @@
|
|||
// Per-child interruption uses `v2.session.interrupt(childID)`. Per-child
|
||||
// backgrounding is intentionally absent: subagent jobs block the parent
|
||||
// session, so only whole-session `v2.session.background(parentID)` exists.
|
||||
import type { EventSubscribeOutput, OpenCodeClient } from "@opencode-ai/client/promise"
|
||||
import type { SessionMessageAssistantTool, SessionMessageInfo, ToolPart } from "@opencode-ai/sdk/v2"
|
||||
import type {
|
||||
EventSubscribeOutput,
|
||||
OpenCodeClient,
|
||||
SessionMessageAssistantTool,
|
||||
SessionMessageInfo,
|
||||
} from "@opencode-ai/client/promise"
|
||||
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_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")
|
||||
}
|
||||
|
||||
export function legacyTool(input: {
|
||||
export function miniTool(input: {
|
||||
sessionID: string
|
||||
messageID: string
|
||||
tool: SessionMessageAssistantTool
|
||||
}): ToolPart {
|
||||
}): MiniToolPart {
|
||||
const tool = input.tool
|
||||
const providerCall =
|
||||
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 text =
|
||||
status === "running"
|
||||
|
|
@ -310,7 +314,7 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
|
|||
}
|
||||
|
||||
const childTool = (child: ChildState, item: SessionMessageAssistantTool, messageID: string) => {
|
||||
const part = legacyTool({
|
||||
const part = miniTool({
|
||||
sessionID: child.sessionID,
|
||||
messageID,
|
||||
tool: item,
|
||||
|
|
|
|||
|
|
@ -1,14 +1,15 @@
|
|||
import type { EventSubscribeOutput, OpenCodeClient } from "@opencode-ai/client/promise"
|
||||
import type {
|
||||
PermissionRequest,
|
||||
QuestionRequest,
|
||||
SessionMessageInfo,
|
||||
EventSubscribeOutput,
|
||||
OpenCodeClient,
|
||||
PermissionV2Request,
|
||||
QuestionV2Request,
|
||||
SessionMessageAssistantTool,
|
||||
} from "@opencode-ai/sdk/v2"
|
||||
SessionMessageInfo,
|
||||
} from "@opencode-ai/client/promise"
|
||||
import { Event } from "@opencode-ai/schema/event"
|
||||
import { blockerStatus, pickBlockerView } from "./session-data"
|
||||
import { writeSessionOutput } from "./stream"
|
||||
import { createSubagentTracker, legacyTool, toolCommit } from "./stream-v2.subagent"
|
||||
import { createSubagentTracker, miniTool, toolCommit } from "./stream-v2.subagent"
|
||||
import type {
|
||||
FooterApi,
|
||||
FooterView,
|
||||
|
|
@ -87,8 +88,6 @@ type ShellWait = {
|
|||
}
|
||||
|
||||
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 ToolState = {
|
||||
|
|
@ -101,8 +100,8 @@ type ToolState = {
|
|||
}
|
||||
|
||||
type State = {
|
||||
permissions: PermissionRequest[]
|
||||
questions: QuestionRequest[]
|
||||
permissions: PermissionV2Request[]
|
||||
questions: QuestionV2Request[]
|
||||
view: FooterView
|
||||
messageIDs: Set<string>
|
||||
text: Map<string, string>
|
||||
|
|
@ -138,27 +137,6 @@ export function formatUnknownError(error: unknown): string {
|
|||
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) {
|
||||
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}`
|
||||
}
|
||||
|
||||
// Matches the commit shapes the legacy session-data reducer produced for direct
|
||||
// shell calls: one "start" commit rendering `$ command` and one "progress"
|
||||
// Direct shell calls use one "start" commit rendering `$ command` and one "progress"
|
||||
// commit rendering the merged output (see toolEntryBody in tool.ts).
|
||||
function shellCommit(
|
||||
callID: string,
|
||||
|
|
@ -384,7 +361,7 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
|
|||
}
|
||||
|
||||
const renderTool = (messageID: string, item: SessionMessageAssistantTool) => {
|
||||
const part = legacyTool({
|
||||
const part = miniTool({
|
||||
sessionID: input.sessionID,
|
||||
messageID,
|
||||
tool: item,
|
||||
|
|
@ -536,8 +513,8 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
|
|||
])
|
||||
const projected = structuredClone(messages.data).toReversed() as SessionMessageInfo[]
|
||||
for (const message of projected) renderMessage(message, next.render, next.reuseVisibleWait)
|
||||
state.permissions = permissions.map(permission)
|
||||
state.questions = questions.map(question)
|
||||
state.permissions = permissions
|
||||
state.questions = questions
|
||||
syncBlockers()
|
||||
await subagents.hydrate({ messages: [...projected], active })
|
||||
const running = input.sessionID in active
|
||||
|
|
@ -770,7 +747,7 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
|
|||
return
|
||||
}
|
||||
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()
|
||||
return
|
||||
}
|
||||
|
|
@ -780,7 +757,7 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
|
|||
return
|
||||
}
|
||||
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()
|
||||
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
|
||||
// footer.event() respectively, adding trace writes along the way. It also
|
||||
// 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"
|
||||
|
||||
type Trace = {
|
||||
|
|
@ -103,9 +103,9 @@ export function traceSubagentState(state: FooterSubagentState) {
|
|||
permissions: state.permissions.map((item) => ({
|
||||
id: item.id,
|
||||
sessionID: item.sessionID,
|
||||
permission: item.permission,
|
||||
patterns: item.patterns,
|
||||
tool: item.tool,
|
||||
action: item.action,
|
||||
resources: item.resources,
|
||||
source: item.source,
|
||||
metadata: 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 {
|
||||
for (const commit of out.commits) {
|
||||
input.trace?.write("ui.commit", commit)
|
||||
|
|
|
|||
|
|
@ -15,10 +15,9 @@
|
|||
import os from "os"
|
||||
import path from "path"
|
||||
import stripAnsi from "strip-ansi"
|
||||
import type { ToolPart } from "@opencode-ai/sdk/v2"
|
||||
import { LANGUAGE_EXTENSIONS } from "@opencode-ai/tui/util/filetype"
|
||||
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 = {
|
||||
output: boolean
|
||||
|
|
@ -1177,7 +1176,7 @@ function rule(name?: string): AnyToolRule | undefined {
|
|||
return TOOL_RULES[name]
|
||||
}
|
||||
|
||||
function frame(part: ToolPart): ToolFrame {
|
||||
function frame(part: MiniToolPart): ToolFrame {
|
||||
const state = dict(part.state)
|
||||
return {
|
||||
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 draw = rule(ctx.name)?.run
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -7,12 +7,16 @@
|
|||
//
|
||||
// Data flow through the system:
|
||||
//
|
||||
// SDK events → session-data reducer → StreamCommit[] + FooterOutput
|
||||
// V2 events / demo actions → StreamCommit[] + FooterOutput
|
||||
// → stream.ts bridges to footer API
|
||||
// → footer.ts queues commits and patches the footer view
|
||||
// → OpenTUI split-footer renderer writes to terminal
|
||||
import type { OpenCodeClient, ReferenceListOutput } from "@opencode-ai/client/promise"
|
||||
import type { FilePart, PermissionRequest, QuestionRequest, ToolPart } from "@opencode-ai/sdk/v2"
|
||||
import type {
|
||||
OpenCodeClient,
|
||||
PermissionV2Request,
|
||||
QuestionV2Request,
|
||||
ReferenceListOutput,
|
||||
} from "@opencode-ai/client/promise"
|
||||
import type { TuiConfig } from "@opencode-ai/tui/config/v1"
|
||||
|
||||
export type RunFilePart = {
|
||||
|
|
@ -30,7 +34,11 @@ export type RunPromptPart =
|
|||
url: string
|
||||
filename?: 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 } }
|
||||
|
||||
|
|
@ -210,6 +218,41 @@ export type 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 RunEntryBody =
|
||||
|
|
@ -220,13 +263,13 @@ export type RunEntryBody =
|
|||
| { type: "structured"; snapshot: ToolSnapshot }
|
||||
|
||||
// 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
|
||||
// "prompt".
|
||||
export type FooterView =
|
||||
| { type: "prompt" }
|
||||
| { type: "permission"; request: PermissionRequest }
|
||||
| { type: "question"; request: QuestionRequest }
|
||||
| { type: "permission"; request: PermissionV2Request }
|
||||
| { type: "question"; request: QuestionV2Request }
|
||||
|
||||
export type FooterPromptRoute =
|
||||
| { type: "composer" }
|
||||
|
|
@ -259,11 +302,11 @@ export type FooterSubagentDetail = {
|
|||
export type FooterSubagentState = {
|
||||
tabs: FooterSubagentTab[]
|
||||
details: Record<string, FooterSubagentDetail>
|
||||
permissions: PermissionRequest[]
|
||||
questions: QuestionRequest[]
|
||||
permissions: PermissionV2Request[]
|
||||
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 = {
|
||||
patch?: FooterPatch
|
||||
view?: FooterView
|
||||
|
|
@ -357,8 +400,8 @@ export type StreamSource = "assistant" | "reasoning" | "tool" | "system"
|
|||
|
||||
export type StreamToolState = "running" | "completed" | "error"
|
||||
|
||||
// A single append-only commit to scrollback. The session-data reducer produces
|
||||
// these from SDK events, and RunFooter.append() queues them for the next
|
||||
// A single append-only commit to scrollback. The transport produces these from
|
||||
// V2 events, and RunFooter.append() queues them for the next
|
||||
// microtask flush. Once flushed, they become immutable terminal scrollback
|
||||
// rows -- they cannot be rewritten.
|
||||
export type StreamCommit = {
|
||||
|
|
@ -370,7 +413,7 @@ export type StreamCommit = {
|
|||
messageID?: string
|
||||
partID?: string
|
||||
tool?: string
|
||||
part?: ToolPart
|
||||
part?: MiniToolPart
|
||||
interrupted?: boolean
|
||||
toolState?: StreamToolState
|
||||
toolError?: string
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue