Skip to content

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

Open
pedramamini wants to merge 1 commit into
mainfrom
fix/1048-codex-multi-path-chooser-v2
Open

feat(agents): chooser for multiple detected binary paths#1317
pedramamini wants to merge 1 commit into
mainfrom
fix/1048-codex-multi-path-chooser-v2

Conversation

@pedramamini

@pedramamini pedramamini commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

Closes #1048

Supersedes #1050, which was closed after its branch picked up an unrelated 57k-line test-coverage dump. This is the same approach, rebuilt cleanly on top of main, plus a fix for the custom-wrapper case and renderer test coverage.

Problem

Agent detection stopped at the first binary it found. Users with more than one valid install - most commonly an nvm-managed global codex alongside an auth wrapper like codex-multi-auth-codex - got whichever one happened to sort first, and had to paste the correct path by hand for every new agent.

What changed

Detection enumerates everything (src/main/agents/path-prober.ts)

New findAllBinaryPaths() combines direct probes of known install locations (Homebrew, npm-global, ~/.local/bin, nvm/fnm/volta bins) with everything which -a / where returns against the expanded shell PATH. Results are de-duplicated by canonical resolved path via fs.realpath, so a symlink alias and its target collapse to one entry. Case-insensitive on Windows.

The existing probeUnixPaths / probeWindowsPaths are now thin first-match wrappers over new *All variants, so single-path callers are unaffected.

Detector populates allPaths (src/main/agents/detector.ts)

Only set when more than one install is found. The currently active path (custom or detected) is always merged in first, so the chooser reflects what is actually in use. Failures are non-fatal and logged at debug - the chooser is an enhancement, and single-path detection still works if the extra probe throws. Skipped for bash, which is on every system and not user-selectable.

Chooser in the agent config panel (AgentConfigPanel.tsx)

Renders under the Path field only when allPaths.length > 1. Selecting a path writes through onCustomPathChange and commits immediately via onCustomPathBlur, which persists to the existing per-agent customPath store - so it survives restarts and becomes the default for the next agent.

Hidden for SSH-backed agents, where the path field means a remote command rather than a local binary.

Custom wrappers stay first-class. A hand-typed path that detection did not find (not on PATH, or a ~-relative path that detection reports expanded) will not match any <option>. Rather than let the browser silently render the first option - misrepresenting which binary is active - it is surfaced as an explicit Custom: <path> entry. The text input remains the way to enter one.

Mapping to the request

Asked for Status
Detect multiple installs incl. PATH, Homebrew, system node/npm, nvm Yes
Let the user choose when multiple are found Yes, dropdown under the Path field
Persist last selection as the default for future agents Yes, via existing customPath store
Keep manual entry for custom wrappers Yes, plus an explicit Custom: entry
Show chooser only on first multi-detection or explicit Detect Partial - see below

The chooser stays visible whenever multiple installs exist rather than appearing once and hiding. It is a compact collapsed <select>, so it is not intrusive, and a persistently visible control is discoverable later when a user adds a second install. Happy to gate it behind the existing Detect button if you'd rather have the original behavior.

Testing

  • path-prober.test.ts: 4 new cases - priority ordering, symlink de-duplication, empty result, and which failure falling back to probe results. 28 pass.
  • AgentConfigPanel.test.tsx: 8 new cases - hidden at 0/1 paths, lists all options, preselection with and without a custom path, immediate persistence on change, the Custom: entry for an undetected wrapper, and hidden under SSH. 33 pass.
  • npx eslint clean on all changed files; prettier reports no changes.

Note: src/__tests__/main/agents/claude-usage-startup.test.ts fails 20 tests on this branch. It fails identically on main at 39c9a6744 - pre-existing and unrelated.

Summary by CodeRabbit

  • New Features

    • Detects multiple local installations of supported agents.
    • Adds an installation chooser when multiple paths are available.
    • Preserves custom paths and clearly labels them as custom selections.
    • Automatically selects the detected installation currently in use.
  • Bug Fixes

    • Removes duplicate installation paths, including equivalent filesystem targets.
    • Continues detection successfully when optional path discovery fails.
  • Tests

    • Added coverage for path discovery, duplicate handling, chooser behavior, custom paths, and SSH configurations.

Detection now enumerates every valid installation instead of stopping at
the first hit: direct probes of Homebrew, npm-global, ~/.local/bin and
nvm/fnm/volta bins, plus everything `which -a` returns against the
expanded shell PATH. Results are de-duplicated by canonical resolved path
so symlink aliases collapse to one entry.

When more than one installation is found, the Path field in the agent
config panel renders a chooser listing the alternatives. Picking one
writes through to the existing per-agent customPath store, so it persists
across restarts and becomes the default for future agents.

