From 2c4ac844f12abc1430a2bf2791eb50280c754105 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Mon, 6 Apr 2026 17:34:45 -0500 Subject: [PATCH] fix(gsd): add backdrop dimming and viewport padding to notification overlay The notification overlay was rendering too small with few entries, allowing underlying content to bleed through. Added viewport padding to fill the overlay box and a new `backdrop` option to OverlayOptions that dims the background behind modal overlays. --- packages/pi-tui/src/overlay-layout.ts | 13 ++++++++++++- packages/pi-tui/src/tui.ts | 2 ++ .../extensions/gsd/bootstrap/register-shortcuts.ts | 1 + .../gsd/commands/handlers/notifications-handler.ts | 1 + .../extensions/gsd/notification-overlay.ts | 5 +++++ 5 files changed, 21 insertions(+), 1 deletion(-) diff --git a/packages/pi-tui/src/overlay-layout.ts b/packages/pi-tui/src/overlay-layout.ts index 1896c5bba..d3c2d8b6a 100644 --- a/packages/pi-tui/src/overlay-layout.ts +++ b/packages/pi-tui/src/overlay-layout.ts @@ -6,7 +6,7 @@ */ import type { OverlayAnchor, OverlayOptions, SizeValue } from "./tui.js"; -import { extractSegments, sliceByColumn, sliceWithWidth, truncateToWidth, visibleWidth } from "./utils.js"; +import { applyBackgroundToLine, extractSegments, sliceByColumn, sliceWithWidth, truncateToWidth, visibleWidth } from "./utils.js"; import { isImageLine } from "./terminal-image.js"; import { CURSOR_MARKER } from "./tui.js"; @@ -324,6 +324,17 @@ export function compositeOverlays( const viewportStart = Math.max(0, workingHeight - termHeight); + // Apply backdrop dimming if any visible overlay requests it + const hasBackdrop = visibleEntries.some((e) => e.options?.backdrop); + if (hasBackdrop) { + const dimFn = (text: string) => `\x1b[2m${text}\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); + } + } + } + // Composite each overlay for (const { overlayLines, row, col, w } of rendered) { for (let i = 0; i < overlayLines.length; i++) { diff --git a/packages/pi-tui/src/tui.ts b/packages/pi-tui/src/tui.ts index 8e3db6f05..0bdb8c746 100644 --- a/packages/pi-tui/src/tui.ts +++ b/packages/pi-tui/src/tui.ts @@ -141,6 +141,8 @@ export interface OverlayOptions { visible?: (termWidth: number, termHeight: number) => boolean; /** If true, don't capture keyboard focus when shown */ nonCapturing?: boolean; + /** If true, dim the background behind the overlay */ + backdrop?: boolean; } /** diff --git a/src/resources/extensions/gsd/bootstrap/register-shortcuts.ts b/src/resources/extensions/gsd/bootstrap/register-shortcuts.ts index 9abe3fbb8..1e0faf7a0 100644 --- a/src/resources/extensions/gsd/bootstrap/register-shortcuts.ts +++ b/src/resources/extensions/gsd/bootstrap/register-shortcuts.ts @@ -44,6 +44,7 @@ export function registerShortcuts(pi: ExtensionAPI): void { minWidth: 60, maxHeight: "88%", anchor: "center", + backdrop: true, }, }, ); diff --git a/src/resources/extensions/gsd/commands/handlers/notifications-handler.ts b/src/resources/extensions/gsd/commands/handlers/notifications-handler.ts index e66309b95..16d30d49a 100644 --- a/src/resources/extensions/gsd/commands/handlers/notifications-handler.ts +++ b/src/resources/extensions/gsd/commands/handlers/notifications-handler.ts @@ -105,6 +105,7 @@ export async function handleNotificationsCommand( minWidth: 60, maxHeight: "88%", anchor: "center", + backdrop: true, }, }, ); diff --git a/src/resources/extensions/gsd/notification-overlay.ts b/src/resources/extensions/gsd/notification-overlay.ts index ec25c440a..6ab73f729 100644 --- a/src/resources/extensions/gsd/notification-overlay.ts +++ b/src/resources/extensions/gsd/notification-overlay.ts @@ -164,6 +164,11 @@ export class GSDNotificationOverlay { this.scrollOffset = Math.min(this.scrollOffset, maxScroll); const visibleContent = content.slice(this.scrollOffset, this.scrollOffset + visibleContentRows); + // Pad to fill viewport so the overlay covers underlying content + while (visibleContent.length < visibleContentRows) { + visibleContent.push(""); + } + const lines = this.wrapInBox(visibleContent, width); this.cachedWidth = width;