@agent-native/toolkit 0.9.0 → 0.10.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 (190) hide show
  1. package/README.md +12 -3
  2. package/agent-native.eject.json +71 -0
  3. package/dist/chat-history/ChatHistoryRail.d.ts +33 -0
  4. package/dist/chat-history/ChatHistoryRail.d.ts.map +1 -0
  5. package/dist/chat-history/ChatHistoryRail.js +35 -0
  6. package/dist/chat-history/ChatHistoryRail.js.map +1 -0
  7. package/dist/chat-history/ChatHistoryRail.spec.d.ts +2 -0
  8. package/dist/chat-history/ChatHistoryRail.spec.d.ts.map +1 -0
  9. package/dist/chat-history/ChatHistoryRail.spec.js +157 -0
  10. package/dist/chat-history/ChatHistoryRail.spec.js.map +1 -0
  11. package/dist/chat-history/index.d.ts +2 -0
  12. package/dist/chat-history/index.d.ts.map +1 -1
  13. package/dist/chat-history/index.js +2 -0
  14. package/dist/chat-history/index.js.map +1 -1
  15. package/dist/chat-history/useChatHistoryRailController.d.ts +26 -0
  16. package/dist/chat-history/useChatHistoryRailController.d.ts.map +1 -0
  17. package/dist/chat-history/useChatHistoryRailController.js +36 -0
  18. package/dist/chat-history/useChatHistoryRailController.js.map +1 -0
  19. package/dist/chat-history/useChatHistoryRailController.spec.d.ts +2 -0
  20. package/dist/chat-history/useChatHistoryRailController.spec.d.ts.map +1 -0
  21. package/dist/chat-history/useChatHistoryRailController.spec.js +101 -0
  22. package/dist/chat-history/useChatHistoryRailController.spec.js.map +1 -0
  23. package/dist/chat-history.css +61 -0
  24. package/dist/composer/ComposerPlusMenu.js +1 -1
  25. package/dist/composer/ComposerPlusMenu.js.map +1 -1
  26. package/dist/composer/PromptComposer.d.ts.map +1 -1
  27. package/dist/composer/PromptComposer.js +22 -4
  28. package/dist/composer/PromptComposer.js.map +1 -1
  29. package/dist/composer/RealtimeVoiceMode.d.ts +5 -1
  30. package/dist/composer/RealtimeVoiceMode.d.ts.map +1 -1
  31. package/dist/composer/RealtimeVoiceMode.js +27 -8
  32. package/dist/composer/RealtimeVoiceMode.js.map +1 -1
  33. package/dist/composer/RealtimeVoiceMode.spec.js +51 -11
  34. package/dist/composer/RealtimeVoiceMode.spec.js.map +1 -1
  35. package/dist/composer/VoiceButton.d.ts +2 -0
  36. package/dist/composer/VoiceButton.d.ts.map +1 -1
  37. package/dist/composer/VoiceButton.js +33 -1
  38. package/dist/composer/VoiceButton.js.map +1 -1
  39. package/dist/composer/VoiceButton.spec.js +9 -1
  40. package/dist/composer/VoiceButton.spec.js.map +1 -1
  41. package/dist/composer/realtime-voice-audio-level.d.ts.map +1 -1
  42. package/dist/composer/realtime-voice-audio-level.js +2 -1
  43. package/dist/composer/realtime-voice-audio-level.js.map +1 -1
  44. package/dist/composer/realtime-voice-audio-level.spec.js +7 -0
  45. package/dist/composer/realtime-voice-audio-level.spec.js.map +1 -1
  46. package/dist/composer/useRealtimeVoiceMode.d.ts +7 -0
  47. package/dist/composer/useRealtimeVoiceMode.d.ts.map +1 -1
  48. package/dist/composer/useRealtimeVoiceMode.js +95 -23
  49. package/dist/composer/useRealtimeVoiceMode.js.map +1 -1
  50. package/dist/composer/useRealtimeVoiceMode.spec.js +58 -9
  51. package/dist/composer/useRealtimeVoiceMode.spec.js.map +1 -1
  52. package/dist/conformance/__fixtures__/css-in-js-adapter.d.ts +7 -0
  53. package/dist/conformance/__fixtures__/css-in-js-adapter.d.ts.map +1 -0
  54. package/dist/conformance/__fixtures__/css-in-js-adapter.js +84 -0
  55. package/dist/conformance/__fixtures__/css-in-js-adapter.js.map +1 -0
  56. package/dist/conformance/conformance.spec.d.ts +2 -0
  57. package/dist/conformance/conformance.spec.d.ts.map +1 -0
  58. package/dist/conformance/conformance.spec.js +58 -0
  59. package/dist/conformance/conformance.spec.js.map +1 -0
  60. package/dist/conformance/index.d.ts +4 -0
  61. package/dist/conformance/index.d.ts.map +1 -0
  62. package/dist/conformance/index.js +4 -0
  63. package/dist/conformance/index.js.map +1 -0
  64. package/dist/conformance/runner.d.ts +12 -0
  65. package/dist/conformance/runner.d.ts.map +1 -0
  66. package/dist/conformance/runner.js +441 -0
  67. package/dist/conformance/runner.js.map +1 -0
  68. package/dist/conformance/types.d.ts +26 -0
  69. package/dist/conformance/types.d.ts.map +1 -0
  70. package/dist/conformance/types.js +13 -0
  71. package/dist/conformance/types.js.map +1 -0
  72. package/dist/conformance/version.d.ts +6 -0
  73. package/dist/conformance/version.d.ts.map +1 -0
  74. package/dist/conformance/version.js +14 -0
  75. package/dist/conformance/version.js.map +1 -0
  76. package/dist/design-system/components.d.ts +64 -0
  77. package/dist/design-system/components.d.ts.map +1 -0
  78. package/dist/design-system/components.js +50 -0
  79. package/dist/design-system/components.js.map +1 -0
  80. package/dist/design-system/context.d.ts +11 -0
  81. package/dist/design-system/context.d.ts.map +1 -0
  82. package/dist/design-system/context.js +17 -0
  83. package/dist/design-system/context.js.map +1 -0
  84. package/dist/design-system/default-adapter.d.ts +3 -0
  85. package/dist/design-system/default-adapter.d.ts.map +1 -0
  86. package/dist/design-system/default-adapter.js +166 -0
  87. package/dist/design-system/default-adapter.js.map +1 -0
  88. package/dist/design-system/default-adapter.spec.d.ts +2 -0
  89. package/dist/design-system/default-adapter.spec.d.ts.map +1 -0
  90. package/dist/design-system/default-adapter.spec.js +96 -0
  91. package/dist/design-system/default-adapter.spec.js.map +1 -0
  92. package/dist/design-system/definition.d.ts +9 -0
  93. package/dist/design-system/definition.d.ts.map +1 -0
  94. package/dist/design-system/definition.js +4 -0
  95. package/dist/design-system/definition.js.map +1 -0
  96. package/dist/design-system/design-system.spec.d.ts +2 -0
  97. package/dist/design-system/design-system.spec.d.ts.map +1 -0
  98. package/dist/design-system/design-system.spec.js +121 -0
  99. package/dist/design-system/design-system.spec.js.map +1 -0
  100. package/dist/design-system/error-boundary.d.ts +18 -0
  101. package/dist/design-system/error-boundary.d.ts.map +1 -0
  102. package/dist/design-system/error-boundary.js +16 -0
  103. package/dist/design-system/error-boundary.js.map +1 -0
  104. package/dist/design-system/index.d.ts +8 -0
  105. package/dist/design-system/index.d.ts.map +1 -0
  106. package/dist/design-system/index.js +8 -0
  107. package/dist/design-system/index.js.map +1 -0
  108. package/dist/design-system/theme.d.ts +30 -0
  109. package/dist/design-system/theme.d.ts.map +1 -0
  110. package/dist/design-system/theme.js +95 -0
  111. package/dist/design-system/theme.js.map +1 -0
  112. package/dist/design-system/theme.spec.d.ts +2 -0
  113. package/dist/design-system/theme.spec.d.ts.map +1 -0
  114. package/dist/design-system/theme.spec.js +43 -0
  115. package/dist/design-system/theme.spec.js.map +1 -0
  116. package/dist/design-system/types.d.ts +288 -0
  117. package/dist/design-system/types.d.ts.map +1 -0
  118. package/dist/design-system/types.js +2 -0
  119. package/dist/design-system/types.js.map +1 -0
  120. package/dist/provider.d.ts +4 -3
  121. package/dist/provider.d.ts.map +1 -1
  122. package/dist/provider.js +46 -4
  123. package/dist/provider.js.map +1 -1
  124. package/dist/styles.css +35 -0
  125. package/dist/ui/button.d.ts +5 -0
  126. package/dist/ui/button.d.ts.map +1 -1
  127. package/dist/ui/button.js +25 -4
  128. package/dist/ui/button.js.map +1 -1
  129. package/dist/ui/dialog.d.ts +4 -0
  130. package/dist/ui/dialog.d.ts.map +1 -1
  131. package/dist/ui/dialog.js +2 -2
  132. package/dist/ui/dialog.js.map +1 -1
  133. package/dist/ui/popover.d.ts +1 -0
  134. package/dist/ui/popover.d.ts.map +1 -1
  135. package/dist/ui/popover.js +2 -2
  136. package/dist/ui/popover.js.map +1 -1
  137. package/dist/ui/select.d.ts +3 -1
  138. package/dist/ui/select.d.ts.map +1 -1
  139. package/dist/ui/select.js +1 -1
  140. package/dist/ui/select.js.map +1 -1
  141. package/dist/ui/sidebar.d.ts +6 -2
  142. package/dist/ui/sidebar.d.ts.map +1 -1
  143. package/dist/ui/sidebar.js +12 -15
  144. package/dist/ui/sidebar.js.map +1 -1
  145. package/dist/ui/tooltip.d.ts +3 -1
  146. package/dist/ui/tooltip.d.ts.map +1 -1
  147. package/dist/ui/tooltip.js +2 -2
  148. package/dist/ui/tooltip.js.map +1 -1
  149. package/package.json +31 -1
  150. package/src/chat-history/ChatHistoryRail.spec.tsx +312 -0
  151. package/src/chat-history/ChatHistoryRail.tsx +150 -0
  152. package/src/chat-history/index.ts +14 -0
  153. package/src/chat-history/useChatHistoryRailController.spec.tsx +131 -0
  154. package/src/chat-history/useChatHistoryRailController.ts +76 -0
  155. package/src/chat-history.css +61 -0
  156. package/src/composer/ComposerPlusMenu.tsx +11 -8
  157. package/src/composer/PromptComposer.tsx +27 -6
  158. package/src/composer/RealtimeVoiceMode.spec.tsx +116 -11
  159. package/src/composer/RealtimeVoiceMode.tsx +106 -36
  160. package/src/composer/VoiceButton.spec.ts +17 -1
  161. package/src/composer/VoiceButton.tsx +49 -0
  162. package/src/composer/realtime-voice-audio-level.spec.ts +15 -0
  163. package/src/composer/realtime-voice-audio-level.ts +2 -1
  164. package/src/composer/useRealtimeVoiceMode.spec.ts +82 -8
  165. package/src/composer/useRealtimeVoiceMode.tsx +112 -25
  166. package/src/conformance/__fixtures__/css-in-js-adapter.tsx +401 -0
  167. package/src/conformance/conformance.spec.tsx +83 -0
  168. package/src/conformance/index.ts +3 -0
  169. package/src/conformance/runner.tsx +710 -0
  170. package/src/conformance/types.ts +45 -0
  171. package/src/conformance/version.ts +22 -0
  172. package/src/design-system/components.tsx +146 -0
  173. package/src/design-system/context.tsx +31 -0
  174. package/src/design-system/default-adapter.spec.tsx +143 -0
  175. package/src/design-system/default-adapter.tsx +915 -0
  176. package/src/design-system/definition.ts +14 -0
  177. package/src/design-system/design-system.spec.tsx +183 -0
  178. package/src/design-system/error-boundary.tsx +37 -0
  179. package/src/design-system/index.ts +7 -0
  180. package/src/design-system/theme.spec.ts +61 -0
  181. package/src/design-system/theme.ts +145 -0
  182. package/src/design-system/types.ts +363 -0
  183. package/src/provider.tsx +98 -5
  184. package/src/styles.css +35 -0
  185. package/src/ui/button.tsx +95 -6
  186. package/src/ui/dialog.tsx +48 -23
  187. package/src/ui/popover.tsx +16 -2
  188. package/src/ui/select.tsx +7 -3
  189. package/src/ui/sidebar.tsx +47 -48
  190. package/src/ui/tooltip.tsx +7 -3
