@happyvertical/smrt-chat 0.51.39 → 0.52.0

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 (62) hide show
  1. package/AGENTS.md +19 -2
  2. package/README.md +13 -0
  3. package/dist/assistant-turn-events.d.ts +152 -0
  4. package/dist/assistant-turn-events.d.ts.map +1 -0
  5. package/dist/assistant-turn-events.js +2 -0
  6. package/dist/assistant-turn.d.ts +236 -0
  7. package/dist/assistant-turn.d.ts.map +1 -0
  8. package/dist/chunks/{ChatService-D48KBDZy.js → ChatService-jJi2JJJi.js} +2 -2
  9. package/dist/chunks/ChatService-jJi2JJJi.js.map +1 -0
  10. package/dist/chunks/assistant-turn-events-BbZUf-Ju.js +77 -0
  11. package/dist/chunks/assistant-turn-events-BbZUf-Ju.js.map +1 -0
  12. package/dist/index.d.ts +3 -1
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +776 -104
  15. package/dist/index.js.map +1 -1
  16. package/dist/internal/agent-runtime.js +1 -1
  17. package/dist/manifest.json +6 -2
  18. package/dist/models/AgentSession.d.ts +11 -1
  19. package/dist/models/AgentSession.d.ts.map +1 -1
  20. package/dist/smrt-knowledge.json +7 -13
  21. package/dist/svelte/components/assistant/AssistantChoiceCards.svelte +309 -0
  22. package/dist/svelte/components/assistant/AssistantChoiceCards.svelte.d.ts +19 -0
  23. package/dist/svelte/components/assistant/AssistantChoiceCards.svelte.d.ts.map +1 -0
  24. package/dist/svelte/components/assistant/AssistantComposer.svelte +116 -14
  25. package/dist/svelte/components/assistant/AssistantComposer.svelte.d.ts +28 -0
  26. package/dist/svelte/components/assistant/AssistantComposer.svelte.d.ts.map +1 -1
  27. package/dist/svelte/components/assistant/AssistantDock.svelte +196 -1
  28. package/dist/svelte/components/assistant/AssistantDock.svelte.d.ts +38 -1
  29. package/dist/svelte/components/assistant/AssistantDock.svelte.d.ts.map +1 -1
  30. package/dist/svelte/components/assistant/AssistantThreadList.svelte +13 -1
  31. package/dist/svelte/components/assistant/__tests__/AssistantDock.test.js +16 -16
  32. package/dist/svelte/components/assistant/__tests__/assistant-composer-dictation.test.js +197 -0
  33. package/dist/svelte/components/assistant/__tests__/assistant-dock-choices-pending.test.js +224 -0
  34. package/dist/svelte/components/assistant/__tests__/assistant-dock-choices-preview.test.js +312 -0
  35. package/dist/svelte/components/assistant/__tests__/assistant-dock-choices.test.js +338 -0
  36. package/dist/svelte/components/assistant/__tests__/assistant-dock-client-tools.test.js +442 -0
  37. package/dist/svelte/components/assistant/__tests__/assistant-dock-run.test.js +303 -0
  38. package/dist/svelte/components/assistant/__tests__/assistant-dock-settle.test.js +146 -0
  39. package/dist/svelte/components/assistant/assistant-choices.svelte.d.ts +251 -0
  40. package/dist/svelte/components/assistant/assistant-choices.svelte.d.ts.map +1 -0
  41. package/dist/svelte/components/assistant/assistant-choices.svelte.js +520 -0
  42. package/dist/svelte/components/assistant/assistant-transport.d.ts +54 -1
  43. package/dist/svelte/components/assistant/assistant-transport.d.ts.map +1 -1
  44. package/dist/svelte/components/assistant/assistant-transport.js +64 -0
  45. package/dist/svelte/components/assistant/client-tools.d.ts +85 -0
  46. package/dist/svelte/components/assistant/client-tools.d.ts.map +1 -0
  47. package/dist/svelte/components/assistant/client-tools.js +61 -0
  48. package/dist/svelte/components/assistant/create-assistant-dock-controller.svelte.d.ts +159 -0
  49. package/dist/svelte/components/assistant/create-assistant-dock-controller.svelte.d.ts.map +1 -1
  50. package/dist/svelte/components/assistant/create-assistant-dock-controller.svelte.js +0 -0
  51. package/dist/svelte/i18n.d.ts +15 -0
  52. package/dist/svelte/i18n.d.ts.map +1 -1
  53. package/dist/svelte/i18n.js +17 -1
  54. package/dist/svelte/index.d.ts +6 -2
  55. package/dist/svelte/index.d.ts.map +1 -1
  56. package/dist/svelte/index.js +8 -1
  57. package/dist/tool-allow-list.d.ts +8 -0
  58. package/dist/tool-allow-list.d.ts.map +1 -0
  59. package/dist/tool-loop.d.ts +191 -3
  60. package/dist/tool-loop.d.ts.map +1 -1
  61. package/package.json +19 -15
  62. package/dist/chunks/ChatService-D48KBDZy.js.map +0 -1
@@ -9,9 +9,20 @@
9
9
  * flow, which is sized for a dedicated upload panel, not a one-line composer
