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

This commit is contained in:
Simon Klee 2026-07-22 10:35:35 +02:00 committed by GitHub
commit 794137b33b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 67 additions and 159 deletions

View file

@ -11,7 +11,6 @@
"bin"
],
"exports": {
"./daemon": "./src/daemon.ts",
"./run": "./src/run/index.ts",
"./server-process": "./src/server-process.ts"
},

View file

@ -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([])
})

View file

@ -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",

View file

@ -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`,

View file

@ -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({

View file

@ -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

View file

@ -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,

View file

@ -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,

View file

@ -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: "" }
}

View file

@ -76,7 +76,7 @@ export function permissionLabel(option: PermissionOption): string {
export { permissionAlwaysLines }
export function permissionReply(
function permissionReply(
sessionID: string,
requestID: string,
reply: PermissionReply["reply"],

View file

@ -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,

View file

@ -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"

View file

@ -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) } } : {}

View file

@ -85,7 +85,7 @@ function traceCommit(commit: StreamCommit) {
}
}
export function traceSubagentState(state: FooterSubagentState) {
function traceSubagentState(state: FooterSubagentState) {
return {
tabs: state.tabs,
details: Object.fromEntries(

View file

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

View file

@ -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) {

View file

@ -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

View file

@ -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" },
)
}

View file

@ -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}

View file

@ -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}

View file

@ -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",

View file

@ -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

View file

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