From 411f9f0e90d61c2409bbd30f0eab6195dfb971fb Mon Sep 17 00:00:00 2001 From: Xubin Ren <52506698+Re-bin@users.noreply.github.com> Date: Thu, 13 Aug 2026 01:36:25 +0900 Subject: [PATCH] fix(tui): clear stale composer placeholder --- tui/src/app.test.ts | 24 ++++++++++++++++++++++++ tui/src/app.ts | 17 +++++++++++++++-- 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/tui/src/app.test.ts b/tui/src/app.test.ts index daaf3b724..be86ecf93 100644 --- a/tui/src/app.test.ts +++ b/tui/src/app.test.ts @@ -137,6 +137,30 @@ describe("NanobotTui layout", () => { expect(sent).toEqual(["你好"]) }) + test("clears the placeholder on the first typed character", async () => { + setup = await createRenderer({ width: 72, height: 20, screenMode: "alternate-screen" }) + const app = mount(setup) + const composer = (app as unknown as { composer: TextareaRenderable }).composer + await setup.renderOnce() + expect(setup.captureCharFrame()).toContain("Ask nanobot anything") + + setup.mockInput.typeText("bu") + await setup.flush() + const frame = setup.captureCharFrame() + + expect(composer.plainText).toBe("bu") + expect(composer.placeholder).toBeNull() + expect(frame).toContain("bu") + expect(frame).not.toContain("Ask nanobot anything") + expect(frame).not.toContain("buAsk nanobot anything") + + setup.mockInput.pressBackspace() + setup.mockInput.pressBackspace() + await setup.flush() + expect(composer.placeholder).toBe("Ask nanobot anything") + expect(setup.captureCharFrame()).toContain("Ask nanobot anything") + }) + test("recalls submitted prompts without stealing multiline cursor movement", async () => { const sent: string[] = [] setup = await createRenderer({ width: 72, height: 20, screenMode: "alternate-screen" }) diff --git a/tui/src/app.ts b/tui/src/app.ts index 3223f722f..df59f6f11 100644 --- a/tui/src/app.ts +++ b/tui/src/app.ts @@ -82,6 +82,8 @@ const LIGHT: Palette = { cool: "#0F766E", } +const COMPOSER_PLACEHOLDER = "Ask nanobot anything" + function syntaxStyle(palette: Palette): SyntaxStyle { const color = (value: string) => { const parsed = RGBA.fromHex(value) @@ -231,7 +233,7 @@ export class NanobotTui { minHeight: 1, maxHeight: 8, wrapMode: "word", - placeholder: "Ask nanobot anything", + placeholder: COMPOSER_PLACEHOLDER, placeholderColor: this.palette.faint, textColor: this.palette.text, focusedTextColor: this.palette.text, @@ -243,7 +245,10 @@ export class NanobotTui { { name: "return", action: "submit" }, { name: "return", meta: true, action: "newline" }, ], - onContentChange: () => this.resizeComposer(), + onContentChange: () => { + this.syncComposerPlaceholder() + this.resizeComposer() + }, // IMEs may commit their final composed glyph after Enter. Matching the // OpenCode/OpenTUI integration, defer twice before reading plainText. onSubmit: () => this.deferSubmit(), @@ -673,6 +678,14 @@ export class NanobotTui { this.composerFrame.maxHeight = maxHeight + 2 } + private syncComposerPlaceholder(): void { + // OpenTUI normally suppresses placeholder glyphs while the editor is not + // empty. Explicitly removing them also invalidates their old cells, which + // prevents stale placeholder text in differential/embedded terminals. + const placeholder = this.composer.plainText ? null : COMPOSER_PLACEHOLDER + if (this.composer.placeholder !== placeholder) this.composer.placeholder = placeholder + } + private async copySelection(text: string): Promise { if (!text) return try {