@dxos/react-ui-assistant 0.11.1

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.
Files changed (91) hide show
  1. package/LICENSE +105 -0
  2. package/dist/lib/index.mjs +839 -0
  3. package/dist/lib/index.mjs.map +1 -0
  4. package/dist/lib/testing.mjs +214 -0
  5. package/dist/lib/testing.mjs.map +1 -0
  6. package/dist/types/src/components/ChatThread/ChatThread.d.ts +52 -0
  7. package/dist/types/src/components/ChatThread/ChatThread.d.ts.map +1 -0
  8. package/dist/types/src/components/ChatThread/ChatThread.stories.d.ts +38 -0
  9. package/dist/types/src/components/ChatThread/ChatThread.stories.d.ts.map +1 -0
  10. package/dist/types/src/components/ChatThread/index.d.ts +2 -0
  11. package/dist/types/src/components/ChatThread/index.d.ts.map +1 -0
  12. package/dist/types/src/components/MessageChrome/MessageChrome.d.ts +28 -0
  13. package/dist/types/src/components/MessageChrome/MessageChrome.d.ts.map +1 -0
  14. package/dist/types/src/components/MessageChrome/index.d.ts +2 -0
  15. package/dist/types/src/components/MessageChrome/index.d.ts.map +1 -0
  16. package/dist/types/src/components/index.d.ts +4 -0
  17. package/dist/types/src/components/index.d.ts.map +1 -0
  18. package/dist/types/src/components/widgets/FallbackWidget.d.ts +7 -0
  19. package/dist/types/src/components/widgets/FallbackWidget.d.ts.map +1 -0
  20. package/dist/types/src/components/widgets/ReasoningWidget.d.ts +25 -0
  21. package/dist/types/src/components/widgets/ReasoningWidget.d.ts.map +1 -0
  22. package/dist/types/src/components/widgets/ReasoningWidget.stories.d.ts +18 -0
  23. package/dist/types/src/components/widgets/ReasoningWidget.stories.d.ts.map +1 -0
  24. package/dist/types/src/components/widgets/ReferenceWidget.d.ts +9 -0
  25. package/dist/types/src/components/widgets/ReferenceWidget.d.ts.map +1 -0
  26. package/dist/types/src/components/widgets/SelectWidget.d.ts +14 -0
  27. package/dist/types/src/components/widgets/SelectWidget.d.ts.map +1 -0
  28. package/dist/types/src/components/widgets/StatsWidget.d.ts +12 -0
  29. package/dist/types/src/components/widgets/StatsWidget.d.ts.map +1 -0
  30. package/dist/types/src/components/widgets/StatusWidget.d.ts +12 -0
  31. package/dist/types/src/components/widgets/StatusWidget.d.ts.map +1 -0
  32. package/dist/types/src/components/widgets/SuggestionWidget.d.ts +11 -0
  33. package/dist/types/src/components/widgets/SuggestionWidget.d.ts.map +1 -0
  34. package/dist/types/src/components/widgets/SummaryWidget.d.ts +4 -0
  35. package/dist/types/src/components/widgets/SummaryWidget.d.ts.map +1 -0
  36. package/dist/types/src/components/widgets/ToolWidget.d.ts +10 -0
  37. package/dist/types/src/components/widgets/ToolWidget.d.ts.map +1 -0
  38. package/dist/types/src/components/widgets/ToolWidget.stories.d.ts +31 -0
  39. package/dist/types/src/components/widgets/ToolWidget.stories.d.ts.map +1 -0
  40. package/dist/types/src/components/widgets/defaults.d.ts +11 -0
  41. package/dist/types/src/components/widgets/defaults.d.ts.map +1 -0
  42. package/dist/types/src/components/widgets/format-time.d.ts +2 -0
  43. package/dist/types/src/components/widgets/format-time.d.ts.map +1 -0
  44. package/dist/types/src/components/widgets/index.d.ts +11 -0
  45. package/dist/types/src/components/widgets/index.d.ts.map +1 -0
  46. package/dist/types/src/index.d.ts +6 -0
  47. package/dist/types/src/index.d.ts.map +1 -0
  48. package/dist/types/src/registry.d.ts +11 -0
  49. package/dist/types/src/registry.d.ts.map +1 -0
  50. package/dist/types/src/renderer.d.ts +25 -0
  51. package/dist/types/src/renderer.d.ts.map +1 -0
  52. package/dist/types/src/testing/index.d.ts +2 -0
  53. package/dist/types/src/testing/index.d.ts.map +1 -0
  54. package/dist/types/src/testing/test-generator.d.ts +7 -0
  55. package/dist/types/src/testing/test-generator.d.ts.map +1 -0
  56. package/dist/types/src/testing/test-generator.test.d.ts +2 -0
  57. package/dist/types/src/testing/test-generator.test.d.ts.map +1 -0
  58. package/dist/types/src/translations.d.ts +19 -0
  59. package/dist/types/src/translations.d.ts.map +1 -0
  60. package/dist/types/src/types.d.ts +28 -0
  61. package/dist/types/src/types.d.ts.map +1 -0
  62. package/dist/types/tsconfig.tsbuildinfo +1 -0
  63. package/package.json +67 -0
  64. package/src/components/ChatThread/ChatThread.stories.tsx +419 -0
  65. package/src/components/ChatThread/ChatThread.tsx +188 -0
  66. package/src/components/ChatThread/index.ts +5 -0
  67. package/src/components/MessageChrome/MessageChrome.tsx +147 -0
  68. package/src/components/MessageChrome/index.ts +5 -0
  69. package/src/components/index.ts +7 -0
  70. package/src/components/widgets/FallbackWidget.tsx +26 -0
  71. package/src/components/widgets/ReasoningWidget.stories.tsx +68 -0
  72. package/src/components/widgets/ReasoningWidget.ts +139 -0
  73. package/src/components/widgets/ReferenceWidget.ts +28 -0
  74. package/src/components/widgets/SelectWidget.ts +39 -0
  75. package/src/components/widgets/StatsWidget.ts +31 -0
  76. package/src/components/widgets/StatusWidget.ts +53 -0
  77. package/src/components/widgets/SuggestionWidget.ts +50 -0
  78. package/src/components/widgets/SummaryWidget.tsx +27 -0
  79. package/src/components/widgets/ToolWidget.stories.tsx +55 -0
  80. package/src/components/widgets/ToolWidget.tsx +185 -0
  81. package/src/components/widgets/defaults.ts +14 -0
  82. package/src/components/widgets/format-time.ts +45 -0
  83. package/src/components/widgets/index.ts +14 -0
  84. package/src/index.ts +9 -0
  85. package/src/registry.tsx +144 -0
  86. package/src/renderer.ts +216 -0
  87. package/src/testing/index.ts +5 -0
  88. package/src/testing/test-generator.test.ts +75 -0
  89. package/src/testing/test-generator.ts +280 -0
  90. package/src/translations.ts +27 -0
  91. package/src/types.ts +32 -0
