From 56c19b3aae2ffd98c8fe3b2636cba262e900eb97 Mon Sep 17 00:00:00 2001 From: Logan Yang Date: Sun, 7 Jun 2026 14:56:09 -0700 Subject: [PATCH] fix(chat): keep status-bar clearance in sync so bottom-right banners can't cover the input The desktop chat input reserves space for Obsidian's bottom-right status bar via the `--copilot-status-bar-clearance` variable, but `ChatViewLayout` only re-measured the overlap on `css-change`. When a core or plugin status-bar item appears or grows at runtime (update-available notice, sync status, ...) the bar can get taller without a `css-change` event, so the stale clearance let the banner overlap the chat input and its send button. Attach a ResizeObserver to the status bar so any size change re-runs the measurement. This stays generalizable: it measures the bar's geometry rather than hardcoding any specific banner or plugin, and rebinds across theme reloads that swap the element. Mobile is unaffected (the whole mechanism early-returns on mobile). Closes #99 Co-Authored-By: Claude Opus 4.8 (1M context) --- .../chat-components/ChatViewLayout.ts | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/components/chat-components/ChatViewLayout.ts b/src/components/chat-components/ChatViewLayout.ts index 167b1007..60634d9d 100644 --- a/src/components/chat-components/ChatViewLayout.ts +++ b/src/components/chat-components/ChatViewLayout.ts @@ -12,6 +12,8 @@ const CSS_CHANGE_DEBOUNCE_MS = 600; export class ChatViewLayout { private debounceTimer: number | null = null; private cssChangeRef: ReturnType | null = null; + private statusBarResizeObserver: ResizeObserver | null = null; + private observedStatusBar: HTMLElement | null = null; constructor( private containerEl: HTMLElement, @@ -32,6 +34,9 @@ export class ChatViewLayout { this.workspace.offref(this.cssChangeRef); this.cssChangeRef = null; } + this.statusBarResizeObserver?.disconnect(); + this.statusBarResizeObserver = null; + this.observedStatusBar = null; } /** @@ -43,6 +48,14 @@ export class ChatViewLayout { * reflow so nothing flickers. Themes that already position content above * the status bar (no overlap) get 0 clearance automatically. Auto-hide * themes (opacity: 0) also get 0 since the bar is transparent. + * + * The desktop status bar lives in the bottom-right and hosts core and + * plugin items (update-available notice, sync status, word count, ...). + * When one of those appears or grows -- making the bar taller, e.g. items + * wrap to a second line -- the overlap changes without a `css-change` + * event, so a ResizeObserver on the bar keeps the clearance in sync and + * the chat input from being covered. Measuring the bar's geometry stays + * generalizable: no specific banner or plugin is hardcoded. */ private setupStatusBarClearance(): void { if (Platform.isMobile) return; @@ -53,6 +66,8 @@ export class ChatViewLayout { const viewContent = this.containerEl.querySelector(".view-content"); if (!statusBar || !viewContent) return; + this.observeStatusBar(statusBar, syncClearance); + // Zero out clearance and force reflow to measure natural overlap. // Remove any inline override left from a prior run so the CSS default // (0px) applies and the resulting rect reflects the natural overlap. @@ -81,4 +96,17 @@ export class ChatViewLayout { this.debounceTimer = window.setTimeout(syncClearance, CSS_CHANGE_DEBOUNCE_MS); }); } + + /** + * Keep a single ResizeObserver pointed at the current status bar element so + * runtime size changes re-trigger a measurement. Rebinds when the element is + * replaced (e.g. after a theme reload swaps the DOM); a no-op otherwise. + */ + private observeStatusBar(statusBar: HTMLElement, onResize: () => void): void { + if (this.observedStatusBar === statusBar) return; + this.statusBarResizeObserver?.disconnect(); + this.statusBarResizeObserver = new ResizeObserver(() => onResize()); + this.statusBarResizeObserver.observe(statusBar); + this.observedStatusBar = statusBar; + } }