@happyvertical/smrt-chat 0.51.5 → 0.51.7
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 -0
- package/dist/index.js +1 -1
- package/dist/manifest.json +1 -1
- package/dist/smrt-knowledge.json +5 -5
- package/dist/svelte/components/agent/ToolCallDisplay.svelte +103 -7
- package/dist/svelte/components/agent/ToolCallDisplay.svelte.d.ts +18 -0
- package/dist/svelte/components/agent/ToolCallDisplay.svelte.d.ts.map +1 -1
- package/dist/svelte/components/agent/__tests__/ToolCallDisplay.test.js +100 -9
- package/dist/svelte/components/assistant/AssistantComposer.svelte +354 -0
- package/dist/svelte/components/assistant/AssistantComposer.svelte.d.ts +20 -0
- package/dist/svelte/components/assistant/AssistantComposer.svelte.d.ts.map +1 -0
- package/dist/svelte/components/assistant/AssistantDock.svelte +534 -0
- package/dist/svelte/components/assistant/AssistantDock.svelte.d.ts +30 -0
- package/dist/svelte/components/assistant/AssistantDock.svelte.d.ts.map +1 -0
- package/dist/svelte/components/assistant/AssistantThreadList.svelte +136 -0
- package/dist/svelte/components/assistant/AssistantThreadList.svelte.d.ts +15 -0
- package/dist/svelte/components/assistant/AssistantThreadList.svelte.d.ts.map +1 -0
- package/dist/svelte/components/assistant/__tests__/AssistantComposer.test.js +75 -0
- package/dist/svelte/components/assistant/__tests__/AssistantDock.test.js +395 -0
- package/dist/svelte/components/assistant/__tests__/AssistantThreadList.test.js +45 -0
- package/dist/svelte/components/assistant/__tests__/action-status.test.js +25 -0
- package/dist/svelte/components/assistant/__tests__/assistant-transport.test.js +253 -0
- package/dist/svelte/components/assistant/__tests__/attachment-href.test.js +31 -0
- package/dist/svelte/components/assistant/__tests__/create-assistant-dock-controller.test.js +2492 -0
- package/dist/svelte/components/assistant/action-status.d.ts +14 -0
- package/dist/svelte/components/assistant/action-status.d.ts.map +1 -0
- package/dist/svelte/components/assistant/action-status.js +7 -0
- package/dist/svelte/components/assistant/assistant-transport.d.ts +239 -0
- package/dist/svelte/components/assistant/assistant-transport.d.ts.map +1 -0
- package/dist/svelte/components/assistant/assistant-transport.js +306 -0
- package/dist/svelte/components/assistant/attachment-href.d.ts +23 -0
- package/dist/svelte/components/assistant/attachment-href.d.ts.map +1 -0
- package/dist/svelte/components/assistant/attachment-href.js +36 -0
- package/dist/svelte/components/assistant/create-assistant-dock-controller.svelte.d.ts +172 -0
- package/dist/svelte/components/assistant/create-assistant-dock-controller.svelte.d.ts.map +1 -0
- package/dist/svelte/components/assistant/create-assistant-dock-controller.svelte.js +0 -0
- package/dist/svelte/components/shared/ModelPicker.svelte +84 -0
- package/dist/svelte/components/shared/ModelPicker.svelte.d.ts +26 -0
- package/dist/svelte/components/shared/ModelPicker.svelte.d.ts.map +1 -0
- package/dist/svelte/components/shared/__tests__/ModelPicker.test.js +36 -0
- package/dist/svelte/i18n.d.ts +21 -0
- package/dist/svelte/i18n.d.ts.map +1 -1
- package/dist/svelte/i18n.js +25 -0
- package/dist/svelte/index.d.ts +7 -0
- package/dist/svelte/index.d.ts.map +1 -1
- package/dist/svelte/index.js +12 -0
- package/package.json +11 -11
|
@@ -0,0 +1,534 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* AssistantDock - shell-mounted, route-aware assistant surface (#2904).
|
|
4
|
+
*
|
|
5
|
+
* Consumers mount this inside a host shell's own focus-tool primitive, e.g.
|
|
6
|
+
* smrt-svelte's `ShellDockTool` (see docs/assistant-dock.md "Shell mounting
|
|
7
|
+
* recipe"). This component does not import smrt-svelte — it only needs a
|
|
8
|
+
* `DataSurfaceRegistry` instance (already owned by the host shell) and an
|
|
9
|
+
* `AssistantTransport`.
|
|
10
|
+
*/
|
|
11
|
+
import { MessageBubble } from '@happyvertical/smrt-ui/chat';
|
|
12
|
+
import type {
|
|
13
|
+
DataSurfaceIdentity,
|
|
14
|
+
DataSurfaceRegistry,
|
|
15
|
+
} from '@happyvertical/smrt-ui/data-surface';
|
|
16
|
+
import { useI18n } from '@happyvertical/smrt-ui/i18n';
|
|
17
|
+
import { Button } from '@happyvertical/smrt-ui/ui';
|
|
18
|
+
import { untrack } from 'svelte';
|
|
19
|
+
import { M } from '../../i18n.js';
|
|
20
|
+
import ToolCallDisplay from '../agent/ToolCallDisplay.svelte';
|
|
21
|
+
import ModelPicker from '../shared/ModelPicker.svelte';
|
|
22
|
+
import AssistantComposer from './AssistantComposer.svelte';
|
|
23
|
+
import AssistantThreadList from './AssistantThreadList.svelte';
|
|
24
|
+
import { toolCallStatusForAction } from './action-status.js';
|
|
25
|
+
import type {
|
|
26
|
+
AssistantAttachmentRef,
|
|
27
|
+
AssistantMessage,
|
|
28
|
+
AssistantTransport,
|
|
29
|
+
} from './assistant-transport.js';
|
|
30
|
+
import { safeAttachmentHref } from './attachment-href.js';
|
|
31
|
+
import {
|
|
32
|
+
type AssistantActionClient,
|
|
33
|
+
type AssistantDockController,
|
|
34
|
+
createAssistantDockController,
|
|
35
|
+
} from './create-assistant-dock-controller.svelte.js';
|
|
36
|
+
|
|
37
|
+
// AssistantMessage.role is 'user' | 'assistant' | 'system' | 'tool'.
|
|
38
|
+
// MessageBubble's canonical styling axes are `variant` ('default' | 'agent' |
|
|
39
|
+
// 'system') + `own` — used directly (rather than its legacy `role` prop,
|
|
40
|
+
// whose narrower type isn't exported from `@happyvertical/smrt-ui/chat`) so
|
|
41
|
+
// messages render with the package's own bubble styling instead of raw
|
|
42
|
+
// "role: content" text (#2904 review fix). Tool output renders with the
|
|
43
|
+
// agent's tone.
|
|
44
|
+
function bubbleVariant(
|
|
45
|
+
role: AssistantMessage['role'],
|
|
46
|
+
): 'default' | 'agent' | 'system' {
|
|
47
|
+
if (role === 'system') return 'system';
|
|
48
|
+
if (role === 'user') return 'default';
|
|
49
|
+
return 'agent';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Cycle-3 second final finding 1: message.attachments was populated by the
|
|
53
|
+
// transport (sendMessage/loadMessages) but never rendered anywhere — a
|
|
54
|
+
// staged, uploaded, and sent attachment became permanently invisible once
|
|
55
|
+
// the composer's chip row cleared on send. Same non-i18n unit formatting
|
|
56
|
+
// precedent as ../shared/FileUpload.svelte's own formatSize().
|
|
57
|
+
function formatAttachmentSize(bytes: number): string {
|
|
58
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
59
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
60
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface Props {
|
|
64
|
+
/** Thread/message I/O backend; see `./assistant-transport.js`. */
|
|
65
|
+
transport: AssistantTransport;
|
|
66
|
+
/** The host shell's `DataSurfaceRegistry` instance — the dock discovers
|
|
67
|
+
* currently-mounted surfaces from this and fails closed when none are
|
|
68
|
+
* registered. */
|
|
69
|
+
registry: DataSurfaceRegistry;
|
|
70
|
+
/** Client-side seam to a server-hosted `DataSurfaceActionAdapter`; required
|
|
71
|
+
* to preview/apply proposed actions, optional for plain chat. */
|
|
72
|
+
actionClient?: AssistantActionClient;
|
|
73
|
+
/** Explicit surface narrowing filter (#2904 review finding 1; narrowed
|
|
74
|
+
* against the live registry per Copilot PR #2919 jAwr0): when set, the
|
|
75
|
+
* dock scopes discovery AND the preview/apply mount gate to the
|
|
76
|
+
* INTERSECTION of this list and `registry.list()` — an identity present
|
|
77
|
+
* here but not genuinely registered is never mounted. `registry` is still
|
|
78
|
+
* required (both for that intersection and for `syncRegistry`'s swap
|
|
79
|
+
* handling). Documented in `docs/assistant-dock.md` "Tenant scoping";
|
|
80
|
+
* previously only reachable by constructing `createAssistantDockController`
|
|
81
|
+
* directly, which this component itself does not expose. */
|
|
82
|
+
surfaces?: DataSurfaceIdentity[];
|
|
83
|
+
/** Whether the dock is currently visible; polling pauses while false. */
|
|
84
|
+
visible?: boolean;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const {
|
|
88
|
+
transport,
|
|
89
|
+
registry,
|
|
90
|
+
actionClient,
|
|
91
|
+
surfaces,
|
|
92
|
+
visible = true,
|
|
93
|
+
}: Props = $props();
|
|
94
|
+
const { t } = useI18n();
|
|
95
|
+
|
|
96
|
+
// Passed to the controller as getters (not direct values) so a later
|
|
97
|
+
// reassignment of these bound props (e.g. a host swapping the transport or
|
|
98
|
+
// registry instance) is actually observed, rather than only the value
|
|
99
|
+
// captured at the first run of this script — this also silences Svelte's
|
|
100
|
+
// `state_referenced_locally` warning for props read once outside a closure.
|
|
101
|
+
const controller: AssistantDockController = createAssistantDockController({
|
|
102
|
+
get transport() {
|
|
103
|
+
return transport;
|
|
104
|
+
},
|
|
105
|
+
get registry() {
|
|
106
|
+
return registry;
|
|
107
|
+
},
|
|
108
|
+
get actionClient() {
|
|
109
|
+
return actionClient;
|
|
110
|
+
},
|
|
111
|
+
get surfaces() {
|
|
112
|
+
return surfaces;
|
|
113
|
+
},
|
|
114
|
+
visible: () => visible,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// F1 (#2904 review): the whole body runs under `untrack` so the effect takes
|
|
118
|
+
// NO dependency on any $state read transitively by loadThreads/loadModels/
|
|
119
|
+
// startPolling (e.g. controller.pendingSends inside startPolling). Without
|
|
120
|
+
// this, every send() (which reassigns pendingSends before its first await)
|
|
121
|
+
// re-ran this effect, and the cleanup below permanently unsubscribed the
|
|
122
|
+
// registry listener on the very first message — freezing `surfaces` and
|
|
123
|
+
// silently breaking the route-scoping fail-closed guarantee. This effect
|
|
124
|
+
// must run exactly once per mount (loadThreads/loadModels fire once), with
|
|
125
|
+
// its cleanup firing exactly once, on unmount.
|
|
126
|
+
$effect(() => {
|
|
127
|
+
untrack(() => {
|
|
128
|
+
// Finding 4 (#2904 review, fresh cycle): loadThreads/loadModels already
|
|
129
|
+
// catch internally and record the failure on controller.error (see
|
|
130
|
+
// create-assistant-dock-controller.svelte.ts) — void-firing them here
|
|
131
|
+
// was never the source of an unhandled rejection, but nothing rendered
|
|
132
|
+
// the failure before this fix. No .catch() needed here now.
|
|
133
|
+
void controller.loadThreads();
|
|
134
|
+
void controller.loadModels();
|
|
135
|
+
controller.startPolling();
|
|
136
|
+
});
|
|
137
|
+
return () => controller.dispose();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// Finding B (#2904 review, third final pass): a SEPARATE effect, scoped to
|
|
141
|
+
// only the `registry` prop, so reassigning it (a host swapping
|
|
142
|
+
// tenant/workspace context) is actually observed — the getter passed to
|
|
143
|
+
// createAssistantDockController above makes `options.registry` return the
|
|
144
|
+
// new value, but nothing previously re-subscribed to it. `registry` is read
|
|
145
|
+
// directly here (a tracked dependency) so this effect reruns on a prop
|
|
146
|
+
// swap; everything inside stays `untrack`-ed so it does NOT also rerun on
|
|
147
|
+
// unrelated $state changes elsewhere (preserving the F1 guarantee that the
|
|
148
|
+
// mount effect above runs exactly once per mount).
|
|
149
|
+
$effect(() => {
|
|
150
|
+
void registry;
|
|
151
|
+
untrack(() => controller.syncRegistry());
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// Cycle-2 second final finding 1: a SEPARATE effect, scoped to only the
|
|
155
|
+
// `surfaces` prop, mirroring the `registry` effect above — reassigning
|
|
156
|
+
// `surfaces` (a host recomputing the override per route) is now observed:
|
|
157
|
+
// the controller re-reads the getter and invalidates any outstanding preview
|
|
158
|
+
// for a surface that fell out of scope. `surfaces` is read directly here (a
|
|
159
|
+
// tracked dependency) so this effect reruns on a prop swap; the body stays
|
|
160
|
+
// `untrack`-ed so it does not also rerun on unrelated $state changes
|
|
161
|
+
// elsewhere (preserving the F1 guarantee that the mount effect runs exactly
|
|
162
|
+
// once per mount).
|
|
163
|
+
$effect(() => {
|
|
164
|
+
void surfaces;
|
|
165
|
+
untrack(() => controller.syncSurfaces());
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// Copilot PR #2919 jAwsd: a SEPARATE effect, scoped to only the `transport`
|
|
169
|
+
// prop, mirroring the `registry` effect above — a host swapping the
|
|
170
|
+
// transport instance (e.g. alongside a registry swap, for a full
|
|
171
|
+
// tenant/workspace context change) previously left threads, messages, and
|
|
172
|
+
// pending sends from the OLD context in place. `transport` is read directly
|
|
173
|
+
// here (a tracked dependency) so this effect reruns on a prop swap; the body
|
|
174
|
+
// stays `untrack`-ed so it does not also rerun on unrelated $state changes
|
|
175
|
+
// elsewhere (preserving the F1 guarantee that the mount effect runs exactly
|
|
176
|
+
// once per mount).
|
|
177
|
+
$effect(() => {
|
|
178
|
+
void transport;
|
|
179
|
+
untrack(() => controller.syncTransport());
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
async function handleSelectThread(threadId: string) {
|
|
183
|
+
// openThread() catches internally and records any failure on
|
|
184
|
+
// controller.error (cycle-2 second final finding 2) — never rejects.
|
|
185
|
+
await controller.openThread(threadId);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async function handleCreateThread() {
|
|
189
|
+
// createThread() can still reject (its return value is the thing callers
|
|
190
|
+
// need); catch here so the un-awaited onclick in AssistantThreadList never
|
|
191
|
+
// produces an unhandled rejection. The failure is already recorded on
|
|
192
|
+
// controller.error by createThread itself (cycle-2 second final finding 2).
|
|
193
|
+
try {
|
|
194
|
+
const thread = await controller.createThread('New conversation');
|
|
195
|
+
await controller.openThread(thread.id);
|
|
196
|
+
} catch {
|
|
197
|
+
// Already surfaced via controller.error.
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async function handleSend(
|
|
202
|
+
content: string,
|
|
203
|
+
attachments: AssistantAttachmentRef[],
|
|
204
|
+
) {
|
|
205
|
+
// Finding 4 (#2904 review, fresh cycle): controller.send()/doSend()
|
|
206
|
+
// already marks the pendingSend 'failed' and rethrows on a transport
|
|
207
|
+
// error. Catch here to ALSO record it on controller.error (rendered as a
|
|
208
|
+
// dock-level banner), then rethrow so AssistantComposer's own `await
|
|
209
|
+
// onsend(...)` still sees the rejection and restores the user's draft
|
|
210
|
+
// instead of discarding it.
|
|
211
|
+
try {
|
|
212
|
+
await controller.send(content, attachments);
|
|
213
|
+
controller.setError(null);
|
|
214
|
+
} catch (error) {
|
|
215
|
+
controller.setError(error instanceof Error ? error.message : String(error));
|
|
216
|
+
throw error;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async function handleUpload(
|
|
221
|
+
files: FileList,
|
|
222
|
+
): Promise<AssistantAttachmentRef[]> {
|
|
223
|
+
// Cycle-2 third final: rethrows on failure (mirrors handleSend's pattern)
|
|
224
|
+
// rather than returning [] — the composer's own catch (added alongside
|
|
225
|
+
// this fix) needs the rejection to show its inline per-attempt error;
|
|
226
|
+
// returning [] here would make a failed upload look like a successful
|
|
227
|
+
// empty batch. This handler ALSO records the failure on controller.error
|
|
228
|
+
// so the dock-level banner matches the send path.
|
|
229
|
+
try {
|
|
230
|
+
const uploaded: AssistantAttachmentRef[] = [];
|
|
231
|
+
for (const file of Array.from(files)) {
|
|
232
|
+
uploaded.push(await transport.uploadAttachment(file));
|
|
233
|
+
}
|
|
234
|
+
controller.setError(null);
|
|
235
|
+
return uploaded;
|
|
236
|
+
} catch (error) {
|
|
237
|
+
controller.setError(error instanceof Error ? error.message : String(error));
|
|
238
|
+
throw error;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async function handleConfirmAction(requestId: string) {
|
|
243
|
+
// No idempotencyKey minted here: `applyAction` reuses the key generated
|
|
244
|
+
// once in `previewAction` and stored on the action state, so a retried
|
|
245
|
+
// Confirm click (e.g. after a client-side timeout on a call the server
|
|
246
|
+
// actually completed) dedups against that same attempt instead of
|
|
247
|
+
// re-executing. See `AssistantActionState.idempotencyKey`.
|
|
248
|
+
await controller.applyAction(requestId);
|
|
249
|
+
}
|
|
250
|
+
</script>
|
|
251
|
+
|
|
252
|
+
<div class="assistant-dock">
|
|
253
|
+
<AssistantThreadList
|
|
254
|
+
threads={controller.threads}
|
|
255
|
+
activeThreadId={controller.activeThreadId}
|
|
256
|
+
onselect={handleSelectThread}
|
|
257
|
+
oncreate={handleCreateThread}
|
|
258
|
+
/>
|
|
259
|
+
|
|
260
|
+
<div class="assistant-dock-main">
|
|
261
|
+
{#if controller.error}
|
|
262
|
+
<p class="assistant-dock-error" role="alert">
|
|
263
|
+
{t(M['chat.assistant_dock.error'], { message: controller.error })}
|
|
264
|
+
</p>
|
|
265
|
+
{/if}
|
|
266
|
+
|
|
267
|
+
{#if controller.surfaces.length === 0}
|
|
268
|
+
<p class="assistant-dock-empty">
|
|
269
|
+
{t(M['chat.assistant_dock.no_surfaces'])}
|
|
270
|
+
</p>
|
|
271
|
+
{/if}
|
|
272
|
+
|
|
273
|
+
<div class="assistant-dock-scroll">
|
|
274
|
+
<ul class="assistant-dock-messages">
|
|
275
|
+
{#each controller.messages as message (message.id)}
|
|
276
|
+
<li>
|
|
277
|
+
<MessageBubble
|
|
278
|
+
variant={bubbleVariant(message.role)}
|
|
279
|
+
own={message.role === 'user'}
|
|
280
|
+
>
|
|
281
|
+
{#snippet children()}
|
|
282
|
+
<p class="assistant-dock-message-content">{message.content}</p>
|
|
283
|
+
{#if message.attachments && message.attachments.length > 0}
|
|
284
|
+
<ul
|
|
285
|
+
class="assistant-dock-attachments"
|
|
286
|
+
aria-label={t(M['chat.assistant_dock.attachments'])}
|
|
287
|
+
>
|
|
288
|
+
{#each message.attachments as attachment (attachment.id)}
|
|
289
|
+
{@const href = safeAttachmentHref(attachment.url)}
|
|
290
|
+
<li class="assistant-dock-attachment">
|
|
291
|
+
{#if href}
|
|
292
|
+
<a
|
|
293
|
+
{href}
|
|
294
|
+
target="_blank"
|
|
295
|
+
rel="noopener noreferrer"
|
|
296
|
+
class="assistant-dock-attachment-link"
|
|
297
|
+
>
|
|
298
|
+
{attachment.name}
|
|
299
|
+
</a>
|
|
300
|
+
{:else}
|
|
301
|
+
<span class="assistant-dock-attachment-name">
|
|
302
|
+
{attachment.name}
|
|
303
|
+
</span>
|
|
304
|
+
{/if}
|
|
305
|
+
{#if attachment.size !== undefined}
|
|
306
|
+
<span class="assistant-dock-attachment-size">
|
|
307
|
+
{formatAttachmentSize(attachment.size)}
|
|
308
|
+
</span>
|
|
309
|
+
{/if}
|
|
310
|
+
</li>
|
|
311
|
+
{/each}
|
|
312
|
+
</ul>
|
|
313
|
+
{/if}
|
|
314
|
+
{/snippet}
|
|
315
|
+
</MessageBubble>
|
|
316
|
+
</li>
|
|
317
|
+
{/each}
|
|
318
|
+
</ul>
|
|
319
|
+
|
|
320
|
+
{#if controller.actions.size > 0}
|
|
321
|
+
<ul class="assistant-dock-actions">
|
|
322
|
+
{#each [...controller.actions.entries()] as [requestId, action] (requestId)}
|
|
323
|
+
<li>
|
|
324
|
+
<ToolCallDisplay
|
|
325
|
+
toolCall={{
|
|
326
|
+
toolName: action.request.actionId,
|
|
327
|
+
toolCallId: requestId,
|
|
328
|
+
status: toolCallStatusForAction(action.status),
|
|
329
|
+
error: action.error,
|
|
330
|
+
}}
|
|
331
|
+
actionResult={action.applyResult ??
|
|
332
|
+
// Finding 3 (#2904 review, fresh cycle): only surface the
|
|
333
|
+
// preview result — and its live Confirm/Reject — while the
|
|
334
|
+
// action is actually AWAITING confirmation. Once Confirm
|
|
335
|
+
// has been clicked ('applying'), the preview's phase:
|
|
336
|
+
// 'preview' result must stop rendering those buttons; the
|
|
337
|
+
// toolCall.status 'running' mapping above shows the
|
|
338
|
+
// existing spinner/"Executing..." state instead.
|
|
339
|
+
(action.status === 'previewed'
|
|
340
|
+
? action.previewResult
|
|
341
|
+
: undefined)}
|
|
342
|
+
onconfirmaction={() => handleConfirmAction(requestId)}
|
|
343
|
+
onrejectaction={() => controller.rejectAction(requestId)}
|
|
344
|
+
/>
|
|
345
|
+
</li>
|
|
346
|
+
{/each}
|
|
347
|
+
</ul>
|
|
348
|
+
{/if}
|
|
349
|
+
|
|
350
|
+
{#if controller.pendingSends.some((p) => p.status === 'stale')}
|
|
351
|
+
<div class="assistant-dock-stale">
|
|
352
|
+
<p>{t(M['chat.assistant_dock.taking_longer'])}</p>
|
|
353
|
+
{#each controller.pendingSends.filter((p) => p.status === 'stale') as pending (pending.clientRequestId)}
|
|
354
|
+
<Button
|
|
355
|
+
type="button"
|
|
356
|
+
size="sm"
|
|
357
|
+
onclick={() => controller.retry(pending.clientRequestId)}
|
|
358
|
+
>
|
|
359
|
+
{t(M['chat.assistant_dock.retry'], { content: pending.content })}
|
|
360
|
+
</Button>
|
|
361
|
+
{/each}
|
|
362
|
+
</div>
|
|
363
|
+
{/if}
|
|
364
|
+
|
|
365
|
+
{#if controller.pendingSends.some((p) => p.status === 'failed')}
|
|
366
|
+
<!-- Finding 4 (#2904 review, fresh cycle): a send that failed
|
|
367
|
+
transport-side previously had no visible representation at
|
|
368
|
+
all — only 'stale' rendered above. retry(clientRequestId)
|
|
369
|
+
already exists and reuses the same id, so it's the same
|
|
370
|
+
affordance as the stale case. -->
|
|
371
|
+
<div class="assistant-dock-failed">
|
|
372
|
+
<p>{t(M['chat.assistant_dock.send_failed'])}</p>
|
|
373
|
+
{#each controller.pendingSends.filter((p) => p.status === 'failed') as pending (pending.clientRequestId)}
|
|
374
|
+
<Button
|
|
375
|
+
type="button"
|
|
376
|
+
size="sm"
|
|
377
|
+
onclick={() => controller.retry(pending.clientRequestId)}
|
|
378
|
+
>
|
|
379
|
+
{t(M['chat.assistant_dock.retry'], { content: pending.content })}
|
|
380
|
+
</Button>
|
|
381
|
+
{/each}
|
|
382
|
+
</div>
|
|
383
|
+
{/if}
|
|
384
|
+
</div>
|
|
385
|
+
|
|
386
|
+
<div class="assistant-dock-composer">
|
|
387
|
+
{#if controller.models.length > 0}
|
|
388
|
+
<div class="assistant-dock-composer-header">
|
|
389
|
+
<ModelPicker
|
|
390
|
+
models={controller.models}
|
|
391
|
+
value={controller.selectedModel ?? controller.models[0].id}
|
|
392
|
+
onchange={(modelId) => controller.setSelectedModel(modelId)}
|
|
393
|
+
/>
|
|
394
|
+
</div>
|
|
395
|
+
{/if}
|
|
396
|
+
<AssistantComposer
|
|
397
|
+
onsend={handleSend}
|
|
398
|
+
onupload={handleUpload}
|
|
399
|
+
disabled={!controller.activeThreadId}
|
|
400
|
+
/>
|
|
401
|
+
</div>
|
|
402
|
+
</div>
|
|
403
|
+
</div>
|
|
404
|
+
|
|
405
|
+
<style>
|
|
406
|
+
.assistant-dock {
|
|
407
|
+
display: flex;
|
|
408
|
+
height: 100%;
|
|
409
|
+
min-height: 0;
|
|
410
|
+
font-family: var(--smrt-font-family, system-ui, sans-serif);
|
|
411
|
+
background: var(--smrt-color-surface, #ffffff);
|
|
412
|
+
color: var(--smrt-color-on-surface, #1a1c1e);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
.assistant-dock-main {
|
|
416
|
+
flex: 1;
|
|
417
|
+
display: flex;
|
|
418
|
+
flex-direction: column;
|
|
419
|
+
min-height: 0;
|
|
420
|
+
min-width: 0;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
.assistant-dock-empty {
|
|
424
|
+
margin: 0;
|
|
425
|
+
padding: var(--smrt-spacing-2, 8px) var(--smrt-spacing-3, 12px);
|
|
426
|
+
font: var(--smrt-typography-body-small-font, 0.8125rem/1.4 sans-serif);
|
|
427
|
+
color: var(--smrt-color-on-surface-variant, #43474e);
|
|
428
|
+
background: var(--smrt-color-surface-container-low, #f7f7fb);
|
|
429
|
+
border-bottom: 1px solid var(--smrt-color-outline-variant, #c4c6cf);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
.assistant-dock-scroll {
|
|
433
|
+
flex: 1;
|
|
434
|
+
overflow-y: auto;
|
|
435
|
+
display: flex;
|
|
436
|
+
flex-direction: column;
|
|
437
|
+
gap: var(--smrt-spacing-3, 12px);
|
|
438
|
+
padding: var(--smrt-spacing-3, 12px);
|
|
439
|
+
min-height: 0;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
.assistant-dock-messages {
|
|
443
|
+
list-style: none;
|
|
444
|
+
margin: 0;
|
|
445
|
+
padding: 0;
|
|
446
|
+
display: flex;
|
|
447
|
+
flex-direction: column;
|
|
448
|
+
gap: var(--smrt-spacing-2, 8px);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
.assistant-dock-message-content {
|
|
452
|
+
margin: 0;
|
|
453
|
+
white-space: pre-wrap;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
.assistant-dock-attachments {
|
|
457
|
+
list-style: none;
|
|
458
|
+
margin: var(--smrt-spacing-2, 8px) 0 0;
|
|
459
|
+
padding: 0;
|
|
460
|
+
display: flex;
|
|
461
|
+
flex-direction: column;
|
|
462
|
+
gap: var(--smrt-spacing-1, 4px);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
.assistant-dock-attachment {
|
|
466
|
+
display: flex;
|
|
467
|
+
align-items: baseline;
|
|
468
|
+
gap: var(--smrt-spacing-1, 4px);
|
|
469
|
+
font: var(--smrt-typography-body-small-font, 0.8125rem/1.4 sans-serif);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
.assistant-dock-attachment-link {
|
|
473
|
+
color: inherit;
|
|
474
|
+
text-decoration: underline;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
.assistant-dock-attachment-name {
|
|
478
|
+
overflow: hidden;
|
|
479
|
+
text-overflow: ellipsis;
|
|
480
|
+
white-space: nowrap;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
.assistant-dock-attachment-size {
|
|
484
|
+
opacity: 0.75;
|
|
485
|
+
font-size: var(--smrt-typography-label-small-size, 0.6875rem);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
.assistant-dock-actions {
|
|
489
|
+
list-style: none;
|
|
490
|
+
margin: 0;
|
|
491
|
+
padding: 0;
|
|
492
|
+
display: flex;
|
|
493
|
+
flex-direction: column;
|
|
494
|
+
gap: var(--smrt-spacing-2, 8px);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
.assistant-dock-stale {
|
|
498
|
+
padding: var(--smrt-spacing-2, 8px) var(--smrt-spacing-3, 12px);
|
|
499
|
+
border-radius: var(--smrt-radius-medium, 8px);
|
|
500
|
+
background: var(--smrt-color-tertiary-container, #ffd8e4);
|
|
501
|
+
color: var(--smrt-color-on-tertiary-container, #31111d);
|
|
502
|
+
font: var(--smrt-typography-body-small-font, 0.8125rem/1.4 sans-serif);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
.assistant-dock-stale p {
|
|
506
|
+
margin: 0 0 var(--smrt-spacing-2, 8px);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
.assistant-dock-failed {
|
|
510
|
+
padding: var(--smrt-spacing-2, 8px) var(--smrt-spacing-3, 12px);
|
|
511
|
+
border-radius: var(--smrt-radius-medium, 8px);
|
|
512
|
+
background: var(--smrt-color-error-container, #ffdad6);
|
|
513
|
+
color: var(--smrt-color-on-error-container, #410002);
|
|
514
|
+
font: var(--smrt-typography-body-small-font, 0.8125rem/1.4 sans-serif);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
.assistant-dock-failed p {
|
|
518
|
+
margin: 0 0 var(--smrt-spacing-2, 8px);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
.assistant-dock-error {
|
|
522
|
+
margin: 0;
|
|
523
|
+
padding: var(--smrt-spacing-2, 8px) var(--smrt-spacing-3, 12px);
|
|
524
|
+
font: var(--smrt-typography-body-small-font, 0.8125rem/1.4 sans-serif);
|
|
525
|
+
color: var(--smrt-color-on-error-container, #410002);
|
|
526
|
+
background: var(--smrt-color-error-container, #ffdad6);
|
|
527
|
+
border-bottom: 1px solid var(--smrt-color-outline-variant, #c4c6cf);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
.assistant-dock-composer-header {
|
|
531
|
+
padding: 0 var(--smrt-spacing-2, 8px);
|
|
532
|
+
padding-top: var(--smrt-spacing-2, 8px);
|
|
533
|
+
}
|
|
534
|
+
</style>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { DataSurfaceIdentity, DataSurfaceRegistry } from '@happyvertical/smrt-ui/data-surface';
|
|
2
|
+
import type { AssistantTransport } from './assistant-transport.js';
|
|
3
|
+
import { type AssistantActionClient } from './create-assistant-dock-controller.svelte.js';
|
|
4
|
+
export interface Props {
|
|
5
|
+
/** Thread/message I/O backend; see `./assistant-transport.js`. */
|
|
6
|
+
transport: AssistantTransport;
|
|
7
|
+
/** The host shell's `DataSurfaceRegistry` instance — the dock discovers
|
|
8
|
+
* currently-mounted surfaces from this and fails closed when none are
|
|
9
|
+
* registered. */
|
|
10
|
+
registry: DataSurfaceRegistry;
|
|
11
|
+
/** Client-side seam to a server-hosted `DataSurfaceActionAdapter`; required
|
|
12
|
+
* to preview/apply proposed actions, optional for plain chat. */
|
|
13
|
+
actionClient?: AssistantActionClient;
|
|
14
|
+
/** Explicit surface narrowing filter (#2904 review finding 1; narrowed
|
|
15
|
+
* against the live registry per Copilot PR #2919 jAwr0): when set, the
|
|
16
|
+
* dock scopes discovery AND the preview/apply mount gate to the
|
|
17
|
+
* INTERSECTION of this list and `registry.list()` — an identity present
|
|
18
|
+
* here but not genuinely registered is never mounted. `registry` is still
|
|
19
|
+
* required (both for that intersection and for `syncRegistry`'s swap
|
|
20
|
+
* handling). Documented in `docs/assistant-dock.md` "Tenant scoping";
|
|
21
|
+
* previously only reachable by constructing `createAssistantDockController`
|
|
22
|
+
* directly, which this component itself does not expose. */
|
|
23
|
+
surfaces?: DataSurfaceIdentity[];
|
|
24
|
+
/** Whether the dock is currently visible; polling pauses while false. */
|
|
25
|
+
visible?: boolean;
|
|
26
|
+
}
|
|
27
|
+
declare const AssistantDock: import("svelte").Component<Props, {}, "">;
|
|
28
|
+
type AssistantDock = ReturnType<typeof AssistantDock>;
|
|
29
|
+
export default AssistantDock;
|
|
30
|
+
//# sourceMappingURL=AssistantDock.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AssistantDock.svelte.d.ts","sourceRoot":"","sources":["../../../../src/svelte/components/assistant/AssistantDock.svelte.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EACV,mBAAmB,EACnB,mBAAmB,EACpB,MAAM,qCAAqC,CAAC;AAU7C,OAAO,KAAK,EAGV,kBAAkB,EACnB,MAAM,0BAA0B,CAAC;AAElC,OAAO,EACL,KAAK,qBAAqB,EAG3B,MAAM,8CAA8C,CAAC;AAGtD,MAAM,WAAW,KAAK;IACpB,kEAAkE;IAClE,SAAS,EAAE,kBAAkB,CAAC;IAC9B;;qBAEiB;IACjB,QAAQ,EAAE,mBAAmB,CAAC;IAC9B;qEACiE;IACjE,YAAY,CAAC,EAAE,qBAAqB,CAAC;IACrC;;;;;;;;gEAQ4D;IAC5D,QAAQ,CAAC,EAAE,mBAAmB,EAAE,CAAC;IACjC,yEAAyE;IACzE,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAsUD,QAAA,MAAM,aAAa,2CAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
|