Skip to content

Commit ce08b7f

Browse files
docs: update spectrum-ts documentation for v3.0.0 (#83)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent 082dbcd commit ce08b7f

12 files changed

Lines changed: 168 additions & 67 deletions

docs-src/spectrum-ts/content.mdx.vel

Lines changed: 47 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
---
22
title: "Content"
3-
description: "Build text, attachments, voice, contacts, polls, rich links, groups, replies, edits, typing indicators, rename, avatar, and platform-specific content for outgoing messages"
3+
description: "Build text, markdown, attachments, voice, contacts, polls, rich links, groups, replies, edits, unsends, typing indicators, rename, avatar, and platform-specific content for outgoing messages"
44
---
55

66
import { TypeTooltip } from "/snippets/type-tooltip.mdx";
77

88
{% set ci = symbol("ts:spectrum-ts#ContentInput") %}
99
{% set cb = symbol("ts:spectrum-ts#ContentBuilder") %}
1010

11-
Spectrum exposes a family of content builders — `text`, `streamText`, `attachment`, `voice`, `contact`, `richlink`, `poll`, `group`, `custom`, `reaction`, `reply`, `edit`, `typing`, `rename`, and `avatar` — plus a string shortcut that's equivalent to `text()`. Any API that takes a <TypeTooltip name="ContentInput" type={`{{ ci.signature }}`} /> accepts a plain string or a <TypeTooltip name="ContentBuilder" type={`{{ cb.signature }}`} />.
11+
Spectrum exposes a family of content builders — `text`, `markdown`, `attachment`, `voice`, `contact`, `richlink`, `poll`, `group`, `custom`, `reaction`, `reply`, `edit`, `unsend`, `typing`, `rename`, and `avatar` — plus a string shortcut that's equivalent to `text()`. Any API that takes a <TypeTooltip name="ContentInput" type={`{{ ci.signature }}`} /> accepts a plain string or a <TypeTooltip name="ContentBuilder" type={`{{ cb.signature }}`} />.
1212

1313
## Text
1414

@@ -23,50 +23,66 @@ await space.send("Hello, world.");
2323

2424
## Streaming text
2525

26-
Send streaming LLM output as Spectrum content. The `streamText` builder wraps an async stream of text deltas so it can be sent like any other content item. On platforms that support it (iMessage in remote mode), the first chunk is sent immediately as a real message and then edited in place as more text arrives.
27-
28-
```ts
29-
import { streamText } from "spectrum-ts";
30-
```
31-
32-
`streamText` accepts whatever the popular LLM SDKs return — the Vercel AI SDK `streamText()` result, a raw `AsyncIterable` of chunks, or a `ReadableStream`:
26+
Both `text()` and `markdown()` accept a streaming source — an AI SDK result, an `AsyncIterable`, or a `ReadableStream` — in addition to a plain string. On platforms that support it, text streams live: iMessage (remote) sends the first chunk as a real message and edits in place as more text arrives; Telegram (private chats) animates a native draft preview. Platforms without streaming support wait for the stream to finish and send the full text as one message.
3327

3428
<Tabs>
3529
<Tab title="Vercel AI SDK">
3630
```ts
37-
import { streamText } from "spectrum-ts";
31+
import { text } from "spectrum-ts";
3832
import { streamText as aiStreamText } from "ai";
3933

4034
const result = aiStreamText({ model, prompt: message.content.text });
41-
await space.send(streamText(result));
35+
await space.send(text(result));
4236
```
4337
</Tab>
4438
<Tab title="AsyncIterable">
4539
```ts
46-
import { streamText } from "spectrum-ts";
40+
import { text } from "spectrum-ts";
4741

4842
async function* generate() {
4943
yield "Hello, ";
5044
yield "world!";
5145
}
5246

53-
await space.send(streamText(generate()));
47+
await space.send(text(generate()));
5448
```
5549
</Tab>
5650
<Tab title="Custom extractor">
5751
```ts
58-
import { streamText } from "spectrum-ts";
52+
import { text } from "spectrum-ts";
5953

6054
await space.send(
61-
streamText(customStream, {
55+
text(customStream, {
6256
extract: (chunk) => chunk.delta?.text ?? null,
6357
}),
6458
);
6559
```
6660
</Tab>
6761
</Tabs>
6862

69-
Platforms that cannot stream silently skip the send with a warning.
63+
A stream can only be sent once. Pass `options.extract` for any chunk shape the built-in auto-detection doesn't recognize.
64+
65+
## Markdown
66+
67+
Send styled text written in standard markdown (CommonMark plus GFM tables and strikethrough). Each platform renders markdown to its native format — Telegram uses `parse_mode: "HTML"`, iMessage (remote) uses UTF-16 styled text formatting ranges. Platforms without native markdown support receive readable plain text via the send pipeline's automatic fallback.
68+
69+
```ts
70+
import { markdown } from "spectrum-ts";
71+
72+
await space.send(markdown("**Bold** and _italic_ text."));
73+
```
74+
75+
`markdown()` also accepts a stream source, just like `text()`. Markdown streams render progressively on platforms with native support; everywhere else the accumulated text falls back through the markdown pipeline instead of surfacing raw `**` markers:
76+
77+
```ts
78+
import { markdown } from "spectrum-ts";
79+
import { streamText as aiStreamText } from "ai";
80+
81+
const result = aiStreamText({ model, prompt: message.content.text });
82+
await space.send(markdown(result));
83+
```
84+
85+
Markdown is outbound-only by design — inbound messages always surface as `text` content regardless of platform formatting.
7086

7187
## Attachments
7288

@@ -275,7 +291,7 @@ import { reply, text } from "spectrum-ts";
275291
await space.send(reply(text("Got it"), message));
276292
```
277293

278-
`reply()` cannot wrap `reply`, `edit`, `reaction`, `group`, `typing`, `rename`, or `avatar` content.
294+
`reply()` cannot wrap `reply`, `edit`, `reaction`, `group`, `typing`, `rename`, `avatar`, or `unsend` content.
279295

280296
## Edits
281297

@@ -288,7 +304,20 @@ const sent = await space.send("Draft");
288304
await space.send(edit(text("Final version"), sent));
289305
```
290306

291-
`edit()` cannot wrap `edit`, `reply`, `reaction`, `group`, `typing`, `rename`, or `avatar` content.
307+
`edit()` cannot wrap `edit`, `reply`, `reaction`, `group`, `typing`, `rename`, `avatar`, or `unsend` content.
308+
309+
## Unsend
310+
311+
Retract a previously-sent outbound message. Unsends are fire-and-forget — `space.send(unsend(...))` resolves to `undefined`.
312+
313+
```ts
314+
import { unsend } from "spectrum-ts";
315+
316+
const sent = await space.send("Oops");
317+
await space.send(unsend(sent));
318+
```
319+
320+
`message.unsend()` and `space.unsend(message)` are sugar for `space.send(unsend(message))`. Only outbound messages can be unsent — the builder throws at build time for inbound targets. Platform constraints (e.g. iMessage enforces Apple's ~2-minute unsend window for regular messages) surface from the provider at send time.
292321

293322
## Typing indicators
294323

docs-src/spectrum-ts/custom-platforms.mdx.vel

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,9 @@ export const myPlatform = definePlatform("my-platform", {
3737
}),
3838
},
3939

40-
// Resolve or create a conversation
40+
// Create a conversation from participants
4141
space: {
42-
resolve: async ({ input, client }) => ({
42+
create: async ({ input, client }) => ({
4343
id: await client.findOrCreateConversation(input.users.map(u => u.id)),
4444
}),
4545
},
@@ -69,8 +69,7 @@ export const myPlatform = definePlatform("my-platform", {
6969
case "text":
7070
return await client.send(space.id, content.text);
7171
case "reaction":
72-
await client.react(space.id, content.target.id, content.emoji);
73-
return;
72+
return await client.react(space.id, content.target.id, content.emoji);
7473
case "reply":
7574
return await client.reply(space.id, content.target.id, content.content);
7675
case "typing":
@@ -93,14 +92,15 @@ export const myPlatform = definePlatform("my-platform", {
9392
| `config` | Yes | A Zod schema that validates the object passed to `platform.config()`. If every field is optional, `platform.config()` can be called with no arguments. |
9493
| `user.resolve` | Yes | Resolves a user from a string ID. Returns at minimum `{ id: string }`. |
9594
| `user.schema` | No | Optional Zod schema for extra user properties. |
96-
| `space.resolve` | Yes | Resolves or creates a conversation. Receives an array of users plus optional params. |
95+
| `space.create` | Yes | Creates a conversation from participants. Receives an array of users plus optional params. |
96+
| `space.get` | No | Hydrates a space from a known platform space ID. When omitted, the framework builds `{ id }` and validates it against `space.schema`. Providers whose schema requires more fields must implement this. |
9797
| `space.schema` | No | Optional Zod schema for the resolved space. |
98-
| `space.params` | No | Zod schema for additional space creation parameters — surfaces as the second arg to `platform(app).space()`. |
99-
| `space.actions` | No | A map of content-builder factories that become sugar methods on the resolved space. Each `space.<name>(...args)` delegates to `space.send(factory(...args))`. Names that collide with built-in `Space` methods (`send`, `edit`, `startTyping`, `stopTyping`, `responding`, `getMessage`, `rename`, `avatar`) are skipped at runtime with a warning. |
98+
| `space.params` | No | Zod schema for additional space parameters — surfaces as the second arg to `platform(app).space.create()` and `platform(app).space.get()`. |
99+
| `space.actions` | No | A map of content-builder factories that become sugar methods on the resolved space. Each `space.<name>(...args)` delegates to `space.send(factory(...args))`. Names that collide with built-in `Space` methods (`send`, `edit`, `unsend`, `startTyping`, `stopTyping`, `responding`, `getMessage`, `rename`, `avatar`) are skipped at runtime with a warning. |
100100
| `lifecycle.createClient` | Yes | Creates the platform client. Receives `config`, `projectId`, `projectSecret` (both may be `undefined`), and `store`. |
101101
| `lifecycle.destroyClient` | No | Tears down the client on shutdown. Omit if no cleanup is needed. |
102102
| `messages` | Yes | Async generator that yields incoming messages. |
103-
| `send` | Yes | Dispatches a content item to a space. All content types — text, attachments, reactions, replies, edits, typing indicators — flow through this single action. Return a `ProviderMessageRecord` for content that produces a message, or `undefined` for fire-and-forget signals (reactions, typing, edits). |
103+
| `send` | Yes | Dispatches a content item to a space. All content types — text, markdown, attachments, reactions, replies, edits, unsends, typing indicators — flow through this single action. Return a `ProviderMessageRecord` for content that produces a message (including reactions — the record is the unsend handle), or `undefined` for fire-and-forget signals (typing, edits, unsends). |
104104
| `actions.getMessage` | No | Fetches a message by ID from a space. Receives `(ctx, space, messageId)` where `ctx` is `{ client, config, store }`. Powers `space.getMessage(id)`. When omitted, `space.getMessage()` throws `UnsupportedError`. |
105105
| `actions.[custom]` | No | Platform-specific methods projected onto the platform instance. Each receives `(ctx, ...args)` where `ctx` is `{ client, config, store }`; the public signature drops `ctx`. Names that collide with reserved instance keys (`user`, `space`, `messages`, plus any event names) are skipped at runtime with a warning. |
106106
| `events.[custom]` | No | Additional async generators for platform-specific events — exposed on `app.[eventName]`. |
@@ -216,7 +216,7 @@ export const myWebhookPlatform = definePlatform("my-webhook-platform", {
216216
},
217217

218218
space: {
219-
resolve: async ({ input }) => ({ id: input.users[0].id }),
219+
create: async ({ input }) => ({ id: input.users[0].id }),
220220
},
221221
});
222222
```
@@ -236,7 +236,7 @@ const app = await Spectrum({
236236

237237
const mine = myPlatform(app);
238238
const user = await mine.user("user-123");
239-
const space = await mine.space(user);
239+
const space = await mine.space.create(user);
240240

241241
await space.send("Hello from my custom platform.");
242242
```

docs-src/spectrum-ts/messages.mdx.vel

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,12 +58,20 @@ Every message conforms to <TypeTooltip name="Message" type={`{{ message.signatur
5858
</tr>
5959
<tr>
6060
<td><code>react(reaction)</code></td>
61-
<td>React to this message. No-op on platforms that don't support reactions.</td>
61+
<td>React to this message. Returns the reaction <code>Message</code> — keep it as the handle to <code>unsend()</code> later. No-op on platforms that don't support reactions.</td>
6262
</tr>
6363
<tr>
6464
<td><code>reply(...content)</code></td>
6565
<td>Reply threaded to this message. Falls back silently on platforms without thread support.</td>
6666
</tr>
67+
<tr>
68+
<td><code>edit(newContent)</code></td>
69+
<td>Rewrite the content of this outbound message. Fire-and-forget.</td>
70+
</tr>
71+
<tr>
72+
<td><code>unsend()</code></td>
73+
<td>Retract this outbound message. Fire-and-forget.</td>
74+
</tr>
6775
</tbody>
6876
</table>
6977

@@ -115,6 +123,7 @@ for await (const [space, message] of app.messages) {
115123
| Type | Fields |
116124
|---|---|
117125
| `"text"` | `text: string` |
126+
| `"markdown"` | `markdown: string` — outbound-only styled text |
118127
| `"attachment"` | `id: string`, `name: string`, `mimeType: string`, `size?: number`, `read()`, `stream()` |
119128
| `"voice"` | `name?: string`, `mimeType: string`, `duration?: number`, `size?: number`, `read()`, `stream()` |
120129
| `"contact"` | `name?`, `phones?`, `emails?`, `addresses?`, `org?`, `urls?`, `birthday?`, `note?`, `photo?`, `user?` |
@@ -125,6 +134,7 @@ for await (const [space, message] of app.messages) {
125134
| `"group"` | `items: Message[]` — bundled multi-message unit |
126135
| `"reply"` | `content: Content`, `target: Message` — threaded reply wrapping inner content |
127136
| `"edit"` | `content: Content`, `target: Message` — rewrite of a previously-sent message |
137+
| `"unsend"` | `target: Message` — retraction of a previously-sent message |
128138
| `"typing"` | `state: "start" \| "stop"` — typing indicator signal |
129139
| `"streamText"` | `stream: () => AsyncIterable<string>` — streaming text content |
130140
| `"custom"` | `raw: unknown` — platform-specific structured data |

docs-src/spectrum-ts/platform-narrowing.mdx.vel

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,15 @@ Every platform provider exports a callable — `imessage`, `terminal`, `whatsapp
1111

1212
## Narrowing the app
1313

14-
Pass a `Spectrum` instance to get a <TypeTooltip name="PlatformInstance" type={`{{ pi.signature }}`} /> for that platform. The instance gives you `user()` and `space()` resolvers, plus access to any custom events the provider emits.
14+
Pass a `Spectrum` instance to get a <TypeTooltip name="PlatformInstance" type={`{{ pi.signature }}`} /> for that platform. The instance gives you `user()` and `space.create()` / `space.get()` resolvers, plus access to any custom events the provider emits.
1515

1616
```ts
1717
import { imessage } from "spectrum-ts/providers/imessage";
1818

1919
const im = imessage(app);
2020

2121
const user = await im.user("+15551234567");
22-
const space = await im.space(user);
22+
const space = await im.space.create(user);
2323

2424
await space.send("Hello from a new conversation.");
2525
```
@@ -57,14 +57,14 @@ for await (const [space, message] of app.messages) {
5757

5858
## Creating group conversations
5959

60-
The `space()` method accepts multiple users. On iMessage:
60+
`space.create(...)` accepts a single user or an array of users. On iMessage:
6161

6262
```ts
6363
const im = imessage(app);
6464
const alice = await im.user("+15551111111");
6565
const bob = await im.user("+15552222222");
6666

67-
const group = await im.space(alice, bob);
67+
const group = await im.space.create([alice, bob]);
6868
await group.send("Welcome to the group.");
6969
```
7070

docs-src/spectrum-ts/providers/imessage.mdx.vel

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -101,33 +101,39 @@ for await (const [space, message] of app.messages) {
101101

102102
## Creating conversations
103103

104-
Resolve users by phone number or email, then pass them to `space()`:
104+
Resolve users by phone number or email, then create a space with `space.create(...)`:
105105

106106
```ts
107107
const im = imessage(app);
108108
const alice = await im.user("+15551111111");
109109
const bob = await im.user("+15552222222");
110110

111111
// DM
112-
const dm = await im.space(alice);
112+
const dm = await im.space.create(alice);
113113
await dm.send("Hi Alice");
114114

115115
// Group
116-
const group = await im.space(alice, bob);
116+
const group = await im.space.create([alice, bob]);
117117
await group.send("Welcome to the group.");
118118
```
119119

120-
Space creation requires cloud or dedicated mode. In local mode `space()` throws — the local Messages database doesn't expose chat creation.
120+
To look up an existing conversation by its chat GUID, use `space.get(id)`:
121+
122+
```ts
123+
const existing = await im.space.get("any;-;+15551111111");
124+
```
125+
126+
Space creation requires cloud or dedicated mode. In local mode `space.create()` throws — the local Messages database doesn't expose chat creation. Shared mode cannot create group chats — use a dedicated number, or `space.get(chatGuid)` for an existing group.
121127

122128
### Per-phone routing
123129

124130
If your account has multiple dedicated phone numbers, you can pin a conversation to a specific line by passing `phone` as a space parameter:
125131

126132
```ts
127-
const dm = await im.space(alice, { phone: "+15559999999" });
133+
const dm = await im.space.create(alice, { phone: "+15559999999" });
128134
```
129135

130-
When omitted, Spectrum picks a phone at random from the available dedicated lines. All subsequent actions on that space — sending, typing, replies, edits, reactions, and lookups — route through the chosen number.
136+
When omitted, Spectrum picks a phone at random from the available dedicated lines. All subsequent actions on that space — sending, typing, replies, edits, reactions, unsends, and lookups — route through the chosen number.
131137

132138
<Note>
133139
Per-phone routing applies to dedicated lines (Business plan) only. On shared-pool plans the `phone` parameter is ignored — all conversations route through the shared pool automatically.
@@ -144,7 +150,7 @@ await space.send(effect("Happy birthday!", imessage.effect.message.celebration))
144150
await space.send(effect(attachment("/path/to/photo.jpg"), imessage.effect.message.confetti));
145151
```
146152

147-
The wrapped content can be a string or any `attachment(...)`. Effects only apply on iMessage — other platforms see the inner content unchanged.
153+
The wrapped content can be a string, `markdown(...)`, or any `attachment(...)`. Effects only apply on iMessage — other platforms see the inner content unchanged.
148154

149155
<AccordionGroup>
150156
<Accordion title="Bubble effects" description="Animate the sent message bubble.">

docs-src/spectrum-ts/providers/telegram.mdx.vel

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ Resolve a user by their Telegram user ID and open a space. You can also pass a `
5252
```ts
5353
const tg = telegram(app);
5454
const user = await tg.user("123456789");
55-
const space = await tg.space(user);
55+
const space = await tg.space.create(user);
5656

5757
await space.send("Hello from Spectrum.");
5858
```
@@ -66,9 +66,11 @@ In cloud mode (when `projectId` and `projectSecret` are provided), the Telegram
6666
| Feature | Support |
6767
|---|---|
6868
| Text messages | Send and receive |
69+
| Markdown | Send (rendered as Telegram HTML via `parse_mode`) |
70+
| Streaming text / markdown | Send (native draft preview in private chats) |
6971
| Media (photos, documents, audio, video) | Send and receive |
7072
| Reactions | Send and receive |
7173
| Threaded replies | Send and receive |
7274
| Typing indicators | Send |
73-
| Message edits | Send and receive |
75+
| Message edits | Send and receive (text and markdown) |
7476
| Custom Bot API calls | Via platform-specific actions |

docs-src/spectrum-ts/providers/terminal.mdx.vel

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,11 +75,11 @@ By default the TUI starts on `chat-1`; new chats opened with `Ctrl+N` get `chat-
7575
import { terminal } from "spectrum-ts/providers/terminal";
7676

7777
const t = terminal(app);
78-
const debug = await t.space({ id: "debug" });
78+
const debug = await t.space.get("debug");
7979
await debug.send("agent online");
8080
```
8181

82-
Calling `space()` ensures the chat exists in the sidebar — useful for kicking off a conversation before any user input.
82+
Calling `space.get()` ensures the chat exists in the sidebar — useful for kicking off a conversation before any user input.
8383

8484
## Reactions and replies
8585

docs-src/spectrum-ts/providers/whatsapp-business.mdx.vel

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { whatsappBusiness } from "spectrum-ts/providers/whatsapp-business";
1010
The WhatsApp Business provider wraps the official WhatsApp Business Cloud API. Reactions and threaded replies map to native WhatsApp features.
1111

1212
<Note>
13-
WhatsApp Business supports **1:1 conversations only**. The API does not expose group management for business accounts — calling `space(userA, userB)` throws.
13+
WhatsApp Business supports **1:1 conversations only**. The API does not expose group management for business accounts — calling `space.create([userA, userB])` throws.
1414
</Note>
1515

1616
## Config
@@ -61,9 +61,9 @@ Resolve a user by their WhatsApp phone number (international format, digits only
6161
```ts
6262
const wa = whatsappBusiness(app);
6363
const customer = await wa.user("15551234567");
64-
const space = await wa.space(customer);
64+
const space = await wa.space.create(customer);
6565

6666
await space.send("Thanks for reaching out.");
6767
```
6868

69-
Passing more than one user to `space()` throws — the provider rejects group creation explicitly.
69+
Passing more than one user to `space.create()` throws — the provider rejects group creation explicitly.

0 commit comments

Comments
 (0)