* Finish structured agent readiness before delivering held work
* Publish the Codex process before protocol startup
* Publish ACP agent processes before session startup
* Publish Pi at spawn and settle signed-out starts once
* Adapt permission fixtures to startup lifecycle
* Keep model discovery for chats before their first start
* Use one host readiness promise for provider operations
* Await provider opening in the execution host
* Await Claude initialization in host-owned open
* Observe Claude readiness through the host status
* Keep optional readback behind host readiness
* Recheck launch permission after host readiness
* Preserve the awaited child during permission revalidation
* Await Codex protocol open under host ownership
* Await ACP protocol open in the host
* Derive ACP opening from its protocol session facts
* Exercise unanswered ACP options with an explicit silent route
* Await Pi open and fail signed-out construction
* Read Pi catalog facts from awaited opening
* Keep the Pi fixture exit observer
* Record failed owned opening before the first message
* Abort a matching provider open when its child exits
* Cover provider exit during an unanswered host open
* Cover explicit failure facts on a host-stopped child
* Capture message choices per accepted send and preserve picker state
* Apply accepted Claude message choices at prompt handover
* Apply captured message choices at provider dispatch
* Bind native chat choices to accepted message dispatch
* Reset Codex turn choices for each captured message
* Resolve Codex message defaults from native facts
* Align message choice and Pi default fixtures with provider dispatch
* Align option fixtures with saved message choices
* Match Codex message choices to the active turn origin
* fix: let pending commands reach handover refusal
* Abort pre-prompt message choice writes with host-owned wait
* Verify message choices through provider input boundaries
* Keep legacy mobile test hosts explicit about message choices
* Keep startup waits cancellable and preserve operation order
* Derive Codex startup exits from host phase
* Open Codex protocol in background-task test fixtures
* Keep ACP startup ownership until handshake completes
* Run ACP opening catalog fixtures with Node SQLite
* Verify saved ACP picks without live option writes
* Keep ACP catalog bookkeeping from failing an options read
* Restore signed-out restart and fail Pi startup faults
* Run Pi opening catalog fixtures with Node SQLite
* Preserve message choices across cold clients and kept cards
* Remove the unused provider option restore ledger
* Clarify launch choices and legacy option hydration
* Remove remaining restore-ledger forwarding and assertions
* Remove unused ACP and Pi restore bookkeeping
* Keep live choices and delivery order while agents start
* Drop retired startup revision from Codex opening
* Keep live-option cancellation until message choices and clarify catalog reads
* Hold ACP option replies in live-write cancellation regressions
* Remove retired Pi startup revision and match saved-pick behavior
* Check running choices and retained rewind messages after delivery integration
* Apply startup picks and let parked messages release operations [skip ci]
* Remove unused Codex opening report field [skip ci]
* Keep ACP restoration facts local to the provider [skip ci]
* Apply Pi startup picks after readiness and remove unused open facts [skip ci]
* Cover goal changes during the held Codex message wait [skip ci]
* Save a waiting startup pick after Stop ends its opening [skip ci]
* Save choices when startup ends or a replacement is still opening
* Check saved choices separately from Claude startup reports
* Release scripted Codex startup before awaiting queued picks
* Check the saved picker selection in per-message choice fixtures
* Exercise current choices and eager client capabilities in runtime fixtures
* Reuse runtime fixture reads and type the fake connection directly
* Align startup fixtures and preserve observed Claude exits
* Honor queued send authorization in Claude runtime fixtures
* Open Codex protocol sessions before Stop fixture dispatch
* Wait for the opened Codex child before runtime notifications
* Keep repeated permission picks usable and align Codex fixtures
* Validate pending picks against the live option state
* Order startup picks before later messages and retain flow regressions
* Keep reviewed lifecycle contracts while pruning duplicate tests
* Save picks after a proven exit without requiring a child
* Type the startup reply fixture against the Claude control contract
* Keep Codex startup flow contracts and remove overlapping adapter tests
* Restore the committed dependency lockfile after test tooling
* Keep startup recovery flows and remove duplicate Pi handshake cases
* Cover both Pi startup choices in the delivery flow
* Match the Pi prompt entry recorded by the scripted wire
* Honor the before-input callback in the startup reply fixture
* Keep message choice flows and remove overlapping selector tests
* Keep startup option menus independent and show the saved choice
* Exercise startup picks across real menus and the runtime
* Match startup picker fixture to saved message choices
* Keep saved effort visible when picking a model during startup
* Include the running-turn scope in repeated Stop fixture
* Use the current attach contract in import fixtures
* Keep the Codex journal translator within its line limit
* Match ACP startup fixtures to saved picks and late Stop behavior
* Check startup effort after saved picker choices are acknowledged
* Complete Stop target types and keep the effort regression concise
* Use requested picker values in the effort regression acknowledgement
* Require owned provider spawn and one host-awaited open (from #28055)
* Restore the live checks the startup cleanup's deleted tests pinned
One assertion each, in the surviving suites: a stale-generation exit is
ignored, a failed create lets its sink go, a readback before open is saved,
an idle stop that fails is retried, a no-pick Pi start teaches its default,
an older client's pre-spawn failure and packed answer, an undrivable chat
stays readable, the resting slash menu stays on its own host, the main agent
reads apart from subagents, restore publishes status, naming runs off the
host's status feed, and a throwing logger never reaches the operation.
Also corrects the reliability gate note that claimed the send-idempotency
suite replaced the refusal-retry tests.
* Read a rewind in doubt outside the chat's serialize and apply it in the commit
The started settle and the attach's ready-child path read the provider's
history first, then apply what it proves in a short commit that re-checks
the rewind it read.
* Accept messages while the agent starts: run the start outside the chat's serialize
The chat's serialize was both where a send was admitted and where its agent
started, so every slow step of a start (closing-child join, lease
reconcile, recovery probes, resolvers, history read, spawn, sink drain,
provider dispatch) held every later send.
A start now registers in one short step before its first wait: its abort
controller (which Stop, close and quit already reach outside the lock),
its startup clock (the 10-minute ceiling now counts from here) and quit's
drain. Every wait after runs outside the lock, raced with that signal, and
every write is a commit that first re-checks the start is still the
session's. A start that loses the check is late: its spawn is closed
through the post-acquisition failure path and nothing is handed to it.
- The delivery loop's prepare returns a start handle; the loop awaits the
start outside, then re-prepares. Handover writes the pending row and
begins the dispatch abort handle in one commit, calls the provider
outside after re-checking the child, and resolves in a second commit.
- Ready operations claim a start instead of running one in their step.
- A client attach runs the same steps under one serialize, as before.
- An exit reported between spawn and publish is held for the publish
commit, which ends the child it indexes.
- An unproven close still fails only what was queued when its start was
asked for.
* Test that a send is accepted at every wait an agent start makes
One whole-flow test holds the start at each wait in turn (closing-child
join, reconcile probe, recovery re-probe, resolver, spawn, sink drain,
open, started recovery, pending row, provider call): a second send is
accepted at once, a Stop is recorded without waiting and nothing reaches
the provider, and what was sent goes to the published child once, in
order. Also quit before spawn, concurrent sends, a lease left recovering,
and an idle sweep racing a send.
* Bound each startup step on its own and drop the startup silence rule
A start no longer ends after 60 s without output: output is not protocol
progress. Each protocol step now has its own bound where its handshake
lives, and one 10-minute ceiling from registration stays as the outer bound.
- Codex: initialize 15 s and thread start/resume on the connection's 30 s
default again, instead of the startup ceiling.
- ACP: initialize 60 s, authenticate 120 s, session new/load/resume/fork
300 s; turns stay unbounded and option writes keep their caller's bound.
- Claude: the initialize answer has a 60 s bound; past it the open fails
with the ordinary startup-failure words and the host ends the child.
- The onOutput plumbing that only fed the silence rule is removed.
* Count Pi ready once it reports its state
Pi's start no longer waits for its model listing and slash commands.
The open returns after get_state and the unknown-provider check; the
listing then arrives as an options report (with the catalog listing the
start resolved) and the commands as a later publish. An empty model list
no longer fails the start as signed out: a signed-out Pi runs the unknown
provider, which still fails it, and a prompt it cannot run is refused
with the sign-in words by the existing prompt path.
get_state keeps the connection's 30 s request bound instead of the
startup ceiling.
* Test that model and effort picked while a chat is created ride its first message
A slow host's create reply arrives after the user picked a model and effort. The picker's view
layers the launch's held picks over its seed and the host's saved selection, and every send
carries what the picker shows, so the first message carries the picks whether the host's status
names its selection or not, and while the last pick is still on its way to the host.
* Start a phone's new empty chat on its first message
The phone's new chat is an agent.launch with no prompt, which always created the chat with its
agent started, while desktop chats start on the first message. The phone now advertises the
create-without-start capability, and a launch with no prompt from a caller advertising it creates
the chat at rest, keyed in the ledger by its create intent as a client's create is. Launches with
a prompt, the CLI (which advertises nothing), orchestration workers and ensure still start at once;
an older host ignores the capability and stays eager.
* Take main's lockfile; the branch changes no dependencies
* Restore main's exact text in files the merge only reformatted
The pre-commit formatter rewrote 37 of main's files staged by the merge, though this branch never changed them, pushing two past the line limit. Committed without the hook because the hook would reformat main's text again; CI lints the result.
* Fit the startup tests to the owned-spawn, host-awaited-open contract
Test doubles gain the mandatory open and an acquisition generation, the
removed started event gives way to the rig's proveStart, and two literal
types are tightened for CI's typecheck. A Stop during a held start writes
no host note on this branch: the taken-back send draws its own
stopped-before-start row, which the startup-attempt test now asserts.
* Fit the remaining CI checks to the startup limits and current releases
- Run agent-launch-at-rest.test.ts in the SQLite Node runtime project.
- Pin the send-options cross-version case to v1.4.228, the first published release with agent-session.send-options.v1; v1.4.226 was never pushed.
- Load the baseline's isStructuredTab from src/shared (moved in v1.4.228), falling back to the renderer path for older baselines.
- End the never-proven start with a short startup ceiling: the silence limit it relied on is gone, and the ceiling counts from registration.
* Read the option stamp from the chat's one host-execution selection
* fix: preserve startup authority and accepted agent choices
* Retain dispatch authority and reconcile accepted options
* fix(native-chat): preserve accepted choices and cancellation authority
* Restore established client selection flow
* Preserve provider output observation and typed startup fixtures
* fix: preserve initial Claude launch choices and effort readback
* test: complete Claude catalog session fixture
* Preserve incoming main files after merge formatting
* Preserve compact handover through startup merge
* Type instant startup fixtures before their open is supplied
* Keep browser sign-in under its existing expiry owner
* Finish the main merge: retired-model display, start rewind cut, new tests
A model the Claude CLI retired is reported again, so the picker shows what the child runs and the
next send moves the chat off it. The start reads a rewind in doubt against the handle it answered
with, the record its commit then applies to. Main's tests follow the start's open: the admission
fixture holds a real turn, and a sign-in close during setup fails the open.
* Type the start's rewind record as the store returns it
* Type the Sleep test's held open as the provider open it replaces
* Sleep counts a chat stopped once the start it aborted leaves it at rest
A start runs outside the chat's lane, so Sleep's step can run before the start it aborted has
settled; it then re-reads the chat once that start is done, instead of reporting it unverified.
* Keep Sleep's start-abort bookkeeping beside the rest of Sleep's stop
* feat(relay): add the per-cell phonePath switch, effective only in reserve mode
Step 6 of the relay redesign keys every phone-path change on this switch. The cell
reports the effective value in runtime status, and the flag tool refuses phonePath=memory
unless the cell is reserve, and a reserve flip that would turn phones over unnamed.
* feat(relay): resume paired phones from one device read on memory cells
With phonePath=memory in reserve mode, a phone resume is admitted from one device
read and the cell's own seat, and takes no activity leases. A refused device closes
4401, a failed read 4503, and only a missing device row falls through to the invite
path. A phone whose host has no seat gets one assignment read: 4409 when the row
names another cell or the read fails, otherwise 4404.
* feat(relay): keep a memory resume's basis in the cell and confirm it from one read
A phone resumed on the memory path keeps its connection basis in the cell, not in
relay_connection_bases. Its confirm rereads the device row once, renews it only when
less than 29 days remain, and always answers a current-version confirm renewed:true
with the expiry the database holds. Confirms route by where their basis lives, so a
flip between accept and confirm completes either way. Retries answer from a 10-min
in-memory result map.
* test(relay): roll a memory phone cell back with confirms in flight
Flipping back by admitMode=db, phonePath=db or a dead-man trip leaves in-flight
confirms to finish on their memory basis, writes no phone lease and leaks none, and
evacuation still refuses the cell while Postgres records it as reserve.
* feat(relay): batch a memory cell's resume-confirmed audit rows
Memory confirms buffer their audit rows and the cell writes them as one multi-row
insert every 5 s or 500 rows, idempotent by a per-event id. The buffer holds at most
5,000 rows and drops a row older than 10 minutes rather than retrying it, counting
and logging both kinds of drop.
* feat(relay): count and log phone-path failures, and every attach deadline
The runtime metrics line gains phone refusals by close code, attach deadlines, accept
database errors, resume confirm failures by code, completed accepts by phone path and
the resume audit drop total. Every attach-deadline 4404 now logs what the cell knew of
the desktop's control (pong age, ping outstanding, control age and round trip), so a
half-open control can be told from a slow data dial.
* feat(relay): close an attach-deadline 4404 with the attach-timeout reason
The phone close reason from #30607 now rides the close frame of the 4404 a cell
sends when the desktop's data leg misses the attach deadline, so OTA phones can
redial the same cell once instead of waiting out the host-offline delay. The failed
relay-hello frame is unchanged, and old phones ignore the reason.
* test(relay): drill a memory cell through a database outage, and pin phone renewal
A blocked database leaves splices forwarding, closes new resumes 4503 and fails
confirms cleanly, lets an in-flight confirm finish on its memory basis after a flip
back, and drops audit rows past 10 minutes instead of writing them late. On the phone,
a relay-only session lives 31 days under the once-a-day renewal rule, and would not
if a not-due confirm answered renewed:false.
* fix(relay-ops): store phonePath=db as no key, so old images keep taking writes
An explicit phonePath=db left in the switch file would make the flag tool refuse every
later write after an image rollback, including a watcher's admitMode=db flip back.
* fix(relay): log a failed no-seat read, recheck the switch after the device read
A failed assignment read for a phone with no seat now logs a rate-limited line before
its 4409. A switch that turned off during the device read sends the phone down today's
path. resolveResume reuses the one-read lookup, so the expiry and grace rules have one copy.
* fix(relay): log a failed memory renewal; test flips, replays and basis cleanup
A renewal write that fails still answers the stored expiry, but now logs a rate-limited
line so a persistently failing update is visible before phones age toward re-pairing.
* test(relay): assert a rolled-back memory splice issues no statement when it closes
* fix(relay): flush resume audit early only on crossing the batch size, drop the shutdown flush
A failing database no longer takes a 500-row insert per confirm; retries wait for the
5 s timer. The shutdown flush raced the pool closing, so it is gone and the at most 5 s
loss is documented. A stopped buffer no longer re-arms its timer.
* test(relay): assert the aged audit rows were dropped, not written
---------
Co-authored-by: E2E Test <e2e@test.local>
* fix(mobile): edit a queued message in a plain text field with compact cancel and save buttons
* fix(mobile): the queued edit field grows from one line to four in the phone's web page
* fix(mobile): the queued edit's cancel and save buttons take 44pt-tall taps, and the queue no longer jumps while typing
- Measure the edit field's height in a hidden copy of the textarea instead
of collapsing the live one: WebKit clamped the queue list's scroll
position on every keystroke. It also re-measures when the field's width
changes (rotation).
- react-native-web drops hitSlop, so the X and check were 28pt targets.
Each button is now a 44pt-tall target reaching its neighbours, with the
28pt square drawn inside; the layout looks the same.
- Announce saving with aria-busy, which react-native-web maps.
- List the new web sibling in web-overrides.json.
* fix(sidebar): phone feed list leftovers before the flip (dark)
Bump the feed's semantics version for P11's desktop facts; mark switch-on phone page bundles so packaging and orcad staging refuse them; show the page's last-listed rows before the first feed read; draw the host's pending creations; pass the listed host's own label into host sections instead of the running device's platform.
* fix(sidebar): review fixes for the phone feed leftovers (dark)
A dark orcad may stage the dark page (only a marker of an off switch is refused); the bundle tree writer leaves the marker; the pre-feed list keeps the view's filters; a creation names its project only outside that project's header and survives search when its project matches; the feed view and row labels read one resolved desktop platform.
* fix(sidebar): second review fixes for the phone feed leftovers (dark)
Test the off-switch marker check across switches; resolve the desktop platform once for rows and headers; a section search narrows keeps only matching rows.
* fix(mobile): recover a relay that died during an app switch in one short probe, and retry an attach timeout through the director once
Refs STA-11130.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(mobile): redial the same cell once on attach-timeout instead of re-resolving
Aligns with the relay step 6 design: reason string attach-timeout, no director
re-resolve (the director names the cell the host is seated on).
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(mobile): probe decisively only on a real background return, 4s, and jitter the attach-timeout redial
Co-Authored-By: Claude <noreply@anthropic.com>
* test(mobile): replace type assertions with a typed fake in the relay probe tests
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* fix(mobile): put the session tabs in the top bar on a landscape phone
A sideways phone is about 390pt tall and the session header spent two rows
on it. On a landscape phone the tab strip now shares the top bar with the
back button, title and panel icons. Portrait and tablet layouts are unchanged.
* fix(mobile): give session tabs side borders and a filled active tab
In the landscape top bar the tabs sat between the title and the panel
icons with nothing marking where one tab ended. Each tab now has a 1pt
border on its sides, running the full height of the bar, and the active
tab gets the raised fill, as the desktop tab strip draws them. The same
strip is used in portrait, so both orientations match.
* fix(mobile): one button style for the session header and tab strip; size tabs to titles
The header had three button sizes and the new-tab and quick-commands
buttons were bare 16pt glyphs in a 40x36 area with no extra touch room,
so "+" was hard to hit. Every header and tab-strip button now uses the
same 36pt rounded button with an 18pt icon and a 44pt touch area. Tabs
size to their titles (44 to 128pt) instead of a fixed 128pt, the strip
row is 44pt, terminal tabs get a terminal icon, and tabs expose the tab
role and selected state to screen readers.
* fix(mobile): reset tab strip measurements on view attachment
* style(mobile): format tab strip ref dependencies
* fix(mobile): reserve independent header button touch targets
* fix(mobile): type header button test against its component
* fix(mobile): keep two header rows in landscape windows too narrow for top-bar tabs
A short, narrow window (a ~390x350 web window) is wider than tall, so it
counted as a landscape phone and moved the tabs into a top bar that has no
width left for them. Top-bar tabs now also need at least 568pt of width,
the narrowest landscape phone.
* fix(mobile): give the landscape top-bar tab strip a fixed left edge so scrolled tabs never read as part of the title
* chore: recompute mergeability after retarget to main
* feat(mobile): the phone page lists workspaces from the host's sidebar feed (dark)
* fix(mobile): settle pending option changes by latest intent; feed carries the desktop's focused host and host names (dark)
* fix(sidebar): the feed carries the desktop's pinned placement; e2e compares the phone list to the desktop sidebar (dark)
* test(sidebar): the phone list e2e also expands and unpins
* fix(sidebar): phone feed list review fixes (dark)
A collapsed or worktree.ps-failing feed list no longer shows an empty or error state; a queued worktree read uses the latest client; a superseded option change does not report a refusal; the window shadow leaves the focused host to main's test (focus-read ratchet).
* fix(sidebar): phone feed tests typecheck; e2e helper no longer pulls the native runtime types
Mobile test fixtures fill the fields the test project's types require; the feed trigger type lives with the feed reader, so the e2e helper's imports stop dragging react-native globals into the e2e program; React Doctor's two blocking findings.
* fix(native-chat): a Claude chat keeps a model its CLI stopped listing
Claude Code 2.1.302 stopped listing opus[1m], which still runs, and chats on
it moved to the list's default on the next send, effort remapped. The picker
no longer derives a replacement from the list: the chat's own model shows as
a selected-only entry, its effort read-only, and every send carries both.
A real model_not_found reply still recovers: the options answer shows what
the reset child runs, so the next send records it. A reopened chat's catalog
read lists its own model so older clients that replace one keep it too.
Tracked in STA-10947.
* test(mobile): drop the send-options capability the options hook no longer reads
* test: an unlisted model's saved effort shows read-only at rest, as live, for every agent
* fix(native-chat): round-1 review fixes; a new chat's unlisted remembered model falls back as on main
- The read-only value predicate keys on the effort row, so the phone's legacy
host Permissions row opens as before.
- The desktop read-only effort row honours a row kept from the previous model.
- A live child with no listing of its own answers with the saved listing's
ids, so the read-only effort reads the same live and at rest.
- The read-only effort borrows listed rows' effort label and level names.
- The host seeds a new Claude chat from a remembered default the saved listing
no longer names on that listing's default, effort nearest, as main did; a
chat's own saved or running model is never replaced.
Tracked in STA-10947.
* test: pin the new-chat seed fallback on its typed helper, with no casts
* fix(native-chat): round-2: a carried effort moves with a fallen-back model; one guard
- An at-rest create carrying an effort but no model, on a remembered model
the saved listing no longer names, now moves that effort to the nearest
level the fallback model offers, as main did.
- A create whose own options name no model, and whose merged options carry
none, reads no listing.
- The seed fallback keeps one guard and no unreachable branch.
- The read-only effort finds its level's name in any listed model's choices.
Tracked in STA-10947.
* fix(native-chat): a live options read with no record borrows no saved ids and never throws
* feat(sidebar): Smart Done means finished and unread (dark)
Owner decision Q1: with ORCA_SIDEBAR_INPUTS=main, a Done row holds its tier only
while the workspace unread flag is set, so opening the workspace or sending the
next prompt ends it. Reuses the existing flag; no new state or timer.
Co-Authored-By: Claude <noreply@anthropic.com>
* test(sidebar): reading a workspace drops its Done row in the live list
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* refactor(native-chat): move the slash picker builder and upload chunking to shared
Mobile builds the same skill and command list as desktop.
* feat(mobile): redesign the chat composer with file attachments and inline skills
* fix(mobile): the composer field owns its input style
The page's font-size census cannot follow a style passed in as a prop.
* feat(sidebar): sort stamps reconcile stored tiers at startup against live hook rows and terminals (dark)
* test(sidebar): a restart clears an unbacked local tier and keeps an SSH host's
* fix(sidebar): run the sort tier reconcile once hook rows and PTYs settle, seeding hydrated rows
* fix(sidebar): clear a stored tier only on proof its terminals died; a rest seed adds no clear
* chore(sidebar): merge origin/main; locality helper moved to shared
* feat(sidebar): sort Recent and Smart by host stamps behind the sidebar-inputs switch
With ORCA_SIDEBAR_INPUTS=main, the window, main and the phone order Recent by
lastActivityAt and Smart by smartTier then smartAt (Needs you, Done, Working,
then the rest by Recent). The window holds its displayed order while the
pointer is over the list or a drag runs. Switch off: today's sort path.
Co-Authored-By: Claude <noreply@anthropic.com>
* chore: restore files the merge commit's formatter rewrote
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(sidebar): stamp-order review fixes behind the sidebar-inputs switch
The window writes no Recent stamps with the switch on (terminal start and
exit, unread marks, comment edits), the hover hold covers only Recent and
Smart and ends with a drag, main follows only explicit metadata saves, and
the phone picks its sort from the unfiltered rows.
Co-Authored-By: Claude <noreply@anthropic.com>
* test(sidebar): folder writers and a Manual drag under the stamp switch
Co-Authored-By: Claude <noreply@anthropic.com>
* test(sidebar): name the stamp type in the ps summaries test
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* fix(mobile): put the session tabs in the top bar on a landscape phone
A sideways phone is about 390pt tall and the session header spent two rows
on it. On a landscape phone the tab strip now shares the top bar with the
back button, title and panel icons. Portrait and tablet layouts are unchanged.
* fix(mobile): give session tabs side borders and a filled active tab
In the landscape top bar the tabs sat between the title and the panel
icons with nothing marking where one tab ended. Each tab now has a 1pt
border on its sides, running the full height of the bar, and the active
tab gets the raised fill, as the desktop tab strip draws them. The same
strip is used in portrait, so both orientations match.
* fix(mobile): reset tab strip measurements on view attachment
* style(mobile): format tab strip ref dependencies
* fix(mobile): keep two header rows in landscape windows too narrow for top-bar tabs
A short, narrow window (a ~390x350 web window) is wider than tall, so it
counted as a landscape phone and moved the tabs into a top bar that has no
width left for them. Top-bar tabs now also need at least 568pt of width,
the narrowest landscape phone.
* fix(mobile): give the landscape top-bar tab strip a fixed left edge so scrolled tabs never read as part of the title
* fix(mobile): 12pt between the session title and the landscape tab strip
* 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.
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.
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>
* 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
* 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.
* 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>
* 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
* 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>
* 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
* 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
* 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
* 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>
* 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
* 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
* 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
* 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>
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>
* 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.
* 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.
* 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
* 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.
* 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
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.
* 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