UI — NiceGUI

The NiceGUI web frontend is composed per app (klea_agent.ui.web / klea_rag.ui.web, ADR-0031): each app builds its page from the shared components below. klea_utils provides only the reusable helpers and components, never a full page.

Shared frontend helpers

These are framework-agnostic: the NiceGUI web frontend and the TUI share one source of truth for chat/session state, the API client, the stream-event reducer, and transcript export.

In-memory chat state shared by the Klea frontends.

Framework-agnostic session-state store for the web (NiceGUI) and terminal (Textual) clients: the per-chat ChatData cache and the helpers that operate on it (create-or-get, sorting, choice resolution, credential checks). Keyed by {user_id}:{chat_id} so colliding chat_ids across different users do not interfere.

File: klea_utils/ui/state.py

Copyright 2026 Ankur Sinha Author: Ankur Sinha <sanjay DOT ankur AT gmail DOT com>

class klea_utils.ui.state.ChatData[source]

Bases: TypedDict

In-memory state for one chat session (see ensure_chat()).

Keys are created by ensure_chat(); the NotRequired ones are populated lazily by the stream/hydration/context-control paths. The dict is a frontend cache only, not an API contract.

context: NotRequired[dict[str, Any]]

Hydrated graph session context (e.g. the agent operating mode).

created: float

datetime.timestamp() of creation.

expanded: set[int]

Indices of transcript messages currently expanded in the UI.

inspector_entries: list[InspectorEntry | InspectorMarker]

a per-query InspectorMarker followed by that query’s entries. Kept for the browser session (not persisted).

Type:

Inspector items for this chat, in order

inspector_expanded: set[int]

Indices into inspector_entries currently expanded in the UI.

inspector_sections_collapsed: set[int]

Indices of inspector query sections currently collapsed.

interrupt: NotRequired[dict[str, Any]]

the interrupt data (kind, question/questions, interrupt_id and a hitl_response_schema when the node supplied one). Cleared when the next turn starts.

Type:

Pending HITL ask (ADR-0046) while the run is paused

messages: list[MessageData]

Rendered transcript messages.

mode_pref: NotRequired[str]

App-defined context-control preferences (mode / access level).

model_info: dict[str, dict[str, Any]]

Active model config per role (from fetch_active_models).

name: str

Human-readable display name (auto-generated, renameable).

pinned: bool

Whether the chat session is pinned to the top of the list.

preview: NotRequired[str]

Truncated first user query, from the server session list; used as a human-readable hint in the session picker / chat-list tooltip.

state_sections: dict[str, StateSection]

Status-pane sections, keyed by node label / section key.

stores_config: NotRequired[str | None]

Absolute stores-JSON path selected for this chat (/stores <path>); None means the deployment default (KLEA_STORES_CONFIG_FILE).

token_usage: TokenUsage

Accumulated token totals for this chat.

turn_status: TurnStatus

Current/last turn’s status (progress/error/stopped/awaiting_input).

class klea_utils.ui.state.ChatIndicator(kind: Literal['spinner', 'icon'], icon: str, classes: str)[source]

Bases: NamedTuple

A chat row’s avatar indicator (see chat_indicator()).

Parameters:
  • kind – "spinner" (a running run) or "icon".

  • icon – Material Icons name when kind is "icon".

  • classes – Extra CSS classes (sizing / colour) for the widget.

classes: str

Alias for field number 2

icon: str

Alias for field number 1

kind: Literal['spinner', 'icon']

Alias for field number 0

class klea_utils.ui.state.InspectorEntry[source]

Bases: TypedDict

An inspector entry buffered for the most recent query in a chat.

class klea_utils.ui.state.InspectorMarker[source]

Bases: TypedDict

A per-query heading in the inspector pane.

Marks the start of a query’s inspection section, so entries from different queries can be told apart when the pane keeps them all.

class klea_utils.ui.state.MessageData[source]

Bases: TypedDict

A single rendered chat message.

role is "user", "agent", "tool" or "notice"; header is an optional small title (used by tool blocks and notices). Tool messages add the mime/display/data/meta fields used by the chat-bubble renderer, plus is_error for an errored tool result (styled by the renderer). Notices carry an optional kind for future styling.

class klea_utils.ui.state.StateSection[source]

Bases: TypedDict

A status-pane section streamed by a graph node.

class klea_utils.ui.state.TokenUsage[source]

Bases: TypedDict

Accumulated token totals for an in-memory chat session.

class klea_utils.ui.state.TurnStatus[source]

Bases: TypedDict

Per-chat turn status content (the chat-pane region under the transcript).

Rendered under the transcript for the chat and scoped to its current (or last) turn. kind selects the rendering:

kind

meaning

idle

nothing to show

progress

an in-flight run; heading is the live line

error

a failed run; message and resumable (Retry)

stopped

the run was cancelled by the user

awaiting_input

the run paused for HITL input; the ask is on ChatData.interrupt (ADR-0046)

Any new turn (send / retry) resets it, so a terminal status never survives past the next user action.

klea_utils.ui.state.chat_indicator(*, streaming: bool, awaiting: bool, pinned: bool, active: bool) → ChatIndicator[source]

Return the avatar indicator for a chat row.

The avatar cell is the only QItemSection Quasar keeps visible when the chat drawer is collapsed to its mini rail, so the running / awaiting state and the active marker live there (the row’s name label is hidden when collapsed). All icon names are Material Icons (the set NiceGUI/Quasar loads).

Precedence: streaming > awaiting input > pinned > plain history. The currently shown chat is marked with the theme’s primary colour.

Parameters:
  • streaming – Whether the chat has an active run.

  • awaiting – Whether the chat is paused at a HITL interrupt.

  • pinned – Whether the chat is pinned.

  • active – Whether this is the currently shown chat.

Returns:

The ChatIndicator to render.

klea_utils.ui.state.chat_preview_text(text: str | None, max_chars: int = 120) → str[source]

Return a single-line, truncated preview for a chat list or tooltip.

Collapses whitespace (a query may contain newlines) and appends an ellipsis when the text exceeds max_chars.

Parameters:
  • text – The raw preview (a first user query), or None.

  • max_chars – Maximum display length, including the ellipsis.

Returns:

The normalized preview, or "" when there is none.

klea_utils.ui.state.ensure_chat(user_id: str, chat_id: str) → ChatData[source]

Return the chat session dict for user_id / chat_id, creating it if missing.

The dict schema is documented on ChatData; this function creates the always-present keys and returns the (possibly new) entry.

klea_utils.ui.state.format_token_usage(usage: Mapping[str, Any]) → str[source]

Format accumulated token usage for the web/TUI status line.

cached_tokens is a subset of input_tokens (cache reads), so it is shown parenthetically only when non-zero rather than added to the totals.

Parameters:

usage – The chat’s token_usage mapping.

Returns:

"<in> in (<cached> cached) / <out> out", or "<in> in / <out> out" when no tokens were cached.

klea_utils.ui.state.get_chats_sorted(user_id: str) → list[tuple[str, ChatData]][source]

Return (chat_id, data) pairs for user_id, pinned first, then by creation desc.

Filters by the user_id prefix so that in a multi-browser scenario (same NiceGUI process) each user only sees their own chats.

klea_utils.ui.state.idle_turn_status() → TurnStatus[source]

Return the empty per-chat status.

klea_utils.ui.state.interrupt_display(response: Mapping[str, Any] | None, *, cancel: bool = False) → str[source]

Render a HITL answer/cancel as the user turn shown in the transcript.

Delegates to klea_utils.api.hitl.render_interrupt_response() (the same renderer the server uses for the persisted row) so the live and reloaded text cannot drift; a cancel is marked locally (the server writes no user row for a cancel).

Parameters:
  • response – The resume mapping sent to the server, or None.

  • cancel – Whether the user cancelled the interrupt.

Returns:

The user-turn text.

klea_utils.ui.state.missing_credentials(model_info: dict[str, Any]) → list[str][source]

Return roles whose resolved model needs an API key that is not set.

Used by the UI to flag roles that cannot run until the user stores a provider credential (or sets the provider’s environment variable). Roles whose provider needs no key (e.g. local Ollama) are ignored, as are roles with no model resolved yet.

Parameters:

model_info – The resolved per-role config from fetch_active_models / fetch_session_models (each value carries a credential block).

Returns:

Role names whose credential source is none but required.

klea_utils.ui.state.resolve_chat_choice(current_chat: Mapping[str, Any] | None, pending: Any, pref: Any, context_value: Any, allowed: Container[str], default: str) → str[source]

Return the effective choice for current_chat.

pending (PageContext.query_extra) is page-session scoped, not per-chat, so it is only consulted before a chat exists – e.g. choosing an operating mode or access level for the first message. Once a chat exists its per-chat preference and hydrated context win, so switching chats restores each chat’s own value instead of the last selection made elsewhere.

Parameters:
  • current_chat – The active chat session dict, or a falsy value.

  • pending – The pending request, or None.

  • pref – The per-chat preference, or None.

  • context_value – The value from the hydrated context event.

  • allowed – The set of valid option values.

  • default – The fallback value.

Returns:

The resolved value, else default.

klea_utils.ui.state.resolve_choice(pending: Any, pref: Any, context_value: Any, allowed: Container[str], default: str) → str[source]

Return the effective UI choice from pending, pref, context, default.

Used by the status-pane context controls (operating mode / tool access level) so their defaults render before a chat exists: the caller passes the pending request (PageContext.query_extra), the per-chat preference, and the hydrated context value; the first one that is a known option wins, otherwise default.

Parameters:
  • pending – The pending request (next query’s value), or None.

  • pref – The per-chat preference, or None.

  • context_value – The value from the hydrated context event.

  • allowed – The set of valid option values.

  • default – The fallback value.

Returns:

The first valid value among the candidates, else default.

Server API client shared by the Klea frontends.

Thin httpx wrappers over the backend’s session / model / credential / command endpoints, used by the web (NiceGUI) and terminal (Textual) clients. All functions accept the server_url as their first argument so the caller can point them at any running Klea backend without shared state.

File: klea_utils/ui/client.py

Copyright 2026 Ankur Sinha Author: Ankur Sinha <sanjay DOT ankur AT gmail DOT com>

async klea_utils.ui.client.clear_credential(server_url: str, user_id: str, provider: str, endpoint: str = '') → bool[source]

DELETE the stored credential for a provider(+endpoint).

async klea_utils.ui.client.clear_model_override(server_url: str, user_id: str, chat_id: str, role: str) → bool[source]

DELETE the model override for a chat role.

async klea_utils.ui.client.clear_session_model_override(server_url: str, user_id: str, role: str) → bool[source]

DELETE the per-session default model override for a role.

async klea_utils.ui.client.create_chat_on_server(server_url: str, user_id: str, chat_id: str) → None[source]

POST a new chat to the server so it persists.

async klea_utils.ui.client.delete_chat_on_server(server_url: str, user_id: str, chat_id: str) → None[source]

DELETE the chat on the server.

async klea_utils.ui.client.fetch_chat_context(server_url: str, user_id: str, chat_id: str) → dict[str, Any][source]

Fetch a chat’s session context and pending interrupt (ADR-0032/0046).

Calls GET /chat/{user_id}/{chat_id}/context and returns the body ({"context": {...}, "pending_interrupt": {...}, "usage": {...}}), so a frontend can restore a chat’s context, token totals, and re-present a paused HITL ask on load/switch.

Parameters:
  • server_url – Base URL of the backend API server.

  • user_id – Opaque persistent user identifier.

  • chat_id – Chat conversation identifier.

Returns:

The context body, or {} on failure.

async klea_utils.ui.client.fetch_chat_events(server_url: str, user_id: str, chat_id: str) → list[dict[str, Any]][source]

Fetch a chat’s persisted UI events (inspection/status panes).

Calls GET /chat/{user_id}/{chat_id}/events; the frontends replay the events through the shared reducer to rebuild the panes on load (ADR-0032 update).

Parameters:
  • server_url – Base URL of the backend API server.

  • user_id – Opaque persistent user identifier.

  • chat_id – Chat conversation identifier.

Returns:

The event dicts, or [] on failure.

async klea_utils.ui.client.fetch_chat_messages(server_url: str, user_id: str, chat_id: str) → list[dict[str, Any]][source]

Fetch a chat’s persisted messages.

Calls GET /chat/{user_id}/{chat_id}/messages.

Parameters:
  • server_url – Base URL of the backend API server.

  • user_id – Opaque persistent user identifier.

  • chat_id – Chat conversation identifier.

Returns:

The message dicts, or [] on failure.

async klea_utils.ui.client.fetch_chats(server_url: str, user_id: str) → list[dict[str, Any]][source]

Fetch the user’s chats, most recently updated first.

Calls GET /chat/{user_id} (the server orders by updated_at descending), so the first entry is the “continue” target.

Parameters:
  • server_url – Base URL of the backend API server.

  • user_id – Opaque persistent user identifier.

Returns:

The chat metadata dicts, or [] on failure.

async klea_utils.ui.client.fetch_commands(server_url: str) → list[dict[str, Any]][source]

Fetch the server-side session-command catalogue (ADR-0047).

Parameters:

server_url – Base URL of the backend API server.

Returns:

The command metadata dicts, or [] on failure.

async klea_utils.ui.client.hydrate_chats(server_url: str, user_id: str) → None[source]

Fetch all chats and their messages from the server into the local state.

Populates the in-memory chats dict with every conversation belonging to user_id, including the full message history for each chat.

After this call the frontend can switch between any chat without additional server round-trips. If the server has no data for this user yet the local store stays empty.

async klea_utils.ui.client.rename_chat_on_server(server_url: str, user_id: str, chat_id: str, title: str) → None[source]

PATCH the chat title on the server.

klea_utils.ui.client.server_message_to_data(message: Mapping[str, Any]) → MessageData[source]

Convert a server message row to the frontend MessageData shape.

A tool row is rebuilt from its stored metadata (header / mime / data / meta / is_error) so a reloaded chat renders the same blocks as the live stream; other roles map to user/agent.

Parameters:

message – A GET .../messages row (role, content, metadata, created_at).

Returns:

A message dict for ChatData["messages"].

async klea_utils.ui.client.set_credential(server_url: str, user_id: str, provider: str, secret: str, endpoint: str = '') → bool[source]

PUT a provider credential for the user. The secret is not logged.

async klea_utils.ui.client.set_model_override(server_url: str, user_id: str, chat_id: str, role: str, payload: dict) → bool[source]

POST a model override for a chat role.

async klea_utils.ui.client.set_session_model_override(server_url: str, user_id: str, role: str, payload: dict) → bool[source]

POST a per-session default model override for a role.

Framework-agnostic stream-event reducer shared by the Klea frontends.

The /query/stream SSE events are turned into frontend state by apply_stream_event(), a pure function: it mutates the chat dict it is given and returns an action string, touching no UI, no HTTP and no NiceGUI. The web (NiceGUI) and terminal (Textual) clients consume the same reducer, so the event-to-state contract lives in one place; the UI-driving coroutines (run_stream / stop_stream) stay in each frontend.

See devdocs/system/streams.md and devdocs/system/tui.md.

File: klea_utils/ui/stream_events.py

Copyright 2026 Ankur Sinha Author: Ankur Sinha <sanjay DOT ankur AT gmail DOT com>

klea_utils.ui.stream_events.apply_stream_event(chat: ChatData, event: dict[str, Any]) → str | None[source]

Apply one stream event’s pure state mutations to the chat dict.

Mutates chat in place (token usage, status sections, inspector entries, and, on completion, the final message) and returns the action the UI layer reacts to:

return value

meaning

"usage"

token usage totals were incremented

"state"

a status-pane section was stored

"inspect"

an inspection entry was appended

"context"

session context (e.g. the operating mode) was stored

"notice"

a user-facing progress notice was appended

"complete"

the final assistant message was appended

"error"

the backend signalled an error

None

no state change (progress / info / token events)

Inspector entries are appended to inspector_entries as they arrive; the caller renders each one incrementally. A per-query marker is prepended by the caller at stream start.

Parameters:
  • chat – Chat session dict (see state.ensure_chat).

  • event – Parsed SSE event dict from stream_events.

Returns:

Action string described above, or None.

klea_utils.ui.stream_events.replay_chat_events(chat: ChatData, events: Iterable[dict[str, Any]]) → None[source]

Rebuild the pane state from a chat’s stored UI events (ADR-0032 update).

Mirrors the live stream so a reloaded chat shows the same inspection / status panes: a query event opens a new inspector section (the same marker the live path inserts at run start), and every other event is reduced through apply_stream_event(). The transcript is hydrated separately from the persisted messages.

Parameters:
  • chat – The chat session dict (see state.ensure_chat).

  • events – The stored events (GET .../events), oldest first.

Session transcript export helpers.

Framework-agnostic rendering of a chat’s rendered transcript (the klea_utils.ui.state.MessageData entries) to Markdown, shared by the frontends’ /export client command (ADR-0047).

File: klea_utils/ui/export.py

Copyright 2026 Ankur Sinha Author: Ankur Sinha <sanjay DOT ankur AT gmail DOT com>

klea_utils.ui.export.export_filename(name: str, *, suffix: str = 'md') → str[source]

Return a filesystem-safe export filename derived from name.

Replaces any run of characters outside [A-Za-z0-9._-] with a single hyphen; falls back to klea-session when name has no usable characters.

