@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
@@ -0,0 +1,47 @@
1
+ const SILENT_LEVELS = { input: 0, output: 0 };
2
+ function clampLevel(value) {
3
+ if (!Number.isFinite(value))
4
+ return 0;
5
+ return Math.max(0, Math.min(1, value));
6
+ }
7
+ export function normalizeRealtimeVoiceRms(samples) {
8
+ if (samples.length === 0)
9
+ return 0;
10
+ let sumSquares = 0;
11
+ for (const sample of samples) {
12
+ const normalized = (sample - 128) / 128;
13
+ sumSquares += normalized * normalized;
14
+ }
15
+ const rms = Math.sqrt(sumSquares / samples.length);
16
+ return clampLevel(Math.max(0, rms - 0.012) * 3.2);
17
+ }
18
+ export function smoothRealtimeVoiceLevel(previous, next) {
19
+ const target = clampLevel(next);
20
+ const factor = target > previous ? 0.55 : 0.2;
21
+ return clampLevel(previous + (target - previous) * factor);
22
+ }
23
+ export function createRealtimeVoiceAudioLevelStore() {
24
+ let snapshot = SILENT_LEVELS;
25
+ const listeners = new Set();
26
+ const set = (levels) => {
27
+ const next = {
28
+ input: clampLevel(levels.input),
29
+ output: clampLevel(levels.output),
30
+ };
31
+ if (next.input === snapshot.input && next.output === snapshot.output)
32
+ return;
33
+ snapshot = next;
34
+ for (const listener of listeners)
35
+ listener();
36
+ };
37
+ return {
38
+ getSnapshot: () => snapshot,
39
+ subscribe(listener) {
40
+ listeners.add(listener);
41
+ return () => listeners.delete(listener);
42
+ },
43
+ set,
44
+ reset: () => set(SILENT_LEVELS),
45
+ };
46
+ }
47
+ //# sourceMappingURL=realtime-voice-audio-level.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"realtime-voice-audio-level.js","sourceRoot":"","sources":["../../../src/client/composer/realtime-voice-audio-level.ts"],"names":[],"mappings":"AAYA,MAAM,aAAa,GAA6B,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC;AAExE,SAAS,UAAU,CAAC,KAAa;IAC/B,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,OAAO,CAAC,CAAC;IACtC,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC;AACzC,CAAC;AAED,MAAM,UAAU,yBAAyB,CAAC,OAAmB;IAC3D,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,CAAC,CAAC;IACnC,IAAI,UAAU,GAAG,CAAC,CAAC;IACnB,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC7B,MAAM,UAAU,GAAG,CAAC,MAAM,GAAG,GAAG,CAAC,GAAG,GAAG,CAAC;QACxC,UAAU,IAAI,UAAU,GAAG,UAAU,CAAC;IACxC,CAAC;IACD,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACnD,OAAO,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,GAAG,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC;AACpD,CAAC;AAED,MAAM,UAAU,wBAAwB,CACtC,QAAgB,EAChB,IAAY;IAEZ,MAAM,MAAM,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC;IAChC,MAAM,MAAM,GAAG,MAAM,GAAG,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC;IAC9C,OAAO,UAAU,CAAC,QAAQ,GAAG,CAAC,MAAM,GAAG,QAAQ,CAAC,GAAG,MAAM,CAAC,CAAC;AAC7D,CAAC;AAED,MAAM,UAAU,kCAAkC;IAChD,IAAI,QAAQ,GAAG,aAAa,CAAC;IAC7B,MAAM,SAAS,GAAG,IAAI,GAAG,EAAc,CAAC;IAExC,MAAM,GAAG,GAAG,CAAC,MAAgC,EAAE,EAAE;QAC/C,MAAM,IAAI,GAAG;YACX,KAAK,EAAE,UAAU,CAAC,MAAM,CAAC,KAAK,CAAC;YAC/B,MAAM,EAAE,UAAU,CAAC,MAAM,CAAC,MAAM,CAAC;SAClC,CAAC;QACF,IAAI,IAAI,CAAC,KAAK,KAAK,QAAQ,CAAC,KAAK,IAAI,IAAI,CAAC,MAAM,KAAK,QAAQ,CAAC,MAAM;YAClE,OAAO;QACT,QAAQ,GAAG,IAAI,CAAC;QAChB,KAAK,MAAM,QAAQ,IAAI,SAAS;YAAE,QAAQ,EAAE,CAAC;IAC/C,CAAC,CAAC;IAEF,OAAO;QACL,WAAW,EAAE,GAAG,EAAE,CAAC,QAAQ;QAC3B,SAAS,CAAC,QAAQ;YAChB,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;YACxB,OAAO,GAAG,EAAE,CAAC,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QAC1C,CAAC;QACD,GAAG;QACH,KAAK,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,aAAa,CAAC;KAChC,CAAC;AACJ,CAAC","sourcesContent":["export interface RealtimeVoiceAudioLevels {\n input: number;\n output: number;\n}\n\nexport interface RealtimeVoiceAudioLevelStore {\n getSnapshot: () => RealtimeVoiceAudioLevels;\n subscribe: (listener: () => void) => () => void;\n set: (levels: RealtimeVoiceAudioLevels) => void;\n reset: () => void;\n}\n\nconst SILENT_LEVELS: RealtimeVoiceAudioLevels = { input: 0, output: 0 };\n\nfunction clampLevel(value: number): number {\n if (!Number.isFinite(value)) return 0;\n return Math.max(0, Math.min(1, value));\n}\n\nexport function normalizeRealtimeVoiceRms(samples: Uint8Array): number {\n if (samples.length === 0) return 0;\n let sumSquares = 0;\n for (const sample of samples) {\n const normalized = (sample - 128) / 128;\n sumSquares += normalized * normalized;\n }\n const rms = Math.sqrt(sumSquares / samples.length);\n return clampLevel(Math.max(0, rms - 0.012) * 3.2);\n}\n\nexport function smoothRealtimeVoiceLevel(\n previous: number,\n next: number,\n): number {\n const target = clampLevel(next);\n const factor = target > previous ? 0.55 : 0.2;\n return clampLevel(previous + (target - previous) * factor);\n}\n\nexport function createRealtimeVoiceAudioLevelStore(): RealtimeVoiceAudioLevelStore {\n let snapshot = SILENT_LEVELS;\n const listeners = new Set<() => void>();\n\n const set = (levels: RealtimeVoiceAudioLevels) => {\n const next = {\n input: clampLevel(levels.input),\n output: clampLevel(levels.output),\n };\n if (next.input === snapshot.input && next.output === snapshot.output)\n return;\n snapshot = next;\n for (const listener of listeners) listener();\n };\n\n return {\n getSnapshot: () => snapshot,\n subscribe(listener) {\n listeners.add(listener);\n return () => listeners.delete(listener);\n },\n set,\n reset: () => set(SILENT_LEVELS),\n };\n}\n"]}
@@ -0,0 +1,26 @@
1
+ export type RealtimeVoiceTranscriptRole = "user" | "assistant";
2
+ export interface RealtimeVoiceTranscriptMessage {
3
+ id: string;
4
+ threadId: string;
5
+ role: RealtimeVoiceTranscriptRole;
6
+ text: string;
7
+ createdAt: string;
8
+ }
9
+ export interface RealtimeVoiceTranscriptSink {
10
+ threadId: string;
11
+ active: boolean;
12
+ append: (message: RealtimeVoiceTranscriptMessage) => boolean;
13
+ }
14
+ export interface RealtimeVoiceTranscriptRegistry {
15
+ activeThreadId: () => string | undefined;
16
+ publish: (message: RealtimeVoiceTranscriptMessage) => void;
17
+ register: (sink: RealtimeVoiceTranscriptSink) => () => void;
18
+ pendingCount: () => number;
19
+ }
20
+ export declare function appendRealtimeVoiceTranscriptToRepository(value: unknown, transcript: RealtimeVoiceTranscriptMessage): {
21
+ appended: boolean;
22
+ repository: Record<string, unknown>;
23
+ };
24
+ export declare function createRealtimeVoiceTranscriptRegistry(): RealtimeVoiceTranscriptRegistry;
25
+ export declare const realtimeVoiceTranscriptRegistry: RealtimeVoiceTranscriptRegistry;
26
+ //# sourceMappingURL=realtime-voice-transcript.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"realtime-voice-transcript.d.ts","sourceRoot":"","sources":["../../../src/client/composer/realtime-voice-transcript.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,2BAA2B,GAAG,MAAM,GAAG,WAAW,CAAC;AAE/D,MAAM,WAAW,8BAA8B;IAC7C,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,2BAA2B,CAAC;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,2BAA2B;IAC1C,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,OAAO,CAAC;IAChB,MAAM,EAAE,CAAC,OAAO,EAAE,8BAA8B,KAAK,OAAO,CAAC;CAC9D;AAED,MAAM,WAAW,+BAA+B;IAC9C,cAAc,EAAE,MAAM,MAAM,GAAG,SAAS,CAAC;IACzC,OAAO,EAAE,CAAC,OAAO,EAAE,8BAA8B,KAAK,IAAI,CAAC;IAC3D,QAAQ,EAAE,CAAC,IAAI,EAAE,2BAA2B,KAAK,MAAM,IAAI,CAAC;IAC5D,YAAY,EAAE,MAAM,MAAM,CAAC;CAC5B;AAED,wBAAgB,yCAAyC,CACvD,KAAK,EAAE,OAAO,EACd,UAAU,EAAE,8BAA8B,GACzC;IAAE,QAAQ,EAAE,OAAO,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,CA6C5D;AAED,wBAAgB,qCAAqC,IAAI,+BAA+B,CAyDvF;AAED,eAAO,MAAM,+BAA+B,iCACH,CAAC"}
@@ -0,0 +1,103 @@
1
+ export function appendRealtimeVoiceTranscriptToRepository(value, transcript) {
2
+ const repository = value && typeof value === "object" && !Array.isArray(value)
3
+ ? value
4
+ : {};
5
+ const messages = Array.isArray(repository.messages)
6
+ ? [...repository.messages]
7
+ : [];
8
+ const duplicate = messages.some((entry) => {
9
+ if (!entry || typeof entry !== "object")
10
+ return false;
11
+ const record = entry;
12
+ const message = record.message && typeof record.message === "object"
13
+ ? record.message
14
+ : record;
15
+ return message.id === transcript.id;
16
+ });
17
+ if (duplicate)
18
+ return { appended: false, repository };
19
+ const parentId = typeof repository.headId === "string" ? repository.headId : null;
20
+ const createdAt = new Date(transcript.createdAt);
21
+ const message = {
22
+ id: transcript.id,
23
+ role: transcript.role,
24
+ content: [{ type: "text", text: transcript.text }],
25
+ createdAt: Number.isNaN(createdAt.getTime()) ? new Date() : createdAt,
26
+ metadata: {
27
+ custom: {
28
+ source: "realtime-voice",
29
+ },
30
+ },
31
+ ...(transcript.role === "assistant"
32
+ ? { status: { type: "complete", reason: "stop" } }
33
+ : {}),
34
+ };
35
+ return {
36
+ appended: true,
37
+ repository: {
38
+ ...repository,
39
+ messages: [...messages, { message, parentId }],
40
+ headId: transcript.id,
41
+ },
42
+ };
43
+ }
44
+ export function createRealtimeVoiceTranscriptRegistry() {
45
+ const sinks = new Map();
46
+ const pending = new Map();
47
+ const delivered = new Set();
48
+ let order = 0;
49
+ const flush = () => {
50
+ for (const [id, message] of pending) {
51
+ let consumed = false;
52
+ for (const sink of sinks.values()) {
53
+ if (sink.threadId !== message.threadId)
54
+ continue;
55
+ try {
56
+ consumed = sink.append(message) || consumed;
57
+ }
58
+ catch {
59
+ // Keep the completed transcript queued until the chat can import it.
60
+ }
61
+ }
62
+ if (!consumed)
63
+ continue;
64
+ pending.delete(id);
65
+ delivered.add(id);
66
+ if (delivered.size > 1_000) {
67
+ const oldest = delivered.values().next().value;
68
+ if (typeof oldest === "string")
69
+ delivered.delete(oldest);
70
+ }
71
+ }
72
+ };
73
+ return {
74
+ activeThreadId() {
75
+ let active;
76
+ for (const sink of sinks.values()) {
77
+ if (!sink.active || (active && sink.order <= active.order))
78
+ continue;
79
+ active = sink;
80
+ }
81
+ return active?.threadId;
82
+ },
83
+ publish(message) {
84
+ const id = message.id.trim();
85
+ const threadId = message.threadId.trim();
86
+ const text = message.text.trim();
87
+ if (!id || !threadId || !text || delivered.has(id) || pending.has(id)) {
88
+ return;
89
+ }
90
+ pending.set(id, { ...message, id, threadId, text });
91
+ flush();
92
+ },
93
+ register(sink) {
94
+ const token = Symbol("realtime-voice-transcript-sink");
95
+ sinks.set(token, { ...sink, order: ++order });
96
+ flush();
97
+ return () => sinks.delete(token);
98
+ },
99
+ pendingCount: () => pending.size,
100
+ };
101
+ }
102
+ export const realtimeVoiceTranscriptRegistry = createRealtimeVoiceTranscriptRegistry();
103
+ //# sourceMappingURL=realtime-voice-transcript.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"realtime-voice-transcript.js","sourceRoot":"","sources":["../../../src/client/composer/realtime-voice-transcript.ts"],"names":[],"mappings":"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","sourcesContent":["export type RealtimeVoiceTranscriptRole = \"user\" | \"assistant\";\n\nexport interface RealtimeVoiceTranscriptMessage {\n id: string;\n threadId: string;\n role: RealtimeVoiceTranscriptRole;\n text: string;\n createdAt: string;\n}\n\nexport interface RealtimeVoiceTranscriptSink {\n threadId: string;\n active: boolean;\n append: (message: RealtimeVoiceTranscriptMessage) => boolean;\n}\n\nexport interface RealtimeVoiceTranscriptRegistry {\n activeThreadId: () => string | undefined;\n publish: (message: RealtimeVoiceTranscriptMessage) => void;\n register: (sink: RealtimeVoiceTranscriptSink) => () => void;\n pendingCount: () => number;\n}\n\nexport function appendRealtimeVoiceTranscriptToRepository(\n value: unknown,\n transcript: RealtimeVoiceTranscriptMessage,\n): { appended: boolean; repository: Record<string, unknown> } {\n const repository =\n value && typeof value === \"object\" && !Array.isArray(value)\n ? (value as Record<string, unknown>)\n : {};\n const messages = Array.isArray(repository.messages)\n ? [...repository.messages]\n : [];\n const duplicate = messages.some((entry) => {\n if (!entry || typeof entry !== \"object\") return false;\n const record = entry as Record<string, unknown>;\n const message =\n record.message && typeof record.message === \"object\"\n ? (record.message as Record<string, unknown>)\n : record;\n return message.id === transcript.id;\n });\n if (duplicate) return { appended: false, repository };\n\n const parentId =\n typeof repository.headId === \"string\" ? repository.headId : null;\n const createdAt = new Date(transcript.createdAt);\n const message = {\n id: transcript.id,\n role: transcript.role,\n content: [{ type: \"text\", text: transcript.text }],\n createdAt: Number.isNaN(createdAt.getTime()) ? new Date() : createdAt,\n metadata: {\n custom: {\n source: \"realtime-voice\",\n },\n },\n ...(transcript.role === \"assistant\"\n ? { status: { type: \"complete\", reason: \"stop\" } }\n : {}),\n };\n\n return {\n appended: true,\n repository: {\n ...repository,\n messages: [...messages, { message, parentId }],\n headId: transcript.id,\n },\n };\n}\n\nexport function createRealtimeVoiceTranscriptRegistry(): RealtimeVoiceTranscriptRegistry {\n const sinks = new Map<\n symbol,\n RealtimeVoiceTranscriptSink & { order: number }\n >();\n const pending = new Map<string, RealtimeVoiceTranscriptMessage>();\n const delivered = new Set<string>();\n let order = 0;\n\n const flush = () => {\n for (const [id, message] of pending) {\n let consumed = false;\n for (const sink of sinks.values()) {\n if (sink.threadId !== message.threadId) continue;\n try {\n consumed = sink.append(message) || consumed;\n } catch {\n // Keep the completed transcript queued until the chat can import it.\n }\n }\n if (!consumed) continue;\n pending.delete(id);\n delivered.add(id);\n if (delivered.size > 1_000) {\n const oldest = delivered.values().next().value;\n if (typeof oldest === \"string\") delivered.delete(oldest);\n }\n }\n };\n\n return {\n activeThreadId() {\n let active: (RealtimeVoiceTranscriptSink & { order: number }) | undefined;\n for (const sink of sinks.values()) {\n if (!sink.active || (active && sink.order <= active.order)) continue;\n active = sink;\n }\n return active?.threadId;\n },\n publish(message) {\n const id = message.id.trim();\n const threadId = message.threadId.trim();\n const text = message.text.trim();\n if (!id || !threadId || !text || delivered.has(id) || pending.has(id)) {\n return;\n }\n pending.set(id, { ...message, id, threadId, text });\n flush();\n },\n register(sink) {\n const token = Symbol(\"realtime-voice-transcript-sink\");\n sinks.set(token, { ...sink, order: ++order });\n flush();\n return () => sinks.delete(token);\n },\n pendingCount: () => pending.size,\n };\n}\n\nexport const realtimeVoiceTranscriptRegistry =\n createRealtimeVoiceTranscriptRegistry();\n"]}
@@ -1,4 +1,5 @@
1
1
  import { type ReactNode } from "react";
