mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 00:02:31 +00:00
Fix terminal glyph renderer fallback (#1850)
* fix terminal glyph renderer fallback * remove terminal glyph design doc from pr
This commit is contained in:
@@ -12,7 +12,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 { terminalOutputPrefersDomRenderer } from '@/lib/pane-manager/terminal-complex-script'
|
||||
import {
|
||||
paneLeafId,
|
||||
POST_REPLAY_MODE_RESET,
|
||||
@@ -626,7 +626,7 @@ export function connectPanePty(
|
||||
// Why: drain any queued background bytes BEFORE the replay paint, so the
|
||||
// scheduler's deferred drain cannot land older bytes on top of the replay.
|
||||
flushTerminalOutput(pane.terminal)
|
||||
if (terminalOutputRequiresDomRenderer(data)) {
|
||||
if (terminalOutputPrefersDomRenderer(data)) {
|
||||
manager.markPaneHasComplexScriptOutput(pane.id)
|
||||
}
|
||||
replayIntoTerminal(pane, deps.replayingPanesRef, data)
|
||||
@@ -641,7 +641,7 @@ export function connectPanePty(
|
||||
}
|
||||
|
||||
const dataCallback = (data: string): void => {
|
||||
if (terminalOutputRequiresDomRenderer(data)) {
|
||||
if (terminalOutputPrefersDomRenderer(data)) {
|
||||
manager.markPaneHasComplexScriptOutput(pane.id)
|
||||
}
|
||||
// Why: visibility is the right gate — split-pane layouts have multiple
|
||||
|
||||
@@ -1,16 +1,44 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { terminalOutputRequiresDomRenderer } from './terminal-complex-script'
|
||||
import { terminalOutputPrefersDomRenderer } from './terminal-complex-script'
|
||||
|
||||
describe('terminalOutputRequiresDomRenderer', () => {
|
||||
describe('terminalOutputPrefersDomRenderer', () => {
|
||||
it('detects Arabic terminal output', () => {
|
||||
expect(terminalOutputRequiresDomRenderer('Arabic: السلام عليكم')).toBe(true)
|
||||
expect(terminalOutputPrefersDomRenderer('Arabic: السلام عليكم')).toBe(true)
|
||||
})
|
||||
|
||||
it('detects RTL scripts that need browser text shaping/order', () => {
|
||||
expect(terminalOutputRequiresDomRenderer('Hebrew: שלום')).toBe(true)
|
||||
expect(terminalOutputPrefersDomRenderer('Hebrew: שלום')).toBe(true)
|
||||
})
|
||||
|
||||
it('does not disable WebGL for ordinary terminal output', () => {
|
||||
expect(terminalOutputRequiresDomRenderer('abc 123 ── ✓')).toBe(false)
|
||||
it('detects glyph classes common in agent terminal UIs', () => {
|
||||
expect(terminalOutputPrefersDomRenderer('⠋ Working')).toBe(true)
|
||||
expect(terminalOutputPrefersDomRenderer('├─ file.ts')).toBe(true)
|
||||
expect(terminalOutputPrefersDomRenderer('█ progress')).toBe(true)
|
||||
expect(terminalOutputPrefersDomRenderer('◆ status')).toBe(true)
|
||||
expect(terminalOutputPrefersDomRenderer('\uE0B0 prompt')).toBe(true)
|
||||
expect(terminalOutputPrefersDomRenderer('bad replacement �')).toBe(true)
|
||||
})
|
||||
|
||||
it('detects emoji and variation sequences', () => {
|
||||
expect(terminalOutputPrefersDomRenderer('status 🚀')).toBe(true)
|
||||
expect(terminalOutputPrefersDomRenderer('developer 👩💻')).toBe(true)
|
||||
expect(terminalOutputPrefersDomRenderer('heart ♥️')).toBe(true)
|
||||
})
|
||||
|
||||
it('detects supplementary-plane complex-script ranges', () => {
|
||||
expect(terminalOutputPrefersDomRenderer('Adlam: 𞤀')).toBe(true)
|
||||
expect(terminalOutputPrefersDomRenderer('Medefaidrin: ')).toBe(true)
|
||||
})
|
||||
|
||||
it('detects split surrogate chunks so fallback is not lost at chunk boundaries', () => {
|
||||
const [high, low] = Array.from('🚀')[0].split('')
|
||||
|
||||
expect(terminalOutputPrefersDomRenderer(high)).toBe(true)
|
||||
expect(terminalOutputPrefersDomRenderer(low)).toBe(true)
|
||||
})
|
||||
|
||||
it('does not disable WebGL for ordinary terminal output or ANSI controls alone', () => {
|
||||
expect(terminalOutputPrefersDomRenderer('abc 123 ✓')).toBe(false)
|
||||
expect(terminalOutputPrefersDomRenderer('\x1b[32mplain green\x1b[0m')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,8 +1,45 @@
|
||||
// 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
|
||||
// Why: xterm WebGL renders from a glyph atlas; agent TUIs often combine glyphs
|
||||
// that are safer through the browser text path even when they are not RTL.
|
||||
const EMOJI_PRESENTATION_PATTERN = /\p{Emoji_Presentation}/u
|
||||
|
||||
export function terminalOutputRequiresDomRenderer(data: string): boolean {
|
||||
return COMPLEX_SCRIPT_PATTERN.test(data)
|
||||
function isInRange(value: number, start: number, end: number): boolean {
|
||||
return value >= start && value <= end
|
||||
}
|
||||
|
||||
function isRendererRiskCodePoint(value: number): boolean {
|
||||
return (
|
||||
isInRange(value, 0x0590, 0x08ff) ||
|
||||
value === 0x200d ||
|
||||
isInRange(value, 0x2500, 0x259f) ||
|
||||
isInRange(value, 0x25a0, 0x25ff) ||
|
||||
isInRange(value, 0x2800, 0x28ff) ||
|
||||
isInRange(value, 0xd800, 0xdfff) ||
|
||||
isInRange(value, 0xe000, 0xf8ff) ||
|
||||
isInRange(value, 0xfb1d, 0xfdff) ||
|
||||
isInRange(value, 0xfe00, 0xfe0f) ||
|
||||
isInRange(value, 0xfe70, 0xfeff) ||
|
||||
value === 0xfffd ||
|
||||
isInRange(value, 0x10ec0, 0x10eff) ||
|
||||
isInRange(value, 0x1e900, 0x1e95f) ||
|
||||
isInRange(value, 0xe0100, 0xe01ef)
|
||||
)
|
||||
}
|
||||
|
||||
export function terminalOutputPrefersDomRenderer(data: string): boolean {
|
||||
if (EMOJI_PRESENTATION_PATTERN.test(data)) {
|
||||
return true
|
||||
}
|
||||
for (let i = 0; i < data.length; i += 1) {
|
||||
const codePoint = data.codePointAt(i)
|
||||
if (codePoint === undefined) {
|
||||
continue
|
||||
}
|
||||
if (isRendererRiskCodePoint(codePoint)) {
|
||||
return true
|
||||
}
|
||||
if (codePoint > 0xffff) {
|
||||
i += 1
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user