mini: add monochrome ASCII mode (#38173)
This commit is contained in:
parent
7ec1b6e580
commit
e8d273ae5b
26 changed files with 550 additions and 292 deletions
|
|
@ -131,7 +131,7 @@ test("updates a config draft while preserving JSONC comments", async () => {
|
||||||
const service = yield* Config.Service
|
const service = yield* Config.Service
|
||||||
return yield* service.update((draft) => {
|
return yield* service.update((draft) => {
|
||||||
draft.prompt = { paste: "compact" }
|
draft.prompt = { paste: "compact" }
|
||||||
draft.mini = { thinking: "hide", shell_output: "hide", turn_summary: "hide" }
|
draft.mini = { thinking: "hide", shell_output: "hide", turn_summary: "hide", mono: true }
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
@ -139,7 +139,7 @@ test("updates a config draft while preserving JSONC comments", async () => {
|
||||||
expect(config).toEqual({
|
expect(config).toEqual({
|
||||||
animations: true,
|
animations: true,
|
||||||
prompt: { paste: "compact" },
|
prompt: { paste: "compact" },
|
||||||
mini: { thinking: "hide", shell_output: "hide", turn_summary: "hide" },
|
mini: { thinking: "hide", shell_output: "hide", turn_summary: "hide", mono: true },
|
||||||
})
|
})
|
||||||
expect(await Bun.file(path.join(directory, "cli.json")).text()).toContain("// Keep this comment")
|
expect(await Bun.file(path.join(directory, "cli.json")).text()).toContain("// Keep this comment")
|
||||||
} finally {
|
} finally {
|
||||||
|
|
|
||||||
|
|
@ -125,13 +125,16 @@ export const Info = Schema.Struct({
|
||||||
mini: Schema.optional(
|
mini: Schema.optional(
|
||||||
Schema.Struct({
|
Schema.Struct({
|
||||||
thinking: Schema.optional(Schema.Literals(["show", "hide"])).annotate({
|
thinking: Schema.optional(Schema.Literals(["show", "hide"])).annotate({
|
||||||
description: "Show or hide model reasoning in Mini",
|
description: "Show or hide model reasoning",
|
||||||
}),
|
}),
|
||||||
shell_output: Schema.optional(Schema.Literals(["show", "hide"])).annotate({
|
shell_output: Schema.optional(Schema.Literals(["show", "hide"])).annotate({
|
||||||
description: "Show or hide raw shell tool output in Mini",
|
description: "Show or hide raw shell tool output",
|
||||||
}),
|
}),
|
||||||
turn_summary: Schema.optional(Schema.Literals(["show", "hide"])).annotate({
|
turn_summary: Schema.optional(Schema.Literals(["show", "hide"])).annotate({
|
||||||
description: "Show or hide the agent, model, and duration summary in Mini scrollback",
|
description: "Show or hide the agent, model, and duration summary in scrollback",
|
||||||
|
}),
|
||||||
|
mono: Schema.optional(Schema.Boolean).annotate({
|
||||||
|
description: "Use monochrome ASCII output",
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
).annotate({ description: "Mini transcript presentation settings" }),
|
).annotate({ description: "Mini transcript presentation settings" }),
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { toolEntryBody } from "./tool"
|
import { toolEntryBody } from "./tool"
|
||||||
|
import { monoPrefix, monoToolText } from "./mono"
|
||||||
import type { RunEntryBody, ScrollbackOptions, StreamCommit } from "./types"
|
import type { RunEntryBody, ScrollbackOptions, StreamCommit } from "./types"
|
||||||
|
|
||||||
export type EntryFlags = {
|
export type EntryFlags = {
|
||||||
|
|
@ -48,17 +49,17 @@ function markdownBody(content: string): RunEntryBody {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function userBody(raw: string): RunEntryBody {
|
function userBody(raw: string, mono: boolean): RunEntryBody {
|
||||||
if (!raw.trim()) {
|
if (!raw.trim()) {
|
||||||
return RUN_ENTRY_NONE
|
return RUN_ENTRY_NONE
|
||||||
}
|
}
|
||||||
|
|
||||||
const lead = raw.match(/^\n+/)?.[0] ?? ""
|
const lead = raw.match(/^\n+/)?.[0] ?? ""
|
||||||
const body = lead ? raw.slice(lead.length) : raw
|
const body = lead ? raw.slice(lead.length) : raw
|
||||||
return textBody(`${lead}› ${body}`)
|
return textBody(`${lead}${mono ? ">" : "›"} ${body}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
function reasoningBody(raw: string): RunEntryBody {
|
function reasoningBody(raw: string, mono: boolean): RunEntryBody {
|
||||||
const clean = raw.replace(/\[REDACTED\]/g, "")
|
const clean = raw.replace(/\[REDACTED\]/g, "")
|
||||||
if (!clean) {
|
if (!clean) {
|
||||||
return RUN_ENTRY_NONE
|
return RUN_ENTRY_NONE
|
||||||
|
|
@ -68,16 +69,39 @@ function reasoningBody(raw: string): RunEntryBody {
|
||||||
const body = lead ? clean.slice(lead.length) : clean
|
const body = lead ? clean.slice(lead.length) : clean
|
||||||
const mark = "Thinking:"
|
const mark = "Thinking:"
|
||||||
if (body.startsWith(mark)) {
|
if (body.startsWith(mark)) {
|
||||||
|
if (mono) return textBody(`${lead}${mark} ${body.slice(mark.length).trimStart()}`)
|
||||||
return codeBody(`${lead}_Thinking:_ ${body.slice(mark.length).trimStart()}`, "markdown")
|
return codeBody(`${lead}_Thinking:_ ${body.slice(mark.length).trimStart()}`, "markdown")
|
||||||
}
|
}
|
||||||
|
|
||||||
return codeBody(clean, "markdown")
|
return mono ? textBody(clean) : codeBody(clean, "markdown")
|
||||||
}
|
}
|
||||||
|
|
||||||
function systemBody(raw: string, phase: StreamCommit["phase"]): RunEntryBody {
|
function systemBody(raw: string, phase: StreamCommit["phase"]): RunEntryBody {
|
||||||
return textBody(phase === "progress" ? raw : raw.trim())
|
return textBody(phase === "progress" ? raw : raw.trim())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function monoBody(body: RunEntryBody): RunEntryBody {
|
||||||
|
if (body.type === "none" || body.type === "text") return body
|
||||||
|
if (body.type === "code" || body.type === "markdown") return textBody(body.content)
|
||||||
|
const snapshot = body.snapshot
|
||||||
|
if (snapshot.kind === "code") return textBody(`${snapshot.title}\n${snapshot.content}`)
|
||||||
|
if (snapshot.kind === "diff") {
|
||||||
|
return textBody(
|
||||||
|
snapshot.items
|
||||||
|
.map((item) => `${item.title}\n${item.diff.trim() || `-${item.deletions ?? 0} lines`}`)
|
||||||
|
.join("\n\n"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (snapshot.kind === "task") {
|
||||||
|
return textBody([snapshot.title, ...snapshot.rows, snapshot.tail].filter(Boolean).join("\n"))
|
||||||
|
}
|
||||||
|
return textBody(
|
||||||
|
["# Questions", ...snapshot.items.flatMap((item) => [item.question, item.answer]), snapshot.tail]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join("\n"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export function entryFlags(commit: StreamCommit): EntryFlags {
|
export function entryFlags(commit: StreamCommit): EntryFlags {
|
||||||
if (commit.summary) {
|
if (commit.summary) {
|
||||||
return {
|
return {
|
||||||
|
|
@ -168,13 +192,17 @@ export function entryBody(commit: StreamCommit, options?: ScrollbackOptions): Ru
|
||||||
}
|
}
|
||||||
|
|
||||||
const raw = cleanRunText(commit.text)
|
const raw = cleanRunText(commit.text)
|
||||||
|
const mono = options?.mono === true
|
||||||
|
|
||||||
if (commit.kind === "user") {
|
if (commit.kind === "user") {
|
||||||
return userBody(raw)
|
return userBody(raw, mono)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (commit.kind === "tool") {
|
if (commit.kind === "tool") {
|
||||||
return toolEntryBody(commit, raw, options) ?? RUN_ENTRY_NONE
|
const body = toolEntryBody(commit, raw, options) ?? RUN_ENTRY_NONE
|
||||||
|
const result = mono ? monoBody(body) : body
|
||||||
|
if (!mono || body.type !== "text" || result.type !== "text" || commit.phase === "progress") return result
|
||||||
|
return textBody(monoToolText(result.content, true))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (commit.kind === "assistant") {
|
if (commit.kind === "assistant") {
|
||||||
|
|
@ -186,7 +214,7 @@ export function entryBody(commit: StreamCommit, options?: ScrollbackOptions): Ru
|
||||||
return commit.interrupted ? textBody("assistant interrupted") : RUN_ENTRY_NONE
|
return commit.interrupted ? textBody("assistant interrupted") : RUN_ENTRY_NONE
|
||||||
}
|
}
|
||||||
|
|
||||||
return markdownBody(raw)
|
return mono ? textBody(raw) : markdownBody(raw)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (commit.kind === "reasoning") {
|
if (commit.kind === "reasoning") {
|
||||||
|
|
@ -198,8 +226,10 @@ export function entryBody(commit: StreamCommit, options?: ScrollbackOptions): Ru
|
||||||
return commit.interrupted ? textBody("reasoning interrupted") : RUN_ENTRY_NONE
|
return commit.interrupted ? textBody("reasoning interrupted") : RUN_ENTRY_NONE
|
||||||
}
|
}
|
||||||
|
|
||||||
return reasoningBody(raw)
|
return reasoningBody(raw, mono)
|
||||||
}
|
}
|
||||||
|
|
||||||
return systemBody(raw, commit.phase)
|
const body = systemBody(raw, commit.phase)
|
||||||
|
if (!mono || body.type !== "text") return body
|
||||||
|
return textBody(monoPrefix(body.content, true))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -62,29 +62,13 @@ type SettingEntry = PanelEntry & {
|
||||||
}
|
}
|
||||||
|
|
||||||
const PANEL_PAD = 2
|
const PANEL_PAD = 2
|
||||||
|
const panelPad = (mono?: boolean) => (mono ? 1 : PANEL_PAD)
|
||||||
const PANEL_LIST_ROWS = 10
|
const PANEL_LIST_ROWS = 10
|
||||||
const PANEL_FRAME_ROWS = 6
|
const PANEL_FRAME_ROWS = 6
|
||||||
export const RUN_COMMAND_PANEL_ROWS = PANEL_LIST_ROWS + PANEL_FRAME_ROWS
|
export const RUN_COMMAND_PANEL_ROWS = PANEL_LIST_ROWS + PANEL_FRAME_ROWS
|
||||||
const SUBAGENT_LIST_ROWS = 12
|
const SUBAGENT_LIST_ROWS = 12
|
||||||
export const RUN_SUBAGENT_PANEL_ROWS = SUBAGENT_LIST_ROWS + PANEL_FRAME_ROWS
|
export const RUN_SUBAGENT_PANEL_ROWS = SUBAGENT_LIST_ROWS + PANEL_FRAME_ROWS
|
||||||
const PANEL_PAGE = PANEL_LIST_ROWS - 1
|
const PANEL_PAGE = PANEL_LIST_ROWS - 1
|
||||||
const PANEL_BORDER = {
|
|
||||||
topLeft: "",
|
|
||||||
bottomLeft: "",
|
|
||||||
vertical: "┃",
|
|
||||||
topRight: "",
|
|
||||||
bottomRight: "",
|
|
||||||
horizontal: " ",
|
|
||||||
bottomT: "",
|
|
||||||
topT: "",
|
|
||||||
cross: "",
|
|
||||||
leftT: "",
|
|
||||||
rightT: "",
|
|
||||||
}
|
|
||||||
const PANEL_BOTTOM_BORDER = {
|
|
||||||
...PANEL_BORDER,
|
|
||||||
vertical: "╹",
|
|
||||||
}
|
|
||||||
const HALF_BLOCK_BORDER = {
|
const HALF_BLOCK_BORDER = {
|
||||||
topLeft: "",
|
topLeft: "",
|
||||||
bottomLeft: "",
|
bottomLeft: "",
|
||||||
|
|
@ -280,19 +264,17 @@ function PanelShell(props: {
|
||||||
onQuery: (query: string) => void
|
onQuery: (query: string) => void
|
||||||
children: JSX.Element
|
children: JSX.Element
|
||||||
hint?: string
|
hint?: string
|
||||||
dark?: boolean
|
mono?: boolean
|
||||||
chrome?: "default" | "minimal"
|
|
||||||
}) {
|
}) {
|
||||||
const background = () => (props.dark ? props.theme().shade : props.theme().surface)
|
const background = () => props.theme().shade
|
||||||
const minimal = () => props.chrome === "minimal"
|
|
||||||
const content = (
|
const content = (
|
||||||
<>
|
<>
|
||||||
<box height={1} flexShrink={0} backgroundColor={background()} />
|
<box height={1} flexShrink={0} backgroundColor={background()} />
|
||||||
<box
|
<box
|
||||||
width="100%"
|
width="100%"
|
||||||
height={1}
|
height={1}
|
||||||
paddingLeft={PANEL_PAD}
|
paddingLeft={panelPad(props.mono)}
|
||||||
paddingRight={PANEL_PAD}
|
paddingRight={panelPad(props.mono)}
|
||||||
flexDirection="row"
|
flexDirection="row"
|
||||||
gap={1}
|
gap={1}
|
||||||
flexShrink={0}
|
flexShrink={0}
|
||||||
|
|
@ -308,15 +290,15 @@ function PanelShell(props: {
|
||||||
) : null}
|
) : null}
|
||||||
<box flexGrow={1} flexShrink={1} backgroundColor="transparent" />
|
<box flexGrow={1} flexShrink={1} backgroundColor="transparent" />
|
||||||
<text fg={props.theme().muted} wrapMode="none" truncate flexShrink={0}>
|
<text fg={props.theme().muted} wrapMode="none" truncate flexShrink={0}>
|
||||||
{props.hint ? `${props.hint} · ` : ""}esc
|
{props.hint ? `${props.hint} ${props.mono ? "-" : "·"} ` : ""}esc
|
||||||
</text>
|
</text>
|
||||||
</box>
|
</box>
|
||||||
<box height={1} flexShrink={0} backgroundColor={background()} />
|
<box height={1} flexShrink={0} backgroundColor={background()} />
|
||||||
<box
|
<box
|
||||||
width="100%"
|
width="100%"
|
||||||
height={1}
|
height={1}
|
||||||
paddingLeft={PANEL_PAD}
|
paddingLeft={panelPad(props.mono)}
|
||||||
paddingRight={PANEL_PAD}
|
paddingRight={panelPad(props.mono)}
|
||||||
flexShrink={0}
|
flexShrink={0}
|
||||||
backgroundColor={background()}
|
backgroundColor={background()}
|
||||||
>
|
>
|
||||||
|
|
@ -347,25 +329,11 @@ function PanelShell(props: {
|
||||||
)
|
)
|
||||||
return (
|
return (
|
||||||
<box width="100%" flexDirection="column" border={false} backgroundColor="transparent" flexShrink={0}>
|
<box width="100%" flexDirection="column" border={false} backgroundColor="transparent" flexShrink={0}>
|
||||||
{minimal() ? (
|
<box width="100%" flexDirection="column" border={false} backgroundColor="transparent" flexShrink={0}>
|
||||||
<box width="100%" flexDirection="column" border={false} backgroundColor="transparent" flexShrink={0}>
|
{content}
|
||||||
{content}
|
</box>
|
||||||
</box>
|
<box width="100%" height={1} border={false} backgroundColor="transparent" flexShrink={0}>
|
||||||
) : (
|
{props.mono ? null : (
|
||||||
<box
|
|
||||||
width="100%"
|
|
||||||
flexDirection="column"
|
|
||||||
border={["left"]}
|
|
||||||
borderColor={props.theme().highlight}
|
|
||||||
backgroundColor="transparent"
|
|
||||||
customBorderChars={PANEL_BORDER}
|
|
||||||
flexShrink={0}
|
|
||||||
>
|
|
||||||
{content}
|
|
||||||
</box>
|
|
||||||
)}
|
|
||||||
{minimal() ? (
|
|
||||||
<box width="100%" height={1} border={false} backgroundColor="transparent" flexShrink={0}>
|
|
||||||
<box
|
<box
|
||||||
width="100%"
|
width="100%"
|
||||||
height={1}
|
height={1}
|
||||||
|
|
@ -374,27 +342,8 @@ function PanelShell(props: {
|
||||||
backgroundColor="transparent"
|
backgroundColor="transparent"
|
||||||
customBorderChars={HALF_BLOCK_BORDER}
|
customBorderChars={HALF_BLOCK_BORDER}
|
||||||
/>
|
/>
|
||||||
</box>
|
)}
|
||||||
) : (
|
</box>
|
||||||
<box
|
|
||||||
width="100%"
|
|
||||||
height={1}
|
|
||||||
border={["left"]}
|
|
||||||
borderColor={props.theme().highlight}
|
|
||||||
backgroundColor="transparent"
|
|
||||||
customBorderChars={PANEL_BOTTOM_BORDER}
|
|
||||||
flexShrink={0}
|
|
||||||
>
|
|
||||||
<box
|
|
||||||
width="100%"
|
|
||||||
height={1}
|
|
||||||
border={["bottom"]}
|
|
||||||
borderColor={background()}
|
|
||||||
backgroundColor="transparent"
|
|
||||||
customBorderChars={HALF_BLOCK_BORDER}
|
|
||||||
/>
|
|
||||||
</box>
|
|
||||||
)}
|
|
||||||
</box>
|
</box>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -418,6 +367,7 @@ export function RunCommandMenuBody(props: {
|
||||||
onCommand: (name: string) => void
|
onCommand: (name: string) => void
|
||||||
onNew: () => void
|
onNew: () => void
|
||||||
onExit: () => void
|
onExit: () => void
|
||||||
|
mono?: boolean
|
||||||
}) {
|
}) {
|
||||||
const skills = createMemo(() => (props.commands() ?? []).filter((item) => item.source === "skill"))
|
const skills = createMemo(() => (props.commands() ?? []).filter((item) => item.source === "skill"))
|
||||||
const activeSubagentCount = createMemo(() => props.subagents().filter((item) => item.status === "running").length)
|
const activeSubagentCount = createMemo(() => props.subagents().filter((item) => item.status === "running").length)
|
||||||
|
|
@ -433,18 +383,18 @@ export function RunCommandMenuBody(props: {
|
||||||
},
|
},
|
||||||
...(props.subagents().length > 0
|
...(props.subagents().length > 0
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
action: "subagent" as const,
|
action: "subagent" as const,
|
||||||
category: "Session",
|
category: "Session",
|
||||||
display: "View subagents",
|
display: "View subagents",
|
||||||
footer:
|
footer:
|
||||||
activeSubagentCount() > 0 ? `${activeSubagentCount()} active` : `${props.subagents().length} recent`,
|
activeSubagentCount() > 0 ? `${activeSubagentCount()} active` : `${props.subagents().length} recent`,
|
||||||
keywords: props
|
keywords: props
|
||||||
.subagents()
|
.subagents()
|
||||||
.map((item) => `${item.label} ${item.description} ${item.title ?? ""}`)
|
.map((item) => `${item.label} ${item.description} ${item.title ?? ""}`)
|
||||||
.join(" "),
|
.join(" "),
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
{
|
{
|
||||||
action: "slash",
|
action: "slash",
|
||||||
|
|
@ -458,16 +408,16 @@ export function RunCommandMenuBody(props: {
|
||||||
const prompt: CommandEntry[] =
|
const prompt: CommandEntry[] =
|
||||||
props.commands() === undefined || skills().length > 0
|
props.commands() === undefined || skills().length > 0
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
action: "skill" as const,
|
action: "skill" as const,
|
||||||
category: "Prompt",
|
category: "Prompt",
|
||||||
display: "Skills",
|
display: "Skills",
|
||||||
footer: "/skills",
|
footer: "/skills",
|
||||||
keywords: `skill skills ${skills()
|
keywords: `skill skills ${skills()
|
||||||
.map((item) => `${item.name} ${item.description ?? ""}`)
|
.map((item) => `${item.name} ${item.description ?? ""}`)
|
||||||
.join(" ")}`.trim(),
|
.join(" ")}`.trim(),
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
: []
|
: []
|
||||||
const agent: CommandEntry[] = [
|
const agent: CommandEntry[] = [
|
||||||
{
|
{
|
||||||
|
|
@ -477,17 +427,17 @@ export function RunCommandMenuBody(props: {
|
||||||
},
|
},
|
||||||
...(props.queued().length > 0
|
...(props.queued().length > 0
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
action: "queued" as const,
|
action: "queued" as const,
|
||||||
category: "Agent",
|
category: "Agent",
|
||||||
display: "View pending work",
|
display: "View pending work",
|
||||||
footer: `${props.queued().length} pending`,
|
footer: `${props.queued().length} pending`,
|
||||||
keywords: props
|
keywords: props
|
||||||
.queued()
|
.queued()
|
||||||
.map((item) => item.prompt.text)
|
.map((item) => item.prompt.text)
|
||||||
.join(" "),
|
.join(" "),
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
{
|
{
|
||||||
action: "variant.cycle",
|
action: "variant.cycle",
|
||||||
|
|
@ -498,13 +448,13 @@ export function RunCommandMenuBody(props: {
|
||||||
},
|
},
|
||||||
...(props.variants().length > 0
|
...(props.variants().length > 0
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
action: "variant.list" as const,
|
action: "variant.list" as const,
|
||||||
category: "Agent",
|
category: "Agent",
|
||||||
display: "Switch model variant",
|
display: "Switch model variant",
|
||||||
keywords: `variant variants ${props.variants().join(" ")}`,
|
keywords: `variant variants ${props.variants().join(" ")}`,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
]
|
]
|
||||||
const commands = (props.commands() ?? [])
|
const commands = (props.commands() ?? [])
|
||||||
|
|
@ -533,7 +483,7 @@ export function RunCommandMenuBody(props: {
|
||||||
{
|
{
|
||||||
action: "settings",
|
action: "settings",
|
||||||
category: "System",
|
category: "System",
|
||||||
display: "Open settings",
|
display: "Settings",
|
||||||
footer: "/settings",
|
footer: "/settings",
|
||||||
keywords: "/settings settings preferences configuration",
|
keywords: "/settings settings preferences configuration",
|
||||||
},
|
},
|
||||||
|
|
@ -611,8 +561,7 @@ export function RunCommandMenuBody(props: {
|
||||||
theme={props.theme}
|
theme={props.theme}
|
||||||
inputRef={controller.inputRef}
|
inputRef={controller.inputRef}
|
||||||
onQuery={controller.setQuery}
|
onQuery={controller.setQuery}
|
||||||
dark
|
mono={props.mono}
|
||||||
chrome="minimal"
|
|
||||||
>
|
>
|
||||||
<RunFooterMenu
|
<RunFooterMenu
|
||||||
theme={props.theme}
|
theme={props.theme}
|
||||||
|
|
@ -623,11 +572,12 @@ export function RunCommandMenuBody(props: {
|
||||||
limit={PANEL_LIST_ROWS}
|
limit={PANEL_LIST_ROWS}
|
||||||
empty="No results found"
|
empty="No results found"
|
||||||
border={false}
|
border={false}
|
||||||
paddingLeft={PANEL_PAD}
|
paddingLeft={panelPad(props.mono)}
|
||||||
paddingRight={PANEL_PAD}
|
paddingRight={panelPad(props.mono)}
|
||||||
grouped={!controller.query().trim()}
|
grouped={!controller.query().trim()}
|
||||||
background
|
background
|
||||||
headerColor={props.theme().muted}
|
headerColor={props.theme().muted}
|
||||||
|
mono={props.mono}
|
||||||
/>
|
/>
|
||||||
</PanelShell>
|
</PanelShell>
|
||||||
)
|
)
|
||||||
|
|
@ -638,21 +588,20 @@ export function RunSettingsBody(props: {
|
||||||
settings: Accessor<MiniSettings>
|
settings: Accessor<MiniSettings>
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
onChange: (change: MiniSettingChange) => void | Promise<void>
|
onChange: (change: MiniSettingChange) => void | Promise<void>
|
||||||
|
mono?: boolean
|
||||||
}) {
|
}) {
|
||||||
const [saving, setSaving] = createSignal<keyof MiniSettings>()
|
const [saving, setSaving] = createSignal<keyof MiniSettings>()
|
||||||
const entries = createMemo<SettingEntry[]>(() => [
|
const entries = createMemo<SettingEntry[]>(() => [
|
||||||
{
|
{
|
||||||
category: "Transcript",
|
category: "Transcript",
|
||||||
display: "Thinking",
|
display: "Thinking",
|
||||||
description: "future sessions",
|
|
||||||
footer: saving() === "thinking" ? "saving" : props.settings().thinking,
|
footer: saving() === "thinking" ? "saving" : props.settings().thinking,
|
||||||
keywords: `thinking reasoning ${props.settings().thinking}`,
|
keywords: `thinking reasoning ${props.settings().thinking}`,
|
||||||
key: "thinking",
|
key: "thinking",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
category: "Transcript",
|
category: "Transcript",
|
||||||
display: "Shell tool output",
|
display: "Shell",
|
||||||
description: "model-issued commands",
|
|
||||||
footer: saving() === "shell_output" ? "saving" : props.settings().shell_output,
|
footer: saving() === "shell_output" ? "saving" : props.settings().shell_output,
|
||||||
keywords: `shell tool command output ${props.settings().shell_output}`,
|
keywords: `shell tool command output ${props.settings().shell_output}`,
|
||||||
key: "shell_output",
|
key: "shell_output",
|
||||||
|
|
@ -660,18 +609,27 @@ export function RunSettingsBody(props: {
|
||||||
{
|
{
|
||||||
category: "Transcript",
|
category: "Transcript",
|
||||||
display: "Turn summary",
|
display: "Turn summary",
|
||||||
description: "agent, model, and duration",
|
|
||||||
footer: saving() === "turn_summary" ? "saving" : props.settings().turn_summary,
|
footer: saving() === "turn_summary" ? "saving" : props.settings().turn_summary,
|
||||||
keywords: `turn summary agent model duration ${props.settings().turn_summary}`,
|
keywords: `turn summary agent model duration ${props.settings().turn_summary}`,
|
||||||
key: "turn_summary",
|
key: "turn_summary",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
category: "Terminal",
|
||||||
|
display: "Monochrome UI",
|
||||||
|
footer: saving() === "mono" ? "saving" : props.settings().mono ? "on" : "off",
|
||||||
|
keywords: `mono monochrome ascii legacy compat terminal ${props.settings().mono ? "on" : "off"}`,
|
||||||
|
key: "mono",
|
||||||
|
},
|
||||||
])
|
])
|
||||||
const change = (item: SettingEntry) => {
|
const change = (item: SettingEntry) => {
|
||||||
if (saving()) return
|
if (saving()) return
|
||||||
const current = props.settings()[item.key]
|
const next: MiniSettingChange =
|
||||||
|
item.key === "mono"
|
||||||
|
? { key: "mono", value: !props.settings().mono }
|
||||||
|
: { key: item.key, value: props.settings()[item.key] === "show" ? "hide" : "show" }
|
||||||
setSaving(item.key)
|
setSaving(item.key)
|
||||||
void Promise.resolve(props.onChange({ key: item.key, value: current === "show" ? "hide" : "show" }))
|
void Promise.resolve(props.onChange(next))
|
||||||
.catch(() => {})
|
.catch(() => { })
|
||||||
.finally(() => setSaving())
|
.finally(() => setSaving())
|
||||||
}
|
}
|
||||||
const controller = createSearchablePanelController({
|
const controller = createSearchablePanelController({
|
||||||
|
|
@ -700,8 +658,7 @@ export function RunSettingsBody(props: {
|
||||||
inputRef={controller.inputRef}
|
inputRef={controller.inputRef}
|
||||||
onQuery={controller.setQuery}
|
onQuery={controller.setQuery}
|
||||||
hint="left/right change"
|
hint="left/right change"
|
||||||
dark
|
mono={props.mono}
|
||||||
chrome="minimal"
|
|
||||||
>
|
>
|
||||||
<RunFooterMenu
|
<RunFooterMenu
|
||||||
theme={props.theme}
|
theme={props.theme}
|
||||||
|
|
@ -712,11 +669,12 @@ export function RunSettingsBody(props: {
|
||||||
limit={PANEL_LIST_ROWS}
|
limit={PANEL_LIST_ROWS}
|
||||||
empty="No settings found"
|
empty="No settings found"
|
||||||
border={false}
|
border={false}
|
||||||
paddingLeft={PANEL_PAD}
|
paddingLeft={panelPad(props.mono)}
|
||||||
paddingRight={PANEL_PAD}
|
paddingRight={panelPad(props.mono)}
|
||||||
grouped={!controller.query().trim()}
|
grouped={!controller.query().trim()}
|
||||||
background
|
background
|
||||||
headerColor={props.theme().muted}
|
headerColor={props.theme().muted}
|
||||||
|
mono={props.mono}
|
||||||
/>
|
/>
|
||||||
</PanelShell>
|
</PanelShell>
|
||||||
)
|
)
|
||||||
|
|
@ -729,6 +687,7 @@ export function RunSubagentSelectBody(props: {
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
onSelect: (sessionID: string) => void
|
onSelect: (sessionID: string) => void
|
||||||
onRows?: (rows: number) => void
|
onRows?: (rows: number) => void
|
||||||
|
mono?: boolean
|
||||||
}) {
|
}) {
|
||||||
const entries = createMemo<SubagentEntry[]>(() =>
|
const entries = createMemo<SubagentEntry[]>(() =>
|
||||||
props.tabs().map((item) => {
|
props.tabs().map((item) => {
|
||||||
|
|
@ -764,8 +723,7 @@ export function RunSubagentSelectBody(props: {
|
||||||
theme={props.theme}
|
theme={props.theme}
|
||||||
inputRef={controller.inputRef}
|
inputRef={controller.inputRef}
|
||||||
onQuery={controller.setQuery}
|
onQuery={controller.setQuery}
|
||||||
dark
|
mono={props.mono}
|
||||||
chrome="minimal"
|
|
||||||
>
|
>
|
||||||
<RunFooterMenu
|
<RunFooterMenu
|
||||||
theme={props.theme}
|
theme={props.theme}
|
||||||
|
|
@ -776,10 +734,11 @@ export function RunSubagentSelectBody(props: {
|
||||||
limit={SUBAGENT_LIST_ROWS}
|
limit={SUBAGENT_LIST_ROWS}
|
||||||
empty="No subagents found"
|
empty="No subagents found"
|
||||||
border={false}
|
border={false}
|
||||||
paddingLeft={PANEL_PAD}
|
paddingLeft={panelPad(props.mono)}
|
||||||
paddingRight={PANEL_PAD}
|
paddingRight={panelPad(props.mono)}
|
||||||
grouped={false}
|
grouped={false}
|
||||||
background
|
background
|
||||||
|
mono={props.mono}
|
||||||
/>
|
/>
|
||||||
</PanelShell>
|
</PanelShell>
|
||||||
)
|
)
|
||||||
|
|
@ -790,6 +749,7 @@ export function RunQueuedPromptSelectBody(props: {
|
||||||
prompts: Accessor<FooterQueuedPrompt[]>
|
prompts: Accessor<FooterQueuedPrompt[]>
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
onRows?: (rows: number) => void
|
onRows?: (rows: number) => void
|
||||||
|
mono?: boolean
|
||||||
}) {
|
}) {
|
||||||
const entries = createMemo<QueuedEntry[]>(() =>
|
const entries = createMemo<QueuedEntry[]>(() =>
|
||||||
props.prompts().map((prompt) => ({
|
props.prompts().map((prompt) => ({
|
||||||
|
|
@ -818,8 +778,7 @@ export function RunQueuedPromptSelectBody(props: {
|
||||||
theme={props.theme}
|
theme={props.theme}
|
||||||
inputRef={controller.inputRef}
|
inputRef={controller.inputRef}
|
||||||
onQuery={controller.setQuery}
|
onQuery={controller.setQuery}
|
||||||
dark
|
mono={props.mono}
|
||||||
chrome="minimal"
|
|
||||||
>
|
>
|
||||||
<RunFooterMenu
|
<RunFooterMenu
|
||||||
theme={props.theme}
|
theme={props.theme}
|
||||||
|
|
@ -830,10 +789,11 @@ export function RunQueuedPromptSelectBody(props: {
|
||||||
limit={SUBAGENT_LIST_ROWS}
|
limit={SUBAGENT_LIST_ROWS}
|
||||||
empty="No pending work"
|
empty="No pending work"
|
||||||
border={false}
|
border={false}
|
||||||
paddingLeft={PANEL_PAD}
|
paddingLeft={panelPad(props.mono)}
|
||||||
paddingRight={PANEL_PAD}
|
paddingRight={panelPad(props.mono)}
|
||||||
grouped={false}
|
grouped={false}
|
||||||
background
|
background
|
||||||
|
mono={props.mono}
|
||||||
/>
|
/>
|
||||||
</PanelShell>
|
</PanelShell>
|
||||||
)
|
)
|
||||||
|
|
@ -844,6 +804,7 @@ export function RunSkillSelectBody(props: {
|
||||||
commands: Accessor<RunCommand[] | undefined>
|
commands: Accessor<RunCommand[] | undefined>
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
onSelect: (name: string) => void
|
onSelect: (name: string) => void
|
||||||
|
mono?: boolean
|
||||||
}) {
|
}) {
|
||||||
const entries = createMemo<SkillEntry[]>(() =>
|
const entries = createMemo<SkillEntry[]>(() =>
|
||||||
(props.commands() ?? [])
|
(props.commands() ?? [])
|
||||||
|
|
@ -874,8 +835,7 @@ export function RunSkillSelectBody(props: {
|
||||||
theme={props.theme}
|
theme={props.theme}
|
||||||
inputRef={controller.inputRef}
|
inputRef={controller.inputRef}
|
||||||
onQuery={controller.setQuery}
|
onQuery={controller.setQuery}
|
||||||
dark
|
mono={props.mono}
|
||||||
chrome="minimal"
|
|
||||||
>
|
>
|
||||||
<RunFooterMenu
|
<RunFooterMenu
|
||||||
theme={props.theme}
|
theme={props.theme}
|
||||||
|
|
@ -886,10 +846,11 @@ export function RunSkillSelectBody(props: {
|
||||||
limit={PANEL_LIST_ROWS}
|
limit={PANEL_LIST_ROWS}
|
||||||
empty={props.commands() ? "No skills found" : "Skills loading"}
|
empty={props.commands() ? "No skills found" : "Skills loading"}
|
||||||
border={false}
|
border={false}
|
||||||
paddingLeft={PANEL_PAD}
|
paddingLeft={panelPad(props.mono)}
|
||||||
paddingRight={PANEL_PAD}
|
paddingRight={panelPad(props.mono)}
|
||||||
grouped={false}
|
grouped={false}
|
||||||
background
|
background
|
||||||
|
mono={props.mono}
|
||||||
/>
|
/>
|
||||||
</PanelShell>
|
</PanelShell>
|
||||||
)
|
)
|
||||||
|
|
@ -901,6 +862,7 @@ export function RunVariantSelectBody(props: {
|
||||||
current: Accessor<string | undefined>
|
current: Accessor<string | undefined>
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
onSelect: (variant: string | undefined) => void
|
onSelect: (variant: string | undefined) => void
|
||||||
|
mono?: boolean
|
||||||
}) {
|
}) {
|
||||||
const entries = createMemo<VariantEntry[]>(() => [
|
const entries = createMemo<VariantEntry[]>(() => [
|
||||||
{
|
{
|
||||||
|
|
@ -938,8 +900,7 @@ export function RunVariantSelectBody(props: {
|
||||||
theme={props.theme}
|
theme={props.theme}
|
||||||
inputRef={controller.inputRef}
|
inputRef={controller.inputRef}
|
||||||
onQuery={controller.setQuery}
|
onQuery={controller.setQuery}
|
||||||
dark
|
mono={props.mono}
|
||||||
chrome="minimal"
|
|
||||||
>
|
>
|
||||||
<RunFooterMenu
|
<RunFooterMenu
|
||||||
theme={props.theme}
|
theme={props.theme}
|
||||||
|
|
@ -950,10 +911,11 @@ export function RunVariantSelectBody(props: {
|
||||||
limit={PANEL_LIST_ROWS}
|
limit={PANEL_LIST_ROWS}
|
||||||
empty="No results found"
|
empty="No results found"
|
||||||
border={false}
|
border={false}
|
||||||
paddingLeft={PANEL_PAD}
|
paddingLeft={panelPad(props.mono)}
|
||||||
paddingRight={PANEL_PAD}
|
paddingRight={panelPad(props.mono)}
|
||||||
grouped={false}
|
grouped={false}
|
||||||
background
|
background
|
||||||
|
mono={props.mono}
|
||||||
/>
|
/>
|
||||||
</PanelShell>
|
</PanelShell>
|
||||||
)
|
)
|
||||||
|
|
@ -965,6 +927,7 @@ export function RunModelSelectBody(props: {
|
||||||
current: Accessor<RunInput["model"]>
|
current: Accessor<RunInput["model"]>
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
onSelect: (model: NonNullable<RunInput["model"]>) => void
|
onSelect: (model: NonNullable<RunInput["model"]>) => void
|
||||||
|
mono?: boolean
|
||||||
}) {
|
}) {
|
||||||
const entries = createMemo<ModelEntry[]>(() =>
|
const entries = createMemo<ModelEntry[]>(() =>
|
||||||
(props.providers() ?? [])
|
(props.providers() ?? [])
|
||||||
|
|
@ -1025,8 +988,7 @@ export function RunModelSelectBody(props: {
|
||||||
theme={props.theme}
|
theme={props.theme}
|
||||||
inputRef={controller.inputRef}
|
inputRef={controller.inputRef}
|
||||||
onQuery={controller.setQuery}
|
onQuery={controller.setQuery}
|
||||||
dark
|
mono={props.mono}
|
||||||
chrome="minimal"
|
|
||||||
>
|
>
|
||||||
<RunFooterMenu
|
<RunFooterMenu
|
||||||
theme={props.theme}
|
theme={props.theme}
|
||||||
|
|
@ -1037,11 +999,12 @@ export function RunModelSelectBody(props: {
|
||||||
limit={PANEL_LIST_ROWS}
|
limit={PANEL_LIST_ROWS}
|
||||||
empty={props.providers() ? "No results found" : "Models loading"}
|
empty={props.providers() ? "No results found" : "Models loading"}
|
||||||
border={false}
|
border={false}
|
||||||
paddingLeft={PANEL_PAD}
|
paddingLeft={panelPad(props.mono)}
|
||||||
paddingRight={PANEL_PAD}
|
paddingRight={panelPad(props.mono)}
|
||||||
grouped={!controller.query().trim()}
|
grouped={!controller.query().trim()}
|
||||||
background
|
background
|
||||||
headerColor={props.theme().muted}
|
headerColor={props.theme().muted}
|
||||||
|
mono={props.mono}
|
||||||
/>
|
/>
|
||||||
</PanelShell>
|
</PanelShell>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@ export function RunFormBody(props: {
|
||||||
openExternal?: (url: string) => Promise<unknown>
|
openExternal?: (url: string) => Promise<unknown>
|
||||||
state?: FormBodyState
|
state?: FormBodyState
|
||||||
onState?: (state: FormBodyState) => void
|
onState?: (state: FormBodyState) => void
|
||||||
|
mono?: boolean
|
||||||
}) {
|
}) {
|
||||||
const [state, setLocalState] = createSignal(props.state ?? createFormBodyState(props.request))
|
const [state, setLocalState] = createSignal(props.state ?? createFormBodyState(props.request))
|
||||||
const setState = (next: FormBodyState | ((previous: FormBodyState) => FormBodyState)) => {
|
const setState = (next: FormBodyState | ((previous: FormBodyState) => FormBodyState)) => {
|
||||||
|
|
@ -258,7 +259,7 @@ export function RunFormBody(props: {
|
||||||
<box width="100%" height="100%" flexDirection="column" backgroundColor={props.theme.surface}>
|
<box width="100%" height="100%" flexDirection="column" backgroundColor={props.theme.surface}>
|
||||||
<box flexDirection="column" gap={1} paddingLeft={2} paddingRight={3} paddingTop={1} flexGrow={1} flexShrink={1}>
|
<box flexDirection="column" gap={1} paddingLeft={2} paddingRight={3} paddingTop={1} flexGrow={1} flexShrink={1}>
|
||||||
<box flexDirection="row" gap={1} flexShrink={0}>
|
<box flexDirection="row" gap={1} flexShrink={0}>
|
||||||
<text fg={unsupported() ? props.theme.warning : props.theme.highlight}>◆</text>
|
<text fg={unsupported() ? props.theme.warning : props.theme.highlight}>{props.mono ? "*" : "◆"}</text>
|
||||||
<text fg={props.theme.text}>{props.request.title}</text>
|
<text fg={props.theme.text}>{props.request.title}</text>
|
||||||
<Show when={!unsupported() && !formSingle(props.request)}>
|
<Show when={!unsupported() && !formSingle(props.request)}>
|
||||||
<text fg={props.theme.muted}>
|
<text fg={props.theme.muted}>
|
||||||
|
|
@ -352,7 +353,9 @@ export function RunFormBody(props: {
|
||||||
onMouseOver={() => setState((previous) => formSetSelected(previous, index()))}
|
onMouseOver={() => setState((previous) => formSetSelected(previous, index()))}
|
||||||
onMouseUp={() => choose(index())}
|
onMouseUp={() => choose(index())}
|
||||||
>
|
>
|
||||||
<text fg={active() ? props.theme.highlight : props.theme.muted}>{index() + 1}.</text>
|
<text fg={active() ? props.theme.highlight : props.theme.muted}>
|
||||||
|
{props.mono ? `${active() ? ">" : " "}${index() + 1}.` : `${index() + 1}.`}
|
||||||
|
</text>
|
||||||
<text fg={active() ? props.theme.text : props.theme.muted}>
|
<text fg={active() ? props.theme.text : props.theme.muted}>
|
||||||
{multiple() ? `[${picked() ? "x" : " "}] ` : ""}
|
{multiple() ? `[${picked() ? "x" : " "}] ` : ""}
|
||||||
{row.label}
|
{row.label}
|
||||||
|
|
@ -368,7 +371,9 @@ export function RunFormBody(props: {
|
||||||
<Show when={custom()}>
|
<Show when={custom()}>
|
||||||
<box flexDirection="row" gap={1} onMouseUp={() => choose(rows().length)}>
|
<box flexDirection="row" gap={1} onMouseUp={() => choose(rows().length)}>
|
||||||
<text fg={state().selected === rows().length ? props.theme.highlight : props.theme.muted}>
|
<text fg={state().selected === rows().length ? props.theme.highlight : props.theme.muted}>
|
||||||
{rows().length + 1}.
|
{props.mono
|
||||||
|
? `${state().selected === rows().length ? ">" : " "}${rows().length + 1}.`
|
||||||
|
: `${rows().length + 1}.`}
|
||||||
</text>
|
</text>
|
||||||
<text fg={state().selected === rows().length ? props.theme.text : props.theme.muted}>
|
<text fg={state().selected === rows().length ? props.theme.text : props.theme.muted}>
|
||||||
Type your own answer
|
Type your own answer
|
||||||
|
|
@ -413,7 +418,9 @@ export function RunFormBody(props: {
|
||||||
? "enter submit esc dismiss"
|
? "enter submit esc dismiss"
|
||||||
: textual() || state().editing
|
: textual() || state().editing
|
||||||
? "enter save esc dismiss"
|
? "enter save esc dismiss"
|
||||||
: "↑↓ select enter choose tab next esc dismiss"}
|
: props.mono
|
||||||
|
? "up/down select enter choose tab next esc dismiss"
|
||||||
|
: "↑↓ select enter choose tab next esc dismiss"}
|
||||||
</text>
|
</text>
|
||||||
<Show when={state().error}>
|
<Show when={state().error}>
|
||||||
<text fg={props.theme.error} wrapMode="none" truncate>
|
<text fg={props.theme.error} wrapMode="none" truncate>
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import { transparent, type RunFooterTheme } from "./theme"
|
||||||
import { Locale } from "../util/locale"
|
import { Locale } from "../util/locale"
|
||||||
import { stringWidth } from "../util/string-width"
|
import { stringWidth } from "../util/string-width"
|
||||||
import { moveSelection, moveSelectionOffset, reconcileSelection, revealSelectionOffset } from "../ui/select-controller"
|
import { moveSelection, moveSelectionOffset, reconcileSelection, revealSelectionOffset } from "../ui/select-controller"
|
||||||
|
import { monoTruncate } from "./mono"
|
||||||
|
|
||||||
export const FOOTER_MENU_ROWS = 8
|
export const FOOTER_MENU_ROWS = 8
|
||||||
|
|
||||||
|
|
@ -77,6 +78,7 @@ export function RunFooterMenu(props: {
|
||||||
grouped?: boolean
|
grouped?: boolean
|
||||||
background?: boolean
|
background?: boolean
|
||||||
headerColor?: ColorInput
|
headerColor?: ColorInput
|
||||||
|
mono?: boolean
|
||||||
}) {
|
}) {
|
||||||
const term = useTerminalDimensions()
|
const term = useTerminalDimensions()
|
||||||
const limit = () => props.limit ?? FOOTER_MENU_ROWS
|
const limit = () => props.limit ?? FOOTER_MENU_ROWS
|
||||||
|
|
@ -170,7 +172,8 @@ export function RunFooterMenu(props: {
|
||||||
descriptionColumn() -
|
descriptionColumn() -
|
||||||
footerWidth -
|
footerWidth -
|
||||||
4
|
4
|
||||||
return Locale.truncate(item.description, Math.max(12, available))
|
const width = Math.max(12, available)
|
||||||
|
return props.mono ? monoTruncate(item.description, width, true) : Locale.truncate(item.description, width)
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<box
|
<box
|
||||||
|
|
@ -187,7 +190,7 @@ export function RunFooterMenu(props: {
|
||||||
>
|
>
|
||||||
{border() ? (
|
{border() ? (
|
||||||
<text fg={props.theme().border} wrapMode="none">
|
<text fg={props.theme().border} wrapMode="none">
|
||||||
┃
|
{props.mono ? "|" : "┃"}
|
||||||
</text>
|
</text>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
<box
|
<box
|
||||||
|
|
@ -224,6 +227,8 @@ export function RunFooterMenu(props: {
|
||||||
}
|
}
|
||||||
|
|
||||||
const active = () => row.index === props.selected()
|
const active = () => row.index === props.selected()
|
||||||
|
const attributes = () =>
|
||||||
|
active() ? TextAttributes.BOLD | (props.mono ? TextAttributes.INVERSE : 0) : undefined
|
||||||
const background = () =>
|
const background = () =>
|
||||||
active()
|
active()
|
||||||
? props.background
|
? props.background
|
||||||
|
|
@ -236,7 +241,7 @@ export function RunFooterMenu(props: {
|
||||||
<box paddingRight={0} flexDirection="row" backgroundColor={background()}>
|
<box paddingRight={0} flexDirection="row" backgroundColor={background()}>
|
||||||
{border() ? (
|
{border() ? (
|
||||||
<text fg={props.theme().highlight} bg={background()} wrapMode="none">
|
<text fg={props.theme().highlight} bg={background()} wrapMode="none">
|
||||||
{active() ? "▌" : " "}
|
{active() ? (props.mono ? ">" : "▌") : " "}
|
||||||
</text>
|
</text>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
<box
|
<box
|
||||||
|
|
@ -250,7 +255,7 @@ export function RunFooterMenu(props: {
|
||||||
<box flexDirection="row" gap={0} flexGrow={1} flexShrink={1}>
|
<box flexDirection="row" gap={0} flexGrow={1} flexShrink={1}>
|
||||||
<text
|
<text
|
||||||
fg={active() ? props.theme().selectedText : props.theme().text}
|
fg={active() ? props.theme().selectedText : props.theme().text}
|
||||||
attributes={active() ? TextAttributes.BOLD : undefined}
|
attributes={attributes()}
|
||||||
wrapMode="none"
|
wrapMode="none"
|
||||||
truncate
|
truncate
|
||||||
flexShrink={0}
|
flexShrink={0}
|
||||||
|
|
@ -281,7 +286,7 @@ export function RunFooterMenu(props: {
|
||||||
{row.item.footer ? (
|
{row.item.footer ? (
|
||||||
<text
|
<text
|
||||||
fg={active() ? props.theme().selectedText : props.theme().muted}
|
fg={active() ? props.theme().selectedText : props.theme().muted}
|
||||||
attributes={active() ? TextAttributes.BOLD : undefined}
|
attributes={attributes()}
|
||||||
wrapMode="none"
|
wrapMode="none"
|
||||||
truncate
|
truncate
|
||||||
flexShrink={0}
|
flexShrink={0}
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@
|
||||||
// The diff view (when available) uses the same diff component as scrollback
|
// The diff view (when available) uses the same diff component as scrollback
|
||||||
// tool snapshots.
|
// tool snapshots.
|
||||||
/** @jsxImportSource @opentui/solid */
|
/** @jsxImportSource @opentui/solid */
|
||||||
import type { TextareaRenderable } from "@opentui/core"
|
import { TextAttributes, type TextareaRenderable } from "@opentui/core"
|
||||||
import { useKeyboard, useTerminalDimensions } from "@opentui/solid"
|
import { useKeyboard, useTerminalDimensions } from "@opentui/solid"
|
||||||
import { For, Match, Show, Switch, createEffect, createMemo, createSignal } from "solid-js"
|
import { For, Match, Show, Switch, createEffect, createMemo, createSignal } from "solid-js"
|
||||||
import {
|
import {
|
||||||
|
|
@ -40,6 +40,7 @@ function buttons(
|
||||||
disabled: boolean,
|
disabled: boolean,
|
||||||
onHover: (option: PermissionOption) => void,
|
onHover: (option: PermissionOption) => void,
|
||||||
onSelect: (option: PermissionOption) => void,
|
onSelect: (option: PermissionOption) => void,
|
||||||
|
mono: boolean,
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<box flexDirection="row" gap={1} flexShrink={0}>
|
<box flexDirection="row" gap={1} flexShrink={0}>
|
||||||
|
|
@ -56,7 +57,12 @@ function buttons(
|
||||||
if (!disabled) onSelect(option)
|
if (!disabled) onSelect(option)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<text fg={option === selected ? theme.surface : theme.muted}>{permissionLabel(option)}</text>
|
<text
|
||||||
|
fg={option === selected ? theme.surface : theme.muted}
|
||||||
|
attributes={option === selected && mono ? TextAttributes.INVERSE : undefined}
|
||||||
|
>
|
||||||
|
{permissionLabel(option)}
|
||||||
|
</text>
|
||||||
</box>
|
</box>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
|
|
@ -134,12 +140,20 @@ export function RunPermissionBody(props: {
|
||||||
theme: RunFooterTheme
|
theme: RunFooterTheme
|
||||||
block: RunBlockTheme
|
block: RunBlockTheme
|
||||||
onReply: (input: PermissionReply) => void | Promise<void>
|
onReply: (input: PermissionReply) => void | Promise<void>
|
||||||
|
mono?: boolean
|
||||||
}) {
|
}) {
|
||||||
const dims = useTerminalDimensions()
|
const dims = useTerminalDimensions()
|
||||||
const [state, setState] = createSignal(createPermissionBodyState(props.request))
|
const [state, setState] = createSignal(createPermissionBodyState(props.request))
|
||||||
const info = createMemo(() => permissionInfo(props.request, props.directory?.()))
|
const info = createMemo(() => permissionInfo(props.request, props.directory?.(), props.mono))
|
||||||
const ft = createMemo(() => toolFiletype(info().file))
|
const ft = createMemo(() => toolFiletype(info().file))
|
||||||
const narrow = createMemo(() => footerWidthPolicy(dims().width).dialog.narrow)
|
const narrow = createMemo(() => footerWidthPolicy(dims().width).dialog.narrow)
|
||||||
|
const scrollbar = createMemo(() => ({
|
||||||
|
visible: !props.mono,
|
||||||
|
trackOptions: {
|
||||||
|
backgroundColor: props.theme.surface,
|
||||||
|
foregroundColor: props.theme.line,
|
||||||
|
},
|
||||||
|
}))
|
||||||
const opts = createMemo(() =>
|
const opts = createMemo(() =>
|
||||||
permissionOptions(state().stage).filter((option) => option !== "always" || (props.request.save?.length ?? 0) > 0),
|
permissionOptions(state().stage).filter((option) => option !== "always" || (props.request.save?.length ?? 0) > 0),
|
||||||
)
|
)
|
||||||
|
|
@ -269,7 +283,9 @@ export function RunPermissionBody(props: {
|
||||||
flexShrink={0}
|
flexShrink={0}
|
||||||
>
|
>
|
||||||
<box flexDirection="row" gap={1} paddingLeft={1}>
|
<box flexDirection="row" gap={1} paddingLeft={1}>
|
||||||
<text fg={state().stage === "reject" ? props.theme.error : props.theme.warning}>△</text>
|
<text fg={state().stage === "reject" ? props.theme.error : props.theme.warning}>
|
||||||
|
{props.mono ? "!" : "△"}
|
||||||
|
</text>
|
||||||
<text fg={props.theme.text}>{title()}</text>
|
<text fg={props.theme.text}>{title()}</text>
|
||||||
</box>
|
</box>
|
||||||
<Switch>
|
<Switch>
|
||||||
|
|
@ -346,16 +362,7 @@ export function RunPermissionBody(props: {
|
||||||
<box width="100%" flexGrow={1} flexShrink={1} paddingLeft={1} paddingRight={3} paddingBottom={1}>
|
<box width="100%" flexGrow={1} flexShrink={1} paddingLeft={1} paddingRight={3} paddingBottom={1}>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={state().stage === "permission"}>
|
<Match when={state().stage === "permission"}>
|
||||||
<scrollbox
|
<scrollbox width="100%" height="100%" verticalScrollbarOptions={scrollbar()}>
|
||||||
width="100%"
|
|
||||||
height="100%"
|
|
||||||
verticalScrollbarOptions={{
|
|
||||||
trackOptions: {
|
|
||||||
backgroundColor: props.theme.surface,
|
|
||||||
foregroundColor: props.theme.line,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<box width="100%" flexDirection="column" gap={1}>
|
<box width="100%" flexDirection="column" gap={1}>
|
||||||
<Show
|
<Show
|
||||||
when={info().diff}
|
when={info().diff}
|
||||||
|
|
@ -427,16 +434,7 @@ export function RunPermissionBody(props: {
|
||||||
</scrollbox>
|
</scrollbox>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={true}>
|
<Match when={true}>
|
||||||
<scrollbox
|
<scrollbox width="100%" height="100%" verticalScrollbarOptions={scrollbar()}>
|
||||||
width="100%"
|
|
||||||
height="100%"
|
|
||||||
verticalScrollbarOptions={{
|
|
||||||
trackOptions: {
|
|
||||||
backgroundColor: props.theme.surface,
|
|
||||||
foregroundColor: props.theme.line,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<box width="100%" flexDirection="column" gap={1} paddingLeft={1}>
|
<box width="100%" flexDirection="column" gap={1} paddingLeft={1}>
|
||||||
<For each={permissionAlwaysLines(props.request)}>
|
<For each={permissionAlwaysLines(props.request)}>
|
||||||
{(line) => (
|
{(line) => (
|
||||||
|
|
@ -472,6 +470,7 @@ export function RunPermissionBody(props: {
|
||||||
setState((prev) => permissionHover(prev, option))
|
setState((prev) => permissionHover(prev, option))
|
||||||
},
|
},
|
||||||
run,
|
run,
|
||||||
|
props.mono ?? false,
|
||||||
)}
|
)}
|
||||||
<Show
|
<Show
|
||||||
when={!busy()}
|
when={!busy()}
|
||||||
|
|
@ -483,7 +482,7 @@ export function RunPermissionBody(props: {
|
||||||
>
|
>
|
||||||
<box flexDirection="row" gap={2} flexShrink={0}>
|
<box flexDirection="row" gap={2} flexShrink={0}>
|
||||||
<text fg={props.theme.text}>
|
<text fg={props.theme.text}>
|
||||||
{"⇆"} <span style={{ fg: props.theme.muted }}>select</span>
|
{props.mono ? "left/right" : "⇆"} <span style={{ fg: props.theme.muted }}>select</span>
|
||||||
</text>
|
</text>
|
||||||
<text fg={props.theme.text}>
|
<text fg={props.theme.text}>
|
||||||
enter <span style={{ fg: props.theme.muted }}>confirm</span>
|
enter <span style={{ fg: props.theme.muted }}>confirm</span>
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ import {
|
||||||
import { parseFileLineRange, parseSlashHead, stripFileLineRange } from "../prompt/parse"
|
import { parseFileLineRange, parseSlashHead, stripFileLineRange } from "../prompt/parse"
|
||||||
import { Keymap } from "../context/keymap"
|
import { Keymap } from "../context/keymap"
|
||||||
import { realignEditorPromptParts, resolveEditorSlashValue } from "./prompt.editor"
|
import { realignEditorPromptParts, resolveEditorSlashValue } from "./prompt.editor"
|
||||||
|
import { monoTruncateMiddle } from "./mono"
|
||||||
import { FOOTER_MENU_ROWS, createFooterMenuState, type RunFooterMenuItem } from "./footer.menu"
|
import { FOOTER_MENU_ROWS, createFooterMenuState, type RunFooterMenuItem } from "./footer.menu"
|
||||||
import type { RunFooterTheme } from "./theme"
|
import type { RunFooterTheme } from "./theme"
|
||||||
import type { FooterState, RunAgent, RunCommand, RunPrompt, RunPromptPart, RunReference, RunTuiConfig } from "./types"
|
import type { FooterState, RunAgent, RunCommand, RunPrompt, RunPromptPart, RunReference, RunTuiConfig } from "./types"
|
||||||
|
|
@ -70,6 +71,7 @@ type PromptInput = {
|
||||||
prompt: Accessor<boolean>
|
prompt: Accessor<boolean>
|
||||||
width: Accessor<number>
|
width: Accessor<number>
|
||||||
theme: Accessor<RunFooterTheme>
|
theme: Accessor<RunFooterTheme>
|
||||||
|
mono: Accessor<boolean>
|
||||||
history?: Accessor<RunPrompt[]>
|
history?: Accessor<RunPrompt[]>
|
||||||
onSubmit: (input: RunPrompt) => boolean | Promise<boolean>
|
onSubmit: (input: RunPrompt) => boolean | Promise<boolean>
|
||||||
onCycle: () => void
|
onCycle: () => void
|
||||||
|
|
@ -302,7 +304,9 @@ export function createPromptState(input: PromptInput): PromptState {
|
||||||
const references = createMemo<Auto[]>(() => {
|
const references = createMemo<Auto[]>(() => {
|
||||||
return input.references().map((item) => ({
|
return input.references().map((item) => ({
|
||||||
kind: "mention",
|
kind: "mention",
|
||||||
display: Locale.truncateMiddle("@" + item.name, width()),
|
display: input.mono()
|
||||||
|
? monoTruncateMiddle("@" + item.name, width(), true)
|
||||||
|
: Locale.truncateMiddle("@" + item.name, width()),
|
||||||
value: item.name,
|
value: item.name,
|
||||||
description: item.description ?? (item.source.type === "git" ? item.source.repository : item.source.path),
|
description: item.description ?? (item.source.type === "git" ? item.source.repository : item.source.path),
|
||||||
part: {
|
part: {
|
||||||
|
|
@ -344,7 +348,9 @@ export function createPromptState(input: PromptInput): PromptState {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
kind: "mention",
|
kind: "mention",
|
||||||
display: Locale.truncateMiddle("@" + filename, width()),
|
display: input.mono()
|
||||||
|
? monoTruncateMiddle("@" + filename, width(), true)
|
||||||
|
: Locale.truncateMiddle("@" + filename, width()),
|
||||||
value: filename,
|
value: filename,
|
||||||
directory: item.endsWith("/"),
|
directory: item.endsWith("/"),
|
||||||
part: {
|
part: {
|
||||||
|
|
|
||||||
|
|
@ -28,20 +28,20 @@ function statusColor(theme: RunFooterTheme, status: FooterSubagentTab["status"])
|
||||||
return theme.highlight
|
return theme.highlight
|
||||||
}
|
}
|
||||||
|
|
||||||
function statusIcon(status: FooterSubagentTab["status"]) {
|
function statusIcon(status: FooterSubagentTab["status"], mono: boolean) {
|
||||||
if (status === "completed") {
|
if (status === "completed") {
|
||||||
return "●"
|
return mono ? "*" : "●"
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status === "cancelled") {
|
if (status === "cancelled") {
|
||||||
return "○"
|
return mono ? "-" : "○"
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status === "error") {
|
if (status === "error") {
|
||||||
return "◍"
|
return mono ? "!" : "◍"
|
||||||
}
|
}
|
||||||
|
|
||||||
return "◔"
|
return mono ? "." : "◔"
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RunFooterSubagentBody(props: {
|
export function RunFooterSubagentBody(props: {
|
||||||
|
|
@ -57,6 +57,7 @@ export function RunFooterSubagentBody(props: {
|
||||||
// command itself is dispatched through the keymap in footer.view.
|
// command itself is dispatched through the keymap in footer.view.
|
||||||
interrupt?: () => string | undefined
|
interrupt?: () => string | undefined
|
||||||
shellOutput?: () => boolean
|
shellOutput?: () => boolean
|
||||||
|
mono?: boolean
|
||||||
}) {
|
}) {
|
||||||
const theme = createMemo(() => props.theme())
|
const theme = createMemo(() => props.theme())
|
||||||
const footer = createMemo(() => theme().footer)
|
const footer = createMemo(() => theme().footer)
|
||||||
|
|
@ -67,6 +68,7 @@ export function RunFooterSubagentBody(props: {
|
||||||
backgroundColor: footer().surface,
|
backgroundColor: footer().surface,
|
||||||
foregroundColor: footer().line,
|
foregroundColor: footer().line,
|
||||||
},
|
},
|
||||||
|
visible: !props.mono,
|
||||||
}))
|
}))
|
||||||
const title = createMemo(() => {
|
const title = createMemo(() => {
|
||||||
const current = tab()
|
const current = tab()
|
||||||
|
|
@ -87,7 +89,11 @@ export function RunFooterSubagentBody(props: {
|
||||||
const rows = indexArray(commits, (commit, index) => (
|
const rows = indexArray(commits, (commit, index) => (
|
||||||
<box flexDirection="column" gap={0} flexShrink={0}>
|
<box flexDirection="column" gap={0} flexShrink={0}>
|
||||||
{index > 0 && separatorRows(commits()[index - 1], commit()) > 0 ? <box height={1} flexShrink={0} /> : null}
|
{index > 0 && separatorRows(commits()[index - 1], commit()) > 0 ? <box height={1} flexShrink={0} /> : null}
|
||||||
<RunEntryContent commit={commit()} theme={theme()} opts={{ shellOutput: props.shellOutput?.() ?? true }} />
|
<RunEntryContent
|
||||||
|
commit={commit()}
|
||||||
|
theme={theme()}
|
||||||
|
opts={{ shellOutput: props.shellOutput?.() ?? true, mono: props.mono }}
|
||||||
|
/>
|
||||||
</box>
|
</box>
|
||||||
))
|
))
|
||||||
let scroll: ScrollBoxRenderable | undefined
|
let scroll: ScrollBoxRenderable | undefined
|
||||||
|
|
@ -134,11 +140,15 @@ export function RunFooterSubagentBody(props: {
|
||||||
<box width="100%" flexDirection="row" gap={1} paddingBottom={1} flexShrink={0}>
|
<box width="100%" flexDirection="row" gap={1} paddingBottom={1} flexShrink={0}>
|
||||||
{current().status === "running" ? (
|
{current().status === "running" ? (
|
||||||
<box flexShrink={0}>
|
<box flexShrink={0}>
|
||||||
<spinner frames={SPINNER_FRAMES} interval={80} color={statusColor(footer(), current().status)} />
|
<spinner
|
||||||
|
frames={props.mono ? ["-", "\\", "|", "/"] : SPINNER_FRAMES}
|
||||||
|
interval={props.mono ? 160 : 80}
|
||||||
|
color={statusColor(footer(), current().status)}
|
||||||
|
/>
|
||||||
</box>
|
</box>
|
||||||
) : (
|
) : (
|
||||||
<text fg={statusColor(footer(), current().status)} wrapMode="none" truncate flexShrink={0}>
|
<text fg={statusColor(footer(), current().status)} wrapMode="none" truncate flexShrink={0}>
|
||||||
{statusIcon(current().status)}
|
{statusIcon(current().status, props.mono ?? false)}
|
||||||
</text>
|
</text>
|
||||||
)}
|
)}
|
||||||
<text fg={footer().text} wrapMode="none" truncate flexGrow={1} flexShrink={1}>
|
<text fg={footer().text} wrapMode="none" truncate flexGrow={1} flexShrink={1}>
|
||||||
|
|
|
||||||
|
|
@ -82,6 +82,7 @@ type RunFooterOptions = {
|
||||||
first: boolean
|
first: boolean
|
||||||
history?: RunPrompt[]
|
history?: RunPrompt[]
|
||||||
theme: RunTheme
|
theme: RunTheme
|
||||||
|
mono: boolean
|
||||||
tuiConfig: RunTuiConfig
|
tuiConfig: RunTuiConfig
|
||||||
miniSettings: {
|
miniSettings: {
|
||||||
current: MiniSettings
|
current: MiniSettings
|
||||||
|
|
@ -202,7 +203,7 @@ export class RunFooter implements FooterApi {
|
||||||
private paletteRefreshRunning = false
|
private paletteRefreshRunning = false
|
||||||
private paletteRefreshQueued = false
|
private paletteRefreshQueued = false
|
||||||
private themeRefreshTimeouts: NodeJS.Timeout[] = []
|
private themeRefreshTimeouts: NodeJS.Timeout[] = []
|
||||||
private unsubscribeThemeSignal: () => void
|
private unsubscribeThemeSignal = () => {}
|
||||||
|
|
||||||
private createScrollback(wrote: boolean): RunScrollbackStream {
|
private createScrollback(wrote: boolean): RunScrollbackStream {
|
||||||
return new RunScrollbackStream(this.renderer, this.theme(), {
|
return new RunScrollbackStream(this.renderer, this.theme(), {
|
||||||
|
|
@ -214,6 +215,7 @@ export class RunFooter implements FooterApi {
|
||||||
.finally(() => this.destroyTheme(theme))
|
.finally(() => this.destroyTheme(theme))
|
||||||
},
|
},
|
||||||
shellOutput: () => this.miniSettings().shell_output === "show",
|
shellOutput: () => this.miniSettings().shell_output === "show",
|
||||||
|
mono: this.options.mono,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -281,10 +283,12 @@ export class RunFooter implements FooterApi {
|
||||||
this.scrollback = this.createScrollback(options.wrote ?? false)
|
this.scrollback = this.createScrollback(options.wrote ?? false)
|
||||||
|
|
||||||
this.renderer.on(CliRenderEvents.DESTROY, this.handleDestroy)
|
this.renderer.on(CliRenderEvents.DESTROY, this.handleDestroy)
|
||||||
this.renderer.on(CliRenderEvents.PALETTE, this.handlePalette)
|
if (!options.mono) {
|
||||||
this.renderer.on(CliRenderEvents.THEME_MODE, this.handleThemeRefresh)
|
this.renderer.on(CliRenderEvents.PALETTE, this.handlePalette)
|
||||||
this.renderer.prependInputHandler(this.handleThemeNotification)
|
this.renderer.on(CliRenderEvents.THEME_MODE, this.handleThemeRefresh)
|
||||||
this.unsubscribeThemeSignal = options.subscribeThemeSignal(this.handleThemeSignal)
|
this.renderer.prependInputHandler(this.handleThemeNotification)
|
||||||
|
this.unsubscribeThemeSignal = options.subscribeThemeSignal(this.handleThemeSignal)
|
||||||
|
}
|
||||||
|
|
||||||
const footer = this
|
const footer = this
|
||||||
void render(
|
void render(
|
||||||
|
|
@ -307,6 +311,7 @@ export class RunFooter implements FooterApi {
|
||||||
variants: footer.variants,
|
variants: footer.variants,
|
||||||
currentVariant: footer.currentVariant,
|
currentVariant: footer.currentVariant,
|
||||||
theme: footer.theme,
|
theme: footer.theme,
|
||||||
|
mono: options.mono,
|
||||||
tuiConfig: options.tuiConfig,
|
tuiConfig: options.tuiConfig,
|
||||||
miniSettings: footer.miniSettings,
|
miniSettings: footer.miniSettings,
|
||||||
history: footer.history,
|
history: footer.history,
|
||||||
|
|
@ -876,7 +881,7 @@ export class RunFooter implements FooterApi {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.setMiniSettings(await this.options.miniSettings.update(change))
|
this.setMiniSettings(await this.options.miniSettings.update(change))
|
||||||
this.setNotice("settings updated")
|
this.setNotice(change.key === "mono" ? "Mono applies after restart" : "settings updated")
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.setNotice("failed to save settings")
|
this.setNotice("failed to save settings")
|
||||||
throw error
|
throw error
|
||||||
|
|
@ -1018,7 +1023,7 @@ export class RunFooter implements FooterApi {
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleThemeRefresh = (): void => {
|
private handleThemeRefresh = (): void => {
|
||||||
if (this.isGone) {
|
if (this.isGone || this.options.mono) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ import { createFormBodyState, type FormBodyState } from "./form.shared"
|
||||||
import { footerWidthPolicy } from "./footer.width"
|
import { footerWidthPolicy } from "./footer.width"
|
||||||
import { Keymap } from "../context/keymap"
|
import { Keymap } from "../context/keymap"
|
||||||
import { modelInfo } from "./variant.shared"
|
import { modelInfo } from "./variant.shared"
|
||||||
|
import { monoShortcut } from "./mono"
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
FooterPromptRoute,
|
FooterPromptRoute,
|
||||||
|
|
@ -84,6 +85,7 @@ type RunFooterViewProps = {
|
||||||
subagent?: () => FooterSubagentState
|
subagent?: () => FooterSubagentState
|
||||||
queuedPrompts?: () => FooterQueuedPrompt[]
|
queuedPrompts?: () => FooterQueuedPrompt[]
|
||||||
theme: () => RunTheme
|
theme: () => RunTheme
|
||||||
|
mono: boolean
|
||||||
tuiConfig: RunTuiConfig
|
tuiConfig: RunTuiConfig
|
||||||
miniSettings: () => MiniSettings
|
miniSettings: () => MiniSettings
|
||||||
history?: () => RunPrompt[]
|
history?: () => RunPrompt[]
|
||||||
|
|
@ -174,14 +176,15 @@ export function RunFooterView(props: RunFooterViewProps) {
|
||||||
return current.type === "subagent" ? subagent().details[current.sessionID] : undefined
|
return current.type === "subagent" ? subagent().details[current.sessionID] : undefined
|
||||||
})
|
})
|
||||||
const shortcuts = Keymap.useShortcuts()
|
const shortcuts = Keymap.useShortcuts()
|
||||||
const command = () => shortcuts.get("command.palette.show") ?? ""
|
const shortcut = (id: string) => monoShortcut(shortcuts.get(id) ?? "", props.mono)
|
||||||
const subagentShortcut = () => shortcuts.get("session.child.first") ?? ""
|
const command = () => shortcut("command.palette.show")
|
||||||
const queuedShortcut = () => shortcuts.get("session.queued_prompts") ?? ""
|
const subagentShortcut = () => shortcut("session.child.first")
|
||||||
const backgroundShortcut = () => shortcuts.get("session.background") ?? ""
|
const queuedShortcut = () => shortcut("session.queued_prompts")
|
||||||
const subagentInterruptShortcut = () => shortcuts.get("subagent.interrupt") ?? ""
|
const backgroundShortcut = () => shortcut("session.background")
|
||||||
const interrupt = () => shortcuts.get("session.interrupt") ?? ""
|
const subagentInterruptShortcut = () => shortcut("subagent.interrupt")
|
||||||
const variantCycle = () => shortcuts.all("variant.cycle") ?? ""
|
const interrupt = () => shortcut("session.interrupt")
|
||||||
const clearShortcut = () => shortcuts.get("prompt.clear") ?? ""
|
const variantCycle = () => monoShortcut(shortcuts.all("variant.cycle") ?? "", props.mono)
|
||||||
|
const clearShortcut = () => shortcut("prompt.clear")
|
||||||
const busy = createMemo(() => props.state().phase === "running")
|
const busy = createMemo(() => props.state().phase === "running")
|
||||||
const armed = createMemo(() => props.state().interrupt > 0)
|
const armed = createMemo(() => props.state().interrupt > 0)
|
||||||
const exiting = createMemo(() => props.state().exit > 0)
|
const exiting = createMemo(() => props.state().exit > 0)
|
||||||
|
|
@ -197,6 +200,12 @@ export function RunFooterView(props: RunFooterViewProps) {
|
||||||
const theme = createMemo(() => runTheme().footer)
|
const theme = createMemo(() => runTheme().footer)
|
||||||
const block = createMemo(() => runTheme().block)
|
const block = createMemo(() => runTheme().block)
|
||||||
const spin = createMemo(() => {
|
const spin = createMemo(() => {
|
||||||
|
if (props.mono) {
|
||||||
|
return {
|
||||||
|
frames: ["-", "\\", "|", "/"],
|
||||||
|
color: theme().text,
|
||||||
|
}
|
||||||
|
}
|
||||||
const options = {
|
const options = {
|
||||||
color: theme().highlight,
|
color: theme().highlight,
|
||||||
style: "blocks" as const,
|
style: "blocks" as const,
|
||||||
|
|
@ -326,6 +335,7 @@ export function RunFooterView(props: RunFooterViewProps) {
|
||||||
prompt,
|
prompt,
|
||||||
width,
|
width,
|
||||||
theme,
|
theme,
|
||||||
|
mono: () => props.mono,
|
||||||
history: props.history,
|
history: props.history,
|
||||||
onSubmit: props.onSubmit,
|
onSubmit: props.onSubmit,
|
||||||
onCycle: props.onCycle,
|
onCycle: props.onCycle,
|
||||||
|
|
@ -380,7 +390,7 @@ export function RunFooterView(props: RunFooterViewProps) {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
return usage()
|
return props.mono ? usage().replaceAll(" · ", " - ") : usage()
|
||||||
})
|
})
|
||||||
const modelStatus = createMemo(() => {
|
const modelStatus = createMemo(() => {
|
||||||
const current = model()
|
const current = model()
|
||||||
|
|
@ -441,7 +451,7 @@ export function RunFooterView(props: RunFooterViewProps) {
|
||||||
return { key: command(), label: "cmd" }
|
return { key: command(), label: "cmd" }
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
const sectionSeparator = () => <span style={{ fg: theme().muted }}>· </span>
|
const sectionSeparator = () => <span style={{ fg: theme().muted }}>{props.mono ? "- " : "· "}</span>
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
props.onRequestExit?.(composer.requestExit)
|
props.onRequestExit?.(composer.requestExit)
|
||||||
|
|
@ -619,7 +629,7 @@ export function RunFooterView(props: RunFooterViewProps) {
|
||||||
? undefined
|
? undefined
|
||||||
: {
|
: {
|
||||||
...EMPTY_BORDER,
|
...EMPTY_BORDER,
|
||||||
vertical: "█",
|
vertical: props.mono ? "|" : "█",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
@ -654,6 +664,7 @@ export function RunFooterView(props: RunFooterViewProps) {
|
||||||
onClose={closePanel}
|
onClose={closePanel}
|
||||||
onSelect={openTab}
|
onSelect={openTab}
|
||||||
onRows={setSubagentMenuRows}
|
onRows={setSubagentMenuRows}
|
||||||
|
mono={props.mono}
|
||||||
/>
|
/>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={selectingQueued()}>
|
<Match when={selectingQueued()}>
|
||||||
|
|
@ -662,6 +673,7 @@ export function RunFooterView(props: RunFooterViewProps) {
|
||||||
prompts={queuedPrompts}
|
prompts={queuedPrompts}
|
||||||
onClose={closePanel}
|
onClose={closePanel}
|
||||||
onRows={setSubagentMenuRows}
|
onRows={setSubagentMenuRows}
|
||||||
|
mono={props.mono}
|
||||||
/>
|
/>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={commanding()}>
|
<Match when={commanding()}>
|
||||||
|
|
@ -696,6 +708,7 @@ export function RunFooterView(props: RunFooterViewProps) {
|
||||||
closePanel()
|
closePanel()
|
||||||
}}
|
}}
|
||||||
onExit={props.onExit}
|
onExit={props.onExit}
|
||||||
|
mono={props.mono}
|
||||||
/>
|
/>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={skilling()}>
|
<Match when={skilling()}>
|
||||||
|
|
@ -715,6 +728,7 @@ export function RunFooterView(props: RunFooterViewProps) {
|
||||||
})
|
})
|
||||||
closePanel()
|
closePanel()
|
||||||
}}
|
}}
|
||||||
|
mono={props.mono}
|
||||||
/>
|
/>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={modeling()}>
|
<Match when={modeling()}>
|
||||||
|
|
@ -727,6 +741,7 @@ export function RunFooterView(props: RunFooterViewProps) {
|
||||||
props.onModelSelect(model)
|
props.onModelSelect(model)
|
||||||
closePanel()
|
closePanel()
|
||||||
}}
|
}}
|
||||||
|
mono={props.mono}
|
||||||
/>
|
/>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={varianting()}>
|
<Match when={varianting()}>
|
||||||
|
|
@ -739,6 +754,7 @@ export function RunFooterView(props: RunFooterViewProps) {
|
||||||
props.onVariantSelect(variant)
|
props.onVariantSelect(variant)
|
||||||
closePanel()
|
closePanel()
|
||||||
}}
|
}}
|
||||||
|
mono={props.mono}
|
||||||
/>
|
/>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={setting()}>
|
<Match when={setting()}>
|
||||||
|
|
@ -747,6 +763,7 @@ export function RunFooterView(props: RunFooterViewProps) {
|
||||||
settings={props.miniSettings}
|
settings={props.miniSettings}
|
||||||
onClose={closePanel}
|
onClose={closePanel}
|
||||||
onChange={props.onMiniSettingChange}
|
onChange={props.onMiniSettingChange}
|
||||||
|
mono={props.mono}
|
||||||
/>
|
/>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={active().type === "permission"}>
|
<Match when={active().type === "permission"}>
|
||||||
|
|
@ -756,6 +773,7 @@ export function RunFooterView(props: RunFooterViewProps) {
|
||||||
theme={theme()}
|
theme={theme()}
|
||||||
block={block()}
|
block={block()}
|
||||||
onReply={props.onPermissionReply}
|
onReply={props.onPermissionReply}
|
||||||
|
mono={props.mono}
|
||||||
/>
|
/>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={active().type === "form"}>
|
<Match when={active().type === "form"}>
|
||||||
|
|
@ -779,6 +797,7 @@ export function RunFooterView(props: RunFooterViewProps) {
|
||||||
settledForms.add(input.formID)
|
settledForms.add(input.formID)
|
||||||
formStates.delete(input.formID)
|
formStates.delete(input.formID)
|
||||||
}}
|
}}
|
||||||
|
mono={props.mono}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
|
|
@ -800,6 +819,7 @@ export function RunFooterView(props: RunFooterViewProps) {
|
||||||
limit={FOOTER_MENU_ROWS}
|
limit={FOOTER_MENU_ROWS}
|
||||||
border={false}
|
border={false}
|
||||||
paddingLeft={0}
|
paddingLeft={0}
|
||||||
|
mono={props.mono}
|
||||||
/>
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
|
|
@ -814,7 +834,12 @@ export function RunFooterView(props: RunFooterViewProps) {
|
||||||
>
|
>
|
||||||
<Show when={modeLabel()}>
|
<Show when={modeLabel()}>
|
||||||
{(label) => (
|
{(label) => (
|
||||||
<box paddingLeft={1} paddingRight={1} backgroundColor={theme().statusAccent} flexShrink={0}>
|
<box
|
||||||
|
paddingLeft={props.mono ? 0 : 1}
|
||||||
|
paddingRight={1}
|
||||||
|
backgroundColor={theme().statusAccent}
|
||||||
|
flexShrink={0}
|
||||||
|
>
|
||||||
<text wrapMode="none" truncate>
|
<text wrapMode="none" truncate>
|
||||||
<span style={{ fg: modeColor(), bold: true }}>{label()}</span>
|
<span style={{ fg: modeColor(), bold: true }}>{label()}</span>
|
||||||
</text>
|
</text>
|
||||||
|
|
@ -828,7 +853,7 @@ export function RunFooterView(props: RunFooterViewProps) {
|
||||||
flexGrow={1}
|
flexGrow={1}
|
||||||
flexShrink={1}
|
flexShrink={1}
|
||||||
minWidth={12}
|
minWidth={12}
|
||||||
paddingLeft={1}
|
paddingLeft={props.mono ? 0 : 1}
|
||||||
paddingRight={1}
|
paddingRight={1}
|
||||||
backgroundColor="transparent"
|
backgroundColor="transparent"
|
||||||
>
|
>
|
||||||
|
|
@ -914,7 +939,7 @@ export function RunFooterView(props: RunFooterViewProps) {
|
||||||
borderColor={theme().highlight}
|
borderColor={theme().highlight}
|
||||||
customBorderChars={{
|
customBorderChars={{
|
||||||
...EMPTY_BORDER,
|
...EMPTY_BORDER,
|
||||||
vertical: "┃",
|
vertical: props.mono ? "|" : "┃",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<RunFooterSubagentBody
|
<RunFooterSubagentBody
|
||||||
|
|
@ -928,6 +953,7 @@ export function RunFooterView(props: RunFooterViewProps) {
|
||||||
onClose={closeTab}
|
onClose={closeTab}
|
||||||
interrupt={() => subagentInterruptShortcut() || undefined}
|
interrupt={() => subagentInterruptShortcut() || undefined}
|
||||||
shellOutput={() => props.miniSettings().shell_output === "show"}
|
shellOutput={() => props.miniSettings().shell_output === "show"}
|
||||||
|
mono={props.mono}
|
||||||
/>
|
/>
|
||||||
</box>
|
</box>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
|
||||||
54
packages/tui/src/mini/mono.ts
Normal file
54
packages/tui/src/mini/mono.ts
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
const prefixes: Record<number, string> = {
|
||||||
|
0x2192: "->",
|
||||||
|
0x2190: "<-",
|
||||||
|
0x2731: "*",
|
||||||
|
0x2699: "*",
|
||||||
|
0x2716: "!",
|
||||||
|
0x2717: "!",
|
||||||
|
0x25c8: "*",
|
||||||
|
0x25c9: "*",
|
||||||
|
0x27f3: "*",
|
||||||
|
}
|
||||||
|
|
||||||
|
export function monoPrefix(value: string, mono: boolean): string {
|
||||||
|
if (!mono) return value
|
||||||
|
const point = value.codePointAt(0)
|
||||||
|
if (point === undefined) return value
|
||||||
|
const prefix = prefixes[point]
|
||||||
|
if (!prefix) return value
|
||||||
|
return prefix + value.slice(point > 0xffff ? 2 : 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function monoToolText(value: string, mono: boolean): string {
|
||||||
|
const result = monoPrefix(value, mono)
|
||||||
|
if (!mono) return result
|
||||||
|
const separator = ` ${String.fromCodePoint(0xb7)} `
|
||||||
|
const index = result.lastIndexOf(separator)
|
||||||
|
if (index === -1) return result
|
||||||
|
const head = result.slice(0, index)
|
||||||
|
if (!head.includes(" completed") && head !== "patch" && !/^\d+ questions$/.test(head)) return result
|
||||||
|
return result.slice(0, index) + " - " + result.slice(index + separator.length)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function monoShortcut(value: string, mono: boolean): string {
|
||||||
|
if (!mono) return value
|
||||||
|
return value
|
||||||
|
.replaceAll(String.fromCodePoint(0x2192), "right")
|
||||||
|
.replaceAll(String.fromCodePoint(0x2190), "left")
|
||||||
|
.replaceAll(String.fromCodePoint(0x2191), "up")
|
||||||
|
.replaceAll(String.fromCodePoint(0x2193), "down")
|
||||||
|
}
|
||||||
|
|
||||||
|
export function monoTruncate(value: string, width: number, mono: boolean): string {
|
||||||
|
if (!mono || value.length <= width) return value
|
||||||
|
if (width <= 3) return ".".repeat(Math.max(0, width))
|
||||||
|
return value.slice(0, width - 3) + "..."
|
||||||
|
}
|
||||||
|
|
||||||
|
export function monoTruncateMiddle(value: string, width: number, mono: boolean): string {
|
||||||
|
if (!mono || value.length <= width) return value
|
||||||
|
if (width <= 3) return ".".repeat(Math.max(0, width))
|
||||||
|
const available = width - 3
|
||||||
|
const left = Math.ceil(available / 2)
|
||||||
|
return value.slice(0, left) + "..." + value.slice(value.length - (available - left))
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import type { MiniPermissionRequest, PermissionReply } from "./types"
|
import type { MiniPermissionRequest, PermissionReply } from "./types"
|
||||||
import { permissionAlwaysLines, permissionOptionLabel, permissionPresentation } from "../util/permission"
|
import { permissionAlwaysLines, permissionOptionLabel, permissionPresentation } from "../util/permission"
|
||||||
import { toolPath } from "./tool"
|
import { toolPath } from "./tool"
|
||||||
|
import { monoPrefix } from "./mono"
|
||||||
|
|
||||||
export type PermissionStage = "permission" | "always" | "reject"
|
export type PermissionStage = "permission" | "always" | "reject"
|
||||||
export type PermissionOption = "once" | "always" | "reject" | "confirm" | "cancel"
|
export type PermissionOption = "once" | "always" | "reject" | "confirm" | "cancel"
|
||||||
|
|
@ -44,9 +45,9 @@ export function permissionOptions(stage: PermissionStage): PermissionOption[] {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
export function permissionInfo(request: MiniPermissionRequest, directory?: string) {
|
export function permissionInfo(request: MiniPermissionRequest, directory?: string, mono = false) {
|
||||||
const state = request.tool?.state
|
const state = request.tool?.state
|
||||||
return permissionPresentation(
|
const info = permissionPresentation(
|
||||||
{
|
{
|
||||||
action: request.action,
|
action: request.action,
|
||||||
resources: request.resources,
|
resources: request.resources,
|
||||||
|
|
@ -56,6 +57,17 @@ export function permissionInfo(request: MiniPermissionRequest, directory?: strin
|
||||||
},
|
},
|
||||||
(value) => toolPath(value, { home: true, directory }),
|
(value) => toolPath(value, { home: true, directory }),
|
||||||
)
|
)
|
||||||
|
if (!mono) return info
|
||||||
|
return {
|
||||||
|
...info,
|
||||||
|
icon: monoPrefix(info.icon, true),
|
||||||
|
lines: [
|
||||||
|
...(info.diff || info.patch ? [info.diff ?? info.patch!] : []),
|
||||||
|
...info.lines.map((line) => monoPrefix(line, true)),
|
||||||
|
],
|
||||||
|
diff: undefined,
|
||||||
|
patch: undefined,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function permissionLabel(option: PermissionOption): string {
|
export function permissionLabel(option: PermissionOption): string {
|
||||||
|
|
|
||||||
|
|
@ -89,5 +89,6 @@ export function resolveMiniSettings(config?: { mini?: Partial<MiniSettings> }):
|
||||||
thinking: config?.mini?.thinking ?? "hide",
|
thinking: config?.mini?.thinking ?? "hide",
|
||||||
shell_output: config?.mini?.shell_output ?? "hide",
|
shell_output: config?.mini?.shell_output ?? "hide",
|
||||||
turn_summary: config?.mini?.turn_summary ?? "show",
|
turn_summary: config?.mini?.turn_summary ?? "show",
|
||||||
|
mono: config?.mini?.mono ?? false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -164,6 +164,9 @@ function queueSplash(
|
||||||
// the entry splash, RunFooter takes over the footer region.
|
// the entry splash, RunFooter takes over the footer region.
|
||||||
export async function createRuntimeLifecycle(input: LifecycleInput): Promise<Lifecycle> {
|
export async function createRuntimeLifecycle(input: LifecycleInput): Promise<Lifecycle> {
|
||||||
const footerTask = import("./footer")
|
const footerTask = import("./footer")
|
||||||
|
const tuiConfig = await input.tuiConfig
|
||||||
|
const miniSettings = resolveMiniSettings(tuiConfig)
|
||||||
|
const mono = miniSettings.mono
|
||||||
const renderer = await createCliRenderer({
|
const renderer = await createCliRenderer({
|
||||||
stdin: input.host.terminal.stdin,
|
stdin: input.host.terminal.stdin,
|
||||||
targetFps: 30,
|
targetFps: 30,
|
||||||
|
|
@ -172,15 +175,16 @@ export async function createRuntimeLifecycle(input: LifecycleInput): Promise<Lif
|
||||||
autoFocus: false,
|
autoFocus: false,
|
||||||
openConsoleOnError: false,
|
openConsoleOnError: false,
|
||||||
exitOnCtrlC: false,
|
exitOnCtrlC: false,
|
||||||
useKittyKeyboard: { events: input.host.platform === "win32" },
|
useKittyKeyboard: mono
|
||||||
|
? { disambiguate: false, alternateKeys: false, events: false, allKeysAsEscapes: false, reportText: false }
|
||||||
|
: { events: input.host.platform === "win32" },
|
||||||
screenMode: "split-footer",
|
screenMode: "split-footer",
|
||||||
footerHeight: FOOTER_HEIGHT,
|
footerHeight: FOOTER_HEIGHT,
|
||||||
externalOutputMode: "capture-stdout",
|
externalOutputMode: "capture-stdout",
|
||||||
consoleMode: "disabled",
|
consoleMode: "disabled",
|
||||||
clearOnShutdown: false,
|
clearOnShutdown: false,
|
||||||
})
|
})
|
||||||
const tuiConfig = await input.tuiConfig
|
const theme = await resolveRunTheme(renderer, tuiConfig.theme, mono)
|
||||||
const theme = await resolveRunTheme(renderer, tuiConfig.theme)
|
|
||||||
renderer.setBackgroundColor(theme.background)
|
renderer.setBackgroundColor(theme.background)
|
||||||
const state: SplashState = {
|
const state: SplashState = {
|
||||||
entry: false,
|
entry: false,
|
||||||
|
|
@ -190,6 +194,7 @@ export async function createRuntimeLifecycle(input: LifecycleInput): Promise<Lif
|
||||||
const meta = splashMeta({
|
const meta = splashMeta({
|
||||||
title: splash.title,
|
title: splash.title,
|
||||||
session_id: input.sessionID,
|
session_id: input.sessionID,
|
||||||
|
mono,
|
||||||
})
|
})
|
||||||
const labels = footerLabels({
|
const labels = footerLabels({
|
||||||
agent: input.agent,
|
agent: input.agent,
|
||||||
|
|
@ -205,6 +210,7 @@ export async function createRuntimeLifecycle(input: LifecycleInput): Promise<Lif
|
||||||
theme: theme.splash,
|
theme: theme.splash,
|
||||||
showSession: splash.showSession,
|
showSession: splash.showSession,
|
||||||
detail: directoryLabel(input.getDirectory(), input.host.paths.home),
|
detail: directoryLabel(input.getDirectory(), input.host.paths.home),
|
||||||
|
mono,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
await renderer.idle().catch(() => {})
|
await renderer.idle().catch(() => {})
|
||||||
|
|
@ -226,10 +232,11 @@ export async function createRuntimeLifecycle(input: LifecycleInput): Promise<Lif
|
||||||
first: input.first,
|
first: input.first,
|
||||||
history: input.history,
|
history: input.history,
|
||||||
theme,
|
theme,
|
||||||
|
mono,
|
||||||
wrote,
|
wrote,
|
||||||
tuiConfig,
|
tuiConfig,
|
||||||
miniSettings: {
|
miniSettings: {
|
||||||
current: resolveMiniSettings(tuiConfig),
|
current: miniSettings,
|
||||||
update: input.onMiniSettingChange,
|
update: input.onMiniSettingChange,
|
||||||
},
|
},
|
||||||
onPermissionReply: input.onPermissionReply,
|
onPermissionReply: input.onPermissionReply,
|
||||||
|
|
@ -319,8 +326,10 @@ export async function createRuntimeLifecycle(input: LifecycleInput): Promise<Lif
|
||||||
...splashMeta({
|
...splashMeta({
|
||||||
title: splash.title,
|
title: splash.title,
|
||||||
session_id: sessionID,
|
session_id: sessionID,
|
||||||
|
mono,
|
||||||
}),
|
}),
|
||||||
theme: footer.currentTheme().splash,
|
theme: footer.currentTheme().splash,
|
||||||
|
mono,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
await renderer.idle().catch(() => {})
|
await renderer.idle().catch(() => {})
|
||||||
|
|
@ -374,10 +383,12 @@ export async function createRuntimeLifecycle(input: LifecycleInput): Promise<Lif
|
||||||
...splashMeta({
|
...splashMeta({
|
||||||
title: splash.title,
|
title: splash.title,
|
||||||
session_id: next.sessionID ?? input.getSessionID?.() ?? input.sessionID,
|
session_id: next.sessionID ?? input.getSessionID?.() ?? input.sessionID,
|
||||||
|
mono,
|
||||||
}),
|
}),
|
||||||
theme: footer.currentTheme().splash,
|
theme: footer.currentTheme().splash,
|
||||||
showSession: splash.showSession,
|
showSession: splash.showSession,
|
||||||
detail: directoryLabel(input.getDirectory(), input.host.paths.home),
|
detail: directoryLabel(input.getDirectory(), input.host.paths.home),
|
||||||
|
mono,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
renderer.requestRender()
|
renderer.requestRender()
|
||||||
|
|
|
||||||
|
|
@ -89,6 +89,7 @@ export class RunScrollbackStream {
|
||||||
private treeSitterClient: TreeSitterClient | undefined
|
private treeSitterClient: TreeSitterClient | undefined
|
||||||
private wrote: boolean
|
private wrote: boolean
|
||||||
private shellOutput: () => boolean
|
private shellOutput: () => boolean
|
||||||
|
private mono: boolean
|
||||||
private pendingThemes: RunTheme[] = []
|
private pendingThemes: RunTheme[] = []
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
|
@ -99,11 +100,13 @@ export class RunScrollbackStream {
|
||||||
treeSitterClient?: TreeSitterClient
|
treeSitterClient?: TreeSitterClient
|
||||||
onThemeRelease?: (theme: RunTheme) => void
|
onThemeRelease?: (theme: RunTheme) => void
|
||||||
shellOutput?: () => boolean
|
shellOutput?: () => boolean
|
||||||
|
mono?: boolean
|
||||||
} = {},
|
} = {},
|
||||||
) {
|
) {
|
||||||
this.treeSitterClient = options.treeSitterClient
|
this.treeSitterClient = options.treeSitterClient
|
||||||
this.wrote = options.wrote ?? false
|
this.wrote = options.wrote ?? false
|
||||||
this.shellOutput = options.shellOutput ?? (() => true)
|
this.shellOutput = options.shellOutput ?? (() => true)
|
||||||
|
this.mono = options.mono ?? false
|
||||||
this.onThemeRelease = options.onThemeRelease
|
this.onThemeRelease = options.onThemeRelease
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -351,13 +354,13 @@ export class RunScrollbackStream {
|
||||||
|
|
||||||
if (commit.summary) {
|
if (commit.summary) {
|
||||||
this.writeSpacer(1)
|
this.writeSpacer(1)
|
||||||
this.renderer.writeToScrollback(turnSummaryWriter({ ...commit.summary, theme: this.theme }))
|
this.renderer.writeToScrollback(turnSummaryWriter({ ...commit.summary, theme: this.theme, mono: this.mono }))
|
||||||
this.markRendered(commit)
|
this.markRendered(commit)
|
||||||
this.tail = commit
|
this.tail = commit
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const body = entryBody(commit, { shellOutput: this.shellOutput() })
|
const body = entryBody(commit, { shellOutput: this.shellOutput(), mono: this.mono })
|
||||||
if (body.type === "none") {
|
if (body.type === "none") {
|
||||||
if (entryDone(commit)) {
|
if (entryDone(commit)) {
|
||||||
this.markRendered(await this.finishActive(false))
|
this.markRendered(await this.finishActive(false))
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { entryBody, entryFlags } from "./entry.body"
|
||||||
import { entryColor, entryLook, entrySyntax } from "./scrollback.shared"
|
import { entryColor, entryLook, entrySyntax } from "./scrollback.shared"
|
||||||
import { toolFiletype, toolStructuredFinal } from "./tool"
|
import { toolFiletype, toolStructuredFinal } from "./tool"
|
||||||
import { RUN_THEME_FALLBACK, transparent, type RunTheme } from "./theme"
|
import { RUN_THEME_FALLBACK, transparent, type RunTheme } from "./theme"
|
||||||
import type { EntryLayout, RunEntryBody, ScrollbackOptions, StreamCommit } from "./types"
|
import type { EntryLayout, RunEntryBody, ScrollbackOptions, StreamCommit, TurnSummary } from "./types"
|
||||||
|
|
||||||
export function entryGroupKey(commit: StreamCommit): string | undefined {
|
export function entryGroupKey(commit: StreamCommit): string | undefined {
|
||||||
if (!commit.partID) {
|
if (!commit.partID) {
|
||||||
|
|
@ -281,7 +281,7 @@ export function spacerWriter(): ScrollbackWriter {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function turnSummaryWriter(input: { agent: string; model: string; duration: string; theme: RunTheme }) {
|
export function turnSummaryWriter(input: TurnSummary & { theme: RunTheme; mono?: boolean }) {
|
||||||
return createScrollbackWriter(
|
return createScrollbackWriter(
|
||||||
() => (
|
() => (
|
||||||
<box width="100%" height={1}>
|
<box width="100%" height={1}>
|
||||||
|
|
@ -289,7 +289,7 @@ export function turnSummaryWriter(input: { agent: string; model: string; duratio
|
||||||
<span style={{ fg: input.theme.block.text }}>{input.agent}</span>
|
<span style={{ fg: input.theme.block.text }}>{input.agent}</span>
|
||||||
<span style={{ fg: input.theme.block.muted }}>
|
<span style={{ fg: input.theme.block.muted }}>
|
||||||
{" "}
|
{" "}
|
||||||
· {input.model} · {input.duration}
|
{input.mono ? "-" : "·"} {input.model} {input.mono ? "-" : "·"} {input.duration}
|
||||||
</span>
|
</span>
|
||||||
</text>
|
</text>
|
||||||
</box>
|
</box>
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import {
|
||||||
} from "@opentui/core"
|
} from "@opentui/core"
|
||||||
import { Locale } from "../util/locale"
|
import { Locale } from "../util/locale"
|
||||||
import { go } from "../logo"
|
import { go } from "../logo"
|
||||||
|
import { monoTruncate, monoTruncateMiddle } from "./mono"
|
||||||
import type { RunSplashTheme } from "./theme"
|
import type { RunSplashTheme } from "./theme"
|
||||||
|
|
||||||
const SPLASH_TITLE_LIMIT = 50
|
const SPLASH_TITLE_LIMIT = 50
|
||||||
|
|
@ -27,6 +28,7 @@ const SPLASH_TITLE_FALLBACK = "Untitled session"
|
||||||
type SplashInput = {
|
type SplashInput = {
|
||||||
title: string | undefined
|
title: string | undefined
|
||||||
session_id: string
|
session_id: string
|
||||||
|
mono?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
type SplashWriterInput = SplashInput & {
|
type SplashWriterInput = SplashInput & {
|
||||||
|
|
@ -69,7 +71,7 @@ function cells(line: string): Cell[] {
|
||||||
return list
|
return list
|
||||||
}
|
}
|
||||||
|
|
||||||
function title(text: string | undefined): string {
|
function title(text: string | undefined, mono = false): string {
|
||||||
if (!text) {
|
if (!text) {
|
||||||
return SPLASH_TITLE_FALLBACK
|
return SPLASH_TITLE_FALLBACK
|
||||||
}
|
}
|
||||||
|
|
@ -94,7 +96,7 @@ function title(text: string | undefined): string {
|
||||||
return SPLASH_TITLE_FALLBACK
|
return SPLASH_TITLE_FALLBACK
|
||||||
}
|
}
|
||||||
|
|
||||||
return Locale.truncate(value, SPLASH_TITLE_LIMIT)
|
return mono ? monoTruncate(value, SPLASH_TITLE_LIMIT, true) : Locale.truncate(value, SPLASH_TITLE_LIMIT)
|
||||||
}
|
}
|
||||||
|
|
||||||
function write(
|
function write(
|
||||||
|
|
@ -181,7 +183,7 @@ function build(input: SplashWriterInput, kind: "entry" | "exit", ctx: Scrollback
|
||||||
let height = 1
|
let height = 1
|
||||||
|
|
||||||
if (kind === "entry") {
|
if (kind === "entry") {
|
||||||
const mark = go.right.slice(1)
|
const mark = input.mono ? ["[O]"] : go.right.slice(1)
|
||||||
const top = 1
|
const top = 1
|
||||||
const body_left = (mark[0]?.length ?? 0) + 2
|
const body_left = (mark[0]?.length ?? 0) + 2
|
||||||
|
|
||||||
|
|
@ -200,16 +202,18 @@ function build(input: SplashWriterInput, kind: "entry" | "exit", ctx: Scrollback
|
||||||
lines,
|
lines,
|
||||||
body_left,
|
body_left,
|
||||||
top + 1,
|
top + 1,
|
||||||
Locale.truncateMiddle(input.detail, Math.max(1, width - body_left)),
|
input.mono
|
||||||
|
? monoTruncateMiddle(input.detail, Math.max(1, width - body_left), true)
|
||||||
|
: Locale.truncateMiddle(input.detail, Math.max(1, width - body_left)),
|
||||||
left,
|
left,
|
||||||
undefined,
|
undefined,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
height = top + mark.length
|
height = top + Math.max(mark.length, input.detail ? 2 : 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (kind === "exit") {
|
if (kind === "exit") {
|
||||||
const mark = go.right.slice(1)
|
const mark = input.mono ? ["[O]"] : go.right.slice(1)
|
||||||
const top = 1
|
const top = 1
|
||||||
const body_left = (mark[0]?.length ?? 0) + 2
|
const body_left = (mark[0]?.length ?? 0) + 2
|
||||||
const session = "Session "
|
const session = "Session "
|
||||||
|
|
@ -239,7 +243,7 @@ function build(input: SplashWriterInput, kind: "entry" | "exit", ctx: Scrollback
|
||||||
undefined,
|
undefined,
|
||||||
TextAttributes.BOLD,
|
TextAttributes.BOLD,
|
||||||
)
|
)
|
||||||
height = top + mark.length
|
height = top + Math.max(mark.length, 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
const root = new BoxRenderable(ctx.renderContext, {
|
const root = new BoxRenderable(ctx.renderContext, {
|
||||||
|
|
@ -266,7 +270,7 @@ function build(input: SplashWriterInput, kind: "entry" | "exit", ctx: Scrollback
|
||||||
|
|
||||||
export function splashMeta(input: SplashInput): SplashMeta {
|
export function splashMeta(input: SplashInput): SplashMeta {
|
||||||
return {
|
return {
|
||||||
title: title(input.title),
|
title: title(input.title, input.mono),
|
||||||
session_id: input.session_id,
|
session_id: input.session_id,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -506,7 +506,71 @@ export const RUN_THEME_FALLBACK: RunTheme = {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function resolveRunTheme(renderer: CliRenderer, config?: RunTuiConfig["theme"]): Promise<RunTheme> {
|
function monoTheme(mode: "dark" | "light"): RunTheme {
|
||||||
|
const foreground = RGBA.defaultForeground(mode === "light" ? "#000000" : "#ffffff")
|
||||||
|
const background = RGBA.defaultBackground(mode === "light" ? "#ffffff" : "#000000")
|
||||||
|
return {
|
||||||
|
background,
|
||||||
|
footer: {
|
||||||
|
highlight: foreground,
|
||||||
|
selected: background,
|
||||||
|
selectedText: foreground,
|
||||||
|
warning: foreground,
|
||||||
|
error: foreground,
|
||||||
|
muted: foreground,
|
||||||
|
text: foreground,
|
||||||
|
status: background,
|
||||||
|
statusAccent: background,
|
||||||
|
shade: background,
|
||||||
|
surface: background,
|
||||||
|
pane: background,
|
||||||
|
border: foreground,
|
||||||
|
line: background,
|
||||||
|
},
|
||||||
|
entry: {
|
||||||
|
system: tone(foreground),
|
||||||
|
user: tone(foreground),
|
||||||
|
assistant: tone(foreground),
|
||||||
|
reasoning: tone(foreground),
|
||||||
|
tool: tone(foreground),
|
||||||
|
error: tone(foreground),
|
||||||
|
},
|
||||||
|
splash: {
|
||||||
|
left: foreground,
|
||||||
|
right: foreground,
|
||||||
|
leftShadow: background,
|
||||||
|
},
|
||||||
|
block: {
|
||||||
|
text: foreground,
|
||||||
|
muted: foreground,
|
||||||
|
diffRemoved: foreground,
|
||||||
|
diffAddedBg: background,
|
||||||
|
diffRemovedBg: background,
|
||||||
|
diffContextBg: background,
|
||||||
|
diffHighlightAdded: foreground,
|
||||||
|
diffHighlightRemoved: foreground,
|
||||||
|
diffLineNumber: foreground,
|
||||||
|
diffAddedLineNumberBg: background,
|
||||||
|
diffRemovedLineNumberBg: background,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RUN_THEME_MONO = monoTheme("dark")
|
||||||
|
const RUN_THEME_MONO_LIGHT = monoTheme("light")
|
||||||
|
|
||||||
|
export async function resolveRunTheme(
|
||||||
|
renderer: CliRenderer,
|
||||||
|
config?: RunTuiConfig["theme"],
|
||||||
|
mono = false,
|
||||||
|
): Promise<RunTheme> {
|
||||||
|
if (mono) {
|
||||||
|
const mode =
|
||||||
|
config?.mode === "light" || config?.mode === "dark"
|
||||||
|
? config.mode
|
||||||
|
: (renderer.themeMode ?? (await renderer.waitForThemeMode(300)))
|
||||||
|
return mode === "light" ? RUN_THEME_MONO_LIGHT : RUN_THEME_MONO
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const colors = await renderer.getPalette({
|
const colors = await renderer.getPalette({
|
||||||
size: 256,
|
size: 256,
|
||||||
|
|
|
||||||
|
|
@ -185,6 +185,7 @@ export type TurnSummary = {
|
||||||
export type ScrollbackOptions = {
|
export type ScrollbackOptions = {
|
||||||
suppressBackgrounds?: boolean
|
suppressBackgrounds?: boolean
|
||||||
shellOutput?: boolean
|
shellOutput?: boolean
|
||||||
|
mono?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ToolCodeSnapshot = {
|
export type ToolCodeSnapshot = {
|
||||||
|
|
@ -397,12 +398,12 @@ export type MiniSettings = {
|
||||||
thinking: "show" | "hide"
|
thinking: "show" | "hide"
|
||||||
shell_output: "show" | "hide"
|
shell_output: "show" | "hide"
|
||||||
turn_summary: "show" | "hide"
|
turn_summary: "show" | "hide"
|
||||||
|
mono: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MiniSettingChange = {
|
export type MiniSettingChange = {
|
||||||
key: keyof MiniSettings
|
[Key in keyof MiniSettings]: { key: Key; value: MiniSettings[Key] }
|
||||||
value: "show" | "hide"
|
}[keyof MiniSettings]
|
||||||
}
|
|
||||||
|
|
||||||
// Lifecycle phase of a scrollback entry. "start" opens the entry, "progress"
|
// Lifecycle phase of a scrollback entry. "start" opens the entry, "progress"
|
||||||
// appends content (coalesced in the footer queue), "final" closes it.
|
// appends content (coalesced in the footer queue), "final" closes it.
|
||||||
|
|
|
||||||
|
|
@ -43,20 +43,21 @@ function structured(next: StreamCommit) {
|
||||||
|
|
||||||
describe("run entry body", () => {
|
describe("run entry body", () => {
|
||||||
test("renders a failed direct shell as an error instead of completed success", () => {
|
test("renders a failed direct shell as an error instead of completed success", () => {
|
||||||
expect(
|
const failed = commit({
|
||||||
entryBody(
|
kind: "tool",
|
||||||
commit({
|
text: "Shell exited with code 7",
|
||||||
kind: "tool",
|
phase: "final",
|
||||||
text: "Shell exited with code 7",
|
source: "tool",
|
||||||
phase: "final",
|
tool: "shell",
|
||||||
source: "tool",
|
toolState: "error",
|
||||||
tool: "shell",
|
toolError: "Shell → exited · code 7",
|
||||||
toolState: "error",
|
shell: { command: "false" },
|
||||||
toolError: "Shell exited with 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",
|
||||||
).toEqual({ type: "text", content: "✖ shell failed: Shell exited with code 7" })
|
content: "! shell failed: Shell → exited · code 7",
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
test("renders assistant, reasoning, and user entries in their display formats", () => {
|
test("renders assistant, reasoning, and user entries in their display formats", () => {
|
||||||
|
|
@ -114,6 +115,11 @@ describe("run entry body", () => {
|
||||||
type: "text",
|
type: "text",
|
||||||
content: "› Inspect footer tabs",
|
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 [
|
for (const item of [
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,8 @@ test("down opens subagents from an empty prompt", async () => {
|
||||||
subagent={subagents}
|
subagent={subagents}
|
||||||
theme={() => RUN_THEME_FALLBACK}
|
theme={() => RUN_THEME_FALLBACK}
|
||||||
tuiConfig={config}
|
tuiConfig={config}
|
||||||
miniSettings={() => ({ thinking: "hide", shell_output: "hide", turn_summary: "show" })}
|
miniSettings={() => ({ thinking: "hide", shell_output: "hide", turn_summary: "show", mono: false })}
|
||||||
|
mono={false}
|
||||||
onSubmit={() => true}
|
onSubmit={() => true}
|
||||||
onPermissionReply={() => {}}
|
onPermissionReply={() => {}}
|
||||||
onFormReply={() => {}}
|
onFormReply={() => {}}
|
||||||
|
|
|
||||||
|
|
@ -121,6 +121,7 @@ async function renderFooter(
|
||||||
view?: FooterView
|
view?: FooterView
|
||||||
onFormReply?: (input: unknown) => void
|
onFormReply?: (input: unknown) => void
|
||||||
miniSettings?: MiniSettings
|
miniSettings?: MiniSettings
|
||||||
|
mono?: boolean
|
||||||
onMiniSettingChange?: (change: MiniSettingChange) => void
|
onMiniSettingChange?: (change: MiniSettingChange) => void
|
||||||
} = {},
|
} = {},
|
||||||
) {
|
) {
|
||||||
|
|
@ -131,7 +132,7 @@ async function renderFooter(
|
||||||
const state = footerState(input.state)
|
const state = footerState(input.state)
|
||||||
const config = input.tuiConfig ?? tuiConfig
|
const config = input.tuiConfig ?? tuiConfig
|
||||||
const [miniSettings] = createSignal<MiniSettings>(
|
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() {
|
function Harness() {
|
||||||
return (
|
return (
|
||||||
|
|
@ -150,6 +151,7 @@ async function renderFooter(
|
||||||
view={view}
|
view={view}
|
||||||
subagent={subagents}
|
subagent={subagents}
|
||||||
theme={input.theme ?? (() => RUN_THEME_FALLBACK)}
|
theme={input.theme ?? (() => RUN_THEME_FALLBACK)}
|
||||||
|
mono={input.mono ?? false}
|
||||||
tuiConfig={config}
|
tuiConfig={config}
|
||||||
miniSettings={miniSettings}
|
miniSettings={miniSettings}
|
||||||
onSubmit={input.onSubmit ?? (() => true)}
|
onSubmit={input.onSubmit ?? (() => true)}
|
||||||
|
|
@ -397,6 +399,7 @@ test("direct command panel renders grouped command palette", async () => {
|
||||||
const frame = app.captureCharFrame()
|
const frame = app.captureCharFrame()
|
||||||
|
|
||||||
expect(frame).toContain("Commands")
|
expect(frame).toContain("Commands")
|
||||||
|
expect(frame).toMatch(/^ {2}Commands/m)
|
||||||
expect(frame).toContain("Search")
|
expect(frame).toContain("Search")
|
||||||
expect(frame).toContain("Session")
|
expect(frame).toContain("Session")
|
||||||
expect(frame).toContain("Agent")
|
expect(frame).toContain("Agent")
|
||||||
|
|
@ -424,6 +427,7 @@ test("direct settings panel changes Mini transcript preferences", async () => {
|
||||||
thinking: "hide",
|
thinking: "hide",
|
||||||
shell_output: "hide",
|
shell_output: "hide",
|
||||||
turn_summary: "show",
|
turn_summary: "show",
|
||||||
|
mono: false,
|
||||||
})
|
})
|
||||||
const app = await testRender(
|
const app = await testRender(
|
||||||
() => (
|
() => (
|
||||||
|
|
@ -435,6 +439,7 @@ test("direct settings panel changes Mini transcript preferences", async () => {
|
||||||
onChange={(change) => {
|
onChange={(change) => {
|
||||||
setSettings((current) => ({ ...current, [change.key]: change.value }))
|
setSettings((current) => ({ ...current, [change.key]: change.value }))
|
||||||
}}
|
}}
|
||||||
|
mono
|
||||||
/>
|
/>
|
||||||
</box>
|
</box>
|
||||||
),
|
),
|
||||||
|
|
@ -443,23 +448,32 @@ test("direct settings panel changes Mini transcript preferences", async () => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await app.renderOnce()
|
await app.renderOnce()
|
||||||
expect(app.captureCharFrame()).toContain("Settings")
|
const frame = app.captureCharFrame()
|
||||||
expect(app.captureCharFrame()).toContain("Thinking")
|
expect(frame).toContain("Settings")
|
||||||
expect(app.captureCharFrame()).toContain("Shell tool output")
|
expect(frame).toMatch(/^ Settings/m)
|
||||||
expect(app.captureCharFrame()).toContain("Turn summary")
|
expect(frame).toContain("Thinking")
|
||||||
expect(app.captureCharFrame()).toContain("left/right change")
|
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")
|
app.mockInput.pressKey("ARROW_RIGHT")
|
||||||
await app.renderOnce()
|
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_DOWN")
|
app.mockInput.pressKey("ARROW_DOWN")
|
||||||
app.mockInput.pressKey("ARROW_RIGHT")
|
app.mockInput.pressKey("ARROW_RIGHT")
|
||||||
await app.renderOnce()
|
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 {
|
} finally {
|
||||||
app.renderer.destroy()
|
app.renderer.destroy()
|
||||||
}
|
}
|
||||||
|
|
@ -935,11 +949,11 @@ test("direct footer closes settings with ctrl-c instead of arming exit", async (
|
||||||
await app.renderOnce()
|
await app.renderOnce()
|
||||||
app.mockInput.pressEnter()
|
app.mockInput.pressEnter()
|
||||||
await app.renderOnce()
|
await app.renderOnce()
|
||||||
expect(app.captureCharFrame()).toContain("Shell tool output")
|
expect(app.captureCharFrame()).toContain("Shell")
|
||||||
|
|
||||||
app.mockInput.pressKey("c", { ctrl: true })
|
app.mockInput.pressKey("c", { ctrl: true })
|
||||||
await app.renderOnce()
|
await app.renderOnce()
|
||||||
expect(app.captureCharFrame()).not.toContain("Shell tool output")
|
expect(app.captureCharFrame()).not.toContain("Shell")
|
||||||
expect(app.renderer.currentFocusedEditor?.plainText).toBe("")
|
expect(app.renderer.currentFocusedEditor?.plainText).toBe("")
|
||||||
} finally {
|
} finally {
|
||||||
app.cleanup()
|
app.cleanup()
|
||||||
|
|
@ -1113,7 +1127,8 @@ test("direct footer shows authoritative pending work while running", async () =>
|
||||||
]}
|
]}
|
||||||
theme={() => RUN_THEME_FALLBACK}
|
theme={() => RUN_THEME_FALLBACK}
|
||||||
tuiConfig={tuiConfig}
|
tuiConfig={tuiConfig}
|
||||||
miniSettings={() => ({ thinking: "hide", shell_output: "hide", turn_summary: "show" })}
|
miniSettings={() => ({ thinking: "hide", shell_output: "hide", turn_summary: "show", mono: false })}
|
||||||
|
mono={false}
|
||||||
onSubmit={() => true}
|
onSubmit={() => true}
|
||||||
onPermissionReply={() => {}}
|
onPermissionReply={() => {}}
|
||||||
onFormReply={() => {}}
|
onFormReply={() => {}}
|
||||||
|
|
@ -1255,14 +1270,17 @@ test("direct footer omits interrupt key hint when interrupt is unbound", async (
|
||||||
const app = await renderFooter({
|
const app = await renderFooter({
|
||||||
tuiConfig: createTuiResolvedConfig({ keybinds: { session_interrupt: "none", input_clear: "ctrl+l" } }),
|
tuiConfig: createTuiResolvedConfig({ keybinds: { session_interrupt: "none", input_clear: "ctrl+l" } }),
|
||||||
state: { phase: "running" },
|
state: { phase: "running" },
|
||||||
|
mono: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await app.renderOnce()
|
await app.renderOnce()
|
||||||
const frame = app.captureCharFrame()
|
const frame = app.captureCharFrame()
|
||||||
|
const statusline = frame.split("\n").find((line) => line.includes("interrupt"))
|
||||||
|
|
||||||
expect(frame).toContain("interrupt")
|
expect(frame).toContain("interrupt")
|
||||||
expect(frame).not.toContain("ctrl+l")
|
expect(frame).not.toContain("ctrl+l")
|
||||||
|
expect(statusline).toMatch(/^\S/)
|
||||||
} finally {
|
} finally {
|
||||||
app.cleanup()
|
app.cleanup()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -155,28 +155,32 @@ describe("run permission shared", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
test("uses source patch text when an edit has no generated diff", () => {
|
test("uses source patch text when an edit has no generated diff", () => {
|
||||||
expect(
|
const patch = '*** Begin Patch\n*** Update File: src/index.ts\n@@\n-old\n+const arrow = "→"\n*** End Patch'
|
||||||
permissionInfo(
|
const request = req({
|
||||||
req({
|
action: "edit",
|
||||||
action: "edit",
|
resources: ["src/index.ts"],
|
||||||
resources: ["src/index.ts"],
|
source: { type: "tool", messageID: "msg-edit", callID: "call-edit" },
|
||||||
source: { type: "tool", messageID: "msg-edit", callID: "call-edit" },
|
tool: canonicalToolPart(
|
||||||
tool: canonicalToolPart(
|
"edit",
|
||||||
"edit",
|
{
|
||||||
{
|
status: "running",
|
||||||
status: "running",
|
input: { patchText: patch },
|
||||||
input: { patchText: "*** Begin Patch\n*** Update File: src/index.ts\n@@\n-old\n+new\n*** End Patch" },
|
structured: {},
|
||||||
structured: {},
|
content: [],
|
||||||
content: [],
|
},
|
||||||
},
|
"call-edit",
|
||||||
"call-edit",
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
),
|
),
|
||||||
).toMatchObject({
|
})
|
||||||
|
expect(permissionInfo(request)).toMatchObject({
|
||||||
title: "Edit src/index.ts",
|
title: "Edit src/index.ts",
|
||||||
diff: undefined,
|
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.theme).toEqual({ mode: "light" })
|
||||||
expect(result.leader.timeout).toBe(450)
|
expect(result.leader.timeout).toBe(450)
|
||||||
expect(result.session?.thinking).toBe("show")
|
expect(result.session?.thinking).toBe("show")
|
||||||
expect(resolveMiniSettings(result)).toEqual({ thinking: "hide", shell_output: "hide", turn_summary: "show" })
|
expect(resolveMiniSettings(result)).toEqual({
|
||||||
expect(resolveMiniSettings({ mini: { thinking: "show", shell_output: "show", turn_summary: "hide" } })).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",
|
thinking: "show",
|
||||||
shell_output: "show",
|
shell_output: "show",
|
||||||
turn_summary: "hide",
|
turn_summary: "hide",
|
||||||
|
mono: true,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { expect, test } from "bun:test"
|
import { expect, test } from "bun:test"
|
||||||
import { RGBA, type CliRenderer, type TerminalColors } from "@opentui/core"
|
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"
|
import { DEFAULT_THEMES } from "../../src/theme"
|
||||||
|
|
||||||
const palette = ["#15161e", "#f7768e", "#9ece6a", "#e0af68", "#7aa2f7", "#bb9af7", "#7dcfff", "#c0caf5"] as const
|
const palette = ["#15161e", "#f7768e", "#9ece6a", "#e0af68", "#7aa2f7", "#bb9af7", "#7dcfff", "#c0caf5"] as const
|
||||||
|
|
@ -23,12 +23,14 @@ function terminalColors(input: Partial<TerminalColors> = {}): TerminalColors {
|
||||||
function renderer(
|
function renderer(
|
||||||
input: {
|
input: {
|
||||||
themeMode?: "dark" | "light"
|
themeMode?: "dark" | "light"
|
||||||
|
resolvedThemeMode?: "dark" | "light"
|
||||||
colors?: TerminalColors
|
colors?: TerminalColors
|
||||||
fail?: boolean
|
fail?: boolean
|
||||||
} = {},
|
} = {},
|
||||||
) {
|
) {
|
||||||
return {
|
return {
|
||||||
themeMode: input.themeMode,
|
themeMode: input.themeMode,
|
||||||
|
waitForThemeMode: async () => input.resolvedThemeMode ?? input.themeMode ?? null,
|
||||||
getPalette: async () => {
|
getPalette: async () => {
|
||||||
if (input.fail) {
|
if (input.fail) {
|
||||||
throw new Error("boom")
|
throw new Error("boom")
|
||||||
|
|
@ -61,6 +63,21 @@ function spread(color: RGBA) {
|
||||||
|
|
||||||
test("falls back when palette lookup fails", async () => {
|
test("falls back when palette lookup fails", async () => {
|
||||||
expect(await resolveRunTheme(renderer({ fail: true }))).toBe(RUN_THEME_FALLBACK)
|
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", () => {
|
test("resolveTheme preserves Mini indexed color and result shape semantics", () => {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue