Skip to content

CommandCenter AI chat#

UI scope#

The AI chat sidebar is the shared assistant surface for CommandCenter. The header opens the shared surface; New conversation starts a general conversation, while Analytics and exception analysis supply explicit context from their existing assistant actions. The enableAIChat preview flag controls availability, but it is not an authorization boundary.

Desktop chat occupies space beside the routed page rather than covering it. Standard and expanded widths are 410px and 640px. At viewport widths of 1100px or less it becomes a full-width dialog. Both modes use a bounded message scroller and composer; the sidebar must not increase the page height or remount the Analytics editors.

Closing chat does not delete its session or cancel work on the server. Changing routes preserves the conversation, while page-specific actions such as SQL insertion remain available only for the matching mounted editor. SQL insertion is explicit and must never execute or publish the query.

The UI does not pretend to support every control in the original visual specification. Model selection, attachments, feedback, regeneration, and billing/credits require additional backend contracts. No fixed model names, invented activity timestamps, simulated streaming, or promotional credits are displayed.

Explanation summaries are the one exception, and only as inert plumbing: cc.enableChatReasoning() (default off) reveals a panel that renders an explanation field the Chat API does not send yet. With the flag on and no server-authored explanation, it renders nothing — it has no fallback and must never derive content from the answer text. .specs/specs-ai-chat-sidebar.md §5.4 records the contract it waits on.

Integration boundary#

