@tangle-network/sandbox-ui 0.61.0 → 0.63.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.
@@ -2,11 +2,9 @@ import {
2
2
  ProviderIcon
3
3
  } from "./chunk-GTJ5PSK5.js";
4
4
  import {
5
- buildWorkflowGraph
6
- } from "./chunk-WEYFEMIN.js";
7
- import {
5
+ buildWorkflowGraph,
8
6
  providerLabel
9
- } from "./chunk-7JLB7UZ3.js";
7
+ } from "./chunk-34F66QTV.js";
10
8
  import "./chunk-U67V476Y.js";
11
9
 
12
10
  // src/workflows/WorkflowGraph.tsx
package/dist/chat.d.ts CHANGED
@@ -124,8 +124,21 @@ interface AgentSessionHarnessControl {
124
124
  * switch harness.
125
125
  */
126
126
  locked?: boolean;
127
- /** Tooltip shown on the locked trigger. */
127
+ /** Tooltip shown on the locked trigger when no `onNewChat` is provided. */
128
128
  lockReason?: string;
129
+ /**
130
+ * Fork action for a locked harness. When provided, the locked trigger turns
131
+ * into an informative chip: hover or tap opens a small popover explaining the
132
+ * lock and offering a button that calls this to start a fresh session. When
133
+ * omitted, the locked trigger stays a plain inert button with `lockReason`.
134
+ */
135
+ onNewChat?: () => void;
136
+ /** Popover heading. Defaults to `Agent fixed to {harness} for this conversation.` */
137
+ lockTitle?: React.ReactNode;
138
+ /** Popover sub-line. Defaults to "Conversations stay on one agent." */
139
+ lockBody?: React.ReactNode;
140
+ /** Fork button label. Defaults to "New chat to switch agent". */
141
+ newChatLabel?: string;
129
142
  }
