@tangle-network/sandbox-ui 0.42.0 → 0.44.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.
@@ -1,3 +1,6 @@
1
+ import {
2
+ ProviderIcon
3
+ } from "./chunk-H7GMPMPB.js";
1
4
  import {
2
5
  buildWorkflowGraph
3
6
  } from "./chunk-SUC6ZJZW.js";
@@ -75,7 +78,18 @@ function WorkflowNode({ data }) {
75
78
  children: d.subtitle
76
79
  }
77
80
  ),
78
- d.provider && /* @__PURE__ */ jsx("span", { className: "mt-1.5 inline-block rounded bg-background-tertiary px-1.5 py-0.5 text-[10px] text-text-muted", children: providerLabel(d.provider) }),
81
+ d.provider && /* @__PURE__ */ jsxs("span", { className: "mt-1.5 inline-flex items-center gap-1 rounded bg-background-tertiary px-1.5 py-0.5 text-[10px] text-text-muted", children: [
82
+ /* @__PURE__ */ jsx(
83
+ ProviderIcon,
84
+ {
85
+ id: d.provider,
86
+ displayName: providerLabel(d.provider),
87
+ size: 12,
88
+ className: "rounded-[2px]"
89
+ }
90
+ ),
91
+ providerLabel(d.provider)
92
+ ] }),
79
93
  /* @__PURE__ */ jsx(
80
94
  Handle,
81
95
  {
@@ -38,6 +38,9 @@ interface ReasoningEventData {
38
38
  interface ToolCallEventData {
39
39
  callId: string;
40
40
  name: string;
41
+ /** The parsed arguments the agent invoked the tool with. Lets a renderer show
42
+ * exactly what was called. Omitted by servers predating the field. */
43
+ args?: Record<string, unknown>;
41
44
  }
42
45
  interface ToolResultEventData {
43
46
  callId: string;
@@ -84,6 +87,9 @@ interface UsageEventData {
84
87
  completionTokens: number | null;
85
88
  costUsd: number | null;
86
89
  balanceUsd: number | null;
90
+ /** Wall-clock duration of the turn in milliseconds, when the server measures
91
+ * it. Drives the renderer's tokens/sec figure. Omitted by older servers. */
92
+ durationMs?: number | null;
87
93
  /** True when a completed turn was replayed from storage (no charge). */
88
94
  replayed?: boolean;
89
95
  }
@@ -151,11 +157,12 @@ interface ChatMessage {
151
157
  id: string;
152
158
  role: ChatRole;
153
159
  text: string;
154
- /** Present only on `tool` messages — the activity's tool name, state, and (once
155
- * finished) its outcome. */
160
+ /** Present only on `tool` messages — the activity's tool name, the arguments it
161
+ * was called with, its state, and (once finished) its outcome. */
156
162
  tool?: {
157
163
  name: string;
158
164
  status: ToolActivityStatus;
165
+ args?: Record<string, unknown>;
159
166
  outcome?: ToolOutcome;
160
167
  };
161
168
  }
@@ -176,6 +183,13 @@ interface PendingProposal {
176
183
  interface UsageInfo {
177
184
  costUsd: number | null;
178
185
  balanceUsd: number | null;
186
+ /** Token counts + wall-clock duration for the settled turn, when the server
187
+ * reports them — drive per-message tokens/sec + cost in a renderer. Optional
188
+ * so a consumer constructing this on the prior `{ costUsd, balanceUsd,
189
+ * replayed }` shape stays valid; a renderer treats a missing value as null. */
190
+ promptTokens?: number | null;
191
+ completionTokens?: number | null;
192
+ durationMs?: number | null;
179
193
  replayed: boolean;
180
194
  }
181
195
  /**
@@ -202,6 +216,10 @@ interface AssistantTranscriptView {
202
216
  * (drives a "thinking" affordance). */
203
217
  isThinking: boolean;
204
218
  pendingProposals: PendingProposal[];
219
+ /** Cost/tokens/duration for the most recently settled turn, or null before any
220
+ * turn settles. Optional so a host predating the field stays a valid consumer;
221
+ * a renderer treats a missing value the same as null. */
222
+ usage?: UsageInfo | null;
205
223
  /** The panel's bound proposal card for a pending proposal — render it where the
206
224
  * confirm/cancel UI should appear. */
207
225
  renderProposal: (proposal: PendingProposal) => ReactNode;
@@ -303,6 +321,13 @@ interface AssistantClient {
303
321
  fetchThreadHistory(threadId: string, signal?: AbortSignal): Promise<ThreadHistoryResult>;
304
322
  streamChat(req: ChatRequest, onEvent: (event: AssistantStreamEvent) => void, signal: AbortSignal): Promise<void>;
305
323
  confirmProposal(proposalId: string): Promise<ConfirmResult>;
324
+ /** Delete a thread and its server-side turns/proposals. Resolves `{ ok }`; a
325
+ * 404 (already gone) is treated as success so a double-delete is harmless.
326
+ * Optional so a host with no delete endpoint stays a valid client — the panel
327
+ * hides the delete affordance when it's absent (see `useAssistantThreads`). */
328
+ deleteThread?(threadId: string): Promise<{
329
+ ok: boolean;
330
+ }>;
306
331
  }
307
332
  /**
308
333
  * Build an assistant client bound to one host's transport. The returned methods
@@ -450,6 +475,15 @@ interface AssistantThreads {
450
475
  /** Load (or reload) the thread list. Must be called to populate `threads` —
451
476
  * the hook never fetches on mount (the panel calls this when history opens). */
452
477
  refresh: () => void;
478
+ /** Delete a thread. Optimistically drops it from the list (within the current
479
+ * owner scope); on failure the list is reloaded to restore the true state. A
480
+ * no-op resolving `{ ok: false }` when the client has no `deleteThread`. */
481
+ remove: (threadId: string) => Promise<{
482
+ ok: boolean;
483
+ }>;
484
+ /** Whether the configured client supports deletion — drives whether a host
485
+ * shows the delete affordance. */
486
+ canRemove: boolean;
453
487
  }
454
488
  declare function useAssistantThreads(userId: string | null): AssistantThreads;
455
489
 
package/dist/assistant.js CHANGED
@@ -1,3 +1,6 @@
1
+ import {
2
+ ProviderIcon
3
+ } from "./chunk-H7GMPMPB.js";
1
4
  import {
2
5
  providerLabel
3
6
  } from "./chunk-7JLB7UZ3.js";
@@ -443,6 +446,18 @@ function createAssistantClient(config) {
443
446
  ok: false,
444
447
  error: body?.error?.message ?? "The action could not be completed"
445
448
  };
449
+ },
450
+ async deleteThread(threadId) {
451
+ try {
452
+ const res = await fetch(url(`/threads/${encodeURIComponent(threadId)}`), {
453
+ method: "DELETE",
454
+ headers: authHeaders(),
455
+ credentials
456
+ });
457
+ return { ok: res.ok || res.status === 404 };
458
+ } catch {
459
+ return { ok: false };
460
+ }
446
461
  }
447
462
  };
448
463
  }
@@ -845,7 +860,11 @@ function applyStreamEvent(state, event) {
845
860
  id: chipId,
846
861
  role: "tool",
847
862
  text: "",
848
- tool: { name: event.data.name, status: "running" }
863
+ tool: {
864
+ name: event.data.name,
865
+ status: "running",
866
+ args: event.data.args
867
+ }
849
868
  };
850
869
  return {
851
870
  ...state,
@@ -865,7 +884,14 @@ function applyStreamEvent(state, event) {
865
884
  (m) => m.id === chipId ? {
866
885
  ...m,
867
886
  text: errText,
868
- tool: { name: event.data.name, status, outcome }
887
+ // Preserve the args the matching tool_call recorded — the
888
+ // result event doesn't carry them.
889
+ tool: {
890
+ name: event.data.name,
891
+ status,
892
+ args: m.tool?.args,
893
+ outcome
894
+ }
869
895
  } : m
870
896
  )
871
897
  };
@@ -893,6 +919,9 @@ function applyStreamEvent(state, event) {
893
919
  usage: {
894
920
  costUsd: event.data.costUsd,
895
921
  balanceUsd: event.data.balanceUsd,
922
+ promptTokens: event.data.promptTokens,
923
+ completionTokens: event.data.completionTokens,
924
+ durationMs: event.data.durationMs ?? null,
896
925
  replayed: event.data.replayed ?? false
897
926
  }
898
927
  };
@@ -1406,6 +1435,7 @@ function useAssistantThreads(userId) {
1406
1435
  const clientRef = useRef2(client);
1407
1436
  clientRef.current = client;
1408
1437
  const abortRef = useRef2(null);
1438
+ const pendingDeletesRef = useRef2(/* @__PURE__ */ new Set());
1409
1439
  const [state, setState] = useState2(() => ({
1410
1440
  threads: [],
1411
1441
  loading: false,
@@ -1440,7 +1470,11 @@ function useAssistantThreads(userId) {
1440
1470
  if (!isCurrent()) return;
1441
1471
  setState((s) => ({
1442
1472
  // null = transient failure: keep the prior list, just drop the spinner.
1443
- threads: result ?? s.threads,
1473
+ // Drop any in-flight/finished deletions so a stale fetch can't resurrect
1474
+ // a row we already removed.
1475
+ threads: (result ?? s.threads).filter(
1476
+ (t) => !pendingDeletesRef.current.has(t.id)
1477
+ ),
1444
1478
  loading: false,
1445
1479
  loaded: true,
1446
1480
  ownerUserId: requestedUserId,
@@ -1452,13 +1486,45 @@ function useAssistantThreads(userId) {
1452
1486
  }
1453
1487
  });
1454
1488
  }, []);
1455
- useEffect3(() => () => abortRef.current?.abort(), [userId, client]);
1489
+ const remove = useCallback2(
1490
+ async (threadId) => {
1491
+ const requestedClient = clientRef.current;
1492
+ const requestedUserId = userRef.current;
1493
+ if (!requestedClient.deleteThread) return { ok: false };
1494
+ pendingDeletesRef.current.add(threadId);
1495
+ setState(
1496
+ (s) => s.ownerClient === requestedClient && s.ownerUserId === requestedUserId ? { ...s, threads: s.threads.filter((t) => t.id !== threadId) } : s
1497
+ );
1498
+ let res;
1499
+ try {
1500
+ res = await requestedClient.deleteThread(threadId);
1501
+ } catch {
1502
+ res = { ok: false };
1503
+ }
1504
+ if (!res.ok) {
1505
+ pendingDeletesRef.current.delete(threadId);
1506
+ if (userRef.current === requestedUserId && clientRef.current === requestedClient) {
1507
+ refresh();
1508
+ }
1509
+ }
1510
+ return res;
1511
+ },
1512
+ [refresh]
1513
+ );
1514
+ useEffect3(() => {
1515
+ return () => abortRef.current?.abort();
1516
+ }, [userId, client]);
1517
+ useEffect3(() => {
1518
+ return () => pendingDeletesRef.current.clear();
1519
+ }, []);
1456
1520
  const stale = state.ownerUserId !== userId || state.ownerClient !== client;
1457
1521
  return {
1458
1522
  threads: stale ? [] : state.threads,
1459
1523
  loading: stale ? false : state.loading,
1460
1524
  loaded: stale ? false : state.loaded,
1461
- refresh
1525
+ refresh,
1526
+ remove,
1527
+ canRemove: typeof client.deleteThread === "function"
1462
1528
  };
1463
1529
  }
1464
1530
 
@@ -1466,13 +1532,13 @@ function useAssistantThreads(userId) {
1466
1532
  import { MessageSquare } from "lucide-react";
1467
1533
  import {
1468
1534
  useEffect as useEffect5,
1469
- useRef as useRef5
1535
+ useRef as useRef6
1470
1536
  } from "react";
1471
1537
 
1472
1538
  // src/assistant/AssistantPanel.tsx
1473
- import { AgentTimeline, ChatInput } from "@tangle-network/ui/chat";
1474
- import { History, Minus, Plus, RotateCcw, X } from "lucide-react";
1475
- import { useState as useState5 } from "react";
1539
+ import { AgentTimeline, ChatInput, ThinkingIndicator } from "@tangle-network/ui/chat";
1540
+ import { History, MessageSquarePlus, Minus, Plus, Trash2, X } from "lucide-react";
1541
+ import { useRef as useRef4, useState as useState5 } from "react";
1476
1542
 
1477
1543
  // src/assistant/build-timeline.tsx
1478
1544
  import { jsx as jsx2 } from "react/jsx-runtime";
@@ -1607,7 +1673,7 @@ function ProposalCard({
1607
1673
  )
1608
1674
  ] })
1609
1675
  ] }),
