@happyvertical/smrt-chat 0.51.4 → 0.51.6

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 (47) hide show
  1. package/AGENTS.md +19 -0
  2. package/dist/index.js +1 -1
  3. package/dist/manifest.json +1 -1
  4. package/dist/smrt-knowledge.json +5 -5
  5. package/dist/svelte/components/agent/ToolCallDisplay.svelte +103 -7
  6. package/dist/svelte/components/agent/ToolCallDisplay.svelte.d.ts +18 -0
  7. package/dist/svelte/components/agent/ToolCallDisplay.svelte.d.ts.map +1 -1
  8. package/dist/svelte/components/agent/__tests__/ToolCallDisplay.test.js +100 -9
  9. package/dist/svelte/components/assistant/AssistantComposer.svelte +354 -0
  10. package/dist/svelte/components/assistant/AssistantComposer.svelte.d.ts +20 -0
  11. package/dist/svelte/components/assistant/AssistantComposer.svelte.d.ts.map +1 -0
  12. package/dist/svelte/components/assistant/AssistantDock.svelte +534 -0
  13. package/dist/svelte/components/assistant/AssistantDock.svelte.d.ts +30 -0
  14. package/dist/svelte/components/assistant/AssistantDock.svelte.d.ts.map +1 -0
  15. package/dist/svelte/components/assistant/AssistantThreadList.svelte +136 -0
  16. package/dist/svelte/components/assistant/AssistantThreadList.svelte.d.ts +15 -0
  17. package/dist/svelte/components/assistant/AssistantThreadList.svelte.d.ts.map +1 -0
  18. package/dist/svelte/components/assistant/__tests__/AssistantComposer.test.js +75 -0
  19. package/dist/svelte/components/assistant/__tests__/AssistantDock.test.js +395 -0
  20. package/dist/svelte/components/assistant/__tests__/AssistantThreadList.test.js +45 -0
  21. package/dist/svelte/components/assistant/__tests__/action-status.test.js +25 -0
  22. package/dist/svelte/components/assistant/__tests__/assistant-transport.test.js +253 -0
  23. package/dist/svelte/components/assistant/__tests__/attachment-href.test.js +31 -0
  24. package/dist/svelte/components/assistant/__tests__/create-assistant-dock-controller.test.js +2492 -0
  25. package/dist/svelte/components/assistant/action-status.d.ts +14 -0
  26. package/dist/svelte/components/assistant/action-status.d.ts.map +1 -0
  27. package/dist/svelte/components/assistant/action-status.js +7 -0
  28. package/dist/svelte/components/assistant/assistant-transport.d.ts +239 -0
  29. package/dist/svelte/components/assistant/assistant-transport.d.ts.map +1 -0
  30. package/dist/svelte/components/assistant/assistant-transport.js +306 -0
  31. package/dist/svelte/components/assistant/attachment-href.d.ts +23 -0
  32. package/dist/svelte/components/assistant/attachment-href.d.ts.map +1 -0
  33. package/dist/svelte/components/assistant/attachment-href.js +36 -0
  34. package/dist/svelte/components/assistant/create-assistant-dock-controller.svelte.d.ts +172 -0
  35. package/dist/svelte/components/assistant/create-assistant-dock-controller.svelte.d.ts.map +1 -0
  36. package/dist/svelte/components/assistant/create-assistant-dock-controller.svelte.js +0 -0
  37. package/dist/svelte/components/shared/ModelPicker.svelte +84 -0
  38. package/dist/svelte/components/shared/ModelPicker.svelte.d.ts +26 -0
  39. package/dist/svelte/components/shared/ModelPicker.svelte.d.ts.map +1 -0
  40. package/dist/svelte/components/shared/__tests__/ModelPicker.test.js +36 -0
  41. package/dist/svelte/i18n.d.ts +21 -0
  42. package/dist/svelte/i18n.d.ts.map +1 -1
  43. package/dist/svelte/i18n.js +25 -0
  44. package/dist/svelte/index.d.ts +7 -0
  45. package/dist/svelte/index.d.ts.map +1 -1
  46. package/dist/svelte/index.js +12 -0
  47. package/package.json +11 -11
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Unit coverage for toolCallStatusForAction (#2904 review, Copilot PR #2919
3
+ * jAwua/jAwtb) — both AssistantDock.svelte and the demo route previously let
4
+ * `status === 'previewing'` fall through to 'success', showing "Completed"
5
+ * while a preview request was still in flight or about to fail.
6
+ */
7
+ import { describe, expect, it } from 'vitest';
8
+ import { toolCallStatusForAction } from '../action-status.js';
9
+ describe('toolCallStatusForAction', () => {
10
+ it('maps previewing to running', () => {
11
+ expect(toolCallStatusForAction('previewing')).toBe('running');
12
+ });
13
+ it('maps applying to running', () => {
14
+ expect(toolCallStatusForAction('applying')).toBe('running');
15
+ });
16
+ it('maps failed to error', () => {
17
+ expect(toolCallStatusForAction('failed')).toBe('error');
18
+ });
19
+ it('maps previewed to success', () => {
20
+ expect(toolCallStatusForAction('previewed')).toBe('success');
21
+ });
22
+ it('maps applied to success', () => {
23
+ expect(toolCallStatusForAction('applied')).toBe('success');
24
+ });
25
+ });
@@ -0,0 +1,253 @@
1
+ // @vitest-environment jsdom
2
+ /**
3
+ * Coverage for assistant-transport.ts's implementations directly (not
4
+ * through the controller) — #2904 review, Copilot PR #2919.
5
+ */
6
+ import { describe, expect, it, vi } from 'vitest';
7
+ import { createInMemoryAssistantTransport, createSmrtAssistantTransport, normalizeAssistantMessage, normalizeAssistantThreadSummary, sortAssistantMessagesChronologically, } from '../assistant-transport.js';
8
+ describe('createInMemoryAssistantTransport', () => {
9
+ // jAwu8: simulateInProgressOnce alone left the turn pending forever;
10
+ // resolveInProgressAfterLoads makes the resolution point configurable.
11
+ it('resolveInProgressAfterLoads appends the assistant reply on the Nth loadMessages() call', async () => {
12
+ const transport = createInMemoryAssistantTransport({
13
+ simulateInProgressOnce: true,
14
+ resolveInProgressAfterLoads: 2,
15
+ respond: (threadId, userMessage) => ({
16
+ id: 'reply-1',
17
+ threadId,
18
+ content: `reply to: ${userMessage.content}`,
19
+ role: 'assistant',
20
+ createdAt: new Date(),
21
+ }),
22
+ });
23
+ const thread = await transport.createThread('t1');
24
+ const result = await transport.sendMessage({
25
+ threadId: thread.id,
26
+ content: 'hello',
27
+ clientRequestId: 'req-1',
28
+ });
29
+ expect(result.inProgress).toBe(true);
30
+ // First load: still 1 load remaining, no reply yet.
31
+ let messages = await transport.loadMessages(thread.id);
32
+ expect(messages.some((m) => m.role === 'assistant')).toBe(false);
33
+ // Second load: resolves.
34
+ messages = await transport.loadMessages(thread.id);
35
+ const reply = messages.find((m) => m.role === 'assistant');
36
+ expect(reply?.content).toBe('reply to: hello');
37
+ });
38
+ it('without resolveInProgressAfterLoads, the turn never resolves on its own', async () => {
39
+ const transport = createInMemoryAssistantTransport({
40
+ simulateInProgressOnce: true,
41
+ });
42
+ const thread = await transport.createThread('t1');
43
+ await transport.sendMessage({
44
+ threadId: thread.id,
45
+ content: 'hello',
46
+ clientRequestId: 'req-1',
47
+ });
48
+ for (let i = 0; i < 5; i += 1) {
49
+ const messages = await transport.loadMessages(thread.id);
50
+ expect(messages.some((m) => m.role === 'assistant')).toBe(false);
51
+ }
52
+ });
53
+ });
54
+ // Copilot PR #2919 jAwvV: the normalizers tolerate a raw generated-model
55
+ // snake_case row (created_at/thread_id), a JSON-string `attachments` column
56
+ // with `filename` fields, and newest-first pagination — none of which the
57
+ // controller's camelCase/chronological/`{name}` expectations understand
58
+ // directly.
59
+ describe('normalizeAssistantThreadSummary', () => {
60
+ it('accepts the documented camelCase wire shape unchanged', () => {
61
+ const result = normalizeAssistantThreadSummary({
62
+ id: 't1',
63
+ title: 'Order question',
64
+ isResolved: true,
65
+ messageCount: 3,
66
+ lastMessageAt: '2026-01-01T00:00:00.000Z',
67
+ });
68
+ expect(result).toEqual({
69
+ id: 't1',
70
+ title: 'Order question',
71
+ isResolved: true,
72
+ messageCount: 3,
73
+ lastMessageAt: '2026-01-01T00:00:00.000Z',
74
+ });
75
+ });
76
+ it('falls back to snake_case fields from a raw generated-model row', () => {
77
+ const result = normalizeAssistantThreadSummary({
78
+ id: 't1',
79
+ title: 'Order question',
80
+ is_resolved: true,
81
+ message_count: 5,
82
+ last_message_at: '2026-01-01T00:00:00.000Z',
83
+ });
84
+ expect(result.isResolved).toBe(true);
85
+ expect(result.messageCount).toBe(5);
86
+ expect(result.lastMessageAt).toBe('2026-01-01T00:00:00.000Z');
87
+ });
88
+ });
89
+ describe('normalizeAssistantMessage', () => {
90
+ it('accepts the documented camelCase wire shape with an attachments array', () => {
91
+ const result = normalizeAssistantMessage({
92
+ id: 'm1',
93
+ threadId: 't1',
94
+ content: 'hello',
95
+ role: 'user',
96
+ createdAt: '2026-01-01T00:00:00.000Z',
97
+ attachments: [{ name: 'report.pdf', url: 'https://example.com/r.pdf' }],
98
+ });
99
+ expect(result.threadId).toBe('t1');
100
+ expect(result.createdAt).toBe('2026-01-01T00:00:00.000Z');
101
+ expect(result.attachments).toEqual([
102
+ { id: 'att-0', name: 'report.pdf', url: 'https://example.com/r.pdf' },
103
+ ]);
104
+ });
105
+ it('falls back to snake_case created_at/thread_id from a raw generated-model row', () => {
106
+ const result = normalizeAssistantMessage({
107
+ id: 'm1',
108
+ thread_id: 't1',
109
+ content: 'hello',
110
+ role: 'user',
111
+ created_at: '2026-01-01T00:00:00.000Z',
112
+ });
113
+ expect(result.threadId).toBe('t1');
114
+ expect(result.createdAt).toBe('2026-01-01T00:00:00.000Z');
115
+ });
116
+ it('parses a JSON-STRING attachments column with filename fields', () => {
117
+ const result = normalizeAssistantMessage({
118
+ id: 'm1',
119
+ threadId: 't1',
120
+ content: 'hello',
121
+ role: 'user',
122
+ createdAt: '2026-01-01T00:00:00.000Z',
123
+ attachments: JSON.stringify([{ filename: 'contract.docx', size: 2048 }]),
124
+ });
125
+ expect(result.attachments).toEqual([
126
+ { id: 'att-0', name: 'contract.docx', size: 2048 },
127
+ ]);
128
+ });
129
+ it('returns undefined attachments for a malformed JSON string', () => {
130
+ const result = normalizeAssistantMessage({
131
+ id: 'm1',
132
+ threadId: 't1',
133
+ content: 'hello',
134
+ role: 'user',
135
+ createdAt: '2026-01-01T00:00:00.000Z',
136
+ attachments: 'not json',
137
+ });
138
+ expect(result.attachments).toBeUndefined();
139
+ });
140
+ });
141
+ describe('sortAssistantMessagesChronologically', () => {
142
+ it('reorders a newest-first (generated-model pagination) list to oldest-first', () => {
143
+ const newestFirst = [
144
+ {
145
+ id: 'm3',
146
+ threadId: 't1',
147
+ content: 'third',
148
+ role: 'user',
149
+ createdAt: '2026-01-03T00:00:00.000Z',
150
+ },
151
+ {
152
+ id: 'm1',
153
+ threadId: 't1',
154
+ content: 'first',
155
+ role: 'user',
156
+ createdAt: '2026-01-01T00:00:00.000Z',
157
+ },
158
+ {
159
+ id: 'm2',
160
+ threadId: 't1',
161
+ content: 'second',
162
+ role: 'assistant',
163
+ createdAt: '2026-01-02T00:00:00.000Z',
164
+ },
165
+ ];
166
+ const chronological = sortAssistantMessagesChronologically(newestFirst);
167
+ expect(chronological.map((m) => m.id)).toEqual(['m1', 'm2', 'm3']);
168
+ });
169
+ });
170
+ // Copilot PR #2919 jAwqo/jAwrQ: createSmrtAssistantTransport must call the
171
+ // host-supplied readEndpoint, never a raw baseUrl-derived generated list
172
+ // route.
173
+ describe('createSmrtAssistantTransport reads', () => {
174
+ function fakeFetch(responsesByUrl) {
175
+ const calledUrls = [];
176
+ const fetchImpl = vi.fn(async (input) => {
177
+ const url = String(input);
178
+ calledUrls.push(url);
179
+ const body = responsesByUrl[url];
180
+ if (body === undefined) {
181
+ return new Response('not found', { status: 404 });
182
+ }
183
+ return new Response(JSON.stringify(body), {
184
+ status: 200,
185
+ headers: { 'content-type': 'application/json' },
186
+ });
187
+ });
188
+ return { fetchImpl: fetchImpl, calledUrls };
189
+ }
190
+ it('listThreads() calls GET {readEndpoint}/threads and normalizes the rows', async () => {
191
+ const { fetchImpl, calledUrls } = fakeFetch({
192
+ 'https://api.example.com/assistant/threads': {
193
+ items: [
194
+ {
195
+ id: 't1',
196
+ title: 'Order question',
197
+ is_resolved: false,
198
+ message_count: 2,
199
+ },
200
+ ],
201
+ },
202
+ });
203
+ const transport = createSmrtAssistantTransport({
204
+ readEndpoint: 'https://api.example.com/assistant',
205
+ token: 'test-token',
206
+ fetchImpl,
207
+ });
208
+ const threads = await transport.listThreads();
209
+ expect(calledUrls).toEqual(['https://api.example.com/assistant/threads']);
210
+ expect(threads).toEqual([
211
+ {
212
+ id: 't1',
213
+ title: 'Order question',
214
+ isResolved: false,
215
+ messageCount: 2,
216
+ lastMessageAt: null,
217
+ },
218
+ ]);
219
+ });
220
+ it('loadMessages() calls GET {readEndpoint}/threads/{id}/messages, never a raw list route, and sorts chronologically', async () => {
221
+ const { fetchImpl, calledUrls } = fakeFetch({
222
+ 'https://api.example.com/assistant/threads/t1/messages': {
223
+ items: [
224
+ {
225
+ id: 'm2',
226
+ thread_id: 't1',
227
+ content: 'second',
228
+ role: 'assistant',
229
+ created_at: '2026-01-02T00:00:00.000Z',
230
+ },
231
+ {
232
+ id: 'm1',
233
+ thread_id: 't1',
234
+ content: 'first',
235
+ role: 'user',
236
+ created_at: '2026-01-01T00:00:00.000Z',
237
+ },
238
+ ],
239
+ },
240
+ });
241
+ const transport = createSmrtAssistantTransport({
242
+ readEndpoint: 'https://api.example.com/assistant',
243
+ token: 'test-token',
244
+ fetchImpl,
245
+ });
246
+ const messages = await transport.loadMessages('t1');
247
+ expect(calledUrls).toEqual([
248
+ 'https://api.example.com/assistant/threads/t1/messages',
249
+ ]);
250
+ expect(messages.map((m) => m.id)).toEqual(['m1', 'm2']);
251
+ expect(messages[0]?.threadId).toBe('t1');
252
+ });
253
+ });
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Unit coverage for safeAttachmentHref (#2904 review, cycle-3 second final
3
+ * F1 addendum) — attachment.url is transport-supplied data bound directly
4
+ * to an <a href> in AssistantDock.svelte and the demo route; only
5
+ * http(s) URLs may ever become a clickable link.
6
+ */
7
+ import { describe, expect, it } from 'vitest';
8
+ import { safeAttachmentHref } from '../attachment-href.js';
9
+ describe('safeAttachmentHref', () => {
10
+ it('returns undefined for a javascript: URL', () => {
11
+ expect(safeAttachmentHref('javascript:alert(1)')).toBeUndefined();
12
+ });
13
+ it('returns undefined for a data: URL', () => {
14
+ expect(safeAttachmentHref('data:text/html,<script>alert(1)</script>')).toBeUndefined();
15
+ });
16
+ it('resolves a relative path against the current origin as http(s)', () => {
17
+ expect(safeAttachmentHref('/files/report.pdf')).toBe('/files/report.pdf');
18
+ });
19
+ it('returns undefined when url is undefined', () => {
20
+ expect(safeAttachmentHref(undefined)).toBeUndefined();
21
+ });
22
+ it('returns the URL unchanged for an absolute http URL', () => {
23
+ expect(safeAttachmentHref('http://example.com/report.pdf')).toBe('http://example.com/report.pdf');
24
+ });
25
+ it('returns the URL unchanged for an absolute https URL', () => {
26
+ expect(safeAttachmentHref('https://example.com/report.pdf')).toBe('https://example.com/report.pdf');
27
+ });
28
+ it('returns undefined for a malformed URL', () => {
29
+ expect(safeAttachmentHref('http://[')).toBeUndefined();
30
+ });
31
+ });