Skip to content

fix(web): answer the mobile API from host RPCs instead of stubs - #26622

Open
espetro wants to merge 1 commit into
stablyai:mainfrom
espetro:fix/web-mobile-api
Open

espetro wants to merge 1 commit into
stablyai:mainfrom
espetro:fix/web-mobile-api

Conversation

@espetro

@espetro espetro commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

ELI5

The web client's mobile pane was fake: every call (getPairingQR, listDevices, …) returned hardcoded empty answers, so a browser paired to a headless orcad could never mint a pairing QR and showed a misleading "WebSocket transport is not running" toast even while the transport was healthy.

What Changed

  • Wires getPairingQR, getRuntimePairingUrl, listNetworkInterfaces, listDevices, revokeDevice through callRuntimeResult to the host's mobile.* RPCs (already registered and exercised on orcad).
  • isWebSocketReady/getRelayStatus now derive from mobile.hostStatus (webSocketEndpoint, relayAvailable) instead of constants and local state.
  • Windows firewall/repair calls keep their stubs - they have no RPC equivalent and only apply to Windows desktops.

Before: paired web client → window.api.mobile stubs → available: false always. After: the same calls answer from the live host over the runtime WebSocket.

Why

The mobile.* RPC surface already exists on the host (registered in orcad-entry); the web client just never called it. Delegating 1:1 is the smallest change and keeps the desktop window.api.mobile.* shape, so no callers branch. mobile.hostStatus is the single source of truth for transport/relay facts a remote client can actually observe.

Linked Issue

Fixes part of #18504 (headless serve cannot complete mobile pairing - this removes the web-client-side stub failure mode; host-side items in that issue are separate).

Visual Proof

N/A for the diff itself (no styling change). Behavioral before/after: before, the Mobile pane on a web session always showed the pairing fallback with a "WebSocket transport is not running" toast; after, it renders the live QR/device list from the host. Happy to attach captures if useful.

Testing

  • pnpm tc:web clean.
  • Manually: booted orcad and probed mobile.hostStatus / mobile.listNetworkInterfaces over the same WebSocket path the web client uses - all answer correctly; QR mints in the Mobile pane.
  • Tested on macOS (host) + browser client. Older hosts without the mobile.* methods: the calls surface transport errors rather than wrong answers - mixed-version pairing already requires a host new enough to register the surface.

AI Disclosure

Authored with an AI coding agent (GLM via Maki); human reviewed and verified the diff.

Review

Agent skill upstream boundary

  • Not applicable, or this change copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

Notes

Security: no new surface - calls go to already-authenticated runtime RPCs on the paired WS. Cross-platform: Windows firewall stubs intentionally unchanged (desktop-only concern). Remote/SSH: this is the paired-web-client path. Backwards compatibility: additive client behavior; hosts without the RPCs were already unable to serve mobile pairing from the web. Performance: one RPC per call, same as desktop IPC cost.

Checklist

  • This PR is small and focused
  • I explained what changed and why (ELI5, the user-facing before/after, the mechanism, and why over the alternatives)
  • Before/after screenshots or videos attached for UI changes, or N/A with reason
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered (or N/A)
  • pnpm lint, pnpm typecheck, pnpm test, and pnpm build pass (or CI will cover; local preferred)

@espetro

espetro commented Oct 8, 2026

Copy link
Copy Markdown
Contributor Author

