Commit Graph
803 Commits
Author SHA1 Message Date
Jinwoo-H a593e5d8a7 fix(mobile): stop hybrid-only failures from blocking unpair, sleep, diagnostics
Three native-path regressions plus a doc correction:

- Unpair awaited removeMobileWebHostCache before removeHost. The native store
  throws on an empty identity or a failed tree delete, and that cache need not
  exist at all on a native build, so a hybrid-only failure stranded a paired
  host. Both cache cleanups are best-effort now.
- Activation diagnostics dropped the target and the RPC failure code, leaving
  concurrent activations indistinguishable and failures unexplained. Restore
  the redacting helpers from main; a new test pins that only the 8-char suffix
  reaches the log.
- The Sleep action lost its `.catch`, so a rejected fire-and-forget sleep
  surfaced as an unhandled rejection.
- The README claimed an unset architecture keeps native. It does for release
  builds, but a development build defaults to hybrid; document the real rule
  and how to opt a dev build back into native.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-04 02:10:23 -04:00
Jinwoo-H 7f6d5334e7 fix(mobile): re-arm the bounded terminal fit loop and dead-tab recovery
The stream presentation replaced runTerminalViewportFitPass with its own
correction pass, leaving the STA-3337 budget built and cleared but never
charged. It also coerced absent host dims to 80x24, which can never equal a
phone viewport, so a host that omits cols looped scrollback -> measure ->
unsubscribe -> resubscribe with no cap, backoff, or degrade toast.

Restore readTerminalViewportDims plus the `hostCols ?? viewport ?? 80`
fallback (absent dims now yield hold), call runTerminalViewportFitPass from
the presentation, and let a `resized` frame charge convergence through
observeResize. Two other main behaviors return with it: a stream end/error
signals terminal-inventory recovery so `exit` retires the tab promptly
instead of waiting for the 60s sweep, and the display-mode Map keeps its
identity when the mode is unchanged so a stream pass stops re-rendering the
whole route.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-04 02:10:14 -04:00
Jinwoo-H d03b19d001 fix(mobile): restore the host-stack coordinator for native deep routes
Home Resume/Tasks/Accounts and notification taps went back to a plain
router.push into the nested host navigator. On the native build a cold push
there resolves to the host index without the dynamic id, so HostProtocolGate
mounts with hostId undefined and the host screen renders blank (the bug #12001
fixed by mounting /h/[hostId] first and replacing once its stack commits).

navigateFromMobileHome now maps a MobileWebNavigationIntentTarget onto the
matching HostStackRouteTarget and hands the deep ones (session, tasks,
accounts) to coordinateHostStackNavigation via useOpenHostStackRoute. Host-index
intents (newWorkspace, workspaceList) and the whole hybrid build keep their
plain push, and the hybrid navigation intent is still published either way.

