Skip to content

Droid Chat#214

Merged
arul28 merged 17 commits intomainfrom
cursor/droid-ai-chat-surface-6b3b
Apr 28, 2026
Merged

Droid Chat#214
arul28 merged 17 commits intomainfrom
cursor/droid-ai-chat-surface-6b3b

Conversation

@arul28
Copy link
Copy Markdown
Owner

@arul28 arul28 commented Apr 28, 2026

Summary

Describe the change.

What Changed

Key files and behaviors.

Validation

How you tested.

Risks

Anything to watch.

Summary by CodeRabbit

  • New Features

    • Added support for Factory Droid CLI as a first-class AI provider with configurable autonomy modes
    • Added Droid model discovery and selection in chat and mission interfaces
    • Added authentication detection for Droid via environment credentials and local configuration
    • Extended AI provider settings to include Droid connection status and model availability
  • Improvements

    • Enhanced process termination and resource cleanup for better stability

Greptile Summary

This PR adds Factory Droid CLI as a first-class AI provider, wiring it through auth detection, model discovery, ACP connection pooling, permission modes, and the full chat turn lifecycle alongside the existing Claude/Codex/Cursor/OpenCode providers. The cursorAcpPool subprocess management is extracted into a reusable acpCliPool abstraction shared by both Cursor and Droid, and utils.ts gains cross-platform SIGTERM→SIGKILL process-tree escalation helpers.

Confidence Score: 5/5

Safe to merge; all findings are P2 (minor memory leak and dedup threshold quality concern)

No P0 or P1 issues found. The two P2s (stale droidSessionDedup entry on pool-key swap and the 32-char replay dedup threshold) don't affect correctness in the common path — the leak is bounded to one Map entry per model-switch and the dedup false-positive requires an exact substring match in history text.

apps/desktop/src/main/services/chat/agentChatService.ts — droidSessionDedup cleanup in ensureDroidRuntime pool-key-change branch and the isDuplicateDroidNotification replay threshold

Important Files Changed

Filename Overview
apps/desktop/src/main/services/chat/agentChatService.ts Major extension adding full Droid provider support (runtime, permission modes, model resolution, ACP session lifecycle, dedup logic); two P2 issues: stale droidSessionDedup entry on pool-key swap, and substring dedup threshold too low at 32 chars
apps/desktop/src/main/services/chat/droidAcpPool.ts New module: thin Droid-specific wrapper over acpCliPool with ref-counted connection pooling, stale-entry detection, and pending-init deduplication — mirrors cursorAcpPool cleanly
apps/desktop/src/main/services/chat/acpCliPool.ts New shared abstraction for ACP-over-CLI process management, extracted from cursorAcpPool; handles spawn, ndJson stream setup, ACP initialize handshake, ref counting, and shutdown
apps/desktop/src/main/services/chat/droidModelsDiscovery.ts New module: multi-probe model discovery from droid exec --help, JSON fallbacks, and ~/.factory/config.json custom models; TTL cache with in-flight dedup guard
apps/desktop/src/main/services/ai/authDetector.ts Added Droid CLI probe and inspectDroidCliAuthentication path; existing Cursor probe logic unchanged
apps/desktop/src/main/services/ai/aiIntegrationService.ts Droid availability, model list, and connection status wired into AiIntegrationStatus; model discovery triggered when FACTORY_API_KEY or CLI auth detected
apps/desktop/src/main/services/ai/providerConnectionStatus.ts Droid connection status block added; correctly combines CLI auth and FACTORY_API_KEY env var, with user-facing blocker messages for common misconfiguration states
apps/desktop/src/main/services/shared/utils.ts New cross-platform process-tree helpers: destroyChildProcessStreams, signalChildProcessTree, terminateChildProcessTree; spawnAsync improved with SIGTERM→SIGKILL escalation and hard-resolve safety timeout
apps/desktop/src/shared/modelRegistry.ts Added factory provider family, Droid dynamic descriptor creation, grouping helpers, sort order, and regex fixes for OpenAI model ID patterns

Sequence Diagram

sequenceDiagram
    participant UI as Renderer
    participant ACS as agentChatService
    participant DAP as droidAcpPool
    participant ACP as acpCliPool
    participant DCLI as droid CLI (subprocess)
    participant DMD as droidModelsDiscovery
    participant AD as authDetector

    UI->>ACS: sendMessage (provider=droid)
    ACS->>AD: detectAuth()
    AD-->>ACS: DetectedAuth (cli-subscription droid or FACTORY_API_KEY)
    ACS->>DMD: discoverDroidCliModelDescriptors(droidPath)
    DMD->>DCLI: droid exec --help
    DCLI-->>DMD: model list stdout
    DMD-->>ACS: ModelDescriptor[]
    ACS->>ACS: ensureDroidRuntime(managed)
    ACS->>DAP: acquireDroidAcpConnection(poolKey, modelId, launchSettings)
    DAP->>ACP: acquireAcpCliConnection(options)
    ACP->>DCLI: spawn droid exec --output-format acp --auto medium
    DCLI-->>ACP: ACP initialize handshake
    ACP-->>DAP: AcpCliPooled
    DAP-->>ACS: DroidAcpPooled + generation
    ACS->>DCLI: newSession / resumeSession (ACP)
    DCLI-->>ACS: sessionId
    ACS->>DCLI: sendMessage (ACP turn)
    loop streaming chunks
        DCLI-->>ACS: agent_message_chunk
        ACS->>ACS: isDuplicateDroidNotification?
        ACS-->>UI: chat_event (text delta)
    end
    DCLI-->>ACS: turn_complete
    ACS-->>UI: chat_event (done)
Loading

Fix All in Claude Code

Prompt To Fix All With AI
This is a comment left during a code review.
Path: apps/desktop/src/main/services/chat/agentChatService.ts
Line: 13333-13340

Comment:
**`droidSessionDedup` entry leaked on pool-key swap**

When a model or autonomy-mode change causes `ensureDroidRuntime` to tear down an existing runtime because `existing.poolKey !== poolKey`, the `acpHostSessionOwners` entry for the old session ID is correctly deleted (line 13334), but `clearDroidSessionDedup(existing.acpSessionId)` is never called. The dedup-state object (`historyText`, `currentTurnText`, `seenModes`) for the closed ACP session will remain in the `droidSessionDedup` map for the lifetime of the service. By contrast, the normal `teardownRuntime` path at line 6719 always calls `clearDroidSessionDedup`.

Add `clearDroidSessionDedup(existing.acpSessionId)` after the `acpHostSessionOwners.delete` call on line 13334.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: apps/desktop/src/main/services/chat/agentChatService.ts
Line: 3375-3395

Comment:
**`REPLAY_MIN_LEN=32` substring check risks suppressing legitimate new responses**

`isDuplicateDroidNotification` guards against a known Droid CLI bug where it re-emits history as a final chunk. The current check — `entry.historyText.includes(chunkText)` for any chunk ≥ 32 chars — can produce false positives: if the model naturally generates the same code snippet, phrase, or short paragraph it used in an earlier turn, the new streaming delta will be silently dropped from the UI.

32 characters is quite short; common repetitions in code conversations (e.g. a repeated function signature, a recurring import line, or a standard greeting like "Sure, here's the updated code:") can easily exceed that threshold and match history text exactly. Consider either raising the threshold significantly (e.g. 256 chars) so that only replay-sized chunks are suppressed, or comparing the chunk length against the total `historyText` length so only chunks that match the complete accumulated history are filtered.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: apps/desktop/src/main/services/chat/agentChatService.ts
Line: 13416-13420

Comment:
**`cursorAcpSessionRequest` injecting Cursor-specific field into Droid ACP calls**

`cursorAcpSessionRequest` appends `mcpServers: []` (the Cursor-specific field) to every request it wraps. It's used here for both the `resumeAcpSession` call and the `newSession` call on the Droid ACP connection. If the Droid ACP server validates incoming request schemas strictly, the extra field could cause request rejections. Since Droid is a separate CLI from Cursor, consider using a plain request object or a Droid-specific wrapper instead.

How can I resolve this? If you propose a fix, please make it concise.

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

cursoragent and others added 16 commits April 14, 2026 10:15
Share createAcpHostClient and acquireAcpCliConnection between providers.
Refactor Cursor ACP pool to delegate spawn/init and cursor_login handling.

Co-authored-by: Arul Sharma <arul28@users.noreply.github.com>
- Spawn droid exec --output-format acp with pooled JSON-RPC like Cursor
- Dynamic droid/* model descriptors, discovery + defaults, auth via CLI + FACTORY_API_KEY
- Wire DroidRuntime through agent chat (turns, steer queue, interrupt, permissions, persistence)
- Extend AI status, model picker grouping, providers settings, missions permissions for droid

Co-authored-by: Arul Sharma <arul28@users.noreply.github.com>
…safety

- Serialize acquire per poolKey with shared pending init; fix double inner ref on cursor/droid fast path
- Evict stale entries on child close/error; kill process and log stderr tail on init/handshake failure
- Resolve ACP terminal cwd within lane root; cap waitForTerminalExit with SIGKILL + close wait

Co-authored-by: Arul Sharma <arul28@users.noreply.github.com>
- Prefer DROID_EXECUTABLE/FACTORY_DROID_EXECUTABLE over synthetic auth paths
- Do not treat droid --version as login; use FACTORY_API_KEY, ~/.factory/settings, or auth probes
- Only inject FACTORY_API_KEY cli-subscription when binary resolves (not fallback-command)
- Align droid blocker and settings login hint with actual auth steps
- Honor interrupt during Droid runtime setup and before prompt dispatch; avoid Claude fallback on cancel mid-setup
- Approval_request detail: cursorAcp only for Cursor; add acpHost source
- Map unified edit mode to Droid --auto medium

Co-authored-by: Arul Sharma <arul28@users.noreply.github.com>
…interrupt events

- inspectDroidCliPresence: STRONG_UNAUTH => verified true; inconclusive => verified false
- detectCliAuthStatuses droid: use resolveDroidExecutable path for probes
- ensureDroidRuntime: abort if managed.closed after acquire; release pool
- runDroidTurn: emit interrupted status + terminal events on early exit and closed-during-setup

Co-authored-by: Arul Sharma <arul28@users.noreply.github.com>
…er on close

- Replace recursive acquireAcpCliConnection with retry loop (max attempts + backoff)
- Clear SIGKILL timeout when process closes before WAIT_FOR_TERMINAL_EXIT_MAX_MS

Co-authored-by: Arul Sharma <arul28@users.noreply.github.com>
…nd pool key normalization

- authDetector: inspect stdout+stderr for auth patterns regardless of exit code
- acpCliPool: getRootPath returns options.spawn.cwd instead of empty string
- agentChatService: prevent double releaseDroidAcpConnection via released flag
- agentChatService: droidPoolKeyFor uses resolved model ID, not raw session model
- cursorAcpPool/droidAcpPool: add generation tokens to prevent stale-generation
  races — acquire returns { pooled, generation }, release validates generation
- agentChatService: thread poolGeneration through CursorRuntime/DroidRuntime
- Update test mocks to match new { pooled, generation } return shape
Introduces an autonomous PR-to-merge driver that runs automate → finalize
once, then polls CI and review comments on a self-paced 12-min cadence,
fixing valid comments and failing tests in place. Prefers TeamCreate agent
teams when available, falls back to parallel Agent calls otherwise. Opens
the PR via `ade prs create` when possible so it shows up in ADE's PR
tracking; falls back to `gh pr create` only after the agent has genuinely
exhausted the ADE path via `--help`-driven discovery.

Also narrows /automate to run only the new and affected tests (not the
full suite), and makes /finalize's 8-shard parallel run explicit so shards
don't get chained serially.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
(cherry picked from commit 68ef71a)
Lane worktrees use symlinks named node_modules pointing at the main
checkout's installed copies. The previous `node_modules/` pattern only
matched directories, so git saw the symlinks as untracked — which in turn
blocked `ade prs create` preflight and forced /shipLane to fall back to
`gh pr create`. Without the slash, the pattern matches both directories
(main checkout) and symlinks (worktrees).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
(cherry picked from commit 07623ee)
The 8-shard vitest run, parallel tsup builds, and typecheck fans sometimes
leave worker pools behind after the phase exits. They don't fail CI but
accumulate across runs and can hold file locks. New Phase 3j kills them
scoped to apps/ paths so vitest instances the user may have running
elsewhere aren't affected. Adds a Cleanup line to the Phase 4 summary and
the completion checklist.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
(cherry picked from commit 3a5fbe5)
Resolves conflicts in package.json, finalize/automate/shipLane commands, ship-lane playbook, main.ts shutdown chain, agentChatService chat plumbing, cursorAcpPool refactor, AgentChatPane provider routing, sessionService tests, and shared utils spawn/termination helpers. Drops the lane's mcpServers wiring for ACP sessions to align with main's intentional removal in PR #166; restores Windows-portable spawn invocation in acpHostClient.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirrors the codexExecutable test pattern: env override beats auth path,
auth path beats PATH lookup, fallback returns the bare 'droid' command.
aiIntegrationService.ts now reads providerConnections.droid.runtimeAvailable
unconditionally, but the test helper only built {claude, codex, cursor},
producing TypeError on getStatus. Add droid to the helper, ServiceFactoryOptions
availability, and the cli statuses / detectAllAuth mocks.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 28, 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 Apr 28, 2026 4:14pm

@arul28
Copy link
Copy Markdown
Owner Author

arul28 commented Apr 28, 2026

@copilot review but do not make fixes

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 28, 2026

📝 Walkthrough

Walkthrough

This pull request adds comprehensive support for Factory Droid CLI as a first-class AI provider across authentication, connection pooling, model discovery, orchestration, and UI. It introduces new ACP infrastructure, auth detection logic, type definitions, and React components to integrate Droid alongside existing providers.

Changes

Cohort / File(s) Summary
CLI & Bootstrap
apps/ade-cli/src/adeRpcServer.ts, bootstrap.ts, cli.ts
Adds droidPermissionMode parameter to chat session creation and updates stale-session reconciliation to exclude "droid-chat" tool type.
SDK Dependencies
apps/desktop/package.json
Bumps @agentclientprotocol/sdk from ^0.17.1 to ^0.20.0 and @opencode-ai/sdk from ^1.3.17 to ^1.4.2.
Authentication & Executable Resolution
apps/desktop/src/main/services/ai/authDetector.ts, authDetector.test.ts, droidExecutable.ts, droidExecutable.test.ts
Introduces Droid CLI detection from FACTORY_API_KEY, ~/.factory/settings.json token, and CLI probes; adds resolveDroidExecutable function with env var, auth-path, and fallback resolution strategies.
Provider Connection Status
apps/desktop/src/main/services/ai/providerConnectionStatus.ts, agentExecutor.ts
Adds "droid" provider to connection status building with auth/runtime availability checks, updates AgentProvider union to include "droid".
Model Discovery & Integration
apps/desktop/src/main/services/ai/droidModelsDiscovery.ts, droidModelsDiscovery.test.ts, aiIntegrationService.ts, aiIntegrationService.test.ts
Implements Droid model discovery via CLI help parsing, JSON/CLI probes, ~/.factory/config.json custom models, and built-in defaults; integrates into AI status with Droid models and availability.
ACP Connection Pooling
apps/desktop/src/main/services/chat/acpCliPool.ts, acpHostClient.ts
Introduces new ACP CLI pooling service with connection caching, initialization handshake, stderr logging, and lifecycle controls; adds ACP host client implementing file/terminal operations with root-path constraints.
Provider-Specific ACP Pools
apps/desktop/src/main/services/chat/cursorAcpPool.ts, droidAcpPool.ts
Refactors Cursor pool to delegate to generic ACP CLI pool; adds Droid-specific pool with exec argument construction, autonomy flags, and generation tracking.
Chat Service & Tests
apps/desktop/src/main/services/chat/agentChatService.ts, agentChatService.test.ts
Extends agent chat to acquire/manage Droid ACP sessions, realign to selected model, handle autonomy modes, and surface structured failures; adds comprehensive Droid-focused test coverage.
Shutdown & Cleanup
apps/desktop/src/main/main.ts
Adds shutdownAcpCliConnections() call during app shutdown to close pooled CLI connections.
Orchestration: Executor & Permissions
apps/desktop/src/main/services/orchestrator/orchestratorService.ts, orchestratorQueries.ts, permissionMapping.ts, providerOrchestratorAdapter.ts, recoveryService.ts, aiOrchestratorService.ts
Adds "droid" executor kind recognition across permission parsing, adapter registration, teammate detection, model inference, and plan adjustment; maps Droid permission modes to native fields.
Orchestration: Diagnostics & Delivery
apps/desktop/src/main/services/orchestrator/workerTracking.ts, workerDeliveryService.ts
Extends failure diagnosis and worker delivery to accept "droid" as valid executor kind in JSON schemas and validation.
Session & PTY Services
apps/desktop/src/main/services/sessions/sessionService.ts, sessionService.test.ts, pty/ptyService.ts, pty/ptyService.test.ts
Adds "droid-chat" tool type support with resume-command inference; normalizes legacy sessions with chat:droid:* prefixes to "droid-chat".
Remote Sync
apps/desktop/src/main/services/sync/syncRemoteCommandService.ts
Propagates droidPermissionMode in chat session serialization and argument parsing for remote operations.
Config & IPC
apps/desktop/src/main/services/config/projectConfigService.ts, ipc/registerIpc.ts
Extends AI config coercion and IPC status/permission mapping to recognize "droid" provider with default unavailable state and blocker messages.
Type Definitions
apps/desktop/src/shared/types/chat.ts, config.ts, models.ts, missions.ts, orchestrator.ts, sessions.ts
Adds AgentChatDroidPermissionMode, "droid" to provider/executor/tool-type unions, droidPermissionMode field across session types, TerminalToolType "droid-chat", and error detail field.
Model Registry & Selection
apps/desktop/src/shared/modelRegistry.ts, modelRegistry.test.ts
Implements dynamic Droid model parsing (droid/<modelId>), custom model proxies, Droid-specific display formatting/grouping, and descriptor creation; adds "factory" family and "droid" provider group with sorting/labeling.
Model Selection UI
apps/desktop/src/renderer/components/shared/providerModelSelectorGrouping.ts, providerModelSelectorGrouping.test.ts
Adds "droid" provider group with Droid-specific subsection keying/labeling for custom/factory models.
Permission Options
apps/desktop/src/renderer/components/shared/permissionOptions.ts, permissionOptions.test.ts
Defines Droid-specific permission presets (plan, edit, default, full-auto) for CLI-wrapped factory models; extends familyToPermissionKey and permissionFamilyLabel to support "droid".
Model & Provider Utilities
apps/desktop/src/renderer/lib/modelOptions.ts, modelOptions.test.ts, lib/sessions.ts
Adds includeDroid flag to model derivation; maps "droid" provider to "droid-chat" tool type and session labeling.
Chat UI Components
apps/desktop/src/renderer/components/chat/AgentChatComposer.tsx, AgentChatComposer.test.tsx, AgentChatPane.tsx, AgentChatMessageList.tsx
Adds Droid autonomy mode combobox to composer, parallel control slot wiring for droidPermissionMode, model metadata resolution for factory/droid families, and error detail rendering.
Settings & Providers UI
apps/desktop/src/renderer/components/settings/ProvidersSection.tsx, ProvidersSection.test.tsx, UsageGuardrailsSection.tsx
Registers Droid as CLI runtime with login/install instructions, renders Droid provider card with connection/credential status and factory-env support; adds Droid auth to guardrails.
Mission & Permission UI
apps/desktop/src/renderer/components/missions/CreateMissionDialog.tsx, WorkerPermissionsEditor.tsx, useMissionsStore.ts
Includes Droid models in cache prewarming/refresh, grants full-auto default permissions, detects factory auth as Droid subscription, extends permission family editor for Droid.
Model Catalog & Logos
apps/desktop/src/renderer/components/shared/ModelCatalogPanel.tsx, ProviderLogos.tsx, terminals/ToolLogos.tsx
Adds Droid CLI dynamic model support with grouping/sorting, implements DroidLogo component, resolves Droid model family colors/icons, maps "droid-chat" tool type to Droid logo.
Process Management
apps/desktop/src/main/services/shared/utils.ts, utils.test.ts
Refactors spawnAsync to spawn non-Windows processes as detached, adds process-tree signaling/termination helpers with timeout-based SIGTERM/SIGKILL escalation, improves stream cleanup.
Documentation
apps/desktop/src/renderer/assets/provider-logos/README.md
Documents Droid branding and asset usage in provider-logos directory.
Test Mocks
apps/desktop/src/renderer/browserMock.ts, apps/desktop/src/main/services/orchestrator/aiOrchestratorService.test.ts
Extends browser mock and test service mocks with Droid provider availability, models, and permission mode support.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

  • arul28/ADE#118: Adds Cursor CLI as first-class provider with parallel code-level changes to the same auth detection, connection status, ACP pooling, and UI wiring infrastructure.
  • arul28/ADE#106: Introduces cliExecutableResolver and executable resolution logic that forms the foundation for resolveDroidExecutable used in this PR.
  • arul28/ADE#201: Modifies the same ade-cli files (adeRpcServer.ts, cli.ts) for session configuration and command handling.

Suggested labels

desktop, feature, provider-support

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 13.89% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The pull request title "Droid Chat" is vague and generic, using a high-level feature name without conveying the scope or specifics of the substantial changes involved. Consider a more descriptive title that captures the primary scope, such as "Add Droid AI chat surface with ACP pool and model discovery" or "Implement Factory Droid chat support with shared ACP infrastructure".
✅ Passed checks (3 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 cursor/droid-ai-chat-surface-6b3b

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment thread apps/desktop/src/main/services/chat/droidModelsDiscovery.ts Outdated
Comment thread apps/desktop/src/main/services/ai/authDetector.ts Outdated
Comment thread apps/desktop/src/main/services/chat/droidModelsDiscovery.ts
Copy link
Copy Markdown

@capy-ai capy-ai Bot left a comment

Choose a reason for hiding this comment

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

Added 1 comment


// The replay chunk contains text from previous turns. If the chunk's
// text is found in the accumulated history, it's a replay — drop it.
if (entry.historyText.length > 0 && entry.historyText.includes(chunkText)) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[🟡 Medium] [🟡 Investigate]

The isDuplicateDroidNotification function uses String.includes() to detect replay chunks from the Droid CLI. Both historyText.includes(chunkText) (line 3376) and currentTurnText.includes(chunkText) (line 3382) match when a legitimate new streaming chunk happens to be a substring of previously accumulated text. For example, if a prior turn produced "This function returns the value" and a new turn streams a chunk "the value", it would be silently dropped as a false-positive replay. Short chunks (common in LLM streaming) are especially vulnerable. A length-ratio guard (e.g., only treat as replay if chunkText.length > historyText.length * 0.5) or a prefix-match approach would reduce false positives while still catching the described whole-conversation replay behavior.

// agentChatService.ts
if (entry.historyText.length > 0 && entry.historyText.includes(chunkText)) {
  return true;
}

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

Caution

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

⚠️ Outside diff range comments (5)
apps/desktop/src/renderer/lib/modelOptions.test.ts (1)

15-24: ⚠️ Potential issue | 🟡 Minor

Please add a Droid-specific assertion here.

This helper now carries droid, but the suite still never proves that a Droid-authenticated status contributes Droid model ids/options. A provider-specific regression would still pass.

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

In `@apps/desktop/src/renderer/lib/modelOptions.test.ts` around lines 15 - 24, The
test helper makeStatus now includes droid fields but the suite lacks a
Droid-specific assertion; update the relevant test in modelOptions.test.ts to
include a case where makeStatus returns detectedAuth: ['droid'] and models.droid
contains one or more model entries, then assert that the code under test (the
function that builds model options) includes those Droid model ids/options in
its output (use the same assertions pattern as for claude/codex/cursor to verify
provider-specific inclusion). Reference makeStatus, AiSettingsStatus,
availableProviders, detectedAuth, models, and the droid key when adding this
assertion.
apps/desktop/src/shared/types/config.ts (1)

1069-1082: ⚠️ Potential issue | 🟠 Major

Add "droid" to AiChatConfig.defaultProvider too.

These status contracts now make Droid first-class, but Line 1206 still narrows AiChatConfig.defaultProvider to "codex" | "claude" | "cursor" | "opencode" | "last_used". That leaves typed config flows unable to persist Droid as the default chat provider.

Suggested fix
 export type AiChatConfig = {
-  defaultProvider?: "codex" | "claude" | "cursor" | "opencode" | "last_used";
+  defaultProvider?: "codex" | "claude" | "cursor" | "droid" | "opencode" | "last_used";
   defaultApprovalPolicy?: "auto" | "approve_mutations" | "approve_all";
   sendOnEnter?: boolean;

As per coding guidelines, keep IPC contracts, preload types, shared types, and renderer usage in sync whenever an interface changes.

Also applies to: 1280-1293

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

In `@apps/desktop/src/shared/types/config.ts` around lines 1069 - 1082, The
defaultProvider union for AiChatConfig still excludes "droid", causing Droid not
to be persistable: update the AiChatConfig.defaultProvider type to include
"droid" alongside "codex" | "claude" | "cursor" | "opencode" | "last_used" and
mirror that change in the other occurrence noted (the second union around the
later block), ensuring both places accept "droid" so the shared
IPC/preload/renderer types stay in sync with AiSettingsStatus which already
treats droid as first-class.
apps/desktop/src/renderer/lib/sessions.ts (1)

52-65: ⚠️ Potential issue | 🟡 Minor

Sync the new Droid label with the other session-label helpers.

Line 62 introduces "Droid chat" as the autogenerated title, but isGenericSessionTitle() and SHORT_TOOL_TYPE_LABELS below still don't know about droid-chat. That makes Droid sessions behave differently from the other chat providers: optimistic/new sessions can keep the generic title instead of falling through to goal/summary, and compact cards will render droid chat instead of Droid.

💡 Suggested follow-up
 const SHORT_TOOL_TYPE_LABELS: Record<string, string> = {
   shell: "Shell",
   "run-shell": "Run",
   cursor: "Cursor",
+  "droid-chat": "Droid",
   aider: "Aider",
   continue: "Continue",
 };
@@
   if (
     normalized === "opencode chat" ||
     normalized === "claude chat" ||
     normalized === "codex chat" ||
     normalized === "cursor chat" ||
+    normalized === "droid chat" ||
     normalized === "claude code" ||
     normalized === "claude cli" ||
     normalized === "claude session" ||
     normalized === "codex" ||

Also applies to: 132-145

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

In `@apps/desktop/src/renderer/lib/sessions.ts` around lines 52 - 65, Update the
other session-label helpers to recognize the new "droid-chat" tool type so Droid
sessions behave like other chat providers: add "droid-chat" to the logic in
isGenericSessionTitle() and include a corresponding entry ("Droid") in
SHORT_TOOL_TYPE_LABELS (and any similar mappings) so the optimistic/new session
title, compact cards, and summary/goal fallback match defaultSessionLabel's
"Droid chat" behavior; use the exact symbol names isGenericSessionTitle and
SHORT_TOOL_TYPE_LABELS to locate and modify the code.
apps/desktop/src/renderer/components/shared/permissionOptions.ts (1)

28-34: ⚠️ Potential issue | 🟠 Major

Persistent-identity Droid still falls back to the generic edit preset.

CLI-wrapped factory now has its own permission ladder, but this helper still only special-cases Anthropic. Any persistent-identity flow that uses the shared helper will label or create Droid’s guarded mode as legacy edit, which no longer matches the Droid-specific presets added below.

💡 Suggested fix
 export function resolvePersistentIdentityGuardedPermissionMode(opts: {
   family: string;
   isCliWrapped: boolean;
 }): AgentChatPermissionMode {
   const family = normalizePermissionFamily(opts.family);
-  return opts.isCliWrapped && family === "anthropic" ? "default" : "edit";
+  if (!opts.isCliWrapped) return "edit";
+  if (family === "anthropic") return "default";
+  if (family === "factory") return "default"; // or the explicit guarded Droid tier you want here
+  return "edit";
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/renderer/components/shared/permissionOptions.ts` around
lines 28 - 34, The helper resolvePersistentIdentityGuardedPermissionMode
currently only special-cases "anthropic" and returns "edit" for other families;
update its conditional to treat CLI-wrapped "factory" the same as "anthropic" so
Droid persistent-identity uses the Droid-specific guarded preset. In practice,
inside resolvePersistentIdentityGuardedPermissionMode (which calls
normalizePermissionFamily on opts.family), change the ternary/conditional that
checks opts.isCliWrapped && family === "anthropic" to also match the normalized
"factory" family (e.g., opts.isCliWrapped && (family === "anthropic" || family
=== "factory")) so the function returns "default" for CLI-wrapped factory flows.
apps/desktop/src/main/services/ai/aiIntegrationService.ts (1)

1276-1280: ⚠️ Potential issue | 🟠 Major

Droid is exposed in status, but its runtime connection is still missing.

availableProviders.droid and models.droid are now returned here, but buildRuntimeConnections() still builds only { claude, codex, cursor }. Any consumer reading runtimeConnections will see Droid as missing even when the provider is ready.

🛠️ Suggested fix
   const runtimeConnections: AiRuntimeConnections = {
     claude: toCliRuntimeConnection(args.providerConnections.claude),
     codex: toCliRuntimeConnection(args.providerConnections.codex),
     cursor: toCliRuntimeConnection(args.providerConnections.cursor),
+    droid: toCliRuntimeConnection(args.providerConnections.droid),
   };

Also applies to: 1342-1354

🤖 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.ts` around lines 1276
- 1280, The runtimeConnections object is missing Droid even though availability
and models now include it; update buildRuntimeConnections (and any places
constructing runtimeConnections) to include droid alongside claude, codex, and
cursor so that availableProviders.droid and models.droid are reflected in
runtimeConnections; locate the buildRuntimeConnections function and the code
constructing the availability/runtimeConnections objects and add the droid entry
using providerConnections.droid.runtimeAvailable (and the corresponding
model/runtime info) so consumers no longer see Droid as absent.
🧹 Nitpick comments (4)
apps/desktop/src/main/services/orchestrator/orchestratorQueries.ts (1)

281-297: Consider centralizing executor-kind normalization.

normalizeExecutorKind here and the mirrored allowlist in recovery logic now both require manual updates ("droid" in this PR). A shared helper/constant would reduce drift risk.

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

In `@apps/desktop/src/main/services/orchestrator/orchestratorQueries.ts` around
lines 281 - 297, The normalizeExecutorKind implementation duplicates an
allowlist that's also replicated in the recovery logic, causing drift when
updating executor kinds (e.g., "droid"); extract the canonical list and
normalization into a single shared helper or exported constant used by both
normalizeExecutorKind and the recovery logic so updates are made in one place;
specifically, create an exported constant (e.g., ALLOWED_EXECUTOR_KINDS) and a
single normalize function (e.g., normalizeExecutorKind or
normalizeExecutorKindFromList) and replace the local array/allowlist usages in
normalizeExecutorKind and the recovery code to reference this shared symbol.
apps/desktop/src/main/services/orchestrator/aiOrchestratorService.ts (1)

5497-5508: Consider centralizing executor-kind allowlists to prevent enum drift.

The same executor list is repeated in multiple spots. A single constant would reduce maintenance risk when providers change again.

♻️ Suggested refactor
+const PLAN_ADJUSTER_EXECUTOR_KINDS = ["claude", "codex", "cursor", "droid", "opencode", "manual"] as const;
+
 const adjustmentSchema = {
   type: "object",
   properties: {
@@
-              newExecutorKind: { type: "string", enum: ["claude", "codex", "cursor", "droid", "opencode", "manual"] },
+              newExecutorKind: { type: "string", enum: [...PLAN_ADJUSTER_EXECUTOR_KINDS] },
@@
-                  executorKind: { type: "string", enum: ["claude", "codex", "cursor", "droid", "opencode", "manual"] }
+                  executorKind: { type: "string", enum: [...PLAN_ADJUSTER_EXECUTOR_KINDS] }
@@
-        const executorKind = typeof newStep.executorKind === "string" &&
-          ["claude", "codex", "cursor", "droid", "opencode", "manual"].includes(newStep.executorKind)
+        const executorKind = typeof newStep.executorKind === "string" &&
+          PLAN_ADJUSTER_EXECUTOR_KINDS.includes(
+            newStep.executorKind as (typeof PLAN_ADJUSTER_EXECUTOR_KINDS)[number]
+          )
           ? (newStep.executorKind as OrchestratorExecutorKind)
           : ("opencode" as OrchestratorExecutorKind);

Also applies to: 5565-5568

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

In `@apps/desktop/src/main/services/orchestrator/aiOrchestratorService.ts` around
lines 5497 - 5508, The duplicated executor-kind enum should be centralized:
create a single constant (e.g., ALLOWED_EXECUTOR_KINDS or ExecutorKindEnum) and
reference it from the JSON schema definitions instead of repeating literals;
update the schema entries that use newExecutorKind and
newStep.properties.executorKind (and the other occurrence around the noted
5565-5568 block) to read their enum from that constant so changes to allowed
executors are made in one place.
apps/desktop/src/renderer/components/settings/ProvidersSection.tsx (1)

722-751: Consider extracting a shared CLI runtime card renderer.

This new card duplicates the Claude/Codex/Cursor card structure almost 1:1, which increases drift risk when status or messaging logic changes later.

🤖 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 722 - 751, The Factory Droid card duplicates the existing CLI runtime card
layout used for Claude/Codex/Cursor; extract a reusable component (e.g.,
CliRuntimeCard) and replace duplicated JSX in ProvidersSection with that
component. CliRuntimeCard should accept props: tool (from CLI_TOOLS), connection
(providerConnections[tool.cli]), isInitialCheckInFlight, and functions/values
used to compute UI like describeCredentialSource(connection),
getStatusTone(connection), buildCliMessage(tool, connection), and should render
ProviderLogo, status icon logic, credentialSourceDesc, and path exactly once.
Update ProvidersSection to call <CliRuntimeCard .../> for the Droid entry (and
reuse for other CLI entries) so status/messaging logic lives in one place and
you no longer repeat the section JSX.
apps/desktop/src/main/services/chat/droidModelsDiscovery.ts (1)

46-47: Key the discovery cache by executable path.

The cache is global, but this function takes droidPath. If ADE switches between bundled/user-installed Droid binaries, or tests invoke different fixtures, you'll reuse the wrong model list until the TTL expires.

♻️ Suggested refactor
-let cached: { at: number; models: DroidExecHelpModelRow[] } | null = null;
+const cache = new Map<string, { at: number; models: DroidExecHelpModelRow[] }>();
 const TTL_MS = 120_000;
@@
 async function listDroidModelsFromCli(droidPath: string): Promise<DroidExecHelpModelRow[]> {
   const now = Date.now();
-  if (cached && now - cached.at < TTL_MS) {
-    return cached.models;
+  const cached = cache.get(droidPath);
+  if (cached && now - cached.at < TTL_MS) {
+    return cached.models;
   }
@@
       const rows = parseDroidExecHelpModels(helpResult.stdout ?? "");
       if (rows.length) {
-        cached = { at: now, models: rows };
+        cache.set(droidPath, { at: now, models: rows });
         return rows;
       }
@@
           if (rows.length) {
-            cached = { at: now, models: rows };
+            cache.set(droidPath, { at: now, models: rows });
             return rows;
           }
@@
       if (bare.length >= 3) {
-        cached = { at: now, models: bare };
+        cache.set(droidPath, { at: now, models: bare });
         return bare;
       }
@@
-  cached = { at: now, models: [] };
+  cache.set(droidPath, { at: now, models: [] });
   return [];
 }
@@
 export function clearDroidCliModelsCache(): void {
-  cached = null;
+  cache.clear();
 }

Also applies to: 92-105, 141-145, 163-178

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

In `@apps/desktop/src/main/services/chat/droidModelsDiscovery.ts` around lines 46
- 47, The global cache variable cached (and TTL_MS usage) is keyed only by time
and will return models for the wrong executable when different droidPath values
are used; change the cache to be keyed by the executable path (e.g., replace
cached with a Map<string, { at: number; models: DroidExecHelpModelRow[] }>) and
update every place that reads/writes cached (the discovery logic that accepts
droidPath and the checks that compare TTL_MS) to lookup/insert by droidPath;
ensure when storing you record Date.now() in at and when reading you validate
TTL_MS per-path and fall back to fresh discovery if expired.
🤖 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/ade-cli/src/cli.ts`:
- Line 1516: The new droid-related flags used at the readValue call site
(droidPermissionMode: readValue(args, ["--droid-permission-mode",
"--droid-autonomy", "--autonomy"])) are value-taking flags but were not added to
VALUE_CARRIER_FLAGS; update the VALUE_CARRIER_FLAGS set/array to include
"--droid-permission-mode", "--droid-autonomy", and "--autonomy" so the CLI
parser treats them as value carriers and prevents misparsing of "--help" as a
flag value.

In `@apps/desktop/src/main/services/ai/aiIntegrationService.ts`:
- Around line 810-827: The current gate uses hasDroidCliAuth to decide whether
to run discoverDroidCliModelDescriptors, which prevents discovery when the user
has a FACTORY_API_KEY auth path but an unauthenticated CLI; update the condition
to also allow discovery when auth contains a factory API key. Concretely, add a
predicate (e.g., hasFactoryApiKey = auth.some(entry => entry.key ===
"FACTORY_API_KEY" || entry.type === "factory-api" /* adjust to real schema */))
and change the if to if (hasDroidCliAuth || hasFactoryApiKey) { ... } so
resolveDroidExecutable and discoverDroidCliModelDescriptors still run for
env/API-key-based auth while keeping the existing try/catch behavior.

In `@apps/desktop/src/main/services/ai/authDetector.ts`:
- Around line 1040-1058: The code is re-resolving the Droid binary with
resolveDroidExecutable and treating a "fallback-command" result as installed:
false, which discards a previously-detected runnable cmd/path (from
commandExists/commandPath); change the logic in the droid branch to prefer the
already-discovered executable path (the cmd or commandPath result) and only call
resolveDroidExecutable when no prior path is available; when
resolveDroidExecutable returns "fallback-command" do not mark installed false if
a prior path exists—use the prior path and call
inspectDroidCliPresence(resolvedPathOrPrior) to set
installed/authenticated/verified via inspectDroidCliPresence instead of hiding
PATH-only/custom installs.

In `@apps/desktop/src/main/services/chat/acpCliPool.ts`:
- Around line 77-96: The acquireAcpCliConnection function captures poolEpoch
into initEpoch but doesn't fail fast when poolEpoch advances (shutdown) during
retries; add an early check that compares poolEpoch to initEpoch at the top of
each attempt (after the backoff await and before creating/claiming pendingInit
or spawning new ACL processes) and immediately throw or return a failed/aborted
error when they differ so we don't spawn/init doomed CLI processes; ensure the
same guard is added in the other affected section referenced (around lines
200-202) that creates or uses pendingInit.

