Skip to content

[stacked on #2194] Project a declared provider icon glyph onto ProviderInfo and draw it - #2195

Closed
SawyerHood wants to merge 1 commit into
provider-gaps-thread-event-typesfrom
provider-gaps-icon-glyph
Closed

[stacked on #2194] Project a declared provider icon glyph onto ProviderInfo and draw it#2195
SawyerHood wants to merge 1 commit into
provider-gaps-thread-event-typesfrom
provider-gaps-icon-glyph

Conversation

@SawyerHood

@SawyerHood SawyerHood commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator

Stacked on #2194 (event types by name). Layer 3 of 3 of the provider-gaps stack (#2193#2194#2195). Closes gap G-C from #2189.

What was wrong

PluginProviderDeclaration.icon accepts a named host glyph ("Zap") or a plugin-relative SVG path ("./icons/agent.svg"). The projection kept only the path form — ProviderInfo.logoUrl, served by the provider-logo route — and dropped the glyph on the floor, so a provider plugin without an SVG asset showed its display name's initial ("E" for Echo) in the picker. Every first-party plugin ships an asset, which is why nobody hit it: exactly the privilege asymmetry the canary exists to find.

What changed

packages/domain/src/provider-types.tsProviderInfo gains icon?: { glyph: string }: absent when the declaration named a path or nothing, so at most one of icon / logoUrl is set. Same vocabulary as an item presentation's icon (and the design doc's declared { glyph } | { asset }, with the asset half already travelling as logoUrl). Optional per the pattern of the other WS2a projections on this schema (family, strings, extensionKinds): absence means the provider declared none, never a default.

apps/server/src/services/providers/plugin-provider-registration.ts projects a non-path icon as { glyph }.

apps/app/src/lib/provider-icon.tsgetProviderIconInfo(providerId, source?) takes a ProviderIconSource ({ logoUrl, icon? } — pass the ProviderInfo itself) instead of a bare logoUrl. Resolution order: plugin slot → vendored brand mark → logoUrlthe declared glyph → generic ACP glyph → undefined (the picker's initial). The glyph renders through the shared icon set (Icon), so it inherits the surrounding text color like a vendored mark; a glyph name this host does not know resolves to nothing and the fallback chain continues. Callers: useThreadCreationOptions (the picker tabs and trigger, desktop and compact), ProvidersSettingsSection.

apps/mobileProviderPickerOption carries glyph; ProviderPicker draws it through isIconName for the row icon and the trigger, falling back to Zap as before.

Tests: plugin-provider-registration.test.ts projects glyph vs path (never both) and pins the first-party projections against the plugins' own declarations via loadFirstPartyProviderDeclarations — codex, claude-code, pi, acp-cursor keep their logo URLs; acp-opencode/omp/grok/hermes-agent stay icon-less (the app vendors their marks by id); none gets a glyph. provider-icon.test.tsx covers glyph rendering (svg[data-icon="Zap"], no <img>), unknown glyph → undefined, logo winning over glyph. execution-options.test.ts (mobile) covers the option projection.

docs/api_to_audit.md: the app.experimental_useProviders "Icons" item notes the new field and the fold-into-one-field question for stabilization.

Wire: this is server→app (ProviderInfo over /api/v1/system/providers and the SDK's provider directory), additive and optional. Nothing between the server and the host daemon changes — no session payload, host RPC or WebSocket message carries ProviderInfo — so HOST_DAEMON_PROTOCOL_VERSION does not bump.

How you verified

  • Live: dev app on this branch (scripts/bb-dev-app current, the only instance running; stopped afterwards). The echo plugin installed from its path; GET /api/v1/system/providersecho-agent: logoUrl null, icon {glyph: "Zap"}; the four first-party rows unchanged (logoUrl set, no icon). In the picker the Echo tab renders svg[data-icon="Zap"] (was the initial "E"); selecting it puts the bolt in the trigger; Settings → Providers shows the bolt on the Echo row. Screenshots in thread storage thr_iiqzqxqgut: glyph-picker-open.png, glyph-picker-echo.png, glyph-picker-echo-crop.png, glyph-settings-providers.png.
  • pnpm exec turbo run typecheck --filter=@bb/domain --filter=@bb/server --filter=@bb/app --filter=@bb/mobile: pass.
  • @bb/server plugin-provider-registration tests: 14 pass (the first-party pin fails on the previous commit only if a glyph appears — it is a guard, not a change).
  • @bb/app provider-icon, ModelReasoningPicker, ProvidersSettings, useThreadCreationOptions: 56 pass. @bb/mobile execution-options: 9 pass. @bb/domain: 150 pass.
  • Corpus A4 (BB_PROVIDER_CORPUS_DIR, compare mode): 307/307 threads, 93,262 rows, zero diffs, empty allowlist, exit 0. Note: the shared ~/.bb/provider-corpus/snapshots/rows baseline was rewritten at 15:13 today by the WS3 layer-1 thread (Project grammar v3 items to presentation-driven rows (WS3 layer 1) #2192; it left rows.bak-pre-ws3-1513), so the comparison ran against that pre-WS3, main-minted baseline through a shadow corpus dir (/tmp/corpus-gc, symlinks to the corpus, rows → the backup, empty allowlist). Against WS3's rewritten baseline the same run shows WS3's 37 changed threads, none of them from this PR.
  • G1 ratchet (scripts/check-provider-literal-ratchet.mjs): not on this base (it lives on Add the provider-literal ratchet guardrail (G1) #2120); skipped.

AGENT GENERATED: by Claude Opus 5

PluginProviderDeclaration.icon accepts a named host glyph ("Zap") or a
plugin-relative SVG path. The projection kept only the path form
(logoUrl, served by the logo route) and dropped the glyph, so a provider
plugin without an SVG asset showed its display name's initial in the
picker — a privilege asymmetry the echo canary surfaced, since every
first-party plugin ships an asset.

ProviderInfo gains an optional `icon: { glyph }` (absent when the
declaration named a path or nothing; at most one of icon/logoUrl is set),
the same vocabulary an item presentation's icon uses. The registration
projects it. The web app resolves it in getProviderIconInfo after logoUrl
and before the generic ACP glyph, drawn through the shared icon set so it
inherits the text color; an unknown glyph name resolves to nothing and the
initial fallback stands. The picker, the providers settings list and the
mobile provider picker carry it. The first-party projections are pinned
against the plugins' own declarations (four logos, four icon-less ACP
agents, no glyph).

Server→app wire only (ProviderInfo); nothing between the server and the
host daemon changes, so HOST_DAEMON_PROTOCOL_VERSION stays.

Co-Authored-By: Claude <noreply@anthropic.com>
@SawyerHood SawyerHood changed the title Project a declared provider icon glyph onto ProviderInfo and draw it [stacked on #2194] Project a declared provider icon glyph onto ProviderInfo and draw it Aug 21, 2026
@SawyerHood SawyerHood closed this Aug 21, 2026
@SawyerHood
SawyerHood deleted the provider-gaps-icon-glyph branch August 21, 2026 21:40
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.

1 participant