@agent-native/core 0.94.1 → 0.94.3

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 (141) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +14 -0
  3. package/corpus/core/docs/content/cloneable-saas.mdx +15 -15
  4. package/corpus/core/docs/content/creating-templates.mdx +1 -1
  5. package/corpus/core/docs/content/extensions.mdx +1 -1
  6. package/corpus/core/docs/content/local-file-mode.mdx +6 -6
  7. package/corpus/core/docs/content/locales/ar-SA/voice-input.mdx +2 -2
  8. package/corpus/core/docs/content/locales/de-DE/voice-input.mdx +2 -2
  9. package/corpus/core/docs/content/locales/es-ES/voice-input.mdx +2 -2
  10. package/corpus/core/docs/content/locales/fr-FR/voice-input.mdx +2 -2
  11. package/corpus/core/docs/content/locales/hi-IN/voice-input.mdx +2 -2
  12. package/corpus/core/docs/content/locales/ja-JP/voice-input.mdx +2 -2
  13. package/corpus/core/docs/content/locales/ko-KR/voice-input.mdx +2 -2
  14. package/corpus/core/docs/content/locales/pt-BR/voice-input.mdx +2 -2
  15. package/corpus/core/docs/content/locales/zh-CN/voice-input.mdx +2 -2
  16. package/corpus/core/docs/content/locales/zh-TW/voice-input.mdx +2 -2
  17. package/corpus/core/docs/content/sharing.mdx +4 -4
  18. package/corpus/core/docs/content/template-analytics.mdx +2 -2
  19. package/corpus/core/docs/content/template-calendar.mdx +4 -4
  20. package/corpus/core/docs/content/template-clips.mdx +2 -2
  21. package/corpus/core/docs/content/template-content.mdx +12 -11
  22. package/corpus/core/docs/content/template-mail.mdx +1 -1
  23. package/corpus/core/docs/content/template-slides.mdx +2 -2
  24. package/corpus/core/docs/content/voice-input.mdx +2 -2
  25. package/corpus/core/docs/content/workspace.mdx +1 -1
  26. package/corpus/core/package.json +1 -1
  27. package/corpus/core/src/a2a/artifact-response.ts +154 -1
  28. package/corpus/core/src/client/AssistantChat.tsx +30 -0
  29. package/corpus/core/src/client/MultiTabAssistantChat.tsx +19 -1
  30. package/corpus/core/src/client/agent-chat.ts +6 -0
  31. package/corpus/core/src/client/composer/RealtimeVoiceMode.tsx +72 -2
  32. package/corpus/core/src/client/composer/realtime-voice-audio-level.ts +64 -0
  33. package/corpus/core/src/client/composer/realtime-voice-transcript.ts +134 -0
  34. package/corpus/core/src/client/composer/useRealtimeVoiceMode.tsx +220 -6
  35. package/corpus/core/src/client/use-chat-threads.ts +33 -9
  36. package/corpus/core/src/integrations/a2a-continuation-processor.ts +118 -31
  37. package/corpus/core/src/integrations/a2a-continuations-store.ts +165 -7
  38. package/corpus/core/src/integrations/adapters/slack.ts +77 -7
  39. package/corpus/core/src/integrations/types.ts +30 -0
  40. package/corpus/core/src/integrations/webhook-handler.ts +54 -18
  41. package/corpus/core/src/scripts/call-agent.ts +1 -0
  42. package/corpus/core/src/server/request-context.ts +2 -0
  43. package/corpus/core/src/templates/workspace-core/.agents/skills/voice-transcription/SKILL.md +11 -5
  44. package/corpus/templates/clips/app/components/player/comments-panel.tsx +98 -1
  45. package/corpus/templates/clips/changelog/2026-07-10-comment-urls-open-as-clickable-links.md +6 -0
  46. package/corpus/templates/clips/changelog/2026-07-10-desktop-recording-uploads-now-show-a-brief-uploaded-state-wi.md +6 -0
  47. package/corpus/templates/clips/changelog/2026-07-10-desktop-recordings-now-retry-saving-their-locally-captured-t.md +6 -0
  48. package/corpus/templates/clips/changelog/2026-07-10-desktop-update-prompts-are-more-compact-and-easier-to-dismiss.md +6 -0
  49. package/corpus/templates/clips/changelog/2026-07-10-quiet-recordings-now-finish-saving-instead-of-getting-stuck.md +6 -0
  50. package/corpus/templates/clips/desktop/src/components/UpdateBanner.tsx +7 -4
  51. package/corpus/templates/clips/desktop/src/lib/recorder.ts +284 -85
  52. package/corpus/templates/clips/desktop/src/overlays/finalizing.tsx +148 -40
  53. package/corpus/templates/clips/desktop/src/overlays/meeting-notification.tsx +13 -13
  54. package/corpus/templates/clips/desktop/src/styles.css +83 -13
  55. package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +43 -28
  56. package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +130 -65
  57. package/corpus/templates/clips/desktop/src-tauri/src/notifications.rs +10 -4
  58. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +82 -1
  59. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/reset-chunks.post.ts +82 -1
  60. package/dist/a2a/artifact-response.d.ts.map +1 -1
  61. package/dist/a2a/artifact-response.js +127 -1
  62. package/dist/a2a/artifact-response.js.map +1 -1
  63. package/dist/client/AssistantChat.d.ts.map +1 -1
  64. package/dist/client/AssistantChat.js +20 -0
  65. package/dist/client/AssistantChat.js.map +1 -1
  66. package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
  67. package/dist/client/MultiTabAssistantChat.js +9 -0
  68. package/dist/client/MultiTabAssistantChat.js.map +1 -1
  69. package/dist/client/agent-chat.d.ts +6 -0
  70. package/dist/client/agent-chat.d.ts.map +1 -1
  71. package/dist/client/agent-chat.js.map +1 -1
  72. package/dist/client/composer/RealtimeVoiceMode.d.ts +3 -1
  73. package/dist/client/composer/RealtimeVoiceMode.d.ts.map +1 -1
  74. package/dist/client/composer/RealtimeVoiceMode.js +32 -3
  75. package/dist/client/composer/RealtimeVoiceMode.js.map +1 -1
  76. package/dist/client/composer/realtime-voice-audio-level.d.ts +14 -0
  77. package/dist/client/composer/realtime-voice-audio-level.d.ts.map +1 -0
  78. package/dist/client/composer/realtime-voice-audio-level.js +47 -0
  79. package/dist/client/composer/realtime-voice-audio-level.js.map +1 -0
  80. package/dist/client/composer/realtime-voice-transcript.d.ts +26 -0
  81. package/dist/client/composer/realtime-voice-transcript.d.ts.map +1 -0
  82. package/dist/client/composer/realtime-voice-transcript.js +103 -0
  83. package/dist/client/composer/realtime-voice-transcript.js.map +1 -0
  84. package/dist/client/composer/useRealtimeVoiceMode.d.ts +14 -0
  85. package/dist/client/composer/useRealtimeVoiceMode.d.ts.map +1 -1
  86. package/dist/client/composer/useRealtimeVoiceMode.js +177 -6
  87. package/dist/client/composer/useRealtimeVoiceMode.js.map +1 -1
  88. package/dist/client/use-chat-threads.d.ts.map +1 -1
  89. package/dist/client/use-chat-threads.js +26 -6
  90. package/dist/client/use-chat-threads.js.map +1 -1
  91. package/dist/collab/routes.d.ts +1 -1
  92. package/dist/integrations/a2a-continuation-processor.d.ts.map +1 -1
  93. package/dist/integrations/a2a-continuation-processor.js +76 -21
  94. package/dist/integrations/a2a-continuation-processor.js.map +1 -1
  95. package/dist/integrations/a2a-continuations-store.d.ts +4 -1
  96. package/dist/integrations/a2a-continuations-store.d.ts.map +1 -1
  97. package/dist/integrations/a2a-continuations-store.js +130 -6
  98. package/dist/integrations/a2a-continuations-store.js.map +1 -1
  99. package/dist/integrations/adapters/slack.d.ts.map +1 -1
  100. package/dist/integrations/adapters/slack.js +56 -7
  101. package/dist/integrations/adapters/slack.js.map +1 -1
  102. package/dist/integrations/types.d.ts +25 -0
  103. package/dist/integrations/types.d.ts.map +1 -1
  104. package/dist/integrations/types.js.map +1 -1
  105. package/dist/integrations/webhook-handler.js +54 -16
  106. package/dist/integrations/webhook-handler.js.map +1 -1
  107. package/dist/observability/routes.d.ts +1 -1
  108. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  109. package/dist/scripts/call-agent.d.ts.map +1 -1
  110. package/dist/scripts/call-agent.js +1 -0
  111. package/dist/scripts/call-agent.js.map +1 -1
  112. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  113. package/dist/server/request-context.d.ts +2 -0
  114. package/dist/server/request-context.d.ts.map +1 -1
  115. package/dist/server/request-context.js.map +1 -1
  116. package/dist/templates/workspace-core/.agents/skills/voice-transcription/SKILL.md +11 -5
  117. package/docs/content/cloneable-saas.mdx +15 -15
  118. package/docs/content/creating-templates.mdx +1 -1
  119. package/docs/content/extensions.mdx +1 -1
  120. package/docs/content/local-file-mode.mdx +6 -6
  121. package/docs/content/locales/ar-SA/voice-input.mdx +2 -2
  122. package/docs/content/locales/de-DE/voice-input.mdx +2 -2
  123. package/docs/content/locales/es-ES/voice-input.mdx +2 -2
  124. package/docs/content/locales/fr-FR/voice-input.mdx +2 -2
  125. package/docs/content/locales/hi-IN/voice-input.mdx +2 -2
  126. package/docs/content/locales/ja-JP/voice-input.mdx +2 -2
  127. package/docs/content/locales/ko-KR/voice-input.mdx +2 -2
  128. package/docs/content/locales/pt-BR/voice-input.mdx +2 -2
  129. package/docs/content/locales/zh-CN/voice-input.mdx +2 -2
  130. package/docs/content/locales/zh-TW/voice-input.mdx +2 -2
  131. package/docs/content/sharing.mdx +4 -4
  132. package/docs/content/template-analytics.mdx +2 -2
  133. package/docs/content/template-calendar.mdx +4 -4
  134. package/docs/content/template-clips.mdx +2 -2
  135. package/docs/content/template-content.mdx +12 -11
  136. package/docs/content/template-mail.mdx +1 -1
  137. package/docs/content/template-slides.mdx +2 -2
  138. package/docs/content/voice-input.mdx +2 -2
  139. package/docs/content/workspace.mdx +1 -1
  140. package/package.json +1 -1
  141. package/src/templates/workspace-core/.agents/skills/voice-transcription/SKILL.md +11 -5
