Commit Graph
41 Commits
Author SHA1 Message Date
Jinwoo Hong b8d4cde09f refactor(mobile): send six screen-mounted call sites through typed RpcOperations (step 4, wave 3) (#20919)
* test(mobile): record six screen-mounted call sites before migrating them

Five new mount adapters and six scenarios, recorded against the pinned baseline's
product code so the goldens are main's behaviour, not the refactor's.

Each site is a screen the recorder could not previously mount:

- `home.host-accounts` mounts `fetchMobileHomeAccounts`, whose decoder is
  re-exported through `AccountUsage.tsx`. That module loads under the mount
  loader, so the inventory's "no recording can load it" was already stale.
- `notifications.display-test-screen` mounts the settings push probe and presses
  its button by reading the handler back off the rendered inert `Pressable`.
- `aiVault.history-screen` mounts the history panel, which is where the last
  `worktree.ps` lives. Split in two: the base stops once the worktree list has
  seeded the scopes, because a reply partition there changes the scopePaths the
  downstream `aiVault.listSessions` carries, and a matrix variant cannot assert
  params it moved. The full chain is a second scenario, driven as a pilot only.
- `tasks.route-repo-list` mounts the tasks screen-root hook and calls its own
  `ensureLoaded`, which is the only thing that fires `repo.list`.
- `linear.select-workspace-picker` calls the render helper the tasks surface
  calls and invokes the `onSelect` on the element it returns. The picker draws
  inside `BottomDrawer`, whose reanimated timing driver and gesture builder the
  recorder would have to impersonate for a row to exist; the closure is the same
  either way, and the workspace a selection carries comes from the scenario.

Five substitute members are added, each with the recording that reads it:
`react-native-safe-area-context.useSafeAreaInsets` and
`expo-router.useLocalSearchParams` for `tasks.route-repo-list`, and
`react-native.TextInput`, `.SectionList` and `.RefreshControl` for
`aiVault.history-screen` once its list renders. `useLocalSearchParams` answers one
pinned route for the same reason the window size is pinned: a screen's own address
is not a device reading, and the one screen that reads it sends `repo.list`, which
takes no params.

Touching the substitute table moves `recorderSha256`, so all 641 existing goldens
are re-recorded. Recorded from a detached worktree at the pinned baseline with this
branch's recorder laid over it: every pre-existing golden is header-only, verified
by resolving both sides through the value pool — 641 header-only, 0 body, 0 deleted,
one distinct `recorderSha256`, `baseline` and `lockfileSha256` across all of them.

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

* test(mobile): type the linear workspace picker's model fixture

`mobile/tsconfig.json` covers the recorder, and the fixture's setters were written
with the argument the product happens to pass rather than the `SetStateAction` the
model declares. Typing them moves `adapterSha256` on the two goldens recorded through
this module, so they are re-recorded here rather than in the refactor commit, which
must move none.

Re-recorded at the pinned baseline: `linear-select-workspace` and its reply matrix,
header-only, bodies unchanged.

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

* refactor(mobile): send six screen-mounted call sites through typed RpcOperations

Nine references off the raw request port, across six files. Every one is proven
against the goldens recorded in the previous commit from the pinned baseline's
product code: this commit moves no file under mobile/rpc-foundation/goldens.

Reused rather than redefined:

- `worktree.ps` in the history panel sends through `worktreeCatalogRead`. Same
  question, same acceptance — a refused list leaves the screen on what it holds.
- `repo.list` in the tasks screen-root hook sends through `newTabRepoListRead`.
  Its policy raises the host's message and its reader takes `repos` off the payload
  while preserving the property-read exception a null result used to throw at the
  cast, which is what this call site did by hand. Its name still says new-tab; a
  third consumer does not make renaming it this bucket's business.

Four operations are new, each because no existing reader on the method takes this
consumer's input:

- `files.read-directory-or-skip` and `files.legacy-explorer-list-or-skip` for the
  explorer. Both skip, because neither refusal is the operation's to decide: the
  readDir refusal code selects the legacy fallback and the list refusal supplies the
  message. The existing `files.list-or-skip` reads the `files` member alone, and the
  explorer also needs `truncated` for the "Showing first 5000" note.
- `accounts.home-snapshot-or-skip` for the Home card, decoded by
  `decodeAccountsSnapshot` at the call site as before.
- `notifications.test-push-or-skip` for the settings probe, whose `forbidden` and
  `method_not_found` refusals mean "try the next desktop".
- `linear.select-workspace-or-skip` for the filter sheet.

Two behaviours are preserved rather than repaired, both recorded:

- The workspace switch never read its reply. `.then(() => loadLinearContext())` runs
  on a refusal exactly as on a success, so only a transport rejection reaches the
  error copy. Interpreting the operation here would surface a refused switch for the
  first time; that is a product change with its own re-record.
- `app/terminal-settings.tsx` still reads `ms` off the reply envelope instead of off
  its result, so the value is always undefined. It did not migrate, and the inventory
  now carries the defect as its own note.

Four mutants are added, one per new family that admits a state-only one:
the Home snapshot, the push test result and the tasks repo list each decoded one
level above the envelope, and the workspace switch with its context reload dropped.
`aiVault.history-screen` gets none and says why in the suite: everything
`worktree.ps` publishes also moves the `scopePaths` the next scripted completion
asserts, so a mutant aborts the sequence instead of diverging from it. Its evidence
is the reply matrix at that request.

The tasks source-parity ratchet moves with the family it guards: hook, statement,
declaration, render and style counts are unchanged, and the semantic source is a pure
deletion of four lines — two `rpc:` call signatures and the two method literals they
carried.

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

* test(mobile): matrix the six new screen families' replies

One golden per scripted reply, eleven partitions each, recorded at the pinned
baseline alongside the pilots. Seven sites: `accounts.list`, `notifications.testPush`,
`repo.list`, `linear.selectWorkspace`, and all three of the history screen's —
`worktree.ps` and the two `status.get` reads its scan chains off the worktree list.

The history matrix is also that family's defect evidence in place of a mutant: every
partition at `worktree.ps` changes the `scopePaths` the downstream `aiVault.listSessions`
carries, and the sender args are recorded with it.

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

* docs(mobile): correct three operation and mutant comments

Comment-only, no product behaviour and no golden movement.

- `worktreeCatalogRead` says two readers; there are three. Names the third
  (the agent-history panel's `scopePaths` seed) and drops the stale count from
  the module header, which described call sites rather than the two operations.
- `newTabRepoListRead`'s census counted the two operations over `repo.list`, not
  its own two callers, and claimed both read a workspace's connection id. The
  tasks route keeps the whole list for its repo pickers. The split from
  `nativeChatRepoListRead` stays where it belongs: acceptance.
- The `aiVault.history-screen` mutant note pointed at the reply matrix as the
  accepted-vs-refused oracle. Decoding
  `matrix-aivault.history-screen-worktree.ps-1.json` through the value pool
  shows `normal`'s projected state is identical to all seven non-crashing
  partitions (spinner, two labels, zero rows). The real oracles are the next
  request's `scopePaths` (`["/repo/feature"]` vs `[]`) and the crash channel the
  three `inner-*` partitions land in.

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

* docs(mobile): give the second files.list reader its real reason

Comment-only, no product behaviour and no golden movement.

`legacyFileListRead` claimed "the member reader rejects this consumer's
input". Nothing rejects: `rpcUncheckedMemberReader` returns the member,
and reusing it here would simply drop `truncated`. The reason the explorer
declares its own operation is the other direction. Widening
`files.list-or-skip` to a payload reader would split the `workspace-files`
variant it shares with `nativeChatFileSearchRead` over
`files.searchPaths`, whose only caller feeds both through one
`extractPaths` in `use-mobile-native-chat-file-search.ts`, so the member
read would move into that hook rather than disappear.

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

* style(mobile): indent the six scenario entries spliced during the merge

The conflict on `pilot-scenarios.json` was resolved by id rather than by
hunk, splicing this branch's six entries into main's text at the array's
close. The splice started at the entry's `{` instead of at its line, so
those six lines lost their indentation. oxfmt's only change is those six
lines; the parsed document is identical, and the recording suite still
matches all 667 goldens, so no scenario digest depends on the raw text.

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

* test(mobile): re-record the merged goldens once at the pin

One record for the whole merged tree, at the unchanged baseline
e7206f62a8, through a detached worktree reset
to that pin with this branch's rpc-recording tree, scenarios and recorder
script overlaid. Product source in that worktree was proven identical to the
baseline before the run, so the recordings describe the pre-refactor product.

13 goldens move, all of them the ones #20915 added. They arrived carrying the
recorder digest from before this branch edited `screen-native-substitutes.ts`,
and `recorderSha256` is the only key that moves on any of them; every
recording body is identical after decoding through the value pool. The other
654 were re-recorded byte-for-byte and are not in this commit.

All 667 goldens now carry one `recorderSha256`, one `baseline` and one
`lockfileSha256`.

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

* docs(mobile): state the real gates on two screen holdouts

Comment-only, no product behaviour and no golden movement.

The accounts route said "the screen now mounts". It does not, at this
commit: it reads `expo-router.useFocusEffect` and `react-native.ScrollView`,
neither is a substituted member, and the trap refuses before any effect
runs. The note now names that as the first gate and the `accounts.subscribe`
effect as the second, and says why the two members are not added here.

The host-screen overlay note blamed a "reanimated timing driver" for
deciding when the drawer's children exist. Nothing gates them:
`resolveBottomDrawerMounted` returns `visible || mounted`, `BottomDrawer`
renders `MountedBottomDrawer` on that, and that component renders its
children unconditionally inside its `Modal`. The blocker is the module's
own imports of reanimated and gesture-handler, neither substituted.

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

* test(mobile): drop the tasks route adapter's unreachable reload action

No scenario names `reload-repos`, and no schedule driver can generate it:
the drivers emit only disconnect, cutover, reset, unmount, blur and remount.
Every other action on this adapter is reached by a scenario. Deleting the
branch leaves the remount and unmount branches, which are driven.

Re-recorded once at the pin e7206f62a8 with
the product source in that worktree proven identical to the baseline first.
Two goldens move, both in the `tasks.route-repo-list` family, with
`adapterSha256` the only moved key and both recording bodies identical after
decoding through the value pool. The other 665 re-recorded byte-for-byte.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-15 22:10:22 -04:00
Jinwoo Hong 36ef93a64f refactor(mobile): migrate the small domains onto RpcOperation (step 4) (#20705)
* refactor(mobile): pin each RPC golden to its own mount adapter, not every domain's

`recorderSha256` covered the whole recorder directory, mount adapters included, so a domain PR
that adds its adapter module moved the header of all 153 goldens. #20568 did exactly that and its
merge with main conflicted on that one line in 153 files; every future domain PR would collide
with every other in flight the same way.

Split the directory at a real seam instead of a filename convention: `adapters/` holds one module
per domain, registered in `adapters/mounted-operation-modules.ts`, and `recorderSha256` now covers
the engine only. A new `adapterSha256` covers the source of the module that mounts each operation
a golden's scenarios drive, read off the same `mounts` calls that build the table the recording
runs against, so the pin cannot name a file the runner did not use.

Adding a domain's module now re-digests nothing already recorded; editing one fails exactly the
goldens mounted through it. `adapter-seam.test.ts` keeps the split from drifting: an engine file
inside `adapters/`, an adapter defined in an engine file, a register entry naming the wrong file,
and an adapter importing a sibling each fail.

The five adapters that were inline in `pilot-mount-adapters.ts` move into their own modules, which
leaves that file as the registry and nothing else. `GOLDEN_FORMAT_VERSION` goes to 5 for the new
header field; the goldens re-record in the next commit.

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

* test(mobile): re-record the RPC goldens under the split recorder/adapter digest

Header-only. Every changed line is `recorderSha256` (the engine digest no longer covers
`adapters/`), the new `adapterSha256`, or `goldenFormatVersion` 4 -> 5; `baseline` is unchanged and
recording ran against the same pinned product tree.

    git diff -U0 -- mobile/rpc-foundation/goldens | grep -E '^[+-]' \
      | grep -vE '^(\+\+\+|---)' \
      | grep -vE '^[+-]  "(recorderSha256|adapterSha256|goldenFormatVersion)":' | wc -l
    0

The seven `adapterSha256` values partition the 153 goldens by the module each was recorded
through: 58 settings, 37 hosted review, 21 source control, 11 new-tab agents, 9 file inventory,
9 tasks, 8 workspace settings.

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

* refactor(mobile): stop pinning goldens to recorder inputs no recording can read

The adapter split left three per-domain edits still moving all 153 headers: the mutant table, the
per-family mutant registry beside it, and the probe-hole witness. None can change a recording --
the loader consults a mutant only when a mutant test asks for one, and no suite but the two
recording drivers writes a golden -- so pinning them claimed a provenance the goldens do not have
and charged every domain a full re-record for it.

`mutants/` now holds the table, the registry, the reference states, the mutant suites and the
probe-hole witness, and `recorderSha256` skips it. What makes that sound is that no recording can
reach it: `operationModuleLoader` takes a resolved mutation spec instead of importing a table by
name, so nothing on the recording path names `mutants/` at all. `mutants/mutant-seam.test.ts`
checks exactly that, and fails if an engine file names the directory or anything outside imports
from it.

`recorderSha256` also pins only the suites in `recording-drivers.ts`, which
`scripts/rpc-recording.mts` records from, so the two cannot drift. A suite that reads goldens, or
writes one to a scratch directory, is no longer provenance for a recorded file.

`OPERATION_EXPOSURES` went the other way, because it does change what a recording loads: withhold
the resume-metadata exposure and exactly four goldens fail. Each domain module now declares its own
exposures and gets its own loader, so `adapterSha256` pins the ones that reached each golden.

Two assertions in the digest boundary test were vacuous: `join(root, '.')` normalises back to
`root` and hit `recorderSha256`'s per-root cache, so the prose-is-ignored claim never recomputed
anything. Each call now spells the root differently.

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

* test(mobile): re-record the RPC goldens under the mutant and driver exclusions

Header-only, and no format bump: the header shape is unchanged. `recorderSha256` moves on all 153
because the engine set shrank, and `adapterSha256` moves on the 58 settings goldens because that
module now carries its own exposure declaration.

    git diff -U0 HEAD~1 -- mobile/rpc-foundation/goldens | grep -E '^[+-]' \
      | grep -vE '^(\+\+\+|---)' \
      | grep -vE '^[+-]  "(recorderSha256|adapterSha256)":' | wc -l
    0

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

* fix(mobile): restore the preferences actions the merge resolution dropped

#20568 added `resume` and `trust` actions to the `settings.task-preferences`
adapter while it still lived in `pilot-mount-adapters.ts`. This branch had already
moved that adapter into `adapters/task-mount-adapters.ts`, so resolving the
`pilot-mount-adapters.ts` conflict in favour of the registry merge silently
discarded them and `tw-task-preferences-resume-write` failed to record at all
("Missing or completed request: ui.set#1").

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

* test(mobile): re-record the RPC goldens at main's tip after the merge

All 208 goldens, header-only. `baseline` moves from 50e752fc66 to main's tip
c6a7216984, `goldenFormatVersion` from 4 to 5, `recorderSha256` to the value of
the engine with `adapters/` and `mutants/` carved out, and `adapterSha256` is new
on every file. Nine distinct adapter digests over 208 goldens: each golden now
pins only the module that mounts it.

No observation moved. The whole-diff census against origin/main reports exactly
four changed keys and nothing else:

  208 "adapterSha256":   416 "baseline":
  416 "goldenFormatVersion":   416 "recorderSha256":

Recorded in place rather than through the README's detached-baseline dance: this
branch changes no product file, so its tree at the merge is byte-identical to
c6a7216984 under mobile/src, src/shared and the lockfile, and the parity claim
stays non-circular. README says so now.

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

* test(mobile): hold the recording drivers to the engine's mutant-seam rule

The name scan exempted every `.test.ts` on the ground that a test cannot change a
recording. Two of them can: the recording drivers are the recording path. A driver
that read the mutant table by path rather than importing it passed both seam checks
— the import scan sees no import, and the name scan waved it through as a test:

  const table = resolve(import.meta.dirname, 'mutants/operation-mutations.ts')
  console.log(readFileSync(table, 'utf8').length)

at the top of `pilot-recordings.test.ts` gave 2 passed before, and after this change
fails with ["pilot-recordings.test.ts"].

Only non-driver tests are exempt now. This file lives in `mutants/`, which
`recorderSha256` skips, so no golden moves: the recorder suite is green on the
existing 208 with zero dirty.

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

* refactor(mobile): drop the registry parameter no caller varies

`pilotMountAdapters` took `registered` so a caller could mount a different module
set; all six callers take the default. The header-digest tests vary the registry
through `goldenRecording`, which keeps its own parameter and is where the stub
roots need it. Engine source, so `recorderSha256` moves and the goldens follow in
the next commit.

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

* test(mobile): re-record the RPC goldens after the registry parameter came out

All 208, `recorderSha256` only. The re-record against the previous commit moves
416 lines, every one of them that field:

  416 "recorderSha256":

Against origin/main the picture is unchanged from the merge: 208 goldens, 0 added
or deleted, 0 non-header lines, and exactly four keys differing —

  208 "adapterSha256"   416 "baseline"   416 "goldenFormatVersion"   416 "recorderSha256"

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

* docs(mobile): wrap the recording README at the width the rest of it uses

Seven lines this branch added ran past 100 columns, worst 124. No wording changed.
Markdown is outside `recorderSha256`, so no golden moves.

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

* docs(mobile): name the worktree overlay, not the archive that cannot work

`git archive` was offered alongside a detached checkout as a way to lay this
branch's recorder over the pinned baseline. It cannot work: the fence in
scripts/rpc-recording.mts runs `git diff --quiet <baseline>` and an untracked-file
check, both of which need a real `.git`. In an archive tree git exits non-zero for
lack of a repository and the script reports "Product sources or lockfile differ
from the pinned main baseline", which reads as a product mismatch that is not
there. The transport agent lost time to exactly that.

Names `git worktree add --detach` only, and says what the misleading failure looks
like if someone tries an archive anyway. Markdown is outside `recorderSha256`, so
no golden moves.

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

* test(mobile): close two ways an adapter module escapes its own digest

Two holes, one class: the seam was checked by how an import was spelled and by
what the register's values evaluated to, never by where they resolve or where they
were written.

Inward imports: the scan dropped every specifier starting with `..`, so
`'../adapters/settings-mount-adapters'` climbed out of the directory and back into
it unseen. A reviewer had `new-tab-agent-mount-adapters.ts` project a value read
from the settings module, edited that module, and watched the mounted state change
while the new-tab adapter digest held. Specifiers now resolve against the
directory and anything landing back inside it fails:

  ["new-tab-agent-mount-adapters.ts imports ../adapters/settings-mount-adapters"]

The register: `adapters/mounted-operation-modules.ts` is pinned by nothing —
`recorderSha256` skips the directory and `adapterSha256` reads each entry's
`source`. An `exposes` written inline there drives the mounted product module with
no digest covering it. The same reviewer replaced the new-tab entry's `exposes`
with a literal overriding `loadMobileNewTabAgentOptions`; twelve fence tests
passed. Both `mounts` and `exposes` must now be identifiers the register imports
from that entry's own module:

  ["new-tab-agent-mount-adapters.ts writes exposes inline instead of importing it"]

Checked on the register's syntax, not its values, because an inline literal and an
imported binding are indistinguishable once evaluated.

Pinning the register in the engine digest would also close it, and is the wrong
trade: every domain adding a register line would re-digest all 208 goldens, which
is the conflict this PR exists to remove. Keeping the register an index costs
nothing and keeps a domain's line local.

Both fixes live in a `.test.ts` outside the drivers, so no golden moves.

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

* test(mobile): prove the mutant seam from the drivers out, not by spelling

The seam rested on a grep for the literal `mutants`, which the exported
`MUTANT_DIRECTORY` spells without containing. A reviewer had
`pilot-mount-adapters.ts` read the mutant table through that constant and both
checks passed. The README's claim — that nothing on the recording path names the
directory — was false as written.

Three changes, in order of strength:

Reachability is now proved forward. The suite walks the static import graph from
the two recording drivers and fails if any module under `mutants/` is in it. That
answers the real question, what a golden's bytes can depend on, instead of the old
inward scan's question, who mentions this directory. Non-emptiness is asserted on
both sides so a graph that resolved nothing cannot pass by reaching nothing.

The name scan covers both spellings, for paths a module can be read by rather than
imported. The reviewer's probe now fails as ["pilot-mount-adapters.ts"].

`MUTANT_DIRECTORY` is no longer exported. Its two consumers were both tests of the
digest, and they now spell the path instead, which is strictly better for them: a
test that imports the constant follows a rename silently, while one that spells it
fails on a rename — and that specific directory name is the whole soundness
argument. This edits `recorder-digest.ts`, so the goldens re-record in the next
commit.

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

* test(mobile): re-record the RPC goldens after MUTANT_DIRECTORY stopped being exported

All 208, `recorderSha256` only. Against the previous commit the diff is 416 lines
and every one of them is that field:

  416 "recorderSha256":

Against origin/main, unchanged: 208 goldens, 0 added or deleted, 0 non-header
lines, four keys differing —

  208 "adapterSha256"   416 "baseline"   416 "goldenFormatVersion"   416 "recorderSha256"

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

* docs(mobile): state the mutant seam's actual argument, and its edge

The README claimed nothing on the recording path names `mutants/`. That was the
old inward scan's claim and a reviewer falsified it with the exported constant. It
now describes what the check does: a forward walk of the import graph from the two
recording drivers, plus a name scan in both spellings for read-by-path, plus the
constant no longer being exported. It also names the case neither closes — a path
assembled from fragments at runtime.

Markdown is outside `recorderSha256`, so no golden moves.

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

* test(mobile): prove the engine/adapter seam in both directions

The inward scan only held adapters to the seam. An engine file importing an
adapter executes code its own digest skips and that every golden recorded
through another domain leaves out of `adapterSha256`, so the register is now
the only crossing allowed from the engine side.

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

* test(mobile): name what the driver walk missed instead of counting it

Seeding `seen` with the drivers made the driver-presence check true by
construction, and the size bound compared a graph inflated by `typeof import`
product modules against a recorder-sized number. Both go; the walk now reports
the recording files it failed to reach, which is empty today and names an
orphan engine file the moment one appears.

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

* docs(mobile): reflow four paragraphs left ragged by the rewrap

Orphan fragments only, no wording change: the golden-schema field list, the
mutant-evidence paragraph, the probe-witness sentence and the re-anchor note.

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

* test(mobile): record the small-domain call sites before migrating them

Thirteen new families cover the files, new-workspace, host-screen, home and
worktree call sites step 4 migrates next: ownership capture, the preview loader
and its terminal-artifact grant refresh, the artifact save, the tab doc's three
shapes, the drawer's execution target and setup hook, the Codex reset-credit
probe, the host view settings, the Home stats card and the three workspace
catalog reads.

Recorded against main's product code, so these are the parity baseline the
refactor must not move. Four new adapter modules under adapters/ and no engine
edit, so recorderSha256 is unmoved and every existing golden is byte-identical:
40 files added, none changed.

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

* refactor(mobile): send the small-domain reads through RpcOperation

Thirty-five of the domain's fifty-five raw-port references now go through a
declared operation: the files domain's preview, artifact and tab-doc reads and
its ownership capture, the New Workspace drawer, the host screen's metadata and
view-settings mirror, the Home stats card, and all three workspace catalog reads.

No behaviour change, and the oracle says so: zero goldens move. Acceptance is
preserved call site by call site, including two that look like defects and stay
that way — a refused worktree.listRetiredNames still settles as an empty registry
rather than holding the previous names, and a refused ui.get on a null result
still throws into the host screen's own catch.

Where two call sites disagreed about one method, both policies are named: files.read
and files.readPreview throw for a session file tab and skip for the preview screen,
repo.hooks throws for task create and skips for the drawer, and status.get now
carries a fourth family for the Codex capability probe's object-or-null rule.
The drawer's SSH connect, SSH state and agent detection reuse the workspace-create
operations the tasks migration already declared rather than restating them.

Two things outside the call sites. requestSingleFlight now shares the params
optionality rule that request already had, so an all-optional schema such as
preflight.check can omit its params on both helpers instead of only one; that is
type-level and puts nothing new on the wire. And the retired-names fixture
resolved a reply with no `ok`, a shape no host sends, which read as a refusal once
the acceptance policy routed on it.

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

* test(mobile): drive the image arm of the preview loader

The mutation census found two migrated operations that no recording reached:
repointing files.readPreview or files.readTerminalArtifactPreview to a wrong
method, and swapping either one's acceptance policy, changed nothing any golden
observed. Both preview-load scenarios read a text path, so the loader's image
branch was migrated with no wire behind it.

Two scenarios now read an image path through each arm, and the adapter takes the
path from the scenario instead of a constant. All four mutations die on the new
goldens. They are recorded from the pinned baseline with this branch's recorder
laid over it, so they are main's behaviour and not the migration's: the candidate
run against the refactored tree compares clean.

The adapter edit re-digests the nineteen goldens mounted through it. The diff is
one adapterSha256 line each and no observation moves, which is what pinning the
adapter per golden rather than per suite is for.

Two casts also take the SAFETY form the house style asks for.

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

* refactor(mobile): drop the casts the changed-code gate flags

Seven type assertions the gate counted as new, all removed rather than silenced
where the type system could already answer. `normalizeMobileFilePreviewResponse`
narrows on `ok` instead of asserting each arm; the snapshot adapter narrows on
the fetch result's own discriminant; and the drawer's two probe reads go through
one overloaded member read that keeps their optional-chaining behaviour. The
remaining three keep a cast and now carry the rationale on the asserting line.

No behaviour change. The two adapter edits re-digest the sixteen goldens mounted
through them, one adapterSha256 line each with no observation moved, recorded
from the pinned baseline the same way.

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

* refactor(mobile): drop the import the narrowing left behind

RpcSuccess is no longer named once the response reads through its own discriminant.

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

* test(mobile): record the host screen's worktree mutations

Review of #20705 showed the `use-host-worktree-actions.ts` holdout reason was
wrong: its only native call is the pinned-id write, and that sits behind
`if (hostId)`, so mounting with no hostId never reaches it.

Two scenarios in one new family, recorded from the pinned baseline with the call
site still on the raw port. The first drives all three sends so the reply matrix
covers each method; the second refuses `worktree.rm` to pin the row restore.

The adapter is a new module, so no existing golden's `adapterSha256` moves and
none of the 250 goldens already here is rewritten.

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

* refactor(mobile): send the host list's worktree mutations through RpcOperation

Pin, remove and activate move onto operations in host-screen-operations.ts. All
three skip on refusal, which is the policy each site already applied by hand: the
pin and activate writes swallow everything in a `.catch`, and the remove restores
the row on a refused reply. `worktree.set` therefore carries a second policy next
to source-control's `worktree.set-review-link`, which throws; both are named.

Zero goldens move. The inventory loses use-host-worktree-actions.ts and states
the real reason the drawer's repo list stays: it renders the last-visited-repo
hook, whose default import of async-storage the recorder's proxy refuses at
module load, before the hostId guard the reviewer expected to save it.

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

* test(mobile): type the worktree-action fixture row and follow the activation scan

The fixture row I recorded from had four fields, which `tsc` rejects as a
`Worktree`. Filling it out moves the five goldens of this branch's own new family
and nothing else; the recorded sends are unchanged, only the projected row is.

`mobile-worktree-activation-source.test.ts` scanned the hook for the literal
`sendRequest('worktree.activate'`, which the previous commit replaced. It now
asserts the operation call and its two flags in the hook, plus the method in
host-screen-operations.ts, so the pair still pins the same wire.

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

* refactor(mobile): take the five deletions round-1 review asked for

- `fileOwnershipRuntimeStatusRead` was `taskRuntimeStatusRead` field for field.
  It is now a re-export of it. The goldens are keyed on the scenario family, not
  the operation name, so `matrix-files.mutation-ownership-status.get-1.json`
  survives unchanged.
- `readProbeMember`'s two overloads asserted shapes nothing checked. Gone; the
  nested read goes through the same single-signature function.
- `normalizeMobileFilePreviewResponse` had no product caller. Deleted with its
  re-export; its twelve assertions now drive the accepted and refused arms
  directly.
- The three inline copies of the accepted-result union are gone. They name each
  operation's own `interpret` return instead of importing `RpcAcceptedResult`:
  importing the contract would pull all three call sites into the cast fence,
  where their existing SAFETY assertions fail it.
- `codex-reset-credit-capability-operation.ts` is now `-operations.ts`. No
  adapter names it, so no golden re-digests.

Zero goldens move.

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

* refactor(mobile): give the skip verdict its own transport module

The three settle helpers typed their interpret parameter as
`ReturnType<typeof <op>.interpret>`, naming one operation while being called with
others whose verdicts happen to be structurally identical. Narrowing a named
reader would have silently retyped unrelated helpers.

`RpcAcceptedResult` moves to `rpc-accepted-result.ts`, outside the cast fence's
three region seeds, so a consumer can name the verdict without becoming an
operation implementation. `rpc-operation-contract.ts` re-exports it.

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

* refactor(mobile): drop three sender aliases nothing imports

MobileHostScreenRpcSender, MobileNewWorkspaceRpcSender and
MobileWorktreeCatalogRpcSender each appeared only in the file that declared
them. A named type with no consumer is a cost, not a boundary.

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

* docs(mobile): say why a holdout is a holdout in the port inventory

A site can be pointed at an operation without being mountable, so "cannot be
migrated" was the wrong claim. The rule is record-first: the golden recorded
against the old code is the only parity proof, so a site the recorder cannot
mount cannot be recorded, and unrecorded sites do not migrate. Stated once in
the list's header.

codex-reset-credit.ts loads fine under the module loader; probed it, and its
attempt-journal access throws on async-storage at call time before the send,
with no guard to skip it. The old comment described it as a storage read
around the send.

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

* docs(mobile): state the status.get policies without counting them

"the fourth policy on this method", "the first of two" and "the second of two"
were already wrong after round 1 folded the files family's status read into the
tasks one. Each comment now states its own invariant, which no later policy can
invalidate.

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

* test(mobile): stop the activation scan claiming to pin the wire

`expect(operations).toContain("method: 'worktree.activate'")` passes whichever
operation carries that method, so it survives swapping worktreePinWrite's and
worktreeActivate's methods. tsc and the host-worktree-actions-pin-open-delete
golden both fail on that swap; the scan keeps only what it can prove, which is
that the callback sends through worktreeActivate with the two flags.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-15 13:10:32 -04:00
Jinwoo Hong 85d7cf3cc1 fix(mobile): preserve delivery ambiguity across transport cutover (#20280)
* fix(mobile): preserve delivery ambiguity across transport cutover

Let physical close settle requests and retain its error as the cutover cause, copying only an existing delivery-unknown mark. Pin sent and unsent caller outcomes and both cutover predicate carriers.

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

* docs(mobile): pin the RpcClient.close() settlement contract

close() was declared `() => void` with no stated obligation. That was harmless
while migrateTo rejected pendings itself; now that it does not, close() is the
retiring generation's only settlement path, so a type-compatible implementation
that leaves a request pending strands its caller for good.

States the obligation on the declaration and pins it for both trackers the real
implementations reject through. Dropping the delivery-unknown flag, dropping the
relay mark, or leaving pendings in the map each fail a test.

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

* test(mobile): read the cutover cause without a type assertion

main's new casting gate rejects `(error as Error).cause`; narrow instead so the
assertion still distinguishes a missing cause from an unmarked one.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-13 18:23:13 -04:00
Jinwoo Hong d936d8da82 revert(mobile): pull the relay connect-speed mobile pass pending a smaller, verified re-land (#19348)
* Revert "feat(mobile): time relay dial stages so diagnostics say where a slow connect went (#19245)"

This reverts commit 83b1558ecc.

* Revert "perf(mobile): race the direct and relay dials from t=0 on every reconnect (#19308)"

This reverts commit ceafdcad2f.

* Revert "feat(mobile): draw the last known tab strip while a session reconnects (mobile pass) (#19281)"

This reverts commit 643571def6.

* Revert "perf(mobile): open a session with parallel startup RPCs and a pre-warmed terminal engine (#19260)"

This reverts commit c37413271e.

* Revert "perf(mobile): cut the relay reconnect critical path and admit dead sockets faster (mobile pass) (#19280)"

This reverts commit e628090ad4.

* chore: keep the react-doctor suppression for the startup timers

The pattern it covers (a variable number of timers cleared through one cleanup)
predates #19260 and is unchanged by the revert; dropping the entry only re-exposed
a pre-existing finding to the changed-code gate.
2026-09-07 16:44:26 -04:00
Jinwoo Hong c37413271e perf(mobile): open a session with parallel startup RPCs and a pre-warmed terminal engine (#19260)
Startup RPCs now fan out in parallel and the xterm engine pre-warms inside the
real terminal frame while they are in flight, so the first pane inherits a warm
WebView and an already-measured viewport instead of paying a round trip for it.

The pre-warm opens its engine before measuring: web-ready only reports that the
bundle loaded, and the WebView answers a measure with null until a terminal
exists. It also pre-warms at the user's saved text size, because cell size is
what the frame height gets divided by.

Host writes such as worktree.activate wait for an evaluated status.get reply.
Navigation still fails open when a host cannot answer one, but that fallback no
longer reads as a passing compatibility verdict.
2026-09-07 13:16:44 -04:00
Brennan BensonandMerge Sim 7f8eb90ac3 Align worktree host labels across desktop and mobile (#18237)
* refactor: align worktree host labels across clients

* fix(mobile): expose safe host display labels

* fix(mobile): preserve legacy mixed-host labels

---------

Co-authored-by: Merge Sim <sim@local>
2026-09-02 15:32:06 -07:00
Neil 3db5a6aab8 perf(mobile): index slept worktree identity lookups (#17512) 2026-08-30 20:47:58 -07:00
Neil 15abc3fe26 Split mobile host screen layers (#17181)
* Split speech session lifecycle

* Split terminal output scheduler pipeline

* Split mobile browser pane modules

* Prune resolved max-lines suppressions

* Split pane tree equalization logic

* Extract mobile troubleshoot screen styles

* Split external automation manager

* Split main window service attachments

* Split hosted review creation checks

* Split automation dispatch event handling

* Split settings navigation metadata

* Split daemon initialization lifecycle

* Split GitLab item dialog

* Split relay dispatcher layers

* Split mobile host screen

* Retarget mobile view settings source test

* Fix F3-speech for #17123

* Fix F1-cycle for #17131

* Fix F4-navtest for #17157

* Fix F2-allowlist for #17161
2026-08-29 20:16:28 -07:00
Jinwoo Hong 5c10bf9001 fix(sta-5781): stop cross-client resets of workspace view preferences (#17057) 2026-08-28 15:20:21 -07:00
Jinwoo Hong bf5660df51 feat(mobile): add causal network diagnostics (#16837) 2026-08-27 21:42:23 -07:00
Brennan Benson d2a35eebe3 fix(mobile): avoid unsupported Hermes array sorting (#16506) 2026-08-25 17:11:09 -07:00
Neil aa4c9c707c Refactor mobile home, worktree modal, and RPC client (#16165)
* refactor(mobile): split home modal and rpc client

* fix(mobile): restore render-phase remount key in NewWorktreeModal

The split moved the form-reset epoch from render-phase refs into
useState + useLayoutEffect, which changed when the remount key is
computed. On the render where visible flips false->true the key was
still the old epoch, so the previous session's NewWorktreeModalContent
rendered with visible === true carrying stale form state. Child layout
effects run before the parent's, so visible-gated hooks
(useNewWorkspaceRepositories, useNewWorktreeDrawerNavigation,
useNewWorkspaceRuntimeContext) fired for that stale instance before the
parent bumped the epoch and remounted.

Restore the ref-based computation so the key is correct on the first
render where visible flips true, keeping the composite open/client
epoch semantics and the file split intact.
2026-08-24 23:50:10 -07:00
Brennan BensonandNeil ec4687c434 feat(agents): distinguish Claude background monitoring (takes over #14205) (#16201)
* feat(agents): distinguish Claude background monitoring

Adds an optional `workingMode: 'monitoring'` discriminator for a Claude
session whose lead turn finished but which still has background shell tasks
or session crons registered. The wire state stays `working`, so older peers
that never read the field keep rendering Working.

(cherry picked from commit d5d54b4bdd)

Rebased onto current main (554 commits of drift) by Brennan Benson;
conflicts resolved by keeping both sides where main and this branch made
independent additions to the same construct.

* fix(sidebar): keep monitoring status visible

(cherry picked from commit fd6b38654d)

* test(agents): cover Claude monitoring drain

(cherry picked from commit ce4d61ebf8)

* test(mobile): avoid unresolved renderer test type

(cherry picked from commit bbcfa35ff9)

* feat(agents): render Claude monitoring as a static turquoise dot

Replaces the yellow Radio glyph from #14205 with a static dot in a new
--agent-monitoring token (#8abeb7), defined once for light and once for
dark like --workspace-status-done, so the status keeps its identity when
the theme flips. Deliberately a fixed UI value: it never reads terminal
theme state at runtime.

Adds the turn-boundary notification pins. The monitoring predicate and
the turnCompletedAt stamp are computed from the same "lead said done but
the pane resolves to working" expression, so a rename can silently drop
the stamp and kill a completion notification that works today with
nothing else going red.

* revert(agents): restore the yellow Radio glyph for monitoring

Brennan chose #14205's original treatment over the turquoise dot, so the visual
goes back to nwparker's: lucide Radio in text-yellow-500 across the sidebar,
dashboard dot, cmd-j palette and agent-map ring.

Reverts only the visual surface. The turn-boundary notification pins stay — the
monitoring predicate and the turnCompletedAt stamp share an expression, so a
rename can silently drop the stamp and kill a completion that works today with
nothing else going red. The --agent-monitoring token is removed with its last
consumer rather than left dead in main.css.

---------

Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
2026-08-24 15:24:32 -07:00
Brennan Benson fab6e0d6e7 fix(mobile): scope optimistic workspace removal to the deleted host (#15424)
* fix(mobile): scope optimistic workspace removal to the deleted host

A worktreeId repeats across hosts, so filtering the list on the bare id also
removed the identically-named workspace belonging to the other host. Match on
(worktreeId, hostId) through a named helper so the rule is testable.

* fix(mobile): key host worktree rows consistently
2026-08-19 17:12:44 -07:00
Jinjing 7aaa7c6f5b refactor(sidebar): group worktree-list files by domain (#14486)
* refactor(sidebar): group worktree-list files by domain

Follow-up to #14465 / #14467. Keep the landed extract and reorganize the
flat worktree-list dump into drag/, headers/, reveal/, rows/, scroll/,
and viewport/. Fold tiny modules into their owners, move leftover
sidebar-root files into the module, and retarget imports and source-path
tests. Layout-only; no behavior change.

* fix(sidebar): merge duplicate virtual-rows imports

Inlining virtual-row-dom-attributes left a second import from the same
module, which fails audit:code-quality:native --deny-warnings.

* refactor(sidebar): condense indentation comments

Shorten explanations to focus on the essential why, removing redundant
detail and improving readability without changing functionality.

* refactor: organize worktree-list into lifecycle dest folders

* fix react doctor

* fix: update reliability-gates path after worktree-list reorg

host-filtering.test.ts moved from viewport/ to listing/; keep the
runtime-routing.active-server-preference gate pointing at the real file.

* Extract workspace status colors to design tokens

Define theme-aware color tokens for workspace PR-state indicators (done, in-review, in-progress) to ensure consistent identity across theme switches. Update references to use the new tokens and refactor EmptyState button to use the Button component.

* fix(sidebar): stop mutating refs during worktree-list render

React Doctor fails static analysis when refs are written in render.
Commit reused array identity and the Smart live-signal latch after
paint, and return the attention map from the sort memo instead of
stashing it on a render-time ref.
2026-08-15 13:40:09 -07:00
Brennan Benson ab9d1a29a9 fix(worktree): never reissue a generated workspace name (#14350)
* fix(worktree): never reissue a generated workspace name

Generated workspace names were deduped only against currently-live
worktrees, so deleting a workspace returned its name to the pool. A later
workspace could draw the same name, land on the same directory path, and
inherit the previous occupant's agent conversation history — coding-agent
CLIs key their prompt history and transcripts by cwd.

Names are now retired permanently per repo. The registry is written in
main with the name Git actually used (the create loop can advance past a
requested name on collision), and seeded once per run from workspace
directories and surviving agent transcript buckets so already-spent names
are excluded from the start. Suggestions degrade to -2, -3 variants
instead of recycling, and those variants retire too.

User-typed names are untouched: retirement filters suggestions only.

* fix(mobile): honor retired workspace names, on one shared implementation

Mobile hand-duplicated the desktop name-suggestion algorithm and deduped
only against live workspaces, so a phone could still be offered a name
whose deleted workspace left agent conversation state behind at that path.

Both platforms now call one shared selector in src/shared, so the two can
no longer drift. The host publishes retired names as an optional field on
the existing worktree.list response, and mobile fetches them per selected
repo while the create sheet is open — mirroring the desktop hook.

Mobile never calls worktree.list for its catalog (it uses worktree.ps,
which carries rows only), so this is a targeted request rather than a
change to the catalog or its cache. Hosts predating the field omit it and
mobile falls back to live-only dedupe, which is the pre-change behavior.

* fix(worktree): close retirement consistency gaps

* test(worktree): cover retirement runtime contracts

* fix(worktree): retire generated collision names

* fix(worktree): enforce retired names at creation

* refactor(ai-vault): extract the Claude project-dir encoder

The bucket-name encoder and its scope-boundary check were private to the
session scanner, so a second consumer had to reimplement them — and got the
per-character encoding wrong. Move both to a shared module with direct tests.

* fix(worktree): make the retirement seed scan actually match buckets

The bucket encoder collapsed runs of non-alphanumerics while the real one
emits a dash per character, so every dot-path bucket missed and the Windows
default workspace root (C:\...) matched nothing at all. Reuse the shared
encoder and its boundary check, which also stops a repo absorbing a sibling
whose path merely shares its prefix.

Also:
- Derive the workspace leaf by stripping the known encoded parent instead of
  guessing from trailing dash segments, which retired the parent directory's
  name whenever a workspace was named numerically.
- Reuse isAutoGeneratedCreatureBranchName so the -10 and -100 tiers retire.
- Drop the .codex/sessions root: Codex keeps the cwd inside the transcript
  rather than in a directory name, so the scan could only ever see a year
  folder. Reading transcript contents is not a trade this feature justifies,
  so the gap is documented instead.
- Honor CLAUDE_CONFIG_DIR, which relocates the bucket root.
- Delete the unused retirableLeafName export.

Tests write buckets with the real per-character encoding against a fake home,
covering POSIX, dot-directory, Windows drive and WSL UNC roots; all three
platform cases fail against the previous encoder.

* fix(worktree): retire only generated names, keyed by cwd namespace

Two problems in the host-side registry.

Retirement fired for every create, including names the user typed. The
creature pool contains ordinary words — orca, runner, sole, molly, oscar — so
typing a retired 'nautilus' silently produced directory and branch
'nautilus-2' and burned the name for good. Creates now carry an explicit
nameWasGenerated flag; both the skip and the retire are gated on it, and it
defaults to false so CLI and automation callers are unaffected.

The registry was keyed by repo id, but both readers already discarded the id
and unioned by the cwd collision key, because the collision this prevents is
on the path. Keying by that namespace directly fixes several things at once:
entries no longer orphan when a repo is removed, remove/re-add no longer loses
every retirement for an unchanged path, the missing removeProject prune is
moot, and the backfill promise no longer merges into only the first repo id it
saw. The feature is unreleased, so no migration is needed.

Also:
- Memoize the collision key. It runs computeWorktreePath, which for a WSL repo
  is a blocking execFileSync('wsl.exe') whose failure path is uncached, and
  the previous code recomputed it once per repo on every create and every
  listRetiredNames call.
- Drop retiredNamesByRepo from the worktree list result. It had no readers and
  leaked onto 'orca worktree list --json', and its awaited backfill sat on CLI
  selector resolution. The dedicated listRetiredNames RPC keeps its consumers.
- Make the three RuntimeStore methods required. RuntimeStore is file-private
  with two constructors, so the 'older embedders' the optionality protected do
  not exist, and the optional chain silently returned no retirements.
- Revert the unrelated forceDeleteBranch rewrite, and make room under the
  file's line budget by extracting the create-args mapping instead.

* fix(worktree): send name provenance and stop gating Create on the fetch

Desktop and mobile now mark a create as generated-name only when the user
typed nothing and the composer fell back to the suggestion, so the host knows
which names it may retire.

Remove the retired-names loading gate from every create path. The host already
skips retired candidates before doing any git work, so the client gate bought
nothing while it could disable Create for the length of a full mobile
reconnect ladder (the wait had no timeout) and blank the desktop button
between queued creates. The suggestion still waits; the button never does.

Also make the web client call worktree.listRetiredNames instead of hardcoding
an empty list — the method is registered and mobile-allowlisted, so the
comment claiming no wire call existed was wrong — and filter the mobile
response to strings so a malformed row cannot throw during normalization.

* fix(worktree): key retirement by repo id and prune it with the repo

Reverts the collision-key storage key. It was a function of workspaceDir,
nestWorkspaces, worktreeBasePath and repo.path, so toggling any one of those
orphaned every retirement for every affected repo at once — trading a rare
churn (remove/re-add) for a common one. The read path already unions by cwd
namespace at query time, so cross-repo sharing never depended on the storage
key.

Instead, address the growth and orphaning directly:
- Drop the registry in removeProject, and in removeProjectForHost once the last
  host's copy of the repo id is gone, alongside the sparse-preset deletes that
  already follow this convention.
- Bound each repo's registry. The cap sits far above the 552-name pool because
  evicting inside it would reissue a name whose agent state is still on disk;
  only -2/-3 tier accumulation can ever reach it.
- Carry retirements through profile transfer, re-keyed to the destination repo
  id and dropped from the source, mirroring sparsePresetsByRepo.

Separately, fix the backfill merge: the scan promise is cached per cwd
namespace, but it closed over the first repo id that triggered it, so a second
repo in the same namespace received nothing. The scan stays shared; the merge
moves out of the cached promise and runs for whichever repo asked.

Local repos re-seed on re-add through that backfill. SSH repos do not — the
scan cannot see the execution host — which is now stated in the module.

* docs(worktree): spell out why the retirement bound sits above the pool

Names the trap directly: the neighbouring 50/200 bounds cap histories, so
lowering this one to match them would silently start reissuing names whose
agent state is still on disk. Also states that oldest-first eviction is a
deliberate least-bad choice rather than a neutral one.

* fix(worktree): send name provenance from the web runtime client

This client hand-enumerates worktree.create params, so the new optional field
was silently dropped and typecheck could not see it. On web and paired-desktop
the host therefore never received it: generated names were never retired, and
the host-side skip that backstops a stale suggestion was disabled too. The same
client does fetch retired names for suggestions, so it was filtering against a
registry nothing ever wrote to.

The test asserts both directions, and fails without the fix.

* fix(worktree): retire names that took more than one collision suffix

isAutoGeneratedCreatureBranchName strips exactly one trailing -N, which is
right for auto-rename eligibility but wrong here. Once the pool is spent the
suggester emits nautilus-2, and a collision on that yields nautilus-2-3 —
which a single strip leaves as nautilus-2, not a pool name, so retirement
no-opped at exactly the tier where every base name is already gone. Strip
repeated suffixes locally rather than moving the auto-rename predicate.

* perf(worktree): keep the retirement backfill off the blocking WSL probe

The backfill runs on composer repo-select, not just at create time, and it
derived the probe path synchronously — which for a WSL repo with a mirrored
workspace dir reaches getWslHome and its blocking execFileSync('wsl.exe').
A stopped distro froze the main process for up to 5s on composer open.

Adds an async twin of computeWorktreePath and uses it for the probe. Resolving
the home there also warms the shared cache, so later sync callers are free.

Also stops memoizing the collision key when the WSL home is still unresolved:
only the success path is cached upstream, so caching the fallback namespace
would strand the repo there for the rest of the session.

* fix(worktree): hold retired names across a refresh instead of blanking

refreshKey changes on every workspace-list mutation, so create-multiple
refetches after each create and the hook returned an empty list until the
refetch landed — precisely the window in which resetForNextCreate clears the
name field and a fresh suggestion is drawn. Keep the previous answer while
revalidating and reset only when the repo changes; a failed refresh keeps what
was already loaded rather than un-retiring everything.

Also makes the returned array referentially stable, so the suggestion memo
downstream stops rerunning on every refetch.

* refactor(worktree): put the retired-name cache rules on one implementation

The desktop and mobile hooks that fetch retired names had already drifted
four ways. The transports genuinely differ (IPC vs RPC), but the caching
rules must not, and mobile's copy reset to [] on any error -- which
un-retires every name for the rest of the sheet session, the one outcome
retirement exists to prevent.

Moves the rules into src/shared/worktree/retired-name-cache: response
normalization, the never-leak-across-repos rule, and the hold-previous-on-
failure rule. Pure, no React, because src/shared is on the main process's
import graph. Each platform keeps its own transport and effect.

Mobile moves up to desktop's behavior: it now holds the previous answer
through a failed refresh, and refetches when the workspace list changes
instead of never refetching after mount.

Also drops the unused `loading` return. Neither platform consumed it; its
only consumer was the Create-button gate reviewed out earlier, and removing
it makes that regression unexpressible.

* fix(worktree): import shared types from their real modules

Main dropped the src/shared/types barrel, so the retirement module's import
resolved locally but not against the PR's merge base.

* refactor(worktree): bound the retirement registry by tier compaction, not eviction

Retirement is a correctness guarantee — a spent name's directory may still hold
agent conversation state keyed by that cwd — so the 2000-entry cap was the wrong
shape: reaching it handed a name back. At the owner's measured rate (~6.6 pool
names retired per day in one repo) the cap was ~9 months out.

Names come from a fixed 552-entry pool and the suggester only reaches tier N+1
once every tier-N name is taken, so a completed tier is exactly a set that no
longer needs listing. A row is now a watermark plus the names above it: reads
answer at-or-below the watermark with no lookup, and compaction drops the 552
entries the watermark now covers. Bounded at one pool per repo forever, with no
eviction and nothing un-retired.

Tiers can complete out of order (a create-time collision can spend `nautilus-2`
while tier 1 is open), so compaction loops and higher-tier names simply wait.

The RPC result carries the watermark beside the names as a new field; a client
predating it reads the names only and under-retires the compacted tiers, which
degrades to the pre-retirement behavior rather than breaking.

* fix(worktree): preserve generated name retirement across failures
2026-08-14 22:18:36 -07:00
Neil 77f23b013f refactor(shared): drop the shared/types barrel and import from the real modules (#14447)
#14397 split `shared/types.ts` into 46 per-domain modules but kept the path as
a re-export barrel so the import sites did not have to change. This removes
the barrel: every consumer now imports from the module that actually declares
the type, and `src/shared/types.ts` is deleted.

Barrels hide where a type lives, make every consumer look like it depends on
the whole domain, and let an unrelated edit invalidate a module that ~2,000
files transitively import.

2,323 import declarations across 2,321 files. Rewritten mechanically: each
specifier was resolved to an absolute path via the TypeScript AST and
recomputed, rather than string-substituted, so alias forms (`@/../../shared/
types`) and per-specifier `type` modifiers survive.

Four cases the mechanical pass had to handle, each found by a gate rather than
by reading the diff:

- Modules inside `src/shared` import the barrel as `./types`, not
  `shared/types`. A pre-filter on the latter string skipped 176 of them and
  left imports dangling at a deleted file, which surfaced as confusing
  `Property 'x' is optional in type 'Repo' but required in Pick<Repo, ...>`
  errors rather than "module not found".
- The barrel RENAMED one type on the way through
  (`WorkspaceSource as WorkspaceCreateTelemetrySource`), so the original name
  in the owning module has to be re-aliased at each consumer.
- Three test files put `;(globalThis as ...)` on the line after the import.
  TypeScript parses that `;` as the import statement's terminator, so
  replacing through `statement.getEnd()` deletes it and breaks ASI. The
  rewrite now stops at the module specifier.
- A file that already imported directly from a module got a SECOND import
  from it, because the barrel re-exported those same names — which trips
  `import/no-duplicates` under `--deny-warnings`. A post-pass merges
  declarations sharing a specifier and type-only-ness; the `import type` plus
  `import` pair from one module is left alone, since that form is allowed.

Splitting one barrel import into several genuinely adds lines, which pushed
`terminal-layout-pty-ownership.ts` to 301 counted lines: its 107-character
import must wrap, and neither local type collapses onto one line (101 and 116
characters). Rather than contort a type declaration to fit a line budget,
`collectLeafIds` and `pruneLeaves` move to `terminal-pane-layout-tree.ts` —
they are pure structural operations on the layout tree and independent of PTY
ownership. `visible-worktrees.ts` similarly loses its own mini-barrel
re-export of `isDefaultBranchWorkspace`, with the four real consumers
repointed at the declaring module. No `max-lines` bypass added.

Verified: cold `tsc --noEmit` green on node, cli, and web (buildinfo deleted
first — these projects are `composite: true` and reuse stale caches); the full
`pnpm lint` green, not just bare oxlint — the narrower local check is what let
the duplicate imports reach CI; max-lines ratchet OK at 344.
2026-08-13 22:48:24 -07:00
Jinwoo HongandOrcaWin 738f640428 fix(mobile): relay UX overhaul — steady status colors, visible relay dials, coordinated deep links (F1-F10)
* fix(mobile): keep healthy relays green through focus and network nudges (F1+F2)

Focus/app-resume nudges probe the active relay instead of suspending it;
network-change nudges replace it make-before-break, suspending only after a
failed dial. Mount, Retry, and host-swap windows read 'connecting' instead of
'disconnected'; the host list keeps last-known worktrees for every
not-connected state and spins instead of rendering nothing.

* feat(mobile): surface the pairing relay path in the pairing log (F3)

The relay candidate was silent during pairing: dialing, E2EE handshake,
director recovery, and the winning path now emit redacted phase lines through
the same connectOptions.onLog the direct path already used.

* docs(mobile): relay UX investigation findings and F0-F10 fix plan

* feat(mobile): name and narrate relay dials while they happen (F5)

migrateTo forwards the dialing session's connecting/handshaking/reconnecting
phases whenever the client is suspended or disconnected — never downgrading a
live session — and exposes getPendingPath so the host card can say
'· Orca Relay' during the dial instead of only after it.

* feat(mobile): race a relay dial when the direct dial stalls (F6)

A 2.5s grace timer starts relay recovery while an unauthenticated direct dial
is still inside its 12s connect window; the race gets one attempt through the
existing mutex/cooldown machinery, cancels when direct authenticates, and
never arms for hosts without a relay endpoint.

* fix(mobile): overlay the protocol gate instead of unmounting the host stack (F9)

A pending status.get used to swap the mounted HostStack for a spinner at the
moment the socket connected, destroying in-flight nested navigation. Once
children have rendered for a host they stay mounted under an opaque
touch-blocking overlay; first visits and blocked verdicts keep the old
behavior.

* fix(mobile): keep loaded data through transient connection blips (F10)

Git history no longer blanks on reconnect (and commit files refetch instead
of caching an offline empty answer), the repo picker keeps its last-good list
when an in-flight repo.list rejects, the diff review's ready-state
preservation actually runs, and proven host capabilities survive a drop
flagged unverified instead of being wiped.

* feat(mobile): coordinate every home deep push and bounce dead resume targets (F4+F7+F8)

Notification taps, the Accounts card, and host-edit now use the shared
mount-then-replace transition (with a focused-route walker so root-layout
scope works); the Resume card renders from the snapshot in a disabled state
so its late arrival can't shift Tasks under the thumb; resume targets are
validated against proven catalog data, and a session route whose worktree
the host proves missing bounces to the host index with a notice banner
instead of stranding on a dead screen.

* test(mobile): cover the resume-target and notice policies (F7)

Key notice dismissal by code so closing one banner cannot swallow a later,
different one, and move the visibility rule into host-route-notice.ts where it
is testable without a screen.

Adds the missing units for F7's decision points: isResumeTargetConfirmedMissing
(unproven catalog is silence, synthetic routes exempt), the validating
last-visited reader, and the notice visibility rule.

* fix(mobile): review-pass hardening for the gate overlay and diff preservation

Adversarial review findings: the reader's hunk position now survives a
connection blip (reset only on item change), the covered stack is hidden from
TalkBack while the gate overlay is up, and the overlay's hit-test comment is
scoped honestly to in-tree views (native-Modal drawers present above it —
follow-up).

* fix(mobile): CI + CodeRabbit review fixes for #12609

Move the findings doc under docs/ (root directory guard), drop two unused
eslint-disable directives, and address review findings: an unproven snapshot
seed can no longer downgrade a proven worktree catalog; a locally-aborted
relay dial skips the director fallback; post-migration bookkeeping failures
log instead of masquerading as dial failures (which could suspend the healthy
session); the auth wait arms its timeout before subscribing; forwarded dial
phases stop at close(); the legacy selector_not_found fallback requires
runtime_error; the diff-loading effect depends on the fields it reads; and
host-edit auto-cancellation is now pinned by a test.

* fix(mobile): second review round — queued replacements, race fence, confirmed bounces

A network-change replacement now survives the recovery mutex and cooldowns as
a queued intent instead of being dropped or suspending a healthy session —
only a failed dial or a dead probe tears one down. The happy-eyeballs
migration withdraws when direct authenticated during the relay dial
(first-authenticated-wins). A worktree bounce requires two consecutive
host-proven misses, since a transient desktop repo-scan rejection answers
selector_not_found for a live worktree. Background network flaps no longer
wake a billed relay splice, the lifecycle foreground flag stays in sync, a
screen unmount cancels only its own pending host-stack transition, and diff
review keeps the loaded review when its reconnect refresh rejects.

Extracted mobile-endpoint-nudge-router.ts and the establisher's dialEligible
pass, and split the supervisor nudge tests, to stay under max-lines.

* fix(mobile): satisfy the React Doctor changed-code gate

Render-phase ref writes move into effects: the protocol gate's resolved/mounted
latches now record committed outcomes only (a discarded children render can no
longer count as mounted), and the bounce hook syncs its callback ref in an
effect. Array<T> annotations become T[] in the extracted modules.

* fix(mobile): keep the loaded diff when the reconnect refetch rejects (F10)

The diff-loading hook's catch was the one path still erasing a ready diff —
the same keepLoadedDiff guard its disconnect and loading branches already use,
now pinned by a reject-after-ready test.

* fix(mobile): process foreground revival nudges

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-08-04 19:47:39 -07:00
NeilandOrca 96e31e7bf8 Remove a paired computer's deleted projects from every connected device (#12215)
* fix(repos): remove a paired computer's deleted projects from every connected device

A project deleted on a paired Orca host stayed in every connected client's
sidebar and could not be removed there.

Two independent defects:

1. Host-local repo IPC mutations only sent `repos:changed` to the host's own
   renderer (src/main/ipc/repos.ts:2711). The runtime client-event stream was
   fed only by mutations arriving over runtime RPC, and clients refetch a remote
   catalog only on a `reposChanged` event -- there is no polling on desktop -- so
   the deleted rows persisted indefinitely. The shared `notifyReposChanged`
   helper now also calls the new
   `OrcaRuntimeService.notifyReposChangedForRemoteClients()`
   (src/main/runtime/orca-runtime.ts:5175), mirroring the existing
   `notifyWorktreesChangedForRemoteClients` precedent. This covers every repo,
   project-group and folder-workspace IPC mutation, so renames, colors, reorders
   and adds propagate too.

2. Deleting the ghost row on the client routed `repo.rm` to the owner, which
   answered `repo_not_found`. `removeProject` wrapped its whole body in one
   try/catch, so the rejection aborted the local purge before the `set()`
   (src/renderer/src/store/slices/repos.ts:3466) and the delete button appeared
   to do nothing. Only `repo_not_found` is now tolerated; any other failure still
   keeps the row, and an opt-in `errorFeedback: 'toast'` makes it visible at the
   three single-project user-initiated entry points. Bulk and background callers
   keep today's silence plus their own aggregate reporting.

Closes #11994

Co-authored-by: Orca <help@stably.ai>

* fix(repos): revert inert RepositoryPane removeProject arg

The settings pane's only render site drops the argument; the toast is
already delivered by removeSettingsProjectFromAllHosts.

Co-authored-by: Orca <help@stably.ai>

* fix(repos): scope duplicate-repo-id deletes to the owning execution host

Cover the cross-host collisions #11994's broadcast now fans out to every paired
device. Same-name projects on different hosts were already isolated (per-host
UUIDs, host-scoped catalog merge and purge) and are pinned by regression tests.

Two same-repo-id paths were not: `repo.rm` with a `path:`/`name:` selector and
`deleteProjectHostSetup` both resolved one row and then deleted by bare id,
taking the sibling host's registration with it.

Co-authored-by: Orca <help@stably.ai>

* test(mobile): align the poll-interval rationale with the new reposChanged emission

Co-authored-by: Orca <help@stably.ai>

* fix(repos): resolve deleteProjectHostSetup's repo row only on the setup's own host

The sibling-host fallback could only ever pick a row on a host the caller
did not name; with no exact match the setup is stale and the existing path
already drops just the setup.

Co-authored-by: Orca <help@stably.ai>

---------

Co-authored-by: Orca <help@stably.ai>
2026-08-04 01:26:48 -07:00
OrcaWinandOrcaWin fb1259a09d fix(mobile): keep cached workspace counts across a transient RPC failure (#12408)
* fix(mobile): keep cached workspace counts across a transient RPC failure

The Home host card showed "12 worktrees · 2 active" until any worktree.ps
failed — a backgrounded app, a Wi-Fi→cellular handoff, or a sleep/resume
that kills the socket mid-request. Two things then went wrong:

- render dropped the counts: `markHomeWorktreeCatalogUnavailable` kept the
  proven numbers in state, but the card only rendered them when
  `catalogUnavailable` was unset, so the line collapsed to "Worktree list
  unavailable" even though the last successful counts were right there.
- nothing re-drove the fetch: the per-host wiring latched a `statsFetched`
  boolean on the first connect, and the logical client survives socket
  drops, so its reconnect never re-read the catalog. The card stayed wrong
  until the user navigated away and back.

Keep the proven counts and flag them stale (`staleCounts`), rendered as
"Last known: 12 worktrees · 2 active"; a host whose catalog never loaded
still reads "Worktree list unavailable" (STA-3123). Replace the one-shot
latch with createHostConnectRefetchGate, which fires on each transition
INTO 'connected' — one refetch per reconnect, no polling timer — mirroring
useWorktreeResync on the host screen. fetchHomeHostWorktreeInfo moves out
of app/index.tsx so its rejection path is covered by tests.

* fix(mobile): bound "Last known" counts and survive a path cutover

Review found two ways the home host card's stale-count fix misbehaves.

1. A migrateTo cutover (relay->direct probe, forced replacement) rejects
   in-flight requests with LogicalClientCutoverError and republishes
   'connected' from 'connected', so the connect gate never re-arms and the
   card latched on "Last known: ..." with nothing left to clear it.
   worktree.ps now re-issues on the authenticated replacement, bounded,
   like runtime-capability-probe and worktree-create-retry already do.

2. "Last known: N worktrees" had no age bound. The home snapshot is
   persisted, so a cold start whose first worktree.ps failed rendered
   counts proven days ago exactly like counts proven seconds ago - the case
   STA-3123 deliberately rendered as "Worktree list unavailable". Counts now
   carry countsProvenAt and expire out of the "last known" wording after
   10 minutes; counts persisted by an older build count as expired.

Also, per review: the card derives its own worktree line from
HostWorktreeInfo, so a caller can no longer re-gate the counts away (that
was the original defect), and the derivation is covered by a render test -
mobile/vitest.config.ts never collected *.test.tsx, so component tests
were silently dead. Home stats are keyed by host and summed instead of
letting whichever desktop replied last overwrite the shared header row,
which the per-reconnect refetch made churn on flaky links.

* fix(mobile): age bounds liveness, not the counts; scope the header total to paired hosts

Round-2 review follow-up.

Age bound was anchored on proof time inside the failure branch only, so a
session connected past the window that then hit one failed refresh rendered
the pre-fix "Worktree list unavailable" — the exact case this PR exists for —
while identically aged counts still rendered unlabeled as live whenever the
refresh was merely pending. Age now decides live vs "Last known" and the
failure branch keeps whatever the host last proved; "Worktree list unavailable"
is reserved for a catalog that never loaded.

Header stats summed every entry ever cached, so removing a desktop left its
lifetime numbers in the total for the rest of the session. totalHomeStats now
sums the hosts still paired, which also covers removal from the host screen.

wireHostSubscriptions is the effect body moved verbatim out of useEffect;
react-doctor's effect-needs-cleanup false-positives on `subscribe` inside one
and the changed-code gate has no working suppression path (an inline directive
reads as unused to the plugin-less scan).

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-08-03 23:26:02 -07:00
e59a319ffe fix(sidebar): keep each project's entry-point workspace visible under "Hide sleeping" (#12257)
"Hide sleeping" swept each project's main workspace out of the sidebar as soon as
it had no live PTY, browser tab or agent — even with "Hide default branch" off.
For a project whose only row is that workspace (a folder workspace, a fresh
clone, a detached-HEAD main), the entire project vanished with no in-place way
back.

Adds a shared `isSleepingSweepExemptWorkspace` predicate keyed on
`isMainWorktree` rather than the branch name, so folder workspaces (no branch),
detached-HEAD mains, and SSH rows whose head/branch are blanked while a provider
is disconnected all stay put. Wired into `computeVisibleWorktreeIds` (sidebar,
Cmd+1-9, workspace board), the jump palette's duplicate inline pass, and mobile's
`filterWorktrees`.

Ships default-on with an escape hatch: a persisted
`alwaysShowDefaultBranchWorkspace` setting surfaced as "Except default branch"
under "Hide sleeping". Explicit "Hide default branch" still wins, since it
filters before the sleeping sweep.

Mobile reads the setting but never writes it back, so a desktop opt-out can't be
clobbered by a filter tap before the ui.get roundtrip lands.

Combines the two PRs open against #8873. #8966's exempt set is a strict subset of
this one, so its production diff was subsumed rather than ported; its jump-palette
render harness and e2e spec were carried over, and are the only such coverage here.

Fixes #8873
Closes #8966

Co-authored-by: Rod Boev <rod.boev@gmail.com>
Co-authored-by: Orca <help@stably.ai>
2026-08-03 22:55:17 -07:00
Brennan Benson 3d68212f7b fix(mobile): surface worktree catalog failures instead of silent 0 worktrees (STA-3123) (#12235)
* fix(mobile): surface worktree catalog failures instead of showing 0 worktrees (STA-3123)

A connected host whose worktree.ps request fails now shows an explicit
catalog-failure state (with the RPC error code) on the host page, and
'Worktree list unavailable' on the home host card, instead of silently
rendering as a healthy host with zero workspaces.

* fix(mobile): mark cached worktree catalogs unavailable
2026-08-03 00:48:30 -07:00
Brennan Benson 169ec8f08d fix(mobile): refresh folder workspace catalog (#11767) 2026-08-01 01:42:27 -07:00
NeilandBrennan Benson 6e7ceafd07 perf(mobile): avoid unchanged worktree catalog payloads (#11735)
* perf(mobile): avoid unchanged worktree catalog payloads

* fix(mobile): isolate catalog snapshots by limit

* review: reassert host truth on unchanged polls; content-address snapshots

Client — the `changed` gate meant an unchanged poll skipped setWorktrees /
setLastKnownWorktrees / setCachedWorktrees, so optimistic local edits
(togglePin, handleDeleteWorktree's failure re-add) and the #8498 cache guard
were no longer repaired while the host catalog was stable. The gate bought
nothing: setCachedWorktrees is an in-memory Map write and areWorktreeListsEqual
already ran every poll, so the steady state still short-circuits on array
identity. All wire savings are unaffected. admit() now just returns the
confirmed rows and HostScreen applies them exactly as it did pre-PR.

Also on the client:
- a stale response from a superseded client/host no longer clears the token the
  current client/host just established
- discriminate on `worktrees` rather than on `'unchanged' in response`, so a
  future catalog field named `unchanged` can't reclassify a full response
- useRef over useMemo for the snapshot client; React may discard memoized values
- hoist WORKTREE_PS_FULL_LIMIT so the truncates-at-200 rationale travels with it

Host — replace the per-limit snapshot cache with a content-addressed id (ETag
semantics). Ownership lives in the id, so concurrent clients, differing limits,
and runtime restarts are correct by construction; this drops the LRU, the
eviction policy, the per-runtime WeakMap, and the retention of up to 8 full
catalogs. The remaining cache is a pure memo: because ids derive from content,
dropping or thrashing it costs CPU and nothing else. Keeping the memo also
keeps the measured steady-state cost — hashing every poll instead measured
2.24ms vs 0.75ms for the compare on a 310KB catalog.

Verified: mobile 2784 passed / 3 skipped, src/main/runtime/rpc 1064 passed,
node + mobile typechecks, oxlint, oxfmt, max-lines ratchet.

* fix(runtime): isolate catalog snapshot memo

---------

Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
2026-07-31 23:12:13 -07:00
NeilandOrca aab112933e Revert "fix(memory): bound OOM-prone accumulators (#10179)" (#10255)
Co-authored-by: Orca <help@stably.ai>
2026-07-23 18:35:31 -07:00
Neil 8f40ddf328 fix(memory): bound OOM-prone accumulators (#10179) 2026-07-23 06:22:56 -07:00
NeilandOrca 4468d54f3c perf(mobile): gate host polling on foreground/background (#9857)
* perf(mobile): gate host polling on foreground

The mobile host screen ran two 3s polls (routed + embedded), each firing worktree.ps
AND repo.list, with no foreground/background gate — so a connected phone kept pinging
every 3s (worktree.ps is a full multi-repo process scan) plus a radio wakeup, including
brief background windows while the socket stays parked.

Consolidate both into one startHostWorktreeRefresh lifecycle and AppState-gate the
interval so BOTH polls stop while backgrounded and refresh immediately on foreground
return. worktree.ps keeps its 3s cadence while foregrounded (it carries live agent
status/preview/unread that no push event replaces). repo.list stays on the interval as
an AppState-gated, self-throttling (REPO_METADATA_REFRESH_MS=60s) convergence safety-net
— desktop Settings repo edits notify only the renderer, not the runtime clientEvents
stream, so it can't be made purely event-driven without going stale — and additionally
gets a reposChanged/worktreesChanged fast-path and reconnect-replay refetch.

Verified in a deps-installed mobile checkout: full mobile suite 2232 pass, typecheck,
oxlint (within the frozen max-lines budget), and oxfmt --check all clean.

Co-authored-by: Orca <help@stably.ai>

* chore(mobile): drop stale fetchRepoMetadata dep from the reconnect effect

Address CodeRabbit nitpick: the reconnect effect no longer calls fetchRepoMetadata
(that refetch moved into startHostWorktreeRefresh), so it shouldn't remain in the
effect's dependency array.

Co-authored-by: Orca <help@stably.ai>

---------

Co-authored-by: Orca <help@stably.ai>
2026-07-22 14:12:55 -07:00
OrcaWin 05c32c4757 fix(runtime): isolate navigation across paired clients (#9664) 2026-07-20 21:36:15 -07:00
Brennan Benson e3c8d96638 Access the Floating Workspace from mobile (#8405) (#9523)
* Access the Floating Workspace from mobile (#8405)

Surface the desktop Floating Workspace (the global, repo-less scratchpad
of terminal tabs under the synthetic `global-floating-terminal` id) on the
mobile app so a Claude session left running there is reachable from a phone.

Adds a terminal-icon button to the mobile host header (phone + tablet
sidebar) that opens the existing Session screen for the floating id. The
sentinel already had host-side RPC support (#5946: local runtime, homedir
cwd, explicit-id fast paths in session.tabs.*); this wires up the mobile
surface and gates it on a new `floatingWorkspaceEnabled` status flag so the
entry hides on hosts that predate it or where the feature is disabled.

The Session screen learns an `isFloatingWorkspaceRoute` flag (mirroring the
existing `folder:` route pattern) that hides repo-backed surfaces — Files,
Source Control, PR/checks, agent history — skips the diff-comment and GitHub
probes, routes terminal URL taps to the phone browser, and limits the New
Tab drawer to terminals + agents (browser/markdown creation resolves a real
worktree host-side and stays desktop-only). useLiveWorktreeName
short-circuits for the sentinel so it no longer polls worktree.show forever.

Extracted the host status.get gating into a useHostStatusGates hook to keep
the host screen under the max-lines ratchet.

* Harden mobile Floating Workspace routing

* Fix mobile host gate reuse race

* Harden floating mobile session polling

* fix(mobile): harden floating workspace route reuse

* fix(mobile): skip floating workspace repo lookup

* fix(mobile): clarify floating workspace header action
2026-07-20 21:05:40 -07:00
8f6e44ed53 Show agent session history on mobile (#6786)
* Show agent session history on mobile

Bring the desktop "Agent Session History" panel to Orca Mobile as a
per-worktree screen: browse past agent transcript sessions across the
host with scope tabs (Workspace/Project/All), search, grouping, session
cards, and tap-to-read message previews.

The transcript scan previously ran only over Electron IPC, so mobile
could not reach it. Expose it over the runtime RPC protocol mobile
already speaks (aiVault.listSessions) so the scan runs on whichever host
owns the transcripts — correct for local and SSH/remote hosts. Both the
desktop IPC handler and the new RPC method share one cache, so opening
the desktop panel and the mobile screen never double-scan.

The pure filter/group/display logic is lifted into /shared (the renderer
re-exports it) so the standalone mobile package can reuse it. Mobile
narrows scoped tabs client-side by cwd path-prefix because the host scan
treats scope paths as a widening union.

Resume-from-mobile is intentionally a follow-up.

* Fix mobile agent history list rendering and RPC authorization

- Authorize aiVault.listSessions in the mobile RPC allowlist so the
  mobile client's call is not rejected before dispatch (without this the
  screen could never load sessions at runtime).
- Name each SectionList section's rows `data` (the field React Native
  reads) instead of `cards`, fixing a type error and silent empty-section
  rendering.

* Address review feedback on agent session history

- Match quoted repo:/path: search operator values so labels and paths
  with spaces match (e.g. path:"/Users/ada/My Project").
- Hold a scoped tab in loading until the worktree list resolves instead
  of firing an unscoped fetch that briefly shows unrelated host history;
  proceed once loaded even if the worktree is absent (no stuck spinner).
- Clear cached host capabilities on disconnect/host-switch and failed
  status.get so a capability-gated action can't linger for a host that
  doesn't support it.
- Cover the real OrcaRuntimeService codex-home forwarding path and the
  quoted-operator parser with tests.

* Hide redundant mobile current worktree badges

Co-authored-by: Orca <help@stably.ai>

* Resume agent sessions from mobile history (#6969)

Co-authored-by: Orca <help@stably.ai>

* Adapt merged seams to main's lint and reply-sender hardening

Co-authored-by: Orca <help@stably.ai>

* Cap mobile project-scope paths to the aiVault RPC bound

Co-authored-by: Orca <help@stably.ai>

* Share the aiVault scopePaths bound between the RPC schema and mobile

Co-authored-by: Orca <help@stably.ai>

* Guard shared AI Vault inflight cleanup against concurrent key replacement

The extracted cache module's .finally() cleared inflight tracking
unconditionally, dropping the if (inflightKey === key) guard its sibling
outer cache kept: an older scan resolving after a different-key scan
replaced the tracking would null the newer scan's dedup slot, so a
re-request started a duplicate transcript rescan. Mirrors the sibling
guard; the regression test flushes a macrotask so a reverted guard fails
fast on the call count instead of hanging.

Co-authored-by: Orca <help@stably.ai>

* Harden aiVault.listSessions contract and gate mobile header entry on capability

- Clamp scopePaths (64) instead of rejecting, cap limit at 2000, and make
  executionHostId optional so mobile can omit it; restamp per caller.
- Retain successful mobile terminal-create mutation ids for 60s so resume
  retries dedupe after transient socket drops.
- Gate the session-header Agent History action on the aiVault.v1 capability
  (mirrors the host-list action) so old hosts never show a dead-end entry.
- Fix stale contract comments (scopePaths clamp semantics; filters move
  includes quoted repo:/path: operator parsing).

* Add subagent field to session test fixtures after #7423 merge

AiVaultSession.subagent became required on main; the five fixtures added on
this branch predate it. Top-level scanned sessions carry null.

---------

Co-authored-by: Orca <help@stably.ai>
Co-authored-by: Brennan Benson <brennanbenson@Brennans-MacBook-Pro.local>
2026-07-10 13:48:10 -07:00
Jinjing 44d5ed0439 1.4.131 rc2 release prep (#8020)
* Support WSL Codex settings promotion and harden config write-back

- Enable settings promotion for WSL runtimes using per-distro baselines.
- Create parent directories if missing to prevent promotion ENOENTs.
- Keep restrictive permissions (0600) and follow symlinks on promote.
- Respect CRLF line endings when inserting keys into CRLF config files.
- Skip redundant baseline file writes when settings are unchanged.
- Include the release scan report for the 1.4.131-rc2 prep.

* Refactor sleeping agent wake flow and fetch rate limits via backend

- Background-mount only targeted terminal tabs during passive wake to
  prevent spawning unnecessary PTYs for unvisited tabs.
- Latch edge-triggered wake requests that arrive mid-hibernation and
  track active claims to prevent double-resuming a provider session.
- Query the ChatGPT wham usage backend API directly with fetch for
  rate limits, avoiding launching Codex or WSL login shells.
- Asynchronously probe and serialize WSL auth files with timeouts to
  prevent synchronous I/O from stalling Electron's main process.
- Fix config promotion edge cases such as missing parent directories,
  dangling symlinks, and atomic write permission widening.

* Support WSL dotfile-symlink write-back and lengthen redeem timeout

- Preserve symlinked Codex config on WSL by writing through the
  existing file instead of atomic-rename, since \\wsl$ symlink
  metadata isn't reliably detected and rename would clobber the link.
- Tighten new ~/.codex directory creation to 0700 (holds auth.json).
- Give explicit reset-credit redemption a 30s backend timeout instead
  of the 10s background-poll default, since it's user-triggered.
- Read sleeping-agent session state from the worktree's actual
  execution-host partition instead of always the local one, so the
  headless-wake check works correctly for SSH-hosted worktrees.
- Isolate serve-sim watcher tests from the real $TMPDIR/serve-sim
  state file to avoid leaking unrelated events.
2026-07-09 21:54:22 -07:00
JinjingandOrca 430d4b9482 Show all worktrees across all hosts on mobile (#7500)
* Show all worktrees across all hosts on mobile

Avoid honoring desktop's host-filtering settings since mobile lacks the
UI to manage or unhide them. This prevents worktrees from being silently
hidden under certain host scopes.

Additionally, this removes worktree filtering based on repo metadata, which
previously caused worktrees to vanish when same-named repos on different
hosts collapsed to a single ID.

* fix(daemon): preserve promisify.custom type through wrapChildProcessApi

The windows-hidden-console-children test (from #7499, admin-merged with a
failing verify) failed tsgo: promisify(wrapped) resolved to its zero-arg
overload because the wrapper erased its argument to a bare variadic function
and the fake never statically carried promisify.custom. Preserve the wrapped
type via a generic overload (accurate: the wrapper copies the call signature
and symbols verbatim) and build the fake as a real CustomPromisify, so
promisify routes through the custom overload as it does in production.

Co-authored-by: Orca <help@stably.ai>

---------

Co-authored-by: Orca <help@stably.ai>
2026-07-05 23:10:48 -07:00
Brennan BensonandOrca 5fd9f0c62c Prevent mobile worktree selection from focusing desktop (#6461)
Co-authored-by: Orca <help@stably.ai>
2026-06-26 19:34:33 -07:00
Brennan BensonandOrca c0fd54c341 Show Agent activity for mobile smart sort (#6347)
Co-authored-by: Orca <help@stably.ai>
2026-06-25 19:00:35 -07:00
0c0367ea88 feat(agents): native Xiaomi MiMo Code support (#6239)
* feat(agents): native Xiaomi MiMo Code support

Add mimo-code TUI agent (detect mimo, --prompt, --session resume).
Inject MIMOCODE_HOME overlay and /hook/mimo-code status plugin on mimo
launch when agent status hooks are enabled; restore via shell-ready
wrappers. Reuse OpenCode-family hook normalization in the listener.

SSH remote MiMo hook overlays are not included (local/daemon first).

Closes #6220

* fix(mimo): remirror overlay config idempotently

rmSync overlay config before mirror so a second mimo launch does not
hit EEXIST in mirrorEntry and fall back to the user MIMOCODE_HOME.

* review: harden mimo code support

Co-authored-by: Orca <help@stably.ai>

---------

Co-authored-by: Doan Bac Tam <24356000+doanbactam@users.noreply.github.com>
Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com>
Co-authored-by: Orca <help@stably.ai>
2026-06-24 00:17:49 -07:00
Jinwoo HongandOrca 5d8617bc71 Fix mobile workspace parity (#6207)
Co-authored-by: Orca <help@stably.ai>
2026-06-23 17:07:28 -07:00
Jinwoo HongandOrca 32e6ab7655 Fix mobile workspace list parity (#6001)
Co-authored-by: Orca <help@stably.ai>
2026-06-21 15:11:55 -07:00
Jinjing 27bf41793c Select last visited worktree repo by default on mobile (#5912)
- Resolve the initial repository in the mobile NewWorktreeModal using
  the last-visited worktree's repo ID loaded from AsyncStorage.
- Extract and share new-workspace dialog repository selection and
  resolution logic to src/shared, mirroring it on mobile to bypass
  Metro bundling limits.
- Add unit tests for last-visited repo reading and selection resolution.
2026-06-20 15:12:20 -07:00
Jinwoo HongandOrca 06d674b35d Fix mobile folder workspace visibility (#5679)
Co-authored-by: Orca <help@stably.ai>
2026-06-17 23:18:34 -07:00
c9bd61376f feat(mobile): combine PR sidebar and checks parity (#5641)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Orca <help@stably.ai>
Co-authored-by: gsxdsm <gsxdsm@users.noreply.github.com>
2026-06-17 22:52:12 -07:00
Jinwoo HongandOrca e3bf7d8614 feat(mobile): pickers, workspace parity, active-workspace focus, tap-to-open, source-control parity, artifact viewing (#5330)
Co-authored-by: Orca <help@stably.ai>
2026-06-15 22:02:51 -07:00