Skip to content

Redesign the Compute tab - #272

Merged
Aayam Bansal (aayambansal) merged 21 commits into
mainfrom
design/compute-redesign
Aug 8, 2026
Merged

Redesign the Compute tab#272
Aayam Bansal (aayambansal) merged 21 commits into
mainfrom
design/compute-redesign

Conversation

@KB-syntheticsciences

@KB-syntheticsciences KB (KB-syntheticsciences) commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

Redesigns the Compute tab: a host instrument strip, a collapsible kernel plate,
and a numbered run ledger. Everything is inside src/atlas/, apart from three
helpers added to notebook/runtime.ts whose only consumer is KernelCard.tsx.

What changes for a user

before after
host figures tiles, free memory one display figure, memory used, 20-bar history, headed "System statistics"
kernel card full record always open collapses to a head; memory and CPU survive the collapse
kernel identity Py tile beside the name Kernel 01 · Python eyebrow above it
uptime recomputed only on poll ticks once a second
ledger plain grid dotted-leader rows, two columns
jobs flat list numbered ledger, status on every row, sweep on running rows
network state trailing the sandbox behind a middot own line, warning dot
refresh button present, flickered every poll removed; the panel already polls

Fixes that are not cosmetic

  • The jobs total froze on the Jobs tab. The count was refreshed only while
    Kernels was selected, so a project with 11 runs kept reading 10. The panel now
    reports its own total off the poll it already runs.
  • Environment rows overlapped at narrow widths. justify-self: end sizes a
    grid item to its content, so text-overflow: ellipsis never fired and a long
    working directory painted across its own label.
  • Uptime and the plate's figures could not move. Uptime was never
    re-evaluated between polls; memory was drawn on a GB scale where a 16 MB
    kernel reads 0.0 forever.

Design decisions worth a second opinion

  • CPU is a share of the machine, not percent of one core. The backend
    measures the top convention, where a kernel across two cores reads 200%. That
    is divided by the host's core count here so it never exceeds 100%, at the cost
    of resolution — a single-threaded kernel tops out at 12.5% on an 8-core box.
    The segments below carry the core count that a small share hides.
  • Typography follows Streamline OpenScience and unify credential access #269. This surface uses the app's sans face in sentence
    case, with mono kept only for code-shaped content: commands, log output,
    paths, run ids, digests and version strings. That is the same line the rest of
    the app draws, and it is why the mono font picker in Settings still applies
    where it should.

New modules

host-instruments.ts, kernel-plate.ts and run-ledger.ts hold the arithmetic
as pure functions, each with its own tests, so the figures can be asserted
without mounting a component or standing up a server. host-tiles.ts is
replaced by host-instruments.ts.

Testing

  • Repo typecheck: 7/7 packages.
  • 368 pass / 0 fail across the compute and runtime suites.
  • Verified in a browser against a live server: collapse and expand, the kernel
    controls end to end, the ticking stopwatch, the jobs ledger and its row
    detail, the two gauges at equal width, and no element on the surface rendering
    in capitals.

One unrelated stale test, for information

ProjectsWorkbench > locks the screen to the compact sizing contract asserts
home-workbench.css contains min-height: 48px; #269 changed that bar to
52px without updating the assertion. CI does not exercise it and this branch
does not touch the file, so nothing here is blocked by it — noted only so it is
not a surprise when running the suite locally.

The tab stated everything at one volume: three metered tiles whose captions
repeated what their numbers already said, a flat run list where every row
read "Modal · succeeded", and a kernel sitting flat on the panel under a
boxed callout explaining how kernels work.

3a treats it as an instrument. Free memory — the figure you actually want
before starting a run — leads at display size over its unit and ceiling in
small mono, beside a twenty-sample sparkline of memory pressure with the
newest few in the accent, so the reading carries direction as well as a
value. Cores stay discrete because they are countable. The tabs are set in
mono capitals in the same voice as those labels, so the strip and the
navigation read as one apparatus rather than a readout with tabs bolted
underneath.

