refactor(mcp): rebuild Project Status app view as Web Components - #82
Merged
Conversation
Replace the single-file Bun.build view with native custom elements (<project-status-app> delegating to <health-section>, <operations-brief-section>, <activity-section> over a shared BaseComponent) on a Vite + vite-plugin-singlefile toolchain managed by bun. Payload data is populated exclusively via textContent and attribute setters, preserving the injection-safety invariant; the strict default-src 'none' CSP is unchanged and the built bundle is deterministic. Stop committing the built bundle (footgun): dist/project-status.html is gitignored and produced by bun run build. status_app.go switches to a directory embed (//go:embed all:views/dist) with a committed dist/.gitkeep placeholder and a built-in 'view not built' fallback page (same strict CSP), so a clean go build/install compiles and runs without a JS toolchain. CI and release build the bundle before the Go build so shipped binaries embed the real view; the removed 'bundle freshness' diff guard is replaced by a Vitest component suite (typecheck + test) run in CI. Add Vitest component tests (happy-dom) covering rendering from the Go golden fixtures, per-project reset, and injection inertness; a dev-only preview harness with selectable fixtures; and convert the headless render check to verify/verify.ts (run via bun). Update the views README and the MCP Apps contributor skill for the new toolchain and embed model.
Make a missing or stale embedded view bundle a hard, immediate go test failure instead of a convention or CI-only diff. The Vite build injects a <meta name="cairnline-views-src-sha256"> over a deterministic, sorted source set (src/** plus index.html, package.json, bun.lock, vite.config.ts; scheme: outer sha256 of per-file relpath + hex(sha256(bytes))). A new guard test TestProjectStatusView_BundleBuiltAndFresh reads the embedded bundle and fails 'not built' when the meta is absent (fallback embedded) or 'STALE' when the meta hash differs from the hash recomputed from the working-tree source. The source set is embedded into the test binary (a _test.go embed, excluded from the shipped binary) so editing any src file busts the go test cache and the guard re-runs at plain go test; a runtime file read would be masked by a cached pass. go build/go install still compile and serve the 'view not built' placeholder. Add convenience entrypoints: a top-level Makefile (views/build/test) and a //go:generate on status_app.go that rebuild the bundle. Document the guard as the structural protection in the views README and the MCP Apps skill.
chicoxyzzy
force-pushed
the
feat/mcp-apps-views-web-components
branch
from
July 9, 2026 18:25
8ece7c6 to
caf6516
Compare
chicoxyzzy
marked this pull request as ready for review
July 9, 2026 18:38
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Stacked on
feat/mcp-apps-ui-extension(PR #80, which now also carries #81'sdocs/skills/mcp-apps/SKILL.md). Merge that first, or review this against that base.Before: the Project Status MCP app view was a single TypeScript file that imperatively built the entire DOM, bundled by a hand-rolled
Bun.buildscript into one committed ~356 KBdist/project-status.html. The complaint: too much dynamic JS building DOM inside what is effectively one HTML file — no real structure.After: the view is native Web Components (custom elements, no framework). A root
<project-status-app>owns the ext-apps handshake, per-project state, and dispatch, delegating to<health-section>,<operations-brief-section>, and<activity-section>over a sharedBaseComponent. Rendered output is identical to before (same three sections, badges, count grids, inert action pills, activity buckets).What changed
textContent/attributes — noinnerHTMLwith interpolated data — preserving the injection-safety property the prior review verified. A payload containing<script>or HTML entities renders inert.default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'. Built HTML has zeroeval(, zero external script/style/asset references, inline module script only. Styling via shadow DOM + adopted stylesheets.ui/: Vite +vite-plugin-singlefile(keeps the single-file embed, matches the official ext-apps templates), bun package manager with a committed frozen lockfile,tsc --noEmittypecheck, oxlint/oxfmt. Deterministic, byte-reproducible build.structuredContentpayloads, per-project state reset (no cross-project bleed), and escaping/inertness.bun run preview(Vite dev) loads the view outside an MCP host with selectable fixture payloads, for local iteration. Documented in the views README.verify/verify.mjs→verify/verify.ts(run via bun); config files are.ts. No.mjs.dist/ is no longer committed — with a structural build guard (needs sign-off)
A committed build artifact was flagged as a footgun. This mirrors Hecate's
ui/distconvention and adds structural protection so forgetting to build fails hard and immediately atgo testtime (not just a CI diff):internal/app/views/dist/project-status.htmlis gitignored and built in CI/release; a committed emptydist/.gitkeep+ directory embed//go:embed all:views/distkeep the Go build compiling on a clean checkout.<meta name="cairnline-views-src-sha256">— a deterministic hash over the view source set (src/**,index.html,package.json,bun.lock,vite.config.ts). A Go test (TestProjectStatusView_BundleBuiltAndFresh) recomputes that hash from the working-tree source and failsgo testloudly if the embedded bundle is missing ("not built") or its hash doesn't match the current source ("STALE — rebuild"). The source files are embedded into the test binary, so editing any of them busts Go's test cache and the guard re-runs on plaingo test— a cached PASS can't mask a stale bundle. The guard is test-only;go build/go installare unaffected.make views/go generate ./internal/app//bun run buildrefresh the bundle. CI builds the bundle beforego test; a separateviewsjob runs typecheck + Vitest.Trade-off for sign-off: previously a clean
go installfrom a fresh clone produced a fully-working Project Status view (committed bundle). Nowgo build/go installstill compile and run but serve a minimal "view not built" placeholder untilbun run buildpopulatesdist/, andgo test ./...fails loudly until the bundle is built/fresh. Matches Hecate'sui/distbehavior, with an added hard local guard.Verification
bun install --frozen-lockfileclean;bun run typecheckclean;bun run test→ 20/20;bun run buildbyte-reproducible (identical sha256 across two frozen rebuilds); oxlint + oxfmt clean.go test ./...green with a fresh bundle; the guard demonstrably fails on a missing bundle ("not built") and on an unbuilt src edit ("STALE"), then passes after rebuild.go vet ./...clean; golden JSON fixtures byte-unchanged.bun verify/verify.tsheadless render: handshake ordered, all sections rendered, no cross-project bleed; screenshot captured.Docs
Views README +
docs/skills/mcp-apps/SKILL.mdupdated for the new toolchain, preview flow, CSP/injection-safety invariants, and the gitignored-dist + build-guard model.