You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
import { TypeTooltip } from "/snippets/type-tooltip.mdx";
7
7
8
8
{% set ci = symbol("ts:spectrum-ts#ContentInput") %}
9
9
{% set cb = symbol("ts:spectrum-ts#ContentBuilder") %}
10
10
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 }}`} />.
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.
33
27
34
28
<Tabs>
35
29
<Tab title="Vercel AI SDK">
36
30
```ts
37
-
import { streamText } from "spectrum-ts";
31
+
import { text } from "spectrum-ts";
38
32
import { streamText as aiStreamText } from "ai";
39
33
40
34
const result = aiStreamText({ model, prompt: message.content.text });
41
-
await space.send(streamText(result));
35
+
await space.send(text(result));
42
36
```
43
37
</Tab>
44
38
<Tab title="AsyncIterable">
45
39
```ts
46
-
import { streamText } from "spectrum-ts";
40
+
import { text } from "spectrum-ts";
47
41
48
42
async function* generate() {
49
43
yield "Hello, ";
50
44
yield "world!";
51
45
}
52
46
53
-
await space.send(streamText(generate()));
47
+
await space.send(text(generate()));
54
48
```
55
49
</Tab>
56
50
<Tab title="Custom extractor">
57
51
```ts
58
-
import { streamText } from "spectrum-ts";
52
+
import { text } from "spectrum-ts";
59
53
60
54
await space.send(
61
-
streamText(customStream, {
55
+
text(customStream, {
62
56
extract: (chunk) => chunk.delta?.text ?? null,
63
57
}),
64
58
);
65
59
```
66
60
</Tab>
67
61
</Tabs>
68
62
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.
70
86
71
87
## Attachments
72
88
@@ -275,7 +291,7 @@ import { reply, text } from "spectrum-ts";
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.
| `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. |
94
93
| `user.resolve` | Yes | Resolves a user from a string ID. Returns at minimum `{ id: string }`. |
95
94
| `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. |
97
97
| `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. |
100
100
| `lifecycle.createClient` | Yes | Creates the platform client. Receives `config`, `projectId`, `projectSecret` (both may be `undefined`), and `store`. |
101
101
| `lifecycle.destroyClient` | No | Tears down the client on shutdown. Omit if no cleanup is needed. |
| `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). |
104
104
| `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`. |
105
105
| `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. |
106
106
| `events.[custom]` | No | Additional async generators for platform-specific events — exposed on `app.[eventName]`. |
Copy file name to clipboardExpand all lines: docs-src/spectrum-ts/messages.mdx.vel
+11-1Lines changed: 11 additions & 1 deletion
Original file line number
Diff line number
Diff line change
@@ -58,12 +58,20 @@ Every message conforms to <TypeTooltip name="Message" type={`{{ message.signatur
58
58
</tr>
59
59
<tr>
60
60
<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>
62
62
</tr>
63
63
<tr>
64
64
<td><code>reply(...content)</code></td>
65
65
<td>Reply threaded to this message. Falls back silently on platforms without thread support.</td>
66
66
</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>
67
75
</tbody>
68
76
</table>
69
77
@@ -115,6 +123,7 @@ for await (const [space, message] of app.messages) {
115
123
| Type | Fields |
116
124
|---|---|
117
125
| `"text"` | `text: string` |
126
+
| `"markdown"` | `markdown: string` — outbound-only styled text |
Copy file name to clipboardExpand all lines: docs-src/spectrum-ts/platform-narrowing.mdx.vel
+4-4Lines changed: 4 additions & 4 deletions
Original file line number
Diff line number
Diff line change
@@ -11,15 +11,15 @@ Every platform provider exports a callable — `imessage`, `terminal`, `whatsapp
11
11
12
12
## Narrowing the app
13
13
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.
15
15
16
16
```ts
17
17
import { imessage } from "spectrum-ts/providers/imessage";
18
18
19
19
const im = imessage(app);
20
20
21
21
const user = await im.user("+15551234567");
22
-
const space = await im.space(user);
22
+
const space = await im.space.create(user);
23
23
24
24
await space.send("Hello from a new conversation.");
25
25
```
@@ -57,14 +57,14 @@ for await (const [space, message] of app.messages) {
57
57
58
58
## Creating group conversations
59
59
60
-
The `space()` method accepts multiple users. On iMessage:
60
+
`space.create(...)` accepts a single user or an array of users. On iMessage:
61
61
62
62
```ts
63
63
const im = imessage(app);
64
64
const alice = await im.user("+15551111111");
65
65
const bob = await im.user("+15552222222");
66
66
67
-
const group = await im.space(alice, bob);
67
+
const group = await im.space.create([alice, bob]);
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.
121
127
122
128
### Per-phone routing
123
129
124
130
If your account has multiple dedicated phone numbers, you can pin a conversation to a specific line by passing `phone` as a space parameter:
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.
131
137
132
138
<Note>
133
139
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.
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.
148
154
149
155
<AccordionGroup>
150
156
<Accordion title="Bubble effects" description="Animate the sent message bubble.">
Copy file name to clipboardExpand all lines: docs-src/spectrum-ts/providers/whatsapp-business.mdx.vel
+3-3Lines changed: 3 additions & 3 deletions
Original file line number
Diff line number
Diff line change
@@ -10,7 +10,7 @@ import { whatsappBusiness } from "spectrum-ts/providers/whatsapp-business";
10
10
The WhatsApp Business provider wraps the official WhatsApp Business Cloud API. Reactions and threaded replies map to native WhatsApp features.
11
11
12
12
<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.
14
14
</Note>
15
15
16
16
## Config
@@ -61,9 +61,9 @@ Resolve a user by their WhatsApp phone number (international format, digits only
61
61
```ts
62
62
const wa = whatsappBusiness(app);
63
63
const customer = await wa.user("15551234567");
64
-
const space = await wa.space(customer);
64
+
const space = await wa.space.create(customer);
65
65
66
66
await space.send("Thanks for reaching out.");
67
67
```
68
68
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