@happyvertical/smrt-chat 0.51.9 → 0.51.11

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 (40) hide show
  1. package/AGENTS.md +6 -3
  2. package/dist/chunks/{ChatService-B2piYas4.js → ChatService-D48KBDZy.js} +110 -7
  3. package/dist/chunks/ChatService-D48KBDZy.js.map +1 -0
  4. package/dist/collections/AgentSessionCollection.d.ts +2 -0
  5. package/dist/collections/AgentSessionCollection.d.ts.map +1 -1
  6. package/dist/index.js +10 -7
  7. package/dist/index.js.map +1 -1
  8. package/dist/internal/agent-runtime.js +1 -1
  9. package/dist/manifest.json +34 -5
  10. package/dist/models/AgentSession.d.ts +15 -0
  11. package/dist/models/AgentSession.d.ts.map +1 -1
  12. package/dist/persona-conversation.d.ts +6 -0
  13. package/dist/persona-conversation.d.ts.map +1 -1
  14. package/dist/services/ChatService.d.ts +16 -1
  15. package/dist/services/ChatService.d.ts.map +1 -1
  16. package/dist/smrt-knowledge.json +23 -9
  17. package/dist/svelte/components/assistant/AssistantComposer.svelte +2 -0
  18. package/dist/svelte/components/assistant/AssistantDock.svelte +339 -144
  19. package/dist/svelte/components/assistant/AssistantDock.svelte.d.ts +18 -3
  20. package/dist/svelte/components/assistant/AssistantDock.svelte.d.ts.map +1 -1
  21. package/dist/svelte/components/assistant/__tests__/AssistantDock.test.js +121 -1
  22. package/dist/svelte/components/assistant/__tests__/assistant-dock-action-outcome.test.js +320 -0
  23. package/dist/svelte/components/assistant/__tests__/assistant-dock-host-actions.test.js +185 -0
  24. package/dist/svelte/components/assistant/__tests__/create-assistant-dock-controller.test.js +10 -11
  25. package/dist/svelte/components/assistant/create-assistant-dock-controller.svelte.d.ts +30 -0
  26. package/dist/svelte/components/assistant/create-assistant-dock-controller.svelte.d.ts.map +1 -1
  27. package/dist/svelte/components/assistant/create-assistant-dock-controller.svelte.js +0 -0
  28. package/dist/svelte/i18n.d.ts +3 -0
  29. package/dist/svelte/i18n.d.ts.map +1 -1
  30. package/dist/svelte/i18n.js +3 -0
  31. package/dist/svelte/index.d.ts +1 -1
  32. package/dist/svelte/index.d.ts.map +1 -1
  33. package/dist/svelte/index.js +1 -1
  34. package/dist/tool-loop.d.ts +7 -0
  35. package/dist/tool-loop.d.ts.map +1 -1
  36. package/dist/types.d.ts +2 -0
  37. package/dist/types.d.ts.map +1 -1
  38. package/dist/voice.d.ts.map +1 -1
  39. package/package.json +14 -12
  40. package/dist/chunks/ChatService-B2piYas4.js.map +0 -1
@@ -10,12 +10,14 @@
10
10
  */
11
11
  import { MessageBubble } from '@happyvertical/smrt-ui/chat';
12
12
  import type {
13
+ DataSurfaceActionRequest,
14
+ DataSurfaceActionResult,
13
15
  DataSurfaceIdentity,
14
16
  DataSurfaceRegistry,
15
17
  } from '@happyvertical/smrt-ui/data-surface';
16
18
  import { useI18n } from '@happyvertical/smrt-ui/i18n';
17
19
  import { Button } from '@happyvertical/smrt-ui/ui';
18
- import { untrack } from 'svelte';
20
+ import { type Snippet, tick, untrack } from 'svelte';
19
21
  import { M } from '../../i18n.js';
20
22
  import ToolCallDisplay from '../agent/ToolCallDisplay.svelte';
21
23
  import ModelPicker from '../shared/ModelPicker.svelte';
