Let plugin pages declare their own icon - #512
Conversation
|
@codex review |
Plugin pages had no way to set their own mark, so every external page showed the generic browser icon in Search Buzz. Page now takes an optional icon that holds a data:image/ URL, and search rows render it. register keeps a matching icon. Any other value is dropped with one console warning that names the page, and the page still registers, because an icon is decoration and must not fail plugin activation. External plugins ship only plugin.js, so data URLs need no new asset path, network load, or host-asset access. PageIcon renders the image and falls back to the component icon when it fails to load. A failed source does not retry; a new source gets one attempt. Bundled pages keep their host icons. This edits the FOUNDATION page contract in features/pages/service.ts. Design: docs/plans/2026-10-01-plugin-page-icon/6-design.md (D1-D5) Structure: docs/plans/2026-10-01-plugin-page-icon/7-structure.md Part of slice 1: Search Buzz shows a plugin page's declared icon Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: Matthew Boston <mboston@squareup.com>
…es by full key A primary plugin page with a declared icon now shows it in the shell page navigation, through the same PageIcon fallback that Search Buzz uses. Nav keeps the bold weight for component icons only. pagePresentation matched Messages by local page id, so an external page with id "channels" took the Messages label and icon and would also have hidden its declared icon. It now matches the full key buzz.channels/channels, the same host policy bundledOrder follows. The bundled Channels page still shows Messages, and bundled order is unchanged. pagePresentation now declares its return shape so every branch carries the optional image field. Design: docs/plans/2026-10-01-plugin-page-icon/6-design.md (D4, D5) Structure: docs/plans/2026-10-01-plugin-page-icon/7-structure.md Part of slice 2: Primary nav shows the icon, and host-owned keys keep host marks Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: Matthew Boston <mboston@squareup.com>
Plugin authors read the page contract in docs/plugin-architecture.md and type against the generated @buzz/author package. The doc now names the optional icon field, its data:image/ form, where it renders, the generic-icon fallback, the registration warning, and that bundled pages keep their host icons. External plugins ship only plugin.js, so the doc points authors at an inline bundler import. The author consumer test now registers a page with a string icon and expects a type error for a numeric one, so the author.ts re-export must keep carrying icon as an optional string. Design: docs/plans/2026-10-01-plugin-page-icon/6-design.md (D9) Structure: docs/plans/2026-10-01-plugin-page-icon/7-structure.md Part of slice 3: PR Beacon shows the Owner Owl through the published author contract Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: Matthew Boston <mboston@squareup.com>
f6d320c to
985c049
Compare
wesbillman
left a comment
There was a problem hiding this comment.
Carl, an automated reviewer, commenting via Wes’s GitHub account.
No blocking findings; one P3 interaction defect inline. Reviewed head 985c049a293f7f146f6c2a12b96bd9cebbfa16f7 against base/merge-base 539bb13578d7c8c5a4d42002112857d66e20e19b. Meets the 9/10 bar for minimalness, elegance, and correctness, with the minor interaction fix noted below: one optional contract field, one shared renderer, and the existing contribution lifecycle.
- Validation: traced registration → contribution → presentation → search/primary navigation, including invalid values, bundled precedence, and source replacement. Independent Chromium/WebKit component probes verified decoding, error fallback, decorative naming, and the reported pointer behavior. Existing CI is green for this head; broad suites were not duplicated locally.
- Remaining acceptance: the PR’s human Tauri desktop test is still unchecked. Import the companion plugin into a desktop build of this head and confirm its icon in Search Buzz in light and dark themes. Our browser probes exercised the real icon/navigation components, not the full desktop workflow. No approval or readiness attestation is being given.
- Optional public-PR hygiene: the Companion PRs section links to a private repository. Prefer a public-safe dependency description/reference, keeping private coordination links in the internal thread.
wesbillman
left a comment
There was a problem hiding this comment.
Awesome! I wanted to get to this but you beat me to it! Thanks!!
An <img> is draggable by default, so pressing a plugin page icon and moving the pointer a few pixels started a drag and swallowed the row's click in Chromium and WebKit. The SVG fallback never had this problem. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: Matthew Boston <mboston@squareup.com>
|
🤖 Re: the review's public-PR hygiene note. The Companion PRs section no longer links to the private repository. It now describes the companion change in plain text: a Block-internal plugin, PR Beacon, adopts |
Summary
Plugin pages can now declare their own icon.
ctx.pages.registeraccepts an optionalicon, adata:image/...URL, and Search Buzz and the primary page navigation show it beside the page title. Today every page outside the host's hardcodedbundledIconsmap inpresentation.tsgets the generic browser-window icon, so external plugins such as PR Beacon cannot be told apart in Search Buzz.A missing, invalid, or unloadable icon falls back to the generic icon. Bundled pages keep their host icons.
Design Decisions
manifest.jsonand a self-containedplugin.js(docs/plugin-architecture.md), the renderer CSP already allowsimg-src data:, and a data URL makes no network request. Named host icons (a plugin picks from the design-system icon gateway) were the runner-up: they keep design-system control but give plugins no brand mark. Remote URLs and manifest fields were rejected because they add a fetch or a Rust manifest-schema change.console.warnthat names the plugin and page id, and the page still registers and opens.bundledIcons, its declared icon is ignored.pagePresentationnow checkspage.key === "buzz.channels/channels"instead ofpage.id === "channels". Before, any external page with local idchannelstook the Messages label and icon.src/features/pages/service.tsgains theiconfield and its validation.src/plugins/author.tsalready re-exportsPage, so it is unchanged. This edit was requested by the human author.Changes
Screenshots
All shots come from buzz-app's browser test fixture with synthetic data, with a page registered using PR Beacon's real Owner Owl
iconvalue.Search Buzz, before and after
Search Buzz filtered to "PR". The fixture and the query are the same in both shots.
main)Other icon states
How to Verify
pnpm checkandpnpm lint: pass (4 info-level Biome notes, all on base).pnpm vitest run: 6344 tests pass. New coverage: icon validation (valid, invalid, case, absent; warning names the page and never echoes the value),PageIconimage, load-error fallback, retry on a new source, and no-image rendering, a declared icon on a Search Buzz row and a primary nav row, and bundled-key precedence including an external page with idchannels.src/plugins/author.test.mjs: the generated@buzz/authorpackage acceptsicon: stringand rejectsicon: 1.node --test tests/integration/*.test.mjs: 182/182 pass andcargo test -p buzzodz-pluginspasses, both run with Hermit active and aftercargo fetch --locked. Without Hermit,plugin-cliandplugin-manager-lockfail to spawncargo, andagent-build-configfails offline on uncached crates.pnpm test:browser:ci: 1084 passed, 5 failed, 1 skipped. The 5 failures (profiles.spec.mjs:524chromium and webkit,reactions-polish.spec.mjs:6chromium and webkit,channel-completion.spec.mjs:7webkit) fail the same way on base e5a7046 when run alone, so they are not caused by this change.https://icon logs one warning and shows the generic icon, and an undecodable PNG falls back to the generic icon.main539bb13 (985c049). feat(ui): Switch shared icons to Tabler #523 moved shared icons to Tabler, which drops theweightprop, soPageIconnow takesstrokeWidthand the primary nav passesstrokeWidth={2.5}, the same value feat(ui): Switch shared icons to Tabler #523 gave the other nav icons. On 985c049 with Hermit active:pnpm check,pnpm lint,pnpm build,vitest run,node --test tests/integration/*.test.mjs, andcargo test -p buzzodz-pluginspass. Browser tests were not rerun locally.<img>is no longer draggable. Pressing the icon and moving the pointer a few pixels started a native drag that swallowed the row's click in Chromium and WebKit.PageIcon.test.tsxasserts the attribute; thesrc/app/shellandsrc/features/pagestests, Biome, andpnpm typecheckpass.tests/browser/channel-tabs.spec.mjs:281(crowded tab strip), which fails the same way onmainsince e697aa7 (Use top tabs in the new-tab picker #505).Merge risk
Two-way door. The change adds an optional field and a render path; no data, storage, manifest, or Rust changes. Plugins that do not set
iconrender as before. Recovery is a revert of this PR; plugins that seticonthen show the generic icon again and keep working, because older hosts already ignore the field.Pre-merge
just desktop): import the PR Beacon plugin from its companion branch and confirm the owl shows in Search Buzz in light and dark themes, then addbuzz-review-completed.Review notes
withAcceptedIconvalidatesiconfrom one read, thencreateContributionscopies the page with a second read, so a page object with a getter could pass the check and store a different value. Plugins are trusted same-process code and can already render any image element in their own component, so this grants no new capability. Fix by returning a copy holding the checked value.exactOptionalPropertyTypes(done in the implementation). The validation rule now requires the comma, sodata:image/png;base64with no payload is dropped. Non-page search rows render throughPageIconwith no image, covered by a test. The D4rgenumeration and two D2 scoring criteria lacked recorded evidence; neither changes the outcome.References
docs/plugin-architecture.md→ "Starting contracts" documentsicon.src/app/shell/PanelLaunchers.tsx(LauncherIcon).Companion PRs
A Block-internal plugin, PR Beacon, adopts
iconin a companion PR. It registers its Owner Owl as a data URL, which is what the screenshots above show.