fix terminal complex script rendering (#1675)
This commit is contained in:
parent
01bab271eb
commit
ad5d5dc841
20
package.json
20
package.json
|
|
@ -64,15 +64,15 @@
|
|||
"@tiptap/pm": "^3.22.5",
|
||||
"@tiptap/react": "^3.22.5",
|
||||
"@tiptap/starter-kit": "^3.22.5",
|
||||
"@xterm/addon-fit": "0.12.0-beta.198",
|
||||
"@xterm/addon-ligatures": "0.11.0-beta.198",
|
||||
"@xterm/addon-search": "0.17.0-beta.198",
|
||||
"@xterm/addon-serialize": "0.15.0-beta.198",
|
||||
"@xterm/addon-unicode11": "0.10.0-beta.198",
|
||||
"@xterm/addon-web-links": "0.13.0-beta.198",
|
||||
"@xterm/addon-webgl": "0.20.0-beta.197",
|
||||
"@xterm/headless": "6.1.0-beta.198",
|
||||
"@xterm/xterm": "6.1.0-beta.198",
|
||||
"@xterm/addon-fit": "0.12.0-beta.216",
|
||||
"@xterm/addon-ligatures": "0.11.0-beta.216",
|
||||
"@xterm/addon-search": "0.17.0-beta.216",
|
||||
"@xterm/addon-serialize": "0.15.0-beta.216",
|
||||
"@xterm/addon-unicode11": "0.10.0-beta.216",
|
||||
"@xterm/addon-web-links": "0.13.0-beta.216",
|
||||
"@xterm/addon-webgl": "0.20.0-beta.215",
|
||||
"@xterm/headless": "6.1.0-beta.216",
|
||||
"@xterm/xterm": "6.1.0-beta.216",
|
||||
"agent-browser": "~0.24.1",
|
||||
"better-sqlite3": "^12.9.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
|
|
@ -168,7 +168,7 @@
|
|||
],
|
||||
"patchedDependencies": {
|
||||
"node-pty@1.1.0": "config/patches/node-pty@1.1.0.patch",
|
||||
"@xterm/addon-ligatures@0.11.0-beta.198": "config/patches/@xterm__addon-ligatures@0.11.0-beta.198.patch"
|
||||
"@xterm/addon-ligatures@0.11.0-beta.216": "config/patches/@xterm__addon-ligatures@0.11.0-beta.216.patch"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
122
pnpm-lock.yaml
122
pnpm-lock.yaml
|
|
@ -5,9 +5,9 @@ settings:
|
|||
excludeLinksFromLockfile: false
|
||||
|
||||
patchedDependencies:
|
||||
'@xterm/addon-ligatures@0.11.0-beta.198':
|
||||
'@xterm/addon-ligatures@0.11.0-beta.216':
|
||||
hash: df21db050a4d85552cafbe583ece863d7fa19ed634a1219210a0455b986b6634
|
||||
path: config/patches/@xterm__addon-ligatures@0.11.0-beta.198.patch
|
||||
path: config/patches/@xterm__addon-ligatures@0.11.0-beta.216.patch
|
||||
node-pty@1.1.0:
|
||||
hash: 93c259888173175ad66ab303977deb17f2aee9e276cb35522dc61815c65358d4
|
||||
path: config/patches/node-pty@1.1.0.patch
|
||||
|
|
@ -86,32 +86,32 @@ importers:
|
|||
specifier: ^3.22.5
|
||||
version: 3.22.5
|
||||
'@xterm/addon-fit':
|
||||
specifier: 0.12.0-beta.198
|
||||
version: 0.12.0-beta.198(@xterm/xterm@6.1.0-beta.198)
|
||||
specifier: 0.12.0-beta.216
|
||||
version: 0.12.0-beta.216(@xterm/xterm@6.1.0-beta.216)
|
||||
'@xterm/addon-ligatures':
|
||||
specifier: 0.11.0-beta.198
|
||||
version: 0.11.0-beta.198(patch_hash=df21db050a4d85552cafbe583ece863d7fa19ed634a1219210a0455b986b6634)(@xterm/xterm@6.1.0-beta.198)
|
||||
specifier: 0.11.0-beta.216
|
||||
version: 0.11.0-beta.216(patch_hash=df21db050a4d85552cafbe583ece863d7fa19ed634a1219210a0455b986b6634)(@xterm/xterm@6.1.0-beta.216)
|
||||
'@xterm/addon-search':
|
||||
specifier: 0.17.0-beta.198
|
||||
version: 0.17.0-beta.198(@xterm/xterm@6.1.0-beta.198)
|
||||
specifier: 0.17.0-beta.216
|
||||
version: 0.17.0-beta.216(@xterm/xterm@6.1.0-beta.216)
|
||||
'@xterm/addon-serialize':
|
||||
specifier: 0.15.0-beta.198
|
||||
version: 0.15.0-beta.198(@xterm/xterm@6.1.0-beta.198)
|
||||
specifier: 0.15.0-beta.216
|
||||
version: 0.15.0-beta.216(@xterm/xterm@6.1.0-beta.216)
|
||||
'@xterm/addon-unicode11':
|
||||
specifier: 0.10.0-beta.198
|
||||
version: 0.10.0-beta.198(@xterm/xterm@6.1.0-beta.198)
|
||||
specifier: 0.10.0-beta.216
|
||||
version: 0.10.0-beta.216(@xterm/xterm@6.1.0-beta.216)
|
||||
'@xterm/addon-web-links':
|
||||
specifier: 0.13.0-beta.198
|
||||
version: 0.13.0-beta.198(@xterm/xterm@6.1.0-beta.198)
|
||||
specifier: 0.13.0-beta.216
|
||||
version: 0.13.0-beta.216(@xterm/xterm@6.1.0-beta.216)
|
||||
'@xterm/addon-webgl':
|
||||
specifier: 0.20.0-beta.197
|
||||
version: 0.20.0-beta.197(@xterm/xterm@6.1.0-beta.198)
|
||||
specifier: 0.20.0-beta.215
|
||||
version: 0.20.0-beta.215(@xterm/xterm@6.1.0-beta.216)
|
||||
'@xterm/headless':
|
||||
specifier: 6.1.0-beta.198
|
||||
version: 6.1.0-beta.198
|
||||
specifier: 6.1.0-beta.216
|
||||
version: 6.1.0-beta.216
|
||||
'@xterm/xterm':
|
||||
specifier: 6.1.0-beta.198
|
||||
version: 6.1.0-beta.198
|
||||
specifier: 6.1.0-beta.216
|
||||
version: 6.1.0-beta.216
|
||||
agent-browser:
|
||||
specifier: ~0.24.1
|
||||
version: 0.24.1
|
||||
|
|
@ -2997,47 +2997,47 @@ packages:
|
|||
resolution: {integrity: sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw==}
|
||||
engines: {node: '>=14.6'}
|
||||
|
||||
'@xterm/addon-fit@0.12.0-beta.198':
|
||||
resolution: {integrity: sha512-1EZFzOaUjRnlo5p2diIV9+HHVtHqr0O7TEFCstSuFqV/5QaKkRiWiKaMfsPEg2pF5P7yBOASk73yUPII7oquaA==}
|
||||
'@xterm/addon-fit@0.12.0-beta.216':
|
||||
resolution: {integrity: sha512-IgKE3ngNodSnmj1O+EEYpKQZkSbAUbghPlCWd8G32RL0piIMqb3FX3BuYLnWZeLNoD9iMtublLMG1T9XjGeVvA==}
|
||||
peerDependencies:
|
||||
'@xterm/xterm': ^6.1.0-beta.198
|
||||
'@xterm/xterm': ^6.1.0-beta.216
|
||||
|
||||
'@xterm/addon-ligatures@0.11.0-beta.198':
|
||||
resolution: {integrity: sha512-LpF9KYiixxybSdyWpBtaPg5S1NNosQU0vcYg9P71xZFVRPT5vjaFvxYd80hV0oeKdnT25EMgNxwxbqHasmHQvg==}
|
||||
'@xterm/addon-ligatures@0.11.0-beta.216':
|
||||
resolution: {integrity: sha512-NJrK8MRi64Kb1Kxz5Jmrnd7H2gTlBqM3wHfObneD7T4xGFoPZkeX2/Kit4IcHt2Y5CTAQFAc92DilsncVm1HnQ==}
|
||||
engines: {node: '>8.0.0'}
|
||||
peerDependencies:
|
||||
'@xterm/xterm': ^6.1.0-beta.198
|
||||
'@xterm/xterm': ^6.1.0-beta.216
|
||||
|
||||
'@xterm/addon-search@0.17.0-beta.198':
|
||||
resolution: {integrity: sha512-loNSX0cB4P+9Sg13sgB/cQPPBgdOi1hxRyZ75Jj90+4iKe227mRmgzdqU3wxr10nyFWJWCpysCjVT+47tdC8mQ==}
|
||||
'@xterm/addon-search@0.17.0-beta.216':
|
||||
resolution: {integrity: sha512-p//4yv5aB2eyQcCFmPQ/s7tTFSTwKbtxs89xP1gvMbT9QuZgjW9FNqaQKb1GLDWtMpSfZdy7RrUp4GiCg3PVJQ==}
|
||||
peerDependencies:
|
||||
'@xterm/xterm': ^6.1.0-beta.198
|
||||
'@xterm/xterm': ^6.1.0-beta.216
|
||||
|
||||
'@xterm/addon-serialize@0.15.0-beta.198':
|
||||
resolution: {integrity: sha512-0yf13cLPaKX8nkgUqclcdbU5FexacA/xnvWzHzGFYTku0WH+SqygbQcDOcg4bTUGgCcvfZqntEb1NQpr+69KgQ==}
|
||||
'@xterm/addon-serialize@0.15.0-beta.216':
|
||||
resolution: {integrity: sha512-scj/83XfYHg5LiDscYDP9dnadUMZt8To1tbdncQP/dZq2pD4TsTMokW1pciVYYazLBxCeKZybbVLEuOo7IPCqQ==}
|
||||
peerDependencies:
|
||||
'@xterm/xterm': ^6.1.0-beta.198
|
||||
'@xterm/xterm': ^6.1.0-beta.216
|
||||
|
||||
'@xterm/addon-unicode11@0.10.0-beta.198':
|
||||
resolution: {integrity: sha512-3itjZ5lTQ5tnDcpUAplX88BsTJTZOvEzX9wNTNMO2aPuctLavMNg2+pXU8KTkeRhUZnDyhx9Lsv81dDs94vWDQ==}
|
||||
'@xterm/addon-unicode11@0.10.0-beta.216':
|
||||
resolution: {integrity: sha512-i7TrEHOTzUEOClH1+6IHoHy7bR/XHVRBjHc5e0u6A1HucFkAlCU+bqUY8EfwNOh1/iUjuB06EtNh6BM1o/ZAlA==}
|
||||
peerDependencies:
|
||||
'@xterm/xterm': ^6.1.0-beta.198
|
||||
'@xterm/xterm': ^6.1.0-beta.216
|
||||
|
||||
'@xterm/addon-web-links@0.13.0-beta.198':
|
||||
resolution: {integrity: sha512-qA/B1tuNLpqlzscrAVFjLKjuH0CmKKyfbTn78Y3RLKs6H7xxdYSE6/DjJ1zg9C6aDtLwF9lsaOFpPSPf2AfpYQ==}
|
||||
'@xterm/addon-web-links@0.13.0-beta.216':
|
||||
resolution: {integrity: sha512-ZP3BDy1na/37TZHO8FB+XHJFoO8muyPoOzkaL2X28n5A9ZFQPbf834EMRsvDczKnfQvtZcve+ZCmMJj1ZHGjow==}
|
||||
peerDependencies:
|
||||
'@xterm/xterm': ^6.1.0-beta.198
|
||||
'@xterm/xterm': ^6.1.0-beta.216
|
||||
|
||||
'@xterm/addon-webgl@0.20.0-beta.197':
|
||||
resolution: {integrity: sha512-0o30FvLzfd9ODuB1gbpwYMGQHYiz0FebmVaMP0Idesw1sLIG3qc7aIIBQEUnWiBDsKbDHhaysW8WTxacg2AhaQ==}
|
||||
'@xterm/addon-webgl@0.20.0-beta.215':
|
||||
resolution: {integrity: sha512-oCbH3YxiGOzRcKxwTfSRCA1TqpoT/AitO2X5MuqD14DVnb4Z3rTKQYfHBA7R6HA7U4K9OzmtIsi5+VyEIEaWsg==}
|
||||
peerDependencies:
|
||||
'@xterm/xterm': ^6.1.0-beta.198
|
||||
'@xterm/xterm': ^6.1.0-beta.216
|
||||
|
||||
'@xterm/headless@6.1.0-beta.198':
|
||||
resolution: {integrity: sha512-pDS/I5HLGwwJf8sWZFaT+qbHxgFu0qdVQv5/XDzhNyKXMXQJ7cL3ZsJsIox5JxLo2ZuYLVVY8/lirHronhOSfA==}
|
||||
'@xterm/headless@6.1.0-beta.216':
|
||||
resolution: {integrity: sha512-HCut3ABwBv5zVUAB+5KVTW4svr/fCeVMW9xtaJIhmXZsNHOrhVFOGX7E2CYzRa8xdX/aI2JdTIhgLl8xjcHEoA==}
|
||||
|
||||
'@xterm/xterm@6.1.0-beta.198':
|
||||
resolution: {integrity: sha512-udjhNFi4YfypbVBJV7gFNwQV3KziZFJdq+fg87WefGF5JkGbC+4LvIhEsHXR0Kb5Ez8ESxeqM7Yjvj+kw8zDAg==}
|
||||
'@xterm/xterm@6.1.0-beta.216':
|
||||
resolution: {integrity: sha512-87rfymzVje5eYUlGG94hz1WkOYvFRcFDGdiOAbg4d8xt8OGSGR2nMNU4I1n5MDE1RBPBqRd+WVJ5w7q3pwMoZA==}
|
||||
|
||||
abbrev@4.0.0:
|
||||
resolution: {integrity: sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==}
|
||||
|
|
@ -8910,39 +8910,39 @@ snapshots:
|
|||
'@xmldom/xmldom@0.9.10':
|
||||
optional: true
|
||||
|
||||
'@xterm/addon-fit@0.12.0-beta.198(@xterm/xterm@6.1.0-beta.198)':
|
||||
'@xterm/addon-fit@0.12.0-beta.216(@xterm/xterm@6.1.0-beta.216)':
|
||||
dependencies:
|
||||
'@xterm/xterm': 6.1.0-beta.198
|
||||
'@xterm/xterm': 6.1.0-beta.216
|
||||
|
||||
'@xterm/addon-ligatures@0.11.0-beta.198(patch_hash=df21db050a4d85552cafbe583ece863d7fa19ed634a1219210a0455b986b6634)(@xterm/xterm@6.1.0-beta.198)':
|
||||
'@xterm/addon-ligatures@0.11.0-beta.216(patch_hash=df21db050a4d85552cafbe583ece863d7fa19ed634a1219210a0455b986b6634)(@xterm/xterm@6.1.0-beta.216)':
|
||||
dependencies:
|
||||
'@xterm/xterm': 6.1.0-beta.198
|
||||
'@xterm/xterm': 6.1.0-beta.216
|
||||
lru-cache: 6.0.0
|
||||
opentype.js: 0.8.0
|
||||
|
||||
'@xterm/addon-search@0.17.0-beta.198(@xterm/xterm@6.1.0-beta.198)':
|
||||
'@xterm/addon-search@0.17.0-beta.216(@xterm/xterm@6.1.0-beta.216)':
|
||||
dependencies:
|
||||
'@xterm/xterm': 6.1.0-beta.198
|
||||
'@xterm/xterm': 6.1.0-beta.216
|
||||
|
||||
'@xterm/addon-serialize@0.15.0-beta.198(@xterm/xterm@6.1.0-beta.198)':
|
||||
'@xterm/addon-serialize@0.15.0-beta.216(@xterm/xterm@6.1.0-beta.216)':
|
||||
dependencies:
|
||||
'@xterm/xterm': 6.1.0-beta.198
|
||||
'@xterm/xterm': 6.1.0-beta.216
|
||||
|
||||
'@xterm/addon-unicode11@0.10.0-beta.198(@xterm/xterm@6.1.0-beta.198)':
|
||||
'@xterm/addon-unicode11@0.10.0-beta.216(@xterm/xterm@6.1.0-beta.216)':
|
||||
dependencies:
|
||||
'@xterm/xterm': 6.1.0-beta.198
|
||||
'@xterm/xterm': 6.1.0-beta.216
|
||||
|
||||
'@xterm/addon-web-links@0.13.0-beta.198(@xterm/xterm@6.1.0-beta.198)':
|
||||
'@xterm/addon-web-links@0.13.0-beta.216(@xterm/xterm@6.1.0-beta.216)':
|
||||
dependencies:
|
||||
'@xterm/xterm': 6.1.0-beta.198
|
||||
'@xterm/xterm': 6.1.0-beta.216
|
||||
|
||||
'@xterm/addon-webgl@0.20.0-beta.197(@xterm/xterm@6.1.0-beta.198)':
|
||||
'@xterm/addon-webgl@0.20.0-beta.215(@xterm/xterm@6.1.0-beta.216)':
|
||||
dependencies:
|
||||
'@xterm/xterm': 6.1.0-beta.198
|
||||
'@xterm/xterm': 6.1.0-beta.216
|
||||
|
||||
'@xterm/headless@6.1.0-beta.198': {}
|
||||
'@xterm/headless@6.1.0-beta.216': {}
|
||||
|
||||
'@xterm/xterm@6.1.0-beta.198': {}
|
||||
'@xterm/xterm@6.1.0-beta.216': {}
|
||||
|
||||
abbrev@4.0.0: {}
|
||||
|
||||
|
|
|
|||
|
|
@ -162,6 +162,7 @@ function createPane(paneId: number) {
|
|||
function createManager(paneCount = 1) {
|
||||
return {
|
||||
setPaneGpuRendering: vi.fn(),
|
||||
markPaneHasComplexScriptOutput: vi.fn(),
|
||||
getPanes: vi.fn(() => Array.from({ length: paneCount }, (_, index) => ({ id: index + 1 }))),
|
||||
closePane: vi.fn(),
|
||||
getActivePane: vi.fn<() => { id: number } | null>(() => null)
|
||||
|
|
@ -767,6 +768,29 @@ describe('connectPanePty', () => {
|
|||
expect(pane.terminal.write).toHaveBeenCalledWith('hello\r\n')
|
||||
})
|
||||
|
||||
it('marks panes that receive Arabic output for DOM rendering', async () => {
|
||||
const { connectPanePty } = await import('./pty-connection')
|
||||
const transport = createMockTransport()
|
||||
const capturedDataCallback: { current: ((data: string) => void) | null } = { current: null }
|
||||
transport.connect.mockImplementation(async ({ callbacks }: { callbacks: ConnectCallbacks }) => {
|
||||
capturedDataCallback.current = callbacks.onData ?? null
|
||||
return 'pty-id'
|
||||
})
|
||||
transportFactoryQueue.push(transport)
|
||||
|
||||
const pane = createPane(1)
|
||||
const manager = createManager(1)
|
||||
const deps = createDeps()
|
||||
|
||||
connectPanePty(pane as never, manager as never, deps as never)
|
||||
await flushAsyncTicks(6)
|
||||
|
||||
capturedDataCallback.current?.('Arabic: السلام عليكم\r\n')
|
||||
|
||||
expect(manager.markPaneHasComplexScriptOutput).toHaveBeenCalledWith(1)
|
||||
expect(pane.terminal.write).toHaveBeenCalledWith('Arabic: السلام عليكم\r\n')
|
||||
})
|
||||
|
||||
it('reattaches via daemon sessionId when an in-session PTY is live', async () => {
|
||||
const { connectPanePty } = await import('./pty-connection')
|
||||
const transport = createMockTransport()
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import { safeFit } from '@/lib/pane-manager/pane-tree-ops'
|
|||
import { getFitOverrideForPty, bindPanePtyId } from '@/lib/pane-manager/mobile-fit-overrides'
|
||||
import { isPtyLocked } from '@/lib/pane-manager/mobile-driver-state'
|
||||
import { isPaneReplaying, replayIntoTerminal } from './replay-guard'
|
||||
import { terminalOutputRequiresDomRenderer } from '@/lib/pane-manager/terminal-complex-script'
|
||||
import {
|
||||
paneLeafId,
|
||||
POST_REPLAY_MODE_RESET,
|
||||
|
|
@ -580,12 +581,19 @@ export function connectPanePty(
|
|||
// sequences don't leak into the shell. xterm.write() buffers internally
|
||||
// regardless of DOM visibility and the guard stays engaged via the
|
||||
// write-completion callback until xterm finishes parsing.
|
||||
const writeReplayData = (data: string): void => {
|
||||
if (terminalOutputRequiresDomRenderer(data)) {
|
||||
manager.markPaneHasComplexScriptOutput(pane.id)
|
||||
}
|
||||
replayIntoTerminal(pane, deps.replayingPanesRef, data)
|
||||
}
|
||||
|
||||
const replayDataCallback = (data: string): void => {
|
||||
// Relay replay buffer holds the last 100 KB of output, which may
|
||||
// overlap with content already rendered in xterm before the
|
||||
// disconnect. Clear first to prevent duplication on SSH reconnect.
|
||||
replayIntoTerminal(pane, deps.replayingPanesRef, '\x1b[2J\x1b[3J\x1b[H')
|
||||
replayIntoTerminal(pane, deps.replayingPanesRef, data)
|
||||
writeReplayData('\x1b[2J\x1b[3J\x1b[H')
|
||||
writeReplayData(data)
|
||||
}
|
||||
|
||||
const dataCallback = (data: string): void => {
|
||||
|
|
@ -595,6 +603,9 @@ export function connectPanePty(
|
|||
// keeps GPU resources from leaking. Visibility-gated buffering used
|
||||
// to feed bytes into xterm at stale dimensions on resume, which was
|
||||
// the root of the cursor-on-strange-line and broken-wide-char bugs.
|
||||
if (terminalOutputRequiresDomRenderer(data)) {
|
||||
manager.markPaneHasComplexScriptOutput(pane.id)
|
||||
}
|
||||
pane.terminal.write(data)
|
||||
|
||||
if (pendingStartupCommand) {
|
||||
|
|
@ -663,12 +674,12 @@ export function connectPanePty(
|
|||
// the daemon and relay are by definition tracking the same session
|
||||
// and only the freshest source belongs on screen.
|
||||
if (connectResult?.snapshot) {
|
||||
replayIntoTerminal(pane, deps.replayingPanesRef, '\x1b[2J\x1b[3J\x1b[H')
|
||||
replayIntoTerminal(pane, deps.replayingPanesRef, connectResult.snapshot)
|
||||
writeReplayData('\x1b[2J\x1b[3J\x1b[H')
|
||||
writeReplayData(connectResult.snapshot)
|
||||
// Snapshot reattach keeps a live session, so avoid the broader mode
|
||||
// reset. Focus reporting is the unsafe exception: preserving `?1004h`
|
||||
// can make restored shells ring BEL on pane focus/blur.
|
||||
replayIntoTerminal(pane, deps.replayingPanesRef, POST_REPLAY_FOCUS_REPORTING_RESET)
|
||||
writeReplayData(POST_REPLAY_FOCUS_REPORTING_RESET)
|
||||
if (connectResult.coldRestore) {
|
||||
// Snapshot superseded the cold-restore payload — ack it so the
|
||||
// daemon does not redeliver it on the next reattach.
|
||||
|
|
@ -679,9 +690,9 @@ export function connectPanePty(
|
|||
// already hold pre-disconnect content; clear first to avoid
|
||||
// duplication. Focus-reporting reset prevents BEL from stale mode
|
||||
// bits in the replayed data.
|
||||
replayIntoTerminal(pane, deps.replayingPanesRef, '\x1b[2J\x1b[3J\x1b[H')
|
||||
replayIntoTerminal(pane, deps.replayingPanesRef, connectResult.replay)
|
||||
replayIntoTerminal(pane, deps.replayingPanesRef, POST_REPLAY_FOCUS_REPORTING_RESET)
|
||||
writeReplayData('\x1b[2J\x1b[3J\x1b[H')
|
||||
writeReplayData(connectResult.replay)
|
||||
writeReplayData(POST_REPLAY_FOCUS_REPORTING_RESET)
|
||||
if (connectResult.coldRestore) {
|
||||
window.api.pty.ackColdRestore(ptyId)
|
||||
}
|
||||
|
|
@ -693,18 +704,14 @@ export function connectPanePty(
|
|||
// may contain query sequences the previous agent CLI emitted;
|
||||
// writing them through xterm.write would trigger auto-replies that
|
||||
// land in the new shell's stdin. See replay-guard.ts.
|
||||
replayIntoTerminal(pane, deps.replayingPanesRef, '\x1b[2J\x1b[3J\x1b[H')
|
||||
replayIntoTerminal(pane, deps.replayingPanesRef, connectResult.coldRestore.scrollback)
|
||||
replayIntoTerminal(
|
||||
pane,
|
||||
deps.replayingPanesRef,
|
||||
'\r\n\x1b[2m--- session restored ---\x1b[0m\r\n\r\n'
|
||||
)
|
||||
writeReplayData('\x1b[2J\x1b[3J\x1b[H')
|
||||
writeReplayData(connectResult.coldRestore.scrollback)
|
||||
writeReplayData('\r\n\x1b[2m--- session restored ---\x1b[0m\r\n\r\n')
|
||||
// Cold-restore means the daemon lost the session and spawned a
|
||||
// fresh shell — no TUI is consuming the mode-setting bytes that a
|
||||
// crashed TUI (e.g. Claude's \e[?1004h) left in the scrollback, so
|
||||
// reset them to match the fresh shell's expectations.
|
||||
replayIntoTerminal(pane, deps.replayingPanesRef, POST_REPLAY_MODE_RESET)
|
||||
writeReplayData(POST_REPLAY_MODE_RESET)
|
||||
window.api.pty.ackColdRestore(ptyId)
|
||||
}
|
||||
if (connectResult?.sessionExpired) {
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ function createPane(): ManagedPaneInternal {
|
|||
gpuRenderingEnabled: true,
|
||||
webglAttachmentDeferred: false,
|
||||
webglDisabledAfterContextLoss: false,
|
||||
hasComplexScriptOutput: false,
|
||||
fitAddon: {
|
||||
fit: vi.fn(),
|
||||
proposeDimensions: vi.fn(() => ({ cols: 80, rows: 24 }))
|
||||
|
|
|
|||
|
|
@ -1,7 +1,12 @@
|
|||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { WebglAddon } from '@xterm/addon-webgl'
|
||||
import type { ManagedPaneInternal } from './pane-manager-types'
|
||||
import { attachWebgl, openTerminal, resetTerminalWebglSuggestion } from './pane-lifecycle'
|
||||
import {
|
||||
attachWebgl,
|
||||
markComplexScriptOutput,
|
||||
resetTerminalWebglSuggestion
|
||||
} from './pane-webgl-renderer'
|
||||
import { openTerminal } from './pane-lifecycle'
|
||||
import { buildDefaultTerminalOptions } from './pane-terminal-options'
|
||||
|
||||
const webglMock = vi.hoisted(() => ({
|
||||
|
|
@ -35,6 +40,7 @@ function createPane(): ManagedPaneInternal {
|
|||
gpuRenderingEnabled: true,
|
||||
webglAttachmentDeferred: false,
|
||||
webglDisabledAfterContextLoss: false,
|
||||
hasComplexScriptOutput: false,
|
||||
fitAddon: {
|
||||
fit: vi.fn()
|
||||
} as never,
|
||||
|
|
@ -159,6 +165,33 @@ describe('attachWebgl', () => {
|
|||
|
||||
expect(forcedPane.terminal.loadAddon).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('keeps auto-mode panes on DOM after complex-script output', () => {
|
||||
const pane = createPane()
|
||||
|
||||
attachWebgl(pane)
|
||||
expect(pane.terminal.loadAddon).toHaveBeenCalledTimes(1)
|
||||
|
||||
markComplexScriptOutput(pane)
|
||||
|
||||
expect(pane.hasComplexScriptOutput).toBe(true)
|
||||
expect(pane.webglAddon).toBeNull()
|
||||
expect(webglMock.dispose).toHaveBeenCalledTimes(1)
|
||||
|
||||
attachWebgl(pane)
|
||||
|
||||
expect(pane.terminal.loadAddon).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('allows explicit on mode to override complex-script DOM fallback', () => {
|
||||
const pane = createPane()
|
||||
|
||||
markComplexScriptOutput(pane)
|
||||
pane.terminalGpuAcceleration = 'on'
|
||||
attachWebgl(pane)
|
||||
|
||||
expect(pane.terminal.loadAddon).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('openTerminal — Unicode 11 ordering', () => {
|
||||
|
|
@ -239,6 +272,7 @@ describe('openTerminal — Unicode 11 ordering', () => {
|
|||
gpuRenderingEnabled: false,
|
||||
webglAttachmentDeferred: false,
|
||||
webglDisabledAfterContextLoss: false,
|
||||
hasComplexScriptOutput: false,
|
||||
fitAddon,
|
||||
fitResizeObserver: null,
|
||||
pendingObservedFitRafId: null,
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import { LigaturesAddon } from '@xterm/addon-ligatures'
|
|||
import { SearchAddon } from '@xterm/addon-search'
|
||||
import { Unicode11Addon } from '@xterm/addon-unicode11'
|
||||
import { WebLinksAddon } from '@xterm/addon-web-links'
|
||||
import { WebglAddon } from '@xterm/addon-webgl'
|
||||
import { SerializeAddon } from '@xterm/addon-serialize'
|
||||
|
||||
import type { PaneManagerOptions, ManagedPaneInternal } from './pane-manager-types'
|
||||
|
|
@ -23,25 +22,12 @@ import {
|
|||
detachPaneFitResizeObserver
|
||||
} from './pane-fit-resize-observer'
|
||||
import { buildDefaultTerminalOptions } from './pane-terminal-options'
|
||||
import type { GlobalSettings } from '../../../../shared/types'
|
||||
import { ENABLE_WEBGL_RENDERER, attachWebgl, disposeWebgl } from './pane-webgl-renderer'
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Pane creation, terminal open/close, addon management
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const ENABLE_WEBGL_RENDERER = true
|
||||
let suggestedRendererType: 'dom' | undefined
|
||||
|
||||
export function resetTerminalWebglSuggestion(): void {
|
||||
// Why: VS Code clears its suggested renderer when gpuAcceleration changes,
|
||||
// letting "auto" retry WebGL after a user toggles the setting.
|
||||
suggestedRendererType = undefined
|
||||
}
|
||||
|
||||
function shouldUseWebgl(mode: GlobalSettings['terminalGpuAcceleration']): boolean {
|
||||
return mode === 'on' || (mode === 'auto' && suggestedRendererType === undefined)
|
||||
}
|
||||
|
||||
function getTerminalUrlOpenHint(): string {
|
||||
return navigator.userAgent.includes('Mac')
|
||||
? '⌘+click to open or ⇧⌘+click for system browser'
|
||||
|
|
@ -124,6 +110,7 @@ export function createPaneDOM(
|
|||
gpuRenderingEnabled: ENABLE_WEBGL_RENDERER,
|
||||
webglAttachmentDeferred: false,
|
||||
webglDisabledAfterContextLoss: false,
|
||||
hasComplexScriptOutput: false,
|
||||
fitAddon,
|
||||
fitResizeObserver: null,
|
||||
pendingObservedFitRafId: null,
|
||||
|
|
@ -290,74 +277,6 @@ export function setLigaturesEnabled(pane: ManagedPaneInternal, enabled: boolean)
|
|||
}
|
||||
}
|
||||
|
||||
export function disposeWebgl(
|
||||
pane: ManagedPaneInternal,
|
||||
options?: { refreshDimensions?: boolean }
|
||||
): void {
|
||||
if (!pane.webglAddon) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
pane.webglAddon.dispose()
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
pane.webglAddon = null
|
||||
if (options?.refreshDimensions) {
|
||||
// Why: VS Code refreshes terminal dimensions after WebGL teardown because
|
||||
// DOM and WebGL renderer cell metrics differ. Without this, Linux DOM
|
||||
// scrollbars can desync and trigger visible reflow jitter.
|
||||
requestAnimationFrame(() => {
|
||||
try {
|
||||
pane.fitAddon.fit()
|
||||
pane.terminal.refresh(0, pane.terminal.rows - 1)
|
||||
} catch {
|
||||
/* ignore — pane may have been disposed in the meantime */
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export function attachWebgl(pane: ManagedPaneInternal): void {
|
||||
if (
|
||||
!ENABLE_WEBGL_RENDERER ||
|
||||
!pane.gpuRenderingEnabled ||
|
||||
!shouldUseWebgl(pane.terminalGpuAcceleration) ||
|
||||
pane.webglAttachmentDeferred ||
|
||||
pane.webglDisabledAfterContextLoss
|
||||
) {
|
||||
pane.webglAddon = null
|
||||
return
|
||||
}
|
||||
try {
|
||||
const webglAddon = new WebglAddon()
|
||||
webglAddon.onContextLoss(() => {
|
||||
console.warn(
|
||||
'[terminal] WebGL context lost for pane',
|
||||
pane.id,
|
||||
'— falling back to DOM renderer'
|
||||
)
|
||||
// Why: Chromium starts reclaiming terminal contexts under pressure.
|
||||
// Recreating WebGL for this pane can loop context loss and leave xterm
|
||||
// visually blank, so keep the pane on the DOM renderer until remount.
|
||||
pane.webglDisabledAfterContextLoss = true
|
||||
disposeWebgl(pane, { refreshDimensions: true })
|
||||
})
|
||||
pane.terminal.loadAddon(webglAddon)
|
||||
pane.webglAddon = webglAddon
|
||||
} catch (err) {
|
||||
if (pane.terminalGpuAcceleration === 'auto') {
|
||||
// Why: mirrors VS Code's `terminal.integrated.gpuAcceleration=auto`
|
||||
// behavior: once WebGL fails, keep subsequent auto panes on DOM until
|
||||
// the setting changes and resets the suggestion.
|
||||
suggestedRendererType = 'dom'
|
||||
}
|
||||
// WebGL not available — default DOM renderer is fine, but log it for debugging
|
||||
console.warn('[terminal] WebGL unavailable for pane', pane.id, '— using DOM renderer:', err)
|
||||
pane.webglAddon = null
|
||||
}
|
||||
}
|
||||
|
||||
export function disposePane(
|
||||
pane: ManagedPaneInternal,
|
||||
panes: Map<number, ManagedPaneInternal>
|
||||
|
|
|
|||
|
|
@ -80,6 +80,9 @@ export type ManagedPaneInternal = {
|
|||
gpuRenderingEnabled: boolean
|
||||
webglAttachmentDeferred: boolean
|
||||
webglDisabledAfterContextLoss: boolean
|
||||
// Why: complex-script shaping/RTL rendering is visibly wrong in xterm WebGL;
|
||||
// keep auto-mode panes on DOM once their output proves they need browser text shaping.
|
||||
hasComplexScriptOutput: boolean
|
||||
webglAddon: WebglAddon | null
|
||||
// Why nullable: ligatures are opt-in per font and toggleable at runtime,
|
||||
// so the addon instance only exists while the feature is active. A null
|
||||
|
|
|
|||
|
|
@ -17,14 +17,8 @@ import {
|
|||
handlePaneDrop,
|
||||
updateMultiPaneState
|
||||
} from './pane-drag-reorder'
|
||||
import {
|
||||
createPaneDOM,
|
||||
openTerminal,
|
||||
attachWebgl,
|
||||
disposeWebgl,
|
||||
setLigaturesEnabled,
|
||||
disposePane
|
||||
} from './pane-lifecycle'
|
||||
import { createPaneDOM, openTerminal, setLigaturesEnabled, disposePane } from './pane-lifecycle'
|
||||
import { disposeWebgl } from './pane-webgl-renderer'
|
||||
import { shouldFollowMouseFocus } from './focus-follows-mouse'
|
||||
import {
|
||||
findPaneChildren,
|
||||
|
|
@ -40,6 +34,12 @@ import { scheduleSplitScrollRestore } from './pane-split-scroll'
|
|||
import { toPublicPane } from './pane-public-view'
|
||||
import { applyTerminalGpuAcceleration } from './pane-terminal-gpu-acceleration'
|
||||
import { reattachWebglIfNeeded } from './pane-webgl-reattach'
|
||||
import {
|
||||
markPaneComplexScriptOutput,
|
||||
resumePaneRendering,
|
||||
setPaneGpuRenderingState,
|
||||
suspendPaneRendering
|
||||
} from './pane-rendering-control'
|
||||
|
||||
export type { PaneManagerOptions, PaneStyleOptions, ManagedPane, DropZone }
|
||||
|
||||
|
|
@ -224,50 +224,25 @@ export class PaneManager {
|
|||
}
|
||||
|
||||
setPaneGpuRendering(paneId: number, enabled: boolean): void {
|
||||
const pane = this.panes.get(paneId)
|
||||
if (!pane) {
|
||||
return
|
||||
}
|
||||
pane.gpuRenderingEnabled = enabled
|
||||
if (!enabled) {
|
||||
disposeWebgl(pane, { refreshDimensions: true })
|
||||
return
|
||||
}
|
||||
if (pane.webglAttachmentDeferred || pane.webglDisabledAfterContextLoss) {
|
||||
return
|
||||
}
|
||||
if (!pane.webglAddon) {
|
||||
attachWebgl(pane)
|
||||
safeFit(pane)
|
||||
}
|
||||
setPaneGpuRenderingState(this.panes, paneId, enabled)
|
||||
}
|
||||
|
||||
setTerminalGpuAcceleration(mode: PaneManagerOptions['terminalGpuAcceleration']): void {
|
||||
applyTerminalGpuAcceleration(this.panes.values(), this.options, mode)
|
||||
}
|
||||
|
||||
markPaneHasComplexScriptOutput(paneId: number): void {
|
||||
markPaneComplexScriptOutput(this.panes, paneId)
|
||||
}
|
||||
|
||||
suspendRendering(): void {
|
||||
this.renderingSuspended = true
|
||||
for (const pane of this.panes.values()) {
|
||||
pane.webglAttachmentDeferred = true
|
||||
disposeWebgl(pane)
|
||||
}
|
||||
suspendPaneRendering(this.panes.values())
|
||||
}
|
||||
|
||||
resumeRendering(): void {
|
||||
this.renderingSuspended = false
|
||||
for (const pane of this.panes.values()) {
|
||||
pane.webglAttachmentDeferred = false
|
||||
reattachWebglIfNeeded(pane)
|
||||
// Why: fresh WebGL canvas has no content — refresh prevents frozen terminal.
|
||||
if (pane.webglAddon) {
|
||||
try {
|
||||
pane.terminal.refresh(0, pane.terminal.rows - 1)
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
}
|
||||
resumePaneRendering(this.panes.values())
|
||||
}
|
||||
|
||||
movePane(sourcePaneId: number, targetPaneId: number, zone: DropZone): void {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,59 @@
|
|||
import type { ManagedPaneInternal } from './pane-manager-types'
|
||||
import { safeFit } from './pane-tree-ops'
|
||||
import { attachWebgl, disposeWebgl, markComplexScriptOutput } from './pane-webgl-renderer'
|
||||
import { reattachWebglIfNeeded } from './pane-webgl-reattach'
|
||||
|
||||
export function setPaneGpuRenderingState(
|
||||
panes: Map<number, ManagedPaneInternal>,
|
||||
paneId: number,
|
||||
enabled: boolean
|
||||
): void {
|
||||
const pane = panes.get(paneId)
|
||||
if (!pane) {
|
||||
return
|
||||
}
|
||||
pane.gpuRenderingEnabled = enabled
|
||||
if (!enabled) {
|
||||
disposeWebgl(pane, { refreshDimensions: true })
|
||||
return
|
||||
}
|
||||
if (pane.webglAttachmentDeferred || pane.webglDisabledAfterContextLoss) {
|
||||
return
|
||||
}
|
||||
if (!pane.webglAddon) {
|
||||
attachWebgl(pane)
|
||||
safeFit(pane)
|
||||
}
|
||||
}
|
||||
|
||||
export function markPaneComplexScriptOutput(
|
||||
panes: Map<number, ManagedPaneInternal>,
|
||||
paneId: number
|
||||
): void {
|
||||
const pane = panes.get(paneId)
|
||||
if (pane) {
|
||||
markComplexScriptOutput(pane)
|
||||
}
|
||||
}
|
||||
|
||||
export function suspendPaneRendering(panes: Iterable<ManagedPaneInternal>): void {
|
||||
for (const pane of panes) {
|
||||
pane.webglAttachmentDeferred = true
|
||||
disposeWebgl(pane)
|
||||
}
|
||||
}
|
||||
|
||||
export function resumePaneRendering(panes: Iterable<ManagedPaneInternal>): void {
|
||||
for (const pane of panes) {
|
||||
pane.webglAttachmentDeferred = false
|
||||
reattachWebglIfNeeded(pane)
|
||||
// Why: fresh WebGL canvas has no content — refresh prevents frozen terminal.
|
||||
if (pane.webglAddon) {
|
||||
try {
|
||||
pane.terminal.refresh(0, pane.terminal.rows - 1)
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -16,6 +16,7 @@ function createPane(): ManagedPaneInternal {
|
|||
gpuRenderingEnabled: true,
|
||||
webglAttachmentDeferred: false,
|
||||
webglDisabledAfterContextLoss: false,
|
||||
hasComplexScriptOutput: false,
|
||||
webglAddon: {
|
||||
dispose: vi.fn()
|
||||
} as never,
|
||||
|
|
@ -57,4 +58,15 @@ describe('applyTerminalGpuAcceleration', () => {
|
|||
expect(pane.webglAddon).toBeNull()
|
||||
expect(pane.fitAddon.fit).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('returns complex-script panes to DOM when switching from forced WebGL back to auto', () => {
|
||||
const pane = createPane()
|
||||
pane.hasComplexScriptOutput = true
|
||||
const options: PaneManagerOptions = { terminalGpuAcceleration: 'on' }
|
||||
|
||||
applyTerminalGpuAcceleration([pane], options, 'auto')
|
||||
|
||||
expect(pane.webglAddon).toBeNull()
|
||||
expect(pane.fitAddon.fit).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { ManagedPaneInternal, PaneManagerOptions } from './pane-manager-types'
|
||||
import { attachWebgl, disposeWebgl, resetTerminalWebglSuggestion } from './pane-lifecycle'
|
||||
import { attachWebgl, disposeWebgl, resetTerminalWebglSuggestion } from './pane-webgl-renderer'
|
||||
import { safeFit } from './pane-tree-ops'
|
||||
|
||||
export function applyTerminalGpuAcceleration(
|
||||
|
|
@ -15,7 +15,7 @@ export function applyTerminalGpuAcceleration(
|
|||
}
|
||||
for (const pane of panes) {
|
||||
pane.terminalGpuAcceleration = nextMode
|
||||
if (nextMode === 'off') {
|
||||
if (nextMode === 'off' || (nextMode === 'auto' && pane.hasComplexScriptOutput)) {
|
||||
disposeWebgl(pane, { refreshDimensions: true })
|
||||
continue
|
||||
}
|
||||
|
|
@ -23,7 +23,8 @@ export function applyTerminalGpuAcceleration(
|
|||
pane.gpuRenderingEnabled &&
|
||||
!pane.webglAddon &&
|
||||
!pane.webglAttachmentDeferred &&
|
||||
!pane.webglDisabledAfterContextLoss
|
||||
!pane.webglDisabledAfterContextLoss &&
|
||||
(nextMode === 'on' || !pane.hasComplexScriptOutput)
|
||||
) {
|
||||
attachWebgl(pane)
|
||||
safeFit(pane)
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ function createPane({
|
|||
gpuRenderingEnabled: true,
|
||||
webglAttachmentDeferred: false,
|
||||
webglDisabledAfterContextLoss: false,
|
||||
hasComplexScriptOutput: false,
|
||||
fitAddon: {
|
||||
fit,
|
||||
proposeDimensions
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import type {
|
|||
} from './pane-manager-types'
|
||||
import { createDivider } from './pane-divider'
|
||||
import { getFitOverrideForPty } from './mobile-fit-overrides'
|
||||
import { disposeWebgl, attachWebgl } from './pane-lifecycle'
|
||||
import { disposeWebgl, attachWebgl } from './pane-webgl-renderer'
|
||||
|
||||
export { findLineByContent, captureScrollState, restoreScrollState } from './pane-scroll'
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { ManagedPaneInternal } from './pane-manager-types'
|
||||
import { attachWebgl } from './pane-lifecycle'
|
||||
import { attachWebgl } from './pane-webgl-renderer'
|
||||
|
||||
export function reattachWebglIfNeeded(pane: ManagedPaneInternal): void {
|
||||
if (pane.gpuRenderingEnabled && !pane.webglAddon && !pane.webglDisabledAfterContextLoss) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,98 @@
|
|||
import { WebglAddon } from '@xterm/addon-webgl'
|
||||
import type { ManagedPaneInternal } from './pane-manager-types'
|
||||
|
||||
export const ENABLE_WEBGL_RENDERER = true
|
||||
let suggestedRendererType: 'dom' | undefined
|
||||
|
||||
export function resetTerminalWebglSuggestion(): void {
|
||||
// Why: VS Code clears its suggested renderer when gpuAcceleration changes,
|
||||
// letting "auto" retry WebGL after a user toggles the setting.
|
||||
suggestedRendererType = undefined
|
||||
}
|
||||
|
||||
function shouldUseWebgl(pane: ManagedPaneInternal): boolean {
|
||||
if (pane.terminalGpuAcceleration === 'on') {
|
||||
return true
|
||||
}
|
||||
return (
|
||||
pane.terminalGpuAcceleration === 'auto' &&
|
||||
suggestedRendererType === undefined &&
|
||||
!pane.hasComplexScriptOutput
|
||||
)
|
||||
}
|
||||
|
||||
export function disposeWebgl(
|
||||
pane: ManagedPaneInternal,
|
||||
options?: { refreshDimensions?: boolean }
|
||||
): void {
|
||||
if (!pane.webglAddon) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
pane.webglAddon.dispose()
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
pane.webglAddon = null
|
||||
if (options?.refreshDimensions) {
|
||||
// Why: VS Code refreshes terminal dimensions after WebGL teardown because
|
||||
// DOM and WebGL renderer cell metrics differ. Without this, Linux DOM
|
||||
// scrollbars can desync and trigger visible reflow jitter.
|
||||
requestAnimationFrame(() => {
|
||||
try {
|
||||
pane.fitAddon.fit()
|
||||
pane.terminal.refresh(0, pane.terminal.rows - 1)
|
||||
} catch {
|
||||
/* ignore — pane may have been disposed in the meantime */
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export function markComplexScriptOutput(pane: ManagedPaneInternal): void {
|
||||
pane.hasComplexScriptOutput = true
|
||||
if (pane.terminalGpuAcceleration !== 'auto') {
|
||||
return
|
||||
}
|
||||
disposeWebgl(pane, { refreshDimensions: true })
|
||||
}
|
||||
|
||||
export function attachWebgl(pane: ManagedPaneInternal): void {
|
||||
if (
|
||||
!ENABLE_WEBGL_RENDERER ||
|
||||
!pane.gpuRenderingEnabled ||
|
||||
!shouldUseWebgl(pane) ||
|
||||
pane.webglAttachmentDeferred ||
|
||||
pane.webglDisabledAfterContextLoss
|
||||
) {
|
||||
pane.webglAddon = null
|
||||
return
|
||||
}
|
||||
try {
|
||||
const webglAddon = new WebglAddon()
|
||||
webglAddon.onContextLoss(() => {
|
||||
console.warn(
|
||||
'[terminal] WebGL context lost for pane',
|
||||
pane.id,
|
||||
'— falling back to DOM renderer'
|
||||
)
|
||||
// Why: Chromium starts reclaiming terminal contexts under pressure.
|
||||
// Recreating WebGL for this pane can loop context loss and leave xterm
|
||||
// visually blank, so keep the pane on the DOM renderer until remount.
|
||||
pane.webglDisabledAfterContextLoss = true
|
||||
disposeWebgl(pane, { refreshDimensions: true })
|
||||
})
|
||||
pane.terminal.loadAddon(webglAddon)
|
||||
pane.webglAddon = webglAddon
|
||||
} catch (err) {
|
||||
if (pane.terminalGpuAcceleration === 'auto') {
|
||||
// Why: mirrors VS Code's `terminal.integrated.gpuAcceleration=auto`
|
||||
// behavior: once WebGL fails, keep subsequent auto panes on DOM until
|
||||
// the setting changes and resets the suggestion.
|
||||
suggestedRendererType = 'dom'
|
||||
}
|
||||
// WebGL not available — default DOM renderer is fine, but log it for debugging
|
||||
console.warn('[terminal] WebGL unavailable for pane', pane.id, '— using DOM renderer:', err)
|
||||
pane.webglAddon = null
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import { terminalOutputRequiresDomRenderer } from './terminal-complex-script'
|
||||
|
||||
describe('terminalOutputRequiresDomRenderer', () => {
|
||||
it('detects Arabic terminal output', () => {
|
||||
expect(terminalOutputRequiresDomRenderer('Arabic: السلام عليكم')).toBe(true)
|
||||
})
|
||||
|
||||
it('detects RTL scripts that need browser text shaping/order', () => {
|
||||
expect(terminalOutputRequiresDomRenderer('Hebrew: שלום')).toBe(true)
|
||||
})
|
||||
|
||||
it('does not disable WebGL for ordinary terminal output', () => {
|
||||
expect(terminalOutputRequiresDomRenderer('abc 123 ── ✓')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
// Why: xterm's WebGL renderer draws per-cell glyphs from a texture atlas,
|
||||
// bypassing the browser shaping/bidi path that the DOM renderer provides.
|
||||
const COMPLEX_SCRIPT_PATTERN =
|
||||
/[\u0590-\u05FF\u0600-\u06FF\u0700-\u074F\u0750-\u077F\u0780-\u07BF\u07C0-\u07FF\u0840-\u085F\u0860-\u086F\u0870-\u089F\u08A0-\u08FF\uFB1D-\uFDFF\uFE70-\uFEFF]|\u{10EC0}-\u{10EFF}|\u{1E900}-\u{1E95F}/u
|
||||
|
||||
export function terminalOutputRequiresDomRenderer(data: string): boolean {
|
||||
return COMPLEX_SCRIPT_PATTERN.test(data)
|
||||
}
|
||||
Loading…
Reference in New Issue