From 8847f615ac102409f1fcccf4953a4846876561b8 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Mon, 20 Jul 2026 21:25:04 -0700 Subject: [PATCH] fix(terminal): restore clickable link hover after returning to the Orca window (#9659) xterm's Linkifier only re-runs providers when the hovered cell changes; window blur fires mouseleave which clears the current link but leaves the cell cache, so on refocus with a stationary pointer the link stays dead until a scroll. Adds the same guarded hover-cache reset the reveal/on-write paths already use, to the window-wake path. Closes #9116 --- .../terminal-visibility-resume.test.ts | 35 ++++++++++++++++++- .../terminal-visibility-resume.ts | 12 ++++++- 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/src/renderer/src/components/terminal-pane/terminal-visibility-resume.test.ts b/src/renderer/src/components/terminal-pane/terminal-visibility-resume.test.ts index 29d9f27d2..4af5a12dc 100644 --- a/src/renderer/src/components/terminal-pane/terminal-visibility-resume.test.ts +++ b/src/renderer/src/components/terminal-pane/terminal-visibility-resume.test.ts @@ -28,9 +28,11 @@ vi.mock('./terminal-webgl-atlas-recovery', () => ({ scheduleTabRevealWebglAtlasRecovery: () => scheduleTabRevealWebglAtlasRecovery() })) const resetTerminalLinkifierHoverState = vi.fn() +const isTerminalLinkifierHoverActive = vi.fn((_terminal: unknown) => false) vi.mock('@/lib/pane-manager/terminal-linkifier-hover-reset', () => ({ resetTerminalLinkifierHoverState: (terminal: unknown) => - resetTerminalLinkifierHoverState(terminal) + resetTerminalLinkifierHoverState(terminal), + isTerminalLinkifierHoverActive: (terminal: unknown) => isTerminalLinkifierHoverActive(terminal) })) type FakeManager = { @@ -115,6 +117,37 @@ describe('resumeTerminalVisibility reveal repaint', () => { expect(order).toEqual(['resume-rendering', 'reveal-repaint']) }) + it('resets each pane linkifier hover cache on window wake recovery so links recover without a scroll', () => { + const first = { name: 'pane-a' } + const second = { name: 'pane-b' } + const manager = createManager() + manager.getPanes.mockReturnValue([{ terminal: first }, { terminal: second }]) + + recoverVisibleTerminalWindowWake({ + manager: manager as never as PaneManager, + isActive: true, + clearGlyphAtlases: false + }) + + expect(resetTerminalLinkifierHoverState).toHaveBeenCalledWith(first) + expect(resetTerminalLinkifierHoverState).toHaveBeenCalledWith(second) + }) + + it('keeps a genuinely-hovered link intact on window wake recovery', () => { + const hovered = { name: 'hovered-pane' } + const manager = createManager() + manager.getPanes.mockReturnValue([{ terminal: hovered }]) + isTerminalLinkifierHoverActive.mockReturnValueOnce(true) + + recoverVisibleTerminalWindowWake({ + manager: manager as never as PaneManager, + isActive: true, + clearGlyphAtlases: false + }) + + expect(resetTerminalLinkifierHoverState).not.toHaveBeenCalled() + }) + it('schedules the atlas-clearing repaint on genuine wake recovery', () => { const manager = createManager() recoverVisibleTerminalWindowWake({ diff --git a/src/renderer/src/components/terminal-pane/terminal-visibility-resume.ts b/src/renderer/src/components/terminal-pane/terminal-visibility-resume.ts index c330efb37..7fd0bcaff 100644 --- a/src/renderer/src/components/terminal-pane/terminal-visibility-resume.ts +++ b/src/renderer/src/components/terminal-pane/terminal-visibility-resume.ts @@ -9,7 +9,10 @@ import { enforceTerminalCurrentScrollIntent, syncTerminalScrollIntentFromViewport } from '@/lib/pane-manager/terminal-scroll-intent' -import { resetTerminalLinkifierHoverState } from '@/lib/pane-manager/terminal-linkifier-hover-reset' +import { + isTerminalLinkifierHoverActive, + resetTerminalLinkifierHoverState +} from '@/lib/pane-manager/terminal-linkifier-hover-reset' import { fitAndFocusPanes, fitPanes, focusActivePane } from './pane-helpers' import { scheduleTabRevealWebglAtlasRecovery } from './terminal-webgl-atlas-recovery' @@ -143,6 +146,13 @@ export function recoverVisibleTerminalWindowWake({ for (const pane of manager.getPanes()) { requestTerminalBacklogRecovery(pane.terminal) flushTerminalOutput(pane.terminal, { maxChars: WINDOW_WAKE_FLUSH_CHARS }) + // Why: window blur fires mouseleave, clearing xterm's current link but not + // its hover cell cache; on refocus the stationary pointer sits on the same + // cell, so the link stays dead until a scroll. Skip while a link is hovered + // to avoid flickering its underline (same guard as the on-write reset). + if (!isTerminalLinkifierHoverActive(pane.terminal)) { + resetTerminalLinkifierHoverState(pane.terminal) + } } syncTerminalViewportIntents(manager) manager.resumeRendering()