Skip to content

Web UI

The bundled Web UI is a local chat application for using a lovia Agent or validating one before building a custom front end.

Start in one command

pip install "lovia[web]"
lovia web

Open http://127.0.0.1:8000 and configure one model on first launch. Later, lovia web --check validates the configuration and probes the endpoint without starting the server.

Model configuration has two scopes. A project file replaces the user-level configuration as a whole; the files are not merged field by field:

Path Scope
~/.lovia/config.json Default for the current user
./.lovia/config.json Complete override for the current project

The CLI adds .lovia/ to .gitignore to protect keys and chat data. On platforms with Unix permissions, the configuration is owner-readable and writable only.

The default Agent includes Todo, Memory, time and HTTP Tools, web search, scheduling, and a coding-mode Workspace rooted at the current directory. It also discovers:

  • AGENTS.md as Agent instructions;
  • ./.agents/skills and ~/.agents/skills as the Skills directories (project scope first; manage the list in Settings → Skills);
  • ./.lovia/memory as the Memory directory.

The legacy ./skills directory is no longer discovered automatically. Move it to ./.agents/skills, or add it in Settings → Skills. Web search uses Tavily when its key is configured, otherwise it tries the optional DuckDuckGo backend.

Default features can make model calls outside the main Run. Memory performs one digest after each completed Run and another call when a periodic dream is due; titles and follow-up suggestions also use separate calls. Disable the relevant features with --no-memory or --no-followups, or assign auxiliary work to a cheaper model. See Memory for the full cost boundary.

Local use by default

Loopback 127.0.0.1 requires no credentials. Binding another address requires --token or LOVIA_WEB_TOKEN; the server generates one when none is supplied. A client holding that token can use every Agent capability, including file edits and shell execution. Treat it as a password and prefer --readonly when allowing other devices. For multi-user operation, see Deployment.

Models and switching

Model profiles are stored in config.json. Connection testing makes a real request to check reachability, authentication, the model list, and the reported context window. A model ID can still be entered when the endpoint does not advertise it.

API keys are write-only: the server returns only whether a key is set and a masked hint, never the complete value. Configuration updates are validated as a whole, written atomically, and applied without a restart.

A model switch applies from the next message, including later messages in an existing chat, scheduled runs, and background subagents. An in-flight reply continues on its original model. Vision and auxiliary work such as titles and follow-up suggestions can be assigned to separate profiles.

Extra request fields (Advanced) is a JSON object merged verbatim into every request to that endpoint — the provider's extra_body. It is how reasoning gets configured, in the endpoint's own dialect, since one OpenAI-compatible flavor hides many: {"reasoning_effort": "medium"} for OpenAI, DeepSeek, Qwen 3.8 and vLLM; {"chat_template_kwargs": {"enable_thinking": false}} for a vLLM/SGLang-served Qwen or GLM; {"thinking": {"type": "disabled"}} for the DeepSeek and Z.ai APIs; {"output_config": {"effort": "medium"}} for Anthropic. A null value removes a field the adapter would send ({"stream_options": null} for gateways that reject it). Nothing is translated, and the endpoint decides what it accepts — an official API rejects unknown fields on the next message, while vLLM may ignore them silently; the thinking trace in the chat is the real check. The chips under the editor insert these shapes; they claim nothing about the model.

Every call through the profile carries the fields — the main reply, background subagents, compaction summaries, and, for a profile assigned the vision or aux role, image descriptions or titles and follow-ups. A second parameter set on the same endpoint is a second profile: Duplicate copies one server-side (key included, since the page never holds it), so "thinking off for titles" is a copy with enable_thinking: false assigned to the aux role. Thinking models need output room: a max_tokens here overrides --max-tokens for that endpoint, but the context policy's output reserve does not follow it.

The search backend, Tavily key, and skill directories also live in config.json. Model connections, additional profiles, role assignments, search, and skills have no CLI flags; use Settings or maintain the file directly.

Skills

