Nothing in the UI said which build was loaded. /api/version has reported
version, build and source_commit since the harness started versioning itself
apart from the public semver, but the only way to read it was to curl the
endpoint — so "is the preview actually running the commit I just merged?" took
a terminal to answer.
Pins a footer under the settings sidebar nav showing the registered version
(plus the harness build when it differs) and the short source commit, with the
full hash on hover. It sits outside the nav's scroll container so it stays at
the bottom-left, and it is .admin-only, so syncAdminVisibility() hides it from
non-admins the same way it hides the Admin nav group.
The commit resolves at import via `git rev-parse HEAD` and is the string
"unknown" when that fails — a read-only Docker tree with no .git. The footer
treats "unknown" as absent and stays hidden when nothing is left to show,
rather than printing it. The collapsed rail and the two narrow tab-rail
layouts hide it too: neither has a bottom-left to write in.
style-part-01..08 were arbitrary 6,000-line cuts, so the names said nothing
and a reader had no way to guess which file held a rule.
The original stylesheet turns out to be roughly area-grouped already, so
cutting where the content changes rather than every 6,000 lines produces
boundaries worth naming. Classifying every top-level rule by selector prefix
and smoothing over a 40-rule window gives 17 stable runs: agent-chat, compare,
memory, documents, admin-settings, skills, gallery, cookbook, tasks,
image-editor, email, notes, calendar, research.
The numeric prefix stays because load order is load-bearing, and it also
disambiguates the areas that appear twice: the original interleaves, and
merging non-adjacent blocks of the same area would reorder the cascade.
The names describe where a file sits, not a claim that it holds every rule for
that area or only rules for it. The header in each file says so, because that
is the misreading this naming invites.
Reconstruction was checked against lab's style.css before rewriting: identical
after whitespace normalisation, all 47,528 lines accounted for. The computed
style baseline still reproduces exactly.
static/style.css was 47,530 lines. It is now nine files under static/css/:
tokens.css holds the :root custom properties, light theme and density classes,
and style-part-01..08 carry the rest in their original order. Nothing was
rewritten; every line moved verbatim and the <link> order in index.html
reproduces the original file byte for byte.
Cut points are brace-depth zero and outside block comments, so no rule or
comment is split. All 47,531 source lines are accounted for across the
fragments.
The computed-style baseline recorded before the split is reproduced exactly,
which is the evidence that the cascade is unchanged rather than an argument
that it should be.
Three things the split broke and this fixes:
- The harness self-test swapped two conflicting .attach-strip blocks inside
style.css to prove the digest is order-sensitive. It rewrote one hardcoded
URL, so with the file gone it silently measured an unmodified page. It now
searches every stylesheet, rewrites whichever holds the pair, and fails
loudly if none does.
- Four tests asserted the cache-bust token by matching /static/style.css?v=.
They check the real invariant now, that every app stylesheet shares one
token with app.js, through stylesheet_cache_version().
- The three panel stylesheets carried their own tokens, so a browser could
hold half an old cascade and half a new one. All app stylesheets now bust
together.
Duplicate @keyframes names resolve last-wins across the whole cascade, so
every definition but the last was dead code that still read as live at its
call site. Two of the five duplicated names differed from the winner:
fadeIn style.css had an opacity-only variant before the one that
adds translateY, so every consumer was already sliding
research-pulse a background-colour pulse sat before the opacity/scale one
that actually runs
The other three (spin, loading-bounce, pulse) were byte-identical repeats.
Removing the losing definitions changes nothing rendered, which the computed
style snapshot confirms: the committed baseline is reproduced exactly across
all three pages and 24 variants. Deciding that a consumer wanted the plain
fade, or the background pulse, would be a visual change and belongs in its own
PR with screenshots.
This also unblocks the mechanical stylesheet split. While two definitions of a
name differed, neither could be moved: relocating either changes which one is
last, and therefore changes behaviour. A regression test now fails on any
duplicate name so the trap cannot come back.
Last step of splitting static/style.css by panel. This moves the cookbook,
deep research, memory and settings rules that can move into
static/css/cookbook-research-memory-settings.css.
After the three steps style.css still holds the shell and every rule whose
move would change the cascade, which on this branch is most of the file.
Eager ordered links, verbatim moves, no rendering change.
Second step of splitting static/style.css by panel. This moves the email,
calendar, notes and tasks rules that can move into
static/css/email-calendar-notes-tasks.css.
Same shape as the first step: eager ordered links, verbatim moves, no
rendering change. The header comment in each split file lists the load order
and the earlier file's header is updated so all of them agree.
static/style.css is 51,425 lines and holds every panel's styles, so two
people working on unrelated panels still edit the same file. This is the
first of three steps that give each panel group its own stylesheet. It moves
the document library, image gallery and image editor rules that can move
into static/css/documents-gallery-editor.css.
The new file is loaded eagerly from index.html immediately after style.css,
so the cascade is the concatenation of the two in that order, which is the
order those rules already had. Nothing is deferred and nothing renders
differently.
Rules moved verbatim. A rule stays in style.css when its selector group
covers more than one panel, when the class is shared design language used
from modules outside the panel, or when moving it would flip which of two
equally specific rules wins on an element the markup puts both classes on.
That last rule is what keeps this change small: most panel-prefixed rules on
this branch compete with a shared component rule somewhere later in the file.
Nineteen tests read static/style.css directly and go red the moment a rule
they assert on moves, which says nothing about the page. They now go through
tests/helpers/stylesheets.app_css(), which concatenates the stylesheets in
the order index.html loads them. The helper also exposes the <link> markup
for tests that build a synthetic page through Playwright.