From 1ffd4af903846ff23b8dd068dffeb286fa2d637f Mon Sep 17 00:00:00 2001 From: yujiezhang-ops Date: Thu, 6 Aug 2026 15:14:23 +0800 Subject: [PATCH] fix: give each desktop Agent its own mark, and lay the Provider/Profile cards out as a grid DesktopAppSection rendered agentId="codex" as a literal for every desktop Agent, so WorkBuddy -- a Tencent product -- displayed OpenAI's mark. That is a trademark problem rather than a styling one, and it also bypassed AgentIcon's fallback, the path that exists precisely for an Agent with no mark of its own. Passing desktopApp.id fixes it. ChatGPT Desktop keeps the OpenAI mark through a new chatgpt-desktop entry, because it is OpenAI's own app sharing Codex's configuration; WorkBuddy now falls through to the generic Lucide symbol, which needs no new asset and no NOTICE change. No licensed WorkBuddy mark was found to prefer instead. .provider-list and .profile-list declared `display: grid` with no grid-template-columns, so every card was a full-width band and both pages read as tables with rounded corners. auto-fill with a 340px floor gives two to four columns as the pane grows and collapses to one below roughly 790px without a media query; align-items: start keeps cards at their natural height, which matters because the Anthropic endpoint row only renders when that endpoint is set. Verified at 600/860/1280/1900px: column count 1/2/2/4, no card overflow, no horizontal page scroll. Closes #66 Closes #67 Co-Authored-By: Claude Fable 5 --- .../src/components/DesktopAppSection.test.tsx | 23 +++++++++++++++ frontend/src/components/DesktopAppSection.tsx | 7 ++++- frontend/src/components/icons/agents.test.tsx | 29 ++++++++++++++++++- frontend/src/components/icons/agents.tsx | 10 +++++++ frontend/src/styles/app.css | 14 ++++++++- 5 files changed, 80 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/DesktopAppSection.test.tsx b/frontend/src/components/DesktopAppSection.test.tsx index 42de8dc0..cf2676bd 100644 --- a/frontend/src/components/DesktopAppSection.test.tsx +++ b/frontend/src/components/DesktopAppSection.test.tsx @@ -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( + + + , + ); + 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"'); + }); }); diff --git a/frontend/src/components/DesktopAppSection.tsx b/frontend/src/components/DesktopAppSection.tsx index 9b1771e2..b005a6d1 100644 --- a/frontend/src/components/DesktopAppSection.tsx +++ b/frontend/src/components/DesktopAppSection.tsx @@ -119,7 +119,12 @@ export function DesktopAppSection({ app: desktopApp, onChanged, onSetup, onConfi ) : null}
- + {/* 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. */} + {desktopApp.name} diff --git a/frontend/src/components/icons/agents.test.tsx b/frontend/src/components/icons/agents.test.tsx index 64df46a3..0630f1ab 100644 --- a/frontend/src/components/icons/agents.test.tsx +++ b/frontend/src/components/icons/agents.test.tsx @@ -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"); @@ -110,6 +112,7 @@ describe("AgentIcon", () => { // recorded in asset-rights.json does not touch geometry. const PUBLISHED_VIEWBOX: Record = { 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", @@ -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(); + const { container: codex } = render(); + 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(); + 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(); + 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); diff --git a/frontend/src/components/icons/agents.tsx b/frontend/src/components/icons/agents.tsx index 4efb1c3d..285bdbe7 100644 --- a/frontend/src/components/icons/agents.tsx +++ b/frontend/src/components/icons/agents.tsx @@ -77,6 +77,16 @@ const MARKS: Record = { 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 }, diff --git a/frontend/src/styles/app.css b/frontend/src/styles/app.css index c8fce5a3..24d16b52 100644 --- a/frontend/src/styles/app.css +++ b/frontend/src/styles/app.css @@ -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,