Settings → Skills manages the skill directories — an ordered list; each skill is a subdirectory with a SKILL.md (see Skills). On duplicate skill names the earlier directory wins. The list defaults to ./.agents/skills and ~/.agents/skills; remove a default to disable it, or add any other directory (~ works). The pane shows what each directory contributes — discovered skills, parse problems, shadowed duplicates — and a directory that does not exist is simply skipped, so a stale entry never prevents the server from starting. Changing the list applies immediately; adding a skill inside an already-listed directory needs no action at all.

Serve your own Agent

Create app.py:

from lovia import Agent

assistant = Agent(
    name="assistant",
    instructions="Answer clearly and use tools when they improve accuracy.",
    model="<model>",
)

Then run:

lovia web --app app:assistant

--app MODULE:ATTR accepts one Agent, a {name: agent} mapping, or an app built with create_app() (or a zero-argument factory returning any of them). An app is served as built — your own auth, storage, and UI options — with the CLI still handling host, port, logging, and the startup banner; options it would otherwise pass to create_app() (--token, --db, --title, …) are ignored with a warning. For Python deployment and ASGI integration, see Web server.

The interface renders GitHub-flavored Markdown, highlighted code, Mermaid, and inline images. The default Agent is told about these capabilities; a custom Agent is not. Add SURFACE_NOTE when you want the model to use them deliberately:

from lovia.web import SURFACE_NOTE

assistant = Agent(
    name="assistant",
    instructions="Answer clearly.\n\n" + SURFACE_NOTE,
    model="<model>",
)

Images and files

Attachments are stored under the Workspace's uploads/ directory, while the message records a relative path. The Agent can therefore open a file through Workspace Tools even when the model cannot consume it directly. --no-workspace disables this path as well.

A Markdown image whose path is workspace-relative — ![chart](uploads/chart.png) — displays the Workspace file itself, in a reply and in the Files panel's Markdown preview alike. Paths resolve against the document's own directory and then the Workspace root, and are served read-only through the same endpoint as the preview. SVG renders too, through the endpoint's attachment form: browsers ignore Content-Disposition for an image, where an SVG's scripts are disabled, and honor it for navigation, where they would run — so the picture shows and the URL only ever downloads.

A link — [report](report.md) — points at the same endpoint. In a reply it opens in a new tab when the browser can display the file (images, PDF) and downloads it otherwise; either way the conversation stays put. Inside the Files panel the same link opens in the viewer instead. External links in a reply also open in a new tab.

Vision configuration controls whether images are sent inline:

  • Official OpenAI and Anthropic endpoints are treated as vision-capable by default. For another multimodal endpoint, set the profile's vision capability to supported ("vision": "on" in config.json).
  • A text-only main model can assign the vision role to another profile. The main model then receives text through the describe_image Tool; raw image data does not enter its conversation history.

Uploads default to 25 MiB per file (LOVIA_MAX_UPLOAD_MB). Named extensions must match the built-in allowlist; override it with LOVIA_UPLOAD_ALLOWED_EXT, separated by commas or spaces, or use * for any.

A file uploaded from the Files panel is not automatically attached to the next message. The Recent list leaves out environment noise (node_modules/, venv/, __pycache__/, *.pyc) and the agent's scratch dir tmp/, and the "new files" badge counts only writes Recent would show; that is presentation only — browsing shows every directory, and any path the agent references (an inline image under tmp/, a tool card's open in Files) opens as long as the agent itself could read it. Deleting a chat does not remove files under uploads/; the application or user must clean them up.

Background processes belong to the chat rather than one Run. Finishing a Run does not stop them; deleting the chat or stopping the Web server reaps them, and a server restart does not restore them. See Workspace for the full lifecycle.

Charts

The show_chart tool draws interactive charts in the chat — line, bar, pie, scatter, heatmap, candlestick, sankey and the other Apache ECharts series types. The model writes an ECharts option as the call's argument, and the card renders it, live and when the chat is reopened. The default agent of lovia web has the tool; for your own agent, add it: tools=[show_chart] (from lovia.web). Any tool can produce a chart too: return ui_result("chart", option), which draws from real data without the model copying numbers (see UI components). A tool that takes the option from the model — say one that runs a query and puts the rows into the option's dataset — checks it with validate_chart_option(option) first: it raises the same tool errors as show_chart, so the model can fix the option and retry.

