@tangle-network/sandbox-ui 0.107.0 → 0.108.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.
@@ -104,6 +104,33 @@ async function fetchJson(url, token, init) {
104
104
  if (!res.ok) throw await responseError(res, "Request failed");
105
105
  return res.json();
106
106
  }
107
+ function readDegradation(raw) {
108
+ if (!raw || typeof raw !== "object") return null;
109
+ const record = raw;
110
+ if (typeof record.message !== "string") return null;
111
+ return {
112
+ kind: "hub-connections",
113
+ reason: typeof record.reason === "string" ? record.reason : "unknown",
114
+ message: record.message,
115
+ requestShape: typeof record.requestShape === "string" ? record.requestShape : "unknown"
116
+ };
117
+ }
118
+ function bumpHubFrameEpoch(epochs, sessionId) {
119
+ epochs.set(sessionId, (epochs.get(sessionId) ?? 0) + 1);
120
+ }
121
+ function withDegradation(prev, sessionId, value) {
122
+ const current = prev.get(sessionId) ?? null;
123
+ if (value === null) {
124
+ if (current === null) return prev;
125
+ const next = new Map(prev);
126
+ next.delete(sessionId);
127
+ return next;
128
+ }
129
+ if (current && current.reason === value.reason && current.message === value.message && current.requestShape === value.requestShape) {
130
+ return prev;
131
+ }
132
+ return new Map(prev).set(sessionId, value);
133
+ }
107
134
  function useSessionStream({
108
135
  apiUrl,
109
136
  token,
@@ -119,11 +146,16 @@ function useSessionStream({
119
146
  const [isStreaming, setIsStreaming] = useState(false);
120
147
  const [error, setError] = useState(null);
121
148
  const [connected, setConnected] = useState(false);
149
+ const [degradationBySession, setDegradationBySession] = useState(() => /* @__PURE__ */ new Map());
150
+ const degradation = degradationBySession.get(sessionId) ?? null;
122
151
  const abortRef = useRef(null);
123
152
  const reconnectTimerRef = useRef(null);
124
153
  const activeAssistantMessageIdsRef = useRef(/* @__PURE__ */ new Set());
125
154
  const completedExecutionIdsRef = useRef(/* @__PURE__ */ new Set());
126
155
  const lastEventIdRef = useRef("");
156
+ const hubFrameEpochRef = useRef(/* @__PURE__ */ new Map());
157
+ const seedGenerationRef = useRef(/* @__PURE__ */ new Map());
158
+ const executionScopedStreamRef = useRef(false);
127
159
  const insertionCounterRef = useRef(0);
128
160
  const echoCounterRef = useRef(0);
129
161
  const pendingEchoesRef = useRef(/* @__PURE__ */ new Map());
@@ -169,6 +201,29 @@ function useSessionStream({
169
201
  setError(msg);
170
202
  }
171
203
  }, [apiUrl, token, sessionId]);
204
+ const seedDegradation = useCallback(async () => {
205
+ if (!token || !sessionId || !apiUrl) return;
206
+ const epochAtRequest = hubFrameEpochRef.current.get(sessionId) ?? 0;
207
+ const generation = (seedGenerationRef.current.get(sessionId) ?? 0) + 1;
208
+ seedGenerationRef.current.set(sessionId, generation);
209
+ let seeded;
210
+ try {
211
+ const url = `${apiUrl}/session/sessions/${encodeURIComponent(sessionId)}`;
212
+ const status = await fetchJson(url, token);
213
+ const field = status.hubConnectionsDegraded;
214
+ if (field === void 0 || field === null) {
215
+ seeded = null;
216
+ } else {
217
+ seeded = readDegradation(field);
218
+ if (!seeded) return;
219
+ }
220
+ } catch {
221
+ return;
222
+ }
223
+ if ((hubFrameEpochRef.current.get(sessionId) ?? 0) !== epochAtRequest) return;
224
+ if (seedGenerationRef.current.get(sessionId) !== generation) return;
225
+ setDegradationBySession((prev) => withDegradation(prev, sessionId, seeded));
226
+ }, [apiUrl, token, sessionId]);
172
227
  const connectSSE = useCallback(async () => {
173
228
  if (!token || !sessionId || !apiUrl || !enabled) return;
174
229
  if (reconnectTimerRef.current) {
@@ -181,6 +236,7 @@ function useSessionStream({
181
236
  try {
182
237
  const eventQuery = new URLSearchParams({ sessionId });
183
238
  const activeExecutionIds = [...activeAssistantMessageIdsRef.current];
239
+ executionScopedStreamRef.current = activeExecutionIds.length > 0;
184
240
  if (activeExecutionIds.length === 1) {
185
241
  eventQuery.set("executionId", activeExecutionIds[0]);
186
242
  } else if (activeExecutionIds.length > 1) {
@@ -198,6 +254,7 @@ function useSessionStream({
198
254
  if (!res.ok) throw await responseError(res, "Live response connection failed");
199
255
  setConnected(true);
200
256
  setError(null);
257
+ void seedDegradation();
201
258
  const reader = res.body?.getReader();
202
259
  if (!reader) throw new Error("No response body");
203
260
  const decoder = new TextDecoder();
@@ -387,7 +444,22 @@ function useSessionStream({
387
444
  const executionId = props.executionId ?? props.executionID ?? "";
388
445
  const stillStreaming = retireExecution(executionId);
389
446
  setIsStreaming(stillStreaming);
390
- if (!stillStreaming) refetch();
447
+ if (!stillStreaming) {
448
+ refetch();
449
+ if (executionScopedStreamRef.current) void seedDegradation();
450
+ }
451
+ } else if (type === "hub.connections.degraded") {
452
+ const framedSessionId = typeof props.sessionId === "string" ? props.sessionId : null;
453
+ if (!framedSessionId) return;
454
+ const value = readDegradation(props);
455
+ if (!value) return;
456
+ bumpHubFrameEpoch(hubFrameEpochRef.current, framedSessionId);
457
+ setDegradationBySession((prev) => withDegradation(prev, framedSessionId, value));
458
+ } else if (type === "hub.connections.restored") {
459
+ const framedSessionId = typeof props.sessionId === "string" ? props.sessionId : null;
460
+ if (!framedSessionId) return;
461
+ bumpHubFrameEpoch(hubFrameEpochRef.current, framedSessionId);
462
+ setDegradationBySession((prev) => withDegradation(prev, framedSessionId, null));
391
463
  } else if (type === "session.error") {
392
464
  const executionId = props.executionId ?? props.executionID ?? "";
393
465
  const stillStreaming = retireExecution(executionId);
@@ -395,7 +467,7 @@ function useSessionStream({
395
467
  setError(formatError(readErrorDetails(props), "Agent response failed"));
396
468
  if (!stillStreaming) refetch();
397
469
  }
398
- }, [refetch]);
470
+ }, [refetch, seedDegradation]);
399
471
  handleSSEEventRef.current = handleSSEEvent;
400
472
  const send = useCallback(async (text, options) => {
401
473
  if (!token || !sessionId || !apiUrl || !enabled) {
@@ -505,7 +577,17 @@ function useSessionStream({
505
577
  setConnected(false);
506
578
  };
507
579
  }, [enabled, token, sessionId, refetch, connectSSE]);
508
- return { messages, partMap, isStreaming, send, abort, refetch, error, connected };
580
+ return {
581
+ messages,
582
+ partMap,
583
+ isStreaming,
584
+ send,
585
+ abort,
586
+ refetch,
587
+ error,
588
+ connected,
589
+ degradation
590
+ };
509
591
  }
510
592
 
511
593
  // src/hooks/use-sidecar-auth.ts
@@ -619,12 +619,24 @@ function DirectoryPane({
619
619
  import { Binary, ShieldCheck, TerminalSquare } from "lucide-react";
620
620
 
621
621
  // src/workspace/status-banner.tsx
622
- import { Loader2, AlertCircle, CheckCircle, Wifi } from "lucide-react";
622
+ import { Loader2, AlertCircle, AlertTriangle, CheckCircle, Wifi } from "lucide-react";
623
623
  import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
624
624
  var BANNER_STYLES = {
625
625
  provisioning: { bg: "bg-primary/5", border: "border-primary/20", text: "text-primary", icon: Loader2 },
626
626
  connecting: { bg: "bg-[var(--code-number)]/5", border: "border-[var(--code-number)]/20", text: "text-[var(--code-number)]", icon: Wifi },
627
627
  error: { bg: "bg-[var(--code-error)]/5", border: "border-[var(--code-error)]/20", text: "text-[var(--code-error)]", icon: AlertCircle },
628
+ // Degraded, not broken: the thing still works, with less than was asked for.
629
+ // `error` would overstate it and `info` would understate it, and a notice
630
+ // styled as neither gets read as neither. Uses the brand's theme-reactive
631
+ // `--surface-warning-*` triple rather than a `warning` color utility —
632
+ // `warning` is not registered in this package's `@theme`, so `bg-warning`
633
+ // and friends emit no rule at all.
634
+ warning: {
635
+ bg: "bg-[var(--surface-warning-bg)]",
636
+ border: "border-[var(--surface-warning-border)]",
637
+ text: "text-[var(--surface-warning-text)]",
638
+ icon: AlertTriangle
639
+ },
628
640
  success: { bg: "bg-[var(--code-success)]/5", border: "border-[var(--code-success)]/20", text: "text-[var(--code-success)]", icon: CheckCircle },
629
641
  info: { bg: "bg-surface-container-high", border: "border-[var(--md3-outline-variant)]", text: "text-muted-foreground", icon: AlertCircle }
630
642
  };
@@ -632,6 +644,7 @@ function StatusBanner({ type, message, detail, onDismiss, className }) {
632
644
  const style = BANNER_STYLES[type];
633
645
  const Icon = style.icon;
634
646
  const isAnimated = type === "provisioning" || type === "connecting";
647
+ const role = type === "error" ? "alert" : "status";
635
648
  return /* @__PURE__ */ jsxs3(
636
649
  "div",
637
650
  {
@@ -642,15 +655,17 @@ function StatusBanner({ type, message, detail, onDismiss, className }) {
642
655
  className
643
656
  ),
644
657
  children: [
645
- /* @__PURE__ */ jsx4(
646
- Icon,
647
- {
648
- className: cn("h-4 w-4 shrink-0", style.text, isAnimated && "animate-spin"),
649
- ...isAnimated ? { "data-motion": "essential" } : {}
650
- }
651
- ),
652
- /* @__PURE__ */ jsx4("span", { className: "font-medium text-foreground", children: message }),
653
- detail && /* @__PURE__ */ jsx4("span", { className: "font-mono text-xs text-muted-foreground tabular-nums", children: detail }),
658
+ /* @__PURE__ */ jsxs3("div", { role, className: "flex min-w-0 items-center gap-2.5", children: [
659
+ /* @__PURE__ */ jsx4(
660
+ Icon,
661
+ {
662
+ className: cn("h-4 w-4 shrink-0", style.text, isAnimated && "animate-spin"),
663
+ ...isAnimated ? { "data-motion": "essential" } : {}
664
+ }
665
+ ),
666
+ /* @__PURE__ */ jsx4("span", { className: "font-medium text-foreground", children: message }),
667
+ detail && /* @__PURE__ */ jsx4("span", { className: "font-mono text-xs text-muted-foreground tabular-nums", children: detail })
668
+ ] }),
654
669
  onDismiss && /* @__PURE__ */ jsx4(
655
670
  "button",
656
671
  {
package/dist/hooks.d.ts CHANGED
@@ -2,7 +2,7 @@ export { AgentStreamEvent, AppendUserMessageOptions, ApplySdkEventOptions, AuthU
2
2
  import { AgentInteractiveSessionControlClaim } from '@tangle-network/agent-interface';
3
3
  import * as _tanstack_react_query from '@tanstack/react-query';
4
4
  import { Session } from './types.js';
5
- export { e as SESSION_STREAM_RECONNECT_DELAY_MS, f as SendMessageOptions, S as SessionInfo, g as SessionStreamError, a as SidecarAuth, U as UseSessionStreamOptions, b as UseSessionStreamResult, c as UseSidecarAuthOptions, u as useSessionStream, d as useSidecarAuth } from './sdk-hooks-Cjt_Wq7g.js';
5
+ export { f as SESSION_STREAM_RECONNECT_DELAY_MS, g as SendMessageOptions, S as SessionDegradation, a as SessionInfo, h as SessionStreamError, b as SidecarAuth, U as UseSessionStreamOptions, c as UseSessionStreamResult, d as UseSidecarAuthOptions, u as useSessionStream, e as useSidecarAuth } from './sdk-hooks-Djkjq4cp.js';
6
6
  import '@tangle-network/ui/types';
7
7
  import '@tangle-network/ui/stores';
8
8
  import '@tangle-network/ui/sdk-hooks';
package/dist/hooks.js CHANGED
@@ -23,7 +23,7 @@ import {
23
23
  SessionStreamError,
24
24
  useSessionStream,
25
25
  useSidecarAuth
26
- } from "./chunk-3GX5BSD4.js";
26
+ } from "./chunk-72UF2H5H.js";
27
27
  import {
28
28
  usePtySession
29
29
  } from "./chunk-F7LNWZQR.js";
package/dist/index.d.ts CHANGED
@@ -18,7 +18,7 @@ export { AuthHeader, AuthHeaderProps, GitHubLoginButton, GitHubLoginButtonProps,
18
18
  export { TangleLoginButton, TangleLoginButtonProps } from './auth.js';
19
19
  export { AgentStreamEvent, AppendUserMessageOptions, ApplySdkEventOptions, AuthUser, AutomationStreamEvent, BeginAssistantMessageOptions, BotStreamEvent, CompleteAssistantMessageOptions, RealtimeSessionOptions, RealtimeSessionRegistry, RealtimeSessionRegistryProps, RealtimeSessionState, RealtimeSessionTarget, SSEEvent, SdkSessionAttachment, SdkSessionEvent, SdkSessionSeed, TaskStreamEvent, TerminalStreamEvent, UseAuthOptions, UseAuthResult, UseRunGroupsOptions, UseSSEStreamOptions, UseSSEStreamResult, UseSdkSessionOptions, UseSdkSessionReturn, UseToolCallStreamReturn, createAuthFetcher, useApiKey, useAuth, useAutoScroll, useDropdownMenu, useLiveTime, useRealtimeSession, useRunCollapseState, useRunGroups, useSSEStream, useSdkSession, useToolCallStream } from '@tangle-network/ui/hooks';
20
20
  export { SandboxMetrics, SidecarMetricsPayload, UsePtySessionOptions, UsePtySessionReturn, UseSandboxMetricsOptions, UseSandboxMetricsResult, useCreateSession, useDeleteSession, usePtySession, useRenameSession, useSandboxMetrics, useSessions } from './hooks.js';
21
- export { S as SessionInfo, a as SidecarAuth, U as UseSessionStreamOptions, b as UseSessionStreamResult, c as UseSidecarAuthOptions, u as useSessionStream, d as useSidecarAuth } from './sdk-hooks-Cjt_Wq7g.js';
21
+ export { S as SessionDegradation, a as SessionInfo, b as SidecarAuth, U as UseSessionStreamOptions, c as UseSessionStreamResult, d as UseSidecarAuthOptions, u as useSessionStream, e as useSidecarAuth } from './sdk-hooks-Djkjq4cp.js';
22
22
  export { ActiveProjectActivity, ActiveSessionActivityOptions, ActiveSessionConnectionOptions, ActiveSessionConnectionState, ActiveSessionReconnectState, ActiveSessionRecord, ActiveSessionStatus, ActiveSessionTransportMode, ActiveSessionsState, RegisterActiveSessionOptions, SessionProjectKey, activeSessionsAtom, addMessage, addParts, bumpActiveSessionActivity, clearChat, getActiveSession, getAllActiveSessions, getAllProjectActivity, getSessionsByActivity, getSessionsForNavbar, getSessionsForProject, getTotalRunningSessionCount, hasBackgroundRunningSessions, isStreamingAtom, messagesAtom, partMapAtom, registerActiveSession, resetActiveSessions, setActiveSessionAttention, setActiveSessionConnection, setActiveSessionError, setActiveSessionRunning, setForegroundActiveSession, unregisterActiveSession, updateActiveSessionMeta, updatePart, useActiveSession, useActiveSessions, useActiveSessionsState, useHasBackgroundRunningSessions, useNavbarSessions, useProjectActivity, useProjectSessions, useSessionsByActivity, useTotalRunningSessions } from '@tangle-network/ui/stores';
23
23
  export { ChatSession, connectSession, disconnectSession, sessionAtom } from './stores.js';
24
24
  export { AgentBranding, CustomToolRenderer, DisplayVariant, FinalTextPart, GroupedMessage, MessageRun, MessageUser, ReasoningPart, Run, RunStats, SessionMessage, SessionPart, TextPart, ToolCategory, ToolDisplayMetadata, ToolPart, ToolState, ToolStatus, ToolTime } from '@tangle-network/ui/types';
package/dist/index.js CHANGED
@@ -21,7 +21,7 @@ import {
21
21
  import {
22
22
  useSessionStream,
23
23
  useSidecarAuth
24
- } from "./chunk-3GX5BSD4.js";
24
+ } from "./chunk-72UF2H5H.js";
25
25
  import {
26
26
  activeSessionsAtom,
27
27
  addMessage,
@@ -132,7 +132,7 @@ import {
132
132
  dispatchSessionCreated,
133
133
  shellShouldRevalidate,
134
134
  useOptimisticSessionItems
135
- } from "./chunk-OI7HEWLZ.js";
135
+ } from "./chunk-D5VXCWUG.js";
136
136
  import {
137
137
  BRAND_PRISM_THEME,
138
138
  CodeSurface,
@@ -38,6 +38,31 @@ interface SendMessageOptions {
38
38
  * selected harness/model actually supports (a capability filter), never a hard-coded subset. */
39
39
  reasoningEffort?: ReasoningEffort;
40
40
  }
41
+ /**
42
+ * A sandbox-level condition that degrades the session without ending it.
43
+ *
44
+ * Distinct from `error`, which reports a turn that failed: the session here is
45
+ * running and will answer, it is just missing something the caller asked for.
46
+ * Reported separately so it is not cleared by the next successful turn — the
47
+ * condition outlives the turn that revealed it.
48
+ */
49
+ interface SessionDegradation {
50
+ /** Which capability is degraded. A union so this can grow without a break. */
51
+ readonly kind: 'hub-connections';
52
+ /** Machine-readable cause. */
53
+ readonly reason: string;
54
+ /** Operator/user-facing sentence, authored by the sidecar. */
55
+ readonly message: string;
56
+ /**
57
+ * What the session asked for, NOT how much it lost.
58
+ *
59
+ * There is deliberately no count of missing integrations: the sidecar reaches
60
+ * this state only for an attach-all request, and the call that would
61
+ * enumerate the tenant's connections is the one that failed authentication.
62
+ * Any number rendered here would be fabricated.
63
+ */
64
+ readonly requestShape: string;
65
+ }
41
66
  interface UseSessionStreamResult {
42
67
  /** All messages in the session (fetched + streaming). */
43
68
  messages: SessionMessage[];
@@ -55,6 +80,21 @@ interface UseSessionStreamResult {
55
80
  error: string | null;
56
81
  /** Whether the SSE connection is active. */
57
82
  connected: boolean;
83
+ /**
84
+ * The degradation currently reported for THIS session, or `null`.
85
+ *
86
+ * Today the only one is a Hub credential the sandbox could not present, which
87
+ * leaves the agent with an EMPTY toolbelt while the session otherwise runs
88
+ * normally. Without surfacing it, that state is indistinguishable to the user
89
+ * from having connected no integrations at all — so a caller that renders a
90
+ * transcript should render this too.
91
+ *
92
+ * Seeded from session status rather than derived from the live stream alone:
93
+ * the sidecar's bus never replays, so a reload — or attaching a beat after
94
+ * spawn — would otherwise show a normal-looking session with no tools and no
95
+ * explanation. Live frames update it, and a recovered credential clears it.
96
+ */
97
+ degradation: SessionDegradation | null;
58
98
  }
59
99
  declare class SessionStreamError extends Error {
60
100
  readonly code: string;
@@ -102,4 +142,4 @@ interface SidecarAuth {
102
142
  */
103
143
  declare function useSidecarAuth({ resourceId, apiUrl, signMessage }: UseSidecarAuthOptions): SidecarAuth;
104
144
 
105
- export { type SessionInfo as S, type UseSessionStreamOptions as U, type SidecarAuth as a, type UseSessionStreamResult as b, type UseSidecarAuthOptions as c, useSidecarAuth as d, SESSION_STREAM_RECONNECT_DELAY_MS as e, type SendMessageOptions as f, SessionStreamError as g, useSessionStream as u };
145
+ export { type SessionDegradation as S, type UseSessionStreamOptions as U, type SessionInfo as a, type SidecarAuth as b, type UseSessionStreamResult as c, type UseSidecarAuthOptions as d, useSidecarAuth as e, SESSION_STREAM_RECONNECT_DELAY_MS as f, type SendMessageOptions as g, SessionStreamError as h, useSessionStream as u };
@@ -1,4 +1,4 @@
1
1
  export { AgentStreamEvent, AppendUserMessageOptions, ApplySdkEventOptions, AutomationStreamEvent, BeginAssistantMessageOptions, BotStreamEvent, CompleteAssistantMessageOptions, ConnectionState, RealtimeSessionOptions, RealtimeSessionRegistry, RealtimeSessionRegistryProps, RealtimeSessionState, RealtimeSessionTarget, SSEEvent, SdkSessionAttachment, SdkSessionEvent, SdkSessionSeed, TaskStreamEvent, TerminalStreamEvent, UseRunGroupsOptions, UseSSEStreamOptions, UseSSEStreamResult, UseSdkSessionOptions, UseSdkSessionReturn, UseToolCallStreamReturn, useAutoScroll, useDropdownMenu, useRealtimeSession, useRunCollapseState, useRunGroups, useSSEStream, useSdkSession, useToolCallStream } from '@tangle-network/ui/sdk-hooks';
2
- export { S as SessionInfo, a as SidecarAuth, U as UseSessionStreamOptions, b as UseSessionStreamResult, c as UseSidecarAuthOptions, u as useSessionStream, d as useSidecarAuth } from './sdk-hooks-Cjt_Wq7g.js';
2
+ export { a as SessionInfo, b as SidecarAuth, U as UseSessionStreamOptions, c as UseSessionStreamResult, d as UseSidecarAuthOptions, u as useSessionStream, e as useSidecarAuth } from './sdk-hooks-Djkjq4cp.js';
3
3
  import '@tangle-network/agent-interface';
4
4
  import '@tangle-network/ui/types';
package/dist/sdk-hooks.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  useSessionStream,
3
3
  useSidecarAuth
4
- } from "./chunk-3GX5BSD4.js";
4
+ } from "./chunk-72UF2H5H.js";
5
5
  import "./chunk-CZ7CSFO4.js";
6
6
 
7
7
  // src/sdk-hooks.ts
@@ -96,7 +96,7 @@ declare function DirectoryPane({ root, selectedPath, onSelect, onRefresh, onUplo
96
96
  /**
97
97
  * StatusBanner — full-width notification banner for connection/provisioning states.
98
98
  */
99
- type BannerType = "provisioning" | "connecting" | "error" | "success" | "info";
99
+ type BannerType = "provisioning" | "connecting" | "error" | "warning" | "success" | "info";
100
100
  interface StatusBannerProps {
101
101
  type: BannerType;
102
102
  message: string;
package/dist/workspace.js CHANGED
@@ -20,7 +20,7 @@ import {
20
20
  dispatchSessionCreated,
21
21
  shellShouldRevalidate,
22
22
  useOptimisticSessionItems
23
- } from "./chunk-OI7HEWLZ.js";
23
+ } from "./chunk-D5VXCWUG.js";
24
24
  import "./chunk-BGVEHENC.js";
25
25
  import "./chunk-EI44GEQ5.js";
26
26
  import "./chunk-CZ7CSFO4.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/sandbox-ui",
3
- "version": "0.107.0",
3
+ "version": "0.108.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",