Skip to content

feat(agent-adapter,ui): surface codex skill brand icons and display names in the command catalog - #431

Open
Zerlight wants to merge 15 commits into
ruocheng/code-412from
ruocheng/code-525
Open

feat(agent-adapter,ui): surface codex skill brand icons and display names in the command catalog#431
Zerlight wants to merge 15 commits into
ruocheng/code-412from
ruocheng/code-525

Conversation

@Zerlight

@Zerlight Zerlight commented Aug 8, 2026

Copy link
Copy Markdown
Member

Summary

Stacked on the CODE-412 PR (base ruocheng/code-412); only the 11 commits above it are new here.

Codex plugin skills ship brand identity on skills/list (interface.displayName / iconSmall / brandColor — verified live on codex 0.144.6); carry it through instead of dropping it (Closes CODE-525):

  • Additive AgentCommand fields (displayName, iconDataUri, brandColor); icons travel as data URIs, size-capped per icon and in aggregate per catalog. WIRE_PROTOCOL_VERSION 73 → 74, floor untouched — old clients unaffected.
  • Rendered in the command menu, composer/transcript command chips (tinted with the brand color), tool rows, and dedicated branded activity groups (thinking glued into runs so brand groups don't split). Skills without brand data keep today's appearance.
  • opencode MCP tool titles normalize to the shared mcp__<server>__<tool> slug, live and on cold history reads. Cold reads resolve server names from the session directory's config plus a new engine-supplied mcpServerNames hint, so calls to engine-injected servers (managed connectors, the daemon's simulator endpoint) retitle too. The hint is adapter-local context (AgentHistoryReadContext), deliberately off the wire.

Verification

  • pnpm check:ci (0 errors) and full pnpm exec vitest run (2764 passed) at this tip.
  • Codex skills/list interface fields probed against a live codex app-server (0.144.6); findings recorded in CODE-525.
  • opencode's flat sanitize(server)_sanitize(tool) join verified against anomalyco/opencode 1.18.15 source; cold reads use a pure config.get — never mcp.status, whose lazy init spawns/dials every configured server (noted in readHistory).
  • Unit coverage: icon payload caps, brand lookup (incl. prototype-key hardening), opencode retitle mapping and adapter replay, engine pass-through of the injected-names hint (history-service, start-options-mcp).

Checklist

  • pnpm check:ci and pnpm test both pass (plus cargo fmt / clippy / test for Rust changes)
  • I ran the affected surface and observed the change working
  • If a wire message changed: WIRE_PROTOCOL_VERSION is bumped (73 → 74)
  • New code and assets are my own work, or their origin and license compatibility are noted above
  • Docs and comments are updated where behavior changed

Copilot AI lite review requested due to automatic review settings August 8, 2026 02:35
@linear-code

linear-code Bot commented Aug 8, 2026

Copy link
Copy Markdown

CODE-525

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR carries provider-supplied command branding (display names, icons, brand colors) end-to-end—starting from codex skills/list through the schema and wire protocol—so the UI can render branded command rows/chips. It also normalizes opencode MCP tool titles to the shared mcp__<server>__<tool> slug and adds integration brand glyphs + per-brand activity grouping in the chat timeline.

Changes:

  • Extend AgentCommand (schema + wire v74) with displayName, iconDataUri, and brandColor, and propagate these through codex command ingest (with per-icon + total payload caps).
  • Add UI command catalog context + branded glyph/tint rendering across the composer, transcript command echoes, tool rows, and activity-run headers/groups.
  • Normalize opencode MCP tool names to the shared MCP slug for both live streaming and cold history reads, including an engine-supplied injected-server-name hint.

Reviewed changes

