fix(terminal): keep SSH reconnect banner above the xterm canvas (#10242)

Portal the non-blocking reconnect banner into each pane container so WebGL
compositing cannot paint terminal text through it, raise z-index above
pane-local chrome, and use a fully opaque card background.
This commit is contained in:
Neil 2026-07-23 17:07:27 -07:00 committed by GitHub
parent 34e0233cf5
commit f26ea6a4a5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 21 additions and 13 deletions

View File

@ -2896,16 +2896,23 @@ export default function TerminalPane({
onRestartDaemon={() => daemonActions.setPending('restart')}
/>
)}
{showSshReconnectOverlay && sshReconnectTargetId && sshReconnectStatus ? (
<TerminalSshReconnectOverlay
targetId={sshReconnectTargetId}
targetLabel={sshReconnectTargetLabel}
status={sshReconnectStatus}
targetRemoved={sshReconnectTargetRemoved}
worktreeId={worktreeId}
sshOwnerEnvironmentId={sshReconnectEnvironmentId}
/>
) : null}
{/* Why: portal into the pane so the banner stacks above the xterm canvas (sibling mount painted under WebGL). */}
{showSshReconnectOverlay && sshReconnectTargetId && sshReconnectStatus
? managedPanes.map((pane) =>
createPortal(
<TerminalSshReconnectOverlay
targetId={sshReconnectTargetId}
targetLabel={sshReconnectTargetLabel}
status={sshReconnectStatus}
targetRemoved={sshReconnectTargetRemoved}
worktreeId={worktreeId}
sshOwnerEnvironmentId={sshReconnectEnvironmentId}
/>,
pane.container,
`ssh-reconnect-${pane.id}`
)
)
: null}
<DaemonActionDialog api={daemonActions} />
{isActive && (
<TerminalSessionStateSaveFailureDialog

View File

@ -86,7 +86,7 @@ describe('TerminalSshReconnectOverlay', () => {
expect(screen.getByText(/This terminal is waiting for devbox/)).toBeInTheDocument()
expect(screen.getByRole('status')).toBeInTheDocument()
const banner = container.querySelector('[data-terminal-ssh-reconnect-banner="disconnected"]')
expect(banner).toHaveClass('inset-x-3', 'bottom-3')
expect(banner).toHaveClass('inset-x-3', 'bottom-3', 'z-40')
expect(banner).not.toHaveClass('inset-0', 'bg-background/75')
await user.click(screen.getByRole('button', { name: 'Connect' }))

View File

@ -136,13 +136,14 @@ export function TerminalSshReconnectOverlay({
}
}, [isConnecting, mountedRef, setSshConnectionState, sshOwnerEnvironmentId, targetId])
// Why: z-40 clears pane-local chrome (focus rim z-30); bg-card is fully opaque so terminal text cannot paint through.
return (
<div
className="pointer-events-none absolute inset-x-3 bottom-3 z-30 flex justify-center"
className="pointer-events-none absolute inset-x-3 bottom-3 z-40 flex justify-center"
data-terminal-ssh-reconnect-banner={status}
>
<div
className="pointer-events-auto flex w-full max-w-xl items-center gap-3 rounded-md border border-border bg-card/95 px-3 py-3 text-card-foreground shadow-xs backdrop-blur-[1px]"
className="pointer-events-auto flex w-full max-w-xl items-center gap-3 rounded-md border border-border bg-card px-3 py-3 text-card-foreground shadow-xs"
role="status"
aria-live="polite"
>