98 Commits
Author SHA1 Message Date
NeilandKelvin Amoaba 5d61e00c6d build(mobile): upgrade to Expo SDK 58 and React Native 0.88 (#37935)
* build(mobile): upgrade to Expo SDK 58 and React Native 0.88

Expo 58 drops Zod from @expo/cli, which clears the last Zod 3 advisory
(GHSA-mrhx-c7xc-m3w5) in mobile along with its local patch and override.

- Expo 55 -> 58, React Native 0.83 -> 0.88, React 19.3, gesture-handler 3,
  webview 14, reanimated 4.7, worklets 0.13 (SDK 58 bundled versions,
  pinned below today's batch to respect minimumReleaseAge).
- Port the react-native and react-native-webview patches unchanged; drop
  expo-constants, expo-notifications, @expo/cli, expo-router, zod and
  query-string patches plus the zod/query-string/xcode>uuid overrides,
  which upstream fixed or no longer need.
- iOS minimum rises to 16.4 (Expo 58 / RN 0.88 requirement).
- Migrate to RN 0.88 strict types: instance ref types, NamedStyles,
  AppState.currentState, list header/footer nulls.
- Replace removed runtime APIs: InteractionManager -> idle callback with a
  macrotask fallback; StyleSheet.absoluteFillObject -> absoluteFill;
  FileHandle.readBytes -> readBytesSync (it became async).
- Resolve react-native/asset-registry for shared assets in Metro and alias
  the old registry path for react-native-svg in the desktop mobile-web bundle.
- Reduce the iOS scene plugin to the reconnect override; Expo 58's template
  now ships the scene delegate itself.

* fix(mobile): bridge react-native-svg web imports and libsodium runtime for RN 0.88

- Share react-native-svg web shims between the mobile-web build and its render
  checks: map the old asset-registry path to react-native-web's registry and stub
  the Android-only touch-sound TurboModule.
- Patch react-native-libsodium 1.7.0 to read the JSI runtime from the bridge proxy;
  RN 0.88 removed RCTCxxBridge.runtime.

* fix(mobile): use Expo 58's legacy promise resolver for audio permissions

* fix(mobile): skip null handles in the RPC recording timer shims for RN 0.88 types

* style(mobile): format MobilePRSidebar after rebase

* fix(mobile): name RN 0.88 text input refs for declarations; allow queued back in stack render check

- Type text input refs with an exported interface so declaration emit can name hook results
  (RN 0.88 aliases TextInputInstance to an unexported class).
- Expo Router 58 drains back() after a commit; check the interrupted push's exit from the
  frame it starts, bounded to three frames of lag.

* chore(mobile): justify the text input handle interface for React Doctor

* chore(mobile): use a nameable intersection alias for text input refs

* fix(mobile): use sync expo-file-system writes, copies and moves on Expo 58

Expo 58 made File.write/copy/move and FileHandle.writeBytes return promises.
Our callers never awaited them, so failures escaped their try/catch and the
store's tmp-then-move and PDF writes could race their close or rename.

* build(mobile): raise the page bundle budget to 10 MiB for Expo 58

Expo 58 adds about 310 KB to the page: expo-router (+258 KB, now carrying
React Navigation, net about +110 KB) and Reanimated 4.7 (+152 KB). Main had
85 KB of headroom left. 10 MiB still stays within the 10 MiB per-asset contract.

* fix(mobile): drop the window-metrics seed RN 0.88 removed from Android

RN 0.88 no longer holds window metrics: DeviceInfoModule reads the activity's
WindowMetrics on demand and setWindowDisplayMetrics is gone, so MainActivity
no longer compiled. Layout scale still comes from the screen metrics we reseed.

---------

Co-authored-by: Kelvin Amoaba <97001695+AmoabaKelvin@users.noreply.github.com>
2026-11-11 23:30:02 +00:00
c9ef81809f test: remove 198 low-value test registrations (#41175)
* test: remove 198 low-value test registrations

* ci: fail over unavailable Ubuntu package mirrors (#41174)

* ci: fail over unavailable Ubuntu package mirrors

* fix(ci): recover package retries across headless jobs

* fix(ci): allow complete fallback package index downloads

* fix(ci): recover dpkg locks and bound WebKit setup retries

* fix(ci): run apt deadlines inside sudo sessions

* fix(ci): invoke installed Playwright without root pnpm

* fix(ci): allow large fallback dependency downloads to finish

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
2026-11-11 16:59:03 +00:00
Neil 920a83889c refactor: move browser contracts and publication to defining owners (#40209)
* refactor: move atomic publication to its filesystem owner

* refactor: group shared browser definitions by feature

* refactor: bind proxy process and session contracts to defining owners

* refactor: consolidate direct session contract type imports

* refactor: retain atomic publication test timing under moved owner

* fix(benchmark): resolve historical tunnel framing source before loading
2026-11-09 00:45:45 -08:00
OrcaWinandOrcaWin a489ef5ba3 test: reconcile approved test removals with current main (#40176)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-11-08 23:19:51 -08:00
Neil 19043a3d76 refactor(mobile): organize pairing, session, RPC and host owners (#39846)
* refactor(mobile): give relay and encryption concrete owners

* refactor(mobile): organize session and review feature owners

* refactor(mobile): organize RPC and host ownership

* test(mobile): point render fixtures at concrete owners

* refactor(mobile): use explicit type-only owner imports
2026-11-08 07:19:05 -08:00
OrcaWinandOrcaWin 92ef9440db test: remove 221 low-value cloud, mobile and journal tests (#39449)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-11-07 02:12:41 -08:00
OrcaWinandOrcaWin ff416c2f6c test: remove 181 low-value mobile, CI and runtime tests (#39351)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-11-06 23:39:35 -08:00
Brennan Benson 4cccf7b04a perf(native-chat): reuse unchanged phone transcript rows (#38956)
* perf(native-chat): reuse unchanged phone transcript rows

* fix(native-chat): narrow retained projection output reuse

* refactor(native-chat): isolate phone session status type

* Document source comparison key safety

* Use last-member array accessor

* test(native-chat): check phone captures across source boundaries

* ci(mobile): route recorded transcript decoder inputs
2026-11-06 10:14:07 -08:00
Kelvin Amoaba 6bd548ba5c feat(mobile): render math in chat and PR descriptions, with GitHub's line-count table (#38682)
* feat(mobile): render math in Markdown with desktop chat's delimiter rules

Move the price, citation, closer and display rules plus the KaTeX settings
to src/shared/markdown-math-rules and point desktop chat at them. Mobile's
markdown-it gets the same grammar, and pages typeset formulas with KaTeX,
whose fonts ship as page assets. Mermaid's engine leaves katex external so
both share one katex chunk.

* build(mobile): count KaTeX fonts in the page budget and keep math to one chunk

Only the licensed terminal font keeps its own allowance; KaTeX's fonts
count toward the budget like code. The typesetter imports katex itself,
sharing mermaid's chunk, so math adds one chunk instead of four.

* fix(mobile): draw the PR line-count table's +/− glyphs at GitHub's size

github.com draws \Huge at \Large (1.44em), so the line-count table's
glyphs sit inline at text height. Cap the larger size commands the same
way, and let inline math take its height from KaTeX's struts so rows keep
normal height and numbers stay on the text baseline.

* test(mobile): pin math delimiters to desktop chat's results

* refactor(mobile): drop type assertions from the math tree nodes

* chore(mobile): register the math mdast nodes beside <details>

* fix(mobile): read bracket math only in chat, keep math parsing linear

GitHub text (PRs, Tasks, files) reads $ and $$ only, so \[Bug\] stays a
bracket; chat rows opt into desktop chat's \( and \[ too. Remember failed
block scans and skip dollar-run tails so hostile input stays linear, leave
unfinished math out of remend like desktop, bound the formula cache by bytes,
retry a failed KaTeX load, and count symbol commands in table widths.

* fix(mobile): copy formulas as TeX and keep GitHub's unclosed $$ as text

Share desktop's selection-to-TeX reader so a copied selection carries each
formula once, as its source. An unclosed $$ in GitHub text stays a
paragraph, chat's math delimiters reach HTML-derived Markdown and summaries,
failed block scans only skip openers inside the lines they read, unfinished
math anywhere in the streaming tail skips remend, and table widths no
longer spread every command into Math.max.

* refactor(mobile): share the formula copy handler and cap sizes only in GitHub text

Desktop and mobile copy through one shared handler. GitHub surfaces draw
\Huge as github.com does while chat keeps desktop chat's sizes, PR and file
text skip bracket scanning entirely, and the dollar-run scan and token
alias live in one place.

* chore(mobile): stop tracking the generated KaTeX stylesheet

#38611 committed it by mistake; postinstall writes it and .gitignore now
lists it.

* fix(mobile): follow desktop chat's new dollar rule through the shared module

#38651 changed desktop chat's inline $ rule (shell variables and prices stay
text, a single $ can span lines, $5+2$ is math). Move those checks into
src/shared/markdown-math-rules, point desktop at them, and give mobile's
markdown-it rule the same behaviour.
2026-11-06 00:53:54 +00:00
OrcaWinandOrcaWin 8e46e2066d test: remove 98 low-value mobile tests and 15 entire suites (#38637)
* test: remove 98 low-value mobile tests

* test: drop stale typecheck allowance for removed browser budget test

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-11-05 11:45:58 -08:00
Neil 9bd6272fdf refactor(tooling): organize build, CI, and test owners (#38130)
* refactor(tooling): organize test runtime and maintenance owners

* test(tooling): preserve relocated runtime fixture dependency edges

* refactor(tooling): preserve xterm routing owner checks

* ci(tooling): include routing owner in sparse checkout

* test(tooling): preserve copied workflow dependency closures

* fix(tooling): normalize hashed JSON assets in release parity

* fix(tooling): distinguish asset aliases from literal references

* fix(tooling): frame normalized asset reference payloads

* fix(tooling): normalize decoded JSON asset references

* refactor(tooling): organize build, CI and release owners

* fix(tooling): align moved generator and workflow path contracts

* fix(tooling): update generated plugin bundle owner banners

* test(tooling): create relocated mobile verifier fixture directories

* test(tooling): copy headless detector dependency closure

* test(tooling): align native batch policy fixtures with owner path

* fix(tooling): link moved reliability command to historical evidence

* fix(tooling): preserve nested sandbox owners and canonical guide source

* fix(tooling): target release version owners in checkout fixture
2026-11-04 21:08:19 -08:00
OrcaWinandOrcaWin b17646a5d9 test: remove 2191 low-value test declarations (#38113)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-11-04 03:40:25 -08:00
OrcaWinandOrcaWin 6792990b83 test: remove 1015 low-value test declarations (#38087)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-11-04 01:39:49 -08:00
OrcaWinandOrcaWin e98c6a439a test: remove 2816 low-value test declarations (#38030)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-11-03 21:45:29 -08:00
Neil 926f8bad13 refactor: organize terminal and runtime contract owners (#38011) 2026-11-03 19:56:34 -08:00
OrcaWinandOrcaWin d5f2d9269f test: remove 454 low-value test declarations and copied benchmarks (#38014)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-11-03 19:32:19 -08:00
Neil 9c7fe9793e fix(deps): backport minimatch, glob, and deepmerge security fixes (#37981)
Backport upstream matching and abort fixes plus safe deepmerge property handling across the independent workspaces. Includes installed-package regression tests and audit-tool fixture repair. Agent-reviewed at 83ca381d519e6247d7c1b3f0a52ce2907a9f9a19; CI aggregate passed. Version-based scanner alerts remain open pending documented disposition or compatible upstream releases.
2026-11-03 17:13:11 -08:00
Neil 2c03c4b989 refactor: organize agent status and lifecycle owners (#37961) 2026-11-03 16:36:54 -08:00
Jinwoo Hong 7b45308ee8 fix(mobile): type desktop replies from shared contracts and keep phone list facts beside the desktop row (#37917)
* refactor(mobile): keep phone list facts beside the desktop's worktree row

The phone wrote its own list facts (lineage depth, host labels, list keys
and so on) onto the worktree.ps row object it got from the desktop, so a
desktop field with one of those names would be read as the phone's own.

List items are now { row, view }: the row is the desktop's, typed from the
shared RuntimeWorktreePsSummary contract, and view holds what the phone
works out. Host labels are a lookup beside the rows instead of fields on
them. A type test fails if the desktop row gains a view field's name or a
type the phone row would not accept, and a contract test feeds rows from
the desktop's own worktree.ps builder through the phone list.

* fix(mobile): type desktop replies from shared contracts and reject unchecked rows (#37923)

* wip: contract guards

* fix(mobile): type desktop replies from the shared contracts and ban unchecked z.custom

Session tabs, the createSupport reply and native-chat frames were typed by
hand on the phone, so a desktop change could leave the phone compiling
against a stale shape. They are now Pick/Partial of shared types; the
createSupport reply and native-chat transcript frames gain shared types the
desktop uses too, which also gives the phone the 'chat-program' reason it
was missing. Type tests fail when a desktop tab gains a phone-only name or
a field type the phone would not accept.

Catalog rows were admitted with z.custom<Worktree>() and no check. They now
need a string worktreeId, and an ast-grep rule rejects a z.custom with no
check anywhere in mobile/src.

* test(mobile): the desktop contract check also catches a null the phone would not accept
2026-11-03 18:33:05 -05:00
Brennan Benson a56e6de883 Keep phone fork pull requests on their selected repository (#36762)
* Keep opened GitHub work items bound to their exact repository

* Bind GitHub work item ownership through consumers and dispatch

* Keep empty mention lists from consuming selection keys

* Preserve GitHub action and consumer ownership across resource changes

* Exercise repository account updates through their production store action

* Exercise account publication and cache settlement through GitHub consumers

* Simplify fork PR contract verification

* Make contract verification imports and clocks deterministic

* Correct mobile task test fixtures and model types

* Correct task recording fixtures and CI types

* Correct task fixture types and static checks

* Consolidate replay test RPC imports

* Separate GitHub metadata imports from Linear hooks

* Scope mobile PR sidebar reads to the selected resource

* Separate desktop GitHub changes from phone updates

* Preserve complete phone GitHub repository identity updates

* Fix relay authority probe census across topology cases

* Repair repository owner proof fixtures and compiler allocation

* Preserve repository row observation in host lookup

* Seed metadata fixture before server-side store acquisition

* Preserve nullable fixture settings and discover mixed test types

* Refetch phone reviewer list after a connection change

A disconnect or session change while the reviewer list loaded left the drawer showing an empty list until it was reopened. The drawer now shows its loading state while disconnected and fetches the same repository again once connected, still ignoring the cancelled load's result. Also merges a duplicate type import that failed mobile lint.

* Keep phone reviewer rows through connection changes

The reviewer picker now owns one query per open visit of a client, worktree and repository. A connection or session change retires the in-flight request at once but keeps the people already loaded; reconnecting refreshes them in the background. Without loaded people the drawer shows its spinner, or the existing connection explanation once an outage is revealed, and never a false empty list. Closing, retargeting or reopening starts a fresh visit that no earlier reply can paint. Also wraps the synchronous bridge teardown steps so the cleanup loop awaits only promises.

* Keep the viewed phone PR through connection changes

A relay/direct switch or reconnect selected a fresh, hidden PR observation in Source Control and Diff Review, so the next render unmounted the PR, closed the reviewer picker and erased an unsent comment. The PR observation is now scoped to the client, worktree and branch. One connection listener retires the in-flight reads when the connection changes and repeats the query the screen already asked for once connected, so accepted content stays mounted and refreshes in place. A failed background refresh keeps the accepted result. Task PR files likewise keep their loaded contents and expansion across a connection loss and read an interrupted file again on reconnect.

* Show a failed PR refresh over kept content; recover task files on every connection change

A failed refresh of an accepted PR (or no-PR) result now keeps the content and shows the existing "Failed to refresh pull request." line with Retry above it, whoever started the read, until the next good read. Task PR file reads are retired and re-read from the task selection's own connection listener, so a reconnect batched into one render or a connection switch at the same state also recovers an interrupted file. The reviewer-picker contract test leaves the React Native test program for now; its checked owner follows.

* Typecheck the phone/host contract tests through emitted declarations

The reviewer-picker contract test and the recorded request params test import real desktop sources, which do not typecheck under the phone's React Native libraries. Both source graphs now emit declarations under their own compiler settings, and a strict Node program checks the two test bodies against them; Vitest still runs them against the original sources through path aliases. The tests typecheck ratchet runs this program and fails unless both bodies are in it and it passes, the CI test runner checks that Bun ran and passed both, and the Mobile workflow finishes compiler work before starting tests. The six other excluded tests stay runtime-only and are labelled that way.

* Name the types phone declaration emission needs

Declaration emission for the phone/host contract program could not name the i18n instance's type or the buffered terminal draft send type; annotate the one and export the other. Also types the drawer mock's children in the Source Control scenario so the React Native test program checks it.

* Stub every icon in the Source Control scenario

Source Control modules read icons at import time, so the scenario's fixed icon list failed to load the file under Bun.

* Give the recorded params check's stand-in method a permission

The recorded request params test is now typechecked and its stand-in method lacked the permission every RPC method declares; parsing never reads it.

* Use the real composer styles in the Source Control scenario

Source Control's list styles read composer style constants at import time, so stubbing that module failed the file under Bun.

* Mock the PR sidebar hooks without loading their native modules

Only the PR sidebar reads these hooks, so the Source Control scenario's mocks need not load the real modules, whose imports reach Expo native code that does not run under Vitest.

* Count recording-internal observations apart from recorded ordinals

The scripted transport runs the real request tracker, and its connection wait, id allocation, completion, late-reply and terminal observations each took the shared write ordinal that requests, frames and effects record. Every recording's ordinals shifted although the same frames went out in the same order. Those observations now use their own counter, keeping their association with the native request, so recorded ordinals match the existing recordings again.

* Record a cleanup checkpoint only when teardown adds effects

Comparing the whole snapshot before and after teardown added a cleanup checkpoint whenever a pending request settled at dispose, which changed about 166 recordings without new evidence. Restore the condition the recordings were made with: teardown effects alone add the checkpoint.

* Run the bridged recording corpus on Node

Its goldens hold V8's TypeError wording, which the Bun pass reports differently, so ten exact error comparisons failed only on engine text. It now runs with family-recordings on Node; the comparison stays exact.

* Bind the probe test's fresh status.get by its actual child

* Edit the GitLab status scenario's GitHub issue only after opening it

* Type the Source Control scenario's composer adapters with the real component props

* Mock the Source Control scenario's icons and protocol gates without loading the real modules

* Give the PR sidebar probe test the shared typed fake client

* Select the GitLab status scenario's GitHub issue as a typed task item

* Declare the buffered terminal draft send as an exported type

* Run the Source Control scenario's focus effect only when its callback changes, as the router does

The router mock ran the loaders' focus effect after every commit, so each status read re-rendered and re-ran it. Also mock AppState for the comments section and stop act from awaiting held PR reloads.

* Re-record the GitHub and Tasks goldens for the selected-repository requests

Re-recorded with pnpm --dir mobile rpc:record for the 71 scenario IDs: 66 changed and 5 added for the exact-target status.get probe, 0 removed. The goldens now carry the selected repository on GitHub requests, the exact-target probe before GitHub operations, the skipped hidden metadata loads and the new item-detail failure copy.

* Declare the probed GitHub operations' params in the order the app sends them

The bridge probe test compares the forwarded frame byte for byte with the declared params, so tk-item-detail-github, tk-item-status-gitlab and tk-project-row-files-merge now declare the exact key order their goldens record. Values are unchanged, and completion matching ignores key order.

* Close the native connection before the page in recordings, and make late replies and inherited timers explicit

Recording teardown now closes the native client, flushes its rejection through the bridge, then closes the page, so page callers see the relayed native close. Sibling rejections name the stable logical site in every mode. Replies scripted after a timeout or disconnect are explicit late replies that must resolve nothing. Four product timers inherited from main are declared by producer, scenario, owner and delay and still reported; every other run keeps zero residual timers. The bridge probe oracle for tk-item-status-gitlab expects the published deadline failure and the exact retained issue.

* Judge unchanged bridged recordings as main does and changed ones by their own outcomes

Unchanged producers follow main's order again: identical earns identical, History keeps its strict path, and other divergences take a legacy label from the reply-meta counterfactual's typed evidence. A bounded guard pins zero unclassified, small-class membership and the branch-base tally less each named moved or removed producer; eight moved producers without a base pin are named as old-class-pending until their baseline replay. The re-recorded producers are judged directly in both page modes against their reviewed goldens, with only the page's refusal of the injected absent result applied, or by their exact strict own-undefined stop, and are pinned as bridge contracts. Bridge-only preludes are observed after their action, and the pending invocation's relayed native close is read from its own page promise. Every producer must be routed exactly once.

* Say what to do when the GitHub server check fails, and share one deadline with the action

The exact-target check runs before a GitHub action is sent, so a timeout or lost connection now shows the existing 'The Orca server connection changed. Try again.' and a refused or unreadable answer shows the existing 'Update Orca on the executing server' message, instead of raw text naming status.get. The check's own connection-changed error passes through. On native, the checks and the action share one 30-second budget, so a dead server fails in 30 seconds instead of 60.

* Pass the scenario manifest to the changed-producer comparison

* Correct the bridged corpus repairs from their first full run

The re-recorded producers no longer stop on the strict own-undefined matcher and all replay in bridge mode, so they are all judged directly; the page's refusal of the GitHub server check now expects the connection-changed copy. Unchanged legacy labels read their first difference from the replay without injected metadata. The sibling timeout and each interruption, including the cutover, end the requests the goldens record as ended, so those replies are explicit late replies. New producers in pinned families get their bridge-contract pins, and an unexpected timer or changed outcome now names its scenario and values.

* Restore six unchanged scenarios to their single own-undefined steps

tw-smart-search-all-providers, tw-smart-search-linear-listed, tk-list-gitlab-items, review-mark-reviewed-persists, review-mark-reviewed-rolls-back and session-create-terminal-without-active-tab had their own-undefined steps split into native and bridge copies, so the page replay no longer reached the strict own-undefined stop and these unchanged producers left their baseline labels. Their steps are the branch-base steps again; native recordings are unchanged.

* Fill the eight moved producers' base classes from the admitted baseline replay

pr-read-surface was identical and the seven github.pr-read matrices were result-absent-settlement at the branch base, so the unchanged tally is exact again with no pending members.

* Deliver the sibling disconnect's trailing replies as late replies too

A disconnect rejects preflight.check, linear.status and jira.status before the suffix replays their replies, exactly as the sibling timeout does, so those declared replies are late in both variants; the cutover stays strict.

* Expect the page's refusal in the two Linear producers' result-absent checkpoints

Natively the injected { ok: true } reads as an empty result, so the Linear detail and status update finish; the page refuses that frame and the screen shows the refusal instead. Each producer now has one finite expectation for that checkpoint, guarded on the reviewed golden's final effects, and everything else must still equal the golden.

* Share one deadline between the shell's server check and the GitHub action

The shell gave the exact-target check and the forwarded action each the page's full timeout unless the page asked otherwise, so a dead server took twice as long to fail on the hosted page as on native. The two legs now always share the page's one deadline; the forwarded options are unchanged.

* Deliver the sibling client cutover's trailing replies as late replies too

* Build the changed producers' page expectations from one finite table before comparing

* Re-record the three exact-target check goldens for the mapped copy

The GitHub check now shows the update-server or connection-changed copy instead of raw reply text; the item detail's empty-message cases now also record their detailError effect. Recorded on the admitted run (check-3949 Job 1).

* Declare the traced inherited timers per inner scenario and recording mode

Each declaration now names the modes where its inner scenario leaves the timer. Adds the inner scenarios traced in check-3949/qa-report-trace.md Job 2, with the ruled delays and owner chains: the reveal's 300 ms first-poll timer for all ten reply partitions of tabs.list-1 and tabs.activate-1 in both modes, and the pr-triage 20,000 ms reconnect waiter for transport-rejection and transport-rejection-no-message in both modes and result-absent through the page only. Exact counts, owners and delays stay enforced.

* Expect the compiler wait before Mobile verify's tests and only the fastlane tools after

* Call recorded timer callbacks through a typed signature

* Show the reviewer picker's no-rows wait with the screen header's connection verdict

The drawer classified only its client's state and counters, so during relay recovery it flipped between Disconnected and Connecting on every dial and never showed relay or pairing verdicts. The source control and diff review screens now pass the host's own verdict inputs (reconnect attempts, last connect, relay recovery status), re-read on host state and path changes, so the drawer shows the same steady verdict as the header.

* Say a timed-out GitHub target check timed out, on native and in the shell

A server check that ran out its own deadline on a connection that never changed now reads "Timed out confirming the executing Orca server.", as the shell's budget already did, and the native shared budget running out uses the same copy. A lost or changed connection still reads "The Orca server connection changed. Try again."

* Name the paired desktop when it is the build missing the exact-target check

When the phone's paired desktop relays to an updated executing server but lacks the exact-target capability itself, the edit now says "Update Orca desktop" instead of pointing at the executing server. An old executing server still gets the executing-server copy.

* Preserve GitHub detail argument types and optional capabilities

* Say what updating the paired desktop enables, as the executing-server copy does

* Drop the tab-reveal timer declarations now that main removed the reveal operation

* Let keyed details queries own pending read replacement

* Repair GitHub RPC and relay test fixture boundaries

* Own the new PR tab's caches, sheets, confirmations and polls by the complete PR

Fork #N and upstream #N were one owner to the tab: check details, open sheets and confirmations keyed by worktree and number survived a repository change, so a fork draft or decision could act on upstream and fork check data could show for upstream. Each is now owned by the PR incarnation (client, worktree, branch and full resource key), selected during render, so a departure closes old requests without retargeting them, A -> B -> A starts fresh, an outgoing sheet renders nothing and sends nothing, a late save closes only its own request, and a departed PR's poll neither blocks nor re-arms its successor.

* Load the reviewers sheet's people through the selected repository's resource-owned query

The new sheet read people once on the first search with no repository, connection recovery or retry, so a fork PR listed the wrong repository and a search while reconnecting stuck on an error. The first search of an open sheet now demands the selected repository's list through the exact-target helper; the query reads only while connected, retires its attempt on every real state or generation change, recovers on reconnect, keeps accepted rows and search through refreshes, and shows the header's own calm connection verdict while it has no candidates. Closing or a PR departure ends the visit; reopening starts fresh.

* Mark a failed PR discovery from every caller, polls included, and keep a poll's checks-only failure quiet

Main's poller made a failed background discovery silent, narrowing the ruled all-caller contract. Any current failed discovery over an accepted PR or no-PR result now keeps it and shows the one refresh-failed row with Retry, and the poll still backs off on 'failed'. A poll whose discovery succeeded but whose checks read failed for the same complete PR lands its discovery (clearing an older warning) and keeps the prior checks, reporting 'failed' once; equal numbers in another repository no longer count as the same PR. A viewed-file update from a departed observation changes nothing.

* Send the phone's addressed details reads with the server's read intent where it is advertised

github.workItemDetails accepts optional canonical:false (ordinary) / true (Refresh), advertised by github.exact-details-read-intent.v1; missing intent means ordinary. The phone's addressed details reads (PR tab and Tasks) now pass that intent through the existing exact-target preparation: a host advertising it gets canonical:false for ordinary loads, recovery and polls and canonical:true for the user's explicit Refresh; a canonical-only host gets ordinary reads without the owner and Refresh with the owner only; hosts with neither keep today's params, and an explicit Refresh there is refused as unsupported. Refresh intent travels only with the Refresh command, never as reconnect demand. The poll's checks-only settling moves into pr-sidebar-load-result to keep the controller within its size limit.

* Keep today's details params for a host with neither read-intent capability, Refresh included

* Drive the reviewer target contract through the new tab's reviewers sheet, first search included

* Cover a poll's discovery warning, its clearing, and a same-PR checks-only poll failure in the controller

* Prove the Source Control PR tab's demand, retained sheet and draft, failure line and fork/upstream request ownership on the new navigation

* Cover the details read intent the phone sends to explicit-intent, canonical-only and older hosts

* Fix the round's test typecheck and assertion findings: awaited controller loads, the diff review props, typed doubles

* Send Refresh's owner to a canonical-only server outside the exact-target check, as the desktop does

* Restore the base's mobile change-scope test, which the base merge dropped by mistake

* Prove the PR poller keeps each fork/upstream incarnation's poll, backoff and cadence its own

* Verify expected GitHub account at repository execution capture

* Give the Linear detail recording adapter the detail Refresh request its loader reads

* Keep the detail Refresh request ref out of the recorded owner projection, like the other ref

* Count cancelled GitHub probes against the desktop request cap main introduced

* Keep the comment tree out of the diff review controller test, as its drawers already are

* Restore the Source Control action icons the scenario rewrite dropped from its icon mock

* Supply the settled-list setter main added to the Tasks list fixture

* Preserve captured GitHub account expectations through client dispatch

* Re-record the Linear team-context goldens for this PR's actionItem owner effect

* Retain observed runtime status after switching environments

* Fix account contract fixture types and RPC setup timing

* Restore the negative-capability switch probe assertion

* Align account fixtures with runtime identity and configured origin

* Consolidate details test imports

* Type the reviewer test's search stand-in locally so native declarations stay out of its Node-checked program

* Add the PR tab Retry label's English entry the localization check requires

* Restore the PR tab Retry label's translations in the other mobile locales

* Pass the phone write reread through the reviewer contract.

The sheet fixture omitted the required reread, so the phone typecheck failed. Desktop settlement tests that main already removed were still on this branch and pulled telemetry into the web typecheck.

* Declare the phone pull request refresh failure.

The sidebar already shows this sentence when a refresh fails. The English catalog omitted it, and the boot bundle still carried two strings no startup call needs.

* Let the phone sidebar tests subscribe to connection state.

The sidebar records the client's connection before it loads a pull request. These tests drive that through the screen's connection prop, so the stand-in client only has to accept the subscription.

* Justify the phone sidebar test client.

The changed-code check rejects a new cast unless the line says why it is sound. These tests still only subscribe and read connection state.

* Name the repository on the phone sidebar details fixture.

A same-numbered pull request in another repository is not this one. The loaded body has to name the repository the test selected.

* Keep phone review-thread feedback on its accepted detail revision

* Capture phone review actions from the current render

* Align phone recording fixtures with the integrated detail loader

* Keep provider-switch detail fixtures addressed to the selected item

* Preserve same-turn bridge refusal evidence across client call shapes

* Publish phone detail revisions through one synchronous owner

* Type the queued publication regression with its provider contract

* Conform phone regression and relay types to static analysis

* Trim duplicate phone RPC tests and focus recorded observations
2026-11-03 14:19:08 -08:00
Kelvin Amoaba ac99f1cb39 fix(emulator): list only the worktree's attached emulators, iOS and Android (#37127)
* fix(emulator): scope emulator list to the worktree's sessions

emulator list ignored --worktree and returned serve-sim's global iOS helper list. It now lists the session registry's attached sessions (iOS and Android) for the selected worktree, or every worktree's for --worktree all.

* fix(emulator): drop stopped devices before listing sessions

* fix(emulator): point the no-active hint at emulator devices

* test(emulator): assert list results, not serve-sim calls

* refactor(emulator): prune stopped devices inside the registry listing
2026-11-01 21:34:03 +00:00
Neil 5228bff55c refactor(mobile): group web bundle transfer owners (#37124) 2026-11-01 11:43:21 -08:00
Neil b2842603e4 refactor: finish shared native-chat model and transcript organization (#36843) 2026-10-31 20:19:28 -07:00
Kelvin Amoaba 36a9024a0b fix(mobile): draw Claude Code's ⏵⏵ mode marker in the Android terminal (#36516)
* fix(mobile): draw Claude Code's ⏵⏵ mode marker on Android terminals

Android ships no font with U+23F4-23F7, so the terminal drew boxes.
Bundle a 664-byte Noto Sans Symbols 2 subset, inline it in both
terminal hosts, append it to the Android font stack only, and load it
with the Nerd font before the WebGL atlas first paints.

* fix(mobile): spell the terminal font probe glyphs as escapes

* refactor(mobile): tidy terminal symbols font wiring
2026-10-31 01:16:24 +00:00
Brennan Benson f0d55e7319 Nest chat-UI orchestration workers under the agent that started them (#34601)
* Nest chat-UI orchestration workers under the agent that started them

Main's lineage projection only walked terminal panes, so workers running
in the chat UI never got a parent link and listed flat in the sidebar.
It now also projects chat workers, keyed by their visible chat row, and
resolves a chat creator or coordinator to that chat's visible row
instead of its handle or private mailbox pane key.

* Fix public worker lineage across assignment and row identities

* Complete terminal lineage provenance and bound active session selection

* Repair lineage test runtime boundaries and SQLite counters

* Batch public lineage verification after exact recency selection

* Use checked runtime values in lineage integration tests

* Document the trusted Dispatch SQL row boundary

* Keep lineage integration imports within their test programs

* Load native lineage fixtures before test execution

* test: complete lineage inputs in runtime and sidebar fixtures

* test: seed benchmark facts from active dispatches

* test: replace partial orchestration DB casts with typed fixtures

* test: complete stale-leaf mailbox fixture session input

* fix(test): render worker lineage fixture with production list
2026-10-28 16:06:38 -07:00
Blue DaydreamingandNeil 51d25013f1 fix(typecheck): report Bun version probe timeouts (#33762)
When the pinned Bun binary is installed but its version probe times out,
required typecheck callers (repo typecheck, CLI build, E2E and mobile
typecheck) now report the timeout with the executable path and budget
instead of claiming Bun is missing. Each caller suggests its own compiler
fallback. Optional Bun consumers keep returning null and falling back.

Fixes #33707

Co-authored-by: Neil <neil@stably.ai>
2026-10-28 07:18:52 -07:00
Brennan Benson fa5284aca2 feat: edit queued messages inline with the full chat composer (#31180)
* fix(native-chat): a refused queued message still blocks the queue when its card write fails

Every reader of a queued card goes through one derived view of the journal's dispatch facts,
so a card whose message was refused reads as returned even when its card-table update and heal
both failed. One decision and one writer store the derived state for every action.

* refactor(native-chat): keep each queued message in the chat's journal, not a second table

A queued message lived in its own SQL table, and a hook that may be skipped copied
each refusal from the journal into it. When that write failed, a refused card
vanished: the cards behind it sent, and its Send and Delete did nothing.

Each queued message is now a `queue` row in the chat's journal beside the
submission that hands it off and the dispatch row that answers it. The reducer
folds the card from those rows, so a refusal returns the card and holds the
cards behind it with nothing written, and replay reads the same. Every action
writes its own rows in one transaction. Cards an earlier build stored are read
through the same classification until they move: each action moves its own
card, and a bulk import runs once per open. A new epoch restates every card
still owed an answer in the transaction that deletes the old rows.

The settlement hook and its savepoint, the open-time heal, the drain's heal and
the derived-card view are gone. The published card list is unchanged.

* test(native-chat): adapt the queue tests to cards kept in the journal

Cards are written through the journal, so tests queue them with their body
alone and hand them off under their real fingerprint. Tests of the removed
settlement hook, heal and SQL store are deleted; the text-update spec now
covers the edit decision alone, and a test of an earlier build's stored holds
seeds the legacy table and reads the imported cards. Rolling over an epoch now
keeps a card whose hand-off was never handed over, so that test checks the
retried id never sends rather than that nothing sends.

* test(native-chat): a refused card holds the queue through rewind, a new epoch and a restart

One whole-flow test through the host: the refusal writes no card row, a rewind
that retires the refused hand-off and a rolled-over epoch keep the card
returned, a restart and Resume send nothing behind it, its own Send refused
again still holds the queue, and its Delete releases the next card in order.

* test(native-chat): an earlier build's queued cards move into the journal once, each read the same

One mixed-import test through the host. The first open's import fails and
moves nothing, yet every card reads as it will; Delete and Send each move only
their own card. The next open moves the rest in one transaction, writing a
refusal only where its hand-off is gone, nothing for answered cards, and leaving
an unreadable row on disk. A later command turn spends a moved command card, a
third open writes nothing, an old Delete replays from its receipt, and a chat
with only answered legacy rows clears them in a delete-only transaction.

* test(native-chat): adapt typechecked tests to cards kept in the journal

The row-kind table lists a queue row, the clear and legacy-import tests
match the current signatures, and the downgrade test keeps only the
leftover-send case: a queued card is now a newer-version journal row an
older build keeps read-only, and users don't downgrade.

* refactor(native-chat): drop the journal row writer's unused transaction hook

No caller passes one since the queue's savepoint bookkeeping left.

* test(native-chat): downgrade probes write rows at the version older builds read

The highest version this build reads is now 4, the queue row's. A newer
build's row in these probes is written at version 3 so the older build
meets its kind, not a newer version, and the Stop-event probe hands the
older build the rows as stored, since a read raises each row's version.

* test(native-chat): unit tests follow the queue row's version 4

The legacy-import test opens real SQLite, so it runs in the Node runtime
project. The newest version this build reads is now 4, and a read raises a
row's version to it, so the turn-lifecycle test no longer pins the
in-memory version (the written one stays pinned by journalRowSchemaVersion).

* fix(native-chat): one batch echoing two refused queued messages spends both

The per-row delivery check returned only the first waiting card an echo
proved delivered, so when one settlement carried echoes for two cards the
second stayed waiting and was sent again. It now returns every card the
row proves delivered, and the fold withdraws each; replay folds the same.

* fix(native-chat): a rewind moves an earlier build's cards into the journal first

While the open's import had failed, an earlier build's card that read as
refused through its hand-off lost that reading when an in-place rewind
retired the hand-off: it disappeared and the card behind it sent. The
rewind now adopts every legacy card ahead of its tombstones and deletes
their rows in its own transaction, as an epoch replacement already does,
so the fold latches each card's reading before its hand-off is gone.

* fix(native-chat): keep an earlier build's card whose hand-off is still unconfirmed

The import dropped a legacy card handed off as a send whose delivery was
unknown, though the fold keeps such a card linked and a later refusal
returns it. The import now keeps every card whose hand-off a later row can
still answer, through the same check the reducer folds dispatch rows by.

* fix(native-chat): one delivery echo withdraws at most one queued card

One provider echo proves one delivery, yet it withdrew every waiting card
with the same text whose hand-off was refused, so a second identical card
was lost. The fold now pairs each echo with one card: on its own row the
earliest-positioned match no other echo of that row took, and for a card
that waits again the earliest applied echo no card has taken. Paired echoes
live in the fold's state, so live and replay pair the same; readers of
cards not yet imported work on a copy.

* fix(native-chat): a queued message handed to the agent is sent again only on proof it never got it

Pressing Stop after Orca handed a queued or steered message to the agent,
before the agent confirmed it, put the card back in the queue, and Resume
or the next send sent it again. A check matching the agent's echo of the
text was the only guard against a double send, and it kept trading a
resend for a lost card. Whether the card waited again was decided by the
rejection's category, not by proof the agent never got the message.

A refused hand-off now waits again only where Orca proves the agent never
received it: the host still held it, or whoever settled it recorded that.
A failure still returns the card and holds the cards behind it. Anything
else may have reached the agent, so the card is spent and its message
reads as sent, derived on every fold from the stored rejection: with the
turn it was answered into, as that turn's unconfirmed message where it was
handed over; withdrawn before any turn opened, as a person's own send a
Stop took back. The reopen mark ignores a recovered doubt, and the restart
reconciler no longer judges a queued hand-off unsent from what history
lacks. The echo text matching and its pairing are deleted.

A close or quit while the agent starts settles sends with the unrun hold.
The host hands nothing to a child before its start answers
(structured-agent-session-delivery-loop.ts: a handover needs phase
ready), so such a send went to an earlier child and may have run: it now
reads as sent instead of waiting again.

* fix(native-chat): Claude's report that it removed a queued message unrun lets the card wait again

Claude says when it removed a send without running it: the interrupt
receipt's cancelled list, or a command_lifecycle cancelled, discarded or
refused frame for a command that never started. Every one settles through
rejectClaudeDispatchWaiters, which now marks the settlement unrun. For a
queued card's hand-off the host records that as proof the agent never got
it, so the card waits again under the queue's pause. A direct send keeps
the plain refusal: there the proof would owe it as stopped history.

* fix(native-chat): an earlier build's card stopped after hand-over follows the same rule on upgrade

Earlier builds put a card back to waiting after any refusal without a
verdict and cleared its link, so a card the agent may already have would
be sent again after the upgrade. The card's own hand-offs in the journal
decide now: unchanged and last sent as one that may have reached the
agent, it is spent on that hand-off and its message reads as sent; edited
or sent again since, it is held for its own Send under a host-only marker
that publishes as a plain hold; with no hand-off in doubt, including one
that never left Orca, it waits as before. The import writes a card spent
on a hand-off in doubt, so its message shows.

* fix(native-chat): read a queued row's hand-off before the lookup that uses it

The lookup's callback lost the narrowing of the row's change, which the
mobile test typecheck rejects.

* test(native-chat): a card steered into a turn a Stop ends stays sent in the Stop tests

Three tests pinned the old rule, that a card handed into a running turn
comes back to waiting when a Stop ends it. Under the new rule nothing
proves the agent never got it, so the card is spent: with the turn named,
its message reads as that turn's unconfirmed one; without, as a send the
Stop took back. Neither is ever sent again.

* fix(native-chat): a queued message stopped before Orca handed it to the agent waits again

A Stop that aborts a hand-off after its row is written but before the agent call now records
the withdrawal as never reaching the agent, for queued hand-offs only, so plan D's rule returns
the card to waiting under the Stop's pause instead of spending it. Direct sends keep the plain
withdrawal (the flag would owe them as stopped history). The agent's own refusal while aborted
is kept rather than overwritten, and an abort thrown during the call stays in doubt.

* fix(native-chat): a queued message stopped while the agent prepares it waits again

The abort signal reaches only an adapter's preparation (its options, the previous turn opening),
never its write, so the signal's own reason thrown back is the adapter saying nothing was
written. The handover now reads that, for queued hand-offs only, as never reaching the agent,
so the card waits again under the Stop's pause. Claude no longer wraps that abort as an option
refusal, which had returned a stopped card as a refused one blocking the queue. A real refusal
keeps its cause and its barrier; any other throw stays in doubt and is never resent.

* test(native-chat): the usage-limit Continue backlog case writes its card through the journal queue

Main's #30396 wrote its backlog card with the stored fingerprint and host-instance stamp the
journal queue derives instead; the card the Continue must yield to is the same waiting card.

* test(claude): the real-CLI queued Stop expects the unrun mark Claude's cancelled-unrun receipt carries

* feat(mobile): edit a queued message's images on the phone: remove or attach

* fix(mobile): gate queued image edits on the host reader verdict

* test(mobile): declare the upload fixture chat mode

* fix(native-chat): retain spent legacy messages when import fails

* fix(mobile): reveal the full queued image editor on open

* fix(mobile): invalidate queued editor geometry without a render ref write

* refactor(chat): prepare composer drafts and measured queue dragging

* feat(native-chat): add conditional queue removal and authoritative reordering

* feat(native-chat): move queued edits into the normal composer on desktop and phone

* fix(native-chat): own complete phone drafts before authoring

* fix: preserve submitted composer drafts across storage replacement

* fix composer validation and mobile CI diagnostics

* fix mobile composer test contracts and diagnostic output

* Capture mobile test compiler diagnostics in CI

* Fix queued message contract typing and English strings

* test: complete queued extraction transport fixtures

* refactor: separate composer skill binding from send mutations

* fix(mobile): grant draft storage within route limits

* fix(native-chat): settle each draft save on its own and publish complete composer drafts

Phone: a queued Edit copy no longer consumes the save that later typing
owns. Each ordinary save and the exact copy settle only their own result;
an explicit save waits for its own pending write, and a failed or retired
write never schedules another or revives the copy.

Desktop: a pasted-text leaf no longer shifts which selected file binds
where, and a hand-back held during an IME composition reaches the store
only merged with the visible editor document, including writes the editor
admits without an update and the composition settle.

* fix(native-chat): import the draft type once in the draft cache

* test(mobile): type the held-write state without an assertion

* test(native-chat): assert the handed-back file on Send without reading a typed transport as a mock

* test(mobile): pass main's agent-confirmed flag to the submitted-files harness

* fix: reveal queued reorder grip on touch screens

* fix: save reopened phone drafts and batch reorder measurements

* test: use an explicit React module type

* Preserve queued message sortable identities on unchanged renders

* Prevent stale queued drag completion after unmount

* fix(mobile): hand off navigation across session workspaces

* test(mobile): use resolved session identity in dictation fixture

* feat: edit queued messages inline with the chat composer

* fix: normalize queued edit skill ownership

* fix: retire phone skill selections with the sent draft

* fix: consolidate queued edit module imports

* fix: separate edit save action and stabilize image callbacks

* fix: complete phone inline composer integration

* fix: retain same-name queued skill selections

* fix: complete desktop queued composer integration

* test: use typed phone composer host lookups

* fix: include queued edit localization keys

* Preserve inline edit input across save and attachment completion

* Preserve complete queued edits across delayed saves

* Preserve queued edit input across delayed completion

* Complete queued edit lease loss and pending cancellation

* Use recognized queued edit test fixtures

* Consolidate queued edit imports and type host test spy

* Print mobile test compiler diagnostics on ratchet failure

* Correct queued edit integration fixture types

* fix: keep queued edit placeholder independent of readiness

* Fix queued edit compiler fixture boundaries

* Align upload completion test with ordinary insertion

* Combine mobile composer draft imports

* Load queued edit host fixture during test collection

* Size inline phone queue editor to available space

* Use typed scroll components in phone presentation contract

* Save typed text in a queued edit as written

The inline editor is the chat composer, so it keeps the composer's own identity model and no longer refuses Save for text that looks like a command. Command rows picked from the menu are still refused, Enter still means Save, and Edit never sends or runs anything.

Regression tests cover Undo after deleting an accepted command or skill pill, a scanned skill named like a command, two saved files behind one token, and typed command text saving literally. Also fixes two merge leftovers in tests: a missing import and a fixture that stored the whole menu row as a skill.

* Keep ordinary sends on the main composer contract

* Fix mobile composer imports and fixture loading

* Consolidate queued edit imports and duplicate regression

* Restore ordinary send catalog short circuit and repair test boundaries

* Preserve returned rich drafts through send acceptance

* Remove obsolete queued editor strings from new locales
2026-10-28 02:09:49 -07:00
Pandu dwi PutraandBrennan Benson ec49e9fac0 fix(native-chat): list OMP skills in the / menu as /skill:<name> (#31999)
* fix(native-chat): list OMP skills in the / menu as /skill:<name>

* test(native-chat): OMP now carries the Pi-shaped skill profile

* Publish native OMP skills with compatible command catalogs

* Index native skill descriptions once per picker build

* Combine catalog test composer imports

* Correct native catalog test types and project inputs

* Run cross-platform picker scaling contract with mobile tests

* fix: discover native commands before the first chat message

* test: isolate native command discovery from mobile typechecking

* fix: release command discovery ownership after managed root exit

* refactor: reuse the ACP child owner during shutdown drain

* test(acp): load filesystem implementation before test execution

* test: drive discovery close completion explicitly

* test: await discovery admission cleanup between demand waves

* test: control discovery close ordering in demand waves

---------

Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
2026-10-27 17:14:20 -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
Neil 4b347681bc feat(mobile): skip dev-client rebuilds when the Expo native fingerprint is unchanged (#33410)
Add `pnpm native ensure <ios|android>`, which installs a cached dev-client
build keyed by the Expo fingerprint and only runs `expo run` on a miss.
The cache lives in the user cache dir so every worktree shares it, and
app.config.js registers it as Expo's buildCacheProvider so plain
`expo run` reuses hits too. A report-only PR job flags fingerprint
changes against the base in the step summary.
2026-10-24 23:08:43 -07:00
Neil ec5cfbad37 fix(deps): patch vulnerable Zod collections in mobile and docs (#32941) 2026-10-23 15:29:15 -07:00
Neil 52e413ec85 fix(mobile): finish bundle downloads over slow links with smaller ranges (#32174)
* fix(mobile): serve bundle ranges within slow-link stall budget

* test(mobile): consolidate range compatibility coverage and typed dispatch
2026-10-22 22:05:45 -07:00
Neil a8dff64cb8 chore: remove unused shadcn generator dependency (#31737) 2026-10-21 18:59:05 -07:00
Neil 5f50b95c9c fix: remediate dependency security advisories across workspaces (#31681)
* fix: remediate dependency security advisories across workspaces

* test: verify security patches without ad hoc dependency installs
2026-10-21 16:31:17 -07:00
NeilandArthur Chang 0cba9f2e08 fix(mobile): render offline Nerd Font symbols on first terminal display (#31446)
Fixes #17168

Supersedes community PR #17220. Original contributor credit is preserved below.

Co-authored-by: Arthur Chang <arthurc0102@gmail.com>
2026-10-21 01:17:08 -07:00
Neil 553bde347a ci: simplify CocoaPods pin validation (#30208) 2026-10-17 23:09:25 -07:00
Neil ed40719964 Pin CocoaPods release installer in the Ruby lockfile (#30181) 2026-10-17 22:00:57 -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 da8de743eb feat(mobile): the phone filters and sorts with the desktop's shared Workspace options rules (#28986)
* fix(mobile): phone server rows follow the desktop's other-client and sleeping filters

A phone created through a desktop now records whom it acted for, so the
desktop's Hide other-client workspaces filter keeps the user's own phone
workspaces (locally and on servers) and hides only other people's.
The desktop relay marks each server row its sidebar would hide, and the
rows its own browser or chat tabs keep awake; the phone applies both.

* test(e2e): dedupe sidebar row ids in the visibility spec

* fix(mobile): address review: owner naming, old-server relayed phones, scope trim

* refactor: move the sidebar's project section grouping into shared

The project sections the desktop sidebar draws under Group by Project are
built from the project/setup projection, the per-host catalog fold, the
endpoint host stamp, and the section labelling and ordering rules. Move the
pure parts of each into src/shared (no behaviour change) so the paired phone
can call the same code instead of guessing the grouping.

* fix(mobile): group by Project with the desktop sidebar's own sections

The phone grouped Project sections by repo display name, so it merged
checkouts the desktop keeps apart (several local stablyai/orca checkouts,
or a server's copy) and split what the desktop merges. The phone now reads
each host's repo.list, project.list and projectHostSetup.list (the desktop's
own and each server's through the relay), folds them with the renderer's
shared catalog fold and projection merge, and builds sections with the
desktop's shared grouping, labelling and ordering code. Keys, labels and
Recent order equal the sidebar's; a host that predates the project lists
gets the repo-derived projection, as the desktop does.

Header colour and icon come from the section's anchor repo and rows from
their own repo by id, as on the desktop. The repo filter lists every
catalog repo by id per host instead of merging same-named repos. Group-by
labels read Project and None. project.list and projectHostSetup.list are
routed for paired phones (read-only, workspace permission).

* refactor(mobile): record a phone's owner as creator and fold desktop tab activity into the host flag

A phone now records its owner as the creator (this host for a directly
paired phone, the relaying desktop for a delegated one), so the shared rule
is unchanged and onBehalfOf is gone. The relay ORs the desktop's saved
browser/chat tabs into hasHostSidebarActivity, so the phone's status dot
follows too, and local worktree.ps counts the same signals. The old-server
fallback is removed.

* test(e2e): poll the phone and the sidebar together in the visibility spec

* fix(mobile): one Project-section builder for desktop and phone, with the desktop's repo order

Review fixes. The sidebar's buildOrderedGroups now calls the shared
buildProjectSections for Group by Project (notice and pending repos are
optional inputs), so desktop and phone run one function. The phone ranks
manual header order by its folded catalog order, which is each host's
repo.list order with the desktop's own repos first, as the renderer loads
them; only the desktop's cross-host manualRepoOrder (pairing-local) is
unknown to it. The projection selector is now a memo over the shared
resolver. A transient project-list failure keeps that host's last catalog;
only forbidden or method_not_found falls back to the repo-derived
projection. Catalog rows are checked on every member grouping reads, and
the parity test answers the renderer's real catalog fetch, includes a
server's own project rows, and fixes Manual and Recent orders that differ.

* fix(mobile): use the repo-derived catalog until a host's project lists first answer

* fix(mobile): the desktop serves its sidebar's Project catalog instead of the phone fetching it

The phone fetched repo.list, project.list and projectHostSetup.list from
the desktop and every relay-ready server each minute and folded them
itself, so rows from offline or older servers fell back to repo sections
and keys moved on cold start.

The desktop renderer now publishes the inputs its own Project headers use
(state.repos in sidebar order, the projection selector's projects and
setups, projectOrderBy) on the runtime window graph it already sends to
main, only when they change. Main keeps the last one and answers it on a
new paired-desktop read, mobileRelay.projectSections. The phone reads it
with its repo metadata and groups with the shared builder; a desktop that
predates the read (forbidden or method_not_found) leaves the phone grouping
rows by repo from the desktop's repo.list, and a transient failure keeps
the last catalog. The payload carries only grouping and label fields, no
hooks, credentials or uploaded icons. The phone's own catalog fetching and
its project.list/projectHostSetup.list routes are removed.

* fix(mobile): keep uploaded repo icons on the phone, and isolate the snapshot tests

* test(e2e): the desktop window serves its sidebar's Project catalog to the phone

* chore(i18n): regenerate the runtime English catalog (drift already on main)

* fix(mobile): read the Project catalog as its own request, outside the recorded repo-metadata flow

The extra read inside the repo-metadata refresh stalled its recorded RPC
sequence. It now runs beside it with its own throttle. The repo-metadata
goldens are re-recorded for the intended state change: the name-keyed
colour, icon and id maps became desktopRepos.

* refactor(sidebar): one shared Workspace options filter and sort evaluator

The sidebar, the jump palette and the options badge each applied the
Workspace options filters with their own copy of the rules. They now
call one pure evaluator in src/shared/workspace-view, which reads each
row through a small fact reader so the phone can call it next with the
facts its host serves. The sort comparator moves there too, with each
surface supplying its own Agent activity rank.

No behaviour change: a parity test runs the old sidebar chain and the
new evaluator over every toggle combination, and the old comparator
against the new one for each sort mode.

* wip: workspace view descriptors

* refactor(sidebar): address review of the shared Workspace options evaluator

One predicate builder instead of a per-row scope argument; one
exemption-narrowing function taking the hide flag; consumers import the
shared predicates directly; Agent activity ties fall to the label in the
shared comparator. The activation filter lift reuses the shared
predicates. The parity test now covers other-client rows, live agent
attention classes and the sort-label map.

* fix(mobile): fetch the Project catalog at once when the screen moves to another host

The refresh window is now kept per client, so a host screen reused for another desktop doesn't wait out the previous host's 60s window before showing its Project headers.

* wip: desktop option items from shared choices

* refactor(sidebar): the Workspace options menu renders from one shared option list

src/shared/workspace-view/workspace-view-options.ts lists every Workspace
options control (Hosts, Projects, Group by, Sort by, Project order, Card
layout, Show properties or Card display, and each filter) with its
section, its English copy, its choices and when it shows. The desktop
menu walks that list and renders each option through a table keyed on
the option key, so a new option fails to compile until the desktop has
a control for it; the phone sheet will do the same.

The choice lists (sort modes, project order, card layout, agent activity
layout, card properties) are derived from the shared ids, and a test
checks the desktop's English copy matches the shared copy. The unused
classic branch of the card property list is removed.

No visible change.

* wip: row facts

* refactor(sidebar): address review of the shared Workspace options list

The show rules get their own tests with fixed expectations (the cases
the old filter-section test covered, plus the rest), and the menu test
checks each option's own control rather than any matching text. Card
display rows toggle one property each, with the folder-path copy in
shared. The unused Status and Tasks copy left by the removed classic
card list is deleted from the catalogs. Filter rows write through the
store directly; files are named for what they hold.

* feat(runtime): worktree.ps carries the Workspace options row facts

Each worktree.ps row now says whether it is the project's default-branch
workspace, created by an automation, created by the CLI, or on a
detached HEAD, answered by the same shared predicates the desktop
sidebar filters with. The phone has none of these facts today, so its
copies of the filters either guess (default branch) or are missing.

Additive and inert: optional fields that nothing reads yet. An older
host omits them; readers must treat absence as 'not that kind', so the
row stays visible. Server rows relayed through the desktop pass them
through unchanged.

* feat(mobile): the desktop's own phone reads and writes its other-client filter

ui.get and ui.set strip the pairing-local UI fields for every paired
client. That rule exists for clients with their own host catalog (a
desktop paired to a server, a web client). A phone has none: it shows
its desktop's view, so 'Hide other-client workspaces' must be one shared
value. For the host's own phone (clientKind mobile, no relaying parent)
ui.get now always answers the field and ui.set persists it; delegated
phones, paired desktops and web clients keep the stripping rule.

The phone's view-settings schema reads the rest of the Workspace options
values it does not read yet (the automation, CLI and detached-HEAD
filters, project order, card properties, agent activity layout), each
salvaged to absent so an older desktop keeps the phone's local value.

* feat(runtime): address review of the worktree.ps row facts

Name the fact defaultBranch, and say what absence means for it: a reader
keeps its own guess, because 'hide default branch' must not start hiding
or showing rows an older host never described. Folder workspace rows get
the facts too, so every row from a newer host carries them. The
desktopOtherClient comment now says it is a verdict, not a fact. The test
covers the rows where the host disagrees with the phone's current guess.

* wip: phone uses the shared evaluator

* feat(mobile): address review of the phone's other-client filter mirror

One isHostOwnPhone predicate decides both who shares the filter value
(ui.get/ui.set) and who gets the other-client row marks, so the two
cannot drift. The pairing-local field list notes the exception. Tests
cover a paired desktop still being stripped, and a phone write that
leaves the field out not touching the host value.

* feat(mobile): the phone filters and sorts with the desktop's shared Workspace options rules

The phone's own filter chain and comparator are replaced by the shared
evaluator, reading the facts the host stamps on each row. Phone filter
state takes the desktop's setting names and carries every filter; the
badge and Clear filters match the desktop's. The desktop's other-client
marks become facts the phone filters with the shared value.

A parity test runs the same workspaces through the desktop's reading
and the phone's (facts stamped as the host stamps them) over every
filter combination, folder projects included.

* fix(mobile): host scope in shared filters, patch-key writes, live view push

Folds the phone's host filter into the shared filters so Clear and the badge agree,
writes back only the touched filter keys, and pushes workspaceViewChanged so an open
phone list re-reads desktop Workspace options.

* fix(mobile): Clear sends only changed filters; skip a view re-read older than a local edit

An older host's strict ui.set refuses a key it lacks, so Clear names only what differs. A ui.get
that started before a local edit no longer overwrites it; the edit's own change event re-reads.
The change publisher's field list now lives with it and derives the filter fields.

* fix(workspace-options): all hosts and clear reach the desktop; one filter key list drives every surface

Host visibility: the legacy host scope fallback moves to shared code; the phone reads it, and the
host resets the scope whenever a client writes the host list, so All hosts and Clear stick.
An older desktop that keeps the other-client filter to itself has its marks treated as hide.
Badge counts, is-anything-filtered, Clear and reveal derive from the shared filter keys on the
desktop sidebar and the kanban filter menu. The phone uses the desktop's group and sort values,
holds its view in one state value, and reads its UI fields from one shared list.

* feat(workspace-options): Status and PR grouping read the shared tables

The phone groups by status with the desktop's status helpers and default catalog (its own copy
had the defaults in reverse order) and by PR with a shared lane table, so its labels now read
In review / In progress as the desktop's do. The desktop's PR lane decision ends in the same
shared state-to-lane step.

* test(sidebar): folder rows survive restrictive filters; create grace ignores non-finite times

* fix(mobile): write the host scope with the host list, and wait for the first read before forcing the other-client filter

An older desktop narrows back to its legacy single-host scope when the phone
writes only a null host list, so the phone now also writes the scope for a null
or one-host list. The other-client fallback waits for the first ui.get so a new
desktop's marks don't hide rows before its filter value arrives. Inline the
clear-filters key list and note the PR state asymmetry.

* test(mobile): re-record view-settings goldens for the unknown other-client state

* fix(sidebar): reuse the palette's default host instead of reading the focus setting again

* fix(sidebar): read the host options once in Workspace options and pass them to the Hosts item

* fix: parse the ui.get reply into a named type, fix the row memo test's filters, prune the focus-read baseline

* refactor(sidebar): one switch-to-kind table, and fold the leftover helper files back

- The four kind switches come from WORKSPACE_KIND_SWITCH in workspace-kinds;
  the shared evaluator and main's phone-push check both apply it, and the filter
  type derives its switch fields from it.
- git-identity.ts folds back into git-identity-display.ts, as on main.
- SidebarFilterState moves into sidebar-filter-actions.ts; visible-worktree-kinds.ts is gone.
- buildWorkspaceRowPredicate is module-private.
2026-10-15 07:32:59 -04:00
Neil c827ed5324 ci: run full native mobile typechecks without compiler caches (#28409) 2026-10-13 14:27:14 -07:00
Neil fba7b753ec Cache mobile typechecks and overlap CI setup (#28192)
* ci: run mobile runtime test groups on separate runners

* ci: use a lightweight runner for the mobile result gate

* ci: distribute Bun tests across both mobile runners

* ci: reuse mobile compiler state without adding runner queues

* test: verify eviction of source variants in one cache bucket

* test: baseline existing serializer limits in the capacity capture
2026-10-13 03:35:51 -07:00
Neil 1912c90141 Extract subprocess handling into @orca/process-host (#26963)
Extract subprocess execution and process contracts into @orca/process-host with explicit public entry points, independent checks, and one Node 18 compatible CommonJS runtime. Migrate application consumers and integrate package builds, dependency selection, import enforcement, and runtime resource staging.

Preserve daemon startup ownership and profile isolation, and qualify development rebuild recovery and concurrent output publication. Remove unnecessary aliases, test forwarding exports, and a duplicate output scan.

Validation: full typechecks, changed-code quality, package and artifact checks, native Windows builds/types plus 306 runtime tests, and independent native reviews.

Matched full E2E controls on original main and unchanged reviewed head both pass all 17 applicable tests (nine skips). Earlier intermittent E2E failures are documented in the PR; no assertions were weakened. The final combined-tree audit preserves both branches with no import-boundary violations.
2026-10-09 18:32:28 -07:00
Neil ddcc2796ba Preserve Mermaid exports through mobile bundling (#24661) 2026-10-02 06:10:27 -07:00
Neil 8186ded0bd fix(mobile): preserve terminal mode query variables in WebView builds (#24626) 2026-10-02 04:57:06 -07:00
Brennan Benson e594cb06af test(mobile): record RPC goldens without a pinned commit, and check recorded requests against the desktop's params rules (#23732)
* test(mobile): add rpc:diff to decode what a recording change moved

The RPC recording goldens are content-addressed JSON, so their raw git diff is
pool hashes. `pnpm --dir mobile rpc:diff [<base>]` decodes both sides and prints,
per golden, the checkpoint, field and JSON path that moved with both values,
grouped across checkpoints, plus added and removed goldens. `--summary <file>`
appends a Markdown report capped for GitHub's step-summary limit.

It reads any pooled format, so it can prove the next commit's format change
moves no recorded value. Checkpoints are matched by occurrence because an id can
repeat within one golden.

This commit adds files under the recorder directory, which moves the header
digest every golden pins; the next commit removes that header.

* test(mobile): record RPC goldens without a pinned commit or input digests

Every golden carried a pinned `baseline` commit plus digests of the recorder,
its mount adapter and its scenario, and the record script refused to run unless
the product tree matched the pin. So every behaviour change repinned to its own
branch commit and rewrote all ~790 files, the squash made that commit
unreachable, and main's pin job stayed red until a hand-made repin pull request
landed (22 of them in 12 days). The digests could only fail when an input moved
and the recording did not, which is exactly the change that carries no
information; every run already re-derives each golden from the current tree and
compares it.

Format 6 keeps the format version, operation, family, named deltas, the value
pool and the recording. Removed: the pin and fence, the three digest modules and
their test, the pin guard and its CI job, and the dead scenario `version` field
(the manifest reader now refuses `baseline` and `version` with a message).

- `pnpm --dir mobile rpc:record [<golden-id>...] [--prune]` records all or some
  goldens; orphans are listed, and deleted only with `--prune`. Every derived
  test title now starts with its golden id so an id selects it.
- `compareGolden` reports every difference in one failure (identity fields by
  name, the checkpoint list, each checkpoint/field/path grouped), keeps the
  final byte compare, and ends with the command to re-record that golden.
- `unhandled-recording.test.ts` now drives a detached rejection through
  `runRecording` into a checkpoint and the cleanup checkpoint; no golden carries
  one, and disconnecting the capture passed every suite before.
- Seam rules that existed only to keep a digest honest are gone; the
  mutant-reachability, register-completeness and one-exposure rules stay.
- CI: `Mobile tests on main` runs the whole mobile suite on every merge that
  touches mobile/, src/shared/, the root lockfile or the host RPC paths, since
  `verify` never runs on main. A new `Mobile RPC Recording Replay` workflow
  replays the recordings on pull requests that touch src/shared/ or the root
  lockfile without touching mobile/. `verify` writes the `rpc:diff` report to
  the job summary.

Proof: `rpc:diff` against the parent reports no recorded behaviour moved; each
golden only loses its ten header lines.

* test(mobile): check every recorded request against the host's params contract

The goldens script the host's replies, so a scenario could record a success
for a request the real host would refuse, and a desktop change that tightens a
params schema moved no golden at all.

`recorded-request-params.test.ts` parses every distinct request the corpus puts
on the wire with the host dispatcher's own `parseRpcRequestParams` and the
schema `rpc-params-catalog.generated.ts` binds to that method. It fails on a
method the host lacks, params it refuses, params sent to a method that takes
none (the dispatcher never reads them), and keys the schema silently strips
unless an inventory entry gives the reason; a stale entry fails too. Each rule
is also shown firing on a made-up request, since the corpus has no instance of
three of them. It imports the desktop dispatcher, so it sits beside the other
Node-side tests outside the RN test program, and the params-contract boundary
now exempts test files, which are never bundled.

It found twelve requests the host would refuse, all from invented fixture
values, not product code, fixed at their source:
- git.branchDiff sent `base-oid`/`head-oid`/`merge-base` where the host needs
  full object ids (diff-review and source-control adapters, and the branch
  compare replies in the manifest that feed them);
- an iOS push registration without `apnsEnvironment`, which a real iOS token
  always carries (`push-token.ts`); the adapter now defaults to `production`;
- `settings.update` given Linear's `assigned` filter as a GitHub preset, which
  the product type forbids; the scenario now picks `my-issues`;
- GitLab `projectRef` as a string where the host and the product type take
  `{ host, path }` (7 methods, 5 adapters and the manifest).

46 goldens move, and a decoded comparison of every one of them shows no change
other than those substitutions; `rpc:diff` lists them.

* ci(mobile): detect a mobile change without a SIGPIPE-prone grep pipe

Under the runner's pipefail, grep -q exiting on its first match SIGPIPEs git
diff on a long file list, so a large pull request touching mobile/ read as
uncovered and replayed the recordings a second time.

* test(mobile): drop comments that still describe the golden header and digests

Eleven adapters justified an import rule by the header a golden no longer
carries, and that rule's test is gone. The census failure now names the
rpc:record and --prune commands.

* test(mobile): refuse a golden that keeps a key no recording writes

Decoding dropped unknown top-level keys, so an old header left behind by a
hand-resolved merge conflict passed every compare unseen.

* ci(mobile): summarize RPC recording changes after a failed test step too

* test(mobile): stream rpc:record output instead of capturing it

A captured run stayed silent for its whole duration and clipped its tail,
where the failure summary sits, past 8 MB.

* test(ci): let the Ruby-gate contract skip the always-run RPC summary step

fef088d8f4 gave the summary step an `if: ${{ !cancelled() }}`, and this test lists every gated step in `verify` and expects each to be gated on the Ruby scope.

* test(mobile): replay only a golden file that is exactly what rpc:record writes

Replay compared two re-encodings of decoded values, so anything decoding drops (a leftover header
key, a hand edit) sat in the committed file uncompared; a key allow-list covered one case of that.
Replay now passes only if the file text equals the formatted golden for the run, sharing one
formatter with writeGolden, and keeps the field-level report as the failure message. The allow-list
goes; the value-based compareGolden stays for the bridged run, which has no file.

* test(mobile): end a corrupt or hand-edited golden's failure with the re-record command

A hand edit to a pooled value failed in decode with only "Golden value <hash> does not hash to its
pool key": no golden id and no command to fix it. readGolden now prefixes parse and decode failures
with the golden id and ends them with the rpc:record command. The rpc:diff header also said it
always exits 0; it exits non-zero when git or a golden cannot be read, and now says so.

* ci(mobile): run Mobile Checks on every src/shared and root lockfile change

Replaces the replay-only workflow: mobile imports hundreds of shared modules, so a shared edit can
move a golden or break mobile's typecheck, and the full job catches both before merge. A root
lockfile-only change skips the Ruby release checks, which read no root Node dependency.

* test(mobile): list or prune orphaned goldens even when the recording run fails

Orphans come from the manifest, not the run, so a failed or timed-out rpc:record still reports
them; the exit code stays non-zero. README: say what a failed replay reports (first differing
path per field, capped groups) and what rpc:diff compares with and without a base.

* test(mobile): pin the RPC recording goldens to LF so a CRLF checkout still replays

Replay now requires the committed golden text to equal exactly what rpc:record writes, which is
LF. A Windows checkout with core.autocrlf=true converted every golden to CRLF and failed all 790
with "holds the same recording but is not the file rpc:record writes for it".
2026-09-29 23:26:55 -07:00
Brennan Benson c5fc0c6f26 fix(ci): keep a squash-merged RPC recording pin reachable through its pull request (#23720)
* fix(ci): keep a squash-merged RPC recording pin reachable through its pull request

Main's "RPC recording pin" check has been red since #22762: that branch pinned
the recording corpus to its own commit 03995ae, and the squash-merge left that
commit out of main's history. Every behaviour-change squash did the same, and
each needed a hand-made repin PR to clear it (#23565, #23535, #23046 and more).

The guard now accepts a pin that is either in this history or in the head of the
pull request whose squash wrote it into the manifest. It finds that pull request
from the `(#n)` subject of the commit that added the pin and fetches
`refs/pull/<n>/head`, which GitHub keeps after the branch is deleted. The
reproduce step uses the same lookup, so it can still check the pinned tree out.

* fix(ci): give the recording pin lookup room to walk a blobless clone

In CI's blobless clone, `git log -S` fetches the manifest's blobs one commit at a
time, a few seconds each. Under the 30 s process default the walk was killed after
a handful of manifest commits, which main's history already exceeds (up to 7
manifest commits between a pin landing and the next pin change), and the guard
then failed with an empty "Could not find the commit that pinned" error. The
lookup and the pull request fetch now carry explicit budgets and say when they
timed out.

The not-an-ancestor instruction now names the pull request whose head was
checked, or says the commit that pinned it names none.

Adds the two merge-preview shapes the guard runs on: a branch opened after a
squash resolves main's pin through the squash's pull request, and a branch whose
rebase dropped its own pinned commit fails on its pull request instead of on main.

* fix(mobile): tell a missing recording pin apart from product drift

After a squash the pinned commit can live only in its pull request's head, so a
clone that never fetched it makes `git diff --quiet <baseline>` exit 128. The
recorder reported that as "Product sources or lockfile differ from the pinned
main baseline", which sends the developer to repin a tree that may match. It now
prints git's error and the command that fetches the pin.

* fix(ci): ask GitHub which pull request holds a squash-dropped recording pin

The recording pin guard found the pull request that keeps a squash-dropped
pin by walking main's first-parent history for the commit that wrote the pin
into the manifest and reading "(#n)" off its subject. A merger who edits the
squash title loses the number, and the push to main turns red anyway. That
already happened on main: of the 22 squashes that left a pin outside main's
history, #21674's title had no "(#n)".

The guard now asks GitHub for the pull requests associated with the pinned
commit (GET /repos/{owner}/{repo}/commits/{sha}/pulls) and, for each in turn,
fetches refs/pull/<n>/head and accepts only when git proves the pin is an
ancestor of that head. GitHub only nominates candidates, so a wrong answer can
fail the guard but never pass it. The endpoint named the right pull request
for all 22 historical cases, #21674 included, and names none for commits a
force-push orphaned.

This removes the pickaxe walk, its 600 s budget and its lazy blob fetches in
a blobless clone, the first-parent subtlety, and the subject regex. A revert
that restores an older pull-request-only pin now resolves too, because the
lookup is by the pin itself rather than by the commit that last wrote it.

CI passes the job token to both guard steps and grants the job
pull-requests: read. Local runs work without a token on this public repo and
send GITHUB_TOKEN or GH_TOKEN when set. A failed lookup throws with the HTTP
status, and names the rate limit when an unauthenticated call is refused.
2026-09-28 21:17:07 -07:00
Jinwoo Hong b76bc79d73 build(mobile): key Metro's transform cache on the shell build kind (#22244)
`babel-preset-expo` inlines `EXPO_PUBLIC_MOBILE_SHELL` into
`mobileShellBuildKind` at transform time
(babel-preset-expo/build/inline-env-vars.js:51), but nothing Metro hashes
into the transform cache key carries that value: the key is
`metro/src/DeltaBundler/getTransformCacheKey.js:21`, whose inputs are the
Metro version, `cacheVersion`, the transformer path and
`@expo/metro-config/build/transform-worker/metro-transform-worker.js:600`,
none of which reads the environment. A release assembled after an
opposite-kind build reuses the warm entries and bakes the wrong shell,
and the absence of the variable's name in the bundle cannot tell the two
apart. The newest published `@expo/metro-config` (58.0.4) keys it no
differently.

Folds the kind into `cacheVersion`, by the same `=== 'ota'` rule the app
applies, keeping Metro's own version as the prefix.

Proven with four `expo export --platform android` runs against an
isolated Metro cache. Before: `ota` then `native` produced byte-identical
bundles, both `return 'ota'`. After: the second run returns `'native'`
under a different bundle hash.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-22 07:57:43 -04:00
Jinwoo Hong d9954000b3 refactor(mobile): the rich editor's document becomes scope-threaded modules and a bundled factory (OTA phase C, C7.10 C1) (#21969)
* refactor(mobile): split the rich editor document's stylesheet and markup apart

The body constant carried the tail of a `:root` block, every CSS rule and the
editable surface's markup in one string, which only the HTML builder could
splice. A page mounting the document needs the stylesheet and the markup
separately, so they become a function over the theme and a constant.

Byte-for-byte inert: `mobile-rich-markdown-editor-document.test.ts`'s digest of
the shipped document is unchanged.

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

* refactor(mobile): give the keyboard-inset normaliser its own module

It is the host's half of the inset, read by the controller, and it sat in the
module holding the document's in-page script. The script is about to become
ordinary TypeScript under `rich-markdown/`, where a native-side normaliser does
not belong.

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

* feat(mobile): the rich editor's document becomes scope-threaded modules and a factory

The editor's ~600-line program lived in seven string constants a concatenator
glued into one `<script>`: unreadable, untypeable, and unreachable from a page,
which is where the OTA shell has to run it (ruling 26).

It is now ordinary TypeScript under `src/components/rich-markdown/`. Every
function that touches editor state takes `scope: RichMarkdownEditorScope` first,
`createRichMarkdownEditorDocument(host)` builds the scope, runs the start
sequence and returns `{ send, stop }`, and the six window reads the script did
are host seams with those reads as their defaults: `postToHost`, `promptForUrl`,
`keyboardInsetSource`, `clearTimer`, `getSelection`, `getDocument`.
`runCommand` is async because a host that answers the URL prompt with a modal
cannot answer synchronously; the thirteen commands that never wait stay one
synchronous act.

No module holds a `let` and none does work at parse time (rulings 20, 21), so a
second mount starts from its own state and `stop` takes back both the surface's
four listeners and the viewport's two.

The native document is an esbuild IIFE bundle of `native-document-entry.ts`,
written beside the terminal document's artifact by a fifth postinstall
generator. Nothing ships it yet: the HTML builder still splices the old strings,
which the next commit changes.

Red-first: `rich-markdown-document-parse-time.test.ts` and
`rich-markdown-host-seams.test.ts`. Their readers are the terminal census's,
extracted to `src/test-support/webview-document-census.ts` and pointed at both
documents rather than copied.

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

* refactor(mobile): ship the bundled document and retire the editor's script strings

`buildMobileRichMarkdownEditorHtml` splices the esbuild bundle of
`src/components/rich-markdown/`, and the seven string constants and their
concatenator go. `escapeInjectedJavaScriptString` stays: it is the escape for
`injectJavaScript`, which is still how the native host reaches the document.

Equivalence, since a byte golden over the script cannot survive a bundler:

- `rich-markdown/native-document-bundle.test.ts` evaluates the shipped artifact
  exactly as the WebView does — its markup, its bridge, its `execCommand`, its
  `prompt`, its `visualViewport` — and drives it through the injected handle:
  `keyboardInset` then `ready`, all five members, a markdown round trip through
  the real escape, an edit under the host's generation, every toolbar command's
  engine verb, the `javascript:` refusal, a tapped link, and the module list.
- `mobile-rich-markdown-editor-document.test.ts` keeps a byte pin, now over the
  page around the document. Measured on main's own document with its script
  region removed and on this one: 5,621 bytes, both
  `5054e1d5c87e4ce1805d4856ddc8bf36804e697675e6013d84da453d3e81af25`. The
  whole-document digest it replaces was `1ef29c88…`, 29,852 bytes.

Every assertion `mobile-rich-markdown-editor-html.test.ts` made by extracting
functions out of the emitted text is kept, aimed at the modules:

- nested/ordered/task list rendering and serialization, entities, explicit
  numbering, the parent-start fallback, read-only checkboxes →
  `markdown-round-trip.test.ts`, over real elements rather than shaped objects.
- the emitChange/setEditable guards and the generation carried through a
  replacement → `editor-content.test.ts`, behaviourally.
- dismissKeyboard, the tapped caret, the label tap, the restored caret, the
  end-of-document fallback, the detached caret → `editor-selection.test.ts`,
  with a blur that drops the ranges the way WebKit does.
- parseable script and the injection escape stay in the HTML test.

New with the factory: `document-lifecycle.test.ts` — stop takes the four surface
listeners and the viewport observer off, a second mount is its own document, two
documents do not share `editable`, and a start that throws unwinds.

`use-mobile-rich-markdown-editor-controller`, `MobileRichMarkdownEditor` and the
web fallback tests are untouched and green.

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

* test(mobile): read the document's mutable bindings from the tree, not the line start

The census matched `/^(let|var) /gm`, so `export let`, a declaration indented
inside a top-level block and a `for (let …)` head were all invisible — three
shapes of the one binding two documents would share — and its single
precondition proved only the shape it could already see.

`moduleLevelMutableBindings` walks the program instead and stops at every
function body, because a binding one call owns is not module state. Its
preconditions are one per shape, with the kind each reports, and a negative case
over a `const` and a function-local `let`/`var` so the empty list is a
measurement rather than a reader that refuses everything.

Red-first: `export let pendingReport = 0` planted in `keyboard-inset.ts` reds it
with `keyboard-inset: let pendingReport`, which the old matcher passed.

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

* fix(mobile): pin both WebView document bundles to the mobile root

esbuild writes each module's path into a bundle as a comment relative to the
working directory, and neither generator set `absWorkingDir`. So the artifact's
bytes followed the cwd of whatever postinstall run wrote it: measured from the
repo root, `mobile/`, and `mobile/src`, three digests — and from outside the
repo the comments carried `/Users/<name>/…`, a machine path in the one file
every bundle test compares against a build it makes itself.

Both generators now pin the mobile root, so the four cwds measured agree, and
both bundle tests carry the pin: a digest built in a child process from the OS
temp directory equals the committed artifact's, and no comment in either
artifact is an absolute path or climbs out with `../`.

`build-terminal-document-script.mjs` had the defect verbatim on main; C1 copied
its shape, so both are fixed here rather than leaving the original to be found
again. Neither artifact's bytes move: both were generated from `mobile/`, which
is what `absWorkingDir` now names.

Red-first: deleting the `absWorkingDir` line from either generator reds that
generator's case.

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

* test(mobile): cover the getSelection seam's override, not just its default

Five of the six seams had both halves and this one had only its window default,
which is the half that cannot fail on the page: there the caret has to come from
the object the host hands over, because a document mounted inside a screen
shares `window` with every other field on it.

The case gives the document a selection of its own, blurs the surface the way
WebKit does — dropping the ranges, which is the whole reason a caret is saved —
and reads the restored caret back out of the host's object. The window's own
selection stays empty throughout, which is what says the default was never
consulted.

Red-first: `rememberSelection` reading `window.getSelection()` instead of the
field reds it; every other case in the file stays green.

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

* fix(mobile): make the editor document's stop cancel its pending timer

`stop` took the surface's four listeners and the viewport's observer off and
left the input timer, while the scope kept the handle and the `clearTimer` seam
kept the means to cancel it. A listener comes off with the element it was on; a
scheduled callback holds the scope and fires into a document the host has
already unmounted, posting a change under the generation of content it has
replaced.

`stopEditorContent` cancels it through the seam and clears the field, and the
sequence runs it last — after the listeners that could have scheduled another
one are gone.

Nothing schedules the handle today. The cancel is here because the seam and the
field exist for the day something does, and that is not the moment to discover
`stop` never reached it. The case plants the pending change rather than waiting
for a debounce, and carries its own control: the same timer posts while the
document is running, and posts nothing once it is stopped.

Red-first: dropping `stopEditorContent` from the sequence reds both that case
and the parse-time census's start/stop set comparison.

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

* docs(mobile): correct the postinstall generator count in both censuses

Two comments said four generators and six generated files. There are five
generators writing six files, and the six are not the six either comment
described: `census-source-files.ts` still named the page's copy of the terminal
document, which ruling 25 retired and #21962 stopped ignoring, while C7.10 C1
added the rich Markdown editor's.

Both now name the lists of record — `mobile/package.json`'s postinstall for the
generators, `mobile/.gitignore` for the files — and say the count is a reading
that grows rather than a fence, which is what made the old numbers wrong twice
over.

Verified against both lists: 5 and 6.

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

* docs(mobile): say which digest is the document and which is the page around it

The docstring put main's whole-document digest and byte count in the sentence
introducing the shell pin, so it read as if `1ef29c88…` and 29,852 bytes were
what the constant below asserts. They are not: that digest is of main's whole
document, script included, and nothing in the file reproduces it. The constant
is of the document with its `<script>` region emptied, taken on main's document
and on this one.

Both are now named and separated, with what each covers and why the shell one
was read twice.

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

* test(mobile): name the parse-time fixture by its role

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

* fix(mobile): drop an editor command whose dialog answered after the host moved on

C7.10 C1 made `runCommand` async so a host can answer the URL prompt with a
modal. Inside the WebView that changes nothing — `window.prompt` resolves within
a microtask, and the host reaches the document through `injectJavaScript`, which
is a later task — but on the page the modal is a real task boundary, and while
it is open the host can replace the content, make the editor read-only or
unmount it entirely. The continuation ran anyway: `createLink` against markdown
nobody chose, and a change posted under the new generation carrying an edit made
against the old one.

`acceptsCommands` is the question both halves ask: not stopped, still editable,
still the same generation, still contenteditable. `insertUrl` asks it before
`execCommand` and `runCommand` asks it again before emitting, each against the
generation read before its own wait.

The scope gains `stopped`, which `stopRichMarkdownEditorDocument` sets.

Inert on native, where no state can change across a microtask, so the answer to
both questions is the one the old code assumed.

Red-first: with either check removed, the new case reports
`[ 'createLink', 'createLink' ]` against `[ 'createLink' ]`. The case carries its
own control — an answer that arrives while nothing has moved is still applied
and still reported.

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

* fix(mobile): make the editor's block reader always consume a line

`markdownToHtml` looped forever on `# `, `- ` and `1. `. `isBlockStart` admits a
marker followed by a space, and the list test admits the same, but the heading
reader requires text after the hashes and `parseListLine` requires text after
the marker — so on those lines the list branch consumed nothing and returned the
index it was given, and the paragraph loop gathered nothing and pushed an empty
paragraph without advancing. A one-line file the host handed to `setMarkdown`
froze the WebView.

Two guards, both by the same rule: a branch may only commit if it moved the
index. The list branch falls through when its run is empty, and the paragraph
falls back to the line itself when it gathered none.

Present on main verbatim, so this is inherited rather than introduced — but the
fix is observationally inert, because the only inputs it changes are the ones
that previously never returned. Every input that produced output produces the
same output.

Evidence, from a probe that bounds the loop from the inside rather than waiting
on it: before, `# ` and `- ` both UNBOUNDED; after, twenty marker and fence
shapes all return. The pinned cases carry their own control, `# ok` and `- ok`,
so the fallback is not swallowing the readers it falls back from.

A red-first case is not possible here: without the fix the case does not fail,
it hangs the worker. The probe above is the measurement.

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

* test(mobile): see every declaration that runs as a document module is evaluated

The parse-time reader inspected only variable declarations, while
`DECLARATION_KINDS` admits classes and default exports. So
`class A { static value = install() }`, a static block, and
`export default install()` all passed a census whose whole job is to refuse
exactly that — and a static field reading `document` passed too, which is the
remount defect the rule exists for, wearing a different shape.

Three shapes now, each reported by what it does rather than what it looks like:
a variable initialiser, a class's static members, and a default export that is
an expression. `DECLARES_WITHOUT_RUNNING` keeps the last one from walking into
the body of `export default function () {}`, whose calls run when something
calls it.

The preconditions are one per shape, with a negative case beside them: an
instance field runs per `new` and nothing in a document is ever constructed, and
a default-exported function declares a body rather than running one.

Inherited from the terminal's census, which had the same reader; both use this
one, and both are green.

Red-first: removing the class branch reds the new precondition case.

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

* test(mobile): read lifecycle exports from the tree, not from one exact spelling

The reader was a regular expression needing `export function`, one line, the
scope parameter and no return type. `export async function startX(`, a return
type, or a parameter list the formatter wrapped made a real lifecycle export
vanish — and the comparison it feeds is a set against the names the sequence
calls, so a function missing from *both* lists makes them agree. A start nobody
runs would have read as a start nobody needs.

It now qualifies a function by what it is: exported, named for its lifecycle,
and taking the document's scope as its only parameter. That last clause is
ruling 20's own wording — a start takes nothing the scope does not already carry
— and the regex was enforcing it by accident, through the single parameter its
pattern happened to allow.

Surfaced by the change: the terminal's `startEdgeScroll(scope, dir)`, which the
regex never matched and the sequence never calls. It takes a direction, so it is
the overlay's act for a drag rather than a module's lifecycle, and the one-
parameter rule refuses it for the stated reason instead of by accident. Both
censuses are green.

Red-first: restoring the regex reds the new precondition case, which covers
`async`, a return type and wrapped parameters, with refusals beside them for a
two-parameter start, another document's scope type, and an unexported function.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-21 07:16:23 -04:00