Skip to content

fix(web): persist the default CLI at any installed count, not only a sole one - #4151

Open
jsmith7735 wants to merge 1 commit into
career-ops-hq:mainfrom
jsmith7735:fix/web-persist-default-cli
Open

fix(web): persist the default CLI at any installed count, not only a sole one#4151
jsmith7735 wants to merge 1 commit into
career-ops-hq:mainfrom
jsmith7735:fix/web-persist-default-cli

Conversation

@jsmith7735

@jsmith7735 jsmith7735 commented Sep 13, 2026

Copy link
Copy Markdown

What does this PR do?

Persists the default CLI on machines with two or more AI CLIs installed. Today only a sole install is persisted, so every multi-CLI machine gets a Config page that looks configured while every AI feature silently does nothing.

Related issue

None — bug fix, which CONTRIBUTING.md exempts from issue-first. Happy to open one if you'd prefer it tracked.

Type of change

  • Bug fix
  • New feature
  • Documentation / translation
  • Refactor (no behavior change)

The bug

config-form.tsx currently persists the auto-selection only when exactly one CLI is installed. The multi-CLI path falls through into precisely the state the code's own comment warns about:

// Highlight + persist the only installed CLI when Config was never saved.
// Highlight-only used to look configured while jobs still read empty localStorage.
const only = list.filter((c) => c.installed);
if (only.length !== 1) return list.find((c) => c.installed)?.id || "";   // ← highlights, persists nothing
if (!readSavedCliId()) persistCliId(only[0].id);

With two or more installed, the early return highlights the first CLI and writes nothing.

Every AI surface reads career-ops:config directly and sends it as cliId. So Config displays a selected CLI over an empty localStorage, /api/explore/ai answers {"error":"query and cliId required"}, and the assistant shows "no CLI configured" next to a Config page that looks configured.

Nothing prompts a Save, because nothing looks unset. The workaround a user lands on by accident: select a different CLI → Save → reload → select the original → Save → reload.

Reproduction

main at 1.32.0, clean browser profile, four CLIs installed (claude, codex, gemini, opencode):

localStorage after visiting /config, no clicks: <EMPTY>

With this change:

localStorage after visiting /config, no clicks: {"mode":"cli","cliId":"claude"}

The fix

Whatever Config renders as selected is what it must persist, so the pick becomes pickDefaultInstalled() — first installed, any count.

pickSoleInstalled() keeps its narrower meaning for resolveCliId(), which answers a different question ("can I choose for the user unambiguously?") and is deliberately left alone.

On the test move

Both helpers now live in src/lib/cli-pick.mjs, re-exported from saved-cli.ts.

That is what lets tests/lib/saved-cli-pick.test.mjs import the real functions. The suite previously carried a hand-copied mirror of pickSoleInstalled:

// Mirror of pickSoleInstalled in src/lib/saved-cli.ts (TS; this suite is .mjs).
function pickSoleInstalled(clis) { ... }

A mirrored test cannot fail when the real function changes — plausibly why this gap went unnoticed. The extraction follows the .mjs pattern web/README.md prescribes for exactly this reason.

Mutation-checked: reverting pickDefaultInstalled to the sole-install behavior turns 2 tests red.

Tests

web/ suite: 502 pass / 0 fail (was 497). npx tsc --noEmit clean. npm run build clean.

Checklist

  • I have read CONTRIBUTING.md
  • Bug fix — exempt from issue-first
  • My PR does not include personal data
  • I ran node test-all.mjs and all tests pass — see note
  • My changes respect the Data Contract (only web/ system-layer files touched)
  • My changes align with the project roadmap

On the test box: node test-all.mjs reports 2 failures on this branch. Both are pre-existing and environmental — verified by stashing the changes and re-running on a pristine tree, which fails identically: live archive: no PDF written to jds/ (fetches a live posting over the network), and tests/mjs-files.test.mjs flagging a gitignored file in my own checkout that won't exist in CI. Flagging rather than ticking a box that isn't strictly true.

Summary

Users with multiple installed AI CLIs now get the first installed CLI saved to localStorage. AI features no longer treat the Config page as unconfigured.

Changes

  • web/src/components/config-form.tsx:72-83: Select and persist the first installed CLI when no saved selection exists.
  • web/src/lib/cli-pick.mjs:5-21: Add shared helpers for sole-CLI resolution and default selection.
  • web/src/lib/saved-cli.ts:28-39: Re-export the helpers and retain sole-CLI behavior for unambiguous resolution.
  • web/tests/lib/saved-cli-pick.test.mjs:38-75: Test single-, multi-, zero-, sparse-, and undefined-CLI cases against the real helpers.

Validation

  • 502 web tests pass.
  • TypeScript checks pass.
  • The production build succeeds.

System files

No changes affect AGENTS.md, modes/, update-system.mjs, DATA_CONTRACT.md, providers/, or .github/.

…sole one

The sole-install persistence added in config-form.tsx fixed the one-CLI case
but left the multi-CLI case in exactly the state its own comment warns about:

    const only = list.filter((c) => c.installed);
    if (only.length !== 1) return list.find((c) => c.installed)?.id || "";
    if (!readSavedCliId()) persistCliId(only[0].id);

With two or more CLIs installed the early return highlights the first one and
persists nothing. Every AI surface reads `career-ops:config` directly and sends
it as `cliId`, so on a machine with claude + codex (or, here, claude, codex,
gemini and opencode) Config shows a selected CLI over empty localStorage and
every AI feature silently does nothing -- /api/explore/ai answers
{"error":"query and cliId required"} and the console shows "no CLI configured".

