Skip to content

Guarantee a utility surface for runtime-installed plugins - #165

Merged
DianaSensei merged 3 commits into
mainfrom
claude/miniapp-container-logs-overflow-szzaje
Sep 22, 2026
Merged

DianaSensei merged 3 commits into
mainfrom
claude/miniapp-container-logs-overflow-szzaje

Conversation

@DianaSensei

@DianaSensei DianaSensei commented Sep 22, 2026 •

Copy link
Copy Markdown
Owner

Correction, after this PR was opened. My original description said nothing in the repo documented where plugin styling comes from. That was wrong: src/styles/externalPluginClassnamesSafelist.ts has carried a safelist all along. The diagnosis below is rewritten around that. The change itself is unchanged and still the right one — see "How the two relate".

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.ts is 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:

Regenerate by re-running the extraction one-off in the migration PR description if the plugin source changes meaningfully after this point (no automated re-sync — that would require checking out the plugin repo during this app's build, which is more coupling than a safelist is worth).

Plugin code moved on after that extraction. Nothing re-synced it. Every class that broke is one plugin code added afterwards:

In the snapshot → had a rule Added later → no rule
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/60

max-h-64 having a rule while h-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:

Plugin Broken
Container Manager Log dialog ran off the top and bottom of the screen, its own toolbar out of reach; "Jump to latest" covered the first log line; stderr lines lost their red rule
Kafka Explorer Message preview collapsed; column resize handle overlapped the column
Redis Client INFO panel lost its second column; selection bar flush to the pane edges
RabbitMQ Client Scroll panes lost their padding; every editor stuck at the 180px default

This change

src/styles/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.

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-7 tomorrow 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 by ui/hostCssClasses.test.ts in each plugin (miniapp #24–#27), and documented in docs/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-miniapp fix each plugin against the currently released host and add scripts/check-host-classes.mjs, which resolves every class a plugin writes against a real host stylesheet.

Verification

  • npm run build — clean
  • npx vitest run — 1399 tests / 121 files pass
  • Every previously-missing class confirmed present in the rebuilt CSS
  • All four plugin branches verified clean against builds of both the released host and this one

Not 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 returns 403 This workflow run cannot be retried.

🤖 Generated with Claude Code

https://claude.ai/code/session_017Nz2ELEkkMQzn6N429NkFy

DianaSensei and others added 2 commits September 22, 2026 07:24
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

codecov Bot commented Sep 22, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 42.67%. Comparing base (c44a211) to head (74c4b6c).

Additional details and impacted files

Impacted file tree graph

@@            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     
Flag Coverage Δ
frontend 37.94% <ø> (+0.01%) ⬆️
rust 65.43% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.
see 2 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@DianaSensei
DianaSensei enabled auto-merge (rebase) September 22, 2026 08:01

@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 — 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 after tailwindcss.
  • 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 →

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using DeepSeek Flash (model usage covered by Pullfrog) | 𝕏

Comment thread docs/plugin-sdk/05-external-install.md

Copy link
Copy Markdown
Owner Author

The one red check here, github-advanced-security, is not this PR's and cannot be fixed from it.

The job never analysed anything. It failed while starting GitHub's own code-scanning agent session:

Creating copilot-sdk session with model: claude-opus-5[ReasoningEffort=medium]
Error creating PR review request: SessionModelError: Execution failed:
  CAPIError: 400 The requested model is not supported.
##[error]Process completed with exit code 1

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 api.individual.githubcopilot.com.

I could not re-run it to confirm: rerun-failed-jobs returns 403 This workflow run cannot be retried. The error is a deterministic 400 rather than a transient one, so a re-run would not be expected to change it either way.

Everything else on this commit is green, including the checks that actually cover the change:

Check
Typecheck and unit tests ✅ 1399 tests
frontend · rust ✅
CodeQL — Analyze (javascript-typescript) ✅
SonarCloud ✅ Quality Gate passed, 0 new issues
Codecov project · patch ✅
npm audit · cargo audit · Secret scan · Dependency review ✅

mergeable_state is blocked purely because that one check is required. Unblocking it needs either a repo-admin override or GitHub's code-scanning agent model configuration to be corrected — neither is reachable from this branch.


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
@sonarqubecloud

Copy link
Copy Markdown

@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

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 that src/styles/externalPluginClassnamesSafelist.ts is 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 contain max-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 →

Pullfrog  | View workflow run | Using DeepSeek Flash (model usage covered by Pullfrog) | 𝕏

@DianaSensei
DianaSensei merged commit 3a0deea into main Sep 22, 2026
20 of 21 checks passed
@DianaSensei
DianaSensei deleted the claude/miniapp-container-logs-overflow-szzaje branch September 22, 2026 08:54
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