@AmethystLiang three small web-client/orcad fixes from running the orcad-served webapp headless:

  1. feat/orcad-help - orcad --help prints usage instead of Unknown argument (the --bind flag was undiscoverable, and it's the one that makes headless orcad reachable over LAN/Tailscale).
  2. fix/web-mobile-api - the paired web client's window.api.mobile was hardcoded stubs (available: false always); now answers from the host's mobile.* RPCs, so the Mobile pane can actually mint a pairing QR (refs [Bug]: Headless serve mode cannot complete mobile pairing — multiple distinct failures #18504).
  3. fix/web-offline-startup - a web client whose paired host is unreachable at boot mounted into no-save degraded mode with a "Session restore failed" lock; now mounts offline with a retry toast and self-reloads on reconnect.

Each branch is off current main and self-contained. Refs the template's issue-linking requirement: #2 links #18504; #1 relates to #13201; #3 has no matching issue yet - happy to file one if preferred.

@coderabbitai

coderabbitai Bot commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

📝 Walkthrough

Walkthrough

The web mobile API now forwards supported mobile operations to runtime RPCs instead of returning fixed empty or unavailable results. It obtains host status through the mobile.hostStatus RPC. WebSocket readiness uses the host endpoint, and relay status reflects the host’s relayAvailable value. Windows firewall and runtime-access-grant operations retain local unsupported or empty results.

Priority: ⬇️ Low

Merge Risk: 🟠 High · up to bed4e

The web app cannot typecheck and build, and the paired web client cannot reach the new mobile host operations. The web build and mobile pairing/device workflows are not ready to merge.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: replacing web mobile API stubs with host RPC responses.
Description check ✅ Passed The description is complete and relevant. It explains the user impact, implementation, rationale, linked issue, visual-proof exception, testing, AI use, compatibility considerations, and checklist sta…
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 1 functions across 1 files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2


ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: a5c6f3bc-6064-49ee-a643-7b09a02aac8e
📥 Commits

Reviewing files that changed from the base of the PR and between 907cb6e and bed4ec0.

📒 Files selected for processing (1)
  • src/renderer/src/web/preload-api/web-mobile-api.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 8 remain after this review.

Comment on lines +2 to +3
import type { MobileHostStatus } from '../../../../shared/mobile-pairing-rpc-contract'
import { callRuntimeResult } from './web-runtime-calls'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🔎 Supported by static analysis

🏁 Script executed:

find src -iname '*mobile*pairing*contract*' -o -iname '*mobile*host*status*'
rg -n 'mobile-pairing-rpc-contract|include|paths|tc:web' tsconfig*.json src/renderer package.json

Repository: stablyai/orca

Length of output: 45651


🏁 Script executed:

set -eu
HEAD=bed4ec0991780702a87a3f77a2ea980bed75dbff

printf '%s\n' '--- reviewed head and target file ---'
git rev-parse --verify "$HEAD^{commit}"
git show "$HEAD:src/renderer/src/web/preload-api/web-mobile-api.ts" | nl -ba | sed -n '1,240p'

printf '%s\n' '--- exact MobileHostStatus references at reviewed head ---'
git grep -n -F -- "$HEAD" MobileHostStatus 2>/dev/null || :
git grep -n -F "$HEAD" -- 'mobile-pairing-rpc-contract' 2>/dev/null || :

printf '%s\n' '--- candidate declarations and path files ---'
git ls-tree -r --name-only "$HEAD" | grep -E '(^|/)(mobile-pairing-rpc-contract|tsconfig[^/]*\.json|package\.json|vite[^/]*|webpack[^/]*)$' || :
for f in tsconfig.json tsconfig.web.json tsconfig.renderer.json package.json; do
  if git cat-file -e "$HEAD:$f" 2>/dev/null; then
    printf '%s\n' "--- $f ---"
    git show "$HEAD:$f" | nl -ba | sed -n '1,180p'
  fi
done

printf '%s\n' '--- web-mobile-api references and web entrypoints ---'
git grep -n -F "$HEAD" -- 'web-mobile-api' 'createWebMobileApi' 'preload-api' 2>/dev/null | sed -n '1,240p'

Repository: stablyai/orca

Length of output: 28535


🏁 Script executed:

set -eu
HEAD=bed4ec0991780702a87a3f77a2ea980bed75dbff

printf '%s\n' '--- all mobile pairing files at reviewed head ---'
git ls-tree -r --name-only "$HEAD" | grep -Ei 'mobile|pairing|rpc-contract' | grep -E '(^|/)([^/]*(mobile|pairing|rpc)[^/]*)\.(ts|tsx|json|mjs|mts)$' | sed -n '1,240p'

printf '%s\n' '--- exact symbol and import references ---'
git grep -n -F 'MobileHostStatus' "$HEAD" -- ':!pnpm-lock.yaml' ':!*.map' || :
git grep -n -F 'mobile-pairing-rpc-contract' "$HEAD" -- ':!pnpm-lock.yaml' ':!*.map' || :

printf '%s\n' '--- resolved relative-import candidates ---'
for p in \
  src/shared/mobile-pairing-rpc-contract.ts \
  src/shared/mobile-pairing-rpc-contract.tsx \
  src/shared/mobile-pairing-rpc-contract/index.ts \
  src/shared/mobile-pairing-rpc-contract.mts \
  src/shared/mobile-pairing-rpc-contract.d.ts
do
  if git cat-file -e "$HEAD:$p" 2>/dev/null; then
    printf '%s\n' "--- $p ---"
    git show "$HEAD:$p" | nl -ba | sed -n '1,180p'
  fi
done

printf '%s\n' '--- web typecheck configs ---'
for p in config/tsconfig.tc.web.json config/tsconfig.web.json vite.web.config.ts config/scripts/project-renderer-web-client.mjs config/scripts/run-vite-web-build.mjs config/scripts/verify-web-build.mjs; do
  if git cat-file -e "$HEAD:$p" 2>/dev/null; then
    printf '%s\n' "--- $p ---"
    git show "$HEAD:$p" | nl -ba | sed -n '1,260p'
  fi
done

printf '%s\n' '--- target-file references in source/config ---'
git grep -n -F 'createWebMobileApi' "$HEAD" -- ':!pnpm-lock.yaml' ':!*.map' || :
git grep -n -F 'web-mobile-api' "$HEAD" -- ':!pnpm-lock.yaml' ':!*.map' || :
git grep -n -F 'src/renderer/src/web' "$HEAD" -- '*.json' '*.ts' '*.tsx' '*.mjs' '*.js' || :

Repository: stablyai/orca

Length of output: 35108


🏁 Script executed:

set -eu
HEAD=bed4ec0991780702a87a3f77a2ea980bed75dbff
for p in src/renderer/web-index.html src/renderer/src/web/main.tsx src/renderer/src/web/web-preload-api.ts; do
  printf '%s\n' "--- $p ---"
  git show "$HEAD:$p" | nl -ba | grep -E 'web-preload-api|script|createWebMobileApi|export|from ' | sed -n '1,120p'
done

Repository: stablyai/orca

Length of output: 4633


Add the missing MobileHostStatus contract.

The relative import has no target in the reviewed tree. The web typecheck includes this file, and the web entrypoint imports it through web-preload-api.ts. This unresolved module blocks the web typecheck and build. Add the checked-in contract or update the import to the actual contract path.

Comment on lines +11 to +21
const hostStatus = () => callRuntimeResult<MobileHostStatus>('mobile.hostStatus')
return {
mobile: {
listNetworkInterfaces: () => Promise.resolve({ interfaces: [] }),
getPairingQR: () => Promise.resolve({ available: false }),
listNetworkInterfaces: () => callRuntimeResult('mobile.listNetworkInterfaces'),
getPairingQR: (args) => callRuntimeResult('mobile.getPairingQR', args),
getWindowsFirewallStatus: () => Promise.resolve({ supported: false }),
repairWindowsFirewall: () => Promise.resolve({ ok: false, reason: 'unsupported' }),
openWindowsNetworkSettings: () => Promise.resolve(false),
getRuntimePairingUrl: () => Promise.resolve({ available: false }),
listDevices: () => Promise.resolve({ devices: [] }),
revokeDevice: () => Promise.resolve({ revoked: false }),
getRuntimePairingUrl: (args) => callRuntimeResult('mobile.getRuntimePairingUrl', args),
listDevices: () => callRuntimeResult('mobile.listDevices'),
revokeDevice: (args) => callRuntimeResult('mobile.revokeDevice', args),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🔎 Supported by static analysis

🏁 Script executed:

rg -n 'register.*mobile|MOBILE_WEB_BUNDLE_METHODS|mobile\.(hostStatus|listNetworkInterfaces|getPairingQR|getRuntimePairingUrl|listDevices|revokeDevice)' src/main/runtime src/main
sed -n '1,140p' src/main/runtime/rpc/methods/index.ts
sed -n '1,240p' src/main/runtime/runtime-rpc/runtime-rpc-mobile-method-allowlist.ts

Repository: stablyai/orca

Length of output: 15743


🏁 Script executed:

set -o pipefail
printf '%s\n' '--- changed web API ---'
nl -ba src/renderer/src/web/preload-api/web-mobile-api.ts | sed -n '1,90p'
printf '%s\n' '--- runtime call dispatch ---'
rg -n -F --glob '*.ts' -- 'callRuntimeResult' src/renderer/src/web src/main src/preload
rg -n -F --glob '*.ts' -- 'new RpcDispatcher' src/main/runtime src/main/startup
printf '%s\n' '--- runtime registration assembly ---'
rg -n -F --glob '*.ts' -- 'ALL_RPC_METHODS' src/main
rg -n -F --glob '*.ts' -- 'MOBILE_WEB_BUNDLE_METHODS' src/main/runtime
rg -n -F --glob '*.ts' -- 'registerMobileHandlers' src/main
printf '%s\n' '--- relevant runtime and IPC definitions ---'
nl -ba src/renderer/src/web/preload-api/web-runtime-calls.ts | sed -n '1,115p'
nl -ba src/main/runtime/rpc/methods/mobile-web-bundle.ts | sed -n '145,190p'
nl -ba src/main/startup/main-process-runtime-launch.ts | sed -n '1,100p'
nl -ba src/main/ipc/mobile.ts | sed -n '50,90p'
nl -ba src/main/ipc/mobile.ts | sed -n '240,275p'

Repository: stablyai/orca

Length of output: 41651


🏁 Script executed:

set -o pipefail
printf '%s\n' '--- runtime server registration ---'
rg -n -F --glob '*.ts' -- 'ALL_RPC_METHODS' src/main/runtime src/main/startup
rg -n -F --glob '*.ts' -- 'new RpcDispatcher' src/main/runtime src/main
rg -n -F --glob '*.ts' -- 'methods:' src/main/runtime/runtime-rpc src/main/runtime | head -40
printf '%s\n' '--- web mobile API consumer ---'
rg -n -F --glob '*.ts' -- 'createWebMobileApi' src/renderer src/main src/preload
printf '%s\n' '--- runtime server constructor ---'
rg -n -F --glob '*.ts' -- 'class OrcaRuntimeRpcServer' src/main/runtime

Repository: stablyai/orca

Length of output: 41186


Register the mobile methods on the runtime RPC boundary.

createWebMobileApi is included in web-preload-api.ts, so paired web clients can invoke all six methods. Those calls use callRuntimeResult, but the production dispatcher defaults to ALL_RPC_METHODS, which contains no matching mobile methods. registerMobileHandlers only creates mobile:* Electron IPC handlers. The paired web calls therefore fail before reaching the existing host logic. Add runtime registrations for all six methods and preserve their current arguments and result shapes.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant