mirror of
https://github.com/stablyai/orca.git
synced 2026-09-21 16:02:20 +00:00
* feat(mobile): paint browser screencast frames through web siblings (OTA phase C, C6.2) The pane's frame path is written against React Native's native-prop writer, which does not exist on React Native Web: a ref there is the DOM node, so both writes throw and the pane never shows a frame. Three `.web.ts` siblings, each for a measured gap. - The image and layer writes move out of `mobile-browser-frame-state.ts` into `browser-frame-layer-paint.ts`, whose sibling paints the frame as a `background-image` on the element RN Web sizes and flips the double buffer with one opacity write per layer. The pane still never re-renders while it streams. - A `background-image` write fires no load event, so the offscreen layer would never become visible. The sibling arms the flip from an image decode instead, and the flip itself is shared with the native `onLoad` path rather than written twice. - The data URI keeps the base64 the bridge already carried instead of encoding the bytes back into the same string. Measured in this tree against the `buffer` shim the page bundle resolves: 0.256 ms per frame at 45,815 bytes and 2.61 ms at 463,942, against under a microsecond for the carried string. Per C6 ruling 5 the pane asks for binary frames only when the shell granted the lane, and renders its existing stream-error state otherwise, so a page never waits on frames a shell without the encoder cannot send. The grant name is a placeholder until C6.1 reports it. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * feat(mobile): budget the mobile view's frame area against the bridge cap (OTA phase C, C6.3) A screencast frame crosses the bridge as one message under BRIDGE_MAX_MESSAGE_BYTES. Measured here: the phone's mobile view at the native device scale factor asks for a 390x712 viewport at 2x, which is 1,110,720 device pixels, and at the worst case JPEG has at quality 72 that is a 807,559-byte message against a 655,360-byte cap — 123% of it. The `.web.ts` sibling holds the mobile view inside that. The budget is computed rather than written down: the cap, less an envelope this measures from the frame's own shape at its widest (435 bytes), is what the base64 may occupy; three quarters of that is the JPEG; divided by one named worst-case constant of 0.545 bytes per pixel it is an area of 901,271 pixels. The phone lands on a device scale factor of 1.80 and a 654,205-byte message, 99.8% of the cap. A cap that moved and a budget that did not would be a pane going dark on a page it could have streamed. Web view mode is untouched, and byte-identical to the native request: there the frame is a desktop viewport letterboxed into maxWidth/maxHeight, which the page cannot predict, so C6 ruling 1's drop-the-over-cap-frame rule is its only protection. Native is unchanged. The constants and the assembly move to a third module because a `.web.ts` cannot import a value from the file it shadows — the bundler resolves the specifier back to the sibling itself — and two copies of them would drift. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * feat(mobile): put the browser pane's text inputs on the web font-size seam (OTA phase C, C6.4) The pane has two text inputs, the address bar at 12px and the key row's "Type on page…" at 14px, and neither went through TEXT_INPUT_FONT_SIZE. In a browser an input under 16px makes iOS zoom the page on focus and never zoom back, and keyboard-occlusion.web.ts reads a visual viewport scale other than 1 as "no keyboard" — so one focus would leave the pane's keyboard lift at 0 for the rest of the typing session. C4.2's failure exactly, on a screen its census does not walk: that census walks the source-control hub and the review route, and the pane is in neither until C7 lists a route that mounts it. The key row's input goes straight onto the seam, whose native value is the theme's body size, so it renders at the 14px it already did. The address bar is a `.web.ts` split instead, so native keeps the 12px meta size it has always shown; the input and the label painted over it move together, or the address would resize on every focus. The address bar also gets inputMode="url" on the web only. keyboardType is a native enum a browser does not read, so the page's address bar was falling back to a plain keyboard; inputMode takes precedence over keyboardType, so it stays undefined on both native platforms. One consequence recorded rather than fixed, and pinned in config/scripts/mobile-web-app-browser-pane-text-inputs.test.mjs: the C4.2 census resolves an import through .ts/.tsx only, never .web.ts, so it reads the native address style that no browser loads and reports it as an offender. Whoever lists the pane's route either teaches resolveLocal the extensions the builder already prefers, or moves the address bar onto the seam natively at 14px. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): name C6.1's binary screencast grant (OTA phase C, C6.2) C6.1 has decided the name: `screencastBinary`, one camelCase token. Replaces the placeholder this PR landed with while C6.1 was still choosing. The placeholder was also unusable, which the test added here would have caught: `GRANT_NAME_PATTERN` in the manifest contract admits a bare name or a `native.`-prefixed verb and nothing else, so a route declaring `browser.screencast.binary` would have been refused by the bundle before any shell saw it, and the pane would have taken its stream-error branch for a reason no screen could report. The name is now checked against `MobileWebBundleRouteSchema` itself rather than against a restated regex, with the dotted spelling as the failing case beside it. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): keep the address field test inside the typecheck ratchet (OTA phase C, C6.4) `findByType('TextInput')` does not typecheck: a host-component string is not an `ElementType`, so the file fell out of `tsc -p tsconfig.test.json` and the tests-typecheck ratchet reported it. Found by reading the ratchet's exit code rather than its piped tail, which is how it was missed the first time. The element is looked up by its placeholder instead, and the ratchet is green with 732 test files in the program. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): answer a frame decode for the frame, not the layer (OTA phase C, C6.2) Round 1 folds on #21754. The undecodable arm freed the pending slot without checking whose frame had failed, while the displayable arm checked. Reproduced: frame 2 goes pending on layer 1, frame 3 repoints the same layer, frame 2's decode rejects and clears the slot layer 1 is holding for frame 3, then frame 3 decodes and the flip is refused because the slot no longer names its layer. The newest frame sits decoded at opacity 0 behind an older one, and a page that has gone still sends no further frame to recover with. Web only; native never calls this. Both arms now answer for the frame they were armed with. The displayable arm's own guard had no test: deleting it left `src/browser/` and the full suite green, because the case that exercised it settled both decodes and asserted an end state both orders produce. The harness now settles one decode at a time, keyed on the source it was given, and the ordered case reds without the guard. Also: the paint sibling's opacity test claimed "no re-render" while asserting two style strings, so it is named for what it checks and the claim is counted where React is — across ten streamed frames the three state setters are called once each, on the mount frame. And the overrides allowlist is rebuilt from main's bytes plus the new entries, so two pre-existing reasons keep their literal em dash instead of a re-serialized escape. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): find the address input by its imported type (OTA phase C, C6.4) `findByType('TextInput')` does not typecheck — a host-component string is not an `ElementType` — so the file dropped out of `tsc -p tsconfig.test.json` and the tests-typecheck ratchet reported it. The imported component is what the element is looked up by now; the react-native mock stands it up as that same string at runtime, so the lookup is unchanged and the file is back inside the ratchet's program. Why the earlier run reported 0: the command was `node scripts/check-tests-typecheck-ratchet.mjs 2>&1 | tail -2; echo $?`, and `$?` after a pipeline is the exit code of `tail`, which is always 0. The banner line that printed was the last line of the failure banner, not the success one. Every gate in this branch's report is now read from the command itself, unpiped. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(scripts): read a text input's size from the file the page loads (OTA phase C, C6.4) The census followed an import through `.ts`/`.tsx` only, while the closure it walks comes from esbuild, which prefers `.web.tsx`/`.web.ts`. So a style module with a platform sibling was judged on the half no browser loads. That fails in the direction that matters: a split whose web half sits under the focus-zoom floor reads as clean because its native half is on the seam, which is the exact shape the seam exists to catch. `resolveLocal` now tries the extensions in the builder's own order. The seam comparison collapses a resolved path onto its module identity before matching, because the seam is itself a split — `text-input-font-size.web.ts` is where the raise lives — and without that every binding in the tree would stop naming the seam: deleting it reds both C4 route closures. The browser pane's own census flips from pinning its address field as an offender to expecting none. The two C4 route closures still answer 0 offenders and 0 unresolved, run with the closure tests enabled. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): bound the frame envelope above every double it can carry (OTA phase C, C6.4) Round 1 folds on #21760. The envelope estimate serialized each metadata field as a 16-character double, so the bound was 435 where a real frame event at its widest measures 516. Since the budget spends whatever the cap leaves, that 81-byte shortfall was the whole margin: a frame on the budget would have gone over the cap and been dropped. The bound is now the skeleton plus the widest a double can print, for the nine metadata keys imported from the protocol module rather than copied, so a tenth field cannot be added without being paid for. Two corrections to the fold as written, both measured here. The widest is 25 characters, not 24. Exponential form tops out at 24 (`-1.7976931348623157e+308`), but ToString only leaves fixed notation below 1e-6, and just above it a double prints as sign, `0.`, five zeros and seventeen digits: `-0.0000012345678901234567`. A sweep over four million random bit patterns found 25 and nothing longer; a deterministic sweep over both forms is in the test. And the base64 expansion has to count padding. Three quarters of the room claims up to two characters base64 does not have for an image of 3k+1 bytes, which at a margin the budget now spends exactly is a dropped frame. The two agree at today's envelope size because the room happens to divide by four, so this is a latent fix rather than a reproduced one, and the test pins the discrepancy directly instead of implying it. Budget moves from 901,271 to 901,161 pixels; the phone's mobile view stays at a device scale factor of 1.80. Pinning the bound against C6.1's real encoder is C6.5's, once both are on main; the docstring says so, and says what the bound does not cover: the metadata object is loose, so unknown keys and web view mode's letterboxed frame are ruling 1's to drop rather than this budget's to predict. Also: the pane census listed its own closure by hand, so "no unresolved styles" said the walk read those files, not that they are the pane's set. It now scans `src/browser` for every non-test module that renders a `TextInput` and asserts the list matches; a third module planted there reds it. And the seam's native-consumer pin names the key row and the address bar directly rather than transitively, matched at the `fontSize` property instead of anywhere in the file — a file-wide search survives the change, because the import line does. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): check the frame write against a real react-native-web Image (OTA phase C, C6.2) Round 2 folds on #21754. Every test for the web paint sibling handed it a `div > div` of its own making, so the assumption it rests on — that the host's first element child is the one carrying the frame — was only ever checked against a shape written to match it. React Native Web also renders an accessibility `<img>` in there, and a release that reorders those children would keep all of them green while the pane painted nothing. One test now renders the real component, asks it which child it painted, and checks the write lands on that one. Pointing the sibling at `lastElementChild` reds it and leaves the hand-built cases passing, which is the gap. A second case records what the `<img>` does: the streaming path writes styles and never props, so it keeps the source it mounted with for the life of the pane, and that is what a screen reader and the image context menu see. react-native-web ships no type declarations, so the component comes through `createRequire`, whose return is `any` at its own signature; the one prop it renders with is declared rather than asserted, and the file stays inside the tests-typecheck ratchet. The module docstring also claimed more than the code does. The frame path adds no render, but a render from any of the pane's other state — address focus, a dialog, the view mode, zoom — repaints both layers from `renderedFrameSource`, which reads `frameUriRef.current`, so both land on the newest frame whether or not it has decoded. Native clobbers the same way through `setNativeProps`. Said plainly, along with what restores the buffering. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * docs(mobile): say what a render does to the accessibility image (OTA phase C, C6.2) pullfrog is right, and the test carried the same wrong claim. The note said the hidden `<img>` keeps the frame it mounted with for the life of the pane, two paragraphs after saying a render from the pane's other state passes `renderedFrameSource` as `source` — and React Native Web derives that image's `src` from the same prop it paints the background from, so the first such render moves it. Measured here rather than reasoned about: rendering the real component, writing a frame imperatively, then re-rendering with a new source moves the `src` and leaves the background where the imperative write put it. The two halves are now two cases, named for what each one shows, and the note says the streaming writes never touch it while a render does — so it holds the frame the pane last rendered with, not the one on screen. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb