@happyvertical/smrt-chat 0.51.5 → 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,354 @@
1
+ <script lang="ts">
2
+ /**
3
+ * AssistantComposer - text + attachment composer for AssistantDock (#2904).
4
+ *
5
+ * Enter-to-send / Shift+Enter-for-newline matches the package convention in
6
+ * `../messages/MessageInput.svelte` (same `Textarea` primitive, same
7
+ * `handleKeydown` shape). Attachments use a compact attach button + a small
8
+ * chip row instead of the full `../shared/FileUpload.svelte` dropzone/preview
9
+ * flow, which is sized for a dedicated upload panel, not a one-line composer
10
+ * (#2904 review fix).
11
+ */
12
+ import { Textarea } from '@happyvertical/smrt-ui/forms';
13
+ import { useI18n } from '@happyvertical/smrt-ui/i18n';
14
+ import { Button } from '@happyvertical/smrt-ui/ui';
15
+ import { M } from '../../i18n.js';
16
+ import type { AssistantAttachmentRef } from './assistant-transport.js';
17
+
18
+ const { t } = useI18n();
19
+
20
+ export interface Props {
21
+ /** Called with the trimmed message text and any staged attachments when
22
+ * the user sends — via Enter or the Send button. May return a Promise
23
+ * (or reject/throw): the draft text and staged attachments are kept until
24
+ * it settles, cleared only on success (#2904 review finding 4) — a
25
+ * rejection restores them and shows an inline error. */
26
+ onsend: (
27
+ content: string,
28
+ attachments: AssistantAttachmentRef[],
29
+ ) => void | Promise<void>;
30
+ /** Called with the picked/dropped files; resolves to the uploaded
31
+ * attachment refs to stage as removable chips above the input. */
32
+ onupload: (files: FileList) => Promise<AssistantAttachmentRef[]>;
33
+ /** Disables the composer (e.g. no active thread yet). */
34
+ disabled?: boolean;
35
+ /** Placeholder text for the empty textarea. */
36
+ placeholder?: string;
37
+ }
38
+
39
+ const {
40
+ onsend,
41
+ onupload,
42
+ disabled = false,
43
+ placeholder = 'Ask the assistant…',
44
+ }: Props = $props();
45
+
46
+ let content = $state('');
47
+ let stagedAttachments = $state<AssistantAttachmentRef[]>([]);
48
+ let uploading = $state(false);
49
+ let sending = $state(false);
50
+ let sendError = $state<string | null>(null);
51
+ // Cycle-2 third final: a rejecting onupload (e.g. the default
52
+ // createSmrtAssistantTransport's requireWrite('uploadAttachment') error when
53
+ // no writeEndpoint is configured) previously had no catch anywhere in
54
+ // handleFileChange/handleDrop — the chip row silently never updated and the
55
+ // rejection escaped as an unhandled promise rejection from the DOM event
56
+ // handler. Distinct slot from `sendError` since the two failures are
57
+ // unrelated and can occur independently (e.g. staging fails while a
58
+ // previous message is still sending).
59
+ let uploadError = $state<string | null>(null);
60
+ let fileInputEl: HTMLInputElement | undefined;
61
+ // Captured from the textarea's input event so auto-resize works without
62
+ // binding to the Textarea primitive's inner DOM node.
63
+ let textareaEl: HTMLTextAreaElement | undefined;
64
+
65
+ async function handleFileChange(event: Event) {
66
+ const input = event.currentTarget as HTMLInputElement;
67
+ const files = input.files;
68
+ if (!files || files.length === 0) return;
69
+ uploading = true;
70
+ uploadError = null;
71
+ try {
72
+ const uploaded = await onupload(files);
73
+ stagedAttachments = [...stagedAttachments, ...uploaded];
74
+ } catch (error) {
75
+ // Already-staged chips are left untouched — only this batch failed.
76
+ uploadError =
77
+ error instanceof Error
78
+ ? error.message
79
+ : t(M['chat.assistant_composer.upload_failed']);
80
+ } finally {
81
+ uploading = false;
82
+ input.value = '';
83
+ }
84
+ }
85
+
86
+ async function handleDrop(event: DragEvent) {
87
+ event.preventDefault();
88
+ if (disabled || uploading) return;
89
+ const files = event.dataTransfer?.files;
90
+ if (!files || files.length === 0) return;
91
+ uploading = true;
92
+ uploadError = null;
93
+ try {
94
+ const uploaded = await onupload(files);
95
+ stagedAttachments = [...stagedAttachments, ...uploaded];
96
+ } catch (error) {
97
+ // Already-staged chips are left untouched — only this batch failed.
98
+ uploadError =
99
+ error instanceof Error
100
+ ? error.message
101
+ : t(M['chat.assistant_composer.upload_failed']);
102
+ } finally {
103
+ uploading = false;
104
+ }
105
+ }
106
+
107
+ async function handleSend() {
108
+ const trimmed = content.trim();
109
+ if (!trimmed || disabled || sending) return;
110
+ sendError = null;
111
+ sending = true;
112
+ // #2904 review finding 4: keep the draft text/attachments until onsend
113
+ // settles — clearing them synchronously (the previous behavior) lost the
114
+ // user's message forever on a transport failure, with no visible error.
115
+ try {
116
+ await onsend(trimmed, stagedAttachments);
117
+ content = '';
118
+ stagedAttachments = [];
119
+ if (textareaEl) {
120
+ textareaEl.style.height = 'auto';
121
+ }
122
+ } catch (error) {
123
+ sendError =
124
+ error instanceof Error
125
+ ? error.message
126
+ : t(M['chat.assistant_composer.send_failed']);
127
+ } finally {
128
+ sending = false;
129
+ }
130
+ }
131
+
132
+ function handleKeydown(event: KeyboardEvent) {
133
+ // Matches ../messages/MessageInput.svelte's handleKeydown convention:
134
+ // plain Enter sends, Shift+Enter inserts a newline. `isComposing` guards
135
+ // against an IME's confirmation Enter being treated as a send.
136
+ if (event.key === 'Enter' && !event.shiftKey && !event.isComposing) {
137
+ event.preventDefault();
138
+ handleSend();
139
+ }
140
+ }
141
+
142
+ function handleInput(event: Event) {
143
+ const el = event.currentTarget as HTMLTextAreaElement;
144
+ textareaEl = el;
145
+ el.style.height = 'auto';
146
+ el.style.height = `${Math.min(el.scrollHeight, 120)}px`;
147
+ }
148
+
149
+ function removeAttachment(id: string) {
150
+ stagedAttachments = stagedAttachments.filter((a) => a.id !== id);
151
+ }
152
+ </script>
153
+
154
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
155
+ <div class="assistant-composer" ondrop={handleDrop} ondragover={(e) => e.preventDefault()}>
156
+ {#if sendError}
157
+ <p class="assistant-composer-error" role="alert">
158
+ {t(M['chat.assistant_composer.send_error'], { message: sendError })}
159
+ </p>
160
+ {/if}
161
+ {#if uploadError}
162
+ <p class="assistant-composer-error" role="alert">
163
+ {t(M['chat.assistant_composer.upload_error'], { message: uploadError })}
164
+ </p>
165
+ {/if}
166
+ {#if stagedAttachments.length > 0}
167
+ <ul class="assistant-composer-attachments">
168
+ {#each stagedAttachments as attachment (attachment.id)}
169
+ <li class="assistant-composer-chip">
170
+ <span class="assistant-composer-chip-name">{attachment.name}</span>
171
+ <Button
172
+ type="button"
173
+ variant="ghost"
174
+ size="sm"
175
+ class="assistant-composer-chip-remove"
176
+ onclick={() => removeAttachment(attachment.id)}
177
+ aria-label={t(M['chat.assistant_composer.remove_attachment'], {
178
+ name: attachment.name,
179
+ })}
180
+ >
181
+ ×
182
+ </Button>
183
+ </li>
184
+ {/each}
185
+ </ul>
186
+ {/if}
187
+ <div class="assistant-composer-row">
188
+ <!-- raw-primitive-allow: compact icon-only attach control backed by a hidden native file input; opens the OS picker, matching MessageInput's icon-button send control pattern -->
189
+ <Button
190
+ type="button"
191
+ variant="secondary"
192
+ size="sm"
193
+ class="assistant-composer-attach"
194
+ onclick={() => fileInputEl?.click()}
195
+ disabled={disabled || uploading}
196
+ aria-label={t(M['chat.assistant_composer.attach_files'])}
197
+ title={t(M['chat.assistant_composer.attach_files'])}
198
+ >
199
+ <svg viewBox="0 0 20 20" width="18" height="18" aria-hidden="true">
200
+ <path
201
+ d="M14.5 6.5l-6 6a2.5 2.5 0 003.54 3.54l6-6a4.5 4.5 0 00-6.36-6.36l-6.5 6.5a6.5 6.5 0 009.19 9.19"
202
+ fill="none"
203
+ stroke="currentColor"
204
+ stroke-width="1.4"
205
+ stroke-linecap="round"
206
+ stroke-linejoin="round"
207
+ />
208
+ </svg>
209
+ </Button>
210
+ <!-- raw-primitive-allow: visually-hidden native file input that backs the attach button's OS picker; the base Input primitive renders a visible bordered control and cannot be this hidden picker (same pattern as ../shared/FileUpload.svelte). Cycle-3 second final F2: the visually-hidden clip-rect styling keeps this element IN the accessibility tree, so unlike FileUpload's own wrapping label element with visible text, this needs an explicit aria-label to avoid an unnamed file control. -->
211
+ <input
212
+ bind:this={fileInputEl}
213
+ type="file"
214
+ class="assistant-composer-file-input"
215
+ multiple
216
+ onchange={handleFileChange}
217
+ disabled={disabled || uploading}
218
+ tabindex="-1"
219
+ aria-label={t(M['chat.assistant_composer.attach_files'])}
220
+ />
221
+ <Textarea
222
+ bind:value={content}
223
+ class="assistant-composer-textarea"
224
+ {placeholder}
225
+ disabled={disabled || uploading}
226
+ rows={1}
227
+ onkeydown={handleKeydown}
228
+ oninput={handleInput}
229
+ aria-label={t(M['chat.assistant_composer.message_label'])}
230
+ />
231
+ <Button
232
+ type="button"
233
+ class="assistant-composer-send"
234
+ onclick={handleSend}
235
+ disabled={disabled || uploading || sending || !content.trim()}
236
+ >
237
+ {t(M['chat.assistant_composer.send'])}
238
+ </Button>
239
+ </div>
240
+ </div>
241
+
242
+ <style>
243
+ .assistant-composer {
244
+ border-top: 1px solid var(--smrt-color-outline-variant, #c4c6cf);
245
+ background: var(--smrt-color-surface, #ffffff);
246
+ padding: var(--smrt-spacing-2, 8px);
247
+ flex-shrink: 0;
248
+ }
249
+
250
+ .assistant-composer-row {
251
+ display: flex;
252
+ align-items: flex-end;
253
+ gap: var(--smrt-spacing-2, 8px);
254
+ }
255
+
256
+ :global(.assistant-composer-attach) {
257
+ flex-shrink: 0;
258
+ display: inline-flex;
259
+ align-items: center;
260
+ justify-content: center;
261
+ width: 32px;
262
+ height: 32px;
263
+ padding: 0;
264
+ border: 1px solid var(--smrt-color-outline-variant, #c4c6cf);
265
+ border-radius: var(--smrt-radius-full, 9999px);
266
+ background: var(--smrt-color-surface-container-low, #f7f7fb);
267
+ color: var(--smrt-color-on-surface-variant, #43474e);
268
+ cursor: pointer;
269
+ }
270
+
271
+ :global(.assistant-composer-attach:hover:not(:disabled)) {
272
+ background: var(--smrt-color-surface-container, #f0f0f4);
273
+ }
274
+
275
+ :global(.assistant-composer-attach:disabled) {
276
+ opacity: 0.5;
277
+ cursor: not-allowed;
278
+ }
279
+
280
+ .assistant-composer-file-input {
281
+ position: absolute;
282
+ width: 1px;
283
+ height: 1px;
284
+ overflow: hidden;
285
+ clip: rect(0 0 0 0);
286
+ }
287
+
288
+ :global(.assistant-composer-textarea) {
289
+ flex: 1;
290
+ resize: none;
291
+ }
292
+
293
+ :global(.assistant-composer-send) {
294
+ flex-shrink: 0;
295
+ padding: var(--smrt-spacing-2, 8px) var(--smrt-spacing-3, 12px);
296
+ border: none;
297
+ border-radius: var(--smrt-radius-medium, 8px);
298
+ background: var(--smrt-color-primary, #005ac1);
299
+ color: var(--smrt-color-on-primary, #ffffff);
300
+ font: var(--smrt-typography-label-large-font, 500 0.875rem/1.25 sans-serif);
301
+ cursor: pointer;
302
+ }
303
+
304
+ :global(.assistant-composer-send:disabled) {
305
+ opacity: 0.5;
306
+ cursor: not-allowed;
307
+ }
308
+
309
+ .assistant-composer-attachments {
310
+ list-style: none;
311
+ display: flex;
312
+ flex-wrap: wrap;
313
+ gap: var(--smrt-spacing-2, 8px);
314
+ margin: 0 0 var(--smrt-spacing-2, 8px);
315
+ padding: 0;
316
+ }
317
+
318
+ .assistant-composer-chip {
319
+ display: inline-flex;
320
+ align-items: center;
321
+ gap: var(--smrt-spacing-1, 4px);
322
+ padding: var(--smrt-spacing-1, 4px) var(--smrt-spacing-2, 8px);
323
+ border-radius: var(--smrt-radius-full, 9999px);
324
+ background: var(--smrt-color-surface-container-low, #f7f7fb);
325
+ border: 1px solid var(--smrt-color-outline-variant, #c4c6cf);
326
+ font: var(--smrt-typography-label-small-font, 500 0.6875rem/1 sans-serif);
327
+ color: var(--smrt-color-on-surface, #1a1c1e);
328
+ max-width: 160px;
329
+ }
330
+
331
+ .assistant-composer-chip-name {
332
+ overflow: hidden;
333
+ text-overflow: ellipsis;
334
+ white-space: nowrap;
335
+ }
336
+
337
+ :global(.assistant-composer-chip-remove) {
338
+ border: none;
339
+ background: none;
340
+ color: var(--smrt-color-on-surface-variant, #43474e);
341
+ cursor: pointer;
342
+ line-height: 1;
343
+ padding: 0;
344
+ }
345
+
346
+ .assistant-composer-error {
347
+ margin: 0 0 var(--smrt-spacing-2, 8px);
348
+ padding: var(--smrt-spacing-2, 8px) var(--smrt-spacing-3, 12px);
349
+ border-radius: var(--smrt-radius-medium, 8px);
350
+ background: var(--smrt-color-error-container, #ffdad6);
351
+ color: var(--smrt-color-on-error-container, #410002);
352
+ font: var(--smrt-typography-body-small-font, 0.8125rem/1.4 sans-serif);
353
+ }
354
+ </style>
@@ -0,0 +1,20 @@
1
+ import type { AssistantAttachmentRef } from './assistant-transport.js';
2
+ export interface Props {
3
+ /** Called with the trimmed message text and any staged attachments when
4
+ * the user sends — via Enter or the Send button. May return a Promise
5
+ * (or reject/throw): the draft text and staged attachments are kept until
6
+ * it settles, cleared only on success (#2904 review finding 4) — a
7
+ * rejection restores them and shows an inline error. */
8
+ onsend: (content: string, attachments: AssistantAttachmentRef[]) => void | Promise<void>;
9
+ /** Called with the picked/dropped files; resolves to the uploaded
10
+ * attachment refs to stage as removable chips above the input. */
11
+ onupload: (files: FileList) => Promise<AssistantAttachmentRef[]>;
12
+ /** Disables the composer (e.g. no active thread yet). */
13
+ disabled?: boolean;
14
+ /** Placeholder text for the empty textarea. */
15
+ placeholder?: string;
16
+ }
17
+ declare const AssistantComposer: import("svelte").Component<Props, {}, "">;
18
+ type AssistantComposer = ReturnType<typeof AssistantComposer>;
19
+ export default AssistantComposer;
20
+ //# sourceMappingURL=AssistantComposer.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AssistantComposer.svelte.d.ts","sourceRoot":"","sources":["../../../../src/svelte/components/assistant/AssistantComposer.svelte.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAGvE,MAAM,WAAW,KAAK;IACpB;;;;4DAIwD;IACxD,MAAM,EAAE,CACN,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,sBAAsB,EAAE,KAClC,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1B;sEACkE;IAClE,QAAQ,EAAE,CAAC,KAAK,EAAE,QAAQ,KAAK,OAAO,CAAC,sBAAsB,EAAE,CAAC,CAAC;IACjE,yDAAyD;IACzD,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,+CAA+C;IAC/C,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AA6KD,QAAA,MAAM,iBAAiB,2CAAwC,CAAC;AAChE,KAAK,iBAAiB,GAAG,UAAU,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAC9D,eAAe,iBAAiB,CAAC"}