fix: recover terminal WebGL atlas after corrupting text/URL pastes (#5960)

A single-line URL pasted into a TUI with bracketed-paste mode on (e.g.
Claude Code) plans to mode 'direct' with bracketed=false, so the existing
image-only WebGL atlas recovery never fired and the shared glyph atlas could
stay garbled until a manual repaint.

Recovery now keys off whether the paste reaches the TUI bracketed. The plan
carries a derived `bracketedToTerminal` flag (true when Orca brackets the
bytes, or when bracketed-paste mode wraps a direct paste), and the
menu/keyboard/context-menu/middle-click paste paths schedule recovery off it.
The image path keeps working because it is force-bracketed.

Supersedes community PR #6025, adopting its refactor and fixing the gate so it
fires for the reported repro.

Fixes #5960

Co-authored-by: Anddy Sahir Agudelo <andiagudelo@gmail.com>
This commit is contained in:
Neil
2026-06-27 23:11:48 -07:00
co-authored by Anddy Sahir Agudelo
parent b45cb545c5
commit 8b72336d7d
9 changed files with 120 additions and 32 deletions
@@ -115,7 +115,7 @@ import {
} from '@/components/terminal-quick-commands/TerminalQuickCommandDialog'
import { keybindingMatchesAction } from '../../../../shared/keybindings'
import { pasteTerminalClipboard } from './terminal-clipboard-paste'
import { scheduleImagePasteWebglAtlasRecovery } from './terminal-webgl-paste-recovery'
import { maybeScheduleWebglAtlasRecoveryForPaste } from './terminal-webgl-paste-recovery'
import { restoreTerminalFitToDesktop, restoreTerminalFitsToDesktop } from './terminal-fit-restore'
import { useVisibleTerminalTabClaim } from './use-visible-terminal-tab-claim'
@@ -1635,9 +1635,7 @@ export default function TerminalPane({
if (text) {
recordTerminalUserInputForLeaf(tabId, pane.leafId)
}
if (options?.recoverImagePasteWebglAtlas) {
scheduleImagePasteWebglAtlasRecovery()
}
maybeScheduleWebglAtlasRecoveryForPaste(plan)
}
const pasteFromClipboard = (
@@ -2408,6 +2406,9 @@ export default function TerminalPane({
return
}
recordTerminalUserInputForLeaf(tabId, clickedPane.leafId)
// Why: a middle-click URL paste into a bracketed-paste-mode TUI can
// corrupt the shared WebGL glyph atlas just like menu/keyboard pastes.
maybeScheduleWebglAtlasRecoveryForPaste(plan)
})
},
[getPrimarySelectionMiddleClickPane, tabId, worktreeId]
@@ -22,7 +22,7 @@ describe('terminal clipboard paste', () => {
expect(pasteText).toHaveBeenCalledWith(
'/var/folders/3l/b7w02vh17tg5r5s3nhhdf3kh0000gn/T/orca-paste-1760000000000-id.png',
{ forceBracketedPaste: true, recoverImagePasteWebglAtlas: true }
{ forceBracketedPaste: true }
)
})
@@ -103,8 +103,7 @@ describe('terminal clipboard paste', () => {
runtimeEnvironmentId: undefined
})
expect(pasteText).toHaveBeenCalledWith('/var/tmp/orca-paste-1760000000000-id.png', {
forceBracketedPaste: true,
recoverImagePasteWebglAtlas: true
forceBracketedPaste: true
})
})
@@ -126,8 +125,7 @@ describe('terminal clipboard paste', () => {
runtimeEnvironmentId: 'remote-host-1'
})
expect(pasteText).toHaveBeenCalledWith('/tmp/orca-paste-1760000000000-runtime.png', {
forceBracketedPaste: true,
recoverImagePasteWebglAtlas: true
forceBracketedPaste: true
})
})
@@ -143,8 +141,7 @@ describe('terminal clipboard paste', () => {
})
expect(pasteText).toHaveBeenCalledWith('/tmp/orca-paste-1760000000000-id.png', {
forceBracketedPaste: true,
recoverImagePasteWebglAtlas: true
forceBracketedPaste: true
})
})
@@ -165,8 +162,7 @@ describe('terminal clipboard paste', () => {
runtimeEnvironmentId: undefined
})
expect(pasteText).toHaveBeenCalledWith('/tmp/orca-paste-1760000000000-id.png', {
forceBracketedPaste: true,
recoverImagePasteWebglAtlas: true
forceBracketedPaste: true
})
})
@@ -83,8 +83,10 @@ export async function pasteTerminalClipboard({
const result = await pasteText(filePath, {
// Why: a generated clipboard-image path is terminal image injection, not
// ordinary one-line text. Keep it off the Ctrl+C stale-text paste path.
forceBracketedPaste: true,
recoverImagePasteWebglAtlas: true
// WebGL atlas recovery now keys off the resulting bracketed paste, so it
// covers this force-bracketed image path and corrupting text/URL pastes
// (issue #5960) alike.
forceBracketedPaste: true
})
if (result === false) {
return { status: 'skipped', reason: 'image-paste-rejected' }
@@ -136,6 +136,49 @@ describe('terminal paste coordinator', () => {
expect(writePty).not.toHaveBeenCalled()
})
it('marks a single-line URL bracketed-to-terminal when bracketed-paste mode is on (issue #5960)', () => {
// Why: a TUI like Claude Code wraps even a direct paste in bracketed-paste
// escapes when bracketed-paste mode is on, so the corrupting redraw — and
// thus WebGL atlas recovery — must trigger though `bracketed` stays false.
const plan = planTerminalPaste({
text: 'https://github.com/stablyai/orca/pull/6025',
source: 'context-menu',
target: terminalTarget(),
terminalBracketedPasteMode: true
})
expect(plan.mode).toBe('direct')
expect(plan.bracketed).toBe(false)
expect(plan.bracketedToTerminal).toBe(true)
})
it('does not mark a single-line paste bracketed-to-terminal without bracketed-paste mode', () => {
const plan = planTerminalPaste({
text: 'https://github.com/stablyai/orca/pull/6025',
source: 'context-menu',
target: terminalTarget(),
terminalBracketedPasteMode: false
})
expect(plan.mode).toBe('direct')
expect(plan.bracketed).toBe(false)
expect(plan.bracketedToTerminal).toBe(false)
})
it('marks a force-bracketed image paste bracketed-to-terminal even with mode off', () => {
const plan = planTerminalPaste({
text: '/tmp/orca-paste-1760000000000-id.png',
source: 'context-menu',
target: terminalTarget(),
forceBracketedPaste: true,
terminalBracketedPasteMode: false
})
expect(plan.mode).toBe('bracketed-terminal')
expect(plan.bracketed).toBe(true)
expect(plan.bracketedToTerminal).toBe(true)
})
it('forces small Windows multiline paste through bracketed terminal input', async () => {
const pasteText = vi.fn()
const plan = planTerminalPaste({
@@ -169,6 +169,7 @@ function buildTerminalPastePlan({
shouldChunk,
maxBytes
})
const bracketed = mode === 'bracketed-terminal' || (mode === 'chunked' && shouldBracketChunk)
const plan: TerminalPastePlan = {
target,
payload,
@@ -176,7 +177,11 @@ function buildTerminalPastePlan({
newlinePolicy: 'preserve',
runtimeKey: target.runtime.runtimeKey,
...(shouldChunk ? { maxChunkBytes } : {}),
bracketed: mode === 'bracketed-terminal' || (mode === 'chunked' && shouldBracketChunk),
bracketed,
// Why: a `direct` paste still reaches the TUI bracketed when the terminal
// has bracketed-paste mode on, and that wrap drives the corrupting redraw
// (issue #5960) the same as an Orca-bracketed paste.
bracketedToTerminal: bracketed || (mode === 'direct' && terminalBracketedPasteMode),
redactedDiagnostic: '',
...(mode === 'reject' ? { rejectReason: 'payload-too-large' } : {})
}
@@ -39,6 +39,11 @@ export type TerminalPastePlan = {
runtimeKey: string
maxChunkBytes?: number
bracketed: boolean
// Whether the bytes reach the TUI wrapped in bracketed-paste escapes — true
// either because Orca brackets them (`bracketed`) or because the terminal has
// bracketed-paste mode on and wraps a direct paste itself. The corrupting TUI
// redraw (issue #5960) keys off this, not `bracketed` alone.
bracketedToTerminal: boolean
redactedDiagnostic: string
rejectReason?: TerminalPasteExecutionReason
}
@@ -46,7 +51,6 @@ export type TerminalPastePlan = {
export type TerminalPasteTextOptions = {
forceBracketedPaste?: boolean
forceBracketedPasteForMultiline?: boolean
recoverImagePasteWebglAtlas?: boolean
}
export type TerminalPasteExecutionReason =
@@ -3,9 +3,12 @@ import {
registerLivePaneManager,
unregisterLivePaneManager
} from '@/lib/pane-manager/pane-manager-registry'
import { scheduleImagePasteWebglAtlasRecovery } from './terminal-webgl-paste-recovery'
import {
maybeScheduleWebglAtlasRecoveryForPaste,
schedulePasteWebglAtlasRecovery
} from './terminal-webgl-paste-recovery'
describe('terminal image paste WebGL recovery', () => {
describe('terminal paste WebGL recovery', () => {
const registeredManagers: { resetWebglTextureAtlases(): void }[] = []
function registerManager(): { resetWebglTextureAtlases: Mock<() => void> } {
@@ -38,7 +41,7 @@ describe('terminal image paste WebGL recovery', () => {
const manager = registerManager()
const otherManager = registerManager()
scheduleImagePasteWebglAtlasRecovery()
schedulePasteWebglAtlasRecovery()
expect(manager.resetWebglTextureAtlases).not.toHaveBeenCalled()
rafCallbacks[0]?.(0)
@@ -56,7 +59,7 @@ describe('terminal image paste WebGL recovery', () => {
vi.stubGlobal('requestAnimationFrame', undefined)
const manager = registerManager()
scheduleImagePasteWebglAtlasRecovery()
schedulePasteWebglAtlasRecovery()
expect(manager.resetWebglTextureAtlases).not.toHaveBeenCalled()
vi.advanceTimersByTime(0)
@@ -80,7 +83,31 @@ describe('terminal image paste WebGL recovery', () => {
registerLivePaneManager(manager)
registeredManagers.push(manager)
expect(() => scheduleImagePasteWebglAtlasRecovery()).not.toThrow()
expect(() => schedulePasteWebglAtlasRecovery()).not.toThrow()
expect(() => vi.runAllTimers()).not.toThrow()
})
describe('maybeScheduleWebglAtlasRecoveryForPaste', () => {
it('recovers the atlas when the paste reaches the TUI bracketed', () => {
vi.useFakeTimers()
vi.stubGlobal('requestAnimationFrame', undefined)
const manager = registerManager()
maybeScheduleWebglAtlasRecoveryForPaste({ bracketedToTerminal: true })
vi.advanceTimersByTime(500)
expect(manager.resetWebglTextureAtlases).toHaveBeenCalled()
})
it('skips recovery when the paste is not delivered bracketed', () => {
vi.useFakeTimers()
vi.stubGlobal('requestAnimationFrame', undefined)
const manager = registerManager()
maybeScheduleWebglAtlasRecoveryForPaste({ bracketedToTerminal: false })
vi.advanceTimersByTime(500)
expect(manager.resetWebglTextureAtlases).not.toHaveBeenCalled()
})
})
})
@@ -1,6 +1,6 @@
import { resetAllTerminalWebglAtlases } from '@/lib/pane-manager/pane-manager-registry'
const IMAGE_PASTE_ATLAS_RECOVERY_DELAYS_MS = [120, 500]
const PASTE_ATLAS_RECOVERY_DELAYS_MS = [120, 500]
function scheduleNextFrame(callback: () => void): void {
if (typeof globalThis.requestAnimationFrame === 'function') {
@@ -21,12 +21,24 @@ function resetAtlases(): void {
}
}
export function scheduleImagePasteWebglAtlasRecovery(): void {
// Why: Claude Code redraws its image chip immediately after bracketed paste,
// and xterm WebGL atlas corruption can appear after that redraw without a
// context-loss event. A few cheap resets cover the post-paste paint window.
export function schedulePasteWebglAtlasRecovery(): void {
// Why: a TUI (e.g. Claude Code) redraws immediately after a bracketed paste —
// an image chip, or a pasted URL/text — and xterm WebGL atlas corruption can
// appear after that redraw without a context-loss event. A few cheap resets
// cover the post-paste paint window.
scheduleNextFrame(() => resetAtlases())
for (const delayMs of IMAGE_PASTE_ATLAS_RECOVERY_DELAYS_MS) {
for (const delayMs of PASTE_ATLAS_RECOVERY_DELAYS_MS) {
globalThis.setTimeout(() => resetAtlases(), delayMs)
}
}
// Recover the atlas only when the paste reaches the TUI bracketed — that wrap
// triggers the redraw that can corrupt it (image paste, or a text/URL paste
// into a bracketed-paste-mode TUI, issue #5960). Plain direct pastes don't.
export function maybeScheduleWebglAtlasRecoveryForPaste(plan: {
bracketedToTerminal: boolean
}): void {
if (plan.bracketedToTerminal) {
schedulePasteWebglAtlasRecovery()
}
}
@@ -23,7 +23,7 @@ import { resolveTerminalPasteRuntime } from './terminal-paste-runtime'
import { getTerminalPasteSshRemotePlatform } from './terminal-paste-ssh-platform'
import { isTerminalPanePasteTargetCurrent } from './terminal-paste-target-state'
import { writeTerminalPastePtyInput } from './terminal-pty-paste-writer'
import { scheduleImagePasteWebglAtlasRecovery } from './terminal-webgl-paste-recovery'
import { maybeScheduleWebglAtlasRecoveryForPaste } from './terminal-webgl-paste-recovery'
import {
REQUEST_ACTIVE_TERMINAL_PANE_SPLIT_EVENT,
type RequestActiveTerminalPaneSplitDetail
@@ -243,9 +243,7 @@ export function useTerminalPaneContextMenu({
if (text) {
recordTerminalUserInputForLeaf(tabId, pane.leafId)
}
if (options?.recoverImagePasteWebglAtlas) {
scheduleImagePasteWebglAtlasRecovery()
}
maybeScheduleWebglAtlasRecoveryForPaste(plan)
return true
}