Parameters:
  • name – The session name (usually the chat title).

  • suffix – The file extension, without the dot (default md).

Returns:

A safe <slug>.<suffix> filename.

klea_utils.ui.export.export_timestamp(when: datetime | None = None) → str[source]

Return a human-readable, ASCII timestamp for the export header.

klea_utils.ui.export.transcript_markdown(messages: Sequence[Mapping[str, Any]], *, title: str = 'Klea session', exported_at: str = '') → str[source]

Render messages as a Markdown transcript.

Each turn is a ## heading (## User, ## Assistant, ## Tool: <name>) rather than bold, so the role labels stay distinct from any **bold** in the message content.

Parameters:
  • messages – The chat’s rendered transcript entries (each with role/text/stamp, and header for tool blocks).

  • title – The document title (usually the session name); whitespace is collapsed and an empty title falls back to Klea session.

  • exported_at – An optional timestamp line (empty to omit).

Returns:

The Markdown document, ending in a newline.

Frontend identity resolution (ADR-0050 invariant I5).

Frontends (the Textual TUI and the NiceGUI web UI) resolve their user_id through one seam: an explicit --user-id / KLEA_USER_ID, then a persisted (machine-local) UUID. The persisted UUID is only sent to a loopback server, so it is never leaked to a remote deployment; a remote server without an explicit identity gets an empty user_id (the server treats user_id as an opaque token).

File: klea_utils/ui/identity.py

Copyright 2026 Ankur Sinha Author: Ankur Sinha <sanjay DOT ankur AT gmail dot com>

klea_utils.ui.identity.USER_ID_ENV_VAR = 'KLEA_USER_ID'

Env var carrying an explicit user id (mirrors the --user-id flag).

klea_utils.ui.identity.is_loopback_url(server_url: str) → bool[source]

Return whether server_url points at a loopback host.

Parameters:

server_url – Base URL of the backend API server.

Returns:

True for 127.0.0.1 / localhost / ::1.

klea_utils.ui.identity.persisted_user_id(app_name: str) → str[source]

Return the persisted frontend-local UUID for app_name, creating it once.

Stored as plain text under the app’s platformdirs user data dir, so the same machine keeps one identity across restarts.

Parameters:

app_name – Identity key used to locate the persisted UUID (the server app, e.g. "klea-agent").

Returns:

The persisted UUID.

klea_utils.ui.identity.resolve_user_id(*, explicit: str = '', env: str | None = None, server_url: str = '', app_name: str = 'klea') → str[source]

Resolve a frontend’s user_id through the ADR-0050 seam.

Order: explicit (--user-id) > env (KLEA_USER_ID) > the persisted local UUID, the last only when server_url is loopback (I5).

Parameters:
  • explicit – The --user-id value, or "".

  • env – The env-var value; None reads KLEA_USER_ID.

  • server_url – Base URL of the backend API server.

  • app_name – Identity key used to locate the persisted UUID (the server app, e.g. "klea-agent").

Returns:

The resolved user id ("" when none applies).

Framework-agnostic model-configuration helpers (shared by web and TUI).

The model-config dialog exists twice (a NiceGUI dialog and a Textual modal) but the logic behind it does not: parsing a role’s model string into editable fields and joining them back, deciding the credential scope and status of a (possibly unsaved) model, selecting the roles to write (every shown modifiable one, compared against the effective model) and persisting a role’s override. This module holds that logic with no frontend imports, so both frontends share one source of truth. The frontends keep only their own widgets and styling (a tone is mapped to a colour class per frontend).

File: klea_utils/ui/model_config.py

Copyright 2026 Ankur Sinha Author: Ankur Sinha <sanjay DOT ankur AT gmail DOT com>

class klea_utils.ui.model_config.CatalogueCache(server_url: str, user_id: str)[source]

Bases: object

Lazily fetch and cache the models.dev catalogue for a user.

Providers are fetched once; each provider’s models are fetched the first time it is picked and cached for the life of the instance.

Parameters:
  • server_url – Base URL of the backend API server.

  • user_id – Opaque persistent user identifier.

async load_models(provider: str) → list[str][source]

Return provider’s models, fetching and caching on first use.

Parameters:

provider – Klea provider id.

Returns:

The provider’s models, or [].

models_for(provider: str) → list[str][source]

Return the already-loaded models for provider (no fetch).

Parameters:

provider – Klea provider id.

Returns:

The cached models, or [].

async providers() → list[str][source]

Return the catalogue providers, fetching once.

Returns:

Sorted provider ids.

class klea_utils.ui.model_config.ModelOrigin(provider: str, model: str, suffix: str, url: str, backend: str, inference_provider: str)[source]

Bases: object

Parsed components of a role’s saved model string.

Holds both the originals (for the “unchanged” branch of build_model_string()) and the initial field values a picker is prefilled with.

Parameters:
  • provider – Original provider id.

  • model – Original model id.

  • suffix – Original non-URL suffix (put back while the model is unchanged); empty for a URL suffix.

  • url – Initial custom-URL field value.

  • backend – Initial HuggingFace backend field value.

  • inference_provider – Initial HuggingFace inference-provider value.

class klea_utils.ui.model_config.SaveOutcome(ok: bool, saved: list[tuple[str, str]], failed_role: str = '')[source]

Bases: object

The result of persisting a dialog’s shown modifiable roles.

Parameters:
  • ok – Whether every shown modifiable role saved.

  • saved – The (role, model) pairs persisted before any failure.

  • failed_role – The role that failed, when ok is False.

klea_utils.ui.model_config.TONE_MUTED = 'muted'

Credential-status tones, mapped to a colour/class by each frontend.

klea_utils.ui.model_config.build_model_string(provider: str, model: str, *, url: str = '', backend: str = 'endpoint', inference_provider: str = '', original: ModelOrigin | None = None) → str[source]

Join the picker’s field values back into a model string.

HuggingFace joins from its backend + inference-provider fields; other providers use the custom URL when one is set, else restore the original suffix while the model is unchanged.

Parameters:
  • provider – Provider field value.

  • model – Model field value.

  • url – Custom-URL field value.

  • backend – HuggingFace backend field value.

  • inference_provider – HuggingFace inference-provider field value.

  • original – The role’s original parsed model, if any.

Returns:

The joined model string.

klea_utils.ui.model_config.changed_roles(info: Mapping[str, Any], role_values: Mapping[str, str]) → list[tuple[str, str]][source]

Return the (role, new_model) pairs whose value changed.

info must be the effective (precedence-resolved) config: the baseline is the model each role would actually use, not the dialog’s seed or a stored snapshot, so an untouched inherited value is not reported as a change and is not pinned. Locked roles are skipped.

Parameters:
  • info – The effective model config (role -> cfg).

  • role_values – Current input values by role.

Returns:

The changed roles, in iteration order.

async klea_utils.ui.model_config.clear_role(server_url: str, user_id: str, chat_id: str, *, session_scope: bool, role: str) → bool[source]

Clear one role’s override (reverting it to the inherited model).

Parameters:
  • server_url – Base URL of the backend API server.

  • user_id – Opaque persistent user identifier.

  • chat_id – Active chat id (ignored for a session-scope clear).

  • session_scope – Clear the per-session default instead of per-chat.

  • role – The role to reset.

Returns:

Whether the reset succeeded.

klea_utils.ui.model_config.credential_display(model_name: str, saved: Mapping[str, Any] | None, stored_by_scope: Mapping[tuple[str, str], Mapping[str, Any]]) → tuple[str, str][source]

Return (label, tone) for a model’s credential status.

Parameters:
  • model_name – The model string.

  • saved – The saved credential block, if any.

  • stored_by_scope – Stored credentials keyed (provider, endpoint).

Returns:

The display label and a tone ("muted" / "error").

klea_utils.ui.model_config.live_credential(model_name: str, saved: Mapping[str, Any] | None, stored_by_scope: Mapping[tuple[str, str], Mapping[str, Any]]) → dict[str, Any][source]

Credential block for a (possibly unsaved) model string.

Provider and key requirement are derived client-side so typed but unsaved models are recognised; the source/masked come from the stored credentials or, when the scope matches, the saved block.

Parameters:
  • model_name – The model string.

  • saved – The saved credential block, if any.

  • stored_by_scope – Stored credentials keyed (provider, endpoint).

Returns:

A credential block (provider/endpoint/requires_key/ source/masked).

klea_utils.ui.model_config.live_info(info: Mapping[str, Any], role_values: Mapping[str, str], stored_by_scope: Mapping[tuple[str, str], Mapping[str, Any]]) → dict[str, Any][source]

Model config built from the dialog’s current input values.

Parameters:
  • info – The saved model config (role -> cfg).

  • role_values – Current input values by role.

  • stored_by_scope – Stored credentials keyed (provider, endpoint).

