mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-08-31 00:03:01 +03:00
fix(tui): prevent terminal cell artifacts
This commit is contained in:
+10
-10
@@ -5,7 +5,7 @@
|
||||
"": {
|
||||
"name": "@nanobot/tui",
|
||||
"dependencies": {
|
||||
"@opentui/core": "0.5.1",
|
||||
"@opentui/core": "0.5.3",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.3.13",
|
||||
@@ -14,23 +14,23 @@
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@opentui/core": ["@opentui/core@0.5.1", "", { "dependencies": { "bun-ffi-structs": "0.3.1", "diff": "9.0.0", "marked": "17.0.1", "string-width": "7.2.0", "strip-ansi": "7.1.2" }, "optionalDependencies": { "@opentui/core-darwin-arm64": "0.5.1", "@opentui/core-darwin-x64": "0.5.1", "@opentui/core-linux-arm64": "0.5.1", "@opentui/core-linux-arm64-musl": "0.5.1", "@opentui/core-linux-x64": "0.5.1", "@opentui/core-linux-x64-musl": "0.5.1", "@opentui/core-win32-arm64": "0.5.1", "@opentui/core-win32-x64": "0.5.1" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-mIBFyqIP4rkhQ35uldLXWawWQ6S9tvNWvmxGmDJ7W9cLXjegG6gKEfZ/4NyIMma755ERs/sqO/pIh3Ytf3DDFg=="],
|
||||
"@opentui/core": ["@opentui/core@0.5.3", "", { "dependencies": { "bun-ffi-structs": "0.3.1", "diff": "9.0.0", "marked": "17.0.1", "string-width": "7.2.0", "strip-ansi": "7.1.2" }, "optionalDependencies": { "@opentui/core-darwin-arm64": "0.5.3", "@opentui/core-darwin-x64": "0.5.3", "@opentui/core-linux-arm64": "0.5.3", "@opentui/core-linux-arm64-musl": "0.5.3", "@opentui/core-linux-x64": "0.5.3", "@opentui/core-linux-x64-musl": "0.5.3", "@opentui/core-win32-arm64": "0.5.3", "@opentui/core-win32-x64": "0.5.3" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-K8EQu44cx0rhnn3v3baCQW18Bpci3GltZayOwVpGGsbiAGL1WUYqwQjuaWsmS0c4dCa9rQ5xCEoHB1C4936nDg=="],
|
||||
|
||||
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.5.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Yl3JBLYRrBN+SxXY/gYaqCT/JNrN50K4xO7hYC+/Si8/FgOrBlbRmfJIUNQdZMMLUvOMA+I813+hDw3xfarBzQ=="],
|
||||
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.5.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-R39YeUqaMb/rH1h6G4MkB4MLVKIrRaUaXLfVqorZM4xgU5BxnfPetRk1vWR9vuLCvDwskg+kQ589kULw0o6AWA=="],
|
||||
|
||||
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.5.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-kqMVu+LGuHSCxYFkVJtmuyLLLTMztILSNnlx1eSpHHUiDV4PMc+zkxwRIXO+o0TFTW3gNUKleKUkggriYje7Vw=="],
|
||||
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.5.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-1pmUas/chTVFGeiN19kaOx+5Xbte/DLhcgKyACwWO0M3+xE3z1v/6QGSyX6CoP5HBpmDroiX+JHv1ic/JlGd/g=="],
|
||||
|
||||
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.5.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-PpE1nCHRkxEvSYyZFMToPHjQoVh50A7+BbgetlTX/5ImXzo6iSO83a+7M/1WgZnNu+uZJf5GZKAAcLoRrvQl3Q=="],
|
||||
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.5.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nMo9Q9VIaQVdw2SNKlwIEWMmf3z+cI4jRdCkh36e2RU1FO7LrIBAEmV1ZuRp1CIFVGPkqCXIizCeckZHTr4yQ=="],
|
||||
|
||||
"@opentui/core-linux-arm64-musl": ["@opentui/core-linux-arm64-musl@0.5.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-rmFMtiCm8I0fESB834sTN/ewoI+QDSber588ZO+i08JR6mbv7hkiKW2H/MhiAY1GxGK4nXApleBMyGVOlVDvgQ=="],
|
||||
"@opentui/core-linux-arm64-musl": ["@opentui/core-linux-arm64-musl@0.5.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-QOYAxbbWrhYo27Cd6m0ATzpEx9YCAKAq82LgfUn0xu+VKXLNu+Q3hMNSVbG0SepUQQZil5rz228R9o9Cs8995w=="],
|
||||
|
||||
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.5.1", "", { "os": "linux", "cpu": "x64" }, "sha512-/CxFxFv+ffMof2nYQrpgEfNkWKkKxYUSfwdt2RdDN5fZRhcxjE949743rV0Oovw5Az63qxPgbyfcZVNVO2HVNg=="],
|
||||
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.5.3", "", { "os": "linux", "cpu": "x64" }, "sha512-hdAYLriLpTj3lvpMyL25GPBzvM2w/n2KCSbIwTmgS2F/dPZYCKJxHEETj2lCvtStSp7KuY8tkg3Xl5RAq1v7gA=="],
|
||||
|
||||
"@opentui/core-linux-x64-musl": ["@opentui/core-linux-x64-musl@0.5.1", "", { "os": "linux", "cpu": "x64" }, "sha512-WO8RjhqKyqW/7P0xHdEVT8JGfU2MO7RlK0kdkNnRSnAEVwsTNd2ibhmKDPLGpo/DKaLuA00CsnrNiLGZZiQJKQ=="],
|
||||
"@opentui/core-linux-x64-musl": ["@opentui/core-linux-x64-musl@0.5.3", "", { "os": "linux", "cpu": "x64" }, "sha512-BkVIiPQ1TOf5/FfmIpf7DQU5rT/FO6ASW5R/o/wonI5Pdul7XiDCu86gzGyk1x5k9Sbh6GLeq1fe8/tPmI7IaA=="],
|
||||
|
||||
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.5.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-AgeTjZbdMxSiuBjyLvcug91qd1Ds6Dlg5z4lCInqL7mPQicDEnKZs5lF2FAaktcU7RPi2wLybbQ/vM0NbpXYmw=="],
|
||||
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.5.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-AjObTyZPU0xsK3Yk8GmhkboK6OcMoHBbydqYAybeHD4+v6axScSuZ3OEI9J05JJ9T7H2nZNky75tsdnjsvZJmg=="],
|
||||
|
||||
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.5.1", "", { "os": "win32", "cpu": "x64" }, "sha512-VttbQHVoZQ5uW5IcQeUHPEx/WFQ2mMflukhhbBjpNSdZOPdzmmC4QGFPQznJVwuzXTnjQ2Nll4AY0ROJ/Q3nkw=="],
|
||||
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.5.3", "", { "os": "win32", "cpu": "x64" }, "sha512-e3nRlF2nSkLKCUPBF32OL9EDgtQDIh2pBo7tjhumpTyJ3qoNOa3us7DsM290Vw4xnakM6jpk9r9NzRf72CuMVg=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="],
|
||||
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@
|
||||
"test": "bun test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@opentui/core": "0.5.1"
|
||||
"@opentui/core": "0.5.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.3.13",
|
||||
|
||||
@@ -646,13 +646,28 @@ describe("NanobotTui layout", () => {
|
||||
runtimeControls: {
|
||||
modelText: TextRenderable
|
||||
accessText: TextRenderable
|
||||
contextText: TextRenderable
|
||||
visible: boolean
|
||||
menuRoot: { getChildren(): unknown[] }
|
||||
}
|
||||
titleText: TextRenderable
|
||||
status: TextRenderable
|
||||
meta: TextRenderable
|
||||
}
|
||||
|
||||
try {
|
||||
await waitUntil(() => (app as unknown as { ready: boolean }).ready)
|
||||
await setup.renderOnce()
|
||||
expect(setup.renderer.getCursorState()).toMatchObject({
|
||||
style: "line",
|
||||
blinking: false,
|
||||
})
|
||||
expect(ui.runtimeControls.modelText.selectable).toBe(false)
|
||||
expect(ui.runtimeControls.accessText.selectable).toBe(false)
|
||||
expect(ui.runtimeControls.contextText.selectable).toBe(false)
|
||||
expect(ui.titleText.selectable).toBe(false)
|
||||
expect(ui.status.selectable).toBe(false)
|
||||
expect(ui.meta.selectable).toBe(false)
|
||||
app.accept({ event: "goal_status", chat_id: "chat", status: "running" })
|
||||
await setup.flush()
|
||||
await setup.mockMouse.click(
|
||||
@@ -662,6 +677,7 @@ describe("NanobotTui layout", () => {
|
||||
await waitUntil(() => ui.runtimeControls.visible)
|
||||
await setup.flush()
|
||||
const modelRows = ui.runtimeControls.menuRoot.getChildren() as TextRenderable[]
|
||||
expect(modelRows.every((row) => !row.selectable)).toBe(true)
|
||||
const fast = modelRows.find((row) => row.plainText.includes("fast"))
|
||||
if (!fast) throw new Error("fast model row was not rendered")
|
||||
await setup.mockMouse.click(fast.x + 2, fast.y)
|
||||
@@ -1431,6 +1447,24 @@ describe("NanobotTui layout", () => {
|
||||
expect(setup.renderer.getSelection()).toBeNull()
|
||||
})
|
||||
|
||||
test("clears transcript selection when clicking non-content chrome", async () => {
|
||||
setup = await createRenderer({ width: 72, height: 20, screenMode: "alternate-screen" })
|
||||
const app = mount(setup)
|
||||
const status = (app as unknown as { status: TextRenderable }).status
|
||||
app.accept({ event: "delta", chat_id: "chat", text: "selectable answer" })
|
||||
app.accept({ event: "stream_end", chat_id: "chat" })
|
||||
await setup.flush()
|
||||
|
||||
const rows = setup.captureCharFrame().split("\n")
|
||||
const y = rows.findIndex((row) => row.includes("selectable answer"))
|
||||
const x = rows[y]?.indexOf("selectable answer") ?? -1
|
||||
await setup.mockMouse.drag(x, y, x + "selectable answer".length, y)
|
||||
expect(setup.renderer.getSelection()?.getSelectedText()).toBe("selectable answer")
|
||||
|
||||
await setup.mockMouse.click(status.x, status.y)
|
||||
expect(setup.renderer.getSelection()).toBeNull()
|
||||
})
|
||||
|
||||
test("animates one stable status line while the agent works", async () => {
|
||||
setup = await createRenderer({ width: 88, height: 24, screenMode: "alternate-screen" })
|
||||
const app = mount(setup)
|
||||
|
||||
@@ -479,6 +479,14 @@ export class NanobotTui {
|
||||
paddingRight: 1,
|
||||
flexDirection: "column",
|
||||
backgroundColor: RGBA.defaultBackground(),
|
||||
onMouseDown: (event) => {
|
||||
// Selection belongs to transcript/input content, never to empty chrome.
|
||||
// Clearing it here prevents default-background cells from lingering as
|
||||
// opaque blocks in terminals with differential repainting.
|
||||
if (event.button === 0 && event.target && !event.target.selectable) {
|
||||
this.renderer.clearSelection()
|
||||
}
|
||||
},
|
||||
})
|
||||
this.title = new BoxRenderable(renderer, {
|
||||
id: "nanobot-tui-title",
|
||||
@@ -496,6 +504,7 @@ export class NanobotTui {
|
||||
flexShrink: 0,
|
||||
truncate: true,
|
||||
fg: this.palette.muted,
|
||||
selectable: false,
|
||||
})
|
||||
this.runtimeControls = new RuntimeControls(
|
||||
renderer,
|
||||
@@ -553,6 +562,9 @@ export class NanobotTui {
|
||||
backgroundColor: composerSurface,
|
||||
focusedBackgroundColor: composerSurface,
|
||||
cursorColor: this.palette.accent,
|
||||
// A steady line cursor avoids the block-cell trails produced by some
|
||||
// terminals when a retained full-screen UI redraws around the composer.
|
||||
cursorStyle: { style: "line", blinking: false },
|
||||
showCursor: true,
|
||||
keyBindings: [
|
||||
{ name: "return", shift: true, action: "newline" },
|
||||
@@ -586,6 +598,7 @@ export class NanobotTui {
|
||||
minWidth: 0,
|
||||
flexGrow: 1,
|
||||
flexShrink: 1,
|
||||
selectable: false,
|
||||
})
|
||||
this.meta = new TextRenderable(renderer, {
|
||||
id: "nanobot-tui-meta",
|
||||
@@ -594,6 +607,7 @@ export class NanobotTui {
|
||||
height: 1,
|
||||
width: "auto",
|
||||
flexShrink: 1,
|
||||
selectable: false,
|
||||
})
|
||||
|
||||
const statusRow = new BoxRenderable(renderer, {
|
||||
|
||||
@@ -119,6 +119,7 @@ export class PickerMenu<T> {
|
||||
width: "100%",
|
||||
height: 1,
|
||||
fg: this.theme.muted,
|
||||
selectable: false,
|
||||
}))
|
||||
return
|
||||
}
|
||||
@@ -131,6 +132,7 @@ export class PickerMenu<T> {
|
||||
height: 1,
|
||||
wrapMode: "none",
|
||||
fg: selected ? this.theme.text : this.theme.muted,
|
||||
selectable: false,
|
||||
...(selected && this.theme.selectedBackground
|
||||
? { backgroundColor: RGBA.fromHex(this.theme.selectedBackground) }
|
||||
: {}),
|
||||
|
||||
@@ -81,6 +81,7 @@ export class RuntimeControls {
|
||||
height: 1,
|
||||
flexShrink: 0,
|
||||
fg: theme.faint,
|
||||
selectable: false,
|
||||
})
|
||||
this.render()
|
||||
}
|
||||
@@ -159,6 +160,7 @@ export class RuntimeControls {
|
||||
height: 1,
|
||||
flexShrink: id === "model" ? 1 : 0,
|
||||
fg: this.theme.muted,
|
||||
selectable: false,
|
||||
onMouseOver: () => { text.fg = this.theme.accent },
|
||||
onMouseOut: () => this.renderColors(),
|
||||
onMouseDown: (event) => {
|
||||
|
||||
Reference in New Issue
Block a user