@tangle-network/sandbox-ui 0.113.7 → 0.114.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.
Files changed (106) hide show
  1. package/dist/WorkflowGraph-B-NsdYz_.js +1961 -0
  2. package/dist/assets.d.ts +214 -206
  3. package/dist/assets.js +1057 -908
  4. package/dist/auth-CWe17Hly.js +37 -0
  5. package/dist/auth.d.ts +2 -30
  6. package/dist/auth.js +2 -15
  7. package/dist/chat-BOvFAe6p.js +903 -0
  8. package/dist/chat.d.ts +2 -230
  9. package/dist/chat.js +2 -34
  10. package/dist/connectors.d.ts +52 -49
  11. package/dist/connectors.js +595 -643
  12. package/dist/dashboard-CZ3VgNPR.js +4969 -0
  13. package/dist/dashboard.d.ts +4 -1297
  14. package/dist/dashboard.js +4 -152
  15. package/dist/editor.d.ts +2 -1
  16. package/dist/editor.js +2 -31
  17. package/dist/files.d.ts +2 -6
  18. package/dist/files.js +2 -33
  19. package/dist/globals.css +63 -3
  20. package/dist/hooks.d.ts +146 -148
  21. package/dist/hooks.js +222 -55
  22. package/dist/index-CKRaj1pM.d.ts +776 -0
  23. package/dist/index-CXX5IpH2.d.ts +2 -0
  24. package/dist/index-DPePq7c8.d.ts +1340 -0
  25. package/dist/index-DiyLLZuJ.d.ts +230 -0
  26. package/dist/index-DvmZLMT1.d.ts +30 -0
  27. package/dist/index-I8ZLVtHd.d.ts +330 -0
  28. package/dist/index-PRM636Ex.d.ts +86 -0
  29. package/dist/index.d.ts +21 -33
  30. package/dist/index.js +21 -701
  31. package/dist/info-panel-C9cqDzOL.js +594 -0
  32. package/dist/integrations.d.ts +133 -141
  33. package/dist/integrations.js +491 -673
  34. package/dist/markdown.d.ts +2 -1
  35. package/dist/markdown.js +2 -13
  36. package/dist/model-I-NIxeM-.js +1250 -0
  37. package/dist/model-brand-B9A4H9pJ.d.ts +98 -0
  38. package/dist/model-brand-hMyDTUqB.js +624 -0
  39. package/dist/motion-DkyBRJb9.js +37 -0
  40. package/dist/openui.d.ts +2 -1
  41. package/dist/openui.js +2 -7
  42. package/dist/pages.d.ts +307 -300
  43. package/dist/pages.js +4404 -4181
  44. package/dist/primitives-Dh1vG6J3.js +113 -0
  45. package/dist/primitives.d.ts +2 -74
  46. package/dist/primitives.js +2 -204
  47. package/dist/provider-logo-DjqmConF.js +311 -0
  48. package/dist/run.d.ts +2 -1
  49. package/dist/run.js +2 -23
  50. package/dist/sdk-hooks.d.ts +3 -4
  51. package/dist/sdk-hooks.js +3 -31
  52. package/dist/sidecar-DgH_vQ6m.d.ts +9 -0
  53. package/dist/stores.d.ts +14 -15
  54. package/dist/stores.js +13 -81
  55. package/dist/styles.css +63 -3
  56. package/dist/template-card-L-4gmjx2.d.ts +90 -0
  57. package/dist/terminal-CN9ZgHRt.js +197 -0
  58. package/dist/terminal.d.ts +60 -60
  59. package/dist/terminal.js +2 -10
  60. package/dist/tokens.css +51 -0
  61. package/dist/types.d.ts +4 -11
  62. package/dist/types.js +0 -1
  63. package/dist/use-pty-session-C8cE0adT.js +669 -0
  64. package/dist/use-sidecar-auth-CCTJcrq7.d.ts +145 -0
  65. package/dist/use-sidecar-auth-DkdcufOz.js +742 -0
  66. package/dist/utils-DzWbktWG.js +2 -0
  67. package/dist/utils.d.ts +2 -1
  68. package/dist/utils.js +2 -37
  69. package/dist/workbench-BCaCKl-J.js +8347 -0
  70. package/dist/workbench.d.ts +2 -310
  71. package/dist/workbench.js +2 -44
  72. package/dist/workflows.d.ts +341 -339
  73. package/dist/workflows.js +74 -109
  74. package/dist/workspace-B6AtI6ld.js +2510 -0
  75. package/dist/workspace.d.ts +2 -761
  76. package/dist/workspace.js +2 -51
  77. package/package.json +46 -40
  78. package/dist/WorkflowGraph-2QOORYIR.js +0 -1722
  79. package/dist/chunk-2BUPSB7O.js +0 -0
  80. package/dist/chunk-3KBNPQQF.js +0 -2697
  81. package/dist/chunk-3LIXVEEE.js +0 -279
  82. package/dist/chunk-72UF2H5H.js +0 -707
  83. package/dist/chunk-AI25G5MX.js +0 -944
  84. package/dist/chunk-ANSC5AFC.js +0 -314
  85. package/dist/chunk-AZ3AWMTM.js +0 -8
  86. package/dist/chunk-BGVEHENC.js +0 -10
  87. package/dist/chunk-CZ7CSFO4.js +0 -39
  88. package/dist/chunk-DYXXYGS4.js +0 -4700
  89. package/dist/chunk-EI44GEQ5.js +0 -6
  90. package/dist/chunk-IACRDXER.js +0 -219
  91. package/dist/chunk-IOB2PW5Z.js +0 -55
  92. package/dist/chunk-M4K7ZGR2.js +0 -24
  93. package/dist/chunk-MYZAW4RX.js +0 -665
  94. package/dist/chunk-OMUIPJQZ.js +0 -555
  95. package/dist/chunk-RQZRXKMR.js +0 -398
  96. package/dist/chunk-SOKKTB7W.js +0 -230
  97. package/dist/chunk-TADGS4Q7.js +0 -1010
  98. package/dist/chunk-UCQNOUN3.js +0 -7964
  99. package/dist/chunk-ULPLRBMF.js +0 -34
  100. package/dist/chunk-WID73FPH.js +0 -89
  101. package/dist/chunk-X3YXP7FG.js +0 -38
  102. package/dist/logo-DiAjq759.d.ts +0 -15
  103. package/dist/model-brand-BNnaUKKZ.d.ts +0 -99
  104. package/dist/ports-list-CDIvTNnX.d.ts +0 -17
  105. package/dist/sdk-hooks-Djkjq4cp.d.ts +0 -145
  106. package/dist/template-card-Bn-Bw5xC.d.ts +0 -87
