Skip to content

Ios Fixes#229

Merged
arul28 merged 6 commits into
mainfrom
ade/ios-fixes-13925e5f
May 2, 2026
Merged

Ios Fixes#229
arul28 merged 6 commits into
mainfrom
ade/ios-fixes-13925e5f

Conversation

@arul28
Copy link
Copy Markdown
Owner

@arul28 arul28 commented May 1, 2026

Summary

Describe the change.

What Changed

Key files and behaviors.

Validation

How you tested.

Risks

Anything to watch.

Summary by CodeRabbit

  • New Features

    • Cursor integration now uses the Cursor SDK with API-key authentication.
    • New shared model catalog API exposed (used by UI and remote command chat.modelCatalog).
    • macOS Keychain support for storing API keys.
  • Improvements

    • Faster, more reliable model discovery with probe/cached modes and better runtime readiness/status.
    • Clearer Cursor connection/status and troubleshooting messages reflecting SDK/API-key state.
    • iOS simulator helper packaging, build-cache relocation, and streaming fallback behavior improved.

Greptile Summary

This PR introduces Cursor SDK API-key authentication (replacing CLI-based auth), a shared model catalog API consumed by both the desktop UI and the iOS app, macOS Keychain storage for API keys, iOS simulator helper packaging improvements, and streaming fallback behaviour refinements. The most significant structural addition is getModelCatalog / chat.modelCatalog, which aggregates models across all provider groups into a structured response used by the new iOS model picker.

Confidence Score: 4/5

Safe to merge; all findings are P2 — no runtime errors or data-loss paths introduced.

No P0 or P1 issues found. The two P2 findings are: a dead ternary in findContainingAppBundle (macOS-only path, no functional impact) and the absence of per-provider error fields in the catalog response (UX gap, not a crash). Previously-flagged concerns around keychain mutation ordering, TOCTOU socket paths, and @MainActor isolation have all been addressed in this PR.

apps/desktop/src/main/services/ios/iosSimulatorService.ts (dead ternary), apps/desktop/src/main/services/chat/agentChatService.ts (catalog error propagation)

Important Files Changed

Filename Overview
apps/desktop/src/main/services/chat/agentChatService.ts Adds getModelCatalog which aggregates models across all 5 provider groups and serialises them into a structured catalog for iOS; provider errors are silently swallowed returning an empty group indistinguishable from a successful empty response.
apps/desktop/src/main/services/chat/cursorSdkPool.ts Adds per-session private socket directories, ensurePrivateSocketPath with fd-based ownership/type checks, cleanupCursorSdkRuntimePaths for one-shot catalog/cloud requests (addressing prior accumulation issue), and exports resolveCursorSdkUserHome.
apps/desktop/src/main/services/chat/cursorSdkHooks.ts New module: writes the ADE bridge script and updates ~/.cursor/hooks.json in the user's real home directory; handles Electron/Node/Windows variants and guards non-ADE Cursor sessions with env-var checks in the bridge script.
apps/desktop/src/main/services/chat/cursorModelsDiscovery.ts Adds SDK + REST API dual-probe with TTL cache, auth-failure classification, background warm-up, and probeCursorSdkModelDiscovery; cached-only mode correctly defers to warm-up on first access.
apps/desktop/src/main/services/ios/iosSimulatorService.ts Relocates build cache for packaged apps, removes the blanket IOSurface disable for packaged builds, improves stream-fallback status emission ordering; findContainingAppBundle contains an identical dead ternary in both branches.
apps/desktop/src/main/services/ai/aiIntegrationService.ts Adds invalidateProviderReadinessCaches (exported for IPC handlers), extends verifyApiKeyConnection to probe Cursor model discovery after key verification, and prevents stale status cache from surviving a generation change.
apps/desktop/src/shared/modelCatalog.ts New shared module: provider group classification, subsection key/label/sort helpers, and buildProviderGroupBlocks used by both the desktop UI and the new getModelCatalog API.
apps/ios/ADE/Services/SyncService.swift Adds @MainActor-isolated getChatModelCatalog and cachedChatModelCatalog with per-host TTL cache and in-flight deduplication; @MainActor on listChatModels addresses the previously-flagged concurrency race.
apps/ios/ADE/Views/Work/WorkModelCatalog.swift Adds host-catalog-based workModelCatalogGroups overload, workCatalogModelOption, and new registry/runtime ID lookup helpers for Claude and Codex models including gpt-5.3-codex-spark and gpt-5.2.
apps/desktop/src/main/services/pty/ptyService.ts Adds sanitizeGeneratedCliTitle to strip emojis, ANSI, quotes, and generic provider command tokens from AI-generated session titles; enrichSessions now surfaces live PTY status/endedAt/exitCode overrides consistently.
apps/desktop/src/main/services/chat/cursorSdkWorker.ts Removes inline hook/config file writers (moved to cursorSdkHooks.ts), improves error detail extraction, and enriches cloud run results with agent name via Agent.get.

Comments Outside Diff (3)

  1. apps/desktop/src/main/services/chat/cursorSdkPool.ts, line 451-472 (link)

    P1 Orphaned stateRoot directories accumulate per one-shot request

    runCursorSdkCatalogRequest and runCursorSdkCloudRequest build their poolKey with Date.now() + Math.random(), so every call passes a unique key to createCursorSdkConnection. Inside that function, fs.mkdirSync(paths.stateRoot, { recursive: true }) creates {projectRoot}/.ade/cache/cursor-sdk/{unique_hash}/state/. These directories are never cleaned up — neither in the finally block that calls releaseCursorSdkConnection, nor anywhere else. The Cursor SDK worker may also write state files into stateRoot before being disposed. Users who access the model catalog or cloud APIs frequently will accumulate a new directory subtree per call indefinitely.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: apps/desktop/src/main/services/chat/cursorSdkPool.ts
    Line: 451-472
    
    Comment:
    **Orphaned `stateRoot` directories accumulate per one-shot request**
    
    `runCursorSdkCatalogRequest` and `runCursorSdkCloudRequest` build their `poolKey` with `Date.now() + Math.random()`, so every call passes a unique key to `createCursorSdkConnection`. Inside that function, `fs.mkdirSync(paths.stateRoot, { recursive: true })` creates `{projectRoot}/.ade/cache/cursor-sdk/{unique_hash}/state/`. These directories are never cleaned up — neither in the `finally` block that calls `releaseCursorSdkConnection`, nor anywhere else. The Cursor SDK worker may also write state files into `stateRoot` before being disposed. Users who access the model catalog or cloud APIs frequently will accumulate a new directory subtree per call indefinitely.
    
    How can I resolve this? If you propose a fix, please make it concise.

    Fix in Claude Code

  2. apps/desktop/src/main/services/chat/cursorSdkPool.ts, line 451-472 (link)

    P1 Per-call socket subdirectory accumulation in /tmp/

    runCursorSdkCatalogRequest (and runCursorSdkCloudRequest at line 500) build their poolKey with Date.now() + Math.random(), giving each one-shot call a unique key. socketPathFor now maps that key to /tmp/ade-cursor-sdk-${uid}/${hashKey(poolKey)}/hook.sock, and ensurePrivateSocketPath creates the per-call hash subdirectory. On dispose, removeSocketIfNeeded in the worker deletes only the hook.sock file — the empty hash directory is never removed. Every catalog or cloud API call leaves a stale directory under /tmp/ade-cursor-sdk-${uid}/. Unlike the tmpdir socket files in the old flat layout, these new subdirectories require an explicit rmdir that isn't currently issued.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: apps/desktop/src/main/services/chat/cursorSdkPool.ts
    Line: 451-472
    
    Comment:
    **Per-call socket subdirectory accumulation in `/tmp/`**
    
    `runCursorSdkCatalogRequest` (and `runCursorSdkCloudRequest` at line 500) build their `poolKey` with `Date.now() + Math.random()`, giving each one-shot call a unique key. `socketPathFor` now maps that key to `/tmp/ade-cursor-sdk-${uid}/${hashKey(poolKey)}/hook.sock`, and `ensurePrivateSocketPath` creates the per-call hash subdirectory. On dispose, `removeSocketIfNeeded` in the worker deletes only the `hook.sock` file — the empty hash directory is never removed. Every catalog or cloud API call leaves a stale directory under `/tmp/ade-cursor-sdk-${uid}/`. Unlike the tmpdir socket files in the old flat layout, these new subdirectories require an explicit `rmdir` that isn't currently issued.
    
    How can I resolve this? If you propose a fix, please make it concise.

    Fix in Claude Code

  3. apps/desktop/src/main/services/chat/cursorSdkPool.ts, line 483-505 (link)

    P1 Orphaned per-call directories for one-shot catalog requests

    runCursorSdkCatalogRequest (and runCursorSdkCloudRequest at line 533) build a unique poolKey per call via Date.now() + Math.random(). This creates two temporary directory trees that survive beyond the request:

    1. {projectRoot}/.ade/cache/cursor-sdk/{hash}/cleanupCursorSdkStateRoot removes only the nested state/ subdirectory; the parent {hash}/ dir remains empty after every catalog call.
    2. /tmp/ade-cursor-sdk-{uid}/{hash}/removeSocketIfNeeded in the worker deletes only hook.sock; the parent {hash}/ subdirectory is never removed.

    Both accumulate indefinitely. Fix: in cleanupCursorSdkStateRoot use cacheRoot (parent of stateRoot) instead, and add an fs.rmSync for the socket's parent directory in releaseCursorSdkConnection.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: apps/desktop/src/main/services/chat/cursorSdkPool.ts
    Line: 483-505
    
    Comment:
    **Orphaned per-call directories for one-shot catalog requests**
    
    `runCursorSdkCatalogRequest` (and `runCursorSdkCloudRequest` at line 533) build a unique `poolKey` per call via `Date.now() + Math.random()`. This creates two temporary directory trees that survive beyond the request:
    
    1. `{projectRoot}/.ade/cache/cursor-sdk/{hash}/``cleanupCursorSdkStateRoot` removes only the nested `state/` subdirectory; the parent `{hash}/` dir remains empty after every catalog call.
    2. `/tmp/ade-cursor-sdk-{uid}/{hash}/``removeSocketIfNeeded` in the worker deletes only `hook.sock`; the parent `{hash}/` subdirectory is never removed.
    
    Both accumulate indefinitely. Fix: in `cleanupCursorSdkStateRoot` use `cacheRoot` (parent of `stateRoot`) instead, and add an `fs.rmSync` for the socket's parent directory in `releaseCursorSdkConnection`.
    
    How can I resolve this? If you propose a fix, please make it concise.

    Fix in Claude Code

Fix All in Claude Code

Prompt To Fix All With AI
Fix the following 3 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 3
apps/desktop/src/main/services/ios/iosSimulatorService.ts:405-415
**Dead ternary – both branches are identical**

`current === path.sep ? path.join(current, part) : path.join(current, part)` evaluates the same expression regardless of the condition, making the comparison unreachable dead code. The intent was probably to special-case the root separator so segments are appended without a double `/`, but `path.join` already handles that correctly — the ternary is simply redundant. The real concern is that on Windows (where `path.sep === "\\"`) an initial `current = ""` produces `path.join("", "C:")``"C:"` on the first iteration, and then `path.join("C:", "Applications")``"C:Applications"` (missing the backslash), corrupting the reconstructed path. The function is only called from macOS-only code paths so the Windows bug doesn't fire, but the dead branch signals the intent was never properly encoded.

### Issue 2 of 3
apps/desktop/src/main/services/ios/iosSimulatorService.ts:425
Both ternary branches are identical — the condition is dead code. Drop it and let `path.join` handle the separator normalisation (which it already does correctly).

```suggestion
    current = path.join(current || path.sep, part);
```

### Issue 3 of 3
apps/desktop/src/main/services/chat/agentChatService.ts:1
**`getModelCatalog` swallows per-provider errors without any indication in the response**

Each provider fetch is wrapped in a `try/catch` that returns `{ provider, models: [] }` on failure. The returned `AgentChatModelCatalog` has no per-group error field, so iOS callers cannot distinguish "zero models available" from "provider fetch failed". If `getAvailableModels` for, say, `cursor` throws because the Cursor SDK can't initialise, the catalog returned to the iOS client looks identical to a successful empty Cursor section — the model picker silently shows nothing for that group without any error message. Consider adding an optional `error` or `failureKind` field to `AgentChatModelCatalogGroup` (and its Swift counterpart) so the client can surface a "failed to load" indicator.

Reviews (7): Last reviewed commit: "ship: fix review and ci follow-up" | Re-trigger Greptile

@vercel
Copy link
Copy Markdown

vercel Bot commented May 1, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
ade Ignored Ignored Preview May 2, 2026 0:23am

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 1, 2026

📝 Walkthrough

Walkthrough

Replaces Cursor ACP/CLI integration with a Cursor SDK + API-key flow; implements an SDK worker pool, SDK-based model discovery/probing, macOS Keychain API-key storage, shared modelCatalog utilities, runtime-driven session/title behaviors, iOS simulator helper build changes, audit/dev tooling, and many tests/docs updates.

Changes

Cursor SDK integration & runtime

Layer / File(s) Summary
Data Shape / Protocol
apps/desktop/src/main/services/chat/cursorSdkProtocol.ts
Worker init payload field renamed homeDiruserHomeDir. CursorSdkCloudRunStartedResult adds optional agentName.
Core Worker / Pool
apps/desktop/src/main/services/chat/cursorSdkWorker.ts, apps/desktop/src/main/services/chat/cursorSdkPool.ts
Implements SDK worker init (sets ADE env, creates dirs, ensures user hook), hardened socket paths using hashed poolKey, private dir/socket ownership checks, stateRoot lifecycle, RPC request type bookkeeping, and centralized worker env builder.
Hook Installer
apps/desktop/src/main/services/chat/cursorSdkHooks.ts, .../cursorSdkHooks.test.ts
Adds ADE bridge hook script writer, Windows .cmd wrapper, hooks.json merging/validation, and tests for idempotency/behavior/timeout/error cases.
Cloud result integration
apps/desktop/src/main/services/chat/cursorSdkWorker.ts
After cloud streaming, fetches Agent.get to include agentName in returned results.
Tests
.../cursorSdkPool.test.ts, .../cursorSdkHooks.test.ts
Unit tests for path/env construction, worker env, hook command shaping, and behavior scenarios.

Cursor model discovery & policy