Nothing prompts a Save, because nothing looks unset. Reproduced on a clean
browser profile against 1.32.0: localStorage is empty after visiting /config.

Whatever Config renders as selected is what it must persist, so the pick is now
pickDefaultInstalled() -- first installed, any count -- while pickSoleInstalled
keeps its narrower meaning for resolveCliId().

Both live in src/lib/cli-pick.mjs, and saved-cli.ts re-exports them. That is
what lets tests/lib/saved-cli-pick.test.mjs import the REAL functions: the suite
previously held a hand-copied mirror of pickSoleInstalled, which by
construction cannot fail when the real function changes. Reverting
pickDefaultInstalled to the sole-install behaviour now turns 2 tests red.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Yafkyw2o71V3kyMu4QRbqB
@github-actions

Copy link
Copy Markdown
Contributor

Welcome to career-ops, @jsmith7735! Thanks for your first PR.

A few things to know:

  • Tests run automatically, but on a first contribution they wait for a maintainer to approve the run: if it looks stuck, that queue is on us, not on you
  • Link a related issue if this is a feature (bug fixes, providers, docs and translations need no issue)
  • CONTRIBUTING.md has the specifics

We review every PR by hand. Join our Discord if anything blocks you.

@coderabbitai

coderabbitai Bot commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 260d7568-7607-4cbe-a805-6309648199cb

📥 Commits

Reviewing files that changed from the base of the PR and between bb641dc and 319420d.

📒 Files selected for processing (4)
  • web/src/components/config-form.tsx
  • web/src/lib/cli-pick.mjs
  • web/src/lib/saved-cli.ts
  • web/tests/lib/saved-cli-pick.test.mjs
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • career-ops-hq/career-ops-docs (manual)

Included review availability: Your plan provides up to 10 included reviews per hour; 9 remain after this review.


📝 Walkthrough

Walkthrough

ConfigForm now persists the first installed CLI when no CLI is selected. CLI selection helpers moved to cli-pick.mjs, while saved-cli.ts re-exports them. Tests cover sole and default selection behavior.

Changes

CLI selection behavior

Layer / File(s) Summary
Shared CLI selection helpers
web/src/lib/cli-pick.mjs, web/src/lib/saved-cli.ts
Added pickSoleInstalled and pickDefaultInstalled. saved-cli.ts imports and re-exports both helpers.
Configuration default persistence
web/src/components/config-form.tsx
ConfigForm uses pickDefaultInstalled and persists the selected CLI when no CLI is already saved.
Selection behavior tests
web/tests/lib/saved-cli-pick.test.mjs
Tests cover sparse input, installed-CLI ordering, empty results, and multi-CLI default persistence.

Priority: ➖ Normal

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

Change: Bug fix

Suggested labels: area:web

Merge Risk: ⚪ Minimal · up to 31942

The CLI selection persistence change is covered by the supplied tests and is ready to merge.

🚥 Pre-merge checks | ✅ 8 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Agent-Operated Pr Disclosure ❓ Inconclusive The description does not contain ## AI assistance or ## Human review. The reviewed commit identifies the author as J. Smith <j.smith@example.com>, not app/copilot-swe-agent, and it includes a … Provide the PR source branch, author identity, and labels. If the source branch matches copilot/* or the author is app/copilot-swe-agent, this check warns unless the description adds both required sections and the PR has the `agent-gene…
✅ Passed checks (8 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title uses the required Conventional Commits prefix and scope. It accurately describes the web change: persisting the default CLI for any installed count.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
User Layer Untouched ✅ Passed PASS: The authoritative PR diff changes only web/src/components/config-form.tsx, web/src/lib/cli-pick.mjs, web/src/lib/saved-cli.ts, and web/tests/lib/saved-cli-pick.test.mjs. None matches the…
No Personal Data ✅ Passed PASS: The pull-request diff contains no real personal data. The changed content contains CLI identifiers and code comments only, such as claude, codex, and gemini in `web/tests/lib/saved-cli-pic…
Shipped File Registered ✅ Passed PASS: The PR adds only files under web/; the new .mjs is web/src/lib/cli-pick.mjs:1, not a new top-level file. The updater explicitly excludes web/ because it is not shipped by `update-system.…
Provider Contract ✅ Passed PASS: The PR changes only web/src/components/config-form.tsx, web/src/lib/cli-pick.mjs, web/src/lib/saved-cli.ts, and web/tests/lib/saved-cli-pick.test.mjs. No .mjs provider under providers/ changed, …
Full details: Agent-Operated Pr Disclosure

Explanation

The description does not contain ## AI assistance or ## Human review. The reviewed commit identifies the author as J. Smith &lt;j.smith@example.com&gt;, not app/copilot-swe-agent, and it includes a Claude co-author, but the source branch and PR labels are not available in the review checkout. Therefore, the required copilot/* branch condition and agent-generated label cannot be verified.

Resolution

Provide the PR source branch, author identity, and labels. If the source branch matches copilot/* or the author is app/copilot-swe-agent, this check warns unless the description adds both required sections and the PR has the agent-generated label.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • 🛠️ register-shipped-file
  • 🛠️ provider-test-scaffold
  • 🛠️ sync-language-mode
🚀 Post-Merge Actions
  • localization drift report
  • documentation drift report

Warning

⚠️ This pull request shows signs of AI-generated slop (redundant_comments). It has been flagged by CodeRabbit slop detection and should be reviewed carefully.


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.

@coderabbitai coderabbitai Bot added the quality-check Automated signal: a maintainer reads this one by hand before anything else happens label Sep 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

quality-check Automated signal: a maintainer reads this one by hand before anything else happens

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant