Skip to content

feat(inference): the Inference screen watches a download instead of owning one - #494

Merged
JArmandoAnaya merged 2 commits into
mainfrom
feat/download-progress-ui
Aug 10, 2026
Merged

feat(inference): the Inference screen watches a download instead of owning one#494
JArmandoAnaya merged 2 commits into
mainfrom
feat/download-progress-ui

Conversation

@JArmandoAnaya

Copy link
Copy Markdown
Contributor

The Inference screen half of #492, on top of the wire PR 1 landed. Frontend and
DESIGN.md only — no kernel, no wire, no generated artifacts.

Closes #492.

What was wrong

The screen followed a job id it kept in component state, set from the 202 it
had itself received. So only the mount that pressed the button could see a
transfer
: a reload, a second tab, or walking to another screen and back all
produced Not set up beside a download that was still running, next to a
Download weights button that looked as though nothing had been done.

What it showed was 1 of 5 in a microline. No bar, and no sentence for any state
the job could be in.

What it does now

Everything comes off ConnectionOut.download. useDownloadRun holds no job
id; it reads the row. Recovery is then a property of the shape rather than a
feature anybody wrote — a page that arrives mid-transfer sees it on its first
fetch, because the connection it was going to list says so.

The list polls itself, and only while something is moving. useConnections
takes a refetchInterval that is a function of the answer: two seconds while any
row reports a live transfer, false otherwise. Nothing the browser does reaches
the job — it runs in a worker the server owns — so navigating away stops the poll
and nothing else.

It deliberately does not reuse usePollingQuery, which keeps asking when it
has no data. That is the right rule for a job somebody is waiting on and the
wrong one here: this list is also the annotator's read, and a broken endpoint
would be polled forever behind a screen that never asked for one.

A determinate bar with prose beside it, both and never one — a bar cannot say
queued, and prose alone makes somebody read a number every two seconds to find
out whether anything is moving.

State What the row shows
queued Queued — starts as soon as a worker is free. No bar: an empty track that has not moved is not information.
running 400.0 MB of 1.6 GB · 25%, determinate.
running, every byte in Checking what arrived…, bar full. The job ends by reading the config and recording the connection ready, so a full bar with no sentence reads as a stall.
succeeded Nothing. The Ready badge is the success treatment.
failed The destructive alert, carrying the wire's own sentence, with the retry driven by allowed_actions (principle 9).

Sizes, not a bare percentage: 38% of an unstated amount answers neither how
much longer
nor how much disk. Tabular figures per DESIGN.md's Numbers rule,
because the number changes every two seconds and the words after it must not move
under a reader's eye.

DESIGN.md

The brand's second site is restated from the ingest progress bar's fill to
the Progress primitive's fill. The recorded rationale — a progress bar is
the one piece of chrome a person watches rather than reads
— already argues at
the level of the component, so a rule phrased as one of its callers would make
every later bar look like the third site the list forbids. The invariant stays
checkable by the same git grep, and adding a caller adds no hit, because the
fill lives inside the primitive. The --color-brand comment in styles.css
carried the same stale phrasing and moves with it.

The expected-hits note was already wrong and is corrected here. The grep
returns four lines, not three: the fourth is that styles.css comment, which
states the rule rather than applying it. Three usages is the invariant. This is
adjacent to the sentence being rewritten, so leaving a count a reader can falsify
in one command seemed worse than fixing it.

Decisions worth arguing with

A total that could not be read gets prose and no bar, where the issue asked
for an indeterminate treatment. Progress renders an indeterminate value as an
empty track, which reads as 0% — a lie in the one case where the truth is this
is going, and nobody can say how far
. Giving the primitive an indeterminate
animation is a design-system change and not this screen's to make; the row says
700.0 MB so far — the published size could not be read. The same reasoning
gives the settling phase a full bar plus its own sentence rather than an
indeterminate one: every byte really is here.

bytes() became locale-aware — one Intl.NumberFormat, built once at module
scope. DESIGN.md's Numbers rule asks for one shared helper and this screen now
formats sizes in four places. A whole number of bytes stays whole (512 B, not
512.0 B).

The integrity check keeps its job id. It is a different question with its own
vocabulary and it is not on the connection's wire model, so there is nothing to
read it off — it therefore still has the coupling the download shed, and a reload
still loses a check in flight. Recorded on #492 rather than fixed here; widening
the wire for it is a second design decision, not a consequence of this one.

Test plan

Mutation-verified, three mutations, each reverted by its recorded diff
(git apply -R) with the anchor asserted unique before and present after:

Mutation What went red
refetchInterval deleted follows a transfer to its end with no reload and no click
the interval made unconditional that test, plus both browser tests that count reads
live sourced from mutation.isPending instead of the wire all three wire-derived browser tests, including the reload

