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
This commit is contained in:
Neil 2026-07-20 21:25:04 -07:00 committed by GitHub
parent a6f54acd0f
commit 8847f615ac
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 45 additions and 2 deletions

View File

@ -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({

View File

@ -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()