2
+ import { type RealtimeVoiceAudioLevelStore } from "./realtime-voice-audio-level.js";
2
3
  import { type RealtimeVoiceModeCopy, type RealtimeVoiceModeState } from "./RealtimeVoiceMode.js";
3
4
  type RealtimeServerEvent = Record<string, unknown> & {
4
5
  type?: string;
@@ -14,10 +15,23 @@ export interface RealtimeVoiceModeApi {
14
15
  active: boolean;
15
16
  errorMessage: string | null;
16
17
  chatVisible: boolean;
18
+ audioLevels: RealtimeVoiceAudioLevelStore;
17
19
  start: () => Promise<void>;
18
20
  end: () => void;
19
21
  toggleChat: () => void;
20
22
  }
23
+ export interface CompletedRealtimeVoiceTranscript {
24
+ role: "user" | "assistant";
25
+ text: string;
26
+ providerId?: string;
27
+ }
28
+ /**
29
+ * Voice mode owns the chat only temporarily. Restore the captured transcript
30
+ * when it is still the user's active thread (or the chat has no active thread)
31
+ * but never pull them back after they deliberately selected another thread.
32
+ */
33
+ export declare function shouldRestoreRealtimeVoiceTranscriptThread(transcriptThreadId: string | undefined, activeThreadId: string | undefined): transcriptThreadId is string;
34
+ export declare function extractCompletedRealtimeVoiceTranscript(event: RealtimeServerEvent): CompletedRealtimeVoiceTranscript | null;
21
35
  export interface RealtimeVoiceModeProviderProps {
22
36
  children: ReactNode;
23
37
  browserTabId?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"useRealtimeVoiceMode.d.ts","sourceRoot":"","sources":["../../../src/client/composer/useRealtimeVoiceMode.tsx"],"names":[],"mappings":"AAAA,OAAO,EAQL,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AAWf,OAAO,EAEL,KAAK,qBAAqB,EAC1B,KAAK,sBAAsB,EAC5B,MAAM,wBAAwB,CAAC;AAOhC,KAAK,mBAAmB,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAEvE,MAAM,WAAW,uBAAuB;IACtC,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,WAAW,GAAG,QAAQ,GAAG,mBAAmB,CAAC;IACrD,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,MAAM,GAAG,sBAAsB,CAAC;IACvC,MAAM,EAAE,OAAO,CAAC;IAChB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,WAAW,EAAE,OAAO,CAAC;IACrB,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3B,GAAG,EAAE,MAAM,IAAI,CAAC;IAChB,UAAU,EAAE,MAAM,IAAI,CAAC;CACxB;AAED,MAAM,WAAW,8BAA8B;IAC7C,QAAQ,EAAE,SAAS,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAiBD,wBAAsB,0BAA0B,CAC9C,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE;IAAE,YAAY,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,WAAW,CAAA;CAAO,GAC5D,OAAO,CAAC,MAAM,CAAC,CAoBjB;AAED,wBAAsB,wBAAwB,CAAC,KAAK,EAAE;IACpD,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,GAAG,OAAO,CAAC,uBAAuB,CAAC,CAuBnC;AAED,wBAAgB,iCAAiC,CAC/C,KAAK,EAAE,mBAAmB,GACzB,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,aAAa,EAAE,MAAM,CAAA;CAAE,CAAC,CAmChE;AA0DD,wBAAgB,wBAAwB,IAAI,qBAAqB,CAGhE;AAqUD,wBAAgB,yBAAyB,CAAC,EACxC,QAAQ,EACR,YAAY,GACb,EAAE,8BAA8B,+BA2BhC;AAED;;;GAGG;AACH,wBAAgB,yBAAyB,CAAC,EACxC,QAAQ,EACR,YAAY,GACb,EAAE,8BAA8B,+BAgBhC;AAWD,wBAAgB,oBAAoB,IAAI,oBAAoB,CAQ3D;AAED,wBAAgB,4BAA4B,IAAI,oBAAoB,GAAG,IAAI,CAE1E;AAED,wBAAsB,wBAAwB,IAAI,OAAO,CAAC;IACxD,UAAU,EAAE,OAAO,CAAC;IACpB,GAAG,EAAE,OAAO,CAAC;CACd,CAAC,CAMD"}
1
+ {"version":3,"file":"useRealtimeVoiceMode.d.ts","sourceRoot":"","sources":["../../../src/client/composer/useRealtimeVoiceMode.tsx"],"names":[],"mappings":"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"}
@@ -1,16 +1,43 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, } from "react";
3
3
  import { createPortal } from "react-dom";
