@assistant-ui/mcp-docs-server 0.1.34 → 0.1.36
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.docs/organized/code-examples/waterfall.md +4 -4
- package/.docs/organized/code-examples/with-a2a.md +5 -5
- package/.docs/organized/code-examples/with-ag-ui.md +6 -6
- package/.docs/organized/code-examples/with-ai-sdk-v6.md +7 -7
- package/.docs/organized/code-examples/with-artifacts.md +7 -7
- package/.docs/organized/code-examples/with-assistant-transport.md +73 -57
- package/.docs/organized/code-examples/with-browser-extension.md +7 -7
- package/.docs/organized/code-examples/with-chain-of-thought.md +44 -93
- package/.docs/organized/code-examples/with-cloud-standalone.md +7 -7
- package/.docs/organized/code-examples/with-cloud.md +7 -7
- package/.docs/organized/code-examples/with-custom-thread-list.md +7 -7
- package/.docs/organized/code-examples/with-elevenlabs-conversational.md +9 -9
- package/.docs/organized/code-examples/with-elevenlabs-scribe.md +9 -9
- package/.docs/organized/code-examples/with-eve.md +343 -0
- package/.docs/organized/code-examples/with-expo.md +943 -940
- package/.docs/organized/code-examples/with-external-store.md +5 -5
- package/.docs/organized/code-examples/with-ffmpeg.md +8 -11
- package/.docs/organized/code-examples/with-generative-ui.md +33 -309
- package/.docs/organized/code-examples/with-google-adk.md +5 -5
- package/.docs/organized/code-examples/with-heat-graph.md +4 -4
- package/.docs/organized/code-examples/with-image-generation.md +7 -7
- package/.docs/organized/code-examples/with-interactables.md +169 -341
- package/.docs/organized/code-examples/with-langchain.md +7 -7
- package/.docs/organized/code-examples/with-langgraph.md +23 -160
- package/.docs/organized/code-examples/with-livekit.md +10 -10
- package/.docs/organized/code-examples/with-mcp.md +7 -7
- package/.docs/organized/code-examples/with-opencode.md +106 -580
- package/.docs/organized/code-examples/with-pi.md +2046 -0
- package/.docs/organized/code-examples/with-react-hook-form.md +16 -9
- package/.docs/organized/code-examples/with-react-ink-web.md +691 -0
- package/.docs/organized/code-examples/with-react-ink.md +29 -17
- package/.docs/organized/code-examples/with-react-router.md +12 -12
- package/.docs/organized/code-examples/with-resumable-stream.md +8 -8
- package/.docs/organized/code-examples/with-store.md +14 -10
- package/.docs/organized/code-examples/with-tanstack.md +21 -7
- package/.docs/organized/code-examples/with-tap-runtime.md +5 -5
- package/.docs/organized/code-examples/with-virtualized-thread.md +676 -0
- package/.docs/raw/docs/(docs)/architecture.mdx +13 -12
- package/.docs/raw/docs/(docs)/cli.mdx +4 -2
- package/.docs/raw/docs/(docs)/devtools.mdx +25 -2
- package/.docs/raw/docs/(docs)/index.mdx +5 -2
- package/.docs/raw/docs/(docs)/installation.mdx +5 -2
- package/.docs/raw/docs/(reference)/api-reference/external-store/message-conversion.mdx +16 -16
- package/.docs/raw/docs/(reference)/api-reference/hooks/composer-triggers.mdx +73 -42
- package/.docs/raw/docs/(reference)/api-reference/hooks/model-context.mdx +3 -20
- package/.docs/raw/docs/(reference)/api-reference/hooks/primitives.mdx +234 -123
- package/.docs/raw/docs/(reference)/api-reference/integrations/eve.mdx +51 -0
- package/.docs/raw/docs/(reference)/api-reference/integrations/index.mdx +4 -1
- package/.docs/raw/docs/(reference)/api-reference/integrations/react-ai-sdk.mdx +70 -38
- package/.docs/raw/docs/(reference)/api-reference/model-context/context.mdx +1 -1
- package/.docs/raw/docs/(reference)/api-reference/primitives/action-bar.mdx +30 -0
- package/.docs/raw/docs/(reference)/api-reference/primitives/attachment.mdx +7 -0
- package/.docs/raw/docs/(reference)/api-reference/primitives/branch-picker.mdx +26 -0
- package/.docs/raw/docs/(reference)/api-reference/primitives/chain-of-thought.mdx +15 -0
- package/.docs/raw/docs/(reference)/api-reference/primitives/composer.mdx +103 -0
- package/.docs/raw/docs/(reference)/api-reference/primitives/message-part.mdx +16 -0
- package/.docs/raw/docs/(reference)/api-reference/primitives/message.mdx +56 -0
- package/.docs/raw/docs/(reference)/api-reference/primitives/queue-item.mdx +12 -0
- package/.docs/raw/docs/(reference)/api-reference/primitives/selection-toolbar.mdx +12 -0
- package/.docs/raw/docs/(reference)/api-reference/primitives/suggestion.mdx +14 -0
- package/.docs/raw/docs/(reference)/api-reference/primitives/thread.mdx +57 -1
- package/.docs/raw/docs/(reference)/api-reference/tools/index.mdx +6 -0
- package/.docs/raw/docs/(reference)/api-reference/tools/interactables-legacy.mdx +55 -0
- package/.docs/raw/docs/(reference)/api-reference/tools/interactables.mdx +151 -0
- package/.docs/raw/docs/(reference)/api-reference/tools/rendering.mdx +17 -38
- package/.docs/raw/docs/(reference)/api-reference/tools/status.mdx +22 -0
- package/.docs/raw/docs/(reference)/api-reference/tools/toolkits.mdx +20 -27
- package/.docs/raw/docs/(reference)/api-reference/utilities/miscellaneous.mdx +40 -25
- package/.docs/raw/docs/guides/chain-of-thought.mdx +1 -1
- package/.docs/raw/docs/guides/headless-composer-input.mdx +113 -0
- package/.docs/raw/docs/guides/index.mdx +3 -0
- package/.docs/raw/docs/guides/input-history.mdx +55 -0
- package/.docs/raw/docs/guides/latex.mdx +28 -22
- package/.docs/raw/docs/guides/mentions.mdx +32 -7
- package/.docs/raw/docs/guides/slash-commands.mdx +3 -6
- package/.docs/raw/docs/guides/speech.mdx +5 -7
- package/.docs/raw/docs/guides/virtualization.mdx +133 -0
- package/.docs/raw/docs/guides/voice.mdx +3 -2
- package/.docs/raw/docs/ink/hooks.mdx +2 -2
- package/.docs/raw/docs/integrations/attachments/custom-adapter.mdx +4 -12
- package/.docs/raw/docs/integrations/auth/better-auth.mdx +7 -10
- package/.docs/raw/docs/integrations/auth/clerk.mdx +3 -9
- package/.docs/raw/docs/integrations/auth/next-auth.mdx +3 -8
- package/.docs/raw/docs/integrations/frameworks/ai-sdk.mdx +8 -5
- package/.docs/raw/docs/integrations/index.mdx +5 -12
- package/.docs/raw/docs/integrations/observability/helicone.mdx +3 -4
- package/.docs/raw/docs/integrations/observability/langfuse.mdx +3 -5
- package/.docs/raw/docs/integrations/observability/langsmith.mdx +3 -2
- package/.docs/raw/docs/integrations/persistence/custom-adapter.mdx +4 -3
- package/.docs/raw/docs/primitives/composer.mdx +8 -0
- package/.docs/raw/docs/primitives/thread.mdx +24 -0
- package/.docs/raw/docs/react-native/hooks.mdx +1 -1
- package/.docs/raw/docs/runtimes/ag-ui/runtime-options.mdx +22 -3
- package/.docs/raw/docs/runtimes/concepts/stability.mdx +1 -1
- package/.docs/raw/docs/runtimes/custom/data-stream.mdx +1 -1
- package/.docs/raw/docs/runtimes/custom/external-store.mdx +54 -0
- package/.docs/raw/docs/runtimes/custom/local-runtime.mdx +108 -4
- package/.docs/raw/docs/runtimes/eve/overview.mdx +100 -0
- package/.docs/raw/docs/runtimes/eve/quickstart.mdx +143 -0
- package/.docs/raw/docs/runtimes/langchain.mdx +386 -18
- package/.docs/raw/docs/runtimes/langgraph/quickstart.mdx +1 -1
- package/.docs/raw/docs/runtimes/langgraph/tutorial/part-1.mdx +1 -1
- package/.docs/raw/docs/runtimes/pick-a-runtime.mdx +12 -6
- package/.docs/raw/docs/tools/defining-tools.mdx +4 -0
- package/.docs/raw/docs/tools/interactables-legacy.mdx +410 -0
- package/.docs/raw/docs/tools/interactables.mdx +892 -223
- package/.docs/raw/docs/tools/mcp.mdx +4 -4
- package/.docs/raw/docs/tools/multi-agent.mdx +5 -0
- package/.docs/raw/docs/tools/tool-ui.mdx +37 -1
- package/.docs/raw/docs/ui/composer-trigger-popover.mdx +2 -0
- package/.docs/raw/docs/ui/dot-matrix.mdx +133 -0
- package/.docs/raw/docs/ui/file.mdx +1 -1
- package/.docs/raw/docs/ui/model-selector.mdx +219 -52
- package/.docs/raw/docs/ui/number-roll.mdx +154 -0
- package/.docs/raw/docs/ui/part-grouping.mdx +38 -0
- package/.docs/raw/docs/ui/reasoning.mdx +3 -3
- package/.docs/raw/docs/ui/streamdown.mdx +2 -0
- package/.docs/raw/docs/ui/syntax-highlighting.mdx +1 -1
- package/.docs/raw/docs/ui/thread.mdx +52 -0
- package/.docs/raw/docs/ui/tool-fallback.mdx +2 -2
- package/.docs/raw/docs/utilities/react-o11y.mdx +92 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +18 -2
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/src/index.ts +14 -6
- package/src/tools/tests/mcp-protocol.test.ts +9 -0
|
@@ -0,0 +1,691 @@
|
|
|
1
|
+
# Example: with-react-ink-web
|
|
2
|
+
|
|
3
|
+
## app/globals.css
|
|
4
|
+
|
|
5
|
+
```css
|
|
6
|
+
* {
|
|
7
|
+
box-sizing: border-box;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
html,
|
|
11
|
+
body {
|
|
12
|
+
height: 100%;
|
|
13
|
+
margin: 0;
|
|
14
|
+
background: #1a1a1a;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.terminal-center {
|
|
18
|
+
display: flex;
|
|
19
|
+
height: 100vh;
|
|
20
|
+
align-items: center;
|
|
21
|
+
justify-content: center;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.terminal-host {
|
|
25
|
+
width: 100%;
|
|
26
|
+
max-width: 960px;
|
|
27
|
+
height: min(100%, 560px);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.terminal-host .wterm {
|
|
31
|
+
height: 100%;
|
|
32
|
+
border-radius: 0;
|
|
33
|
+
box-shadow: none;
|
|
34
|
+
background: #1a1a1a;
|
|
35
|
+
--term-bg: #1a1a1a;
|
|
36
|
+
--term-fg: #e0e0e0;
|
|
37
|
+
--term-cursor: #34d399;
|
|
38
|
+
--term-font-family: Menlo, Monaco, "Courier New", monospace;
|
|
39
|
+
--term-font-size: 13px;
|
|
40
|
+
--term-line-height: 1.3;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.terminal-loading {
|
|
44
|
+
display: flex;
|
|
45
|
+
height: 100vh;
|
|
46
|
+
align-items: center;
|
|
47
|
+
justify-content: center;
|
|
48
|
+
font-family: ui-monospace, Menlo, Monaco, monospace;
|
|
49
|
+
font-size: 14px;
|
|
50
|
+
color: #6b7280;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## app/ink-app.tsx
|
|
56
|
+
|
|
57
|
+
```tsx
|
|
58
|
+
"use client";
|
|
59
|
+
|
|
60
|
+
// Mirrors examples/with-react-ink/src/app.tsx; keep in sync.
|
|
61
|
+
import { useMemo } from "react";
|
|
62
|
+
import { Box, Text } from "ink";
|
|
63
|
+
import {
|
|
64
|
+
AssistantRuntimeProvider,
|
|
65
|
+
StatusBarPrimitive,
|
|
66
|
+
useAui,
|
|
67
|
+
} from "@assistant-ui/react-ink";
|
|
68
|
+
import {
|
|
69
|
+
useChatRuntime,
|
|
70
|
+
AssistantChatTransport,
|
|
71
|
+
} from "@assistant-ui/react-ai-sdk";
|
|
72
|
+
import { lastAssistantMessageIsCompleteWithToolCalls } from "ai";
|
|
73
|
+
import { Thread } from "./thread";
|
|
74
|
+
|
|
75
|
+
const CHAT_API =
|
|
76
|
+
process.env.NEXT_PUBLIC_CHAT_ENDPOINT_URL ??
|
|
77
|
+
(process.env.NODE_ENV === "development"
|
|
78
|
+
? "http://localhost:3000/api/chat"
|
|
79
|
+
: "https://www.assistant-ui.com/api/chat");
|
|
80
|
+
|
|
81
|
+
const MODEL_NAME = "assistant-ui";
|
|
82
|
+
|
|
83
|
+
export const InkApp = () => {
|
|
84
|
+
const transport = useMemo(
|
|
85
|
+
() => new AssistantChatTransport({ api: CHAT_API }),
|
|
86
|
+
[],
|
|
87
|
+
);
|
|
88
|
+
const runtime = useChatRuntime({
|
|
89
|
+
transport,
|
|
90
|
+
sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithToolCalls,
|
|
91
|
+
});
|
|
92
|
+
const aui = useAui();
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<AssistantRuntimeProvider aui={aui} runtime={runtime}>
|
|
96
|
+
<Box flexDirection="column" padding={1}>
|
|
97
|
+
<Box>
|
|
98
|
+
<Text bold color="cyan">
|
|
99
|
+
{MODEL_NAME}
|
|
100
|
+
</Text>
|
|
101
|
+
<Text dimColor>{" ~/assistant-ui"}</Text>
|
|
102
|
+
</Box>
|
|
103
|
+
<StatusBarPrimitive.Root>
|
|
104
|
+
<Text dimColor>
|
|
105
|
+
model: <StatusBarPrimitive.ModelName name={MODEL_NAME} /> ·{" "}
|
|
106
|
+
<StatusBarPrimitive.MessageCount /> · <StatusBarPrimitive.Status />
|
|
107
|
+
</Text>
|
|
108
|
+
</StatusBarPrimitive.Root>
|
|
109
|
+
<Box marginTop={1}>
|
|
110
|
+
<Thread />
|
|
111
|
+
</Box>
|
|
112
|
+
</Box>
|
|
113
|
+
</AssistantRuntimeProvider>
|
|
114
|
+
);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## app/ink-streams.ts
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
type Listener = (...args: unknown[]) => void;
|
|
123
|
+
|
|
124
|
+
class MiniEmitter {
|
|
125
|
+
private listeners = new Map<string, Set<Listener>>();
|
|
126
|
+
|
|
127
|
+
on(event: string, fn: Listener) {
|
|
128
|
+
let set = this.listeners.get(event);
|
|
129
|
+
if (!set) {
|
|
130
|
+
set = new Set();
|
|
131
|
+
this.listeners.set(event, set);
|
|
132
|
+
}
|
|
133
|
+
set.add(fn);
|
|
134
|
+
return this;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
off(event: string, fn: Listener) {
|
|
138
|
+
this.listeners.get(event)?.delete(fn);
|
|
139
|
+
return this;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
addListener(event: string, fn: Listener) {
|
|
143
|
+
return this.on(event, fn);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
removeListener(event: string, fn: Listener) {
|
|
147
|
+
return this.off(event, fn);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
emit(event: string, ...args: unknown[]) {
|
|
151
|
+
const set = this.listeners.get(event);
|
|
152
|
+
if (!set) return false;
|
|
153
|
+
for (const fn of [...set]) fn(...args);
|
|
154
|
+
return set.size > 0;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export type InkStreams = {
|
|
159
|
+
stdout: NodeJS.WriteStream;
|
|
160
|
+
stdin: NodeJS.ReadStream;
|
|
161
|
+
pushInput: (data: string) => void;
|
|
162
|
+
setSize: (cols: number, rows: number) => void;
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
// Fake streams implementing the subset of the Node stream API that ink 6
|
|
166
|
+
// touches (see ink's Ink/App classes). The writer receives ink's ANSI output
|
|
167
|
+
// with bare \n rewritten to \r\n (wterm does not emulate ONLCR). Ink's
|
|
168
|
+
// clear-terminal sequence passes through: its 3J resets wterm's scrollback on
|
|
169
|
+
// every overflow repaint, leaving only the final frame's overflow scrollable.
|
|
170
|
+
export function createInkStreams(
|
|
171
|
+
writeToTerminal: (data: string) => void,
|
|
172
|
+
): InkStreams {
|
|
173
|
+
const stdoutBase = new MiniEmitter() as MiniEmitter & Record<string, unknown>;
|
|
174
|
+
Object.assign(stdoutBase, {
|
|
175
|
+
columns: 80,
|
|
176
|
+
rows: 24,
|
|
177
|
+
isTTY: true,
|
|
178
|
+
writable: true,
|
|
179
|
+
setDefaultEncoding: () => stdoutBase,
|
|
180
|
+
cork: () => {},
|
|
181
|
+
uncork: () => {},
|
|
182
|
+
write: (chunk: unknown, encoding?: unknown, cb?: () => void) => {
|
|
183
|
+
const str = typeof chunk === "string" ? chunk : String(chunk);
|
|
184
|
+
if (str.length > 0) {
|
|
185
|
+
writeToTerminal(str.replace(/\r?\n/g, "\r\n"));
|
|
186
|
+
}
|
|
187
|
+
if (typeof encoding === "function") (encoding as () => void)();
|
|
188
|
+
else cb?.();
|
|
189
|
+
return true;
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
const inputBuffer: string[] = [];
|
|
194
|
+
const stdinBase = new MiniEmitter() as MiniEmitter & Record<string, unknown>;
|
|
195
|
+
Object.assign(stdinBase, {
|
|
196
|
+
columns: 80,
|
|
197
|
+
rows: 24,
|
|
198
|
+
isTTY: true,
|
|
199
|
+
readable: true,
|
|
200
|
+
setEncoding: () => stdinBase,
|
|
201
|
+
setRawMode: () => stdinBase,
|
|
202
|
+
resume: () => stdinBase,
|
|
203
|
+
pause: () => stdinBase,
|
|
204
|
+
ref: () => stdinBase,
|
|
205
|
+
unref: () => stdinBase,
|
|
206
|
+
read: () => (inputBuffer.length > 0 ? inputBuffer.shift()! : null),
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
stdout: stdoutBase as unknown as NodeJS.WriteStream,
|
|
211
|
+
stdin: stdinBase as unknown as NodeJS.ReadStream,
|
|
212
|
+
pushInput: (data: string) => {
|
|
213
|
+
inputBuffer.push(data);
|
|
214
|
+
stdinBase.emit("readable");
|
|
215
|
+
},
|
|
216
|
+
setSize: (cols: number, rows: number) => {
|
|
217
|
+
const changed =
|
|
218
|
+
stdoutBase["columns"] !== cols || stdoutBase["rows"] !== rows;
|
|
219
|
+
stdoutBase["columns"] = cols;
|
|
220
|
+
stdoutBase["rows"] = rows;
|
|
221
|
+
stdinBase["columns"] = cols;
|
|
222
|
+
stdinBase["rows"] = rows;
|
|
223
|
+
if (changed) stdoutBase.emit("resize");
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
## app/layout.tsx
|
|
231
|
+
|
|
232
|
+
```tsx
|
|
233
|
+
import type { Metadata } from "next";
|
|
234
|
+
import type { ReactNode } from "react";
|
|
235
|
+
import "./globals.css";
|
|
236
|
+
|
|
237
|
+
export const metadata: Metadata = {
|
|
238
|
+
title: "assistant-ui ink demo",
|
|
239
|
+
description: "The with-react-ink example running in the browser via ink-web.",
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
export default function RootLayout({ children }: { children: ReactNode }) {
|
|
243
|
+
return (
|
|
244
|
+
<html lang="en">
|
|
245
|
+
<body>{children}</body>
|
|
246
|
+
</html>
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
## app/page.tsx
|
|
253
|
+
|
|
254
|
+
```tsx
|
|
255
|
+
"use client";
|
|
256
|
+
|
|
257
|
+
import dynamic from "next/dynamic";
|
|
258
|
+
|
|
259
|
+
const Terminal = dynamic(() => import("./terminal"), {
|
|
260
|
+
ssr: false,
|
|
261
|
+
loading: () => <div className="terminal-loading">starting terminal...</div>,
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
export default function Home() {
|
|
265
|
+
return <Terminal />;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
## app/terminal.tsx
|
|
271
|
+
|
|
272
|
+
```tsx
|
|
273
|
+
"use client";
|
|
274
|
+
|
|
275
|
+
import { useCallback, useEffect, useRef } from "react";
|
|
276
|
+
import { Terminal, useTerminal, type WTerm } from "@wterm/react";
|
|
277
|
+
import { render, type Instance } from "ink";
|
|
278
|
+
import { waitForYogaInit } from "ink-web";
|
|
279
|
+
import { createInkStreams, type InkStreams } from "./ink-streams";
|
|
280
|
+
import { InkApp } from "./ink-app";
|
|
281
|
+
import "@wterm/dom/src/terminal.css";
|
|
282
|
+
|
|
283
|
+
const inkApp = <InkApp />;
|
|
284
|
+
|
|
285
|
+
export default function InkTerminal() {
|
|
286
|
+
const { ref, write } = useTerminal();
|
|
287
|
+
const wrapperRef = useRef<HTMLDivElement>(null);
|
|
288
|
+
const streamsRef = useRef<InkStreams | null>(null);
|
|
289
|
+
const instanceRef = useRef<Instance | null>(null);
|
|
290
|
+
const startedRef = useRef(false);
|
|
291
|
+
const disposedRef = useRef(false);
|
|
292
|
+
const colsRef = useRef(0);
|
|
293
|
+
const scrollPendingRef = useRef(false);
|
|
294
|
+
|
|
295
|
+
const writeRef = useRef(write);
|
|
296
|
+
writeRef.current = write;
|
|
297
|
+
|
|
298
|
+
// wterm does not auto-scroll on new output (vercel-labs/wterm#61); follow
|
|
299
|
+
// the tail after its coalesced write -> setTimeout(0) -> rAF render pass.
|
|
300
|
+
// Skipped while the visitor has scrolled up to read scrollback.
|
|
301
|
+
const scheduleScroll = useCallback(() => {
|
|
302
|
+
if (scrollPendingRef.current) return;
|
|
303
|
+
const el = wrapperRef.current?.querySelector(".wterm");
|
|
304
|
+
if (el && el.scrollHeight - el.scrollTop - el.clientHeight > 100) return;
|
|
305
|
+
scrollPendingRef.current = true;
|
|
306
|
+
setTimeout(() => {
|
|
307
|
+
requestAnimationFrame(() => {
|
|
308
|
+
scrollPendingRef.current = false;
|
|
309
|
+
const target = wrapperRef.current?.querySelector(".wterm");
|
|
310
|
+
if (target) target.scrollTop = target.scrollHeight;
|
|
311
|
+
});
|
|
312
|
+
}, 0);
|
|
313
|
+
}, []);
|
|
314
|
+
|
|
315
|
+
const getStreams = useCallback(() => {
|
|
316
|
+
streamsRef.current ??= createInkStreams((data) => {
|
|
317
|
+
writeRef.current(data);
|
|
318
|
+
scheduleScroll();
|
|
319
|
+
});
|
|
320
|
+
return streamsRef.current;
|
|
321
|
+
}, [scheduleScroll]);
|
|
322
|
+
|
|
323
|
+
const handleReady = useCallback(
|
|
324
|
+
(wt: WTerm) => {
|
|
325
|
+
const streams = getStreams();
|
|
326
|
+
colsRef.current = wt.cols;
|
|
327
|
+
streams.setSize(wt.cols, wt.rows);
|
|
328
|
+
// ink components draw their own cursor; the bundled ink's cli-cursor
|
|
329
|
+
// shim is a no-op, so hide wterm's grid cursor here.
|
|
330
|
+
wt.write("\x1b[?25l");
|
|
331
|
+
|
|
332
|
+
if (!startedRef.current) {
|
|
333
|
+
startedRef.current = true;
|
|
334
|
+
void waitForYogaInit().then(() => {
|
|
335
|
+
if (disposedRef.current) return;
|
|
336
|
+
instanceRef.current = render(inkApp, {
|
|
337
|
+
stdout: streams.stdout,
|
|
338
|
+
stderr: streams.stdout,
|
|
339
|
+
stdin: streams.stdin,
|
|
340
|
+
patchConsole: false,
|
|
341
|
+
exitOnCtrlC: false,
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
} else if (instanceRef.current) {
|
|
345
|
+
instanceRef.current.clear();
|
|
346
|
+
instanceRef.current.rerender(inkApp);
|
|
347
|
+
}
|
|
348
|
+
},
|
|
349
|
+
[getStreams],
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
// wterm encodes the Backspace key as \x7f, which the bundled ink 6 parses
|
|
353
|
+
// as key.delete (forward delete); rewrite to \x08, which it parses as
|
|
354
|
+
// backspace. \x03 (Ctrl+C) is dropped so it stays free for copy.
|
|
355
|
+
const handleData = useCallback(
|
|
356
|
+
(data: string) => {
|
|
357
|
+
const mapped = data.replaceAll("\x7f", "\x08").replaceAll("\x03", "");
|
|
358
|
+
if (mapped.length === 0) return;
|
|
359
|
+
getStreams().pushInput(mapped);
|
|
360
|
+
},
|
|
361
|
+
[getStreams],
|
|
362
|
+
);
|
|
363
|
+
|
|
364
|
+
const handleResize = useCallback(
|
|
365
|
+
(cols: number, rows: number) => {
|
|
366
|
+
if (cols < colsRef.current && instanceRef.current) {
|
|
367
|
+
instanceRef.current.clear();
|
|
368
|
+
writeRef.current("\x1b[2J\x1b[3J\x1b[H");
|
|
369
|
+
}
|
|
370
|
+
colsRef.current = cols;
|
|
371
|
+
getStreams().setSize(cols, rows);
|
|
372
|
+
},
|
|
373
|
+
[getStreams],
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
useEffect(() => {
|
|
377
|
+
disposedRef.current = false;
|
|
378
|
+
return () => {
|
|
379
|
+
disposedRef.current = true;
|
|
380
|
+
instanceRef.current?.unmount();
|
|
381
|
+
instanceRef.current = null;
|
|
382
|
+
startedRef.current = false;
|
|
383
|
+
};
|
|
384
|
+
}, []);
|
|
385
|
+
|
|
386
|
+
return (
|
|
387
|
+
<div className="terminal-center">
|
|
388
|
+
<div ref={wrapperRef} className="terminal-host">
|
|
389
|
+
<Terminal
|
|
390
|
+
ref={ref}
|
|
391
|
+
autoResize
|
|
392
|
+
onReady={handleReady}
|
|
393
|
+
onData={handleData}
|
|
394
|
+
onResize={handleResize}
|
|
395
|
+
/>
|
|
396
|
+
</div>
|
|
397
|
+
</div>
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
## app/thread.tsx
|
|
404
|
+
|
|
405
|
+
```tsx
|
|
406
|
+
// Mirrors examples/with-react-ink/src/components/thread.tsx; keep in sync.
|
|
407
|
+
import { Box, Text, useStdout } from "ink";
|
|
408
|
+
import {
|
|
409
|
+
ThreadPrimitive,
|
|
410
|
+
ComposerPrimitive,
|
|
411
|
+
MessagePrimitive,
|
|
412
|
+
ErrorPrimitive,
|
|
413
|
+
LoadingPrimitive,
|
|
414
|
+
LiveChecklist,
|
|
415
|
+
} from "@assistant-ui/react-ink";
|
|
416
|
+
import { MarkdownText } from "@assistant-ui/react-ink-markdown";
|
|
417
|
+
|
|
418
|
+
// markdansi defaults width and color from process.stdout, which does not
|
|
419
|
+
// exist in the browser bundle; pass both explicitly so it never reads it.
|
|
420
|
+
// color is forwarded to markdansi but not yet declared on MarkdownTextProps.
|
|
421
|
+
const BrowserMarkdownText = ({ text }: { text: string }) => {
|
|
422
|
+
const { stdout } = useStdout();
|
|
423
|
+
return (
|
|
424
|
+
<MarkdownText
|
|
425
|
+
text={text}
|
|
426
|
+
width={(stdout?.columns ?? 80) - 4}
|
|
427
|
+
hyperlinks={false}
|
|
428
|
+
{...{ color: true }}
|
|
429
|
+
/>
|
|
430
|
+
);
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
const UserMessage = () => (
|
|
434
|
+
<MessagePrimitive.Root>
|
|
435
|
+
<Box marginBottom={1}>
|
|
436
|
+
<Text bold color="green">
|
|
437
|
+
You:{" "}
|
|
438
|
+
</Text>
|
|
439
|
+
<MessagePrimitive.Content
|
|
440
|
+
renderText={({ part }) => <Text wrap="wrap">{part.text}</Text>}
|
|
441
|
+
/>
|
|
442
|
+
</Box>
|
|
443
|
+
</MessagePrimitive.Root>
|
|
444
|
+
);
|
|
445
|
+
|
|
446
|
+
const AssistantMessage = () => (
|
|
447
|
+
<MessagePrimitive.Root>
|
|
448
|
+
<Box flexDirection="column" marginBottom={1}>
|
|
449
|
+
<Text bold color="blue">
|
|
450
|
+
AI:
|
|
451
|
+
</Text>
|
|
452
|
+
<MessagePrimitive.Content
|
|
453
|
+
renderText={({ part }) => <BrowserMarkdownText text={part.text} />}
|
|
454
|
+
renderReasoning={({ part }) => (
|
|
455
|
+
<Text dimColor italic>
|
|
456
|
+
{part.text}
|
|
457
|
+
</Text>
|
|
458
|
+
)}
|
|
459
|
+
/>
|
|
460
|
+
<LiveChecklist title="Plan" showProgress marginTop={1} />
|
|
461
|
+
<ErrorPrimitive.Root>
|
|
462
|
+
<ErrorPrimitive.Message />
|
|
463
|
+
</ErrorPrimitive.Root>
|
|
464
|
+
</Box>
|
|
465
|
+
</MessagePrimitive.Root>
|
|
466
|
+
);
|
|
467
|
+
|
|
468
|
+
const Loading = () => (
|
|
469
|
+
<LoadingPrimitive.Root marginBottom={1}>
|
|
470
|
+
<LoadingPrimitive.Spinner />
|
|
471
|
+
<Text> </Text>
|
|
472
|
+
<LoadingPrimitive.Text>Working</LoadingPrimitive.Text>
|
|
473
|
+
<Text> </Text>
|
|
474
|
+
<LoadingPrimitive.ElapsedTime />
|
|
475
|
+
</LoadingPrimitive.Root>
|
|
476
|
+
);
|
|
477
|
+
|
|
478
|
+
export const Thread = () => {
|
|
479
|
+
return (
|
|
480
|
+
<ThreadPrimitive.Root>
|
|
481
|
+
<ThreadPrimitive.Empty>
|
|
482
|
+
<Box flexDirection="column" marginBottom={1}>
|
|
483
|
+
<Text>
|
|
484
|
+
A real LLM streaming into a real Ink render loop in your browser.
|
|
485
|
+
</Text>
|
|
486
|
+
<Text dimColor>
|
|
487
|
+
{' try: "what is assistant-ui?" or "how do I render markdown?"'}
|
|
488
|
+
</Text>
|
|
489
|
+
</Box>
|
|
490
|
+
</ThreadPrimitive.Empty>
|
|
491
|
+
|
|
492
|
+
<ThreadPrimitive.Messages>
|
|
493
|
+
{({ message }) =>
|
|
494
|
+
message.role === "user" ? <UserMessage /> : <AssistantMessage />
|
|
495
|
+
}
|
|
496
|
+
</ThreadPrimitive.Messages>
|
|
497
|
+
|
|
498
|
+
<Loading />
|
|
499
|
+
|
|
500
|
+
<Box borderStyle="round" borderColor="gray" paddingX={1}>
|
|
501
|
+
<Text color="gray">{"> "}</Text>
|
|
502
|
+
<ComposerPrimitive.Input
|
|
503
|
+
submitOnEnter
|
|
504
|
+
multiLine
|
|
505
|
+
placeholder="Type a message... (Enter to send)"
|
|
506
|
+
autoFocus
|
|
507
|
+
/>
|
|
508
|
+
</Box>
|
|
509
|
+
</ThreadPrimitive.Root>
|
|
510
|
+
);
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
## next.config.ts
|
|
516
|
+
|
|
517
|
+
```typescript
|
|
518
|
+
import path from "node:path";
|
|
519
|
+
import type { NextConfig } from "next";
|
|
520
|
+
|
|
521
|
+
const config: NextConfig = {
|
|
522
|
+
output: "export",
|
|
523
|
+
transpilePackages: ["ink-web"],
|
|
524
|
+
turbopack: {
|
|
525
|
+
// Pin the workspace root; vercel build's environment makes Turbopack's
|
|
526
|
+
// lockfile-based inference resolve to the app directory instead of the
|
|
527
|
+
// monorepo root, which breaks resolution of pnpm-symlinked packages.
|
|
528
|
+
root: path.join(__dirname, "../.."),
|
|
529
|
+
resolveAlias: {
|
|
530
|
+
ink: "ink-web",
|
|
531
|
+
"supports-hyperlinks": {
|
|
532
|
+
browser: "./shims/supports-hyperlinks.ts",
|
|
533
|
+
},
|
|
534
|
+
},
|
|
535
|
+
},
|
|
536
|
+
};
|
|
537
|
+
|
|
538
|
+
export default config;
|
|
539
|
+
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
## package.json
|
|
543
|
+
|
|
544
|
+
```json
|
|
545
|
+
{
|
|
546
|
+
"name": "with-react-ink-web",
|
|
547
|
+
"version": "0.0.0",
|
|
548
|
+
"private": true,
|
|
549
|
+
"scripts": {
|
|
550
|
+
"dev": "next dev -p 8081",
|
|
551
|
+
"build": "next build",
|
|
552
|
+
"start": "next start"
|
|
553
|
+
},
|
|
554
|
+
"dependencies": {
|
|
555
|
+
"@assistant-ui/react-ai-sdk": "workspace:*",
|
|
556
|
+
"@assistant-ui/react-ink": "workspace:*",
|
|
557
|
+
"@assistant-ui/react-ink-markdown": "workspace:*",
|
|
558
|
+
"@wterm/dom": "^0.3.0",
|
|
559
|
+
"@wterm/react": "^0.3.0",
|
|
560
|
+
"ai": "^6.0.208",
|
|
561
|
+
"ink": "^7.1.0",
|
|
562
|
+
"ink-web": "^0.2.0",
|
|
563
|
+
"next": "^16.2.9",
|
|
564
|
+
"react": "^19.2.7",
|
|
565
|
+
"react-dom": "^19.2.7",
|
|
566
|
+
"react-reconciler": "^0.33.0",
|
|
567
|
+
"scheduler": "^0.27.0"
|
|
568
|
+
},
|
|
569
|
+
"devDependencies": {
|
|
570
|
+
"@types/node": "^26.0.0",
|
|
571
|
+
"@types/react": "^19.2.17",
|
|
572
|
+
"typescript": "^6.0.3"
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
## README.md
|
|
579
|
+
|
|
580
|
+
```markdown
|
|
581
|
+
# Welcome to the assistant-ui ink web demo 👋
|
|
582
|
+
|
|
583
|
+
This app runs the [`@assistant-ui/react-ink`](https://www.assistant-ui.com/docs/ink) terminal chat in the browser against a real LLM. It powers the live demo embedded at [assistant-ui.com/ink](https://www.assistant-ui.com/ink) and is deployed at [assistant-ui-ink.vercel.app](https://assistant-ui-ink.vercel.app).
|
|
584
|
+
|
|
585
|
+
## Get started
|
|
586
|
+
|
|
587
|
+
1. Install dependencies
|
|
588
|
+
|
|
589
|
+
```bash
|
|
590
|
+
pnpm install
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
2. Start the docs app, which serves the chat endpoint on [localhost:3000](http://localhost:3000)
|
|
594
|
+
|
|
595
|
+
```bash
|
|
596
|
+
pnpm --filter @assistant-ui/docs dev
|
|
597
|
+
```
|
|
598
|
+
|
|
599
|
+
3. Start the app
|
|
600
|
+
|
|
601
|
+
```bash
|
|
602
|
+
pnpm dev
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
The terminal opens on [localhost:8081](http://localhost:8081), and the docs app embeds this URL on its `/ink` page in development.
|
|
606
|
+
|
|
607
|
+
## How it works
|
|
608
|
+
|
|
609
|
+
Ink has no official web target, so this app renders the real `@assistant-ui/react-ink` component tree through [ink-web](https://github.com/cjroth/ink-web)'s browser build of Ink, bridged to a [wterm](https://github.com/vercel-labs/wterm) terminal. wterm renders to the DOM, so text selection, copy, find, and screen readers work natively.
|
|
610
|
+
|
|
611
|
+
Chat requests go to the assistant-ui docs site's `/api/chat`, the same pattern as the React Native demo at [assistant-ui-expo.vercel.app](https://assistant-ui-expo.vercel.app). This app holds no API keys; set `NEXT_PUBLIC_CHAT_ENDPOINT_URL` to point at a different endpoint.
|
|
612
|
+
|
|
613
|
+
## Learn more
|
|
614
|
+
|
|
615
|
+
- [assistant-ui ink documentation](https://www.assistant-ui.com/docs/ink): primitives, hooks, and adapters for terminal UIs.
|
|
616
|
+
- [with-react-ink](../with-react-ink): the real terminal version of this example. Start there if you are building an Ink app.
|
|
617
|
+
|
|
618
|
+
```
|
|
619
|
+
|
|
620
|
+
## shims/supports-hyperlinks.ts
|
|
621
|
+
|
|
622
|
+
```typescript
|
|
623
|
+
const supportsHyperlinks = {
|
|
624
|
+
stdout: false,
|
|
625
|
+
stderr: false,
|
|
626
|
+
};
|
|
627
|
+
|
|
628
|
+
export default supportsHyperlinks;
|
|
629
|
+
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
## tsconfig.json
|
|
633
|
+
|
|
634
|
+
```json
|
|
635
|
+
{
|
|
636
|
+
"compilerOptions": {
|
|
637
|
+
"target": "ES2022",
|
|
638
|
+
"lib": ["dom", "dom.iterable", "esnext"],
|
|
639
|
+
"allowJs": false,
|
|
640
|
+
"skipLibCheck": true,
|
|
641
|
+
"strict": true,
|
|
642
|
+
"noEmit": true,
|
|
643
|
+
"esModuleInterop": true,
|
|
644
|
+
"module": "esnext",
|
|
645
|
+
"moduleResolution": "bundler",
|
|
646
|
+
"resolveJsonModule": true,
|
|
647
|
+
"isolatedModules": true,
|
|
648
|
+
"jsx": "react-jsx",
|
|
649
|
+
"incremental": true,
|
|
650
|
+
"plugins": [
|
|
651
|
+
{
|
|
652
|
+
"name": "next"
|
|
653
|
+
}
|
|
654
|
+
]
|
|
655
|
+
},
|
|
656
|
+
"include": [
|
|
657
|
+
"next-env.d.ts",
|
|
658
|
+
"**/*.ts",
|
|
659
|
+
"**/*.tsx",
|
|
660
|
+
".next/types/**/*.ts",
|
|
661
|
+
".next/dev/types/**/*.ts"
|
|
662
|
+
],
|
|
663
|
+
"exclude": ["node_modules"]
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
```
|
|
667
|
+
|
|
668
|
+
## vercel.json
|
|
669
|
+
|
|
670
|
+
```json
|
|
671
|
+
{
|
|
672
|
+
"$schema": "https://openapi.vercel.sh/vercel.json",
|
|
673
|
+
"ignoreCommand": "bash ../../scripts/vercel-ignore-changeset-release.sh",
|
|
674
|
+
"buildCommand": "cd ../.. && pnpm turbo build --filter=@assistant-ui/react-ink --filter=@assistant-ui/react-ink-markdown --filter=@assistant-ui/react-ai-sdk && cd examples/with-react-ink-web && pnpm run build",
|
|
675
|
+
"outputDirectory": "out",
|
|
676
|
+
"framework": null,
|
|
677
|
+
"headers": [
|
|
678
|
+
{
|
|
679
|
+
"source": "/(.*)",
|
|
680
|
+
"headers": [
|
|
681
|
+
{
|
|
682
|
+
"key": "Content-Security-Policy",
|
|
683
|
+
"value": "frame-ancestors 'self' https://*.assistant-ui.com https://*.vercel.app http://localhost:*;"
|
|
684
|
+
}
|
|
685
|
+
]
|
|
686
|
+
}
|
|
687
|
+
]
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
```
|
|
691
|
+
|