Runs become a numbered ledger under a single RECENT RUNS header, each row
an index, the name with its target beneath it, then an uppercase status in
a fixed column, then the age. The status is stated on every row rather
than only where something is wrong: in a ledger it is a column, and a
column with holes is harder to scan than one always filled, because the
eye tracks a fixed position instead of hunting for which rows chose to
speak. The kernel moves into its own inset card — it is a distinct object
with its own controls — with its metrics set as a printed record, label
and value joined by a dotted leader.

The sparkline is the one genuinely new capability. The compute route
reports a point in time and the strip kept nothing, so it now holds a
bounded twenty-sample buffer. A poll that failed contributes nothing at
all: repeating the last reading would draw a flat stretch that looks like
measured calm, and inserting a zero would draw a cliff that never
happened. Short of a full window the bars are right-aligned against an
empty left, so a freshly opened panel reads as not-yet-measured rather
than as a climb from nothing.

Every colour reaches the surface through a semantic token — the app ships
sixteen themes, and the terracotta, the instrument blue and the status
colours all resolve per theme. Verified against a running build: the
ledger, the sparkline filling from seven bars to twenty, and every colour
moving between light and dark.

The logic worth testing is pure and tested apart from the components:
host-instruments covers the reading, the bounded sample buffer and the
histogram's alignment; run-ledger covers the numbering, the tones and the
uppercasing. The tile model they replace is deleted rather than left
behind.
…h button

The 3a override set the header to two grid columns while the markup still had
three children, so the language badge and the name were pushed to opposite ends
of the row, the subtitle right-aligned under a title that was itself in the
trailing column, and the owner pill wrapped onto a full-width row. Place all
three children explicitly: the language becomes an eyebrow at row 1, the title
sits beneath it, and the owner pill takes the trailing column. The lifecycle
pill gets justify-self so it stops stretching the width of the card, which is a
single-column grid.

Also removes the panel's refresh control. It was disabled for the duration of
every 2.5s poll, and the :disabled opacity rule made it flicker roughly twice a
second. The panel already refreshes on that interval and on visibilitychange,
so the button asked the user to trigger what was happening anyway; the
"Synced Ns ago" line beside the title carries the same information honestly.
The card rendered its whole record at once — an eight-row ledger, the
environment block, three controls and an identity list — so a session with more
than one runtime became a stack of full records, and the question the list is
actually for (which kernels exist, is any of them busy) was buried.

3a collapses it to a head: the eyebrow and name on the left, uptime and a
lifecycle pill on the right, and a two-figure usage strip that survives the
collapse because "is this runtime in my way" is what a collapsed list still has
to answer. The head is the toggle rather than the caret alone — this pane is
read on a tablet, where a 9px caret is well under a reliable thumb target.

The usage strip draws the kernel's own memory against the host's total and
counts the cores it is turning, so both figures need a host reading. That is
already on the host strip's poll above, so it is lifted from there through the
surface rather than giving every card its own /notebook/compute request; the
ceiling a plate shows then always agrees with the headline beside it. Anything
unmeasured stays "—" rather than being filled in with a zero that would read as
a measurement.

Also fixes text overlapping in the environment block. The base rule gives the
value `justify-self: end`, which sizes a grid item to its content rather than
its column, so `text-overflow: ellipsis` never fired and a working directory
longer than the pane overflowed leftward across its own label. Stretching the
value to the column is what makes it clip.

Two pieces of the old card go with the redesign: the standing paragraph under
the controls, whose every sentence is already on the button it describes as a
title, and the owner pill for a kernel owned by the session being read, which
said only what the panel's heading already says. The pill still appears for a
runtime owned by some other session, where nothing else on the card carries it.
"Unavailable" is three times the width of the figure it replaces and says
nothing the lifecycle pill beside it does not already say.
The eyebrow sat beside the title group rather than inside it, so the head had
three flex children and space-between spread them across the row — which
centred the kernel's name instead of setting it under its eyebrow.
…rols on one row

The headline read free memory while the histogram beside it plots the fraction
in use, so the number counted down as the bars climbed and the two instruments
appeared to disagree about the same machine. Both now measure the same thing.
The figure is derived by subtraction rather than reported, and a body that
carries a total without an available degrades to "—" instead of guessing.

