@happyvertical/smrt-chat 0.53.1 → 0.53.2

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 (37) hide show
  1. package/dist/index.js +1 -1
  2. package/dist/manifest.json +1 -1
  3. package/dist/smrt-knowledge.json +3 -3
  4. package/package.json +13 -13
  5. package/dist/__tests__/chat-test-db.d.ts +0 -8
  6. package/dist/__tests__/chat-test-db.d.ts.map +0 -1
  7. package/dist/__tests__/raw-collections.d.ts +0 -35
  8. package/dist/__tests__/raw-collections.d.ts.map +0 -1
  9. package/dist/svelte/components/agent/__tests__/ToolCallDisplay.test.js +0 -135
  10. package/dist/svelte/components/agent/message-blocks.test.js +0 -31
  11. package/dist/svelte/components/assistant/__tests__/AssistantComposer.test.js +0 -75
  12. package/dist/svelte/components/assistant/__tests__/AssistantDock.test.js +0 -515
  13. package/dist/svelte/components/assistant/__tests__/AssistantThreadList.test.js +0 -45
  14. package/dist/svelte/components/assistant/__tests__/action-status.test.js +0 -25
  15. package/dist/svelte/components/assistant/__tests__/assistant-composer-dictation.test.js +0 -197
  16. package/dist/svelte/components/assistant/__tests__/assistant-dock-action-outcome.test.js +0 -320
  17. package/dist/svelte/components/assistant/__tests__/assistant-dock-choices-pending.test.js +0 -224
  18. package/dist/svelte/components/assistant/__tests__/assistant-dock-choices-preview.test.js +0 -312
  19. package/dist/svelte/components/assistant/__tests__/assistant-dock-choices.test.js +0 -338
  20. package/dist/svelte/components/assistant/__tests__/assistant-dock-client-tools.test.js +0 -442
  21. package/dist/svelte/components/assistant/__tests__/assistant-dock-draft-settled.test.js +0 -448
  22. package/dist/svelte/components/assistant/__tests__/assistant-dock-host-actions.test.js +0 -185
  23. package/dist/svelte/components/assistant/__tests__/assistant-dock-run.test.js +0 -303
  24. package/dist/svelte/components/assistant/__tests__/assistant-dock-settle.test.js +0 -146
  25. package/dist/svelte/components/assistant/__tests__/assistant-transport.test.js +0 -253
  26. package/dist/svelte/components/assistant/__tests__/attachment-href.test.js +0 -31
  27. package/dist/svelte/components/assistant/__tests__/create-assistant-dock-controller.test.js +0 -2491
  28. package/dist/svelte/components/layout/__tests__/ChatLayout.test.js +0 -67
  29. package/dist/svelte/components/messages/__tests__/MessageInput.test.js +0 -38
  30. package/dist/svelte/components/messages/__tests__/MessageItem.test.js +0 -108
  31. package/dist/svelte/components/shared/__tests__/Avatar.test.js +0 -20
  32. package/dist/svelte/components/shared/__tests__/LinkPreview.test.js +0 -29
  33. package/dist/svelte/components/shared/__tests__/MentionAutocomplete.test.js +0 -66
  34. package/dist/svelte/components/shared/__tests__/ModelPicker.test.js +0 -36
  35. package/dist/svelte/components/shared/__tests__/ReactionPicker.test.js +0 -35
  36. package/dist/svelte/components/shared/__tests__/ReadReceipts.test.js +0 -28
  37. package/dist/svelte/components/shared/__tests__/UserPresence.test.js +0 -23
