feat(agents): chooser for multiple detected binary paths#1050
feat(agents): chooser for multiple detected binary paths#1050pedramamini wants to merge 2 commits into
Conversation
Detection now returns every valid installation it finds (Homebrew + nvm + npm-global + custom wrappers like codex-multi-auth-codex), de-duplicated by canonical path. When more than one is present, the Path field in the agent config panel renders a dropdown so the user can pick which binary to use; the selection is persisted via the existing customPath store and becomes the default for future agents. Closes #1048
📝 WalkthroughWalkthroughThis PR adds multi-path binary discovery for agents. It refactors path-prober to enumerate all valid installation locations (not just the first), propagates those paths through type definitions, integrates discovery into agent detection, and provides a UI dropdown for users to choose which installation to use when multiple are found. ChangesMulti-path agent discovery and selection
Sequence DiagramsequenceDiagram
participant Detector as Agent Detector
participant Prober as findAllBinaryPaths
participant DirectProbe as Direct Probes<br/>(Windows/Unix paths)
participant ShellLookup as which/where<br/>(expanded PATH)
participant Realpath as fs.realpath<br/>(dedup)
participant Config as AgentConfig
Detector->>Prober: findAllBinaryPaths('codex')
Prober->>DirectProbe: probeWindowsPathsAll() or probeUnixPathsAll()
DirectProbe-->>Prober: [path1, path2, ...]
Prober->>ShellLookup: execFileNoThrow('which/where', ['-a', 'codex'])
ShellLookup-->>Prober: path3, path4, ...
Prober->>Realpath: resolve each to canonical path
Realpath-->>Prober: [deduped paths in order]
Prober-->>Detector: [all_unique_paths]
Detector->>Config: allPaths = all_unique_paths
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested labels
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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 |
Greptile SummaryThis PR adds a binary-path chooser for agents that have multiple valid installations detected on the host. Detection now enumerates all paths (direct probes +
Confidence Score: 3/5Safe to merge for users who won't use the chooser in the Wizard or Director's Notes; the stale-state bug silently breaks persistence in those two contexts. The chooser's onChange handler calls the blur callback synchronously right after scheduling a React state update, so AgentSelectionScreen and EncoreTab will call setCustomPath / setDirectorNotesSettings with the previously-selected path rather than the one the user just picked. The user sees the right value in the UI but the wrong one gets persisted — a silent data-integrity failure on the primary feature path. Focus on AgentConfigPanel.tsx (the chooser onChange handler) and its callers AgentSelectionScreen.tsx and EncoreTab.tsx, where the blur callback reads stale state. Important Files Changed
Sequence DiagramsequenceDiagram
participant Detector as AgentDetector
participant Prober as path-prober
participant Shell as which -a / where
participant UI as AgentConfigPanel
participant IPC as agents:setCustomPath
Detector->>Prober: findAllBinaryPaths(binaryName)
Prober->>Prober: probeUnixPathsAll / probeWindowsPathsAll
Prober->>Shell: which -a binaryName
Shell-->>Prober: list of paths
Prober->>Prober: deduplicate via fs.realpath
Prober-->>Detector: string[] (all paths)
Detector->>Detector: prepend active path if not in list
Detector-->>UI: AgentConfig.allPaths
UI->>UI: "render select when allPaths.length > 1"
UI->>UI: onChange calls onCustomPathChange(next) schedules state update
UI->>UI: onCustomPathBlur reads STALE customPath state
UI->>IPC: setCustomPath with stale value
Reviews (1): Last reviewed commit: "add image" | Re-trigger Greptile |
| onChange={(e) => { | ||
| const next = e.target.value; | ||
| onCustomPathChange(next); | ||
| // Persist immediately - selecting from the chooser is an explicit commit | ||
| onCustomPathBlur?.(); | ||
| }} |
There was a problem hiding this comment.
Stale-state read: blur callback sees old
customPath
onCustomPathChange(next) schedules a React state update (async), so customPath in the parent has not changed yet when onCustomPathBlur?.() fires immediately after in the same event handler. Callers that read the state value inside their blur handler — AgentSelectionScreen.tsx (const pathToSet = customPath.trim() || null then setCustomPath(pathToSet)) and EncoreTab.tsx (customPath: ac.customPath || undefined) — will therefore persist the previous path instead of the one the user just selected. The fix is to pass next directly into the blur callback, or refactor the callbacks to accept the new value as a parameter.
| const active = detection.path; | ||
| const merged = active && !found.includes(active) ? [active, ...found] : found; | ||
| if (merged.length > 1) { | ||
| allPaths = merged; |
There was a problem hiding this comment.
String-equality dedup misses symlink aliases:
active can re-introduce a duplicate
findAllBinaryPaths de-duplicates by resolved canonical path (realpath), so if /usr/local/bin/codex and /opt/homebrew/bin/codex both point to the same binary, only one enters found. But found.includes(active) uses plain string equality, so when detection.path is /usr/local/bin/codex (what which returned first) and found already contains /opt/homebrew/bin/codex, the check returns false and active is prepended — giving the chooser two options that launch the exact same binary. The dedup step should resolve active via realpath and compare against the canonical keys already seen by findAllBinaryPaths.
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/main/agents/detector.ts`:
- Around line 152-153: The membership check that builds merged uses exact string
comparison and can duplicate the same Windows path with different casing; before
checking if active is in found, normalize/canonicalize paths (e.g., use
path.normalize + path.resolve and on Windows compare case-insensitively via
.toLowerCase(), or use fs.realpathSync to canonicalize) and compare against a
normalized version of found (map found to normalizedFound) using that
normalizedActive; only prepend the original active when normalizedActive is not
present in normalizedFound so merged does not contain duplicates. Ensure this
change updates the check around the merged = active && !found.includes(active) ?
[active, ...found] : found; logic and references to active/found/merged.
In `@src/main/agents/path-prober.ts`:
- Around line 638-640: In findAllBinaryPaths, replace the empty catch that
swallows all errors with explicit handling: log recoverable/expected lookup
errors at debug level with contextual information (include the binary
name/expanded path and the caught error) and allow the function to continue
returning direct probe results; for any unexpected errors (non-network,
non-lookup-specific, or anything not matched by your recoverable checks) rethrow
the error so it can bubble to Sentry. Implement the checks by inspecting the
caught error (e.g., error.name or instanceof specific error classes) and use the
module's logger (the same logger used elsewhere in the file) to emit the
debug/error messages before continuing or rethrowing.
In `@src/renderer/components/shared/AgentConfigPanel.tsx`:
- Around line 448-453: The onChange handler updates local state via
onCustomPathChange(next) then immediately calls onCustomPathBlur() which may
read the stale customPath state; change the commit to use the selected value
directly by passing next into the blur/commit callback (e.g., call
onCustomPathBlur(next) or a new onCustomPathCommit(next)) or otherwise ensure
the persistence callback receives the explicit next value instead of relying on
reading customPath from state; update the onCustomPathBlur signature/usage
accordingly wherever it’s implemented.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: f2cbf9ac-756f-4e94-904a-4b4b408add0f
⛔ Files ignored due to path filters (1)
assets/logo.pngis excluded by!**/*.png
📒 Files selected for processing (8)
src/__tests__/main/agents/path-prober.test.tssrc/main/agents/definitions.tssrc/main/agents/detector.tssrc/main/agents/index.tssrc/main/agents/path-prober.tssrc/main/preload/agents.tssrc/renderer/components/shared/AgentConfigPanel.tsxsrc/renderer/types/index.ts
| const merged = active && !found.includes(active) ? [active, ...found] : found; | ||
| if (merged.length > 1) { |
There was a problem hiding this comment.
Normalize path comparison before prepending active path.
Line 152 uses exact string comparison; on Windows this can duplicate the same path with different casing in allPaths. Normalize (or canonicalize) before the membership check.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/main/agents/detector.ts` around lines 152 - 153, The membership check
that builds merged uses exact string comparison and can duplicate the same
Windows path with different casing; before checking if active is in found,
normalize/canonicalize paths (e.g., use path.normalize + path.resolve and on
Windows compare case-insensitively via .toLowerCase(), or use fs.realpathSync to
canonicalize) and compare against a normalized version of found (map found to
normalizedFound) using that normalizedActive; only prepend the original active
when normalizedActive is not present in normalizedFound so merged does not
contain duplicates. Ensure this change updates the check around the merged =
active && !found.includes(active) ? [active, ...found] : found; logic and
references to active/found/merged.
| } catch { | ||
| // which/where failures are non-fatal; we still have direct probe results | ||
| } |
There was a problem hiding this comment.
Avoid silently swallowing lookup failures in findAllBinaryPaths.
Line 638-Line 640 catches and ignores all errors, which hides actionable diagnostics when environment expansion or lookup behavior regresses. Please explicitly handle expected failures (debug log with context) and rethrow/report unexpected ones.
As per coding guidelines: "Do not silently swallow errors. Let unhandled exceptions bubble up to Sentry for error tracking in production. Handle expected/recoverable errors explicitly (e.g., NETWORK_ERROR). For unexpected errors, re-throw them to allow Sentry to capture them."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/main/agents/path-prober.ts` around lines 638 - 640, In
findAllBinaryPaths, replace the empty catch that swallows all errors with
explicit handling: log recoverable/expected lookup errors at debug level with
contextual information (include the binary name/expanded path and the caught
error) and allow the function to continue returning direct probe results; for
any unexpected errors (non-network, non-lookup-specific, or anything not matched
by your recoverable checks) rethrow the error so it can bubble to Sentry.
Implement the checks by inspecting the caught error (e.g., error.name or
instanceof specific error classes) and use the module's logger (the same logger
used elsewhere in the file) to emit the debug/error messages before continuing
or rethrowing.
| onChange={(e) => { | ||
| const next = e.target.value; | ||
| onCustomPathChange(next); | ||
| // Persist immediately - selecting from the chooser is an explicit commit | ||
| onCustomPathBlur?.(); | ||
| }} |
There was a problem hiding this comment.
Persisted path can be stale when selecting from the dropdown.
Line 450 updates state, then Line 452 immediately commits via blur callback. If the blur handler reads customPath from state, it may persist the previous value instead of next. Please commit using the selected value directly (e.g., pass next through the callback contract) or defer persistence until the state update is reflected.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/renderer/components/shared/AgentConfigPanel.tsx` around lines 448 - 453,
The onChange handler updates local state via onCustomPathChange(next) then
immediately calls onCustomPathBlur() which may read the stale customPath state;
change the commit to use the selected value directly by passing next into the
blur/commit callback (e.g., call onCustomPathBlur(next) or a new
onCustomPathCommit(next)) or otherwise ensure the persistence callback receives
the explicit next value instead of relying on reading customPath from state;
update the onCustomPathBlur signature/usage accordingly wherever it’s
implemented.
Closes #1048
Summary
nvm/fnm/volta, plus anything in shell PATH such ascodex-multi-auth-codex), de-duplicated by canonical resolved path (realpath) so symlinked aliases collapse to one entry.AgentConfigPanelrenders an inline dropdown listing all of them.agents:setCustomPathIPC, which already persists the choice per-agent inagentConfigsStoreand is loaded on app startup — so the user's preferred binary becomes the default for every future agent they create.agent.allPaths.length > 1.Implementation
path-prober.ts: newfindAllBinaryPaths(binaryName)combines direct probes withwhich -a/where, dedupes byfs.realpath. Existingprobe{Windows,Unix}Pathsare now thin wrappers overprobe{Windows,Unix}PathsAll(no behavior change for the single-path callers).detector.ts: populatesAgentConfig.allPathsonly when more than one path exists; the active path (custom or detected) is always prepended so the chooser reflects what's in use even for a manually-entered wrapper.AgentConfigPanel.tsx: chooser is a small<select>under the path input; selection commits viaonCustomPathChange+onCustomPathBlurimmediately.Test plan
vitest run src/__tests__/main/agents/— 307/307 pass (includes 4 new tests covering priority, symlink dedup, empty result, andwhichfailure tolerance)vitest run src/__tests__/renderer/components/shared/AgentConfigPanel.test.tsx— 41/41 passnpm run lint(tsc projects) — cleannpm run lint:eslint— clean on changed filescodexbinaries (e.g. Homebrew +~/bin/codex-multi-auth-codex), open the Create New Agent dialog, expand Codex config, confirm the chooser appears and selection persists across app restarts.Note
Pre-push hook was bypassed because of a pre-existing failure in
src/__tests__/renderer/components/SessionActivityGraph.test.tsx(renders day-based labels for week lookbacks— a date/locale-sensitive assertion checking for the literal string'May 13'). It fails identically on unmodifiedorigin/mainand is unrelated to this change.Summary by CodeRabbit
New Features
Tests