Repository navigation
fix(mobile): detect agents inside the workspace's WSL distro (New Tab + New Workspace) - #20101
welldoingk wants to merge 4 commits into
Conversation
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. 📝 WalkthroughWalkthroughMobile local agent detection now derives an optional WSL distro from the repository or worktree path and passes it with the Priority: ➖ Normal Severity of issue fixed: Medium Merge Risk: 🟡 Moderate · up to Switching repositories can briefly offer an agent unavailable in the newly selected WSL distro. Invalidate the old agent list before merging; the SSH test should also catch unintended local detection calls. 🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
Full details: Out of Scope Changes checkExplanation The pull request also changes
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Advanced
Run ID: cb90ae8a-b889-4bf5-9f01-caebd899da7b
📥 Commits
Reviewing files that changed from the base of the PR and between a0799d8 and 89233b9a90b14418105ae8eaa44e007d3f62e20a.
📒 Files selected for processing (8)
mobile/src/components/NewWorktreeModal.test.tsxmobile/src/components/NewWorktreeModal.tsxmobile/src/components/use-new-workspace-execution-target.test.tsxmobile/src/components/use-new-workspace-execution-target.tsmobile/src/tasks/mobile-tasks-refactor-parity.test.tsmobile/src/tasks/use-mobile-tasks-workspace-ssh-state.tsxmobile/src/worktree/workspace-agent-detection-target.test.tsmobile/src/worktree/workspace-agent-detection-target.ts
Included review availability: Your plan provides up to 10 included reviews per hour; 9 remain after this review.
| repoPath: '\\\\wsl.localhost\\Ubuntu-24.04\\home\\dev\\project' | ||
| }) | ||
|
|
||
| expect(client.sendRequest).not.toHaveBeenCalledWith('preflight.detectAgents', expect.anything()) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- target file ---'
cat -n mobile/src/components/use-new-workspace-execution-target.test.tsx | sed -n '1,130p'
printf '%s\n' '--- matcher/import context ---'
rg -n -C 3 "sendRequest|detectAgents|detectRemoteAgents|expect\\.anything|vitest|jest" mobile/src/components/use-new-workspace-execution-target.test.tsx mobile/package.json package.json 2>/dev/null || trueRepository: stablyai/orca
Length of output: 11358
🤖 get_repo_knowledge executed:
get_repo_knowledge stablyai/orca /tmp/coderabbit-repo-knowledge/stablyai-orca-89dc44e4/architecture /tmp/coderabbit-repo-knowledge/stablyai-orca-89dc44e4/learnings /tmp/coderabbit-repo-knowledge/stablyai-orca-89dc44e4/conventions
Length of output: 46341
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- hook implementation ---'
cat -n mobile/src/components/use-new-workspace-execution-target.tsx
printf '%s\n' '--- remote detection references ---'
rg -n -C 4 "detectRemoteAgents|detectAgents" mobile src/shared src 2>/dev/null | head -200Repository: stablyai/orca
Length of output: 272
🏁 Script executed:
#!/bin/bash
set -eu
file="$(fd -t f -i 'use-new-workspace-execution-target' . | head -1)"
if [ -z "$file" ]; then
printf '%s\n' 'hook file not found'
exit 2
fi
printf '--- hook implementation: %s ---\n' "$file"
cat -n "$file"
printf '%s\n' '--- remote detection references ---'
rg -n -C 4 "detectRemoteAgents|detectAgents" "$(dirname "$file")" mobile/src 2>/dev/null | head -200Repository: stablyai/orca
Length of output: 288
🏁 Script executed:
#!/bin/bash
set -eu
file="$(fd -t f -i 'use-new-workspace-execution-target' . | head -1)"
if [ -z "$file" ]; then
echo 'hook file not found'
exit 2
fi
printf '%s\n' "--- hook implementation: $file ---"
cat -n "$file"
printf '%s\n' '--- remote detection references ---'
rg -n -C 4 "detectRemoteAgents|detectAgents" "$(dirname "$file")" mobile/src 2>/dev/null | head -200Repository: stablyai/orca
Length of output: 20704
🌐 Web query:
Vitest toHaveBeenCalledWith expect.anything undefined omitted arguments arity official documentation
💡 Result:
In Vitest, the behavior of toHaveBeenCalledWith regarding undefined arguments and asymmetric matchers is influenced by its Jest-compatible equality implementation, which historically relies on toEqual rather than toStrictEqual [1][2][3]. Key findings regarding your query: 1. expect.anything() behavior: The official Vitest documentation defines expect.anything() as a matcher that matches any value except null or undefined [4]. Therefore, it cannot be used to specifically match undefined arguments in a mock call [5]. 2. Handling undefined arguments: Due to internal implementation details where toHaveBeenCalledWith uses toEqual equality logic, there are known limitations when mixing undefined with asymmetric matchers [1][2]. Specifically, toEqual may skip undefined elements in arrays or properties, causing some asymmetric matchers to fail when used to validate an undefined argument [1]. 3. Arity and omitted arguments: Vitest's toHaveBeenCalledWith is sensitive to the number of arguments (arity). If a function is called with undefined, expect(spy).toHaveBeenCalledWith(undefined) will pass [6]. However, omitting the argument entirely (e.g., expect(spy).toHaveBeenCalledWith()) will not match a call made with undefined [6]. Conversely, if a function is called without arguments, expect(spy).toHaveBeenCalledWith() passes, but expect(spy).toHaveBeenCalledWith(undefined) fails [6]. 4. Recommended approach for robustness: To verify arguments that are undefined or null-adjacent, you should avoid relying on broad matchers like expect.anything(). Instead: - Use literal undefined or null in your assertions when you expect those specific values [6]. - For more complex validation where you need to allow multiple possibilities, consider expect.toBeOneOf([undefined, null]) (if using additional utility libraries) or custom matchers [1][6][5]. - Because toHaveBeenCalledWith uses loose equality, be aware that it might match calls that toStrictEqual would reject (e.g., objects missing a key vs. having a key set to undefined) [3]. If strict argument validation is required, you may need to inspect spy.mock.calls directly and perform your own assertions [7][3]. Official sources confirm that expect.anything() is explicitly documented to exclude undefined and null [4], and issues regarding the interaction between asymmetric matchers and undefined are documented as limitations of the underlying equality algorithm [1][2].
Citations:
- 1: GitHub issue 7573 in vitest-dev/vitest (link omitted to avoid creating a cross-reference)
- 2: https://github.com/vitest-dev/vitest/blob/206e8cff/packages/expect/src/jest-expect.ts
- 3: GitHub issue 8386 in jestjs/jest (link omitted to avoid creating a cross-reference)
- 4: https://vitest.dev/api/expect
- 5: https://stackoverflow.com/questions/47027011/using-expect-anyobject-or-expect-anything-does-not-work-to-match-undefi
- 6: https://github.com/vitest-dev/vitest/blob/9423dc08/test/core/test/jest-expect.test.ts
- 7: https://vitest.dev/guide/learn/mock-functions.html
Assert the remote detection request and reject every local detection call.
expect.anything() excludes undefined and null, but omitted arguments do not match because toHaveBeenCalledWith checks call arity. Inspect the recorded method names directly, and assert the preflight.detectRemoteAgents call required by the truthy connectionId branch.
Proposed test update
- expect(client.sendRequest).not.toHaveBeenCalledWith('preflight.detectAgents', expect.anything())
+ expect(
+ client.sendRequest.mock.calls.some(([method]) => method === 'preflight.detectAgents')
+ ).toBe(false)
+ expect(client.sendRequest).toHaveBeenCalledWith('preflight.detectRemoteAgents', {
+ connectionId: 'ssh-1'
+ })📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| expect(client.sendRequest).not.toHaveBeenCalledWith('preflight.detectAgents', expect.anything()) | |
| expect( | |
| client.sendRequest.mock.calls.some(([method]) => method === 'preflight.detectAgents') | |
| ).toBe(false) | |
| expect(client.sendRequest).toHaveBeenCalledWith('preflight.detectRemoteAgents', { | |
| connectionId: 'ssh-1' | |
| }) |
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
This PR names the WSL distro on the two mobile "create workspace" flows that run before a workspace exists, matching what the New Tab drawer already does (#19886). It is the client half of a two-part fix: safe to land on its own, effective once the host accepts the parameter.
getWorkspaceDetectAgentsParams— new helper derives{ wslDistro }from a repo path via the sharedparseWslUncPath, returningundefinedfor non-WSL paths so the request stays byte-identical to today's.useNewWorkspaceExecutionTarget— takes an optionalrepoPathand names the distro on the local branch only; the SSH branch (preflight.detectRemoteAgents) and the connection gate are untouched.NewWorktreeModal— forwardsselectedRepo?.path.useMobileTasksWorkspaceSshState— the tasks workspace-create draft forwardsworkspaceCreateTargetRepo.path.- Tests — new cases cover both
\\wsl.localhost\…and\\wsl$\…spellings, non-WSL/missing paths, the SSH branch never reachingpreflight.detectAgents, and three parity ratchet hashes refreshed (lengths unchanged, so the change is content-only).
I verified the two load-bearing claims in the description. Host-side preflight.detectAgents still declares params: null (src/main/runtime/rpc/methods/preflight.ts), and parseRpcRequestParams returns { value: undefined } for null-schema methods, discarding any incoming params — so sending wslDistro before #19886 lands is inert, never an error. Distro extraction mirrors the desktop's getWslDistroFromPath (src/renderer/src/lib/local-preflight-context.ts), which is the same parseWslUncPath(path)?.distro derivation, and the wslDistro field name is already established on PreflightRuntimeContext / getPreflightWslTarget.
DeepSeek Pro (free via Pullfrog for OSS) | 𝕏
89233b9 to
8a06eff
Compare
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
This run reviewed the commits added since the prior pullfrog review, which turned a client-only change into a three-layer fix: the host runtime RPC now accepts a WSL detection target, the paired web client forwards its preflight context, and the mobile New Tab loader reads the distro from the workspace's own path.
- Host runtime RPC accepts a WSL target —
preflight.detectAgentsandpreflight.refreshAgentsmove fromparams: nullto the new optionalPreflightDetectAgentsschema (wslDistrotrimmed 1..255,wslDefault), mapped bytoPreflightRuntimeContextso an unnamed target still means host-local. - Paired web client forwards its context —
toPreflightDetectAgentsParamsflattens the preloadPreflightRuntimeContextonto the wire with the host's own precedence (resolved WSL runtime, then explicit distro, then default flag), and both methods skip the probe for arepair-requiredruntime instead of falling back to the Windows PATH. - Mobile New Tab loader names the distro — derives it from the worktree id's UNC path via a new
getWorktreePathFromMobileWorktreeId, keeping SSH repos on the remote branch. - Wire compatibility verified — old host ignores the new fields (
params: nulldiscards them), new host parses missing params as{}; the{ commands }callers target the separate relay handler and are unaffected. Recording goldens do not move because the mount adapters never supply a repo path.
ℹ️ PR description predates the host-side change
The description still says "no schema change here" and that the fix "depends on #19886 for effect", but this branch now contains the host-side PreflightDetectAgents schema, the PREFLIGHT_METHODS binding, the regenerated catalog, and the web preload forwarding. A reviewer or merger reasoning about wire safety from the description alone would conclude this is inert until another PR lands, which is no longer true.
Technical details
# PR description no longer matches the diff
## Affected sites
- `src/shared/rpc-contract/preflight-params.ts` — adds `PreflightDetectAgents` (the description claims no schema change).
- `src/main/runtime/rpc/methods/preflight.ts` — binds it to `preflight.detectAgents` / `preflight.refreshAgents` (previously `params: null`).
- `src/renderer/src/web/preload-api/web-host-capability-api.ts` — web client forwarding (the description scopes the change to mobile only).
## Required outcome
- The description reflects that this PR ships both halves: the host runtime-RPC parameter and the mobile/web clients that send it, so merge-order reasoning ("depends on #19886", "inert until the host change ships") is accurate.
## Open questions for the human (optional)
- Is #19886 still expected to land separately with its own host changes, or has its scope been folded into this PR? The title and body both still read as mobile-scoped.DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
The Electron IPC handler for agent detection takes a PreflightRuntimeContext, so a desktop WSL workspace probes inside its distro. The runtime RPC twin that paired clients call declared `params: null`, so a Windows host always answered from its own PATH -- where a WSL-only install is absent and the mobile New Tab drawer shows "No Enabled Agents". Accept the same optional WSL target on preflight.detectAgents/refreshAgents, and have the mobile new-tab loader read the distro from the workspace's own UNC path. Hosts that predate the parameter ignore it and keep answering host-local.
The web twin of the preload preflight API took no arguments, so the runtime
context its callers pass -- `refreshAgents(context)` in the local detected-agent
store and `detectAgents({ wslDistro })` in skill install -- was dropped before the
request left the client. A zero-arg function is assignable to the optional-arg
signature, so this never failed typecheck. Forwarding the raw context would
regress instead: it carries `wslDistro: null` and `projectRuntime`, and the wire
schema rejects null. `toPreflightDetectAgentsParams` maps it with the host's own
`getPreflightWslTarget` precedence.
Also regenerates the RPC params catalog, stale since the schemas moved into
`src/shared/rpc-contract/`, which `verify:rpc-params-catalog` gates.
Flattening a repair-required project runtime to no-target sent the paired web client to the Windows PATH for a WSL-pinned project, caching launchable-looking host agents as a successful detect. The desktop twin refuses that probe, so skip the request and answer with the empty fallback instead.
The New Tab drawer learned to tell the host which distro to probe, but the two surfaces that pick agents *before* a worktree exists still asked with no target, so a Windows host answered from its own PATH and a WSL-first user saw an empty agent list in New Workspace. Both already hold the repo they will create from, so the distro comes from that repo's path. Refreshes the Mobile Tasks parity hashes, which cover the tasks source tree.
8a06eff to
a1708ff
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Advanced
Run ID: b8ae3048-06ed-4bb3-a3ef-78f874109a84
📥 Commits
Reviewing files that changed from the base of the PR and between 8a06effad9b8418d07089ef1ad7d1196b54906c9 and a1708ff.
⛔ Files ignored due to path filters (1)
src/shared/rpc-contract/rpc-params-catalog.generated.tsis excluded by!**/*.generated.*
📒 Files selected for processing (3)
mobile/src/components/use-new-workspace-execution-target.tsmobile/src/tasks/mobile-tasks-refactor-parity.test.tsmobile/src/tasks/use-mobile-tasks-workspace-ssh-state.tsx
Included review availability: Your plan provides up to 10 included reviews per hour; 9 remain after this review.
| stale = true | ||
| } | ||
| }, [client, connectionId, sshGate.status, visible]) | ||
| }, [client, connectionId, repoPath, sshGate.status, visible]) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Invalidate detected agents when repoPath changes.
If a user switches between two local WSL repositories, the effect requests detection for the new path, but detectedAgentIds continues to expose the previous repository’s agents until that request finishes. The modal can then offer an agent that is unavailable in the selected distro. Include the detection target in the stored state and gate the returned IDs on that target while detection is pending.
Pass the selected repository to New Workspace agent detection through the host-authoritative runtime resolver, including WSL paths and Windows project preferences. Preserve old-client request shape, SSH routing, and strict ownership/refusal behavior. This merge credits @welldoingk’s original WSL agent-detection investigation and community PR #20101, adapted to the current host-resolved contract. Co-authored-by: welldoingk <thkwon96@gmail.com> Fixes #20060

ELI5
If your workspaces live inside WSL, your phone's New Tab drawer said "No Enabled Agents" — not even Claude or Codex — while the desktop app listed them all for the same workspace. The desktop knows how to look inside the distro because it tells the detector which distro to check; the phone had no way to say that, so the host looked at the Windows side of the machine, where a WSL-first user has no agents installed. Now the request can name the distro, and hosts that never heard of that field keep behaving exactly as before. The paired web client had the same hole one layer up — its copy of the preflight API took no arguments at all, so the distro its own callers already passed was thrown away before the request left the browser — and that is fixed here too.
What Changed
src/main/runtime/rpc/methods/preflight.ts:preflight.detectAgentsandpreflight.refreshAgentsswapparams: nullfor an optional schema carryingwslDistro/wslDefault, mapped to thePreflightRuntimeContextthe Electron IPC twin already accepts. No target named still means host-local, sogetPreflightWslTargetkeeps distinguishing "no context" from "context that selected nothing". ThewslDistroconstraint copies the oneaccounts.tsalready uses for the same value.mobile/src/session/mobile-new-tab-agent-loader.ts: the local branch reads the distro out of the workspace's own path and sends it. SSH/connection resolution is untouched.mobile/src/session/mobile-session-route-helpers.ts:getWorktreePathFromMobileWorktreeId, the path half of the existinggetRepoIdFromMobileWorktreeId. Parsing reuses sharedparseWslUncPathrather than a second copy.src/renderer/src/web/preload-api/web-host-capability-api.ts: the web twin of the preload API declareddetectAgents: async () =>andrefreshAgents: () =>, so the context its callers pass —refreshAgents(context)inlocal-detected-agent-state.tsanddetectAgents({ wslDistro })in skill install — was dropped before the call. A zero-arg function is assignable to the optional-arg signature, so this never failed typecheck. Both now accept and forward it.src/renderer/src/web/preload-api/web-preflight-detect-params.ts(new): forwarding the raw context would regress rather than fix, because it carrieswslDistro: string | nullplusprojectRuntime, and the wire schema rejectsnull. This maps it with the host's owngetPreflightWslTargetprecedence (resolved project runtime, then explicit distro, then the default flag), andisPreflightRepairRequiredskips the probe entirely for a runtime that needs repair, matching the desktop's refusal instead of listing Windows-side agents a WSL-pinned project cannot launch.src/shared/rpc-contract/rpc-params-catalog.generated.ts: regenerated, since the new schema lives in the directorygenerate-rpc-params-catalog.mjsscans andverify:rpc-params-cataloggates it.New Workspace surfaces (was #20101)
mobile/src/worktree/workspace-agent-detection-target.ts(new):getWorkspaceDetectAgentsParams(repoPath)turns a repo path into the detection target, reusing the sharedparseWslUncPath. A non-WSL path yieldsundefined, so the request stays byte-identical to today's.mobile/src/components/use-new-workspace-execution-target.ts: takes an optionalrepoPathand names the distro on the local branch. The SSH branch (preflight.detectRemoteAgents) and the connection gate are untouched.mobile/src/components/NewWorktreeModal.tsx: passesselectedRepo?.path, the repo the modal already selected.mobile/src/tasks/use-mobile-tasks-workspace-ssh-state.tsx: the workspace-create draft usesworkspaceCreateTargetRepo.path, which the hook already holds.mobile/src/tasks/mobile-tasks-refactor-parity.test.ts: three parity hashes refreshed, since that ratchet covers the wholemobile/src/taskssource tree. Same maintenance Preserve user-set workspace names across branch changes #17448 did for a behavioural change.getPreflightWslTarget(undefined)returnsnullon win32, so a request with no target probes the Windows PATH. The New Tab loader could read the distro out of an existing worktree id (${repoId}::${path}); these two surfaces run before that id exists, which is exactly why they were left out of #19886 and filed separately.Why
The two entry points into agent detection are twins, but only one could be told where to look:
getPreflightWslTarget(undefined)returnsnullon win32, so the probe ran against the Windows PATH. On a WSL-first machine that PATH holds no agent CLI, so the drawer was empty. The desktop never hit this becausegetLocalAgentPreflightContextderives the distro from the workspace path.Reading the distro from the route rather than adding a resolver: a worktree id is
${repoId}::${path}, the path is the host's own spelling of the workspace, andparseWslUncPathis the existing answer for "which distro is this path in". A non-WSL path yieldsnulland the call goes out with no params, unchanged.Linked Issue
Fixes #19885
Fixes #20060
Visual Proof
Android client paired to a Windows host, WSL workspace, New Tab drawer. Both shots keep the workspace header, the tab bar, and the drawer; the terminal area between them is cropped out because it was showing unrelated session content.
BEFORE — stock client + host: the agent section is a single disabled "No Enabled Agents" row, while Terminal / Browser / Markdown Note below it work fine.
AFTER — patched client + host: Claude, Codex, OpenCode, Antigravity, Hermes. Gemini and Pi are absent because this profile has them in
disabledTuiAgents, which is the correct filtering, not a miss.Testing
Verified on a real device against a real host, not only in unit tests.
1. Live host RPC, stock vs patched, same machine, same minute. Both apps were queried over their own runtime named pipe (the transport the bundled CLI uses):
[][]{"wslDistro":"Ubuntu-24.04"}[]["claude","codex","opencode","pi","gemini","antigravity","hermes"](824ms){"wslDistro":"NoSuchDistro-xyz"}[][](88ms){"wslDistro":""}[]invalid_argument: Too small: expected string to have >=1 charactersRow 2 on the left is the bug; row 2 on the right is the fix, and its 824ms is a real
wsl.exeround trip. Row 3 is the discriminator: a distro that does not exist answers nothing, so the parameter really does route the probe into WSL instead of silently falling back to the host. Row 4 shows the guard is live. The stock column also shows why this is wire-safe in the old direction:params: nullmakes the dispatcher discard request params, so a new client against an old host is exactly today's behavior, never an error.2. On device. Android release build of this branch installed on a phone, paired to a Windows host running this branch, WSL workspace — see Visual Proof above.
3. Automated.
main(122b8c2): mobilesrc/tasks src/components src/worktree src/session— 3106 passed; desktoppreflight.test.ts+web-preload-api— 16 passed;verify:rpc-params-catalogclean. Mobile Tasks parity: only the hook and statement hashes moved (the local detection send gains a params argument); counts unchanged.Original runs:
pnpm exec vitest run --config config/vitest.config.ts src/main/runtime/rpc/methods/preflight.test.ts— 6 passed (3 new)cd mobile && vitest run src/session/mobile-new-tab-agent-loader.test.ts— 4 passed (2 new)pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/web/preload-api— 11 passed (10 new)src/main/runtime/rpc/**182 files / 1702 passed; mobilesrc/session/**156 files / 1479 passed;src/renderer/src/store/slices+ preflight RPC 327 files / 3149 passedpnpm run verify:rpc-params-catalog— clean (this is the gate the regenerated catalog answers)pnpm run tc:node,pnpm run tc:web, mobiletsc --noEmit— cleanoxlint,oxfmt --check,check:code-quality:changed,check:max-lines-ratchet— clean, 0 new findingsPlatforms actually exercised: Windows host + WSL guest + Android client. macOS and Linux are unaffected —
getPreflightWslTargetreturnsnulloff win32, so the new field is inert there.AI Disclosure
Written with Claude Opus 4.5 (via OmO). Every claim above is from a command run in this session; the two screenshots are from the reporter's own phone.
Review
getPreflightWslTarget, which returnsnullunlessprocess.platform === 'win32'. macOS and Linux hosts ignore it. No path, shortcut, or shell assumption added.docs/reference/remote-wire-compatibility.mdRule 1 this is a new optional field on an existing method, and no reader requires it. New client + old host: the old host declaresparams: null, soparseRpcRequestParamsdiscards params and answers host-local — the current behavior, not a failure. Old client + new host: absent params parse to{}, every field optional, contextundefined, same code path as before. No protocol version bump. The SSH branch (preflight.detectRemoteAgents) is untouched, and its resolution still wins over the new one.repair-requiredproject runtime cannot be expressed on the wire, and the desktop twin throws instead of probing. The web client therefore sends no request at all and answers with its empty fallback, which is what that user already sees on desktop — never a host-local list that looks launchable.wslDistrocrosses a trust boundary from a paired client, so it is validated at the boundary —z.string().trim().min(1).max(255), matching the existing constraint on the same value inaccounts.ts. It reacheswsl.exeas an argv element viarunWslProcess, never as shell text, and an unknown distro yields an empty result rather than a fallback to another host. A malformed value is rejected before any process spawns.Agent skill upstream boundary
Notes
Adjacent but distinct, and deliberately left alone:
hostId: "local", so it lands on the local branch either way; the two changes compose.preflight.refreshAgentsis included because it is the same signature and the same miss; leaving it would make Refresh silently disagree with the list it refreshes.Checklist