Originally created by: adityaharishch
Three related pieces of work on the AI-provider layer: a redesigned picker shared by setup and Settings, a substantial hardening of how Meridian invokes cursor-agent, and a last-resort fallback so a permanently-broken agent CLI no longer costs us the summary.
Setup and Settings now render the same component, so they cannot drift. Three recommended providers (Claude Code, Codex, Cursor) plus bring-your-own-API-key; each opens a detail view that installs the CLI, tests the connection, and sets it as default. Copilot is dropped from the chooser but stays a valid stored value, so legacy users get a switch banner rather than being stranded.
Two behavioural fixes worth calling out:
value reads from settings.llm_provider on disk, and the commit only mutates state on success. The two regression tests that pinned the old shape are rewritten against the new invariants.In-app install runs the vendor installer through the user's login shell (the tray is a Finder-launched .app with the stripped launchd PATH, which has no npm/node). Cursor sign-in uses cursor-agent login with the browser enabled, so the user's subscription is used - no API key, nothing metered.
cursor-agent is a coding agent, but Meridian only ever asks it for inference over untrusted text (coding-agent transcripts, screen OCR). Its defaults are wrong in both directions: too much capability, and a large irrelevant context. src/llm/cursor_cli.rs is now the single source of truth for invoking it.
This also closes a real gap: the summariser built its own argv, so it summarised untrusted transcripts with cursor-agent's default full write + shell tool access. It now inherits the same hardening as the provider backend.
| Lever | Effect |
|---|---|
--allowed-tools "" |
no tools at all; 21,040 -> 9,970 tokens |
sandbox HOME |
<agent_skills> 190 entries -> 0 |
--workspace <empty> |
stops Cursor injecting the user's ~/CLAUDE.md |
--mode ask |
read-only, server-enforced |
--output-format json |
real error / usage-limit detection |
CURSOR_API_KEY stripped |
subscription guaranteed, never metered |
Net: ~21,000 -> ~3,400 input tokens per call, summary quality unchanged.
The non-obvious part of the sandbox: the four .cursor skill entries must be recreated empty and read-only, because cursor-agent self-provisions its built-in skills and plugin cache on startup and would otherwise just repopulate them. The sandbox never touches the user's files - it only creates entries under its own directory, and the real skill directories are simply not linked in.
Every degradation is driven by a detected cause, never a blind retry: unknown flag -> unhardened call, auth failure -> real HOME, unavailable model -> auto. A usage limit deliberately does not retry, because Cursor limits are account-level and a second call would burn quota to hit the same wall.
cursor-agent has no --no-session-persistence, so every call writes a chat into the store the cursor-cli source ingests from. Only the summariser was fingerprinted, so every other AI process routed through Cursor was ingested as developer activity and re-summarised next tick. All Meridian-issued prompts now carry a marker the ingest guard drops.
A coding-agent transcript is still summarised by its own CLI - that agent holds the context and its subscription is already paid for. What changes is the outcome when that CLI is permanently unable to answer. Previously the row was simply left pending and dead-lettered after three drains; now it escalates once to the provider the user chose in Settings.
1. the session's own CLI x2 attempts (codex -> codex, cursor -> cursor-agent, ...)
2. rate limit? -> STOP. back the source off, retry on a later tick
3. otherwise failed -> the global provider, ONCE
4. still nothing -> row stays pending; dead-lettered after 3 drains
Step 2 is the load-bearing one. A RateLimited breaks out of the loop before the fallback code is reachable, so there is no path where a quota triggers a substitute - a quota refills, and is waited out. Only a Failed (crashed, not installed, signed out, subscription ended, unusable output) escalates, and only after every retry is spent.
Two decisions worth a reviewer's eye:
fallback::try_summarise returns Ok(None) and spawns nothing. A third run of the same binary would fail identically.Failed, not RateLimited. That flag drives the per-agent-source backoff, and the fallback is a different account - parking Codex's queue because Claude ran out of quota would punish the wrong subscription. The resolver's provider-level backoff already prevents redialling.Fallback summaries persist as summary_source = "fallback:<provider>", so a substitute's work is never mistaken for the agent's own. Nothing downstream reads that column today, so the vocabulary change is additive.
cursor.com/install is a rolling script with no version flag. Pinned to the build this was verified against, by rewriting version strings by pattern - a literal swap would silently no-op the day Cursor publishes a new build and hand the user "latest" again.
-D warnings, 707 Rust tests, tray check, UI build, 252 UI tests - all green<agent_skills> block verified by decoding the actual persisted request
Originally posted by: coderabbitai[bot]
✨ Finishing Touches
🧪 Generate unit tests (beta)
- [ ] Create PR with unit tests - [ ] Commit unit tests in branch `feat/ai-provider-picker-redesign`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
- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai) - [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai) - [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai) - [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)Comment
@coderabbitai helpto get the list of available commands.Originally posted by: adityaharishch
Thanks - this was a genuinely good review. I verified every finding against the code rather than taking them on trust, and all eight were real. All are now fixed in
4bada8dc,3168ed97,7b0f59ab.Two of them (#4, [#8]) were tests that couldn't fail, which is the worst kind of finding to receive and the most useful to get. I reproduced both before fixing them.
Blocking
#1 Windows build. Confirmed in the CI log -
error[E0425]in thecheckstep, andtest/check traynever ran. Applied your fix. I could not cross-compile locally to prove it (no MSVC toolchain here), so this rests on the reasoning plus the next CI run.#2 Stale model override in the wizard. Confirmed, including the Cursor consequence you traced - a non-empty
cfg.modelsuppressesDEFAULT_MODEL, so the wizard path could move a user off the ZDR-eligible model. Fixed by sharing the field-building (providerChoiceFieldsinllm-providers.ts), not by copying the line, since the divergence is the finding.customkeeps its model - that one is a real per-endpoint setting rather than a leftover.High
#3 Recovery not shared. You were right that this is the same drift one level up. The ladder now lives in
cursor_cli::run_hardened, which takes the per-attempt closure; each call site supplies aCursorCallErrorimpl so a rate limit still returnsNoneand degrades nothing. Eight tests drive the ladder directly - healthy path takes one attempt, each lever drops on its own detected cause, a rate limit never degrades, and it terminates once the levers are spent.#4 Invariants pinned on the compliant surface. Reproduced: I rewrote
page.tsxback to{ llm_provider: id }and the suite stayed green. Both test files now readapp/setup/page.tsx, and with that mutation applied the two new assertions fail.model-pickernow pins the rule on the shared builder both surfaces call.Medium
#5 Optimistic state / superseded-write race. Fixed at the root: setup commits first and moves the UI on success, matching Settings.
setProviderreturns the promise (andWizis typedPromise<void>), so the shared detail view can render "Switching…" and surface a failure in the wizard too.#6
temp_dir()inside%USERPROFILE%. Real. Addedscratch_root(), which prefers%PUBLIC%when temp resolves under the home and warns if neither is outside it. Your suspicion about the test was right in mechanism - it re-derived the path and compared raw strings - so it now asserts on what the product computes, through a canonicalisingis_under.Related bug this surfaced: the sandbox set only
HOME, butcursor-agentis a Node bundle and Node readsUSERPROFILEon Windows - so the whole skills sandbox was a no-op there regardless of the path. Both are set now, with a test.#7 Pin fails open. Agreed, and thanks for actually fetching the installer.
install_providernow comparescursor-agent --versionagainst the pin and warns on mismatch, naming the constant to update. Deliberately does not fail the install - an unpinned CLI still works, because the ladder degrades on an unknown flag.#8 Tautological marker test. Reproduced exactly as you did. Prompt building is extracted to
build_prompt, and the test asserts on that. Verified by mutation: removing the marker now fails. Also took your point that the contract iscontains, not position - the assertion no longer over-specifies.Minor
All four fixed: login stdout is now drained as it arrives and included in the timeout message (with the three copies of the output-tail trimming folded into one helper); cwd pinned to
neutral_workspace(); the shared-heading claim made true by actually sharing it; stale model-UI comments corrected; and the brittlenot.toContain('pending')now matches the identifier.Verification
fmt, clippy
-D warnings, 717 Rust tests (+13), tray check, UI build, 258 UI tests (+6).One thing I'd flag: #1 and [#6] are the two I could not fully verify locally, since both are Windows-specific and I have no MSVC toolchain. [#1] is mechanical, but the
%PUBLIC%path in [#6] is reasoned rather than measured - if you have a Windows box, that is the piece worth a real run.Related
Tickets:
#1Tickets:
#6Tickets:
#8Ticket changed by: adityaharishch