mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-08-31 08:13:11 +03:00
fix(tui): expose /exit in command menu
This commit is contained in:
@@ -2106,6 +2106,36 @@ describe("NanobotTui layout", () => {
|
|||||||
|
|
||||||
expect(setup.renderer.isDestroyed).toBe(true)
|
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", () => {
|
describe("NanobotTui in a Herdr pane", () => {
|
||||||
|
|||||||
+11
-2
@@ -194,6 +194,12 @@ const LOCAL_COMMANDS: TuiCommand[] = [
|
|||||||
description: "Continue from an earlier completed reply",
|
description: "Continue from an earlier completed reply",
|
||||||
action: "branch",
|
action: "branch",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
command: "/exit",
|
||||||
|
title: "Exit",
|
||||||
|
description: "Close this terminal UI",
|
||||||
|
action: "exit",
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
function syntaxStyle(palette: Palette): SyntaxStyle {
|
function syntaxStyle(palette: Palette): SyntaxStyle {
|
||||||
@@ -456,7 +462,9 @@ export class NanobotTui {
|
|||||||
this.palette = this.activeThemeMode === "light" ? LIGHT : DARK
|
this.palette = this.activeThemeMode === "light" ? LIGHT : DARK
|
||||||
this.host = host
|
this.host = host
|
||||||
this.localCommands = host.hosted
|
this.localCommands = host.hosted
|
||||||
? LOCAL_COMMANDS.filter(({ command }) => command === "/context" || command === "/diff")
|
? LOCAL_COMMANDS.filter(({ command }) => (
|
||||||
|
command === "/context" || command === "/diff" || command === "/exit"
|
||||||
|
))
|
||||||
: LOCAL_COMMANDS
|
: LOCAL_COMMANDS
|
||||||
this.transcript = new Transcript(
|
this.transcript = new Transcript(
|
||||||
renderer,
|
renderer,
|
||||||
@@ -809,7 +817,7 @@ export class NanobotTui {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!visibleContent) return
|
if (!visibleContent) return
|
||||||
if (["exit", "quit", "/exit", "/quit", ":q"].includes(visibleContent.toLowerCase())) {
|
if (["exit", "quit", "/quit", ":q"].includes(visibleContent.toLowerCase())) {
|
||||||
this.quit()
|
this.quit()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -826,6 +834,7 @@ export class NanobotTui {
|
|||||||
else if (command.command.action === "context") void this.openContext()
|
else if (command.command.action === "context") void this.openContext()
|
||||||
else if (command.command.action === "diff") this.openDiff()
|
else if (command.command.action === "diff") this.openDiff()
|
||||||
else if (command.command.action === "branch") void this.openBranch()
|
else if (command.command.action === "branch") void this.openBranch()
|
||||||
|
else if (command.command.action === "exit") this.quit()
|
||||||
else this.startNewChat()
|
else this.startNewChat()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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", () => {
|
test("resolves argument-sensitive command lifecycles", () => {
|
||||||
const goal: SlashCommand = {
|
const goal: SlashCommand = {
|
||||||
command: "/goal",
|
command: "/goal",
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { PickerMenu, type PickerMenuTheme } from "./picker-menu"
|
|||||||
|
|
||||||
export type CommandMenuTheme = PickerMenuTheme
|
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 {
|
export interface TuiCommand {
|
||||||
command: string
|
command: string
|
||||||
|
|||||||
Reference in New Issue
Block a user