The chart's title is shown as the card's heading, above the chart, so it never collides with the legend. A toolbar beside it (shown on hover) downloads the chart as PNG or SVG (title included), downloads its data as CSV (JSON for trees, sankeys and graphs, which have no table shape), and opens the chart full screen.

Good to know:

  • ECharts (about 1 MB, bundled) loads with the first chart, so chats without charts never fetch it.
  • The chat's HTML export includes each chart as an SVG image.
  • The option is sanitized before it is drawn. Images, links, the graphic and toolbox components, and HTML tooltips are removed, and so are series types that need code or map data (custom, map).
  • For house conventions — preferred chart types, units, titles — write them into the instructions or a Skill. No code is needed.
  • The palette and inks are CSS variables (--lv-chart-1 … --lv-chart-8, --lv-chart-text, --lv-chart-title, --lv-chart-axis, --lv-chart-grid). Override them in the head block of your own template to match a brand.

Tables

The show_table tool shows the user an interactive table instead of a Markdown one: click a heading to sort (again to reverse, a third time to restore the order), type to filter longer tables, and page through 20 rows at a time. Numbers align right and sort as numbers. A toolbar copies the table (tab-separated, ready to paste into a spreadsheet), downloads it as CSV, or opens it full screen; both exports take the current view, filtered and sorted. The chat's HTML export includes it as a plain table with every row.

The default agent of lovia web has the tool; for your own agent, add it: tools=[show_table] (from lovia.web). The more useful path is often a tool of your own that returns ui_result("table", {"title": ..., "columns": [...], "rows": [[...], ...]}): the rows come from your data — a query, an API — and the model never writes the numbers out, which is faster, cheaper, and exact. Cells are strings, numbers, booleans, or null, and values are shown as given. Keys the table doesn't know are ignored, so the same props can carry extras meant for the model, such as a total row count or the SQL that produced them. For a large result, keep the rows out of the model's context with put_ui_data: the model reads a preview, and the table shows every row (see Bulk data).

Pages

The show_html tool shows an interactive page inline: a calculator, a simulation, a custom visualisation, anything a chart or table can't do. The model writes a self-contained page, with inline CSS and JavaScript, and the card runs it in a sandbox. The default agent of lovia web has the tool; for your own agent, add it: tools=[show_html] (from lovia.web). A tool of your own can return ui_result("html", {"html": page, "title": ...}).

What the page can and can't do:

  • It can't reach the app or the network. It runs in an opaque origin (no cookies, storage, or access to the chat), and its policy blocks every request: no fetch, CDN scripts, web fonts, or remote images. Links don't navigate.
  • It matches the chat's look through CSS variables: var(--lv-ink), var(--lv-surface), var(--lv-border), var(--lv-accent), var(--lv-chart-1) … var(--lv-chart-8), in light and dark themes.
  • A button can ask a follow-up as the user: lovia.send("Break it down by region"). This works only from a real click inside the page.
  • The card grows with the page, up to most of the screen, and has full screen and an HTML download.
  • When you reopen a chat, its pages wait for a click on Run, so a page stuck in a loop can't freeze the chat each time it opens. A theme switch reloads a page.
  • The chat's HTML export has no scripts, so it shows the page's source instead.

Useful CLI options

Apart from model, search, and skills configuration, options resolve as CLI flag, environment variable, then default.

Flag Environment Default
--host / --port LOVIA_HOST / LOVIA_PORT 127.0.0.1 / 8000
--root-path LOVIA_ROOT_PATH None; the prefix a proxy serves the app under (details)
--token LOVIA_WEB_TOKEN Not needed on loopback; generated otherwise
--db LOVIA_DB ./.lovia/<agent>.db
--app MODULE:ATTR LOVIA_APP Build the default Agent (or: an Agent, mapping, or app of your own)
--memory-dir / --no-memory LOVIA_MEMORY_DIR ./.lovia/memory
--workspace, --readonly / --trusted / --no-workspace LOVIA_WORKSPACE, LOVIA_WORKSPACE_MODE . in coding mode
--instructions-file LOVIA_INSTRUCTIONS_FILE AGENTS.md when present
--max-retries / --max-turns LOVIA_MAX_RETRIES / LOVIA_MAX_TURNS 4 / 50
--max-tokens LOVIA_MAX_TOKENS The provider's; a max_tokens in a profile's extra request fields wins
--title LOVIA_TITLE lovia
--log-level LOVIA_LOG_LEVEL info
--no-followups LOVIA_FOLLOWUPS Follow-up suggestions enabled
--no-subagents — Background subagents enabled
--check — Check model configuration and exit

