sideagent 0.0.0-stage → 0.1.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/dist/index.js ADDED
@@ -0,0 +1,742 @@
1
+ // src/client/AgentChat.tsx
2
+ import {
3
+ AssistantRuntimeProvider,
4
+ ComposerPrimitive,
5
+ MessagePrimitive,
6
+ ThreadPrimitive,
7
+ useLocalRuntime
8
+ } from "@assistant-ui/react";
9
+ import {
10
+ CheckCircledIcon,
11
+ ChatBubbleIcon,
12
+ ChevronRightIcon,
13
+ ArrowLeftIcon,
14
+ ArrowUpIcon,
15
+ CounterClockwiseClockIcon,
16
+ Cross2Icon,
17
+ CrossCircledIcon,
18
+ ExternalLinkIcon,
19
+ FileTextIcon,
20
+ ExclamationTriangleIcon,
21
+ OpenInNewWindowIcon,
22
+ PlusIcon,
23
+ ReloadIcon,
24
+ TrashIcon,
25
+ ViewVerticalIcon
26
+ } from "@radix-ui/react-icons";
27
+ import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
28
+
29
+ // src/client/hooks.ts
30
+ function safely(fn) {
31
+ try {
32
+ fn?.();
33
+ } catch (err) {
34
+ console.error("[sideagent] hook threw:", err);
35
+ }
36
+ }
37
+
38
+ // src/client/results.tsx
39
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
40
+ var MAX_ROWS = 50;
41
+ var MAX_COLS = 8;
42
+ var MAX_CELL = 80;
43
+ function humanize(key) {
44
+ const s = key.replace(/[_-]+/g, " ").replace(/([a-z0-9])([A-Z])/g, "$1 $2").trim().toLowerCase();
45
+ return s.charAt(0).toUpperCase() + s.slice(1);
46
+ }
47
+ var ISO = /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}(:\d{2}(\.\d+)?)?(Z|[+-]\d{2}:?\d{2})?)?$/;
48
+ function formatCell(v) {
49
+ if (v === null || v === void 0) return "\u2014";
50
+ if (typeof v === "boolean") return v ? "Yes" : "No";
51
+ if (typeof v === "number") return Number.isFinite(v) ? v.toLocaleString() : String(v);
52
+ if (typeof v === "string") {
53
+ if (ISO.test(v)) {
54
+ const d = new Date(v);
55
+ if (!Number.isNaN(d.getTime())) return v.length <= 10 ? d.toLocaleDateString() : d.toLocaleString();
56
+ }
57
+ return v.length > MAX_CELL ? `${v.slice(0, MAX_CELL)}\u2026` : v;
58
+ }
59
+ const j = JSON.stringify(v);
60
+ return j.length > MAX_CELL ? `${j.slice(0, MAX_CELL)}\u2026` : j;
61
+ }
62
+ var isRecord = (v) => !!v && typeof v === "object" && !Array.isArray(v);
63
+ var isPrimitive = (v) => v === null || typeof v !== "object";
64
+ function Table({ rows }) {
65
+ const keys = [];
66
+ for (const r of rows) for (const k of Object.keys(r)) if (!keys.includes(k)) keys.push(k);
67
+ const cols = keys.slice(0, MAX_COLS);
68
+ const shown = rows.slice(0, MAX_ROWS);
69
+ const numeric = new Set(cols.filter((c) => shown.every((r) => r[c] == null || typeof r[c] === "number")));
70
+ return /* @__PURE__ */ jsxs("div", { className: "sa-result", children: [
71
+ /* @__PURE__ */ jsx("div", { className: "sa-table-wrap", children: /* @__PURE__ */ jsxs("table", { className: "sa-table", children: [
72
+ /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsx("tr", { children: cols.map((c) => /* @__PURE__ */ jsx("th", { className: numeric.has(c) ? "sa-num" : void 0, scope: "col", children: humanize(c) }, c)) }) }),
73
+ /* @__PURE__ */ jsx("tbody", { children: shown.map((r, i) => /* @__PURE__ */ jsx("tr", { children: cols.map((c) => /* @__PURE__ */ jsx("td", { className: numeric.has(c) ? "sa-num" : void 0, children: formatCell(r[c]) }, c)) }, i)) })
74
+ ] }) }),
75
+ (rows.length > shown.length || keys.length > cols.length) && /* @__PURE__ */ jsxs("div", { className: "sa-more", children: [
76
+ rows.length > shown.length && `+${rows.length - shown.length} more rows. `,
77
+ keys.length > cols.length && `+${keys.length - cols.length} more columns.`
78
+ ] })
79
+ ] });
80
+ }
81
+ function Fields({ obj }) {
82
+ const entries = Object.entries(obj).slice(0, 30);
83
+ return /* @__PURE__ */ jsx("dl", { className: "sa-fields", children: entries.map(([k, v]) => /* @__PURE__ */ jsxs("div", { className: "sa-field", children: [
84
+ /* @__PURE__ */ jsx("dt", { children: humanize(k) }),
85
+ /* @__PURE__ */ jsx("dd", { children: formatCell(v) })
86
+ ] }, k)) });
87
+ }
88
+ function ResultView({ result }) {
89
+ if (result === null || result === void 0 || Array.isArray(result) && result.length === 0) {
90
+ return /* @__PURE__ */ jsx("div", { className: "sa-more", children: "No results." });
91
+ }
92
+ if (isRecord(result) && result.rejected === true) return /* @__PURE__ */ jsx("div", { className: "sa-more", children: "You rejected this action." });
93
+ if (isRecord(result) && typeof result.error === "string") return /* @__PURE__ */ jsx("div", { className: "sa-error", children: result.error });
94
+ if (Array.isArray(result)) {
95
+ if (result.every(isRecord)) return /* @__PURE__ */ jsx(Table, { rows: result });
96
+ if (result.every(isPrimitive)) {
97
+ return /* @__PURE__ */ jsx("ul", { className: "sa-list", children: result.slice(0, MAX_ROWS).map((v, i) => /* @__PURE__ */ jsx("li", { children: formatCell(v) }, i)) });
98
+ }
99
+ }
100
+ if (isRecord(result)) {
101
+ const arrays = Object.entries(result).filter(([, v]) => Array.isArray(v) && v.length > 0 && v.every(isRecord));
102
+ if (arrays.length === 1) {
103
+ const rest = Object.fromEntries(Object.entries(result).filter(([k]) => k !== arrays[0][0]));
104
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
105
+ Object.keys(rest).length > 0 && /* @__PURE__ */ jsx(Fields, { obj: rest }),
106
+ /* @__PURE__ */ jsx(Table, { rows: arrays[0][1] })
107
+ ] });
108
+ }
109
+ return /* @__PURE__ */ jsx(Fields, { obj: result });
110
+ }
111
+ return /* @__PURE__ */ jsx("div", { children: formatCell(result) });
112
+ }
113
+
114
+ // src/client/styles.ts
115
+ var CSS = `
116
+ .sa-root{
117
+ --sa-bg:var(--sideagent-bg,#ffffff);
118
+ --sa-fg:var(--sideagent-fg,#171717);
119
+ --sa-muted:var(--sideagent-muted,#6b6b6b);
120
+ --sa-border:var(--sideagent-border,#e5e5e5);
121
+ --sa-surface:var(--sideagent-surface,#f5f5f5);
122
+ --sa-accent:var(--sideagent-accent,#171717);
123
+ --sa-accent-fg:var(--sideagent-accent-fg,#ffffff);
124
+ --sa-danger:var(--sideagent-danger,#b42318);
125
+ --sa-radius:var(--sideagent-radius,10px);
126
+ --sa-font:var(--sideagent-font,inherit);
127
+ color-scheme:light;
128
+ }
129
+ @media (prefers-color-scheme:dark){
130
+ .sa-root[data-theme=auto]{
131
+ --sa-bg:var(--sideagent-bg,#171717);--sa-fg:var(--sideagent-fg,#f5f5f5);--sa-muted:var(--sideagent-muted,#a3a3a3);
132
+ --sa-border:var(--sideagent-border,#2e2e2e);--sa-surface:var(--sideagent-surface,#262626);
133
+ --sa-accent:var(--sideagent-accent,#f5f5f5);--sa-accent-fg:var(--sideagent-accent-fg,#171717);color-scheme:dark}
134
+ }
135
+ :is(.dark,[data-theme=dark],[data-bs-theme=dark]) .sa-root[data-theme=auto],
136
+ .sa-root[data-theme=dark]{
137
+ --sa-bg:var(--sideagent-bg,#171717);--sa-fg:var(--sideagent-fg,#f5f5f5);--sa-muted:var(--sideagent-muted,#a3a3a3);
138
+ --sa-border:var(--sideagent-border,#2e2e2e);--sa-surface:var(--sideagent-surface,#262626);
139
+ --sa-accent:var(--sideagent-accent,#f5f5f5);--sa-accent-fg:var(--sideagent-accent-fg,#171717);color-scheme:dark}
140
+ .sa-root{font-family:var(--sa-font);font-size:14px;line-height:1.5;color:var(--sa-fg);box-sizing:border-box;-webkit-font-smoothing:antialiased}
141
+ .sa-root *{box-sizing:border-box}
142
+ .sa-panel{display:flex;flex-direction:column;background:var(--sa-bg);border:1px solid var(--sa-border);overflow:hidden}
143
+ .sa-floating .sa-panel{position:fixed;right:16px;bottom:80px;width:min(400px,calc(100vw - 32px));height:min(600px,calc(100vh - 104px));border-radius:calc(var(--sa-radius) + 4px);box-shadow:0 8px 30px rgba(0,0,0,.12);z-index:2147483000}
144
+ .sa-sidebar .sa-panel{position:fixed;top:0;right:0;bottom:0;width:min(400px,100vw);border-width:0 0 0 1px;z-index:2147483000}
145
+ .sa-shell{display:flex;height:100dvh;width:100%;min-width:0}
146
+ .sa-shell-main{flex:1;min-width:0;overflow:auto}
147
+ .sa-docked{display:contents}
148
+ .sa-docked .sa-panel{position:relative;flex:none;max-width:70vw;height:100%;border-width:0 0 0 1px;z-index:auto}
149
+ .sa-resizer{position:absolute;top:0;bottom:0;left:-3px;width:7px;cursor:col-resize;touch-action:none;z-index:1}
150
+ .sa-resizer::after{content:"";position:absolute;top:0;bottom:0;left:3px;width:2px;background:transparent;transition:background .15s}
151
+ .sa-resizer:hover::after,.sa-resizer:focus-visible::after,.sa-resizing .sa-resizer::after{background:var(--sa-muted)}
152
+ .sa-resizer:focus-visible{outline:none}
153
+ .sa-resizing{user-select:none;cursor:col-resize}
154
+ .sa-fab{position:fixed;right:16px;bottom:16px;width:48px;height:48px;border-radius:50%;border:0;background:var(--sa-accent);color:var(--sa-accent-fg);cursor:pointer;display:grid;place-items:center;box-shadow:0 4px 14px rgba(0,0,0,.2);z-index:2147483000}
155
+ .sa-header{display:flex;align-items:center;gap:6px;padding:10px 12px;border-bottom:1px solid var(--sa-border)}
156
+ .sa-title{font-weight:600;flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
157
+ .sa-btn{border:1px solid var(--sa-border);background:transparent;color:var(--sa-fg);border-radius:calc(var(--sa-radius) - 2px);padding:4px 9px;min-height:28px;font:inherit;font-size:12px;cursor:pointer}
158
+ .sa-btn:hover{background:var(--sa-surface)}
159
+ .sa-btn[aria-pressed=true]{background:var(--sa-surface);border-color:var(--sa-muted)}
160
+ .sa-btn-primary{background:var(--sa-accent);color:var(--sa-accent-fg);border-color:var(--sa-accent)}
161
+ .sa-btn-primary:hover{background:var(--sa-accent);opacity:.88}
162
+ .sa-btn:disabled{opacity:.5;cursor:not-allowed}
163
+ .sa-root :is(button,textarea):focus-visible{outline:2px solid var(--sa-fg);outline-offset:2px}
164
+ .sa-viewport{flex:1;overflow-y:auto;padding:14px 12px;display:flex;flex-direction:column;gap:10px}
165
+ .sa-empty{color:var(--sa-muted);text-align:center;margin:auto;padding:24px}
166
+ .sa-msg{max-width:90%;white-space:pre-wrap;word-break:break-word}
167
+ .sa-user{align-self:flex-end;background:var(--sa-surface);padding:7px 12px;border-radius:calc(var(--sa-radius) + 4px)}
168
+ .sa-assistant{align-self:flex-start}
169
+ .sa-icon-btn{display:inline-grid;place-items:center;min-width:28px;padding:4px}
170
+ .sa-icon-btn svg{width:15px;height:15px}
171
+ .sa-tool{align-self:stretch;border:1px solid var(--sa-border);border-radius:var(--sa-radius);padding:8px 10px;font-size:13px;min-width:0}
172
+ .sa-tool-head{display:flex;align-items:center;gap:6px;font-weight:600;font-size:12.5px;margin-bottom:6px}
173
+ .sa-tool[data-state=done] .sa-tool-head svg{color:var(--sa-muted)}
174
+ .sa-tool[data-state=error] .sa-tool-head,.sa-tool[data-state=rejected] .sa-tool-head{color:var(--sa-danger)}
175
+ .sa-spin{animation:sa-rot 1s linear infinite}
176
+ @keyframes sa-rot{to{transform:rotate(360deg)}}
177
+ @media (prefers-reduced-motion:reduce){.sa-spin{animation:none}}
178
+ .sa-table-wrap{overflow:auto;max-height:240px;border:1px solid var(--sa-border);border-radius:calc(var(--sa-radius) - 2px)}
179
+ .sa-table{border-collapse:collapse;width:100%;font-size:12.5px}
180
+ .sa-table th,.sa-table td{padding:5px 9px;text-align:left;white-space:nowrap;border-bottom:1px solid var(--sa-border)}
181
+ .sa-table th{position:sticky;top:0;background:var(--sa-surface);font-weight:600;color:var(--sa-muted)}
182
+ .sa-table tr:last-child td{border-bottom:0}
183
+ .sa-table .sa-num{text-align:right;font-variant-numeric:tabular-nums}
184
+ .sa-more{color:var(--sa-muted);font-size:12px;margin-top:4px}
185
+ .sa-fields{margin:0;display:grid;gap:4px}
186
+ .sa-field{display:grid;grid-template-columns:minmax(80px,35%) 1fr;gap:8px}
187
+ .sa-field dt{color:var(--sa-muted)}
188
+ .sa-field dd{margin:0;word-break:break-word}
189
+ .sa-list{margin:0;padding-left:18px}
190
+ .sa-sources{margin:0;padding:0;list-style:none;display:grid;gap:4px}
191
+ .sa-sources li{display:flex;align-items:center;gap:6px}
192
+ .sa-sources svg{flex:none;color:var(--sa-muted)}
193
+ .sa-sources a{color:inherit;text-decoration:underline;text-underline-offset:2px;display:inline-flex;align-items:center;gap:4px}
194
+ .sa-details{margin-top:6px}
195
+ .sa-details summary{cursor:pointer;color:var(--sa-muted);font-size:12px;list-style:none;display:flex;align-items:center;gap:2px}
196
+ .sa-details summary::-webkit-details-marker{display:none}
197
+ .sa-details[open] summary svg{transform:rotate(90deg)}
198
+ .sa-details pre{margin:6px 0 0;white-space:pre-wrap;word-break:break-word;max-height:160px;overflow:auto;font-size:11.5px}
199
+ .sa-approval-title{display:flex;align-items:center;gap:6px}
200
+ .sa-approval{margin:0 12px 8px;border:1px solid var(--sa-fg);border-radius:var(--sa-radius);padding:10px;background:var(--sa-surface)}
201
+ .sa-approval pre{margin:6px 0;white-space:pre-wrap;word-break:break-word;max-height:120px;overflow:auto;font-size:12px}
202
+ .sa-approval-text{margin:6px 0}
203
+ .sa-approval .sa-fields{margin:0 0 6px}
204
+ .sa-approval .sa-row{margin-top:8px}
205
+ .sa-row{display:flex;gap:8px}
206
+ .sa-composer{display:flex;flex-direction:column;gap:4px;margin:0 12px 12px;padding:10px 10px 8px 14px;border:1px solid var(--sa-border);border-radius:18px;background:var(--sa-surface)}
207
+ .sa-input{width:100%;resize:none;border:0;background:transparent;padding:2px 0;font:inherit;color:inherit;max-height:160px}
208
+ .sa-root .sa-input:focus-visible{outline:none}
209
+ .sa-input::placeholder{color:var(--sa-muted)}
210
+ .sa-composer-bar{display:flex;align-items:center;justify-content:space-between;gap:8px}
211
+ .sa-hint{color:var(--sa-muted);font-size:12px;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
212
+ .sa-send{flex:none;display:grid;place-items:center;width:32px;height:32px;border-radius:50%;border:0;background:var(--sa-accent);color:var(--sa-accent-fg);cursor:pointer}
213
+ .sa-send:hover{opacity:.88}
214
+ .sa-send:disabled{opacity:.35;cursor:not-allowed}
215
+ .sa-send svg{width:16px;height:16px}
216
+ .sa-chatwrap{display:flex;flex-direction:column;flex:1;min-height:0}
217
+ .sa-chatwrap[hidden]{display:none}
218
+ .sa-history{gap:6px}
219
+ .sa-back{align-self:flex-start;display:inline-flex;align-items:center;gap:4px;margin-bottom:4px}
220
+ .sa-thread{display:flex;gap:6px;align-items:stretch}
221
+ .sa-thread-open{flex:1;min-width:0;text-align:left;border:1px solid var(--sa-border);background:transparent;color:inherit;border-radius:var(--sa-radius);padding:8px 10px;font:inherit;cursor:pointer;display:flex;flex-direction:column;gap:2px}
222
+ .sa-thread-open:hover{background:var(--sa-surface)}
223
+ .sa-thread-title{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
224
+ .sa-error{color:var(--sa-danger)}
225
+ `;
226
+
227
+ // src/client/transport.ts
228
+ async function* streamChat(body, opts) {
229
+ const res = await fetch(opts.endpoint, {
230
+ method: "POST",
231
+ credentials: opts.credentials ?? "same-origin",
232
+ headers: { "content-type": "application/json", ...opts.headers },
233
+ body: JSON.stringify(body),
234
+ signal: opts.signal
235
+ });
236
+ if (!res.ok || !res.body) {
237
+ let message = `Request failed (${res.status})`;
238
+ try {
239
+ const j = await res.json();
240
+ if (j?.error) message = String(j.error);
241
+ } catch {
242
+ }
243
+ yield { type: "error", code: `http_${res.status}`, message };
244
+ return;
245
+ }
246
+ const reader = res.body.getReader();
247
+ const decoder = new TextDecoder();
248
+ let buf = "";
249
+ for (; ; ) {
250
+ const { done, value } = await reader.read();
251
+ if (done) break;
252
+ buf += decoder.decode(value, { stream: true });
253
+ let i;
254
+ while ((i = buf.indexOf("\n\n")) >= 0) {
255
+ const block = buf.slice(0, i);
256
+ buf = buf.slice(i + 2);
257
+ if (block.startsWith("data: ")) yield JSON.parse(block.slice(6));
258
+ }
259
+ }
260
+ }
261
+ async function callJson(body, opts) {
262
+ const res = await fetch(opts.endpoint, {
263
+ method: "POST",
264
+ credentials: opts.credentials ?? "same-origin",
265
+ headers: { "content-type": "application/json", ...opts.headers },
266
+ body: JSON.stringify(body)
267
+ });
268
+ let data = null;
269
+ try {
270
+ data = await res.json();
271
+ } catch {
272
+ }
273
+ return { status: res.status, data };
274
+ }
275
+
276
+ // src/client/AgentChat.tsx
277
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
278
+ var MODE_KEY = "sideagent:mode";
279
+ var WIDTH_KEY = "sideagent:width";
280
+ var threadKey = (endpoint) => `sideagent:thread:${endpoint}`;
281
+ function storeGet(k) {
282
+ try {
283
+ return localStorage.getItem(k);
284
+ } catch {
285
+ return null;
286
+ }
287
+ }
288
+ function storeSet(k, v) {
289
+ try {
290
+ if (v === null) localStorage.removeItem(k);
291
+ else localStorage.setItem(k, v);
292
+ } catch {
293
+ }
294
+ }
295
+ function toInitialMessages(messages) {
296
+ return (messages ?? []).map(
297
+ (m) => m.role === "user" ? { role: "user", content: m.parts.map((p) => ({ type: "text", text: p.type === "text" ? p.text : "" })) } : {
298
+ role: "assistant",
299
+ content: m.parts.map(
300
+ (p) => p.type === "text" ? { type: "text", text: p.text } : { type: "tool-call", toolCallId: p.callId, toolName: p.operationId, args: p.args ?? {}, argsText: pretty(p.args), result: p.result }
301
+ ),
302
+ status: { type: "complete", reason: "stop" }
303
+ }
304
+ );
305
+ }
306
+ function fireComplete(hooks, ev) {
307
+ const oe = { operationId: ev.operationId, status: ev.status, args: ev.args, result: ev.result };
308
+ safely(() => hooks?.onAnyOperation?.(oe));
309
+ safely(() => hooks?.onOperationComplete?.[ev.operationId]?.(oe));
310
+ for (const tag of ev.invalidates) safely(() => hooks?.onInvalidate?.(tag, oe));
311
+ }
312
+ var ago = (t) => {
313
+ const s = Math.round((t - Date.now()) / 1e3);
314
+ const rtf = new Intl.RelativeTimeFormat(void 0, { numeric: "auto" });
315
+ const a = Math.abs(s);
316
+ if (a < 60) return rtf.format(s, "second");
317
+ if (a < 3600) return rtf.format(Math.round(s / 60), "minute");
318
+ if (a < 86400) return rtf.format(Math.round(s / 3600), "hour");
319
+ return rtf.format(Math.round(s / 86400), "day");
320
+ };
321
+ var MODES = ["floating", "sidebar"];
322
+ var MIN_WIDTH = 300;
323
+ var DEFAULT_WIDTH = 400;
324
+ var maxWidth = () => Math.max(MIN_WIDTH, Math.min(760, Math.round(window.innerWidth * 0.7)));
325
+ var clampWidth = (w) => Math.min(maxWidth(), Math.max(MIN_WIDTH, Math.round(w)));
326
+ var DockContext = createContext(false);
327
+ var AgentContext = createContext(null);
328
+ function useAgent() {
329
+ const ctx = useContext(AgentContext);
330
+ if (!ctx) throw new Error("useAgent must be used inside <AgentProvider>");
331
+ return ctx;
332
+ }
333
+ function AgentProvider({ children, className, style, ...chat }) {
334
+ const [open, setOpen] = useState(chat.defaultOpen ?? false);
335
+ const controls = useMemo(() => ({ open, setOpen }), [open]);
336
+ return /* @__PURE__ */ jsx2(AgentContext.Provider, { value: controls, children: /* @__PURE__ */ jsx2(DockContext.Provider, { value: true, children: /* @__PURE__ */ jsxs2("div", { className: `sa-shell${className ? ` ${className}` : ""}`, style, children: [
337
+ /* @__PURE__ */ jsx2("div", { className: "sa-shell-main", children }),
338
+ /* @__PURE__ */ jsx2(AgentChat, { ...chat })
339
+ ] }) }) });
340
+ }
341
+ function readMode(fallback) {
342
+ try {
343
+ const v = localStorage.getItem(MODE_KEY);
344
+ return v && MODES.includes(v) ? v : fallback;
345
+ } catch {
346
+ return fallback;
347
+ }
348
+ }
349
+ var pretty = (v) => {
350
+ try {
351
+ return JSON.stringify(v, null, 2);
352
+ } catch {
353
+ return String(v);
354
+ }
355
+ };
356
+ var Text = ({ text }) => /* @__PURE__ */ jsx2("span", { children: text });
357
+ var RenderersContext = createContext(void 0);
358
+ var httpUrl = (u) => {
359
+ try {
360
+ const url = new URL(String(u));
361
+ return url.protocol === "http:" || url.protocol === "https:" ? url.toString() : null;
362
+ } catch {
363
+ return null;
364
+ }
365
+ };
366
+ function Sources({ result }) {
367
+ const sources = result?.sources ?? [];
368
+ if (typeof result?.error === "string") return /* @__PURE__ */ jsx2("div", { className: "sa-error", children: result.error });
369
+ if (!sources.length) return /* @__PURE__ */ jsx2("div", { className: "sa-more", children: "No relevant sources found." });
370
+ return /* @__PURE__ */ jsx2("ul", { className: "sa-sources", children: sources.map((s) => {
371
+ const href = s.url ? httpUrl(s.url) : null;
372
+ return /* @__PURE__ */ jsxs2("li", { children: [
373
+ /* @__PURE__ */ jsx2(FileTextIcon, {}),
374
+ href ? /* @__PURE__ */ jsxs2("a", { href, target: "_blank", rel: "noopener noreferrer", children: [
375
+ s.title,
376
+ " ",
377
+ /* @__PURE__ */ jsx2(ExternalLinkIcon, {})
378
+ ] }) : /* @__PURE__ */ jsx2("span", { children: s.title })
379
+ ] }, s.id);
380
+ }) });
381
+ }
382
+ function ToolCard(props) {
383
+ const renderers = useContext(RenderersContext);
384
+ const { toolName, args, result } = props;
385
+ const rejected = result?.rejected === true;
386
+ const failed = !rejected && typeof result?.error === "string";
387
+ const running = result === void 0;
388
+ const custom = renderers?.[toolName];
389
+ return /* @__PURE__ */ jsxs2("div", { className: "sa-tool", "data-state": running ? "running" : rejected ? "rejected" : failed ? "error" : "done", children: [
390
+ /* @__PURE__ */ jsxs2("div", { className: "sa-tool-head", children: [
391
+ running ? /* @__PURE__ */ jsx2(ReloadIcon, { className: "sa-spin" }) : rejected || failed ? /* @__PURE__ */ jsx2(CrossCircledIcon, {}) : /* @__PURE__ */ jsx2(CheckCircledIcon, {}),
392
+ /* @__PURE__ */ jsx2("span", { children: toolName === "search_knowledge" ? "Sources" : humanize(toolName) })
393
+ ] }),
394
+ !running && (custom ? custom({ operationId: toolName, args, result }) : toolName === "search_knowledge" ? /* @__PURE__ */ jsx2(Sources, { result }) : /* @__PURE__ */ jsx2(ResultView, { result })),
395
+ /* @__PURE__ */ jsxs2("details", { className: "sa-details", children: [
396
+ /* @__PURE__ */ jsxs2("summary", { children: [
397
+ /* @__PURE__ */ jsx2(ChevronRightIcon, {}),
398
+ " Details"
399
+ ] }),
400
+ /* @__PURE__ */ jsx2("pre", { children: pretty(args) }),
401
+ !running && /* @__PURE__ */ jsx2("pre", { children: pretty(result) })
402
+ ] })
403
+ ] });
404
+ }
405
+ var UserMessage = () => /* @__PURE__ */ jsx2(MessagePrimitive.Root, { className: "sa-msg sa-user", children: /* @__PURE__ */ jsx2(MessagePrimitive.Parts, { components: { Text } }) });
406
+ var AssistantMessage = () => /* @__PURE__ */ jsx2(MessagePrimitive.Root, { style: { display: "contents" }, children: /* @__PURE__ */ jsx2(
407
+ MessagePrimitive.Parts,
408
+ {
409
+ components: {
410
+ Text: ({ text }) => text ? /* @__PURE__ */ jsx2("div", { className: "sa-msg sa-assistant", children: text }) : null,
411
+ tools: { Fallback: ToolCard }
412
+ }
413
+ }
414
+ ) });
415
+ function summaryFields(args) {
416
+ const a = args && typeof args === "object" ? args : {};
417
+ const body = a.body && typeof a.body === "object" && !Array.isArray(a.body) ? a.body : {};
418
+ const { body: _body, ...rest } = a;
419
+ return Object.entries({ ...rest, ...body }).filter(([, v]) => ["string", "number", "boolean"].includes(typeof v)).map(([k, v]) => [humanize(k), String(v)]);
420
+ }
421
+ function ApprovalCard({ event, onDecide }) {
422
+ const action = humanize(event.operationId).toLowerCase();
423
+ const fields = summaryFields(event.args);
424
+ return /* @__PURE__ */ jsxs2("div", { className: "sa-approval", role: "alertdialog", "aria-label": "Approval needed", children: [
425
+ /* @__PURE__ */ jsxs2("strong", { className: "sa-approval-title", children: [
426
+ /* @__PURE__ */ jsx2(ExclamationTriangleIcon, {}),
427
+ " Confirm before I continue"
428
+ ] }),
429
+ /* @__PURE__ */ jsxs2("p", { className: "sa-approval-text", children: [
430
+ "I'd like to ",
431
+ /* @__PURE__ */ jsx2("strong", { children: action }),
432
+ ". Nothing changes until you approve."
433
+ ] }),
434
+ fields.length > 0 && /* @__PURE__ */ jsx2("dl", { className: "sa-fields", children: fields.map(([k, v]) => /* @__PURE__ */ jsxs2("div", { className: "sa-field", children: [
435
+ /* @__PURE__ */ jsx2("dt", { children: k }),
436
+ /* @__PURE__ */ jsx2("dd", { children: v })
437
+ ] }, k)) }),
438
+ /* @__PURE__ */ jsxs2("details", { className: "sa-details", children: [
439
+ /* @__PURE__ */ jsxs2("summary", { children: [
440
+ /* @__PURE__ */ jsx2(ChevronRightIcon, {}),
441
+ " Details"
442
+ ] }),
443
+ /* @__PURE__ */ jsxs2("pre", { children: [
444
+ event.method,
445
+ " ",
446
+ event.path,
447
+ "\n",
448
+ pretty(event.args)
449
+ ] })
450
+ ] }),
451
+ /* @__PURE__ */ jsxs2("div", { className: "sa-row", children: [
452
+ /* @__PURE__ */ jsx2("button", { className: "sa-btn sa-btn-primary", onClick: () => onDecide("approve"), children: "Approve" }),
453
+ /* @__PURE__ */ jsx2("button", { className: "sa-btn", onClick: () => onDecide("reject"), children: "Reject" })
454
+ ] })
455
+ ] });
456
+ }
457
+ function Chat(props) {
458
+ const { endpoint, hooks, credentials, headers } = props;
459
+ const latest = useRef(props);
460
+ latest.current = props;
461
+ const threadId = useRef(props.session.threadId);
462
+ const [loadedPending, setLoadedPending] = useState(props.session.pending ?? null);
463
+ const [approval, setApproval] = useState(null);
464
+ const adapter = useMemo(
465
+ () => ({
466
+ async *run({ messages, abortSignal }) {
467
+ const p = latest.current;
468
+ const last = [...messages].reverse().find((m) => m.role === "user");
469
+ const text = (last?.content ?? []).map((c) => c.type === "text" ? c.text : "").join("\n").trim();
470
+ const ctx = typeof p.context === "function" ? p.context() : p.context;
471
+ const parts = [];
472
+ const upsertTool = (callId, patch) => {
473
+ let part = parts.find((x) => x.type === "tool-call" && x.toolCallId === callId);
474
+ if (!part) {
475
+ part = { type: "tool-call", toolCallId: callId, toolName: "", args: {}, argsText: "{}" };
476
+ parts.push(part);
477
+ }
478
+ Object.assign(part, patch);
479
+ };
480
+ safely(p.hooks?.onTurnStart);
481
+ let body = { threadId: threadId.current, message: text, context: ctx };
482
+ try {
483
+ for (; ; ) {
484
+ let pending;
485
+ for await (const ev of streamChat(body, { endpoint: p.endpoint, credentials: p.credentials, headers: p.headers, signal: abortSignal })) {
486
+ switch (ev.type) {
487
+ case "turn.start":
488
+ threadId.current = ev.threadId;
489
+ p.onThread(ev.threadId);
490
+ break;
491
+ case "text.delta": {
492
+ const tail = parts[parts.length - 1];
493
+ if (tail?.type === "text") tail.text += ev.text;
494
+ else parts.push({ type: "text", text: ev.text });
495
+ break;
496
+ }
497
+ case "approval.request":
498
+ pending = ev;
499
+ upsertTool(ev.callId, { toolName: ev.operationId, args: ev.args, argsText: pretty(ev.args) });
500
+ break;
501
+ case "tool.start":
502
+ upsertTool(ev.callId, { toolName: ev.operationId });
503
+ break;
504
+ case "tool.rejected":
505
+ upsertTool(ev.callId, { toolName: ev.operationId, result: { rejected: true } });
506
+ break;
507
+ case "tool.complete": {
508
+ upsertTool(ev.callId, { toolName: ev.operationId, args: ev.args ?? {}, argsText: pretty(ev.args), result: ev.result ?? null });
509
+ fireComplete(p.hooks, ev);
510
+ break;
511
+ }
512
+ case "error":
513
+ parts.push({ type: "text", text: `\u26A0 ${ev.message}` });
514
+ safely(() => p.hooks?.onError?.({ code: ev.code, message: ev.message }));
515
+ break;
516
+ }
517
+ yield { content: parts.map((x) => ({ ...x })) };
518
+ }
519
+ if (!pending) break;
520
+ const req = pending;
521
+ safely(() => p.hooks?.onApprovalRequested?.(req));
522
+ const decision = await new Promise((resolve) => {
523
+ abortSignal.addEventListener("abort", () => resolve("reject"), { once: true });
524
+ setApproval({ event: req, decide: resolve });
525
+ });
526
+ setApproval(null);
527
+ body = { threadId: threadId.current, approval: { token: req.token, decision } };
528
+ }
529
+ } finally {
530
+ setApproval(null);
531
+ safely(p.hooks?.onTurnEnd);
532
+ }
533
+ yield { content: parts.map((x) => ({ ...x })) };
534
+ }
535
+ }),
536
+ // endpoint/hooks/etc. are read through `latest`, so the adapter stays stable.
537
+ [endpoint, hooks, credentials, headers]
538
+ );
539
+ const runtime = useLocalRuntime(adapter, { initialMessages: toInitialMessages(props.session.messages) });
540
+ const decideLoaded = async (decision) => {
541
+ const req = loadedPending;
542
+ if (!req || !threadId.current) return;
543
+ setLoadedPending(null);
544
+ for await (const ev of streamChat({ threadId: threadId.current, approval: { token: req.token, decision } }, { endpoint, credentials, headers })) {
545
+ if (ev.type === "tool.complete") fireComplete(hooks, ev);
546
+ if (ev.type === "error") safely(() => hooks?.onError?.({ code: ev.code, message: ev.message }));
547
+ }
548
+ props.onReload();
549
+ };
550
+ const shownApproval = approval?.event ?? loadedPending;
551
+ const decide = (d) => approval ? approval.decide(d) : void decideLoaded(d);
552
+ return /* @__PURE__ */ jsx2(RenderersContext.Provider, { value: props.resultRenderers, children: /* @__PURE__ */ jsx2(AssistantRuntimeProvider, { runtime, children: /* @__PURE__ */ jsxs2(ThreadPrimitive.Root, { style: { display: "contents" }, children: [
553
+ /* @__PURE__ */ jsxs2(ThreadPrimitive.Viewport, { className: "sa-viewport", children: [
554
+ /* @__PURE__ */ jsx2(ThreadPrimitive.Empty, { children: /* @__PURE__ */ jsx2("div", { className: "sa-empty", children: "Ask me to look things up or make changes. I will ask before changing anything." }) }),
555
+ /* @__PURE__ */ jsx2(ThreadPrimitive.Messages, { components: { UserMessage, AssistantMessage } })
556
+ ] }),
557
+ shownApproval && /* @__PURE__ */ jsx2(ApprovalCard, { event: shownApproval, onDecide: decide }),
558
+ /* @__PURE__ */ jsxs2(ComposerPrimitive.Root, { className: "sa-composer", children: [
559
+ /* @__PURE__ */ jsx2(ComposerPrimitive.Input, { className: "sa-input", rows: 2, placeholder: props.placeholder ?? "Message\u2026", disabled: !!shownApproval }),
560
+ /* @__PURE__ */ jsxs2("div", { className: "sa-composer-bar", children: [
561
+ /* @__PURE__ */ jsx2("span", { className: "sa-hint", children: "Enter to send \xB7 Shift+Enter for a new line" }),
562
+ /* @__PURE__ */ jsx2(ComposerPrimitive.Send, { className: "sa-send", "aria-label": "Send", title: "Send", children: /* @__PURE__ */ jsx2(ArrowUpIcon, {}) })
563
+ ] })
564
+ ] })
565
+ ] }) }) });
566
+ }
567
+ function HistoryList(props) {
568
+ const [threads, setThreads] = useState(null);
569
+ const [error, setError] = useState(false);
570
+ const opts = { endpoint: props.endpoint, credentials: props.credentials, headers: props.headers };
571
+ useEffect(() => {
572
+ callJson({ action: "threads.list" }, opts).then((r) => r.data?.threads ? setThreads(r.data.threads) : setError(true)).catch(() => setError(true));
573
+ }, []);
574
+ const remove = async (id) => {
575
+ await callJson({ action: "threads.delete", threadId: id }, opts);
576
+ setThreads((t) => t?.filter((x) => x.id !== id) ?? null);
577
+ props.onDeleted(id);
578
+ };
579
+ return /* @__PURE__ */ jsxs2("div", { className: "sa-viewport sa-history", children: [
580
+ /* @__PURE__ */ jsxs2("button", { className: "sa-btn sa-back", onClick: props.onClose, children: [
581
+ /* @__PURE__ */ jsx2(ArrowLeftIcon, {}),
582
+ " Back to chat"
583
+ ] }),
584
+ error && /* @__PURE__ */ jsx2("div", { className: "sa-error", children: "Could not load history." }),
585
+ threads === null && !error && /* @__PURE__ */ jsx2("div", { className: "sa-more", children: "Loading\u2026" }),
586
+ threads?.length === 0 && /* @__PURE__ */ jsx2("div", { className: "sa-empty", children: "No conversations yet." }),
587
+ threads?.map((t) => /* @__PURE__ */ jsxs2("div", { className: "sa-thread", children: [
588
+ /* @__PURE__ */ jsxs2("button", { className: "sa-thread-open", onClick: () => props.onOpen(t.id), children: [
589
+ /* @__PURE__ */ jsx2("span", { className: "sa-thread-title", children: t.title }),
590
+ /* @__PURE__ */ jsx2("span", { className: "sa-more", children: ago(t.updatedAt) })
591
+ ] }),
592
+ /* @__PURE__ */ jsx2("button", { className: "sa-btn sa-icon-btn", "aria-label": "Delete conversation", title: "Delete", onClick: () => void remove(t.id), children: /* @__PURE__ */ jsx2(TrashIcon, {}) })
593
+ ] }, t.id))
594
+ ] });
595
+ }
596
+ function AgentChat(props) {
597
+ const [mode, setModeState] = useState(() => readMode(props.mode ?? "floating"));
598
+ const [localOpen, setLocalOpen] = useState(props.defaultOpen ?? false);
599
+ const shared = useContext(AgentContext);
600
+ const open = shared ? shared.open : localOpen;
601
+ const setOpen = shared ? shared.setOpen : setLocalOpen;
602
+ const [view, setView] = useState("chat");
603
+ const [session, setSession] = useState({ key: 0 });
604
+ const [loading, setLoading] = useState(false);
605
+ const docked = useContext(DockContext) && mode === "sidebar";
606
+ const [width, setWidth] = useState(() => clampWidth(Number(storeGet(WIDTH_KEY)) || DEFAULT_WIDTH));
607
+ const [resizing, setResizing] = useState(false);
608
+ const booted = useRef(false);
609
+ const key = threadKey(props.endpoint);
610
+ const opts = { endpoint: props.endpoint, credentials: props.credentials, headers: props.headers };
611
+ const setMode = useCallback((m) => {
612
+ setModeState(m);
613
+ storeSet(MODE_KEY, m);
614
+ }, []);
615
+ const openThread = useCallback(
616
+ async (id) => {
617
+ setLoading(true);
618
+ try {
619
+ const r = await callJson({ action: "threads.get", threadId: id }, opts);
620
+ if (r.status === 200 && r.data) {
621
+ setSession((s) => ({ key: s.key + 1, threadId: id, messages: r.data.messages, pending: r.data.pending }));
622
+ storeSet(key, id);
623
+ } else {
624
+ if (r.status === 404) storeSet(key, null);
625
+ setSession((s) => ({ key: s.key + 1 }));
626
+ }
627
+ } catch {
628
+ setSession((s) => ({ key: s.key + 1 }));
629
+ } finally {
630
+ setLoading(false);
631
+ setView("chat");
632
+ }
633
+ },
634
+ // eslint-disable-next-line react-hooks/exhaustive-deps
635
+ [props.endpoint, props.credentials, props.headers]
636
+ );
637
+ const resizeTo = useCallback((w) => {
638
+ const next = clampWidth(w);
639
+ setWidth(next);
640
+ return next;
641
+ }, []);
642
+ const onHandleDown = (e) => {
643
+ e.currentTarget.setPointerCapture(e.pointerId);
644
+ setResizing(true);
645
+ };
646
+ const onHandleMove = (e) => {
647
+ if (resizing) resizeTo(e.currentTarget.parentElement.getBoundingClientRect().right - e.clientX);
648
+ };
649
+ const onHandleUp = () => {
650
+ setResizing(false);
651
+ storeSet(WIDTH_KEY, String(width));
652
+ };
653
+ const onHandleKey = (e) => {
654
+ const d = e.key === "ArrowLeft" ? 16 : e.key === "ArrowRight" ? -16 : 0;
655
+ if (d) {
656
+ e.preventDefault();
657
+ storeSet(WIDTH_KEY, String(resizeTo(width + d)));
658
+ }
659
+ };
660
+ const newChat = () => {
661
+ storeSet(key, null);
662
+ setSession((s) => ({ key: s.key + 1 }));
663
+ setView("chat");
664
+ };
665
+ const visible = open;
666
+ useEffect(() => {
667
+ if (!visible || booted.current) return;
668
+ booted.current = true;
669
+ const last = storeGet(key);
670
+ if (last) void openThread(last);
671
+ }, [visible, key, openThread]);
672
+ return /* @__PURE__ */ jsxs2("div", { className: `sa-root sa-${mode}${docked ? " sa-docked" : ""}${resizing ? " sa-resizing" : ""}`, "data-theme": props.theme ?? "auto", children: [
673
+ /* @__PURE__ */ jsx2("style", { children: CSS }),
674
+ !open && /* @__PURE__ */ jsx2("button", { className: "sa-fab", "aria-label": "Open assistant", onClick: () => setOpen(true), children: /* @__PURE__ */ jsx2(ChatBubbleIcon, { width: 22, height: 22 }) }),
675
+ visible && /* @__PURE__ */ jsxs2("section", { className: "sa-panel", "aria-label": props.title ?? "Assistant", style: docked ? { width } : void 0, children: [
676
+ docked && /* @__PURE__ */ jsx2(
677
+ "div",
678
+ {
679
+ className: "sa-resizer",
680
+ role: "separator",
681
+ "aria-orientation": "vertical",
682
+ "aria-label": "Resize assistant",
683
+ "aria-valuemin": MIN_WIDTH,
684
+ "aria-valuemax": maxWidth(),
685
+ "aria-valuenow": width,
686
+ tabIndex: 0,
687
+ onPointerDown: onHandleDown,
688
+ onPointerMove: onHandleMove,
689
+ onPointerUp: onHandleUp,
690
+ onPointerCancel: onHandleUp,
691
+ onKeyDown: onHandleKey
692
+ }
693
+ ),
694
+ /* @__PURE__ */ jsxs2("header", { className: "sa-header", children: [
695
+ /* @__PURE__ */ jsx2("span", { className: "sa-title", children: props.title ?? "Assistant" }),
696
+ props.allowModeSwitch !== false && /* @__PURE__ */ jsx2(
697
+ "button",
698
+ {
699
+ className: "sa-btn sa-icon-btn",
700
+ onClick: () => setMode(mode === "floating" ? "sidebar" : "floating"),
701
+ title: mode === "floating" ? "Dock to sidebar" : "Float",
702
+ "aria-label": mode === "floating" ? "Dock to sidebar" : "Float",
703
+ children: mode === "floating" ? /* @__PURE__ */ jsx2(ViewVerticalIcon, {}) : /* @__PURE__ */ jsx2(OpenInNewWindowIcon, {})
704
+ }
705
+ ),
706
+ /* @__PURE__ */ jsx2("button", { className: "sa-btn sa-icon-btn", "aria-pressed": view === "history", onClick: () => setView((v) => v === "history" ? "chat" : "history"), title: "History", "aria-label": "History", children: /* @__PURE__ */ jsx2(CounterClockwiseClockIcon, {}) }),
707
+ /* @__PURE__ */ jsx2("button", { className: "sa-btn sa-icon-btn", onClick: newChat, title: "New chat", "aria-label": "New chat", children: /* @__PURE__ */ jsx2(PlusIcon, {}) }),
708
+ /* @__PURE__ */ jsx2("button", { className: "sa-btn sa-icon-btn", onClick: () => setOpen(false), "aria-label": "Close", title: "Close", children: /* @__PURE__ */ jsx2(Cross2Icon, {}) })
709
+ ] }),
710
+ view === "history" && /* @__PURE__ */ jsx2(
711
+ HistoryList,
712
+ {
713
+ ...opts,
714
+ onOpen: (id) => void openThread(id),
715
+ onClose: () => setView("chat"),
716
+ onDeleted: (id) => {
717
+ if (id === session.threadId) newChat();
718
+ }
719
+ }
720
+ ),
721
+ /* @__PURE__ */ jsx2("div", { className: "sa-chatwrap", hidden: view === "history", children: loading ? /* @__PURE__ */ jsx2("div", { className: "sa-viewport", children: /* @__PURE__ */ jsx2("div", { className: "sa-more", children: "Loading\u2026" }) }) : /* @__PURE__ */ jsx2(
722
+ Chat,
723
+ {
724
+ ...props,
725
+ session,
726
+ onThread: (id) => storeSet(key, id),
727
+ onReload: () => session.threadId && void openThread(session.threadId)
728
+ },
729
+ session.key
730
+ ) })
731
+ ] })
732
+ ] });
733
+ }
734
+ export {
735
+ AgentChat,
736
+ AgentProvider,
737
+ ResultView,
738
+ callJson,
739
+ humanize,
740
+ streamChat,
741
+ useAgent
742
+ };