feat(tui): print resume command on exit

This commit is contained in:
chengyongru
2026-08-20 17:20:36 +08:00
committed by chengyongru
parent 997bdfc79d
commit 415df576b4
8 changed files with 57 additions and 9 deletions
+23 -2
View File
@@ -6,7 +6,7 @@ import {
type TestRendererSetup,
} from "@opentui/core/testing"
import { NanobotTui, type AppOptions } from "./app"
import { NanobotTui, sessionExitMessage, type AppOptions } from "./app"
import type { MessageOptions, SlashCommand, WorkspaceScopePayload } from "./protocol"
import type { HostAgentState, HostMetadata, TuiHost } from "./host"
@@ -38,6 +38,12 @@ function occurrences(frame: string, value: string): number {
return frame.split(value).length - 1
}
test("formats a reusable session ID after exit", () => {
expect(sessionExitMessage("resume-chat")).toBe(
"Resume with: nanobot agent --session websocket:resume-chat\n",
)
})
function contrastRatio(foreground: string, background: string): number {
const luminance = (color: string) => {
const channel = (offset: number) => {
@@ -2055,19 +2061,29 @@ describe("NanobotTui layout", () => {
test("destroys the renderer and transport together", async () => {
setup = await createRenderer({ width: 72, height: 20, screenMode: "alternate-screen" })
let closed = false
const exited: string[] = []
const transport = client()
transport.close = () => { closed = true }
const app = NanobotTui.mount(
setup.renderer,
options,
{
...options,
chatId: "original-chat",
onExit: (chatId) => {
expect(setup?.renderer.isDestroyed).toBe(true)
exited.push(chatId)
},
},
transport,
new MockTreeSitterClient({ autoResolveTimeout: 0 }),
)
app.stop()
app.stop()
expect(closed).toBe(true)
expect(setup.renderer.isDestroyed).toBe(true)
expect(exited).toEqual(["chat"])
})
test("exits immediately when Ctrl+C is pressed on an idle empty composer", async () => {
@@ -2244,6 +2260,7 @@ if (process.platform !== "win32") {
NANOBOT_TUI_WS_URL: "ws://127.0.0.1:9/ws",
NANOBOT_TUI_API_URL: "",
NANOBOT_TUI_API_TOKEN: "",
NANOBOT_TUI_CHAT_ID: "resume-chat",
NANOBOT_TUI_MODEL: "test/model",
NANOBOT_TUI_WORKSPACE: "/tmp/nanobot-test",
NANOBOT_TUI_VERSION: "test",
@@ -2278,5 +2295,9 @@ if (process.platform !== "win32") {
expect(error).toBe("")
expect(output).toContain("\x1b[?1049h")
expect(output).toContain("\x1b[?1049l")
expect(output.indexOf("\x1b[?1049l")).toBeLessThan(output.indexOf("Resume with:"))
expect(output).toContain(
"Resume with: nanobot agent --session websocket:resume-chat\n",
)
})
}
+8
View File
@@ -94,6 +94,7 @@ interface AppOptions {
version: string
access: string
theme: "auto" | ThemeMode
onExit?: (chatId: string) => void
}
interface ChatClient {
@@ -336,6 +337,11 @@ function singleLine(value: string, limit = 120): string {
return value.replace(/\s+/gu, " ").trim().slice(0, limit)
}
export function sessionExitMessage(chatId: string): string {
const sessionId = `websocket:${chatId}`
return `Resume with: nanobot agent --session ${sessionId}\n`
}
async function copyWithSystemClipboard(text: string): Promise<void> {
const commands = process.platform === "darwin"
? [["pbcopy"]]
@@ -2312,6 +2318,8 @@ export class NanobotTui {
this.host.release()
this.client.close()
this.renderer.destroy()
const chatId = this.client.activeChatId || this.options.chatId
if (chatId) this.options.onExit?.(chatId)
}
private handleDestroy = (): void => {
+4 -1
View File
@@ -1,4 +1,4 @@
import { NanobotTui, type AppOptions } from "./app"
import { NanobotTui, sessionExitMessage, type AppOptions } from "./app"
import { currentGitBranch } from "./host"
function themePreference(): AppOptions["theme"] {
@@ -32,6 +32,9 @@ const options: AppOptions = {
version: process.env.NANOBOT_TUI_VERSION?.trim() || "dev",
access: process.env.NANOBOT_TUI_ACCESS?.trim() || "workspace access",
theme: themePreference(),
onExit: (chatId) => {
process.stdout.write(sessionExitMessage(chatId))
},
}
let app: NanobotTui | undefined