@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.
- package/agent-native.eject.json +16 -1
- package/dist/app/agentkit/react/components.d.ts +5 -3
- package/dist/app/agentkit/react/components.d.ts.map +1 -1
- package/dist/app/agentkit/react/components.js +103 -42
- package/dist/app/agentkit/react/components.js.map +1 -1
- package/dist/app/agentkit/react/styles.css +93 -65
- package/dist/app/agentkit/react/voice-transcripts.d.ts +22 -1
- package/dist/app/agentkit/react/voice-transcripts.d.ts.map +1 -1
- package/dist/app/agentkit/react/voice-transcripts.js +209 -8
- package/dist/app/agentkit/react/voice-transcripts.js.map +1 -1
- package/dist/app/chat/AgentKitAssistantChat.d.ts.map +1 -1
- package/dist/app/chat/AgentKitAssistantChat.js +55 -40
- package/dist/app/chat/AgentKitAssistantChat.js.map +1 -1
- package/dist/app/chat/agentkit-chat/guided-questions.d.ts +1 -0
- package/dist/app/chat/agentkit-chat/guided-questions.d.ts.map +1 -1
- package/dist/app/chat/agentkit-chat/guided-questions.js +18 -6
- package/dist/app/chat/agentkit-chat/guided-questions.js.map +1 -1
- package/dist/app/chat/agentkit-chat/index.d.ts +1 -1
- package/dist/app/chat/agentkit-chat/index.d.ts.map +1 -1
- package/dist/app/chat/agentkit-chat/index.js +1 -1
- package/dist/app/chat/agentkit-chat/index.js.map +1 -1
- package/dist/app/chat/chat/run-recovery.d.ts.map +1 -1
- package/dist/app/chat/chat/run-recovery.js +5 -1
- package/dist/app/chat/chat/run-recovery.js.map +1 -1
- package/dist/app/chat/composer/runtime-adapters.d.ts.map +1 -1
- package/dist/app/chat/composer/runtime-adapters.js +2 -0
- package/dist/app/chat/composer/runtime-adapters.js.map +1 -1
- package/dist/app/chat/reply-connection-suggestion.d.ts +29 -0
- package/dist/app/chat/reply-connection-suggestion.d.ts.map +1 -0
- package/dist/app/chat/reply-connection-suggestion.js +42 -0
- package/dist/app/chat/reply-connection-suggestion.js.map +1 -0
- package/dist/app/i18n/catalogs/ar-SA.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/ar-SA.js +7 -0
- package/dist/app/i18n/catalogs/ar-SA.js.map +1 -1
- package/dist/app/i18n/catalogs/de-DE.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/de-DE.js +7 -0
- package/dist/app/i18n/catalogs/de-DE.js.map +1 -1
- package/dist/app/i18n/catalogs/en-US.d.ts +7 -0
- package/dist/app/i18n/catalogs/en-US.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/en-US.js +7 -0
- package/dist/app/i18n/catalogs/en-US.js.map +1 -1
- package/dist/app/i18n/catalogs/es-ES.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/es-ES.js +7 -0
- package/dist/app/i18n/catalogs/es-ES.js.map +1 -1
- package/dist/app/i18n/catalogs/fr-FR.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/fr-FR.js +7 -0
- package/dist/app/i18n/catalogs/fr-FR.js.map +1 -1
- package/dist/app/i18n/catalogs/hi-IN.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/hi-IN.js +7 -0
- package/dist/app/i18n/catalogs/hi-IN.js.map +1 -1
- package/dist/app/i18n/catalogs/ja-JP.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/ja-JP.js +7 -0
- package/dist/app/i18n/catalogs/ja-JP.js.map +1 -1
- package/dist/app/i18n/catalogs/ko-KR.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/ko-KR.js +7 -0
- package/dist/app/i18n/catalogs/ko-KR.js.map +1 -1
- package/dist/app/i18n/catalogs/pt-BR.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/pt-BR.js +7 -0
- package/dist/app/i18n/catalogs/pt-BR.js.map +1 -1
- package/dist/app/i18n/catalogs/zh-CN.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/zh-CN.js +7 -0
- package/dist/app/i18n/catalogs/zh-CN.js.map +1 -1
- package/dist/app/i18n/catalogs/zh-TW.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/zh-TW.js +7 -0
- package/dist/app/i18n/catalogs/zh-TW.js.map +1 -1
- package/dist/app/mcp-approval/McpApprovalReview.d.ts +25 -0
- package/dist/app/mcp-approval/McpApprovalReview.d.ts.map +1 -0
- package/dist/app/mcp-approval/McpApprovalReview.js +342 -0
- package/dist/app/mcp-approval/McpApprovalReview.js.map +1 -0
- package/dist/app/onboarding/FirstRunOnboarding.d.ts.map +1 -1
- package/dist/app/onboarding/FirstRunOnboarding.js +299 -71
- package/dist/app/onboarding/FirstRunOnboarding.js.map +1 -1
- package/dist/app/onboarding/GettingStartedCard.js +1 -1
- package/dist/app/onboarding/GettingStartedCard.js.map +1 -1
- package/dist/app/onboarding/first-run-builder-credits-flag.d.ts +2 -0
- package/dist/app/onboarding/first-run-builder-credits-flag.d.ts.map +1 -0
- package/dist/app/onboarding/first-run-builder-credits-flag.js +8 -0
- package/dist/app/onboarding/first-run-builder-credits-flag.js.map +1 -0
- package/dist/app/providers/AppProviders.d.ts.map +1 -1
- package/dist/app/providers/AppProviders.js +14 -1
- package/dist/app/providers/AppProviders.js.map +1 -1
- package/dist/app/resources/McpConnectionSuggestion.d.ts +2 -4
- package/dist/app/resources/McpConnectionSuggestion.d.ts.map +1 -1
- package/dist/app/resources/McpConnectionSuggestion.js +59 -36
- package/dist/app/resources/McpConnectionSuggestion.js.map +1 -1
- package/dist/app/settings/account/PreferencesSettings.d.ts.map +1 -1
- package/dist/app/settings/account/PreferencesSettings.js +14 -1
- package/dist/app/settings/account/PreferencesSettings.js.map +1 -1
- package/dist/app/settings/account/ThemeFamilySettings.js +2 -2
- package/dist/app/settings/account/ThemeFamilySettings.js.map +1 -1
- package/dist/app/settings/account/search-entries.d.ts +1 -0
- package/dist/app/settings/account/search-entries.d.ts.map +1 -1
- package/dist/app/settings/account/search-entries.js +5 -0
- package/dist/app/settings/account/search-entries.js.map +1 -1
- package/dist/app/settings/shell/SettingsShell.d.ts.map +1 -1
- package/dist/app/settings/shell/SettingsShell.js +26 -2
- package/dist/app/settings/shell/SettingsShell.js.map +1 -1
- package/dist/app/shared/AppShellSkeleton.d.ts.map +1 -1
- package/dist/app/shared/AppShellSkeleton.js +4 -10
- package/dist/app/shared/AppShellSkeleton.js.map +1 -1
- package/dist/app/shared/EnvironmentBadge.d.ts +2 -1
- package/dist/app/shared/EnvironmentBadge.d.ts.map +1 -1
- package/dist/app/shared/EnvironmentBadge.js +2 -3
- package/dist/app/shared/EnvironmentBadge.js.map +1 -1
- package/dist/app/shared/is-builder-io-employee.d.ts +8 -0
- package/dist/app/shared/is-builder-io-employee.d.ts.map +1 -0
- package/dist/app/shared/is-builder-io-employee.js +10 -0
- package/dist/app/shared/is-builder-io-employee.js.map +1 -0
- package/dist/app/styles/agent-conversation.css +30 -17
- package/dist/app/styles/theme-families.css +22 -5
- package/dist/app/styles/tokens/agent-kit.css +3 -3
- package/dist/composer/PromptComposer.d.ts +15 -3
- package/dist/composer/PromptComposer.d.ts.map +1 -1
- package/dist/composer/PromptComposer.js +74 -16
- package/dist/composer/PromptComposer.js.map +1 -1
- package/dist/composer/RealtimeVoiceMode.d.ts +7 -0
- package/dist/composer/RealtimeVoiceMode.d.ts.map +1 -1
- package/dist/composer/RealtimeVoiceMode.js +9 -2
- package/dist/composer/RealtimeVoiceMode.js.map +1 -1
- package/dist/composer/TiptapComposer.d.ts +7 -0
- package/dist/composer/TiptapComposer.d.ts.map +1 -1
- package/dist/composer/TiptapComposer.js +12 -4
- package/dist/composer/TiptapComposer.js.map +1 -1
- package/dist/composer/VoiceButton.d.ts +2 -0
- package/dist/composer/VoiceButton.d.ts.map +1 -1
- package/dist/composer/VoiceButton.js +5 -4
- package/dist/composer/VoiceButton.js.map +1 -1
- package/dist/composer/index.d.ts +2 -2
- package/dist/composer/index.d.ts.map +1 -1
- package/dist/composer/index.js +1 -1
- package/dist/composer/index.js.map +1 -1
- package/dist/composer/prompt-attachments.d.ts +20 -0
- package/dist/composer/prompt-attachments.d.ts.map +1 -1
- package/dist/composer/prompt-attachments.js +188 -11
- package/dist/composer/prompt-attachments.js.map +1 -1
- package/dist/composer/realtime-voice-transcript.d.ts +27 -0
- package/dist/composer/realtime-voice-transcript.d.ts.map +1 -1
- package/dist/composer/realtime-voice-transcript.js +34 -1
- package/dist/composer/realtime-voice-transcript.js.map +1 -1
- package/dist/composer/realtime-voice-turn-metrics.d.ts +43 -0
- package/dist/composer/realtime-voice-turn-metrics.d.ts.map +1 -0
- package/dist/composer/realtime-voice-turn-metrics.js +132 -0
- package/dist/composer/realtime-voice-turn-metrics.js.map +1 -0
- package/dist/composer/runtime-adapters.d.ts +4 -1
- package/dist/composer/runtime-adapters.d.ts.map +1 -1
- package/dist/composer/runtime-adapters.js +2 -1
- package/dist/composer/runtime-adapters.js.map +1 -1
- package/dist/composer/useRealtimeVoiceMode.d.ts +61 -9
- package/dist/composer/useRealtimeVoiceMode.d.ts.map +1 -1
- package/dist/composer/useRealtimeVoiceMode.js +536 -99
- package/dist/composer/useRealtimeVoiceMode.js.map +1 -1
- package/dist/composer/voice-chat-icon.d.ts +7 -0
- package/dist/composer/voice-chat-icon.d.ts.map +1 -0
- package/dist/composer/voice-chat-icon.js +14 -0
- package/dist/composer/voice-chat-icon.js.map +1 -0
- package/dist/editor/DragHandle.d.ts +12 -1
- package/dist/editor/DragHandle.d.ts.map +1 -1
- package/dist/editor/DragHandle.js +165 -41
- package/dist/editor/DragHandle.js.map +1 -1
- package/dist/editor/RegistryBlockNode.d.ts +20 -0
- package/dist/editor/RegistryBlockNode.d.ts.map +1 -1
- package/dist/editor/RegistryBlockNode.js +58 -13
- package/dist/editor/RegistryBlockNode.js.map +1 -1
- package/dist/editor/index.d.ts +2 -2
- package/dist/editor/index.d.ts.map +1 -1
- package/dist/editor/index.js +1 -1
- package/dist/editor/index.js.map +1 -1
- package/dist/styles.css +2 -0
- package/dist/ui/dropdown-menu.d.ts.map +1 -1
- package/dist/ui/dropdown-menu.js +5 -1
- package/dist/ui/dropdown-menu.js.map +1 -1
- package/package.json +14 -4
- package/src/app/agentkit/react/components.regenerate-readiness.spec.tsx +3 -1
- package/src/app/agentkit/react/components.spec.tsx +24 -4
- package/src/app/agentkit/react/components.tsx +174 -44
- package/src/app/agentkit/react/composer-context.spec.tsx +3 -0
- package/src/app/agentkit/react/disclosures.spec.tsx +124 -0
- package/src/app/agentkit/react/lifecycle.spec.tsx +8 -2
- package/src/app/agentkit/react/message-actions.spec.tsx +161 -5
- package/src/app/agentkit/react/saved-reply-reload.spec.tsx +206 -0
- package/src/app/agentkit/react/styles.css +93 -65
- package/src/app/agentkit/react/voice-transcripts.spec.tsx +239 -4
- package/src/app/agentkit/react/voice-transcripts.ts +245 -8
- package/src/app/chat/AgentKitAssistantChat.spec.tsx +59 -11
- package/src/app/chat/AgentKitAssistantChat.tsx +74 -62
- package/src/app/chat/agentkit-chat/guided-answers-context.spec.ts +9 -0
- package/src/app/chat/agentkit-chat/guided-questions.tsx +17 -7
- package/src/app/chat/agentkit-chat/index.ts +1 -0
- package/src/app/chat/chat/run-recovery.spec.tsx +45 -0
- package/src/app/chat/chat/run-recovery.tsx +6 -1
- package/src/app/chat/composer/runtime-adapters.tsx +2 -0
- package/src/app/chat/reply-connection-suggestion.spec.ts +121 -0
- package/src/app/chat/reply-connection-suggestion.ts +76 -0
- package/src/app/i18n/catalogs/ar-SA.ts +10 -0
- package/src/app/i18n/catalogs/de-DE.ts +10 -0
- package/src/app/i18n/catalogs/en-US.ts +10 -0
- package/src/app/i18n/catalogs/es-ES.ts +10 -0
- package/src/app/i18n/catalogs/fr-FR.ts +10 -0
- package/src/app/i18n/catalogs/hi-IN.ts +10 -0
- package/src/app/i18n/catalogs/ja-JP.ts +10 -0
- package/src/app/i18n/catalogs/ko-KR.ts +10 -0
- package/src/app/i18n/catalogs/pt-BR.ts +10 -0
- package/src/app/i18n/catalogs/zh-CN.ts +10 -0
- package/src/app/i18n/catalogs/zh-TW.ts +10 -0
- package/src/app/mcp-approval/McpApprovalReview.spec.tsx +687 -0
- package/src/app/mcp-approval/McpApprovalReview.tsx +913 -0
- package/src/app/onboarding/FirstRunOnboarding.spec.tsx +736 -26
- package/src/app/onboarding/FirstRunOnboarding.tsx +473 -145
- package/src/app/onboarding/GettingStartedCard.tsx +1 -1
- package/src/app/onboarding/first-run-builder-credits-flag.spec.ts +13 -0
- package/src/app/onboarding/first-run-builder-credits-flag.ts +9 -0
- package/src/app/providers/AppProviders.spec.tsx +109 -0
- package/src/app/providers/AppProviders.tsx +53 -1
- package/src/app/resources/McpConnectionSuggestion.spec.ts +77 -12
- package/src/app/resources/McpConnectionSuggestion.tsx +69 -51
- package/src/app/settings/VoiceChatSettings.spec.tsx +6 -1
- package/src/app/settings/account/PreferencesSettings.spec.tsx +73 -0
- package/src/app/settings/account/PreferencesSettings.tsx +24 -7
- package/src/app/settings/account/ThemeFamilySettings.tsx +2 -2
- package/src/app/settings/account/search-entries.spec.ts +14 -0
- package/src/app/settings/account/search-entries.ts +9 -0
- package/src/app/settings/shell/SettingsShell.tsx +29 -2
- package/src/app/shared/AppShellSkeleton.spec.tsx +2 -2
- package/src/app/shared/AppShellSkeleton.tsx +3 -16
- package/src/app/shared/EnvironmentBadge.tsx +3 -4
- package/src/app/shared/is-builder-io-employee.ts +17 -0
- package/src/app/styles/agent-conversation.css +30 -17
- package/src/app/styles/theme-families.css +22 -5
- package/src/app/styles/theme-families.spec.ts +73 -0
- package/src/app/styles/tokens/agent-kit.css +3 -3
- package/src/composer/AgentSuggestionBar.overflow.spec.tsx +1 -1
- package/src/composer/AttachmentChip.spec.tsx +87 -0
- package/src/composer/PromptComposer.context.spec.tsx +8 -4
- package/src/composer/PromptComposer.setup-card.spec.tsx +170 -0
- package/src/composer/PromptComposer.tsx +125 -14
- package/src/composer/RealtimeVoiceMode.spec.tsx +48 -0
- package/src/composer/RealtimeVoiceMode.tsx +45 -2
- package/src/composer/TiptapComposer.tsx +25 -0
- package/src/composer/TiptapComposer.voice-toolbar.spec.tsx +4 -1
- package/src/composer/VoiceButton.tsx +8 -3
- package/src/composer/index.ts +4 -0
- package/src/composer/prompt-attachments.spec.ts +160 -8
- package/src/composer/prompt-attachments.ts +221 -12
- package/src/composer/realtime-voice-transcript.spec.ts +36 -0
- package/src/composer/realtime-voice-transcript.ts +56 -1
- package/src/composer/realtime-voice-turn-metrics.spec.ts +175 -0
- package/src/composer/realtime-voice-turn-metrics.ts +192 -0
- package/src/composer/runtime-adapters.tsx +5 -1
- package/src/composer/useRealtimeVoiceMode.session.spec.tsx +487 -2
- package/src/composer/useRealtimeVoiceMode.spec.ts +267 -65
- package/src/composer/useRealtimeVoiceMode.tsx +716 -139
- package/src/composer/voice-chat-icon.tsx +19 -0
- package/src/editor/DragHandle.spec.ts +217 -0
- package/src/editor/DragHandle.ts +214 -55
- package/src/editor/RegistryBlockNode.spec.ts +172 -2
- package/src/editor/RegistryBlockNode.tsx +91 -13
- package/src/editor/index.ts +4 -0
- package/src/styles.css +2 -0
- package/src/ui/dropdown-menu.tsx +4 -0
|
@@ -12,6 +12,7 @@ import { afterEach, describe, expect, it, vi } from "vitest";
|
|
|
12
12
|
import {
|
|
13
13
|
realtimeVoiceTranscriptRegistry,
|
|
14
14
|
type RealtimeVoiceTranscriptMessage,
|
|
15
|
+
type RealtimeVoiceWorkUpdate,
|
|
15
16
|
} from "../../../composer/realtime-voice-transcript.js";
|
|
16
17
|
import { ComposerRuntimeAdaptersProvider } from "../../../composer/runtime-adapters.js";
|
|
17
18
|
import { AgentKitChat } from "./components.js";
|
|
@@ -25,8 +26,16 @@ afterEach(async () => {
|
|
|
25
26
|
vi.unstubAllGlobals();
|
|
26
27
|
});
|
|
27
28
|
|
|
28
|
-
async function mountChat(
|
|
29
|
+
async function mountChat(
|
|
30
|
+
threadId: string,
|
|
31
|
+
stored: Partial<AgentThreadSnapshot> = {},
|
|
32
|
+
{ loadBeforeMount = true } = {},
|
|
33
|
+
) {
|
|
29
34
|
const saved: AgentThreadSnapshot[] = [];
|
|
35
|
+
let releaseSnapshot = () => {};
|
|
36
|
+
const snapshotReleased = loadBeforeMount
|
|
37
|
+
? Promise.resolve()
|
|
38
|
+
: new Promise<void>((resolve) => (releaseSnapshot = resolve));
|
|
30
39
|
const transport: AgentTransport = {
|
|
31
40
|
async startRun() {
|
|
32
41
|
return { runId: "unused" };
|
|
@@ -34,7 +43,14 @@ async function mountChat(threadId: string) {
|
|
|
34
43
|
async *subscribeToRun() {},
|
|
35
44
|
async cancelRun() {},
|
|
36
45
|
async getThreadSnapshot() {
|
|
37
|
-
|
|
46
|
+
await snapshotReleased;
|
|
47
|
+
return {
|
|
48
|
+
id: threadId,
|
|
49
|
+
createdAt: AT,
|
|
50
|
+
updatedAt: AT,
|
|
51
|
+
messages: [],
|
|
52
|
+
...stored,
|
|
53
|
+
};
|
|
38
54
|
},
|
|
39
55
|
async persistThreadSnapshot({ snapshot }) {
|
|
40
56
|
saved.push(snapshot);
|
|
@@ -45,7 +61,8 @@ async function mountChat(threadId: string) {
|
|
|
45
61
|
aiSetupReadiness: "not-applicable",
|
|
46
62
|
reconnect: { attempts: 0, delayMs: 0 },
|
|
47
63
|
});
|
|
48
|
-
|
|
64
|
+
const loaded = client.loadThread(threadId);
|
|
65
|
+
if (loadBeforeMount) await loaded;
|
|
49
66
|
vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
|
|
50
67
|
const container = document.createElement("div");
|
|
51
68
|
document.body.appendChild(container);
|
|
@@ -65,7 +82,14 @@ async function mountChat(threadId: string) {
|
|
|
65
82
|
await client.shutdown();
|
|
66
83
|
container.remove();
|
|
67
84
|
});
|
|
68
|
-
|
|
85
|
+
const finishLoading = async () => {
|
|
86
|
+
await act(async () => {
|
|
87
|
+
releaseSnapshot();
|
|
88
|
+
await loaded;
|
|
89
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
90
|
+
});
|
|
91
|
+
};
|
|
92
|
+
return { container, client, saved, finishLoading };
|
|
69
93
|
}
|
|
70
94
|
|
|
71
95
|
async function publish(
|
|
@@ -193,4 +217,215 @@ describe("voice chat transcripts in AgentKitChat", () => {
|
|
|
193
217
|
|
|
194
218
|
expect(messages(container)).toEqual([]);
|
|
195
219
|
});
|
|
220
|
+
|
|
221
|
+
it("waits for running work to finish before saving a final turn", async () => {
|
|
222
|
+
const threadId = "thread-voice-open-work";
|
|
223
|
+
const { saved } = await mountChat(threadId);
|
|
224
|
+
const call = {
|
|
225
|
+
id: "call-edit",
|
|
226
|
+
name: "edit-design",
|
|
227
|
+
startedAt: "2026-10-24T00:00:02.000Z",
|
|
228
|
+
};
|
|
229
|
+
const publishWork = async (update: RealtimeVoiceWorkUpdate) => {
|
|
230
|
+
await act(async () => {
|
|
231
|
+
realtimeVoiceTranscriptRegistry.publishWork(update);
|
|
232
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
233
|
+
});
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
await publishWork({
|
|
237
|
+
id: "call-edit",
|
|
238
|
+
threadId,
|
|
239
|
+
calls: [{ ...call, status: "running" }],
|
|
240
|
+
});
|
|
241
|
+
await publish(threadId, {
|
|
242
|
+
id: `${threadId}:user-2`,
|
|
243
|
+
role: "user",
|
|
244
|
+
text: "Make it blue too.",
|
|
245
|
+
createdAt: "2026-10-24T00:00:03.000Z",
|
|
246
|
+
});
|
|
247
|
+
expect(saved).toEqual([]);
|
|
248
|
+
|
|
249
|
+
await publishWork({
|
|
250
|
+
id: "call-edit",
|
|
251
|
+
threadId,
|
|
252
|
+
calls: [
|
|
253
|
+
{
|
|
254
|
+
...call,
|
|
255
|
+
status: "completed",
|
|
256
|
+
output: "{}",
|
|
257
|
+
completedAt: "2026-10-24T00:00:04.000Z",
|
|
258
|
+
},
|
|
259
|
+
],
|
|
260
|
+
completedAt: "2026-10-24T00:00:04.000Z",
|
|
261
|
+
});
|
|
262
|
+
expect(saved.at(-1)?.runs).toEqual([
|
|
263
|
+
expect.objectContaining({ id: "voice:call-edit", status: "completed" }),
|
|
264
|
+
]);
|
|
265
|
+
expect(saved.at(-1)?.messages.map((message) => message.id)).toContain(
|
|
266
|
+
`${threadId}:user-2`,
|
|
267
|
+
);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it.each([
|
|
271
|
+
["before", true],
|
|
272
|
+
["after", false],
|
|
273
|
+
])(
|
|
274
|
+
"closes voice work a reload left running when the thread loads %s the chat mounts",
|
|
275
|
+
async (_when, loadBeforeMount) => {
|
|
276
|
+
const threadId = `thread-voice-orphan-${String(loadBeforeMount)}`;
|
|
277
|
+
const runId = "voice:call-edit";
|
|
278
|
+
const started = "2026-10-24T00:00:02.000Z";
|
|
279
|
+
const { client, saved, finishLoading } = await mountChat(
|
|
280
|
+
threadId,
|
|
281
|
+
{
|
|
282
|
+
events: [
|
|
283
|
+
{
|
|
284
|
+
id: `${runId}:call-edit:tool-started`,
|
|
285
|
+
threadId,
|
|
286
|
+
runId,
|
|
287
|
+
sequence: 1,
|
|
288
|
+
occurredAt: started,
|
|
289
|
+
type: "tool.started",
|
|
290
|
+
toolCall: {
|
|
291
|
+
id: "call-edit",
|
|
292
|
+
name: "edit-design",
|
|
293
|
+
status: "running",
|
|
294
|
+
runId,
|
|
295
|
+
},
|
|
296
|
+
},
|
|
297
|
+
],
|
|
298
|
+
runs: [{ id: runId, threadId, status: "running", lastSequence: 1 }],
|
|
299
|
+
},
|
|
300
|
+
{ loadBeforeMount },
|
|
301
|
+
);
|
|
302
|
+
await finishLoading();
|
|
303
|
+
|
|
304
|
+
expect(
|
|
305
|
+
client
|
|
306
|
+
.getThread(threadId)
|
|
307
|
+
.events.filter((event) => event.runId === runId)
|
|
308
|
+
.map((event) =>
|
|
309
|
+
event.type === "tool.updated"
|
|
310
|
+
? `${event.type}:${event.toolCall.status}`
|
|
311
|
+
: event.type,
|
|
312
|
+
),
|
|
313
|
+
).toEqual([
|
|
314
|
+
"tool.started",
|
|
315
|
+
"activity.started",
|
|
316
|
+
"tool.updated:cancelled",
|
|
317
|
+
"activity.completed",
|
|
318
|
+
"run.completed",
|
|
319
|
+
]);
|
|
320
|
+
|
|
321
|
+
await publish(threadId, {
|
|
322
|
+
id: `${threadId}:user-1`,
|
|
323
|
+
role: "user",
|
|
324
|
+
text: "Try again.",
|
|
325
|
+
});
|
|
326
|
+
expect(saved.at(-1)?.messages.map((message) => message.id)).toEqual([
|
|
327
|
+
`${threadId}:user-1`,
|
|
328
|
+
]);
|
|
329
|
+
},
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
it("shows a reply's tool calls as one work item between the turns and saves it", async () => {
|
|
333
|
+
const threadId = "thread-voice-work";
|
|
334
|
+
const { container, client, saved } = await mountChat(threadId);
|
|
335
|
+
const search = {
|
|
336
|
+
id: "call-search",
|
|
337
|
+
name: "web-search",
|
|
338
|
+
input: { query: "cats" },
|
|
339
|
+
startedAt: "2026-10-24T00:00:02.000Z",
|
|
340
|
+
};
|
|
341
|
+
const read = {
|
|
342
|
+
id: "call-read",
|
|
343
|
+
name: "web-request",
|
|
344
|
+
input: { url: "https://example.com/cats" },
|
|
345
|
+
startedAt: "2026-10-24T00:00:03.000Z",
|
|
346
|
+
};
|
|
347
|
+
const publishWork = async (update: RealtimeVoiceWorkUpdate) => {
|
|
348
|
+
await act(async () => {
|
|
349
|
+
realtimeVoiceTranscriptRegistry.publishWork(update);
|
|
350
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
351
|
+
});
|
|
352
|
+
};
|
|
353
|
+
const reply = { id: "call-search", threadId };
|
|
354
|
+
|
|
355
|
+
await publish(threadId, {
|
|
356
|
+
id: `${threadId}:user-1`,
|
|
357
|
+
role: "user",
|
|
358
|
+
text: "Search the web for cats.",
|
|
359
|
+
});
|
|
360
|
+
await publish(threadId, {
|
|
361
|
+
id: `${threadId}:assistant-1`,
|
|
362
|
+
role: "assistant",
|
|
363
|
+
text: "On it, searching now.",
|
|
364
|
+
createdAt: "2026-10-24T00:00:01.000Z",
|
|
365
|
+
});
|
|
366
|
+
await publishWork({ ...reply, calls: [{ ...search, status: "running" }] });
|
|
367
|
+
|
|
368
|
+
const running = container.textContent ?? "";
|
|
369
|
+
expect(running.indexOf("Searched 1 query")).toBeGreaterThan(
|
|
370
|
+
running.indexOf("On it, searching now."),
|
|
371
|
+
);
|
|
372
|
+
// The local run never holds the composer the way a server run does.
|
|
373
|
+
expect(client.getThread(threadId).activeRunIds).toEqual([]);
|
|
374
|
+
// It still shows what it is doing now.
|
|
375
|
+
const liveStep = () =>
|
|
376
|
+
container.querySelector('[data-agentkit-current-activity="true"]')
|
|
377
|
+
?.textContent;
|
|
378
|
+
expect(liveStep()).toBeTruthy();
|
|
379
|
+
|
|
380
|
+
const finishedSearch = {
|
|
381
|
+
...search,
|
|
382
|
+
status: "completed" as const,
|
|
383
|
+
output: "{}",
|
|
384
|
+
completedAt: "2026-10-24T00:00:02.500Z",
|
|
385
|
+
};
|
|
386
|
+
await publishWork({
|
|
387
|
+
...reply,
|
|
388
|
+
calls: [finishedSearch, { ...read, status: "running" }],
|
|
389
|
+
});
|
|
390
|
+
await publishWork({
|
|
391
|
+
...reply,
|
|
392
|
+
calls: [
|
|
393
|
+
finishedSearch,
|
|
394
|
+
{
|
|
395
|
+
...read,
|
|
396
|
+
status: "completed",
|
|
397
|
+
output: "<html>",
|
|
398
|
+
completedAt: "2026-10-24T00:00:04.000Z",
|
|
399
|
+
},
|
|
400
|
+
],
|
|
401
|
+
completedAt: "2026-10-24T00:00:04.000Z",
|
|
402
|
+
});
|
|
403
|
+
await publish(threadId, {
|
|
404
|
+
id: `${threadId}:assistant-2`,
|
|
405
|
+
role: "assistant",
|
|
406
|
+
text: "Cats are great.",
|
|
407
|
+
createdAt: "2026-10-24T00:00:05.000Z",
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
const done = container.textContent ?? "";
|
|
411
|
+
expect(liveStep()).toBeUndefined();
|
|
412
|
+
expect(done.match(/Worked/g)).toHaveLength(1);
|
|
413
|
+
const row = done.indexOf("Worked");
|
|
414
|
+
expect(row).toBeGreaterThan(done.indexOf("On it, searching now."));
|
|
415
|
+
expect(row).toBeLessThan(done.indexOf("Cats are great."));
|
|
416
|
+
expect(saved.at(-1)?.events?.map((event) => event.type)).toEqual([
|
|
417
|
+
"tool.started",
|
|
418
|
+
"activity.started",
|
|
419
|
+
"tool.updated",
|
|
420
|
+
"activity.completed",
|
|
421
|
+
"tool.started",
|
|
422
|
+
"activity.started",
|
|
423
|
+
"tool.updated",
|
|
424
|
+
"activity.completed",
|
|
425
|
+
"run.completed",
|
|
426
|
+
]);
|
|
427
|
+
expect(saved.at(-1)?.runs).toEqual([
|
|
428
|
+
expect.objectContaining({ id: "voice:call-search", status: "completed" }),
|
|
429
|
+
]);
|
|
430
|
+
});
|
|
196
431
|
});
|
|
@@ -2,16 +2,26 @@ import {
|
|
|
2
2
|
hasActiveAgentRuns,
|
|
3
3
|
type AgentKitController,
|
|
4
4
|
} from "@agent-native/agentkit/client";
|
|
5
|
-
import
|
|
5
|
+
import {
|
|
6
|
+
inferAgentActivityKind,
|
|
7
|
+
type AgentActivity,
|
|
8
|
+
type AgentEvent,
|
|
9
|
+
type AgentMessage,
|
|
10
|
+
type AgentToolCall,
|
|
11
|
+
} from "@agent-native/agentkit/protocol";
|
|
6
12
|
import { useEffect } from "react";
|
|
7
13
|
|
|
8
14
|
import {
|
|
15
|
+
realtimeVoiceToolLabel,
|
|
9
16
|
realtimeVoiceTranscriptRegistry,
|
|
10
17
|
type RealtimeVoiceTranscriptMessage,
|
|
18
|
+
type RealtimeVoiceWorkCall,
|
|
19
|
+
type RealtimeVoiceWorkUpdate,
|
|
11
20
|
} from "../../../composer/realtime-voice-transcript.js";
|
|
12
21
|
import { isChatThreadViewed } from "./viewed-threads.js";
|
|
13
22
|
|
|
14
23
|
const REALTIME_VOICE_MESSAGE_SOURCE = "realtime-voice";
|
|
24
|
+
const VOICE_RUN_PREFIX = "voice:";
|
|
15
25
|
|
|
16
26
|
function realtimeVoiceTranscriptMessage(
|
|
17
27
|
transcript: RealtimeVoiceTranscriptMessage,
|
|
@@ -29,6 +39,175 @@ function realtimeVoiceTranscriptMessage(
|
|
|
29
39
|
};
|
|
30
40
|
}
|
|
31
41
|
|
|
42
|
+
/**
|
|
43
|
+
* The events a voice reply's work still needs in its thread, as one local
|
|
44
|
+
* run. The run never starts on the server, so it never counts as an active
|
|
45
|
+
* run that would hold the composer or the transcript. `existingIds` and
|
|
46
|
+
* `lastSequence` describe the run as the thread already has it.
|
|
47
|
+
*/
|
|
48
|
+
export function realtimeVoiceWorkEvents(
|
|
49
|
+
update: RealtimeVoiceWorkUpdate,
|
|
50
|
+
existingIds: ReadonlySet<string> = new Set(),
|
|
51
|
+
lastSequence = 0,
|
|
52
|
+
): AgentEvent[] {
|
|
53
|
+
const runId = `${VOICE_RUN_PREFIX}${update.id}`;
|
|
54
|
+
const base = (id: string, occurredAt: string) => ({
|
|
55
|
+
id: `${runId}:${id}`,
|
|
56
|
+
threadId: update.threadId,
|
|
57
|
+
runId,
|
|
58
|
+
// Numbered below, once the events this run already has are left out.
|
|
59
|
+
sequence: 0,
|
|
60
|
+
occurredAt,
|
|
61
|
+
});
|
|
62
|
+
const steps: { at: string; event: AgentEvent }[] = [];
|
|
63
|
+
for (const call of update.calls) {
|
|
64
|
+
const toolCall: AgentToolCall = {
|
|
65
|
+
id: call.id,
|
|
66
|
+
name: call.name,
|
|
67
|
+
status: "running",
|
|
68
|
+
runId,
|
|
69
|
+
...(call.input ? { input: call.input } : {}),
|
|
70
|
+
};
|
|
71
|
+
const activity: AgentActivity = {
|
|
72
|
+
id: call.id,
|
|
73
|
+
kind: inferAgentActivityKind(call.name),
|
|
74
|
+
label: realtimeVoiceToolLabel(call.name),
|
|
75
|
+
status: "running",
|
|
76
|
+
runId,
|
|
77
|
+
startedAt: call.startedAt,
|
|
78
|
+
};
|
|
79
|
+
steps.push(
|
|
80
|
+
{
|
|
81
|
+
at: call.startedAt,
|
|
82
|
+
event: {
|
|
83
|
+
...base(`${call.id}:tool-started`, call.startedAt),
|
|
84
|
+
type: "tool.started",
|
|
85
|
+
toolCall,
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
at: call.startedAt,
|
|
90
|
+
event: {
|
|
91
|
+
...base(`${call.id}:activity-started`, call.startedAt),
|
|
92
|
+
type: "activity.started",
|
|
93
|
+
activity,
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
);
|
|
97
|
+
const { completedAt } = call;
|
|
98
|
+
if (!completedAt || call.status === "running") continue;
|
|
99
|
+
steps.push(
|
|
100
|
+
{
|
|
101
|
+
at: completedAt,
|
|
102
|
+
event: {
|
|
103
|
+
...base(`${call.id}:tool-ended`, completedAt),
|
|
104
|
+
type: "tool.updated",
|
|
105
|
+
toolCall: {
|
|
106
|
+
...toolCall,
|
|
107
|
+
status: call.status,
|
|
108
|
+
...(call.output !== undefined ? { output: call.output } : {}),
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
at: completedAt,
|
|
114
|
+
event: {
|
|
115
|
+
...base(`${call.id}:activity-ended`, completedAt),
|
|
116
|
+
type: "activity.completed",
|
|
117
|
+
activity: { ...activity, status: call.status, completedAt },
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
// Stable: a call's start keeps its place before its end at the same time.
|
|
123
|
+
steps.sort((left, right) => Date.parse(left.at) - Date.parse(right.at));
|
|
124
|
+
if (update.completedAt) {
|
|
125
|
+
steps.push({
|
|
126
|
+
at: update.completedAt,
|
|
127
|
+
event: {
|
|
128
|
+
...base("completed", update.completedAt),
|
|
129
|
+
type: "run.completed",
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
let sequence = lastSequence;
|
|
134
|
+
return steps
|
|
135
|
+
.filter(({ event }) => !existingIds.has(event.id))
|
|
136
|
+
.map(({ event }) => ({ ...event, sequence: ++sequence }));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Voice work this page is still running, by thread. Saved work that is not
|
|
141
|
+
* here can never finish, so it is closed instead of waited on.
|
|
142
|
+
*/
|
|
143
|
+
const runningVoiceWork = new Map<string, Set<string>>();
|
|
144
|
+
|
|
145
|
+
interface VoiceRunThread {
|
|
146
|
+
events: readonly AgentEvent[];
|
|
147
|
+
runs: Readonly<Record<string, { lastSequence: number }>>;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function voiceRunPosition(thread: VoiceRunThread, runId: string) {
|
|
151
|
+
return {
|
|
152
|
+
existingIds: new Set(
|
|
153
|
+
thread.events
|
|
154
|
+
.filter((event) => event.runId === runId)
|
|
155
|
+
.map((event) => event.id),
|
|
156
|
+
),
|
|
157
|
+
lastSequence:
|
|
158
|
+
new Map(Object.entries(thread.runs)).get(runId)?.lastSequence ?? 0,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* The events that end, as cancelled, every voice run in `thread` with no
|
|
164
|
+
* terminal event. A snapshot saved while a call ran keeps such a run after a
|
|
165
|
+
* reload, and nothing is left to finish it.
|
|
166
|
+
*/
|
|
167
|
+
export function orphanedVoiceWorkEvents(
|
|
168
|
+
thread: VoiceRunThread,
|
|
169
|
+
threadId: string,
|
|
170
|
+
completedAt = new Date().toISOString(),
|
|
171
|
+
): AgentEvent[] {
|
|
172
|
+
const runs = new Map<
|
|
173
|
+
string,
|
|
174
|
+
{ calls: RealtimeVoiceWorkCall[]; completed: boolean }
|
|
175
|
+
>();
|
|
176
|
+
for (const event of thread.events) {
|
|
177
|
+
if (!event.runId.startsWith(VOICE_RUN_PREFIX)) continue;
|
|
178
|
+
const run = runs.get(event.runId) ?? { calls: [], completed: false };
|
|
179
|
+
runs.set(event.runId, run);
|
|
180
|
+
if (event.type === "run.completed") run.completed = true;
|
|
181
|
+
if (event.type !== "tool.started") continue;
|
|
182
|
+
const { input } = event.toolCall;
|
|
183
|
+
run.calls.push({
|
|
184
|
+
id: event.toolCall.id,
|
|
185
|
+
name: event.toolCall.name,
|
|
186
|
+
status: "cancelled",
|
|
187
|
+
startedAt: event.occurredAt,
|
|
188
|
+
completedAt,
|
|
189
|
+
...(input && typeof input === "object" && !Array.isArray(input)
|
|
190
|
+
? { input: input as Record<string, unknown> }
|
|
191
|
+
: {}),
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
return [...runs]
|
|
195
|
+
.filter(([, run]) => !run.completed)
|
|
196
|
+
.flatMap(([runId, run]) => {
|
|
197
|
+
const { existingIds, lastSequence } = voiceRunPosition(thread, runId);
|
|
198
|
+
return realtimeVoiceWorkEvents(
|
|
199
|
+
{
|
|
200
|
+
id: runId.slice(VOICE_RUN_PREFIX.length),
|
|
201
|
+
threadId,
|
|
202
|
+
calls: run.calls,
|
|
203
|
+
completedAt,
|
|
204
|
+
},
|
|
205
|
+
existingIds,
|
|
206
|
+
lastSequence,
|
|
207
|
+
);
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
32
211
|
/** A voice chat turn still being spoken; hosts must not save it. */
|
|
33
212
|
export function isStreamingVoiceMessage(message: AgentMessage): boolean {
|
|
34
213
|
const custom = message.metadata?.custom as { source?: unknown } | undefined;
|
|
@@ -51,23 +230,81 @@ export function useRealtimeVoiceTranscripts(
|
|
|
51
230
|
useEffect(() => {
|
|
52
231
|
if (!threadId || !controller.upsertLocalMessages) return;
|
|
53
232
|
const upsert = controller.upsertLocalMessages.bind(controller);
|
|
54
|
-
|
|
233
|
+
const appendEvents = controller.appendLocalEvents?.bind(controller);
|
|
234
|
+
/** False when the thread could not be checked yet. */
|
|
235
|
+
const closeOrphanedWork = (): boolean => {
|
|
236
|
+
if (!appendEvents || runningVoiceWork.get(threadId)?.size) return false;
|
|
237
|
+
const thread = controller.getThread(threadId);
|
|
238
|
+
if (hasActiveAgentRuns(thread)) return false;
|
|
239
|
+
const events = orphanedVoiceWorkEvents(thread, threadId);
|
|
240
|
+
if (events.length > 0) appendEvents(threadId, events);
|
|
241
|
+
return true;
|
|
242
|
+
};
|
|
243
|
+
const persist = () => {
|
|
244
|
+
// The work's completion saves the thread, this turn included.
|
|
245
|
+
if (runningVoiceWork.get(threadId)?.size) return;
|
|
246
|
+
closeOrphanedWork();
|
|
247
|
+
const messages = controller
|
|
248
|
+
.getThread(threadId)
|
|
249
|
+
.messages.filter((message) => !isStreamingVoiceMessage(message));
|
|
250
|
+
// A failed save surfaces through the controller's connection error;
|
|
251
|
+
// a host without transport persistence saves the thread itself.
|
|
252
|
+
void controller.persistThreadSnapshotWithResult?.(threadId, messages);
|
|
253
|
+
};
|
|
254
|
+
// The thread's snapshot can land after this mounts, so check each time
|
|
255
|
+
// its events change rather than once.
|
|
256
|
+
let checkedEventCount = -1;
|
|
257
|
+
const closeOrphanedWorkOnChange = () => {
|
|
258
|
+
const { length } = controller.getThread(threadId).events;
|
|
259
|
+
if (length === checkedEventCount) return;
|
|
260
|
+
if (closeOrphanedWork()) checkedEventCount = length;
|
|
261
|
+
};
|
|
262
|
+
closeOrphanedWorkOnChange();
|
|
263
|
+
const unsubscribe = controller.subscribe(closeOrphanedWorkOnChange);
|
|
264
|
+
const unregister = realtimeVoiceTranscriptRegistry.register({
|
|
55
265
|
threadId,
|
|
56
266
|
active: () => isChatThreadViewed(threadId),
|
|
267
|
+
...(appendEvents
|
|
268
|
+
? {
|
|
269
|
+
appendWork: (update: RealtimeVoiceWorkUpdate) => {
|
|
270
|
+
if (hasActiveAgentRuns(controller.getThread(threadId))) {
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
const runId = `${VOICE_RUN_PREFIX}${update.id}`;
|
|
274
|
+
const { existingIds, lastSequence } = voiceRunPosition(
|
|
275
|
+
controller.getThread(threadId),
|
|
276
|
+
runId,
|
|
277
|
+
);
|
|
278
|
+
const running = runningVoiceWork.get(threadId) ?? new Set();
|
|
279
|
+
runningVoiceWork.set(threadId, running);
|
|
280
|
+
// Marked before the append notifies, so its own events never
|
|
281
|
+
// read as orphaned.
|
|
282
|
+
if (!update.completedAt) running.add(runId);
|
|
283
|
+
appendEvents(
|
|
284
|
+
threadId,
|
|
285
|
+
realtimeVoiceWorkEvents(update, existingIds, lastSequence),
|
|
286
|
+
);
|
|
287
|
+
if (update.completedAt) {
|
|
288
|
+
running.delete(runId);
|
|
289
|
+
persist();
|
|
290
|
+
}
|
|
291
|
+
return true;
|
|
292
|
+
},
|
|
293
|
+
}
|
|
294
|
+
: {}),
|
|
57
295
|
append: (transcript) => {
|
|
58
296
|
// A run owns the thread snapshot while it streams; the turn waits in
|
|
59
297
|
// the registry and lands when this sink re-registers after the run.
|
|
60
298
|
if (hasActiveAgentRuns(controller.getThread(threadId))) return false;
|
|
61
299
|
upsert(threadId, [realtimeVoiceTranscriptMessage(transcript)]);
|
|
62
300
|
if (transcript.status === "streaming") return true;
|
|
63
|
-
|
|
64
|
-
.getThread(threadId)
|
|
65
|
-
.messages.filter((message) => !isStreamingVoiceMessage(message));
|
|
66
|
-
// A failed save surfaces through the controller's connection error;
|
|
67
|
-
// a host without transport persistence saves the thread itself.
|
|
68
|
-
void controller.persistThreadSnapshotWithResult?.(threadId, messages);
|
|
301
|
+
persist();
|
|
69
302
|
return true;
|
|
70
303
|
},
|
|
71
304
|
});
|
|
305
|
+
return () => {
|
|
306
|
+
unsubscribe();
|
|
307
|
+
unregister();
|
|
308
|
+
};
|
|
72
309
|
}, [controller, threadId, running]);
|
|
73
310
|
}
|
|
@@ -76,6 +76,7 @@ const chatMocks = vi.hoisted(() => ({
|
|
|
76
76
|
activeRunIds: [] as string[],
|
|
77
77
|
runs: {} as Record<string, any>,
|
|
78
78
|
approvals: {} as Record<string, any>,
|
|
79
|
+
connectionRequests: {} as Record<string, any>,
|
|
79
80
|
suggestions: [] as any[],
|
|
80
81
|
tools: {} as Record<string, unknown>,
|
|
81
82
|
activities: {} as Record<string, unknown>,
|
|
@@ -1060,6 +1061,7 @@ beforeEach(() => {
|
|
|
1060
1061
|
activeRunIds: [],
|
|
1061
1062
|
runs: {},
|
|
1062
1063
|
approvals: {},
|
|
1064
|
+
connectionRequests: {},
|
|
1063
1065
|
suggestions: [],
|
|
1064
1066
|
tools: {},
|
|
1065
1067
|
activities: {},
|
|
@@ -3040,22 +3042,19 @@ describe("AgentKitAssistantChat host behavior", () => {
|
|
|
3040
3042
|
}
|
|
3041
3043
|
});
|
|
3042
3044
|
|
|
3043
|
-
it("
|
|
3045
|
+
it("closes the real editor to typing while AI setup is missing", async () => {
|
|
3044
3046
|
chatMocks.useRealChat = true;
|
|
3045
3047
|
chatMocks.readiness = { canChat: false, missing: true, state: "missing" };
|
|
3046
3048
|
await useRealComposer();
|
|
3047
3049
|
await mount(baseProps({ showModelSelector: false }));
|
|
3048
3050
|
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
chatMocks.composerProps.composerRef.current.setText("Draft before setup"),
|
|
3057
|
-
);
|
|
3058
|
-
expect(editor?.textContent).toBe("Draft before setup");
|
|
3051
|
+
expect(container.querySelector('[contenteditable="false"]')).not.toBeNull();
|
|
3052
|
+
expect(container.querySelector('[contenteditable="true"]')).toBeNull();
|
|
3053
|
+
expect(
|
|
3054
|
+
container
|
|
3055
|
+
.querySelector('[role="textbox"]')
|
|
3056
|
+
?.getAttribute("aria-disabled"),
|
|
3057
|
+
).toBe("true");
|
|
3059
3058
|
});
|
|
3060
3059
|
|
|
3061
3060
|
it("keeps the real editor editable while a submission is in flight", async () => {
|
|
@@ -9365,6 +9364,55 @@ describe("AgentKitAssistantChat host behavior", () => {
|
|
|
9365
9364
|
});
|
|
9366
9365
|
});
|
|
9367
9366
|
|
|
9367
|
+
it("keeps a host-owned setup route open until the server confirms setup is missing", async () => {
|
|
9368
|
+
chatMocks.readiness = { canChat: false, missing: false, state: "unknown" };
|
|
9369
|
+
const props = baseProps({ setupCardOwner: "host" });
|
|
9370
|
+
await mount(props);
|
|
9371
|
+
await act(async () =>
|
|
9372
|
+
chatMocks.composerProps.onModelConnectionRequiredChange(true),
|
|
9373
|
+
);
|
|
9374
|
+
await flush();
|
|
9375
|
+
|
|
9376
|
+
expect(chatMocks.composerProps.setupCardVisible).toBe(false);
|
|
9377
|
+
|
|
9378
|
+
chatMocks.readiness = { canChat: false, missing: true, state: "missing" };
|
|
9379
|
+
await act(async () => root.render(<AgentKitAssistantChat {...props} />));
|
|
9380
|
+
await flush();
|
|
9381
|
+
|
|
9382
|
+
expect(chatMocks.composerProps.setupCardVisible).toBe(true);
|
|
9383
|
+
});
|
|
9384
|
+
|
|
9385
|
+
it("does not replay a refused prompt when only the model list guessed setup was missing", async () => {
|
|
9386
|
+
chatMocks.readiness = { canChat: false, missing: false, state: "unknown" };
|
|
9387
|
+
chatMocks.failureError = {
|
|
9388
|
+
code: "missing_credentials",
|
|
9389
|
+
message: "No LLM provider is connected.",
|
|
9390
|
+
};
|
|
9391
|
+
chatMocks.thread.messages = [
|
|
9392
|
+
{
|
|
9393
|
+
id: "user-refused",
|
|
9394
|
+
role: "user",
|
|
9395
|
+
parts: [{ type: "text", text: "Old prompt" }],
|
|
9396
|
+
},
|
|
9397
|
+
];
|
|
9398
|
+
const props = baseProps({});
|
|
9399
|
+
await mount(props);
|
|
9400
|
+
await act(async () =>
|
|
9401
|
+
chatMocks.composerProps.onModelConnectionRequiredChange(true),
|
|
9402
|
+
);
|
|
9403
|
+
await flush();
|
|
9404
|
+
|
|
9405
|
+
chatMocks.readiness = {
|
|
9406
|
+
canChat: true,
|
|
9407
|
+
missing: false,
|
|
9408
|
+
state: "configured",
|
|
9409
|
+
};
|
|
9410
|
+
await act(async () => root.render(<AgentKitAssistantChat {...props} />));
|
|
9411
|
+
await flush();
|
|
9412
|
+
|
|
9413
|
+
expect(chatMocks.control.sendMessage).not.toHaveBeenCalled();
|
|
9414
|
+
});
|
|
9415
|
+
|
|
9368
9416
|
it("does not replay a refused prompt when the thread reopens already connected", async () => {
|
|
9369
9417
|
chatMocks.readiness = { canChat: false, missing: false, state: "unknown" };
|
|
9370
9418
|
chatMocks.failureError = {
|