Skip to content

Droid Updates#319

Merged
arul28 merged 14 commits into
mainfrom
ade/droid-updates-99a3f743
May 18, 2026
Merged

Droid Updates#319
arul28 merged 14 commits into
mainfrom
ade/droid-updates-99a3f743

Conversation

@arul28

@arul28 arul28 commented May 18, 2026

Copy link
Copy Markdown
Owner

Summary

Describe the change.

What Changed

Key files and behaviors.

Validation

How you tested.

Risks

Anything to watch.

Summary by CodeRabbit

Release Notes

  • New Features
    • Model picker for browsing and selecting models with favorites and recent usage tracking
    • Added support for Ollama and LM Studio local model providers
    • Enhanced model catalog with reasoning tiers and service tier information
    • Improved model discovery with intelligent caching and refresh controls
    • Cross-surface model preferences persistence for consistent defaults

Review Change Stack

Greptile Summary

This PR migrates the Factory Droid runtime from the ACP (Agent Client Protocol) to the Factory Droid SDK, adds a full model picker UI (favorites, recents, per-surface defaults, provider rail), extends reasoning-tier and service-tier metadata to Cursor and Droid model discovery, and introduces Ollama and LM Studio as local provider options.

  • Droid ACP → SDK: droidAcpPool.ts is deleted; replaced by droidSdkPool.ts (ref-counted fork pool) + droidSdkWorker.ts (forked child process wrapping @factory/droid-sdk). Session persistence migrates from acpSessionId to droidSdkSessionId with a backward-compat fallback reader.
  • Model picker: New ModelPicker component with Zustand stores for favorites/recents, per-provider TTL catalog cache (runtimeCatalogCache.ts), and a runtime catalog that merges the static MODEL_REGISTRY with live IPC-fetched data.
  • Fast mode / reasoning: codexFastMode and reasoningEffort are now applied across opencode, cursor, and droid providers, not just codex.

Confidence Score: 5/5

Large but well-structured migration; no critical breakage found. The three findings are all non-blocking cleanup or future-proofing items.

The Droid ACP-to-SDK migration, model picker, and provider expansion are all implemented with appropriate guards, backward-compat session ID reading, and test reset hooks. No blocking logic errors were found in the changed paths.

The module-level runtimeCatalogDescriptorsById in modelCatalog.ts is worth a second look for local providers (Ollama/LM Studio) where the model list can shrink between refreshes.

Important Files Changed

Filename Overview
apps/desktop/src/main/services/chat/agentChatService.ts Migrates Droid runtime from ACP protocol to Factory SDK; adds SDK session ID persistence, plan/spec interaction mode wiring, and extends fast mode + reasoning effort to all provider families. Large refactor but well-structured.
apps/desktop/src/main/services/chat/droidSdkPool.ts New IPC-style fork pool for the Factory Droid SDK worker with ref-counted lifecycle management. sanitizeEnv is already flagged in a prior thread.
apps/desktop/src/main/services/chat/droidSdkWorker.ts Forked worker that wraps @factory/droid-sdk; handles permission and ask-user flows via bidirectional IPC with proper abort and cleanup.
apps/desktop/src/main/services/chat/droidModelsDiscovery.ts Switches model discovery from CLI help-text parsing to SDK session init; extracts reasoning tiers and service tiers from SDK model metadata. Static fallback removal already flagged in prior thread.
apps/desktop/src/main/services/chat/cursorModelsDiscovery.ts Enriches Cursor model rows with parameter definitions, variants, reasoning tiers, and service tiers; removes static fallback rows. knownAuthFailure is computed but unused.
apps/desktop/src/renderer/components/shared/ModelPicker/modelCatalog.ts New model catalog utilities that merge static registry with runtime catalog; module-level runtimeCatalogDescriptorsById is never pruned across refreshes.
apps/desktop/src/renderer/components/shared/ModelPicker/runtimeCatalogCache.ts New per-provider TTL cache for the runtime model catalog with shared singleton and test-reset helpers.
apps/desktop/src/renderer/components/shared/ModelPicker/ModelPicker.tsx New model picker component with provider rail, favorites, recents, and per-surface defaults; deduplicates in-flight catalog requests correctly.
apps/desktop/src/main/services/ipc/registerIpc.ts Adds IPC handlers for model catalog and OpenCode binary check. arg as never cast in the catalog handler bypasses type-level validation.
apps/desktop/src/renderer/components/shared/ModelPicker/useModelFavorites.ts Zustand store for model favorites with localStorage hot cache and RPC sync; optimistic update pattern is correct.
apps/desktop/src/renderer/components/shared/ModelPicker/useModelRecents.ts Zustand store for recent models with capped list (MAX_RECENTS=10) and localStorage cache; mirrors favorites pattern.

Sequence Diagram

sequenceDiagram
    participant Renderer as Renderer (ModelPicker)
    participant IPC as IPC Bridge
    participant AgentChat as AgentChatService
    participant Pool as DroidSdkPool
    participant Worker as DroidSdkWorker (fork)
    participant SDK as @factory/droid-sdk

    Renderer->>IPC: "agentChatModelCatalog({mode, refreshProvider})"
    IPC->>AgentChat: getModelCatalog(args)
    AgentChat->>SDK: createSession (model discovery)
    SDK-->>AgentChat: initResult (availableModels)
    AgentChat-->>IPC: AgentChatModelCatalog
    IPC-->>Renderer: catalog (descriptors, tiers, availability)
    Renderer->>Renderer: rememberRuntimeCatalog / runtimeCatalogDescriptorsById

    note over Renderer,SDK: On chat start (ensureDroidRuntime)
    AgentChat->>Pool: acquireDroidSdkConnection(poolKey, settings)
    Pool->>Worker: fork(droidSdkWorker.cjs)
    Worker->>SDK: createSession(execPath, modelId, autonomyLevel)
    SDK-->>Worker: session ready + sessionId
    Worker-->>Pool: DroidSdkReady
    Pool-->>AgentChat: DroidSdkPooled

    note over AgentChat,SDK: During turn
    AgentChat->>Worker: sendPrompt(text, attachments)
    Worker->>SDK: session.prompt(...)
    SDK-->>Worker: sdk_event stream
    Worker-->>AgentChat: sdk_event (via IPC)
    AgentChat->>AgentChat: mapDroidSdkMessageToChatEvents(state, event)

    note over AgentChat,Worker: On permission request
    SDK-->>Worker: requestPermission callback
    Worker-->>AgentChat: permission_request (IPC)
    AgentChat-->>Renderer: tool approval UI
    Renderer-->>AgentChat: DroidSdkPermissionDecision
    AgentChat-->>Worker: permission_response (IPC)
    Worker-->>SDK: resolve permission
Loading