Returns:

A copy of info with each role’s model and credential block replaced by its live values.

klea_utils.ui.model_config.parse_model_origin(model: str) → ModelOrigin[source]

Parse model into a ModelOrigin for prefilling a picker.

A half-typed string (e.g. openai:) is kept whole in the model field so nothing the user typed is lost.

Parameters:

model – The saved model string.

Returns:

The parsed origin and initial field values.

klea_utils.ui.model_config.required_credential_scopes(info: Mapping[str, Any]) → dict[tuple[str, str], dict][source]

Return the distinct provider(+endpoint) scopes the roles need a key for.

Parameters:

info – The model config (role -> cfg).

Returns:

{(provider, endpoint): credential} for key-requiring roles.

klea_utils.ui.model_config.role_label(role: str) → str[source]

Return the human-facing label for a model role key.

tool_picker -> "Tool picker" (str.capitalize() alone would give "Tool_picker").

Parameters:

role – Model role key (chat, tool_picker, …).

Returns:

Display label.

async klea_utils.ui.model_config.save_roles(server_url: str, user_id: str, chat_id: str, *, session_scope: bool, info: Mapping[str, Any], role_values: Mapping[str, str]) → SaveOutcome[source]

Persist every shown modifiable role, stopping at the first failure.

Which roles are written is decided by shown_overrides(): every shown modifiable role with a non-empty model is written, compared against the effective (precedence-resolved) model in info; only the roles whose model genuinely differs from that baseline are promoted into the session default, so an untouched inherited role is written but not stamped over the default.

Parameters:
  • server_url – Base URL of the backend API server.

  • user_id – Opaque persistent user identifier.

  • chat_id – Active chat id (ignored for a session-scope save).

  • session_scope – Save per-session defaults instead of per-chat.

  • info – The effective model config (role -> cfg).

  • role_values – Current input values by role.

Returns:

A SaveOutcome.

klea_utils.ui.model_config.shown_overrides(info: Mapping[str, Any], role_values: Mapping[str, str]) → list[tuple[str, str, bool]][source]

Return the roles to persist: every shown modifiable role with a model.

Unlike changed_roles(), info must be the effective config: every shown modifiable role with a non-empty model is written, so “save what is shown” holds even when the dialog’s seed was stale. The third element says whether the value differs from the effective model, so that only a real change is promoted into the session default and an untouched role is not stamped over it.

Parameters:
  • info – The effective model config (role -> cfg).

  • role_values – Current input values by role.

Returns:

(role, model, changed) triples, in iteration order.

klea_utils.ui.model_config.use_for_all_targets(info: Mapping[str, Any], role: str, value: str) → dict[str, str][source]

Return the roles of the same kind that role’s model applies to.

Scoped by model_type so a chat model is never copied onto the embedding role. Autofills only; the user still saves.

Parameters:
  • info – The model config (role -> cfg).

  • role – The source role.

  • value – The model string to apply.

Returns:

{target_role: value} for the other modifiable same-kind roles.

Shared exit-confirmation text for the web and terminal frontends.

Kept in one place so both frontends show identical wording (the web header’s exit button and the TUI’s Ctrl+Q).

File: klea_utils/ui/exit_confirm.py

Copyright 2026 Ankur Sinha Author: Ankur Sinha <sanjay DOT ankur AT gmail DOT com>

klea_utils.ui.exit_confirm.EXIT_CONFIRM_MESSAGE = 'This stops any active run and closes this interface. The backend server is stopped only if this client started it.'

Confirmation body (used verbatim by both frontends).

klea_utils.ui.exit_confirm.EXIT_CONFIRM_TITLE = 'Exit Klea?'

Confirmation title.

Argparse parser for the Klea NiceGUI frontend entry point.

Provides a standard argparse parser for the arguments passed by the Typer web command when it launches app.py. The entry point simply calls make_parser().parse_args() and picks only the arguments it needs.

Usage:

from klea_utils.ui.web.nicegui.parser import make_parser

args = make_parser("My frontend").parse_args()
run_app(
    args.title, args.url,
    subtitle=args.subtitle,
    disclaimer=args.disclaimer,
    reload=args.reload,
)

File: klea_utils/ui/web/nicegui/parser.py

Copyright 2026 Ankur Sinha Author: Ankur Sinha <sanjay DOT ankur AT gmail DOT com>

klea_utils.ui.web.nicegui.parser.make_parser(description: str = 'Klea web interface') → ArgumentParser[source]

Return a preconfigured argparse.ArgumentParser.

Parameters:

description – Description shown in --help.

Shared NiceGUI web-entry helpers for Klea apps.

These helpers are intentionally free of any nicegui import so an app’s ui/web/app.py can call them before the NiceGUI machinery is imported (NiceGUI reads NICEGUI_STORAGE_PATH at import time).

File: klea_utils/ui/web/nicegui/entry.py

Copyright 2026 Ankur Sinha Author: Ankur Sinha <sanjay DOT ankur AT gmail DOT com>

klea_utils.ui.web.nicegui.entry.app_name_from_argv(default: str) → str[source]

Return the --app-name flag value from sys.argv, else default.

Parameters:

default – App name to fall back to.

Returns:

The parsed --app-name value, or default.

klea_utils.ui.web.nicegui.entry.default_storage_env(app_name: str) → str[source]

Point NICEGUI_STORAGE_PATH at the per-app data dir when unset.

Called at the very top of each app’s ui/web/app.py, before anything imports NiceGUI: nicegui/storage.py honours NICEGUI_STORAGE_PATH at import time, so setting it early gives the storage subsystem the correct per-app directory from the start.

Parameters:

app_name – App name (overridable via --app-name) used for the platformdirs data directory.

Returns:

The resolved NICEGUI_STORAGE_PATH value.

Reusable custom NiceGUI widgets for Klea web interfaces.

The widgets now live in klea_utils.ui.web.nicegui.components (ADR-0031); this module re-exports them so existing imports keep working.

File: klea_utils/ui/web/nicegui/widgets.py

Copyright 2026 Ankur Sinha Author: Ankur Sinha <sanjay DOT ankur AT gmail DOT com>

class klea_utils.ui.web.nicegui.widgets.ChatBubble(text: str, stamp: str, role: str, collapsed: bool, idx: int, header: str = '', mime: str = '', data: str = '', meta: dict | None = None, is_error: bool = False, on_expand=None, on_copy=None)[source]

Bases: Element

A full-width chat transcript block with built-in actions.

All roles span the full chat width (opencode-style); the surface colour distinguishes them (semantic chat-bubble--<role> classes in theme.py), so user blocks read as tinted prompts, agent blocks as plain document text, and tool blocks as neutral blocks. Each block has an optional header, collapsible text content, a timestamp, a copy button, and an expand / collapse toggle – all flowing naturally inside the block (no CSS hacks).

Usage inside a @ui.refreshable:

ChatBubble(
    text="Hello", stamp="12:00", role="user",
    collapsed=False, idx=0,
    on_expand=lambda: print("toggle"),
    on_copy=lambda: print("copy"),
)

Shared components

Shared page context for Klea NiceGUI components.

A PageContext carries everything the reusable components need to coordinate without being assembled into a single closure: the page configuration, the mutable runtime state (active chat, expand state, streaming flag), the NiceGUI element references, and the cross-component callbacks. Components write into the context at attach time; handlers read it at event time, after the whole page has been assembled.

File: klea_utils/ui/web/nicegui/components/context.py

Copyright 2026 Ankur Sinha Author: Ankur Sinha <sanjay DOT ankur AT gmail DOT com>

class klea_utils.ui.web.nicegui.components.context.PageContext(server_url: str, user_id: str, title: str = 'Klea', subtitle: str = '', disclaimer: str = '', footer_text: str = 'Powered by <a href="https://github.com/neuroml/klea">Klea</a>', chat_id: str = '', stream_tasks: dict[str, ~typing.Any] = <factory>, retry_cbs: dict[str, ~typing.Any] = <factory>, mini_state: bool = True, status_models_expanded: bool = False, query_extra: dict[str, ~typing.Any] = <factory>, command_catalogue: ~typing.Any = None, dark: ~typing.Any = None, left_drawer: ~typing.Any = None, toggle_icon: ~typing.Any = None, center_panels: ~typing.Any = None, chat_area: ~typing.Any = None, scroll_area: ~typing.Any = None, turn_status_container: ~typing.Any = None, interrupt_container: ~typing.Any = None, input_row: ~typing.Any = None, turn_status_label: ~typing.Any = None, text: ~typing.Any = None, loading_row: ~typing.Any = None, inspector_container: ~typing.Any = None, inspector_scroll: ~typing.Any = None, dialog_container: ~typing.Any = None, render_chat_area: ~collections.abc.Callable[[], None] = <function _noop>, scroll_chat_bottom: ~collections.abc.Callable[[], None] = <function _noop>, refresh_chat_list: ~collections.abc.Callable[[...], ~typing.Any] = <function _noop>, refresh_status_pane: ~collections.abc.Callable[[...], ~typing.Any] = <function _noop>, refresh_inspector: ~collections.abc.Callable[[...], ~typing.Any] = <function _noop>, begin_inspector_section: ~collections.abc.Callable[[...], None] = <function _noop_args>, append_inspector: ~collections.abc.Callable[[...], None] = <function _noop_args>, reset_center_tab: ~collections.abc.Callable[[], None] = <function _noop>, refresh_send_state: ~collections.abc.Callable[[], None] = <function _noop>, refresh_turn_status: ~collections.abc.Callable[[], None] = <function _noop>, refresh_stream_button: ~collections.abc.Callable[[], None] = <function _noop>, submit_interrupt: ~collections.abc.Callable[[...], ~typing.Any] = <function _noop_args>, fetch_model_info: ~collections.abc.Callable[[], ~typing.Any] | None = None, fetch_session_model_info: ~collections.abc.Callable[[], ~typing.Any] | None = None, fetch_credentials: ~collections.abc.Callable[[], ~typing.Any] | None = None, model_config_dialog: ~collections.abc.Callable[[], ~typing.Any] | None = None, session_model_info: dict[str, ~typing.Any] = <factory>, credentials: list[dict[str, ~typing.Any]] = <factory>, status_extras: list[~collections.abc.Callable[[], ~typing.Any]] = <factory>, chat_created_hooks: list[~collections.abc.Callable[[~typing.Any], None]] = <factory>, switch_chat: ~collections.abc.Callable[[str], None] = <function _noop_arg>, new_chat: ~collections.abc.Callable[[], None] = <function _noop>, rename_chat: ~collections.abc.Callable[[str, str], None] = <function _noop_args>, delete_chat: ~collections.abc.Callable[[str], None] = <function _noop_arg>, show_model_dialog: ~collections.abc.Callable[[], None] = <function _noop>, context_hooks: list[~collections.abc.Callable[[~typing.Any], None]] = <factory>, ensure_active_chat: ~collections.abc.Callable[[], str] = <function _noop_str>, exit_app: ~collections.abc.Callable[[], None] = <function _noop>)[source]

Bases: object

Shared mutable state and element/callback registry for a Klea page.

Components (components/*.py) attach into this object: they read the configuration, keep their mutable state here, and register the element references and cross-component callbacks they need. The page assembly creates one context, attaches all components, then lets handlers resolve references at event time.

Warning

Do not create NiceGUI elements from a ``nicegui.background_tasks`` task. NiceGUI resolves a new element’s parent from a per-task slot stack; a background task has an empty stack and raises “The current slot cannot be determined because the slot stack for this task is empty”. Either wire the work as an async event handler (NiceGUI awaits it in the client/slot context, so ui.dialog() etc. work), or enter a slot explicitly before creating elements, e.g. with ctx.turn_status_container: (the turn status region) or with ctx.dialog_container: (dialogs opened from a background task). Updating existing elements and app.storage from a background task is fine.

Parameters:
  • server_url – Base URL of the backend API server.

  • user_id – Opaque persistent user identifier.

  • title – Bold application title in the header bar.

  • subtitle – Optional smaller text shown next to title.

  • disclaimer – Optional text shown in the footer.

  • footer_text – HTML content for the footer bar.

  • chat_id – Active chat conversation identifier.

append_inspector(**_kwargs: Any) → None

No-op default for callbacks that take arguments.

begin_inspector_section(**_kwargs: Any) → None

Incremental inspector updates: open a new query section (chat_id, marker) and append one entry (chat_id, entry) without a full rebuild.

chat_created_hooks: list[Callable[[Any], None]]

Hooks invoked with a chat’s data dict when it is first created, so an app context control can carry a selection made before the first message (e.g. operating mode / access level in query_extra) onto that chat.

chat_is_streaming(chat_id: str) → bool[source]

Return whether chat_id has an active run on this page.

Reads the per-chat registry (the chat_id is scoped by this page’s user_id), not the display state in the chats store.

Parameters:

chat_id – Chat conversation identifier.

Returns:

True when a live run is registered for the chat.

command_catalogue: Any = None

Merged command catalogue (the frontend’s client commands plus the server catalogue fetched from GET /commands); None until the initial load fetches it (ADR-0047).

context_hooks: list[Callable[[Any], None]]

Hooks invoked with a chat’s data dict whenever a graph context event updates its session context, so a context control (operating mode / tool access) can reconcile its per-chat preference with the checkpointed value the server just reported (e.g. after a /mode command).

delete_chat() → None

Request deletion of a chat session (chat_id): opens a confirmation dialog, then deletes on confirm. Registered by the chat-list component and used by the client /sessions delete command (ADR-0047).

dialog_container: Any = None

Hidden container used as an explicit slot when building dialogs from a context that has no ambient slot (e.g. the first-run prompt runs in a background task). See the slot warning in the class docstring.

ensure_active_chat() → str

Return the active chat id, creating and registering one (server-side creation, chat-list refresh, created-hooks) when none is active. Registered by the input area; lets client-command output (e.g. /help) land in a chat even when the first input is a command (ADR-0047).

exit_app() → None

Stop the frontend and the server it spawned: stops any active runs, then shuts the NiceGUI server down. Registered by the header component and used by the header’s exit button and the /exit client command (ADR-0047).

input_row: Any = None

The composer row (input + send), hidden while a HITL ask is active.

inspector_container: Any = None

Stable inspector pane content column / scroll area (incremental append).

interrupt_container: Any = None

Pinned container for the HITL ask form, between the transcript and the composer: while an ask is active it replaces the input row (ADR-0046).

new_chat() → None

Create and switch to a new chat session (registered by the chat-list component; used by the client /new command, ADR-0047).

refresh_chat_list() → None

No-op default for callbacks that are not yet/never registered.

refresh_inspector() → None

No-op default for callbacks that are not yet/never registered.

refresh_send_state() → None

No-op default for callbacks that are not yet/never registered.

refresh_status_pane() → None

No-op default for callbacks that are not yet/never registered.

refresh_stream_button() → None

Flip the send control between Send and Stop based on the current chat’s run state (registered by the input area; called by the stream component).

refresh_turn_status() → None

Re-render the turn status region from the active chat’s turn status. Registered by the chat-area component; the stream component calls it on each status transition.

rename_chat(**_kwargs: Any) → None

Rename a chat session (chat_id, name); registered by the chat-list component and used by the client /rename command (ADR-0047).

render_chat_area() → None

No-op default for callbacks that are not yet/never registered.

reset_center_tab() → None

No-op default for callbacks that are not yet/never registered.

scroll_chat_bottom() → None

No-op default for callbacks that are not yet/never registered.

show_model_dialog() → None

Open the model/credential dialog from a synchronous caller; registered by the model-dialog component and used by the client /model command (ADR-0047).

status_models_expanded: bool = False

Whether the status pane’s Models group is expanded; kept here so the state survives the pane’s refreshes (which rebuild the element).

stream_tasks: dict[str, Any]

the asyncio task driving each chat’s run and its Retry callback. Page-scoped (the tasks are bound to this page’s event loop) and independent of the process-global chats store, so several chats can run at once.

Type:

Per-chat run registry, keyed by "{user_id}:{chat_id}"

submit_interrupt(**_kwargs: Any) → None

Submit a HITL interrupt answer/cancel from the status-region form (response, interrupt_id, cancel); registered by the input area, called by the chat area’s form (ADR-0046).

switch_chat() → None

No-op default for one-argument callbacks (e.g. chat switching).

turn_status_label: Any = None

Live progress label inside the turn status region, updated in place during a run (the region itself is re-rendered from per-chat state).

NiceGUI server bootstrap for Klea pages.

Shared, app-agnostic boilerplate: process logging, per-app NiceGUI storage path, the / page handler (which resolves the per-browser identity and delegates the actual composition to a page builder supplied by the caller), and nicegui.ui.run().

The page layout itself is not defined here – the caller passes the page_builder callable so each app composes the page from klea_utils.ui.web.nicegui.components (ADR-0031).

File: klea_utils/ui/web/nicegui/components/bootstrap.py

Copyright 2026 Ankur Sinha Author: Ankur Sinha <sanjay DOT ankur AT gmail DOT com>

klea_utils.ui.web.nicegui.components.bootstrap.default_favicon_path() → Path | None[source]

Return the bundled Klea favicon path, or None when absent.

The caller passes the result to NiceGUI’s favicon; None leaves NiceGUI’s own default icon in place.

async klea_utils.ui.web.nicegui.components.bootstrap.resolve_initial_chat(*, resume: str = '', continue_session: bool = False, server_url: str = '', user_id: str = '') → str[source]

Return the chat id the page should open on load.

--resume <chat_id> wins; else --continue opens the most recently updated chat for the user (GET /chat/{user_id} is ordered most-recent first); else "" so the frontend starts a new chat.

Parameters:
  • resume – The --resume chat id, or "".

  • continue_session – Whether --continue was given.

  • server_url – Base URL of the backend API server.

  • user_id – The resolved user id.

Returns:

The chat id to open, or "" for a fresh chat.

klea_utils.ui.web.nicegui.components.bootstrap.run_nicegui_server(title: str, server_url: str, *, page_builder: Callable[[...], Any], subtitle: str = '', disclaimer: str = '', footer_text: str = 'Powered by <a href="https://github.com/neuroml/klea">Klea</a>', reload: bool = False, nicegui_url: str = '0.0.0.0:7860', storage_secret: str = 'klea-nicegui-secret-change-me', app_name: str = 'klea-agent-web', favicon: str | Path | None = None, user_id: str = '', identity_app_name: str = 'klea-agent', resume: str = '', continue_session: bool = False, open_browser: bool = True) → None[source]

Start the NiceGUI web server with a Klea page.

This is the process-level entry point for a Klea frontend. It registers a @ui.page("/") handler that resolves the per-browser user_id and delegates page composition to page_builder (signature (chat_id, user_id, server_url, title, subtitle, disclaimer, footer_text)), then starts the NiceGUI server.

Backend readiness is handled by the page itself: the layout is delivered immediately and the health probe + chat hydration run as a background task, so main_page returns within response_timeout even on a cold start.

Parameters:
  • title – Application title (displayed in the header and browser tab).

  • server_url – Base URL of the backend API server (e.g. http://127.0.0.1:8005).

  • page_builder – Callable that composes the page layout.

  • subtitle – Optional smaller text shown next to title.

  • disclaimer – Optional text shown below the chat input.

  • footer_text – HTML content for the footer bar.

  • reload – When True, enable NiceGUI’s file-watch hot reload.

  • nicegui_url – host:port to bind the NiceGUI web server to.

  • storage_secret – Secret used by NiceGUI for browser session persistence.

  • app_name – Log identity for this frontend process, used as the log file name so each app keeps its own logs.

  • favicon – Browser-tab favicon (a file path, URL, raw SVG or a single character). Defaults to the bundled Klea icon.

  • user_id – Explicit --user-id override for the page identity.

  • identity_app_name – Canonical identity key (the server app, e.g. "klea-agent") for the shared host-local identity file.

  • resume – Chat id to open on load (--resume), or "".

  • continue_session – Open the most recently updated chat on load (--continue).

  • open_browser – When True, open the UI in the user’s default browser once the port is ready (NiceGUI’s show; uses the portable webbrowser module). Pass False for headless/remote use.

NiceGUI user-storage helpers for Klea pages.

Covers the persistent per-browser identity (app.storage.user) and defends against the stale-session case where NiceGUI raises AssertionError because the backing storage-user-*.json file was lost while the browser still sends the old session cookie.

File: klea_utils/ui/web/nicegui/components/storage.py

Copyright 2026 Ankur Sinha Author: Ankur Sinha <sanjay DOT ankur AT gmail DOT com>

async klea_utils.ui.web.nicegui.components.storage.browser_user_id() → str[source]

Return the persistent per-browser user_id, creating it if missing.

The multi-user fallback: identity comes from the browser’s signed session cookie (app.storage.user), so each browser/profile is a distinct user. Must be called before any await so app.storage.user is still in the request context.

Returns:

The stored (or freshly generated) user_id string.

klea_utils.ui.web.nicegui.components.storage.client_is_loopback() → bool[source]

Return whether the current request’s client is on a loopback host.

Guards the shared local identity file: a remote browser must never be handed the machine-local user_id (it would expose the local user’s chats). A None client (no request context) is treated as not loopback.

Returns:

True when the request client host is loopback.

async klea_utils.ui.web.nicegui.components.storage.ensure_user_storage()[source]

Return app.storage.user, recreating it if stale.

When the .nicegui/storage-user-*.json file is missing but the browser still sends the old session cookie, app.storage.user raises AssertionError. We warn and recreate the backing FilePersistentDict for that session_id so the page can continue with a fresh user_id instead of 500.

async klea_utils.ui.web.nicegui.components.storage.resolve_page_user_id(*, explicit: str = '', server_url: str = '', identity_app_name: str = 'klea-agent') → str[source]

Resolve the page’s user_id through the shared identity seam.

Order: an explicit --user-id / KLEA_USER_ID > the canonical host-local identity file (only when both the backend URL and the requesting browser are loopback, so the TUI and web share one identity for a local single-user setup) > the per-browser cookie (remote / multi-user).

Parameters:
  • explicit – The --user-id value, or "".

  • server_url – Base URL of the backend API server.

  • identity_app_name – Canonical identity key (the server app, e.g. "klea-agent").

Returns:

The resolved user id.

klea_utils.ui.web.nicegui.components.storage.safe_set_user(key: str, value: Any) → None[source]

Set app.storage.user[key] if storage is available, else warn.

klea_utils.ui.web.nicegui.components.storage.user_storage_or_none()[source]

Return app.storage.user or None if stale (no await).

Page theme component: CSS overrides and persistent dark mode.

See devdocs/system/web-theming.md.

File: klea_utils/ui/web/nicegui/components/theme.py

Copyright 2026 Ankur Sinha Author: Ankur Sinha <sanjay DOT ankur AT gmail DOT com>

klea_utils.ui.web.nicegui.components.theme.install_theme(ctx: PageContext) → None[source]

Install the page CSS overrides and persistent dark mode.

Binds the dark-mode flag to app.storage.user["dark_mode"] when storage is available, and stores the resulting ui.dark_mode element on ctx.dark for the header toggle.

Parameters:

ctx – The shared page context.

SSE stream handling component for Klea pages.

The pure event-to-state reducer moved to klea_utils.ui.stream_events (shared with the terminal client); this module keeps the UI-driving coroutines (run_stream(), stop_stream()) that consume the backend’s /query/stream events and update the chat panel, status pane and inspector.

See devdocs/system/streams.md and devdocs/system/api-sse-sequence.md.

File: klea_utils/ui/web/nicegui/components/stream.py

Copyright 2026 Ankur Sinha Author: Ankur Sinha <sanjay DOT ankur AT gmail DOT com>

async klea_utils.ui.web.nicegui.components.stream.run_stream(ctx: PageContext, query: str, chat_id: str, resume: bool = False, interrupt_response: dict[str, Any] | None = None, interrupt_id: str | None = None, interrupt_cancel: bool = False) → None[source]

Stream a query’s events into the UI for chat_id.

Shows a progress row while streaming, commits the final answer and inspector data on completion, and surfaces errors inline (with a Retry action when the run can be resumed from its checkpoint). When the run pauses for human input it renders the ask (the status region builds the form) and stops streaming until the user answers or cancels.

Parameters:
  • ctx – The shared page context.

  • query – The user’s query text (unused when resuming/answering).

  • chat_id – Chat conversation identifier.

  • resume – Resume the chat’s last failed run instead of starting a new turn (the query is not resent).

  • interrupt_response – Answer a pending HITL interrupt (ADR-0046).

  • interrupt_id – Id of the interrupt being answered.

  • interrupt_cancel – Cancel the pending interrupt instead of answering.

klea_utils.ui.web.nicegui.components.stream.stop_all_streams(ctx: PageContext) → None[source]

Stop every active run on this page (used when exiting the frontend).

Cancels each chat’s run_stream task and fires the best-effort cancel request, so a reused/remote server does not keep running after the page goes away.

Parameters:

ctx – The shared page context.

klea_utils.ui.web.nicegui.components.stream.stop_stream(ctx: PageContext, chat_id: str) → None[source]

Stop chat_id’s active run: cancel locally and ask the server to cancel.

Cancels the chat’s run_stream task (closing the SSE connection) and fires a best-effort /query/cancel so the server stops the graph run even if the disconnect alone would not. Idempotent and safe to call when the chat is not streaming.

Parameters:
  • ctx – The shared page context.

  • chat_id – Chat whose run should stop.

Compact single-choice control for the NiceGUI status pane.

A q-btn-group (connected buttons on one line) is used rather than ui.toggle because each option can carry its own active colour (for example destructive = red, scientific = green), which a q-btn-toggle cannot express. Labels and button groups use fixed widths so that stacked controls line up regardless of their text lengths.

File: klea_utils/ui/web/nicegui/components/choice.py

Copyright 2026 Ankur Sinha Author: Ankur Sinha <sanjay DOT ankur AT gmail DOT com>

klea_utils.ui.web.nicegui.components.choice.DEFAULT_GROUP_WIDTH = '10rem'

Fixed width for the button group, so controls with different option lengths (e.g. General | Scientific vs Full | Read-only) match.

klea_utils.ui.web.nicegui.components.choice.DEFAULT_LABEL_WIDTH = '13ch'

Fixed width for the row label (fits the longest expected label, e.g. "Project root:"), so labels of different lengths stay aligned.

klea_utils.ui.web.nicegui.components.choice.choice_buttons(label: str, options: dict[str, str], selected: str, on_select: Callable[[str], None], *, colors: dict[str, str] | None = None, tooltips: dict[str, str] | None = None, info: str = '', label_width: str = '13ch', group_width: str = '10rem') → None[source]

Render a compact, aligned segmented single-choice control.

The selected option is filled with its colour (or primary) and white text; the others are drawn as outlines. Buttons are dense/size=sm and share the group width equally, so the control stays close to the surrounding label text and two stacked controls align.

The choice-row / choice-label / choice-btn classes are styled in klea_utils.ui.web.nicegui.components.theme so the inactive option stays legible in dark mode (its outline follows its text colour).

Parameters:
  • label – Row label (e.g. "Mode:").

  • options – {value: label} mapping; iteration order is display order.

  • selected – The currently selected value.

  • on_select – Callback invoked with the newly selected value.

  • colors – Optional {value: quasar-colour} for the active state (e.g. {"full": "red-5", "read_only": "green-5"}).

  • tooltips – Optional {value: text} tooltips per option.

  • info – Optional explanatory tooltip attached to the whole group (e.g. an operating-mode downgrade note).

  • label_width – CSS width for the label (default DEFAULT_LABEL_WIDTH); pass the same value for stacked controls to align them.

  • group_width – CSS width for the button group (default DEFAULT_GROUP_WIDTH); pass the same value for stacked controls to align them.

Session-command handling for the web frontend (ADR-0047).

The web frontend owns its client-side commands and merges them with the server catalogue fetched from GET /commands. An input beginning with / is validated against the merge: a client command runs locally and its output is rendered as a system chat bubble; a server command is forwarded as the query (the graph’s command node runs it); an unknown command is rejected locally and never sent.

File: klea_utils/ui/web/nicegui/components/commands.py

Copyright 2026 Ankur Sinha Author: Ankur Sinha <sanjay DOT ankur AT gmail DOT com>

klea_utils.ui.web.nicegui.components.commands.WEB_CAPABILITIES: frozenset[str] = frozenset({'file_picker'})

it cannot read the user’s filesystem or run a shell, but it can show a file picker / upload.

Type:

Capabilities the browser frontend has (ADR-0047)

klea_utils.ui.web.nicegui.components.commands.attach_autocomplete(ctx: PageContext, text: Any, suggestions: Any) → None[source]

Attach a /-command autocomplete list to the chat input (ADR-0047).

As the user types /prefix, suggestions (a container above the input) is populated with the matching commands from the merged catalogue; clicking one inserts /name followed by a space into the input.

Parameters:
  • ctx – The page context (its command_catalogue backs the list).

  • text – The chat ui.textarea element.

  • suggestions – The container element the suggestions render into.

klea_utils.ui.web.nicegui.components.commands.build_catalogue(server_commands: list[dict[str, Any]]) → CommandRegistry[source]

Merge the client commands with the server catalogue (ADR-0047).

Parameters:

server_commands – Command metadata dicts from GET /commands.

Returns:

The merged registry (client + server).

klea_utils.ui.web.nicegui.components.commands.client_commands() → CommandRegistry[source]

Return the web frontend’s own client-side commands (ADR-0047).

Returns:

A registry holding the commands the browser handles itself.

klea_utils.ui.web.nicegui.components.commands.handle_command(ctx: PageContext, text: str) → bool[source]

Handle text as a command if it is one; return True when consumed.

A client command (or an unknown / parse-error command) is handled locally and rendered as a system bubble. A server command returns False so the caller forwards it as the query; a non-command also returns False.

Parameters:
  • ctx – The page context.

  • text – The raw input.

Returns:

True when the input was consumed as a client command.

Chat message block widget.

File: klea_utils/ui/web/nicegui/components/chat_bubble.py

Copyright 2026 Ankur Sinha Author: Ankur Sinha <sanjay DOT ankur AT gmail DOT com>

class klea_utils.ui.web.nicegui.components.chat_bubble.ChatBubble(text: str, stamp: str, role: str, collapsed: bool, idx: int, header: str = '', mime: str = '', data: str = '', meta: dict | None = None, is_error: bool = False, on_expand=None, on_copy=None)[source]

Bases: Element

A full-width chat transcript block with built-in actions.

All roles span the full chat width (opencode-style); the surface colour distinguishes them (semantic chat-bubble--<role> classes in theme.py), so user blocks read as tinted prompts, agent blocks as plain document text, and tool blocks as neutral blocks. Each block has an optional header, collapsible text content, a timestamp, a copy button, and an expand / collapse toggle – all flowing naturally inside the block (no CSS hacks).

Usage inside a @ui.refreshable:

ChatBubble(
    text="Hello", stamp="12:00", role="user",
    collapsed=False, idx=0,
    on_expand=lambda: print("toggle"),
    on_copy=lambda: print("copy"),
)
klea_utils.ui.web.nicegui.components.chat_bubble.GLYPHS = GlyphSet(success='✔', failure='✘', pending='⋯', agent='◆', prompt='❯', separator='│', overflow='…')

Browser output is always Unicode-capable, so the web reuses the TUI’s glyph table directly (the terminal detection / ASCII fallback does not apply); one shared source keeps the role markers identical in both frontends.

Chat area component: message list, scroll, and empty-state.

Builds the scroll area that holds the message bubbles and the stream progress container, and registers the render/scroll callbacks on the page context.

File: klea_utils/ui/web/nicegui/components/chat_area.py

Copyright 2026 Ankur Sinha Author: Ankur Sinha <sanjay DOT ankur AT gmail DOT com>

klea_utils.ui.web.nicegui.components.chat_area.attach_chat_area(ctx: PageContext) → None[source]

Build the chat scroll area and register its render callbacks.

Must be called with the chat tab panel as the ambient NiceGUI context (the surrounding layout decides where the tab panels go).

Parameters:

ctx – The shared page context; chat_area, scroll_area and turn_status_container are filled in here.

klea_utils.ui.web.nicegui.components.chat_area.render_turn_status(ctx: PageContext, retry_cb=None) → None[source]

Render the turn status region for the active chat from its state.

The region is a single slot under the transcript, owned by the chat’s status dict (state.ChatData), so it is restored on chat switch and replaced wholesale on the next turn. kind selects the content:

  • progress - a spinner plus the live heading.

  • error - the message and, when resumable, a Retry action.

  • stopped - a plain “Stopped” line.

  • idle/missing - nothing.

Parameters:
  • ctx – The shared page context (uses turn_status_container).

  • retry_cb – Callable invoked by the Retry button; the stream component passes it when rendering a live error.

Chat list component: left drawer with sessions and session management.

Holds the session list, chat switching / renaming / pinning / deletion, the delete-user-session flow, and the drawer rail toggle. All identity and active-chat reads go through the shared PageContext, so a user-identity reset (delete all data) takes effect immediately for every handler without a closure rebind.

File: klea_utils/ui/web/nicegui/components/chat_list.py

Copyright 2026 Ankur Sinha Author: Ankur Sinha <sanjay DOT ankur AT gmail DOT com>

klea_utils.ui.web.nicegui.components.chat_list.attach_chat_list(ctx: PageContext) → None[source]

Build the left drawer (session rail) and register its handlers.

Registers refresh_chat_list and switch_chat on the context.

Parameters:

ctx – The shared page context; left_drawer and toggle_icon are filled in here.

Header component: title bar, dark-mode toggle and exit button.

File: klea_utils/ui/web/nicegui/components/header.py

Copyright 2026 Ankur Sinha Author: Ankur Sinha <sanjay DOT ankur AT gmail DOT com>

klea_utils.ui.web.nicegui.components.header.attach_header(ctx: PageContext) → None[source]

Build the page header: title, subtitle, dark-mode toggle, exit button.

Relies on ctx.dark being set by theme.install_theme() before this is called. Registers ctx.exit_app: it stops this page’s active runs, then shuts the NiceGUI server down (which, for a client-spawned server, ends the subprocess via the CLI’s process-group teardown). ctx.exit_app is the immediate action used by the /exit client command; the header’s exit button confirms first, since an accidental click would stop the server for every open tab.

Parameters:

ctx – The shared page context.

Footer component: disclaimer and footer text on the footer band.

File: klea_utils/ui/web/nicegui/components/footer.py

Copyright 2026 Ankur Sinha Author: Ankur Sinha <sanjay DOT ankur AT gmail DOT com>

Build the page footer: disclaimer and footer text on one row.

Both share the footer band’s single line, centred as disclaimer | footer_text, so the disclaimer no longer needs a line below the chat input and the input can sit flush at the bottom of the scroll area. The row wraps on narrow screens.

Parameters:

ctx – The shared page context (supplies disclaimer and footer_text).

Inspector panel component: streamed inspect entries for the active chat.

Entries arrive as the graph runs. The pane appends them incrementally (no full rebuild per node) and groups them into a collapsible section per query, marked by the query text and time. Sections are kept for the browser session so a chat accumulates its inspection history.

File: klea_utils/ui/web/nicegui/components/inspector.py

Copyright 2026 Ankur Sinha Author: Ankur Sinha <sanjay DOT ankur AT gmail DOT com>

klea_utils.ui.web.nicegui.components.inspector.attach_inspector_panel(ctx: PageContext) → None[source]

Build the inspector panel for the active chat in the inspect tab.

Must be called with the inspect tab panel as the ambient NiceGUI context. Builds the scroll area and content column once (so entries can be appended incrementally) and registers the refresh/append callbacks on the context.

Parameters:

ctx – The shared page context.

Inspector code block: JSON highlighting and copy button for inspect details.

ui.code renders its content through a markdown2 fenced block, which breaks when the content itself contains a code fence (the fence is closed early and the rest is rendered as markdown). This module renders the formatted details directly with Pygments’ JSON lexer, keeping ui.code’s code box and copy button without the markdown indirection.

File: klea_utils/ui/web/nicegui/components/inspector_code.py

Copyright 2026 Ankur Sinha Author: Ankur Sinha <sanjay DOT ankur AT gmail DOT com>

klea_utils.ui.web.nicegui.components.inspector_code.CODE_HL_CLASS = 'inspector-code-hl'

CSS class of the Pygments wrapper; theme.py serves its token colours.

klea_utils.ui.web.nicegui.components.inspector_code.highlight_details(text: str) → str[source]

Return text highlighted as JSON (HTML).

Uses Pygments’ stock JSON lexer (object keys as Name.Tag, string values as String.Double); it already tolerates the real newlines inside string values that format_details() produces.

Parameters:

text – The formatted details string.

Returns:

HTML with Pygments token spans; theme.py serves the matching .{CODE_HL_CLASS} colours.

klea_utils.ui.web.nicegui.components.inspector_code.render_inspector_code(text: str) → None[source]

Render a code box with highlighted text and a copy button.

Mirrors ui.code (box, JSON highlighting, copy button) without its markdown2 fenced block, so content containing code fences or multi-line values renders intact.

Parameters:

text – The formatted details string.

Status pane component: the right drawer with chat state details.

Shows the chat name, per-role model summary (with a settings button opening the model-config dialog), token usage totals, and the per-node status sections streamed by the graph. Everything up to (and including) the token usage stays visible: it is a sticky header, so the state sections below scroll under it (mirroring the TUI status panel).

File: klea_utils/ui/web/nicegui/components/status_pane.py

Copyright 2026 Ankur Sinha Author: Ankur Sinha <sanjay DOT ankur AT gmail DOT com>

klea_utils.ui.web.nicegui.components.status_pane.attach_status_pane(ctx: PageContext) → None[source]

Build the right drawer (status pane) and register its refresh.

Must be called with ctx.model_config_dialog already registered (by model_dialog.attach_model_info()), since the pane’s settings button invokes it.

Parameters:

ctx – The shared page context; refresh_status_pane is set here.

Model configuration component: per-session / per-chat model overrides and the provider credentials editor.

Models are selected per role (chat/plan/guard/…). The dialog edits the per-session defaults when no chat is active and the current chat’s overrides otherwise; saving in a chat also promotes the changed role into the session defaults (“last used”). API keys are not part of the model override — they are provider-scoped and edited in a separate, masked (write-only) credentials dialog.

Each role’s model is picked with a provider field, a model field and an optional custom URL, all filled from the models.dev catalogue and joined back into a single model string on save.

File: klea_utils/ui/web/nicegui/components/model_dialog.py

Copyright 2026 Ankur Sinha Author: Ankur Sinha <sanjay DOT ankur AT gmail DOT com>

klea_utils.ui.web.nicegui.components.model_dialog.attach_model_info(ctx: PageContext) → None[source]

Register the model-info fetches and the model-config dialog.

Parameters:

ctx – The shared page context; fetch_model_info, fetch_session_model_info and model_config_dialog are set here.

Chat input component: text area, send handling, and stream kick-off.

File: klea_utils/ui/web/nicegui/components/input_area.py

Copyright 2026 Ankur Sinha Author: Ankur Sinha <sanjay DOT ankur AT gmail DOT com>

klea_utils.ui.web.nicegui.components.input_area.attach_input(ctx: PageContext) → None[source]

Build the chat input row and wire send / Enter handling.

Must be called with the chat tab panel (below the chat area) as the ambient NiceGUI context. The input is disabled until initial_load.attach_initial_load() enables it after the backend health probe succeeds.

Parameters:

ctx – The shared page context; text is filled in here.

Initial-load component: backend readiness probe and chat hydration.

Runs in a background task after the layout is delivered so the page frame renders immediately even when the backend needs 30-60s to become ready (e.g. a cold HuggingFace container).

File: klea_utils/ui/web/nicegui/components/initial_load.py

Copyright 2026 Ankur Sinha Author: Ankur Sinha <sanjay DOT ankur AT gmail DOT com>

klea_utils.ui.web.nicegui.components.initial_load.attach_initial_load(ctx: PageContext) → None[source]

Schedule the backend probe + hydration in a background task.

Requires ctx.loading_row (the backend banner) and ctx.text (the chat input) to already be built.

Parameters:

ctx – The shared page context.

Text helpers

Human-readable formatting for streamed inspection details.

NodeStreamData.details is structured JSON (dict/list/scalar) carried over the inspect stream event. json.dumps escapes embedded newlines, so multi-line values such as prompts collapse into a single unreadable line. format_details() keeps the JSON shape but renders string values faithfully: embedded newlines become real line breaks, and a value that is itself a JSON object/array is inlined and pretty-printed. The output is plain text, for any frontend (the NiceGUI inspector, the TUI).

The structure, indentation and quote/backslash escaping are all delegated to json.dumps. Only the two value transformations are custom (see _prepare()), because json.dumps always escapes newlines.

File: klea_utils/ui/inspect_format.py

Copyright 2026 Ankur Sinha Author: Ankur Sinha <sanjay DOT ankur AT gmail DOT com>

klea_utils.ui.inspect_format.format_details(details: Any) → str[source]

Render structured inspect details as readable indented JSON.

The structure matches json.dumps(..., indent=2); only string values are treated differently:

  • embedded newlines are kept as real line breaks (at their original column, so the text is not altered), so multi-line prompts read as text;

  • a value that is itself a JSON object/array string is parsed and inlined as nested, pretty-printed JSON;

  • all other values (numbers, booleans, null, single-line strings such as a Pydantic repr) are rendered as normal JSON.

Parameters:

details – The details payload of an inspect event.

Returns:

A human-readable, indented multi-line string.

Bare-URL linkification for rendered text

Two variants: linkify_md() wraps bare URLs as Markdown links for a Markdown renderer, and linkify_html() escapes the text and wraps bare URLs in safe anchors for plain (non-Markdown) rendering.

File: klea_utils/ui/linkify.py

Copyright 2026 Ankur Sinha Author: Ankur Sinha <sanjay DOT ankur AT gmail DOT com>

klea_utils.ui.linkify.linkify_html(text: str) → str[source]

HTML-escape text and wrap bare URLs in safe <a> anchors.

For plain-text (non-markdown) rendering, e.g. user input: the text is escaped so it is shown verbatim (no Markdown decoration), while bare URLs become clickable links. The result is safe to pass to ui.html(sanitize=True).

Parameters:

text – The literal text to render.

Returns:

HTML with the text escaped and bare URLs wrapped in anchors.

klea_utils.ui.linkify.linkify_md(text: str) → str[source]

Convert bare URLs in markdown text to [url](url) links.

Existing [text](url) links are returned verbatim so they are not re-wrapped, and fenced code blocks (triple-backtick or tilde fences) are left untouched: a URL in code would otherwise be wrapped and the literal [url](url) shown inside the code span (fenced blocks are not parsed as markdown links). Callers pass the result to a markdown renderer, which turns the wrapped URLs into clickable anchors.

Parameters:

text – Markdown source text

Returns:

Text with bare URLs wrapped as [url](url)