> ## Documentation Index
> Fetch the complete documentation index at: https://help.stralos.app/llms.txt
> Use this file to discover all available pages before exploring further.

# AI copilot frontend

> Current ChatPanel, draft-review, and AppFrame integration contract in stralos-ui-assistant and stralos-ui

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.

```tsx theme={null}
<AppFrame
  navigation={navigation}
  projectSelector={<ProjectSelector />}
  trailingActions={<AssistantToggle />}
>
  {children}
</AppFrame>
```

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

| Prop | Type | Description |
| - | - | - |
| `open` | `boolean` | Controls whether the panel is visible. |
| `onOpenChange` | `(open: boolean) => void` | Called when the panel requests a visibility change (close button, Escape, backdrop click at mobile). |
| `messages` | `ChatMessage[]` | Ordered conversation messages to render. |
| `context` | `ChatContext` | Scope context shown as a teal badge in the header (project name, estimate label, org name, role). |
| `isTurnRunning` | `boolean` | When `true`, the composer is disabled and a **Stop** button is shown instead of **Send**. |
| `conversations` | `ChatConversationSummary[]` | Conversation history shown in the history list. |
| `activeConversationId` | `string \| null` | The currently active conversation, highlighted in the history list. |
| `onSend` | `(text: string) => void` | Called when the user submits a message. The shell starts the SSE turn. |
| `onStop` | `() => void` | Called when the user clicks **Stop**. The shell aborts the stream. |
| `onRetry` | `(messageId: string) => void` | Emits retry intent for a failed or cancelled message. The current shell controller ignores the identifier and re-sends the most recently submitted text from the active session. |
| `onNewConversation` | `() => void` | Called when the user clicks the **New conversation** button. |
| `onSelectConversation` | `(id: string) => void` | Called when the user picks a conversation from the history list. |
| `onNavigate` | `(path: string) => void` | Wired to `window.__STRALOS_NAV__.navigate`. Called when the user follows an in-app deep link from the assistant. |
| `isRouteKnown` | `(path: string) => boolean` | Shell-injected catalogue membership check. Used to gate every rendered inline link and citation chip. Must normalise the path and match templates. |
| `onAcceptDraft` | `(draftId: string, ifVersion: number, force?: boolean) => Promise<void>` | Accepts the reviewed draft version; `force` is reserved for a user-acknowledged ambiguous retry. |
| `onRejectDraft` | `(draftId: string, reason?: string) => Promise<void>` | Rejects a pending draft with an optional reason. |
| `onEditDraft` | `(draftId: string, body: Record<string, unknown>, ifVersion: number) => Promise<void>` | Sends the complete edited draft body at the reviewed version. |
| `emptyStateHint` | `ReactNode` (optional) | Replaces the text of Doug's opening greeting bubble in an empty conversation. The example prompts below the greeting still render. |
| `pillHidden` | `boolean` (optional) | Whether the host's floating "Talk with Doug" button is hidden. Defaults to `false`. |
| `onPillHiddenChange` | `(hidden: boolean) => void` (optional) | When supplied, the header shows a hide/show control for the host's floating button. Absent means no control. The current shell supplies neither. |

### 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

| Control | Behaviour |
| - | - |
| **New conversation** (plus icon) | Calls `onNewConversation` and dismisses the history list. |
| **Recent conversations** (history icon) | Toggles the history list. Button shows `solid` style when the list is open. `aria-pressed` reflects the toggle state. |
| **Hide/Show the Talk with Doug button** | Rendered only when `onPillHiddenChange` is supplied. Calls it with the opposite of `pillHidden`. |
| **Close Doug** (close button) | Calls `onOpenChange(false)`. |

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

| Field | Type | Description |
| - | - | - |
| `id` | `string` | Stable message identifier. |
| `role` | `"user" \| "assistant"` | Message origin. |
| `content` | `string` | Markdown subset (assistant) or plain text (user). |
| `status` | `"streaming" \| "complete" \| "error" \| "canceled"` (optional) | Stream state. `error` and `canceled` show a **Retry** action. |
| `toolProgress` | `ToolProgressLine[]` (optional) | Interleaved tool-progress lines rendered above the message text. |
| `citations` | `CitationRef[]` (optional) | Citation footnotes rendered below the message text. |
| `drafts` | `DraftReviewCardModel[]` (optional) | Record-draft review cards anchored to this assistant turn. |

### ToolProgressLine

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

| Field | Type | Description |
| - | - | - |
| `id` | `string` | Tool call ID. |
| `label` | `string` | Human-readable progress label, for example `Looking up the baseline…`. |
| `state` | `"running" \| "ok" \| "error"` | Shown as a spinner, green tick, or red cross respectively. |
| `summary` | `string` (optional) | Short result note appended after the label with an em dash. Never raw record data. |

### 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.

| Field | Type | Description |
| - | - | - |
| `id` | `string` | Referenced from assistant text via a `[^id]` marker. |
| `type` | `string` | Pinned union: `help`, `route`, `tag`, `document`, `work_item`, `profile_field`. M1 emits `help` and `route` only. Unknown values render as a plain labelled chip (forward-compatible). |
| `label` | `string` | Human-readable reference label. |
| `url` | `string` (optional) | A `help.stralos.app` URL (opens in a new tab) or an app-catalogue route (calls `onNavigate`). Absent or unsafe URLs render as an inert chip. |

## Link safety

All links rendered inside assistant messages — both inline markdown links and citation chips — are gated by a shared `classifyLink` function.

| URL shape | Outcome |
| - | - |
| App-relative path (`/…`) recognised by `isRouteKnown` | In-app deep link via `onNavigate`. |
| `https://help.stralos.app/…` | External link opened in a new tab with `rel="noopener noreferrer"`. |
| Anything else, or a path containing backslashes or control characters | Rendered as inert text — no anchor element. |

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.

| Status | Rendered state |
| - | - |
| `pending` | Review, Accept, Edit, and Reject actions. |
| `applying` | Passive **Applying…** state. |
| `applying` with `ambiguous: true` | Warning and force-retry only. |
| `accepted` | Compact confirmation, an optional catalogue-gated deep link, and an optional follow-up note. |
| `rejected` | Muted rejection summary. |
| `superseded` | Muted replacement summary. |

### 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.

| Tone | Presentation | Accessibility |
| - | - | - |
| `warning` | Error-toned card with red note text. | `role="alert"` for immediate announcement. |
| `info` | Success-toned card with muted note text. | No alert role; the follow-up is informational. |

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](https://help.stralos.app/internal/assistant-shell-draft-controller.md).

For the route catalogue and nav bridge contract, see [Frontend architecture — route catalogue](https://help.stralos.app/internal/frontend-architecture.md#route-catalog).

For the assistant widget remote and its host contract, see [Doug assistant frontend](https://help.stralos.app/internal/assistant-frontend.md).

For the assistant service backend, see [Doug assistant service](https://help.stralos.app/internal/assistant-service.md).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.