Layer / File(s) Summary
Types / Error model
apps/desktop/src/main/services/chat/cursorModelsDiscovery.ts
Adds discovery result types and CursorModelDiscoveryFailureKind (`auth
Core discovery
apps/desktop/src/main/services/chat/cursorModelsDiscovery.ts
Implements timed probe pipeline, SDK list + HTTP fallback, background warm-up with de-dup, cached rows with TTL/generation guard, recent-failure tracking, and probeCursorSdkModelDiscovery/listCursorModelsFromSdk options.
Descriptor conversion / API
apps/desktop/src/main/services/chat/cursorModelsDiscovery.ts
Converts SDK rows → ModelDescriptor via cursorRowsToDescriptors; discoverCursorSdkModelDescriptors now supports mode (probe/cached-only) and suppresses fallbacks after auth failures.
Policy / Path guarding
apps/desktop/src/main/services/chat/cursorSdkPolicy.ts, .../cursorSdkPolicy.test.ts
Adds realpath-aware canonicalization, nearest-existing ancestor resolution, shell-command token extraction, symlink escape checks, lane-scoped allow rules for Cursor support reads, and userHomeDir-aware evaluation.
Tests
.../cursorModelsDiscovery.test.ts, .../cursorSdkPolicy.test.ts
Coverage for warm-up/cached/probe behaviors, timeout/auth classification, fallback suppression, and path/symlink policy rules.

API key storage & Keychain

Layer / File(s) Summary
Data/Status
apps/desktop/src/main/services/ai/apiKeyStore.ts, apps/desktop/src/shared/types/config.ts
Expands ApiKeyStoreStatus / AiSettingsStatus.apiKeyStore with macOS Keychain metadata fields; exports test hook __setSafeStorageForTests.
Core storage
apps/desktop/src/main/services/ai/apiKeyStore.ts
Adds macOS Keychain backend (uses /usr/bin/security via spawnSync), provider-index entry in Keychain, migration from legacy encrypted store, on-demand Keychain reads, and error tracking.
Wiring
apps/desktop/src/main/services/ai/aiIntegrationService.ts
verifyApiKeyConnection uses probe to surface auth failures and conditionally invalidates provider readiness caches.
Tests
apps/desktop/src/main/services/ai/apiKeyStore.test.ts
Extensive tests for Keychain writes/reads/deletes, migration, index-write failures, decryption-failure reporting, and cold-load/ on-demand behaviors.

AI integration, provider health & readiness

Layer / File(s) Summary
Runtime health types
apps/desktop/src/main/services/ai/providerRuntimeHealth.ts
Adds ProviderRuntimeHealthProvider union including "cursor", updates public APIs to accept it.
Auth reporting
apps/desktop/src/main/services/ai/authDetector.ts
verifyCursorApiKey reports runtime-ready or emits an auth-failed runtime-health event with a human message on auth rejection.
Integration service
apps/desktop/src/main/services/ai/aiIntegrationService.ts, .../aiIntegrationService.test.ts
Adds invalidateProviderReadinessCaches(), reworks verifyApiKeyConnection to run timed probe for Cursor, tags status requests with generation to avoid stale writes, and exposes cache invalidation method; tests for Cursor verify behaviors.
Provider connection status
apps/desktop/src/main/services/ai/providerConnectionStatus.ts, .../providerConnectionStatus.test.ts
Applies runtime-health to Cursor status, driving usageAvailable/runtimeAvailable/blocker logic and tests for auth-failed mapping.

Agent chat service, titles & model catalog

Layer / File(s) Summary
Types / Catalog API
apps/desktop/src/shared/types/chat.ts
Adds AgentChat model-catalog types for catalog/group/provider/subsection models.
Model catalog utilities
apps/desktop/src/shared/modelCatalog.ts
New shared modelCatalog: grouping, subsection keys/labels, provider badges/colors, query matching, sort helpers, and buildProviderGroupBlocks.
AgentChat service
apps/desktop/src/main/services/chat/agentChatService.ts, .../agentChatService.test.ts
Adds getModelCatalog(), richer model metadata, descriptor-aware reasoning-effort validation, runtime-driven title adoption with persisted runtimeTitleAdopted flag, Codex lifecycle sync updates, and Cursor runtime/auth preflight reporting; adds tests for title adoption and serialization.
Renderer wiring
apps/desktop/src/renderer/components/chat/AgentChatPane.tsx
Centralizes model-id ordering, probes Cursor models only when runtime ready (activateRuntime), merges probe results, and uses shared ordering helper.

OpenCode / PTY / session title hygiene

Layer / File(s) Summary
API surface
apps/desktop/src/main/services/opencode/openCodeRuntime.ts
Makes title optional (`string
PTY title sanitization
apps/desktop/src/main/services/pty/ptyService.ts, .../ptyService.test.ts
Adds sanitizeGeneratedCliTitle to strip ANSI/pictographs, reject provider slash-command-like or placeholder titles; adjusts enrichment to force running when live; tests added for CLI title seeding and enrichment.
Session UI
apps/desktop/src/renderer/lib/sessions.ts, tests
isLowSignalSessionLabel now treats provider slash-command inputs as low-signal.

Shared registry / profiles

Layer / File(s) Summary
Registry updates
apps/desktop/src/shared/modelRegistry.ts, .../modelRegistry.test.ts
Adds openai/gpt-5.3-codex-spark and openai/gpt-5.2, adjusts anthropic/claude-opus-4-7 contextWindow and reasoning tiers, updates dynamic OpenCode auth routing.
Profiles
apps/desktop/src/shared/modelProfiles.ts, .../modelProfiles.test.ts
thinkingLevelToReasoningEffort no longer maps "max""high"; test adjusted.
Renderer grouping adapter
apps/desktop/src/renderer/components/shared/providerModelSelectorGrouping.ts
Replaced prior local grouping implementation with export * from "../../../shared/modelCatalog".

iOS simulator helpers & streaming

Layer / File(s) Summary
Docs / packaging
apps/desktop/native/ios-sim-helpers/README.md, apps/desktop/package.json
Docs for ADE_IOS_SIM_HELPER_BUILD_ROOT; packaging adds native/ios-sim-helpers as extraResources filtered list.
Build script
apps/desktop/native/ios-sim-helpers/build.sh
Allows ADE_IOS_SIM_HELPER_BUILD_ROOT override and attempts ad-hoc codesign when available (non-fatal, logs warnings).
Pack/unpack validation
apps/desktop/scripts/after-pack-runtime-fixes.cjs, apps/desktop/scripts/validate-mac-artifacts.mjs
Marks build.sh executable in packaged resources and validates helper source files are present and build cache absent.
Runtime
apps/desktop/src/main/services/ios/iosSimulatorService.ts, .../iosSimulatorService.test.ts
Detects helper roots from listed source files, computes per-user build root when packaged, exposes test reset helper, removes packaged gating for IOSurface availability, and improves stream fallback/degradation reporting and selection logic.

Audit & developer tooling

Layer / File(s) Summary
Audit CLI
apps/desktop/scripts/audit-chat-model-runtime.mjs
New Node CLI script for list/dry-run/smoke modes: transpiles registry, builds bounded test cases, can drive ADE renderer via CDP for smoke tests, verifies agentChat.create payload mappings, and reports results (human/JSON).
Dev launcher / packaging scripts
apps/desktop/scripts/dev.cjs, apps/desktop/scripts/after-pack-runtime-fixes.cjs
Dev launcher checks remote-debugging port availability and constructs electron args programmatically; after-pack sets helper script perms.
Packaging
apps/desktop/package.json, apps/desktop/scripts/validate-mac-artifacts.mjs
Adds test:chat-model-runtime-audit script and bundles iOS helper resources with validation.

Preload, IPC, main startup

Layer / File(s) Summary
Preload bridge
apps/desktop/src/preload/preload.ts, .../preload.test.ts
ai.verifyApiKey clears aiStatusCache before/after calling IPC verify; test ensures cache cleared after verification.
IPC wiring
apps/desktop/src/main/services/ipc/registerIpc.ts
aiStoreApiKey / aiDeleteApiKey call ctx.aiIntegrationService.invalidateProviderReadinessCaches() (errors logged, not thrown). sessionsGet prefers PTY-enriched session shapes.
Startup
apps/desktop/src/main/main.ts
recoverManagedOpenCodeOrphans() invoked fire-and-forget with rejection logging instead of awaited in startup path.

Sync & iOS host catalog

Layer / File(s) Summary
Server action & iOS types
apps/desktop/src/shared/types/sync.ts, apps/ios/ADE/Models/RemoteModels.swift
Adds "chat.modelCatalog" remote action; introduces Codable AgentChatModelCatalog* types for host catalog.
Sync client
apps/desktop/src/main/services/sync/syncRemoteCommandService.ts, apps/ios/ADE/Services/SyncService.swift
Registers remote action chat.modelCatalog; iOS SyncService adds TTL cache and in-flight dedupe plus cachedChatModelCatalog() / getChatModelCatalog() APIs.
iOS UI
apps/ios/ADE/Views/Work/*
Work model picker uses host catalog (caching) and preserves reasoningEfforts in WorkModelOption; selection syncing uses semantic equivalence.

UI / tests / minor UX

Layer / File(s) Summary
Providers UI
apps/desktop/src/renderer/components/settings/ProvidersSection.tsx, .../ProvidersSection.test.tsx
Shows macOS Keychain error in warnings, clears discovery cache around verify flows, and refreshes status with force/refreshOpenCodeInventory; test stubs icons and asserts Cursor verify flow.
Session list / styling
apps/desktop/src/renderer/components/terminals/SessionCard.tsx, .../SessionListPane.test.tsx
Title styling changed to font-semibold unconditionally; tests assert class presence.
Simulator panel
apps/desktop/src/renderer/components/chat/ChatIosSimulatorPanel.tsx, .../ChatIosSimulatorPanel.test.tsx
Adds cancellation of pending restart, handles missing streamUrl gracefully, prefers fallbackMessage for user messaging, and test ensures service-managed fallback switching doesn’t double-stop/start.
Model selector grouping
apps/desktop/src/renderer/components/shared/providerModelSelectorGrouping.ts
Now a wildcard re-export from shared modelCatalog; grouping logic centralized.
Numerous tests
many *.test.ts(x) files
Adds/updates tests across Cursor SDK, model discovery, title adoption, Keychain migration, simulator fallback, PTY enrichment, and more.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Suggested labels

desktop, ios, docs

✨ 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 ade/ios-fixes-13925e5f

@arul28
Copy link
Copy Markdown
Owner Author

arul28 commented May 1, 2026

@copilot review but do not make fixes

Copy link
Copy Markdown
Contributor

Copilot AI commented May 1, 2026

@copilot review but do not make fixes

Here's my review of the PR:

Overview

This PR ships three main things: (1) a new host-catalog sync path for iOS model discovery, (2) Cursor SDK hook scaffolding, and (3) a macOS Keychain backend for API key storage. The scope is large but the intent is clear. Below are the issues I found, roughly ordered by severity.


Issues

1. codexApprovalPolicyWireValue — silent semantic change (agentChatService.ts)

The function previously mapped ADE internal policy names to Codex wire names ("untrusted""unlessTrusted", etc). That mapping was deleted and replaced with a pass-through. If the Codex CLI runtime still expects the old wire names, this will silently misconfigure sandboxing. Needs explicit confirmation that the runtime was updated or never required the mapping.

2. EFFORT_ALIASES for codex — max removed with no replacement (agentChatService.ts)

Before: codex: { minimal: "low", max: "xhigh", none: "low" }. After: codex: {}. minimal and none are now first-class entries in CODEX_REASONING_EFFORTS, which is fine. But max was previously aliased to xhigh — now validateReasoningEffort("codex", "max") returns null. Any caller or persisted session state that used "max" for codex will silently drop the effort.

3. macOS Keychain writes API key as a CLI argument (apiKeyStore.ts)

writeMacosKeychainSecret passes the key value as the -w argument directly to spawnSync. On macOS, process arguments are visible to other processes running as the same user (e.g., ps -ef). The security CLI unfortunately doesn't support reading the password from stdin for add-generic-password. The exposure window is small (synchronous call), but this is worth a comment acknowledging the tradeoff.

4. shellQuote may be insufficient for paths with special characters (cursorSdkHooks.ts)

return `"${value.replace(/(["\\$`])/g, "\\$1")}"`;

This escapes ", \, $, and ` but not newlines, carriage returns, or null bytes. A Node.js path containing a newline would produce a broken shell script written to disk. Even if this is unlikely in practice, it should be guarded.

5. nearestExistingPath is unbounded on deeply nested non-existent paths (cursorSdkPolicy.ts)

The loop climbs to the filesystem root calling fs.existsSync on each ancestor. On a path like /a/b/c/d/.../z with 100 non-existent components, this makes 100 synchronous stat calls in the hook hot-path. The loop should be capped or replaced with a single realpathSync with a fallback to path.dirname.

6. cachedChatModelCatalog key is always "catalog" regardless of host (SyncService.swift)

let cacheKey = chatModelsCacheKey(provider: "catalog")

chatModelsCacheKey appends the host profile identity, so the key differs per host — that part is fine. But cachedChatModelCatalog() returns nil if the host profile changed between populating the cache and reading it, which means the iOS model picker shows no catalog on a host switch until the async fetch completes. A minor UX edge case but worth noting.

7. Speculative model IDs in ACTIVE_FALLBACK_SDK_ROWS (cursorModelsDiscovery.ts)

The fallback list contains IDs that don't appear in the Cursor API today (gpt-5.4-nano, grok-4-20, kimi-k2.5, gemini-3.1-pro, etc.). When a user has a valid API key but the SDK call times out, the UI will show these IDs. Selecting one will fail at runtime with an opaque error. Consider separating "known stable" fallbacks from "speculative" ones, or only showing the minimal fallback set when the SDK is unreachable.

8. workDynamicModelOption naming collision in WorkModelCatalog.swift

There are now two private functions named workDynamicModelOption — one taking AgentChatModelCatalogModel (new overload around line 400 in the diff) and one taking AgentChatModelInfo (original around line 788). Swift resolves this by parameter type, which works, but it's easy to call the wrong one during future edits. Different names (e.g., workDynamicModelOptionFromCatalog) would make intent clearer.

9. runtimeTitleAdopted not persisted (agentChatService.ts)

The new flag prevents ADE from overwriting a runtime-adopted title after a restart. But runtimeTitleAdopted lives only in ManagedChatSession (in-memory). After a desktop restart the flag is reset to false, so ADE may overwrite the runtime title on the next session activity. The autoTitleStage is persisted; runtimeTitleAdopted should either also be persisted or derived from whether autoTitleStage !== "none".


Minor observations

  • The __setSafeStorageForTests export in apiKeyStore.ts leaks a test-only escape hatch into the production module. A common pattern is to gate it behind process.env.NODE_ENV === "test" at the call site or only export it in test builds.
  • WorkModelPickerSheet now calls workModelIdsEquivalent inside catalogLocation on every render cycle (via syncSelectionStateToCatalog). For catalogs with many models this could be noticeable; caching the lookup or using workModelCatalogLookup would be more efficient.
  • The CURSOR_RUNTIME_AUTH_ERROR constant defined in agentChatService.ts and CURSOR_AGENT_AUTH_BLOCKER in cursorModelsDiscovery.ts contain identical human-readable strings. These should be deduplicated into a shared constant.

Copilot finished work on behalf of arul28 May 1, 2026 21:42
Comment thread apps/ios/ADE/Views/Work/WorkModelPickerSheet.swift
Comment thread apps/desktop/src/main/services/ai/apiKeyStore.ts
Comment thread apps/desktop/src/main/services/chat/cursorSdkPool.ts
Comment thread apps/desktop/src/main/services/chat/cursorSdkHooks.ts
Copy link
Copy Markdown
Contributor

Copilot AI commented May 1, 2026

@copilot review but do not make fixes

Code Review

Overview

This PR introduces three main features: (1) a unified chat.modelCatalog IPC endpoint that streams a pre-built structured catalog from desktop to iOS in one call, replacing the old per-provider loop; (2) a Cursor SDK tool-gate hook system that installs a Node.js bridge script into ~/.cursor/hooks.json; and (3) macOS Keychain integration for persistent API key storage. It also extends reasoning effort options and tightens auto-title/PTY title sanitization.


Issues & Observations

1. codexApprovalPolicyWireValue is now a no-op identity function
agentChatService.ts L2395–2396 changed the function from mapping hyphenated values to camelCase wire format (on-requestonRequest, untrustedunlessTrusted) to simply returning the value unchanged. CODEX_APPROVAL_POLICY_ALIASES handles the reverse (camelCase → hyphenated) for normalizing responses. This is correct only if the Codex CLI runtime now accepts the hyphenated format directly — worth an explicit comment or test confirming the runtime accepts "on-request" over the old "onRequest".

2. Two workDynamicModelOption overloads in WorkModelCatalog.swift with divergent tier heuristics
Line 398 (new, for AgentChatModelCatalogModel) includes "spark" in the .fast tier; line 741 (old, for AgentChatModelInfo) does not. The two parallel code paths will silently diverge as new model families are added. They should share a single tier-classification helper.

3. spawnSync in apiKeyStore.ts blocks the Electron main thread
runSecurity() calls spawnSync(MACOS_SECURITY_BIN, ...) with a 5-second timeout. This is synchronous and will block the main process event loop for up to 5 seconds on I/O errors or a slow keychain. Keychain reads happen on every call to ensureStore() when macOS Keychain is available, which means every getKey() call could stall the UI.

4. Hook bridge script embedded as a template literal
The CJS bridge script in cursorSdkHooks.ts (L68–L183) is inlined as a string. The TypeScript compiler, linter, and test coverage tooling do not analyze the embedded JavaScript. Bugs in the bridge script (e.g., the responseText.includes("\\n") condition on "close" vs partial-read) will only surface at runtime. The test in cursorSdkHooks.test.ts exercises the happy path and error fallback but doesn't cover the failClosed socket-timeout path.

5. failClosed: true mid-session impact
The hook entry (adeEntry = { command, failClosed: true }) means if ADE's policy gate socket is unavailable even momentarily (restart, crash, port contention), every queued Cursor tool call is denied. An in-progress agent session would be blocked until ADE restarts and the socket reconnects. Worth documenting in the hook entry comment.

6. validateReasoningEffort now returns null for unknown/unrecognized efforts
Previously it returned a provider-specific fallback (DEFAULT_REASONING_EFFORT for Codex, "medium" for Claude). Now it returns null. Any upstream code path that previously relied on the silent correction (e.g., an old persisted session with effort: "max" for Codex, which would have been aliased to "xhigh") will now receive null and the effort will be omitted. This is probably the desired behavior given none and minimal are now first-class Codex efforts, but the EFFORT_ALIASES for codex being cleared to {} is a silent breaking change for in-flight sessions that persist a non-canonical effort string.

7. fetchedAt from server is unused for iOS cache TTL
SyncService.getChatModelCatalog() stores a local fetchedAt: Date for TTL purposes. The fetchedAt: String from the desktop response (AgentChatModelCatalog.fetchedAt) is decoded by Swift but never compared against the TTL. The two clocks are independent, which is fine, but the fetchedAt field in the Swift model is dead metadata.

8. WorkModelPickerSheet.swiftcatalogLocation uses workModelIdsEquivalent but the new overload of workModelCatalogGroups uses model.id (registry id) not runtimeModelId
When syncing selection state, catalogLocation(for:) searches by workModelIdsEquivalent($0.id, modelId) where $0.id is the ADE registry id (e.g., anthropic/claude-opus-4-7). If the session's stored currentModelId is a runtime id (e.g., claude-opus-4-7 or opus), the lookup may miss and fall back to group-level selection. This was the source of incorrect provider tabs being highlighted. The new workCanonicalClaudeRegistryId / workClaudeRuntimeModelId lookup keys added to workModelLookupKeys help the curated path but catalogLocation doesn't go through that normalization.


Minor Notes

  • WorkNewChatSheet.swift: Droid is now surfaced before OpenCode in the provider picker — intentional reordering but worth confirming this matches the desired priority.
  • audit-chat-model-runtime.mjs (1089-line new script) is in apps/desktop/scripts/ — looks like a development/diagnostic tool. Should have a // header comment clarifying it's not part of the production build.
  • The cursorModelsDiscovery.ts fallback list (ACTIVE_FALLBACK_SDK_ROWS) includes speculative model ids (e.g., gpt-5.4-nano, gemini-3.1-pro, grok-4-20, kimi-k2.5) that don't exist in the model registry. These will appear as orphaned rows if the SDK list call fails.

Copilot finished work on behalf of arul28 May 1, 2026 21:45
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: 16

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (6)
apps/desktop/src/main/services/ai/providerConnectionStatus.ts (1)

95-105: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Keep Cursor usageAvailable in sync with runtime-health downgrades.

After this change, a Cursor auth-failed health state flips runtimeAvailable to false but leaves usageAvailable as true. That produces a contradictory status object for the same API-key path.

Suggested fix
   function applyRuntimeHealth(
     status: AiProviderConnectionStatus,
     health: ReturnType<typeof getProviderRuntimeHealth> | null,
   ): void {
     if (!health) return;
     if (health.state === "auth-failed" || health.state === "runtime-failed") {
       status.runtimeAvailable = false;
+      if (status.provider === "cursor") {
+        status.usageAvailable = false;
+      }
       status.blocker = health.message
         ?? (health.state === "auth-failed"
           ? `${status.provider} runtime was detected, but ADE chat reported that login is still required.`
           : `${status.provider} runtime was detected, but ADE could not launch it from this app session.`);
     } else if (health.state === "ready") {
       status.runtimeAvailable = true;
       status.authAvailable = true;
+      if (status.provider === "cursor") {
+        status.usageAvailable = true;
+      }
       status.blocker = null;
     }
   }

Also applies to: 228-228

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/main/services/ai/providerConnectionStatus.ts` around lines
95 - 105, The health-state handling updates runtimeAvailable (in
providerConnectionStatus.ts) but fails to mirror that downgrade to
usageAvailable, causing inconsistent status objects; update the branches that
set status.runtimeAvailable = false for health.state values like "auth-failed"
and "runtime-failed" (and the similar block around the other occurrence
referenced) to also set status.usageAvailable = false and ensure when
health.state === "ready" you set status.usageAvailable = true alongside
runtimeAvailable and authAvailable; locate the status object mutations in the
function handling health (look for the health.state checks and the status
variable) and apply the same usageAvailable changes in both places.
apps/desktop/src/main/services/ai/authDetector.ts (1)

791-833: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Clear the stale Cursor auth failure on successful verification.

This path now records auth-failed into global runtime health, but the success path never marks Cursor ready again. After one bad key, a later successful re-check can still leave buildProviderConnections() downgraded until some unrelated code resets that shared state.

Suggested fix
-import { reportProviderRuntimeAuthFailure } from "./providerRuntimeHealth";
+import {
+  reportProviderRuntimeAuthFailure,
+  reportProviderRuntimeReady,
+} from "./providerRuntimeHealth";

 async function verifyCursorApiKey(
   key: string,
   verifiedAt: string,
 ): Promise<ApiKeyVerificationResult> {
   let timeoutHandle: ReturnType<typeof setTimeout> | null = null;
   try {
     const { Cursor } = await import("@cursor/sdk");
     const user = await Promise.race([
       Cursor.me({ apiKey: key }),
       new Promise<never>((_, reject) => {
         timeoutHandle = setTimeout(
           () => reject(new Error("Verification timed out.")),
           API_KEY_VERIFY_TIMEOUT_MS,
         );
       }),
     ]);
+    reportProviderRuntimeReady("cursor");
     return {
       provider: "cursor",
       ok: true,
       message: user.userEmail
         ? `Connection verified for ${user.userEmail}.`
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/main/services/ai/authDetector.ts` around lines 791 - 833,
The success branch in verifyCursorApiKey currently never clears the prior
"auth-failed" state, so add a call to clear the stale Cursor auth failure before
returning on success; locate verifyCursorApiKey and, just before the successful
return in the try block, invoke the existing runtime-health clearing helper
(e.g., clearProviderRuntimeAuthFailure("cursor") or the counterpart to
reportProviderRuntimeAuthFailure) to mark Cursor as healthy so
buildProviderConnections() isn't left downgraded after a later successful
verification.
apps/desktop/src/renderer/components/settings/ProvidersSection.tsx (1)

451-467: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Clear the previous verification badge before each new verify attempt.

If either IPC call rejects, verificationByProvider keeps its old value, so the panel can continue showing the last success/failure badge for a different key or an earlier attempt. Clearing that entry before the request avoids stale verification state on retries.

💡 Localized fix
   setError(null);
   setNotice(null);
   setVerifyingProvider(provider);
+  setVerificationByProvider((prev) => {
+    const next = { ...prev };
+    delete next[provider];
+    return next;
+  });
   try {
     invalidateAiDiscoveryCache();
     const result = await window.ade.ai.verifyApiKey(provider);
   setError(null);
   setNotice(null);
   setVerifyingProvider("cursor");
+  setVerificationByProvider((prev) => {
+    const next = { ...prev };
+    delete next.cursor;
+    return next;
+  });
   try {
     await window.ade.ai.storeApiKey("cursor", trimmed);

Also applies to: 470-492

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/renderer/components/settings/ProvidersSection.tsx` around
lines 451 - 467, The verification badge for a provider can remain stale because
verificationByProvider isn't cleared before making IPC calls; modify
verifyApiKey to clear the entry for the target provider (use
setVerificationByProvider to remove or set undefined for [provider]) immediately
after setVerifyingProvider(provider) and before calling
invalidateAiDiscoveryCache()/window.ade.ai.verifyApiKey, so any old
success/failure badge is removed while the new request runs; apply the same
change to the other verification routine covering lines 470-492 (the other
function that also calls setVerificationByProvider/verifyApiKey IPCs) so both
code paths clear the previous verification state up-front.
apps/desktop/src/main/services/ai/apiKeyStore.ts (1)

381-395: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Only update cache after persistence succeeds.

Both storeApiKey() and deleteApiKey() mutate the in-memory store before the Keychain/encrypted-store write completes. If that write throws, the current process reports the new state even though nothing was durably saved, and the mismatch only shows up after restart.

Also applies to: 412-423

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/main/services/ai/apiKeyStore.ts` around lines 381 - 395,
storeApiKey and deleteApiKey currently mutate the in-memory store before
performing durable writes; change both to perform persistence first and only
update the in-memory cache on success: in storeApiKey, trim inputs and validate,
then perform the platform write(s) (call writeMacosKeychainSecret and update
provider index via
readMacosKeychainProviderIndex/writeMacosKeychainProviderIndex for macOS, or
call persistEncryptedStore for the encrypted path) using local variables and
without touching ensureStore() result, and only after those calls succeed assign
the key into the result of ensureStore(); apply the analogous approach to
deleteApiKey (perform keychain/index removal or persistEncryptedStore first,
then remove from in-memory store), and ensure any thrown errors leave the
in-memory store unchanged.
apps/desktop/src/renderer/components/chat/ChatIosSimulatorPanel.tsx (1)

1715-1725: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Don't overwrite service-managed fallback messaging with lastError.

This branch still copies event.status.lastError into liveVisual.error during service-managed fallback. The reconnect overlay reads liveVisual.error first, so users will still see the raw transport error instead of the new degradation/fallback reason you added.

Suggested fix
         if ((event.type === "stream-stopped" || event.type === "stream-error") && !event.status.streamUrl) {
           setLiveVisual((current) => current?.kind === "mjpeg"
             ? {
                 ...current,
                 status: event.type === "stream-error" ? "reconnecting" : current.status,
                 url: null,
-                error: event.status.lastError ?? current.error,
+                error: serviceManagedFallback
+                  ? (event.status.degradationReason ?? event.status.fallbackReason ?? current.error)
+                  : (event.status.lastError ?? current.error),
               }
             : current);
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/renderer/components/chat/ChatIosSimulatorPanel.tsx` around
lines 1715 - 1725, The code is copying event.status.lastError into
liveVisual.error even when serviceManagedFallback is true, which causes the
reconnect overlay to show the raw transport error; update the setLiveVisual call
used when handling "stream-stopped"/"stream-error" so that inside the updater
you only assign error = event.status.lastError ?? current.error when
serviceManagedFallback is false, otherwise preserve current.error (or set the
service-managed fallback message) so serviceManagedFallback prevents overwriting
liveVisual.error; adjust the setMessage branch accordingly if needed (symbols:
setLiveVisual, liveVisual, event, event.status.lastError,
serviceManagedFallback).
apps/ios/ADE/Views/Work/WorkModelCatalog.swift (1)

487-505: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Normalize Claude aliases to one canonical equivalence key before adding shorthand names.

workDeduplicatedModelOptions() ultimately relies on the first non-registry key from workModelLookupKeys(). With these new Claude mappings, equivalent IDs can now diverge: e.g. anthropic/claude-sonnet-4-6 yields sonnet, while claude-sonnet-4-6 yields claude-sonnet-4-6. That means the same model can survive deduplication twice when the host mixes canonical and runtime IDs.

A safer pattern here is to derive one canonical Claude key for equivalence/deduplication first, and only append shorthand aliases like sonnet/haiku for lookup convenience after that.

Also applies to: 510-538

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ios/ADE/Views/Work/WorkModelCatalog.swift` around lines 487 - 505, The
code appends runtime and registry Claude IDs before adding shorthand aliases,
causing mixed canonical/runtime keys to bypass deduplication in
workDeduplicatedModelOptions()/workModelLookupKeys(); change the order so you
first derive and append one canonical Claude equivalence key using
workCanonicalClaudeRegistryId(for: trimmed) (or runtime->canonical mapping if
needed), then append any shorthand aliases like the short names (sonnet/haiku)
afterwards; apply the same canonical-first ordering to the second Claude block
(lines ~510-538) and mirror the pattern for any analogous Codex/other model
mapping code (workCanonicalCodexRegistryId(), workCodexRuntimeModelId()) so
shorthand aliases are only added after a canonical key is present.
🟡 Minor comments (9)
apps/desktop/src/main/services/pty/ptyService.ts-2147-2149 (1)

2147-2149: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Prefer the in-memory chat binding over the persisted row here.

The non-live branch now returns row.chatSessionId before terminalChatSessions.get(row.id), but terminalSessionFromSummary() does the opposite. That can make enrichSessions() return a stale chat binding after a rebinding/reattach path updated only the in-memory map.

Suggested fix
           chatSessionId: live
             ? terminalChatSessions.get(row.id) ?? live[1].chatSessionId ?? row.chatSessionId ?? null
-            : row.chatSessionId ?? terminalChatSessions.get(row.id) ?? null,
+            : terminalChatSessions.get(row.id) ?? row.chatSessionId ?? null,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/main/services/pty/ptyService.ts` around lines 2147 - 2149,
The non-live branch currently prefers row.chatSessionId over the in-memory
binding, which can return stale bindings; update the chatSessionId assignment so
it checks terminalChatSessions.get(row.id) before row.chatSessionId (matching
the live branch and terminalSessionFromSummary()), i.e. prefer
terminalChatSessions.get(row.id) as the first fallback, then row.chatSessionId,
and finally null; adjust the same logic used by
enrichSessions()/terminalSessionFromSummary() to ensure consistent
in-memory-first behavior.
apps/desktop/scripts/dev.cjs-259-263 (1)

259-263: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add a preflight availability check for the remote debugging port.

Line 259 always forces CDP to a fixed port, but there’s no check that it’s free. If occupied, dev tooling depending on CDP can fail unpredictably.

🔧 Suggested fix
   const remoteDebugPort = Number.parseInt(remoteDebugPortRaw, 10);
   if (!Number.isFinite(remoteDebugPort) || remoteDebugPort <= 0 || remoteDebugPort > 65535) {
     throw new Error(`Invalid Electron remote debugging port: ${remoteDebugPortRaw}`);
   }
+  if (!(await isPortFree(remoteDebugPort))) {
+    throw new Error(
+      `Electron remote debugging port ${remoteDebugPort} is already in use. ` +
+      `Set ADE_APP_CONTROL_CDP_PORT (or ADE_ELECTRON_REMOTE_DEBUGGING_PORT) to a free port.`
+    );
+  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/scripts/dev.cjs` around lines 259 - 263, Add a preflight check
to ensure the remote debugging port is free before adding
`--remote-debugging-port=${remoteDebugPort}` to `electronArgs`: use a small
port-availability probe (for example creating a temporary TCP server or using a
get-free-port helper) to detect if `remoteDebugPort` is in use, and if it is
either pick an available port and update `remoteDebugPort`/the argument or fail
fast with a clear error; update the code that builds `electronArgs` and the
`spawnProcess("electron", npxCommand, electronArgs, electronEnv)` call so the
actual, verified port is passed to Electron.
getting-started/connect-provider.mdx-58-63 (1)

58-63: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Cursor is described as SDK/API-key based but still grouped under “CLI-based providers.”

This section grouping is now inconsistent and can mislead setup choices. Consider renaming the tab/category to include SDK providers.

Proposed doc tweak
-  <Tab title="CLI-based providers">
+  <Tab title="CLI and SDK-based providers">

Also applies to: 75-75

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@getting-started/connect-provider.mdx` around lines 58 - 63, The "Cursor"
entry is currently placed under the "CLI-based providers" grouping which is
misleading because Cursor is SDK/API-key based; update the UI/markdown grouping
in connect-provider.mdx so SDK-backed providers are clearly represented — either
rename the "CLI-based providers" category to something like "CLI & SDK
providers" (update the category header text used around the CardGroup) or move
the Cursor Card out of that CardGroup into a new "SDK/API-key providers"
section; also apply the same change where the duplicate occurs around line 75 to
keep both instances consistent (look for the "Cursor" Card, "CardGroup", and
"Note" elements and the literal "CLI-based providers" label).
apps/desktop/src/renderer/components/chat/ChatIosSimulatorPanel.test.tsx-366-366 (1)

366-366: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Replace fixed real-time sleep with deterministic timer control.

Line 366 uses a wall-clock wait, which can make this test flaky on slower CI nodes.

Suggested stabilization patch
   it("does not stop a service-managed fallback while it is switching backends", async () => {
+    vi.useFakeTimers();
     const { api, emit } = installIosSimulatorApi();
@@
-    await new Promise((resolve) => setTimeout(resolve, 700));
+    await act(async () => {
+      await vi.advanceTimersByTimeAsync(700);
+    });
@@
     await waitFor(() => expect(api.startStream).toHaveBeenCalledTimes(1));
     expect(screen.queryByText(/Live stream failed/)).toBeNull();
+    vi.useRealTimers();
   });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/renderer/components/chat/ChatIosSimulatorPanel.test.tsx` at
line 366, The test currently uses a real-time sleep via "await new
Promise((resolve) => setTimeout(resolve, 700));" which causes flakiness; replace
that with deterministic timer control by enabling fake timers
(jest.useFakeTimers() or equivalent), trigger the actions that start the
timeout, then advance timers with jest.advanceTimersByTime(700) or
jest.runOnlyPendingTimers()/jest.runAllTimers(), and finally restore timers with
jest.useRealTimers(); update the test to remove the real Promise sleep and use
the fake-timer API around the code that relies on the timeout.
apps/ios/ADE/Views/Work/WorkModelPickerSheet.swift-265-269 (1)

265-269: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Deduplicate normalized reasoning tiers before rendering pills.

Line 266 lowercases host-provided values, and reasoningPills later uses ForEach(tiers, id: \.self). If the catalog ever sends duplicates that differ only by case or whitespace, SwiftUI gets duplicate IDs and the pill row becomes unstable.

♻️ Suggested fix
  private func supportedReasoningTiers(for model: WorkModelOption) -> [String] {
-    model.reasoningEfforts
-      .map { $0.effort.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() }
-      .filter { !$0.isEmpty }
+    var seen = Set<String>()
+    return model.reasoningEfforts
+      .map { $0.effort.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() }
+      .filter { !$0.isEmpty }
+      .filter { seen.insert($0).inserted }
  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ios/ADE/Views/Work/WorkModelPickerSheet.swift` around lines 265 - 269,
supportedReasoningTiers(for:) currently normalizes reasoningEfforts but can
return duplicate strings (differing only by case/whitespace), causing duplicate
IDs in the ForEach used by reasoningPills; update supportedReasoningTiers(for:)
to deduplicate the normalized values before returning (preserving original
order) so tiers used in ForEach(tiers, id: \.self) are unique and stable.
apps/desktop/src/main/services/chat/agentChatService.test.ts-911-917 (1)

911-917: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Make the title wait helper use a real timeout budget.

This helper only gives the test ~100 zero-delay polls. On slower CI runners, title propagation can legitimately land after that window, which makes all of the new runtime-title tests flaky.

Suggested change
 async function waitForSessionTitle(sessionService: ReturnType<typeof createMockSessionService>, sessionId: string, title: string): Promise<void> {
-  for (let attempt = 0; attempt < 100; attempt += 1) {
+  const deadline = Date.now() + 2_000;
+  while (Date.now() < deadline) {
     if (sessionService.get(sessionId)?.title === title) return;
-    await new Promise((resolve) => setTimeout(resolve, 0));
+    await new Promise((resolve) => setTimeout(resolve, 10));
   }
   throw new Error(`Timed out waiting for session title '${title}'.`);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/main/services/chat/agentChatService.test.ts` around lines
911 - 917, The helper waitForSessionTitle currently polls 100 times with
zero-delay sleeps which can flake on CI; change waitForSessionTitle to use a
real timeout budget by tracking elapsed time (e.g., Date.now or performance.now)
and looping with a small non-zero delay (e.g., 20–100ms) until elapsed exceeds a
configurable maxTimeout (default e.g., 5000ms), returning when
sessionService.get(sessionId)?.title matches or throwing a timeout error after
the budget; update the function signature to accept an optional timeoutMs
parameter if needed and ensure the polling uses the sessionService.get(...)
check exactly as before.
apps/desktop/src/main/services/chat/agentChatService.test.ts-3340-3413 (1)

3340-3413: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Remove scheduler-dependent races from the new title tests.

This block still relies on timing luck: Line 3366 uses a fixed 20 ms sleep, and Line 3404 dereferences mockState.droidPooled.bridge before the async hook registration is proven to exist. Both can turn into flaky failures instead of testing the title-adoption behavior.

Suggested change
       await service.updateSession({ sessionId: session.id, title: "Manual Title", manuallyNamed: true });
       mockState.emitCodexPayload({
         jsonrpc: "2.0",
         method: "thread/name/updated",
         params: { threadId: "thread-1", name: "Should Not Win" },
       });
-      await new Promise((resolve) => setTimeout(resolve, 20));
+      await waitForSessionTitle(sessionService, session.id, "Manual Title");
       expect(sessionService.get(session.id)?.title).toBe("Manual Title");
@@
       await service.sendMessage({ sessionId: session.id, text: "Use ACP title." }, { awaitDispatch: true });
+      for (let attempt = 0; attempt < 100 && !mockState.droidPooled?.bridge.onSessionUpdate; attempt += 1) {
+        await new Promise((resolve) => setTimeout(resolve, 0));
+      }
+      expect(mockState.droidPooled?.bridge.onSessionUpdate).toBeTypeOf("function");
       mockState.droidPooled.bridge.onSessionUpdate?.({
         sessionId: "droid-acp-session-1",
         update: {
           sessionUpdate: "session_info_update",
           title: "Droid Native Title",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/main/services/chat/agentChatService.test.ts` around lines
3340 - 3413, The tests use fragile timing: replace the fixed sleep after the
second Codex thread/name/updated emit with an explicit assertion/wait (e.g.,
await vi.waitFor or await waitForSessionTitle) that the session title remains
"Manual Title" instead of setTimeout(20); and for the Droid test, ensure the
mock bridge handler exists before calling onSessionUpdate by awaiting its
registration (e.g., await vi.waitFor(() =>
mockState.droidPooled.bridge?.onSessionUpdate) or awaiting a condition on
sessionService) and only then invoke
mockState.droidPooled.bridge.onSessionUpdate; update the examples around
sendMessage, mockState.emitCodexPayload, waitForSessionTitle, and
mockState.droidPooled.bridge.onSessionUpdate accordingly.
apps/desktop/src/main/services/chat/agentChatService.ts-1401-1411 (1)

1401-1411: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Validate Codex tiers against the descriptor too.

This helper is now used for both Claude and Codex, but Line 1408 only enforces descriptor.reasoningTiers for Claude. That means a Codex model that advertises a restricted tier set can still accept an unsupported effort here and only get corrected later by the runtime.

Suggested fix
 function validateReasoningEffortForDescriptor(
   provider: "codex" | "claude",
   effort: string | null | undefined,
   descriptor?: ModelDescriptor | null,
 ): string | null {
   const validated = validateReasoningEffort(provider, effort);
   if (!validated) return null;
-  if (provider === "claude" && descriptor?.reasoningTiers?.length && !descriptor.reasoningTiers.includes(validated)) {
+  if (descriptor?.reasoningTiers?.length && !descriptor.reasoningTiers.includes(validated)) {
     return null;
   }
   return validated;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/main/services/chat/agentChatService.ts` around lines 1401 -
1411, The function validateReasoningEffortForDescriptor improperly only
restricts descriptor.reasoningTiers for provider "claude"; update it so that
after calling validateReasoningEffort(provider, effort) you check
descriptor?.reasoningTiers for both "claude" and "codex" (i.e., any provider
using this helper) and return null if the validated tier is not included. Modify
validateReasoningEffortForDescriptor (and keep calling validateReasoningEffort)
to apply the descriptor.reasoningTiers inclusion check regardless of provider,
using the existing descriptor.reasoningTiers?.includes(validated) logic.
apps/desktop/src/shared/modelCatalog.ts-352-361 (1)

352-361: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Deduplicate injected OpenCode providers to prevent duplicate provider blocks.

If opencodeProviders contains repeated IDs, this loop can push duplicates because existingFamilies is never updated after insertion.

Suggested fix
       for (const { id, name } of potentialProviders) {
-        if (!existingFamilies.has(id)) {
+        if (!id || existingFamilies.has(id)) {
+          continue;
+        }
+        existingFamilies.add(id);
+        {
           providers.push({
             key: id,
             label: PROVIDER_LABELS[id] ?? name,
             badgeColor: PROVIDER_BADGE_COLORS[id] ?? "#6B7280",
             subsections: [],
             modelCount: 0,
           });
         }
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/shared/modelCatalog.ts` around lines 352 - 361, The loop
over potentialProviders (involving variables potentialProviders,
existingFamilies, and providers) can push duplicate provider entries when
opencodeProviders contains repeated ids because existingFamilies is not updated
after adding a new provider; fix this by adding the inserted id to
existingFamilies (or otherwise marking it) immediately after pushing the new
provider so subsequent iterations see it and skip duplicates.
🧹 Nitpick comments (4)
apps/desktop/src/main/services/sync/syncRemoteCommandService.test.ts (1)

1545-1549: ⚡ Quick win

Also add chat.modelCatalog to IOS_REMOTE_COMMAND_ACTIONS parity coverage.

Line 1546 adds runtime coverage for the new action, but the shared iOS action list won’t catch registration drift unless this action is included there too.

✅ Suggested test-list update
 const IOS_REMOTE_COMMAND_ACTIONS = [
@@
   "chat.models",
+  "chat.modelCatalog",
   "chat.listSessions",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/main/services/sync/syncRemoteCommandService.test.ts` around
lines 1545 - 1549, Add the new action string "chat.modelCatalog" to the
IOS_REMOTE_COMMAND_ACTIONS constant so the iOS parity list matches runtime
registration; locate the IOS_REMOTE_COMMAND_ACTIONS definition (the shared iOS
action list) and append "chat.modelCatalog" to the array/enum that lists
permitted remote commands, then run the test suite to confirm parity coverage
passes for the new test in syncRemoteCommandService.test.ts which exercises
service.execute("chat.modelCatalog", ...).
apps/desktop/package.json (1)

190-198: ⚡ Quick win

Scope the iOS helper payload to macOS packaging.

build.extraResources applies to every target, so Windows artifacts will now ship build.sh, sim-capture.swift, and sim-input.m even though only the mac runtime can use them. Moving this fileset under the mac-specific build config keeps unsupported releases smaller and avoids distributing Apple-private helper sources where they can never run.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/package.json` around lines 190 - 198, The extraResources entry
that currently sits under the global build.extraResources (the object with
"from":"native/ios-sim-helpers", "to":"native/ios-sim-helpers", "filter":[...])
should be moved into the mac packaging config so only mac builds include those
files; remove that entry from the top-level build.extraResources and add the
same mapping under the mac-specific target (e.g., targets.mac or the mac build
config block) so files like "build.sh", "sim-capture.swift", and "sim-input.m"
are packaged only for macOS.
apps/desktop/native/ios-sim-helpers/build.sh (1)

85-88: ⚡ Quick win

Don't hide helper signing failures.

If ad-hoc signing fails here, the script still reports success and the packaged app only fails later when it tries to launch the helper. Emitting at least a warning makes those runtime failures diagnosable.

♻️ Suggested adjustment
 if command -v codesign >/dev/null 2>&1; then
-  codesign --force --sign - "$CAPTURE" >/dev/null 2>&1 || true
-  codesign --force --sign - "$INPUT" >/dev/null 2>&1 || true
+  if ! codesign --force --sign - "$CAPTURE" >/dev/null 2>&1; then
+    echo "warning: failed to ad-hoc sign $CAPTURE" >&2
+  fi
+  if ! codesign --force --sign - "$INPUT" >/dev/null 2>&1; then
+    echo "warning: failed to ad-hoc sign $INPUT" >&2
+  fi
 fi
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/native/ios-sim-helpers/build.sh` around lines 85 - 88, The
codesign calls in build.sh are currently silenced with ">/dev/null 2>&1 || true"
so helper signing failures are hidden; change the two lines that run codesign
against "$CAPTURE" and "$INPUT" to run without "|| true", capture their exit
status (e.g., if ! codesign --force --sign - "$CAPTURE" >/dev/null 2>&1; then
echo "Warning: codesign failed for $CAPTURE" >&2; fi) and do the same for
"$INPUT"; optionally set a nonfatal flag (e.g., SIGN_WARNED=1) so later
packaging can decide to fail or emit a summary, but at minimum print a clear
stderr warning including the file variable name when codesign exits nonzero.
apps/desktop/src/main/services/ai/aiIntegrationService.test.ts (1)

482-486: ⚡ Quick win

Harden verify-response assertions against accidental API-key leakage

These toMatchObject checks will still pass if the response accidentally includes sensitive fields like key. Add explicit negative assertions (or stricter equality) so leakage fails the tests.

Proposed test hardening
-    await expect(service.verifyApiKeyConnection("cursor")).resolves.toMatchObject({
-      provider: "cursor",
-      ok: true,
-      source: "store",
-    });
+    const verifyOkResult = await service.verifyApiKeyConnection("cursor");
+    expect(verifyOkResult).toMatchObject({
+      provider: "cursor",
+      ok: true,
+      source: "store",
+    });
+    expect(verifyOkResult).not.toHaveProperty("key");
@@
-    await expect(service.verifyApiKeyConnection("cursor")).resolves.toMatchObject({
-      provider: "cursor",
-      ok: false,
-      source: "store",
-    });
+    const verifyRejectedResult = await service.verifyApiKeyConnection("cursor");
+    expect(verifyRejectedResult).toMatchObject({
+      provider: "cursor",
+      ok: false,
+      source: "store",
+    });
+    expect(verifyRejectedResult).not.toHaveProperty("key");

As per coding guidelines, "Do not store secrets in plaintext project files when an encrypted store already exists."

Also applies to: 524-528

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/main/services/ai/aiIntegrationService.test.ts` around lines
482 - 486, The current test for service.verifyApiKeyConnection("cursor") uses
toMatchObject which would still pass if the response accidentally includes
sensitive fields; update the assertions to explicitly forbid secret fields
and/or assert exact equality: after calling
service.verifyApiKeyConnection("cursor") assert the result strictly equals the
expected object (provider: "cursor", ok: true, source: "store") or add negative
assertions like expect(result).not.toHaveProperty("key") and
expect(result).not.toHaveProperty("apiKey") (and similarly for "secret" or other
sensitive names) for both the check around verifyApiKeyConnection("cursor") and
the similar block at the later test (the one covering the same flow around lines
524-528).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/desktop/scripts/audit-chat-model-runtime.mjs`:
- Around line 1043-1079: The current ok computation treats empty results as
success; change it to fail when the audit produced zero models or zero cases
(and still check results for non-empty success): after building models, cases
and results, set ok to false if models.length === 0 or cases.length === 0;
otherwise set ok = results.length > 0 && results.every((r) => r.ok). Ensure this
logic is used where ok is computed (the variable named ok in this file) so
list/dry-run/smoke modes correctly return non-zero exit when nothing was
audited.

In `@apps/desktop/src/main/services/ai/aiIntegrationService.ts`:
- Around line 969-983: The current flow calls probeCursorSdkModelDiscovery after
verifyProviderApiKey and treats any probe failure as a verification failure;
wrap the probeCursorSdkModelDiscovery call in a try/catch and only convert the
successful verifyProviderApiKey result into an auth-failure when discovery
exists and discovery.failureKind === "auth"; on any probe timeout/throw or
non-auth discovery result, return the original verification response unchanged.
Keep the existing invalidateProviderReadinessCaches() and
invalidateInFinally=false behavior but ensure probe exceptions do not cause you
to overwrite or reject the successful verification (use apiEntry, providerName,
verifyProviderApiKey, and probeCursorSdkModelDiscovery identifiers to locate the
logic).

In `@apps/desktop/src/main/services/ai/apiKeyStore.ts`:
- Around line 162-181: In writeMacosKeychainSecret the API key is currently
passed on the command line via the "-w value" argument (making it visible to
process inspectors); change the call to runSecurity to pass "-w" "-" (i.e. "-w",
"-") and feed the secret via the child process stdin instead of the argv,
updating runSecurity (or its call site) to accept and write stdin data (the
value string) to the spawned process; keep the existing error handling
(rememberKeychainError/clearKeychainError) but ensure runSecurity returns error
details when its stdin-based invocation fails.

In `@apps/desktop/src/main/services/chat/agentChatService.ts`:
- Around line 16903-16926: descriptorInfo currently keys entries only by
patched.id which allows later providers to overwrite earlier ones when the same
descriptor id is surfaced by multiple providers; change the keying to include
the provider (e.g., use a composite key like `${provider.name}:${patched.id}` or
use a nested Map keyed by provider then id) wherever
descriptorInfo.set(patched.id, { provider, info }) is used (also update the
similar set at the other occurrence around lines 16950-16952) and adjust lookups
(descriptorInfo.get(...)) accordingly so each provider+descriptor pair is
preserved and the exported runtimeModelId/provider pair correctly reflects the
original backend.
- Around line 5381-5386: The current logger.info call
("agent_chat.runtime_title_adopted") writes the conversation-derived variable
title verbatim; remove title from structured logs and instead log a
non-sensitive surrogate (e.g., a stable hash of title, a redaction flag, or only
its length/boolean presence) to avoid creating a retention path for PII. Locate
the logger.info("agent_chat.runtime_title_adopted", { sessionId:
managed.session.id, provider: managed.session.provider, source, title }) and
replace the title field with a non-reversible identifier (e.g., titleHash) or a
redacted indicator while preserving sessionId, provider, and source for context.
Ensure any hashing uses a one-way algorithm and that no plaintext title is
written to logs or structured telemetry.
- Around line 2484-2495: On mismatch between requested and runtime Codex effort,
do not overwrite session with requestedReasoningEffort; instead preserve the
runtime-effective value. In the mismatch branch (where validateReasoningEffort,
normalizeReasoningEffort and onReasoningMismatch are used) keep the
onReasoningMismatch call but change managed.session.reasoningEffort =
requestedReasoningEffort to managed.session.reasoningEffort = reasoningEffort
(the runtimeReasoningEffort), and continue to update
managed.session.permissionMode via syncLegacyPermissionMode(managed.session) as
before so ADE state reflects the live thread.

In `@apps/desktop/src/main/services/chat/cursorModelsDiscovery.ts`:
- Around line 321-329: When the warm path (warmCursorModelsFromSdk) catches an
auth error it must persist that failure into the shared discovery failure state
so discoverCursorSdkModelDescriptors (in cached-or-fallback mode) will stop
returning synthetic fallbacks; update the catch handler that currently calls
recordCursorModelDiscoveryFailure(error) and returns [] to also write the error
into the same persistent/visible failure slot that
discoverCursorSdkModelDescriptors consults (or rethrow/resolve a special failure
sentinel that discoverCursorSdkModelDescriptors understands), and mirror the
same change for the second warm-path block (lines ~481-493) so sdkWarmInFlight,
recordCursorModelDiscoveryFailure, and discoverCursorSdkModelDescriptors all
consistently observe auth failures instead of silently returning fallback rows.

In `@apps/desktop/src/main/services/chat/cursorSdkPolicy.ts`:
- Around line 234-250: looksLikePathToken currently rejects Windows absolute
paths (e.g., C:\tmp\file or C:/tmp/file); update the function to detect Windows
drive-letter absolute paths (e.g., a regex like /^[A-Za-z]:[\\/]/) and return
true for them before the checks that require a forward slash and before the
URI-like regex (/^[a-zA-Z][a-zA-Z0-9+.-]*:/) so that C:/... is not misclassified
as a scheme; modify the logic inside looksLikePathToken to run this drive-letter
detection after trimming (trimShellToken) and before the slash/URI checks so
Windows absolute paths are accepted and still pass other existing exclusions
(leading -, env assignments, etc.).

In `@apps/desktop/src/main/services/chat/cursorSdkWorker.ts`:
- Around line 48-85: The fallback branch that consumes record.toJSON() can leak
sensitive fields; restrict errorMessage() to only include the explicit safe
allowlist (code, status, operation, endpoint, requestId) and remove or disable
the generic toJSON() traversal. Edit the function errorMessage to stop iterating
Object.entries(json) and instead only read those same allowed keys from record
or from json if present, ensuring you still trim and stringify values before
adding to detailEntries; keep the existing primary message logic and return
format unchanged.

In `@apps/desktop/src/main/services/ios/iosSimulatorService.ts`:
- Around line 386-398: Update iosSimHelperRoot so packaged apps do not search
launch-directory fallbacks: detect packaged mode (e.g., process.defaultApp is
falsy or NODE_ENV !== 'development') and when packaged exclude candidates that
use process.cwd() or apps/desktop (the entries that join process.cwd() and the
"apps/desktop" path); only allow ADE_IOS_SIM_HELPER_ROOT override and bundled
resource paths in packaged mode so the function iosSimHelperRoot returns null
instead of a CWD-based path, and ensure buildIosurfaceIndigoHelpers reacts to
null by failing cleanly rather than executing build.sh from CWD.

In `@apps/desktop/src/main/services/ipc/registerIpc.ts`:
- Around line 3227-3231: Wrap the cache invalidation call in a try/catch so
failures are best-effort and do not turn a successful secret mutation into an
IPC error: after calling storeApiKey (in the ipcMain.handle for
IPC.aiStoreApiKey) call
ctx.aiIntegrationService.invalidateProviderReadinessCaches() inside a try block
and catch any error, logging it (e.g., ctx.processLogger.error or console.error)
instead of letting it throw; apply the same try/catch/log pattern to the
corresponding delete handler where invalidateProviderReadinessCaches() is
invoked.

In `@apps/desktop/src/renderer/components/chat/AgentChatPane.tsx`:
- Around line 2445-2453: The code currently swallows errors from
getAgentChatModelsCached(...) and treats probe failures the same as an empty
cursor model list, causing all cursor IDs to be removed via isCursorModelId;
change this so you distinguish an actual empty result from an exception: call
getAgentChatModelsCached and capture errors separately (e.g., let cursorModels;
try { cursorModels = await getAgentChatModelsCached(...) } catch (err) {
cursorModels = null; /* probe failed */ }), then only compute withoutCursor =
orderModelIds(available.filter(id => !isCursorModelId(id))) and call
setAvailableModelIds(withoutCursor) when cursorModels is an explicit empty
array; if cursorModels === null (probe failure) preserve existing cursor IDs in
available (i.e., do not strip cursor IDs) and optionally log the error for
diagnostics.