@@ -82,6 +84,23 @@ export interface Props {
82
84
  surfaces?: DataSurfaceIdentity[];
83
85
  /** Whether the dock is currently visible; polling pauses while false. */
84
86
  visible?: boolean;
87
+ /** Renders a message's own `toolCallData` (#2988), inside that message's
88
+ * bubble below its text. Called only for messages whose `toolCallData` is
89
+ * set. Without it the dock renders no tool-call region at all: the payload
90
+ * is host-defined, so the dock never stringifies it or injects it as HTML.
91
+ * Render it with ordinary Svelte markup in the host's own snippet. */
92
+ toolCall?: Snippet<[AssistantMessage]>;
93
+ /** Hands the host this dock's own controller once, on mount (#2989), so it
94
+ * can propose an action with `controller.previewAction(request)`. The
95
+ * proposal renders with Confirm/Reject like any other; everything else
96
+ * (apply, the idempotency key, mount checks) stays with the dock. */
97
+ oncontroller?: (controller: AssistantDockController) => void;
98
+ /** Called after the server accepts an apply (#2989). See
99
+ * `AssistantDockControllerOptions.onActionApplied`. */
100
+ onactionapplied?: (
101
+ request: DataSurfaceActionRequest,
102
+ result: DataSurfaceActionResult,
103
+ ) => void;
85
104
  }
86
105
 
87
106
  const {
@@ -90,6 +109,9 @@ const {
90
109
  actionClient,
91
110
  surfaces,
92
111
  visible = true,
112
+ toolCall,
113
+ oncontroller,
114
+ onactionapplied,
93
115
  }: Props = $props();
94
116
  const { t } = useI18n();
95
117
 
@@ -112,6 +134,7 @@ const controller: AssistantDockController = createAssistantDockController({
112
134
  return surfaces;
113
135
  },
114
136
  visible: () => visible,
137
+ onActionApplied: (request, result) => onactionapplied?.(request, result),
115
138
  });
116
139
 
117
140
  // F1 (#2904 review): the whole body runs under `untrack` so the effect takes
@@ -133,6 +156,7 @@ $effect(() => {
133
156
  void controller.loadThreads();
134
157
  void controller.loadModels();
135
158
  controller.startPolling();
159
+ oncontroller?.(controller);
136
160
  });
137
161
  return () => controller.dispose();
138
162
  });
@@ -179,7 +203,39 @@ $effect(() => {
179
203
  untrack(() => controller.syncTransport());
180
204
  });
181
205
 
