@agent-native/core 0.109.0 → 0.109.2
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/integrations.mdx +87 -253
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/a2a/client.ts +24 -2
- package/corpus/core/src/a2a/correlation.ts +50 -0
- package/corpus/core/src/a2a/handlers.ts +140 -14
- package/corpus/core/src/a2a/index.ts +1 -0
- package/corpus/core/src/a2a/invoke.ts +10 -1
- package/corpus/core/src/a2a/task-store.ts +146 -6
- package/corpus/core/src/a2a/types.ts +16 -0
- package/corpus/core/src/action.ts +2 -0
- package/corpus/core/src/agent/production-agent.ts +23 -0
- package/corpus/core/src/client/blocks/library/diagram.config.ts +10 -1
- package/corpus/core/src/client/blocks/library/diagram.tsx +9 -4
- package/corpus/core/src/client/use-run-stuck-detection.ts +27 -1
- package/corpus/core/src/db/client.ts +55 -0
- package/corpus/core/src/deploy/build.ts +63 -3
- package/corpus/core/src/observability/traces.ts +172 -19
- package/corpus/core/src/scripts/call-agent.ts +53 -2
- package/corpus/core/src/server/agent-chat/action-filters-a2a.ts +72 -25
- package/corpus/core/src/server/agent-chat-plugin.ts +107 -6
- package/corpus/core/src/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +21 -0
- package/corpus/core/src/templates/workspace-core/.agents/skills/observability/SKILL.md +7 -1
- package/corpus/core/src/templates/workspace-core/.agents/skills/tracking/SKILL.md +1 -1
- package/corpus/templates/analytics/actions/query-agent-native-analytics.ts +1 -1
- package/corpus/templates/analytics/changelog/2026-07-17-analytics-now-includes-a-default-alert-for-spikes-in-stuck-agent-chats.md +6 -0
- package/corpus/templates/analytics/docs/schemas/first-party-analytics.md +23 -14
- package/corpus/templates/analytics/docs/schemas/tracked-events.md +5 -1
- package/corpus/templates/analytics/server/jobs/analytics-alerts.ts +3 -3
- package/corpus/templates/analytics/server/lib/analytics-alerts.ts +209 -87
- package/corpus/templates/clips/.agents/skills/dictate/SKILL.md +28 -5
- package/corpus/templates/clips/.agents/skills/meetings/SKILL.md +7 -0
- package/corpus/templates/clips/.agents/skills/recording/SKILL.md +23 -0
- package/corpus/templates/clips/AGENTS.md +10 -0
- package/corpus/templates/clips/actions/create-dictation.ts +29 -4
- package/corpus/templates/clips/actions/finalize-recording.ts +542 -65
- package/corpus/templates/clips/actions/get-recording-player-data.ts +16 -6
- package/corpus/templates/clips/actions/set-mobile-capture-state.ts +37 -0
- package/corpus/templates/clips/actions/update-dictation.ts +9 -1
- package/corpus/templates/clips/app/components/player/playback-comment-overlay.tsx +17 -2
- package/corpus/templates/clips/app/components/player/video-player.tsx +14 -2
- package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +2 -2
- package/corpus/templates/clips/app/routes/r.$recordingId.tsx +14 -3
- package/corpus/templates/clips/app/routes/record.tsx +3 -3
- package/corpus/templates/clips/app/routes/share.$shareId.tsx +19 -5
- package/corpus/templates/clips/changelog/2026-07-17-capture-dictation-background-meeting-audio-and-camera-videos.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-17-clips-now-retries-storage-verification-in-the-background-ins.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-17-delayed-upload-verification-keeps-local-backups-retryable.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-17-fixed-broken-thumbnail-icons-appearing-over-playable-clip-em.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-17-playback-comments-stay-readable-at-faster-playback-speeds.md +6 -0
- package/corpus/templates/clips/chrome-extension/src/finalize-recovery.ts +3 -0
- package/corpus/templates/clips/chrome-extension/src/offscreen.ts +3 -3
- package/corpus/templates/clips/desktop/src/app.tsx +35 -6
- package/corpus/templates/clips/desktop/src/lib/processing-backup-recovery.ts +19 -0
- package/corpus/templates/clips/desktop/src/lib/recorder.ts +141 -18
- package/corpus/templates/clips/desktop/src/lib/upload-verification.ts +28 -7
- package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +3 -4
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen/live_upload.rs +32 -15
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +59 -18
- package/corpus/templates/clips/server/db/schema.ts +9 -1
- package/corpus/templates/clips/server/jobs/media-verification.ts +130 -0
- package/corpus/templates/clips/server/lib/builder-media-compression.ts +74 -7
- package/corpus/templates/clips/server/lib/media-verification-state.ts +55 -0
- package/corpus/templates/clips/server/lib/player-thumbnail-url.ts +31 -0
- package/corpus/templates/clips/server/lib/post-finalize-dispatch.ts +10 -3
- package/corpus/templates/clips/server/plugins/auth.ts +5 -5
- package/corpus/templates/clips/server/plugins/calendar-jobs.ts +2 -0
- package/corpus/templates/clips/server/routes/api/_agent-native-background/post-finalize-worker.post.ts +12 -2
- package/corpus/templates/clips/server/routes/api/public-recording.get.ts +13 -1
- package/corpus/templates/clips/server/routes/api/thumbnail/[recordingId].get.ts +340 -0
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/abort.post.ts +24 -7
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +91 -7
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/reset-chunks.post.ts +22 -1
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/status.get.ts +14 -0
- package/corpus/templates/clips/shared/finalize-recovery.ts +61 -2
- package/corpus/templates/clips/shared/share-meta.ts +1 -1
- package/corpus/templates/plan/changelog/2026-07-17-diagrams-can-opt-into-a-polished-clean-rendering-mode-withou.md +6 -0
- package/corpus/templates/plan/shared/plan-content.ts +3 -0
- package/dist/a2a/client.d.ts +11 -2
- package/dist/a2a/client.d.ts.map +1 -1
- package/dist/a2a/client.js +16 -3
- package/dist/a2a/client.js.map +1 -1
- package/dist/a2a/correlation.d.ts +10 -0
- package/dist/a2a/correlation.d.ts.map +1 -0
- package/dist/a2a/correlation.js +40 -0
- package/dist/a2a/correlation.js.map +1 -0
- package/dist/a2a/handlers.d.ts.map +1 -1
- package/dist/a2a/handlers.js +100 -15
- package/dist/a2a/handlers.js.map +1 -1
- package/dist/a2a/index.d.ts +1 -1
- package/dist/a2a/index.d.ts.map +1 -1
- package/dist/a2a/index.js.map +1 -1
- package/dist/a2a/invoke.d.ts +4 -1
- package/dist/a2a/invoke.d.ts.map +1 -1
- package/dist/a2a/invoke.js +3 -0
- package/dist/a2a/invoke.js.map +1 -1
- package/dist/a2a/task-store.d.ts +12 -1
- package/dist/a2a/task-store.d.ts.map +1 -1
- package/dist/a2a/task-store.js +96 -6
- package/dist/a2a/task-store.js.map +1 -1
- package/dist/a2a/types.d.ts +15 -0
- package/dist/a2a/types.d.ts.map +1 -1
- package/dist/a2a/types.js.map +1 -1
- package/dist/action.d.ts +2 -0
- package/dist/action.d.ts.map +1 -1
- package/dist/action.js.map +1 -1
- package/dist/agent/production-agent.d.ts +9 -0
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +14 -0
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/client/blocks/library/diagram.config.d.ts +2 -0
- package/dist/client/blocks/library/diagram.config.d.ts.map +1 -1
- package/dist/client/blocks/library/diagram.config.js +8 -1
- package/dist/client/blocks/library/diagram.config.js.map +1 -1
- package/dist/client/blocks/library/diagram.d.ts.map +1 -1
- package/dist/client/blocks/library/diagram.js +9 -4
- package/dist/client/blocks/library/diagram.js.map +1 -1
- package/dist/client/use-run-stuck-detection.d.ts.map +1 -1
- package/dist/client/use-run-stuck-detection.js +25 -1
- package/dist/client/use-run-stuck-detection.js.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/db/client.d.ts +7 -0
- package/dist/db/client.d.ts.map +1 -1
- package/dist/db/client.js +33 -0
- package/dist/db/client.js.map +1 -1
- package/dist/deploy/build.d.ts +3 -0
- package/dist/deploy/build.d.ts.map +1 -1
- package/dist/deploy/build.js +45 -1
- package/dist/deploy/build.js.map +1 -1
- package/dist/observability/routes.d.ts +1 -1
- package/dist/observability/traces.d.ts +9 -0
- package/dist/observability/traces.d.ts.map +1 -1
- package/dist/observability/traces.js +137 -19
- package/dist/observability/traces.js.map +1 -1
- package/dist/progress/routes.d.ts +1 -1
- package/dist/scripts/call-agent.d.ts.map +1 -1
- package/dist/scripts/call-agent.js +35 -3
- package/dist/scripts/call-agent.js.map +1 -1
- package/dist/server/agent-chat/action-filters-a2a.d.ts +18 -2
- package/dist/server/agent-chat/action-filters-a2a.d.ts.map +1 -1
- package/dist/server/agent-chat/action-filters-a2a.js +35 -7
- package/dist/server/agent-chat/action-filters-a2a.js.map +1 -1
- package/dist/server/agent-chat-plugin.d.ts.map +1 -1
- package/dist/server/agent-chat-plugin.js +93 -6
- package/dist/server/agent-chat-plugin.js.map +1 -1
- package/dist/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +21 -0
- package/dist/templates/workspace-core/.agents/skills/observability/SKILL.md +7 -1
- package/dist/templates/workspace-core/.agents/skills/tracking/SKILL.md +1 -1
- package/docs/content/integrations.mdx +87 -253
- package/package.json +1 -1
- package/src/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +21 -0
- package/src/templates/workspace-core/.agents/skills/observability/SKILL.md +7 -1
- package/src/templates/workspace-core/.agents/skills/tracking/SKILL.md +1 -1
|
@@ -51,7 +51,10 @@ import { invoke } from "@tauri-apps/api/core";
|
|
|
51
51
|
import { emit, listen, type UnlistenFn } from "@tauri-apps/api/event";
|
|
52
52
|
import { open as openExternal } from "@tauri-apps/plugin-shell";
|
|
53
53
|
|
|
54
|
-
import {
|
|
54
|
+
import {
|
|
55
|
+
waitForAcceptedRecordingAfterFinalizeError,
|
|
56
|
+
waitForReadyRecordingAfterFinalizeError,
|
|
57
|
+
} from "../../../shared/finalize-recovery";
|
|
55
58
|
import type { LocalRecordingMode } from "../shared/config";
|
|
56
59
|
import { createAudioCue, type AudioCue } from "./audio-cue";
|
|
57
60
|
import { createCameraCompositeStream } from "./camera-composite";
|
|
@@ -75,6 +78,7 @@ import {
|
|
|
75
78
|
createPauseTransitionQueue,
|
|
76
79
|
type PauseTransitionQueue,
|
|
77
80
|
} from "./pause-transition";
|
|
81
|
+
import { reconcileProcessingBackup } from "./processing-backup-recovery";
|
|
78
82
|
import { buildCaptureTitle, type CaptureTitleResult } from "./recording-title";
|
|
79
83
|
import { singleFlight } from "./single-flight";
|
|
80
84
|
import {
|
|
@@ -83,6 +87,7 @@ import {
|
|
|
83
87
|
type TranscriptionCapture,
|
|
84
88
|
} from "./transcription-capture";
|
|
85
89
|
import {
|
|
90
|
+
parseFinalizeReceipt,
|
|
86
91
|
verifyFinalizeReceipt,
|
|
87
92
|
type FinalizeReceipt,
|
|
88
93
|
} from "./upload-verification";
|
|
@@ -741,6 +746,7 @@ export async function dismissBrowserRecordingBackup(
|
|
|
741
746
|
async function markBrowserRecordingBackupError(
|
|
742
747
|
recordingId: string,
|
|
743
748
|
error: string,
|
|
749
|
+
incrementRetryCount = true,
|
|
744
750
|
): Promise<void> {
|
|
745
751
|
const meta = await getBrowserRecordingBackupMeta(recordingId);
|
|
746
752
|
if (!meta) return;
|
|
@@ -748,11 +754,84 @@ async function markBrowserRecordingBackupError(
|
|
|
748
754
|
...meta,
|
|
749
755
|
lastAttemptAt: new Date().toISOString(),
|
|
750
756
|
lastError: error,
|
|
751
|
-
retryCount: meta.retryCount + 1,
|
|
757
|
+
retryCount: incrementRetryCount ? meta.retryCount + 1 : meta.retryCount,
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
const MEDIA_VERIFICATION_BACKUP_ERROR =
|
|
762
|
+
"The server could not finish verifying this upload. The local copy is still saved; retry to continue.";
|
|
763
|
+
|
|
764
|
+
async function flagBrowserBackupAfterProcessing(recordingId: string) {
|
|
765
|
+
await markBrowserRecordingBackupError(
|
|
766
|
+
recordingId,
|
|
767
|
+
MEDIA_VERIFICATION_BACKUP_ERROR,
|
|
768
|
+
false,
|
|
769
|
+
);
|
|
770
|
+
await emit("clips:pending-uploads-changed").catch(() => {});
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
async function flagNativeBackupAfterProcessing(recordingId: string) {
|
|
774
|
+
await invoke("native_fullscreen_recording_mark_upload_error", {
|
|
775
|
+
recordingId,
|
|
776
|
+
error: MEDIA_VERIFICATION_BACKUP_ERROR,
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
function scheduleBrowserBackupCleanupAfterProcessing(args: {
|
|
781
|
+
serverUrl: string;
|
|
782
|
+
recordingId: string;
|
|
783
|
+
authToken?: string;
|
|
784
|
+
}): void {
|
|
785
|
+
void reconcileProcessingBackup({
|
|
786
|
+
waitForReady: () =>
|
|
787
|
+
waitForReadyRecordingAfterFinalizeError({
|
|
788
|
+
uploadUrl: chunkUrl(args.serverUrl, args.recordingId, 0, false),
|
|
789
|
+
recordingId: args.recordingId,
|
|
790
|
+
authToken: args.authToken,
|
|
791
|
+
preferAuthenticated: true,
|
|
792
|
+
timeoutMs: 12 * 60 * 1000,
|
|
793
|
+
}),
|
|
794
|
+
onReady: () => deleteBrowserRecordingBackup(args.recordingId),
|
|
795
|
+
onUnresolved: () => flagBrowserBackupAfterProcessing(args.recordingId),
|
|
796
|
+
onPollError: (err) => {
|
|
797
|
+
console.warn(
|
|
798
|
+
"[clips-recorder] background backup cleanup check failed:",
|
|
799
|
+
err,
|
|
800
|
+
);
|
|
801
|
+
},
|
|
802
|
+
}).catch((err) => {
|
|
803
|
+
console.warn("[clips-recorder] background backup flag failed:", err);
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
export function scheduleNativeBackupCleanupAfterProcessing(args: {
|
|
808
|
+
serverUrl: string;
|
|
809
|
+
recordingId: string;
|
|
810
|
+
authToken?: string;
|
|
811
|
+
}): void {
|
|
812
|
+
void reconcileProcessingBackup({
|
|
813
|
+
waitForReady: () =>
|
|
814
|
+
waitForReadyRecordingAfterFinalizeError({
|
|
815
|
+
uploadUrl: chunkUrl(args.serverUrl, args.recordingId, 0, false),
|
|
816
|
+
recordingId: args.recordingId,
|
|
817
|
+
authToken: args.authToken,
|
|
818
|
+
preferAuthenticated: true,
|
|
819
|
+
timeoutMs: 12 * 60 * 1000,
|
|
820
|
+
}),
|
|
821
|
+
onReady: () =>
|
|
822
|
+
invoke("native_fullscreen_recording_dismiss_upload", {
|
|
823
|
+
recordingId: args.recordingId,
|
|
824
|
+
}),
|
|
825
|
+
onUnresolved: () => flagNativeBackupAfterProcessing(args.recordingId),
|
|
826
|
+
onPollError: (err) => {
|
|
827
|
+
console.warn("[clips-recorder] native backup cleanup check failed:", err);
|
|
828
|
+
},
|
|
829
|
+
}).catch((err) => {
|
|
830
|
+
console.warn("[clips-recorder] native backup flag failed:", err);
|
|
752
831
|
});
|
|
753
832
|
}
|
|
754
833
|
|
|
755
|
-
async function
|
|
834
|
+
async function recoverAcceptedRecordingAfterFinalizeError({
|
|
756
835
|
serverUrl,
|
|
757
836
|
recordingId,
|
|
758
837
|
authToken,
|
|
@@ -760,19 +839,27 @@ async function recoverReadyRecordingAfterFinalizeError({
|
|
|
760
839
|
serverUrl: string;
|
|
761
840
|
recordingId: string;
|
|
762
841
|
authToken?: string;
|
|
763
|
-
}): Promise<
|
|
764
|
-
const recovered = await
|
|
842
|
+
}): Promise<"processing" | "ready" | null> {
|
|
843
|
+
const recovered = await waitForAcceptedRecordingAfterFinalizeError({
|
|
765
844
|
uploadUrl: chunkUrl(serverUrl, recordingId, 0, false),
|
|
766
845
|
recordingId,
|
|
767
846
|
authToken,
|
|
768
847
|
preferAuthenticated: true,
|
|
769
848
|
});
|
|
770
|
-
if (!recovered) return
|
|
849
|
+
if (!recovered) return null;
|
|
850
|
+
if (recovered.status === "processing") {
|
|
851
|
+
scheduleBrowserBackupCleanupAfterProcessing({
|
|
852
|
+
serverUrl,
|
|
853
|
+
recordingId,
|
|
854
|
+
authToken,
|
|
855
|
+
});
|
|
856
|
+
return "processing";
|
|
857
|
+
}
|
|
771
858
|
await markBrowserRecordingBackupError(
|
|
772
859
|
recordingId,
|
|
773
860
|
"Upload completed, but its final receipt could not be verified. The local backup was kept.",
|
|
774
861
|
).catch(() => {});
|
|
775
|
-
return
|
|
862
|
+
return "ready";
|
|
776
863
|
}
|
|
777
864
|
|
|
778
865
|
export async function listBrowserRecordingBackups(): Promise<
|
|
@@ -824,7 +911,7 @@ async function postBackupChunk(
|
|
|
824
911
|
`Upload retry failed (${res.status}): ${body.slice(0, 200)}`,
|
|
825
912
|
);
|
|
826
913
|
}
|
|
827
|
-
return
|
|
914
|
+
return parseFinalizeReceipt(body);
|
|
828
915
|
}
|
|
829
916
|
|
|
830
917
|
async function resetBrowserRecordingBackupUpload(
|
|
@@ -951,10 +1038,21 @@ export async function retryBrowserRecordingBackup(input: {
|
|
|
951
1038
|
validatedChunks,
|
|
952
1039
|
input.authToken,
|
|
953
1040
|
);
|
|
954
|
-
verifyFinalizeReceipt(receipt, meta);
|
|
1041
|
+
const receiptStatus = verifyFinalizeReceipt(receipt, meta);
|
|
1042
|
+
if (receiptStatus === "processing") {
|
|
1043
|
+
scheduleBrowserBackupCleanupAfterProcessing({
|
|
1044
|
+
serverUrl: meta.serverUrl,
|
|
1045
|
+
recordingId: meta.recordingId,
|
|
1046
|
+
authToken: input.authToken,
|
|
1047
|
+
});
|
|
1048
|
+
return {
|
|
1049
|
+
recordingId: meta.recordingId,
|
|
1050
|
+
viewUrl: `/r/${meta.recordingId}`,
|
|
1051
|
+
};
|
|
1052
|
+
}
|
|
955
1053
|
} catch (err) {
|
|
956
1054
|
if (
|
|
957
|
-
await
|
|
1055
|
+
await recoverAcceptedRecordingAfterFinalizeError({
|
|
958
1056
|
serverUrl: meta.serverUrl,
|
|
959
1057
|
recordingId: meta.recordingId,
|
|
960
1058
|
authToken: input.authToken,
|
|
@@ -1007,10 +1105,21 @@ export async function retryBrowserRecordingBackup(input: {
|
|
|
1007
1105
|
new Blob([], { type: meta.mimeType }),
|
|
1008
1106
|
input.authToken,
|
|
1009
1107
|
);
|
|
1010
|
-
verifyFinalizeReceipt(receipt, meta);
|
|
1108
|
+
const receiptStatus = verifyFinalizeReceipt(receipt, meta);
|
|
1109
|
+
if (receiptStatus === "processing") {
|
|
1110
|
+
scheduleBrowserBackupCleanupAfterProcessing({
|
|
1111
|
+
serverUrl: meta.serverUrl,
|
|
1112
|
+
recordingId: meta.recordingId,
|
|
1113
|
+
authToken: input.authToken,
|
|
1114
|
+
});
|
|
1115
|
+
return {
|
|
1116
|
+
recordingId: meta.recordingId,
|
|
1117
|
+
viewUrl: `/r/${meta.recordingId}`,
|
|
1118
|
+
};
|
|
1119
|
+
}
|
|
1011
1120
|
} catch (err) {
|
|
1012
1121
|
if (
|
|
1013
|
-
await
|
|
1122
|
+
await recoverAcceptedRecordingAfterFinalizeError({
|
|
1014
1123
|
serverUrl: meta.serverUrl,
|
|
1015
1124
|
recordingId: meta.recordingId,
|
|
1016
1125
|
authToken: input.authToken,
|
|
@@ -1143,6 +1252,7 @@ interface NativeFullscreenUploadResult {
|
|
|
1143
1252
|
durationMs: number;
|
|
1144
1253
|
width?: number;
|
|
1145
1254
|
height?: number;
|
|
1255
|
+
verificationPending?: boolean;
|
|
1146
1256
|
}
|
|
1147
1257
|
|
|
1148
1258
|
interface NativeFullscreenSaveResult {
|
|
@@ -2490,7 +2600,7 @@ async function startNativeFullscreenRecording(
|
|
|
2490
2600
|
uploadResult = await uploadPromise;
|
|
2491
2601
|
} catch (err) {
|
|
2492
2602
|
if (
|
|
2493
|
-
await
|
|
2603
|
+
await recoverAcceptedRecordingAfterFinalizeError({
|
|
2494
2604
|
serverUrl: params.serverUrl,
|
|
2495
2605
|
recordingId: id,
|
|
2496
2606
|
authToken: params.authToken,
|
|
@@ -2505,6 +2615,13 @@ async function startNativeFullscreenRecording(
|
|
|
2505
2615
|
);
|
|
2506
2616
|
throw err;
|
|
2507
2617
|
}
|
|
2618
|
+
if (uploadResult.verificationPending) {
|
|
2619
|
+
scheduleNativeBackupCleanupAfterProcessing({
|
|
2620
|
+
serverUrl: params.serverUrl,
|
|
2621
|
+
recordingId: id,
|
|
2622
|
+
authToken: params.authToken,
|
|
2623
|
+
});
|
|
2624
|
+
}
|
|
2508
2625
|
const transcriptSaved = await transcriptSavePromise;
|
|
2509
2626
|
if (!transcriptSaved && capturedTranscript?.text.trim()) {
|
|
2510
2627
|
// The first write runs in parallel with native upload. Retry once
|
|
@@ -3875,18 +3992,24 @@ async function startRecordingInner(
|
|
|
3875
3992
|
`Finalize failed (${finalRes.status}): ${bodyText.slice(0, 200)}`,
|
|
3876
3993
|
);
|
|
3877
3994
|
}
|
|
3878
|
-
const receipt = bodyText
|
|
3879
|
-
|
|
3880
|
-
: null;
|
|
3881
|
-
verifyFinalizeReceipt(receipt, {
|
|
3995
|
+
const receipt = parseFinalizeReceipt(bodyText);
|
|
3996
|
+
const receiptStatus = verifyFinalizeReceipt(receipt, {
|
|
3882
3997
|
bytes: backupBytes,
|
|
3883
3998
|
durationMs,
|
|
3884
3999
|
});
|
|
4000
|
+
if (receiptStatus === "processing") {
|
|
4001
|
+
scheduleBrowserBackupCleanupAfterProcessing({
|
|
4002
|
+
serverUrl: params.serverUrl,
|
|
4003
|
+
recordingId: id,
|
|
4004
|
+
authToken: params.authToken,
|
|
4005
|
+
});
|
|
4006
|
+
return { recordingId: id, viewUrl };
|
|
4007
|
+
}
|
|
3885
4008
|
} catch (err) {
|
|
3886
4009
|
console.error("[clips-recorder] finalize fetch failed:", err);
|
|
3887
4010
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
3888
4011
|
if (
|
|
3889
|
-
await
|
|
4012
|
+
await recoverAcceptedRecordingAfterFinalizeError({
|
|
3890
4013
|
serverUrl: params.serverUrl,
|
|
3891
4014
|
recordingId: id,
|
|
3892
4015
|
authToken: params.authToken,
|
|
@@ -2,6 +2,7 @@ export interface FinalizeReceipt {
|
|
|
2
2
|
ok?: unknown;
|
|
3
3
|
finalized?: unknown;
|
|
4
4
|
status?: unknown;
|
|
5
|
+
verificationPending?: unknown;
|
|
5
6
|
sourceSizeBytes?: unknown;
|
|
6
7
|
durationMs?: unknown;
|
|
7
8
|
}
|
|
@@ -11,16 +12,34 @@ export interface LocalRecordingProof {
|
|
|
11
12
|
durationMs: number;
|
|
12
13
|
}
|
|
13
14
|
|
|
15
|
+
export type FinalizeReceiptStatus = "processing" | "ready";
|
|
16
|
+
|
|
17
|
+
export function parseFinalizeReceipt(body: string): FinalizeReceipt | null {
|
|
18
|
+
if (!body) return null;
|
|
19
|
+
try {
|
|
20
|
+
const parsed: unknown = JSON.parse(body);
|
|
21
|
+
return parsed && typeof parsed === "object"
|
|
22
|
+
? (parsed as FinalizeReceipt)
|
|
23
|
+
: null;
|
|
24
|
+
} catch {
|
|
25
|
+
throw new Error("Upload returned an invalid finalization response");
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
14
29
|
export function verifyFinalizeReceipt(
|
|
15
30
|
receipt: FinalizeReceipt | null,
|
|
16
31
|
local: LocalRecordingProof,
|
|
17
|
-
):
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
receipt.
|
|
21
|
-
receipt.
|
|
22
|
-
|
|
23
|
-
|
|
32
|
+
): FinalizeReceiptStatus {
|
|
33
|
+
const ready =
|
|
34
|
+
receipt?.ok === true &&
|
|
35
|
+
receipt.finalized === true &&
|
|
36
|
+
receipt.status === "ready";
|
|
37
|
+
const processing =
|
|
38
|
+
receipt?.ok === true &&
|
|
39
|
+
receipt.finalized === false &&
|
|
40
|
+
receipt.status === "processing" &&
|
|
41
|
+
receipt.verificationPending === true;
|
|
42
|
+
if (!ready && !processing) {
|
|
24
43
|
throw new Error(
|
|
25
44
|
"Clip may be incomplete. Finalization was not confirmed; the local backup was kept.",
|
|
26
45
|
);
|
|
@@ -48,4 +67,6 @@ export function verifyFinalizeReceipt(
|
|
|
48
67
|
"Clip may be incomplete. The uploaded duration did not match the local recording; the local backup was kept.",
|
|
49
68
|
);
|
|
50
69
|
}
|
|
70
|
+
|
|
71
|
+
return ready ? "ready" : "processing";
|
|
51
72
|
}
|
|
@@ -141,6 +141,7 @@ pub fn run() {
|
|
|
141
141
|
native_screen::native_fullscreen_recording_rotate_segment,
|
|
142
142
|
native_screen::native_fullscreen_pending_uploads,
|
|
143
143
|
native_screen::native_fullscreen_recording_retry_upload,
|
|
144
|
+
native_screen::native_fullscreen_recording_mark_upload_error,
|
|
144
145
|
native_screen::native_fullscreen_recording_dismiss_upload,
|
|
145
146
|
native_screen::native_fullscreen_open_drafts_folder,
|
|
146
147
|
// local-only always-on screen memory compatibility helpers
|
|
@@ -249,8 +250,7 @@ pub fn run() {
|
|
|
249
250
|
use tauri_plugin_deep_link::DeepLinkExt;
|
|
250
251
|
let dl_handle = app.handle().clone();
|
|
251
252
|
app.deep_link().on_open_url(move |event| {
|
|
252
|
-
let urls: Vec<String> =
|
|
253
|
-
event.urls().iter().map(|u| u.to_string()).collect();
|
|
253
|
+
let urls: Vec<String> = event.urls().iter().map(|u| u.to_string()).collect();
|
|
254
254
|
dlog!("[clips-tray] deep link opened: {:?}", urls);
|
|
255
255
|
present_popover(&dl_handle);
|
|
256
256
|
let _ = dl_handle.emit("clips:deep-link", urls);
|
|
@@ -263,8 +263,7 @@ pub fn run() {
|
|
|
263
263
|
// arguments rather than an on_open_url event. Consume it here so
|
|
264
264
|
// the popover appears and the browser does not hit its fallback.
|
|
265
265
|
if let Ok(Some(urls)) = app.deep_link().get_current() {
|
|
266
|
-
let url_strings: Vec<String> =
|
|
267
|
-
urls.iter().map(|u| u.to_string()).collect();
|
|
266
|
+
let url_strings: Vec<String> = urls.iter().map(|u| u.to_string()).collect();
|
|
268
267
|
dlog!("[clips-tray] deep link cold start: {:?}", url_strings);
|
|
269
268
|
present_popover(app.handle());
|
|
270
269
|
let _ = app.handle().emit("clips:deep-link", url_strings);
|
|
@@ -36,8 +36,12 @@ pub(super) struct LiveUploadCtrl {
|
|
|
36
36
|
|
|
37
37
|
pub(super) struct LiveUpload {
|
|
38
38
|
pub(super) ctrl: Arc<LiveUploadCtrl>,
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
pub(super) result_rx: tokio::sync::oneshot::Receiver<Result<LiveUploadResult, String>>,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
pub(super) struct LiveUploadResult {
|
|
43
|
+
pub(super) bytes: u64,
|
|
44
|
+
pub(super) verification_pending: bool,
|
|
41
45
|
}
|
|
42
46
|
|
|
43
47
|
struct LiveUploadParams {
|
|
@@ -163,7 +167,7 @@ async fn send_live_upload_post_with_retry(
|
|
|
163
167
|
duration_ms: Option<u128>,
|
|
164
168
|
expected_source_bytes: Option<u64>,
|
|
165
169
|
bytes: &[u8],
|
|
166
|
-
) -> Result<
|
|
170
|
+
) -> Result<bool, String> {
|
|
167
171
|
let rec = ¶ms.recording_id;
|
|
168
172
|
let mut attempt: u32 = 0;
|
|
169
173
|
loop {
|
|
@@ -198,7 +202,7 @@ async fn send_live_upload_post_with_retry(
|
|
|
198
202
|
)
|
|
199
203
|
.await;
|
|
200
204
|
let err = match result {
|
|
201
|
-
Ok(
|
|
205
|
+
Ok(verification_pending) => return Ok(verification_pending),
|
|
202
206
|
Err(err) => err,
|
|
203
207
|
};
|
|
204
208
|
if attempt >= LIVE_UPLOAD_CHUNK_ATTEMPTS {
|
|
@@ -219,7 +223,7 @@ async fn live_upload_loop(
|
|
|
219
223
|
app: AppHandle,
|
|
220
224
|
ctrl: Arc<LiveUploadCtrl>,
|
|
221
225
|
params: LiveUploadParams,
|
|
222
|
-
) -> Result<
|
|
226
|
+
) -> Result<LiveUploadResult, String> {
|
|
223
227
|
let rec = params.recording_id.clone();
|
|
224
228
|
eprintln!(
|
|
225
229
|
"[live-upload] loop started for {rec}: file={} chunk={} bytes",
|
|
@@ -290,6 +294,7 @@ async fn live_upload_loop(
|
|
|
290
294
|
// sentinel makes GCS silently commit only the aligned prefix, and
|
|
291
295
|
// the session close then fails forever with 308 (incomplete).
|
|
292
296
|
let mut final_sent = false;
|
|
297
|
+
let mut verification_pending = false;
|
|
293
298
|
while final_len > offset {
|
|
294
299
|
let take = chunk.min(final_len - offset);
|
|
295
300
|
let is_last = offset + take >= final_len;
|
|
@@ -302,7 +307,7 @@ async fn live_upload_loop(
|
|
|
302
307
|
eprintln!(
|
|
303
308
|
"[live-upload] {rec}: sending tail chunk #{index} offset={offset} size={take} final={is_last}"
|
|
304
309
|
);
|
|
305
|
-
|
|
310
|
+
let receipt = send_live_upload_post_with_retry(
|
|
306
311
|
&client,
|
|
307
312
|
&ctrl,
|
|
308
313
|
¶ms,
|
|
@@ -313,10 +318,16 @@ async fn live_upload_loop(
|
|
|
313
318
|
is_last.then_some(final_len),
|
|
314
319
|
&bytes,
|
|
315
320
|
)
|
|
316
|
-
.await
|
|
317
|
-
{
|
|
318
|
-
|
|
319
|
-
|
|
321
|
+
.await;
|
|
322
|
+
let pending = match receipt {
|
|
323
|
+
Ok(pending) => pending,
|
|
324
|
+
Err(e) => {
|
|
325
|
+
eprintln!("[live-upload] {rec}: tail chunk #{index} failed: {e}");
|
|
326
|
+
return Err(e);
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
if is_last {
|
|
330
|
+
verification_pending = pending;
|
|
320
331
|
}
|
|
321
332
|
offset += take;
|
|
322
333
|
index += 1;
|
|
@@ -331,7 +342,7 @@ async fn live_upload_loop(
|
|
|
331
342
|
"[live-upload] {rec}: sending final post #{index} (total={}, duration_ms={duration_ms})",
|
|
332
343
|
index + 1
|
|
333
344
|
);
|
|
334
|
-
|
|
345
|
+
verification_pending = match send_live_upload_post_with_retry(
|
|
335
346
|
&client,
|
|
336
347
|
&ctrl,
|
|
337
348
|
¶ms,
|
|
@@ -344,13 +355,19 @@ async fn live_upload_loop(
|
|
|
344
355
|
)
|
|
345
356
|
.await
|
|
346
357
|
{
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
358
|
+
Ok(pending) => pending,
|
|
359
|
+
Err(e) => {
|
|
360
|
+
eprintln!("[live-upload] {rec}: final post failed: {e}");
|
|
361
|
+
return Err(e);
|
|
362
|
+
}
|
|
363
|
+
};
|
|
350
364
|
}
|
|
351
365
|
emit_native_upload_progress(&app, "opening", "Uploading clip", None, Some(1.0));
|
|
352
366
|
eprintln!("[live-upload] {rec}: done — {index} post(s), {final_len} bytes total");
|
|
353
|
-
return Ok(
|
|
367
|
+
return Ok(LiveUploadResult {
|
|
368
|
+
bytes: final_len,
|
|
369
|
+
verification_pending,
|
|
370
|
+
});
|
|
354
371
|
}
|
|
355
372
|
|
|
356
373
|
if !wait_logged {
|
|
@@ -692,6 +692,7 @@ pub struct NativeFullscreenUploadResult {
|
|
|
692
692
|
width: Option<u32>,
|
|
693
693
|
height: Option<u32>,
|
|
694
694
|
bytes: u64,
|
|
695
|
+
verification_pending: bool,
|
|
695
696
|
}
|
|
696
697
|
|
|
697
698
|
#[derive(Serialize, Clone)]
|
|
@@ -1388,20 +1389,23 @@ pub async fn native_fullscreen_recording_stop_and_upload(
|
|
|
1388
1389
|
eprintln!(
|
|
1389
1390
|
"[live-upload] stop: finalize result for {recording_id}: {}",
|
|
1390
1391
|
match &result {
|
|
1391
|
-
Ok(
|
|
1392
|
+
Ok(upload) => format!("ok ({} bytes)", upload.bytes),
|
|
1392
1393
|
Err(e) => format!("error: {e}"),
|
|
1393
1394
|
}
|
|
1394
1395
|
);
|
|
1395
1396
|
return match result {
|
|
1396
|
-
Ok(
|
|
1397
|
-
|
|
1397
|
+
Ok(upload) => {
|
|
1398
|
+
if !upload.verification_pending {
|
|
1399
|
+
clear_saved_recording_after_success(&app, &saved);
|
|
1400
|
+
}
|
|
1398
1401
|
emit_native_upload_finished(&app, &server_url, &recording_id, true, None, None);
|
|
1399
1402
|
Ok(NativeFullscreenUploadResult {
|
|
1400
1403
|
recording_id,
|
|
1401
1404
|
duration_ms: verified_duration_ms,
|
|
1402
1405
|
width: session.width,
|
|
1403
1406
|
height: session.height,
|
|
1404
|
-
bytes,
|
|
1407
|
+
bytes: upload.bytes,
|
|
1408
|
+
verification_pending: upload.verification_pending,
|
|
1405
1409
|
})
|
|
1406
1410
|
}
|
|
1407
1411
|
Err(err) => {
|
|
@@ -1481,7 +1485,9 @@ pub async fn native_fullscreen_recording_stop_and_upload(
|
|
|
1481
1485
|
|
|
1482
1486
|
match result {
|
|
1483
1487
|
Ok(result) => {
|
|
1484
|
-
|
|
1488
|
+
if !result.verification_pending {
|
|
1489
|
+
clear_saved_recording_after_success(&app, &saved);
|
|
1490
|
+
}
|
|
1485
1491
|
emit_native_upload_finished(&app, &server_url, &recording_id, true, None, None);
|
|
1486
1492
|
Ok(result)
|
|
1487
1493
|
}
|
|
@@ -2599,7 +2605,9 @@ pub async fn native_fullscreen_recording_retry_upload(
|
|
|
2599
2605
|
|
|
2600
2606
|
match result {
|
|
2601
2607
|
Ok(result) => {
|
|
2602
|
-
|
|
2608
|
+
if !result.verification_pending {
|
|
2609
|
+
clear_saved_recording_after_success(&app, &saved);
|
|
2610
|
+
}
|
|
2603
2611
|
Ok(result)
|
|
2604
2612
|
}
|
|
2605
2613
|
Err(err) => {
|
|
@@ -2618,6 +2626,20 @@ pub async fn native_fullscreen_recording_retry_upload(
|
|
|
2618
2626
|
}
|
|
2619
2627
|
}
|
|
2620
2628
|
|
|
2629
|
+
#[tauri::command]
|
|
2630
|
+
pub async fn native_fullscreen_recording_mark_upload_error(
|
|
2631
|
+
app: AppHandle,
|
|
2632
|
+
recording_id: String,
|
|
2633
|
+
error: String,
|
|
2634
|
+
) -> Result<(), String> {
|
|
2635
|
+
let mut saved = read_saved_recording_metadata(&app, &recording_id)?;
|
|
2636
|
+
saved.last_attempt_at = Some(now_iso());
|
|
2637
|
+
saved.last_error = Some(error);
|
|
2638
|
+
write_saved_recording_metadata(&app, &saved)?;
|
|
2639
|
+
let _ = app.emit("clips:pending-uploads-changed", ());
|
|
2640
|
+
Ok(())
|
|
2641
|
+
}
|
|
2642
|
+
|
|
2621
2643
|
#[tauri::command]
|
|
2622
2644
|
pub async fn native_fullscreen_recording_dismiss_upload(
|
|
2623
2645
|
app: AppHandle,
|
|
@@ -4003,6 +4025,7 @@ async fn upload_prepared_recording_file(
|
|
|
4003
4025
|
.map_err(|e| format!("upload client failed: {e}"))?;
|
|
4004
4026
|
let mut file =
|
|
4005
4027
|
File::open(&prepared.path).map_err(|e| format!("native recording open failed: {e}"))?;
|
|
4028
|
+
let verification_pending;
|
|
4006
4029
|
|
|
4007
4030
|
if upload_mode == NativeUploadMode::Streaming {
|
|
4008
4031
|
// Resumable providers require every non-final body to be aligned. The
|
|
@@ -4055,7 +4078,7 @@ async fn upload_prepared_recording_file(
|
|
|
4055
4078
|
None,
|
|
4056
4079
|
Some(streaming_full_chunks as f32 / total_posts as f32),
|
|
4057
4080
|
);
|
|
4058
|
-
send_upload_post(
|
|
4081
|
+
verification_pending = send_upload_post(
|
|
4059
4082
|
&client,
|
|
4060
4083
|
&server_url,
|
|
4061
4084
|
&recording_id,
|
|
@@ -4123,7 +4146,7 @@ async fn upload_prepared_recording_file(
|
|
|
4123
4146
|
None,
|
|
4124
4147
|
Some(total_chunks as f32 / total_posts as f32),
|
|
4125
4148
|
);
|
|
4126
|
-
send_upload_post(
|
|
4149
|
+
verification_pending = send_upload_post(
|
|
4127
4150
|
&client,
|
|
4128
4151
|
&server_url,
|
|
4129
4152
|
&recording_id,
|
|
@@ -4153,6 +4176,7 @@ async fn upload_prepared_recording_file(
|
|
|
4153
4176
|
width,
|
|
4154
4177
|
height,
|
|
4155
4178
|
bytes: total_bytes,
|
|
4179
|
+
verification_pending,
|
|
4156
4180
|
})
|
|
4157
4181
|
}
|
|
4158
4182
|
|
|
@@ -4270,7 +4294,7 @@ async fn send_upload_post(
|
|
|
4270
4294
|
locally_transcoded: bool,
|
|
4271
4295
|
expected_source_bytes: Option<u64>,
|
|
4272
4296
|
body: Vec<u8>,
|
|
4273
|
-
) -> Result<
|
|
4297
|
+
) -> Result<bool, String> {
|
|
4274
4298
|
let body_len = body.len();
|
|
4275
4299
|
let url = upload_url(
|
|
4276
4300
|
server_url,
|
|
@@ -4319,7 +4343,7 @@ async fn send_upload_post(
|
|
|
4319
4343
|
body.chars().take(400).collect::<String>()
|
|
4320
4344
|
));
|
|
4321
4345
|
}
|
|
4322
|
-
if is_final {
|
|
4346
|
+
let verification_pending = if is_final {
|
|
4323
4347
|
verify_native_finalize_receipt(
|
|
4324
4348
|
&body,
|
|
4325
4349
|
expected_source_bytes.ok_or_else(|| {
|
|
@@ -4328,13 +4352,15 @@ async fn send_upload_post(
|
|
|
4328
4352
|
duration_ms.ok_or_else(|| {
|
|
4329
4353
|
"Clip may be incomplete: final upload had no local duration".to_string()
|
|
4330
4354
|
})?,
|
|
4331
|
-
)
|
|
4332
|
-
}
|
|
4355
|
+
)?
|
|
4356
|
+
} else {
|
|
4357
|
+
false
|
|
4358
|
+
};
|
|
4333
4359
|
eprintln!(
|
|
4334
4360
|
"[clips-tray] native upload post ok recording={recording_id} mode={} index={index}/{total} final={is_final}",
|
|
4335
4361
|
upload_mode.label()
|
|
4336
4362
|
);
|
|
4337
|
-
Ok(
|
|
4363
|
+
Ok(verification_pending)
|
|
4338
4364
|
}
|
|
4339
4365
|
|
|
4340
4366
|
#[derive(Deserialize)]
|
|
@@ -4343,6 +4369,7 @@ struct NativeFinalizeReceipt {
|
|
|
4343
4369
|
ok: bool,
|
|
4344
4370
|
finalized: bool,
|
|
4345
4371
|
status: Option<String>,
|
|
4372
|
+
verification_pending: Option<bool>,
|
|
4346
4373
|
source_size_bytes: Option<u64>,
|
|
4347
4374
|
duration_ms: Option<u128>,
|
|
4348
4375
|
}
|
|
@@ -4351,12 +4378,17 @@ fn verify_native_finalize_receipt(
|
|
|
4351
4378
|
body: &str,
|
|
4352
4379
|
expected_source_bytes: u64,
|
|
4353
4380
|
expected_duration_ms: u128,
|
|
4354
|
-
) -> Result<
|
|
4381
|
+
) -> Result<bool, String> {
|
|
4355
4382
|
let receipt: NativeFinalizeReceipt = serde_json::from_str(body).map_err(|_| {
|
|
4356
4383
|
"Clip may be incomplete. The final upload receipt was unreadable; the local backup was kept."
|
|
4357
4384
|
.to_string()
|
|
4358
4385
|
})?;
|
|
4359
|
-
|
|
4386
|
+
let ready = receipt.ok && receipt.finalized && receipt.status.as_deref() == Some("ready");
|
|
4387
|
+
let processing = receipt.ok
|
|
4388
|
+
&& !receipt.finalized
|
|
4389
|
+
&& receipt.status.as_deref() == Some("processing")
|
|
4390
|
+
&& receipt.verification_pending == Some(true);
|
|
4391
|
+
if !ready && !processing {
|
|
4360
4392
|
return Err(
|
|
4361
4393
|
"Clip may be incomplete. Finalization was not confirmed; the local backup was kept."
|
|
4362
4394
|
.to_string(),
|
|
@@ -4374,7 +4406,7 @@ fn verify_native_finalize_receipt(
|
|
|
4374
4406
|
.to_string(),
|
|
4375
4407
|
);
|
|
4376
4408
|
}
|
|
4377
|
-
Ok(
|
|
4409
|
+
Ok(processing)
|
|
4378
4410
|
}
|
|
4379
4411
|
|
|
4380
4412
|
fn media_durations_materially_match(expected_ms: u128, actual_ms: u128) -> bool {
|
|
@@ -4399,9 +4431,18 @@ mod native_finalize_receipt_tests {
|
|
|
4399
4431
|
}
|
|
4400
4432
|
|
|
4401
4433
|
#[test]
|
|
4402
|
-
fn
|
|
4434
|
+
fn accepts_matching_ready_and_pending_verification_receipts() {
|
|
4403
4435
|
let ready = r#"{"ok":true,"finalized":true,"status":"ready","sourceSizeBytes":581614005,"durationMs":1593259}"#;
|
|
4404
|
-
|
|
4436
|
+
assert_eq!(
|
|
4437
|
+
verify_native_finalize_receipt(ready, 581_614_005, 1_592_773),
|
|
4438
|
+
Ok(false)
|
|
4439
|
+
);
|
|
4440
|
+
|
|
4441
|
+
let processing = r#"{"ok":true,"finalized":false,"status":"processing","verificationPending":true,"sourceSizeBytes":581614005,"durationMs":1593259}"#;
|
|
4442
|
+
assert_eq!(
|
|
4443
|
+
verify_native_finalize_receipt(processing, 581_614_005, 1_592_773),
|
|
4444
|
+
Ok(true)
|
|
4445
|
+
);
|
|
4405
4446
|
|
|
4406
4447
|
let short = r#"{"ok":true,"finalized":true,"status":"ready","sourceSizeBytes":581614005,"durationMs":483000}"#;
|
|
4407
4448
|
assert!(
|
|
@@ -586,7 +586,15 @@ export const dictations = table("clips_dictations", {
|
|
|
586
586
|
// are text-only since native recognition runs on-device.
|
|
587
587
|
audioUrl: text("audio_url"),
|
|
588
588
|
source: text("source", {
|
|
589
|
-
enum: [
|
|
589
|
+
enum: [
|
|
590
|
+
"fn-hold",
|
|
591
|
+
"cmd-shift-space",
|
|
592
|
+
"manual",
|
|
593
|
+
"mobile",
|
|
594
|
+
"other",
|
|
595
|
+
"fn",
|
|
596
|
+
"custom",
|
|
597
|
+
],
|
|
590
598
|
})
|
|
591
599
|
.notNull()
|
|
592
600
|
.default("fn-hold"),
|