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.
| # | Enhancement | Scope | Effort |
|---|---|---|---|
| 21a | Tool call collapsible disclosure | frontend | Small |
| 21b | Model / temp / context display | frontend | Small |
| 21c | Session search bar | frontend | Small |
| 21d | manage_library tool | frontend backend | Medium |
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.
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.
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,
}
Add ts?: string to the ToolCall type. Replace ToolChips with a ToolBlock component styled like ThinkingBlock:
▶ web_search (2 calls) — tool name + call count<details> per call showing [HH:MM:SS] → args … / [HH:MM:SS] ← result … in monospaceactive_tool behaviour unchanged)Applies to all tools: web_search, web_fetch, search_memory, search_library, search_papers, get_datetime, get_location.
useChatStream.ts tool assembly logic — just add ts passthrough when building the pending tool call record.Display the active model, temperature, and context window size in the same row as the token count and compact button.
selectedModel — already in App.tsx state (fetched from /api/settings/generator on mount)temperature, num_ctx — in config/generator.yaml, readable via GET /api/settings/generatorFetch /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.
Filter the session list by title above the "New chat" button — useful once sessions accumulate.
All client-side in SessionSidebar.tsx:
const [query, setQuery] = useState("")<input> above the "New chat" button (only shown when sidebar is open)sessions by s.title.toLowerCase().includes(query.toLowerCase()) before rendering the listNo backend changes. No new API calls.
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.
src/local/tools/library_agent_tool.py — follows *AgentTool naming convention (LLM-powered, Gemma-callable, result returns to Gemma via tool.result.*).
{
"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"]
}
documents.yaml — names + descriptions.{name, description} to documents.yaml, publish tool.schema so SearchLibraryTool re-registers the new collection with GeneratorAgent.library.ingest.complete with filename + chunk count.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)".
tool.call.consult_librarian / tool.result.consult_librariantool.activity.consult_librarianlibrary.ingest.complete — async completion signaltool.schema — re-published by SearchLibraryTool after new collection createdGateway 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.
list_collections() → list[dict] — return [{name, description, chunk_count}] from documents.yaml + ChromaDB countdelete_document(source_file: str, collection: str) → int — remove all chunks for a source filecreate_collection(name: str, description: str) — append entry to documents.yamlingest_file — already existsLibraryAgentTool 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.
TOOL_CALL_CONSULT_LIBRARIAN / TOOL_RESULT_CONSULT_LIBRARIAN to CHAT_OBSERVELIBRARY_INGEST_COMPLETE to CHAT_OBSERVE; translate to {type: "library_ingested", filename, chunks} for the frontend notificationLibraryAgentTool skeleton: bus wiring, consult_librarian tool schema published on startup, stub response so Gemma can call itDocumentService.list_collections() + delete_document(); librarian handles list and delete instructions end-to-enddocuments.yaml, publish library.collection.created → SearchLibraryTool re-publishes schemalibrary.ingest.complete bus event, gateway forwards to frontend, toast/status notification in UIsearch_memory