Skip to content

feat(app): Inference section, rail entry, download-size surface (#421 slice 2) - #454

Merged
JArmandoAnaya merged 4 commits into
mainfrom
feat/inference-section
Aug 9, 2026
Merged

feat(app): Inference section, rail entry, download-size surface (#421 slice 2)#454
JArmandoAnaya merged 4 commits into
mainfrom
feat/inference-section

Conversation

@JArmandoAnaya

Copy link
Copy Markdown
Contributor

The Inference section, the wire surface D1 was missing, and the callback #424's D6
panel has been waiting for (#421 slice 2).

The first attempt at this slice halted before its first edit: it was authorised by a
rail decision comment that did not exist, and #421 explicitly reserved the placement
question. The Decision (Armando, 2026-08-08) comment choosing option (a) is what
unblocked it, and this PR carries that supersession into the two documents that state
the old rule.

What ships

The rail entry, and the governance documents that own the rule

Inference joins the rail between Projects and the account control. The closed-rail
rule lives in DESIGN.md, twice, and both citations move — the skill's sitemap
enumerates routes and tabs, so rail membership was never in it. Both are updated here,
along with a line saying what earns an entry: a workspace-level object every project
uses and that has nowhere else to live. Frequency of use does not qualify anything.

information-architecture gains the route, the supersession, and one consequence worth
writing down rather than rediscovering: a rail destination has no back-link, for the
same reason a tab has none. The rail is its way out, and a second answer to "where am I"
inside the pane would contradict it. PARENT.inference therefore exists as the address
other screens send people to — D6's panel is the first — and not as a parent anything
returns from.

The download-size surface (server)

D1 requires the local form to show what a download costs before somebody confirms,
and nothing on the wire could say. GET /inference/download-size?model_id=…&model_revision=…
answers from the publishing hub's file listing.

The shape is a standalone lookup keyed on the pair, not a field on a connection
response, because the moment the number is needed is the moment before a connection
exists — by the time one does, the decision has been taken. Query parameters rather than
a path, because a model id contains a slash and a segment that must be escaped to be
written is a URL people get wrong by hand.

Three decisions inside it worth naming:

  • Every file in the revision counts, because download fetches every file in the
    revision. A figure counting only .safetensors would understate what lands on the
    disk for any repository that also publishes a tokenizer or a second serialisation of
    the same tensors. The two numbers describe the same set on purpose.
  • A file the listing does not size is refused, not skipped. Silently dropping it
    answers with a number smaller than the truth, which is worse than no number when the
    number's only job is to inform a decision.
  • Cached per model_id@revision, and nothing invalidates it — correct rather than
    merely fast. A pinned revision is a fixed set of files, so there is no event that
    could change the answer. A refusal is not cached: one bad moment on the network
    should not be permanent for the life of the process.

huggingface_hub stays inside a function, so the base import graph is untouched and
tests/architecture/test_optional_runtime.py still holds. A machine without the extra is
refused here with the same install command the download gives — the size is read with the
client that would do the fetching, so it cannot be otherwise.

SDK-first: the behaviour is visionset.inference.download_size, and it reaches a
terminal as visionset inference size — the one command in that file which opens no
workspace, and which therefore carries domain_errors itself rather than inheriting it
from opened_workspace. No MCP tool, matching the existing surface: MCP has no inference
tools at all.

The Connections screens (ui-core)

List, empty state, two-step create, edit, delete, and the download_weights action with
its background job. inferenceQueries.ts moved from annotator/ to data/ — two
surfaces read the connection list now, and a second copy under whichever screen was
written second is how two callers come to disagree about what "ready" means.

Nothing on the screen decides what is legal. Download weights is rendered from
allowed_actions and from nothing else, including on a machine with no local runtime:
whether this machine has the extra is not a fact about the connection, and a control
that vanished would take the install command with it (principle 9).

The status column has two values, not three. #421's journey lists
Ready / Not set up / Unreachable; the wire has two, because setup_state is
deliberately not a reachability answer. See §"Deferred" below.

The local form stays usable when the size cannot be read, and quotes the refusal
verbatim. LOCAL_INFERENCE_UNAVAILABLE is one of the four codes that opt out of the
opaque body precisely so the install command reaches a person; a sentence written in the
client would throw it away. Creating a connection downloads nothing, so an unknown size
is information rather than a gate.

No credential field, and its absence is the decision rather than an oversight — where
an HTTP connection's secret lives is still open on #421, and a placeholder would answer
it by publishing a shape.

D6's destination, and a defect wiring it found

One callback in routes.tsx. Wiring it surfaced a real bug shipped in #451: SuggestPanel
sat at bottom-2 right-2 and ZoomWidget at bottom-3 right-3, so the widget's subtree
intercepted the pointer and the panel's action could not be clicked at all. It was
invisible until now because the action had no destination and nothing ever clicked it. The
panel now clears the widget; the e2e that gave it a destination is what found it.

#424 — the closure check, position by position

Where Status
D1 SAM 2 as the suggested default model + adapter in #448; the form pre-fill and its download size here complete
D2 the gesture #451 complete
D3 geometry follows the schema #448 + #451 complete
D4 ephemeral preview, acceptance enters history #451, with the annotated entry ratified 2026-08-08 complete
D5 server-side inference through the connection #448 complete
D6 the no-connection panel #451; its action's destination here complete

Both of D6's clauses are covered: the panel's action lands on the Inference section, and
a local connection whose runtime is missing shows the install command in the same panel
(the suggest call's refusal, rendered as written). #424's own open-questions list adds
nothing further — its one reconciliation, D4's progress state, was ratified 2026-08-08.

Closes #424

Deferred, and recorded on #421 rather than stubbed

The per-row Test for HTTP connections. ConnectionAction is
download_weights | update | delete, there is no /inference/connections/{id}/test, and
nothing here declares one — a declared action with nothing behind it is an offer the
product cannot keep. It ships with the HTTP endpoint contract; the visible consequence is
the two-value status column above. Comment posted on #421.

#421 stays open: the credential question, and this.

Mutation verification

Three, each applied to the committed tree, anchor asserted present before and after,
each reverted by git apply -R on its recorded patch with a clean tree confirmed.

Mutation Tests that turned red
Download weights derived from setup_state instead of allowed_actions 1: inference.test.tsxdoes not offer Download weights when the wire withholds it
the size lookup takes a snapshot before reading the listing 12, led by test_download_size.pytest_measuring_a_size_downloads_nothing, and including the CLI's the size command reads a listing and downloads nothing
routes.tsx stops passing onConfigureInference 1: annotate.spec.tsthe no-connection panel now has somewhere to send you (#424 D6)

The two the dispatch asked to be named are the first two rows. The first mutation is the
one that matters most: the mutated row has the identical setup_state, so a screen
deriving the action from the row's own state still renders the button — only reading
allowed_actions gets it right.

Test plan

bash scripts/check.sh run in stages — the harness kills a command at ~10 minutes
and both tests/kernel and tests/server exceed it on this box — with pytest split by
the directories ls tests/ reported at run time. Every exit code verbatim.

Stage Exit
pytest tests/architecture 0
pytest tests/cli 0
pytest tests/examples 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/server 0
pytest tests/test_versioning.py 0
ruff check . 0
ruff format --check . 0
mypy src/visionset 0
lint-imports (4 contracts kept, 0 broken) 0
check.sh frontend (build, tests, lint) 0
check.sh generated (openapi, client, mcp reference, version sync) 0
check.sh browser (annotator + app e2e, then the real-server cycle) 0

tests/scripts is not a pytest target — it holds the node --test gates, which the
frontend and generated groups run.

Frontend totals: annotator 904 passed (34 files), ui-core 803 passed (45 files). Browser:
233 passed, no flakes, then the cycle suite 1 passed.

New coverage:

  • tests/inference/test_download_size.py — 14: metadata only, the pinned revision, every
    file counted, the two refusals, the kernel-vocabulary translation, and the cache's
    identity, bound and per-revision key.
  • tests/server/test_inference.py — 5 new: the route's body, that it names no
    connection, read-once, the missing half of the pair, and the unstubbed install command.
  • tests/cli/test_inference_commands.py — 4 new, invoked without --workspace, which
    is the assertion as much as the helper.
  • frontend/ui-core/src/screens/inference.test.tsx — 23: list states, the wire-declared
    actions, the create flow including the size and the missing-extra prose, the delete copy.
  • e2e/shell.spec.ts — the rail's link count is the assertion, so a fourth
    destination arriving without a decision fails there first; plus the entry's href,
    active state and absent back-link.
  • e2e/annotate.spec.ts — D6 end to end: arm the tool, get the panel, click through to
    the section.

Found, not fixed

Nothing outstanding — the one defect found (the panel under the zoom widget) is fixed
above rather than recorded, because this slice is what made it reachable.

cf. #58, #417, #418, #421, #425, #448, #451

…ing is fetched

D1 on #424 requires the local-connection form to show what a download would
cost before somebody confirms it, and nothing on the wire could say. The size
is read from the publishing hub's file listing rather than from the files: it
has to be answerable before the download it describes, so it cannot be a
by-product of one.

Every file in the revision counts, because the snapshot download fetches every
file in the revision. A file the listing does not size is refused rather than
skipped — a number smaller than the truth is worse than no number when its only
job is to inform a decision. Cached per model_id@revision, which is correct
rather than merely fast: a pinned revision is a fixed set of files, so the
answer cannot go stale and nothing has to invalidate it.

huggingface_hub stays inside a function, so the base import graph is unchanged
and tests/architecture/test_optional_runtime.py still holds. A machine without
the extra is refused here with the same install command the download gives.

cf. #421, #424, #418
The Inference section is a top-level rail destination, per the decision recorded
on #421 (2026-08-08) which supersedes #58's rail rule. Model connections carry
no project id — one workspace is one SQLite file and every project uses the same
connections — so a project tab would state a scope the object does not have.
DESIGN.md carries the new membership in both places it states the rule, and the
information-architecture skill carries the route, the supersession and the
back-link consequence.

Screens render what the wire declares and nothing else: Download weights comes
from allowed_actions, not from setup_state, and the action stays declared on a
machine without the local runtime so the install command has somewhere to be
shown. The local form pre-fills D1's suggested model and shows what fetching it
would cost before anything is confirmed; when the size cannot be read the form
stays usable and quotes the server's refusal verbatim.

The status column has two values rather than three: setup_state is deliberately
not a reachability answer, and the test action ships with the HTTP endpoint
contract (cf. #421).

#424's D6 panel finally has a destination — one callback in routes.tsx. Wiring
it found that the panel sat under ZoomWidget and could not be clicked at all;
both are bottom-right overlays and the panel now clears the widget.

cf. #58, #421, #424
@JArmandoAnaya
JArmandoAnaya merged commit 1e7ee85 into main Aug 9, 2026
13 checks passed
@JArmandoAnaya
JArmandoAnaya deleted the feat/inference-section branch August 9, 2026 00:30
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.

Interactive auto-labeling — click-to-suggest in the editor canvas

1 participant