Comments Outside Diff (1)

  1. apps/desktop/src/main/services/chat/agentChatService.ts, line 757 (link)

    P2 eventMapperState is never reset between turns

    createDroidSdkEventMapperState() is called once when DroidRuntime is constructed and the same instance is passed to mapDroidSdkMessageToChatEvents on every event for the lifetime of that runtime. The assistantDeltaItemIds and thinkingDeltaItemIds sets and toolNamesByUseId map grow across all turns in the session. If the SDK reuses a messageId or block index across turns (e.g., the same messageId:text:0 key appears in a later turn's create_message), the deduplication guard will silently suppress valid text events in the new turn. At minimum, resetting the state at the start of each new turn prevents cross-turn ID collisions and bounds memory growth for long sessions.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: apps/desktop/src/main/services/chat/agentChatService.ts
    Line: 757
    
    Comment:
    **`eventMapperState` is never reset between turns**
    
    `createDroidSdkEventMapperState()` is called once when `DroidRuntime` is constructed and the same instance is passed to `mapDroidSdkMessageToChatEvents` on every event for the lifetime of that runtime. The `assistantDeltaItemIds` and `thinkingDeltaItemIds` sets and `toolNamesByUseId` map grow across all turns in the session. If the SDK reuses a `messageId` or block index across turns (e.g., the same `messageId:text:0` key appears in a later turn's `create_message`), the deduplication guard will silently suppress valid text events in the new turn. At minimum, resetting the state at the start of each new turn prevents cross-turn ID collisions and bounds memory growth for long sessions.
    
    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/chat/cursorModelsDiscovery.ts:741-743
**Dead `knownAuthFailure` variable**

`knownAuthFailure` is computed from `result?.failureKind` and `recentFailure?.kind`, but is then suppressed with `void` and never read. Its former role—determining whether to apply the hardcoded fallback rows—has been removed entirely. The variable and its computation can be deleted alongside the `recentFailure` binding; keeping them suggests the auth-failure check still matters when it doesn't.

### Issue 2 of 3
apps/desktop/src/main/services/ipc/registerIpc.ts:6696-6699
**`arg as never` bypasses type safety**

Casting `arg` to `never` before passing it to `getModelCatalog` silences TypeScript rather than asserting the actual expected type. `never` is the bottom type—assignable to anything—so any shape mismatch between what the renderer sends and what `getModelCatalog` expects will go undetected at compile time. Prefer `arg as AgentChatModelCatalogArgs` (imported from `"../../../shared/types/chat"`) so the compiler validates the contract.

### Issue 3 of 3
apps/desktop/src/renderer/components/shared/ModelPicker/modelCatalog.ts:200-215
**Module-level descriptor cache grows without bound across refreshes**

`runtimeCatalogDescriptorsById` is a module-level `Map` populated by `descriptorsFromAgentChatModelCatalog` on every catalog load, but entries are never removed. For dynamic providers like Ollama and LM Studio—where the model list changes as local servers start and stop—a model that disappears from the server will stay in this map indefinitely. Subsequent calls to `resolveModelDescriptorWithRuntimeCatalog` will return the stale descriptor, and the picker may display models that no longer exist. Clearing (or selectively pruning) the map at the start of each catalog load would bound its size and keep it accurate.

Reviews (2): Last reviewed commit: "ship: iter 1 — fix test-desktop(8) timeo..." | Re-trigger Greptile

@vercel

vercel Bot commented May 18, 2026

Copy link
Copy Markdown

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 18, 2026 10:14pm

@arul28

arul28 commented May 18, 2026

Copy link
Copy Markdown
Owner Author

@copilot review but do not make fixes

@coderabbitai

coderabbitai Bot commented May 18, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Adds a persistent cross-surface model picker (favorites/recents), shared runtime model catalog with refresh/TTL, new picker UIs for CLI/Desktop/iOS, and migrates Droid runtime from ACP to a new Droid SDK (protocol, worker, pool, service refactors). Includes provider auth gating and tests.

Changes

Cross-surface Model Picker + Droid SDK

Layer / File(s) Summary
End-to-end implementation and wiring
apps/ade-cli/*, apps/desktop/*, apps/ios/*
Implements model picker storage/RPC/sync, TUI/desktop/iOS picker UIs and hooks, runtime catalog cache/refresh, OpenCode gating, and full Droid SDK integration (protocol/worker/pool and service refactors) with comprehensive tests.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Suggested labels

desktop, ios

✨ 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/droid-updates-99a3f743

@capy-ai

capy-ai Bot commented May 18, 2026

Copy link
Copy Markdown

Capy auto-review is paused for this organization because the monthly auto-review limit has been reached. Increase the limit or turn it off in billing settings to resume automatic reviews.

Copilot AI commented May 18, 2026

Copy link
Copy Markdown
Contributor

@copilot review but do not make fixes

Reviewed at 0682bbc without applying fixes. No blocking issues found in the last 14 commits. One non-blocking note: useProviderAuthStatus.ts has a stale comment describing a binary.present fallback, but the current logic only treats auth.ready/runtimeAvailable as Claude-ok.

Copilot finished work on behalf of arul28 May 18, 2026 21:27

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 19

Caution

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

⚠️ Outside diff range comments (3)
apps/desktop/src/renderer/components/chat/AgentChatPane.tsx (2)

6417-6422: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Explicitly clear fast mode when the next model does not support it.

This now drops codexFastMode from the update payload entirely. Because updateSession is patch-like elsewhere in this file, switching from a fast-mode-capable Codex model to one that does not support it can leave the old codexFastMode=true latched on the session while the toggle disappears from the UI.

Suggested fix
               void window.ade.agentChat.updateSession({
                 sessionId: selectedSessionId,
                 modelId: nextModelId,
                 reasoningEffort: snapshot.nextReasoningEffort,
-                ...(modelSupportsFastMode(snapshot.nextDesc) ? { codexFastMode } : {}),
+                ...(snapshot.nextProvider === "codex"
+                  ? { codexFastMode: modelSupportsFastMode(snapshot.nextDesc) ? codexFastMode : false }
+                  : {}),
                 ...nextNativeControlPayload,
               }).then((updatedSession) => {

Mirror the same explicit reset in the pre-send update at Line 5104 so the stale flag cannot survive that path either.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src/renderer/components/chat/AgentChatPane.tsx` around lines
6417 - 6422, When calling window.ade.agentChat.updateSession with the new model
(the patch that builds the payload using selectedSessionId, nextModelId,
snapshot.nextReasoningEffort, modelSupportsFastMode(snapshot.nextDesc) ? {
codexFastMode } : {}, and ...nextNativeControlPayload), explicitly set
codexFastMode: false when modelSupportsFastMode(snapshot.nextDesc) is false so
the old fast-mode flag cannot remain latched; mirror the same explicit reset in
the pre-send update path (the other updateSession call used before sending
messages) so both paths clear codexFastMode when the new model does not support
fast mode.

6627-6650: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use the runtime-catalog resolver when parallel slots are actually launched.

This setter now accepts runtime-catalog-only models, but the parallel preview/launch path still resolves slot.modelId with plain getModelById (Line 2439, Line 2461, Lines 4722-4743, and Lines 4768-4779). A dynamic Cursor/Droid model can therefore look valid in the picker and then be launched with the wrong provider metadata.

Suggested direction
-              const desc = resolveModelDescriptorWithRuntimeCatalog(nextModelId) ?? getModelById(nextModelId);
+              const desc = resolveModelDescriptorWithRuntimeCatalog(nextModelId) ?? getModelById(nextModelId);
               const tiers = desc?.reasoningTiers ?? [];

Use the same resolveModelDescriptorWithRuntimeCatalog(...) ?? getModelById(...) pattern in the slot preview and submit-time slot.modelId lookups as well, so the configured model and launched model stay in sync.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src/renderer/components/chat/AgentChatPane.tsx` around lines
6627 - 6650, The preview/launch code still uses getModelById for slot.modelId
lookups which can mis-resolve runtime-catalog-only models; update the slot
preview and submit/launch paths to use
resolveModelDescriptorWithRuntimeCatalog(slot.modelId) ??
getModelById(slot.modelId) wherever slot.modelId is resolved (e.g., the
preview/render code and the submit/launch handlers that currently call
getModelById), so that functions like selectReasoningEffort,
getExecutionModeOptions, recommendedOpenCodePermissionModeForModel, and
shouldResetOpenCodePermissionForModel operate on the descriptor returned from
resolveModelDescriptorWithRuntimeCatalog when available and keep configured vs
launched provider metadata in sync.
apps/ios/ADE/Models/RemoteModels.swift (1)

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

Consider making isAvailable optional for defensive backward compatibility.

AgentChatModelCatalogModel uses synthesized Codable with a non-optional Bool for isAvailable. If older host versions omit this key in payloads, the entire catalog will fail to decode. While all current desktop payloads include isAvailable, making it optional—following the pattern used elsewhere in RemoteModels.swift for defensive decoding—would safeguard against unexpected older host versions.

💡 Suggested adjustment
-  var isAvailable: Bool
+  var isAvailable: Bool?
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/ios/ADE/Models/RemoteModels.swift` around lines 1900 - 1924, The model's
synthesized Codable will fail if the JSON omits isAvailable; change the
AgentChatModelCatalogModel property isAvailable from Bool to Bool? to allow
missing keys during decoding, and update any consumers of
AgentChatModelCatalogModel.isAvailable to handle nil (e.g., treat nil as false
or provide a safe default) so runtime code doesn't force-unwrap or assume
non-optional state; locate the struct AgentChatModelCatalogModel and adjust
usages accordingly (comparisons, UI bindings, filtering) to handle the optional
value.
🧹 Nitpick comments (2)
apps/ade-cli/src/services/modelPickerStore.ts (1)

5-7: ⚡ Quick win

Rename new constants to camelCase to match repo naming rules.

The newly introduced constant identifiers use uppercase snake case; please rename them (including the exported one) to camelCase for guideline compliance.

As per coding guidelines, **/*.{ts,tsx,js,jsx}: Use camelCase for variable names in TypeScript/JavaScript files.

Also applies to: 139-139

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/ade-cli/src/services/modelPickerStore.ts` around lines 5 - 7, The three
newly added constants use UPPER_SNAKE_CASE and must be renamed to camelCase:
change MAX_RECENTS → maxRecents, STORE_VERSION → storeVersion, and
PERSIST_DEBOUNCE_MS → persistDebounceMs, update any export that exposes them
(including the exported one around the other occurrence noted), and update every
reference/usage (reads, tests, imports/exports) to use the new names; keep the
same values and types and run a quick grep/IDE refactor to ensure no lingering
references remain.
apps/desktop/src/renderer/components/chat/AgentChatPane.submit.test.tsx (1)

783-787: ⚡ Quick win

Extract repeated model-picker interaction steps into a helper.

The same pointerDownclick trigger → click provider-tab sequence is duplicated across many tests. A small helper will reduce drift and future flake-prone edits.

♻️ Proposed refactor
 async function clickEnabledModelOption(name: RegExp | string) {
   const options = await screen.findAllByRole("option", { name });
   const enabledOption = options.find((option) => option.getAttribute("aria-disabled") !== "true");
   expect(enabledOption).toBeTruthy();
   fireEvent.click(enabledOption!);
 }
+
+async function openModelPickerAndSelectProviderTab(providerTabName: RegExp | string) {
+  const trigger = await screen.findByRole("button", { name: /^Select model/ });
+  fireEvent.pointerDown(trigger, { button: 0 });
+  fireEvent.click(trigger);
+  fireEvent.click(await screen.findByRole("tab", { name: providerTabName }));
+}

Then replace repeated blocks with:

-const modelTrigger = await screen.findByRole("button", { name: /^Select model/ });
-fireEvent.pointerDown(modelTrigger, { button: 0 });
-fireEvent.click(modelTrigger);
-fireEvent.click(await screen.findByRole("tab", { name: /^Anthropic$/i }));
+await openModelPickerAndSelectProviderTab(/^Anthropic$/i);
As per coding guidelines, "apps/desktop/src/**: Electron desktop app — check for IPC security, proper main/renderer process separation, and React best practices."

Also applies to: 1796-1805, 1853-1862, 2107-2113, 2148-2154, 2190-2195, 2245-2250, 2293-2299, 2336-2342, 2367-2373, 2414-2420, 2486-2492, 2972-2977, 2982-2987, 3140-3151

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src/renderer/components/chat/AgentChatPane.submit.test.tsx`
around lines 783 - 787, Tests duplicate the model-picker interaction
(pointerDown → click trigger → click provider tab → click model) across many
cases; extract this into a helper like selectModelOption(providerPattern,
modelPattern) used by AgentChatPane.submit tests. Implement a new utility
function in the test file (or a shared test-utils) that takes a provider regex
(e.g. /Anthropic/i) and a model regex (e.g. /Claude Sonnet 4\.6/i), performs
await screen.findByRole("button", { name: /^Select model/ }),
fireEvent.pointerDown(...), fireEvent.click(...) on the trigger, clicks the
provider tab via screen.findByRole("tab", { name: providerPattern }), then calls
the existing clickEnabledModelOption(modelPattern); replace each repeated block
(instances referenced in the comment) with calls to this helper to reduce
duplication and flakiness.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/ade-cli/src/services/modelPickerStore.ts`:
- Around line 153-155: Call and await any pending flush on the shared store
before nulling the singleton to avoid lost debents across tests: update
resetSharedModelPickerStoreForTests to first detect sharedStoreInstance and
invoke its flush method (e.g. flushPendingWrites(), flushDebouncedWrites(), or
flush()) and await the result if it returns a Promise, then set
sharedStoreInstance = null; you may need to make
resetSharedModelPickerStoreForTests async to accommodate awaiting the flush.

In `@apps/ade-cli/src/tuiClient/app.tsx`:
- Around line 3362-3401: openModelPicker currently opens the model picker via
setRightPane/setRightOpen but does not mark it as user-sticky, so subsequent
context-default effects can immediately replace it; modify openModelPicker to
set the sticky marker (e.g. include a flag like sticky: true or call
markRightPaneSticky) when calling setRightPane, and ensure the corresponding
commit and Escape-close handlers (the commit path and the Escape/close handlers
referenced around the other ranges) clear that sticky marker (e.g. remove the
flag or call unmarkRightPaneSticky) so the pane is only sticky while explicitly
opened by the user; update usages in openModelPicker and the commit/Escape close
flows to toggle this sticky state alongside setRightPane/setRightOpen.
- Around line 5653-5684: The bug is that when getModelById(descriptor) is
undefined for catalog-only models, the code falls back to modelState.provider
and keeps the old runtime; instead, locate the provider that produced
catalogModel (the provider object in the loop that found catalogModel or the
current picker selection) and use that to compute provider =
normalizeProvider(resolveProviderGroupForModel(...)) (or a normalizeProvider
call that accepts the catalog provider info) before calling
modelStatePatchForModel; update the fallback logic so provider is derived from
catalogModel's provider/subsection/provider.id (from modelCatalogRef.current
groups) instead of modelState.provider so session creation targets the correct
runtime.
- Around line 3337-3357: The effect that hydrates model picker favorites/recents
is currently keyed to socketPath and returns early if connectionRef.current is
not set, so it never retries when the RPC connection arrives; update the
useEffect dependency from [socketPath] to depend on the actual connection state
(e.g., [connection] or connectionRef.current wrapped in state) so the effect
re-runs when the connection becomes available, leaving the internal early-return
and cancel logic intact and still calling
getModelPickerFavorites/getModelPickerRecents and then
setModelPickerFavorites/setModelPickerRecents.

In `@apps/ade-cli/src/tuiClient/components/ModelPicker/modelPickerLayout.ts`:
- Around line 157-160: The code currently filters pool using
input.selection.provider which can be stale; instead first resolve the selected
rail to an existing entry (e.g. find an entry in allEntries whose family or
provider matches input.selection.provider) and if none exists, set the resolved
selection to a valid fallback (such as the favorites rail or the first available
rail) and update railIndex accordingly (referencing railIndex, favorites,
input.selection.provider); only after that derive pool = allEntries.filter(entry
=> entry.family === resolvedProvider) so headings/empty-states and selection
context stay consistent.

In `@apps/desktop/src/main/services/chat/droidSdkEventMapper.ts`:
- Around line 249-258: The done-event builds a final usage object that can
contain cacheCreationTokens while token events use cacheWriteTokens, causing
downstream inconsistency; update the mapping so the final returned usage uses
the same key as token events (cacheWriteTokens) by normalizing the usage
produced by usageFrom (or immediately after computing usageFrom(tokenUsage))—if
usage.cacheCreationTokens exists, set usage.cacheWriteTokens =
usage.cacheCreationTokens and delete usage.cacheCreationTokens before returning
the done event (touch symbols: tokenUsage, usageFrom, usage, and the returned
object in the done-event branch).
- Around line 136-144: The fallback itemId generation in the extractTextBlocks
flatMap uses a hardcoded "droid-message" prefix which doesn't match the deltas'
itemIdFor fallback ("droid-text"/"droid-thinking"), causing dedupe mismatches;
update the itemId creation in the mapping inside extractTextBlocks flatMap to
use the same logic as itemIdFor (or call itemIdFor) so that when
readString(record.messageId) is null the fallback prefix depends on block.kind
("droid-text" for text, "droid-thinking" for thinking) and produces identical
itemId strings to the delta events (ensure references: extractTextBlocks,
block.kind, itemId, meta.state.assistantDeltaItemIds / thinkingDeltaItemIds).

In `@apps/desktop/src/main/services/chat/droidSdkWorker.ts`:
- Around line 102-110: The permission waiter map currently keys entries by
request.id which can collide; update requestPermission to generate a
guaranteed-unique correlation key (e.g., crypto.randomUUID() or an incrementing
uniqueToken) and use that uniqueKey for
permissionWaiters.set/permissionWaiters.delete and include the same uniqueKey on
the IPC payload (the posted request) so the resolver map and incoming responses
correlate by this uniqueKey rather than request.id; apply the same change to the
corresponding response handler code that resolves entries from permissionWaiters
so it looks up by the uniqueKey and still cleans up the entry after resolving.
- Around line 225-257: sendPrompt is vulnerable to concurrent runs because it
uses a single global currentAbort that gets overwritten; either serialize sends
or scope aborts per-call: update sendPrompt to check and block concurrent sends
for the same session (e.g., if currentAbortForSession[session.sessionId] exists
throw an error) or replace the single currentAbort with a per-session map (e.g.,
currentAbortForSession: Record<string, AbortController>) and set
currentAbortForSession[session.sessionId] = controller before streaming from
session.stream and clear that entry in the finally block only if it still equals
the controller; ensure post-stream cleanup and cancel logic use the per-session
entry so earlier streams are not left active or interleaving events.

In `@apps/desktop/src/preload/preload.ts`:
- Around line 3117-3137: The current modelPicker fallbacks passed into
callProjectRuntimeSyncOr (modelPicker.getFavorites, setFavorites,
toggleFavorite, getRecents, pushRecent) return synthetic empty arrays and never
persist state; replace those inline fallback async handlers to delegate to the
real main-process persistent store instead (i.e., call the existing main-store
API or IPC store methods from the preload layer) so getFavorites/getRecents read
from the main store and setFavorites/pushRecent/toggleFavorite update and
persist the same main-store; keep the same return shapes but ensure each
fallback invokes the main-process store methods rather than returning hard-coded
defaults.
- Around line 1192-1194: The fallback gating only considers async action calls;
extend it so sync runtime calls also fall back: update the condition around
canUseFallback (used when deciding to callLocalProjectActionIfBound) to also
allow fallback for sync paths (callProjectRuntimeSyncOr /
callLocalProjectSyncIfBound) by treating the same "not callable" error as
eligible for fallback (or adding a corresponding
isLocalRuntimeSyncActionNotCallableError) and ensure ADE_LOCAL_RUNTIME_FALLBACK
is applied consistently for both async (callLocalProjectActionIfBound) and sync
(callLocalProjectSyncIfBound / callProjectRuntimeSyncOr) code paths, while still
validating with isSafeLocalRuntimeFallbackError.

In `@apps/desktop/src/renderer/components/chat/AgentChatPane.tsx`:
- Line 2261: selectedModelDesc is now resolved via
resolveModelDescriptorWithRuntimeCatalog but other code paths still call
getModelById(modelId) causing runtime-only models to fall back to the wrong
provider; replace those direct getModelById calls with the runtime-aware
resolver (i.e., use resolveModelDescriptorWithRuntimeCatalog(modelId) ??
getModelById(modelId)) wherever sessionProvider, composer, slash-command fetch,
or CLI launch logic references getModelById (examples: the spots around where
sessionProvider is computed and the handlers at lines you noted), and ensure
downstream code reads provider info from the resulting descriptor
(selectedModelDesc or newly resolved descriptor) instead of re-querying by
modelId.

In `@apps/desktop/src/renderer/components/shared/ModelPicker/ModelListRow.tsx`:
- Around line 177-183: The row action buttons in ModelListRow currently set
tabIndex={-1}, preventing keyboard focus; locate the interactive elements wired
to handleToggleFavorite and handleFavoriteKeyDown (and the analogous handlers
for reasoning and sign-in buttons) and remove the tabIndex={-1} (or change it to
tabIndex={0}) so the controls are focusable by keyboard, ensuring the existing
onKeyDown handlers (handleFavoriteKeyDown, the reasoning and sign-in key
handlers) properly handle Enter/Space activation and that
aria-label/aria-pressed attributes are preserved for accessibility.

In
`@apps/desktop/src/renderer/components/shared/ModelPicker/ModelPickerContent.tsx`:
- Around line 436-442: The current handleCopyId uses try/catch around
navigator.clipboard.writeText(modelId) which returns a Promise, so rejected
promises aren't caught; update handleCopyId to handle async rejections by
awaiting the Promise in an async function or attaching a .catch handler to
navigator.clipboard.writeText(modelId) and log or ignore errors there. Locate
the handleCopyId callback in ModelPickerContent.tsx and ensure clipboard
failures are handled via async/await with try/catch or via .catch to avoid
unhandled promise rejections.

In
`@apps/desktop/src/renderer/components/shared/ModelPicker/useModelFavorites.ts`:
- Around line 85-90: The hydration logic in hydrateFromRemote currently sets
hydrated and leaves the module-level hydrationStarted flag preventing retries
when getRpcApi() is temporarily unavailable; change hydrateFromRemote (and the
related block guarded by hydrationStarted) so that if getRpcApi() returns null
you do NOT set hydrated or permanently flip hydrationStarted — instead exit
early without marking hydrated (and ensure hydrationStarted remains false or is
reset), so the function can be retried later once the RPC surface is available;
conversely, only set hydrated and mark hydrationStarted true after a successful
remote reconciliation (use the existing set(...) and hydrationStarted variables
around the successful RPC calls) and apply the same change in the similar block
referenced at lines ~102-123.

In `@apps/desktop/src/renderer/components/shared/ModelPicker/useModelRecents.ts`:
- Around line 86-91: The hydration path in useModelRecents (hydrateFromRemote)
incorrectly sets hydrated = true when getRpcApi() returns null, and leaves
hydrationStarted blocking future attempts; instead, when !api is detected do NOT
set hydrated to true and reset or clear hydrationStarted so the function can be
retried later (or schedule a retry). Update hydrateFromRemote and the related
hydrationStarted/hydrated state updates so that hydrationStarted is only
set/cleared around actual attempts, and hydrated is only set true after a
successful reconcile (reference hydrateFromRemote, hydrationStarted, hydrated,
and set).

In `@apps/desktop/src/renderer/components/terminals/WorkViewArea.tsx`:
- Around line 434-474: The dropdown menu is rendered inline and gets clipped by
parent overflow; change the inline menu render (the JSX block that currently
conditionally renders the <div role="menu"...> when open) to render via a portal
instead (e.g., ReactDOM.createPortal or your app's Portal component) into
document.body and position it using the trigger/button's bounding rect (use
containerRef or the button ref to compute left/top) so the menu escapes parent
overflow; alternatively integrate a collision-aware positioning helper
(floating-ui) for flip/viewport logic — update the conditional render logic that
uses open to create a portaled menu and compute absolute screen coordinates
before rendering.

In `@apps/desktop/src/shared/modelCatalog.ts`:
- Around line 149-153: The classifyProviderGroup function currently only returns
"ollama" or "lmstudio" when providerRoute === "opencode", so direct local
descriptors (ModelDescriptor with family === "ollama" or "lmstudio" but
providerRoute === "openai-compatible") fall through; fix by adding a family
check at the top of classifyProviderGroup (check model.family === "ollama" ||
model.family === "lmstudio") to return model.family before the providerRoute ===
"opencode" branch so both OpenCode-routed and direct local models are classified
into the new local groups.

In `@apps/ios/ADE/Services/SyncService.swift`:
- Around line 3641-3644: The cachedChatModelCatalog() helper currently picks the
newest entry from chatModelCatalogCache without respecting the per-host/project
scoping encoded in chatModelsCacheKey; update cachedChatModelCatalog() (and the
fallback lookups that use it) to first filter chatModelCatalogCache entries to
only those whose cache key matches the current chatModelsCacheKey scope (or
otherwise contains the current host/project identifier) before sorting by
fetchedAt and checking chatModelsCacheTTL; use the same scoping logic wherever
cachedChatModelCatalog() is used so a catalog from a different host/project
isn’t returned (refer to chatModelCatalogCache, chatModelsCacheKey,
cached.fetchedAt and chatModelsCacheTTL to locate and fix the code).

---

Outside diff comments:
In `@apps/desktop/src/renderer/components/chat/AgentChatPane.tsx`:
- Around line 6417-6422: When calling window.ade.agentChat.updateSession with
the new model (the patch that builds the payload using selectedSessionId,
nextModelId, snapshot.nextReasoningEffort,
modelSupportsFastMode(snapshot.nextDesc) ? { codexFastMode } : {}, and
...nextNativeControlPayload), explicitly set codexFastMode: false when
modelSupportsFastMode(snapshot.nextDesc) is false so the old fast-mode flag
cannot remain latched; mirror the same explicit reset in the pre-send update
path (the other updateSession call used before sending messages) so both paths
clear codexFastMode when the new model does not support fast mode.
- Around line 6627-6650: The preview/launch code still uses getModelById for
slot.modelId lookups which can mis-resolve runtime-catalog-only models; update
the slot preview and submit/launch paths to use
resolveModelDescriptorWithRuntimeCatalog(slot.modelId) ??
getModelById(slot.modelId) wherever slot.modelId is resolved (e.g., the
preview/render code and the submit/launch handlers that currently call
getModelById), so that functions like selectReasoningEffort,
getExecutionModeOptions, recommendedOpenCodePermissionModeForModel, and
shouldResetOpenCodePermissionForModel operate on the descriptor returned from
resolveModelDescriptorWithRuntimeCatalog when available and keep configured vs
launched provider metadata in sync.

In `@apps/ios/ADE/Models/RemoteModels.swift`:
- Around line 1900-1924: The model's synthesized Codable will fail if the JSON
omits isAvailable; change the AgentChatModelCatalogModel property isAvailable
from Bool to Bool? to allow missing keys during decoding, and update any
consumers of AgentChatModelCatalogModel.isAvailable to handle nil (e.g., treat
nil as false or provide a safe default) so runtime code doesn't force-unwrap or
assume non-optional state; locate the struct AgentChatModelCatalogModel and
adjust usages accordingly (comparisons, UI bindings, filtering) to handle the
optional value.

---

Nitpick comments:
In `@apps/ade-cli/src/services/modelPickerStore.ts`:
- Around line 5-7: The three newly added constants use UPPER_SNAKE_CASE and must
be renamed to camelCase: change MAX_RECENTS → maxRecents, STORE_VERSION →
storeVersion, and PERSIST_DEBOUNCE_MS → persistDebounceMs, update any export
that exposes them (including the exported one around the other occurrence
noted), and update every reference/usage (reads, tests, imports/exports) to use
the new names; keep the same values and types and run a quick grep/IDE refactor
to ensure no lingering references remain.

In `@apps/desktop/src/renderer/components/chat/AgentChatPane.submit.test.tsx`:
- Around line 783-787: Tests duplicate the model-picker interaction (pointerDown
→ click trigger → click provider tab → click model) across many cases; extract
this into a helper like selectModelOption(providerPattern, modelPattern) used by
AgentChatPane.submit tests. Implement a new utility function in the test file
(or a shared test-utils) that takes a provider regex (e.g. /Anthropic/i) and a
model regex (e.g. /Claude Sonnet 4\.6/i), performs await
screen.findByRole("button", { name: /^Select model/ }),
fireEvent.pointerDown(...), fireEvent.click(...) on the trigger, clicks the
provider tab via screen.findByRole("tab", { name: providerPattern }), then calls
the existing clickEnabledModelOption(modelPattern); replace each repeated block
(instances referenced in the comment) with calls to this helper to reduce
duplication and flakiness.
🪄 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: d3c454ab-5337-4443-9bd9-f5bd908019e9

📥 Commits

Reviewing files that changed from the base of the PR and between 1b4815f and 0682bbc.

⛔ Files ignored due to path filters (7)
  • apps/desktop/package-lock.json is excluded by !**/package-lock.json, !**/package-lock.json
  • docs/ARCHITECTURE.md is excluded by !docs/**
  • docs/features/ade-code/README.md is excluded by !docs/**
  • docs/features/chat/README.md is excluded by !docs/**
  • docs/features/chat/agent-routing.md is excluded by !docs/**
  • docs/features/chat/composer-and-ui.md is excluded by !docs/**
  • docs/features/sync-and-multi-device/remote-commands.md is excluded by !docs/**
📒 Files selected for processing (103)
  • apps/ade-cli/src/adeRpcServer.ts
  • apps/ade-cli/src/bootstrap.ts
  • apps/ade-cli/src/services/modelPickerStore.test.ts
  • apps/ade-cli/src/services/modelPickerStore.ts
  • apps/ade-cli/src/services/sync/syncRemoteCommandService.ts
  • apps/ade-cli/src/services/sync/syncService.ts
  • apps/ade-cli/src/tuiClient/adeApi.ts
  • apps/ade-cli/src/tuiClient/app.tsx
  • apps/ade-cli/src/tuiClient/components/ModelPicker/ModelPickerPane.tsx
  • apps/ade-cli/src/tuiClient/components/ModelPicker/modelPickerLayout.test.ts
  • apps/ade-cli/src/tuiClient/components/ModelPicker/modelPickerLayout.ts
  • apps/ade-cli/src/tuiClient/components/ModelPicker/types.ts
  • apps/ade-cli/src/tuiClient/components/RightPane.tsx
  • apps/ade-cli/src/tuiClient/theme.ts
  • apps/ade-cli/src/tuiClient/types.ts
  • apps/desktop/package.json
  • apps/desktop/src/main/services/adeActions/registry.test.ts
  • apps/desktop/src/main/services/adeActions/registry.ts
  • apps/desktop/src/main/services/ai/aiIntegrationService.test.ts
  • apps/desktop/src/main/services/ai/aiIntegrationService.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/cursorSdkPool.ts
  • apps/desktop/src/main/services/chat/cursorSdkProtocol.ts
  • apps/desktop/src/main/services/chat/cursorSdkWorker.ts
  • apps/desktop/src/main/services/chat/droidAcpPool.ts
  • apps/desktop/src/main/services/chat/droidModelsDiscovery.test.ts
  • apps/desktop/src/main/services/chat/droidModelsDiscovery.ts
  • apps/desktop/src/main/services/chat/droidSdkEventMapper.ts
  • apps/desktop/src/main/services/chat/droidSdkPool.ts
  • apps/desktop/src/main/services/chat/droidSdkProtocol.ts
  • apps/desktop/src/main/services/chat/droidSdkWorker.ts
  • apps/desktop/src/main/services/ipc/registerIpc.ts
  • apps/desktop/src/main/services/ipc/runtimeBridge.ts
  • apps/desktop/src/main/services/opencode/openCodeBinaryManager.test.ts
  • apps/desktop/src/main/services/opencode/openCodeBinaryManager.ts
  • apps/desktop/src/main/services/opencode/openCodeInventory.test.ts
  • apps/desktop/src/main/services/opencode/openCodeInventory.ts
  • apps/desktop/src/main/services/sync/syncRemoteCommandService.test.ts
  • apps/desktop/src/preload/global.d.ts
  • apps/desktop/src/preload/preload.test.ts
  • apps/desktop/src/preload/preload.ts
  • apps/desktop/src/renderer/browserMock.ts
  • apps/desktop/src/renderer/components/app/FeedbackReporterModal.tsx
  • apps/desktop/src/renderer/components/chat/AgentChatComposer.tsx
  • apps/desktop/src/renderer/components/chat/AgentChatPane.submit.test.tsx
  • apps/desktop/src/renderer/components/chat/AgentChatPane.tsx
  • apps/desktop/src/renderer/components/cto/IdentityEditor.tsx
  • apps/desktop/src/renderer/components/cto/WorkerCreationWizard.tsx
  • apps/desktop/src/renderer/components/missions/ModelSelector.tsx
  • apps/desktop/src/renderer/components/prs/shared/PrResolverLaunchControls.test.tsx
  • apps/desktop/src/renderer/components/prs/shared/PrResolverLaunchControls.tsx
  • apps/desktop/src/renderer/components/settings/AiFeaturesSection.tsx
  • apps/desktop/src/renderer/components/settings/ProvidersSection.test.tsx
  • apps/desktop/src/renderer/components/settings/ProvidersSection.tsx
  • apps/desktop/src/renderer/components/shared/ModelCatalogPanel.tsx
  • apps/desktop/src/renderer/components/shared/ModelPicker/ModelListRow.tsx
  • apps/desktop/src/renderer/components/shared/ModelPicker/ModelPicker.test.tsx
  • apps/desktop/src/renderer/components/shared/ModelPicker/ModelPicker.tsx
  • apps/desktop/src/renderer/components/shared/ModelPicker/ModelPickerContent.tsx
  • apps/desktop/src/renderer/components/shared/ModelPicker/ModelPickerRail.tsx
  • apps/desktop/src/renderer/components/shared/ModelPicker/ReasoningEffortPicker.test.tsx
  • apps/desktop/src/renderer/components/shared/ModelPicker/ReasoningEffortPicker.tsx
  • apps/desktop/src/renderer/components/shared/ModelPicker/modelCatalog.test.ts
  • apps/desktop/src/renderer/components/shared/ModelPicker/modelCatalog.ts
  • apps/desktop/src/renderer/components/shared/ModelPicker/modelOrdering.test.ts
  • apps/desktop/src/renderer/components/shared/ModelPicker/modelOrdering.ts
  • apps/desktop/src/renderer/components/shared/ModelPicker/modelPickerSearch.test.ts
  • apps/desktop/src/renderer/components/shared/ModelPicker/modelPickerSearch.ts
  • apps/desktop/src/renderer/components/shared/ModelPicker/providerEmptyState.test.tsx
  • apps/desktop/src/renderer/components/shared/ModelPicker/providerEmptyState.tsx
  • apps/desktop/src/renderer/components/shared/ModelPicker/runtimeCatalogCache.ts
  • apps/desktop/src/renderer/components/shared/ModelPicker/useAuthOnlyFilter.ts
  • apps/desktop/src/renderer/components/shared/ModelPicker/useModelFavorites.ts
  • apps/desktop/src/renderer/components/shared/ModelPicker/useModelRecents.ts
  • apps/desktop/src/renderer/components/shared/ModelPicker/usePerSurfaceModelDefaults.ts
  • apps/desktop/src/renderer/components/shared/ModelPicker/useProviderAuthStatus.test.ts
  • apps/desktop/src/renderer/components/shared/ModelPicker/useProviderAuthStatus.ts
  • apps/desktop/src/renderer/components/shared/ModelPicker/useReasoningByFamily.ts
  • apps/desktop/src/renderer/components/shared/ProviderLogos.tsx
  • apps/desktop/src/renderer/components/shared/ProviderModelSelector.tsx
  • apps/desktop/src/renderer/components/shared/ReviewLaunchModelControls.tsx
  • apps/desktop/src/renderer/components/terminals/WorkViewArea.test.tsx
  • apps/desktop/src/renderer/components/terminals/WorkViewArea.tsx
  • apps/desktop/src/shared/ipc.ts
  • apps/desktop/src/shared/modelCatalog.ts
  • apps/desktop/src/shared/modelRegistry.ts
  • apps/desktop/src/shared/types/chat.ts
  • apps/desktop/src/shared/types/sync.ts
  • apps/desktop/tsup.config.ts
  • apps/ios/ADE/Models/RemoteModels.swift
  • apps/ios/ADE/Services/SyncService.swift
  • apps/ios/ADE/Views/Work/WorkModelCatalog.swift
  • apps/ios/ADE/Views/Work/WorkModelPickerSheet.swift
  • apps/ios/ADE/Views/Work/WorkPreviews.swift
  • apps/ios/ADE/Views/Work/WorkSessionSettingsSheet+Actions.swift
  • apps/ios/ADE/Views/Work/WorkSessionSettingsSheet.swift
  • apps/ios/ADE/Views/Work/WorkStatusAndFormattingHelpers.swift
  • apps/ios/ADETests/ADETests.swift
💤 Files with no reviewable changes (3)
  • apps/desktop/src/renderer/components/shared/ProviderModelSelector.tsx
  • apps/desktop/src/renderer/components/shared/ModelCatalogPanel.tsx
  • apps/desktop/src/main/services/chat/droidAcpPool.ts

Comment thread apps/ade-cli/src/services/modelPickerStore.ts
Comment on lines +3337 to +3357
useEffect(() => {
const conn = connectionRef.current;
if (!conn) return;
let cancelled = false;
void (async () => {
try {
const [favorites, recents] = await Promise.all([
getModelPickerFavorites(conn).catch(() => [] as string[]),
getModelPickerRecents(conn).catch(() => [] as string[]),
]);
if (cancelled) return;
setModelPickerFavorites(favorites);
setModelPickerRecents(recents);
} catch {
// Best-effort hydration — picker still functions with empty state.
}
})();
return () => {
cancelled = true;
};
}, [socketPath]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

cd /tmp/repo && wc -l apps/ade-cli/src/tuiClient/app.tsx

Repository: arul28/ADE

Length of output: 114


🏁 Script executed:

# Read the specific lines mentioned in the review
sed -n '3337,3357p' apps/ade-cli/src/tuiClient/app.tsx

Repository: arul28/ADE

Length of output: 700


🏁 Script executed:

# Get broader context around this effect to understand connectionRef and socketPath
sed -n '3300,3380p' apps/ade-cli/src/tuiClient/app.tsx

Repository: arul28/ADE

Length of output: 3646


🏁 Script executed:

# Search for connectionRef definition and usage
rg "connectionRef\s*=" apps/ade-cli/src/tuiClient/app.tsx -A 2 -B 2

Repository: arul28/ADE

Length of output: 321


🏁 Script executed:

# Search for socketPath usage and definition
rg "socketPath" apps/ade-cli/src/tuiClient/app.tsx -B 2 -A 2 | head -50

Repository: arul28/ADE

Length of output: 1514


🏁 Script executed:

# Search for "connection" variable definition to see if it exists as suggested in the fix
rg "const connection\s*=" apps/ade-cli/src/tuiClient/app.tsx -B 2 -A 2

Repository: arul28/ADE

Length of output: 344


🏁 Script executed:

# Look for any connection-related state or props
rg "\bconnection\b" apps/ade-cli/src/tuiClient/app.tsx | head -20

Repository: arul28/ADE

Length of output: 1388


🏁 Script executed:

# Find where connectionRef.current is assigned
rg "connectionRef\.current\s*=" apps/ade-cli/src/tuiClient/app.tsx -B 2 -A 2

Repository: arul28/ADE

Length of output: 979


🏁 Script executed:

# Search for setConnection to understand when/how connection state is updated
rg "setConnection" apps/ade-cli/src/tuiClient/app.tsx -B 2 -A 2

Repository: arul28/ADE

Length of output: 926


🏁 Script executed:

# Search for useEffect that might set connectionRef.current
rg "useEffect.*connection" apps/ade-cli/src/tuiClient/app.tsx -B 2 -A 5

Repository: arul28/ADE

Length of output: 36


🏁 Script executed:

# Check where connection state is defined and used
rg "const \[connection,\s*setConnection\]" apps/ade-cli/src/tuiClient/app.tsx -B 3 -A 10

Repository: arul28/ADE

Length of output: 1066


🏁 Script executed:

# Check the detailed flow around connection establishment to confirm the timing
sed -n '2800,2900p' apps/ade-cli/src/tuiClient/app.tsx

Repository: arul28/ADE

Length of output: 4378


🏁 Script executed:

# Verify socketPath doesn't change - search for its usage patterns
rg "socketPath" apps/ade-cli/src/tuiClient/app.tsx | grep -E "(props|argument|\{|:)" | head -10

Repository: arul28/ADE

Length of output: 495


🏁 Script executed:

# Double-check the useEffect that establishes the connection to understand the race condition
rg -B 5 "connectionRef\.current = conn" apps/ade-cli/src/tuiClient/app.tsx | head -30

Repository: arul28/ADE

Length of output: 250


Hydration is keyed off socketPath, not the actual connection.

This effect runs on mount before connectionRef.current is populated, returns early, and never retries once the RPC connection is established. In the common startup path, favorites/recents stay empty instead of hydrating from ade-cli. Depend on the connection state instead of the static socketPath prop to trigger the effect when the connection becomes available.

Suggested fix
-  useEffect(() => {
-    const conn = connectionRef.current;
-    if (!conn) return;
+  useEffect(() => {
+    if (!connection) return;
+    const conn = connection;
     let cancelled = false;
     void (async () => {
       try {
@@
     return () => {
       cancelled = true;
     };
-  }, [socketPath]);
+  }, [connection]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/ade-cli/src/tuiClient/app.tsx` around lines 3337 - 3357, The effect that
hydrates model picker favorites/recents is currently keyed to socketPath and
returns early if connectionRef.current is not set, so it never retries when the
RPC connection arrives; update the useEffect dependency from [socketPath] to
depend on the actual connection state (e.g., [connection] or
connectionRef.current wrapped in state) so the effect re-runs when the
connection becomes available, leaving the internal early-return and cancel logic
intact and still calling getModelPickerFavorites/getModelPickerRecents and then
setModelPickerFavorites/setModelPickerRecents.

Comment thread apps/ade-cli/src/tuiClient/app.tsx
Comment thread apps/ade-cli/src/tuiClient/app.tsx
Comment on lines +86 to +91
hydrateFromRemote: async () => {
const api = getRpcApi();
if (!api) {
set({ hydrated: true });
return;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Recents hydration has the same permanent no-retry path.

If RPC is unavailable on first load, hydrated is set to true and hydrationStarted blocks further attempts, so recents may never reconcile with the shared authoritative store.

Also applies to: 103-123

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src/renderer/components/shared/ModelPicker/useModelRecents.ts`
around lines 86 - 91, The hydration path in useModelRecents (hydrateFromRemote)
incorrectly sets hydrated = true when getRpcApi() returns null, and leaves
hydrationStarted blocking future attempts; instead, when !api is detected do NOT
set hydrated to true and reset or clear hydrationStarted so the function can be
retried later (or schedule a retry). Update hydrateFromRemote and the related
hydrationStarted/hydrated state updates so that hydrationStarted is only
set/cleared around actual attempts, and hydrated is only set true after a
successful reconcile (reference hydrateFromRemote, hydrationStarted, hydrated,
and set).

Comment on lines +434 to +474
return (
<div ref={containerRef} className="relative min-w-0 shrink-0">
<button
type="button"
disabled={disabled}
data-state={open ? "open" : "closed"}
onClick={() => {
if (!disabled) setOpen((current) => !current);
}}
className={cn(
"inline-flex min-w-0 max-w-[11rem] items-center gap-1.5 rounded-md border font-sans transition-colors duration-150",
compact ? "h-7 px-1.5 text-[10px]" : "h-8 px-2 text-[11px] sm:text-[12px]",
"border-white/[0.06] bg-white/[0.03] text-fg/80",
"hover:border-violet-400/20 hover:bg-violet-500/[0.06] hover:text-fg",
open && "border-violet-400/30 bg-violet-500/[0.08] text-fg",
disabled && "cursor-not-allowed opacity-60 hover:border-white/[0.06] hover:bg-white/[0.03]",
)}
aria-label={`${providerLabel} permission mode`}
aria-haspopup="menu"
aria-expanded={open}
title={`Permission mode: ${selected.label}`}
>
<span className={cn("h-1.5 w-1.5 shrink-0 rounded-full", permissionSafetyDotClass(selected))} />
<span className="min-w-0 truncate font-medium leading-none">{selected.label}</span>
<CaretDown
size={compact ? 9 : 10}
weight="bold"
className={cn(
"shrink-0 text-muted-fg/60 transition-transform duration-150",
open && "rotate-180 text-fg/80",
)}
/>
</button>
{open ? (
<motion.div
key="cli-permission-picker"
data-cli-permission-picker-panel="true"
className="fixed z-[82] overflow-y-auto rounded-xl border border-white/[0.10] bg-popover/95 p-1 shadow-2xl shadow-black/35 backdrop-blur-xl"
style={panelStyle ?? undefined}
initial={{ opacity: 0, y: 6, scale: 0.98 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: 6, scale: 0.98 }}
transition={{ duration: 0.14 }}
<div
role="menu"
aria-label={`${providerLabel} permission mode`}
className={cn(
"absolute right-0 top-full z-[100] mt-1.5 w-[280px] overflow-y-auto rounded-xl border border-white/[0.08] p-1",
"bg-[#13111A]/95 shadow-[0_18px_48px_rgba(0,0,0,0.55)] backdrop-blur-md",
)}

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 | 🏗️ Heavy lift

Keep the permission menu out of the clipped Work surface.

This dropdown now renders inline with absolute top-full, but the continuation composer lives inside multiple overflow-hidden Work containers. In grid tiles and other short panes, the menu can be cut off or rendered off-screen, which blocks changing permission mode. The old portal avoided that failure mode.

Please keep this menu portaled, or add viewport collision/flip handling before shipping.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src/renderer/components/terminals/WorkViewArea.tsx` around lines
434 - 474, The dropdown menu is rendered inline and gets clipped by parent
overflow; change the inline menu render (the JSX block that currently
conditionally renders the <div role="menu"...> when open) to render via a portal
instead (e.g., ReactDOM.createPortal or your app's Portal component) into
document.body and position it using the trigger/button's bounding rect (use
containerRef or the button ref to compute left/top) so the menu escapes parent
overflow; alternatively integrate a collision-aware positioning helper
(floating-ui) for flip/viewport logic — update the conditional render logic that
uses open to create a portaled menu and compute absolute screen coordinates
before rendering.

Comment thread apps/desktop/src/shared/modelCatalog.ts
Comment on lines +3641 to +3644
func cachedChatModelCatalog() -> AgentChatModelCatalog? {
guard let cached = chatModelCatalogCache.values.sorted(by: { $0.fetchedAt > $1.fetchedAt }).first else { return nil }
guard Date().timeIntervalSince(cached.fetchedAt) < chatModelsCacheTTL else { return nil }
return cached.catalog

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Scope the fallback catalog to the current host/project.

This helper ignores the host/project scoping encoded into chatModelsCacheKey, so the fallback paths on Line 3666 and Line 3703 can reuse a catalog from the previous machine or project after reconnect/switch. That will show the wrong model availability/auth state until a live refresh succeeds.

🛠️ Suggested fix
   `@MainActor`
   func cachedChatModelCatalog() -> AgentChatModelCatalog? {
-    guard let cached = chatModelCatalogCache.values.sorted(by: { $0.fetchedAt > $1.fetchedAt }).first else { return nil }
+    let hostKey = activeHostProfile?.hostIdentity
+      ?? activeHostProfile?.lastHostDeviceId
+      ?? activeHostProfile?.lastSuccessfulAddress
+      ?? currentAddress
+      ?? hostName
+      ?? "unpaired-host"
+    let projectKey = activeProjectRootPath ?? activeProjectId ?? "no-project"
+    let scopeSuffix = [hostKey, projectKey].joined(separator: "\u{1f}")
+    guard let cached = chatModelCatalogCache
+      .filter({ $0.key.hasSuffix(scopeSuffix) })
+      .map(\.value)
+      .max(by: { $0.fetchedAt < $1.fetchedAt }) else { return nil }
     guard Date().timeIntervalSince(cached.fetchedAt) < chatModelsCacheTTL else { return nil }
     return cached.catalog
   }

Also applies to: 3666-3667, 3703-3704

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/ios/ADE/Services/SyncService.swift` around lines 3641 - 3644, The
cachedChatModelCatalog() helper currently picks the newest entry from
chatModelCatalogCache without respecting the per-host/project scoping encoded in
chatModelsCacheKey; update cachedChatModelCatalog() (and the fallback lookups
that use it) to first filter chatModelCatalogCache entries to only those whose
cache key matches the current chatModelsCacheKey scope (or otherwise contains
the current host/project identifier) before sorting by fetchedAt and checking
chatModelsCacheTTL; use the same scoping logic wherever cachedChatModelCatalog()
is used so a catalog from a different host/project isn’t returned (refer to
chatModelCatalogCache, chatModelsCacheKey, cached.fetchedAt and
chatModelsCacheTTL to locate and fix the code).

Comment thread apps/desktop/src/main/services/chat/droidSdkPool.ts
Comment thread apps/desktop/src/main/services/chat/droidModelsDiscovery.ts
arul28 and others added 14 commits May 18, 2026 18:11
Pre-merge snapshot to protect uncommitted work before pulling origin/main:
- droidSdk{Pool,Worker,EventMapper,Protocol}.ts replace deleted droidAcpPool.ts
- New apps/desktop/.../shared/ModelPicker/ folder (favorites + recents + providers split)
- Removed ModelCatalogPanel.tsx and ProviderModelSelector.tsx (superseded)
- Wires through agentChatService, AgentChatPane, AgentChatComposer, multiple settings/launch controls

Will be replayed onto merged main; this commit may be squashed later.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… sync

Adds a unified favorites/recents/providers picker in the ADE CLI right pane,
triggered by /model or the model row in new-chat-setup. Authoritative storage
for favorites and recents moves from desktop localStorage into the ade-cli
process so desktop, TUI, and iOS all share one source of truth.

ade-cli
- services/modelPickerStore.ts: process-singleton JSON-backed store at
  ~/.ade/modelPicker.json, capped at 10 recents, debounced writes, tolerates
  malformed files.
- adeRpcServer.ts: modelPicker.{getFavorites,setFavorites,toggleFavorite,
  getRecents,pushRecent} JSON-RPC methods.
- tuiClient/components/ModelPicker/: Ink picker pane with mini-rail, search,
  windowed list, keyboard handling (tab/shift-tab cycles rail, f favorites,
  / search, arrows move, enter selects, esc closes/clears).
- tuiClient/app.tsx: hydrates favorites/recents on connect, /model and
  chat:modelPicker now open the right-pane picker, and Enter on the model
  row in new-chat-setup opens the picker with surface=new-chat (escape and
  commit return to the setup pane).

Desktop
- useModelFavorites / useModelRecents: optimistic local update + RPC commit
  via window.ade.modelPicker.* (localStorage kept as offline cache so first
  render stays instant).
- preload.ts + global.d.ts: window.ade.modelPicker namespace wired through
  callProjectRuntimeSyncOr.

ModelStatus and the inline model row are intentionally untouched.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- agentChatService.getSlashCommands now returns /clear plus filesystem-discovered Claude + Codex prompt commands for droid sessions, mirroring codex. Previously droid sessions fell through to the OpenCode/Cursor branch and returned an empty list.
- adeApi.ts droid default model now resolves from the model registry via getDefaultModelDescriptor("droid") instead of a hardcoded string.

Tests added for both lane-only and live-session droid slash command paths.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
WorkModelPickerSheet rebuilt to match desktop ModelPicker layout:
- Vertical rail (Favorites star, Recents clock, divider, per-provider entries)
- Right content pane with section header, search, grouped model rows
- Per-row favorite star + active-checkmark + reasoning pills
- Optimistic favorites/recents store with rollback on RPC error
- Liquid glass styling via ADEColor.glassBorder + recessedBackground

SyncService.swift gains 5 RPC methods consuming the cross-surface
favorites/recents contract:
  modelPicker.getFavorites / setFavorites / toggleFavorite / getRecents / pushRecent

Chat session structs (AgentChatSession, AgentChatSessionSummary,
AgentChatUpdateSessionRequest) now round-trip droidPermissionMode
through createChatSession + updateChatSession, matching the desktop
schema.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
pushRecent debounces persistence by 250ms. If the user changes model and
closes the TUI immediately, the write hasn't fired yet and the recent is
lost. Wire flush() to a one-shot process.exit handler so the pending
write lands before the process terminates. Only registered for the
production singleton (filePath unset) — tests provide their own path
and handle teardown explicitly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Replace funnel icon with a visible, labeled toggle ('Show all models')
  with a switch indicator. Old funnel button had zero affordance — users
  didn't realize a toggle existed.
- When 'Show all models' is on, surface every provider family ADE
  supports (anthropic, openai, factory/droid, cursor, opencode, ollama,
  lmstudio) in the rail in a stable order — including dynamic-only
  providers (opencode, lmstudio) that have no static registry entries.
  Previously these families silently disappeared because providersPresent
  was derived only from models present in MODEL_REGISTRY.
- Whitelist modelPicker.* methods in runtimeBridge's sync allowlist so
  desktop's IPC bridge no longer rejects them as 'Local sync method is
  not exposed'.

Known gaps (next PR): Cursor/OpenCode/Droid panes show 'no models match'
empty state because they have no registry entries. Claude row currently
greys out from useProviderAuthStatus mis-classifying anthropic.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pull the reasoning effort UI out of the ModelPicker popover and into its
own dropdown rendered alongside it in the prompt-box row. The model
picker is now purely model selection; reasoning is a sibling control
that only appears when the active model has reasoning tiers.

- New `ReasoningEffortPicker.tsx` — popover trigger with current tier chip,
  side='top' (dropup), lists tiers from the descriptor, persists per
  family via useReasoningByFamily, returns null when no tiers.
- Removed showReasoning / reasoningEffort / onReasoningEffortChange props
  from ModelPicker.tsx and the corresponding render from
  ModelPickerContent.tsx. Deleted dead ReasoningEffortControl.tsx.
- Updated 10 callers across chat composer, chat pane, PR resolver,
  missions, CTO identity editor, review launch, AI settings, feedback
  modal, work view, parallel slots. Each renders <ModelPicker /> +
  <ReasoningEffortPicker /> side by side.
- Mobile (apps/ios) and TUI (apps/ade-cli) intentionally untouched; the
  user only wanted the split on desktop.
- Tests: new ReasoningEffortPicker.test.tsx (8 cases), updated
  ModelPicker.test.tsx + PrResolverLaunchControls.test.tsx for the new
  surface area.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…up CTA

Three fixes for the empty-rail / greyed-out feedback:

1. Canonical model lists for dynamic-only providers (Cursor, Droid,
   OpenCode, LM Studio). MODEL_REGISTRY only had static entries (4
   Anthropic + 6 OpenAI + 1 Ollama); the dynamic providers contributed
   nothing without runtime discovery, so their rail panes were empty.
   Now the picker surfaces the saved canonical model lists for those
   providers always — sourced from DROID_DEFAULT_MODEL_IDS in
   droidModelsDiscovery.ts and analogous defaults wired into
   modelRegistry.ts. Rows mark isAvailable=false when the provider isn't
   connected so they dim with a sign-in affordance.

2. Claude greyed-out auth status bug. useProviderAuthStatus was treating
   anthropic as "unauthed" whenever availableProviders.claude.runtimeAvailable
   wasn't a literal true — but the object also exposes auth.ready and
   binary.present, which can be true while runtimeAvailable is still
   transitional. Hook now accepts either as the auth signal so Claude
   models stay clickable when the user is actively chatting with them.

3. New providerEmptyState component with an inline "Set up X" CTA that
   opens Settings → Providers for any provider the user hasn't connected.
   Replaces the previous "no models match this view" empty state.

Includes 28 new tests across useProviderAuthStatus, modelCatalog, and
providerEmptyState. Existing 17 ModelPicker tests still pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Root cause: probeOpenCodeProviderInventory only ran when getStatus()
was called with refreshOpenCodeInventory=true. AgentChatPane only
forces that flag when sessionProvider === "opencode" — meaning the
user has to ALREADY be in an OpenCode chat for the inventory to
populate. Chicken-and-egg: model picker shows zero discovered OpenCode
models, user can't pick one, user never enters an OpenCode chat, cache
stays empty forever.

Fix: on the default code path, if peekOpenCodeInventoryCache returns
null AND the OpenCode binary is installed, fall through to a probe
with force=false. The in-flight dedup map + 60s TTL still apply, so
the OpenCode server boots at most once per project+config across the
session. Warm reads are unchanged (peek wins).

The existing test "does not cold-probe OpenCode inventory on default
getStatus" had codified the broken behavior — replaced with one that
asserts the cold path now probes once, plus a second test that
verifies a populated peek still bypasses the probe.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ode on binary

Per user direction, the preview-canonical approach is wrong. Replaced
with a clean separation:

- Anthropic + OpenAI: unchanged. Hardcoded MODEL_REGISTRY entries remain
  because they encode first-party CLI/SDK routing metadata that the
  vendors don't expose via a list-models API.
- Cursor + Droid: zero hardcoded preview. When the CLI is installed +
  authed, dynamic discovery surfaces models. When not, the pane shows
  ONLY a "Set up X" empty state — no preview rows.
- OpenCode + Ollama + LM Studio: same pattern, but all three are
  gated on OpenCode binary availability. When the OpenCode binary is
  missing, all three panes show the SAME "Install OpenCode" empty
  state. When present, models flow through probeOpenCodeProviderInventory.

Changes:
- Deleted DROID_CANONICAL_MODEL_IDS, CURSOR_CANONICAL_MODEL_IDS,
  OPENCODE_CANONICAL_PROVIDER_MODELS from modelRegistry.ts and the
  DROID_DEFAULT_MODEL_IDS re-export in droidModelsDiscovery.ts.
- Removed buildDynamicCanonicalDescriptors + canonical-injection path
  from modelCatalog.ts and ModelPickerContent.expandedModels.
- useProviderAuthStatus now tracks opencodeBinaryInstalled via a new
  opencodeBinaryInstalledFromStatus mapper; hook returns
  { status, opencodeBinaryInstalled, loaded }.
- ProviderEmptyState gains a discriminated "opencode-required" mode
  with family in opencode/ollama/lmstudio. Identical title + body
  + CTAs across all three families.
- ModelPickerContent.filterAvailable hides opencode/ollama/lmstudio
  models entirely when binary missing so the pane falls through to
  the opencode-required empty state. ProviderSetupBanner suppressed
  for those families in that state (single unified empty message).

Tests updated:
- modelCatalog.test.ts: removed canonical-surfacing assertions, added
  negative assertions for no-canonical-injection. Rebucket-to-opencode
  and discovered-droid-model tests retained and tightened.
- providerEmptyState.test.tsx: new opencode-required describe block
  (16 tests total).
- useProviderAuthStatus.test.ts: new describe for
  opencodeBinaryInstalledFromStatus boolean coercion.
- ModelPicker.test.tsx: new describe for OpenCode binary gating
  (asserts identical empty state copy across all three rails;
  ProviderSetupBanner suppressed).

Total: 4 ModelPicker test files, 54 tests, all pass. tsc clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
useProviderAuthStore initializes opencodeBinaryInstalled to false. On
first picker open, fetchStatus() takes ~2s (OpenCode server boot +
provider.list). During that window the picker rendered the
'Install OpenCode' empty state on the opencode/ollama/lmstudio panes —
which is wrong for users who DO have OpenCode installed.

Diagnostic confirmed the daemon returns opencodeBinaryInstalled=true
with 72 availableModelIds in ~2s; the renderer just hadn't received
that response yet. The bug was rendering an authoritative 'not installed'
claim during the loading window.

Fix: treat opencode as installed until internalAuth.loaded === true.
Empty state only fires after a confirmed loaded+missing signal.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three fixes for the OpenCode-detection UX:

1. Real binary detection, no defaults. Previous fix defaulted
   opencodeBinaryInstalled to true while the slow getStatus() was
   in-flight — fast for users who have it, wrong claim for users who
   don't. Added a cheap dedicated IPC `ai.isOpenCodeInstalled` that
   only runs the synchronous binary lookup (resolveOpenCodeBinary)
   without the 2-second server probe. Hook now runs both calls in
   parallel on mount; binary state is known in ~ms while the full
   provider/model catalog continues to populate in the background.
   New `binaryProbed` state field gates the "Install OpenCode" empty
   state on a CONFIRMED answer, not a guess.

2. Local-provider models stay in their own rail. rebucketOpenCodeFamily
   no longer moves family=lmstudio or family=ollama into the opencode
   bucket. LM Studio models discovered via OpenCode now appear in the
   LM Studio rail (where users expect them), Ollama in the Ollama rail.
   The OpenCode rail is reserved for cloud-routed models (anthropic,
   openai, google, etc. via the OpenCode router).

3. Drop "via OpenCode" suffix from sub-provider labels. Rows shown
   inside the OpenCode rail are already implicitly "via OpenCode" —
   the underlying provider name alone is enough. "openai via OpenCode"
   → "Openai". (Title-casing matches existing rail-entry capitalization.)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ments

CI fix:
- aiOrchestratorService.test.ts: bump health-sweep test deadline 180s→280s and vitest timeout 240s→300s; CI shard 8 hit the ceiling, logic is correct (passes in ~4s locally)

Review fixes (CodeRabbit):
- modelPickerStore: flush before resetSharedModelPickerStoreForTests
- tuiClient/app.tsx: sticky model picker pane + catalog-aware provider resolution
- modelPickerLayout: stale-provider fallback before deriving railIndex/pool
- droidSdkEventMapper: kind-aware itemId fallback for dedupe (text/thinking)
- droidSdkWorker: unique waiter IDs + per-run AbortController set
- preload: extend not-callable fallback to sync calls (modelPicker.*)
- AgentChatPane: thread runtime-catalog descriptor through draft provider paths
- ModelPickerContent: handle clipboard rejection via .catch
- useModelFavorites / useModelRecents: retry hydration after RPC failure
- shared/modelCatalog: classify direct local models into ollama/lmstudio

8 comments deferred with documented reasons (P2 P-fix, iOS Swift, invasive
refactor scope) — recorded in shipLane state.
@arul28 arul28 force-pushed the ade/droid-updates-99a3f743 branch from 0682bbc to 34115ac Compare May 18, 2026 22:14
@arul28

arul28 commented May 18, 2026

Copy link
Copy Markdown
Owner Author

@copilot review but do not make fixes

Copilot AI commented May 18, 2026

Copy link
Copy Markdown
Contributor

@copilot review but do not make fixes

Reviewed at 34115ac with no fixes applied. Current CI failure is test-desktop (8) timing out in src/main/services/orchestrator/aiOrchestratorService.test.ts (first reconcile did not enter within 180s); ci-pass then fails as expected. Droid SDK review: (1) sanitizeEnv in droidSdkPool.ts is still a no-op and the name is misleading, (2) droidModelsDiscovery.ts still has no static fallback when SDK discovery fails (returns empty list), (3) cross-turn mapper-state concern appears already addressed because runtime.eventMapperState is reset at turn start in agentChatService.ts:17924.

Copilot finished work on behalf of arul28 May 18, 2026 22:16
@arul28 arul28 merged commit 049a79f into main May 18, 2026
26 checks passed
@arul28 arul28 deleted the ade/droid-updates-99a3f743 branch May 18, 2026 22:22
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