@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.
- package/AGENTS.md +6 -3
- package/dist/chunks/{ChatService-B2piYas4.js → ChatService-D48KBDZy.js} +110 -7
- package/dist/chunks/ChatService-D48KBDZy.js.map +1 -0
- package/dist/collections/AgentSessionCollection.d.ts +2 -0
- package/dist/collections/AgentSessionCollection.d.ts.map +1 -1
- package/dist/index.js +10 -7
- package/dist/index.js.map +1 -1
- package/dist/internal/agent-runtime.js +1 -1
- package/dist/manifest.json +34 -5
- package/dist/models/AgentSession.d.ts +15 -0
- package/dist/models/AgentSession.d.ts.map +1 -1
- package/dist/persona-conversation.d.ts +6 -0
- package/dist/persona-conversation.d.ts.map +1 -1
- package/dist/services/ChatService.d.ts +16 -1
- package/dist/services/ChatService.d.ts.map +1 -1
- package/dist/smrt-knowledge.json +23 -9
- package/dist/svelte/components/assistant/AssistantComposer.svelte +2 -0
- package/dist/svelte/components/assistant/AssistantDock.svelte +339 -144
- package/dist/svelte/components/assistant/AssistantDock.svelte.d.ts +18 -3
- package/dist/svelte/components/assistant/AssistantDock.svelte.d.ts.map +1 -1
- package/dist/svelte/components/assistant/__tests__/AssistantDock.test.js +121 -1
- package/dist/svelte/components/assistant/__tests__/assistant-dock-action-outcome.test.js +320 -0
- package/dist/svelte/components/assistant/__tests__/assistant-dock-host-actions.test.js +185 -0
- package/dist/svelte/components/assistant/__tests__/create-assistant-dock-controller.test.js +10 -11
- package/dist/svelte/components/assistant/create-assistant-dock-controller.svelte.d.ts +30 -0
- package/dist/svelte/components/assistant/create-assistant-dock-controller.svelte.d.ts.map +1 -1
- package/dist/svelte/components/assistant/create-assistant-dock-controller.svelte.js +0 -0
- package/dist/svelte/i18n.d.ts +3 -0
- package/dist/svelte/i18n.d.ts.map +1 -1
- package/dist/svelte/i18n.js +3 -0
- package/dist/svelte/index.d.ts +1 -1
- package/dist/svelte/index.d.ts.map +1 -1
- package/dist/svelte/index.js +1 -1
- package/dist/tool-loop.d.ts +7 -0
- package/dist/tool-loop.d.ts.map +1 -1
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/voice.d.ts.map +1 -1
- package/package.json +14 -12
- 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
|
-
<
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
<
|
|
275
|
-
{
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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.
|
|
351
|
-
<
|
|
352
|
-
|
|
353
|
-
|
|
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
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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
|
-
</
|
|
383
|
-
{/if}
|
|
384
|
-
</div>
|
|
402
|
+
</ul>
|
|
385
403
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
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
|
-
|
|
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
|
|
3
|
-
import {
|
|
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;
|
|
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"}
|