@invergent/website-widget 2.7.4 → 2.7.6

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/ui.cjs ADDED
@@ -0,0 +1,1254 @@
1
+ 'use strict';
2
+
3
+ var preact = require('preact');
4
+ var client = require('@ag-ui/client');
5
+ var rxjs = require('rxjs');
6
+ var hooks = require('preact/hooks');
7
+ var jsxRuntime = require('preact/jsx-runtime');
8
+
9
+ // src/ui/mount.tsx
10
+
11
+ // src/constants.ts
12
+ var SDK_VERSION = "0.1.0";
13
+ var CSRF_HEADER = "X-CSRF-Token";
14
+ var VERSION_HEADER = "X-Surogates-Widget-Version";
15
+ var PATH_BOOTSTRAP = "/v1/website/sessions";
16
+ var PATH_MESSAGES = (sessionId) => `/v1/website/sessions/${sessionId}/messages`;
17
+ var PATH_EVENTS = (sessionId, after) => `/v1/website/sessions/${sessionId}/events?after=${after}`;
18
+ var PATH_END = (sessionId) => `/v1/website/sessions/${sessionId}/end`;
19
+ var SURG_EVENT = {
20
+ USER_MESSAGE: "user.message",
21
+ LLM_REQUEST: "llm.request",
22
+ LLM_RESPONSE: "llm.response",
23
+ LLM_THINKING: "llm.thinking",
24
+ LLM_DELTA: "llm.delta",
25
+ TOOL_CALL: "tool.call",
26
+ TOOL_RESULT: "tool.result",
27
+ SANDBOX_PROVISION: "sandbox.provision",
28
+ SANDBOX_EXECUTE: "sandbox.execute",
29
+ SANDBOX_RESULT: "sandbox.result",
30
+ SANDBOX_DESTROY: "sandbox.destroy",
31
+ SESSION_START: "session.start",
32
+ SESSION_PAUSE: "session.pause",
33
+ SESSION_RESUME: "session.resume",
34
+ SESSION_COMPLETE: "session.complete",
35
+ SESSION_FAIL: "session.fail",
36
+ SESSION_DONE: "session.done",
37
+ CONTEXT_COMPACT: "context.compact",
38
+ MEMORY_UPDATE: "memory.update",
39
+ EXPERT_DELEGATION: "expert.delegation",
40
+ EXPERT_RESULT: "expert.result",
41
+ EXPERT_FAILURE: "expert.failure",
42
+ POLICY_DENIED: "policy.denied",
43
+ POLICY_ALLOWED: "policy.allowed",
44
+ HARNESS_CRASH: "harness.crash"
45
+ };
46
+
47
+ // src/errors.ts
48
+ var SurogatesError = class extends Error {
49
+ constructor(message, opts) {
50
+ super(message);
51
+ this.name = "SurogatesError";
52
+ if (opts?.status !== void 0) this.status = opts.status;
53
+ if (opts?.detail !== void 0) this.detail = opts.detail;
54
+ if (opts?.cause !== void 0) this.cause = opts.cause;
55
+ }
56
+ };
57
+ var SurogatesAuthError = class extends SurogatesError {
58
+ constructor(message, opts) {
59
+ super(message, opts);
60
+ this.name = "SurogatesAuthError";
61
+ }
62
+ };
63
+ var SurogatesRateLimitError = class extends SurogatesError {
64
+ constructor(message, opts) {
65
+ super(message, opts);
66
+ this.name = "SurogatesRateLimitError";
67
+ if (opts?.retryAfter !== void 0) this.retryAfter = opts.retryAfter;
68
+ }
69
+ };
70
+ var SurogatesProtocolError = class extends SurogatesError {
71
+ constructor(message, opts) {
72
+ super(message, opts);
73
+ this.name = "SurogatesProtocolError";
74
+ }
75
+ };
76
+ var SurogatesNetworkError = class extends SurogatesError {
77
+ constructor(message, opts) {
78
+ super(message, opts);
79
+ this.name = "SurogatesNetworkError";
80
+ }
81
+ };
82
+
83
+ // src/protocol.ts
84
+ function withAgentId(path, agentId) {
85
+ if (!agentId) return path;
86
+ const sep = path.includes("?") ? "&" : "?";
87
+ return `${path}${sep}agent_id=${encodeURIComponent(agentId)}`;
88
+ }
89
+ async function doFetch(url, init) {
90
+ let response;
91
+ try {
92
+ response = await fetch(url, {
93
+ ...init,
94
+ credentials: "include",
95
+ headers: {
96
+ ...init.headers,
97
+ [VERSION_HEADER]: SDK_VERSION
98
+ }
99
+ });
100
+ } catch (cause) {
101
+ throw new SurogatesNetworkError("Network request failed", { cause });
102
+ }
103
+ return response;
104
+ }
105
+ async function extractDetail(response) {
106
+ try {
107
+ const body = await response.json();
108
+ return typeof body.detail === "string" ? body.detail : void 0;
109
+ } catch {
110
+ return void 0;
111
+ }
112
+ }
113
+ async function raiseForStatus(response, action) {
114
+ const detail = await extractDetail(response);
115
+ if (response.status === 401 || response.status === 403) {
116
+ throw new SurogatesAuthError(`${action} rejected (${response.status})`, {
117
+ status: response.status,
118
+ ...detail !== void 0 && { detail }
119
+ });
120
+ }
121
+ if (response.status === 429) {
122
+ const retryAfterHeader = response.headers.get("Retry-After");
123
+ const retryAfter = retryAfterHeader ? Number(retryAfterHeader) : void 0;
124
+ throw new SurogatesRateLimitError(`${action} rate-limited`, {
125
+ status: response.status,
126
+ ...retryAfter !== void 0 && Number.isFinite(retryAfter) && { retryAfter },
127
+ ...detail !== void 0 && { detail }
128
+ });
129
+ }
130
+ if (response.status >= 500) {
131
+ throw new SurogatesNetworkError(
132
+ `${action} failed (${response.status}${detail ? `: ${detail}` : ""})`
133
+ );
134
+ }
135
+ throw new SurogatesProtocolError(`${action} failed (${response.status})`, {
136
+ status: response.status,
137
+ ...detail !== void 0 && { detail }
138
+ });
139
+ }
140
+ async function bootstrap(apiUrl, publishableKey, agentId) {
141
+ const response = await doFetch(apiUrl + withAgentId(PATH_BOOTSTRAP, agentId), {
142
+ method: "POST",
143
+ headers: {
144
+ Authorization: `Bearer ${publishableKey}`,
145
+ "Content-Type": "application/json"
146
+ }
147
+ });
148
+ if (!response.ok) {
149
+ await raiseForStatus(response, "Bootstrap");
150
+ }
151
+ let body;
152
+ try {
153
+ body = await response.json();
154
+ } catch (cause) {
155
+ throw new SurogatesProtocolError("Bootstrap returned non-JSON body", { cause });
156
+ }
157
+ if (!body || typeof body.session_id !== "string" || typeof body.csrf_token !== "string") {
158
+ throw new SurogatesProtocolError("Bootstrap response missing required fields");
159
+ }
160
+ return {
161
+ sessionId: body.session_id,
162
+ csrfToken: body.csrf_token,
163
+ expiresAt: Number(body.expires_at) || 0,
164
+ agentName: body.agent_name ?? ""
165
+ };
166
+ }
167
+ async function sendMessage(apiUrl, sessionId, csrfToken, content, agentId) {
168
+ const response = await doFetch(apiUrl + withAgentId(PATH_MESSAGES(sessionId), agentId), {
169
+ method: "POST",
170
+ headers: {
171
+ "Content-Type": "application/json",
172
+ [CSRF_HEADER]: csrfToken
173
+ },
174
+ body: JSON.stringify({ content })
175
+ });
176
+ if (!response.ok) {
177
+ await raiseForStatus(response, "Send message");
178
+ }
179
+ const body = await response.json().catch(() => ({}));
180
+ return { eventId: Number(body.event_id ?? 0) };
181
+ }
182
+ async function endSession(apiUrl, sessionId, csrfToken, agentId) {
183
+ try {
184
+ await doFetch(apiUrl + withAgentId(PATH_END(sessionId), agentId), {
185
+ method: "POST",
186
+ headers: { [CSRF_HEADER]: csrfToken }
187
+ });
188
+ } catch {
189
+ }
190
+ }
191
+ function openEventStream(apiUrl, sessionId, cursor, agentId) {
192
+ const url = apiUrl + withAgentId(PATH_EVENTS(sessionId, cursor), agentId);
193
+ return new EventSource(url, { withCredentials: true });
194
+ }
195
+ var END_OF_TURN_FINISH_REASONS = /* @__PURE__ */ new Set(["stop", "end_turn", "length"]);
196
+ var CONTINUATION_FINISH_REASONS = /* @__PURE__ */ new Set(["tool_calls", "function_call", "tool_use"]);
197
+ var Translator = class {
198
+ constructor() {
199
+ /**
200
+ * Tool calls that have been announced (``TOOL_CALL_CHUNK`` emitted)
201
+ * but have not yet received a matching ``tool.result``. Non-empty
202
+ * means the turn is mid-execution even if an ``llm.response`` looks
203
+ * final.
204
+ */
205
+ this.pendingToolCalls = /* @__PURE__ */ new Set();
206
+ /** Set once we've seen an end-of-turn signal. */
207
+ this._turnComplete = false;
208
+ }
209
+ get isTurnComplete() {
210
+ return this._turnComplete;
211
+ }
212
+ /**
213
+ * Translate one SSE frame into zero or more AG-UI events.
214
+ *
215
+ * Returns an empty array when the frame carries no user-visible
216
+ * information (internal bookkeeping events like ``harness.wake``).
217
+ * Returns multiple events when a single Surogates event corresponds
218
+ * to a chunk triad that the AG-UI client can't reconstruct on its
219
+ * own -- though we try to use chunk events to keep this to one
220
+ * output event per input in the common path.
221
+ */
222
+ translate(frame) {
223
+ switch (frame.type) {
224
+ case SURG_EVENT.LLM_DELTA:
225
+ return this.handleLlmDelta(frame);
226
+ case SURG_EVENT.LLM_RESPONSE:
227
+ return this.handleLlmResponse(frame);
228
+ case SURG_EVENT.LLM_THINKING:
229
+ return this.handleLlmThinking(frame);
230
+ case SURG_EVENT.TOOL_CALL:
231
+ return this.handleToolCall(frame);
232
+ case SURG_EVENT.TOOL_RESULT:
233
+ return this.handleToolResult(frame);
234
+ case SURG_EVENT.POLICY_DENIED:
235
+ return this.handlePolicyDenied(frame);
236
+ case SURG_EVENT.EXPERT_DELEGATION:
237
+ return this.handleExpertDelegation(frame);
238
+ case SURG_EVENT.EXPERT_RESULT:
239
+ return this.handleExpertResult(frame);
240
+ case SURG_EVENT.EXPERT_FAILURE:
241
+ return this.asCustom(frame);
242
+ case SURG_EVENT.SESSION_FAIL:
243
+ case SURG_EVENT.HARNESS_CRASH:
244
+ return this.handleTerminalFailure(frame);
245
+ case SURG_EVENT.SESSION_DONE:
246
+ case SURG_EVENT.SESSION_COMPLETE:
247
+ this._turnComplete = true;
248
+ return [];
249
+ // Frames we do not surface to AG-UI consumers: internal
250
+ // orchestration (``user.message`` is already in the consumer's
251
+ // own ``agent.messages``; ``llm.request``, ``session.start``,
252
+ // ``sandbox.*``, ``harness.wake`` are implementation details).
253
+ case SURG_EVENT.USER_MESSAGE:
254
+ case SURG_EVENT.LLM_REQUEST:
255
+ case SURG_EVENT.SESSION_START:
256
+ case SURG_EVENT.SESSION_PAUSE:
257
+ case SURG_EVENT.SESSION_RESUME:
258
+ case SURG_EVENT.SANDBOX_PROVISION:
259
+ case SURG_EVENT.SANDBOX_EXECUTE:
260
+ case SURG_EVENT.SANDBOX_RESULT:
261
+ case SURG_EVENT.SANDBOX_DESTROY:
262
+ case SURG_EVENT.POLICY_ALLOWED:
263
+ return [];
264
+ // Everything else (``memory.update``, ``context.compact``,
265
+ // saga events, future server additions) is forwarded as CUSTOM
266
+ // so advanced consumers that want that visibility can still
267
+ // subscribe -- without us pre-committing to a particular AG-UI
268
+ // shape for a Surogates-specific event that might evolve.
269
+ default:
270
+ return this.asCustom(frame);
271
+ }
272
+ }
273
+ // ------------------------------------------------------------------
274
+ // Individual handlers
275
+ // ------------------------------------------------------------------
276
+ handleLlmDelta(frame) {
277
+ const payload = frame.data ?? {};
278
+ const delta = payload.delta ?? payload.content ?? "";
279
+ if (!delta) return [];
280
+ const messageId = this.currentMessageId ?? payload.message_id ?? this.mintMessageId(frame.id);
281
+ this.currentMessageId = messageId;
282
+ return [
283
+ {
284
+ type: client.EventType.TEXT_MESSAGE_CHUNK,
285
+ messageId,
286
+ role: "assistant",
287
+ delta
288
+ }
289
+ ];
290
+ }
291
+ handleLlmResponse(frame) {
292
+ const payload = frame.data ?? {};
293
+ const events = [];
294
+ if (!this.currentMessageId && payload.content) {
295
+ const messageId = payload.message_id ?? payload.id ?? this.mintMessageId(frame.id);
296
+ events.push({
297
+ type: client.EventType.TEXT_MESSAGE_CHUNK,
298
+ messageId,
299
+ role: "assistant",
300
+ delta: payload.content
301
+ });
302
+ }
303
+ this.currentMessageId = void 0;
304
+ const toolCalls = Array.isArray(payload.tool_calls) ? payload.tool_calls : [];
305
+ const finish = payload.finish_reason ?? payload.stop_reason ?? "";
306
+ if (CONTINUATION_FINISH_REASONS.has(finish)) {
307
+ return events;
308
+ }
309
+ const modelDone = toolCalls.length === 0 && (finish === "" || END_OF_TURN_FINISH_REASONS.has(finish));
310
+ if (modelDone && this.pendingToolCalls.size === 0) {
311
+ this._turnComplete = true;
312
+ }
313
+ return events;
314
+ }
315
+ handleLlmThinking(frame) {
316
+ const payload = frame.data ?? {};
317
+ const delta = payload.delta ?? payload.content ?? "";
318
+ if (!delta) return [];
319
+ const messageId = this.currentReasoningId ?? payload.message_id ?? this.mintMessageId(frame.id);
320
+ const isNew = this.currentReasoningId !== messageId;
321
+ this.currentReasoningId = messageId;
322
+ const events = [];
323
+ if (isNew) {
324
+ events.push({
325
+ type: client.EventType.REASONING_START,
326
+ messageId
327
+ });
328
+ events.push({
329
+ type: client.EventType.REASONING_MESSAGE_START,
330
+ messageId,
331
+ role: "reasoning"
332
+ });
333
+ }
334
+ events.push({
335
+ type: client.EventType.REASONING_MESSAGE_CONTENT,
336
+ messageId,
337
+ delta
338
+ });
339
+ return events;
340
+ }
341
+ handleToolCall(frame) {
342
+ const payload = frame.data ?? {};
343
+ const toolCallId = payload.tool_call_id ?? `tc-${frame.id}`;
344
+ const toolCallName = payload.name ?? "unknown_tool";
345
+ this.currentMessageId = void 0;
346
+ this.currentReasoningId = void 0;
347
+ this.pendingToolCalls.add(toolCallId);
348
+ const args = typeof payload.arguments === "string" ? payload.arguments : JSON.stringify(payload.arguments ?? {});
349
+ return [
350
+ {
351
+ type: client.EventType.TOOL_CALL_CHUNK,
352
+ toolCallId,
353
+ toolCallName,
354
+ delta: args
355
+ }
356
+ ];
357
+ }
358
+ handleToolResult(frame) {
359
+ const payload = frame.data ?? {};
360
+ const toolCallId = payload.tool_call_id ?? `tc-${frame.id}`;
361
+ const content = payload.content ?? "";
362
+ this.pendingToolCalls.delete(toolCallId);
363
+ return [
364
+ {
365
+ type: client.EventType.TOOL_CALL_RESULT,
366
+ messageId: `tr-${frame.id}`,
367
+ toolCallId,
368
+ content,
369
+ role: "tool"
370
+ }
371
+ ];
372
+ }
373
+ handlePolicyDenied(frame) {
374
+ const payload = frame.data ?? {};
375
+ return [
376
+ {
377
+ type: client.EventType.CUSTOM,
378
+ name: SURG_EVENT.POLICY_DENIED,
379
+ value: { tool: payload.tool, reason: payload.reason }
380
+ }
381
+ ];
382
+ }
383
+ handleExpertDelegation(frame) {
384
+ const payload = frame.data ?? {};
385
+ const name = payload.expert_name ?? "expert";
386
+ return [
387
+ {
388
+ type: client.EventType.STEP_STARTED,
389
+ stepName: `expert:${name}`
390
+ }
391
+ ];
392
+ }
393
+ handleExpertResult(frame) {
394
+ const payload = frame.data ?? {};
395
+ const name = payload.expert_name ?? "expert";
396
+ return [
397
+ {
398
+ type: client.EventType.STEP_FINISHED,
399
+ stepName: `expert:${name}`
400
+ }
401
+ ];
402
+ }
403
+ handleTerminalFailure(frame) {
404
+ this._turnComplete = true;
405
+ const payload = frame.data ?? {};
406
+ const message = payload.error ?? payload.message ?? `${frame.type} fired`;
407
+ return [
408
+ {
409
+ type: client.EventType.RUN_ERROR,
410
+ message,
411
+ code: frame.type
412
+ }
413
+ ];
414
+ }
415
+ asCustom(frame) {
416
+ return [
417
+ {
418
+ type: client.EventType.CUSTOM,
419
+ name: frame.type,
420
+ value: frame.data
421
+ }
422
+ ];
423
+ }
424
+ /**
425
+ * Generate a stable message id derived from the triggering frame's
426
+ * event id. The event id is globally unique per session, so using
427
+ * it as a suffix guarantees uniqueness without pulling in ``uuid``.
428
+ */
429
+ mintMessageId(frameId) {
430
+ return `msg-${frameId}`;
431
+ }
432
+ };
433
+
434
+ // src/agent.ts
435
+ var SURG_EVENT_NAMES = Object.values(SURG_EVENT);
436
+ var DRAIN_WINDOW_MS = 250;
437
+ var WebsiteAgent = class extends client.AbstractAgent {
438
+ constructor(config) {
439
+ super(config);
440
+ /** Monotonic cursor across runs. Passed to the SSE ``?after=`` param. */
441
+ this.cursor = 0;
442
+ if (!config.apiUrl) {
443
+ throw new client.AGUIError('WebsiteAgent: "apiUrl" is required.');
444
+ }
445
+ if (!config.publishableKey) {
446
+ throw new client.AGUIError('WebsiteAgent: "publishableKey" is required.');
447
+ }
448
+ this.apiUrl = config.apiUrl.replace(/\/+$/, "");
449
+ this.publishableKey = config.publishableKey;
450
+ }
451
+ /**
452
+ * Ensure we have a live session cookie + CSRF token.
453
+ *
454
+ * Idempotent; returns the cached result when one already exists.
455
+ * ``bootstrap()`` is the only call that requires the publishable
456
+ * key to travel in an Authorization header, so callers that want
457
+ * to eagerly validate their configuration (e.g. to surface a
458
+ * setup error at widget-load time) can invoke this directly.
459
+ */
460
+ async ensureBootstrapped() {
461
+ if (this.bootstrapResult) return this.bootstrapResult;
462
+ this.bootstrapResult = await bootstrap(
463
+ this.apiUrl,
464
+ this.publishableKey,
465
+ this.agentId
466
+ );
467
+ return this.bootstrapResult;
468
+ }
469
+ /**
470
+ * Close the current session on the server side and drop local state.
471
+ *
472
+ * Callers should invoke this when the visitor closes the chat UI
473
+ * so the server can mark the session complete instead of waiting
474
+ * for the idle-reset timer. Safe to call when no session has been
475
+ * bootstrapped yet.
476
+ */
477
+ async end() {
478
+ const current = this.bootstrapResult;
479
+ if (!current) return;
480
+ this.bootstrapResult = void 0;
481
+ this.cursor = 0;
482
+ await endSession(this.apiUrl, current.sessionId, current.csrfToken, this.agentId);
483
+ }
484
+ /**
485
+ * Implementation of the AG-UI ``run()`` contract.
486
+ *
487
+ * Returns a cold observable: one subscription per run. AG-UI's
488
+ * ``runAgent`` pipeline pipes this through chunk transformation,
489
+ * verification, and subscriber notifications before updating
490
+ * ``this.messages``.
491
+ */
492
+ run(input) {
493
+ return new rxjs.Observable((subscriber) => {
494
+ const ctx = {
495
+ terminated: false,
496
+ source: void 0,
497
+ drainTimer: void 0
498
+ };
499
+ void this.driveRun(input, subscriber, ctx).catch((err) => {
500
+ if (!ctx.terminated) {
501
+ ctx.terminated = true;
502
+ subscriber.error(err);
503
+ }
504
+ });
505
+ return () => {
506
+ ctx.terminated = true;
507
+ if (ctx.drainTimer !== void 0) return;
508
+ closeEventSource(ctx.source);
509
+ ctx.source = void 0;
510
+ };
511
+ });
512
+ }
513
+ /**
514
+ * Async body of the observable factory.
515
+ *
516
+ * Emits ``RUN_STARTED`` up front so every downstream failure is
517
+ * well-bracketed by the AG-UI lifecycle. ``RUN_FINISHED`` is
518
+ * emitted by the SSE listeners when the translator detects end-of-
519
+ * turn; ``RUN_ERROR`` is emitted from the catch below when bootstrap
520
+ * or message-send fails before the SSE has any chance to close out.
521
+ *
522
+ * The ``ctx.terminated`` guard is the single enforcement point for
523
+ * AG-UI's "exactly one terminal event" invariant. Every emission
524
+ * site checks it before calling ``subscriber.next`` with RUN_FINISHED
525
+ * or RUN_ERROR, and every terminal call sets it to true first.
526
+ */
527
+ async driveRun(input, subscriber, ctx) {
528
+ if (ctx.terminated) return;
529
+ subscriber.next({
530
+ type: client.EventType.RUN_STARTED,
531
+ threadId: input.threadId,
532
+ runId: input.runId
533
+ });
534
+ try {
535
+ const userText = this.extractUserText(input.messages);
536
+ const boot = await this.ensureBootstrapped();
537
+ this.replaceStreamSource(
538
+ ctx,
539
+ boot.sessionId,
540
+ this.cursor,
541
+ new Translator(),
542
+ subscriber,
543
+ input
544
+ );
545
+ await this.sendWithCookieRecovery(boot, userText, (fresh) => {
546
+ this.replaceStreamSource(
547
+ ctx,
548
+ fresh.sessionId,
549
+ 0,
550
+ new Translator(),
551
+ subscriber,
552
+ input
553
+ );
554
+ });
555
+ } catch (err) {
556
+ if (ctx.terminated) return;
557
+ ctx.terminated = true;
558
+ const message = err instanceof Error ? err.message : String(err);
559
+ const code = err instanceof SurogatesAuthError ? "auth" : "error";
560
+ subscriber.next({
561
+ type: client.EventType.RUN_ERROR,
562
+ message,
563
+ code
564
+ });
565
+ closeEventSource(ctx.source);
566
+ ctx.source = void 0;
567
+ subscriber.complete();
568
+ }
569
+ }
570
+ /**
571
+ * Tear down whatever EventSource is currently live on ``ctx`` and
572
+ * open a fresh one against *sessionId* starting at *after*. Called
573
+ * both for the initial stream and after cookie recovery. Always
574
+ * clears the stale ``onerror`` first so the old source can't race
575
+ * a ``RUN_ERROR`` into the subscriber after it's been replaced.
576
+ */
577
+ replaceStreamSource(ctx, sessionId, after, translator, subscriber, input) {
578
+ closeEventSource(ctx.source);
579
+ const source = openEventStream(this.apiUrl, sessionId, after, this.agentId);
580
+ ctx.source = source;
581
+ this.attachStreamListeners(source, translator, subscriber, input, ctx);
582
+ }
583
+ /**
584
+ * Try ``sendMessage``; on auth failure re-bootstrap once and retry.
585
+ *
586
+ * The re-bootstrap mints a new session id, so the caller passes an
587
+ * ``onRebootstrap`` callback that reopens the SSE stream against the
588
+ * fresh session before the retry. Visitor continuity across
589
+ * bootstraps is a deliberate non-feature for v1; the new session
590
+ * starts empty. Only frames emitted on the OLD session between
591
+ * SSE-open and send-failure are lost -- and those are always
592
+ * previous-turn events, since the current turn's ``sendMessage`` is
593
+ * what 401-ed.
594
+ */
595
+ async sendWithCookieRecovery(boot, content, onRebootstrap) {
596
+ try {
597
+ await sendMessage(this.apiUrl, boot.sessionId, boot.csrfToken, content, this.agentId);
598
+ return;
599
+ } catch (err) {
600
+ if (!(err instanceof SurogatesAuthError)) throw err;
601
+ this.bootstrapResult = void 0;
602
+ this.cursor = 0;
603
+ const fresh = await this.ensureBootstrapped();
604
+ onRebootstrap(fresh);
605
+ await sendMessage(this.apiUrl, fresh.sessionId, fresh.csrfToken, content, this.agentId);
606
+ }
607
+ }
608
+ /**
609
+ * Wire up the EventSource to translate and forward every frame,
610
+ * and terminate the observable when the translator flags
611
+ * end-of-turn or a transport failure occurs.
612
+ *
613
+ * After the translator flips end-of-turn we emit ``RUN_FINISHED``
614
+ * immediately, then keep the stream open for ``DRAIN_WINDOW_MS``
615
+ * so trailing frames (late ``memory.update``, the sentinel
616
+ * ``session.done``) advance ``this.cursor`` rather than arriving
617
+ * on the next run. The ``ctx.terminated`` gate stops additional
618
+ * subscriber emissions during the drain.
619
+ */
620
+ attachStreamListeners(source, translator, subscriber, input, ctx) {
621
+ const scheduleDrainClose = () => {
622
+ if (ctx.drainTimer !== void 0) globalThis.clearTimeout(ctx.drainTimer);
623
+ ctx.drainTimer = globalThis.setTimeout(() => {
624
+ ctx.drainTimer = void 0;
625
+ closeEventSource(source);
626
+ }, DRAIN_WINDOW_MS);
627
+ };
628
+ const finish = () => {
629
+ if (ctx.terminated) return;
630
+ ctx.terminated = true;
631
+ subscriber.next({
632
+ type: client.EventType.RUN_FINISHED,
633
+ threadId: input.threadId,
634
+ runId: input.runId
635
+ });
636
+ scheduleDrainClose();
637
+ subscriber.complete();
638
+ };
639
+ const handleFrame = (name, data, lastEventId) => {
640
+ let parsed;
641
+ try {
642
+ parsed = JSON.parse(data);
643
+ } catch {
644
+ parsed = data;
645
+ }
646
+ const frame = {
647
+ id: Number(lastEventId) || 0,
648
+ type: name,
649
+ data: parsed
650
+ };
651
+ if (frame.id > this.cursor) this.cursor = frame.id;
652
+ if (ctx.terminated) return;
653
+ const events = translator.translate(frame);
654
+ for (const e of events) subscriber.next(e);
655
+ if (translator.isTurnComplete) finish();
656
+ };
657
+ for (const name of SURG_EVENT_NAMES) {
658
+ source.addEventListener(name, (ev) => {
659
+ handleFrame(name, ev.data, ev.lastEventId);
660
+ });
661
+ }
662
+ source.onerror = () => {
663
+ if (source.readyState !== 2) return;
664
+ if (ctx.terminated) return;
665
+ ctx.terminated = true;
666
+ subscriber.next({
667
+ type: client.EventType.RUN_ERROR,
668
+ message: "SSE connection closed",
669
+ code: "sse_closed"
670
+ });
671
+ subscriber.complete();
672
+ };
673
+ }
674
+ /**
675
+ * Find the last user-authored message body to post. AG-UI supports
676
+ * multimodal content arrays; this v1 only handles string content
677
+ * (the website channel's ``POST /messages`` endpoint accepts plain
678
+ * text). Multimodal support lands once the server grows a media
679
+ * upload path.
680
+ */
681
+ extractUserText(messages) {
682
+ for (let i = messages.length - 1; i >= 0; i--) {
683
+ const m = messages[i];
684
+ if (!m || m.role !== "user") continue;
685
+ const content = m.content;
686
+ if (typeof content === "string" && content.trim().length > 0) {
687
+ return content;
688
+ }
689
+ }
690
+ throw new client.AGUIError(
691
+ "WebsiteAgent: no user message with string content found in input.messages."
692
+ );
693
+ }
694
+ };
695
+ function closeEventSource(source) {
696
+ if (!source) return;
697
+ try {
698
+ source.onerror = null;
699
+ } catch {
700
+ }
701
+ source.close();
702
+ }
703
+
704
+ // src/ui/styles.ts
705
+ var WIDGET_STYLES = `
706
+ :host { all: initial; }
707
+ * { box-sizing: border-box; }
708
+
709
+ .surg-root {
710
+ --surg-radius: 16px;
711
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
712
+ font-size: 14px;
713
+ line-height: 1.5;
714
+ color: #0f172a;
715
+ }
716
+
717
+ .surg-launcher {
718
+ position: fixed;
719
+ bottom: 20px;
720
+ width: 56px;
721
+ height: 56px;
722
+ border-radius: 50%;
723
+ border: none;
724
+ cursor: pointer;
725
+ background: var(--surg-accent);
726
+ color: var(--surg-accent-fg);
727
+ box-shadow: 0 6px 24px rgba(15, 23, 42, 0.25);
728
+ display: flex;
729
+ align-items: center;
730
+ justify-content: center;
731
+ transition: transform 0.15s ease, box-shadow 0.15s ease;
732
+ z-index: 2147483000;
733
+ }
734
+ .surg-launcher:hover { transform: scale(1.06); box-shadow: 0 8px 28px rgba(15, 23, 42, 0.3); }
735
+ .surg-launcher svg { width: 26px; height: 26px; }
736
+ .surg-pos-bottom-right { right: 20px; }
737
+ .surg-pos-bottom-left { left: 20px; }
738
+
739
+ .surg-panel {
740
+ position: fixed;
741
+ bottom: 88px;
742
+ width: 380px;
743
+ max-width: calc(100vw - 40px);
744
+ height: 560px;
745
+ max-height: calc(100vh - 120px);
746
+ background: #ffffff;
747
+ border-radius: var(--surg-radius);
748
+ box-shadow: 0 12px 48px rgba(15, 23, 42, 0.28);
749
+ display: flex;
750
+ flex-direction: column;
751
+ overflow: hidden;
752
+ z-index: 2147483000;
753
+ animation: surg-rise 0.18s ease;
754
+ }
755
+ @keyframes surg-rise { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: translateY(0); } }
756
+
757
+ .surg-inline {
758
+ position: relative;
759
+ bottom: auto; right: auto; left: auto;
760
+ width: 100%;
761
+ height: 100%;
762
+ box-shadow: none;
763
+ border: 1px solid #e2e8f0;
764
+ animation: none;
765
+ }
766
+
767
+ .surg-header {
768
+ background: var(--surg-accent);
769
+ color: var(--surg-accent-fg);
770
+ padding: 14px 16px;
771
+ display: flex;
772
+ align-items: center;
773
+ justify-content: space-between;
774
+ flex-shrink: 0;
775
+ }
776
+ .surg-header-id { display: flex; align-items: center; gap: 10px; min-width: 0; }
777
+ .surg-header-text { display: flex; flex-direction: column; line-height: 1.25; min-width: 0; }
778
+ .surg-title { font-weight: 600; font-size: 15px; }
779
+ .surg-subtitle { font-size: 11px; font-weight: 400; opacity: 0.85; }
780
+ .surg-logo {
781
+ width: 32px; height: 32px; border-radius: 50%; object-fit: cover;
782
+ background: rgba(255, 255, 255, 0.2); flex-shrink: 0;
783
+ }
784
+ .surg-launcher-logo {
785
+ width: 32px; height: 32px; border-radius: 50%; object-fit: cover;
786
+ }
787
+ .surg-close {
788
+ background: transparent;
789
+ border: none;
790
+ color: inherit;
791
+ cursor: pointer;
792
+ opacity: 0.85;
793
+ padding: 4px;
794
+ display: flex;
795
+ border-radius: 6px;
796
+ }
797
+ .surg-close:hover { opacity: 1; background: rgba(255, 255, 255, 0.18); }
798
+
799
+ .surg-messages {
800
+ flex: 1;
801
+ overflow-y: auto;
802
+ padding: 16px;
803
+ display: flex;
804
+ flex-direction: column;
805
+ gap: 10px;
806
+ background: #f8fafc;
807
+ }
808
+
809
+ .surg-bubble {
810
+ max-width: 82%;
811
+ padding: 9px 13px;
812
+ border-radius: 14px;
813
+ word-wrap: break-word;
814
+ white-space: normal;
815
+ }
816
+ .surg-bubble p { margin: 0 0 8px; }
817
+ .surg-bubble p:last-child { margin-bottom: 0; }
818
+ .surg-bubble ul, .surg-bubble ol { margin: 4px 0; padding-left: 20px; }
819
+ .surg-bubble code {
820
+ background: rgba(15, 23, 42, 0.08);
821
+ padding: 1px 5px;
822
+ border-radius: 5px;
823
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
824
+ font-size: 0.9em;
825
+ }
826
+ .surg-bubble pre {
827
+ background: #0f172a;
828
+ color: #e2e8f0;
829
+ padding: 10px 12px;
830
+ border-radius: 10px;
831
+ overflow-x: auto;
832
+ margin: 6px 0;
833
+ }
834
+ .surg-bubble pre code { background: transparent; padding: 0; color: inherit; }
835
+ .surg-bubble a { color: var(--surg-accent); }
836
+
837
+ .surg-user {
838
+ align-self: flex-end;
839
+ background: var(--surg-accent);
840
+ color: var(--surg-accent-fg);
841
+ border-bottom-right-radius: 4px;
842
+ white-space: pre-wrap;
843
+ }
844
+ .surg-user a { color: var(--surg-accent-fg); text-decoration: underline; }
845
+ .surg-assistant {
846
+ align-self: flex-start;
847
+ background: #ffffff;
848
+ border: 1px solid #e2e8f0;
849
+ border-bottom-left-radius: 4px;
850
+ }
851
+
852
+ .surg-typing { display: flex; gap: 4px; padding: 4px 2px; }
853
+ .surg-typing span {
854
+ width: 7px; height: 7px; border-radius: 50%;
855
+ background: #94a3b8;
856
+ animation: surg-blink 1.2s infinite ease-in-out;
857
+ }
858
+ .surg-typing span:nth-child(2) { animation-delay: 0.2s; }
859
+ .surg-typing span:nth-child(3) { animation-delay: 0.4s; }
860
+ @keyframes surg-blink { 0%, 60%, 100% { opacity: 0.3; } 30% { opacity: 1; } }
861
+
862
+ .surg-error {
863
+ margin: 0 16px;
864
+ padding: 8px 12px;
865
+ background: #fef2f2;
866
+ border: 1px solid #fecaca;
867
+ color: #b91c1c;
868
+ border-radius: 10px;
869
+ font-size: 13px;
870
+ flex-shrink: 0;
871
+ }
872
+
873
+ .surg-composer {
874
+ display: flex;
875
+ gap: 8px;
876
+ padding: 12px;
877
+ border-top: 1px solid #e2e8f0;
878
+ background: #ffffff;
879
+ flex-shrink: 0;
880
+ }
881
+ .surg-input {
882
+ flex: 1;
883
+ resize: none;
884
+ border: 1px solid #cbd5e1;
885
+ border-radius: 12px;
886
+ padding: 9px 12px;
887
+ font: inherit;
888
+ color: inherit;
889
+ max-height: 120px;
890
+ outline: none;
891
+ }
892
+ .surg-input:focus { border-color: var(--surg-accent); }
893
+ .surg-send {
894
+ border: none;
895
+ background: var(--surg-accent);
896
+ color: var(--surg-accent-fg);
897
+ border-radius: 12px;
898
+ width: 40px;
899
+ cursor: pointer;
900
+ display: flex;
901
+ align-items: center;
902
+ justify-content: center;
903
+ flex-shrink: 0;
904
+ }
905
+ .surg-send:disabled { opacity: 0.5; cursor: not-allowed; }
906
+ .surg-send svg { width: 18px; height: 18px; }
907
+
908
+ .surg-powered {
909
+ text-align: center;
910
+ font-size: 11px;
911
+ color: #94a3b8;
912
+ padding: 0 0 8px;
913
+ background: #ffffff;
914
+ }
915
+ `;
916
+
917
+ // src/ui/markdown.ts
918
+ var ESCAPE_MAP = {
919
+ "&": "&",
920
+ "<": "&lt;",
921
+ ">": "&gt;",
922
+ '"': "&quot;",
923
+ "'": "&#39;"
924
+ };
925
+ function escapeHtml(s) {
926
+ return s.replace(/[&<>"']/g, (c) => ESCAPE_MAP[c] ?? c);
927
+ }
928
+ function safeHref(raw) {
929
+ const url = raw.trim();
930
+ if (/^(https?:|mailto:)/i.test(url)) return url;
931
+ return void 0;
932
+ }
933
+ function renderInline(escaped) {
934
+ let out = escaped;
935
+ out = out.replace(/`([^`]+)`/g, (_m, code) => `<code>${code}</code>`);
936
+ out = out.replace(
937
+ /\[([^\]]+)\]\(([^)\s]+)\)/g,
938
+ (_m, text, href) => {
939
+ const safe = safeHref(href);
940
+ if (!safe) return `${text}`;
941
+ return `<a href="${safe}" target="_blank" rel="noopener noreferrer">${text}</a>`;
942
+ }
943
+ );
944
+ out = out.replace(/\*\*([^*]+)\*\*/g, (_m, t) => `<strong>${t}</strong>`);
945
+ out = out.replace(/(^|[^*])\*([^*\n]+)\*/g, (_m, pre, t) => `${pre}<em>${t}</em>`);
946
+ return out;
947
+ }
948
+ function renderMarkdown(src) {
949
+ const parts = src.split(/```/);
950
+ const html = [];
951
+ parts.forEach((part, index) => {
952
+ const isCodeFence = index % 2 === 1;
953
+ if (isCodeFence) {
954
+ const body = part.replace(/^[^\n]*\n/, (m) => part.includes("\n") ? "" : m);
955
+ html.push(`<pre><code>${escapeHtml(body.replace(/\n$/, ""))}</code></pre>`);
956
+ return;
957
+ }
958
+ const blocks = part.split(/\n{2,}/);
959
+ for (const block of blocks) {
960
+ const lines = block.split("\n").filter((l) => l.trim().length > 0);
961
+ if (lines.length === 0) continue;
962
+ const isUnordered = lines.every((l) => /^\s*[-*]\s+/.test(l));
963
+ const isOrdered = lines.every((l) => /^\s*\d+\.\s+/.test(l));
964
+ if (isUnordered || isOrdered) {
965
+ const items = lines.map((l) => l.replace(/^\s*(?:[-*]|\d+\.)\s+/, "")).map((l) => `<li>${renderInline(escapeHtml(l))}</li>`).join("");
966
+ html.push(isOrdered ? `<ol>${items}</ol>` : `<ul>${items}</ul>`);
967
+ continue;
968
+ }
969
+ const paragraph = lines.map((l) => renderInline(escapeHtml(l))).join("<br>");
970
+ html.push(`<p>${paragraph}</p>`);
971
+ }
972
+ });
973
+ return html.join("");
974
+ }
975
+
976
+ // src/ui/next-action.ts
977
+ var NEXT_ACTION_RE = /<next_action([^>]*)>([\s\S]*?)<\/next_action>\s*/gi;
978
+ var PARTIAL_FOOTER_RE = /(?:^|\n)\s*<(?:n(?:e(?:x(?:t(?:_(?:a(?:c(?:t(?:i(?:o(?:n[\s\S]*)?)?)?)?)?)?)?)?)?)?)?$/i;
979
+ function stripNextAction(text) {
980
+ if (!text) return text;
981
+ const withoutBlocks = text.replace(NEXT_ACTION_RE, "").trimEnd();
982
+ const match = PARTIAL_FOOTER_RE.exec(withoutBlocks);
983
+ return match ? withoutBlocks.slice(0, match.index).trimEnd() : withoutBlocks;
984
+ }
985
+ var DEFAULT_SUBTITLE = "Typically replies instantly";
986
+ var DEFAULT_WELCOME = "Hi! \u{1F44B} How can I help you today?";
987
+ function uid() {
988
+ return `u-${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`;
989
+ }
990
+ function snapshot(messages) {
991
+ const out = [];
992
+ for (const m of messages) {
993
+ if (m.role !== "user" && m.role !== "assistant") continue;
994
+ const content = typeof m.content === "string" ? m.content : "";
995
+ out.push({ id: m.id ?? uid(), role: m.role, content });
996
+ }
997
+ return out;
998
+ }
999
+ function friendlyError(err) {
1000
+ if (err instanceof SurogatesAuthError) {
1001
+ return "This chat isn\u2019t accepting messages from this site yet. The site owner needs to allow this origin.";
1002
+ }
1003
+ if (err instanceof SurogatesRateLimitError) {
1004
+ return "You\u2019re sending messages too quickly. Please wait a moment and try again.";
1005
+ }
1006
+ if (err && typeof err === "object") {
1007
+ const code = err.code;
1008
+ if (code === "auth") {
1009
+ return "This chat isn\u2019t accepting messages from this site yet. The site owner needs to allow this origin.";
1010
+ }
1011
+ if (code === "sse_closed") return "Connection lost. Please send your message again.";
1012
+ const message = err.message;
1013
+ if (message && message.length < 160) return message;
1014
+ }
1015
+ return "Something went wrong. Please try again.";
1016
+ }
1017
+ var ICON_CHAT = /* @__PURE__ */ jsxRuntime.jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z" }) });
1018
+ var ICON_CLOSE = /* @__PURE__ */ jsxRuntime.jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", children: [
1019
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
1020
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
1021
+ ] });
1022
+ var ICON_SEND = /* @__PURE__ */ jsxRuntime.jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", children: [
1023
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "22", y1: "2", x2: "11", y2: "13" }),
1024
+ /* @__PURE__ */ jsxRuntime.jsx("polygon", { points: "22 2 15 22 11 13 2 9 22 2" })
1025
+ ] });
1026
+ function Widget({ agent, config, registerOpenControl, onOpenChange }) {
1027
+ const inline = !!config.inline;
1028
+ const [open, setOpen] = hooks.useState(inline || !!config.openByDefault);
1029
+ const [messages, setMessages] = hooks.useState(() => snapshot(agent.messages));
1030
+ const [running, setRunning] = hooks.useState(false);
1031
+ const [error, setError] = hooks.useState(null);
1032
+ const [input, setInput] = hooks.useState("");
1033
+ const [agentName, setAgentName] = hooks.useState("");
1034
+ const title = config.title || agentName || "Assistant";
1035
+ const listRef = hooks.useRef(null);
1036
+ const bootstrapStarted = hooks.useRef(false);
1037
+ hooks.useEffect(() => registerOpenControl(setOpen), [registerOpenControl]);
1038
+ hooks.useEffect(() => onOpenChange?.(open), [open, onOpenChange]);
1039
+ hooks.useEffect(() => {
1040
+ const sub = agent.subscribe({
1041
+ onMessagesChanged: () => setMessages(snapshot(agent.messages)),
1042
+ onRunStartedEvent: () => {
1043
+ setRunning(true);
1044
+ setError(null);
1045
+ },
1046
+ onRunFinishedEvent: () => setRunning(false),
1047
+ onRunErrorEvent: ({ event }) => {
1048
+ setRunning(false);
1049
+ setError(friendlyError(event));
1050
+ }
1051
+ });
1052
+ return () => sub.unsubscribe();
1053
+ }, [agent]);
1054
+ hooks.useEffect(() => {
1055
+ if (!open || bootstrapStarted.current) return;
1056
+ bootstrapStarted.current = true;
1057
+ agent.ensureBootstrapped().then((res) => {
1058
+ if (res.agentName) setAgentName(res.agentName);
1059
+ }).catch((err) => {
1060
+ bootstrapStarted.current = false;
1061
+ setError(friendlyError(err));
1062
+ });
1063
+ }, [open, agent]);
1064
+ hooks.useEffect(() => {
1065
+ const el = listRef.current;
1066
+ if (el) el.scrollTop = el.scrollHeight;
1067
+ }, [messages, running]);
1068
+ const send = () => {
1069
+ const content = input.trim();
1070
+ if (!content || running) return;
1071
+ setInput("");
1072
+ agent.addMessage({ id: uid(), role: "user", content });
1073
+ setMessages(snapshot(agent.messages));
1074
+ void agent.runAgent().catch(() => {
1075
+ });
1076
+ };
1077
+ const onKeyDown = (e) => {
1078
+ if (e.key === "Enter" && !e.shiftKey) {
1079
+ e.preventDefault();
1080
+ send();
1081
+ }
1082
+ };
1083
+ const subtitle = config.subtitle ?? DEFAULT_SUBTITLE;
1084
+ const welcomeText = config.welcomeMessage ?? DEFAULT_WELCOME;
1085
+ const hasAssistant = messages.some((m) => m.role === "assistant");
1086
+ const showWelcome = !!welcomeText && !hasAssistant;
1087
+ const showTyping = running && messages[messages.length - 1]?.role !== "assistant";
1088
+ if (!inline && !open) {
1089
+ return /* @__PURE__ */ jsxRuntime.jsx(
1090
+ "button",
1091
+ {
1092
+ type: "button",
1093
+ class: `surg-launcher surg-pos-${config.position ?? "bottom-right"}`,
1094
+ "aria-label": "Open chat",
1095
+ onClick: () => setOpen(true),
1096
+ children: config.logoUrl ? /* @__PURE__ */ jsxRuntime.jsx("img", { class: "surg-launcher-logo", src: config.logoUrl, alt: "" }) : ICON_CHAT
1097
+ }
1098
+ );
1099
+ }
1100
+ const panelClass = inline ? "surg-panel surg-inline" : `surg-panel surg-pos-${config.position ?? "bottom-right"}`;
1101
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { class: panelClass, role: "dialog", "aria-label": title, children: [
1102
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { class: "surg-header", children: [
1103
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { class: "surg-header-id", children: [
1104
+ config.logoUrl && /* @__PURE__ */ jsxRuntime.jsx("img", { class: "surg-logo", src: config.logoUrl, alt: "" }),
1105
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { class: "surg-header-text", children: [
1106
+ /* @__PURE__ */ jsxRuntime.jsx("span", { class: "surg-title", children: title }),
1107
+ subtitle && /* @__PURE__ */ jsxRuntime.jsx("span", { class: "surg-subtitle", children: subtitle })
1108
+ ] })
1109
+ ] }),
1110
+ !inline && /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", class: "surg-close", "aria-label": "Close chat", onClick: () => setOpen(false), children: ICON_CLOSE })
1111
+ ] }),
1112
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { class: "surg-messages", ref: listRef, children: [
1113
+ showWelcome && /* @__PURE__ */ jsxRuntime.jsx(
1114
+ "div",
1115
+ {
1116
+ class: "surg-bubble surg-assistant",
1117
+ dangerouslySetInnerHTML: { __html: renderMarkdown(welcomeText) }
1118
+ }
1119
+ ),
1120
+ messages.map(
1121
+ (m) => m.role === "assistant" ? /* @__PURE__ */ jsxRuntime.jsx(
1122
+ "div",
1123
+ {
1124
+ class: "surg-bubble surg-assistant",
1125
+ dangerouslySetInnerHTML: { __html: renderMarkdown(stripNextAction(m.content)) }
1126
+ },
1127
+ m.id
1128
+ ) : /* @__PURE__ */ jsxRuntime.jsx("div", { class: "surg-bubble surg-user", children: m.content }, m.id)
1129
+ ),
1130
+ showTyping && /* @__PURE__ */ jsxRuntime.jsxs("div", { class: "surg-bubble surg-assistant surg-typing", children: [
1131
+ /* @__PURE__ */ jsxRuntime.jsx("span", {}),
1132
+ /* @__PURE__ */ jsxRuntime.jsx("span", {}),
1133
+ /* @__PURE__ */ jsxRuntime.jsx("span", {})
1134
+ ] })
1135
+ ] }),
1136
+ error && /* @__PURE__ */ jsxRuntime.jsx("div", { class: "surg-error", children: error }),
1137
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { class: "surg-composer", children: [
1138
+ /* @__PURE__ */ jsxRuntime.jsx(
1139
+ "textarea",
1140
+ {
1141
+ class: "surg-input",
1142
+ rows: 1,
1143
+ placeholder: "Type a message\u2026",
1144
+ value: input,
1145
+ disabled: running,
1146
+ onInput: (e) => setInput(e.target.value),
1147
+ onKeyDown
1148
+ }
1149
+ ),
1150
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", class: "surg-send", "aria-label": "Send", disabled: running || !input.trim(), onClick: send, children: ICON_SEND })
1151
+ ] }),
1152
+ /* @__PURE__ */ jsxRuntime.jsx("div", { class: "surg-powered", children: "Powered by Surogate" })
1153
+ ] });
1154
+ }
1155
+ var DEFAULT_ACCENT = "#4f46e5";
1156
+ function accentForeground(hex) {
1157
+ const v = hex.replace("#", "");
1158
+ const full = v.length === 3 ? v.split("").map((c) => c + c).join("") : v;
1159
+ if (full.length !== 6 || /[^0-9a-f]/i.test(full)) return "#ffffff";
1160
+ const r = parseInt(full.slice(0, 2), 16);
1161
+ const g = parseInt(full.slice(2, 4), 16);
1162
+ const b = parseInt(full.slice(4, 6), 16);
1163
+ const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
1164
+ return luminance > 0.62 ? "#0f172a" : "#ffffff";
1165
+ }
1166
+ function mount(config) {
1167
+ if (!config || !config.apiUrl || !config.publishableKey) {
1168
+ throw new Error(
1169
+ 'SurogatesWidget.mount: "apiUrl" and "publishableKey" are required.'
1170
+ );
1171
+ }
1172
+ const accent = config.accentColor ?? DEFAULT_ACCENT;
1173
+ let destroyed = false;
1174
+ const host = document.createElement("div");
1175
+ host.setAttribute("data-surogates-widget", "");
1176
+ const shadow = host.attachShadow({ mode: "open" });
1177
+ const style = document.createElement("style");
1178
+ style.textContent = WIDGET_STYLES;
1179
+ shadow.appendChild(style);
1180
+ const root = document.createElement("div");
1181
+ root.className = "surg-root";
1182
+ shadow.appendChild(root);
1183
+ const applyAccent = (accentColor) => {
1184
+ const a = accentColor ?? DEFAULT_ACCENT;
1185
+ root.style.setProperty("--surg-accent", a);
1186
+ root.style.setProperty("--surg-accent-fg", accentForeground(a));
1187
+ };
1188
+ applyAccent(accent);
1189
+ const attach = () => (config.target ?? document.body)?.appendChild(host);
1190
+ if (config.target || document.body) {
1191
+ attach();
1192
+ } else {
1193
+ document.addEventListener(
1194
+ "DOMContentLoaded",
1195
+ () => {
1196
+ if (!destroyed) attach();
1197
+ },
1198
+ { once: true }
1199
+ );
1200
+ }
1201
+ const agent = new WebsiteAgent(config);
1202
+ let setOpen;
1203
+ let isOpen = config.inline || !!config.openByDefault;
1204
+ const registerOpenControl = (fn) => {
1205
+ setOpen = fn;
1206
+ };
1207
+ const onOpenChange = (o) => {
1208
+ isOpen = o;
1209
+ };
1210
+ let current = config;
1211
+ const renderWidget = () => {
1212
+ preact.render(
1213
+ /* @__PURE__ */ jsxRuntime.jsx(
1214
+ Widget,
1215
+ {
1216
+ agent,
1217
+ config: current,
1218
+ registerOpenControl,
1219
+ onOpenChange
1220
+ }
1221
+ ),
1222
+ root
1223
+ );
1224
+ };
1225
+ renderWidget();
1226
+ const endOnUnload = () => {
1227
+ void agent.end();
1228
+ };
1229
+ window.addEventListener("beforeunload", endOnUnload);
1230
+ return {
1231
+ agent,
1232
+ open: () => setOpen?.(true),
1233
+ close: () => setOpen?.(false),
1234
+ toggle: () => setOpen?.(!isOpen),
1235
+ update: (partial) => {
1236
+ if (destroyed) return;
1237
+ current = { ...current, ...partial };
1238
+ if ("accentColor" in partial) applyAccent(current.accentColor);
1239
+ renderWidget();
1240
+ },
1241
+ destroy: () => {
1242
+ if (destroyed) return;
1243
+ destroyed = true;
1244
+ window.removeEventListener("beforeunload", endOnUnload);
1245
+ void agent.end();
1246
+ preact.render(null, root);
1247
+ host.remove();
1248
+ }
1249
+ };
1250
+ }
1251
+
1252
+ exports.mount = mount;
1253
+ //# sourceMappingURL=ui.cjs.map
1254
+ //# sourceMappingURL=ui.cjs.map