@tangle-network/sandbox-ui 0.97.0 → 0.99.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.
@@ -1,29 +1,6 @@
1
1
  import {
2
2
  formatBytes
3
3
  } from "./chunk-TK46XFLM.js";
4
- import {
5
- HARNESS_OPTIONS,
6
- HarnessLogo,
7
- chatCapableHarnesses
8
- } from "./chunk-WSN2RZC6.js";
9
- import {
10
- ModelPicker,
11
- PickerMenuFooter,
12
- PickerMenuMeta,
13
- PickerMenuSearch,
14
- PickerMenuSection,
15
- PickerMenuTag,
16
- isTextChatModel,
17
- pickerMenuBodyClass,
18
- pickerMenuContentClass,
19
- pickerMenuItemClass,
20
- stripBrandPrefix
21
- } from "./chunk-WMVUJ5SA.js";
22
- import {
23
- ModelBrandStack,
24
- canonicalModelId,
25
- resolveModelBrandIdentity
26
- } from "./chunk-5XRAKVIV.js";
27
4
  import {
28
5
  cn
29
6
  } from "./chunk-EI44GEQ5.js";
@@ -46,22 +23,6 @@ import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
46
23
  import { Brain, ChevronDown, Sparkles } from "lucide-react";
47
24
  import { jsx, jsxs } from "react/jsx-runtime";
48
25
  var REASONING_LADDER = reasoningLadder;
49
- function clampReasoningLevel(value, available) {
50
- if (value === "auto" || !available || available.includes(value)) {
51
- return value;
52
- }
53
- const requestedRank = REASONING_LADDER.indexOf(value);
54
- let closest;
55
- let closestRank = -1;
56
- for (const effort of available) {
57
- const rank = REASONING_LADDER.indexOf(effort);
58
- if (rank <= requestedRank && rank > closestRank) {
59
- closest = effort;
60
- closestRank = rank;
61
- }
62
- }
63
- return closest ?? "auto";
64
- }
65
26
  function ReasoningGlyph({ level }) {
66
27
  if (level === "auto") {
67
28
  return /* @__PURE__ */ jsx(Sparkles, { className: "h-3.5 w-3.5 text-muted-foreground" });
@@ -184,1573 +145,973 @@ function ReasoningLevelPicker({
184
145
  ] });
185
146
  }
186
147
 
187
- // src/chat/agent-session-controls.tsx
188
- import {
189
- harnessHonorsEffort,
190
- harnessHonorsModel,
191
- reasoningEffortsFor
192
- } from "@tangle-network/agent-interface";
193
- import * as DropdownMenu2 from "@radix-ui/react-dropdown-menu";
194
- import { ChevronDown as ChevronDown3, SlidersHorizontal } from "lucide-react";
195
- import * as React5 from "react";
196
-
197
- // src/chat/harness-model-compat.ts
148
+ // src/chat/agent-composer.tsx
198
149
  import {
199
- harnessSupportsModel,
200
- snapModelToHarness as snapModelIdToHarness
201
- } from "@tangle-network/agent-interface";
202
- import { modelProvider, snapHarnessToModel } from "@tangle-network/agent-interface";
203
- function isModelCompatibleWithHarness(harness, modelId) {
204
- return harnessSupportsModel(harness, modelId);
205
- }
206
- function snapModelToHarness(harness, modelId, models) {
207
- return snapModelIdToHarness(harness, modelId, models.map(canonicalModelId));
208
- }
209
-
210
- // src/chat/informative-lock.tsx
211
- import { Lock, Plus } from "lucide-react";
212
- import * as React3 from "react";
213
-
214
- // src/lib/use-click-outside.ts
150
+ ArrowUp,
151
+ Folder,
152
+ Loader2,
153
+ Paperclip,
154
+ RotateCcw,
155
+ Square,
156
+ Upload,
157
+ X
158
+ } from "lucide-react";
215
159
  import * as React from "react";
216
- function useClickOutside(onOutside, extras) {
217
- const ref = React.useRef(null);
218
- const extrasRef = React.useRef(extras);
219
- extrasRef.current = extras;
220
- React.useEffect(() => {
221
- function handler(event) {
222
- const target = event.target;
223
- if (!ref.current || ref.current.contains(target)) return;
224
- if (extrasRef.current?.some((extra) => extra.current?.contains(target))) return;
225
- onOutside();
160
+
161
+ // src/chat/attachment-validation.ts
162
+ function isAcceptedType(file, accept) {
163
+ if (!accept || accept.trim().length === 0) return true;
164
+ const patterns = accept.split(",").map((pattern) => pattern.trim()).filter((pattern) => pattern.length > 0);
165
+ if (patterns.length === 0) return true;
166
+ const name = file.name.toLowerCase();
167
+ const type = (file.type || "").toLowerCase();
168
+ return patterns.some((pattern) => {
169
+ const lowerPattern = pattern.toLowerCase();
170
+ if (lowerPattern.startsWith(".")) {
171
+ return name.endsWith(lowerPattern);
226
172
  }
227
- document.addEventListener("mousedown", handler);
228
- return () => document.removeEventListener("mousedown", handler);
229
- }, [onOutside]);
230
- return ref;
173
+ if (lowerPattern.endsWith("/*")) {
174
+ const prefix = lowerPattern.slice(0, -1);
175
+ return type.startsWith(prefix);
176
+ }
177
+ return type === lowerPattern;
178
+ });
231
179
  }
232
-
233
- // src/chat/anchored-popover.tsx
234
- import * as React2 from "react";
235
- import { createPortal } from "react-dom";
236
- import { jsx as jsx2 } from "react/jsx-runtime";
237
- var AnchoredPopover = React2.forwardRef(
238
- function AnchoredPopover2({ anchorRef, side, children, className, style, ...rest }, forwardedRef) {
239
- const innerRef = React2.useRef(null);
240
- const setRefs = (node) => {
241
- innerRef.current = node;
242
- if (typeof forwardedRef === "function") forwardedRef(node);
243
- else if (forwardedRef) forwardedRef.current = node;
244
- };
245
- const [position, setPosition] = React2.useState(null);
246
- const place = React2.useCallback(() => {
247
- const anchor = anchorRef.current;
248
- if (!anchor) return;
249
- const rect = anchor.getBoundingClientRect();
250
- const width = innerRef.current?.offsetWidth ?? 0;
251
- const margin = 8;
252
- const left = Math.max(
253
- margin,
254
- Math.min(rect.left, window.innerWidth - width - margin)
255
- );
256
- setPosition(
257
- side === "top" ? { left, bottom: window.innerHeight - rect.top + margin } : { left, top: rect.bottom + margin }
258
- );
259
- }, [anchorRef, side]);
260
- React2.useLayoutEffect(() => {
261
- place();
262
- window.addEventListener("resize", place);
263
- window.addEventListener("scroll", place, true);
264
- return () => {
265
- window.removeEventListener("resize", place);
266
- window.removeEventListener("scroll", place, true);
267
- };
268
- }, [place]);
269
- return createPortal(
270
- /* @__PURE__ */ jsx2(
271
- "div",
272
- {
273
- ...rest,
274
- ref: setRefs,
275
- className: cn("fixed z-50", className),
276
- style: {
277
- ...style,
278
- pointerEvents: "auto",
279
- ...position ?? { visibility: "hidden", left: 0, top: 0 }
280
- },
281
- children
282
- }
283
- ),
284
- document.body
285
- );
180
+ function validateComposerFiles(files, config) {
181
+ const { maxSizeBytes, maxCount, currentCount = 0, accept } = config;
182
+ const list = Array.isArray(files) ? files : Array.from(files);
183
+ const accepted = [];
184
+ const rejected = [];
185
+ for (const file of list) {
186
+ if (!isAcceptedType(file, accept)) {
187
+ rejected.push({
188
+ file,
189
+ reason: `"${file.name}" is not an accepted file type (${accept}).`
190
+ });
191
+ continue;
192
+ }
193
+ if (maxSizeBytes !== void 0 && file.size > maxSizeBytes) {
194
+ rejected.push({
195
+ file,
196
+ reason: `"${file.name}" (${formatBytes(file.size)}) exceeds the ${formatBytes(maxSizeBytes)} limit.`
197
+ });
198
+ continue;
199
+ }
200
+ if (maxCount !== void 0 && currentCount + accepted.length >= maxCount) {
201
+ rejected.push({
202
+ file,
203
+ reason: `"${file.name}" was not added \u2014 the ${maxCount}-file limit is already reached.`
204
+ });
205
+ continue;
206
+ }
207
+ accepted.push(file);
286
208
  }
287
- );
209
+ return { accepted, rejected };
210
+ }
288
211
 
289
- // src/chat/informative-lock.tsx
290
- import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
291
- function InformativeLock({
292
- children,
293
- ariaLabel,
294
- lockTitle,
295
- lockBody,
296
- newChatLabel,
297
- onNewChat,
212
+ // src/chat/agent-composer.tsx
213
+ import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
214
+ var IMAGE_EXTENSION_BY_MIME = {
215
+ "image/png": "png",
216
+ "image/jpeg": "jpg",
217
+ "image/jpg": "jpg",
218
+ "image/gif": "gif",
219
+ "image/webp": "webp",
220
+ "image/bmp": "bmp",
221
+ "image/svg+xml": "svg"
222
+ };
223
+ function isGenericImageName(name) {
224
+ return /^(image)?(\.[a-z0-9]+)?$/i.test(name.trim());
225
+ }
226
+ function imageExtension(file) {
227
+ const fromMime = IMAGE_EXTENSION_BY_MIME[file.type.toLowerCase()];
228
+ if (fromMime) return fromMime;
229
+ const fromName = /\.([a-z0-9]+)$/i.exec(file.name)?.[1];
230
+ return fromName?.toLowerCase() ?? "png";
231
+ }
232
+ var MentionEditor = React.lazy(() => import("./mention-editor-3Q6MYOIU.js"));
233
+ function AgentComposer({
234
+ value,
235
+ onChange,
236
+ onSubmit,
237
+ placeholder = "Send a message\u2026",
238
+ disabled,
239
+ busy,
240
+ onCancel,
241
+ controls,
242
+ trailing,
243
+ onAttach,
244
+ onAttachFolder,
245
+ attachments = [],
246
+ onRemoveFile,
247
+ onRetryFile,
248
+ accept,
249
+ onRejectFiles,
250
+ dropTitle = "Drop files to add context",
251
+ dropDescription = "They attach to your next message.",
252
+ canSubmitWhileBusy = false,
253
+ canSubmitAttachmentsOnly = false,
254
+ mention,
255
+ focusShortcut = false,
256
+ minRows = 2,
257
+ maxHeight = 200,
298
258
  className,
299
- triggerClassName,
300
- popoverClassName,
301
- side = "top"
259
+ sendLabel = "Send",
260
+ autoFocus
302
261
  }) {
303
- const [open, setOpen] = React3.useState(false);
304
- const closeTimer = React3.useRef(null);
305
- const popoverRef = React3.useRef(null);
306
- const ref = useClickOutside(() => setOpen(false), [popoverRef]);
307
- const cancelClose = React3.useCallback(() => {
308
- if (closeTimer.current) {
309
- clearTimeout(closeTimer.current);
310
- closeTimer.current = null;
262
+ const textareaRef = React.useRef(null);
263
+ const richFocusRef = React.useRef(null);
264
+ const fileInputRef = React.useRef(null);
265
+ const folderInputRef = React.useRef(null);
266
+ const [dragOver, setDragOver] = React.useState(false);
267
+ const dragDepth = React.useRef(0);
268
+ const pastedImageCount = React.useRef(0);
269
+ const resize = React.useCallback(
270
+ (el) => {
271
+ el.style.height = "auto";
272
+ el.style.height = `${Math.min(el.scrollHeight, maxHeight)}px`;
273
+ },
274
+ [maxHeight]
275
+ );
276
+ React.useEffect(() => {
277
+ if (textareaRef.current) resize(textareaRef.current);
278
+ }, [value, resize]);
279
+ React.useEffect(() => {
280
+ if (!focusShortcut || disabled) return;
281
+ function onKeyDown(event) {
282
+ if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "l") {
283
+ event.preventDefault();
284
+ if (mention) richFocusRef.current?.();
285
+ else textareaRef.current?.focus();
286
+ }
311
287
  }
288
+ document.addEventListener("keydown", onKeyDown);
289
+ return () => document.removeEventListener("keydown", onKeyDown);
290
+ }, [focusShortcut, disabled, mention]);
291
+ const canSend = !disabled && (!busy || canSubmitWhileBusy) && (value.trim().length > 0 || canSubmitAttachmentsOnly && attachments.length > 0);
292
+ const submit = () => {
293
+ if (!canSend) return;
294
+ onSubmit();
295
+ };
296
+ const registerRichFocus = React.useCallback((focus) => {
297
+ richFocusRef.current = focus;
312
298
  }, []);
313
- const scheduleClose = React3.useCallback(() => {
314
- cancelClose();
315
- closeTimer.current = setTimeout(() => setOpen(false), 150);
316
- }, [cancelClose]);
317
- React3.useEffect(() => cancelClose, [cancelClose]);
318
- return /* @__PURE__ */ jsxs2(
319
- "div",
320
- {
321
- ref,
322
- className: cn("relative inline-flex", className),
323
- onMouseEnter: () => {
324
- cancelClose();
325
- setOpen(true);
326
- },
327
- onMouseLeave: scheduleClose,
328
- onKeyDown: (event) => {
329
- if (event.key === "Escape") setOpen(false);
330
- },
331
- children: [
332
- /* @__PURE__ */ jsx3(
333
- "button",
334
- {
335
- type: "button",
336
- onClick: () => setOpen(true),
337
- "data-state": open ? "open" : "closed",
338
- "aria-label": ariaLabel,
339
- "aria-haspopup": "dialog",
340
- "aria-expanded": open,
341
- className: cn(
342
- "inline-flex h-8 items-center gap-1.5 rounded-lg border border-[var(--md3-outline-variant)] bg-surface-container px-2.5 opacity-60",
343
- "text-xs font-medium text-foreground shadow-sm transition-colors",
344
- "hover:border-[var(--md3-outline-variant)] hover:bg-surface-container-high focus:outline-none focus-visible:ring-1 focus-visible:ring-ring",
345
- "data-[state=open]:border-[var(--md3-outline-variant)] data-[state=open]:bg-surface-container-high",
346
- triggerClassName
347
- ),
348
- children
349
- }
350
- ),
351
- open && /* @__PURE__ */ jsxs2(
352
- AnchoredPopover,
353
- {
354
- ref: popoverRef,
355
- anchorRef: ref,
356
- side,
357
- role: "dialog",
358
- onMouseEnter: () => {
359
- cancelClose();
360
- setOpen(true);
361
- },
362
- onMouseLeave: scheduleClose,
363
- className: cn(
364
- "w-72 rounded-[var(--radius-md)] border border-[var(--md3-outline-variant)] bg-surface-container-highest p-3",
365
- "shadow-[0_8px_30px_rgba(0,0,0,0.45)] ring-1 ring-[#ffffff14]",
366
- popoverClassName
367
- ),
368
- children: [
369
- /* @__PURE__ */ jsxs2("p", { className: "flex items-start gap-2 text-sm font-medium text-foreground", children: [
370
- /* @__PURE__ */ jsx3(Lock, { className: "mt-0.5 h-3.5 w-3.5 shrink-0 text-muted-foreground" }),
371
- /* @__PURE__ */ jsx3("span", { children: lockTitle })
372
- ] }),
373
- /* @__PURE__ */ jsx3("p", { className: "mt-1.5 text-xs text-muted-foreground", children: lockBody }),
374
- /* @__PURE__ */ jsxs2(
375
- "button",
376
- {
377
- type: "button",
378
- onClick: () => {
379
- setOpen(false);
380
- onNewChat();
381
- },
382
- className: cn(
383
- "mt-3 flex w-full items-center justify-center gap-1.5 rounded-md px-2.5 py-2 text-sm font-medium text-foreground",
384
- "bg-primary/10 ring-1 ring-inset ring-primary/25 transition-colors",
385
- "hover:bg-primary/15 focus:outline-none focus-visible:ring-1 focus-visible:ring-ring"
386
- ),
387
- children: [
388
- /* @__PURE__ */ jsx3(Plus, { className: "h-4 w-4" }),
389
- newChatLabel
390
- ]
391
- }
392
- )
393
- ]
394
- }
395
- )
396
- ]
397
- }
398
- );
399
- }
400
-
401
- // src/chat/agent-profile-picker.tsx
402
- import { Bot, Check, ChevronDown as ChevronDown2, Pencil, Plus as Plus2, Trash2 } from "lucide-react";
403
- import * as React4 from "react";
404
- import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
405
- function profileSubtitle(profile) {
406
- if (profile.description) return profile.description;
407
- const count = profile.capabilities?.length ?? 0;
408
- return `${count} ${count === 1 ? "tool group" : "tool groups"}`;
409
- }
410
- function AgentProfilePicker({
411
- value,
412
- onChange,
413
- profiles,
414
- capabilities,
415
- onCreate,
416
- onUpdate,
417
- onDelete,
418
- disabled,
419
- locked,
420
- lockReason,
421
- onNewChat,
422
- className,
423
- triggerClassName,
424
- popoverClassName,
425
- side = "top"
426
- }) {
427
- const [open, setOpen] = React4.useState(false);
428
- const [composing, setComposing] = React4.useState(
429
- null
430
- );
431
- const popoverRef = React4.useRef(null);
432
- const ref = useClickOutside(() => {
433
- setOpen(false);
434
- setComposing(null);
435
- }, [popoverRef]);
436
- const canAuthor = Boolean(capabilities && (onCreate || onUpdate));
437
- const builtins = profiles.filter((profile) => profile.builtin);
438
- const custom = profiles.filter((profile) => !profile.builtin);
439
- const selected = profiles.find((profile) => profile.id === value);
440
- const selectedName = selected?.name ?? value;
441
- React4.useEffect(() => {
442
- if (!locked) return;
443
- setOpen(false);
444
- setComposing(null);
445
- }, [locked]);
446
- function select(id) {
447
- onChange(id);
448
- setOpen(false);
449
- }
450
- async function save(draft) {
451
- if (draft.id) await onUpdate?.(draft.id, draft);
452
- else await onCreate?.(draft);
453
- setComposing(null);
454
- }
455
- if (locked && onNewChat) {
456
- return /* @__PURE__ */ jsxs3(
457
- InformativeLock,
458
- {
459
- ariaLabel: "Agent profile (locked)",
460
- lockTitle: `Profile fixed to ${selectedName} for this conversation.`,
461
- lockBody: "Conversations stay on one profile.",
462
- newChatLabel: "New chat to switch profile",
463
- onNewChat,
464
- className,
465
- triggerClassName,
466
- popoverClassName,
467
- side,
468
- children: [
469
- /* @__PURE__ */ jsx4(Bot, { className: "h-3.5 w-3.5 text-muted-foreground" }),
470
- /* @__PURE__ */ jsx4("span", { className: "max-w-[140px] truncate", children: selectedName })
471
- ]
472
- }
473
- );
474
- }
475
- return /* @__PURE__ */ jsxs3("div", { ref, className: cn("relative inline-flex", className), children: [
476
- /* @__PURE__ */ jsxs3(
477
- "button",
478
- {
479
- type: "button",
480
- disabled: disabled || locked,
481
- title: locked ? lockReason : void 0,
482
- onClick: () => setOpen((v) => !v),
483
- className: cn(
484
- "inline-flex h-8 items-center gap-1.5 rounded-lg border border-[var(--md3-outline-variant)] bg-surface-container px-2.5",
485
- "text-xs font-medium text-foreground shadow-sm transition-colors",
486
- "hover:border-[var(--md3-outline-variant)] hover:bg-surface-container-high focus:outline-none focus-visible:ring-1 focus-visible:ring-ring",
487
- "data-[state=open]:border-[var(--md3-outline-variant)] data-[state=open]:bg-surface-container-high",
488
- "disabled:cursor-not-allowed disabled:opacity-60",
489
- triggerClassName
490
- ),
491
- "data-state": open ? "open" : "closed",
492
- "aria-label": "Agent profile",
493
- children: [
494
- /* @__PURE__ */ jsx4(Bot, { className: "h-3.5 w-3.5 text-muted-foreground" }),
495
- /* @__PURE__ */ jsx4("span", { className: "max-w-[140px] truncate", children: selectedName }),
496
- !locked && /* @__PURE__ */ jsx4(ChevronDown2, { className: "ml-auto h-3.5 w-3.5 shrink-0 text-muted-foreground" })
497
- ]
498
- }
499
- ),
500
- open && !locked && /* @__PURE__ */ jsx4(
501
- AnchoredPopover,
502
- {
503
- ref: popoverRef,
504
- anchorRef: ref,
505
- side,
506
- className: cn(
507
- "w-[min(320px,calc(100vw-2rem))] overflow-hidden rounded-[var(--radius-md)] border border-[var(--md3-outline-variant)] bg-surface-container-highest p-1",
508
- "shadow-[0_8px_30px_rgba(0,0,0,0.45)] ring-1 ring-[#ffffff14]",
509
- popoverClassName
510
- ),
511
- "data-testid": "agent-profile-popover",
512
- children: composing ? /* @__PURE__ */ jsx4(
513
- ProfileForm,
514
- {
515
- capabilities: capabilities ?? [],
516
- initial: composing === "new" ? void 0 : composing,
517
- onCancel: () => setComposing(null),
518
- onSave: save
519
- }
520
- ) : /* @__PURE__ */ jsxs3("div", { className: "max-h-[min(60vh,420px)] overflow-y-auto", children: [
521
- /* @__PURE__ */ jsx4(SectionHeader, { children: "Modes" }),
522
- builtins.map((profile) => /* @__PURE__ */ jsx4(
523
- ProfileRow,
524
- {
525
- active: profile.id === value,
526
- title: profile.name,
527
- subtitle: profileSubtitle(profile),
528
- onSelect: () => select(profile.id)
529
- },
530
- profile.id
531
- )),
532
- custom.length > 0 && /* @__PURE__ */ jsx4(SectionHeader, { children: "Your agents" }),
533
- custom.map((profile) => /* @__PURE__ */ jsx4(
534
- ProfileRow,
535
- {
536
- active: profile.id === value,
537
- title: profile.name,
538
- subtitle: profileSubtitle(profile),
539
- onSelect: () => select(profile.id),
540
- onEdit: canAuthor ? () => setComposing(profile) : void 0,
541
- onDelete: onDelete ? () => void onDelete(profile.id) : void 0
542
- },
543
- profile.id
544
- )),
545
- canAuthor && /* @__PURE__ */ jsxs3(
546
- "button",
547
- {
548
- type: "button",
549
- onClick: () => setComposing("new"),
550
- 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",
551
- children: [
552
- /* @__PURE__ */ jsx4(Plus2, { className: "h-4 w-4" }),
553
- " New agent\u2026"
554
- ]
555
- }
556
- )
557
- ] })
558
- }
559
- )
560
- ] });
561
- }
562
- function SectionHeader({ children }) {
563
- return /* @__PURE__ */ jsx4("div", { className: "px-2.5 pb-1 pt-2 text-[10px] font-semibold uppercase tracking-wide text-muted-foreground/70", children });
564
- }
565
- function ProfileRow({
566
- active,
567
- title,
568
- subtitle,
569
- onSelect,
570
- onEdit,
571
- onDelete
572
- }) {
573
- return /* @__PURE__ */ jsxs3(
574
- "div",
575
- {
576
- className: cn(
577
- "group flex items-center gap-2 rounded-md px-2.5 py-2 text-sm transition-colors",
578
- active ? "bg-[var(--accent-surface-soft)] text-[var(--accent-text)]" : "hover:bg-accent/40"
579
- ),
580
- children: [
581
- /* @__PURE__ */ jsxs3(
582
- "button",
583
- {
584
- type: "button",
585
- onClick: onSelect,
586
- className: "flex min-w-0 flex-1 items-center gap-2 text-left",
587
- children: [
588
- /* @__PURE__ */ jsx4("span", { className: "flex h-4 w-4 shrink-0 items-center justify-center", children: active && /* @__PURE__ */ jsx4(Check, { className: "h-3.5 w-3.5" }) }),
589
- /* @__PURE__ */ jsxs3("span", { className: "min-w-0", children: [
590
- /* @__PURE__ */ jsx4("span", { className: "block truncate font-medium", children: title }),
591
- /* @__PURE__ */ jsx4("span", { className: "block truncate text-xs text-muted-foreground", children: subtitle })
592
- ] })
593
- ]
594
- }
595
- ),
596
- (onEdit || onDelete) && /* @__PURE__ */ jsxs3("span", { className: "flex shrink-0 items-center gap-1 opacity-0 transition-opacity group-hover:opacity-100", children: [
597
- onEdit && /* @__PURE__ */ jsx4(
598
- "button",
599
- {
600
- type: "button",
601
- onClick: onEdit,
602
- title: "Edit",
603
- className: "rounded p-1 text-muted-foreground hover:text-foreground",
604
- children: /* @__PURE__ */ jsx4(Pencil, { className: "h-3 w-3" })
605
- }
606
- ),
607
- onDelete && /* @__PURE__ */ jsx4(
608
- "button",
609
- {
610
- type: "button",
611
- onClick: onDelete,
612
- title: "Delete",
613
- className: "rounded p-1 text-muted-foreground hover:text-[var(--status-error,#ff4d6d)]",
614
- children: /* @__PURE__ */ jsx4(Trash2, { className: "h-3 w-3" })
615
- }
616
- )
617
- ] })
618
- ]
619
- }
620
- );
621
- }
622
- function ProfileForm({
623
- capabilities,
624
- initial,
625
- onCancel,
626
- onSave
627
- }) {
628
- const [name, setName] = React4.useState(initial?.name ?? "");
629
- const [caps, setCaps] = React4.useState(
630
- initial?.capabilities ? [...initial.capabilities] : capabilities.map((c) => c.id)
631
- );
632
- const [instructions, setInstructions] = React4.useState(initial?.instructions ?? "");
633
- const toggle = (id) => setCaps(
634
- (prev) => prev.includes(id) ? prev.filter((c) => c !== id) : [...prev, id]
635
- );
636
- return /* @__PURE__ */ jsxs3("div", { className: "space-y-3 p-2.5", children: [
637
- /* @__PURE__ */ jsx4(
638
- "input",
639
- {
640
- autoFocus: true,
641
- value: name,
642
- onChange: (event) => setName(event.target.value),
643
- placeholder: "Agent name (e.g. Tax Reviewer)",
644
- 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"
645
- }
646
- ),
647
- capabilities.length > 0 && /* @__PURE__ */ jsxs3("div", { children: [
648
- /* @__PURE__ */ jsx4("p", { className: "mb-1.5 text-[10px] font-semibold uppercase tracking-wide text-muted-foreground/70", children: "Tools" }),
649
- /* @__PURE__ */ jsx4("div", { className: "space-y-0.5", children: capabilities.map((cap) => /* @__PURE__ */ jsxs3(
650
- "label",
651
- {
652
- className: "flex cursor-pointer items-start gap-2 rounded-md px-2 py-1.5 hover:bg-accent/40",
653
- children: [
654
- /* @__PURE__ */ jsx4(
655
- "input",
656
- {
657
- type: "checkbox",
658
- checked: caps.includes(cap.id),
659
- onChange: () => toggle(cap.id),
660
- className: "mt-0.5"
661
- }
662
- ),
663
- /* @__PURE__ */ jsxs3("span", { className: "min-w-0", children: [
664
- /* @__PURE__ */ jsx4("span", { className: "block text-sm font-medium", children: cap.label }),
665
- cap.description && /* @__PURE__ */ jsx4("span", { className: "block text-xs text-muted-foreground", children: cap.description })
666
- ] })
667
- ]
668
- },
669
- cap.id
670
- )) })
671
- ] }),
672
- /* @__PURE__ */ jsx4(
673
- "textarea",
674
- {
675
- value: instructions,
676
- onChange: (event) => setInstructions(event.target.value),
677
- rows: 2,
678
- placeholder: "Instructions (optional) \u2014 how this agent should behave",
679
- 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"
680
- }
681
- ),
682
- /* @__PURE__ */ jsxs3("div", { className: "flex justify-end gap-2", children: [
683
- /* @__PURE__ */ jsx4(
684
- "button",
685
- {
686
- type: "button",
687
- onClick: onCancel,
688
- className: "rounded-lg px-3 py-1.5 text-sm text-muted-foreground hover:bg-accent/40",
689
- children: "Cancel"
690
- }
691
- ),
692
- /* @__PURE__ */ jsx4(
693
- "button",
694
- {
695
- type: "button",
696
- disabled: !name.trim(),
697
- onClick: () => onSave({
698
- id: initial?.id,
699
- name: name.trim(),
700
- capabilities: caps,
701
- instructions: instructions.trim()
702
- }),
703
- 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",
704
- children: "Save agent"
705
- }
706
- )
707
- ] })
708
- ] });
709
- }
710
-
711
- // src/chat/agent-session-controls.tsx
712
- import { Fragment as Fragment2, jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
713
- function harnessAutonomyNote(harness) {
714
- const ownModel = !harnessHonorsModel(harness);
715
- const ownEffort = !harnessHonorsEffort(harness);
716
- if (ownModel && ownEffort) return "own model + thinking";
717
- if (ownModel) return "own model";
718
- if (ownEffort) return "own thinking";
719
- return null;
720
- }
721
- var HARNESS_GROUP_LABELS = {
722
- steerable: "Uses your model & thinking",
723
- fixed: "Brings its own setup",
724
- "no-agent": "No agent"
725
- };
726
- var HARNESS_GROUP_ORDER = [
727
- "steerable",
728
- "fixed",
729
- "no-agent"
730
- ];
731
- function harnessGroup(option) {
732
- if (!option.chatCapable) return "no-agent";
733
- return harnessAutonomyNote(option.type) === null ? "steerable" : "fixed";
734
- }
735
- var HARNESS_SEARCH_THRESHOLD = 7;
736
- function HarnessDropdown({
737
- value,
738
- onChange,
739
- available,
740
- disabled,
741
- locked,
742
- lockReason,
743
- onNewChat,
744
- lockTitle,
745
- lockBody,
746
- newChatLabel,
747
- side = "bottom",
748
- align = "start",
749
- avoidCollisions,
750
- triggerClassName
751
- }) {
752
- const allowed = new Set(
753
- available ?? HARNESS_OPTIONS.map((h) => h.type)
754
- );
755
- const options = HARNESS_OPTIONS.filter((h) => allowed.has(h.type));
756
- const selected = options.find((option) => option.type === value);
757
- const [query, setQuery] = React5.useState("");
758
- const showSearch = options.length > HARNESS_SEARCH_THRESHOLD;
759
- const filtered = React5.useMemo(() => {
760
- const q = query.trim().toLowerCase();
761
- if (!q) return options;
762
- return options.filter(
763
- (o) => o.label.toLowerCase().includes(q) || o.type.toLowerCase().includes(q) || (o.description?.toLowerCase() ?? "").includes(q)
764
- );
765
- }, [options, query]);
766
- const sections = React5.useMemo(() => {
767
- const byGroup = /* @__PURE__ */ new Map();
768
- for (const option of filtered) {
769
- const key = harnessGroup(option);
770
- const bucket = byGroup.get(key);
771
- if (bucket) bucket.push(option);
772
- else byGroup.set(key, [option]);
773
- }
774
- return HARNESS_GROUP_ORDER.map((key) => ({
775
- key,
776
- label: HARNESS_GROUP_LABELS[key],
777
- items: byGroup.get(key) ?? []
778
- })).filter((section) => section.items.length > 0);
779
- }, [filtered]);
780
- if (locked && onNewChat) {
781
- const label = selected?.label ?? value;
782
- return /* @__PURE__ */ jsxs4(
783
- InformativeLock,
784
- {
785
- ariaLabel: "Agent harness (locked)",
786
- lockTitle: lockTitle ?? `Agent fixed to ${label} for this conversation.`,
787
- lockBody: lockBody ?? "Conversations stay on one agent.",
788
- newChatLabel: newChatLabel ?? "New chat to switch agent",
789
- onNewChat,
790
- triggerClassName,
791
- children: [
792
- /* @__PURE__ */ jsx5(HarnessLogo, { type: value, size: 16 }),
793
- /* @__PURE__ */ jsx5("span", { children: label })
794
- ]
795
- }
796
- );
797
- }
798
- return /* @__PURE__ */ jsxs4(
799
- DropdownMenu2.Root,
800
- {
801
- onOpenChange: (nextOpen) => {
802
- if (!nextOpen) setQuery("");
803
- },
804
- children: [
805
- /* @__PURE__ */ jsx5(DropdownMenu2.Trigger, { asChild: true, children: /* @__PURE__ */ jsxs4(
806
- "button",
807
- {
808
- type: "button",
809
- disabled: disabled || locked,
810
- title: locked ? lockReason : void 0,
811
- className: cn(
812
- "inline-flex h-8 items-center gap-1.5 rounded-lg border border-[var(--md3-outline-variant)] bg-surface-container px-2.5",
813
- "text-xs font-medium text-foreground shadow-sm transition-colors",
814
- "hover:border-[var(--md3-outline-variant)] hover:bg-surface-container-high focus:outline-none focus-visible:ring-1 focus-visible:ring-ring",
815
- "data-[state=open]:border-[var(--md3-outline-variant)] data-[state=open]:bg-surface-container-high",
816
- "disabled:cursor-not-allowed disabled:opacity-60",
817
- triggerClassName
818
- ),
819
- "aria-label": "Agent harness",
820
- children: [
821
- /* @__PURE__ */ jsx5(HarnessLogo, { type: value, size: 16, className: "shrink-0" }),
822
- /* @__PURE__ */ jsx5("span", { className: "truncate", children: selected?.label ?? value }),
823
- !locked && /* @__PURE__ */ jsx5(ChevronDown3, { className: "ml-auto h-3.5 w-3.5 shrink-0 text-muted-foreground" })
824
- ]
825
- }
826
- ) }),
827
- /* @__PURE__ */ jsx5(DropdownMenu2.Portal, { children: /* @__PURE__ */ jsxs4(
828
- DropdownMenu2.Content,
829
- {
830
- side,
831
- align,
832
- avoidCollisions,
833
- collisionPadding: 24,
834
- sideOffset: 6,
835
- className: pickerMenuContentClass,
836
- children: [
837
- showSearch && /* @__PURE__ */ jsx5(
838
- PickerMenuSearch,
839
- {
840
- value: query,
841
- onChange: setQuery,
842
- placeholder: "Search agents..."
843
- }
844
- ),
845
- /* @__PURE__ */ jsx5("div", { className: pickerMenuBodyClass, children: sections.length === 0 ? /* @__PURE__ */ jsx5("div", { className: "px-3 py-8 text-center text-xs text-muted-foreground", children: "No agents match." }) : sections.map((section) => /* @__PURE__ */ jsx5(PickerMenuSection, { label: section.label, children: section.items.map((option) => /* @__PURE__ */ jsxs4(
846
- DropdownMenu2.Item,
847
- {
848
- onSelect: (event) => {
849
- event.preventDefault();
850
- onChange(option.type);
851
- },
852
- className: pickerMenuItemClass({
853
- active: option.type === value
854
- }),
855
- children: [
856
- /* @__PURE__ */ jsx5(
857
- HarnessLogo,
858
- {
859
- type: option.type,
860
- size: 20,
861
- className: "mt-0.5"
862
- }
863
- ),
864
- /* @__PURE__ */ jsxs4("div", { className: "min-w-0 flex-1", children: [
865
- /* @__PURE__ */ jsxs4("div", { className: "flex items-center gap-1.5", children: [
866
- /* @__PURE__ */ jsx5("span", { className: "min-w-0 truncate text-sm font-medium", children: option.label }),
867
- harnessAutonomyNote(option.type) && /* @__PURE__ */ jsx5(PickerMenuTag, { children: harnessAutonomyNote(option.type) })
868
- ] }),
869
- /* @__PURE__ */ jsx5(PickerMenuMeta, { parts: [option.description] })
870
- ] })
871
- ]
872
- },
873
- option.type
874
- )) }, section.key)) }),
875
- showSearch && /* @__PURE__ */ jsxs4(PickerMenuFooter, { children: [
876
- filtered.length,
877
- " of ",
878
- options.length,
879
- " agent",
880
- options.length === 1 ? "" : "s"
881
- ] })
882
- ]
883
- }
884
- ) })
885
- ]
886
- }
887
- );
888
- }
889
- function modelReasoningCapability(models, canonicalId) {
890
- if (!models || !canonicalId) return null;
891
- const entry = models.find((m) => canonicalModelId(m) === canonicalId);
892
- return entry ? {
893
- supportsReasoning: entry.supportsReasoning,
894
- maxEffort: entry.maxReasoningEffort
895
- } : null;
896
- }
897
- function AgentSessionControls({
898
- harness,
899
- profile,
900
- model,
901
- reasoning,
902
- filterModelsToHarness,
903
- trailing,
904
- className,
905
- context = "chat",
906
- layout = "inline",
907
- menuPlacement = "auto"
908
- }) {
909
- if (!harness && !profile && !model && !reasoning && !trailing) return null;
910
- const restrictModelsToHarness = Boolean(
911
- harness && (harness.locked || filterModelsToHarness)
912
- );
913
- const selectorHonorsModel = !harness || harnessHonorsModel(harness.value);
914
- const selectorHonorsEffort = !harness || harnessHonorsEffort(harness.value);
915
- const clampEffort = (harnessValue, modelId) => {
916
- if (!reasoning) return;
917
- const available = reasoningEffortsFor(
918
- harnessValue,
919
- modelReasoningCapability(model?.models, modelId)
920
- );
921
- const clamped = clampReasoningLevel(reasoning.value, available);
922
- if (clamped !== reasoning.value) reasoning.onChange(clamped);
299
+ const dropEnabled = Boolean(onAttach);
300
+ const handleDragEnter = (event) => {
301
+ if (!dropEnabled) return;
302
+ event.preventDefault();
303
+ event.stopPropagation();
304
+ dragDepth.current++;
305
+ if (event.dataTransfer?.types.includes("Files")) setDragOver(true);
923
306
  };
924
- const handleHarnessChange = (next) => {
925
- harness?.onChange(next);
926
- let nextModelId = model?.value;
927
- if (model) {
928
- const snapped = snapModelToHarness(next, model.value, model.models);
929
- if (snapped !== model.value) {
930
- model.onChange(snapped);
931
- nextModelId = snapped;
932
- }
307
+ const handleDragLeave = (event) => {
308
+ if (!dropEnabled) return;
309
+ event.preventDefault();
310
+ event.stopPropagation();
311
+ dragDepth.current--;
312
+ if (dragDepth.current <= 0) {
313
+ dragDepth.current = 0;
314
+ setDragOver(false);
933
315
  }
934
- clampEffort(next, nextModelId);
935
316
  };
936
- const handleModelChange = (nextModelId) => {
937
- model?.onChange(nextModelId);
938
- let nextHarness = harness?.value;
939
- if (harness && !restrictModelsToHarness) {
940
- const snapped = snapHarnessToModel(harness.value, nextModelId);
941
- if (snapped !== harness.value) {
942
- harness.onChange(snapped);
943
- nextHarness = snapped;
944
- }
945
- }
946
- if (nextHarness) clampEffort(nextHarness, nextModelId);
317
+ const handleDragOver = (event) => {
318
+ if (!dropEnabled) return;
319
+ event.preventDefault();
320
+ event.stopPropagation();
321
+ if (event.dataTransfer) event.dataTransfer.dropEffect = "copy";
947
322
  };
948
- const chatModels = model && context === "chat" && model.modalities === void 0 ? model.models.filter(isTextChatModel) : model?.models;
949
- const visibleModels = model && restrictModelsToHarness && harness ? (chatModels ?? []).filter(
950
- (entry) => isModelCompatibleWithHarness(
951
- harness.value,
952
- canonicalModelId(entry)
953
- )
954
- ) : chatModels;
955
- const harnessAvailable = harness?.available ?? (context === "chat" ? chatCapableHarnesses : void 0);
956
- const collapsed = layout === "gear" || layout === "combined";
957
- const menuSide = collapsed ? "left" : "bottom";
958
- const menuAlign = collapsed ? "end" : "start";
959
- const collapsedTriggerClass = layout === "combined" ? "w-full bg-transparent shadow-none hover:bg-muted/60 data-[state=open]:bg-muted/60" : collapsed ? "w-full" : void 0;
960
- const secondaryTriggerClass = collapsed ? collapsedTriggerClass : cn(
961
- "border-transparent bg-transparent text-muted-foreground shadow-none",
962
- "hover:border-transparent hover:bg-muted/50 hover:text-foreground",
963
- "data-[state=open]:border-transparent data-[state=open]:bg-muted/50 data-[state=open]:text-foreground"
964
- );
965
- const modelTriggerClass = collapsed ? cn(collapsedTriggerClass, "h-8 rounded-lg px-2.5 text-xs") : void 0;
966
- const inlineForceDown = menuPlacement === "down" && layout === "inline";
967
- const avoidCollisions = inlineForceDown ? false : void 0;
968
- const harnessNode = harness && /* @__PURE__ */ jsx5(
969
- HarnessDropdown,
970
- {
971
- ...harness,
972
- available: harnessAvailable,
973
- onChange: handleHarnessChange,
974
- side: menuSide,
975
- align: menuAlign,
976
- avoidCollisions,
977
- triggerClassName: secondaryTriggerClass
978
- }
979
- );
980
- const profileNode = profile && /* @__PURE__ */ jsx5(
981
- AgentProfilePicker,
982
- {
983
- value: profile.value,
984
- onChange: profile.onChange,
985
- profiles: profile.profiles,
986
- capabilities: profile.capabilities,
987
- onCreate: profile.onCreate,
988
- onUpdate: profile.onUpdate,
989
- onDelete: profile.onDelete,
990
- disabled: profile.disabled,
991
- locked: profile.locked,
992
- lockReason: profile.lockReason,
993
- onNewChat: profile.onNewChat,
994
- side: inlineForceDown || collapsed ? "bottom" : "top",
995
- triggerClassName: secondaryTriggerClass,
996
- popoverClassName: collapsed ? "w-full" : void 0
997
- }
998
- );
999
- const modelNode = model && /* @__PURE__ */ jsx5(
1000
- ModelPicker,
1001
- {
1002
- variant: "pill",
1003
- label: "",
1004
- value: model.value,
1005
- onChange: handleModelChange,
1006
- models: visibleModels ?? [],
1007
- loading: model.loading,
1008
- popular: model.popular,
1009
- recents: model.recents,
1010
- modalities: model.modalities,
1011
- excludeProviders: model.excludeProviders,
1012
- disabled: model.disabled || (visibleModels ?? []).length === 0 || !selectorHonorsModel,
1013
- side: collapsed ? void 0 : "bottom",
1014
- avoidCollisions,
1015
- triggerClassName: modelTriggerClass
1016
- }
1017
- );
1018
- const modelReasoning = modelReasoningCapability(model?.models, model?.value);
1019
- const reasoningOptions = (harness && HARNESS_REASONING_OPTIONS[harness.value]) ?? reasoning?.options;
1020
- const reasoningNode = reasoning && /* @__PURE__ */ jsx5(
1021
- ReasoningLevelPicker,
1022
- {
1023
- value: reasoning.value,
1024
- onChange: reasoning.onChange,
1025
- options: reasoningOptions,
1026
- disabled: reasoning.disabled || !selectorHonorsEffort,
1027
- available: harness ? reasoningEffortsFor(harness.value, modelReasoning) : reasoning.available,
1028
- side: collapsed ? void 0 : "bottom",
1029
- avoidCollisions,
1030
- triggerClassName: secondaryTriggerClass
1031
- }
1032
- );
1033
- if (layout === "gear") {
1034
- return /* @__PURE__ */ jsx5(
1035
- GearControls,
1036
- {
1037
- className,
1038
- profileNode,
1039
- harnessNode,
1040
- modelNode,
1041
- reasoningNode,
1042
- trailing
1043
- }
1044
- );
1045
- }
1046
- if (layout === "combined") {
1047
- const segments = [];
1048
- if (harness) {
1049
- const harnessLabel = HARNESS_OPTIONS.find((option) => option.type === harness.value)?.label ?? harness.value;
1050
- segments.push({
1051
- key: "harness",
1052
- label: harnessLabel,
1053
- node: /* @__PURE__ */ jsxs4("span", { className: "flex items-center gap-1.5", children: [
1054
- /* @__PURE__ */ jsx5(HarnessLogo, { type: harness.value, size: 16, className: "shrink-0" }),
1055
- harnessLabel
1056
- ] })
1057
- });
1058
- }
1059
- if (model && selectorHonorsModel) {
1060
- const current = (model.models ?? []).find(
1061
- (entry) => canonicalModelId(entry) === model.value
1062
- );
1063
- const identity = current ? resolveModelBrandIdentity(current) : null;
1064
- const modelLabel = stripBrandPrefix(
1065
- current?.name ?? current?.id ?? model.value,
1066
- identity
1067
- );
1068
- segments.push({
1069
- key: "model",
1070
- label: modelLabel,
1071
- node: /* @__PURE__ */ jsxs4("span", { className: "flex items-center gap-1.5", children: [
1072
- identity && /* @__PURE__ */ jsx5(ModelBrandStack, { identity, size: "sm" }),
1073
- modelLabel
1074
- ] })
1075
- });
1076
- }
1077
- if (reasoning && selectorHonorsEffort) {
1078
- const effortLabel = (reasoningOptions ?? DEFAULT_REASONING_LEVEL_OPTIONS).find(
1079
- (option) => option.value === reasoning.value
1080
- )?.label ?? "Auto";
1081
- segments.push({
1082
- key: "effort",
1083
- label: effortLabel,
1084
- node: /* @__PURE__ */ jsxs4("span", { className: "flex items-center gap-1.5", children: [
1085
- /* @__PURE__ */ jsx5("span", { className: "flex w-3.5 shrink-0 justify-center", children: /* @__PURE__ */ jsx5(ReasoningGlyph, { level: reasoning.value }) }),
1086
- effortLabel
1087
- ] })
1088
- });
1089
- }
1090
- const summary = /* @__PURE__ */ jsx5("span", { className: "flex items-center gap-1.5", children: segments.map((segment, index) => /* @__PURE__ */ jsxs4(React5.Fragment, { children: [
1091
- index > 0 && /* @__PURE__ */ jsx5(
1092
- "span",
1093
- {
1094
- className: "text-base leading-none text-muted-foreground/60",
1095
- "aria-hidden": true,
1096
- children: "\xB7"
1097
- }
1098
- ),
1099
- segment.node
1100
- ] }, segment.key)) });
1101
- return /* @__PURE__ */ jsx5(
1102
- SummaryControls,
1103
- {
1104
- className,
1105
- summary,
1106
- title: segments.map((segment) => segment.label).join(" \xB7 "),
1107
- contentSide: menuPlacement === "down" ? "bottom" : "top",
1108
- profileNode,
1109
- harnessNode,
1110
- modelNode,
1111
- reasoningNode,
1112
- trailing
1113
- }
1114
- );
1115
- }
1116
- return /* @__PURE__ */ jsxs4(
1117
- "div",
1118
- {
1119
- className: cn("flex flex-wrap items-center gap-2", className),
1120
- "data-testid": "agent-session-controls",
1121
- children: [
1122
- profileNode,
1123
- harnessNode,
1124
- modelNode,
1125
- reasoningNode,
1126
- trailing && /* @__PURE__ */ jsx5("div", { className: "ml-auto flex items-center gap-2", children: trailing })
1127
- ]
1128
- }
1129
- );
1130
- }
1131
- function GearControls({
1132
- className,
1133
- profileNode,
1134
- harnessNode,
1135
- modelNode,
1136
- reasoningNode,
1137
- trailing
1138
- }) {
1139
- return /* @__PURE__ */ jsx5(
1140
- CollapsibleControls,
1141
- {
1142
- className,
1143
- trigger: /* @__PURE__ */ jsx5(SlidersHorizontal, { className: "h-4 w-4 text-muted-foreground" }),
1144
- triggerClassName: cn(
1145
- "inline-flex h-8 w-8 items-center justify-center rounded-lg border border-[var(--md3-outline-variant)] bg-surface-container",
1146
- "text-foreground shadow-sm transition-colors",
1147
- "hover:border-[var(--md3-outline-variant)] hover:bg-surface-container-high focus:outline-none focus-visible:ring-1 focus-visible:ring-ring",
1148
- "data-[state=open]:border-[var(--md3-outline-variant)] data-[state=open]:bg-surface-container-high"
1149
- ),
1150
- contentSide: "top",
1151
- contentClassName: cn(
1152
- "w-56 gap-2 border-[var(--md3-outline-variant)] bg-surface-container-highest p-2",
1153
- "shadow-[0_8px_30px_rgba(0,0,0,0.45)] ring-1 ring-[#ffffff14]"
1154
- ),
1155
- profileNode,
1156
- harnessNode,
1157
- modelNode,
1158
- reasoningNode,
1159
- trailing
1160
- }
1161
- );
1162
- }
1163
- function GearSection({
1164
- label,
1165
- children
1166
- }) {
1167
- return /* @__PURE__ */ jsxs4("div", { className: "flex flex-col gap-1", children: [
1168
- /* @__PURE__ */ jsx5("span", { className: "px-0.5 text-[10px] font-medium uppercase tracking-wide text-muted-foreground/70", children: label }),
1169
- children
1170
- ] });
1171
- }
1172
- function SummaryControls({
1173
- className,
1174
- summary,
1175
- title,
1176
- contentSide,
1177
- profileNode,
1178
- harnessNode,
1179
- modelNode,
1180
- reasoningNode,
1181
- trailing
1182
- }) {
1183
- return /* @__PURE__ */ jsx5(
1184
- CollapsibleControls,
1185
- {
1186
- className,
1187
- trigger: /* @__PURE__ */ jsxs4(Fragment2, { children: [
1188
- summary,
1189
- /* @__PURE__ */ jsx5(ChevronDown3, { className: "h-3.5 w-3.5 shrink-0 text-muted-foreground" })
1190
- ] }),
1191
- triggerTitle: title,
1192
- triggerClassName: cn(
1193
- // Container-less trigger (Codex-style): bare label + chevron docked in
1194
- // the composer footer, no border/card/shadow. Only a subtle rounded
1195
- // hover/open tint marks it as interactive.
1196
- "inline-flex h-8 items-center gap-1.5 rounded-md px-1.5",
1197
- "whitespace-nowrap text-xs font-medium text-foreground transition-colors",
1198
- "hover:bg-muted/50 focus:outline-none focus-visible:ring-1 focus-visible:ring-ring",
1199
- "data-[state=open]:bg-muted/50"
1200
- ),
1201
- contentSide,
1202
- contentClassName: "w-64 gap-2.5 border-border bg-popover p-2.5 text-popover-foreground shadow-lg",
1203
- profileNode,
1204
- harnessNode,
1205
- modelNode,
1206
- reasoningNode,
1207
- trailing
323
+ const deliverFiles = (files) => {
324
+ if (!onAttach || files.length === 0) return;
325
+ const { accepted, rejected } = validateComposerFiles(files, { accept });
326
+ if (rejected.length > 0) onRejectFiles?.(rejected);
327
+ if (accepted.length === 0) return;
328
+ const dt = new DataTransfer();
329
+ for (const file of accepted) dt.items.add(file);
330
+ onAttach(dt.files);
331
+ };
332
+ const handleDrop = (event) => {
333
+ if (!dropEnabled) return;
334
+ event.preventDefault();
335
+ event.stopPropagation();
336
+ dragDepth.current = 0;
337
+ setDragOver(false);
338
+ const files = event.dataTransfer?.files;
339
+ if (files?.length) deliverFiles(Array.from(files));
340
+ };
341
+ const renamePastedFiles = (files) => files.map((file) => {
342
+ if (file.type.startsWith("image/") && isGenericImageName(file.name)) {
343
+ pastedImageCount.current += 1;
344
+ return new File(
345
+ [file],
346
+ `pasted-image-${pastedImageCount.current}.${imageExtension(file)}`,
347
+ { type: file.type }
348
+ );
1208
349
  }
350
+ return file;
351
+ });
352
+ const handlePaste = (event) => {
353
+ if (!onAttach) return;
354
+ const clipboardFiles = event.clipboardData?.files;
355
+ if (!clipboardFiles || clipboardFiles.length === 0) return;
356
+ event.preventDefault();
357
+ deliverFiles(renamePastedFiles(Array.from(clipboardFiles)));
358
+ };
359
+ const handleEditorPasteFiles = (clipboardFiles) => {
360
+ if (!onAttach || clipboardFiles.length === 0) return false;
361
+ deliverFiles(renamePastedFiles(Array.from(clipboardFiles)));
362
+ return true;
363
+ };
364
+ const folderChips = attachments.filter((file) => file.kind === "folder");
365
+ const fileChips = attachments.filter((file) => file.kind !== "folder");
366
+ const attachButtonClass = cn(
367
+ "flex size-8 shrink-0 items-center justify-center rounded-lg text-muted-foreground transition-colors",
368
+ "hover:bg-surface-container-highest hover:text-foreground focus:outline-none focus-visible:ring-1 focus-visible:ring-ring",
369
+ "disabled:cursor-not-allowed disabled:opacity-50"
1209
370
  );
1210
- }
1211
- function CollapsibleControls({
1212
- className,
1213
- trigger,
1214
- triggerTitle,
1215
- triggerClassName,
1216
- contentSide,
1217
- contentClassName,
1218
- profileNode,
1219
- harnessNode,
1220
- modelNode,
1221
- reasoningNode,
1222
- trailing
1223
- }) {
1224
- return /* @__PURE__ */ jsxs4(
1225
- "div",
1226
- {
1227
- className: cn("flex items-center gap-2", className),
1228
- "data-testid": "agent-session-controls",
1229
- children: [
1230
- trailing && /* @__PURE__ */ jsx5("div", { className: "flex items-center gap-2", children: trailing }),
1231
- /* @__PURE__ */ jsxs4(DropdownMenu2.Root, { children: [
1232
- /* @__PURE__ */ jsx5(DropdownMenu2.Trigger, { asChild: true, children: /* @__PURE__ */ jsx5(
1233
- "button",
371
+ return /* @__PURE__ */ jsxs2("div", { className: cn("relative", className), children: [
372
+ dragOver && /* @__PURE__ */ jsx2("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__ */ jsxs2("div", { className: "text-center", children: [
373
+ /* @__PURE__ */ jsx2("span", { className: "mx-auto mb-2 flex size-11 items-center justify-center rounded-xl bg-primary/10 text-primary", children: /* @__PURE__ */ jsx2(Upload, { className: "size-5" }) }),
374
+ /* @__PURE__ */ jsx2("p", { className: "font-semibold text-foreground text-sm", children: dropTitle }),
375
+ /* @__PURE__ */ jsx2("p", { className: "mt-0.5 text-muted-foreground text-xs", children: dropDescription })
376
+ ] }) }),
377
+ /* @__PURE__ */ jsxs2(
378
+ "div",
379
+ {
380
+ "data-testid": "agent-composer",
381
+ onDragEnter: handleDragEnter,
382
+ onDragLeave: handleDragLeave,
383
+ onDragOver: handleDragOver,
384
+ onDrop: handleDrop,
385
+ className: cn(
386
+ "flex flex-col gap-2 rounded-2xl border border-[var(--md3-outline-variant)] bg-surface-container-high p-2.5 shadow-sm transition-colors",
387
+ "focus-within:border-primary/60",
388
+ disabled && "opacity-60"
389
+ ),
390
+ children: [
391
+ attachments.length > 0 && /* @__PURE__ */ jsx2("div", { className: "flex flex-wrap gap-1.5", children: [...folderChips, ...fileChips].map((file) => {
392
+ const isError = file.status === "error";
393
+ const isPending = file.status === "pending";
394
+ return /* @__PURE__ */ jsxs2(
395
+ "span",
396
+ {
397
+ title: isError ? file.errorMessage : void 0,
398
+ className: cn(
399
+ "inline-flex items-center gap-1.5 rounded-full border px-2.5 py-1 text-xs",
400
+ isError ? "border-[var(--status-error,#ff4d6d)]/40 text-[var(--status-error,#ff4d6d)]" : "border-[var(--md3-outline-variant)] bg-surface-container text-foreground",
401
+ isPending && "opacity-60"
402
+ ),
403
+ children: [
404
+ file.kind !== "folder" && file.previewUrl ? /* @__PURE__ */ jsx2(
405
+ "img",
406
+ {
407
+ src: file.previewUrl,
408
+ alt: "",
409
+ className: "size-8 rounded object-cover"
410
+ }
411
+ ) : file.kind === "folder" ? /* @__PURE__ */ jsx2(Folder, { className: "size-3 shrink-0" }) : /* @__PURE__ */ jsx2(Paperclip, { className: "size-3 shrink-0" }),
412
+ /* @__PURE__ */ jsx2("span", { className: "max-w-[150px] truncate", children: file.name }),
413
+ file.fileCount !== void 0 && /* @__PURE__ */ jsxs2("span", { className: "text-muted-foreground", children: [
414
+ "(",
415
+ file.fileCount,
416
+ ")"
417
+ ] }),
418
+ file.status === "uploading" && /* @__PURE__ */ jsx2(Loader2, { className: "size-3 animate-spin" }),
419
+ isError && file.errorMessage && /* @__PURE__ */ jsx2("span", { className: "max-w-[150px] truncate text-[var(--status-error,#ff4d6d)]/80", children: file.errorMessage }),
420
+ isError && onRetryFile && /* @__PURE__ */ jsx2(
421
+ "button",
422
+ {
423
+ type: "button",
424
+ "aria-label": `Retry upload ${file.name}`,
425
+ onClick: () => onRetryFile(file.id),
426
+ className: "rounded p-0.5 text-muted-foreground transition-colors hover:text-foreground focus:outline-none focus-visible:ring-1 focus-visible:ring-ring",
427
+ children: /* @__PURE__ */ jsx2(RotateCcw, { className: "size-3" })
428
+ }
429
+ ),
430
+ onRemoveFile && /* @__PURE__ */ jsx2(
431
+ "button",
432
+ {
433
+ type: "button",
434
+ "aria-label": `Remove ${file.name}`,
435
+ onClick: () => onRemoveFile(file.id),
436
+ className: "rounded p-0.5 text-muted-foreground transition-colors hover:text-foreground focus:outline-none focus-visible:ring-1 focus-visible:ring-ring",
437
+ children: /* @__PURE__ */ jsx2(X, { className: "size-3" })
438
+ }
439
+ )
440
+ ]
441
+ },
442
+ file.id
443
+ );
444
+ }) }),
445
+ mention ? /* @__PURE__ */ jsx2(
446
+ React.Suspense,
1234
447
  {
1235
- type: "button",
1236
- "aria-label": "Session controls",
1237
- title: triggerTitle,
1238
- className: triggerClassName,
1239
- children: trigger
448
+ fallback: /* @__PURE__ */ jsx2(
449
+ "textarea",
450
+ {
451
+ rows: minRows,
452
+ value,
453
+ disabled: true,
454
+ placeholder,
455
+ "aria-label": "Message input",
456
+ className: cn(
457
+ "w-full resize-none bg-transparent px-2 py-1 text-sm leading-relaxed text-foreground outline-none",
458
+ "placeholder:text-muted-foreground"
459
+ )
460
+ }
461
+ ),
462
+ children: /* @__PURE__ */ jsx2(
463
+ MentionEditor,
464
+ {
465
+ value,
466
+ onChange,
467
+ onSubmit: submit,
468
+ placeholder,
469
+ disabled,
470
+ autoFocus,
471
+ minRows,
472
+ maxHeight,
473
+ mention,
474
+ registerFocus: registerRichFocus,
475
+ onPasteFiles: onAttach ? handleEditorPasteFiles : void 0
476
+ }
477
+ )
1240
478
  }
1241
- ) }),
1242
- /* @__PURE__ */ jsx5(DropdownMenu2.Portal, { children: /* @__PURE__ */ jsxs4(
1243
- DropdownMenu2.Content,
479
+ ) : /* @__PURE__ */ jsx2(
480
+ "textarea",
1244
481
  {
1245
- side: contentSide,
1246
- align: "end",
1247
- sideOffset: 8,
1248
- onInteractOutside: (event) => {
1249
- const target = event.target;
1250
- if (target?.closest(
1251
- "[data-radix-popper-content-wrapper],[data-radix-menu-content],[role=menu],[role=listbox]"
1252
- )) {
482
+ ref: textareaRef,
483
+ rows: minRows,
484
+ value,
485
+ disabled,
486
+ autoFocus,
487
+ onChange: (event) => onChange(event.target.value),
488
+ onInput: (event) => resize(event.currentTarget),
489
+ onPaste: handlePaste,
490
+ onKeyDown: (event) => {
491
+ if (event.key === "Enter" && !event.shiftKey && !event.nativeEvent.isComposing && event.keyCode !== 229) {
1253
492
  event.preventDefault();
493
+ submit();
1254
494
  }
1255
495
  },
496
+ placeholder,
497
+ "aria-label": "Message input",
1256
498
  className: cn(
1257
- "z-50 flex flex-col rounded-[var(--radius-md)] border",
1258
- "data-[state=open]:animate-in data-[state=closed]:animate-out",
1259
- "data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
1260
- "data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
1261
- contentClassName
1262
- ),
1263
- children: [
1264
- profileNode && /* @__PURE__ */ jsx5(GearSection, { label: "Agent", children: profileNode }),
1265
- harnessNode && /* @__PURE__ */ jsx5(GearSection, { label: "Harness", children: harnessNode }),
1266
- modelNode && /* @__PURE__ */ jsx5(GearSection, { label: "Model", children: modelNode }),
1267
- reasoningNode && /* @__PURE__ */ jsx5(GearSection, { label: "Effort", children: reasoningNode })
1268
- ]
499
+ "w-full resize-none bg-transparent px-2 py-1 text-sm leading-relaxed text-foreground outline-none",
500
+ "placeholder:text-muted-foreground"
501
+ )
1269
502
  }
1270
- ) })
1271
- ] })
1272
- ]
1273
- }
1274
- );
503
+ ),
504
+ /* @__PURE__ */ jsxs2("div", { className: "flex items-end gap-2", children: [
505
+ onAttach && /* @__PURE__ */ jsxs2(Fragment, { children: [
506
+ /* @__PURE__ */ jsx2(
507
+ "button",
508
+ {
509
+ type: "button",
510
+ onClick: () => fileInputRef.current?.click(),
511
+ disabled,
512
+ "aria-label": "Attach files",
513
+ title: "Attach files",
514
+ className: attachButtonClass,
515
+ children: /* @__PURE__ */ jsx2(Paperclip, { className: "size-4" })
516
+ }
517
+ ),
518
+ /* @__PURE__ */ jsx2(
519
+ "input",
520
+ {
521
+ ref: fileInputRef,
522
+ type: "file",
523
+ multiple: true,
524
+ className: "hidden",
525
+ accept,
526
+ onChange: (event) => {
527
+ if (event.target.files?.length)
528
+ deliverFiles(Array.from(event.target.files));
529
+ event.target.value = "";
530
+ }
531
+ }
532
+ )
533
+ ] }),
534
+ onAttachFolder && /* @__PURE__ */ jsxs2(Fragment, { children: [
535
+ /* @__PURE__ */ jsx2(
536
+ "button",
537
+ {
538
+ type: "button",
539
+ onClick: () => folderInputRef.current?.click(),
540
+ disabled,
541
+ "aria-label": "Attach folder",
542
+ title: "Attach folder",
543
+ className: attachButtonClass,
544
+ children: /* @__PURE__ */ jsx2(Folder, { className: "size-4" })
545
+ }
546
+ ),
547
+ /* @__PURE__ */ jsx2(
548
+ "input",
549
+ {
550
+ ref: folderInputRef,
551
+ type: "file",
552
+ multiple: true,
553
+ className: "hidden",
554
+ onChange: (event) => {
555
+ if (event.target.files?.length)
556
+ (onAttachFolder ?? onAttach)?.(event.target.files);
557
+ event.target.value = "";
558
+ },
559
+ ...{ webkitdirectory: "" }
560
+ }
561
+ )
562
+ ] }),
563
+ /* @__PURE__ */ jsxs2("div", { className: "flex min-w-0 flex-1 items-center gap-2 overflow-x-auto overscroll-x-contain [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden", children: [
564
+ controls,
565
+ trailing && /* @__PURE__ */ jsx2("div", { className: "ml-auto flex shrink-0 items-center gap-2", children: trailing })
566
+ ] }),
567
+ busy && onCancel ? /* @__PURE__ */ jsx2(
568
+ "button",
569
+ {
570
+ type: "button",
571
+ onClick: onCancel,
572
+ "aria-label": "Stop response",
573
+ className: cn(
574
+ "flex size-9 shrink-0 items-center justify-center rounded-full",
575
+ "bg-[var(--status-error,#ff4d6d)]/15 text-[var(--status-error,#ff4d6d)] transition-colors",
576
+ "hover:bg-[var(--status-error,#ff4d6d)]/25"
577
+ ),
578
+ children: /* @__PURE__ */ jsx2(Square, { className: "size-3.5 fill-current" })
579
+ }
580
+ ) : /* @__PURE__ */ jsx2(
581
+ "button",
582
+ {
583
+ type: "button",
584
+ onClick: submit,
585
+ disabled: !canSend,
586
+ "aria-label": sendLabel,
587
+ className: cn(
588
+ "flex size-9 shrink-0 items-center justify-center rounded-full bg-primary text-primary-foreground",
589
+ "transition-opacity hover:opacity-90 disabled:opacity-40"
590
+ ),
591
+ children: busy ? /* @__PURE__ */ jsx2(Loader2, { className: "size-4 animate-spin" }) : /* @__PURE__ */ jsx2(ArrowUp, { className: "size-4" })
592
+ }
593
+ )
594
+ ] })
595
+ ]
596
+ }
597
+ ),
598
+ focusShortcut && /* @__PURE__ */ jsx2("div", { className: "mt-1.5 flex justify-end px-1", children: /* @__PURE__ */ jsxs2("span", { className: "text-muted-foreground text-xs", children: [
599
+ /* @__PURE__ */ jsx2("kbd", { className: "rounded border border-[var(--md3-outline-variant)] bg-surface-container px-1 py-0.5 text-[10px]", children: "Cmd" }),
600
+ /* @__PURE__ */ jsx2("kbd", { className: "ml-0.5 rounded border border-[var(--md3-outline-variant)] bg-surface-container px-1 py-0.5 text-[10px]", children: "L" }),
601
+ /* @__PURE__ */ jsx2("span", { className: "ml-1", children: "to focus" })
602
+ ] }) })
603
+ ] });
1275
604
  }
1276
605
 
1277
- // src/chat/agent-composer.tsx
1278
- import {
1279
- ArrowUp,
1280
- Folder,
1281
- Loader2,
1282
- Paperclip,
1283
- RotateCcw,
1284
- Square,
1285
- Upload,
1286
- X
1287
- } from "lucide-react";
1288
- import * as React6 from "react";
606
+ // src/chat/agent-profile-picker.tsx
607
+ import { Bot, Check, ChevronDown as ChevronDown2, Pencil, Plus as Plus2, Trash2 } from "lucide-react";
608
+ import * as React5 from "react";
1289
609
 
1290
- // src/chat/attachment-validation.ts
1291
- function isAcceptedType(file, accept) {
1292
- if (!accept || accept.trim().length === 0) return true;
1293
- const patterns = accept.split(",").map((pattern) => pattern.trim()).filter((pattern) => pattern.length > 0);
1294
- if (patterns.length === 0) return true;
1295
- const name = file.name.toLowerCase();
1296
- const type = (file.type || "").toLowerCase();
1297
- return patterns.some((pattern) => {
1298
- const lowerPattern = pattern.toLowerCase();
1299
- if (lowerPattern.startsWith(".")) {
1300
- return name.endsWith(lowerPattern);
1301
- }
1302
- if (lowerPattern.endsWith("/*")) {
1303
- const prefix = lowerPattern.slice(0, -1);
1304
- return type.startsWith(prefix);
610
+ // src/lib/use-click-outside.ts
611
+ import * as React2 from "react";
612
+ function useClickOutside(onOutside, extras) {
613
+ const ref = React2.useRef(null);
614
+ const extrasRef = React2.useRef(extras);
615
+ extrasRef.current = extras;
616
+ React2.useEffect(() => {
617
+ function handler(event) {
618
+ const target = event.target;
619
+ if (!ref.current || ref.current.contains(target)) return;
620
+ if (extrasRef.current?.some((extra) => extra.current?.contains(target))) return;
621
+ onOutside();
1305
622
  }
1306
- return type === lowerPattern;
1307
- });
623
+ document.addEventListener("mousedown", handler);
624
+ return () => document.removeEventListener("mousedown", handler);
625
+ }, [onOutside]);
626
+ return ref;
1308
627
  }
1309
- function validateComposerFiles(files, config) {
1310
- const { maxSizeBytes, maxCount, currentCount = 0, accept } = config;
1311
- const list = Array.isArray(files) ? files : Array.from(files);
1312
- const accepted = [];
1313
- const rejected = [];
1314
- for (const file of list) {
1315
- if (!isAcceptedType(file, accept)) {
1316
- rejected.push({
1317
- file,
1318
- reason: `"${file.name}" is not an accepted file type (${accept}).`
1319
- });
1320
- continue;
1321
- }
1322
- if (maxSizeBytes !== void 0 && file.size > maxSizeBytes) {
1323
- rejected.push({
1324
- file,
1325
- reason: `"${file.name}" (${formatBytes(file.size)}) exceeds the ${formatBytes(maxSizeBytes)} limit.`
1326
- });
1327
- continue;
1328
- }
1329
- if (maxCount !== void 0 && currentCount + accepted.length >= maxCount) {
1330
- rejected.push({
1331
- file,
1332
- reason: `"${file.name}" was not added \u2014 the ${maxCount}-file limit is already reached.`
1333
- });
1334
- continue;
1335
- }
1336
- accepted.push(file);
628
+
629
+ // src/chat/anchored-popover.tsx
630
+ import * as React3 from "react";
631
+ import { createPortal } from "react-dom";
632
+ import { jsx as jsx3 } from "react/jsx-runtime";
633
+ var AnchoredPopover = React3.forwardRef(
634
+ function AnchoredPopover2({ anchorRef, side, children, className, style, ...rest }, forwardedRef) {
635
+ const innerRef = React3.useRef(null);
636
+ const setRefs = (node) => {
637
+ innerRef.current = node;
638
+ if (typeof forwardedRef === "function") forwardedRef(node);
639
+ else if (forwardedRef) forwardedRef.current = node;
640
+ };
641
+ const [position, setPosition] = React3.useState(null);
642
+ const place = React3.useCallback(() => {
643
+ const anchor = anchorRef.current;
644
+ if (!anchor) return;
645
+ const rect = anchor.getBoundingClientRect();
646
+ const width = innerRef.current?.offsetWidth ?? 0;
647
+ const margin = 8;
648
+ const left = Math.max(
649
+ margin,
650
+ Math.min(rect.left, window.innerWidth - width - margin)
651
+ );
652
+ setPosition(
653
+ side === "top" ? { left, bottom: window.innerHeight - rect.top + margin } : { left, top: rect.bottom + margin }
654
+ );
655
+ }, [anchorRef, side]);
656
+ React3.useLayoutEffect(() => {
657
+ place();
658
+ window.addEventListener("resize", place);
659
+ window.addEventListener("scroll", place, true);
660
+ return () => {
661
+ window.removeEventListener("resize", place);
662
+ window.removeEventListener("scroll", place, true);
663
+ };
664
+ }, [place]);
665
+ return createPortal(
666
+ /* @__PURE__ */ jsx3(
667
+ "div",
668
+ {
669
+ ...rest,
670
+ ref: setRefs,
671
+ className: cn("fixed z-50", className),
672
+ style: {
673
+ ...style,
674
+ pointerEvents: "auto",
675
+ ...position ?? { visibility: "hidden", left: 0, top: 0 }
676
+ },
677
+ children
678
+ }
679
+ ),
680
+ document.body
681
+ );
1337
682
  }
1338
- return { accepted, rejected };
1339
- }
683
+ );
1340
684
 
1341
- // src/chat/agent-composer.tsx
1342
- import { Fragment as Fragment3, jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
1343
- var IMAGE_EXTENSION_BY_MIME = {
1344
- "image/png": "png",
1345
- "image/jpeg": "jpg",
1346
- "image/jpg": "jpg",
1347
- "image/gif": "gif",
1348
- "image/webp": "webp",
1349
- "image/bmp": "bmp",
1350
- "image/svg+xml": "svg"
1351
- };
1352
- function isGenericImageName(name) {
1353
- return /^(image)?(\.[a-z0-9]+)?$/i.test(name.trim());
1354
- }
1355
- function imageExtension(file) {
1356
- const fromMime = IMAGE_EXTENSION_BY_MIME[file.type.toLowerCase()];
1357
- if (fromMime) return fromMime;
1358
- const fromName = /\.([a-z0-9]+)$/i.exec(file.name)?.[1];
1359
- return fromName?.toLowerCase() ?? "png";
1360
- }
1361
- var MentionEditor = React6.lazy(() => import("./mention-editor-3Q6MYOIU.js"));
1362
- function AgentComposer({
1363
- value,
1364
- onChange,
1365
- onSubmit,
1366
- placeholder = "Send a message\u2026",
1367
- disabled,
1368
- busy,
1369
- onCancel,
1370
- harness,
1371
- profile,
1372
- model,
1373
- reasoning,
1374
- context = "chat",
1375
- controls,
1376
- trailing,
1377
- onAttach,
1378
- onAttachFolder,
1379
- attachments = [],
1380
- onRemoveFile,
1381
- onRetryFile,
1382
- accept,
1383
- onRejectFiles,
1384
- dropTitle = "Drop files to add context",
1385
- dropDescription = "They attach to your next message.",
1386
- canSubmitWhileBusy = false,
1387
- canSubmitAttachmentsOnly = false,
1388
- mention,
1389
- focusShortcut = false,
1390
- minRows = 2,
1391
- maxHeight = 200,
685
+ // src/chat/informative-lock.tsx
686
+ import { Lock, Plus } from "lucide-react";
687
+ import * as React4 from "react";
688
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
689
+ function InformativeLock({
690
+ children,
691
+ ariaLabel,
692
+ lockTitle,
693
+ lockBody,
694
+ newChatLabel,
695
+ onNewChat,
1392
696
  className,
1393
- sendLabel = "Send",
1394
- autoFocus
697
+ triggerClassName,
698
+ popoverClassName,
699
+ side = "top"
1395
700
  }) {
1396
- const textareaRef = React6.useRef(null);
1397
- const richFocusRef = React6.useRef(null);
1398
- const fileInputRef = React6.useRef(null);
1399
- const folderInputRef = React6.useRef(null);
1400
- const [dragOver, setDragOver] = React6.useState(false);
1401
- const dragDepth = React6.useRef(0);
1402
- const pastedImageCount = React6.useRef(0);
1403
- const resize = React6.useCallback(
1404
- (el) => {
1405
- el.style.height = "auto";
1406
- el.style.height = `${Math.min(el.scrollHeight, maxHeight)}px`;
1407
- },
1408
- [maxHeight]
1409
- );
1410
- React6.useEffect(() => {
1411
- if (textareaRef.current) resize(textareaRef.current);
1412
- }, [value, resize]);
1413
- React6.useEffect(() => {
1414
- if (!focusShortcut || disabled) return;
1415
- function onKeyDown(event) {
1416
- if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "l") {
1417
- event.preventDefault();
1418
- if (mention) richFocusRef.current?.();
1419
- else textareaRef.current?.focus();
1420
- }
701
+ const [open, setOpen] = React4.useState(false);
702
+ const closeTimer = React4.useRef(null);
703
+ const popoverRef = React4.useRef(null);
704
+ const ref = useClickOutside(() => setOpen(false), [popoverRef]);
705
+ const cancelClose = React4.useCallback(() => {
706
+ if (closeTimer.current) {
707
+ clearTimeout(closeTimer.current);
708
+ closeTimer.current = null;
1421
709
  }
1422
- document.addEventListener("keydown", onKeyDown);
1423
- return () => document.removeEventListener("keydown", onKeyDown);
1424
- }, [focusShortcut, disabled, mention]);
1425
- const canSend = !disabled && (!busy || canSubmitWhileBusy) && (value.trim().length > 0 || canSubmitAttachmentsOnly && attachments.length > 0);
1426
- const submit = () => {
1427
- if (!canSend) return;
1428
- onSubmit();
1429
- };
1430
- const registerRichFocus = React6.useCallback((focus) => {
1431
- richFocusRef.current = focus;
1432
710
  }, []);
1433
- const dropEnabled = Boolean(onAttach);
1434
- const handleDragEnter = (event) => {
1435
- if (!dropEnabled) return;
1436
- event.preventDefault();
1437
- event.stopPropagation();
1438
- dragDepth.current++;
1439
- if (event.dataTransfer?.types.includes("Files")) setDragOver(true);
1440
- };
1441
- const handleDragLeave = (event) => {
1442
- if (!dropEnabled) return;
1443
- event.preventDefault();
1444
- event.stopPropagation();
1445
- dragDepth.current--;
1446
- if (dragDepth.current <= 0) {
1447
- dragDepth.current = 0;
1448
- setDragOver(false);
1449
- }
1450
- };
1451
- const handleDragOver = (event) => {
1452
- if (!dropEnabled) return;
1453
- event.preventDefault();
1454
- event.stopPropagation();
1455
- if (event.dataTransfer) event.dataTransfer.dropEffect = "copy";
1456
- };
1457
- const deliverFiles = (files) => {
1458
- if (!onAttach || files.length === 0) return;
1459
- const { accepted, rejected } = validateComposerFiles(files, { accept });
1460
- if (rejected.length > 0) onRejectFiles?.(rejected);
1461
- if (accepted.length === 0) return;
1462
- const dt = new DataTransfer();
1463
- for (const file of accepted) dt.items.add(file);
1464
- onAttach(dt.files);
1465
- };
1466
- const handleDrop = (event) => {
1467
- if (!dropEnabled) return;
1468
- event.preventDefault();
1469
- event.stopPropagation();
1470
- dragDepth.current = 0;
1471
- setDragOver(false);
1472
- const files = event.dataTransfer?.files;
1473
- if (files?.length) deliverFiles(Array.from(files));
1474
- };
1475
- const renamePastedFiles = (files) => files.map((file) => {
1476
- if (file.type.startsWith("image/") && isGenericImageName(file.name)) {
1477
- pastedImageCount.current += 1;
1478
- return new File(
1479
- [file],
1480
- `pasted-image-${pastedImageCount.current}.${imageExtension(file)}`,
1481
- { type: file.type }
1482
- );
711
+ const scheduleClose = React4.useCallback(() => {
712
+ cancelClose();
713
+ closeTimer.current = setTimeout(() => setOpen(false), 150);
714
+ }, [cancelClose]);
715
+ React4.useEffect(() => cancelClose, [cancelClose]);
716
+ return /* @__PURE__ */ jsxs3(
717
+ "div",
718
+ {
719
+ ref,
720
+ className: cn("relative inline-flex", className),
721
+ onMouseEnter: () => {
722
+ cancelClose();
723
+ setOpen(true);
724
+ },
725
+ onMouseLeave: scheduleClose,
726
+ onKeyDown: (event) => {
727
+ if (event.key === "Escape") setOpen(false);
728
+ },
729
+ children: [
730
+ /* @__PURE__ */ jsx4(
731
+ "button",
732
+ {
733
+ type: "button",
734
+ onClick: () => setOpen(true),
735
+ "data-state": open ? "open" : "closed",
736
+ "aria-label": ariaLabel,
737
+ "aria-haspopup": "dialog",
738
+ "aria-expanded": open,
739
+ className: cn(
740
+ "inline-flex h-8 items-center gap-1.5 rounded-lg border border-[var(--md3-outline-variant)] bg-surface-container px-2.5 opacity-60",
741
+ "text-xs font-medium text-foreground shadow-sm transition-colors",
742
+ "hover:border-[var(--md3-outline-variant)] hover:bg-surface-container-high focus:outline-none focus-visible:ring-1 focus-visible:ring-ring",
743
+ "data-[state=open]:border-[var(--md3-outline-variant)] data-[state=open]:bg-surface-container-high",
744
+ triggerClassName
745
+ ),
746
+ children
747
+ }
748
+ ),
749
+ open && /* @__PURE__ */ jsxs3(
750
+ AnchoredPopover,
751
+ {
752
+ ref: popoverRef,
753
+ anchorRef: ref,
754
+ side,
755
+ role: "dialog",
756
+ onMouseEnter: () => {
757
+ cancelClose();
758
+ setOpen(true);
759
+ },
760
+ onMouseLeave: scheduleClose,
761
+ className: cn(
762
+ "w-72 rounded-[var(--radius-md)] border border-[var(--md3-outline-variant)] bg-surface-container-highest p-3",
763
+ "shadow-[0_8px_30px_rgba(0,0,0,0.45)] ring-1 ring-[#ffffff14]",
764
+ popoverClassName
765
+ ),
766
+ children: [
767
+ /* @__PURE__ */ jsxs3("p", { className: "flex items-start gap-2 text-sm font-medium text-foreground", children: [
768
+ /* @__PURE__ */ jsx4(Lock, { className: "mt-0.5 h-3.5 w-3.5 shrink-0 text-muted-foreground" }),
769
+ /* @__PURE__ */ jsx4("span", { children: lockTitle })
770
+ ] }),
771
+ /* @__PURE__ */ jsx4("p", { className: "mt-1.5 text-xs text-muted-foreground", children: lockBody }),
772
+ /* @__PURE__ */ jsxs3(
773
+ "button",
774
+ {
775
+ type: "button",
776
+ onClick: () => {
777
+ setOpen(false);
778
+ onNewChat();
779
+ },
780
+ className: cn(
781
+ "mt-3 flex w-full items-center justify-center gap-1.5 rounded-md px-2.5 py-2 text-sm font-medium text-foreground",
782
+ "bg-primary/10 ring-1 ring-inset ring-primary/25 transition-colors",
783
+ "hover:bg-primary/15 focus:outline-none focus-visible:ring-1 focus-visible:ring-ring"
784
+ ),
785
+ children: [
786
+ /* @__PURE__ */ jsx4(Plus, { className: "h-4 w-4" }),
787
+ newChatLabel
788
+ ]
789
+ }
790
+ )
791
+ ]
792
+ }
793
+ )
794
+ ]
1483
795
  }
1484
- return file;
1485
- });
1486
- const handlePaste = (event) => {
1487
- if (!onAttach) return;
1488
- const clipboardFiles = event.clipboardData?.files;
1489
- if (!clipboardFiles || clipboardFiles.length === 0) return;
1490
- event.preventDefault();
1491
- deliverFiles(renamePastedFiles(Array.from(clipboardFiles)));
1492
- };
1493
- const handleEditorPasteFiles = (clipboardFiles) => {
1494
- if (!onAttach || clipboardFiles.length === 0) return false;
1495
- deliverFiles(renamePastedFiles(Array.from(clipboardFiles)));
1496
- return true;
1497
- };
1498
- const folderChips = attachments.filter((file) => file.kind === "folder");
1499
- const fileChips = attachments.filter((file) => file.kind !== "folder");
1500
- const attachButtonClass = cn(
1501
- "flex size-8 shrink-0 items-center justify-center rounded-lg text-muted-foreground transition-colors",
1502
- "hover:bg-surface-container-highest hover:text-foreground focus:outline-none focus-visible:ring-1 focus-visible:ring-ring",
1503
- "disabled:cursor-not-allowed disabled:opacity-50"
1504
796
  );
1505
- return /* @__PURE__ */ jsxs5("div", { className: cn("relative", className), children: [
1506
- dragOver && /* @__PURE__ */ jsx6("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__ */ jsxs5("div", { className: "text-center", children: [
1507
- /* @__PURE__ */ jsx6("span", { className: "mx-auto mb-2 flex size-11 items-center justify-center rounded-xl bg-primary/10 text-primary", children: /* @__PURE__ */ jsx6(Upload, { className: "size-5" }) }),
1508
- /* @__PURE__ */ jsx6("p", { className: "font-semibold text-foreground text-sm", children: dropTitle }),
1509
- /* @__PURE__ */ jsx6("p", { className: "mt-0.5 text-muted-foreground text-xs", children: dropDescription })
1510
- ] }) }),
1511
- /* @__PURE__ */ jsxs5(
1512
- "div",
797
+ }
798
+
799
+ // src/chat/agent-profile-picker.tsx
800
+ import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
801
+ function profileSubtitle(profile) {
802
+ if (profile.description) return profile.description;
803
+ const count = profile.capabilities?.length ?? 0;
804
+ return `${count} ${count === 1 ? "tool group" : "tool groups"}`;
805
+ }
806
+ function AgentProfilePicker({
807
+ value,
808
+ onChange,
809
+ profiles,
810
+ capabilities,
811
+ onCreate,
812
+ onUpdate,
813
+ onDelete,
814
+ disabled,
815
+ locked,
816
+ lockReason,
817
+ onNewChat,
818
+ className,
819
+ triggerClassName,
820
+ popoverClassName,
821
+ side = "top"
822
+ }) {
823
+ const [open, setOpen] = React5.useState(false);
824
+ const [composing, setComposing] = React5.useState(
825
+ null
826
+ );
827
+ const popoverRef = React5.useRef(null);
828
+ const ref = useClickOutside(() => {
829
+ setOpen(false);
830
+ setComposing(null);
831
+ }, [popoverRef]);
832
+ const canAuthor = Boolean(capabilities && (onCreate || onUpdate));
833
+ const builtins = profiles.filter((profile) => profile.builtin);
834
+ const custom = profiles.filter((profile) => !profile.builtin);
835
+ const selected = profiles.find((profile) => profile.id === value);
836
+ const selectedName = selected?.name ?? value;
837
+ React5.useEffect(() => {
838
+ if (!locked) return;
839
+ setOpen(false);
840
+ setComposing(null);
841
+ }, [locked]);
842
+ function select(id) {
843
+ onChange(id);
844
+ setOpen(false);
845
+ }
846
+ async function save(draft) {
847
+ if (draft.id) await onUpdate?.(draft.id, draft);
848
+ else await onCreate?.(draft);
849
+ setComposing(null);
850
+ }
851
+ if (locked && onNewChat) {
852
+ return /* @__PURE__ */ jsxs4(
853
+ InformativeLock,
1513
854
  {
1514
- "data-testid": "agent-composer",
1515
- onDragEnter: handleDragEnter,
1516
- onDragLeave: handleDragLeave,
1517
- onDragOver: handleDragOver,
1518
- onDrop: handleDrop,
855
+ ariaLabel: "Agent profile (locked)",
856
+ lockTitle: `Profile fixed to ${selectedName} for this conversation.`,
857
+ lockBody: "Conversations stay on one profile.",
858
+ newChatLabel: "New chat to switch profile",
859
+ onNewChat,
860
+ className,
861
+ triggerClassName,
862
+ popoverClassName,
863
+ side,
864
+ children: [
865
+ /* @__PURE__ */ jsx5(Bot, { className: "h-3.5 w-3.5 text-muted-foreground" }),
866
+ /* @__PURE__ */ jsx5("span", { className: "max-w-[140px] truncate", children: selectedName })
867
+ ]
868
+ }
869
+ );
870
+ }
871
+ return /* @__PURE__ */ jsxs4("div", { ref, className: cn("relative inline-flex", className), children: [
872
+ /* @__PURE__ */ jsxs4(
873
+ "button",
874
+ {
875
+ type: "button",
876
+ disabled: disabled || locked,
877
+ title: locked ? lockReason : void 0,
878
+ onClick: () => setOpen((v) => !v),
1519
879
  className: cn(
1520
- "flex flex-col gap-2 rounded-2xl border border-[var(--md3-outline-variant)] bg-surface-container-high p-2.5 shadow-sm transition-colors",
1521
- "focus-within:border-primary/60",
1522
- disabled && "opacity-60"
880
+ "inline-flex h-8 items-center gap-1.5 rounded-lg border border-[var(--md3-outline-variant)] bg-surface-container px-2.5",
881
+ "text-xs font-medium text-foreground shadow-sm transition-colors",
882
+ "hover:border-[var(--md3-outline-variant)] hover:bg-surface-container-high focus:outline-none focus-visible:ring-1 focus-visible:ring-ring",
883
+ "data-[state=open]:border-[var(--md3-outline-variant)] data-[state=open]:bg-surface-container-high",
884
+ "disabled:cursor-not-allowed disabled:opacity-60",
885
+ triggerClassName
1523
886
  ),
887
+ "data-state": open ? "open" : "closed",
888
+ "aria-label": "Agent profile",
1524
889
  children: [
1525
- attachments.length > 0 && /* @__PURE__ */ jsx6("div", { className: "flex flex-wrap gap-1.5", children: [...folderChips, ...fileChips].map((file) => {
1526
- const isError = file.status === "error";
1527
- const isPending = file.status === "pending";
1528
- return /* @__PURE__ */ jsxs5(
1529
- "span",
1530
- {
1531
- title: isError ? file.errorMessage : void 0,
1532
- className: cn(
1533
- "inline-flex items-center gap-1.5 rounded-full border px-2.5 py-1 text-xs",
1534
- isError ? "border-[var(--status-error,#ff4d6d)]/40 text-[var(--status-error,#ff4d6d)]" : "border-[var(--md3-outline-variant)] bg-surface-container text-foreground",
1535
- isPending && "opacity-60"
1536
- ),
1537
- children: [
1538
- file.kind !== "folder" && file.previewUrl ? /* @__PURE__ */ jsx6(
1539
- "img",
1540
- {
1541
- src: file.previewUrl,
1542
- alt: "",
1543
- className: "size-8 rounded object-cover"
1544
- }
1545
- ) : file.kind === "folder" ? /* @__PURE__ */ jsx6(Folder, { className: "size-3 shrink-0" }) : /* @__PURE__ */ jsx6(Paperclip, { className: "size-3 shrink-0" }),
1546
- /* @__PURE__ */ jsx6("span", { className: "max-w-[150px] truncate", children: file.name }),
1547
- file.fileCount !== void 0 && /* @__PURE__ */ jsxs5("span", { className: "text-muted-foreground", children: [
1548
- "(",
1549
- file.fileCount,
1550
- ")"
1551
- ] }),
1552
- file.status === "uploading" && /* @__PURE__ */ jsx6(Loader2, { className: "size-3 animate-spin" }),
1553
- isError && file.errorMessage && /* @__PURE__ */ jsx6("span", { className: "max-w-[150px] truncate text-[var(--status-error,#ff4d6d)]/80", children: file.errorMessage }),
1554
- isError && onRetryFile && /* @__PURE__ */ jsx6(
1555
- "button",
1556
- {
1557
- type: "button",
1558
- "aria-label": `Retry upload ${file.name}`,
1559
- onClick: () => onRetryFile(file.id),
1560
- className: "rounded p-0.5 text-muted-foreground transition-colors hover:text-foreground focus:outline-none focus-visible:ring-1 focus-visible:ring-ring",
1561
- children: /* @__PURE__ */ jsx6(RotateCcw, { className: "size-3" })
1562
- }
1563
- ),
1564
- onRemoveFile && /* @__PURE__ */ jsx6(
1565
- "button",
1566
- {
1567
- type: "button",
1568
- "aria-label": `Remove ${file.name}`,
1569
- onClick: () => onRemoveFile(file.id),
1570
- className: "rounded p-0.5 text-muted-foreground transition-colors hover:text-foreground focus:outline-none focus-visible:ring-1 focus-visible:ring-ring",
1571
- children: /* @__PURE__ */ jsx6(X, { className: "size-3" })
1572
- }
1573
- )
1574
- ]
1575
- },
1576
- file.id
1577
- );
1578
- }) }),
1579
- mention ? /* @__PURE__ */ jsx6(
1580
- React6.Suspense,
890
+ /* @__PURE__ */ jsx5(Bot, { className: "h-3.5 w-3.5 text-muted-foreground" }),
891
+ /* @__PURE__ */ jsx5("span", { className: "max-w-[140px] truncate", children: selectedName }),
892
+ !locked && /* @__PURE__ */ jsx5(ChevronDown2, { className: "ml-auto h-3.5 w-3.5 shrink-0 text-muted-foreground" })
893
+ ]
894
+ }
895
+ ),
896
+ open && !locked && /* @__PURE__ */ jsx5(
897
+ AnchoredPopover,
898
+ {
899
+ ref: popoverRef,
900
+ anchorRef: ref,
901
+ side,
902
+ className: cn(
903
+ "w-[min(320px,calc(100vw-2rem))] overflow-hidden rounded-[var(--radius-md)] border border-[var(--md3-outline-variant)] bg-surface-container-highest p-1",
904
+ "shadow-[0_8px_30px_rgba(0,0,0,0.45)] ring-1 ring-[#ffffff14]",
905
+ popoverClassName
906
+ ),
907
+ "data-testid": "agent-profile-popover",
908
+ children: composing ? /* @__PURE__ */ jsx5(
909
+ ProfileForm,
910
+ {
911
+ capabilities: capabilities ?? [],
912
+ initial: composing === "new" ? void 0 : composing,
913
+ onCancel: () => setComposing(null),
914
+ onSave: save
915
+ }
916
+ ) : /* @__PURE__ */ jsxs4("div", { className: "max-h-[min(60vh,420px)] overflow-y-auto", children: [
917
+ /* @__PURE__ */ jsx5(SectionHeader, { children: "Modes" }),
918
+ builtins.map((profile) => /* @__PURE__ */ jsx5(
919
+ ProfileRow,
1581
920
  {
1582
- fallback: /* @__PURE__ */ jsx6(
1583
- "textarea",
1584
- {
1585
- rows: minRows,
1586
- value,
1587
- disabled: true,
1588
- placeholder,
1589
- "aria-label": "Message input",
1590
- className: cn(
1591
- "w-full resize-none bg-transparent px-2 py-1 text-sm leading-relaxed text-foreground outline-none",
1592
- "placeholder:text-muted-foreground"
1593
- )
1594
- }
1595
- ),
1596
- children: /* @__PURE__ */ jsx6(
1597
- MentionEditor,
1598
- {
1599
- value,
1600
- onChange,
1601
- onSubmit: submit,
1602
- placeholder,
1603
- disabled,
1604
- autoFocus,
1605
- minRows,
1606
- maxHeight,
1607
- mention,
1608
- registerFocus: registerRichFocus,
1609
- onPasteFiles: onAttach ? handleEditorPasteFiles : void 0
1610
- }
1611
- )
921
+ active: profile.id === value,
922
+ title: profile.name,
923
+ subtitle: profileSubtitle(profile),
924
+ onSelect: () => select(profile.id)
925
+ },
926
+ profile.id
927
+ )),
928
+ custom.length > 0 && /* @__PURE__ */ jsx5(SectionHeader, { children: "Your agents" }),
929
+ custom.map((profile) => /* @__PURE__ */ jsx5(
930
+ ProfileRow,
931
+ {
932
+ active: profile.id === value,
933
+ title: profile.name,
934
+ subtitle: profileSubtitle(profile),
935
+ onSelect: () => select(profile.id),
936
+ onEdit: canAuthor ? () => setComposing(profile) : void 0,
937
+ onDelete: onDelete ? () => void onDelete(profile.id) : void 0
938
+ },
939
+ profile.id
940
+ )),
941
+ canAuthor && /* @__PURE__ */ jsxs4(
942
+ "button",
943
+ {
944
+ type: "button",
945
+ onClick: () => setComposing("new"),
946
+ 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",
947
+ children: [
948
+ /* @__PURE__ */ jsx5(Plus2, { className: "h-4 w-4" }),
949
+ " New agent\u2026"
950
+ ]
1612
951
  }
1613
- ) : /* @__PURE__ */ jsx6(
1614
- "textarea",
952
+ )
953
+ ] })
954
+ }
955
+ )
956
+ ] });
957
+ }
958
+ function SectionHeader({ children }) {
959
+ return /* @__PURE__ */ jsx5("div", { className: "px-2.5 pb-1 pt-2 text-[10px] font-semibold uppercase tracking-wide text-muted-foreground/70", children });
960
+ }
961
+ function ProfileRow({
962
+ active,
963
+ title,
964
+ subtitle,
965
+ onSelect,
966
+ onEdit,
967
+ onDelete
968
+ }) {
969
+ return /* @__PURE__ */ jsxs4(
970
+ "div",
971
+ {
972
+ className: cn(
973
+ "group flex items-center gap-2 rounded-md px-2.5 py-2 text-sm transition-colors",
974
+ active ? "bg-[var(--accent-surface-soft)] text-[var(--accent-text)]" : "hover:bg-accent/40"
975
+ ),
976
+ children: [
977
+ /* @__PURE__ */ jsxs4(
978
+ "button",
979
+ {
980
+ type: "button",
981
+ onClick: onSelect,
982
+ className: "flex min-w-0 flex-1 items-center gap-2 text-left",
983
+ children: [
984
+ /* @__PURE__ */ jsx5("span", { className: "flex h-4 w-4 shrink-0 items-center justify-center", children: active && /* @__PURE__ */ jsx5(Check, { className: "h-3.5 w-3.5" }) }),
985
+ /* @__PURE__ */ jsxs4("span", { className: "min-w-0", children: [
986
+ /* @__PURE__ */ jsx5("span", { className: "block truncate font-medium", children: title }),
987
+ /* @__PURE__ */ jsx5("span", { className: "block truncate text-xs text-muted-foreground", children: subtitle })
988
+ ] })
989
+ ]
990
+ }
991
+ ),
992
+ (onEdit || onDelete) && /* @__PURE__ */ jsxs4("span", { className: "flex shrink-0 items-center gap-1 opacity-0 transition-opacity group-hover:opacity-100", children: [
993
+ onEdit && /* @__PURE__ */ jsx5(
994
+ "button",
1615
995
  {
1616
- ref: textareaRef,
1617
- rows: minRows,
1618
- value,
1619
- disabled,
1620
- autoFocus,
1621
- onChange: (event) => onChange(event.target.value),
1622
- onInput: (event) => resize(event.currentTarget),
1623
- onPaste: handlePaste,
1624
- onKeyDown: (event) => {
1625
- if (event.key === "Enter" && !event.shiftKey && !event.nativeEvent.isComposing && event.keyCode !== 229) {
1626
- event.preventDefault();
1627
- submit();
1628
- }
1629
- },
1630
- placeholder,
1631
- "aria-label": "Message input",
1632
- className: cn(
1633
- "w-full resize-none bg-transparent px-2 py-1 text-sm leading-relaxed text-foreground outline-none",
1634
- "placeholder:text-muted-foreground"
1635
- )
996
+ type: "button",
997
+ onClick: onEdit,
998
+ title: "Edit",
999
+ className: "rounded p-1 text-muted-foreground hover:text-foreground",
1000
+ children: /* @__PURE__ */ jsx5(Pencil, { className: "h-3 w-3" })
1636
1001
  }
1637
1002
  ),
1638
- /* @__PURE__ */ jsxs5("div", { className: "flex items-end gap-2", children: [
1639
- onAttach && /* @__PURE__ */ jsxs5(Fragment3, { children: [
1640
- /* @__PURE__ */ jsx6(
1641
- "button",
1642
- {
1643
- type: "button",
1644
- onClick: () => fileInputRef.current?.click(),
1645
- disabled,
1646
- "aria-label": "Attach files",
1647
- title: "Attach files",
1648
- className: attachButtonClass,
1649
- children: /* @__PURE__ */ jsx6(Paperclip, { className: "size-4" })
1650
- }
1651
- ),
1652
- /* @__PURE__ */ jsx6(
1653
- "input",
1654
- {
1655
- ref: fileInputRef,
1656
- type: "file",
1657
- multiple: true,
1658
- className: "hidden",
1659
- accept,
1660
- onChange: (event) => {
1661
- if (event.target.files?.length)
1662
- deliverFiles(Array.from(event.target.files));
1663
- event.target.value = "";
1664
- }
1665
- }
1666
- )
1667
- ] }),
1668
- onAttachFolder && /* @__PURE__ */ jsxs5(Fragment3, { children: [
1669
- /* @__PURE__ */ jsx6(
1670
- "button",
1671
- {
1672
- type: "button",
1673
- onClick: () => folderInputRef.current?.click(),
1674
- disabled,
1675
- "aria-label": "Attach folder",
1676
- title: "Attach folder",
1677
- className: attachButtonClass,
1678
- children: /* @__PURE__ */ jsx6(Folder, { className: "size-4" })
1679
- }
1680
- ),
1681
- /* @__PURE__ */ jsx6(
1682
- "input",
1683
- {
1684
- ref: folderInputRef,
1685
- type: "file",
1686
- multiple: true,
1687
- className: "hidden",
1688
- onChange: (event) => {
1689
- if (event.target.files?.length)
1690
- (onAttachFolder ?? onAttach)?.(event.target.files);
1691
- event.target.value = "";
1692
- },
1693
- ...{ webkitdirectory: "" }
1694
- }
1695
- )
1696
- ] }),
1697
- /* @__PURE__ */ jsxs5("div", { className: "flex min-w-0 flex-1 items-center gap-2 overflow-x-auto overscroll-x-contain [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden", children: [
1698
- controls ?? /* @__PURE__ */ jsx6(
1699
- AgentSessionControls,
1700
- {
1701
- context,
1702
- harness,
1703
- profile,
1704
- model,
1705
- reasoning
1706
- }
1707
- ),
1708
- trailing && /* @__PURE__ */ jsx6("div", { className: "ml-auto flex shrink-0 items-center gap-2", children: trailing })
1709
- ] }),
1710
- busy && onCancel ? /* @__PURE__ */ jsx6(
1711
- "button",
1712
- {
1713
- type: "button",
1714
- onClick: onCancel,
1715
- "aria-label": "Stop response",
1716
- className: cn(
1717
- "flex size-9 shrink-0 items-center justify-center rounded-full",
1718
- "bg-[var(--status-error,#ff4d6d)]/15 text-[var(--status-error,#ff4d6d)] transition-colors",
1719
- "hover:bg-[var(--status-error,#ff4d6d)]/25"
1720
- ),
1721
- children: /* @__PURE__ */ jsx6(Square, { className: "size-3.5 fill-current" })
1722
- }
1723
- ) : /* @__PURE__ */ jsx6(
1724
- "button",
1003
+ onDelete && /* @__PURE__ */ jsx5(
1004
+ "button",
1005
+ {
1006
+ type: "button",
1007
+ onClick: onDelete,
1008
+ title: "Delete",
1009
+ className: "rounded p-1 text-muted-foreground hover:text-[var(--status-error,#ff4d6d)]",
1010
+ children: /* @__PURE__ */ jsx5(Trash2, { className: "h-3 w-3" })
1011
+ }
1012
+ )
1013
+ ] })
1014
+ ]
1015
+ }
1016
+ );
1017
+ }
1018
+ function ProfileForm({
1019
+ capabilities,
1020
+ initial,
1021
+ onCancel,
1022
+ onSave
1023
+ }) {
1024
+ const [name, setName] = React5.useState(initial?.name ?? "");
1025
+ const [caps, setCaps] = React5.useState(
1026
+ initial?.capabilities ? [...initial.capabilities] : capabilities.map((c) => c.id)
1027
+ );
1028
+ const [instructions, setInstructions] = React5.useState(initial?.instructions ?? "");
1029
+ const toggle = (id) => setCaps(
1030
+ (prev) => prev.includes(id) ? prev.filter((c) => c !== id) : [...prev, id]
1031
+ );
1032
+ return /* @__PURE__ */ jsxs4("div", { className: "space-y-3 p-2.5", children: [
1033
+ /* @__PURE__ */ jsx5(
1034
+ "input",
1035
+ {
1036
+ autoFocus: true,
1037
+ value: name,
1038
+ onChange: (event) => setName(event.target.value),
1039
+ placeholder: "Agent name (e.g. Tax Reviewer)",
1040
+ 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"
1041
+ }
1042
+ ),
1043
+ capabilities.length > 0 && /* @__PURE__ */ jsxs4("div", { children: [
1044
+ /* @__PURE__ */ jsx5("p", { className: "mb-1.5 text-[10px] font-semibold uppercase tracking-wide text-muted-foreground/70", children: "Tools" }),
1045
+ /* @__PURE__ */ jsx5("div", { className: "space-y-0.5", children: capabilities.map((cap) => /* @__PURE__ */ jsxs4(
1046
+ "label",
1047
+ {
1048
+ className: "flex cursor-pointer items-start gap-2 rounded-md px-2 py-1.5 hover:bg-accent/40",
1049
+ children: [
1050
+ /* @__PURE__ */ jsx5(
1051
+ "input",
1725
1052
  {
1726
- type: "button",
1727
- onClick: submit,
1728
- disabled: !canSend,
1729
- "aria-label": sendLabel,
1730
- className: cn(
1731
- "flex size-9 shrink-0 items-center justify-center rounded-full bg-primary text-primary-foreground",
1732
- "transition-opacity hover:opacity-90 disabled:opacity-40"
1733
- ),
1734
- children: busy ? /* @__PURE__ */ jsx6(Loader2, { className: "size-4 animate-spin" }) : /* @__PURE__ */ jsx6(ArrowUp, { className: "size-4" })
1053
+ type: "checkbox",
1054
+ checked: caps.includes(cap.id),
1055
+ onChange: () => toggle(cap.id),
1056
+ className: "mt-0.5"
1735
1057
  }
1736
- )
1737
- ] })
1738
- ]
1058
+ ),
1059
+ /* @__PURE__ */ jsxs4("span", { className: "min-w-0", children: [
1060
+ /* @__PURE__ */ jsx5("span", { className: "block text-sm font-medium", children: cap.label }),
1061
+ cap.description && /* @__PURE__ */ jsx5("span", { className: "block text-xs text-muted-foreground", children: cap.description })
1062
+ ] })
1063
+ ]
1064
+ },
1065
+ cap.id
1066
+ )) })
1067
+ ] }),
1068
+ /* @__PURE__ */ jsx5(
1069
+ "textarea",
1070
+ {
1071
+ value: instructions,
1072
+ onChange: (event) => setInstructions(event.target.value),
1073
+ rows: 2,
1074
+ placeholder: "Instructions (optional) \u2014 how this agent should behave",
1075
+ 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"
1739
1076
  }
1740
1077
  ),
1741
- focusShortcut && /* @__PURE__ */ jsx6("div", { className: "mt-1.5 flex justify-end px-1", children: /* @__PURE__ */ jsxs5("span", { className: "text-muted-foreground text-xs", children: [
1742
- /* @__PURE__ */ jsx6("kbd", { className: "rounded border border-[var(--md3-outline-variant)] bg-surface-container px-1 py-0.5 text-[10px]", children: "Cmd" }),
1743
- /* @__PURE__ */ jsx6("kbd", { className: "ml-0.5 rounded border border-[var(--md3-outline-variant)] bg-surface-container px-1 py-0.5 text-[10px]", children: "L" }),
1744
- /* @__PURE__ */ jsx6("span", { className: "ml-1", children: "to focus" })
1745
- ] }) })
1078
+ /* @__PURE__ */ jsxs4("div", { className: "flex justify-end gap-2", children: [
1079
+ /* @__PURE__ */ jsx5(
1080
+ "button",
1081
+ {
1082
+ type: "button",
1083
+ onClick: onCancel,
1084
+ className: "rounded-lg px-3 py-1.5 text-sm text-muted-foreground hover:bg-accent/40",
1085
+ children: "Cancel"
1086
+ }
1087
+ ),
1088
+ /* @__PURE__ */ jsx5(
1089
+ "button",
1090
+ {
1091
+ type: "button",
1092
+ disabled: !name.trim(),
1093
+ onClick: () => onSave({
1094
+ id: initial?.id,
1095
+ name: name.trim(),
1096
+ capabilities: caps,
1097
+ instructions: instructions.trim()
1098
+ }),
1099
+ 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",
1100
+ children: "Save agent"
1101
+ }
1102
+ )
1103
+ ] })
1746
1104
  ] });
1747
1105
  }
1748
1106
 
1107
+ // src/chat/harness-model-compat.ts
1108
+ import { modelProvider, snapHarnessToModel } from "@tangle-network/agent-interface";
1109
+
1749
1110
  // src/chat/artifact-agent-dock.tsx
1750
- import * as React7 from "react";
1111
+ import * as React6 from "react";
1751
1112
  import { Sparkles as Sparkles2, Send, Square as Square2, X as X2 } from "lucide-react";
1752
1113
  import { ChatMessage } from "@tangle-network/ui/chat";
1753
- import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
1114
+ import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
1754
1115
  function defaultScopeLabel(scope) {
1755
1116
  if (scope.label) return scope.label;
1756
1117
  switch (scope.kind) {
@@ -1796,23 +1157,23 @@ function ArtifactAgentDock({
1796
1157
  onError,
1797
1158
  className
1798
1159
  }) {
1799
- const [threadId, setThreadId] = React7.useState(null);
1800
- const [messages, setMessages] = React7.useState([]);
1801
- const [composer, setComposer] = React7.useState(defaultPrompt ?? "");
1802
- const [streamContent, setStreamContent] = React7.useState("");
1803
- const [sending, setSending] = React7.useState(false);
1804
- const [loading, setLoading] = React7.useState(false);
1805
- const abortRef = React7.useRef(null);
1806
- const scrollRef = React7.useRef(null);
1807
- const scopeTokenRef = React7.useRef(0);
1808
- const reportError = React7.useCallback(
1160
+ const [threadId, setThreadId] = React6.useState(null);
1161
+ const [messages, setMessages] = React6.useState([]);
1162
+ const [composer, setComposer] = React6.useState(defaultPrompt ?? "");
1163
+ const [streamContent, setStreamContent] = React6.useState("");
1164
+ const [sending, setSending] = React6.useState(false);
1165
+ const [loading, setLoading] = React6.useState(false);
1166
+ const abortRef = React6.useRef(null);
1167
+ const scrollRef = React6.useRef(null);
1168
+ const scopeTokenRef = React6.useRef(0);
1169
+ const reportError = React6.useCallback(
1809
1170
  (message) => {
1810
1171
  if (onError) onError(message);
1811
1172
  else if (typeof console !== "undefined") console.error("[ArtifactAgentDock]", message);
1812
1173
  },
1813
1174
  [onError]
1814
1175
  );
1815
- React7.useEffect(() => {
1176
+ React6.useEffect(() => {
1816
1177
  if (!open) return;
1817
1178
  scopeTokenRef.current += 1;
1818
1179
  abortRef.current?.abort();
@@ -1848,11 +1209,11 @@ function ArtifactAgentDock({
1848
1209
  cancelled = true;
1849
1210
  };
1850
1211
  }, [open, scope.kind, scope.key, transport, reportError]);
1851
- React7.useEffect(() => {
1212
+ React6.useEffect(() => {
1852
1213
  if (!scrollRef.current) return;
1853
1214
  scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
1854
1215
  }, [messages.length, streamContent]);
1855
- const send = React7.useCallback(async () => {
1216
+ const send = React6.useCallback(async () => {
1856
1217
  const text = composer.trim();
1857
1218
  if (!text || !threadId || sending) return;
1858
1219
  const token = scopeTokenRef.current;
@@ -1902,7 +1263,7 @@ function ArtifactAgentDock({
1902
1263
  if (abortRef.current === controller) abortRef.current = null;
1903
1264
  }
1904
1265
  }, [composer, threadId, sending, transport, reportError]);
1905
- const cancel = React7.useCallback(() => {
1266
+ const cancel = React6.useCallback(() => {
1906
1267
  if (threadId && transport.cancel) {
1907
1268
  try {
1908
1269
  void Promise.resolve(transport.cancel({ threadId })).catch(() => {
@@ -1916,7 +1277,7 @@ function ArtifactAgentDock({
1916
1277
  }, [threadId, transport]);
1917
1278
  if (!open) return null;
1918
1279
  const heading = defaultScopeLabel(scope);
1919
- return /* @__PURE__ */ jsxs6(
1280
+ return /* @__PURE__ */ jsxs5(
1920
1281
  "aside",
1921
1282
  {
1922
1283
  className: cn(
@@ -1925,38 +1286,38 @@ function ArtifactAgentDock({
1925
1286
  ),
1926
1287
  "aria-label": `Agent chat about ${heading}`,
1927
1288
  children: [
1928
- /* @__PURE__ */ jsxs6("header", { className: "flex items-center justify-between border-b border-[var(--md3-outline-variant)] bg-surface-container-low px-4 py-3", children: [
1929
- /* @__PURE__ */ jsxs6("div", { className: "flex min-w-0 items-center gap-2", children: [
1930
- /* @__PURE__ */ jsx7(Sparkles2, { className: "h-4 w-4 shrink-0 text-muted-foreground" }),
1931
- /* @__PURE__ */ jsxs6("div", { className: "min-w-0", children: [
1932
- /* @__PURE__ */ jsx7("div", { className: "text-[10px] font-medium uppercase tracking-wider text-muted-foreground", children: "Discussing" }),
1933
- /* @__PURE__ */ jsx7("div", { className: "truncate text-sm font-semibold text-foreground", children: heading })
1289
+ /* @__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: [
1290
+ /* @__PURE__ */ jsxs5("div", { className: "flex min-w-0 items-center gap-2", children: [
1291
+ /* @__PURE__ */ jsx6(Sparkles2, { className: "h-4 w-4 shrink-0 text-muted-foreground" }),
1292
+ /* @__PURE__ */ jsxs5("div", { className: "min-w-0", children: [
1293
+ /* @__PURE__ */ jsx6("div", { className: "text-[10px] font-medium uppercase tracking-wider text-muted-foreground", children: "Discussing" }),
1294
+ /* @__PURE__ */ jsx6("div", { className: "truncate text-sm font-semibold text-foreground", children: heading })
1934
1295
  ] })
1935
1296
  ] }),
1936
- /* @__PURE__ */ jsx7(
1297
+ /* @__PURE__ */ jsx6(
1937
1298
  "button",
1938
1299
  {
1939
1300
  type: "button",
1940
1301
  onClick: () => onOpenChange(false),
1941
1302
  "aria-label": "Close artifact chat",
1942
1303
  className: "rounded-md p-1 text-muted-foreground transition-colors hover:bg-surface-container-high hover:text-foreground",
1943
- children: /* @__PURE__ */ jsx7(X2, { className: "h-4 w-4" })
1304
+ children: /* @__PURE__ */ jsx6(X2, { className: "h-4 w-4" })
1944
1305
  }
1945
1306
  )
1946
1307
  ] }),
1947
- /* @__PURE__ */ jsx7("div", { ref: scrollRef, className: "flex-1 overflow-y-auto px-3 py-4", children: loading ? /* @__PURE__ */ jsx7("div", { className: "px-3 py-8 text-center text-xs text-muted-foreground", children: "Loading conversation\u2026" }) : messages.length === 0 && !streamContent ? /* @__PURE__ */ jsxs6("div", { className: "px-3 py-12 text-center", children: [
1948
- /* @__PURE__ */ jsx7(Sparkles2, { className: "mx-auto mb-3 h-5 w-5 text-muted-foreground/40" }),
1949
- /* @__PURE__ */ jsxs6("p", { className: "text-sm text-foreground", children: [
1308
+ /* @__PURE__ */ jsx6("div", { ref: scrollRef, className: "flex-1 overflow-y-auto px-3 py-4", children: loading ? /* @__PURE__ */ jsx6("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: [
1309
+ /* @__PURE__ */ jsx6(Sparkles2, { className: "mx-auto mb-3 h-5 w-5 text-muted-foreground/40" }),
1310
+ /* @__PURE__ */ jsxs5("p", { className: "text-sm text-foreground", children: [
1950
1311
  "Ask the agent about this ",
1951
1312
  scope.kind === "vault-file" ? "document" : scope.kind,
1952
1313
  "."
1953
1314
  ] }),
1954
- /* @__PURE__ */ jsx7("p", { className: "mt-1 text-xs text-muted-foreground", children: "It can read the current contents and propose edits, rewrites, or follow-ups." })
1955
- ] }) : /* @__PURE__ */ jsxs6("div", { className: "space-y-3", children: [
1956
- messages.map((m) => /* @__PURE__ */ jsx7(ChatMessage, { role: m.role, content: m.content, timestamp: m.createdAt }, m.id)),
1957
- streamContent && /* @__PURE__ */ jsx7(ChatMessage, { role: "assistant", content: streamContent, isStreaming: true })
1315
+ /* @__PURE__ */ jsx6("p", { className: "mt-1 text-xs text-muted-foreground", children: "It can read the current contents and propose edits, rewrites, or follow-ups." })
1316
+ ] }) : /* @__PURE__ */ jsxs5("div", { className: "space-y-3", children: [
1317
+ messages.map((m) => /* @__PURE__ */ jsx6(ChatMessage, { role: m.role, content: m.content, timestamp: m.createdAt }, m.id)),
1318
+ streamContent && /* @__PURE__ */ jsx6(ChatMessage, { role: "assistant", content: streamContent, isStreaming: true })
1958
1319
  ] }) }),
1959
- /* @__PURE__ */ jsx7(
1320
+ /* @__PURE__ */ jsx6(
1960
1321
  "form",
1961
1322
  {
1962
1323
  className: "border-t border-[var(--md3-outline-variant)] bg-surface-container-low p-3",
@@ -1964,8 +1325,8 @@ function ArtifactAgentDock({
1964
1325
  e.preventDefault();
1965
1326
  void send();
1966
1327
  },
1967
- children: /* @__PURE__ */ jsxs6("div", { className: "flex items-end gap-2", children: [
1968
- /* @__PURE__ */ jsx7(
1328
+ children: /* @__PURE__ */ jsxs5("div", { className: "flex items-end gap-2", children: [
1329
+ /* @__PURE__ */ jsx6(
1969
1330
  "textarea",
1970
1331
  {
1971
1332
  value: composer,
@@ -1982,23 +1343,23 @@ function ArtifactAgentDock({
1982
1343
  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"
1983
1344
  }
1984
1345
  ),
1985
- sending ? /* @__PURE__ */ jsx7(
1346
+ sending ? /* @__PURE__ */ jsx6(
1986
1347
  "button",
1987
1348
  {
1988
1349
  type: "button",
1989
1350
  onClick: cancel,
1990
1351
  "aria-label": "Cancel",
1991
1352
  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",
1992
- children: /* @__PURE__ */ jsx7(Square2, { className: "h-4 w-4" })
1353
+ children: /* @__PURE__ */ jsx6(Square2, { className: "h-4 w-4" })
1993
1354
  }
1994
- ) : /* @__PURE__ */ jsx7(
1355
+ ) : /* @__PURE__ */ jsx6(
1995
1356
  "button",
1996
1357
  {
1997
1358
  type: "submit",
1998
1359
  disabled: !composer.trim() || !threadId,
1999
1360
  "aria-label": "Send",
2000
1361
  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",
2001
- children: /* @__PURE__ */ jsx7(Send, { className: "h-4 w-4" })
1362
+ children: /* @__PURE__ */ jsx6(Send, { className: "h-4 w-4" })
2002
1363
  }
2003
1364
  )
2004
1365
  ] })
@@ -2088,15 +1449,12 @@ export {
2088
1449
  DEFAULT_REASONING_LEVEL_OPTIONS,
2089
1450
  HARNESS_REASONING_OPTIONS,
2090
1451
  ReasoningLevelPicker,
2091
- isModelCompatibleWithHarness,
2092
- snapModelToHarness,
2093
- modelProvider,
2094
- snapHarnessToModel,
2095
- AgentProfilePicker,
2096
- AgentSessionControls,
2097
1452
  isAcceptedType,
2098
1453
  validateComposerFiles,
2099
1454
  AgentComposer,
1455
+ AgentProfilePicker,
1456
+ modelProvider,
1457
+ snapHarnessToModel,
2100
1458
  ArtifactAgentDock,
2101
1459
  createFetchTransport,
2102
1460
  AgentTimeline,