In `@apps/desktop/src/main/services/chat/acpHostClient.ts`:
- Around line 274-282: killTerminal currently only sends a one-time SIGTERM (via
signalChildProcessTree) and can leave processes running if they ignore SIGTERM;
update AcpHostTermState to include killTimer?: NodeJS.Timeout | null, then in
killTerminal set a timer (e.g., 5s) that will call signalChildProcessTree(...,
"SIGKILL") if the process hasn't exited, storing the timer on the terminal
state; ensure you clear and null out killTimer when the terminal's close/exit
handler runs (and when releaseTerminal or waitForTerminalExit cleans up) so
timers don't leak or double-kill, and guard against scheduling multiple timers
if killTerminal is called repeatedly.

In `@apps/desktop/src/main/services/chat/agentChatService.test.ts`:
- Around line 9784-9882: The test currently only asserts setSessionModel was
eventually called with the translated ACP id but doesn't ensure the untranslated
Droid help id was never sent; update the test to assert setSessionModel was
never called with the original help id string (e.g.,
"custom:claude-sonnet-4-6-thinking-32000") by checking the mock for absence of
that argument before/after service.sendMessage, using the existing
setSessionModel mock to verify the untranslated modelId was not passed at any
point.
- Around line 9682-9782: The test currently only checks
unstable_setSessionModel() was called but not that it happened before the first
prompt; update the mocks so unstable_setSessionModel (the setSessionModel fn)
records a sequence/order marker (e.g., increment mockState.sequenceCounter and
push the value to mockState.droidSetModelSeq) and modify the prompt mock (the
prompt fn that pushes to mockState.droidPromptCalls) to push its own sequence
number, then add an assertion that the setSessionModel sequence for the session
is less than the prompt sequence (i.e., mockState.droidSetModelSeq[0] <
mockState.droidPromptSeq[0]) to guarantee the model switch happened before the
first prompt.

In `@apps/desktop/src/main/services/chat/cursorAcpPool.ts`:
- Around line 97-112: Before deleting stale pool entries from cursorPools (the
branches checking staleOuter / existing for args.poolKey), call
clearCursorTerminalTimers(...) with the pooled object being evicted so any
pending terminal timers are cleared; specifically, in the first branch where
staleOuter && !hasActiveAcpCliPoolEntry(innerKey) invoke
clearCursorTerminalTimers(staleOuter.pooled or staleOuter) before
cursorPools.delete(args.poolKey), and likewise in the later branch where
existing is truthy (stale existing) call
clearCursorTerminalTimers(existing.pooled or existing) before
cursorPools.delete(args.poolKey). Ensure you still perform
acquireAcpCliConnection(acpOptions) and increment existing.ref only in the
live-connection branch.
- Around line 114-149: The init waiters can receive a stale pooled object if the
pool was torn down between awaiting init and taking a retain; after awaiting
init in the non-owner path (the code around init, pooled, and the non-owner
branch that calls acquireAcpCliConnection), validate that the pooled structure
is still current by comparing its generation with the live entry in cursorPools
(use cursorPools.get(args.poolKey) and the generation field) and only increment
ref or return the pooled if they match; if they don’t match, discard the awaited
pooled and repeat/acquire the current connection (call acquireAcpCliConnection
again or re-run the acquire logic) so the waiter always returns the live pooled
instance and increments its ref atomically.

