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

# Chat & Streaming

> Send messages, stream replies, and read tool cards in the Desktop app

Every reply streams live from your local agent — text, reasoning, tool cards, and usage all arrive as typed events over `127.0.0.1`.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents import Agent

agent = Agent(
    name="Assistant",
    instructions="You are a helpful assistant.",
)
# The Desktop app streams this agent's reply token-by-token.
agent.start("Write a haiku about the sea", stream=True)
```

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph LR
    You[👤 You] --> Msg[💬 Message]
    Msg --> Engine[🧠 Engine]
    Engine --> Stream[📡 SSE Events]
    Stream --> Turn[✅ Rendered Turn]

    classDef you fill:#8B0000,stroke:#7C90A0,color:#fff
    classDef msg fill:#6366F1,stroke:#7C90A0,color:#fff
    classDef engine fill:#189AB4,stroke:#7C90A0,color:#fff
    classDef stream fill:#F59E0B,stroke:#7C90A0,color:#fff
    classDef turn fill:#10B981,stroke:#7C90A0,color:#fff

    class You you
    class Msg msg
    class Engine engine
    class Stream stream
    class Turn turn
```

## Quick Start

<Steps>
  <Step title="Send your first message">
    Type in the composer and press `Enter`. The reply streams in as the agent produces it.
  </Step>

  <Step title="Watch the events render">
    Text, a live reasoning panel, tool cards, and a usage line all appear in the same turn as they stream.
  </Step>

  <Step title="Act on the turn">
    Hover a turn to **Copy**, **Regenerate**, **Fork**, or **Delete** it.
  </Step>
</Steps>

***

## How It Works

The Desktop app opens `POST /chat` and streams these 11 event types straight into the turn — you write no client code for it.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents import Agent

agent = Agent(
    name="Assistant",
    instructions="You are a helpful assistant.",
)
# The Desktop app opens POST /chat and streams these 11 event types
# straight into the turn — you write no client code for it.
agent.start("Summarize today's notes", stream=True)
```

The engine sends Server-Sent Events on `POST /chat`.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sequenceDiagram
    participant You as 👤 You
    participant App as 🌐 App
    participant Engine as 🧠 Engine
    participant Agent as 🤖 Agent

    You->>App: type message + Enter
    App->>Engine: POST /chat (prompt)
    Engine->>Agent: agent.start(stream=True)
    Agent-->>Engine: chunks
    Engine-->>App: start / delta / tool_call / usage / end
    App-->>You: streamed turn
```

Each event maps to one visible piece of the turn:

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph LR
    Send[💬 Send] --> Start[🟢 start]
    Start --> Reason[🧠 reasoning]
    Reason --> Delta[✍️ delta]
    Delta --> Draft[🔧 tool_drafting]
    Draft --> Call[🔨 tool_call]
    Call --> Ask[🔑 approval_request]
    Ask --> Result[📦 tool_result]
    Result --> Use[📊 usage]
    Use --> End[✅ end]

    classDef input fill:#6366F1,stroke:#7C90A0,color:#fff
    classDef middle fill:#F59E0B,stroke:#7C90A0,color:#fff
    classDef success fill:#10B981,stroke:#7C90A0,color:#fff

    class Send input
    class Start,Reason,Delta,Draft,Call,Ask middle
    class Result,Use,End success
