@happyvertical/smrt-chat 0.53.0 → 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,303 +0,0 @@
1
- // @vitest-environment jsdom
2
- /**
3
- * The supervised run (#assistant-watch): goal and current step, pause and
4
- * continue, waiting for the person (a confirm, a staged proposal, a host
5
- * hold, a limit), done / failed / cancelled, and the tool filter that keeps
6
- * a turned-off capability from ever being offered.
7
- */
8
- import { createDataSurfaceRegistry } from '@happyvertical/smrt-ui/data-surface';
9
- import { describe, expect, it } from 'vitest';
10
- import { createAssistantDockController, } from '../create-assistant-dock-controller.svelte.js';
11
- function message(id, role, content) {
12
- return {
13
- id,
14
- threadId: 't1',
15
- role,
16
- content,
17
- createdAt: new Date(0).toISOString(),
18
- };
19
- }
20
- /** Suspends on `legs[0]`, then each resume on the next leg; ends with a reply. */
21
- function scriptedTransport(legs, finish = {}) {
22
- const sent = [];
23
- const resumed = [];
24
- let leg = 0;
25
- const suspendOrFinish = (input) => {
26
- const calls = legs[leg];
27
- leg += 1;
28
- if (calls) {
29
- for (const call of calls) {
30
- input.onEvent?.({
31
- type: 'step',
32
- step: {
33
- kind: 'tool_call',
34
- callId: call.id,
35
- tool: call.name,
36
- label: `Step ${call.id}`,
37
- location: 'client',
38
- },
39
- });
40
- }
41
- return { continuationId: `cont-${leg}`, calls };
42
- }
43
- return null;
44
- };
45
- const transport = {
46
- sent,
47
- resumed,
48
- listThreads: async () => [
49
- { id: 't1', title: 'T', isResolved: false, messageCount: 0 },
50
- ],
51
- createThread: async () => ({
52
- id: 't1',
53
- title: 'T',
54
- isResolved: false,
55
- messageCount: 0,
56
- }),
57
- loadMessages: async () => [],
58
- uploadAttachment: async () => {
59
- throw new Error('no');
60
- },
61
- async sendMessage(input) {
62
- sent.push(input);
63
- const suspension = suspendOrFinish(input);
64
- return {
65
- inProgress: false,
66
- userMessage: message('u1', 'user', input.content),
67
- ...(suspension ? { clientToolCalls: suspension } : {}),
68
- };
69
- },
70
- async resumeTurn(input) {
71
- resumed.push(input);
72
- const suspension = suspendOrFinish(input);
73
- if (suspension)
74
- return { inProgress: false, clientToolCalls: suspension };
75
- if (finish.fail)
76
- throw new Error('The assistant ran into a problem.');
77
- input.onEvent?.({
78
- type: 'done',
79
- stoppedReason: finish.stoppedReason ?? 'stop',
80
- });
81
- return {
82
- inProgress: false,
83
- assistantMessage: message('a1', 'assistant', 'Done.'),
84
- };
85
- },
86
- };
87
- return transport;
88
- }
89
- const NAV = {
90
- name: 'nav_site_section',
91
- description: 'Open a section',
92
- inputSchema: { type: 'object' },
93
- effect: 'read',
94
- proposal: true,
95
- };
96
- const STAGE = {
97
- name: 'smrt_ui_execute_form_control',
98
- description: 'Fill in a field',
99
- inputSchema: { type: 'object' },
100
- effect: 'write',
101
- proposal: true,
102
- };
103
- const WRITE = {
104
- name: 'articles_update',
105
- description: 'Update an article',
106
- inputSchema: { type: 'object' },
107
- effect: 'write',
108
- };
109
- function tools(list) {
110
- const executed = [];
111
- const source = {
112
- executed,
113
- list: () => list,
114
- async execute(name) {
115
- executed.push(name);
116
- return '{"ok":true}';
117
- },
118
- };
119
- return source;
120
- }
121
- async function open(transport, pageTools, extra = {}) {
122
- const controller = createAssistantDockController({
123
- transport,
124
- registry: createDataSurfaceRegistry(),
125
- pageTools,
126
- settleTimeoutMs: 0,
127
- ...extra,
128
- });
129
- await controller.loadThreads();
130
- await controller.openThread('t1');
131
- return controller;
132
- }
133
- async function until(check) {
134
- for (let i = 0; i < 100 && !check(); i += 1) {
135
- await new Promise((resolve) => setTimeout(resolve, 0));
136
- }
137
- }
138
- const call = (id, name) => ({
139
- id,
140
- name,
141
- args: {},
142
- effect: 'read',
143
- });
144
- describe('assistant run', () => {
145
- it('tracks the goal and steps across page changes and ends done', async () => {
146
- const transport = scriptedTransport([
147
- [call('1', NAV.name)],
148
- [call('2', NAV.name)],
149
- ]);
150
- const page = tools([NAV]);
151
- const seen = [];
152
- const controller = await open(transport, page, {
153
- onRun: (run) => seen.push(run),
154
- });
155
- const sending = controller.send('Show me upcoming meetings, then the council page');
156
- await until(() => (controller.run?.stepCount ?? 0) > 0);
157
- expect(controller.run).toMatchObject({
158
- goal: 'Show me upcoming meetings, then the council page',
159
- state: 'running',
160
- step: 'Step 1',
161
- });
162
- await sending;
163
- expect(controller.run).toMatchObject({
164
- state: 'done',
165
- stepCount: 2,
166
- pageTools: [NAV.name, NAV.name],
167
- waitingFor: null,
168
- stoppedReason: null,
169
- });
170
- expect(controller.run?.endedAt).not.toBeNull();
171
- expect(seen.some((run) => run?.state === 'running')).toBe(true);
172
- controller.dismissRun();
173
- expect(controller.run).toBeNull();
174
- controller.dispose();
175
- });
176
- it('pauses before the next step and continues', async () => {
177
- const transport = scriptedTransport([[call('1', NAV.name)]]);
178
- const page = tools([NAV]);
179
- const controller = await open(transport, page);
180
- // Pause as soon as the run starts: the first page step must wait.
181
- const sending = controller.send('go');
182
- controller.pauseRun();
183
- expect(controller.run?.state).toBe('paused');
184
- expect(controller.status.label).toBe('Paused');
185
- await new Promise((resolve) => setTimeout(resolve, 20));
186
- expect(page.executed).toEqual([]);
187
- controller.continueRun();
188
- await sending;
189
- expect(page.executed).toEqual([NAV.name]);
190
- expect(controller.run?.state).toBe('done');
191
- controller.dispose();
192
- });
193
- it('stops a run paused for too long', async () => {
194
- const transport = scriptedTransport([[call('1', NAV.name)]]);
195
- const page = tools([NAV]);
196
- const controller = await open(transport, page, { maxPauseMs: 10 });
197
- const sending = controller.send('go');
198
- controller.pauseRun();
199
- await sending;
200
- expect(page.executed).toEqual([]);
201
- expect(controller.run).toMatchObject({
202
- state: 'cancelled',
203
- stoppedReason: 'paused_too_long',
204
- });
205
- controller.dispose();
206
- });
207
- it('waits for the person on a confirm, and after staging a proposal', async () => {
208
- const confirmTransport = scriptedTransport([
209
- [{ ...call('1', WRITE.name), effect: 'write' }],
210
- ]);
211
- const controller = await open(confirmTransport, tools([WRITE]));
212
- const sending = controller.send('rename it');
213
- await until(() => controller.toolRequests.length > 0);
214
- expect(controller.run?.state).toBe('waiting');
215
- expect(controller.run?.waitingFor).toEqual({ kind: 'confirm', count: 1 });
216
- controller.declineToolRequest('1');
217
- await sending;
218
- expect(controller.run?.state).toBe('done');
219
- controller.dispose();
220
- const stageTransport = scriptedTransport([
221
- [{ ...call('1', STAGE.name), effect: 'write' }],
222
- ]);
223
- const staged = await open(stageTransport, tools([STAGE]));
224
- await staged.send('fill the event form');
225
- // Nothing was saved: the value is staged and waits for review.
226
- expect(staged.run?.state).toBe('waiting');
227
- expect(staged.run?.waitingFor).toEqual({ kind: 'review', count: 1 });
228
- staged.acknowledgeRun();
229
- expect(staged.run?.state).toBe('done');
230
- staged.dispose();
231
- });
232
- it('holds for a host choice until it is released', async () => {
233
- const transport = scriptedTransport([]);
234
- const controller = await open(transport, tools([]));
235
- await controller.send('make three pictures');
236
- expect(controller.run?.state).toBe('done');
237
- const release = controller.holdForUser({
238
- id: 'pictures',
239
- kind: 'choice',
240
- label: 'Pick a picture',
241
- });
242
- expect(controller.run?.waitingFor).toEqual({
243
- kind: 'choice',
244
- count: 1,
245
- label: 'Pick a picture',
246
- });
247
- expect(controller.run?.state).toBe('waiting');
248
- release();
249
- expect(controller.run?.state).toBe('done');
250
- controller.dispose();
251
- });
252
- it('asks to continue after the step or budget limit', async () => {
253
- const transport = scriptedTransport([[call('1', NAV.name)]], {
254
- stoppedReason: 'max_steps',
255
- });
256
- const controller = await open(transport, tools([NAV]));
257
- await controller.send('look everywhere');
258
- expect(controller.run).toMatchObject({
259
- state: 'waiting',
260
- waitingFor: { kind: 'continue', count: 1 },
261
- stoppedReason: 'max_steps',
262
- });
263
- controller.dispose();
264
- });
265
- it('fails red on an error and ends cancelled on Stop', async () => {
266
- const failing = scriptedTransport([[call('1', NAV.name)]], { fail: true });
267
- const controller = await open(failing, tools([NAV]));
268
- await expect(controller.send('go')).rejects.toThrow();
269
- expect(controller.run).toMatchObject({
270
- state: 'failed',
271
- stoppedReason: 'error',
272
- error: 'The assistant ran into a problem.',
273
- });
274
- controller.dispose();
275
- const transport = scriptedTransport([[call('1', NAV.name)]]);
276
- const stopped = await open(transport, tools([NAV]));
277
- const sending = stopped.send('go');
278
- stopped.pauseRun();
279
- stopped.cancel();
280
- await sending;
281
- expect(stopped.run).toMatchObject({
282
- state: 'cancelled',
283
- stoppedReason: 'user',
284
- });
285
- stopped.dispose();
286
- });
287
- it('never offers or runs a tool the host filter turns off', async () => {
288
- const transport = scriptedTransport([[call('1', NAV.name)]]);
289
- const page = tools([NAV, STAGE]);
290
- const controller = await open(transport, page, {
291
- clientToolFilter: (tool) => !tool.name.startsWith('nav_'),
292
- });
293
- await controller.send('open events');
294
- expect(transport.sent[0].clientTools?.map((t) => t.name)).toEqual([
295
- STAGE.name,
296
- ]);
297
- expect(page.executed).toEqual([]);
298
- expect(transport.resumed[0].results).toEqual([
299
- { id: '1', ok: false, error: 'not_available' },
300
- ]);
301
- controller.dispose();
302
- });
303
- });
@@ -1,146 +0,0 @@
1
- // @vitest-environment jsdom
2
- /**
3
- * A step that navigates must be followed by a step that sees the NEW page's
4
- * tools: the dock waits for the host's settle hook, tracked link-surface
5
- * navigations, and a quiet registry before it resumes the turn.
6
- */
7
- import { createDataSurfaceRegistry, registerLinkSurface, } from '@happyvertical/smrt-ui/data';
8
- import { describe, expect, it } from 'vitest';
9
- import { createAssistantDockController } from '../create-assistant-dock-controller.svelte.js';
10
- function message(id, role, content) {
11
- return {
12
- id,
13
- threadId: 't1',
14
- role,
15
- content,
16
- createdAt: new Date(0).toISOString(),
17
- };
18
- }
19
- function transportFor(calls) {
20
- const sent = [];
21
- const resumed = [];
22
- const transport = {
23
- sent,
24
- resumed,
25
- listThreads: async () => [
26
- { id: 't1', title: 'T', isResolved: false, messageCount: 0 },
27
- ],
28
- createThread: async () => ({
29
- id: 't1',
30
- title: 'T',
31
- isResolved: false,
32
- messageCount: 0,
33
- }),
34
- loadMessages: async () => [],
35
- uploadAttachment: async () => {
36
- throw new Error('no');
37
- },
38
- async sendMessage(input) {
39
- sent.push(input);
40
- return {
41
- inProgress: false,
42
- userMessage: message('u1', 'user', input.content),
43
- clientToolCalls: { continuationId: 'cont-1', calls },
44
- };
45
- },
46
- async resumeTurn(input) {
47
- resumed.push(input);
48
- return {
49
- inProgress: false,
50
- assistantMessage: message('a1', 'assistant', 'Here it is.'),
51
- };
52
- },
53
- };
54
- return transport;
55
- }
56
- const tool = (name) => ({
57
- name,
58
- description: name,
59
- inputSchema: { type: 'object' },
60
- effect: 'read',
61
- });
62
- describe('dock waits for the page to settle before the next step', () => {
63
- it('declares the new page tools after a navigation', async () => {
64
- const registry = createDataSurfaceRegistry();
65
- let current = [tool('nav_site_section')];
66
- const listeners = new Set();
67
- let finishNavigation;
68
- registerLinkSurface({
69
- registry,
70
- surfaceId: 'site-sections',
71
- label: 'Sections',
72
- description: 'Sections',
73
- links: [{ id: 'events', label: 'Events', href: '/events' }],
74
- navigate: () => new Promise((resolve) => {
75
- finishNavigation = () => {
76
- // The new page mounts and registers its own tools.
77
- current = [tool('nav_site_section'), tool('events_open')];
78
- for (const listener of listeners)
79
- listener({ type: 'change' });
80
- resolve();
81
- };
82
- }),
83
- });
84
- const pageTools = {
85
- list: () => current,
86
- subscribe(listener) {
87
- listeners.add(listener);
88
- return () => listeners.delete(listener);
89
- },
90
- async execute() {
91
- await registry.execute({
92
- version: 1,
93
- commandId: 'open-1',
94
- identity: { surfaceId: 'site-sections', kind: 'list' },
95
- expectedRevision: 1,
96
- controlId: 'open',
97
- payload: { target: 'Events' },
98
- });
99
- // The navigation finishes a little after the command answers.
100
- setTimeout(() => finishNavigation(), 30);
101
- return '{"ok":true}';
102
- },
103
- };
104
- const transport = transportFor([
105
- { id: 'c1', name: 'nav_site_section', args: {}, effect: 'read' },
106
- ]);
107
- let hostSettled = 0;
108
- const controller = createAssistantDockController({
109
- transport,
110
- registry,
111
- pageTools,
112
- settle: () => {
113
- hostSettled += 1;
114
- },
115
- });
116
- await controller.loadThreads();
117
- await controller.openThread('t1');
118
- await controller.send('show me events');
119
- expect(hostSettled).toBe(1);
120
- expect(transport.resumed[0].clientTools?.map((t) => t.name)).toEqual([
121
- 'nav_site_section',
122
- 'events_open',
123
- ]);
124
- controller.dispose();
125
- });
126
- it('is bounded: a host hook that never resolves only delays the step', async () => {
127
- const transport = transportFor([
128
- { id: 'c1', name: 'nav_site_section', args: {}, effect: 'read' },
129
- ]);
130
- const controller = createAssistantDockController({
131
- transport,
132
- registry: createDataSurfaceRegistry(),
133
- pageTools: {
134
- list: () => [tool('nav_site_section')],
135
- execute: async () => '{}',
136
- },
137
- settle: () => new Promise(() => { }),
138
- settleTimeoutMs: 50,
139
- });
140
- await controller.loadThreads();
141
- await controller.openThread('t1');
142
- await controller.send('go');
143
- expect(transport.resumed).toHaveLength(1);
144
- controller.dispose();
145
- });
146
- });