@happyvertical/smrt-chat 0.51.10 → 0.51.15

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 (44) hide show
  1. package/AGENTS.md +14 -3
  2. package/agents/dev-workbench.md +71 -0
  3. package/dist/chunks/{ChatService-B2piYas4.js → ChatService-D48KBDZy.js} +110 -7
  4. package/dist/chunks/ChatService-D48KBDZy.js.map +1 -0
  5. package/dist/collections/AgentSessionCollection.d.ts +2 -0
  6. package/dist/collections/AgentSessionCollection.d.ts.map +1 -1
  7. package/dist/index.js +10 -7
  8. package/dist/index.js.map +1 -1
  9. package/dist/internal/agent-runtime.js +1 -1
  10. package/dist/manifest.json +34 -5
  11. package/dist/models/AgentSession.d.ts +15 -0
  12. package/dist/models/AgentSession.d.ts.map +1 -1
  13. package/dist/persona-conversation.d.ts +6 -0
  14. package/dist/persona-conversation.d.ts.map +1 -1
  15. package/dist/services/ChatService.d.ts +16 -1
  16. package/dist/services/ChatService.d.ts.map +1 -1
  17. package/dist/smrt-knowledge.json +68 -15
  18. package/dist/svelte/components/assistant/AssistantComposer.svelte +30 -6
  19. package/dist/svelte/components/assistant/AssistantComposer.svelte.d.ts +5 -1
  20. package/dist/svelte/components/assistant/AssistantComposer.svelte.d.ts.map +1 -1
  21. package/dist/svelte/components/assistant/AssistantDock.svelte +365 -144
  22. package/dist/svelte/components/assistant/AssistantDock.svelte.d.ts +30 -3
  23. package/dist/svelte/components/assistant/AssistantDock.svelte.d.ts.map +1 -1
  24. package/dist/svelte/components/assistant/__tests__/AssistantDock.test.js +121 -1
  25. package/dist/svelte/components/assistant/__tests__/assistant-dock-action-outcome.test.js +320 -0
  26. package/dist/svelte/components/assistant/__tests__/assistant-dock-draft-settled.test.js +448 -0
  27. package/dist/svelte/components/assistant/__tests__/assistant-dock-host-actions.test.js +185 -0
  28. package/dist/svelte/components/assistant/__tests__/create-assistant-dock-controller.test.js +10 -11
  29. package/dist/svelte/components/assistant/create-assistant-dock-controller.svelte.d.ts +83 -0
  30. package/dist/svelte/components/assistant/create-assistant-dock-controller.svelte.d.ts.map +1 -1
  31. package/dist/svelte/components/assistant/create-assistant-dock-controller.svelte.js +0 -0
  32. package/dist/svelte/i18n.d.ts +3 -0
  33. package/dist/svelte/i18n.d.ts.map +1 -1
  34. package/dist/svelte/i18n.js +3 -0
  35. package/dist/svelte/index.d.ts +1 -1
  36. package/dist/svelte/index.d.ts.map +1 -1
  37. package/dist/svelte/index.js +1 -1
  38. package/dist/tool-loop.d.ts +7 -0
  39. package/dist/tool-loop.d.ts.map +1 -1
  40. package/dist/types.d.ts +2 -0
  41. package/dist/types.d.ts.map +1 -1
  42. package/dist/voice.d.ts.map +1 -1
  43. package/package.json +27 -21
  44. 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';