@@ -0,0 +1,903 @@
1
+ import { t as cn$1 } from "./utils-DzWbktWG.js";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import * as React$1 from "react";
4
+ import { focusField, focusRing } from "@tangle-network/ui/utils";
5
+ import { Bot, Brain, Check, ChevronDown, Lock, Pencil, Plus, Send, Sparkles, Square, Trash2, X } from "lucide-react";
6
+ import { AgentTimeline, ChatContainer as ChatContainer$1, ChatMessage, ChatMessage as ChatMessage$1, MessageList, ThinkingIndicator, UserMessage } from "@tangle-network/ui/chat";
7
+ import { createPortal } from "react-dom";
8
+ import { modelProvider, reasoningLadder, snapHarnessToModel } from "@tangle-network/agent-interface";
9
+ import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
10
+ //#region src/chat/reasoning-level-picker.tsx
11
+ /** Ordered ascending intensity (excludes "auto"), straight from the canonical ladder. */
12
+ const REASONING_LADDER = reasoningLadder;
13
+ /**
14
+ * Proportional intensity meter. Auto shows a Sparkles glyph (no fixed depth);
15
+ * every other level fills a fixed-width bar meter to its rank in the ladder, so
16
+ * the rows read as an ascending scale regardless of how many levels exist.
17
+ */
18
+ function ReasoningGlyph({ level }) {
19
+ if (level === "auto") return /* @__PURE__ */ jsx(Sparkles, { className: "h-3.5 w-3.5 text-muted-foreground" });
20
+ const BARS = 4;
21
+ const rank = REASONING_LADDER.indexOf(level) + 1;
22
+ const filled = Math.max(1, Math.ceil(rank / REASONING_LADDER.length * BARS));
23
+ return /* @__PURE__ */ jsx("span", {
24
+ "aria-hidden": true,
25
+ className: "inline-flex h-3.5 items-end gap-px",
26
+ style: { width: 16 },
27
+ children: Array.from({ length: BARS }).map((_, i) => /* @__PURE__ */ jsx("span", {
28
+ className: cn$1("w-1 rounded-[1px]", i < filled ? "bg-foreground" : "bg-border"),
29
+ style: { height: `${35 + i * 22}%` }
30
+ }, i))
31
+ });
32
+ }
33
+ const DEFAULT_REASONING_LEVEL_OPTIONS = [
34
+ {
35
+ value: "auto",
36
+ label: "Auto",
37
+ description: "Let the agent pick the right depth."
38
+ },
39
+ {
40
+ value: "none",
41
+ label: "None",
42
+ description: "No extended thinking — fastest, cheapest."
43
+ },
44
+ {
45
+ value: "minimal",
46
+ label: "Minimal",
47
+ description: "Thinking on, lowest budget."
48
+ },
49
+ {
50
+ value: "low",
51
+ label: "Low",
52
+ description: "Fast, direct answers."
53
+ },
54
+ {
55
+ value: "medium",
56
+ label: "Medium",
57
+ description: "Inspect context before acting."
58
+ },
59
+ {
60
+ value: "high",
61
+ label: "High",
62
+ description: "Deeper planning and edge-case checks."
63
+ },
64
+ {
65
+ value: "xhigh",
66
+ label: "Extra High",
67
+ description: "Extended reasoning for the hardest problems."
68
+ },
69
+ {
70
+ value: "ultracode",
71
+ label: "Ultracode",
72
+ description: "Maximum extended thinking."
73
+ }
74
+ ];
75
+ /**
76
+ * Per-harness option overrides for harnesses whose real control isn't a depth scale, so the generic
77
+ * ladder's labels would mislead. A harness with no entry uses {@link DEFAULT_REASONING_LEVEL_OPTIONS};
78
+ * either way the picker still intersects the chosen options with the harness/model `available` set, so
79
+ * only supported values render.
80
+ *
81
+ * `kimi-code` is a BINARY thinking toggle, not a gradient: its `--thinking` flag is simply off or on.
82
+ * `none` emits `--no-thinking` (off) and `high` maps to thinking-on, so the
83
+ * menu reads Auto / No thinking / Thinking rather than a misleading five-step scale.
84
+ */
85
+ const HARNESS_REASONING_OPTIONS = { "kimi-code": [
86
+ {
87
+ value: "auto",
88
+ label: "Auto",
89
+ description: "Use Kimi's default."
90
+ },
91
+ {
92
+ value: "none",
93
+ label: "No thinking",
94
+ description: "Kimi answers directly — thinking off."
95
+ },
96
+ {
97
+ value: "high",
98
+ label: "Thinking",
99
+ description: "Kimi reasons before answering."
100
+ }
101
+ ] };
102
+ function ReasoningLevelPicker({ value, onChange, disabled, className, triggerClassName, options = DEFAULT_REASONING_LEVEL_OPTIONS, available, side, avoidCollisions }) {
103
+ const shown = available ? options.filter((o) => o.value === "auto" || available.includes(o.value)) : options;
104
+ const label = shown.find((option) => option.value === value)?.label ?? "Auto";
105
+ return /* @__PURE__ */ jsxs(DropdownMenu.Root, { children: [/* @__PURE__ */ jsx(DropdownMenu.Trigger, {
106
+ asChild: true,
107
+ children: /* @__PURE__ */ jsxs("button", {
108
+ type: "button",
109
+ disabled,
110
+ className: cn$1("inline-flex h-8 items-center gap-1.5 rounded-lg border border-[var(--md3-outline-variant)] bg-surface-container px-2.5", "text-xs font-medium text-foreground shadow-sm transition-colors", "hover:border-[var(--md3-outline-variant)] hover:bg-surface-container-high", focusRing, "data-[state=open]:border-[var(--md3-outline-variant)] data-[state=open]:bg-surface-container-high", "disabled:cursor-not-allowed disabled:opacity-50", className, triggerClassName),
111
+ "aria-label": "Reasoning level",
112
+ children: [
113
+ /* @__PURE__ */ jsx(Brain, { className: "h-3.5 w-3.5 shrink-0 text-muted-foreground" }),
114
+ /* @__PURE__ */ jsx("span", { children: label }),
115
+ /* @__PURE__ */ jsx(ChevronDown, { className: "ml-auto h-3.5 w-3.5 shrink-0 text-muted-foreground" })
116
+ ]
117
+ })
118
+ }), /* @__PURE__ */ jsx(DropdownMenu.Portal, { children: /* @__PURE__ */ jsx(DropdownMenu.Content, {
119
+ side,
120
+ align: "start",
121
+ avoidCollisions,
122
+ collisionPadding: 24,
123
+ sideOffset: 6,
124
+ className: cn$1("z-50 w-64 max-h-[var(--radix-dropdown-menu-content-available-height)] overflow-y-auto rounded-[var(--radius-md)] border border-[var(--md3-outline-variant)] bg-surface-container-highest p-1", "shadow-[0_8px_30px_rgba(0,0,0,0.45)] ring-1 ring-[#ffffff14]", "data-[state=open]:animate-in data-[state=closed]:animate-out", "data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0", "data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95"),
125
+ children: shown.map((option) => /* @__PURE__ */ jsxs(DropdownMenu.Item, {
126
+ onSelect: () => onChange(option.value),
127
+ className: cn$1("flex cursor-pointer items-start gap-2.5 rounded-md px-2.5 py-2 outline-none", "transition-colors hover:bg-accent/40 focus:bg-accent/40", option.value === value && "bg-[var(--accent-surface-soft)] text-[var(--accent-text)]"),
128
+ children: [/* @__PURE__ */ jsx("span", {
129
+ className: "mt-0.5 flex w-3.5 shrink-0 justify-center",
130
+ children: /* @__PURE__ */ jsx(ReasoningGlyph, { level: option.value })
131
+ }), /* @__PURE__ */ jsxs("span", {
132
+ className: "flex min-w-0 flex-col gap-0.5",
133
+ children: [/* @__PURE__ */ jsx("span", {
134
+ className: "text-sm font-medium",
135
+ children: option.label
136
+ }), /* @__PURE__ */ jsx("span", {
137
+ className: "text-xs text-muted-foreground",
138
+ children: option.description
139
+ })]
140
+ })]
141
+ }, option.value))
142
+ }) })] });
143
+ }
144
+ //#endregion
145
+ //#region src/lib/use-click-outside.ts
146
+ /**
147
+ * Returns a ref; calls `onOutside` when a mousedown lands outside that element
148
+ * and outside every element in `extras`. For self-contained popovers/menus
149
+ * that close on an outside click; `extras` covers body-portaled panels, whose
150
+ * DOM nodes are not descendants of the returned ref.
151
+ */
152
+ function useClickOutside(onOutside, extras) {
153
+ const ref = React$1.useRef(null);
154
+ const extrasRef = React$1.useRef(extras);
155
+ extrasRef.current = extras;
156
+ React$1.useEffect(() => {
157
+ function handler(event) {
158
+ const target = event.target;
159
+ if (!ref.current || ref.current.contains(target)) return;
160
+ if (extrasRef.current?.some((extra) => extra.current?.contains(target))) return;
161
+ onOutside();
162
+ }
163
+ document.addEventListener("mousedown", handler);
164
+ return () => document.removeEventListener("mousedown", handler);
165
+ }, [onOutside]);
166
+ return ref;
167
+ }
168
+ //#endregion
169
+ //#region src/chat/anchored-popover.tsx
170
+ /**
171
+ * Body-portaled popover for the self-contained (non-Radix) pickers. Composer
172
+ * control strips scroll horizontally and sit under overlapping siblings, so an
173
+ * inline `position:absolute` panel gets clipped by the strip's overflow and
174
+ * loses hit-testing to later stacking contexts — portaling to `document.body`
175
+ * with viewport-fixed coordinates sidesteps both. Position tracks the anchor
176
+ * on scroll/resize while open; callers keep ownership of open state, outside
177
+ * clicks (pass the returned node to `useClickOutside` extras), and contents.
178
+ */
179
+ const AnchoredPopover = React$1.forwardRef(function AnchoredPopover({ anchorRef, side, children, className, style, ...rest }, forwardedRef) {
180
+ const innerRef = React$1.useRef(null);
181
+ const setRefs = (node) => {
182
+ innerRef.current = node;
183
+ if (typeof forwardedRef === "function") forwardedRef(node);
184
+ else if (forwardedRef) forwardedRef.current = node;
185
+ };
186
+ const [position, setPosition] = React$1.useState(null);
187
+ const place = React$1.useCallback(() => {
188
+ const anchor = anchorRef.current;
189
+ if (!anchor) return;
190
+ const rect = anchor.getBoundingClientRect();
191
+ const width = innerRef.current?.offsetWidth ?? 0;
192
+ const margin = 8;
193
+ const left = Math.max(margin, Math.min(rect.left, window.innerWidth - width - margin));
194
+ setPosition(side === "top" ? {
195
+ left,
196
+ bottom: window.innerHeight - rect.top + margin
197
+ } : {
198
+ left,
199
+ top: rect.bottom + margin
200
+ });
201
+ }, [anchorRef, side]);
202
+ React$1.useLayoutEffect(() => {
203
+ place();
204
+ window.addEventListener("resize", place);
205
+ window.addEventListener("scroll", place, true);
206
+ return () => {
207
+ window.removeEventListener("resize", place);
208
+ window.removeEventListener("scroll", place, true);
209
+ };
210
+ }, [place]);
211
+ return createPortal(/* @__PURE__ */ jsx("div", {
212
+ ...rest,
213
+ ref: setRefs,
214
+ className: cn$1("fixed z-50", className),
215
+ style: {
216
+ ...style,
217
+ pointerEvents: "auto",
218
+ ...position ?? {
219
+ visibility: "hidden",
220
+ left: 0,
221
+ top: 0
222
+ }
223
+ },
224
+ children
225
+ }), document.body);
226
+ });
227
+ //#endregion
228
+ //#region src/chat/informative-lock.tsx
229
+ /**
230
+ * Interactive locked-control shell shared by session-bound pickers. It keeps
231
+ * the pinned selection visible while explaining how to switch in a new chat.
232
+ */
233
+ function InformativeLock({ children, ariaLabel, lockTitle, lockBody, newChatLabel, onNewChat, className, triggerClassName, popoverClassName, side = "top" }) {
234
+ const [open, setOpen] = React$1.useState(false);
235
+ const closeTimer = React$1.useRef(null);
236
+ const popoverRef = React$1.useRef(null);
237
+ const ref = useClickOutside(() => setOpen(false), [popoverRef]);
238
+ const cancelClose = React$1.useCallback(() => {
239
+ if (closeTimer.current) {
240
+ clearTimeout(closeTimer.current);
241
+ closeTimer.current = null;
242
+ }
243
+ }, []);
244
+ const scheduleClose = React$1.useCallback(() => {
245
+ cancelClose();
246
+ closeTimer.current = setTimeout(() => setOpen(false), 150);
247
+ }, [cancelClose]);
248
+ React$1.useEffect(() => cancelClose, [cancelClose]);
249
+ return /* @__PURE__ */ jsxs("div", {
250
+ ref,
251
+ className: cn$1("relative inline-flex", className),
252
+ onMouseEnter: () => {
253
+ cancelClose();
254
+ setOpen(true);
255
+ },
256
+ onMouseLeave: scheduleClose,
257
+ onKeyDown: (event) => {
258
+ if (event.key === "Escape") setOpen(false);
259
+ },
260
+ children: [/* @__PURE__ */ jsx("button", {
261
+ type: "button",
262
+ onClick: () => setOpen(true),
263
+ "data-state": open ? "open" : "closed",
264
+ "aria-label": ariaLabel,
265
+ "aria-haspopup": "dialog",
266
+ "aria-expanded": open,
267
+ className: cn$1("inline-flex h-8 items-center gap-1.5 rounded-lg border border-[var(--md3-outline-variant)] bg-surface-container px-2.5 opacity-60", "text-xs font-medium text-foreground shadow-sm transition-colors", "hover:border-[var(--md3-outline-variant)] hover:bg-surface-container-high", focusRing, "data-[state=open]:border-[var(--md3-outline-variant)] data-[state=open]:bg-surface-container-high", triggerClassName),
268
+ children
269
+ }), open && /* @__PURE__ */ jsxs(AnchoredPopover, {
270
+ ref: popoverRef,
271
+ anchorRef: ref,
272
+ side,
273
+ role: "dialog",
274
+ onMouseEnter: () => {
275
+ cancelClose();
276
+ setOpen(true);
277
+ },
278
+ onMouseLeave: scheduleClose,
279
+ className: cn$1("w-72 rounded-[var(--radius-md)] border border-[var(--md3-outline-variant)] bg-surface-container-highest p-3", "shadow-[0_8px_30px_rgba(0,0,0,0.45)] ring-1 ring-[#ffffff14]", popoverClassName),
280
+ children: [
281
+ /* @__PURE__ */ jsxs("p", {
282
+ className: "flex items-start gap-2 text-sm font-medium text-foreground",
283
+ children: [/* @__PURE__ */ jsx(Lock, { className: "mt-0.5 h-3.5 w-3.5 shrink-0 text-muted-foreground" }), /* @__PURE__ */ jsx("span", { children: lockTitle })]
284
+ }),
285
+ /* @__PURE__ */ jsx("p", {
286
+ className: "mt-1.5 text-xs text-muted-foreground",
287
+ children: lockBody
288
+ }),
289
+ /* @__PURE__ */ jsxs("button", {
290
+ type: "button",
291
+ onClick: () => {
292
+ setOpen(false);
293
+ onNewChat();
294
+ },
295
+ className: cn$1("mt-3 flex w-full items-center justify-center gap-1.5 rounded-md px-2.5 py-2 text-sm font-medium text-foreground", "bg-primary/10 ring-1 ring-inset ring-primary/25 transition-colors", "hover:bg-primary/15", focusRing),
296
+ children: [/* @__PURE__ */ jsx(Plus, { className: "h-4 w-4" }), newChatLabel]
297
+ })
298
+ ]
299
+ })]
300
+ });
301
+ }
302
+ //#endregion
303
+ //#region src/chat/agent-profile-picker.tsx
304
+ function profileSubtitle(profile) {
305
+ if (profile.description) return profile.description;
306
+ const count = profile.capabilities?.length ?? 0;
307
+ return `${count} ${count === 1 ? "tool group" : "tool groups"}`;
308
+ }
309
+ /**
310
+ * Agent / mode switcher styled to match the chat control strip's pills
311
+ * (harness, model, effort). The pill shows the active profile; the menu lists
312
+ * builtin modes and saved custom agents. When a capability catalog and a write
313
+ * callback are supplied it also authors agents inline (name + tool toggles +
314
+ * persona) — a self-contained click-outside popover rather than a Radix menu,
315
+ * because the inline form needs its own focus and typing without the menu
316
+ * stealing keystrokes.
317
+ */
318
+ function AgentProfilePicker({ value, onChange, profiles, capabilities, onCreate, onUpdate, onDelete, disabled, locked, lockReason, onNewChat, className, triggerClassName, popoverClassName, side = "top" }) {
319
+ const [open, setOpen] = React$1.useState(false);
320
+ const [composing, setComposing] = React$1.useState(null);
321
+ const popoverRef = React$1.useRef(null);
322
+ const ref = useClickOutside(() => {
323
+ setOpen(false);
324
+ setComposing(null);
325
+ }, [popoverRef]);
326
+ const canAuthor = Boolean(capabilities && (onCreate || onUpdate));
327
+ const builtins = profiles.filter((profile) => profile.builtin);
328
+ const custom = profiles.filter((profile) => !profile.builtin);
329
+ const selectedName = profiles.find((profile) => profile.id === value)?.name ?? value;
330
+ React$1.useEffect(() => {
331
+ if (!locked) return;
332
+ setOpen(false);
333
+ setComposing(null);
334
+ }, [locked]);
335
+ function select(id) {
336
+ onChange(id);
337
+ setOpen(false);
338
+ }
339
+ async function save(draft) {
340
+ if (draft.id) await onUpdate?.(draft.id, draft);
341
+ else await onCreate?.(draft);
342
+ setComposing(null);
343
+ }
344
+ if (locked && onNewChat) return /* @__PURE__ */ jsxs(InformativeLock, {
345
+ ariaLabel: "Agent profile (locked)",
346
+ lockTitle: `Profile fixed to ${selectedName} for this conversation.`,
347
+ lockBody: "Conversations stay on one profile.",
348
+ newChatLabel: "New chat to switch profile",
349
+ onNewChat,
350
+ className,
351
+ triggerClassName,
352
+ popoverClassName,
353
+ side,
354
+ children: [/* @__PURE__ */ jsx(Bot, { className: "h-3.5 w-3.5 text-muted-foreground" }), /* @__PURE__ */ jsx("span", {
355
+ className: "max-w-[140px] truncate",
356
+ children: selectedName
357
+ })]
358
+ });
359
+ return /* @__PURE__ */ jsxs("div", {
360
+ ref,
361
+ className: cn$1("relative inline-flex", className),
362
+ children: [/* @__PURE__ */ jsxs("button", {
363
+ type: "button",
364
+ disabled: disabled || locked,
365
+ title: locked ? lockReason : void 0,
366
+ onClick: () => setOpen((v) => !v),
367
+ className: cn$1("inline-flex h-8 items-center gap-1.5 rounded-lg border border-[var(--md3-outline-variant)] bg-surface-container px-2.5", "text-xs font-medium text-foreground shadow-sm transition-colors", "hover:border-[var(--md3-outline-variant)] hover:bg-surface-container-high", focusRing, "data-[state=open]:border-[var(--md3-outline-variant)] data-[state=open]:bg-surface-container-high", "disabled:cursor-not-allowed disabled:opacity-60", triggerClassName),
368
+ "data-state": open ? "open" : "closed",
369
+ "aria-label": "Agent profile",
370
+ children: [
371
+ /* @__PURE__ */ jsx(Bot, { className: "h-3.5 w-3.5 text-muted-foreground" }),
372
+ /* @__PURE__ */ jsx("span", {
373
+ className: "max-w-[140px] truncate",
374
+ children: selectedName
375
+ }),
376
+ !locked && /* @__PURE__ */ jsx(ChevronDown, { className: "ml-auto h-3.5 w-3.5 shrink-0 text-muted-foreground" })
377
+ ]
378
+ }), open && !locked && /* @__PURE__ */ jsx(AnchoredPopover, {
379
+ ref: popoverRef,
380
+ anchorRef: ref,
381
+ side,
382
+ className: cn$1("w-[min(320px,calc(100vw-2rem))] overflow-hidden rounded-[var(--radius-md)] border border-[var(--md3-outline-variant)] bg-surface-container-highest p-1", "shadow-[0_8px_30px_rgba(0,0,0,0.45)] ring-1 ring-[#ffffff14]", popoverClassName),
383
+ "data-testid": "agent-profile-popover",
384
+ children: composing ? /* @__PURE__ */ jsx(ProfileForm, {
385
+ capabilities: capabilities ?? [],
386
+ initial: composing === "new" ? void 0 : composing,
387
+ onCancel: () => setComposing(null),
388
+ onSave: save
389
+ }) : /* @__PURE__ */ jsxs("div", {
390
+ className: "max-h-[min(60vh,420px)] overflow-y-auto",
391
+ children: [
392
+ /* @__PURE__ */ jsx(SectionHeader, { children: "Modes" }),
393
+ builtins.map((profile) => /* @__PURE__ */ jsx(ProfileRow, {
394
+ active: profile.id === value,
395
+ title: profile.name,
396
+ subtitle: profileSubtitle(profile),
397
+ onSelect: () => select(profile.id)
398
+ }, profile.id)),
399
+ custom.length > 0 && /* @__PURE__ */ jsx(SectionHeader, { children: "Your agents" }),
400
+ custom.map((profile) => /* @__PURE__ */ jsx(ProfileRow, {
401
+ active: profile.id === value,
402
+ title: profile.name,
403
+ subtitle: profileSubtitle(profile),
404
+ onSelect: () => select(profile.id),
405
+ onEdit: canAuthor ? () => setComposing(profile) : void 0,
406
+ onDelete: onDelete ? () => void onDelete(profile.id) : void 0
407
+ }, profile.id)),
408
+ canAuthor && /* @__PURE__ */ jsxs("button", {
409
+ type: "button",
410
+ onClick: () => setComposing("new"),
411
+ className: "mt-1 flex w-full items-center gap-2 rounded-md px-2.5 py-2 text-left text-sm text-[var(--accent-text)] transition-colors hover:bg-accent/40",
412
+ children: [/* @__PURE__ */ jsx(Plus, { className: "h-4 w-4" }), " New agent…"]
413
+ })
414
+ ]
415
+ })
416
+ })]
417
+ });
418
+ }
419
+ function SectionHeader({ children }) {
420
+ return /* @__PURE__ */ jsx("div", {
421
+ className: "px-2.5 pb-1 pt-2 text-[10px] font-semibold uppercase tracking-wide text-muted-foreground/70",
422
+ children
423
+ });
424
+ }
425
+ function ProfileRow({ active, title, subtitle, onSelect, onEdit, onDelete }) {
426
+ return /* @__PURE__ */ jsxs("div", {
427
+ className: cn$1("group flex items-center gap-2 rounded-md px-2.5 py-2 text-sm transition-colors", active ? "bg-[var(--accent-surface-soft)] text-[var(--accent-text)]" : "hover:bg-accent/40"),
428
+ children: [/* @__PURE__ */ jsxs("button", {
429
+ type: "button",
430
+ onClick: onSelect,
431
+ className: "flex min-w-0 flex-1 items-center gap-2 text-left",
432
+ children: [/* @__PURE__ */ jsx("span", {
433
+ className: "flex h-4 w-4 shrink-0 items-center justify-center",
434
+ children: active && /* @__PURE__ */ jsx(Check, { className: "h-3.5 w-3.5" })
435
+ }), /* @__PURE__ */ jsxs("span", {
436
+ className: "min-w-0",
437
+ children: [/* @__PURE__ */ jsx("span", {
438
+ className: "block truncate font-medium",
439
+ children: title
440
+ }), /* @__PURE__ */ jsx("span", {
441
+ className: "block truncate text-xs text-muted-foreground",
442
+ children: subtitle
443
+ })]
444
+ })]
445
+ }), (onEdit || onDelete) && /* @__PURE__ */ jsxs("span", {
446
+ className: "flex shrink-0 items-center gap-1 opacity-0 transition-opacity group-hover:opacity-100",
447
+ children: [onEdit && /* @__PURE__ */ jsx("button", {
448
+ type: "button",
449
+ onClick: onEdit,
450
+ title: "Edit",
451
+ className: "rounded p-1 text-muted-foreground hover:text-foreground",
452
+ children: /* @__PURE__ */ jsx(Pencil, { className: "h-3 w-3" })
453
+ }), onDelete && /* @__PURE__ */ jsx("button", {
454
+ type: "button",
455
+ onClick: onDelete,
456
+ title: "Delete",
457
+ className: "rounded p-1 text-muted-foreground hover:text-[var(--status-error,#ff4d6d)]",
458
+ children: /* @__PURE__ */ jsx(Trash2, { className: "h-3 w-3" })
459
+ })]
460
+ })]
461
+ });
462
+ }
463
+ function ProfileForm({ capabilities, initial, onCancel, onSave }) {
464
+ const [name, setName] = React$1.useState(initial?.name ?? "");
465
+ const [caps, setCaps] = React$1.useState(initial?.capabilities ? [...initial.capabilities] : capabilities.map((c) => c.id));
466
+ const [instructions, setInstructions] = React$1.useState(initial?.instructions ?? "");
467
+ const toggle = (id) => setCaps((prev) => prev.includes(id) ? prev.filter((c) => c !== id) : [...prev, id]);
468
+ return /* @__PURE__ */ jsxs("div", {
469
+ className: "space-y-3 p-2.5",
470
+ children: [
471
+ /* @__PURE__ */ jsx("input", {
472
+ autoFocus: true,
473
+ value: name,
474
+ onChange: (event) => setName(event.target.value),
475
+ placeholder: "Agent name (e.g. Tax Reviewer)",
476
+ className: `w-full rounded-lg border bg-surface-container px-3 py-2 text-sm ${focusField}`
477
+ }),
478
+ capabilities.length > 0 && /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("p", {
479
+ className: "mb-1.5 text-[10px] font-semibold uppercase tracking-wide text-muted-foreground/70",
480
+ children: "Tools"
481
+ }), /* @__PURE__ */ jsx("div", {
482
+ className: "space-y-0.5",
483
+ children: capabilities.map((cap) => /* @__PURE__ */ jsxs("label", {
484
+ className: "flex cursor-pointer items-start gap-2 rounded-md px-2 py-1.5 hover:bg-accent/40",
485
+ children: [/* @__PURE__ */ jsx("input", {
486
+ type: "checkbox",
487
+ checked: caps.includes(cap.id),
488
+ onChange: () => toggle(cap.id),
489
+ className: "mt-0.5"
490
+ }), /* @__PURE__ */ jsxs("span", {
491
+ className: "min-w-0",
492
+ children: [/* @__PURE__ */ jsx("span", {
493
+ className: "block text-sm font-medium",
494
+ children: cap.label
495
+ }), cap.description && /* @__PURE__ */ jsx("span", {
496
+ className: "block text-xs text-muted-foreground",
497
+ children: cap.description
498
+ })]
499
+ })]
500
+ }, cap.id))
501
+ })] }),
502
+ /* @__PURE__ */ jsx("textarea", {
503
+ value: instructions,
504
+ onChange: (event) => setInstructions(event.target.value),
505
+ rows: 2,
506
+ placeholder: "Instructions (optional) — how this agent should behave",
507
+ className: `w-full resize-none rounded-lg border bg-surface-container px-3 py-2 text-sm ${focusField}`
508
+ }),
509
+ /* @__PURE__ */ jsxs("div", {
510
+ className: "flex justify-end gap-2",
511
+ children: [/* @__PURE__ */ jsx("button", {
512
+ type: "button",
513
+ onClick: onCancel,
514
+ className: "rounded-lg px-3 py-1.5 text-sm text-muted-foreground hover:bg-accent/40",
515
+ children: "Cancel"
516
+ }), /* @__PURE__ */ jsx("button", {
517
+ type: "button",
518
+ disabled: !name.trim(),
519
+ onClick: () => onSave({
520
+ id: initial?.id,
521
+ name: name.trim(),
522
+ capabilities: caps,
523
+ instructions: instructions.trim()
524
+ }),
525
+ className: "rounded-lg bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground transition-opacity hover:opacity-90 disabled:opacity-40",
526
+ children: "Save agent"
527
+ })]
528
+ })
529
+ ]
530
+ });
531
+ }
532
+ //#endregion
533
+ //#region src/chat/artifact-agent-dock.tsx
534
+ function defaultScopeLabel(scope) {
535
+ if (scope.label) return scope.label;
536
+ switch (scope.kind) {
537
+ case "vault-file": return scope.key.split("/").pop() ?? scope.key;
538
+ case "sequence": return `Sequence ${scope.key.slice(0, 8)}`;
539
+ case "content": return `Content ${scope.key.slice(0, 8)}`;
540
+ case "image": return `Image ${scope.key.slice(0, 8)}`;
541
+ case "video": return `Video ${scope.key.slice(0, 8)}`;
542
+ case "project": return "Project chat";
543
+ case "workspace": return "Workspace chat";
544
+ default: return scope.key;
545
+ }
546
+ }
547
+ function normalizeRole(role) {
548
+ return role === "assistant" || role === "system" ? role : "user";
549
+ }
550
+ /** Stable client-side message id. Uses `crypto.randomUUID()` when available,
551
+ * else a UUIDv4 from `getRandomValues`. Avoids `Date.now()`, which collides
552
+ * when a user+assistant pair are created in the same millisecond. */
553
+ function createDockMessageId() {
554
+ const c = typeof globalThis !== "undefined" ? globalThis.crypto : void 0;
555
+ if (c && typeof c.randomUUID === "function") return c.randomUUID();
556
+ if (c && typeof c.getRandomValues === "function") {
557
+ const b = c.getRandomValues(/* @__PURE__ */ new Uint8Array(16));
558
+ b[6] = b[6] & 15 | 64;
559
+ b[8] = b[8] & 63 | 128;
560
+ const h = Array.from(b, (x) => x.toString(16).padStart(2, "0")).join("");
561
+ return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20)}`;
562
+ }
563
+ throw new Error("crypto.getRandomValues is required for message ids");
564
+ }
565
+ /**
566
+ * ArtifactAgentDock — slide-in right-rail chat scoped to a single artifact.
567
+ *
568
+ * Renders a persistent, per-artifact conversation surface that streams via a
569
+ * consumer-supplied transport. Mount alongside any artifact view (file editor,
570
+ * sequence editor, asset preview) to give every artifact its own dedicated
571
+ * agent conversation without forking the chat UI per app.
572
+ *
573
+ * Behavior:
574
+ * - On open, calls `transport.ensureThread(scope)` to get the canonical thread
575
+ * id and `transport.loadMessages(threadId)` to populate prior turns.
576
+ * - On submit, calls `transport.sendStream(...)` and concatenates delta events
577
+ * into a single streaming assistant message; renders inline.
578
+ * - Composer: Enter sends, Shift+Enter newline, Cancel during stream supported.
579
+ * - Accessibility: scope `aria-label` on the panel, `aria-pressed` is the
580
+ * consumer's job on whatever button toggles `open`.
581
+ */
582
+ function ArtifactAgentDock({ scope, open, onOpenChange, transport, defaultPrompt, onError, className }) {
583
+ const [threadId, setThreadId] = React$1.useState(null);
584
+ const [messages, setMessages] = React$1.useState([]);
585
+ const [composer, setComposer] = React$1.useState(defaultPrompt ?? "");
586
+ const [streamContent, setStreamContent] = React$1.useState("");
587
+ const [sending, setSending] = React$1.useState(false);
588
+ const [loading, setLoading] = React$1.useState(false);
589
+ const abortRef = React$1.useRef(null);
590
+ const scrollRef = React$1.useRef(null);
591
+ const scopeTokenRef = React$1.useRef(0);
592
+ const reportError = React$1.useCallback((message) => {
593
+ if (onError) onError(message);
594
+ else if (typeof console !== "undefined") console.error("[ArtifactAgentDock]", message);
595
+ }, [onError]);
596
+ React$1.useEffect(() => {
597
+ if (!open) return;
598
+ scopeTokenRef.current += 1;
599
+ abortRef.current?.abort();
600
+ abortRef.current = null;
601
+ let cancelled = false;
602
+ async function bootstrap() {
603
+ setLoading(true);
604
+ try {
605
+ const { id } = await transport.ensureThread(scope);
606
+ if (cancelled) return;
607
+ setThreadId(id);
608
+ if (transport.loadMessages) {
609
+ const prior = await transport.loadMessages(id);
610
+ if (cancelled) return;
611
+ setMessages(prior.map((m) => ({
612
+ ...m,
613
+ role: normalizeRole(m.role),
614
+ createdAt: m.createdAt instanceof Date ? m.createdAt : m.createdAt ? new Date(m.createdAt) : void 0
615
+ })));
616
+ } else setMessages([]);
617
+ } catch (err) {
618
+ reportError(err instanceof Error ? err.message : "Failed to open artifact chat");
619
+ } finally {
620
+ if (!cancelled) setLoading(false);
621
+ }
622
+ }
623
+ bootstrap();
624
+ return () => {
625
+ cancelled = true;
626
+ };
627
+ }, [
628
+ open,
629
+ scope.kind,
630
+ scope.key,
631
+ transport,
632
+ reportError
633
+ ]);
634
+ React$1.useEffect(() => {
635
+ if (!scrollRef.current) return;
636
+ scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
637
+ }, [messages.length, streamContent]);
638
+ const send = React$1.useCallback(async () => {
639
+ const text = composer.trim();
640
+ if (!text || !threadId || sending) return;
641
+ const token = scopeTokenRef.current;
642
+ const userMsg = {
643
+ id: createDockMessageId(),
644
+ role: "user",
645
+ content: text,
646
+ createdAt: /* @__PURE__ */ new Date()
647
+ };
648
+ setMessages((prev) => [...prev, userMsg]);
649
+ setComposer("");
650
+ setSending(true);
651
+ setStreamContent("");
652
+ const controller = new AbortController();
653
+ abortRef.current = controller;
654
+ let assistantContent = "";
655
+ let streamFailed = false;
656
+ try {
657
+ for await (const event of transport.sendStream({
658
+ threadId,
659
+ content: text,
660
+ signal: controller.signal
661
+ })) {
662
+ if (scopeTokenRef.current !== token) return;
663
+ if (event.type === "delta") {
664
+ assistantContent += event.text;
665
+ setStreamContent(assistantContent);
666
+ } else if (event.type === "error") {
667
+ streamFailed = true;
668
+ reportError(event.message);
669
+ }
670
+ }
671
+ if (scopeTokenRef.current !== token) return;
672
+ if (assistantContent && !streamFailed) setMessages((prev) => [...prev, {
673
+ id: createDockMessageId(),
674
+ role: "assistant",
675
+ content: assistantContent,
676
+ createdAt: /* @__PURE__ */ new Date()
677
+ }]);
678
+ setStreamContent("");
679
+ } catch (err) {
680
+ if (err?.name === "AbortError") return;
681
+ if (scopeTokenRef.current === token) reportError(err instanceof Error ? err.message : "Send failed");
682
+ } finally {
683
+ if (scopeTokenRef.current === token) setSending(false);
684
+ if (abortRef.current === controller) abortRef.current = null;
685
+ }
686
+ }, [
687
+ composer,
688
+ threadId,
689
+ sending,
690
+ transport,
691
+ reportError
692
+ ]);
693
+ const cancel = React$1.useCallback(() => {
694
+ if (threadId && transport.cancel) try {
695
+ Promise.resolve(transport.cancel({ threadId })).catch(() => {});
696
+ } catch {}
697
+ abortRef.current?.abort();
698
+ setSending(false);
699
+ setStreamContent("");
700
+ }, [threadId, transport]);
701
+ if (!open) return null;
702
+ const heading = defaultScopeLabel(scope);
703
+ return /* @__PURE__ */ jsxs("aside", {
704
+ className: cn$1("flex h-full w-[420px] min-w-[360px] max-w-[480px] flex-col border-l border-[var(--md3-outline-variant)] bg-surface-container shadow-[var(--shadow-dropdown)]", className),
705
+ "aria-label": `Agent chat about ${heading}`,
706
+ children: [
707
+ /* @__PURE__ */ jsxs("header", {
708
+ className: "flex items-center justify-between border-b border-[var(--md3-outline-variant)] bg-surface-container-low px-4 py-3",
709
+ children: [/* @__PURE__ */ jsxs("div", {
710
+ className: "flex min-w-0 items-center gap-2",
711
+ children: [/* @__PURE__ */ jsx(Sparkles, { className: "h-4 w-4 shrink-0 text-muted-foreground" }), /* @__PURE__ */ jsxs("div", {
712
+ className: "min-w-0",
713
+ children: [/* @__PURE__ */ jsx("div", {
714
+ className: "text-[10px] font-medium uppercase tracking-wider text-muted-foreground",
715
+ children: "Discussing"
716
+ }), /* @__PURE__ */ jsx("div", {
717
+ className: "truncate text-sm font-semibold text-foreground",
718
+ children: heading
719
+ })]
720
+ })]
721
+ }), /* @__PURE__ */ jsx("button", {
722
+ type: "button",
723
+ onClick: () => onOpenChange(false),
724
+ "aria-label": "Close artifact chat",
725
+ className: "rounded-md p-1 text-muted-foreground transition-colors hover:bg-surface-container-high hover:text-foreground",
726
+ children: /* @__PURE__ */ jsx(X, { className: "h-4 w-4" })
727
+ })]
728
+ }),
729
+ /* @__PURE__ */ jsx("div", {
730
+ ref: scrollRef,
731
+ className: "flex-1 overflow-y-auto px-3 py-4",
732
+ children: loading ? /* @__PURE__ */ jsx("div", {
733
+ className: "px-3 py-8 text-center text-xs text-muted-foreground",
734
+ children: "Loading conversation…"
735
+ }) : messages.length === 0 && !streamContent ? /* @__PURE__ */ jsxs("div", {
736
+ className: "px-3 py-12 text-center",
737
+ children: [
738
+ /* @__PURE__ */ jsx(Sparkles, { className: "mx-auto mb-3 h-5 w-5 text-muted-foreground/40" }),
739
+ /* @__PURE__ */ jsxs("p", {
740
+ className: "text-sm text-foreground",
741
+ children: [
742
+ "Ask the agent about this ",
743
+ scope.kind === "vault-file" ? "document" : scope.kind,
744
+ "."
745
+ ]
746
+ }),
747
+ /* @__PURE__ */ jsx("p", {
748
+ className: "mt-1 text-xs text-muted-foreground",
749
+ children: "It can read the current contents and propose edits, rewrites, or follow-ups."
750
+ })
751
+ ]
752
+ }) : /* @__PURE__ */ jsxs("div", {
753
+ className: "space-y-3",
754
+ children: [messages.map((m) => /* @__PURE__ */ jsx(ChatMessage, {
755
+ role: m.role,
756
+ content: m.content,
757
+ timestamp: m.createdAt
758
+ }, m.id)), streamContent && /* @__PURE__ */ jsx(ChatMessage, {
759
+ role: "assistant",
760
+ content: streamContent,
761
+ isStreaming: true
762
+ })]
763
+ })
764
+ }),
765
+ /* @__PURE__ */ jsx("form", {
766
+ className: "border-t border-[var(--md3-outline-variant)] bg-surface-container-low p-3",
767
+ onSubmit: (e) => {
768
+ e.preventDefault();
769
+ send();
770
+ },
771
+ children: /* @__PURE__ */ jsxs("div", {
772
+ className: "flex items-end gap-2",
773
+ children: [/* @__PURE__ */ jsx("textarea", {
774
+ value: composer,
775
+ onChange: (e) => setComposer(e.target.value),
776
+ onKeyDown: (e) => {
777
+ if (e.key === "Enter" && !e.shiftKey && !sending) {
778
+ e.preventDefault();
779
+ send();
780
+ }
781
+ },
782
+ placeholder: `Ask about ${heading}…`,
783
+ rows: 2,
784
+ disabled: !threadId,
785
+ className: `min-h-[44px] flex-1 resize-none rounded-[20px] border bg-surface-dim px-3.5 py-2 text-sm text-foreground placeholder:text-muted-foreground disabled:opacity-50 ${focusField}`
786
+ }), sending ? /* @__PURE__ */ jsx("button", {
787
+ type: "button",
788
+ onClick: cancel,
789
+ "aria-label": "Cancel",
790
+ className: "inline-flex h-9 w-9 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-surface-container-high hover:text-foreground",
791
+ children: /* @__PURE__ */ jsx(Square, { className: "h-4 w-4" })
792
+ }) : /* @__PURE__ */ jsx("button", {
793
+ type: "submit",
794
+ disabled: !composer.trim() || !threadId,
795
+ "aria-label": "Send",
796
+ className: "inline-flex h-9 w-9 items-center justify-center rounded-full bg-primary text-primary-foreground transition-colors hover:bg-primary/90 disabled:opacity-50",
797
+ children: /* @__PURE__ */ jsx(Send, { className: "h-4 w-4" })
798
+ })]
799
+ })
800
+ })
801
+ ]
802
+ });
803
+ }
804
+ /**
805
+ * Build a transport that hits the Tangle product convention: POST
806
+ * /api/threads with body.scope = ArtifactScope to ensure the canonical
807
+ * thread; GET /api/threads/:id/messages to load history; POST /api/chat
808
+ * with body = {workspaceId, threadId, content} and an NDJSON event stream
809
+ * to send. Most consumers can use this as-is; advanced ones can supply
810
+ * their own transport object.
811
+ *
812
+ * The expected NDJSON event shape:
813
+ * { type: "message.part.updated", data: { delta: string, part?: {type} } } // assistant chunk
814
+ * { type: "error", data: { message: string, ... } } // surface error
815
+ *
816
+ * Other event types are ignored — consumers that need tool-call rendering
817
+ * or other event semantics should ship a richer transport.
818
+ */
819
+ function createFetchTransport(opts) {
820
+ const threadsUrl = opts.threadsUrl ?? "/api/threads";
821
+ const chatUrl = opts.chatUrl ?? "/api/chat";
822
+ const f = opts.fetchImpl ?? ((...args) => fetch(...args));
823
+ const workspaceId = opts.workspaceId;
824
+ return {
825
+ async ensureThread(scope) {
826
+ const url = threadsUrl.includes("?") ? `${threadsUrl}&workspaceId=${encodeURIComponent(workspaceId)}` : `${threadsUrl}?workspaceId=${encodeURIComponent(workspaceId)}`;
827
+ const res = await f(url, {
828
+ method: "POST",
829
+ headers: { "Content-Type": "application/json" },
830
+ body: JSON.stringify({
831
+ workspaceId,
832
+ scope,
833
+ ensureScoped: true
834
+ })
835
+ });
836
+ if (!res.ok) throw new Error(`Thread ensure failed (${res.status})`);
837
+ const data = await res.json();
838
+ const id = data.thread?.id ?? data.id;
839
+ if (!id) throw new Error("Malformed ensure response");
840
+ return { id };
841
+ },
842
+ async loadMessages(threadId) {
843
+ const res = await f(`${threadsUrl}/${encodeURIComponent(threadId)}/messages`);
844
+ if (!res.ok) return [];
845
+ return ((await res.json()).messages ?? []).map((m) => ({
846
+ id: m.id,
847
+ role: normalizeRole(m.role),
848
+ content: m.content,
849
+ createdAt: new Date(m.createdAt)
850
+ }));
851
+ },
852
+ sendStream({ threadId, content, signal }) {
853
+ return (async function* () {
854
+ const res = await f(chatUrl, {
855
+ method: "POST",
856
+ headers: { "Content-Type": "application/json" },
857
+ body: JSON.stringify({
858
+ workspaceId,
859
+ threadId,
860
+ content
861
+ }),
862
+ signal
863
+ });
864
+ if (!res.ok || !res.body) {
865
+ yield {
866
+ type: "error",
867
+ message: await res.text().catch(() => "") || `Send failed (${res.status})`
868
+ };
869
+ return;
870
+ }
871
+ const reader = res.body.getReader();
872
+ const decoder = new TextDecoder();
873
+ let buffer = "";
874
+ while (true) {
875
+ const { done, value } = await reader.read();
876
+ if (done) break;
877
+ buffer += decoder.decode(value, { stream: true });
878
+ const lines = buffer.split("\n");
879
+ buffer = lines.pop() ?? "";
880
+ for (const line of lines) {
881
+ if (!line.trim()) continue;
882
+ try {
883
+ const event = JSON.parse(line);
884
+ if (event.type === "message.part.updated" && event.data) {
885
+ const delta = event.data.delta;
886
+ const part = event.data.part;
887
+ if ((!part || typeof part !== "object" || part.type === void 0 || part.type === "text") && typeof delta === "string" && delta.length > 0) yield {
888
+ type: "delta",
889
+ text: delta
890
+ };
891
+ } else if (event.type === "error" && event.data) yield {
892
+ type: "error",
893
+ message: typeof event.data.message === "string" ? event.data.message : "Stream error"
894
+ };
895
+ } catch {}
896
+ }
897
+ }
898
+ })();
899
+ }
900
+ };
901
+ }
902
+ //#endregion
903
+ export { ThinkingIndicator as a, createFetchTransport as c, AgentProfilePicker as d, DEFAULT_REASONING_LEVEL_OPTIONS as f, MessageList as i, modelProvider as l, ReasoningLevelPicker as m, ChatContainer$1 as n, UserMessage as o, HARNESS_REASONING_OPTIONS as p, ChatMessage$1 as r, ArtifactAgentDock as s, AgentTimeline as t, snapHarnessToModel as u };