Guarantee a utility surface for runtime-installed plugins - #165
Conversation
Nothing in docs/plugin-sdk/ said where a plugin's styling comes from, so nothing warned that the host's Tailwind sheet is built by scanning the host's own src/** — a class written only in plugin code has no rule behind it. Named utilities survive that because the host uses the same kit; arbitrary values do not, since each is a separate rule emitted only if a host file spells it identically. Container Manager's log dialog lost h-[68vh] and max-h-[calc(100vh-13rem)] exactly this way: the log box fell back to height:auto and grew off both ends of the screen. Document the rule and the inline-style workaround where bundle authoring is already explained. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Nz2ELEkkMQzn6N429NkFy
A plugin bundle ships no CSS: it is built into one ESM file and styled entirely by this app's compiled Tailwind sheet. That sheet is generated by scanning src/**, and plugin source is never there — it is fetched at runtime from a URL. So a class written only in a plugin lands in the DOM with no rule behind it, silently, and "does the app happen to use this class somewhere" became an unwritten API. It had already broken all four plugins. Auditing each one against the CSS this app actually emits found bottom-3, pl-1.5, -ml-1.5, py-5, h-64, m-4, mx-5, min-h-16, min-h-20, -mt-2.5, align-top and translate-x-full all missing — while max-h-64, right beside h-64, was present. Container Manager's "Jump to latest" button had no offset and covered the first log line; its stderr rule vanished; Kafka's message preview collapsed and its resize handle overlapped the column; Redis's INFO panel lost its second column; Rabbit's scroll panes lost their padding. plugin-utilities.css uses @source inline(...) to emit the common scales — spacing, sizing, position, grid, vertical-align — whether or not this app's own code uses them, so a plugin has stable ground to stand on. It stays at general scales: colour, shadow and radius come from the design-system vocabulary, which is already used throughout src/**. Arbitrary values (max-w-[12rem], grid-cols-[minmax(...)]) can never be enumerated, so plugins must write those as inline styles; 05-external-install.md now says so, and the plugin repo checks it with scripts/check-host-classes.mjs. Costs 62 kB raw / 7 kB gzipped in a locally bundled desktop app. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Nz2ELEkkMQzn6N429NkFy
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #165 +/- ##
==========================================
+ Coverage 42.66% 42.67% +0.01%
==========================================
Files 300 300
Lines 19917 19917
Branches 4933 4933
==========================================
+ Hits 8498 8500 +2
Misses 10402 10402
+ Partials 1017 1015 -2
Flags with carried forward coverage won't be shown. Click here to find out more. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
ℹ️ No critical issues — one documentation gap worth a look.
Reviewed changes
src/styles/plugin-utilities.css(new) —@source inline(...)patterns force Tailwind to emit general spacing/size/position/translate/grid/align scales for plugins fetched at runtime.src/styles/globals.css— imports the safelist immediately aftertailwindcss.docs/plugin-sdk/05-external-install.md— new section stating a plugin bundle ships no CSS and must use named utilities or inline styles for arbitrary values.
Verified this run: npm run build clean, every previously-missing class from the PR body is present in the built stylesheet except border-bad/60, and all four merged companion plugin branches (container-manager, kafka-explorer, redis-client, rabbit-client) pass check-host-classes.mjs against this build.
Important
Pullfrog covered this run's model usage. DeepSeek Flash is fast and cheap — expect lighter work than a frontier model. This model allowance is temporary and separate from your Pullfrog plan. Connect a model-provider subscription or API key →
DeepSeek Flash (model usage covered by Pullfrog) | 𝕏
|
The one red check here, The job never analysed anything. It failed while starting GitHub's own code-scanning agent session: That is a service-side configuration error in GitHub's code-scanning agent — its own API rejecting the model it was configured with — raised before any scan ran. This PR's diff is one CSS file and one docs file; nothing in it can cause or fix a 400 from I could not re-run it to confirm: Everything else on this commit is green, including the checks that actually cover the change:
Generated by Claude Code |
…tes it src/styles/externalPluginClassnamesSafelist.ts already existed and already solved this problem — a snapshot of plugin class names extracted once when the four tools left this repo (1b59500, 5ad6ba2), kept purely as scannable text. I added a second mechanism without saying how the two relate, which would leave a reviewer guessing. The snapshot is not broken; it is stale. Its own comment predicted this: "no automated re-sync". Plugin code moved on after that extraction, and nothing re-syncs it — which is exactly why bottom-3, py-5, align-top, translate-x-full and min-h-16 had no rule, while max-w-[9rem] and grid-cols-[3rem_minmax(0,1fr)_6.5rem_5.5rem], both in the snapshot, did. @source inline() addresses precisely that weakness for the general scales: it enumerates by scale rather than by usage, so it needs no plugin checkout and cannot go stale. It does not replace the snapshot — arbitrary values can only ever come from there, and so remain able to go stale, which is why plugins must write those as inline styles. Both files now say this, as does 05-external-install.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Nz2ELEkkMQzn6N429NkFy
|
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
The commit since the prior pullfrog review is documentation-only and directly addresses the one open concern from that review (the undocumented split between the new scale safelist and the pre-existing snapshot safelist).
docs/plugin-sdk/05-external-install.md— added a paragraph explaining thatsrc/styles/externalPluginClassnamesSafelist.tsis the one-off snapshot extracted when the four tools moved out, that it's stale by design ("no automated re-sync"), and that it remains the only thing covering plugin-only arbitrary values. A follow-on paragraph corrects the earlier over-broad "named utilities are safe" claim by listing the named classes that actually lacked rules (bottom-3,py-5,align-top,translate-x-full,min-h-16). Verified the snapshot really does containmax-w-[9rem],grid-cols-[3rem_minmax(0,1fr)_6.5rem_5.5rem], and the plugin-only colour classes it is cited for.src/styles/plugin-utilities.css— header comment now cross-references the snapshot safelist, states the two are complementary rather than substitutes, and points at the plugin-side guard test (ui/hostCssClasses.test.ts).src/styles/globals.css— one-line comment above the@import.
The prior review thread on the colour/arbitrary-value documentation gap has been replied to and resolved.
Important
Pullfrog covered this run's model usage. DeepSeek Flash is fast and cheap — expect lighter work than a frontier model. This model allowance is temporary and separate from your Pullfrog plan. Connect a model-provider subscription or API key →
DeepSeek Flash (model usage covered by Pullfrog) | 𝕏




The problem
A plugin bundle ships no CSS. It is built into one ESM file and styled entirely by this app's compiled Tailwind sheet — which Tailwind generates by scanning
src/**. Plugin source is never there: it is fetched at runtime from a URL, long after this app was built.So a class written only in a plugin lands in the DOM with no rule behind it, silently.
The mechanism that already existed, and why it stopped working
src/styles/externalPluginClassnamesSafelist.tsis a snapshot of plugin class names, extracted once when the four tools left this repo (1b59500, 5ad6ba2) and kept purely as scannable text so Tailwind emits them.It is not broken. It is stale, exactly as its own comment predicted:
Plugin code moved on after that extraction. Nothing re-synced it. Every class that broke is one plugin code added afterwards:
max-h-64·max-w-[45%]·min-h-[16px]·grid-cols-[1fr_auto]·max-h-[65vh]·w-[110px]bottom-3·pl-1.5·-ml-1.5·py-5·h-64·m-4·mx-5·min-h-16·min-h-20·-mt-2.5·align-top·translate-x-full·border-bad/60max-h-64having a rule whileh-64, beside it in the same file, had none looked arbitrary when I first reported it. It isn't. It's a date.What users saw:
This change
src/styles/plugin-utilities.cssuses@source inline(...)to emit the common scales — spacing, sizing, position, grid, vertical-align — whether or not this app's own code uses them.How the two relate
It enumerates by scale, not by usage. That is the point: it needs no plugin checkout, so it sidesteps the coupling the original author rejected, and it cannot go stale — a plugin adding
py-7tomorrow is already covered.It does not replace the snapshot. Arbitrary values (
max-w-[9rem],grid-cols-[3rem_minmax(...)]) can only ever come from there, so those remain able to go stale. That is why plugins must write new ones as inline styles — now enforced byui/hostCssClasses.test.tsin each plugin (miniapp #24–#27), and documented indocs/plugin-sdk/05-external-install.md.It deliberately stays at general scales. Colour, shadow and radius are not listed: those come from the design-system vocabulary, already used throughout
src/**.Cost
+62 kB raw / +7 kB gzipped, in a desktop app that bundles its assets locally — no network fetch.
What this does not fix
The safelist only exists in host builds from here on. A plugin installs into whatever host the user already has, so plugin code still has to render correctly on older hosts. The companion PRs in
developer-desktop-miniappfix each plugin against the currently released host and addscripts/check-host-classes.mjs, which resolves every class a plugin writes against a real host stylesheet.Verification
npm run build— cleannpx vitest run— 1399 tests / 121 files passNot verified here: running the app to look at the four tools by eye. Resolving classes against real CSS is conclusive about whether a rule exists, but it is not a substitute for opening the app.
CI
The one red check,
github-advanced-security, is a service-side failure in GitHub's code-scanning agent (CAPIError: 400 The requested model is not supported) raised before any scan ran — see the comment below. It cannot be fixed from this branch, and re-running it returns403 This workflow run cannot be retried.🤖 Generated with Claude Code
https://claude.ai/code/session_017Nz2ELEkkMQzn6N429NkFy