Manual entry still works for wrappers like codex-multi-auth-codex. A
hand-typed path that detection did not find is surfaced as an explicit
"Custom: <path>" entry rather than letting the select silently display
the first detected option.

The chooser is hidden for SSH-backed agents, where the path field means
a remote command rather than a local binary.

Closes #1048
@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Agent detection now enumerates multiple binary installations, exposes them through AgentConfig.allPaths, and provides a local configuration-panel chooser that saves detected or manually entered paths.

Changes

Agent path detection and selection

Layer / File(s) Summary
Prioritized binary path discovery
src/main/agents/path-prober.ts, src/__tests__/main/agents/path-prober.test.ts
Platform probes enumerate direct matches, merge shell lookup results, canonicalize duplicates, preserve priority, and tolerate lookup failures.
Agent detection path enrichment
src/shared/types.ts, src/main/agents/index.ts, src/main/agents/detector.ts
AgentConfig supports allPaths; path-prober helpers are re-exported; detection attaches multiple paths while preserving the active path.
Detected installation chooser
src/renderer/components/shared/AgentConfigPanel.tsx, src/__tests__/renderer/components/shared/AgentConfigPanel.test.tsx
Local configurations can select among detected paths, retain custom paths, save selections, and hide the chooser for SSH agents.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant AgentDetector
  participant findAllBinaryPaths
  participant AgentConfigPanel
  AgentDetector->>findAllBinaryPaths: enumerate binary installations
  findAllBinaryPaths-->>AgentDetector: return ordered unique paths
  AgentDetector->>AgentConfigPanel: provide agent config with allPaths
  AgentConfigPanel->>AgentConfigPanel: display and save selected path
Loading

Possibly related PRs

Suggested reviewers: copilot, reachrazamair, chr1syy

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR covers multi-path detection, selection, persistence, and manual entry, but it does not honor the chooser-visibility requirement. Update the chooser to appear only on initial multi-detection or after an explicit Detect action, while keeping the stored selection as default.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: adding a chooser for multiple detected binary paths.
Out of Scope Changes check ✅ Passed The changes stay focused on Codex path detection, selection, persistence, and tests, with no clear unrelated additions.
✨ 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-v2

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.

@greptile-apps

greptile-apps Bot commented Jul 28, 2026

Copy link
Copy Markdown

Greptile Summary

Adds multi-install binary discovery and a persistent local-agent path chooser.

  • Enumerates known and shell-resolved binary paths, canonicalizing symlink duplicates.
  • Adds allPaths to detected agent configuration and exposes it through shared types.
  • Renders a detected-installation chooser for local agents and adds main/renderer coverage.

Confidence Score: 2/5

The PR should not merge until chooser persistence stores the selected path and discovery can actually enumerate differently named wrappers.

The chooser persists state from the previous render, while the probing command cannot return the differently named authentication wrapper central to the feature, leaving both selection durability and the primary detection scenario broken.

Files Needing Attention: src/renderer/components/shared/AgentConfigPanel.tsx and src/main/agents/path-prober.ts

Important Files Changed

Filename Overview
src/main/agents/path-prober.ts Adds comprehensive same-name path enumeration, but cannot discover the differently named wrappers advertised by the feature.
src/main/agents/detector.ts Merges the active path into detected alternatives and degrades safely when supplemental probing fails.
src/renderer/components/shared/AgentConfigPanel.tsx Adds the local installation chooser, but commits before React state carries the selected value to persistence handlers.
src/shared/types.ts Adds the optional serializable allPaths field needed to carry alternatives to the renderer.
src/tests/main/agents/path-prober.test.ts Covers ordering and canonicalization, but models an impossible differently named result from which -a codex.
src/tests/renderer/components/shared/AgentConfigPanel.test.tsx Covers chooser rendering and callback invocation without exercising integrated state-to-persistence behavior.

Sequence Diagram

sequenceDiagram
  participant Detector
  participant Prober
  participant Panel
  participant Store
  Detector->>Prober: findAllBinaryPaths(binaryName)
  Prober-->>Detector: canonicalized paths
  Detector-->>Panel: AgentConfig.allPaths
  Panel->>Panel: onCustomPathChange(selectedPath)
  Panel->>Store: onCustomPathBlur()
  Note over Panel,Store: Persistence currently reads the prior render's path
Loading

Reviews (1): Last reviewed commit: "feat(agents): chooser for multiple detec..." | Re-trigger Greptile

Comment on lines +574 to +576
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 Chooser persists the previous path

When a user selects another installation in the configuration wizard, onCustomPathChange(next) schedules a React state update and onCustomPathBlur() immediately persists the path captured by the current render. This writes the previous path to agentConfigsStore, so subsequent detection and launches continue using the old binary.

