diff --git a/packages/cli/package.json b/packages/cli/package.json index 7ace585e6a..acf8ec75dd 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -11,7 +11,6 @@ "bin" ], "exports": { - "./daemon": "./src/daemon.ts", "./run": "./src/run/index.ts", "./server-process": "./src/server-process.ts" }, diff --git a/packages/cli/test/import-boundaries.test.ts b/packages/cli/test/import-boundaries.test.ts index f0eef3cbd8..8b189ce2c0 100644 --- a/packages/cli/test/import-boundaries.test.ts +++ b/packages/cli/test/import-boundaries.test.ts @@ -18,10 +18,12 @@ describe("CLI frontend import boundaries", () => { test("exposes only the intentional package entrypoints", async () => { const run = await import("@opencode-ai/cli/run") const mini = await import("@opencode-ai/tui/mini") + const tool = await import("@opencode-ai/tui/mini/tool") const cli = await Bun.file(path.join(root, "packages/cli/package.json")).json() expect(Object.keys(run).sort()).toEqual(["runNonInteractive", "runV1Bridge"]) expect(Object.keys(mini).sort()).toEqual(["runMiniFrontend"]) + expect(Object.keys(tool).sort()).toEqual(["toolInlineInfo", "toolOutputText"]) expect(Object.keys(cli.exports).filter((key) => key === "./mini" || key.startsWith("./mini/"))).toEqual([]) }) diff --git a/packages/tui/package.json b/packages/tui/package.json index 8ced93ff1f..0fb182f8d3 100644 --- a/packages/tui/package.json +++ b/packages/tui/package.json @@ -30,7 +30,7 @@ "./editor": "./src/editor.ts", "./editor-zed": "./src/editor-zed.ts", "./mini": "./src/mini/index.ts", - "./mini/tool": "./src/mini/tool.ts", + "./mini/tool": "./src/mini/tool.public.ts", "./model-preference": "./src/model-preference.ts", "./runtime": "./src/runtime.tsx", "./terminal-win32": "./src/terminal-win32.ts", diff --git a/packages/tui/src/mini/demo.ts b/packages/tui/src/mini/demo.ts index e4c11c9813..a16ae991ac 100644 --- a/packages/tui/src/mini/demo.ts +++ b/packages/tui/src/mini/demo.ts @@ -16,8 +16,9 @@ // the synthetic tool parts through the same callbacks used by the live footer. import path from "path" import type { JsonValue, SessionMessageAssistantTool } from "@opencode-ai/client/promise" +import { parseSlashHead } from "../prompt/parse" import { writeSessionOutput } from "./stream" -import { toolCommit } from "./stream-v2.subagent" +import { toolCommit, toolFinalPhase } from "./stream-v2.subagent" import type { FooterApi, FooterView, @@ -110,7 +111,6 @@ const SAMPLE_TABLE = [ type Ref = { msg: string - part: string call: string tool: string input: Record @@ -126,7 +126,6 @@ type FormRequest = { type Perm = { ref: Ref done: { - title: string output: string metadata?: Record } @@ -203,7 +202,6 @@ function showSubagent( description: input.description, status: input.status, title: input.title, - lastUpdatedAt: Date.now(), }, ], details: { @@ -337,7 +335,6 @@ async function emitReasoning(state: State, body: string, signal?: AbortSignal): function make(state: State, tool: string, input: Record): Ref { return { msg: open(state), - part: take(state, "part", "part"), call: take(state, "call", "call"), tool, input, @@ -346,7 +343,7 @@ function make(state: State, tool: string, input: Record): Ref } function startTool(state: State, ref: Ref, structured: Record = {}): SessionMessageAssistantTool { - state.started.add(ref.part) + state.started.add(ref.call) const part = { type: "tool" as const, id: ref.call, @@ -386,12 +383,11 @@ function doneTool( state: State, ref: Ref, output: { - title: string output: string metadata?: Record }, ): void { - if (!state.started.has(ref.part)) startTool(state, ref) + if (!state.started.has(ref.call)) startTool(state, ref) const part: SessionMessageAssistantTool = { type: "tool", id: ref.call, @@ -404,11 +400,11 @@ function doneTool( }, time: { created: ref.start, ran: ref.start, completed: Date.now() }, } - present(state, [toolCommit(part, ref.msg, output.output ? "progress" : "final")]) + present(state, [toolCommit(part, ref.msg, toolFinalPhase(part))]) } function failTool(state: State, ref: Ref, error: string): void { - if (!state.started.has(ref.part)) startTool(state, ref) + if (!state.started.has(ref.call)) startTool(state, ref) present(state, [ toolCommit( { @@ -443,7 +439,6 @@ async function emitBash(state: State, signal?: AbortSignal): Promise { startTool(state, ref) await wait(70, signal) doneTool(state, ref, { - title: "git status", output: `${process.cwd()}\ngit status\nOn branch demo\nnothing to commit, working tree clean\n`, metadata: { exit: 0, @@ -458,7 +453,6 @@ function emitWrite(state: State): void { content: "export const demo = 42\n", }) doneTool(state, ref, { - title: "write", output: "", metadata: {}, }) @@ -470,7 +464,6 @@ function emitEdit(state: State): void { path: file, }) doneTool(state, ref, { - title: "edit", output: "", metadata: { files: [ @@ -490,7 +483,6 @@ function emitPatch(state: State): void { patchText: "*** Begin Patch\n*** End Patch", }) doneTool(state, ref, { - title: "patch", output: "", metadata: { files: [ @@ -517,10 +509,9 @@ function emitTask(state: State): void { agent: "explore", }) doneTool(state, ref, { - title: "Reducer touchpoints found", output: "", metadata: { - sessionID: "sub_demo_1", + sessionID: "ses_demo_child", status: "completed", output: "", }, @@ -542,7 +533,7 @@ function emitTask(state: State): void { time: { created: Date.now(), ran: Date.now() }, } satisfies SessionMessageAssistantTool showSubagent(state, { - sessionID: "sub_demo_1", + sessionID: "ses_demo_child", label: "Explore", description: "Scan run/* for reducer touchpoints", status: "completed", @@ -562,16 +553,7 @@ function emitTask(state: State): void { messageID: "sub_demo_msg_reasoning", partID: "sub_demo_reasoning_1", }, - { - kind: "tool", - text: "running read", - phase: "start", - source: "tool", - messageID: "sub_demo_msg_tool", - partID: "sub_demo_tool_1", - tool: "read", - part, - }, + toolCommit(part, "sub_demo_msg_tool", "start"), { kind: "assistant", text: "Footer updates flow through stream.ts into RunFooter", @@ -610,7 +592,6 @@ function emitQuestionTool(state: State): void { ], }) doneTool(state, ref, { - title: "question", output: "", metadata: { answers: [["Diff"], ["Usage", "custom-note"]], @@ -635,7 +616,6 @@ function emitPermission(state: State, kind: PermissionKind = "edit"): void { patterns: [command], always: ["*"], done: { - title: "git status --short", output: `${root}\ngit status --short\n M src/demo-format.ts\n?? src/demo-permission.ts\n`, metadata: { exit: 0, @@ -658,7 +638,6 @@ function emitPermission(state: State, kind: PermissionKind = "edit"): void { patterns: [target], always: [target], done: { - title: "read", output: ["1: {", '2: "name": "opencode",', '3: "private": true', "4: }"].join("\n"), metadata: {}, }, @@ -677,7 +656,6 @@ function emitPermission(state: State, kind: PermissionKind = "edit"): void { patterns: ["explore"], always: ["*"], done: { - title: "Footer spacing checked", output: "", metadata: { sessionID: "sub_demo_perm_1", @@ -707,7 +685,6 @@ function emitPermission(state: State, kind: PermissionKind = "edit"): void { }, always: [`${dir}/**`], done: { - title: "read", output: `1: # External demo\n2: Shared preview file\nPath: ${target}`, metadata: {}, }, @@ -726,7 +703,6 @@ function emitPermission(state: State, kind: PermissionKind = "edit"): void { patterns: ["*"], always: ["*"], done: { - title: "Retry allowed", output: "Continuing after repeated failures.\n", metadata: {}, }, @@ -744,7 +720,6 @@ function emitPermission(state: State, kind: PermissionKind = "edit"): void { patterns: [file], always: [file], done: { - title: "edit", output: "", metadata: { files: [{ file, status: "modified", patch: diff }], @@ -955,8 +930,9 @@ export function createRunDemo(input: Input) { const prompt = async (line: RunPrompt, signal?: AbortSignal): Promise => { const text = line.text.trim() - const list = text.split(/\s+/) - const cmd = list[0] || "" + const head = parseSlashHead(text) + const list = head?.arguments.split(/\s+/).filter(Boolean) ?? [] + const cmd = head ? `/${head.name}` : "" clearSubagent(state.footer) @@ -966,7 +942,7 @@ export function createRunDemo(input: Input) { } if (cmd === "/permission") { - const kind = permissionKind(list[1]) + const kind = permissionKind(list[0]) if (!kind) { note(state.footer, `Pick a permission kind: ${PERMISSIONS.join(", ")}`) return true @@ -977,7 +953,7 @@ export function createRunDemo(input: Input) { } if (cmd === "/form") { - const kind = formKind(list[1]) + const kind = formKind(list[0]) if (!kind) { note(state.footer, `Pick a form kind: ${FORMS.join(", ")}`) return true @@ -988,8 +964,8 @@ export function createRunDemo(input: Input) { } if (cmd === "/fmt") { - const kind = (list[1] || "").toLowerCase() - const body = list.slice(2).join(" ") + const kind = (list[0] || "").toLowerCase() + const body = list.slice(1).join(" ") if (!kind) { note(state.footer, `Pick a kind: ${KINDS.join(", ")}`) return true @@ -1032,7 +1008,6 @@ export function createRunDemo(input: Input) { clearBlocker(state) if (form.kind === "question") { doneTool(state, form.ref, { - title: "question", output: "", metadata: { answers: form.request.fields.map((field) => { @@ -1045,7 +1020,6 @@ export function createRunDemo(input: Input) { return true } doneTool(state, form.ref, { - title: form.request.title, output: `Form submitted: ${Object.entries(input.answer) .map(([key, value]) => `${key}=${Array.isArray(value) ? value.join(", ") : String(value)}`) .join("; ")}\n`, diff --git a/packages/tui/src/mini/footer.command.tsx b/packages/tui/src/mini/footer.command.tsx index f2edfcbbf0..4dff4f8112 100644 --- a/packages/tui/src/mini/footer.command.tsx +++ b/packages/tui/src/mini/footer.command.tsx @@ -54,10 +54,6 @@ type SubagentEntry = PanelEntry & { current: boolean } -type QueuedEntry = PanelEntry & { - prompt: FooterQueuedPrompt -} - type SettingEntry = PanelEntry & { key: keyof MiniSettings } @@ -747,13 +743,12 @@ export function RunQueuedPromptSelectBody(props: { onRows?: (rows: number) => void mono?: boolean }) { - const entries = createMemo(() => + const entries = createMemo(() => props.prompts().map((prompt) => ({ category: "", display: prompt.prompt.text.replaceAll("\n", " "), footer: prompt.delivery, keywords: prompt.prompt.text, - prompt, })), ) const controller = createSearchablePanelController({ diff --git a/packages/tui/src/mini/footer.prompt.tsx b/packages/tui/src/mini/footer.prompt.tsx index 544d76007d..8b8acf9848 100644 --- a/packages/tui/src/mini/footer.prompt.tsx +++ b/packages/tui/src/mini/footer.prompt.tsx @@ -31,13 +31,13 @@ import { realignEditorPromptParts, resolveEditorSlashValue } from "./prompt.edit import { monoTruncateMiddle } from "./mono" import { FOOTER_MENU_ROWS, createFooterMenuState, type RunFooterMenuItem } from "./footer.menu" import type { RunFooterTheme } from "./theme" -import type { FooterState, RunAgent, RunCommand, RunPrompt, RunPromptPart, RunReference, RunTuiConfig } from "./types" +import type { FooterState, RunAgent, RunCommand, RunPrompt, RunPromptPart, RunReference } from "./types" const AUTOCOMPLETE_ROWS = FOOTER_MENU_ROWS const AUTOCOMPLETE_BOTTOM_ROWS = 1 export const TEXTAREA_MIN_ROWS = 1 -export const TEXTAREA_MAX_ROWS = 6 +const TEXTAREA_MAX_ROWS = 6 export const PROMPT_MAX_ROWS = TEXTAREA_MAX_ROWS + AUTOCOMPLETE_ROWS - 1 + AUTOCOMPLETE_BOTTOM_ROWS type Mention = Extract @@ -65,7 +65,6 @@ type PromptInput = { agents: Accessor references: Accessor commands: Accessor - tuiConfig: RunTuiConfig state: Accessor view: Accessor prompt: Accessor @@ -142,7 +141,7 @@ function parseSlashCommand(text: string, commands: RunCommand[] | undefined) { } } -export function selectedCommand(text: string, command: RunPrompt["command"], commands?: RunCommand[]) { +export function selectedCommand(text: string, command: RunPrompt["command"]) { if (!command) { return } @@ -152,14 +151,10 @@ export function selectedCommand(text: string, command: RunPrompt["command"], com return } - // Bound drafts (e.g. the skill picker) may predate or omit the catalog - // source; resolve it at submit time so routing never degrades to a plain - // command for a skill entry. - const source = command.source ?? commands?.find((item) => item.name === command.name)?.source return { name: command.name, arguments: head.arguments, - ...(source ? { source } : {}), + ...(command.source ? { source: command.source } : {}), } } @@ -1140,7 +1135,7 @@ export function createPromptState(input: PromptInput): PromptState { return } - const command = next.mode === "shell" ? undefined : selectedCommand(next.text, next.command, input.commands()) + const command = next.mode === "shell" ? undefined : selectedCommand(next.text, next.command) if (!command && next.mode !== "shell" && isExitCommand(next.text)) { input.onExit() return diff --git a/packages/tui/src/mini/footer.ts b/packages/tui/src/mini/footer.ts index b21c249612..2673cc5e6f 100644 --- a/packages/tui/src/mini/footer.ts +++ b/packages/tui/src/mini/footer.ts @@ -74,7 +74,6 @@ type RunFooterOptions = { agents: RunAgent[] references: RunReference[] wrote?: boolean - sessionID: () => string | undefined agentLabel: string modelLabel: string model: RunInput["model"] @@ -312,7 +311,6 @@ export class RunFooter implements FooterApi { currentVariant: footer.currentVariant, theme: footer.theme, mono: options.mono, - tuiConfig: options.tuiConfig, miniSettings: footer.miniSettings, history: footer.history, onSubmit: footer.handlePrompt, diff --git a/packages/tui/src/mini/footer.view.tsx b/packages/tui/src/mini/footer.view.tsx index 72e4cf68ca..dfc326f4b4 100644 --- a/packages/tui/src/mini/footer.view.tsx +++ b/packages/tui/src/mini/footer.view.tsx @@ -50,7 +50,6 @@ import type { RunPrompt, RunProvider, RunReference, - RunTuiConfig, } from "./types" import type { RunTheme } from "./theme" @@ -88,7 +87,6 @@ type RunFooterViewProps = { queuedPrompts?: () => FooterQueuedPrompt[] theme: () => RunTheme mono: boolean - tuiConfig: RunTuiConfig miniSettings: () => MiniSettings history?: () => RunPrompt[] onSubmit: (input: RunPrompt) => boolean @@ -359,7 +357,6 @@ export function RunFooterView(props: RunFooterViewProps) { agents: props.agents, references: props.references, commands: props.commands, - tuiConfig: props.tuiConfig, state: props.state, view: promptView, prompt, diff --git a/packages/tui/src/mini/form.shared.ts b/packages/tui/src/mini/form.shared.ts index 7fdcd7fe51..e571dabbdc 100644 --- a/packages/tui/src/mini/form.shared.ts +++ b/packages/tui/src/mini/form.shared.ts @@ -14,7 +14,7 @@ import { import type { FormAnswerField } from "../util/form" import type { FormReply, MiniFormRequest } from "./types" -export { formCustom, formLabel, formRows, formSelected, formTextual, formValidateValue } +export { formCustom, formLabel, formRows, formTextual, formValidateValue } export type FormBodyState = { formID: string @@ -97,7 +97,7 @@ export function formSetSelected(state: FormBodyState, selected: number): FormBod return { ...state, selected, error: "" } } -export function formSetEditing(state: FormBodyState, editing: boolean): FormBodyState { +function formSetEditing(state: FormBodyState, editing: boolean): FormBodyState { return { ...state, editing, error: "" } } diff --git a/packages/tui/src/mini/permission.shared.ts b/packages/tui/src/mini/permission.shared.ts index fa4610efcb..e293d3ef17 100644 --- a/packages/tui/src/mini/permission.shared.ts +++ b/packages/tui/src/mini/permission.shared.ts @@ -76,7 +76,7 @@ export function permissionLabel(option: PermissionOption): string { export { permissionAlwaysLines } -export function permissionReply( +function permissionReply( sessionID: string, requestID: string, reply: PermissionReply["reply"], diff --git a/packages/tui/src/mini/runtime.lifecycle.ts b/packages/tui/src/mini/runtime.lifecycle.ts index 5354946d91..9c43d7fd6f 100644 --- a/packages/tui/src/mini/runtime.lifecycle.ts +++ b/packages/tui/src/mini/runtime.lifecycle.ts @@ -232,7 +232,6 @@ export async function createRuntimeLifecycle(input: LifecycleInput): Promise input.sessionID), ...labels, model: input.model, variant: input.variant, diff --git a/packages/tui/src/mini/scrollback.writer.tsx b/packages/tui/src/mini/scrollback.writer.tsx index 436397248a..6b141e28e8 100644 --- a/packages/tui/src/mini/scrollback.writer.tsx +++ b/packages/tui/src/mini/scrollback.writer.tsx @@ -30,7 +30,7 @@ export function sameEntryGroup(left: StreamCommit | undefined, right: StreamComm return Boolean(current && next && current === next) } -export function entryLayout(commit: StreamCommit, body: RunEntryBody = entryBody(commit)): EntryLayout { +function entryLayout(commit: StreamCommit, body: RunEntryBody = entryBody(commit)): EntryLayout { if (commit.kind === "tool") { if (body.type === "structured" || body.type === "markdown") { return "block" diff --git a/packages/tui/src/mini/stream-v2.subagent.ts b/packages/tui/src/mini/stream-v2.subagent.ts index 164aa9a179..7ae8865cad 100644 --- a/packages/tui/src/mini/stream-v2.subagent.ts +++ b/packages/tui/src/mini/stream-v2.subagent.ts @@ -194,7 +194,6 @@ function tab(child: ChildState): FooterSubagentTab { status: child.status, background: child.background ? true : undefined, title: child.title, - lastUpdatedAt: child.lastUpdatedAt, } } @@ -1084,11 +1083,11 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac input.emit() }, snapshot() { - const tabs = [...children.values()].map(tab).toSorted((a, b) => { + const tabs = [...children.values()].toSorted((a, b) => { const active = Number(b.status === "running") - Number(a.status === "running") if (active !== 0) return active return b.lastUpdatedAt - a.lastUpdatedAt - }) + }).map(tab) const child = selected ? children.get(selected) : undefined const details: Record = child && !child.detailStale ? { [child.sessionID]: { commits: child.frames.map((item) => item.commit) } } : {} diff --git a/packages/tui/src/mini/stream.ts b/packages/tui/src/mini/stream.ts index c22131faaa..b67962765c 100644 --- a/packages/tui/src/mini/stream.ts +++ b/packages/tui/src/mini/stream.ts @@ -85,7 +85,7 @@ function traceCommit(commit: StreamCommit) { } } -export function traceSubagentState(state: FooterSubagentState) { +function traceSubagentState(state: FooterSubagentState) { return { tabs: state.tabs, details: Object.fromEntries( diff --git a/packages/tui/src/mini/tool.public.ts b/packages/tui/src/mini/tool.public.ts new file mode 100644 index 0000000000..8202308974 --- /dev/null +++ b/packages/tui/src/mini/tool.public.ts @@ -0,0 +1,2 @@ +export { toolInlineInfo, toolOutputText } from "./tool" +export type { MiniToolPart } from "./types" diff --git a/packages/tui/src/mini/tool.ts b/packages/tui/src/mini/tool.ts index a51d0d79ae..09d0ff471f 100644 --- a/packages/tui/src/mini/tool.ts +++ b/packages/tui/src/mini/tool.ts @@ -27,18 +27,17 @@ import { import { formatPath } from "../util/path-format" import type { RunEntryBody, StreamCommit, ToolSnapshot } from "./types" -export type { MiniToolPart } from "./types" export { canonicalToolName } from "../util/tool-display" -export type ToolView = { +type ToolView = { output: boolean final: boolean snap?: "code" | "diff" | "structured" } -export type ToolPhase = "start" | "progress" | "final" +type ToolPhase = "start" | "progress" | "final" -export type ToolDict = Record +type ToolDict = Record type PatchFile = { status?: string @@ -78,7 +77,7 @@ type ToolMetadata = ToolDict & { exit?: number } -export type ToolFrame = { +type ToolFrame = { directory?: string raw: string name: string @@ -94,7 +93,7 @@ export type ToolFrame = { } } -export type ToolInline = { +type ToolInline = { icon: string title: string description?: string @@ -102,7 +101,7 @@ export type ToolInline = { body?: string } -export type ToolProps = { +type ToolProps = { input: ToolInput metadata: ToolMetadata frame: ToolFrame @@ -166,7 +165,7 @@ export function toolOutputText(name: string, content: ReadonlyArray<{ type: stri function normalizeInput(name: string, value: unknown) { const input = dict(value) - const path = typeof input.path === "string" ? input.path : text(input.filePath) || text(input.filepath) + const path = typeof input.path === "string" ? input.path : text(input.filePath) const agent = typeof input.agent === "string" ? input.agent : text(input.subagent_type) return { ...input, @@ -191,7 +190,7 @@ function normalizeFile(value: unknown): PatchFile | undefined { : legacy === "move" ? "moved" : legacy) - const patch = typeof file.patch === "string" ? file.patch : text(file.diff) || undefined + const patch = typeof file.patch === "string" ? file.patch : undefined const deletions = finiteNumber(file.deletions) return { ...file, @@ -214,11 +213,6 @@ function normalizeStructured(name: string, value: unknown) { ...structured, ...(["edit", "patch"].includes(name) && Array.isArray(structured.files) ? { files } : {}), ...(name === "subagent" && sessionID ? { sessionID } : {}), - ...(name === "shell" && - finiteNumber(structured.exit) === undefined && - finiteNumber(structured.exitCode) !== undefined - ? { exit: finiteNumber(structured.exitCode) } - : {}), } } @@ -674,7 +668,7 @@ function scrollShellFinal(p: ToolProps): string { return fail(p.frame) } - const code = p.metadata.exit ?? finiteNumber(p.frame.meta.exitCode) ?? finiteNumber(p.frame.meta.exit_code) + const code = p.metadata.exit const time = span(p.frame) if (code === undefined) { if (!time) { @@ -1113,7 +1107,7 @@ function frame(part: SessionMessageAssistantTool, directory?: string): ToolFrame } } -export function toolFrame(commit: StreamCommit, raw: string): ToolFrame { +function toolFrame(commit: StreamCommit, raw: string): ToolFrame { const current = commit.part ? frame(commit.part, commit.directory) : undefined return { directory: commit.directory, @@ -1198,7 +1192,7 @@ export function toolScroll(phase: ToolPhase, ctx: ToolFrame): string { return fallbackFinal(ctx) } -export function toolSnapshot(commit: StreamCommit, raw: string): ToolSnapshot | undefined { +function toolSnapshot(commit: StreamCommit, raw: string): ToolSnapshot | undefined { const ctx = toolFrame(commit, raw) const draw = rule(ctx.name)?.snap if (!draw) { diff --git a/packages/tui/src/mini/types.ts b/packages/tui/src/mini/types.ts index 232f74c1e5..3d7920f93e 100644 --- a/packages/tui/src/mini/types.ts +++ b/packages/tui/src/mini/types.ts @@ -53,7 +53,7 @@ export type RunCommand = { source?: string } -export type RunProviderModel = { +type RunProviderModel = { name?: string cost?: { input: number @@ -159,7 +159,7 @@ export type MiniHost = { export type EntryKind = "system" | "user" | "assistant" | "reasoning" | "tool" | "error" // Whether the assistant is actively processing a turn. -export type FooterPhase = "idle" | "running" +type FooterPhase = "idle" | "running" // Full snapshot of footer status bar state. Every update replaces the whole // object in the SolidJS signal so the view re-renders atomically. @@ -188,14 +188,14 @@ export type ScrollbackOptions = { mono?: boolean } -export type ToolCodeSnapshot = { +type ToolCodeSnapshot = { kind: "code" title: string content: string file?: string } -export type ToolDiffSnapshot = { +type ToolDiffSnapshot = { kind: "diff" items: Array<{ title: string @@ -205,14 +205,14 @@ export type ToolDiffSnapshot = { }> } -export type ToolTaskSnapshot = { +type ToolTaskSnapshot = { kind: "task" title: string rows: string[] tail: string } -export type ToolQuestionSnapshot = { +type ToolQuestionSnapshot = { kind: "question" items: Array<{ question: string @@ -223,15 +223,7 @@ export type ToolQuestionSnapshot = { export type ToolSnapshot = ToolCodeSnapshot | ToolDiffSnapshot | ToolTaskSnapshot | ToolQuestionSnapshot -export type MiniToolState = - | { status: "pending"; input: Record; raw?: string } - | { - status: "running" - input: Record - title?: string - metadata?: Record - time: { start: number } - } +type MiniToolState = | { status: "completed" input: Record @@ -304,7 +296,6 @@ export type FooterSubagentTab = { status: "running" | "completed" | "cancelled" | "error" background?: boolean title?: string - lastUpdatedAt: number } export type FooterSubagentDetail = { @@ -392,7 +383,7 @@ export type FormCancel = { location?: LocationRef } -export type RunTuiConfig = Pick +export type RunTuiConfig = Pick export type MiniSettings = { thinking: "show" | "hide" @@ -408,11 +399,11 @@ export type MiniSettingChange = { // Lifecycle phase of a scrollback entry. "start" opens the entry, "progress" // appends content (coalesced in the footer queue), "final" closes it. -export type StreamPhase = "start" | "progress" | "final" +type StreamPhase = "start" | "progress" | "final" -export type StreamSource = "assistant" | "reasoning" | "tool" | "system" +type StreamSource = "assistant" | "reasoning" | "tool" | "system" -export type StreamToolState = "running" | "completed" | "error" +type StreamToolState = "running" | "completed" | "error" // A single append-only commit to scrollback. The transport produces these from // V2 events, and RunFooter.append() queues them for the next diff --git a/packages/tui/test/mini/fixture/tui-runtime.ts b/packages/tui/test/mini/fixture/tui-runtime.ts deleted file mode 100644 index 0334ea5abe..0000000000 --- a/packages/tui/test/mini/fixture/tui-runtime.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { resolve, type Info, type Resolved } from "../../../src/config" -import { TuiKeybind } from "../../../src/config/keybind" - -type ResolvedInput = Omit & { - attention?: Partial - keybinds?: Partial - leader_timeout?: number -} - -export function createTuiResolvedConfig(input: ResolvedInput = {}) { - const { leader_timeout, ...current } = input - return resolve( - { - ...current, - leader: leader_timeout === undefined ? undefined : { timeout: leader_timeout }, - }, - { terminalSuspend: process.platform !== "win32" }, - ) -} diff --git a/packages/tui/test/mini/footer-keymap.test.tsx b/packages/tui/test/mini/footer-keymap.test.tsx index 15ddbaacbf..da2513dba4 100644 --- a/packages/tui/test/mini/footer-keymap.test.tsx +++ b/packages/tui/test/mini/footer-keymap.test.tsx @@ -26,7 +26,6 @@ test("down opens subagents from an empty prompt", async () => { label: "Explore", description: "Inspect the keymap", status: "running", - lastUpdatedAt: 1, }, ], details: {}, @@ -54,7 +53,6 @@ test("down opens subagents from an empty prompt", async () => { view={view} subagent={subagents} theme={() => RUN_THEME_FALLBACK} - tuiConfig={config} miniSettings={() => ({ thinking: "hide", shell_output: "hide", turn_summary: "show", footer: "show", mono: false })} mono={false} onSubmit={() => true} diff --git a/packages/tui/test/mini/footer.view.test.tsx b/packages/tui/test/mini/footer.view.test.tsx index 93f4c1b031..38eabf85ae 100644 --- a/packages/tui/test/mini/footer.view.test.tsx +++ b/packages/tui/test/mini/footer.view.test.tsx @@ -35,7 +35,7 @@ import type { } from "../../src/mini/types" import { selectedCommand } from "../../src/mini/footer.prompt" import { RejectField } from "../../src/mini/footer.permission" -import { createTuiResolvedConfig } from "./fixture/tui-runtime" +import { createTuiResolvedConfig } from "../fixture/tui-runtime" const tuiConfig = createTuiResolvedConfig() @@ -87,7 +87,6 @@ function subagent(input: { label: input.label, description: input.description, status: input.status ?? "running", - lastUpdatedAt: 1, } satisfies FooterSubagentTab } @@ -152,7 +151,6 @@ async function renderFooter( subagent={subagents} theme={input.theme ?? (() => RUN_THEME_FALLBACK)} mono={input.mono ?? false} - tuiConfig={config} miniSettings={miniSettings} onSubmit={input.onSubmit ?? (() => true)} onPermissionReply={() => {}} @@ -996,27 +994,17 @@ test("direct footer closes settings with ctrl-c instead of arming exit", async ( } }) -test("selectedCommand backfills the catalog source for bound drafts", () => { - const catalog = [command({ name: "opencode-ts", description: "TS skill", source: "skill" })] - - // The skill picker binds `/name ` drafts; older drafts may lack source. - expect(selectedCommand("/opencode-ts fix it", { name: "opencode-ts", arguments: "" }, catalog)).toEqual({ - name: "opencode-ts", - arguments: "fix it", - source: "skill", - }) - // An explicit source wins without a catalog lookup. +test("selectedCommand validates the bound command and refreshes its arguments", () => { expect(selectedCommand("/opencode-ts", { name: "opencode-ts", arguments: "", source: "skill" })).toEqual({ name: "opencode-ts", arguments: "", source: "skill", }) - // Plain commands stay untagged. - expect( - selectedCommand("/deploy prod", { name: "deploy", arguments: "" }, [ - command({ name: "deploy", description: "Deploy" }), - ]), - ).toEqual({ name: "deploy", arguments: "prod" }) + expect(selectedCommand("/deploy prod", { name: "deploy", arguments: "" })).toEqual({ + name: "deploy", + arguments: "prod", + }) + expect(selectedCommand("/other", { name: "deploy", arguments: "" })).toBeUndefined() }) test("direct footer tags skill slash submissions with their catalog source", async () => { @@ -1162,7 +1150,6 @@ test("direct footer shows authoritative pending work while running", async () => }, ]} theme={() => RUN_THEME_FALLBACK} - tuiConfig={tuiConfig} miniSettings={() => ({ thinking: "hide", shell_output: "hide", turn_summary: "show", footer: "show", mono: false })} mono={false} onSubmit={() => true} diff --git a/packages/tui/test/mini/runtime.boot.test.ts b/packages/tui/test/mini/runtime.boot.test.ts index 8d85cfa1f0..431b5bdc01 100644 --- a/packages/tui/test/mini/runtime.boot.test.ts +++ b/packages/tui/test/mini/runtime.boot.test.ts @@ -3,7 +3,7 @@ import { OpenCode } from "@opencode-ai/client/promise" import type { Resolved } from "../../src/config" import { resolveMiniSettings, resolveModelInfo, resolveRunTuiConfig } from "../../src/mini/runtime.boot" import { catalogModel, catalogProvider } from "./fixture/catalog" -import { createTuiResolvedConfig } from "./fixture/tui-runtime" +import { createTuiResolvedConfig } from "../fixture/tui-runtime" function config(input?: { leader?: string @@ -21,7 +21,7 @@ function config(input?: { }): Resolved { const bind = input?.bindings return createTuiResolvedConfig({ - leader_timeout: input?.leaderTimeout, + leader: input?.leaderTimeout === undefined ? undefined : { timeout: input.leaderTimeout }, keybinds: { ...(input?.leader && { leader: input.leader }), ...(bind?.commandList && { command_list: bind.commandList }), @@ -95,14 +95,12 @@ describe("run runtime boot", () => { const result = await resolveRunTuiConfig( createTuiResolvedConfig({ theme: { mode: "light" }, - leader_timeout: 450, - session: { thinking: "show" }, + leader: { timeout: 450 }, }), ) expect(result.theme).toEqual({ mode: "light" }) expect(result.leader.timeout).toBe(450) - expect(result.session?.thinking).toBe("show") expect(resolveMiniSettings(result)).toEqual({ thinking: "hide", shell_output: "hide", diff --git a/packages/tui/test/mini/runtime.test.ts b/packages/tui/test/mini/runtime.test.ts index 26acbb64d3..6aff69274d 100644 --- a/packages/tui/test/mini/runtime.test.ts +++ b/packages/tui/test/mini/runtime.test.ts @@ -5,7 +5,6 @@ import type { LifecycleInput } from "../../src/mini/runtime.lifecycle" import type { FooterEvent, MiniHost } from "../../src/mini/types" import { catalogModel, catalogProvider, stubCatalogLists } from "./fixture/catalog" import { createFooterApiFixture } from "./fixture/footer-api" -import { createTuiResolvedConfig } from "./fixture/tui-runtime" function defer() { let resolve!: (value: T | PromiseLike) => void diff --git a/packages/tui/test/mini/tool.test.ts b/packages/tui/test/mini/tool.test.ts index 4ebd16c816..86c40d1492 100644 --- a/packages/tui/test/mini/tool.test.ts +++ b/packages/tui/test/mini/tool.test.ts @@ -33,7 +33,7 @@ describe("Mini tool presentation", () => { type: "update", filePath: "/tmp/project/src/a.ts", relativePath: "src/a.ts", - diff: "@@ -1 +1 @@\n-old\n+new", + patch: "@@ -1 +1 @@\n-old\n+new", }, ], },