In `@apps/desktop/src/renderer/components/chat/ChatIosSimulatorPanel.tsx`:
- Around line 1681-1684: The pending restart timer must be canceled not only on
"stream-started" but also when a "stream-status" event shows the stream
recovered; update the handler that currently calls
cancelDeviceBackedStreamRestart() so it also calls it when event.type ===
"stream-status" and (event.status.streamUrl || event.status.lastFrameAt),
keeping the existing serviceManagedFallback branch; apply the same change to the
other similar handler that schedules/cancels device-backed stream restarts
(i.e., the locations that call scheduleDeviceBackedStreamRestart and
cancelDeviceBackedStreamRestart) so a recovered stream won't be flapped.

In `@apps/desktop/src/shared/types/sync.ts`:
- Line 568: Install the missing dev dependencies and update tsconfig so desktop
typechecks run: add eslint and tsup to devDependencies and run npm/yarn install
(so the tools at eslint/bin/eslint.js and tsup are present), add `@types/node` to
devDependencies and ensure the TypeScript config includes node definitions
(e.g., "types": ["node"] or otherwise references `@types/node`) to fix TS2688, and
set "ignoreDeprecations": "6.0" in tsconfig.json to address the baseUrl
deprecation warning; after these dependency and tsconfig changes, re-run the
standard desktop checks to validate the shared types change in sync.ts (the
change around "chat.modelCatalog").