The Resume card also regains the `name` param it lost, so the session header
has a title before the workspace loads.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-04 02:10:05 -04:00
Jinwoo-H 8ec4d59331 Merge remote-tracking branch 'origin/main' into mobile-rearch
Resolves #16239's shared-client terminal identity against the hybrid split: the
hosted page has no native client, so identity readiness is a flag
(hostClientIdentityReady) rather than a non-null clientId, and the bridge
terminal operations keep their workspaceId/terminalId/clientId contract.
Adds getClientId to the disabled hosted client context and keeps the
mobile-web extra resource alongside main's new emoji shortcode dataset.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-04 01:32:48 -04:00
Jinwoo-H 4d4e361f52 Revert "fix(mobile): surface a dropped hosted tab subscription instead of spinning forever"
This reverts commit ca7c7a281d. The root cause of the dropped subscription is
still unknown; surfacing the drop only trades one symptom for another.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-03 23:07:21 -04:00
Jinwoo-H ca7c7a281d fix(mobile): surface a dropped hosted tab subscription instead of spinning forever
A rejected session snapshot (invalid shape, oversize, or a failed post) cancelled
the shell-side subscription silently, the bridge client discarded the late error
because the subscribe request had already resolved, and the page kept "Loading
tabs" with no error and no retry. The shell now posts an error on the subscribe
request id, the client routes it to the subscription's onError (which already
falls back to polling), and the session screen shows Retry after two consecutive
failures. Reuses the existing response opcode, so mixed versions degrade to
today's behaviour.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-03 22:52:21 -04:00
Jinwoo-H 979cf30247 feat(mobile): opt-in inspectable Android release build for hosted WebView devtools
`-PorcaInspectableRelease=true` marks the release variant debuggable through an
Expo config plugin and flips a build config field the shell's inspection policy
reads. The OS debuggable flag stays a hard requirement, so a shipped production
APK can never be inspected regardless of the Gradle property. Default builds are
byte-for-byte unchanged.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-03 22:52:21 -04:00
Jinwoo-H 738132bf9a fix(mobile): address hosted markdown tabs by id so external files load
Tabs opened from outside the worktree carry an absolute path; the hosted
snapshot stripped it and the read payload was rejected before any request.
The shell now resolves the file from its own session.tabs.list by tab id,
carries isDirty through, clamps oversized reads to read-only instead of
failing, and the retry state names the error code.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-03 21:35:33 -04:00
Jinwoo-H 54de839211 fix(mobile): no empty repo placeholders while workspaces are still loading
Placeholder sections for repos with no rows were built before the paged
worktree list landed, so every repo flashed as a count-0 header. Gate them
on rows loaded, and let the hosted host state reuse the in-memory cache so
returning from a session keeps the last complete list.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-03 21:35:33 -04:00
Jinwoo-H 5bc7e95040 fix(mobile): keep the resume route across a hosted package swap
A desktop update swaps the hosted page under the user; the shell reset the
remembered route to the workspace list on every session id, so a session
only came back through cold resume after the list mounted and fetched.
Scope the memory to the host so init replays the session route directly.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-03 21:35:33 -04:00
Jinwoo-H fc1e78f862 fix(mobile): open network diagnostics through the shell from the hosted host list
The hosted page pushed /connection-log page-locally; the page has no such
route and cannot show the shell's transport log anyway. Hand the shell a
connectionLog native route instead, and fence the class with a reachability
test over the hosted module graph. Re-pair goes through repairPairing too.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-03 21:35:33 -04:00
Shahar MorandMerge Sim 7106101ed2 fix(mobile): restore terminal input when reopening worktrees (#16239)
* fix(mobile): restore terminal input when reopening worktrees

* test(mobile): update session parity facts

* refactor(mobile): split host client hooks

* chore: restore localization formatter scope

* fix(mobile): retain RpcClient type import

---------

Co-authored-by: Merge Sim <sim@local>
2026-09-03 18:32:53 -07:00
Jinwoo-H 2620be9b1f Merge remote-tracking branch 'origin/main' into mobile-rearch
Reconciles main's structured native Codex chat (#18074) and the stage-aware
relay dial bound (#18518) with the hybrid operations layer:

- native-chat controller keeps the operations/target/disconnect-retention
  seam and routes agent-session tabs through the structured hooks; the
  active-resolution and terminal-write hooks are extracted to stay under
  the line cap
- image attachments keep the hosted attachImage/pasteImages path and add
  main's structured (paste-free) send
- bare Codex launches take the structured path only on the native client;
  hosted adapter creates stay on the adapter
- file taps resolve against the source session tab when a structured chat
  has no backing terminal
- relay session advertises client capabilities after resume confirm
- package downloader contract split out to break the import cycle the
  native code-quality audit now rejects

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-03 19:14:17 -04:00
Brennan BensonandMerge Sim 98e77ef1a7 feat(mobile): structured native Codex chat (#18074)
* feat(mobile): finalize structured native Codex chat

* fix(mobile): close structured chat lifecycle gaps

* wip(mobile): fence stale structured inventory and bound operation-id retention

Fence local structured-session inventory and subscription responses with a
sync generation so a toggle-off clear, reconnect restore, or retry cannot
apply a mirror from a superseded instance. Bound mobile ambiguous
operation-ID retention at 128 with unmount cleanup.

Staged on the reconcile branch only: the sync module is now 312 lines and
needs a real split before this can reach the PR head.

* fix(ci): split the structured session-tabs sync and give static analysis mobile types

The local structured session-tabs sync module outgrew the 300-line cap once it
took on generation fencing, so split it along its real seams instead of raising
the cap: the generation/cursor fence, snapshot projection, snapshot apply,
inventory refresh, and the subscription loop. The original path stays as a
barrel so no importer moves.

Repoint the host-session-mirror settle census at the apply module, which owns
two receipts now — the snapshot it mirrors in, and the toggle-off teardown that
retracts what it published. The teardown receipt is named rather than anonymous
so the pin says which direction it settles.

The changed-code quality gate lints mobile files and resolves their types from
mobile/node_modules, but mobile is a separate pnpm project that the root install
never populates, so every mobile type degraded to an `error` type and the gate
reported phantom findings. Install mobile dependencies in static analysis when
the diff touches mobile, gated on a new classifier output.

* fix(mobile): let a slow capability handshake still reach connected

The mobile capability update is an advisory whose result is discarded, yet an
unanswered one was fatal while an explicit rejection was tolerated. A 5s timeout
on the direct client force-closed the socket, and on the relay path it failed
`confirmResume` before `connected` was ever published, so a consistently slow
link redialled forever. Both paths now share one helper that settles every
ambiguous outcome (timeout, mid-flight drop) like a rejection and rejects only
when the frame never reached the wire — the one case nothing else recovers from,
since the socket's own desync force-close is gated on already being connected.
The generation guard still keeps a replaced session from connecting.

Retained structured-session operation ids were capped at 128 with oldest-first
eviction, but every retained id belongs to a send whose outcome is unknown, so
eviction turned a user's retry into a second message on the host. Bound the map
by expiry against the id's own embedded timestamp instead, mirroring the host's
operation ledger, so no id is released while the host would still honour it.

Also give the mobile CI install the root install's lockfile drift guard (mobile's
lockfile carries patchedDependencies a silent rewrite would drop), gate
mobile_dependencies on should_run, and key the pnpm store cache on both lockfiles.

* refactor(mobile): extract the relay pending-request registry

The merge composed two independently-sized changes — this branch's capability
handshake settle and main's dial-stage tracking — pushing the relay session file
to 304 lines against a 300 cap. Neither side broke it alone.

Move the in-flight request registry (id generation, tracking, settlement, and
reject-all with its delivery-ambiguity marking) into RelayPendingRequests,
matching the existing collaborator pattern alongside RelayDialStageTracker and
RpcSessionLivenessWatchdog. No behavior change.

---------

Co-authored-by: Merge Sim <sim@local>
2026-09-03 15:19:26 -07:00
Jinwoo Hong 4d24fb340b fix(mobile): stage-aware relay dial bound so a slow cell is not hung up on (#18518)
A phone returning to foreground on 2026-09-03 logged "replacement session
authentication timed out" five dials in a row while the desktop's relay
control was live. The cell (production-gce-c27) had taken relay-auth but
its assignment/reservation transactions were lock-contended (55P03 retries,
14–16s per accept); the phone's flat 12s migrateTo bound closed the socket
2–4s before the cell finished (cell logged host_data_reservation_already_bound),
and because the timeout counted as a director-class failure the phone
re-resolved the same cell and waited 12s again before logging — every
retry landed in the same contended window.

- MobileRelayE2eeLink reports onOpen once relay-auth is on the wire;
  MobileRelayRpcSession exposes a dial stage
  (opening → awaiting-hello → handshaking → confirming).
- waitForAuthenticated keeps the caller's bound until the socket opens, then
  re-arms a per-stage budget (30s awaiting-hello, 12s handshaking, 35s
  confirming) so a reachable, slow cell is not treated as a black hole.
- The timeout error carries the stalled stage and shows up in the
  "relay dial failed" log line; a stall past the open socket no longer
  triggers the director re-resolve round.

Phone-local only: no wire change.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-03 17:16:35 -04:00
Jinwoo-H 655d573df4 test(mobile): merge the duplicate react import
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-03 15:36:40 -04:00
Jinwoo-H 22574ed602 style(mobile): present the hosted package download as a centered banner
Over a live hosted page the progress block sat at the left edge with no surface
of its own. Give it the panel background, centered layout, and a hairline
divider so it reads as a banner above the page.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-03 15:33:00 -04:00
Jinwoo-H 5a393dfca1 fix(mobile): keep the hosted browser tab from crashing on react-native-web
Frame layers were swapped through `setNativeProps`, which react-native-web refs
(DOM nodes) do not have. Opening a browser tab in the hybrid page threw inside
the ref callbacks, the route error boundary caught it and reset to `/`, so the
tab flashed and bounced back to the workspace list. Mutate the DOM directly on
web and keep `setNativeProps` on native.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-03 15:30:59 -04:00
Jinwoo-H 8bc82a6563 fix(mobile): serve the host index route on the hosted page
Shared session code exits to `/h/<hostId>` when there is no history to pop
(leaveSession after a fresh page load into a session, the missing-worktree
bounce). The hosted page only listed workspaces at `/`, so those exits
rendered expo-router's Unmatched Route screen. Alias the host index to the
hosted list.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-03 14:15:30 -04:00
Jinwoo-H 35a47e6333 fix(mobile): stop the hybrid catalog refresh re-arming every render
useHybridHostRepoMetadata listed the whole state object as a dependency, so the
callback and the refresh effect that depends on it re-ran on every render. Each
refresh fetched, set state, rendered, and re-armed: a self-sustaining request
loop the shell broker rate-limited, which the list showed as network_error.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-03 06:26:50 -04:00
Jinwoo-H 10f8820064 test(mobile): derive the renderer type for the CI type-aware lint
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-03 06:09:55 -04:00
Jinwoo-H 4d0babfe53 Merge branch 'mr-flash' into mobile-rearch 2026-09-03 05:52:55 -04:00
Jinwoo-H bf2de0dd9b fix(mobile): keep the hosted list loading through the bridge warm-up
The hosted page reads `connected` from the shell's relayed snapshot, so its
first worktree.ps can be issued before that socket serves one. Page storage is
disabled, so the hosted list always starts at zero rows — the precondition the
list state machine needs to render `catalog-error` — while the native list is
seeded from its persisted cache and hides the same failure behind stale rows.

Let a relayed transport spend one silent catalog retry per binding: the first
failure leaves catalogError null, so the list stays in `loading` and the
existing refresh retries. A direct socket omits the flag and is unchanged.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-03 05:52:11 -04:00
Jinwoo-H 8b9c02ff0f Merge branch 'mr-spinner' into mobile-rearch 2026-09-03 05:49:06 -04:00
Jinwoo-H dd5c5faaa0 Merge branch 'mr-inset' into mobile-rearch 2026-09-03 05:49:06 -04:00
Jinwoo-H 0f8329954f fix(mobile): give the hosted page's top inset a single owner
The Expo shell pads the container that holds the WebView by the device top
inset, and the hosted page pads again from env(safe-area-inset-top). Android
derives that value from the window's display cutout without subtracting the
WebView's offset, so every hybrid screen sat a second status bar below the
system one. iOS never showed it because WKWebView recomputes its own safe area
from the view's position.

The shell keeps the inset. The hosted route root now pins the page's top inset
to zero and leaves the edges the WebView still meets alone, so the reserved
space is applied exactly once on both platforms.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-03 05:48:39 -04:00
Jinwoo-H ebd74e681b fix(mobile): keep hybrid busy spinners animating on react-native-web
Animated.loop branches on the raw `useNativeDriver` flag, not on driver
availability. On react-native-web that sends the loop down `_startNativeLoop`,
which starts one JS-driven timing pass whose `iterations` the JS driver ignores,
so the spinner ramps 0 -> 1 once and freezes. Verified against the real
react-native-web Animated: 3.5s of frames yields zero restarts with the flag on,
3+ with it off.

The same components also lost their ring gap on web: an inline `borderColor`
after a StyleSheet `borderTopColor` is emitted as four inline longhands that
outrank the class, rendering a solid ring instead of an arc.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-03 05:47:06 -04:00
Jinwoo-H 5cb3eecf76 test(mobile): type the owned shell session structurally for the CI lint
The type-aware gate on CI resolves the file: shell package as any, which
collapsed the ref's nullable union whichever module named the type.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-03 05:43:57 -04:00
Jinwoo-H 543b51f7e8 test(mobile): name the owned shell session type through the app module
The type-aware CI lint resolves the file: shell package as an error type, which
turned the ref's union into any and failed the changed-code gate.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-03 05:31:52 -04:00
Jinwoo-H 50502fb8a6 Merge branch 'worktree-agent-a5d9fcc87bd472be9' into mobile-rearch 2026-09-03 04:45:27 -04:00
Jinwoo-H 9a027cbdb8 fix(mobile): stop a mid-commit view activation raising a false recovery banner
The hybrid screen's focus effect calls activateSessionView, which on Android
routes to activateViewSession and required the view to already be in the
module's session registry. The registry is filled by the sessionId prop commit,
so a focus that lands first threw mobile_web_shell_view_unavailable and the
screen pinned "Hosted session could not be restored." above a page that was
healthy the whole time. Nothing clears that warning, so the banner also kept the
WebView pushed down for the rest of the session.

The prop is the activation authority, so a view missing from the registry is
mid-commit for the same session, not a failure.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-03 04:22:05 -04:00
Jinwoo-H 2772f21e50 fix(mobile): derive the Android origin label instead of slicing the session id
Deriving the host label by slicing the session id forced the id itself into a
hostname alphabet, and the previous fix changed the native stores to mint
lowercase base32. But the session id is a wire token: `ShellSessionIdSchema` in
`src/shared/mobile-web/bridge-contract.ts` pins it to 43 base64url characters,
and the page that validates it is served by the desktop, which updates
independently of the app. A 52-character base32 id therefore failed
`parseMobileWebBridgeInitialMessage` inside the page, which silently dropped
`init`: no bridge client, no `ready`, no `health`, and a workspace list that
spun forever behind "The workspace interface has not reported healthy".

Session ids go back to base64url and the origin label is now the first 32 hex
characters of their SHA-256. Lowercase hex is canonical for Chromium's host
canonicalisation and parseable by `java.net.URI.getHost()`, so the original 403
and dropped-bridge-message defects stay fixed without touching the wire token.

`mobile-web-shell-init-contract.test.ts` parses the exact init the hybrid screen
posts, which is the oracle that was missing: nothing checked that the shell's
own init survives the contract the page enforces.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-03 04:18:24 -04:00
Jinwoo-H b78660541d chore(mobile): split the download-measurement harness under max-lines
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-03 04:07:21 -04:00
Jinwoo-H b3c416963c Merge branch 'worktree-agent-a5d9fcc87bd472be9' into mobile-rearch
# Conflicts:
#	mobile/app/hybrid.tsx
#	mobile/src/mobile-web/use-mobile-web-package-recovery.ts
2026-09-03 04:07:05 -04:00
Jinwoo-H 6e1d98c103 Merge commit 'c6d79aa7eb' into mobile-rearch
# Conflicts:
#	mobile/src/mobile-web/MobileWebPackageProgress.tsx
2026-09-03 03:53:22 -04:00
Jinwoo-H c6d79aa7eb perf(mobile): pipeline and widen mobile-web package reads
The hybrid app pulled its 9.1 MiB mobile-web package one 48 KiB chunk per RPC
round trip, strictly serially, and 99.3% of the package is a single 9.5 MB
script — so the whole download was 245 sequential round trips against whatever
the relay path's latency happened to be. Measured on the real package over the
real mobile relay client with a 120 ms round trip: 31.7 s (0.30 MB/s).

Two changes, both negotiated:

- the downloader now keeps up to MOBILE_WEB_PACKAGE_MAX_CONCURRENT_READS reads
  in flight across the whole manifest while still draining to the stager in
  offset order (the native stage appends at the file's current length), and
  narrows the window instead of failing when a host answers
  mobile_web_package_read_limited;
- mobileWeb.package.asset.gzip takes an optional `length` so one read answers up
  to eight chunks. The params schema is strict, so older hosts reject the field —
  it is gated on the new mobileWeb.package.range.v1 capability, and the client
  still splits the range into 48 KiB stage writes.

Same package, same harness, relay path: 31.7 s -> 2.8 s at 120 ms RTT and
62.9 s -> 5.5 s at 250 ms RTT, with 245 requests down to 76.

The download still dies when the app is backgrounded past
RELAY_BACKGROUND_GRACE_MS — the session suspends, the refresh effect aborts, and
the stage is discarded — so the progress panel now says to keep the app open.
Resuming from the staged offset needs a native stage-reopen API on both
platforms and is not attempted here.

mobile/scripts/measure-mobile-web-package-download.mjs drives the real
downloader over both the direct and relay mobile clients with an injectable
round trip, which is where every number above comes from.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-03 03:50:20 -04:00
Jinwoo-H 49054a12ea fix(mobile): stop the health-timeout reload livelock
A missed interactive health deadline recovers to the previous verified
generation. With no previous generation the recovery throws and the shell bumped
the view epoch, which remounts the WebView, reloads the document and re-arms the
very deadline that expired. A page that simply needs longer than one deadline
could therefore never finish loading: on the Android emulator the hosted
document reloaded every ten seconds indefinitely.

The restart is now only taken where the view is actually gone (crash loop) or
the user asked for it (manual recovery). A health timeout with nothing to
recover to keeps the page and reports the warning.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-03 03:48:08 -04:00
Jinwoo-H 7638a124c5 fix(mobile): stop re-downloading the package a session already runs
`reuseVerifiedBuild` only reused a build the cache probe had verified. That
probe resolves null for a host paired for the first time, so every later
refresh in the same host epoch re-downloaded the whole package — minutes over
the relay — even though the running session was already on that build. An
owned session only ever comes from `openSession`, so its build is verified by
construction; reuse it directly.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-03 03:48:08 -04:00
Jinwoo-H 7baa64d36b fix(mobile): make the Android hosted origin a canonical host label
The Android private origin's host label is the session id's prefix, but the
ids were base64url. `https` is a special scheme, so Chromium ASCII-lowercases
the host of every URL it loads and reports back, while `Uri.parse` keeps the
mixed case the shell derived: `serveRequest`'s origin check rejected the main
document and answered 403, which Android renders as
`net::ERR_HTTP_RESPONSE_CODE_FAILURE`. `java.net.URI.getHost()` is also null
for a label holding `_`, so the same ids dropped every bridge message.

Session ids now come from a lowercase base32 alphabet,
`mobileWebOriginForSession` rejects anything a URL host cannot carry, and host
comparisons are case-insensitive. A failed main-frame document no longer stops
at Chromium's error page: the shell hides the WebView and reports `failed` with
a reason the React Native shell shows.

iOS uses a custom scheme, whose opaque host preserves case and `_`, which is
why only the Android lane saw this.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-03 03:48:08 -04:00
Jinwoo-H e4feda6965 fix(mobile): plain-language hybrid shell states and a single primary action
The hybrid shell spoke in implementation terms — "Verified desktop-served
interface", "Preparing verified interface…", "Connect to this desktop once to
cache its verified workspace UI." — and rendered four equal recovery buttons
inside a grey box.

Every user-facing string is now one plain sentence, carried as a
MobileWebShellNotice so the stable failure code lives on a small "Error: <code>"
support line instead of inside the copy. The header drops its subtitle, the
download state collapses to one line plus the bar, and recovery promotes Retry
to the app's primary button style with the rest demoted to text links; the
grey container is gone. Recovery labels move to Use last version / Reset and
carry stable testIDs so e2e no longer depends on visible copy.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-03 03:29:37 -04:00
Jinwoo-H 62e58c6d3c fix(mobile): mirror main's host-label state into the hybrid host screen hooks
Why: main #18xxx added repoHostIdByRepoId/hostLabelById/hostPlatform to the native host
screen state that HostScreenView consumes; the hybrid twin hooks must expose the same shape
or the hosted route fails typecheck. The hosted bridge publishes no host labels yet, so the
maps stay empty and rows keep today's single-host presentation.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-03 01:25:14 -04:00
Jinwoo-H 856b253fe6 Merge remote-tracking branch 'origin/main' into mobile-rearch
# Conflicts:
#	config/scripts/electron-builder-config.test.mjs
#	config/scripts/pr-code-change-scope.mjs
#	config/scripts/pr-code-change-scope.test.mjs
#	config/scripts/run-ssh-docker-e2e.mjs
#	src/main/git/command-runner/gh-exec-file.ts
#	src/main/providers/ssh-filesystem-provider.ts
2026-09-03 01:08:19 -04:00
Brennan BensonandMerge Sim 7f8eb90ac3 Align worktree host labels across desktop and mobile (#18237)
* refactor: align worktree host labels across clients

* fix(mobile): expose safe host display labels

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

---------

Co-authored-by: Merge Sim <sim@local>
2026-09-02 15:32:06 -07:00
Jinwoo-H 96bc6da82d Merge commit '20a499baa5' into mobile-rearch 2026-09-02 02:18:09 -04:00
Jinwoo-H c9611a674e Merge branch 'mr-sshskill2' into mobile-rearch 2026-09-02 02:11:08 -04:00
Jinwoo-H 0772dff1a6 fix(ssh): keep a native-chat session bound when the relay drops
A relay teardown calls agentHookServer.clearStatusEntriesForConnection, so an
unreachable SSH host reports a terminal with no agentStatus. Both native-chat
binding resolvers read that as "no such session" and returned not_found, which
makes loss of contact evidence the session is gone — the failure mode
docs/reference/ssh-execution-boundary.md exists to prevent.

Whether the spec saw it was pure timing: the mobile-session snapshot only loses
the provider session once a refresh lands inside the disconnect window. A 3s
wait after disconnect turns it into a deterministic failure on the previous
commit, which is what CI was hitting.

Runtime side: remember the last-known agent and provider session per terminal
handle, and fall back to it when the live tab no longer carries one. The
terminal context stays the existence gate, so a closed terminal still resolves
to nothing and drops the memory with it.

Broker side: only a vanished tab, a different terminal, or a tab rebound to
another provider session revokes the opaque grant. A tab whose host simply
stopped reporting status keeps it, so the read surfaces host_error.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-02 02:10:21 -04:00
Jinwoo-H 20a499baa5 test(mobile-e2e): name both landmarks when screenshot parity fails
The failure printed only landmarkDelta, which cannot say which side moved. The
two landmarks come from different sources, a native accessibility frame and a
hosted DOM rect, so the absolute pair is what makes the next failure readable.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-02 02:07:21 -04:00
Jinwoo-H 06a2dd7b5b Revert "read the native landmark after the route settles"
The landmark delta is bit-identical across the run before the change and the
run after it (0.047498512585812364 both times), so re-reading after the settle
changes nothing and the animation rationale was wrong. Reverting rather than
leaving an inert change carrying a disproved explanation.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-02 02:05:21 -04:00
Jinwoo-H 5e39d8647c test(mobile-e2e): assert session-origin reviewOpen matches native on this host
Opening a changed file from a session-origin Source Control sends
files.openDiff, which needs a renderer notifier. The e2e pairs against
`orca serve --mobile-pairing`, which has none, so the host answers
renderer_unavailable on native and hybrid alike. The journey asserted the diff
tab unconditionally and could never pass.

The native baseline now probes the same session-origin path and records what
the paired host answered. The hosted journey takes whichever arrives first, the
diff route or the bridge error, and pins it against the native result, so the
claim is "hybrid matches native on this host" rather than "hybrid fails". On a
full desktop the diff route wins and reviewOpen.headless records false.

Kept as evidence, not fixed: the hybrid banner reads "Source control action
failed" where native names renderer_unavailable.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-02 01:45:51 -04:00
Jinwoo-H 4dc2229283 test(mobile-e2e): capture a native journey stop only once its tree repeats
A single accessibility read caught the Tasks list mid-load on one client and a
second, transient xterm helper node on the other, and both read as client
differences. Poll until two consecutive volatile-free reads match, then
screenshot. Re-running the A/B with this in place left the twelve stops
identical except for live GitHub issue content.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-02 01:35:30 -04:00