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