130
143
  interface AgentSessionModelControl {
131
144
  /** Canonical model id (provider-prefixed, e.g. "anthropic/claude-opus-4-8"). */
@@ -180,6 +193,16 @@ interface AgentSessionControlsProps {
180
193
  model?: AgentSessionModelControl;
181
194
  /** Thinking-effort level applied to subsequent turns. */
182
195
  reasoning?: AgentSessionReasoningControl;
196
+ /**
197
+ * Restrict the model catalog to what the selected harness can actually run,
198
+ * the same way `harness.locked` does — but WITHOUT locking the harness
199
+ * dropdown. Off by default: the strip shows every model and picking an
200
+ * incompatible one snaps the harness to the model's native backend. Turn it
201
+ * ON for a harness-first surface (e.g. a workspace-wide, sandbox-bound
202
+ * harness) where that silent backend swap is undesirable — an incompatible
203
+ * model is then simply not offered.
204
+ */
205
+ filterModelsToHarness?: boolean;
183
206
  /** Right-aligned extra content (token meter, cost, status). */
184
207
  trailing?: React.ReactNode;
185
208
  className?: string;
@@ -218,12 +241,17 @@ interface AgentSessionControlsProps {
218
241
  * coherent automatically (see harness-model-compat): picking a harness
219
242
  * snaps an incompatible model to that harness's best catalog option;
220
243
  * picking a model the current harness can't run switches to the model's
221
- * native harness — unless the harness is `locked`, in which case the
222
- * catalog itself is filtered to compatible models.
244
+ * native harness — unless the catalog is restricted to the harness (when
245
+ * `locked`, or the consumer sets `filterModelsToHarness`), in which case the
246
+ * incompatible model is never offered and no swap happens.
247
+ *
248
+ * When a reasoning control is also present, the effort is re-clamped into the
249
+ * set the new (harness, model) pair supports on every harness/model change, so
250
+ * the effort picker's label never disagrees with the value actually in effect.
223
251
  *
224
252
  * Designed to slot into `SandboxWorkbench`'s `session.composerControls`.
225
253
  */
226
- declare function AgentSessionControls({ harness, profile, model, reasoning, trailing, className, context, layout, menuPlacement, }: AgentSessionControlsProps): react_jsx_runtime.JSX.Element | null;
254
+ declare function AgentSessionControls({ harness, profile, model, reasoning, filterModelsToHarness, trailing, className, context, layout, menuPlacement, }: AgentSessionControlsProps): react_jsx_runtime.JSX.Element | null;
227
255
 
228
256
  /** A staged attachment shown as a chip above the input. */
229
257
  interface ComposerFile {
package/dist/chat.js CHANGED
@@ -17,7 +17,7 @@ import {
17
17
  modelProvider,
18
18
  snapHarnessToModel,
19
19
  snapModelToHarness
20
- } from "./chunk-V5JZOO3W.js";
20
+ } from "./chunk-2E3FJM56.js";
21
21
  import "./chunk-HU4B2M4K.js";
22
22
  import "./chunk-SF6LPMMN.js";
23
23
  import "./chunk-EI44GEQ5.js";
@@ -28,6 +28,12 @@ import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
28
28
  import { Brain, ChevronDown, Sparkles } from "lucide-react";
29
29
  import { jsx, jsxs } from "react/jsx-runtime";
30
30
  var REASONING_LADDER = reasoningLadder;
31
+ function clampReasoningLevel(value, available) {
32
+ if (value === "auto" || !available || available.includes(value)) {
33
+ return value;
34
+ }
35
+ return available.length ? available[available.length - 1] : "auto";
36
+ }
31
37
  function ReasoningGlyph({ level }) {
32
38
  if (level === "auto") {
33
39
  return /* @__PURE__ */ jsx(Sparkles, { className: "h-3.5 w-3.5 text-muted-foreground" });
@@ -144,9 +150,30 @@ function ReasoningLevelPicker({
144
150
  }
145
151
 
146
152
  // src/chat/agent-session-controls.tsx
147
- import { reasoningEffortsFor } from "@tangle-network/agent-interface";
153
+ import {
154
+ harnessHonorsEffort,
155
+ harnessHonorsModel,
156
+ reasoningEffortsFor
157
+ } from "@tangle-network/agent-interface";
148
158
  import * as DropdownMenu2 from "@radix-ui/react-dropdown-menu";
149
- import { ChevronDown as ChevronDown3, Lock, SlidersHorizontal } from "lucide-react";
159
+ import { ChevronDown as ChevronDown3, Lock, Plus as Plus2, SlidersHorizontal } from "lucide-react";
160
+ import * as React3 from "react";
161
+
162
+ // src/lib/use-click-outside.ts
163
+ import * as React from "react";
164
+ function useClickOutside(onOutside) {
165
+ const ref = React.useRef(null);
166
+ React.useEffect(() => {
167
+ function handler(event) {
168
+ if (ref.current && !ref.current.contains(event.target)) {
169
+ onOutside();
170
+ }
171
+ }
172
+ document.addEventListener("mousedown", handler);
173
+ return () => document.removeEventListener("mousedown", handler);
174
+ }, [onOutside]);
175
+ return ref;
176
+ }
150
177
 
151
178
  // src/chat/harness-model-compat.ts
152
179
  import {
@@ -163,21 +190,8 @@ function snapModelToHarness(harness, modelId, models) {
163
190
 
164
191
  // src/chat/agent-profile-picker.tsx
165
192
  import { Bot, Check, ChevronDown as ChevronDown2, Pencil, Plus, Trash2 } from "lucide-react";
166
- import * as React from "react";
193
+ import * as React2 from "react";
167
194
  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
195
  function profileSubtitle(profile) {
182
196
  if (profile.description) return profile.description;
183
197
  const count = profile.capabilities?.length ?? 0;
@@ -195,8 +209,8 @@ function AgentProfilePicker({
195
209
  className,
196
210
  side = "top"
197
211
  }) {
198
- const [open, setOpen] = React.useState(false);
199
- const [composing, setComposing] = React.useState(
212
+ const [open, setOpen] = React2.useState(false);
213
+ const [composing, setComposing] = React2.useState(
200
214
  null
201
215
  );
202
216
  const ref = useClickOutside(() => {
@@ -363,11 +377,11 @@ function ProfileForm({
363
377
  onCancel,
364
378
  onSave
365
379
  }) {
366
- const [name, setName] = React.useState(initial?.name ?? "");
367
- const [caps, setCaps] = React.useState(
380
+ const [name, setName] = React2.useState(initial?.name ?? "");
381
+ const [caps, setCaps] = React2.useState(
368
382
  initial?.capabilities ? [...initial.capabilities] : capabilities.map((c) => c.id)
369
383
  );
370
- const [instructions, setInstructions] = React.useState(initial?.instructions ?? "");
384
+ const [instructions, setInstructions] = React2.useState(initial?.instructions ?? "");
371
385
  const toggle = (id) => setCaps(
372
386
  (prev) => prev.includes(id) ? prev.filter((c) => c !== id) : [...prev, id]
373
387
  );
@@ -448,6 +462,110 @@ function ProfileForm({
448
462
 
449
463
  // src/chat/agent-session-controls.tsx
450
464
  import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
465
+ function selectorIgnoreNote(harness) {
466
+ const ignoresModel = !harnessHonorsModel(harness);
467
+ const ignoresEffort = !harnessHonorsEffort(harness);
468
+ if (ignoresModel && ignoresEffort) return "Ignores model & effort";
469
+ if (ignoresModel) return "Ignores model selection";
470
+ if (ignoresEffort) return "Ignores reasoning effort";
471
+ return null;
472
+ }
473
+ function InformativeLock({
474
+ label,
475
+ lockTitle,
476
+ lockBody,
477
+ newChatLabel,
478
+ onNewChat
479
+ }) {
480
+ const [open, setOpen] = React3.useState(false);
481
+ const closeTimer = React3.useRef(null);
482
+ const ref = useClickOutside(() => setOpen(false));
483
+ const cancelClose = React3.useCallback(() => {
484
+ if (closeTimer.current) {
485
+ clearTimeout(closeTimer.current);
486
+ closeTimer.current = null;
487
+ }
488
+ }, []);
489
+ const scheduleClose = React3.useCallback(() => {
490
+ cancelClose();
491
+ closeTimer.current = setTimeout(() => setOpen(false), 150);
492
+ }, [cancelClose]);
493
+ React3.useEffect(() => cancelClose, [cancelClose]);
494
+ return /* @__PURE__ */ jsxs3(
495
+ "div",
496
+ {
497
+ ref,
498
+ className: "relative inline-flex",
499
+ onMouseEnter: () => {
500
+ cancelClose();
501
+ setOpen(true);
502
+ },
503
+ onMouseLeave: scheduleClose,
504
+ onKeyDown: (event) => {
505
+ if (event.key === "Escape") setOpen(false);
506
+ },
507
+ children: [
508
+ /* @__PURE__ */ jsxs3(
509
+ "button",
510
+ {
511
+ type: "button",
512
+ onClick: () => setOpen(true),
513
+ "data-state": open ? "open" : "closed",
514
+ "aria-label": "Agent harness (locked)",
515
+ "aria-haspopup": "dialog",
516
+ "aria-expanded": open,
517
+ className: cn(
518
+ "inline-flex h-8 items-center gap-1.5 rounded-lg border border-[var(--md3-outline-variant)] bg-surface-container px-2.5",
519
+ "text-xs font-medium text-foreground shadow-sm transition-colors",
520
+ "hover:border-[var(--md3-outline-variant)] hover:bg-surface-container-high focus:outline-none focus-visible:ring-1 focus-visible:ring-ring",
521
+ "data-[state=open]:border-[var(--md3-outline-variant)] data-[state=open]:bg-surface-container-high"
522
+ ),
523
+ children: [
524
+ /* @__PURE__ */ jsx3(Lock, { className: "h-3 w-3 text-muted-foreground" }),
525
+ /* @__PURE__ */ jsx3("span", { children: label })
526
+ ]
527
+ }
528
+ ),
529
+ open && /* @__PURE__ */ jsxs3(
530
+ "div",
531
+ {
532
+ role: "dialog",
533
+ className: cn(
534
+ "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",
535
+ "shadow-[0_8px_30px_rgba(0,0,0,0.45)] ring-1 ring-[#ffffff14]"
536
+ ),
537
+ children: [
538
+ /* @__PURE__ */ jsxs3("p", { className: "flex items-start gap-2 text-sm font-medium text-foreground", children: [
539
+ /* @__PURE__ */ jsx3(Lock, { className: "mt-0.5 h-3.5 w-3.5 shrink-0 text-muted-foreground" }),
540
+ /* @__PURE__ */ jsx3("span", { children: lockTitle ?? `Agent fixed to ${label} for this conversation.` })
541
+ ] }),
542
+ /* @__PURE__ */ jsx3("p", { className: "mt-1.5 text-xs text-muted-foreground", children: lockBody ?? "Conversations stay on one agent." }),
543
+ /* @__PURE__ */ jsxs3(
544
+ "button",
545
+ {
546
+ type: "button",
547
+ onClick: () => {
548
+ setOpen(false);
549
+ onNewChat();
550
+ },
551
+ className: cn(
552
+ "mt-3 flex w-full items-center justify-center gap-1.5 rounded-md px-2.5 py-2 text-sm font-medium text-foreground",
553
+ "bg-primary/10 ring-1 ring-inset ring-primary/25 transition-colors",
554
+ "hover:bg-primary/15 focus:outline-none focus-visible:ring-1 focus-visible:ring-ring"
555
+ ),
556
+ children: [
557
+ /* @__PURE__ */ jsx3(Plus2, { className: "h-4 w-4" }),
558
+ newChatLabel ?? "New chat to switch agent"
559
+ ]
560
+ }
561
+ )
562
+ ]
563
+ }
564
+ )
565
+ ]
566
+ }
567
+ );
568
+ }
451
569
  function HarnessDropdown({
452
570
  value,
453
571
  onChange,
@@ -455,6 +573,10 @@ function HarnessDropdown({
455
573
  disabled,
456
574
  locked,
457
575
  lockReason,
576
+ onNewChat,
577
+ lockTitle,
578
+ lockBody,
579
+ newChatLabel,
458
580
  side = "bottom",
459
581
  align = "start",
460
582
  avoidCollisions
@@ -464,6 +586,18 @@ function HarnessDropdown({
464
586
  );
465
587
  const options = HARNESS_OPTIONS.filter((h) => allowed.has(h.type));
466
588
  const selected = options.find((option) => option.type === value);
589
+ if (locked && onNewChat) {
590
+ return /* @__PURE__ */ jsx3(
591
+ InformativeLock,
592
+ {
593
+ label: selected?.label ?? value,
594
+ lockTitle,
595
+ lockBody,
596
+ newChatLabel,
597
+ onNewChat
598
+ }
599
+ );
600
+ }
467
601
  return /* @__PURE__ */ jsxs3(DropdownMenu2.Root, { children: [
468
602
  /* @__PURE__ */ jsx3(DropdownMenu2.Trigger, { asChild: true, children: /* @__PURE__ */ jsxs3(
469
603
  "button",
@@ -520,7 +654,8 @@ function HarnessDropdown({
520
654
  /* @__PURE__ */ jsx3(HarnessLogo, { type: option.type, size: 20, className: "mt-0.5" }),
521
655
  /* @__PURE__ */ jsxs3("span", { className: "flex min-w-0 flex-col gap-0.5", children: [
522
656
  /* @__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 })
657
+ option.description && /* @__PURE__ */ jsx3("span", { className: "text-xs text-muted-foreground", children: option.description }),
658
+ selectorIgnoreNote(option.type) && /* @__PURE__ */ jsx3("span", { className: "mt-0.5 inline-flex w-fit items-center rounded border border-[var(--md3-outline-variant)] px-1.5 py-0.5 text-[10px] font-medium uppercase tracking-wide text-muted-foreground", children: selectorIgnoreNote(option.type) })
524
659
  ] })
525
660
  ]
526
661
  },
@@ -530,11 +665,20 @@ function HarnessDropdown({
530
665
  ) })
531
666
  ] });
532
667
  }
668
+ function modelReasoningCapability(models, canonicalId) {
669
+ if (!models || !canonicalId) return null;
670
+ const entry = models.find((m) => canonicalModelId(m) === canonicalId);
671
+ return entry ? {
672
+ supportsReasoning: entry.supportsReasoning,
673
+ maxEffort: entry.maxReasoningEffort
674
+ } : null;
675
+ }
533
676
  function AgentSessionControls({
534
677
  harness,
535
678
  profile,
536
679
  model,
537
680
  reasoning,
681
+ filterModelsToHarness,
538
682
  trailing,
539
683
  className,
540
684
  context = "chat",
@@ -542,21 +686,45 @@ function AgentSessionControls({
542
686
  menuPlacement = "auto"
543
687
  }) {
544
688
  if (!harness && !profile && !model && !reasoning && !trailing) return null;
689
+ const restrictModelsToHarness = Boolean(
690
+ harness && (harness.locked || filterModelsToHarness)
691
+ );
692
+ const selectorHonorsModel = !harness || harnessHonorsModel(harness.value);
693
+ const selectorHonorsEffort = !harness || harnessHonorsEffort(harness.value);
694
+ const clampEffort = (harnessValue, modelId) => {
695
+ if (!reasoning) return;
696
+ const available = reasoningEffortsFor(
697
+ harnessValue,
698
+ modelReasoningCapability(model?.models, modelId)
699
+ );
700
+ const clamped = clampReasoningLevel(reasoning.value, available);
701
+ if (clamped !== reasoning.value) reasoning.onChange(clamped);
702
+ };
545
703
  const handleHarnessChange = (next) => {
546
704
  harness?.onChange(next);
705
+ let nextModelId = model?.value;
547
706
  if (model) {
548
707
  const snapped = snapModelToHarness(next, model.value, model.models);
549
- if (snapped !== model.value) model.onChange(snapped);
708
+ if (snapped !== model.value) {
709
+ model.onChange(snapped);
710
+ nextModelId = snapped;
711
+ }
550
712
  }
713
+ clampEffort(next, nextModelId);
551
714
  };
552
715
  const handleModelChange = (nextModelId) => {
553
716
  model?.onChange(nextModelId);
554
- if (harness && !harness.locked) {
717
+ let nextHarness = harness?.value;
718
+ if (harness && !restrictModelsToHarness) {
555
719
  const snapped = snapHarnessToModel(harness.value, nextModelId);
556
- if (snapped !== harness.value) harness.onChange(snapped);
720
+ if (snapped !== harness.value) {
721
+ harness.onChange(snapped);
722
+ nextHarness = snapped;
723
+ }
557
724
  }
725
+ if (nextHarness) clampEffort(nextHarness, nextModelId);
558
726
  };
559
- const visibleModels = model && harness?.locked ? model.models.filter(
727
+ const visibleModels = model && restrictModelsToHarness && harness ? model.models.filter(
560
728
  (entry) => isModelCompatibleWithHarness(
561
729
  harness.value,
562
730
  canonicalModelId(entry)
@@ -603,25 +771,19 @@ function AgentSessionControls({
603
771
  loading: model.loading,
604
772
  popular: model.popular,
605
773
  recents: model.recents,
606
- disabled: model.disabled || (visibleModels ?? []).length === 0,
774
+ disabled: model.disabled || (visibleModels ?? []).length === 0 || !selectorHonorsModel,
607
775
  side: layout === "gear" ? void 0 : "bottom",
608
776
  avoidCollisions
609
777
  }
610
778
  );
611
- const selectedModel = model?.models.find(
612
- (entry) => canonicalModelId(entry) === model.value
613
- );
614
- const modelReasoning = selectedModel ? {
615
- supportsReasoning: selectedModel.supportsReasoning,
616
- maxEffort: selectedModel.maxReasoningEffort
617
- } : null;
779
+ const modelReasoning = modelReasoningCapability(model?.models, model?.value);
618
780
  const reasoningNode = reasoning && /* @__PURE__ */ jsx3(
619
781
  ReasoningLevelPicker,
620
782
  {
621
783
  value: reasoning.value,
622
784
  onChange: reasoning.onChange,
623
785
  options: reasoning.options,
624
- disabled: reasoning.disabled,
786
+ disabled: reasoning.disabled || !selectorHonorsEffort,
625
787
  available: harness ? reasoningEffortsFor(harness.value, modelReasoning) : reasoning.available,
626
788
  side: layout === "gear" ? void 0 : "bottom",
627
789
  avoidCollisions
@@ -739,7 +901,7 @@ import {
739
901
  Upload,
740
902
  X
741
903
  } from "lucide-react";
742
- import * as React2 from "react";
904
+ import * as React4 from "react";
743
905
  import { Fragment, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
744
906
  function AgentComposer({
745
907
  value,
@@ -770,22 +932,22 @@ function AgentComposer({
770
932
  sendLabel = "Send",
771
933
  autoFocus
772
934
  }) {
773
- const textareaRef = React2.useRef(null);
774
- const fileInputRef = React2.useRef(null);
775
- const folderInputRef = React2.useRef(null);
776
- const [dragOver, setDragOver] = React2.useState(false);
777
- const dragDepth = React2.useRef(0);
778
- const resize = React2.useCallback(
935
+ const textareaRef = React4.useRef(null);
936
+ const fileInputRef = React4.useRef(null);
937
+ const folderInputRef = React4.useRef(null);
938
+ const [dragOver, setDragOver] = React4.useState(false);
939
+ const dragDepth = React4.useRef(0);
940
+ const resize = React4.useCallback(
779
941
  (el) => {
780
942
  el.style.height = "auto";
781
943
  el.style.height = `${Math.min(el.scrollHeight, maxHeight)}px`;
782
944
  },
783
945
  [maxHeight]
784
946
  );
785
- React2.useEffect(() => {
947
+ React4.useEffect(() => {
786
948
  if (textareaRef.current) resize(textareaRef.current);
787
949
  }, [value, resize]);
788
- React2.useEffect(() => {
950
+ React4.useEffect(() => {
789
951
  if (!focusShortcut || disabled) return;
790
952
  function onKeyDown(event) {
791
953
  if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "l") {
@@ -1024,7 +1186,7 @@ function AgentComposer({
1024
1186
  }
1025
1187
 
1026
1188
  // src/chat/artifact-agent-dock.tsx
1027
- import * as React3 from "react";
1189
+ import * as React5 from "react";
1028
1190
  import { Sparkles as Sparkles2, Send, Square as Square2, X as X2 } from "lucide-react";
1029
1191
  import { ChatMessage } from "@tangle-network/ui/chat";
1030
1192
  import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
@@ -1073,23 +1235,23 @@ function ArtifactAgentDock({
1073
1235
  onError,
1074
1236
  className
1075
1237
  }) {
1076
- const [threadId, setThreadId] = React3.useState(null);
1077
- const [messages, setMessages] = React3.useState([]);
1078
- const [composer, setComposer] = React3.useState(defaultPrompt ?? "");
1079
- const [streamContent, setStreamContent] = React3.useState("");
1080
- const [sending, setSending] = React3.useState(false);
1081
- const [loading, setLoading] = React3.useState(false);
1082
- const abortRef = React3.useRef(null);
1083
- const scrollRef = React3.useRef(null);
1084
- const scopeTokenRef = React3.useRef(0);
1085
- const reportError = React3.useCallback(
1238
+ const [threadId, setThreadId] = React5.useState(null);
1239
+ const [messages, setMessages] = React5.useState([]);
1240
+ const [composer, setComposer] = React5.useState(defaultPrompt ?? "");
1241
+ const [streamContent, setStreamContent] = React5.useState("");
1242
+ const [sending, setSending] = React5.useState(false);
1243
+ const [loading, setLoading] = React5.useState(false);
1244
+ const abortRef = React5.useRef(null);
1245
+ const scrollRef = React5.useRef(null);
1246
+ const scopeTokenRef = React5.useRef(0);
1247
+ const reportError = React5.useCallback(
1086
1248
  (message) => {
1087
1249
  if (onError) onError(message);
1088
1250
  else if (typeof console !== "undefined") console.error("[ArtifactAgentDock]", message);
1089
1251
  },
1090
1252
  [onError]
1091
1253
  );
1092
- React3.useEffect(() => {
1254
+ React5.useEffect(() => {
1093
1255
  if (!open) return;
1094
1256
  scopeTokenRef.current += 1;
1095
1257
  abortRef.current?.abort();
@@ -1125,11 +1287,11 @@ function ArtifactAgentDock({
1125
1287
  cancelled = true;
1126
1288
  };
1127
1289
  }, [open, scope.kind, scope.key, transport, reportError]);
1128
- React3.useEffect(() => {
1290
+ React5.useEffect(() => {
1129
1291
  if (!scrollRef.current) return;
1130
1292
  scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
1131
1293
  }, [messages.length, streamContent]);
1132
- const send = React3.useCallback(async () => {
1294
+ const send = React5.useCallback(async () => {
1133
1295
  const text = composer.trim();
1134
1296
  if (!text || !threadId || sending) return;
1135
1297
  const token = scopeTokenRef.current;
@@ -1179,7 +1341,7 @@ function ArtifactAgentDock({
1179
1341
  if (abortRef.current === controller) abortRef.current = null;
1180
1342
  }
1181
1343
  }, [composer, threadId, sending, transport, reportError]);
1182
- const cancel = React3.useCallback(() => {
1344
+ const cancel = React5.useCallback(() => {
1183
1345
  if (threadId && transport.cancel) {
1184
1346
  try {
1185
1347
  void Promise.resolve(transport.cancel({ threadId })).catch(() => {
@@ -1,6 +1,17 @@
1
- import {
2
- providerLabel
3
- } from "./chunk-7JLB7UZ3.js";
1
+ // src/workflows/provider-label.ts
2
+ var PROVIDER_LABELS = {
3
+ github: "GitHub",
4
+ gitlab: "GitLab",
5
+ slack: "Slack",
6
+ stripe: "Stripe",
7
+ notion: "Notion",
8
+ linear: "Linear",
9
+ discord: "Discord"
10
+ };
11
+ function providerLabel(provider) {
12
+ const key = provider.toLowerCase();
13
+ return PROVIDER_LABELS[key] ?? provider.charAt(0).toUpperCase() + provider.slice(1);
14
+ }
4
15
 
5
16
  // src/workflows/model.ts
6
17
  import { parse as parseYaml } from "yaml";
@@ -293,5 +304,6 @@ function buildWorkflowGraph(yaml, options) {
293
304
  }
294
305
 
295
306
  export {
307
+ providerLabel,
296
308
  buildWorkflowGraph
297
309
  };
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-3J6FG3FJ.js";
4
4
  import {
5
5
  PortsList
6
- } from "./chunk-OQZQFSWM.js";
6
+ } from "./chunk-DZA4UBXU.js";
7
7
  import {
8
8
  TerminalView
9
9
  } from "./chunk-VIREDUBF.js";
@@ -564,13 +564,17 @@ function RailExpandable({
564
564
  loadSubItems,
565
565
  activeSubIds,
566
566
  emptyLabel = "Nothing here yet",
567
+ defaultOpen,
567
568
  onNavigate,
568
569
  LinkComponent,
569
570
  className
570
571
  }) {
571
572
  const Link = LinkComponent ?? DefaultLink;
572
- const [open, setOpen] = React3.useState(false);
573
+ const [open, setOpen] = React3.useState(defaultOpen ?? false);
573
574
  const { items, loading, ensureLoaded } = useExpandableItems(subItems, loadSubItems);
575
+ React3.useEffect(() => {
576
+ if (defaultOpen) ensureLoaded();
577
+ }, [defaultOpen, ensureLoaded]);
574
578
  const activeSet = React3.useMemo(() => new Set(activeSubIds ?? []), [activeSubIds]);
575
579
  const triggerRef = React3.useRef(null);
576
580
  const closeTimer = React3.useRef(null);
@@ -589,8 +593,8 @@ function RailExpandable({
589
593
  onClick: onNavigate,
590
594
  ...item.prefetch !== void 0 ? { prefetch: item.prefetch } : {},
591
595
  className: cn(
592
- "flex h-8 min-w-0 items-center gap-2 rounded-md px-2.5 text-[12px] transition-colors",
593
- active ? "bg-[var(--accent-surface-strong)] font-medium text-[var(--accent-text)]" : "text-muted-foreground hover:bg-[var(--accent-surface-soft)] hover:text-foreground"
596
+ "flex h-7 min-w-0 items-center gap-2 rounded-md px-2.5 text-[12px] transition-colors",
597
+ active ? "bg-[var(--accent-surface-strong)] font-medium text-[var(--accent-text)]" : item.emphasis ? "font-semibold text-[var(--accent-text)] hover:bg-[var(--accent-surface-soft)]" : "text-muted-foreground hover:bg-[var(--accent-surface-soft)] hover:text-foreground"
594
598
  ),
595
599
  children: [
596
600
  FIcon ? /* @__PURE__ */ jsx3(FIcon, { className: "h-4 w-4 shrink-0" }) : null,
@@ -719,18 +723,15 @@ function RailExpandable({
719
723
  /* @__PURE__ */ jsx3(
720
724
  Icon,
721
725
  {
722
- className: cn(
723
- "h-[17px] w-[17px] shrink-0 transition-opacity duration-150",
724
- open ? "opacity-0" : "opacity-100 group-hover/exp:opacity-0"
725
- )
726
+ className: "h-[17px] w-[17px] shrink-0 opacity-100 transition-opacity duration-150 group-hover/exp:opacity-0"
726
727
  }
727
728
  ),
728
729
  /* @__PURE__ */ jsx3(
729
730
  ChevronRightIcon,
730
731
  {
731
732
  className: cn(
732
- "absolute h-4 w-4 transition-all duration-150",
733
- open ? "rotate-90 opacity-100" : "opacity-0 group-hover/exp:opacity-100"
733
+ "absolute h-4 w-4 opacity-0 transition-all duration-150 group-hover/exp:opacity-100",
734
+ open && "rotate-90"
734
735
  )
735
736
  }
736
737
  )
@@ -2368,6 +2369,7 @@ function SidebarLayoutInner({
2368
2369
  loadSubItems: item.loadSubItems,
2369
2370
  activeSubIds: item.subActiveIds,
2370
2371
  emptyLabel: item.emptyLabel,
2372
+ defaultOpen: item.defaultOpen,
2371
2373
  onNavigate: handleNavClick,
2372
2374
  LinkComponent
2373
2375
  },
@@ -185,6 +185,9 @@ interface RailExpandableSubItem {
185
185
  prefetch?: "none" | "intent" | "render" | "viewport";
186
186
  /** Show a live working indicator on the row while the item is busy. */
187
187
  isLoading?: boolean;
188
+ /** Render the row emphasized (bold + accent) so it stands out — e.g. a
189
+ * trailing "view all" action below a capped list. */
190
+ emphasis?: boolean;
188
191
  }
189
192
  interface RailExpandableProps {
190
193
  icon: React.ComponentType<{
@@ -201,6 +204,8 @@ interface RailExpandableProps {
201
204
  subItems?: RailExpandableSubItem[];
202
205
  /** Lazy sub-items — called once on first open; the result is cached. */
203
206
  loadSubItems?: () => Promise<RailExpandableSubItem[]>;
207
+ /** Start expanded on mount (labeled rail). Defaults to collapsed. */
208
+ defaultOpen?: boolean;
204
209
  /** Ids of sub-items whose route is active (drives sub-item highlight). */
205
210
  activeSubIds?: string[];
206
211
  /** Shown when the resolved sub-item list is empty. */
@@ -218,7 +223,7 @@ interface RailExpandableProps {
218
223
  * Sub-items can be fixed (`subItems`) or lazy (`loadSubItems`, with a loading
219
224
  * skeleton). Used for e.g. a "History" entry fronting recent sessions.
220
225
  */
221
- declare function RailExpandable({ icon: Icon, label, href, isActive, showLabel, subItems, loadSubItems, activeSubIds, emptyLabel, onNavigate, LinkComponent, className, }: RailExpandableProps): react_jsx_runtime.JSX.Element;
226
+ declare function RailExpandable({ icon: Icon, label, href, isActive, showLabel, subItems, loadSubItems, activeSubIds, emptyLabel, defaultOpen, onNavigate, LinkComponent, className, }: RailExpandableProps): react_jsx_runtime.JSX.Element;
222
227
  interface RailModeButtonProps {
223
228
  mode: string;
224
229
  icon: React.ComponentType<{
@@ -913,6 +918,8 @@ interface SidebarLayoutNavItem {
913
918
  subActiveIds?: string[];
914
919
  /** Empty-state text for an `expandable` item that resolves to no sub-items. */
915
920
  emptyLabel?: string;
921
+ /** Start an `expandable` item expanded on mount (labeled rail). */
922
+ defaultOpen?: boolean;
916
923
  badge?: number;
917
924
  /**
918
925
  * React Router prefetch behavior for this link, forwarded to the underlying
package/dist/dashboard.js CHANGED
@@ -54,7 +54,7 @@ import {
54
54
  canAdminSandbox,
55
55
  parseInsufficientBalance,
56
56
  useSidebar
57
- } from "./chunk-OQZQFSWM.js";
57
+ } from "./chunk-DZA4UBXU.js";
58
58
  import {
59
59
  BillingDashboard,
60
60
  InfoPanel,