1610
- showGraph && tab === "graph" ? /* @__PURE__ */ jsx3("div", { className: "mt-1 h-48 overflow-hidden rounded border border-border", children: renderGraph?.(view.preview.content) }) : /* @__PURE__ */ jsx3("pre", { className: "mt-1 max-h-48 overflow-auto rounded border border-border bg-muted/50 p-2 text-xs", children: /* @__PURE__ */ jsx3("code", { children: view.preview.content }) })
1676
+ showGraph && tab === "graph" ? /* @__PURE__ */ jsx3("div", { className: "mt-1 h-64 overflow-hidden rounded border border-border", children: renderGraph?.(view.preview.content) }) : /* @__PURE__ */ jsx3("pre", { className: "mt-1 max-h-48 overflow-auto rounded border border-border bg-muted/50 p-2 text-xs", children: /* @__PURE__ */ jsx3("code", { children: view.preview.content }) })
1611
1677
  ] }),
1612
1678
  proposal.requirements && proposal.requirements.length > 0 && /* @__PURE__ */ jsxs("div", { className: "mt-3 rounded border border-border p-2", children: [
1613
1679
  /* @__PURE__ */ jsx3("p", { className: "text-muted-foreground text-xs", children: "Integrations" }),
@@ -1674,16 +1740,33 @@ function RequirementRow({
1674
1740
  const canConnect = !req.connected && req.connectUrl !== null;
1675
1741
  const target = req.connectUrl ?? "/app/integrations";
1676
1742
  return /* @__PURE__ */ jsxs("li", { className: "flex items-center justify-between gap-2 text-xs", children: [
1677
- /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-2", children: [
1743
+ /* @__PURE__ */ jsxs("span", { className: "flex min-w-0 items-center gap-2", children: [
1678
1744
  /* @__PURE__ */ jsx3(
1679
- "span",
1745
+ ProviderIcon,
1680
1746
  {
1681
- "aria-hidden": "true",
1682
- className: `h-2 w-2 rounded-full ${req.connected ? "bg-primary" : "border border-muted-foreground"}`
1747
+ id: req.provider,
1748
+ displayName: label,
1749
+ size: 16,
1750
+ className: "rounded"
1683
1751
  }
1684
1752
  ),
1685
- /* @__PURE__ */ jsx3("span", { className: "text-foreground", children: kindLabel }),
1686
- /* @__PURE__ */ jsx3("span", { className: "text-muted-foreground", children: statusText })
1753
+ /* @__PURE__ */ jsx3("span", { className: "truncate text-foreground", children: kindLabel }),
1754
+ /* @__PURE__ */ jsxs("span", { className: "flex shrink-0 items-center gap-1", children: [
1755
+ /* @__PURE__ */ jsx3(
1756
+ "span",
1757
+ {
1758
+ "aria-hidden": "true",
1759
+ className: `h-1.5 w-1.5 rounded-full ${req.connected ? "bg-primary" : "border border-muted-foreground"}`
1760
+ }
1761
+ ),
1762
+ /* @__PURE__ */ jsx3(
1763
+ "span",
1764
+ {
1765
+ className: req.connected ? "text-primary" : "text-muted-foreground",
1766
+ children: statusText
1767
+ }
1768
+ )
1769
+ ] })
1687
1770
  ] }),
1688
1771
  canConnect && /* @__PURE__ */ jsxs(
1689
1772
  "button",
@@ -1841,10 +1924,15 @@ function AssistantPanel({
1841
1924
  const font = useFontScale();
1842
1925
  const [historyOpen, setHistoryOpen] = useState5(false);
1843
1926
  const { state } = chat;
1927
+ const chatRef = useRef4(chat);
1928
+ chatRef.current = chat;
1844
1929
  const effectiveBalance = state.usage?.balanceUsd ?? balanceUsd;
1845
1930
  const errorView = state.error ? presentError(state.error.code, state.error.message) : null;
1846
1931
  const low = isLowBalance(effectiveBalance) && !errorView;
1847
1932
  const streaming = state.status === "streaming";
1933
+ const firstUserText = state.messages.find((m) => m.role === "user")?.text.trim();
1934
+ const titleChars = firstUserText ? Array.from(firstUserText) : [];
1935
+ const conversationTitle = firstUserText ? titleChars.length > 60 ? `${titleChars.slice(0, 60).join("")}\u2026` : firstUserText : null;
1848
1936
  const renderProposal = (proposal) => /* @__PURE__ */ jsx4(
1849
1937
  ProposalCard,
1850
1938
  {
@@ -1864,20 +1952,42 @@ function AssistantPanel({
1864
1952
  return next;
1865
1953
  });
1866
1954
  };
1955
+ const deleteThread = async (threadId) => {
1956
+ const pre = chatRef.current.state;
1957
+ if (pre.threadId === threadId && pre.status !== "idle") return;
1958
+ if (!window.confirm("Delete this conversation? This can't be undone.")) {
1959
+ return;
1960
+ }
1961
+ const res = await threads.remove(threadId);
1962
+ const live = chatRef.current.state;
1963
+ if (res.ok && live.threadId === threadId && live.status === "idle") {
1964
+ chatRef.current.reset();
1965
+ }
1966
+ };
1867
1967
  return /* @__PURE__ */ jsxs2("div", { className: "flex h-full flex-col bg-background", children: [
1868
- /* @__PURE__ */ jsxs2("div", { className: "flex items-center justify-between border-border border-b px-4 py-3", children: [
1869
- /* @__PURE__ */ jsxs2("div", { className: "flex items-baseline gap-2", children: [
1870
- /* @__PURE__ */ jsx4("span", { className: "font-medium text-foreground text-sm", children: "Assistant" }),
1871
- /* @__PURE__ */ jsx4(
1968
+ /* @__PURE__ */ jsxs2("div", { className: "flex items-center justify-between gap-2 border-border border-b px-4 py-3", children: [
1969
+ /* @__PURE__ */ jsxs2("div", { className: "flex min-w-0 flex-col", children: [
1970
+ /* @__PURE__ */ jsxs2("div", { className: "flex items-baseline gap-2", children: [
1971
+ /* @__PURE__ */ jsx4("span", { className: "font-medium text-foreground text-sm", children: "Assistant" }),
1972
+ /* @__PURE__ */ jsx4(
1973
+ "span",
1974
+ {
1975
+ "aria-label": "Your credit balance",
1976
+ className: "text-muted-foreground text-xs",
1977
+ children: formatMoney(effectiveBalance)
1978
+ }
1979
+ )
1980
+ ] }),
1981
+ conversationTitle && /* @__PURE__ */ jsx4(
1872
1982
  "span",
1873
1983
  {
1874
- "aria-label": "Your credit balance",
1875
- className: "text-muted-foreground text-xs",
1876
- children: formatMoney(effectiveBalance)
1984
+ className: "truncate text-muted-foreground text-xs",
1985
+ title: conversationTitle,
1986
+ children: conversationTitle
1877
1987
  }
1878
1988
  )
1879
1989
  ] }),
1880
- /* @__PURE__ */ jsxs2("div", { className: "flex items-center gap-1", children: [
1990
+ /* @__PURE__ */ jsxs2("div", { className: "flex shrink-0 items-center gap-1", children: [
1881
1991
  /* @__PURE__ */ jsxs2("div", { className: "flex items-center", role: "group", "aria-label": "Text size", children: [
1882
1992
  /* @__PURE__ */ jsx4(
1883
1993
  "button",
@@ -1919,8 +2029,9 @@ function AssistantPanel({
1919
2029
  type: "button",
1920
2030
  onClick: chat.reset,
1921
2031
  "aria-label": "New chat",
2032
+ title: "New chat",
1922
2033
  className: "rounded p-1 text-muted-foreground hover:text-foreground",
1923
- children: /* @__PURE__ */ jsx4(RotateCcw, { className: "h-4 w-4" })
2034
+ children: /* @__PURE__ */ jsx4(MessageSquarePlus, { className: "h-4 w-4" })
1924
2035
  }
1925
2036
  ),
1926
2037
  /* @__PURE__ */ jsx4(
@@ -1958,18 +2069,35 @@ function AssistantPanel({
1958
2069
  }
1959
2070
  )
1960
2071
  ] }),
1961
- historyOpen && /* @__PURE__ */ jsx4("div", { className: "max-h-48 overflow-y-auto border-border border-b", children: threads.threads.length === 0 ? /* @__PURE__ */ jsx4("p", { className: "px-4 py-3 text-muted-foreground text-xs", children: threads.loaded ? "No past conversations." : "Loading\u2026" }) : /* @__PURE__ */ jsx4("ul", { children: threads.threads.map((t) => /* @__PURE__ */ jsx4("li", { children: /* @__PURE__ */ jsx4(
1962
- "button",
1963
- {
1964
- type: "button",
1965
- onClick: () => {
1966
- chat.switchThread(t.id);
1967
- setHistoryOpen(false);
1968
- },
1969
- className: "block w-full truncate px-4 py-2 text-left text-foreground text-xs hover:bg-muted/50",
1970
- children: t.title ?? "Untitled conversation"
1971
- }
1972
- ) }, t.id)) }) }),
2072
+ historyOpen && /* @__PURE__ */ jsx4("div", { className: "max-h-48 overflow-y-auto border-border border-b", children: threads.threads.length === 0 ? /* @__PURE__ */ jsx4("p", { className: "px-4 py-3 text-muted-foreground text-xs", children: threads.loaded ? "No past conversations." : "Loading\u2026" }) : /* @__PURE__ */ jsx4("ul", { children: threads.threads.map((t) => {
2073
+ const busyActive = state.threadId === t.id && state.status !== "idle";
2074
+ return /* @__PURE__ */ jsxs2("li", { className: "group flex items-center hover:bg-muted/50", children: [
2075
+ /* @__PURE__ */ jsx4(
2076
+ "button",
2077
+ {
2078
+ type: "button",
2079
+ onClick: () => {
2080
+ chat.switchThread(t.id);
2081
+ setHistoryOpen(false);
2082
+ },
2083
+ className: "min-w-0 flex-1 truncate px-4 py-2 text-left text-foreground text-xs",
2084
+ children: t.title ?? "Untitled conversation"
2085
+ }
2086
+ ),
2087
+ threads.canRemove && /* @__PURE__ */ jsx4(
2088
+ "button",
2089
+ {
2090
+ type: "button",
2091
+ onClick: () => void deleteThread(t.id),
2092
+ disabled: busyActive,
2093
+ "aria-label": "Delete conversation",
2094
+ title: busyActive ? "Can't delete while this conversation is active" : "Delete conversation",
2095
+ className: "shrink-0 p-2 text-muted-foreground opacity-0 transition hover:text-destructive focus-visible:opacity-100 group-hover:opacity-100 disabled:cursor-not-allowed disabled:opacity-30",
2096
+ children: /* @__PURE__ */ jsx4(Trash2, { className: "h-3.5 w-3.5" })
2097
+ }
2098
+ )
2099
+ ] }, t.id);
2100
+ }) }) }),
1973
2101
  /* @__PURE__ */ jsx4(
1974
2102
  "div",
1975
2103
  {
@@ -1986,6 +2114,7 @@ function AssistantPanel({
1986
2114
  isStreaming: streaming,
1987
2115
  isThinking,
1988
2116
  pendingProposals: state.pendingProposals,
2117
+ usage: state.usage,
1989
2118
  renderProposal
1990
2119
  }) : /* @__PURE__ */ jsx4(
1991
2120
  AgentTimeline,
@@ -2038,19 +2167,22 @@ function AssistantPanel({
2038
2167
  ]
2039
2168
  }
2040
2169
  ),
2041
- /* @__PURE__ */ jsx4("div", { className: "border-border border-t p-2", children: /* @__PURE__ */ jsx4(
2042
- ChatInput,
2043
- {
2044
- onSend: (message) => chat.send(message),
2045
- onCancel: chat.stop,
2046
- isStreaming: streaming,
2047
- disabled: chat.restoring || state.status === "awaiting_confirm",
2048
- placeholder: "Message the assistant\u2026",
2049
- inputLabel: null,
2050
- idleStatus: null,
2051
- streamingStatus: null
2052
- }
2053
- ) })
2170
+ /* @__PURE__ */ jsxs2("div", { className: "border-border border-t p-2", children: [
2171
+ streaming && /* @__PURE__ */ jsx4("div", { className: "px-2 pb-1.5", "aria-label": "Assistant is working", children: /* @__PURE__ */ jsx4(ThinkingIndicator, {}) }),
2172
+ /* @__PURE__ */ jsx4(
2173
+ ChatInput,
2174
+ {
2175
+ onSend: (message) => chat.send(message),
2176
+ onCancel: chat.stop,
2177
+ isStreaming: streaming,
2178
+ disabled: chat.restoring || state.status === "awaiting_confirm",
2179
+ placeholder: "Message the assistant\u2026",
2180
+ inputLabel: null,
2181
+ idleStatus: null,
2182
+ streamingStatus: null
2183
+ }
2184
+ )
2185
+ ] })
2054
2186
  ] });
2055
2187
  }
2056
2188
 
@@ -2092,7 +2224,7 @@ function useAssistantLauncher() {
2092
2224
  }
2093
2225
 
2094
2226
  // src/assistant/ResizeHandle.tsx
2095
- import { useRef as useRef4 } from "react";
2227
+ import { useRef as useRef5 } from "react";
2096
2228
  import { jsx as jsx6 } from "react/jsx-runtime";
2097
2229
  function ResizeHandle({
2098
2230
  width,
@@ -2101,7 +2233,7 @@ function ResizeHandle({
2101
2233
  onCommit,
2102
2234
  onNudge
2103
2235
  }) {
2104
- const dragRef = useRef4(null);
2236
+ const dragRef = useRef5(null);
2105
2237
  const onPointerDown = (e) => {
2106
2238
  if (e.button !== 0) return;
2107
2239
  e.preventDefault();
@@ -2190,10 +2322,10 @@ function AssistantDock({
2190
2322
  const chat = useAssistantChat(userId, { onWorkflowMutation });
2191
2323
  const isDesktop = useIsDesktop();
2192
2324
  const { width, maxWidth, setWidth, previewWidth, nudgeWidth } = usePanelWidth();
2193
- const launcherRef = useRef5(null);
2194
- const dialogRef = useRef5(null);
2195
- const returnFocusRef = useRef5(null);
2196
- const wasOpenRef = useRef5(false);
2325
+ const launcherRef = useRef6(null);
2326
+ const dialogRef = useRef6(null);
2327
+ const returnFocusRef = useRef6(null);
2328
+ const wasOpenRef = useRef6(false);
2197
2329
  useEffect5(() => {
2198
2330
  if (!open) return;
2199
2331
  const onKeyDown = (e) => {
@@ -0,0 +1,191 @@
1
+ // src/integrations/provider-logo.tsx
2
+ import * as React from "react";
3
+ import { jsx } from "react/jsx-runtime";
4
+ function normalizeProviderId(id) {
5
+ return id.toLowerCase().replace(/[_\s]+/g, "-").replace(/-(business|oauth|api|app|mail|sms|pack|connector|v\d+)$/g, "");
6
+ }
7
+ var PROVIDER_LOGO_SLUGS = {
8
+ gmail: "gmail",
9
+ googlemail: "gmail",
10
+ google: "google",
11
+ "google-drive": "googledrive",
12
+ "google-calendar": "googlecalendar",
13
+ "google-sheets": "googlesheets",
14
+ "google-docs": "googledocs",
15
+ "google-meet": "googlemeet",
16
+ "google-forms": "googleforms",
17
+ "google-ads": "googleads",
18
+ "google-analytics": "googleanalytics",
19
+ outlook: "microsoftoutlook",
20
+ "outlook-mail": "microsoftoutlook",
21
+ "microsoft-outlook": "microsoftoutlook",
22
+ "microsoft-calendar": "microsoftoutlook",
23
+ "microsoft-teams": "microsoftteams",
24
+ teams: "microsoftteams",
25
+ "microsoft-excel": "microsoftexcel",
26
+ excel: "microsoftexcel",
27
+ onedrive: "microsoftonedrive",
28
+ sharepoint: "microsoftsharepoint",
29
+ twitter: "x",
30
+ x: "x",
31
+ meta: "meta",
32
+ "stripe-pack": "stripe",
33
+ "twilio-sms": "twilio",
34
+ webhook: "webhooks",
35
+ webhooks: "webhooks",
36
+ hubspot: "hubspot",
37
+ salesforce: "salesforce",
38
+ pipedrive: "pipedrive",
39
+ zoho: "zoho",
40
+ quickbooks: "quickbooks",
41
+ intercom: "intercom",
42
+ zendesk: "zendesk",
43
+ freshdesk: "freshdesk",
44
+ monday: "mondaydotcom",
45
+ "monday-com": "mondaydotcom",
46
+ clickup: "clickup",
47
+ basecamp: "basecamp",
48
+ todoist: "todoist",
49
+ calendly: "calendly",
50
+ typeform: "typeform",
51
+ surveymonkey: "surveymonkey",
52
+ klaviyo: "klaviyo",
53
+ sendinblue: "brevo",
54
+ brevo: "brevo",
55
+ "constant-contact": "constantcontact",
56
+ "active-campaign": "activecampaign",
57
+ activecampaign: "activecampaign",
58
+ "google-chat": "googlechat",
59
+ whatsapp: "whatsapp",
60
+ telegram: "telegram",
61
+ bigquery: "googlebigquery",
62
+ snowflake: "snowflake",
63
+ postgres: "postgresql",
64
+ postgresql: "postgresql",
65
+ mysql: "mysql",
66
+ mongodb: "mongodb",
67
+ redis: "redis",
68
+ supabase: "supabase",
69
+ firebase: "firebase",
70
+ "aws-s3": "amazons3",
71
+ s3: "amazons3",
72
+ woocommerce: "woocommerce",
73
+ bigcommerce: "bigcommerce",
74
+ squarespace: "squarespace",
75
+ wix: "wix",
76
+ webflow: "webflow",
77
+ wordpress: "wordpress",
78
+ contentful: "contentful",
79
+ sanity: "sanity",
80
+ figma: "figma",
81
+ miro: "miro",
82
+ confluence: "confluence",
83
+ bitbucket: "bitbucket",
84
+ pagerduty: "pagerduty",
85
+ datadog: "datadog",
86
+ sentry: "sentry",
87
+ segment: "segment",
88
+ amplitude: "amplitude",
89
+ mixpanel: "mixpanel",
90
+ posthog: "posthog",
91
+ facebook: "facebook",
92
+ instagram: "instagram",
93
+ tiktok: "tiktok",
94
+ youtube: "youtube",
95
+ reddit: "reddit",
96
+ pinterest: "pinterest",
97
+ buffer: "buffer",
98
+ hootsuite: "hootsuite"
99
+ };
100
+ function providerLogoCandidates(opts) {
101
+ const out = [];
102
+ if (opts.iconUrl) out.push(opts.iconUrl);
103
+ const raw = opts.id.toLowerCase();
104
+ const norm = normalizeProviderId(raw);
105
+ const slugs = /* @__PURE__ */ new Set();
106
+ const curated = PROVIDER_LOGO_SLUGS[raw] ?? PROVIDER_LOGO_SLUGS[norm];
107
+ if (curated) slugs.add(curated);
108
+ slugs.add(norm.replace(/-/g, ""));
109
+ slugs.add(raw.replace(/[-_\s]/g, ""));
110
+ for (const slug of slugs) {
111
+ if (slug) out.push(`https://cdn.simpleicons.org/${slug}`);
112
+ }
113
+ return out;
114
+ }
115
+ function monogramColor(seed) {
116
+ const palette = [
117
+ "#6366f1",
118
+ "#8b5cf6",
119
+ "#ec4899",
120
+ "#f43f5e",
121
+ "#f97316",
122
+ "#eab308",
123
+ "#22c55e",
124
+ "#14b8a6",
125
+ "#0ea5e9",
126
+ "#3b82f6"
127
+ ];
128
+ let hash = 0;
129
+ for (let i = 0; i < seed.length; i += 1) {
130
+ hash = hash * 31 + seed.charCodeAt(i) >>> 0;
131
+ }
132
+ return palette[hash % palette.length];
133
+ }
134
+ function ProviderIcon({
135
+ id,
136
+ iconUrl,
137
+ displayName,
138
+ size = 24,
139
+ className
140
+ }) {
141
+ const candidates = React.useMemo(
142
+ () => providerLogoCandidates({ id, iconUrl }),
143
+ [id, iconUrl]
144
+ );
145
+ const [index, setIndex] = React.useState(0);
146
+ const candidateKey = `${id} ${iconUrl ?? ""}`;
147
+ const [seenKey, setSeenKey] = React.useState(candidateKey);
148
+ if (candidateKey !== seenKey) {
149
+ setSeenKey(candidateKey);
150
+ setIndex(0);
151
+ }
152
+ const label = (displayName ?? id).trim();
153
+ const initial = label.charAt(0).toUpperCase() || "?";
154
+ const src = candidates[index];
155
+ if (!src) {
156
+ return /* @__PURE__ */ jsx(
157
+ "span",
158
+ {
159
+ className: `flex shrink-0 items-center justify-center font-semibold text-white ${className ?? ""}`,
160
+ style: {
161
+ width: size,
162
+ height: size,
163
+ backgroundColor: monogramColor(id),
164
+ fontSize: Math.round(size * 0.42)
165
+ },
166
+ "aria-hidden": true,
167
+ children: initial
168
+ }
169
+ );
170
+ }
171
+ return /* @__PURE__ */ jsx(
172
+ "img",
173
+ {
174
+ src,
175
+ alt: "",
176
+ width: size,
177
+ height: size,
178
+ loading: "lazy",
179
+ className: `shrink-0 object-contain ${className ?? ""}`,
180
+ style: { width: size, height: size },
181
+ onError: () => setIndex((i) => i + 1)
182
+ }
183
+ );
184
+ }
185
+
186
+ export {
187
+ normalizeProviderId,
188
+ providerLogoCandidates,
189
+ monogramColor,
190
+ ProviderIcon
191
+ };
package/dist/globals.css CHANGED
@@ -692,6 +692,9 @@
692
692
  .h-56 {
693
693
  height: calc(var(--spacing) * 56);
694
694
  }
695
+ .h-64 {
696
+ height: calc(var(--spacing) * 64);
697
+ }
695
698
  .h-\[3px\] {
696
699
  height: 3px;
697
700
  }
@@ -2378,6 +2381,9 @@
2378
2381
  .pb-1 {
2379
2382
  padding-bottom: calc(var(--spacing) * 1);
2380
2383
  }
2384
+ .pb-1\.5 {
2385
+ padding-bottom: calc(var(--spacing) * 1.5);
2386
+ }
2381
2387
  .pb-2 {
2382
2388
  padding-bottom: calc(var(--spacing) * 2);
2383
2389
  }
@@ -1,3 +1,9 @@
1
+ import {
2
+ monogramColor,
3
+ normalizeProviderId,
4
+ providerLogoCandidates
5
+ } from "./chunk-H7GMPMPB.js";
6
+
1
7
  // src/integrations/integrations-panel.tsx
2
8
  import * as React from "react";
3
9
  import { Card, CardContent, EmptyState } from "@tangle-network/ui/primitives";
@@ -42,141 +48,17 @@ var DEFAULT_FEATURED_IDS = [
42
48
  function defaultConnectorOf(provider) {
43
49
  return provider.connectors?.[0]?.connectorId ?? provider.providerId;
44
50
  }
45
- function normalizeProviderId(id) {
46
- return id.toLowerCase().replace(/[_\s]+/g, "-").replace(/-(business|oauth|api|app|mail|sms|pack|connector|v\d+)$/g, "");
47
- }
48
- var PROVIDER_LOGO_SLUGS = {
49
- gmail: "gmail",
50
- googlemail: "gmail",
51
- google: "google",
52
- "google-drive": "googledrive",
53
- "google-calendar": "googlecalendar",
54
- "google-sheets": "googlesheets",
55
- "google-docs": "googledocs",
56
- "google-meet": "googlemeet",
57
- "google-forms": "googleforms",
58
- "google-ads": "googleads",
59
- "google-analytics": "googleanalytics",
60
- outlook: "microsoftoutlook",
61
- "outlook-mail": "microsoftoutlook",
62
- "microsoft-outlook": "microsoftoutlook",
63
- "microsoft-calendar": "microsoftoutlook",
64
- "microsoft-teams": "microsoftteams",
65
- teams: "microsoftteams",
66
- "microsoft-excel": "microsoftexcel",
67
- excel: "microsoftexcel",
68
- onedrive: "microsoftonedrive",
69
- sharepoint: "microsoftsharepoint",
70
- twitter: "x",
71
- x: "x",
72
- meta: "meta",
73
- "stripe-pack": "stripe",
74
- "twilio-sms": "twilio",
75
- webhook: "webhooks",
76
- webhooks: "webhooks",
77
- hubspot: "hubspot",
78
- salesforce: "salesforce",
79
- pipedrive: "pipedrive",
80
- zoho: "zoho",
81
- quickbooks: "quickbooks",
82
- intercom: "intercom",
83
- zendesk: "zendesk",
84
- freshdesk: "freshdesk",
85
- monday: "mondaydotcom",
86
- "monday-com": "mondaydotcom",
87
- clickup: "clickup",
88
- basecamp: "basecamp",
89
- todoist: "todoist",
90
- calendly: "calendly",
91
- typeform: "typeform",
92
- surveymonkey: "surveymonkey",
93
- klaviyo: "klaviyo",
94
- sendinblue: "brevo",
95
- brevo: "brevo",
96
- "constant-contact": "constantcontact",
97
- "active-campaign": "activecampaign",
98
- activecampaign: "activecampaign",
99
- "google-chat": "googlechat",
100
- whatsapp: "whatsapp",
101
- telegram: "telegram",
102
- bigquery: "googlebigquery",
103
- snowflake: "snowflake",
104
- postgres: "postgresql",
105
- postgresql: "postgresql",
106
- mysql: "mysql",
107
- mongodb: "mongodb",
108
- redis: "redis",
109
- supabase: "supabase",
110
- firebase: "firebase",
111
- "aws-s3": "amazons3",
112
- s3: "amazons3",
113
- woocommerce: "woocommerce",
114
- bigcommerce: "bigcommerce",
115
- squarespace: "squarespace",
116
- wix: "wix",
117
- webflow: "webflow",
118
- wordpress: "wordpress",
119
- contentful: "contentful",
120
- sanity: "sanity",
121
- figma: "figma",
122
- miro: "miro",
123
- confluence: "confluence",
124
- bitbucket: "bitbucket",
125
- pagerduty: "pagerduty",
126
- datadog: "datadog",
127
- sentry: "sentry",
128
- segment: "segment",
129
- amplitude: "amplitude",
130
- mixpanel: "mixpanel",
131
- posthog: "posthog",
132
- facebook: "facebook",
133
- instagram: "instagram",
134
- tiktok: "tiktok",
135
- youtube: "youtube",
136
- reddit: "reddit",
137
- pinterest: "pinterest",
138
- buffer: "buffer",
139
- hootsuite: "hootsuite"
140
- };
141
- function logoCandidates(provider) {
142
- const out = [];
143
- if (provider.iconUrl) out.push(provider.iconUrl);
144
- const raw = provider.providerId.toLowerCase();
145
- const norm = normalizeProviderId(raw);
146
- const slugs = /* @__PURE__ */ new Set();
147
- const curated = PROVIDER_LOGO_SLUGS[raw] ?? PROVIDER_LOGO_SLUGS[norm];
148
- if (curated) slugs.add(curated);
149
- slugs.add(norm.replace(/-/g, ""));
150
- slugs.add(raw.replace(/[-_\s]/g, ""));
151
- for (const slug of slugs) {
152
- if (slug) out.push(`https://cdn.simpleicons.org/${slug}`);
153
- }
154
- return out;
155
- }
156
- function monogramColor(seed) {
157
- const palette = [
158
- "#6366f1",
159
- "#8b5cf6",
160
- "#ec4899",
161
- "#f43f5e",
162
- "#f97316",
163
- "#eab308",
164
- "#22c55e",
165
- "#14b8a6",
166
- "#0ea5e9",
167
- "#3b82f6"
168
- ];
169
- let hash = 0;
170
- for (let i = 0; i < seed.length; i += 1) {
171
- hash = hash * 31 + seed.charCodeAt(i) >>> 0;
172
- }
173
- return palette[hash % palette.length];
174
- }
175
51
  function ProviderLogo({
176
52
  provider,
177
53
  size = 56
178
54
  }) {
179
- const candidates = React.useMemo(() => logoCandidates(provider), [provider]);
55
+ const candidates = React.useMemo(
56
+ () => providerLogoCandidates({
57
+ id: provider.providerId,
58
+ iconUrl: provider.iconUrl
59
+ }),
60
+ [provider]
61
+ );
180
62
  const [index, setIndex] = React.useState(0);
181
63
  const label = (provider.displayName ?? provider.providerId).trim();
182
64
  const initial = label.charAt(0).toUpperCase() || "?";
package/dist/styles.css CHANGED
@@ -692,6 +692,9 @@
692
692
  .h-56 {
693
693
  height: calc(var(--spacing) * 56);
694
694
  }
695
+ .h-64 {
696
+ height: calc(var(--spacing) * 64);
697
+ }
695
698
  .h-\[3px\] {
696
699
  height: 3px;
697
700
  }
@@ -2378,6 +2381,9 @@
2378
2381
  .pb-1 {
2379
2382
  padding-bottom: calc(var(--spacing) * 1);
2380
2383
  }
2384
+ .pb-1\.5 {
2385
+ padding-bottom: calc(var(--spacing) * 1.5);
2386
+ }
2381
2387
  .pb-2 {
2382
2388
  padding-bottom: calc(var(--spacing) * 2);
2383
2389
  }
package/dist/workflows.js CHANGED
@@ -4,11 +4,32 @@ import {
4
4
  import "./chunk-7JLB7UZ3.js";
5
5
 
6
6
  // src/workflows/WorkflowGraphLazy.tsx
7
- import { Component, lazy, Suspense } from "react";
7
+ import {
8
+ Component,
9
+ lazy,
10
+ Suspense,
11
+ useMemo,
12
+ useState
13
+ } from "react";
8
14
  import { jsx, jsxs } from "react/jsx-runtime";
9
- var WorkflowGraphImpl = lazy(
10
- () => import("./WorkflowGraph-5QO6WCY2.js").then((m) => ({ default: m.WorkflowGraph }))
11
- );
15
+ function retryImport(factory, retries = 2, delayMs = 350) {
16
+ return factory().catch((err) => {
17
+ if (retries <= 0) throw err;
18
+ return new Promise((resolve, reject) => {
19
+ setTimeout(
20
+ () => retryImport(factory, retries - 1, delayMs).then(resolve, reject),
21
+ delayMs
22
+ );
23
+ });
24
+ });
25
+ }
26
+ function lazyWorkflowGraph() {
27
+ return lazy(
28
+ () => retryImport(
29
+ () => import("./WorkflowGraph-7YYGIBNV.js").then((m) => ({ default: m.WorkflowGraph }))
30
+ )
31
+ );
32
+ }
12
33
  var GraphErrorBoundary = class extends Component {
13
34
  state = { hasError: false };
14
35
  static getDerivedStateFromError() {
@@ -18,27 +39,45 @@ var GraphErrorBoundary = class extends Component {
18
39
  return this.state.hasError ? this.props.fallback : this.props.children;
19
40
  }
20
41
  };
21
- function GraphFallback({ yaml, className }) {
42
+ function GraphFallback({
43
+ yaml,
44
+ className,
45
+ onRetry
46
+ }) {
22
47
  return /* @__PURE__ */ jsxs(
23
48
  "div",
24
49
  {
25
50
  className: `overflow-auto rounded-lg border border-border bg-background p-2 ${className ?? ""}`,
26
51
  children: [
27
- /* @__PURE__ */ jsx("p", { className: "mb-1 text-text-muted text-xs", children: "Couldn't render the graph \u2014 showing the definition." }),
52
+ /* @__PURE__ */ jsxs("div", { className: "mb-1 flex items-center justify-between gap-2", children: [
53
+ /* @__PURE__ */ jsx("p", { className: "text-text-muted text-xs", children: "Couldn't render the graph \u2014 showing the definition." }),
54
+ /* @__PURE__ */ jsx(
55
+ "button",
56
+ {
57
+ type: "button",
58
+ onClick: onRetry,
59
+ className: "shrink-0 rounded border border-border px-1.5 py-0.5 text-text-muted text-xs transition hover:text-text",
60
+ children: "Retry"
61
+ }
62
+ )
63
+ ] }),
28
64
  /* @__PURE__ */ jsx("pre", { className: "text-text text-xs", children: /* @__PURE__ */ jsx("code", { children: yaml }) })
29
65
  ]
30
66
  }
31
67
  );
32
68
  }
33
69
  function WorkflowGraph(props) {
70
+ const [attempt, setAttempt] = useState(0);
71
+ const Impl = useMemo(() => lazyWorkflowGraph(), [attempt]);
34
72
  return (
35
- // Key the boundary by the workflow so a different/edited definition remounts
36
- // it and re-attempts the graph — a transient chunk/render failure on one
37
- // workflow doesn't permanently pin the YAML fallback for the next.
73
+ // Key the boundary by the workflow AND the attempt so a different/edited
74
+ // definition or a manual retry remounts it and re-attempts the graph — a
75
+ // transient chunk/render failure on one workflow doesn't permanently pin the
76
+ // YAML fallback.
38
77
  /* @__PURE__ */ jsx(
39
78
  GraphErrorBoundary,
40
79
  {
41
- fallback: /* @__PURE__ */ jsx(GraphFallback, { ...props }),
80
+ fallback: /* @__PURE__ */ jsx(GraphFallback, { ...props, onRetry: () => setAttempt((a) => a + 1) }),
42
81
  children: /* @__PURE__ */ jsx(
43
82
  Suspense,
44
83
  {
@@ -49,11 +88,11 @@ function WorkflowGraph(props) {
49
88
  children: "Loading graph\u2026"
50
89
  }
51
90
  ),
52
- children: /* @__PURE__ */ jsx(WorkflowGraphImpl, { ...props })
91
+ children: /* @__PURE__ */ jsx(Impl, { ...props })
53
92
  }
54
93
  )
55
94
  },
56
- props.yaml
95
+ `${props.yaml}::${attempt}`
57
96
  )
58
97
  );
59
98
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/sandbox-ui",
3
- "version": "0.42.0",
3
+ "version": "0.44.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",