10
10
  * (#2904 review fix).
11
11
  */
12
- import { Textarea } from '@happyvertical/smrt-ui/forms';
12
+ import {
13
+ Dictation,
14
+ DictationButton,
15
+ type DictationSourceProvider,
16
+ DictationStatus,
17
+ type DictationTranscribe,
18
+ insertTextAtCursor,
19
+ longPress,
20
+ primeReadyBeep,
21
+ Textarea,
22
+ } from '@happyvertical/smrt-ui/forms';
13
23
  import { useI18n } from '@happyvertical/smrt-ui/i18n';
14
24
  import { Button } from '@happyvertical/smrt-ui/ui';
25
+ import { onDestroy } from 'svelte';
15
26
  import { M } from '../../i18n.js';
16
27
  import type { AssistantAttachmentRef } from './assistant-transport.js';
17
28
 
@@ -38,6 +49,23 @@ export interface Props {
38
49
  * the user to edit, and read back what they typed. Setting it never sends.
39
50
  * Cleared after `onsend` resolves. */
40
51
  value?: string;
52
+ /**
53
+ * Speak instead of typing: a speech source (smrt-svelte's
54
+ * `createSttDictationSource()`). With one, the composer shows a
55
+ * microphone button, and pressing and holding the message box starts
56
+ * listening too (it keeps listening after the finger comes up; tap the
57
+ * microphone, send, or press Escape to stop). Heard words go in at the
58
+ * cursor. Without one there is no microphone.
59
+ */
60
+ dictation?: DictationSourceProvider | null;
61
+ /**
62
+ * Writes a recorded message down, for browsers whose speech recognition
63
+ * is missing or broken (Firefox, Brave): the composer then records the
64
+ * message and shows "Writing it down…" while this runs. Normally smrt-ui's
65
+ * `createHttpTranscriber('/your/route')`. Also shows the microphone on its
66
+ * own, without `dictation`.
67
+ */
68
+ transcribe?: DictationTranscribe | null;
41
69
  }
42
70
 
43
71
  let {
@@ -46,7 +74,10 @@ let {
46
74
  disabled = false,
47
75
  placeholder = 'Ask the assistant…',
48
76
  value: content = $bindable(''),
77
+ dictation: dictationSource = null,
78
+ transcribe = null,
49
79
  }: Props = $props();
80
+ const canDictate = $derived(Boolean(dictationSource || transcribe));
50
81
  let stagedAttachments = $state<AssistantAttachmentRef[]>([]);
51
82
  let uploading = $state(false);
52
83
  let sending = $state(false);
@@ -69,6 +100,35 @@ let textareaComponent:
69
100
  | { getElement(): HTMLTextAreaElement | null }
70
101
  | undefined = $state();
71
102
 
103
+ function messageField(): HTMLTextAreaElement | null {
104
+ textareaEl ??= textareaComponent?.getElement() ?? undefined;
105
+ return textareaEl ?? null;
106
+ }
107
+
108
+ // One dictation per composer; the source is read when listening starts.
109
+ const dictation = new Dictation({
110
+ source: () => {
111
+ if (!dictationSource) throw new Error('Speech input is not available.');
112
+ return dictationSource();
113
+ },
114
+ onText: (text) => {
115
+ const field = messageField();
116
+ if (field) insertTextAtCursor(field, text);
117
+ else content = content ? `${content} ${text}` : text;
118
+ },
119
+ });
120
+ // The recording fallback follows the prop (a host may set it later).
121
+ $effect.pre(() => {
122
+ dictation.setOptions({ transcribe });
123
+ });
124
+
125
+ onDestroy(() => dictation.dispose());
126
+
127
+ function startDictationFromHold() {
128
+ if (disabled || uploading || dictation.active) return;
129
+ void dictation.start();
130
+ }
131
+
72
132
  function resize(el: HTMLTextAreaElement) {
73
133
  el.style.height = 'auto';
74
134
  el.style.height = `${Math.min(el.scrollHeight, 120)}px`;
@@ -127,6 +187,7 @@ async function handleDrop(event: DragEvent) {
127
187
  async function handleSend() {
128
188
  const trimmed = content.trim();
129
189
  if (!trimmed || disabled || sending) return;
190
+ if (dictation.active) void dictation.stop();
130
191
  sendError = null;
131
192
  sending = true;
132
193
  // #2904 review finding 4: keep the draft text/attachments until onsend
@@ -152,6 +213,11 @@ async function handleSend() {
152
213
  }
153
214
 
154
215
  function handleKeydown(event: KeyboardEvent) {
216
+ if (event.key === 'Escape' && dictation.active) {
217
+ event.preventDefault();
218
+ void dictation.stop();
219
+ return;
220
+ }
155
221
  // Matches ../messages/MessageInput.svelte's handleKeydown convention:
156
222
  // plain Enter sends, Shift+Enter inserts a newline. `isComposing` guards
157
223
  // against an IME's confirmation Enter being treated as a send.
@@ -205,6 +271,9 @@ function removeAttachment(id: string) {
205
271
  {/each}
206
272
  </ul>
207
273
  {/if}
274
+ {#if canDictate}
275
+ <DictationStatus {dictation} />
276
+ {/if}
208
277
  <div class="assistant-composer-row">
209
278
  <!-- 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 -->
210
279
  <Button
@@ -239,17 +308,30 @@ function removeAttachment(id: string) {
239
308
  tabindex="-1"
240
309
  aria-label={t(M['chat.assistant_composer.attach_files'])}
241
310
  />
242
- <Textarea
243
- bind:this={textareaComponent}
244
- bind:value={content}
245
- class="assistant-composer-textarea"
246
- {placeholder}
247
- disabled={disabled || uploading}
248
- rows={1}
249
- onkeydown={handleKeydown}
250
- oninput={handleInput}
251
- aria-label={t(M['chat.assistant_composer.message_label'])}
252
- />
311
+ <div
312
+ class="assistant-composer-field"
313
+ use:longPress={{
314
+ onPressStart: () => primeReadyBeep(),
315
+ onLongPress: startDictationFromHold,
316
+ onRelease: () => void dictation.unlock(),
317
+ disabled: !canDictate || disabled || uploading,
318
+ }}
319
+ >
320
+ <Textarea
321
+ bind:this={textareaComponent}
322
+ bind:value={content}
323
+ class="assistant-composer-textarea"
324
+ {placeholder}
325
+ disabled={disabled || uploading}
326
+ rows={1}
327
+ onkeydown={handleKeydown}
328
+ oninput={handleInput}
329
+ aria-label={t(M['chat.assistant_composer.message_label'])}
330
+ />
331
+ </div>
332
+ {#if canDictate}
333
+ <DictationButton {dictation} disabled={disabled || uploading} />
334
+ {/if}
253
335
  <Button
254
336
  type="button"
255
337
  class="assistant-composer-send"
@@ -280,8 +362,8 @@ function removeAttachment(id: string) {
280
362
  display: inline-flex;
281
363
  align-items: center;
282
364
  justify-content: center;
283
- width: 32px;
284
- height: 32px;
365
+ width: 44px;
366
+ height: 44px;
285
367
  padding: 0;
286
368
  border: 1px solid var(--smrt-color-outline-variant, #c4c6cf);
287
369
  border-radius: var(--smrt-radius-full, 9999px);
@@ -307,6 +389,12 @@ function removeAttachment(id: string) {
307
389
  clip: rect(0 0 0 0);
308
390
  }
309
391
 
392
+ .assistant-composer-field {
393
+ flex: 1;
394
+ display: flex;
395
+ min-inline-size: 0;
396
+ }
397
+
310
398
  :global(.assistant-composer-textarea) {
311
399
  flex: 1;
312
400
  resize: none;
@@ -314,6 +402,20 @@ function removeAttachment(id: string) {
314
402
  font-family: var(--smrt-font-family, inherit);
315
403
  }
316
404
 
405
+ /* Phones: 16px text so iOS Safari doesn't zoom the page when the
406
+ message field gets focus. */
407
+ @media (max-width: 48rem) {
408
+ .assistant-composer-field {
409
+ flex: 1;
410
+ display: flex;
411
+ min-inline-size: 0;
412
+ }
413
+
414
+ :global(.assistant-composer-textarea) {
415
+ font-size: max(1rem, 16px);
416
+ }
417
+ }
418
+
317
419
  :global(.assistant-composer-send) {
318
420
  flex-shrink: 0;
319
421
  padding: var(--smrt-spacing-2, 8px) var(--smrt-spacing-3, 12px);
@@ -1,3 +1,14 @@
1
+ /**
2
+ * AssistantComposer - text + attachment composer for AssistantDock (#2904).
3
+ *
4
+ * Enter-to-send / Shift+Enter-for-newline matches the package convention in
5
+ * `../messages/MessageInput.svelte` (same `Textarea` primitive, same
6
+ * `handleKeydown` shape). Attachments use a compact attach button + a small
7
+ * chip row instead of the full `../shared/FileUpload.svelte` dropzone/preview
8
+ * flow, which is sized for a dedicated upload panel, not a one-line composer
9
+ * (#2904 review fix).
10
+ */
11
+ import { type DictationSourceProvider, type DictationTranscribe } from '@happyvertical/smrt-ui/forms';
1
12
  import type { AssistantAttachmentRef } from './assistant-transport.js';
2
13
  export interface Props {
3
14
  /** Called with the trimmed message text and any staged attachments when
@@ -17,6 +28,23 @@ export interface Props {
17
28
  * the user to edit, and read back what they typed. Setting it never sends.
18
29
  * Cleared after `onsend` resolves. */
19
30
  value?: string;
31
+ /**
32
+ * Speak instead of typing: a speech source (smrt-svelte's
33
+ * `createSttDictationSource()`). With one, the composer shows a
34
+ * microphone button, and pressing and holding the message box starts
35
+ * listening too (it keeps listening after the finger comes up; tap the
36
+ * microphone, send, or press Escape to stop). Heard words go in at the
37
+ * cursor. Without one there is no microphone.
38
+ */
39
+ dictation?: DictationSourceProvider | null;
40
+ /**
41
+ * Writes a recorded message down, for browsers whose speech recognition
42
+ * is missing or broken (Firefox, Brave): the composer then records the
43
+ * message and shows "Writing it down…" while this runs. Normally smrt-ui's
44
+ * `createHttpTranscriber('/your/route')`. Also shows the microphone on its
45
+ * own, without `dictation`.
46
+ */
47
+ transcribe?: DictationTranscribe | null;
20
48
  }
21
49
  declare const AssistantComposer: import("svelte").Component<Props, {}, "value">;
22
50
  type AssistantComposer = ReturnType<typeof AssistantComposer>;
@@ -1 +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;IACrB;;0CAEsC;IACtC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AA8LD,QAAA,MAAM,iBAAiB,gDAAwC,CAAC;AAChE,KAAK,iBAAiB,GAAG,UAAU,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAC9D,eAAe,iBAAiB,CAAC"}
1
+ {"version":3,"file":"AssistantComposer.svelte.d.ts","sourceRoot":"","sources":["../../../../src/svelte/components/assistant/AssistantComposer.svelte.ts"],"names":[],"mappings":"AAGA;;;;;;;;;GASG;AACH,OAAO,EAGL,KAAK,uBAAuB,EAE5B,KAAK,mBAAmB,EAKzB,MAAM,8BAA8B,CAAC;AAKtC,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;IACrB;;0CAEsC;IACtC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;;;;OAOG;IACH,SAAS,CAAC,EAAE,uBAAuB,GAAG,IAAI,CAAC;IAC3C;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,mBAAmB,GAAG,IAAI,CAAC;CACzC;AAkPD,QAAA,MAAM,iBAAiB,gDAAwC,CAAC;AAChE,KAAK,iBAAiB,GAAG,UAAU,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAC9D,eAAe,iBAAiB,CAAC"}
@@ -15,25 +15,39 @@ import type {
15
15
  DataSurfaceIdentity,
16
16
  DataSurfaceRegistry,
17
17
  } from '@happyvertical/smrt-ui/data-surface';
18
+ import type {
19
+ DictationSourceProvider,
20
+ DictationTranscribe,
21
+ } from '@happyvertical/smrt-ui/forms';
18
22
  import { useI18n } from '@happyvertical/smrt-ui/i18n';
19
23
  import { Button } from '@happyvertical/smrt-ui/ui';
20
24
  import { type Snippet, tick, untrack } from 'svelte';
25
+ import type { AssistantStatus } from '../../../assistant-turn-events.js';
21
26
  import { M } from '../../i18n.js';
22
27
  import ToolCallDisplay from '../agent/ToolCallDisplay.svelte';
23
28
  import ModelPicker from '../shared/ModelPicker.svelte';
29
+ import AssistantChoiceCards from './AssistantChoiceCards.svelte';
24
30
  import AssistantComposer from './AssistantComposer.svelte';
25
31
  import AssistantThreadList from './AssistantThreadList.svelte';
26
32
  import { toolCallStatusForAction } from './action-status.js';
33
+ import type { AssistantChoiceSourceRegistry } from './assistant-choices.svelte.js';
27
34
  import type {
28
35
  AssistantAttachmentRef,
29
36
  AssistantMessage,
30
37
  AssistantTransport,
31
38
  } from './assistant-transport.js';
32
39
  import { safeAttachmentHref } from './attachment-href.js';
40
+ import {
41
+ type AssistantClientTool,
42
+ type AssistantClientToolPolicy,
43
+ type AssistantClientToolSource,
44
+ defaultClientToolPolicy,
45
+ } from './client-tools.js';
33
46
  import {
34
47
  type AssistantActionClient,
35
48
  type AssistantActionOutcome,
36
49
  type AssistantDockController,
50
+ type AssistantRun,
37
51
  createAssistantDockController,
38
52
  } from './create-assistant-dock-controller.svelte.js';
39
53
 
@@ -117,6 +131,39 @@ export interface Props {
117
131
  /** Placeholder for the composer's empty textarea (#2991). Defaults to the
118
132
  * composer's own placeholder. */
119
133
  composerPlaceholder?: string;
134
+ /** The page's browser tools (#2908) — typically
135
+ * `installWebMcpPageToolRegistry()` from
136
+ * `@happyvertical/smrt-web/webmcp-page-tools`. Offered to the model each
137
+ * turn; see `./client-tools.ts` for when a call waits for the user. */
138
+ pageTools?: AssistantClientToolSource;
139
+ /** Narrows when a browser tool call waits for the user (#2908).
140
+ * Destructive calls always wait. */
141
+ clientToolPolicy?: (tool: AssistantClientTool) => AssistantClientToolPolicy;
142
+ /** Called whenever the assistant's generic status changes (#2908), for a
143
+ * host's own "working" line. Also readable as `controller.status`. */
144
+ onstatus?: (status: AssistantStatus) => void;
145
+ /** Waits for the page to settle after a step's browser tools ran, before
146
+ * the turn resumes. See `AssistantDockControllerOptions.settle`. */
147
+ settle?: () => Promise<void> | void;
148
+ /** Narrows the page tools the dock declares and runs (e.g. a person's
149
+ * setting). See `AssistantDockControllerOptions.clientToolFilter`. */
150
+ clientToolFilter?: (tool: AssistantClientTool) => boolean;
151
+ /** How long a paused run may wait before it stops. Default 15 min. */
152
+ maxPauseMs?: number;
153
+ /** Called whenever the supervised run changes (#assistant-watch). Also
154
+ * readable as `controller.run`. */
155
+ onrun?: (run: AssistantRun | null) => void;
156
+ /** Page features that can offer a few options for the person to pick
157
+ * (see `./assistant-choices.svelte.ts`). The options show as cards here;
158
+ * the person's click applies one. */
159
+ choiceSources?: AssistantChoiceSourceRegistry;
160
+ /** Speak instead of typing in the composer (a speech source, e.g.
161
+ * smrt-svelte's `createSttDictationSource()`); see `AssistantComposer`. */
162
+ dictation?: DictationSourceProvider | null;
163
+ /** Writes a recorded message down when the browser cannot recognise
164
+ * speech (Firefox, Brave), e.g. smrt-ui's `createHttpTranscriber()`;
165
+ * see `AssistantComposer`. */
166
+ transcribe?: DictationTranscribe | null;
120
167
  }
121
168
 
122
169
  const {
@@ -131,6 +178,16 @@ const {
131
178
  onactionsettled,
132
179
  initialDraft,
133
180
  composerPlaceholder,
181
+ pageTools,
182
+ clientToolPolicy,
183
+ onstatus,
184
+ settle,
185
+ clientToolFilter,
186
+ maxPauseMs,
187
+ onrun,
188
+ choiceSources,
189
+ dictation,
190
+ transcribe,
134
191
  }: Props = $props();
135
192
  const { t } = useI18n();
136
193
 
@@ -157,8 +214,34 @@ const controller: AssistantDockController = createAssistantDockController({
157
214
  onActionSettled: (request, outcome) => onactionsettled?.(request, outcome),
158
215
  // Read once, on mount, like the controller itself.
159
216
  initialDraft: untrack(() => initialDraft),
217
+ get pageTools() {
218
+ return pageTools;
219
+ },
220
+ // Read per call so a reassigned prop is observed; the controller still
221
+ // forces `confirm` for a destructive tool whatever this returns.
222
+ clientToolPolicy: (tool) =>
223
+ (clientToolPolicy ?? defaultClientToolPolicy)(tool),
224
+ onStatus: (status) => onstatus?.(status),
225
+ settle: () => settle?.(),
226
+ // Read per call so a changed setting applies to the next step.
227
+ clientToolFilter: (tool) => clientToolFilter?.(tool) ?? true,
228
+ get maxPauseMs() {
229
+ return maxPauseMs;
230
+ },
231
+ onRun: (run) => onrun?.(run),
232
+ get choiceSources() {
233
+ return choiceSources;
234
+ },
160
235
  });
161
236
 
237
+ function formatToolArgs(args: Record<string, unknown>): string {
238
+ try {
239
+ return JSON.stringify(args, null, 2);
240
+ } catch {
241
+ return '';
242
+ }
243
+ }
244
+
162
245
  // F1 (#2904 review): the whole body runs under `untrack` so the effect takes
163
246
  // NO dependency on any $state read transitively by loadThreads/loadModels/
164
247
  // startPolling (e.g. controller.pendingSends inside startPolling). Without
@@ -365,7 +448,18 @@ async function handleConfirmAction(requestId: string) {
365
448
  </p>
366
449
  {/if}
367
450
 
368
- {#if controller.surfaces.length === 0}
451
+ {#if controller.status.state === 'working'}
452
+ <div class="assistant-dock-status" role="status" aria-live="polite">
453
+ <span class="assistant-dock-status-label">{controller.status.label}</span>
454
+ {#if controller.status.cancellable}
455
+ <Button type="button" size="sm" variant="ghost" onclick={() => controller.cancel()}>
456
+ {t(M['chat.assistant_dock.stop'])}
457
+ </Button>
458
+ {/if}
459
+ </div>
460
+ {/if}
461
+
462
+ {#if controller.surfaces.length === 0 && !pageTools}
369
463
  <p class="assistant-dock-empty">
370
464
  {t(M['chat.assistant_dock.no_surfaces'])}
371
465
  </p>
@@ -423,6 +517,54 @@ async function handleConfirmAction(requestId: string) {
423
517
  {/each}
424
518
  </ul>
425
519
 
520
+ {#if controller.streamingText}
521
+ <div
522
+ class="assistant-dock-streaming"
523
+ aria-label={t(M['chat.assistant_dock.reply_in_progress'])}
524
+ >
525
+ <MessageBubble variant="agent" own={false}>
526
+ {#snippet children()}
527
+ <p class="assistant-dock-message-content">{controller.streamingText}</p>
528
+ {/snippet}
529
+ </MessageBubble>
530
+ </div>
531
+ {/if}
532
+
533
+ {#each controller.toolRequests.filter((r) => r.status === 'waiting') as request (request.id)}
534
+ <div class="assistant-dock-tool-request" role="group" aria-label={t(M['chat.assistant_dock.tool_request_title'])}>
535
+ <p class="assistant-dock-tool-request-title">
536
+ {t(M['chat.assistant_dock.tool_request_title'])}
537
+ </p>
538
+ <p class="assistant-dock-tool-request-description">{request.description || request.name}</p>
539
+ {#if request.effect === 'destructive'}
540
+ <p class="assistant-dock-tool-request-warning">
541
+ {t(M['chat.assistant_dock.tool_request_destructive'])}
542
+ </p>
543
+ {/if}
544
+ <details class="assistant-dock-tool-request-details">
545
+ <summary>{t(M['chat.assistant_dock.tool_request_details'])}</summary>
546
+ <pre>{formatToolArgs(request.args)}</pre>
547
+ </details>
548
+ <div class="assistant-dock-tool-request-actions">
549
+ <Button type="button" size="sm" onclick={() => controller.approveToolRequest(request.id)}>
550
+ {t(M['chat.assistant_dock.tool_request_allow'])}
551
+ </Button>
552
+ <Button type="button" size="sm" variant="ghost" onclick={() => controller.declineToolRequest(request.id)}>
553
+ {t(M['chat.assistant_dock.tool_request_decline'])}
554
+ </Button>
555
+ </div>
556
+ </div>
557
+ {/each}
558
+
559
+ <AssistantChoiceCards
560
+ choices={controller.choices}
561
+ onchoose={(setId, optionId) => void controller.chooseOption(setId, optionId)}
562
+ ondismiss={(setId) => controller.dismissChoices(setId)}
563
+ onpreview={(setId, optionId) => void controller.previewOption(setId, optionId)}
564
+ oncommit={(setId) => void controller.commitOption(setId)}
565
+ oncancel={(setId) => controller.cancelChoices(setId)}
566
+ />
567
+
426
568
  {#if controller.actions.size > 0}
427
569
  <ul class="assistant-dock-actions">
428
570
  {#each [...controller.actions.entries()] as [requestId, action] (requestId)}
@@ -524,6 +666,8 @@ async function handleConfirmAction(requestId: string) {
524
666
  onupload={handleUpload}
525
667
  disabled={!controller.activeThreadId}
526
668
  placeholder={composerPlaceholder}
669
+ {dictation}
670
+ {transcribe}
527
671
  />
528
672
  </div>
529
673
  </div>
@@ -549,10 +693,59 @@ async function handleConfirmAction(requestId: string) {
549
693
  min-height: 0;
550
694
  }
551
695
 
696
+ .assistant-dock-status {
697
+ display: flex;
698
+ align-items: center;
699
+ justify-content: space-between;
700
+ gap: 0.5rem;
701
+ padding: 0.35rem 0.75rem;
702
+ border-bottom: 1px solid var(--smrt-color-outline-variant, #c4c7c5);
703
+ font-size: var(--smrt-typography-body-medium-size, 0.85rem);
704
+ color: var(--smrt-color-on-surface-variant, #44474e);
705
+ }
706
+
707
+ .assistant-dock-tool-request {
708
+ margin: 0.5rem 0;
709
+ padding: 0.75rem;
710
+ border: 1px solid var(--smrt-color-outline-variant, #c4c7c5);
711
+ border-radius: var(--smrt-radius-md, 8px);
712
+ background: var(--smrt-color-surface-container, #f3f3f3);
713
+ }
714
+
715
+ .assistant-dock-tool-request p {
716
+ margin: 0 0 0.4rem;
717
+ }
718
+
719
+ .assistant-dock-tool-request-title {
720
+ font-weight: var(--smrt-typography-weight-semibold, 600);
721
+ }
722
+
723
+ .assistant-dock-tool-request-warning {
724
+ color: var(--smrt-color-error, #b3261e);
725
+ }
726
+
727
+ .assistant-dock-tool-request-details pre {
728
+ max-height: 10rem;
729
+ overflow: auto;
730
+ font-size: var(--smrt-typography-body-small-size, 0.75rem);
731
+ white-space: pre-wrap;
732
+ word-break: break-word;
733
+ }
734
+
735
+ .assistant-dock-tool-request-actions {
736
+ display: flex;
737
+ flex-wrap: wrap;
738
+ gap: 0.5rem;
739
+ margin-top: 0.5rem;
740
+ }
741
+
552
742
  .assistant-dock-threads {
553
743
  display: flex;
554
744
  flex-shrink: 0;
555
745
  min-height: 0;
746
+ /* A long thread title must not squeeze the conversation (titles clip). */
747
+ max-width: min(16rem, 40%);
748
+ min-width: 0;
556
749
  }
557
750
 
558
751
  .assistant-dock .assistant-dock-layout > :global(.assistant-dock-threads-toggle) {
@@ -571,6 +764,7 @@ async function handleConfirmAction(requestId: string) {
571
764
  gap: var(--smrt-spacing-2, 8px);
572
765
  flex-shrink: 0;
573
766
  width: 100%;
767
+ min-height: 44px;
574
768
  padding: var(--smrt-spacing-2, 8px) var(--smrt-spacing-3, 12px);
575
769
  border: none;
576
770
  border-bottom: 1px solid var(--smrt-color-outline-variant, #c4c6cf);
@@ -604,6 +798,7 @@ async function handleConfirmAction(requestId: string) {
604
798
 
605
799
  .assistant-dock-layout[data-threads-open] .assistant-dock-threads {
606
800
  display: flex;
801
+ max-width: none;
607
802
  max-height: 40%;
608
803
  border-bottom: 1px solid var(--smrt-color-outline-variant, #c4c6cf);
609
804
  }
@@ -1,7 +1,11 @@
1
1
  import type { DataSurfaceActionRequest, DataSurfaceActionResult, DataSurfaceIdentity, DataSurfaceRegistry } from '@happyvertical/smrt-ui/data-surface';
2
+ import type { DictationSourceProvider, DictationTranscribe } from '@happyvertical/smrt-ui/forms';
2
3
  import { type Snippet } from 'svelte';
4
+ import type { AssistantStatus } from '../../../assistant-turn-events.js';
5
+ import type { AssistantChoiceSourceRegistry } from './assistant-choices.svelte.js';
3
6
  import type { AssistantMessage, AssistantTransport } from './assistant-transport.js';
4
- import { type AssistantActionClient, type AssistantActionOutcome, type AssistantDockController } from './create-assistant-dock-controller.svelte.js';
7
+ import { type AssistantClientTool, type AssistantClientToolPolicy, type AssistantClientToolSource } from './client-tools.js';
8
+ import { type AssistantActionClient, type AssistantActionOutcome, type AssistantDockController, type AssistantRun } from './create-assistant-dock-controller.svelte.js';
5
9
  export interface Props {
6
10
  /** Thread/message I/O backend; see `./assistant-transport.js`. */
7
11
  transport: AssistantTransport;
@@ -50,6 +54,39 @@ export interface Props {
50
54
  /** Placeholder for the composer's empty textarea (#2991). Defaults to the
51
55
  * composer's own placeholder. */
52
56
  composerPlaceholder?: string;
57
+ /** The page's browser tools (#2908) — typically
58
+ * `installWebMcpPageToolRegistry()` from
59
+ * `@happyvertical/smrt-web/webmcp-page-tools`. Offered to the model each
60
+ * turn; see `./client-tools.ts` for when a call waits for the user. */
61
+ pageTools?: AssistantClientToolSource;
62
+ /** Narrows when a browser tool call waits for the user (#2908).
63
+ * Destructive calls always wait. */
64
+ clientToolPolicy?: (tool: AssistantClientTool) => AssistantClientToolPolicy;
65
+ /** Called whenever the assistant's generic status changes (#2908), for a
66
+ * host's own "working" line. Also readable as `controller.status`. */
67
+ onstatus?: (status: AssistantStatus) => void;
68
+ /** Waits for the page to settle after a step's browser tools ran, before
69
+ * the turn resumes. See `AssistantDockControllerOptions.settle`. */
70
+ settle?: () => Promise<void> | void;
71
+ /** Narrows the page tools the dock declares and runs (e.g. a person's
72
+ * setting). See `AssistantDockControllerOptions.clientToolFilter`. */
73
+ clientToolFilter?: (tool: AssistantClientTool) => boolean;
74
+ /** How long a paused run may wait before it stops. Default 15 min. */
75
+ maxPauseMs?: number;
76
+ /** Called whenever the supervised run changes (#assistant-watch). Also
77
+ * readable as `controller.run`. */
78
+ onrun?: (run: AssistantRun | null) => void;
79
+ /** Page features that can offer a few options for the person to pick
80
+ * (see `./assistant-choices.svelte.ts`). The options show as cards here;
81
+ * the person's click applies one. */
82
+ choiceSources?: AssistantChoiceSourceRegistry;
83
+ /** Speak instead of typing in the composer (a speech source, e.g.
84
+ * smrt-svelte's `createSttDictationSource()`); see `AssistantComposer`. */
85
+ dictation?: DictationSourceProvider | null;
86
+ /** Writes a recorded message down when the browser cannot recognise
87
+ * speech (Firefox, Brave), e.g. smrt-ui's `createHttpTranscriber()`;
88
+ * see `AssistantComposer`. */
89
+ transcribe?: DictationTranscribe | null;
53
90
  }
54
91
  declare const AssistantDock: import("svelte").Component<Props, {}, "">;
55
92
  type AssistantDock = ReturnType<typeof AssistantDock>;
@@ -1 +1 @@
1
- {"version":3,"file":"AssistantDock.svelte.d.ts","sourceRoot":"","sources":["../../../../src/svelte/components/assistant/AssistantDock.svelte.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EACV,wBAAwB,EACxB,uBAAuB,EACvB,mBAAmB,EACnB,mBAAmB,EACpB,MAAM,qCAAqC,CAAC;AAG7C,OAAO,EAAE,KAAK,OAAO,EAAiB,MAAM,QAAQ,CAAC;AAOrD,OAAO,KAAK,EAEV,gBAAgB,EAChB,kBAAkB,EACnB,MAAM,0BAA0B,CAAC;AAElC,OAAO,EACL,KAAK,qBAAqB,EAC1B,KAAK,sBAAsB,EAC3B,KAAK,uBAAuB,EAE7B,MAAM,8CAA8C,CAAC;AAGtD,MAAM,WAAW,KAAK;IACpB,kEAAkE;IAClE,SAAS,EAAE,kBAAkB,CAAC;IAC9B;;qBAEiB;IACjB,QAAQ,EAAE,mBAAmB,CAAC;IAC9B;qEACiE;IACjE,YAAY,CAAC,EAAE,qBAAqB,CAAC;IACrC;;;;;;;;gEAQ4D;IAC5D,QAAQ,CAAC,EAAE,mBAAmB,EAAE,CAAC;IACjC,yEAAyE;IACzE,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;0EAIsE;IACtE,QAAQ,CAAC,EAAE,OAAO,CAAC,CAAC,gBAAgB,CAAC,CAAC,CAAC;IACvC;;;yEAGqE;IACrE,YAAY,CAAC,EAAE,CAAC,UAAU,EAAE,uBAAuB,KAAK,IAAI,CAAC;IAC7D;2DACuD;IACvD,eAAe,CAAC,EAAE,CAChB,OAAO,EAAE,wBAAwB,EACjC,MAAM,EAAE,uBAAuB,KAC5B,IAAI,CAAC;IACV;;2DAEuD;IACvD,eAAe,CAAC,EAAE,CAChB,OAAO,EAAE,wBAAwB,EACjC,OAAO,EAAE,sBAAsB,KAC5B,IAAI,CAAC;IACV;;;0DAGsD;IACtD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;qCACiC;IACjC,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAwYD,QAAA,MAAM,aAAa,2CAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
1
+ {"version":3,"file":"AssistantDock.svelte.d.ts","sourceRoot":"","sources":["../../../../src/svelte/components/assistant/AssistantDock.svelte.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EACV,wBAAwB,EACxB,uBAAuB,EACvB,mBAAmB,EACnB,mBAAmB,EACpB,MAAM,qCAAqC,CAAC;AAC7C,OAAO,KAAK,EACV,uBAAuB,EACvB,mBAAmB,EACpB,MAAM,8BAA8B,CAAC;AAGtC,OAAO,EAAE,KAAK,OAAO,EAAiB,MAAM,QAAQ,CAAC;AACrD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mCAAmC,CAAC;AAQzE,OAAO,KAAK,EAAE,6BAA6B,EAAE,MAAM,+BAA+B,CAAC;AACnF,OAAO,KAAK,EAEV,gBAAgB,EAChB,kBAAkB,EACnB,MAAM,0BAA0B,CAAC;AAElC,OAAO,EACL,KAAK,mBAAmB,EACxB,KAAK,yBAAyB,EAC9B,KAAK,yBAAyB,EAE/B,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACL,KAAK,qBAAqB,EAC1B,KAAK,sBAAsB,EAC3B,KAAK,uBAAuB,EAC5B,KAAK,YAAY,EAElB,MAAM,8CAA8C,CAAC;AAGtD,MAAM,WAAW,KAAK;IACpB,kEAAkE;IAClE,SAAS,EAAE,kBAAkB,CAAC;IAC9B;;qBAEiB;IACjB,QAAQ,EAAE,mBAAmB,CAAC;IAC9B;qEACiE;IACjE,YAAY,CAAC,EAAE,qBAAqB,CAAC;IACrC;;;;;;;;gEAQ4D;IAC5D,QAAQ,CAAC,EAAE,mBAAmB,EAAE,CAAC;IACjC,yEAAyE;IACzE,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;0EAIsE;IACtE,QAAQ,CAAC,EAAE,OAAO,CAAC,CAAC,gBAAgB,CAAC,CAAC,CAAC;IACvC;;;yEAGqE;IACrE,YAAY,CAAC,EAAE,CAAC,UAAU,EAAE,uBAAuB,KAAK,IAAI,CAAC;IAC7D;2DACuD;IACvD,eAAe,CAAC,EAAE,CAChB,OAAO,EAAE,wBAAwB,EACjC,MAAM,EAAE,uBAAuB,KAC5B,IAAI,CAAC;IACV;;2DAEuD;IACvD,eAAe,CAAC,EAAE,CAChB,OAAO,EAAE,wBAAwB,EACjC,OAAO,EAAE,sBAAsB,KAC5B,IAAI,CAAC;IACV;;;0DAGsD;IACtD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;qCACiC;IACjC,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B;;;2EAGuE;IACvE,SAAS,CAAC,EAAE,yBAAyB,CAAC;IACtC;wCACoC;IACpC,gBAAgB,CAAC,EAAE,CAAC,IAAI,EAAE,mBAAmB,KAAK,yBAAyB,CAAC;IAC5E;0EACsE;IACtE,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,eAAe,KAAK,IAAI,CAAC;IAC7C;wEACoE;IACpE,MAAM,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IACpC;0EACsE;IACtE,gBAAgB,CAAC,EAAE,CAAC,IAAI,EAAE,mBAAmB,KAAK,OAAO,CAAC;IAC1D,sEAAsE;IACtE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;uCACmC;IACnC,KAAK,CAAC,EAAE,CAAC,GAAG,EAAE,YAAY,GAAG,IAAI,KAAK,IAAI,CAAC;IAC3C;;yCAEqC;IACrC,aAAa,CAAC,EAAE,6BAA6B,CAAC;IAC9C;+EAC2E;IAC3E,SAAS,CAAC,EAAE,uBAAuB,GAAG,IAAI,CAAC;IAC3C;;kCAE8B;IAC9B,UAAU,CAAC,EAAE,mBAAmB,GAAG,IAAI,CAAC;CACzC;AAkeD,QAAA,MAAM,aAAa,2CAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
@@ -99,9 +99,10 @@ const { threads, activeThreadId = null, onselect, oncreate }: Props = $props();
99
99
  :global(.assistant-thread-list-item) {
100
100
  display: flex;
101
101
  align-items: center;
102
- justify-content: space-between;
102
+ justify-content: flex-start;
103
103
  gap: var(--smrt-spacing-2, 8px);
104
104
  width: 100%;
105
+ min-width: 0;
105
106
  padding: var(--smrt-spacing-2, 8px) var(--smrt-spacing-3, 12px);
106
107
  border: none;
107
108
  border-radius: var(--smrt-radius-medium, 8px);
@@ -112,6 +113,16 @@ const { threads, activeThreadId = null, onselect, oncreate }: Props = $props();
112
113
  cursor: pointer;
113
114
  }
114
115
 
116
+ /* The Button's inner row: title left (clipped with an ellipsis), count right. */
117
+ :global(.assistant-thread-list-item > .content) {
118
+ flex: 1 1 auto;
119
+ min-width: 0;
120
+ display: flex;
121
+ align-items: center;
122
+ justify-content: space-between;
123
+ gap: var(--smrt-spacing-2, 8px);
124
+ }
125
+
115
126
  :global(.assistant-thread-list-item:hover) {
116
127
  background: var(--smrt-color-surface-container, #f0f0f4);
117
128
  }
@@ -123,6 +134,7 @@ const { threads, activeThreadId = null, onselect, oncreate }: Props = $props();
123
134
  }
124
135
 
125
136
  .title {
137
+ min-width: 0;
126
138
  overflow: hidden;
127
139
  text-overflow: ellipsis;
128
140
  white-space: nowrap;