Files
orca/mobile/scripts
Jinwoo-H 2a5d3eccdb feat(mobile): generate the terminal document from its modules
The WebView document is no longer a hand-written IIFE pasted into a template
string. `scripts/build-terminal-document-script.mjs` reads `document-scope.ts`
and the 36 modules under `src/terminal/document/` in document order, strips
their imports, exports and line-scoped lint directives, substitutes the
`document-constants.ts` exports textually, reprints each with esbuild and wraps
the result in one IIFE. `terminal-webview-html.ts` composes the shell, that
generated script and the close fragment. The artifact is gitignored and built by
postinstall, like the two engine artifacts.

The emitted document is token-equivalent to the old one under eight counted
normalisation classes, each pinned as an exact number in
`document/terminal-document-flip.test.ts` against the pre-flip text:

  qualifiedReferences     609
  scopeFieldDeclarations   73
  rebindings              373
  bracedBodies            279
  unboundCatches           36
  numberProperties         17
  shorthandProperties       4
  unshadowedNames           7

Any other difference fails with the token index and both sides. The second case
pins that the new document adds the scope object and nothing else.

Ruling 17: the behavioural tests now grep the generated document through
`XTERM_HTML`, never a module source, so every assertion still speaks about what
the WebView runs. Every assertion stays and the `expect` count per file is
unchanged: scroll-routing 95, text-zoom 59, engine 49, url-tap 33, reflow 22,
keyboard-avoidance 18, query-reply 14. One control per file was run by deleting
the module line the updated pattern guards; all seven red, and the tree restores
green.

Pattern changes, old -> new.

