@noodleseed/assistant 1.1.1 → 1.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/react.cjs CHANGED
@@ -207,75 +207,1220 @@ function radiusValue(radius) {
207
207
  return 14;
208
208
  }
209
209
 
210
+ // src/browser-context.ts
211
+ function browserClientContext() {
212
+ let timeZone;
213
+ try {
214
+ timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
215
+ } catch {
216
+ }
217
+ const locale = globalThis.navigator?.language;
218
+ return {
219
+ ...locale ? { locale } : {},
220
+ ...timeZone ? { timeZone } : {}
221
+ };
222
+ }
223
+
224
+ // src/events.ts
225
+ function toAssistantClientEvent(event) {
226
+ const value = event.data;
227
+ switch (event.event) {
228
+ case "content":
229
+ if (typeof value.delta === "string" && hasOptionalTurnId(value)) {
230
+ return event;
231
+ }
232
+ break;
233
+ case "tool_proposed":
234
+ if (hasString(value, "id") && hasString(value, "tool") && hasOptionalJson(value.arguments) && hasOptionalString(value.expiresAt) && (value.requiresConfirmation === void 0 || value.requiresConfirmation === true) && hasOptionalTurnId(value)) {
235
+ return event;
236
+ }
237
+ break;
238
+ case "input_requested":
239
+ if (hasString(value, "id") && hasString(value, "message") && isRecord(value.requestedSchema) && hasString(value, "expiresAt") && hasOptionalTurnId(value)) {
240
+ return event;
241
+ }
242
+ break;
243
+ case "interaction_resolved":
244
+ if (hasString(value, "id") && (value.action === void 0 || isInteractionAction(value.action)) && hasOptionalTurnId(value)) {
245
+ return event;
246
+ }
247
+ break;
248
+ case "tool_completed":
249
+ if (hasString(value, "id") && hasString(value, "tool") && isJsonValue(value.result, 0) && (value.replayed === void 0 || value.replayed === true) && hasOptionalTurnId(value)) {
250
+ return event;
251
+ }
252
+ break;
253
+ case "view_available":
254
+ if (isViewAvailableDetail(value)) return event;
255
+ break;
256
+ case "error":
257
+ if (hasString(value, "code") && (value.status === void 0 || typeof value.status === "number") && (value.retryable === void 0 || typeof value.retryable === "boolean") && hasOptionalTurnId(value)) {
258
+ return event;
259
+ }
260
+ break;
261
+ case "done":
262
+ if (hasOptionalTurnId(value)) return event;
263
+ break;
264
+ case "interaction_proposed":
265
+ if (hasString(value, "id") && (hasString(value, "tool") || hasString(value, "title")) && hasOptionalJson(value.arguments)) {
266
+ return event;
267
+ }
268
+ break;
269
+ }
270
+ return { event: "unrecognized", data: { name: event.event, payload: value } };
271
+ }
272
+ function isViewAvailableDetail(value) {
273
+ return hasString(value, "id") && hasString(value, "tool") && typeof value.resourceUri === "string" && value.resourceUri.startsWith("ui://") && hasOptionalString(value.title) && (value.replayed === void 0 || value.replayed === true) && isJsonValue(value.result, 0) && hasOptionalTurnId(value);
274
+ }
275
+ function hasOptionalTurnId(value) {
276
+ return hasOptionalString(value.turnId);
277
+ }
278
+ function hasString(value, key) {
279
+ const entry = value[key];
280
+ return typeof entry === "string" && entry.length > 0;
281
+ }
282
+ function hasOptionalString(value) {
283
+ return value === void 0 || typeof value === "string";
284
+ }
285
+ function hasOptionalJson(value) {
286
+ return value === void 0 || isJsonValue(value, 0);
287
+ }
288
+ function isInteractionAction(value) {
289
+ return value === "accept" || value === "decline" || value === "cancel";
290
+ }
291
+ function isJsonValue(value, depth) {
292
+ if (depth > 16) return false;
293
+ if (value === null || typeof value === "string" || typeof value === "boolean") return true;
294
+ if (typeof value === "number") return Number.isFinite(value);
295
+ if (Array.isArray(value)) {
296
+ return value.length <= 256 && value.every((entry) => isJsonValue(entry, depth + 1));
297
+ }
298
+ if (!isRecord(value)) return false;
299
+ const entries = Object.entries(value);
300
+ return entries.length <= 256 && entries.every(([, entry]) => isJsonValue(entry, depth + 1));
301
+ }
302
+ function isRecord(value) {
303
+ return typeof value === "object" && value !== null && !Array.isArray(value);
304
+ }
305
+
306
+ // src/model-context.ts
307
+ var MAX_BYTES = 16 * 1024;
308
+ var MAX_DEPTH = 8;
309
+ var MAX_ENTRIES = 128;
310
+ var SENSITIVE_KEY = /(?:secret|token|api[-_]?key|password|credential|authorization|cookie)/i;
311
+ var CREDENTIAL_SHAPED_TEXT = /(?:-----BEGIN [A-Z ]*PRIVATE KEY-----|\bsk-[A-Za-z0-9_-]{20,}\b|\b(?:bearer|basic)\s+[A-Za-z0-9._~+/=-]{20,}\b|\b[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,}\b)/i;
312
+ function copyAssistantModelContext(update) {
313
+ const copied = copyJsonValue(update, "$", 0, /* @__PURE__ */ new Set());
314
+ assertModelContextShape(copied);
315
+ const encoded = JSON.stringify(copied);
316
+ if (new TextEncoder().encode(encoded).byteLength > MAX_BYTES) {
317
+ throw new Error("Model context must not exceed 16 KiB");
318
+ }
319
+ return copied;
320
+ }
321
+ function assertModelContextShape(value) {
322
+ if (!isPlainRecord(value)) throw new Error("Model context must be a JSON object");
323
+ for (const key of Object.keys(value)) {
324
+ if (key !== "content" && key !== "structuredContent") {
325
+ throw new Error(`Model context contains unknown field ${key}`);
326
+ }
327
+ }
328
+ if (value.content !== void 0) {
329
+ if (!Array.isArray(value.content)) throw new Error("Model context content must be an array");
330
+ for (const part of value.content) {
331
+ if (!isPlainRecord(part) || Object.keys(part).some((key) => key !== "type" && key !== "text") || part.type !== "text" || typeof part.text !== "string") {
332
+ throw new Error("Model context content supports text parts only");
333
+ }
334
+ }
335
+ }
336
+ if (value.structuredContent !== void 0 && !isPlainRecord(value.structuredContent)) {
337
+ throw new Error("Model context structuredContent must be an object");
338
+ }
339
+ }
340
+ function copyJsonValue(value, path, depth, ancestors) {
341
+ if (depth > MAX_DEPTH) throw new Error(`Model context exceeds maximum depth at ${path}`);
342
+ if (typeof value === "string") {
343
+ if (CREDENTIAL_SHAPED_TEXT.test(value)) {
344
+ throw new Error(`Model context contains credential-shaped text at ${path}`);
345
+ }
346
+ return value;
347
+ }
348
+ if (value === null || typeof value === "boolean" || typeof value === "number" && Number.isFinite(value)) {
349
+ return value;
350
+ }
351
+ if (typeof value !== "object") throw new Error(`Model context contains non-JSON data at ${path}`);
352
+ if (hasToJsonProperty(value)) {
353
+ throw new Error(`Model context must not define or inherit toJSON at ${path}`);
354
+ }
355
+ const prototype = Object.getPrototypeOf(value);
356
+ if (Array.isArray(value)) {
357
+ if (prototype !== Array.prototype) {
358
+ throw new Error(`Model context contains a non-JSON object at ${path}`);
359
+ }
360
+ } else if (prototype !== Object.prototype && prototype !== null) {
361
+ throw new Error(`Model context contains a non-JSON object at ${path}`);
362
+ }
363
+ if (ancestors.has(value)) throw new Error(`Model context contains a cycle at ${path}`);
364
+ if (Array.isArray(value)) {
365
+ if (value.length > MAX_ENTRIES) {
366
+ throw new Error(`Model context has more than 128 entries at ${path}`);
367
+ }
368
+ const copy2 = [];
369
+ ancestors.add(value);
370
+ for (let index = 0; index < value.length; index += 1) {
371
+ const entry = Object.hasOwn(value, index) ? value[index] : null;
372
+ copy2.push(copyJsonValue(entry, `${path}.${index}`, depth + 1, ancestors));
373
+ }
374
+ ancestors.delete(value);
375
+ return copy2;
376
+ }
377
+ const entries = Object.entries(value);
378
+ if (entries.length > MAX_ENTRIES) {
379
+ throw new Error(`Model context has more than 128 entries at ${path}`);
380
+ }
381
+ const copy = /* @__PURE__ */ Object.create(null);
382
+ ancestors.add(value);
383
+ for (const [key, entry] of entries) {
384
+ if (SENSITIVE_KEY.test(key)) {
385
+ throw new Error(`Model context contains sensitive key ${path}.${key}`);
386
+ }
387
+ copy[key] = copyJsonValue(entry, `${path}.${key}`, depth + 1, ancestors);
388
+ }
389
+ ancestors.delete(value);
390
+ return copy;
391
+ }
392
+ function isPlainRecord(value) {
393
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
394
+ const prototype = Object.getPrototypeOf(value);
395
+ return prototype === Object.prototype || prototype === null;
396
+ }
397
+ function hasToJsonProperty(value) {
398
+ let current = value;
399
+ const visited = /* @__PURE__ */ new Set();
400
+ while (current !== null && !visited.has(current)) {
401
+ visited.add(current);
402
+ if (Object.getOwnPropertyDescriptor(current, "toJSON") !== void 0) return true;
403
+ current = Object.getPrototypeOf(current);
404
+ }
405
+ return false;
406
+ }
407
+
210
408
  // src/transport.ts