4
+ import { requestAgentChatThreadOpen } from "../agent-chat.js";
4
5
  import { SIDEBAR_STATE_CHANGE_EVENT, } from "../agent-sidebar-state.js";
5
6
  import { agentNativePath } from "../api-path.js";
6
7
  import { readClientAppState, setClientAppState } from "../application-state.js";
7
8
  import { getBrowserTabId } from "../browser-tab-id.js";
8
9
  import { useT } from "../i18n.js";
10
+ import { createRealtimeVoiceAudioLevelStore, normalizeRealtimeVoiceRms, smoothRealtimeVoiceLevel, } from "./realtime-voice-audio-level.js";
11
+ import { realtimeVoiceTranscriptRegistry } from "./realtime-voice-transcript.js";
9
12
  import { RealtimeVoiceModeDock, } from "./RealtimeVoiceMode.js";
10
13
  const REALTIME_VOICE_STATE_KEY = "realtime-voice-session";
11
14
  const REALTIME_VOICE_REQUEST_SOURCE = "realtime-voice";
12
15
  const REALTIME_VOICE_SESSION_PATH = "/_agent-native/realtime-voice/session";
13
16
  const REALTIME_VOICE_TOOL_PATH = "/_agent-native/realtime-voice/tool";
17
+ /**
18
+ * Voice mode owns the chat only temporarily. Restore the captured transcript
19
+ * when it is still the user's active thread (or the chat has no active thread)
20
+ * but never pull them back after they deliberately selected another thread.
21
+ */
22
+ export function shouldRestoreRealtimeVoiceTranscriptThread(transcriptThreadId, activeThreadId) {
23
+ return Boolean(transcriptThreadId &&
24
+ (!activeThreadId || activeThreadId === transcriptThreadId));
25
+ }
26
+ export function extractCompletedRealtimeVoiceTranscript(event) {
27
+ const userCompleted = event.type === "conversation.item.input_audio_transcription.completed";
28
+ const assistantCompleted = event.type === "response.output_audio_transcript.done";
29
+ if (!userCompleted && !assistantCompleted)
30
+ return null;
31
+ const text = typeof event.transcript === "string" ? event.transcript.trim() : "";
32
+ if (!text)
33
+ return null;
34
+ const providerId = [event.item_id, event.response_id, event.event_id].find((value) => typeof value === "string" && value.length > 0);
35
+ return {
36
+ role: userCompleted ? "user" : "assistant",
37
+ text,
38
+ ...(providerId ? { providerId } : {}),
39
+ };
40
+ }
14
41
  function errorMessage(error) {
15
42
  return error instanceof Error ? error.message : String(error);
16
43
  }