@@ -0,0 +1,76 @@
1
+ import { useCallback, useEffect, useMemo, useState } from "react";
2
+
3
+ export interface ChatHistoryRailControllerLabels {
4
+ newChat: string;
5
+ showMore: string;
6
+ showLess: string;
7
+ }
8
+
9
+ export interface UseChatHistoryRailControllerOptions<Item> {
10
+ items: readonly Item[];
11
+ onNewChat: () => void;
12
+ labels: ChatHistoryRailControllerLabels;
13
+ previewCount?: number;
14
+ expandedCount?: number;
15
+ }
16
+
17
+ export interface ChatHistoryRailController<Item> {
18
+ expanded: boolean;
19
+ collapsedLimit: number;
20
+ expandedLimit: number;
21
+ visibleItems: Item[];
22
+ canExpand: boolean;
23
+ newChatLabel: string;
24
+ disclosureLabel: string;
25
+ onNewChat: () => void;
26
+ toggleExpanded: () => void;
27
+ resetExpanded: () => void;
28
+ }
29
+
30
+ function normalizeLimit(value: number | undefined, fallback: number): number {
31
+ if (value === undefined || !Number.isFinite(value)) return fallback;
32
+ return Math.max(1, Math.floor(value));
33
+ }
34
+
35
+ export function useChatHistoryRailController<Item>({
36
+ items,
37
+ onNewChat,
38
+ labels,
39
+ previewCount,
40
+ expandedCount,
41
+ }: UseChatHistoryRailControllerOptions<Item>): ChatHistoryRailController<Item> {
42
+ const [expandedState, setExpandedState] = useState(false);
43
+ const collapsedLimit = normalizeLimit(previewCount, 5);
44
+ const expandedLimit = Math.max(
45
+ collapsedLimit,
46
+ normalizeLimit(expandedCount, 15),
47
+ );
48
+ const canExpand = items.length > collapsedLimit;
49
+ const expanded = canExpand && expandedState;
50
+ const visibleItems = useMemo(
51
+ () => items.slice(0, expanded ? expandedLimit : collapsedLimit),
52
+ [collapsedLimit, expanded, expandedLimit, items],
53
+ );
54
+
55
+ useEffect(() => {
56
+ if (!canExpand) setExpandedState(false);
57
+ }, [canExpand]);
58
+
59
+ const toggleExpanded = useCallback(() => {
60
+ if (canExpand) setExpandedState((current) => !current);
61
+ }, [canExpand]);
62
+ const resetExpanded = useCallback(() => setExpandedState(false), []);
63
+
64
+ return {
65
+ expanded,
66
+ collapsedLimit,
67
+ expandedLimit,
68
+ visibleItems,
69
+ canExpand,
70
+ newChatLabel: labels.newChat,
71
+ disclosureLabel: expanded ? labels.showLess : labels.showMore,
72
+ onNewChat,
73
+ toggleExpanded,
74
+ resetExpanded,
75
+ };
76
+ }
@@ -16,6 +16,7 @@
16
16
  .an-chat-history {
17
17
  display: flex;
18
18
  flex-direction: column;
19
+ min-width: 0;
19
20
  min-height: 0;
20
21
  }