The control row was a two-column grid, which stranded Stop on a line of its
own — and Stop is the control that discards work, so it read as an afterthought
rather than the one to be careful with.
…e jobs total live

Three defects with the same shape: something that changes on its own was only
ever read when something else happened to change.

Uptime froze. The kernel object is reconciled in place, so it does not change
while a runtime simply keeps running — nothing re-evaluated the label and the
head sat at "2s" for as long as the kernel lived. The card now drives it from
its own one-second clock, which exists only while there is something to count.

The jobs total beside the tab label froze at whatever it read when the user
switched tabs: the surface's poll was gated on the kernels tab, so a project
with eleven runs kept reading ten while the eleventh was still going. The jobs
panel now reports its own count off the poll it already runs, and the surface
polls only when that panel is not mounted — one request for one number.

A run that is still moving now sweeps a gradient along the foot of its row. The
status column already names it, but a word does not read as motion, and this is
the only row in the ledger whose value will change while it is being read. The
sweep uses the same warning tone as the status text, so the row says one thing
in two ways, and it degrades to a static bar under prefers-reduced-motion.

Also splits the network state onto its own line under the environment header.
Joined to the sandbox by a middot it made a label long enough to wrap inside
its own pill, and it buried the fact on this card that decides what a run can
reach. An open network carries the warning tone; a blocked one stays grey,
rather than spending attention on the safe and far more common case.
Both numbers on a collapsed plate were live and neither could ever change.

Memory was rendered in GB to one decimal because the host ceiling beside it is
tens of GB. A Python kernel holds tens of MB, so every kernel that was not a
training run read "0.0" for its entire life. It now uses the same adaptive
helper the opened ledger does — "16 MB" against "/ 16.4 GB" — so the head and
the row beneath it state one measurement in one unit.

CPU was converted from the percentage the backend measures into a core count,
which is a resolution most kernels never reach: single-threaded Python lives
between 0 and 100%, so it read "0 of 8" no matter how hard it was working. The
conversion was also internally inconsistent — at 24% the figure rounded to zero
cores while the segment bar ceiled to one, so the number and the bar disagreed.
The head now states utilisation as the ledger already did, and the segments
keep counting cores, which is what makes multi-core work legible at a glance.

The regression test drives the reconciled store rather than the helper: these
figures hang off a nested `resources` object, and what failed here was never
the arithmetic but whether a changed reading could reach the rendered head.
…the opened plate

CPU was stated in the unit the backend measures — percent of one core over the
kernel's process group — which reads 300 for a kernel across three cores and
raises the fair question of how a percentage exceeds 100. It is now divided by
the host's core count, so it is a share of the whole machine and cannot.

The trade is resolution: a single-threaded Python kernel, which is most of
them, tops out at 12.5% of an eight-core box. Hence the decimal, and hence the
segments below it keeping the core count that a small share hides.

The opened ledger takes the same value rather than reading the field a second
time — computing it there independently would print 187.5% beside 23.4% for one
kernel. Only the placeholder differs: a lone "—" among three "Unavailable"
siblings reads as a different kind of absence rather than the same one. Where
there is no core count the share is not stated at all, since passing a per-core
figure off as a machine one is wrong by a factor of eight, silently.

Also gives the opened body its vertical rhythm. The card is gap: 0 so the
divider under the head can run the full width of the plate, and the blocks
below carried only horizontal margins — so they stacked flush and the whole
record read as one dense paragraph of figures.
…chine

Dividing by the host's core count capped the figure at 100%, but it turned a
statement about this kernel into one about the machine it happens to be on: the
same kernel doing the same work read 23.4% on an eight-core box and 93.8% on a
two-core one. The card now states what the backend measures — percent of one
core over the kernel's process group, the top(1) convention — so a kernel
across two cores reads about 200% and the number means the same thing wherever
it is read.

It also restores resolution the normalising cost: a single-threaded Python
kernel, which is most of them, could never read above 12.5% of an eight-core
host however hard it was working.

The segments below carry the machine's scale, one per core, so a figure over
100% is legible rather than surprising, and the unit is stated in the group's
title instead of being left to be inferred.