@@ -169,17 +196,29 @@ function useRealtimeVoiceModeController(browserTabId, copy) {
169
196
  const [state, setState] = useState("idle");
170
197
  const [error, setError] = useState(null);
171
198
  const [chatVisible, setChatVisible] = useState(false);
199
+ const [audioLevels] = useState(createRealtimeVoiceAudioLevelStore);
172
200
  const stateRef = useRef(state);
173
201
  const peerRef = useRef(null);
174
202
  const channelRef = useRef(null);
175
203
  const streamRef = useRef(null);
176
204
  const audioRef = useRef(null);
205
+ const audioContextRef = useRef(null);
206
+ const inputAnalyserRef = useRef(null);
207
+ const outputAnalyserRef = useRef(null);
208
+ const inputMeterBufferRef = useRef(null);
209
+ const outputMeterBufferRef = useRef(null);
210
+ const inputSourceRef = useRef(null);
211
+ const outputSourceRef = useRef(null);
212
+ const meterFrameRef = useRef(null);
213
+ const lastMeterSampleRef = useRef(0);
177
214
  const abortRef = useRef(null);
178
215
  const handledCallsRef = useRef(new Set());
179
216
  const sessionIdRef = useRef(undefined);
180
217
  const startedAtRef = useRef(undefined);
181
218
  const lastUserTextRef = useRef("");
182
219
  const lastAssistantTextRef = useRef("");
220
+ const transcriptThreadIdRef = useRef(undefined);
221
+ const transcriptSequenceRef = useRef(0);
183
222
  useEffect(() => {
184
223
  stateRef.current = state;
185
224
  }, [state]);
@@ -205,6 +244,65 @@ function useRealtimeVoiceModeController(browserTabId, copy) {
205
244
  setState(nextState);
206
245
  syncAppState(nextState);
207
246
  }, [syncAppState]);
