mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 16:02:32 +00:00
* feat(mobile): give the hybrid shell switches a third answer for the unresolved flag Every route switch read the flag as `enabled !== true`, which spends the window before the read settles on the native screen. With the flag on that window costs a full native mount — subscriptions opened, screen painted — that the shell then tears down and replaces. `shellSwitchDecision` answers `pending` there instead, and `ShellSwitchPendingScreen` is what a switch paints while it waits: the base background with nothing on it, lifted out of the `web` route where this view already was rather than written again. A route the shell could never open is still answered `native` with no wait, because the flag cannot change that outcome and a neutral frame in front of a decided one is the flash this removes. No switch is wired to it yet. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): hold every hybrid shell switch on the neutral state while the flag is null All ten switches read the flag as `enabled !== true`, so the window before the storage read settles was spent on the native renderer. With the flag on that window costs a full native mount — the session screen opens its terminal, chat and tab subscriptions — which the shell then tears down and replaces, and the user sees the native screen flash before the page. Each now asks `useShellSwitchDecision` and paints `ShellSwitchPendingScreen` while the answer is `pending`, so exactly one renderer mounts and it mounts once. `tasks` and `agent-history` build their route before the decision rather than after it, because the decision needs to know whether the shell is a possible outcome at all. `web` already had this frame inline and now takes the shared one; its spinner's accessible name moves from "Checking host" to "Loading". The two cases that pinned the old behaviour — "renders the native panel while the flag read is still settling" on the files and agent-history routes — now assert that neither renderer mounts there. `shell-switch-null-flag.test.tsx` drives all nine switched routes through the three states and counts committed mounts rather than renders. Five route tests gain a `react-native` mock, which the neutral screen's `View` is the first thing in their graphs to need. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): fence the neutral state across every hybrid shell switch Reading the flag through the shared decision is not on its own enough. A tenth switch could ask `useShellSwitchDecision`, ignore `pending` and fall through to its native screen, satisfying the reader rule and still flashing native in front of a flag-on user. So the census also says every switch names the neutral screen — existence, not shape; where it names it is the route tests' business. `matchesOf` is the snippet reader beside the three rules: the needles are identifiers, and a list of paths says which file moved but nothing about what in it did. Verified as a fence by deleting the `pending` branch from the tasks switch, which the rule caught and named. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): measure what the neutral window costs a released phone The only thing this change costs a user with the flag off is the window itself, so it is worth a number rather than a claim. `loadMobileWebShellEnabled` answers `false` outside `__DEV__` before it looks at the key, so a release build reaches AsyncStorage zero times: the window is React's own passive-effect flush and one microtask, not a bridge round trip, and the switch has its answer on the first turn after the first commit. Pinned per switch, because a reader that grew a storage call would move it from a microtask to a bridge hop. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): type the switch table so the tests-typecheck ratchet reads it `as const` on the table made the catch-all's `page` a readonly tuple, which `useLocalSearchParams`' own param type does not admit, and the file dropped out of `tsconfig.test.json`. An explicit element type says the same thing and checks. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): keep release builds out of the neutral state entirely Review round 1 (pullfrog). The hook started at `null` on every build, so a store build committed one neutral frame before its native renderer — the one cost this change landed on every released phone, and it bought nothing there, because the flag cannot be turned on outside `__DEV__`. `mobileWebShellFlagCanBeOn` names that build-kind test once, beside the reader that already made it, and the hook starts its state on the answer. Outside `__DEV__` the hook holds `false` from its first render, the `pending` branch is unreachable, and a store build commits native on frame one. The effect still runs and still answers `false`; the initialiser is a starting point, not a second read path. Red-first, both build kinds pinned rather than inherited from the runner: 18 of 54 cases failed — "commits native on its first frame" and "does the same when the bundler defined no `__DEV__` at all", nine switches each. The neutral screen is mocked with a mount counter now, because a frame committed and replaced inside one `act` leaves nothing in the tree; its shape stays pinned in the web route's test, which renders the real one. The census gains the build-kind fence as a third rule. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb