@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.
- package/AGENTS.md +14 -3
- package/agents/dev-workbench.md +71 -0
- 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 +68 -15
- package/dist/svelte/components/assistant/AssistantComposer.svelte +30 -6
- package/dist/svelte/components/assistant/AssistantComposer.svelte.d.ts +5 -1
- package/dist/svelte/components/assistant/AssistantComposer.svelte.d.ts.map +1 -1
- package/dist/svelte/components/assistant/AssistantDock.svelte +365 -144
- package/dist/svelte/components/assistant/AssistantDock.svelte.d.ts +30 -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-draft-settled.test.js +448 -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 +83 -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 +27 -21
- 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
|
-
<
|
|
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>
|
|
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.
|
|
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>
|
|
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
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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
|
-
</
|
|
383
|
-
{/if}
|
|
384
|
-
</div>
|
|
424
|
+
</ul>
|
|
385
425
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
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
|
-
|
|
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
|
|
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 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;
|
|
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"}
|