Commit Graph
966 Commits
Author SHA1 Message Date
Neil aa07d5891a fix(mobile): break the transport and terminal-document import cycles, and gate cycles after mobile's install (#30290)
* fix(mobile): break the transport and terminal-document import cycles and gate them after mobile's install

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

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

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

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

Specs reach mobile/src/transport, whose expo-crypto and async-storage imports resolve only after mobile's install and then load React Native's globals (numeric timers, a required NODE_ENV), adding errors in unrelated specs. An empty stub pins both, so the static job reads 232 either way.
2026-10-18 04:00:24 -07:00
Neil c82cf2b35d fix(mobile): a session resumed from the phone's history shows its conversation before the first new turn (#10923) (#30283) 2026-10-18 04:00:18 -07:00
Neil 2c12c1edf1 refactor(runtime): move runtime/relay to runtime/mobile-cloud-relay (#30274)
The folder is the cloud relay that reaches phones; under its old name it read as the legacy SSH
relay. git mv plus import and path-literal rewrites only.
2026-10-18 01:35:40 -07:00
Neil ed3ee7e08a refactor: name the vm transcript and mobile pending-write caps for what they count (#30251)
Both cap UTF-16 string length for local display and memory, never wire bytes, so _BYTES misled readers into treating them as consumer byte budgets.
2026-10-18 00:35:30 -07:00
Neilandzhangxaochen 720f45c8f7 Fix mobile Source Control rendering for large branches
Render committed branch changes as virtualized SectionList rows for clean and dirty working trees, keeping all files accessible without delaying Back. Share the existing sleek scrollbar rules with mobile web pages and retain explicit hidden-indicator settings.

Fixes #30170. Review reference: #30171; thanks to @zhangxaochen for the diagnosis and initial proposed fix.

Co-authored-by: zhangxaochen <zhangxaochen@163.com>
2026-10-18 00:20:23 -07:00
Kelvin Amoaba 12f3089b9c feat(mobile): draw chat tool runs the way desktop does (#30135) 2026-10-18 01:48:36 +00:00
Brennan Benson 285f95c045 fix(mobile): a compact phone queue: shorter header, a capped scrolling list, and an editor that fits the card (#30016)
* fix(mobile): a compact queue: shorter header, a capped scrolling list, and an editor that fits the card

* fix(mobile): keep queue actions visible and reveal new cards

* test(mobile): allow shared SVG host fixture

* fix(mobile): queue header and editor controls grow with large system text instead of clipping

* fix(mobile): at large text sizes the queue header's buttons wrap below its label instead of squeezing it out

* fix(mobile): at large text sizes the queued editor's Cancel and Save wrap below its heading instead of truncating it

* fix(mobile): at large text sizes the queue header's and editor's buttons take a line of their own

* fix(mobile): the large-text button lines wrap when one button is all a small phone's line holds

* test(mobile): the composer tray test's react-native stub reports the text scale

* fix(mobile): while a card is edited at large text, the paused header's button line gives its height to the editor
2026-10-17 13:47:48 -07:00
Jinwoo Hong cf6a5446a6 Show which desktop the mobile Tasks screen is for (#30105)
* feat(mobile): name the desktop under the Tasks title

Tasks can open on any connected desktop, so the header now shows which
one, in the same subtitle line the host screen uses under its title.

* fix(mobile): reserve the Tasks desktop line so the title does not jump

The subtitle line now holds its height before the host profile is read,
a failed read leaves it blank, and the tests cover a desktop switch
while a read is in flight.

* test(mobile): re-measure the page script sweep after the Tasks subtitle

The Tasks route now shares the host screen's styles, so it emits one
script fewer (77 to 76 at its prefix, 80 to 79 for the tree). Re-swept
every route prefix; the worst route step is now 11.
2026-10-17 13:20:33 -07:00
Jinwoo HongandClaude 46984db859 fix(mobile): ask which desktop to open Tasks on when several are connected (#30048)
* fix(mobile): ask which desktop to open Tasks on when several are connected

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

* fix(mobile): offer only desktops that have the tapped task provider

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-17 13:08:59 -07:00
Jinwoo Hong 90c1c0ac50 feat(mobile): draw the host Accounts screen from the desktop's page (#30020)
* fix(mobile): wall hosts that cannot serve the page instead of opening native screens

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-10-17 03:11:24 -07:00
Brennan Bensonandm4air 3d2e0bd26d fix(native-chat): a running Claude chat keeps its model when two Claude Code versions list differently (#30022)
* fix(native-chat): a running Claude chat keeps its model when two Claude Code versions list differently

A send's stand-in for a model the list lacks took its rows and default from the
chat's own answer but kept only rows the account's saved list names, and every
running chat rewrote that list whatever CLI version it ran. After Claude Code
2.1.280 -> 2.1.302 the filter dropped the default row and sends moved to Sonnet.

- The options answer names the ids its agent listed (listedModelIds); a live
  catalog is verified only by its own listing. An older host's answer may move
  only to the chat's own default row, and only where the host's list names it.
- Only a session's start (and the probe) writes the account's list; later reads
  no longer do, so an older running CLI can't replace a newer start's list.

Tracked in STA-10947.

* fix: keep running chat model selection within its own listing

Preserve learned model menus on later reads and check launch-added model membership before saving.

Tracked in STA-10947.

* test: validate ACP menu regression fixtures

Use the protocol schema for config options and supply the catalog row default flag.

Tracked in STA-10947.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-17 02:38:19 -07:00
Jinwoo Hong 86f6258d54 refactor(mobile): delete native screens the OTA shell no longer reaches (#29979)
* fix(mobile): wall hosts that cannot serve the page instead of opening native screens

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* test(mobile): read the nullable page route refusal in its policy test
2026-10-17 02:01:20 -07:00
zhangxaochenandNeil 92999b0e5f fix(mobile): prevent markdown and file tab reload and screen flicker on retap and revisit (#30033)
* fix(mobile): prevent markdown and file tab reload and screen flicker on retap and revisit

* fix(mobile): preserve newer saves and refresh document metadata

* test(mobile): supply current document state in recorded reads

---------

Co-authored-by: Neil <neil@stably.ai>
2026-10-17 01:35:34 -07:00
Brennan Benson ee4fd06593 fix(mobile): the chat's Working line uses desktop's spinner (#30008)
* fix(mobile): the chat's Working line uses desktop's spinner (turning circle, hourglass under reduced motion)

* fix(mobile): keep working animations off interaction queue
2026-10-17 01:17:28 -07:00
Brennan Benson f4aa82874b fix(mobile): show the same Working line in terminal-backed chats and drop the floating 'Agent is working' pill (#29972) 2026-10-17 00:09:10 -07:00
Brennan Benson 58f1f7de1e fix(mobile): edit queued messages in place (#28646)
* fix(mobile): edit queued messages in place

* fix(mobile): use the platform input font size for queued edits

* fix(mobile): handle missing host support while probing queued edits

* Keep phone queued edits owned through save settlement

* Keep phone queued edits local to mounted chat

* fix(mobile): retire queued editor when its card disappears

* fix(mobile): retain queued edits while reconnecting

* test(mobile): await reconnect timer inside act callback

* fix(mobile): publish queued edit transport after commit

* test(mobile): await queued Edit inside act callbacks

* fix(mobile): hand a stale queued Save to the composer and reopen on the newer text

* fix(mobile): close an unchanged queued Save locally and keep stale typing in its own chat

* fix(mobile): explain disconnected queued edit refusal

* fix(mobile): combine queued session contract imports
2026-10-17 00:06:07 -07:00
Brennan Benson 5e8d40d61b fix(mobile): remove the dead space around the chat composer (#29224)
* fix(mobile): remove the dead space around the chat composer

The Tools toggle floats over the transcript and shows only when the chat
has tool calls; the composer is one row (attach, message, mic, send) until
focused or drafted; Stop takes Send's slot while there is nothing to send;
the bridge lane's working dots move to the transcript's tail.

* fix(mobile): name the composer's content facts for their role

* fix(mobile): preserve composer controls and picker focus

* fix(mobile): typecheck working indicator test lookup

* test(mobile): find the floating Tools pill when checking a chat switch keeps it expanded
2026-10-16 21:27:06 -07:00
Jinwoo HongandClaude 550cc5cdcc fix(sidebar): order sidebar names the same on desktop, phone and server (#29946)
* fix(sidebar): order sidebar text the same on every device

Sidebar names, project labels, project-group names and folder workspace
names were ordered with localeCompare, whose result depends on the ICU data
bundled with Electron, the phone WebView or Node. Order them with one
comparator in src/shared/sidebar-list/text-order.ts instead: case-folded and
NFC-normalized, digit runs by value, the rest by code point, and equal text
broken by host-qualified id.

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

* perf(sidebar): fold each sort label once per sort; reuse the phone row identity helper

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

* fix(sidebar): ignore accents when ordering sidebar names

Accents and case now only break exact ties, so "Éclair" sorts beside "eclair".

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-16 21:07:05 -07:00
Kelvin Amoaba 432c6d0735 fix(mobile): show message times, keep the long-press sheet open, and say when a conversation isn't found (#29105) 2026-10-17 03:37:12 +00:00
NeilandClaude ad6b318620 fix(mobile): re-read a server's capabilities when the server reconnects, not only the desktop (#29913)
useWorkspaceServer keyed the server's status read on the desktop's connection state, so a server restart or update under a connected desktop never re-read what it supports. It now passes the server's computed state.

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-16 20:07:50 -07:00
Brennan Benson b61b9da778 fix(mobile): a streaming reply reveals word by word, like the desktop (#29148)
* fix(mobile): a streaming reply reveals word by word, like the desktop

The phone drew each streamed chunk as it landed, so a reply arrived in a few
jumps. Move the desktop's paced reveal into src/shared and use it on both
surfaces: the phone paces the growing block of the newest reply, remembers
progress per chat list so a recycled row resumes, and draws everything at once
with reduced motion. The terminal-backed chat's live row now counts as growing.

* fix(mobile): hold the reply reveals in the chat overlay

The list's turn resolver must not scan terminal-backed transcripts, and the
overlay outlives the list across chat and terminal toggles, so it owns the
reveal memory and the reduced-motion read. Also fix the moved hook's import
in the onboarding route, and mock the OS read where tests render the overlay.

* test(mobile): mock the message row's other children in the reveal test

Their real modules reach React Native's Flow sources, which Bun cannot load;
the other message row tests mock them the same way.

* style: format the desktop paced-text hook

* Keep phone reply reveal progress owned by its message

* Add braces around phone reveal test assertion

* Start phone prose streaming only at a text block

* Find the mocked phone tool row with a node predicate

* refactor(mobile): move chat prose rendering into its own file

Keeps MobileNativeChatMessage.tsx under the 400-line limit after the main merge.
2026-10-16 18:51:18 -07:00
Brennan Benson 07558ba8ce fix(native-chat): a dot between the queue header's label and its count (#29268)
* fix(native-chat): a dot between the queue header's label and its count

* style(mobile): wrap the queue header dot's props

* refactor(mobile): move the queued card's text into its own file

The queue file sat at the 400-line cap, so the header dot had no room.
2026-10-16 18:39:08 -07:00
Brennan Benson 64e544a442 feat(mobile): Resume in New CLI from a phone chat tab (#29783)
* feat(mobile): Resume in New CLI from a phone chat tab

A chat tab on the phone had only Close in its long-press menu, so a chat
that would not load left no way to reach its conversation. The phone now
offers Resume in New CLI, like the desktop tab menu: the chat's host
prepares the conversation and the phone opens a copy in a new terminal tab.

The resume and fork startup builders move to src/shared so the desktop and
the phone build the same command.

* test(mobile): type the fake terminal create instead of casting its calls

* fix(mobile): pass the host's fork source as is; match the quoted args in the test

* test(mobile): give the fake host client the untyped mock the other client fakes use

* style: format the shared resume builder and phone fork files

* Fix mobile chat CLI fork creation on owning host

* Fix mobile chat fork lint and split create owner helpers

* Keep chat fork beside phone anchor across tab groups

* Combine mobile terminal runtime type imports

* Repair mobile create CI contracts after chat fork extraction

* Classify chat fork preflight refusals and preserve mobile create recordings

* Keep route-owned create cleanup quiet after unmount

* Preserve typed runtime repair refusal in chat fork preflight

* Preserve checked store through mobile chat fork preflight

* Register mobile chat fork runtime fixture before test setup

* Remove unused mobile create recording value

* style(mobile): format the chat fork create files; move fork reply messages into the fork feedback module

Formatting pushed the create hook past its line limit, so the fork-only reply message, partial-success warning and host capability check now live beside the other fork messages. Same order and copy as before.

* Keep quick command mutation aligned with formatted request

* Keep mobile chat CLI forks in terminal view

* Pass mobile chat resolution its existing controller arguments

* Match the mobile create fixture mock layout

* Keep mobile chat CLI forks open when placement is unavailable

* Brace placement contract fixture checks

* test(mobile): keep the prompt controller mock under its line limit after the main merge

* Expose pane admission in the runtime test fixture type

* Complete the chat fork contract controller fixture
2026-10-16 18:10:24 -07:00
Jinwoo Hong 2ff42fd37c fix(mobile): stop re-downloading a current OTA bundle on entry (#29860)
* fix(mobile): stop rewriting the cached manifest on a same-build hit

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

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

Opens a same-build hit straight from session.cached, folds the manifest
file name and parser back into the generation store, removes the unused
moveFile adapter and fake hooks, and trims tests that passed on main.
2026-10-16 16:55:28 -07:00
Kelvin Amoaba d963f2a23a fix(mobile): draw your own messages as markdown, fold long ones and open attached pictures full size (#29075) 2026-10-16 23:27:45 +00:00
Kelvin Amoaba 17a2bced11 fix(mobile): stop phone reconnects from undoing a desktop take-back (#29465) 2026-10-16 23:04:48 +00:00
Brennan Benson 8c1ade9e44 Keep pending chat decisions available outside loaded history (#29121)
* fix(native-chat): retire acknowledged send echoes after catch-up

* test: follow phone lint rules in reconnect regression

* test: format reconnect message expectation

* Keep current chat requests independent of loaded history

* Read current chat requests in bounded cursor pieces

* Fix current request subscription static checks

* Match generated request schema import ordering

* fix(native-chat): correct current request type boundaries

* Exercise current request reads in wire compatibility inventory

* Align current request fixtures with provider commit and RPC surface

* test(native-chat): include current request browser capability

* Keep the mobile chat session hook within its line budget

* Format mobile current request readers

* fix(native-chat): reopen once for a current-request version whose read keeps failing

A deterministic read failure reopened the subscription, whose first frame re-read the same version and reset the backoff, so the pane resubscribed every 750 ms forever.

* test(native-chat): type the current-request snapshot fixture

* test(native-chat): wait for the hydrated open before driving the subscription
2026-10-16 16:01:58 -07:00
Kelvin Amoaba 4446d4f628 fix(native-chat): stop following the end when the reader scrolls over a visual (#29116) 2026-10-16 18:11:33 +00:00
Neil 44167e816f test(mobile): model the slow link per window so the slow-link test does no CPU-bound work (#29773)
CI's Mobile verify timed the 'finishes' case out at 5s: it hashed and base64-encoded
twelve real 384 KiB windows under fake timers. The link now costs a fixed 17s of
fake time per window over a 1 KiB grid, so the test runs in ~20ms and still fails
on the per-read timeout without the #29751 fix.
2026-10-16 10:19:58 -07:00
Neil 43c066edff fix(mobile): keep the app download going over a slow link instead of timing out each queued read (#29751)
* fix(mobile): keep the app download going over a slow link instead of timing out each queued read

Each bundle window read had its own 30s request timeout, but its reply queues
behind the other three reads in flight. On a slow link (Tailscale through a
relay) the fourth reply could not land within 30s, so the download showed
progress and then failed with 'Request timed out: mobileWeb.bundle.range'.

The fetch now fails only when no window reply lands for 30s, and each read's
own deadline is a long backstop.

Fixes #29697

* fix(mobile): bound a stalled bundle fetch's pending reads to the in-flight budget

* test(mobile): re-record bundle-fetch goldens for the per-read backstop option
2026-10-16 10:13:14 -07:00
Neil 98064b2608 fix(ssh): a managed server restart no longer marks still-running agents done (#29604)
* fix(runtime): keep a restored agent turn unverifiable instead of done after a server restart

* fix(runtime): retire restored turns by the synced leaf's title clock; no Stop for unverifiable mobile turns

* fix(runtime): capability-gate restored agent rows so older clients keep the legacy done projection

* fix(runtime): retire restored turns only on shell titles; restored rows never pierce live client status

* test(web): the browser client advertises the restored agent status capability
2026-10-16 04:00:24 -07:00
Neil 6b5cd3246a fix(mobile): unbreak mobile typecheck after shared socket-liveness change (#29496) 2026-10-15 23:40:11 -07:00
Neil 0c8303c210 fix(mobile): Agent Session History resumes sessions in SSH workspaces that can read them (#29461)
* fix(mobile): resume Agent Session History rows in an SSH workspace that can read them

* fix(mobile): keep transcript-referencing resumes on the transcript's own host
2026-10-15 23:26:03 -07:00
Neil 6ae1386f98 fix(native-chat): never run a chat on an older host with more access than Ask for approval allows (#29442)
* fix(native-chat): refuse to start or send on a host that ignores Ask for approval

* fix(native-chat): hide Continue on a host held for its permission mode

* fix(native-chat): hold queued cards and goal turns on a held host; re-check on reconnect

* fix(native-chat): hold goal objectives and start retries on a held host; re-ask while held

* test(native-chat): give the remote Continue test a host that applies permission modes
2026-10-15 23:23:38 -07:00
Jinwoo Hong 13084ba347 feat(mobile): long-press a workspace to see its linked PR, issues and notes (#29225)
* fix(mobile): phone server rows follow the desktop's other-client and sleeping filters

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* wip: workspace view descriptors

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

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

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

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

* wip: desktop option items from shared choices

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

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

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

No visible change.

* wip: row facts

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

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

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

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

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

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

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

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

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

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

* wip: phone uses the shared evaluator

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

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

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

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

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

* wip: phone view state carries project order and card props

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

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

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

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

* feat(mobile): the phone's Workspace options sheet, rendered from the desktop's shared option list

The Filter drawer becomes the desktop's Workspace options: hosts, projects with search, group,
sort, project order, card layout and properties, card display, and every filter. Each option
renders through a table keyed on the shared option keys, so a new desktop option fails to compile
until the phone has a control. Sort and group pickers use the desktop's words; Repo reads Project.

* fix(mobile): Workspace options sheet review fixes

Hosts read as on the desktop (none checked while all show; the last shown host is disabled).
Project order falls back to the served value until the shared one is read. Card layout and card
style changes now push to an open phone. Pick-one rows read as radio buttons. Simpler wiring:
flat card state, the preset flag rides the patch, shared choices used directly.

* fix(mobile): reset project order on host switch; simpler preset-flag handling

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

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

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

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

* feat(mobile): phone rows follow Card layout, Show properties, Card display and agent activity

One shared helper (getWorktreeCardDetails) decides which card details show for the chosen
properties and layout; the desktop card's flags and the phone row both read it. The phone row
hides the PR badge, note/issue/Linear glyphs, host badge and agent rows the desktop turns off,
and the Full list agent layout shows every agent row.

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

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

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

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

* fix(mobile): let a tall bottom drawer scroll on the page

The drag wrapper around the drawer's ScrollView had no flex limit, so it grew to
its content and the sheet clipped it instead of scrolling. The full Workspace
options sheet is the first drawer tall enough to hit this.

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

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

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

* test(mobile): give the project catalog fixture its project order

* feat(mobile): name the options chip Workspace options, as the desktop does

The chip keeps the active-filter count and reads to screen readers with the
desktop trigger's label. The pluralized filter count moves to shared code so
both surfaces word it the same way.

* fix(mobile): show the Workspace options count as a corner badge so the chip row fits

"Workspace options (2)" squeezed the Sort and Group chips to ellipses at phone
width. The count moves to a corner badge, as on the desktop trigger, and the
chip drops its icon, which the desktop trigger does not show either.

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

- The four kind switches come from WORKSPACE_KIND_SWITCH in workspace-kinds;
  the shared evaluator and main's phone-push check both apply it, and the filter
  type derives its switch fields from it.
- git-identity.ts folds back into git-identity-display.ts, as on main.
- SidebarFilterState moves into sidebar-filter-actions.ts; visible-worktree-kinds.ts is gone.
- buildWorkspaceRowPredicate is module-private.

* feat(mobile): Workspace options sheet in the desktop menu's shape

The sheet showed every choice of every option, so the filters sat screens down.
It now follows the desktop menu: Hosts and Projects rows with their value, Group by
as a segmented control, Sort by, Project order, Card layout and Show properties
as rows that open a page of choices, and the filters as switches. Each option's
control (an inline row, or a value plus a page) still comes from one table keyed
by the shared option list. The segmented control is the voice settings one,
extracted so both screens share it.

* fix(mobile): Workspace options review fixes

- A page whose option stops showing is forgotten, so it cannot reopen by itself.
- Only the switch takes the tap on a filter row, so the web page writes once.
- Sections split their rows in one pass; unused heading styles go.

* fix(mobile): neutral filter switches on the page

react-native-web paints an on-switch thumb teal unless given activeThumbColor,
which React Native's types omit. MobileSwitch applies the app's switch colors,
and its web sibling also sets that prop.

* refactor(mobile): one MobileSwitch file, adding the web-only thumb color on web

* fix(mobile): Show properties reads Hover under Compact, as the desktop does

The row's value now comes from getWorkspaceCardPropertiesValue in the shared
options, which the desktop trigger uses too: Hover under the compact layout,
otherwise how many properties show. Card display shows no value, as on the desktop.

* test(mobile): find the renamed Workspace options chip in the page drawer render check

* feat(mobile): drop the Sort and Group chips; the Workspace options chip covers both

The desktop has only its options button, and the sheet now holds Sort by and
Group by. The chips' pickers, their screen state and the sort label go with them.

* feat(mobile): option pages slide in from the right and keep the sheet's height

The main sheet stays laid out under an open page, so the drawer keeps its height
going in and out. The page slides in over 200 ms and back out on back (the row,
or Android's back key), instantly under reduced motion. The reduced-motion hook
moves to platform/ now that two screens use it.

* feat(mobile): Compact rows fold into one title line like the desktop compact card

The phone's Compact layout now moves repo identity into a title-row icon chip
and drops the identity meta row (repo, host, branch or folder path, child
badge), matching the desktop's compact card. The rule lives next to
getWorktreeCardDetails as getWorktreeCardLayout, and the desktop presentation
reads it too so both surfaces share one decision.

* fix(mobile): keep a remote host glyph on compact rows; pass the card layout from the list

Review follow-ups: compact rows show an icon-only host glyph for remote
projects as the desktop compact card does, the list computes the layout once
next to the card details, and a new-card-style row test.

* fix(mobile): option pages fill the sheet and open at its top

- The page layer grows to the pager's height, so no part of the main sheet shows below a short page.
- The drawer takes a scrollResetKey; the sheet passes the open page, so a page opened from a row low on a scrolled sheet starts at the top.
- The main sheet's height is measured under a page too, and the pager state lives in the sheet so it resets when the drawer closes.
- A misplaced doc comment goes back to its function.

* fix(mobile): phone rows show the Jira, automation and CLI glyphs; the options pager forgets a gone page in an effect

* fix(mobile): Jira glyph sits before Linear as on the desktop; Jira reader tests beside it

* feat(mobile): long-press a workspace to see its details

The phone's long-press sheet now lists the workspace's linked PR and
issues (tap to open), who created it, its notes and live ports (Copy),
the phone's take on the desktop sidebar card's hover card. Folder
workspaces get the sheet too, with details only.

The desktop lets phones run workspacePorts.scanHost (read only).

* fix(mobile): details read links the desktop's way and stay one-off

Links come through the shared getWorkspaceAttachments, so references the
desktop keeps apart stay apart, in the host's order with reviews first.
The branch review read no longer takes the host's active-branch slot,
each read re-runs only for its own inputs, a reopened sheet reads afresh,
and Copy's confirmation never outlives the sheet.

* fix(mobile): details clear on close without an effect; lint findings

* fix(mobile): the phone leaves Ports out of Show properties and Card display

* refactor(mobile): clearer names and tests for the phone's hidden card properties

* feat(mobile): details sheet heading carries the origin; no ports

As Jinwoo approved: the sheet heads with the workspace (primary text),
its short branch, and a quiet origin line (automation name, or Orca CLI).
The group below holds only linked reviews and tasks, then notes.
Pin uses the pin icon. Ports are left out of the phone, so the desktop
change that let phones scan them is reverted.

* test(mobile): details section render helper matches its props

* fix(mobile): details review read asks by branch; review merge by number; comments

* fix(mobile): details rows share the action rows' text column

* refactor(mobile): details sheet reads only the row

The sheet no longer fetches. The host stamps two optional row fields:
automationName beside automationCreated, and the branch PR's title, url
and checks on linkedPR (from the GitHub cache it already reads). Older
hosts omit them and the sheet shows what they send. Links match by
provider, type and number; the reference label is one shared helper the
desktop card uses too. Folder rows keep no long-press. One sheet file
plus the pure model.

* fix: review nits on the row-only details sheet

* refactor(mobile): one view state and one settings read for the phone's Workspace options; tighter card glyph wiring

- The phone's Card layout and card style now live in its view state; one parallel settings.get
  reads them with pinned placement beside ui.get.
- The Card layout preset flag is a write option, no longer a view field or a change-event field.
- getWorktreeCardDetails reads the raw desktop settings and resolves the layout once; the desktop
  card computes it once in its foundation.
- The row computes its glyph presence in one place; Ports is hidden through one set.
- Drops the vanished-page effect, the native-only a11y props and the web-only switch branch.

* fix(mobile): the list settings read applies each half on its own, with tests; pinned placement changes announce themselves

* fix(mobile): compact host glyph tests for older rows and the new card style; truer comments

* fix(mobile): Card layout writes the setting before its preset; Card display names folder paths when the desktop has folder workspaces

* fix(mobile): match the branch PR by repo and link, not number alone
2026-10-15 23:01:45 -07:00
Brennan Benson ec9a0af3e8 Show phone subagents and open the correct parent chat (#28686)
The phone's worktree list now shows the running subagents the desktop sidebar shows, under their parent agent with their own ages, read from the host's existing child records. Tapping one opens its parent's existing chat or terminal. The feature was reviewed in #26423 and merged into this branch.

Links from the phone to native chats whose saved tab ID differs from the session-derived one (restored, reserved and headless chats) now resolve to the chat's real tab instead of missing it.
2026-10-15 21:21:05 -07:00
Neil f152101010 ci: reuse prepared Windows security addon in PR packages (#29352)
* ci: reuse prepared Windows security addon in PR packages

* ci: prepare bounded Windows security reuse qualification

* ci: ignore owned Windows benchmark receipts during source checks

* test: inject Windows security qualification dependencies

* test(mobile): register chat view mocks in test roots
2026-10-15 21:05:34 -07:00
Neil 330fd55376 test(mobile): split MobileNativeChatView.test.ts under the max-lines limit (#29368)
Pure move: the 36 test cases and their assertions are unchanged, now in six feature files.
Shared mocks and render helpers live in MobileNativeChatView.harness.test-fixture.ts. The only
non-move edits are typing and formatting: the harness adds getComposerEditGeneration to its
default props (a required prop the fixture never passed), tests use hostType(node) for tag
comparisons, and the typecheck baseline and reliability gate paths drop the removed file.
2026-10-15 19:49:38 -07:00
Jinwoo Hong 7538fa9466 feat(mobile): the phone's Workspace options sheet, with every desktop option (#28997)
* fix(mobile): phone server rows follow the desktop's other-client and sleeping filters

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* wip: workspace view descriptors

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

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

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

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

* wip: desktop option items from shared choices

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

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

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

No visible change.

* wip: row facts

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

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

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

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

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

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

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

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

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

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

* wip: phone uses the shared evaluator

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

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

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

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

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

* wip: phone view state carries project order and card props

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

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

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

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

* feat(mobile): the phone's Workspace options sheet, rendered from the desktop's shared option list

The Filter drawer becomes the desktop's Workspace options: hosts, projects with search, group,
sort, project order, card layout and properties, card display, and every filter. Each option
renders through a table keyed on the shared option keys, so a new desktop option fails to compile
until the phone has a control. Sort and group pickers use the desktop's words; Repo reads Project.

* fix(mobile): Workspace options sheet review fixes

Hosts read as on the desktop (none checked while all show; the last shown host is disabled).
Project order falls back to the served value until the shared one is read. Card layout and card
style changes now push to an open phone. Pick-one rows read as radio buttons. Simpler wiring:
flat card state, the preset flag rides the patch, shared choices used directly.

* fix(mobile): reset project order on host switch; simpler preset-flag handling

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

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

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

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

* feat(mobile): phone rows follow Card layout, Show properties, Card display and agent activity

One shared helper (getWorktreeCardDetails) decides which card details show for the chosen
properties and layout; the desktop card's flags and the phone row both read it. The phone row
hides the PR badge, note/issue/Linear glyphs, host badge and agent rows the desktop turns off,
and the Full list agent layout shows every agent row.

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

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

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

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

* fix(mobile): let a tall bottom drawer scroll on the page

The drag wrapper around the drawer's ScrollView had no flex limit, so it grew to
its content and the sheet clipped it instead of scrolling. The full Workspace
options sheet is the first drawer tall enough to hit this.

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

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

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

* test(mobile): give the project catalog fixture its project order

* feat(mobile): name the options chip Workspace options, as the desktop does

The chip keeps the active-filter count and reads to screen readers with the
desktop trigger's label. The pluralized filter count moves to shared code so
both surfaces word it the same way.

* fix(mobile): show the Workspace options count as a corner badge so the chip row fits

"Workspace options (2)" squeezed the Sort and Group chips to ellipses at phone
width. The count moves to a corner badge, as on the desktop trigger, and the
chip drops its icon, which the desktop trigger does not show either.

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

- The four kind switches come from WORKSPACE_KIND_SWITCH in workspace-kinds;
  the shared evaluator and main's phone-push check both apply it, and the filter
  type derives its switch fields from it.
- git-identity.ts folds back into git-identity-display.ts, as on main.
- SidebarFilterState moves into sidebar-filter-actions.ts; visible-worktree-kinds.ts is gone.
- buildWorkspaceRowPredicate is module-private.

* feat(mobile): Workspace options sheet in the desktop menu's shape

The sheet showed every choice of every option, so the filters sat screens down.
It now follows the desktop menu: Hosts and Projects rows with their value, Group by
as a segmented control, Sort by, Project order, Card layout and Show properties
as rows that open a page of choices, and the filters as switches. Each option's
control (an inline row, or a value plus a page) still comes from one table keyed
by the shared option list. The segmented control is the voice settings one,
extracted so both screens share it.

* fix(mobile): Workspace options review fixes

- A page whose option stops showing is forgotten, so it cannot reopen by itself.
- Only the switch takes the tap on a filter row, so the web page writes once.
- Sections split their rows in one pass; unused heading styles go.

* fix(mobile): neutral filter switches on the page

react-native-web paints an on-switch thumb teal unless given activeThumbColor,
which React Native's types omit. MobileSwitch applies the app's switch colors,
and its web sibling also sets that prop.

* refactor(mobile): one MobileSwitch file, adding the web-only thumb color on web

* fix(mobile): Show properties reads Hover under Compact, as the desktop does

The row's value now comes from getWorkspaceCardPropertiesValue in the shared
options, which the desktop trigger uses too: Hover under the compact layout,
otherwise how many properties show. Card display shows no value, as on the desktop.

* test(mobile): find the renamed Workspace options chip in the page drawer render check

* feat(mobile): drop the Sort and Group chips; the Workspace options chip covers both

The desktop has only its options button, and the sheet now holds Sort by and
Group by. The chips' pickers, their screen state and the sort label go with them.

* feat(mobile): option pages slide in from the right and keep the sheet's height

The main sheet stays laid out under an open page, so the drawer keeps its height
going in and out. The page slides in over 200 ms and back out on back (the row,
or Android's back key), instantly under reduced motion. The reduced-motion hook
moves to platform/ now that two screens use it.

* fix(mobile): option pages fill the sheet and open at its top

- The page layer grows to the pager's height, so no part of the main sheet shows below a short page.
- The drawer takes a scrollResetKey; the sheet passes the open page, so a page opened from a row low on a scrolled sheet starts at the top.
- The main sheet's height is measured under a page too, and the pager state lives in the sheet so it resets when the drawer closes.
- A misplaced doc comment goes back to its function.

* fix(mobile): phone rows show the Jira, automation and CLI glyphs; the options pager forgets a gone page in an effect

* fix(mobile): Jira glyph sits before Linear as on the desktop; Jira reader tests beside it

* fix(mobile): the phone leaves Ports out of Show properties and Card display

* refactor(mobile): clearer names and tests for the phone's hidden card properties

* refactor(mobile): one view state and one settings read for the phone's Workspace options; tighter card glyph wiring

- The phone's Card layout and card style now live in its view state; one parallel settings.get
  reads them with pinned placement beside ui.get.
- The Card layout preset flag is a write option, no longer a view field or a change-event field.
- getWorktreeCardDetails reads the raw desktop settings and resolves the layout once; the desktop
  card computes it once in its foundation.
- The row computes its glyph presence in one place; Ports is hidden through one set.
- Drops the vanished-page effect, the native-only a11y props and the web-only switch branch.

* fix(mobile): the list settings read applies each half on its own, with tests; pinned placement changes announce themselves

* fix(mobile): Card layout writes the setting before its preset; Card display names folder paths when the desktop has folder workspaces
2026-10-15 19:27:51 -07:00
Brennan BensonandOrcaWin 7c23f79622 fix(quick-commands): save agent prompts whole, bounded by what a chat message carries (#24488)
* fix(quick-commands): save agent prompts whole, bounded by what a chat message carries

Agent quick-command prompts were silently cut to 6,000 characters on save:
the editor accepted a long paste, settings normalization trimmed it, and
reopening showed the shortened copy. The 6,000 was a Windows launch speed
threshold, not a limit on prompts.

- Storage no longer trims. A save is refused, never trimmed, when a prompt's
  chat text block is over 256 KiB of JSON (measured on its real escaped size,
  the measure the chat send schema applies) or the list is too large for one
  paired reply. Host and desktop store both check before writing.
- The editor shows the size against the limit and blocks Save past it, and
  stays open with the text when the host refuses the save.
- terminal.quick-commands.long-prompts.v1: hosts withhold prompts over 6,000
  characters from clients that do not advertise it (older clients reject a
  whole list holding one); editors keep an older host's 6,000 cap.
- The phone editor no longer drops pasted text past 6,000 via maxLength.

Launch delivery of long prompts is left to the launch-prompt series
(#24257, #23962, #24080); this branch's earlier paste-after-ready delivery
duplicated it and is removed.

* fix(quick-commands): drop a stale duplicate doc comment

* fix(quick-commands): advertise long prompts to this build's own host too

* fix(quick-commands): keep submitted editor immutable until save settles

* test(quick-commands): keep storage and save lifecycle coverage focused

* fix(quick-commands): keep portalled choices within pending form ownership

* Preserve retained credentials when settings validation refuses a save

---------

Co-authored-by: OrcaWin <alpha-eng@stably.ai>
2026-10-15 18:37:41 -07:00
Brennan Benson e9c77360f5 fix(mobile): open each chat at its newest message when switching chats (#29099)
* fix(mobile): open each chat at its newest message on a chat switch

The chat view stays mounted across chat tabs, so a reader's scroll in one chat
carried into the next: it opened mid-transcript and not following. Reset follow
intent and the page's history head when the chat changes, before paint, and keep
view-wide state (Tools toggle, text size).

* fix(mobile): pin a switched-to chat to its own end on the page

The page pin scrolled to the last reported content height, which after a
chat switch is still the previous chat's, so a taller chat could paint
one frame mid-history before its own height arrived. Pin to the scroller's
live height instead.
2026-10-15 18:29:26 -07:00
Neil a622f9efd3 fix(mobile): stop "Opening workspace" spinning forever when the page never loads (#29274)
* fix(mobile): fail a shell document the view never finishes loading instead of spinning forever

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

* refactor(mobile): arm both shell document deadlines from one case
2026-10-15 18:27:25 -07:00
Brennan Benson d6f9df9b74 fix(native-chat): settle acknowledged messages after a paged reconnect (#28993)
* fix(native-chat): retire acknowledged send echoes after catch-up

* test: follow phone lint rules in reconnect regression

* test: format reconnect message expectation
2026-10-15 18:16:56 -07:00
Brennan Benson ef98eff38b Allow worktree parents across projects on the same machine (#28942)
* Allow worktree parentage across projects on one host

* Keep sidebar placement and project ownership consistent

* Keep project labels on phone rows outside their own section

* Derive sidebar placement and reveal from shared occurrences

* Preserve project text across workspace card styles

* Resolve saved lineage peers without rebuilding host projects

* fix: preserve independent project deletion progress

* fix: satisfy mobile formatting and test lint checks

* test: declare project fixture arrays without assertions

* Align reveal fixtures with rendered workspace occurrences

* fix: reuse project name for cross-repo child cards

* fix: satisfy mobile section lint after main merge

* fix: retain project label for nested mobile rows

* test: keep nested card owner assertion resilient

* test: assert project badge visibility across card styles

* test: drop lineage tests already covered by the whole-flow suites

The host-peer projection is exercised by the runtime and SSH listing
integration tests, and the simple phone nesting case is covered by the
deep-chain, Pinned and hidden-project flow test.
2026-10-15 16:47:44 -07:00
Brennan Benson e2f2277d51 feat(native-chat): one queue header that becomes the pause row; drop the composer Resume and the send dialog (#28654)
* feat(native-chat): one queue header that becomes the pause row; no composer Resume or send dialog

The queued cards now sit under one header row on desktop and phone: "Queued"
and the count normally, and "Paused" with the count, Clear all and Resume
while the host holds the queue after a Stop. A chevron folds only the cards;
the choice is local to the view and resets when the queue empties.

The composer's Send no longer turns into a second Resume, and a message sent
into a paused queue no longer asks "Send message?": it sends, its accepted
turn lifts the pause, and the cards follow it. Clear all deletes every card
through the existing per-card delete. No wire or host change.

* test(mobile): find the background-tasks strip's toggle inside the strip, not the queue header's

* Make queued Clear all atomic across clients

* Show queued message failures in collapsed headers

* Use per-card delete for paused queue Clear all

* Join repeated paused queue Clear actions
2026-10-15 15:37:21 -07:00
Jinjing 0d4eb242d7 Recover failed-job log retrieval onto main (#29180)
* feat(checks): add optional job log outcome

* fix(checks): preserve host capture lifetimes and safe outcomes

* fix(checks): retrieve and retry bounded failed-job logs

* test(checks): cover queued snapshots and captured error safety

* test(checks): verify sanitized host excerpts and paired failures

* test(checks): identify measurements and preserve paired cleanup

* Use the generated check-log fixture in host retrieval tests

* Keep real paired-server shutdown outside the injected stall deadline
2026-10-15 10:36:21 -07:00
Kelvin Amoaba 507d0a9122 fix(mobile): show answered approvals and questions as receipts, and a waiting question as "Awaiting user input" (#28906)
* fix(mobile): draw answered approvals and questions as receipts, and a pending question as its ask row

The phone projected a structured chat's journal with the host's plain
projection: an answered approval read "Allow Bash? {json} resolved", an
answered question listed its choices without the answer, and every
AskUserQuestion call stayed a raw tool line, also while its card was open
above the composer.

Desktop's question projection and receipt rules move to src/shared
(structured-agent-question-projection, native-chat-resolution-receipt,
with the receipt subject and answer heading and their words). The phone's
structured session now projects through them and hands each row its
receipt; new MobileNativeChatResolutionReceipt and
MobileNativeChatAwaitingInputRow draw them as desktop does. In a terminal
chat, a question call draws as "Awaiting user input:" until answered and
"Asked:" after.

* fix(mobile): say a question was asked, not awaited, once its agent has ended

A structured chat's question stays pending in the journal after the agent
that asked it ends (a restart, a crash, a new generation). The host stops
listing it as actionable, so the answer card goes away, but the phone's
row still read "Awaiting user input" and announced itself as live.

The session hook now returns the host's actionable prompt ids next to the
receipts, and each row asks the same shared
isActionableStructuredAgentSessionPrompt desktop's transcript row calls.
A question nobody waits on reads "Asked: …", as on desktop. The row
callback moves to its own hook so the chat view stays within its size.

* fix(mobile): decide a native chat's open question with desktop's selector

The phone decided whether a question call was still awaited with its own
rule (no answer yet and the turn working). Desktop asks the shared
selectActiveToolCall, which also reads the call's state, so a call marked
completed with no output reads "Asked" there and "Awaiting" on the phone.
Native chats now call the same selector. Terminal chats keep their rule.

* fix(mobile): zoom a receipt and its question row with the chat

Before receipts, an answered question or approval was a faint system line
that grew with the chat's pinch zoom. The receipt and the question row drew
at fixed sizes, so zooming no longer enlarged them. Both now take the
chat's zoom, through the same scaledType the notice row uses, which moves
next to the other row styles.

* fix(mobile): list a group's questions once nobody can answer them, and zoom the row's icon

A grouped prompt's row names only a count ("Asked: 2 questions"). Once it
is cancelled, or the agent that asked has ended, the answer card is gone
and the receipt lists no answers, so the phone showed the questions
nowhere. The row now lists them under its line, following desktop's
listsQuestions rule: the receipt asks for the list only when it shows no
answers, and the transcript's own question rows keep it off, as desktop's
tool run does. While the host waits, the card shows them.

The row's icon also grows with the chat's zoom, as the notice row's icon
does, so it stays level with its line.

* refactor(mobile): trim comments and simplify the question row and receipt

Comments that restated the code go. The answer heading takes the subject
its caller already has, dropping two conditions that never changed the
result, on desktop and the phone. The question row lists a group's
questions whenever its caller asks, as desktop's does, and the receipt
asks only while nobody waits. fontScale is required on both new rows.
2026-10-15 14:14:50 +00:00