Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ jobs:

# The config's own webServer builds the engine, the design system and the
# bundle, copies it into the package data, creates a throwaway workspace and
# starts `visionset ui`. One command, so a failure in any of those steps is
# starts `visionset server`. One command, so a failure in any of those steps is
# reported at this step rather than inside a server log.
- name: Drive the whole cycle
run: pnpm --filter @visionset/app cycle
Expand Down
13 changes: 12 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,18 @@ nothing was being distributed. This is the first version that is.

### Changed

- **The browser application moved from `/ui` to `/app`.** `visionset ui` now serves it at
- **`visionset ui` is now `visionset server`** (#329). The command starts the FastAPI server; the
browser application is one client of it, alongside REST, the SDK and MCP, so the old name
described a single consumer of the process rather than the process. Nothing else moved — same
flags and defaults, same `/app` mount, same one-sentence refusal at exit 1 outside a workspace.

**There is no alias.** `visionset ui` now fails with Typer's ordinary `No such command 'ui'`,
so a script that calls the old name needs one edit. A deprecation shim was declined
deliberately rather than overlooked: this is a pre-1.0 beta with no installed base to carry,
and a hidden second spelling of the front door is a thing every later reader has to explain
away.

- **The browser application moved from `/ui` to `/app`.** `visionset server` now serves it at
`http://127.0.0.1:8000/app/`, and `/` still redirects there. One constant moved —
`UI_PREFIX` in `src/visionset/server/main.py` — and `frontend/app/vite.config.ts`'s build
`base` follows it, which is what keeps the router's basename in agreement: it is read from
Expand Down
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ a deliberate manual run, because each costs minutes or needs its own install.
| Frontend lint | `pnpm -r lint` — **after** a build: `frontend/app` resolves `@visionset/annotator` through its `dist/`, so its typecheck has no declarations until the engine is built | `frontend` |
| Annotator headless boundary | `pnpm --filter @visionset/annotator lint` | part of `frontend` (`pnpm -r lint`) |
| Annotator end-to-end (chromium) | `pnpm --filter @visionset/app e2e` (needs `playwright install chromium` once) | `browser` |
| Browser cycle (chromium) | `pnpm --filter @visionset/app cycle` — the whole product against a real `visionset ui`; needs `uv sync` and `playwright install chromium`. Repeatable in one workspace: `--repeat-each=N` costs one build rather than N | `browser` |
| Browser cycle (chromium) | `pnpm --filter @visionset/app cycle` — the whole product against a real `visionset server`; needs `uv sync` and `playwright install chromium`. Repeatable in one workspace: `--repeat-each=N` costs one build rather than N | `browser` |
| Annotator benchmark (manual) | `pnpm --filter @visionset/app bench` — frame times, recorded not gated | — manual |
| Browser client | part of `pnpm test` — `ui-core`'s `data/` suite drives the 401 flow, the token form and the error envelope with a stubbed `fetch`, no server | part of `frontend` |
| Design tokens | part of `pnpm test` — `tests/scripts/design_tokens.test.mjs` refuses a colour inside a class name, and `ui-core`'s `tokens.test.ts` gates the stylesheet against its TypeScript mirror | part of `frontend` |
Expand Down Expand Up @@ -149,7 +149,7 @@ VISIONSET_REQUIRE_WHEEL=1 uv run pytest tests/packaging
**The order in that script is the whole point.** `uv build` copies
`src/visionset/_static/` as package data *at the moment it runs*, and a fresh checkout's
`_static/` holds only `README.md` and `.gitkeep` — so a wheel built before
`pnpm bundle:static` contains **no app at all**. It installs, `visionset ui` starts, and
`pnpm bundle:static` contains **no app at all**. It installs, `visionset server` starts, and
`/app/` answers a 404 naming a script the user does not have. There is no error and no
traceback anywhere in that sequence, which is why the script checks after each step and
why `tests/dist/` checks the artifact rather than the source tree.
Expand Down
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,15 @@ uv tool install "git+https://github.com/Robomous/VisionSet" # PyPI lands with

visionset init ~/datasets/road-signs # a workspace, here and nowhere else
cd ~/datasets/road-signs
visionset ui # API at http://127.0.0.1:8000, app at /app
visionset server # API at http://127.0.0.1:8000, app at /app
```

Then follow [the tutorial](docs/tutorial.md): a clip of video to a YOLO dataset in about half an
hour. Full prerequisites — Python 3.12, and ffmpeg only if you are starting from video — are in
[docs/install.md](docs/install.md).

`init` is the only command that creates a workspace, and it refuses a directory that already holds
something. `visionset ui` run outside one refuses with one sentence and exit 1; it never creates
something. `visionset server` run outside one refuses with one sentence and exit 1; it never creates
one, because a command that silently made a workspace out of whatever directory you were standing
in is how data ends up somewhere nobody chose.

Expand Down Expand Up @@ -93,9 +93,9 @@ into 50 deduplicated assets in an approved batch, then shows a re-run creating n
ffmpeg.

The same cycle runs over each of the other two surfaces, and both start the shipped command for
real: [`examples/http_end_to_end.py`](examples/http_end_to_end.py) starts `visionset ui` on a free
port and drives the API with `urllib` and a bearer token — multipart upload, 202-and-poll ingest,
hash-checked manifest and a 401 it asserts — while
real: [`examples/http_end_to_end.py`](examples/http_end_to_end.py) starts `visionset server` on a
free port and drives the API with `urllib` and a bearer token — multipart upload, 202-and-poll
ingest, hash-checked manifest and a 401 it asserts — while
[`examples/mcp_end_to_end.py`](examples/mcp_end_to_end.py) spawns `visionset mcp` and talks
JSON-RPC down its pipe, scaling every box out of the preview it saw and into the asset's own
pixels.
Expand Down Expand Up @@ -136,7 +136,7 @@ uv sync # Python env + dev tools
pnpm install # frontend workspace
```

Then `uv run visionset ui` and `pnpm --filter @visionset/app dev`. Or run the whole thing in
Then `uv run visionset server` and `pnpm --filter @visionset/app dev`. Or run the whole thing in
containers instead, with nothing installed on the host and nothing built.

### Run it with Docker, and sign in with nothing
Expand Down
2 changes: 1 addition & 1 deletion docker/api-dev.sh
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ fi
# `--reload-dir` is not tidiness. uvicorn's default watch list is the working
# directory, which here is the whole bind-mounted repository: `node_modules/` and
# `workspace-data/` among the rest — so every SQLite write during an ingest would
# restart the server mid-run. `visionset ui` scopes its own `reload_dirs` to the
# restart the server mid-run. `visionset server` scopes its own `reload_dirs` to the
# package for exactly this reason; raw uvicorn does not inherit that, so the scope
# is stated here instead.
exec uvicorn visionset.server.main:app \
Expand Down
2 changes: 1 addition & 1 deletion docker/compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ services:
# port open to every interface would hand the workspace to the LAN. The two
# lines belong together and neither is safe alone.
VISIONSET_UI_SESSION: always
# Raw uvicorn rather than `visionset ui`, and deliberately so: that
# Raw uvicorn rather than `visionset server`, and deliberately so: that
# command also serves the compiled bundle at /app, which in development is
# vite's job on :5173 — and building that bundle is exactly what this stack
# exists to avoid needing.
Expand Down
2 changes: 1 addition & 1 deletion docker/nginx.conf
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# The dev stack's one entry point. Everything the browser asks for arrives here.
#
# Dev only. In production there is no proxy and no second origin: the wheel's
# `visionset ui` serves the API at the root and the compiled bundle at /app, from
# `visionset server` serves the API at the root and the compiled bundle at /app, from
# one uvicorn process. Nothing in this file describes how VisionSet is deployed.

events {}
Expand Down
2 changes: 1 addition & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,4 @@ rather than here — it is the file to read *before* building any screen, and th
| [mcp-walkthrough.md](mcp-walkthrough.md) | A session over MCP, start to finish: the cycle in the order an agent meets it, and then what twelve real agent runs did with it — where the coordinate frame held, how refusals were read, and the two pieces of friction that changed the tools |
| [ui.md](ui.md) | The browser client: why no screen calls `fetch`, how a refusal is read (branch on `code`, and the two codes the client adds), where the token is kept and the three alternatives that were rejected, why a 401 is handled in one subscription, the loading/empty/error component, polling, and the dev proxy that keeps CORS out of production |
| [releasing.md](releasing.md) | Cutting a release: what ships, why the beta goes to PyPI as a pre-release, the npm scope, the order the steps have to happen in, and the one step that needs credentials this repository does not hold |
| [cli.md](cli.md) | The command line: the whole cycle as a script, the three exit codes (and why one of them also means "no"), why stdout is data and stderr is prose, what `--json` promises and how it stays the API's shape, why `--workspace` follows the subcommand, and what `visionset init` and `visionset ui` each do |
| [cli.md](cli.md) | The command line: the whole cycle as a script, the three exit codes (and why one of them also means "no"), why stdout is data and stderr is prose, what `--json` promises and how it stays the API's shape, why `--workspace` follows the subcommand, and what `visionset init` and `visionset server` each do |
13 changes: 6 additions & 7 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,10 @@ endpoint — what a failure looks like, and how to read one.
The routes themselves are described by [`openapi.json`](../openapi.json) at the repo root, which
is generated (`uv run python scripts/export_openapi.py`) and diffed in CI. Never hand-edit it.

For a worked external client, [`examples/http_end_to_end.py`](../examples/http_end_to_end.py)
starts `visionset ui` on a free port and drives the whole cycle — upload, launch-and-poll ingest,
annotate, promote, publish, verify, export — with `urllib` and a bearer token, and nothing else.
It is deliberately dependency-free: a contract only a smart client can drive is not really a
contract.
For a worked external client, [`examples/http_end_to_end.py`](../examples/http_end_to_end.py) starts
`visionset server` on a free port and drives the whole cycle — upload, launch-and-poll ingest,
annotate, promote, publish, verify, export — with `urllib` and a bearer token, and nothing else. It
is deliberately dependency-free: a contract only a smart client can drive is not really a contract.

## Authentication

Expand Down Expand Up @@ -293,8 +292,8 @@ because somebody added it to an entity.

## Where the UI lives

The compiled application is mounted at **`/app`** and `/` redirects to it. `visionset ui` starts
both halves with one command; see [cli.md](cli.md#visionset-ui).
The compiled application is mounted at **`/app`** and `/` redirects to it. `visionset server` starts
both halves with one command; see [cli.md](cli.md#visionset-server).

**The API owns the root, and that is why the app does not.** `/projects/{project_id}` is a shipped
route, so an application served from `/` could never claim `/projects/abc` as one of its *own*
Expand Down
9 changes: 5 additions & 4 deletions docs/auth.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,8 @@ browser -> GET /session
Set-Cookie: visionset_session=…; HttpOnly; SameSite=Strict; Path=/
```

`visionset ui`, then a browser, and you are on the project list. Nothing typed and nothing copied.
`visionset server`, then a browser, and you are on the project list. Nothing typed and nothing
copied.

**The token is untouched.** `Authorization: Bearer` still authenticates every route, the SDK, the
CLI, MCP and every third-party client are unaffected, and `AuthProvider.verify(token) -> bool` did
Expand Down Expand Up @@ -224,10 +225,10 @@ branches it can only reach two: **`VISIONSET_WORKSPACE`**, then the nearest work
the working directory. The precedence table and the argument for why only that last case walks
upward live in [workspaces.md](workspaces.md#which-workspace-when-nobody-said).

A server started by **`visionset ui`** always lands on the first of those. That command applies the
full precedence itself — including `--workspace`, which no server can see — and then exports the
A server started by **`visionset server`** always lands on the first of those. That command applies
the full precedence itself — including `--workspace`, which no server can see — and then exports the
answer, so the two resolvers cannot disagree about a workspace one of them was told about and the
other was not. See [cli.md](cli.md#visionset-ui).
other was not. See [cli.md](cli.md#visionset-server).

It is opened by the first request that needs it and kept for the life of the process — never at
import time, because `scripts/export_openapi.py` imports the application in a checkout that has no
Expand Down
20 changes: 10 additions & 10 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ visionset backfill-thumbnails --project P
visionset token create --name NAME
visionset token list
visionset token revoke NAME [--yes]
visionset ui [--host] [--port] [--reload] # no --json
visionset server [--host] [--port] [--reload] # no --json
visionset mcp # stdio; no --json
```

Expand Down Expand Up @@ -84,7 +84,7 @@ actually do. It does not rewrite the kernel's sentence: bending a domain message
is how the other surfaces end up with the wrong wording.

```
$ visionset ui
$ visionset server
Error: /tmp is not a VisionSet workspace (no visionset.db); use WorkspaceService.init to create one
Point at one with --workspace, or set VISIONSET_WORKSPACE.
$ echo $?
Expand Down Expand Up @@ -207,7 +207,7 @@ Creates a workspace, which every other command needs and none of them makes.
$ visionset init ./datasets/robots
Created workspace 'robots' at /home/you/datasets/robots.
/home/you/datasets/robots
Next: visionset token create --name <name>, then visionset ui.
Next: visionset token create --name <name>, then visionset server.
```

The root is the only thing on stdout, so `WS=$(visionset init ./robots)` is exactly the path — and
Expand All @@ -221,18 +221,18 @@ it is the *resolved* path, which is the useful answer when you typed `.`.
Creating one where a workspace already sits is refused too — the remedy is to use it, not to make a
second. Both refusals are one sentence at exit 1.

Deliberately **not** folded into `visionset ui`, which would have to create a workspace when the
Deliberately **not** folded into `visionset server`, which would have to create a workspace when the
directory looked empty: that breaks "`init` creates, `open` never does" and means a mistyped path
silently becomes a new empty workspace instead of an error. See
[workspaces.md](workspaces.md#at-a-terminal).

## `visionset ui`
## `visionset server`

Starts the server against the resolved workspace, serving the REST API at the root and the compiled
UI bundle at `/app`; `/` redirects to the app.

```
$ visionset ui
$ visionset server
VisionSet 0.0.1.dev0
workspace /home/you/datasets/robots
UI and API http://127.0.0.1:8000/
Expand Down Expand Up @@ -309,7 +309,7 @@ content addressing means it also creates no asset it created before, which is th
interrupted run. The batch id goes to stdout.

`--fps` is video-only and a usage error on a folder. The run is **synchronous**, and there is no
`--resume`: polling needs a second process, which is what `visionset ui` and
`--resume`: polling needs a second process, which is what `visionset server` and
`GET /ingest-jobs/{id}` are for. See [ingest.md](ingest.md#at-a-terminal).

### `visionset batch`
Expand Down Expand Up @@ -393,12 +393,12 @@ looks like.
visionset mcp [--workspace PATH]
```

Normally a client spawns it rather than a person running it. Like `ui`, it resolves the workspace
Normally a client spawns it rather than a person running it. Like `server`, it resolves the workspace
with the full precedence and then **states** the answer in `VISIONSET_WORKSPACE`, so the server it
starts cannot disagree with it, and it opens the workspace first so that `NotAWorkspace` is one
sentence at exit 1 rather than a refusal inside the agent's first tool call.

The target is named as a module for a subprocess rather than imported, for the reason `ui` names
The target is named as a module for a subprocess rather than imported, for the reason `server` names
uvicorn's app by import string — import-linter forbids `visionset.cli` importing `visionset.mcp`.
The subprocess inherits stdin and stdout, because those two streams *are* the transport, which is
also why this is the one command that prints **nothing at all** on stdout: a stray line would
Expand Down Expand Up @@ -445,7 +445,7 @@ repository; each module declares its own fixtures, including an autouse one that
`VISIONSET_WORKSPACE` so a developer with it exported gets CI's results. **Use
`monkeypatch.setenv(VAR, "")` if any command the module exercises can write `os.environ`, and
`delenv(VAR, raising=False)` otherwise** — `delenv` records no undo when the variable was already
absent, so a written one leaks into every later module. Only `ui` writes it today.
absent, so a written one leaks into every later module. Only `server` writes it today.

**A test module's basename must be unique across the whole suite.** With no `__init__.py` anywhere,
pytest imports a test module under its bare basename, so `tests/cli/test_batches.py` beside
Expand Down
6 changes: 3 additions & 3 deletions docs/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ real dependency since #16, so a second hand-rolled PNG encoder beside it would b
| Stage | What happens |
| --- | --- |
| Setup | `WorkspaceService.init` then `TokenService.create` — the last SDK lines in the file |
| Serve | `visionset ui --host 127.0.0.1 --port <free> --workspace <root>` as a subprocess, polled at `/health` until it answers |
| Serve | `visionset server --host 127.0.0.1 --port <free> --workspace <root>` as a subprocess, polled at `/health` until it answers |
| Project | `POST /projects`, `POST /projects/{p}/schema/versions` |
| Upload | `POST /projects/{p}/sources/images` — four PNGs as `multipart/form-data`, built by hand |
| Ingest | `POST /sources/{s}/ingest-jobs` → **202** + `Location`, then `GET /ingest-jobs/{id}` until it settles |
Expand All @@ -237,7 +237,7 @@ nothing else — not `httpx`, not `requests`, not `curl`. That is two arguments
contract only a smart client can drive is not really a contract. The multipart body is twenty-odd
lines in the file, written out rather than delegated, and it is the price of the claim.

**The server actually starts.** [`tests/cli/test_ui.py`](../tests/cli/test_ui.py) patches
**The server actually starts.** [`tests/cli/test_server.py`](../tests/cli/test_server.py) patches
`uvicorn.run` and asserts the arguments, which is right for a unit test and says nothing about
whether the process comes up. This example binds an ephemeral port, spawns the shipped command
against it, and waits for `/health` — the one unauthenticated route, and therefore the readiness
Expand Down Expand Up @@ -342,7 +342,7 @@ travels the way it really travels: resolved by the CLI, opened once to run any m
**One session for the whole walk.** `tests/mcp/_flow.py` opens a fresh session per call, which is
convenient for a test and is not what a client does. Holding one open is safe because the server
opens and closes the workspace *inside each tool call* — so a long-lived session holds no SQLite
handle and locks nobody out of `visionset ui`.
handle and locks nobody out of `visionset server`.

**The pixels an agent sees are not the frame its coordinates live in.** The frames are 640×480 on
purpose. The preview is capped at 256 pixels on its long edge, so what arrives is 256×192 and
Expand Down
Loading
Loading