@runtypelabs/persona 4.23.2 → 4.25.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.
Files changed (166) hide show
  1. package/dist/animations/glyph-cycle.d.cts +1 -1
  2. package/dist/animations/glyph-cycle.d.ts +1 -1
  3. package/dist/animations/{types-CI7OckWI.d.cts → types-Db61aI9J.d.cts} +2 -0
  4. package/dist/animations/{types-CI7OckWI.d.ts → types-Db61aI9J.d.ts} +2 -0
  5. package/dist/animations/wipe.d.cts +1 -1
  6. package/dist/animations/wipe.d.ts +1 -1
  7. package/dist/animations-extra.d.cts +2 -0
  8. package/dist/animations-extra.d.ts +2 -0
  9. package/dist/approval-ui-3NMYNXBD.js +1 -0
  10. package/dist/approval-ui.cjs +2 -2
  11. package/dist/approval-ui.d.cts +278 -37
  12. package/dist/approval-ui.d.ts +278 -37
  13. package/dist/approval-ui.js +2 -2
  14. package/dist/{artifacts-ui-F4N6VMKF.js → artifacts-ui-IGB5YKLO.js} +4 -4
  15. package/dist/artifacts-ui.d.cts +273 -25
  16. package/dist/artifacts-ui.d.ts +273 -25
  17. package/dist/{chunk-NIKYKSNZ.js → chunk-7CO4T5KB.js} +1 -1
  18. package/dist/codegen.cjs +11 -11
  19. package/dist/codegen.js +13 -13
  20. package/dist/context-mentions.d.cts +273 -25
  21. package/dist/context-mentions.d.ts +273 -25
  22. package/dist/event-stream-view.d.cts +273 -25
  23. package/dist/event-stream-view.d.ts +273 -25
  24. package/dist/forms-ui.d.cts +319 -27
  25. package/dist/forms-ui.d.ts +319 -27
  26. package/dist/history-view.d.cts +2 -0
  27. package/dist/history-view.d.ts +2 -0
  28. package/dist/index.cjs +70 -69
  29. package/dist/index.cjs.map +1 -1
  30. package/dist/index.d.cts +344 -39
  31. package/dist/index.d.ts +344 -39
  32. package/dist/index.global.js +57 -54
  33. package/dist/index.global.js.map +1 -1
  34. package/dist/index.js +70 -69
  35. package/dist/index.js.map +1 -1
  36. package/dist/install.global.js +1 -1
  37. package/dist/install.global.js.map +1 -1
  38. package/dist/launcher.global.js +2 -2
  39. package/dist/launcher.global.js.map +1 -1
  40. package/dist/session-reconnect.d.cts +273 -25
  41. package/dist/session-reconnect.d.ts +273 -25
  42. package/dist/smart-dom-reader.d.cts +273 -25
  43. package/dist/smart-dom-reader.d.ts +273 -25
  44. package/dist/theme-editor-preview.cjs +74 -73
  45. package/dist/theme-editor-preview.d.cts +280 -25
  46. package/dist/theme-editor-preview.d.ts +280 -25
  47. package/dist/theme-editor-preview.js +51 -50
  48. package/dist/theme-editor.cjs +10 -10
  49. package/dist/theme-editor.d.cts +293 -27
  50. package/dist/theme-editor.d.ts +293 -27
  51. package/dist/theme-editor.js +10 -10
  52. package/dist/theme-reference.cjs +1 -1
  53. package/dist/theme-reference.d.cts +1522 -11
  54. package/dist/theme-reference.d.ts +1522 -11
  55. package/dist/theme-reference.js +1 -1
  56. package/dist/voice-runtime-7DA7MJH4.js +1 -0
  57. package/dist/voice-runtime.cjs +1 -1
  58. package/dist/voice-runtime.d.cts +33 -0
  59. package/dist/voice-runtime.d.ts +33 -0
  60. package/dist/voice-runtime.js +1 -1
  61. package/dist/widget.css +1 -1
  62. package/package.json +3 -2
  63. package/src/__fixtures__/defaults-parity.v4.json +776 -0
  64. package/src/approval-ui-entry.ts +0 -1
  65. package/src/approval-ui-loader.ts +5 -4
  66. package/src/approval-ui.ts +0 -1
  67. package/src/client-identity-proof.test.ts +193 -0
  68. package/src/client-session-warm.test.ts +216 -0
  69. package/src/client-visitor-history.test.ts +6 -0
  70. package/src/client.ts +122 -30
  71. package/src/codegen.test.ts +24 -0
  72. package/src/components/activity-row.test.ts +100 -0
  73. package/src/components/activity-row.ts +136 -0
  74. package/src/components/approval-actions.test.ts +8 -5
  75. package/src/components/approval-actions.ts +19 -6
  76. package/src/components/approval-bubble.test.ts +56 -9
  77. package/src/components/approval-bubble.ts +36 -41
  78. package/src/components/composer-builder.ts +5 -2
  79. package/src/components/composer-parts.ts +10 -2
  80. package/src/components/header-builder.test.ts +32 -0
  81. package/src/components/header-builder.ts +7 -4
  82. package/src/components/header-layouts.test.ts +12 -0
  83. package/src/components/header-layouts.ts +3 -2
  84. package/src/components/launcher.ts +11 -2
  85. package/src/components/message-bubble.test.ts +22 -0
  86. package/src/components/message-bubble.ts +7 -3
  87. package/src/components/panel.test.ts +52 -1
  88. package/src/components/panel.ts +47 -8
  89. package/src/components/pill-composer-builder.test.ts +2 -0
  90. package/src/components/pill-composer-builder.ts +0 -1
  91. package/src/components/reasoning-bubble.ts +29 -17
  92. package/src/components/suggestions.test.ts +1 -0
  93. package/src/components/suggestions.ts +0 -16
  94. package/src/components/tool-bubble.ts +131 -114
  95. package/src/components/tool-details.test.ts +87 -0
  96. package/src/components/tool-details.ts +102 -0
  97. package/src/components/welcome.test.ts +8 -0
  98. package/src/components/welcome.ts +4 -2
  99. package/src/defaults-parity.test.ts +158 -0
  100. package/src/defaults.test.ts +69 -0
  101. package/src/defaults.ts +128 -49
  102. package/src/generated/runtype-openapi-contract.ts +2 -2
  103. package/src/index-core.ts +4 -0
  104. package/src/install.test.ts +24 -0
  105. package/src/install.ts +3 -1
  106. package/src/launcher-global.ts +2 -1
  107. package/src/launcher-variants.test.ts +52 -0
  108. package/src/runtime/init-update-reset.test.ts +21 -0
  109. package/src/session.ts +59 -7
  110. package/src/session.voice.test.ts +72 -1
  111. package/src/styles/widget-styles.test.ts +19 -0
  112. package/src/styles/widget.css +295 -23
  113. package/src/theme-editor/index.ts +2 -0
  114. package/src/theme-editor/preview-utils.test.ts +8 -0
  115. package/src/theme-editor/preview-utils.ts +7 -3
  116. package/src/theme-editor/sections.test.ts +31 -0
  117. package/src/theme-editor/sections.ts +55 -1
  118. package/src/theme-editor/state-preferences.test.ts +49 -0
  119. package/src/theme-editor/state.ts +73 -15
  120. package/src/theme-reference.test.ts +20 -0
  121. package/src/theme-reference.ts +11 -5
  122. package/src/tool-call-display-defaults.test.ts +33 -1
  123. package/src/types/theme.ts +80 -3
  124. package/src/types.ts +207 -22
  125. package/src/ui.activity-row.test.ts +91 -0
  126. package/src/ui.approval-plugin.test.ts +63 -0
  127. package/src/ui.composer-compact.test.ts +24 -2
  128. package/src/ui.composer-placement.test.ts +23 -0
  129. package/src/ui.detached-panel.test.ts +33 -6
  130. package/src/ui.docked.test.ts +19 -0
  131. package/src/ui.header-update-stability.test.ts +25 -0
  132. package/src/ui.identity-proof.test.ts +58 -0
  133. package/src/ui.launcher-teaser.test.ts +25 -0
  134. package/src/ui.message-width.test.ts +17 -2
  135. package/src/ui.reasoning-display.test.ts +92 -0
  136. package/src/ui.rebuild-composer.test.ts +20 -3
  137. package/src/ui.scroll.test.ts +32 -0
  138. package/src/ui.session-init.test.ts +312 -0
  139. package/src/ui.status-indicator.test.ts +82 -0
  140. package/src/ui.suggest-replies.test.ts +11 -0
  141. package/src/ui.ts +422 -74
  142. package/src/ui.v5-defaults.test.ts +144 -0
  143. package/src/ui.voice-prewarm.test.ts +222 -0
  144. package/src/ui.welcome.test.ts +92 -3
  145. package/src/utils/__snapshots__/tokens-parity.test.ts.snap +811 -140
  146. package/src/utils/code-generators.ts +14 -0
  147. package/src/utils/composer-compact.test.ts +0 -2
  148. package/src/utils/composer-compact.ts +2 -4
  149. package/src/utils/composer-placement.ts +2 -3
  150. package/src/utils/config-merge.test.ts +81 -5
  151. package/src/utils/config-merge.ts +13 -3
  152. package/src/utils/defaults-provenance.ts +67 -0
  153. package/src/utils/dock.ts +7 -1
  154. package/src/utils/launcher-variant.ts +16 -0
  155. package/src/utils/panel-config.ts +97 -0
  156. package/src/utils/status-indicator.ts +39 -0
  157. package/src/utils/theme.test.ts +182 -0
  158. package/src/utils/theme.ts +131 -18
  159. package/src/utils/tokens.test.ts +122 -1
  160. package/src/utils/tokens.ts +177 -17
  161. package/src/voice/runtype-voice-provider.ts +238 -27
  162. package/src/voice/voice.test.ts +272 -2
  163. package/src/welcome.test.ts +23 -0
  164. package/src/welcome.ts +16 -9
  165. package/dist/approval-ui-K7R5Q5OC.js +0 -1
  166. package/dist/voice-runtime-EFR3R5ES.js +0 -1
