Metadata-Version: 2.5
Name: kollabor-webui
Version: 0.18.5
Summary: Web UI for Kollab Engine
License-Expression: Apache-2.0
Requires-Python: >=3.14
Requires-Dist: fastapi>=0.115.0
Requires-Dist: kollabor-engine>=0.18.5
Requires-Dist: psutil>=5.9.0
Requires-Dist: uvicorn[standard]>=0.32.0
Description-Content-Type: text/markdown

# kollabor-webui

`kollabor-webui` is the browser UI shell for `kollabor-engine`.

It serves the assistant-ui single-page app, proxies engine configuration to the
browser, and connects to the local engine API for sessions, assistant-transport
streaming, permission prompts, profiles, MCP, and hub controls.

Visual direction and review rules: [Web UI design guide](../../design.md).

## Architecture

| Module/Asset | Responsibility |
|---|---|
| `src/kollabor_webui/__init__.py` | `kollabor-webui` command entrypoint and uvicorn launcher |
| `src/kollabor_webui/server.py` | FastAPI config endpoint, Vite assets, and SPA fallback |
| `frontend/src/` | assistant-ui React/TypeScript source |
| `frontend/dist/` | local Vite output served by source checkouts (ignored) |
| `src/kollabor_webui/static/` | legacy static fallback and packaged assistant-ui output |

The checked-in frontend source is the source of truth. `hatch_build.py` runs
`npm ci` and `npm run build` into an isolated temporary directory, then
force-includes those assets in a wheel under `kollabor_webui/static/assistant`.
The output stays available until Hatch finishes packaging. Missing npm or a
failed compilation fails the build; an old dist or legacy UI cannot silently
substitute. Source distributions contain frontend source, the lockfile and the
canonical changelog input, and compile only when their wheel is built. The
monorepo keeps using `kollabor/updates/CHANGELOG.md`; Hatch includes that same
file as `frontend/CHANGELOG.md` for standalone source builds. A source checkout
normally serves `frontend/dist` directly. When its source, public assets or
changelog input is newer, startup rebuilds it; missing dependencies or a failed
build stops startup instead of serving older output.

To package the local app and PhoneLink service from one UI build, compile once
into a fresh isolated directory with `npm --prefix packages/kollabor-webui/frontend run build -- --outDir /absolute/frontend-dist`
from the repository root. Pass that same directory to
`python scripts/build_binary.py --frontend-dist /absolute/frontend-dist` and
`python scripts/relay/build_service.py /absolute/service.tar.gz --frontend-dist /absolute/frontend-dist`.
Both builders reuse the selected assets without rebuilding them. The builders,
WebUI wheel hook and runtime also accept `KOLLAB_FRONTEND_DIST=/absolute/frontend-dist`.
At runtime this selection overrides both source and packaged assets and skips
source rebuilds, including in editable installs. An invalid selection stops startup.
An explicit `--frontend-dist` takes precedence over the environment input.
Keep these outputs outside the shared checkout's `frontend/dist`.

The release Binaries workflow compiles one `frontend-dist` artifact for all
native binaries and the `connection-service` source archive. Desktop stages
those same native binaries. PyPI publication downloads that frontend artifact
and supplies `KOLLAB_FRONTEND_DIST` to the WebUI wheel build, so these packages
share the same compiled UI. The connection-service artifact is deployment
input; producing it does not update a running public service.

## Interface

