refactor(tui): narrow mini compatibility surfaces (#38262)
This commit is contained in:
parent
0e08b7330f
commit
794137b33b
23 changed files with 67 additions and 159 deletions
|
|
@ -11,7 +11,6 @@
|
|||
"bin"
|
||||
],
|
||||
"exports": {
|
||||
"./daemon": "./src/daemon.ts",
|
||||
"./run": "./src/run/index.ts",
|
||||
"./server-process": "./src/server-process.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([])
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -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,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`,
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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: "" }
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ export function permissionLabel(option: PermissionOption): string {
|
|||
|
||||
export { permissionAlwaysLines }
|
||||
|
||||
export function permissionReply(
|
||||
function permissionReply(
|
||||
sessionID: string,
|
||||
requestID: string,
|
||||
reply: PermissionReply["reply"],
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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) } } : {}
|
||||
|
|
|
|||
|
|
@ -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
packages/tui/src/mini/tool.public.ts
Normal file
2
packages/tui/src/mini/tool.public.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export { toolInlineInfo, toolOutputText } from "./tool"
|
||||
export type { MiniToolPart } from "./types"
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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" },
|
||||
)
|
||||
}
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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,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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue