Skip to content

feat(agents): chooser for multiple detected binary paths#1050

Open
pedramamini wants to merge 2 commits into
mainfrom
fix/1048-codex-multi-path-chooser
Open

feat(agents): chooser for multiple detected binary paths#1050
pedramamini wants to merge 2 commits into
mainfrom
fix/1048-codex-multi-path-chooser

Conversation

@pedramamini
Copy link
Copy Markdown
Collaborator

@pedramamini pedramamini commented May 27, 2026

Closes #1048

Summary

  • Detection now enumerates every valid installation it finds for an agent (Homebrew, npm-global, nvm/fnm/volta, plus anything in shell PATH such as codex-multi-auth-codex), de-duplicated by canonical resolved path (realpath) so symlinked aliases collapse to one entry.
  • When more than one path is detected, the Path field in AgentConfigPanel renders an inline dropdown listing all of them.
  • Selecting an alternative writes through the existing agents:setCustomPath IPC, which already persists the choice per-agent in agentConfigsStore and is loaded on app startup — so the user's preferred binary becomes the default for every future agent they create.
  • Single-install behavior is unchanged: the chooser is only rendered when agent.allPaths.length > 1.
  • SSH-remote detection is intentionally left as a single-path lookup; the chooser only renders for local detection.

Implementation

  • path-prober.ts: new findAllBinaryPaths(binaryName) combines direct probes with which -a / where, dedupes by fs.realpath. Existing probe{Windows,Unix}Paths are now thin wrappers over probe{Windows,Unix}PathsAll (no behavior change for the single-path callers).
  • detector.ts: populates AgentConfig.allPaths only 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 via onCustomPathChange + onCustomPathBlur immediately.

