diff --git a/tui/src/app.test.ts b/tui/src/app.test.ts index da213636a..a1f4403f9 100644 --- a/tui/src/app.test.ts +++ b/tui/src/app.test.ts @@ -20,6 +20,18 @@ const options: AppOptions = { theme: "auto", } +interface HiddenScrollBar { + visible: boolean + slider: { visible: boolean } + startArrow: { visible: boolean } + endArrow: { visible: boolean } +} + +interface HiddenScrollBox { + verticalScrollBar: HiddenScrollBar + horizontalScrollBar: HiddenScrollBar +} + function occurrences(frame: string, value: string): number { return frame.split(value).length - 1 } @@ -718,6 +730,8 @@ describe("NanobotTui layout", () => { shell: { backgroundColor: { intent: string } } composerFrame: { backgroundColor: { intent: string } } composer: { backgroundColor: { intent: string } } + transcript: { root: HiddenScrollBox } + diffViewer: { scroll: HiddenScrollBox } } const lines = setup.captureSpans().lines const spans = lines.flatMap((line) => line.spans) @@ -730,6 +744,14 @@ describe("NanobotTui layout", () => { expect(internals.composer.backgroundColor.intent).toBe("default") expect(spans.length).toBeGreaterThan(0) expect(brandedRows).toHaveLength(0) + for (const scrollBox of [internals.transcript.root, internals.diffViewer.scroll]) { + for (const bar of [scrollBox.verticalScrollBar, scrollBox.horizontalScrollBar]) { + expect(bar.visible).toBeFalse() + expect(bar.slider.visible).toBeFalse() + expect(bar.startArrow.visible).toBeFalse() + expect(bar.endArrow.visible).toBeFalse() + } + } }) test("rethemes the complete retained interface when the terminal appearance changes", async () => { diff --git a/tui/src/diff-viewer.ts b/tui/src/diff-viewer.ts index ae15f98b2..252a45a1f 100644 --- a/tui/src/diff-viewer.ts +++ b/tui/src/diff-viewer.ts @@ -12,6 +12,7 @@ import { } from "@opentui/core" import type { FileEditEvent, HistoryMessage } from "./protocol" +import { hideScrollbars } from "./scrollbox" export interface DiffViewerTheme { text: string @@ -152,9 +153,10 @@ export class DiffViewer { paddingTop: 1, paddingBottom: 1, }, - verticalScrollbarOptions: { visible: true }, + verticalScrollbarOptions: { visible: false }, horizontalScrollbarOptions: { visible: false }, }) + hideScrollbars(this.scroll) this.footer = new TextRenderable(renderer, { id: "nanobot-tui-diff-footer", content: "←/→ file · pgup/pgdn scroll · esc close", diff --git a/tui/src/scrollbox.ts b/tui/src/scrollbox.ts new file mode 100644 index 000000000..ace372c1d --- /dev/null +++ b/tui/src/scrollbox.ts @@ -0,0 +1,14 @@ +import type { ScrollBoxRenderable } from "@opentui/core" + +/** Keep scrolling functional without drawing terminal-dependent block glyphs. */ +export function hideScrollbars(scrollBox: ScrollBoxRenderable): void { + for (const bar of [scrollBox.verticalScrollBar, scrollBox.horizontalScrollBar]) { + // ScrollBar can make itself visible again when content starts overflowing. + // Hiding its children as well keeps embedded terminals deterministic. + bar.visible = false + bar.showArrows = false + bar.slider.visible = false + bar.startArrow.visible = false + bar.endArrow.visible = false + } +} diff --git a/tui/src/transcript.ts b/tui/src/transcript.ts index 0eb68e1e4..0c21e3a49 100644 --- a/tui/src/transcript.ts +++ b/tui/src/transcript.ts @@ -11,6 +11,7 @@ import { } from "@opentui/core" import type { FileEditEvent, HistoryMessage, ToolProgressEvent } from "./protocol" +import { hideScrollbars } from "./scrollbox" export interface TranscriptTheme { text: string @@ -88,8 +89,7 @@ export class Transcript { horizontalScrollbarOptions: { visible: false }, onMouseScroll: () => this.scheduleNavigationUpdate(), }) - this.root.verticalScrollBar.visible = false - this.root.horizontalScrollBar.visible = false + hideScrollbars(this.root) } setTheme(theme: TranscriptTheme): void { @@ -151,7 +151,7 @@ export class Transcript { this.wrote = false this.nextId = 0 this.navigation = { awayFromBottom: false, unseenOutput: false } - this.root.verticalScrollBar.visible = false + hideScrollbars(this.root) this.header(header) this.emitNavigation() }