mini: add monochrome ASCII mode (#38173)
This commit is contained in:
parent
7ec1b6e580
commit
e8d273ae5b
26 changed files with 550 additions and 292 deletions
|
|
@ -43,20 +43,21 @@ function structured(next: StreamCommit) {
|
|||
|
||||
describe("run entry body", () => {
|
||||
test("renders a failed direct shell as an error instead of completed success", () => {
|
||||
expect(
|
||||
entryBody(
|
||||
commit({
|
||||
kind: "tool",
|
||||
text: "Shell exited with code 7",
|
||||
phase: "final",
|
||||
source: "tool",
|
||||
tool: "shell",
|
||||
toolState: "error",
|
||||
toolError: "Shell exited with code 7",
|
||||
shell: { command: "false" },
|
||||
}),
|
||||
),
|
||||
).toEqual({ type: "text", content: "✖ shell failed: Shell exited with code 7" })
|
||||
const failed = commit({
|
||||
kind: "tool",
|
||||
text: "Shell exited with code 7",
|
||||
phase: "final",
|
||||
source: "tool",
|
||||
tool: "shell",
|
||||
toolState: "error",
|
||||
toolError: "Shell → exited · code 7",
|
||||
shell: { command: "false" },
|
||||
})
|
||||
expect(entryBody(failed)).toEqual({ type: "text", content: "✖ shell failed: Shell → exited · code 7" })
|
||||
expect(entryBody(failed, { mono: true })).toEqual({
|
||||
type: "text",
|
||||
content: "! shell failed: Shell → exited · code 7",
|
||||
})
|
||||
})
|
||||
|
||||
test("renders assistant, reasoning, and user entries in their display formats", () => {
|
||||
|
|
@ -114,6 +115,11 @@ describe("run entry body", () => {
|
|||
type: "text",
|
||||
content: "› Inspect footer tabs",
|
||||
})
|
||||
expect(
|
||||
entryBody(commit({ kind: "user", text: "Inspect footer tabs", phase: "start", source: "system" }), {
|
||||
mono: true,
|
||||
}),
|
||||
).toEqual({ type: "text", content: "> Inspect footer tabs" })
|
||||
})
|
||||
|
||||
for (const item of [
|
||||
|
|
|
|||
|
|
@ -55,7 +55,8 @@ test("down opens subagents from an empty prompt", async () => {
|
|||
subagent={subagents}
|
||||
theme={() => RUN_THEME_FALLBACK}
|
||||
tuiConfig={config}
|
||||
miniSettings={() => ({ thinking: "hide", shell_output: "hide", turn_summary: "show" })}
|
||||
miniSettings={() => ({ thinking: "hide", shell_output: "hide", turn_summary: "show", mono: false })}
|
||||
mono={false}
|
||||
onSubmit={() => true}
|
||||
onPermissionReply={() => {}}
|
||||
onFormReply={() => {}}
|
||||
|
|
|
|||
|
|
@ -121,6 +121,7 @@ async function renderFooter(
|
|||
view?: FooterView
|
||||
onFormReply?: (input: unknown) => void
|
||||
miniSettings?: MiniSettings
|
||||
mono?: boolean
|
||||
onMiniSettingChange?: (change: MiniSettingChange) => void
|
||||
} = {},
|
||||
) {
|
||||
|
|
@ -131,7 +132,7 @@ async function renderFooter(
|
|||
const state = footerState(input.state)
|
||||
const config = input.tuiConfig ?? tuiConfig
|
||||
const [miniSettings] = createSignal<MiniSettings>(
|
||||
input.miniSettings ?? { thinking: "hide", shell_output: "hide", turn_summary: "show" },
|
||||
input.miniSettings ?? { thinking: "hide", shell_output: "hide", turn_summary: "show", mono: false },
|
||||
)
|
||||
function Harness() {
|
||||
return (
|
||||
|
|
@ -150,6 +151,7 @@ async function renderFooter(
|
|||
view={view}
|
||||
subagent={subagents}
|
||||
theme={input.theme ?? (() => RUN_THEME_FALLBACK)}
|
||||
mono={input.mono ?? false}
|
||||
tuiConfig={config}
|
||||
miniSettings={miniSettings}
|
||||
onSubmit={input.onSubmit ?? (() => true)}
|
||||
|
|
@ -397,6 +399,7 @@ test("direct command panel renders grouped command palette", async () => {
|
|||
const frame = app.captureCharFrame()
|
||||
|
||||
expect(frame).toContain("Commands")
|
||||
expect(frame).toMatch(/^ {2}Commands/m)
|
||||
expect(frame).toContain("Search")
|
||||
expect(frame).toContain("Session")
|
||||
expect(frame).toContain("Agent")
|
||||
|
|
@ -424,6 +427,7 @@ test("direct settings panel changes Mini transcript preferences", async () => {
|
|||
thinking: "hide",
|
||||
shell_output: "hide",
|
||||
turn_summary: "show",
|
||||
mono: false,
|
||||
})
|
||||
const app = await testRender(
|
||||
() => (
|
||||
|
|
@ -435,6 +439,7 @@ test("direct settings panel changes Mini transcript preferences", async () => {
|
|||
onChange={(change) => {
|
||||
setSettings((current) => ({ ...current, [change.key]: change.value }))
|
||||
}}
|
||||
mono
|
||||
/>
|
||||
</box>
|
||||
),
|
||||
|
|
@ -443,23 +448,32 @@ test("direct settings panel changes Mini transcript preferences", async () => {
|
|||
|
||||
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("Turn summary")
|
||||
expect(app.captureCharFrame()).toContain("left/right change")
|
||||
const frame = app.captureCharFrame()
|
||||
expect(frame).toContain("Settings")
|
||||
expect(frame).toMatch(/^ Settings/m)
|
||||
expect(frame).toContain("Thinking")
|
||||
expect(frame).toContain("Shell")
|
||||
expect(frame).toContain("Turn summary")
|
||||
expect(frame).toContain("Monochrome UI")
|
||||
expect(frame).toContain("left/right change")
|
||||
expect(frame).not.toMatch(/[^\x00-\x7F]/)
|
||||
|
||||
app.mockInput.pressKey("ARROW_RIGHT")
|
||||
await app.renderOnce()
|
||||
|
||||
expect(settings()).toEqual({ thinking: "show", shell_output: "hide", turn_summary: "show" })
|
||||
expect(settings()).toEqual({ thinking: "show", shell_output: "hide", turn_summary: "show", mono: false })
|
||||
|
||||
app.mockInput.pressKey("ARROW_DOWN")
|
||||
app.mockInput.pressKey("ARROW_DOWN")
|
||||
app.mockInput.pressKey("ARROW_RIGHT")
|
||||
await app.renderOnce()
|
||||
|
||||
expect(settings()).toEqual({ thinking: "show", shell_output: "hide", turn_summary: "hide" })
|
||||
expect(settings()).toEqual({ thinking: "show", shell_output: "hide", turn_summary: "hide", mono: false })
|
||||
|
||||
app.mockInput.pressKey("ARROW_DOWN")
|
||||
app.mockInput.pressKey("ARROW_RIGHT")
|
||||
await app.renderOnce()
|
||||
expect(settings().mono).toBe(true)
|
||||
} finally {
|
||||
app.renderer.destroy()
|
||||
}
|
||||
|
|
@ -935,11 +949,11 @@ test("direct footer closes settings with ctrl-c instead of arming exit", async (
|
|||
await app.renderOnce()
|
||||
app.mockInput.pressEnter()
|
||||
await app.renderOnce()
|
||||
expect(app.captureCharFrame()).toContain("Shell tool output")
|
||||
expect(app.captureCharFrame()).toContain("Shell")
|
||||
|
||||
app.mockInput.pressKey("c", { ctrl: true })
|
||||
await app.renderOnce()
|
||||
expect(app.captureCharFrame()).not.toContain("Shell tool output")
|
||||
expect(app.captureCharFrame()).not.toContain("Shell")
|
||||
expect(app.renderer.currentFocusedEditor?.plainText).toBe("")
|
||||
} finally {
|
||||
app.cleanup()
|
||||
|
|
@ -1113,7 +1127,8 @@ test("direct footer shows authoritative pending work while running", async () =>
|
|||
]}
|
||||
theme={() => RUN_THEME_FALLBACK}
|
||||
tuiConfig={tuiConfig}
|
||||
miniSettings={() => ({ thinking: "hide", shell_output: "hide", turn_summary: "show" })}
|
||||
miniSettings={() => ({ thinking: "hide", shell_output: "hide", turn_summary: "show", mono: false })}
|
||||
mono={false}
|
||||
onSubmit={() => true}
|
||||
onPermissionReply={() => {}}
|
||||
onFormReply={() => {}}
|
||||
|
|
@ -1255,14 +1270,17 @@ test("direct footer omits interrupt key hint when interrupt is unbound", async (
|
|||
const app = await renderFooter({
|
||||
tuiConfig: createTuiResolvedConfig({ keybinds: { session_interrupt: "none", input_clear: "ctrl+l" } }),
|
||||
state: { phase: "running" },
|
||||
mono: true,
|
||||
})
|
||||
|
||||
try {
|
||||
await app.renderOnce()
|
||||
const frame = app.captureCharFrame()
|
||||
const statusline = frame.split("\n").find((line) => line.includes("interrupt"))
|
||||
|
||||
expect(frame).toContain("interrupt")
|
||||
expect(frame).not.toContain("ctrl+l")
|
||||
expect(statusline).toMatch(/^\S/)
|
||||
} finally {
|
||||
app.cleanup()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -155,28 +155,32 @@ describe("run permission shared", () => {
|
|||
})
|
||||
|
||||
test("uses source patch text when an edit has no generated diff", () => {
|
||||
expect(
|
||||
permissionInfo(
|
||||
req({
|
||||
action: "edit",
|
||||
resources: ["src/index.ts"],
|
||||
source: { type: "tool", messageID: "msg-edit", callID: "call-edit" },
|
||||
tool: canonicalToolPart(
|
||||
"edit",
|
||||
{
|
||||
status: "running",
|
||||
input: { patchText: "*** Begin Patch\n*** Update File: src/index.ts\n@@\n-old\n+new\n*** End Patch" },
|
||||
structured: {},
|
||||
content: [],
|
||||
},
|
||||
"call-edit",
|
||||
),
|
||||
}),
|
||||
const patch = '*** Begin Patch\n*** Update File: src/index.ts\n@@\n-old\n+const arrow = "→"\n*** End Patch'
|
||||
const request = req({
|
||||
action: "edit",
|
||||
resources: ["src/index.ts"],
|
||||
source: { type: "tool", messageID: "msg-edit", callID: "call-edit" },
|
||||
tool: canonicalToolPart(
|
||||
"edit",
|
||||
{
|
||||
status: "running",
|
||||
input: { patchText: patch },
|
||||
structured: {},
|
||||
content: [],
|
||||
},
|
||||
"call-edit",
|
||||
),
|
||||
).toMatchObject({
|
||||
})
|
||||
expect(permissionInfo(request)).toMatchObject({
|
||||
title: "Edit src/index.ts",
|
||||
diff: undefined,
|
||||
patch: "*** Begin Patch\n*** Update File: src/index.ts\n@@\n-old\n+new\n*** End Patch",
|
||||
patch,
|
||||
})
|
||||
expect(permissionInfo(request, undefined, true)).toMatchObject({
|
||||
title: "Edit src/index.ts",
|
||||
lines: [patch],
|
||||
diff: undefined,
|
||||
patch: undefined,
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -103,11 +103,19 @@ describe("run runtime boot", () => {
|
|||
expect(result.theme).toEqual({ mode: "light" })
|
||||
expect(result.leader.timeout).toBe(450)
|
||||
expect(result.session?.thinking).toBe("show")
|
||||
expect(resolveMiniSettings(result)).toEqual({ thinking: "hide", shell_output: "hide", turn_summary: "show" })
|
||||
expect(resolveMiniSettings({ mini: { thinking: "show", shell_output: "show", turn_summary: "hide" } })).toEqual({
|
||||
expect(resolveMiniSettings(result)).toEqual({
|
||||
thinking: "hide",
|
||||
shell_output: "hide",
|
||||
turn_summary: "show",
|
||||
mono: false,
|
||||
})
|
||||
expect(
|
||||
resolveMiniSettings({ mini: { thinking: "show", shell_output: "show", turn_summary: "hide", mono: true } }),
|
||||
).toEqual({
|
||||
thinking: "show",
|
||||
shell_output: "show",
|
||||
turn_summary: "hide",
|
||||
mono: true,
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { expect, test } from "bun:test"
|
||||
import { RGBA, type CliRenderer, type TerminalColors } from "@opentui/core"
|
||||
import { RUN_THEME_FALLBACK, generateSystem, resolveRunTheme, resolveTheme } from "../../src/mini/theme"
|
||||
import { RUN_THEME_MONO, RUN_THEME_FALLBACK, generateSystem, resolveRunTheme, resolveTheme } from "../../src/mini/theme"
|
||||
import { DEFAULT_THEMES } from "../../src/theme"
|
||||
|
||||
const palette = ["#15161e", "#f7768e", "#9ece6a", "#e0af68", "#7aa2f7", "#bb9af7", "#7dcfff", "#c0caf5"] as const
|
||||
|
|
@ -23,12 +23,14 @@ function terminalColors(input: Partial<TerminalColors> = {}): TerminalColors {
|
|||
function renderer(
|
||||
input: {
|
||||
themeMode?: "dark" | "light"
|
||||
resolvedThemeMode?: "dark" | "light"
|
||||
colors?: TerminalColors
|
||||
fail?: boolean
|
||||
} = {},
|
||||
) {
|
||||
return {
|
||||
themeMode: input.themeMode,
|
||||
waitForThemeMode: async () => input.resolvedThemeMode ?? input.themeMode ?? null,
|
||||
getPalette: async () => {
|
||||
if (input.fail) {
|
||||
throw new Error("boom")
|
||||
|
|
@ -61,6 +63,21 @@ function spread(color: RGBA) {
|
|||
|
||||
test("falls back when palette lookup fails", async () => {
|
||||
expect(await resolveRunTheme(renderer({ fail: true }))).toBe(RUN_THEME_FALLBACK)
|
||||
expect(await resolveRunTheme(renderer({ fail: true }), undefined, true)).toBe(RUN_THEME_MONO)
|
||||
const light = await resolveRunTheme(renderer({ resolvedThemeMode: "light" }), undefined, true)
|
||||
expect(expectRgba(light.footer.text).toInts().slice(0, 3)).toEqual([0, 0, 0])
|
||||
expect(RUN_THEME_MONO.block.syntax).toBeUndefined()
|
||||
for (const color of [
|
||||
RUN_THEME_MONO.background,
|
||||
...Object.values(RUN_THEME_MONO.footer),
|
||||
...Object.values(RUN_THEME_MONO.splash),
|
||||
...Object.values(RUN_THEME_MONO.entry).flatMap((tone) => [tone.body, tone.start].filter(Boolean)),
|
||||
...Object.entries(RUN_THEME_MONO.block)
|
||||
.filter(([key]) => key !== "syntax")
|
||||
.map(([, value]) => value),
|
||||
]) {
|
||||
expect(expectRgba(color).intent).toBe("default")
|
||||
}
|
||||
})
|
||||
|
||||
test("resolveTheme preserves Mini indexed color and result shape semantics", () => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue