diff --git a/gui/public/provider-icons/README.md b/gui/public/provider-icons/README.md
index 235fa7bf27..39be97558e 100644
--- a/gui/public/provider-icons/README.md
+++ b/gui/public/provider-icons/README.md
@@ -101,6 +101,24 @@ at 20px.
`viewBox="0 0 823 823"` by centering the 823x806 trace. Named
`hermes-agent` rather than `hermes` because Hermes is also a provider name
and this directory is one flat namespace.
+- `devin.svg` — fetched 2026-09-12 from `https://docs.devin.ai/logo/light.svg`,
+ Devin's own documentation logo, and CROPPED to its symbol. The source is a
+ 160x24 horizontal lockup: the three-leaf mark occupies the left 24 units and
+ the `devin` wordmark starts at x=34.2, so a `viewBox="0 0 24 24"` window keeps
+ the whole symbol and excludes every wordmark path. The thirteen symbol paths
+ and all seven gradients are verbatim — nothing is translated, so the
+ `userSpaceOnUse` gradient coordinates stay valid — and only the two wordmark
+ paths are dropped. Cropping a lockup to its symbol is what `cursor-color.svg`
+ and `gajae-code.svg` already do; the alternative here was a wordmark in a 20px
+ box, which this file refuses.
+
+ `devin.ai/favicon.svg` sits behind a Vercel security checkpoint that answers
+ 429 with an HTML page, and `cognition.ai` publishes only a raster favicon, so
+ the docs site is the reachable first-party vector. Windsurf does publish
+ `windsurf.com/favicon.svg` — a plated `W` — but that names the retired brand
+ for a provider labelled Cognition. Multi-colour gradient, so it is drawn as an
+ image and is not a candidate for the masked set.
+
- `gajae-code.svg` — traced 2026-08-31 from `Yeachan-Heo/gajae-code`
`assets/character.png` (3190496 bytes, 1550x2048 RGBA), the mascot. No SVG
exists upstream: `assets/` and `docs/` hold only raster, `public/` is a 404,
diff --git a/gui/public/provider-icons/devin.svg b/gui/public/provider-icons/devin.svg
new file mode 100644
index 0000000000..c0620e2602
--- /dev/null
+++ b/gui/public/provider-icons/devin.svg
@@ -0,0 +1,49 @@
+
diff --git a/gui/src/provider-icons.ts b/gui/src/provider-icons.ts
index 99f46a93c4..e27572e45c 100644
--- a/gui/src/provider-icons.ts
+++ b/gui/src/provider-icons.ts
@@ -13,6 +13,15 @@ const PROVIDER_ICON_ALIASES: Record = {
commandcode: "commandcode-color.svg",
cursor: "cursor-color.svg",
deepseek: "deepseek-color.svg",
+ /*
+ * One mark for both Devin providers. `devin` is Cognition's cloud, reached
+ * through the Windsurf sign-in, and `devin-cli` drives the installed Devin
+ * CLI; they are two transports into the same product, the meta-model/meta-muse
+ * shape. Windsurf still publishes its own `W` app icon, but showing it next
+ * to a row labelled Cognition would name the retired brand.
+ */
+ devin: "devin.svg",
+ "devin-cli": "devin.svg",
firepass: "firepass-color.svg",
fireworks: "fireworks-color.svg",
github: "github-copilot-color.svg",
@@ -124,6 +133,8 @@ const PROVIDER_DISPLAY_NAMES: Record = {
xiaomi: "Xiaomi",
cursor: "Cursor",
deepseek: "DeepSeek",
+ devin: "Cognition (Devin/Windsurf)",
+ "devin-cli": "Devin CLI",
github: "GitHub",
"github-copilot": "GitHub Copilot",
"gitlab-duo": "GitLab Duo",
diff --git a/src/providers/registry.ts b/src/providers/registry.ts
index 27bc1738ba..a46e357390 100644
--- a/src/providers/registry.ts
+++ b/src/providers/registry.ts
@@ -1331,7 +1331,15 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
baseUrl: "https://cli.devin.ai",
authKind: "local",
featured: false,
- dashboardPreset: false,
+ // Reachable from the dashboard's add-provider list. `derive.ts` builds that
+ // list from `featured || authKind === "key" || dashboardPreset`, and this
+ // provider is none of the first two, so without this flag the only way to
+ // add it was to hand-edit config.json — which is how it came to be missing
+ // from a picker that already had the cloud `devin` row. The other local
+ // providers (ollama, vLLM, LM Studio) are reachable through `featured`;
+ // this one stays out of the featured strip because it needs an installed
+ // CLI and a completed `devin auth login` before it can answer anything.
+ dashboardPreset: true,
note: "Drives the locally installed Devin CLI over the Agent Client Protocol (`devin acp`, newline-delimited JSON-RPC on stdio). Requires the CLI on PATH and a completed `devin auth login`; no API key is stored by opencodex. Set OPENCODEX_DEVIN_CLI_BIN to point at a specific build, and OPENCODEX_DEVIN_CLI_ALLOW_TOOLS=1 to let the CLI read and write files — the default is to refuse.",
models: [...DEVIN_CLI_MODELS],
defaultModel: DEVIN_CLI_DEFAULT_MODEL,
diff --git a/tests/providers/devin-cli-adapter.test.ts b/tests/providers/devin-cli-adapter.test.ts
index 270832cfa1..91bb4c2464 100644
--- a/tests/providers/devin-cli-adapter.test.ts
+++ b/tests/providers/devin-cli-adapter.test.ts
@@ -13,6 +13,7 @@ import {
import { DEVIN_CLI_BIN_ENV, resolveDevinCliBinary } from "../../src/adapters/devin-cli/binary";
import { createDevinCliAdapter } from "../../src/adapters/devin-cli/adapter";
import { PROVIDER_REGISTRY } from "../../src/providers/registry";
+import { formatProviderDisplayName, providerIconSrc } from "../../gui/src/provider-icons";
import type { AdapterEvent, OcxParsedRequest } from "../../src/types";
import { EventEmitter } from "node:events";
import { PassThrough } from "node:stream";
@@ -25,9 +26,23 @@ describe("devin-cli registration", () => {
// The installed CLI carries its own credentials from `devin auth login`,
// so the proxy must never ask for or hold a key for this provider.
expect(entry?.authKind).toBe("local");
- expect(entry?.dashboardPreset).toBe(false);
+ // Addable from the dashboard. It is neither `featured` nor key-auth, so this
+ // flag is the only thing that puts it in the add-provider list; without it
+ // the provider existed but could only be reached by hand-editing config.
+ expect(entry?.dashboardPreset).toBe(true);
expect(createDevinCliAdapter({ adapter: "devin-cli", baseUrl: "https://cli.devin.ai" }).name).toBe("devin-cli");
});
+
+ test("both Devin providers render the Devin mark and a readable name", () => {
+ // Neither id had an icon alias, so the dashboard drew a coloured initial
+ // tile for both, and the title-cased fallback turned the local one into
+ // "Devin Cli".
+ expect(providerIconSrc("devin")).toBe("/provider-icons/devin.svg");
+ expect(providerIconSrc("devin-cli")).toBe("/provider-icons/devin.svg");
+ const englishT = ((_key: string, fallback?: string) => fallback ?? "") as Parameters[1];
+ expect(formatProviderDisplayName("devin", englishT)).toBe("Cognition (Devin/Windsurf)");
+ expect(formatProviderDisplayName("devin-cli", englishT)).toBe("Devin CLI");
+ });
});
describe("acp handshake frames", () => {