In `@apps/ios/ADE/Services/SyncService.swift`:
- Around line 3291-3295: The cachedChatModelCatalog() helper currently returns
cached.catalog without checking TTL; update cachedChatModelCatalog() to compute
the age using Date().timeIntervalSince(cached.fetchedAt) and compare it against
chatModelsCacheTTL, returning nil if the entry is older than the TTL (and
optionally remove the stale entry from chatModelCatalogCache[cacheKey]); use the
same cacheKey logic (chatModelsCacheKey(provider: "catalog")) and the existing
cached structure (cached.fetchedAt / cached.catalog) when implementing the
check.

---

Outside diff comments:
In `@apps/desktop/src/main/services/ai/apiKeyStore.ts`:
- Around line 381-395: storeApiKey and deleteApiKey currently mutate the
in-memory store before performing durable writes; change both to perform
persistence first and only update the in-memory cache on success: in
storeApiKey, trim inputs and validate, then perform the platform write(s) (call
writeMacosKeychainSecret and update provider index via
readMacosKeychainProviderIndex/writeMacosKeychainProviderIndex for macOS, or
call persistEncryptedStore for the encrypted path) using local variables and
without touching ensureStore() result, and only after those calls succeed assign
the key into the result of ensureStore(); apply the analogous approach to
deleteApiKey (perform keychain/index removal or persistEncryptedStore first,
then remove from in-memory store), and ensure any thrown errors leave the
in-memory store unchanged.

