mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-14 20:21:39 +03:00
mini: add more minimal output presets. (#47931)
This commit is contained in:
committed by
GitHub
Unverified
parent
4fef8edbe8
commit
2eea36e731
@@ -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",
|
||||
}),
|
||||
|
||||
@@ -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(() => {})
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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: () => {},
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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")
|
||||
})
|
||||
Reference in New Issue
Block a user