The figure no longer depends on the host reading at all, so a capacity body
that arrives without its cpu section now costs the segments' accuracy and not
the kernel's own number.
…f the machine"

This reverts commit c0da1aed.

A figure above 100% reads as a bug to anyone who has not met the top(1)
convention, and that cost outweighs the precision the per-core figure bought.
The kernel's CPU is stated as a share of the whole machine again, so it stays
between 0 and 100%.

The known trade stands: a single-threaded Python kernel cannot read above 12.5%
on an eight-core host. The segments below carry what the small figure hides —
23.4% says little, two of eight segments says "two cores".

Rounds the share once and derives both the figure and the lit segments from it,
which the reverted version did not: reading each independently let a kernel at
0.2% of one core light a whole segment while the figure beside it read 0.0%.
The strip states the machine and every kernel plate states that kernel, and
nothing on the surface said which was which. The two are easy to confuse now
that both carry a memory figure and a CPU one — and a reader who took the
strip's "6.7 GB USED" for their kernel's own would be out by three orders of
magnitude.

Set in the same mono capitals as the instrument labels beneath it and in the
faint tone, so it names the block without competing with the figure it
introduces. It is the section's accessible name too, replacing "Host capacity",
which was never shown to anyone reading the screen.
origin/main's compute surface names no font at all: PR #269 put the whole app
on one sans face at html/body and stripped the mono treatment out. This branch
still reached for --font-code in fifteen places, so the surface was the only
part of the app in a second face.

Dropping the declaration rather than naming --font-family-sans, so these
inherit whatever the app is set to and cannot drift from it later.

Mono was also supplying tabular figures for free. Inter's are proportional, so
the uptime — which now ticks once a second — and the ledger's right-aligned
values would reflow a pixel or two on every poll. The three places that show
changing numbers ask for tabular-nums explicitly.

The capitals and letter-spacing stay: that is the ledger's structure rather
than its typeface, and it is what the approved 3a renders show.
origin/main's compute surface uses text-transform nowhere and keeps
letter-spacing only as negative tracking on one heading; it renders the kernel
state as plain lowercase text. This branch had eight uppercase transforms and
twelve letter-spacings, so the surface spoke in a different voice from the rest
of the app.

Removing the transform alone would not have been enough: several labels were
shouted in the source rather than by CSS, and would have kept shouting. The
eyebrow, the host strip's Cores label, the memory unit and ceiling, the jobs
ledger heading, and the run status and target all now read in sentence case at
their source.

The ledger's target reads as the server names it — "This computer", "Modal" —
rather than being upper-cased on the way to the screen.
…it it

The sentence-case pass removed the uppercase and tracking declarations from
this surface, but the base rule in atlas.css sets them on
`.kernel-card__metric span`, and the cascade resolves per property — so the
base declarations kept winning and the ten ledger labels kept shouting.

This is exactly why origin/main writes `letter-spacing: 0; text-transform:
none` out by name rather than deleting its own declarations. Same fix here, so
the branch now matches it in effect and not only in source.

The test that was supposed to guard this asserted the stylesheet does not
CONTAIN "text-transform: uppercase", which was true the whole time the screen
was uppercase — absence of a declaration is not absence of the effect. It now
asserts the base rule still sets it and that this file turns it off by name,
so the guard fails if either half of that pair moves.
3a pinned the cores gauge to 126px against a memory gauge that took the
remaining space. That is close to even in a narrow pane, but this one is read
wide, where it left the memory track nearly twice the length of the core
segments — two instruments of different sizes rather than one strip. Both are
now equal halves.

Also removes the .kernel-card__control-note rule, which styles a paragraph this
branch deleted along with the standing note under the controls.
@vercel

vercel Bot commented Aug 7, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
openscience Ready Ready Preview Aug 8, 2026 7:45am

Request Review

The surface moved to the app's sans face by editing stylesheets, but this panel
styles inline — so its rules were missed, and the ledger's labels stayed mono
while the kernel plate's equivalents beside them did not. The section heading,
the ordinal, the target, the status word and the age now use the app's face.