@@ -30,6 +32,7 @@ import type {
30
32
  import { safeAttachmentHref } from './attachment-href.js';
31
33
  import {
32
34
  type AssistantActionClient,
35
+ type AssistantActionOutcome,
33
36
  type AssistantDockController,
34
37
  createAssistantDockController,
35
38
  } from './create-assistant-dock-controller.svelte.js';
@@ -82,6 +85,38 @@ export interface Props {
82
85
  surfaces?: DataSurfaceIdentity[];
83
86
  /** Whether the dock is currently visible; polling pauses while false. */
84
87
  visible?: boolean;
88
+ /** Renders a message's own `toolCallData` (#2988), inside that message's
89
+ * bubble below its text. Called only for messages whose `toolCallData` is
90
+ * set. Without it the dock renders no tool-call region at all: the payload
91
+ * is host-defined, so the dock never stringifies it or injects it as HTML.
92
+ * Render it with ordinary Svelte markup in the host's own snippet. */
93
+ toolCall?: Snippet<[AssistantMessage]>;
94
+ /** Hands the host this dock's own controller once, on mount (#2989), so it
95
+ * can propose an action with `controller.previewAction(request)`. The
96
+ * proposal renders with Confirm/Reject like any other; everything else
97
+ * (apply, the idempotency key, mount checks) stays with the dock. */
98
+ oncontroller?: (controller: AssistantDockController) => void;
99
+ /** Called after the server accepts an apply (#2989). See
100
+ * `AssistantDockControllerOptions.onActionApplied`. */
101
+ onactionapplied?: (
102
+ request: DataSurfaceActionRequest,
103
+ result: DataSurfaceActionResult,
104
+ ) => void;
105
+ /** Called each time a proposed action reaches an outcome (#2991): applied,
106
+ * rejected (by the server or the user), or unknown. See
107
+ * `AssistantDockControllerOptions.onActionSettled`. */
108
+ onactionsettled?: (
109
+ request: DataSurfaceActionRequest,
110
+ outcome: AssistantActionOutcome,
111
+ ) => void;
112
+ /** Initial composer draft (#2991), for example a prompt computed for the
113
+ * item being edited. Read once, on mount; the user edits and sends it.
114
+ * It is never sent on their behalf. To replace it later, call
115
+ * `controller.setDraft(text)` from `oncontroller`. */
116
+ initialDraft?: string;
117
+ /** Placeholder for the composer's empty textarea (#2991). Defaults to the
118
+ * composer's own placeholder. */
119
+ composerPlaceholder?: string;
85
120
  }
86
121
 
87
122
  const {
@@ -90,6 +125,12 @@ const {
90
125
  actionClient,
91
126
  surfaces,
92
127
  visible = true,
128
+ toolCall,
129
+ oncontroller,
130
+ onactionapplied,
131
+ onactionsettled,
132
+ initialDraft,
133
+ composerPlaceholder,
93
134
  }: Props = $props();
94
135
  const { t } = useI18n();
95
136
 
@@ -112,6 +153,10 @@ const controller: AssistantDockController = createAssistantDockController({
112
153
  return surfaces;
113
154
  },
114
155
  visible: () => visible,
156
+ onActionApplied: (request, result) => onactionapplied?.(request, result),
157
+ onActionSettled: (request, outcome) => onactionsettled?.(request, outcome),
158
+ // Read once, on mount, like the controller itself.
159
+ initialDraft: untrack(() => initialDraft),
115
160
  });
116
161
 
117
162
  // F1 (#2904 review): the whole body runs under `untrack` so the effect takes
@@ -133,6 +178,7 @@ $effect(() => {
133
178
  void controller.loadThreads();
134
179
  void controller.loadModels();
135
180
  controller.startPolling();
181
+ oncontroller?.(controller);
136
182
  });
137
183
  return () => controller.dispose();
138
184
  });
@@ -179,7 +225,39 @@ $effect(() => {
179
225
  untrack(() => controller.syncTransport());
180
226
  });
181
227
 
228
+ // #3000: in a narrow container (below the `@container` breakpoint in the
229
+ // styles) the thread list collapses behind a "Conversations" toggle so the
230
+ // conversation keeps the full dock width. Wide layouts ignore this state —
231
+ // the toggle is hidden there and the list always sits beside the
232
+ // conversation. Picking or creating a thread closes the narrow list again.
233
+ let threadsOpen = $state(false);
234
+ const uid = $props.id();
235
+ const threadsId = `assistant-dock-threads-${uid}`;
236
+ let threadsEl: HTMLDivElement | undefined = $state();
237
+ // The shared Button primitive exposes no element ref, so the toggle is
238
+ // found by id when focus has to move back to it.
239
+ const threadsToggleId = `${threadsId}-toggle`;
240
+
241
+ // Closing the narrow list hides the element that holds focus (the thread row
242
+ // or "New conversation" button just activated). Hand focus back to the
243
+ // toggle so keyboard and screen-reader users keep their place. In wide
244
+ // layouts the list stays visible, so focus is left alone.
245
+ function closeThreads() {
246
+ if (!threadsOpen) return;
247
+ const focusWasInside =
248
+ typeof document !== 'undefined' &&
249
+ !!threadsEl?.contains(document.activeElement);
250
+ threadsOpen = false;
251
+ if (!focusWasInside) return;
252
+ void tick().then(() => {
253
+ if (threadsEl && threadsEl.offsetParent === null) {
254
+ document.getElementById(threadsToggleId)?.focus();
255
+ }
256
+ });
257
+ }
258
+
182
259
  async function handleSelectThread(threadId: string) {
260
+ closeThreads();
183
261
  // openThread() catches internally and records any failure on
184
262
  // controller.error (cycle-2 second final finding 2) — never rejects.
185
263
  await controller.openThread(threadId);
@@ -190,6 +268,7 @@ async function handleCreateThread() {
190
268
  // need); catch here so the un-awaited onclick in AssistantThreadList never
191
269
  // produces an unhandled rejection. The failure is already recorded on
192
270
  // controller.error by createThread itself (cycle-2 second final finding 2).
271
+ closeThreads();
193
272
  try {
194
273
  const thread = await controller.createThread('New conversation');
195
274
  await controller.openThread(thread.id);
@@ -250,161 +329,213 @@ async function handleConfirmAction(requestId: string) {
250
329
  </script>
251
330
 
252
331
  <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>
332
+ <div
333
+ class="assistant-dock-layout"
334
+ data-threads-open={threadsOpen || undefined}
335
+ >
336
+ <Button
337
+ type="button"
338
+ variant="ghost"
339
+ id={threadsToggleId}
340
+ class="assistant-dock-threads-toggle"
341
+ aria-expanded={threadsOpen}
342
+ aria-controls={threadsId}
343
+ onclick={() => (threadsOpen = !threadsOpen)}
344
+ >
345
+ {t(M['chat.assistant_dock.conversations_toggle'])}
346
+ </Button>
347
+
348
+ <div
349
+ class="assistant-dock-threads"
350
+ id={threadsId}
351
+ bind:this={threadsEl}
352
+ >
353
+ <AssistantThreadList
354
+ threads={controller.threads}
355
+ activeThreadId={controller.activeThreadId}
356
+ onselect={handleSelectThread}
357
+ oncreate={handleCreateThread}
358
+ />
359
+ </div>
360
+
361
+ <div class="assistant-dock-main">
362
+ {#if controller.error}
363
+ <p class="assistant-dock-error" role="alert">
364
+ {t(M['chat.assistant_dock.error'], { message: controller.error })}
365
+ </p>
348
366
  {/if}
349
367
 
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>
368
+ {#if controller.surfaces.length === 0}
369
+ <p class="assistant-dock-empty">
370
+ {t(M['chat.assistant_dock.no_surfaces'])}
371
+ </p>
363
372
  {/if}
364
373
 
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>
374
+ <div class="assistant-dock-scroll">
375
+ <ul class="assistant-dock-messages">
376
+ {#each controller.messages as message (message.id)}
377
+ <li>
378
+ <MessageBubble
379
+ variant={bubbleVariant(message.role)}
380
+ own={message.role === 'user'}
381
+ >
382
+ {#snippet children()}
383
+ <p class="assistant-dock-message-content">{message.content}</p>
384
+ {#if toolCall && message.toolCallData != null}
385
+ <div class="assistant-dock-tool-call">
386
+ {@render toolCall(message)}
387
+ </div>
388
+ {/if}
389
+ {#if message.attachments && message.attachments.length > 0}
390
+ <ul
391
+ class="assistant-dock-attachments"
392
+ aria-label={t(M['chat.assistant_dock.attachments'])}
393
+ >
394
+ {#each message.attachments as attachment (attachment.id)}
395
+ {@const href = safeAttachmentHref(attachment.url)}
396
+ <li class="assistant-dock-attachment">
397
+ {#if href}
398
+ <a
399
+ {href}
400
+ target="_blank"
401
+ rel="noopener noreferrer"
402
+ class="assistant-dock-attachment-link"
403
+ >
404
+ {attachment.name}
405
+ </a>
406
+ {:else}
407
+ <span class="assistant-dock-attachment-name">
408
+ {attachment.name}
409
+ </span>
410
+ {/if}
411
+ {#if attachment.size !== undefined}
412
+ <span class="assistant-dock-attachment-size">
413
+ {formatAttachmentSize(attachment.size)}
414
+ </span>
415
+ {/if}
416
+ </li>
417
+ {/each}
418
+ </ul>
419
+ {/if}
420
+ {/snippet}
421
+ </MessageBubble>
422
+ </li>
381
423
  {/each}
382
- </div>
383
- {/if}
384
- </div>
424
+ </ul>
385
425
 
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
- />
426
+ {#if controller.actions.size > 0}
427
+ <ul class="assistant-dock-actions">
428
+ {#each [...controller.actions.entries()] as [requestId, action] (requestId)}
429
+ <li>
430
+ <ToolCallDisplay
431
+ toolCall={{
432
+ toolName: action.request.actionId,
433
+ toolCallId: requestId,
434
+ status: toolCallStatusForAction(action.status),
435
+ error: action.error,
436
+ }}
437
+ actionResult={action.applyResult ??
438
+ // Finding 3 (#2904 review, fresh cycle): only surface the
439
+ // preview result — and its live Confirm/Reject — while the
440
+ // action is actually AWAITING confirmation. Once Confirm
441
+ // has been clicked ('applying'), the preview's phase:
442
+ // 'preview' result must stop rendering those buttons; the
443
+ // toolCall.status 'running' mapping above shows the
444
+ // existing spinner/"Executing..." state instead.
445
+ (action.status === 'previewed'
446
+ ? action.previewResult
447
+ : undefined)}
448
+ onconfirmaction={() => handleConfirmAction(requestId)}
449
+ onrejectaction={() => controller.rejectAction(requestId)}
450
+ />
451
+ {#if action.outcomeUnknown}
452
+ <!-- #2990: no decision was reached, so the change may have
453
+ landed. Reject is withdrawn (ToolCallDisplay only
454
+ offers it for a live preview); the only affordance is a
455
+ same-key retry. -->
456
+ <div class="assistant-dock-action-unknown" role="status">
457
+ <p>{t(M['chat.assistant_dock.action_outcome_unknown'])}</p>
458
+ <Button
459
+ type="button"
460
+ size="sm"
461
+ disabled={action.status === 'applying'}
462
+ onclick={() => handleConfirmAction(requestId)}
463
+ >
464
+ {t(M['chat.assistant_dock.action_check_again'])}
465
+ </Button>
466
+ </div>
467
+ {/if}
468
+ </li>
469
+ {/each}
470
+ </ul>
471
+ {/if}
472
+
473
+ {#if controller.pendingSends.some((p) => p.status === 'stale')}
474
+ <div class="assistant-dock-stale">
475
+ <p>{t(M['chat.assistant_dock.taking_longer'])}</p>
476
+ {#each controller.pendingSends.filter((p) => p.status === 'stale') as pending (pending.clientRequestId)}
477
+ <Button
478
+ type="button"
479
+ size="sm"
480
+ onclick={() => controller.retry(pending.clientRequestId)}
481
+ >
482
+ {t(M['chat.assistant_dock.retry'], { content: pending.content })}
483
+ </Button>
484
+ {/each}
485
+ </div>
486
+ {/if}
487
+
488
+ {#if controller.pendingSends.some((p) => p.status === 'failed')}
489
+ <!-- Finding 4 (#2904 review, fresh cycle): a send that failed
490
+ transport-side previously had no visible representation at
491
+ all — only 'stale' rendered above. retry(clientRequestId)
492
+ already exists and reuses the same id, so it's the same
493
+ affordance as the stale case. -->
494
+ <div class="assistant-dock-failed">
495
+ <p>{t(M['chat.assistant_dock.send_failed'])}</p>
496
+ {#each controller.pendingSends.filter((p) => p.status === 'failed') as pending (pending.clientRequestId)}
497
+ <Button
498
+ type="button"
499
+ size="sm"
500
+ onclick={() => controller.retry(pending.clientRequestId)}
501
+ >
502
+ {t(M['chat.assistant_dock.retry'], { content: pending.content })}
503
+ </Button>
504
+ {/each}
505
+ </div>
506
+ {/if}
507
+ </div>
508
+
509
+ <div class="assistant-dock-composer">
510
+ {#if controller.models.length > 0}
511
+ <div class="assistant-dock-composer-header">
512
+ <ModelPicker
513
+ models={controller.models}
514
+ value={controller.selectedModel ?? controller.models[0].id}
515
+ onchange={(modelId) => controller.setSelectedModel(modelId)}
516
+ />
517
+ </div>
518
+ {/if}
519
+ <AssistantComposer
520
+ bind:value={
521
+ () => controller.draft, (text) => controller.setDraft(text)
522
+ }
523
+ onsend={handleSend}
524
+ onupload={handleUpload}
525
+ disabled={!controller.activeThreadId}
526
+ placeholder={composerPlaceholder}
527
+ />
528
+ </div>
401
529
  </div>
402
530
  </div>
403
531
  </div>
404
532
 
405
533
  <style>
534
+ /* #3000: the dock sizes itself against its own container (a shell dock
535
+ * edge can be ~250px wide) rather than the viewport. A container cannot
536
+ * query itself, so the flex layout lives on an inner wrapper. */
406
537
  .assistant-dock {
407
- display: flex;
538
+ container-type: inline-size;
408
539
  height: 100%;
409
540
  min-height: 0;
410
541
  font-family: var(--smrt-font-family, system-ui, sans-serif);
@@ -412,6 +543,78 @@ async function handleConfirmAction(requestId: string) {
412
543
  color: var(--smrt-color-on-surface, #1a1c1e);
413
544
  }
414
545
 
546
+ .assistant-dock-layout {
547
+ display: flex;
548
+ height: 100%;
549
+ min-height: 0;
550
+ }
551
+
552
+ .assistant-dock-threads {
553
+ display: flex;
554
+ flex-shrink: 0;
555
+ min-height: 0;
556
+ }
557
+
558
+ .assistant-dock .assistant-dock-layout > :global(.assistant-dock-threads-toggle) {
559
+ display: none;
560
+ }
561
+
562
+ @container (max-width: 479px) {
563
+ .assistant-dock-layout {
564
+ flex-direction: column;
565
+ }
566
+
567
+ .assistant-dock .assistant-dock-layout > :global(.assistant-dock-threads-toggle) {
568
+ display: flex;
569
+ justify-content: flex-start;
570
+ align-items: center;
571
+ gap: var(--smrt-spacing-2, 8px);
572
+ flex-shrink: 0;
573
+ width: 100%;
574
+ padding: var(--smrt-spacing-2, 8px) var(--smrt-spacing-3, 12px);
575
+ border: none;
576
+ border-bottom: 1px solid var(--smrt-color-outline-variant, #c4c6cf);
577
+ background: var(--smrt-color-surface-container-low, #f7f7fb);
578
+ color: var(--smrt-color-on-surface, #1a1c1e);
579
+ font: var(--smrt-typography-label-large-font, 500 0.875rem/1.25 sans-serif);
580
+ font-family: inherit;
581
+ border-radius: 0;
582
+ text-align: left;
583
+ cursor: pointer;
584
+ }
585
+
586
+ .assistant-dock .assistant-dock-layout > :global(.assistant-dock-threads-toggle::before) {
587
+ content: '▸' / '';
588
+ color: var(--smrt-color-on-surface-variant, #43474e);
589
+ }
590
+
591
+ .assistant-dock .assistant-dock-layout[data-threads-open]
592
+ > :global(.assistant-dock-threads-toggle::before) {
593
+ content: '▾' / '';
594
+ }
595
+
596
+ .assistant-dock .assistant-dock-layout > :global(.assistant-dock-threads-toggle:focus-visible) {
597
+ outline: 2px solid var(--smrt-color-primary, #005ac1);
598
+ outline-offset: -2px;
599
+ }
600
+
601
+ .assistant-dock-threads {
602
+ display: none;
603
+ }
604
+
605
+ .assistant-dock-layout[data-threads-open] .assistant-dock-threads {
606
+ display: flex;
607
+ max-height: 40%;
608
+ border-bottom: 1px solid var(--smrt-color-outline-variant, #c4c6cf);
609
+ }
610
+
611
+ .assistant-dock-threads > :global(.assistant-thread-list) {
612
+ flex: 1;
613
+ min-width: 0;
614
+ border-right: none;
615
+ }
616
+ }
617
+
415
618
  .assistant-dock-main {
416
619
  flex: 1;
417
620
  display: flex;
@@ -448,6 +651,11 @@ async function handleConfirmAction(requestId: string) {
448
651
  gap: var(--smrt-spacing-2, 8px);
449
652
  }
450
653
 
654
+ .assistant-dock-tool-call {
655
+ margin-top: var(--smrt-spacing-2, 8px);
656
+ min-width: 0;
657
+ }
658
+
451
659
  .assistant-dock-message-content {
452
660
  margin: 0;
453
661
  white-space: pre-wrap;
@@ -494,6 +702,19 @@ async function handleConfirmAction(requestId: string) {
494
702
  gap: var(--smrt-spacing-2, 8px);
495
703
  }
496
704
 
705
+ .assistant-dock-action-unknown {
706
+ margin-top: var(--smrt-spacing-2, 8px);
707
+ padding: var(--smrt-spacing-2, 8px) var(--smrt-spacing-3, 12px);
708
+ border-radius: var(--smrt-radius-medium, 8px);
709
+ background: var(--smrt-color-tertiary-container, #ffd8e4);
710
+ color: var(--smrt-color-on-tertiary-container, #31111d);
711
+ font: var(--smrt-typography-body-small-font, 0.8125rem/1.4 sans-serif);
712
+ }
713
+
714
+ .assistant-dock-action-unknown p {
715
+ margin: 0 0 var(--smrt-spacing-2, 8px);
716
+ }
717
+
497
718
  .assistant-dock-stale {
498
719
  padding: var(--smrt-spacing-2, 8px) var(--smrt-spacing-3, 12px);
499
720
  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 AssistantActionOutcome, 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,32 @@ 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;
41
+ /** Called each time a proposed action reaches an outcome (#2991): applied,
42
+ * rejected (by the server or the user), or unknown. See
43
+ * `AssistantDockControllerOptions.onActionSettled`. */
44
+ onactionsettled?: (request: DataSurfaceActionRequest, outcome: AssistantActionOutcome) => void;
45
+ /** Initial composer draft (#2991), for example a prompt computed for the
46
+ * item being edited. Read once, on mount; the user edits and sends it.
47
+ * It is never sent on their behalf. To replace it later, call
48
+ * `controller.setDraft(text)` from `oncontroller`. */
49
+ initialDraft?: string;
50
+ /** Placeholder for the composer's empty textarea (#2991). Defaults to the
51
+ * composer's own placeholder. */
52
+ composerPlaceholder?: string;
26
53
  }
27
54
  declare const AssistantDock: import("svelte").Component<Props, {}, "">;
28
55
  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,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"}