Commit Graph
123 Commits
Author SHA1 Message Date
Kelvin Amoaba c5957e7d4d fix(mobile): notice a silent drop while dictating within seconds (#35834)
An overdue chunk reply now asks the connection for one decisive liveness
probe instead of waiting for the idle watchdog, so the resend lands inside
the host's grace. Native clients probe directly; the OTA page asks the
shell with a new notify that older shells refuse with a diagnostic only.
2026-10-29 14:32:59 +00:00
OrcaWinandOrcaWin 1a03d59ea4 fix(mobile): release retired route storage mirrors (#35586)
* fix(mobile): release retired route storage mirrors

* fix(mobile): keep route hydration through unrelated writes and route changes

Stamp writes per owned key so an unrelated write no longer voids a cold
route's hydration into host-profile-unreadable, and retain the next
route's keys before releasing the previous route's so shared host-wide
keys stay mirrored.

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-10-28 21:15:44 -07:00
OrcaWinandOrcaWin ccd0829e6f feat(mobile): localize app screens, settings, pairing and host lists (#35498)
Moves ~600 strings in the root screens, settings, pairing, onboarding, accounts,
agent history, diagnostics, connection status, home and host workspace lists to the
translation API, with all ten translations.

- Connection verdicts carry an explicit `dialing` flag instead of matching the
  "…" ending of an English label
- Update-failure copy and connection-path accessibility labels are built from data,
  not by editing translated text
- Workspace status names stay the host's labels, as on desktop; transport errors
  stay English
- The stale-key check keeps a plural family while any of its forms is referenced

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-10-28 18:03:37 -07:00
OrcaWinandOrcaWin 12a7d4c13a feat(mobile): localization infrastructure and language setting (#35399)
* feat(mobile): add UI localization infrastructure

Mobile follows the phone's language (or an in-app choice under Settings > Appearance)
across the same 11 languages as desktop, switching live like the theme does.

- i18next with desktop's translate(key, fallback, options) call shape; catalogs in
  mobile/src/i18n/locales load lazily, English fallbacks stay inline
- Hermes lacks Intl.PluralRules: load the FormatJS polyfill with the 11 languages' rules
- OTA pages get the shell's language through an optional bridge init field
- iOS permission prompts localized; iOS and Android 13+ per-app language pickers
- verify:mobile-localization and a coverage ratchet run in lint, check:changed and CI

* fix(mobile): mount pages after the newest language request; tighten localization gates

- Wait for the latest page language activation, not a superseded one
- Keep literal fallbacks when a key also has a dynamic one
- Count headerTitle as visible copy; select the mobile check when the runtime catalog generator changes

* feat(mobile): repaint themed components on language changes

Components that read the theme already subscribe for repaint; a language switch
repaints them too, so labels built by helpers during render follow it. RPC
recordings load the real i18next and JSON catalogs.

* fix(mobile): scope the translate() gate to render bodies and memos; prune stale keys on sync

Handlers and effects may call translate() when they run. sync:mobile-localization now
removes keys no code references from every catalog.

* fix(mobile): use node: imports in the locale config plugin; pass uiLocale in bridge hook test

* chore(mobile): refresh the localization coverage baseline

* fix(mobile): follow render callbacks in the translate() gate; keep used plural siblings on prune

Also let anonymous default-exported screens count as components, flag any plural
family a locale started, and mark JSON modules as ES modules in the recording loader.

* fix(mobile): treat async and named local callbacks as handlers in the translate() gate

* chore(mobile): refresh the localization coverage baseline

* chore(mobile): refresh the localization coverage baseline

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-10-28 17:14:05 -07:00
Kelvin Amoaba e5b45ac90f fix(mobile): keep dictation recording through a reconnect and keep what was already said (#35338)
* fix(mobile): keep dictation recording through a reconnect

The host tied a phone's dictation to its socket, so any reconnect cancelled
it, and the phone ended the recording as soon as the connection dropped.
The host now owns the session by device and waits a grace period for the
phone to come back. The phone keeps recording through the reconnect and
keeps the text transcribed so far if the connection does not return.

Fixes #35211

* fix(mobile): bound dictation reconnect waits and release retired relay waiters

* fix(mobile): resend an unsent dictation finish after a connection move and send the text so far only when it changes

* fix(mobile): hold dictation requests through a suspended relay and bound the start wait

* fix(mobile): recognise a suspended relay client across the page bridge

* refactor(dictation): refresh one abandon timer, report text by version and share the reconnect wait

* fix(mobile): resend dictation text each second and never wait offline for a cancel
2026-10-28 22:27:21 +00:00
NeilandKelvin Amoaba 64344142fa fix(mobile): stop held terminal keys when you switch terminals or leave the app (#31712)
* fix(mobile): restore lifecycle-safe terminal accessory repeat

The selection hook replaced the established repeat controller with unbounded timers, allowing held keys to cross terminal/client changes. Reintegrate the existing pinned sender, lifecycle cancellation and backpressure.

* test(mobile): stop held terminal input after refusal or disconnect

* test(mobile): give accessory-repeat harnesses the PTY modes and focus hook main now reads

* fix(mobile): stop a held key when its strip unmounts and repeat through the current live-input mode

* fix(mobile): type the accessory repeat sender's input

* refactor(mobile): read app state live in the accessory repeat gate

* test(mobile): pin the key bar's held press to the repeat controller's first send

---------

Co-authored-by: Kelvin Amoaba <97001695+AmoabaKelvin@users.noreply.github.com>
2026-10-28 22:17:20 +00:00
OrcaWinandOrcaWin 7d29cd0839 fix(mobile): recover host profile reads without discarding cached bundles (#34806)
* fix(mobile): retry unavailable host profiles without discarding cache

* fix(mobile): cancel startup work on terminal shell failures via flow advance

Merging main pushed use-mobile-web-shell-session.ts over max-lines; move
the host-profile cancellation into the reducer so the flow-change cancel
path from #34780 handles it.

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-10-28 12:48:17 -07:00
OrcaWinandOrcaWin e0420b3b58 fix(mobile): skip queued generation stages after cancellation (#34809)
* fix(mobile): skip queued generation stages after cancellation

* test(mobile): fold canceled queued-stage case into download suite

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-10-28 12:48:11 -07:00
OrcaWinandOrcaWin d4b6e928ac fix(mobile): index reusable bundle assets once per generation (#34789)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-10-28 12:48:05 -07:00
OrcaWinandOrcaWin 6d5db9a397 fix(mobile): cancel superseded shell download flows (#34780)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-10-27 23:40:40 -07:00
OrcaWinandOrcaWin 4270c886d8 fix(mobile): release native picks after page teardown (#34760)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-10-27 23:18:36 -07:00
OrcaWinandOrcaWin 1ac9be7605 fix(mobile): let native screens handle Android Back after handoff (#34766)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-10-27 23:18:31 -07:00
OrcaWinandOrcaWin 9c094e16d2 fix(mobile): share microphone ownership across page sessions (#34762)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-10-27 23:18:27 -07:00
OrcaWinandOrcaWin d5dcc2cdf7 perf(mobile): keep terminal backlog draining linear (#34741)
* perf(mobile): drain terminal backlog without repeated array shifts

* test(mobile): cover backlog reuse after mid-drain dispose

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-10-27 23:06:29 -07:00
Kelvin Amoaba 941f4ece37 feat(mobile): support the iPhone Duo folded and unfolded, with controls in its side status bar (#34484) 2026-10-27 22:16:33 +00:00
Kelvin Amoabaandzhangxaochen 2a9d874e67 fix(mobile): leave Agent Session History when you resume a session from it (#34485)
* fix(mobile): leave Agent Session History when resuming a session from it

Resuming always pushed the session screen, so Back from it landed on the
history screen again (A -> B -> A). History now learns it was opened from a
session (origin=session, as Source Control does): resuming into that same
workspace pops back to it, anything else replaces the history screen.

The history screen is a page in the shell, and the page could only ask the
shell to push. The navigate notify gains an optional replace flag; a page with
no in-page history asks the shell to replace its screen, and one with history
below pops its own entry and has the shell push. Older shells strip the flag
and push as before.

Co-authored-by: zhangxaochen <3416677+zhangxaochen@users.noreply.github.com>

* fix(mobile): show the resumed terminal on the session that opened Agent Session History

Going back to the session screen below kept its old tab: the session pins its
own pick over the host's caller-scoped snapshot. History now hands the resumed
terminal to that session through the registry Source Control already uses for
opened diffs, renamed to session-screen-handlers since it carries both.

* refactor(mobile): positional replace flag on notifyNavigate, share the owned-session test fixture

* fix(mobile): keep MobileWebShellScreen under its line cap

---------

Co-authored-by: zhangxaochen <3416677+zhangxaochen@users.noreply.github.com>
2026-10-27 17:58:49 +00:00
Kelvin Amoaba f87a746200 feat(mobile): light and dark themes with desktop's palettes (#34217)
* feat(mobile): light and dark themes with desktop's palettes

Settings → Appearance offers System, Light and Dark. The shell passes the
resolved scheme to over-the-air pages in its init frame.

Fixes #22184

* fix(mobile): draw your chat messages in desktop's user bubble

Desktop uses a faint surface with a hairline border in both themes; the inverted fill went black in light.

* test(mobile): re-measure the page script sweep after the shared theme hooks

Every route now imports the theme hooks while the entry does not, so they emit one shared script (96 for the tree). Re-swept every route prefix; the worst route step is now 15.

* test(mobile): read themed stylesheets in the text-input font-size census

The census only unwrapped StyleSheet.create({...}); inputs styled through themedStyleSheet(() => ({...})) came back unread.
2026-10-27 17:35:31 +00:00
Kelvin Amoaba 86f2f24ddc feat(mobile): open PDFs from the file browser in Quick Look or the system viewer (#33771) 2026-10-26 22:26:58 +00:00
OrcaWinandOrcaWin de93f326ea test: remove low-value inventories, passthroughs and redundant cases (#33796)
* test: remove low-value inventories, passthroughs and redundant cases

* style: format test suites after pruning redundant cases

* test: remove obsolete workflow and flaky-test references

* test: prune deleted builder test from Windows shim allowlist

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-10-25 23:41:49 -07:00
OrcaWinandOrcaWin 7d6603aa17 [comment audit] Complete repository-wide comment cleanup (#33249)
* [comment audit] Align legacy failure-read counts with cached display

* [comment audit] Condense shared provider lifecycle and browser recovery comments [skip ci]

* [comment audit] Condense shared generation, path and preview comments [skip ci]

* [comment audit] Condense host, file and Git contract rationale [skip ci]

* [comment audit] Tighten Git lifecycle and provider contract comments [skip ci]

* [comment audit] Clarify shared host authority and launch isolation [skip ci]

* [comment audit] simplify local access mobile and native chat contracts [skip ci]

* [comment audit] simplify native chat presentation and state contracts [skip ci]

* [comment audit] simplify native runtime and provider contracts [skip ci]

* [comment audit] simplify migration and orchestration contracts [skip ci]

* [comment audit] simplify plugin trust and process capture rationale [skip ci]

* [comment audit] simplify terminal delivery and remote compatibility rationale [skip ci]

* [comment audit] clarify remote recovery and launch wire rationale [skip ci]

* [comment audit] shorten shared RPC compatibility and runtime evidence comments [skip ci]

* [comment audit] simplify secure storage shell startup and sidebar rationale [skip ci]

* [comment audit] clarify sidebar, skill and SSH invariants [skip ci]

* [comment audit] condense structured chat delivery and timing rationale [skip ci]

* [comment audit] condense terminal replay and telemetry invariants [skip ci]

* [comment audit] clarify launch permission and terminal delivery rationale [skip ci]

* [comment audit] clarify workspace ownership and layout contracts [skip ci]

* [comment audit] wait for fitted and painted SSH terminal links

* [comment audit] clarify layout persistence and session ownership [skip ci]

* [comment audit] clarify host routing and workspace lifecycle contracts [skip ci]

* [comment audit] clarify startup persistence and app lifetime comments [skip ci]

* [comment audit] clarify recovery and activity boundaries [skip ci]

* [comment audit] condense activity and artifact invariants [skip ci]

* [comment audit] condense automation authority contracts [skip ci]

* [comment audit] simplify automation scheduling and history contracts [skip ci]

* [comment audit] clarify browser chrome and annotation lifecycles [skip ci]

* [comment audit] clarify browser routing and retained guest ownership [skip ci]

* [comment audit] clarify browser recovery and document ownership comments [skip ci]

* [comment audit] restore hosted runners required by native IME fixtures

* [comment audit] shorten preview, palette and dashboard invariants [skip ci]

* [comment audit] clarify terminal preview, dashboard and retention comments [skip ci]

* [comment audit] clarify diff-note recovery and editor lifecycle comments [skip ci]

* [comment audit] shorten rich-text and combined-diff ownership comments [skip ci]

* [comment audit] shorten autosave, draft recovery and model ownership comments [skip ci]

* [comment audit] shorten save-queue, notebook and Markdown constraints [skip ci]

* [comment audit] shorten Monaco, PDF and rich Markdown safeguards [skip ci]

* [comment audit] clarify rich Markdown source and selection safeguards [skip ci]

* [comment audit] clarify editor loading and emulator stream lifetimes [skip ci]

* [comment audit] trim emulator and feature tour narration [skip ci]

* [comment audit] clarify floating workspace and GitHub cache ownership [skip ci]

* [comment audit] trim provider controls and Linear filter explanations [skip ci]

* [comment audit] clarify pairing and chat resume lifetimes [skip ci]

* [comment audit] clarify chat delivery and transcript lifetimes [skip ci]

* [comment audit] clarify session options and host-owned status [skip ci]

* [comment audit] clarify draft durability and live-status precedence [skip ci]

* [comment audit] Trim chat send and model ownership explanations [skip ci]

* [comment audit] Shorten transcript ownership and send recovery comments [skip ci]

* [comment audit] Simplify native chat composer and transcript hook comments [skip ci]

* [comment audit] Shorten native chat scroll and structured session explanations [skip ci]

* [comment audit] Trim workspace picker and onboarding commentary [skip ci]

* [comment audit] Simplify pet rendering, review cache and quick open comments [skip ci]

* [comment audit] Trim sidebar and session history explanations [skip ci]

* [comment audit] Condense session resume and review checks comments [skip ci]

* [comment audit] Shorten checks state and file explorer refresh comments [skip ci]

* [comment audit] Trim plugin bridge and source-control action rationale [skip ci]

* [comment audit] Trim source-control listing and review rationale [skip ci]

* [comment audit] Shorten sidebar lifecycle and explorer safeguards [skip ci]

* [comment audit] Condense Settings account and appearance rationale [skip ci]

* [comment audit] Trim pairing and repository Settings explanations [skip ci]

* [comment audit] Condense Settings search SSH and terminal safeguards [skip ci]

* [comment audit] Shorten Settings voice and provider ownership comments [skip ci]

* [comment audit] Condense Settings draft persistence and host-state rationale [skip ci]

* [comment audit] Trim Settings hooks and sidebar setup rationale [skip ci]

* [comment audit] Condense sidebar deletion and board-filter safeguards [skip ci]

* [comment audit] Condense worktree cards, host ownership and Markdown contracts [skip ci]

* [comment audit] Condense sidebar drag, folder and filtering contracts [skip ci]

* [comment audit] Condense sidebar parity, sleep and host ownership contracts [skip ci]

* [comment audit] Prune focus-read baseline after comment cleanup

* [comment audit] Clarify sidebar visibility, board drops and agent evidence [skip ci]

* [comment audit] Tighten sidebar listing, drag and reveal contracts [skip ci]

* [comment audit] Clarify viewport geometry and skill update safeguards [skip ci]

* [comment audit] Tighten skill inventory, usage and host status comments [skip ci]

* [comment audit] Clarify usage ownership and resource kill safeguards [skip ci]

* [comment audit] Tighten tab selection, rename and search contracts [skip ci]

* [comment audit] Tighten tab-strip geometry and drag ownership comments [skip ci]

* [comment audit] Tighten Tasks recovery, pagination and mutation comments [skip ci]

* [comment audit] Tighten provider loading and terminal recovery comments [skip ci]

* [comment audit] Tighten completion, cadence and PTY ownership comments [skip ci]

* [comment audit] Clarify terminal restore and parked-pane boundaries [skip ci]

* [comment audit] Tighten PTY startup and replay explanations [skip ci]

* [comment audit] Clarify PTY identity and delivery ownership [skip ci]

* [comment audit] Shorten remote reconnect and input replay rationale [skip ci]

* [comment audit] Condense terminal parking clipboard and file link comments [skip ci]

* [comment audit] Clarify IME ownership and trim trace history [skip ci]

* [comment audit] Shorten pane recovery and parked watcher explanations [skip ci]

* [comment audit] Condense snapshot replay and visibility recovery rationale [skip ci]

* [comment audit] Shorten terminal parking and keyboard ownership rationale [skip ci]

* [comment audit] Trim renderer lifecycle and menu rationale [skip ci]

* [comment audit] Condense workspace cleanup ownership and settlement notes [skip ci]

* [comment audit] Clarify renderer hook evidence and source ownership [skip ci]

* [comment audit] Tighten IPC ownership and reconnect explanations [skip ci]

* [comment audit] Trim renderer focus, paste and attention comments [skip ci]

* [comment audit] Shorten scroll, locale and agent lifecycle comments [skip ci]

* [comment audit] Trim agent launch, resume and ownership commentary [skip ci]

* [comment audit] Shorten account, browser and editor comments [skip ci]

* [comment audit] Clarify host-mirror waits, previews and input ownership [skip ci]

* [comment audit] Condense launch recovery and editor tokenization comments [skip ci]

* [comment audit] Condense workspace selection and terminal pane rationale [skip ci]

* [comment audit] Condense terminal fitting and rendering recovery comments [skip ci]

* [comment audit] Condense terminal input and host routing explanations [skip ci]

* [comment audit] Condense ownership and sleeping-session recovery rationale [skip ci]

* [comment audit] Condense structured launch and terminal link explanations [skip ci]

* [comment audit] Condense terminal ownership and workspace file routing rationale [skip ci]

* [comment audit] Condense session persistence and workspace activation rationale [skip ci]

* [comment audit] Condense workspace routing and browser ownership rationale [skip ci]

* [comment audit] Condense mirror recovery and layout authority rationale [skip ci]

* [comment audit] Condense runtime transport and file safety rationale [skip ci]

* [comment audit] Condense runtime graph and structured session rationale [skip ci]

* [comment audit] Condense browser creation and remote tab recovery rationale [skip ci]

* [comment audit] Condense snapshot authority and publisher recovery rationale [skip ci]

* [comment audit] Condense frame, startup and provider cache rationale [skip ci]

* [comment audit] Condense host twin and agent lifecycle rationale [skip ci]

* [comment audit] Condense browser authority and editor cleanup rationale [skip ci]

* [comment audit] Condense editor buffer identity and review cache rationale [skip ci]

* [comment audit] Condense provider lifetime and repository ownership rationale [skip ci]

* [comment audit] Condense reconnect, hydration and terminal retirement rationale [skip ci]

* [comment audit] Condense recovery, reconciliation and persistence rationale [skip ci]

* [comment audit] Condense cleanup ownership and listing admission rationale [skip ci]

* [comment audit] Condense teardown ordering and terminal ownership rationale [skip ci]

* [comment audit] Condense recovery ledger and web host-boundary rationale [skip ci]

* [comment audit] Condense web reconnect and credential lifecycle rationale [skip ci]

* [comment audit] Condense cloud rollout and assignment safety rationale [skip ci]

* [comment audit] Condense relay session and database recovery rationale [skip ci]

* [comment audit] Condense relay placement and protocol compatibility rationale [skip ci]

* [comment audit] Condense build and packaging constraints [skip ci]

* [comment audit] Condense CI gates and validation isolation rationale [skip ci]

* [comment audit] Condense catalog identity and browser validation rationale [skip ci]

* [comment audit] Condense mobile browser and frame-budget rationale [skip ci]

* [comment audit] Shorten mobile route and browser test explanations [skip ci]

* [comment audit] Condense editor and session seam test rationale [skip ci]

* [comment audit] Trim session and terminal closure history [skip ci]

* [comment audit] Shorten terminal browser and census explanations [skip ci]

* [comment audit] Preserve root-only E2E diagnostic baseline

* [comment audit] Condense mobile seam and HTML preview test rationale [skip ci]

* [comment audit] Condense preview instrumentation and render harness rationale [skip ci]

* [comment audit] Condense bundle contracts and native-build constraints [skip ci]

* [comment audit] Shorten native packaging and CI routing rationale [skip ci]

* [comment audit] Condense release tooling and benchmark constraints [skip ci]

* [comment audit] Condense packaging and platform gate rationale [skip ci]

* [comment audit] Shorten mobile route and history rationale [skip ci]

* [comment audit] Condense mobile browser and component rationale [skip ci]

* [comment audit] Shorten drawer and workspace-form rationale [skip ci]

* [comment audit] Trim mobile review and editor-document explanations [skip ci]

* [comment audit] Condense editor lifecycle and Markdown explanations [skip ci]

* [comment audit] Trim mobile file, diagnostics and dictation commentary [skip ci]

* [comment audit] Simplify mobile host-list and workspace-view explanations [skip ci]

* [comment audit] Clarify mobile shell lifetime and bridge delivery constraints [skip ci]

* [comment audit] Condense bridge stream, native request and audio rationale [skip ci]

* [comment audit] Reduce bridge schema and page-client commentary [skip ci]

* [comment audit] Tighten mobile page bridge contracts [skip ci]

* [comment audit] Clarify mobile cache and session constraints [skip ci]

* [comment audit] Condense mobile route and lifecycle rationale [skip ci]

* [comment audit] Tighten mobile shell hook timing rationale [skip ci]

* [comment audit] Clarify route handoff and notification compatibility [skip ci]

* [comment audit] Clarify push registration and dictation cleanup constraints [skip ci]

* [comment audit] Condense keyboard geometry and native media ownership rationale [skip ci]

* [comment audit] Tighten mobile chat scroll and composer concurrency rationale [skip ci]

* [comment audit] Condense mobile reply compatibility and upload ownership rationale [skip ci]

* [comment audit] Condense optimistic echo and scroll ownership rationale [skip ci]

* [comment audit] Tighten stream replay and queued session ownership rationale [skip ci]

* [comment audit] Condense mobile composer and reply-schema constraints [skip ci]

* [comment audit] Clarify mobile transcript recovery and session ownership [skip ci]

* [comment audit] Clarify mobile queue and source-control compatibility [skip ci]

* [comment audit] Condense storage and task launch constraints [skip ci]

* [comment audit] Clarify task schemas and terminal document ownership [skip ci]

* [comment audit] Tighten terminal input and viewport rationale [skip ci]

* [comment audit] Shorten terminal and RPC recorder contracts [skip ci]

* [comment audit] Condense recorder engine and connection contracts [skip ci]

* [comment audit] Shorten host credential and relay recovery comments [skip ci]

* [comment audit] Tighten pairing recovery and bundle transport rationale [skip ci]

* [comment audit] Clarify RPC delivery and workspace catalog boundaries [skip ci]

* [comment audit] Condense catalog and relay entry contracts [skip ci]

* [comment audit] Shorten shutdown and SSH relay delivery contracts [skip ci]

* [comment audit] Condense WSL relay and E2E regression rationale [skip ci]

* [comment audit] Shorten cross-version harness and renderer fixture explanations [skip ci]

* [comment audit] Condense paired-host and E2E lifecycle rationale [skip ci]

* [comment audit] Shorten layout oracle and regression explanations [skip ci]

* [comment audit] Condense paired recovery and ownership regression contracts [skip ci]

* [comment audit] Shorten restart, sidebar and SSH lifecycle contracts [skip ci]

* [comment audit] Condense terminal rendering and IME regression rationale [skip ci]

* [comment audit] Shorten terminal recovery and benchmark contracts [skip ci]

* [comment audit] Condense tooling and Windows lifecycle proof rationale [skip ci]

* [comment audit] Condense workflow routing and release rationale [skip ci]

* [comment audit] Condense mobile security and installer contracts [skip ci]

* [comment audit] Shorten native rationale and stylesheet workarounds [skip ci]

* [comment audit] Shorten Windows visibility probe rationale [skip ci]

* [comment audit] Condense embedded release and signing rationale [skip ci]

* [comment audit] Retain current main test behavior in final reconciliation [skip ci]

* [comment audit] Complete final comment review across current source inventory

* [comment audit] Handle consolidated audit reports before final CI

* [comment audit] Preserve credential storage census detail [skip ci]

* [comment audit] Scale broad changed E2E selections with verified timing shards

* [comment audit] Align E2E assertions with browser close and settled session metadata

* [comment audit] Match existing E2Es to current resource and windowing contracts

* [comment audit] Observe the shared server outage notice in reconnect coverage

* [comment audit] Make existing renderer probes measure settled tab and PTY work

* [comment audit] Align palette E2E selector with task search

* [comment audit] Refresh remaining palette search selectors

* [comment audit] Keep selector correction within selected audit specs

* [comment audit] Await emoji fixture settings inside renderer evaluation

* [comment audit] Cancel fixture fit retries between terminal tests

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-10-25 04:05:46 -07:00
Neil cc2b6fe6a2 build(mobile): move to TypeScript 7 and Vitest 5, typecheck through the root tsrs runner (#33419)
- Bump mobile typescript to ^7.0.2 and vitest to 5.0.5; keep TypeScript 6 as typescript-api
  for the test-support code that walks the AST, mirroring the root.
- Patch Expo CLI's tsconfig reader to fall back to typescript-api: TypeScript 7 has no
  compiler API, and Metro otherwise crashes reading tsconfig paths.
- Register mobile/tsconfig.json and mobile/tsconfig.test.json with the root typecheck runner;
  locally they use incremental tsrs (Bun elsewhere), CI uses Bun like root CI.
- Route the tests-typecheck ratchet through the same compiler selection.
- Enforce root/mobile parity for typescript, typescript-api and vitest.
2026-10-24 23:09:20 -07:00
OrcaWinandOrcaWin 01652255f0 test: strengthen hook, build, and mobile lifecycle coverage (#32968)
* test: strengthen hook, build, and mobile lifecycle coverage

* fix: use relocated Jira types in issue user field

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-10-23 16:38:53 -07:00
OrcaWinandOrcaWin 6882870850 test: strengthen hook delivery, CLI and mobile shell coverage (audit 9) (#32876)
* test: audit CLI environment, Jira and orchestration coverage

* test: audit project setup routing and refusal coverage

* test: audit hook delivery, config gates and mobile shell coverage

* docs: retain a reachable audit tracker source commit

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-10-23 12:04:06 -07:00
OrcaWinandOrcaWin d91a3d5804 test: strengthen CLI, lifecycle, bundle and build coverage (audit 8) (#32777)
* test: audit repository visibility and session-search CLI coverage

* test: verify service identities and public skill-sharing output

* test: verify terminal retry commands and unsuccessful navigation attempts

* test: exercise bulk-close refusal and printed legacy prompt receipts

* test: audit host discovery and agent launch recovery identities

* test: audit ACP replay, lifecycle, and hook delivery coverage

* test: audit automation errors and build artifact guards

* test: audit mobile bridge and bundle lifecycle coverage

* docs: reconcile audit tracker with input-ownership changes

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-10-23 11:06:37 -07:00
OrcaWinandOrcaWin 633da07af5 test: audit CLI dispatch and mobile host boundaries (#32576)
* test: verify independent CLI quoting and screenshot bytes

* test: audit CLI registration and offline hook boundaries

* test: record CLI automation fence review

* test: audit computer CLI dispatch and exported files

* test: record CLI enrollment and emulator contract review

* test: audit file command path and navigation contracts

* test: bracket login shutdown grace and fallback deadlines

* test: keep managed server timeout expectations independent

* test: record Linear CLI input and dispatch review

* test: verify legacy completion dispatch boundary

* test: replace copied config and browser controls

* test: isolate gate refusal from inherited caller identity

* test: exercise real mobile host admission and teardown boundaries

* docs: reconcile audit entrypoints and refresh completed reviews

* test: fix mobile audit React Doctor findings

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-10-23 07:34:51 -07:00
Jinwoo Hong 385418e0db fix(mobile): the shell returns to a screen already on its stack (#32110)
* fix(mobile): the shell returns to a screen already on its stack

The page's own "back to hosts" chevron calls dismissTo('/') inside the page, which the page's
router hands to the shell as one navigate message. The shell pushed every such href, so leaving
a host from its page left [Home, Host, Home] with the old host page still mounted (WebView and
live subscriptions), and every round trip stacked another one. System Back then walked back
through stale host pages.

The shell now reads the navigation container's state first: an href that is already a screen
below this one (same path and query) is popped back to, and an href that is this screen is
dropped. Anything else is pushed as before. The pop names its navigator and source route
explicitly, because a screen's dispatch otherwise stamps its own key as the source and a parent
stack that cannot find it drops the pop.

The wide host-route dismissTo stays for the hrefs this rule does not cover: a host route whose
query differs from the one below (the missing-worktree bounce), and a cold deep link with no host
route below to return to. Overlay replace is unchanged, and so is the bridge message.

* fix(mobile): a param cleared to empty and a fragment still match a screen on the stack

The session screen clears paneKey with setParams({ paneKey: '' }), which leaves ?paneKey= on the
stack entry, so a later navigate to the plain session href found no match and pushed a duplicate.
Empty query values are now dropped when canonicalising, and the fragment is stripped since
expo-router keeps it as a param no href spells the same way.
2026-10-22 21:07:32 -04:00
Jinwoo Hong 5aa5467c27 feat(mobile): one download bar whose fill follows progress on the native driver (#31755)
* feat(mobile): one download bar whose fill follows progress on the native driver

The page download's replies keep the JS thread busy, so React committed
nothing until the end and both bars sat at zero, then jumped to full.
The session now publishes each progress step to a small feed as it is
dispatched, and one shared bar eases its fill to each value with
Animated on the native driver (scaleX). The caption stays React-drawn.

* fix(mobile): the download bar always moves on the native driver

With reduced motion, the fill jumped through a JS-side setValue on a value
that never became native, so on Fabric it only moved with React commits,
which the download holds off. An unread preference also counted as
reduced, so a freshly mounted bar took that path. Every step is now a
native-driven timing, of zero length when motion is reduced, and the
preference is read through a ref.

* fix(mobile): the download bar starts at most one ease per 50 ms

The feed fires per asset, often well under a millisecond apart. Starting a
new 150 ms timing on each one replaced every ease before it drew a frame,
so on iOS the fill stayed near empty until the last update.

Now a callback inside the 50 ms window only records the latest fraction;
the next callback past the window, or a trailing timer at its end, eases
to it over 100 ms. Full lands at once. The effect cleanup clears the timer.
2026-10-22 21:03:28 -04:00
Jinwoo Hong ccc520ab09 feat(mobile): download only the page assets the cached generation lacks (#31769)
* feat(mobile): download only the page assets the cached generation lacks

Before paging the bundle, the phone matches the new manifest against the
host's active generation by content (sha256 and length). Matches are read
from disk and hashed again while the changed assets come over the wire;
one whose bytes no longer hash to the manifest is downloaded instead.
Progress counts only the bytes that cross the wire.

* test(mobile): put the older-build manifest note back on its describe

* perf(mobile): copy unchanged page assets natively instead of reading and hashing them

The reuse pass read every unchanged asset into JS and hashed it again,
which kept the JS thread busy for most of an update. The cached
generation was hash-verified when it was written, is never written
again, and is what the page already opens as is, so matching assets are
now copied natively into the staging tree and checked by size. A copy
that fails or comes up short is downloaded instead; stage refuses a
reused copy that is no longer whole. Path rules move to
generation-asset-path.ts and the copying to generation-reuse.ts.

* fix(mobile): copy reused page assets at stage, on the store's queue

Copies made when the manifest was read sat in tmp for the whole download,
where a second shell mount's sweep, or a second download of the same
build, could take them away and fail the stage. The fetch now only checks
that each reusable file is still whole (a native stat) and leaves it to
the stage, which copies it from the generation active at that moment and
checks its size. Copy destinations must not exist, as on both platforms.
2026-10-22 20:59:14 -04:00
Neil 42da9f430f refactor(mobile): group platform-seam analysis tools (#31845) 2026-10-21 23:15:21 -07:00
Jinwoo Hong f47f632bd7 feat(mobile): Home's New Workspace opens the desktop's create drawer over Home (native part ships with the next app build) (#31305)
* fix(mobile): the home screen's New Workspace opens the create sheet again

The native host-list route dropped the home screen's action=newWorktree when it built
the page's route, so the page opened as a plain list. The route now forwards it, and
the page names it back once applied so a reload does not reopen the sheet.

Reaches phones only with the next native build.

* test(mobile): drop the New Workspace tests that only re-check types and a null guard

* fix(mobile): Home's New Workspace opens the create sheet over Home

Replaces the action=newWorktree route hand-off. The quick action picks a
host (as Tasks does), then mounts the host's create sheet on Home; a
create opens the new workspace's session marked as just created.

The sheet controller becomes a plain visible/onClose component, and the
host screen's route-action state, the action route param and its builder
are deleted.

* fix(mobile): Home's create sheet seeds only proven worktree rows

Home's cold-start seed is a pathless resume row that the name suggester
cannot read, so the sheet now seeds from proven rows and is keyed by host.
The host screen gets one setter for the sheet's visibility and its ref.

* test(mobile): name the Home new-workspace test after the screen it renders

* proto(mobile): Home's New Workspace opens a native drawer hosting the page's create form

Home pushes /new-workspace/<host>, a react-native-screens formSheet over Home whose content is
MobileWebShellScreen for the page route /h/<host>/new-workspace. The page renders the shared
Create Workspace form with its drawers inline, so the form and its pickers fill the native
drawer. The shell's loading, download and failure states render inside the drawer. Prototype only.

* proto(mobile): the New Workspace drawer is the host screen's own sheet over a transparent modal

The native route is a transparentModal hosting the shell over Home; the page route renders the
host screen's NewWorktreeModal on a transparent page, so the drawer is the same component. While
the page loads, the shell shows a placeholder of that sheet (scrim, grabber, spinner or download
bar), and the page's sheet opens in place without its slide. A host that cannot serve the page
(old desktop, blocked, failed) opens the host list instead of a wall in a drawer.

* feat(mobile): Home's New Workspace opens the page's create drawer over a dimmed Home

A transparent native overlay route hosts the host picker and then the page's
/h/[hostId]/new-workspace route, which renders the same NewWorktreeModal as the
host screen's +. The native scrim matches the drawer backdrop and the page's
first drawer opens already dimmed, so Home stays dimmed throughout. Old desktops
without the route fall back to the host list.

* fix(mobile): one loading layer for Home's New Workspace overlay

The overlay route owns a single tappable loading layer (spinner, or the
download on a small card) that stays mounted from the pick until the page
paints, so the fade and spinner no longer restart per shell state. The shell
reports its load and shows the host list for anything it cannot open; the
picker hands the pick over at once and keeps sliding away. The page's first
drawer takes the dimmed backdrop once, and a gate's overlay pending view no
longer stacks a second scrim over a mounted stack. Native /h/[hostId]/new-workspace
redirects to the overlay. Drops the unused session-route params, the
clipboard grant, and pass-through tests.

* feat(mobile): New Workspace loads in a short drawer over an undimmed Home

While the page loads, the overlay shows one short native drawer in the shared
drawer chrome with a spinner or the download, and nothing dims Home: a tap
above it, a drag down or Back cancels. The page's real drawer then slides up
with its own backdrop exactly as on the host screen's +. Drops the native
scrim, the loading card and the already-dimmed backdrop handoff; the overlay's
WebView stays invisible until the page paints, since a fresh one draws opaque.

* feat(mobile): the New Workspace loading drawer dims Home like any drawer

The short loading drawer opens with the shared drawer backdrop and show
duration, and a tap on that backdrop, a drag down or Back cancels. It stays
in the screen's own window (a native Modal over the loading WebView stalls
the page) and goes in the frame the page paints: the shell reports its load
from a layout effect so the WebView shows and the drawer leaves together.

* fix(mobile): the page's first paint waits for a drawer opening with it

A drawer opening as its page screen mounts holds that screen's first-paint
report until its open animation ends, through a hold on the existing report
rather than a new bridge message. The New Workspace overlay's loading drawer
now stays until the create drawer is up, so a cold open no longer shows Home
undimmed between the two.

* refactor(mobile): the New Workspace overlay owns its exits; the loading drawer fades out

The shell no longer imports the feature: an overlay is a shell with
onOverlayLoad, which reports `unavailable` for what it cannot show, and the
overlay route opens the host list for every exit. HostProtocolGate takes one
transparentWhilePending flag. The loading drawer's drag handler follows a
stable cancel, and on paint the drawer fades out eased in, as the shell's page
cover does, because the WebView takes a frame or two to show the page after it
is revealed; removing it outright left Home undimmed for those frames.

* Revert "fix(mobile): the page's first paint waits for a drawer opening with it"

This reverts commit be0913b9d5.

* fix(mobile): Home's host picker leaves when there is no desktop to pick

An unreadable host catalog, or offered desktops that are all gone, left the
picker invisible inside the transparent overlay, holding a screen over Home
with nothing to dismiss it. The catalog read now resolves to no hosts on
failure, and an empty pick list goes back to Home.

* fix(mobile): Home's create drawer names SSH hosts as the host screen does

The page drawer opened from Home passed no host labels, so a project on an SSH
host showed its run target by raw host id. Reading the SSH target summaries
and display overrides moves into one function that both the host screen's
metadata fetch and the page drawer call.
2026-10-21 20:02:11 -04:00
Jinwoo Hong 92bbb380a6 Sidebar: land the switch-on fixes from the flip branch on main, switch still off (#31466)
* fix(sidebar): land flip-a's switch-on fixes on main, still dark (no flip)

The fixes found while the sidebar ran from main's feed, without turning
that on: ORCA_SIDEBAR_INPUTS_IS_MAIN stays false, and none of flip-a's
stop-gaps come along.

- Main stops counting a closed tab's terminal once it was asked to stop
  and no pane shows it; a failed or refused stop takes the request back,
  and a host out of contact stays unverifiable.
- Main fetches a paired server's rows with the transport's normal bound,
  so a server slower than 5 s still lists.
- A deferred unread raise flips the folder row it was decided for, and a
  read while the owner check is out cancels it.
- Phone: no worktree.ps flash before the feed; a host's last installed
  feed stands in while a fresh read is out; it highlights its own
  last-opened workspace and a fold never scrolls; projects file under
  their project group with the desktop's indent; the collapse spinner
  shows only after 500 ms; "Reopen to update" keeps the rows on screen.
- e2e helpers and journeys for the switch-on suite.

* fix(sidebar): keep an owed relay stop marked, clear a thrown stop, and prune pending unread raises

- A stop-and-wait that throws takes its stop request back, like one that fails.
- A final stop the legacy relay still owes stays marked, since the relay retries it.
- Pending unread raises are tracked only while one waits, so the map no longer grows.
- The paired-server fallback poll test gives its third fetch a reply and asserts live.
2026-10-21 12:24:27 -04:00
Neil 513c24bc16 fix(mobile): preserve unsent terminal commands across workspace navigation (#31314)
Persist unsent buffered terminal commands per paired host, workspace, execution host, and surface across navigation. Preserve newer typing during storage hydration and clear successfully sent drafts.

Validation: draft/storage/bridge tests, signed iPhone simulator build, and all CI checks passed.
2026-10-20 19:31:05 -07:00
Kelvin Amoaba cf7423c8f4 feat(mobile): redesign the chat composer with file attachments and inline skills (#30359)
* refactor(native-chat): move the slash picker builder and upload chunking to shared

Mobile builds the same skill and command list as desktop.

* feat(mobile): redesign the chat composer with file attachments and inline skills

* fix(mobile): the composer field owns its input style

The page's font-size census cannot follow a style passed in as a prop.
2026-10-18 20:03:26 +00:00
Neil aa07d5891a fix(mobile): break the transport and terminal-document import cycles, and gate cycles after mobile's install (#30290)
* fix(mobile): break the transport and terminal-document import cycles and gate them after mobile's install

client-context re-exported the host-client hooks, which import client-context back: the re-export
barrel is gone and importers (and their test mocks) name host-client-hooks directly. In the terminal
document, the cell-metric getters move to a leaf cell-metrics module and select-mode entry/exit to
selection-mode, so fit, transform, overlay and range no longer import each other. pr.yml lints imports
before mobile's install, when mobile's tsconfig can't load, so Mobile verify now runs the same import
rules over mobile after its install with zero warnings allowed.

* fix(mobile): the browser-pane render harness imports useHostClient from host-client-hooks

Its route source is a template string, so no typecheck saw the removed client-context re-export; the mobile web bundle check did.

* fix(ci): the e2e typecheck ratchet counts the same with or without mobile's install

Specs reach mobile/src/transport, whose expo-crypto and async-storage imports resolve only after mobile's install and then load React Native's globals (numeric timers, a required NODE_ENV), adding errors in unrelated specs. An empty stub pins both, so the static job reads 232 either way.
2026-10-18 04:00:24 -07:00
Jinwoo Hong 90c1c0ac50 feat(mobile): draw the host Accounts screen from the desktop's page (#30020)
* fix(mobile): wall hosts that cannot serve the page instead of opening native screens

* refactor(mobile): drop the native-route state and the shell fallback prop; unknown routes say unavailable

* test(mobile): read the nullable page route refusal in its policy test

* refactor(mobile): every build is the OTA shell; drop the native/OTA build switch

The owner retired the native-screen fallback with no emergency native build, so
EXPO_PUBLIC_MOBILE_SHELL, mobileShellBuildKind, shellSwitchDecision, the Metro
cache-key suffix and both release workflows' shell input go. Every host route
mounts the shell; a route naming no screen the shell could open gets the
existing refusal screen instead of a native screen.

Claude-Session: https://claude.ai/code/session_6cea92f6

* refactor(mobile): delete native screens the OTA shell no longer reaches

With every build the OTA shell, these native siblings run nowhere: the
native-executed closure (index.js plus every non-web route) does not reach
them, and the page resolves their .web siblings. Each .web sibling takes the
plain name, its web-overrides entry goes, and the native-only rich Markdown
editor and Mermaid WebView generators and their outputs go with them.

Kept: native siblings the node test suite still renders shared screens
through (terminal WebView, browser frame path, clipboard, dictation, media
picker, live input, snapshot budget, input styles).

Claude-Session: https://claude.ai/code/session_6cea92f6

* test(mobile): drop the phone-preview comparison the deleted native sibling held

Claude-Session: https://claude.ai/code/session_6cea92f6

* refactor(mobile): drop the dead web route, flag-era comments and the one-value shell kind

The host route files no longer switch, so their comments say what they do now:
a route naming no screen shows the unavailable screen. app/h/[hostId]/web.tsx
duplicated index.tsx and nothing linked to it, so it and its stack entry go
(the bundle sweep is re-measured: 77 scripts at 15 routes). Diagnostics print
the bridge protocol on its own instead of a shell kind that has one value.

Claude-Session: https://claude.ai/code/session_6cea92f6

* refactor(mobile): one host for the rich editor, plain Text, and drop two unused native deps

MobileSelectableText was react-native's Text, so callers use Text. The rich
Markdown editor's host-facing seams (messages, URL prompt, keyboard inset) are
required now that the page is its only host; the WebView defaults go. Comments
that described deleted native components are rewritten, and
expo-image-manipulator and react-native-uitextview leave package.json and the
lockfile.

Claude-Session: https://claude.ai/code/session_6cea92f6

* feat(mobile): draw the host Accounts screen from the desktop's page

Accounts was the last host screen the shell still drew natively, so its
changes needed a phone build. It is now a page route like Tasks: the
screen moves to src/accounts/MobileAccountsScreen.tsx, accounts.web.tsx
renders it on the page, and accounts.tsx mounts the shell for it.

What React Native Web needed: Alert.alert is a no-op there, so the
reset confirmation, its outcome and a failed account switch are
ConfirmModal drawers (ConfirmModal can now drop Cancel for a notice);
Back goes through the route handoff, which pops the shell's stack; and
the reset idempotency key is built from getRandomBytes, since
randomUUID needs a secure context the iOS custom scheme is not.

edit.tsx edits the phone's own pairing and never renders on the page,
so the route manifest leaves it out and the page's host stack does not
declare it. The script sweep is re-measured for the 14-route tree.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(accounts): answer a same-offer Codex reset retry with its recorded outcome; mobile mirrors the desktop

The desktop keeps no client journal for a reset credit: the host's ledger
owns the attempt. A remote client that lost a reply and retried the same
offer under a new key was refused with "already attempted" (or "unknown
outcome" while the provider call was unresolved), which is why the phone
carried its own durable journal to replay the old key.

The coordinator now answers a request for an offer an earlier key already
claimed as that attempt: the settled outcome, the running attempt, or a
resume of the pending one under its original key. A different offer for
the account is still refused while an outcome is unknown, so nothing can
spend twice. The reply shape is the one a same-key replay always had, so
installed clients with their journal read it unchanged.

With that, the Accounts page sends a fresh key per attempt like the
desktop and keeps nothing on the phone: the AsyncStorage journal, its
client singleflight and the retry-record warnings are deleted, and the
journal-resume recording goes with them.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(accounts): resume an account's unresolved Codex reset when the retry names a newer offer

Review follow-ups:
- Host: after an unknown provider outcome the usage refresh moves the
  offer, so a retry names a newer one. The coordinator now answers it as
  the account's unresolved attempt (resumed under its own key and scope),
  as the desktop's consumeCurrent does, instead of refusing forever. One
  ledger lookup finds own key, offer claimant, then unresolved attempt.
- Mobile: a reply is matched on target, account and account revision,
  not the offer, so the resumed attempt's scope is accepted.
- The key is minted inside requestCodexResetCredit from randomV4Uuid, the
  session helper moved to src/platform/random-v4-uuid.ts.
- RefreshControl goes; the header refresh stays. ConfirmModal's
  onConfirm is optional for an OK-only notice.
- Stale journal comments and the mobile docs line are corrected.
- The edit.tsx page exclusion is dropped from this PR; the script sweep
  is re-measured for 15 routes.
- Root oxlint: escape the apostrophe; zod v4 strictObject.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): secure UUID fallback from expo-crypto; pin reset retry routing

- randomV4Uuid never falls back to Math.random: without crypto.randomUUID
  it builds the v4 UUID from ExpoCrypto.getRandomBytes, as the old reset
  key did. Tests get Node's secure bytes for expo-crypto from the setup
  file, since the native module has no Node build.
- Host tests: a new offer after a settled attempt spends again rather
  than replaying, and a retry naming a newer offer joins a resumed
  attempt in flight (one provider call, original key).

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* chore: restore main's bytes for files the merge's pre-commit format touched

The merge commit's lint-staged run reformatted 27 files this PR does not
change. Committed without the hook so they match origin/main exactly.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(mobile): stand in for expo-crypto with a Node alias, not a setup-file mock

Anti-slop forbids module mocking outside test files. The vitest config
now aliases expo-crypto to a faithful Node implementation of the names
shared code reaches; a test file's own mock still wins.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* docs(mobile): match the bridged-parity comments to the measured 795-golden tally

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-10-17 03:11:24 -07:00
Jinwoo Hong 86f6258d54 refactor(mobile): delete native screens the OTA shell no longer reaches (#29979)
* fix(mobile): wall hosts that cannot serve the page instead of opening native screens

* refactor(mobile): drop the native-route state and the shell fallback prop; unknown routes say unavailable

* test(mobile): read the nullable page route refusal in its policy test

* refactor(mobile): every build is the OTA shell; drop the native/OTA build switch

The owner retired the native-screen fallback with no emergency native build, so
EXPO_PUBLIC_MOBILE_SHELL, mobileShellBuildKind, shellSwitchDecision, the Metro
cache-key suffix and both release workflows' shell input go. Every host route
mounts the shell; a route naming no screen the shell could open gets the
existing refusal screen instead of a native screen.

Claude-Session: https://claude.ai/code/session_6cea92f6

* refactor(mobile): delete native screens the OTA shell no longer reaches

With every build the OTA shell, these native siblings run nowhere: the
native-executed closure (index.js plus every non-web route) does not reach
them, and the page resolves their .web siblings. Each .web sibling takes the
plain name, its web-overrides entry goes, and the native-only rich Markdown
editor and Mermaid WebView generators and their outputs go with them.

Kept: native siblings the node test suite still renders shared screens
through (terminal WebView, browser frame path, clipboard, dictation, media
picker, live input, snapshot budget, input styles).

Claude-Session: https://claude.ai/code/session_6cea92f6

* test(mobile): drop the phone-preview comparison the deleted native sibling held

Claude-Session: https://claude.ai/code/session_6cea92f6

* refactor(mobile): drop the dead web route, flag-era comments and the one-value shell kind

The host route files no longer switch, so their comments say what they do now:
a route naming no screen shows the unavailable screen. app/h/[hostId]/web.tsx
duplicated index.tsx and nothing linked to it, so it and its stack entry go
(the bundle sweep is re-measured: 77 scripts at 15 routes). Diagnostics print
the bridge protocol on its own instead of a shell kind that has one value.

Claude-Session: https://claude.ai/code/session_6cea92f6

* refactor(mobile): one host for the rich editor, plain Text, and drop two unused native deps

MobileSelectableText was react-native's Text, so callers use Text. The rich
Markdown editor's host-facing seams (messages, URL prompt, keyboard inset) are
required now that the page is its only host; the WebView defaults go. Comments
that described deleted native components are rewritten, and
expo-image-manipulator and react-native-uitextview leave package.json and the
lockfile.

Claude-Session: https://claude.ai/code/session_6cea92f6
2026-10-17 02:23:51 -07:00
Jinwoo Hong 37f23b9445 refactor(mobile): every build is the OTA shell; drop the native/OTA build switch (#29969)
* fix(mobile): wall hosts that cannot serve the page instead of opening native screens

* refactor(mobile): drop the native-route state and the shell fallback prop; unknown routes say unavailable

* test(mobile): read the nullable page route refusal in its policy test

* refactor(mobile): every build is the OTA shell; drop the native/OTA build switch

The owner retired the native-screen fallback with no emergency native build, so
EXPO_PUBLIC_MOBILE_SHELL, mobileShellBuildKind, shellSwitchDecision, the Metro
cache-key suffix and both release workflows' shell input go. Every host route
mounts the shell; a route naming no screen the shell could open gets the
existing refusal screen instead of a native screen.

Claude-Session: https://claude.ai/code/session_6cea92f6

* refactor(mobile): drop the dead web route, flag-era comments and the one-value shell kind

The host route files no longer switch, so their comments say what they do now:
a route naming no screen shows the unavailable screen. app/h/[hostId]/web.tsx
duplicated index.tsx and nothing linked to it, so it and its stack entry go
(the bundle sweep is re-measured: 77 scripts at 15 routes). Diagnostics print
the bridge protocol on its own instead of a shell kind that has one value.

Claude-Session: https://claude.ai/code/session_6cea92f6
2026-10-17 02:12:06 -07:00
Jinwoo Hong b2419a58ad fix(mobile): show the update screen instead of native screens when the desktop has no page (#29956)
* fix(mobile): wall hosts that cannot serve the page instead of opening native screens

* refactor(mobile): drop the native-route state and the shell fallback prop; unknown routes say unavailable

* test(mobile): read the nullable page route refusal in its policy test
2026-10-17 02:01:20 -07:00
Jinwoo Hong 2ff42fd37c fix(mobile): stop re-downloading a current OTA bundle on entry (#29860)
* fix(mobile): stop rewriting the cached manifest on a same-build hit

A same-build cache hit rewrote manifest.json (write next, delete, move)
while the native view was reading it. A read inside the gap reported
generation-unreadable, which discarded the cache and redownloaded the
same build. A matching build id now writes nothing to the generation
directory; the fresh routes still apply to the current session.

* refactor(mobile): drop code the manifest rewrite left behind

Opens a same-build hit straight from session.cached, folds the manifest
file name and parser back into the generation store, removes the unused
moveFile adapter and fake hooks, and trims tests that passed on main.
2026-10-16 16:55:28 -07:00
Neil a622f9efd3 fix(mobile): stop "Opening workspace" spinning forever when the page never loads (#29274)
* fix(mobile): fail a shell document the view never finishes loading instead of spinning forever

A load the WebView refuses or never commits reports nothing, which is how iOS 18
left the OTA workspace page on "Opening workspace" forever (#28626). Arm a
document-load deadline whenever the view is given a document; expiry takes the
existing document-load-failed recovery (drop the cache, refetch once, then the
failure screen with Retry).

* refactor(mobile): arm both shell document deadlines from one case
2026-10-15 18:27:25 -07:00
Jinwoo Hong 27c0275e3a feat(mobile): log OTA shell lifecycle to the connection diagnostics (#28840)
* feat(mobile): log OTA shell lifecycle to the connection diagnostics

Exported connection logs showed nothing about the web shell, so a phone
stuck on "Opening workspace" could not say whether it stalled activating
the download, loading the page, or waiting for its first paint. Record
sparse shell milestones, page signals, one-shot 15s stall warnings and
bridge diagnostics into the per-host connection log. Diagnostics only.

Refs STA-10545, #28626

* fix(mobile): cut shell log volume and pinpoint each waiting screen

Every page route mounts its own shell, so four lines per healthy open
evicted connection history from the 200-entry log. A healthy open now
writes one line, on paint, carrying source, build and elapsed.

The 15s stall line now also covers "Checking host" (gate verdict and
whether the cache read answered) and a download with no progress. The
"page has not drawn" line counts the page's ready asks and whether the
document loaded, telling a page that keeps refusing init from one that
took it and died. Frames dropped with no bridge host, an unreadable
host profile, post-failed error names and route-refused issues are now
logged; path masking catches quoted, bracketed and Windows paths. The
exported report header names the shell build and bridge protocol.

Diagnostics only: no behaviour, UI or wire change.

Refs STA-10545

* fix(mobile): dedupe flapping shell lines and mask spaced paths

A host with no cache flapping between unreachable and dialling wrote
"checking host" and "host unreachable" every cycle per mounted shell;
each is now written once per host until a different shell line lands.
Path masking keeps going across a space while the next word still holds
a separator, and accepts a comma before the path. The paint line's
elapsed time is renamed activatedMs, which is what it measures. A
route-only session rebuild re-arms the stall timer.

Refs STA-10545

* refactor(mobile): split the shell log module by responsibility

shell-lifecycle-log.ts passed the 300-line lint cap. The line type,
recorder and flapping dedupe move to shell-lifecycle-line-recorder.ts,
bridge diagnostic lines to shell-bridge-log-lines.ts, and user-path
masking to shell-log-path-masking.ts, with tests split to match. No
behaviour change.

Refs STA-10545
2026-10-14 17:42:22 -04:00
Jinwoo HongandClaude cd1ff506e1 feat(mobile): list the desktop's server projects on the phone's Tasks screen (#27741)
* test(e2e): add a paired phone client helper and phone-mirror baseline specs

Later phone-mirror slices need one oracle that drives a real mobile-scoped device with the
phone's own capabilities against a serve host or a paired desktop. Pins #26022 across a serve
restart and today's gap: a desktop's phone does not list the desktop's server workspaces.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* feat(mobile): show SSH host health on the workspace row host badge

Rows on an SSH host the desktop cannot reach looked healthy on the phone. The badge now
adds the desktop sidebar's own word ("devbox · Disconnected"), derived by the same shared
function from the connection status ssh.listTargetSummaries already sends; old desktops keep today's badge.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(e2e): compose the phone-mirror topology and check the cold snapshot

Slices need one composer for host, desktop and phone with a single teardown, so specs stop
copying setup. The serve-restart spec now reads the first tab-stream snapshot after the cold
start, which is what the phone builds its tab strip from.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* feat(mobile): name an unhealthy SSH host even in a single-host list

The desktop card shows SSH status on any host count, so the phone now reads target summaries
whenever a repo is on SSH and badges an unhealthy host alone too. Labels and health share one
target-to-host keying, and a hook-level test pins the reply-to-row wiring.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(e2e): keep phone-mirror teardown errors from masking test failures

Teardown failures become a test attachment instead of replacing the real failure, and the
cold-snapshot poll reports stream errors together with the PTY list.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): clear stale SSH host badges and announce them without the dot

A catalog that no longer spans hosts or holds an SSH repo now clears the health and label maps,
so a host that left cannot keep badging rows. The badge gets a screen-reader label ("devbox,
Disconnected") instead of reading the middle dot.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* feat(runtime): let a paired desktop register its phones as delegated mobile devices

A desktop paired to this runtime can now call pairing.delegatedMobileDevice.sync to make each of
its phones an ordinary mobile-scope device it owns, so the phone mirrors that host through the
desktop. Children are never pending, inherit the parent's reach, and die with the parent's grant.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* refactor(runtime): move the mobile device revoke primitive below dispatch

Dispatch binds the caller-bound delegated-device context, so the revoke path it needs now lives in
a layer under it as a pure move; this drops the abstract method seam. No behaviour change.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(runtime): keep a runtime grant until its delegated phones are revoked

A failed child cleanup now leaves the parent in place so a retry can still find the children, and
revokeRuntimeAccess lives beside the revoke it cascades to. Adds restart, rename and bounds coverage,
shares credential minting, and drops the timing-dependent census.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(runtime): fail a delegated sync whose dropped phone cannot be revoked

Matches the parent revoke: a child whose cleanup could not be saved is kept and the sync errors
before minting anything, so the desktop retries instead of believing the server matches its list.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* feat(mobile): relay a paired phone's server-workspace requests through the desktop

A phone mirrors its desktop, including workspaces on the desktop's configured servers. The desktop
now forwards a request that names `executionHost: runtime:<env>` to that server over one socket per
(phone, server), signed in as the phone's delegated device; replies pass through untouched except
stream ids. Hidden until the phone client ships: the relay capability is not advertised yet.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(mobile): type the single-host health fixtures inline so the test tsc passes

Fixtures hoisted into consts widened their health maps to string, which the now-required
hostHealthById rejects under the tests-typecheck ratchet; passing them inline keeps the union.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* docs(mobile): state the desktop relay contract in its entry module

main dropped docs/reference, so the relay's contract lives beside the code. Also pins that a
server whose delegated sync fails (e.g. delegated_device_revoke_failed) is unavailable until a
later request syncs it again.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(mobile): pin the relay's configured-server adapter

The production hosts adapter was only typechecked; this pins that it resolves configured servers
by exact id only (never a name or a removed server), fences by pairing revision and runtime
identity, and stops reporting retirements once unsubscribed.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(mobile): re-record the repo-metadata goldens for SSH host health

The settled metadata state now carries hostHealthById, and an SSH-only catalog now reads the
target summaries so a lone unhealthy host can be badged. Scoped re-record via rpc-recording.mts.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* refactor(mobile): tighten the desktop relay after elegance review

Reuses the shared E2EE socket opener, folds relay state into fewer fields, never sends a request
whose link ended while opening, passes failure codes through, relays only this desktop's own
phones, and drops the unused host-state reader until S2 needs one.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): bound relay frames and stop sticky relay verdicts

Restores the 8 MiB frame cap on every remote runtime socket, re-checks an update-needed server on
each new link so an in-place upgrade relays, answers a phone the server just declined without
syncing again, and ends a link whose mirrored capability update cannot be queued.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): run targeted desktop-owned calls on the desktop

The phone tags every call on a workspace screen with its execution host, so a
paired-desktop method carrying `runtime:<env>` now runs locally instead of
failing 'forbidden'. The Mac stays the only reader of the routing table.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* feat(mobile): list the desktop's configured servers and their last workspace rows for the phone

The phone needs the same host list the desktop shows, including servers that are offline or
too old to relay to. mobileRelay.hosts.list/worktrees answer from the desktop's own status and
its own worktree.ps fetches, cached per server and fenced by pairing, never from relayed replies.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* refactor(mobile): list server rows through one desktop fetch, with no side effect on listing

The phone now always lists a server's rows through mobileRelay.hosts.worktrees, so the
desktop's own fetch is both the live source and the cache, and stamping stays in main.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): fetch a server's full workspace list within a bound, and forget removed servers

The desktop now asks for the phone's full-catalog limit, answers within 5s with the last rows
when a server hangs, drops a server's rows when it is removed or re-paired, and checks the
server's identity after the fetch so a reply from before a re-pair is never served.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* feat(mobile): name a server workspace's host on the phone's calls

A phone reaches a server's workspace through its desktop only if each call names the host; the
direct, cloud-relay and shell-bridge transports now carry `executionHost`, streams keep it for
replay and unsubscribe, and an older shell (no `executionHost` grant) refuses rather than strips it.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): list managed servers as the desktop sidebar does, and hide rows mid-delete

A server Orca manages on an SSH host now lists once under that host's name, or not at all while
the host is reached over the SSH relay, using the sidebar's own folding. Rows the server is
deleting are left out at reply time, as a phone asking the server itself would see.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): keep a disconnected server's last rows, and never join a re-paired server's fetch

Retirement also fires on a manual disconnect or managed-server update, which wrongly emptied
the phone's list; rows are now pruned only once a server is no longer configured. Fetches are
shared per server identity, so a poll after a re-pair asks the new server right away.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* feat(mobile): list the paired desktop's server workspaces on the phone

Fixes the phone missing a desktop's `orca serve` workspaces: rows come from the desktop's
mobileRelay.hosts.* with each server's health ("Update needed", "Disconnected"), open through the
relay, follow the desktop's hidden hosts, and the desktop now advertises the relay capability.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(mobile): pin that a desktop failing the hosts list leaves only its own rows

The page asks a relaying desktop for its servers; a refusal such as method_not_found must leave
today's list with no server rows and no catalog error.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): mirror the desktop's shown hosts exactly, one host included

Jinwoo's call: the phone follows visibleWorkspaceHostIds as the desktop sidebar does, so a
one-host list narrows the phone too instead of being read as a single-host focus.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): scope a server workspace at its route, never the desktop

Every screen under a server workspace's route now reads one context and
gets that server's client and a router that keeps pushes on it, refusing
to fall back to the desktop; pins write to the row's owner like the Mac.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): never open an unreachable server workspace, and prove the relay in e2e

A server workspace the phone can't reach now gets one explicit screen at its route
and no row tap or action-sheet entry opens it; native routes read the route's host,
a census pins every workspace route, and the e2e reads files and git on the server.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* feat(mobile): gate a server workspace on that server's own features

A targeted status.get now relays to the server, so a server workspace's route
provides that server's status to every screen under it and its action sheet,
while whether the phone can reach the server stays the desktop's answer.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(ci): run the remote-runtime WebSocket opener test on Node

Under the bun vitest project, `ws` resolves to Bun's built-in client, which
ignores maxPayload, so the frame-cap test hung. The opener only ships on
Node/Electron, so run its test in the Node runtime project like its siblings.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): run a server workspace's SSH calls on that server

A phone in a server workspace asks for SSH state before a file mutation, but the Mac answered
about its own targets, so server-owned SSH repos failed. The desktop asks the server, so the
ssh.* family now routes by target; an e2e pins files, uploads, commits and review on the server.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(mobile): keep the server files/git e2e offline and baseline-git safe

Review eligibility reached real gh/glab over the network and proved nothing beyond repoSlug;
setup git now ignores the runner's config and avoids `init -b` (Git 2.25). The relay
integration test is the routing oracle, so the census re-listing goes.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* feat(mobile): sleep, wake and new workspaces on a desktop's servers from the phone

The desktop renderer alone sleeps a server's workspace and holds its slept agents, so the phone
now asks the desktop (two paired-desktop methods) instead of the headless server; the
new-workspace sheet lists and creates on reachable servers the way the desktop's composer does.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): sleep and wake only a server the desktop has configured

A phone's sleep or wake names a server by host id; the desktop now refuses one that is not in
its own configured servers, read from its settings rather than any fetched workspace list.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): keep server workspaces' repos, picks and sleeps on their own host

A hung server no longer holds back the desktop's repos, a repo id shared by two hosts no longer
swaps the pick, and a server row's optimistic sleep waits for the server's own list to confirm it.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(mobile): give the server-workspaces test rows every Worktree field

The tests-typecheck ratchet checks mobile test files; the fixture row was missing required
Worktree members, so the file no longer typechecked.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): give the file preview the workspace router, back to 128 page assets

The preview was the one workspace screen on the plain router, so the scoped router
was reached by a route set of its own and esbuild split it into an extra chunk
(129 assets); every workspace screen now uses it and it shares the route's chunk.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* revert(mobile): drop server sleep and wake from the phone for now

The workspace-layout refactor deletes the notifier path these used; sleep and
wake on a server come back later as server-side layout.sleep / layout.wake.
A server row's Sleep behaves exactly as on feat/mobile-remote-workspaces.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* refactor(mobile-relay): derive the phone allowlist from the routing table

Adding a phone method now takes one edit (its route). A server-targeted call on a
legacy non-E2EE socket is refused instead of running locally, and only
execution-host methods enter the relay.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* refactor(mobile-relay): drop unread freshness fields from server workspace lists

The phone reads only the rows, so stale, fetchedAt, totalCount and truncated
were dead wire weight. The desktop still serves a server's last rows while
the server is unreachable.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* refactor(mobile): read only the rows of a server's workspace list

The desktop no longer sends a freshness flag, so the phone's schema drops it.
The relay capability list names the delegated-devices constant instead of
repeating its literal, which also retires a now-circular type check.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): open server-workspace diffs and file taps on the phone's own screens

A headless orca serve has no renderer, so files.openDiff / files.open answer
renderer_unavailable and the Changes tap showed that raw code. On that refusal the
phone now opens its review screen (git.diff) or file preview (files.read) instead.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): show a server workspace as disconnected while its server is down

The phone read the desktop's connection state for a server workspace, so a stopped
server kept a green dot and "Loading tabs" spun forever. The workspace route now
narrows that state by the server's health from mobileRelay.hosts.list, reusing the
existing disconnected session UI and refetching tabs when the server returns.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): name run targets and the desktop's own row as the host list does

The Run on picker showed a server's raw environment id behind "Remote ·" and
always a monitor glyph; it now takes the host list's badge labels and icon, as
the desktop's picker does. Server rows also made the desktop row read "This
computer" because its platform was only read for a mixed desktop catalog.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): drop Open in Session and preview new notes in server workspaces

A server workspace's host has no editor tabs, so the review screen no longer offers
Open in Session there, and a new markdown note whose files.open answers
renderer_unavailable opens in the file preview instead of a raw error toast.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* refactor(mobile): share one desktop server-list feed between the host list and workspaces

A server workspace polled mobileRelay.hosts.list beside the host list's own read, so
the phone could ask the desktop twice per tick and the row badge and the screen could
disagree. One feed per desktop client now owns that read; both readers join it.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(e2e): read a server's reachability from the hosts list, not a dropped stale flag

eff410fe30 dropped `stale` from mobileRelay.hosts.worktrees, so the mirror spec's
schema never parsed and its first poll timed out. It now waits on the server's
relay/health from mobileRelay.hosts.list, the field the phone itself reads.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* build(mobile): re-measure the page's script sweep for server workspaces

Main already sat on its 78-script ceiling (re-swept: 78, table: 74); the workspace
route's shared modules split importer sets three ways more, reaching 81. Re-swept per
route prefix on this head, as #24257 did for its feature; the spread is now 1 to 11.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(mobile): mock react-native where the server-list feed now loads it

The merged server-list feed imports react-native's AppState, which vitest cannot parse unmocked.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): offer the desktop's servers in a new workspace opened before the list loads

The sheet's run targets came from the host list's server rows, which never load while
the sheet is open (the list deliberately pauses its refresh behind it). From Home's
New Workspace that left only the desktop's repos. The sheet now reads the desktop's
server-list feed itself while it is open.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): name the desktop's servers in a new workspace opened before the list loads

The previous fix offered the servers but still named them from the host list's rows,
so a sheet opened from Home showed a server's raw id under Run on. The sheet now takes
both the servers and their names from the server-list feed while it is open.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* refactor(mobile): export the server-list feed's React reader beside the feed

useRelayHosts moves from workspace-server.ts into relay-hosts-feed.ts, so every reader (workspace screens now, the new-workspace sheet next) subscribes through the feed's own module. No behaviour change.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(mobile): type the server run-target fixture's host id as a runtime id

A plain string executionHostId is not an ExecutionHostId, so the mobile tests typecheck ratchet refused the file.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(e2e): stop requiring the dropped stale flag in the files and git review spec

eff410fe30 removed stale from mobileRelay.hosts.worktrees, so the schema never parsed and the row poll at :100 timed out.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* feat(mobile): offer a server's SSH repos on the New Workspace sheet

The desktop's composer offers a paired server's own SSH repos and connects them through that
server. The phone left them out only because ssh.* was the Mac's own; it now reaches the server
(#26519), so the drawer's existing SSH gate connects through the picked repo's server client.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): hide New Note where the host opens no editor tabs; drop stale pushes

A server workspace's host has no desktop window, so New Note left an unopenable
untitled.md each tap. It now hides under the same route rule as Open in Session, and
the no-window fallbacks no longer push a screen after the user has moved on.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* refactor(mobile): let the new-workspace sheet read its servers where it knows it is open

The sheet's controller already owns whether it is open, so it now reads the desktop's
server list and names itself; the overlay's mirrored visibility state and the list-side
hook go away. A server row no longer offers Sleep, which would sleep it on the desktop.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): key the New Workspace SSH gate by the host holding the target

The desktop and a server can each have an SSH target with the same id. The gate's state, Connect
progress and detected agents were keyed by that id alone, so switching to the server's target showed
the desktop's "connected" until the server answered, and let create through.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* refactor(mobile): name the editor-tab gate for what it measures

useWorkspaceOnPairedHost says the workspace runs on the paired desktop itself; its doc
states the rule (hide actions whose only result is an editor tab) and its known gap.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): keep polling the server list and read a server's reachability from relay

A hosts-list read slower than the poll stopped the feed for good, since the next tick was
scheduled when the read started; it is now scheduled from the finish. A server workspace now
reads connected only when the desktop relays to it, so a server with no status yet is not shown up.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* feat(mobile): list the desktop's server repos on the phone's Tasks screen

The desktop's task page lists every host's repos and sends each repo's work-item calls to the host
that owns it. The phone listed only the desktop's own repos; it now adds each reachable server's
repos and runs every call naming one of them on that server through the desktop.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): offer only the repos the desktop's New Workspace offers

The sheet now builds its lists from the desktop's own eligibility rule, so a server's
per-workspace-environment SSH repo never appears or wins a server's row. A connect reply that
arrives after the picked target changed no longer overwrites the newly shown target's state.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): show a reached server as connecting and a too-old one as disconnected

A server the desktop has reached but whose status is not answered yet is moments from relaying,
so it reads connecting; a server too old to relay to never will, so it reads disconnected.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* refactor(mobile): export the New Workspace SSH target key

Names an SSH target by its holding host and id, so other server-aware screens can key their SSH
state the same way.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): let Tasks show the desktop's repos before a server answers

The Tasks list waited for every server's repo list, so one slow server held up the desktop's own.
The desktop list now settles alone and each server's joins as it answers, through the same hook
the new-workspace sheet uses. Server repos name their host in the repo pickers, and agent detection
takes the router's per-repo resolver.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): keep the New Workspace sheet on a hidden repo's project

A last-visited per-workspace-environment repo is hidden, so the sheet fell back to the first
listed project. It now maps it to its project's eligible copy with the desktop's resolver, moved to
src/shared. The phone's mirror of the seeding rule is gone, since the hook imports the shared one.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): run a server SSH repo's Tasks checks on that server

F2 lists a server's SSH-backed repos, so Tasks now reads and connects their SSH targets through
the repo's own client and keys the drawer's SSH state by (host, connectionId): the desktop and a
server can each hold a target with the same id. Tests drive the real GitHub list loader.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): keep a Tasks repo filter and the listed items while servers join

A saved selection naming a server's repo waits until every listed server has answered or failed,
instead of widening to all repos. A server's repos joining reload the list quietly, so the
desktop's items stay up, and a mixed list names the desktop's own host in the pickers too.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): apply a Tasks repo filter early and keep it through server blips

A saved filter applies once every saved id is listed or every server has answered, so a
desktop-only filter never waits on servers, and a server that drops out briefly keeps its ids.
One shared hook gives both repo pickers their server clients, keyed on the ready set, and an
unchanged server answer no longer reloads the list.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* refactor(mobile): derive the Tasks repo filter instead of pruning it

The saved selection is now the user's choice, written only by them and never pruned; every
reader narrows it to what is listed now, so a late, flapping or reconnecting server keeps its
repos selected and a removed one reads as all. This deletes the reconcile hook, its refs and
the server-settled tracking.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): edit the Tasks repo filter from what's shown

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): load the Tasks list once per server repo change

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* refactor(shared): move the account capabilities into their own group module

Main's merge put protocol-version.ts at 301 lines, over the 300 cap. The account group moves out
like the relay and skill groups did, same strings in the same order; the page's script sweep is
re-measured because the split changed its chunking (81 to 79 at 16 routes).

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(mobile): read the Tasks mock's exports from a typed map

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): use type imports and full deps in the Tasks server-repo code

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* refactor(mobile): name a server workspace through the request envelope

The server name now rides the generic request envelope from #27815 instead of
its own executionHost option, bridge grant and per-transport plumbing. Server
rows show only where the transport carries an envelope (native, or a shell
that grants rpcEnvelope), so an older app keeps today's list.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(mobile): pin the server-call wire and the no-envelope deep link

A server workspace call still reaches the desktop as the same top-level
executionHost frame, on the direct and relay wires, and a deep link into a
server workspace under an app without rpcEnvelope shows the unreachable screen.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* refactor(mobile): read a relay stream's envelope slot in sorted key order

Equal envelopes name one cleanup slot whatever their key order, the scoped view
passes its client's envelope answer through as it does the generation, and the
direct wire harness is shared instead of rebuilt.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): a phone's new chat in a server workspace follows the desktop's chat setting

A relayed agent.launch ran on the server, which picked chat vs terminal from its
own setting. The desktop relay now stamps its own default as nativeChatDefault;
new servers use it in place of their setting (feasibility still decides), older
servers strip it. Outside the replay fingerprint.

Co-Authored-By: Claude <noreply@anthropic.com>

* refactor(mobile): move the relay's delegated phone grant cache into its own module

Keeps mobile-desktop-relay.ts under the line limit once #27749's page relay lands on it.

Co-Authored-By: Claude <noreply@anthropic.com>

* revert(mobile): stop offering a server's SSH repos on the phone

Since #27609 a server cannot serve SSH repos at all (repo.addRemote throws and SSH-routed work on
a server fails), so the phone should not offer them. This reverts #27728's own commits
(c4c058ad15, 4a5ccaa9a3, 3b414b8842, a2bb1cf8a5, adb8ac5825) and the server-SSH parts of
ce4b125b40: the New Workspace sheet and the Tasks screen again leave a server's SSH-backed repos
out, and Tasks' SSH checks run on the desktop with its SSH state keyed by connection id only.
Affected goldens re-recorded with `pnpm --dir mobile rpc:record`.

Kept from ce4b125b40: useHostProtocolGates in host-protocol-gates-context (later commits import
it there), the Tasks route requiring the gate, and the typed ListedRepo (the SSH filter reads it).
Kept from #27728: nothing; the SSH filter now lives in the shared listServerRepos both screens use.
The Tasks filter-edit test lists a third server repo so picking two stays a filter.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): show the early terminal tab from the relaying desktop's chat default

The early-tab preflight read the server's own chat setting while the launch
used the desktop's stamped nativeChatDefault, so on a windowed server with
chats off a relayed chat launch flashed a terminal tab. Both now read the
settings through readAgentLaunchModeSettings(runtime, nativeChatDefault).

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-14 01:55:55 -04:00
Jinwoo HongandClaude 5f134d8f01 feat(mobile): show and use the desktop's server workspaces on the phone (#26501)
* test(e2e): add a paired phone client helper and phone-mirror baseline specs

Later phone-mirror slices need one oracle that drives a real mobile-scoped device with the
phone's own capabilities against a serve host or a paired desktop. Pins #26022 across a serve
restart and today's gap: a desktop's phone does not list the desktop's server workspaces.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* feat(mobile): show SSH host health on the workspace row host badge

Rows on an SSH host the desktop cannot reach looked healthy on the phone. The badge now
adds the desktop sidebar's own word ("devbox · Disconnected"), derived by the same shared
function from the connection status ssh.listTargetSummaries already sends; old desktops keep today's badge.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(e2e): compose the phone-mirror topology and check the cold snapshot

Slices need one composer for host, desktop and phone with a single teardown, so specs stop
copying setup. The serve-restart spec now reads the first tab-stream snapshot after the cold
start, which is what the phone builds its tab strip from.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* feat(mobile): name an unhealthy SSH host even in a single-host list

The desktop card shows SSH status on any host count, so the phone now reads target summaries
whenever a repo is on SSH and badges an unhealthy host alone too. Labels and health share one
target-to-host keying, and a hook-level test pins the reply-to-row wiring.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(e2e): keep phone-mirror teardown errors from masking test failures

Teardown failures become a test attachment instead of replacing the real failure, and the
cold-snapshot poll reports stream errors together with the PTY list.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): clear stale SSH host badges and announce them without the dot

A catalog that no longer spans hosts or holds an SSH repo now clears the health and label maps,
so a host that left cannot keep badging rows. The badge gets a screen-reader label ("devbox,
Disconnected") instead of reading the middle dot.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* feat(runtime): let a paired desktop register its phones as delegated mobile devices

A desktop paired to this runtime can now call pairing.delegatedMobileDevice.sync to make each of
its phones an ordinary mobile-scope device it owns, so the phone mirrors that host through the
desktop. Children are never pending, inherit the parent's reach, and die with the parent's grant.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* refactor(runtime): move the mobile device revoke primitive below dispatch

Dispatch binds the caller-bound delegated-device context, so the revoke path it needs now lives in
a layer under it as a pure move; this drops the abstract method seam. No behaviour change.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(runtime): keep a runtime grant until its delegated phones are revoked

A failed child cleanup now leaves the parent in place so a retry can still find the children, and
revokeRuntimeAccess lives beside the revoke it cascades to. Adds restart, rename and bounds coverage,
shares credential minting, and drops the timing-dependent census.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(runtime): fail a delegated sync whose dropped phone cannot be revoked

Matches the parent revoke: a child whose cleanup could not be saved is kept and the sync errors
before minting anything, so the desktop retries instead of believing the server matches its list.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* feat(mobile): relay a paired phone's server-workspace requests through the desktop

A phone mirrors its desktop, including workspaces on the desktop's configured servers. The desktop
now forwards a request that names `executionHost: runtime:<env>` to that server over one socket per
(phone, server), signed in as the phone's delegated device; replies pass through untouched except
stream ids. Hidden until the phone client ships: the relay capability is not advertised yet.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(mobile): type the single-host health fixtures inline so the test tsc passes

Fixtures hoisted into consts widened their health maps to string, which the now-required
hostHealthById rejects under the tests-typecheck ratchet; passing them inline keeps the union.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* docs(mobile): state the desktop relay contract in its entry module

main dropped docs/reference, so the relay's contract lives beside the code. Also pins that a
server whose delegated sync fails (e.g. delegated_device_revoke_failed) is unavailable until a
later request syncs it again.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(mobile): pin the relay's configured-server adapter

The production hosts adapter was only typechecked; this pins that it resolves configured servers
by exact id only (never a name or a removed server), fences by pairing revision and runtime
identity, and stops reporting retirements once unsubscribed.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(mobile): re-record the repo-metadata goldens for SSH host health

The settled metadata state now carries hostHealthById, and an SSH-only catalog now reads the
target summaries so a lone unhealthy host can be badged. Scoped re-record via rpc-recording.mts.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* refactor(mobile): tighten the desktop relay after elegance review

Reuses the shared E2EE socket opener, folds relay state into fewer fields, never sends a request
whose link ended while opening, passes failure codes through, relays only this desktop's own
phones, and drops the unused host-state reader until S2 needs one.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): bound relay frames and stop sticky relay verdicts

Restores the 8 MiB frame cap on every remote runtime socket, re-checks an update-needed server on
each new link so an in-place upgrade relays, answers a phone the server just declined without
syncing again, and ends a link whose mirrored capability update cannot be queued.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): run targeted desktop-owned calls on the desktop

The phone tags every call on a workspace screen with its execution host, so a
paired-desktop method carrying `runtime:<env>` now runs locally instead of
failing 'forbidden'. The Mac stays the only reader of the routing table.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* feat(mobile): list the desktop's configured servers and their last workspace rows for the phone

The phone needs the same host list the desktop shows, including servers that are offline or
too old to relay to. mobileRelay.hosts.list/worktrees answer from the desktop's own status and
its own worktree.ps fetches, cached per server and fenced by pairing, never from relayed replies.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* refactor(mobile): list server rows through one desktop fetch, with no side effect on listing

The phone now always lists a server's rows through mobileRelay.hosts.worktrees, so the
desktop's own fetch is both the live source and the cache, and stamping stays in main.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): fetch a server's full workspace list within a bound, and forget removed servers

The desktop now asks for the phone's full-catalog limit, answers within 5s with the last rows
when a server hangs, drops a server's rows when it is removed or re-paired, and checks the
server's identity after the fetch so a reply from before a re-pair is never served.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* feat(mobile): name a server workspace's host on the phone's calls

A phone reaches a server's workspace through its desktop only if each call names the host; the
direct, cloud-relay and shell-bridge transports now carry `executionHost`, streams keep it for
replay and unsubscribe, and an older shell (no `executionHost` grant) refuses rather than strips it.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): list managed servers as the desktop sidebar does, and hide rows mid-delete

A server Orca manages on an SSH host now lists once under that host's name, or not at all while
the host is reached over the SSH relay, using the sidebar's own folding. Rows the server is
deleting are left out at reply time, as a phone asking the server itself would see.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): keep a disconnected server's last rows, and never join a re-paired server's fetch

Retirement also fires on a manual disconnect or managed-server update, which wrongly emptied
the phone's list; rows are now pruned only once a server is no longer configured. Fetches are
shared per server identity, so a poll after a re-pair asks the new server right away.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* feat(mobile): list the paired desktop's server workspaces on the phone

Fixes the phone missing a desktop's `orca serve` workspaces: rows come from the desktop's
mobileRelay.hosts.* with each server's health ("Update needed", "Disconnected"), open through the
relay, follow the desktop's hidden hosts, and the desktop now advertises the relay capability.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(mobile): pin that a desktop failing the hosts list leaves only its own rows

The page asks a relaying desktop for its servers; a refusal such as method_not_found must leave
today's list with no server rows and no catalog error.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): mirror the desktop's shown hosts exactly, one host included

Jinwoo's call: the phone follows visibleWorkspaceHostIds as the desktop sidebar does, so a
one-host list narrows the phone too instead of being read as a single-host focus.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): scope a server workspace at its route, never the desktop

Every screen under a server workspace's route now reads one context and
gets that server's client and a router that keeps pushes on it, refusing
to fall back to the desktop; pins write to the row's owner like the Mac.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): never open an unreachable server workspace, and prove the relay in e2e

A server workspace the phone can't reach now gets one explicit screen at its route
and no row tap or action-sheet entry opens it; native routes read the route's host,
a census pins every workspace route, and the e2e reads files and git on the server.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* feat(mobile): gate a server workspace on that server's own features

A targeted status.get now relays to the server, so a server workspace's route
provides that server's status to every screen under it and its action sheet,
while whether the phone can reach the server stays the desktop's answer.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(ci): run the remote-runtime WebSocket opener test on Node

Under the bun vitest project, `ws` resolves to Bun's built-in client, which
ignores maxPayload, so the frame-cap test hung. The opener only ships on
Node/Electron, so run its test in the Node runtime project like its siblings.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): give the file preview the workspace router, back to 128 page assets

The preview was the one workspace screen on the plain router, so the scoped router
was reached by a route set of its own and esbuild split it into an extra chunk
(129 assets); every workspace screen now uses it and it shares the route's chunk.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* refactor(mobile-relay): derive the phone allowlist from the routing table

Adding a phone method now takes one edit (its route). A server-targeted call on a
legacy non-E2EE socket is refused instead of running locally, and only
execution-host methods enter the relay.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* refactor(mobile-relay): drop unread freshness fields from server workspace lists

The phone reads only the rows, so stale, fetchedAt, totalCount and truncated
were dead wire weight. The desktop still serves a server's last rows while
the server is unreachable.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* refactor(mobile): read only the rows of a server's workspace list

The desktop no longer sends a freshness flag, so the phone's schema drops it.
The relay capability list names the delegated-devices constant instead of
repeating its literal, which also retires a now-circular type check.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): show a server workspace as disconnected while its server is down

The phone read the desktop's connection state for a server workspace, so a stopped
server kept a green dot and "Loading tabs" spun forever. The workspace route now
narrows that state by the server's health from mobileRelay.hosts.list, reusing the
existing disconnected session UI and refetching tabs when the server returns.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* refactor(mobile): share one desktop server-list feed between the host list and workspaces

A server workspace polled mobileRelay.hosts.list beside the host list's own read, so
the phone could ask the desktop twice per tick and the row badge and the screen could
disagree. One feed per desktop client now owns that read; both readers join it.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(e2e): read a server's reachability from the hosts list, not a dropped stale flag

eff410fe30 dropped `stale` from mobileRelay.hosts.worktrees, so the mirror spec's
schema never parsed and its first poll timed out. It now waits on the server's
relay/health from mobileRelay.hosts.list, the field the phone itself reads.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* build(mobile): re-measure the page's script sweep for server workspaces

Main already sat on its 78-script ceiling (re-swept: 78, table: 74); the workspace
route's shared modules split importer sets three ways more, reaching 81. Re-swept per
route prefix on this head, as #24257 did for its feature; the spread is now 1 to 11.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* refactor(mobile): export the server-list feed's React reader beside the feed

useRelayHosts moves from workspace-server.ts into relay-hosts-feed.ts, so every reader (workspace screens now, the new-workspace sheet next) subscribes through the feed's own module. No behaviour change.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): keep polling the server list and read a server's reachability from relay

A hosts-list read slower than the poll stopped the feed for good, since the next tick was
scheduled when the read started; it is now scheduled from the finish. A server workspace now
reads connected only when the desktop relays to it, so a server with no status yet is not shown up.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): show a reached server as connecting and a too-old one as disconnected

A server the desktop has reached but whose status is not answered yet is moments from relaying,
so it reads connecting; a server too old to relay to never will, so it reads disconnected.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* refactor(shared): move the account capabilities into their own group module

Main's merge put protocol-version.ts at 301 lines, over the 300 cap. The account group moves out
like the relay and skill groups did, same strings in the same order; the page's script sweep is
re-measured because the split changed its chunking (81 to 79 at 16 routes).

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* refactor(mobile): name a server workspace through the request envelope

The server name now rides the generic request envelope from #27815 instead of
its own executionHost option, bridge grant and per-transport plumbing. Server
rows show only where the transport carries an envelope (native, or a shell
that grants rpcEnvelope), so an older app keeps today's list.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(mobile): pin the server-call wire and the no-envelope deep link

A server workspace call still reaches the desktop as the same top-level
executionHost frame, on the direct and relay wires, and a deep link into a
server workspace under an app without rpcEnvelope shows the unreachable screen.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* refactor(mobile): read a relay stream's envelope slot in sorted key order

Equal envelopes name one cleanup slot whatever their key order, the scoped view
passes its client's envelope answer through as it does the generation, and the
direct wire harness is shared instead of rebuilt.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): a phone's new chat in a server workspace follows the desktop's chat setting

A relayed agent.launch ran on the server, which picked chat vs terminal from its
own setting. The desktop relay now stamps its own default as nativeChatDefault;
new servers use it in place of their setting (feasibility still decides), older
servers strip it. Outside the replay fingerprint.

Co-Authored-By: Claude <noreply@anthropic.com>

* refactor(mobile): move the relay's delegated phone grant cache into its own module

Keeps mobile-desktop-relay.ts under the line limit once #27749's page relay lands on it.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(mobile): show the early terminal tab from the relaying desktop's chat default

The early-tab preflight read the server's own chat setting while the launch
used the desktop's stamped nativeChatDefault, so on a windowed server with
chats off a relayed chat launch flashed a terminal tab. Both now read the
settings through readAgentLaunchModeSettings(runtime, nativeChatDefault).

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-13 23:35:33 -04:00
Neil 13a75d3939 [CI] Reduce redundant test work and import existing helpers directly (#28365)
* [CI] Reduce redundant test work and import existing helpers directly

* [CI] Preserve provider guards and pin the mobile browser test core

* [CI] Fix CI test setup and verify retained contracts

* [CI] Compare native media chunks without boxed byte arrays

* [CI] Detect shifted native media reads with position-sensitive bytes
2026-10-13 20:25:00 -07:00
Jinwoo Hong 3a011e48d1 fix(native-chat): load long chats on the phone by keeping every update within its limit (#28203)
* fix(native-chat): fit chat stream pages in one mobile bridge frame

A long native chat failed to open on a phone with "the shell ended this
stream (overflow)". The host sized each history page to ~1.94 MiB (half
the relay cap), but the mobile app ends any stream whose event frame is
over 640 KiB, so the opening snapshot of a long chat never arrived.
Pages now fit that frame; older rows load through Load earlier as before.

The page also words a stream the app ended in plain language instead of
the protocol's reason.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* refactor(native-chat): keep the outline on the relay page share; plainer end words

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(mobile): read the bridge frame cap from its shared definition

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* feat(native-chat): bounded frames for the phone's chat stream

A phone page now asks the host (when it advertises
agent-session.bounded-frames.v1) to keep every agentSession.subscribe
frame within the bridge's 640 KiB frame:

- each row is cut to what the phone draws (tool output to its detail
  length unless it draws as a diff, tool inputs to the diff view's
  length) and then to one row's share of a page, its long strings cut to
  a common length; the row is marked `truncated` with its full size
- history frames carry rows and metadata held to a fixed ceiling; the
  draft queue and the background-task roster go in frames of their own;
  the command list is not sent, since the phone reads it from options
- the queue's admission bound counts whole cards and is sized to one
  frame, so a phone always gets every card whole

Other subscribers get the frames they always did.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* refactor(native-chat): bounded frames review round 1

- the phone waits for the host's capabilities before opening the
  stream, so its opening frame is never sent unbounded
- rows are bounded once, by the reader that sizes the page; the item
  form is an options field and always a function
- one cut rule (`cutLongStrings`) serves the display trims and the fit
- a side list rides an empty history batch rather than a second frame;
  commands changes send a bounded subscriber nothing
- queue admission measures the candidate list once; a legacy queue of
  more cards than fit lists the ones it sends first

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* refactor(native-chat): bounded frames review round 2

- an over-budget legacy queue cuts its text to one common length and
  marks the cards it cut, and no longer names a next card it left out
- one predicate says whether a subscriber is owed the `/` list
- a lighter invariant fixture

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(native-chat): type the shortening guards against named shapes

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* refactor(native-chat): bound every phone's chat stream at the host

The host now bounds frames for every phone (clientKind 'mobile') at the RPC
boundary, so the subscribe param, the host capability and the phone's wait
for the capability probe are gone. Phones of every version already ignore
the stream's slash commands, and a phone reached through the desktop relay
signs in as a phone too.

- A live gap of more than one page reaches a phone as the newest page, so
  no publish outruns the bridge's unacked window; older rows load on scroll.
- Queue admission sizes each card as wide as a failed send can make it.
- Rows are bounded only as a page takes them; the unread row marker is gone
  and a shortened queue card is marked `truncated: true`.
- A bounded subscriber no longer reads or tracks the slash commands.
- The stream-end words are back to main's: `overflow` also covers a page
  that stopped acking, which "too large" would misstate.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(native-chat): count every card in line and never shorten one

Queue admission now counts every card the queue can publish again without
passing admission: waiting and returned cards, and sent cards whose delivery
is not yet accepted, which a refusal or a Stop puts back. The fixed per-card
reserve for a failed send's words is gone.

When a phone's list is over its frame room anyway (an older build's larger
queue, or a card that grew after admission), the phone gets the cards it
sends first, whole, and the rest once those leave. Cards are never shortened,
so no app version can Edit a partial copy over the full draft, and the
`truncated` card flag is gone.

Bounded frames strip only the roster from an event; no event carries the
other side lists.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* refactor(native-chat): bound only phone frames; keep main's queue and page sizes

Queue admission is main's again. The phone's list is bounded where it is
sent: the cards it sends first, whole, so every frame fits for any queue
size and no card is shortened.

History pages keep main's size for every reader but phone subscribe frames.
A reader is now its item form plus its page budget, so desktop pages, the
outline and phone history replies (chunked by the app's bridge) are as on
main, and only bounded frames use the bridge frame's share.

Drops the queue's `nextQueuedMessageId` rewrite, which no phone reads, and a
test that duplicated the prefix check.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* refactor(native-chat): bound phone rows by size alone; final tidy

Rows the journal already caps at a 16 KiB head gain little from a second,
display-shaped cut, and the diff check behind it passed a character count as
a line count. A row is now held to its frame share by shortening alone, and
native-chat-diff is back to main's.

Unexports file-local bounds, fixes a capability-era test comment, and adds
tests for string shortening and a phone's stop-note refresh.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(native-chat): assert a phone's stop-note refresh pages

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(native-chat): run the bounded-frames suite in the Node runtime project

It opens a real SQLite journal.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(native-chat): bound legacy dispatch reasons on read

Rows written before the write-side bound carry a provider's full error text,
which every page and phone frame attaches with its submission.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-10-13 13:39:34 -04:00
Jinwoo Hong 2b62a3db1a fix(mobile): drop the download bar glide so it always matches the MB counter (#28089)
The OTA download bar no longer glides: its width is bytes received over the total, the same number the MB caption shows, so the two always agree.

On a fast local link the screen still only re-renders near the end of the download, because decrypting replies keeps the JS thread busy. The glide then started late and was cut off by "Opening workspace", leaving a ~10% bar next to "8.0 of 8.1 MB". At normal speeds, renders land every 100-250 ms since the native hashing in #28056, so the glide added only lag. Verified on the iOS simulator: at 1 MB/s bar and caption agree within 1 point in every screenshot, and uncapped they go from empty to full together.
2026-10-12 18:05:19 -04:00
Jinwoo Hong 974b78d4e3 feat(mobile): show the OTA download as a progress bar that renders while downloading (#28056)
The OTA download screen now shows a progress bar over bytes received, labelled "Downloading the latest app" with a "from <host> · X.X of Y.Y MB" caption, instead of a spinner with raw file and byte counts. Related offline and failure copy says "the latest app" instead of "the workspace".

The bar barely moved before because React never committed progress during the download. SHA-256 of each asset ran in JS (3.8 s of a 5.7 s download), and React Native's scheduler runs queued native callbacks ahead of normal-priority renders, forcing a render only after 5 s. Assets are now hashed with expo-crypto's native digest (~5 ms total), so progress renders every 100–250 ms at 1 MB/s. Verified on an Android emulator and the iOS simulator.

The bar glides between bursts of progress (moving average of burst gaps, clamped 200–2000 ms) and is ShellWaitingFrame's indicator, so the label keeps the same position as on "Opening workspace". On an uncapped local link, decryption still keeps the JS thread busy and the bar can jump; that is left for a follow-up.
2026-10-12 17:35:20 -04:00
Jinwoo Hong 7cb30b7f8f feat(mobile): let the OTA page send extra top-level request fields (#27815)
* feat(mobile): let the OTA page send extra top-level request fields

A page request could only carry method, params and three fixed options, so any new
top-level wire field needed a new app binary. Requests and subscribes now carry an
optional verbatim `envelope`, gated by a new `rpcEnvelope` grant; nothing sets it yet.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* refactor(mobile): one envelope merge seam per transport

Review follow-up: the direct tracker and registry now hand frames to a single
sendFrame that adds the credential and envelope, like the relay; page refusals
use one fixed code per error class and an envelope-only options bag posts no options.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): refuse a non-object envelope before treating it as empty

null threw synchronously and [] or a Date read as empty and was dropped; the schema
now runs first. The tracker tests settle their requests instead of leaving timers.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* refactor(mobile): type the direct socket's request as the wire request

The typecheck now proves every direct request carries the transport fields, and the
native-verb fence says why it drops an envelope. No behaviour change.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-10-12 04:02:25 -04:00
Neil 11cf21e492 [CI] Remove duplicate terminal backlog test replays (#27503) 2026-10-11 08:46:46 -07:00
Jinwoo Hong 6013d093e2 fix(mobile): one host sidebar on iPad, with the page owning the host area (#26761)
* feat(mobile): let the page own the wide host area, one sidebar only

On wide layouts a detail route's page session ran app/h/_layout.tsx inside
the detail pane, judged itself wide, and drew a second host sidebar beside
the native one.

- The page draws the host sidebar only when init carries the new optional
  `ownsHostArea` fact, never from its own width.
- The page manifest's `/h/[hostId]` route declares `canOwnHostArea`.
- On wide layouts the native host route mounts one host-area page session.
  A declaring bundle serves it with every served route's grants, so hops
  stay in the page, and the native layout drops its sidebar. Without the
  declaration, or without a bundle, the session answers native-route and
  every wide detail route stays native.
- The owner is recorded per host so the layout and switches agree and a
  relaunch does not flash.

Claude-Session: lane-ipad-page-owns-host

* fix(mobile): decide the wide host area per session, without a stored owner

Round 1 kept a per-host owner record that the native layout trusted. When
the record said the page owned the area, a detail route opened natively
(deep link, notification, handoff, narrow-to-wide with a detail pushed)
showed no sidebar at all.

- Drop the record. A wide session decides from the manifest it already
  read: it is served only when the host route declares canOwnHostArea,
  and it rebuilds when the layout class changes.
- The native layout hides its sidebar only on the host route, while that
  host's page session is serving, as the shell screen reports through a
  layout context. Detail routes keep the native sidebar, and a page there
  draws none, so there is exactly one.
- Phones read and render nothing new.
- The host-area page can read and write every workspace-scoped key of its
  host (chat tabs, live-input handles), as a per-workspace session does.

Claude-Session: lane-ipad-page-owns-host

* test(mobile): type the layout-class doubles rather than assert them

Claude-Session: lane-ipad-page-owns-host

* fix(mobile): keep detail pages through a layout flip, guard capped keys

- A wide/narrow flip rebuilds a session only on the host route or under a
  bundle without canOwnHostArea, where the answer changes. A detail page
  under a declaring bundle keeps its document, scroll and draft.
- In the host-area page, a workspace key the 32-entry init cap leaves out
  is named in storageOversize, so the page cannot replace the device's
  chat-tab list from a default. The init frame bounds that list to the
  page's schema; the host still refuses every key in the full list.
- A leaving host-area session clears only its own report.

Claude-Session: lane-ipad-page-owns-host

* refactor(mobile): host-area ownership from the reducer, storage read on demand

- The serving report is a focus-scoped boolean, so a host pushed over
  another no longer leaves the first one's area unclaimed after the pop.
- The reducer takes `layout-changed {wide}`: it keeps the session when the
  route view is unchanged and restarts it otherwise; `routeViewOf` returns
  `ownsHostArea`.
- One web override, `usePageOwnsHostArea`; the native rule is inline in the
  host layout.
- A host-area page reads a host workspace key from the shell on demand
  (`native.storage.read`, granted to that session only) instead of init
  enumerating the store.

Claude-Session: lane-ipad-page-owns-host

* fix(mobile): host-area page reads back its own workspace-key writes

A page read right after a page write of the same key is answered from the
page's own value, not from a shell that may not have the notify yet. A
refused read crosses as an error, never as an unset key.

Claude-Session: lane-ipad-page-owns-host

* refactor(mobile): read-your-writes from the shell's mirror, final review fixes

- The shell answers a workspace-key read from the mirror, which every
  shell-side write updates synchronously (removals held as known-absent),
  and reads the store only on a miss. The page shim's written-keys set
  goes.
- `onLayoutChanged` compares the route view field by field; the session
  is created with its layout and the layout effect fires on `wide` alone.
- The device verb dispatcher's type excludes the storage read.
- The sidebar matrix mounts the real shell screen to decide the report.

Claude-Session: lane-ipad-page-owns-host

* feat(mobile): host-area page takes the whole screen from the download on

The native sidebar steps aside once the manifest says the page owns the
host area: fetching, activating, ready and the failure screens after them
are full screen, as on a phone. It stays while the host is being checked.

Claude-Session: lane-ipad-page-owns-host

* fix(mobile): host-area workspace-key read survives a write during it

On a mirror miss the read went through the mirror's hydrate, which drops
its answer when any mirrored write lands meanwhile, so a stored key read
as unset right after a relaunch. A miss now reads the store directly, and
a store failure rejects rather than reading as unset.

Claude-Session: lane-ipad-page-owns-host

* fix(mobile): a wide detail session hands the host route to the shell

A session opened on its own under a page that can own the host area
kept a hop to the host route in-page (a missing worktree's notice, or
leaving the session), drawing a second host list beside the native
sidebar. Its pairs now leave the host route out, so the hop goes to the
shell, which opens the area-owning session. Phones keep the hop in-page.

Claude-Session: lane-ipad-page-owns-host

* test(mobile): a phone mount's layout dispatch leaves the session alone

Claude-Session: lane-ipad-page-owns-host

* fix(mobile): leaving a wide detail session returns to the host area

The leave-session chevron and the missing-worktree bounce hand the host
route to the shell on a wide layout, which pushed it: a second host-area
session over the session, with Back reopening the session. On a wide
layout the shell now dismisses to this host's route instead, taking the
href's params, or replaces the session when nothing is below it. Phones
still push.

Claude-Session: lane-ipad-page-owns-host

* fix(mobile): a narrow-wide flip restarts a detail session's page

The route view comparison now counts the paired routes, so a detail
session under a page that can own the host area restarts on a flip
instead of keeping the narrow pairs, whose host-route pair let the page
draw a second host list beside the native sidebar.

Claude-Session: lane-ipad-page-owns-host
2026-10-09 14:57:07 -04:00