```

Every event carries a `msg_id` that scopes it to a specific assistant message; `start` additionally carries a `run_id` that scopes it to a specific run, so a `POST /approve/{aid}` or `POST /cancel` sent later addresses the right run and not whichever one happens to be pending. The vocabulary is exactly the **11 events** below — a `StreamProtocolVocabulary` test in the engine fails CI if a new emitter or a new documented event ever drifts from that list, so this table stays a faithful mirror of the wire.

Every event the stream emits (from `engine/server.py`), with the exact payload a client sees on the SSE wire:

| Event              | Payload                                                   | Rendered as                                                                                         |
| ------------------ | --------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| `start`            | `{msg_id, run_id}`                                        | New assistant turn container                                                                        |
| `reasoning`        | `{msg_id, text}` (incremental)                            | Live "Thinking… Xs" panel (collapses to "Thought for Xs")                                           |
| `delta`            | `{msg_id, text}`                                          | Streamed text into the current turn                                                                 |
| `tool_drafting`    | `{msg_id, name}`                                          | "Preparing tool…" state / argument preview inside a tool card                                       |
| `tool_call`        | `{msg_id, call_id, name, args}`                           | Tool card (running dot)                                                                             |
| `tool_result`      | `{msg_id, call_id, name, ok, output, seconds}`            | Tool card result pane (tail-truncated with **Show all** + **Copy output**)                          |
| `approval_request` | `{msg_id, approval_id, call_id, name, args}`              | [Approval card](/docs/features/desktop/approvals) (Allow / Always allow / Deny) — human-in-the-loop gate |
| `usage`            | `{msg_id, chars, seconds, ttft}`                          | Chars, seconds, time-to-first-token under the reply                                                 |
| `cancelled`        | `{msg_id, run_id}`                                        | "Stopped" indicator                                                                                 |
| `error`            | `{msg_id, message, kind}`                                 | Typed error card — `auth` / `rate_limit` / `no_answer` / `empty` / `internal`                       |
| `end`              | `{msg_id, user_index, assistant_index, versions, active}` | Finalizes the turn                                                                                  |

Every event carries `msg_id` so the client can address a specific message rather than assuming the last one is live. Clients integrating with the SSE stream should handle **all 11** events — a client written against a subset silently ignores the rest, and `approval_request` in particular will block the run invisibly until the 300 s [approval timeout](/docs/features/desktop/approvals) if the client does not render it.

<Note>
  Silence is treated as a failure, and the two shapes are reported differently:

  * **`empty`** — nothing at all happened (no text, no tools). Still reported as **"No output"**.
  * **`no_answer`** — the tools ran but the model produced no follow-up answer. Reported as **"The tools ran, but no answer came back"** and lists how many tool calls succeeded above.

  Tool cards remain rendered even when the answer never arrives — the work isn't hidden by a failure banner.
</Note>

***

## Capabilities

<AccordionGroup>
  <Accordion title="Reasoning panel">
    When the model emits reasoning, a live "Thinking… Xs" panel shows it and collapses to "Thought for Xs" when the turn ends. Toggle it with **Show reasoning** and **Collapse reasoning by default** in Settings.
  </Accordion>

  <Accordion title="Tool cards">
    A `tool_call` opens a card with a running dot; `tool_drafting` previews the arguments; `tool_result` fills the result pane. Long output is tail-truncated with **Show all** and **Copy output**. Tool cards remain visible even if the model produces no follow-up answer — a `no_answer` banner then explains what didn't come back.
  </Accordion>

  <Accordion title="Attachments">
    Drop a file and the composer branches on its MIME type — images and PDFs reach a vision model as bytes, everything else folds into the prompt as text.

    ```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    graph TB
        Drop[📎 File dropped] --> Mime{MIME type?}
        Mime -->|image/* or application/pdf| Vision[🖼️ base64 data: URL<br/>→ agent.start attachments=]
        Mime -->|anything else| Text[📄 Clip to 100 kB<br/>→ fold into prompt]
        Vision --> Model[🧠 Vision model sees bytes]
        Text --> Model

        classDef input fill:#6366F1,stroke:#7C90A0,color:#fff
        classDef decision fill:#F59E0B,stroke:#7C90A0,color:#fff
        classDef process fill:#189AB4,stroke:#7C90A0,color:#fff
        classDef result fill:#10B981,stroke:#7C90A0,color:#fff

        class Drop input
        class Mime decision
        class Vision,Text process
        class Model result
    ```

    Drag-drop or use the **+** button. Chips show each file's size. Limits: **2 MB max**, up to **5 files**.

    * **Images (`image/*`) and PDFs** are read as base64 and passed to the agent as `attachments=[...]`, so a vision-capable model (`gpt-4o`, `claude-3-*`, `gemini-*`) sees the image itself — not a text description of it. Point the Desktop app at a text-only model and the image is ignored silently.
    * **Text files** are clipped to **100 kB** of text and folded into the prompt, so long pastes condense into an attachment instead of filling the context.

    Only text is stored in the turn's history; the image bytes are ephemeral to the turn.

    <Note>
      Point Desktop at a vision-capable model (`gpt-4o`, `claude-3-7-sonnet-*`, `gemini-2.0-flash`, …) if you want it to actually see dropped images. A text-only model still runs, but the image never reaches it.
    </Note>
  </Accordion>

  <Accordion title="Cancel, regenerate, fork">
    **Stop** cancels a live run — the client is told explicitly with a `cancelled` event rather than inferring it from silence. Cancelling with **Stop** no longer double-reports as "No output" — a `cancelled` event is the only banner the user sees. Per-turn hover actions cover **Copy**, **Regenerate**, **Fork** (`POST /fork/{cid}/{idx}`), and **Delete message** (`DELETE /messages/{cid}/{idx}`).
  </Accordion>
</AccordionGroup>

***

## Message Versions

Regenerating an answer keeps the previous one as a prior version on the same message, so you can compare without copying anything out.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph LR
    Answer[💬 Answer v1] --> Regen[🔁 Regenerate]
    Regen --> V2[💬 Answer v2]
    V2 --> Pick[◀ 2/2 ▶ picker]

    classDef msg fill:#8B0000,stroke:#7C90A0,color:#fff
    classDef act fill:#189AB4,stroke:#7C90A0,color:#fff
    classDef pick fill:#10B981,stroke:#7C90A0,color:#fff

    class Answer,V2 msg
    class Regen act
    class Pick pick
```

* **Regenerate** sends `POST /chat` with `regenerate_of` set to the message index, so the new answer is filed as another version instead of a duplicate question.
* A `< 1/2 >` picker appears on hover once a message has more than one version, opening at the newest.
* Stepping through the picker calls `POST /version/{cid}/{idx}/{n}` and repaints the chosen answer.
* In the server payload every assistant row exposes `versions`, `active`, `version_count`, and `version_active`.

<Note>
  A message written before versioning has no `versions` key and is its own single version — nothing to pick between, so no picker shows.
</Note>

***

## Message Queue

Typing while a turn is running queues your next message instead of interrupting it.

* The queued message sends automatically when the current turn finishes.
* **Stop** clears the queue rather than sending it — cancelling means "not this either".

***

## In-App Confirmation Dialogs

Destructive actions — delete conversation, delete-all, delete message — route through an in-app modal, not the browser's `window.confirm`.

<Warning>
  `window.confirm` returns `false` immediately in WKWebView, which silently cancelled every destructive action. The in-app dialog is why **Delete conversation** now actually works. The `confirm_delete` setting gates whether the dialog appears at all.
</Warning>

***

## Keyboard Shortcuts

| Key           | Action                   |
| ------------- | ------------------------ |
| `⌘N`          | New chat                 |
| `⌘K`          | Search all conversations |
| `⌘,`          | Open settings            |
| `Enter`       | Send                     |
| `Shift+Enter` | Newline                  |
| `Esc`         | Close overlay            |

***

## Related

<CardGroup cols={2}>
  <Card title="Approvals & Safety" icon="shield-check" href="/docs/features/desktop/approvals">
    How tool calls are approved before they run
  </Card>

  <Card title="Conversations & Search" icon="magnifying-glass" href="/docs/features/desktop/conversations">
    Fork, delete, projects, and `⌘K` search
  </Card>
</CardGroup>
