mini: add quiet transcript settings (#38152)
This commit is contained in:
parent
6f4b9504e5
commit
dd6c95fdc7
22 changed files with 398 additions and 52 deletions
|
|
@ -32,6 +32,9 @@ export default Runtime.handler(Commands.commands.mini, (input) =>
|
|||
replayLimit: Option.getOrUndefined(input.replayLimit),
|
||||
demo: input.demo,
|
||||
tuiConfig: resolved,
|
||||
config: {
|
||||
update: (update) => runServicePromise(config.update(update)),
|
||||
},
|
||||
}),
|
||||
)
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ export type MiniCommandInput = {
|
|||
replayLimit?: number
|
||||
demo?: boolean
|
||||
tuiConfig?: MiniFrontendInput["tuiConfig"]
|
||||
config?: MiniFrontendInput["config"]
|
||||
}
|
||||
|
||||
type Model = MiniFrontendInput["model"]
|
||||
|
|
@ -119,6 +120,7 @@ export async function runMini(input: MiniCommandInput) {
|
|||
replayLimit: input.replayLimit,
|
||||
demo: input.demo,
|
||||
tuiConfig: input.tuiConfig,
|
||||
config: input.config,
|
||||
})
|
||||
})
|
||||
if (result.exitCode !== 0) process.exit(result.exitCode)
|
||||
|
|
|
|||
|
|
@ -131,11 +131,16 @@ test("updates a config draft while preserving JSONC comments", async () => {
|
|||
const service = yield* Config.Service
|
||||
return yield* service.update((draft) => {
|
||||
draft.prompt = { paste: "compact" }
|
||||
draft.mini = { thinking: "hide", shell_output: "hide" }
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
expect(config).toEqual({ animations: true, prompt: { paste: "compact" } })
|
||||
expect(config).toEqual({
|
||||
animations: true,
|
||||
prompt: { paste: "compact" },
|
||||
mini: { thinking: "hide", shell_output: "hide" },
|
||||
})
|
||||
expect(await Bun.file(path.join(directory, "cli.json")).text()).toContain("// Keep this comment")
|
||||
} finally {
|
||||
await Bun.$`rm -rf ${directory}`
|
||||
|
|
|
|||
|
|
@ -122,6 +122,16 @@ export const Info = Schema.Struct({
|
|||
}),
|
||||
}),
|
||||
).annotate({ description: "Session transcript presentation settings" }),
|
||||
mini: Schema.optional(
|
||||
Schema.Struct({
|
||||
thinking: Schema.optional(Schema.Literals(["show", "hide"])).annotate({
|
||||
description: "Show or hide model reasoning in Mini",
|
||||
}),
|
||||
shell_output: Schema.optional(Schema.Literals(["show", "hide"])).annotate({
|
||||
description: "Show or hide raw shell tool output in Mini",
|
||||
}),
|
||||
}),
|
||||
).annotate({ description: "Mini transcript presentation settings" }),
|
||||
hints: Schema.optional(
|
||||
Schema.Struct({
|
||||
onboarding: Schema.optional(Schema.Boolean).annotate({ description: "Show getting-started guidance" }),
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { toolEntryBody } from "./tool"
|
||||
import type { RunEntryBody, StreamCommit } from "./types"
|
||||
import type { RunEntryBody, ScrollbackOptions, StreamCommit } from "./types"
|
||||
|
||||
export type EntryFlags = {
|
||||
startOnNewLine: boolean
|
||||
|
|
@ -162,7 +162,7 @@ export function entryCanStream(commit: StreamCommit, body: RunEntryBody): boolea
|
|||
return commit.kind === "assistant" || commit.kind === "reasoning"
|
||||
}
|
||||
|
||||
export function entryBody(commit: StreamCommit): RunEntryBody {
|
||||
export function entryBody(commit: StreamCommit, options?: ScrollbackOptions): RunEntryBody {
|
||||
if (commit.summary) {
|
||||
return RUN_ENTRY_NONE
|
||||
}
|
||||
|
|
@ -174,7 +174,7 @@ export function entryBody(commit: StreamCommit): RunEntryBody {
|
|||
}
|
||||
|
||||
if (commit.kind === "tool") {
|
||||
return toolEntryBody(commit, raw) ?? RUN_ENTRY_NONE
|
||||
return toolEntryBody(commit, raw, options) ?? RUN_ENTRY_NONE
|
||||
}
|
||||
|
||||
if (commit.kind === "assistant") {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,15 @@ import fuzzysort from "fuzzysort"
|
|||
import { createEffect, createMemo, createSignal, type Accessor } from "solid-js"
|
||||
import { RunFooterMenu, createFooterMenuState, type RunFooterMenuItem } from "./footer.menu"
|
||||
import type { RunFooterTheme } from "./theme"
|
||||
import type { FooterQueuedPrompt, FooterSubagentTab, RunCommand, RunInput, RunProvider } from "./types"
|
||||
import type {
|
||||
FooterQueuedPrompt,
|
||||
FooterSubagentTab,
|
||||
MiniSettingChange,
|
||||
MiniSettings,
|
||||
RunCommand,
|
||||
RunInput,
|
||||
RunProvider,
|
||||
} from "./types"
|
||||
|
||||
type PanelEntry = RunFooterMenuItem & {
|
||||
category: string
|
||||
|
|
@ -20,6 +28,7 @@ type CommandEntry =
|
|||
| (PanelEntry & { action: "subagent" })
|
||||
| (PanelEntry & { action: "variant.cycle" })
|
||||
| (PanelEntry & { action: "variant.list" })
|
||||
| (PanelEntry & { action: "settings" })
|
||||
| (PanelEntry & { action: "slash"; name: string })
|
||||
| (PanelEntry & { action: "exit" })
|
||||
|
||||
|
|
@ -48,6 +57,10 @@ type QueuedEntry = PanelEntry & {
|
|||
prompt: FooterQueuedPrompt
|
||||
}
|
||||
|
||||
type SettingEntry = PanelEntry & {
|
||||
key: keyof MiniSettings
|
||||
}
|
||||
|
||||
const PANEL_PAD = 2
|
||||
const PANEL_LIST_ROWS = 10
|
||||
const PANEL_FRAME_ROWS = 6
|
||||
|
|
@ -266,6 +279,7 @@ function PanelShell(props: {
|
|||
inputRef: (input: InputRenderable) => void
|
||||
onQuery: (query: string) => void
|
||||
children: JSX.Element
|
||||
hint?: string
|
||||
dark?: boolean
|
||||
chrome?: "default" | "minimal"
|
||||
}) {
|
||||
|
|
@ -294,7 +308,7 @@ function PanelShell(props: {
|
|||
) : null}
|
||||
<box flexGrow={1} flexShrink={1} backgroundColor="transparent" />
|
||||
<text fg={props.theme().muted} wrapMode="none" truncate flexShrink={0}>
|
||||
esc
|
||||
{props.hint ? `${props.hint} · ` : ""}esc
|
||||
</text>
|
||||
</box>
|
||||
<box height={1} flexShrink={0} backgroundColor={background()} />
|
||||
|
|
@ -400,6 +414,7 @@ export function RunCommandMenuBody(props: {
|
|||
onQueued: () => void
|
||||
onVariant: () => void
|
||||
onVariantCycle: () => void
|
||||
onSettings: () => void
|
||||
onCommand: (name: string) => void
|
||||
onNew: () => void
|
||||
onExit: () => void
|
||||
|
|
@ -407,7 +422,7 @@ export function RunCommandMenuBody(props: {
|
|||
const skills = createMemo(() => (props.commands() ?? []).filter((item) => item.source === "skill"))
|
||||
const activeSubagentCount = createMemo(() => props.subagents().filter((item) => item.status === "running").length)
|
||||
const entries = createMemo<CommandEntry[]>(() => {
|
||||
const builtins = ["editor", "new"]
|
||||
const builtins = ["editor", "new", "settings"]
|
||||
const session: CommandEntry[] = [
|
||||
{
|
||||
action: "editor",
|
||||
|
|
@ -515,6 +530,13 @@ export function RunCommandMenuBody(props: {
|
|||
...prompt,
|
||||
...agent,
|
||||
...commands,
|
||||
{
|
||||
action: "settings",
|
||||
category: "System",
|
||||
display: "Open settings",
|
||||
footer: "/settings",
|
||||
keywords: "/settings settings preferences configuration",
|
||||
},
|
||||
{ action: "exit", category: "System", display: "Exit", footer: "/exit", keywords: "/exit exit" },
|
||||
]
|
||||
})
|
||||
|
|
@ -554,6 +576,11 @@ export function RunCommandMenuBody(props: {
|
|||
return
|
||||
}
|
||||
|
||||
if (item.action === "settings") {
|
||||
props.onSettings()
|
||||
return
|
||||
}
|
||||
|
||||
if (item.action === "exit") {
|
||||
props.onExit()
|
||||
return
|
||||
|
|
@ -606,6 +633,87 @@ export function RunCommandMenuBody(props: {
|
|||
)
|
||||
}
|
||||
|
||||
export function RunSettingsBody(props: {
|
||||
theme: Accessor<RunFooterTheme>
|
||||
settings: Accessor<MiniSettings>
|
||||
onClose: () => void
|
||||
onChange: (change: MiniSettingChange) => void | Promise<void>
|
||||
}) {
|
||||
const [saving, setSaving] = createSignal<keyof MiniSettings>()
|
||||
const entries = createMemo<SettingEntry[]>(() => [
|
||||
{
|
||||
category: "Transcript",
|
||||
display: "Thinking",
|
||||
description: "future sessions",
|
||||
footer: saving() === "thinking" ? "saving" : props.settings().thinking,
|
||||
keywords: `thinking reasoning ${props.settings().thinking}`,
|
||||
key: "thinking",
|
||||
},
|
||||
{
|
||||
category: "Transcript",
|
||||
display: "Shell tool output",
|
||||
description: "model-issued commands",
|
||||
footer: saving() === "shell_output" ? "saving" : props.settings().shell_output,
|
||||
keywords: `shell tool command output ${props.settings().shell_output}`,
|
||||
key: "shell_output",
|
||||
},
|
||||
])
|
||||
const change = (item: SettingEntry) => {
|
||||
if (saving()) return
|
||||
const current = props.settings()[item.key]
|
||||
setSaving(item.key)
|
||||
void Promise.resolve(props.onChange({ key: item.key, value: current === "show" ? "hide" : "show" }))
|
||||
.catch(() => {})
|
||||
.finally(() => setSaving())
|
||||
}
|
||||
const controller = createSearchablePanelController({
|
||||
entries,
|
||||
limit: PANEL_LIST_ROWS,
|
||||
onClose: props.onClose,
|
||||
onSelect: change,
|
||||
onKey(event, item) {
|
||||
const name = event.name.toLowerCase()
|
||||
if (name !== "left" && name !== "right") return false
|
||||
event.preventDefault()
|
||||
if (item) change(item)
|
||||
return true
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<PanelShell
|
||||
title="Settings"
|
||||
countVisible={false}
|
||||
query={controller.query()}
|
||||
count={controller.items().length}
|
||||
total={entries().length}
|
||||
placeholder="Search"
|
||||
theme={props.theme}
|
||||
inputRef={controller.inputRef}
|
||||
onQuery={controller.setQuery}
|
||||
hint="left/right change"
|
||||
dark
|
||||
chrome="minimal"
|
||||
>
|
||||
<RunFooterMenu
|
||||
theme={props.theme}
|
||||
items={controller.items}
|
||||
selected={controller.menu.selected}
|
||||
offset={controller.menu.offset}
|
||||
rows={() => PANEL_LIST_ROWS}
|
||||
limit={PANEL_LIST_ROWS}
|
||||
empty="No settings found"
|
||||
border={false}
|
||||
paddingLeft={PANEL_PAD}
|
||||
paddingRight={PANEL_PAD}
|
||||
grouped={!controller.query().trim()}
|
||||
background
|
||||
headerColor={props.theme().muted}
|
||||
/>
|
||||
</PanelShell>
|
||||
)
|
||||
}
|
||||
|
||||
export function RunSubagentSelectBody(props: {
|
||||
theme: Accessor<RunFooterTheme>
|
||||
tabs: Accessor<FooterSubagentTab[]>
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ type Auto = RunFooterMenuItem & {
|
|||
type SlashOption = RunFooterMenuItem & {
|
||||
kind: "slash"
|
||||
name: string
|
||||
action?: "skill-menu" | "editor"
|
||||
action?: "skill-menu" | "editor" | "settings"
|
||||
}
|
||||
|
||||
type PromptOption = Auto | SlashOption
|
||||
|
|
@ -79,6 +79,7 @@ type PromptInput = {
|
|||
onExitRequest?: () => boolean
|
||||
onExit: () => void
|
||||
onSkillMenu: () => void
|
||||
onSettings: () => void
|
||||
onRows: (rows: number) => void
|
||||
onStatus: (text: string) => void
|
||||
}
|
||||
|
|
@ -380,6 +381,13 @@ export function createPromptState(input: PromptInput): PromptState {
|
|||
display: "/editor",
|
||||
description: "compose in your external editor",
|
||||
} satisfies SlashOption,
|
||||
{
|
||||
kind: "slash",
|
||||
action: "settings" as const,
|
||||
name: "settings",
|
||||
display: "/settings",
|
||||
description: "configure Mini transcript output",
|
||||
} satisfies SlashOption,
|
||||
{ kind: "slash", name: "new", display: "/new", description: "start a new session" } satisfies SlashOption,
|
||||
{ kind: "slash", name: "exit", display: "/exit", description: "close OpenCode" } satisfies SlashOption,
|
||||
]
|
||||
|
|
@ -815,6 +823,12 @@ export function createPromptState(input: PromptInput): PromptState {
|
|||
return
|
||||
}
|
||||
|
||||
if (next.action === "settings" && !shell()) {
|
||||
cancelAutocomplete()
|
||||
input.onSettings()
|
||||
return
|
||||
}
|
||||
|
||||
const cursor = area.cursorOffset
|
||||
const head = parseSlashHead(area.plainText)
|
||||
const local = !shell() && (next.name === "new" || next.name === "exit")
|
||||
|
|
@ -922,6 +936,7 @@ export function createPromptState(input: PromptInput): PromptState {
|
|||
if (current === "skill") return false
|
||||
if (current === "model") return false
|
||||
if (current === "variant") return false
|
||||
if (current === "settings") return false
|
||||
if (current === "queued-menu") return false
|
||||
if (current === "subagent-menu") return false
|
||||
return true
|
||||
|
|
@ -1119,6 +1134,12 @@ export function createPromptState(input: PromptInput): PromptState {
|
|||
return
|
||||
}
|
||||
|
||||
if (!command && next.mode !== "shell" && next.text.trim().toLowerCase() === "/settings") {
|
||||
resetDraft()
|
||||
input.onSettings()
|
||||
return
|
||||
}
|
||||
|
||||
const parsed =
|
||||
command || next.mode === "shell" || isNewCommand(next.text)
|
||||
? undefined
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ export function RunFooterSubagentBody(props: {
|
|||
// Formatted interrupt shortcut from the registered keymap binding; the
|
||||
// command itself is dispatched through the keymap in footer.view.
|
||||
interrupt?: () => string | undefined
|
||||
shellOutput?: () => boolean
|
||||
}) {
|
||||
const theme = createMemo(() => props.theme())
|
||||
const footer = createMemo(() => theme().footer)
|
||||
|
|
@ -86,7 +87,7 @@ export function RunFooterSubagentBody(props: {
|
|||
const rows = indexArray(commits, (commit, index) => (
|
||||
<box flexDirection="column" gap={0} flexShrink={0}>
|
||||
{index > 0 && separatorRows(commits()[index - 1], commit()) > 0 ? <box height={1} flexShrink={0} /> : null}
|
||||
<RunEntryContent commit={commit()} theme={theme()} />
|
||||
<RunEntryContent commit={commit()} theme={theme()} opts={{ shellOutput: props.shellOutput?.() ?? true }} />
|
||||
</box>
|
||||
))
|
||||
let scroll: ScrollBoxRenderable | undefined
|
||||
|
|
|
|||
|
|
@ -48,6 +48,8 @@ import type {
|
|||
FooterView,
|
||||
FormCancel,
|
||||
FormReply,
|
||||
MiniSettingChange,
|
||||
MiniSettings,
|
||||
PermissionReply,
|
||||
RunAgent,
|
||||
RunCommand,
|
||||
|
|
@ -81,6 +83,10 @@ type RunFooterOptions = {
|
|||
history?: RunPrompt[]
|
||||
theme: RunTheme
|
||||
tuiConfig: RunTuiConfig
|
||||
miniSettings: {
|
||||
current: MiniSettings
|
||||
update?: (change: MiniSettingChange) => Promise<MiniSettings>
|
||||
}
|
||||
onPermissionReply: (input: PermissionReply) => void | Promise<void>
|
||||
onFormReply: (input: FormReply) => void | Promise<void>
|
||||
onFormCancel: (input: FormCancel) => void | Promise<void>
|
||||
|
|
@ -97,11 +103,7 @@ type RunFooterOptions = {
|
|||
|
||||
const PERMISSION_ROWS = 12
|
||||
const FORM_ROWS = 14
|
||||
const COMMAND_ROWS = RUN_COMMAND_PANEL_ROWS
|
||||
const SKILL_ROWS = RUN_COMMAND_PANEL_ROWS
|
||||
const SUBAGENT_ROWS = RUN_SUBAGENT_PANEL_ROWS
|
||||
const MODEL_ROWS = RUN_COMMAND_PANEL_ROWS
|
||||
const VARIANT_ROWS = RUN_COMMAND_PANEL_ROWS
|
||||
const NOTICE_DURATION = 3000
|
||||
const THEME_REFRESH_DELAYS = [1000, 1000] as const
|
||||
|
||||
|
|
@ -185,6 +187,8 @@ export class RunFooter implements FooterApi {
|
|||
private setQueuedPrompts: Setter<FooterQueuedPrompt[]>
|
||||
private history: Accessor<RunPrompt[]>
|
||||
private setHistory: Setter<RunPrompt[]>
|
||||
private miniSettings: Accessor<MiniSettings>
|
||||
private setMiniSettings: Setter<MiniSettings>
|
||||
private promptRoute: FooterPromptRoute = { type: "composer" }
|
||||
private subagentMenuRows = SUBAGENT_ROWS
|
||||
private interruptTimeout: NodeJS.Timeout | undefined
|
||||
|
|
@ -209,6 +213,7 @@ export class RunFooter implements FooterApi {
|
|||
.catch(() => {})
|
||||
.finally(() => this.destroyTheme(theme))
|
||||
},
|
||||
shellOutput: () => this.miniSettings().shell_output === "show",
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -269,6 +274,9 @@ export class RunFooter implements FooterApi {
|
|||
const [history, setHistory] = createSignal(options.history ?? [])
|
||||
this.history = history
|
||||
this.setHistory = setHistory
|
||||
const [miniSettings, setMiniSettings] = createSignal(options.miniSettings.current)
|
||||
this.miniSettings = miniSettings
|
||||
this.setMiniSettings = setMiniSettings
|
||||
this.base = Math.max(1, renderer.footerHeight - TEXTAREA_MIN_ROWS)
|
||||
this.scrollback = this.createScrollback(options.wrote ?? false)
|
||||
|
||||
|
|
@ -300,6 +308,7 @@ export class RunFooter implements FooterApi {
|
|||
currentVariant: footer.currentVariant,
|
||||
theme: footer.theme,
|
||||
tuiConfig: options.tuiConfig,
|
||||
miniSettings: footer.miniSettings,
|
||||
history: footer.history,
|
||||
onSubmit: footer.handlePrompt,
|
||||
onPermissionReply: footer.handlePermissionReply,
|
||||
|
|
@ -318,6 +327,7 @@ export class RunFooter implements FooterApi {
|
|||
onRows: footer.syncRows,
|
||||
onLayout: footer.syncLayout,
|
||||
onStatus: footer.setStatus,
|
||||
onMiniSettingChange: footer.handleMiniSettingChange,
|
||||
onSubagentSelect: options.onSubagentSelect,
|
||||
onSubagentInterrupt: options.onSubagentInterrupt,
|
||||
})
|
||||
|
|
@ -662,26 +672,19 @@ export class RunFooter implements FooterApi {
|
|||
// get fixed extra rows; the prompt view scales with textarea line count.
|
||||
private applyHeight(): void {
|
||||
const type = this.view().type
|
||||
const route = this.promptRoute.type
|
||||
const height =
|
||||
type === "permission"
|
||||
? this.base + PERMISSION_ROWS
|
||||
: type === "form"
|
||||
? this.base + FORM_ROWS
|
||||
: this.promptRoute.type === "command"
|
||||
? 1 + COMMAND_ROWS
|
||||
: this.promptRoute.type === "skill"
|
||||
? 1 + SKILL_ROWS
|
||||
: this.promptRoute.type === "model"
|
||||
? 1 + MODEL_ROWS
|
||||
: this.promptRoute.type === "variant"
|
||||
? 1 + VARIANT_ROWS
|
||||
: this.promptRoute.type === "queued-menu"
|
||||
? 1 + this.subagentMenuRows
|
||||
: this.promptRoute.type === "subagent-menu"
|
||||
? 1 + this.subagentMenuRows
|
||||
: this.promptRoute.type === "subagent"
|
||||
? this.base + SUBAGENT_INSPECTOR_ROWS
|
||||
: this.base + Math.max(TEXTAREA_MIN_ROWS, Math.min(PROMPT_MAX_ROWS, this.rows))
|
||||
: ["command", "skill", "model", "variant", "settings"].includes(route)
|
||||
? 1 + RUN_COMMAND_PANEL_ROWS
|
||||
: route === "queued-menu" || route === "subagent-menu"
|
||||
? 1 + this.subagentMenuRows
|
||||
: route === "subagent"
|
||||
? this.base + SUBAGENT_INSPECTOR_ROWS
|
||||
: this.base + Math.max(TEXTAREA_MIN_ROWS, Math.min(PROMPT_MAX_ROWS, this.rows))
|
||||
|
||||
if (height !== this.renderer.footerHeight) {
|
||||
this.renderer.footerHeight = height
|
||||
|
|
@ -864,6 +867,21 @@ export class RunFooter implements FooterApi {
|
|||
.catch(() => {})
|
||||
}
|
||||
|
||||
private handleMiniSettingChange = async (change: MiniSettingChange): Promise<void> => {
|
||||
if (!this.options.miniSettings.update) {
|
||||
this.setNotice("settings are unavailable")
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
this.setMiniSettings(await this.options.miniSettings.update(change))
|
||||
this.setNotice("settings updated")
|
||||
} catch (error) {
|
||||
this.setNotice("failed to save settings")
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
private clearInterruptTimer(): void {
|
||||
if (!this.interruptTimeout) {
|
||||
return
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import {
|
|||
RunCommandMenuBody,
|
||||
RunModelSelectBody,
|
||||
RunQueuedPromptSelectBody,
|
||||
RunSettingsBody,
|
||||
RunSkillSelectBody,
|
||||
RunSubagentSelectBody,
|
||||
RunVariantSelectBody,
|
||||
|
|
@ -39,6 +40,8 @@ import type {
|
|||
FooterView,
|
||||
FormCancel,
|
||||
FormReply,
|
||||
MiniSettingChange,
|
||||
MiniSettings,
|
||||
PermissionReply,
|
||||
RunAgent,
|
||||
RunCommand,
|
||||
|
|
@ -82,6 +85,7 @@ type RunFooterViewProps = {
|
|||
queuedPrompts?: () => FooterQueuedPrompt[]
|
||||
theme: () => RunTheme
|
||||
tuiConfig: RunTuiConfig
|
||||
miniSettings: () => MiniSettings
|
||||
history?: () => RunPrompt[]
|
||||
onSubmit: (input: RunPrompt) => boolean
|
||||
onPermissionReply: (input: PermissionReply) => void | Promise<void>
|
||||
|
|
@ -100,6 +104,7 @@ type RunFooterViewProps = {
|
|||
onRows: (rows: number) => void
|
||||
onLayout: (input: { route: FooterPromptRoute; subagentRows: number }) => void
|
||||
onStatus: (text: string) => void
|
||||
onMiniSettingChange: (change: MiniSettingChange) => void | Promise<void>
|
||||
onSubagentSelect?: (sessionID: string | undefined) => void
|
||||
onSubagentInterrupt?: (sessionID: string) => void
|
||||
}
|
||||
|
|
@ -131,6 +136,7 @@ export function RunFooterView(props: RunFooterViewProps) {
|
|||
const skilling = createMemo(() => active().type === "prompt" && route().type === "skill")
|
||||
const modeling = createMemo(() => active().type === "prompt" && route().type === "model")
|
||||
const varianting = createMemo(() => active().type === "prompt" && route().type === "variant")
|
||||
const setting = createMemo(() => active().type === "prompt" && route().type === "settings")
|
||||
const panel = createMemo(
|
||||
() =>
|
||||
active().type === "permission" ||
|
||||
|
|
@ -140,7 +146,8 @@ export function RunFooterView(props: RunFooterViewProps) {
|
|||
commanding() ||
|
||||
skilling() ||
|
||||
modeling() ||
|
||||
varianting(),
|
||||
varianting() ||
|
||||
setting(),
|
||||
)
|
||||
const selected = createMemo(() => {
|
||||
const current = route()
|
||||
|
|
@ -257,6 +264,11 @@ export function RunFooterView(props: RunFooterViewProps) {
|
|||
props.onSubagentSelect?.(undefined)
|
||||
}
|
||||
|
||||
const openSettings = () => {
|
||||
setRoute({ type: "settings" })
|
||||
props.onSubagentSelect?.(undefined)
|
||||
}
|
||||
|
||||
const openSubagentMenu = () => {
|
||||
if (tabs().length === 0) {
|
||||
return
|
||||
|
|
@ -323,6 +335,7 @@ export function RunFooterView(props: RunFooterViewProps) {
|
|||
onExitRequest: props.onExitRequest,
|
||||
onExit: props.onExit,
|
||||
onSkillMenu: openSkillMenu,
|
||||
onSettings: openSettings,
|
||||
onRows: props.onRows,
|
||||
onStatus: props.onStatus,
|
||||
})
|
||||
|
|
@ -559,6 +572,7 @@ export function RunFooterView(props: RunFooterViewProps) {
|
|||
current.type !== "skill" &&
|
||||
current.type !== "model" &&
|
||||
current.type !== "variant" &&
|
||||
current.type !== "settings" &&
|
||||
current.type !== "queued-menu" &&
|
||||
current.type !== "subagent-menu"
|
||||
) {
|
||||
|
|
@ -668,6 +682,7 @@ export function RunFooterView(props: RunFooterViewProps) {
|
|||
onSubagent={openSubagentMenu}
|
||||
onQueued={openQueuedMenu}
|
||||
onVariant={openVariant}
|
||||
onSettings={openSettings}
|
||||
onVariantCycle={() => {
|
||||
props.onCycle()
|
||||
closePanel()
|
||||
|
|
@ -726,6 +741,14 @@ export function RunFooterView(props: RunFooterViewProps) {
|
|||
}}
|
||||
/>
|
||||
</Match>
|
||||
<Match when={setting()}>
|
||||
<RunSettingsBody
|
||||
theme={theme}
|
||||
settings={props.miniSettings}
|
||||
onClose={closePanel}
|
||||
onChange={props.onMiniSettingChange}
|
||||
/>
|
||||
</Match>
|
||||
<Match when={active().type === "permission"}>
|
||||
<RunPermissionBody
|
||||
request={permission()!.request}
|
||||
|
|
@ -904,6 +927,7 @@ export function RunFooterView(props: RunFooterViewProps) {
|
|||
onCycle={cycleTab}
|
||||
onClose={closeTab}
|
||||
interrupt={() => subagentInterruptShortcut() || undefined}
|
||||
shellOutput={() => props.miniSettings().shell_output === "show"}
|
||||
/>
|
||||
</box>
|
||||
</Show>
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import type { LocationRef } from "@opencode-ai/client/promise"
|
|||
import { resolve } from "../config"
|
||||
import { loadRunProviders } from "./catalog.shared"
|
||||
import { resolveCurrentSession, sessionHistory } from "./session.shared"
|
||||
import type { RunInput, RunPrompt, RunProvider, RunTuiConfig } from "./types"
|
||||
import type { MiniSettings, RunInput, RunPrompt, RunProvider, RunTuiConfig } from "./types"
|
||||
import { pickVariant } from "./variant.shared"
|
||||
|
||||
export type ModelInfo = {
|
||||
|
|
@ -83,3 +83,10 @@ export async function resolveRunTuiConfig(
|
|||
.then((value) => value ?? defaultRunTuiConfig(platform))
|
||||
.catch(() => defaultRunTuiConfig(platform))
|
||||
}
|
||||
|
||||
export function resolveMiniSettings(config?: { mini?: Partial<MiniSettings> }): MiniSettings {
|
||||
return {
|
||||
thinking: config?.mini?.thinking ?? "hide",
|
||||
shell_output: config?.mini?.shell_output ?? "hide",
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ import type {
|
|||
FooterApi,
|
||||
FormCancel,
|
||||
FormReply,
|
||||
MiniSettingChange,
|
||||
MiniSettings,
|
||||
MiniHost,
|
||||
PermissionReply,
|
||||
RunAgent,
|
||||
|
|
@ -26,6 +28,7 @@ import type {
|
|||
RunReference,
|
||||
RunTuiConfig,
|
||||
} from "./types"
|
||||
import { resolveMiniSettings } from "./runtime.boot"
|
||||
import { formatModelLabel } from "./variant.shared"
|
||||
|
||||
const FOOTER_HEIGHT = 4
|
||||
|
|
@ -62,6 +65,7 @@ export type LifecycleInput = {
|
|||
model: RunInput["model"]
|
||||
variant: string | undefined
|
||||
tuiConfig: RunTuiConfig | Promise<RunTuiConfig>
|
||||
onMiniSettingChange?: (change: MiniSettingChange) => Promise<MiniSettings>
|
||||
onPermissionReply: (input: PermissionReply) => void | Promise<void>
|
||||
onFormReply: (input: FormReply) => void | Promise<void>
|
||||
onFormCancel: (input: FormCancel) => void | Promise<void>
|
||||
|
|
@ -224,6 +228,10 @@ export async function createRuntimeLifecycle(input: LifecycleInput): Promise<Lif
|
|||
theme,
|
||||
wrote,
|
||||
tuiConfig,
|
||||
miniSettings: {
|
||||
current: resolveMiniSettings(tuiConfig),
|
||||
update: input.onMiniSettingChange,
|
||||
},
|
||||
onPermissionReply: input.onPermissionReply,
|
||||
onFormReply: input.onFormReply,
|
||||
onFormCancel: input.onFormCancel,
|
||||
|
|
|
|||
|
|
@ -10,11 +10,27 @@
|
|||
// 4. runs the prompt queue until the footer closes.
|
||||
import { SessionMessage } from "@opencode-ai/schema/session-message"
|
||||
import type { LocationRef } from "@opencode-ai/client/promise"
|
||||
import type { Config } from "../config"
|
||||
import { loadRunAgents, loadRunCommands, loadRunReferences, waitForDefaultModel } from "./catalog.shared"
|
||||
import { resolveModelInfo, resolveModelInfoStrict, resolveRunTuiConfig, resolveSessionInfo } from "./runtime.boot"
|
||||
import {
|
||||
resolveMiniSettings,
|
||||
resolveModelInfo,
|
||||
resolveModelInfoStrict,
|
||||
resolveRunTuiConfig,
|
||||
resolveSessionInfo,
|
||||
} from "./runtime.boot"
|
||||
import { createRuntimeLifecycle } from "./runtime.lifecycle"
|
||||
import { cycleVariant, formatModelLabel, resolveVariant } from "./variant.shared"
|
||||
import type { LocalReplayRow, MiniHost, RunInput, RunPrompt, RunProvider, RunTuiConfig, StreamCommit } from "./types"
|
||||
import type {
|
||||
LocalReplayRow,
|
||||
MiniHost,
|
||||
MiniSettings,
|
||||
RunInput,
|
||||
RunPrompt,
|
||||
RunProvider,
|
||||
RunTuiConfig,
|
||||
StreamCommit,
|
||||
} from "./types"
|
||||
|
||||
type BootContext = Pick<RunInput, "sdk" | "agent" | "model" | "variant"> & {
|
||||
location: LocationRef
|
||||
|
|
@ -43,6 +59,7 @@ type RunRuntimeInput = {
|
|||
replayLimit?: number
|
||||
demo?: RunInput["demo"]
|
||||
tuiConfig?: RunTuiConfig | Promise<RunTuiConfig>
|
||||
config?: Pick<Config.Interface, "update">
|
||||
}
|
||||
|
||||
export type RunDeferredInput = {
|
||||
|
|
@ -62,6 +79,7 @@ export type RunDeferredInput = {
|
|||
replayLimit?: number
|
||||
demo?: RunInput["demo"]
|
||||
tuiConfig?: RunTuiConfig | Promise<RunTuiConfig>
|
||||
config?: Pick<Config.Interface, "update">
|
||||
}
|
||||
|
||||
type StreamTransportModule = Pick<
|
||||
|
|
@ -174,7 +192,12 @@ function abortable<A>(task: Promise<A>, signal: AbortSignal): Promise<A | undefi
|
|||
async function runInteractiveRuntime(input: RunRuntimeInput, deps: RunRuntimeDeps = {}): Promise<void> {
|
||||
const start = input.host.startup.now()
|
||||
const log = input.host.diagnostics.trace
|
||||
const tuiConfigTask = resolveRunTuiConfig(input.tuiConfig, input.host.platform)
|
||||
const config = input.config
|
||||
const configState: { current: MiniSettings } = { current: resolveMiniSettings() }
|
||||
const tuiConfigTask = resolveRunTuiConfig(input.tuiConfig, input.host.platform).then((tuiConfig) => {
|
||||
configState.current = resolveMiniSettings(tuiConfig)
|
||||
return tuiConfig
|
||||
})
|
||||
const ctx = await input.boot()
|
||||
const runtimeController = new AbortController()
|
||||
const session = {
|
||||
|
|
@ -226,6 +249,16 @@ async function runInteractiveRuntime(input: RunRuntimeInput, deps: RunRuntimeDep
|
|||
model: state.model,
|
||||
variant: state.activeVariant,
|
||||
tuiConfig: tuiConfigTask,
|
||||
onMiniSettingChange: config
|
||||
? async (change) => {
|
||||
const info = await config.update((draft) => {
|
||||
if (!draft.mini || typeof draft.mini !== "object") draft.mini = {}
|
||||
draft.mini[change.key] = change.value
|
||||
})
|
||||
configState.current = resolveMiniSettings(info)
|
||||
return configState.current
|
||||
}
|
||||
: undefined,
|
||||
onPermissionReply: async (next) => {
|
||||
if (state.demo?.permission(next)) {
|
||||
return
|
||||
|
|
@ -359,8 +392,8 @@ async function runInteractiveRuntime(input: RunRuntimeInput, deps: RunRuntimeDep
|
|||
})
|
||||
},
|
||||
})
|
||||
const tuiConfig = await tuiConfigTask
|
||||
const thinking = input.thinking ?? tuiConfig.session?.thinking !== "hide"
|
||||
await tuiConfigTask
|
||||
const thinking = () => input.thinking ?? configState.current.thinking === "show"
|
||||
const footer = shell.footer
|
||||
const firstPaint = footer.idle().catch(() => {})
|
||||
const offRuntimeClose = footer.onClose(() => runtimeController.abort())
|
||||
|
|
@ -679,7 +712,7 @@ async function runInteractiveRuntime(input: RunRuntimeInput, deps: RunRuntimeDep
|
|||
return createRunDemo({
|
||||
footer,
|
||||
sessionID: state.sessionID,
|
||||
thinking,
|
||||
thinking: thinking(),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -722,7 +755,7 @@ async function runInteractiveRuntime(input: RunRuntimeInput, deps: RunRuntimeDep
|
|||
readTextFile: input.host.files.readText,
|
||||
location: state.location,
|
||||
sessionID: state.sessionID,
|
||||
thinking,
|
||||
thinking: thinking(),
|
||||
replay: input.replay,
|
||||
replayLimit: input.replayLimit,
|
||||
footer,
|
||||
|
|
@ -1018,6 +1051,7 @@ export async function runInteractiveDeferredMode(input: RunDeferredInput, deps?:
|
|||
replayLimit: input.replayLimit,
|
||||
demo: input.demo,
|
||||
tuiConfig: input.tuiConfig,
|
||||
config: input.config,
|
||||
reconnect: input.reconnect,
|
||||
resolveSession: input.target,
|
||||
createSession: input.createSession,
|
||||
|
|
|
|||
|
|
@ -88,6 +88,7 @@ export class RunScrollbackStream {
|
|||
private active: ActiveEntry | undefined
|
||||
private treeSitterClient: TreeSitterClient | undefined
|
||||
private wrote: boolean
|
||||
private shellOutput: () => boolean
|
||||
private pendingThemes: RunTheme[] = []
|
||||
|
||||
constructor(
|
||||
|
|
@ -97,10 +98,12 @@ export class RunScrollbackStream {
|
|||
wrote?: boolean
|
||||
treeSitterClient?: TreeSitterClient
|
||||
onThemeRelease?: (theme: RunTheme) => void
|
||||
shellOutput?: () => boolean
|
||||
} = {},
|
||||
) {
|
||||
this.treeSitterClient = options.treeSitterClient
|
||||
this.wrote = options.wrote ?? false
|
||||
this.shellOutput = options.shellOutput ?? (() => true)
|
||||
this.onThemeRelease = options.onThemeRelease
|
||||
}
|
||||
|
||||
|
|
@ -354,7 +357,7 @@ export class RunScrollbackStream {
|
|||
return
|
||||
}
|
||||
|
||||
const body = entryBody(commit)
|
||||
const body = entryBody(commit, { shellOutput: this.shellOutput() })
|
||||
if (body.type === "none") {
|
||||
if (entryDone(commit)) {
|
||||
this.markRendered(await this.finishActive(false))
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ export function RunEntryContent(props: {
|
|||
opts?: ScrollbackOptions
|
||||
}) {
|
||||
const theme = createMemo(() => props.theme ?? RUN_THEME_FALLBACK)
|
||||
const body = createMemo(() => props.body ?? entryBody(props.commit))
|
||||
const body = createMemo(() => props.body ?? entryBody(props.commit, props.opts))
|
||||
const style = createMemo(() => entryLook(props.commit, theme().entry))
|
||||
const syntax = createMemo(() => entrySyntax(theme()))
|
||||
const color = createMemo(() => entryColor(props.commit, theme()))
|
||||
|
|
|
|||
|
|
@ -758,10 +758,12 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
|
|||
client.session.active(options),
|
||||
])
|
||||
if (!current(attempt)) return
|
||||
state.pending = new Map(pending.flatMap((item) => {
|
||||
const prompt = pendingPrompt(item)
|
||||
return prompt ? [[prompt.messageID, prompt] as const] : []
|
||||
}))
|
||||
state.pending = new Map(
|
||||
pending.flatMap((item) => {
|
||||
const prompt = pendingPrompt(item)
|
||||
return prompt ? [[prompt.messageID, prompt] as const] : []
|
||||
}),
|
||||
)
|
||||
syncPending()
|
||||
state.permissions = permissions
|
||||
pruneToolSources()
|
||||
|
|
|
|||
|
|
@ -1273,7 +1273,11 @@ function shellOutput(command: string, raw: string): string | undefined {
|
|||
return `\n${body}`
|
||||
}
|
||||
|
||||
export function toolEntryBody(commit: StreamCommit, raw: string): RunEntryBody | undefined {
|
||||
export function toolEntryBody(
|
||||
commit: StreamCommit,
|
||||
raw: string,
|
||||
options?: { shellOutput?: boolean },
|
||||
): RunEntryBody | undefined {
|
||||
if (commit.shell) {
|
||||
if (commit.phase === "start") {
|
||||
return textBody(`$ ${commit.shell.command}`)
|
||||
|
|
@ -1294,6 +1298,8 @@ export function toolEntryBody(commit: StreamCommit, raw: string): RunEntryBody |
|
|||
const ctx = toolFrame(commit, raw)
|
||||
const view = toolView(ctx.name)
|
||||
|
||||
if (ctx.name === "shell" && commit.phase === "progress" && options?.shellOutput === false) return undefined
|
||||
|
||||
if (ctx.name === "subagent") {
|
||||
if (commit.phase === "start") {
|
||||
return undefined
|
||||
|
|
|
|||
|
|
@ -184,6 +184,7 @@ export type TurnSummary = {
|
|||
|
||||
export type ScrollbackOptions = {
|
||||
suppressBackgrounds?: boolean
|
||||
shellOutput?: boolean
|
||||
}
|
||||
|
||||
export type ToolCodeSnapshot = {
|
||||
|
|
@ -293,6 +294,7 @@ export type FooterPromptRoute =
|
|||
| { type: "skill" }
|
||||
| { type: "model" }
|
||||
| { type: "variant" }
|
||||
| { type: "settings" }
|
||||
|
||||
export type FooterSubagentTab = {
|
||||
sessionID: string
|
||||
|
|
@ -389,7 +391,17 @@ export type FormCancel = {
|
|||
location?: LocationRef
|
||||
}
|
||||
|
||||
export type RunTuiConfig = Pick<Config.Resolved, "keybinds" | "leader" | "theme" | "session">
|
||||
export type RunTuiConfig = Pick<Config.Resolved, "keybinds" | "leader" | "theme" | "session" | "mini">
|
||||
|
||||
export type MiniSettings = {
|
||||
thinking: "show" | "hide"
|
||||
shell_output: "show" | "hide"
|
||||
}
|
||||
|
||||
export type MiniSettingChange = {
|
||||
key: keyof MiniSettings
|
||||
value: "show" | "hide"
|
||||
}
|
||||
|
||||
// Lifecycle phase of a scrollback entry. "start" opens the entry, "progress"
|
||||
// appends content (coalesced in the footer queue), "final" closes it.
|
||||
|
|
|
|||
|
|
@ -389,7 +389,6 @@ describe("run entry body", () => {
|
|||
type: "text",
|
||||
content: "$ pwd",
|
||||
})
|
||||
|
||||
expect(
|
||||
entryBody(
|
||||
commit({
|
||||
|
|
@ -411,6 +410,15 @@ describe("run entry body", () => {
|
|||
})
|
||||
})
|
||||
|
||||
test("hides shell tool output but not direct shell output", () => {
|
||||
const output = commit({ kind: "tool", text: "output", phase: "progress", source: "tool", tool: "shell" })
|
||||
expect(entryBody(output, { shellOutput: false })).toEqual({ type: "none" })
|
||||
expect(entryBody({ ...output, shell: { command: "pwd" } }, { shellOutput: false })).toEqual({
|
||||
type: "text",
|
||||
content: "\noutput",
|
||||
})
|
||||
})
|
||||
|
||||
test("falls back to patch summary when patch has no visible diff items", () => {
|
||||
expect(
|
||||
entryBody(
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ test("down opens subagents from an empty prompt", async () => {
|
|||
subagent={subagents}
|
||||
theme={() => RUN_THEME_FALLBACK}
|
||||
tuiConfig={config}
|
||||
miniSettings={() => ({ thinking: "hide", shell_output: "hide" })}
|
||||
onSubmit={() => true}
|
||||
onPermissionReply={() => {}}
|
||||
onFormReply={() => {}}
|
||||
|
|
@ -69,6 +70,7 @@ test("down opens subagents from an empty prompt", async () => {
|
|||
onRows={() => {}}
|
||||
onLayout={() => {}}
|
||||
onStatus={() => {}}
|
||||
onMiniSettingChange={() => {}}
|
||||
/>
|
||||
</Keymap.Provider>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import {
|
|||
RunCommandMenuBody,
|
||||
RunModelSelectBody,
|
||||
RunQueuedPromptSelectBody,
|
||||
RunSettingsBody,
|
||||
RunSkillSelectBody,
|
||||
RunSubagentSelectBody,
|
||||
RunVariantSelectBody,
|
||||
|
|
@ -23,6 +24,8 @@ import type {
|
|||
FooterSubagentState,
|
||||
FooterSubagentTab,
|
||||
FooterView,
|
||||
MiniSettingChange,
|
||||
MiniSettings,
|
||||
RunCommand,
|
||||
RunInput,
|
||||
RunPrompt,
|
||||
|
|
@ -117,6 +120,8 @@ async function renderFooter(
|
|||
onSubmit?: (prompt: RunPrompt) => boolean
|
||||
view?: FooterView
|
||||
onFormReply?: (input: unknown) => void
|
||||
miniSettings?: MiniSettings
|
||||
onMiniSettingChange?: (change: MiniSettingChange) => void
|
||||
} = {},
|
||||
) {
|
||||
const [view, setView] = createSignal<FooterView>(input.view ?? { type: "prompt" })
|
||||
|
|
@ -125,6 +130,7 @@ async function renderFooter(
|
|||
)
|
||||
const state = footerState(input.state)
|
||||
const config = input.tuiConfig ?? tuiConfig
|
||||
const [miniSettings] = createSignal<MiniSettings>(input.miniSettings ?? { thinking: "hide", shell_output: "hide" })
|
||||
function Harness() {
|
||||
return (
|
||||
<Keymap.Provider config={config}>
|
||||
|
|
@ -143,6 +149,7 @@ async function renderFooter(
|
|||
subagent={subagents}
|
||||
theme={input.theme ?? (() => RUN_THEME_FALLBACK)}
|
||||
tuiConfig={config}
|
||||
miniSettings={miniSettings}
|
||||
onSubmit={input.onSubmit ?? (() => true)}
|
||||
onPermissionReply={() => {}}
|
||||
onFormReply={(value) => input.onFormReply?.(value)}
|
||||
|
|
@ -157,6 +164,7 @@ async function renderFooter(
|
|||
onRows={() => {}}
|
||||
onLayout={() => {}}
|
||||
onStatus={() => {}}
|
||||
onMiniSettingChange={(change) => input.onMiniSettingChange?.(change)}
|
||||
/>
|
||||
</Keymap.Provider>
|
||||
)
|
||||
|
|
@ -369,6 +377,7 @@ test("direct command panel renders grouped command palette", async () => {
|
|||
onQueued={() => {}}
|
||||
onVariant={() => {}}
|
||||
onVariantCycle={() => {}}
|
||||
onSettings={() => {}}
|
||||
onCommand={() => {}}
|
||||
onNew={() => {}}
|
||||
onExit={() => {}}
|
||||
|
|
@ -408,6 +417,40 @@ test("direct command panel renders grouped command palette", async () => {
|
|||
}
|
||||
})
|
||||
|
||||
test("direct settings panel changes Mini transcript preferences", async () => {
|
||||
const [settings, setSettings] = createSignal<MiniSettings>({ thinking: "hide", shell_output: "hide" })
|
||||
const app = await testRender(
|
||||
() => (
|
||||
<box width={100} height={RUN_COMMAND_PANEL_ROWS}>
|
||||
<RunSettingsBody
|
||||
theme={() => RUN_THEME_FALLBACK.footer}
|
||||
settings={settings}
|
||||
onClose={() => {}}
|
||||
onChange={(change) => {
|
||||
setSettings((current) => ({ ...current, [change.key]: change.value }))
|
||||
}}
|
||||
/>
|
||||
</box>
|
||||
),
|
||||
{ width: 100, height: RUN_COMMAND_PANEL_ROWS },
|
||||
)
|
||||
|
||||
try {
|
||||
await app.renderOnce()
|
||||
expect(app.captureCharFrame()).toContain("Settings")
|
||||
expect(app.captureCharFrame()).toContain("Thinking")
|
||||
expect(app.captureCharFrame()).toContain("Shell tool output")
|
||||
expect(app.captureCharFrame()).toContain("left/right change")
|
||||
|
||||
app.mockInput.pressKey("ARROW_RIGHT")
|
||||
await app.renderOnce()
|
||||
|
||||
expect(settings()).toEqual({ thinking: "show", shell_output: "hide" })
|
||||
} finally {
|
||||
app.renderer.destroy()
|
||||
}
|
||||
})
|
||||
|
||||
test("direct skill panel renders searchable skill list", async () => {
|
||||
const [commands] = createSignal<RunCommand[] | undefined>([
|
||||
command({ name: "review", description: "Review code" }),
|
||||
|
|
@ -518,6 +561,7 @@ test("direct command panel shows subagent entry when available", async () => {
|
|||
onQueued={() => {}}
|
||||
onVariant={() => {}}
|
||||
onVariantCycle={() => {}}
|
||||
onSettings={() => {}}
|
||||
onCommand={() => {}}
|
||||
onNew={() => {}}
|
||||
onExit={() => {}}
|
||||
|
|
@ -566,6 +610,7 @@ test("direct command panel keeps completed subagents available", async () => {
|
|||
onQueued={() => {}}
|
||||
onVariant={() => {}}
|
||||
onVariantCycle={() => {}}
|
||||
onSettings={() => {}}
|
||||
onCommand={() => {}}
|
||||
onNew={() => {}}
|
||||
onExit={() => {}}
|
||||
|
|
@ -822,7 +867,7 @@ test("direct footer submits slash autocomplete selections without dispatching sh
|
|||
await app.renderOnce()
|
||||
|
||||
app.mockInput.pressKey("!")
|
||||
"/rev".split("").forEach((key) => app.mockInput.pressKey(key))
|
||||
"/settings".split("").forEach((key) => app.mockInput.pressKey(key))
|
||||
await app.renderOnce()
|
||||
app.mockInput.pressEnter()
|
||||
await app.renderOnce()
|
||||
|
|
@ -834,7 +879,7 @@ test("direct footer submits slash autocomplete selections without dispatching sh
|
|||
{ text: "/new ", parts: [] },
|
||||
{ text: "/new ", parts: [] },
|
||||
])
|
||||
expect(app.captureCharFrame()).toContain("/review")
|
||||
expect(app.renderer.currentFocusedEditor?.plainText).toBe("/settings ")
|
||||
} finally {
|
||||
app.cleanup()
|
||||
}
|
||||
|
|
@ -867,6 +912,26 @@ test("direct footer slash autocomplete keeps a real skills command", async () =>
|
|||
}
|
||||
})
|
||||
|
||||
test("direct footer closes settings with ctrl-c instead of arming exit", async () => {
|
||||
const app = await renderFooter({ height: 20 })
|
||||
|
||||
try {
|
||||
await app.renderOnce()
|
||||
"/settings".split("").forEach((key) => app.mockInput.pressKey(key))
|
||||
await app.renderOnce()
|
||||
app.mockInput.pressEnter()
|
||||
await app.renderOnce()
|
||||
expect(app.captureCharFrame()).toContain("Shell tool output")
|
||||
|
||||
app.mockInput.pressKey("c", { ctrl: true })
|
||||
await app.renderOnce()
|
||||
expect(app.captureCharFrame()).not.toContain("Shell tool output")
|
||||
expect(app.renderer.currentFocusedEditor?.plainText).toBe("")
|
||||
} finally {
|
||||
app.cleanup()
|
||||
}
|
||||
})
|
||||
|
||||
test("selectedCommand backfills the catalog source for bound drafts", () => {
|
||||
const catalog = [command({ name: "opencode-ts", description: "TS skill", source: "skill" })]
|
||||
|
||||
|
|
@ -1034,6 +1099,7 @@ test("direct footer shows authoritative pending work while running", async () =>
|
|||
]}
|
||||
theme={() => RUN_THEME_FALLBACK}
|
||||
tuiConfig={tuiConfig}
|
||||
miniSettings={() => ({ thinking: "hide", shell_output: "hide" })}
|
||||
onSubmit={() => true}
|
||||
onPermissionReply={() => {}}
|
||||
onFormReply={() => {}}
|
||||
|
|
@ -1048,6 +1114,7 @@ test("direct footer shows authoritative pending work while running", async () =>
|
|||
onRows={() => {}}
|
||||
onLayout={() => {}}
|
||||
onStatus={() => {}}
|
||||
onMiniSettingChange={() => {}}
|
||||
/>
|
||||
</Keymap.Provider>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { afterEach, describe, expect, mock, spyOn, test } from "bun:test"
|
||||
import { OpenCode } from "@opencode-ai/client/promise"
|
||||
import type { Resolved } from "../../src/config"
|
||||
import { resolveModelInfo, resolveRunTuiConfig } from "../../src/mini/runtime.boot"
|
||||
import { resolveMiniSettings, resolveModelInfo, resolveRunTuiConfig } from "../../src/mini/runtime.boot"
|
||||
import { catalogModel, catalogProvider } from "./fixture/catalog"
|
||||
import { createTuiResolvedConfig } from "./fixture/tui-runtime"
|
||||
|
||||
|
|
@ -91,18 +91,23 @@ describe("run runtime boot", () => {
|
|||
expect(result.keybinds.get("leader")).toEqual([])
|
||||
})
|
||||
|
||||
test("preserves current theme mode, leader, and thinking config", async () => {
|
||||
test("preserves shared config while resolving independent Mini defaults", async () => {
|
||||
const result = await resolveRunTuiConfig(
|
||||
createTuiResolvedConfig({
|
||||
theme: { mode: "light" },
|
||||
leader_timeout: 450,
|
||||
session: { thinking: "hide" },
|
||||
session: { thinking: "show" },
|
||||
}),
|
||||
)
|
||||
|
||||
expect(result.theme).toEqual({ mode: "light" })
|
||||
expect(result.leader.timeout).toBe(450)
|
||||
expect(result.session?.thinking).toBe("hide")
|
||||
expect(result.session?.thinking).toBe("show")
|
||||
expect(resolveMiniSettings(result)).toEqual({ thinking: "hide", shell_output: "hide" })
|
||||
expect(resolveMiniSettings({ mini: { thinking: "show", shell_output: "show" } })).toEqual({
|
||||
thinking: "show",
|
||||
shell_output: "show",
|
||||
})
|
||||
})
|
||||
|
||||
test("loads v2 providers and models for model selector data", async () => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue