Commit Graph
827 Commits
Author SHA1 Message Date
Jinwoo-H 2ca0dc7c59 Merge branch 'fix2-wire-merge' into mobile-rearch 2026-09-04 16:42:47 -04:00
Jinwoo-H 4c7fd1aec6 fix(mobile-web): make the shell to page bridge direction forward compatible
W1 (P1): every shell-authored result and event schema was .strict(), and the page fails a schema mismatch as invalid_message with retryable:false. The shell (APK) and the page (served by the desktop) ship from different releases, so one additive field or one new session tab kind from a newer APK killed the subscription and its one-shot fallback on the same byte - Loading tabs forever, surviving force-quit. tolerantMobileWebShellPayload deep-rewrites a schema at the page's two shell-payload parse sites: strict objects strip unknown keys, an array of unions drops members it cannot classify, and an unknown value for an optional/nullable closed set reads as absent. Page to shell request schemas keep .strict() - the shell is the security authority there. A census ratchet walks every contract export the page parses and fails if a strict node survives the transform.

W3 (P2): a host RPC failure collapsed into host_error, which is retryable, so method_not_found and the mobile allowlist's forbidden looked like blips. Both now map to unsupported_capability (non-retryable) through mobileWebBrokerHostRpcError, applied by codemod to the 44 regular 'if (!x.ok) throw host_error' sites.

W4 (P3): BrowserScreencastResult gains the navigation member the host already emits. Decoders unchanged.

W2 (P2): the file: confinement test gains a clientKind runtime case - pairedDeviceId is minted for scope 'runtime' too, so the fence also governs the web client, a remote desktop, and remote orca CLI.

W5 (P3): inputFloor and queryReplyAuthority stay literals with a WHY comment. Traced: the host publishes neither over the terminal stream. isMobileTerminalQueryReplyAuthority is never sent, and opcode-17 WriteUnavailable reports one refused write with no regain signal, so it is not the floor state the field declares.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-04 16:41:17 -04:00
Jinwoo-H b6608834a9 refactor(mobile): collapse six shell subscription ledgers onto one base
The account, workspace, session, sourceControl, nativeChat, and browser ledgers were the same algorithm six times: identical record shape, admission guards, subscribe-then-reattach dance, cancel/cancelByRequest/countForOperation/dispose, and delivery chain. MobileWebSubscriptionLedger<TEvent, TRecord> now owns all of it; each concrete ledger keeps only its host subscribe call, its projection, and its operation key. MobileWebCapabilitySubscriptions replaces its four hand-written six-way fan-outs with loops over a ledger list.

postClosed stays a required constructor option and every construction site, tests included, now supplies it - three test fixtures previously left it undefined, which is why no shell-side closure frame had coverage.

Adds the missing closure-path tests: every ledger's invalid-host-message and failed-page-post paths, plus dispose/closeAll behaviour. dispose() stays silent toward the page because the document is going away with the shell; the new closeAll(closure) is what the broker calls on replaceClient, so a page that outlives a client swap learns its live subscriptions are over instead of freezing on their last value.

Behaviour change: a duplicate nativeChat subscription ID now raises invalid_request like the other five ledgers instead of rate_limited. The broker's replay guard rejects duplicate subscription IDs before the ledger sees them, so the path is unreachable in production.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-04 16:33:32 -04:00
Jinwoo-H 30e69bfbf8 Merge test review fixes: Android DevTools presence oracle, grant-key, scheduler, broker backpressure, filter, dispose, reachability census
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-04 02:45:11 -04:00
Jinwoo-H 47c2080d45 Merge design review fixes: file: confinement, snapshot byte-cap degrade, subscriptionClosed frame, cutover retry
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-04 02:45:02 -04:00
Jinwoo-H 001b66ebe0 test(mobile): close the Android release DevTools hole
Replacing the policy call with `true` shipped a release APK with WebView
DevTools on and passed every gate: the only assertion tying the probe to the
policy pinned the absence of an old code shape, and the JVM suite always passed
isDebugBuild/isInspectableRelease explicitly, so the BuildConfig defaults the
sole caller uses were never compiled, let alone run.

The census asserts the policy-gated call is the one and only
setWebContentsDebuggingEnabled in the shell. The JVM test drives the
one-argument overload, and testDebugUnitTest now finalizes testReleaseUnitTest
so the shipped BuildConfig values are exercised under the CI command.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-04 02:40:31 -04:00
Jinwoo-H 189146aed4 fix(mobile-web): retry the package download across a logical-session cutover
A relay/direct cutover rejects in-flight requests without changing `connState`.
The capability probe retries; the package downloader did not. Every throw
collapsed to `host_error`, and the refresh effect's deps are all unchanged by a
seamless cutover, so nothing re-ran and the user had to tap Retry — on the one
screen that has no content yet.

The contract now marks a cutover or ambiguous-delivery throw `retryable`, using
the same two predicates the native-chat send path already trusts, and both
package reads re-issue it on the replacement session with bounded backoff: the
chunk read next to its existing read-limited retry, and the manifest read, which
is the one request a cutover can kill before any chunk exists to retry.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-04 02:38:27 -04:00
Jinwoo-H aad3b40d6f test(mobile): pin all four package-download abort short-circuits
Neutering throwIfAborted survived: the one abort test was killed by the chunk
pipeline's own check, not by the downloader. Each case here aborts where only
the downloader's check can catch it, paired with an un-aborted control that
reaches stager.commit.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-04 02:35:28 -04:00
Jinwoo-H 608402b9b9 feat(mobile-web): give the bridge a terminal frame for a subscription
The bridge had seven shell -> page frames and none of them closed a
subscription. `response` is keyed by `requestId` and only reports the subscribe
call; `event` carries values. Once the subscribe response said `success`, every
shell-side failure after that point was unrepresentable — and the shell failed
subscriptions late in six ledgers, calling `cancel()` and discarding the request
id the signature returns for exactly this purpose. The page kept a live entry
with no timeout and no heartbeat, so tabs, workspaces, source control, accounts,
native chat, and dictation could all freeze on their last value with no error.

Adds `subscriptionClosed { subscriptionId, error: { code, retryable } }`, which
is additive to bridge version 2: a page built before it fails the union parse,
and `native-shell-channel` already returns on a parse failure rather than
tearing the session down, so an older page ignores it exactly as it ignores any
unknown type.

Every ledger's `cancel()` now takes an optional reason and posts the frame, and
the page routes it to the existing `fail()`, which already deletes the entry and
calls `onError`. Normal end-of-stream retirements pass no reason, so they stay
silent. No UI or presentation change: this is protocol completeness, and the
retry policy on top of `retryable` is a separate decision.

Two files crossed max-lines from the additions, so the event-verdict logic and
the closed-frame poster moved to modules of their own.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-04 02:34:12 -04:00
Jinwoo-H 138f70f53f test(mobile): census native-only APIs over the whole hosted graph
The binding test banned Clipboard.setStringAsync, Linking.openURL and
router.push('/terminal-settings') over a 42-file list, so injecting all three
into MobileTerminalInputActions.tsx passed. The census walks the 1052 modules
the hosted session route actually reaches and catches it.

AsyncStorage is deliberately excluded: it is genuinely reachable from the hosted
bundle today through the shared storage and transport modules.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-04 02:33:37 -04:00
Jinwoo-H 59798d6858 test(mobile): give the hosted module-graph walk a size floor
Seeding the walk with an empty pending list made the reachability oracle pass
over an empty universe. The walk moves to a shared support module so the
session-route census can reuse it instead of re-implementing the resolution order.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-04 02:33:31 -04:00
Jinwoo-H 3322a0b186 test(mobile): pin the workspace list sleeping and default-branch predicates
Three survivors in filterWorktrees: dropping the unread arm, widening the live
terminal check to >= 0 (which makes "Hide sleeping" filter nothing), and
dropping the non-empty branch guard that keeps a detached main worktree visible.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-04 02:27:52 -04:00
Jinwoo-H 39c7c59f85 Merge dead-code review fixes: orphaned host-screen chain, broker error classes, history rewriter, unused exports
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-04 02:27:28 -04:00
Jinwoo-H 0ae25258d8 test(mobile): pin the broker pending cap and response byte budget
MOBILE_WEB_BRIDGE_MAX_PENDING_REQUESTS appeared in no test, so disabling the
shared 64-request cap survived, as did dropping the maxResponseBytes check.
The saturation fill uses distinct operations so the shared cap is what fires.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-04 02:26:43 -04:00
Jinwoo-H 989a0f0e6b refactor(mobile-tasks): name the Tasks barrel for what it re-exports
mobile-tasks-legacy-foundation.tsx is a 13-line `export *` barrel over the
live Tasks domain — provider and view-state types, options, item mapping,
mutation targets, review and comment surfaces, project fields, repository
presentation, and the PR file diff. "Legacy foundation" describes neither its
contents nor its status, and AGENTS.md rules out that class of name.

Renamed to mobile-tasks-model.tsx, matching the `model` the Tasks route already
threads through its hook composition, and repointed all 58 importers.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-04 02:25:33 -04:00
Jinwoo-H d11b8c5791 fix(mobile-web): degrade an oversize session snapshot instead of killing it
Two limits govern the same snapshot and they disagreed. The 200-tab count limit
slices, keeps the active tab, and reports `truncated`. The 128 KiB event byte
cap cancelled the subscription with no frame the page could see, and since a
browser tab at the schema maximum serializes to roughly 5 KB, ~40 long-URL tabs
crossed the byte cap long before the count cap ever fired. The page then showed
"Loading tabs" forever, deterministically, on every re-entry into that
workspace.

The byte cap now drops tabs to fit — active tab reserved first — and reports the
same `truncated` flag, so the projection is the single place that decides how a
snapshot degrades.

The sibling native-chat cap is left alone: it bounds one incremental chat event,
which has no partial form to degrade to.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-04 02:23:38 -04:00
Jinwoo-H 3072647344 refactor: delete twenty exports referenced nowhere
Each name was re-verified with `rg -w` to appear only on its own declaration
line: localTranscriptFileSource (superseded by createProviderTranscriptFileSource
plus a bare filePath), the two unused bridge-operation-registry lookups,
projectRowGitHubRepository, mobileSessionMarkdownTargetKey, four ReturnType
aliases on the hybrid host-screen hooks, MobileSessionPanelRouteActionsModel,
and nine zod-derived contract aliases whose schemas stay in use.

The Tasks parity ratchet counts top-level declarations, so its declaration
count and digest move with projectRowGitHubRepository; the reason is recorded
in the file's baseline note.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-04 02:22:58 -04:00
Jinwoo-H b3513f9e29 Merge host-side review fixes: query-reply guard, files.unwatch, asar dedupe, gzip cache bound
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-04 02:22:03 -04:00
Jinwoo-H f283f88bd8 fix(mobile-web): confine file: browser creates to the named workspace
`session.createBrowser` is reachable from unprivileged page script and the page
it creates is streamed back over `browser.screencast`, so a `file:` URL turned
any host file into frames the page could decode. `browser.navigate` already
refused `file:` and result URLs were redacted; only the create path was open.

Both sides of the call now fence it instead of banning the scheme, because the
native HTML-artifact file tap is the same call:

- the shell re-resolves the path through `files.resolveTerminalPath` against the
  workspace the page named and forwards the host's own absolute path, so nothing
  the page wrote reaches `browser.tabCreate`, and an SSH worktree (whose path is
  on another machine) is refused;
- the runtime independently requires a paired caller's `file:` URL to sit inside
  that worktree's root on this host, so the page is not the only fence.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-04 02:21:13 -04:00
Jinwoo-H e02097ad8e docs(browser): record why the navigation screencast event needs no gate
Traced the receiving side rather than assuming: handleStreamingResponse passes
every screencast result straight to the listener, use-mobile-browser-stream
casts the payload with no schema parse, and handleBrowserScreencastEvent is an
if/else-if chain with no else and no throw. A build predating the member drops
it silently, so Rule 3 is satisfied without a capability gate.

Pin that with a test, since the reasoning only holds while the handler stays
tolerant of an unrecognized type.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-04 02:12:45 -04:00
Jinwoo-H 5fa0e7dff3 refactor(mobile-web): merge the two history installers into one URL rewriter
mobile-web-history-session-fragment and mobile-web-queryless-history were the
same module twice: same writer/target types, same private WeakSet, same origin
guard and try/catch, differing only in the URL mutation. Both installed at
module scope on the same history object, so every pushState was double-wrapped
and each navigation parsed the URL twice.

One installer now takes an ordered rewrite list behind a single WeakSet and a
single URL parse. The shell-session fragment gate moved from install time into
the rewrite, which is where the hash is read anyway.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-04 02:11:37 -04:00
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 2afc88d027 refactor(mobile-web): collapse five duplicated subscription error classes
The account, session, source-control, workspace, and browser ledgers each
declared a byte-identical private error class, but mobileWebBridgeErrorCode
recognised only the session one. The other four collapsed to host_error, which
also flipped retryability: a malformed subscribe or a hit per-ledger cap read
as a retryable host fault.

All five now throw the shared MobileWebBrokerError, so the converter has two
branches and no longer imports a ledger.

The downgrade is latent rather than live: the four non-session grants pin
maxConcurrent to 1 and the replay guard pre-empts duplicate subscription IDs,
so the broker's own guards fire first today. The new test covers both layers —
the per-ledger converter path (red before this change for all four) and an
end-to-end proof that a duplicate surviving replay-ring eviction reaches the
page with its own non-retryable code.

Leaves the shared ledger shell unextracted: cancel is not mechanical across the
six (browser clears a pending frame, source-control latches a closing flag), so
a base class would need per-ledger hooks.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-04 02:07:17 -04:00
Jinwoo-H f1cfb2c6ee fix(terminal): guard binary query-reply frames like terminal.send
Opcode 18 set inputKind:'query-reply' on the client's word alone, and that
kind forces clientId undefined in sendTerminalStreamInput, so the frame
skipped beginMobileInputFloor entirely. A phone that is not the elected
reply authority could write arbitrary bytes into a desktop-driven pane
unfloored.

Lift the terminal.send guard into terminal-query-reply-guard.ts and apply it
at all three sites. A binary frame that fails is dropped, matching the JSON
path, which throws on shape and returns accepted:false on authority — neither
demotes the bytes to ordinary input. The page-side sender now runs the same
isTerminalQueryReply grammar check the native sender does, so the phone never
emits a non-grammar query reply on either opcode.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-04 02:04:45 -04:00
Jinwoo-H 6553c694f0 refactor(mobile): drop the orphaned non-hybrid host-screen controller chain
The hybrid WebView migration routes h/[hostId] through
useHybridHostScreenController, leaving the seven pre-hybrid hooks with zero
call sites. The four view components typed their prop against the dead
controller's ReturnType, so they now take HybridHostScreenController directly.

That retype surfaced a live mismatch: the hybrid forceReconnectHost takes no
arguments, but the header and workspace list still passed hostId through the
old loose type.

Repoints the STA-5781 cross-client source pin at
use-hybrid-host-screen-settings.ts, which carries the same patch-only builder
call; verified the assertion still fails when that call is mutated.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-04 01:57:54 -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