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