Knowledge Base Used: Agent Run: Driving, Parsing, and Tracking AI Agent CLIs

Comment on lines +677 to +678
// returns every match by default.
const args = isWindows() ? [binaryName] : ['-a', binaryName];

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 Exact-name lookup omits wrappers

When codex and a differently named wrapper such as codex-multi-auth-codex are both installed, the direct probes construct only codex paths and which -a codex/where codex searches only that canonical name. The wrapper is therefore omitted from allPaths, so the chooser does not offer the primary wrapper scenario this feature is intended to support.

Knowledge Base Used: Agent Run: Driving, Parsing, and Tracking AI Agent CLIs

Copilot AI 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.

Pull request overview

This PR improves agent binary detection by enumerating multiple valid installation paths (including PATH and common install locations) and surfaces those options in the renderer so users can choose the intended binary when more than one is present.

Changes:

  • Add AgentConfig.allPaths to carry multiple detected binary paths from main to renderer.
  • Implement findAllBinaryPaths() to combine direct probes with which -a (Unix) / where (Windows), with canonical-path de-duplication.
  • Add a “Detected installations” chooser under the Path field in AgentConfigPanel, with accompanying main and renderer test coverage.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/shared/types.ts Extends AgentConfig with optional allPaths for multi-install UI.
src/main/agents/path-prober.ts Adds multi-path probing and findAllBinaryPaths() (probe + which/where, de-dupe).
src/main/agents/index.ts Re-exports new prober APIs (*All, findAllBinaryPaths).
src/main/agents/detector.ts Populates allPaths when multiple installs are detected.
src/renderer/components/shared/AgentConfigPanel.tsx Renders the multi-install chooser and wires selection into custom path updates.
src/tests/main/agents/path-prober.test.ts Adds tests for ordering, de-dupe, empty results, and which failure tolerance.
src/tests/renderer/components/shared/AgentConfigPanel.test.tsx Adds tests for chooser visibility, options, preselection, persistence, custom entry, and SSH hiding.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +572 to +576
const next = e.target.value;
if (next === CUSTOM_PATH_OPTION) return;
onCustomPathChange(next);
// Persist immediately - selecting from the chooser is an explicit commit
onCustomPathBlur();
Comment on lines +204 to +212
const found = await findAllBinaryPaths(agentDef.binaryName);
// Always include the active path (custom or detected) so the
// chooser reflects what is currently in use, even if it isn't
// one of the auto-probed locations.
const active = detection.path;
const merged = active && !found.includes(active) ? [active, ...found] : found;
if (merged.length > 1) {
allPaths = merged;
}
Comment on lines +597 to +600
<p className="text-xs opacity-50 mt-1">
Multiple {agent.binaryName} binaries were found. Your selection is saved as the
default for future agents.
</p>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
src/main/agents/detector.ts (1)

213-218: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Non-fatal catch skips Sentry reporting used elsewhere in this file.

findAllBinaryPaths already swallows its own expected failures internally (probe misses via Promise.allSettled, which/where failures via its own try/catch), so a throw reaching this outer catch is more likely an unexpected bug than a routine "binary not found" case. runModelDiscovery and runConfigOptionDiscovery in this same file both call captureException for their non-fatal catches; this new catch only logs via logger.debug, losing production visibility into genuinely unexpected failures.

As per coding guidelines, "Do not silently swallow unexpected exceptions... rethrow unexpected errors so Sentry can capture them, and use the Sentry reporting utilities for intentional exception or event reporting."

♻️ Proposed fix
 				} catch (err) {
 					// Non-fatal: chooser is just a nice-to-have, single-path mode still works.
 					logger.debug(`findAllBinaryPaths failed for ${agentDef.binaryName}`, LOG_CONTEXT, {
 						err,
 					});
+					captureException(err, { operation: 'agent:findAllBinaryPaths', agentId: agentDef.id });
 				}
🤖 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 213 - 218, Update the outer catch
around findAllBinaryPaths to report the caught exception through the same
captureException Sentry utility used by runModelDiscovery and
runConfigOptionDiscovery, while retaining the existing debug context. Do not
treat unexpected errors as silently swallowed; preserve the non-fatal flow only
if that matches the established handling in those neighboring discovery methods.

Source: Coding guidelines

🤖 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 208-209: Update the active-path merge in the detection flow around
detection.path so deduplication compares canonical filesystem targets rather
than raw strings. Resolve active and existing found entries with the same
realpath behavior used by findAllBinaryPaths, retain only one path for
equivalent targets, and preserve the existing ordering and handling of
unresolved paths.

