Repository navigation
Description
Foundation slice: split the UI into per-view modules so v1 slices can run in parallel (2026-10-27)
Today the whole Lab UI is one app.js (about 1,600 lines) and one style.css, served from a fixed allowlist in lab/internal/ui/server.go. Every UI slice edits the same files, so the remaining v1 slices (#242 Issues, #261 Insights, #255 Formula, #243 Unblock, #254 Loop, onboarding) can only run one after another and conflict on every merge.
Pure refactor. No visible change, no behaviour change.
-
Split
app.jsinto native ES modules underweb/js/:core.js: state, router and URL state, fetch/session/readBack, DOM patch helpers,Intlformatters, aria-live;- one module per view:
inbox.js,molecule.js,work.js,issues.js,insights.js,formula.js,loop.js; - an
app.jsentry that imports them.
Each view module exports
mount(root, ctx)andupdate(state). Views never import each other, onlycore.js. -
Split
style.cssthe same way:tokens.css,base.css, plus one stylesheet per view, linked fromindex.html. -
Serving: the server serves exactly these files from the allowlist. Keep the same cookie,
no-storeand asset-version rules: there is still no wildcard, and any unknown path stays not-found. Every module gets the sameassetVersionMarktreatment. -
Tests: add
servedJS(t), which concatenates every served module in a fixed order. Move string-contract tests to it, plus a test that each listed module is served and that unlisted paths are 404. Existing base tests that fetch only/app.jsare by-design protected deltas (identity). -
Views that don't exist yet (
issues,insights,formula,loop) start as their current placeholder markup moved verbatim, so the next slices each own one module.
Done when
go test ./internal/ui/...passes.- The served page is byte-for-byte unchanged in behaviour: same DOM ids, same routes, same strings.
- No module exceeds 600 lines.
- The operator sees no visual change.
UX rules for every redesign slice (from the 2026-10-26 UI review)
- Follow the approved mockup in
docs/design/ui/redesign-2026-10-26/(its README lists the tokens and rules), with the assets inlab/internal/ui/web/assets/. Reuse the.btnprimary, secondary and danger system and the glass panel from UI polish: composed Start Work controls, segmented lane bar, sharper glass (visual only) #241. - One primary action per card or row. Destructive actions (Retire, Stop Molecule, Stop loop) sit behind a disclosure and a confirmation that names the Molecule or loop. Never place them next to a primary action.
- State in the URL: view, selected Molecule, filter and search (for example
?view=inbox,?m=<id>,?filter=…&q=…) viahistory.pushState, so Back, reload and copied links work. Navigation uses real<a href>. - Forms: real
<form>s, so Enter submits. Inputs carryname,autocomplete="off"and an example placeholder ending in "…". A submit button stays enabled until the request starts, then shows progress. Errors appear inline, linked witharia-describedby, and focus moves to them. - No rebuild under the user: polling patches the DOM in place, keyed by id. Never rebuild a block that has focus or an input being typed in. Keep one persistent
aria-liveregion and update its text only when it changes. - Readable values:
- dates via
Intl.DateTimeFormatplus relative time; - money via
Intl.NumberFormat(one precision rule; small amounts never round to $0); - counts via
Intl.PluralRules; tabular-numsfor numbers;- never raw phase codes,
assay-…ormol-…ids, full digests or ISO timestamps in the main UI (short forms only, behind "details" with a copy button).
- dates via
- Plain language, second person, Title Case buttons: "Plan ready for your approval", "Coding · 18 min", "Ready to publish". Keep the primitives true: Lab, Molecule, Atom and Formula keep their names and meanings.
- Accessibility and touch:
- a skip link;
- one
<main>; - ordered headings;
- visible
:focus-visibleeverywhere; - targets at least 44 px on coarse pointers;
touch-action: manipulation;- reduced motion and forced colors honoured;
- no horizontal page scroll at 400 px.
Review checklist (read before coding)
The adversarial review checks these every time. Recent slices failed review on all of them, so build them in from the start:
- Bounds: sizes, counts, time and total work are bounded, and hitting a bound fails safe (incomplete or truncated, never silently accepted).
- Filesystem safety: paths cannot traverse. Use
LstatandO_NOFOLLOW, and check symlinks and ownership. Watch for TOCTOU races on shared dirs (/tmp). Only ever delete Forgecell-owned, marked paths. - Untrusted input: issue text, repository files, model and tool output, and ledgers can be malformed, huge or hostile. Validate them, never interpolate them into HTML or a shell, and never let them trigger retries or approvals.
- No local leaks: no absolute local paths, environment variables or credentials in output, logs, the UI or ledgers.
- Contract changes: a changed adapter invocation bumps the protocol, and approved digests still bind exactly what was shown.
- Tests: deterministic. Use no wall-clock budgets (use readiness markers or seams), write nothing into the user's real temp or home, and cover the failure path as well as the happy path.
Plan checks (for intake)
Check category must be one of regression, candidate or setup. Use targeted package tests only as plain go test commands (for example go test ./internal/<pkg>/...); the protected view adds -json itself when protected deltas are declared. The Formula baseline already runs the full suite once, so don't add another full go test ./... check, and don't repeat its go vet or gofmt checks. A plan may hold at most 16 checks in total, counting the 5 Formula baseline checks, so prefer one combined targeted command over one check per package.
Answers to the planning questions (2026-10-27)
- Splitting big views. A view may have its own sibling sub-modules, for example
molecule.jsplusmolecule-plan.js,molecule-decision.jsandmolecule-timeline.js.- A view may import its own sub-modules and
core.js, never another view. - Helpers used by two or more views go to
core.js, or tocore-*.jsfiles such ascore-gates.jsforgatePost,submitGate,readBackandjobText, so no file exceeds 600 lines.
- A view may import its own sub-modules and
- Module ownership.
loop.jsowns the existing side-panel drive forms and the header loop pill.insights.jsowns the Lab overview rendering.formula.jsowns the active-Formula card.issues.jsowns the Issues placeholder.- Moving working code is fine; behaviour stays identical.
- Paths.
web/app.jsstays the entry at/app.js. Modules live inweb/js/*.js, served as/js/*.js. Stylesheets live inweb/css/*.css, served as/css/*.css, linked fromindex.htmlin a fixed order (tokens.css,base.css, then the views)./style.cssis removed and returns 404. The allowlist names every file explicitly, with no wildcard.
- Markup. It stays in
index.html. Each module mounts on its existingdata-view-panel, so the served page before the script runs is byte-for-byte unchanged. - CSS contract tests. Yes: add
servedCSS(t), which concatenates every linked stylesheet in link order.mediaBlockandcssRulesgather from all sheets, so every@mediaand forced-colors block is still tested.
Expected protected deltas. Base tests that fetch only /app.js or /style.css and assert moved strings fail by design in the protected view. That's expected; they're declared after the first run.
- Keep base tests compiling: never change the signature of a Go function or helper that a base
_test.gofile calls. Add new ones instead.
Plan checks for this slice (2026-10-27)
- "Operator sees no visual change" is a manual step after delivery, not a plan check. Don't add a check for it.
- Every plan check:
category: regressionorcandidate, withreviewedAcceptanceandindependentProvenanceleft empty. The analysis must never assert independent acceptance. - One targeted check is enough:
go test ./internal/ui/....
Activity
refactor(ui): split app.js/style.css into per-view ES modules and sty…
DanielNetzer commentedon Oct 27, 2026
Merged via #270.