Code-shaped content keeps mono, which is what origin/main does too: the
command, the log, the working directory, run ids, artifact paths, sha256
digests and the reproducibility values.

The ordinal loses mono's fixed advance with this change, so it asks for tabular
figures to keep its column straight; the age already did.

The test pins both halves by name, since the failure mode here is not that a
rule is wrong but that a panel is forgotten.
@KB-syntheticsciences KB (KB-syntheticsciences) changed the title Rebuild the Compute tab to the 3a design Rebuild the Compute tab Aug 7, 2026
@KB-syntheticsciences KB (KB-syntheticsciences) changed the title Rebuild the Compute tab Redesign the Compute tab Aug 7, 2026
… light themes

The lifecycle pill set the status word in the status colour on a 12% tint of
that same colour. On a dark ground the tint darkens it and the hue reads
clearly; on a light one the tint is near-white and the hue has nothing to
separate from.

Resolving all shipped themes and measuring the pair gives the scale of it:
1.0:1 on vesper/light — the word and its ground at identical luminance — 1.3
on shadesofpurple, 1.6 on dracula, nord and monokai, and under AA on nearly
every light variant. Dark variants were fine throughout, which is why building
against one dark palette never showed it.

The word is now set in the neutral text colour and the status colour moves to a
dot inside the pill, where legibility does not depend on contrast with the
text. The tinted ground and the coloured ring still carry the tone. Across 90
readings — every theme, both variants, all three tinted states — the worst case
is 3.8:1 and nothing falls below AA-large.

Two alternatives were measured and rejected: the design system's own
surface-*-weak / text-on-*-base pairs fix warning alone (its on-colour is
neutral, which is the whole reason it passes) and leave success and critical
where they were.
@aayambansal

Copy link
Copy Markdown
Member

KB (@KB-syntheticsciences) can you ensure that there is minimal consistent typography & like it's not all caps or all lowercase and it is uniform all around! Also, please make sure that the text is not cluttered (too close together).... Check out Claude Science implementation - added screenshot in the openscience fixes docuement!

@aayambansal

Copy link
Copy Markdown
Member

otherwise, looks good :)

… the last labels

The surface rendered eleven distinct sizes — 9.5, 10, 10.5, 11, 11.5, 12, 13,
13.5, 14, 15, 17 — several of them near-duplicates doing the same job: three
for small labels, three for values, three for sub-headings. Differences that
small do not read as hierarchy, only as inconsistency. They collapse to
10 label · 12 body · 14 sub-heading · 17 object · 44 display.

Casing was mixed on the same card: "Lifecycle" and "Python environment" beside
"ready", "bubblewrap sandbox" and "network disabled". The three helpers still
returning lowercase now return sentence case, which is where the difference
belongs — the acronyms (RAM, CPU, GPU, VRAM) and the code-shaped strings
(paths, ids, digests) are deliberately left alone.

kernelStateLabel now capitalises the states it used to pass through raw, so
"running" and "crashed" match "Ready" and "Not started" rather than only the
two that had explicit cases.
Gaps were 1, 3, 4, 5, 6, 7, 9, 10, 12, 16 and 20 with no rule behind which was
used where, so grouping read as crowding: a ledger row sat 9px from the next
while the block below it sat 16px away, close enough that neither separation
said anything.

Text now sits on 8 within a group, 12 between rows, 20 between blocks. The
values left off that scale are icon-sized and deliberate — 6 between a dot and
its word, which reads as pairing rather than separation, and 3 and 4 for the
core segments and gauge strips.

The plate's own padding is square at 16 rather than 16/16/14, and its head
breathes at 16 instead of 12.
Stacked, the unit and ceiling spent three lines saying what one says, and put a
column of 10px type against a 44px numeral — the densest corner of the surface.
They now read as one phrase beside the figure, in a single muted tone, which is
how the Claude Code compute panel sets the same information.

Two tones inside one line read as an accident rather than a hierarchy, so the
ceiling matches the unit rather than sitting a step quieter.
@aayambansal
Aayam Bansal (aayambansal) merged commit f8d1528 into main Aug 8, 2026
15 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.

2 participants