@tangle-network/sandbox-ui 0.58.0 → 0.60.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.
@@ -0,0 +1,1383 @@
1
+ import {
2
+ HARNESS_OPTIONS,
3
+ HarnessLogo,
4
+ chatCapableHarnesses
5
+ } from "./chunk-HU4B2M4K.js";
6
+ import {
7
+ ModelPicker,
8
+ canonicalModelId
9
+ } from "./chunk-SF6LPMMN.js";
10
+ import {
11
+ cn
12
+ } from "./chunk-EI44GEQ5.js";
13
+
14
+ // src/chat/index.ts
15
+ import {
16
+ AgentTimeline,
17
+ ChatContainer,
18
+ ChatInput,
19
+ ChatMessage as ChatMessage2,
20
+ MessageList,
21
+ ThinkingIndicator,
22
+ UserMessage
23
+ } from "@tangle-network/ui/chat";
24
+
25
+ // src/chat/reasoning-level-picker.tsx
26
+ import { reasoningLadder } from "@tangle-network/agent-interface";
27
+ import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
28
+ import { Brain, ChevronDown, Sparkles } from "lucide-react";
29
+ import { jsx, jsxs } from "react/jsx-runtime";
30
+ var REASONING_LADDER = reasoningLadder;
31
+ function ReasoningGlyph({ level }) {
32
+ if (level === "auto") {
33
+ return /* @__PURE__ */ jsx(Sparkles, { className: "h-3.5 w-3.5 text-muted-foreground" });
34
+ }
35
+ const BARS = 4;
36
+ const rank = REASONING_LADDER.indexOf(level) + 1;
37
+ const filled = Math.max(1, Math.ceil(rank / REASONING_LADDER.length * BARS));
38
+ return /* @__PURE__ */ jsx(
39
+ "span",
40
+ {
41
+ "aria-hidden": true,
42
+ className: "inline-flex h-3.5 items-end gap-px",
43
+ style: { width: 16 },
44
+ children: Array.from({ length: BARS }).map((_, i) => /* @__PURE__ */ jsx(
45
+ "span",
46
+ {
47
+ className: cn(
48
+ "w-1 rounded-[1px]",
49
+ i < filled ? "bg-foreground" : "bg-border"
50
+ ),
51
+ style: { height: `${35 + i * 22}%` }
52
+ },
53
+ i
54
+ ))
55
+ }
56
+ );
57
+ }
58
+ var DEFAULT_REASONING_LEVEL_OPTIONS = [
59
+ { value: "auto", label: "Auto", description: "Let the agent pick the right depth." },
60
+ { value: "none", label: "None", description: "No extended thinking \u2014 fastest, cheapest." },
61
+ { value: "minimal", label: "Minimal", description: "Thinking on, lowest budget." },
62
+ { value: "low", label: "Low", description: "Fast, direct answers." },
63
+ { value: "medium", label: "Medium", description: "Inspect context before acting." },
64
+ { value: "high", label: "High", description: "Deeper planning and edge-case checks." },
65
+ { value: "xhigh", label: "Extra High", description: "Extended reasoning for hard problems (Codex/OpenAI)." },
66
+ { value: "ultracode", label: "Ultracode", description: "Maximum extended thinking (Claude Code's ultracode)." }
67
+ ];
68
+ function ReasoningLevelPicker({
69
+ value,
70
+ onChange,
71
+ disabled,
72
+ className,
73
+ triggerClassName,
74
+ options = DEFAULT_REASONING_LEVEL_OPTIONS,
75
+ available,
76
+ side,
77
+ avoidCollisions
78
+ }) {
79
+ const shown = available ? options.filter((o) => o.value === "auto" || available.includes(o.value)) : options;
80
+ const selected = shown.find((option) => option.value === value);
81
+ const label = selected?.label ?? "Auto";
82
+ return /* @__PURE__ */ jsxs(DropdownMenu.Root, { children: [
83
+ /* @__PURE__ */ jsx(DropdownMenu.Trigger, { asChild: true, children: /* @__PURE__ */ jsxs(
84
+ "button",
85
+ {
86
+ type: "button",
87
+ disabled,
88
+ className: cn(
89
+ "inline-flex h-8 items-center gap-1.5 rounded-lg border border-[var(--md3-outline-variant)] bg-surface-container px-2.5",
90
+ "text-xs font-medium text-foreground shadow-sm transition-colors",
91
+ "hover:border-[var(--md3-outline-variant)] hover:bg-surface-container-high focus:outline-none focus-visible:ring-1 focus-visible:ring-ring",
92
+ "data-[state=open]:border-[var(--md3-outline-variant)] data-[state=open]:bg-surface-container-high",
93
+ "disabled:cursor-not-allowed disabled:opacity-50",
94
+ className,
95
+ triggerClassName
96
+ ),
97
+ "aria-label": "Reasoning level",
98
+ children: [
99
+ /* @__PURE__ */ jsx(Brain, { className: "h-3.5 w-3.5 text-muted-foreground" }),
100
+ /* @__PURE__ */ jsx("span", { children: label }),
101
+ /* @__PURE__ */ jsx(ChevronDown, { className: "h-3.5 w-3.5 text-muted-foreground" })
102
+ ]
103
+ }
104
+ ) }),
105
+ /* @__PURE__ */ jsx(DropdownMenu.Portal, { children: /* @__PURE__ */ jsx(
106
+ DropdownMenu.Content,
107
+ {
108
+ side,
109
+ align: "start",
110
+ avoidCollisions,
111
+ collisionPadding: 24,
112
+ sideOffset: 6,
113
+ className: cn(
114
+ // Cap to the viewport space on the open side and scroll, so the menu
115
+ // pinned downward (floating composer) never runs off the bottom edge.
116
+ "z-50 w-64 max-h-[var(--radix-dropdown-menu-content-available-height)] overflow-y-auto rounded-[var(--radius-md)] border border-[var(--md3-outline-variant)] bg-surface-container-highest p-1",
117
+ "shadow-[0_8px_30px_rgba(0,0,0,0.45)] ring-1 ring-[#ffffff14]",
118
+ "data-[state=open]:animate-in data-[state=closed]:animate-out",
119
+ "data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
120
+ "data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95"
121
+ ),
122
+ children: shown.map((option) => /* @__PURE__ */ jsxs(
123
+ DropdownMenu.Item,
124
+ {
125
+ onSelect: () => onChange(option.value),
126
+ className: cn(
127
+ "flex cursor-pointer items-start gap-2.5 rounded-md px-2.5 py-2 outline-none",
128
+ "transition-colors hover:bg-accent/40 focus:bg-accent/40",
129
+ option.value === value && "bg-[var(--accent-surface-soft)] text-[var(--accent-text)]"
130
+ ),
131
+ children: [
132
+ /* @__PURE__ */ jsx("span", { className: "mt-0.5 flex w-3.5 shrink-0 justify-center", children: /* @__PURE__ */ jsx(ReasoningGlyph, { level: option.value }) }),
133
+ /* @__PURE__ */ jsxs("span", { className: "flex min-w-0 flex-col gap-0.5", children: [
134
+ /* @__PURE__ */ jsx("span", { className: "text-sm font-medium", children: option.label }),
135
+ /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: option.description })
136
+ ] })
137
+ ]
138
+ },
139
+ option.value
140
+ ))
141
+ }
142
+ ) })
143
+ ] });
144
+ }
145
+
146
+ // src/chat/agent-session-controls.tsx
147
+ import { reasoningEffortsFor } from "@tangle-network/agent-interface";
148
+ import * as DropdownMenu2 from "@radix-ui/react-dropdown-menu";
149
+ import { ChevronDown as ChevronDown3, Lock, SlidersHorizontal } from "lucide-react";
150
+
151
+ // src/chat/harness-model-compat.ts
152
+ import {
153
+ harnessSupportsModel,
154
+ snapModelToHarness as snapModelIdToHarness
155
+ } from "@tangle-network/agent-interface";
156
+ import { modelProvider, snapHarnessToModel } from "@tangle-network/agent-interface";
157
+ function isModelCompatibleWithHarness(harness, modelId) {
158
+ return harnessSupportsModel(harness, modelId);
159
+ }
160
+ function snapModelToHarness(harness, modelId, models) {
161
+ return snapModelIdToHarness(harness, modelId, models.map(canonicalModelId));
162
+ }
163
+
164
+ // src/chat/agent-profile-picker.tsx
165
+ import { Bot, Check, ChevronDown as ChevronDown2, Pencil, Plus, Trash2 } from "lucide-react";
166
+ import * as React from "react";
167
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
168
+ function useClickOutside(onOutside) {
169
+ const ref = React.useRef(null);
170
+ React.useEffect(() => {
171
+ function handler(event) {
172
+ if (ref.current && !ref.current.contains(event.target)) {
173
+ onOutside();
174
+ }
175
+ }
176
+ document.addEventListener("mousedown", handler);
177
+ return () => document.removeEventListener("mousedown", handler);
178
+ }, [onOutside]);
179
+ return ref;
180
+ }
181
+ function profileSubtitle(profile) {
182
+ if (profile.description) return profile.description;
183
+ const count = profile.capabilities?.length ?? 0;
184
+ return `${count} ${count === 1 ? "tool group" : "tool groups"}`;
185
+ }
186
+ function AgentProfilePicker({
187
+ value,
188
+ onChange,
189
+ profiles,
190
+ capabilities,
191
+ onCreate,
192
+ onUpdate,
193
+ onDelete,
194
+ disabled,
195
+ className,
196
+ side = "top"
197
+ }) {
198
+ const [open, setOpen] = React.useState(false);
199
+ const [composing, setComposing] = React.useState(
200
+ null
201
+ );
202
+ const ref = useClickOutside(() => {
203
+ setOpen(false);
204
+ setComposing(null);
205
+ });
206
+ const canAuthor = Boolean(capabilities && (onCreate || onUpdate));
207
+ const builtins = profiles.filter((profile) => profile.builtin);
208
+ const custom = profiles.filter((profile) => !profile.builtin);
209
+ const selected = profiles.find((profile) => profile.id === value);
210
+ function select(id) {
211
+ onChange(id);
212
+ setOpen(false);
213
+ }
214
+ async function save(draft) {
215
+ if (draft.id) await onUpdate?.(draft.id, draft);
216
+ else await onCreate?.(draft);
217
+ setComposing(null);
218
+ }
219
+ return /* @__PURE__ */ jsxs2("div", { ref, className: cn("relative inline-flex", className), children: [
220
+ /* @__PURE__ */ jsxs2(
221
+ "button",
222
+ {
223
+ type: "button",
224
+ disabled,
225
+ onClick: () => setOpen((v) => !v),
226
+ className: cn(
227
+ "inline-flex h-8 items-center gap-1.5 rounded-lg border border-[var(--md3-outline-variant)] bg-surface-container px-2.5",
228
+ "text-xs font-medium text-foreground shadow-sm transition-colors",
229
+ "hover:border-[var(--md3-outline-variant)] hover:bg-surface-container-high focus:outline-none focus-visible:ring-1 focus-visible:ring-ring",
230
+ "data-[state=open]:border-[var(--md3-outline-variant)] data-[state=open]:bg-surface-container-high",
231
+ "disabled:cursor-not-allowed disabled:opacity-60"
232
+ ),
233
+ "data-state": open ? "open" : "closed",
234
+ "aria-label": "Agent profile",
235
+ children: [
236
+ /* @__PURE__ */ jsx2(Bot, { className: "h-3.5 w-3.5 text-muted-foreground" }),
237
+ /* @__PURE__ */ jsx2("span", { className: "max-w-[140px] truncate", children: selected?.name ?? value }),
238
+ /* @__PURE__ */ jsx2(ChevronDown2, { className: "h-3.5 w-3.5 text-muted-foreground" })
239
+ ]
240
+ }
241
+ ),
242
+ open && /* @__PURE__ */ jsx2(
243
+ "div",
244
+ {
245
+ className: cn(
246
+ "absolute left-0 z-50 w-[320px] overflow-hidden rounded-[var(--radius-md)] border border-[var(--md3-outline-variant)] bg-surface-container-highest p-1",
247
+ "shadow-[0_8px_30px_rgba(0,0,0,0.45)] ring-1 ring-[#ffffff14]",
248
+ side === "top" ? "bottom-full mb-2" : "top-full mt-2"
249
+ ),
250
+ children: composing ? /* @__PURE__ */ jsx2(
251
+ ProfileForm,
252
+ {
253
+ capabilities: capabilities ?? [],
254
+ initial: composing === "new" ? void 0 : composing,
255
+ onCancel: () => setComposing(null),
256
+ onSave: save
257
+ }
258
+ ) : /* @__PURE__ */ jsxs2("div", { className: "max-h-[min(60vh,420px)] overflow-y-auto", children: [
259
+ /* @__PURE__ */ jsx2(SectionHeader, { children: "Modes" }),
260
+ builtins.map((profile) => /* @__PURE__ */ jsx2(
261
+ ProfileRow,
262
+ {
263
+ active: profile.id === value,
264
+ title: profile.name,
265
+ subtitle: profileSubtitle(profile),
266
+ onSelect: () => select(profile.id)
267
+ },
268
+ profile.id
269
+ )),
270
+ custom.length > 0 && /* @__PURE__ */ jsx2(SectionHeader, { children: "Your agents" }),
271
+ custom.map((profile) => /* @__PURE__ */ jsx2(
272
+ ProfileRow,
273
+ {
274
+ active: profile.id === value,
275
+ title: profile.name,
276
+ subtitle: profileSubtitle(profile),
277
+ onSelect: () => select(profile.id),
278
+ onEdit: canAuthor ? () => setComposing(profile) : void 0,
279
+ onDelete: onDelete ? () => void onDelete(profile.id) : void 0
280
+ },
281
+ profile.id
282
+ )),
283
+ canAuthor && /* @__PURE__ */ jsxs2(
284
+ "button",
285
+ {
286
+ type: "button",
287
+ onClick: () => setComposing("new"),
288
+ className: "mt-1 flex w-full items-center gap-2 rounded-md px-2.5 py-2 text-left text-sm text-[var(--accent-text)] transition-colors hover:bg-accent/40",
289
+ children: [
290
+ /* @__PURE__ */ jsx2(Plus, { className: "h-4 w-4" }),
291
+ " New agent\u2026"
292
+ ]
293
+ }
294
+ )
295
+ ] })
296
+ }
297
+ )
298
+ ] });
299
+ }
300
+ function SectionHeader({ children }) {
301
+ return /* @__PURE__ */ jsx2("div", { className: "px-2.5 pb-1 pt-2 text-[10px] font-semibold uppercase tracking-wide text-muted-foreground/70", children });
302
+ }
303
+ function ProfileRow({
304
+ active,
305
+ title,
306
+ subtitle,
307
+ onSelect,
308
+ onEdit,
309
+ onDelete
310
+ }) {
311
+ return /* @__PURE__ */ jsxs2(
312
+ "div",
313
+ {
314
+ className: cn(
315
+ "group flex items-center gap-2 rounded-md px-2.5 py-2 text-sm transition-colors",
316
+ active ? "bg-[var(--accent-surface-soft)] text-[var(--accent-text)]" : "hover:bg-accent/40"
317
+ ),
318
+ children: [
319
+ /* @__PURE__ */ jsxs2(
320
+ "button",
321
+ {
322
+ type: "button",
323
+ onClick: onSelect,
324
+ className: "flex min-w-0 flex-1 items-center gap-2 text-left",
325
+ children: [
326
+ /* @__PURE__ */ jsx2("span", { className: "flex h-4 w-4 shrink-0 items-center justify-center", children: active && /* @__PURE__ */ jsx2(Check, { className: "h-3.5 w-3.5" }) }),
327
+ /* @__PURE__ */ jsxs2("span", { className: "min-w-0", children: [
328
+ /* @__PURE__ */ jsx2("span", { className: "block truncate font-medium", children: title }),
329
+ /* @__PURE__ */ jsx2("span", { className: "block truncate text-xs text-muted-foreground", children: subtitle })
330
+ ] })
331
+ ]
332
+ }
333
+ ),
334
+ (onEdit || onDelete) && /* @__PURE__ */ jsxs2("span", { className: "flex shrink-0 items-center gap-1 opacity-0 transition-opacity group-hover:opacity-100", children: [
335
+ onEdit && /* @__PURE__ */ jsx2(
336
+ "button",
337
+ {
338
+ type: "button",
339
+ onClick: onEdit,
340
+ title: "Edit",
341
+ className: "rounded p-1 text-muted-foreground hover:text-foreground",
342
+ children: /* @__PURE__ */ jsx2(Pencil, { className: "h-3 w-3" })
343
+ }
344
+ ),
345
+ onDelete && /* @__PURE__ */ jsx2(
346
+ "button",
347
+ {
348
+ type: "button",
349
+ onClick: onDelete,
350
+ title: "Delete",
351
+ className: "rounded p-1 text-muted-foreground hover:text-[var(--status-error,#ff4d6d)]",
352
+ children: /* @__PURE__ */ jsx2(Trash2, { className: "h-3 w-3" })
353
+ }
354
+ )
355
+ ] })
356
+ ]
357
+ }
358
+ );
359
+ }
360
+ function ProfileForm({
361
+ capabilities,
362
+ initial,
363
+ onCancel,
364
+ onSave
365
+ }) {
366
+ const [name, setName] = React.useState(initial?.name ?? "");
367
+ const [caps, setCaps] = React.useState(
368
+ initial?.capabilities ? [...initial.capabilities] : capabilities.map((c) => c.id)
369
+ );
370
+ const [instructions, setInstructions] = React.useState(initial?.instructions ?? "");
371
+ const toggle = (id) => setCaps(
372
+ (prev) => prev.includes(id) ? prev.filter((c) => c !== id) : [...prev, id]
373
+ );
374
+ return /* @__PURE__ */ jsxs2("div", { className: "space-y-3 p-2.5", children: [
375
+ /* @__PURE__ */ jsx2(
376
+ "input",
377
+ {
378
+ autoFocus: true,
379
+ value: name,
380
+ onChange: (event) => setName(event.target.value),
381
+ placeholder: "Agent name (e.g. Tax Reviewer)",
382
+ className: "w-full rounded-lg border border-[var(--md3-outline-variant)] bg-surface-container px-3 py-2 text-sm outline-none focus:border-primary"
383
+ }
384
+ ),
385
+ capabilities.length > 0 && /* @__PURE__ */ jsxs2("div", { children: [
386
+ /* @__PURE__ */ jsx2("p", { className: "mb-1.5 text-[10px] font-semibold uppercase tracking-wide text-muted-foreground/70", children: "Tools" }),
387
+ /* @__PURE__ */ jsx2("div", { className: "space-y-0.5", children: capabilities.map((cap) => /* @__PURE__ */ jsxs2(
388
+ "label",
389
+ {
390
+ className: "flex cursor-pointer items-start gap-2 rounded-md px-2 py-1.5 hover:bg-accent/40",
391
+ children: [
392
+ /* @__PURE__ */ jsx2(
393
+ "input",
394
+ {
395
+ type: "checkbox",
396
+ checked: caps.includes(cap.id),
397
+ onChange: () => toggle(cap.id),
398
+ className: "mt-0.5"
399
+ }
400
+ ),
401
+ /* @__PURE__ */ jsxs2("span", { className: "min-w-0", children: [
402
+ /* @__PURE__ */ jsx2("span", { className: "block text-sm font-medium", children: cap.label }),
403
+ cap.description && /* @__PURE__ */ jsx2("span", { className: "block text-xs text-muted-foreground", children: cap.description })
404
+ ] })
405
+ ]
406
+ },
407
+ cap.id
408
+ )) })
409
+ ] }),
410
+ /* @__PURE__ */ jsx2(
411
+ "textarea",
412
+ {
413
+ value: instructions,
414
+ onChange: (event) => setInstructions(event.target.value),
415
+ rows: 2,
416
+ placeholder: "Instructions (optional) \u2014 how this agent should behave",
417
+ className: "w-full resize-none rounded-lg border border-[var(--md3-outline-variant)] bg-surface-container px-3 py-2 text-sm outline-none focus:border-primary"
418
+ }
419
+ ),
420
+ /* @__PURE__ */ jsxs2("div", { className: "flex justify-end gap-2", children: [
421
+ /* @__PURE__ */ jsx2(
422
+ "button",
423
+ {
424
+ type: "button",
425
+ onClick: onCancel,
426
+ className: "rounded-lg px-3 py-1.5 text-sm text-muted-foreground hover:bg-accent/40",
427
+ children: "Cancel"
428
+ }
429
+ ),
430
+ /* @__PURE__ */ jsx2(
431
+ "button",
432
+ {
433
+ type: "button",
434
+ disabled: !name.trim(),
435
+ onClick: () => onSave({
436
+ id: initial?.id,
437
+ name: name.trim(),
438
+ capabilities: caps,
439
+ instructions: instructions.trim()
440
+ }),
441
+ className: "rounded-lg bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground transition-opacity hover:opacity-90 disabled:opacity-40",
442
+ children: "Save agent"
443
+ }
444
+ )
445
+ ] })
446
+ ] });
447
+ }
448
+
449
+ // src/chat/agent-session-controls.tsx
450
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
451
+ function HarnessDropdown({
452
+ value,
453
+ onChange,
454
+ available,
455
+ disabled,
456
+ locked,
457
+ lockReason,
458
+ side = "bottom",
459
+ align = "start",
460
+ avoidCollisions
461
+ }) {
462
+ const allowed = new Set(
463
+ available ?? HARNESS_OPTIONS.map((h) => h.type)
464
+ );
465
+ const options = HARNESS_OPTIONS.filter((h) => allowed.has(h.type));
466
+ const selected = options.find((option) => option.type === value);
467
+ return /* @__PURE__ */ jsxs3(DropdownMenu2.Root, { children: [
468
+ /* @__PURE__ */ jsx3(DropdownMenu2.Trigger, { asChild: true, children: /* @__PURE__ */ jsxs3(
469
+ "button",
470
+ {
471
+ type: "button",
472
+ disabled: disabled || locked,
473
+ title: locked ? lockReason : void 0,
474
+ className: cn(
475
+ "inline-flex h-8 items-center gap-1.5 rounded-lg border border-[var(--md3-outline-variant)] bg-surface-container px-2.5",
476
+ "text-xs font-medium text-foreground shadow-sm transition-colors",
477
+ "hover:border-[var(--md3-outline-variant)] hover:bg-surface-container-high focus:outline-none focus-visible:ring-1 focus-visible:ring-ring",
478
+ "data-[state=open]:border-[var(--md3-outline-variant)] data-[state=open]:bg-surface-container-high",
479
+ "disabled:cursor-not-allowed disabled:opacity-60"
480
+ ),
481
+ "aria-label": "Agent harness",
482
+ children: [
483
+ locked ? /* @__PURE__ */ jsx3(Lock, { className: "h-3 w-3 text-muted-foreground" }) : /* @__PURE__ */ jsx3(HarnessLogo, { type: value, size: 16 }),
484
+ /* @__PURE__ */ jsx3("span", { children: selected?.label ?? value }),
485
+ !locked && /* @__PURE__ */ jsx3(ChevronDown3, { className: "h-3.5 w-3.5 text-muted-foreground" })
486
+ ]
487
+ }
488
+ ) }),
489
+ /* @__PURE__ */ jsx3(DropdownMenu2.Portal, { children: /* @__PURE__ */ jsx3(
490
+ DropdownMenu2.Content,
491
+ {
492
+ side,
493
+ align,
494
+ avoidCollisions,
495
+ collisionPadding: 24,
496
+ sideOffset: 6,
497
+ className: cn(
498
+ // Cap to the viewport space on the open side and scroll, so a tall
499
+ // backend list pinned downward (floating composer) never runs off
500
+ // the bottom edge.
501
+ "z-50 w-72 max-h-[var(--radix-dropdown-menu-content-available-height)] overflow-y-auto rounded-[var(--radius-md)] border border-[var(--md3-outline-variant)] bg-surface-container-highest p-1",
502
+ "shadow-[0_8px_30px_rgba(0,0,0,0.45)] ring-1 ring-[#ffffff14]",
503
+ "data-[state=open]:animate-in data-[state=closed]:animate-out",
504
+ "data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
505
+ "data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95"
506
+ ),
507
+ children: options.map((option) => /* @__PURE__ */ jsxs3(
508
+ DropdownMenu2.Item,
509
+ {
510
+ onSelect: (event) => {
511
+ event.preventDefault();
512
+ onChange(option.type);
513
+ },
514
+ className: cn(
515
+ "flex cursor-pointer items-start gap-2.5 rounded-md px-2.5 py-2 outline-none",
516
+ "transition-colors hover:bg-accent/50 focus:bg-accent/50",
517
+ option.type === value && "bg-primary/10 font-medium text-foreground ring-1 ring-inset ring-primary/25 hover:bg-primary/15 focus:bg-primary/15"
518
+ ),
519
+ children: [
520
+ /* @__PURE__ */ jsx3(HarnessLogo, { type: option.type, size: 20, className: "mt-0.5" }),
521
+ /* @__PURE__ */ jsxs3("span", { className: "flex min-w-0 flex-col gap-0.5", children: [
522
+ /* @__PURE__ */ jsx3("span", { className: "text-sm font-medium", children: option.label }),
523
+ option.description && /* @__PURE__ */ jsx3("span", { className: "text-xs text-muted-foreground", children: option.description })
524
+ ] })
525
+ ]
526
+ },
527
+ option.type
528
+ ))
529
+ }
530
+ ) })
531
+ ] });
532
+ }
533
+ function AgentSessionControls({
534
+ harness,
535
+ profile,
536
+ model,
537
+ reasoning,
538
+ trailing,
539
+ className,
540
+ context = "chat",
541
+ layout = "inline",
542
+ menuPlacement = "auto"
543
+ }) {
544
+ if (!harness && !profile && !model && !reasoning && !trailing) return null;
545
+ const handleHarnessChange = (next) => {
546
+ harness?.onChange(next);
547
+ if (model) {
548
+ const snapped = snapModelToHarness(next, model.value, model.models);
549
+ if (snapped !== model.value) model.onChange(snapped);
550
+ }
551
+ };
552
+ const handleModelChange = (nextModelId) => {
553
+ model?.onChange(nextModelId);
554
+ if (harness && !harness.locked) {
555
+ const snapped = snapHarnessToModel(harness.value, nextModelId);
556
+ if (snapped !== harness.value) harness.onChange(snapped);
557
+ }
558
+ };
559
+ const visibleModels = model && harness?.locked ? model.models.filter(
560
+ (entry) => isModelCompatibleWithHarness(
561
+ harness.value,
562
+ canonicalModelId(entry)
563
+ )
564
+ ) : model?.models;
565
+ const harnessAvailable = harness?.available ?? (context === "chat" ? chatCapableHarnesses : void 0);
566
+ const menuSide = layout === "gear" ? "left" : "bottom";
567
+ const menuAlign = layout === "gear" ? "end" : "start";
568
+ const inlineForceDown = menuPlacement === "down" && layout !== "gear";
569
+ const avoidCollisions = inlineForceDown ? false : void 0;
570
+ const harnessNode = harness && /* @__PURE__ */ jsx3(
571
+ HarnessDropdown,
572
+ {
573
+ ...harness,
574
+ available: harnessAvailable,
575
+ onChange: handleHarnessChange,
576
+ side: menuSide,
577
+ align: menuAlign,
578
+ avoidCollisions
579
+ }
580
+ );
581
+ const profileNode = profile && /* @__PURE__ */ jsx3(
582
+ AgentProfilePicker,
583
+ {
584
+ value: profile.value,
585
+ onChange: profile.onChange,
586
+ profiles: profile.profiles,
587
+ capabilities: profile.capabilities,
588
+ onCreate: profile.onCreate,
589
+ onUpdate: profile.onUpdate,
590
+ onDelete: profile.onDelete,
591
+ disabled: profile.disabled,
592
+ side: inlineForceDown || layout === "gear" ? "bottom" : "top"
593
+ }
594
+ );
595
+ const modelNode = model && /* @__PURE__ */ jsx3(
596
+ ModelPicker,
597
+ {
598
+ variant: "pill",
599
+ label: "",
600
+ value: model.value,
601
+ onChange: handleModelChange,
602
+ models: visibleModels ?? [],
603
+ loading: model.loading,
604
+ popular: model.popular,
605
+ recents: model.recents,
606
+ disabled: model.disabled || (visibleModels ?? []).length === 0,
607
+ side: layout === "gear" ? void 0 : "bottom",
608
+ avoidCollisions
609
+ }
610
+ );
611
+ const selectedModel = model?.models.find(
612
+ (entry) => canonicalModelId(entry) === model.value
613
+ );
614
+ const modelReasoning = selectedModel ? {
615
+ supportsReasoning: selectedModel.supportsReasoning,
616
+ maxEffort: selectedModel.maxReasoningEffort
617
+ } : null;
618
+ const reasoningNode = reasoning && /* @__PURE__ */ jsx3(
619
+ ReasoningLevelPicker,
620
+ {
621
+ value: reasoning.value,
622
+ onChange: reasoning.onChange,
623
+ options: reasoning.options,
624
+ disabled: reasoning.disabled,
625
+ available: harness ? reasoningEffortsFor(harness.value, modelReasoning) : reasoning.available,
626
+ side: layout === "gear" ? void 0 : "bottom",
627
+ avoidCollisions
628
+ }
629
+ );
630
+ if (layout === "gear") {
631
+ return /* @__PURE__ */ jsx3(
632
+ GearControls,
633
+ {
634
+ className,
635
+ profileNode,
636
+ harnessNode,
637
+ modelNode,
638
+ reasoningNode,
639
+ trailing
640
+ }
641
+ );
642
+ }
643
+ return /* @__PURE__ */ jsxs3(
644
+ "div",
645
+ {
646
+ className: cn("flex flex-wrap items-center gap-2", className),
647
+ "data-testid": "agent-session-controls",
648
+ children: [
649
+ profileNode,
650
+ harnessNode,
651
+ modelNode,
652
+ reasoningNode,
653
+ trailing && /* @__PURE__ */ jsx3("div", { className: "ml-auto flex items-center gap-2", children: trailing })
654
+ ]
655
+ }
656
+ );
657
+ }
658
+ function GearControls({
659
+ className,
660
+ profileNode,
661
+ harnessNode,
662
+ modelNode,
663
+ reasoningNode,
664
+ trailing
665
+ }) {
666
+ return /* @__PURE__ */ jsxs3(
667
+ "div",
668
+ {
669
+ className: cn("flex items-center gap-2", className),
670
+ "data-testid": "agent-session-controls",
671
+ children: [
672
+ trailing && /* @__PURE__ */ jsx3("div", { className: "flex items-center gap-2", children: trailing }),
673
+ /* @__PURE__ */ jsxs3(DropdownMenu2.Root, { children: [
674
+ /* @__PURE__ */ jsx3(DropdownMenu2.Trigger, { asChild: true, children: /* @__PURE__ */ jsx3(
675
+ "button",
676
+ {
677
+ type: "button",
678
+ "aria-label": "Session controls",
679
+ className: cn(
680
+ "inline-flex h-8 w-8 items-center justify-center rounded-lg border border-[var(--md3-outline-variant)] bg-surface-container",
681
+ "text-foreground shadow-sm transition-colors",
682
+ "hover:border-[var(--md3-outline-variant)] hover:bg-surface-container-high focus:outline-none focus-visible:ring-1 focus-visible:ring-ring",
683
+ "data-[state=open]:border-[var(--md3-outline-variant)] data-[state=open]:bg-surface-container-high"
684
+ ),
685
+ children: /* @__PURE__ */ jsx3(SlidersHorizontal, { className: "h-4 w-4 text-muted-foreground" })
686
+ }
687
+ ) }),
688
+ /* @__PURE__ */ jsx3(DropdownMenu2.Portal, { children: /* @__PURE__ */ jsxs3(
689
+ DropdownMenu2.Content,
690
+ {
691
+ side: "top",
692
+ align: "end",
693
+ sideOffset: 8,
694
+ onInteractOutside: (event) => {
695
+ const target = event.target;
696
+ if (target?.closest(
697
+ "[data-radix-popper-content-wrapper],[data-radix-menu-content],[role=menu],[role=listbox]"
698
+ )) {
699
+ event.preventDefault();
700
+ }
701
+ },
702
+ className: cn(
703
+ "z-50 flex w-56 flex-col gap-2 rounded-[var(--radius-md)] border border-[var(--md3-outline-variant)] bg-surface-container-highest p-2",
704
+ "shadow-[0_8px_30px_rgba(0,0,0,0.45)] ring-1 ring-[#ffffff14]",
705
+ "data-[state=open]:animate-in data-[state=closed]:animate-out",
706
+ "data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
707
+ "data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95"
708
+ ),
709
+ children: [
710
+ profileNode && /* @__PURE__ */ jsx3(GearSection, { label: "Agent", children: profileNode }),
711
+ harnessNode && /* @__PURE__ */ jsx3(GearSection, { label: "Harness", children: harnessNode }),
712
+ modelNode && /* @__PURE__ */ jsx3(GearSection, { label: "Model", children: modelNode }),
713
+ reasoningNode && /* @__PURE__ */ jsx3(GearSection, { label: "Effort", children: reasoningNode })
714
+ ]
715
+ }
716
+ ) })
717
+ ] })
718
+ ]
719
+ }
720
+ );
721
+ }
722
+ function GearSection({
723
+ label,
724
+ children
725
+ }) {
726
+ return /* @__PURE__ */ jsxs3("div", { className: "flex flex-col gap-1", children: [
727
+ /* @__PURE__ */ jsx3("span", { className: "px-0.5 text-[10px] font-medium uppercase tracking-wide text-muted-foreground/70", children: label }),
728
+ children
729
+ ] });
730
+ }
731
+
732
+ // src/chat/agent-composer.tsx
733
+ import {
734
+ ArrowUp,
735
+ Folder,
736
+ Loader2,
737
+ Paperclip,
738
+ Square,
739
+ Upload,
740
+ X
741
+ } from "lucide-react";
742
+ import * as React2 from "react";
743
+ import { Fragment, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
744
+ function AgentComposer({
745
+ value,
746
+ onChange,
747
+ onSubmit,
748
+ placeholder = "Send a message\u2026",
749
+ disabled,
750
+ busy,
751
+ onCancel,
752
+ harness,
753
+ profile,
754
+ model,
755
+ reasoning,
756
+ context = "chat",
757
+ controls,
758
+ trailing,
759
+ onAttach,
760
+ onAttachFolder,
761
+ attachments = [],
762
+ onRemoveFile,
763
+ accept,
764
+ dropTitle = "Drop files to add context",
765
+ dropDescription = "They attach to your next message.",
766
+ focusShortcut = false,
767
+ minRows = 2,
768
+ maxHeight = 200,
769
+ className,
770
+ sendLabel = "Send",
771
+ autoFocus
772
+ }) {
773
+ const textareaRef = React2.useRef(null);
774
+ const fileInputRef = React2.useRef(null);
775
+ const folderInputRef = React2.useRef(null);
776
+ const [dragOver, setDragOver] = React2.useState(false);
777
+ const dragDepth = React2.useRef(0);
778
+ const resize = React2.useCallback(
779
+ (el) => {
780
+ el.style.height = "auto";
781
+ el.style.height = `${Math.min(el.scrollHeight, maxHeight)}px`;
782
+ },
783
+ [maxHeight]
784
+ );
785
+ React2.useEffect(() => {
786
+ if (textareaRef.current) resize(textareaRef.current);
787
+ }, [value, resize]);
788
+ React2.useEffect(() => {
789
+ if (!focusShortcut || disabled) return;
790
+ function onKeyDown(event) {
791
+ if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "l") {
792
+ event.preventDefault();
793
+ textareaRef.current?.focus();
794
+ }
795
+ }
796
+ document.addEventListener("keydown", onKeyDown);
797
+ return () => document.removeEventListener("keydown", onKeyDown);
798
+ }, [focusShortcut, disabled]);
799
+ const canSend = !disabled && !busy && value.trim().length > 0;
800
+ const submit = () => {
801
+ if (!canSend) return;
802
+ onSubmit();
803
+ };
804
+ const dropEnabled = Boolean(onAttach);
805
+ const handleDragEnter = (event) => {
806
+ if (!dropEnabled) return;
807
+ event.preventDefault();
808
+ event.stopPropagation();
809
+ dragDepth.current++;
810
+ if (event.dataTransfer?.types.includes("Files")) setDragOver(true);
811
+ };
812
+ const handleDragLeave = (event) => {
813
+ if (!dropEnabled) return;
814
+ event.preventDefault();
815
+ event.stopPropagation();
816
+ dragDepth.current--;
817
+ if (dragDepth.current <= 0) {
818
+ dragDepth.current = 0;
819
+ setDragOver(false);
820
+ }
821
+ };
822
+ const handleDragOver = (event) => {
823
+ if (!dropEnabled) return;
824
+ event.preventDefault();
825
+ event.stopPropagation();
826
+ if (event.dataTransfer) event.dataTransfer.dropEffect = "copy";
827
+ };
828
+ const handleDrop = (event) => {
829
+ if (!dropEnabled) return;
830
+ event.preventDefault();
831
+ event.stopPropagation();
832
+ dragDepth.current = 0;
833
+ setDragOver(false);
834
+ const files = event.dataTransfer?.files;
835
+ if (files?.length) onAttach?.(files);
836
+ };
837
+ const folderChips = attachments.filter((file) => file.kind === "folder");
838
+ const fileChips = attachments.filter((file) => file.kind !== "folder");
839
+ const attachButtonClass = cn(
840
+ "flex size-8 shrink-0 items-center justify-center rounded-lg text-muted-foreground transition-colors",
841
+ "hover:bg-surface-container-highest hover:text-foreground focus:outline-none focus-visible:ring-1 focus-visible:ring-ring",
842
+ "disabled:cursor-not-allowed disabled:opacity-50"
843
+ );
844
+ return /* @__PURE__ */ jsxs4("div", { className: cn("relative", className), children: [
845
+ dragOver && /* @__PURE__ */ jsx4("div", { className: "pointer-events-none absolute inset-0 z-10 flex items-center justify-center rounded-2xl border-2 border-dashed border-primary/50 bg-surface-container-high/95", children: /* @__PURE__ */ jsxs4("div", { className: "text-center", children: [
846
+ /* @__PURE__ */ jsx4("span", { className: "mx-auto mb-2 flex size-11 items-center justify-center rounded-xl bg-primary/10 text-primary", children: /* @__PURE__ */ jsx4(Upload, { className: "size-5" }) }),
847
+ /* @__PURE__ */ jsx4("p", { className: "font-semibold text-foreground text-sm", children: dropTitle }),
848
+ /* @__PURE__ */ jsx4("p", { className: "mt-0.5 text-muted-foreground text-xs", children: dropDescription })
849
+ ] }) }),
850
+ /* @__PURE__ */ jsxs4(
851
+ "div",
852
+ {
853
+ "data-testid": "agent-composer",
854
+ onDragEnter: handleDragEnter,
855
+ onDragLeave: handleDragLeave,
856
+ onDragOver: handleDragOver,
857
+ onDrop: handleDrop,
858
+ className: cn(
859
+ "flex flex-col gap-2 rounded-2xl border border-[var(--md3-outline-variant)] bg-surface-container-high p-2.5 shadow-sm transition-colors",
860
+ "focus-within:border-primary/60",
861
+ disabled && "opacity-60"
862
+ ),
863
+ children: [
864
+ attachments.length > 0 && /* @__PURE__ */ jsx4("div", { className: "flex flex-wrap gap-1.5", children: [...folderChips, ...fileChips].map((file) => /* @__PURE__ */ jsxs4(
865
+ "span",
866
+ {
867
+ className: cn(
868
+ "inline-flex items-center gap-1.5 rounded-full border px-2.5 py-1 text-xs",
869
+ file.status === "error" ? "border-[var(--status-error,#ff4d6d)]/40 text-[var(--status-error,#ff4d6d)]" : "border-[var(--md3-outline-variant)] bg-surface-container text-foreground"
870
+ ),
871
+ children: [
872
+ file.kind === "folder" ? /* @__PURE__ */ jsx4(Folder, { className: "size-3 shrink-0" }) : /* @__PURE__ */ jsx4(Paperclip, { className: "size-3 shrink-0" }),
873
+ /* @__PURE__ */ jsx4("span", { className: "max-w-[150px] truncate", children: file.name }),
874
+ file.fileCount !== void 0 && /* @__PURE__ */ jsxs4("span", { className: "text-muted-foreground", children: [
875
+ "(",
876
+ file.fileCount,
877
+ ")"
878
+ ] }),
879
+ file.status === "uploading" && /* @__PURE__ */ jsx4(Loader2, { className: "size-3 animate-spin" }),
880
+ onRemoveFile && /* @__PURE__ */ jsx4(
881
+ "button",
882
+ {
883
+ type: "button",
884
+ "aria-label": `Remove ${file.name}`,
885
+ onClick: () => onRemoveFile(file.id),
886
+ className: "rounded p-0.5 text-muted-foreground transition-colors hover:text-foreground focus:outline-none focus-visible:ring-1 focus-visible:ring-ring",
887
+ children: /* @__PURE__ */ jsx4(X, { className: "size-3" })
888
+ }
889
+ )
890
+ ]
891
+ },
892
+ file.id
893
+ )) }),
894
+ /* @__PURE__ */ jsx4(
895
+ "textarea",
896
+ {
897
+ ref: textareaRef,
898
+ rows: minRows,
899
+ value,
900
+ disabled,
901
+ autoFocus,
902
+ onChange: (event) => onChange(event.target.value),
903
+ onInput: (event) => resize(event.currentTarget),
904
+ onKeyDown: (event) => {
905
+ if (event.key === "Enter" && !event.shiftKey && !event.nativeEvent.isComposing && event.keyCode !== 229) {
906
+ event.preventDefault();
907
+ submit();
908
+ }
909
+ },
910
+ placeholder,
911
+ "aria-label": "Message input",
912
+ className: cn(
913
+ "w-full resize-none bg-transparent px-2 py-1 text-sm leading-relaxed text-foreground outline-none",
914
+ "placeholder:text-muted-foreground"
915
+ )
916
+ }
917
+ ),
918
+ /* @__PURE__ */ jsxs4("div", { className: "flex items-end gap-2", children: [
919
+ /* @__PURE__ */ jsx4("div", { className: "flex min-w-0 flex-1 items-center gap-2", children: controls ?? /* @__PURE__ */ jsx4(
920
+ AgentSessionControls,
921
+ {
922
+ context,
923
+ harness,
924
+ profile,
925
+ model,
926
+ reasoning
927
+ }
928
+ ) }),
929
+ onAttach && /* @__PURE__ */ jsxs4(Fragment, { children: [
930
+ /* @__PURE__ */ jsx4(
931
+ "button",
932
+ {
933
+ type: "button",
934
+ onClick: () => fileInputRef.current?.click(),
935
+ disabled,
936
+ "aria-label": "Attach files",
937
+ title: "Attach files",
938
+ className: attachButtonClass,
939
+ children: /* @__PURE__ */ jsx4(Paperclip, { className: "size-4" })
940
+ }
941
+ ),
942
+ /* @__PURE__ */ jsx4(
943
+ "input",
944
+ {
945
+ ref: fileInputRef,
946
+ type: "file",
947
+ multiple: true,
948
+ className: "hidden",
949
+ accept,
950
+ onChange: (event) => {
951
+ if (event.target.files?.length) onAttach(event.target.files);
952
+ event.target.value = "";
953
+ }
954
+ }
955
+ )
956
+ ] }),
957
+ onAttachFolder && /* @__PURE__ */ jsxs4(Fragment, { children: [
958
+ /* @__PURE__ */ jsx4(
959
+ "button",
960
+ {
961
+ type: "button",
962
+ onClick: () => folderInputRef.current?.click(),
963
+ disabled,
964
+ "aria-label": "Attach folder",
965
+ title: "Attach folder",
966
+ className: attachButtonClass,
967
+ children: /* @__PURE__ */ jsx4(Folder, { className: "size-4" })
968
+ }
969
+ ),
970
+ /* @__PURE__ */ jsx4(
971
+ "input",
972
+ {
973
+ ref: folderInputRef,
974
+ type: "file",
975
+ multiple: true,
976
+ className: "hidden",
977
+ onChange: (event) => {
978
+ if (event.target.files?.length)
979
+ (onAttachFolder ?? onAttach)?.(event.target.files);
980
+ event.target.value = "";
981
+ },
982
+ ...{ webkitdirectory: "" }
983
+ }
984
+ )
985
+ ] }),
986
+ trailing && /* @__PURE__ */ jsx4("div", { className: "flex shrink-0 items-center gap-2", children: trailing }),
987
+ busy && onCancel ? /* @__PURE__ */ jsx4(
988
+ "button",
989
+ {
990
+ type: "button",
991
+ onClick: onCancel,
992
+ "aria-label": "Stop response",
993
+ className: cn(
994
+ "flex size-9 shrink-0 items-center justify-center rounded-full",
995
+ "bg-[var(--status-error,#ff4d6d)]/15 text-[var(--status-error,#ff4d6d)] transition-colors",
996
+ "hover:bg-[var(--status-error,#ff4d6d)]/25"
997
+ ),
998
+ children: /* @__PURE__ */ jsx4(Square, { className: "size-3.5 fill-current" })
999
+ }
1000
+ ) : /* @__PURE__ */ jsx4(
1001
+ "button",
1002
+ {
1003
+ type: "button",
1004
+ onClick: submit,
1005
+ disabled: !canSend,
1006
+ "aria-label": sendLabel,
1007
+ className: cn(
1008
+ "flex size-9 shrink-0 items-center justify-center rounded-full bg-primary text-primary-foreground",
1009
+ "transition-opacity hover:opacity-90 disabled:opacity-40"
1010
+ ),
1011
+ children: busy ? /* @__PURE__ */ jsx4(Loader2, { className: "size-4 animate-spin" }) : /* @__PURE__ */ jsx4(ArrowUp, { className: "size-4" })
1012
+ }
1013
+ )
1014
+ ] })
1015
+ ]
1016
+ }
1017
+ ),
1018
+ focusShortcut && /* @__PURE__ */ jsx4("div", { className: "mt-1.5 flex justify-end px-1", children: /* @__PURE__ */ jsxs4("span", { className: "text-muted-foreground text-xs", children: [
1019
+ /* @__PURE__ */ jsx4("kbd", { className: "rounded border border-[var(--md3-outline-variant)] bg-surface-container px-1 py-0.5 text-[10px]", children: "Cmd" }),
1020
+ /* @__PURE__ */ jsx4("kbd", { className: "ml-0.5 rounded border border-[var(--md3-outline-variant)] bg-surface-container px-1 py-0.5 text-[10px]", children: "L" }),
1021
+ /* @__PURE__ */ jsx4("span", { className: "ml-1", children: "to focus" })
1022
+ ] }) })
1023
+ ] });
1024
+ }
1025
+
1026
+ // src/chat/artifact-agent-dock.tsx
1027
+ import * as React3 from "react";
1028
+ import { Sparkles as Sparkles2, Send, Square as Square2, X as X2 } from "lucide-react";
1029
+ import { ChatMessage } from "@tangle-network/ui/chat";
1030
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
1031
+ function defaultScopeLabel(scope) {
1032
+ if (scope.label) return scope.label;
1033
+ switch (scope.kind) {
1034
+ case "vault-file":
1035
+ return scope.key.split("/").pop() ?? scope.key;
1036
+ case "sequence":
1037
+ return `Sequence ${scope.key.slice(0, 8)}`;
1038
+ case "content":
1039
+ return `Content ${scope.key.slice(0, 8)}`;
1040
+ case "image":
1041
+ return `Image ${scope.key.slice(0, 8)}`;
1042
+ case "video":
1043
+ return `Video ${scope.key.slice(0, 8)}`;
1044
+ case "project":
1045
+ return "Project chat";
1046
+ case "workspace":
1047
+ return "Workspace chat";
1048
+ default:
1049
+ return scope.key;
1050
+ }
1051
+ }
1052
+ function normalizeRole(role) {
1053
+ return role === "assistant" || role === "system" ? role : "user";
1054
+ }
1055
+ function createDockMessageId() {
1056
+ const c = typeof globalThis !== "undefined" ? globalThis.crypto : void 0;
1057
+ if (c && typeof c.randomUUID === "function") return c.randomUUID();
1058
+ if (c && typeof c.getRandomValues === "function") {
1059
+ const b = c.getRandomValues(new Uint8Array(16));
1060
+ b[6] = b[6] & 15 | 64;
1061
+ b[8] = b[8] & 63 | 128;
1062
+ const h = Array.from(b, (x) => x.toString(16).padStart(2, "0")).join("");
1063
+ return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20)}`;
1064
+ }
1065
+ throw new Error("crypto.getRandomValues is required for message ids");
1066
+ }
1067
+ function ArtifactAgentDock({
1068
+ scope,
1069
+ open,
1070
+ onOpenChange,
1071
+ transport,
1072
+ defaultPrompt,
1073
+ onError,
1074
+ className
1075
+ }) {
1076
+ const [threadId, setThreadId] = React3.useState(null);
1077
+ const [messages, setMessages] = React3.useState([]);
1078
+ const [composer, setComposer] = React3.useState(defaultPrompt ?? "");
1079
+ const [streamContent, setStreamContent] = React3.useState("");
1080
+ const [sending, setSending] = React3.useState(false);
1081
+ const [loading, setLoading] = React3.useState(false);
1082
+ const abortRef = React3.useRef(null);
1083
+ const scrollRef = React3.useRef(null);
1084
+ const scopeTokenRef = React3.useRef(0);
1085
+ const reportError = React3.useCallback(
1086
+ (message) => {
1087
+ if (onError) onError(message);
1088
+ else if (typeof console !== "undefined") console.error("[ArtifactAgentDock]", message);
1089
+ },
1090
+ [onError]
1091
+ );
1092
+ React3.useEffect(() => {
1093
+ if (!open) return;
1094
+ scopeTokenRef.current += 1;
1095
+ abortRef.current?.abort();
1096
+ abortRef.current = null;
1097
+ let cancelled = false;
1098
+ async function bootstrap() {
1099
+ setLoading(true);
1100
+ try {
1101
+ const { id } = await transport.ensureThread(scope);
1102
+ if (cancelled) return;
1103
+ setThreadId(id);
1104
+ if (transport.loadMessages) {
1105
+ const prior = await transport.loadMessages(id);
1106
+ if (cancelled) return;
1107
+ setMessages(
1108
+ prior.map((m) => ({
1109
+ ...m,
1110
+ role: normalizeRole(m.role),
1111
+ createdAt: m.createdAt instanceof Date ? m.createdAt : m.createdAt ? new Date(m.createdAt) : void 0
1112
+ }))
1113
+ );
1114
+ } else {
1115
+ setMessages([]);
1116
+ }
1117
+ } catch (err) {
1118
+ reportError(err instanceof Error ? err.message : "Failed to open artifact chat");
1119
+ } finally {
1120
+ if (!cancelled) setLoading(false);
1121
+ }
1122
+ }
1123
+ void bootstrap();
1124
+ return () => {
1125
+ cancelled = true;
1126
+ };
1127
+ }, [open, scope.kind, scope.key, transport, reportError]);
1128
+ React3.useEffect(() => {
1129
+ if (!scrollRef.current) return;
1130
+ scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
1131
+ }, [messages.length, streamContent]);
1132
+ const send = React3.useCallback(async () => {
1133
+ const text = composer.trim();
1134
+ if (!text || !threadId || sending) return;
1135
+ const token = scopeTokenRef.current;
1136
+ const userMsg = {
1137
+ id: createDockMessageId(),
1138
+ role: "user",
1139
+ content: text,
1140
+ createdAt: /* @__PURE__ */ new Date()
1141
+ };
1142
+ setMessages((prev) => [...prev, userMsg]);
1143
+ setComposer("");
1144
+ setSending(true);
1145
+ setStreamContent("");
1146
+ const controller = new AbortController();
1147
+ abortRef.current = controller;
1148
+ let assistantContent = "";
1149
+ let streamFailed = false;
1150
+ try {
1151
+ for await (const event of transport.sendStream({ threadId, content: text, signal: controller.signal })) {
1152
+ if (scopeTokenRef.current !== token) return;
1153
+ if (event.type === "delta") {
1154
+ assistantContent += event.text;
1155
+ setStreamContent(assistantContent);
1156
+ } else if (event.type === "error") {
1157
+ streamFailed = true;
1158
+ reportError(event.message);
1159
+ }
1160
+ }
1161
+ if (scopeTokenRef.current !== token) return;
1162
+ if (assistantContent && !streamFailed) {
1163
+ setMessages((prev) => [
1164
+ ...prev,
1165
+ {
1166
+ id: createDockMessageId(),
1167
+ role: "assistant",
1168
+ content: assistantContent,
1169
+ createdAt: /* @__PURE__ */ new Date()
1170
+ }
1171
+ ]);
1172
+ }
1173
+ setStreamContent("");
1174
+ } catch (err) {
1175
+ if (err?.name === "AbortError") return;
1176
+ if (scopeTokenRef.current === token) reportError(err instanceof Error ? err.message : "Send failed");
1177
+ } finally {
1178
+ if (scopeTokenRef.current === token) setSending(false);
1179
+ if (abortRef.current === controller) abortRef.current = null;
1180
+ }
1181
+ }, [composer, threadId, sending, transport, reportError]);
1182
+ const cancel = React3.useCallback(() => {
1183
+ if (threadId && transport.cancel) {
1184
+ try {
1185
+ void Promise.resolve(transport.cancel({ threadId })).catch(() => {
1186
+ });
1187
+ } catch {
1188
+ }
1189
+ }
1190
+ abortRef.current?.abort();
1191
+ setSending(false);
1192
+ setStreamContent("");
1193
+ }, [threadId, transport]);
1194
+ if (!open) return null;
1195
+ const heading = defaultScopeLabel(scope);
1196
+ return /* @__PURE__ */ jsxs5(
1197
+ "aside",
1198
+ {
1199
+ className: cn(
1200
+ "flex h-full w-[420px] min-w-[360px] max-w-[480px] flex-col border-l border-[var(--md3-outline-variant)] bg-surface-container shadow-[var(--shadow-dropdown)]",
1201
+ className
1202
+ ),
1203
+ "aria-label": `Agent chat about ${heading}`,
1204
+ children: [
1205
+ /* @__PURE__ */ jsxs5("header", { className: "flex items-center justify-between border-b border-[var(--md3-outline-variant)] bg-surface-container-low px-4 py-3", children: [
1206
+ /* @__PURE__ */ jsxs5("div", { className: "flex min-w-0 items-center gap-2", children: [
1207
+ /* @__PURE__ */ jsx5(Sparkles2, { className: "h-4 w-4 shrink-0 text-muted-foreground" }),
1208
+ /* @__PURE__ */ jsxs5("div", { className: "min-w-0", children: [
1209
+ /* @__PURE__ */ jsx5("div", { className: "text-[10px] font-medium uppercase tracking-wider text-muted-foreground", children: "Discussing" }),
1210
+ /* @__PURE__ */ jsx5("div", { className: "truncate text-sm font-semibold text-foreground", children: heading })
1211
+ ] })
1212
+ ] }),
1213
+ /* @__PURE__ */ jsx5(
1214
+ "button",
1215
+ {
1216
+ type: "button",
1217
+ onClick: () => onOpenChange(false),
1218
+ "aria-label": "Close artifact chat",
1219
+ className: "rounded-md p-1 text-muted-foreground transition-colors hover:bg-surface-container-high hover:text-foreground",
1220
+ children: /* @__PURE__ */ jsx5(X2, { className: "h-4 w-4" })
1221
+ }
1222
+ )
1223
+ ] }),
1224
+ /* @__PURE__ */ jsx5("div", { ref: scrollRef, className: "flex-1 overflow-y-auto px-3 py-4", children: loading ? /* @__PURE__ */ jsx5("div", { className: "px-3 py-8 text-center text-xs text-muted-foreground", children: "Loading conversation\u2026" }) : messages.length === 0 && !streamContent ? /* @__PURE__ */ jsxs5("div", { className: "px-3 py-12 text-center", children: [
1225
+ /* @__PURE__ */ jsx5(Sparkles2, { className: "mx-auto mb-3 h-5 w-5 text-muted-foreground/40" }),
1226
+ /* @__PURE__ */ jsxs5("p", { className: "text-sm text-foreground", children: [
1227
+ "Ask the agent about this ",
1228
+ scope.kind === "vault-file" ? "document" : scope.kind,
1229
+ "."
1230
+ ] }),
1231
+ /* @__PURE__ */ jsx5("p", { className: "mt-1 text-xs text-muted-foreground", children: "It can read the current contents and propose edits, rewrites, or follow-ups." })
1232
+ ] }) : /* @__PURE__ */ jsxs5("div", { className: "space-y-3", children: [
1233
+ messages.map((m) => /* @__PURE__ */ jsx5(ChatMessage, { role: m.role, content: m.content, timestamp: m.createdAt }, m.id)),
1234
+ streamContent && /* @__PURE__ */ jsx5(ChatMessage, { role: "assistant", content: streamContent, isStreaming: true })
1235
+ ] }) }),
1236
+ /* @__PURE__ */ jsx5(
1237
+ "form",
1238
+ {
1239
+ className: "border-t border-[var(--md3-outline-variant)] bg-surface-container-low p-3",
1240
+ onSubmit: (e) => {
1241
+ e.preventDefault();
1242
+ void send();
1243
+ },
1244
+ children: /* @__PURE__ */ jsxs5("div", { className: "flex items-end gap-2", children: [
1245
+ /* @__PURE__ */ jsx5(
1246
+ "textarea",
1247
+ {
1248
+ value: composer,
1249
+ onChange: (e) => setComposer(e.target.value),
1250
+ onKeyDown: (e) => {
1251
+ if (e.key === "Enter" && !e.shiftKey && !sending) {
1252
+ e.preventDefault();
1253
+ void send();
1254
+ }
1255
+ },
1256
+ placeholder: `Ask about ${heading}\u2026`,
1257
+ rows: 2,
1258
+ disabled: !threadId,
1259
+ className: "min-h-[44px] flex-1 resize-none rounded-[20px] border border-[var(--md3-outline-variant)] bg-surface-dim px-3.5 py-2 text-sm text-foreground placeholder:text-muted-foreground transition-colors focus:outline-none focus:border-[var(--border-accent)] disabled:opacity-50"
1260
+ }
1261
+ ),
1262
+ sending ? /* @__PURE__ */ jsx5(
1263
+ "button",
1264
+ {
1265
+ type: "button",
1266
+ onClick: cancel,
1267
+ "aria-label": "Cancel",
1268
+ className: "inline-flex h-9 w-9 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-surface-container-high hover:text-foreground",
1269
+ children: /* @__PURE__ */ jsx5(Square2, { className: "h-4 w-4" })
1270
+ }
1271
+ ) : /* @__PURE__ */ jsx5(
1272
+ "button",
1273
+ {
1274
+ type: "submit",
1275
+ disabled: !composer.trim() || !threadId,
1276
+ "aria-label": "Send",
1277
+ className: "inline-flex h-9 w-9 items-center justify-center rounded-full bg-primary text-primary-foreground transition-colors hover:bg-primary/90 disabled:opacity-50",
1278
+ children: /* @__PURE__ */ jsx5(Send, { className: "h-4 w-4" })
1279
+ }
1280
+ )
1281
+ ] })
1282
+ }
1283
+ )
1284
+ ]
1285
+ }
1286
+ );
1287
+ }
1288
+ function createFetchTransport(opts) {
1289
+ const threadsUrl = opts.threadsUrl ?? "/api/threads";
1290
+ const chatUrl = opts.chatUrl ?? "/api/chat";
1291
+ const f = opts.fetchImpl ?? ((...args) => fetch(...args));
1292
+ const workspaceId = opts.workspaceId;
1293
+ return {
1294
+ async ensureThread(scope) {
1295
+ const url = threadsUrl.includes("?") ? `${threadsUrl}&workspaceId=${encodeURIComponent(workspaceId)}` : `${threadsUrl}?workspaceId=${encodeURIComponent(workspaceId)}`;
1296
+ const res = await f(url, {
1297
+ method: "POST",
1298
+ headers: { "Content-Type": "application/json" },
1299
+ body: JSON.stringify({ workspaceId, scope, ensureScoped: true })
1300
+ });
1301
+ if (!res.ok) throw new Error(`Thread ensure failed (${res.status})`);
1302
+ const data = await res.json();
1303
+ const id = data.thread?.id ?? data.id;
1304
+ if (!id) throw new Error("Malformed ensure response");
1305
+ return { id };
1306
+ },
1307
+ async loadMessages(threadId) {
1308
+ const res = await f(`${threadsUrl}/${encodeURIComponent(threadId)}/messages`);
1309
+ if (!res.ok) return [];
1310
+ const data = await res.json();
1311
+ return (data.messages ?? []).map((m) => ({
1312
+ id: m.id,
1313
+ role: normalizeRole(m.role),
1314
+ content: m.content,
1315
+ createdAt: new Date(m.createdAt)
1316
+ }));
1317
+ },
1318
+ sendStream({ threadId, content, signal }) {
1319
+ return (async function* () {
1320
+ const res = await f(chatUrl, {
1321
+ method: "POST",
1322
+ headers: { "Content-Type": "application/json" },
1323
+ body: JSON.stringify({ workspaceId, threadId, content }),
1324
+ signal
1325
+ });
1326
+ if (!res.ok || !res.body) {
1327
+ const text = await res.text().catch(() => "");
1328
+ yield { type: "error", message: text || `Send failed (${res.status})` };
1329
+ return;
1330
+ }
1331
+ const reader = res.body.getReader();
1332
+ const decoder = new TextDecoder();
1333
+ let buffer = "";
1334
+ while (true) {
1335
+ const { done, value } = await reader.read();
1336
+ if (done) break;
1337
+ buffer += decoder.decode(value, { stream: true });
1338
+ const lines = buffer.split("\n");
1339
+ buffer = lines.pop() ?? "";
1340
+ for (const line of lines) {
1341
+ if (!line.trim()) continue;
1342
+ try {
1343
+ const event = JSON.parse(line);
1344
+ if (event.type === "message.part.updated" && event.data) {
1345
+ const delta = event.data.delta;
1346
+ const part = event.data.part;
1347
+ const isText = !part || typeof part !== "object" || part.type === void 0 || part.type === "text";
1348
+ if (isText && typeof delta === "string" && delta.length > 0) {
1349
+ yield { type: "delta", text: delta };
1350
+ }
1351
+ } else if (event.type === "error" && event.data) {
1352
+ const message = typeof event.data.message === "string" ? event.data.message : "Stream error";
1353
+ yield { type: "error", message };
1354
+ }
1355
+ } catch {
1356
+ }
1357
+ }
1358
+ }
1359
+ })();
1360
+ }
1361
+ };
1362
+ }
1363
+
1364
+ export {
1365
+ DEFAULT_REASONING_LEVEL_OPTIONS,
1366
+ ReasoningLevelPicker,
1367
+ isModelCompatibleWithHarness,
1368
+ snapModelToHarness,
1369
+ modelProvider,
1370
+ snapHarnessToModel,
1371
+ AgentProfilePicker,
1372
+ AgentSessionControls,
1373
+ AgentComposer,
1374
+ ArtifactAgentDock,
1375
+ createFetchTransport,
1376
+ AgentTimeline,
1377
+ ChatContainer,
1378
+ ChatInput,
1379
+ ChatMessage2 as ChatMessage,
1380
+ MessageList,
1381
+ ThinkingIndicator,
1382
+ UserMessage
1383
+ };