@@ -217,14 +217,17 @@ describe('Voice Support Check', () => {
217
217
  const WS_OPEN = 1;
218
218
 
219
219
  class MockWebSocket {
220
+ static CONNECTING = 0;
220
221
  static OPEN = WS_OPEN;
221
222
  static instances: MockWebSocket[] = [];
222
223
 
223
224
  url: string;
224
225
  protocols: string | string[] | undefined;
226
+ /** Negotiated subprotocol, set by the test before `triggerOpen()`. */
227
+ protocol = '';
225
228
  binaryType = '';
226
229
  readyState = 0;
227
- sent: ArrayBuffer[] = [];
230
+ sent: Array<ArrayBuffer | string> = [];
228
231
  closeCalls: Array<{ code?: number; reason?: string }> = [];
229
232
  onopen: (() => void) | null = null;
230
233
  onmessage: ((ev: { data: unknown }) => void) | null = null;
@@ -237,7 +240,7 @@ class MockWebSocket {
237
240
  MockWebSocket.instances.push(this);
238
241
  }
239
242
 
240
- send(data: ArrayBuffer) {
243
+ send(data: ArrayBuffer | string) {
241
244
  this.sent.push(data);
242
245
  }
243
246
 
@@ -535,4 +538,271 @@ describe('RuntypeVoiceProvider (realtime streaming)', () => {
535
538
  engine.finishedCb?.(); // playback drained
536
539
  expect(statuses).toEqual(['listening']);
537
540
  });
541
+ });
542
+
543
+ describe('RuntypeVoiceProvider prewarm', () => {
544
+ let getUserMedia: ReturnType<typeof vi.fn>;
545
+ let fetchMock: ReturnType<typeof vi.fn>;
546
+
547
+ beforeEach(() => {
548
+ vi.useFakeTimers();
549
+ MockWebSocket.instances = [];
550
+ getUserMedia = vi.fn(async () => makeStream().stream);
551
+ fetchMock = vi.fn(async () => new Response(null, { status: 204 }));
552
+ vi.stubGlobal('WebSocket', MockWebSocket);
553
+ vi.stubGlobal('navigator', { mediaDevices: { getUserMedia } });
554
+ vi.stubGlobal('fetch', fetchMock);
555
+ (globalThis as any).window.AudioContext = MockAudioContext;
556
+ (globalThis as any).window.webkitAudioContext = MockAudioContext;
557
+ (globalThis as any).window.location = { protocol: 'https:' };
558
+ });
559
+
560
+ afterEach(() => {
561
+ vi.useRealTimers();
562
+ vi.unstubAllGlobals();
563
+ delete (globalThis as any).window.AudioContext;
564
+ delete (globalThis as any).window.webkitAudioContext;
565
+ delete (globalThis as any).window.location;
566
+ });
567
+
568
+ const config = (extra: Partial<NonNullable<VoiceConfig['runtype']>> = {}) => ({
569
+ agentId: 'agent/1',
570
+ clientToken: 'ct_secret',
571
+ host: 'https://api.example.com',
572
+ ...extra,
573
+ });
574
+ const attachConfig = (extra: Partial<NonNullable<VoiceConfig['runtype']>> = {}) =>
575
+ config({ prewarmMode: 'attach', ...extra });
576
+ const lastWs = () => MockWebSocket.instances[MockWebSocket.instances.length - 1];
577
+ const openAs = (ws: MockWebSocket, protocol: string) => {
578
+ ws.protocol = protocol;
579
+ ws.triggerOpen();
580
+ };
581
+
582
+ describe("'request' mode (default)", () => {
583
+ it('POSTs the prewarm endpoint with the bearer token and no body', () => {
584
+ new RuntypeVoiceProvider(config()).prewarm();
585
+
586
+ expect(fetchMock).toHaveBeenCalledOnce();
587
+ const [url, init] = fetchMock.mock.calls[0];
588
+ expect(url).toBe('https://api.example.com/v1/client/agents/agent%2F1/voice/prewarm');
589
+ expect(init).toEqual({
590
+ method: 'POST',
591
+ headers: { Authorization: 'Bearer ct_secret' },
592
+ keepalive: true,
593
+ });
594
+ expect(MockWebSocket.instances).toHaveLength(0);
595
+ });
596
+
597
+ it.each([
598
+ ['http://localhost:8787', 'http://localhost:8787'],
599
+ ['wss://api.example.com', 'https://api.example.com'],
600
+ ['api.example.com', 'https://api.example.com'],
601
+ ])('derives the http base from %s', (host, base) => {
602
+ new RuntypeVoiceProvider(config({ host })).prewarm();
603
+ expect(fetchMock.mock.calls[0][0]).toBe(`${base}/v1/client/agents/agent%2F1/voice/prewarm`);
604
+ });
605
+
606
+ it('fires at most once per 30s per provider', () => {
607
+ const provider = new RuntypeVoiceProvider(config());
608
+ provider.prewarm();
609
+ provider.prewarm();
610
+ vi.advanceTimersByTime(29_999);
611
+ provider.prewarm();
612
+ expect(fetchMock).toHaveBeenCalledOnce();
613
+ vi.advanceTimersByTime(1);
614
+ provider.prewarm();
615
+ expect(fetchMock).toHaveBeenCalledTimes(2);
616
+ });
617
+
618
+ it('swallows rejected and throwing fetches without emitting errors or status', async () => {
619
+ const errors: Error[] = [];
620
+ const statuses: string[] = [];
621
+ fetchMock.mockRejectedValueOnce(new Error('offline'));
622
+ const provider = new RuntypeVoiceProvider(config());
623
+ provider.onError((e) => errors.push(e));
624
+ provider.onStatusChange((s) => statuses.push(s));
625
+ provider.prewarm();
626
+ await vi.advanceTimersByTimeAsync(30_000);
627
+ fetchMock.mockImplementationOnce(() => {
628
+ throw new Error('sync');
629
+ });
630
+ expect(() => provider.prewarm()).not.toThrow();
631
+ await vi.advanceTimersByTimeAsync(0);
632
+ expect(fetchMock).toHaveBeenCalledTimes(2);
633
+ expect(errors).toEqual([]);
634
+ expect(statuses).toEqual([]);
635
+ });
636
+
637
+ it('does nothing without credentials or during a live call', async () => {
638
+ new RuntypeVoiceProvider(config({ clientToken: undefined })).prewarm();
639
+ expect(fetchMock).not.toHaveBeenCalled();
640
+
641
+ const provider = new RuntypeVoiceProvider(config());
642
+ await provider.startListening();
643
+ provider.prewarm();
644
+ expect(fetchMock).not.toHaveBeenCalled();
645
+ });
646
+ });
647
+
648
+ describe("'attach' mode", () => {
649
+ it('opens the voice socket with the attach capability and subprotocol, then pings', () => {
650
+ new RuntypeVoiceProvider(attachConfig()).prewarm();
651
+
652
+ const ws = lastWs();
653
+ expect(ws.url).toBe('wss://api.example.com/ws/agents/agent%2F1/voice?clientCapabilities=attach');
654
+ expect(ws.protocols).toEqual(['runtype.bearer', 'runtype.attach', 'ct_secret']);
655
+ expect(fetchMock).not.toHaveBeenCalled();
656
+ openAs(ws, 'runtype.attach');
657
+ expect(ws.sent).toEqual(['{"type":"ping"}']);
658
+ });
659
+
660
+ it('sends a clamped attachIdleMs when configured', () => {
661
+ new RuntypeVoiceProvider(attachConfig({ attachIdleMs: 5_000 })).prewarm();
662
+ expect(lastWs().url).toContain('clientCapabilities=attach&attachIdleMs=30000');
663
+ });
664
+
665
+ it('never attaches twice concurrently', () => {
666
+ const provider = new RuntypeVoiceProvider(attachConfig());
667
+ provider.prewarm();
668
+ vi.advanceTimersByTime(60_000);
669
+ provider.prewarm();
670
+ expect(MockWebSocket.instances).toHaveLength(1);
671
+ });
672
+
673
+ it('reuses an attached socket on click: sends start and captures on it', async () => {
674
+ const statuses: string[] = [];
675
+ const provider = new RuntypeVoiceProvider(attachConfig());
676
+ provider.onStatusChange((s) => statuses.push(s));
677
+ provider.prewarm();
678
+ const ws = lastWs();
679
+ openAs(ws, 'runtype.attach');
680
+ ws.triggerMessage('{"type":"attached","idleMs":30000}');
681
+
682
+ await provider.startListening();
683
+
684
+ expect(MockWebSocket.instances).toHaveLength(1);
685
+ expect(ws.sent).toEqual(['{"type":"ping"}', '{"type":"start"}']);
686
+ expect(statuses).toEqual(['listening']);
687
+ pumpCapture(constantBuffer(0.1));
688
+ expect(ws.sent).toHaveLength(3);
689
+ // The call owns the socket now: the attach idle timer no longer closes it.
690
+ vi.advanceTimersByTime(600_000);
691
+ expect(ws.closeCalls).toEqual([]);
692
+ });
693
+
694
+ it('adopts a socket whose server ignored attach without sending start', async () => {
695
+ const transcripts: string[] = [];
696
+ const provider = new RuntypeVoiceProvider(attachConfig());
697
+ provider.onTranscript((_role, text) => transcripts.push(text));
698
+ provider.prewarm();
699
+ const ws = lastWs();
700
+ openAs(ws, 'runtype.bearer');
701
+ expect(ws.sent).toEqual([]);
702
+
703
+ await provider.startListening();
704
+
705
+ expect(MockWebSocket.instances).toHaveLength(1);
706
+ expect(ws.sent).toEqual([]);
707
+ ws.triggerMessage(JSON.stringify({ type: 'transcript_final', role: 'user', text: 'hi' }));
708
+ expect(transcripts).toEqual(['hi']);
709
+ });
710
+
711
+ it('opens a fresh socket when the attached one dropped before the click', async () => {
712
+ const provider = new RuntypeVoiceProvider(attachConfig());
713
+ provider.prewarm();
714
+ const attached = lastWs();
715
+ openAs(attached, 'runtype.attach');
716
+ attached.triggerClose(4408);
717
+
718
+ await provider.startListening();
719
+
720
+ expect(MockWebSocket.instances).toHaveLength(2);
721
+ expect(lastWs().url).toBe('wss://api.example.com/ws/agents/agent%2F1/voice');
722
+ expect(lastWs().protocols).toEqual(['runtype.bearer', 'ct_secret']);
723
+ });
724
+
725
+ it('abandons a handshake stalled for 5s and opens a fresh socket', async () => {
726
+ const provider = new RuntypeVoiceProvider(attachConfig());
727
+ provider.prewarm();
728
+ const stalled = lastWs();
729
+
730
+ const started = provider.startListening();
731
+ await vi.advanceTimersByTimeAsync(4_999);
732
+ expect(MockWebSocket.instances).toHaveLength(1);
733
+ await vi.advanceTimersByTimeAsync(1);
734
+ await started;
735
+
736
+ expect(stalled.closeCalls).toEqual([{ code: 1000, reason: 'prewarm released' }]);
737
+ expect(MockWebSocket.instances).toHaveLength(2);
738
+ expect(lastWs().protocols).toEqual(['runtype.bearer', 'ct_secret']);
739
+ });
740
+
741
+ it('uses an attach that completes while the click waits on it', async () => {
742
+ const provider = new RuntypeVoiceProvider(attachConfig());
743
+ provider.prewarm();
744
+ const ws = lastWs();
745
+
746
+ const started = provider.startListening();
747
+ await vi.advanceTimersByTimeAsync(1_000);
748
+ openAs(ws, 'runtype.attach');
749
+ await started;
750
+
751
+ expect(MockWebSocket.instances).toHaveLength(1);
752
+ expect(ws.sent).toContain('{"type":"start"}');
753
+ });
754
+
755
+ it('closes an unused attached socket at the idle window', () => {
756
+ const errors: Error[] = [];
757
+ const provider = new RuntypeVoiceProvider(attachConfig());
758
+ provider.onError((e) => errors.push(e));
759
+ provider.prewarm();
760
+ const ws = lastWs();
761
+ openAs(ws, 'runtype.attach');
762
+
763
+ vi.advanceTimersByTime(29_999);
764
+ expect(ws.closeCalls).toEqual([]);
765
+ vi.advanceTimersByTime(1);
766
+ expect(ws.closeCalls).toEqual([{ code: 1000, reason: 'prewarm released' }]);
767
+ expect(errors).toEqual([]);
768
+ });
769
+
770
+ it("follows the server's idle window from the attached reply", () => {
771
+ const provider = new RuntypeVoiceProvider(attachConfig({ attachIdleMs: 120_000 }));
772
+ provider.prewarm();
773
+ const ws = lastWs();
774
+ openAs(ws, 'runtype.attach');
775
+ vi.advanceTimersByTime(1_000);
776
+ ws.triggerMessage('{"type":"attached","idleMs":60000}');
777
+
778
+ vi.advanceTimersByTime(58_999);
779
+ expect(ws.closeCalls).toEqual([]);
780
+ vi.advanceTimersByTime(1);
781
+ expect(ws.closeCalls).toHaveLength(1);
782
+ });
783
+
784
+ it('closes the attaching socket when the call is hung up mid-handshake', async () => {
785
+ const provider = new RuntypeVoiceProvider(attachConfig());
786
+ provider.prewarm();
787
+ const ws = lastWs();
788
+
789
+ const started = provider.startListening();
790
+ await vi.advanceTimersByTimeAsync(100);
791
+ await provider.stopListening();
792
+ await started;
793
+
794
+ expect(ws.closeCalls).toEqual([{ code: 1000, reason: 'prewarm released' }]);
795
+ expect(MockWebSocket.instances).toHaveLength(1);
796
+ expect(provider.isBargeInActive()).toBe(false);
797
+ });
798
+
799
+ it('disconnect closes an attached socket', async () => {
800
+ const provider = new RuntypeVoiceProvider(attachConfig());
801
+ provider.prewarm();
802
+ const ws = lastWs();
803
+ openAs(ws, 'runtype.attach');
804
+ await provider.disconnect();
805
+ expect(ws.closeCalls).toEqual([{ code: 1000, reason: 'prewarm released' }]);
806
+ });
807
+ });
538
808
  });
@@ -274,3 +274,26 @@ describe("resolveConversationState", () => {
274
274
  expect(resolveConversationState([])).toBe("empty");
275
275
  });
276
276
  });
