mirror of
https://github.com/stablyai/orca.git
synced 2026-10-02 16:02:15 +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
43 lines
1.9 KiB
TypeScript
43 lines
1.9 KiB
TypeScript
import type { BridgeInitRoute } from './bridge/bridge-init-route'
|
|
import { useMobileWebShellEnabled } from './use-mobile-web-shell-enabled'
|
|
|
|
/**
|
|
* Which renderer a hybrid-shell route switch mounts, once that is knowable.
|
|
*
|
|
* `pending` is the third answer every switch was missing. Where the flag can be on it is read from
|
|
* storage after the first render, so `null` is a window every switch passes through, and each one
|
|
* used to spend it on the native screen: a flag-on user watched the native screen mount, subscribe
|
|
* and paint, then be torn down and replaced by the page. One decision here so a switch cannot hold
|
|
* a private opinion about `null`, and so the flag keeps exactly one reader — which is what makes
|
|
* the census beside this file total rather than a list somebody remembers to extend.
|
|
*
|
|
* `pending` is unreachable on a build that cannot have the flag on: the hook starts at `false`
|
|
* there, so a store build commits its native renderer on frame one and pays nothing for a neutral
|
|
* state it could never have used.
|
|
*
|
|
* A route the shell could never open is answered `native` without waiting: the flag cannot change
|
|
* that outcome, and a neutral frame in front of a decided one is a flash this file exists to remove.
|
|
*/
|
|
export type ShellSwitchDecision =
|
|
| { readonly kind: 'pending' }
|
|
| { readonly kind: 'native' }
|
|
| { readonly kind: 'shell'; readonly route: BridgeInitRoute }
|
|
|
|
export function shellSwitchDecision(
|
|
enabled: boolean | null,
|
|
route: BridgeInitRoute | null
|
|
): ShellSwitchDecision {
|
|
if (route === null) {
|
|
return { kind: 'native' }
|
|
}
|
|
if (enabled === null) {
|
|
return { kind: 'pending' }
|
|
}
|
|
return enabled ? { kind: 'shell', route } : { kind: 'native' }
|
|
}
|
|
|
|
/** The route is `null` when this switch's params name no screen the shell could open. */
|
|
export function useShellSwitchDecision(route: BridgeInitRoute | null): ShellSwitchDecision {
|
|
return shellSwitchDecision(useMobileWebShellEnabled(), route)
|
|
}
|