From e427c9eeae3a32b7985b8dc744aa092e29249494 Mon Sep 17 00:00:00 2001 From: Kail Tian Date: Tue, 25 Aug 2026 15:47:04 +0800 Subject: [PATCH] style(tui): keep short transcripts and composer top-aligned --- tui/src/app.test.ts | 40 ++++++++++++++++++++++++++++++++++++++++ tui/src/transcript.ts | 3 ++- 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/tui/src/app.test.ts b/tui/src/app.test.ts index 8f055ca23..7821d9772 100644 --- a/tui/src/app.test.ts +++ b/tui/src/app.test.ts @@ -14,6 +14,7 @@ import type { WorkspaceScopePayload, } from "./protocol" import type { HostAgentState, HostMetadata, TuiHost } from "./host" +import type { Transcript } from "./transcript" const options: AppOptions = { wsUrl: "ws://localhost.invalid/ws", @@ -123,6 +124,45 @@ describe("NanobotTui layout", () => { const createRenderer = (options: Parameters[0]) => createTestRenderer(options) + test("keeps short transcripts and the composer anchored at the top", async () => { + setup = await createRenderer({ + width: 100, + height: 18, + screenMode: "alternate-screen", + consoleMode: "disabled", + }) + const app = mount(setup) + const ui = app as unknown as { + transcript: Transcript + title: BoxRenderable + status: TextRenderable + } + const positions = () => ({ + transcriptHeight: ui.transcript.root.height, + titleY: ui.title.y, + statusY: ui.status.y, + }) + + let anchoredPositions: ReturnType | undefined + for (const height of [18, 30, 36]) { + setup.resize(100, height) + await setup.renderOnce() + expect(ui.title.y).toBe(ui.transcript.root.y + ui.transcript.root.height) + expect(ui.status.y + ui.status.height).toBeLessThan(setup.renderer.height) + if (!anchoredPositions) anchoredPositions = positions() + else expect(positions()).toEqual(anchoredPositions) + } + + ui.transcript.user("A short prompt") + await setup.renderOnce() + const promptPositions = positions() + expect(promptPositions.titleY).toBeGreaterThan(anchoredPositions?.titleY || 0) + + setup.resize(100, 40) + await setup.renderOnce() + expect(positions()).toEqual(promptPositions) + }) + test("reflows a single retained layout across terminal resizes", async () => { setup = await createRenderer({ width: 100, diff --git a/tui/src/transcript.ts b/tui/src/transcript.ts index 43d982136..d9e528bbd 100644 --- a/tui/src/transcript.ts +++ b/tui/src/transcript.ts @@ -91,7 +91,7 @@ export class Transcript { id: "nanobot-tui-transcript", width: "100%", minHeight: 0, - flexGrow: 1, + flexGrow: 0, scrollX: false, scrollY: true, stickyScroll: true, @@ -99,6 +99,7 @@ export class Transcript { viewportCulling: true, contentOptions: { flexDirection: "column", + minHeight: 0, paddingTop: 1, paddingBottom: 1, paddingLeft: 1,