Skip to content

Commit d9c2fca

Browse files
authored
docs: update documentation for 4.27.0 release (#439)
* docs: update documentation for 4.27.0 release * docs: fix broken apiUrl link in adapters feature matrix * docs: fix error code thrown-by columns and add missing UNKNOWN_USER_ID_FORMAT
1 parent 7b4480a commit d9c2fca

21 files changed

Lines changed: 322 additions & 38 deletions

File tree

apps/docs/content/docs/adapters.mdx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ Ready to build your own? Follow the [building](/docs/contributing/building) guid
2020
| Edit message | <Check /> | <Check /> | <Check /> | <Check /> | <Check /> | <Check /> | <Check /> | <Check /> |
2121
| Delete message | <Check /> | <Check /> | <Check /> | <Check /> | <Check /> | <Check /> | <Check /> | <Cross /> |
2222
| File uploads | <Check /> | <Check /> | <Cross /> | <Check /> | <Warn /> Single file | <Cross /> | <Cross /> | <Check /> Images, audio, docs |
23-
| Streaming | <Check /> Native | <Warn /> Post+Edit | <Warn /> Post+Edit | <Warn /> Post+Edit | <Warn /> Post+Edit | <Cross /> | <Cross /> | <Cross /> |
23+
| Streaming | <Check /> Native | <Warn /> Native (DMs) / Post+Edit | <Warn /> Post+Edit | <Warn /> Post+Edit | <Warn /> Post+Edit | <Cross /> | <Cross /> | <Cross /> |
2424
| Scheduled messages | <Check /> Native | <Cross /> | <Cross /> | <Cross /> | <Cross /> | <Cross /> | <Cross /> | <Cross /> |
2525

2626
### Rich content
@@ -47,6 +47,8 @@ Ready to build your own? Follow the [building](/docs/contributing/building) guid
4747
| Typing indicator | <Cross /> | <Check /> | <Cross /> | <Check /> | <Check /> | <Cross /> | <Cross /> | <Cross /> |
4848
| DMs | <Check /> | <Check /> | <Check /> | <Check /> | <Check /> | <Cross /> | <Cross /> | <Check /> |
4949
| Ephemeral messages | <Check /> Native | <Cross /> | <Check /> Native | <Cross /> | <Cross /> | <Cross /> | <Cross /> | <Cross /> |
50+
| User lookup ([`getUser`](/docs/api/chat#getuser)) | <Check /> | <Warn /> Cached | <Warn /> Cached | <Check /> | <Warn /> Seen users | <Check /> | <Check /> | <Cross /> |
51+
| Custom API endpoint (`apiUrl`) | <Check /> | <Check /> | <Check /> | <Check /> | <Check /> | <Check /> | <Check /> | <Check /> |
5052

5153
### Message history
5254

apps/docs/content/docs/api/chat.mdx

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,44 @@ Returns `ModalResponse | undefined` to control the modal after submission:
261261
- `{ action: "update", modal: ModalElement }` — replace the modal content
262262
- `{ action: "push", modal: ModalElement }` — push a new modal view onto the stack
263263

264+
### onOptionsLoad
265+
266+
Fires when an `ExternalSelect` requests options dynamically. The handler is keyed on the select's `id` and must return options synchronously enough for Slack's 3-second budget (the adapter caps the loader at ~2.5s and substitutes an empty result on timeout). Slack-only.
267+
268+
```typescript
269+
bot.onOptionsLoad("assignee", async (event) => {
270+
const people = await peopleService.search(event.query);
271+
return people.map((p) => ({ label: p.fullName, value: p.id }));
272+
});
273+
```
274+
275+
Return an array of `OptionsLoadGroup` (`{ label, options }[]`) instead of a flat array to render grouped headers (e.g. "Recent" / "All"). Slack limits: max 100 groups, max 100 options per group.
276+
277+
<TypeTable
278+
type={{
279+
'event.actionId': {
280+
description: 'The id of the select requesting options (matches the id passed to bot.onOptionsLoad).',
281+
type: 'string',
282+
},
283+
'event.query': {
284+
description: 'The text the user has typed so far.',
285+
type: 'string',
286+
},
287+
'event.user': {
288+
description: 'The user requesting options.',
289+
type: 'Author',
290+
},
291+
'event.adapter': {
292+
description: 'The adapter that received this event.',
293+
type: 'Adapter',
294+
},
295+
'event.raw': {
296+
description: 'Raw platform-specific payload.',
297+
type: 'unknown',
298+
},
299+
}}
300+
/>
301+
264302
### onSlashCommand
265303

266304
Fires when a user invokes a `/command` in the message composer. Currently supported on Slack.
@@ -498,10 +536,16 @@ const user = await bot.getUser(message.author);
498536
- **GitHub**`email` is `null` unless the user made it public, or you authenticated with the `user:email` scope.
499537
- **Linear** — full profile (incl. email + avatar) for any active workspace member.
500538

501-
Fields that aren't available return `undefined`. Numeric user IDs (Discord/Telegram/GitHub) can be ambiguous when multiple of those adapters are registered — call the platform's adapter directly (`adapter.getUser(userId)`) in that case.
539+
Fields that aren't available return `undefined`. Numeric user IDs (Discord/Telegram/GitHub) can be ambiguous when multiple of those adapters are registered — `bot.getUser` throws a `ChatError` with code `AMBIGUOUS_USER_ID` in that case. Pass an `Author` from a message handler (which already carries the adapter), or call the adapter directly (`adapter.getUser(userId)`).
502540
</Callout>
503541

504-
Adapters that don't support user lookups will throw a `ChatError` with code `NOT_SUPPORTED`. Handle both cases if your bot runs on multiple platforms:
542+
`bot.getUser` throws a `ChatError` in three cases. Handle them if your bot runs on multiple platforms:
543+
544+
| Code | When |
545+
|------|------|
546+
| `NOT_SUPPORTED` | The resolved adapter doesn't implement `getUser` (e.g. WhatsApp) |
547+
| `AMBIGUOUS_USER_ID` | A numeric user ID could belong to more than one registered adapter (Discord/Telegram/GitHub) |
548+
| `UNKNOWN_USER_ID_FORMAT` | The `userId` string doesn't match any registered platform's ID format |
505549

506550
```typescript
507551
import { ChatError } from "chat";
@@ -512,8 +556,14 @@ try {
512556
// User not found on this platform
513557
}
514558
} catch (error) {
515-
if (error instanceof ChatError && error.code === "NOT_SUPPORTED") {
516-
// This adapter doesn't support user lookups
559+
if (error instanceof ChatError) {
560+
if (error.code === "NOT_SUPPORTED") {
561+
// This adapter doesn't support user lookups
562+
} else if (error.code === "AMBIGUOUS_USER_ID") {
563+
// Pass message.author or call adapter.getUser(userId) directly
564+
} else if (error.code === "UNKNOWN_USER_ID_FORMAT") {
565+
// userId doesn't match any known platform format
566+
}
517567
}
518568
}
519569
```

apps/docs/content/docs/api/index.mdx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ import { Chat, root, paragraph, text, Card, Button, emoji } from "chat";
3131
| Export | Description |
3232
|--------|-------------|
3333
| [`PostableMessage`](/docs/api/postable-message) | Union type accepted by `thread.post()` |
34+
| [`Plan`](/docs/api/postable-message#plan) | Step-by-step task list that mutates after posting |
35+
| [`StreamingPlan`](/docs/api/postable-message#streamingplan) | Wraps an async iterable with platform-specific streaming options |
3436
| [`Cards`](/docs/api/cards) | Rich card components — `Card`, `Text`, `Button`, `Actions`, etc. |
3537
| [`Markdown`](/docs/api/markdown) | AST builder functions — `root`, `paragraph`, `text`, `strong`, etc. |
3638
| [`Modals`](/docs/api/modals) | Modal form components — `Modal`, `TextInput`, `Select`, etc. |

apps/docs/content/docs/api/markdown.mdx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,25 @@ import {
1515
} from "chat";
1616
```
1717

18+
## Type re-exports
19+
20+
The chat package re-exports mdast's union and content types so adapters and downstream code can build exhaustively-typed AST walkers without depending on `mdast` directly:
21+
22+
```typescript
23+
import type { Nodes, Root, Content } from "chat";
24+
25+
function render(node: Nodes): string {
26+
switch (node.type) {
27+
case "text": return node.value;
28+
case "strong": return node.children.map(render).join("");
29+
// ...
30+
default: throw new Error(`Unhandled: ${node satisfies never}`);
31+
}
32+
}
33+
```
34+
35+
Adapters use this pattern to make the type checker reject the build when a new mdast node type is introduced upstream.
36+
1837
## Node builders
1938

2039
### root

apps/docs/content/docs/api/modals.mdx

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,52 @@ Select({
167167
}}
168168
/>
169169

170+
## ExternalSelect
171+
172+
Dropdown that loads options dynamically from a handler as the user types. Slack-only. Pair with [`bot.onOptionsLoad`](/docs/api/chat#onoptionsload) to supply options. See [Modals → ExternalSelect](/docs/modals#externalselect) for a full example, grouped-options support, and Slack setup notes.
173+
174+
```typescript
175+
ExternalSelect({
176+
id: "assignee",
177+
label: "Assignee",
178+
placeholder: "Search people",
179+
minQueryLength: 1,
180+
initialOption: { label: "Alice", value: "U123" },
181+
})
182+
```
183+
184+
<TypeTable
185+
type={{
186+
id: {
187+
description: 'Input ID — used as the key in event.values.',
188+
type: 'string',
189+
},
190+
label: {
191+
description: 'Label displayed above the select.',
192+
type: 'string',
193+
},
194+
placeholder: {
195+
description: 'Placeholder text.',
196+
type: 'string',
197+
},
198+
minQueryLength: {
199+
description: 'Minimum characters before the loader fires (Slack default: 3).',
200+
type: 'number',
201+
},
202+
initialOption: {
203+
description: 'Pre-selected option when the modal opens. Unlike static Select where initialOption is a value string, ExternalSelect needs the full label/value object since the loader has not run yet.',
204+
type: '{ label: string, value: string }',
205+
},
206+
optional: {
207+
description: 'Whether the field can be left empty.',
208+
type: 'boolean',
209+
default: 'false',
210+
},
211+
}}
212+
/>
213+
214+
The loader registered via `bot.onOptionsLoad("assignee", handler)` returns either a flat `SelectOptionElement[]` or `OptionsLoadGroup[]` (`{ label, options }[]`) for grouped options.
215+
170216
## RadioSelect
171217

172218
Radio button group for mutually exclusive choices.

apps/docs/content/docs/api/postable-message.mdx

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,14 @@ type: reference
77
`PostableMessage` is the union of all message formats accepted by `thread.post()` and `sent.edit()`.
88

99
```typescript
10-
type PostableMessage = AdapterPostableMessage | AsyncIterable<string | StreamChunk | StreamEvent>;
10+
type PostableMessage =
11+
| AdapterPostableMessage
12+
| AsyncIterable<string | StreamChunk | StreamEvent>
13+
| PostableObject;
1114
```
1215

16+
`PostableObject` covers `Plan` (mutable task lists) and `StreamingPlan` (streams with platform-specific options) — both documented below.
17+
1318
## String
1419

1520
Raw text passed through as-is to the platform.
@@ -133,6 +138,55 @@ await thread.post({
133138
}}
134139
/>
135140

141+
## Plan
142+
143+
A `Plan` is a step-by-step task list that mutates after posting. Each `addTask` / `updateTask` / `complete` call re-renders the same message in place. See [Plan API](/docs/streaming#plan-api) for full usage.
144+
145+
```typescript
146+
import { Plan } from "chat";
147+
148+
const plan = new Plan({ initialMessage: "Researching options..." });
149+
await thread.post(plan);
150+
await plan.addTask({ title: "Look up records" });
151+
await plan.complete({ completeMessage: "Done!" });
152+
```
153+
154+
Adapters that don't support `PostableObject` editing render the plan as fallback text and ignore subsequent mutations.
155+
156+
## StreamingPlan
157+
158+
Wraps an async iterable with platform-specific streaming options. Use this when you need to pass options like task grouping or stop blocks through `thread.post()`. See [Streaming with options](/docs/streaming#streaming-with-options).
159+
160+
```typescript
161+
import { StreamingPlan } from "chat";
162+
163+
const planned = new StreamingPlan(stream, {
164+
groupTasks: "plan",
165+
endWith: [feedbackBlock],
166+
updateIntervalMs: 750,
167+
});
168+
169+
await thread.post(planned);
170+
```
171+
172+
<TypeTable
173+
type={{
174+
groupTasks: {
175+
description: 'Slack: render task_update chunks as `"plan"` (single grouped block) or `"timeline"` (inline cards, default).',
176+
type: '"plan" | "timeline" | undefined',
177+
},
178+
endWith: {
179+
description: 'Slack: Block Kit elements appended when the stream stops (e.g. retry / feedback buttons).',
180+
type: 'unknown[] | undefined',
181+
},
182+
updateIntervalMs: {
183+
description: 'Fallback adapters: minimum interval between post+edit cycles in ms.',
184+
type: 'number | undefined',
185+
default: '500',
186+
},
187+
}}
188+
/>
189+
136190
## AsyncIterable (streaming)
137191

138192
An async iterable of strings, `StreamChunk` objects, or stream events. The SDK streams the message in real time using platform-native APIs where available.

apps/docs/content/docs/api/thread.mdx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ A `Thread` is provided to your event handlers and represents a conversation thre
3939

4040
## post
4141

42-
Post a message to the thread. Accepts strings, structured messages, cards, and streams.
42+
Post a message to the thread. Accepts strings, structured messages, cards, streams, and `PostableObject` instances (`Plan`, `StreamingPlan`).
4343

4444
```typescript
4545
// Plain text
@@ -56,11 +56,19 @@ await thread.post(Card({ title: "Hi", children: [Text("Hello")] }));
5656

5757
// Stream (fullStream recommended for multi-step agents)
5858
await thread.post(result.fullStream);
59+
60+
// Plan (mutable task list)
61+
const plan = new Plan({ initialMessage: "Working..." });
62+
await thread.post(plan);
63+
await plan.addTask({ title: "Step 1" });
64+
65+
// Streaming with platform options
66+
await thread.post(new StreamingPlan(stream, { groupTasks: "plan" }));
5967
```
6068

6169
**Parameters:** `message: string | PostableMessage | CardJSXElement`
6270

63-
**Returns:** `Promise<SentMessage>`the sent message with `edit()`, `delete()`, `addReaction()`, and `removeReaction()` methods.
71+
**Returns:** `Promise<SentMessage | PostableObject>`for plain messages and streams, a `SentMessage` with `edit()`, `delete()`, `addReaction()`, and `removeReaction()` methods; for `Plan` / `StreamingPlan` inputs, the same object is returned so you can keep mutating it.
6472

6573
See [Posting Messages](/docs/posting-messages) for details on each format.
6674

apps/docs/content/docs/concurrency.mdx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,10 @@ const bot = new Chat({
124124
| `debounceMs` | debounce | `1500` | Debounce window in milliseconds |
125125
| `maxConcurrent` | concurrent | `Infinity` | Max concurrent handlers per thread |
126126

127+
<Callout type="warn">
128+
`maxConcurrent` only applies to the `concurrent` strategy. Pairing it with any other strategy logs a warning and the value is ignored. Setting `maxConcurrent` to a value less than `1` throws at construction time — `0` would deadlock the strategy and is rejected up front.
129+
</Callout>
130+
127131
## MessageContext
128132

129133
All handler types (`onNewMention`, `onSubscribedMessage`, `onNewMessage`) accept an optional `MessageContext` as their last parameter. It is only populated when using the `queue` strategy and messages were skipped.

apps/docs/content/docs/error-handling.mdx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,19 @@ import { ChatError, RateLimitError, NotImplementedError, LockError } from "chat"
1616

1717
### ChatError
1818

19-
Base error class for all SDK errors. Every error below extends `ChatError`.
19+
Base error class for all SDK errors. Every error below extends `ChatError`. The `code` property carries a machine-readable identifier you can branch on:
20+
21+
| Code | Thrown by | Meaning |
22+
|------|-----------|---------|
23+
| `NOT_SUPPORTED` | `bot.openDM`, `bot.getUser` | The resolved adapter doesn't implement this method |
24+
| `INVALID_THREAD_ID` | `bot.thread`, internal routing | Thread ID does not match the `adapter:channel:thread` shape |
25+
| `INVALID_CHANNEL_ID` | `bot.channel` | Channel ID does not match the `adapter:channel` shape |
26+
| `ADAPTER_NOT_FOUND` | `bot.thread`, `bot.channel` | Thread/channel ID references an adapter that wasn't registered on this `Chat` instance |
27+
| `AMBIGUOUS_USER_ID` | `bot.getUser`, `bot.openDM` | Numeric user ID could match more than one registered adapter (Discord/Telegram/GitHub) |
28+
| `UNKNOWN_USER_ID_FORMAT` | `bot.getUser`, `bot.openDM` | The `userId` doesn't match any platform's known ID format |
29+
| `RATE_LIMITED` | Any platform call | Platform returned 429; see `RateLimitError` below |
30+
| `NOT_IMPLEMENTED` | Any platform call | The adapter doesn't implement this feature; see `NotImplementedError` below |
31+
| `LOCK_FAILED` | Inbound message routing | Distributed lock was busy; see `LockError` below |
2032

2133
<TypeTable
2234
type={{
@@ -25,7 +37,7 @@ Base error class for all SDK errors. Every error below extends `ChatError`.
2537
type: 'string',
2638
},
2739
code: {
28-
description: 'Machine-readable error code.',
40+
description: 'Machine-readable error code (see table above).',
2941
type: 'string',
3042
},
3143
cause: {

apps/docs/content/docs/index.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ Each adapter factory auto-detects credentials from environment variables (`SLACK
5252
| Platform | Package | Mentions | Reactions | Cards | Modals | Streaming | DMs |
5353
|----------|---------|----------|-----------|-------|--------|-----------|-----|
5454
| Slack | `@chat-adapter/slack` | Yes | Yes | Yes | Yes | Native | Yes |
55-
| Microsoft Teams | `@chat-adapter/teams` | Yes | Read-only | Yes | No | Post+Edit | Yes |
55+
| Microsoft Teams | `@chat-adapter/teams` | Yes | Read-only | Yes | No | Native (DMs) / Post+Edit | Yes |
5656
| Google Chat | `@chat-adapter/gchat` | Yes | Yes | Yes | No | Post+Edit | Yes |
5757
| Discord | `@chat-adapter/discord` | Yes | Yes | Yes | No | Post+Edit | Yes |
5858
| Telegram | `@chat-adapter/telegram` | Yes | Yes | Partial | No | Post+Edit | Yes |

0 commit comments

Comments
 (0)