From e17d917afe102f02111aceff91237e47d1e0c6e0 Mon Sep 17 00:00:00 2001 From: Jinwoo Hong <73622457+Jinwoo-H@users.noreply.github.com> Date: Thu, 16 Apr 2026 01:59:42 -0400 Subject: [PATCH] fix: prevent title bar flickering with multiple split-pane agents (#701) --- .../terminal-pane/pty-connection.ts | 9 +++++++- .../use-terminal-pane-lifecycle.ts | 23 ++++++++++++++++++- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/src/renderer/src/components/terminal-pane/pty-connection.ts b/src/renderer/src/components/terminal-pane/pty-connection.ts index 340f69aaa30..5b933e37362 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection.ts @@ -98,7 +98,14 @@ export function connectPanePty( const onTitleChange = (title: string, rawTitle: string): void => { manager.setPaneGpuRendering(pane.id, !isGeminiTerminalTitle(rawTitle)) deps.setRuntimePaneTitle(deps.tabId, pane.id, title) - deps.updateTabTitle(deps.tabId, title) + // Why: only the focused pane should drive the tab title — otherwise two + // agents in split panes cause rapid title flickering as each emits OSC + // sequences. Mirrors Ghostty's approach: only the active split's title + // propagates to the tab. When focus changes, onActivePaneChange syncs + // the newly active pane's stored title to the tab. + if (manager.getActivePane()?.id === pane.id) { + deps.updateTabTitle(deps.tabId, title) + } if (!hasConsideredInitialCacheTimerSeed) { hasConsideredInitialCacheTimerSeed = true diff --git a/src/renderer/src/components/terminal-pane/use-terminal-pane-lifecycle.ts b/src/renderer/src/components/terminal-pane/use-terminal-pane-lifecycle.ts index f2660b0605e..9304be555b8 100644 --- a/src/renderer/src/components/terminal-pane/use-terminal-pane-lifecycle.ts +++ b/src/renderer/src/components/terminal-pane/use-terminal-pane-lifecycle.ts @@ -304,13 +304,34 @@ export function useTerminalPaneLifecycle({ // Dismiss the rename dialog if it was open for the closed pane, // otherwise it would submit against a non-existent pane. setRenamingPaneId((prev) => (prev === paneId ? null : prev)) + // Why: PaneManager.closePane() reassigns activePaneId directly without + // calling setActivePane(), so onActivePaneChange does not fire. Sync the + // tab title to the survivor's stored title here so the tab label doesn't + // stay stuck on the closed pane's last title. + const newActivePane = managerRef.current?.getActivePane() + if (newActivePane) { + const paneTitles = useAppStore.getState().runtimePaneTitlesByTabId[tabId] ?? {} + const activeTitle = paneTitles[newActivePane.id] + if (activeTitle) { + updateTabTitle(tabId, activeTitle) + } + } scheduleRuntimeGraphSync() }, - onActivePaneChange: () => { + onActivePaneChange: (pane) => { scheduleRuntimeGraphSync() if (shouldPersistLayout) { persistLayoutSnapshot() } + // Why: when the user switches focus between split panes, update the + // tab title to the newly active pane's last-known title so the tab + // label reflects the focused agent — not a stale title from the + // previously focused pane. + const paneTitles = useAppStore.getState().runtimePaneTitlesByTabId[tabId] ?? {} + const paneTitle = paneTitles[pane.id] + if (paneTitle) { + updateTabTitle(tabId, paneTitle) + } }, onLayoutChanged: () => { scheduleRuntimeGraphSync()