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