In `@apps/desktop/src/main/services/chat/droidAcpPool.ts`:
- Around line 125-132: The returned generation can mismatch the awaited pooled
because after awaiting init (stored in pooled) the pool entry at
droidPools.get(args.poolKey) may have been replaced; modify init so it resolves
an object that includes the current generation (e.g., make init return { pooled,
generation }) or, if changing init is undesirable, immediately capture the
generation atomically by reading the entry inside the same init promise context;
then use that captured generation when returning instead of re-reading
droidPools after await; reference symbols: init, initOwner, pooled, generation,
droidPools, args.poolKey, acquireAcpCliConnection.

In `@apps/desktop/src/main/services/orchestrator/orchestratorService.ts`:
- Line 3826: defaultAdapterFor currently allows "droid" but the fallback logic
later only routes to Claude/Codex causing misrouting; update defaultAdapterFor
to not include "droid" (or add an explicit early check like if (kind ===
"droid") return null) so Droid requests are never sent through the Claude/Codex
fallback path and instead fall back to the shared-adapter existence check;
locate the check in the defaultAdapterFor function and remove "droid" from the
allowed kinds or add the explicit return for "droid".

In `@apps/desktop/src/main/services/orchestrator/permissionMapping.ts`:
- Line 106: The new default droid: "full-auto" can be incorrectly set for legacy
configs because the cli.mode normalization only maps to claude/codex; update the
normalization logic and mergeMissionPermissionConfig to also derive and
propagate a droid permission from legacy cli.mode using the same mapping rules
applied to claude/codex (i.e., when computing normalized permissions from
cli.mode, set the corresponding droid property rather than leaving droid at a
hardcoded "full-auto"), so legacy restrictive CLI settings flow through to the
droid key as well.

In `@apps/desktop/src/main/services/shared/utils.ts`:
- Around line 191-215: The timeout path calls terminateChildProcessTree(child,
null, 1_500) but drops its returned escalation timer; change that call to
capture the returned timer into hardResolveHandle (e.g., hardResolveHandle =
terminateChildProcessTree(...)) and ensure settle clears it (already clearing
hardResolveHandle via clearTimeout in settle) so the delayed SIGKILL can be
cancelled if the child exits after SIGTERM; keep references to timeoutHandle and
hardResolveHandle as NodeJS.Timeout | null and make sure no other timers are
leaked, leaving destroyChildProcessStreams(child), settle,
child.once("close"/"error") and the existing clearTimeout calls intact.

In `@apps/desktop/src/main/services/sync/syncRemoteCommandService.ts`:
- Around line 552-553: The current parsing logic can set
parsed.droidPermissionMode to null when asTrimmedString returns null; change
both occurrences (the assignment for parsed.droidPermissionMode at the first
block and the duplicate at lines ~677-678) so that when
value.droidPermissionMode is null/undefined you set parsed.droidPermissionMode =
undefined, and when asTrimmedString(...) returns null/empty you also map that to
undefined (e.g. capture the trimmed result and use (trimmed ?? undefined) cast
to AgentChatCreateArgs["droidPermissionMode"]); keep the existing cursorModeId
behavior intact if desired but apply the same null→undefined normalization for
any other droidPermissionMode assignments.

In `@apps/desktop/src/renderer/browserMock.ts`:
- Around line 376-386: The new inferMockChatProvider can return "droid" for
toolType values that don't end in "-chat", but isMockChatToolType still only
accepts cursor or values ending in "-chat", causing Droid sessions to be
filtered out; update isMockChatToolType to include the same Droid detection used
in inferMockChatProvider (e.g., accept toolType === "droid" or
toolType.startsWith("droid") / toolType === "droid-chat") so that tool types
mapped to "droid" by inferMockChatProvider are preserved in the browser mock
session list.

In `@apps/desktop/src/renderer/components/chat/AgentChatPane.tsx`:
- Around line 1609-1611: The droid autonomy should prefer a legacy
session-stored permission when session.droidPermissionMode is missing; update
the logic that calls setDroidPermissionMode so it falls back to
session.permissionMode before falling back to
initialNativeControls.droidPermissionMode (i.e., use session.droidPermissionMode
?? session.permissionMode ?? initialNativeControls.droidPermissionMode), keeping
setOpenCodePermissionMode and cursorModeId logic unchanged.

In `@apps/desktop/src/renderer/components/settings/UsageGuardrailsSection.tsx`:
- Around line 161-169: The warning condition in showEmptyQuotaWarning
incorrectly includes cliAuth.droid?.authAvailable but does not check any droid
windows, causing a false warning for Droid-only auth; update the condition in
showEmptyQuotaWarning to either remove cliAuth.droid?.authAvailable from the
initial auth check or add a droid window presence check (e.g., include a
droidWindows.length check alongside claudeWindows and codexWindows) so the logic
only triggers when an authenticated provider has zero corresponding windows.

In
`@apps/desktop/src/renderer/components/shared/providerModelSelectorGrouping.ts`:
- Around line 164-173: The classifyProviderGroup function now classifies models
with family === "factory" into the "droid" group, but there is no corresponding
provider metadata for the raw provider key "factory", causing fallback
label/color/order; add a "factory" entry to the provider metadata maps used for
rendering (e.g., the provider metadata/labels/colors/order objects/constants) so
that the provider key "factory" has the intended display label, color and sort
order; update the same provider maps where other providers are defined so
"factory" uses the Droid styling and ordering to avoid default fallbacks.

---