247
+ const startMeterLoop = useCallback(() => {
248
+ if (meterFrameRef.current !== null)
249
+ return;
250
+ const sample = (timestamp) => {
251
+ meterFrameRef.current = requestAnimationFrame(sample);
252
+ if (timestamp - lastMeterSampleRef.current < 50)
253
+ return;
254
+ lastMeterSampleRef.current = timestamp;
255
+ const current = audioLevels.getSnapshot();
256
+ let input = current.input;
257
+ let output = current.output;
258
+ const inputAnalyser = inputAnalyserRef.current;
259
+ const inputBuffer = inputMeterBufferRef.current;
260
+ if (inputAnalyser && inputBuffer) {
261
+ inputAnalyser.getByteTimeDomainData(inputBuffer);
262
+ input = smoothRealtimeVoiceLevel(input, normalizeRealtimeVoiceRms(inputBuffer));
263
+ }
264
+ const outputAnalyser = outputAnalyserRef.current;
265
+ const outputBuffer = outputMeterBufferRef.current;
266
+ if (outputAnalyser && outputBuffer) {
267
+ outputAnalyser.getByteTimeDomainData(outputBuffer);
268
+ output = smoothRealtimeVoiceLevel(output, normalizeRealtimeVoiceRms(outputBuffer));
269
+ }
270
+ audioLevels.set({ input, output });
271
+ };
272
+ meterFrameRef.current = requestAnimationFrame(sample);
273
+ }, [audioLevels]);
274
+ const attachAudioMeter = useCallback((stream, channel) => {
275
+ try {
276
+ const AudioCtor = window.AudioContext ??
277
+ window.webkitAudioContext;
278
+ if (!AudioCtor)
279
+ return;
280
+ const context = audioContextRef.current ?? new AudioCtor();
281
+ audioContextRef.current = context;
282
+ void context.resume().catch(() => undefined);
283
+ const analyser = context.createAnalyser();
284
+ analyser.fftSize = 512;
285
+ const source = context.createMediaStreamSource(stream);
286
+ source.connect(analyser);
287
+ const buffer = new Uint8Array(analyser.frequencyBinCount);
288
+ if (channel === "input") {
289
+ inputSourceRef.current?.disconnect();
290
+ inputSourceRef.current = source;
291
+ inputAnalyserRef.current = analyser;
292
+ inputMeterBufferRef.current = buffer;
293
+ }
294
+ else {
295
+ outputSourceRef.current?.disconnect();
296
+ outputSourceRef.current = source;
297
+ outputAnalyserRef.current = analyser;
298
+ outputMeterBufferRef.current = buffer;
299
+ }
300
+ startMeterLoop();
301
+ }
302
+ catch {
303
+ // Audio metering is visual-only; keep the realtime call healthy.
304
+ }
305
+ }, [startMeterLoop]);
208
306
  const cleanupTransport = useCallback(() => {
209
307
  abortRef.current?.abort();
210
308
  abortRef.current = null;
@@ -220,8 +318,26 @@ function useRealtimeVoiceModeController(browserTabId, copy) {
220
318
  audioRef.current.srcObject = null;
221
319
  }
222
320
  audioRef.current = null;
321
+ if (meterFrameRef.current !== null) {
322
+ cancelAnimationFrame(meterFrameRef.current);
323
+ meterFrameRef.current = null;
324
+ }
325
+ inputSourceRef.current?.disconnect();
326
+ outputSourceRef.current?.disconnect();
327
+ inputSourceRef.current = null;
328
+ outputSourceRef.current = null;
329
+ inputAnalyserRef.current = null;
330
+ outputAnalyserRef.current = null;
331
+ inputMeterBufferRef.current = null;
332
+ outputMeterBufferRef.current = null;
333
+ lastMeterSampleRef.current = 0;
334
+ const audioContext = audioContextRef.current;
335
+ audioContextRef.current = null;
336
+ if (audioContext)
337
+ void audioContext.close().catch(() => undefined);
338
+ audioLevels.reset();
223
339
  handledCallsRef.current.clear();
224
- }, []);
340
+ }, [audioLevels]);
225
341
  const fail = useCallback((message, options) => {
226
342
  cleanupTransport();
227
343
  setError(message);
@@ -263,6 +379,21 @@ function useRealtimeVoiceModeController(browserTabId, copy) {
263
379
  });
264
380
  sendDataChannelEvent(channelRef.current, { type: "response.create" });
265
381
  }, [browserTabId, transition]);