In `@apps/desktop/src/main/services/ai/authDetector.ts`:
- Around line 791-833: The success branch in verifyCursorApiKey currently never
clears the prior "auth-failed" state, so add a call to clear the stale Cursor
auth failure before returning on success; locate verifyCursorApiKey and, just
before the successful return in the try block, invoke the existing
runtime-health clearing helper (e.g., clearProviderRuntimeAuthFailure("cursor")
or the counterpart to reportProviderRuntimeAuthFailure) to mark Cursor as
healthy so buildProviderConnections() isn't left downgraded after a later
successful verification.

In `@apps/desktop/src/main/services/ai/providerConnectionStatus.ts`:
- Around line 95-105: The health-state handling updates runtimeAvailable (in
providerConnectionStatus.ts) but fails to mirror that downgrade to
usageAvailable, causing inconsistent status objects; update the branches that
set status.runtimeAvailable = false for health.state values like "auth-failed"
and "runtime-failed" (and the similar block around the other occurrence
referenced) to also set status.usageAvailable = false and ensure when
health.state === "ready" you set status.usageAvailable = true alongside
runtimeAvailable and authAvailable; locate the status object mutations in the
function handling health (look for the health.state checks and the status
variable) and apply the same usageAvailable changes in both places.

In `@apps/desktop/src/renderer/components/chat/ChatIosSimulatorPanel.tsx`:
- Around line 1715-1725: The code is copying event.status.lastError into
liveVisual.error even when serviceManagedFallback is true, which causes the
reconnect overlay to show the raw transport error; update the setLiveVisual call
used when handling "stream-stopped"/"stream-error" so that inside the updater
you only assign error = event.status.lastError ?? current.error when
serviceManagedFallback is false, otherwise preserve current.error (or set the
service-managed fallback message) so serviceManagedFallback prevents overwriting
liveVisual.error; adjust the setMessage branch accordingly if needed (symbols:
setLiveVisual, liveVisual, event, event.status.lastError,
serviceManagedFallback).

In `@apps/desktop/src/renderer/components/settings/ProvidersSection.tsx`:
- Around line 451-467: The verification badge for a provider can remain stale
because verificationByProvider isn't cleared before making IPC calls; modify
verifyApiKey to clear the entry for the target provider (use
setVerificationByProvider to remove or set undefined for [provider]) immediately
after setVerifyingProvider(provider) and before calling
invalidateAiDiscoveryCache()/window.ade.ai.verifyApiKey, so any old
success/failure badge is removed while the new request runs; apply the same
change to the other verification routine covering lines 470-492 (the other
function that also calls setVerificationByProvider/verifyApiKey IPCs) so both
code paths clear the previous verification state up-front.

In `@apps/ios/ADE/Views/Work/WorkModelCatalog.swift`:
- Around line 487-505: The code appends runtime and registry Claude IDs before
adding shorthand aliases, causing mixed canonical/runtime keys to bypass
deduplication in workDeduplicatedModelOptions()/workModelLookupKeys(); change
the order so you first derive and append one canonical Claude equivalence key
using workCanonicalClaudeRegistryId(for: trimmed) (or runtime->canonical mapping
if needed), then append any shorthand aliases like the short names
(sonnet/haiku) afterwards; apply the same canonical-first ordering to the second
Claude block (lines ~510-538) and mirror the pattern for any analogous
Codex/other model mapping code (workCanonicalCodexRegistryId(),
workCodexRuntimeModelId()) so shorthand aliases are only added after a canonical
key is present.

---

Minor comments:
In `@apps/desktop/scripts/dev.cjs`:
- Around line 259-263: Add a preflight check to ensure the remote debugging port
is free before adding `--remote-debugging-port=${remoteDebugPort}` to
`electronArgs`: use a small port-availability probe (for example creating a
temporary TCP server or using a get-free-port helper) to detect if
`remoteDebugPort` is in use, and if it is either pick an available port and
update `remoteDebugPort`/the argument or fail fast with a clear error; update
the code that builds `electronArgs` and the `spawnProcess("electron",
npxCommand, electronArgs, electronEnv)` call so the actual, verified port is
passed to Electron.

In `@apps/desktop/src/main/services/chat/agentChatService.test.ts`:
- Around line 911-917: The helper waitForSessionTitle currently polls 100 times
with zero-delay sleeps which can flake on CI; change waitForSessionTitle to use
a real timeout budget by tracking elapsed time (e.g., Date.now or
performance.now) and looping with a small non-zero delay (e.g., 20–100ms) until
elapsed exceeds a configurable maxTimeout (default e.g., 5000ms), returning when
sessionService.get(sessionId)?.title matches or throwing a timeout error after
the budget; update the function signature to accept an optional timeoutMs
parameter if needed and ensure the polling uses the sessionService.get(...)
check exactly as before.
- Around line 3340-3413: The tests use fragile timing: replace the fixed sleep
after the second Codex thread/name/updated emit with an explicit assertion/wait
(e.g., await vi.waitFor or await waitForSessionTitle) that the session title
remains "Manual Title" instead of setTimeout(20); and for the Droid test, ensure
the mock bridge handler exists before calling onSessionUpdate by awaiting its
registration (e.g., await vi.waitFor(() =>
mockState.droidPooled.bridge?.onSessionUpdate) or awaiting a condition on
sessionService) and only then invoke
mockState.droidPooled.bridge.onSessionUpdate; update the examples around
sendMessage, mockState.emitCodexPayload, waitForSessionTitle, and
mockState.droidPooled.bridge.onSessionUpdate accordingly.

In `@apps/desktop/src/main/services/chat/agentChatService.ts`:
- Around line 1401-1411: The function validateReasoningEffortForDescriptor
improperly only restricts descriptor.reasoningTiers for provider "claude";
update it so that after calling validateReasoningEffort(provider, effort) you
check descriptor?.reasoningTiers for both "claude" and "codex" (i.e., any
provider using this helper) and return null if the validated tier is not
included. Modify validateReasoningEffortForDescriptor (and keep calling
validateReasoningEffort) to apply the descriptor.reasoningTiers inclusion check
regardless of provider, using the existing
descriptor.reasoningTiers?.includes(validated) logic.

In `@apps/desktop/src/main/services/pty/ptyService.ts`:
- Around line 2147-2149: The non-live branch currently prefers row.chatSessionId
over the in-memory binding, which can return stale bindings; update the
chatSessionId assignment so it checks terminalChatSessions.get(row.id) before
row.chatSessionId (matching the live branch and terminalSessionFromSummary()),
i.e. prefer terminalChatSessions.get(row.id) as the first fallback, then
row.chatSessionId, and finally null; adjust the same logic used by
enrichSessions()/terminalSessionFromSummary() to ensure consistent
in-memory-first behavior.

In `@apps/desktop/src/renderer/components/chat/ChatIosSimulatorPanel.test.tsx`:
- Line 366: The test currently uses a real-time sleep via "await new
Promise((resolve) => setTimeout(resolve, 700));" which causes flakiness; replace
that with deterministic timer control by enabling fake timers
(jest.useFakeTimers() or equivalent), trigger the actions that start the
timeout, then advance timers with jest.advanceTimersByTime(700) or
jest.runOnlyPendingTimers()/jest.runAllTimers(), and finally restore timers with
jest.useRealTimers(); update the test to remove the real Promise sleep and use
the fake-timer API around the code that relies on the timeout.

In `@apps/desktop/src/shared/modelCatalog.ts`:
- Around line 352-361: The loop over potentialProviders (involving variables
potentialProviders, existingFamilies, and providers) can push duplicate provider
entries when opencodeProviders contains repeated ids because existingFamilies is
not updated after adding a new provider; fix this by adding the inserted id to
existingFamilies (or otherwise marking it) immediately after pushing the new
provider so subsequent iterations see it and skip duplicates.

In `@apps/ios/ADE/Views/Work/WorkModelPickerSheet.swift`:
- Around line 265-269: supportedReasoningTiers(for:) currently normalizes
reasoningEfforts but can return duplicate strings (differing only by
case/whitespace), causing duplicate IDs in the ForEach used by reasoningPills;
update supportedReasoningTiers(for:) to deduplicate the normalized values before
returning (preserving original order) so tiers used in ForEach(tiers, id:
\.self) are unique and stable.

In `@getting-started/connect-provider.mdx`:
- Around line 58-63: The "Cursor" entry is currently placed under the "CLI-based
providers" grouping which is misleading because Cursor is SDK/API-key based;
update the UI/markdown grouping in connect-provider.mdx so SDK-backed providers
are clearly represented — either rename the "CLI-based providers" category to
something like "CLI & SDK providers" (update the category header text used
around the CardGroup) or move the Cursor Card out of that CardGroup into a new
"SDK/API-key providers" section; also apply the same change where the duplicate
occurs around line 75 to keep both instances consistent (look for the "Cursor"
Card, "CardGroup", and "Note" elements and the literal "CLI-based providers"
label).

---

Nitpick comments:
In `@apps/desktop/native/ios-sim-helpers/build.sh`:
- Around line 85-88: The codesign calls in build.sh are currently silenced with
">/dev/null 2>&1 || true" so helper signing failures are hidden; change the two
lines that run codesign against "$CAPTURE" and "$INPUT" to run without "||
true", capture their exit status (e.g., if ! codesign --force --sign -
"$CAPTURE" >/dev/null 2>&1; then echo "Warning: codesign failed for $CAPTURE"
>&2; fi) and do the same for "$INPUT"; optionally set a nonfatal flag (e.g.,
SIGN_WARNED=1) so later packaging can decide to fail or emit a summary, but at
minimum print a clear stderr warning including the file variable name when
codesign exits nonzero.

In `@apps/desktop/package.json`:
- Around line 190-198: The extraResources entry that currently sits under the
global build.extraResources (the object with "from":"native/ios-sim-helpers",
"to":"native/ios-sim-helpers", "filter":[...]) should be moved into the mac
packaging config so only mac builds include those files; remove that entry from
the top-level build.extraResources and add the same mapping under the
mac-specific target (e.g., targets.mac or the mac build config block) so files
like "build.sh", "sim-capture.swift", and "sim-input.m" are packaged only for
macOS.

In `@apps/desktop/src/main/services/ai/aiIntegrationService.test.ts`:
- Around line 482-486: The current test for
service.verifyApiKeyConnection("cursor") uses toMatchObject which would still
pass if the response accidentally includes sensitive fields; update the
assertions to explicitly forbid secret fields and/or assert exact equality:
after calling service.verifyApiKeyConnection("cursor") assert the result
strictly equals the expected object (provider: "cursor", ok: true, source:
"store") or add negative assertions like
expect(result).not.toHaveProperty("key") and
expect(result).not.toHaveProperty("apiKey") (and similarly for "secret" or other
sensitive names) for both the check around verifyApiKeyConnection("cursor") and
the similar block at the later test (the one covering the same flow around lines
524-528).

In `@apps/desktop/src/main/services/sync/syncRemoteCommandService.test.ts`:
- Around line 1545-1549: Add the new action string "chat.modelCatalog" to the
IOS_REMOTE_COMMAND_ACTIONS constant so the iOS parity list matches runtime
registration; locate the IOS_REMOTE_COMMAND_ACTIONS definition (the shared iOS
action list) and append "chat.modelCatalog" to the array/enum that lists
permitted remote commands, then run the test suite to confirm parity coverage
passes for the new test in syncRemoteCommandService.test.ts which exercises
service.execute("chat.modelCatalog", ...).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

Comment thread apps/desktop/scripts/audit-chat-model-runtime.mjs
Comment thread apps/desktop/src/main/services/ai/aiIntegrationService.ts
Comment on lines +162 to +181
function writeMacosKeychainSecret(account: string, value: string): void {
if (!isMacosKeychainAvailable()) {
throw new Error("macOS Keychain is unavailable. Cannot persist API keys.");
}
const result = runSecurity([
"add-generic-password",
"-a",
account,
"-s",
MACOS_KEYCHAIN_SERVICE,
"-w",
value,
"-U",
]);
if (!result.ok) {
rememberKeychainError("write", result);
throw new Error(macosKeychainError ?? "macOS Keychain write failed.");
}
clearKeychainError();
}
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

🧩 Analysis chain

🏁 Script executed:

# First, find the implementation of runSecurity in the file
rg -A 20 "function runSecurity|const runSecurity" apps/desktop/src/main/services/ai/apiKeyStore.ts

Repository: arul28/ADE

Length of output: 932


🏁 Script executed:

# Check for imports and encrypted-store usage in the file
rg "import|encrypted" apps/desktop/src/main/services/ai/apiKeyStore.ts

Repository: arul28/ADE

Length of output: 1066


🏁 Script executed:

# Search for how storeApiKey and deleteApiKey work
rg -A 10 "function storeApiKey|function deleteApiKey|export.*storeApiKey|export.*deleteApiKey" apps/desktop/src/main/services/ai/apiKeyStore.ts

Repository: arul28/ADE

Length of output: 1050


