feat(providers): add Devin CLI provider via ACP - #11239
adampeterhiggins wants to merge 7 commits into
Conversation
Add a first-class Devin provider so T3 Code can run Devin's SWE models through the local `devin` CLI's ACP server. - Contracts: `DevinSettings` (enabled, binaryPath, customModels), provider kind, "adaptive" default model, "swe-1-6-fast" text-generation default. - `DevinAcpSupport` builds `devin --permission-mode <mode> acp` spawn args, maps T3 runtime modes onto Devin's session modes (accept-edits, smart, ask, plan, bypass), and applies model selection through the session's `model` config option. - `DevinAdapter` runs sessions over `AcpSessionRuntime`: prompt streaming, steering, interrupt/stop, permission requests via `request.opened`, and `session/elicitation` mapped to user-input questions (form mode only; URL-mode elicitations are cancelled). - `DevinProvider` probes `devin --version` / `auth status` and discovers models via `devin models list --format json` with a static SWE fallback catalog; `devin update` provides maintenance. - `DevinDriver` registers the provider (multi-instance capable) and `DevinTextGeneration` reuses the ACP runtime in `ask` mode for commit messages, PR content, branch names, and thread titles. - Web/mobile: provider icon, driver metadata, settings search, composer draft persistence, and status ordering. Docs: providers-devin page. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
The "Your agents" step only showed Claude Code and Codex. Add Devin as a primary agent card with its install command (cli.devin.ai install.sh / setup.ps1) and `devin auth login` for the sign-in terminal. Since Devin is opt-in, a disabled card now offers an Enable button that flips the providerInstances envelope instead of a dead "Disabled" label. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Replace the placeholder "D" letterform with the official Devin mark (three interlocking hexagons, 24x24, currentColor) sourced from the published vector used by @lobehub/icons. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
The picker's display-name helper strips a leading subProvider qualifier
from the model name. Devin variant labels are already fully qualified
("Inkling High", "SWE-2 Max"), so tagging them with the family slug made
rows render as just the effort suffix ("High") with "Devin · inkling" as
the subtitle. Drop subProvider from the catalog rows.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
… options Devin encodes effort, speed, and context in the model uid itself (`claude-opus-5-high-fast`, `glm-5-2-max-1m`), so the picker showed one row per variant — ~215 entries — instead of one row per model with selectable axes like other providers. `devinModelCatalog` now parses each variant uid right-to-left (context, speed, effort suffixes, `-`/`_` separators) and groups variants by base into a single `ServerProviderModel` per family with `effort`/`speed`/ `context` option descriptors. Bare variants contribute a "Default" effort choice; variant uids are kept as aliases so stored flat selections still resolve. The adapter folds dim selections back into a concrete advertised uid by parsed-dims matching rather than string composition, so irregular uids (`MODEL_GPT_5_2_LOW`, `claude-5-fable-*`) and missing combinations relax instead of failing the turn. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
The registry's snapshot merge retained all previously seen models for every driver except codex/antigravity/opencode, so Devin's old per-variant rows survived each probe alongside the grouped family rows. Give Devin the same rule: a ready probe's discovered list replaces the inventory, while warning/error/disabled/unauthenticated snapshots keep the last good catalog. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Families like gpt-5.1 and claude-sonnet-4.5 carry their effort tiers in
uids that share no base (`MODEL_PRIVATE_12..15`, `MODEL_PRIVATE_2/3`), so
uid-suffix parsing left them as separate rows. Variants in one family are
the same model by definition — when the uids can't prove it, parse the
label remainder ("GPT-5.1 Low Thinking" → low) and emit a single effort
select whose choices carry the concrete uid. The resolver now passes a
dim selection that is itself an advertised model value straight through,
so opaque families resolve without a shared base.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (34)
📝 WalkthroughWalkthroughThis PR adds Devin as a built-in provider. It adds settings, model catalogs, ACP sessions, provider discovery, text generation, onboarding support, icons, persistence, tests, and documentation. ChangesDevin provider integration
Estimated code review effort: 5 (Critical) | ~90 minutes Change: Feature Sequence Diagram(s)sequenceDiagram
participant T3Code
participant DevinDriver
participant DevinProvider
participant DevinAdapter
participant DevinCLI
T3Code->>DevinDriver: Create Devin provider instance
DevinDriver->>DevinProvider: Check CLI status and models
DevinProvider->>DevinCLI: Run version, auth, and model commands
DevinProvider-->>DevinDriver: Return provider snapshot
T3Code->>DevinAdapter: Start ACP session
DevinAdapter->>DevinCLI: Spawn devin acp
DevinCLI-->>DevinAdapter: Stream ACP events
DevinAdapter-->>T3Code: Emit turns, content, and approval events
Suggested reviewers: ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
| for (const selection of input.selections ?? []) { | ||
| if (isDevinModelDimOptionId(selection.id)) continue; | ||
| const option = configOptions.find((candidate) => candidate.id === selection.id); | ||
| if (!option || option.id === "model") continue; |
There was a problem hiding this comment.
🟠 High acp/DevinAcpSupport.ts:249
An approval-required turn can run with bypass instead of requiring approval when selections contain { id: "mode", value: "bypass" }. applyDevinAcpModelSelection forwards mode through setConfigOption after spawning with normal, while resolveDevinModeId does not replace an already-writable mode; exclude mode here so the runtime-mode resolver remains its sole owner.
- if (!option || option.id === "model") continue;
+ if (!option || option.id === "model" || option.id === "mode") continue;🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/acp/DevinAcpSupport.ts around line 249:
An approval-required turn can run with `bypass` instead of requiring approval when selections contain `{ id: "mode", value: "bypass" }`. `applyDevinAcpModelSelection` forwards `mode` through `setConfigOption` after spawning with `normal`, while `resolveDevinModeId` does not replace an already-writable mode; exclude `mode` here so the runtime-mode resolver remains its sole owner.
| ]; | ||
| const { enabled: _legacyEnabled, ...legacyConfigRest } = legacyConfig ?? {}; | ||
| updateSettings({ | ||
| providerInstances: { |
There was a problem hiding this comment.
🟡 Medium onboarding/WelcomeWizard.tsx:702
Enabling two disabled agents before the first settings update is reflected causes the later update to replace providerInstances with a stale snapshot and silently remove the earlier enabled envelope. enableProvider spreads settings.providerInstances captured during render, while updateSettings replaces the entire map; merge each entry against the latest settings or serialize these updates so concurrent enables are preserved.
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/onboarding/WelcomeWizard.tsx around line 702:
Enabling two disabled agents before the first settings update is reflected causes the later update to replace `providerInstances` with a stale snapshot and silently remove the earlier enabled envelope. `enableProvider` spreads `settings.providerInstances` captured during render, while `updateSettings` replaces the entire map; merge each entry against the latest settings or serialize these updates so concurrent enables are preserved.
| if (!wanted.hasAny && input.advertisedValues.includes(model)) return model; | ||
|
|
||
| const wantedBase = normalizeDevinModelId(model); | ||
| const candidates = input.advertisedValues.filter( | ||
| (uid) => parseDevinModelUid(uid).base === wantedBase, | ||
| ); | ||
| if (candidates.length === 0) return model; | ||
|
|
||
| if (!wanted.hasAny) { | ||
| // No dims selected: keep the session's current value when it already | ||
| // belongs to this family, else the bare variant, else first advertised. | ||
| if (input.currentValue && candidates.includes(input.currentValue)) { | ||
| return input.currentValue; | ||
| } | ||
| const bare = candidates.find((uid) => { | ||
| const dims = parseDevinModelUid(uid); | ||
| return dims.effort === undefined && dims.speed === undefined && dims.context === undefined; | ||
| }); | ||
| return bare ?? candidates[0]!; |
There was a problem hiding this comment.
🟡 Medium provider/devinModelCatalog.ts:504
When no non-default dimensions are selected, resolveDevinModelUid resets a session from currentValue such as glm-5-2-max to the grouped model slug glm-5-2. The early exact-match return at line 504 prevents the later currentValue preservation branch from running; defer the model pass-through until after checking whether currentValue belongs to the family.
- if (!wanted.hasAny && input.advertisedValues.includes(model)) return model;
-
const wantedBase = normalizeDevinModelId(model);
@@
if (input.currentValue && candidates.includes(input.currentValue)) {
return input.currentValue;
}
+ if (input.advertisedValues.includes(model)) return model;
const bare = candidates.find((uid) => {🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/devinModelCatalog.ts around lines 504-522:
When no non-default dimensions are selected, `resolveDevinModelUid` resets a session from `currentValue` such as `glm-5-2-max` to the grouped `model` slug `glm-5-2`. The early exact-match return at line 504 prevents the later `currentValue` preservation branch from running; defer the model pass-through until after checking whether `currentValue` belongs to the family.
| }); | ||
|
|
||
| const DEVIN_PLAN_MODE_ALIASES = ["plan", "architect"]; | ||
| const DEVIN_READ_ONLY_MODE_IDS = new Set(["plan", "ask"]); |
There was a problem hiding this comment.
🟠 High acp/DevinAcpSupport.ts:121
An approval-required turn remains in read-only architect mode after a plan turn, so normal editing requests cannot proceed. architect is accepted by DEVIN_PLAN_MODE_ALIASES but missing from DEVIN_READ_ONLY_MODE_IDS, preventing the restore path from selecting a writable mode; add it to the read-only IDs.
| const DEVIN_READ_ONLY_MODE_IDS = new Set(["plan", "ask"]); | |
| const DEVIN_READ_ONLY_MODE_IDS = new Set(["plan", "ask", "architect"]); |
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/acp/DevinAcpSupport.ts around line 121:
An approval-required turn remains in read-only `architect` mode after a plan turn, so normal editing requests cannot proceed. `architect` is accepted by `DEVIN_PLAN_MODE_ALIASES` but missing from `DEVIN_READ_ONLY_MODE_IDS`, preventing the restore path from selecting a writable mode; add it to the read-only IDs.
| // A sendTurn while a prompt is in flight is a steer: the agent folds | ||
| // the new prompt into the ongoing work, so the active turn id is | ||
| // reused instead of opening a new turn. | ||
| const steeringTurnId = ctx.promptsInFlight > 0 ? ctx.activeTurnId : undefined; |
There was a problem hiding this comment.
🟠 High Layers/DevinAdapter.ts:807
A concurrent second sendTurn creates a separate turnId and emits another turn.started, leaving the first turn without a terminal event and attributing streamed output to the wrong turn. ctx.promptsInFlight is incremented before ctx.activeTurnId is assigned, so the second call sees an active prompt but no active ID; reserve ctx.activeTurnId immediately after choosing turnId.
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/Layers/DevinAdapter.ts around line 807:
A concurrent second `sendTurn` creates a separate `turnId` and emits another `turn.started`, leaving the first turn without a terminal event and attributing streamed output to the wrong turn. `ctx.promptsInFlight` is incremented before `ctx.activeTurnId` is assigned, so the second call sees an active prompt but no active ID; reserve `ctx.activeTurnId` immediately after choosing `turnId`.
ApprovabilityVerdict: Not approved Macroscope's review found this PR not approvable — This 4,038-line change introduces a new production ACP provider with external process execution, authentication, permissions, sessions, model discovery, onboarding, and text generation, along with new product defaults. It also adds a static-analysis suppression and retains unresolved Medium/High findings involving permission enforcement, concurrency, model selection, and settings updates. Not approved because:
Adjust the Minimum Blocking Severity for this repo — including turning it Off — in Settings. You can add or adjust custom eligibility rules. Learn more. |
Summary
devin acpover Agent Client Protocol, same architecture as the Cursor provider): sessions, streaming, steering, interrupt/stop, permission requests, elicitation handling, and text generation.claude-opus-5-high-fast,glm-5-2-max-1m,MODEL_GPT_5_2_LOW), sodevinModelCataloggroupsdevin models list --format jsoninto one picker row per family witheffort/speed/contextoption descriptors. Families whose variant uids share no base (MODEL_PRIVATE_*) are grouped via label-derived dims, with the concrete uid carried in the option choice. At dispatch the adapter resolves{ model, effort, speed, context }back to a concrete advertised uid by parsed-dims matching — irregular uids and missing combinations relax instead of failing the turn.providers.devin, opt-in like Cursor/Grok), onboarding wizard entry with install/sign-in actions, official Devin mark on web and mobile, docs page.Details
devin --model <uid> --permission-mode <mode> acp; mode picks the closest advertised ACP mode (accept-edits / smart / ask / plan / bypass).devin --version+devin auth status; ACP advertisedevin-browserauth.session/request_permissionmaps torequest.openedwith risk classification;session/elicitationmaps touser-input.requested(URL-mode elicitations are cancelled — chat can't answer them).askmode withswe-2family default.swe-2-high) resolve to grouped rows via aliases.Test plan
apps/serverfocused tests: catalog grouping/resolution, ACP support (spawn args, mode mapping, model selection), adapter prompt flow, provider snapshot/auth parsing — 39+ testsProviderRegistrymerge tests for stale Devin rowsdevin acp: session start, model apply, streaming deltas, permission promptGenerated with Devin
Summary by CodeRabbit
New Features
Documentation