@tangle-network/sandbox-ui 0.28.0 → 0.29.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.
@@ -813,10 +813,69 @@ function RuntimePane({
813
813
  }
814
814
 
815
815
  // src/workspace/session-sidebar.tsx
816
- import { useCallback, useMemo as useMemo3, useRef as useRef3, useState as useState3 } from "react";
816
+ import { useCallback as useCallback2, useImperativeHandle, useMemo as useMemo3, useRef as useRef4, useState as useState4 } from "react";
817
817
  import { ArrowLeft, FolderTree, MessageSquareText, Plus, Search as Search2, Settings, Sparkles } from "lucide-react";
818
818
  import { useNavbarSessions } from "@tangle-network/ui/stores";
819
+
820
+ // src/workspace/session-optimistic.ts
821
+ import { useCallback, useEffect as useEffect3, useRef as useRef3, useState as useState3 } from "react";
822
+ var SESSION_CREATED_EVENT = "sandbox-thread-created";
823
+ function dispatchSessionCreated(detail, eventName = SESSION_CREATED_EVENT) {
824
+ if (typeof window === "undefined") return;
825
+ window.dispatchEvent(new CustomEvent(eventName, { detail }));
826
+ }
827
+ function toItem(detail) {
828
+ return { updatedAt: /* @__PURE__ */ new Date(), ...detail };
829
+ }
830
+ function useOptimisticSessionItems(items, options = {}) {
831
+ const { listenForCreate = true, eventName = SESSION_CREATED_EVENT } = options;
832
+ const [optimisticItems, setOptimisticItems] = useState3(items);
833
+ useEffect3(() => {
834
+ setOptimisticItems(items);
835
+ }, [items]);
836
+ const add = useCallback((detail) => {
837
+ const next = toItem(detail);
838
+ setOptimisticItems((prev) => [next, ...prev.filter((item) => item.id !== next.id)]);
839
+ }, []);
840
+ const update = useCallback((id, patch) => {
841
+ setOptimisticItems(
842
+ (prev) => prev.map((item) => item.id === id ? { ...item, ...patch } : item)
843
+ );
844
+ }, []);
845
+ const remove = useCallback((id) => {
846
+ setOptimisticItems((prev) => prev.filter((item) => item.id !== id));
847
+ }, []);
848
+ const reset = useCallback(() => setOptimisticItems(items), [items]);
849
+ const addRef = useRef3(add);
850
+ addRef.current = add;
851
+ useEffect3(() => {
852
+ if (!listenForCreate || typeof window === "undefined") return;
853
+ function onCreated(event) {
854
+ const detail = event.detail;
855
+ if (!detail?.id || !detail.title) return;
856
+ addRef.current(detail);
857
+ }
858
+ window.addEventListener(eventName, onCreated);
859
+ return () => window.removeEventListener(eventName, onCreated);
860
+ }, [listenForCreate, eventName]);
861
+ return {
862
+ items: optimisticItems,
863
+ controller: { add, update, remove, reset }
864
+ };
865
+ }
866
+
867
+ // src/workspace/session-sidebar.tsx
819
868
  import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
869
+ var NO_CONTROLLER = {
870
+ add: () => {
871
+ },
872
+ update: () => {
873
+ },
874
+ remove: () => {
875
+ },
876
+ reset: () => {
877
+ }
878
+ };
820
879
  function statusDot(status) {
821
880
  switch (status) {
822
881
  case "running":
@@ -888,24 +947,24 @@ function navigateToHref(href) {
888
947
  window.location.assign(href);
889
948
  }
890
949
  function useResizable(defaultWidth, min, max, onChange) {
891
- const [width, setWidth] = useState3(defaultWidth);
892
- const dragging = useRef3(false);
893
- const startX = useRef3(0);
894
- const startW = useRef3(0);
895
- const onPointerDown = useCallback((e) => {
950
+ const [width, setWidth] = useState4(defaultWidth);
951
+ const dragging = useRef4(false);
952
+ const startX = useRef4(0);
953
+ const startW = useRef4(0);
954
+ const onPointerDown = useCallback2((e) => {
896
955
  dragging.current = true;
897
956
  startX.current = e.clientX;
898
957
  startW.current = width;
899
958
  e.currentTarget.setPointerCapture(e.pointerId);
900
959
  }, [width]);
901
- const onPointerMove = useCallback((e) => {
960
+ const onPointerMove = useCallback2((e) => {
902
961
  if (!dragging.current) return;
903
962
  const delta = e.clientX - startX.current;
904
963
  const next = Math.min(max, Math.max(min, startW.current + delta));
905
964
  setWidth(next);
906
965
  onChange?.(next);
907
966
  }, [min, max, onChange]);
908
- const onPointerUp = useCallback(() => {
967
+ const onPointerUp = useCallback2(() => {
909
968
  dragging.current = false;
910
969
  }, []);
911
970
  return { width, onPointerDown, onPointerMove, onPointerUp };
@@ -933,18 +992,32 @@ function SessionSidebar({
933
992
  minWidth = 200,
934
993
  maxWidth = 400,
935
994
  onWidthChange,
936
- renderItemActions
995
+ renderItemActions,
996
+ optimistic = false,
997
+ optimisticRef
937
998
  }) {
938
- const [query, setQuery] = useState3("");
939
- const [activeFilterId, setActiveFilterId] = useState3(defaultFilterId ?? filters[0]?.id ?? "all");
999
+ const [query, setQuery] = useState4("");
1000
+ const [activeFilterId, setActiveFilterId] = useState4(defaultFilterId ?? filters[0]?.id ?? "all");
1001
+ const optimisticOptions = typeof optimistic === "object" ? optimistic : void 0;
1002
+ const optimisticState = useOptimisticSessionItems(
1003
+ items,
1004
+ optimistic === false ? { listenForCreate: false } : optimisticOptions
1005
+ );
1006
+ const optimisticEnabled = optimistic !== false;
1007
+ const effectiveItems = optimisticEnabled ? optimisticState.items : items;
1008
+ useImperativeHandle(
1009
+ optimisticRef,
1010
+ () => optimisticEnabled ? optimisticState.controller : NO_CONTROLLER,
1011
+ [optimisticEnabled, optimisticState.controller]
1012
+ );
940
1013
  const activeSessions = useNavbarSessions(projectId);
941
1014
  const sessionsById = useMemo3(
942
1015
  () => new Map(activeSessions.map((session) => [session.sessionId, session])),
943
1016
  [activeSessions]
944
1017
  );
945
1018
  const orderedItems = useMemo3(
946
- () => sortItems(items, sessionsById),
947
- [items, sessionsById]
1019
+ () => sortItems(effectiveItems, sessionsById),
1020
+ [effectiveItems, sessionsById]
948
1021
  );
949
1022
  const visibleItems = useMemo3(() => {
950
1023
  const normalizedQuery = query.trim().toLowerCase();
@@ -998,7 +1071,7 @@ function SessionSidebar({
998
1071
  ] })
999
1072
  ] }) }),
1000
1073
  (enableSearch && items.length > 0 || filters.length > 0) && /* @__PURE__ */ jsxs7("div", { className: "shrink-0 border-b border-border px-3 py-2.5", children: [
1001
- enableSearch && items.length > 0 && /* @__PURE__ */ jsxs7("div", { className: "relative", children: [
1074
+ enableSearch && effectiveItems.length > 0 && /* @__PURE__ */ jsxs7("div", { className: "relative", children: [
1002
1075
  /* @__PURE__ */ jsx7(Search2, { className: "pointer-events-none absolute left-2 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground" }),
1003
1076
  /* @__PURE__ */ jsx7(
1004
1077
  "input",
@@ -1136,6 +1209,20 @@ function SessionSidebar({
1136
1209
  );
1137
1210
  }
1138
1211
 
1212
+ // src/workspace/shell-revalidation.ts
1213
+ function createShellShouldRevalidate(options = {}) {
1214
+ const appPathPrefix = options.appPathPrefix ?? "/app";
1215
+ return function shouldRevalidate(args) {
1216
+ const { currentUrl, nextUrl, formMethod, defaultShouldRevalidate } = args;
1217
+ if (formMethod && formMethod !== "GET") return true;
1218
+ if (currentUrl.pathname !== nextUrl.pathname && currentUrl.pathname.startsWith(appPathPrefix) && nextUrl.pathname.startsWith(appPathPrefix)) {
1219
+ return false;
1220
+ }
1221
+ return defaultShouldRevalidate;
1222
+ };
1223
+ }
1224
+ var shellShouldRevalidate = createShellShouldRevalidate();
1225
+
1139
1226
  // src/workspace/session-activity-monitor.tsx
1140
1227
  import { Activity, AlertCircle as AlertCircle2, LoaderCircle, MessageSquareText as MessageSquareText2 } from "lucide-react";
1141
1228
  import {
@@ -1238,7 +1325,7 @@ function SessionActivityMonitor({
1238
1325
  }
1239
1326
 
1240
1327
  // src/workspace/sandbox-workbench.tsx
1241
- import { useEffect as useEffect3, useMemo as useMemo4, useState as useState4 } from "react";
1328
+ import { useEffect as useEffect4, useMemo as useMemo4, useState as useState5 } from "react";
1242
1329
  import {
1243
1330
  Bot,
1244
1331
  Boxes,
@@ -1438,10 +1525,10 @@ function SandboxWorkbench({
1438
1525
  emptyArtifactState,
1439
1526
  className
1440
1527
  }) {
1441
- const [uncontrolledArtifactId, setUncontrolledArtifactId] = useState4(
1528
+ const [uncontrolledArtifactId, setUncontrolledArtifactId] = useState5(
1442
1529
  activeArtifactId ?? artifacts[0]?.id
1443
1530
  );
1444
- useEffect3(() => {
1531
+ useEffect4(() => {
1445
1532
  if (activeArtifactId !== void 0) return;
1446
1533
  setUncontrolledArtifactId((current) => {
1447
1534
  if (artifacts.length === 0) return void 0;
@@ -1593,7 +1680,7 @@ function AgentWorkbench(props) {
1593
1680
  }
1594
1681
 
1595
1682
  // src/workspace/audit-results.tsx
1596
- import { useState as useState5 } from "react";
1683
+ import { useState as useState6 } from "react";
1597
1684
  import { CheckCircle as CheckCircle2, XCircle, ChevronRight, Shield } from "lucide-react";
1598
1685
  import { jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
1599
1686
  function AuditResults({ forms, crossFormChecks = [], overallScore, className }) {
@@ -1624,7 +1711,7 @@ function AuditResults({ forms, crossFormChecks = [], overallScore, className })
1624
1711
  ] });
1625
1712
  }
1626
1713
  function FormAuditCard({ form }) {
1627
- const [expanded, setExpanded] = useState5(form.failed > 0);
1714
+ const [expanded, setExpanded] = useState6(form.failed > 0);
1628
1715
  const allPassed = form.failed === 0 && form.found;
1629
1716
  return /* @__PURE__ */ jsxs10("div", { className: "rounded-[var(--radius-md)] border border-border overflow-hidden", children: [
1630
1717
  /* @__PURE__ */ jsxs10(
@@ -1762,7 +1849,7 @@ function TaskBoard({
1762
1849
  }
1763
1850
 
1764
1851
  // src/workspace/calendar-view.tsx
1765
- import { useState as useState6, useMemo as useMemo6 } from "react";
1852
+ import { useState as useState7, useMemo as useMemo6 } from "react";
1766
1853
  import { Fragment as Fragment3, jsx as jsx12, jsxs as jsxs12 } from "react/jsx-runtime";
1767
1854
  function toDateKey(d) {
1768
1855
  return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
@@ -1806,11 +1893,11 @@ function CalendarView({
1806
1893
  }) {
1807
1894
  const today = /* @__PURE__ */ new Date();
1808
1895
  const todayKey = toDateKey(today);
1809
- const [internalMonth, setInternalMonth] = useState6({
1896
+ const [internalMonth, setInternalMonth] = useState7({
1810
1897
  year: today.getFullYear(),
1811
1898
  month: today.getMonth()
1812
1899
  });
1813
- const [internalSelectedDay, setInternalSelectedDay] = useState6(
1900
+ const [internalSelectedDay, setInternalSelectedDay] = useState7(
1814
1901
  todayKey
1815
1902
  );
1816
1903
  const viewYear = controlledYear ?? internalMonth.year;
@@ -1983,7 +2070,7 @@ function CalendarView({
1983
2070
  }
1984
2071
 
1985
2072
  // src/workspace/approval-queue.tsx
1986
- import { useState as useState7, useMemo as useMemo7 } from "react";
2073
+ import { useState as useState8, useMemo as useMemo7 } from "react";
1987
2074
  import { jsx as jsx13, jsxs as jsxs13 } from "react/jsx-runtime";
1988
2075
  function ApprovalQueue({
1989
2076
  items,
@@ -1998,9 +2085,9 @@ function ApprovalQueue({
1998
2085
  emptyState,
1999
2086
  showResolved: controlledShowResolved
2000
2087
  }) {
2001
- const [rejectingId, setRejectingId] = useState7(null);
2002
- const [rejectReason, setRejectReason] = useState7("");
2003
- const [showResolved, setShowResolved] = useState7(controlledShowResolved ?? false);
2088
+ const [rejectingId, setRejectingId] = useState8(null);
2089
+ const [rejectReason, setRejectReason] = useState8("");
2090
+ const [showResolved, setShowResolved] = useState8(controlledShowResolved ?? false);
2004
2091
  const pending = useMemo7(
2005
2092
  () => items.filter((a) => a.status === "pending"),
2006
2093
  [items]
@@ -2209,7 +2296,12 @@ export {
2209
2296
  StatusBar,
2210
2297
  TerminalPanel,
2211
2298
  RuntimePane,
2299
+ SESSION_CREATED_EVENT,
2300
+ dispatchSessionCreated,
2301
+ useOptimisticSessionItems,
2212
2302
  SessionSidebar,
2303
+ createShellShouldRevalidate,
2304
+ shellShouldRevalidate,
2213
2305
  SessionActivityMonitor,
2214
2306
  SandboxWorkbench,
2215
2307
  AgentWorkbench,
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export { ArtifactPane, ArtifactPaneProps, Avatar, AvatarFallback, AvatarImage, Badge, BadgeProps, Button, ButtonProps, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, CodeBlock, CodeBlockProps, CopyButton, Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, DropZone, DropZoneProps, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, EmptyState, EmptyStateProps, InlineCode, InlineCodeProps, Input, InputProps, Label, Progress, SegmentedControl, SegmentedControlOption, SegmentedControlProps, Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue, SidebarDropZone, SidebarDropZoneProps, Skeleton, SkeletonCard, SkeletonTable, StatCard, StatCardProps, Switch, Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow, Tabs, TabsContent, TabsList, TabsTrigger, TerminalDisplay, TerminalCursor as TerminalDisplayCursor, TerminalInput as TerminalDisplayInput, TerminalLine as TerminalDisplayLine, Textarea, TextareaProps, ThemeToggle, Toast, ToastContainer, ToastProvider, UploadFile, UploadProgress, UploadProgressProps, badgeVariants, buttonVariants, useTheme, useToast } from '@tangle-network/ui/primitives';
2
2
  export { Logo, LogoProps, TangleKnot } from './primitives.js';
3
3
  export { CodeBlock as CodeBlockDisplay, Markdown, MarkdownProps } from '@tangle-network/ui/markdown';
4
- export { AgentWorkbench, ApprovalConfidenceStat, ApprovalItem, ApprovalQueue, ApprovalQueueProps, AuditCheck, AuditResults, AuditResultsProps, BannerType, CalendarEvent, CalendarView, CalendarViewProps, ContextBadge, DirectoryPane, DirectoryPaneProps, FormAudit, RuntimePane, RuntimePaneProps, SandboxWorkbench, SandboxWorkbenchArtifact, SandboxWorkbenchCustomArtifact, SandboxWorkbenchFileArtifact, SandboxWorkbenchLayoutOptions, SandboxWorkbenchMarkdownArtifact, SandboxWorkbenchOpenUIArtifact, SandboxWorkbenchProps, SandboxWorkbenchSessionProps, SessionActivityMonitor, SessionActivityMonitorProps, SessionSidebar, SessionSidebarBadge, SessionSidebarFilter, SessionSidebarItem, SessionSidebarLink, SessionSidebarProps, StatusBanner, StatusBannerProps, StatusBar, StatusBarProps, TaskBoard, TaskBoardColumn, TaskBoardItem, TaskBoardProps, TerminalLine, TerminalPanel, TerminalProps, WorkspaceLayout, WorkspaceLayoutProps } from './workspace.js';
4
+ export { AgentWorkbench, ApprovalConfidenceStat, ApprovalItem, ApprovalQueue, ApprovalQueueProps, AuditCheck, AuditResults, AuditResultsProps, BannerType, CalendarEvent, CalendarView, CalendarViewProps, ContextBadge, DirectoryPane, DirectoryPaneProps, FormAudit, RuntimePane, RuntimePaneProps, SESSION_CREATED_EVENT, SandboxWorkbench, SandboxWorkbenchArtifact, SandboxWorkbenchCustomArtifact, SandboxWorkbenchFileArtifact, SandboxWorkbenchLayoutOptions, SandboxWorkbenchMarkdownArtifact, SandboxWorkbenchOpenUIArtifact, SandboxWorkbenchProps, SandboxWorkbenchSessionProps, SessionActivityMonitor, SessionActivityMonitorProps, SessionCreatedDetail, SessionOptimisticController, SessionSidebar, SessionSidebarBadge, SessionSidebarFilter, SessionSidebarItem, SessionSidebarLink, SessionSidebarProps, ShellShouldRevalidateArgs, ShellShouldRevalidateOptions, StatusBanner, StatusBannerProps, StatusBar, StatusBarProps, TaskBoard, TaskBoardColumn, TaskBoardItem, TaskBoardProps, TerminalLine, TerminalPanel, TerminalProps, UseOptimisticSessionItemsOptions, WorkspaceLayout, WorkspaceLayoutProps, createShellShouldRevalidate, dispatchSessionCreated, shellShouldRevalidate, useOptimisticSessionItems } from './workspace.js';
5
5
  export { OpenUIAction, OpenUIActionsNode, OpenUIArtifactRenderer, OpenUIArtifactRendererProps, OpenUIBadgeNode, OpenUICardNode, OpenUICodeNode, OpenUIComponentNode, OpenUIGridNode, OpenUIHeadingNode, OpenUIKeyValueNode, OpenUIMarkdownNode, OpenUIPrimitive, OpenUISeparatorNode, OpenUIStackNode, OpenUIStatNode, OpenUITableNode, OpenUITextNode } from '@tangle-network/ui/openui';
6
6
  export { AgentTimeline, AgentTimelineArtifactItem, AgentTimelineCustomItem, AgentTimelineItem, AgentTimelineMessageItem, AgentTimelineProps, AgentTimelineStatusItem, AgentTimelineTone, AgentTimelineToolGroupItem, AgentTimelineToolItem, ChatContainer, ChatContainerProps, ChatInput, ChatInputProps, ChatMessage, ChatMessageProps, MessageList, MessageListProps, MessageRole, PendingFile, ThinkingIndicator, ThinkingIndicatorProps, UserMessage, UserMessageProps } from '@tangle-network/ui/chat';
7
7
  export { AgentSessionControls, AgentSessionControlsProps, AgentSessionHarnessControl, AgentSessionModelControl, AgentSessionReasoningControl, ArtifactAgentDock, ArtifactAgentDockProps, ArtifactAgentDockTransport, ArtifactDockMessage, ArtifactDockStreamEvent, ArtifactKind, ArtifactScope, DEFAULT_REASONING_LEVEL_OPTIONS, HARNESS_MODEL_POLICIES, ReasoningLevel, ReasoningLevelOption, ReasoningLevelPicker, ReasoningLevelPickerProps, createFetchTransport, isModelCompatibleWithHarness, modelProvider, snapHarnessToModel, snapModelToHarness } from './chat.js';
package/dist/index.js CHANGED
@@ -120,6 +120,7 @@ import {
120
120
  CalendarView,
121
121
  DirectoryPane,
122
122
  RuntimePane,
123
+ SESSION_CREATED_EVENT,
123
124
  SandboxWorkbench,
124
125
  SessionActivityMonitor,
125
126
  SessionSidebar,
@@ -127,8 +128,12 @@ import {
127
128
  StatusBar,
128
129
  TaskBoard,
129
130
  TerminalPanel,
130
- WorkspaceLayout
131
- } from "./chunk-I3IVCGND.js";
131
+ WorkspaceLayout,
132
+ createShellShouldRevalidate,
133
+ dispatchSessionCreated,
134
+ shellShouldRevalidate,
135
+ useOptimisticSessionItems
136
+ } from "./chunk-AUFV7766.js";
132
137
  import {
133
138
  OpenUIArtifactRenderer
134
139
  } from "./chunk-AZ3AWMTM.js";
@@ -442,6 +447,7 @@ export {
442
447
  RichFileTree,
443
448
  RunGroup,
444
449
  RuntimePane,
450
+ SESSION_CREATED_EVENT,
445
451
  SIDEBAR_MOBILE_WIDTH,
446
452
  SIDEBAR_PANEL_WIDTH,
447
453
  SIDEBAR_RAIL_WIDTH,
@@ -534,8 +540,10 @@ export {
534
540
  copyText,
535
541
  createAuthFetcher,
536
542
  createFetchTransport,
543
+ createShellShouldRevalidate,
537
544
  dedupeModels,
538
545
  disconnectSession,
546
+ dispatchSessionCreated,
539
547
  filterFileTree,
540
548
  formatBytes,
541
549
  formatContext,
@@ -569,6 +577,7 @@ export {
569
577
  setActiveSessionError,
570
578
  setActiveSessionRunning,
571
579
  setForegroundActiveSession,
580
+ shellShouldRevalidate,
572
581
  snapHarnessToModel,
573
582
  snapModelToHarness,
574
583
  timeAgo,
@@ -588,6 +597,7 @@ export {
588
597
  useHasBackgroundRunningSessions,
589
598
  useLiveTime,
590
599
  useNavbarSessions,
600
+ useOptimisticSessionItems,
591
601
  useProjectActivity,
592
602
  useProjectSessions,
593
603
  usePtySession,
@@ -1,5 +1,5 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import { ReactNode } from 'react';
2
+ import { ReactNode, Ref } from 'react';
3
3
  export { ArtifactPane, ArtifactPaneProps } from '@tangle-network/ui/primitives';
4
4
  import { FileNode, FileTreeVisibilityOptions, FileTabData } from '@tangle-network/ui/files';
5
5
  import { SessionProjectKey, ActiveSessionStatus, ActiveSessionRecord } from '@tangle-network/ui/stores';
@@ -160,6 +160,67 @@ interface RuntimePaneProps {
160
160
  */
161
161
  declare function RuntimePane({ title, subtitle, statusBanner, statusBar, content, terminal, audit, inspector, footer, className, }: RuntimePaneProps): react_jsx_runtime.JSX.Element;
162
162
 
163
+ /**
164
+ * Optimistic session/thread list: reflect a just-created / renamed / deleted
165
+ * item in the sidebar INSTANTLY, before the server loader round-trips.
166
+ *
167
+ * The shell loader is the source of truth and reconciles in the background
168
+ * (typically the app calls `useRevalidator().revalidate()` after its mutation
169
+ * fetch settles). Until that lands, a local mirror of the loader items carries
170
+ * the optimistic edit so a delete / rename / new-thread does not feel broken
171
+ * for the duration of a D1 round-trip.
172
+ *
173
+ * Two ways to drive it, usable together:
174
+ * 1. A window `CustomEvent` (default name {@link SESSION_CREATED_EVENT}) — the
175
+ * surface that creates a thread (often a different route from the sidebar's
176
+ * route) dispatches it; the sidebar prepends the item with no prop wiring.
177
+ * 2. The imperative {@link SessionOptimisticController} returned here — the
178
+ * sidebar's own action handlers (rename / delete / pin) call it directly.
179
+ */
180
+ /** Default window-event name for "a session/thread was just created". */
181
+ declare const SESSION_CREATED_EVENT = "sandbox-thread-created";
182
+ /**
183
+ * Payload of a {@link SESSION_CREATED_EVENT}. Only `id` + `title` are required;
184
+ * everything else mirrors {@link SessionSidebarItem}. `updatedAt` defaults to
185
+ * now so the new item sorts to the top.
186
+ */
187
+ interface SessionCreatedDetail extends Partial<Omit<SessionSidebarItem, "id" | "title">> {
188
+ id: string;
189
+ title: string;
190
+ }
191
+ /** Dispatch the shared "session created" event so any mounted sidebar updates. */
192
+ declare function dispatchSessionCreated(detail: SessionCreatedDetail, eventName?: string): void;
193
+ /** Imperative handle for driving optimistic edits from a mutation handler. */
194
+ interface SessionOptimisticController {
195
+ /** Prepend (or move to top) an item — call before the create fetch settles. */
196
+ add: (item: SessionCreatedDetail) => void;
197
+ /** Patch an existing item in place — e.g. a rename or category change. */
198
+ update: (id: string, patch: Partial<SessionSidebarItem>) => void;
199
+ /** Drop an item — call before the delete fetch settles. */
200
+ remove: (id: string) => void;
201
+ /** Discard all optimistic edits and snap back to the loader items. */
202
+ reset: () => void;
203
+ }
204
+ interface UseOptimisticSessionItemsOptions {
205
+ /**
206
+ * Listen for the shared window create-event and prepend its item. Set `false`
207
+ * to drive optimistic state purely through the imperative controller.
208
+ * Defaults to `true`.
209
+ */
210
+ listenForCreate?: boolean;
211
+ /** Override the window event name to listen for. */
212
+ eventName?: string;
213
+ }
214
+ /**
215
+ * Mirror `items` with optimistic add/update/remove on top. The mirror re-syncs
216
+ * whenever `items` changes (the loader is authoritative), so an optimistic edit
217
+ * is naturally superseded once the server result arrives.
218
+ */
219
+ declare function useOptimisticSessionItems(items: SessionSidebarItem[], options?: UseOptimisticSessionItemsOptions): {
220
+ items: SessionSidebarItem[];
221
+ controller: SessionOptimisticController;
222
+ };
223
+
163
224
  interface SessionSidebarItem {
164
225
  id: string;
165
226
  title: string;
@@ -218,8 +279,97 @@ interface SessionSidebarProps {
218
279
  session: ActiveSessionRecord | null;
219
280
  isActive: boolean;
220
281
  }) => ReactNode;
282
+ /**
283
+ * Opt into optimistic item updates: the sidebar keeps a local mirror of
284
+ * `items` so a just-created / renamed / deleted thread shows instantly,
285
+ * before the loader round-trips. Pass `true` for defaults, or an options
286
+ * object to tune the window create-event. Off by default — existing callers
287
+ * render `items` verbatim and are unaffected.
288
+ */
289
+ optimistic?: boolean | UseOptimisticSessionItemsOptions;
290
+ /**
291
+ * Ref to the imperative optimistic controller (`add` / `update` / `remove` /
292
+ * `reset`). Drive optimistic edits from the consumer's own rename/delete/pin
293
+ * handlers. Only populated when `optimistic` is enabled.
294
+ */
295
+ optimisticRef?: Ref<SessionOptimisticController | null>;
221
296
  }
222
- declare function SessionSidebar({ title, subtitle, projectId, items, currentItemId, createLabel, onCreate, onSelectItem, onSelectLink, links, className, emptyMessage, searchPlaceholder, enableSearch, activityMonitor, filters, defaultFilterId, resizable, defaultWidth, minWidth, maxWidth, onWidthChange, renderItemActions, }: SessionSidebarProps): react_jsx_runtime.JSX.Element;
297
+ declare function SessionSidebar({ title, subtitle, projectId, items, currentItemId, createLabel, onCreate, onSelectItem, onSelectLink, links, className, emptyMessage, searchPlaceholder, enableSearch, activityMonitor, filters, defaultFilterId, resizable, defaultWidth, minWidth, maxWidth, onWidthChange, renderItemActions, optimistic, optimisticRef, }: SessionSidebarProps): react_jsx_runtime.JSX.Element;
298
+
299
+ /**
300
+ * Snappy in-app navigation: a shared `shouldRevalidate` rule for the agent-app
301
+ * shell route(s) that own the workspace-list / sidebar loader.
302
+ *
303
+ * The shell loader (user + workspace list, sidebar state) runs on every
304
+ * navigation, but its data does not change just by moving between in-app pages.
305
+ * Refetching it on every internal GET nav adds a loader round-trip to each
306
+ * click — on Cloudflare Workers + D1 (HTTP-proxied) that is a visible stall.
307
+ *
308
+ * This helper encodes the rule that makes nav feel instant WITHOUT stranding
309
+ * freshly-mutated data:
310
+ *
311
+ * - Non-GET navigation (form submit / action) → always revalidate.
312
+ * - Internal app→app navigation that CHANGES the path → skip revalidation.
313
+ * - Imperative `useRevalidator().revalidate()` (which targets the SAME url,
314
+ * so `currentUrl.pathname === nextUrl.pathname`) → revalidate. Creating a
315
+ * project / thread typically goes through a raw `fetch`, so a same-url
316
+ * revalidate is the ONLY way the new row reaches the shell loader; the
317
+ * same-path carve-out below is what keeps that working. Suppressing it is
318
+ * the project-switcher regression — do not "simplify" it away.
319
+ * - Anything else (cross-path-prefix nav, entering/leaving the app) →
320
+ * defer to `defaultShouldRevalidate`.
321
+ *
322
+ * Framework-agnostic by design: sandbox-ui does not depend on react-router, so
323
+ * this takes a plain args object. The shape is a structural subset of
324
+ * react-router's `ShouldRevalidateFunctionArgs`, so an app can wire it as a
325
+ * route export with no adapter:
326
+ *
327
+ * ```ts
328
+ * // app/routes/app.tsx
329
+ * import { shellShouldRevalidate } from "@tangle-network/sandbox-ui/workspace";
330
+ * export const shouldRevalidate = shellShouldRevalidate;
331
+ * ```
332
+ *
333
+ * `shouldRevalidate` MUST be a per-app react-router route export — it cannot be
334
+ * exported FROM a library on the app's behalf — so the "out of the box" win is
335
+ * the one-line re-export above instead of every app re-deriving the rule (and
336
+ * re-introducing the same-url bug).
337
+ */
338
+ /**
339
+ * The subset of react-router's `ShouldRevalidateFunctionArgs` this rule reads.
340
+ * Kept structural so callers can pass the full react-router args object as-is.
341
+ */
342
+ interface ShellShouldRevalidateArgs {
343
+ currentUrl: URL;
344
+ nextUrl: URL;
345
+ formMethod?: string;
346
+ defaultShouldRevalidate: boolean;
347
+ }
348
+ interface ShellShouldRevalidateOptions {
349
+ /**
350
+ * Path prefix that marks "inside the app shell". Internal navigation between
351
+ * two paths under this prefix skips the shell-loader refetch. Defaults to
352
+ * `"/app"`.
353
+ */
354
+ appPathPrefix?: string;
355
+ }
356
+ /**
357
+ * Build a `shouldRevalidate` function for the shell route under `appPathPrefix`.
358
+ * Use this when the app mounts under a non-default prefix; otherwise prefer the
359
+ * ready-made {@link shellShouldRevalidate} export.
360
+ */
361
+ declare function createShellShouldRevalidate(options?: ShellShouldRevalidateOptions): (args: ShellShouldRevalidateArgs) => boolean;
362
+ /**
363
+ * Ready-made shell `shouldRevalidate` for apps mounted under `/app`. Wire it as
364
+ * a one-line route export:
365
+ *
366
+ * ```ts
367
+ * export const shouldRevalidate = shellShouldRevalidate;
368
+ * ```
369
+ *
370
+ * For a non-`/app` prefix, use {@link createShellShouldRevalidate}.
371
+ */
372
+ declare const shellShouldRevalidate: (args: ShellShouldRevalidateArgs) => boolean;
223
373
 
224
374
  interface SessionActivityMonitorProps {
225
375
  className?: string;
@@ -478,4 +628,4 @@ interface ApprovalQueueProps {
478
628
  */
479
629
  declare function ApprovalQueue({ items, className, onApprove, onReject, renderItemDetail, renderTypeBadge, renderStats, canResolve, header, emptyState, showResolved: controlledShowResolved, }: ApprovalQueueProps): react_jsx_runtime.JSX.Element;
480
630
 
481
- export { AgentWorkbench, type ApprovalConfidenceStat, type ApprovalItem, ApprovalQueue, type ApprovalQueueProps, type AuditCheck, AuditResults, type AuditResultsProps, type BannerType, type CalendarEvent, CalendarView, type CalendarViewProps, type ContextBadge, DirectoryPane, type DirectoryPaneProps, type FormAudit, RuntimePane, type RuntimePaneProps, SandboxWorkbench, type SandboxWorkbenchArtifact, type SandboxWorkbenchCustomArtifact, type SandboxWorkbenchFileArtifact, type SandboxWorkbenchLayoutOptions, type SandboxWorkbenchMarkdownArtifact, type SandboxWorkbenchOpenUIArtifact, type SandboxWorkbenchProps, type SandboxWorkbenchSessionProps, SessionActivityMonitor, type SessionActivityMonitorProps, SessionSidebar, type SessionSidebarBadge, type SessionSidebarFilter, type SessionSidebarItem, type SessionSidebarLink, type SessionSidebarProps, StatusBanner, type StatusBannerProps, StatusBar, type StatusBarProps, TaskBoard, type TaskBoardColumn, type TaskBoardItem, type TaskBoardProps, type TerminalLine, TerminalPanel, type TerminalProps, WorkspaceLayout, type WorkspaceLayoutProps };
631
+ export { AgentWorkbench, type ApprovalConfidenceStat, type ApprovalItem, ApprovalQueue, type ApprovalQueueProps, type AuditCheck, AuditResults, type AuditResultsProps, type BannerType, type CalendarEvent, CalendarView, type CalendarViewProps, type ContextBadge, DirectoryPane, type DirectoryPaneProps, type FormAudit, RuntimePane, type RuntimePaneProps, SESSION_CREATED_EVENT, SandboxWorkbench, type SandboxWorkbenchArtifact, type SandboxWorkbenchCustomArtifact, type SandboxWorkbenchFileArtifact, type SandboxWorkbenchLayoutOptions, type SandboxWorkbenchMarkdownArtifact, type SandboxWorkbenchOpenUIArtifact, type SandboxWorkbenchProps, type SandboxWorkbenchSessionProps, SessionActivityMonitor, type SessionActivityMonitorProps, type SessionCreatedDetail, type SessionOptimisticController, SessionSidebar, type SessionSidebarBadge, type SessionSidebarFilter, type SessionSidebarItem, type SessionSidebarLink, type SessionSidebarProps, type ShellShouldRevalidateArgs, type ShellShouldRevalidateOptions, StatusBanner, type StatusBannerProps, StatusBar, type StatusBarProps, TaskBoard, type TaskBoardColumn, type TaskBoardItem, type TaskBoardProps, type TerminalLine, TerminalPanel, type TerminalProps, type UseOptimisticSessionItemsOptions, WorkspaceLayout, type WorkspaceLayoutProps, createShellShouldRevalidate, dispatchSessionCreated, shellShouldRevalidate, useOptimisticSessionItems };
package/dist/workspace.js CHANGED
@@ -6,6 +6,7 @@ import {
6
6
  CalendarView,
7
7
  DirectoryPane,
8
8
  RuntimePane,
9
+ SESSION_CREATED_EVENT,
9
10
  SandboxWorkbench,
10
11
  SessionActivityMonitor,
11
12
  SessionSidebar,
@@ -13,8 +14,12 @@ import {
13
14
  StatusBar,
14
15
  TaskBoard,
15
16
  TerminalPanel,
16
- WorkspaceLayout
17
- } from "./chunk-I3IVCGND.js";
17
+ WorkspaceLayout,
18
+ createShellShouldRevalidate,
19
+ dispatchSessionCreated,
20
+ shellShouldRevalidate,
21
+ useOptimisticSessionItems
22
+ } from "./chunk-AUFV7766.js";
18
23
  import "./chunk-EI44GEQ5.js";
19
24
  export {
20
25
  AgentWorkbench,
@@ -24,6 +29,7 @@ export {
24
29
  CalendarView,
25
30
  DirectoryPane,
26
31
  RuntimePane,
32
+ SESSION_CREATED_EVENT,
27
33
  SandboxWorkbench,
28
34
  SessionActivityMonitor,
29
35
  SessionSidebar,
@@ -31,5 +37,9 @@ export {
31
37
  StatusBar,
32
38
  TaskBoard,
33
39
  TerminalPanel,
34
- WorkspaceLayout
40
+ WorkspaceLayout,
41
+ createShellShouldRevalidate,
42
+ dispatchSessionCreated,
43
+ shellShouldRevalidate,
44
+ useOptimisticSessionItems
35
45
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/sandbox-ui",
3
- "version": "0.28.0",
3
+ "version": "0.29.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",