fix(tui): expose /exit in command menu

This commit is contained in:
Kail Tian
2026-08-19 20:43:57 +08:00
committed by chengyongru
parent ac13ad65cd
commit 1018bdb7fe
4 changed files with 68 additions and 3 deletions
+30
View File
@@ -2106,6 +2106,36 @@ describe("NanobotTui layout", () => {
expect(setup.renderer.isDestroyed).toBe(true)
})
test("discovers and runs the local /exit command", async () => {
setup = await createRenderer({ width: 72, height: 20, screenMode: "alternate-screen" })
const sent: string[] = []
let closed = false
const transport = client(sent)
transport.close = () => { closed = true }
const app = NanobotTui.mount(
setup.renderer,
options,
transport,
new MockTreeSitterClient({ autoResolveTimeout: 0 }),
)
const ui = app as unknown as {
composer: TextareaRenderable
commandMenu: { visible: boolean }
}
await setup.mockInput.typeText("/exit")
await setup.flush()
expect(ui.commandMenu.visible).toBe(true)
expect(setup.captureCharFrame()).toContain("/exit")
expect(ui.composer.plainText).toBe("/exit")
ui.composer.submit()
await waitUntil(() => closed)
expect(sent).toEqual([])
expect(setup.renderer.isDestroyed).toBe(true)
})
})
describe("NanobotTui in a Herdr pane", () => {
+11 -2
View File
@@ -194,6 +194,12 @@ const LOCAL_COMMANDS: TuiCommand[] = [
description: "Continue from an earlier completed reply",
action: "branch",
},
{
command: "/exit",
title: "Exit",
description: "Close this terminal UI",
action: "exit",
},
]
function syntaxStyle(palette: Palette): SyntaxStyle {
@@ -456,7 +462,9 @@ export class NanobotTui {
this.palette = this.activeThemeMode === "light" ? LIGHT : DARK
this.host = host
this.localCommands = host.hosted
? LOCAL_COMMANDS.filter(({ command }) => command === "/context" || command === "/diff")
? LOCAL_COMMANDS.filter(({ command }) => (
command === "/context" || command === "/diff" || command === "/exit"
))
: LOCAL_COMMANDS
this.transcript = new Transcript(
renderer,
@@ -809,7 +817,7 @@ export class NanobotTui {
return
}
if (!visibleContent) return
if (["exit", "quit", "/exit", "/quit", ":q"].includes(visibleContent.toLowerCase())) {
if (["exit", "quit", "/quit", ":q"].includes(visibleContent.toLowerCase())) {
this.quit()
return
}
@@ -826,6 +834,7 @@ export class NanobotTui {
else if (command.command.action === "context") void this.openContext()
else if (command.command.action === "diff") this.openDiff()
else if (command.command.action === "branch") void this.openBranch()
else if (command.command.action === "exit") this.quit()
else this.startNewChat()
return
}
+26
View File
@@ -93,6 +93,32 @@ describe("CommandMenu", () => {
})
})
test("discovers and completes the local exit action", async () => {
setup = await createTestRenderer({ width: 60, height: 12, screenMode: "alternate-screen" })
const menu = new CommandMenu(setup.renderer, {
text: "#FFFFFF",
muted: "#999999",
border: "#555555",
})
setup.renderer.root.add(menu.root)
menu.setCommands([], [{
command: "/exit",
title: "Exit",
description: "Close this terminal UI",
action: "exit",
}])
menu.update("/ex")
await setup.renderOnce()
expect(setup.captureCharFrame()).toContain("/exit")
expect(menu.completion("/ex")).toBe("/exit")
expect(menu.resolve("/exit")).toMatchObject({
source: "tui",
command: { action: "exit" },
})
})
test("resolves argument-sensitive command lifecycles", () => {
const goal: SlashCommand = {
command: "/goal",
+1 -1
View File
@@ -5,7 +5,7 @@ import { PickerMenu, type PickerMenuTheme } from "./picker-menu"
export type CommandMenuTheme = PickerMenuTheme
export type TuiCommandAction = "sessions" | "new-chat" | "context" | "diff" | "branch"
export type TuiCommandAction = "sessions" | "new-chat" | "context" | "diff" | "branch" | "exit"
export interface TuiCommand {
command: string