@tangle-network/sandbox-ui 0.112.0 → 0.113.1

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.
@@ -7,6 +7,7 @@ import {
7
7
 
8
8
  // src/workspace/workspace-layout.tsx
9
9
  import {
10
+ useCallback,
10
11
  useEffect,
11
12
  useMemo,
12
13
  useRef,
@@ -46,6 +47,14 @@ var DESKTOP_BREAKPOINT = "(min-width: 1024px)";
46
47
  function clamp(value, min, max) {
47
48
  return Math.min(Math.max(value, min), max);
48
49
  }
50
+ function isEditableTarget(target) {
51
+ if (!(target instanceof HTMLElement)) return false;
52
+ if (target.isContentEditable) return true;
53
+ const tag = target.tagName;
54
+ if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return true;
55
+ const editable = target.closest("[contenteditable]");
56
+ return editable !== null && editable.getAttribute("contenteditable") !== "false";
57
+ }
49
58
  function readStoredLayout(key) {
50
59
  if (typeof window === "undefined") return null;
51
60
  try {
@@ -215,6 +224,15 @@ function WorkspaceLayout({
215
224
  leftLabel = "Left workspace panel",
216
225
  rightLabel = "Right workspace panel",
217
226
  bottomLabel = "Bottom runtime panel",
227
+ leftOpen: leftOpenProp,
228
+ onLeftOpenChange,
229
+ rightOpen: rightOpenProp,
230
+ onRightOpenChange,
231
+ keyboardShortcuts = false,
232
+ leftCollapsedControl,
233
+ leftContentClassName,
234
+ rightContentClassName,
235
+ centerHeaderVisibility = "always",
218
236
  className
219
237
  }) {
220
238
  const desktop = useDesktopMediaQuery(DESKTOP_BREAKPOINT);
@@ -223,8 +241,28 @@ function WorkspaceLayout({
223
241
  () => persistenceKey ? readStoredLayout(persistenceKey) : null,
224
242
  [persistenceKey]
225
243
  );
226
- const [leftOpen, setLeftOpen] = useState(storedLayout?.leftOpen ?? defaultLeftOpen);
227
- const [rightOpen, setRightOpen] = useState(storedLayout?.rightOpen ?? defaultRightOpen);
244
+ const [uncontrolledLeftOpen, setUncontrolledLeftOpen] = useState(storedLayout?.leftOpen ?? defaultLeftOpen);
245
+ const [uncontrolledRightOpen, setUncontrolledRightOpen] = useState(storedLayout?.rightOpen ?? defaultRightOpen);
246
+ const leftControlled = leftOpenProp !== void 0;
247
+ const rightControlled = rightOpenProp !== void 0;
248
+ const leftOpen = leftControlled ? leftOpenProp : uncontrolledLeftOpen;
249
+ const rightOpen = rightControlled ? rightOpenProp : uncontrolledRightOpen;
250
+ const setLeftOpen = useCallback(
251
+ (open) => {
252
+ if (!leftControlled) setUncontrolledLeftOpen(open);
253
+ onLeftOpenChange?.(open);
254
+ },
255
+ [leftControlled, onLeftOpenChange]
256
+ );
257
+ const setRightOpen = useCallback(
258
+ (open) => {
259
+ if (!rightControlled) setUncontrolledRightOpen(open);
260
+ onRightOpenChange?.(open);
261
+ },
262
+ [rightControlled, onRightOpenChange]
263
+ );
264
+ const hasLeft = Boolean(left);
265
+ const hasRight = Boolean(right);
228
266
  const [bottomOpen, setBottomOpen] = useState(storedLayout?.bottomOpen ?? defaultBottomOpen);
229
267
  const [leftWidth, setLeftWidth] = useState(
230
268
  clamp(storedLayout?.leftWidth ?? defaultLeftWidth, minLeftWidth, maxLeftWidth)
@@ -235,6 +273,8 @@ function WorkspaceLayout({
235
273
  const [bottomHeight, setBottomHeight] = useState(
236
274
  clamp(storedLayout?.bottomHeight ?? defaultBottomHeight, minBottomHeight, maxBottomHeight)
237
275
  );
276
+ const centerHeaderHasContent = Boolean(centerHeader) || hasLeft && !leftOpen || hasRight && !rightOpen || Boolean(bottom) && !bottomOpen;
277
+ const showCenterHeader = centerHeaderVisibility === "auto" ? centerHeaderHasContent : Boolean(centerHeader || left || right || bottom);
238
278
  useEffect(() => {
239
279
  if (!persistenceKey || typeof window === "undefined") return;
240
280
  const payload = {
@@ -247,6 +287,23 @@ function WorkspaceLayout({
247
287
  };
248
288
  window.localStorage.setItem(persistenceKey, JSON.stringify(payload));
249
289
  }, [bottomHeight, bottomOpen, leftOpen, leftWidth, persistenceKey, rightOpen, rightWidth]);
290
+ useEffect(() => {
291
+ if (!keyboardShortcuts || typeof window === "undefined") return;
292
+ const handleKeyDown = (event) => {
293
+ if (!(event.metaKey || event.ctrlKey) || event.altKey || event.shiftKey) return;
294
+ if (isEditableTarget(event.target)) return;
295
+ const key = event.key.toLowerCase();
296
+ if (key === "b" && hasLeft) {
297
+ event.preventDefault();
298
+ setLeftOpen(!leftOpen);
299
+ } else if (key === "e" && hasRight) {
300
+ event.preventDefault();
301
+ setRightOpen(!rightOpen);
302
+ }
303
+ };
304
+ window.addEventListener("keydown", handleKeyDown);
305
+ return () => window.removeEventListener("keydown", handleKeyDown);
306
+ }, [hasLeft, hasRight, keyboardShortcuts, leftOpen, rightOpen, setLeftOpen, setRightOpen]);
250
307
  useEffect(() => {
251
308
  if (!desktop) return;
252
309
  const handlePointerMove = (event) => {
@@ -336,7 +393,7 @@ function WorkspaceLayout({
336
393
  }
337
394
  )
338
395
  ] }),
339
- /* @__PURE__ */ jsx2("div", { className: "min-h-0 flex-1 overflow-auto py-1", children: left })
396
+ /* @__PURE__ */ jsx2("div", { className: cn("min-h-0 flex-1 overflow-auto py-1", leftContentClassName), children: left })
340
397
  ]
341
398
  }
342
399
  ),
@@ -350,8 +407,8 @@ function WorkspaceLayout({
350
407
  )
351
408
  ] }),
352
409
  /* @__PURE__ */ jsxs("main", { className: "flex min-w-0 flex-1 flex-col", children: [
353
- (centerHeader || left || right || bottom) && /* @__PURE__ */ jsxs(WorkspacePaneHeader, { className: "gap-2", children: [
354
- left && !leftOpen && /* @__PURE__ */ jsx2(
410
+ showCenterHeader && /* @__PURE__ */ jsxs(WorkspacePaneHeader, { className: "gap-2", children: [
411
+ left && !leftOpen && (leftCollapsedControl ?? /* @__PURE__ */ jsx2(
355
412
  "button",
356
413
  {
357
414
  type: "button",
@@ -360,7 +417,7 @@ function WorkspaceLayout({
360
417
  className: "rounded-[2px] p-1 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary/60",
361
418
  children: /* @__PURE__ */ jsx2(PanelLeftOpen, { className: "h-4 w-4" })
362
419
  }
363
- ),
420
+ )),
364
421
  /* @__PURE__ */ jsx2("div", { className: "min-w-0 flex-1", children: centerHeader }),
365
422
  bottom && !bottomOpen && /* @__PURE__ */ jsx2(
366
423
  "button",
@@ -449,7 +506,7 @@ function WorkspaceLayout({
449
506
  }
450
507
  )
451
508
  ] }),
452
- /* @__PURE__ */ jsx2("div", { className: "min-h-0 flex-1 overflow-auto", children: right })
509
+ /* @__PURE__ */ jsx2("div", { className: cn("min-h-0 flex-1 overflow-auto", rightContentClassName), children: right })
453
510
  ]
454
511
  }
455
512
  )
@@ -865,12 +922,35 @@ function RuntimePane({
865
922
  }
866
923
 
867
924
  // src/workspace/session-sidebar.tsx
868
- import { useCallback as useCallback2, useImperativeHandle, useMemo as useMemo3, useRef as useRef4, useState as useState4 } from "react";
869
- import { ArrowLeft, FolderTree, MessageSquareText, Plus, Search as Search2, Settings, Sparkles } from "lucide-react";
925
+ import { useCallback as useCallback3, useImperativeHandle, useMemo as useMemo3, useRef as useRef4, useState as useState4 } from "react";
926
+ import { ArrowLeft, FolderTree, MessageSquareText, PanelLeftClose as PanelLeftClose2, Plus, Search as Search2, Settings, Sparkles } from "lucide-react";
927
+
928
+ // src/lib/format-relative-age.ts
929
+ var SECOND = 1e3;
930
+ var MINUTE = 60 * SECOND;
931
+ var HOUR = 60 * MINUTE;
932
+ var DAY = 24 * HOUR;
933
+ var WEEK = 7 * DAY;
934
+ var MONTH = 30 * DAY;
935
+ var YEAR = 365 * DAY;
936
+ function formatRelativeAge(date, now = Date.now()) {
937
+ const timestamp = date instanceof Date ? date.getTime() : new Date(date).getTime();
938
+ if (Number.isNaN(timestamp)) return "";
939
+ const elapsed = Math.max(0, now - timestamp);
940
+ if (elapsed < MINUTE) return "now";
941
+ if (elapsed < HOUR) return `${Math.floor(elapsed / MINUTE)}m`;
942
+ if (elapsed < DAY) return `${Math.floor(elapsed / HOUR)}h`;
943
+ if (elapsed < WEEK) return `${Math.floor(elapsed / DAY)}d`;
944
+ if (elapsed < MONTH) return `${Math.floor(elapsed / WEEK)}w`;
945
+ if (elapsed < YEAR) return `${Math.floor(elapsed / MONTH)}mo`;
946
+ return `${Math.floor(elapsed / YEAR)}y`;
947
+ }
948
+
949
+ // src/workspace/session-sidebar.tsx
870
950
  import { useNavbarSessions } from "@tangle-network/ui/stores";
871
951
 
872
952
  // src/workspace/session-optimistic.ts
873
- import { useCallback, useEffect as useEffect3, useRef as useRef3, useState as useState3 } from "react";
953
+ import { useCallback as useCallback2, useEffect as useEffect3, useRef as useRef3, useState as useState3 } from "react";
874
954
  var SESSION_CREATED_EVENT = "sandbox-thread-created";
875
955
  function dispatchSessionCreated(detail, eventName = SESSION_CREATED_EVENT) {
876
956
  if (typeof window === "undefined") return;
@@ -885,19 +965,19 @@ function useOptimisticSessionItems(items, options = {}) {
885
965
  useEffect3(() => {
886
966
  setOptimisticItems(items);
887
967
  }, [items]);
888
- const add = useCallback((detail) => {
968
+ const add = useCallback2((detail) => {
889
969
  const next = toItem(detail);
890
970
  setOptimisticItems((prev) => [next, ...prev.filter((item) => item.id !== next.id)]);
891
971
  }, []);
892
- const update = useCallback((id, patch) => {
972
+ const update = useCallback2((id, patch) => {
893
973
  setOptimisticItems(
894
974
  (prev) => prev.map((item) => item.id === id ? { ...item, ...patch } : item)
895
975
  );
896
976
  }, []);
897
- const remove = useCallback((id) => {
977
+ const remove = useCallback2((id) => {
898
978
  setOptimisticItems((prev) => prev.filter((item) => item.id !== id));
899
979
  }, []);
900
- const reset = useCallback(() => setOptimisticItems(items), [items]);
980
+ const reset = useCallback2(() => setOptimisticItems(items), [items]);
901
981
  const addRef = useRef3(add);
902
982
  addRef.current = add;
903
983
  useEffect3(() => {
@@ -928,6 +1008,24 @@ var NO_CONTROLLER = {
928
1008
  reset: () => {
929
1009
  }
930
1010
  };
1011
+ var STATUS_GROUPS = [
1012
+ { key: "attention", label: "Needs input" },
1013
+ { key: "failed", label: "Failed" },
1014
+ { key: "working", label: "Working" },
1015
+ { key: "done", label: "Done" }
1016
+ ];
1017
+ function groupKeyForStatus(status) {
1018
+ switch (status) {
1019
+ case "attention-needed":
1020
+ return "attention";
1021
+ case "error":
1022
+ return "failed";
1023
+ case "running":
1024
+ return "working";
1025
+ default:
1026
+ return "done";
1027
+ }
1028
+ }
931
1029
  function statusDot(status) {
932
1030
  switch (status) {
933
1031
  case "running":
@@ -952,6 +1050,11 @@ function iconForLink(icon) {
952
1050
  return Sparkles;
953
1051
  }
954
1052
  }
1053
+ function toDate(value) {
1054
+ if (value === void 0) return null;
1055
+ const date = value instanceof Date ? value : new Date(value);
1056
+ return Number.isNaN(date.getTime()) ? null : date;
1057
+ }
955
1058
  function sortItems(items, sessionStatusById) {
956
1059
  return [...items].sort((left, right) => {
957
1060
  if (Boolean(left.isPinned) !== Boolean(right.isPinned)) {
@@ -1003,20 +1106,20 @@ function useResizable(defaultWidth, min, max, onChange) {
1003
1106
  const dragging = useRef4(false);
1004
1107
  const startX = useRef4(0);
1005
1108
  const startW = useRef4(0);
1006
- const onPointerDown = useCallback2((e) => {
1109
+ const onPointerDown = useCallback3((e) => {
1007
1110
  dragging.current = true;
1008
1111
  startX.current = e.clientX;
1009
1112
  startW.current = width;
1010
1113
  e.currentTarget.setPointerCapture(e.pointerId);
1011
1114
  }, [width]);
1012
- const onPointerMove = useCallback2((e) => {
1115
+ const onPointerMove = useCallback3((e) => {
1013
1116
  if (!dragging.current) return;
1014
1117
  const delta = e.clientX - startX.current;
1015
1118
  const next = Math.min(max, Math.max(min, startW.current + delta));
1016
1119
  setWidth(next);
1017
1120
  onChange?.(next);
1018
1121
  }, [min, max, onChange]);
1019
- const onPointerUp = useCallback2(() => {
1122
+ const onPointerUp = useCallback3(() => {
1020
1123
  dragging.current = false;
1021
1124
  }, []);
1022
1125
  return { width, onPointerDown, onPointerMove, onPointerUp };
@@ -1046,8 +1149,14 @@ function SessionSidebar({
1046
1149
  onWidthChange,
1047
1150
  renderItemActions,
1048
1151
  optimistic = false,
1049
- optimisticRef
1152
+ optimisticRef,
1153
+ variant = "default",
1154
+ groupBy = "none",
1155
+ showUpdatedAt = false,
1156
+ onCollapse,
1157
+ fill = false
1050
1158
  }) {
1159
+ const quiet = variant === "quiet";
1051
1160
  const [query, setQuery] = useState4("");
1052
1161
  const [activeFilterId, setActiveFilterId] = useState4(defaultFilterId ?? filters[0]?.id ?? "all");
1053
1162
  const optimisticOptions = typeof optimistic === "object" ? optimistic : void 0;
@@ -1085,6 +1194,19 @@ function SessionSidebar({
1085
1194
  return haystack.includes(normalizedQuery);
1086
1195
  });
1087
1196
  }, [activeFilterId, currentItemId, filters, orderedItems, query, sessionsById]);
1197
+ const groups = useMemo3(() => {
1198
+ if (groupBy !== "status") {
1199
+ return [{ key: "all", label: null, items: visibleItems }];
1200
+ }
1201
+ return STATUS_GROUPS.map((group) => ({
1202
+ key: group.key,
1203
+ label: group.label,
1204
+ items: visibleItems.filter(
1205
+ (item) => groupKeyForStatus(sessionsById.get(item.id)?.status ?? item.status) === group.key
1206
+ )
1207
+ })).filter((group) => group.items.length > 0);
1208
+ }, [groupBy, sessionsById, visibleItems]);
1209
+ const showGroupLabels = groups.length > 1;
1088
1210
  const runningCount = activeSessions.filter((session) => session.status === "running").length;
1089
1211
  const filterCounts = useMemo3(() => Object.fromEntries(filters.map((filter) => [
1090
1212
  filter.id,
@@ -1094,13 +1216,134 @@ function SessionSidebar({
1094
1216
  })).length
1095
1217
  ])), [currentItemId, filters, orderedItems, sessionsById]);
1096
1218
  const resize = useResizable(defaultWidth, minWidth, maxWidth, onWidthChange);
1219
+ const fillsParent = fill && !resizable;
1220
+ const renderRow = (item, index) => {
1221
+ const session = sessionsById.get(item.id) ?? null;
1222
+ const isActive = currentItemId === item.id;
1223
+ const status = session?.status ?? item.status;
1224
+ const isWorking = status === "running";
1225
+ const visibleBadges = [
1226
+ ...item.isPinned ? [{ id: `${item.id}-pinned`, label: "Pinned", tone: "neutral" }] : [],
1227
+ ...item.badges ?? []
1228
+ ].filter((badge) => !quiet || (badge.tone ?? "neutral") !== "neutral");
1229
+ const updatedAt = showUpdatedAt && item.meta === void 0 ? toDate(item.updatedAt) : null;
1230
+ const metaLine = item.meta ?? (updatedAt ? /* @__PURE__ */ jsx8("time", { dateTime: updatedAt.toISOString(), title: updatedAt.toLocaleString(), children: formatRelativeAge(updatedAt) }) : null);
1231
+ return /* @__PURE__ */ jsx8(
1232
+ "li",
1233
+ {
1234
+ className: "agent-arrive",
1235
+ style: { "--stagger-index": index },
1236
+ children: /* @__PURE__ */ jsxs7(
1237
+ "div",
1238
+ {
1239
+ className: cn(
1240
+ quiet ? "group relative flex items-center gap-2 rounded-lg py-2 pl-2 pr-3 transition-colors" : "group relative flex items-center gap-2 rounded-[var(--radius-sm)] px-2.5 py-1.5 transition-colors",
1241
+ MOTION_CONTROL,
1242
+ quiet ? isActive ? "bg-surface-container-high" : "hover:bg-surface-container" : isActive ? "bg-accent font-medium text-foreground" : "text-muted-foreground hover:bg-accent/30"
1243
+ ),
1244
+ children: [
1245
+ /* @__PURE__ */ jsxs7(
1246
+ "button",
1247
+ {
1248
+ type: "button",
1249
+ onClick: () => {
1250
+ if (onSelectItem) {
1251
+ onSelectItem(item);
1252
+ return;
1253
+ }
1254
+ navigateToHref(item.href);
1255
+ },
1256
+ "aria-current": isActive ? "page" : void 0,
1257
+ className: "min-w-0 flex flex-1 items-center gap-2 text-left",
1258
+ children: [
1259
+ item.icon ? /* @__PURE__ */ jsxs7("span", { className: "relative flex h-5 w-4 shrink-0 items-center justify-center", "aria-hidden": "true", children: [
1260
+ item.icon,
1261
+ /* @__PURE__ */ jsx8("span", { className: cn("absolute -bottom-0.5 -right-0.5 h-1.5 w-1.5 rounded-full", statusDot(status)) })
1262
+ ] }) : /* @__PURE__ */ jsx8("span", { className: cn("h-1.5 w-1.5 shrink-0 rounded-full", statusDot(status)) }),
1263
+ /* @__PURE__ */ jsxs7("div", { className: "min-w-0 flex-1", children: [
1264
+ /* @__PURE__ */ jsx8(
1265
+ "div",
1266
+ {
1267
+ className: cn(
1268
+ quiet ? "truncate text-sm leading-5 text-foreground/75 group-hover:text-foreground" : "truncate text-sm font-medium",
1269
+ isActive && "text-foreground",
1270
+ isWorking && "agent-shimmer"
1271
+ ),
1272
+ ...isWorking ? { "data-motion": "essential" } : {},
1273
+ children: item.title
1274
+ }
1275
+ ),
1276
+ item.subtitle && /* @__PURE__ */ jsx8("div", { className: "truncate text-xs leading-tight text-muted-foreground", children: item.subtitle }),
1277
+ metaLine && /* @__PURE__ */ jsx8("div", { className: "truncate text-xs leading-tight text-muted-foreground", children: metaLine })
1278
+ ] })
1279
+ ]
1280
+ }
1281
+ ),
1282
+ /* @__PURE__ */ jsxs7("div", { className: "flex shrink-0 items-center gap-1", children: [
1283
+ visibleBadges.length > 0 && visibleBadges.slice(0, 1).map((badge) => /* @__PURE__ */ jsx8(
1284
+ "span",
1285
+ {
1286
+ className: cn(
1287
+ "rounded-full border px-1.5 py-px text-[8px] font-semibold uppercase",
1288
+ badgeTone(badge.tone)
1289
+ ),
1290
+ children: badge.label
1291
+ },
1292
+ badge.id
1293
+ )),
1294
+ session?.isForeground && /* @__PURE__ */ jsx8("span", { className: "h-1.5 w-1.5 rounded-full bg-[var(--status-running)]", title: "Live" }),
1295
+ renderItemActions ? /* @__PURE__ */ jsx8(
1296
+ "div",
1297
+ {
1298
+ className: cn("opacity-0 transition-opacity group-hover:opacity-100", MOTION_CONTROL),
1299
+ onClick: (event) => event.stopPropagation(),
1300
+ children: renderItemActions(item, { session, isActive })
1301
+ }
1302
+ ) : null
1303
+ ] })
1304
+ ]
1305
+ }
1306
+ )
1307
+ },
1308
+ item.id
1309
+ );
1310
+ };
1311
+ let rowIndex = 0;
1097
1312
  return /* @__PURE__ */ jsxs7(
1098
1313
  "aside",
1099
1314
  {
1100
- className: cn("relative flex shrink-0 flex-col border-r border-[var(--md3-outline-variant)] bg-surface-container-low", className),
1101
- style: { width: resizable ? resize.width : defaultWidth },
1315
+ className: cn(
1316
+ "relative flex shrink-0 flex-col border-r border-[var(--md3-outline-variant)] bg-surface-container-low",
1317
+ fillsParent && "h-full w-full",
1318
+ className
1319
+ ),
1320
+ style: fillsParent ? void 0 : { width: resizable ? resize.width : defaultWidth },
1102
1321
  children: [
1103
- /* @__PURE__ */ jsx8(WorkspacePaneHeader, { children: /* @__PURE__ */ jsxs7("div", { className: "flex w-full items-center justify-between gap-2", children: [
1322
+ quiet ? /* @__PURE__ */ jsxs7("div", { className: "flex h-14 shrink-0 items-center justify-between gap-2 px-3", children: [
1323
+ /* @__PURE__ */ jsx8("span", { className: "sr-only", children: title }),
1324
+ onCreate ? /* @__PURE__ */ jsxs7(
1325
+ "button",
1326
+ {
1327
+ type: "button",
1328
+ onClick: onCreate,
1329
+ className: cn("flex items-center gap-1.5 rounded-md px-1.5 py-1 text-sm text-muted-foreground transition-colors hover:text-foreground", MOTION_CONTROL),
1330
+ children: [
1331
+ /* @__PURE__ */ jsx8(Plus, { className: "h-4 w-4", "aria-hidden": "true" }),
1332
+ /* @__PURE__ */ jsx8("span", { children: createLabel })
1333
+ ]
1334
+ }
1335
+ ) : /* @__PURE__ */ jsx8("span", {}),
1336
+ onCollapse && /* @__PURE__ */ jsx8(
1337
+ "button",
1338
+ {
1339
+ type: "button",
1340
+ onClick: onCollapse,
1341
+ "aria-label": "Hide chats",
1342
+ className: cn("flex h-7 w-7 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-surface-container hover:text-foreground", MOTION_CONTROL),
1343
+ children: /* @__PURE__ */ jsx8(PanelLeftClose2, { className: "h-4 w-4" })
1344
+ }
1345
+ )
1346
+ ] }) : /* @__PURE__ */ jsx8(WorkspacePaneHeader, { children: /* @__PURE__ */ jsxs7("div", { className: "flex w-full items-center justify-between gap-2", children: [
1104
1347
  /* @__PURE__ */ jsxs7("div", { className: "flex min-w-0 items-center gap-2", children: [
1105
1348
  /* @__PURE__ */ jsx8("div", { className: "flex h-7 w-7 shrink-0 items-center justify-center rounded-[var(--radius-sm)] border border-[var(--border-accent)] bg-[var(--accent-surface-soft)] text-primary", children: /* @__PURE__ */ jsx8(MessageSquareText, { className: "h-3.5 w-3.5" }) }),
1106
1349
  /* @__PURE__ */ jsxs7("div", { className: "min-w-0", children: [
@@ -1122,129 +1365,69 @@ function SessionSidebar({
1122
1365
  )
1123
1366
  ] })
1124
1367
  ] }) }),
1125
- (enableSearch && items.length > 0 || filters.length > 0) && /* @__PURE__ */ jsxs7("div", { className: "shrink-0 border-b border-[var(--md3-outline-variant)] px-3 py-2.5", children: [
1126
- enableSearch && effectiveItems.length > 0 && /* @__PURE__ */ jsxs7("div", { className: "relative", children: [
1127
- /* @__PURE__ */ jsx8(Search2, { className: "pointer-events-none absolute left-2 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground" }),
1128
- /* @__PURE__ */ jsx8(
1129
- "input",
1130
- {
1131
- value: query,
1132
- onChange: (event) => setQuery(event.target.value),
1133
- placeholder: searchPlaceholder,
1134
- "aria-label": searchPlaceholder,
1135
- className: cn("h-8 w-full rounded-[var(--radius-sm)] border border-[var(--md3-outline-variant)] bg-surface-container-lowest pl-7 pr-2 text-sm text-foreground placeholder:text-muted-foreground transition-colors focus-visible:border-[var(--border-accent)] focus-visible:bg-transparent focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-[var(--border-accent)]", MOTION_CONTROL)
1136
- }
1137
- )
1138
- ] }),
1139
- filters.length > 0 && /* @__PURE__ */ jsx8("div", { className: cn("flex flex-wrap gap-1", enableSearch && items.length > 0 && "mt-2"), children: filters.map((filter) => {
1140
- const isSelected = activeFilterId === filter.id;
1141
- return /* @__PURE__ */ jsxs7(
1142
- "button",
1143
- {
1144
- type: "button",
1145
- onClick: () => setActiveFilterId(filter.id),
1146
- className: cn(
1147
- "inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-xs font-medium transition-colors",
1148
- MOTION_CONTROL,
1149
- isSelected ? "border-[var(--border-accent)] bg-[var(--accent-surface-soft)] text-primary" : "border-[var(--md3-outline-variant)] text-muted-foreground hover:text-foreground"
1150
- ),
1151
- children: [
1152
- /* @__PURE__ */ jsx8("span", { children: filter.label }),
1153
- /* @__PURE__ */ jsx8("span", { className: "text-[11px] opacity-60", children: filterCounts[filter.id] ?? 0 })
1154
- ]
1155
- },
1156
- filter.id
1157
- );
1158
- }) })
1159
- ] }),
1368
+ (enableSearch && items.length > 0 || filters.length > 0) && /* @__PURE__ */ jsxs7(
1369
+ "div",
1370
+ {
1371
+ className: cn(
1372
+ "shrink-0",
1373
+ quiet ? "px-3 pb-1" : "border-b border-[var(--md3-outline-variant)] px-3 py-2.5"
1374
+ ),
1375
+ children: [
1376
+ enableSearch && effectiveItems.length > 0 && /* @__PURE__ */ jsxs7("div", { className: "relative", children: [
1377
+ /* @__PURE__ */ jsx8(Search2, { className: "pointer-events-none absolute left-2 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground" }),
1378
+ /* @__PURE__ */ jsx8(
1379
+ "input",
1380
+ {
1381
+ value: query,
1382
+ onChange: (event) => setQuery(event.target.value),
1383
+ placeholder: searchPlaceholder,
1384
+ "aria-label": searchPlaceholder,
1385
+ className: cn(
1386
+ quiet ? "h-8 w-full rounded-md border-0 bg-transparent pl-7 pr-2 text-sm text-foreground placeholder:text-muted-foreground transition-colors focus-visible:bg-surface-container focus-visible:outline-none" : "h-8 w-full rounded-[var(--radius-sm)] border border-[var(--md3-outline-variant)] bg-surface-container-lowest pl-7 pr-2 text-sm text-foreground placeholder:text-muted-foreground transition-colors focus-visible:border-[var(--border-accent)] focus-visible:bg-transparent focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-[var(--border-accent)]",
1387
+ MOTION_CONTROL
1388
+ )
1389
+ }
1390
+ )
1391
+ ] }),
1392
+ filters.length > 0 && /* @__PURE__ */ jsx8("div", { className: cn("flex flex-wrap gap-1", enableSearch && items.length > 0 && "mt-2"), children: filters.map((filter) => {
1393
+ const isSelected = activeFilterId === filter.id;
1394
+ return /* @__PURE__ */ jsxs7(
1395
+ "button",
1396
+ {
1397
+ type: "button",
1398
+ onClick: () => setActiveFilterId(filter.id),
1399
+ className: cn(
1400
+ "inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-xs font-medium transition-colors",
1401
+ MOTION_CONTROL,
1402
+ isSelected ? "border-[var(--border-accent)] bg-[var(--accent-surface-soft)] text-primary" : "border-[var(--md3-outline-variant)] text-muted-foreground hover:text-foreground"
1403
+ ),
1404
+ children: [
1405
+ /* @__PURE__ */ jsx8("span", { children: filter.label }),
1406
+ /* @__PURE__ */ jsx8("span", { className: "text-[11px] opacity-60", children: filterCounts[filter.id] ?? 0 })
1407
+ ]
1408
+ },
1409
+ filter.id
1410
+ );
1411
+ }) })
1412
+ ]
1413
+ }
1414
+ ),
1160
1415
  /* @__PURE__ */ jsx8("nav", { "aria-label": "Sessions", className: "flex-1 overflow-y-auto px-1.5 py-1.5", children: visibleItems.length === 0 ? /* @__PURE__ */ jsxs7("div", { className: "flex flex-col items-center justify-center gap-2 px-4 py-10 text-center text-sm text-muted-foreground", children: [
1161
1416
  /* @__PURE__ */ jsx8(MessageSquareText, { className: "h-5 w-5 opacity-50", "aria-hidden": "true" }),
1162
1417
  /* @__PURE__ */ jsx8("p", { children: query.trim() ? `No sessions match "${query.trim()}".` : emptyMessage })
1163
- ] }) : /* @__PURE__ */ jsx8("ul", { className: "space-y-px", children: visibleItems.map((item, index) => {
1164
- const session = sessionsById.get(item.id) ?? null;
1165
- const isActive = currentItemId === item.id;
1166
- const status = session?.status ?? item.status;
1167
- const isWorking = status === "running";
1168
- const visibleBadges = [
1169
- ...item.isPinned ? [{ id: `${item.id}-pinned`, label: "Pinned", tone: "neutral" }] : [],
1170
- ...item.badges ?? []
1171
- ];
1172
- return /* @__PURE__ */ jsx8(
1173
- "li",
1418
+ ] }) : showGroupLabels ? groups.map((group, groupIndex) => /* @__PURE__ */ jsxs7("section", { "aria-label": group.label ?? void 0, children: [
1419
+ /* @__PURE__ */ jsx8(
1420
+ "div",
1174
1421
  {
1175
- className: "agent-arrive",
1176
- style: { "--stagger-index": index },
1177
- children: /* @__PURE__ */ jsxs7(
1178
- "div",
1179
- {
1180
- className: cn(
1181
- "group relative flex items-center gap-2 rounded-[var(--radius-sm)] px-2.5 py-1.5 transition-colors",
1182
- MOTION_CONTROL,
1183
- isActive ? "bg-accent font-medium text-foreground" : "text-muted-foreground hover:bg-accent/30"
1184
- ),
1185
- children: [
1186
- /* @__PURE__ */ jsxs7(
1187
- "button",
1188
- {
1189
- type: "button",
1190
- onClick: () => {
1191
- if (onSelectItem) {
1192
- onSelectItem(item);
1193
- return;
1194
- }
1195
- navigateToHref(item.href);
1196
- },
1197
- "aria-current": isActive ? "page" : void 0,
1198
- className: "min-w-0 flex flex-1 items-center gap-2 text-left",
1199
- children: [
1200
- /* @__PURE__ */ jsx8("span", { className: cn("h-1.5 w-1.5 shrink-0 rounded-full", statusDot(status)) }),
1201
- /* @__PURE__ */ jsxs7("div", { className: "min-w-0 flex-1", children: [
1202
- /* @__PURE__ */ jsx8(
1203
- "div",
1204
- {
1205
- className: cn(
1206
- "truncate text-sm font-medium",
1207
- isActive && "text-foreground",
1208
- isWorking && "agent-shimmer"
1209
- ),
1210
- ...isWorking ? { "data-motion": "essential" } : {},
1211
- children: item.title
1212
- }
1213
- ),
1214
- item.subtitle && /* @__PURE__ */ jsx8("div", { className: "truncate text-xs leading-tight text-muted-foreground", children: item.subtitle })
1215
- ] })
1216
- ]
1217
- }
1218
- ),
1219
- /* @__PURE__ */ jsxs7("div", { className: "flex shrink-0 items-center gap-1", children: [
1220
- visibleBadges.length > 0 && visibleBadges.slice(0, 1).map((badge) => /* @__PURE__ */ jsx8(
1221
- "span",
1222
- {
1223
- className: cn(
1224
- "rounded-full border px-1.5 py-px text-[8px] font-semibold uppercase",
1225
- badgeTone(badge.tone)
1226
- ),
1227
- children: badge.label
1228
- },
1229
- badge.id
1230
- )),
1231
- session?.isForeground && /* @__PURE__ */ jsx8("span", { className: "h-1.5 w-1.5 rounded-full bg-[var(--status-running)]", title: "Live" }),
1232
- renderItemActions ? /* @__PURE__ */ jsx8(
1233
- "div",
1234
- {
1235
- className: cn("opacity-0 transition-opacity group-hover:opacity-100", MOTION_CONTROL),
1236
- onClick: (event) => event.stopPropagation(),
1237
- children: renderItemActions(item, { session, isActive })
1238
- }
1239
- ) : null
1240
- ] })
1241
- ]
1242
- }
1243
- )
1244
- },
1245
- item.id
1246
- );
1247
- }) }) }),
1422
+ className: cn(
1423
+ "px-2 pb-1 text-[11px] font-medium uppercase tracking-wider text-muted-foreground",
1424
+ groupIndex === 0 ? "pt-1" : "pt-3"
1425
+ ),
1426
+ children: group.label
1427
+ }
1428
+ ),
1429
+ /* @__PURE__ */ jsx8("ul", { className: "space-y-px", children: group.items.map((item) => renderRow(item, rowIndex++)) })
1430
+ ] }, group.key)) : /* @__PURE__ */ jsx8("ul", { className: "space-y-px", children: visibleItems.map((item) => renderRow(item, rowIndex++)) }) }),
1248
1431
  activityMonitor && /* @__PURE__ */ jsx8("div", { className: "border-t border-[var(--md3-outline-variant)] px-2 py-1.5", children: activityMonitor }),
1249
1432
  links.length > 0 && /* @__PURE__ */ jsx8("nav", { "aria-label": "Workspace sections", className: "border-t border-[var(--md3-outline-variant)] px-1.5 py-1.5", children: /* @__PURE__ */ jsx8("div", { className: "space-y-px", children: links.map((link) => {
1250
1433
  const Icon = iconForLink(link.icon);
@@ -1434,6 +1617,13 @@ function artifactTabLabel(artifact) {
1434
1617
  if (artifact.kind === "file") return artifact.filename;
1435
1618
  return "Artifact";
1436
1619
  }
1620
+ function sortPinnedFirst(artifacts) {
1621
+ if (!artifacts.some((artifact) => artifact.pinned)) return artifacts;
1622
+ return [
1623
+ ...artifacts.filter((artifact) => artifact.pinned),
1624
+ ...artifacts.filter((artifact) => !artifact.pinned)
1625
+ ];
1626
+ }
1437
1627
  function ArtifactTabs({
1438
1628
  artifacts,
1439
1629
  activeArtifactId,
@@ -1442,7 +1632,7 @@ function ArtifactTabs({
1442
1632
  }) {
1443
1633
  if (artifacts.length === 0) return null;
1444
1634
  return /* @__PURE__ */ jsx10("div", { className: "flex items-center overflow-x-auto border-b border-[var(--md3-outline-variant)] bg-surface-container-high", children: artifacts.map((artifact) => {
1445
- const Icon = getArtifactTabIcon(artifact.kind);
1635
+ const Icon = artifact.icon ?? getArtifactTabIcon(artifact.kind);
1446
1636
  const isActive = artifact.id === activeArtifactId;
1447
1637
  return /* @__PURE__ */ jsxs9(
1448
1638
  "div",
@@ -1464,7 +1654,7 @@ function ArtifactTabs({
1464
1654
  ]
1465
1655
  }
1466
1656
  ),
1467
- onClose && /* @__PURE__ */ jsx10(
1657
+ onClose && !artifact.pinned && /* @__PURE__ */ jsx10(
1468
1658
  "button",
1469
1659
  {
1470
1660
  type: "button",
@@ -1551,6 +1741,9 @@ function regionHeader(sections, fallback) {
1551
1741
  return fallback;
1552
1742
  }
1553
1743
  switch (sections[0]?.key) {
1744
+ // The rail brings its own header row.
1745
+ case "rail":
1746
+ return void 0;
1554
1747
  case "directory":
1555
1748
  return /* @__PURE__ */ jsxs9("div", { className: "flex items-center gap-2 text-xs font-semibold uppercase tracking-[0.12em] text-muted-foreground", children: [
1556
1749
  /* @__PURE__ */ jsx10(FolderTree2, { className: "h-3.5 w-3.5" }),
@@ -1588,9 +1781,12 @@ function SandboxWorkbench({
1588
1781
  title = "Sandbox session",
1589
1782
  subtitle,
1590
1783
  status,
1784
+ centerHeader: centerHeaderProp,
1785
+ composer,
1786
+ rail,
1591
1787
  directory,
1592
1788
  session,
1593
- artifacts = [],
1789
+ artifacts: artifactsProp = [],
1594
1790
  activeArtifactId,
1595
1791
  onArtifactChange,
1596
1792
  onArtifactClose,
@@ -1599,6 +1795,7 @@ function SandboxWorkbench({
1599
1795
  emptyArtifactState,
1600
1796
  className
1601
1797
  }) {
1798
+ const artifacts = useMemo4(() => sortPinnedFirst(artifactsProp), [artifactsProp]);
1602
1799
  const [uncontrolledArtifactId, setUncontrolledArtifactId] = useState5(
1603
1800
  activeArtifactId ?? artifacts[0]?.id
1604
1801
  );
@@ -1621,7 +1818,7 @@ function SandboxWorkbench({
1621
1818
  }
1622
1819
  onArtifactChange?.(artifactId);
1623
1820
  };
1624
- const centerHeader = /* @__PURE__ */ jsxs9("div", { className: "flex min-w-0 items-start justify-between gap-4 rounded-[var(--radius-xl)] border border-[var(--md3-outline-variant)] bg-surface-container px-4 py-3.5", children: [
1821
+ const brandedHeader = /* @__PURE__ */ jsxs9("div", { className: "flex min-w-0 items-start justify-between gap-4 rounded-[var(--radius-xl)] border border-[var(--md3-outline-variant)] bg-surface-container px-4 py-3.5", children: [
1625
1822
  /* @__PURE__ */ jsxs9("div", { className: "min-w-0", children: [
1626
1823
  /* @__PURE__ */ jsx10("div", { className: "text-[10px] font-semibold uppercase tracking-[0.18em] text-primary", children: "Tangle Sandbox" }),
1627
1824
  /* @__PURE__ */ jsx10("div", { className: "truncate text-[17px] font-semibold tracking-[0.01em] text-foreground", children: title }),
@@ -1635,8 +1832,22 @@ function SandboxWorkbench({
1635
1832
  ] })
1636
1833
  ] })
1637
1834
  ] });
1835
+ const quiet = centerHeaderProp === null;
1836
+ const centerHeader = centerHeaderProp === void 0 ? brandedHeader : centerHeaderProp ?? void 0;
1638
1837
  const { composerControls, ...chatSession } = session;
1639
- const center = /* @__PURE__ */ jsx10(
1838
+ const transcript = /* @__PURE__ */ jsxs9("div", { className: cn("flex h-full min-h-0 flex-col", quiet && "bg-surface"), children: [
1839
+ /* @__PURE__ */ jsx10(
1840
+ ChatContainer,
1841
+ {
1842
+ ...chatSession,
1843
+ className: "min-h-0 flex-1",
1844
+ presentation: session.presentation ?? "timeline"
1845
+ }
1846
+ ),
1847
+ composerControls && /* @__PURE__ */ jsx10("div", { className: "shrink-0 border-t border-[var(--md3-outline-variant)] bg-surface-container-high px-3 py-2", children: composerControls }),
1848
+ composer && /* @__PURE__ */ jsx10("div", { className: "shrink-0", children: /* @__PURE__ */ jsx10("div", { className: "mx-auto w-full max-w-3xl px-3 pb-3", children: composer }) })
1849
+ ] });
1850
+ const center = quiet ? transcript : /* @__PURE__ */ jsx10(
1640
1851
  ArtifactPane2,
1641
1852
  {
1642
1853
  eyebrow: session.eyebrow ?? "Agent Session",
@@ -1646,17 +1857,7 @@ function SandboxWorkbench({
1646
1857
  headerActions: session.headerActions,
1647
1858
  className: "h-full",
1648
1859
  contentClassName: "bg-surface-container",
1649
- children: /* @__PURE__ */ jsxs9("div", { className: "flex h-full min-h-0 flex-col", children: [
1650
- /* @__PURE__ */ jsx10(
1651
- ChatContainer,
1652
- {
1653
- ...chatSession,
1654
- className: "min-h-0 flex-1",
1655
- presentation: session.presentation ?? "timeline"
1656
- }
1657
- ),
1658
- composerControls && /* @__PURE__ */ jsx10("div", { className: "shrink-0 border-t border-[var(--md3-outline-variant)] bg-surface-container-high px-3 py-2", children: composerControls })
1659
- ] })
1860
+ children: transcript
1660
1861
  }
1661
1862
  );
1662
1863
  const artifactPanel = artifacts.length > 0 ? /* @__PURE__ */ jsxs9("section", { className: "flex h-full min-h-0 flex-col bg-surface-container", children: [
@@ -1686,6 +1887,9 @@ function SandboxWorkbench({
1686
1887
  right: [],
1687
1888
  bottom: []
1688
1889
  };
1890
+ if (rail) {
1891
+ regionSections.left.push({ key: "rail", content: rail });
1892
+ }
1689
1893
  if (directory && directoryPlacement !== "hidden") {
1690
1894
  regionSections[directoryPlacement].push({
1691
1895
  key: "directory",
@@ -1707,6 +1911,7 @@ function SandboxWorkbench({
1707
1911
  const left = renderRegion(regionSections.left, "left");
1708
1912
  const right = renderRegion(regionSections.right, "right");
1709
1913
  const bottom = renderRegion(regionSections.bottom, "bottom");
1914
+ const railOnly = regionSections.left.length === 1 && regionSections.left[0]?.key === "rail";
1710
1915
  const genericPanelsHeader = /* @__PURE__ */ jsx10("span", { className: "text-xs font-semibold uppercase tracking-[0.12em] text-muted-foreground", children: "Workspace Panels" });
1711
1916
  return /* @__PURE__ */ jsx10(
1712
1917
  WorkspaceLayout,
@@ -1732,6 +1937,14 @@ function SandboxWorkbench({
1732
1937
  minRightWidth: layout?.minRightWidth,
1733
1938
  maxRightWidth: layout?.maxRightWidth,
1734
1939
  resizable: layout?.resizable,
1940
+ leftOpen: layout?.leftOpen,
1941
+ onLeftOpenChange: layout?.onLeftOpenChange,
1942
+ rightOpen: layout?.rightOpen,
1943
+ onRightOpenChange: layout?.onRightOpenChange,
1944
+ keyboardShortcuts: layout?.keyboardShortcuts,
1945
+ leftCollapsedControl: layout?.leftCollapsedControl,
1946
+ leftContentClassName: railOnly ? "py-0" : void 0,
1947
+ centerHeaderVisibility: quiet ? "auto" : "always",
1735
1948
  className: cn("p-3 lg:p-4", className)
1736
1949
  }
1737
1950
  );
@@ -2371,6 +2584,7 @@ export {
2371
2584
  StatusBar,
2372
2585
  TerminalPanel,
2373
2586
  RuntimePane,
2587
+ formatRelativeAge,
2374
2588
  SESSION_CREATED_EVENT,
2375
2589
  dispatchSessionCreated,
2376
2590
  useOptimisticSessionItems,