feat(app): Inference section, rail entry, download-size surface (#421 slice 2) - #454
Merged
Conversation
…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
This was referenced Aug 9, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 whatunblocked 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
Inferencejoins the rail between Projects and the account control. The closed-railrule lives in
DESIGN.md, twice, and both citations move — the skill's sitemapenumerates 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-architecturegains the route, the supersession, and one consequence worthwriting 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.inferencetherefore exists as the addressother 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:
downloadfetches every file in therevision. A figure counting only
.safetensorswould understate what lands on thedisk 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.
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.
model_id@revision, and nothing invalidates it — correct rather thanmerely 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_hubstays inside a function, so the base import graph is untouched andtests/architecture/test_optional_runtime.pystill holds. A machine without the extra isrefused 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 aterminal as
visionset inference size— the one command in that file which opens noworkspace, and which therefore carries
domain_errorsitself rather than inheriting itfrom
opened_workspace. No MCP tool, matching the existing surface: MCP has no inferencetools at all.
The Connections screens (
ui-core)List, empty state, two-step create, edit, delete, and the
download_weightsaction withits background job.
inferenceQueries.tsmoved fromannotator/todata/— twosurfaces 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 weightsis rendered fromallowed_actionsand 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, becausesetup_stateisdeliberately 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_UNAVAILABLEis one of the four codes that opt out of theopaque 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:SuggestPanelsat at
bottom-2 right-2andZoomWidgetatbottom-3 right-3, so the widget's subtreeintercepted 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
annotatedentry ratified 2026-08-08Both 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.
ConnectionActionisdownload_weights | update | delete, there is no/inference/connections/{id}/test, andnothing 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 -Ron its recorded patch with a clean tree confirmed.Download weightsderived fromsetup_stateinstead ofallowed_actionsinference.test.tsx› does not offer Download weights when the wire withholds ittest_download_size.py› test_measuring_a_size_downloads_nothing, and including the CLI's the size command reads a listing and downloads nothingroutes.tsxstops passingonConfigureInferenceannotate.spec.ts› the 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 screenderiving the action from the row's own state still renders the button — only reading
allowed_actionsgets it right.Test plan
bash scripts/check.shrun in stages — the harness kills a command at ~10 minutesand both
tests/kernelandtests/serverexceed it on this box — with pytest split bythe directories
ls tests/reported at run time. Every exit code verbatim.pytest tests/architecturepytest tests/clipytest tests/examplespytest tests/formatspytest tests/inferencepytest tests/jobspytest tests/kernelpytest tests/mcppytest tests/packagingpytest tests/serverpytest tests/test_versioning.pyruff check .ruff format --check .mypy src/visionsetlint-imports(4 contracts kept, 0 broken)check.sh frontend(build, tests, lint)check.sh generated(openapi, client, mcp reference, version sync)check.sh browser(annotator + app e2e, then the real-server cycle)tests/scriptsis not a pytest target — it holds thenode --testgates, which thefrontendandgeneratedgroups 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, everyfile 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 noconnection, read-once, the missing half of the pair, and the unstubbed install command.
tests/cli/test_inference_commands.py— 4 new, invoked without--workspace, whichis the assertion as much as the helper.
frontend/ui-core/src/screens/inference.test.tsx— 23: list states, the wire-declaredactions, 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 fourthdestination 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 tothe 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