mirror of
https://github.com/stablyai/orca.git
synced 2026-10-01 00:02:10 +00:00
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:
@@ -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 () {
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
Reference in New Issue
Block a user