🏁 Script executed:

# Check if there are alternatives like stdin being used elsewhere in the codebase
rg -r "stdin|spawnSync.*password|security.*stdin" apps/desktop/src --type ts

Repository: arul28/ADE

Length of output: 11793


🏁 Script executed:

# Search for encrypted-store usage in the codebase
fd -e ts -e tsx | xargs rg "encrypted-store|encryptedStore" | head -20

Repository: arul28/ADE

Length of output: 1408


Don't pass the API key as a command-line argument to the security command.

Line 173 passes the raw key via -w value, making the secret observable through process inspection (e.g., ps, /proc/[pid]/cmdline) while the child process runs. Use stdin instead: the security command accepts -w - to read the password from stdin, eliminating plaintext exposure on the command line.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/main/services/ai/apiKeyStore.ts` around lines 162 - 181, In
writeMacosKeychainSecret the API key is currently passed on the command line via
the "-w value" argument (making it visible to process inspectors); change the
call to runSecurity to pass "-w" "-" (i.e. "-w", "-") and feed the secret via
the child process stdin instead of the argv, updating runSecurity (or its call
site) to accept and write stdin data (the value string) to the spawned process;
keep the existing error handling (rememberKeychainError/clearKeychainError) but
ensure runSecurity returns error details when its stdin-based invocation fails.

Comment thread apps/desktop/src/main/services/chat/agentChatService.ts
Comment thread apps/desktop/src/main/services/chat/agentChatService.ts
Comment thread apps/desktop/src/main/services/ipc/registerIpc.ts Outdated
Comment thread apps/desktop/src/renderer/components/chat/AgentChatPane.tsx Outdated
Comment thread apps/desktop/src/renderer/components/chat/ChatIosSimulatorPanel.tsx Outdated
Comment thread apps/desktop/src/shared/types/sync.ts
Comment thread apps/ios/ADE/Services/SyncService.swift
@arul28
Copy link
Copy Markdown
Owner Author

arul28 commented May 1, 2026

@copilot review but do not make fixes

Copy link
Copy Markdown
Contributor

Copilot AI commented May 1, 2026

@copilot review but do not make fixes

Here's my review of the follow-up commit (5dca2ec) that addresses the earlier feedback:

What was addressed

  • iOS loading spinner stuck on cancellationdefer { isLoadingCatalog = false; syncSelectionStateToCatalog() } now guarantees the spinner clears even on Task.isCancelled.
  • Cache/Keychain inconsistencystoreApiKey/deleteApiKey now persist to Keychain/disk first, then mutate in-memory cache only on success.
  • TOCTOU on socket directoryensurePrivateDirectory now opens the directory with O_NOFOLLOW and calls fchmodSync on the open fd instead of chmodding by name.
  • Speculative fallback model IDsACTIVE_FALLBACK_SDK_ROWS removed; only the minimal two-row fallback is used when the SDK is unreachable.
  • workDynamicModelOption naming collision — renamed to workCatalogModelOption for the AgentChatModelCatalogModel path; tier heuristic extracted to shared workDynamicModelTier(for:curated:).
  • runtimeTitleAdopted not persisted — now written to PersistedChatState and restored on session hydration.
  • max effort alias for Codex — re-added codex: { max: "xhigh" } to EFFORT_ALIASES.

Remaining / new issues

1. storeApiKey still has a window where cache is ahead of Keychain index (apiKeyStore.ts L388–400)

writeMacosKeychainSecret(normalizedProvider, normalizedKey);
const index = readMacosKeychainProviderIndex();
writeMacosKeychainProviderIndex(new Set([...index.providers, normalizedProvider]));
store[normalizedProvider] = normalizedKey;   // ← only set after both writes succeed

The secret is now written before the cache update — good. But writeMacosKeychainProviderIndex can still throw (e.g., user denies Keychain access on the second call). If it throws, the key is in the Keychain but not in the index, so listStoredProviders() won't include it and the Settings UI will appear empty until the next full probe. Consider wrapping both keychain writes in a single try-block and rolling back the secret if the index write fails.

2. deleteApiKey Keychain path has an analogous gap

deleteMacosKeychainSecret(normalizedProvider);
const index = readMacosKeychainProviderIndex();
writeMacosKeychainProviderIndex(index.providers.filter(...));
delete store[normalizedProvider];

If deleteMacosKeychainSecret succeeds but writeMacosKeychainProviderIndex throws, the key is gone from the Keychain but the index still lists the provider. Subsequent listStoredProviders() calls will return the provider name but getApiKey will return null — a confusing inconsistency.

3. windowsCmdQuote escapes " with ^" which is wrong in double-quote context (cursorSdkHooks.ts)

return `"${value.replace(/(["^&|<>%])/g, "^$1")}"`;

Inside a double-quoted CMD string, ^ is not an escape character — the standard escape for " inside a quoted CMD string is "". The current implementation would produce "node ^"C:\path\to\script.cjs^"" which CMD interprets incorrectly. The backtick-quoting issue from Greptile issue 4 is also only partially resolved — the check for how Cursor's runner tokenises the command field is still unconfirmed.

4. O_DIRECTORY fallback to 0 silently degrades on platforms that define it (cursorSdkPool.ts)

fd = fs.openSync(dir, O_RDONLY | (O_DIRECTORY ?? 0) | (O_NOFOLLOW ?? 0));

