Skip to content

refactor(mcp): rebuild Project Status app view as Web Components - #82

Merged
chicoxyzzy merged 2 commits into
mainfrom
feat/mcp-apps-views-web-components
Jul 9, 2026
Merged

refactor(mcp): rebuild Project Status app view as Web Components#82
chicoxyzzy merged 2 commits into
mainfrom
feat/mcp-apps-views-web-components

Conversation

@chicoxyzzy

@chicoxyzzy chicoxyzzy commented Jul 9, 2026

Copy link
Copy Markdown
Member

Summary

Stacked on feat/mcp-apps-ui-extension (PR #80, which now also carries #81's docs/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.build script into one committed ~356 KB dist/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 shared BaseComponent. Rendered output is identical to before (same three sections, badges, count grids, inert action pills, activity buckets).

What changed

  • Web Components restructure. One component per section + a shared base. Templates are static, data-free structure only; every payload value is written via textContent/attributes — no innerHTML with interpolated data — preserving the injection-safety property the prior review verified. A payload containing <script> or HTML entities renders inert.
  • CSP unchanged and strict: default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'. Built HTML has zero eval(, zero external script/style/asset references, inline module script only. Styling via shadow DOM + adopted stylesheets.
  • Toolchain mirrors Hecate's 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 --noEmit typecheck, oxlint/oxfmt. Deterministic, byte-reproducible build.
  • Vitest component tests (happy-dom): 20 tests across the four components — rendering from the golden fixture structuredContent payloads, per-project state reset (no cross-project bleed), and escaping/inertness.
  • Dev preview: bun run preview (Vite dev) loads the view outside an MCP host with selectable fixture payloads, for local iteration. Documented in the views README.
  • TS everywhere: verify/verify.mjsverify/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/dist convention and adds structural protection so forgetting to build fails hard and immediately at go test time (not just a CI diff):

  • internal/app/views/dist/project-status.html is gitignored and built in CI/release; a committed empty dist/.gitkeep + directory embed //go:embed all:views/dist keep the Go build compiling on a clean checkout.
  • Structural staleness guard. The Vite build injects <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 fails go test loudly 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 plain go test — a cached PASS can't mask a stale bundle. The guard is test-only; go build/go install are unaffected.
  • Convenience: make views / go generate ./internal/app/ / bun run build refresh the bundle. CI builds the bundle before go test; a separate views job runs typecheck + Vitest.

Trade-off for sign-off: previously a clean go install from a fresh clone produced a fully-working Project Status view (committed bundle). Now go build/go install still compile and run but serve a minimal "view not built" placeholder until bun run build populates dist/, and go test ./... fails loudly until the bundle is built/fresh. Matches Hecate's ui/dist behavior, with an added hard local guard.

Verification

  • bun install --frozen-lockfile clean; bun run typecheck clean; bun run test → 20/20; bun run build byte-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.ts headless render: handshake ordered, all sections rendered, no cross-project bleed; screenshot captured.

Docs

Views README + docs/skills/mcp-apps/SKILL.md updated for the new toolchain, preview flow, CSP/injection-safety invariants, and the gitignored-dist + build-guard model.

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
chicoxyzzy force-pushed the feat/mcp-apps-views-web-components branch from 8ece7c6 to caf6516 Compare July 9, 2026 18:25
@chicoxyzzy
chicoxyzzy changed the base branch from feat/mcp-apps-ui-extension to main July 9, 2026 18:32
@chicoxyzzy chicoxyzzy closed this Jul 9, 2026
@chicoxyzzy chicoxyzzy reopened this Jul 9, 2026
@chicoxyzzy
chicoxyzzy marked this pull request as ready for review July 9, 2026 18:38
@chicoxyzzy
chicoxyzzy merged commit 6429ce4 into main Jul 9, 2026
2 checks passed
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