[Bug][Mobile]: New-tab agent menu resolves detection from repo.connectionId only, so runtime-hosted workspaces list the paired host's agents
- Dominant language
- TypeScript
- Stars
- 72.1k
- Forks
- 4.7k
- Avg merge
- 14h 54m
- Merged PRs (30d)
- 520
Description
## Summary
The mobile new-tab agent menu resolves the agent-detection host from `repo.connectionId` only. Runtime-hosted repos have `connectionId: null`, so they fall through to `preflight.detectAgents`, which runs on **the paired host** rather than on the runtime environment that owns the workspace. The menu then lists the paired host's installed agents; agents installed only on the runtime host never appear, and when the paired host has no agents at all the section is empty.
This is the mobile counterpart of #9370, which was fixed for desktop by #9790 (`AgentsPane` + `QuickLaunchButton`). The mobile loader was never extended, so it still has the SSH-only resolution that #9790 replaced.
Code references are from `main` @ `c065a2d` (2026-08-10).
## Where it goes wrong
`mobile/src/session/mobile-new-tab-agent-loader.ts` declares a repo shape that cannot express runtime ownership:
```ts
// :11-14
type RuntimeRepoSummary = {
id: string
connectionId?: string | null
}
```
and then branches on that field alone:
```ts
// :58-61
const connectionId = repo.connectionId?.trim() || null
return connectionId
? client.sendRequest('preflight.detectRemoteAgents', { connectionId })
: client.sendRequest('preflight.detectAgents') // runtime-hosted repos land here
```
`repo.list` returns full `Repo` rows (`src/main/runtime/rpc/methods/repo.ts:109-114` → `runtime.listRepos()`), so the field that disambiguates this case is already on the wire — and its doc comment names this exact failure mode:
```ts
// src/shared/types.ts:269-273
/**
* Explicit execution owner for this repo. Runtime-host repos need this
* because they otherwise look identical to local repos (`connectionId: null`).
*/
executionHostId?: 'local' | `ssh:${string}` | `runtime:${string}` | null
```
Desktop resolves all three owners from that field:
```ts
// src/renderer/src/hooks/useAgentDetectionTarget.ts:53-60
const executionHost = parseExecutionHostId(getExecutionHostIdForWorktree(state, worktreeId))
if (executionHost?.kind === 'ssh') {
return `ssh:${executionHost.targetId}`
}
if (executionHost?.kind === 'runtime') {
return `runtime:${executionHost.environmentId}`
}
return AGENT_DETECTION_LOCAL_TARGET_KEY
```
The mobile loader has no `runtime` branch, and the fallback is silent — there is no signal distinguishing "the runtime host reports no agents" from "we asked the wrong host".
## Steps to reproduce
1. Run `orca serve` on a headless host **B** and install an agent there that is **not** installed on your desktop **A** (e.g. `pi`).
2. On desktop **A**, register **B** as a Remote Orca Server environment and open a workspace that lives on **B**.
3. Pair Orca Mobile (Android or iOS) to **A**.
4. In the mobile app, open that workspace and tap `+` to open the new-tab drawer.
**Actual:** the agent section lists **A**'s installed agents. `pi` (installed only on **B**) is absent. If **A** has no agents installed, the section is empty.
**Expected:** the agent section lists the agents installed on **B**, matching what desktop **A** shows for the same workspace after #9790.
## Note on the fix
The loader cannot fix this on its own. `preflight.detectAgents` takes no params (`src/main/runtime/rpc/methods/preflight.ts:28-31`, `params: null`) and mobile's `RpcClient` addresses only the single paired host (`mobile/src/transport/rpc-client.ts:972`), so there is currently no way for mobile to target a specific runtime environment. Desktop sidesteps this because its renderer dials each environment directly (`src/renderer/src/store/slices/runtime-detected-agents.ts:69-72` via `callRuntimeRpc({ kind: 'environment', environmentId })`).
Making mobile correct therefore needs a host-side delegation step. The transport already exists — `callRuntimeEnvironment(userDataPath, selector, method, params, ...)` (`src/main/ipc/runtime-environment-transport-routing.ts:78`) — but `rpc/methods/` does not currently perform environment delegation anywhere (no `userDataPath` usage in that layer), so which layer should own it, how to prevent delegation loops, and whether a mobile client may name an arbitrary environment all look like maintainer calls rather than something to settle in a drive-by patch. Filing this instead of a PR for that reason.
Two smaller things worth folding in whatever the shape of the fix:
- `executionHostId` should be added to `RuntimeRepoSummary` and preferred over `connectionId`, so the resolution cannot drift from desktop again.
- Any failure in the loader currently blanks the whole section: `loadMobileNewTabAgentOptions` throws on a `repo.list`/`settings.get` failure or on `worktree_repo_not_found` (`mobile-new-tab-agent-loader.ts:29-34, 55-57`), and the call site maps that to an empty list (`mobile/app/h/[hostId]/session/[worktreeId].tsx:3747-3751`). A wrong-host result and a hard failure are indistinguishable to the user.
Existing coverage in `mobile/src/session/mobile-new-tab-agent-loader.test.ts` has only the floating-workspace (local) and SSH cases — a runtime-owned case would be the natural regression test.
## Related
- #9370 — desktop new-tab menu listed local agents for runtime-hosted workspaces (closed as a duplicate of a consolidated investigation)
- #9790 — fixed the desktop surfaces (`AgentsPane`, `QuickLaunchButton`)
- #9373 — open PR extracting the shared desktop resolution hook
- #3290 / #3334 — added agent presets to the mobile new-tab drawer in the first place
- #11869 — mobile not surfacing remote runtime workspaces (same "mobile can't see the runtime host's data" family)
Contributor guide
Assessment
This issue has not been assessed yet.