@@ -1,338 +0,0 @@
1
- // @vitest-environment jsdom
2
- /**
3
- * Choices: the assistant offers a few options (from a page-registered
4
- * source, never from the model), the person clicks one, and only that click
5
- * applies it.
6
- */
7
- import { createDataSurfaceRegistry } from '@happyvertical/smrt-ui/data-surface';
8
- import { render, screen, userEvent } from '@happyvertical/smrt-vitest/svelte';
9
- import { describe, expect, it, vi } from 'vitest';
10
- import AssistantDock from '../AssistantDock.svelte';
11
- import { choiceToolName, createAssistantChoiceSourceRegistry, normalizeChoiceOptions, safeChoiceImageUrl, } from '../assistant-choices.svelte.js';
12
- import { createAssistantDockController } from '../create-assistant-dock-controller.svelte.js';
13
- function message(id, role, content) {
14
- return {
15
- id,
16
- threadId: 't1',
17
- role,
18
- content,
19
- createdAt: new Date(0).toISOString(),
20
- };
21
- }
22
- function suspendingTransport(calls) {
23
- const sent = [];
24
- const resumed = [];
25
- const transport = {
26
- sent,
27
- resumed,
28
- async listThreads() {
29
- return [{ id: 't1', title: 'T', isResolved: false, messageCount: 0 }];
30
- },
31
- async createThread() {
32
- return { id: 't1', title: 'T', isResolved: false, messageCount: 0 };
33
- },
34
- async loadMessages() {
35
- return [];
36
- },
37
- async uploadAttachment() {
38
- throw new Error('no');
39
- },
40
- async sendMessage(input) {
41
- sent.push(input);
42
- return {
43
- inProgress: false,
44
- userMessage: message('u1', 'user', input.content),
45
- clientToolCalls: {
46
- continuationId: `cont-${sent.length}`,
47
- calls: calls.map((call) => ({
48
- ...call,
49
- id: sent.length > 1 ? `${call.id}-${sent.length}` : call.id,
50
- })),
51
- },
52
- };
53
- },
54
- async resumeTurn(input) {
55
- resumed.push(input);
56
- const reply = message('a1', 'assistant', 'Pick one of these.');
57
- return { inProgress: false, assistantMessage: reply };
58
- },
59
- };
60
- return transport;
61
- }
62
- function cropSource(apply = vi.fn(async () => 'Cropped to a square.')) {
63
- const offer = vi.fn(async () => ({
64
- title: 'Pick a crop',
65
- options: [
66
- { id: 'wide', label: 'Wide', imageUrl: '/p/1?w=16', value: { w: 16 } },
67
- { id: 'square', label: 'Square', imageUrl: '/p/1?w=1' },
68
- { id: 'evil', label: 'Remote', imageUrl: 'https://evil.test/x.png' },
69
- ],
70
- }));
71
- const source = {
72
- id: 'picture_crop',
73
- description: 'Offer crops of the selected picture.',
74
- inputSchema: {
75
- type: 'object',
76
- properties: { tighter: { type: 'boolean' } },
77
- },
78
- offer,
79
- apply,
80
- };
81
- return { source, offer, apply };
82
- }
83
- async function until(check) {
84
- for (let i = 0; i < 50 && !check(); i += 1) {
85
- await new Promise((resolve) => setTimeout(resolve, 0));
86
- }
87
- }
88
- describe('choice helpers', () => {
89
- it('only allows same-origin image paths', () => {
90
- expect(safeChoiceImageUrl('/sites/a/p.jpg?size=card')).toBe('/sites/a/p.jpg?size=card');
91
- for (const bad of [
92
- '//evil.test/x',
93
- 'https://x.test/a.jpg',
94
- '/\\evil',
95
- 'javascript:alert(1)',
96
- '/a b',
97
- 5,
98
- ]) {
99
- expect(safeChoiceImageUrl(bad)).toBeNull();
100
- }
101
- });
102
- it('keeps at most four unique, labelled options', () => {
103
- const options = normalizeChoiceOptions([
104
- { id: 'a', label: 'A' },
105
- { id: 'a', label: 'dupe' },
106
- { id: '', label: 'no id' },
107
- { id: 'b', label: '' },
108
- { id: 'c', label: 'C', imageUrl: 'https://x.test/c.jpg' },
109
- { id: 'd', label: 'D' },
110
- { id: 'e', label: 'E' },
111
- { id: 'f', label: 'F' },
112
- ]);
113
- expect(options.map((o) => o.id)).toEqual(['a', 'c', 'd', 'e']);
114
- expect(options[1].imageUrl).toBeUndefined();
115
- });
116
- it('rejects bad source ids and unregisters', () => {
117
- const registry = createAssistantChoiceSourceRegistry();
118
- expect(() => registry.register({ ...cropSource().source, id: 'Bad Id' })).toThrow();
119
- const off = registry.register(cropSource().source);
120
- expect(registry.list()).toHaveLength(1);
121
- off();
122
- expect(registry.list()).toHaveLength(0);
123
- });
124
- });
125
- describe('dock choices', () => {
126
- it("offers each source as a read tool, and applies only on the person's pick", async () => {
127
- const registry = createAssistantChoiceSourceRegistry();
128
- const { source, offer, apply } = cropSource();
129
- registry.register(source);
130
- const transport = suspendingTransport([
131
- {
132
- id: 'c1',
133
- name: choiceToolName('picture_crop'),
134
- args: { tighter: true },
135
- effect: 'read',
136
- },
137
- ]);
138
- const controller = createAssistantDockController({
139
- transport,
140
- registry: createDataSurfaceRegistry(),
141
- choiceSources: registry,
142
- });
143
- await controller.loadThreads();
144
- await controller.openThread('t1');
145
- await controller.send('crop this tighter');
146
- const declared = transport.sent[0].clientTools ?? [];
147
- expect(declared.map((tool) => tool.name)).toContain('assistant_offer_picture_crop');
148
- expect(declared.find((tool) => tool.name === 'assistant_offer_picture_crop')
149
- ?.effect).toBe('read');
150
- expect(offer).toHaveBeenCalledWith({ tighter: true }, expect.anything());
151
- // The model learns the options by id and label only; nothing applied yet.
152
- const result = transport.resumed[0].results[0];
153
- expect(result.ok).toBe(true);
154
- expect(JSON.parse(result.result ?? '{}')).toEqual({
155
- offered: true,
156
- waitingForUser: true,
157
- options: [
158
- { id: 'wide', label: 'Wide' },
159
- { id: 'square', label: 'Square' },
160
- { id: 'evil', label: 'Remote' },
161
- ],
162
- });
163
- expect(apply).not.toHaveBeenCalled();
164
- expect(controller.choices[0].status).toBe('waiting');
165
- expect(controller.choices[0].options[2].imageUrl).toBeUndefined();
166
- expect(controller.status.label).toBe('Pick one of the options');
167
- // A supervised run waits for the person's pick.
168
- expect(controller.run?.state).toBe('waiting');
169
- expect(controller.run?.waitingFor).toMatchObject({
170
- kind: 'choice',
171
- label: 'Pick a crop',
172
- });
173
- await controller.chooseOption('c1', 'square');
174
- expect(apply).toHaveBeenCalledWith(expect.objectContaining({ id: 'square' }));
175
- expect(controller.choices[0]).toMatchObject({
176
- status: 'applied',
177
- chosenOptionId: 'square',
178
- outcome: 'Cropped to a square.',
179
- });
180
- expect(controller.run?.waitingFor?.kind).not.toBe('choice');
181
- // A second pick is ignored once applied.
182
- await controller.chooseOption('c1', 'wide');
183
- expect(apply).toHaveBeenCalledTimes(1);
184
- });
185
- it('reports a source error to the model and shows nothing', async () => {
186
- const registry = createAssistantChoiceSourceRegistry();
187
- registry.register({
188
- id: 'picture_find',
189
- description: 'Find pictures.',
190
- offer: () => {
191
- throw new Error('No pictures match "arena".');
192
- },
193
- apply: () => undefined,
194
- });
195
- const transport = suspendingTransport([
196
- {
197
- id: 'c1',
198
- name: 'assistant_offer_picture_find',
199
- args: {},
200
- effect: 'read',
201
- },
202
- ]);
203
- const controller = createAssistantDockController({
204
- transport,
205
- registry: createDataSurfaceRegistry(),
206
- choiceSources: registry,
207
- });
208
- await controller.loadThreads();
209
- await controller.openThread('t1');
210
- await controller.send('find the arena');
211
- expect(transport.resumed[0].results[0]).toEqual({
212
- id: 'c1',
213
- ok: false,
214
- error: 'No pictures match "arena".',
215
- });
216
- expect(controller.choices).toHaveLength(0);
217
- });
218
- it('marks a failed apply so the person can pick again, and fails when the page is gone', async () => {
219
- const registry = createAssistantChoiceSourceRegistry();
220
- const apply = vi
221
- .fn()
222
- .mockRejectedValueOnce(new Error('The picture is not in the story'))
223
- .mockResolvedValueOnce(undefined);
224
- const { source } = cropSource(apply);
225
- const off = registry.register(source);
226
- const transport = suspendingTransport([
227
- {
228
- id: 'c1',
229
- name: 'assistant_offer_picture_crop',
230
- args: {},
231
- effect: 'read',
232
- },
233
- ]);
234
- const controller = createAssistantDockController({
235
- transport,
236
- registry: createDataSurfaceRegistry(),
237
- choiceSources: registry,
238
- });
239
- await controller.loadThreads();
240
- await controller.openThread('t1');
241
- await controller.send('crop');
242
- await controller.chooseOption('c1', 'wide');
243
- expect(controller.choices[0]).toMatchObject({
244
- status: 'failed',
245
- error: 'The picture is not in the story',
246
- });
247
- await controller.chooseOption('c1', 'square');
248
- expect(controller.choices[0].status).toBe('applied');
249
- // A newer request replaces an offer still waiting, not an applied one.
250
- await controller.send('crop again');
251
- expect(controller.choices.map((s) => s.status)).toEqual([
252
- 'applied',
253
- 'waiting',
254
- ]);
255
- await controller.send('and again');
256
- expect(controller.choices.map((s) => s.status)).toEqual([
257
- 'applied',
258
- 'dismissed',
259
- 'waiting',
260
- ]);
261
- // The page went away: a pick fails plainly instead of applying.
262
- off();
263
- await controller.chooseOption('c1-3', 'wide');
264
- expect(controller.choices[2]).toMatchObject({
265
- status: 'failed',
266
- error: 'This is no longer on the page.',
267
- });
268
- expect(apply).toHaveBeenCalledTimes(2);
269
- });
270
- it('renders picture cards and applies the clicked one', async () => {
271
- const registry = createAssistantChoiceSourceRegistry();
272
- const { source, apply } = cropSource();
273
- registry.register(source);
274
- const transport = suspendingTransport([
275
- {
276
- id: 'c1',
277
- name: 'assistant_offer_picture_crop',
278
- args: {},
279
- effect: 'read',
280
- },
281
- ]);
282
- let controller;
283
- render(AssistantDock, {
284
- props: {
285
- transport,
286
- registry: createDataSurfaceRegistry(),
287
- choiceSources: registry,
288
- oncontroller: (c) => {
289
- controller = c;
290
- },
291
- },
292
- });
293
- await until(() => Boolean(controller));
294
- await controller?.openThread('t1');
295
- await controller?.send('crop this');
296
- const group = await screen.findByRole('group', { name: 'Pick a crop' });
297
- const images = group.querySelectorAll('img');
298
- expect(images).toHaveLength(2);
299
- expect(images[0].getAttribute('src')).toBe('/p/1?w=16');
300
- expect(screen.getByText('Pick one to use it.')).toBeTruthy();
301
- await userEvent.click(screen.getByRole('button', { name: /Square/ }));
302
- await until(() => apply.mock.calls.length > 0);
303
- expect(await screen.findByText('Cropped to a square.')).toBeTruthy();
304
- expect(screen
305
- .getByRole('button', { name: /Square/ })
306
- .getAttribute('aria-pressed')).toBe('true');
307
- });
308
- it('dismisses with None of these', async () => {
309
- const registry = createAssistantChoiceSourceRegistry();
310
- const { source, apply } = cropSource();
311
- registry.register(source);
312
- const transport = suspendingTransport([
313
- {
314
- id: 'c1',
315
- name: 'assistant_offer_picture_crop',
316
- args: {},
317
- effect: 'read',
318
- },
319
- ]);
320
- let controller;
321
- render(AssistantDock, {
322
- props: {
323
- transport,
324
- registry: createDataSurfaceRegistry(),
325
- choiceSources: registry,
326
- oncontroller: (c) => {
327
- controller = c;
328
- },
329
- },
330
- });
331
- await until(() => Boolean(controller));
332
- await controller?.openThread('t1');
333
- await controller?.send('crop this');
334
- await userEvent.click(await screen.findByRole('button', { name: 'None of these' }));
335
- expect(screen.queryByRole('group', { name: 'Pick a crop' })).toBeNull();
336
- expect(apply).not.toHaveBeenCalled();
337
- });
338
- });