Copilot reviewed 48 out of 49 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
pnpm-workspace.yaml Add @iconify-json/simple-icons to the shared catalog for brand glyph imports.
pnpm-lock.yaml Lockfile updates for @iconify-json/simple-icons.
package.json Add @iconify-json/simple-icons workspace dependency.
eslint.config.cjs Allow unplugin-icons virtual module usage in the new integration brand component.
apps/webview/package.json Add @iconify-json/simple-icons dependency for webview build.
apps/desktop/package.json Add @iconify-json/simple-icons dependency for desktop build.
packages/presentation/ui/src/tool-utils.ts Update MCP slug parsing doc comment to reflect shared cross-adapter usage.
packages/presentation/ui/src/shell/conversation-surface.tsx Provide command catalog context to the conversation surface for transcript echo chips.
packages/presentation/ui/src/shell/composer-editor/directive-state.ts Add commandFor lookup for branded composer directive chips.
packages/presentation/ui/src/shell/composer-editor/chips.tsx Render branded command chip glyphs/tints in the composer.
packages/presentation/ui/src/shell/composer-command.tsx Show command display names in the command catalog and render branded glyphs for command entries.
packages/presentation/ui/src/chat/user-message.tsx Detect /command args echoes and render a branded chip when the command is in the catalog.
packages/presentation/ui/src/chat/tool.tsx Prefer caller/plugin-provided glyphs even when a tool call fails.
packages/presentation/ui/src/chat/tool-call-item.tsx Add integration brand glyph fallback for MCP tool headers.
packages/presentation/ui/src/chat/integration-brand.tsx New: map MCP server-name tokens to known integration glyphs + labels.
packages/presentation/ui/src/chat/command-catalog.ts New: build/consume an O(1) command lookup for transcript echo branding.
packages/presentation/ui/src/chat/command-brand.tsx New: branded command glyph rendering + runtime tint style + catalog provider.
packages/presentation/ui/src/chat/activity-summary.ts Add brand resolution for activity items/runs based on MCP server name.
packages/presentation/ui/src/chat/activity-run.tsx Render branded activity run headers/icons and add i18n keys for branded wording.
packages/presentation/ui/src/chat/activity-groups.ts Split activity runs by integration brand; glue thinking into surrounding runs.
packages/presentation/ui/src/chat/tests/user-message.test.tsx Test transcript echo branding behavior for known vs unknown leading slashes.
packages/presentation/ui/src/chat/tests/tool-call-metadata.test.tsx Test that known integration MCP calls wear a brand glyph in the header icon slot.
packages/presentation/ui/src/chat/tests/integration-brand.test.tsx New: unit tests for server-name token matching + glyph rendering.
packages/presentation/ui/src/chat/tests/activity-run.test.tsx Test branded run naming/icon behavior, including failure-state tint rules.
packages/presentation/ui/src/tests/composer-command.test.ts Test displayName query matching and hint formatting.
packages/presentation/ui/src/tests/activity-summary.test.ts Add tests for activityRunBrand resolution rules.
packages/presentation/ui/src/tests/activity-groups.test.ts Add tests for per-brand grouping and glue behavior.
packages/presentation/i18n/src/locales/zh-cn.ts Add branded integration activity strings.
packages/presentation/i18n/src/locales/en.ts Add branded integration activity strings.
packages/host/engine/src/session/start-options-resolver.ts Provide engine-side injected MCP server-name hint for cold history reads.
packages/host/engine/src/session/history-service.ts Thread injected server-name hint into adapter history reads (adapter-local context).
packages/host/engine/src/engine.ts Wire the injected-server-name hint provider into HistoryService.
packages/host/engine/src/tests/start-options-mcp.test.ts Test injected MCP server-name enumeration.
packages/host/engine/src/tests/history-service.test.ts Test that injected MCP server names are passed through on cold reads.
packages/host/engine/src/tests/fixtures/history-adapter.ts Update test adapter to accept AgentHistoryReadContext.
packages/host/agent-adapter/src/native/opencode/history.ts Normalize opencode MCP tool names to mcp__<server>__<tool> when resolvable.
packages/host/agent-adapter/src/native/opencode/adapter.ts Use injected/configured server-name sets to retitle opencode MCP tools live + in history.
packages/host/agent-adapter/src/native/codex/adapter.ts Ingest codex skill brand identity and embed/cap icon payloads as data URIs.
packages/host/agent-adapter/src/base.ts Update base adapter history read signature to AgentHistoryReadContext.
packages/host/agent-adapter/src/adapter.ts Introduce AgentHistoryReadContext (wire options + engine-local hints).
packages/host/agent-adapter/src/tests/opencode.test.ts Test live normalization using injected + config-declared MCP servers.
packages/host/agent-adapter/src/tests/opencode-history.test.ts Test opencode MCP prefix splitting + retitling behavior.
packages/host/agent-adapter/src/tests/opencode-history-adapter.test.ts Test cold-read retitling using engine hint + config servers.
packages/host/agent-adapter/src/tests/codex-commands.test.ts Test codex brand identity projection + icon embedding + payload budget cap behavior.
packages/foundation/schema/src/wire/message.ts Bump WIRE_PROTOCOL_VERSION 73 → 74 for additive wire schema changes.
packages/foundation/schema/src/model/agent/input.ts Extend AgentCommand schema with displayName, iconDataUri, brandColor.
packages/client/workbench/tests/integration/dev-mock-transport.test.ts Update mock transport expectations to include branded commands.
packages/client/workbench/src/mock/data/showcase.ts Add branded MCP call examples for the UI showcase.
packages/client/workbench/src/mock/data/commands.ts Add branded mock command fixtures with data-URI icon + brandColor.
Files not reviewed (1)
  • pnpm-lock.yaml: Generated file