New in inference.test.tsx: a transfer nobody on the page started; the queue
named rather than a bar drawn; the settling phase; no bar without a total; no
progress for a connection never downloaded; the poll following a transfer to its
end and stopping; a list nothing is moving in never re-read; and a failure read
off the row with nothing clicked.

New in frontend/app/e2e/inference.spec.ts — the claims that need a browser:

  • a page that never started the download still shows it, asserted across
    page.reload(). This is the shipped bug's exact shape, and there is no jsdom
    equivalent of throwing the whole application away — React tree, query cache,
    every closure that might have held a job id — and seeing what comes back.
  • a transfer left running is where it got to when you come back, across a real
    navigation.
  • the bar moving on the poll alone, with nothing clicked after the screen opened.
  • arriving after a transfer finished; and a failure that happened while nobody
    was watching still saying why.

One thing moved on the repo's say-so. Two of those browser tests originally
asserted "and it stops asking" with page.waitForTimeout, and
tests/scripts/e2e_discipline forbids a spec from waiting on a clock. It is
right to: an assertion that nothing happens over an interval is the shape that
turns a busy runner red. The counting claim moved to inference.test.tsx, where
the request log is the state and jsdom's scheduler is not under test; the second
commit is that change alone.

Found, not fixed

  • useConnections now polls for every caller, including the annotator's
    suggest panel, when a download is live in that workspace. It is correct — the
    panel wants to notice a connection becoming ready — but it is a behaviour
    change for a screen that did not ask for one, and it is worth knowing about
    before somebody debugs a request every two seconds behind the editor.

Local gate

Full run, staged against this box's ~10-minute command ceiling, pytest split by
directory derived from ls tests/ at run time. Every stage's exit code:

Stage Exit
pytest tests/architecture 0
pytest tests/cli 0
pytest tests/examples 0
pytest tests/fixtures 0
pytest tests/formats 0
pytest tests/inference 0
pytest tests/jobs 0
pytest tests/kernel 0
pytest tests/mcp 0
pytest tests/packaging 0
pytest tests/scripts 5
pytest tests/server 0
pytest tests/test_versioning.py 0
ruff check . 0
ruff format --check . 0
mypy src/visionset/kernel 0
lint-imports 0 — 4 contracts kept
check.sh frontend generated 0
check.sh browser 0 — 243 e2e passed, 1 cycle passed

tests/scripts exits 5 by design: nothing pytest-shaped lives there, it is
node --test and runs under check.sh generated. That group has a real stake in
this diff — e2e_discipline.test.mjs is what sent the two clock waits back.

cf. #434, #454, #470, #471, #493.

…wning one

The screen followed a job id it kept in component state, set from the `202` it
had itself received. So only the mount that pressed the button could see a
transfer: a reload, a second tab, or walking to another screen and back all
produced `Not set up` beside a download that was still running. And what it
showed was `1 of 5` in a microline — no bar, and no sentence for any state the
job could be in.

Everything about a transfer now comes off `ConnectionOut.download`. The list
re-reads itself while any row reports a live one and stops the moment none does,
so recovery on return, reload or a fresh tab is a property of the shape rather
than a feature. Nothing the browser does reaches the job — it runs in a worker
the server owns — so leaving the screen stops the poll and nothing else.

A determinate bar with prose beside it, in human sizes: queued names the queue,
transferring reads `400.0 MB of 1.6 GB · 25%`, and the phase after the last byte
— reading what arrived, recording the connection ready — is named rather than
left as a bar sitting full. A total that could not be read gets the sentence and
no bar, because `Progress` renders an indeterminate value as an empty track and
an empty track reads as nought.

DESIGN.md's second brand site is restated at the primitive rather than at one of
its callers: the recorded reason argues about what a progress bar is, and a rule
phrased as *the ingest bar* would make every later bar look like a third site.
The invariant stays checkable by the same grep, and its count is corrected — four
lines match, three of them usages.

The integrity check keeps the job id the download gave up. It is a different
question with its own vocabulary and it is not on the connection's wire model, so
there is nothing to read it off; #492 records that it still has the coupling.
`tests/scripts/e2e_discipline` forbids a browser spec from waiting on a clock,
and it is right to: an assertion that nothing happens over an interval is exactly
the shape that turns a busy runner into a red suite. Two of the new browser tests
were making that claim with `waitForTimeout`.

It moves to `inference.test.tsx`, where the request log is the state and jsdom's
scheduler is not the thing under test. The browser spec keeps what only a browser
can say: a page that never started a transfer showing it, a reload not losing it,
and a bar that moves because a timer fired rather than because anything was
clicked.
@JArmandoAnaya
JArmandoAnaya merged commit 3a9e5ab into main Aug 10, 2026
14 checks passed
@JArmandoAnaya
JArmandoAnaya deleted the feat/download-progress-ui branch August 10, 2026 07:22
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.

Weight download progress: observable on the wire, recoverable from any screen visit

1 participant