Outside diff comments:
In `@apps/desktop/src/main/services/ai/aiIntegrationService.ts`:
- Around line 1276-1280: The runtimeConnections object is missing Droid even
though availability and models now include it; update buildRuntimeConnections
(and any places constructing runtimeConnections) to include droid alongside
claude, codex, and cursor so that availableProviders.droid and models.droid are
reflected in runtimeConnections; locate the buildRuntimeConnections function and
the code constructing the availability/runtimeConnections objects and add the
droid entry using providerConnections.droid.runtimeAvailable (and the
corresponding model/runtime info) so consumers no longer see Droid as absent.

In `@apps/desktop/src/renderer/components/shared/permissionOptions.ts`:
- Around line 28-34: The helper resolvePersistentIdentityGuardedPermissionMode
currently only special-cases "anthropic" and returns "edit" for other families;
update its conditional to treat CLI-wrapped "factory" the same as "anthropic" so
Droid persistent-identity uses the Droid-specific guarded preset. In practice,
inside resolvePersistentIdentityGuardedPermissionMode (which calls
normalizePermissionFamily on opts.family), change the ternary/conditional that
checks opts.isCliWrapped && family === "anthropic" to also match the normalized
"factory" family (e.g., opts.isCliWrapped && (family === "anthropic" || family
=== "factory")) so the function returns "default" for CLI-wrapped factory flows.

In `@apps/desktop/src/renderer/lib/modelOptions.test.ts`:
- Around line 15-24: The test helper makeStatus now includes droid fields but
the suite lacks a Droid-specific assertion; update the relevant test in
modelOptions.test.ts to include a case where makeStatus returns detectedAuth:
['droid'] and models.droid contains one or more model entries, then assert that
the code under test (the function that builds model options) includes those
Droid model ids/options in its output (use the same assertions pattern as for
claude/codex/cursor to verify provider-specific inclusion). Reference
makeStatus, AiSettingsStatus, availableProviders, detectedAuth, models, and the
droid key when adding this assertion.

In `@apps/desktop/src/renderer/lib/sessions.ts`:
- Around line 52-65: Update the other session-label helpers to recognize the new
"droid-chat" tool type so Droid sessions behave like other chat providers: add
"droid-chat" to the logic in isGenericSessionTitle() and include a corresponding
entry ("Droid") in SHORT_TOOL_TYPE_LABELS (and any similar mappings) so the
optimistic/new session title, compact cards, and summary/goal fallback match
defaultSessionLabel's "Droid chat" behavior; use the exact symbol names
isGenericSessionTitle and SHORT_TOOL_TYPE_LABELS to locate and modify the code.

In `@apps/desktop/src/shared/types/config.ts`:
- Around line 1069-1082: The defaultProvider union for AiChatConfig still
excludes "droid", causing Droid not to be persistable: update the
AiChatConfig.defaultProvider type to include "droid" alongside "codex" |
"claude" | "cursor" | "opencode" | "last_used" and mirror that change in the
other occurrence noted (the second union around the later block), ensuring both
places accept "droid" so the shared IPC/preload/renderer types stay in sync with
AiSettingsStatus which already treats droid as first-class.

---

Nitpick comments:
In `@apps/desktop/src/main/services/chat/droidModelsDiscovery.ts`:
- Around line 46-47: The global cache variable cached (and TTL_MS usage) is
keyed only by time and will return models for the wrong executable when
different droidPath values are used; change the cache to be keyed by the
executable path (e.g., replace cached with a Map<string, { at: number; models:
DroidExecHelpModelRow[] }>) and update every place that reads/writes cached (the
discovery logic that accepts droidPath and the checks that compare TTL_MS) to
lookup/insert by droidPath; ensure when storing you record Date.now() in at and
when reading you validate TTL_MS per-path and fall back to fresh discovery if
expired.

In `@apps/desktop/src/main/services/orchestrator/aiOrchestratorService.ts`:
- Around line 5497-5508: The duplicated executor-kind enum should be
centralized: create a single constant (e.g., ALLOWED_EXECUTOR_KINDS or
ExecutorKindEnum) and reference it from the JSON schema definitions instead of
repeating literals; update the schema entries that use newExecutorKind and
newStep.properties.executorKind (and the other occurrence around the noted
5565-5568 block) to read their enum from that constant so changes to allowed
executors are made in one place.

In `@apps/desktop/src/main/services/orchestrator/orchestratorQueries.ts`:
- Around line 281-297: The normalizeExecutorKind implementation duplicates an
allowlist that's also replicated in the recovery logic, causing drift when
updating executor kinds (e.g., "droid"); extract the canonical list and
normalization into a single shared helper or exported constant used by both
normalizeExecutorKind and the recovery logic so updates are made in one place;
specifically, create an exported constant (e.g., ALLOWED_EXECUTOR_KINDS) and a
single normalize function (e.g., normalizeExecutorKind or
normalizeExecutorKindFromList) and replace the local array/allowlist usages in
normalizeExecutorKind and the recovery code to reference this shared symbol.

In `@apps/desktop/src/renderer/components/settings/ProvidersSection.tsx`:
- Around line 722-751: The Factory Droid card duplicates the existing CLI
runtime card layout used for Claude/Codex/Cursor; extract a reusable component
(e.g., CliRuntimeCard) and replace duplicated JSX in ProvidersSection with that
component. CliRuntimeCard should accept props: tool (from CLI_TOOLS), connection
(providerConnections[tool.cli]), isInitialCheckInFlight, and functions/values
used to compute UI like describeCredentialSource(connection),
getStatusTone(connection), buildCliMessage(tool, connection), and should render
ProviderLogo, status icon logic, credentialSourceDesc, and path exactly once.
Update ProvidersSection to call <CliRuntimeCard .../> for the Droid entry (and
reuse for other CLI entries) so status/messaging logic lives in one place and
you no longer repeat the section JSX.
🪄 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: 7689d5a4-8886-44bc-9034-980f6af1d22f

📥 Commits

Reviewing files that changed from the base of the PR and between 7ccdab3 and 12b3ab6.

