@agent-native/toolkit 0.221.0-same-host-mcp-20261027-20261027081850 → 0.221.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 (259) hide show
  1. package/agent-native.eject.json +16 -1
  2. package/dist/app/agentkit/react/components.d.ts +5 -3
  3. package/dist/app/agentkit/react/components.d.ts.map +1 -1
  4. package/dist/app/agentkit/react/components.js +103 -42
  5. package/dist/app/agentkit/react/components.js.map +1 -1
  6. package/dist/app/agentkit/react/styles.css +93 -65
  7. package/dist/app/agentkit/react/voice-transcripts.d.ts +22 -1
  8. package/dist/app/agentkit/react/voice-transcripts.d.ts.map +1 -1
  9. package/dist/app/agentkit/react/voice-transcripts.js +209 -8
  10. package/dist/app/agentkit/react/voice-transcripts.js.map +1 -1
  11. package/dist/app/chat/AgentKitAssistantChat.d.ts.map +1 -1
  12. package/dist/app/chat/AgentKitAssistantChat.js +55 -40
  13. package/dist/app/chat/AgentKitAssistantChat.js.map +1 -1
  14. package/dist/app/chat/agentkit-chat/guided-questions.d.ts +1 -0
  15. package/dist/app/chat/agentkit-chat/guided-questions.d.ts.map +1 -1
  16. package/dist/app/chat/agentkit-chat/guided-questions.js +18 -6
  17. package/dist/app/chat/agentkit-chat/guided-questions.js.map +1 -1
  18. package/dist/app/chat/agentkit-chat/index.d.ts +1 -1
  19. package/dist/app/chat/agentkit-chat/index.d.ts.map +1 -1
  20. package/dist/app/chat/agentkit-chat/index.js +1 -1
  21. package/dist/app/chat/agentkit-chat/index.js.map +1 -1
  22. package/dist/app/chat/chat/run-recovery.d.ts.map +1 -1
  23. package/dist/app/chat/chat/run-recovery.js +5 -1
  24. package/dist/app/chat/chat/run-recovery.js.map +1 -1
  25. package/dist/app/chat/composer/runtime-adapters.d.ts.map +1 -1
  26. package/dist/app/chat/composer/runtime-adapters.js +2 -0
  27. package/dist/app/chat/composer/runtime-adapters.js.map +1 -1
  28. package/dist/app/chat/reply-connection-suggestion.d.ts +29 -0
  29. package/dist/app/chat/reply-connection-suggestion.d.ts.map +1 -0
  30. package/dist/app/chat/reply-connection-suggestion.js +42 -0
  31. package/dist/app/chat/reply-connection-suggestion.js.map +1 -0
  32. package/dist/app/i18n/catalogs/ar-SA.d.ts.map +1 -1
  33. package/dist/app/i18n/catalogs/ar-SA.js +7 -0
  34. package/dist/app/i18n/catalogs/ar-SA.js.map +1 -1
  35. package/dist/app/i18n/catalogs/de-DE.d.ts.map +1 -1
  36. package/dist/app/i18n/catalogs/de-DE.js +7 -0
  37. package/dist/app/i18n/catalogs/de-DE.js.map +1 -1
  38. package/dist/app/i18n/catalogs/en-US.d.ts +7 -0
  39. package/dist/app/i18n/catalogs/en-US.d.ts.map +1 -1
  40. package/dist/app/i18n/catalogs/en-US.js +7 -0
  41. package/dist/app/i18n/catalogs/en-US.js.map +1 -1
  42. package/dist/app/i18n/catalogs/es-ES.d.ts.map +1 -1
  43. package/dist/app/i18n/catalogs/es-ES.js +7 -0
  44. package/dist/app/i18n/catalogs/es-ES.js.map +1 -1
  45. package/dist/app/i18n/catalogs/fr-FR.d.ts.map +1 -1
  46. package/dist/app/i18n/catalogs/fr-FR.js +7 -0
  47. package/dist/app/i18n/catalogs/fr-FR.js.map +1 -1
  48. package/dist/app/i18n/catalogs/hi-IN.d.ts.map +1 -1
  49. package/dist/app/i18n/catalogs/hi-IN.js +7 -0
  50. package/dist/app/i18n/catalogs/hi-IN.js.map +1 -1
  51. package/dist/app/i18n/catalogs/ja-JP.d.ts.map +1 -1
  52. package/dist/app/i18n/catalogs/ja-JP.js +7 -0
  53. package/dist/app/i18n/catalogs/ja-JP.js.map +1 -1
  54. package/dist/app/i18n/catalogs/ko-KR.d.ts.map +1 -1
  55. package/dist/app/i18n/catalogs/ko-KR.js +7 -0
  56. package/dist/app/i18n/catalogs/ko-KR.js.map +1 -1
  57. package/dist/app/i18n/catalogs/pt-BR.d.ts.map +1 -1
  58. package/dist/app/i18n/catalogs/pt-BR.js +7 -0
  59. package/dist/app/i18n/catalogs/pt-BR.js.map +1 -1
  60. package/dist/app/i18n/catalogs/zh-CN.d.ts.map +1 -1
  61. package/dist/app/i18n/catalogs/zh-CN.js +7 -0
  62. package/dist/app/i18n/catalogs/zh-CN.js.map +1 -1
  63. package/dist/app/i18n/catalogs/zh-TW.d.ts.map +1 -1
  64. package/dist/app/i18n/catalogs/zh-TW.js +7 -0
  65. package/dist/app/i18n/catalogs/zh-TW.js.map +1 -1
  66. package/dist/app/mcp-approval/McpApprovalReview.d.ts +25 -0
  67. package/dist/app/mcp-approval/McpApprovalReview.d.ts.map +1 -0
  68. package/dist/app/mcp-approval/McpApprovalReview.js +342 -0
  69. package/dist/app/mcp-approval/McpApprovalReview.js.map +1 -0
  70. package/dist/app/onboarding/FirstRunOnboarding.d.ts.map +1 -1
  71. package/dist/app/onboarding/FirstRunOnboarding.js +299 -71
  72. package/dist/app/onboarding/FirstRunOnboarding.js.map +1 -1
  73. package/dist/app/onboarding/GettingStartedCard.js +1 -1
  74. package/dist/app/onboarding/GettingStartedCard.js.map +1 -1
  75. package/dist/app/onboarding/first-run-builder-credits-flag.d.ts +2 -0
  76. package/dist/app/onboarding/first-run-builder-credits-flag.d.ts.map +1 -0
  77. package/dist/app/onboarding/first-run-builder-credits-flag.js +8 -0
  78. package/dist/app/onboarding/first-run-builder-credits-flag.js.map +1 -0
  79. package/dist/app/providers/AppProviders.d.ts.map +1 -1
  80. package/dist/app/providers/AppProviders.js +14 -1
  81. package/dist/app/providers/AppProviders.js.map +1 -1
  82. package/dist/app/resources/McpConnectionSuggestion.d.ts +2 -4
  83. package/dist/app/resources/McpConnectionSuggestion.d.ts.map +1 -1
  84. package/dist/app/resources/McpConnectionSuggestion.js +59 -36
  85. package/dist/app/resources/McpConnectionSuggestion.js.map +1 -1
  86. package/dist/app/settings/account/PreferencesSettings.d.ts.map +1 -1
  87. package/dist/app/settings/account/PreferencesSettings.js +14 -1
  88. package/dist/app/settings/account/PreferencesSettings.js.map +1 -1
  89. package/dist/app/settings/account/ThemeFamilySettings.js +2 -2
  90. package/dist/app/settings/account/ThemeFamilySettings.js.map +1 -1
  91. package/dist/app/settings/account/search-entries.d.ts +1 -0
  92. package/dist/app/settings/account/search-entries.d.ts.map +1 -1
  93. package/dist/app/settings/account/search-entries.js +5 -0
  94. package/dist/app/settings/account/search-entries.js.map +1 -1
  95. package/dist/app/settings/shell/SettingsShell.d.ts.map +1 -1
  96. package/dist/app/settings/shell/SettingsShell.js +26 -2
  97. package/dist/app/settings/shell/SettingsShell.js.map +1 -1
  98. package/dist/app/shared/AppShellSkeleton.d.ts.map +1 -1
  99. package/dist/app/shared/AppShellSkeleton.js +4 -10
  100. package/dist/app/shared/AppShellSkeleton.js.map +1 -1
  101. package/dist/app/shared/EnvironmentBadge.d.ts +2 -1
  102. package/dist/app/shared/EnvironmentBadge.d.ts.map +1 -1
  103. package/dist/app/shared/EnvironmentBadge.js +2 -3
  104. package/dist/app/shared/EnvironmentBadge.js.map +1 -1
  105. package/dist/app/shared/is-builder-io-employee.d.ts +8 -0
  106. package/dist/app/shared/is-builder-io-employee.d.ts.map +1 -0
  107. package/dist/app/shared/is-builder-io-employee.js +10 -0
  108. package/dist/app/shared/is-builder-io-employee.js.map +1 -0
  109. package/dist/app/styles/agent-conversation.css +30 -17
  110. package/dist/app/styles/theme-families.css +22 -5
  111. package/dist/app/styles/tokens/agent-kit.css +3 -3
  112. package/dist/composer/PromptComposer.d.ts +15 -3
  113. package/dist/composer/PromptComposer.d.ts.map +1 -1
  114. package/dist/composer/PromptComposer.js +74 -16
  115. package/dist/composer/PromptComposer.js.map +1 -1
  116. package/dist/composer/RealtimeVoiceMode.d.ts +7 -0
  117. package/dist/composer/RealtimeVoiceMode.d.ts.map +1 -1
  118. package/dist/composer/RealtimeVoiceMode.js +9 -2
  119. package/dist/composer/RealtimeVoiceMode.js.map +1 -1
  120. package/dist/composer/TiptapComposer.d.ts +7 -0
  121. package/dist/composer/TiptapComposer.d.ts.map +1 -1
  122. package/dist/composer/TiptapComposer.js +12 -4
  123. package/dist/composer/TiptapComposer.js.map +1 -1
  124. package/dist/composer/VoiceButton.d.ts +2 -0
  125. package/dist/composer/VoiceButton.d.ts.map +1 -1
  126. package/dist/composer/VoiceButton.js +5 -4
  127. package/dist/composer/VoiceButton.js.map +1 -1
  128. package/dist/composer/index.d.ts +2 -2
  129. package/dist/composer/index.d.ts.map +1 -1
  130. package/dist/composer/index.js +1 -1
  131. package/dist/composer/index.js.map +1 -1
  132. package/dist/composer/prompt-attachments.d.ts +20 -0
  133. package/dist/composer/prompt-attachments.d.ts.map +1 -1
  134. package/dist/composer/prompt-attachments.js +188 -11
  135. package/dist/composer/prompt-attachments.js.map +1 -1
  136. package/dist/composer/realtime-voice-transcript.d.ts +27 -0
  137. package/dist/composer/realtime-voice-transcript.d.ts.map +1 -1
  138. package/dist/composer/realtime-voice-transcript.js +34 -1
  139. package/dist/composer/realtime-voice-transcript.js.map +1 -1
  140. package/dist/composer/realtime-voice-turn-metrics.d.ts +43 -0
  141. package/dist/composer/realtime-voice-turn-metrics.d.ts.map +1 -0
  142. package/dist/composer/realtime-voice-turn-metrics.js +132 -0
  143. package/dist/composer/realtime-voice-turn-metrics.js.map +1 -0
  144. package/dist/composer/runtime-adapters.d.ts +4 -1
  145. package/dist/composer/runtime-adapters.d.ts.map +1 -1
  146. package/dist/composer/runtime-adapters.js +2 -1
  147. package/dist/composer/runtime-adapters.js.map +1 -1
  148. package/dist/composer/useRealtimeVoiceMode.d.ts +61 -9
  149. package/dist/composer/useRealtimeVoiceMode.d.ts.map +1 -1
  150. package/dist/composer/useRealtimeVoiceMode.js +536 -99
  151. package/dist/composer/useRealtimeVoiceMode.js.map +1 -1
  152. package/dist/composer/voice-chat-icon.d.ts +7 -0
  153. package/dist/composer/voice-chat-icon.d.ts.map +1 -0
  154. package/dist/composer/voice-chat-icon.js +14 -0
  155. package/dist/composer/voice-chat-icon.js.map +1 -0
  156. package/dist/editor/DragHandle.d.ts +12 -1
  157. package/dist/editor/DragHandle.d.ts.map +1 -1
  158. package/dist/editor/DragHandle.js +165 -41
  159. package/dist/editor/DragHandle.js.map +1 -1
  160. package/dist/editor/RegistryBlockNode.d.ts +20 -0
  161. package/dist/editor/RegistryBlockNode.d.ts.map +1 -1
  162. package/dist/editor/RegistryBlockNode.js +58 -13
  163. package/dist/editor/RegistryBlockNode.js.map +1 -1
  164. package/dist/editor/index.d.ts +2 -2
  165. package/dist/editor/index.d.ts.map +1 -1
  166. package/dist/editor/index.js +1 -1
  167. package/dist/editor/index.js.map +1 -1
  168. package/dist/styles.css +2 -0
  169. package/dist/ui/dropdown-menu.d.ts.map +1 -1
  170. package/dist/ui/dropdown-menu.js +5 -1
  171. package/dist/ui/dropdown-menu.js.map +1 -1
  172. package/package.json +14 -4
  173. package/src/app/agentkit/react/components.regenerate-readiness.spec.tsx +3 -1
  174. package/src/app/agentkit/react/components.spec.tsx +24 -4
  175. package/src/app/agentkit/react/components.tsx +174 -44
  176. package/src/app/agentkit/react/composer-context.spec.tsx +3 -0
  177. package/src/app/agentkit/react/disclosures.spec.tsx +124 -0
  178. package/src/app/agentkit/react/lifecycle.spec.tsx +8 -2
  179. package/src/app/agentkit/react/message-actions.spec.tsx +161 -5
  180. package/src/app/agentkit/react/saved-reply-reload.spec.tsx +206 -0
  181. package/src/app/agentkit/react/styles.css +93 -65
  182. package/src/app/agentkit/react/voice-transcripts.spec.tsx +239 -4
  183. package/src/app/agentkit/react/voice-transcripts.ts +245 -8
  184. package/src/app/chat/AgentKitAssistantChat.spec.tsx +59 -11
  185. package/src/app/chat/AgentKitAssistantChat.tsx +74 -62
  186. package/src/app/chat/agentkit-chat/guided-answers-context.spec.ts +9 -0
  187. package/src/app/chat/agentkit-chat/guided-questions.tsx +17 -7
  188. package/src/app/chat/agentkit-chat/index.ts +1 -0
  189. package/src/app/chat/chat/run-recovery.spec.tsx +45 -0
  190. package/src/app/chat/chat/run-recovery.tsx +6 -1
  191. package/src/app/chat/composer/runtime-adapters.tsx +2 -0
  192. package/src/app/chat/reply-connection-suggestion.spec.ts +121 -0
  193. package/src/app/chat/reply-connection-suggestion.ts +76 -0
  194. package/src/app/i18n/catalogs/ar-SA.ts +10 -0
  195. package/src/app/i18n/catalogs/de-DE.ts +10 -0
  196. package/src/app/i18n/catalogs/en-US.ts +10 -0
  197. package/src/app/i18n/catalogs/es-ES.ts +10 -0
  198. package/src/app/i18n/catalogs/fr-FR.ts +10 -0
  199. package/src/app/i18n/catalogs/hi-IN.ts +10 -0
  200. package/src/app/i18n/catalogs/ja-JP.ts +10 -0
  201. package/src/app/i18n/catalogs/ko-KR.ts +10 -0
  202. package/src/app/i18n/catalogs/pt-BR.ts +10 -0
  203. package/src/app/i18n/catalogs/zh-CN.ts +10 -0
  204. package/src/app/i18n/catalogs/zh-TW.ts +10 -0
  205. package/src/app/mcp-approval/McpApprovalReview.spec.tsx +687 -0
  206. package/src/app/mcp-approval/McpApprovalReview.tsx +913 -0
  207. package/src/app/onboarding/FirstRunOnboarding.spec.tsx +736 -26
  208. package/src/app/onboarding/FirstRunOnboarding.tsx +473 -145
  209. package/src/app/onboarding/GettingStartedCard.tsx +1 -1
  210. package/src/app/onboarding/first-run-builder-credits-flag.spec.ts +13 -0
  211. package/src/app/onboarding/first-run-builder-credits-flag.ts +9 -0
  212. package/src/app/providers/AppProviders.spec.tsx +109 -0
  213. package/src/app/providers/AppProviders.tsx +53 -1
  214. package/src/app/resources/McpConnectionSuggestion.spec.ts +77 -12
  215. package/src/app/resources/McpConnectionSuggestion.tsx +69 -51
  216. package/src/app/settings/VoiceChatSettings.spec.tsx +6 -1
  217. package/src/app/settings/account/PreferencesSettings.spec.tsx +73 -0
  218. package/src/app/settings/account/PreferencesSettings.tsx +24 -7
  219. package/src/app/settings/account/ThemeFamilySettings.tsx +2 -2
  220. package/src/app/settings/account/search-entries.spec.ts +14 -0
  221. package/src/app/settings/account/search-entries.ts +9 -0
  222. package/src/app/settings/shell/SettingsShell.tsx +29 -2
  223. package/src/app/shared/AppShellSkeleton.spec.tsx +2 -2
  224. package/src/app/shared/AppShellSkeleton.tsx +3 -16
  225. package/src/app/shared/EnvironmentBadge.tsx +3 -4
  226. package/src/app/shared/is-builder-io-employee.ts +17 -0
  227. package/src/app/styles/agent-conversation.css +30 -17
  228. package/src/app/styles/theme-families.css +22 -5
  229. package/src/app/styles/theme-families.spec.ts +73 -0
  230. package/src/app/styles/tokens/agent-kit.css +3 -3
  231. package/src/composer/AgentSuggestionBar.overflow.spec.tsx +1 -1
  232. package/src/composer/AttachmentChip.spec.tsx +87 -0
  233. package/src/composer/PromptComposer.context.spec.tsx +8 -4
  234. package/src/composer/PromptComposer.setup-card.spec.tsx +170 -0
  235. package/src/composer/PromptComposer.tsx +125 -14
  236. package/src/composer/RealtimeVoiceMode.spec.tsx +48 -0
  237. package/src/composer/RealtimeVoiceMode.tsx +45 -2
  238. package/src/composer/TiptapComposer.tsx +25 -0
  239. package/src/composer/TiptapComposer.voice-toolbar.spec.tsx +4 -1
  240. package/src/composer/VoiceButton.tsx +8 -3
  241. package/src/composer/index.ts +4 -0
  242. package/src/composer/prompt-attachments.spec.ts +160 -8
  243. package/src/composer/prompt-attachments.ts +221 -12
  244. package/src/composer/realtime-voice-transcript.spec.ts +36 -0
  245. package/src/composer/realtime-voice-transcript.ts +56 -1
  246. package/src/composer/realtime-voice-turn-metrics.spec.ts +175 -0
  247. package/src/composer/realtime-voice-turn-metrics.ts +192 -0
  248. package/src/composer/runtime-adapters.tsx +5 -1
  249. package/src/composer/useRealtimeVoiceMode.session.spec.tsx +487 -2
  250. package/src/composer/useRealtimeVoiceMode.spec.ts +267 -65
  251. package/src/composer/useRealtimeVoiceMode.tsx +716 -139
  252. package/src/composer/voice-chat-icon.tsx +19 -0
  253. package/src/editor/DragHandle.spec.ts +217 -0
  254. package/src/editor/DragHandle.ts +214 -55
  255. package/src/editor/RegistryBlockNode.spec.ts +172 -2
  256. package/src/editor/RegistryBlockNode.tsx +91 -13
  257. package/src/editor/index.ts +4 -0
  258. package/src/styles.css +2 -0
  259. package/src/ui/dropdown-menu.tsx +4 -0