terminal-webview-scroll-routing.test.ts
  var deltaY = ts.lastY - y;                    -> const deltaY = ts.lastY - y;
  smoothScrollOffsetY -= deltaY;                -> scope.smoothScrollOffsetY -= deltaY;
  var lines = Math.trunc(-smoothScrollOffsetY / effectiveCellH);
                                                -> const lines = Math.trunc(-scope.smoothScrollOffsetY / effectiveCellH);
  'touchmove' single-quoted, one line           -> "touchmove" double-quoted, printer line break
  }, { capture: true, passive: false });        -> { capture: true, passive: false }
  function momentumStep()                       -> let momentumStep = function()
  pendingNormalScrollDeltaY += deltaY;          -> scope.pendingNormalScrollDeltaY += deltaY;
  if (normalScrollFrameId !== null) return true; -> if (scope.normalScrollFrameId !== null) {
  normalScrollFrameId = requestAnimationFrame(  -> scope.normalScrollFrameId = requestAnimationFrame(
  pendingNormalScrollDeltaY = 0;                -> scope.pendingNormalScrollDeltaY = 0;
  cancelAnimationFrame(normalScrollFrameId);    -> cancelAnimationFrame(scope.normalScrollFrameId);
  var writeQueueHead = 0;                       -> scope.writeQueueHead = 0;
  writeQueueHead++;                             -> scope.writeQueueHead++;
  writeQueue = writeQueue.slice(writeQueueHead); -> scope.writeQueue = scope.writeQueue.slice(scope.writeQueueHead);
  surface.style.transform = 'translate(' + panX  -> scope.surface.style.transform = "translate(" + scope.panX
  getVisualPanY() + 'px) scale('                -> getVisualPanY() + "px) scale("
  var FRICTION = 0.972;                         -> const FRICTION = 0.972;
  var MIN_VEL = 0.012;                          -> const MIN_VEL = 0.012;
  edgeScrollDir = dir;                          -> scope.edgeScrollDir = dir;
  term.scrollLines(edgeScrollDir);              -> scope.term.scrollLines(scope.edgeScrollDir);
  // Latching document-level touch dispatcher    -> function attachSurfaceEventHandlers(
  edgeScrollClientX = clientX;                  -> scope.edgeScrollClientX = clientX;
  edgeScrollClientY = clientY;                  -> scope.edgeScrollClientY = clientY;
  return mode !== 'none';                       -> return mode !== "none";
  var pixelX = cell.x;                          -> const pixelX = cell.x;
  var pixelY = cell.y;                          -> const pixelY = cell.y;
  ...isSafeSgrMouseCoordinate(cell.y)) return   -> ...isSafeSgrMouseCoordinate(cell.y)) {
  ...isSafeSgrMouseCoordinate(sgrRow)) return   -> ...isSafeSgrMouseCoordinate(sgrRow)) {
  if (mouseTrackingMode === 'x10') return pixelPress; -> if (mouseTrackingMode === "x10") { return pixelPress;
  if (mouseTrackingMode === 'x10') return sgrPress;   -> if (mouseTrackingMode === "x10") { return sgrPress;
  if (mouseTrackingMode === 'x10') return press;      -> if (mouseTrackingMode === "x10") { return press;
  if (col > 126 || row > 126) return '';        -> if (col > 126 || row > 126) { return "";
  document.addEventListener('touchend'          -> document.addEventListener( "touchend"
  }, { capture: true, passive: true });         -> { capture: true, passive: true }
  notifyTerminalSurfaceTap(tapCandidate.x, ...) -> notifyTerminalSurfaceTap(scope.tapCandidate.x, ...)
  document.addEventListener('touchstart'        -> document.addEventListener( "touchstart"
  var clickInput = buildMouseClickInput         -> const clickInput = buildMouseClickInput
  notify({ type: 'open-url', url: tappedUrl });      -> notify({ type: "open-url", url: tappedUrl });
  notify({ type: 'terminal-input', bytes: clickInput }); -> notify({ type: "terminal-input", bytes: clickInput });

terminal-webview-text-zoom.test.ts
  var CLAUDE_STATUS_DOT =                       -> scope.CLAUDE_STATUS_DOT =
  var PRIVATE_MODE_SCAN_TAIL_LIMIT              -> scope.PRIVATE_MODE_SCAN_TAIL_LIMIT
  \n\n  function enqueueWrite                   -> \n  function enqueueWrite
  var terminalFontFamily =                      -> scope.terminalFontFamily =
  output = terminalFontFamily;                  -> output = scope.terminalFontFamily;
  String.fromCharCode(0x23fa)                   -> String.fromCharCode(9210)
  TEXT_PRESENTATION_SELECTOR = String.fromCharCode(0xfe0e)  -> scope.TEXT_PRESENTATION_SELECTOR = String.fromCharCode(65038)
  EMOJI_PRESENTATION_SELECTOR = String.fromCharCode(0xfe0f) -> scope.EMOJI_PRESENTATION_SELECTOR = String.fromCharCode(65039)
  data.replace(CLAUDE_STATUS_DOT_PATTERN, ...)  -> data.replace( scope.CLAUDE_STATUS_DOT_PATTERN, scope.CLAUDE_STATUS_DOT + scope.TEXT_PRESENTATION_SELECTOR )
  writeQueue.push(normalizeStatusDotPresentation(data)) -> scope.writeQueue.push(normalizeStatusDotPresentation(data))
  var replayData = normalizeInitialData(initialData) -> const replayData = normalizeInitialData(initialData)
  } else if (msg.type === 'clear') {            -> } else if (msg.type === "clear") {
  } else if (msg.type === 'measure')            -> } else if (msg.type === "measure")
  statusDotPendingSelector = false              -> scope.statusDotPendingSelector = false   (x2)
  term.open(surface)                            -> scope.term.open(scope.surface)
  term.unicode.activeVersion = '11'             -> scope.term.unicode.activeVersion = "11"
  enqueueWrite(ESC + '[0m' + replayData)        -> enqueueWrite(scope.ESC + "[0m" + replayData)
  fontFamily: terminalFontFamily                -> fontFamily: scope.terminalFontFamily
  fontWeight: '300'                             -> fontWeight: "300"
  fontWeightBold: '500'                         -> fontWeightBold: "500"

terminal-webview-engine.test.ts
  var webglAddon = null; .. var webglRecoveryTimer = null;
                                                -> the refreshTerminalSurface()..init( block, with the scope preamble
  window.addEventListener('resize'              -> window.addEventListener("resize"
  'terminal init failed'                        -> "terminal init failed"
  'terminal message failed'                     -> "terminal message failed"
  var everReady = false;                        -> scope.everReady = false;
  everReady = true;                             -> scope.everReady = true;
  fatal === undefined ? !everReady : !!fatal    -> fatal === void 0 ? !scope.everReady : !!fatal
  msg.type === 'init' && !everReady             -> msg.type === "init" && !scope.everReady
  /fatal === undefined \? !ready\b/             -> /fatal === void 0 \? !scope\.ready\b/
  if (msg.type === 'ping')                      -> if (msg.type === "ping")
  notify({ type: 'pong', pingId: msg.id })      -> notify({ type: "pong", pingId: msg.id })

terminal-webview-reflow.test.ts
  } else if (msg.type === 'reflow') {           -> } else if (msg.type === "reflow") {   (x2)
  var MIN_FIT_COLS = 20;                        -> scope.MIN_FIT_COLS = 20;
  if (cols < MIN_FIT_COLS) return;              -> if (cols < scope.MIN_FIT_COLS) {
  flog('measure-skip-small-width'               -> flog("measure-skip-small-width"
  notify({ type: 'measure-result', ... })       -> notify({ type: "measure-result", ... })
  var dispatch = { mode: 'idle'                 -> const dispatch = { mode: "idle"
  window.addEventListener('message'             -> window.addEventListener("message"

terminal-keyboard-avoidance-webview.test.ts
  \n  // reflow()                               -> \n  function reflow(
  } else if (msg.type === 'clear') {            -> } else if (msg.type === "clear") {
  } else if (msg.type === 'measure')            -> } else if (msg.type === "measure")
  \n  var panX                                  -> \n  scope.panX
  TERMINAL_REFLOW_JS fragment import            -> the reflow(cols, rows)..notify( slice of the document

terminal-webview-query-reply.test.ts
  attachTerminalQueryReplyBridge(term, gen)     -> attachTerminalQueryReplyBridge(scope.term, gen)   (x2)
  term.attachCustomKeyEventHandler(function() { return false; })
                                                -> term.attachCustomKeyEventHandler(function() { \n return false; \n });
  term.textarea.readOnly = true                 -> term.textarea.readOnly = true;
  } else if (msg.type === 'clear') {            -> } else if (msg.type === "clear") {
  } else if (msg.type === 'measure')            -> } else if (msg.type === "measure")

terminal-webview-url-tap.test.ts
  notify({ type: 'open-url', url: tappedUrl }); -> notify({ type: "open-url", url: tappedUrl });

terminal-webview-payload-hash.test.ts is the document byte pin; it moves to the
generated document's digest, 730472 -> 723480 bytes.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-20 10:39:02 -04:00
..