⛔ Files ignored due to path filters (2)
  • apps/desktop/package-lock.json is excluded by !**/package-lock.json, !**/package-lock.json
  • apps/desktop/src/renderer/assets/provider-logos/droid.svg is excluded by !**/*.svg
📒 Files selected for processing (69)
  • apps/ade-cli/src/adeRpcServer.ts
  • apps/ade-cli/src/bootstrap.ts
  • apps/ade-cli/src/cli.ts
  • apps/desktop/package.json
  • apps/desktop/src/main/main.ts
  • apps/desktop/src/main/services/ai/agentExecutor.ts
  • apps/desktop/src/main/services/ai/aiIntegrationService.test.ts
  • apps/desktop/src/main/services/ai/aiIntegrationService.ts
  • apps/desktop/src/main/services/ai/authDetector.test.ts
  • apps/desktop/src/main/services/ai/authDetector.ts
  • apps/desktop/src/main/services/ai/droidExecutable.test.ts
  • apps/desktop/src/main/services/ai/droidExecutable.ts
  • apps/desktop/src/main/services/ai/providerConnectionStatus.ts
  • apps/desktop/src/main/services/chat/acpCliPool.ts
  • apps/desktop/src/main/services/chat/acpHostClient.ts
  • apps/desktop/src/main/services/chat/agentChatService.test.ts
  • apps/desktop/src/main/services/chat/agentChatService.ts
  • apps/desktop/src/main/services/chat/cursorAcpPool.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/config/projectConfigService.ts
  • apps/desktop/src/main/services/ipc/registerIpc.ts
  • apps/desktop/src/main/services/orchestrator/aiOrchestratorService.test.ts
  • apps/desktop/src/main/services/orchestrator/aiOrchestratorService.ts
  • apps/desktop/src/main/services/orchestrator/orchestratorQueries.ts
  • apps/desktop/src/main/services/orchestrator/orchestratorService.ts
  • apps/desktop/src/main/services/orchestrator/permissionMapping.ts
  • apps/desktop/src/main/services/orchestrator/providerOrchestratorAdapter.ts
  • apps/desktop/src/main/services/orchestrator/recoveryService.ts
  • apps/desktop/src/main/services/orchestrator/workerDeliveryService.ts
  • apps/desktop/src/main/services/orchestrator/workerTracking.ts
  • apps/desktop/src/main/services/pty/ptyService.test.ts
  • apps/desktop/src/main/services/pty/ptyService.ts
  • apps/desktop/src/main/services/sessions/sessionService.test.ts
  • apps/desktop/src/main/services/sessions/sessionService.ts
  • apps/desktop/src/main/services/shared/utils.test.ts
  • apps/desktop/src/main/services/shared/utils.ts
  • apps/desktop/src/main/services/sync/syncRemoteCommandService.ts
  • apps/desktop/src/renderer/assets/provider-logos/README.md
  • apps/desktop/src/renderer/browserMock.ts
  • apps/desktop/src/renderer/components/chat/AgentChatComposer.test.tsx
  • apps/desktop/src/renderer/components/chat/AgentChatComposer.tsx
  • apps/desktop/src/renderer/components/chat/AgentChatMessageList.tsx
  • apps/desktop/src/renderer/components/chat/AgentChatPane.tsx
  • apps/desktop/src/renderer/components/missions/CreateMissionDialog.tsx
  • apps/desktop/src/renderer/components/missions/WorkerPermissionsEditor.tsx
  • apps/desktop/src/renderer/components/missions/useMissionsStore.ts
  • apps/desktop/src/renderer/components/settings/ProvidersSection.test.tsx
  • apps/desktop/src/renderer/components/settings/ProvidersSection.tsx
  • apps/desktop/src/renderer/components/settings/UsageGuardrailsSection.tsx
  • apps/desktop/src/renderer/components/shared/ModelCatalogPanel.tsx
  • apps/desktop/src/renderer/components/shared/ProviderLogos.tsx
  • apps/desktop/src/renderer/components/shared/permissionOptions.test.ts
  • apps/desktop/src/renderer/components/shared/permissionOptions.ts
  • apps/desktop/src/renderer/components/shared/providerModelSelectorGrouping.test.ts
  • apps/desktop/src/renderer/components/shared/providerModelSelectorGrouping.ts
  • apps/desktop/src/renderer/components/terminals/ToolLogos.tsx
  • apps/desktop/src/renderer/lib/modelOptions.test.ts
  • apps/desktop/src/renderer/lib/modelOptions.ts
  • apps/desktop/src/renderer/lib/sessions.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/missions.ts
  • apps/desktop/src/shared/types/models.ts
  • apps/desktop/src/shared/types/orchestrator.ts
  • apps/desktop/src/shared/types/sessions.ts

Comment thread apps/ade-cli/src/cli.ts
Comment thread apps/desktop/src/main/services/ai/aiIntegrationService.ts
Comment thread apps/desktop/src/main/services/ai/authDetector.ts
Comment thread apps/desktop/src/main/services/chat/acpCliPool.ts
Comment on lines +274 to +282
async killTerminal(params: KillTerminalRequest): Promise<void> {
const t = terminals.get(params.terminalId);
if (t && !t.exited) {
try {
signalChildProcessTree(t.proc, "SIGTERM");
} catch {
// ignore
}
}
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

killTerminal() does not actually guarantee termination.

This only sends SIGTERM once. Any command that traps or ignores SIGTERM will keep running until some later waitForTerminalExit() or releaseTerminal() path happens to clean it up, which makes killTerminal() unreliable for stubborn terminals.

🛠️ Suggested direction
     async killTerminal(params: KillTerminalRequest): Promise<void> {
       const t = terminals.get(params.terminalId);
       if (t && !t.exited) {
         try {
-          signalChildProcessTree(t.proc, "SIGTERM");
+          // Store the returned timer on terminal state and clear it from the
+          // close handler so SIGTERM-resistant processes are force-killed.
+          t.killTimer = terminateChildProcessTree(t.proc, t.killTimer ?? null, 1_500);
         } catch {
           // ignore
         }
       }
     },

You'd also want to add killTimer?: NodeJS.Timeout | null to AcpHostTermState and clear it in the terminal close handler.

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

In `@apps/desktop/src/main/services/chat/acpHostClient.ts` around lines 274 - 282,
killTerminal currently only sends a one-time SIGTERM (via
signalChildProcessTree) and can leave processes running if they ignore SIGTERM;
update AcpHostTermState to include killTimer?: NodeJS.Timeout | null, then in
killTerminal set a timer (e.g., 5s) that will call signalChildProcessTree(...,
"SIGKILL") if the process hasn't exited, storing the timer on the terminal
state; ensure you clear and null out killTimer when the terminal's close/exit
handler runs (and when releaseTerminal or waitForTerminalExit cleans up) so
timers don't leak or double-kill, and guard against scheduling multiple timers
if killTerminal is called repeatedly.

Comment on lines 552 to 553
if ("droidPermissionMode" in value) parsed.droidPermissionMode = value.droidPermissionMode == null ? undefined : asTrimmedString(value.droidPermissionMode) as AgentChatCreateArgs["droidPermissionMode"];
if ("cursorModeId" in value) parsed.cursorModeId = value.cursorModeId == null ? null : asTrimmedString(value.cursorModeId) ?? null;
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 | 🟡 Minor

Normalize empty droidPermissionMode to undefined (not null).

Line 552 and Line 677 can assign null at runtime (from asTrimmedString) even though the logic intends nullish → undefined. This can leak unexpected null into downstream session/update paths.

🔧 Proposed fix
-  if ("droidPermissionMode" in value) parsed.droidPermissionMode = value.droidPermissionMode == null ? undefined : asTrimmedString(value.droidPermissionMode) as AgentChatCreateArgs["droidPermissionMode"];
+  if ("droidPermissionMode" in value) {
+    const droidPermissionMode = asTrimmedString(value.droidPermissionMode);
+    parsed.droidPermissionMode = droidPermissionMode == null
+      ? undefined
+      : droidPermissionMode as AgentChatCreateArgs["droidPermissionMode"];
+  }

-  if ("droidPermissionMode" in value) parsed.droidPermissionMode = value.droidPermissionMode == null ? undefined : asTrimmedString(value.droidPermissionMode) as AgentChatUpdateSessionArgs["droidPermissionMode"];
+  if ("droidPermissionMode" in value) {
+    const droidPermissionMode = asTrimmedString(value.droidPermissionMode);
+    parsed.droidPermissionMode = droidPermissionMode == null
+      ? undefined
+      : droidPermissionMode as AgentChatUpdateSessionArgs["droidPermissionMode"];
+  }

Also applies to: 677-678

🤖 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.ts` around lines
552 - 553, The current parsing logic can set parsed.droidPermissionMode to null
when asTrimmedString returns null; change both occurrences (the assignment for
parsed.droidPermissionMode at the first block and the duplicate at lines
~677-678) so that when value.droidPermissionMode is null/undefined you set
parsed.droidPermissionMode = undefined, and when asTrimmedString(...) returns
null/empty you also map that to undefined (e.g. capture the trimmed result and
use (trimmed ?? undefined) cast to AgentChatCreateArgs["droidPermissionMode"]);
keep the existing cursorModeId behavior intact if desired but apply the same
null→undefined normalization for any other droidPermissionMode assignments.

Comment thread apps/desktop/src/renderer/browserMock.ts
Comment thread apps/desktop/src/renderer/components/chat/AgentChatPane.tsx
…dress 22 review comments

CI: authDetector.test.ts 'treats droid exec list-tools as a valid authenticated probe' was failing because the test mocks did not cover the new droid resolution path; fixed mock setup so installed/verified resolve.

Review: 22 actionable comments addressed across Greptile (3 P1/P2), Capy (1), and CodeRabbit (18) — covers async readFileSync, concurrency guard, dead code, ACP pool races, droid auth gating, permission propagation, terminal kill escalation, generic adapter fallback, browserMock helpers, AgentChatPane legacy autonomy, UsageGuardrails droid signal, providerModelSelectorGrouping factory metadata.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@arul28
Copy link
Copy Markdown
Owner Author

arul28 commented Apr 28, 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