mirror of
https://github.com/stablyai/orca.git
synced 2026-10-17 00:03:08 +00:00
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:
co-authored by
Anddy Sahir Agudelo
parent
b45cb545c5
commit
8b72336d7d
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user