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

# watchChat

> Chat-scoped subscription: run lifecycle, cell snapshots, and heartbeats.

```typescript theme={null}
streaming.chats.watchChat(request, options?) → AsyncIterable<WatchChatEvent>
```

The state channel for a chat: announces `runStarted` immediately when a run is
live, delivers cell snapshots, signals `runComplete` / `runError`, and sends a
`heartbeat` every \~20 seconds. Stays open across runs — your loop decides when
to stop.

## Example

```typescript theme={null}
import { createStreamingClient } from "@textql/sdk/streaming";

const streaming = createStreamingClient({ apiKey: process.env.TEXTQL_API_KEY! });

for await (const event of streaming.chats.watchChat({
  chatId,
  latestCompleteCellId: lastSeenCellId, // omit to replay the whole chat
})) {
  switch (event.payload.case) {
    case "runStarted":
      showSpinner();
      break;
    case "cell":
      upsertCell(event.payload.value); // typed Cell — see the Cell reference
      break;
    case "runComplete":
      hideSpinner();
      console.log("final cell:", event.payload.value.finalCellId);
      break;
    case "runError":
      showError(event.payload.value.error);
      break;
    // "opened", "heartbeat", "handoffPending": safe to ignore
  }
}
```

## Request

| Field                  | Type      | Notes                                                                                                      |
| ---------------------- | --------- | ---------------------------------------------------------------------------------------------------------- |
| `chatId`               | `string`  | required                                                                                                   |
| `latestCompleteCellId` | `string?` | replay starts *after* this cell — pass your last known cell id to catch up; omit to replay the entire chat |
| `resumeCursor`         | `string?` | alternative resume point: the `cursor` from a previous event                                               |

## Events

Every event is a `WatchChatEvent` with a discriminated `payload` and a
`cursor: string` you can store for reconnects.

| `event.payload.case` | `event.payload.value`                        | Meaning                                                        |
| -------------------- | -------------------------------------------- | -------------------------------------------------------------- |
| `"opened"`           | `{}`                                         | subscription acknowledged                                      |
| `"runStarted"`       | `{}`                                         | a run is executing (sent immediately on attach if one is live) |
| `"cell"`             | [`Cell`](/api-reference/sdk/streaming/cells) | cell snapshot — upsert by `id`                                 |
| `"runComplete"`      | `{ finalCellId: string }`                    | run finished                                                   |
| `"runError"`         | `{ error: string, code: string }`            | run failed                                                     |
| `"handoffPending"`   | `{ handoffMarker: string }`                  | run paused for human handoff                                   |
| `"heartbeat"`        | `{}`                                         | keepalive, every \~20s                                         |

To re-attach after a disconnect or page reload, pass the last cell id you
rendered as `latestCompleteCellId` — missed cells replay, then live events
continue. This is how the
[chat-demo](https://github.com/TextQLLabs/textql-ts-sdk/tree/main/examples/chat-demo)
survives mid-run reloads.
