* Add a mobile native-chat opt-in so users pick terminal vs chat once
Mirror the notifications one-time opt-in for the native-chat default view.
After pairing, a full-screen modal (modeled on notification-opt-in) lets the
user choose whether supported agent sessions open in the terminal or in native
chat, then persists the choice to the existing orca:defaultSessionView key.
- Expose readDefaultSessionViewPreference() (tri-state; absent key = undecided)
so the gate can prompt exactly once; loadDefaultSessionView() is unchanged.
- shouldPresentSessionViewOptIn() gates the screen; the home focus effect shows
it after the notification opt-in.
- Settings -> Native chat toggle (already shipped) remains the recovery path.
* fix(mobile): preserve onboarding flow after pairing
* refine mobile session view opt-in copy
* Unify mobile onboarding prompts
* feat(native-chat): add mobile default-view toggle for native chat
Adds a per-device "Native chat" setting on mobile (Settings → Native chat)
that controls whether supported agent sessions open in the native chat view
or the raw terminal. Default stays terminal; flipping it on makes eligible
sessions render as native chat, mirroring the desktop default-view control.
- New orca:defaultSessionView preference (loadDefaultSessionView/save).
- Per-tab chat set refactored into a tri-state override map so a session can
be pinned to terminal or chat regardless of the default; the legacy array
format migrates to chat overrides on load.
- useMobileSessionViewMode resolves each tab as override ?? default and
reloads the default on focus so a Settings change applies without remount.
- Toggle/action-sheet builders take an isTabChatView predicate.
* chore(skills): refresh skill bundle manifest for v1.4.144-rc.2
The v1.4.144-rc.2 release bump left resources/skills/current-manifest.json
pointing at rc.1, so verify:skill-bundle-manifest fails on any branch that
reaches it. Regenerated (appVersion only; no skill content changed) to
unblock CI. Unrelated to the native-chat toggle in this PR.
* fix(native-chat): harden mobile view preferences
* fix(native-chat): harden default view persistence
* fix(native-chat): serialize mobile view persistence
* fix(native-chat): reconcile failed mobile view saves
* fix(native-chat): fail closed on unreadable view overrides
* fix(native-chat): honor fail-closed view races
* feat(native-chat): add native chat view across mobile
* fix(native-chat): address review findings and CodeRabbit threads
Correctness:
- Restore an independent initial readSession seed and surface initial-drain
errors as snapshot frames so the chat view can never strand on 'loading'
- Pair mobile tool results to calls by ordinal FIFO (parallel calls no longer
misgraft results); clear a pending ask only when its own call resolves
- Show a new streaming reply immediately (same-turn suppression, not length)
- Delegate mobile noise filtering to the shared harness-injected classifier
- Admit soft-leaving mobile clients in beginMobileInputFloor (parity with
mobileTookFloor) so grace-window writes aren't dropped
- Self-heal a stale 'working' status once this turn's reply lands
- Catch RPC rejections in mobile file-open helpers; guard sanitizeToolInput
key collisions; settle web/runtime transports on unrecognized first frames
and forward snapshot errors
Perf:
- Throttle the mobile streaming bubble (50ms) so per-part status frames stop
re-parsing the whole accumulated markdown
- Short-circuit markdown path detection on dot-less or oversized runs
(quadratic backtracking guard)
UX/minor:
- Wire hold-mode dictation through the native chat composer
- Allow scoped-package (@) paths in file-path detection
- Move caret after mid-text autocomplete insertion; index-prefixed ask option
keys; single scroll-to-end effect; bounded wait + toast when image attach
races a resubscribe; count-based pending reconciliation; cache-hit search
cancels stale debounce; chat-tab toggle wins over in-flight preference load
- Share shouldStepNativeChatAskAnswer between desktop and mobile; import
block guards/source priority from shared instead of local copies
- Defensive non-positive transcript limits; test strengthening (TTL expiry,
post-unsubscribe stale frame, lease readiness, filtered console.error)
* refactor(native-chat): share desktop/mobile chat logic in src/shared
Extract the parity-mirrored native-chat modules into shared implementations
both surfaces re-export: ask parsing (registry, parseAskFromStatus,
extractPendingAsk, formatAskAnswer), answer stepping offsets/scheduler, diff
detection/parsing, harness-noise filtering, tool fold/pair/split, and tool
summaries. Removes the hand-synced copies and their stale Metro comments.
Divergence reconciliations take the safer side of each: diffs truncate at
120 lines/32KB everywhere (desktop previously unbounded), tool-run summaries
cap at 3 parts with bounded-depth previews, nameless tool calls are skipped,
and basenames split on both separators.
Also: settle and kill every sibling quick-open pass when one reaches
maxResults (main rg/git and relay git; relay rg already did) so a capped
search cannot leave a scan walking a huge tree; fold window-bounding into
the shared merger's applyAppend; localize the web 'Pair a host' snapshot
error.
* fix(native-chat): address CodeRabbit follow-ups on shared modules
- Attachment lease gate re-checks connection/target/tab after the bounded
wait, so a tab/host switch or disconnect mid-wait can't send into a stale
terminal; a moved-away target drops silently like the pre-wait guard and
only an unrecovered lease surfaces the toast. Adds hook tests.
- extractPendingAsk parses transcript tool-calls through the same
registered-parser + canonical-shape fallback as live status, so a custom
question tool that rendered live survives reconnect/replay.
- Direct unit tests for the shared ask parser (FIFO ordering, fallback,
malformed payloads) and tool-summary bounded preview (depth/collection
caps, circular refs, basename/command branches).
* fix(native-chat): treat initialLimit 0 as a valid empty window
Both engine guards used truthiness, so an explicit zero limit skipped the
bounded tail reader and fell back to an unbounded incremental read. Latent
only (every caller clamps positive), hardened for consistency with the
tail reader's non-positive-limit handling.
* fix(mobile): native-chat composer lock UX + send-failure feedback
- Distinguish input-lock reasons: transport 'disconnected' shows Reconnecting…
instead of mislabeling a reconnect as locked-by-another-client
- Guard the composer lock behind a 600ms hold so connState blips / lease
hand-offs don't flicker the placeholder; unlock stays instant
- Surface a rejected send inline above the composer (a bottom toast hides
behind the keyboard); auto-dismisses after 4s
- waiting-session hint invites the first message instead of implying the
agent is still starting
* test(mobile): sync answer-send pacing test to the 500ms advance buffer
Missed in merge 8fe3c391c, which carried main's NATIVE_CHAT_ADVANCE_BUFFER_MS
300->500 (#8568) into the shared stepping module that mobile derives from.
* fix(mobile): restore terminal stream after chat cold start
* fix(native-chat): harden retries, optimistic sends, and file scans
* fix(mobile): deliver AskUserQuestion answers by option number (STA-1860)
Port #8840's fix to the mobile native chat: the Ask card now tracks
per-question option INDICES (+ free text) and the answer-send hook drives
Claude's arrow-navigate selector with buildAskAnswerKeys keystroke groups —
option numbers, next-tab arrows, Enter — paced one selector step apart, instead
of pasting label text that the selector ignores (which silently committed the
default option). Non-Claude agents keep the pasted-label path via the
selection-based formatAskAnswer.
Backcompat: keystrokes are built client-side and written through the EXISTING
terminal.send passthrough with enter:false — the same contract the permission
card already uses — so an older desktop runtime (SSH/relay included) replays
them verbatim; no RPC/contract change in either update order. Free text is
newline-sanitized because terminal.send has no paste framing.
Drops the now-unused formatCompleteAskAnswer from the shared module.
* fix native chat send and runtime races
* fix mobile native chat formatting
* fix(native-chat): mobile empty state matches desktop copy
Mobile showed a single generic line ('Send a message to get started') where
desktop shows a titled two-line empty state naming the agent ('Start a chat with
Claude' + 'Ask Claude to inspect code, explain output, or make a change.'). Align
them from one source of truth so they can't drift again:
- Extract the agent-type label map + formatAgentTypeLabel to
src/shared/agent-type-label.ts (desktop re-exports; mobile imports).
- Add src/shared/native-chat-empty-state.ts with the canonical English copy;
desktop uses it as its i18n fallbacks (localization unchanged — en/es/ja/ko/zh
keys still win), mobile substitutes the agent label and renders it directly
(mobile ships English only).
- Mobile: render title + subtitle for waiting-session AND ready-but-empty (both
are 'start a chat'), error copy for errors; keep the loading spinner.
Live-verified on the iOS sim against a pn-dev of this branch. typecheck node/web
+ mobile tsc clean; 30 mobile + 428 desktop/shared native-chat tests green.
* style: oxfmt the empty-state parity test (line wrap)
---------
Co-authored-by: Brennan Benson <brennanbenson@Brennans-MacBook-Pro.local>
Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
Fixes #6972.\n\nPreserves mobile terminal buffered/live input mode across Android terminal re-entry and session refreshes. Includes follow-up hardening for pre-hydration preference edits and failed storage reads.
## Summary
- add mobile Settings → Browser → Open terminal links
- route terminal HTTP(S)/OSC 8 link taps to either the streamed Orca browser or the phone browser
- keep existing file-path tap behavior intact
## Verification
- pnpm --dir mobile test
- pnpm --dir mobile lint
- pnpm --dir mobile exec tsc --noEmit
- git diff --check
* Add worktree-list sidebar for mobile tablet/foldable layouts
On wide canvases (tablet/foldable, >=700pt) the per-host worktree list now
renders as a persistent left sidebar with the routed screens (terminal,
source control, review, accounts, tasks) shown in a detail pane to its
right — mirroring the desktop's sidebar + center layout. Phones keep the
existing single-pane stack navigation unchanged.
- Reuse the existing worktree-list screen as the sidebar via an `embedded`
mode (props for hostId/action, mount-driven fetch since a sidebar is never
the focused route, hide-sidebar control in place of the back button, and
open-into-detail-pane navigation that replaces rather than stacks).
- The default host route renders an empty WorkspaceDetailPlaceholder on wide
layouts (the list lives in the sidebar) and the full screen on phones, so
exactly one instance mounts either way.
- Hide button collapses the sidebar to give the detail pane full width; an
elevated reveal tab brings it back.
- Detail-pane screen transitions use `animation: 'none'` while the split is
active so workspaces swap instantly instead of sliding and flashing the
screen beneath.
- Drag the sidebar's right edge to resize (clamped 280-560pt, detail pane
kept >=320pt, tap-transparent so list rows still work); width persists via
AsyncStorage.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Address PR review feedback (#5505)
- Clamp the restored/persisted sidebar width against the current window and
re-clamp when the window shrinks, so a width saved on a larger device can't
starve the detail pane below MIN_DETAIL_WIDTH. Extract a shared
clampSidebarToWindow helper reused by load, window-resize, and drag.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Polish mobile tablet sidebar
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com>
Co-authored-by: Orca <help@stably.ai>
* release: v1.4.48-rc.0 [rc-slot:2026-06-05-15]
* release: v0.0.1-rc.0 [rc-slot:2026-06-06-03]
* feat(mobile): add terminal text size (zoom) setting
The mobile terminal fits the desktop's full column count to the phone
width with a CSS scale, which cancels out xterm's raw fontSize — so there
was no way to make text bigger or smaller. Add a persisted baseline zoom
multiplier the WebView applies on top of the fit.
- Settings → Terminal → "Text size": 50%–200% presets.
- Pinch-to-zoom in the terminal snaps to the same presets and persists.
- Per-device display preference; does not change the desktop terminal.
- Extracts the xterm WebView HTML into terminal-webview-html.ts to keep
TerminalWebView.tsx within its max-lines budget.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(mobile): make terminal text size reflow columns instead of CSS scaling
Text size now drives the real xterm fontSize rather than a CSS transform
baseline. A larger cell means fewer columns fit the phone width, so the
existing measure -> terminal.updateViewport pipeline resizes the PTY and the
shell rewraps to the new width (and smaller sizes show more columns). Pinch
still snaps to a preset, but now changes the font size and reflows instead of
scaling pixels; the refit hook re-fits the PTY whenever the scale changes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(mobile): restore terminal touch scrollback (undefined contentWiderThanViewport)
The single-finger touchmove handler gated horizontal panning on
contentWiderThanViewport(), which is never defined — so every one-finger
touchmove threw a ReferenceError before reaching the vertical-scroll code
below it, killing all touch scrolling including scrollback.
Replace the call with the inline overflow check clampPan() already uses
(scrollWidth * getTotalScale() > innerWidth). Vertical scroll now always runs;
two-finger pinch-to-zoom is unaffected. Merged two adjacent var decls to keep
the file under its max-lines cap without a disable.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(mobile): guard terminal text scale viewport refit
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com>
Co-authored-by: Orca <help@stably.ai>
* release: v1.4.48-rc.0 [rc-slot:2026-06-05-15]
* release: v0.0.1-rc.0 [rc-slot:2026-06-06-03]
* feat(mobile): add terminal keyboard autocomplete toggle
Terminal command inputs disable autocorrect/suggestions and use a
password-class keyboard so the OS never rewrites commands, flags, or
paths. Some users want phone-style typing in the command bar.
Add a Settings → Terminal "Autocomplete & autocorrect" toggle
(persisted locally, default off). When on, the command bar enables
autoCorrect/spellCheck and uses the default keyboard so Android shows
its suggestion strip; iOS drops the ascii-capable restriction. The
direct keyboard-capture input is unchanged — it streams raw keystrokes
where suggestions cannot apply.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(mobile): remount command input when autocomplete toggles
Android caches the IME inputType at mount, so flipping the toggle did
not switch suggestions on until the field remounted.
* fix(mobile): guard autocomplete pref loads against unmount and fast toggle
Address review feedback: the settings toggle's initial load could clobber
a fast user toggle, and the session focus-effect could setState after the
screen unfocused. Both now use a guard flag.
* test mobile terminal autocomplete preference
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com>
Co-authored-by: Orca <help@stably.ai>