Commit Graph
372 Commits
Author SHA1 Message Date
Neil 3830851a83 fix(mobile): unblock iOS releases and prepare 0.0.36 (#10888)
* fix(mobile): block iOS uploads below the last shipped App Store version

The closed-train guard looked up each candidate version's own App Store
record, but a version only gets one once it is submitted for review.
0.0.34 reached TestFlight and was never submitted, so it had no record,
nothing looked closed, and the patch-bump walk stopped there — while
0.0.35 had already shipped. Apple rejected the upload after a 24-minute
build (90186 closed train, 90062 needs a higher CFBundleShortVersionString).

Fetch the highest closed version once and treat everything at or below it
as closed, comparing semver numerically so 0.0.10 outranks 0.0.9.

Also read appVersionState alongside appStoreState: the latter is
deprecated in App Store Connect API 3.3 and renames the shipped state to
READY_FOR_DISTRIBUTION, so reading only the old field would silently find
zero closed versions once Apple stops populating it.

* chore(mobile): prepare 0.0.36

app.json sat at 0.0.32 while 0.0.35 shipped on the App Store, because
release versions are resolved on the runner and never committed back.
Close the four-version drift so the checked-in version matches reality
and the iOS release no longer depends on the closed-train walk to find
an open version.

Bump Android versionCode 8 -> 9 in the same commit: the version is shared
between platforms, and shipping 0.0.36 with the code that already shipped
for 0.0.32 produces an APK that cannot install over the released build.
2026-07-27 01:53:11 -07:00
JinjingandNeil 28b395ced2 fix(mobile): harden native chat send budgets, streams, and stop (#10814)
Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
2026-07-26 22:08:38 -07:00
Brennan Benson 8b25cfc0f8 fix(mobile): clear native-chat composer optimistically at send time (#10226)
* fix(mobile): clear native-chat composer optimistically at send time

Over relay the send RPC round trip is visible and a lost ack (or a
relay/direct cutover) could strand the sent prompt in the composer
forever: the unconfirmed-send deadline dropped its tracking entry, so a
late transcript echo could never clear the draft.

Clear the draft at send time and restore it only on a definite
rejection. holdUnconfirmedSend now only manages the delivery-unconfirmed
notice; it no longer touches drafts.

* fix(mobile): isolate question answers from composer drafts
2026-07-26 12:44:39 -07:00
6577b79e2e Add bulk tab closing to mobile long-press sheets (Close Others / Left / Right) and complete the desktop tab context menus (#9323)
* Add Close Tabs to the Left and complete Close Others across tab menus and mobile long-press sheets

* Fold the per-sheet Close action into the bulk-close module (session route max-lines)

* fix(mobile): preserve pinned tabs during bulk close

---------

Co-authored-by: Tom de Bres <tomdebres@users.noreply.github.com>
Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
2026-07-26 02:32:08 -07:00
ye4241 cd4064689d fix(mobile): pop to home when leaving a host so the back chevron animates backward (#9723) 2026-07-26 01:57:25 -07:00
Jinwoo HongandOrcaWin 07671d4a06 fix(mobile): recover unreliable relay connections (#10709)
* fix(mobile): recover unreliable relay connections

* test(mobile): use valid raster preview fixtures

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-07-26 00:48:06 -07:00
Brennan Benson 12fa5ff79e fix(mobile): heal an orphaned native-chat image paste across screen unmounts (#10480)
* fix(mobile): heal an orphaned native-chat image paste across screen unmounts

The stale-input marker lived in a per-screen `useRef`, but the condition it
tracks — a bracketed image paste sitting unsubmitted on the agent's composer
line — lives on the host and outlives the screen. Backing out of a session and
returning remounted the hook with an empty Set, so the next message submitted
on top of the orphaned paste and the agent received `<image path><text>`.

Move the marker to a module-level store keyed by terminal handle, and consult
and consume it from every write path that can submit the composer: the image
hook's text-only send, the controller send (which the chat overlay's question
card reaches directly, bypassing the image hook), and the ask-answer send.

Permission choices and the Escape cancel deliberately do NOT heal: they are
`enter: false` keys for an active overlay that swallows the clear, so healing
there would consume the marker without clearing the line and leave the next
real message corrupted. Desktop scopes its Ctrl+U the same way.

* fix(mobile): stop the ask heal from burning the marker on selector answers

The heal ran on every ask answer, but Claude's and Codex's selector shapes
cannot submit the composer: a single-select answer is a bare option digit and
every stepping group is written `enter: false` (the host coerces it), so the
clear is swallowed by the live overlay while the host still acks the write.
That consumed the one-shot marker and left the orphaned paste to corrupt the
next real message — the same failure this PR exists to fix, through a new door
that main did not have.

Scope the heal to the pasted-label shape, which does commit the composer.
Desktop splits it the same way: use-native-chat-interactive-send.ts routes only
the non-stepping answer through the clearing sender and never pre-clears
sendNativeChatAskAnswer.

Also pin the three deliberate skips (selector answer, permission choice, Escape
cancel) with tests, so the PR's central design argument is an invariant rather
than a comment, and guard the failed-heal toast with the generation check every
other error surface in answerAsk already uses.
2026-07-24 23:10:13 -07:00
Jinjing 49e32ff2b4 fix: prevent UI freeze from dual-modal race in action sheets (#10432)
Add closeBeforePress flag to Rename, Browser, and Refresh actions to
defer modal opening until the action sheet closes. Eliminates the race
condition that caused the mobile app to freeze when opening these modals.
2026-07-24 17:08:02 -07:00
kazu-42andBrennan Benson 4a71a0ecb2 feat(mobile): add safe Codex rate-limit resets (#9394)
* feat(mobile): add safe Codex rate-limit resets

* fix(mobile): address reset credit review feedback

* review: purge removed-account reset attempts, shared capability constant, rebase test mocks

* review: preserve host compatibility and reset durability

* fix(mobile): recover reset capability after cutover

* fix(mobile): validate runtime capability payloads

* fix(mobile): enforce capability payload contract

* fix(mobile): route mock terminals to selected worktree

* test(mobile): pin malformed probe retry behavior

---------

Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
2026-07-24 14:42:04 -07:00
Brennan Benson e651fe91c6 fix(mobile): heal terminal input after ambiguous image-send delivery (#10325)
An image send whose text+Enter RPC ended 'unknown' (ack loss / path
cutover) collapsed to accepted=true, so the terminal was never marked
stale. When the Enter truly never landed, the already-pasted image path
sat on the input line and glued onto the next plain-text message.

Propagate the send outcome through handleNativeChatSendWithOutcome and
mark the terminal input stale on any non-accepted outcome; the next send
heals with Ctrl+U (a no-op when the message did land). Chips still clear
on 'unknown' to avoid a double-send on retry.
2026-07-24 00:47:20 -07:00
Kaynan Sampaio de Camargoandkaynan 69d05b6e24 fix(mobile): resolve permission args when a new session launches an agent (#8469)
The New Workspace flow built a bare launch command client-side and sent it as
`startupCommand`, so the host ran it verbatim and never applied the default
launch args. The first Claude session therefore started in manual mode, while
opening another Claude via the "+" tab (which sends the agent id and lets the
host resolve args) started with `--dangerously-skip-permissions`.

Send `startupAgent` from every client-built create path (blank, reuse-branch,
and new-branch) so the host resolves the launch command, args, env, and
host-shell quoting through the same path the "+" new-tab and CLI use. The
work-item path already delegated via `startupDraft`. Custom `agentDefaultArgs`
are now honored on all paths.

Adds a shared `agentLaunchCreateFields` helper and removes the now-unused
client-side command map, which had also drifted from the canonical launch
commands for continue, hermes, command-code, kiro, and mistral-vibe.

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

Co-authored-by: kaynan <kaynan.camargo@terceiro-sky.com.br>
2026-07-24 00:29:19 -07:00
BingZ 2cf41ab864 fix(mobile): keep terminal caret visible without focus (#10101) 2026-07-23 23:55:35 -07:00
Mark Xian e3adb20917 fix(agents): include OMP terminals in cold session restoration (#8991)
Preserve OMP session identity and exact resume paths across cold restoration, AI Vault, mobile, WSL/SSH, and host-authority routes. Add mixed-version fallback and completed-session recovery coverage.
2026-07-23 19:05:22 -07:00
Neil a2b1185672 perf(mobile): trust healthy session tab streams (#10134) 2026-07-23 19:01:04 -07:00
NeilandOrca aab112933e Revert "fix(memory): bound OOM-prone accumulators (#10179)" (#10255)
Co-authored-by: Orca <help@stably.ai>
2026-07-23 18:35:31 -07:00
Brennan Benson 06f6e3bed2 fix(mobile): make image send retries safe (#10228) 2026-07-23 16:04:05 -07:00
OrcaWinandOrcaWin 801ff57e83 fix(mobile): unblock iOS releases (#10224)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-07-23 14:23:44 -07:00
NeilandJinjing 9500ca7a65 fix(mobile): show attached images in native (rich) chat (#10135)
* fix(mobile): show attached images in native (rich) chat

Attaching an image in the mobile native chat did nothing visible — it reused
the terminal attach flow, which pastes a bracketed host path into the hidden
terminal, so there was no composer preview and nothing in the transcript.

Give native chat the desktop model instead:
- pick + upload shows a removable thumbnail chip in the composer (no early paste)
- on submit, images ride along: Ctrl+U clear -> bracketed paste(s) -> settle ->
  text + Enter (idempotent on retry)
- the optimistic echo carries the local preview URIs and the message renderer
  draws image-ref blocks as real thumbnails when the URI is loadable, so the
  sent photo appears in the conversation immediately
- image-only echoes reconcile by ordinal against user turns after their tail
  (ignores agent replies / paginated history / the 'unknown' ack-loss path)

Terminal chat attach is unchanged (both flows consolidated behind
useMobileSessionImageAttachments). Adds unit coverage for pick+upload, the
ride-along byte order, chip render/remove, and echo reconciliation.

* test(mobile): interactive native-chat image proof (real hooks, click-driven)

Replace the hand-fed component render with an interactive harness that mounts
the real MobileNativeChatComposer/Message + useMobileNativeChatImageAttachments +
drafts under react-native-web and drives the actual flow via clicks. Only the two
OS boundaries are faked: the photo picker and the paired-host RPC socket.

Screenshots (mobile/docs/native-chat-image-attachment/) are produced by real
clicks, not props:
- attach -> real upload pipeline -> chip appears, nothing pasted yet
- send -> real ride-along emits Ctrl+U clear, bracketed image paste, text+Enter
  (shown in the live byte trace) and the sent bubble renders the photo thumbnail

* fix(mobile): scope native-chat image attachments by active tab

Images are now scoped to the tab that initiated the pick, so switching tabs
during upload cannot ride an image into another terminal. Chips stay with
their original tab, and only the active scope's images send with text.
Improved error handling with user-facing toast messages for disconnection
and send failures.

* test(mobile): add image attachment tab-scoping and error tests

Add comprehensive test coverage for tab-scoped attachment behavior,
error handling when transport fails or lease is gated, and edge cases
like attaching images during an in-flight send. Extract baseArgs and
update helpers to reduce boilerplate across test cases.

* fix(mobile): show attached images in native rich chat

Images attached in the mobile native (rich) chat now display as:
- Removable composer chips while composing
- Thumbnails in the sent user bubble after sending (desktop parity)

Implements proper image echo reconciliation by distinguishing
image-source marker turns from text echoes, so an image send isn't
cleared by an unrelated text echo. Adds scope isolation to prevent
chips and drafts from leaking between tabs, and detects tab switches
during the image-paste settle window to abort the send.

Fixes Android tap-target positioning for the image removal badge and
clears stale terminal input after failed pastes to avoid gluing
fragments onto the next message.

* rm stubs

---------

Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
2026-07-23 13:25:50 -07:00
Jinjing 5dc6799c48 Rename 'local network' to 'LAN' across the UI (#10216)
Improves clarity and consistency throughout mobile pairing, settings,
and permission descriptions. Makes the distinction from Tailscale
more explicit where relevant. Updates all translated locales.
2026-07-23 12:48:26 -07:00
Jinjing 46683f0f55 fix(mobile): keep name input continuous during source picker transition (#10145)
* fix(mobile): keep name input continuous during source picker transition

Refactor create-workspace flow to maintain input continuity: single TextInput
morphs from form slot to docked position above the keyboard while results
reflow above it. Prevents field unmount/remount and keeps user focus on the
input as it transitions. Add keyboard inset resolution utilities and fill-mode
bottom drawer support for stable frame heights during result reflowing.

* fix(mobile): keep name input continuous during source picker transition

Extract drawer navigation into a custom hook and add `interactive` prop to
BottomDrawer to pin the form sheet under the source picker. The form stays
visible and laid out but non-interactive during source selection, revealing
its original height when the picker dismisses. Fix bottom-drawer height
calculation to never exceed the space above the keyboard.

* fix(mobile): reset drawer state when create-workspace modal closes

Prevent a queued transition timer from landing after the modal closes and
leaving stale drawer/pin state for the next open.

* fix(mobile): disable source field focus during drawer transitions

Prevent the workspace-name field from reopening the source picker when
the drawer closes. The drawer's dismiss restores focus to the field,
which re-fires onFocus and reopens the drawer. Gate the field's
focusability with an interactive prop so it only accepts focus when
the form sheet is active.
2026-07-23 12:09:30 -07:00
Neil 8f40ddf328 fix(memory): bound OOM-prone accumulators (#10179) 2026-07-23 06:22:56 -07:00
1648251fb8 fix(terminal): restore a dark-background contrast floor (#10108)
* fix(terminal): restore a dark-background contrast floor

Fully disabling xterm minimumContrastRatio on dark backgrounds (#9599)
left near-background body text unreadable — Antigravity paints #262b30
on #1e242a (~1.1:1). Keep light backgrounds at WCAG-AA 4.5 and use a
milder dark floor (3) so dark-on-dark body text is lifted without the
full light-bg correction strength.

Fixes #10104

* fix(terminal): extend dark-bg contrast floor to preview + mobile terminals

The dark-background minimumContrastRatio floor (#10104) is applied per
`new Terminal()` construction site. Beyond the live pane, agent output also
renders in the dashboard popout preview and the mobile WebView, which were
still at the floor-1 default, so Antigravity output stayed unreadable there.

- AgentTerminalPreview: gate via resolveTerminalMinimumContrastRatio
- mobile WebView: port the gate as resolveTerminalContrastFloor (Chrome-74 JS)
- tests: builtin-catalog guard + mobile vm-harness coverage

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

---------

Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
Co-authored-by: Orca <help@stably.ai>
2026-07-23 01:04:45 -07:00
Jinjing 23fc1ea59a fix(mobile): bind markdown creation to file owner (#10083) 2026-07-22 22:14:30 -07:00
Brennan Benson 4fce2de494 fix(mobile): keep native chat from resizing the covered terminal PTY (#9988)
* fix(mobile): keep native chat from resizing the covered terminal PTY

Native chat reads the agent transcript stream and never renders the
terminal grid, but two paths still pushed phone dimensions into the
covered PTY, reflowing the desktop terminal for no benefit:

- The covered lease-only subscribe carried the cached viewport, and
  handleMobileSubscribe phone-fits the PTY whenever a viewport is
  present. The lease now omits the viewport so the host keeps the
  desktop baseline and late-binds on return to the terminal tab.
- useTerminalViewportRefit measured the still-mounted WebView under
  the chat overlay and sent terminal.updateViewport on rotation,
  keyboard, text-scale, reconnect, and iOS-resume triggers. Refits
  are now suppressed while native chat covers the active terminal;
  the triggers already mark the viewport stale, and the
  return-to-terminal resubscribe re-measures.

* fix(mobile): harden native-chat resize suppression
2026-07-22 19:36:22 -07:00
JinjingandOrca 3708c4f6ce fix(mobile): report interrupted native chat sends as delivery-unknown, not failed (#10021)
* fix(mobile): report interrupted native chat sends as delivery-unknown, not failed

A terminal.send interrupted mid-flight showed a definite "Message not sent"
even when the desktop may have already delivered the text. Three paths were
misclassified as definite failures:

- Logical relay/direct cutover: migrateTo rejects in-flight requests with
  LogicalClientCutoverError, which mapped to 'rejected'. Now maps to 'unknown'
  (held unconfirmed + transcript-echo verification; never retried since
  terminal.send is non-idempotent).
- Suspend/close of a half-open session: the stable logical client blanket-
  rejected in-flight pendings with plain 'Client suspended'/'Client closed',
  preempting the physical layer's delivery-unknown marking. It now lets the
  physical close settle them, so post-write failures stay marked and pre-write
  failures stay definite.
- Relay path: mobile-relay-rpc-session never marked delivery ambiguity at all
  (timeout, close, link failure). Post-write rejections are now marked;
  pending entries only exist after the frame reached the authenticated link.

Permission, ask-answer, and cancel-Escape surfaces now show "unconfirmed —
check chat before retrying" instead of a definite "not sent" on ambiguous
outcomes (still not-accepted, never retried). Also consolidates a private
copy of isLogicalClientCutoverError in worktree-create-retry.

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

* chore(skills): regenerate skill-bundle manifest artifacts

---------

Co-authored-by: Orca <help@stably.ai>
2026-07-22 19:28:21 -07:00
Jinjing 6d55c7fa16 rename: rebrand user-facing Native chat to Chat UI (#10036)
Update desktop experimental settings, mobile settings/onboarding, i18n
(en/zh/ja/ko/es), and user-visible error strings. Keep internal APIs and
identifiers as nativeChat.
2026-07-22 19:14:44 -07:00
NeilandOrca 01bcc57ff6 perf(mobile): gate dictation setup progress polling on foreground + single-flight (#9892)
* fix(mobile): gate dictation setup polling

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

* fix(mobile): fence a stale dictation refresh against a newer setPolling intent

An in-flight setup read resolving 'keep polling' after an explicit setPolling(false)
wrote polling=true and rescheduled, resurrecting a poll the caller had just stopped.
Snapshot a pollingRevision when each read starts and only apply its result if no
explicit setPolling superseded it mid-flight — so a late true can't restart a stopped
poll (nor a late false cancel a restart).

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

---------

Co-authored-by: Orca <help@stably.ai>
2026-07-22 16:22:01 -07:00
NeilandOrca 6a43f9935d perf(mobile): coalesce duplicate concurrent home-screen requests (#9888)
* perf(mobile): coalesce overlapping home requests

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

* fix(mobile): queue a trailing follow-up for triggers during an in-flight read

Single-flight returned the in-flight promise to any trigger that arrived mid-read,
so a distinct refresh requested while a slow read was on the wire was silently
answered by the older response and never re-read the latest state (UI could stay
one refresh cycle stale). Coalesce mid-flight triggers into exactly one trailing
follow-up (latest params win) whose fresh result is delivered to those callers.

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

---------

Co-authored-by: Orca <help@stably.ai>
2026-07-22 16:21:58 -07:00
Neil 1d2cd33c83 fix(deps): resolve Dependabot security alerts (#10006) 2026-07-22 16:07:51 -07:00
Brennan Benson 11310eef63 fix(mobile): keep quick-commands button steady while capabilities load (#9979)
* fix(mobile): keep quick-commands button steady while capabilities load

The tab-row quick-commands button only rendered once the capability probe
resolved true, so it popped in after the row was already visible (and
vanished during reconnect re-probes). Render it whenever support is not
confirmed absent and disable it until the probe settles — pre-quick-commands
hosts strip agentPrompt, so the action (not the button) must wait for
confirmation. Confirmed-unsupported hosts still hide it entirely.

* fix(mobile): explain unsupported quick commands on tap instead of hiding

Per feedback on the disabled/hidden states: the button now always renders
and stays tappable. Tapping against a desktop that confirmed no support
shows "Desktop update required for quick commands" (mirroring the browser
streaming copy); tapping while the capability probe is still resolving says
to try again in a moment. The sheet still opens only once support is
confirmed, since pre-quick-commands hosts strip agentPrompt.

* docs(pr): add QA screenshots for quick-commands button states

* test(mobile): lock quick-commands button stability

Add a focused source-contract test for the always-mounted tab action and confirmed-support sheet gate. Keep the non-obvious safety comment concise, and remove PR screenshots now hosted as GitHub user attachments.

* test(mobile): structurally guard quick-command action mount
2026-07-22 14:18:59 -07:00
NeilandOrca c6d280348a perf(mobile): memoize worktree list rows (#9889)
Co-authored-by: Orca <help@stably.ai>
2026-07-22 14:13:06 -07:00
NeilandOrca a3d6f84286 fix(mobile): pause relative-time clocks when hidden (#9886)
Co-authored-by: Orca <help@stably.ai>
2026-07-22 14:13:02 -07:00
NeilandOrca 76f5b8318c fix(mobile): pause session polling in background (#9875)
Co-authored-by: Orca <help@stably.ai>
2026-07-22 14:12:59 -07:00
NeilandOrca 4468d54f3c perf(mobile): gate host polling on foreground/background (#9857)
* perf(mobile): gate host polling on foreground

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

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

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

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

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

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

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

---------

Co-authored-by: Orca <help@stably.ai>
2026-07-22 14:12:55 -07:00
Brennan Benson d6c9fcd537 feat(mobile): surface pairing-auth failures on desktop and mobile (#9782)
* feat(mobile-pairing): surface unpaired-device auth failures instead of silent 4001 loops

Desktop: when a phone repeatedly fails direct-transport E2EE auth with a
token missing from the device registry (pre-v1.4.106 pairing-path bug left
desktops that regenerated their registry rejecting paired phones forever),
throttle to one notification per session and show an actionable toast
pointing at Settings -> Mobile to re-pair.

Mobile: map a bare 4001 close onto the existing auth retry budget (the
encrypted e2ee_error is undecryptable when the desktop keypair changed, so
the close code is the only surviving signal) instead of looping the generic
reconnect forever, and make the auth-failed verdict say 'Pairing invalid -
re-pair with your desktop' instead of a bare 'Auth failed'.

* fix(mobile-pairing): handle stale keys and startup notification races

* fix(mobile-pairing): isolate auth notification failures

* fix(mobile-pairing): keep recovery alert actionable
2026-07-22 14:05:37 -07:00
Brennan Benson 405b9f245a feat(mobile): mount ProtocolBlockScreen when protocol compat is blocked (#9780)
* feat(mobile): mount ProtocolBlockScreen when protocol compat is blocked

ProtocolBlockScreen existed since PR #1440 but was never mounted: on a
'blocked' compat verdict the only output was a console.warn, so a future
MIN_COMPATIBLE_RUNTIME_CLIENT_VERSION bump would have silently shown a
broken host UI instead of the update screen.

Add HostProtocolGate — a choke point in app/h/_layout.tsx above every
/h/[hostId] route — that consumes useHostStatusGates and replaces the
blocked host's entire UI (sidebar + detail stack) with ProtocolBlockScreen.
The host list and other hosts stay usable; the screen's own 'Back to
hosts' escape hatch routes to '/'. Both block reasons render their
respective CTAs (mobile-too-old → App Store, desktop-too-old → GitHub
Releases). Compat logic stays in the src/shared mirror contract — no fork.

* fix(mobile): fence incompatible host routes efficiently

* fix(mobile): route Android updates to releases
2026-07-22 13:21:25 -07:00
Brennan Benson 0121f571e4 fix(agent-status): map codex request_user_input questions to Needs You (#9861)
* fix(agent-status): map codex request_user_input questions to waiting

Codex 0.145 asks user questions via the auto-allowed request_user_input
tool (experimental default_mode_request_user_input): PreToolUse fires
while blocked on the answer with no Stop, so Orca showed the pane as
working/idle instead of Needs You. Map that PreToolUse to waiting
(mirrors grok's ask_user_question), exempt question waits from the codex
yolo auto-approval suppressor, and deliver native-chat answers to the
digit-commit selector by option number (typed labels are ignored and
Enter commits the highlighted first option). Older codex versions emit
no such event and are unchanged.

* fix(native-chat): preserve codex question answer semantics
2026-07-22 12:00:14 -07:00
Brennan Benson dfbc2e8ba7 fix(mobile-quick-commands): replay sheet load killed by connection migration (#9798)
Opening the Quick Commands sheet right after connecting over relay races
the relay->direct cutover, which rejects the in-flight one-shot
settings.getTerminalQuickCommands with LogicalClientCutoverError while
connState stays 'connected'. The sheet then strands on "RPC interrupted
by connection migration" with an empty list until closed and reopened.

The read is side-effect-free, so replay it on cutover (capped at 5,
cancelled if the sheet closes or the client is replaced). Same failure
class and pattern as #9794 (capability probe) and #9796 (terminal
create).
2026-07-22 11:45:59 -07:00
OrcaWin f9f3cd2fbe fix(terminal): prevent reconnect from killing live daemon sessions (#9804) 2026-07-21 19:20:16 -07:00
Brennan Benson ac909c8d83 fix(mobile): stop reporting delivered chat messages as "Message not sent" (#9792)
* fix(mobile): stop reporting delivered chat messages as "Message not sent"

A relay drop or response timeout while terminal.send is in flight rejects
the RPC even though the request usually already reached the desktop — only
the ack was lost. The chat composer treated every failure as definite,
showing "Message not sent" and keeping the draft for a message that is
visibly in the transcript after resync (and baiting a duplicate send).

Mark transport failures that happen after the request frame hit the wire
as delivery-unknown, and hold those sends instead of erroring: when the
transcript echo lands the draft clears silently; only if no echo arrives
within 20s is the failure surfaced. Failures before the frame was written
(and host rejections) still error immediately.

* fix(mobile): close delivery ambiguity races

* fix(mobile): harden ambiguous send reconciliation

* test(mobile): assert ambiguity deadline boundary
2026-07-21 18:39:26 -07:00
Brennan Benson 261a7b714c fix(mobile-relay): back off relay reconnects to stop cellular connect/disconnect churn (#9460)
* fix(mobile-relay): back off relay reconnects to stop cellular connect/disconnect churn

On cellular, the relay path re-dialed instantly on every network flap: a
NAT rebind / Wi-Fi<->cellular handoff silently kills the socket, the
revival trigger treats it as 'link came back' and calls recoverRelay(),
and the relay cell answers the overlapping resume with PEER_DROPPED (4408)
or LIMIT_EXCEEDED (4429). The session collapsed every close to a plain
'disconnected' and re-dialed with no delay, so the phone ping-ponged
connect/disconnect. The documented recovery contract (mobileRelayRecoveryFor,
which prescribes fullJitter backoff) had no callers.

- Add RelayReconnectBackoff: full-jitter exponential backoff (250ms floor,
  30s ceiling) that debounces re-dials via a cooldown window and wires up
  mobileRelayRecoveryFor. Reset on a successful migrate and on a genuine
  background->foreground transition (not on repeat foreground nudges).
- Extract the lease-rotation timer into RelayLeaseRotationTimer so the
  supervisor stays under max-lines (the direct-probe path can't be split
  out — it shares the operationInFlight mutex with recoverRelay).
- Add a deterministic test: repeated network-flap nudges re-dial instantly
  before the fix and are suppressed by the backoff window after.

* fix(mobile-relay): recover drops during direct probes

* fix(mobile-relay): recover half-open relay sessions

* fix(mobile-relay): preserve direct handshakes

* fix(mobile-relay): keep recovery retries bounded

* fix(mobile-relay): avoid redundant recovery dials

* fix(mobile-relay): keep all retries inside cooldown

* fix(mobile-relay): close recovery lifecycle races

* fix(mobile-relay): preserve in-progress direct auth

* fix(mobile-relay): preserve fatal recovery gates

* fix(mobile-relay): preserve backoff across unstable resumes

* fix(mobile-relay): close remaining recovery lifecycle gaps

* fix(mobile-relay): reset backoff only after stable relay
2026-07-21 18:28:16 -07:00
Brennan Benson 6e6b7d8195 fix(mobile): retry session capability probe so tab-row actions survive relay cutover (#9794)
* fix(mobile): retry session capability probe so tab-row actions survive relay cutover

The session screen learned host capabilities (quick commands, browser
screencast, agent history, query-reply input) from a single status.get
fired when the screen connected. Over relay, a relay-to-direct transport
cutover rejects every in-flight request while connState stays
'connected', and a request timeout does the same — so one transient
failure latched the capability flags false (or left them null on an
ok:false reply) and the quick-commands tab-row button stayed hidden
until the screen was remounted.

Replace the one-shot probe with startRuntimeCapabilityProbe: retry
promptly after a cutover (the replacement transport is already
authenticated) and with capped exponential backoff on other failures,
until a probe lands or the effect is cleaned up. Also export the
cutover-error predicate from stable-logical-rpc-client and reuse it in
worktree-create-capability instead of a local copy.

* fix(mobile): reset runtime gates before capability reprobe
2026-07-21 17:55:02 -07:00
OrcaWinandOrcaWin 15362fde16 fix(web): preserve paired runtime ownership (#9776)
* fix(web): preserve paired runtime ownership

* fix(web): validate runtime port scan payloads

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-07-21 17:48:59 -04:00
OrcaWin 05c32c4757 fix(runtime): isolate navigation across paired clients (#9664) 2026-07-20 21:36:15 -07:00
OrcaWin 1293e1c0d8 Fix mixed-version mobile Codex session resume (#9678) 2026-07-20 21:16:08 -07:00
OrcaWin c540ab6d8b fix(mobile): restore notification opt-in route compatibility (#9675) 2026-07-20 21:15:39 -07:00
Brennan Benson e3c8d96638 Access the Floating Workspace from mobile (#8405) (#9523)
* Access the Floating Workspace from mobile (#8405)

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

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

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

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

* Harden mobile Floating Workspace routing

* Fix mobile host gate reuse race

* Harden floating mobile session polling

* fix(mobile): harden floating workspace route reuse

* fix(mobile): skip floating workspace repo lookup

* fix(mobile): clarify floating workspace header action
2026-07-20 21:05:40 -07:00
971b167548 fix(github): load PR diffs for Enterprise remotes (#8932)
* fix(github): load PR diffs for Enterprise remotes

* fix(github): encode PR content paths by segment

* Fix PR review actions failing on GitHub Enterprise remotes

- Threads GitHub host identity (not just owner/repo) through the client,
  work-item-details, issues, and RPC layers so gh commands target the
  correct Enterprise server instead of silently falling back to github.com
- Adds a shared github-api-repository helper to resolve/host-qualify repo
  identity consistently across REST, GraphQL, and CLI shorthand calls
- Scopes the gh rate-limit breaker and singleton rate-limit snapshot by
  host/runtime so a github.com block or probe can't affect GHES or WSL
- Coalesces concurrent host-auth probes and paginates PR file fetching
  beyond 100 results
- Propagates `host` through renderer PR caches, checks-panel keys, and
  preload IPC types so Enterprise and github.com data never collide

* Route gh host qualification through runner options instead of argv sniff

Move GHES/GH_HOST resolution from parsing --hostname/--repo out of gh argv to an explicit options.host passed through ghExecFileAsync, since SSH-backed repos spawn gh with no cwd and argv sniffing couldn't reliably detect the target host. The runner now injects --hostname and qualifies --repo/-R at spawn time from options.host, and rate-limit scoping/guards use the same explicit host instead of inferring it. Also adds a shared githubRepoIdentityKey helper to keep cache/store keys consistent with the new host-aware repository identity.

* Fix gh CLI GHES host pinning and rate-limit scope leaks

- Pin `--host` on every gh call site so a process-level GH_HOST can't
  silently redirect requests, and qualify `-R`/`-R=` repo shorthand
  alongside the existing `--repo=` handling.
- Check the target scope for an active rate-limit block before each
  WSL/native or host fallback retry, not just on the initial attempt,
  so a blocked scope can't be hit again through a fallback path.
- Compute idempotency once per call instead of re-deriving it after
  fallback reassigns args.

* Fix GitHub Enterprise host identity loss across PR/work-item paths

- Thread `host` through mobile PR RPC params, IPC work-item lookups, and
  RPC schemas so GHES identity survives the renderer/mobile/main boundary
  instead of silently falling back to a same-named github.com repo.
- Qualify `--repo`/`-R` args for github.com too (not just GHES), since
  gh resolves bare shorthand against a process-level GH_HOST that can
  redirect pinned github.com commands.
- Cache `getOriginGitHubApiRepository` to avoid a per-call uncached
  `git remote get-url` round trip on connection-backed repos.
- Add a local-fork fallback in `getWorkItemDetails` so PRs living on a
  base repo (not visible via the origin slug) still resolve via cwd.
- Centralize the github.com-vs-GHES host predicate in
  `isDefaultGitHubHost` so cache keys, quota scoping, and identity
  checks can't drift out of sync.

* Make repository identity host-aware across all GitHub surfaces

Generalize the auth-gated enterprise resolver to any remote and build a
cached hosted-identity family (origin/issue/candidates/source) on top of
it, then migrate every github.com-only consumer: Tasks listing/counting,
branch-to-PR discovery, push targets, fork upstream, issue operations,
Projects, web links, avatars, and PR-link facts. Scope the rate-limit
breaker probe per runtime:host and classify WSL UNC cwds correctly.

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

* Fix expected slug to include host field in GitHub PR link test

Updates the smart-source paste-intent test fixture to match the
repository slug shape that now carries a `host` field, keeping GHES
host identity intact through the paste-intent parsing path.

* Surface per-host gh auth state for GitHub Enterprise

diagnoseGhAuth accepts the host a surface needs credentials for, scopes
the account/scope diagnosis to that host, and reports whether gh has any
login there; GhAuthErrorHelp renders host-qualified login/refresh
commands so an unauthenticated GHES host stops masquerading as a
github.com scope problem. Also fixes the mobile paste-intent expectation
for host-carrying parsed links.

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

* Bound GHES identity caches and preserve non-default ports in host identity

Cap the origin-repo and host-auth caches like ownerRepoCache; keep ports
from remote/link URLs so GHES on a non-default port is a distinct
identity; make positional github.com slugs explicit against GH_HOST;
compare work-item sources by host-aware identity key; bail cwd-less
branch lookups when no repository candidate resolved; thread host
through the renderer work-item slug lookup.

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

* Thread GitHub host through issue detail requests

Incorporates ghes-issue-host-support (ed6bb96ef): one hosted issue
repository identity is resolved before the details fan-out so comments,
timeline, participants, and mention lookups cannot drift across hosts,
with SSH guards so unresolved issue/PR repositories never fall through
to gh's default host.

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

* Scope remaining GitHub rate-limit accounting

* Resolve typed PR lookups across hosted repository candidates

getWorkItem's PR path probes upstream-then-origin hosted candidates
instead of origin alone, so fork checkouts resolve the base repo's PR
with the right host; issue detail resolution reuses the up-front hosted
identity and keeps the SSH unresolved-host guards.

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

* Refactor GitHub repository execution setup

* Carry host on smart-submit link intents

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

* Carry the project host on GitHub item dialog origins

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

* Keep GHES web ports but drop SSH transport ports in host identity

Supersedes PR #9118 on this branch: http(s) remote ports identify the
Enterprise web/API endpoint and are preserved, while ssh/git transport
ports (including ssh.github.com:443) never leak into gh's host identity.
Replaces the ssh.github.com:443 special case with the structural
protocol split and ports the PR's parsing test suite.

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

* Support GitHub Enterprise diffs and mutations with host-scoped caches

Parse GitHub host identity from work-item URLs and carry it through PR/issue mutations, labels, and assignments. Bound rate-limit and scope-probe caches (1024 and 512 entries) to prevent unbounded growth when interacting with multiple GHES instances. Normalize repository identity keys to include host so github.com and GHES slugs don't collide in cache and equality checks.

* Support GitHub Enterprise diffs and mutations with host-scoped caches

- Carry host identity through PR mutations and reads so fork PRs on
  different GHES instances don't collide in cache or state tracking.
- Validate host authentication before routing requests to unconfigured
  Enterprise servers; ambient credentials must never reach untrusted hosts.
- Scope rate-limit guards and spend tracking per host so GHES quota stays
  independent from github.com quota.
- Respect explicit --hostname arguments in gh CLI calls ahead of GH_HOST or
  ambient defaults, so breaker state follows the actual request target.
- Detect implicit WSL runtimes from UNC paths for consistent host auth and
  execution-options scoping across mobile and desktop clients.

* Support GitHub Enterprise work-item diffs with host-scoped execution

Enterprise PRs must use their selected host consistently across diff, comments,
and file-content loads. Validate repository slugs before authenticated execution
to prevent path-injection via renderer overrides. Scope project browsing cache
and rate-limit tracking by host to prevent cross-host pollution. Use parsed
URLs as authoritative over ambient hosts for project resolution.

* Support GitHub Enterprise work-item diffs with host-scoped execution

Preserve host identity on PR/issue work items throughout the mutation and diff
pipeline so Enterprise instances (including ported endpoints like
github.acme.test:8443) can execute mutations without ambiguity. Rate-limit gh
commands by the pre-qualified --repo host, cache auth state per ported host,
and surface Enterprise hosts in project metadata and error messages.

* fix(review): drop dead rateLimitGuard/noteRateLimitSpend re-export

Both callers (project-view.ts, mutations.ts) moved to the host-scoped
repositoryRateLimitGuard/noteRepositoryRateLimitSpend; the bucket-only
re-export in internals.ts had zero importers left.

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

* fix(ci): split Enterprise host work-item tests under max-lines

Move GHES/SSH host-routing cases out of work-item-details.test.ts so
the suite stays within the 800-line test max-lines budget.

* test(github): align mocks with host-scoped repository resolution

- Route origin repository resolution through getOwnerRepoForRemote, not getOwnerRepo, to match production path
- Pin github.com host on origin results so host-less fixtures pass host gate in resolveGitHubApiRepository
- Add generation-based invalidation to prevent stale slug-cache writes from in-flight resolutions
- Fix ref-sync race in ProjectPicker: use useLayoutEffect so committed tree owns browse cache key
- Defer handledCrossRepoUrlRef assignment in SmartWorkspaceNameField until resolution succeeds
- Update Enterprise host routing: found work items must not silently fall back to default host when unresolved
- Normalize GHES avatar URLs: accept explicit port 443 as canonical form, not a fallback trigger

---------

Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
Co-authored-by: Orca <help@stably.ai>
2026-07-20 18:55:45 -07:00
Brennan Benson 0ed1d04b3b fix(codex): migrate legacy shared-home sessions before resume (#9624)
* fix(codex): migrate legacy shared-home resumes

* fix(mobile): allow legacy Codex resume preparation
2026-07-20 17:10:07 -07:00
Brennan Benson 658532a1b0 Stop the mobile app from running hot during terminal streaming (#9489)
A busy PTY delivers up to ~200 terminal frames/s to the phone (the desktop
coalesces output at a 5ms window), and each frame paid a full RN-bridge +
WebView postMessage + WebKit IPC + xterm write + paint pipeline. Coalesce
stream writes in the RN layer: leading-edge immediate delivery keeps
keystroke echo instant, and sustained streams batch into at most ~21
WebView messages/s (48ms trailing window).

Measured (iOS Simulator A/B at ~200 lines/s, only this file flipped):
terminal WebContent CPU 8.0% -> 2.8%, app process 19.3% -> 15.2%,
combined continuous CPU -34%.

Ordering boundaries preserve today's semantics: resize/reflow flush
pending bytes first; init/clear drop superseded pre-snapshot bytes;
reload/content-process-termination/unmount clear the buffer. The
notification-dispatch extraction from TerminalWebView is a verbatim move
forced by the max-lines cap.
2026-07-20 14:45:58 -07:00