211
409
  var AssistantTransportError = class extends Error {
212
410
  code = "invalid_response";
213
- constructor(contentType) {
214
- super(`assistant response was not an event stream (content-type: ${contentType || "unknown"})`);
411
+ constructor(message, options) {
412
+ super(message, options);
215
413
  this.name = "AssistantTransportError";
216
414
  }
217
415
  };
218
416
  async function consumeAssistantEvents(response, onEvent) {
219
- let delivered = 0;
220
- let sawText = false;
221
- const deliver = (event) => {
222
- delivered += 1;
223
- onEvent(event);
224
- };
417
+ const stream = new AssistantEventStream(onEvent);
225
418
  if (!response.body) {
226
- const text = await response.text();
227
- sawText = text.trim().length > 0;
228
- emitFrames(text, deliver, true);
229
- } else {
230
- const reader = response.body.getReader();
231
- const decoder = new TextDecoder();
232
- let pending = "";
233
- while (true) {
234
- const { done, value } = await reader.read();
235
- const chunk = decoder.decode(value, { stream: !done });
236
- if (!sawText && chunk.trim().length > 0) sawText = true;
237
- pending += chunk;
238
- pending = emitFrames(pending, deliver, done);
239
- if (done) break;
240
- }
241
- }
242
- if (delivered === 0 && sawText) {
243
- throw new AssistantTransportError(response.headers.get("content-type") ?? "");
244
- }
245
- }
246
- function emitFrames(input, onEvent, flush) {
247
- const normalized = input.replaceAll("\r\n", "\n").replaceAll("\r", "\n");
248
- const frames = normalized.split("\n\n");
249
- const pending = flush ? "" : frames.pop() ?? "";
250
- for (const frame of frames) {
251
- const event = parseFrame(frame);
252
- if (event) onEvent(event);
419
+ let text;
420
+ try {
421
+ text = await response.text();
422
+ } catch (error) {
423
+ if (isAbortError(error)) throw error;
424
+ throw invalidStream("assistant event stream could not be read", error);
425
+ }
426
+ stream.push(text);
427
+ stream.finish(response.headers.get("content-type") ?? "");
428
+ return;
253
429
  }
254
- if (flush && frames.length === 0 && normalized.trim()) {
255
- const event = parseFrame(normalized);
256
- if (event) onEvent(event);
430
+ const reader = response.body.getReader();
431
+ const decoder = new TextDecoder("utf-8", { fatal: true });
432
+ while (true) {
433
+ let result;
434
+ try {
435
+ result = await reader.read();
436
+ } catch (error) {
437
+ if (isAbortError(error)) throw error;
438
+ throw invalidStream("assistant event stream ended unexpectedly", error);
439
+ }
440
+ let chunk;
441
+ try {
442
+ chunk = decoder.decode(result.value, { stream: !result.done });
443
+ } catch (error) {
444
+ throw invalidStream("assistant event stream contained invalid UTF-8", error);
445
+ }
446
+ stream.push(chunk);
447
+ if (result.done) break;
257
448
  }
258
- return pending;
449
+ stream.finish(response.headers.get("content-type") ?? "");
259
450
  }
451
+ var AssistantEventStream = class {
452
+ #onEvent;
453
+ #pending = "";
454
+ #terminal;
455
+ constructor(onEvent) {
456
+ this.#onEvent = onEvent;
457
+ }
458
+ push(chunk) {
459
+ this.#pending += chunk;
460
+ const protectedCarriageReturn = this.#pending.endsWith("\r");
461
+ const normalizable = protectedCarriageReturn ? this.#pending.slice(0, -1) : this.#pending;
462
+ const frames = normalizeLineEndings(normalizable).split("\n\n");
463
+ this.#pending = `${frames.pop() ?? ""}${protectedCarriageReturn ? "\r" : ""}`;
464
+ for (const frame of frames) this.#acceptFrame(frame);
465
+ }
466
+ finish(contentType) {
467
+ if (this.#pending.trim().length > 0) {
468
+ throw invalidStream("assistant event stream ended with an unterminated frame");
469
+ }
470
+ if (!this.#terminal) {
471
+ throw invalidStream(
472
+ `assistant response did not contain a terminal done event (content-type: ${contentType || "unknown"})`
473
+ );
474
+ }
475
+ this.#onEvent(this.#terminal);
476
+ }
477
+ #acceptFrame(frame) {
478
+ const event = parseFrame(frame);
479
+ if (!event) return;
480
+ if (this.#terminal) {
481
+ throw invalidStream("assistant event stream contained an event after done");
482
+ }
483
+ if (event.event === "done") {
484
+ this.#terminal = event;
485
+ return;
486
+ }
487
+ this.#onEvent(event);
488
+ }
489
+ };
260
490
  function parseFrame(frame) {
261
- let event = "message";
491
+ let event;
262
492
  const data = [];
493
+ let hasFields = false;
263
494
  for (const line of frame.split("\n")) {
264
- if (line.startsWith(":")) continue;
495
+ if (!line || line.startsWith(":")) continue;
496
+ hasFields = true;
265
497
  const separator = line.indexOf(":");
266
498
  const field = separator < 0 ? line : line.slice(0, separator);
267
499
  const value = separator < 0 ? "" : line.slice(separator + 1).replace(/^ /, "");
268
500
  if (field === "event") event = value;
269
501
  if (field === "data") data.push(value);
270
502
  }
271
- if (data.length === 0) return void 0;
503
+ if (!hasFields) return void 0;
504
+ if (!event || data.length === 0) {
505
+ throw invalidStream("assistant event stream contained a malformed frame");
506
+ }
507
+ let parsed;
508
+ try {
509
+ parsed = JSON.parse(data.join("\n"));
510
+ } catch (error) {
511
+ throw invalidStream("assistant event stream contained malformed JSON", error);
512
+ }
513
+ if (event === "done" && !isValidDonePayload(parsed)) {
514
+ throw invalidStream("assistant event stream contained an invalid done event");
515
+ }
516
+ return { event, data: isRecord2(parsed) ? parsed : { value: parsed } };
517
+ }
518
+ function normalizeLineEndings(input) {
519
+ return input.replaceAll("\r\n", "\n").replaceAll("\r", "\n");
520
+ }
521
+ function isValidDonePayload(value) {
522
+ return isRecord2(value) && (value.turnId === void 0 || typeof value.turnId === "string" && value.turnId.length > 0);
523
+ }
524
+ function isRecord2(value) {
525
+ return typeof value === "object" && value !== null && !Array.isArray(value);
526
+ }
527
+ function invalidStream(message, cause) {
528
+ return new AssistantTransportError(message, cause === void 0 ? void 0 : { cause });
529
+ }
530
+ function isAbortError(error) {
531
+ return error instanceof DOMException && error.name === "AbortError";
532
+ }
533
+
534
+ // src/client.ts
535
+ var AssistantClientError = class extends Error {
536
+ detail;
537
+ constructor(detail, message, options) {
538
+ super(message, options);
539
+ this.name = "AssistantClientError";
540
+ this.detail = detail;
541
+ }
542
+ };
543
+ var DefaultAssistantClient = class {
544
+ #sessionEndpoint;
545
+ #fetch;
546
+ #listeners = /* @__PURE__ */ new Set();
547
+ #clientContext;
548
+ #session;
549
+ #context;
550
+ #modelContext;
551
+ #active;
552
+ constructor(options) {
553
+ if (!options.sessionEndpoint) throw new Error("sessionEndpoint is required");
554
+ this.#sessionEndpoint = options.sessionEndpoint;
555
+ this.#fetch = options.fetch ?? ((input, init) => globalThis.fetch(input, init));
556
+ this.#context = options.context ? { ...options.context } : void 0;
557
+ this.#modelContext = options.modelContext ? copyAssistantModelContext(options.modelContext) : void 0;
558
+ this.#clientContext = options.clientContext;
559
+ }
560
+ subscribe(listener) {
561
+ this.#listeners.add(listener);
562
+ return () => this.#listeners.delete(listener);
563
+ }
564
+ async sendMessage(text) {
565
+ const message = text.trim();
566
+ if (!message) return;
567
+ await this.#singleFlight(async (signal) => {
568
+ await this.#sendTurn(message, true, signal, this.#modelContext);
569
+ });
570
+ }
571
+ async respond(id, resolution) {
572
+ if (!id) {
573
+ throw clientError("invalid_request", "interaction id is required", false);
574
+ }
575
+ await this.#singleFlight(async (signal) => {
576
+ const session = this.#session;
577
+ if (!session) {
578
+ throw clientError("confirmation_expired", "assistant session is not active", false);
579
+ }
580
+ const modernEndpoint = session.endpoints.interactions;
581
+ if (!modernEndpoint && resolution.action !== "accept") {
582
+ throw clientError(
583
+ "unsupported_service",
584
+ "the assistant service does not support declining or cancelling interactions",
585
+ false
586
+ );
587
+ }
588
+ const endpoint = modernEndpoint ?? session.endpoints.toolConfirmations;
589
+ const body = modernEndpoint ? {
590
+ id,
591
+ action: resolution.action,
592
+ ...resolution.action === "accept" && resolution.content !== void 0 ? { content: resolution.content } : {}
593
+ } : { id };
594
+ this.#emit({ event: "interaction_started", data: { id, action: resolution.action } });
595
+ const response = await this.#request(
596
+ endpoint,
597
+ {
598
+ method: "POST",
599
+ headers: authorizedHeaders(session.token, "text/event-stream"),
600
+ body: JSON.stringify(body),
601
+ signal
602
+ },
603
+ "confirmation_failed"
604
+ );
605
+ if (response.status === 401) {
606
+ this.#session = void 0;
607
+ this.#emit({ event: "session_expired", data: {} });
608
+ }
609
+ if (!response.ok) {
610
+ const serverCode = await readStableErrorCode(response);
611
+ const expired = response.status === 401 || response.status === 409;
612
+ throw clientError(
613
+ serverCode ?? (expired ? "confirmation_expired" : "confirmation_failed"),
614
+ `Assistant interaction failed (${response.status})`,
615
+ false,
616
+ response.status
617
+ );
618
+ }
619
+ await this.#consume(response);
620
+ this.#emit({ event: "interaction_completed", data: { id, action: resolution.action } });
621
+ });
622
+ }
623
+ updateContext(context) {
624
+ this.#context = { ...context };
625
+ this.#emit({ event: "context_changed", data: { context: this.#context } });
626
+ }
627
+ updateModelContext(update) {
628
+ this.#modelContext = copyAssistantModelContext(update);
629
+ this.#emit({ event: "model_context_changed", data: { modelContext: this.#modelContext } });
630
+ }
631
+ resetSession() {
632
+ this.#session = void 0;
633
+ this.#emit({ event: "session_reset", data: {} });
634
+ }
635
+ abort() {
636
+ this.#active?.abort();
637
+ }
638
+ async #sendTurn(message, mayRetry, signal, modelContext) {
639
+ const session = this.#session ?? await this.#createSession(signal);
640
+ if (mayRetry) this.#emit({ event: "message_started", data: { message } });
641
+ const clientContext = this.#resolveClientContext();
642
+ const response = await this.#request(
643
+ session.endpoints.turns,
644
+ {
645
+ method: "POST",
646
+ headers: authorizedHeaders(session.token, "text/event-stream"),
647
+ body: JSON.stringify({
648
+ message,
649
+ ...clientContext ? { clientContext } : {},
650
+ ...modelContext ? { modelContext } : {}
651
+ }),
652
+ signal
653
+ },
654
+ "turn_failed"
655
+ );
656
+ if (response.status === 401 && mayRetry) {
657
+ this.#session = void 0;
658
+ this.#emit({ event: "session_expired", data: {} });
659
+ await this.#sendTurn(message, false, signal, modelContext);
660
+ return;
661
+ }
662
+ if (!response.ok) {
663
+ throw clientError(
664
+ "turn_failed",
665
+ `Assistant turn failed (${response.status})`,
666
+ false,
667
+ response.status
668
+ );
669
+ }
670
+ await this.#consume(response);
671
+ this.#emit({ event: "message_completed", data: {} });
672
+ }
673
+ #resolveClientContext() {
674
+ let value;
675
+ try {
676
+ value = typeof this.#clientContext === "function" ? this.#clientContext() : this.#clientContext;
677
+ } catch (error) {
678
+ throw clientError(
679
+ "turn_failed",
680
+ "Assistant client context provider failed",
681
+ false,
682
+ void 0,
683
+ {
684
+ cause: error
685
+ }
686
+ );
687
+ }
688
+ if (!value) return void 0;
689
+ const context = {
690
+ ...typeof value.locale === "string" && value.locale ? { locale: value.locale } : {},
691
+ ...typeof value.timeZone === "string" && value.timeZone ? { timeZone: value.timeZone } : {}
692
+ };
693
+ return Object.keys(context).length > 0 ? context : void 0;
694
+ }
695
+ async #createSession(signal) {
696
+ const response = await this.#request(
697
+ this.#sessionEndpoint,
698
+ {
699
+ method: "POST",
700
+ headers: { Accept: "application/json", "Content-Type": "application/json" },
701
+ body: JSON.stringify(this.#context ? { context: this.#context } : {}),
702
+ credentials: "same-origin",
703
+ signal
704
+ },
705
+ "session_failed"
706
+ );
707
+ if (!response.ok) {
708
+ throw clientError(
709
+ "session_failed",
710
+ `Assistant session failed (${response.status})`,
711
+ true,
712
+ response.status
713
+ );
714
+ }
715
+ let value;
716
+ try {
717
+ value = await response.json();
718
+ } catch (error) {
719
+ throw clientError(
720
+ "session_failed",
721
+ "Assistant session returned invalid JSON",
722
+ true,
723
+ void 0,
724
+ {
725
+ cause: error
726
+ }
727
+ );
728
+ }
729
+ const session = parseSession(value);
730
+ this.#session = session;
731
+ this.#emit({
732
+ event: "session_started",
733
+ data: {
734
+ expiresAt: session.expiresAt,
735
+ ...session.configuration ? { configuration: session.configuration } : {}
736
+ }
737
+ });
738
+ return session;
739
+ }
740
+ async #consume(response) {
741
+ let streamError;
742
+ try {
743
+ await consumeAssistantEvents(response, (event) => {
744
+ const clientEvent = toAssistantClientEvent(event);
745
+ this.#emit(clientEvent);
746
+ if (clientEvent.event !== "error" || typeof clientEvent.data.code !== "string") return;
747
+ streamError = {
748
+ code: clientEvent.data.code,
749
+ ...typeof clientEvent.data.status === "number" ? { status: clientEvent.data.status } : {},
750
+ retryable: clientEvent.data.retryable === true
751
+ };
752
+ });
753
+ } catch (error) {
754
+ if (error instanceof AssistantTransportError) {
755
+ throw clientError(error.code, error.message, false, void 0, { cause: error });
756
+ }
757
+ throw error;
758
+ }
759
+ if (streamError) {
760
+ throw clientError(
761
+ streamError.code,
762
+ `Assistant stream failed (${streamError.code})`,
763
+ streamError.retryable,
764
+ streamError.status
765
+ );
766
+ }
767
+ }
768
+ async #request(input, init, failureCode) {
769
+ try {
770
+ return await this.#fetch(input, init);
771
+ } catch (error) {
772
+ if (init.signal?.aborted) throw error;
773
+ throw clientError(failureCode, "Assistant request failed", true, void 0, { cause: error });
774
+ }
775
+ }
776
+ async #singleFlight(operation) {
777
+ if (this.#active) {
778
+ throw clientError("request_in_progress", "an assistant request is already in progress", true);
779
+ }
780
+ const controller = new AbortController();
781
+ this.#active = controller;
782
+ try {
783
+ await operation(controller.signal);
784
+ } catch (error) {
785
+ if (controller.signal.aborted) {
786
+ throw clientError("request_aborted", "assistant request was aborted", true, void 0, {
787
+ cause: error
788
+ });
789
+ }
790
+ throw error;
791
+ } finally {
792
+ this.#active = void 0;
793
+ }
794
+ }
795
+ #emit(event) {
796
+ for (const listener of this.#listeners) {
797
+ try {
798
+ listener(event);
799
+ } catch {
800
+ }
801
+ }
802
+ }
803
+ };
804
+ function createAssistantClient(options) {
805
+ return new DefaultAssistantClient(options);
806
+ }
807
+ function authorizedHeaders(token, accept) {
808
+ return {
809
+ Authorization: `Bearer ${token}`,
810
+ "Content-Type": "application/json",
811
+ Accept: accept
812
+ };
813
+ }
814
+ function parseSession(value) {
815
+ if (!isRecord3(value) || !isRecord3(value.endpoints)) {
816
+ throw clientError("session_failed", "Assistant session response is invalid", true);
817
+ }
818
+ const interactions = value.endpoints.interactions;
819
+ if (typeof value.token !== "string" || typeof value.expiresAt !== "string" || typeof value.endpoints.turns !== "string" || typeof value.endpoints.toolConfirmations !== "string" || interactions !== void 0 && typeof interactions !== "string") {
820
+ throw clientError("session_failed", "Assistant session response is invalid", true);
821
+ }
822
+ return {
823
+ token: value.token,
824
+ expiresAt: value.expiresAt,
825
+ endpoints: {
826
+ turns: value.endpoints.turns,
827
+ toolConfirmations: value.endpoints.toolConfirmations,
828
+ ...typeof interactions === "string" ? { interactions } : {}
829
+ },
830
+ ...isRecord3(value.configuration) ? { configuration: value.configuration } : {}
831
+ };
832
+ }
833
+ function isRecord3(value) {
834
+ return typeof value === "object" && value !== null && !Array.isArray(value);
835
+ }
836
+ async function readStableErrorCode(response) {
837
+ let value;
272
838
  try {
273
- return { event, data: JSON.parse(data.join("\n")) };
839
+ value = await response.json();
274
840
  } catch {
275
841
  return void 0;
276
842
  }
843
+ if (!isRecord3(value) || typeof value.code !== "string") return void 0;
844
+ return /^[A-Za-z0-9_.-]{1,64}$/.test(value.code) ? value.code : void 0;
845
+ }
846
+ function clientError(code, message, retryable, status, options) {
847
+ return new AssistantClientError(
848
+ { code, ...status === void 0 ? {} : { status }, retryable },
849
+ message,
850
+ options
851
+ );
277
852
  }
