diff --git a/docs/adapters/creating-an-adapter.md b/docs/adapters/creating-an-adapter.md index 2f4711aa150..c0f43ff8a2b 100644 --- a/docs/adapters/creating-an-adapter.md +++ b/docs/adapters/creating-an-adapter.md @@ -54,7 +54,7 @@ my-adapter/ # external plugin ```ts export const type = "my_agent"; // snake_case, globally unique -export const label = "My Agent (local)"; +export const label = "My Agent"; export const models = [ { id: "model-a", label: "Model A" }, ]; diff --git a/docs/adapters/external-adapters.md b/docs/adapters/external-adapters.md index 522679f667c..3a6b4ee6c7e 100644 --- a/docs/adapters/external-adapters.md +++ b/docs/adapters/external-adapters.md @@ -116,7 +116,7 @@ The plugin loader calls `createServerAdapter()` from your package root. This fun ```ts export const type = "my_adapter"; // snake_case, globally unique -export const label = "My Agent (local)"; +export const label = "My Agent"; export const models = [ { id: "model-a", label: "Model A" }, diff --git a/packages/adapters/acpx-local/src/index.ts b/packages/adapters/acpx-local/src/index.ts index dcbba95354c..ead70e617c7 100644 --- a/packages/adapters/acpx-local/src/index.ts +++ b/packages/adapters/acpx-local/src/index.ts @@ -1,7 +1,7 @@ import type { AdapterModel } from "@paperclipai/adapter-utils"; export const type = "acpx_local"; -export const label = "ACPX (local)"; +export const label = "ACPX"; export const DEFAULT_ACPX_LOCAL_AGENT = "claude"; export const DEFAULT_ACPX_LOCAL_MODE = "persistent"; diff --git a/packages/adapters/claude-local/src/index.ts b/packages/adapters/claude-local/src/index.ts index 1370bd1113e..3310bc8b3f5 100644 --- a/packages/adapters/claude-local/src/index.ts +++ b/packages/adapters/claude-local/src/index.ts @@ -1,7 +1,7 @@ import type { AdapterModelProfileDefinition } from "@paperclipai/adapter-utils"; export const type = "claude_local"; -export const label = "Claude Code (local)"; +export const label = "Claude Code"; export const SANDBOX_INSTALL_COMMAND = "npm install -g @anthropic-ai/claude-code"; diff --git a/packages/adapters/codex-local/src/index.ts b/packages/adapters/codex-local/src/index.ts index dac11bbc764..3a38914972e 100644 --- a/packages/adapters/codex-local/src/index.ts +++ b/packages/adapters/codex-local/src/index.ts @@ -1,7 +1,7 @@ import type { AdapterModelProfileDefinition } from "@paperclipai/adapter-utils"; export const type = "codex_local"; -export const label = "Codex (local)"; +export const label = "Codex"; export const SANDBOX_INSTALL_COMMAND = "npm install -g @openai/codex"; diff --git a/packages/adapters/cursor-local/src/index.ts b/packages/adapters/cursor-local/src/index.ts index f93928d85ea..e1c41e25e5a 100644 --- a/packages/adapters/cursor-local/src/index.ts +++ b/packages/adapters/cursor-local/src/index.ts @@ -1,7 +1,7 @@ import type { AdapterModelProfileDefinition } from "@paperclipai/adapter-utils"; export const type = "cursor"; -export const label = "Cursor CLI (local)"; +export const label = "Cursor CLI"; // Cursor CLI is not distributed as an npm package — the official install // path is the upstream installer script at cursor.com/install. Other adapters diff --git a/packages/adapters/gemini-local/src/index.ts b/packages/adapters/gemini-local/src/index.ts index 72597a24f13..827fac3ef1c 100644 --- a/packages/adapters/gemini-local/src/index.ts +++ b/packages/adapters/gemini-local/src/index.ts @@ -4,7 +4,7 @@ import { } from "@paperclipai/adapter-utils"; export const type = "gemini_local"; -export const label = "Gemini CLI (local)"; +export const label = "Gemini CLI"; export const SANDBOX_INSTALL_COMMAND = buildSandboxNpmInstallCommand("@google/gemini-cli"); diff --git a/packages/adapters/grok-local/src/index.ts b/packages/adapters/grok-local/src/index.ts index 7e12bd1d99f..9e50feed8ff 100644 --- a/packages/adapters/grok-local/src/index.ts +++ b/packages/adapters/grok-local/src/index.ts @@ -1,5 +1,5 @@ export const type = "grok_local"; -export const label = "Grok Build (local)"; +export const label = "Grok Build"; export const DEFAULT_GROK_LOCAL_MODEL = "grok-build"; diff --git a/packages/adapters/opencode-local/src/index.ts b/packages/adapters/opencode-local/src/index.ts index 77fe859c310..18c2df67403 100644 --- a/packages/adapters/opencode-local/src/index.ts +++ b/packages/adapters/opencode-local/src/index.ts @@ -1,7 +1,7 @@ import type { AdapterModelProfileDefinition } from "@paperclipai/adapter-utils"; export const type = "opencode_local"; -export const label = "OpenCode (local)"; +export const label = "OpenCode"; // Use OpenCode's official installer instead of `npm install -g opencode-ai`. // The npm package reifies four large Linux x64 prebuilt-binary subpackages diff --git a/packages/adapters/pi-local/src/index.ts b/packages/adapters/pi-local/src/index.ts index fef8bc2bed3..57fb7daea61 100644 --- a/packages/adapters/pi-local/src/index.ts +++ b/packages/adapters/pi-local/src/index.ts @@ -1,7 +1,7 @@ import type { AdapterModelProfileDefinition } from "@paperclipai/adapter-utils"; export const type = "pi_local"; -export const label = "Pi (local)"; +export const label = "Pi"; export const SANDBOX_INSTALL_COMMAND = "npm install -g @earendil-works/pi-coding-agent@0.74.0"; diff --git a/ui/src/adapters/acpx-local/index.ts b/ui/src/adapters/acpx-local/index.ts index d8ae9675e09..4308d635228 100644 --- a/ui/src/adapters/acpx-local/index.ts +++ b/ui/src/adapters/acpx-local/index.ts @@ -4,7 +4,7 @@ import { SchemaConfigFields } from "../schema-config-fields"; export const acpxLocalUIAdapter: UIAdapterModule = { type: "acpx_local", - label: "ACPX (local)", + label: "ACPX", parseStdoutLine: parseAcpxStdoutLine, ConfigFields: SchemaConfigFields, buildAdapterConfig: buildAcpxLocalConfig, diff --git a/ui/src/adapters/adapter-display-registry.test.ts b/ui/src/adapters/adapter-display-registry.test.ts new file mode 100644 index 00000000000..967f2b05b14 --- /dev/null +++ b/ui/src/adapters/adapter-display-registry.test.ts @@ -0,0 +1,45 @@ +import { describe, expect, it } from "vitest"; + +import { getAdapterDisplay, getAdapterLabel, getAdapterLabels } from "./adapter-display-registry"; + +describe("adapter display registry", () => { + it("uses user-facing labels without the legacy local qualifier for built-in adapters", () => { + expect(getAdapterLabel("codex_local")).toBe("Codex"); + expect(getAdapterLabel("claude_local")).toBe("Claude Code"); + expect(getAdapterLabel("acpx_local")).toBe("ACPX"); + expect(getAdapterLabel("cursor")).toBe("Cursor"); + expect(getAdapterLabel("gemini_local")).toBe("Gemini CLI"); + expect(getAdapterLabel("grok_local")).toBe("Grok Build"); + expect(getAdapterLabel("hermes_local")).toBe("Hermes"); + expect(getAdapterLabel("opencode_local")).toBe("OpenCode"); + expect(getAdapterLabel("pi_local")).toBe("Pi"); + + expect(getAdapterLabels()).toMatchObject({ + codex_local: "Codex", + claude_local: "Claude Code", + acpx_local: "ACPX", + cursor: "Cursor", + gemini_local: "Gemini CLI", + grok_local: "Grok Build", + hermes_local: "Hermes", + opencode_local: "OpenCode", + pi_local: "Pi", + }); + }); + + it("drops local suffixes for unknown plugin adapter labels", () => { + expect(getAdapterLabel("droid_local")).toBe("Droid"); + expect(getAdapterDisplay("droid_local")).toMatchObject({ + label: "Droid", + description: "External adapter", + }); + }); + + it("keeps a gateway suffix for unknown plugin adapter labels", () => { + expect(getAdapterLabel("droid_gateway")).toBe("Droid (gateway)"); + expect(getAdapterDisplay("droid_gateway")).toMatchObject({ + label: "Droid (gateway)", + description: "External gateway adapter", + }); + }); +}); diff --git a/ui/src/adapters/adapter-display-registry.ts b/ui/src/adapters/adapter-display-registry.ts index 17a20dca8f6..fab2163c7e4 100644 --- a/ui/src/adapters/adapter-display-registry.ts +++ b/ui/src/adapters/adapter-display-registry.ts @@ -21,13 +21,19 @@ import { OpenCodeLogoIcon } from "@/components/OpenCodeLogoIcon"; // Type suffix parsing // --------------------------------------------------------------------------- -const TYPE_SUFFIXES: Record = { - _local: "local", +// Suffixes stripped from type ids when deriving a human-readable label for +// unknown (plugin) adapter types. "_local" is a legacy qualifier from before +// first-class Environments and is never displayed; "_gateway" is re-appended +// as " (gateway)" to disambiguate gateway variants. Known adapters in +// `adapterDisplayMap` have final labels and never get a derived suffix. +const STRIPPED_TYPE_SUFFIXES = ["_local", "_gateway"] as const; + +const DISPLAY_SUFFIXES: Record = { _gateway: "gateway", }; function getTypeSuffix(type: string): string | null { - for (const [suffix, mode] of Object.entries(TYPE_SUFFIXES)) { + for (const [suffix, mode] of Object.entries(DISPLAY_SUFFIXES)) { if (type.endsWith(suffix)) return mode; } return null; @@ -55,57 +61,57 @@ export interface AdapterDisplayInfo { const adapterDisplayMap: Record = { acpx_local: { label: "ACPX", - description: "Experimental local ACPX multi-agent adapter", + description: "Experimental ACPX multi-agent harness", icon: Bot, experimental: true, hideFromVisualSelection: true, }, claude_local: { label: "Claude Code", - description: "Local Claude agent", + description: "Claude Code CLI harness", icon: Sparkles, recommended: true, }, codex_local: { label: "Codex", - description: "Local Codex agent", + description: "Codex CLI harness", icon: Code, recommended: true, }, gemini_local: { label: "Gemini CLI", - description: "Local Gemini agent", + description: "Gemini CLI harness", icon: Gem, }, grok_local: { label: "Grok Build", - description: "Local Grok Build agent", + description: "Grok Build harness", icon: Bot, }, hermes_gateway: { - label: "Hermes", + label: "Hermes (gateway)", description: "Remote Hermes API server", icon: Bot, hideFromVisualSelection: true, }, hermes_local: { label: "Hermes", - description: "Local Hermes agent", + description: "Hermes harness", icon: Bot, }, opencode_local: { label: "OpenCode", - description: "Local multi-provider agent", + description: "OpenCode multi-provider harness", icon: OpenCodeLogoIcon, }, pi_local: { label: "Pi", - description: "Local Pi agent", + description: "Pi harness", icon: Terminal, }, cursor: { label: "Cursor", - description: "Local Cursor agent", + description: "Cursor CLI harness", icon: MousePointer2, }, cursor_cloud: { @@ -142,7 +148,7 @@ const adapterDisplayMap: Record = { function humanizeType(type: string): string { // Strip known type suffixes so "droid_local" → "Droid", not "Droid Local" let base = type; - for (const suffix of Object.keys(TYPE_SUFFIXES)) { + for (const suffix of STRIPPED_TYPE_SUFFIXES) { if (base.endsWith(suffix)) { base = base.slice(0, -suffix.length); break; @@ -152,16 +158,20 @@ function humanizeType(type: string): string { } export function getAdapterLabel(type: string): string { - const base = adapterDisplayMap[type]?.label ?? humanizeType(type); - return withSuffix(base, getTypeSuffix(type)); + // Known labels are final — only unknown (plugin) types get a derived + // suffix, so labels like "OpenClaw Gateway" don't become + // "OpenClaw Gateway (gateway)". + const known = adapterDisplayMap[type]; + if (known) return known.label; + return withSuffix(humanizeType(type), getTypeSuffix(type)); } export function getAdapterLabels(): Record { - const suffixed: Record = {}; + const labels: Record = {}; for (const [type, info] of Object.entries(adapterDisplayMap)) { - suffixed[type] = withSuffix(info.label, getTypeSuffix(type)); + labels[type] = info.label; } - return suffixed; + return labels; } export function getAdapterDisplay(type: string): AdapterDisplayInfo { diff --git a/ui/src/adapters/claude-local/index.ts b/ui/src/adapters/claude-local/index.ts index fc2904d0141..5bb32eaa8cd 100644 --- a/ui/src/adapters/claude-local/index.ts +++ b/ui/src/adapters/claude-local/index.ts @@ -5,7 +5,7 @@ import { buildClaudeLocalConfig } from "@paperclipai/adapter-claude-local/ui"; export const claudeLocalUIAdapter: UIAdapterModule = { type: "claude_local", - label: "Claude Code (local)", + label: "Claude Code", parseStdoutLine: parseClaudeStdoutLine, ConfigFields: ClaudeLocalConfigFields, buildAdapterConfig: buildClaudeLocalConfig, diff --git a/ui/src/adapters/codex-local/index.ts b/ui/src/adapters/codex-local/index.ts index 31a11bf3d5a..0d9dd1a3dd4 100644 --- a/ui/src/adapters/codex-local/index.ts +++ b/ui/src/adapters/codex-local/index.ts @@ -5,7 +5,7 @@ import { buildCodexLocalConfig } from "@paperclipai/adapter-codex-local/ui"; export const codexLocalUIAdapter: UIAdapterModule = { type: "codex_local", - label: "Codex (local)", + label: "Codex", parseStdoutLine: parseCodexStdoutLine, ConfigFields: CodexLocalConfigFields, buildAdapterConfig: buildCodexLocalConfig, diff --git a/ui/src/adapters/cursor/index.ts b/ui/src/adapters/cursor/index.ts index b80cff6e9b1..62ede15b2b4 100644 --- a/ui/src/adapters/cursor/index.ts +++ b/ui/src/adapters/cursor/index.ts @@ -5,7 +5,7 @@ import { buildCursorLocalConfig } from "@paperclipai/adapter-cursor-local/ui"; export const cursorLocalUIAdapter: UIAdapterModule = { type: "cursor", - label: "Cursor CLI (local)", + label: "Cursor CLI", parseStdoutLine: parseCursorStdoutLine, ConfigFields: CursorLocalConfigFields, buildAdapterConfig: buildCursorLocalConfig, diff --git a/ui/src/adapters/gemini-local/index.ts b/ui/src/adapters/gemini-local/index.ts index d4cb89e5154..97a585d3812 100644 --- a/ui/src/adapters/gemini-local/index.ts +++ b/ui/src/adapters/gemini-local/index.ts @@ -5,7 +5,7 @@ import { buildGeminiLocalConfig } from "@paperclipai/adapter-gemini-local/ui"; export const geminiLocalUIAdapter: UIAdapterModule = { type: "gemini_local", - label: "Gemini CLI (local)", + label: "Gemini CLI", parseStdoutLine: parseGeminiStdoutLine, ConfigFields: GeminiLocalConfigFields, buildAdapterConfig: buildGeminiLocalConfig, diff --git a/ui/src/adapters/grok-local/index.ts b/ui/src/adapters/grok-local/index.ts index 31b7ffe27e1..ae08b64cacc 100644 --- a/ui/src/adapters/grok-local/index.ts +++ b/ui/src/adapters/grok-local/index.ts @@ -5,7 +5,7 @@ import { GrokLocalConfigFields } from "./config-fields"; export const grokLocalUIAdapter: UIAdapterModule = { type: "grok_local", - label: "Grok Build (local)", + label: "Grok Build", parseStdoutLine: parseGrokStdoutLine, createStdoutParser: createGrokStdoutParser, ConfigFields: GrokLocalConfigFields, diff --git a/ui/src/adapters/opencode-local/index.ts b/ui/src/adapters/opencode-local/index.ts index b68f3ace00d..97e5029b243 100644 --- a/ui/src/adapters/opencode-local/index.ts +++ b/ui/src/adapters/opencode-local/index.ts @@ -5,7 +5,7 @@ import { buildOpenCodeLocalConfig } from "@paperclipai/adapter-opencode-local/ui export const openCodeLocalUIAdapter: UIAdapterModule = { type: "opencode_local", - label: "OpenCode (local)", + label: "OpenCode", parseStdoutLine: parseOpenCodeStdoutLine, ConfigFields: OpenCodeLocalConfigFields, buildAdapterConfig: buildOpenCodeLocalConfig, diff --git a/ui/src/adapters/pi-local/index.ts b/ui/src/adapters/pi-local/index.ts index cfebf6698dd..907bb148d33 100644 --- a/ui/src/adapters/pi-local/index.ts +++ b/ui/src/adapters/pi-local/index.ts @@ -5,7 +5,7 @@ import { buildPiLocalConfig } from "@paperclipai/adapter-pi-local/ui"; export const piLocalUIAdapter: UIAdapterModule = { type: "pi_local", - label: "Pi (local)", + label: "Pi", parseStdoutLine: parsePiStdoutLine, ConfigFields: PiLocalConfigFields, buildAdapterConfig: buildPiLocalConfig, diff --git a/ui/src/pages/AdapterManager.tsx b/ui/src/pages/AdapterManager.tsx index c55b66f17ae..a90cc517f00 100644 --- a/ui/src/pages/AdapterManager.tsx +++ b/ui/src/pages/AdapterManager.tsx @@ -72,7 +72,10 @@ function AdapterRow({
- {adapter.label || getAdapterLabel(adapter.type)} + {/* The server reports label = raw type id, so prefer the display + registry's human label; keep a real self-reported label if an + external adapter ever provides one. */} + {adapter.label && adapter.label !== adapter.type ? adapter.label : getAdapterLabel(adapter.type)} {adapter.source === "external" ? "External" : "Built-in"} {adapter.source === "external" && ( diff --git a/ui/storybook/.storybook/preview.tsx b/ui/storybook/.storybook/preview.tsx index 011ec14b5b0..552da835c2e 100644 --- a/ui/storybook/.storybook/preview.tsx +++ b/ui/storybook/.storybook/preview.tsx @@ -104,7 +104,7 @@ function installStorybookApiFixtures() { return Response.json([ { type: "claude_local", - label: "Claude Local", + label: "Claude Code", source: "builtin", modelsCount: 2, loaded: true, @@ -119,7 +119,7 @@ function installStorybookApiFixtures() { }, { type: "codex_local", - label: "Codex Local", + label: "Codex", source: "builtin", modelsCount: 3, loaded: true, diff --git a/ui/storybook/stories/agent-management.stories.tsx b/ui/storybook/stories/agent-management.stories.tsx index c5a727fe373..2fdf2ef24b3 100644 --- a/ui/storybook/stories/agent-management.stories.tsx +++ b/ui/storybook/stories/agent-management.stories.tsx @@ -298,7 +298,7 @@ const storybookSecrets: CompanySecret[] = [ const adapterFixtures: AdapterInfo[] = [ { type: "codex_local", - label: "Codex Local", + label: "Codex", source: "builtin", modelsCount: 3, loaded: true, @@ -313,7 +313,7 @@ const adapterFixtures: AdapterInfo[] = [ }, { type: "claude_local", - label: "Claude Local", + label: "Claude Code", source: "builtin", modelsCount: 2, loaded: true, diff --git a/ui/storybook/stories/dialogs-modals.stories.tsx b/ui/storybook/stories/dialogs-modals.stories.tsx index c0a7b7e2ef7..24e25c74d29 100644 --- a/ui/storybook/stories/dialogs-modals.stories.tsx +++ b/ui/storybook/stories/dialogs-modals.stories.tsx @@ -373,7 +373,7 @@ function hydrateDialogQueries(queryClient: ReturnType) { queryClient.setQueryData(queryKeys.adapters.all, [ { type: "codex_local", - label: "Codex local", + label: "Codex", source: "builtin", modelsCount: 5, loaded: true, @@ -388,7 +388,7 @@ function hydrateDialogQueries(queryClient: ReturnType) { }, { type: "claude_local", - label: "Claude local", + label: "Claude Code", source: "builtin", modelsCount: 4, loaded: true, @@ -721,7 +721,7 @@ function useCheapLaneAdapterOverrides(variant: CheapLaneVariant) { queryClient.setQueryData(queryKeys.adapters.all, [ { type: "codex_local", - label: "Codex local", + label: "Codex", source: "builtin", modelsCount: 5, loaded: true, @@ -736,7 +736,7 @@ function useCheapLaneAdapterOverrides(variant: CheapLaneVariant) { }, { type: "opencode_local", - label: "OpenCode local", + label: "OpenCode", source: "builtin", modelsCount: 2, loaded: true, diff --git a/ui/storybook/stories/forms-editors.stories.tsx b/ui/storybook/stories/forms-editors.stories.tsx index 8c267fea905..8fa17c20716 100644 --- a/ui/storybook/stories/forms-editors.stories.tsx +++ b/ui/storybook/stories/forms-editors.stories.tsx @@ -121,7 +121,7 @@ const adapterSchema: JsonSchemaNode = { title: "Adapter name", description: "Human-readable name shown in the adapter manager.", minLength: 3, - default: "Codex local", + default: "Codex", }, mode: { type: "string", @@ -175,7 +175,7 @@ const adapterSchema: JsonSchemaNode = { const validAdapterValues = { ...getDefaultValues(adapterSchema), - adapterName: "Codex local", + adapterName: "Codex", mode: "implementation", apiKey: "secret:openai-api-key", concurrency: 2,