277
+
278
+ describe("V5 welcome layout defaults", () => {
279
+ it("centers the empty greeting with concise copy and dismisses every variant", () => {
280
+ for (const variant of ["card", "hero", "none"] as const) {
281
+ const resolved = resolveWelcomeConfig(config({ future: { v5Defaults: true }, welcome: { variant } }));
282
+ expect(resolved).toMatchObject({ layout: "centered", align: "center", title: "What can I help with?", subtitle: "", dismiss: "on-first-message", anchor: "bottom" });
283
+ expect(isWelcomeVisible(resolved, [userMessage])).toBe(false);
284
+ }
285
+ });
286
+
287
+ it("keeps explicit fields and legacy copy above V5 defaults", () => {
288
+ const resolved = resolveWelcomeConfig(config({ future: { v5Defaults: true }, copy: { welcomeTitle: "Legacy", welcomeSubtitle: "Scope" }, welcome: { layout: "top", variant: "hero", dismiss: "never", anchor: "bottom", align: "start" } }), true);
289
+ expect(resolved).toMatchObject({ title: "Legacy", subtitle: "Scope", layout: "top", dismiss: "never", anchor: "bottom", align: "start" });
290
+ });
291
+
292
+ it("makes centered fullscreen geometry available in either default mode", () => {
293
+ for (const v5Defaults of [false, true]) {
294
+ const resolved = resolveWelcomeConfig(config({ future: { v5Defaults }, welcome: { layout: "centered" } }), true);
295
+ expect(resolved.anchor).toBe("center");
296
+ expect(resolveWelcomeConfig(config({ future: { v5Defaults }, welcome: { layout: "centered", anchor: "bottom" } }), true).anchor).toBe("bottom");
297
+ }
298
+ });
299
+ });
package/src/welcome.ts CHANGED
@@ -55,6 +55,7 @@ export interface ResolvedWelcomeConfig {
55
55
  * Optional on the interface, always populated by `resolveWelcomeConfig`:
56
56
  * a plugin literal built against an older minor must still type-check.
57
57
  */
58
+ layout?: "top" | "centered";
58
59
  anchor?: AgentWidgetWelcomeAnchor;
59
60
  /** Percentage string; validated, falls back to "44%". */
60
61
  anchorComposerTop?: string;
@@ -80,10 +81,14 @@ const warnOnce = (config: AgentWidgetConfig | undefined, messages: string[]) =>
80
81
  * `copy` alias, else the default above.
81
82
  */
82
83
  export const resolveWelcomeConfig = (
83
- config?: AgentWidgetConfig
84
+ config?: AgentWidgetConfig,
85
+ fullscreen = config?.launcher?.fullHeight === true &&
86
+ !config.launcher.sidebarMode && config.launcher.mountMode !== "docked"
84
87
  ): ResolvedWelcomeConfig => {
85
88
  const welcome = config?.welcome;
86
89
  const copy = config?.copy;
90
+ const v5 = config?.future?.v5Defaults === true;
91
+ const layout = welcome?.layout ?? (v5 ? "centered" : "top");
87
92
 
88
93
  const variant: AgentWidgetWelcomeVariant = isSet(welcome, "variant")
89
94
  ? welcome!.variant!
@@ -91,13 +96,14 @@ export const resolveWelcomeConfig = (
91
96
  ? "none"
92
97
  : "card";
93
98
 
94
- // Hero IS the greeting: `dismiss` is forced and `message` is suppressed.
99
+ // Legacy V4 heroes force dismissal; opting into layout makes it configurable.
100
+ // A hero remains the greeting, so its separate message is always suppressed.
95
101
  const dismiss: AgentWidgetWelcomeDismiss =
96
- variant === "hero"
102
+ variant === "hero" && !v5 && welcome?.layout === undefined
97
103
  ? "on-first-message"
98
104
  : isSet(welcome, "dismiss")
99
105
  ? welcome!.dismiss!
100
- : "never";
106
+ : v5 || variant === "hero" ? "on-first-message" : "never";
101
107
 
102
108
  const conflicts: string[] = [];
103
109
  if (isSet(welcome, "title") && isSet(copy, "welcomeTitle")) {
@@ -120,14 +126,14 @@ export const resolveWelcomeConfig = (
120
126
  'welcome.message is ignored when welcome.variant is "hero": the hero is the greeting.'
121
127
  );
122
128
  }
123
- if (variant === "hero" && isSet(welcome, "dismiss") && welcome!.dismiss !== "on-first-message") {
129
+ if (!v5 && welcome?.layout === undefined && variant === "hero" && isSet(welcome, "dismiss") && welcome!.dismiss !== "on-first-message") {
124
130
  conflicts.push(
125
131
  'welcome.dismiss is pinned to "on-first-message" when welcome.variant is "hero".'
126
132
  );
127
133
  }
128
134
 
129
135
  const anchor: AgentWidgetWelcomeAnchor =
130
- welcome?.anchor === "center" ? "center" : "bottom";
136
+ welcome?.anchor ?? (layout === "centered" && fullscreen ? "center" : "bottom");
131
137
  const anchorTopValid = parseAnchorFraction(welcome?.anchorComposerTop) !== null;
132
138
  if (isSet(welcome, "anchorComposerTop") && !anchorTopValid) {
133
139
  conflicts.push(
@@ -147,16 +153,16 @@ export const resolveWelcomeConfig = (
147
153
  return {
148
154
  title: isSet(welcome, "title")
149
155
  ? welcome!.title!
150
- : copy?.welcomeTitle ?? DEFAULT_WELCOME_TITLE,
156
+ : copy?.welcomeTitle ?? (v5 ? "What can I help with?" : DEFAULT_WELCOME_TITLE),
151
157
  kicker: welcome?.kicker ?? "",
152
158
  subtitle: isSet(welcome, "subtitle")
153
159
  ? welcome!.subtitle!
154
- : copy?.welcomeSubtitle ?? DEFAULT_WELCOME_SUBTITLE,
160
+ : copy?.welcomeSubtitle ?? (v5 ? "" : DEFAULT_WELCOME_SUBTITLE),
155
161
  icon: welcome?.icon,
156
162
  align:
157
163
  welcome?.align === "start" || welcome?.align === "center"
158
164
  ? welcome.align
159
- : undefined,
165
+ : layout === "centered" ? "center" : undefined,
160
166
  iconPlacement:
161
167
  welcome?.icon && typeof welcome.icon !== "function" &&
162
168
  welcome.icon.placement === "inline"
@@ -165,6 +171,7 @@ export const resolveWelcomeConfig = (
165
171
  variant,
166
172
  dismiss,
167
173
  message: variant === "hero" ? undefined : welcome?.message,
174
+ layout,
168
175
  anchor,
169
176
  anchorComposerTop: anchorTopValid
170
177
  ? welcome!.anchorComposerTop!
@@ -1 +0,0 @@
1
- import{a as E,g as P}from"./chunk-NQLDCJAP.js";import{a as x}from"./chunk-7NQ4ZTHF.js";import{b as q,c as $}from"./chunk-Q735PSZL.js";import{a}from"./chunk-76IELU7F.js";var R=()=>{},z=t=>{R=t.webMcpToolTitle},I=t=>R(t);import{ChevronDown as Y,ChevronUp as ee,ShieldAlert as te,ShieldCheck as K,ShieldX as _}from"lucide";var k=new Map,B=t=>{let o=(t.startsWith(E)?t.slice(E.length):t).replace(/([a-z0-9])([A-Z])/g,"$1 $2").split(/[_\-\s.]+/).filter(Boolean);if(o.length===0)return t;let e=o.join(" ").toLowerCase();return e.charAt(0).toUpperCase()+e.slice(1)},F=t=>t?.approval!==!1?t?.approval:void 0,X=(t,r)=>{let o=F(r)?.detailsDisplay??"collapsed";return k.get(t)??o==="expanded"},j=(t,r,o)=>{let e=F(o);t.setAttribute("aria-expanded",r?"true":"false");let u=t.querySelector("[data-approval-details-label]");u&&(u.textContent=r?e?.hideDetailsLabel??"Hide details":e?.showDetailsLabel??"Show details");let i=t.querySelector("[data-approval-details-chevron]");if(i){i.innerHTML="";let g=x(r?ee:Y,14,"currentColor",2);g&&i.appendChild(g)}},V=(t,r,o)=>{let e=r.querySelector('button[data-bubble-type="approval"]'),u=r.querySelector("[data-approval-details]");if(!e||!u)return;let i=X(t,o);j(e,i,o),u.style.display=i?"":"none"};var Z=(t,r)=>{let o=t.approval,e=r?.approval!==!1?r?.approval:void 0,u=o?.status==="pending",i=a("div",["persona-approval-bubble","persona-w-full","persona-rounded-2xl","persona-border","persona-shadow-sm","persona-overflow-hidden"].join(" "));if(i.id=`bubble-${t.id}`,i.setAttribute("data-message-id",t.id),i.style.backgroundColor=e?.backgroundColor??"var(--persona-approval-bg, #fefce8)",i.style.borderColor=e?.borderColor??"var(--persona-approval-border, #fef08a)",i.style.boxShadow=e?.shadow!==void 0?e.shadow.trim()===""?"none":e.shadow:"var(--persona-approval-shadow, 0 5px 15px rgba(15, 23, 42, 0.08))",!o)return i;let g=a("div","persona-flex persona-items-start persona-gap-3 persona-px-4 persona-py-3"),l=a("div","persona-flex-shrink-0 persona-mt-0.5");l.setAttribute("data-approval-icon","true");let v=o.status==="denied"?_:o.status==="timeout"?te:K,C=o.status==="approved"?"var(--persona-feedback-success, #16a34a)":o.status==="denied"?"var(--persona-feedback-error, #dc2626)":o.status==="timeout"?"var(--persona-feedback-warning, #ca8a04)":e?.titleColor??"currentColor",A=x(v,20,C,2);A&&l.appendChild(A);let m=a("div","persona-flex-1 persona-min-w-0"),f=a("div","persona-flex persona-items-center persona-gap-2"),w=a("span","persona-text-sm persona-font-medium persona-text-persona-primary");if(e?.titleColor&&(w.style.color=e.titleColor),w.textContent=e?.title??"Approval Required",f.appendChild(w),!u){let s=a("span","persona-inline-flex persona-items-center persona-px-2 persona-py-0.5 persona-rounded-full persona-text-xs persona-font-medium");s.setAttribute("data-approval-status",o.status),o.status==="approved"?(s.style.backgroundColor="var(--persona-palette-colors-success-100, #dcfce7)",s.style.color="var(--persona-palette-colors-success-700, #15803d)",s.textContent="Approved"):o.status==="denied"?(s.style.backgroundColor="var(--persona-palette-colors-error-100, #fee2e2)",s.style.color="var(--persona-palette-colors-error-700, #b91c1c)",s.textContent="Denied"):o.status==="timeout"&&(s.style.backgroundColor="var(--persona-palette-colors-warning-100, #fef3c7)",s.style.color="var(--persona-palette-colors-warning-700, #b45309)",s.textContent="Timeout"),f.appendChild(s)}m.appendChild(f);let y=o.toolType==="webmcp"||o.toolName.startsWith(E)?I(o.toolName):void 0,N=e?.formatDescription?.({toolName:o.toolName,toolType:o.toolType,description:o.description,parameters:o.parameters,...y?{displayTitle:y}:{},...o.reason?{reason:o.reason}:{}}),D=!o.toolName,H=N||(D?o.description:`The assistant wants to use \u201C${y??B(o.toolName)}\u201D.`),h=a("p","persona-text-sm persona-mt-0.5 persona-text-persona-muted");if(h.setAttribute("data-approval-summary","true"),e?.descriptionColor&&(h.style.color=e.descriptionColor),h.textContent=H,m.appendChild(h),o.reason){let s=a("p","persona-text-sm persona-mt-1 persona-text-persona-muted");s.setAttribute("data-approval-reason","true"),e?.reasonColor?s.style.color=e.reasonColor:e?.descriptionColor&&(s.style.color=e.descriptionColor);let n=a("span","persona-font-medium");n.textContent=`${e?.reasonLabel??"Agent's stated reason:"} `,s.appendChild(n),s.appendChild(document.createTextNode(o.reason)),m.appendChild(s)}let W=e?.detailsDisplay??"collapsed",M=!!o.description&&!D,S=M||!!o.parameters;if(W!=="hidden"&&S){let s=X(t.id,r),n=a("button","persona-inline-flex persona-items-center persona-gap-1 persona-mt-1 persona-p-0 persona-border-none persona-bg-transparent persona-text-xs persona-font-medium persona-cursor-pointer persona-text-persona-muted");n.type="button",n.setAttribute("data-expand-header","true"),n.setAttribute("data-bubble-type","approval"),e?.descriptionColor&&(n.style.color=e.descriptionColor);let p=a("span");p.setAttribute("data-approval-details-label","true");let b=a("span","persona-inline-flex persona-items-center");b.setAttribute("data-approval-details-chevron","true"),n.append(p,b),j(n,s,r),m.appendChild(n);let c=a("div");if(c.setAttribute("data-approval-details","true"),c.style.display=s?"":"none",M){let d=a("p","persona-text-sm persona-mt-1 persona-text-persona-muted");e?.descriptionColor&&(d.style.color=e.descriptionColor),d.textContent=o.description,c.appendChild(d)}if(o.parameters){let d=a("pre","persona-mt-2 persona-text-xs persona-p-2 persona-rounded persona-overflow-x-auto persona-max-h-32 persona-bg-persona-container persona-text-persona-primary");e?.parameterBackgroundColor&&(d.style.backgroundColor=e.parameterBackgroundColor),e?.parameterTextColor&&(d.style.color=e.parameterTextColor),d.style.fontSize="0.75rem",d.style.lineHeight="1rem",d.textContent=P(o.parameters),c.appendChild(d)}m.appendChild(c)}if(u){let s=a("div","persona-flex persona-gap-2 persona-mt-2");s.setAttribute("data-approval-buttons","true");let n=a("button","persona-inline-flex persona-items-center persona-px-3 persona-py-1.5 persona-rounded-md persona-text-xs persona-font-medium persona-border-none persona-cursor-pointer");n.type="button",n.style.backgroundColor=e?.approveButtonColor??"var(--persona-approval-approve-bg, #22c55e)",n.style.color=e?.approveButtonTextColor??"#ffffff",n.setAttribute("data-approval-action","approve");let p=x(K,14,e?.approveButtonTextColor??"#ffffff",2);p&&(p.style.marginRight="4px",n.appendChild(p));let b=document.createTextNode(e?.approveLabel??"Approve");n.appendChild(b);let c=a("button","persona-inline-flex persona-items-center persona-px-3 persona-py-1.5 persona-rounded-md persona-text-xs persona-font-medium persona-cursor-pointer");c.type="button",c.style.backgroundColor=e?.denyButtonColor??"transparent",c.style.color=e?.denyButtonTextColor??"var(--persona-feedback-error, #dc2626)",c.style.border=`1px solid ${e?.denyButtonTextColor?e.denyButtonTextColor:"var(--persona-palette-colors-error-200, #fca5a5)"}`,c.setAttribute("data-approval-action","deny");let d=x(_,14,e?.denyButtonTextColor??"var(--persona-feedback-error, #dc2626)",2);d&&(d.style.marginRight="4px",c.appendChild(d));let L=document.createTextNode(e?.denyLabel??"Deny");c.appendChild(L),s.append(n,c),m.appendChild(s)}return g.append(l,m),i.appendChild(g),i};import{Ban as oe,ChevronDown as G,ShieldCheck as ne}from"lucide";var re=()=>({keyHandlers:new Map,popovers:new Map,pendingOrder:[],latestPendingApprovalId:null}),J=(t,r)=>{let o=t.keyHandlers.get(r);o&&(document.removeEventListener("keydown",o),t.keyHandlers.delete(r));let e=t.popovers.get(r);e&&(e.destroy(),t.popovers.delete(r))},T=(t,r)=>{J(t,r);let o=t.pendingOrder.indexOf(r);o!==-1&&t.pendingOrder.splice(o,1),t.latestPendingApprovalId===r&&(t.latestPendingApprovalId=t.pendingOrder.length?t.pendingOrder[t.pendingOrder.length-1]:null)},ae=t=>t?.approval!==!1?t?.approval:void 0,se=(t,r)=>{let o=r?.detailsDisplay??"collapsed";return k.get(t)??o==="expanded"},U=t=>{let r=a("span","persona-approval-kbd");return r.textContent=t,r},pe=(t,r)=>{let o=a("span","persona-approval-title");r?.titleColor&&(o.style.color=r.titleColor);let u=t.toolType==="webmcp"||t.toolName.startsWith(E)?I(t.toolName):void 0,i=r?.formatDescription?.({toolName:t.toolName,toolType:t.toolType,description:t.description??"",parameters:t.parameters,...u?{displayTitle:u}:{},...t.reason?{reason:t.reason}:{}});if(i)return o.textContent=i,o;let g=u??B(t.toolName),l=t.toolType&&t.toolType!=="webmcp"?t.toolType:null;o.append("The assistant wants to use ");let v=document.createElement("strong");if(v.textContent=g,o.appendChild(v),l){o.append(" from ");let C=document.createElement("strong");C.textContent=l,o.appendChild(C)}return o},le=t=>{let r=a("div","persona-approval-resolved"),o=x(oe,15,"currentColor",2);o&&r.appendChild(o);let e=a("span","persona-approval-resolved-name");return e.textContent=t.toolName?B(t.toolName):"Tool",r.append(e,document.createTextNode(t.status==="timeout"?" timed out":" denied")),r},ie=(t,r,o,e,u,i,g)=>{let l=a("div","persona-approval-card persona-shadow-sm");l.id=`bubble-${r.id}`,l.setAttribute("data-message-id",r.id),l.setAttribute("data-bubble-type","approval"),e?.backgroundColor&&(l.style.background=e.backgroundColor),e?.borderColor&&(l.style.borderColor=e.borderColor),e?.shadow!==void 0&&(l.style.boxShadow=e.shadow.trim()===""?"none":e.shadow);let v=e?.detailsDisplay??"collapsed",C=!!o.description&&v!=="hidden",A=o.parameters!=null&&v!=="hidden",m=C||A,f=m&&se(r.id,e),w=e?.showDetailsLabel??"Show details",O=e?.hideDetailsLabel??"Hide details",y=a("button","persona-approval-head");y.type="button",m?(y.setAttribute("data-action","toggle-params"),y.setAttribute("aria-expanded",f?"true":"false"),y.setAttribute("aria-label",f?O:w)):y.setAttribute("data-static","true");let N=a("span","persona-approval-logo"),D=x(ne,16,"currentColor",2);D&&N.appendChild(D),y.appendChild(N);let H=pe(o,e);if(m){let n=a("span","persona-approval-toggle");n.setAttribute("aria-hidden","true");let p=x(G,14,"currentColor",2);p&&n.appendChild(p),H.append(" "),H.appendChild(n)}y.appendChild(H),l.appendChild(y);let h=a("div","persona-approval-body");if(m){let n=a("div","persona-approval-details");if(n.setAttribute("data-role","params"),n.hidden=!f,C){let p=a("p","persona-approval-desc");e?.descriptionColor&&(p.style.color=e.descriptionColor),p.textContent=o.description,n.appendChild(p)}if(A){let p=a("pre","persona-approval-params");e?.parameterBackgroundColor&&(p.style.background=e.parameterBackgroundColor),e?.parameterTextColor&&(p.style.color=e.parameterTextColor),p.textContent=P(o.parameters),n.appendChild(p)}h.appendChild(n)}if(o.reason){let n=a("p","persona-approval-reason");e?.reasonColor?n.style.color=e.reasonColor:e?.descriptionColor&&(n.style.color=e.descriptionColor);let p=a("span","persona-approval-reason-label");p.textContent=`${e?.reasonLabel??"Agent's stated reason:"} `,n.append(p,document.createTextNode(o.reason)),h.appendChild(n)}let W=a("div","persona-approval-actions"),M=null,S=n=>{e?.approveButtonColor&&(n.style.background=e.approveButtonColor),e?.approveButtonTextColor&&(n.style.color=e.approveButtonTextColor)},s=a("button","persona-approval-deny");if(s.type="button",s.setAttribute("data-action","deny"),e?.denyButtonColor&&(s.style.background=e.denyButtonColor),e?.denyButtonTextColor&&(s.style.color=e.denyButtonTextColor),s.append(e?.denyLabel??"Deny"),g){let n=a("div","persona-approval-split"),p=a("button","persona-approval-primary");p.type="button",p.setAttribute("data-action","always"),S(p),p.append(e?.approveLabel??"Always allow",U("\u23CE"));let b=a("button","persona-approval-caret");b.type="button",b.setAttribute("data-action","toggle-menu"),b.setAttribute("aria-label","More options"),S(b);let c=x(G,15,"currentColor",2);c&&b.appendChild(c),n.append(p,b),W.append(n,s),s.append(U("Esc"));let d=a("div","persona-approval-menu"),L=a("button","persona-approval-menu-item");L.type="button",L.append("Allow once",U("\u2318\u23CE")),d.appendChild(L),M=q({anchor:n,content:d,placement:"bottom-start",matchAnchorWidth:!0}),t.popovers.set(r.id,M),L.addEventListener("click",()=>{T(t,r.id),u()})}else{let n=a("button","persona-approval-primary persona-approval-primary--solo");n.type="button",n.setAttribute("data-action","allow"),S(n),n.append(e?.approveLabel??"Allow"),W.append(n,s)}return h.appendChild(W),l.appendChild(h),l.addEventListener("click",n=>{let p=n.target instanceof Element?n.target.closest("[data-action]"):null;if(!p)return;let b=p.getAttribute("data-action");if(b==="toggle-params"){let c=l.querySelector('[data-role="params"]');if(c){let d=c.hidden;c.hidden=!d,y.setAttribute("aria-expanded",d?"true":"false"),y.setAttribute("aria-label",d?O:w),k.set(r.id,d)}return}if(b==="toggle-menu"){M?.toggle();return}if(b==="always"){T(t,r.id),u({remember:!0});return}if(b==="allow"){T(t,r.id),u();return}if(b==="deny"){T(t,r.id),i();return}}),l},Q=()=>{let t=re();return{plugin:{id:"persona-built-in-approval",renderApproval:({message:e,approve:u,deny:i,config:g})=>{let l=e?.approval;if(!l)return null;let v=ae(g);if(l.status!=="pending"){if(T(t,e.id),l.status==="approved"){let m=document.createElement("div");return m.style.display="none",m}return le(l)}J(t,e.id);let C=v?.enableAlwaysAllow===!0,A=ie(t,e,l,v,u,i,C);if(C){t.pendingOrder.includes(e.id)||t.pendingOrder.push(e.id),t.latestPendingApprovalId=t.pendingOrder[t.pendingOrder.length-1];let m=f=>{$(f)||e.id===t.latestPendingApprovalId&&(f.key!=="Escape"&&f.key!=="Enter"||(f.preventDefault(),f.stopImmediatePropagation(),T(t,e.id),f.key==="Escape"?i():f.metaKey||f.ctrlKey?u():u({remember:!0})))};t.keyHandlers.set(e.id,m),document.addEventListener("keydown",m)}return A}},teardown:()=>{for(let e of[...t.keyHandlers.keys(),...t.popovers.keys()])T(t,e);t.latestPendingApprovalId=null}}};export{k as approvalDetailsExpansionState,Z as createApprovalBubble,Q as createBuiltInApprovalPlugin,B as humanizeToolName,z as initApprovalUi,V as updateApprovalDetailsUI};
@@ -1 +0,0 @@
1
- import{a as m}from"./chunk-5EIIHQLQ.js";var g=16e3,y=24e3,k=4096,C=4,S=1380533830;function E(r){return r.byteLength>=44&&new DataView(r).getUint32(0,!1)===S?new Uint8Array(r,44):new Uint8Array(r)}function V(r){let t=r.replace(/\/+$/,"");return/^wss?:\/\//i.test(t)?t:/^https?:\/\//i.test(t)?t.replace(/^http/i,"ws"):`${typeof window<"u"&&window.location?.protocol==="https:"?"wss:":"ws:"}//${t}`}var d=class{constructor(t){this.config=t;this.type="runtype";this.ws=null;this.captureContext=null;this.levelCallbacks=[];this.mediaStream=null;this.sourceNode=null;this.processor=null;this.playback=null;this.callLive=!1;this.isSpeaking=!1;this.callGeneration=0;this.intentionalClose=!1;this.resultCallbacks=[];this.errorCallbacks=[];this.statusCallbacks=[];this.transcriptCallbacks=[];this.metricsCallbacks=[]}async connect(){}async startListening(){if(this.callLive)return;let t=this.config?.agentId,e=this.config?.clientToken,i=this.config?.host;if(!t)throw new Error("Runtype voice requires an agentId");if(!e)throw new Error("Runtype voice requires a clientToken");if(!i)throw new Error("Runtype voice requires a host (or widget apiUrl)");let s=++this.callGeneration;this.intentionalClose=!1,this.callLive=!0;try{let o=await navigator.mediaDevices.getUserMedia({audio:{sampleRate:g,channelCount:1,echoCancellation:!0}});if(s!==this.callGeneration){o.getTracks().forEach(a=>a.stop());return}this.mediaStream=o;let p=window.AudioContext||window.webkitAudioContext,u=new p({sampleRate:g});u.state==="suspended"&&await u.resume().catch(()=>{}),this.captureContext=u;let l=this.config?.createPlaybackEngine?await this.config.createPlaybackEngine():new m(y);if(s!==this.callGeneration){l.destroy(),o.getTracks().forEach(a=>a.stop()),u.close().catch(()=>{});return}this.playback=l,l.onFinished(()=>{s===this.callGeneration&&(this.isSpeaking=!1,this.ws&&this.ws.readyState===WebSocket.OPEN&&this.emitStatus("listening"))});let f=`${V(i)}/ws/agents/${encodeURIComponent(t)}/voice`,n=new WebSocket(f,["runtype.bearer",e]);n.binaryType="arraybuffer",this.ws=n,n.onopen=()=>{s===this.callGeneration&&(this.emitStatus("listening"),this.startCapture(u,o,n,s))},n.onmessage=a=>this.handleMessage(a,s),n.onerror=()=>{s===this.callGeneration&&(this.emitError(new Error("Voice connection failed")),this.emitStatus("error"),this.cleanup())},n.onclose=a=>{if(this.intentionalClose){this.intentionalClose=!1;return}if(s===this.callGeneration){if(a.code!==1e3){let c=a.code?` (code ${a.code})`:"";this.emitError(new Error(`Voice connection closed${c}`)),this.emitStatus("error")}else this.emitStatus("idle");this.cleanup()}}}catch(o){throw this.cleanup(),this.emitError(o),this.emitStatus("error"),o}}async stopListening(){this.cleanup(),this.emitStatus("idle")}async disconnect(){this.cleanup(),this.emitStatus("disconnected"),this.resultCallbacks=[],this.errorCallbacks=[],this.statusCallbacks=[],this.transcriptCallbacks=[],this.metricsCallbacks=[]}stopPlayback(){this.playback&&this.playback.flush(),this.isSpeaking=!1,this.ws&&this.ws.readyState===WebSocket.OPEN&&this.emitStatus("listening")}getInterruptionMode(){return"barge-in"}isBargeInActive(){return this.callLive}onLevel(t){this.levelCallbacks.push(t)}async deactivateBargeIn(){this.cleanup(),this.emitStatus("idle")}startCapture(t,e,i,s){let o=t.createMediaStreamSource(e);this.sourceNode=o;let p=t.createScriptProcessor(k,1,1);this.processor=p,p.onaudioprocess=u=>{if(s!==this.callGeneration||i.readyState!==WebSocket.OPEN)return;let l=u.inputBuffer.getChannelData(0),f=new Int16Array(l.length),n=0;for(let a=0;a<l.length;a++){let c=Math.max(-1,Math.min(1,l[a]));n+=c*c,f[a]=c<0?c*32768:c*32767}if(i.send(f.buffer),this.levelCallbacks.length>0){let a=Math.sqrt(n/l.length),c=Math.max(0,Math.min(1,a*C));for(let b of this.levelCallbacks)b(c)}},o.connect(p),p.connect(t.destination)}handleMessage(t,e){if(e!==this.callGeneration)return;if(t.data instanceof ArrayBuffer){this.handleAudioFrame(t.data,e);return}let i;try{i=JSON.parse(t.data)}catch{return}switch(i.type){case"transcript_interim":this.emitStatus("listening"),this.emitTranscript("user",i.text??"",!1);break;case"transcript_final":{let s=i.role==="assistant"?"assistant":"user";this.emitStatus(s==="user"?"processing":"speaking"),this.emitTranscript(s,i.text??"",!0);break}case"audio_end":this.playback?this.playback.markStreamEnd():(this.isSpeaking=!1,this.emitStatus("listening"));break;case"metrics":this.emitMetrics({llmMs:i.llm_ms,ttsMs:i.tts_ms,firstAudioMs:i.first_audio_ms,totalMs:i.total_ms});break;case"error":this.emitError(new Error(i.error||"Voice error")),this.emitStatus("error");break}}handleAudioFrame(t,e){if(e!==this.callGeneration||!this.playback)return;let i=E(t);i.length!==0&&(this.isSpeaking||(this.isSpeaking=!0,this.emitStatus("speaking")),this.playback.enqueue(i))}cleanup(){if(this.callGeneration+=1,this.callLive=!1,this.isSpeaking=!1,this.processor&&(this.processor.onaudioprocess=null,this.processor.disconnect(),this.processor=null),this.sourceNode&&(this.sourceNode.disconnect(),this.sourceNode=null),this.mediaStream&&(this.mediaStream.getTracks().forEach(t=>t.stop()),this.mediaStream=null),this.captureContext&&(this.captureContext.close().catch(()=>{}),this.captureContext=null),this.playback&&(this.playback.destroy(),this.playback=null),this.ws){this.intentionalClose=!0;try{this.ws.close(1e3,"client ended call")}catch{}this.ws=null}}onResult(t){this.resultCallbacks.push(t)}onError(t){this.errorCallbacks.push(t)}onStatusChange(t){this.statusCallbacks.push(t)}onTranscript(t){this.transcriptCallbacks.push(t)}onMetrics(t){this.metricsCallbacks.push(t)}emitStatus(t){this.statusCallbacks.forEach(e=>e(t))}emitError(t){this.errorCallbacks.forEach(e=>e(t))}emitTranscript(t,e,i){this.transcriptCallbacks.forEach(s=>s(t,e,i))}emitMetrics(t){this.metricsCallbacks.forEach(e=>e(t))}};var h=class{constructor(t={}){this.config=t;this.type="browser";this.recognition=null;this.resultCallbacks=[];this.errorCallbacks=[];this.statusCallbacks=[];this.isListening=!1;this.w=typeof window<"u"?window:void 0}async connect(){this.statusCallbacks.forEach(t=>t("connected"))}async startListening(){try{if(this.isListening)throw new Error("Already listening");if(!this.w)throw new Error("Window object not available");let t=this.w.SpeechRecognition||this.w.webkitSpeechRecognition;if(!t)throw new Error("Browser speech recognition not supported");this.recognition=new t,this.recognition.lang=this.config?.language||"en-US",this.recognition.continuous=this.config?.continuous||!1,this.recognition.interimResults=!0,this.recognition.onresult=e=>{let i=Array.from(e.results).map(o=>o[0]).map(o=>o.transcript).join(""),s=e.results[e.results.length-1].isFinal;this.resultCallbacks.forEach(o=>o({text:i,confidence:s?.8:.5,provider:"browser"})),s&&!this.config?.continuous&&this.stopListening()},this.recognition.onerror=e=>{this.errorCallbacks.forEach(i=>i(new Error(e.error))),this.statusCallbacks.forEach(i=>i("error"))},this.recognition.onstart=()=>{this.isListening=!0,this.statusCallbacks.forEach(e=>e("listening"))},this.recognition.onend=()=>{this.isListening=!1,this.statusCallbacks.forEach(e=>e("idle"))},this.recognition.start()}catch(t){throw this.errorCallbacks.forEach(e=>e(t)),this.statusCallbacks.forEach(e=>e("error")),t}}async stopListening(){this.recognition&&(this.recognition.stop(),this.recognition=null),this.isListening=!1,this.statusCallbacks.forEach(t=>t("idle"))}onResult(t){this.resultCallbacks.push(t)}onError(t){this.errorCallbacks.push(t)}onStatusChange(t){this.statusCallbacks.push(t)}async disconnect(){await this.stopListening(),this.statusCallbacks.forEach(t=>t("disconnected"))}static isSupported(){return"SpeechRecognition"in window||"webkitSpeechRecognition"in window}};function v(r){switch(r.type){case"runtype":if(!r.runtype)throw new Error("Runtype voice provider requires configuration");return new d(r.runtype);case"browser":if(!h.isSupported())throw new Error("Browser speech recognition not supported");return new h(r.browser||{});case"custom":{let t=r.custom;if(!t)throw new Error("Custom voice provider requires a `custom` provider instance or factory");let e=typeof t=="function"?t():t;if(!e||typeof e.startListening!="function")throw new Error("Custom voice provider `custom` must be a VoiceProvider (or a factory returning one)");return e}default:throw new Error(`Unknown voice provider type: ${r.type}`)}}function w(r){if(r?.type==="custom"&&r.custom)return v({type:"custom",custom:r.custom});if(r?.type==="runtype"&&r.runtype)return v({type:"runtype",runtype:r.runtype});if(h.isSupported())return v({type:"browser",browser:r?.browser||{language:"en-US"}});throw new Error("No supported voice providers available")}function P(r){try{return w(r),!0}catch{return!1}}export{h as BrowserVoiceProvider,d as RuntypeVoiceProvider,w as createBestAvailableVoiceProvider,v as createVoiceProvider,P as isVoiceSupported};