Web & Desktop (settings, multi-user, deep links, updater)

Overview

eVi ships the same assistant in three skins: a Typer CLI, a web UI (FastAPI + Server-Sent Events), and a Tauri desktop app that wraps that web UI in a native window with a menu bar, system tray, and a signed auto-updater. This page covers the cross-cutting "shell" concerns of the web/desktop surface:

You'd use the web UI when you want a richer chat surface than the terminal (tabs, settings screen, drag-drop uploads, dispatch dashboard) while keeping everything local. You'd use the desktop app for a one-click, always-warm assistant that survives window-close (it hides to the tray) and keeps itself up to date. Everything is local-first: the desktop app spawns the Python server on 127.0.0.1 and there is no cloud component.

How it works

The web server

evi web (or python -m uvicorn evi.apps.web.server:app) serves a single-page app from evi/apps/web/static/index.html. The browser POSTs to /api/chat with {session_id, message} and reads back an SSE stream of JSON event lines. Each session_id maps to one WebSession holding an Agent; sessions are revived from their on-disk transcript when first requested, so closing and reopening the app restores history.

Slash commands (/help, /reset, /tools, /model, /goal, /plan, /auto, /compact, /reload, plus user commands from ~/.evi/commands/) are dispatched server-side — the browser just types and reads; CLI and web behave identically.

The desktop app

desktop/src-tauri/src/main.rs is the native shell. On launch it:

  1. Picks a stable preferred port 8473 (falling back to a random free port if taken). A stable origin means the webview's localStorage — your open tabs and session ids — survives a restart.
  2. Spawns the server. In a packaged build it runs the bundled PyInstaller sidecar evi-server; in a source checkout it spawns py -3.13 -m uvicorn evi.apps.web.server:app (or python3 -m uvicorn … off-Windows) from the repo root.
  3. Loads a loading shim that polls the port and redirects once /api/health is up, so you never see a connection-refused page during the sidecar's cold start.
  4. Builds the File / Edit / View / Help menu bar and a system tray. Closing the window calls prevent_close() and hides it — the assistant and its warm sidecar keep running. Quit from the tray ("Quit eVi") or File → Exit.
  5. Starts a background update check (unless in remote mode or opted out).

Menu/tray actions other than devtools and quit are routed into the page via window.eviUI.handleMenu('<id>') (e.g. new_chat, settings, check_updates, open_logs).

Remote (thin-client) mode: set EVI_REMOTE_URL to point the desktop window at an already-running eVi elsewhere (e.g. a GPU box). The app skips the Python spawn, waits up to 15s for that URL's /api/health, and navigates to it. The updater is disabled in this mode (there's no local bundle to replace).

Settings (read/write config)

The settings panel reads GET /api/config and writes POST /api/config with a nested {section: {key: value}} patch. Key behaviours:

Auth & multi-user

A FastAPI middleware guards every /api/* route. Auth fires only when [web] auth_token is set or [web] multi_user = true with users present; otherwise access is open (fine for localhost-only). When active, a request must carry Authorization: Bearer <token> or ?token=<token> (the query form covers <img> src and SSE streams that can't set headers). A few paths self-bootstrap and stay public: /, /static/*, /images/*, /api/health, /api/auth/check, /api/backend/status, and routine webhooks (/api/routine/*, which authenticate via their own unguessable path token).

In multi-user mode, ~/.evi/users.json holds {name, token} entries. Any user token authenticates and the request is scoped to that user; the owner's auth_token still works and resolves to the user "owner". Each user gets an isolated workspace — sessions, transcripts (~/.evi/users/<name>/transcripts), and memory (~/.evi/users/<name>/memory) — that other users can't see. Skills, plugins, and config stay shared (capabilities, not personal data). Drop a user from the file to revoke access.

Deep links

evi/deeplinks.py defines three routes:

LinkEffect
evi://session/<id>open / resume that session
evi://newstart a new chat
evi://workflow/<name>open the dispatch panel, ready to run a workflow

The desktop app registers the evi:// scheme (via tauri.conf.json, plus a best-effort runtime register_all() for dev/portable runs). An incoming link is mapped to an in-app web path by to_web_path() / the Rust mirror deep_link_to_path(): evi://session/<id> → /?session=<id>, evi://workflow/<name> → /?workflow=<name>, everything else (including evi://new) → /. Unknown routes fall back to / so a stray link never errors the shell. Because the web UI understands /?session= and /?workflow= natively, the same links also work pasted into a browser.

Updater

spawn_update_check() queries eVi's signed GitHub releases (latest.json published by desktop-release.yml). If a newer version exists it downloads, installs, and restarts — on the async runtime, so it never blocks the window. The updater only accepts bundles signed with eVi's key (pubkey baked into tauri.conf.json), so a tampered release can't be installed. Before installing, the sidecar is killed first (Windows keeps onedir DLLs locked while evi-server runs). Progress surfaces as an in-app toast polled via the update_status_cmd Tauri command; Help → Check for Updates triggers it on demand via check_for_update_cmd.

Setup

All state lives under ~/.evi/ (Windows: %USERPROFILE%\.evi\). The relevant config sections in ~/.evi/config.toml:

[web]
auth_token = ""          # empty = open access. Set to require a bearer token.
multi_user = false       # opt-in per-user logins from ~/.evi/users.json

Multi-user logins file, ~/.evi/users.json (managed by the CLI — see below):

[ { "name": "alice", "token": "…" }, { "name": "bob", "token": "…" } ]

Optional pip extra: the web UI needs uvicorn/FastAPI, shipped in the web extra:

pip install 'evi-assistant[web]'

Defaults: evi web binds 127.0.0.1:8000. The desktop app prefers port 8473. Auth is off by default. Multi-user is off by default. Auto-update is on in packaged desktop builds.

Environment variables (desktop):

VarPurpose
EVI_HOMEOverride the ~/.evi/ location entirely
EVI_REMOTE_URLThin-client mode: skip the local spawn and navigate to this URL
EVI_PYTHONInterpreter to spawn in a source checkout (default py -3.13)
EVI_REPO_ROOTPin the repo root instead of auto-detecting it
EVI_AUTO_UPDATESet to 0 / false to disable the background self-update
EVI_TESSERACT_CMDSet automatically when a bundled tesseract ships next to the sidecar

Usage

CLI

evi web [--host 127.0.0.1] [--port 8000]   # launch the web UI
evi web-config token show                   # print the current token (or "(unset)")
evi web-config token rotate [--length 32]   # generate + persist a new token (prints once)
evi web-config token clear                  # unset the token → open access again
evi web-config users list                   # list multi-user logins
evi web-config users add <name>             # add/re-issue a user (prints token once)
evi web-config users remove <name>          # revoke a user
evi link [<id>|new]                         # print an evi:// deep link (default: most recent session)
evi link --open <evi://…>                   # resolve a link and show where it routes

Note the asymmetry: you launch the server with evi web, but its auth/helpers live under evi web-config (the token group is evi web-config token …, not evi web token …).

Web UI

Desktop app

Examples

Example 1 — turn on web auth and connect

# 1. Generate and persist a bearer token (printed once).
evi web-config token rotate
# rotated
# 3f2a9c…<64 hex chars>

# 2. Launch the server — it confirms auth is enabled.
evi web --host 127.0.0.1 --port 8000
# web auth: enabled (browser prompts for the token on first load)
# eVi web → http://127.0.0.1:8000

Open http://127.0.0.1:8000, paste the token into the sign-in form (it's cached in localStorage). Scripts/curl can authenticate either way:

# Header form (preferred):
curl -H "Authorization: Bearer 3f2a9c…" http://127.0.0.1:8000/api/health

# Query form (for streaming/img endpoints that can't set headers):
curl "http://127.0.0.1:8000/api/health?token=3f2a9c…"

To go back to open access: evi web-config token clear.

Example 2 — multi-user team with isolated workspaces

# Add two users; each token is printed exactly once.
evi web-config users add alice
# added alice
#   token: a1b2c3…  (shown once)
evi web-config users add bob

# Enable multi-user mode in ~/.evi/config.toml:
[web]
multi_user = true
evi web

Alice signs in with her token and sees only her sessions, transcripts (~/.evi/users/alice/transcripts), and memory (~/.evi/users/alice/memory); Bob sees only his. Revoke Bob instantly with evi web-config users remove bob.

Example 3 — deep links

# Print a link to your most recent session:
evi link
# evi://session/9f3c2a7b1e4d6058

# Link to a fresh chat:
evi link new
# evi://new

# See where a workflow link routes (no app needed):
evi link --open "evi://workflow/research"
# workflow research -> /?workflow=research

Clicking evi://session/9f3c2a7b1e4d6058 focuses the desktop app on that tab; pasting http://127.0.0.1:8473/?session=9f3c2a7b1e4d6058 into a browser does the same thing against a running server.

Example 4 — desktop as a thin client, no local model

# Point the desktop window at an eVi already running on a GPU box.
# (PowerShell)
$env:EVI_REMOTE_URL = "http://gpu-box:8473"
# (bash)
export EVI_REMOTE_URL="http://gpu-box:8473"

Launch the desktop app: it skips the Python spawn, waits for http://gpu-box:8473/api/health, and loads it. Disable the background updater in this or any build with EVI_AUTO_UPDATE=0.

Notes / limits

Relevant source files

Edit on GitHub ↗