@tangle-network/sandbox-ui 0.113.7 → 0.115.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.
- package/README.md +40 -0
- package/dist/WorkflowGraph-B-NsdYz_.js +1961 -0
- package/dist/assets.d.ts +214 -206
- package/dist/assets.js +1057 -908
- package/dist/auth-CWe17Hly.js +37 -0
- package/dist/auth.d.ts +2 -30
- package/dist/auth.js +2 -15
- package/dist/chat-BOvFAe6p.js +903 -0
- package/dist/chat.d.ts +2 -230
- package/dist/chat.js +2 -34
- package/dist/connectors.d.ts +52 -49
- package/dist/connectors.js +595 -643
- package/dist/dashboard-CZ3VgNPR.js +4969 -0
- package/dist/dashboard.d.ts +4 -1297
- package/dist/dashboard.js +4 -152
- package/dist/editor.d.ts +2 -1
- package/dist/editor.js +2 -31
- package/dist/files.d.ts +2 -6
- package/dist/files.js +2 -33
- package/dist/globals.css +75 -3
- package/dist/hooks.d.ts +146 -148
- package/dist/hooks.js +222 -55
- package/dist/index-CKRaj1pM.d.ts +776 -0
- package/dist/index-CXX5IpH2.d.ts +2 -0
- package/dist/index-CesPlOyx.d.ts +367 -0
- package/dist/index-DPePq7c8.d.ts +1340 -0
- package/dist/index-DiyLLZuJ.d.ts +230 -0
- package/dist/index-DvmZLMT1.d.ts +30 -0
- package/dist/index-PRM636Ex.d.ts +86 -0
- package/dist/index.d.ts +21 -33
- package/dist/index.js +21 -701
- package/dist/info-panel-C9cqDzOL.js +594 -0
- package/dist/integrations.d.ts +133 -141
- package/dist/integrations.js +491 -673
- package/dist/markdown.d.ts +2 -1
- package/dist/markdown.js +2 -13
- package/dist/model-I-NIxeM-.js +1250 -0
- package/dist/model-brand-B9A4H9pJ.d.ts +98 -0
- package/dist/model-brand-hMyDTUqB.js +624 -0
- package/dist/motion-DkyBRJb9.js +37 -0
- package/dist/openui.d.ts +2 -1
- package/dist/openui.js +2 -7
- package/dist/pages.d.ts +307 -300
- package/dist/pages.js +4404 -4181
- package/dist/primitives-Dh1vG6J3.js +113 -0
- package/dist/primitives.d.ts +2 -74
- package/dist/primitives.js +2 -204
- package/dist/provider-logo-DjqmConF.js +311 -0
- package/dist/run.d.ts +2 -1
- package/dist/run.js +2 -23
- package/dist/sdk-hooks.d.ts +3 -4
- package/dist/sdk-hooks.js +3 -31
- package/dist/sidecar-DgH_vQ6m.d.ts +9 -0
- package/dist/stores.d.ts +14 -15
- package/dist/stores.js +13 -81
- package/dist/styles.css +75 -3
- package/dist/template-card-L-4gmjx2.d.ts +90 -0
- package/dist/terminal-CN9ZgHRt.js +197 -0
- package/dist/terminal.d.ts +60 -60
- package/dist/terminal.js +2 -10
- package/dist/tokens.css +51 -0
- package/dist/types.d.ts +4 -11
- package/dist/types.js +0 -1
- package/dist/use-pty-session-C8cE0adT.js +669 -0
- package/dist/use-sidecar-auth-CCTJcrq7.d.ts +145 -0
- package/dist/use-sidecar-auth-DkdcufOz.js +742 -0
- package/dist/utils-DzWbktWG.js +2 -0
- package/dist/utils.d.ts +2 -1
- package/dist/utils.js +2 -37
- package/dist/workbench-ztU44Jt2.js +8455 -0
- package/dist/workbench.d.ts +2 -310
- package/dist/workbench.js +2 -44
- package/dist/workflows.d.ts +341 -339
- package/dist/workflows.js +74 -109
- package/dist/workspace-B6AtI6ld.js +2510 -0
- package/dist/workspace.d.ts +2 -761
- package/dist/workspace.js +2 -51
- package/package.json +46 -40
- package/dist/WorkflowGraph-2QOORYIR.js +0 -1722
- package/dist/chunk-2BUPSB7O.js +0 -0
- package/dist/chunk-3KBNPQQF.js +0 -2697
- package/dist/chunk-3LIXVEEE.js +0 -279
- package/dist/chunk-72UF2H5H.js +0 -707
- package/dist/chunk-AI25G5MX.js +0 -944
- package/dist/chunk-ANSC5AFC.js +0 -314
- package/dist/chunk-AZ3AWMTM.js +0 -8
- package/dist/chunk-BGVEHENC.js +0 -10
- package/dist/chunk-CZ7CSFO4.js +0 -39
- package/dist/chunk-DYXXYGS4.js +0 -4700
- package/dist/chunk-EI44GEQ5.js +0 -6
- package/dist/chunk-IACRDXER.js +0 -219
- package/dist/chunk-IOB2PW5Z.js +0 -55
- package/dist/chunk-M4K7ZGR2.js +0 -24
- package/dist/chunk-MYZAW4RX.js +0 -665
- package/dist/chunk-OMUIPJQZ.js +0 -555
- package/dist/chunk-RQZRXKMR.js +0 -398
- package/dist/chunk-SOKKTB7W.js +0 -230
- package/dist/chunk-TADGS4Q7.js +0 -1010
- package/dist/chunk-UCQNOUN3.js +0 -7964
- package/dist/chunk-ULPLRBMF.js +0 -34
- package/dist/chunk-WID73FPH.js +0 -89
- package/dist/chunk-X3YXP7FG.js +0 -38
- package/dist/logo-DiAjq759.d.ts +0 -15
- package/dist/model-brand-BNnaUKKZ.d.ts +0 -99
- package/dist/ports-list-CDIvTNnX.d.ts +0 -17
- package/dist/sdk-hooks-Djkjq4cp.d.ts +0 -145
- 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 };
|