Test plan

  • vitest run src/__tests__/main/agents/ — 307/307 pass (includes 4 new tests covering priority, symlink dedup, empty result, and which failure tolerance)
  • vitest run src/__tests__/renderer/components/shared/AgentConfigPanel.test.tsx — 41/41 pass
  • npm run lint (tsc projects) — clean
  • npm run lint:eslint — clean on changed files
  • Manual: install two codex binaries (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 unmodified origin/main and is unrelated to this change.

Summary by CodeRabbit

  • New Features

    • Agents now detect all available installation paths in priority order.
    • Added "Detected installations" selector to agent configuration when multiple paths are found.
    • Users can select from available installations for local agents.
  • Tests

    • Added test coverage for binary path discovery and de-duplication logic.

Review Change Stack

Test User added 2 commits May 27, 2026 08:28
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
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 27, 2026

📝 Walkthrough

Walkthrough

This 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.

Changes

Multi-path agent discovery and selection

Layer / File(s) Summary
Binary discovery refactoring and testing
src/main/agents/path-prober.ts, src/__tests__/main/agents/path-prober.test.ts
New probeWindowsPathsAll and probeUnixPathsAll return all matching paths in priority order using parallel fs.promises.access checks. New findAllBinaryPaths merges direct probes with which/where lookups via expanded shell PATH, then deduplicates by resolving each candidate to its real path and preserving first-seen order (lowercased on Windows). Existing single-path functions now delegate to the "all" versions and return only the first result. Tests cover priority ordering, canonical-target deduplication, empty results, and graceful failure when lookup commands error.
Type schema propagation
src/main/agents/definitions.ts, src/main/preload/agents.ts, src/renderer/types/index.ts
AgentConfig interface is extended with optional allPaths?: string[] field across all three definition layers to carry all discovered installation paths in priority order.
Detection integration
src/main/agents/detector.ts
Agent detection now calls findAllBinaryPaths for non-bash agents during setup, merges the currently detected path into the list if missing, and stores the result as allPaths in each agent config. Path enumeration failures are caught and logged at debug level without blocking detection.
Module exports
src/main/agents/index.ts
The agents module re-exports the new discovery functions probeWindowsPathsAll, probeUnixPathsAll, and findAllBinaryPaths alongside existing path-probing exports.
UI path selection component
src/renderer/components/shared/AgentConfigPanel.tsx
A new "Detected installations" dropdown is conditionally rendered for local agents when agent.allPaths contains more than one path. Selecting an installation updates customPath and immediately invokes onCustomPathBlur (if provided) to persist the selection as an explicit user commit.

Sequence Diagram

sequenceDiagram
  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
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • RunMaestro/Maestro#466: Introduces getExpandedEnvWithShell for shell-aware PATH expansion, which this PR's findAllBinaryPaths relies on for which/where lookups.
  • RunMaestro/Maestro#562: Updates the shared PATH expansion to include Node version-manager bin directories, directly affecting which binaries which/where probes discover.
  • RunMaestro/Maestro#687: Extends known-path candidates and PATH adjustments in path-prober that would benefit the new "all matches" discovery logic.

Suggested labels

ready to merge

Poem

A rabbit hops through many paths,
Finds all the binaries in their baths,
De-dupes with care, in order true,
Shows users five—now pick a few! 🐰✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(agents): chooser for multiple detected binary paths' accurately and concisely describes the primary feature being implemented—a UI chooser for selecting among multiple detected agent binary paths.
Linked Issues check ✅ Passed All coding requirements from issue #1048 are fulfilled: multiple installation detection via findAllBinaryPaths, UI chooser in AgentConfigPanel, path persistence via existing IPC, and proper prioritization/deduplication by realpath.
Out of Scope Changes check ✅ Passed All changes directly support the multi-path detection and chooser feature: path-prober enhancements, detector integration, type definitions, UI rendering, and comprehensive tests are all on-scope.
Docstring Coverage ✅ Passed Docstring coverage is 85.71% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/1048-codex-multi-path-chooser

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 and usage tips.

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented May 27, 2026

Greptile Summary

This PR adds a binary-path chooser for agents that have multiple valid installations detected on the host. Detection now enumerates all paths (direct probes + which -a/where, deduplicated via realpath), and when more than one is found the AgentConfigPanel renders a <select> that persists the choice through the existing agents:setCustomPath IPC.

  • path-prober.ts: New probeWindowsPathsAll, probeUnixPathsAll, and findAllBinaryPaths collect every installation; existing single-path functions become thin wrappers with no behavior change.
  • detector.ts: Populates AgentConfig.allPaths only when more than one path is found, always prepending the currently-active path so the chooser reflects what is in use.
  • AgentConfigPanel.tsx: Adds a <select> dropdown under the path text input; selection commits via onCustomPathChange + onCustomPathBlur inline. The blur callback fires with stale React state in callers that read customPath from state (notably AgentSelectionScreen and EncoreTab), causing those contexts to persist the previous path instead of the selection.

Confidence Score: 3/5

Safe 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

Filename Overview
src/renderer/components/shared/AgentConfigPanel.tsx Adds the path-chooser select; the onChange handler calls onCustomPathChange then onCustomPathBlur synchronously, so blur callbacks that read the customPath prop/state (EncoreTab, AgentSelectionScreen) will persist the stale value.
src/main/agents/detector.ts Populates allPaths on agent detection; the found.includes(active) check uses string equality and can produce duplicate entries when the active path is a symlink to an already-deduplicated canonical path.
src/main/agents/path-prober.ts Adds probeWindowsPathsAll, probeUnixPathsAll, and findAllBinaryPaths; refactors single-path functions as thin wrappers. Logic is sound; which -a plus direct-probe merge and realpath dedup are correctly implemented.
src/tests/main/agents/path-prober.test.ts Adds four tests for findAllBinaryPaths covering priority order, symlink dedup, empty result, and which failure tolerance.
src/main/agents/definitions.ts Adds allPaths optional string array to the canonical AgentConfig interface with an explanatory comment.
src/main/preload/agents.ts Adds allPaths optional string array to the preload AgentConfig interface, keeping the IPC bridge in sync.
src/renderer/types/index.ts Adds allPaths optional string array to renderer AgentConfig; consistent with definitions.ts and preload additions.
src/main/agents/index.ts Re-exports the two new All variants and findAllBinaryPaths from the module index.

Sequence Diagram

sequenceDiagram
    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
Loading

Reviews (1): Last reviewed commit: "add image" | Re-trigger Greptile

Comment on lines +448 to +453
onChange={(e) => {
const next = e.target.value;
onCustomPathChange(next);
// Persist immediately - selecting from the chooser is an explicit commit
onCustomPathBlur?.();
}}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 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.

Comment on lines +151 to +154
const active = detection.path;
const merged = active && !found.includes(active) ? [active, ...found] : found;
if (merged.length > 1) {
allPaths = merged;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 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.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between 655a089 and fd62bfa.

⛔ Files ignored due to path filters (1)
  • assets/logo.png is excluded by !**/*.png
📒 Files selected for processing (8)
  • src/__tests__/main/agents/path-prober.test.ts
  • src/main/agents/definitions.ts
  • src/main/agents/detector.ts
  • src/main/agents/index.ts
  • src/main/agents/path-prober.ts
  • src/main/preload/agents.ts
  • src/renderer/components/shared/AgentConfigPanel.tsx
  • src/renderer/types/index.ts

Comment on lines +152 to +153
const merged = active && !found.includes(active) ? [active, ...found] : found;
if (merged.length > 1) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

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.

Comment on lines +638 to +640
} catch {
// which/where failures are non-fatal; we still have direct probe results
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

Comment on lines +448 to +453
onChange={(e) => {
const next = e.target.value;
onCustomPathChange(next);
// Persist immediately - selecting from the chooser is an explicit commit
onCustomPathBlur?.();
}}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

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.

Persist and choose among detected Codex provider paths

1 participant