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