The browser uses the existing ChatService transport at same-origin /api/chat/*. YARP already owns that route. Do not add another frontend proxy, expose provider credentials, or copy a direct backend URL from the external guide into the production SPA.

The authoritative external integration guide is chat-client-integration-guide.md in the ChatApi repository. Its documented async endpoints address requests that outlive browser/proxy timeouts:

Operation Endpoint Meaning
Start a response POST /api/chat/send/async Returns an operation ID, session ID, and status
Read operation status GET /api/chat/operations/{operationId} Returns status and, on success, the final response
Stream operation events GET /api/chat/operations/{operationId}/events Resumable SSE of the same operation's progress — see below
Request cancellation DELETE /api/chat/operations/{operationId} Requests cancellation; acceptance is not a terminal result

The UI polls pending/running operations approximately every 1.5 seconds (10 seconds while the event stream below is connected). Terminal statuses are Succeeded, Failed, and Canceled. Cancellation can race with completion; display the actual terminal result, not an assumed cancellation.

Token streaming is layered on top via GET /api/chat/operations/{operationId}/events, consumed through the browser's EventSource (ChatService.openOperationStream) — same-origin cookies ride along automatically, and its built-in reconnect resends Last-Event-ID for free resume. Events are sequence-numbered and typed: assistant.delta (text), tool.call / tool.result (name and status only — never arguments or a result body), and reasoning content is never forwarded. This is progressive enhancement for display only: the poll above remains the sole source of terminal truth, the 404-is-terminal-unknown rule, and the 5-minute monitoring budget. If the stream never connects, or disconnects and does not reconnect, the poll alone still resolves the conversation exactly as before.

Client session and operation IDs must be explicit in asynchronous callbacks. A response for one conversation must not appear in another conversation selected while the request was running. Only one send can be active in a given conversation.

Do not automatically retry a session-creation or send mutation after an ambiguous transport failure. The server may already have accepted it, and another request can repeat tool execution. Retry monitoring instead of resending when an operation ID is known. Losing the polling connection does not mean the server operation failed or stopped.

If the deployed API lacks the documented async endpoints, report the incompatibility. Do not silently switch to the timeout-prone synchronous endpoint.

systemPrompt is advisory, not authoritative#

The server composes session instructions as a non-overridable base prompt, then a workflow prompt, then the session's available-capability list, then any MCP guidance, and only then the caller's systemPrompt — appended under the heading "Caller customization (lower priority than server policy)". The SPA's Analytics and exception prompts are suggestions. No UI copy may imply the prompt controls the assistant, and caller prompts must not name tools the server does not report as available.

The one sanctioned re-send#

Sessions are held in memory with no TTL and are lost on every Chat API restart, so a routine restart makes POST /api/chat/send/async return 404 "Session … not found." The sidebar distinguishes that from a missing endpoint by probing GET /api/chat/sessions/{id}/messages?count=1; a 405/501 skips the probe entirely. When the session is genuinely gone, the sidebar recreates it — carrying at most the last four turns — and delivers the message once, then discloses what happened via chat.sessionRecovered.

This does not contradict the no-retry rule above. ResolveSession returns NotFound before the operation manager starts any work, so a session-not-found 404 is proof of non-execution rather than ambiguous delivery. Recovery is attempted at most once per conversation. Ambiguous failures — network drops, 5xx, timeouts — still lock the thread and are never resent.

Completed operations are also in-memory (retained 24 hours). An operation poll that 404s must be treated as terminal-unknown: surface chat.resultUnavailable and offer a history refresh. Never synthesise a Failed or Canceled status the server did not report, and never leave a paused-monitoring state whose Resume button cannot succeed.

Capabilities#

GET /api/chat/capabilities reports the effective host tools, MCP tools, skills and client-action contracts. Two things to know before consuming it:

  • kind is an integer on the wire (0 Tool, 1 McpTool, 2 McpPrompt, 3 McpResource, 4 Skill, 5 ClientAction), not the string union the external integration guide documents. The client accepts both and normalises.
  • Treat "unknown" as distinct from "unavailable". A malformed or missing snapshot must degrade to unknown, and unknown must not be read as available: the server silently drops unknown tool names, so scoping a session to a tool that is not registered yields an agent with no tools at all that still answers confidently.

A descriptor also carries functionName: the exact string to echo back in a session's toolNames to select that capability. Populated for kind: 'Tool' (the bare registered name) and kind: 'McpTool' (a server-generated mcp__<server>__<tool> name — the client never constructs this itself); null for every other kind, which is never independently selectable and always reported unavailable.

Capability gating now decides two things, not one. The exception conversation is still scoped to analyzeException the same way as before. Additionally, a session tools selector (sidebar footer) lets the operator scope a conversation's toolNames to specific host and MCP tools by functionName — previously toolNames only ever filtered host tools, so a session "scoped away" from MCP still had every authorized MCP tool reachable through the generic callMcpTool passthrough, and the server's own "Available capabilities for this session" prompt text contradicted what the session could actually do. CapabilityResolver now applies the same scope to MCP entries: an MCP tool excluded from the current scope is still listed (with unavailableReason: "Not included in this session's tool scope.", distinct from being unauthorized by server policy) rather than dropped, so the operator can see it exists without it being usable. Editing an existing thread's tools recreates its session — carrying at most the last four turns, same as evicted-session recovery — because a session's tool set is fixed at creation; this is a deliberate, repeatable operator action and is not the same code path as that recovery (which is latched to once per thread and fires only on a 404). integrationGuidance is parsed but never rendered; it is third-party MCP server text the backend itself labels lower trust than server policy.

Data references (#)#

cc.enableChatTools() (default off, and separate from enableAIChat) lets the operator type # in the composer and attach a read-only CommandCenter query to the next message. The call runs in the browser, as the signed-in user, through the BFF, so it is already scoped to that user's access; the agent never initiates it.

Three properties are load-bearing:

  • Each entry declares a field allowlist. Projections are written out field by field and must never spread a source row — these models carry database and instance names, stage URLs, SSO configuration and free-text schedule arguments.
  • Both a row cap and a byte cap apply, and truncation is disclosed in the message itself, not only in the UI. Otherwise the model states totals from a sample it cannot know is partial.
  • What is sent equals what is shown. The attached block is part of the visible message body, and the chip exposes the identical rows before sending.

Some entries additionally let the operator narrow the attachment before it resolves: #customers to one customer, #customerStages to one or more stages. Narrowing never widens the allowlist — it only reduces the row count — and the resulting disclosure names the selection separately from any row/byte-cap truncation, so the two kinds of narrowing are never mistaken for each other. See .specs/specs-ai-chat-sidebar.md §7.

Attaching real business data does not lower the rollout gate below — it raises it. Keep the flag off outside dev/test until the Chat API authenticates. .specs/specs-ai-chat-sidebar.md holds the contract, including why agent-initiated client tools are not buildable against today's backend.

Page references and actions (+)#

cc.enableChatActions() (default off, and separate again from enableChatTools) lets the operator type + to attach the page they are on — a projection of its state plus the actions that page is willing to accept. The assistant may then propose one of those actions, which renders as a button in its reply. Nothing runs until the operator clicks it.

The invariant: the chat never commits. It only puts the operator in front of the commit with the work already done. This is the same line drawn for SQL insertion. Concretely:

Kind What the button does
navigate Routes to a page. Reversible.
filter Applies a filter to the current page. Reversible.
fillForm Populates fields and stops. The operator presses the page's own Save.
stageOperation Opens the page's own confirmation dialog. The operator confirms there.

No action calls a mutation. There is no client-side authorization anywhere in the SPA — every write is gated server-side at the BFF — so a chat button must never be a shortcut past the page's own confirmation step, which is the only thing standing between a bad suggestion and a write.

Load-bearing properties, beyond the three that already govern #:

  • Pages opt in and declare their own projection. There is no DOM scraping. Opening the sidebar still must not harvest arbitrary page contents; a page states exactly what it exposes and exactly what may be done to it, via usePageContext.
  • One zod schema per action generates the JSON Schema the model is shown, validates the proposal, and types the handler. The SPA has no form schemas, so this is the only enumerable description of a form it has.
  • Validate before affording. A proposal becomes a button only if the action name resolves on the currently mounted page, its arguments pass the schema, and it is the first proposal in the reply. Anything else renders as inert, visible text saying why — refused, never silently dropped. This mirrors safeChatHref.
  • The page attachment is sticky across turns, unlike a # attachment. The catalog has to be in front of the model on every turn or the natural follow-up ("make it 03:00 instead") arrives with nothing valid to propose. It is dropped when the operator removes it or leaves the page.
  • A stale page is a warning, not a rejection. Page data moves under a background refetch; that must not silently kill a good suggestion.

Two pages opt in today. Schedules covers all four action kinds. The query designer (/analytics) covers the Metadata and Liquid tabs: the operator presses "Suggest with AI", which opens the chat with the page attached and the prompt prefilled — and then stops, because they press Send. Two properties of that pilot generalise:

  • Send the shape, not the data. Its projection carries the SQL and the last run's column names, never a cell value. Analytics results are live customer business data, and a name, a description or a template only ever needs the shape. The SQL itself is capped and the truncation disclosed, since a sticky attachment re-sends it on every turn.
  • A generated artefact that will be interpreted gets checked before it is offered. A Liquid template renders through dangerouslySetInnerHTML, so a suggested one is parsed and screened for script-bearing markup inside its own zod schema; a failure renders as inert text naming the reason. That check is a quality gate, not a security boundary — the operator can type the same markup by hand. What protects them is that the suggestion lands in the editor as one undoable edit, for them to read, with the page's own Save still ahead of it.

The proposal travels as a ```cc-action block in the reply body, parsed client-side. .specs/specs-ai-chat-sidebar.md §8 is the contract — the registry, the validation gates, the stickiness rule and the rollout gate. Its JSON deliberately mirrors ChatApi's dormant ClientActionRequest/PageContext contracts, so promoting this to a first-class backend contract later is a transport swap rather than a redesign. Doing that today would mean giving up token streaming: Microsoft.Agents.AI has RunAsync<T> but no RunStreamingAsync<T>, so schema-enforced output would constrain the whole response.

History and data handling#

The conversation selector includes only sessions created during the current shell lifetime. It deliberately does not discover all server sessions or restore identifiers from browser storage without a reliable user-identity/ownership contract. Conversations and drafts survive close/reopen and in-app navigation, but a page reload resets the local conversation selector and operation monitoring. Reloading does not cancel work already accepted by the server.

The existing API's session list describes active sessions, not a durable, paginated conversation archive. Session summaries provide sessionId, messageCount, and isAgent; history messages provide role and content. Those fields cannot establish user ownership, original timestamps, server titles, or a complete transcript.

Local display titles and stable fallback labels are presentation conveniences, not authoritative server metadata. The UI keeps up to 50 recent messages in its query cache. A read-only history refresh is available for uncertain delivery; it does not unlock resending or claim to resolve an unknown operation. The displayed session ID allows operator reconciliation.

No prompt bodies, transcripts, stack traces, identifiers, credentials, or API keys are stored by the sidebar in browser storage. User-bound restoration, stale-reference reconciliation, and durable history are phase-two work.

Render AI responses as untrusted content. Code copy and insertion belong to React-owned controls; never inject response HTML or dynamically append controls with innerHTML. Links must use safe protocols, and remote images/resources must not load just because they occur in a model response.

Only send exception data through the explicit exception-analysis action. Opening the global sidebar must not harvest or transmit arbitrary page contents. The + page reference is not an exception to that rule: it transmits a page's own hand-written projection, on an explicit operator action, and never reads the DOM.

Production rollout gate#

The preview feature flag and the existence of a BFF route do not establish access control. Before enabling chat for a shared production deployment, verify:

  • Authentication and user/tenant ownership for sessions, history, operations, cancellation, and tool execution.
  • The BFF route's authorization/CSRF requirements and the identity forwarded to the downstream Chat API.
  • Retention, deletion, redaction, and audit rules for prompts and operational context.
  • Safe scope for automatic tools and explicit approval for privileged or mutating operations.

Do not claim that every interaction is audited, user-isolated, or RBAC-protected until those properties are implemented and verified on the server.

Phase two: backend feature roadmap#

The Chat API lives in a separate repository. The table below is a contract backlog, not a statement that these features already exist. Inspect that repository and agree on contracts before implementing endpoint changes; update its integration guide and the CommandCenter client together.

Priority Backend work UI enabled or improved Acceptance boundary
1 User/tenant ownership, durable sessions/messages, titles, timestamps, cursor pagination, retention Cross-reload conversation restoration and reliable history Unauthorized identifiers cannot expose another user's data; ordering and pagination are defined
2 Send idempotency, concurrent-send policy, operation retention/recovery, cancellation guarantees, structured errors Safe reconnect/resume and retry A repeated request ID cannot duplicate work; restart and success/cancel races have documented outcomes
3 Server-authoritative capabilities/model catalog and validated conversation settings Model/settings menu Only supported options are selectable; no provider secrets reach the browser
4 Done: streaming (GET /operations/{id}/events, resumable via Last-Event-ID) with ordered, sequence-numbered events and tool-call/result progress by name. Remaining: tool call arguments and result bodies are still withheld — there is no caller identity yet to permission-filter them against Incremental output and named tool-call progress are live; per-tool payload detail still requires row 1's identity work first Events have ordering/identity ✅, reconnect does not duplicate messages ✅, payloads are permission-filtered ⏳ (blocked on row 1)
5 Explicit user-facing explanation summaries Expandable explanation panel Summaries describe relevant evidence/actions, not private chain-of-thought or unfiltered tool output
6 Authenticated uploads, type/size limits, scanning, retention, permission-aware retrieval Attachments and attachment pills Files cannot cross ownership boundaries or bypass size/type/access checks
7 Message-linked feedback and regeneration/branching contracts Feedback and regenerate actions Stable message IDs and idempotency exist; regeneration is not a disguised duplicate ordinary send
8 Tool authorization/approval, redacted audit events, correlation identifiers Accurate permissions disclosure and approval UX Server enforces approval and records outcomes, including denied/canceled actions

Authorization and approval for any currently enabled tools are rollout prerequisites, not work to postpone until other UI features are complete. Durable message identities precede feedback/regeneration; capability discovery governs progressive activation of new UI controls. A credits badge should exist only if an actual entitlement contract is introduced.

Async send, polling, and cancellation are already documented backend capabilities and belong in the first UI integration, not this new-feature backlog.

Regression checklist#

Use existing Vitest/React Testing Library for transport, state transitions, caller integration, and keyboard interactions. Real browser checks remain necessary for layout:

  • Open, expand, and close the sidebar repeatedly while switching Analytics tabs; editor dimensions return to baseline without cumulative main-scroll growth.
  • Preserve SQL/Liquid models, edits, cursor/undo state, and metadata when the sidebar changes width.
  • Cover short/narrow screens, mobile focus containment/return, light/dark themes, reduced motion, long code/history, and long composer drafts.
  • Switch threads and routes during operations; late responses update only the originating thread and stale insertion targets are unavailable.
  • Exercise failures, ambiguous sends, cancellation races, history errors, safe rendering, IME composition, and double submission using mocked responses.

Do not execute queries, publish changes, upload private data, or trigger operational AI tools merely to test the UI.