diff --git a/src/tui/components/event-log-assembly.ts b/src/tui/components/event-log-assembly.ts index 5b2619ee3..23c95286a 100644 --- a/src/tui/components/event-log-assembly.ts +++ b/src/tui/components/event-log-assembly.ts @@ -611,14 +611,17 @@ function blockToLines( ]; } case "user": { - // A subtle box with a blank padded row above and below so the text has - // breathing room. Text starts at column 1 to line up with the assistant's - // "●" marker; a 1-col right margin keeps the fill off the edge. + // Subtle box: paint background only on content rows. Blank spacer rows + // used to be full-width bg fills and could flash as solid grey blocks + // during scroll/repaint when content was empty or mid-frame. + // plainLines/wrapRanges always yield ≥1 row (even for ""), so gate on + // real emptiness rather than userLines.length. + if (!block.content.trim()) return []; const bg = color("userMessageBg"); const LEFT = 1; const RIGHT = 1; const innerWidth = Math.max(1, width - LEFT - RIGHT); - const blankRow = [{ text: " ".repeat(width), backgroundColor: bg }]; + const blankRow: StyledLine = [{ text: "" }]; const userLines = plainLines( compactUserCodeBlocks(block.content), { color: color("text"), backgroundColor: bg }, diff --git a/src/tui/components/event-log.test.ts b/src/tui/components/event-log.test.ts index 387ec112b..3d7f21279 100644 --- a/src/tui/components/event-log.test.ts +++ b/src/tui/components/event-log.test.ts @@ -15,6 +15,7 @@ import { } from "./event-log.js"; import { formatElapsed } from "./in-flight-indicator.js"; import type { ContentBlock, ContentBlockData } from "../use-stream.js"; +import { color } from "../theme.js"; function asBlock(data: ContentBlockData & { id: string }): ContentBlock { return data as ContentBlock; @@ -706,6 +707,25 @@ describe("flat line buffer", () => { } }); + test("user banner spacer rows have no background while body keeps userMessageBg", () => { + const block: ContentBlock = { type: "user", id: "user-bg", content: "hello" }; + const lines = buildLines([block], COLUMNS, false, isExpanded); + // blank spacer, body, blank spacer + expect(lines.length).toBe(3); + const bg = color("userMessageBg"); + expect(lines[0]!.every((seg) => seg.backgroundColor === undefined)).toBe(true); + expect(lines[2]!.every((seg) => seg.backgroundColor === undefined)).toBe(true); + expect(lines[1]!.some((seg) => seg.backgroundColor === bg)).toBe(true); + expect(lines[1]!.every((seg) => seg.backgroundColor === bg)).toBe(true); + }); + + test("empty or whitespace user content yields no grey box", () => { + for (const content of ["", " ", "\n\t"]) { + const block: ContentBlock = { type: "user", id: "empty-user", content }; + expect(buildLines([block], COLUMNS, false, isExpanded)).toEqual([]); + } + }); + test("buildResourceBanner shows brand, workspace path, and optional skills/plugins", () => { const workspace = "/home/user/project"; const banner = buildResourceBanner([{ name: "scribe" }, { name: "tdd" }], ["exa"], 80, workspace);