Skip to content

Commit 752dd58

Browse files
mschristensenclaude
andcommitted
docs: align documentation with View-based write and navigation API
Updates 17 doc pages to reflect the architecture changes where send/regenerate/edit moved from ClientTransport to View, and branch navigation (select, getSelectedIndex) moved from Tree to View. - Feature pages: transport.send() → view.send(), tree.select() → view.select() - React hook reference: ViewHandle now includes write ops and navigation; useSend/useRegenerate/useEdit accept View; useTree has structural queries only - Internals: Tree is a pure data structure; selection lives on View; transport exposes _internalSend via SendDelegate - Glossary: updated own-turn and flatten definitions Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 7298dc6 commit 752dd58

17 files changed

Lines changed: 129 additions & 121 deletions

docs/concepts/transport.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -62,13 +62,14 @@ The client transport manages conversation state: the message list, conversation
6262
import { createClientTransport } from '@ably/ai-transport/vercel';
6363

6464
const transport = createClientTransport({ channel, clientId });
65+
const view = transport.view;
6566

6667
// Send a message - returns immediately with a turn handle
67-
const turn = await transport.send(userMessage);
68+
const turn = await view.send(userMessage);
6869

6970
// Subscribe to accumulated messages - updates on every token
70-
transport.view.on('update', () => {
71-
const messages = transport.view.flattenNodes().map(n => n.message);
71+
view.on('update', () => {
72+
const messages = view.flattenNodes().map(n => n.message);
7273
// the last assistant message grows as tokens stream in
7374
});
7475

@@ -79,12 +80,11 @@ transport.view.on('update', () => {
7980
In React, the hooks handle subscriptions and state management:
8081

8182
```typescript
82-
import { useClientTransport, useView, useSend } from '@ably/ai-transport/react';
83+
import { useClientTransport, useView } from '@ably/ai-transport/react';
8384
import { UIMessageCodec } from '@ably/ai-transport/vercel';
8485

8586
const transport = useClientTransport({ channel, codec: UIMessageCodec, clientId });
86-
const { nodes } = useView(transport);
87-
const send = useSend(transport);
87+
const { nodes, send } = useView(transport);
8888
```
8989

9090
## The codec

docs/concepts/turns.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,10 @@ Pass `reason` to `end()` so all clients see why the turn ended.
4040

4141
### Client side
4242

43-
The client transport creates turns implicitly when you call `send()`, `regenerate()`, or `edit()`:
43+
The client creates turns implicitly when you call `view.send()`, `view.regenerate()`, or `view.edit()`:
4444

4545
```typescript
46-
const turn = await transport.send(userMessage);
46+
const turn = await view.send(userMessage);
4747

4848
// turn.turnId - the unique turn identifier
4949
// turn.stream - a ReadableStream of decoded events

docs/features/branching.md

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,9 @@ User: "What is Rust?" (msg-1, parent: null)
2727
Regeneration forks an assistant message - the server produces a new response for the same prompt:
2828

2929
```typescript
30-
import { useRegenerate } from '@ably/ai-transport/react';
30+
import { useView } from '@ably/ai-transport/react';
3131

32-
const regenerate = useRegenerate(transport);
32+
const { regenerate } = useView(transport);
3333

3434
// Fork the assistant message - starts a new turn with no new user messages.
3535
// nodeId is the x-ably-msg-id (see treeMsgId helper in the quickstart).
@@ -43,9 +43,9 @@ The transport automatically computes `forkOf` (the assistant message being repla
4343
Editing forks a user message - the user provides replacement content, and the server produces a new response:
4444

4545
```typescript
46-
import { useEdit } from '@ably/ai-transport/react';
46+
import { useView } from '@ably/ai-transport/react';
4747

48-
const edit = useEdit(transport);
48+
const { edit } = useView(transport);
4949

5050
const newMessage = {
5151
id: crypto.randomUUID(),
@@ -61,48 +61,48 @@ await edit(nodeId, [newMessage]);
6161

6262
## Branch navigation
6363

64-
`useTree` provides the tree state and navigation:
64+
`useView` provides branch navigation alongside message state:
6565

6666
```typescript
67-
import { useTree } from '@ably/ai-transport/react';
67+
import { useView } from '@ably/ai-transport/react';
6868

69-
const tree = useTree(transport);
69+
const view = useView(transport);
7070

71-
// tree.hasSiblings(nodeId) - does this message have alternatives?
72-
// tree.getSiblings(nodeId) - all alternatives at this fork point
73-
// tree.getSelectedIndex(nodeId) - which sibling is currently selected
74-
// tree.select(nodeId, index) - switch to a different sibling
75-
// tree.getNode(nodeId) - look up a node by msgId
71+
// view.hasSiblings(nodeId) - does this message have alternatives?
72+
// view.getSiblings(nodeId) - all alternatives at this fork point
73+
// view.getSelectedIndex(nodeId) - which sibling is currently selected
74+
// view.select(nodeId, index) - switch to a different sibling
75+
// view.getNode(nodeId) - look up a node by msgId
7676
//
77-
// nodeId is the msgId on each TreeNode — iterate tree.flattenNodes():
78-
// transport.tree.flattenNodes().map((node) => {
77+
// nodeId is the msgId on each TreeNode — iterate view.nodes:
78+
// view.nodes.map((node) => {
7979
// const nodeId = node.msgId;
8080
// });
8181
```
8282

8383
Build a sibling navigator (where `nodeId` is the resolved `x-ably-msg-id` for the message):
8484

8585
```typescript
86-
{tree.hasSiblings(nodeId) && (
86+
{view.hasSiblings(nodeId) && (
8787
<div>
8888
<button
89-
onClick={() => tree.select(nodeId, tree.getSelectedIndex(nodeId) - 1)}
90-
disabled={tree.getSelectedIndex(nodeId) === 0}
89+
onClick={() => view.select(nodeId, view.getSelectedIndex(nodeId) - 1)}
90+
disabled={view.getSelectedIndex(nodeId) === 0}
9191
>
9292
9393
</button>
94-
<span>{tree.getSelectedIndex(nodeId) + 1} / {tree.getSiblings(nodeId).length}</span>
94+
<span>{view.getSelectedIndex(nodeId) + 1} / {view.getSiblings(nodeId).length}</span>
9595
<button
96-
onClick={() => tree.select(nodeId, tree.getSelectedIndex(nodeId) + 1)}
97-
disabled={tree.getSelectedIndex(nodeId) === tree.getSiblings(nodeId).length - 1}
96+
onClick={() => view.select(nodeId, view.getSelectedIndex(nodeId) + 1)}
97+
disabled={view.getSelectedIndex(nodeId) === view.getSiblings(nodeId).length - 1}
9898
>
9999
100100
</button>
101101
</div>
102102
)}
103103
```
104104

105-
Calling `select` updates the tree's active branch. The view re-renders with the selected path.
105+
Calling `select` updates the view's active branch and re-renders with the selected path.
106106

107107
## Server handling
108108

docs/features/cancel.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ Cancel a specific turn or all matching turns:
1010

1111
```typescript
1212
// Cancel a specific turn (returned by send/regenerate/edit)
13-
const turn = await transport.send(userMessage);
13+
const turn = await view.send(userMessage);
1414
await turn.cancel();
1515

1616
// Cancel all your own active turns

docs/features/concurrent-turns.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@ Without concurrent turn support, a transport must serialize interactions: one re
66

77
## How it works
88

9-
Each call to `send()`, `regenerate()`, or `edit()` on the client creates a new turn. On the server, each incoming request calls `newTurn()`. Turns are identified by `turnId` and tracked by `clientId`.
9+
Each call to `view.send()`, `view.regenerate()`, or `view.edit()` on the client creates a new turn. On the server, each incoming request calls `newTurn()`. Turns are identified by `turnId` and tracked by `clientId`.
1010

1111
```typescript
1212
// Client: two sends in quick succession create two concurrent turns
13-
const turnA = await transport.send(messageA);
14-
const turnB = await transport.send(messageB);
13+
const turnA = await view.send(messageA);
14+
const turnB = await view.send(messageB);
1515

1616
// Each has its own stream
1717
const readerA = turnA.stream.getReader();

docs/features/history.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# History and replay
22

3-
`transport.view.loadOlder(limit)` loads conversation history from the Ably channel. A new client - after a page refresh, on a new device, or joining mid-conversation - can hydrate the full conversation from channel history without a separate database.
3+
`view.loadOlder(limit)` loads conversation history from the Ably channel. A new client - after a page refresh, on a new device, or joining mid-conversation - can hydrate the full conversation from channel history without a separate database.
44

55
Without persistent history, page refresh means starting over. With AI Transport, messages are persisted on the Ably channel and decoded through the same codec used for live streaming.
66

@@ -14,7 +14,7 @@ await view.loadOlder(30);
1414
// Call loadOlder again to fetch more older messages
1515
```
1616

17-
History messages are inserted into the transport's conversation tree and trigger an `'update'` notification on the view. After loading history, `transport.view.flattenNodes().map(n => n.message)` returns the combined history + live messages - flattened along the currently selected branch. If the history contains forks (from regeneration or editing), only the active branch is included. Use the conversation tree to navigate between branches (see [Conversation branching](branching.md)).
17+
History messages are inserted into the transport's conversation tree and trigger an `'update'` notification on the view. After loading history, `view.flattenNodes().map(n => n.message)` returns the combined history + live messages - flattened along the currently selected branch. If the history contains forks (from regeneration or editing), only the active branch is included. Use the conversation tree to navigate between branches (see [Conversation branching](branching.md)).
1818

1919
The `limit` parameter controls how many **complete domain messages** to return, not how many Ably wire messages to fetch. A single assistant message may span dozens of Ably messages (one per append). The implementation pages through Ably history until `limit` complete messages have been assembled.
2020

@@ -66,7 +66,7 @@ const { nodes, hasOlder, loading, loadOlder } = useView(transport, { limit: 30 }
6666

6767
History messages carry the same `x-ably-parent` and `x-ably-fork-of` headers as live messages. When loaded, they're inserted into the conversation tree with their full branch structure intact. A client loading history sees the same tree of branches and can navigate siblings just like a client that was present for the original conversation.
6868

69-
Because the tree may contain multiple branches, the view's `flattenNodes()` returns only the messages along the currently selected path - not every message ever published. To see alternative branches, use `useTree` or the tree's `getSiblings()` / `select()` methods.
69+
Because the tree may contain multiple branches, the view's `flattenNodes()` returns only the messages along the currently selected path - not every message ever published. To see alternative branches, use `useView` or the view's `getSiblings()` / `select()` methods.
7070

7171
See [Conversation branching](branching.md) for the tree model.
7272

docs/features/interruption.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,10 @@ Two patterns:
1818
The most common pattern: cancel active turns before sending the new message.
1919

2020
```typescript
21-
import { useActiveTurns, useSend } from '@ably/ai-transport/react';
21+
import { useActiveTurns, useView } from '@ably/ai-transport/react';
2222

2323
const activeTurns = useActiveTurns(transport);
24-
const send = useSend(transport);
24+
const { send } = useView(transport);
2525
const isStreaming = activeTurns.size > 0;
2626

2727
async function handleSend(text: string) {

docs/features/optimistic-updates.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ Without optimistic insertion, the user would see a gap between pressing "send" a
66

77
## How it works
88

9-
The client generates a unique message ID (`x-ably-msg-id`) for each user message and inserts it into the conversation tree with no [serial](../internals/glossary.md#serial-ably) (Ably's server-assigned ordering identifier). The message is visible via `transport.view.flattenNodes()` immediately. The HTTP POST to the server is [fire-and-forget](../internals/glossary.md#fire-and-forget) - `send()` returns without waiting for the server to respond.
9+
The client generates a unique message ID (`x-ably-msg-id`) for each user message and inserts it into the conversation tree with no [serial](../internals/glossary.md#serial-ably) (Ably's server-assigned ordering identifier). The message is visible via `view.flattenNodes()` immediately. The HTTP POST to the server is [fire-and-forget](../internals/glossary.md#fire-and-forget) - `send()` returns without waiting for the server to respond.
1010

1111
The server receives the user message and relays it onto the Ably channel, preserving the original `x-ably-msg-id`. All clients on the channel - including the sender - receive this relay. The sending client recognises its own message by matching the `x-ably-msg-id` against the set of IDs it optimistically inserted. Instead of creating a duplicate, it updates the existing entry with the server-assigned serial, which moves the message from the end of the list to its correct position in serial order. This process is called [optimistic reconciliation](../internals/glossary.md#optimistic-reconciliation).
1212

@@ -30,20 +30,20 @@ sequenceDiagram
3030
Optimistic updates are automatic - there is no opt-in or configuration. Every call to `send()`, `edit()`, or `regenerate()` that includes user messages uses the same mechanism.
3131

3232
```typescript
33-
const turn = await transport.send(userMessage);
33+
const view = transport.view;
34+
const turn = await view.send(userMessage);
3435

3536
// The user message is already in the view - no waiting for the server
36-
const messages = transport.view.flattenNodes().map(n => n.message);
37+
const messages = view.flattenNodes().map(n => n.message);
3738
// messages includes userMessage at the end of the conversation
3839
```
3940

4041
In React, `useView()` re-renders immediately after `send()` because the optimistic insert triggers an `update` event on the view:
4142

4243
```typescript
43-
import { useView, useSend } from '@ably/ai-transport/react';
44+
import { useView } from '@ably/ai-transport/react';
4445

45-
const { nodes } = useView(transport);
46-
const send = useSend(transport);
46+
const { nodes, send } = useView(transport);
4747

4848
// After send(), messages updates instantly with the new user message
4949
await send([userMessage]);
@@ -69,7 +69,7 @@ When `send()` receives an array of messages, each gets its own `x-ably-msg-id` a
6969

7070
```typescript
7171
// Both messages appear immediately, chained in order
72-
const turn = await transport.send([questionOne, questionTwo]);
72+
const turn = await view.send([questionOne, questionTwo]);
7373
```
7474

7575
## Edge cases

docs/features/streaming.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,11 +64,12 @@ transport.close();
6464
On the client, every streaming event is accumulated into the conversation tree as it arrives. The view updates on every event, so the last assistant message grows token by token:
6565

6666
```typescript
67-
const turn = await transport.send(userMessage);
67+
const view = transport.view;
68+
const turn = await view.send(userMessage);
6869

6970
// Subscribe to accumulated messages - updates on every token
70-
const unsubscribe = transport.view.on('update', () => {
71-
const messages = transport.view.flattenNodes().map(n => n.message);
71+
const unsubscribe = view.on('update', () => {
72+
const messages = view.flattenNodes().map(n => n.message);
7273
// the last assistant message grows as tokens arrive
7374
});
7475
```
@@ -81,7 +82,7 @@ This is the primary consumption path. In React, the `useView()` hook handles the
8182

8283
```typescript
8384
// Framework adapter usage - most apps won't consume this directly
84-
const turn = await transport.send(userMessage);
85+
const turn = await view.send(userMessage);
8586
const reader = turn.stream.getReader();
8687
while (true) {
8788
const { done, value } = await reader.read();

docs/frameworks/vercel-ai-sdk.md

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -47,32 +47,24 @@ Use the generic React hooks directly. You manage message state through the trans
4747
import {
4848
useClientTransport,
4949
useView,
50-
useTree,
51-
useSend,
52-
useRegenerate,
53-
useEdit,
5450
useActiveTurns,
5551
} from '@ably/ai-transport/react';
5652
import { UIMessageCodec } from '@ably/ai-transport/vercel';
5753

5854
const transport = useClientTransport({ channel, codec: UIMessageCodec, clientId });
59-
const { nodes, hasOlder, loading, loadOlder } = useView(transport, { limit: 30 });
60-
const tree = useTree(transport);
61-
const send = useSend(transport);
62-
const regenerate = useRegenerate(transport);
63-
const edit = useEdit(transport);
55+
const { nodes, hasOlder, loading, loadOlder, send, regenerate, edit, select, getSelectedIndex, getSiblings, hasSiblings } = useView(transport, { limit: 30 });
6456
const activeTurns = useActiveTurns(transport);
6557
```
6658

67-
This path gives you conversation branching UI (sibling navigation), per-operation hooks, and direct access to the tree state.
59+
This path gives you conversation branching UI (sibling navigation), write operations, and direct access to the view state.
6860

6961
### When to use which
7062

7163
| Use useChat when... | Use generic hooks when... |
7264
|---|---|
7365
| You want the simplest integration | You need conversation branching UI |
7466
| `useChat`'s message state management is sufficient | You need custom message construction |
75-
| You don't need edit or branch navigation | You need `edit()` or `tree.select()` |
67+
| You don't need edit or branch navigation | You need `edit()` or `view.select()` |
7668
| You're already using `useChat` and adding AI Transport | You're building a custom chat UI from scratch |
7769

7870
## Entry points

0 commit comments

Comments
 (0)