@tangle-network/sandbox-ui 0.62.0 → 0.64.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.
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"). */
package/dist/chat.js CHANGED
@@ -17,7 +17,7 @@ import {
17
17
  modelProvider,
18
18
  snapHarnessToModel,
19
19
  snapModelToHarness
20
- } from "./chunk-R72QYE2U.js";
20
+ } from "./chunk-2E3FJM56.js";
21
21
  import "./chunk-HU4B2M4K.js";
22
22
  import "./chunk-SF6LPMMN.js";
23
23
  import "./chunk-EI44GEQ5.js";
@@ -156,7 +156,24 @@ import {
156
156
  reasoningEffortsFor
157
157
  } from "@tangle-network/agent-interface";
158
158
  import * as DropdownMenu2 from "@radix-ui/react-dropdown-menu";
159
- 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
+ }
160
177
 
161
178
  // src/chat/harness-model-compat.ts
162
179
  import {
@@ -173,21 +190,8 @@ function snapModelToHarness(harness, modelId, models) {
173
190
 
174
191
  // src/chat/agent-profile-picker.tsx
175
192
  import { Bot, Check, ChevronDown as ChevronDown2, Pencil, Plus, Trash2 } from "lucide-react";
176
- import * as React from "react";
193
+ import * as React2 from "react";
177
194
  import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
178
- function useClickOutside(onOutside) {
179
- const ref = React.useRef(null);
180
- React.useEffect(() => {
181
- function handler(event) {
182
- if (ref.current && !ref.current.contains(event.target)) {
183
- onOutside();
184
- }
185
- }
186
- document.addEventListener("mousedown", handler);
187
- return () => document.removeEventListener("mousedown", handler);
188
- }, [onOutside]);
189
- return ref;
190
- }
191
195
  function profileSubtitle(profile) {
192
196
  if (profile.description) return profile.description;
193
197
  const count = profile.capabilities?.length ?? 0;
@@ -205,8 +209,8 @@ function AgentProfilePicker({
205
209
  className,
206
210
  side = "top"
207
211
  }) {
208
- const [open, setOpen] = React.useState(false);
209
- const [composing, setComposing] = React.useState(
212
+ const [open, setOpen] = React2.useState(false);
213
+ const [composing, setComposing] = React2.useState(
210
214
  null
211
215
  );
212
216
  const ref = useClickOutside(() => {
@@ -373,11 +377,11 @@ function ProfileForm({
373
377
  onCancel,
374
378
  onSave
375
379
  }) {
376
- const [name, setName] = React.useState(initial?.name ?? "");
377
- const [caps, setCaps] = React.useState(
380
+ const [name, setName] = React2.useState(initial?.name ?? "");
381
+ const [caps, setCaps] = React2.useState(
378
382
  initial?.capabilities ? [...initial.capabilities] : capabilities.map((c) => c.id)
379
383
  );
380
- const [instructions, setInstructions] = React.useState(initial?.instructions ?? "");
384
+ const [instructions, setInstructions] = React2.useState(initial?.instructions ?? "");
381
385
  const toggle = (id) => setCaps(
382
386
  (prev) => prev.includes(id) ? prev.filter((c) => c !== id) : [...prev, id]
383
387
  );
@@ -466,6 +470,102 @@ function selectorIgnoreNote(harness) {
466
470
  if (ignoresEffort) return "Ignores reasoning effort";
467
471
  return null;
468
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
+ }
469
569
  function HarnessDropdown({
470
570
  value,
471
571
  onChange,
@@ -473,6 +573,10 @@ function HarnessDropdown({
473
573
  disabled,
474
574
  locked,
475
575
  lockReason,
576
+ onNewChat,
577
+ lockTitle,
578
+ lockBody,
579
+ newChatLabel,
476
580
  side = "bottom",
477
581
  align = "start",
478
582
  avoidCollisions
@@ -482,6 +586,18 @@ function HarnessDropdown({
482
586
  );
483
587
  const options = HARNESS_OPTIONS.filter((h) => allowed.has(h.type));
484
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
+ }
485
601
  return /* @__PURE__ */ jsxs3(DropdownMenu2.Root, { children: [
486
602
  /* @__PURE__ */ jsx3(DropdownMenu2.Trigger, { asChild: true, children: /* @__PURE__ */ jsxs3(
487
603
  "button",
@@ -785,7 +901,7 @@ import {
785
901
  Upload,
786
902
  X
787
903
  } from "lucide-react";
788
- import * as React2 from "react";
904
+ import * as React4 from "react";
789
905
  import { Fragment, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
790
906
  function AgentComposer({
791
907
  value,
@@ -816,22 +932,22 @@ function AgentComposer({
816
932
  sendLabel = "Send",
817
933
  autoFocus
818
934
  }) {
819
- const textareaRef = React2.useRef(null);
820
- const fileInputRef = React2.useRef(null);
821
- const folderInputRef = React2.useRef(null);
822
- const [dragOver, setDragOver] = React2.useState(false);
823
- const dragDepth = React2.useRef(0);
824
- 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(
825
941
  (el) => {
826
942
  el.style.height = "auto";
827
943
  el.style.height = `${Math.min(el.scrollHeight, maxHeight)}px`;
828
944
  },
829
945
  [maxHeight]
830
946
  );
831
- React2.useEffect(() => {
947
+ React4.useEffect(() => {
832
948
  if (textareaRef.current) resize(textareaRef.current);
833
949
  }, [value, resize]);
834
- React2.useEffect(() => {
950
+ React4.useEffect(() => {
835
951
  if (!focusShortcut || disabled) return;
836
952
  function onKeyDown(event) {
837
953
  if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "l") {
@@ -1070,7 +1186,7 @@ function AgentComposer({
1070
1186
  }
1071
1187
 
1072
1188
  // src/chat/artifact-agent-dock.tsx
1073
- import * as React3 from "react";
1189
+ import * as React5 from "react";
1074
1190
  import { Sparkles as Sparkles2, Send, Square as Square2, X as X2 } from "lucide-react";
1075
1191
  import { ChatMessage } from "@tangle-network/ui/chat";
1076
1192
  import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
@@ -1119,23 +1235,23 @@ function ArtifactAgentDock({
1119
1235
  onError,
1120
1236
  className
1121
1237
  }) {
1122
- const [threadId, setThreadId] = React3.useState(null);
1123
- const [messages, setMessages] = React3.useState([]);
1124
- const [composer, setComposer] = React3.useState(defaultPrompt ?? "");
1125
- const [streamContent, setStreamContent] = React3.useState("");
1126
- const [sending, setSending] = React3.useState(false);
1127
- const [loading, setLoading] = React3.useState(false);
1128
- const abortRef = React3.useRef(null);
1129
- const scrollRef = React3.useRef(null);
1130
- const scopeTokenRef = React3.useRef(0);
1131
- 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(
1132
1248
  (message) => {
1133
1249
  if (onError) onError(message);
1134
1250
  else if (typeof console !== "undefined") console.error("[ArtifactAgentDock]", message);
1135
1251
  },
1136
1252
  [onError]
1137
1253
  );
1138
- React3.useEffect(() => {
1254
+ React5.useEffect(() => {
1139
1255
  if (!open) return;
1140
1256
  scopeTokenRef.current += 1;
1141
1257
  abortRef.current?.abort();
@@ -1171,11 +1287,11 @@ function ArtifactAgentDock({
1171
1287
  cancelled = true;
1172
1288
  };
1173
1289
  }, [open, scope.kind, scope.key, transport, reportError]);
1174
- React3.useEffect(() => {
1290
+ React5.useEffect(() => {
1175
1291
  if (!scrollRef.current) return;
1176
1292
  scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
1177
1293
  }, [messages.length, streamContent]);
1178
- const send = React3.useCallback(async () => {
1294
+ const send = React5.useCallback(async () => {
1179
1295
  const text = composer.trim();
1180
1296
  if (!text || !threadId || sending) return;
1181
1297
  const token = scopeTokenRef.current;
@@ -1225,7 +1341,7 @@ function ArtifactAgentDock({
1225
1341
  if (abortRef.current === controller) abortRef.current = null;
1226
1342
  }
1227
1343
  }, [composer, threadId, sending, transport, reportError]);
1228
- const cancel = React3.useCallback(() => {
1344
+ const cancel = React5.useCallback(() => {
1229
1345
  if (threadId && transport.cancel) {
1230
1346
  try {
1231
1347
  void Promise.resolve(transport.cancel({ threadId })).catch(() => {
@@ -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-QSCCNGXZ.js";
7
7
  import {
8
8
  TerminalView
9
9
  } from "./chunk-VIREDUBF.js";
@@ -22,7 +22,7 @@ import {
22
22
  DropdownMenuTrigger
23
23
  } from "@tangle-network/ui/primitives";
24
24
  import { Skeleton, useTheme } from "@tangle-network/ui/primitives";
25
- import { Check, Monitor, PanelLeft } from "lucide-react";
25
+ import { Check, Monitor, MoreHorizontal, PanelLeft } from "lucide-react";
26
26
 
27
27
  // src/dashboard/sidebar-context.tsx
28
28
  import * as React from "react";
@@ -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);
@@ -581,7 +585,7 @@ function RailExpandable({
581
585
  const subLink = (item) => {
582
586
  const FIcon = item.icon;
583
587
  const active = activeSet.has(item.id);
584
- return /* @__PURE__ */ jsxs2(
588
+ const link = /* @__PURE__ */ jsxs2(
585
589
  Link,
586
590
  {
587
591
  href: item.href,
@@ -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 min-w-0 flex-1 items-center gap-2 rounded-md px-2.5 text-[12px] transition-colors",
597
+ active ? "h-7 bg-[var(--accent-surface-strong)] font-medium text-[var(--accent-text)]" : item.emphasis ? "py-2 font-medium text-[var(--accent-text)] hover:underline" : "h-7 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,
@@ -601,16 +605,41 @@ function RailExpandable({
601
605
  ...item.isLoading ? { role: "status", "aria-label": "Agent responding" } : {},
602
606
  children: item.label
603
607
  }
604
- ),
605
- item.isLoading ? /* @__PURE__ */ jsxs2("span", { className: "responding-ellipsis", "aria-hidden": "true", children: [
606
- /* @__PURE__ */ jsx3("span", {}),
607
- /* @__PURE__ */ jsx3("span", {}),
608
- /* @__PURE__ */ jsx3("span", {})
609
- ] }) : null
608
+ )
610
609
  ]
611
610
  },
612
611
  item.id
613
612
  );
613
+ if (!showLabel || !item.actions?.length) return link;
614
+ return /* @__PURE__ */ jsxs2("div", { className: "group/sub relative flex items-center", children: [
615
+ link,
616
+ /* @__PURE__ */ jsxs2(DropdownMenu, { children: [
617
+ /* @__PURE__ */ jsx3(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsx3(
618
+ "button",
619
+ {
620
+ type: "button",
621
+ "aria-label": "Chat actions",
622
+ className: "absolute right-1 flex h-6 w-6 shrink-0 items-center justify-center rounded-md bg-[var(--accent-surface-soft)] text-muted-foreground opacity-0 transition hover:text-foreground focus-visible:opacity-100 group-hover/sub:opacity-100 data-[state=open]:bg-[var(--accent-surface-strong)] data-[state=open]:opacity-100",
623
+ children: /* @__PURE__ */ jsx3(MoreHorizontal, { className: "h-3.5 w-3.5" })
624
+ }
625
+ ) }),
626
+ /* @__PURE__ */ jsx3(DropdownMenuContent, { align: "end", className: "w-40", children: item.actions.map((action) => /* @__PURE__ */ jsxs2(
627
+ DropdownMenuItem,
628
+ {
629
+ onSelect: action.onSelect,
630
+ className: cn(
631
+ "gap-2",
632
+ action.destructive && "text-destructive focus:text-destructive"
633
+ ),
634
+ children: [
635
+ action.icon ? /* @__PURE__ */ jsx3(action.icon, { className: "h-3.5 w-3.5" }) : null,
636
+ action.label
637
+ ]
638
+ },
639
+ action.id
640
+ )) })
641
+ ] })
642
+ ] }, item.id);
614
643
  };
615
644
  const list = loading ? /* @__PURE__ */ jsx3(SubItemsSkeleton, {}) : items && items.length > 0 ? items.map(subLink) : /* @__PURE__ */ jsx3("p", { className: "px-2.5 py-1.5 text-xs text-muted-foreground", children: emptyLabel });
616
645
  if (!showLabel) {
@@ -719,18 +748,15 @@ function RailExpandable({
719
748
  /* @__PURE__ */ jsx3(
720
749
  Icon,
721
750
  {
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
- )
751
+ className: "h-[17px] w-[17px] shrink-0 opacity-100 transition-opacity duration-150 group-hover/exp:opacity-0"
726
752
  }
727
753
  ),
728
754
  /* @__PURE__ */ jsx3(
729
755
  ChevronRightIcon,
730
756
  {
731
757
  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"
758
+ "absolute h-4 w-4 opacity-0 transition-all duration-150 group-hover/exp:opacity-100",
759
+ open && "rotate-90"
734
760
  )
735
761
  }
736
762
  )
@@ -2368,6 +2394,7 @@ function SidebarLayoutInner({
2368
2394
  loadSubItems: item.loadSubItems,
2369
2395
  activeSubIds: item.subActiveIds,
2370
2396
  emptyLabel: item.emptyLabel,
2397
+ defaultOpen: item.defaultOpen,
2371
2398
  onNavigate: handleNavClick,
2372
2399
  LinkComponent
2373
2400
  },
@@ -174,6 +174,17 @@ interface RailFlyoutProps {
174
174
  * related destinations without permanently spending rail height on them.
175
175
  */
176
176
  declare function RailFlyout({ icon: Icon, label, isActive, showLabel, title, children, className, }: RailFlyoutProps): react_jsx_runtime.JSX.Element;
177
+ /** A single entry in a sub-item's hover actions menu (kebab). */
178
+ interface RailSubItemAction {
179
+ id: string;
180
+ label: string;
181
+ icon?: React.ComponentType<{
182
+ className?: string;
183
+ }>;
184
+ /** Style the item as a destructive action (e.g. delete). */
185
+ destructive?: boolean;
186
+ onSelect: () => void;
187
+ }
177
188
  /** A sub-destination revealed by a {@link RailExpandable} item. */
178
189
  interface RailExpandableSubItem {
179
190
  id: string;
@@ -185,6 +196,12 @@ interface RailExpandableSubItem {
185
196
  prefetch?: "none" | "intent" | "render" | "viewport";
186
197
  /** Show a live working indicator on the row while the item is busy. */
187
198
  isLoading?: boolean;
199
+ /** Render the row emphasized (bold + accent) so it stands out — e.g. a
200
+ * trailing "view all" action below a capped list. */
201
+ emphasis?: boolean;
202
+ /** Hover-revealed kebab actions (e.g. rename/delete). Rendered on the
203
+ * labeled rail only; ignored in the collapsed-rail flyout. */
204
+ actions?: RailSubItemAction[];
188
205
  }
189
206
  interface RailExpandableProps {
190
207
  icon: React.ComponentType<{
@@ -201,6 +218,8 @@ interface RailExpandableProps {
201
218
  subItems?: RailExpandableSubItem[];
202
219
  /** Lazy sub-items — called once on first open; the result is cached. */
203
220
  loadSubItems?: () => Promise<RailExpandableSubItem[]>;
221
+ /** Start expanded on mount (labeled rail). Defaults to collapsed. */
222
+ defaultOpen?: boolean;
204
223
  /** Ids of sub-items whose route is active (drives sub-item highlight). */
205
224
  activeSubIds?: string[];
206
225
  /** Shown when the resolved sub-item list is empty. */
@@ -218,7 +237,7 @@ interface RailExpandableProps {
218
237
  * Sub-items can be fixed (`subItems`) or lazy (`loadSubItems`, with a loading
219
238
  * skeleton). Used for e.g. a "History" entry fronting recent sessions.
220
239
  */
221
- declare function RailExpandable({ icon: Icon, label, href, isActive, showLabel, subItems, loadSubItems, activeSubIds, emptyLabel, onNavigate, LinkComponent, className, }: RailExpandableProps): react_jsx_runtime.JSX.Element;
240
+ declare function RailExpandable({ icon: Icon, label, href, isActive, showLabel, subItems, loadSubItems, activeSubIds, emptyLabel, defaultOpen, onNavigate, LinkComponent, className, }: RailExpandableProps): react_jsx_runtime.JSX.Element;
222
241
  interface RailModeButtonProps {
223
242
  mode: string;
224
243
  icon: React.ComponentType<{
@@ -913,6 +932,8 @@ interface SidebarLayoutNavItem {
913
932
  subActiveIds?: string[];
914
933
  /** Empty-state text for an `expandable` item that resolves to no sub-items. */
915
934
  emptyLabel?: string;
935
+ /** Start an `expandable` item expanded on mount (labeled rail). */
936
+ defaultOpen?: boolean;
916
937
  badge?: number;
917
938
  /**
918
939
  * 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-QSCCNGXZ.js";
58
58
  import {
59
59
  BillingDashboard,
60
60
  InfoPanel,
package/dist/globals.css CHANGED
@@ -3782,6 +3782,13 @@
3782
3782
  }
3783
3783
  }
3784
3784
  }
3785
+ .group-hover\/sub\:opacity-100 {
3786
+ &:is(:where(.group\/sub):hover *) {
3787
+ @media (hover: hover) {
3788
+ opacity: 100%;
3789
+ }
3790
+ }
3791
+ }
3785
3792
  .peer-disabled\:cursor-not-allowed {
3786
3793
  &:is(:where(.peer):disabled ~ *) {
3787
3794
  cursor: not-allowed;
@@ -4984,6 +4991,11 @@
4984
4991
  border-color: var(--md3-outline-variant);
4985
4992
  }
4986
4993
  }
4994
+ .data-\[state\=open\]\:bg-\[var\(--accent-surface-strong\)\] {
4995
+ &[data-state="open"] {
4996
+ background-color: var(--accent-surface-strong);
4997
+ }
4998
+ }
4987
4999
  .data-\[state\=open\]\:bg-accent {
4988
5000
  &[data-state="open"] {
4989
5001
  background-color: var(--color-accent);
@@ -5007,6 +5019,11 @@
5007
5019
  color: var(--color-muted-foreground);
5008
5020
  }
5009
5021
  }
5022
+ .data-\[state\=open\]\:opacity-100 {
5023
+ &[data-state="open"] {
5024
+ opacity: 100%;
5025
+ }
5026
+ }
5010
5027
  .data-\[state\=selected\]\:bg-muted\/50 {
5011
5028
  &[data-state="selected"] {
5012
5029
  background-color: color-mix(in srgb, hsl(var(--muted)) 50%, transparent);
@@ -6014,53 +6031,23 @@
6014
6031
  .shimmer-text {
6015
6032
  background-image: linear-gradient(
6016
6033
  90deg,
6017
- hsl(var(--muted-foreground)) 0%,
6018
- hsl(var(--muted-foreground)) 28%,
6019
- var(--accent-text) 50%,
6020
- hsl(var(--muted-foreground)) 72%,
6021
- hsl(var(--muted-foreground)) 100%
6034
+ hsl(var(--muted-foreground) / 0.5) 0%,
6035
+ hsl(var(--muted-foreground) / 0.5) 40%,
6036
+ hsl(var(--primary)) 50%,
6037
+ hsl(var(--muted-foreground) / 0.5) 60%,
6038
+ hsl(var(--muted-foreground) / 0.5) 100%
6022
6039
  );
6023
6040
  background-size: 200% 100%;
6024
6041
  -webkit-background-clip: text;
6025
6042
  background-clip: text;
6026
6043
  -webkit-text-fill-color: transparent;
6027
- animation: shimmer 1.3s linear infinite;
6044
+ animation: shimmer 1.1s linear infinite;
6028
6045
  }
6029
6046
  @media (prefers-reduced-motion: reduce) {
6030
6047
  .shimmer-text {
6031
6048
  animation: none;
6032
6049
  }
6033
6050
  }
6034
- .responding-ellipsis {
6035
- display: inline-flex;
6036
- width: 1.25rem;
6037
- margin-left: auto;
6038
- flex-shrink: 0;
6039
- align-items: center;
6040
- justify-content: center;
6041
- gap: 0.1875rem;
6042
- color: hsl(var(--primary));
6043
- }
6044
- .responding-ellipsis > span {
6045
- width: 0.25rem;
6046
- height: 0.25rem;
6047
- border-radius: 9999px;
6048
- background: currentColor;
6049
- opacity: 0.6;
6050
- animation: responding-ellipsis 0.8s ease-in-out infinite;
6051
- }
6052
- .responding-ellipsis > span:nth-child(2) {
6053
- animation-delay: 0.15s;
6054
- }
6055
- .responding-ellipsis > span:nth-child(3) {
6056
- animation-delay: 0.3s;
6057
- }
6058
- @media (prefers-reduced-motion: reduce) {
6059
- .responding-ellipsis > span {
6060
- animation: none;
6061
- opacity: 0.8;
6062
- }
6063
- }
6064
6051
  .animate-in {
6065
6052
  animation: fade-in 0.25s ease-out;
6066
6053
  }
@@ -6095,16 +6082,6 @@
6095
6082
  background-position: 200% 0;
6096
6083
  }
6097
6084
  }
6098
- @keyframes responding-ellipsis {
6099
- 0%, 80%, 100% {
6100
- opacity: 0.6;
6101
- transform: translateY(0);
6102
- }
6103
- 40% {
6104
- opacity: 1;
6105
- transform: translateY(-1.5px);
6106
- }
6107
- }
6108
6085
  @keyframes fade-in {
6109
6086
  from {
6110
6087
  opacity: 0;
package/dist/index.js CHANGED
@@ -102,7 +102,7 @@ import {
102
102
  modelProvider,
103
103
  snapHarnessToModel,
104
104
  snapModelToHarness
105
- } from "./chunk-R72QYE2U.js";
105
+ } from "./chunk-2E3FJM56.js";
106
106
  import {
107
107
  ExpandedToolDetail,
108
108
  InlineThinkingItem,
@@ -146,7 +146,7 @@ import {
146
146
  buildUnifiedPatch,
147
147
  computeDiffStats,
148
148
  resolveLanguage
149
- } from "./chunk-J36F6LD2.js";
149
+ } from "./chunk-G4YJYSFA.js";
150
150
  import {
151
151
  FileArtifactPane,
152
152
  FilePreview,
@@ -211,7 +211,7 @@ import {
211
211
  canAdminSandbox,
212
212
  parseInsufficientBalance,
213
213
  useSidebar
214
- } from "./chunk-OQZQFSWM.js";
214
+ } from "./chunk-QSCCNGXZ.js";
215
215
  import "./chunk-VIREDUBF.js";
216
216
  import {
217
217
  usePtySession
package/dist/styles.css CHANGED
@@ -3782,6 +3782,13 @@
3782
3782
  }
3783
3783
  }
3784
3784
  }
3785
+ .group-hover\/sub\:opacity-100 {
3786
+ &:is(:where(.group\/sub):hover *) {
3787
+ @media (hover: hover) {
3788
+ opacity: 100%;
3789
+ }
3790
+ }
3791
+ }
3785
3792
  .peer-disabled\:cursor-not-allowed {
3786
3793
  &:is(:where(.peer):disabled ~ *) {
3787
3794
  cursor: not-allowed;
@@ -4984,6 +4991,11 @@
4984
4991
  border-color: var(--md3-outline-variant);
4985
4992
  }
4986
4993
  }
4994
+ .data-\[state\=open\]\:bg-\[var\(--accent-surface-strong\)\] {
4995
+ &[data-state="open"] {
4996
+ background-color: var(--accent-surface-strong);
4997
+ }
4998
+ }
4987
4999
  .data-\[state\=open\]\:bg-accent {
4988
5000
  &[data-state="open"] {
4989
5001
  background-color: var(--color-accent);
@@ -5007,6 +5019,11 @@
5007
5019
  color: var(--color-muted-foreground);
5008
5020
  }
5009
5021
  }
5022
+ .data-\[state\=open\]\:opacity-100 {
5023
+ &[data-state="open"] {
5024
+ opacity: 100%;
5025
+ }
5026
+ }
5010
5027
  .data-\[state\=selected\]\:bg-muted\/50 {
5011
5028
  &[data-state="selected"] {
5012
5029
  background-color: color-mix(in srgb, hsl(var(--muted)) 50%, transparent);
@@ -6014,53 +6031,23 @@
6014
6031
  .shimmer-text {
6015
6032
  background-image: linear-gradient(
6016
6033
  90deg,
6017
- hsl(var(--muted-foreground)) 0%,
6018
- hsl(var(--muted-foreground)) 28%,
6019
- var(--accent-text) 50%,
6020
- hsl(var(--muted-foreground)) 72%,
6021
- hsl(var(--muted-foreground)) 100%
6034
+ hsl(var(--muted-foreground) / 0.5) 0%,
6035
+ hsl(var(--muted-foreground) / 0.5) 40%,
6036
+ hsl(var(--primary)) 50%,
6037
+ hsl(var(--muted-foreground) / 0.5) 60%,
6038
+ hsl(var(--muted-foreground) / 0.5) 100%
6022
6039
  );
6023
6040
  background-size: 200% 100%;
6024
6041
  -webkit-background-clip: text;
6025
6042
  background-clip: text;
6026
6043
  -webkit-text-fill-color: transparent;
6027
- animation: shimmer 1.3s linear infinite;
6044
+ animation: shimmer 1.1s linear infinite;
6028
6045
  }
6029
6046
  @media (prefers-reduced-motion: reduce) {
6030
6047
  .shimmer-text {
6031
6048
  animation: none;
6032
6049
  }
6033
6050
  }
6034
- .responding-ellipsis {
6035
- display: inline-flex;
6036
- width: 1.25rem;
6037
- margin-left: auto;
6038
- flex-shrink: 0;
6039
- align-items: center;
6040
- justify-content: center;
6041
- gap: 0.1875rem;
6042
- color: hsl(var(--primary));
6043
- }
6044
- .responding-ellipsis > span {
6045
- width: 0.25rem;
6046
- height: 0.25rem;
6047
- border-radius: 9999px;
6048
- background: currentColor;
6049
- opacity: 0.6;
6050
- animation: responding-ellipsis 0.8s ease-in-out infinite;
6051
- }
6052
- .responding-ellipsis > span:nth-child(2) {
6053
- animation-delay: 0.15s;
6054
- }
6055
- .responding-ellipsis > span:nth-child(3) {
6056
- animation-delay: 0.3s;
6057
- }
6058
- @media (prefers-reduced-motion: reduce) {
6059
- .responding-ellipsis > span {
6060
- animation: none;
6061
- opacity: 0.8;
6062
- }
6063
- }
6064
6051
  .animate-in {
6065
6052
  animation: fade-in 0.25s ease-out;
6066
6053
  }
@@ -6095,16 +6082,6 @@
6095
6082
  background-position: 200% 0;
6096
6083
  }
6097
6084
  }
6098
- @keyframes responding-ellipsis {
6099
- 0%, 80%, 100% {
6100
- opacity: 0.6;
6101
- transform: translateY(0);
6102
- }
6103
- 40% {
6104
- opacity: 1;
6105
- transform: translateY(-1.5px);
6106
- }
6107
- }
6108
6085
  @keyframes fade-in {
6109
6086
  from {
6110
6087
  opacity: 0;
package/dist/workbench.js CHANGED
@@ -12,9 +12,9 @@ import {
12
12
  buildUnifiedPatch,
13
13
  computeDiffStats,
14
14
  resolveLanguage
15
- } from "./chunk-J36F6LD2.js";
15
+ } from "./chunk-G4YJYSFA.js";
16
16
  import "./chunk-3J6FG3FJ.js";
17
- import "./chunk-OQZQFSWM.js";
17
+ import "./chunk-QSCCNGXZ.js";
18
18
  import "./chunk-VIREDUBF.js";
19
19
  import "./chunk-77WVVJA4.js";
20
20
  import "./chunk-OMUIPJQZ.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/sandbox-ui",
3
- "version": "0.62.0",
3
+ "version": "0.64.0",
4
4
  "description": "Unified UI component library for Tangle Sandbox — primitives, chat, dashboard, terminal, editor, and workspace components",
5
5
  "repository": {
6
6
  "type": "git",