278
853
 
854
+ // src/input-request-card.ts
855
+ function createInputRequestCard(options) {
856
+ const card = document.createElement("section");
857
+ card.className = "tool-proposal input-request";
858
+ card.dataset.kind = "input";
859
+ const form = document.createElement("form");
860
+ const heading = document.createElement("strong");
861
+ heading.textContent = options.message;
862
+ form.append(heading);
863
+ const properties = record(options.requestedSchema.properties);
864
+ const required = new Set(
865
+ Array.isArray(options.requestedSchema.required) ? options.requestedSchema.required.filter(
866
+ (value) => typeof value === "string"
867
+ ) : []
868
+ );
869
+ const controls = /* @__PURE__ */ new Map();
870
+ const touched = /* @__PURE__ */ new WeakSet();
871
+ for (const [name, rawProperty] of Object.entries(properties)) {
872
+ const property = record(rawProperty);
873
+ const label = document.createElement("label");
874
+ const caption = document.createElement("span");
875
+ caption.textContent = typeof property.title === "string" && property.title ? property.title : humanize(name);
876
+ const control = createControl(name, property, required.has(name));
877
+ const markTouched = () => {
878
+ touched.add(control);
879
+ updateControlValidity(control, property, required.has(name), touched.has(control));
880
+ };
881
+ control.addEventListener("input", markTouched);
882
+ control.addEventListener("change", markTouched);
883
+ label.append(caption, control);
884
+ form.append(label);
885
+ controls.set(name, control);
886
+ }
887
+ const actions = document.createElement("div");
888
+ actions.className = "proposal-actions";
889
+ const submit = button(options.labels.submit, "submit");
890
+ const decline = button(options.labels.decline, "button");
891
+ const cancel = button(options.labels.cancel, "button");
892
+ const buttons = [submit, decline, cancel];
893
+ const resolve = async (response) => {
894
+ buttons.forEach((item) => {
895
+ item.disabled = true;
896
+ });
897
+ try {
898
+ await options.respond(response);
899
+ } catch (error) {
900
+ buttons.forEach((item) => {
901
+ item.disabled = false;
902
+ });
903
+ throw error;
904
+ }
905
+ };
906
+ form.addEventListener("submit", (event) => {
907
+ event.preventDefault();
908
+ for (const [name, control] of controls) {
909
+ updateControlValidity(
910
+ control,
911
+ record(properties[name]),
912
+ required.has(name),
913
+ touched.has(control)
914
+ );
915
+ }
916
+ if (!form.reportValidity()) return;
917
+ void resolve({
918
+ action: "accept",
919
+ content: readContent(controls, properties, required, touched)
920
+ }).catch(() => {
921
+ });
922
+ });
923
+ decline.addEventListener("click", () => {
924
+ void resolve({ action: "decline" }).catch(() => {
925
+ });
926
+ });
927
+ cancel.addEventListener("click", () => {
928
+ void resolve({ action: "cancel" }).catch(() => {
929
+ });
930
+ });
931
+ actions.append(submit, decline, cancel);
932
+ form.append(actions);
933
+ card.append(form);
934
+ return card;
935
+ }
936
+ function createControl(name, property, required) {
937
+ const choices = stringChoices(property);
938
+ if (choices || property.type === "array") {
939
+ const select = document.createElement("select");
940
+ select.name = name;
941
+ select.multiple = property.type === "array";
942
+ const availableChoices = choices ?? arrayChoices(property);
943
+ if (!select.multiple && (!required || !availableChoices.some((choice) => choice.value === property.default))) {
944
+ select.append(placeholderOption(availableChoices));
945
+ }
946
+ for (const choice of availableChoices) {
947
+ select.append(option(choice.title, choice.value));
948
+ }
949
+ applySelectDefault(select, property.default);
950
+ updateControlValidity(select, property, required, false);
951
+ return select;
952
+ }
953
+ const input = document.createElement("input");
954
+ input.name = name;
955
+ input.type = inputType(property);
956
+ input.required = required && inputNeedsNonEmptyValue(property);
957
+ if (property.type === "integer") input.step = "1";
958
+ if (typeof property.minimum === "number") input.min = String(property.minimum);
959
+ if (typeof property.maximum === "number") input.max = String(property.maximum);
960
+ if (typeof property.minLength === "number") input.minLength = property.minLength;
961
+ if (typeof property.maxLength === "number") input.maxLength = property.maxLength;
962
+ if (property.format === "date-time") input.step = "0.001";
963
+ applyInputDefault(input, property);
964
+ return input;
965
+ }
966
+ function readContent(controls, properties, required, touched) {
967
+ const content = {};
968
+ for (const [name, control] of controls) {
969
+ const property = record(properties[name]);
970
+ if (control instanceof HTMLSelectElement && control.multiple) {
971
+ const values = [...control.selectedOptions].map((option2) => option2.value);
972
+ if (shouldIncludeArray(property, required.has(name), touched.has(control), values.length)) {
973
+ content[name] = values;
974
+ }
975
+ } else if (control instanceof HTMLInputElement && control.type === "checkbox") {
976
+ if (required.has(name) || hasDefault(property) || touched.has(control)) {
977
+ content[name] = control.checked;
978
+ }
979
+ } else if (control instanceof HTMLSelectElement) {
980
+ if (!isPlaceholderSelected(control)) content[name] = control.value;
981
+ } else if (control.value !== "" || property.type === "string" && (required.has(name) || hasDefault(property) || touched.has(control))) {
982
+ if (property.format === "date-time") {
983
+ const value = dateTimeLocalToRfc3339(control.value);
984
+ if (value !== void 0) content[name] = value;
985
+ } else {
986
+ content[name] = property.type === "number" || property.type === "integer" ? Number(control.value) : control.value;
987
+ }
988
+ }
989
+ }
990
+ return content;
991
+ }
992
+ function updateControlValidity(control, property, required, touched) {
993
+ control.setCustomValidity("");
994
+ if (!(control instanceof HTMLSelectElement)) return;
995
+ if (!control.multiple) {
996
+ control.required = required;
997
+ if (required && isPlaceholderSelected(control)) {
998
+ control.setCustomValidity("Please select an option.");
999
+ }
1000
+ return;
1001
+ }
1002
+ const count = control.selectedOptions.length;
1003
+ const included = shouldIncludeArray(property, required, touched, count);
1004
+ const minimum = integerBound(property.minItems);
1005
+ const maximum = integerBound(property.maxItems);
1006
+ control.required = included && minimum !== void 0 && minimum > 0;
1007
+ if (included && minimum !== void 0 && count < minimum) {
1008
+ control.setCustomValidity(`Select at least ${minimum} option${minimum === 1 ? "" : "s"}.`);
1009
+ } else if (included && maximum !== void 0 && count > maximum) {
1010
+ control.setCustomValidity(`Select at most ${maximum} option${maximum === 1 ? "" : "s"}.`);
1011
+ }
1012
+ }
1013
+ function shouldIncludeArray(property, required, touched, selectedCount) {
1014
+ return required || hasDefault(property) || touched || selectedCount > 0;
1015
+ }
1016
+ function hasDefault(property) {
1017
+ return Object.hasOwn(property, "default");
1018
+ }
1019
+ function integerBound(value) {
1020
+ return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : void 0;
1021
+ }
1022
+ function inputNeedsNonEmptyValue(property) {
1023
+ if (property.type === "number" || property.type === "integer") return true;
1024
+ if (property.type !== "string") return false;
1025
+ return property.format === "date" || property.format === "date-time" || property.format === "email" || property.format === "uri" || typeof property.minLength === "number" && property.minLength > 0;
1026
+ }
1027
+ function isPlaceholderSelected(select) {
1028
+ return select.selectedOptions[0]?.dataset.placeholder === "true";
1029
+ }
1030
+ function stringChoices(property) {
1031
+ if (Array.isArray(property.enum) && property.enum.every((value) => typeof value === "string")) {
1032
+ return property.enum.map((value) => ({ value, title: value }));
1033
+ }
1034
+ return titledChoices(property.oneOf) ?? titledChoices(property.anyOf);
1035
+ }
1036
+ function arrayChoices(property) {
1037
+ return stringChoices(record(property.items)) ?? [];
1038
+ }
1039
+ function inputType(property) {
1040
+ if (property.type === "boolean") return "checkbox";
1041
+ if (property.type === "number" || property.type === "integer") return "number";
1042
+ if (property.format === "date") return "date";
1043
+ if (property.format === "date-time") return "datetime-local";
1044
+ if (property.format === "email") return "email";
1045
+ if (property.format === "uri") return "url";
1046
+ return "text";
1047
+ }
1048
+ function titledChoices(value) {
1049
+ if (!Array.isArray(value)) return void 0;
1050
+ const choices = value.map((item) => record(item));
1051
+ if (!choices.every((item) => typeof item.const === "string")) return void 0;
1052
+ return choices.map((item) => ({
1053
+ value: String(item.const),
1054
+ title: typeof item.title === "string" ? item.title : String(item.const)
1055
+ }));
1056
+ }
1057
+ function applySelectDefault(select, value) {
1058
+ if (select.multiple) {
1059
+ const selected = new Set(
1060
+ Array.isArray(value) ? value.filter((item) => typeof item === "string") : []
1061
+ );
1062
+ for (const item of select.options) item.selected = selected.has(item.value);
1063
+ return;
1064
+ }
1065
+ if (typeof value === "string") select.value = value;
1066
+ }
1067
+ function applyInputDefault(input, property) {
1068
+ const value = property.default;
1069
+ if (property.type === "boolean") {
1070
+ if (typeof value === "boolean") input.checked = value;
1071
+ return;
1072
+ }
1073
+ if (property.type === "number" || property.type === "integer") {
1074
+ if (typeof value === "number" && Number.isFinite(value)) input.value = String(value);
1075
+ return;
1076
+ }
1077
+ if (typeof value !== "string") return;
1078
+ if (property.format === "date-time") {
1079
+ input.value = rfc3339ToDateTimeLocal(value) ?? "";
1080
+ return;
1081
+ }
1082
+ input.value = value;
1083
+ }
1084
+ function rfc3339ToDateTimeLocal(value) {
1085
+ const instant = new Date(value);
1086
+ if (Number.isNaN(instant.valueOf())) return void 0;
1087
+ return [
1088
+ String(instant.getFullYear()).padStart(4, "0"),
1089
+ "-",
1090
+ twoDigits(instant.getMonth() + 1),
1091
+ "-",
1092
+ twoDigits(instant.getDate()),
1093
+ "T",
1094
+ twoDigits(instant.getHours()),
1095
+ ":",
1096
+ twoDigits(instant.getMinutes()),
1097
+ ":",
1098
+ twoDigits(instant.getSeconds()),
1099
+ ".",
1100
+ String(instant.getMilliseconds()).padStart(3, "0")
1101
+ ].join("");
1102
+ }
1103
+ function dateTimeLocalToRfc3339(value) {
1104
+ const instant = new Date(value);
1105
+ return Number.isNaN(instant.valueOf()) ? void 0 : instant.toISOString();
1106
+ }
1107
+ function twoDigits(value) {
1108
+ return String(value).padStart(2, "0");
1109
+ }
1110
+ function button(label, type) {
1111
+ const element = document.createElement("button");
1112
+ element.type = type;
1113
+ element.textContent = label;
1114
+ return element;
1115
+ }
1116
+ function option(label, value) {
1117
+ const element = document.createElement("option");
1118
+ element.textContent = label;
1119
+ element.value = value;
1120
+ return element;
1121
+ }
1122
+ function placeholderOption(choices) {
1123
+ let value = "";
1124
+ while (choices.some((choice) => choice.value === value)) value += "\0";
1125
+ const element = option("", value);
1126
+ element.dataset.placeholder = "true";
1127
+ return element;
1128
+ }
1129
+ function humanize(value) {
1130
+ const words = value.replaceAll(/([a-z0-9])([A-Z])/g, "$1 $2").replaceAll(/[_-]+/g, " ");
1131
+ return words ? `${words[0]?.toUpperCase() ?? ""}${words.slice(1)}` : value;
1132
+ }
1133
+ function record(value) {
1134
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
1135
+ }
1136
+
1137
+ // src/interaction-card.ts
1138
+ function createInteractionCard(options) {
1139
+ const card = document.createElement("section");
1140
+ card.className = "tool-proposal";
1141
+ const label = document.createElement("strong");
1142
+ label.textContent = options.tool;
1143
+ card.append(label);
1144
+ if (options.arguments && Object.keys(options.arguments).length > 0) {
1145
+ card.append(createArgumentList(options.arguments));
1146
+ }
1147
+ const actions = document.createElement("div");
1148
+ actions.className = "proposal-actions";
1149
+ const buttons = [];
1150
+ for (const [labelText, action] of [
1151
+ [options.labels.accept, "accept"],
1152
+ [options.labels.decline, "decline"],
1153
+ [options.labels.cancel, "cancel"]
1154
+ ]) {
1155
+ const button2 = document.createElement("button");
1156
+ button2.type = "button";
1157
+ button2.textContent = labelText;
1158
+ buttons.push(button2);
1159
+ button2.addEventListener("click", () => {
1160
+ for (const candidate of buttons) candidate.disabled = true;
1161
+ void options.respond(action).catch(() => {
1162
+ for (const candidate of buttons) candidate.disabled = false;
1163
+ });
1164
+ });
1165
+ actions.append(button2);
1166
+ }
1167
+ card.append(actions);
1168
+ return card;
1169
+ }
1170
+ function createArgumentList(arguments_) {
1171
+ const details = document.createElement("dl");
1172
+ details.className = "proposal-arguments";
1173
+ for (const [name, value] of Object.entries(arguments_)) {
1174
+ const term = document.createElement("dt");
1175
+ term.textContent = humanizeArgumentName(name);
1176
+ const description = document.createElement("dd");
1177
+ description.textContent = formatArgumentValue(value);
1178
+ details.append(term, description);
1179
+ }
1180
+ return details;
1181
+ }
1182
+ function humanizeArgumentName(value) {
1183
+ const words = value.replaceAll(/([a-z0-9])([A-Z])/g, "$1 $2").replaceAll(/[_-]+/g, " ");
1184
+ return words ? `${words[0]?.toUpperCase() ?? ""}${words.slice(1)}` : value;
1185
+ }
1186
+ function formatArgumentValue(value) {
1187
+ if (value === null) return "None";
1188
+ if (typeof value === "string") return value;
1189
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
1190
+ return JSON.stringify(value) ?? "";
1191
+ }
1192
+
1193
+ // src/element-event-controller.ts
1194
+ var AssistantElementEventController = class {
1195
+ #host;
1196
+ #assistantBody;
1197
+ #completedTool;
1198
+ #proposalCards = /* @__PURE__ */ new Map();
1199
+ constructor(host) {
1200
+ this.#host = host;
1201
+ }
1202
+ handle(event) {
1203
+ if (event.event === "view_available") {
1204
+ this.#host.element.dispatchEvent(
1205
+ new CustomEvent("assistant-view-available", {
1206
+ detail: event.data
1207
+ })
1208
+ );
1209
+ return;
1210
+ }
1211
+ if (event.event === "unrecognized") return;
1212
+ const { event: name, data } = event;
1213
+ if (name === "session_started") {
1214
+ if (isRecord4(data.configuration)) {
1215
+ this.#host.applyConfiguration(data.configuration);
1216
+ }
1217
+ this.#host.element.dispatchEvent(
1218
+ new CustomEvent("assistant-session-started", {
1219
+ detail: { expiresAt: String(data.expiresAt ?? "") }
1220
+ })
1221
+ );
1222
+ return;
1223
+ }
1224
+ if (name === "session_expired") {
1225
+ this.#host.element.dispatchEvent(new CustomEvent("assistant-session-expired"));
1226
+ return;
1227
+ }
1228
+ if (name === "session_reset") {
1229
+ this.resetConversation();
1230
+ return;
1231
+ }
1232
+ if (name === "context_changed") {
1233
+ this.#host.element.dispatchEvent(
1234
+ new CustomEvent("assistant-context-changed", { detail: { context: data.context } })
1235
+ );
1236
+ return;
1237
+ }
1238
+ if (name === "model_context_changed") {
1239
+ this.#host.element.dispatchEvent(
1240
+ new CustomEvent("assistant-model-context-changed", {
1241
+ detail: { modelContext: data.modelContext }
1242
+ })
1243
+ );
1244
+ return;
1245
+ }
1246
+ if (name === "message_started") {
1247
+ this.#assistantBody = void 0;
1248
+ this.#host.appendMessage("user", String(data.message ?? ""));
1249
+ this.#host.element.dispatchEvent(new CustomEvent("assistant-message-started"));
1250
+ return;
1251
+ }
1252
+ if (name === "message_completed") {
1253
+ this.#assistantBody = void 0;
1254
+ this.#host.element.dispatchEvent(new CustomEvent("assistant-message-completed"));
1255
+ return;
1256
+ }
1257
+ if (name === "interaction_started") {
1258
+ this.#assistantBody = void 0;
1259
+ this.#completedTool = void 0;
1260
+ return;
1261
+ }
1262
+ if (name === "interaction_completed") {
1263
+ this.#finishInteraction(String(data.id ?? ""), String(data.action ?? "accept"));
1264
+ return;
1265
+ }
1266
+ if (name === "content") {
1267
+ const body = this.#assistantBody ?? this.#host.appendMessage("assistant", "");
1268
+ this.#assistantBody = body;
1269
+ if (body) body.textContent += String(data.delta ?? "");
1270
+ return;
1271
+ }
1272
+ if (name === "tool_proposed" || name === "interaction_proposed") {
1273
+ this.#appendToolProposal(
1274
+ String(data.id ?? ""),
1275
+ String(data.tool ?? ("title" in data ? data.title : void 0) ?? "Action"),
1276
+ isRecord4(data.arguments) ? data.arguments : void 0
1277
+ );
1278
+ return;
1279
+ }
1280
+ if (name === "input_requested" && isRecord4(data.requestedSchema)) {
1281
+ this.#appendInputRequest(
1282
+ String(data.id ?? ""),
1283
+ String(data.message ?? "More information is required"),
1284
+ data.requestedSchema
1285
+ );
1286
+ return;
1287
+ }
1288
+ if (name === "tool_completed") {
1289
+ this.#completedTool = data;
1290
+ return;
1291
+ }
1292
+ if (name === "error") {
1293
+ this.#host.dispatchError({
1294
+ code: String(data.code ?? "stream_failed"),
1295
+ ...typeof data.status === "number" ? { status: data.status } : {},
1296
+ retryable: data.retryable === true
1297
+ });
1298
+ }
1299
+ }
1300
+ resetConversation() {
1301
+ this.#host.messages()?.replaceChildren();
1302
+ this.#proposalCards.clear();
1303
+ this.#assistantBody = void 0;
1304
+ this.#completedTool = void 0;
1305
+ this.#host.element.dispatchEvent(new CustomEvent("assistant-session-reset"));
1306
+ }
1307
+ #finishInteraction(id, action) {
1308
+ if (this.#completedTool && !this.#assistantBody) {
1309
+ this.#host.appendMessage("assistant", JSON.stringify(this.#completedTool.result ?? {}) ?? "");
1310
+ }
1311
+ this.#proposalCards.get(id)?.remove();
1312
+ this.#proposalCards.delete(id);
1313
+ if (action === "accept") {
1314
+ this.#host.element.dispatchEvent(
1315
+ new CustomEvent("assistant-tool-completed", { detail: { id } })
1316
+ );
1317
+ }
1318
+ this.#host.element.dispatchEvent(
1319
+ new CustomEvent("assistant-interaction-resolved", { detail: { id, action } })
1320
+ );
1321
+ this.#assistantBody = void 0;
1322
+ this.#completedTool = void 0;
1323
+ }
1324
+ #appendToolProposal(id, tool, arguments_) {
1325
+ const messages = this.#host.messages();
1326
+ if (!messages || !id) return;
1327
+ this.#proposalCards.get(id)?.remove();
1328
+ const labels = this.#host.labels();
1329
+ const card = createInteractionCard({
1330
+ tool,
1331
+ ...arguments_ ? { arguments: arguments_ } : {},
1332
+ labels: {
1333
+ accept: labels.confirm,
1334
+ decline: "Decline",
1335
+ cancel: labels.cancel
1336
+ },
1337
+ respond: (action) => this.#host.respond(id, { action })
1338
+ });
1339
+ messages.append(card);
1340
+ this.#proposalCards.set(id, card);
1341
+ this.#host.element.dispatchEvent(
1342
+ new CustomEvent("assistant-tool-proposed", { detail: { id, tool, arguments: arguments_ } })
1343
+ );
1344
+ }
1345
+ #appendInputRequest(id, message, requestedSchema) {
1346
+ const messages = this.#host.messages();
1347
+ if (!messages || !id) return;
1348
+ this.#proposalCards.get(id)?.remove();
1349
+ const labels = this.#host.labels();
1350
+ const card = createInputRequestCard({
1351
+ message,
1352
+ requestedSchema,
1353
+ labels: {
1354
+ submit: labels.confirm,
1355
+ decline: "Decline",
1356
+ cancel: labels.cancel
1357
+ },
1358
+ respond: (response) => this.#host.respond(id, response)
1359
+ });
1360
+ messages.append(card);
1361
+ this.#proposalCards.set(id, card);
1362
+ this.#host.element.dispatchEvent(
1363
+ new CustomEvent("assistant-input-requested", {
1364
+ detail: { id, message, requestedSchema }
1365
+ })
1366
+ );
1367
+ }
1368
+ };
1369
+ function isRecord4(value) {
1370
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1371
+ }
1372
+
1373
+ // src/element-styles.ts
1374
+ var ASSISTANT_ELEMENT_STYLES = `<style>
1375
+ :host { color-scheme: light dark; font-family: var(--ns-assistant-font-family, var(--ns-assistant-default-font-family)); font-size: var(--ns-assistant-base-font-size, var(--ns-assistant-default-base-font-size)); line-height: var(--ns-assistant-line-height, var(--ns-assistant-default-line-height)); z-index: var(--ns-assistant-z-index, var(--ns-assistant-default-z-index)); }
1376
+ :host([data-mode="floating"]) { position: fixed; bottom: 24px; right: 24px; }
1377
+ :host([data-mode="floating"][data-position="bottom-left"]) { right: auto; left: 24px; }
1378
+ .launcher { width: 54px; height: 54px; border: 0; border-radius: var(--ns-assistant-launcher-radius, var(--ns-assistant-default-launcher-radius)); color: var(--ns-assistant-accent-text, var(--ns-assistant-default-accent-text)); background: var(--ns-assistant-accent, var(--ns-assistant-default-accent)); box-shadow: var(--ns-assistant-shadow, var(--ns-assistant-default-shadow)); cursor: pointer; }
1379
+ .launcher img { width: 24px; height: 24px; object-fit: contain; }
1380
+ .panel { display: none; width: min(var(--ns-assistant-panel-width, var(--ns-assistant-default-panel-width)), calc(100vw - 32px)); min-height: var(--ns-assistant-min-height, var(--ns-assistant-default-min-height)); max-height: min(var(--ns-assistant-max-height, var(--ns-assistant-default-max-height)), calc(100vh - 32px)); overflow: hidden; color: var(--ns-assistant-text, var(--ns-assistant-default-text)); background: var(--ns-assistant-panel, var(--ns-assistant-default-panel)); border-radius: var(--ns-assistant-panel-radius, var(--ns-assistant-default-panel-radius)); box-shadow: var(--ns-assistant-shadow, var(--ns-assistant-default-shadow)); }
1381
+ :host([open]) .panel { display: grid; grid-template-rows: auto auto 1fr auto; }
1382
+ :host([open]) .launcher { display: none; }
1383
+ header { display: flex; gap: var(--ns-assistant-spacing, var(--ns-assistant-default-spacing)); align-items: center; padding: calc(var(--ns-assistant-spacing, var(--ns-assistant-default-spacing)) * 1.2); border-bottom: 1px solid var(--ns-assistant-divider, var(--ns-assistant-default-divider)); }
1384
+ header strong { flex: 1; }
1385
+ .brand-logo { display: block; max-width: 120px; max-height: 28px; object-fit: contain; }
1386
+ button { font: inherit; }
1387
+ .close, .send { border: 0; border-radius: var(--ns-assistant-button-radius, var(--ns-assistant-default-button-radius)); cursor: pointer; }
1388
+ .close { color: var(--ns-assistant-muted-text, var(--ns-assistant-default-muted-text)); background: transparent; font-size: 24px; }
1389
+ .welcome { padding: calc(var(--ns-assistant-spacing, var(--ns-assistant-default-spacing)) * 1.5); }
1390
+ .welcome h2 { margin: 0; font-size: 1.25em; }
1391
+ .welcome p { margin: 6px 0 0; color: var(--ns-assistant-muted-text, var(--ns-assistant-default-muted-text)); }
1392
+ .suggested-prompts { display: flex; flex-wrap: wrap; gap: 8px; padding: 0 var(--ns-assistant-spacing, var(--ns-assistant-default-spacing)); }
1393
+ .suggested-prompts:empty { display: none; }
1394
+ .suggested-prompts button { padding: 7px 10px; color: var(--ns-assistant-text, var(--ns-assistant-default-text)); background: transparent; border: 1px solid var(--ns-assistant-divider, var(--ns-assistant-default-divider)); border-radius: 999px; cursor: pointer; }
1395
+ .messages { min-height: 120px; padding: var(--ns-assistant-spacing, var(--ns-assistant-default-spacing)); overflow: auto; }
1396
+ .message { width: fit-content; max-width: 84%; margin: 8px 0; padding: 10px 12px; border-radius: var(--ns-assistant-card-radius, var(--ns-assistant-default-card-radius)); background: var(--ns-assistant-elevated, var(--ns-assistant-default-elevated)); white-space: pre-wrap; }
1397
+ .message .avatar { float: left; width: 24px; height: 24px; margin-right: 8px; border-radius: 50%; object-fit: cover; }
1398
+ .message time { display: block; margin-top: 4px; color: var(--ns-assistant-muted-text, var(--ns-assistant-default-muted-text)); font-size: .75em; }
1399
+ .message.user { margin-left: auto; color: var(--ns-assistant-accent-text, var(--ns-assistant-default-accent-text)); background: var(--ns-assistant-accent, var(--ns-assistant-default-accent)); }
1400
+ .tool-proposal { display: grid; gap: 10px; margin: 10px 0; padding: 12px; border-radius: var(--ns-assistant-card-radius, var(--ns-assistant-default-card-radius)); background: var(--ns-assistant-elevated, var(--ns-assistant-default-elevated)); }
1401
+ .proposal-arguments { display: grid; grid-template-columns: minmax(90px, auto) 1fr; gap: 4px 10px; margin: 0; font-size: .9em; }
1402
+ .proposal-arguments dt { color: var(--ns-assistant-muted-text, var(--ns-assistant-default-muted-text)); }
1403
+ .proposal-arguments dd { margin: 0; overflow-wrap: anywhere; }
1404
+ .proposal-actions { display: flex; flex-wrap: wrap; gap: 8px; justify-content: end; }
1405
+ .tool-proposal button { padding: 8px 10px; border: 0; border-radius: var(--ns-assistant-button-radius, var(--ns-assistant-default-button-radius)); color: var(--ns-assistant-text, var(--ns-assistant-default-text)); background: var(--ns-assistant-input, var(--ns-assistant-default-input)); cursor: pointer; }
1406
+ .tool-proposal button:first-of-type { color: var(--ns-assistant-accent-text, var(--ns-assistant-default-accent-text)); background: var(--ns-assistant-accent, var(--ns-assistant-default-accent)); }
1407
+ .input-request form { display: grid; gap: 10px; padding: 0; border: 0; }
1408
+ .input-request label { display: grid; gap: 4px; color: var(--ns-assistant-muted-text, var(--ns-assistant-default-muted-text)); font-size: .9em; }
1409
+ .input-request input, .input-request select { width: 100%; box-sizing: border-box; padding: 8px 10px; color: var(--ns-assistant-text, var(--ns-assistant-default-text)); background: var(--ns-assistant-input, var(--ns-assistant-default-input)); border: 1px solid var(--ns-assistant-divider, var(--ns-assistant-default-divider)); border-radius: var(--ns-assistant-input-radius, var(--ns-assistant-default-input-radius)); font: inherit; }
1410
+ .input-request input[type="checkbox"] { width: auto; justify-self: start; }
1411
+ form { display: flex; gap: 8px; align-items: end; padding: var(--ns-assistant-spacing, var(--ns-assistant-default-spacing)); border-top: 1px solid var(--ns-assistant-divider, var(--ns-assistant-default-divider)); }
1412
+ textarea { flex: 1; resize: none; padding: 10px 12px; color: var(--ns-assistant-text, var(--ns-assistant-default-text)); background: var(--ns-assistant-input, var(--ns-assistant-default-input)); border: 0; border-radius: var(--ns-assistant-input-radius, var(--ns-assistant-default-input-radius)); font: inherit; }
1413
+ textarea:focus-visible, button:focus-visible { outline: 3px solid var(--ns-assistant-focus, var(--ns-assistant-default-focus)); outline-offset: 2px; }
1414
+ .send { padding: 10px 14px; color: var(--ns-assistant-accent-text, var(--ns-assistant-default-accent-text)); background: var(--ns-assistant-accent, var(--ns-assistant-default-accent)); }
1415
+ .legal { display: flex; gap: 12px; justify-content: center; padding: 0 12px 10px; font-size: .8em; }
1416
+ .legal:empty { display: none; }
1417
+ .legal a { color: var(--ns-assistant-link, var(--ns-assistant-default-link)); }
1418
+ :host([data-density="compact"]) .welcome { padding: var(--ns-assistant-spacing, var(--ns-assistant-default-spacing)); }
1419
+ @media (max-width: 640px) { :host([mobile-fullscreen][open]) { inset: 0; } :host([mobile-fullscreen][open]) .panel { width: 100vw; min-height: 100vh; max-height: 100vh; border-radius: 0; } }
1420
+ @media (prefers-reduced-motion: reduce) { *, *::before, *::after { transition-duration: 0.01ms !important; animation-duration: 0.01ms !important; } }
1421
+ @media (max-width: 560px) { :host([data-mode="floating"]) { inset: auto 12px 12px; } :host([open]) .panel { width: calc(100vw - 24px); max-height: calc(100vh - 24px); } }
1422
+ </style>`;
1423
+
279
1424
  // src/element.ts
