mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 00:02:31 +00:00
refactor(mobile): extract the terminal write-queue slice into a module
The slice also carries `disposeTermObservers` and `extractMouseModeScanTail`, which belong to other concerns but sit here because emitted-document order pins them here; four names stop being externals as a result. The observer disposal keeps its guard-as-expression form behind a line-scoped disable: the rewrite the rule asks for is outside every counted class. Counts: qualified 50, scope declarations 0, rebindings 11, braced bodies 10, unbound catches 1, number properties 0, shorthand properties 0. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
This commit is contained in:
@@ -21,9 +21,6 @@ export declare function getTotalScale(): number
|
||||
/** `runtime-state-and-text-scaling`: posts one message to the native host. */
|
||||
export declare function notify(message: Record<string, unknown>): void
|
||||
|
||||
/** `write-queue`: runs a callback when the replay queue reaches the point it was enqueued at. */
|
||||
export declare function enqueueWriteBoundary(callback: () => void): void
|
||||
|
||||
/** `terminal-init-and-write`: whether xterm is showing the alternate screen buffer. */
|
||||
export declare function isAlternateBufferActive(): boolean
|
||||
|
||||
@@ -42,9 +39,6 @@ export declare function shouldRouteScrollToTerminalInput(): boolean
|
||||
/** `mouse-report-and-scroll-routing`: sends a line scroll to the terminal at a point. */
|
||||
export declare function routeScrollLines(lines: number, clientX: number, clientY: number): void
|
||||
|
||||
/** `term-observers-and-mode-mirroring`: disposes every xterm listener the last terminal held. */
|
||||
export declare function disposeTermObservers(): void
|
||||
|
||||
/** `surface-touch-gestures`: binds touch, wheel and tap handlers to a surface element. */
|
||||
export declare function attachSurfaceEventHandlers(target: HTMLElement): void
|
||||
|
||||
@@ -84,9 +78,6 @@ export declare function buildMouseClickInput(originX: number, originY: number):
|
||||
/** `mouse-report-and-scroll-routing`: whether a tracking mode consumes plain clicks. */
|
||||
export declare function isClickMouseTrackingMode(mode: string): boolean
|
||||
|
||||
/** `write-queue`: the trailing bytes a DECSET scan must carry into the next chunk. */
|
||||
export declare function extractMouseModeScanTail(input: string): string
|
||||
|
||||
/** `host-message-router`: routes one decoded host message. */
|
||||
export declare function handleMsg(msg: unknown): void
|
||||
|
||||
@@ -101,6 +92,3 @@ export declare function clampPan(): void
|
||||
|
||||
/** `smooth-scroll-and-cell-geometry`: writes the pan and scale onto the surface transform. */
|
||||
export declare function updateTransform(): void
|
||||
|
||||
/** `write-queue`: runs a callback once the write queue has drained. */
|
||||
export declare function afterWritesDrained(callback: () => void): void
|
||||
|
||||
@@ -83,6 +83,7 @@ export type TerminalDocumentTerminal = {
|
||||
readonly rows: number
|
||||
readonly buffer: { readonly active: TerminalDocumentBuffer }
|
||||
readonly options: TerminalDocumentTerminalOptions
|
||||
write: (data: string, callback?: () => void) => void
|
||||
readonly _core?: TerminalDocumentCore
|
||||
readonly modes?: { bracketedPasteMode?: boolean }
|
||||
onLineFeed?: (listener: () => void) => TerminalDocumentDisposable
|
||||
@@ -142,6 +143,28 @@ export type TerminalDocumentScope = {
|
||||
sgrMouseMode: boolean
|
||||
/** `runtime-state`: whether the TUI asked for SGR pixel (1016) mouse reports. */
|
||||
sgrMousePixelsMode: boolean
|
||||
/** `runtime-state`: Claude's record dot, which iOS WebKit would otherwise promote to emoji. */
|
||||
CLAUDE_STATUS_DOT: string
|
||||
/** `runtime-state`: the variation selector that forces the text glyph. */
|
||||
TEXT_PRESENTATION_SELECTOR: string
|
||||
/** `runtime-state`: the variation selector that forces the emoji glyph. */
|
||||
EMOJI_PRESENTATION_SELECTOR: string
|
||||
/** `runtime-state`: the dot with any trailing selectors, as one pattern. */
|
||||
CLAUDE_STATUS_DOT_PATTERN: RegExp
|
||||
/** `runtime-state`: whether a chunk ended mid-selector, so the next one starts inside it. */
|
||||
statusDotPendingSelector: boolean
|
||||
/** `runtime-state`: how far a split DECSET may be carried before the scan gives up. */
|
||||
PRIVATE_MODE_SCAN_TAIL_LIMIT: number
|
||||
/** `runtime-state`: chunks and boundaries waiting for xterm. */
|
||||
writeQueue: TerminalWriteQueueEntry[]
|
||||
/** `runtime-state`: how far the queue has been consumed, before compaction. */
|
||||
writeQueueHead: number
|
||||
/** `runtime-state`: whether a write is parsing right now. */
|
||||
writesDraining: boolean
|
||||
/** `runtime-state`: callbacks waiting for the queue to empty. */
|
||||
afterDrainCallbacks: (() => void)[]
|
||||
/** `runtime-state`: whether the terminal has been initialised. */
|
||||
ready: boolean
|
||||
/** `normal-buffer-smooth-scroll`: sub-row scroll travel not yet committed to xterm. */
|
||||
smoothScrollOffsetY: number
|
||||
/** `normal-buffer-smooth-scroll`: scroll travel waiting for the next frame. */
|
||||
@@ -223,6 +246,9 @@ export type TerminalDocumentModes = {
|
||||
sgrMousePixelsMode: boolean
|
||||
}
|
||||
|
||||
/** One entry of the write queue: a chunk, a boundary callback, or a consumed slot. */
|
||||
export type TerminalWriteQueueEntry = string | (() => void) | undefined
|
||||
|
||||
export type TerminalDocumentDisposable = { dispose?: () => void }
|
||||
|
||||
/** xterm's WebGL addon, as the document loads, repaints and disposes of it. */
|
||||
@@ -238,6 +264,10 @@ export type TerminalDocumentWebglAddon = {
|
||||
* A factory rather than a shared literal so a second document — a test, or a page that remounts —
|
||||
* starts from its own state instead of inheriting what the last one left.
|
||||
*/
|
||||
const statusDot = String.fromCharCode(0x23fa)
|
||||
const textPresentationSelector = String.fromCharCode(0xfe0e)
|
||||
const emojiPresentationSelector = String.fromCharCode(0xfe0f)
|
||||
|
||||
export function createTerminalDocumentScope(): TerminalDocumentScope {
|
||||
return {
|
||||
term: null,
|
||||
@@ -268,6 +298,20 @@ export function createTerminalDocumentScope(): TerminalDocumentScope {
|
||||
trackedMouseTrackingMode: 'none',
|
||||
sgrMouseMode: false,
|
||||
sgrMousePixelsMode: false,
|
||||
CLAUDE_STATUS_DOT: statusDot,
|
||||
TEXT_PRESENTATION_SELECTOR: textPresentationSelector,
|
||||
EMOJI_PRESENTATION_SELECTOR: emojiPresentationSelector,
|
||||
CLAUDE_STATUS_DOT_PATTERN: new RegExp(
|
||||
statusDot + '[' + textPresentationSelector + emojiPresentationSelector + ']*',
|
||||
'g'
|
||||
),
|
||||
statusDotPendingSelector: false,
|
||||
PRIVATE_MODE_SCAN_TAIL_LIMIT: 4096,
|
||||
writeQueue: [],
|
||||
writeQueueHead: 0,
|
||||
writesDraining: false,
|
||||
afterDrainCallbacks: [],
|
||||
ready: false,
|
||||
smoothScrollOffsetY: 0,
|
||||
pendingNormalScrollDeltaY: 0,
|
||||
normalScrollFrameId: null,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { extractMouseModeScanTail } from './document-externals'
|
||||
import { extractMouseModeScanTail } from './write-queue'
|
||||
import { scope } from './document-scope'
|
||||
|
||||
export function isAltScreenActive(data: unknown): data is string {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { enqueueWriteBoundary, notify } from './document-externals'
|
||||
import { enqueueWriteBoundary } from './write-queue'
|
||||
import { notify } from './document-externals'
|
||||
import { scope, type TerminalDocumentDisposable } from './document-scope'
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { attachSurfaceEventHandlers, disposeTermObservers } from './document-externals'
|
||||
import { disposeTermObservers } from './write-queue'
|
||||
import { attachSurfaceEventHandlers } from './document-externals'
|
||||
import { scope, type TerminalDocumentTerminal } from './document-scope'
|
||||
|
||||
/** The surfaces and terminal a swap is replacing, handed back to whoever commits it. */
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import {
|
||||
afterWritesDrained,
|
||||
disposeTermObservers,
|
||||
updateScrollIndicator
|
||||
} from './document-externals'
|
||||
import { afterWritesDrained, disposeTermObservers } from './write-queue'
|
||||
import { updateScrollIndicator } from './document-externals'
|
||||
import { scope } from './document-scope'
|
||||
import { logFeedAndEvict } from './selection-state-and-eviction'
|
||||
import { emitKeyboardAvoidanceMetrics } from './keyboard-avoidance-metrics'
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { emitTerminalDocumentModule } from '../../../scripts/build-terminal-document-script.mjs'
|
||||
import { TERMINAL_HTML_WRITE_QUEUE } from '../terminal-webview-html/write-queue'
|
||||
import { compareTerminalDocumentScripts } from './terminal-document-equivalence.test-support'
|
||||
|
||||
const modulePath = fileURLToPath(new URL('./write-queue.ts', import.meta.url))
|
||||
|
||||
describe('the write-queue module', () => {
|
||||
it('emits the script the document carries, modulo the six normalisations', async () => {
|
||||
const emitted = await emitTerminalDocumentModule(modulePath)
|
||||
expect(compareTerminalDocumentScripts(TERMINAL_HTML_WRITE_QUEUE, emitted, 'scope')).toEqual({
|
||||
equivalent: true,
|
||||
normalisations: {
|
||||
// The queue and its head throughout, plus the status-dot constants, the drain flags and
|
||||
// the generation the pump is running for.
|
||||
qualifiedReferences: 50,
|
||||
scopeFieldDeclarations: 0,
|
||||
rebindings: 11,
|
||||
bracedBodies: 10,
|
||||
// Disposing an observer that is already gone.
|
||||
unboundCatches: 1,
|
||||
numberProperties: 0,
|
||||
shorthandProperties: 0
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,135 @@
|
||||
import { scope } from './document-scope'
|
||||
|
||||
export function resetWriteQueue() {
|
||||
scope.writeQueue = []
|
||||
scope.writeQueueHead = 0
|
||||
}
|
||||
|
||||
export function isStatusDotPresentationSelector(value: string) {
|
||||
return value === scope.TEXT_PRESENTATION_SELECTOR || value === scope.EMOJI_PRESENTATION_SELECTOR
|
||||
}
|
||||
|
||||
export function endsWithStatusDotPresentationSequence(data: string) {
|
||||
let i = data.length - 1
|
||||
while (i >= 0 && isStatusDotPresentationSelector(data.charAt(i))) {
|
||||
i--
|
||||
}
|
||||
return i >= 0 && data.charAt(i) === scope.CLAUDE_STATUS_DOT
|
||||
}
|
||||
|
||||
// Why: iOS WebKit promotes Claude's record/status dot to a colorful emoji glyph.
|
||||
export function normalizeStatusDotPresentation(data: string) {
|
||||
if (typeof data !== 'string' || data.length === 0) {
|
||||
return data
|
||||
}
|
||||
if (scope.statusDotPendingSelector) {
|
||||
scope.statusDotPendingSelector = false
|
||||
let strippedPendingSelectors = false
|
||||
while (data.length > 0 && isStatusDotPresentationSelector(data.charAt(0))) {
|
||||
data = data.slice(1)
|
||||
}
|
||||
strippedPendingSelectors = data.length === 0
|
||||
if (strippedPendingSelectors) {
|
||||
scope.statusDotPendingSelector = true
|
||||
return ''
|
||||
}
|
||||
}
|
||||
const normalized = data.replace(
|
||||
scope.CLAUDE_STATUS_DOT_PATTERN,
|
||||
scope.CLAUDE_STATUS_DOT + scope.TEXT_PRESENTATION_SELECTOR
|
||||
)
|
||||
scope.statusDotPendingSelector = endsWithStatusDotPresentationSequence(data)
|
||||
return normalized
|
||||
}
|
||||
|
||||
export function enqueueWrite(data: string) {
|
||||
scope.writeQueue.push(normalizeStatusDotPresentation(data))
|
||||
}
|
||||
|
||||
export function enqueueWriteBoundary(callback: () => void) {
|
||||
scope.writeQueue.push(callback)
|
||||
}
|
||||
|
||||
export function nextQueuedWrite() {
|
||||
if (scope.writeQueueHead >= scope.writeQueue.length) {
|
||||
resetWriteQueue()
|
||||
return undefined
|
||||
}
|
||||
const next = scope.writeQueue[scope.writeQueueHead]
|
||||
scope.writeQueue[scope.writeQueueHead] = undefined
|
||||
scope.writeQueueHead++
|
||||
// Why: high-throughput terminals can enqueue faster than xterm parses;
|
||||
// compact consumed slots so drain work stays O(1) without retaining old chunks.
|
||||
if (scope.writeQueueHead > 128 && scope.writeQueueHead * 2 > scope.writeQueue.length) {
|
||||
scope.writeQueue = scope.writeQueue.slice(scope.writeQueueHead)
|
||||
scope.writeQueueHead = 0
|
||||
}
|
||||
return next
|
||||
}
|
||||
|
||||
export function disposeTermObservers() {
|
||||
const disposables = scope.termObserverDisposables
|
||||
scope.termObserverDisposables = []
|
||||
for (let i = 0; i < disposables.length; i++) {
|
||||
try {
|
||||
// oxlint-disable-next-line no-unused-expressions, typescript/no-non-null-assertion -- SAFETY: the guard is the call's own condition, and it proves the method is there.
|
||||
disposables[i] && disposables[i].dispose && disposables[i].dispose!()
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
export function extractMouseModeScanTail(input: string) {
|
||||
const start = Math.max(input.lastIndexOf(scope.ESC), input.lastIndexOf(scope.C1_CSI))
|
||||
if (start === -1) {
|
||||
return ''
|
||||
}
|
||||
const tail = input.slice(start)
|
||||
// Why: PTY/SSH chunks can split a long combined DECSET before the final h/l.
|
||||
// Keep parser state far beyond normal mode lists while still bounding memory.
|
||||
if (tail.length > scope.PRIVATE_MODE_SCAN_TAIL_LIMIT) {
|
||||
return ''
|
||||
}
|
||||
if (tail === scope.ESC || tail === scope.ESC + '[' || tail === scope.C1_CSI) {
|
||||
return tail
|
||||
}
|
||||
if (tail.indexOf(scope.ESC + '[?') === 0) {
|
||||
return /^[0-9;]*$/.test(tail.slice(3)) ? tail : ''
|
||||
}
|
||||
if (tail.indexOf(scope.C1_CSI + '?') === 0) {
|
||||
return /^[0-9;]*$/.test(tail.slice(2)) ? tail : ''
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
export function pumpWrites(gen: number): void {
|
||||
if (!scope.ready || !scope.term || scope.writesDraining || gen !== scope.terminalGeneration) {
|
||||
return
|
||||
}
|
||||
const next = nextQueuedWrite()
|
||||
if (typeof next !== 'string') {
|
||||
if (typeof next === 'function') {
|
||||
return (next(), pumpWrites(gen))
|
||||
}
|
||||
const callbacks = scope.afterDrainCallbacks
|
||||
scope.afterDrainCallbacks = []
|
||||
for (let i = 0; i < callbacks.length; i++) {
|
||||
callbacks[i]()
|
||||
}
|
||||
return
|
||||
}
|
||||
scope.writesDraining = true
|
||||
// Why: xterm.write() parses asynchronously. Row adjustment/resizing must
|
||||
// wait until replayed SGR attributes have landed in the buffer.
|
||||
scope.term.write(next, function () {
|
||||
if (gen !== scope.terminalGeneration) {
|
||||
return
|
||||
}
|
||||
scope.writesDraining = false
|
||||
pumpWrites(gen)
|
||||
})
|
||||
}
|
||||
|
||||
export function afterWritesDrained(callback: () => void) {
|
||||
scope.afterDrainCallbacks.push(callback)
|
||||
pumpWrites(scope.terminalGeneration)
|
||||
}
|
||||
Reference in New Issue
Block a user