mirror of
https://github.com/OpenHands/OpenHands.git
synced 2026-10-07 16:38:34 +08:00
feat(acp): show ACP agent name on conversation cards (#658)
The ``acpserver`` conversation tag has been stamped at create time since the ACP integration landed, but the read path silently dropped it — so the sidebar had no way to tell a Claude-Code conversation from a Codex one. Plumb the tag through ``DirectConversationInfo`` → ``AppConversation`` → ``ConversationCard`` / ``CompactConversationRow`` and render a small pill above the LLM-model line. Closes the "conversation list with agent tags" half of agent-canvas#405. Co-authored-by: Debug Agent <debug@example.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
committed by
GitHub
co-authored by
Debug Agent
Claude Opus 4.7
parent
ad5c5533a9
commit
2aa262acdc
@@ -567,6 +567,48 @@ describe("toAppConversation", () => {
|
||||
expect(result.agent_kind).toBe("acp");
|
||||
expect(result.llm_model).toBeNull();
|
||||
});
|
||||
|
||||
it("surfaces acp_server from tags.acpserver for ACP conversations", () => {
|
||||
// The ``acpserver`` conversation tag is stamped at create time
|
||||
// (``buildStartConversationRequest``) but never previously plumbed
|
||||
// through on read — the sidebar chip in agent-canvas#405 needs this
|
||||
// value to resolve the human display name ("Claude Code" / "Codex" /
|
||||
// "Gemini CLI").
|
||||
const result = toAppConversation({
|
||||
...baseInfo,
|
||||
agent: { kind: "ACPAgent", llm: { model: "acp-managed" } },
|
||||
tags: { [ACP_SERVER_TAG_KEY]: "claude-code" },
|
||||
});
|
||||
expect(result.acp_server).toBe("claude-code");
|
||||
});
|
||||
|
||||
it("leaves acp_server null when an ACP conversation has no tag stamped", () => {
|
||||
// Older conversations created before the tag was added, or ACP
|
||||
// conversations created via the raw API, won't have the tag. The
|
||||
// sidebar should still render a chip ("ACP") — but the resolver gets
|
||||
// null here and the UI fallback handles the generic label.
|
||||
const result = toAppConversation({
|
||||
...baseInfo,
|
||||
agent: { kind: "ACPAgent", llm: { model: "acp-managed" } },
|
||||
});
|
||||
expect(result.agent_kind).toBe("acp");
|
||||
expect(result.acp_server).toBeNull();
|
||||
});
|
||||
|
||||
it("ignores tags.acpserver on OpenHands conversations to prevent stray-tag bleed", () => {
|
||||
// The agent-server's pydantic model doesn't enforce that ``acpserver``
|
||||
// is only stamped on ACP conversations. Defensively gating on
|
||||
// ``agent.kind === "ACPAgent"`` keeps a misconfigured tag from
|
||||
// turning the sidebar of an OpenHands conversation into "Claude
|
||||
// Code". Pairs with the ``llm_model`` null-out for ACP.
|
||||
const result = toAppConversation({
|
||||
...baseInfo,
|
||||
agent: { kind: "Agent", llm: { model: "claude-sonnet-4-6" } },
|
||||
tags: { [ACP_SERVER_TAG_KEY]: "claude-code" },
|
||||
});
|
||||
expect(result.agent_kind).toBe("openhands");
|
||||
expect(result.acp_server).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildRuntimeServicesSystemSuffix", () => {
|
||||
|
||||
@@ -497,6 +497,66 @@ describe("AgentServerConversationService", () => {
|
||||
"/workspace/project/agent-canvas",
|
||||
);
|
||||
});
|
||||
|
||||
it("extracts the acpserver tag from the wire payload for the sidebar chip", async () => {
|
||||
// The agent-server stamps ``tags.acpserver`` at conversation create
|
||||
// time (see ``buildStartConversationRequest``); the read path
|
||||
// must surface it so the conversation card can render the human
|
||||
// ACP-agent badge ("Claude Code" / "Codex" / "Gemini CLI").
|
||||
mockHttpGet.mockResolvedValue({
|
||||
data: [
|
||||
{
|
||||
id: "conv-acp",
|
||||
created_at: "2024-01-01",
|
||||
updated_at: "2024-01-01",
|
||||
agent: { kind: "ACPAgent", llm: { model: "acp-managed" } },
|
||||
tags: { acpserver: "claude-code" },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const [conversation] =
|
||||
await AgentServerConversationService.batchGetAppConversations([
|
||||
"conv-acp",
|
||||
]);
|
||||
|
||||
expect(conversation?.agent_kind).toBe("acp");
|
||||
expect(conversation?.acp_server).toBe("claude-code");
|
||||
});
|
||||
|
||||
it("drops non-string tag values while preserving the well-typed ones", async () => {
|
||||
// The wire field is server-validated to ``Record[str, str]`` but a
|
||||
// misbehaving server (or a future schema drift) shouldn't crash the
|
||||
// parser — we drop non-string values and keep the rest so the
|
||||
// sidebar still gets whatever good keys made it through.
|
||||
mockHttpGet.mockResolvedValue({
|
||||
data: [
|
||||
{
|
||||
id: "conv-malformed-tags",
|
||||
created_at: "2024-01-01",
|
||||
updated_at: "2024-01-01",
|
||||
agent: { kind: "ACPAgent", llm: { model: "acp-managed" } },
|
||||
tags: {
|
||||
acpserver: "codex",
|
||||
numeric: 42,
|
||||
nested: { inner: "x" },
|
||||
listy: ["a", "b"],
|
||||
nully: null,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const [conversation] =
|
||||
await AgentServerConversationService.batchGetAppConversations([
|
||||
"conv-malformed-tags",
|
||||
]);
|
||||
|
||||
// ``acp_server`` is the surfaced field on AppConversation; tags is
|
||||
// only on DirectConversationInfo. Asserting both via this read
|
||||
// path keeps the test honest end-to-end.
|
||||
expect(conversation?.acp_server).toBe("codex");
|
||||
});
|
||||
});
|
||||
|
||||
describe("switchProfile", () => {
|
||||
|
||||
@@ -38,6 +38,7 @@ vi.mock("react-i18next", async () => {
|
||||
CONVERSATION$AGO: "ago",
|
||||
CONVERSATION$UPDATED: "Updated",
|
||||
COMMON$NO_REPOSITORY: "No repository",
|
||||
CONVERSATION$ACP_AGENT_GENERIC: "ACP",
|
||||
};
|
||||
return translations[key] || key;
|
||||
},
|
||||
@@ -595,4 +596,81 @@ 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.
|
||||
renderWithProviders(
|
||||
<ConversationCard
|
||||
title="Conversation 1"
|
||||
selectedRepository={null}
|
||||
lastUpdatedAt="2021-10-01T12:00:00Z"
|
||||
agentKind="acp"
|
||||
acpServer="claude-code"
|
||||
/>,
|
||||
);
|
||||
|
||||
const badge = screen.getByTestId("conversation-card-acp-badge");
|
||||
expect(badge).toHaveTextContent("Claude Code");
|
||||
});
|
||||
|
||||
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.
|
||||
renderWithProviders(
|
||||
<ConversationCard
|
||||
title="Conversation 1"
|
||||
selectedRepository={null}
|
||||
lastUpdatedAt="2021-10-01T12:00:00Z"
|
||||
agentKind="acp"
|
||||
acpServer="custom"
|
||||
/>,
|
||||
);
|
||||
|
||||
const badge = screen.getByTestId("conversation-card-acp-badge");
|
||||
expect(badge).toHaveTextContent("ACP");
|
||||
});
|
||||
|
||||
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(
|
||||
<ConversationCard
|
||||
title="Conversation 1"
|
||||
selectedRepository={null}
|
||||
lastUpdatedAt="2021-10-01T12:00:00Z"
|
||||
agentKind="acp"
|
||||
acpServer={null}
|
||||
/>,
|
||||
);
|
||||
|
||||
const badge = screen.getByTestId("conversation-card-acp-badge");
|
||||
expect(badge).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"``.
|
||||
renderWithProviders(
|
||||
<ConversationCard
|
||||
title="Conversation 1"
|
||||
selectedRepository={null}
|
||||
lastUpdatedAt="2021-10-01T12:00:00Z"
|
||||
agentKind="openhands"
|
||||
acpServer="claude-code"
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.queryByTestId("conversation-card-acp-badge"),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { getAcpProviderDisplayName } from "#/constants/acp-providers";
|
||||
|
||||
describe("getAcpProviderDisplayName", () => {
|
||||
it("resolves the three built-in registry keys to their human names", () => {
|
||||
expect(getAcpProviderDisplayName("claude-code")).toBe("Claude Code");
|
||||
expect(getAcpProviderDisplayName("codex")).toBe("Codex");
|
||||
expect(getAcpProviderDisplayName("gemini-cli")).toBe("Gemini CLI");
|
||||
});
|
||||
|
||||
it("returns null for the Custom-command preset so callers can fall back to the generic 'ACP' label", () => {
|
||||
// The custom preset has no canonical brand name — the registry
|
||||
// resolver intentionally returns null so the conversation card renders
|
||||
// ``CONVERSATION$ACP_AGENT_GENERIC`` ("ACP") instead.
|
||||
expect(getAcpProviderDisplayName("custom")).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null for unknown / forward-compatible keys", () => {
|
||||
// A future ACP server Canvas's registry doesn't know about yet
|
||||
// shouldn't crash or render a random fragment of the key — fall back
|
||||
// to the generic chip.
|
||||
expect(getAcpProviderDisplayName("future-acp-server")).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null for empty / null / undefined input", () => {
|
||||
expect(getAcpProviderDisplayName(null)).toBeNull();
|
||||
expect(getAcpProviderDisplayName(undefined)).toBeNull();
|
||||
expect(getAcpProviderDisplayName("")).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -55,6 +55,15 @@ export interface DirectConversationInfo {
|
||||
workspace?: {
|
||||
working_dir?: string | null;
|
||||
} | null;
|
||||
/**
|
||||
* Arbitrary string-keyed conversation tags surfaced by the agent-server
|
||||
* (see ``ConversationInfo.tags``). Canvas only consumes one key today —
|
||||
* ``ACP_SERVER_TAG_KEY`` ("acpserver") — but the field is typed as a
|
||||
* generic record so future readers don't need another wire-shape change.
|
||||
* Keys are constrained to ``^[a-z0-9]+$`` by the agent-server validator;
|
||||
* values are opaque strings.
|
||||
*/
|
||||
tags?: Record<string, string> | null;
|
||||
}
|
||||
|
||||
// Module qualname for the Canvas-UI tool. The agent-server imports this via
|
||||
@@ -246,6 +255,11 @@ export function toAppConversation(
|
||||
// keeps its own model. Null at the boundary so no consumer has to
|
||||
// re-derive the rule. Mirrors OpenHands PR #14401.
|
||||
const isAcp = info.agent?.kind === "ACPAgent";
|
||||
// Only surface ``acp_server`` for ACP conversations even if the wire
|
||||
// payload accidentally carries an ``acpserver`` tag on an OpenHands
|
||||
// conversation — the chip is identity info for the ACP CLI subprocess,
|
||||
// and showing it on a non-ACP conversation would be a lie.
|
||||
const acpServer = isAcp ? (info.tags?.[ACP_SERVER_TAG_KEY] ?? null) : null;
|
||||
return {
|
||||
id: info.id,
|
||||
created_by_user_id: null,
|
||||
@@ -259,6 +273,7 @@ export function toAppConversation(
|
||||
trigger: null,
|
||||
pr_number: [],
|
||||
agent_kind: isAcp ? "acp" : "openhands",
|
||||
acp_server: acpServer,
|
||||
llm_model: isAcp
|
||||
? null
|
||||
: (info.agent?.llm?.model ?? DEFAULT_SETTINGS.llm_model),
|
||||
|
||||
@@ -128,7 +128,12 @@ function normalizeAgent(value: unknown): DirectConversationInfo["agent"] {
|
||||
const llm = isRecord(value.llm)
|
||||
? { model: stringOrNull(value.llm.model) }
|
||||
: null;
|
||||
return { llm };
|
||||
// ``kind`` is the SDK's pydantic discriminator (``"Agent"`` vs ``"ACPAgent"``);
|
||||
// ``toAppConversation`` reads it to derive ``agent_kind`` and to gate the
|
||||
// ACP-server chip + ``llm_model`` null-out. Preserving it here makes the
|
||||
// wire path agree with the unit-test path that builds ``DirectConversationInfo``
|
||||
// directly (e.g. ``__tests__/api/agent-server-adapter.test.ts``).
|
||||
return { kind: stringOrNull(value.kind), llm };
|
||||
}
|
||||
|
||||
function normalizeWorkspace(
|
||||
@@ -138,6 +143,27 @@ function normalizeWorkspace(
|
||||
return { working_dir: stringOrNull(value.working_dir) };
|
||||
}
|
||||
|
||||
/**
|
||||
* Accept the agent-server's ``tags: Record[str, str]`` payload defensively:
|
||||
* the wire shape is guaranteed by the server-side validator (keys
|
||||
* ``^[a-z0-9]+$``, string values), but a non-conforming response (older
|
||||
* server, raw API write, future schema drift) must never crash the parser
|
||||
* — Canvas only consumes ``acpserver`` and falls back to a generic chip
|
||||
* for anything it doesn't recognize. Drop entries whose value isn't a
|
||||
* plain string; return ``null`` when the wire field is absent or not an
|
||||
* object so consumers can use ``info.tags?.[KEY] ?? null`` uniformly.
|
||||
*/
|
||||
function normalizeTags(value: unknown): Record<string, string> | null {
|
||||
if (!isRecord(value)) return null;
|
||||
const tags: Record<string, string> = {};
|
||||
for (const [key, entry] of Object.entries(value)) {
|
||||
if (typeof entry === "string") {
|
||||
tags[key] = entry;
|
||||
}
|
||||
}
|
||||
return tags;
|
||||
}
|
||||
|
||||
function normalizeAbsolutePath(path: string): string | null {
|
||||
if (!path.startsWith("/")) return null;
|
||||
|
||||
@@ -187,6 +213,7 @@ function requireDirectConversationInfo(item: unknown): DirectConversationInfo {
|
||||
metrics: normalizeMetrics(item.metrics),
|
||||
agent: normalizeAgent(item.agent),
|
||||
workspace: normalizeWorkspace(item.workspace),
|
||||
tags: normalizeTags(item.tags),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -131,6 +131,19 @@ export interface AppConversation {
|
||||
* on the subprocess via ``acp_model``, not on ``llm_model``).
|
||||
*/
|
||||
agent_kind?: "openhands" | "acp" | null;
|
||||
/**
|
||||
* For ACP conversations, the registry key of the ACP CLI server the
|
||||
* conversation was launched against (e.g. ``"claude-code"``, ``"codex"``,
|
||||
* ``"gemini-cli"``). Populated from ``info.tags.acpserver`` — see
|
||||
* ``ACP_SERVER_TAG_KEY`` in ``agent-server-adapter.ts`` for the wire
|
||||
* format and the rationale behind the snake_case-incompatible
|
||||
* ``acpserver`` form. ``null`` for OpenHands conversations and for ACP
|
||||
* conversations whose tag wasn't stamped (e.g. created via an older
|
||||
* client or via the raw API). Consumers resolve the display name via
|
||||
* ``getAcpProviderDisplayName(acp_server)`` and fall back to a generic
|
||||
* "ACP" chip when the key is unknown or null.
|
||||
*/
|
||||
acp_server?: string | null;
|
||||
llm_model: string | null;
|
||||
metrics: MetricsSnapshot | null;
|
||||
created_at: string;
|
||||
|
||||
@@ -22,6 +22,8 @@ interface CompactConversationRowProps {
|
||||
showRepositoryMetadata?: boolean;
|
||||
llmModel?: string | null;
|
||||
showLlmProfiles?: boolean;
|
||||
agentKind?: "openhands" | "acp" | null;
|
||||
acpServer?: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -43,6 +45,8 @@ export function CompactConversationRow({
|
||||
showRepositoryMetadata = true,
|
||||
llmModel = null,
|
||||
showLlmProfiles = false,
|
||||
agentKind = null,
|
||||
acpServer = null,
|
||||
}: CompactConversationRowProps) {
|
||||
const disableAnimation = import.meta.env.MODE === "test";
|
||||
|
||||
@@ -67,6 +71,8 @@ export function CompactConversationRow({
|
||||
showRepositoryMetadata={showRepositoryMetadata}
|
||||
llmModel={llmModel}
|
||||
showLlmModel={showLlmProfiles}
|
||||
agentKind={agentKind}
|
||||
acpServer={acpServer}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
+38
@@ -5,6 +5,7 @@ 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 { ConversationRepoLink } from "./conversation-repo-link";
|
||||
import { NoRepository } from "./no-repository";
|
||||
|
||||
@@ -18,6 +19,24 @@ interface ConversationCardFooterProps {
|
||||
showTimestamp?: boolean;
|
||||
llmModel?: string | null;
|
||||
showLlmModel?: boolean;
|
||||
/**
|
||||
* High-level kind of the conversation's agent. The ACP-agent chip is
|
||||
* only rendered when this is ``"acp"``. The OpenHands rendering path
|
||||
* is intentionally untouched — for OpenHands conversations the chip is
|
||||
* suppressed regardless of any ``acpServer`` value (defensive against
|
||||
* stray wire tags on non-ACP conversations).
|
||||
*/
|
||||
agentKind?: "openhands" | "acp" | null;
|
||||
/**
|
||||
* Registry key of the ACP CLI server (``"claude-code"`` / ``"codex"`` /
|
||||
* ``"gemini-cli"`` / unknown / null). Resolved to a human display name
|
||||
* via {@link getAcpProviderDisplayName}; unknown / null falls back to
|
||||
* a generic "ACP" label so a Custom-command preset still produces a
|
||||
* useful chip. Always shown for ACP conversations — this is identity
|
||||
* info, not gated by the ``showLlmModel`` preference (which is about
|
||||
* LLM model strings, an orthogonal concern).
|
||||
*/
|
||||
acpServer?: string | null;
|
||||
}
|
||||
|
||||
export function ConversationCardFooter({
|
||||
@@ -30,11 +49,19 @@ export function ConversationCardFooter({
|
||||
showTimestamp = true,
|
||||
llmModel,
|
||||
showLlmModel = false,
|
||||
agentKind = null,
|
||||
acpServer = null,
|
||||
}: ConversationCardFooterProps) {
|
||||
const { t } = useTranslation("openhands");
|
||||
|
||||
const isPaused = isExecutionPaused(executionStatus);
|
||||
|
||||
const acpDisplayName =
|
||||
agentKind === "acp"
|
||||
? (getAcpProviderDisplayName(acpServer) ??
|
||||
t(I18nKey.CONVERSATION$ACP_AGENT_GENERIC))
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
@@ -42,6 +69,17 @@ export function ConversationCardFooter({
|
||||
isPaused && "opacity-60",
|
||||
)}
|
||||
>
|
||||
{acpDisplayName ? (
|
||||
<div className="pl-[18px]">
|
||||
<span
|
||||
data-testid="conversation-card-acp-badge"
|
||||
className="inline-flex items-center px-1.5 py-0.5 rounded-full bg-[var(--oh-surface-raised)] text-[var(--oh-muted)] text-xs font-medium max-w-full truncate"
|
||||
title={acpDisplayName}
|
||||
>
|
||||
{acpDisplayName}
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
{showLlmModel && llmModel ? (
|
||||
<span
|
||||
className="min-w-0 max-w-full truncate pl-[18px] text-xs text-[var(--oh-muted)]"
|
||||
|
||||
@@ -33,6 +33,8 @@ interface ConversationCardProps {
|
||||
showRepositoryMetadata?: boolean;
|
||||
llmModel?: string | null;
|
||||
showLlmProfiles?: boolean;
|
||||
agentKind?: "openhands" | "acp" | null;
|
||||
acpServer?: string | null;
|
||||
}
|
||||
|
||||
export function ConversationCard({
|
||||
@@ -55,6 +57,8 @@ export function ConversationCard({
|
||||
showRepositoryMetadata = true,
|
||||
llmModel = null,
|
||||
showLlmProfiles = false,
|
||||
agentKind = null,
|
||||
acpServer = null,
|
||||
}: ConversationCardProps) {
|
||||
const posthog = usePostHog();
|
||||
const [titleMode, setTitleMode] = React.useState<"view" | "edit">("view");
|
||||
@@ -128,7 +132,9 @@ export function ConversationCard({
|
||||
|
||||
const hasContextMenu = !!(onDelete || onChangeTitle || showOptions);
|
||||
const shouldRenderFooter =
|
||||
showRepositoryMetadata || (!!llmModel && showLlmProfiles);
|
||||
showRepositoryMetadata ||
|
||||
(!!llmModel && showLlmProfiles) ||
|
||||
agentKind === "acp";
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -210,6 +216,8 @@ export function ConversationCard({
|
||||
showTimestamp={false}
|
||||
llmModel={llmModel}
|
||||
showLlmModel={showLlmProfiles}
|
||||
agentKind={agentKind}
|
||||
acpServer={acpServer}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -399,6 +399,8 @@ export function ConversationPanel({
|
||||
showRepositoryMetadata={showRepoBranchMetadata}
|
||||
llmModel={conversation.llm_model}
|
||||
showLlmProfiles={showLlmProfiles}
|
||||
agentKind={conversation.agent_kind}
|
||||
acpServer={conversation.acp_server}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -440,6 +442,8 @@ export function ConversationPanel({
|
||||
showRepositoryMetadata={showRepoBranchMetadata}
|
||||
llmModel={conversation.llm_model}
|
||||
showLlmProfiles={showLlmProfiles}
|
||||
agentKind={conversation.agent_kind}
|
||||
acpServer={conversation.acp_server}
|
||||
/>
|
||||
</NavigationLink>
|
||||
);
|
||||
|
||||
@@ -99,6 +99,30 @@ export const ACP_PROVIDERS: ACPProviderConfig[] = [
|
||||
|
||||
export const ACP_CUSTOM_PRESET_KEY = "custom";
|
||||
|
||||
/**
|
||||
* Resolve an ACP provider registry key (the value stored under
|
||||
* ``tags.acpserver`` on a conversation) to a human display name for the
|
||||
* sidebar chip.
|
||||
*
|
||||
* Returns ``null`` for an empty / null key and for keys not in
|
||||
* {@link ACP_PROVIDERS} — most notably ``"custom"`` (the user-supplied
|
||||
* command preset has no canonical brand name) and any forward-compatible
|
||||
* value Canvas's registry doesn't know about yet. Callers should fall
|
||||
* back to a generic ``"ACP"`` label in that case so the chip still
|
||||
* communicates "this is an ACP conversation".
|
||||
*
|
||||
* Kept separate from {@link buildAcpAgentSettingsDiff}'s lookup so the
|
||||
* conversation-card render path can resolve display names without
|
||||
* importing the settings-payload builder.
|
||||
*/
|
||||
export function getAcpProviderDisplayName(
|
||||
key: string | null | undefined,
|
||||
): string | null {
|
||||
if (!key) return null;
|
||||
const found = ACP_PROVIDERS.find((p) => p.key === key);
|
||||
return found ? found.display_name : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the ``agent_settings_diff`` payload PATCH /api/settings expects
|
||||
* for the agent-kind/provider choice the user just made.
|
||||
|
||||
@@ -9723,6 +9723,23 @@
|
||||
"uk": "тому",
|
||||
"ca": "fa"
|
||||
},
|
||||
"CONVERSATION$ACP_AGENT_GENERIC": {
|
||||
"en": "ACP",
|
||||
"ja": "ACP",
|
||||
"zh-CN": "ACP",
|
||||
"zh-TW": "ACP",
|
||||
"ko-KR": "ACP",
|
||||
"de": "ACP",
|
||||
"no": "ACP",
|
||||
"it": "ACP",
|
||||
"pt": "ACP",
|
||||
"es": "ACP",
|
||||
"ar": "ACP",
|
||||
"fr": "ACP",
|
||||
"tr": "ACP",
|
||||
"uk": "ACP",
|
||||
"ca": "ACP"
|
||||
},
|
||||
"GITHUB$VSCODE_LINK_DESCRIPTION": {
|
||||
"en": "and use the VS Code link to upload and download your code",
|
||||
"ja": "そしてVS Codeリンクを使用してコードをアップロードおよびダウンロードします",
|
||||
|
||||
Reference in New Issue
Block a user