@@ -1258,9 +1258,9 @@ describe("controlled composer context", () => {
1258
1258
  expect(
1259
1259
  container.querySelector('[data-testid="provider-setup"]'),
1260
1260
  ).not.toBeNull();
1261
- // A draft can be typed before connecting; only sending is gated.
1261
+ // Nothing can be typed until AI is connected.
1262
1262
  expect(
1263
- container.querySelector('[contenteditable="true"]'),
1263
+ container.querySelector('[contenteditable="false"]'),
1264
1264
  ).not.toBeNull();
1265
1265
  const uploadTrigger = container.querySelector<HTMLButtonElement>(
1266
1266
  'button[aria-label="Add context"]',
@@ -1271,7 +1271,7 @@ describe("controlled composer context", () => {
1271
1271
  container
1272
1272
  .querySelector('[role="textbox"]')
1273
1273
  ?.getAttribute("aria-disabled"),
1274
- ).toBe("false");
1274
+ ).toBe("true");
1275
1275
  await act(async () =>
1276
1276
  uploadTrigger.dispatchEvent(
1277
1277
  new KeyboardEvent("keydown", { key: "ArrowDown", bubbles: true }),
@@ -1290,6 +1290,10 @@ describe("controlled composer context", () => {
1290
1290
  ).toBe(false),
1291
1291
  );
1292
1292
  expect(onSubmit).not.toHaveBeenCalled();
1293
+ expect(onDisabledClick).not.toHaveBeenCalled();
1294
+ await act(async () =>
1295
+ container.querySelector<HTMLElement>(".ProseMirror")!.click(),
1296
+ );
1293
1297
  expect(onDisabledClick).toHaveBeenCalledOnce();
1294
1298
  onDisabledClick.mockClear();
1295
1299
  blocked = false;
@@ -1662,7 +1666,7 @@ describe("controlled composer context", () => {
1662
1666
  expect(
1663
1667
  container.querySelector('[data-testid="provider-setup"]'),
1664
1668
  ).not.toBeNull();
1665
- expect(editor.getAttribute("contenteditable")).toBe("true");
1669
+ expect(editor.getAttribute("contenteditable")).toBe("false");
1666
1670
  expect(editor.textContent).toBe("Typed while readiness was checking");
1667
1671
  } else {
1668
1672
  expect(onSubmit).not.toHaveBeenCalled();
@@ -0,0 +1,170 @@
1
+ // @vitest-environment happy-dom
2
+
3
+ import { act } from "react";
4
+ import { createRoot, type Root } from "react-dom/client";
5
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
6
+
7
+ import { PromptComposer } from "./PromptComposer.js";
8
+ import {
9
+ ComposerRuntimeAdaptersProvider,
10
+ type ComposerAgentEngineState,
11
+ } from "./runtime-adapters.js";
12
+
13
+ let container: HTMLDivElement;
14
+ let root: Root;
15
+
16
+ beforeEach(() => {
17
+ vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
18
+ container = document.createElement("div");
19
+ document.body.appendChild(container);
20
+ root = createRoot(container);
21
+ });
22
+
23
+ afterEach(() => {
24
+ act(() => root.unmount());
25
+ container.remove();
26
+ vi.unstubAllGlobals();
27
+ });
28
+
29
+ function SetupCard({ bouncePulse }: { bouncePulse?: number }) {
30
+ return <div data-testid="setup-card" data-bounce={bouncePulse ?? 0} />;
31
+ }
32
+
33
+ async function render({
34
+ readiness,
35
+ configured,
36
+ otherProviderConfigured = false,
37
+ catalogFailed = false,
38
+ onModelConnectionRequiredChange,
39
+ }: {
40
+ readiness: ComposerAgentEngineState;
41
+ configured: boolean;
42
+ otherProviderConfigured?: boolean;
43
+ catalogFailed?: boolean;
44
+ onModelConnectionRequiredChange?: (required: boolean) => void;
45
+ }) {
46
+ await act(async () => {
47
+ root.render(
48
+ <ComposerRuntimeAdaptersProvider
49
+ adapters={{
50
+ models: {
51
+ useAgentEngineConfigured: () => ({
52
+ missing: readiness === "missing",
53
+ state: readiness,
54
+ }),
55
+ BuilderSetupCard: SetupCard,
56
+ },
57
+ }}
58
+ >
59
+ <PromptComposer
60
+ autoFocus={false}
61
+ onSubmit={vi.fn()}
62
+ requireAgentEngine
63
+ modelStatusChecksEnabled={false}
64
+ availableModels={
65
+ catalogFailed
66
+ ? []
67
+ : [
68
+ {
69
+ engine: "anthropic",
70
+ label: "Anthropic",
71
+ models: ["example-model"],
72
+ configured,
73
+ },
74
+ ...(otherProviderConfigured
75
+ ? [
76
+ {
77
+ engine: "openai",
78
+ label: "OpenAI",
79
+ models: ["other-model"],
80
+ configured: true,
81
+ },
82
+ ]
83
+ : []),
84
+ ]
85
+ }
86
+ selectedModel="example-model"
87
+ selectedEngine="anthropic"
88
+ onModelChange={vi.fn()}
89
+ onConnectProvider={vi.fn()}
90
+ modelListLoading={catalogFailed ? false : undefined}
91
+ onModelConnectionRequiredChange={onModelConnectionRequiredChange}
92
+ includeDefaultMentionSearch={false}
93
+ includeDefaultSlashSkills={false}
94
+ voiceEnabled={false}
95
+ />
96
+ </ComposerRuntimeAdaptersProvider>,
97
+ );
98
+ });
99
+ }
100
+
101
+ const card = () =>
102
+ container.querySelector<HTMLElement>('[data-testid="setup-card"]');
103
+ const editor = () => container.querySelector<HTMLElement>(".ProseMirror");
104
+
105
+ describe("PromptComposer with no AI connected", () => {
106
+ it("shows the setup card and closes the prompt before readiness answers", async () => {
107
+ const onRequired = vi.fn();
108
+ await render({
109
+ readiness: "unknown",
110
+ configured: false,
111
+ onModelConnectionRequiredChange: onRequired,
112
+ });
113
+
114
+ expect(card()).not.toBeNull();
115
+ expect(editor()?.getAttribute("contenteditable")).toBe("false");
116
+ expect(onRequired).toHaveBeenLastCalledWith(true);
117
+ });
118
+
119
+ it("points a click on the closed prompt at the setup card", async () => {
120
+ await render({ readiness: "unknown", configured: false });
121
+
122
+ await act(async () => {
123
+ editor()!.click();
124
+ });
125
+
126
+ expect(card()?.dataset.bounce).toBe("1");
127
+ });
128
+
129
+ it("keeps the prompt open once readiness says AI is configured", async () => {
130
+ await render({ readiness: "configured", configured: false });
131
+
132
+ expect(card()).toBeNull();
133
+ expect(editor()?.getAttribute("contenteditable")).toBe("true");
134
+ });
135
+
136
+ it("keeps the prompt open when only the selected model's provider is unconnected", async () => {
137
+ const onRequired = vi.fn();
138
+ await render({
139
+ readiness: "unknown",
140
+ configured: false,
141
+ otherProviderConfigured: true,
142
+ onModelConnectionRequiredChange: onRequired,
143
+ });
144
+
145
+ expect(card()).toBeNull();
146
+ expect(editor()?.getAttribute("contenteditable")).toBe("true");
147
+ expect(onRequired).not.toHaveBeenCalledWith(true);
148
+ });
149
+
150
+ it("keeps the prompt open when the model catalog failed to load", async () => {
151
+ const onRequired = vi.fn();
152
+ await render({
153
+ readiness: "unavailable",
154
+ configured: false,
155
+ catalogFailed: true,
156
+ onModelConnectionRequiredChange: onRequired,
157
+ });
158
+
159
+ expect(card()).toBeNull();
160
+ expect(editor()?.getAttribute("contenteditable")).toBe("true");
161
+ expect(onRequired).not.toHaveBeenCalledWith(true);
162
+ });
163
+
164
+ it("keeps the prompt open while the model list has a connected provider", async () => {
165
+ await render({ readiness: "unknown", configured: true });
166
+
167
+ expect(card()).toBeNull();
168
+ expect(editor()?.getAttribute("contenteditable")).toBe("true");
169
+ });
170
+ });
@@ -16,7 +16,7 @@ import {
16
16
  CompositeAttachmentAdapter,
17
17
  SimpleImageAttachmentAdapter,
18
18
  } from "@assistant-ui/react";
19
- import { IconX } from "@tabler/icons-react";
19
+ import { IconAlertTriangle, IconX } from "@tabler/icons-react";
20
20
  import {
21
21
  useCallback,
22
22
  useEffect,
@@ -51,12 +51,19 @@ import type { ComposerTerminalModeControl } from "./ComposerPlusMenu.js";
51
51
  import type { ComposerContextSnapshot } from "./context-items.js";
52
52
  import { isPastedTextAttachmentName } from "./pasted-text.js";
53
53
  import { PastedTextChip } from "./PastedTextChip.js";
54
- import { escapePromptAttachmentAttribute } from "./prompt-attachments.js";
55
54
  import {
55
+ escapePromptAttachmentAttribute,
56
+ getUnshrinkableImageError,
57
+ probeAgentPromptAttachment,
58
+ type AgentPromptAttachmentReadError,
59
+ } from "./prompt-attachments.js";
60
+ import {
61
+ type ComposerTranslate,
56
62
  type EngineModelGroup,
57
63
  type ComposerAgentEngineState,
58
64
  type AgentChatContextItem,
59
65
  type ReasoningEffort,
66
+ fallbackTranslate,
60
67
  useComposerRuntimeAdapters,
61
68
  } from "./runtime-adapters.js";
62
69
  import {
@@ -162,6 +169,11 @@ export interface PromptComposerProps {
162
169
  * whether this composer shows its own setup card.
163
170
  */
164
171
  setupCardVisible?: boolean;
172
+ /**
173
+ * Reports whether the loaded model list offers only connect paths, so a
174
+ * host that owns the setup card can show it before readiness answers.
175
+ */
176
+ onModelConnectionRequiredChange?: (required: boolean) => void;
165
177
  /** Disable only the send control while the submission is being accepted. */
166
178
  sendButtonDisabled?: boolean;
167
179
  /** Prevent submission while preserving editor focus and draft entry. */
@@ -197,6 +209,8 @@ export interface PromptComposerProps {
197
209
  voiceEnabled?: boolean;
198
210
  /** Let an empty draft turn Send into the voice-chat button (default: true). Dictation is unaffected. */
199
211
  voiceChatEnabled?: boolean;
212
+ /** Starts voice chat from the empty Send slot in place of the provider's default start. */
213
+ onVoiceChatStart?: () => void;
200
214
  /** Show file upload controls and pass submitted files to onSubmit (default: true). */
201
215
  attachmentsEnabled?: boolean;
202
216
  /** Opens host-owned storage setup when the user chooses an upload action. */
@@ -362,8 +376,19 @@ class BinaryDocumentAttachmentAdapter implements AttachmentAdapter {
362
376
 
363
377
  class RasterImageAttachmentAdapter extends SimpleImageAttachmentAdapter {
364
378
  public override accept = IMAGE_ATTACHMENT_ACCEPT;
379
+ private readonly translate: ComposerTranslate;
380
+
381
+ constructor(translate: ComposerTranslate) {
382
+ super();
383
+ this.translate = translate;
384
+ }
365
385
 
366
386
  public override async add(state: { file: File }): Promise<PendingAttachment> {
387
+ const limitError = await getUnshrinkableImageError(
388
+ state.file,
389
+ this.translate,
390
+ );
391
+ if (limitError) throw new Error(limitError);
367
392
  return { ...(await super.add(state)), id: crypto.randomUUID() };
368
393
  }
369
394
  }
@@ -374,9 +399,11 @@ class ChatDocumentAttachmentAdapter extends BinaryDocumentAttachmentAdapter {
374
399
  public override accept = CHAT_DOCUMENT_ATTACHMENT_ACCEPT;
375
400
  }
376
401
 
377
- export function createChatAttachmentAdapter(): AttachmentAdapter {
402
+ export function createChatAttachmentAdapter(
403
+ translate: ComposerTranslate = fallbackTranslate,
404
+ ): AttachmentAdapter {
378
405
  return new CompositeAttachmentAdapter([
379
- new RasterImageAttachmentAdapter(),
406
+ new RasterImageAttachmentAdapter(translate),
380
407
  new ChatDocumentAttachmentAdapter(),
381
408
  new TextAttachmentAdapter(),
382
409
  ]);
@@ -576,7 +603,28 @@ function ImagePreviewLightbox({
576
603
  );
577
604
  }
578
605
 
579
- function AttachmentChip({
606
+ function useAttachmentReadError(
607
+ attachment: Attachment,
608
+ ): AgentPromptAttachmentReadError | null {
609
+ const file = "file" in attachment ? attachment.file : undefined;
610
+ const [readError, setReadError] =
611
+ useState<AgentPromptAttachmentReadError | null>(null);
612
+ useEffect(() => {
613
+ setReadError(null);
614
+ if (!file) return;
615
+ let active = true;
616
+ void probeAgentPromptAttachment(file).then((error) => {
617
+ if (active) setReadError(error);
618
+ });
619
+ return () => {
620
+ active = false;
621
+ };
622
+ }, [file]);
623
+ return readError;
624
+ }
625
+
626
+ /** @internal exported for unit tests only */
627
+ export function AttachmentChip({
580
628
  attachment,
581
629
  onRemove,
582
630
  }: {
@@ -587,8 +635,48 @@ function AttachmentChip({
587
635
  const isImage =
588
636
  getAttachmentImageFile(attachment) !== null ||
589
637
  getAttachmentContentImageSrc(attachment) !== null;
638
+ const readError = useAttachmentReadError(attachment);
590
639
  const t = useComposerRuntimeAdapters().translate!;
591
640
 
641
+ if (readError) {
642
+ const message =
643
+ readError === "too-large"
644
+ ? t("agentChat.composer.attachmentTooLarge", {
645
+ defaultValue: "File is over 8 MB",
646
+ })
647
+ : readError === "not-utf8"
648
+ ? t("agentChat.composer.attachmentNotUtf8", {
649
+ defaultValue: "File isn't UTF-8 text",
650
+ })
651
+ : t("agentChat.composer.attachmentReadFailed", {
652
+ defaultValue: "Couldn't read this file",
653
+ });
654
+ return (
655
+ <div
656
+ role="status"
657
+ title={message}
658
+ className="agent-composer-attachment-chip group relative inline-flex max-w-[240px] items-center gap-2 rounded-md border border-amber-500/40 bg-amber-500/10 px-2 py-1.5 text-xs"
659
+ >
660
+ <IconAlertTriangle className="h-4 w-4 shrink-0 text-amber-500" />
661
+ <span className="flex min-w-0 flex-col">
662
+ <span className="truncate font-medium">{attachment.name}</span>
663
+ <span className="truncate text-muted-foreground">{message}</span>
664
+ </span>
665
+ <button
666
+ type="button"
667
+ onClick={() => onRemove(attachment.id)}
668
+ aria-label={t("agentChat.composer.removeAttachment", {
669
+ name: attachment.name,
670
+ defaultValue: `Remove ${attachment.name}`,
671
+ })}
672
+ className="flex h-5 w-5 shrink-0 cursor-pointer items-center justify-center rounded text-muted-foreground hover:text-foreground"
673
+ >
674
+ <IconX className="h-3 w-3" />
675
+ </button>
676
+ </div>
677
+ );
678
+ }
679
+
592
680
  if (isPastedTextAttachmentName(attachment.name)) {
593
681
  return <PastedTextChip attachment={attachment} onRemove={onRemove} />;
594
682
  }
@@ -704,6 +792,7 @@ function PromptComposerInner({
704
792
  submissionDisabled,
705
793
  showMissingApiKeySetup = true,
706
794
  setupCardVisible: hostSetupCardVisible,
795
+ onModelConnectionRequiredChange,
707
796
  sendButtonDisabled,
708
797
  submitting,
709
798
  willQueue = false,
@@ -724,6 +813,7 @@ function PromptComposerInner({
724
813
  showAutoModelOption = true,
725
814
  voiceEnabled = DEFAULT_VOICE_DICTATION_ENABLED,
726
815
  voiceChatEnabled = true,
816
+ onVoiceChatStart,
727
817
  attachmentsEnabled = true,
728
818
  onAttachmentRequest,
729
819
  contextButtonTooltipDisabled = false,
@@ -886,7 +976,21 @@ function PromptComposerInner({
886
976
  const engineState = engineStatusChecksEnabled
887
977
  ? agentEngineConfigured.state
888
978
  : "configured";
889
- const missingApiKey = engineStatusChecksEnabled && engineState === "missing";
979
+ const [modelListNeedsConnection, setModelListNeedsConnection] =
980
+ useState(false);
981
+ const handleModelConnectionRequiredChange = useCallback(
982
+ (required: boolean) => {
983
+ setModelListNeedsConnection(required);
984
+ onModelConnectionRequiredChange?.(required);
985
+ },
986
+ [onModelConnectionRequiredChange],
987
+ );
988
+ // The model list knows nothing is connected well before the readiness
989
+ // check answers; an answer of "configured" still wins.
990
+ const missingApiKey =
991
+ engineStatusChecksEnabled &&
992
+ (engineState === "missing" ||
993
+ (engineState !== "configured" && modelListNeedsConnection));
890
994
  const [setupBouncePulse, setSetupBouncePulse] = useState(0);
891
995
  const [providerCheckPending, setProviderCheckPending] = useState(false);
892
996
  const heldProviderDraftRef = useRef<ComposerDraftSnapshot | null>(null);
@@ -897,13 +1001,16 @@ function PromptComposerInner({
897
1001
  }
898
1002
  }, []);
899
1003
  const engineSubmissionBlocked = shouldGateComposerForEngine(engineState);
900
- // A missing provider leaves the draft editable; the send gate below holds
901
- // the draft and points at the setup card instead.
902
- const composerDisabled = Boolean(disabled);
903
1004
  const ownSetupCardVisible = Boolean(
904
1005
  missingApiKey && showMissingApiKeySetup && BuilderSetupCard,
905
1006
  );
906
1007
  const setupCardVisible = hostSetupCardVisible ?? ownSetupCardVisible;
1008
+ // With nothing connected the prompt is closed to typing and a click on it
1009
+ // points at the setup card; without a card on screen it stays open, since a
1010
+ // closed prompt with nothing to explain it is a dead end.
1011
+ const composerDisabled = Boolean(
1012
+ disabled || (missingApiKey && setupCardVisible),
1013
+ );
907
1014
  const handleDisabledComposerClick = useCallback(() => {
908
1015
  if (missingApiKey) setSetupBouncePulse((pulse) => pulse + 1);
909
1016
  onDisabledClick?.();
@@ -1043,7 +1150,9 @@ function PromptComposerInner({
1043
1150
  <AgentComposerFrame
1044
1151
  className={cn(
1045
1152
  "text-start",
1046
- (composerDisabled || missingApiKey) &&
1153
+ (composerDisabled ||
1154
+ (engineStatusChecksEnabled && engineState === "missing") ||
1155
+ (missingApiKey && setupCardVisible)) &&
1047
1156
  "agent-composer-area--attached-above",
1048
1157
  className,
1049
1158
  )}
@@ -1138,6 +1247,7 @@ function PromptComposerInner({
1138
1247
  onSlashCommand={onSlashCommand}
1139
1248
  voiceEnabled={voiceEnabled}
1140
1249
  voiceChatEnabled={voiceChatEnabled}
1250
+ onVoiceChatStart={onVoiceChatStart}
1141
1251
  onTextChange={onTextChange}
1142
1252
  mentionItems={mentionItems}
1143
1253
  mentionItemsError={mentionItemsError}
@@ -1170,6 +1280,7 @@ function PromptComposerInner({
1170
1280
  onConnectLocalRuntime={onConnectLocalRuntime}
1171
1281
  imageModelMenu={imageModelMenu}
1172
1282
  setupCardVisible={setupCardVisible}
1283
+ onModelConnectionRequiredChange={handleModelConnectionRequiredChange}
1173
1284
  />
1174
1285
  </AgentComposerFrame>
1175
1286
  </>
@@ -1188,17 +1299,17 @@ function PromptComposerInner({
1188
1299
  * without needing the outer chat to be mounted.
1189
1300
  */
1190
1301
  function PromptComposerRuntime(props: PromptComposerProps) {
1191
- const StaleIndexBoundary =
1192
- useComposerRuntimeAdapters().agentChat!.StaleIndexBoundary!;
1302
+ const adapters = useComposerRuntimeAdapters();
1303
+ const StaleIndexBoundary = adapters.agentChat.StaleIndexBoundary;
1193
1304
  const attachmentAdapter = useMemo(
1194
1305
  () =>
1195
1306
  props.attachmentAdapter ??
1196
1307
  new CompositeAttachmentAdapter([
1197
- new RasterImageAttachmentAdapter(),
1308
+ new RasterImageAttachmentAdapter(adapters.translate),
1198
1309
  new BinaryDocumentAttachmentAdapter(),
1199
1310
  new TextAttachmentAdapter(),
1200
1311
  ]),
1201
- [props.attachmentAdapter],
1312
+ [props.attachmentAdapter, adapters.translate],
1202
1313
  );
1203
1314
  const runtime = useLocalRuntime(NOOP_ADAPTER, {
1204
1315
  adapters: { attachments: attachmentAdapter },
@@ -68,6 +68,12 @@ const copy: RealtimeVoiceModeCopy = {
68
68
  error: "Voice mode needs attention",
69
69
  ending: "Ending voice mode",
70
70
  },
71
+ thinking: "Thinking",
72
+ approval: {
73
+ question: (toolLabel) => `Approve to run ${toolLabel}?`,
74
+ approve: "Approve",
75
+ deny: "Deny",
76
+ },
71
77
  errors: {
72
78
  unsupported: "Unsupported",
73
79
  responseFailed: "Response failed",
@@ -93,6 +99,9 @@ function voiceSession(
93
99
  start: vi.fn(async () => {}),
94
100
  end: vi.fn(),
95
101
  dismissError: vi.fn(),
102
+ currentToolName: null,
103
+ approvals: [],
104
+ resolveApproval: vi.fn(async () => {}),
96
105
  ...overrides,
97
106
  } as unknown as RealtimeVoiceModeApi;
98
107
  }
@@ -171,6 +180,45 @@ describe("RealtimeVoiceComposerControls", () => {
171
180
  expect(new Set(bars())).toEqual(new Set(["scaleY(0.4)"]));
172
181
  });
173
182
 
183
+ it("names the step it is working on, and says it is thinking between steps", () => {
184
+ const status = () =>
185
+ slot("voice-chat-status")?.querySelector('[role="status"]')?.textContent;
186
+ renderControls(voiceSession({ state: "working" }));
187
+ expect(status()).toBe("Thinking");
188
+
189
+ renderControls(
190
+ voiceSession({ state: "working", currentToolName: "patch-deck" }),
191
+ );
192
+ expect(status()).toBe("Patch deck");
193
+ });
194
+
195
+ it("asks to approve a waiting tool call in place of the status", () => {
196
+ const voice = voiceSession({
197
+ state: "working",
198
+ approvals: [{ callId: "call-delete", toolName: "delete-slide" }],
199
+ });
200
+ renderControls(voice);
201
+
202
+ expect(slot("voice-chat-status")).toBeNull();
203
+ const approval = slot("voice-chat-approval")!;
204
+ expect(approval.querySelector('[role="status"]')?.textContent).toBe(
205
+ "Approve to run Delete slide?",
206
+ );
207
+ const [approve, deny] = Array.from(approval.querySelectorAll("button"));
208
+ act(() => approve!.click());
209
+ act(() => deny!.click());
210
+ expect(voice.resolveApproval).toHaveBeenNthCalledWith(
211
+ 1,
212
+ "call-delete",
213
+ true,
214
+ );
215
+ expect(voice.resolveApproval).toHaveBeenNthCalledWith(
216
+ 2,
217
+ "call-delete",
218
+ false,
219
+ );
220
+ });
221
+
174
222
  it("toggles speaker and microphone mute with pressed states", () => {
175
223
  const voice = voiceSession();
176
224
  renderControls(voice);
@@ -7,7 +7,9 @@ import {
7
7
  IconX,
8
8
  } from "@tabler/icons-react";
9
9
 
10
+ import { Button } from "../ui/button.js";
10
11
  import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip.js";
12
+ import { realtimeVoiceToolLabel } from "./realtime-voice-transcript.js";
11
13
  import type { RealtimeVoiceModeApi } from "./useRealtimeVoiceMode.js";
12
14
  import { VoiceLevelWaveform } from "./voice-waveform.js";
13
15
 
@@ -55,6 +57,13 @@ export interface RealtimeVoiceModeCopy {
55
57
  >;
56
58
  };
57
59
  status: Record<RealtimeVoiceModeState, string>;
60
+ /** Shown while working with no tool running: the model is deciding. */
61
+ thinking: string;
62
+ approval: {
63
+ question: (toolLabel: string) => string;
64
+ approve: string;
65
+ deny: string;
66
+ };
58
67
  errors: {
59
68
  unsupported: string;
60
69
  responseFailed: string;
@@ -115,9 +124,39 @@ export function RealtimeVoiceComposerControls({
115
124
  ? copy.unmuteMicrophone
116
125
  : copy.muteMicrophone;
117
126
 
127
+ const approval = state === "ending" ? undefined : voice.approvals[0];
128
+
118
129
  return (
119
130
  <>
120
- {state !== "error" ? (
131
+ {approval ? (
132
+ <div
133
+ data-agent-composer-slot="voice-chat-approval"
134
+ className="flex min-w-0 shrink items-center gap-1"
135
+ >
136
+ <span
137
+ role="status"
138
+ aria-live="polite"
139
+ className="min-w-0 truncate ps-1 agent-kit-caption-copy text-foreground"
140
+ >
141
+ {copy.approval.question(realtimeVoiceToolLabel(approval.toolName))}
142
+ </span>
143
+ <Button
144
+ type="button"
145
+ size="xs"
146
+ onClick={() => void voice.resolveApproval(approval.callId, true)}
147
+ >
148
+ {copy.approval.approve}
149
+ </Button>
150
+ <Button
151
+ type="button"
152
+ size="xs"
153
+ variant="ghost"
154
+ onClick={() => void voice.resolveApproval(approval.callId, false)}
155
+ >
156
+ {copy.approval.deny}
157
+ </Button>
158
+ </div>
159
+ ) : state !== "error" ? (
121
160
  <div
122
161
  data-agent-composer-slot="voice-chat-status"
123
162
  data-voice-speaker={
@@ -157,7 +196,11 @@ export function RealtimeVoiceComposerControls({
157
196
  aria-live="polite"
158
197
  className="min-w-0 truncate agent-kit-caption-copy text-muted-foreground"
159
198
  >
160
- {copy.status[state]}
199
+ {state === "working"
200
+ ? voice.currentToolName
201
+ ? realtimeVoiceToolLabel(voice.currentToolName)
202
+ : copy.thinking
203
+ : copy.status[state]}
161
204
  </span>
162
205
  </div>
163
206
  ) : null}