Skip to main content
The current AI copilot frontend includes the M1 ChatPanel and the M2 draft-review surface in @stralos-co-nz/ui-assistant, plus the AppFrame integration in @stralos-co-nz/ui. All components are fully prop-injected — they contain no API client and no domain imports.

AppFrame trailingActions

AppFrame accepts an optional trailingActions prop (ReactNode). The slot is rendered in the header’s right cluster, after the colour-mode button, immediately before the right edge.
The prop is additive and backward-compatible — existing callers that omit it are unaffected. The shell passes the assistant toggle here (copilot M1 §6.2).

ChatPanel

ChatPanel is a right-side Drawer that renders the full assistant conversation UI. It is responsive-modal: it behaves as a modal dialog (focus trap, backdrop, scroll lock) at the base breakpoint, and as a non-modal click-through panel at sm and above so the user can navigate the app while the panel stays open.

Props

Focus management

  • When the panel opens, focus moves to the composer textarea.
  • When the panel closes, focus returns to the element that had focus before the panel opened (typically the header toggle button).
  • The non-modal path (desktop) performs its own focus save/restore because Chakra UI does not manage focus in non-modal drawers.

Header controls

The panel heading reads Doug, with AI assistant below it.

Context chip

When context carries any populated fields, a teal badge is rendered below the heading. It shows a ·-joined string of: company name, project name, estimate label (when inside a project), and role. The badge carries an aria-label of Doug's scope: <value> for screen readers.

Message types

ChatMessage

ToolProgressLine

Rendered as a small icon + text row above the message content.

CitationRef

Citations are rendered as labelled chips below message content (§5.8). Links are classified before rendering — only known app routes and https://help.stralos.app/ URLs become navigable. All links rendered inside assistant messages — both inline markdown links and citation chips — are gated by a shared classifyLink function. Protocol-relative paths (//…) are rejected by a separate url.startsWith("//") guard.

Markdown rendering

renderAssistantMarkdown is a dependency-free, safe-by-construction markdown renderer. It supports a subset of Markdown:
  • Headings at ### and #### only (# and ## downgrade to bold text; ##### and deeper are treated as plain paragraphs).
  • Fenced code blocks ( ```).
  • Unordered and ordered lists.
  • Inline: bold (**), italic (*), inline code (`), and safe links.
It uses no dangerouslySetInnerHTML. Partial or streaming input degrades gracefully — an unterminated fence or emphasis falls back to plain text rather than breaking. Input is capped at 20,000 characters.

Conversation history

ChatConversationSummary carries a title (server-derived from the first user message, truncated) and an updatedAt ISO timestamp rendered as relative time (for example 5m ago, 2h ago, 3d ago). The relative formatter is built into the component group with no external date library dependency. The history list virtualises the conversations returned by the controller’s first list request. The API is keyset-paginated (20 conversations by default), and the controller does not currently follow nextCursor, so older conversations beyond that first page are not shown in the panel.

Composer

The Composer sub-component is a controlled auto-growing textarea:
  • Enter submits; Shift+Enter inserts a newline.
  • Empty or whitespace-only input is a no-op.
  • The textarea grows to a maximum of ~5 visible rows, then scrolls.
  • While isTurnRunning is true, the textarea is disabled and a Stop button replaces the default Send button.
  • A caveat line — Answers can be wrong — verify important details. — is rendered below the composer at all times.

DraftReviewCard

DraftReviewCard renders an assistant-proposed record mutation for review before a target service is called. It is collapsed by default, and Accept remains disabled until the user expands and reviews the fields at the current draft version. A version change invalidates any open review, edit, or reject interaction immediately. Pending drafts can expose typed inline edits and an optional rejection reason. Update drafts show a formatted before/after diff. Admin-gated tools, including create_rate_item, display the required role. Candidate variation lines are informational only; accepting the parent draft does not write those lines automatically.

Follow-up notes on accepted cards

An accepted card can include a follow-up note when the underlying write succeeded but a subsequent step still needs attention. The Done — … line remains visible so the note does not imply that the write itself failed. The shell currently uses warning notes for a failed assessment run or a baseline that changed after the pre-assessment. It uses an information note when the assessment is still running. Cards without a follow-up are unchanged. The shell injects onAcceptDraft(draftId, ifVersion, force?), onRejectDraft(draftId, reason?), and onEditDraft(draftId, body, ifVersion). The component has no API client and never writes directly.

Shell integration

The assistant panel and its conversation controller now live in stralos-feature-assistant, not the shell. The shell’s role is thin-host: it mounts the assistant widget remote into the appframe-aside slot, reads the availability flag off the bridge the remote publishes, and persists the toggle open/closed state. The shell is responsible for:
  • Loading assistant/availability eagerly so the toggle can render without pulling the panel.
  • Mounting RemoteWidgetSlot with slot="appframe-aside" into the App aside, gated on first open.
  • Passing AssistantToggleButton as trailingActions on AppFrame. The button renders only when the availability bridge reports true.
  • Reading and writing toggle state in sessionStorage under stralos.assistant.toggle.
The ChatPanel component, its props, callbacks, and SSE turn loop are now owned by the stralos-feature-assistant remote. The shell no longer holds AssistantController, assistantClient, draftModel, or turnReducer. AssistantWidget accepts two required props: open (the shell’s toggle boolean) and onOpenChange (called when the panel closes itself). The assistant API base (resolved inside the remote) follows this order:
  1. window.__STRALOS_API_URLS__.assistant.
  2. sameOriginFallback("http://localhost:3307").
For the availability bridge, toggle persistence, and activity surface, see Doug assistant shell draft controller. For the route catalogue and nav bridge contract, see Frontend architecture — route catalogue. For the assistant widget remote and its host contract, see Doug assistant frontend. For the assistant service backend, see Doug assistant service.