mini: add quiet transcript settings (#38152)

This commit is contained in:
Simon Klee 2026-07-21 19:36:34 +02:00 committed by GitHub
commit dd6c95fdc7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 398 additions and 52 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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