Explorar o código

refactor(tui): narrow mini compatibility surfaces (#38262)

Simon Klee hai 3 semanas
pai
achega
794137b33b

+ 0 - 1
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"
   },

+ 2 - 0
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([])
   })
 

+ 1 - 1
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",

+ 16 - 42
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<string, JsonValue>
@@ -126,7 +126,6 @@ type FormRequest = {
 type Perm = {
   ref: Ref
   done: {
-    title: string
     output: string
     metadata?: Record<string, JsonValue>
   }
@@ -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<string, JsonValue>): 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<string, JsonValue>): Ref
 }
 
 function startTool(state: State, ref: Ref, structured: Record<string, JsonValue> = {}): 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<string, JsonValue>
   },
 ): 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<void> {
   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<boolean> => {
     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`,

+ 1 - 6
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<QueuedEntry[]>(() =>
+  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({

+ 5 - 10
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<RunPromptPart, { type: "file" | "agent" }>
@@ -65,7 +65,6 @@ type PromptInput = {
   agents: Accessor<RunAgent[]>
   references: Accessor<RunReference[]>
   commands: Accessor<RunCommand[] | undefined>
-  tuiConfig: RunTuiConfig
   state: Accessor<FooterState>
   view: Accessor<string>
   prompt: Accessor<boolean>
@@ -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

+ 0 - 2
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,

+ 0 - 3
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,

+ 2 - 2
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: "" }
 }
 

+ 1 - 1
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"],

+ 0 - 1
packages/tui/src/mini/runtime.lifecycle.ts

@@ -232,7 +232,6 @@ export async function createRuntimeLifecycle(input: LifecycleInput): Promise<Lif
     findFiles: input.findFiles,
     agents: input.agents,
     references: input.references,
-    sessionID: input.getSessionID ?? (() => input.sessionID),
     ...labels,
     model: input.model,
     variant: input.variant,

+ 1 - 1
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"

+ 2 - 3
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<string, FooterSubagentDetail> =
         child && !child.detailStale ? { [child.sessionID]: { commits: child.frames.map((item) => item.commit) } } : {}

+ 1 - 1
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(

+ 2 - 0
packages/tui/src/mini/tool.public.ts

@@ -0,0 +1,2 @@
+export { toolInlineInfo, toolOutputText } from "./tool"
+export type { MiniToolPart } from "./types"

+ 11 - 17
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<string, unknown>
+type ToolDict = Record<string, unknown>
 
 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) {

+ 11 - 20
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<string, unknown>; raw?: string }
-  | {
-      status: "running"
-      input: Record<string, unknown>
-      title?: string
-      metadata?: Record<string, unknown>
-      time: { start: number }
-    }
+type MiniToolState =
   | {
       status: "completed"
       input: Record<string, unknown>
@@ -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<Config.Resolved, "keybinds" | "leader" | "theme" | "session" | "mini">
+export type RunTuiConfig = Pick<Config.Resolved, "keybinds" | "leader" | "theme" | "mini">
 
 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

+ 0 - 19
packages/tui/test/mini/fixture/tui-runtime.ts

@@ -1,19 +0,0 @@
-import { resolve, type Info, type Resolved } from "../../../src/config"
-import { TuiKeybind } from "../../../src/config/keybind"
-
-type ResolvedInput = Omit<Info, "attention" | "keybinds" | "leader"> & {
-  attention?: Partial<Resolved["attention"]>
-  keybinds?: Partial<TuiKeybind.Keybinds>
-  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" },
-  )
-}

+ 0 - 2
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}

+ 7 - 20
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}

+ 3 - 5
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",

+ 0 - 1
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<T>() {
   let resolve!: (value: T | PromiseLike<T>) => void

+ 1 - 1
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",
               },
             ],
           },