206
+ // #3000: in a narrow container (below the `@container` breakpoint in the
207
+ // styles) the thread list collapses behind a "Conversations" toggle so the
208
+ // conversation keeps the full dock width. Wide layouts ignore this state —
209
+ // the toggle is hidden there and the list always sits beside the
210
+ // conversation. Picking or creating a thread closes the narrow list again.
211
+ let threadsOpen = $state(false);
212
+ const uid = $props.id();
213
+ const threadsId = `assistant-dock-threads-${uid}`;
214
+ let threadsEl: HTMLDivElement | undefined = $state();
215
+ // The shared Button primitive exposes no element ref, so the toggle is
216
+ // found by id when focus has to move back to it.
217
+ const threadsToggleId = `${threadsId}-toggle`;
218
+
219
+ // Closing the narrow list hides the element that holds focus (the thread row
220
+ // or "New conversation" button just activated). Hand focus back to the
221
+ // toggle so keyboard and screen-reader users keep their place. In wide
222
+ // layouts the list stays visible, so focus is left alone.
223
+ function closeThreads() {
224
+ if (!threadsOpen) return;
225
+ const focusWasInside =
226
+ typeof document !== 'undefined' &&
227
+ !!threadsEl?.contains(document.activeElement);
228
+ threadsOpen = false;
229
+ if (!focusWasInside) return;
230
+ void tick().then(() => {
231
+ if (threadsEl && threadsEl.offsetParent === null) {
232
+ document.getElementById(threadsToggleId)?.focus();
233
+ }
234
+ });
235
+ }
236
+
182
237
  async function handleSelectThread(threadId: string) {
238
+ closeThreads();
183
239
  // openThread() catches internally and records any failure on
184
240
  // controller.error (cycle-2 second final finding 2) — never rejects.
185
241
  await controller.openThread(threadId);
@@ -190,6 +246,7 @@ async function handleCreateThread() {
190
246
  // need); catch here so the un-awaited onclick in AssistantThreadList never
191
247
  // produces an unhandled rejection. The failure is already recorded on
192
248
  // controller.error by createThread itself (cycle-2 second final finding 2).
249
+ closeThreads();
193
250
  try {
194
251
  const thread = await controller.createThread('New conversation');
195
252
  await controller.openThread(thread.id);
@@ -250,161 +307,209 @@ async function handleConfirmAction(requestId: string) {
250
307
  </script>
251
308
 
252
309
  <div class="assistant-dock">
253
- <AssistantThreadList
254
- threads={controller.threads}
255
- activeThreadId={controller.activeThreadId}
256
- onselect={handleSelectThread}
257
- oncreate={handleCreateThread}
258
- />
259
-
260
- <div class="assistant-dock-main">
261
- {#if controller.error}
262
- <p class="assistant-dock-error" role="alert">
263
- {t(M['chat.assistant_dock.error'], { message: controller.error })}
264
- </p>
265
- {/if}
266
-
267
- {#if controller.surfaces.length === 0}
268
- <p class="assistant-dock-empty">
269
- {t(M['chat.assistant_dock.no_surfaces'])}
270
- </p>
271
- {/if}
272
-
273
- <div class="assistant-dock-scroll">
274
- <ul class="assistant-dock-messages">
275
- {#each controller.messages as message (message.id)}
276
- <li>
277
- <MessageBubble
278
- variant={bubbleVariant(message.role)}
279
- own={message.role === 'user'}
280
- >
281
- {#snippet children()}
282
- <p class="assistant-dock-message-content">{message.content}</p>
283
- {#if message.attachments && message.attachments.length > 0}
284
- <ul
285
- class="assistant-dock-attachments"
286
- aria-label={t(M['chat.assistant_dock.attachments'])}
287
- >
288
- {#each message.attachments as attachment (attachment.id)}
289
- {@const href = safeAttachmentHref(attachment.url)}
290
- <li class="assistant-dock-attachment">
291
- {#if href}
292
- <a
293
- {href}
294
- target="_blank"
295
- rel="noopener noreferrer"
296
- class="assistant-dock-attachment-link"
297
- >
298
- {attachment.name}
299
- </a>
300
- {:else}
301
- <span class="assistant-dock-attachment-name">
302
- {attachment.name}
303
- </span>
304
- {/if}
305
- {#if attachment.size !== undefined}
306
- <span class="assistant-dock-attachment-size">
307
- {formatAttachmentSize(attachment.size)}
308
- </span>
309
- {/if}
310
- </li>
311
- {/each}
312
- </ul>
313
- {/if}
314
- {/snippet}
315
- </MessageBubble>
316
- </li>
317
- {/each}
318
- </ul>
319
-
320
- {#if controller.actions.size > 0}
321
- <ul class="assistant-dock-actions">
322
- {#each [...controller.actions.entries()] as [requestId, action] (requestId)}
323
- <li>
324
- <ToolCallDisplay
325
- toolCall={{
326
- toolName: action.request.actionId,
327
- toolCallId: requestId,
328
- status: toolCallStatusForAction(action.status),
329
- error: action.error,
330
- }}
331
- actionResult={action.applyResult ??
332
- // Finding 3 (#2904 review, fresh cycle): only surface the
333
- // preview result — and its live Confirm/Reject — while the
334
- // action is actually AWAITING confirmation. Once Confirm
335
- // has been clicked ('applying'), the preview's phase:
336
- // 'preview' result must stop rendering those buttons; the
337
- // toolCall.status 'running' mapping above shows the
338
- // existing spinner/"Executing..." state instead.
339
- (action.status === 'previewed'
340
- ? action.previewResult
341
- : undefined)}
342
- onconfirmaction={() => handleConfirmAction(requestId)}
343
- onrejectaction={() => controller.rejectAction(requestId)}
344
- />
345
- </li>
346
- {/each}
347
- </ul>
310
+ <div
311
+ class="assistant-dock-layout"
312
+ data-threads-open={threadsOpen || undefined}
313
+ >
314
+ <Button
315
+ type="button"
316
+ variant="ghost"
317
+ id={threadsToggleId}
318
+ class="assistant-dock-threads-toggle"
319
+ aria-expanded={threadsOpen}
320
+ aria-controls={threadsId}
321
+ onclick={() => (threadsOpen = !threadsOpen)}
322
+ >
323
+ {t(M['chat.assistant_dock.conversations_toggle'])}
324
+ </Button>
325
+
326
+ <div
327
+ class="assistant-dock-threads"
328
+ id={threadsId}
329
+ bind:this={threadsEl}
330
+ >
331
+ <AssistantThreadList
332
+ threads={controller.threads}
333
+ activeThreadId={controller.activeThreadId}
334
+ onselect={handleSelectThread}
335
+ oncreate={handleCreateThread}
336
+ />
337
+ </div>
338
+
339
+ <div class="assistant-dock-main">
340
+ {#if controller.error}
341
+ <p class="assistant-dock-error" role="alert">
342
+ {t(M['chat.assistant_dock.error'], { message: controller.error })}
343
+ </p>
348
344
  {/if}
349
345
 
350
- {#if controller.pendingSends.some((p) => p.status === 'stale')}
351
- <div class="assistant-dock-stale">
352
- <p>{t(M['chat.assistant_dock.taking_longer'])}</p>
353
- {#each controller.pendingSends.filter((p) => p.status === 'stale') as pending (pending.clientRequestId)}
354
- <Button
355
- type="button"
356
- size="sm"
357
- onclick={() => controller.retry(pending.clientRequestId)}
358
- >
359
- {t(M['chat.assistant_dock.retry'], { content: pending.content })}
360
- </Button>
361
- {/each}
362
- </div>
346
+ {#if controller.surfaces.length === 0}
347
+ <p class="assistant-dock-empty">
348
+ {t(M['chat.assistant_dock.no_surfaces'])}
349
+ </p>
363
350
  {/if}
364
351
 
365
- {#if controller.pendingSends.some((p) => p.status === 'failed')}
366
- <!-- Finding 4 (#2904 review, fresh cycle): a send that failed
367
- transport-side previously had no visible representation at
368
- all — only 'stale' rendered above. retry(clientRequestId)
369
- already exists and reuses the same id, so it's the same
370
- affordance as the stale case. -->
371
- <div class="assistant-dock-failed">
372
- <p>{t(M['chat.assistant_dock.send_failed'])}</p>
373
- {#each controller.pendingSends.filter((p) => p.status === 'failed') as pending (pending.clientRequestId)}
374
- <Button
375
- type="button"
376
- size="sm"
377
- onclick={() => controller.retry(pending.clientRequestId)}
378
- >
379
- {t(M['chat.assistant_dock.retry'], { content: pending.content })}
380
- </Button>
352
+ <div class="assistant-dock-scroll">
353
+ <ul class="assistant-dock-messages">
354
+ {#each controller.messages as message (message.id)}
355
+ <li>
356
+ <MessageBubble
357
+ variant={bubbleVariant(message.role)}
358
+ own={message.role === 'user'}
359
+ >
360
+ {#snippet children()}
361
+ <p class="assistant-dock-message-content">{message.content}</p>
362
+ {#if toolCall && message.toolCallData != null}
363
+ <div class="assistant-dock-tool-call">
364
+ {@render toolCall(message)}
365
+ </div>
366
+ {/if}
367
+ {#if message.attachments && message.attachments.length > 0}
368
+ <ul
369
+ class="assistant-dock-attachments"
370
+ aria-label={t(M['chat.assistant_dock.attachments'])}
371
+ >
372
+ {#each message.attachments as attachment (attachment.id)}
373
+ {@const href = safeAttachmentHref(attachment.url)}
374
+ <li class="assistant-dock-attachment">
375
+ {#if href}
376
+ <a
377
+ {href}
378
+ target="_blank"
379
+ rel="noopener noreferrer"
380
+ class="assistant-dock-attachment-link"
381
+ >
382
+ {attachment.name}
383
+ </a>
384
+ {:else}
385
+ <span class="assistant-dock-attachment-name">
386
+ {attachment.name}
387
+ </span>
388
+ {/if}
389
+ {#if attachment.size !== undefined}
390
+ <span class="assistant-dock-attachment-size">
391
+ {formatAttachmentSize(attachment.size)}
392
+ </span>
393
+ {/if}
394
+ </li>
395
+ {/each}
396
+ </ul>
397
+ {/if}
398
+ {/snippet}
399
+ </MessageBubble>
400
+ </li>
381
401
  {/each}
382
- </div>
383
- {/if}
384
- </div>
402
+ </ul>
385
403
 
386
- <div class="assistant-dock-composer">
387
- {#if controller.models.length > 0}
388
- <div class="assistant-dock-composer-header">
389
- <ModelPicker
390
- models={controller.models}
391
- value={controller.selectedModel ?? controller.models[0].id}
392
- onchange={(modelId) => controller.setSelectedModel(modelId)}
393
- />
394
- </div>
395
- {/if}
396
- <AssistantComposer
397
- onsend={handleSend}
398
- onupload={handleUpload}
399
- disabled={!controller.activeThreadId}
400
- />
404
+ {#if controller.actions.size > 0}
405
+ <ul class="assistant-dock-actions">
406
+ {#each [...controller.actions.entries()] as [requestId, action] (requestId)}
407
+ <li>
408
+ <ToolCallDisplay
409
+ toolCall={{
410
+ toolName: action.request.actionId,
411
+ toolCallId: requestId,
412
+ status: toolCallStatusForAction(action.status),
413
+ error: action.error,
414
+ }}
415
+ actionResult={action.applyResult ??
416
+ // Finding 3 (#2904 review, fresh cycle): only surface the
417
+ // preview result — and its live Confirm/Reject — while the
418
+ // action is actually AWAITING confirmation. Once Confirm
419
+ // has been clicked ('applying'), the preview's phase:
420
+ // 'preview' result must stop rendering those buttons; the
421
+ // toolCall.status 'running' mapping above shows the
422
+ // existing spinner/"Executing..." state instead.
423
+ (action.status === 'previewed'
424
+ ? action.previewResult
425
+ : undefined)}
426
+ onconfirmaction={() => handleConfirmAction(requestId)}
427
+ onrejectaction={() => controller.rejectAction(requestId)}
428
+ />
429
+ {#if action.outcomeUnknown}
430
+ <!-- #2990: no decision was reached, so the change may have
431
+ landed. Reject is withdrawn (ToolCallDisplay only
432
+ offers it for a live preview); the only affordance is a
433
+ same-key retry. -->
434
+ <div class="assistant-dock-action-unknown" role="status">
435
+ <p>{t(M['chat.assistant_dock.action_outcome_unknown'])}</p>
436
+ <Button
437
+ type="button"
438
+ size="sm"
439
+ disabled={action.status === 'applying'}
440
+ onclick={() => handleConfirmAction(requestId)}
441
+ >
442
+ {t(M['chat.assistant_dock.action_check_again'])}
443
+ </Button>
444
+ </div>
445
+ {/if}
446
+ </li>
447
+ {/each}
448
+ </ul>
449
+ {/if}
450
+
451
+ {#if controller.pendingSends.some((p) => p.status === 'stale')}
452
+ <div class="assistant-dock-stale">
453
+ <p>{t(M['chat.assistant_dock.taking_longer'])}</p>
454
+ {#each controller.pendingSends.filter((p) => p.status === 'stale') as pending (pending.clientRequestId)}
455
+ <Button
456
+ type="button"
457
+ size="sm"
458
+ onclick={() => controller.retry(pending.clientRequestId)}
459
+ >
460
+ {t(M['chat.assistant_dock.retry'], { content: pending.content })}
461
+ </Button>
462
+ {/each}
463
+ </div>
464
+ {/if}
465
+
466
+ {#if controller.pendingSends.some((p) => p.status === 'failed')}
467
+ <!-- Finding 4 (#2904 review, fresh cycle): a send that failed
468
+ transport-side previously had no visible representation at
469
+ all — only 'stale' rendered above. retry(clientRequestId)
470
+ already exists and reuses the same id, so it's the same
471
+ affordance as the stale case. -->
472
+ <div class="assistant-dock-failed">
473
+ <p>{t(M['chat.assistant_dock.send_failed'])}</p>
474
+ {#each controller.pendingSends.filter((p) => p.status === 'failed') as pending (pending.clientRequestId)}
475
+ <Button
476
+ type="button"
477
+ size="sm"
478
+ onclick={() => controller.retry(pending.clientRequestId)}
479
+ >
480
+ {t(M['chat.assistant_dock.retry'], { content: pending.content })}
481
+ </Button>
482
+ {/each}
483
+ </div>
484
+ {/if}
485
+ </div>
486
+
487
+ <div class="assistant-dock-composer">
488
+ {#if controller.models.length > 0}
489
+ <div class="assistant-dock-composer-header">
490
+ <ModelPicker
491
+ models={controller.models}
492
+ value={controller.selectedModel ?? controller.models[0].id}
493
+ onchange={(modelId) => controller.setSelectedModel(modelId)}
494
+ />
495
+ </div>
496
+ {/if}
497
+ <AssistantComposer
498
+ onsend={handleSend}
499
+ onupload={handleUpload}
500
+ disabled={!controller.activeThreadId}
501
+ />
502
+ </div>
401
503
  </div>
402
504
  </div>
403
505
  </div>
404
506
 
405
507
  <style>
508
+ /* #3000: the dock sizes itself against its own container (a shell dock
509
+ * edge can be ~250px wide) rather than the viewport. A container cannot
510
+ * query itself, so the flex layout lives on an inner wrapper. */
406
511
  .assistant-dock {
407
- display: flex;
512
+ container-type: inline-size;
408
513
  height: 100%;
409
514
  min-height: 0;
410
515
  font-family: var(--smrt-font-family, system-ui, sans-serif);
@@ -412,6 +517,78 @@ async function handleConfirmAction(requestId: string) {
412
517
  color: var(--smrt-color-on-surface, #1a1c1e);
413
518
  }
414
519
 
520
+ .assistant-dock-layout {
521
+ display: flex;
522
+ height: 100%;
523
+ min-height: 0;
524
+ }
525
+
526
+ .assistant-dock-threads {
527
+ display: flex;
528
+ flex-shrink: 0;
529
+ min-height: 0;
530
+ }
531
+
532
+ .assistant-dock .assistant-dock-layout > :global(.assistant-dock-threads-toggle) {
533
+ display: none;
534
+ }
535
+
536
+ @container (max-width: 479px) {
537
+ .assistant-dock-layout {
538
+ flex-direction: column;
539
+ }
540
+
541
+ .assistant-dock .assistant-dock-layout > :global(.assistant-dock-threads-toggle) {
542
+ display: flex;
543
+ justify-content: flex-start;
544
+ align-items: center;
545
+ gap: var(--smrt-spacing-2, 8px);
546
+ flex-shrink: 0;
547
+ width: 100%;
548
+ padding: var(--smrt-spacing-2, 8px) var(--smrt-spacing-3, 12px);
549
+ border: none;
550
+ border-bottom: 1px solid var(--smrt-color-outline-variant, #c4c6cf);
551
+ background: var(--smrt-color-surface-container-low, #f7f7fb);
552
+ color: var(--smrt-color-on-surface, #1a1c1e);
553
+ font: var(--smrt-typography-label-large-font, 500 0.875rem/1.25 sans-serif);
554
+ font-family: inherit;
555
+ border-radius: 0;
556
+ text-align: left;
557
+ cursor: pointer;
558
+ }
559
+
560
+ .assistant-dock .assistant-dock-layout > :global(.assistant-dock-threads-toggle::before) {
561
+ content: '▸' / '';
562
+ color: var(--smrt-color-on-surface-variant, #43474e);
563
+ }
564
+
565
+ .assistant-dock .assistant-dock-layout[data-threads-open]
566
+ > :global(.assistant-dock-threads-toggle::before) {
567
+ content: '▾' / '';
568
+ }
569
+
570
+ .assistant-dock .assistant-dock-layout > :global(.assistant-dock-threads-toggle:focus-visible) {
571
+ outline: 2px solid var(--smrt-color-primary, #005ac1);
572
+ outline-offset: -2px;
573
+ }
574
+
575
+ .assistant-dock-threads {
576
+ display: none;
577
+ }
578
+
579
+ .assistant-dock-layout[data-threads-open] .assistant-dock-threads {
580
+ display: flex;
581
+ max-height: 40%;
582
+ border-bottom: 1px solid var(--smrt-color-outline-variant, #c4c6cf);
583
+ }
584
+
585
+ .assistant-dock-threads > :global(.assistant-thread-list) {
586
+ flex: 1;
587
+ min-width: 0;
588
+ border-right: none;
589
+ }
590
+ }
591
+
415
592
  .assistant-dock-main {
416
593
  flex: 1;
417
594
  display: flex;
@@ -448,6 +625,11 @@ async function handleConfirmAction(requestId: string) {
448
625
  gap: var(--smrt-spacing-2, 8px);
449
626
  }
450
627
 
628
+ .assistant-dock-tool-call {
629
+ margin-top: var(--smrt-spacing-2, 8px);
630
+ min-width: 0;
631
+ }
632
+
451
633
  .assistant-dock-message-content {
452
634
  margin: 0;
453
635
  white-space: pre-wrap;
@@ -494,6 +676,19 @@ async function handleConfirmAction(requestId: string) {
494
676
  gap: var(--smrt-spacing-2, 8px);
495
677
  }
496
678
 
679
+ .assistant-dock-action-unknown {
680
+ margin-top: var(--smrt-spacing-2, 8px);
681
+ padding: var(--smrt-spacing-2, 8px) var(--smrt-spacing-3, 12px);
682
+ border-radius: var(--smrt-radius-medium, 8px);
683
+ background: var(--smrt-color-tertiary-container, #ffd8e4);
684
+ color: var(--smrt-color-on-tertiary-container, #31111d);
685
+ font: var(--smrt-typography-body-small-font, 0.8125rem/1.4 sans-serif);
686
+ }
687
+
688
+ .assistant-dock-action-unknown p {
689
+ margin: 0 0 var(--smrt-spacing-2, 8px);
690
+ }
691
+
497
692
  .assistant-dock-stale {
498
693
  padding: var(--smrt-spacing-2, 8px) var(--smrt-spacing-3, 12px);
499
694
  border-radius: var(--smrt-radius-medium, 8px);
@@ -1,6 +1,7 @@
1
- import type { DataSurfaceIdentity, DataSurfaceRegistry } from '@happyvertical/smrt-ui/data-surface';
2
- import type { AssistantTransport } from './assistant-transport.js';
3
- import { type AssistantActionClient } from './create-assistant-dock-controller.svelte.js';
1
+ import type { DataSurfaceActionRequest, DataSurfaceActionResult, DataSurfaceIdentity, DataSurfaceRegistry } from '@happyvertical/smrt-ui/data-surface';
2
+ import { type Snippet } from 'svelte';
3
+ import type { AssistantMessage, AssistantTransport } from './assistant-transport.js';
4
+ import { type AssistantActionClient, type AssistantDockController } from './create-assistant-dock-controller.svelte.js';
4
5
  export interface Props {
5
6
  /** Thread/message I/O backend; see `./assistant-transport.js`. */
6
7
  transport: AssistantTransport;
@@ -23,6 +24,20 @@ export interface Props {
23
24
  surfaces?: DataSurfaceIdentity[];
24
25
  /** Whether the dock is currently visible; polling pauses while false. */
25
26
  visible?: boolean;
27
+ /** Renders a message's own `toolCallData` (#2988), inside that message's
28
+ * bubble below its text. Called only for messages whose `toolCallData` is
29
+ * set. Without it the dock renders no tool-call region at all: the payload
30
+ * is host-defined, so the dock never stringifies it or injects it as HTML.
31
+ * Render it with ordinary Svelte markup in the host's own snippet. */
32
+ toolCall?: Snippet<[AssistantMessage]>;
33
+ /** Hands the host this dock's own controller once, on mount (#2989), so it
34
+ * can propose an action with `controller.previewAction(request)`. The
35
+ * proposal renders with Confirm/Reject like any other; everything else
36
+ * (apply, the idempotency key, mount checks) stays with the dock. */
37
+ oncontroller?: (controller: AssistantDockController) => void;
38
+ /** Called after the server accepts an apply (#2989). See
39
+ * `AssistantDockControllerOptions.onActionApplied`. */
40
+ onactionapplied?: (request: DataSurfaceActionRequest, result: DataSurfaceActionResult) => void;
26
41
  }
27
42
  declare const AssistantDock: import("svelte").Component<Props, {}, "">;
28
43
  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,mBAAmB,EACnB,mBAAmB,EACpB,MAAM,qCAAqC,CAAC;AAU7C,OAAO,KAAK,EAGV,kBAAkB,EACnB,MAAM,0BAA0B,CAAC;AAElC,OAAO,EACL,KAAK,qBAAqB,EAG3B,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;CACnB;AAsUD,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;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,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;CACX;AAkYD,QAAA,MAAM,aAAa,2CAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}