mini: add more minimal output presets. (#47931)

This commit is contained in:
Simon Klee
2026-09-08 11:53:02 +02:00
committed by GitHub
Unverified
parent 4fef8edbe8
commit 2eea36e731
16 changed files with 480 additions and 30 deletions
+3
View File
@@ -191,6 +191,9 @@ export const Info = Schema.Struct({
thinking: Schema.optional(Schema.Literals(["show", "hide"])).annotate({
description: "Show or hide model reasoning",
}),
tools: Schema.optional(Schema.Literals(["show", "hide"])).annotate({
description: "Show or hide tool calls and the assistant text that precedes them",
}),
shell_output: Schema.optional(Schema.Literals(["show", "hide"])).annotate({
description: "Show or hide raw shell tool output",
}),
+34 -14
View File
@@ -27,6 +27,7 @@ import type {
RunInput,
RunProvider,
} from "./types"
import { matchMiniVerbosity, verbosityChange, verbosityLabel } from "./verbosity"
type PanelEntry = RunFooterMenuItem & {
category: string
@@ -79,7 +80,7 @@ type SubagentEntry = PanelEntry & {
}
type SettingEntry = PanelEntry & {
key: keyof MiniSettings
key: keyof MiniSettings | "verbosity"
}
const PANEL_PAD = 2
@@ -726,8 +727,16 @@ export function RunSettingsBody(props: {
mono?: boolean
animations?: boolean
}) {
const [saving, setSaving] = createSignal<keyof MiniSettings>()
const [saving, setSaving] = createSignal<SettingEntry["key"]>()
const entries = createMemo<SettingEntry[]>(() => [
{
category: "Transcript",
display: "Verbosity",
footer: saving() === "verbosity" ? "saving" : verbosityLabel(matchMiniVerbosity(props.settings())),
footerTone: saving() === "verbosity" ? "running" : "selection",
keywords: `verbosity quiet default everything custom noise ${verbosityLabel(matchMiniVerbosity(props.settings()))}`,
key: "verbosity",
},
{
category: "Transcript",
display: "Thinking",
@@ -736,6 +745,14 @@ export function RunSettingsBody(props: {
keywords: `thinking reasoning ${props.settings().thinking}`,
key: "thinking",
},
{
category: "Transcript",
display: "Tools",
footer: saving() === "tools" ? "saving" : props.settings().tools,
footerTone: saving() === "tools" ? "running" : "selection",
keywords: `tools files skills activity work steps intermediate ${props.settings().tools}`,
key: "tools",
},
{
category: "Transcript",
display: "Shell",
@@ -800,18 +817,21 @@ export function RunSettingsBody(props: {
const change = (item: SettingEntry, direction = 1) => {
if (saving()) return
const spinners = Config.MiniWorkSpinner.literals
const next: MiniSettingChange =
item.key === "work_spinner"
? {
key: "work_spinner",
value:
spinners[
(spinners.indexOf(props.settings().work_spinner) + direction + spinners.length) % spinners.length
]!,
}
: item.key === "mono"
? { key: "mono", value: !props.settings().mono }
: { key: item.key, value: props.settings()[item.key] === "show" ? "hide" : "show" }
const next: MiniSettingChange | undefined =
item.key === "verbosity"
? verbosityChange(props.settings(), direction < 0 ? -1 : 1)
: item.key === "work_spinner"
? {
key: "work_spinner",
value:
spinners[
(spinners.indexOf(props.settings().work_spinner) + direction + spinners.length) % spinners.length
]!,
}
: item.key === "mono"
? { key: "mono", value: !props.settings().mono }
: { key: item.key, value: props.settings()[item.key] === "show" ? "hide" : "show" }
if (!next) return
setSaving(item.key)
void Promise.resolve(props.onChange(next))
.catch(() => {})
+5
View File
@@ -603,6 +603,7 @@ export class RunFooter implements FooterApi {
}
this.finishStartup()
if (this.miniSettings().tools === "hide" && toolTranscript(commit)) return
const last = this.queue.at(-1)
const merged = last ? coalesceProgressCommit(last, commit) : undefined
if (merged) this.queue[this.queue.length - 1] = merged
@@ -1168,6 +1169,10 @@ export class RunFooter implements FooterApi {
}
}
function toolTranscript(commit: StreamCommit) {
return commit.kind === "tool" || commit.partID?.startsWith("skill:")
}
/** @internal Exported for queue identity regression tests. */
export function coalesceProgressCommit(previous: StreamCommit, current: StreamCommit): StreamCommit | undefined {
if (
+2 -1
View File
@@ -86,7 +86,8 @@ export async function resolveRunTuiConfig(
export function resolveMiniSettings(config?: { mini?: Partial<MiniSettings> }): MiniSettings {
return {
thinking: config?.mini?.thinking ?? "hide",
thinking: config?.mini?.thinking ?? "show",
tools: config?.mini?.tools ?? "hide",
shell_output: config?.mini?.shell_output ?? "hide",
turn_summary: config?.mini?.turn_summary ?? "show",
footer: config?.mini?.footer ?? "show",
+6
View File
@@ -22,6 +22,7 @@ import {
} from "./runtime.boot"
import { createRuntimeLifecycle } from "./runtime.lifecycle"
import { cycleVariant, formatModelLabel, resolveVariant } from "./variant.shared"
import { verbosityPreset } from "./verbosity"
import type {
LocalReplayRow,
MiniHost,
@@ -255,6 +256,10 @@ async function runInteractiveRuntime(input: RunRuntimeInput, deps: RunRuntimeDep
? async (change) => {
const info = await config.update((draft) => {
if (!draft.mini || typeof draft.mini !== "object") draft.mini = {}
if (change.key === "verbosity") {
Object.assign(draft.mini, verbosityPreset(change.value))
return
}
draft.mini[change.key] = change.value
})
configState.current = resolveMiniSettings(info)
@@ -793,6 +798,7 @@ async function runInteractiveRuntime(input: RunRuntimeInput, deps: RunRuntimeDep
location: state.location,
sessionID: state.sessionID,
thinking: thinking(),
tools: configState.current.tools === "show",
replay: input.replay,
replayLimit: input.replayLimit,
footer,
+67 -5
View File
@@ -47,6 +47,7 @@ type StreamInput = {
location?: LocationRef
sessionID: string
thinking: boolean
tools?: boolean
replay?: boolean
replayLimit?: number
footer: FooterApi
@@ -141,6 +142,8 @@ type State = {
tools: Map<string, ToolState>
toolSources: Map<string, SessionMessageAssistantTool>
finishedTools: Set<string>
toolMessages: Set<string>
quietText: Map<string, Array<{ partID: string; text: string }>>
skillMessages: Set<string>
shellCommands: Map<string, string>
shellStarted: Set<string>
@@ -488,6 +491,8 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
tools: new Map(),
toolSources: new Map(),
finishedTools: new Set(),
toolMessages: new Set(),
quietText: new Map(),
skillMessages: new Set(),
shellCommands: new Map(),
shellStarted: new Set(),
@@ -592,7 +597,7 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
const images = freshImages(userImageCommits(messageID, files), render)
if (!render) return
write([
...(!visible ? skillCommits(messageID, skills) : []),
...(!visible && showTools() ? skillCommits(messageID, skills) : []),
...(!visible && text.trim()
? [{ kind: "user", source: "system", text, phase: "start", messageID } as const]
: []),
@@ -662,7 +667,48 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
}
}
const showTools = () => input.tools !== false
const rememberToolMessage = (messageID: string) => {
state.toolMessages.add(messageID)
state.quietText.delete(messageID)
}
const bufferQuietText = (messageID: string, partID: string, text: string, replace: boolean) => {
if (!text || state.toolMessages.has(messageID)) return
const parts = state.quietText.get(messageID) ?? []
const current = parts.find((part) => part.partID === partID)
if (current) {
current.text = replace ? text : current.text + text
return
}
parts.push({ partID, text })
state.quietText.set(messageID, parts)
}
const flushQuietText = (messageID: string) => {
const parts = state.quietText.get(messageID)
state.quietText.delete(messageID)
if (!parts || state.toolMessages.has(messageID)) return
write(
parts
.filter((part) => part.text)
.map((part) => ({
kind: "assistant" as const,
source: "assistant" as const,
text: part.text,
phase: "progress" as const,
messageID,
partID: part.partID,
})),
)
}
const renderTool = (messageID: string, item: SessionMessageAssistantTool, render = true) => {
if (!showTools()) {
rememberToolMessage(messageID)
render = false
}
const part = normalizeTool(item)
const key = permissionSourceKey(messageID, part.id)
if (state.finishedTools.has(key)) {
@@ -721,7 +767,7 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
}
if (message.type === "skill") {
if (state.wait?.messageID === message.id) promoteWait(state.wait, false)
if (!render || state.skillMessages.has(message.id)) {
if (!render || !showTools() || state.skillMessages.has(message.id)) {
state.skillMessages.add(message.id)
return
}
@@ -784,13 +830,16 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
}
if (message.type !== "assistant") return
state.messageIDs.add(message.id)
const hasTools = message.content.some((item) => item.type === "tool")
if (hasTools) rememberToolMessage(message.id)
let textOrdinal = 0
let reasoningOrdinal = 0
for (const item of message.content) {
if (item.type === "text") {
const fragment = fragmentRef(message.id, "text", textOrdinal++)
const update = state.fragments.project(fragment, item.text, render)
if (render && item.text.length > update.previous.length)
const visible = render && (showTools() || !hasTools)
const update = state.fragments.project(fragment, item.text, visible)
if (visible && item.text.length > update.previous.length)
write([
{
kind: "assistant",
@@ -1024,7 +1073,7 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
if (state.wait?.messageID === messageID) promoteWait(state.wait, true)
if (state.skillMessages.has(messageID)) return
state.skillMessages.add(messageID)
write([skillCommit(messageID, event.data.name)])
if (showTools()) write([skillCommit(messageID, event.data.name)])
return
}
if (event.type === "session.compaction.started") {
@@ -1113,6 +1162,10 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
if (event.type === "session.text.delta") {
const fragment = fragmentRef(event.data.assistantMessageID, "text", event.data.ordinal)
if (!state.fragments.delta(fragment, event.data.delta)) return
if (!showTools()) {
bufferQuietText(event.data.assistantMessageID, fragment.partID, event.data.delta, false)
return
}
write([
{
kind: "assistant",
@@ -1130,6 +1183,10 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
fragmentRef(event.data.assistantMessageID, "text", event.data.ordinal),
event.data.text,
)
if (!showTools()) {
bufferQuietText(event.data.assistantMessageID, update.partID, event.data.text, true)
return
}
if (event.data.text.length > update.previous.length)
write([
{
@@ -1303,6 +1360,7 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
event.data.tokens.cache.write
const limit = state.stepModel ? input.contextLimit?.(state.stepModel) : undefined
state.stepModel = undefined
if (!showTools()) flushQuietText(event.data.assistantMessageID)
write([], {
usage:
total > 0 || event.data.cost
@@ -1317,6 +1375,7 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
}
if (event.type === "session.step.failed") {
state.stepModel = undefined
if (!showTools()) flushQuietText(event.data.assistantMessageID)
const rendered = state.errors.has(event.data.assistantMessageID)
state.errors.add(event.data.assistantMessageID)
if (state.wait) state.wait.failureRendered = true
@@ -1344,6 +1403,7 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
event.type === "session.execution.interrupted"
) {
state.executionEpoch++
if (!showTools()) for (const messageID of [...state.quietText.keys()]) flushQuietText(messageID)
paintIdle("")
const current = state.wait
if (!current) return
@@ -1605,6 +1665,8 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
state.tools.clear()
state.toolSources.clear()
state.finishedTools.clear()
state.toolMessages.clear()
state.quietText.clear()
state.skillMessages.clear()
state.shellCommands.clear()
state.shellStarted.clear()
+8 -3
View File
@@ -402,6 +402,7 @@ export type RunTuiConfig = Pick<
export type MiniSettings = {
thinking: "show" | "hide"
tools: "show" | "hide"
shell_output: "show" | "hide"
turn_summary: "show" | "hide"
footer: "show" | "hide"
@@ -410,9 +411,13 @@ export type MiniSettings = {
mono: boolean
}
export type MiniSettingChange = {
[Key in keyof MiniSettings]: { key: Key; value: MiniSettings[Key] }
}[keyof MiniSettings]
export type MiniVerbosity = "quiet" | "default" | "everything"
export type MiniSettingChange =
| {
[Key in keyof MiniSettings]: { key: Key; value: MiniSettings[Key] }
}[keyof MiniSettings]
| { key: "verbosity"; value: MiniVerbosity }
// Lifecycle phase of a scrollback entry. "start" opens the entry, "progress"
// appends content (coalesced in the footer queue), "final" closes it.
+84
View File
@@ -0,0 +1,84 @@
import type { MiniSettingChange, MiniSettings, MiniVerbosity } from "./types"
const levels: readonly MiniVerbosity[] = ["quiet", "default", "everything"]
const knobs = ["thinking", "tools", "shell_output", "turn_summary", "footer"] as const
type VerbosityKnobs = Pick<MiniSettings, (typeof knobs)[number]>
const presets = {
quiet: {
thinking: "hide",
tools: "hide",
shell_output: "hide",
turn_summary: "hide",
footer: "hide",
},
default: {
thinking: "show",
tools: "hide",
shell_output: "hide",
turn_summary: "show",
footer: "show",
},
everything: {
thinking: "show",
tools: "show",
shell_output: "show",
turn_summary: "show",
footer: "show",
},
} as const satisfies Record<MiniVerbosity, VerbosityKnobs>
const labels = {
quiet: "Quiet",
default: "Default",
everything: "Everything",
custom: "Custom",
} as const
export function verbosityPreset(level: MiniVerbosity): VerbosityKnobs {
return { ...presets[level] }
}
export function verbosityLabel(level: MiniVerbosity | "custom") {
return labels[level]
}
export function matchMiniVerbosity(settings: MiniSettings): MiniVerbosity | "custom" {
for (const level of levels) {
if (knobs.every((key) => settings[key] === presets[level][key])) return level
}
return "custom"
}
export function cycleMiniVerbosity(settings: MiniSettings, direction: 1 | -1): MiniVerbosity {
const current = matchMiniVerbosity(settings)
const index = current === "custom" ? nearestMiniVerbosity(settings) : levels.indexOf(current)
const next = index + direction
if (next < 0) return levels[0]!
if (next >= levels.length) return levels[levels.length - 1]!
return levels[next]!
}
export function verbosityChange(settings: MiniSettings, direction: 1 | -1): MiniSettingChange | undefined {
const value = cycleMiniVerbosity(settings, direction)
if (matchMiniVerbosity(settings) === value) return
return { key: "verbosity", value }
}
export function applyMiniSettingChange(settings: MiniSettings, change: MiniSettingChange): MiniSettings {
if (change.key === "verbosity") return { ...settings, ...verbosityPreset(change.value) }
return { ...settings, [change.key]: change.value }
}
function nearestMiniVerbosity(settings: MiniSettings) {
return levels.reduce((best, level, index) => {
if (verbosityDistance(settings, level) < verbosityDistance(settings, levels[best]!)) return index
return best
}, 0)
}
function verbosityDistance(settings: MiniSettings, level: MiniVerbosity) {
return knobs.filter((key) => settings[key] !== presets[level][key]).length
}
+3 -2
View File
@@ -18,9 +18,10 @@ test("validates the three explicit diff source defaults", () => {
})
test("validates mini replay and work spinner settings", () => {
expect(decodeInfo({ mini: { replay: false, replay_limit: 50 } })).toEqual({
mini: { replay: false, replay_limit: 50 },
expect(decodeInfo({ mini: { tools: "hide", replay: false, replay_limit: 50 } })).toEqual({
mini: { tools: "hide", replay: false, replay_limit: 50 },
})
expect(() => decodeInfo({ mini: { tools: "quiet" } })).toThrow()
expect(() => decodeInfo({ mini: { replay_limit: 0 } })).toThrow()
expect(() => decodeInfo({ mini: { replay_limit: 1.5 } })).toThrow()
expect(decodeInfo({ mini: { work_spinner: "quadrant-orbit" } })).toEqual({
@@ -379,6 +379,7 @@ test.each(["once", "always", "reject"] as const)(
miniSettings: {
current: {
thinking: "hide",
tools: "show",
shell_output: "hide",
turn_summary: "hide",
footer: "show",
@@ -67,6 +67,7 @@ async function renderSubagent(interrupt: "ctrl+i" | "none") {
tuiConfig={config}
miniSettings={() => ({
thinking: "hide",
tools: "show",
shell_output: "hide",
turn_summary: "show",
footer: "show",
+48 -1
View File
@@ -34,6 +34,52 @@ test("coalesces progress only within the same message and tool state", () => {
)
})
test.each(["show", "hide"] as const)("tools setting %s controls tool and skill transcript", async (tools) => {
const app = await setup({ tools })
try {
app.footer.append({ kind: "user", text: "do the thing", phase: "start", source: "system" })
app.footer.append({
kind: "tool",
text: "running read",
phase: "start",
source: "tool",
tool: "read",
messageID: "msg_1",
partID: "prt_1",
})
app.footer.append({
kind: "system",
text: `→ Skill "demo"`,
phase: "start",
source: "system",
messageID: "msg_skill",
partID: "skill:demo",
})
app.footer.append({
kind: "assistant",
text: "all done",
phase: "progress",
source: "assistant",
messageID: "msg_2",
partID: "prt_text",
})
await app.footer.idle()
const text = app.externalOutput.takeText()
expect(text).toContain("do the thing")
expect(text).toContain("all done")
if (tools === "hide") {
expect(text).not.toContain("Read")
expect(text).not.toContain("Skill")
return
}
expect(text).toContain("-> Read")
expect(text).toContain("Skill")
} finally {
app.footer.destroy()
app.renderer.destroy()
}
})
test("falls back only when no agent is selected", () => {
const agents: RunAgent[] = [
{ id: "task", name: "Task", mode: "subagent", hidden: false },
@@ -50,6 +96,7 @@ test("falls back only when no agent is selected", () => {
async function setup(
input: {
mono?: boolean
tools?: MiniSettings["tools"]
theme?: RunTuiConfig["theme"]
startup?: { version: string; detail: string }
cursorRow?: number
@@ -93,7 +140,7 @@ async function setup(
theme: mono ? RUN_THEME_MONO : RUN_THEME_FALLBACK,
tuiConfig: createTuiResolvedConfig({ theme: input.theme }),
miniSettings: {
current: { ...resolveMiniSettings(), mono },
current: { ...resolveMiniSettings(), mono, ...(input.tools ? { tools: input.tools } : {}) },
update: input.update,
},
onPermissionReply: () => {},
+32 -3
View File
@@ -24,6 +24,7 @@ import { RunEntryContent } from "../../src/mini/scrollback.writer"
import { RUN_THEME_FALLBACK, RUN_THEME_FALLBACK_LIGHT, resolveRunTheme, type RunTheme } from "../../src/mini/theme"
import { BLOCK_SOFT_SLIDE, SEED_MONO, WORK_SPINNERS } from "../../src/ui/one-cell-motion"
import { resolveMiniSettings } from "../../src/mini/runtime.boot"
import { applyMiniSettingChange } from "../../src/mini/verbosity"
import type {
FooterQueuedPrompt,
FooterState,
@@ -936,7 +937,7 @@ test.each([false, true])("settings change preferences and preview the work spinn
settings={settings}
onClose={() => {}}
onChange={(change) => {
setSettings((current) => ({ ...current, [change.key]: change.value }))
setSettings((current) => applyMiniSettingChange(current, change))
}}
mono={mono}
animations={animations()}
@@ -951,7 +952,9 @@ test.each([false, true])("settings change preferences and preview the work spinn
const frame = app.captureCharFrame()
expect(frame).toContain("Settings")
expect(frame).toMatch(/^ +Settings/m)
expect(frame).toContain("Verbosity")
expect(frame).toContain("Thinking")
expect(frame).toContain("Tools")
expect(frame).toContain("Shell")
expect(frame).toContain("Turn summary")
expect(frame).toContain("Footer details")
@@ -960,14 +963,37 @@ test.each([false, true])("settings change preferences and preview the work spinn
expect(frame).toContain("left/right change")
if (mono) expect(frame).not.toMatch(/[^\x00-\x7F]/)
app.mockInput.pressKey("ARROW_RIGHT")
await app.renderOnce()
expect(settings()).toEqual(applyMiniSettingChange(resolveMiniSettings(), { key: "verbosity", value: "everything" }))
app.mockInput.pressKey("ARROW_LEFT")
await app.renderOnce()
expect(settings()).toEqual(resolveMiniSettings())
app.mockInput.pressKey("ARROW_LEFT")
await app.renderOnce()
expect(settings()).toEqual(applyMiniSettingChange(resolveMiniSettings(), { key: "verbosity", value: "quiet" }))
app.mockInput.pressKey("ARROW_LEFT")
await app.renderOnce()
expect(settings()).toEqual(applyMiniSettingChange(resolveMiniSettings(), { key: "verbosity", value: "quiet" }))
app.mockInput.pressKey("ARROW_RIGHT")
await app.renderOnce()
expect(settings()).toEqual(resolveMiniSettings())
app.mockInput.pressKey("ARROW_DOWN")
app.mockInput.pressKey("ARROW_RIGHT")
await app.renderOnce()
expect(settings()).toEqual({
...resolveMiniSettings(),
thinking: "show",
thinking: "hide",
})
expect(app.captureCharFrame()).toContain("Custom")
app.mockInput.pressKey("ARROW_DOWN")
app.mockInput.pressKey("ARROW_DOWN")
app.mockInput.pressKey("ARROW_DOWN")
app.mockInput.pressKey("ARROW_RIGHT")
@@ -975,7 +1001,7 @@ test.each([false, true])("settings change preferences and preview the work spinn
expect(settings()).toEqual({
...resolveMiniSettings(),
thinking: "show",
thinking: "hide",
turn_summary: "hide",
})
@@ -1987,6 +2013,7 @@ test.each([8, 12])("production footer grows for wrapped instructions in %i rows"
miniSettings: {
current: {
thinking: "hide",
tools: "show",
shell_output: "hide",
turn_summary: "show",
footer: "show",
@@ -2054,6 +2081,7 @@ test.each(["ctrl+i", "none"])("takeovers preserve configured shortcuts with hidd
state: { phase: "running", interrupt: 1 },
miniSettings: {
thinking: "hide",
tools: "show",
shell_output: "hide",
turn_summary: "show",
footer: "hide",
@@ -2371,6 +2399,7 @@ test("direct footer hides routine activity and shows explicit notices", async ()
currentAgent: "Plan",
miniSettings: {
thinking: "hide",
tools: "show",
shell_output: "hide",
turn_summary: "show",
footer: "hide",
+4 -1
View File
@@ -39,7 +39,8 @@ describe("run runtime boot", () => {
expect(result.leader.timeout).toBe(450)
expect(result.cursor).toEqual({ style: "underline", blinking: false })
expect(resolveMiniSettings(result)).toEqual({
thinking: "hide",
thinking: "show",
tools: "hide",
shell_output: "hide",
turn_summary: "show",
footer: "show",
@@ -51,6 +52,7 @@ describe("run runtime boot", () => {
resolveMiniSettings({
mini: {
thinking: "show",
tools: "hide",
shell_output: "show",
turn_summary: "hide",
footer: "hide",
@@ -61,6 +63,7 @@ describe("run runtime boot", () => {
}),
).toEqual({
thinking: "show",
tools: "hide",
shell_output: "show",
turn_summary: "hide",
footer: "hide",
@@ -635,6 +635,135 @@ describe("V2 mini transport", () => {
await transport.close()
})
test("hides tool-side assistant narration when tools are disabled", async () => {
const events = feed()
events.push(connected())
const ui = footer()
const transport = await createSessionTransport({
sdk: sdk({ streams: [events], messages: { ses_1: [] } }),
sessionID: "ses_1",
thinking: false,
tools: false,
footer: ui.api,
})
const tokens = { input: 1, output: 1, reasoning: 0, cache: { read: 0, write: 0 } }
events.push({
id: "evt_work_text",
created: 1,
type: "session.text.delta",
data: {
sessionID: "ses_1",
assistantMessageID: "msg_work",
ordinal: 0,
delta: "I'll check.",
},
})
events.push({
id: "evt_tool_start",
created: 2,
type: "session.tool.input.started",
durable: durable("ses_1", 1),
data: { sessionID: "ses_1", assistantMessageID: "msg_work", id: "call_read", name: "read" },
})
events.push({
id: "evt_tool_called",
created: 3,
type: "session.tool.called",
durable: durable("ses_1", 2),
data: { sessionID: "ses_1", assistantMessageID: "msg_work", id: "call_read", input: {}, executed: true },
})
events.push({
id: "evt_work_step",
created: 4,
type: "session.step.ended",
durable: durable("ses_1", 3),
data: {
sessionID: "ses_1",
assistantMessageID: "msg_work",
finish: "tool-calls",
cost: 0,
tokens,
},
})
events.push({
id: "evt_final_text",
created: 5,
type: "session.text.delta",
data: {
sessionID: "ses_1",
assistantMessageID: "msg_final",
ordinal: 0,
delta: "Done.",
},
})
events.push({
id: "evt_final_step",
created: 6,
type: "session.step.ended",
durable: durable("ses_1", 4),
data: {
sessionID: "ses_1",
assistantMessageID: "msg_final",
finish: "stop",
cost: 0,
tokens,
},
})
while (!ui.commits.some((commit) => commit.text === "Done.")) await Bun.sleep(0)
expect(ui.commits.filter((commit) => commit.kind === "assistant" || commit.kind === "tool").map((commit) => commit.text)).toEqual([
"Done.",
])
await transport.close()
})
test("hides tool-side assistant narration from hydrated history when tools are disabled", async () => {
const events = feed()
events.push(connected())
const ui = footer()
const transport = await createSessionTransport({
sdk: sdk({
streams: [events],
messages: {
ses_1: [
{
id: "msg_final",
type: "assistant",
agent: "build",
model: { providerID: "test", id: "model" },
content: [{ type: "text", text: "Done." }],
time: { created: 4, completed: 5 },
},
{
id: "msg_work",
type: "assistant",
agent: "build",
model: { providerID: "test", id: "model" },
content: [
{ type: "text", text: "I'll check." },
canonicalToolPart("read", { status: "completed", input: {}, content: [{ type: "text", text: "file" }] }),
],
time: { created: 2, completed: 3 },
},
{ id: "msg_user", type: "user", text: "what happened", files: [], agents: [], time: { created: 1 } },
],
},
}),
sessionID: "ses_1",
thinking: false,
tools: false,
replay: true,
footer: ui.api,
})
while (!ui.commits.some((commit) => commit.text === "Done.")) await Bun.sleep(0)
expect(
ui.commits.filter((commit) => commit.kind === "user" || commit.kind === "assistant" || commit.kind === "tool").map((commit) => commit.text),
).toEqual(["what happened", "Done."])
await transport.close()
})
test("recursively hydrates blockers for direct and transitive descendants", async () => {
const events = feed()
events.push(connected())
+53
View File
@@ -0,0 +1,53 @@
import { expect, test } from "bun:test"
import { resolveMiniSettings } from "../../src/mini/runtime.boot"
import {
applyMiniSettingChange,
cycleMiniVerbosity,
matchMiniVerbosity,
verbosityChange,
verbosityLabel,
verbosityPreset,
} from "../../src/mini/verbosity"
test("default Mini settings match the default verbosity preset", () => {
expect(matchMiniVerbosity(resolveMiniSettings())).toBe("default")
})
test("verbosity presets leave splash, spinner, and mono alone", () => {
const current = resolveMiniSettings({ mini: { splash: "hide", work_spinner: "seed", mono: true } })
const quiet = applyMiniSettingChange(current, { key: "verbosity", value: "quiet" })
expect(quiet).toEqual({
...current,
...verbosityPreset("quiet"),
})
expect(quiet.splash).toBe("hide")
expect(quiet.footer).toBe("hide")
expect(applyMiniSettingChange(current, { key: "verbosity", value: "everything" }).mono).toBe(true)
expect(applyMiniSettingChange(current, { key: "thinking", value: "show" }).thinking).toBe("show")
})
test("individual knobs mark verbosity custom until a preset matches again", () => {
const louder = applyMiniSettingChange(resolveMiniSettings(), { key: "tools", value: "show" })
expect(matchMiniVerbosity(louder)).toBe("custom")
expect(verbosityLabel("custom")).toBe("Custom")
expect(matchMiniVerbosity(applyMiniSettingChange(louder, { key: "verbosity", value: "quiet" }))).toBe("quiet")
})
test("verbosity cycles like a clamped slider", () => {
const settings = resolveMiniSettings()
expect(cycleMiniVerbosity(settings, 1)).toBe("everything")
expect(cycleMiniVerbosity(settings, -1)).toBe("quiet")
expect(cycleMiniVerbosity({ ...settings, ...verbosityPreset("quiet") }, -1)).toBe("quiet")
expect(cycleMiniVerbosity({ ...settings, ...verbosityPreset("everything") }, 1)).toBe("everything")
expect(verbosityChange({ ...settings, ...verbosityPreset("quiet") }, -1)).toBeUndefined()
expect(verbosityChange(settings, 1)).toEqual({ key: "verbosity", value: "everything" })
expect(verbosityLabel("quiet")).toBe("Quiet")
expect(verbosityLabel("everything")).toBe("Everything")
})
test("custom verbosity moves toward the nearest preset", () => {
const custom = applyMiniSettingChange(resolveMiniSettings(), { key: "tools", value: "show" })
expect(matchMiniVerbosity(custom)).toBe("custom")
expect(cycleMiniVerbosity(custom, 1)).toBe("everything")
expect(cycleMiniVerbosity(custom, -1)).toBe("quiet")
})