Agentic UI Orchestration & Universal Search

Enterprise-grade feature that lets a user drive any of the ~80 suite applications from the Chat window, and search across all applications with a single interface.

User: "create a new customer named Jeff"
  └─► Chat WS (message_type 1)
        └─► LLM generates {"__ui_plan__": {...}} JSON
              └─► Backend validates plan (ops/app allowlists, value escaping)
                    └─► WS message_type 9 (UI_ACTION) β†’ frontend
                          └─► GBUiOrchestrator executes steps:
                                open window β†’ resolve field by label β†’
                                animated typing β†’ click β†’ submit

1. Message Types

IDNameDirectionPurpose
9UI_ACTIONserver β†’ clientJSON plan of UI steps to animate
  • Backend: botlib/src/message_types.rs (UI_ACTION = 9)
  • Frontend: botui/ui/suite/chat/chat-state.js (MessageType.UI_ACTION = 9)

2. UI Plan Protocol

The LLM emits a self-contained JSON object (mirroring the existing __tool_call__ mechanism) as the first thing in its response when the user requests a UI-level operation:

{"__ui_plan__": {"app": "crm", "steps": [
  {"op": "open", "app": "crm"},
  {"op": "click", "label": "New Lead"},
  {"op": "fill", "field": "First Name", "value": "Jeff"},
  {"op": "fill", "field": "Email", "value": "jeff@example.com"},
  {"op": "submit"}
]}}

Step operations

opargsdescription
openappOpen the app window via WindowManager
clicklabelClick element matching visible text
fillfield, valueType into field resolved by label/placeholder/name
selectfield, valueChoose option in a <select>
submitβ€”Submit the active form
waitmsPause execution

Backend validation (server/src/main_module/ui_plan.rs)

  • op must be in the allowlist above
  • app must exist in apps::registry::all_apps() (or be chat)
  • value/field/label stripped of HTML, truncated (≀ 500 chars value)
  • Plan capped at 32 steps
  • Invalid plans are logged + rejected (no client echo)

3. LLM Instruction Injection

exec.rs appends an β€œAgentic UI” instruction block to the system prompt for the web channel listing available apps (id + title + description) and the exact __ui_plan__ JSON contract. The instruction tells the LLM:

  • Emit the plan as the first line, then a short user-facing confirmation
  • Use only apps present in the list; use click/fill/select/submit
  • For search requests, emit {"__ui_plan__": {"op": "open", ...}} or let the Universal Search handle it

4. Streaming Interception (llm.rs)

In the stream loop, chunks containing "__ui_plan__" are accumulated separately (like __tool_call__), never sent as bot content. After the stream completes:

  1. Parse the plan JSON
  2. Validate via ui_plan.rs
  3. If valid β†’ send WS frame {message_type: 9, plan: {...}} to the client
  4. If invalid β†’ log + continue with normal text response

Backend endpoint

GET /api/ui/search?q=<query> (Bearer token, rate-limited)

Scans the following entity sources with ILIKE and returns a flat result set:

sourcetableapp
peoplepeoplepeople
crm contactscrm_contactscrm
productsproductsproducts
servicesservicesproducts
ticketsticketstickets
kb documentskb_documentsresearch
drive filesdrive_objectsdrive
botsbotsadmin

Response:

{"results": [
  {"app": "crm", "type": "contact", "id": "...", "title": "Jeff Bezos",
   "subtitle": "jeff@example.com", "url": "/suite/crm/crm.html"},
  ...
]}

Frontend (js/gb-search.js)

  • Search input in the desktop shell (taskbar area) + chat window
  • Debounced (300 ms) GET, results dropdown grouped by app
  • Clicking a result opens the app window and focuses the entity:
    1. WindowManager.open(appId, title, "") β†’ fetch hxGet URL β†’ inject
    2. Wait for rows to render (poll for tbody tr or [data-entity])
    3. Flash-highlight + scroll to the matching row (by id/text match)
    4. If the app exposes a detail handler, open it

6. Frontend Orchestrator (js/ui-orchestrator.js)

window.GBUiOrchestrator executes plans with visible animation so users see forms being controlled:

  • Ghost cursor β€” an animated SVG cursor that glides to the target element
  • Focus ring β€” pulsing highlight on the target field/button
  • Typing animation β€” characters appear one by one with a blinking caret
  • Click pulse β€” expanding ring on click targets
  • Step checklist β€” chat shows each step as it executes (β€œOpened CRM β†’ Filled First Name β†’ Submitting…”)
  • Field resolution (no hardcoded IDs): label[for=] β†’ wrapping <label> text β†’ placeholder β†’ name β†’ aria-label
  • Robustness: setTimeout-driven async queue; each step guarded by try/catch; timeouts (e.g. 5 s) for DOM readiness; partial failure reports to chat instead of crashing

Apps can opt into precise targeting via data-gb-field="first_name" on inputs β€” the orchestrator prefers these attributes, falling back to heuristics.

7. Files Changed

FileChange
botlib/src/message_types.rsUI_ACTION = 9
botserver/src/main_module/ui_plan.rsNew β€” plan types + validation
botserver/src/main_module/routes/unified_search.rsNew β€” search endpoint
botserver/src/core/bot/pipeline/exec.rsSystem prompt UI instruction
botserver/src/core/bot/pipeline/llm.rs__ui_plan__ interception
botserver/src/main_module/routes/sub_router.rsRegister search route
botui/ui/suite/chat/chat-state.jsMessageType.UI_ACTION
botui/ui/suite/chat/chat-websocket.jsRoute type 9 β†’ orchestrator
botui/ui/suite/js/ui-orchestrator.jsNew β€” animated driver
botui/ui/suite/js/gb-search.jsNew β€” universal search UI
botui/ui/suite/desktop.htmlSearch bar + scripts
botui/ui/suite/chat/chat.html, partials/chat.htmlInclude scripts

8. Security

  • Ops and app allowlists enforced server-side; the client never trusts the LLM
  • Values are escaped before being written to DOM (textContent-safe writes)
  • Search endpoint is auth-guarded (Bearer) and rate-limited
  • No arbitrary querySelector from LLM input β€” only label/name/placeholder matching and data-gb-field attributes