Use lovia web --help for the complete list.

Questions from the Agent

The default Agent includes the ask_human Tool. A pending question is stored on the server, so refreshes and reconnects do not lose it. After the default ten-minute timeout, the call is cancelled and the model continues with a Tool error. This also prevents scheduled and background work from occupying a run slot indefinitely.

A custom Agent must share one HumanChannel between the Tool and the Web app:

from lovia import Agent
from lovia.tools import HumanChannel, ask_human
from lovia.web import create_app, serve

channel = HumanChannel()
agent = Agent(name="bot", model="<model>", tools=[ask_human(channel)])
serve(create_app(agent, question_channel=channel, question_timeout=600))

Follow-up suggestions

Follow-up suggestions use a separate model call and never enter the Transcript. Disable them to avoid that extra call with --no-followups, or point them at a cheaper model by assigning it the aux role (Models and switching). They are opt-in for a custom Agent served through create_app(); see Web server.

Run and connection boundaries

Runs are managed by the server. Refreshing or closing the page does not cancel a Run, and reconnecting resumes from server state. Explicitly stopping a Run still preserves completed Turns in the Session.

A graceful server shutdown retains the main Run's Checkpoint so it can resume after restart. Process-local approval and SSE subscription state is not restored, nor are Shell background processes or background subagents.

A message sent while a Run is active does one of two things, decided per message:

  • Queue (Enter): the message waits above the composer and is sent as its own Run once the active Run has completed. Several queued messages go one at a time, each after the previous one's Run completes. A Run that fails or is stopped pauses the queue; Send now or withdrawing the message moves on. Queued messages are held by the browser tab: they go only while their chat is open there, and a reload drops them.
  • Steer (⌥/Alt+Enter, or Steer on a queued message): the message joins the Run at its next Turn. If the Run finishes first, one sent with ⌥/Alt+Enter starts the next Run in order; a queued message steered too late returns to the head of the queue. A steer that gets no answer (e.g. the network dropped) may have landed, so it is never resent on its own: it waits at the head of the queue, paused, for you to check the chat.

Attachments can be added while a Run is active: they go with the message, queued or steered. Send waits for any upload still in progress. A queued message can also go back to the composer for editing — text and attachments, when the composer and its tray are empty — or be withdrawn.

Branching a chat

Branch in new chat, on any finished reply, starts a new chat with everything up to and including that reply, so you can take the conversation in another direction from there. Branching from the last reply copies the whole chat. A reply followed by a Run still in progress gets the action once that Run has finished. A branch copies what the server holds at that moment. The original is left untouched, the new chat links back to it, and deleting one leaves the other.

A branch copies the conversation, not the workspace: files written since the branch point stay as they are now.

Ask the agent to do something "in a new chat" and it can start one itself (start_session). Its card opens the new chat, which starts with none of this conversation and links back to the chat that started it.

Background subagents

lovia web enables background subagents by default; disable them with --no-subagents. A default child keeps Skills, Todo, Tools, and Workspace, but omits Scheduling, Memory, and Subagents, so it cannot spawn recursively.

A custom Agent must attach the Subagents Plugin. create_app() wires a Plugin using its default execution mode into Web supervision; disable that with create_app(..., wire_subagents=False). An app mounting build_api_router directly must call wire_subagents(deps) once.

Each background task has an independent Session. When it completes, its report enters the parent's next Turn if the parent Run is active; otherwise the server starts a Run to process the report. The browser does not need to remain online.

A task is not owned by the parent Run, so stopping the parent does not stop the task; deleting the parent chat stops its tasks and deletes them with it. Approvals auto-deny after approval_timeout; unfinished tasks are not resumed after a server restart.

See also