- **Sidebar**: New Session starts a session with the current picks in one
  click; the options button next to it opens Agent, Type, Model and Workspace.
  Agent is a persona with its picture, name and job title, grouped by roster:
  the chosen Workspace's roster first, then every other roster's personas
  (they start as `roster/name`). Picking one sets its Type, the agent bundle it
  runs. The list is the chosen Workspace's: every folder has its own hub, so an
  agent or koordinator running in another folder is free here and "In Use"
  means in use in that folder, whichever roster it came from (a folder inside
  a git repo shares the repo's hub). It starts with koordinator while the
  folder runs none (a terminal makes the first agent in a folder its
  koordinator) and says when each gem last ran there ("Ran 3d Ago", from its
  vault). Workspace is a folder picker:
  the engine's folder, recent folders with their live agents, a folder
  browser, or a typed path.
  Each session row shows its agent under its display name: a live 3D gem that
  acts out what the agent is doing (thinking, typing, searching, messaging,
  error), or the persona's picture, emoji or initials when its roster has no
  gems.
- **Agents**: the sidebar's Agents button lists every roster and team (bundled,
  yours and the chosen folder's own) with each persona's details and the files
  that failed to load. Launch Team starts a team into a folder, its lead first;
  a member already running there is skipped.
- **Agents by computer**: the sidebar lists every agent under the computer it
  runs on. This computer comes first: its chats and every agent running in a
  terminal or as a daemon, each one click from a full chat (the web UI only
  attaches to an agent it did not start, so it offers no Delete). Agents that
  ran in this folder and are not running follow as offline; a click starts one
  as itself, and its vault brings back what it knew. Then each network this
  computer is on (folders here can be on different ones), with its other
  computers. The engine keeps every agent each network has listed for a month
  (`~/.kollab/engine-network.json`): one that left shows offline with when it
  was last seen, and a network no agent here carries now shows its last
  roster. Opening one of its agents then first starts the koordinator of that
  network's folder to carry it. One click opens one as a full chat, through the network, as that
  computer's trust allows: under `open` (the default) every computer of your
  network, under `agents` only the agents it lets this one message, under
  `manual` none.

  Local live and offline rows group by workspace folder. Network agents stay
  under their network and device, then group by workspace. Drag an agent row to
  reorder it within that workspace, including separate sessions of the same agent.
  Rows move aside during mouse or touch dragging; drop to save. With the row focused,
  Space picks it up, arrow keys move it, Space drops it, and Escape cancels.
  Ordering cannot move an agent to a different workspace, device, or network.
  Drag more than 32 pixels past the sidebar's edge to see "Release to hide".
  Releasing there shrinks the row into the Hidden Agents button; moving back or pressing Escape
  cancels. This also works for the last row in a workspace. It uses the same
  recoverable Hide Agent action as the row menu: restore it from Hidden Agents.
  A rejected hide restores the row and shows the engine's error. Hiding does not
  delete a session or stop its agent. The footer button stays available even when
  the hidden list is empty. Reduced motion skips the travel animation.
  The engine stores
  this order in `~/.kollab/hub/sidebar-order.json`, shared across browser
  clients and durable across page, web UI, and engine restarts; it is not
  browser-local storage. Revision checks reject stale-window writes rather
  than overwrite newer moves. Polling cannot change the ranks during a drag;
  cancelled and unchanged drops do not write. Failed saves restore the confirmed
  order and show an error. If the stored JSON is unreadable, the engine
  preserves it and shows a recovery warning; a missing first-run file is normal.
  The Learn book icon in the sidebar footer opens topics about agent roles,
  project folders, and how the sidebar order is saved.
- **Device order**: drag a remote device header to move its whole group within
  that network. Its workspaces and agents travel together, with live feedback.
  Space lifts the focused header, arrow keys move it, Space drops it, and Escape
  cancels. Device ordering uses the same revisioned engine preference file as
  agent ordering and survives refresh. This Computer and Contacts remain fixed.
  A device rename has a new ordering identity because the network API identifies
  devices by name.
- **Touch drag**: press and hold a row, then drag. A lifted row is swapped for a
  clone in the page body, which unmounts the element the finger started on, and
  the browser keeps sending that touch to the unmounted element. `touch-relay.ts`
  hands those events to the window so a touch drag keeps moving, drops and
  cancels; without it the row lifts and the sidebar scrolls instead.
- **Hidden agents**: Hide Agent in a row's menu takes it off the sidebar in
  every window that uses this engine (Kollab.app, browsers, the phone); the
  footer's Hidden Agents list shows it again. The engine keeps the list
  (`~/.kollab/hub/hidden-agents.json`) and sends it with `GET /sessions` as
  `hidden`. A running agent is hidden as the process it is (its `instance`), so
  one launched later under the same name shows.
- **Multi-network (#158)**: when this device belongs to more than one network,
  the sidebar nests them — NETWORKS > DEVICES > WORKSPACES > AGENTS — with
  the devices and workspaces of every membership (`network.networks` on
  `GET /sessions`). A CONTACTS section lists peers this device accepted from
  another network's rooms (a knock); it is display-only: nothing is permitted
  with a contact until `/connect allow` says so. Engines without the
  aggregate render the flat single-network view exactly as before.
- **Chat**: pill composer with attachments, a `/voice` mic, and a
  send/stop button; tool calls show while they run and keep their duration.
  Messages from other agents show as messages from their gems ("Aquamarine →
  Lapis"), the way the terminal draws its hub boxes; one the agent only
  overheard is dimmed. Turns the page did not run (a hub message that woke the
  agent, a turn typed in the terminal) appear without a reload. A failed turn
  shows its error under your message, also after a reload.
- **Trajectory**: every turn, request and tool call as a Table, or as a
  Waterfall built on `@assistant-ui/react-o11y` (one block per turn, tools
  under their model request, each turn on its own time scale).
- **Settings**: daemon-owned panels, see `docs/features/web-settings-panels.md`.
- **Learn**: the book icon in the sidebar footer opens a responsive, ten-topic
  guide at `/learn`; deep links use `/learn?topic=<id>`, and lesson completion is
  stored in this browser.
  What's New comes from the canonical changelog. See
  [`docs/features/learn.md`](../../docs/features/learn.md).
- **Gem Studio** (sidebar footer): every agent is born with a random look (eyes
  and hat) that sticks; dress any gem over it (eyes, hat, color) and set the
  season for all, previewed live. Random Look rolls a new outfit, Reset goes
  back to the born look. The engine keeps the looks, so every browser and every
  gem in the app (session rows, chat, Who Is Online) follows. Each agent has its
  own look: the studio's list dresses the gems of the folder the web UI runs in,
  and an agent of the same name in another folder or on another computer is born
  with its own eyes, hat and color (dress it from its chat's Properties).
- **Session Properties**: right-click a session row (long-press on a phone) or
  double-click its gem to dress that one gem in place. The Chat tab takes the
  agent off the hub (your other agents stop seeing it; your chat keeps working)
  or puts it back, with no restart. `/hub leave` and `/hub join` do the same in
  the terminal.

## Usage

Quickest path — `kollab --web-ui` spawns the engine and this package together
(reusing an already-running engine on 7433 if one is healthy) and cleans both
up on Ctrl+C or SIGTERM:

```bash
kollab --web-ui
```

To run the two pieces separately (useful when iterating on one of them):

Start the engine:

```bash
python -m kollabor_engine serve --host 127.0.0.1 --port 7433
```

Start the web UI:

```bash
KOLLAB_ENGINE_URL=http://127.0.0.1:7433 \
KOLLAB_WEBUI_PORT=8080 \
kollabor-webui
```

Then open:

```text
http://127.0.0.1:8080
```

To open it from another device, such as a phone on your VPN, list the extra
addresses in `KOLLAB_WEBUI_HOSTS` (comma-separated; it also works with
`kollab --web-ui`). Pages opened there reach the engine through the web UI's
`/engine` proxy, and they get the engine token, so list private addresses only:

```bash
KOLLAB_WEBUI_HOSTS=10.8.0.1 kollab --web-ui
```

For an editable checkout without installed console scripts:

```bash
python -c "from kollabor_webui import main; main()"
```

## PhoneLink

**Settings > PhoneLink** pairs a phone browser through the connection service
already used by `/connect`. Scan the expiring QR with the phone camera, compare
the pairing codes, and approve locally. Saved phones reconnect and can be
removed independently. Provider calls and tools run on the computer; the phone
needs no credential setup. Approve only phones you trust with the app's controls.

The directory serves the same built frontend at `/relay/v1/phone/` and accepts
same-origin browser registrations at `/relay/v1/phone/ws`. Each phone has an
isolated private room; the engine's own ASGI application handles encrypted app
requests and streamed responses. The engine token never leaves the computer.
Native `/relay/v1/ws` continues to reject browser Origin headers. The directory
capability endpoint must confirm support before a pairing QR can be created.

Phone chats open with the newest 50 messages, fetch older pages as you scroll
back, and keep a bounded IndexedDB cache so reopening requests only new messages.
Large historical tool results are fetched only after Show more.


See [`docs/features/phone-access.md`](../../docs/features/phone-access.md) for
the contract and its deployment/physical-phone verification gates.

## Frontend development

```bash
cd packages/kollabor-webui/frontend
npm ci
npm run typecheck
npm run build
npm run dev
```

Vite proxies `/api`, `/sessions`, `/profiles`, `/mcp`, and `/hub` to the local
engine while developing.  The production build uses relative asset paths, so
FastAPI can serve hashed assets from any mount path and route client-side
navigation back to `index.html`.

The app is not wrapped in `<StrictMode>`: its dev-only double render breaks
`@assistant-ui/store` 0.3.2 and the composer drops typed text under `npm run
dev` (production is unaffected). Restore it with the assistant-ui 0.15.25
upgrade. `src/dev/gem-lab.html` previews every gem, activity, hat and season.

## Engine endpoints used

- `POST /sessions`
- `GET /sessions` (with `offline`, agents that ran in the engine's folder, and `network`)
- `DELETE /sessions/{session_id}`
- `POST /sessions/{session_id}/assistant`
- `POST /sessions/{session_id}/cancel` (Stop; a closed assistant stream never cancels the turn)
- `POST /sessions/{session_id}/permission`
- `GET /sessions/{session_id}/permissions`
- `POST /sessions/{session_id}/permissions/mode`
- `GET/DELETE /sessions/{session_id}/history` (with `last_turn_error`, why the last turn failed)
- `GET/POST/PUT/DELETE /profiles...`
- `GET/POST/PUT/DELETE /mcp/servers...`
- `GET/POST /sessions/{session_id}/mcp...`
- `GET /agents` (gem pool: colors, live state, hub agent ids, `last_ran`)
- `GET /workspaces`, `GET /workspaces/browse?path=` (New Session's folder picker)
- `GET/PUT /agents/appearance` (Gem Studio looks)
- `GET /hub/agents`
- `POST /hub/messages`

Session rows keep `session_id` as the API key and expose a separate `name` for
display. Kollab-generated timestamped names display as their slug; opaque UUIDs
receive a stable two-word label.

## Authentication

The browser gets the current engine bearer token from `/api/config`, which
reads `~/.kollab/engine.token`.  Every request waits for that first config
load, and retries once after a 401 by refreshing it, covering engine restarts
that rotate the token.

## Validation

```bash
python -m py_compile packages/kollabor-webui/src/kollabor_webui/*.py
python -m pytest tests/unit/test_webui_auth_wiring.py -q
cd packages/kollabor-webui/frontend
npm run typecheck
npm run build
node --test tests/*.test.ts
```

## Learn browser acceptance

The retained `frontend/tests/learn-browser-acceptance.mjs` harness starts a local
Vite server and intercepts all engine API requests; it never starts or contacts a
live engine, router, or user configuration. Install a browser once with
`npx playwright-core install chromium`, or set `CHROME_PATH` to an existing
Chrome/Chromium executable, then run `npm run test:e2e:learn` from `frontend/`.
The harness checks startup, Hub/network listing, profile selection and agent
session launch, Learn navigation/focus, the separate What’s New page, changelog
failure/retry, and responsive layouts. It writes the three viewport screenshots
to the repository's `docs/screenshots/`; set `LEARN_CAPTURE_SCREENSHOTS=0` to
run the checks without rewriting images.


## License

Apache-2.0
