@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.
- package/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +14 -0
- package/corpus/core/docs/content/cloneable-saas.mdx +15 -15
- package/corpus/core/docs/content/creating-templates.mdx +1 -1
- package/corpus/core/docs/content/extensions.mdx +1 -1
- package/corpus/core/docs/content/local-file-mode.mdx +6 -6
- package/corpus/core/docs/content/locales/ar-SA/voice-input.mdx +2 -2
- package/corpus/core/docs/content/locales/de-DE/voice-input.mdx +2 -2
- package/corpus/core/docs/content/locales/es-ES/voice-input.mdx +2 -2
- package/corpus/core/docs/content/locales/fr-FR/voice-input.mdx +2 -2
- package/corpus/core/docs/content/locales/hi-IN/voice-input.mdx +2 -2
- package/corpus/core/docs/content/locales/ja-JP/voice-input.mdx +2 -2
- package/corpus/core/docs/content/locales/ko-KR/voice-input.mdx +2 -2
- package/corpus/core/docs/content/locales/pt-BR/voice-input.mdx +2 -2
- package/corpus/core/docs/content/locales/zh-CN/voice-input.mdx +2 -2
- package/corpus/core/docs/content/locales/zh-TW/voice-input.mdx +2 -2
- package/corpus/core/docs/content/sharing.mdx +4 -4
- package/corpus/core/docs/content/template-analytics.mdx +2 -2
- package/corpus/core/docs/content/template-calendar.mdx +4 -4
- package/corpus/core/docs/content/template-clips.mdx +2 -2
- package/corpus/core/docs/content/template-content.mdx +12 -11
- package/corpus/core/docs/content/template-mail.mdx +1 -1
- package/corpus/core/docs/content/template-slides.mdx +2 -2
- package/corpus/core/docs/content/voice-input.mdx +2 -2
- package/corpus/core/docs/content/workspace.mdx +1 -1
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/a2a/artifact-response.ts +154 -1
- package/corpus/core/src/client/AssistantChat.tsx +30 -0
- package/corpus/core/src/client/MultiTabAssistantChat.tsx +19 -1
- package/corpus/core/src/client/agent-chat.ts +6 -0
- package/corpus/core/src/client/composer/RealtimeVoiceMode.tsx +72 -2
- package/corpus/core/src/client/composer/realtime-voice-audio-level.ts +64 -0
- package/corpus/core/src/client/composer/realtime-voice-transcript.ts +134 -0
- package/corpus/core/src/client/composer/useRealtimeVoiceMode.tsx +220 -6
- package/corpus/core/src/client/use-chat-threads.ts +33 -9
- package/corpus/core/src/integrations/a2a-continuation-processor.ts +118 -31
- package/corpus/core/src/integrations/a2a-continuations-store.ts +165 -7
- package/corpus/core/src/integrations/adapters/slack.ts +77 -7
- package/corpus/core/src/integrations/types.ts +30 -0
- package/corpus/core/src/integrations/webhook-handler.ts +54 -18
- package/corpus/core/src/scripts/call-agent.ts +1 -0
- package/corpus/core/src/server/request-context.ts +2 -0
- package/corpus/core/src/templates/workspace-core/.agents/skills/voice-transcription/SKILL.md +11 -5
- package/corpus/templates/clips/app/components/player/comments-panel.tsx +98 -1
- package/corpus/templates/clips/changelog/2026-07-10-comment-urls-open-as-clickable-links.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-10-desktop-recording-uploads-now-show-a-brief-uploaded-state-wi.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-10-desktop-recordings-now-retry-saving-their-locally-captured-t.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-10-desktop-update-prompts-are-more-compact-and-easier-to-dismiss.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-10-quiet-recordings-now-finish-saving-instead-of-getting-stuck.md +6 -0
- package/corpus/templates/clips/desktop/src/components/UpdateBanner.tsx +7 -4
- package/corpus/templates/clips/desktop/src/lib/recorder.ts +284 -85
- package/corpus/templates/clips/desktop/src/overlays/finalizing.tsx +148 -40
- package/corpus/templates/clips/desktop/src/overlays/meeting-notification.tsx +13 -13
- package/corpus/templates/clips/desktop/src/styles.css +83 -13
- package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +43 -28
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +130 -65
- package/corpus/templates/clips/desktop/src-tauri/src/notifications.rs +10 -4
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +82 -1
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/reset-chunks.post.ts +82 -1
- package/dist/a2a/artifact-response.d.ts.map +1 -1
- package/dist/a2a/artifact-response.js +127 -1
- package/dist/a2a/artifact-response.js.map +1 -1
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +20 -0
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
- package/dist/client/MultiTabAssistantChat.js +9 -0
- package/dist/client/MultiTabAssistantChat.js.map +1 -1
- package/dist/client/agent-chat.d.ts +6 -0
- package/dist/client/agent-chat.d.ts.map +1 -1
- package/dist/client/agent-chat.js.map +1 -1
- package/dist/client/composer/RealtimeVoiceMode.d.ts +3 -1
- package/dist/client/composer/RealtimeVoiceMode.d.ts.map +1 -1
- package/dist/client/composer/RealtimeVoiceMode.js +32 -3
- package/dist/client/composer/RealtimeVoiceMode.js.map +1 -1
- package/dist/client/composer/realtime-voice-audio-level.d.ts +14 -0
- package/dist/client/composer/realtime-voice-audio-level.d.ts.map +1 -0
- package/dist/client/composer/realtime-voice-audio-level.js +47 -0
- package/dist/client/composer/realtime-voice-audio-level.js.map +1 -0
- package/dist/client/composer/realtime-voice-transcript.d.ts +26 -0
- package/dist/client/composer/realtime-voice-transcript.d.ts.map +1 -0
- package/dist/client/composer/realtime-voice-transcript.js +103 -0
- package/dist/client/composer/realtime-voice-transcript.js.map +1 -0
- package/dist/client/composer/useRealtimeVoiceMode.d.ts +14 -0
- package/dist/client/composer/useRealtimeVoiceMode.d.ts.map +1 -1
- package/dist/client/composer/useRealtimeVoiceMode.js +177 -6
- package/dist/client/composer/useRealtimeVoiceMode.js.map +1 -1
- package/dist/client/use-chat-threads.d.ts.map +1 -1
- package/dist/client/use-chat-threads.js +26 -6
- package/dist/client/use-chat-threads.js.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/integrations/a2a-continuation-processor.d.ts.map +1 -1
- package/dist/integrations/a2a-continuation-processor.js +76 -21
- package/dist/integrations/a2a-continuation-processor.js.map +1 -1
- package/dist/integrations/a2a-continuations-store.d.ts +4 -1
- package/dist/integrations/a2a-continuations-store.d.ts.map +1 -1
- package/dist/integrations/a2a-continuations-store.js +130 -6
- package/dist/integrations/a2a-continuations-store.js.map +1 -1
- package/dist/integrations/adapters/slack.d.ts.map +1 -1
- package/dist/integrations/adapters/slack.js +56 -7
- package/dist/integrations/adapters/slack.js.map +1 -1
- package/dist/integrations/types.d.ts +25 -0
- package/dist/integrations/types.d.ts.map +1 -1
- package/dist/integrations/types.js.map +1 -1
- package/dist/integrations/webhook-handler.js +54 -16
- package/dist/integrations/webhook-handler.js.map +1 -1
- package/dist/observability/routes.d.ts +1 -1
- package/dist/provider-api/corpus-jobs.d.ts +2 -2
- package/dist/scripts/call-agent.d.ts.map +1 -1
- package/dist/scripts/call-agent.js +1 -0
- package/dist/scripts/call-agent.js.map +1 -1
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/request-context.d.ts +2 -0
- package/dist/server/request-context.d.ts.map +1 -1
- package/dist/server/request-context.js.map +1 -1
- package/dist/templates/workspace-core/.agents/skills/voice-transcription/SKILL.md +11 -5
- package/docs/content/cloneable-saas.mdx +15 -15
- package/docs/content/creating-templates.mdx +1 -1
- package/docs/content/extensions.mdx +1 -1
- package/docs/content/local-file-mode.mdx +6 -6
- package/docs/content/locales/ar-SA/voice-input.mdx +2 -2
- package/docs/content/locales/de-DE/voice-input.mdx +2 -2
- package/docs/content/locales/es-ES/voice-input.mdx +2 -2
- package/docs/content/locales/fr-FR/voice-input.mdx +2 -2
- package/docs/content/locales/hi-IN/voice-input.mdx +2 -2
- package/docs/content/locales/ja-JP/voice-input.mdx +2 -2
- package/docs/content/locales/ko-KR/voice-input.mdx +2 -2
- package/docs/content/locales/pt-BR/voice-input.mdx +2 -2
- package/docs/content/locales/zh-CN/voice-input.mdx +2 -2
- package/docs/content/locales/zh-TW/voice-input.mdx +2 -2
- package/docs/content/sharing.mdx +4 -4
- package/docs/content/template-analytics.mdx +2 -2
- package/docs/content/template-calendar.mdx +4 -4
- package/docs/content/template-clips.mdx +2 -2
- package/docs/content/template-content.mdx +12 -11
- package/docs/content/template-mail.mdx +1 -1
- package/docs/content/template-slides.mdx +2 -2
- package/docs/content/voice-input.mdx +2 -2
- package/docs/content/workspace.mdx +1 -1
- package/package.json +1 -1
- package/src/templates/workspace-core/.agents/skills/voice-transcription/SKILL.md +11 -5
|
@@ -123,6 +123,8 @@ const CLOUD_CAPTURE_MAX_HEIGHT = 1080;
|
|
|
123
123
|
const CLOUD_RECORDING_MAX_LONG_EDGE = 1920;
|
|
124
124
|
const CLOUD_RECORDING_VIDEO_BITRATE_BPS = 8_000_000;
|
|
125
125
|
const CLOUD_RECORDING_AUDIO_BITRATE_BPS = 128_000;
|
|
126
|
+
const TRANSCRIPT_SAVE_TIMEOUT_MS = 8_000;
|
|
127
|
+
const FINALIZING_RESULT_STORAGE_KEY = "clips-finalizing-result";
|
|
126
128
|
|
|
127
129
|
function isMacPlatform(): boolean {
|
|
128
130
|
if (typeof navigator === "undefined") return false;
|
|
@@ -814,14 +816,21 @@ async function postBackupChunk(
|
|
|
814
816
|
async function resetBrowserRecordingBackupUpload(
|
|
815
817
|
meta: BrowserRecordingBackupMeta,
|
|
816
818
|
authToken?: string,
|
|
817
|
-
): Promise<
|
|
819
|
+
): Promise<UploadMode> {
|
|
818
820
|
const res = await fetch(
|
|
819
821
|
`${meta.serverUrl.replace(/\/+$/, "")}/api/uploads/${meta.recordingId}/reset-chunks`,
|
|
820
822
|
{
|
|
821
823
|
method: "POST",
|
|
822
824
|
headers: buildRetryHeaders("application/json", authToken),
|
|
823
825
|
credentials: "include",
|
|
824
|
-
|
|
826
|
+
// A browser backup can be the only remaining copy after a streamed
|
|
827
|
+
// upload failed. Ask the server to recreate its resumable session so a
|
|
828
|
+
// retry still works on hosted deployments, where SQL chunk scratch space
|
|
829
|
+
// is deliberately unavailable.
|
|
830
|
+
body: JSON.stringify({
|
|
831
|
+
requestStreaming: true,
|
|
832
|
+
mimeType: meta.mimeType,
|
|
833
|
+
}),
|
|
825
834
|
},
|
|
826
835
|
);
|
|
827
836
|
if (!res.ok) {
|
|
@@ -830,6 +839,68 @@ async function resetBrowserRecordingBackupUpload(
|
|
|
830
839
|
`Upload retry setup failed (${res.status}): ${body.slice(0, 200)}`,
|
|
831
840
|
);
|
|
832
841
|
}
|
|
842
|
+
const body = (await res.json().catch(() => null)) as {
|
|
843
|
+
uploadMode?: unknown;
|
|
844
|
+
} | null;
|
|
845
|
+
return body?.uploadMode === "streaming" ? "streaming" : "buffered";
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
async function replayBrowserBackupToResumableSession(
|
|
849
|
+
meta: BrowserRecordingBackupMeta,
|
|
850
|
+
chunks: BrowserRecordingBackupChunk[],
|
|
851
|
+
authToken?: string,
|
|
852
|
+
): Promise<void> {
|
|
853
|
+
// The backup is stored in raw MediaRecorder blobs, which have arbitrary
|
|
854
|
+
// boundaries. A resumable provider needs every non-final request aligned,
|
|
855
|
+
// so replay a logical file rather than reusing those blob boundaries.
|
|
856
|
+
const recording = new Blob(
|
|
857
|
+
chunks.map((chunk) => chunk.blob),
|
|
858
|
+
{
|
|
859
|
+
type: meta.mimeType,
|
|
860
|
+
},
|
|
861
|
+
);
|
|
862
|
+
if (recording.size <= 0) {
|
|
863
|
+
throw new Error("Local recording backup is empty");
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
const fullChunks = Math.floor(recording.size / STREAM_CHUNK_BYTES);
|
|
867
|
+
const totalPosts = fullChunks + 1;
|
|
868
|
+
let offset = 0;
|
|
869
|
+
|
|
870
|
+
for (let index = 0; index < fullChunks; index += 1) {
|
|
871
|
+
const body = recording.slice(
|
|
872
|
+
offset,
|
|
873
|
+
offset + STREAM_CHUNK_BYTES,
|
|
874
|
+
meta.mimeType,
|
|
875
|
+
);
|
|
876
|
+
await postBackupChunk(
|
|
877
|
+
chunkUrl(meta.serverUrl, meta.recordingId, index, false, {
|
|
878
|
+
total: String(totalPosts),
|
|
879
|
+
mimeType: meta.mimeType,
|
|
880
|
+
}),
|
|
881
|
+
body,
|
|
882
|
+
authToken,
|
|
883
|
+
);
|
|
884
|
+
offset += STREAM_CHUNK_BYTES;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
// The final post always closes the session. When the file is exactly
|
|
888
|
+
// aligned it is intentionally empty; the route sends the provider's close
|
|
889
|
+
// request with the bytes committed by the previous chunks.
|
|
890
|
+
const finalBody = recording.slice(offset, recording.size, meta.mimeType);
|
|
891
|
+
await postBackupChunk(
|
|
892
|
+
chunkUrl(meta.serverUrl, meta.recordingId, fullChunks, true, {
|
|
893
|
+
total: String(totalPosts),
|
|
894
|
+
mimeType: meta.mimeType,
|
|
895
|
+
durationMs: String(Math.round(meta.durationMs || 0)),
|
|
896
|
+
...(meta.width ? { width: String(meta.width) } : {}),
|
|
897
|
+
...(meta.height ? { height: String(meta.height) } : {}),
|
|
898
|
+
hasAudio: meta.hasAudio ? "1" : "0",
|
|
899
|
+
hasCamera: meta.hasCamera ? "1" : "0",
|
|
900
|
+
}),
|
|
901
|
+
finalBody,
|
|
902
|
+
authToken,
|
|
903
|
+
);
|
|
833
904
|
}
|
|
834
905
|
|
|
835
906
|
export async function retryBrowserRecordingBackup(input: {
|
|
@@ -854,7 +925,39 @@ export async function retryBrowserRecordingBackup(input: {
|
|
|
854
925
|
lastAttemptAt: new Date().toISOString(),
|
|
855
926
|
lastError: null,
|
|
856
927
|
});
|
|
857
|
-
await resetBrowserRecordingBackupUpload(
|
|
928
|
+
const uploadMode = await resetBrowserRecordingBackupUpload(
|
|
929
|
+
meta,
|
|
930
|
+
input.authToken,
|
|
931
|
+
);
|
|
932
|
+
|
|
933
|
+
if (uploadMode === "streaming") {
|
|
934
|
+
try {
|
|
935
|
+
await replayBrowserBackupToResumableSession(
|
|
936
|
+
meta,
|
|
937
|
+
validatedChunks,
|
|
938
|
+
input.authToken,
|
|
939
|
+
);
|
|
940
|
+
} catch (err) {
|
|
941
|
+
if (
|
|
942
|
+
await recoverReadyRecordingAfterFinalizeError({
|
|
943
|
+
serverUrl: meta.serverUrl,
|
|
944
|
+
recordingId: meta.recordingId,
|
|
945
|
+
authToken: input.authToken,
|
|
946
|
+
})
|
|
947
|
+
) {
|
|
948
|
+
return {
|
|
949
|
+
recordingId: meta.recordingId,
|
|
950
|
+
viewUrl: `/r/${meta.recordingId}`,
|
|
951
|
+
};
|
|
952
|
+
}
|
|
953
|
+
throw err;
|
|
954
|
+
}
|
|
955
|
+
await deleteBrowserRecordingBackup(meta.recordingId);
|
|
956
|
+
return {
|
|
957
|
+
recordingId: meta.recordingId,
|
|
958
|
+
viewUrl: `/r/${meta.recordingId}`,
|
|
959
|
+
};
|
|
960
|
+
}
|
|
858
961
|
|
|
859
962
|
const totalPosts = validatedChunks.length + 1;
|
|
860
963
|
for (const chunk of validatedChunks) {
|
|
@@ -1038,9 +1141,9 @@ async function saveRecordingTranscript(
|
|
|
1038
1141
|
recordingId: string,
|
|
1039
1142
|
transcript: CapturedTranscript,
|
|
1040
1143
|
authToken?: string,
|
|
1041
|
-
): Promise<
|
|
1144
|
+
): Promise<boolean> {
|
|
1042
1145
|
const text = transcript.text.trim();
|
|
1043
|
-
if (!text) return;
|
|
1146
|
+
if (!text) return false;
|
|
1044
1147
|
|
|
1045
1148
|
const url = `${serverUrl.replace(/\/+$/, "")}/_agent-native/actions/save-browser-transcript`;
|
|
1046
1149
|
try {
|
|
@@ -1054,6 +1157,7 @@ async function saveRecordingTranscript(
|
|
|
1054
1157
|
segments: transcript.segments,
|
|
1055
1158
|
source: transcript.source ?? "whisper",
|
|
1056
1159
|
}),
|
|
1160
|
+
signal: AbortSignal.timeout(TRANSCRIPT_SAVE_TIMEOUT_MS),
|
|
1057
1161
|
});
|
|
1058
1162
|
if (!res.ok) {
|
|
1059
1163
|
const body = await res.text().catch(() => "");
|
|
@@ -1062,9 +1166,18 @@ async function saveRecordingTranscript(
|
|
|
1062
1166
|
res.status,
|
|
1063
1167
|
body.slice(0, 200),
|
|
1064
1168
|
);
|
|
1169
|
+
return false;
|
|
1065
1170
|
}
|
|
1171
|
+
console.log("[clips-recorder] native transcript saved", {
|
|
1172
|
+
recordingId,
|
|
1173
|
+
source: transcript.source ?? "whisper",
|
|
1174
|
+
chars: text.length,
|
|
1175
|
+
segments: transcript.segments.length,
|
|
1176
|
+
});
|
|
1177
|
+
return true;
|
|
1066
1178
|
} catch (err) {
|
|
1067
1179
|
console.warn("[clips-recorder] save transcript failed:", err);
|
|
1180
|
+
return false;
|
|
1068
1181
|
}
|
|
1069
1182
|
}
|
|
1070
1183
|
|
|
@@ -1073,9 +1186,9 @@ async function saveRecordingTranscriptFailure(
|
|
|
1073
1186
|
recordingId: string,
|
|
1074
1187
|
failureReason: string,
|
|
1075
1188
|
authToken?: string,
|
|
1076
|
-
): Promise<
|
|
1189
|
+
): Promise<boolean> {
|
|
1077
1190
|
const reason = failureReason.trim();
|
|
1078
|
-
if (!reason) return;
|
|
1191
|
+
if (!reason) return false;
|
|
1079
1192
|
|
|
1080
1193
|
const url = `${serverUrl.replace(/\/+$/, "")}/_agent-native/actions/save-browser-transcript`;
|
|
1081
1194
|
try {
|
|
@@ -1089,6 +1202,7 @@ async function saveRecordingTranscriptFailure(
|
|
|
1089
1202
|
source: "whisper",
|
|
1090
1203
|
failureReason: reason,
|
|
1091
1204
|
}),
|
|
1205
|
+
signal: AbortSignal.timeout(TRANSCRIPT_SAVE_TIMEOUT_MS),
|
|
1092
1206
|
});
|
|
1093
1207
|
if (!res.ok) {
|
|
1094
1208
|
const body = await res.text().catch(() => "");
|
|
@@ -1097,12 +1211,15 @@ async function saveRecordingTranscriptFailure(
|
|
|
1097
1211
|
res.status,
|
|
1098
1212
|
body.slice(0, 200),
|
|
1099
1213
|
);
|
|
1214
|
+
return false;
|
|
1100
1215
|
}
|
|
1216
|
+
return true;
|
|
1101
1217
|
} catch (err) {
|
|
1102
1218
|
console.warn(
|
|
1103
1219
|
"[clips-recorder] save native transcript failure failed:",
|
|
1104
1220
|
err,
|
|
1105
1221
|
);
|
|
1222
|
+
return false;
|
|
1106
1223
|
}
|
|
1107
1224
|
}
|
|
1108
1225
|
|
|
@@ -1670,18 +1787,56 @@ async function runRecordingCountdown(wantsScreen: boolean) {
|
|
|
1670
1787
|
}
|
|
1671
1788
|
|
|
1672
1789
|
function showFinalizingFeedback() {
|
|
1790
|
+
// The finalizing window is created asynchronously. Clear the previous
|
|
1791
|
+
// completion record before showing it so a new stop cannot consume an old
|
|
1792
|
+
// result while its event listener is still mounting.
|
|
1793
|
+
try {
|
|
1794
|
+
window.localStorage.removeItem(FINALIZING_RESULT_STORAGE_KEY);
|
|
1795
|
+
} catch {
|
|
1796
|
+
// Storage is a best-effort event-race fallback only.
|
|
1797
|
+
}
|
|
1673
1798
|
invoke("show_finalizing").catch((err) =>
|
|
1674
1799
|
console.error("[clips-recorder] show_finalizing failed:", err),
|
|
1675
1800
|
);
|
|
1676
1801
|
}
|
|
1677
1802
|
|
|
1678
|
-
async function
|
|
1803
|
+
async function clearRecordingState() {
|
|
1679
1804
|
await invoke("set_recording_state", { active: false }).catch((err) =>
|
|
1680
1805
|
console.error("[clips-recorder] clear recording state failed:", err),
|
|
1681
1806
|
);
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
async function publishFinalizingResult(params: {
|
|
1810
|
+
recordingId: string;
|
|
1811
|
+
viewUrl: string;
|
|
1812
|
+
ok: boolean;
|
|
1813
|
+
error?: string;
|
|
1814
|
+
}) {
|
|
1815
|
+
const payload = {
|
|
1816
|
+
recordingId: params.recordingId,
|
|
1817
|
+
viewUrl: params.viewUrl,
|
|
1818
|
+
ok: params.ok,
|
|
1819
|
+
error: params.error ?? null,
|
|
1820
|
+
localFilePath: null,
|
|
1821
|
+
};
|
|
1822
|
+
let persisted = false;
|
|
1823
|
+
try {
|
|
1824
|
+
// Tauri events are not replayed to a window that has not finished mounting
|
|
1825
|
+
// yet. Keep one result long enough for the finalizing window to consume it.
|
|
1826
|
+
window.localStorage.setItem(
|
|
1827
|
+
FINALIZING_RESULT_STORAGE_KEY,
|
|
1828
|
+
JSON.stringify(payload),
|
|
1829
|
+
);
|
|
1830
|
+
persisted = true;
|
|
1831
|
+
} catch {
|
|
1832
|
+
// The event remains the normal delivery path when storage is unavailable.
|
|
1833
|
+
}
|
|
1834
|
+
await emit("clips:native-upload-finished", payload).catch((err) => {
|
|
1835
|
+
console.error("[clips-recorder] finalizing result event failed:", err);
|
|
1836
|
+
if (!persisted) {
|
|
1837
|
+
void invoke("hide_finalizing").catch(() => {});
|
|
1838
|
+
}
|
|
1839
|
+
});
|
|
1685
1840
|
}
|
|
1686
1841
|
|
|
1687
1842
|
async function claimNativeUploadOpen(recordingId: string): Promise<boolean> {
|
|
@@ -1752,10 +1907,13 @@ async function startNativeFullscreenRecording(
|
|
|
1752
1907
|
const wantsSystemAudio = params.systemAudioOn !== false;
|
|
1753
1908
|
const wantsRecordedAudio = wantsAudio || wantsSystemAudio;
|
|
1754
1909
|
let micDeviceLabel: string | null = params.micLabel || null;
|
|
1755
|
-
const saveTranscriptFailure = async (
|
|
1756
|
-
|
|
1910
|
+
const saveTranscriptFailure = async (
|
|
1911
|
+
failureReason: string,
|
|
1912
|
+
): Promise<boolean> => {
|
|
1913
|
+
if (!wantsRecordedAudio || nativeTranscriptFailureSaved || !id)
|
|
1914
|
+
return false;
|
|
1757
1915
|
nativeTranscriptFailureSaved = true;
|
|
1758
|
-
|
|
1916
|
+
return saveRecordingTranscriptFailure(
|
|
1759
1917
|
params.serverUrl,
|
|
1760
1918
|
id,
|
|
1761
1919
|
failureReason,
|
|
@@ -2264,18 +2422,18 @@ async function startNativeFullscreenRecording(
|
|
|
2264
2422
|
console.warn("[clips-recorder] transcript stop failed:", err);
|
|
2265
2423
|
return null;
|
|
2266
2424
|
});
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2425
|
+
const transcriptSavePromise = capturedTranscript?.text.trim()
|
|
2426
|
+
? saveRecordingTranscript(
|
|
2427
|
+
params.serverUrl,
|
|
2428
|
+
id,
|
|
2429
|
+
capturedTranscript,
|
|
2430
|
+
params.authToken,
|
|
2431
|
+
)
|
|
2432
|
+
: wantsRecordedAudio
|
|
2433
|
+
? saveTranscriptFailure(
|
|
2434
|
+
"No speech was captured during this recording. If you spoke or played system audio, check System Audio, Microphone input, Speech Recognition permission, and the selected mic, then retry transcription.",
|
|
2435
|
+
)
|
|
2436
|
+
: Promise.resolve(true);
|
|
2279
2437
|
|
|
2280
2438
|
// The finalizing window owns the whole stop -> optimized upload ->
|
|
2281
2439
|
// browser-open gap. Only tear down recording chrome here; the outer
|
|
@@ -2317,6 +2475,18 @@ async function startNativeFullscreenRecording(
|
|
|
2317
2475
|
);
|
|
2318
2476
|
throw err;
|
|
2319
2477
|
}
|
|
2478
|
+
const transcriptSaved = await transcriptSavePromise;
|
|
2479
|
+
if (!transcriptSaved && capturedTranscript?.text.trim()) {
|
|
2480
|
+
// The first write runs in parallel with native upload. Retry once
|
|
2481
|
+
// after finalize so a transient or pre-ready action request cannot
|
|
2482
|
+
// strand the local transcript that was already captured.
|
|
2483
|
+
void saveRecordingTranscript(
|
|
2484
|
+
params.serverUrl,
|
|
2485
|
+
id,
|
|
2486
|
+
capturedTranscript,
|
|
2487
|
+
params.authToken,
|
|
2488
|
+
);
|
|
2489
|
+
}
|
|
2320
2490
|
|
|
2321
2491
|
return {
|
|
2322
2492
|
recordingId: uploadResult.recordingId,
|
|
@@ -2324,7 +2494,7 @@ async function startNativeFullscreenRecording(
|
|
|
2324
2494
|
};
|
|
2325
2495
|
} finally {
|
|
2326
2496
|
streamCleanups.forEach((cleanup) => cleanup());
|
|
2327
|
-
await
|
|
2497
|
+
await clearRecordingState();
|
|
2328
2498
|
}
|
|
2329
2499
|
})();
|
|
2330
2500
|
return stopPromise;
|
|
@@ -3077,10 +3247,12 @@ async function startRecordingInner(
|
|
|
3077
3247
|
);
|
|
3078
3248
|
console.log("[clips-recorder] recording row created", { id, uploadMode });
|
|
3079
3249
|
let nativeTranscriptFailureSaved = false;
|
|
3080
|
-
const saveTranscriptFailure = async (
|
|
3081
|
-
|
|
3250
|
+
const saveTranscriptFailure = async (
|
|
3251
|
+
failureReason: string,
|
|
3252
|
+
): Promise<boolean> => {
|
|
3253
|
+
if (!wantsRecordedAudio || nativeTranscriptFailureSaved) return false;
|
|
3082
3254
|
nativeTranscriptFailureSaved = true;
|
|
3083
|
-
|
|
3255
|
+
return saveRecordingTranscriptFailure(
|
|
3084
3256
|
params.serverUrl,
|
|
3085
3257
|
id,
|
|
3086
3258
|
failureReason,
|
|
@@ -3379,6 +3551,7 @@ async function startRecordingInner(
|
|
|
3379
3551
|
// follow, which add ~seconds and would overstate the saved duration.
|
|
3380
3552
|
let stoppedAt = 0;
|
|
3381
3553
|
const viewUrl = `/r/${id}`;
|
|
3554
|
+
const absoluteViewUrl = `${params.serverUrl.replace(/\/+$/, "")}${viewUrl}`;
|
|
3382
3555
|
console.log("[clips-recorder] stop requested");
|
|
3383
3556
|
showFinalizingFeedback();
|
|
3384
3557
|
if (tickHandle) clearInterval(tickHandle);
|
|
@@ -3424,11 +3597,12 @@ async function startRecordingInner(
|
|
|
3424
3597
|
// `recorder.stop()` has already been requested, so recorded duration is
|
|
3425
3598
|
// fixed even if launching the browser takes a moment. Open the existing
|
|
3426
3599
|
// recording row now and let its page poll while upload/finalize continues.
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3600
|
+
//
|
|
3601
|
+
// This must claim the native upload-open slot before launching. The
|
|
3602
|
+
// Finalizing overlay receives the completion event later and uses the
|
|
3603
|
+
// same claim; without it, browser recordings opened once here and then a
|
|
3604
|
+
// second time when finalization completed.
|
|
3605
|
+
await openNativeUploadUrl(id, absoluteViewUrl);
|
|
3432
3606
|
await recorderStopped;
|
|
3433
3607
|
// Recorder has fully stopped and flushed its trailing chunk — this is the
|
|
3434
3608
|
// true end of recorded content. Everything after (transcript, thumbnail,
|
|
@@ -3570,7 +3744,13 @@ async function startRecordingInner(
|
|
|
3570
3744
|
);
|
|
3571
3745
|
await abortRecordingUpload(params.serverUrl, id, failed.message);
|
|
3572
3746
|
} finally {
|
|
3573
|
-
await
|
|
3747
|
+
await clearRecordingState();
|
|
3748
|
+
await publishFinalizingResult({
|
|
3749
|
+
recordingId: id,
|
|
3750
|
+
viewUrl: absoluteViewUrl,
|
|
3751
|
+
ok: false,
|
|
3752
|
+
error: failed.message,
|
|
3753
|
+
});
|
|
3574
3754
|
}
|
|
3575
3755
|
throw failed;
|
|
3576
3756
|
}
|
|
@@ -3601,58 +3781,77 @@ async function startRecordingInner(
|
|
|
3601
3781
|
uploadMode,
|
|
3602
3782
|
anyFailed: !!failed,
|
|
3603
3783
|
});
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
if (!finalRes.ok) {
|
|
3626
|
-
throw new Error(
|
|
3627
|
-
`Finalize failed (${finalRes.status}): ${bodyText.slice(0, 200)}`,
|
|
3784
|
+
try {
|
|
3785
|
+
const result = await finalizeAfterDurableBackup({
|
|
3786
|
+
// Opening the browser must not make the desktop look idle until every
|
|
3787
|
+
// trailing backup write and the final metadata are durable.
|
|
3788
|
+
ensureBackupDurable: async () => {
|
|
3789
|
+
await Promise.allSettled([...backupWrites]);
|
|
3790
|
+
},
|
|
3791
|
+
attemptFinalize: async () => {
|
|
3792
|
+
try {
|
|
3793
|
+
const finalRes = await fetch(finalizeUrl, {
|
|
3794
|
+
method: "POST",
|
|
3795
|
+
headers: { "Content-Type": "application/octet-stream" },
|
|
3796
|
+
credentials: "include",
|
|
3797
|
+
body: finalBody,
|
|
3798
|
+
signal: AbortSignal.timeout(FINALIZE_UPLOAD_TIMEOUT_MS),
|
|
3799
|
+
});
|
|
3800
|
+
const bodyText = await finalRes.text().catch(() => "");
|
|
3801
|
+
console.log(
|
|
3802
|
+
"[clips-recorder] finalize response:",
|
|
3803
|
+
finalRes.status,
|
|
3804
|
+
bodyText.slice(0, 500),
|
|
3628
3805
|
);
|
|
3806
|
+
if (!finalRes.ok) {
|
|
3807
|
+
throw new Error(
|
|
3808
|
+
`Finalize failed (${finalRes.status}): ${bodyText.slice(0, 200)}`,
|
|
3809
|
+
);
|
|
3810
|
+
}
|
|
3811
|
+
} catch (err) {
|
|
3812
|
+
console.error("[clips-recorder] finalize fetch failed:", err);
|
|
3813
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
3814
|
+
if (
|
|
3815
|
+
await recoverReadyRecordingAfterFinalizeError({
|
|
3816
|
+
serverUrl: params.serverUrl,
|
|
3817
|
+
recordingId: id,
|
|
3818
|
+
authToken: params.authToken,
|
|
3819
|
+
})
|
|
3820
|
+
) {
|
|
3821
|
+
return { recordingId: id, viewUrl };
|
|
3822
|
+
}
|
|
3823
|
+
await markBrowserRecordingBackupError(id, error.message).catch(
|
|
3824
|
+
() => {},
|
|
3825
|
+
);
|
|
3826
|
+
await abortRecordingUpload(params.serverUrl, id, error.message);
|
|
3827
|
+
throw error;
|
|
3629
3828
|
}
|
|
3630
|
-
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
recordingId: id,
|
|
3637
|
-
authToken: params.authToken,
|
|
3638
|
-
})
|
|
3639
|
-
) {
|
|
3640
|
-
return { recordingId: id, viewUrl };
|
|
3641
|
-
}
|
|
3642
|
-
await markBrowserRecordingBackupError(id, error.message).catch(
|
|
3643
|
-
() => {},
|
|
3644
|
-
);
|
|
3645
|
-
await abortRecordingUpload(params.serverUrl, id, error.message);
|
|
3646
|
-
throw error;
|
|
3647
|
-
}
|
|
3648
|
-
await deleteBrowserRecordingBackup(id).catch((err) => {
|
|
3649
|
-
console.warn("[clips-recorder] local backup cleanup failed:", err);
|
|
3650
|
-
});
|
|
3829
|
+
await deleteBrowserRecordingBackup(id).catch((err) => {
|
|
3830
|
+
console.warn(
|
|
3831
|
+
"[clips-recorder] local backup cleanup failed:",
|
|
3832
|
+
err,
|
|
3833
|
+
);
|
|
3834
|
+
});
|
|
3651
3835
|
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
3836
|
+
return { recordingId: id, viewUrl };
|
|
3837
|
+
},
|
|
3838
|
+
releaseGuard: clearRecordingState,
|
|
3839
|
+
});
|
|
3840
|
+
await publishFinalizingResult({
|
|
3841
|
+
recordingId: id,
|
|
3842
|
+
viewUrl: absoluteViewUrl,
|
|
3843
|
+
ok: true,
|
|
3844
|
+
});
|
|
3845
|
+
return result;
|
|
3846
|
+
} catch (err) {
|
|
3847
|
+
await publishFinalizingResult({
|
|
3848
|
+
recordingId: id,
|
|
3849
|
+
viewUrl: absoluteViewUrl,
|
|
3850
|
+
ok: false,
|
|
3851
|
+
error: err instanceof Error ? err.message : String(err),
|
|
3852
|
+
});
|
|
3853
|
+
throw err;
|
|
3854
|
+
}
|
|
3656
3855
|
},
|
|
3657
3856
|
|
|
3658
3857
|
async cancel() {
|