@tangle-network/sandbox-ui 0.57.0 → 0.59.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.
@@ -146,78 +146,308 @@ function ReasoningLevelPicker({
146
146
  // src/chat/agent-session-controls.tsx
147
147
  import { reasoningEffortsFor } from "@tangle-network/agent-interface";
148
148
  import * as DropdownMenu2 from "@radix-ui/react-dropdown-menu";
149
- import { ChevronDown as ChevronDown2, Lock, SlidersHorizontal } from "lucide-react";
149
+ import { ChevronDown as ChevronDown3, Lock, SlidersHorizontal } from "lucide-react";
150
150
 
151
151
  // src/chat/harness-model-compat.ts
152
- var HARNESS_MODEL_POLICIES = {
153
- opencode: { providers: null, preferred: [] },
154
- "claude-code": {
155
- providers: ["anthropic"],
156
- preferred: [
157
- /^anthropic\/claude-opus-[\d.-]+$/,
158
- /^anthropic\/claude-sonnet-[\d.-]+$/,
159
- /^anthropic\//
160
- ]
161
- },
162
- codex: {
163
- providers: ["openai"],
164
- // Standard frontier tier first (gpt-N / gpt-N.N), then any gpt
165
- // variant (mini/nano), then anything OpenAI.
166
- preferred: [/^openai\/gpt-\d+(\.\d+)?$/, /^openai\/gpt/, /^openai\//]
167
- },
168
- amp: { providers: null, preferred: [] },
169
- "factory-droids": { providers: null, preferred: [] },
170
- "cli-base": { providers: null, preferred: [] },
171
- "kimi-code": {
172
- providers: ["moonshot"],
173
- preferred: [/^moonshot\//]
174
- },
175
- openclaw: { providers: null, preferred: [] },
176
- nanoclaw: { providers: null, preferred: [] },
177
- hermes: { providers: null, preferred: [] }
178
- };
179
- var PROVIDER_PREFERRED_HARNESS = {
180
- anthropic: "claude-code",
181
- openai: "codex",
182
- moonshot: "kimi-code"
183
- };
184
- function modelProvider(modelId) {
185
- const slash = modelId.indexOf("/");
186
- return slash > 0 ? modelId.slice(0, slash) : null;
187
- }
152
+ import {
153
+ harnessSupportsModel,
154
+ snapModelToHarness as snapModelIdToHarness
155
+ } from "@tangle-network/agent-interface";
156
+ import { modelProvider, snapHarnessToModel } from "@tangle-network/agent-interface";
188
157
  function isModelCompatibleWithHarness(harness, modelId) {
189
- const policy = HARNESS_MODEL_POLICIES[harness];
190
- if (!policy || policy.providers === null) return true;
191
- const provider = modelProvider(modelId);
192
- if (!provider) return true;
193
- return policy.providers.includes(provider);
158
+ return harnessSupportsModel(harness, modelId);
194
159
  }
195
- var numericDesc = new Intl.Collator(void 0, {
196
- numeric: true,
197
- sensitivity: "base"
198
- });
199
160
  function snapModelToHarness(harness, modelId, models) {
200
- if (isModelCompatibleWithHarness(harness, modelId)) return modelId;
201
- const ids = models.map(canonicalModelId);
202
- const policy = HARNESS_MODEL_POLICIES[harness];
203
- if (!policy) return modelId;
204
- for (const pattern of policy.preferred) {
205
- const matches = ids.filter((id) => pattern.test(id)).sort((a, b) => numericDesc.compare(b, a));
206
- if (matches.length > 0) return matches[0];
161
+ return snapModelIdToHarness(harness, modelId, models.map(canonicalModelId));
162
+ }
163
+
164
+ // src/chat/agent-profile-picker.tsx
165
+ import { Bot, Check, ChevronDown as ChevronDown2, Pencil, Plus, Trash2 } from "lucide-react";
166
+ import * as React from "react";
167
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
168
+ function useClickOutside(onOutside) {
169
+ const ref = React.useRef(null);
170
+ React.useEffect(() => {
171
+ function handler(event) {
172
+ if (ref.current && !ref.current.contains(event.target)) {
173
+ onOutside();
174
+ }
175
+ }
176
+ document.addEventListener("mousedown", handler);
177
+ return () => document.removeEventListener("mousedown", handler);
178
+ }, [onOutside]);
179
+ return ref;
180
+ }
181
+ function profileSubtitle(profile) {
182
+ if (profile.description) return profile.description;
183
+ const count = profile.capabilities?.length ?? 0;
184
+ return `${count} ${count === 1 ? "tool group" : "tool groups"}`;
185
+ }
186
+ function AgentProfilePicker({
187
+ value,
188
+ onChange,
189
+ profiles,
190
+ capabilities,
191
+ onCreate,
192
+ onUpdate,
193
+ onDelete,
194
+ disabled,
195
+ className,
196
+ side = "top"
197
+ }) {
198
+ const [open, setOpen] = React.useState(false);
199
+ const [composing, setComposing] = React.useState(
200
+ null
201
+ );
202
+ const ref = useClickOutside(() => {
203
+ setOpen(false);
204
+ setComposing(null);
205
+ });
206
+ const canAuthor = Boolean(capabilities && (onCreate || onUpdate));
207
+ const builtins = profiles.filter((profile) => profile.builtin);
208
+ const custom = profiles.filter((profile) => !profile.builtin);
209
+ const selected = profiles.find((profile) => profile.id === value);
210
+ function select(id) {
211
+ onChange(id);
212
+ setOpen(false);
213
+ }
214
+ async function save(draft) {
215
+ if (draft.id) await onUpdate?.(draft.id, draft);
216
+ else await onCreate?.(draft);
217
+ setComposing(null);
207
218
  }
208
- const fallback = ids.find(
209
- (id) => isModelCompatibleWithHarness(harness, id)
219
+ return /* @__PURE__ */ jsxs2("div", { ref, className: cn("relative inline-flex", className), children: [
220
+ /* @__PURE__ */ jsxs2(
221
+ "button",
222
+ {
223
+ type: "button",
224
+ disabled,
225
+ onClick: () => setOpen((v) => !v),
226
+ className: cn(
227
+ "inline-flex h-8 items-center gap-1.5 rounded-lg border border-[var(--md3-outline-variant)] bg-surface-container px-2.5",
228
+ "text-xs font-medium text-foreground shadow-sm transition-colors",
229
+ "hover:border-[var(--md3-outline-variant)] hover:bg-surface-container-high focus:outline-none focus-visible:ring-1 focus-visible:ring-ring",
230
+ "data-[state=open]:border-[var(--md3-outline-variant)] data-[state=open]:bg-surface-container-high",
231
+ "disabled:cursor-not-allowed disabled:opacity-60"
232
+ ),
233
+ "data-state": open ? "open" : "closed",
234
+ "aria-label": "Agent profile",
235
+ children: [
236
+ /* @__PURE__ */ jsx2(Bot, { className: "h-3.5 w-3.5 text-muted-foreground" }),
237
+ /* @__PURE__ */ jsx2("span", { className: "max-w-[140px] truncate", children: selected?.name ?? value }),
238
+ /* @__PURE__ */ jsx2(ChevronDown2, { className: "h-3.5 w-3.5 text-muted-foreground" })
239
+ ]
240
+ }
241
+ ),
242
+ open && /* @__PURE__ */ jsx2(
243
+ "div",
244
+ {
245
+ className: cn(
246
+ "absolute left-0 z-50 w-[320px] overflow-hidden rounded-[var(--radius-md)] border border-[var(--md3-outline-variant)] bg-surface-container-highest p-1",
247
+ "shadow-[0_8px_30px_rgba(0,0,0,0.45)] ring-1 ring-[#ffffff14]",
248
+ side === "top" ? "bottom-full mb-2" : "top-full mt-2"
249
+ ),
250
+ children: composing ? /* @__PURE__ */ jsx2(
251
+ ProfileForm,
252
+ {
253
+ capabilities: capabilities ?? [],
254
+ initial: composing === "new" ? void 0 : composing,
255
+ onCancel: () => setComposing(null),
256
+ onSave: save
257
+ }
258
+ ) : /* @__PURE__ */ jsxs2("div", { className: "max-h-[min(60vh,420px)] overflow-y-auto", children: [
259
+ /* @__PURE__ */ jsx2(SectionHeader, { children: "Modes" }),
260
+ builtins.map((profile) => /* @__PURE__ */ jsx2(
261
+ ProfileRow,
262
+ {
263
+ active: profile.id === value,
264
+ title: profile.name,
265
+ subtitle: profileSubtitle(profile),
266
+ onSelect: () => select(profile.id)
267
+ },
268
+ profile.id
269
+ )),
270
+ custom.length > 0 && /* @__PURE__ */ jsx2(SectionHeader, { children: "Your agents" }),
271
+ custom.map((profile) => /* @__PURE__ */ jsx2(
272
+ ProfileRow,
273
+ {
274
+ active: profile.id === value,
275
+ title: profile.name,
276
+ subtitle: profileSubtitle(profile),
277
+ onSelect: () => select(profile.id),
278
+ onEdit: canAuthor ? () => setComposing(profile) : void 0,
279
+ onDelete: onDelete ? () => void onDelete(profile.id) : void 0
280
+ },
281
+ profile.id
282
+ )),
283
+ canAuthor && /* @__PURE__ */ jsxs2(
284
+ "button",
285
+ {
286
+ type: "button",
287
+ onClick: () => setComposing("new"),
288
+ className: "mt-1 flex w-full items-center gap-2 rounded-md px-2.5 py-2 text-left text-sm text-[var(--accent-text)] transition-colors hover:bg-accent/40",
289
+ children: [
290
+ /* @__PURE__ */ jsx2(Plus, { className: "h-4 w-4" }),
291
+ " New agent\u2026"
292
+ ]
293
+ }
294
+ )
295
+ ] })
296
+ }
297
+ )
298
+ ] });
299
+ }
300
+ function SectionHeader({ children }) {
301
+ return /* @__PURE__ */ jsx2("div", { className: "px-2.5 pb-1 pt-2 text-[10px] font-semibold uppercase tracking-wide text-muted-foreground/70", children });
302
+ }
303
+ function ProfileRow({
304
+ active,
305
+ title,
306
+ subtitle,
307
+ onSelect,
308
+ onEdit,
309
+ onDelete
310
+ }) {
311
+ return /* @__PURE__ */ jsxs2(
312
+ "div",
313
+ {
314
+ className: cn(
315
+ "group flex items-center gap-2 rounded-md px-2.5 py-2 text-sm transition-colors",
316
+ active ? "bg-[var(--accent-surface-soft)] text-[var(--accent-text)]" : "hover:bg-accent/40"
317
+ ),
318
+ children: [
319
+ /* @__PURE__ */ jsxs2(
320
+ "button",
321
+ {
322
+ type: "button",
323
+ onClick: onSelect,
324
+ className: "flex min-w-0 flex-1 items-center gap-2 text-left",
325
+ children: [
326
+ /* @__PURE__ */ jsx2("span", { className: "flex h-4 w-4 shrink-0 items-center justify-center", children: active && /* @__PURE__ */ jsx2(Check, { className: "h-3.5 w-3.5" }) }),
327
+ /* @__PURE__ */ jsxs2("span", { className: "min-w-0", children: [
328
+ /* @__PURE__ */ jsx2("span", { className: "block truncate font-medium", children: title }),
329
+ /* @__PURE__ */ jsx2("span", { className: "block truncate text-xs text-muted-foreground", children: subtitle })
330
+ ] })
331
+ ]
332
+ }
333
+ ),
334
+ (onEdit || onDelete) && /* @__PURE__ */ jsxs2("span", { className: "flex shrink-0 items-center gap-1 opacity-0 transition-opacity group-hover:opacity-100", children: [
335
+ onEdit && /* @__PURE__ */ jsx2(
336
+ "button",
337
+ {
338
+ type: "button",
339
+ onClick: onEdit,
340
+ title: "Edit",
341
+ className: "rounded p-1 text-muted-foreground hover:text-foreground",
342
+ children: /* @__PURE__ */ jsx2(Pencil, { className: "h-3 w-3" })
343
+ }
344
+ ),
345
+ onDelete && /* @__PURE__ */ jsx2(
346
+ "button",
347
+ {
348
+ type: "button",
349
+ onClick: onDelete,
350
+ title: "Delete",
351
+ className: "rounded p-1 text-muted-foreground hover:text-[var(--status-error,#ff4d6d)]",
352
+ children: /* @__PURE__ */ jsx2(Trash2, { className: "h-3 w-3" })
353
+ }
354
+ )
355
+ ] })
356
+ ]
357
+ }
210
358
  );
211
- return fallback ?? modelId;
212
359
  }
213
- function snapHarnessToModel(harness, modelId) {
214
- if (isModelCompatibleWithHarness(harness, modelId)) return harness;
215
- const provider = modelProvider(modelId);
216
- return provider && PROVIDER_PREFERRED_HARNESS[provider] || "opencode";
360
+ function ProfileForm({
361
+ capabilities,
362
+ initial,
363
+ onCancel,
364
+ onSave
365
+ }) {
366
+ const [name, setName] = React.useState(initial?.name ?? "");
367
+ const [caps, setCaps] = React.useState(
368
+ initial?.capabilities ? [...initial.capabilities] : capabilities.map((c) => c.id)
369
+ );
370
+ const [instructions, setInstructions] = React.useState(initial?.instructions ?? "");
371
+ const toggle = (id) => setCaps(
372
+ (prev) => prev.includes(id) ? prev.filter((c) => c !== id) : [...prev, id]
373
+ );
374
+ return /* @__PURE__ */ jsxs2("div", { className: "space-y-3 p-2.5", children: [
375
+ /* @__PURE__ */ jsx2(
376
+ "input",
377
+ {
378
+ autoFocus: true,
379
+ value: name,
380
+ onChange: (event) => setName(event.target.value),
381
+ placeholder: "Agent name (e.g. Tax Reviewer)",
382
+ className: "w-full rounded-lg border border-[var(--md3-outline-variant)] bg-surface-container px-3 py-2 text-sm outline-none focus:border-primary"
383
+ }
384
+ ),
385
+ capabilities.length > 0 && /* @__PURE__ */ jsxs2("div", { children: [
386
+ /* @__PURE__ */ jsx2("p", { className: "mb-1.5 text-[10px] font-semibold uppercase tracking-wide text-muted-foreground/70", children: "Tools" }),
387
+ /* @__PURE__ */ jsx2("div", { className: "space-y-0.5", children: capabilities.map((cap) => /* @__PURE__ */ jsxs2(
388
+ "label",
389
+ {
390
+ className: "flex cursor-pointer items-start gap-2 rounded-md px-2 py-1.5 hover:bg-accent/40",
391
+ children: [
392
+ /* @__PURE__ */ jsx2(
393
+ "input",
394
+ {
395
+ type: "checkbox",
396
+ checked: caps.includes(cap.id),
397
+ onChange: () => toggle(cap.id),
398
+ className: "mt-0.5"
399
+ }
400
+ ),
401
+ /* @__PURE__ */ jsxs2("span", { className: "min-w-0", children: [
402
+ /* @__PURE__ */ jsx2("span", { className: "block text-sm font-medium", children: cap.label }),
403
+ cap.description && /* @__PURE__ */ jsx2("span", { className: "block text-xs text-muted-foreground", children: cap.description })
404
+ ] })
405
+ ]
406
+ },
407
+ cap.id
408
+ )) })
409
+ ] }),
410
+ /* @__PURE__ */ jsx2(
411
+ "textarea",
412
+ {
413
+ value: instructions,
414
+ onChange: (event) => setInstructions(event.target.value),
415
+ rows: 2,
416
+ placeholder: "Instructions (optional) \u2014 how this agent should behave",
417
+ className: "w-full resize-none rounded-lg border border-[var(--md3-outline-variant)] bg-surface-container px-3 py-2 text-sm outline-none focus:border-primary"
418
+ }
419
+ ),
420
+ /* @__PURE__ */ jsxs2("div", { className: "flex justify-end gap-2", children: [
421
+ /* @__PURE__ */ jsx2(
422
+ "button",
423
+ {
424
+ type: "button",
425
+ onClick: onCancel,
426
+ className: "rounded-lg px-3 py-1.5 text-sm text-muted-foreground hover:bg-accent/40",
427
+ children: "Cancel"
428
+ }
429
+ ),
430
+ /* @__PURE__ */ jsx2(
431
+ "button",
432
+ {
433
+ type: "button",
434
+ disabled: !name.trim(),
435
+ onClick: () => onSave({
436
+ id: initial?.id,
437
+ name: name.trim(),
438
+ capabilities: caps,
439
+ instructions: instructions.trim()
440
+ }),
441
+ className: "rounded-lg bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground transition-opacity hover:opacity-90 disabled:opacity-40",
442
+ children: "Save agent"
443
+ }
444
+ )
445
+ ] })
446
+ ] });
217
447
  }
218
448
 
219
449
  // src/chat/agent-session-controls.tsx
220
- import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
450
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
221
451
  function HarnessDropdown({
222
452
  value,
223
453
  onChange,
@@ -234,8 +464,8 @@ function HarnessDropdown({
234
464
  );
235
465
  const options = HARNESS_OPTIONS.filter((h) => allowed.has(h.type));
236
466
  const selected = options.find((option) => option.type === value);
237
- return /* @__PURE__ */ jsxs2(DropdownMenu2.Root, { children: [
238
- /* @__PURE__ */ jsx2(DropdownMenu2.Trigger, { asChild: true, children: /* @__PURE__ */ jsxs2(
467
+ return /* @__PURE__ */ jsxs3(DropdownMenu2.Root, { children: [
468
+ /* @__PURE__ */ jsx3(DropdownMenu2.Trigger, { asChild: true, children: /* @__PURE__ */ jsxs3(
239
469
  "button",
240
470
  {
241
471
  type: "button",
@@ -250,13 +480,13 @@ function HarnessDropdown({
250
480
  ),
251
481
  "aria-label": "Agent harness",
252
482
  children: [
253
- locked ? /* @__PURE__ */ jsx2(Lock, { className: "h-3 w-3 text-muted-foreground" }) : /* @__PURE__ */ jsx2(HarnessLogo, { type: value, size: 16 }),
254
- /* @__PURE__ */ jsx2("span", { children: selected?.label ?? value }),
255
- !locked && /* @__PURE__ */ jsx2(ChevronDown2, { className: "h-3.5 w-3.5 text-muted-foreground" })
483
+ locked ? /* @__PURE__ */ jsx3(Lock, { className: "h-3 w-3 text-muted-foreground" }) : /* @__PURE__ */ jsx3(HarnessLogo, { type: value, size: 16 }),
484
+ /* @__PURE__ */ jsx3("span", { children: selected?.label ?? value }),
485
+ !locked && /* @__PURE__ */ jsx3(ChevronDown3, { className: "h-3.5 w-3.5 text-muted-foreground" })
256
486
  ]
257
487
  }
258
488
  ) }),
259
- /* @__PURE__ */ jsx2(DropdownMenu2.Portal, { children: /* @__PURE__ */ jsx2(
489
+ /* @__PURE__ */ jsx3(DropdownMenu2.Portal, { children: /* @__PURE__ */ jsx3(
260
490
  DropdownMenu2.Content,
261
491
  {
262
492
  side,
@@ -274,7 +504,7 @@ function HarnessDropdown({
274
504
  "data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
275
505
  "data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95"
276
506
  ),
277
- children: options.map((option) => /* @__PURE__ */ jsxs2(
507
+ children: options.map((option) => /* @__PURE__ */ jsxs3(
278
508
  DropdownMenu2.Item,
279
509
  {
280
510
  onSelect: (event) => {
@@ -287,10 +517,10 @@ function HarnessDropdown({
287
517
  option.type === value && "bg-primary/10 font-medium text-foreground ring-1 ring-inset ring-primary/25 hover:bg-primary/15 focus:bg-primary/15"
288
518
  ),
289
519
  children: [
290
- /* @__PURE__ */ jsx2(HarnessLogo, { type: option.type, size: 20, className: "mt-0.5" }),
291
- /* @__PURE__ */ jsxs2("span", { className: "flex min-w-0 flex-col gap-0.5", children: [
292
- /* @__PURE__ */ jsx2("span", { className: "text-sm font-medium", children: option.label }),
293
- option.description && /* @__PURE__ */ jsx2("span", { className: "text-xs text-muted-foreground", children: option.description })
520
+ /* @__PURE__ */ jsx3(HarnessLogo, { type: option.type, size: 20, className: "mt-0.5" }),
521
+ /* @__PURE__ */ jsxs3("span", { className: "flex min-w-0 flex-col gap-0.5", children: [
522
+ /* @__PURE__ */ jsx3("span", { className: "text-sm font-medium", children: option.label }),
523
+ option.description && /* @__PURE__ */ jsx3("span", { className: "text-xs text-muted-foreground", children: option.description })
294
524
  ] })
295
525
  ]
296
526
  },
@@ -302,6 +532,7 @@ function HarnessDropdown({
302
532
  }
303
533
  function AgentSessionControls({
304
534
  harness,
535
+ profile,
305
536
  model,
306
537
  reasoning,
307
538
  trailing,
@@ -310,7 +541,7 @@ function AgentSessionControls({
310
541
  layout = "inline",
311
542
  menuPlacement = "auto"
312
543
  }) {
313
- if (!harness && !model && !reasoning && !trailing) return null;
544
+ if (!harness && !profile && !model && !reasoning && !trailing) return null;
314
545
  const handleHarnessChange = (next) => {
315
546
  harness?.onChange(next);
316
547
  if (model) {
@@ -336,7 +567,7 @@ function AgentSessionControls({
336
567
  const menuAlign = layout === "gear" ? "end" : "start";
337
568
  const inlineForceDown = menuPlacement === "down" && layout !== "gear";
338
569
  const avoidCollisions = inlineForceDown ? false : void 0;
339
- const harnessNode = harness && /* @__PURE__ */ jsx2(
570
+ const harnessNode = harness && /* @__PURE__ */ jsx3(
340
571
  HarnessDropdown,
341
572
  {
342
573
  ...harness,
@@ -347,7 +578,21 @@ function AgentSessionControls({
347
578
  avoidCollisions
348
579
  }
349
580
  );
350
- const modelNode = model && /* @__PURE__ */ jsx2(
581
+ const profileNode = profile && /* @__PURE__ */ jsx3(
582
+ AgentProfilePicker,
583
+ {
584
+ value: profile.value,
585
+ onChange: profile.onChange,
586
+ profiles: profile.profiles,
587
+ capabilities: profile.capabilities,
588
+ onCreate: profile.onCreate,
589
+ onUpdate: profile.onUpdate,
590
+ onDelete: profile.onDelete,
591
+ disabled: profile.disabled,
592
+ side: inlineForceDown || layout === "gear" ? "bottom" : "top"
593
+ }
594
+ );
595
+ const modelNode = model && /* @__PURE__ */ jsx3(
351
596
  ModelPicker,
352
597
  {
353
598
  variant: "pill",
@@ -370,7 +615,7 @@ function AgentSessionControls({
370
615
  supportsReasoning: selectedModel.supportsReasoning,
371
616
  maxEffort: selectedModel.maxReasoningEffort
372
617
  } : null;
373
- const reasoningNode = reasoning && /* @__PURE__ */ jsx2(
618
+ const reasoningNode = reasoning && /* @__PURE__ */ jsx3(
374
619
  ReasoningLevelPicker,
375
620
  {
376
621
  value: reasoning.value,
@@ -383,10 +628,11 @@ function AgentSessionControls({
383
628
  }
384
629
  );
385
630
  if (layout === "gear") {
386
- return /* @__PURE__ */ jsx2(
631
+ return /* @__PURE__ */ jsx3(
387
632
  GearControls,
388
633
  {
389
634
  className,
635
+ profileNode,
390
636
  harnessNode,
391
637
  modelNode,
392
638
  reasoningNode,
@@ -394,36 +640,38 @@ function AgentSessionControls({
394
640
  }
395
641
  );
396
642
  }
397
- return /* @__PURE__ */ jsxs2(
643
+ return /* @__PURE__ */ jsxs3(
398
644
  "div",
399
645
  {
400
646
  className: cn("flex flex-wrap items-center gap-2", className),
401
647
  "data-testid": "agent-session-controls",
402
648
  children: [
649
+ profileNode,
403
650
  harnessNode,
404
651
  modelNode,
405
652
  reasoningNode,
406
- trailing && /* @__PURE__ */ jsx2("div", { className: "ml-auto flex items-center gap-2", children: trailing })
653
+ trailing && /* @__PURE__ */ jsx3("div", { className: "ml-auto flex items-center gap-2", children: trailing })
407
654
  ]
408
655
  }
409
656
  );
410
657
  }
411
658
  function GearControls({
412
659
  className,
660
+ profileNode,
413
661
  harnessNode,
414
662
  modelNode,
415
663
  reasoningNode,
416
664
  trailing
417
665
  }) {
418
- return /* @__PURE__ */ jsxs2(
666
+ return /* @__PURE__ */ jsxs3(
419
667
  "div",
420
668
  {
421
669
  className: cn("flex items-center gap-2", className),
422
670
  "data-testid": "agent-session-controls",
423
671
  children: [
424
- trailing && /* @__PURE__ */ jsx2("div", { className: "flex items-center gap-2", children: trailing }),
425
- /* @__PURE__ */ jsxs2(DropdownMenu2.Root, { children: [
426
- /* @__PURE__ */ jsx2(DropdownMenu2.Trigger, { asChild: true, children: /* @__PURE__ */ jsx2(
672
+ trailing && /* @__PURE__ */ jsx3("div", { className: "flex items-center gap-2", children: trailing }),
673
+ /* @__PURE__ */ jsxs3(DropdownMenu2.Root, { children: [
674
+ /* @__PURE__ */ jsx3(DropdownMenu2.Trigger, { asChild: true, children: /* @__PURE__ */ jsx3(
427
675
  "button",
428
676
  {
429
677
  type: "button",
@@ -434,10 +682,10 @@ function GearControls({
434
682
  "hover:border-[var(--md3-outline-variant)] hover:bg-surface-container-high focus:outline-none focus-visible:ring-1 focus-visible:ring-ring",
435
683
  "data-[state=open]:border-[var(--md3-outline-variant)] data-[state=open]:bg-surface-container-high"
436
684
  ),
437
- children: /* @__PURE__ */ jsx2(SlidersHorizontal, { className: "h-4 w-4 text-muted-foreground" })
685
+ children: /* @__PURE__ */ jsx3(SlidersHorizontal, { className: "h-4 w-4 text-muted-foreground" })
438
686
  }
439
687
  ) }),
440
- /* @__PURE__ */ jsx2(DropdownMenu2.Portal, { children: /* @__PURE__ */ jsxs2(
688
+ /* @__PURE__ */ jsx3(DropdownMenu2.Portal, { children: /* @__PURE__ */ jsxs3(
441
689
  DropdownMenu2.Content,
442
690
  {
443
691
  side: "top",
@@ -459,9 +707,10 @@ function GearControls({
459
707
  "data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95"
460
708
  ),
461
709
  children: [
462
- harnessNode && /* @__PURE__ */ jsx2(GearSection, { label: "Harness", children: harnessNode }),
463
- modelNode && /* @__PURE__ */ jsx2(GearSection, { label: "Model", children: modelNode }),
464
- reasoningNode && /* @__PURE__ */ jsx2(GearSection, { label: "Effort", children: reasoningNode })
710
+ profileNode && /* @__PURE__ */ jsx3(GearSection, { label: "Agent", children: profileNode }),
711
+ harnessNode && /* @__PURE__ */ jsx3(GearSection, { label: "Harness", children: harnessNode }),
712
+ modelNode && /* @__PURE__ */ jsx3(GearSection, { label: "Model", children: modelNode }),
713
+ reasoningNode && /* @__PURE__ */ jsx3(GearSection, { label: "Effort", children: reasoningNode })
465
714
  ]
466
715
  }
467
716
  ) })
@@ -474,17 +723,123 @@ function GearSection({
474
723
  label,
475
724
  children
476
725
  }) {
477
- return /* @__PURE__ */ jsxs2("div", { className: "flex flex-col gap-1", children: [
478
- /* @__PURE__ */ jsx2("span", { className: "px-0.5 text-[10px] font-medium uppercase tracking-wide text-muted-foreground/70", children: label }),
726
+ return /* @__PURE__ */ jsxs3("div", { className: "flex flex-col gap-1", children: [
727
+ /* @__PURE__ */ jsx3("span", { className: "px-0.5 text-[10px] font-medium uppercase tracking-wide text-muted-foreground/70", children: label }),
479
728
  children
480
729
  ] });
481
730
  }
482
731
 
732
+ // src/chat/agent-composer.tsx
733
+ import { ArrowUp, Loader2 } from "lucide-react";
734
+ import * as React2 from "react";
735
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
736
+ function AgentComposer({
737
+ value,
738
+ onChange,
739
+ onSubmit,
740
+ placeholder = "Send a message\u2026",
741
+ disabled,
742
+ busy,
743
+ harness,
744
+ profile,
745
+ model,
746
+ reasoning,
747
+ context = "chat",
748
+ trailing,
749
+ minRows = 2,
750
+ maxHeight = 200,
751
+ className,
752
+ sendLabel = "Send",
753
+ autoFocus
754
+ }) {
755
+ const textareaRef = React2.useRef(null);
756
+ const resize = React2.useCallback(
757
+ (el) => {
758
+ el.style.height = "auto";
759
+ el.style.height = `${Math.min(el.scrollHeight, maxHeight)}px`;
760
+ },
761
+ [maxHeight]
762
+ );
763
+ React2.useEffect(() => {
764
+ if (textareaRef.current) resize(textareaRef.current);
765
+ }, [value, resize]);
766
+ const canSend = !disabled && !busy && value.trim().length > 0;
767
+ const submit = () => {
768
+ if (!canSend) return;
769
+ onSubmit();
770
+ };
771
+ return /* @__PURE__ */ jsxs4(
772
+ "div",
773
+ {
774
+ "data-testid": "agent-composer",
775
+ className: cn(
776
+ "flex flex-col gap-2 rounded-2xl border border-[var(--md3-outline-variant)] bg-surface-container-high p-2.5 shadow-sm transition-colors",
777
+ "focus-within:border-primary/60",
778
+ disabled && "opacity-60",
779
+ className
780
+ ),
781
+ children: [
782
+ /* @__PURE__ */ jsx4(
783
+ "textarea",
784
+ {
785
+ ref: textareaRef,
786
+ rows: minRows,
787
+ value,
788
+ disabled,
789
+ autoFocus,
790
+ onChange: (event) => onChange(event.target.value),
791
+ onInput: (event) => resize(event.currentTarget),
792
+ onKeyDown: (event) => {
793
+ if (event.key === "Enter" && !event.shiftKey && !event.nativeEvent.isComposing && event.keyCode !== 229) {
794
+ event.preventDefault();
795
+ submit();
796
+ }
797
+ },
798
+ placeholder,
799
+ className: cn(
800
+ "w-full resize-none bg-transparent px-2 py-1 text-sm leading-relaxed text-foreground outline-none",
801
+ "placeholder:text-muted-foreground"
802
+ )
803
+ }
804
+ ),
805
+ /* @__PURE__ */ jsxs4("div", { className: "flex items-end gap-2", children: [
806
+ /* @__PURE__ */ jsx4(
807
+ AgentSessionControls,
808
+ {
809
+ className: "min-w-0 flex-1",
810
+ context,
811
+ harness,
812
+ profile,
813
+ model,
814
+ reasoning
815
+ }
816
+ ),
817
+ trailing && /* @__PURE__ */ jsx4("div", { className: "flex shrink-0 items-center gap-2", children: trailing }),
818
+ /* @__PURE__ */ jsx4(
819
+ "button",
820
+ {
821
+ type: "button",
822
+ onClick: submit,
823
+ disabled: !canSend,
824
+ "aria-label": sendLabel,
825
+ className: cn(
826
+ "ml-auto flex size-9 shrink-0 items-center justify-center rounded-full bg-primary text-primary-foreground",
827
+ "transition-opacity hover:opacity-90 disabled:opacity-40"
828
+ ),
829
+ children: busy ? /* @__PURE__ */ jsx4(Loader2, { className: "size-4 animate-spin" }) : /* @__PURE__ */ jsx4(ArrowUp, { className: "size-4" })
830
+ }
831
+ )
832
+ ] })
833
+ ]
834
+ }
835
+ );
836
+ }
837
+
483
838
  // src/chat/artifact-agent-dock.tsx
484
- import * as React from "react";
839
+ import * as React3 from "react";
485
840
  import { Sparkles as Sparkles2, Send, Square, X } from "lucide-react";
486
841
  import { ChatMessage } from "@tangle-network/ui/chat";
487
- import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
842
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
488
843
  function defaultScopeLabel(scope) {
489
844
  if (scope.label) return scope.label;
490
845
  switch (scope.kind) {
@@ -530,23 +885,23 @@ function ArtifactAgentDock({
530
885
  onError,
531
886
  className
532
887
  }) {
533
- const [threadId, setThreadId] = React.useState(null);
534
- const [messages, setMessages] = React.useState([]);
535
- const [composer, setComposer] = React.useState(defaultPrompt ?? "");
536
- const [streamContent, setStreamContent] = React.useState("");
537
- const [sending, setSending] = React.useState(false);
538
- const [loading, setLoading] = React.useState(false);
539
- const abortRef = React.useRef(null);
540
- const scrollRef = React.useRef(null);
541
- const scopeTokenRef = React.useRef(0);
542
- const reportError = React.useCallback(
888
+ const [threadId, setThreadId] = React3.useState(null);
889
+ const [messages, setMessages] = React3.useState([]);
890
+ const [composer, setComposer] = React3.useState(defaultPrompt ?? "");
891
+ const [streamContent, setStreamContent] = React3.useState("");
892
+ const [sending, setSending] = React3.useState(false);
893
+ const [loading, setLoading] = React3.useState(false);
894
+ const abortRef = React3.useRef(null);
895
+ const scrollRef = React3.useRef(null);
896
+ const scopeTokenRef = React3.useRef(0);
897
+ const reportError = React3.useCallback(
543
898
  (message) => {
544
899
  if (onError) onError(message);
545
900
  else if (typeof console !== "undefined") console.error("[ArtifactAgentDock]", message);
546
901
  },
547
902
  [onError]
548
903
  );
549
- React.useEffect(() => {
904
+ React3.useEffect(() => {
550
905
  if (!open) return;
551
906
  scopeTokenRef.current += 1;
552
907
  abortRef.current?.abort();
@@ -582,11 +937,11 @@ function ArtifactAgentDock({
582
937
  cancelled = true;
583
938
  };
584
939
  }, [open, scope.kind, scope.key, transport, reportError]);
585
- React.useEffect(() => {
940
+ React3.useEffect(() => {
586
941
  if (!scrollRef.current) return;
587
942
  scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
588
943
  }, [messages.length, streamContent]);
589
- const send = React.useCallback(async () => {
944
+ const send = React3.useCallback(async () => {
590
945
  const text = composer.trim();
591
946
  if (!text || !threadId || sending) return;
592
947
  const token = scopeTokenRef.current;
@@ -636,7 +991,7 @@ function ArtifactAgentDock({
636
991
  if (abortRef.current === controller) abortRef.current = null;
637
992
  }
638
993
  }, [composer, threadId, sending, transport, reportError]);
639
- const cancel = React.useCallback(() => {
994
+ const cancel = React3.useCallback(() => {
640
995
  if (threadId && transport.cancel) {
641
996
  try {
642
997
  void Promise.resolve(transport.cancel({ threadId })).catch(() => {
@@ -650,7 +1005,7 @@ function ArtifactAgentDock({
650
1005
  }, [threadId, transport]);
651
1006
  if (!open) return null;
652
1007
  const heading = defaultScopeLabel(scope);
653
- return /* @__PURE__ */ jsxs3(
1008
+ return /* @__PURE__ */ jsxs5(
654
1009
  "aside",
655
1010
  {
656
1011
  className: cn(
@@ -659,38 +1014,38 @@ function ArtifactAgentDock({
659
1014
  ),
660
1015
  "aria-label": `Agent chat about ${heading}`,
661
1016
  children: [
662
- /* @__PURE__ */ jsxs3("header", { className: "flex items-center justify-between border-b border-[var(--md3-outline-variant)] bg-surface-container-low px-4 py-3", children: [
663
- /* @__PURE__ */ jsxs3("div", { className: "flex min-w-0 items-center gap-2", children: [
664
- /* @__PURE__ */ jsx3(Sparkles2, { className: "h-4 w-4 shrink-0 text-muted-foreground" }),
665
- /* @__PURE__ */ jsxs3("div", { className: "min-w-0", children: [
666
- /* @__PURE__ */ jsx3("div", { className: "text-[10px] font-medium uppercase tracking-wider text-muted-foreground", children: "Discussing" }),
667
- /* @__PURE__ */ jsx3("div", { className: "truncate text-sm font-semibold text-foreground", children: heading })
1017
+ /* @__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: [
1018
+ /* @__PURE__ */ jsxs5("div", { className: "flex min-w-0 items-center gap-2", children: [
1019
+ /* @__PURE__ */ jsx5(Sparkles2, { className: "h-4 w-4 shrink-0 text-muted-foreground" }),
1020
+ /* @__PURE__ */ jsxs5("div", { className: "min-w-0", children: [
1021
+ /* @__PURE__ */ jsx5("div", { className: "text-[10px] font-medium uppercase tracking-wider text-muted-foreground", children: "Discussing" }),
1022
+ /* @__PURE__ */ jsx5("div", { className: "truncate text-sm font-semibold text-foreground", children: heading })
668
1023
  ] })
669
1024
  ] }),
670
- /* @__PURE__ */ jsx3(
1025
+ /* @__PURE__ */ jsx5(
671
1026
  "button",
672
1027
  {
673
1028
  type: "button",
674
1029
  onClick: () => onOpenChange(false),
675
1030
  "aria-label": "Close artifact chat",
676
1031
  className: "rounded-md p-1 text-muted-foreground transition-colors hover:bg-surface-container-high hover:text-foreground",
677
- children: /* @__PURE__ */ jsx3(X, { className: "h-4 w-4" })
1032
+ children: /* @__PURE__ */ jsx5(X, { className: "h-4 w-4" })
678
1033
  }
679
1034
  )
680
1035
  ] }),
681
- /* @__PURE__ */ jsx3("div", { ref: scrollRef, className: "flex-1 overflow-y-auto px-3 py-4", children: loading ? /* @__PURE__ */ jsx3("div", { className: "px-3 py-8 text-center text-xs text-muted-foreground", children: "Loading conversation\u2026" }) : messages.length === 0 && !streamContent ? /* @__PURE__ */ jsxs3("div", { className: "px-3 py-12 text-center", children: [
682
- /* @__PURE__ */ jsx3(Sparkles2, { className: "mx-auto mb-3 h-5 w-5 text-muted-foreground/40" }),
683
- /* @__PURE__ */ jsxs3("p", { className: "text-sm text-foreground", children: [
1036
+ /* @__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: [
1037
+ /* @__PURE__ */ jsx5(Sparkles2, { className: "mx-auto mb-3 h-5 w-5 text-muted-foreground/40" }),
1038
+ /* @__PURE__ */ jsxs5("p", { className: "text-sm text-foreground", children: [
684
1039
  "Ask the agent about this ",
685
1040
  scope.kind === "vault-file" ? "document" : scope.kind,
686
1041
  "."
687
1042
  ] }),
688
- /* @__PURE__ */ jsx3("p", { className: "mt-1 text-xs text-muted-foreground", children: "It can read the current contents and propose edits, rewrites, or follow-ups." })
689
- ] }) : /* @__PURE__ */ jsxs3("div", { className: "space-y-3", children: [
690
- messages.map((m) => /* @__PURE__ */ jsx3(ChatMessage, { role: m.role, content: m.content, timestamp: m.createdAt }, m.id)),
691
- streamContent && /* @__PURE__ */ jsx3(ChatMessage, { role: "assistant", content: streamContent, isStreaming: true })
1043
+ /* @__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." })
1044
+ ] }) : /* @__PURE__ */ jsxs5("div", { className: "space-y-3", children: [
1045
+ messages.map((m) => /* @__PURE__ */ jsx5(ChatMessage, { role: m.role, content: m.content, timestamp: m.createdAt }, m.id)),
1046
+ streamContent && /* @__PURE__ */ jsx5(ChatMessage, { role: "assistant", content: streamContent, isStreaming: true })
692
1047
  ] }) }),
693
- /* @__PURE__ */ jsx3(
1048
+ /* @__PURE__ */ jsx5(
694
1049
  "form",
695
1050
  {
696
1051
  className: "border-t border-[var(--md3-outline-variant)] bg-surface-container-low p-3",
@@ -698,8 +1053,8 @@ function ArtifactAgentDock({
698
1053
  e.preventDefault();
699
1054
  void send();
700
1055
  },
701
- children: /* @__PURE__ */ jsxs3("div", { className: "flex items-end gap-2", children: [
702
- /* @__PURE__ */ jsx3(
1056
+ children: /* @__PURE__ */ jsxs5("div", { className: "flex items-end gap-2", children: [
1057
+ /* @__PURE__ */ jsx5(
703
1058
  "textarea",
704
1059
  {
705
1060
  value: composer,
@@ -716,23 +1071,23 @@ function ArtifactAgentDock({
716
1071
  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"
717
1072
  }
718
1073
  ),
719
- sending ? /* @__PURE__ */ jsx3(
1074
+ sending ? /* @__PURE__ */ jsx5(
720
1075
  "button",
721
1076
  {
722
1077
  type: "button",
723
1078
  onClick: cancel,
724
1079
  "aria-label": "Cancel",
725
1080
  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",
726
- children: /* @__PURE__ */ jsx3(Square, { className: "h-4 w-4" })
1081
+ children: /* @__PURE__ */ jsx5(Square, { className: "h-4 w-4" })
727
1082
  }
728
- ) : /* @__PURE__ */ jsx3(
1083
+ ) : /* @__PURE__ */ jsx5(
729
1084
  "button",
730
1085
  {
731
1086
  type: "submit",
732
1087
  disabled: !composer.trim() || !threadId,
733
1088
  "aria-label": "Send",
734
1089
  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",
735
- children: /* @__PURE__ */ jsx3(Send, { className: "h-4 w-4" })
1090
+ children: /* @__PURE__ */ jsx5(Send, { className: "h-4 w-4" })
736
1091
  }
737
1092
  )
738
1093
  ] })
@@ -821,12 +1176,13 @@ function createFetchTransport(opts) {
821
1176
  export {
822
1177
  DEFAULT_REASONING_LEVEL_OPTIONS,
823
1178
  ReasoningLevelPicker,
824
- HARNESS_MODEL_POLICIES,
825
- modelProvider,
826
1179
  isModelCompatibleWithHarness,
827
1180
  snapModelToHarness,
1181
+ modelProvider,
828
1182
  snapHarnessToModel,
1183
+ AgentProfilePicker,
829
1184
  AgentSessionControls,
1185
+ AgentComposer,
830
1186
  ArtifactAgentDock,
831
1187
  createFetchTransport,
832
1188
  AgentTimeline,