@@ -0,0 +1,75 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import * as Effect from 'effect/Effect';
6
+ import * as Layer from 'effect/Layer';
7
+ import { describe, test } from 'vitest';
8
+
9
+ import { Database, Entity, Feed, Obj } from '@dxos/echo';
10
+ import { EffectEx } from '@dxos/effect';
11
+ import { type Message } from '@dxos/types';
12
+
13
+ import { createMessageGenerator } from './test-generator';
14
+
15
+ /**
16
+ * In-memory Database.Service that records appendToFeed calls and emits a change event
17
+ * whenever `appendToFeed` is called. Used to verify the streaming-message generator
18
+ * emits per-chunk updates.
19
+ */
20
+ const makeRecordingDatabaseLayer = () => {
21
+ const items: Message.Message[] = [];
22
+ const subscribers = new Set<() => void>();
23
+
24
+ const dbService: Database.Database = {
25
+ get spaceId() {
26
+ return 'recording' as any;
27
+ },
28
+ add: (obj: any) => obj as any,
29
+ addType: async () => undefined as any,
30
+ remove: () => {},
31
+ appendToFeed: async (_feed: Feed.Feed, newItems: Entity.Unknown[]) => {
32
+ items.push(...(newItems as Message.Message[]));
33
+ for (const fn of subscribers) {
34
+ fn();
35
+ }
36
+ },
37
+ removeFeedItemsByIds: async () => {},
38
+ queryFeed: () => ({ subscribe: () => () => {}, results: [], run: async () => [] }) as any,
39
+ query: () => ({ subscribe: () => () => {}, results: [], run: async () => [] }) as any,
40
+ syncFeed: async () => {},
41
+ getFeedSyncState: async () => ({ blocksToPull: 0, blocksToPush: 0, totalBlocks: 0 }),
42
+ flush: async () => {},
43
+ makeRef: () => undefined as any,
44
+ deleteFromFeed: async () => {},
45
+ } as unknown as Database.Database;
46
+
47
+ return {
48
+ items,
49
+ onAppend: (cb: () => void) => subscribers.add(cb),
50
+ layer: Layer.succeed(Database.Service, { db: dbService }),
51
+ };
52
+ };
53
+
54
+ describe('createMessageGenerator', () => {
55
+ test('streaming message publishes updates for chunk mutations', async ({ expect }) => {
56
+ const recording = makeRecordingDatabaseLayer();
57
+ let updates = 0;
58
+ recording.onAppend(() => updates++);
59
+
60
+ const feed = Obj.make(Feed.Feed, { name: 'recording' });
61
+
62
+ // The streaming step is the third entry (index 2) — earlier indices are the initial
63
+ // user prompt and an assistant message with an Organization link (which requires
64
+ // a real database; the recording layer stub is sufficient for this streaming test).
65
+ await EffectEx.runAndForwardErrors(
66
+ createMessageGenerator()[2]!.pipe(
67
+ Effect.provide(Layer.mergeAll(Feed.ContextFeedService.layer(feed), recording.layer)),
68
+ ),
69
+ );
70
+
71
+ expect(recording.items).toHaveLength(1);
72
+ expect(updates).toBeGreaterThan(1);
73
+ expect(recording.items[0].blocks[0]).toMatchObject({ pending: false });
74
+ });
75
+ });
@@ -0,0 +1,280 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import * as Effect from 'effect/Effect';
6
+
7
+ import { Database, Feed, Obj } from '@dxos/echo';
8
+ import { type Mutable } from '@dxos/echo/Obj';
9
+ import { random } from '@dxos/random';
10
+ import { renderObjectLink, textStream } from '@dxos/react-ui-markdown';
11
+ import { type Actor, type ContentBlock, Message, Organization } from '@dxos/types';
12
+ import { trim } from '@dxos/util';
13
+
14
+ export const createMessage = (role: Actor.Role, blocks: ContentBlock.Any[]): Message.Message => {
15
+ return Obj.make(Message.Message, {
16
+ created: new Date().toISOString(),
17
+ sender: { role },
18
+ blocks,
19
+ });
20
+ };
21
+
22
+ export type MessageGenerator = Effect.Effect<void, never, Database.Service | Feed.ContextFeedService>;
23
+
24
+ export const createMessageGenerator = (): MessageGenerator[] => [
25
+ Effect.gen(function* () {
26
+ const { feed } = yield* Feed.ContextFeedService;
27
+ yield* Feed.append(feed, [
28
+ createMessage('user', [
29
+ {
30
+ _tag: 'text',
31
+ text: random.lorem.sentence(5),
32
+ },
33
+ ]),
34
+ ]);
35
+ }),
36
+
37
+ Effect.gen(function* () {
38
+ const { feed } = yield* Feed.ContextFeedService;
39
+ const { db } = yield* Database.Service;
40
+ const obj1 = db.add(
41
+ Obj.make(Organization.Organization, {
42
+ name: 'DXOS',
43
+ website: 'https://dxos.org',
44
+ description: 'DXOS is a decentralized network for collaborative applications.',
45
+ }),
46
+ );
47
+ // const obj2 = db.add(Obj.make(Person.Person, { fullName: 'Alice' }));
48
+ // const obj3 = db.add(Obj.make(Person.Person, { fullName: 'Bob' }));
49
+ // const obj4 = db.add(Obj.make(Person.Person, { fullName: 'Charlie' }));
50
+ yield* Feed.append(feed, [
51
+ createMessage('assistant', [
52
+ // Inline tag.
53
+ {
54
+ _tag: 'text',
55
+ text: [random.lorem.paragraph(), renderObjectLink(obj1), random.lorem.paragraph(), '\n'].join(' '),
56
+ },
57
+
58
+ // Inline cards.
59
+ // ...[obj1, obj2, obj3, obj4].map(
60
+ // (obj) =>
61
+ // ({
62
+ // _tag: 'text',
63
+ // text: renderObjectLink(obj, true) + '\n',
64
+ // }) satisfies ContentBlock.Text,
65
+ // ),
66
+ ]),
67
+ ]);
68
+ }),
69
+
70
+ // Streaming text block: appends a pending text block, then mutates `text` in chunks
71
+ // so the syncer renders progressive deltas through the feed (not via the controller).
72
+ Effect.gen(function* () {
73
+ const { feed } = yield* Feed.ContextFeedService;
74
+ const { db } = yield* Database.Service;
75
+ const message = createMessage('assistant', [{ _tag: 'text', text: '', pending: true }]);
76
+ yield* Feed.append(feed, [message]);
77
+
78
+ const fullText = [
79
+ 'Streaming a response **word by word** through the feed:',
80
+ random.lorem.paragraph(),
81
+ random.lorem.paragraph(),
82
+ ].join('\n\n');
83
+
84
+ yield* Effect.promise(async () => {
85
+ for await (const chunk of textStream(fullText, { wordsPerChunk: 2, chunkDelay: 60 })) {
86
+ Obj.update(message, (message) => {
87
+ const block = message.blocks[0] as Mutable<ContentBlock.Text>;
88
+ block.text += chunk;
89
+ });
90
+ // Feed queries only react to feed-level updates, not in-place object mutations.
91
+ await db.appendToFeed(feed, []);
92
+ }
93
+ Obj.update(message, (message) => {
94
+ const block = message.blocks[0] as Mutable<ContentBlock.Text>;
95
+ block.pending = false;
96
+ });
97
+ await db.appendToFeed(feed, []);
98
+ });
99
+ }),
100
+
101
+ Effect.gen(function* () {
102
+ const { feed } = yield* Feed.ContextFeedService;
103
+ yield* Feed.append(feed, [
104
+ createMessage('assistant', [
105
+ {
106
+ _tag: 'text',
107
+ text:
108
+ trim`
109
+ ## Markdown
110
+ Here is a [link](https://dxos.org)
111
+
112
+ And some **bold** text.
113
+
114
+ And some JSON
115
+ \`\`\`json
116
+ {
117
+ "key": "value"
118
+ }
119
+ \`\`\`
120
+
121
+ And some code
122
+ \`\`\`js
123
+ const x = 1;
124
+ \`\`\`
125
+ ` + '\n',
126
+ },
127
+ {
128
+ _tag: 'text',
129
+ disposition: 'cot',
130
+ text:
131
+ [
132
+ random.lorem.paragraph(),
133
+ '',
134
+ ...Array.from({
135
+ length: random.number.int({ min: 3, max: 5 }),
136
+ }).map((_, idx) => `${idx + 1}. ${random.lorem.paragraph()}`),
137
+ ].join('\n') + '\n',
138
+ },
139
+ ]),
140
+ ]);
141
+ }),
142
+
143
+ Effect.gen(function* () {
144
+ const { feed } = yield* Feed.ContextFeedService;
145
+ yield* Feed.append(feed, [
146
+ createMessage('assistant', [
147
+ {
148
+ _tag: 'text',
149
+ text: 'How can I help?',
150
+ },
151
+ {
152
+ _tag: 'suggestion',
153
+ text: 'List tools',
154
+ },
155
+ {
156
+ _tag: 'suggestion',
157
+ text: 'Show info',
158
+ },
159
+ {
160
+ _tag: 'suggestion',
161
+ text: random.lorem.paragraph(),
162
+ },
163
+ ]),
164
+ createMessage('assistant', [
165
+ {
166
+ _tag: 'text',
167
+ text: 'Select an option:',
168
+ },
169
+ {
170
+ _tag: 'select',
171
+ options: ['Option 1', 'Option 2', 'Option 3'],
172
+ },
173
+ ]),
174
+ ]);
175
+ }),
176
+
177
+ Effect.gen(function* () {
178
+ const { feed } = yield* Feed.ContextFeedService;
179
+ yield* Feed.append(feed, [
180
+ createMessage('assistant', [
181
+ {
182
+ _tag: 'toolkit',
183
+ },
184
+ ]),
185
+ ]);
186
+ }),
187
+
188
+ Effect.gen(function* () {
189
+ const { feed } = yield* Feed.ContextFeedService;
190
+ yield* Feed.append(feed, [
191
+ createMessage('user', [
192
+ {
193
+ _tag: 'text',
194
+ text: random.lorem.sentence(5),
195
+ },
196
+ ]),
197
+ ]);
198
+ }),
199
+
200
+ Effect.gen(function* () {
201
+ const { feed } = yield* Feed.ContextFeedService;
202
+ yield* Feed.append(feed, [
203
+ createMessage('assistant', [
204
+ {
205
+ _tag: 'text',
206
+ text: random.lorem.paragraph() + '\n',
207
+ },
208
+ ]),
209
+ createMessage('assistant', [
210
+ {
211
+ _tag: 'toolCall',
212
+ toolCallId: '1234',
213
+ name: 'search',
214
+ input: JSON.stringify({}),
215
+ providerExecuted: false,
216
+ },
217
+ ]),
218
+ createMessage('user', [
219
+ {
220
+ _tag: 'toolResult',
221
+ toolCallId: '1234',
222
+ name: 'search',
223
+ result: 'This is a tool result.',
224
+ providerExecuted: false,
225
+ },
226
+ ]),
227
+ ]);
228
+ }),
229
+
230
+ Effect.gen(function* () {
231
+ const { feed } = yield* Feed.ContextFeedService;
232
+ yield* Feed.append(feed, [
233
+ createMessage('assistant', [
234
+ {
235
+ _tag: 'text',
236
+ text: random.lorem.paragraph() + '\n',
237
+ },
238
+ ]),
239
+ createMessage('assistant', [
240
+ {
241
+ _tag: 'toolCall',
242
+ toolCallId: '4567',
243
+ name: 'create',
244
+ input: JSON.stringify({}),
245
+ providerExecuted: false,
246
+ },
247
+ ]),
248
+ createMessage('user', [
249
+ {
250
+ _tag: 'toolResult',
251
+ toolCallId: '4567',
252
+ name: 'create',
253
+ result: 'This is a tool result.',
254
+ providerExecuted: false,
255
+ },
256
+ ]),
257
+ createMessage('assistant', [
258
+ {
259
+ _tag: 'text',
260
+ text:
261
+ Array.from({ length: random.number.int({ min: 2, max: 3 }) })
262
+ .map(() => random.lorem.paragraph())
263
+ .join('\n\n') + '\n',
264
+ },
265
+ ]),
266
+ ]);
267
+ }),
268
+
269
+ Effect.gen(function* () {
270
+ const { feed } = yield* Feed.ContextFeedService;
271
+ yield* Feed.append(feed, [
272
+ createMessage('assistant', [
273
+ {
274
+ _tag: 'text',
275
+ text: random.lorem.paragraph(),
276
+ },
277
+ ]),
278
+ ]);
279
+ }),
280
+ ];
@@ -0,0 +1,27 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import { type Resource } from '@dxos/react-ui';
6
+
7
+ export const translationKey = '@dxos/react-ui-assistant';
8
+
9
+ export const translations = [
10
+ {
11
+ 'en-US': {
12
+ [translationKey]: {
13
+ 'copy.label': 'Copy',
14
+ 'rewind.label': 'Rewind to this prompt',
15
+ 'summary.label': 'Summary',
16
+ 'stats.label': 'Stats',
17
+ 'tool-call.label': 'Calling',
18
+ 'tool-result.label': 'Result',
19
+ 'tool-error.label': 'Tool error',
20
+ 'nav-first.label': 'First message',
21
+ 'nav-previous.label': 'Previous message',
22
+ 'nav-next.label': 'Next message',
23
+ 'nav-last.label': 'Last message',
24
+ },
25
+ },
26
+ },
27
+ ] as const satisfies Resource[];
package/src/types.ts ADDED
@@ -0,0 +1,32 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import * as Schema from 'effect/Schema';
6
+
7
+ /**
8
+ * Which projection of the thread the reader sees. A view is a filter over blocks, applied by the
9
+ * renderer — the model always carries everything.
10
+ *
11
+ * - `summary`: only conversational text (prompts and replies).
12
+ * - `normal`: everything except reasoning.
13
+ * - `thinking`: everything, reasoning included.
14
+ * - `debug`: every block, raw fallbacks visible.
15
+ */
16
+ export const ChatView = Schema.Union([
17
+ Schema.Literal('normal').annotate({ title: 'Normal' }),
18
+ Schema.Literal('summary').annotate({ title: 'Summary' }),
19
+ Schema.Literal('thinking').annotate({ title: 'Thinking' }),
20
+ Schema.Literal('debug').annotate({ title: 'Debug' }),
21
+ ]);
22
+ export type ChatView = Schema.Schema.Type<typeof ChatView>;
23
+
24
+ /**
25
+ * What the thread reports outward. Deliberately the same shapes the assistant plugin's event bus
26
+ * speaks, so a host can pipe these straight through.
27
+ */
28
+ export type ChatThreadEvent =
29
+ /** A suggestion or select option was chosen — the text is ready to submit. */
30
+ | { type: 'submit'; text: string }
31
+ /** Soft-fork the thread from the given message (the prompt toolbar's rewind). */
32
+ | { type: 'rewind'; id: string };