280
1425
  var ASSISTANT_TAG_NAME = "noodle-assistant";
281
1426
  var HTMLElementBase = globalThis.HTMLElement ?? class {
@@ -283,11 +1428,29 @@ var HTMLElementBase = globalThis.HTMLElement ?? class {
283
1428
  var NoodleAssistantElement = class extends HTMLElementBase {
284
1429
  static observedAttributes = ["open", "theme"];
285
1430
  #appearance = resolveAppearance();
286
- #session;
1431
+ #client;
1432
+ #clientEndpoint = "";
1433
+ #unsubscribeClient;
287
1434
  #messages;
288
1435
  #media;
289
1436
  #context;
1437
+ #modelContext;
290
1438
  sessionEndpoint = "";
1439
+ #events = new AssistantElementEventController({
1440
+ element: this,
1441
+ messages: () => this.#messages,
1442
+ appendMessage: (role, text) => this.#appendMessage(role, text),
1443
+ applyConfiguration: (configuration) => {
1444
+ this.#appearance = resolveAppearance(configuration);
1445
+ this.#render();
1446
+ },
1447
+ labels: () => ({
1448
+ confirm: this.#appearance.labels.confirm,
1449
+ cancel: this.#appearance.labels.cancel
1450
+ }),
1451
+ respond: (id, response) => this.respond(id, response),
1452
+ dispatchError: (detail) => this.#dispatchError(detail)
1453
+ });
291
1454
  connectedCallback() {
292
1455
  if (!this.shadowRoot) this.attachShadow({ mode: "open" });
293
1456
  if (!this.sessionEndpoint) this.sessionEndpoint = this.getAttribute("session-endpoint") ?? "";
@@ -300,6 +1463,11 @@ var NoodleAssistantElement = class extends HTMLElementBase {
300
1463
  this.dispatchEvent(new CustomEvent("assistant-ready"));
301
1464
  }
302
1465
  disconnectedCallback() {
1466
+ this.#client?.abort();
1467
+ this.#unsubscribeClient?.();
1468
+ this.#client = void 0;
1469
+ this.#clientEndpoint = "";
1470
+ this.#unsubscribeClient = void 0;
303
1471
  this.#media?.removeEventListener("change", this.#handleSystemTheme);
304
1472
  document.removeEventListener("keydown", this.#handleDocumentKeydown);
305
1473
  document.removeEventListener("pointerdown", this.#handleDocumentPointerDown);
@@ -322,71 +1490,44 @@ var NoodleAssistantElement = class extends HTMLElementBase {
322
1490
  this.shadowRoot?.querySelector("textarea")?.focus();
323
1491
  }
324
1492
  resetSession() {
325
- this.#session = void 0;
326
- this.#messages?.replaceChildren();
327
- this.dispatchEvent(new CustomEvent("assistant-session-reset"));
1493
+ if (this.#client) {
1494
+ this.#client.resetSession();
1495
+ } else {
1496
+ this.#events.resetConversation();
1497
+ }
328
1498
  }
329
1499
  updateContext(context) {
330
1500
  this.#context = { ...context };
331
- this.dispatchEvent(new CustomEvent("assistant-context-changed", { detail: { context } }));
1501
+ if (this.#client) {
1502
+ this.#client.updateContext(this.#context);
1503
+ } else {
1504
+ this.dispatchEvent(
1505
+ new CustomEvent("assistant-context-changed", { detail: { context: this.#context } })
1506
+ );
1507
+ }
1508
+ }
1509
+ /** Replace the compact renderer summary attached to subsequent message turns. */
1510
+ updateModelContext(update) {
1511
+ this.#modelContext = copyAssistantModelContext(update);
1512
+ if (this.#client) {
1513
+ this.#client.updateModelContext(this.#modelContext);
1514
+ } else {
1515
+ this.dispatchEvent(
1516
+ new CustomEvent("assistant-model-context-changed", {
1517
+ detail: { modelContext: this.#modelContext }
1518
+ })
1519
+ );
1520
+ }
332
1521
  }
333
1522
  async sendMessage(text) {
334
1523
  const message = text.trim();
335
1524
  if (!message) return;
336
- if (!this.#session) this.#session = await this.#createSession();
337
- this.#appendMessage("user", message);
338
- this.dispatchEvent(new CustomEvent("assistant-message-started"));
339
- await this.#sendTurn(message, true);
340
- }
341
- async #sendTurn(message, mayRetry) {
342
- if (!this.#session) this.#session = await this.#createSession();
343
- const response = await fetch(this.#session.endpoints.turns, {
344
- method: "POST",
345
- headers: {
346
- Authorization: `Bearer ${this.#session.token}`,
347
- "Content-Type": "application/json",
348
- Accept: "text/event-stream"
349
- },
350
- body: JSON.stringify({ message })
351
- });
352
- if (response.status === 401 && mayRetry) {
353
- this.#session = void 0;
354
- this.dispatchEvent(new CustomEvent("assistant-session-expired"));
355
- await this.#sendTurn(message, false);
356
- return;
357
- }
358
- if (!response.ok) {
359
- this.#dispatchError({ code: "turn_failed", status: response.status, retryable: false });
360
- throw new Error(`Assistant turn failed (${response.status})`);
361
- }
362
- let assistantBody;
363
1525
  try {
364
- await consumeAssistantEvents(response, (event) => {
365
- if (event.event === "content") {
366
- const body = assistantBody ?? this.#appendMessage("assistant", "");
367
- assistantBody = body;
368
- if (body) body.textContent += String(event.data.delta ?? "");
369
- }
370
- if (event.event === "tool_proposed") {
371
- this.#appendToolProposal(
372
- String(event.data.id ?? ""),
373
- String(event.data.tool ?? "Action")
374
- );
375
- }
376
- if (event.event === "error") {
377
- this.#dispatchError({
378
- code: String(event.data.code ?? "stream_failed"),
379
- retryable: event.data.retryable === true
380
- });
381
- }
382
- });
1526
+ await this.#ensureClient().sendMessage(message);
383
1527
  } catch (error) {
384
- if (error instanceof AssistantTransportError) {
385
- this.#dispatchError({ code: error.code, retryable: false });
386
- }
1528
+ this.#dispatchClientError(error, "turn_failed");
387
1529
  throw error;
388
1530
  }
389
- this.dispatchEvent(new CustomEvent("assistant-message-completed"));
390
1531
  }
391
1532
  /**
392
1533
  * Every listener the element attaches to its own DOM must go through this guard: internal UI
@@ -399,46 +1540,15 @@ var NoodleAssistantElement = class extends HTMLElementBase {
399
1540
  });
400
1541
  }
401
1542
  async confirmTool(id) {
402
- if (!this.#session) {
403
- this.#dispatchError({ code: "confirmation_expired", retryable: false });
404
- throw new Error("assistant session is not active");
405
- }
406
- const response = await fetch(this.#session.endpoints.toolConfirmations, {
407
- method: "POST",
408
- headers: {
409
- Authorization: `Bearer ${this.#session.token}`,
410
- "Content-Type": "application/json",
411
- Accept: "text/event-stream"
412
- },
413
- body: JSON.stringify({ id })
414
- });
415
- if (!response.ok) {
416
- const expired = response.status === 401 || response.status === 409;
417
- if (response.status === 401) {
418
- this.#session = void 0;
419
- this.dispatchEvent(new CustomEvent("assistant-session-expired"));
420
- }
421
- this.#dispatchError({
422
- code: expired ? "confirmation_expired" : "confirmation_failed",
423
- status: response.status,
424
- retryable: false
425
- });
426
- throw new Error(`Assistant confirmation failed (${response.status})`);
427
- }
428
- let completed;
429
- let narrationBody;
430
- await consumeAssistantEvents(response, (event) => {
431
- if (event.event === "tool_completed") completed = event.data;
432
- if (event.event === "content") {
433
- const body = narrationBody ?? this.#appendMessage("assistant", "");
434
- narrationBody = body;
435
- if (body) body.textContent += String(event.data.delta ?? "");
436
- }
437
- });
438
- if (completed && !narrationBody) {
439
- this.#appendMessage("assistant", JSON.stringify(completed.result ?? {}));
1543
+ await this.respond(id, { action: "accept" });
1544
+ }
1545
+ async respond(id, response) {
1546
+ try {
1547
+ await this.#ensureClient().respond(id, response);
1548
+ } catch (error) {
1549
+ this.#dispatchClientError(error, "confirmation_failed");
1550
+ throw error;
440
1551
  }
441
- this.dispatchEvent(new CustomEvent("assistant-tool-completed", { detail: { id } }));
442
1552
  }
443
1553
  #handleSystemTheme = () => this.#applyTheme();
444
1554
  #handleDocumentKeydown = (event) => {
@@ -451,39 +1561,20 @@ var NoodleAssistantElement = class extends HTMLElementBase {
451
1561
  this.close();
452
1562
  }
453
1563
  };
454
- async #createSession() {
455
- if (!this.sessionEndpoint) throw new Error("sessionEndpoint is required");
456
- let response;
457
- try {
458
- response = await fetch(this.sessionEndpoint, {
459
- method: "POST",
460
- headers: { Accept: "application/json", "Content-Type": "application/json" },
461
- body: JSON.stringify(this.#context ? { context: this.#context } : {}),
462
- credentials: "same-origin"
463
- });
464
- } catch (error) {
465
- this.#dispatchError({ code: "session_failed", retryable: true });
466
- throw error;
467
- }
468
- if (!response.ok) {
469
- this.#dispatchError({ code: "session_failed", status: response.status, retryable: true });
470
- throw new Error(`Assistant session failed (${response.status})`);
471
- }
472
- let session;
473
- try {
474
- session = await response.json();
475
- } catch (error) {
476
- this.#dispatchError({ code: "session_failed", retryable: true });
477
- throw error;
478
- }
479
- if (session.configuration) {
480
- this.#appearance = resolveAppearance(session.configuration);
481
- this.#render();
482
- }
483
- this.dispatchEvent(
484
- new CustomEvent("assistant-session-started", { detail: { expiresAt: session.expiresAt } })
485
- );
486
- return session;
1564
+ #ensureClient() {
1565
+ if (this.#client && this.#clientEndpoint === this.sessionEndpoint) return this.#client;
1566
+ this.#client?.abort();
1567
+ this.#unsubscribeClient?.();
1568
+ const client = createAssistantClient({
1569
+ sessionEndpoint: this.sessionEndpoint,
1570
+ ...this.#context ? { context: this.#context } : {},
1571
+ clientContext: browserClientContext,
1572
+ ...this.#modelContext ? { modelContext: this.#modelContext } : {}
1573
+ });
1574
+ this.#client = client;
1575
+ this.#clientEndpoint = this.sessionEndpoint;
1576
+ this.#unsubscribeClient = client.subscribe((event) => this.#events.handle(event));
1577
+ return client;
487
1578
  }
488
1579
  #appendMessage(role, text) {
489
1580
  if (!this.#messages) return void 0;
@@ -514,31 +1605,10 @@ var NoodleAssistantElement = class extends HTMLElementBase {
514
1605
  #dispatchError(detail) {
515
1606
  this.dispatchEvent(new CustomEvent("assistant-error", { detail }));
516
1607
  }
517
- #appendToolProposal(id, tool) {
518
- if (!this.#messages || !id) return;
519
- const card = document.createElement("section");
520
- card.className = "tool-proposal";
521
- const label = document.createElement("strong");
522
- label.textContent = tool;
523
- const confirm = document.createElement("button");
524
- confirm.type = "button";
525
- confirm.textContent = this.#appearance.labels.confirm;
526
- confirm.addEventListener("click", () => {
527
- confirm.disabled = true;
528
- void this.confirmTool(id).catch(() => {
529
- confirm.disabled = false;
530
- this.dispatchEvent(
531
- new CustomEvent("assistant-error", { detail: { code: "confirmation_failed" } })
532
- );
533
- });
534
- });
535
- const cancel = document.createElement("button");
536
- cancel.type = "button";
537
- cancel.textContent = this.#appearance.labels.cancel;
538
- cancel.addEventListener("click", () => card.remove());
539
- card.append(label, confirm, cancel);
540
- this.#messages.append(card);
541
- this.dispatchEvent(new CustomEvent("assistant-tool-proposed", { detail: { id, tool } }));
1608
+ #dispatchClientError(error, fallbackCode) {
1609
+ this.#dispatchError(
1610
+ error instanceof AssistantClientError ? error.detail : { code: fallbackCode, retryable: false }
1611
+ );
542
1612
  }
543
1613
  #resolvedMode() {
544
1614
  const configured = this.getAttribute("theme") ?? this.#appearance.theme.defaultMode;
@@ -579,6 +1649,7 @@ var NoodleAssistantElement = class extends HTMLElementBase {
579
1649
  }
580
1650
  #render() {
581
1651
  if (!this.shadowRoot) return;
1652
+ const conversation = this.#messages ? [...this.#messages.childNodes] : [];
582
1653
  const appearance = this.#appearance;
583
1654
  this.setAttribute("role", "complementary");
584
1655
  this.setAttribute("aria-label", appearance.brand.name);
@@ -588,7 +1659,7 @@ var NoodleAssistantElement = class extends HTMLElementBase {
588
1659
  this.dataset.position = appearance.layout.position;
589
1660
  this.dataset.density = appearance.layout.density;
590
1661
  this.toggleAttribute("mobile-fullscreen", appearance.layout.mobileFullscreen);
591
- this.shadowRoot.innerHTML = `${styles}
1662
+ this.shadowRoot.innerHTML = `${ASSISTANT_ELEMENT_STYLES}
592
1663
  <button class="launcher" type="button" aria-label="${escapeAttribute(appearance.labels.open)}"><slot name="launcher-icon">\u2726</slot></button>
593
1664
  <section class="panel" aria-label="${escapeAttribute(appearance.brand.name)}">
594
1665
  <header><slot name="header-leading"></slot><img class="brand-logo" alt=""><strong></strong><slot name="header-actions"></slot><button class="close" type="button" aria-label="${escapeAttribute(appearance.labels.close)}">\xD7</button></header>
@@ -627,11 +1698,11 @@ var NoodleAssistantElement = class extends HTMLElementBase {
627
1698
  queryRequired(this.shadowRoot, ".send").textContent = appearance.labels.send;
628
1699
  const prompts = queryRequired(this.shadowRoot, ".suggested-prompts");
629
1700
  for (const prompt of appearance.suggestedPrompts) {
630
- const button = document.createElement("button");
631
- button.type = "button";
632
- button.textContent = prompt;
633
- button.addEventListener("click", () => this.#sendGuarded(prompt));
634
- prompts.append(button);
1701
+ const button2 = document.createElement("button");
1702
+ button2.type = "button";
1703
+ button2.textContent = prompt;
1704
+ button2.addEventListener("click", () => this.#sendGuarded(prompt));
1705
+ prompts.append(button2);
635
1706
  }
636
1707
  if (!appearance.behavior.showLauncher)
637
1708
  queryRequired(this.shadowRoot, ".launcher").hidden = true;
@@ -651,6 +1722,7 @@ var NoodleAssistantElement = class extends HTMLElementBase {
651
1722
  legal.append(link);
652
1723
  }
653
1724
  this.#messages = this.shadowRoot.querySelector(".messages") ?? void 0;
1725
+ this.#messages?.append(...conversation);
654
1726
  this.shadowRoot.querySelector(".launcher")?.addEventListener("click", () => this.open());
655
1727
  this.shadowRoot.querySelector(".close")?.addEventListener("click", () => this.close());
656
1728
  this.shadowRoot.querySelector("form")?.addEventListener("submit", (event) => {
@@ -683,47 +1755,6 @@ function queryRequired(root, selector) {
683
1755
  if (!element) throw new Error(`assistant renderer is missing ${selector}`);
684
1756
  return element;
685
1757
  }
686
- var styles = `<style>
687
- :host { color-scheme: light dark; font-family: var(--ns-assistant-font-family, var(--ns-assistant-default-font-family)); font-size: var(--ns-assistant-base-font-size, var(--ns-assistant-default-base-font-size)); line-height: var(--ns-assistant-line-height, var(--ns-assistant-default-line-height)); z-index: var(--ns-assistant-z-index, var(--ns-assistant-default-z-index)); }
688
- :host([data-mode="floating"]) { position: fixed; bottom: 24px; right: 24px; }
689
- :host([data-mode="floating"][data-position="bottom-left"]) { right: auto; left: 24px; }
690
- .launcher { width: 54px; height: 54px; border: 0; border-radius: var(--ns-assistant-launcher-radius, var(--ns-assistant-default-launcher-radius)); color: var(--ns-assistant-accent-text, var(--ns-assistant-default-accent-text)); background: var(--ns-assistant-accent, var(--ns-assistant-default-accent)); box-shadow: var(--ns-assistant-shadow, var(--ns-assistant-default-shadow)); cursor: pointer; }
691
- .launcher img { width: 24px; height: 24px; object-fit: contain; }
692
- .panel { display: none; width: min(var(--ns-assistant-panel-width, var(--ns-assistant-default-panel-width)), calc(100vw - 32px)); min-height: var(--ns-assistant-min-height, var(--ns-assistant-default-min-height)); max-height: min(var(--ns-assistant-max-height, var(--ns-assistant-default-max-height)), calc(100vh - 32px)); overflow: hidden; color: var(--ns-assistant-text, var(--ns-assistant-default-text)); background: var(--ns-assistant-panel, var(--ns-assistant-default-panel)); border-radius: var(--ns-assistant-panel-radius, var(--ns-assistant-default-panel-radius)); box-shadow: var(--ns-assistant-shadow, var(--ns-assistant-default-shadow)); }
693
- :host([open]) .panel { display: grid; grid-template-rows: auto auto 1fr auto; }
694
- :host([open]) .launcher { display: none; }
695
- header { display: flex; gap: var(--ns-assistant-spacing, var(--ns-assistant-default-spacing)); align-items: center; padding: calc(var(--ns-assistant-spacing, var(--ns-assistant-default-spacing)) * 1.2); border-bottom: 1px solid var(--ns-assistant-divider, var(--ns-assistant-default-divider)); }
696
- header strong { flex: 1; }
697
- .brand-logo { display: block; max-width: 120px; max-height: 28px; object-fit: contain; }
698
- button { font: inherit; }
699
- .close, .send { border: 0; border-radius: var(--ns-assistant-button-radius, var(--ns-assistant-default-button-radius)); cursor: pointer; }
700
- .close { color: var(--ns-assistant-muted-text, var(--ns-assistant-default-muted-text)); background: transparent; font-size: 24px; }
701
- .welcome { padding: calc(var(--ns-assistant-spacing, var(--ns-assistant-default-spacing)) * 1.5); }
702
- .welcome h2 { margin: 0; font-size: 1.25em; }
703
- .welcome p { margin: 6px 0 0; color: var(--ns-assistant-muted-text, var(--ns-assistant-default-muted-text)); }
704
- .suggested-prompts { display: flex; flex-wrap: wrap; gap: 8px; padding: 0 var(--ns-assistant-spacing, var(--ns-assistant-default-spacing)); }
705
- .suggested-prompts:empty { display: none; }
706
- .suggested-prompts button { padding: 7px 10px; color: var(--ns-assistant-text, var(--ns-assistant-default-text)); background: transparent; border: 1px solid var(--ns-assistant-divider, var(--ns-assistant-default-divider)); border-radius: 999px; cursor: pointer; }
707
- .messages { min-height: 120px; padding: var(--ns-assistant-spacing, var(--ns-assistant-default-spacing)); overflow: auto; }
708
- .message { width: fit-content; max-width: 84%; margin: 8px 0; padding: 10px 12px; border-radius: var(--ns-assistant-card-radius, var(--ns-assistant-default-card-radius)); background: var(--ns-assistant-elevated, var(--ns-assistant-default-elevated)); white-space: pre-wrap; }
709
- .message .avatar { float: left; width: 24px; height: 24px; margin-right: 8px; border-radius: 50%; object-fit: cover; }
710
- .message time { display: block; margin-top: 4px; color: var(--ns-assistant-muted-text, var(--ns-assistant-default-muted-text)); font-size: .75em; }
711
- .message.user { margin-left: auto; color: var(--ns-assistant-accent-text, var(--ns-assistant-default-accent-text)); background: var(--ns-assistant-accent, var(--ns-assistant-default-accent)); }
712
- .tool-proposal { display: grid; grid-template-columns: 1fr auto auto; gap: 8px; align-items: center; margin: 10px 0; padding: 12px; border-radius: var(--ns-assistant-card-radius, var(--ns-assistant-default-card-radius)); background: var(--ns-assistant-elevated, var(--ns-assistant-default-elevated)); }
713
- .tool-proposal button { padding: 8px 10px; border: 0; border-radius: var(--ns-assistant-button-radius, var(--ns-assistant-default-button-radius)); color: var(--ns-assistant-text, var(--ns-assistant-default-text)); background: var(--ns-assistant-input, var(--ns-assistant-default-input)); cursor: pointer; }
714
- .tool-proposal button:first-of-type { color: var(--ns-assistant-accent-text, var(--ns-assistant-default-accent-text)); background: var(--ns-assistant-accent, var(--ns-assistant-default-accent)); }
715
- form { display: flex; gap: 8px; align-items: end; padding: var(--ns-assistant-spacing, var(--ns-assistant-default-spacing)); border-top: 1px solid var(--ns-assistant-divider, var(--ns-assistant-default-divider)); }
716
- textarea { flex: 1; resize: none; padding: 10px 12px; color: var(--ns-assistant-text, var(--ns-assistant-default-text)); background: var(--ns-assistant-input, var(--ns-assistant-default-input)); border: 0; border-radius: var(--ns-assistant-input-radius, var(--ns-assistant-default-input-radius)); font: inherit; }
717
- textarea:focus-visible, button:focus-visible { outline: 3px solid var(--ns-assistant-focus, var(--ns-assistant-default-focus)); outline-offset: 2px; }
718
- .send { padding: 10px 14px; color: var(--ns-assistant-accent-text, var(--ns-assistant-default-accent-text)); background: var(--ns-assistant-accent, var(--ns-assistant-default-accent)); }
719
- .legal { display: flex; gap: 12px; justify-content: center; padding: 0 12px 10px; font-size: .8em; }
720
- .legal:empty { display: none; }
721
- .legal a { color: var(--ns-assistant-link, var(--ns-assistant-default-link)); }
722
- :host([data-density="compact"]) .welcome { padding: var(--ns-assistant-spacing, var(--ns-assistant-default-spacing)); }
723
- @media (max-width: 640px) { :host([mobile-fullscreen][open]) { inset: 0; } :host([mobile-fullscreen][open]) .panel { width: 100vw; min-height: 100vh; max-height: 100vh; border-radius: 0; } }
724
- @media (prefers-reduced-motion: reduce) { *, *::before, *::after { transition-duration: 0.01ms !important; animation-duration: 0.01ms !important; } }
725
- @media (max-width: 560px) { :host([data-mode="floating"]) { inset: auto 12px 12px; } :host([open]) .panel { width: calc(100vw - 24px); max-height: calc(100vh - 24px); } }
726
- </style>`;
727
1758
 
728
1759
  // src/react.ts
729
1760
  var NoodleAssistant = (0, import_react.forwardRef)(