21
22
 
@@ -49,6 +50,7 @@
49
50
 
50
51
  .an-chat-history__list {
51
52
  overflow-y: auto;
53
+ min-width: 0;
52
54
  padding: 4px 0;
53
55
  max-height: 16rem;
54
56
  }
@@ -59,6 +61,10 @@
59
61
  min-height: 0;
60
62
  }
61
63
 
64
+ .an-chat-history-rail .an-chat-history__state:empty {
65
+ display: none;
66
+ }
67
+
62
68
  .an-chat-history__state {
63
69
  padding: 16px 12px;
64
70
  text-align: center;
@@ -72,6 +78,57 @@
72
78
  color: hsl(38 92% 50%);
73
79
  }
74
80
 
81
+ .an-chat-history-rail__footer {
82
+ display: flex;
83
+ align-items: center;
84
+ gap: 2px;
85
+ min-width: 0;
86
+ padding: 1px 4px 0;
87
+ }
88
+
89
+ .an-chat-history-rail__disclosure,
90
+ .an-chat-history-rail__new-chat {
91
+ display: flex;
92
+ align-items: center;
93
+ border: 0;
94
+ border-radius: calc(var(--radius) - 2px);
95
+ background: transparent;
96
+ color: hsl(var(--muted-foreground) / 0.72);
97
+ cursor: pointer;
98
+ }
99
+
100
+ .an-chat-history-rail__disclosure:hover,
101
+ .an-chat-history-rail__new-chat:hover {
102
+ background: hsl(var(--accent) / 0.5);
103
+ color: hsl(var(--accent-foreground));
104
+ }
105
+
106
+ .an-chat-history-rail__disclosure:focus-visible,
107
+ .an-chat-history-rail__new-chat:focus-visible {
108
+ outline: 2px solid hsl(var(--ring));
109
+ outline-offset: -2px;
110
+ }
111
+
112
+ .an-chat-history-rail__disclosure {
113
+ justify-content: center;
114
+ flex: 0 0 auto;
115
+ width: 30px;
116
+ height: auto;
117
+ min-height: 30px;
118
+ }
119
+
120
+ .an-chat-history-rail__new-chat {
121
+ flex: 1 1 auto;
122
+ gap: 8px;
123
+ height: auto;
124
+ min-width: 0;
125
+ min-height: 30px;
126
+ padding: 6px 10px;
127
+ font-size: 12px;
128
+ font-weight: 500;
129
+ text-align: start;
130
+ }
131
+
75
132
  .an-chat-history__section-label {
76
133
  padding: 6px 12px 4px;
77
134
  font-size: 10px;
@@ -83,12 +140,14 @@
83
140
  .an-chat-history--rail .an-chat-history__section {
84
141
  display: grid;
85
142
  gap: 1px;
143
+ min-width: 0;
86
144
  }
87
145
 
88
146
  /* ── Row ──────────────────────────────────────────────────────────────── */
89
147
 
90
148
  .an-chat-history-row {
91
149
  position: relative;
150
+ min-width: 0;
92
151
  }
93
152
 
94
153
  .an-chat-history--rail .an-chat-history-row {
@@ -99,6 +158,7 @@
99
158
  .an-chat-history-row__button {
100
159
  display: block;
101
160
  width: 100%;
161
+ min-width: 0;
102
162
  padding: 8px 12px;
103
163
  border: 0;
104
164
  background: transparent;
@@ -141,6 +201,7 @@
141
201
  justify-content: space-between;
142
202
  gap: 8px;
143
203
  min-width: 0;
204
+ overflow: hidden;
144
205
  }
145
206
 
146
207
  .an-chat-history-row__title {
@@ -490,14 +490,17 @@ function ComposerPlusMenuFull({
490
490
  <Popover open={open} onOpenChange={setOpen}>
491
491
  <Tooltip>
492
492
  <TooltipTrigger asChild>
493
- <PopoverTrigger asChild>
494
- <button
495
- type="button"
496
- className="shrink-0 flex h-7 w-7 items-center justify-center rounded-md text-muted-foreground hover:text-foreground hover:bg-accent/50 disabled:opacity-30 disabled:cursor-not-allowed"
497
- >
498
- <IconPlus className="h-4 w-4" />
499
- </button>
500
- </PopoverTrigger>
493
+ <span className="inline-flex shrink-0">
494
+ <PopoverTrigger asChild>
495
+ <button
496
+ type="button"
497
+ aria-label="Add..."
498
+ className="shrink-0 flex h-7 w-7 items-center justify-center rounded-md text-muted-foreground hover:text-foreground hover:bg-accent/50 disabled:opacity-30 disabled:cursor-not-allowed"
499
+ >
500
+ <IconPlus className="h-4 w-4" />
501
+ </button>
502
+ </PopoverTrigger>
503
+ </span>
501
504
  </TooltipTrigger>
502
505
  <TooltipContent>Add...</TooltipContent>
503
506
  </Tooltip>
@@ -519,6 +519,7 @@ function PromptComposerInner({
519
519
  resolvedModelStatusChecksEnabled,
520
520
  );
521
521
  const missingApiKey = agentEngineConfigured.missing;
522
+ const agentEngineUnavailable = agentEngineConfigured.state === "unavailable";
522
523
  const [missingKeyBouncePulse, setMissingKeyBouncePulse] = useState(0);
523
524
  const bounceMissingKeySetup = useCallback(() => {
524
525
  setMissingKeyBouncePulse((pulse) => pulse + 1);
@@ -542,8 +543,13 @@ function PromptComposerInner({
542
543
  timeoutMs: SUBMIT_ENGINE_STATUS_TIMEOUT_MS,
543
544
  },
544
545
  );
545
- if (state !== "missing") return true;
546
- bounceMissingKeySetup();
546
+ if (state === "missing") {
547
+ bounceMissingKeySetup();
548
+ return false;
549
+ }
550
+ if (typeof window !== "undefined") {
551
+ window.dispatchEvent(new Event("agent-engine:configured-changed"));
552
+ }
547
553
  return false;
548
554
  }, [
549
555
  agentEngineConfigured.state,
@@ -551,6 +557,11 @@ function PromptComposerInner({
551
557
  modelsAdapter,
552
558
  resolvedModelStatusChecksEnabled,
553
559
  ]);
560
+ const retryAgentEngineStatus = useCallback(() => {
561
+ if (typeof window !== "undefined") {
562
+ window.dispatchEvent(new Event("agent-engine:configured-changed"));
563
+ }
564
+ }, []);
554
565
 
555
566
  useEffect(() => {
556
567
  if (!autoFocus) return;
@@ -613,21 +624,31 @@ function PromptComposerInner({
613
624
  <AgentComposerFrame
614
625
  className={cn(
615
626
  "text-start",
616
- missingApiKey && "cursor-pointer",
627
+ (missingApiKey || agentEngineUnavailable) && "cursor-pointer",
617
628
  className,
618
629
  )}
619
630
  rootClassName={rootClassName}
620
631
  style={style}
621
632
  rootStyle={rootStyle}
622
633
  layoutVariant={layoutVariant}
623
- onClick={missingApiKey ? bounceMissingKeySetup : undefined}
634
+ onClick={
635
+ missingApiKey
636
+ ? bounceMissingKeySetup
637
+ : agentEngineUnavailable
638
+ ? retryAgentEngineStatus
639
+ : undefined
640
+ }
624
641
  >
625
642
  <PromptAttachmentStrip />
626
643
  <TiptapComposer
627
644
  focusRef={handleRef}
628
- disabled={disabled || missingApiKey}
645
+ disabled={disabled || missingApiKey || agentEngineUnavailable}
629
646
  placeholder={
630
- missingApiKey ? "Connect AI above to continue..." : placeholder
647
+ missingApiKey
648
+ ? "Connect AI above to continue..."
649
+ : agentEngineUnavailable
650
+ ? "Unable to check AI connection. Click to retry."
651
+ : placeholder
631
652
  }
632
653
  initialText={initialText}
633
654
  initialTextKey={initialTextKey}
@@ -14,7 +14,7 @@ import {
14
14
 
15
15
  const copy: RealtimeVoiceModeCopy = {
16
16
  entryButtonLabel: "Use microphone",
17
- promptTitle: "Talk to your app",
17
+ promptTitle: "Use your voice",
18
18
  promptDescription:
19
19
  "Voice mode keeps listening while the agent navigates and takes actions.",
20
20
  setupTitle: "Set up voice mode",
@@ -23,8 +23,9 @@ const copy: RealtimeVoiceModeCopy = {
23
23
  connectBuilder: "Connect Builder.io",
24
24
  useOpenAiKey: "Add your own keys",
25
25
  startWithOpenAiKey: "Start with OpenAI key",
26
- startVoiceMode: "Start voice mode",
27
- keepDictating: "Keep dictating",
26
+ startVoiceMode: "Real-time voice",
27
+ keepDictating: "Dictate",
28
+ rememberPreference: "Remember my preference",
28
29
  showChat: "Show chat",
29
30
  hideChat: "Hide chat",
30
31
  endVoiceMode: "End voice mode",
@@ -127,23 +128,27 @@ describe("RealtimeVoiceMode", () => {
127
128
 
128
129
  act(() => microphone?.click());
129
130
 
130
- expect(document.body.textContent).toContain("Talk to your app");
131
+ expect(document.body.textContent).toContain("Use your voice");
131
132
  const prompt = document.querySelector<HTMLElement>('[role="dialog"]');
132
133
  expect(prompt?.className).toContain(
133
- "w-[min(calc(100vw-2rem),var(--radix-popover-content-available-width,22rem),22rem)]",
134
+ "w-[min(calc(100vw-2rem),var(--radix-popover-content-available-width,18rem),18rem)]",
134
135
  );
135
- expect(document.body.textContent).toContain("Keep dictating");
136
+ expect(document.body.textContent).not.toContain(
137
+ "Voice mode keeps listening while the agent navigates and takes actions.",
138
+ );
139
+ expect(document.body.textContent).toContain("Dictate");
140
+ expect(document.body.textContent).toContain("Remember my preference");
136
141
  expect(onStartVoiceMode).not.toHaveBeenCalled();
137
142
  expect(onKeepDictating).not.toHaveBeenCalled();
138
143
 
139
144
  const startVoiceMode = Array.from(
140
145
  document.querySelectorAll<HTMLButtonElement>("button"),
141
- ).find((button) => button.textContent?.includes("Start voice mode"));
146
+ ).find((button) => button.textContent?.includes("Real-time voice"));
142
147
  act(() => startVoiceMode?.click());
143
148
 
144
149
  expect(onStartVoiceMode).toHaveBeenCalledOnce();
145
150
  expect(onKeepDictating).not.toHaveBeenCalled();
146
- expect(document.body.textContent).not.toContain("Talk to your app");
151
+ expect(document.body.textContent).not.toContain("Use your voice");
147
152
  });
148
153
 
149
154
  it("does not start realtime voice while provider readiness is unresolved", () => {
@@ -162,14 +167,14 @@ describe("RealtimeVoiceMode", () => {
162
167
 
163
168
  const startVoiceMode = Array.from(
164
169
  document.querySelectorAll<HTMLButtonElement>("button"),
165
- ).find((button) => button.textContent?.includes("Start voice mode"));
170
+ ).find((button) => button.textContent?.includes("Real-time voice"));
166
171
  expect(startVoiceMode?.disabled).toBe(true);
167
172
  act(() => startVoiceMode?.click());
168
173
  expect(onStartVoiceMode).not.toHaveBeenCalled();
169
174
 
170
175
  const keepDictating = Array.from(
171
176
  document.querySelectorAll<HTMLButtonElement>("button"),
172
- ).find((button) => button.textContent === "Keep dictating");
177
+ ).find((button) => button.textContent === "Dictate");
173
178
  expect(keepDictating?.disabled).toBe(false);
174
179
  });
175
180
 
@@ -188,13 +193,83 @@ describe("RealtimeVoiceMode", () => {
188
193
 
189
194
  const keepDictating = Array.from(
190
195
  document.querySelectorAll<HTMLButtonElement>("button"),
191
- ).find((button) => button.textContent === "Keep dictating");
196
+ ).find((button) => button.textContent === "Dictate");
192
197
  act(() => keepDictating?.click());
193
198
 
194
199
  expect(onKeepDictating).toHaveBeenCalledOnce();
195
200
  expect(onStartVoiceMode).not.toHaveBeenCalled();
196
201
  });
197
202
 
203
+ it("remembers the selected input mode only when requested", () => {
204
+ const onStartVoiceMode = vi.fn();
205
+ const onRememberPreference = vi.fn();
206
+
207
+ render(
208
+ <RealtimeVoiceModeEntry
209
+ copy={copy}
210
+ open
211
+ onStartVoiceMode={onStartVoiceMode}
212
+ onKeepDictating={vi.fn()}
213
+ onRememberPreference={onRememberPreference}
214
+ />,
215
+ );
216
+
217
+ const remember = document.querySelector<HTMLElement>('[role="checkbox"]');
218
+ act(() => remember?.click());
219
+ const realtime = Array.from(
220
+ document.querySelectorAll<HTMLButtonElement>("button"),
221
+ ).find((button) => button.textContent?.includes("Real-time voice"));
222
+ act(() => realtime?.click());
223
+
224
+ expect(onRememberPreference).toHaveBeenCalledWith("realtime");
225
+ expect(onStartVoiceMode).toHaveBeenCalledOnce();
226
+ });
227
+
228
+ it("bypasses the chooser for a remembered input mode", () => {
229
+ const onStartVoiceMode = vi.fn();
230
+ const onKeepDictating = vi.fn();
231
+
232
+ render(
233
+ <RealtimeVoiceModeEntry
234
+ copy={copy}
235
+ preferredMode="dictation"
236
+ onStartVoiceMode={onStartVoiceMode}
237
+ onKeepDictating={onKeepDictating}
238
+ />,
239
+ );
240
+
241
+ const microphone = document.querySelector<HTMLButtonElement>(
242
+ 'button[aria-label="Use microphone"]',
243
+ );
244
+ act(() => microphone?.click());
245
+
246
+ expect(onKeepDictating).toHaveBeenCalledOnce();
247
+ expect(onStartVoiceMode).not.toHaveBeenCalled();
248
+ expect(document.body.textContent).not.toContain("Use your voice");
249
+ });
250
+
251
+ it("shows setup instead of starting a remembered realtime mode without a provider", () => {
252
+ const onStartVoiceMode = vi.fn();
253
+
254
+ render(
255
+ <RealtimeVoiceModeEntry
256
+ copy={copy}
257
+ setupRequired
258
+ preferredMode="realtime"
259
+ onStartVoiceMode={onStartVoiceMode}
260
+ onKeepDictating={vi.fn()}
261
+ />,
262
+ );
263
+
264
+ const microphone = document.querySelector<HTMLButtonElement>(
265
+ 'button[aria-label="Use microphone"]',
266
+ );
267
+ act(() => microphone?.click());
268
+
269
+ expect(onStartVoiceMode).not.toHaveBeenCalled();
270
+ expect(document.body.textContent).toContain("Set up voice mode");
271
+ });
272
+
198
273
  it("makes Builder the primary setup action and own keys the secondary", () => {
199
274
  const onConnectBuilder = vi.fn();
200
275
  const onUseOpenAiKey = vi.fn();
@@ -653,6 +728,10 @@ describe("RealtimeVoiceMode", () => {
653
728
  expect(
654
729
  document.querySelector('[data-realtime-voice-waveform="true"]'),
655
730
  ).not.toBeNull();
731
+ const centerBar = document.querySelectorAll<HTMLElement>(
732
+ '[data-realtime-voice-waveform="true"] > span',
733
+ )[2];
734
+ expect(centerBar?.style.transform).toBe("scaleY(0.76)");
656
735
 
657
736
  act(() => audioLevels.set({ input: 0, output: 0.8 }));
658
737
  expect(
@@ -687,6 +766,32 @@ describe("RealtimeVoiceMode", () => {
687
766
  },
688
767
  );
689
768
 
769
+ it("shines only while the agent is working", () => {
770
+ render(
771
+ <RealtimeVoiceModeDock
772
+ state="working"
773
+ copy={copy}
774
+ chatVisible={false}
775
+ onToggleChat={vi.fn()}
776
+ onEndVoiceMode={vi.fn()}
777
+ />,
778
+ );
779
+ expect(
780
+ document.querySelector(".agent-realtime-voice-working"),
781
+ ).not.toBeNull();
782
+
783
+ render(
784
+ <RealtimeVoiceModeDock
785
+ state="listening"
786
+ copy={copy}
787
+ chatVisible={false}
788
+ onToggleChat={vi.fn()}
789
+ onEndVoiceMode={vi.fn()}
790
+ />,
791
+ );
792
+ expect(document.querySelector(".agent-realtime-voice-working")).toBeNull();
793
+ });
794
+
690
795
  it("shows an unmistakable loader and ignores early audio until connected", () => {
691
796
  const audioLevels = createRealtimeVoiceAudioLevelStore();
692
797
  audioLevels.set({ input: 0.8, output: 0 });