382
+ const persistCompletedTranscript = useCallback((event) => {
383
+ const transcript = extractCompletedRealtimeVoiceTranscript(event);
384
+ const threadId = transcriptThreadIdRef.current;
385
+ if (!transcript || !threadId)
386
+ return;
387
+ const sessionIdentity = sessionIdRef.current ?? startedAtRef.current ?? "pending";
388
+ const providerIdentity = transcript.providerId ?? `sequence-${++transcriptSequenceRef.current}`;
389
+ realtimeVoiceTranscriptRegistry.publish({
390
+ id: `realtime-voice:${sessionIdentity}:${transcript.role}:${providerIdentity}`,
391
+ threadId,
392
+ role: transcript.role,
393
+ text: transcript.text,
394
+ createdAt: new Date().toISOString(),
395
+ });
396
+ }, []);
266
397
  const handleServerEvent = useCallback((event) => {
267
398
  if (event.type === "session.created") {
268
399
  const session = event.session;
@@ -293,13 +424,15 @@ function useRealtimeVoiceModeController(browserTabId, copy) {
293
424
  if (typeof event.transcript === "string") {
294
425
  lastAssistantTextRef.current = event.transcript;
295
426
  }
427
+ persistCompletedTranscript(event);
296
428
  syncAppState("speaking");
297
429
  }
298
430
  else if (event.type === "conversation.item.input_audio_transcription.completed") {
299
431
  if (typeof event.transcript === "string") {
300
432
  lastUserTextRef.current = event.transcript;
301
- syncAppState("working");
302
433
  }
434
+ persistCompletedTranscript(event);
435
+ syncAppState("working");
303
436
  }
304
437
  else if (event.type === "response.done") {
305
438
  const response = event.response;
@@ -330,7 +463,14 @@ function useRealtimeVoiceModeController(browserTabId, copy) {
330
463
  if (event.type === "response.done" && calls.length === 0) {
331
464
  transition("listening");
332
465
  }
333
- }, [copy, fail, handleFunctionCall, syncAppState, transition]);
466
+ }, [
467
+ copy,
468
+ fail,
469
+ handleFunctionCall,
470
+ persistCompletedTranscript,
471
+ syncAppState,
472
+ transition,
473
+ ]);
334
474
  const start = useCallback(async () => {
335
475
  if (stateRef.current !== "idle")
336
476
  return;
@@ -345,6 +485,9 @@ function useRealtimeVoiceModeController(browserTabId, copy) {
345
485
  lastUserTextRef.current = "";
346
486
  lastAssistantTextRef.current = "";
347
487
  sessionIdRef.current = undefined;
488
+ transcriptThreadIdRef.current =
489
+ realtimeVoiceTranscriptRegistry.activeThreadId();
490
+ transcriptSequenceRef.current = 0;
348
491
  transition("connecting");
349
492
  setChatVisible(false);
350
493
  window.dispatchEvent(new Event("agent-panel:close"));
@@ -359,6 +502,7 @@ function useRealtimeVoiceModeController(browserTabId, copy) {
359
502
  },
360
503
  });
361
504
  streamRef.current = stream;
505
+ attachAudioMeter(stream, "input");
362
506
  const peer = new RTCPeerConnection();
363
507
  peerRef.current = peer;
364
508
  for (const track of stream.getAudioTracks())
@@ -368,7 +512,10 @@ function useRealtimeVoiceModeController(browserTabId, copy) {
368
512
  audio.setAttribute("playsinline", "");
369
513
  audioRef.current = audio;
370
514
  peer.ontrack = (trackEvent) => {
371
- audio.srcObject = trackEvent.streams[0] ?? null;
515
+ const remoteStream = trackEvent.streams[0] ?? null;
516
+ audio.srcObject = remoteStream;
517
+ if (remoteStream)
518
+ attachAudioMeter(remoteStream, "output");
372
519
  void audio.play().catch(() => undefined);
373
520
  };
374
521
  const channel = peer.createDataChannel("oai-events");
@@ -408,15 +555,38 @@ function useRealtimeVoiceModeController(browserTabId, copy) {
408
555
  const status = startError?.status;
409
556
  fail(errorMessage(startError), { openKeySettings: status === 400 });
410
557
  }
411
- }, [browserTabId, copy, fail, handleServerEvent, transition]);
558
+ }, [
559
+ attachAudioMeter,
560
+ browserTabId,
561
+ copy,
562
+ fail,
563
+ handleServerEvent,
564
+ transition,
565
+ ]);
412
566
  const end = useCallback(() => {
413
567
  if (stateRef.current === "idle" || stateRef.current === "ending")
414
568
  return;
569
+ const transcriptThreadId = transcriptThreadIdRef.current;
570
+ const activeThreadId = realtimeVoiceTranscriptRegistry.activeThreadId();
415
571
  transition("ending");
416
572
  cleanupTransport();
417
573
  setError(null);
418
574
  sessionIdRef.current = undefined;
419
575
  startedAtRef.current = undefined;
576
+ transcriptThreadIdRef.current = undefined;
577
+ setChatVisible(true);
578
+ if (shouldRestoreRealtimeVoiceTranscriptThread(transcriptThreadId, activeThreadId)) {
579
+ requestAgentChatThreadOpen({
580
+ threadId: transcriptThreadId,
581
+ // The request is delivered asynchronously. Re-checking this at the
582
+ // receiver prevents a navigation that happened during that gap from
583
+ // being overwritten.
584
+ onlyIfActiveThreadId: transcriptThreadId,
585
+ });
586
+ }
587
+ else {
588
+ window.dispatchEvent(new Event("agent-panel:open"));
589
+ }
420
590
  transition("idle");
421
591
  }, [cleanupTransport, transition]);
