xuns · Web service¶
Use the same agent in a browser: chat interface, event stream, collapsible tool-activity blocks, file panel, and multi-session switching with renaming.
xuns . # all sessions share the current directory
xuns # one temporary working directory per session
xuns . --host 0.0.0.0 --port 18960 --base-path /xun
xuns --no-initial-agent . # no agent at all, only for browsing /docs/
Parameters¶
| Parameter | Form | Default | Description |
|---|---|---|---|
workdir |
positional, optional | empty | working directory shared by all sessions; when omitted or empty, each session gets its own temporary directory (cleaned up on exit) |
--host |
string | localhost |
listen address; with 0.0.0.0 the startup message additionally prints the aka localhost form |
--port |
integer | 18960 |
port; 0 means the system assigns a random port, and the actual port is printed |
--token |
string | empty | access token; an empty value is auto-generated with secrets.token_urlsafe(24) |
--base-path |
string | empty | URL prefix for sub-path deployments; must not contain empty segments, . or .. |
--manage-sessions / --no-manage-sessions |
boolean flag | on | whether sessions may be created/deleted/renamed in the interface |
--initial-agent / --no-initial-agent |
boolean flag | on | whether an initial agent is created at startup. When off, the process only serves the static UI and the /docs/ site, so the manual can be browsed without configuring an LLM; as long as --manage-sessions stays on, the UI can still create sessions, and creating one builds an agent, which needs a usable LLM configuration |
The corresponding Python API:
from xun import web_session
web_session(
workdir=".", host="localhost", port=18960, token="",
base_path="", manage_sessions=True, initial_agent=True,
)
Once started it prints a direct URL for each session (the token is already included in the query string):
Agents are available at the following URLs:
http://localhost:18960/chat/?session=%2F&token=...
Tokens and login¶
--token is the access credential; the server compares it with hmac.compare_digest and accepts three ways of supplying it:
Authorization: Bearer <token>- the
xun_web_tokencookie (written by the login page, or when you visit/chat/with?token=;HttpOnly+SameSite=Strict, plusSecureover HTTPS) - the
?token=<token>query string — honoured only on the/chatpage: on a match the server sets the cookie and answers with a 303 redirect to the same URL minus thetokenparameter, so API and WebSocket requests cannot authenticate through the query string
An unauthenticated /chat page request is answered with a 303 redirect to the login page at --base-path/login; every other unauthenticated request (static assets and API) returns 401 {"detail":"Not authenticated"}; unauthenticated WebSockets are closed with 1008. The documentation site /docs/ needs no login, so the manual stays easy to browse.
Route overview¶
All paths below carry the --base-path prefix (empty when it is not set).
| Path | Description |
|---|---|
/ |
redirects to /chat/ |
/chat/ |
the front-end interface (static assets, built from web/) |
/docs/ |
bilingual Chinese/English documentation site (packaged in src/xun/assets/docs), public access |
/login |
login page; ?next= only accepts in-site addresses pointing to /chat, anything else falls back to /chat/ |
/api/sessions |
session list, creation, removal and renaming (POST /api/sessions/create, /remove, /rename) |
/session/, /session/<uuid>/ |
the backend display instance of each session (WebSocket /ws, /api/events, /api/prompts, /api/files/...) |
/session/<path>/srv/<key>/ |
temporary static directory hosting, see below |
Session state is reported by the list endpoint as idle / running / waiting (waiting = a prompt is awaiting an answer). The last remaining session cannot be deleted; with --manage-sessions turned off, the create/remove/rename endpoints return 405. Session names can be edited in the sidebar (at most 80 characters after trimming, an empty name answers 422); renaming only changes what the interface shows, never the mount path.
How the event stream is presented¶
User messages stand on their own instead of being folded into a turn; tool calls and results collapse into activity blocks, and only a single-agent block skips the inner nesting — multi-agent sessions still unfold one level at a time. The newest activity block carries an activity preview under its header: at most 3 lines, each holding the first 160 characters of an event, with newer previews pushing older ones out; a running indicator shows whenever any agent in that block is working.
Message channel and reconnection¶
Every message / command the front end sends carries a client-generated client_id (a UUID):
- the server replies with an
acceptedacknowledgement only after taking the message in, and the front end clears the input box and its attachments once that acknowledgement arrives; until then the text stays on screen, so a message that was sent but never applied is never invisible. - if the socket drops, the unacknowledged submission is resent automatically after reconnecting, and the server deduplicates by
client_id(the most recent 2000 are remembered), so one message cannot run twice. - messages without a
client_id(for instance from a client you wrote yourself) are processed as before, just without acknowledgement or deduplication.
See Display & web service for the exact fields and endpoints.
Every session registers the complete set of built-in commands, including /save, /load, /render and /handoff — the commands that read and write the workspace — and they are equally available in the / input box.
File panel¶
Sessions started by xuns enable expose_files, which provides:
- directory browsing and preview: images, PDF (including full screen), text with syntax highlighting; Markdown / HTML is always previewed as its rendered output, a renderable file opens straight into the preview, and the "Render" button is listed before "Source".
- text editing: the preview can be edited and saved in place, and new text files can be created; saving carries the
modified_atvalue from when the file was opened for conflict detection, so a file already changed elsewhere is refused rather than silently overwritten. - inline folder creation (several levels at once), rename/move, delete, and directory upload (the result notification lists every file and then disappears automatically; a file with an existing name is overwritten outright).
- sizes in the file details are shown with a matching magnitude (B / KiB / MiB …, at most one decimal), together with
is_text.
All file operations are restricted to the agent's working directory; anything outside it returns 400, and symbolic links are skipped. The settings menu in the sidebar shows the version and commit the front end was built from, so you can confirm which build of the UI you are looking at.
Serving static directories /srv/¶
If the working directory ends up containing a generated static site (for example this project's site/), you can mount it in the interface as GET /session/<path>/srv/<key>/:
keyis a random token, the link itself is the credential (this path needs no token and only allowsGET/HEAD)- it expires after 1 hour by default (
SERVE_TTL_SECONDS = 3600), and at most 16 may be served at a time (exceeding the limit returns 429) - passing an empty path serves the working directory root itself
Deployment notes¶
- The interface and the backend communicate over WebSocket, so a reverse proxy must allow upgrade requests and preserve
X-Forwarded-Host/X-Forwarded-Proto. - Sub-path deployment only needs
--base-path; all assets and language-switcher links are relative paths, so any prefix works. --host 0.0.0.0exposes the service on every network interface — combine it with a firewall or reverse-proxy authentication.
Front-end development: npm run dev uses concurrently to bring up the backend (xuns, needs uv) and Vite (Vue DevTools) in parallel; use npm run dev:ui if you only want the UI.
cd web && npm install && npm run dev # UI http://127.0.0.1:5173