refactor(mobile): one start seam, no source cache, the reported box as an object

- `useState` already pins each view's WebView source at mount (a new
  test re-renders at another text scale and gets the same object), so
  the module-level `webViewSources` Map goes.
- `initialTextScale` and `buildsTerminalBeforeReady` become one
  `start(): { textScale, shown }` seam.
- `reportedCellBox` holds the last reported box and grid, not a string key.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
This commit is contained in:
Jinwoo-H
2026-09-28 02:19:37 -04:00
parent 642626708e
commit 21954dbd2f
12 changed files with 59 additions and 67 deletions
@@ -45,6 +45,9 @@ export type TerminalDocumentErrorReporter = (
/** A frame from the host, as its transport delivers it: JSON text from a bridge, or the object. */
export type TerminalDocumentHostFrame = string | Record<string, unknown> | undefined
/** How a document starts, as its view mounted. */
export type TerminalDocumentStart = { textScale: number; shown: boolean }
/**
* The ten host seams, kept apart from the state because the host sets them once when it builds
* the scope, before the start sequence runs, and no module writes them afterwards.
@@ -68,13 +71,12 @@ export type TerminalDocumentHostSeams = {
installHostTransport: (receive: (frame: TerminalDocumentHostFrame) => void) => () => void
/** `message-bridge`: whether the engine is here, which is what readiness is reported on. */
hasEngine: () => boolean
/** `text-scaling`: the app's text scale, so the terminal built before ready lays out at it. */
initialTextScale: () => number
/**
* `message-bridge`: whether to build the terminal before ready. Only a document shown when its
* view mounted does; every terminal is a WebGL context, and a page or app holds about sixteen.
* How the document starts, as its view mounted: the app's text scale, which the terminal built
* before ready lays out at, and whether it was shown. Only a shown document builds before ready;
* every terminal is a WebGL context, and a page or app holds about sixteen.
*/
buildsTerminalBeforeReady: () => boolean
start: () => TerminalDocumentStart
/** Every fit, pan, scroll and overlay bound, and every client point mapped into the grid. */
viewportRect: () => TerminalDocumentViewportRect
/** `fit-scale`: calls back when that box changes size or is shown again, handing back its removal. */
@@ -231,13 +233,12 @@ export function windowHasEngine() {
return window.Terminal !== undefined
}
export function windowInitialTextScale() {
export function windowStart(): TerminalDocumentStart {
const scale = window.__orcaTerminalTextScale
return typeof scale === 'number' && scale > 0 ? scale : 1
}
export function windowBuildsTerminalBeforeReady() {
return window.__orcaTerminalShown !== false
return {
textScale: typeof scale === 'number' && scale > 0 ? scale : 1,
shown: window.__orcaTerminalShown !== false
}
}
/**
@@ -1,3 +1,4 @@
import type { TerminalCellBox } from '../terminal-cell-box'
import { DEFAULT_TERMINAL_THEME } from '../terminal-webview-html/theme'
import {
createEngineTerminal,
@@ -9,8 +10,7 @@ import {
postToReactNativeWebView,
windowCapturedEngineErrors,
windowHasEngine,
windowInitialTextScale,
windowBuildsTerminalBeforeReady,
windowStart,
observeWindowViewport,
windowViewportRect,
type TerminalDocumentHost,
@@ -97,7 +97,7 @@ export type TerminalDocumentState = {
/** `terminal-init`: whether the first live chunk since init is still pending. */
firstDataPending: boolean
/** `cell-metrics-probe`: the laid-out cell box last reported for the current terminal. */
reportedCellBox: string
reportedCellBox: { cellBox: TerminalCellBox; cols: number; rows: number } | null
/** `terminal-init`: whether the replayed snapshot was an alternate screen. */
activeAltScreenSnapshot: boolean
/** `fit-scale`: the fit scale the document committed. */
@@ -271,7 +271,7 @@ function createTerminalDocumentState(): TerminalDocumentState {
currentTextScale: 1,
terminalFontFamily: '',
firstDataPending: false,
reportedCellBox: '',
reportedCellBox: null,
activeAltScreenSnapshot: false,
currentScale: 1,
userScale: 1,
@@ -357,8 +357,7 @@ function createTerminalDocumentHostSeams(): TerminalDocumentHostSeams {
paintDocumentBackground: paintWindowDocumentBackground,
installHostTransport: installWindowHostTransport,
hasEngine: windowHasEngine,
initialTextScale: windowInitialTextScale,
buildsTerminalBeforeReady: windowBuildsTerminalBeforeReady,
start: windowStart,
viewportRect: windowViewportRect,
observeViewport: observeWindowViewport,
root: null
@@ -146,7 +146,7 @@ describe('a started document', () => {
})
it('reports ready with the box its own terminal laid out, at the app text scale', () => {
const { scope, posted, built } = started({ initialTextScale: () => 1.25 })
const { scope, posted, built } = started({ start: () => ({ textScale: 1.25, shown: true }) })
try {
expect(built).toHaveLength(1)
expect(built[0].options.fontSize).toBe(fontPxForScale(1.25))
@@ -175,7 +175,7 @@ describe('a started document', () => {
})
it('builds nothing before ready for a view hidden when it mounted, and reports no box', () => {
const { scope, posted, built } = started({ buildsTerminalBeforeReady: () => false })
const { scope, posted, built } = started({ start: () => ({ textScale: 1, shown: false }) })
try {
expect(built).toHaveLength(0)
expect(posted[0]).toEqual({ type: 'web-ready', cellBox: null })
@@ -28,15 +28,17 @@ export function reportLaidOutCellBox(scope: TerminalDocumentScope) {
return
}
const { cols, rows } = scope.term
const key = `${laidOut.fontScale}:${laidOut.cellWidth}x${laidOut.cellHeight}@${cols}x${rows}`
if (key === scope.reportedCellBox) {
const last = scope.reportedCellBox
if (
last &&
last.cols === cols &&
last.rows === rows &&
last.cellBox.fontScale === laidOut.fontScale &&
last.cellBox.cellWidth === laidOut.cellWidth &&
last.cellBox.cellHeight === laidOut.cellHeight
) {
return
}
scope.reportedCellBox = key
notify(scope, {
type: 'cell-metrics',
cellBox: laidOut,
cols,
rows
})
scope.reportedCellBox = { cellBox: laidOut, cols, rows }
notify(scope, { type: 'cell-metrics', ...scope.reportedCellBox })
}
@@ -48,7 +48,7 @@ export function startMessageBridge(scope: TerminalDocumentScope) {
)
if (scope.hasEngine()) {
// Why: ready carries the cell box xterm itself laid out, so the host sizes the first subscribe.
if (scope.buildsTerminalBeforeReady()) {
if (scope.start().shown) {
prepareTerminal(scope)
}
notify(scope, { type: 'web-ready', cellBox: laidOutCellBox(scope) })
@@ -11,7 +11,7 @@ export function attachTermObservers(scope: TerminalDocumentScope) {
return
}
disposeTermObservers(scope)
scope.reportedCellBox = ''
scope.reportedCellBox = null
try {
scope.termObserverDisposables.push(
scope.term.onLineFeed!(function () {
+1 -1
View File
@@ -102,7 +102,7 @@ export function applyTextScale(scope: TerminalDocumentScope, scale: number) {
}
export function startTextScaling(scope: TerminalDocumentScope) {
scope.currentTextScale = scope.initialTextScale()
scope.currentTextScale = scope.start().textScale
scope.scrollIndicator = elementInRoot(scope.root, 'scroll-indicator')
scope.scrollThumb = elementInRoot(scope.root, 'scroll-thumb')
scope.terminalFontFamily =
@@ -32,8 +32,8 @@ vi.mock('./document/create-terminal-document', async (importOriginal) => {
return {
...actual,
createTerminalDocument: (host: Parameters<typeof actual.createTerminalDocument>[0]) => {
startedScales.push(host?.initialTextScale?.() ?? 1)
startedBuilds.push(host?.buildsTerminalBeforeReady?.() ?? true)
startedScales.push(host?.start?.().textScale ?? 1)
startedBuilds.push(host?.start?.().shown ?? true)
if (startThrows) {
throw startThrows
}
@@ -2,7 +2,7 @@ import { Terminal } from '@xterm/xterm'
import { Unicode11Addon } from '@xterm/addon-unicode11'
import { WebglAddon } from '@xterm/addon-webgl'
import type { TerminalDocumentTerminal } from './document/document-terminal-shape'
import type { TerminalViewportChange } from './document/document-host-seams'
import type { TerminalDocumentStart, TerminalViewportChange } from './document/document-host-seams'
import { TERMINAL_DOCUMENT_ELEMENT_STYLE, TERMINAL_DOCUMENT_MARKUP } from './terminal-webview-html'
import { scopeStyleToHost } from '../style-scoping/document-style-scoping'
import { XTERM_ENGINE_CSS } from './terminal-webview-engine-css.generated'
@@ -99,12 +99,10 @@ function createPageWebglAddon(onFallback: (reason: string) => void) {
* A caller's cleanup can therefore never arrive before there is something to clean up.
*/
/** What the view fixed when it mounted, for every document it builds. */
export type TerminalWebDocumentStart = { textScale: number; shown: boolean }
export function mountTerminalWebDocument(
host: HTMLElement,
receive: (message: Record<string, unknown>) => void,
start: TerminalWebDocumentStart = { textScale: 1, shown: true }
start: TerminalDocumentStart = { textScale: 1, shown: true }
): TerminalWebDocument {
ensureDocumentStyle()
host.classList.add(HOST_CLASS)
@@ -138,7 +136,7 @@ export function mountTerminalWebDocument(
function startDocumentOrGiveTheHostBack(
host: HTMLElement,
receive: (message: Record<string, unknown>) => void,
start: TerminalWebDocumentStart,
start: TerminalDocumentStart,
viewport: PageViewport
) {
try {
@@ -194,7 +192,7 @@ function pageViewport(host: HTMLElement) {
function startPageDocument(
host: HTMLElement,
receive: (message: Record<string, unknown>) => void,
start: TerminalWebDocumentStart,
start: TerminalDocumentStart,
viewport: PageViewport
) {
// Written by this document's own reporter: `startHostNotify` installs it through the seam below,
@@ -242,8 +240,7 @@ function startPageDocument(
// fail. Here the engine is the import above, so it is here or this module did not load.
hasEngine: () => true,
initialTextScale: () => start.textScale,
buildsTerminalBeforeReady: () => start.shown,
start: () => start,
// The window here is the whole page, header and dock included; the grid is shown in the host.
viewportRect: viewport.rect,
@@ -244,6 +244,13 @@ describe('the cell box xterm laid out', () => {
expect(start).toBeLessThan(html.indexOf('startTerminalDocument'))
})
it('keeps the source it mounted with across renders, so a new text scale does not reload', () => {
const { webView } = mount(1)
const source = webView().props.source
act(() => renderers[0]!.update(createElement(TerminalWebView, { textScale: 1.5 })))
expect(webView().props.source).toBe(source)
})
it('tells a document whose view was hidden at mount not to build before ready', () => {
let renderer: ReactTestRenderer | undefined
act(() => {
+11 -20
View File
@@ -21,29 +21,20 @@ export const XTERM_HTML = [
TERMINAL_HTML_DOCUMENT_CLOSE
].join('')
const webViewSources = new Map<string, { html: string }>()
/**
* The WebView's document, starting as its view mounted: at this text scale, and whether it was
* shown. Written into the page ahead of the document script, which reads them as it starts; one
* object per pair, so a parent render never hands the WebView a new source and reloads it.
* shown. Written into the page ahead of the document script, which reads them as it starts.
*/
export function xtermWebViewSource(start: { textScale: number; shown: boolean }) {
const key = `${start.textScale}:${start.shown}`
let source = webViewSources.get(key)
if (!source) {
const startValues =
`window.__orcaTerminalTextScale = ${JSON.stringify(start.textScale)};\n` +
`window.__orcaTerminalShown = ${JSON.stringify(start.shown)};\n`
source = {
html: [
TERMINAL_HTML_DOCUMENT_SHELL,
startValues,
TERMINAL_DOCUMENT_SCRIPT,
TERMINAL_HTML_DOCUMENT_CLOSE
].join('')
}
webViewSources.set(key, source)
const startValues =
`window.__orcaTerminalTextScale = ${JSON.stringify(start.textScale)};\n` +
`window.__orcaTerminalShown = ${JSON.stringify(start.shown)};\n`
return {
html: [
TERMINAL_HTML_DOCUMENT_SHELL,
startValues,
TERMINAL_DOCUMENT_SCRIPT,
TERMINAL_HTML_DOCUMENT_CLOSE
].join('')
}
return source
}
@@ -8,10 +8,6 @@ import { startTextScaling } from './document/text-scaling'
import { webviewPageSource } from './document/document-module-source.test-support'
const terminalWebViewSource = readFileSync(join(import.meta.dirname, 'TerminalWebView.tsx'), 'utf8')
const terminalHtmlModuleSource = readFileSync(
join(import.meta.dirname, 'terminal-webview-html.ts'),
'utf8'
)
const terminalHtmlDocumentShellSource = readFileSync(
join(import.meta.dirname, 'terminal-webview-html', 'document-shell.ts'),
'utf8'
@@ -65,10 +61,9 @@ describe('TerminalWebView text zoom', () => {
const end = terminalWebViewSource.indexOf('/>', start)
expect(end).toBeGreaterThan(start)
const webViewProps = terminalWebViewSource.slice(start, end)
// One source object per view, and one per start pair across views.
// One source object per view, pinned at mount.
expect(terminalWebViewSource).toContain('const [source] = useState(() =>')
expect(webViewProps).toContain('source={source}')
expect(terminalHtmlModuleSource).toContain('webViewSources.set(key, source)')
})
it('forces the Claude status dot to text presentation before xterm writes', () => {