# CopilotProvider

> Owns a copilot conversation: streams answers from your transport, stops, retries, keeps sessions, and lets any part of the app open the assistant with useCopilot().

Source: https://docs.nasaqui.com/components/copilot-provider

## Install

```bash
npx shadcn@latest add https://docs.nasaqui.com/r/copilot-provider.json
```

The state behind [`CopilotChat`](https://docs.nasaqui.com/components/copilot-chat) and [`CopilotDock`](https://docs.nasaqui.com/components/copilot-dock). You give it a
transport, a function that turns a request into a stream of events, and it does the rest:

- appends the question and a streaming answer;
- applies text, tool steps, sources, artifacts and follow-ups as they arrive;
- stops on demand;
- retries after an error;
- loads and deletes saved sessions.

Any component below it can open the assistant with `useCopilot().open(...)`.

## When to use

- An app-wide assistant whose answers stream from your backend (SSE, fetch streams, WebSockets).
- Pages that need to open the assistant with a question or with context, such as "Ask about this order".

## When not to use

- A one-off chat with its own state: pass `messages` and `onSend` to `CopilotChat` yourself.
- A single question box: use [`AskAI`](https://docs.nasaqui.com/components/ask-ai).

## Import

```tsx
import { CopilotProvider, CopilotLauncher, useCopilot } from "@fadymondy/nasaq/web";
// inside this monorepo: "@nasaq/web"
```

## Quick start

```tsx
import { type CopilotEvent, CopilotLauncher, CopilotProvider, type CopilotTransport } from "@fadymondy/nasaq/web";

const transport: CopilotTransport = async function* ({ history, message, sessionId }, { signal }) {
  const res = await fetch("/api/copilot", {
    method: "POST",
    body: JSON.stringify({ history, message, sessionId }),
    signal,
  });
  // One JSON event per line, such as {"type":"delta","text":"Hi"}.
  const reader = res.body!.pipeThrough(new TextDecoderStream()).getReader();
  let buffer = "";
  for (;;) {
    const { value, done } = await reader.read();
    if (done) break;
    buffer += value;
    const lines = buffer.split("\n");
    buffer = lines.pop() ?? "";
    for (const line of lines) if (line.trim()) yield JSON.parse(line) as CopilotEvent;
  }
};

export function Shell({ children }: { children: React.ReactNode }) {
  return (
    <CopilotProvider transport={transport} greeting="Hi! Ask me anything." dock={{ launcher: false }}>
      <header>
        <CopilotLauncher />
      </header>
      {children}
    </CopilotProvider>
  );
}
```

Anywhere below:

```tsx
const copilot = useCopilot();

<Button onClick={() => copilot.open({ message: "Summarise this order", autoSend: true, context: [{ id: order.id, label: order.number }] })}>
  Ask AI
</Button>;
```

## Anatomy

```
CopilotProvider          context only, no DOM
├─ children              useCopilot() works here
└─ CopilotDock           when `dock` is set, wired to the state
CopilotLauncher          data-slot="copilot-launcher"  <button aria-expanded>
```

## API

### `CopilotProvider`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `transport` | `CopilotTransport` | required | `(request, { signal }) => AsyncIterable<CopilotEvent>`. Stop when `signal` aborts. |
| `sessions` | `CopilotSessionStore` | | `{ list, load, remove? }`. Turns on History; the list loads when the assistant first opens. |
| `initialMessages` | `CopilotMessage[]` | | |
| `greeting` | `string` | | An assistant message at the start of every new chat. |
| `defaultOpen` / `onOpenChange` | | `false` | |
| `onFeedback` | `(message, "up" \| "down") => void` | | Called when a rating is set. |
| `onArtifactAction` / `onArtifactPick` | | send back | By default the choice is sent to the assistant as a hidden message with `data: { type: "artifact-action" \| "artifact-pick", ... }`. |
| `dock` | `boolean \| CopilotDockProps` | | Mounts a `CopilotDock` wired to the state. |

### Events (`CopilotEvent`)

| Event | Effect |
| --- | --- |
| `{ type: "delta", text }` | Appends text. ` ```artifact ` blocks become artifacts; a block still being written stays hidden. |
| `{ type: "text", text }` | Replaces the text. |
| `{ type: "step", step }` | Adds a tool step or updates the one with the same `id`. |
| `{ type: "sources", sources }` / `{ type: "followUps", followUps }` | |
| `{ type: "artifact", artifact }` | Any JSON; validated and dropped when invalid. |
| `{ type: "session", id, title? }` | The server saved the conversation. |
| `{ type: "error", message? }` | Ends the answer with an error and a Retry button. |
| `{ type: "done" }` | Ends the answer; running steps are marked done. |

### `useCopilot()`

Returns:

- **State:** `messages`, `streaming`, `isOpen`.
- **Opening:** `open({ message?, autoSend?, context? })`, `close`, `toggle`.
- **Conversation:** `send(text, meta?, { hidden?, data? })`, `stop`, `retry(answerId?)`, `newChat`, `feedback`.
- **Composer:** `draft` / `setDraft`, `context` / `setContext`.
- **Sessions:** `sessionId`, `sessions`, `refreshSessions`, `loadSession`, `deleteSession`.
- **Wiring:** `chatProps`, which you spread on your own `CopilotChat` or `CopilotDock`.

`useOptionalCopilot()` returns `null` outside a provider.

### `CopilotLauncher`

`Button` props plus:

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `look` | `"header" \| "icon"` | `"header"` | |
| `shortcut` | `string \| false` | `⌘J` / `Ctrl+J` | |
| `openWith` | `CopilotOpenOptions` | | |
| `labels` | | | |

### Pure helpers

The stream logic is exported for tests and custom state:

- `reduceStream(state, event)` and `startAnswer(id)`, which build up an answer;
- `finishAnswer(state)`, which ends it;
- `splitStreamingText(raw)`, which separates text from artifacts;
- `retryPoint(messages, answerId?)`, which finds the message a retry sends again;
- `describeInteraction(kind, detail)`, which writes the text of a hidden reply.

## Accessibility

- Answers stream into `CopilotChat`'s `log` region. Errors appear as an alert with a Retry button.
- Stop keeps what arrived and is not reported as an error.
- `CopilotLauncher` reports `aria-expanded` and names the shortcut in its title.

## RTL & i18n

The launcher label ships in English and Arabic. The text of hidden replies (`[action] …`, `[picked] …`) is for the model and is not shown.

## Styling & tokens

The provider has no DOM. `CopilotLauncher` is a `Button`; style it with `variant`, `size` and `className`.

## Do / Don't

- Do abort the network request when `signal` aborts, so Stop really stops the server.
- Do yield `{ type: "session" }` once the server saves the conversation, so History and Retry use the right id.
- Don't mount two providers for one assistant; open it from anywhere with `useCopilot()`.

## Related

- [`copilot-chat`](https://docs.nasaqui.com/components/copilot-chat)
- [`copilot-dock`](https://docs.nasaqui.com/components/copilot-dock)
- [`artifact-renderer`](https://docs.nasaqui.com/components/artifact-renderer)

## Lab

https://docs.nasaqui.com/?path=/docs/components-ai-assistant-copilot-provider--docs

## Code

### React

```tsx
import { type CopilotEvent, CopilotLauncher, CopilotProvider, type CopilotTransport } from "@fadymondy/nasaq/web";

const transport: CopilotTransport = async function* ({ history, message, sessionId }, { signal }) {
  const res = await fetch("/api/copilot", {
    method: "POST",
    body: JSON.stringify({ history, message, sessionId }),
    signal,
  });
  // One JSON event per line, such as {"type":"delta","text":"Hi"}.
  const reader = res.body!.pipeThrough(new TextDecoderStream()).getReader();
  let buffer = "";
  for (;;) {
    const { value, done } = await reader.read();
    if (done) break;
    buffer += value;
    const lines = buffer.split("\n");
    buffer = lines.pop() ?? "";
    for (const line of lines) if (line.trim()) yield JSON.parse(line) as CopilotEvent;
  }
};

export function Shell({ children }: { children: React.ReactNode }) {
  return (
    <CopilotProvider transport={transport} greeting="Hi! Ask me anything." dock={{ launcher: false }}>
      <header>
        <CopilotLauncher />
      </header>
      {children}
    </CopilotProvider>
  );
}
```

### shadcn

```tsx
import { type CopilotEvent, CopilotLauncher, CopilotProvider, type CopilotTransport } from "@/components/ui/copilot-provider";

const transport: CopilotTransport = async function* ({ history, message, sessionId }, { signal }) {
  const res = await fetch("/api/copilot", {
    method: "POST",
    body: JSON.stringify({ history, message, sessionId }),
    signal,
  });
  // One JSON event per line, such as {"type":"delta","text":"Hi"}.
  const reader = res.body!.pipeThrough(new TextDecoderStream()).getReader();
  let buffer = "";
  for (;;) {
    const { value, done } = await reader.read();
    if (done) break;
    buffer += value;
    const lines = buffer.split("\n");
    buffer = lines.pop() ?? "";
    for (const line of lines) if (line.trim()) yield JSON.parse(line) as CopilotEvent;
  }
};

export function Shell({ children }: { children: React.ReactNode }) {
  return (
    <CopilotProvider transport={transport} greeting="Hi! Ask me anything." dock={{ launcher: false }}>
      <header>
        <CopilotLauncher />
      </header>
      {children}
    </CopilotProvider>
  );
}
```

### Vue

```vue
<script setup lang="ts">
import { NqButton, NqCopilotLauncher, NqCopilotProvider, type CopilotEvent, type CopilotTransport } from "@fadymondy/nasaq/vue";

// Swap this for your backend: fetch("/api/copilot", { method: "POST", body, signal }) and yield one event per line.
const transport: CopilotTransport = async function* ({ message }, { signal }) {
  const words = `You asked: ${message.text}. This is a demo answer that streams in.`.split(" ");
  for (const word of words) {
    if (signal.aborted) return;
    await new Promise((r) => setTimeout(r, 40));
    yield { type: "delta", text: `${word} ` } satisfies CopilotEvent;
  }
  yield { type: "done" } satisfies CopilotEvent;
};
</script>

<template>
  <div class="relative h-96 w-full">
    <NqCopilotProvider :transport="transport" greeting="Hi! Ask me anything." :dock="{ launcher: false, placement: 'absolute' }">
      <template #default="{ copilot }">
        <div class="flex items-start gap-2">
          <NqCopilotLauncher />
          <NqButton variant="ghost" size="sm" @click="copilot.open({ message: 'Summarise this order', autoSend: true })">Ask about this order</NqButton>
        </div>
      </template>
    </NqCopilotProvider>
  </div>
</template>
```

### Blade

```blade
<div class="relative flex h-96 w-full flex-col gap-3">
    <x-nq::copilot-provider id="cp-root" greeting="Hi! Ask me anything.">
        <div class="flex items-start gap-2">
            <x-nq::copilot-provider.launcher id="cp-launcher" />
            <x-nq::copilot-provider.launcher id="cp-icon" look="icon" />
            <x-nq::button id="cp-ask" variant="ghost" size="sm" x-on:click="open({ message: `Summarise this order`, autoSend: true })">Ask about this order</x-nq::button>
        </div>
        <ul id="cp-log" class="flex flex-col gap-1 text-body-sm">
            <template x-for="m in messages.filter(m => ! m.hidden)" x-bind:key="m.id">
                <li x-bind:data-role="m.role" x-text="m.text"></li>
            </template>
        </ul>
        <p id="cp-status" class="text-caption text-muted-foreground" x-text="isOpen ? (streaming ? `Answering` : `Open`) : `Closed`"></p>
    </x-nq::copilot-provider>
</div>
```

### HTML + Alpine

```html
<div class="relative flex h-96 w-full flex-col gap-3">
    <div data-slot="copilot-provider" x-data="nqCopilotProvider(JSON.parse('{\u0022open\u0022:false,\u0022greeting\u0022:\u0022Hi! Ask me anything.\u0022,\u0022messages\u0022:null,\u0022words\u0022:{\u0022open\u0022:\u0022Open assistant\u0022,\u0022close\u0022:\u0022Close assistant\u0022}}'))" x-modelable="isOpen" x-bind:data-open="isOpen ? `` : null"
    x-on:nq-copilot-open.window="open($event.detail)" x-on:nq-copilot-close.window="close()" x-on:nq-copilot-toggle.window="toggle()"
    id="cp-root" class="contents"><div class="flex items-start gap-2">
            <button data-slot="copilot-launcher"
     type="button"                         aria-expanded="false" x-bind:aria-expanded="isOpen ? `true` : `false`" x-bind:title="(isOpen ? `Close assistant` : `Open assistant`) + ` (Ctrl+J)`" x-bind:aria-label="undefined" x-on:click="launch({})" id="cp-launcher" class="inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-control border font-sans text-label transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 border-border bg-card text-foreground hover:bg-nq-hover h-control-sm px-2.5">
        <svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M11.017 2.814a1 1 0 0 1 1.966 0l1.051 5.558a2 2 0 0 0 1.594 1.594l5.558 1.051a1 1 0 0 1 0 1.966l-5.558 1.051a2 2 0 0 0-1.594 1.594l-1.051 5.558a1 1 0 0 1-1.966 0l-1.051-5.558a2 2 0 0 0-1.594-1.594l-5.558-1.051a1 1 0 0 1 0-1.966l5.558-1.051a2 2 0 0 0 1.594-1.594z"/>
  <path d="M20 2v4"/>
  <path d="M22 4h-4"/>
  <circle cx="4" cy="20" r="2"/>
</svg>            <span>Ask AI</span>
        <kbd dir="ltr" class="hidden rounded-sm border border-border px-1 font-mono text-[11px] text-muted-foreground sm:inline">Ctrl+J</kbd></button>
            <button data-slot="copilot-launcher"
     type="button"                         aria-expanded="false" x-bind:aria-expanded="isOpen ? `true` : `false`" x-bind:title="(isOpen ? `Close assistant` : `Open assistant`) + ` (Ctrl+J)`" x-bind:aria-label="isOpen ? `Close assistant` : `Open assistant`" x-on:click="launch({})" id="cp-icon" class="inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-control border border-transparent font-sans text-label transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 text-foreground hover:bg-nq-hover size-control p-0">
        <svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M11.017 2.814a1 1 0 0 1 1.966 0l1.051 5.558a2 2 0 0 0 1.594 1.594l5.558 1.051a1 1 0 0 1 0 1.966l-5.558 1.051a2 2 0 0 0-1.594 1.594l-1.051 5.558a1 1 0 0 1-1.966 0l-1.051-5.558a2 2 0 0 0-1.594-1.594l-5.558-1.051a1 1 0 0 1 0-1.966l5.558-1.051a2 2 0 0 0 1.594-1.594z"/>
  <path d="M20 2v4"/>
  <path d="M22 4h-4"/>
  <circle cx="4" cy="20" r="2"/>
</svg></button>
            <button data-slot="button"
     type="button"                         id="cp-ask" x-on:click="open({ message: `Summarise this order`, autoSend: true })" class="inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-control border border-transparent font-sans text-label transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 text-foreground hover:bg-nq-hover h-control-sm px-2.5">
        Ask about this order</button>
        </div>
        <ul id="cp-log" class="flex flex-col gap-1 text-body-sm">
            <template x-for="m in messages.filter(m => ! m.hidden)" x-bind:key="m.id">
                <li x-bind:data-role="m.role" x-text="m.text"></li>
            </template>
        </ul>
        <p id="cp-status" class="text-caption text-muted-foreground" x-text="isOpen ? (streaming ? `Answering` : `Open`) : `Closed`"></p></div>
</div>
```
