Phase 21 — Browser UI Enhancements

Four targeted improvements to the web UI. PySide6 and the web UI are parallel interfaces that coexist; these enhancements add capability to the browser experience, not replacements for the desktop app.

#EnhancementScopeEffort
21aTool call collapsible disclosurefrontendSmall
21bModel / temp / context displayfrontendSmall
21cSession search barfrontendSmall
21dmanage_library toolfrontend backendMedium

21a — Tool Call Collapsible Disclosure

Problem

The current ToolChips component renders tool calls inline as raw JSON chips — no timestamp, no running-log feel, inconsistent with the ThinkingBlock style. The PySide6 ToolWindow shows a timestamped → request / ← result log per tool.

Key insight

The gateway already subscribes to tool.call.* and tool.result.* and forwards them as tool_start / tool_result WebSocket events. No backend changes needed. The timestamp is on the MessageEnvelope but is not currently forwarded.

Backend change (ws_bridge.py)

Add ts to both tool_start and tool_result events:

if subject.startswith(_TOOL_CALL_PREFIX):
    tool_name = subject[len(_TOOL_CALL_PREFIX):]
    return {
        "type": "tool_start",
        "tool": tool_name,
        "args": payload.get("args", {}),
        "ts": envelope.timestamp_utc,          # ← add this
        "query_id": query_id,
    }

if subject.startswith(_TOOL_RESULT_PREFIX):
    tool_name = subject[len(_TOOL_RESULT_PREFIX):]
    return {
        "type": "tool_result",
        "tool": tool_name,
        "result": payload.get("result", ""),
        "sources": payload.get("sources", []),
        "ts": envelope.timestamp_utc,          # ← add this
        "query_id": query_id,
    }

Frontend change

Add ts?: string to the ToolCall type. Replace ToolChips with a ToolBlock component styled like ThinkingBlock:

Applies to all tools: web_search, web_fetch, search_memory, search_library, search_papers, get_datetime, get_location.

No changes to useChatStream.ts tool assembly logic — just add ts passthrough when building the pending tool call record.

21b — Model / Temp / Context Display

Goal

Display the active model, temperature, and context window size in the same row as the token count and compact button.

Data sources

Implementation

Fetch /api/settings/generator once on mount alongside the existing model fetch. Pass model, temperature, numCtx as props to TokenGauge (or render alongside it in App.tsx). Display format:

gemma4:e2b · 0.1 · 128k    1,234 tok  compact

All in the existing text-xs text-gray-500 row. No new API endpoint needed.

21c — Session Search Bar

Goal

Filter the session list by title above the "New chat" button — useful once sessions accumulate.

Implementation

All client-side in SessionSidebar.tsx:

No backend changes. No new API calls.

21d — Library Agent Tool

Goal

Add a Gemma-callable LLM-powered librarian agent that exposes library management through conversation. The user says "add this file to the library" — Gemma delegates the whole task in one call. The librarian figures out categorization, creates collections if needed, and ingests asynchronously so the conversation is never blocked. The PySide6 DocumentsWindow panel continues to exist as a parallel interface.

Participant: LibraryAgentTool

src/local/tools/library_agent_tool.py — follows *AgentTool naming convention (LLM-powered, Gemma-callable, result returns to Gemma via tool.result.*).

Gemma-facing tool schema

{
  "name": "consult_librarian",
  "description": "Delegate a library management task to the librarian agent. Use when the user wants to add a file to the library, list what is in the library, or delete a document. Pass the filename of any attachment and a plain-language instruction. The librarian handles categorisation, collection creation, and ingestion.",
  "parameters": {
    "instruction": {
      "type": "string",
      "description": "Plain-language task, e.g. 'add the attached PDF to the library' or 'list all collections' or 'delete OrganizationalBehavior.pdf'."
    },
    "filename": {
      "type": "string",
      "description": "Filename of the attached file, if any."
    }
  },
  "required": ["instruction"]
}

LibraryAgentTool internal flow

  1. List existing collections from documents.yaml — names + descriptions.
  2. Sample the file — first ~2000 tokens (title, opening pages, table of contents) from the attachment. Do NOT read the whole file.
  3. LLM call — ask a small model: "given these existing collections and this file sample, which collection does this belong to, or should a new one be created? If new, suggest a name and one-sentence description."
  4. Act on the decision:
  5. Return immediately to Gemma: "Ingesting filename into collection…" — Gemma tells the user it's underway.
  6. Ingest async — chunking/embedding runs in a background thread. On completion, publish library.ingest.complete with filename + chunk count.

Async completion notification

The gateway subscribes to library.ingest.complete and forwards it to the browser as a typed event. The frontend shows a brief notification: "✓ filename ingested (42 chunks)".

Bus subjects

Attachment routing

Gateway writes attachment bytes to a temp path on receipt; path is included in the query envelope. LibraryAgentTool reads from that path — avoids passing binary over ZMQ.

DocumentService additions needed

Schema re-publication after create_collection

LibraryAgentTool writes to documents.yaml then publishes library.collection.created. SearchLibraryTool subscribes, rebuilds its schema, and re-publishes on tool.schema. GeneratorAgent picks it up via its existing schema listener — no direct coupling between participants.

ws_bridge.py additions

Implementation Order

  1. 21c — session search bar (pure frontend) ✅
  2. 21b — model/temp/context display (pure frontend) ✅
  3. 21a — tool disclosure (one backend line + frontend component) ✅
  4. 21d-1LibraryAgentTool skeleton: bus wiring, consult_librarian tool schema published on startup, stub response so Gemma can call it
  5. 21d-2 — List and delete: DocumentService.list_collections() + delete_document(); librarian handles list and delete instructions end-to-end
  6. 21d-3 — Add with LLM categorisation: attachment temp-file routing, sample first ~2000 tokens, LLM picks or creates collection, write to documents.yaml, publish library.collection.createdSearchLibraryTool re-publishes schema
  7. 21d-4 — Async ingest + browser notification: embedding in background thread, library.ingest.complete bus event, gateway forwards to frontend, toast/status notification in UI
  8. 21d-5 — Reorganise: librarian surveys all collections and documents, proposes and executes moves; determine whether re-embedding is required (chunk ID includes collection — moving a document means deleting old chunks and re-ingesting into the target collection)

Out of Scope