422
592
  const toggleChat = useCallback(() => {
@@ -440,6 +610,7 @@ function useRealtimeVoiceModeController(browserTabId, copy) {
440
610
  active: state !== "idle",
441
611
  errorMessage: error,
442
612
  chatVisible,
613
+ audioLevels,
443
614
  start,
444
615
  end,
445
616
  toggleChat,
@@ -451,7 +622,7 @@ export function RealtimeVoiceModeProvider({ children, browserTabId, }) {
451
622
  const copy = useRealtimeVoiceModeCopy();
452
623
  const voice = useRealtimeVoiceModeController(resolvedBrowserTabId, copy);
453
624
  return (_jsxs(RealtimeVoiceModeContext.Provider, { value: voice, children: [children, voice.active && typeof document !== "undefined"
454
- ? createPortal(_jsx(RealtimeVoiceModeDock, { state: voice.state === "idle" ? "ending" : voice.state, copy: copy, chatVisible: voice.chatVisible, onToggleChat: voice.toggleChat, onEndVoiceMode: voice.end, errorMessage: voice.errorMessage, className: "z-[270]" }), document.body)
625
+ ? createPortal(_jsx(RealtimeVoiceModeDock, { state: voice.state === "idle" ? "ending" : voice.state, copy: copy, chatVisible: voice.chatVisible, audioLevels: voice.audioLevels, onToggleChat: voice.toggleChat, onEndVoiceMode: voice.end, errorMessage: voice.errorMessage }), document.body)
455
626
  : null] }));
456
627
  }
457
628
  /**