Source code for klea_utils.ui.web.nicegui.components.status_pane

#!/usr/bin/env python3
"""
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.

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

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

import json
import logging
from datetime import datetime

from nicegui import ui

from klea_utils.llm import parse_model_name
from klea_utils.ui.linkify import linkify_md
from klea_utils.ui.web.nicegui.components.context import PageContext
from klea_utils.ui.web.nicegui.state import chats

logger = logging.getLogger(__name__)


[docs] def attach_status_pane(ctx: PageContext) -> None: """Build the right drawer (status pane) and register its refresh. Must be called with ``ctx.model_config_dialog`` already registered (by :func:`model_dialog.attach_model_info`), since the pane's settings button invokes it. :param ctx: The shared page context; ``refresh_status_pane`` is set here. """ with ( ui.right_drawer(value=True) .props("width=420 bordered") .classes("overflow-y-auto overflow-x-hidden") ): @ui.refreshable def _status_pane() -> None: """Render chat name, model info and state sections for the active chat.""" current_chat = chats.get(f"{ctx.user_id}:{ctx.chat_id}") if not current_chat: if ctx.chat_id: logger.debug("No chat found for %s, status pane empty", ctx.chat_id) return with ui.column().classes("w-full gap-0"): with ui.row().classes("items-center w-full gap-0"): with ui.label(current_chat.get("name", "")).classes( "text-sm font-bold mb-0" ): created = current_chat.get("created", 0) if created: ui.tooltip( "Created: " + datetime.fromtimestamp(created) .astimezone() .strftime("%a %d %b %Y at %X") ) ui.space() with ( ui.button( icon="settings", on_click=ctx.model_config_dialog, ) .props("flat dense round color=grey-9") .classes("text-sm") ): ui.tooltip("Choose models") # App-defined content slot (e.g. an operating-mode # selector/badge, ADR-0030). Rendered inside the # refreshable pane, so it updates on pane refresh. if ctx.status_extra is not None: ctx.status_extra() model_info = current_chat.get("model_info", {}) if model_info: tooltip_parts: list[str] = [] display_parts: list[str] = [] for role, cfg in model_info.items(): raw = cfg.get("model", "") provider = cfg.get("provider", "") required = cfg.get("required", True) if not raw: # No model set for this role -- show a clear # placeholder so the user knows it is missing. display_short = "Not set" required_mark = " (required)" if required else "" tooltip_short = f"Not set{required_mark}" if cfg.get("overridden"): tooltip_short += " [User]" else: short = ( parse_model_name(raw).model_name if parse_model_name(raw) else raw ) if provider: tooltip_short = f"{short} ({provider})" else: tooltip_short = short if cfg.get("overridden"): tooltip_short += " [User]" display_short = short tooltip_parts.append(f"{role.capitalize()}: {tooltip_short}") display_parts.append(display_short) with ui.label(" | ".join(display_parts)).classes( "text-xs text-grey-5" ): if tooltip_parts: ui.tooltip("\n".join(tooltip_parts)).classes( "model-tooltip" ) token_usage = current_chat.get("token_usage", {}) has_token_usage = any(token_usage.values()) if has_token_usage: usage_display = ( f"{token_usage.get('input_tokens', 0)} in / " f"{token_usage.get('output_tokens', 0)} out" ) ui.label(usage_display).classes("text-xs text-grey-5 mb-0") sections = current_chat.get("state_sections", {}) has_content = bool(model_info) or bool(sections) or has_token_usage if not has_content: ui.label("State updates will appear here").classes( "text-sm text-gray-500" ) return if sections: ui.separator().classes("my-0.5") for node_label, section in sections.items(): with ( ui.element("details") .props("open") .classes("status-entry mb-2 w-full") ): with ( ui.element("summary").classes( "text-xs font-bold cursor-pointer w-full" ), ui.row().classes("w-full flex-nowrap items-center"), ): ui.label(section.get("heading", node_label)) display = section.get("display", "") if display: ui.markdown(linkify_md(display)).classes("text-xs w-full") summary = section.get("summary", "") if summary and not display: ui.label(summary).classes("text-xs text-grey-6 mb-1 w-full") details = section.get("details", {}) if details: with ui.element("details").classes( "status-details text-xs text-grey-5 cursor-pointer w-full" ): with ui.element("summary").classes("text-xs w-full"): ui.label("View details") ui.code( json.dumps(details, indent=2), language="json" ).classes("text-xs") _status_pane() ctx.refresh_status_pane = _status_pane.refresh