Suppressed comments (1)

packages/host/agent-adapter/src/native/opencode/adapter.ts:731

  • readHistory also treats MCP server-name resolution as best-effort, but client.config.get(...) is awaited without error handling. A rejected config read would fail the entire history read (and thus transcript loading) even though only MCP retitling depends on it. Swallow config-get failures and proceed with just the injected/engine-provided mcpServerNames.
      // Best-effort MCP server names so replayed MCP tool titles converge with live ones — a
      // pure config read, never `mcp.status` (its lazy init would spawn/dial every configured
      // server from the shared history instance). Config-declared servers resolve, including
      // disabled ones; engine-injected servers exist only on a session's own live instance, so
      // the caller's `mcpServerNames` hint is the only way their calls resolve here.
      const names = new Set(opts.mcpServerNames);
      const config = await client.config.get({ directory: got.data.directory });
      for (const name of Object.keys(config.data?.mcp ?? {})) names.add(name);

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread packages/host/agent-adapter/src/native/opencode/adapter.ts

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No critical issues — minor suggestions inline.

Reviewed changes

  • Schema is correctly additive. displayName / iconDataUri / brandColor are all optional, and WIRE_PROTOCOL_VERSION 73→74 with MIN_COMPATIBLE_WIRE_VERSION untouched is the right call — nothing is removed, renamed, or re-meant, so no lockstep upgrade is forced.
  • Codex icon embedding looks sound. Extension→MIME allowlist, 32 KiB per-icon cap, stat before readFile, and capSkillIconPayload dropping (not truncating) over-budget icons. The refresh-generation staleness re-check is correctly placed after the awaited icon reads, so a concurrent refresh can't emit a stale catalog.
  • opencode retitling traced end-to-end. Longest-sanitized-prefix matching for sanitize(server)_sanitize(tool) is the right disambiguator given opencode's flat naming, and routing both live parts and permission asks through opencodeMcpTitle keeps the header consistent. AgentHistoryReadContext staying adapter-local rather than on the wire is the right boundary.
  • Engine reorder is safe. SessionStartOptionsResolver moves above HistoryService, and both customMcp and plugins are already initialized at that point — no TDZ.
  • Checked and cleared: the desktop CSP already allows img-src … data: so embedded icons render; --foreground resolves to a real color so the color-mix chip style is valid; the un-guarded client.config.get() calls are fine because the generated opencode client resolves { error } rather than throwing (as the existing comment at opencode/adapter.ts:124 documents) and both sites null-check .data?.mcp.
  • ACTIVITY_RUN_GLUE_KEY mechanics verified against the new tests — glue items attach to an open run without opening one themselves, and flushRun resets runKey so a trailing glue item can't leak into the next group.

ℹ️ .claude/rules/frontend.md now contradicts the dependency set

The renderer rule still reads:

Brand / agent icons: @proj-airi/lobe-icons via unplugin-icons — chosen over simple-icons, which at adoption time (2026-07) lacked usable claudecode/opencode glyphs after trademark removals.

This PR adds @iconify-json/simple-icons as a second brand-icon collection (catalog, root/desktop/webview manifests, eslint override, /// <reference> — all wired correctly). The wiring is fine; the guidance isn't. As written, the next contributor reaching for an integration glyph is told not to use the collection this PR just standardized on. Worth a line in that rule saying which collection owns which case — lobe-icons for agent/model brands, simple-icons for third-party MCP integrations, or whatever the intended split is.

Nitpicks (not blocking)

  • toolCallFromPart now derives kind from the retitled slug rather than the raw provider name. tool-utils.ts's updated JSDoc acknowledges that a server key containing __ mis-splits — previously that was cosmetic (a wrong header label), but now the same mis-split also feeds toolKindFromName, so it can flip a call's kind and kind icon. Still low-impact, just a slightly wider blast radius than the JSDoc implies.
  • HistoryService.injectedMcpServerNames resolves today's injected server set and applies it to historical sessions. A server that was enabled when the session ran but has since been disabled won't retitle on a cold read. Probably acceptable — just noting the reads aren't point-in-time.
  • Skill icon reads fan out through an unbounded Promise.all. Fine for realistic catalog sizes; only a concern if a workspace ever carries hundreds of skills.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using Claude Opus𝕏

Comment thread packages/presentation/ui/src/chat/tool.tsx
Comment thread packages/presentation/ui/src/chat/user-message.tsx
Copilot AI review requested due to automatic review settings August 9, 2026 11:37
@Zerlight
Zerlight force-pushed the ruocheng/code-525 branch from a730f54 to 8cb9c94 Compare August 9, 2026 11:37

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 50 out of 51 changed files in this pull request and generated 1 comment.

Files not reviewed (1)
  • pnpm-lock.yaml: Generated file
Suppressed comments (1)

packages/presentation/ui/src/chat/integration-brand.tsx:88

  • IntegrationIcon renders a decorative SVG glyph but does not mark it as decorative. Other icon components in this codebase set aria-hidden on glyph SVGs (e.g. AgentIcon), and leaving it out can cause screen readers to announce the icon redundantly in tool headers and activity runs.

Comment thread packages/host/agent-adapter/src/native/codex/adapter.ts
@Zerlight

Zerlight commented Aug 9, 2026

Copy link
Copy Markdown
Member Author

frontend.md now records the collection split (8cb9c94): lobe-icons stays the agent/model brand collection; third-party integration glyphs use static ~icons/simple-icons/* imports with chat/integration-brand.tsx as the one map.

The three nitpicks are acknowledged and deliberately left as-is: the __ mis-split stays a documented cosmetic limit on the opencode side (the codex side now guards it in #430), the injected-server set not being point-in-time is acceptable for retitling, and the icon reads are bounded by realistic catalog sizes.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes

Since the prior review at a730f54f the branch was rebased onto a moved ruocheng/code-412 (which itself gained 9a3fcb97 and c4fa4a17) and picked up two commits of its own. The automatic range-diff failed to compute, so I isolated this PR's delta from the base-branch churn commit-by-commit before reviewing — the base commits' edits to codex/tool-view.ts, codex/history-tools.ts, tool-search.tsx, and tool-utils.ts's search case belong to the parent PR and are deliberately out of scope here.

  • Guarded opencode's best-effort config reads against transport rejections (65d548bd). Both collectMcpServerNames and readHistory now wrap client.config.get in try/catch. I checked the load-bearing claim against the installed SDK rather than taking the comment's word for it: @opencode-ai/sdk@1.18.3's dist/gen/client/client.gen.js does let response = await _fetch(request) with no wrapper, and wrapClientError only converts when throwOnError is set — so a dead-server fetch genuinely rejects and these catches are load-bearing, not dead code. This also corrects my own prior "checked and cleared" call, which read the {error} contract as covering transport failures too. The live path matters most: collectMcpServerNames sits in onStart's Promise.all alongside three siblings that already swallow their own failures, so an unguarded rejection there would have failed session start outright.

  • Gated the transcript command-echo chip to single-line messages (8cb9c949). commandEcho now returns undefined for any text containing \n, so multi-line arguments fall back to ContentBlockView block rendering. This also makes the collapsible line-clamp interaction moot by construction — an echo can no longer exceed one line. The existing item.blocks.length === 1 guard at user-message.tsx:112 already kept mixed text+image messages off the chip path.

  • Corrected the failed-call glyph comments to match shipped behavior. Rather than narrowing ToolIcon's icon ?? <CircleXIcon> back to the brand path, the comments at tool.tsx:157 and tool-call-item.tsx:107 now state that any caller-supplied glyph survives a failure — a deliberate widening, with the stale "state glyphs still override" line fixed.

  • Reconciled .claude/rules/frontend.md with the two-collection icon split, naming lobe-icons as the agent/model collection and ~icons/simple-icons/* (via chat/integration-brand.tsx) as the third-party MCP integration collection. That closes the guidance/dependency contradiction from the prior review.

Both new tests are real regression guards, not theatre: drop the try/catch and the opencode test's readHistory rejects; drop the newline gate and the user-message test's img//documents assertions both fire. Ran locally — 21 tests across the two files pass.

Pullfrog  | View workflow run | Using Claude Opus𝕏

Copilot AI review requested due to automatic review settings August 9, 2026 11:57
@Zerlight
Zerlight force-pushed the ruocheng/code-525 branch from 8cb9c94 to 7473d53 Compare August 9, 2026 11:57

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 50 out of 51 changed files in this pull request and generated no new comments.

Files not reviewed (1)
  • pnpm-lock.yaml: Generated file
Suppressed comments (1)

packages/foundation/schema/src/model/agent/input.ts:103

  • iconDataUri is validated only by prefix (data:image/) with no length cap. Given this field is carried on the wire, an unexpectedly large data URI (from a buggy/malicious adapter or future provider) can bloat frames and stress transport reassembly/memory. Other wire payloads (e.g. attachments) enforce .max(...) limits; consider doing the same here for defense-in-depth.
  /** Small brand icon embedded as a data URI — size-capped at adapter ingest (per icon and in
   * aggregate per catalog), so consumers can render it directly (no asset endpoint exists for
   * command icons). */
  iconDataUri: z.string().startsWith('data:image/').optional(),
  /** Brand accent for icon fallbacks (menu initial chips). */

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes

Since the prior review at 8cb9c949 this PR added exactly one commit. The base branch also moved (c4fa4a17794b9355), and the automatic range-diff again failed to compute, so I separated the two before reviewing: the only tree difference besides the commit below is codex/tool-view.ts un-exporting CODEX_PLUGIN_APPS_SERVER, which is parent-PR churn (the file is absent from this PR's 51-file diff) and has no consumer outside its own module.

  • Marked the integration brand glyph decorative (7473d532). IntegrationIcon now passes aria-hidden to the simple-icons SVG, matching the sibling glyph components in this package (agent-icon.tsx, service-icon.tsx, link-icon.tsx, file-identity-icon.tsx) and completing the brand-glyph family — CommandBrandGlyph already had aria-hidden on its initial chip and alt="" on its data-URI img.

No information is lost at either call site, which is the thing worth checking on an aria-hidden: tool-call-item.tsx:120 renders the glyph into the header icon slot beside the visible title, and activity-run.tsx:237 renders it beside a run label whose text already carries the brand name via INTEGRATION_LABELS (activity-run.tsx:53). Screen readers keep "Linear"; they just stop announcing the logo twice.

The 35 tests across integration-brand, tool-call-metadata, and activity-run pass at this tip. Worth noting for anyone reproducing locally: these three files fail with Icon 'simple-icons/asana' not found until pnpm install runs against this branch's manifests — @iconify-json/simple-icons is new here, and a node_modules tree from before the branch makes the failure look like broken ~icons wiring rather than a missing dependency.

Pullfrog  | View workflow run | Using Claude Opus𝕏

Copilot AI review requested due to automatic review settings August 9, 2026 12:12
@Zerlight
Zerlight requested a review from lucas77778 August 9, 2026 12:15

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 50 out of 51 changed files in this pull request and generated no new comments.

Files not reviewed (1)
  • pnpm-lock.yaml: Generated file
Suppressed comments (1)

packages/host/agent-adapter/src/native/codex/adapter.ts:163

  • skillIconDataUri enforces the size cap via stat() before readFile(), but the file can change between the two (TOCTOU) and exceed SKILL_ICON_MAX_BYTES, resulting in a larger-than-intended payload being embedded and sent over the wire. Re-check data.byteLength after reading (and early-return if it exceeds the cap) so the cap is enforced on the actual bytes embedded.
export async function skillIconDataUri(iconPath: string): Promise<string | undefined> {
  const mime = SKILL_ICON_MIME[extname(iconPath).toLowerCase()];
  if (!mime) return undefined;
  try {
    const info = await stat(iconPath);
    if (!info.isFile() || info.size === 0 || info.size > SKILL_ICON_MAX_BYTES) return undefined;
    const data = await readFile(iconPath);
    if (mime === 'image/svg+xml' && SVG_ACTIVE_CONTENT_RE.test(data.toString('utf8'))) {
      return undefined;
    }
    return `data:${mime};base64,${data.toString('base64')}`;
  } catch {

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes

Since the prior review at 7473d532 this PR added exactly one commit. Unlike rounds 2 and 3, the range-diff computed cleanly and held nothing but that commit — no base-branch churn needed separating this time.

  • Screened SVG skill icons for active content at codex ingest (d6327339). skillIconDataUri now rejects an .svg whose bytes match SVG_ACTIVE_CONTENT_RE<script>, <foreignObject>, on*= handlers, javascript:, and non-fragment href/xlink:href — and the AgentCommand.iconDataUri docstring gained the render contract it depends on: <img> only, never inlined markup, "the screen is depth, not a sanitizer."

I checked the load-bearing premise rather than taking the docstring's word for it, since the whole design rests on it. SVG 2 §2.2.4/§2.3 normatively requires an SVG embedded via <img> to be processed in secure animated modescript execution: no, external references: no, interactivity: no — and MDN's SVG as an image confirms JavaScript is disabled and external subresources are not fetched. Chromium's own position matches: issue 40058212 was closed Won't Fix with "Only SVGs in image tags are prevented from running JS", and the <foreignObject>-escapes-<img> report (499559357) was closed not-reproducible. The desktop renderer adds nothing that widens this (webSecurity, sandbox, contextIsolation on; nodeIntegration off). So the framing is right, and the gaps a byte-level regex must have — a UTF-16-encoded SVG read through toString('utf8'), entity-encoded payloads, SMIL <animate> triggers — are not exploitable behind <img>. That matters because the docstring is what stops a future contributor from reaching for dangerouslySetInnerHTML; command-brand.tsx:18 is the only iconDataUri render site repo-wide today, and adapter.ts:1002 (iconSmall ?? iconLarge) is the only producer, so the screen sits on a single funnel.

The false-positive cost is measured rather than assumed: I ran the regex over 281 real .svg files ≤32 KiB under node_modules, and exactly one was rejected — a design-tool export carrying <image href="data:image/png;base64,…">. So a skill shipping a logo with an embedded raster silently loses its icon and falls back to the brandColor chip, which is the documented graceful degradation. Worth leaving alone: relaxing href= to permit data: would re-open nested data:image/svg+xml references, and the fallback already reads fine.

The new test is a real guard, not theatre. Four exact toBeUndefined() rejections plus a toBeDefined() positive control that pins the legitimate fragment-ref idiom (<use xlink:href="#g"> alongside fill="url(#g)") as still allowed — delete the screen and the four rejections fail; over-tighten it to any href at all and both the positive control and the existing catalog test's <svg xmlns="…"/> fixture fail. All 15 tests in codex-commands.test.ts pass at this tip, eslint reports 0 errors on the touched files (one pre-existing consistent-function-scoping warning on the older dataUri helper), and biome reports no format changes.

Pullfrog  | View workflow run | Using Claude Opus𝕏

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants