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