Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions frontend/src/components/DesktopAppSection.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -180,4 +180,27 @@ describe("DesktopAppSection", () => {

expect(screen.queryByRole("heading", { name: "桌面 Agent" })).toBeNull();
});

// This component renders every desktop Agent, and it used to hardcode
// agentId="codex" for all of them. WorkBuddy is a Tencent product, so its card
// displayed OpenAI's mark -- a trademark problem rather than a styling one.
it("marks each desktop Agent with its own icon, not the first one's", () => {
const markOf = (value: DesktopAgentStatus) => {
const { container } = render(
<TaskCenterProvider>
<DesktopAppSection app={value} onChanged={vi.fn()} />
</TaskCenterProvider>,
);
return container.querySelector(".desktop-app-icon")!.innerHTML;
};

const chatgpt = markOf(app({ id: "chatgpt-desktop", name: "ChatGPT Desktop" }));
const workbuddy = markOf(app({ id: "workbuddy", name: "WorkBuddy", profileAgentId: "workbuddy", protocol: "openai" }));

expect(workbuddy).not.toBe(chatgpt);
// ChatGPT Desktop is OpenAI's own app, so a licensed asset is correct there.
expect(chatgpt).toContain('data-mark-kind="asset"');
// WorkBuddy has no licensed mark, so the generic fallback is the right answer.
expect(workbuddy).toContain('data-mark-kind="fallback"');
});
});
7 changes: 6 additions & 1 deletion frontend/src/components/DesktopAppSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,12 @@ export function DesktopAppSection({ app: desktopApp, onChanged, onSetup, onConfi
) : null}
<div className="desktop-app-summary">
<div className="desktop-app-identity">
<span className="desktop-app-icon"><AgentIcon agentId="codex" size={20} /></span>
{/* The Agent's own id, never a literal. This rendered agentId="codex"
for every desktop Agent, so WorkBuddy -- a different vendor's
product -- displayed OpenAI's mark. A literal also bypasses
AgentIcon's fallback, which is what handles an Agent that has no
mark of its own. */}
<span className="desktop-app-icon"><AgentIcon agentId={desktopApp.id} size={20} /></span>
<span>
<strong>{desktopApp.name}</strong>
</span>
Expand Down
29 changes: 28 additions & 1 deletion frontend/src/components/icons/agents.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ describe("AgentIcon", () => {
// set rather than one example is what makes an unregistered mark fail here:
// shipping artwork without a source, licence and hash is the defect.
const assetIds = AGENT_ICON_IDS.filter((id) => agentMarkKind(id) === "asset");
expect(assetIds.sort()).toEqual(["claude-code", "codex", "kilo-cli", "openclaw", "opencode"]);
// chatgpt-desktop is a desktop Agent rather than a CLI, and it reuses the
// OpenAI mark because it is OpenAI's own product sharing Codex's config.
expect(assetIds.sort()).toEqual(["chatgpt-desktop", "claude-code", "codex", "kilo-cli", "openclaw", "opencode"]);
for (const id of assetIds) {
const rights = agentMarkRights(id);
expect(agentMarkKind(id)).toBe("asset");
Expand Down Expand Up @@ -110,6 +112,7 @@ describe("AgentIcon", () => {
// recorded in asset-rights.json does not touch geometry.
const PUBLISHED_VIEWBOX: Record<string, string> = {
codex: "0 0 24 24",
"chatgpt-desktop": "0 0 24 24",
opencode: "0 0 24 24",
"claude-code": "0 0 24 24",
"kilo-cli": "0 0 24 24",
Expand Down Expand Up @@ -144,6 +147,30 @@ describe("AgentIcon", () => {
}
});

it("gives a desktop Agent its own mark rather than another vendor's", () => {
// The desktop card used to pass a literal agentId="codex" for every desktop
// Agent, so WorkBuddy -- a Tencent product -- rendered OpenAI's mark. Reusing
// one vendor's artwork for another vendor's product is a trademark problem,
// not a cosmetic one, so each case is asserted separately.
//
// ChatGPT Desktop is the one legitimate reuse: it is OpenAI's own app and
// shares Codex's configuration, so it renders the same OpenAI mark.
const { container: chatgpt } = render(<AgentIcon agentId="chatgpt-desktop" />);
const { container: codex } = render(<AgentIcon agentId="codex" />);
expect(chatgpt.innerHTML).toBe(codex.innerHTML);

// WorkBuddy has no licensed mark, so it must fall back to the generic symbol
// and must not borrow one that belongs to somebody else.
const { container: workbuddy } = render(<AgentIcon agentId="workbuddy" />);
expect(agentMarkKind("workbuddy")).toBe("fallback");
expect(workbuddy.querySelector('[data-mark-kind="fallback"]')).not.toBeNull();
expect(workbuddy.innerHTML).not.toBe(codex.innerHTML);
for (const id of AGENT_ICON_IDS.filter((value) => agentMarkKind(value) === "asset")) {
const { container } = render(<AgentIcon agentId={id} />);
expect(workbuddy.innerHTML, `workbuddy must not reuse the ${id} mark`).not.toBe(container.innerHTML);
}
});

it("offers a tagline for hover, distinct from the name", () => {
for (const id of ALL) {
const tagline = agentTagline(id);
Expand Down
10 changes: 10 additions & 0 deletions frontend/src/components/icons/agents.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,16 @@ const MARKS: Record<string, Mark> = {
source: assetRightsManifest.assets["kilo-cli"].source,
rights: assetRightsManifest.assets["kilo-cli"],
},
// ChatGPT Desktop is OpenAI's own product and shares Codex's configuration, so
// it reuses the same OpenAI mark rather than registering a second copy of one
// asset. Keyed by desktop Agent id because the desktop card looks itself up by
// id; passing a literal here is what put this mark on WorkBuddy.
"chatgpt-desktop": {
kind: "asset",
markup: codexMark,
source: assetRightsManifest.assets.codex.source,
rights: assetRightsManifest.assets.codex,
},
// Aider has no mark in lobe-icons, so it keeps a generic symbol rather than a
// vendor favicon copied in without an auditable redistribution basis.
aider: { kind: "generic", Icon: GitBranch, source: GENERIC_SOURCE },
Expand Down
14 changes: 13 additions & 1 deletion frontend/src/styles/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -1637,11 +1637,23 @@
.icon-button:hover { color: var(--text-primary); background: var(--surface-pressed); }
.icon-button.is-danger:hover { color: var(--red); background: var(--red-soft); }

/* auto-fill rather than a media query: the pane width already varies with the
sidebar, which itself collapses at two breakpoints, so enumerating column
counts would mean tracking both. 340px is the floor at which a card still fits
its 92px endpoint label plus a readable value, so below roughly 790px of pane
this becomes one column on its own.

align-items: start keeps a card at its natural height. Without it grid
stretches every card in a row to match the tallest, and card heights genuinely
differ here: the Anthropic endpoint row only renders when that endpoint is
set. */
.provider-list,
.profile-list {
width: 100%;
display: grid;
gap: 8px;
grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
align-items: start;
gap: 10px;
}

.provider-card,
Expand Down
Loading