@@ -10,6 +10,7 @@ import {
10
10
  } from "react";
11
11
  import { createPortal } from "react-dom";
12
12
 
13
+ import { requestAgentChatThreadOpen } from "../agent-chat.js";
13
14
  import {
14
15
  SIDEBAR_STATE_CHANGE_EVENT,
15
16
  type AgentSidebarStateChangeDetail,
@@ -18,6 +19,13 @@ import { agentNativePath } from "../api-path.js";
18
19
  import { readClientAppState, setClientAppState } from "../application-state.js";
19
20
  import { getBrowserTabId } from "../browser-tab-id.js";
20
21
  import { useT } from "../i18n.js";
22
+ import {
23
+ createRealtimeVoiceAudioLevelStore,
24
+ normalizeRealtimeVoiceRms,
25
+ smoothRealtimeVoiceLevel,
26
+ type RealtimeVoiceAudioLevelStore,
27
+ } from "./realtime-voice-audio-level.js";
28
+ import { realtimeVoiceTranscriptRegistry } from "./realtime-voice-transcript.js";
21
29
  import {
22
30
  RealtimeVoiceModeDock,
23
31
  type RealtimeVoiceModeCopy,
@@ -43,11 +51,54 @@ export interface RealtimeVoiceModeApi {
43
51
  active: boolean;
44
52
  errorMessage: string | null;
45
53
  chatVisible: boolean;
54
+ audioLevels: RealtimeVoiceAudioLevelStore;
46
55
  start: () => Promise<void>;
47
56
  end: () => void;
48
57
  toggleChat: () => void;
49
58
  }
50
59
 
60
+ export interface CompletedRealtimeVoiceTranscript {
61
+ role: "user" | "assistant";
62
+ text: string;
63
+ providerId?: string;
64
+ }
65
+
66
+ /**
67
+ * Voice mode owns the chat only temporarily. Restore the captured transcript
68
+ * when it is still the user's active thread (or the chat has no active thread)
69
+ * but never pull them back after they deliberately selected another thread.
70
+ */
71
+ export function shouldRestoreRealtimeVoiceTranscriptThread(
72
+ transcriptThreadId: string | undefined,
73
+ activeThreadId: string | undefined,
74
+ ): transcriptThreadId is string {
75
+ return Boolean(
76
+ transcriptThreadId &&
77
+ (!activeThreadId || activeThreadId === transcriptThreadId),
78
+ );
79
+ }
80
+
81
+ export function extractCompletedRealtimeVoiceTranscript(
82
+ event: RealtimeServerEvent,
83
+ ): CompletedRealtimeVoiceTranscript | null {
84
+ const userCompleted =
85
+ event.type === "conversation.item.input_audio_transcription.completed";
86
+ const assistantCompleted =
87
+ event.type === "response.output_audio_transcript.done";
88
+ if (!userCompleted && !assistantCompleted) return null;
89
+ const text =
90
+ typeof event.transcript === "string" ? event.transcript.trim() : "";
91
+ if (!text) return null;
92
+ const providerId = [event.item_id, event.response_id, event.event_id].find(
93
+ (value): value is string => typeof value === "string" && value.length > 0,
94
+ );
95
+ return {
96
+ role: userCompleted ? "user" : "assistant",
97
+ text,
98
+ ...(providerId ? { providerId } : {}),
99
+ };
100
+ }
101
+
51
102
  export interface RealtimeVoiceModeProviderProps {
52
103
  children: ReactNode;
53
104
  browserTabId?: string;
@@ -232,17 +283,29 @@ function useRealtimeVoiceModeController(
232
283
  const [state, setState] = useState<"idle" | RealtimeVoiceModeState>("idle");
233
284
  const [error, setError] = useState<string | null>(null);
234
285
  const [chatVisible, setChatVisible] = useState(false);
286
+ const [audioLevels] = useState(createRealtimeVoiceAudioLevelStore);
235
287
  const stateRef = useRef(state);
236
288
  const peerRef = useRef<RTCPeerConnection | null>(null);
237
289
  const channelRef = useRef<RTCDataChannel | null>(null);
238
290
  const streamRef = useRef<MediaStream | null>(null);
239
291
  const audioRef = useRef<HTMLAudioElement | null>(null);
292
+ const audioContextRef = useRef<AudioContext | null>(null);
293
+ const inputAnalyserRef = useRef<AnalyserNode | null>(null);
294
+ const outputAnalyserRef = useRef<AnalyserNode | null>(null);
295
+ const inputMeterBufferRef = useRef<Uint8Array<ArrayBuffer> | null>(null);
296
+ const outputMeterBufferRef = useRef<Uint8Array<ArrayBuffer> | null>(null);
297
+ const inputSourceRef = useRef<MediaStreamAudioSourceNode | null>(null);
298
+ const outputSourceRef = useRef<MediaStreamAudioSourceNode | null>(null);
299
+ const meterFrameRef = useRef<number | null>(null);
300
+ const lastMeterSampleRef = useRef(0);
240
301
  const abortRef = useRef<AbortController | null>(null);
241
302
  const handledCallsRef = useRef(new Set<string>());
242
303
  const sessionIdRef = useRef<string | undefined>(undefined);
243
304
  const startedAtRef = useRef<string | undefined>(undefined);
244
305
  const lastUserTextRef = useRef("");
245
306
  const lastAssistantTextRef = useRef("");
307
+ const transcriptThreadIdRef = useRef<string | undefined>(undefined);
308
+ const transcriptSequenceRef = useRef(0);
246
309
 
247
310
  useEffect(() => {
248
311
  stateRef.current = state;
@@ -279,6 +342,77 @@ function useRealtimeVoiceModeController(
279
342
  [syncAppState],
280
343
  );
281
344
 
345
+ const startMeterLoop = useCallback(() => {
346
+ if (meterFrameRef.current !== null) return;
347
+ const sample = (timestamp: number) => {
348
+ meterFrameRef.current = requestAnimationFrame(sample);
349
+ if (timestamp - lastMeterSampleRef.current < 50) return;
350
+ lastMeterSampleRef.current = timestamp;
351
+
352
+ const current = audioLevels.getSnapshot();
353
+ let input = current.input;
354
+ let output = current.output;
355
+ const inputAnalyser = inputAnalyserRef.current;
356
+ const inputBuffer = inputMeterBufferRef.current;
357
+ if (inputAnalyser && inputBuffer) {
358
+ inputAnalyser.getByteTimeDomainData(inputBuffer);
359
+ input = smoothRealtimeVoiceLevel(
360
+ input,
361
+ normalizeRealtimeVoiceRms(inputBuffer),
362
+ );
363
+ }
364
+ const outputAnalyser = outputAnalyserRef.current;
365
+ const outputBuffer = outputMeterBufferRef.current;
366
+ if (outputAnalyser && outputBuffer) {
367
+ outputAnalyser.getByteTimeDomainData(outputBuffer);
368
+ output = smoothRealtimeVoiceLevel(
369
+ output,
370
+ normalizeRealtimeVoiceRms(outputBuffer),
371
+ );
372
+ }
373
+ audioLevels.set({ input, output });
374
+ };
375
+ meterFrameRef.current = requestAnimationFrame(sample);
376
+ }, [audioLevels]);
377
+
378
+ const attachAudioMeter = useCallback(
379
+ (stream: MediaStream, channel: "input" | "output") => {
380
+ try {
381
+ const AudioCtor =
382
+ window.AudioContext ??
383
+ (
384
+ window as typeof window & {
385
+ webkitAudioContext?: typeof AudioContext;
386
+ }
387
+ ).webkitAudioContext;
388
+ if (!AudioCtor) return;
389
+ const context = audioContextRef.current ?? new AudioCtor();
390
+ audioContextRef.current = context;
391
+ void context.resume().catch(() => undefined);
392
+ const analyser = context.createAnalyser();
393
+ analyser.fftSize = 512;
394
+ const source = context.createMediaStreamSource(stream);
395
+ source.connect(analyser);
396
+ const buffer = new Uint8Array(analyser.frequencyBinCount);
397
+ if (channel === "input") {
398
+ inputSourceRef.current?.disconnect();
399
+ inputSourceRef.current = source;
400
+ inputAnalyserRef.current = analyser;
401
+ inputMeterBufferRef.current = buffer;
402
+ } else {
403
+ outputSourceRef.current?.disconnect();
404
+ outputSourceRef.current = source;
405
+ outputAnalyserRef.current = analyser;
406
+ outputMeterBufferRef.current = buffer;
407
+ }
408
+ startMeterLoop();
409
+ } catch {
410
+ // Audio metering is visual-only; keep the realtime call healthy.
411
+ }
412
+ },
413
+ [startMeterLoop],
414
+ );
415
+
282
416
  const cleanupTransport = useCallback(() => {
283
417
  abortRef.current?.abort();
284
418
  abortRef.current = null;
@@ -293,8 +427,25 @@ function useRealtimeVoiceModeController(
293
427
  audioRef.current.srcObject = null;
294
428
  }
295
429
  audioRef.current = null;
430
+ if (meterFrameRef.current !== null) {
431
+ cancelAnimationFrame(meterFrameRef.current);
432
+ meterFrameRef.current = null;
433
+ }
434
+ inputSourceRef.current?.disconnect();
435
+ outputSourceRef.current?.disconnect();
436
+ inputSourceRef.current = null;
437
+ outputSourceRef.current = null;
438
+ inputAnalyserRef.current = null;
439
+ outputAnalyserRef.current = null;
440
+ inputMeterBufferRef.current = null;
441
+ outputMeterBufferRef.current = null;
442
+ lastMeterSampleRef.current = 0;
443
+ const audioContext = audioContextRef.current;
444
+ audioContextRef.current = null;
445
+ if (audioContext) void audioContext.close().catch(() => undefined);
446
+ audioLevels.reset();
296
447
  handledCallsRef.current.clear();
297
- }, []);
448
+ }, [audioLevels]);
298
449
 
299
450
  const fail = useCallback(
300
451
  (message: string, options?: { openKeySettings?: boolean }) => {
@@ -342,6 +493,26 @@ function useRealtimeVoiceModeController(
342
493
  [browserTabId, transition],
343
494
  );
344
495
 
496
+ const persistCompletedTranscript = useCallback(
497
+ (event: RealtimeServerEvent) => {
498
+ const transcript = extractCompletedRealtimeVoiceTranscript(event);
499
+ const threadId = transcriptThreadIdRef.current;
500
+ if (!transcript || !threadId) return;
501
+ const sessionIdentity =
502
+ sessionIdRef.current ?? startedAtRef.current ?? "pending";
503
+ const providerIdentity =
504
+ transcript.providerId ?? `sequence-${++transcriptSequenceRef.current}`;
505
+ realtimeVoiceTranscriptRegistry.publish({
506
+ id: `realtime-voice:${sessionIdentity}:${transcript.role}:${providerIdentity}`,
507
+ threadId,
508
+ role: transcript.role,
509
+ text: transcript.text,
510
+ createdAt: new Date().toISOString(),
511
+ });
512
+ },
513
+ [],
514
+ );
515
+
345
516
  const handleServerEvent = useCallback(
346
517
  (event: RealtimeServerEvent) => {
347
518
  if (event.type === "session.created") {
@@ -367,14 +538,16 @@ function useRealtimeVoiceModeController(
367
538
  if (typeof event.transcript === "string") {
368
539
  lastAssistantTextRef.current = event.transcript;
369
540
  }
541
+ persistCompletedTranscript(event);
370
542
  syncAppState("speaking");
371
543
  } else if (
372
544
  event.type === "conversation.item.input_audio_transcription.completed"
373
545
  ) {
374
546
  if (typeof event.transcript === "string") {
375
547
  lastUserTextRef.current = event.transcript;
376
- syncAppState("working");
377
548
  }
549
+ persistCompletedTranscript(event);
550
+ syncAppState("working");
378
551
  } else if (event.type === "response.done") {
379
552
  const response = event.response;
380
553
  const status =
@@ -410,7 +583,14 @@ function useRealtimeVoiceModeController(
410
583
  transition("listening");
411
584
  }
412
585
  },
413
- [copy, fail, handleFunctionCall, syncAppState, transition],
586
+ [
587
+ copy,
588
+ fail,
589
+ handleFunctionCall,
590
+ persistCompletedTranscript,
591
+ syncAppState,
592
+ transition,
593
+ ],
414
594
  );
415
595
 
416
596
  const start = useCallback(async () => {
@@ -430,6 +610,9 @@ function useRealtimeVoiceModeController(
430
610
  lastUserTextRef.current = "";
431
611
  lastAssistantTextRef.current = "";
432
612
  sessionIdRef.current = undefined;
613
+ transcriptThreadIdRef.current =
614
+ realtimeVoiceTranscriptRegistry.activeThreadId();
615
+ transcriptSequenceRef.current = 0;
433
616
  transition("connecting");
434
617
  setChatVisible(false);
435
618
  window.dispatchEvent(new Event("agent-panel:close"));
@@ -445,6 +628,7 @@ function useRealtimeVoiceModeController(
445
628
  },
446
629
  });
447
630
  streamRef.current = stream;
631
+ attachAudioMeter(stream, "input");
448
632
 
449
633
  const peer = new RTCPeerConnection();
450
634
  peerRef.current = peer;
@@ -455,7 +639,9 @@ function useRealtimeVoiceModeController(
455
639
  audio.setAttribute("playsinline", "");
456
640
  audioRef.current = audio;
457
641
  peer.ontrack = (trackEvent) => {
458
- audio.srcObject = trackEvent.streams[0] ?? null;
642
+ const remoteStream = trackEvent.streams[0] ?? null;
643
+ audio.srcObject = remoteStream;
644
+ if (remoteStream) attachAudioMeter(remoteStream, "output");
459
645
  void audio.play().catch(() => undefined);
460
646
  };
461
647
 
@@ -501,15 +687,42 @@ function useRealtimeVoiceModeController(
501
687
  const status = (startError as { status?: unknown })?.status;
502
688
  fail(errorMessage(startError), { openKeySettings: status === 400 });
503
689
  }
504
- }, [browserTabId, copy, fail, handleServerEvent, transition]);
690
+ }, [
691
+ attachAudioMeter,
692
+ browserTabId,
693
+ copy,
694
+ fail,
695
+ handleServerEvent,
696
+ transition,
697
+ ]);
505
698
 
506
699
  const end = useCallback(() => {
507
700
  if (stateRef.current === "idle" || stateRef.current === "ending") return;
701
+ const transcriptThreadId = transcriptThreadIdRef.current;
702
+ const activeThreadId = realtimeVoiceTranscriptRegistry.activeThreadId();
508
703
  transition("ending");
509
704
  cleanupTransport();
510
705
  setError(null);
511
706
  sessionIdRef.current = undefined;
512
707
  startedAtRef.current = undefined;
708
+ transcriptThreadIdRef.current = undefined;
709
+ setChatVisible(true);
710
+ if (
711
+ shouldRestoreRealtimeVoiceTranscriptThread(
712
+ transcriptThreadId,
713
+ activeThreadId,
714
+ )
715
+ ) {
716
+ requestAgentChatThreadOpen({
717
+ threadId: transcriptThreadId,
718
+ // The request is delivered asynchronously. Re-checking this at the
719
+ // receiver prevents a navigation that happened during that gap from
720
+ // being overwritten.
721
+ onlyIfActiveThreadId: transcriptThreadId,
722
+ });
723
+ } else {
724
+ window.dispatchEvent(new Event("agent-panel:open"));
725
+ }
513
726
  transition("idle");
514
727
  }, [cleanupTransport, transition]);
515
728
 
@@ -538,6 +751,7 @@ function useRealtimeVoiceModeController(
538
751
  active: state !== "idle",
539
752
  errorMessage: error,
540
753
  chatVisible,
754
+ audioLevels,
541
755
  start,
542
756
  end,
543
757
  toggleChat,
@@ -568,10 +782,10 @@ export function RealtimeVoiceModeProvider({
568
782
  state={voice.state === "idle" ? "ending" : voice.state}
569
783
  copy={copy}
570
784
  chatVisible={voice.chatVisible}
785
+ audioLevels={voice.audioLevels}
571
786
  onToggleChat={voice.toggleChat}
572
787
  onEndVoiceMode={voice.end}
573
788
  errorMessage={voice.errorMessage}
574
- className="z-[270]"
575
789
  />,
576
790
  document.body,
577
791
  )
@@ -939,15 +939,39 @@ export function useChatThreads(
939
939
  titleSource,
940
940
  { preserveUserTitle },
941
941
  );
942
- await fetch(`${apiUrl}/threads/${encodeURIComponent(id)}`, {
943
- method: "PUT",
944
- headers: { "Content-Type": "application/json" },
945
- body: JSON.stringify({
946
- ...threadDataPayload,
947
- title,
948
- scope: localScope,
949
- }),
950
- });
942
+ const payload = {
943
+ ...threadDataPayload,
944
+ title,
945
+ scope: localScope,
946
+ };
947
+ let response = await fetch(
948
+ `${apiUrl}/threads/${encodeURIComponent(id)}`,
949
+ {
950
+ method: "PUT",
951
+ headers: { "Content-Type": "application/json" },
952
+ body: JSON.stringify(payload),
953
+ },
954
+ );
955
+ // A passive realtime-voice transcript can be the first content in a
956
+ // client-created thread, so no agent run has created its SQL row yet.
957
+ // Materialize that row idempotently and retry the same full save.
958
+ if (response.status === 404) {
959
+ const created = await fetch(`${apiUrl}/threads`, {
960
+ method: "POST",
961
+ headers: { "Content-Type": "application/json" },
962
+ body: JSON.stringify({ id, title, scope: localScope }),
963
+ });
964
+ if (!created.ok) return;
965
+ response = await fetch(
966
+ `${apiUrl}/threads/${encodeURIComponent(id)}`,
967
+ {
968
+ method: "PUT",
969
+ headers: { "Content-Type": "application/json" },
970
+ body: JSON.stringify(payload),
971
+ },
972
+ );
973
+ }
974
+ if (!response.ok) return;
951
975
  emitThreadsUpdated();
952
976
  // Update local thread list metadata. If the thread isn't in our
953
977
  // local list yet (an optimistic-only thread that the server just
@@ -18,7 +18,11 @@ import {
18
18
  type A2AContinuation,
19
19
  } from "./a2a-continuations-store.js";
20
20
  import { signInternalToken } from "./internal-token.js";
21
- import type { PlatformAdapter } from "./types.js";
21
+ import type {
22
+ OutgoingMessage,
23
+ PlatformAdapter,
24
+ PlatformRunProgress,
25
+ } from "./types.js";
22
26
 
23
27
  const PROCESSOR_PATH = `${FRAMEWORK_ROUTE_PREFIX}/integrations/process-a2a-continuation`;
24
28
  const TERMINAL_STATES = new Set(["completed", "failed", "canceled"]);
@@ -150,6 +154,8 @@ async function processClaimedContinuation(
150
154
  return;
151
155
  }
152
156
 
157
+ const progress = await resumeA2AContinuationProgress(continuation, adapter);
158
+
153
159
  const auth = await signContinuationToken(continuation);
154
160
  const client = new A2AClient(continuation.agentUrl, auth.apiKey, {
155
161
  requestTimeoutMs: POLL_REQUEST_TIMEOUT_MS,
@@ -162,6 +168,7 @@ async function processClaimedContinuation(
162
168
  while (Date.now() < deadline) {
163
169
  task = await client.getTask(continuation.a2aTaskId);
164
170
  if (TERMINAL_STATES.has(task.status.state)) break;
171
+ await reportA2AContinuationProgress(continuation, progress, task);
165
172
  await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
166
173
  }
167
174
  } catch (err) {
@@ -171,6 +178,7 @@ async function processClaimedContinuation(
171
178
  continuation,
172
179
  adapter,
173
180
  remotePollFailureReason(continuation),
181
+ progress,
174
182
  );
175
183
  return;
176
184
  }
@@ -182,6 +190,7 @@ async function processClaimedContinuation(
182
190
  continuation,
183
191
  adapter,
184
192
  err instanceof Error ? err.message : String(err),
193
+ progress,
185
194
  );
186
195
  return;
187
196
  }
@@ -190,24 +199,12 @@ async function processClaimedContinuation(
190
199
  }
191
200
 
192
201
  if (!task || !TERMINAL_STATES.has(task.status.state)) {
193
- const recoverableArtifactText = extractRecoverableArtifactText(task);
194
- if (recoverableArtifactText) {
195
- await deliverAndCompleteA2AContinuation(
196
- continuation,
197
- adapter,
198
- formatContinuationArtifactText(
199
- recoverableArtifactText,
200
- continuation.agentUrl,
201
- ),
202
- );
203
- return;
204
- }
205
-
206
202
  if (shouldStopPollingRemoteTask(continuation)) {
207
203
  await notifyAndFailA2AContinuation(
208
204
  continuation,
209
205
  adapter,
210
206
  remotePollFailureReason(continuation),
207
+ progress,
211
208
  );
212
209
  return;
213
210
  }
@@ -219,7 +216,7 @@ async function processClaimedContinuation(
219
216
  const reason =
220
217
  extractTaskText(task) ||
221
218
  `Remote A2A task ${continuation.a2aTaskId} ended with state ${task.status.state}`;
222
- await notifyAndFailA2AContinuation(continuation, adapter, reason);
219
+ await notifyAndFailA2AContinuation(continuation, adapter, reason, progress);
223
220
  return;
224
221
  }
225
222
 
@@ -232,11 +229,64 @@ async function processClaimedContinuation(
232
229
  continuation,
233
230
  adapter,
234
231
  `Remote A2A task ${continuation.a2aTaskId} completed without text`,
232
+ progress,
235
233
  );
236
234
  return;
237
235
  }
238
236
 
239
- await deliverAndCompleteA2AContinuation(continuation, adapter, text);
237
+ await deliverAndCompleteA2AContinuation(
238
+ continuation,
239
+ adapter,
240
+ text,
241
+ progress,
242
+ );
243
+ }
244
+
245
+ async function resumeA2AContinuationProgress(
246
+ continuation: A2AContinuation,
247
+ adapter: PlatformAdapter,
248
+ ): Promise<PlatformRunProgress | null> {
249
+ if (!continuation.progressRef || !adapter.resumeRunProgress) return null;
250
+ try {
251
+ const progress = await adapter.resumeRunProgress(
252
+ continuation.incoming,
253
+ continuation.progressRef,
254
+ );
255
+ if (!progress) return null;
256
+ await progress.onEvent({
257
+ type: "agent_call_progress",
258
+ agent: continuation.agentName,
259
+ state: "working",
260
+ elapsedSeconds: Math.max(
261
+ 0,
262
+ Math.round((Date.now() - continuation.createdAt) / 1_000),
263
+ ),
264
+ detail: "Continuing in the background",
265
+ });
266
+ return progress;
267
+ } catch {
268
+ // A continuation still has a normal reply fallback. Do not log the
269
+ // opaque provider reference or the inbound message payload.
270
+ return null;
271
+ }
272
+ }
273
+
274
+ async function reportA2AContinuationProgress(
275
+ continuation: A2AContinuation,
276
+ progress: PlatformRunProgress | null,
277
+ task: Task,
278
+ ): Promise<void> {
279
+ if (!progress) return;
280
+ await progress.onEvent({
281
+ type: "agent_call_progress",
282
+ agent: continuation.agentName,
283
+ state: task.status.state,
284
+ elapsedSeconds: Math.max(
285
+ 0,
286
+ Math.round((Date.now() - continuation.createdAt) / 1_000),
287
+ ),
288
+ detail: "Still working on the delegated request",
289
+ });
240
290
  }
241
291
 
242
292
  async function waitForContinuationDue(
@@ -262,6 +312,7 @@ async function notifyAndFailA2AContinuation(
262
312
  continuation: A2AContinuation,
263
313
  adapter: PlatformAdapter,
264
314
  reason: string,
315
+ progress: PlatformRunProgress | null = null,
265
316
  ): Promise<void> {
266
317
  const deliveryContinuation = await claimA2AContinuationDelivery(
267
318
  continuation.id,
@@ -273,11 +324,14 @@ async function notifyAndFailA2AContinuation(
273
324
  reason,
274
325
  );
275
326
  try {
327
+ const outgoing = adapter.formatAgentResponse(message);
276
328
  await withTimeout(
277
- adapter.sendResponse(
278
- adapter.formatAgentResponse(message),
279
- deliveryContinuation.incoming,
280
- { placeholderRef: deliveryContinuation.placeholderRef ?? undefined },
329
+ deliverA2AContinuationResponse(
330
+ adapter,
331
+ deliveryContinuation,
332
+ outgoing,
333
+ progress,
334
+ "error",
281
335
  ),
282
336
  PLATFORM_SEND_TIMEOUT_MS,
283
337
  `${deliveryContinuation.platform} failure notification timed out`,
@@ -296,6 +350,7 @@ async function deliverAndCompleteA2AContinuation(
296
350
  continuation: A2AContinuation,
297
351
  adapter: PlatformAdapter,
298
352
  text: string,
353
+ progress: PlatformRunProgress | null = null,
299
354
  ): Promise<void> {
300
355
  const deliveryContinuation = await claimA2AContinuationDelivery(
301
356
  continuation.id,
@@ -303,11 +358,14 @@ async function deliverAndCompleteA2AContinuation(
303
358
  if (!deliveryContinuation) return;
304
359
 
305
360
  try {
361
+ const outgoing = adapter.formatAgentResponse(text);
306
362
  await withTimeout(
307
- adapter.sendResponse(
308
- adapter.formatAgentResponse(text),
309
- deliveryContinuation.incoming,
310
- { placeholderRef: deliveryContinuation.placeholderRef ?? undefined },
363
+ deliverA2AContinuationResponse(
364
+ adapter,
365
+ deliveryContinuation,
366
+ outgoing,
367
+ progress,
368
+ "done",
311
369
  ),
312
370
  PLATFORM_SEND_TIMEOUT_MS,
313
371
  `${deliveryContinuation.platform} response delivery timed out`,
@@ -327,6 +385,42 @@ async function deliverAndCompleteA2AContinuation(
327
385
  await completeAfterSuccessfulDelivery(deliveryContinuation);
328
386
  }
329
387
 
388
+ async function deliverA2AContinuationResponse(
389
+ adapter: PlatformAdapter,
390
+ continuation: A2AContinuation,
391
+ message: OutgoingMessage,
392
+ progress: PlatformRunProgress | null,
393
+ status: "done" | "error",
394
+ ): Promise<void> {
395
+ if (progress) {
396
+ try {
397
+ await progress.onEvent({
398
+ type: "agent_call",
399
+ agent: continuation.agentName,
400
+ status,
401
+ });
402
+ await progress.complete(message);
403
+ return;
404
+ } catch {
405
+ // A resumed Slack stream can no longer be finalized (for example when
406
+ // chat.stopStream rejects). Preserve the final answer with the same
407
+ // thread reply fallback used by the initial webhook run. Also ask the
408
+ // adapter to terminate the native stream: otherwise Slack can keep the
409
+ // task card in its working state after the thread fallback succeeds.
410
+ try {
411
+ await progress.fail?.(
412
+ "I couldn't update the live response, but I posted the final result in this thread.",
413
+ );
414
+ } catch {
415
+ // The thread reply below is still the authoritative final answer.
416
+ }
417
+ }
418
+ }
419
+ await adapter.sendResponse(message, continuation.incoming, {
420
+ placeholderRef: continuation.placeholderRef ?? undefined,
421
+ });
422
+ }
423
+
330
424
  async function rescheduleAndRedispatchA2AContinuation(
331
425
  continuationId: string,
332
426
  ): Promise<void> {
@@ -524,13 +618,6 @@ function extractTaskText(task: Task): string {
524
618
  .join("\n");
525
619
  }
526
620
 
527
- function extractRecoverableArtifactText(task: Task | null): string {
528
- if (!task?.status.message?.metadata?.agentNativeRecoverableArtifacts) {
529
- return "";
530
- }
531
- return extractTaskText(task);
532
- }
533
-
534
621
  function formatContinuationArtifactText(
535
622
  text: string,
536
623
  agentUrl: string,