On Linux, both O_DIRECTORY and O_NOFOLLOW are always defined (they're POSIX). The ?? 0 fallback only applies on Windows where these constants don't exist — but on Windows, O_NOFOLLOW semantics aren't enforced by the kernel anyway. If O_DIRECTORY is 0 on any real platform, opening a symlink that targets a file would succeed and fstatSync(fd).isDirectory() would catch it. This is fine but worth a comment explaining the fallback behaviour.

5. workModelLookupKeys reordering changes lookup priority in subtle ways (WorkModelCatalog.swift)

The commit moves append(trimmed) from before the canonical/runtime ID lookups to after. This means for a raw input like "anthropic/claude-opus-4-7", the lookup now tries "anthropic/claude-opus-4-7" as a registry ID first (via workCanonicalClaudeRegistryId), then the runtime alias, and then the raw string last. Previously the raw string was the first candidate. For inputs that are already canonical IDs the new order should behave identically, but any model whose workCanonicalClaudeRegistryId returns a different value than the raw input will now have a different first-match. This could cause the active model to highlight a different row in the picker.

6. descriptorInfoKey scope leak (minor, agentChatService.ts)

The descriptorInfoKey helper is defined with const inside getModelCatalog but it's a pure function with no closure captures. Defining it inside the function means a new function object is allocated on every getModelCatalog call. No behavioral issue, just unnecessary allocation.


Minor notes

  • The syncSelectionStateToCatalog() call was moved into the defer block in WorkModelPickerSheet. It was previously called both after the cached-data path and after the async path. In the new code the cached-data early sync call is gone — selection now only syncs in the defer. For large catalogs this is fine (the defer runs before the UI updates), but for the fast-path where cached data is already available the first render will show the wrong group/provider until defer runs.
  • registerIpc.ts wraps invalidateProviderReadinessCaches() in try/catch with a warn log but then silently swallows the error — the caller of the IPC handler gets a successful void response even if cache invalidation failed. This is probably fine for UX, but worth documenting in a comment.

Copilot finished work on behalf of arul28 May 1, 2026 22:24
Comment thread apps/desktop/src/main/services/ai/apiKeyStore.ts
Comment thread apps/desktop/src/main/services/ai/apiKeyStore.ts
@arul28
Copy link
Copy Markdown
Owner Author

arul28 commented May 1, 2026

@copilot review but do not make fixes

Copy link
Copy Markdown
Contributor

Copilot AI commented May 1, 2026

@copilot review but do not make fixes

Here's my review of the third commit (f7c957e — "ship: tighten review follow-up"):

What was addressed

  • storeApiKey P1 — key is now written to Keychain, cache is updated, then the provider index write is best-effort via tryWriteMacosKeychainProviderIndex.
  • deleteApiKey P1 — same pattern: Keychain delete → cache delete → best-effort index write.
  • windowsCmdQuote incorrect escaping — simplified to reject ", %, and control characters outright rather than attempting broken caret escaping. Windows Electron path now uses set ELECTRON_RUN_AS_NODE=1&& prefix directly instead of the outer cmd /d /s /c "..." wrapper.
  • catalogDescriptorInfoKey allocation inside hot loop — promoted to module-level pure function.
  • workModelLookupKeys orderingappend(trimmed) restored as the first candidate (raw input wins over canonical/runtime ID aliases).
  • Test coverage — new tests for both the store and delete index-write-failure paths; new test for the Windows Electron hook command format.
  • Explanatory commentstryWriteMacosKeychainProviderIndex, registerIpc.ts try/catch blocks, and the O_NOFOLLOW ?? 0 fallback in cursorSdkPool.ts all have clarifying comments now.

Remaining / new issues

1. tryWriteMacosKeychainProviderIndex error recording is an undocumented side-effect

The error IS preserved in macosKeychainError because writeMacosKeychainSecret calls rememberKeychainError before throwing. That's why the new tests pass. But the comment on the catch block says only "The provider index only powers discovery/listing" — it doesn't mention that the error is still visible in getApiKeyStoreStatus(). Someone reading tryWriteMacosKeychainProviderIndex in isolation would assume the error is swallowed completely.

2. windowsCmdQuoteset builtin works only if Cursor uses exec, not execFile

The format set ELECTRON_RUN_AS_NODE=1&& "path\to\electron.exe" "path\to\script.cjs" works when Cursor's hook runner passes the full command string to cmd.exe /c (i.e., via child_process.exec). If the runner uses execFile with the first word as the executable, set is not a filesystem executable and the hook would fail silently. The test confirms the non-regression but doesn't cover the actual Cursor hook runner path. This is the same concern from Issue 4 of the original Greptile review — it's now narrowed to the Windows Electron case but still unresolved.

3. windowsCmdQuote rejects % — affects paths with % in the user profile

Throwing on % is the right defensive call to prevent %VAR% expansion. In practice, process.execPath for a packaged Electron app on Windows will usually be something like C:\Users\username\AppData\Local\Programs\ade\ADE.exe, which contains no %. But if the user's profile directory or installation path contains a literal % (unusual but valid on NTFS), the hook will fail with an unhelpful error about control characters.

4. apiKeyStore.test.ts — second test does not verify macosKeychainError is cleared on next successful operation

The two new tests verify error recording when writes fail, but there's no test that confirms macosKeychainError is cleared after a subsequent successful Keychain write. The clearKeychainError() call inside writeMacosKeychainSecret would handle this, but a test case covering "store fails, then store succeeds, error cleared" would close the observability loop.

5. WorkModelCatalog.swift workModelLookupKeys — ordering is now raw-first, then registry ID

With append(trimmed) restored as the first candidate, a raw input like "claude-opus-4-7" (runtime id) is tried first. If the curated list contains "anthropic/claude-opus-4-7" (registry id) but not the bare form, the lookup misses on the first candidate, then workCanonicalClaudeRegistryId("claude-opus-4-7") returns "anthropic/claude-opus-4-7" which matches on the second candidate. This is correct. Worth noting the ordering is now: raw → canonical registry id → canonical codex registry id → runtime id → codex runtime id → openai-stripped prefix. The full correctness depends on workCanonicalClaudeRegistryId and workCanonicalCodexRegistryId not returning the raw input unchanged (which they don't — they only return a value if a known alias maps to a different canonical form).

Copilot finished work on behalf of arul28 May 1, 2026 22:47
Comment thread apps/desktop/src/main/services/ai/apiKeyStore.ts
@arul28 arul28 force-pushed the ade/ios-fixes-13925e5f branch from f7c957e to f037e86 Compare May 1, 2026 23:09
@arul28
Copy link
Copy Markdown
Owner Author

arul28 commented May 1, 2026

@copilot review but do not make fixes

Copy link
Copy Markdown
Contributor

Copilot AI commented May 1, 2026

@copilot review but do not make fixes

Here's my review of the PR in its current state (latest commit f7c957e), based on the new Greptile findings:

What Greptile raised

P1 — spawnSync blocking the Electron main process (apiKeyStore.ts)

This is real. On cold start, ensureStore() calls readMacosKeychainStore(keychainProviders) where keychainProviders = knownProviderCandidates(...) = all 10 entries in ENV_KEY_PROVIDERS plus any extras from the index. readMacosKeychainStore loops over each and calls readMacosKeychainSecretrunSecurityspawnSync with a 5-second timeout. Worst case: 10 × 5 s = 50 s blocking the Electron main-process event loop on a contended or locked Keychain.

The cache (if (cache) return cache;) prevents repeat calls, but the first call on startup — before the cache is warm — will block. Every IPC handler that calls getApiKey / storeApiKey / listStoredProviders synchronously awaits this cold path. The fix is to move Keychain reads off the main thread (via a worker_threads helper or by making ensureStore async with a blocking init flag), or at minimum batch the reads into a single security find-generic-password -a __ade_provider_index__ call to get the index and only read secrets for providers in the index rather than probing all known providers upfront.

P1 — Orphaned stateRoot directories (cursorSdkPool.ts)

Also real. Both runCursorSdkCatalogRequest and runCursorSdkCloudRequest embed Date.now() + Math.random() in their poolKey, which makes buildCursorSdkPaths compute a unique keyHash per call and therefore a unique {projectRoot}/.ade/cache/cursor-sdk/{hash}/state/ directory. fs.mkdirSync(paths.stateRoot, { recursive: true }) creates it; releaseCursorSdkConnection just calls pooled.dispose() and pools.delete(poolKey) — no fs.rmSync. The Cursor SDK worker can also write state files into stateRoot before being killed.

For users who open the model picker or trigger cloud operations repeatedly (every model picker open calls catalog.models), a new directory tree accumulates indefinitely. The unique key was presumably added to prevent key collisions across concurrent catalog requests, but a deterministic key per (type, workspacePath) with a concurrency counter would avoid the leak. Alternatively, releaseCursorSdkConnection can fs.rmSync(paths.stateRoot, { recursive: true, force: true }) for any pool entry whose pooled flag was not the reusable long-lived pool.


Other observations (not new)

  • The SECURITY_TIMEOUT_MS = 5_000 timeout is per-call, not aggregate. On a slow Keychain, even index-only reads block. This should ideally have a much tighter timeout (500–1000 ms) given it runs on the main thread.
  • runCursorSdkCatalogRequest passes sessionId: "catalog" — if two concurrent catalog requests race with the same workspace path (but different unique poolKeys), they'll both create worker processes rather than sharing one. The unique key defeats the pool's deduplication intent for that call site entirely.

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: 10

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/desktop/src/main/services/chat/agentChatService.test.ts (1)

1-10506: ⚠️ Potential issue | 🟠 Major

Run desktop validation and fix blocking errors before merge

Run:
npm --prefix apps/desktop install
npm --prefix apps/desktop run typecheck
npm --prefix apps/desktop run lint
npm --prefix apps/desktop run build
npm --prefix apps/desktop run test

Observed blocking issues:

  • tsconfig.json(7,5): TS5101 (baseUrl deprecated) and missing 'node' type definitions (TS2688).
  • Dev tools missing: tsup and vitest reported "not found".
  • ESLint startup/module resolution errors and many lint warnings surfaced.

Required fixes:

  • Install devDependencies (npm --prefix apps/desktop install) or run in CI with deps provisioned.
  • Add/verify @types/node or adjust tsconfig to resolve TS2688/TS5101.
  • Ensure tsup, vitest, eslint and other tooling are available.
  • Re-run the listed commands and merge only after all checks complete successfully.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/main/services/chat/agentChatService.test.ts` around lines 1
- 10506, The CI/dev errors are caused by missing dev tooling and Node types
causing TS2688/TS5101; to fix, add the required devDependencies (tsup, vitest,
eslint, `@types/node`, etc.) into package.json devDependencies and run npm
--prefix apps/desktop install so tests/linters/build tools are available, update
tsconfig.json to include Node typings (e.g. add "types": ["node"] or ensure
"typeRoots" includes node_modules/@types) and remove or correct the deprecated
baseUrl setting so the compilerOptions are valid, then re-run npm --prefix
apps/desktop run typecheck, lint, build and test to confirm
createAgentChatService/startOpenCodeSession/unstable_v2_createSession tests run
cleanly before merging.
🧹 Nitpick comments (2)
apps/desktop/src/renderer/components/chat/AgentChatPane.tsx (1)

2439-2454: ⚡ Quick win

Apply ordering before the first state write.

setAvailableModelIds(available) on Line 2440 bypasses orderModelIds, so the early-return branches (Cursor not ready / probe error) return a differently ordered list than the success branches. Use one ordered value across all paths to keep picker ordering stable.

Suggested patch
-      const available = deriveConfiguredModelIds(status, { includeDroid: true });
-      setAvailableModelIds(available);
+      const available = deriveConfiguredModelIds(status, { includeDroid: true });
+      const orderedAvailable = orderModelIds(available);
+      setAvailableModelIds(orderedAvailable);
       const cursorReady = status.availableProviders?.cursor === true
         || status.providerConnections?.cursor?.runtimeAvailable === true;
-      if (!cursorReady) return available;
+      if (!cursorReady) return orderedAvailable;

       let cursorModels: Awaited<ReturnType<typeof getAgentChatModelsCached>>;
       try {
         cursorModels = await getAgentChatModelsCached({
           projectRoot,
           provider: "cursor",
           activateRuntime: true,
         });
       } catch {
-        return available;
+        return orderedAvailable;
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/renderer/components/chat/AgentChatPane.tsx` around lines
2439 - 2454, The code calls setAvailableModelIds(available) before ordering,
causing early-return branches to return an un-ordered list; compute a single
ordered list by calling orderModelIds on the result of deriveConfiguredModelIds
(use the same ordered value for both setAvailableModelIds and any returns), then
proceed to check cursorReady and call getAgentChatModelsCached; update usages of
available to the ordered variable so picker ordering is consistent across the
cursor-ready, not-ready, and error paths (refer to deriveConfiguredModelIds,
orderModelIds, setAvailableModelIds, cursorReady, and getAgentChatModelsCached).
apps/desktop/src/main/services/chat/agentChatService.test.ts (1)

3316-3318: ⚡ Quick win

Replace fixed-delay negative assertion with deterministic dispatch completion

Using a hardcoded 20ms sleep here can make the test timing-sensitive on slower CI. Prefer awaiting deterministic dispatch completion and asserting call count directly.

Suggested change
-    await service.sendMessage({
+    await service.sendMessage({
       sessionId: session.id,
       text: "/review",
-    });
+    }, { awaitDispatch: true });

@@
-    await new Promise((resolve) => setTimeout(resolve, 20));
-    expect(aiIntegrationService.summarizeTerminal).not.toHaveBeenCalled();
+    expect(aiIntegrationService.summarizeTerminal).toHaveBeenCalledTimes(0);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/main/services/chat/agentChatService.test.ts` around lines
3316 - 3318, Remove the hardcoded 20ms sleep and instead wait deterministically
for all pending dispatches/async microtasks to complete before asserting; for
example, replace the new Promise sleep with awaiting the relevant
dispatch/promise that would trigger summarization (or call a helper like
flushPromises() / await waitFor(() => {/* no-op */}) or advance fake timers via
jest.runAllTimers() if using fake timers) so that
aiIntegrationService.summarizeTerminal is asserted with
expect(aiIntegrationService.summarizeTerminal).not.toHaveBeenCalled() only after
all work has completed.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/desktop/scripts/audit-chat-model-runtime.mjs`:
- Around line 291-303: buildDescriptorIndexes currently stores
descriptor.shortId and descriptor.aliases into the global byId map which allows
the same alias/shortId from different providers to collide; update the function
to namespace shortId and aliases by the resolved provider (use the same provider
key used for byProviderModelId) before set'ing into byId (e.g., store
`${provider}:${shortId}` and `${provider}:${alias}`), and apply the identical
change to the other index-building loop that mirrors lines 329-343 so lookups
are provider-scoped and cannot cross-resolve across providers; use
registry.resolveProviderGroupForModel(descriptor) to obtain the provider key for
each descriptor when namespacing.
- Around line 67-73: The cdpPort calculation currently parses the entire
fallback expression so a non-numeric ADE_MODEL_AUDIT_CDP_PORT yields NaN and
prevents fallback; change this to try each env var in order
(ADE_MODEL_AUDIT_CDP_PORT, ADE_ELECTRON_REMOTE_DEBUGGING_PORT,
ADE_APP_CONTROL_CDP_PORT) by parsing and validating each candidate individually
(Number.parseInt and then Number.isFinite/Number.isInteger and optional
port-range check 1–65535), return the first valid integer, otherwise use the
default 9222; apply the same fix to the duplicate logic referenced around the
cdpPort usage at the other occurrence (lines 215–217) so both places robustly
validate env-derived ports.

In `@apps/desktop/scripts/dev.cjs`:
- Around line 262-265: The current electronArgs array places flags after the app
path so --remote-debugging-port and macOS -ApplePersistenceIgnoreState are
treated as app args; update the construction of electronArgs to put flags before
the app path by creating electronArgs as ["electron",
`--remote-debugging-port=${remoteDebugPort}`, /* mac flags if any */, "."] and
when process.platform === "darwin" push "-ApplePersistenceIgnoreState" and "YES"
into electronArgs before appending the "." element (i.e., modify the
electronArgs creation and the process.platform branch around the electronArgs
variable to ensure flags are inserted prior to ".").

In `@apps/desktop/src/main/services/chat/agentChatService.ts`:
- Around line 1423-1433: The current resolveCodexReasoningEffortForRuntime
unconditionally returns DEFAULT_REASONING_EFFORT even if
validateReasoningEffortForDescriptor previously rejected values for the provided
descriptor, which can reintroduce an unsupported tier; update the function (and
the analogous code at lines ~10783-10787) to run
validateReasoningEffortForDescriptor against DEFAULT_REASONING_EFFORT (after
normalizeReasoningEffort) and only return DEFAULT_REASONING_EFFORT if that
validation succeeds, otherwise return null/undefined (or propagate failure) so
an unsupported value is never sent to runtime; reference functions/values:
resolveCodexReasoningEffortForRuntime, validateReasoningEffortForDescriptor,
normalizeReasoningEffort, DEFAULT_REASONING_EFFORT, and
descriptor.reasoningTiers.
- Around line 1883-1887: The current authentication-detection expression (the
return that checks message.includes(...)) is too broad because matching any
occurrence of "api key" misclassifies unrelated errors; update the logic that
evaluates the message variable so it requires whole-word matches and an
authentication context (e.g., use case-insensitive regex with word boundaries
and require nearby auth qualifiers like "invalid", "missing", "not provided",
"revoked", "not found", "unauthorized", or "forbidden"); replace the simple
message.includes checks with combined regex tests such as /\b(api[- ]key)\b/i
together with patterns like /(invalid|missing|not provided|revoked|not found)/i
(or a single compound regex) so only true auth-related texts trigger the
auth-failure path.

In `@apps/desktop/src/main/services/chat/cursorModelsDiscovery.ts`:
- Around line 328-340: The probe function currently short-circuits and returns
cachedRows from getCachedCursorSdkModels(apiKey), which prevents a live
verification; change probeCursorSdkModelDiscovery to require explicit opt-in to
return cached results by adding an options flag (e.g., options.allowCached or
options.useCache defaulting to false) and only immediately return cachedRows
when that flag is true; otherwise always continue with the live SDK/API
discovery logic (keep getCachedCursorSdkModels, the new options flag, and the
rest of the function intact) so callers that need a fresh probe get a real
network check.

In `@apps/desktop/src/main/services/chat/cursorSdkHooks.ts`:
- Around line 150-156: The guard in main() incorrectly falls back to allow() on
any error even when a socket was explicitly provided; update the logic so that
the presence of socketPath (from parseArg("--socket") or ADE_CURSOR_SDK_SOCKET)
is treated as an explicit launch and must never result in allow() on failure.
Concretely, in the top-level check in main() (symbols: main, socketPath,
adeSession, allow(), deny()), change the branch so that if socketPath is truthy
and something goes wrong you call deny("ADE Cursor policy gate is unavailable.")
(or rethrow) instead of allow(); apply the same change to the analogous
error/fallback handling around the later block referenced (lines ~212-219) so
explicit --socket always yields deny on failure.
- Around line 239-242: The ADE gate is appended last currently; change
construction of nextPreToolUse so adeEntry is first and existing ADE entries are
removed: create nextPreToolUse = [adeEntry, ...existingPreToolUse.filter((entry)
=> !isAdeHookEntry(entry))] (use the same isAdeHookEntry predicate and adeEntry
symbol to avoid duplicates) so the ADE hook runs before any other preToolUse
hooks.

In `@apps/desktop/src/main/services/chat/cursorSdkPolicy.ts`:
- Around line 275-279: The collectPotentialPaths function currently only adds
the entire trimmed string when pathContext is true, which lets raw shell command
strings bypass lane guards; update collectPotentialPaths (and the same fallback
logic used later) to parse raw string shell inputs into tokens before pushing
paths: if pathContext && value is string, run a simple shell-like tokenizer that
splits on whitespace and common shell operators (&&, ||, ;, |, >, <) and extract
path-looking tokens (tokens starting with /, ./, ../, or containing a slash)
into out instead of the whole string; apply the same tokenization logic to the
fallback handling used later so plain command strings like "cd /etc && cat
/etc/passwd" yield the individual path tokens for hardGuard checks.

In `@apps/desktop/src/renderer/components/chat/ChatIosSimulatorPanel.tsx`:
- Around line 1367-1382: The current early-return treats !nextStatus.running
like a soft fallback and can swallow hard startup failures when lastError is
null; change the condition so the reconnect branch only triggers when
nextStatus.degradationReason, nextStatus.fallbackReason, or nextStatus.lastError
are present (i.e., require a non-null error/reason) rather than triggering on
!nextStatus.running alone; keep the setLiveVisual/setMessage/refreshStatus
behavior for true reconnect cases but allow execution to continue (so the
restart effect that watches lastError can schedule recovery) when the service is
not running but there is no lastError.

---

Outside diff comments:
In `@apps/desktop/src/main/services/chat/agentChatService.test.ts`:
- Around line 1-10506: The CI/dev errors are caused by missing dev tooling and
Node types causing TS2688/TS5101; to fix, add the required devDependencies
(tsup, vitest, eslint, `@types/node`, etc.) into package.json devDependencies and
run npm --prefix apps/desktop install so tests/linters/build tools are
available, update tsconfig.json to include Node typings (e.g. add "types":
["node"] or ensure "typeRoots" includes node_modules/@types) and remove or
correct the deprecated baseUrl setting so the compilerOptions are valid, then
re-run npm --prefix apps/desktop run typecheck, lint, build and test to confirm
createAgentChatService/startOpenCodeSession/unstable_v2_createSession tests run
cleanly before merging.

---

Nitpick comments:
In `@apps/desktop/src/main/services/chat/agentChatService.test.ts`:
- Around line 3316-3318: Remove the hardcoded 20ms sleep and instead wait
deterministically for all pending dispatches/async microtasks to complete before
asserting; for example, replace the new Promise sleep with awaiting the relevant
dispatch/promise that would trigger summarization (or call a helper like
flushPromises() / await waitFor(() => {/* no-op */}) or advance fake timers via
jest.runAllTimers() if using fake timers) so that
aiIntegrationService.summarizeTerminal is asserted with
expect(aiIntegrationService.summarizeTerminal).not.toHaveBeenCalled() only after
all work has completed.

In `@apps/desktop/src/renderer/components/chat/AgentChatPane.tsx`:
- Around line 2439-2454: The code calls setAvailableModelIds(available) before
ordering, causing early-return branches to return an un-ordered list; compute a
single ordered list by calling orderModelIds on the result of
deriveConfiguredModelIds (use the same ordered value for both
setAvailableModelIds and any returns), then proceed to check cursorReady and
call getAgentChatModelsCached; update usages of available to the ordered
variable so picker ordering is consistent across the cursor-ready, not-ready,
and error paths (refer to deriveConfiguredModelIds, orderModelIds,
setAvailableModelIds, cursorReady, and getAgentChatModelsCached).
🪄 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: 30384145-4ee9-4519-8978-3dc4a1b25a28

📥 Commits

Reviewing files that changed from the base of the PR and between 6f9b7f9 and f7c957e.

📒 Files selected for processing (33)
  • apps/desktop/native/ios-sim-helpers/build.sh
  • apps/desktop/package.json
  • apps/desktop/scripts/audit-chat-model-runtime.mjs
  • apps/desktop/scripts/dev.cjs
  • apps/desktop/src/main/services/ai/aiIntegrationService.test.ts
  • apps/desktop/src/main/services/ai/aiIntegrationService.ts
  • apps/desktop/src/main/services/ai/apiKeyStore.test.ts
  • apps/desktop/src/main/services/ai/apiKeyStore.ts
  • apps/desktop/src/main/services/ai/authDetector.ts
  • apps/desktop/src/main/services/ai/providerConnectionStatus.ts
  • apps/desktop/src/main/services/chat/agentChatService.test.ts
  • apps/desktop/src/main/services/chat/agentChatService.ts
  • apps/desktop/src/main/services/chat/cursorModelsDiscovery.test.ts
  • apps/desktop/src/main/services/chat/cursorModelsDiscovery.ts
  • apps/desktop/src/main/services/chat/cursorSdkHooks.test.ts
  • apps/desktop/src/main/services/chat/cursorSdkHooks.ts
  • apps/desktop/src/main/services/chat/cursorSdkPolicy.test.ts
  • apps/desktop/src/main/services/chat/cursorSdkPolicy.ts
  • apps/desktop/src/main/services/chat/cursorSdkPool.ts
  • apps/desktop/src/main/services/chat/cursorSdkWorker.ts
  • apps/desktop/src/main/services/ios/iosSimulatorService.ts
  • apps/desktop/src/main/services/ipc/registerIpc.ts
  • apps/desktop/src/main/services/pty/ptyService.ts
  • apps/desktop/src/main/services/sync/syncRemoteCommandService.test.ts
  • apps/desktop/src/renderer/components/chat/AgentChatPane.tsx
  • apps/desktop/src/renderer/components/chat/ChatIosSimulatorPanel.tsx
  • apps/desktop/src/renderer/components/settings/ProvidersSection.test.tsx
  • apps/desktop/src/renderer/components/settings/ProvidersSection.tsx
  • apps/desktop/src/shared/modelCatalog.ts
  • apps/ios/ADE/Services/SyncService.swift
  • apps/ios/ADE/Views/Work/WorkModelCatalog.swift
  • apps/ios/ADE/Views/Work/WorkModelPickerSheet.swift
  • getting-started/connect-provider.mdx
✅ Files skipped from review due to trivial changes (3)
  • apps/desktop/src/renderer/components/settings/ProvidersSection.test.tsx
  • apps/desktop/src/main/services/sync/syncRemoteCommandService.test.ts
  • apps/ios/ADE/Views/Work/WorkModelCatalog.swift
🚧 Files skipped from review as they are similar to previous changes (10)
  • apps/desktop/src/main/services/ai/authDetector.ts
  • apps/desktop/package.json
  • apps/desktop/native/ios-sim-helpers/build.sh
  • getting-started/connect-provider.mdx
  • apps/desktop/src/main/services/chat/cursorSdkHooks.test.ts
  • apps/desktop/src/main/services/chat/cursorSdkPolicy.test.ts
  • apps/desktop/src/renderer/components/settings/ProvidersSection.tsx
  • apps/desktop/src/main/services/chat/cursorModelsDiscovery.test.ts
  • apps/desktop/src/main/services/ipc/registerIpc.ts
  • apps/ios/ADE/Views/Work/WorkModelPickerSheet.swift

Comment thread apps/desktop/scripts/audit-chat-model-runtime.mjs Outdated
Comment thread apps/desktop/scripts/audit-chat-model-runtime.mjs
Comment thread apps/desktop/scripts/dev.cjs Outdated
Comment thread apps/desktop/src/main/services/chat/agentChatService.ts
Comment thread apps/desktop/src/main/services/chat/agentChatService.ts Outdated
Comment thread apps/desktop/src/main/services/chat/cursorModelsDiscovery.ts
Comment thread apps/desktop/src/main/services/chat/cursorSdkHooks.ts
Comment thread apps/desktop/src/main/services/chat/cursorSdkHooks.ts
Comment thread apps/desktop/src/main/services/chat/cursorSdkPolicy.ts
Comment thread apps/desktop/src/renderer/components/chat/ChatIosSimulatorPanel.tsx Outdated
@arul28
Copy link
Copy Markdown
Owner Author

arul28 commented May 1, 2026

@copilot review but do not make fixes

Comment thread apps/ios/ADE/Services/SyncService.swift
@arul28
Copy link
Copy Markdown
Owner Author

arul28 commented May 1, 2026

@copilot review but do not make fixes

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: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/desktop/src/renderer/components/settings/ProvidersSection.tsx (1)

451-473: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Keep verifyingProvider set until the forced refresh completes.

Both verification paths clear verifyingProvider before await refreshStatus(...), which re-enables the action buttons while the post-verify status refresh is still in flight. That makes it easy to race a second verify/edit/delete against the refresh and briefly show stale connection state. Let the existing finally block own the reset.

Suggested fix
       setVerificationByProvider((prev) => ({ ...prev, [provider]: result }));
       setNotice(result.ok ? `${provider} connection verified.` : `${provider} verification failed.`);
-      setVerifyingProvider(null);
       await refreshStatus({ force: true, refreshOpenCodeInventory: true });
@@
       invalidateAiDiscoveryCache();
       setVerificationByProvider((prev) => ({ ...prev, cursor: result }));
       setNotice(result.ok ? "Cursor connection verified." : "Cursor verification failed.");
-      setVerifyingProvider(null);
       await refreshStatus({ force: true, refreshOpenCodeInventory: true });

Also applies to: 475-503

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/renderer/components/settings/ProvidersSection.tsx` around
lines 451 - 473, The code clears verifyingProvider early in the success path
which re-enables UI before refreshStatus completes; in verifyApiKey remove the
in-try call to setVerifyingProvider(null) so the finally block is the single
place that resets it, and keep the await refreshStatus({ force: true,
refreshOpenCodeInventory: true }) before the finally resets; apply the same
change to the sibling function block around lines 475-503 (the other
verification handler) so both verification paths rely on the finally to clear
setVerifyingProvider.
🧹 Nitpick comments (2)
apps/desktop/src/main/services/opencode/openCodeRuntime.test.ts (1)

133-147: 💤 Low value

LGTM — the assertion correctly targets the title: null → body: {} path.

The expect.objectContaining({ body: {} }) matcher is appropriate: the outer objectContaining tolerates the query sibling property, and body: {} deep-equals the empty object the production ternary produces when trimToUndefined(null) evaluates to undefined.

One optional coverage gap worth noting: a whitespace-only title (e.g., " ") is also collapsed to undefined by trimToUndefined and therefore also omits the title field. Adding a second case for that input would give stronger confidence that the trim-then-omit path is fully exercised.

🔬 Optional: extend the test to cover the whitespace-only title edge case
  it("omits the session title when ADE wants OpenCode to auto-name", async () => {
-    await startOpenCodeSession({
+    // null title
+    await startOpenCodeSession({
       directory: "/repo",
       title: null,
       leaseKind: "shared",
       projectConfig: { ai: {} },
       ownerKind: "chat",
       ownerId: "chat-1",
       ownerKey: "chat:chat-1",
     });

     expect(mockState.createSession).toHaveBeenCalledWith(expect.objectContaining({
       body: {},
     }));
+
+    vi.clearAllMocks();
+
+    // whitespace-only title is also auto-named
+    await startOpenCodeSession({
+      directory: "/repo",
+      title: "   ",
+      leaseKind: "shared",
+      projectConfig: { ai: {} },
+      ownerKind: "chat",
+      ownerId: "chat-2",
+      ownerKey: "chat:chat-2",
+    });
+
+    expect(mockState.createSession).toHaveBeenCalledWith(expect.objectContaining({
+      body: {},
+    }));
   });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/main/services/opencode/openCodeRuntime.test.ts` around lines
133 - 147, Test currently verifies null title path; add a second assertion that
a whitespace-only title also collapses and is omitted: call startOpenCodeSession
with title: "   " (or similar whitespace) and the same other args, then assert
mockState.createSession was called with expect.objectContaining({ body: {} }) to
ensure trimToUndefined behavior is exercised for whitespace-only strings; locate
the test block using startOpenCodeSession and mockState.createSession to add
this case.
apps/desktop/src/renderer/components/chat/ChatIosSimulatorPanel.test.tsx (1)

366-366: 💤 Low value

700 ms negative-assertion window is shorter than peer tests and may be flaky on slow CI runners.

The equivalent waits in this file are 1 600 ms (line 415) and 2 500 ms (line 447). If the component has any debounce or async retry logic with a boundary near 700 ms, the assertion on line 368 could pass spuriously. Either use vi.useFakeTimers() for deterministic control or increase the delay to match the longest relevant component timeout.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/renderer/components/chat/ChatIosSimulatorPanel.test.tsx` at
line 366, The 700ms hard wait in the test is too short and can be flaky; update
the test in ChatIosSimulatorPanel.test.tsx to either switch to deterministic
timers by calling vi.useFakeTimers()/vi.runAllTimers() around the async
debounce/timeout behavior (and restore timers after), or simply increase the
await delay to match the longest peer wait (e.g., 1600–2500ms) so the negative
assertion after the Promise settles is not spuriously passing; locate the
Promise await (await new Promise((resolve) => setTimeout(resolve, 700));) and
apply one of these fixes to ensure stability.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/desktop/scripts/audit-chat-model-runtime.mjs`:
- Around line 864-866: The check in verifySessionFields() only runs when
expected.modelId is truthy, so it misses cases where expected.modelId is
explicitly null; change the condition to detect the presence of the expectation
(e.g., use 'modelId' in expected or
Object.prototype.hasOwnProperty.call(expected, "modelId")) and then push an
issue if actualModelId !== expected.modelId (so null vs non-null mismatches are
reported). Update the block referencing label, expected, actualModelId and
issues accordingly.
- Around line 1001-1005: The human summary in printHumanSummary currently marks
the audit "ok" solely when failed.length === 0, causing empty runs to appear
successful; update printHumanSummary to use the same success predicate as main
by treating the run as OK only when there are models, cases, and results
exercised and no failures (e.g., compute ok = models.length > 0 && cases.length
> 0 && results.length > 0 && failed.length === 0) and print "ok" or "failed"
based on that predicate instead of just failed.length.

In `@apps/desktop/src/main/services/chat/cursorModelsDiscovery.ts`:
- Around line 246-285: The function fetchCursorModelsFromSdk currently treats
any successful REST fallback as a full success and calls
recordCursorModelDiscoverySuccess(rows), which promotes provider runtime health
even if the packaged SDK (Cursor.models.list) failed; change the logic so that
only a successful SDK call (i.e., when sdkError is null and the SDK returned
sdkRows) triggers the runtime-ready/reporting path
(recordCursorModelDiscoverySuccess / reportProviderRuntimeReady("cursor")),
while still returning/using apiRows from fetchCursorModelsFromOfficialApi for
catalog population and caching behavior; update related state changes
(sdkCached, sdkLastFailure) and the placement of
recordCursorModelDiscoverySuccess so they only run when the SDK call succeeded
(reference symbols: fetchCursorModelsFromSdk, Cursor.models.list,
fetchCursorModelsFromOfficialApi, sdkError, sdkRows, apiRows, sdkCached,
sdkLastFailure, sdkCacheGeneration, recordCursorModelDiscoverySuccess,
reportProviderRuntimeReady).

In `@apps/desktop/src/main/services/chat/cursorSdkPool.ts`:
- Around line 442-459: cleanupCursorSdkStateRoot currently only removes the
per-call stateRoot, leaving behind the per-request cache/socket directories and
leaking filesystem entries; update the pool entry stored in pools.set to include
the per-request cacheRoot (and any socket dir if separately tracked), then
change cleanupCursorSdkStateRoot(entry: { stateRoot: string; cacheRoot?: string;
cleanupStateRoot: boolean }) to, when cleanupStateRoot is true, remove both
entry.stateRoot and entry.cacheRoot (using fs.rmSync with { recursive: true,
force: true }) inside the try/catch so failures remain best-effort; ensure
pools.set(...) sets cacheRoot when creating one-shot pools so cleanup has the
value to delete.

---

Outside diff comments:
In `@apps/desktop/src/renderer/components/settings/ProvidersSection.tsx`:
- Around line 451-473: The code clears verifyingProvider early in the success
path which re-enables UI before refreshStatus completes; in verifyApiKey remove
the in-try call to setVerifyingProvider(null) so the finally block is the single
place that resets it, and keep the await refreshStatus({ force: true,
refreshOpenCodeInventory: true }) before the finally resets; apply the same
change to the sibling function block around lines 475-503 (the other
verification handler) so both verification paths rely on the finally to clear
setVerifyingProvider.

---

Nitpick comments:
In `@apps/desktop/src/main/services/opencode/openCodeRuntime.test.ts`:
- Around line 133-147: Test currently verifies null title path; add a second
assertion that a whitespace-only title also collapses and is omitted: call
startOpenCodeSession with title: "   " (or similar whitespace) and the same
other args, then assert mockState.createSession was called with
expect.objectContaining({ body: {} }) to ensure trimToUndefined behavior is
exercised for whitespace-only strings; locate the test block using
startOpenCodeSession and mockState.createSession to add this case.

In `@apps/desktop/src/renderer/components/chat/ChatIosSimulatorPanel.test.tsx`:
- Line 366: The 700ms hard wait in the test is too short and can be flaky;
update the test in ChatIosSimulatorPanel.test.tsx to either switch to
deterministic timers by calling vi.useFakeTimers()/vi.runAllTimers() around the
async debounce/timeout behavior (and restore timers after), or simply increase
the await delay to match the longest peer wait (e.g., 1600–2500ms) so the
negative assertion after the Promise settles is not spuriously passing; locate
the Promise await (await new Promise((resolve) => setTimeout(resolve, 700));)
and apply one of these fixes to ensure stability.
🪄 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: 10be0198-5fa4-4e18-af56-055df8327b1a

📥 Commits

Reviewing files that changed from the base of the PR and between f7c957e and bb9a590.

⛔ Files ignored due to path filters (9)
  • changelog/v1.0.11.mdx is excluded by !changelog/**
  • changelog/v1.0.19.mdx is excluded by !changelog/**
  • changelog/v1.1.7.mdx is excluded by !changelog/**
  • changelog/v1.1.8.mdx is excluded by !changelog/**
  • docs/ARCHITECTURE.md is excluded by !docs/**
  • docs/features/chat/README.md is excluded by !docs/**
  • docs/features/ios-simulator/README.md is excluded by !docs/**
  • docs/features/memory/compaction.md is excluded by !docs/**
  • docs/features/terminals-and-sessions/pty-and-processes.md is excluded by !docs/**
📒 Files selected for processing (70)
  • ai-tools/cursor.mdx
  • ai-tools/windsurf.mdx
  • apps/desktop/native/ios-sim-helpers/README.md
  • apps/desktop/native/ios-sim-helpers/build.sh
  • apps/desktop/package.json
  • apps/desktop/scripts/after-pack-runtime-fixes.cjs
  • apps/desktop/scripts/audit-chat-model-runtime.mjs
  • apps/desktop/scripts/dev.cjs
  • apps/desktop/scripts/validate-mac-artifacts.mjs
  • apps/desktop/src/main/main.ts
  • apps/desktop/src/main/services/ai/aiIntegrationService.test.ts
  • apps/desktop/src/main/services/ai/aiIntegrationService.ts
  • apps/desktop/src/main/services/ai/apiKeyStore.test.ts
  • apps/desktop/src/main/services/ai/apiKeyStore.ts
  • apps/desktop/src/main/services/ai/authDetector.ts
  • apps/desktop/src/main/services/ai/providerConnectionStatus.test.ts
  • apps/desktop/src/main/services/ai/providerConnectionStatus.ts
  • apps/desktop/src/main/services/ai/providerRuntimeHealth.ts
  • apps/desktop/src/main/services/ai/tools/systemPrompt.test.ts
  • apps/desktop/src/main/services/ai/tools/systemPrompt.ts
  • apps/desktop/src/main/services/chat/agentChatService.test.ts
  • apps/desktop/src/main/services/chat/agentChatService.ts
  • apps/desktop/src/main/services/chat/cursorModelsDiscovery.test.ts
  • apps/desktop/src/main/services/chat/cursorModelsDiscovery.ts
  • apps/desktop/src/main/services/chat/cursorSdkHooks.test.ts
  • apps/desktop/src/main/services/chat/cursorSdkHooks.ts
  • apps/desktop/src/main/services/chat/cursorSdkPolicy.test.ts
  • apps/desktop/src/main/services/chat/cursorSdkPolicy.ts
  • apps/desktop/src/main/services/chat/cursorSdkPool.test.ts
  • apps/desktop/src/main/services/chat/cursorSdkPool.ts
  • apps/desktop/src/main/services/chat/cursorSdkProtocol.ts
  • apps/desktop/src/main/services/chat/cursorSdkWorker.ts
  • apps/desktop/src/main/services/ios/iosSimulatorService.test.ts
  • apps/desktop/src/main/services/ios/iosSimulatorService.ts
  • apps/desktop/src/main/services/ipc/registerIpc.ts
  • apps/desktop/src/main/services/opencode/openCodeRuntime.test.ts
  • apps/desktop/src/main/services/opencode/openCodeRuntime.ts
  • apps/desktop/src/main/services/pty/ptyService.test.ts
  • apps/desktop/src/main/services/pty/ptyService.ts
  • apps/desktop/src/main/services/sync/syncRemoteCommandService.test.ts
  • apps/desktop/src/main/services/sync/syncRemoteCommandService.ts
  • apps/desktop/src/preload/preload.test.ts
  • apps/desktop/src/preload/preload.ts
  • apps/desktop/src/renderer/components/chat/AgentChatPane.tsx
  • apps/desktop/src/renderer/components/chat/ChatIosSimulatorPanel.test.tsx
  • apps/desktop/src/renderer/components/chat/ChatIosSimulatorPanel.tsx
  • apps/desktop/src/renderer/components/settings/ProvidersSection.test.tsx
  • apps/desktop/src/renderer/components/settings/ProvidersSection.tsx
  • apps/desktop/src/renderer/components/shared/providerModelSelectorGrouping.ts
  • apps/desktop/src/renderer/components/terminals/SessionCard.tsx
  • apps/desktop/src/renderer/components/terminals/SessionListPane.test.tsx
  • apps/desktop/src/renderer/lib/sessions.ts
  • apps/desktop/src/shared/modelCatalog.ts
  • apps/desktop/src/shared/modelProfiles.test.ts
  • apps/desktop/src/shared/modelProfiles.ts
  • apps/desktop/src/shared/modelRegistry.test.ts
  • apps/desktop/src/shared/modelRegistry.ts
  • apps/desktop/src/shared/types/chat.ts
  • apps/desktop/src/shared/types/config.ts
  • apps/desktop/src/shared/types/sync.ts
  • apps/ios/ADE/Models/RemoteModels.swift
  • apps/ios/ADE/Services/SyncService.swift
  • apps/ios/ADE/Views/Work/WorkChatSessionView.swift
  • apps/ios/ADE/Views/Work/WorkModelCatalog.swift
  • apps/ios/ADE/Views/Work/WorkModelPickerSheet.swift
  • apps/ios/ADE/Views/Work/WorkNewChatSheet.swift
  • chat/overview.mdx
  • getting-started/connect-provider.mdx
  • getting-started/open-project.mdx
  • plans/cursor-sdk-chat-integration.md
💤 Files with no reviewable changes (1)
  • apps/desktop/src/shared/modelProfiles.ts
✅ Files skipped from review due to trivial changes (23)
  • apps/desktop/src/renderer/components/terminals/SessionCard.tsx
  • apps/desktop/src/shared/types/config.ts
  • apps/desktop/src/main/services/ai/tools/systemPrompt.ts
  • ai-tools/windsurf.mdx
  • apps/desktop/src/renderer/components/settings/ProvidersSection.test.tsx
  • apps/desktop/src/main/services/chat/cursorSdkPool.test.ts
  • getting-started/connect-provider.mdx
  • chat/overview.mdx
  • apps/desktop/src/main/main.ts
  • apps/desktop/src/main/services/sync/syncRemoteCommandService.ts
  • apps/desktop/src/renderer/components/shared/providerModelSelectorGrouping.ts
  • ai-tools/cursor.mdx
  • apps/desktop/scripts/after-pack-runtime-fixes.cjs
  • apps/ios/ADE/Views/Work/WorkNewChatSheet.swift
  • apps/desktop/src/shared/types/sync.ts
  • apps/desktop/src/main/services/sync/syncRemoteCommandService.test.ts
  • apps/desktop/src/main/services/opencode/openCodeRuntime.ts
  • apps/desktop/src/shared/modelRegistry.ts
  • apps/desktop/src/main/services/ai/providerConnectionStatus.ts
  • apps/desktop/src/renderer/components/chat/AgentChatPane.tsx
  • apps/desktop/native/ios-sim-helpers/README.md
  • apps/desktop/src/renderer/components/chat/ChatIosSimulatorPanel.tsx
  • plans/cursor-sdk-chat-integration.md
🚧 Files skipped from review as they are similar to previous changes (26)
  • getting-started/open-project.mdx
  • apps/desktop/src/renderer/components/terminals/SessionListPane.test.tsx
  • apps/desktop/src/renderer/lib/sessions.ts
  • apps/desktop/src/shared/modelProfiles.test.ts
  • apps/desktop/src/preload/preload.test.ts
  • apps/desktop/src/main/services/chat/cursorSdkProtocol.ts
  • apps/desktop/src/main/services/ai/authDetector.ts
  • apps/desktop/src/shared/types/chat.ts
  • apps/desktop/src/main/services/ipc/registerIpc.ts
  • apps/ios/ADE/Services/SyncService.swift
  • apps/desktop/scripts/validate-mac-artifacts.mjs
  • apps/desktop/src/main/services/pty/ptyService.test.ts
  • apps/desktop/src/preload/preload.ts
  • apps/desktop/src/main/services/ios/iosSimulatorService.test.ts
  • apps/desktop/src/main/services/ai/apiKeyStore.ts
  • apps/desktop/src/main/services/chat/cursorSdkPolicy.ts
  • apps/desktop/native/ios-sim-helpers/build.sh
  • apps/desktop/src/shared/modelRegistry.test.ts
  • apps/desktop/src/main/services/ai/providerRuntimeHealth.ts
  • apps/desktop/src/main/services/ios/iosSimulatorService.ts
  • apps/ios/ADE/Models/RemoteModels.swift
  • apps/ios/ADE/Views/Work/WorkModelPickerSheet.swift
  • apps/desktop/src/shared/modelCatalog.ts
  • apps/desktop/src/main/services/pty/ptyService.ts
  • apps/desktop/src/main/services/chat/agentChatService.test.ts
  • apps/ios/ADE/Views/Work/WorkModelCatalog.swift

Comment thread apps/desktop/scripts/audit-chat-model-runtime.mjs Outdated
Comment thread apps/desktop/scripts/audit-chat-model-runtime.mjs Outdated
Comment thread apps/desktop/src/main/services/chat/cursorModelsDiscovery.ts
Comment thread apps/desktop/src/main/services/chat/cursorSdkPool.ts
@arul28
Copy link
Copy Markdown
Owner Author

arul28 commented May 2, 2026

@copilot review but do not make fixes

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.

2 participants