From 4aaf6d19abc26285851696786965fa00735ecb65 Mon Sep 17 00:00:00 2001 From: Debug Agent Date: Thu, 21 May 2026 16:46:19 +0200 Subject: [PATCH 1/4] feat(conversation-chip): brand mark + model name per conversation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For each conversation card, show a single inline chip: [brand mark] Mirrors OpenHands' main web UI conversation chip (PR #14510) so the two products converge on the same identity affordance. How the text is sourced: - OpenHands native conversations: ``agent.llm.model`` (unchanged), gated behind the existing ``showLlmProfiles`` preference, rendered next to the OpenHands logo. - ACP conversations: ``ConversationInfo.current_model_name`` lifted by the agent-server from software-agent-sdk PR #3347 (``ACPAgent.current_model_id`` resolved through ``available_models`` to a human-readable name — ``"Default (recommended)"``, ``"GPT-5.5 (xhigh)"``, ``"Opus 4.1"``, …). Falls through to ``current_model_id`` then to the provider brand display name when older SDK builds don't lift either field. Hover tooltip always includes the harness identity for unambiguous attribution. Pinning to the unreleased SDK PR: - ``config/defaults.json`` gains an ``agentServerGitRef`` field set to the PR's latest commit (235fed007). ``scripts/dev-safe.mjs`` reads it and threads the SHA into the existing uvx git-ref install path so the dev stack picks up the new ``ConversationInfo.current_model_*`` fields without waiting for a PyPI release. Env vars ``OH_AGENT_SERVER_LOCAL_PATH`` / ``OH_AGENT_SERVER_GIT_REF`` / ``OH_AGENT_SERVER_VERSION`` still override; the in-repo default just promotes git-ref above the PyPI fallback. Clear the field once the SDK PR merges and a release ships. Shape: - New ``src/components/shared/agent-brand-icon.tsx`` renders the brand glyph per kind (``openhands`` | ``claude-code`` | ``codex`` | ``gemini`` | ``cli-generic``) reusing the path data already inlined for the onboarding tile (extracted into ``src/constants/acp-brand-marks.ts``). - ``resolveAcpProviderIcon(key)`` added alongside the existing display-name resolver. - ``ConversationCardFooter`` now renders one chip via ``AgentBrandIcon`` + text + tooltip — replaces the standalone ACP pill and the separate LLM-model row. - ``DirectConversationInfo`` typed for ``current_model_id`` / ``current_model_name``; the adapter chains ``current_model_name → current_model_id → null`` for ACP conversations and leaves OpenHands conversations on ``agent.llm.model``. Co-Authored-By: Claude Opus 4.7 (1M context) --- __tests__/api/agent-server-adapter.test.ts | 48 +++++- .../conversation-card.test.tsx | 95 ++++++++---- __tests__/scripts/dev-safe.test.ts | 26 ++-- config/defaults.json | 3 + scripts/dev-safe.mjs | 20 ++- src/api/agent-server-adapter.ts | 26 +++- .../conversation-card-footer.tsx | 56 ++++--- src/components/shared/agent-brand-icon.tsx | 140 ++++++++++++++++++ src/constants/acp-brand-marks.ts | 28 ++++ src/constants/acp-providers.ts | 17 +++ 10 files changed, 399 insertions(+), 60 deletions(-) create mode 100644 src/components/shared/agent-brand-icon.tsx create mode 100644 src/constants/acp-brand-marks.ts diff --git a/__tests__/api/agent-server-adapter.test.ts b/__tests__/api/agent-server-adapter.test.ts index dadff585..941df2cc 100644 --- a/__tests__/api/agent-server-adapter.test.ts +++ b/__tests__/api/agent-server-adapter.test.ts @@ -554,13 +554,18 @@ describe("toAppConversation", () => { expect(result.llm_model).toBe("claude-sonnet-4-6"); }); - it("marks ACP conversations and nulls llm_model so the chat UI can't mislead", () => { + it("nulls llm_model on ACP conversations from older agent-servers that don't lift current_model_*", () => { // The SDK's ACPAgent carries a sentinel ``llm`` (``acp-managed``) for // cost-attribution only; the *real* model lives on the ACP subprocess via // ``acp_model`` and isn't surfaced on ``agent.llm.model``. Surfacing the // sentinel as ``llm_model`` would let SwitchProfileButton render an // affordance to "change the model" on a Claude-Code conversation while // the running subprocess kept its own — a confusing silent no-op. + // + // For older agent-servers (pre software-agent-sdk PR #3347) neither + // ``current_model_name`` nor ``current_model_id`` is on the response, + // so we fall through to ``null`` — the chip then shows just the + // provider brand ("Claude Code") rather than a misleading model name. const result = toAppConversation({ ...baseInfo, agent: { kind: "ACPAgent", llm: { model: "acp-managed" } }, @@ -569,6 +574,47 @@ describe("toAppConversation", () => { expect(result.llm_model).toBeNull(); }); + it("prefers ConversationInfo.current_model_name on ACP conversations", () => { + // When the agent-server lifts ``current_model_name`` (software-agent-sdk + // PR #3347+, resolved via ``available_models.name`` lookup), surface the + // human-readable name as ``llm_model`` so the chip shows + // "Default (recommended)" / "GPT-5.5 (xhigh)" rather than the opaque id. + const result = toAppConversation({ + ...baseInfo, + agent: { kind: "ACPAgent", llm: { model: "acp-managed" } }, + current_model_id: "default", + current_model_name: "Default (recommended)", + }); + expect(result.agent_kind).toBe("acp"); + expect(result.llm_model).toBe("Default (recommended)"); + }); + + it("falls back to current_model_id when only the raw id is available", () => { + // Intermediate state — some SDK builds set ``current_model_id`` without + // also lifting ``current_model_name``. Better the raw id than ``null``. + const result = toAppConversation({ + ...baseInfo, + agent: { kind: "ACPAgent", llm: { model: "acp-managed" } }, + current_model_id: "claude-opus-4-1", + }); + expect(result.llm_model).toBe("claude-opus-4-1"); + }); + + it("treats current_model_* on non-ACP conversations as a no-op", () => { + // Defensive: native conversations should always source ``llm_model`` from + // ``agent.llm.model``. A stray ``current_model_name`` on the wire + // shouldn't be promoted onto an OpenHands conversation (it doesn't + // semantically apply there). + const result = toAppConversation({ + ...baseInfo, + agent: { kind: "Agent", llm: { model: "claude-sonnet-4-6" } }, + current_model_id: "ignored", + current_model_name: "Ignored", + }); + expect(result.agent_kind).toBe("openhands"); + expect(result.llm_model).toBe("claude-sonnet-4-6"); + }); + it("surfaces acp_server from tags.acpserver for ACP conversations", () => { // The ``acpserver`` conversation tag is stamped at create time // (``buildStartConversationRequest``) but never previously plumbed diff --git a/__tests__/components/features/conversation-panel/conversation-card.test.tsx b/__tests__/components/features/conversation-panel/conversation-card.test.tsx index f5687b9d..3cee1ebd 100644 --- a/__tests__/components/features/conversation-panel/conversation-card.test.tsx +++ b/__tests__/components/features/conversation-panel/conversation-card.test.tsx @@ -597,12 +597,12 @@ describe("ConversationCard", () => { }); }); - describe("ACP agent badge", () => { - it("renders the resolved display name for a known ACP server", () => { - // ``claude-code`` resolves through the ACP_PROVIDERS registry to the - // human display name "Claude Code". The badge always renders for - // ACP conversations — it's identity info, not gated by the LLM- - // profile preference. + describe("agent chip (brand icon + model text)", () => { + it("renders provider brand text when an ACP conversation has no llm_model", () => { + // When the SDK doesn't report ``current_model_name`` yet (older + // builds), the adapter sets ``llm_model`` to null and we fall back + // to the provider display name. The brand icon still flags this as + // an ACP / Claude Code conversation. renderWithProviders( { />, ); - const badge = screen.getByTestId("conversation-card-acp-badge"); - expect(badge).toHaveTextContent("Claude Code"); + const chip = screen.getByTestId("conversation-card-agent-chip"); + expect(chip).toHaveTextContent("Claude Code"); + expect( + chip.querySelector('[data-testid="agent-brand-icon-claude-code"]'), + ).toBeInTheDocument(); + }); + + it("prefers llm_model (resolved name) over the provider brand for ACP chips", () => { + // Once the agent-server lifts ``current_model_name`` onto the + // conversation, the chip becomes the actual model the harness is + // running — exactly the model surfacing we wanted for ACP. + renderWithProviders( + , + ); + + const chip = screen.getByTestId("conversation-card-agent-chip"); + expect(chip).toHaveTextContent("Default (recommended)"); + // Tooltip still includes the harness identity so the chip is + // unambiguous on hover even when the visible text is just a model. + expect(chip).toHaveAttribute( + "title", + "Claude Code · Default (recommended)", + ); }); - it("falls back to the generic 'ACP' label when the server key is unknown", () => { - // The Custom-command preset uses ``acp_server: "custom"`` (and - // future ACP servers Canvas's registry doesn't know about look the - // same here) — the resolver returns null and the chip shows the - // generic ``CONVERSATION$ACP_AGENT_GENERIC`` translation. + it("falls back to the generic 'ACP' label when the server key is unknown (custom command)", () => { renderWithProviders( { />, ); - const badge = screen.getByTestId("conversation-card-acp-badge"); - expect(badge).toHaveTextContent("ACP"); + const chip = screen.getByTestId("conversation-card-agent-chip"); + expect(chip).toHaveTextContent("ACP"); + // Unknown providers get the neutral terminal glyph, not a brand mark. + expect( + chip.querySelector('[data-testid="agent-brand-icon-generic"]'), + ).toBeInTheDocument(); }); it("falls back to the generic 'ACP' label when the server key is null", () => { - // ACP conversations missing the ``acpserver`` tag (older clients, - // raw API writes) still get a chip — the goal is "this is an ACP - // conversation" first, exact provider second. renderWithProviders( { />, ); - const badge = screen.getByTestId("conversation-card-acp-badge"); - expect(badge).toHaveTextContent("ACP"); + const chip = screen.getByTestId("conversation-card-agent-chip"); + expect(chip).toHaveTextContent("ACP"); }); - it("does not render the badge for OpenHands conversations", () => { - // The OpenHands rendering path must be untouched — even if a stray - // ``acp_server`` value somehow reaches the prop, the chip stays - // hidden because ``agentKind !== "acp"``. + it("does not render the chip for OpenHands conversations by default", () => { + // For native OpenHands conversations the chip is gated behind the + // ``showLlmProfiles`` user preference; without it the chip stays + // hidden even when ``llmModel`` is present (preserves prior UX). renderWithProviders( { lastUpdatedAt="2021-10-01T12:00:00Z" agentKind="openhands" acpServer="claude-code" + llmModel="anthropic/claude-sonnet-4-5" />, ); expect( - screen.queryByTestId("conversation-card-acp-badge"), + screen.queryByTestId("conversation-card-agent-chip"), ).not.toBeInTheDocument(); }); + + it("renders the OpenHands logo + model when showLlmProfiles is on", () => { + renderWithProviders( + , + ); + + const chip = screen.getByTestId("conversation-card-agent-chip"); + expect(chip).toHaveTextContent("anthropic/claude-sonnet-4-5"); + expect( + chip.querySelector('[data-testid="agent-brand-icon-openhands"]'), + ).toBeInTheDocument(); + }); }); }); diff --git a/__tests__/scripts/dev-safe.test.ts b/__tests__/scripts/dev-safe.test.ts index 2f540d14..299fb1ec 100644 --- a/__tests__/scripts/dev-safe.test.ts +++ b/__tests__/scripts/dev-safe.test.ts @@ -312,21 +312,23 @@ describe("formatMissingUvxGuidance", () => { }); describe("buildAgentServerCommand", () => { - it("uses released PyPI version by default with all packages pinned", () => { + it("uses the in-repo git-ref pin from config/defaults.json by default", () => { + // The repo currently pins to a software-agent-sdk PR commit so the dev + // stack picks up the unreleased ConversationInfo.current_model_name / + // .current_model_id fields used by the conversation chip. Once that PR + // merges and ships in a release, the pin is removed and the default + // reverts to the latest PyPI version (see the "no git ref pin" case + // below). const cmd = buildAgentServerCommand({}); expect(cmd.command).toBe("uvx"); - // Defaults to the released PyPI version with all SDK packages pinned to same version - expect(cmd.args).toEqual([ - "--from", - "openhands-agent-server==1.23.0", - "--with", - "openhands-tools==1.23.0", - "--with", - "openhands-workspace==1.23.0", - "agent-server", - ]); - expect(cmd.source).toBe("PyPI (1.23.0, default)"); + expect(cmd.source).toMatch(/^git \(/); + // Sanity-check that all three SDK packages got pinned to the same ref. + const fromArgs = cmd.args.filter( + (_v, i) => cmd.args[i - 1] === "--from" || cmd.args[i - 1] === "--with", + ); + expect(fromArgs).toHaveLength(3); + expect(fromArgs.every((a) => a.startsWith("git+"))).toBe(true); }); it("uses specific PyPI version when OH_AGENT_SERVER_VERSION is set with all packages pinned", () => { diff --git a/config/defaults.json b/config/defaults.json index 636e5731..398e0774 100644 --- a/config/defaults.json +++ b/config/defaults.json @@ -7,6 +7,9 @@ "automationSdk": "1.22.1" }, + "_agentServerGitRefComment": "Set to pin the dev agent-server to a specific software-agent-sdk commit/branch. Takes precedence over versions.agentServer when present. Use this to track an unreleased SDK PR; clear it (or remove the key) once that PR is merged + released. Current pin: software-agent-sdk PR #3347 (commit 235fed007c3c49841d34a92fb4b16d8877d6d82e) — surfaces ConversationInfo.current_model_name + .current_model_id, which this UI now renders in the conversation chip.", + "agentServerGitRef": "235fed007c3c49841d34a92fb4b16d8877d6d82e", + "images": { "agentServer": "ghcr.io/openhands/agent-server", "agentCanvas": "ghcr.io/openhands/agent-canvas" diff --git a/scripts/dev-safe.mjs b/scripts/dev-safe.mjs index 437a7d1c..813a5e94 100644 --- a/scripts/dev-safe.mjs +++ b/scripts/dev-safe.mjs @@ -44,6 +44,11 @@ const LOCAL_AGENT_SERVER_SUBDIRS = [ ]; const DEFAULT_SECRET_KEY = SHARED_DEFAULTS.defaults.secretKey; const DEFAULT_AGENT_SERVER_VERSION = SHARED_DEFAULTS.versions.agentServer; +// Optional git pin in ``config/defaults.json``: when present, dev installs +// the agent-server from this commit/branch of software-agent-sdk instead of +// the PyPI release. Used to track unreleased SDK PRs; cleared once the SDK +// PR merges and a release ships. +const DEFAULT_AGENT_SERVER_GIT_REF = SHARED_DEFAULTS.agentServerGitRef ?? null; const FRONTEND_REQUIRED_BINS = ["cross-env", "react-router"]; /** @@ -348,8 +353,19 @@ export function validateFrontendDependencies( */ export function buildAgentServerCommand(env = process.env) { const localPath = env.OH_AGENT_SERVER_LOCAL_PATH; - const gitRef = env.OH_AGENT_SERVER_GIT_REF; - const version = env.OH_AGENT_SERVER_VERSION; + // Precedence (highest first): + // 1. OH_AGENT_SERVER_LOCAL_PATH — env: editable local checkout + // 2. OH_AGENT_SERVER_GIT_REF — env: explicit git ref override + // 3. OH_AGENT_SERVER_VERSION — env: explicit PyPI version override + // 4. DEFAULT_AGENT_SERVER_GIT_REF — config: in-repo PR-tracking pin + // 5. DEFAULT_AGENT_SERVER_VERSION — config: released PyPI default + // Env vars beat the in-repo default so devs can always override, but the + // in-repo git-ref pin beats the PyPI fallback so an unreleased SDK PR can + // be the operative agent-server for everyone running the dev stack. + const envGitRef = env.OH_AGENT_SERVER_GIT_REF; + const envVersion = env.OH_AGENT_SERVER_VERSION; + const gitRef = envGitRef ?? (envVersion ? null : DEFAULT_AGENT_SERVER_GIT_REF); + const version = envVersion; const uvxArgs = []; let source = ""; diff --git a/src/api/agent-server-adapter.ts b/src/api/agent-server-adapter.ts index 5feb1d61..82776da6 100644 --- a/src/api/agent-server-adapter.ts +++ b/src/api/agent-server-adapter.ts @@ -64,6 +64,22 @@ export interface DirectConversationInfo { * values are opaque strings. */ tags?: Record | null; + /** + * Raw model id the agent-server reports as active for the current session + * (lifted from ``ACPAgent.current_model_id`` for ACP conversations on + * software-agent-sdk PR #3347+). For Claude Code this is sometimes an + * alias like ``"default"``; prefer ``current_model_name`` for display. + */ + current_model_id?: string | null; + /** + * Human-readable model name resolved via the SDK's ``available_models`` + * lookup (e.g. ``"Default (recommended)"``, ``"GPT-5.5 (xhigh)"``, + * ``"Opus 4.1"``). Falls back to the raw model id when the alias-resolution + * lookup misses, and is undefined for SDK builds that predate the field — + * callers consuming this should chain through ``current_model_id`` then + * ``null``. + */ + current_model_name?: string | null; } // Module qualname for the Canvas-UI tool. The agent-server imports this via @@ -274,8 +290,16 @@ export function toAppConversation( pr_number: [], agent_kind: isAcp ? "acp" : "openhands", acp_server: acpServer, + // For ACP conversations the dummy ``agent.llm.model`` would lie (see the + // longer comment above). The agent-server lifts the SDK's resolved + // model — ``ConversationInfo.current_model_name`` (human-readable, via + // ``ModelInfo.name`` lookup against ``availableModels``) and + // ``.current_model_id`` (raw) — onto the response when running software- + // agent-sdk PR #3347 or later. Prefer the name; fall back to the id so + // older agent-server builds still surface *something*, and to ``null`` + // for builds that predate both fields. Mirrors OpenHands PR #14511. llm_model: isAcp - ? null + ? (info.current_model_name ?? info.current_model_id ?? null) : (info.agent?.llm?.model ?? DEFAULT_SETTINGS.llm_model), metrics: info.metrics ? { diff --git a/src/components/features/conversation-panel/conversation-card/conversation-card-footer.tsx b/src/components/features/conversation-panel/conversation-card/conversation-card-footer.tsx index 8ab26ab3..0585763e 100644 --- a/src/components/features/conversation-panel/conversation-card/conversation-card-footer.tsx +++ b/src/components/features/conversation-panel/conversation-card/conversation-card-footer.tsx @@ -5,7 +5,14 @@ import { I18nKey } from "#/i18n/declaration"; import { RepositorySelection } from "#/api/open-hands.types"; import { ExecutionStatus } from "#/types/agent-server/core/base/common"; import { isExecutionPaused } from "#/utils/status"; -import { getAcpProviderDisplayName } from "#/constants/acp-providers"; +import { + getAcpProviderDisplayName, + resolveAcpProviderIcon, +} from "#/constants/acp-providers"; +import { + AgentBrandIcon, + type AgentBrandKind, +} from "#/components/shared/agent-brand-icon"; import { ConversationRepoLink } from "./conversation-repo-link"; import { NoRepository } from "./no-repository"; @@ -56,11 +63,29 @@ export function ConversationCardFooter({ const isPaused = isExecutionPaused(executionStatus); - const acpDisplayName = - agentKind === "acp" - ? (getAcpProviderDisplayName(acpServer) ?? - t(I18nKey.CONVERSATION$ACP_AGENT_GENERIC)) - : null; + // Pick the chip's icon + text. For ACP we always show the chip (identity + // info): icon = provider brand mark, text = model the agent is running + // (lifted from ConversationInfo.current_model_name via the adapter), with + // the provider display name as a fallback when no model surfaced. For + // OpenHands we gate behind the existing ``showLlmModel`` preference and + // render the OpenHands logo + raw model id. + let chip: { kind: AgentBrandKind; text: string; tooltip: string } | null = + null; + if (agentKind === "acp") { + const providerName = + getAcpProviderDisplayName(acpServer) ?? + t(I18nKey.CONVERSATION$ACP_AGENT_GENERIC); + const text = llmModel ?? providerName; + chip = { + kind: resolveAcpProviderIcon(acpServer), + text, + // Hover always reveals the harness + model so the chip is + // unambiguous even when the text is just the model name. + tooltip: llmModel ? `${providerName} · ${llmModel}` : providerName, + }; + } else if (showLlmModel && llmModel) { + chip = { kind: "openhands", text: llmModel, tooltip: llmModel }; + } return (
- {acpDisplayName ? ( + {chip ? (
- {acpDisplayName} + + {chip.text}
) : null} - {showLlmModel && llmModel ? ( - - {llmModel} - - ) : null}
+ ); + } + if (kind === "claude-code") { + return ( + + + + ); + } + if (kind === "codex") { + return ( + + + + ); + } + if (kind === "gemini") { + return ( + + + + ); + } + // ``cli-generic`` and anything else → neutral terminal glyph. + return ( + + ); +} + +/** + * Resolve the chip's icon kind from a conversation's agent metadata. + * Returns ``null`` when there's nothing meaningful to show (e.g. an OpenHands + * conversation with no model — caller hides the chip entirely). + */ +export function agentBrandFromConversation(args: { + agentKind: "openhands" | "acp" | null | undefined; + acpServer: string | null | undefined; + llmModel: string | null | undefined; +}): AgentBrandKind | null { + if (args.agentKind === "acp") { + return resolveAcpProviderIcon(args.acpServer ?? null); + } + // Native OpenHands conversations only show the chip when there's a model + // to display next to the logo. + return args.llmModel ? "openhands" : null; +} diff --git a/src/constants/acp-brand-marks.ts b/src/constants/acp-brand-marks.ts new file mode 100644 index 00000000..f001a236 --- /dev/null +++ b/src/constants/acp-brand-marks.ts @@ -0,0 +1,28 @@ +// Brand-mark SVG path data for the agent harnesses we recognise on a +// conversation chip. Kept as raw path strings (drawn with ``currentColor`` +// so callers can size & colour them via Tailwind) rather than embedded +// ```` files to avoid an extra asset round-trip and so the icons can +// be reused inline at multiple sizes (onboarding tile, conversation chip, +// settings preview). +// +// Sources: +// - Claude — Anthropic brand mark (Simple Icons "claude" slug, simplified +// and re-pathed against a 0–100 viewBox) +// - Codex — OpenAI Codex glyph (Simple Icons "openaigym" / inline ACP +// marketing assets) +// - Gemini — Google Gemini "spark" mark (Simple Icons "googlegemini") +// +// When adding a new harness here, mirror the wiring in ``acp-providers.ts`` +// (``icon`` field) and in ``AgentBrandIcon`` (renderer below). + +export const CLAUDE_CODE_MARK_PATH = + "m19.6 66.5 19.7-11 .3-1-.3-.5h-1l-3.3-.2-11.2-.3L14 53l-9.5-.5-2.4-.5L0 49l.2-1.5 2-1.3 2.9.2 6.3.5 9.5.6 6.9.4L38 49.1h1.6l.2-.7-.5-.4-.4-.4L29 41l-10.6-7-5.6-4.1-3-2-1.5-2-.6-4.2 2.7-3 3.7.3.9.2 3.7 2.9 8 6.1L37 36l1.5 1.2.6-.4.1-.3-.7-1.1L33 25l-6-10.4-2.7-4.3-.7-2.6c-.3-1-.4-2-.4-3l3-4.2L28 0l4.2.6L33.8 2l2.6 6 4.1 9.3L47 29.9l2 3.8 1 3.4.3 1h.7v-.5l.5-7.2 1-8.7 1-11.2.3-3.2 1.6-3.8 3-2L61 2.6l2 2.9-.3 1.8-1.1 7.7L59 27.1l-1.5 8.2h.9l1-1.1 4.1-5.4 6.9-8.6 3-3.5L77 13l2.3-1.8h4.3l3.1 4.7-1.4 4.9-4.4 5.6-3.7 4.7-5.3 7.1-3.2 5.7.3.4h.7l12-2.6 6.4-1.1 7.6-1.3 3.5 1.6.4 1.6-1.4 3.4-8.2 2-9.6 2-14.3 3.3-.2.1.2.3 6.4.6 2.8.2h6.8l12.6 1 3.3 2 1.9 2.7-.3 2-5.1 2.6-6.8-1.6-16-3.8-5.4-1.3h-.8v.4l4.6 4.5 8.3 7.5L89 80.1l.5 2.4-1.3 2-1.4-.2-9.2-7-3.6-3-8-6.8h-.5v.7l1.8 2.7 9.8 14.7.5 4.5-.7 1.4-2.6 1-2.7-.6-5.8-8-6-9-4.7-8.2-.5.4-2.9 30.2-1.3 1.5-3 1.2-2.5-2-1.4-3 1.4-6.2 1.6-8 1.3-6.4 1.2-7.9.7-2.6v-.2H49L43 72l-9 12.3-7.2 7.6-1.7.7-3-1.5.3-2.8L24 86l10-12.8 6-7.9 4-4.6-.1-.5h-.3L17.2 77.4l-4.7.6-2-2 .2-3 1-1 8-5.5Z"; +export const CLAUDE_CODE_VIEWBOX = "0 0 100 100"; + +export const CODEX_MARK_PATH = + "M4.04286 0.228393C4.52451 0.0304495 5.0488 -0.0409817 5.56586 0.0208928C6.23236 0.0973928 6.82636 0.380893 7.34786 0.870893C7.35488 0.877545 7.36344 0.882351 7.37278 0.884881C7.38212 0.887412 7.39194 0.887588 7.40136 0.885393C8.10536 0.712393 8.78236 0.773393 9.43186 1.06839L9.46336 1.08339L9.54036 1.12139C10.2189 1.47289 10.7054 2.00639 10.9994 2.72039C11.1384 3.05989 11.2084 3.41439 11.2099 3.78339C11.2197 4.05816 11.1893 4.33288 11.1199 4.59889C11.1164 4.61245 11.1165 4.62665 11.12 4.6402C11.1235 4.65374 11.1303 4.66618 11.1399 4.67639C11.5329 5.0749 11.8063 5.57572 11.9289 6.12189C12.1214 7.07239 11.9239 7.92939 11.3374 8.69189L11.2464 8.80189C10.8579 9.24669 10.3481 9.56836 9.77936 9.72739C9.76694 9.73097 9.75556 9.73747 9.74617 9.74634C9.73677 9.75521 9.72964 9.7662 9.72536 9.77839C9.59786 10.1464 9.46986 10.4604 9.23186 10.7744C8.63236 11.5654 7.75086 12.0054 6.75786 11.9999C5.96636 11.9959 5.26486 11.7064 4.65286 11.1319C4.64358 11.1234 4.63225 11.1174 4.61998 11.1146C4.6077 11.1118 4.59491 11.1123 4.58286 11.1159C4.32386 11.1994 4.06286 11.2114 3.78086 11.2084C3.33033 11.2048 2.88658 11.0984 2.48336 10.8974C2.0613 10.688 1.6939 10.3831 1.41036 10.0069C1.30886 9.87239 1.20836 9.74589 1.13486 9.59639C1.03349 9.39033 0.95066 9.17565 0.887357 8.95489C0.754446 8.45324 0.751521 7.92599 0.878857 7.42289C0.882974 7.41102 0.884341 7.39837 0.882857 7.38589C0.88038 7.37348 0.873877 7.36223 0.864357 7.35389C0.556147 7.04213 0.320543 6.66619 0.174357 6.25289C0.0775698 5.99842 0.0213841 5.73031 0.00785682 5.45839C-0.0163229 5.10033 0.0153902 4.74069 0.101857 4.39239C0.326857 3.65039 0.756357 3.06839 1.39036 2.64589C1.53136 2.55189 1.66536 2.47889 1.79136 2.42689C1.93436 2.36689 2.07786 2.31689 2.22186 2.27489C2.23216 2.27184 2.24153 2.26626 2.24913 2.25866C2.25672 2.25107 2.2623 2.24169 2.26536 2.23139C2.37455 1.83888 2.56235 1.47264 2.81736 1.15489C3.15736 0.731893 3.56586 0.422893 4.04286 0.228393ZM3.64086 4.15339C3.58503 4.05573 3.49269 3.98424 3.38415 3.95465C3.27562 3.92507 3.15977 3.93981 3.06211 3.99564C2.96444 4.05147 2.89295 4.14381 2.86337 4.25235C2.83378 4.36088 2.84853 4.47673 2.90436 4.57439L3.75136 6.05689L2.90736 7.48089C2.85561 7.57738 2.84315 7.69014 2.87257 7.7956C2.902 7.90106 2.97104 7.99108 3.06526 8.04684C3.15949 8.1026 3.27162 8.1198 3.37823 8.09484C3.48484 8.06988 3.57768 8.00469 3.63736 7.91289L4.60736 6.27689C4.64561 6.21237 4.66609 6.13887 4.66671 6.06386C4.66732 5.98886 4.64805 5.91503 4.61086 5.84989L3.64086 4.15339ZM6.36386 7.27339C6.25583 7.27982 6.15434 7.32727 6.08012 7.40603C6.00591 7.48479 5.96458 7.58892 5.96458 7.69714C5.96458 7.80536 6.00591 7.90949 6.08012 7.98826C6.15434 8.06702 6.25583 8.11446 6.36386 8.12089H8.78786C8.89675 8.1156 8.99943 8.06862 9.07462 7.98969C9.14982 7.91075 9.19176 7.80591 9.19176 7.69689C9.19176 7.58787 9.14982 7.48303 9.07462 7.4041C8.99943 7.32516 8.89675 7.27818 8.78786 7.27289H6.36386V7.27339Z"; +export const CODEX_VIEWBOX = "0 0 12 12"; + +export const GEMINI_MARK_PATH = + "M12 0C12.904 6.056 17.944 11.096 24 12C17.944 12.904 12.904 17.944 12 24C11.096 17.944 6.056 12.904 0 12C6.056 11.096 11.096 6.056 12 0Z"; +export const GEMINI_VIEWBOX = "0 0 24 24"; diff --git a/src/constants/acp-providers.ts b/src/constants/acp-providers.ts index aa88a54f..308abafa 100644 --- a/src/constants/acp-providers.ts +++ b/src/constants/acp-providers.ts @@ -123,6 +123,23 @@ export function getAcpProviderDisplayName( return found ? found.display_name : null; } +/** + * Resolve an ACP provider registry key to the icon discriminator the + * conversation chip should render alongside the model text. + * + * Falls back to {@link ACP_PROVIDER_FALLBACK_ICON} for ``"custom"``, + * unknown keys, or a missing key — the chip then shows a neutral + * terminal glyph that still communicates "this is an ACP conversation" + * without claiming a brand identity we don't know. + */ +export function resolveAcpProviderIcon( + key: string | null | undefined, +): ACPProviderIcon { + if (!key) return ACP_PROVIDER_FALLBACK_ICON; + const found = ACP_PROVIDERS.find((p) => p.key === key); + return found?.icon ?? ACP_PROVIDER_FALLBACK_ICON; +} + /** * Build the ``agent_settings_diff`` payload PATCH /api/settings expects * for the agent-kind/provider choice the user just made. From e69ff3f250d4bb0515e2de0394afd1c681706e2f Mon Sep 17 00:00:00 2001 From: Debug Agent Date: Thu, 21 May 2026 16:57:42 +0200 Subject: [PATCH 2/4] chore: bump SDK pin to PR #3347 commit e6e9771 (cold-read fix) The previous pin (235fed007) populated ``current_model_*`` only while the ACP subprocess was live; idle / cold-listed conversations came back with both fields null and the chip fell back to the provider brand ("Claude Code") instead of the model name. The new SDK commit persists the resolved id + name into ``agent_state`` and reads them back on cold conversation reads in ``_compose_conversation_info``, so the chip shows the model on every ACP conversation in the sidebar. Co-Authored-By: Claude Opus 4.7 (1M context) --- config/defaults.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/defaults.json b/config/defaults.json index 398e0774..023c0890 100644 --- a/config/defaults.json +++ b/config/defaults.json @@ -7,8 +7,8 @@ "automationSdk": "1.22.1" }, - "_agentServerGitRefComment": "Set to pin the dev agent-server to a specific software-agent-sdk commit/branch. Takes precedence over versions.agentServer when present. Use this to track an unreleased SDK PR; clear it (or remove the key) once that PR is merged + released. Current pin: software-agent-sdk PR #3347 (commit 235fed007c3c49841d34a92fb4b16d8877d6d82e) — surfaces ConversationInfo.current_model_name + .current_model_id, which this UI now renders in the conversation chip.", - "agentServerGitRef": "235fed007c3c49841d34a92fb4b16d8877d6d82e", + "_agentServerGitRefComment": "Set to pin the dev agent-server to a specific software-agent-sdk commit/branch. Takes precedence over versions.agentServer when present. Use this to track an unreleased SDK PR; clear it (or remove the key) once that PR is merged + released. Current pin: software-agent-sdk PR #3347 (commit e6e97713818dadd4d2f0ac0e327067240a7d2280) — surfaces ConversationInfo.current_model_name + .current_model_id (persisted in agent_state so cold sidebar reads also carry the values), which this UI now renders in the conversation chip.", + "agentServerGitRef": "e6e97713818dadd4d2f0ac0e327067240a7d2280", "images": { "agentServer": "ghcr.io/openhands/agent-server", From ac481a50a56b95b8ff20d38e3f48520780b9189d Mon Sep 17 00:00:00 2001 From: Debug Agent Date: Thu, 21 May 2026 17:21:53 +0200 Subject: [PATCH 3/4] chore: bump SDK pin to PR #3347 commit c1e23d1 (description-resolved name) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previous pin (e6e9771) surfaced ``ModelInfo.name`` which for claude-agent-acp's generic aliases is still an alias ("Default (recommended)") — not what's actually running. The new SDK commit extracts the resolved model identity from ``ModelInfo.description`` for known aliases, so the chip now reads "Opus 4.7 with 1M context" / "Sonnet 4.6" / "Haiku 4.5" instead. Co-Authored-By: Claude Opus 4.7 (1M context) --- config/defaults.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/defaults.json b/config/defaults.json index 023c0890..97bc9156 100644 --- a/config/defaults.json +++ b/config/defaults.json @@ -7,8 +7,8 @@ "automationSdk": "1.22.1" }, - "_agentServerGitRefComment": "Set to pin the dev agent-server to a specific software-agent-sdk commit/branch. Takes precedence over versions.agentServer when present. Use this to track an unreleased SDK PR; clear it (or remove the key) once that PR is merged + released. Current pin: software-agent-sdk PR #3347 (commit e6e97713818dadd4d2f0ac0e327067240a7d2280) — surfaces ConversationInfo.current_model_name + .current_model_id (persisted in agent_state so cold sidebar reads also carry the values), which this UI now renders in the conversation chip.", - "agentServerGitRef": "e6e97713818dadd4d2f0ac0e327067240a7d2280", + "_agentServerGitRefComment": "Set to pin the dev agent-server to a specific software-agent-sdk commit/branch. Takes precedence over versions.agentServer when present. Use this to track an unreleased SDK PR; clear it (or remove the key) once that PR is merged + released. Current pin: software-agent-sdk PR #3347 (commit c1e23d166) — surfaces ConversationInfo.current_model_name + .current_model_id; for generic aliases (default/sonnet/opus/haiku) the name is now resolved from ModelInfo.description's first segment (e.g. 'Opus 4.7 with 1M context') instead of ModelInfo.name (which is also an alias like 'Default (recommended)').", + "agentServerGitRef": "c1e23d166", "images": { "agentServer": "ghcr.io/openhands/agent-server", From 62a115a9e355fa02aac3602c7c00e0c00951fcbd Mon Sep 17 00:00:00 2001 From: Debug Agent Date: Thu, 21 May 2026 17:53:08 +0200 Subject: [PATCH 4/4] chore: bump SDK pin to PR #3347 + #3349 merge (6aeb0817b) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #3347 alone tripped a pre-existing deadlock in \`EventService._setup_stats_streaming\` (introduced in #3308) where the stats_callback re-acquires the conversation state lock the caller already holds, hanging every ACP turn before the assistant's FinishAction is emitted. The new pin merges PR #3349 — which drops the redundant \`with state:\` — into our PR branch so both the chip data and the response flow work together. Drop both pins once #3347 and #3349 merge to main and a new SDK release ships. --- config/defaults.json | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/config/defaults.json b/config/defaults.json index 97bc9156..e042b70f 100644 --- a/config/defaults.json +++ b/config/defaults.json @@ -1,41 +1,34 @@ { "_comment": "Single source of truth for version pins, ports, paths, and defaults shared across the npm and Docker install paths. Read by scripts/dev-safe.mjs, scripts/dev-with-automation.mjs, docker/entrypoint.sh (via generated defaults.env), and .github/workflows/docker.yml.", - "versions": { "agentServer": "1.23.0", "automation": "1.0.0a3", "automationSdk": "1.22.1" }, - - "_agentServerGitRefComment": "Set to pin the dev agent-server to a specific software-agent-sdk commit/branch. Takes precedence over versions.agentServer when present. Use this to track an unreleased SDK PR; clear it (or remove the key) once that PR is merged + released. Current pin: software-agent-sdk PR #3347 (commit c1e23d166) — surfaces ConversationInfo.current_model_name + .current_model_id; for generic aliases (default/sonnet/opus/haiku) the name is now resolved from ModelInfo.description's first segment (e.g. 'Opus 4.7 with 1M context') instead of ModelInfo.name (which is also an alias like 'Default (recommended)').", - "agentServerGitRef": "c1e23d166", - + "_agentServerGitRefComment": "Set to pin the dev agent-server to a specific software-agent-sdk commit/branch. Takes precedence over versions.agentServer when present. Current pin: software-agent-sdk PR #3347 + the deadlock fix PR #3349 merged in (commit 6aeb0817b) \u2014 surfaces current_model_name and fixes the stats_callback re-entry that hung every ACP turn.", + "agentServerGitRef": "6aeb0817bbaa6009fa833df8578c9efe36b973a9", "images": { "agentServer": "ghcr.io/openhands/agent-server", "agentCanvas": "ghcr.io/openhands/agent-canvas" }, - "ports": { "agentServer": 18000, "automation": 18001, "proxy": 8000 }, - "paths": { "stateSubdir": "agent-canvas", "conversations": "agent-canvas/conversations", "bashEvents": "agent-canvas/bash_events", "automationDb": "automation/automations.db" }, - "packages": { "agentServer": "openhands-agent-server", "automation": "openhands-automation", "tools": "openhands-tools", "workspace": "openhands-workspace" }, - "defaults": { "secretKey": "openhands-dev-secret-key-change-in-prod" } -} +} \ No newline at end of file