In `@src/main/agents/path-prober.ts`:
- Around line 665-713: Update the which/where lookup in findAllBinaryPaths to
pass an explicit timeout through the ExecOptions-compatible argument shape
recognized by execFileNoThrow, while preserving the expanded environment. Use
the existing timeout convention or constant if available, and ensure the lookup
returns through its existing non-fatal handling when the timeout is reached.

---

Nitpick comments:
In `@src/main/agents/detector.ts`:
- Around line 213-218: Update the outer catch around findAllBinaryPaths to
report the caught exception through the same captureException Sentry utility
used by runModelDiscovery and runConfigOptionDiscovery, while retaining the
existing debug context. Do not treat unexpected errors as silently swallowed;
preserve the non-fatal flow only if that matches the established handling in
those neighboring discovery methods.
🪄 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 Plus

Run ID: d246f75e-8ffb-40a8-9085-e8e0fb18bec9

📥 Commits

Reviewing files that changed from the base of the PR and between 39c9a67 and d2ebc2e.

📒 Files selected for processing (7)
  • src/__tests__/main/agents/path-prober.test.ts
  • src/__tests__/renderer/components/shared/AgentConfigPanel.test.tsx
  • src/main/agents/detector.ts
  • src/main/agents/index.ts
  • src/main/agents/path-prober.ts
  • src/renderer/components/shared/AgentConfigPanel.tsx
  • src/shared/types.ts

Comment on lines +208 to +209
const active = detection.path;
const merged = active && !found.includes(active) ? [active, ...found] : found;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Active-path merge dedups by string, not canonical path.

found.includes(active) is a strict string comparison. If active (custom or fallback-detected path) is a different string that resolves to the same binary as an entry already in found (e.g. a symlink alias), it will show up twice in allPaths/the chooser — the exact "different path, same canonical target" case findAllBinaryPaths already handles internally for its own candidates via fs.promises.realpath, just not applied to this final merge step.

🤖 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 208 - 209, Update the active-path
merge in the detection flow around detection.path so deduplication compares
canonical filesystem targets rather than raw strings. Resolve active and
existing found entries with the same realpath behavior used by
findAllBinaryPaths, retain only one path for equivalent targets, and preserve
the existing ordering and handling of unresolved paths.

Comment on lines +665 to +713
export async function findAllBinaryPaths(binaryName: string): Promise<string[]> {
// 1. Direct probes
const probedPaths = isWindows()
? await probeWindowsPathsAll(binaryName)
: await probeUnixPathsAll(binaryName);

// 2. which/where lookup
const fromShell: string[] = [];
try {
const command = getWhichCommand();
const env = await getExpandedEnvWithShell();
// On Unix, `which -a` returns every match in PATH. On Windows, `where`
// returns every match by default.
const args = isWindows() ? [binaryName] : ['-a', binaryName];
const result = await execFileNoThrow(command, args, undefined, env);
if (result.exitCode === 0 && result.stdout.trim()) {
const matches = result.stdout
.trim()
.split(/\r?\n/)
.map((p) => p.trim())
.filter((p) => p);
fromShell.push(...matches);
}
} catch {
// which/where failures are non-fatal; we still have direct probe results
}

// 3. De-duplicate by canonical resolved path, preserving order
const seenKeys = new Set<string>();
const result: string[] = [];
const candidates = [...probedPaths, ...fromShell];

for (const candidate of candidates) {
let key: string;
try {
// realpath collapses symlinks (e.g., volta/bin/codex → volta/tools/...)
key = await fs.promises.realpath(candidate);
} catch {
key = candidate;
}
// Windows is case-insensitive
const normalizedKey = isWindows() ? key.toLowerCase() : key;
if (seenKeys.has(normalizedKey)) continue;
seenKeys.add(normalizedKey);
result.push(candidate);
}

return result;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Add a timeout to the which/where lookup.

execFileNoThrow(command, args, undefined, env) passes env as the 4th argument, which execFileNoThrow only treats as ExecOptions (enabling timeout) when the object has an input or timeout key — otherwise it's treated as the legacy env-only signature, so no timeout is applied here. If which/where hangs (e.g. a broken PATH entry pointing at an unresponsive network mount, or a shell profile that never returns), this call — and the sequential for loop in doDetectAgents that awaits it once per detected agent — can block agent detection indefinitely.

🕒 Proposed fix: bound the which/where lookup with a timeout
-		const result = await execFileNoThrow(command, args, undefined, env);
+		const result = await execFileNoThrow(command, args, undefined, {
+			...env,
+			timeout: 5000,
+		});
🤖 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 665 - 713, Update the
which/where lookup in findAllBinaryPaths to pass an explicit timeout through the
ExecOptions-compatible argument shape recognized by execFileNoThrow, while
preserving the expanded environment. Use the existing timeout convention or
constant if available, and ensure the lookup returns through its existing
non-fatal handling when the timeout is reached.

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

2 participants