From 41d5189c4c58a6c4567614f4cb11a7aaf5d6c6c5 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Mon, 6 Apr 2026 19:31:32 -0500 Subject: [PATCH 1/2] fix(gsd): restore consistent overlay height to prevent ghost artifacts Differential renderer can't clear old overlay positions when height changes between filter cycles. Pad to maxVisibleRows so the overlay stays the same size regardless of filter state. --- src/resources/extensions/gsd/notification-overlay.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/resources/extensions/gsd/notification-overlay.ts b/src/resources/extensions/gsd/notification-overlay.ts index 412e68023..902e21b73 100644 --- a/src/resources/extensions/gsd/notification-overlay.ts +++ b/src/resources/extensions/gsd/notification-overlay.ts @@ -163,6 +163,12 @@ export class GSDNotificationOverlay { this.scrollOffset = Math.min(this.scrollOffset, maxScroll); const visibleContent = content.slice(this.scrollOffset, this.scrollOffset + visibleContentRows); + // Pad to consistent height so filter changes don't leave ghost artifacts + // (differential renderer can't clear old overlay positions) + while (visibleContent.length < maxVisibleRows) { + visibleContent.push(""); + } + const lines = this.wrapInBox(visibleContent, width); this.cachedWidth = width; From af158235eb89229429f342bf39e23b494ce2ef74 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Mon, 6 Apr 2026 20:11:07 -0500 Subject: [PATCH 2/2] fix(gsd): remove background color from backdrop, fix message truncation Backdrop was painting empty lines with dark gray background (48;5;233), making the entire screen go black. Now uses dim + gray foreground only. Message truncation now measures actual prefix width with visibleWidth() instead of hardcoded 20-char estimate, and uses truncateToWidth() for proper Unicode handling. --- packages/pi-tui/src/__tests__/overlay-layout.test.ts | 8 ++++---- packages/pi-tui/src/overlay-layout.ts | 5 ++--- src/resources/extensions/gsd/notification-overlay.ts | 12 ++++++------ 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/packages/pi-tui/src/__tests__/overlay-layout.test.ts b/packages/pi-tui/src/__tests__/overlay-layout.test.ts index 20907025a..49d0539da 100644 --- a/packages/pi-tui/src/__tests__/overlay-layout.test.ts +++ b/packages/pi-tui/src/__tests__/overlay-layout.test.ts @@ -34,7 +34,7 @@ describe("compositeOverlays — backdrop", () => { assert.ok(dimmedLine.includes("\x1b[2m"), "base line should be dimmed"); }); - it("backdrop uses 256-color dark gray background", () => { + it("backdrop uses gray foreground for dimming", () => { const base = ["hello world", "second line"]; const overlay = makeEntry(["OV"], { width: 2, @@ -44,11 +44,11 @@ describe("compositeOverlays — backdrop", () => { const result = compositeOverlays(base, [overlay], 20, 20, 2); - // Check a non-overlay line for full backdrop codes + // Check a non-overlay line for backdrop codes (dim + gray fg, no bg) const line = result.find((l) => l.includes("second line")); assert.ok(line, "should have a line containing 'second line'"); - assert.ok(line.includes("\x1b[38;5;242m"), "backdrop should set gray foreground"); - assert.ok(line.includes("\x1b[48;5;233m"), "backdrop should set dark gray background"); + assert.ok(line.includes("\x1b[38;5;240m"), "backdrop should set gray foreground"); + assert.ok(!line.includes("\x1b[48;"), "backdrop should not set background color"); }); it("does not dim when backdrop is false/absent", () => { diff --git a/packages/pi-tui/src/overlay-layout.ts b/packages/pi-tui/src/overlay-layout.ts index ee614f094..5e306ec91 100644 --- a/packages/pi-tui/src/overlay-layout.ts +++ b/packages/pi-tui/src/overlay-layout.ts @@ -325,11 +325,10 @@ export function compositeOverlays( const viewportStart = Math.max(0, workingHeight - termHeight); // Apply backdrop dimming if any visible overlay requests it. - // Uses dim + dark gray background (256-color 233) so the overlay pops visually. + // Uses dim + gray foreground so text fades without painting empty lines. const hasBackdrop = visibleEntries.some((e) => e.options?.backdrop); if (hasBackdrop) { - const dimFn = (text: string) => - `\x1b[2m\x1b[38;5;242m\x1b[48;5;233m${text}\x1b[49m\x1b[39m\x1b[22m`; + const dimFn = (text: string) => `\x1b[2m\x1b[38;5;240m${text}\x1b[39m\x1b[22m`; for (let i = viewportStart; i < result.length; i++) { if (!isImageLine(result[i]) && result[i].length > 0) { result[i] = applyBackgroundToLine(result[i], termWidth, dimFn); diff --git a/src/resources/extensions/gsd/notification-overlay.ts b/src/resources/extensions/gsd/notification-overlay.ts index 902e21b73..2c001d313 100644 --- a/src/resources/extensions/gsd/notification-overlay.ts +++ b/src/resources/extensions/gsd/notification-overlay.ts @@ -258,13 +258,13 @@ export class GSDNotificationOverlay { const time = th.fg("dim", formatTimestamp(entry.ts)); const source = entry.source === "workflow-logger" ? th.fg("dim", " [engine]") : ""; - // First line: icon + timestamp + source - const msgMaxWidth = contentWidth - 20; - const msg = entry.message.length > msgMaxWidth - ? entry.message.slice(0, msgMaxWidth - 1) + "…" - : entry.message; + // Measure actual prefix width to truncate message accurately + const prefix = `${coloredIcon} ${time}${source} `; + const prefixWidth = visibleWidth(prefix); + const msgMaxWidth = Math.max(10, contentWidth - prefixWidth); + const msg = truncateToWidth(entry.message, msgMaxWidth, "…"); - lines.push(row(`${coloredIcon} ${time}${source} ${msg}`)); + lines.push(row(`${prefix}${msg}`)); } return lines;