@agent-native/core 0.157.0 → 0.157.1
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 +1 -1
- package/corpus/templates/analytics/server/jobs/analytics-bigquery-backfill.ts +19 -4
- package/corpus/templates/analytics/server/lib/first-party-analytics-backend.ts +7 -2
- package/corpus/templates/assets/app/global.css +4 -0
- package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +1 -2
- package/corpus/templates/brain/app/routes/sources.tsx +1 -2
- package/corpus/templates/calendar/app/hooks/use-bookings.ts +12 -2
- package/corpus/templates/calendar/app/pages/BookingLinksPage.tsx +64 -2
- package/corpus/templates/calendar/server/handlers/bookings.ts +219 -93
- package/corpus/templates/calendar/server/plugins/auth.ts +0 -2
- package/corpus/templates/clips/actions/create-folder.ts +7 -1
- package/corpus/templates/clips/actions/create-meeting.ts +6 -2
- package/corpus/templates/clips/actions/create-recording.ts +5 -3
- package/corpus/templates/clips/actions/finalize-recording.ts +97 -65
- package/corpus/templates/clips/actions/import-loom-recording.ts +5 -3
- package/corpus/templates/clips/actions/request-recording-access.ts +272 -0
- package/corpus/templates/clips/actions/stitch-recordings.ts +5 -3
- package/corpus/templates/clips/app/components/meetings/share-meeting-dialog.tsx +38 -63
- package/corpus/templates/clips/app/components/player/share-dialog.tsx +75 -75
- package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +3 -21
- package/corpus/templates/clips/app/components/sharing/share-ui.tsx +13 -30
- package/corpus/templates/clips/app/i18n/en-US.ts +10 -0
- package/corpus/templates/clips/app/routes/_app.meetings.$meetingId.tsx +5 -5
- package/corpus/templates/clips/app/routes/r.$recordingId.tsx +9 -16
- package/corpus/templates/clips/app/routes/share.$shareId.tsx +135 -18
- package/corpus/templates/clips/changelog/2026-08-14-long-resumable-uploads-now-abort-cleanly-and-show-errors.md +6 -0
- package/corpus/templates/clips/changelog/2026-08-14-standardized-share-popovers.md +6 -0
- package/corpus/templates/clips/chrome-extension/public/manifest.json +11 -3
- package/corpus/templates/clips/chrome-extension/src/github-preview-content.ts +37 -8
- package/corpus/templates/clips/chrome-extension/src/github-preview.ts +3 -3
- package/corpus/templates/clips/chrome-extension/src/offscreen.ts +8 -25
- package/corpus/templates/clips/desktop/src/app.tsx +24 -0
- package/corpus/templates/clips/desktop/src/lib/media-capture-constraints.ts +42 -13
- package/corpus/templates/clips/desktop/src/lib/recorder.ts +11 -18
- package/corpus/templates/clips/desktop/src/overlays/onboarding.tsx +1 -0
- package/corpus/templates/clips/desktop/src/shared/config.ts +1 -0
- package/corpus/templates/clips/desktop/src/styles.css +24 -9
- package/corpus/templates/clips/desktop/src-tauri/Cargo.toml +3 -0
- package/corpus/templates/clips/desktop/src-tauri/src/config.rs +8 -0
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen/custom_capture.rs +154 -176
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +115 -31
- package/corpus/templates/clips/desktop/src-tauri/src/rewind_clip.rs +7 -0
- package/corpus/templates/clips/server/db/index.ts +1 -0
- package/corpus/templates/clips/server/db/schema.ts +1 -0
- package/corpus/templates/clips/server/lib/calendar-event-meetings.ts +6 -1
- package/corpus/templates/clips/server/lib/emails.ts +23 -0
- package/corpus/templates/clips/server/lib/public-agent-context.ts +55 -0
- package/corpus/templates/clips/server/lib/recordings.ts +2 -1
- package/corpus/templates/clips/server/lib/resumable-upload-cleanup.ts +49 -0
- package/corpus/templates/clips/server/lib/s3-upload-provider.ts +34 -7
- package/corpus/templates/clips/server/lib/upload-interruption.ts +15 -1
- package/corpus/templates/clips/server/lib/upload-lease.ts +94 -9
- package/corpus/templates/clips/server/routes/api/agent-context.json.get.ts +82 -7
- package/corpus/templates/clips/server/routes/api/public-recording.get.ts +1 -0
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +71 -12
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/interrupt.post.ts +4 -3
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/reset-chunks.post.ts +19 -0
- package/corpus/templates/clips/server/routes/api/uploads/reap.post.ts +8 -1
- package/corpus/templates/clips/shared/recording-link.ts +4 -0
- package/corpus/templates/clips/shared/share-loader-response.ts +2 -1
- package/corpus/templates/content/app/components/editor/DocumentToolbar.tsx +7 -16
- package/corpus/templates/factory/AGENTS.md +5 -1
- package/corpus/templates/factory/README.md +6 -2
- package/corpus/templates/factory/actions/view-screen.ts +15 -0
- package/corpus/templates/factory/app/components/factory/FactoryAgentsView.tsx +58 -189
- package/corpus/templates/factory/app/components/layout/Header.tsx +1 -0
- package/corpus/templates/factory/app/components/layout/Layout.tsx +1 -1
- package/corpus/templates/factory/app/components/layout/Sidebar.tsx +7 -0
- package/corpus/templates/factory/app/hooks/use-navigation-state.ts +3 -0
- package/corpus/templates/factory/app/i18n/en-US.ts +3 -1
- package/corpus/templates/factory/app/routes/agents.tsx +9 -0
- package/corpus/templates/mail/server/plugins/auth.ts +0 -2
- package/corpus/templates/plan/app/components/plan/LocalCodebasePicker.tsx +50 -14
- package/corpus/templates/plan/app/pages/PlansPage.tsx +9 -19
- package/corpus/templates/slides/app/components/editor/HistoryPanel.tsx +7 -1
- package/corpus/templates/slides/app/context/DeckContext.tsx +71 -22
- package/corpus/templates/slides/app/lib/editor-shortcuts.ts +46 -0
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +13 -0
- package/dist/client/AgentPanel.d.ts +13 -1
- package/dist/client/AgentPanel.js +12 -10
- package/dist/client/AssistantChat.js +25 -5
- package/dist/client/HighlightedCodeBlock.js +12 -2
- package/dist/client/agent-chat/index.d.ts +1 -0
- package/dist/client/agent-chat/index.js +1 -0
- package/dist/client/chat/markdown-renderer.js +16 -3
- package/dist/client/chat-first/apps-rail.js +1 -1
- package/dist/client/components/ui/message-scroller.js +1 -1
- package/dist/client/settings/AccountSettingsCard.js +48 -19
- package/dist/client/sharing/ShareButton.d.ts +1 -2
- package/dist/client/sharing/ShareButton.js +5 -41
- package/dist/client/sharing/ShareDialog.js +5 -4
- package/dist/client/use-chat-threads.js +10 -14
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/localization/default-messages.d.ts +1 -0
- package/dist/localization/default-messages.js +1 -0
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +11 -11
- package/dist/provider-api/actions/provider-api.d.ts +15 -15
- package/dist/server/auth-marketing.d.ts +1 -0
- package/dist/server/auth-marketing.js +0 -2
- package/dist/server/auth.d.ts +1 -0
- package/dist/server/auth.js +18 -0
- package/dist/server/onboarding-html.d.ts +1 -0
- package/dist/server/onboarding-html.js +2 -88
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/shared/markdown-block-split.d.ts +16 -0
- package/dist/shared/markdown-block-split.js +81 -5
- package/dist/styles/agent-native.css +13 -0
- package/dist/voice/voice-cleanup-prompt.js +1 -0
- package/package.json +2 -2
- package/corpus/templates/clips/shared/mic-audio-cleanup.ts +0 -350
|
@@ -64,22 +64,11 @@ const AUDIO_SIGNAL_MIN_MAX_VOLUME_DB: f64 = -30.0;
|
|
|
64
64
|
// stereo content that must not be flattened.
|
|
65
65
|
const AUDIO_DOWNMIX_FILTER: &str =
|
|
66
66
|
"aformat=channel_layouts=stereo,pan=stereo|FL=0.5*FL+0.5*FR|FR=0.5*FL+0.5*FR";
|
|
67
|
-
// The legacy ScreenCaptureKit output carries mic + system audio together on
|
|
68
|
-
// the two source channels. Remove only the narrow mains tones before the
|
|
69
|
-
// centered downmix; do not apply the mic-only high-pass/expander to system
|
|
70
|
-
// audio, which may legitimately contain bass.
|
|
71
|
-
const AUDIO_MIXED_HUM_FILTER: &str =
|
|
72
|
-
"bandreject=f=50:t=q:w=18,bandreject=f=60:t=q:w=18,bandreject=f=100:t=q:w=18,bandreject=f=120:t=q:w=18";
|
|
73
67
|
// Mild FFT denoise for native mic captures. Browser recordings already request
|
|
74
68
|
// WebRTC noise suppression; ScreenCaptureKit/screencapture mic capture does
|
|
75
69
|
// not, so reduce steady broadband room/mic hiss during the existing optimize
|
|
76
70
|
// step. Keep this conservative to avoid watery speech artifacts.
|
|
77
71
|
const AUDIO_DENOISE_FILTER: &str = "afftdn=nr=10:nf=-50:tn=1";
|
|
78
|
-
// The FFT pass above is intentionally broad and does not reliably remove a
|
|
79
|
-
// tonal HVAC hum. Apply this narrower chain only to mic-only captures so
|
|
80
|
-
// system audio/music keeps its low end when both sources are recorded.
|
|
81
|
-
const AUDIO_MIC_CLEANUP_FILTER: &str =
|
|
82
|
-
"highpass=f=65,bandreject=f=50:t=q:w=18,bandreject=f=60:t=q:w=18,bandreject=f=100:t=q:w=18,bandreject=f=120:t=q:w=18,agate=threshold=0.008:ratio=2:range=0.35:attack=10:release=180:knee=4:detection=rms";
|
|
83
72
|
// loudnorm operates internally at 192 kHz and emits at 192 kHz; without an
|
|
84
73
|
// explicit output rate the AAC track ends up at 192 kHz and plays back slow.
|
|
85
74
|
const AUDIO_OUTPUT_SAMPLE_RATE: u32 = 48000;
|
|
@@ -97,21 +86,19 @@ const AUDIO_MIC_PREGAIN_FILTER: &str = "volume=12dB";
|
|
|
97
86
|
const AUDIO_DOWNMIX_MAKEUP_FILTER: &str = "volume=6dB";
|
|
98
87
|
|
|
99
88
|
// Loudness normalization, optionally preceded by the centered-stereo downmix
|
|
100
|
-
// that repairs the mic+system L/R split
|
|
89
|
+
// that repairs the legacy mic+system L/R split. Realtime custom captures are
|
|
90
|
+
// denoised before they reach the writer, so this pass stays for legacy files
|
|
91
|
+
// and non-ScreenCaptureKit fallbacks.
|
|
101
92
|
// Pair with `-ar AUDIO_OUTPUT_SAMPLE_RATE` so loudnorm's 192 kHz output is
|
|
102
93
|
// resampled back.
|
|
103
94
|
fn audio_filter_chain(downmix: bool, denoise: bool, mic_pregain: bool) -> String {
|
|
104
95
|
let mut filters = Vec::new();
|
|
105
96
|
if downmix {
|
|
106
|
-
filters.push(AUDIO_MIXED_HUM_FILTER);
|
|
107
97
|
filters.push(AUDIO_DOWNMIX_FILTER);
|
|
108
98
|
// Undo pan attenuation; do not also apply mic-only pregain here —
|
|
109
99
|
// system audio would get a double boost.
|
|
110
100
|
filters.push(AUDIO_DOWNMIX_MAKEUP_FILTER);
|
|
111
101
|
}
|
|
112
|
-
if mic_pregain {
|
|
113
|
-
filters.push(AUDIO_MIC_CLEANUP_FILTER);
|
|
114
|
-
}
|
|
115
102
|
if denoise {
|
|
116
103
|
filters.push(AUDIO_DENOISE_FILTER);
|
|
117
104
|
}
|
|
@@ -345,6 +332,9 @@ struct NativeFullscreenSession {
|
|
|
345
332
|
/// nothing is written to disk. `begin` attaches it and flips this false.
|
|
346
333
|
pending_recording_output: bool,
|
|
347
334
|
custom_pipeline: bool,
|
|
335
|
+
/// True when the native realtime microphone denoiser already touched the
|
|
336
|
+
/// audio in this file. Retry preparation must not denoise it a second time.
|
|
337
|
+
audio_cleanup_applied: bool,
|
|
348
338
|
/// Active live-upload task: streams the growing fragmented MP4 to the
|
|
349
339
|
/// server in chunks during recording (custom pipeline only). `None` when
|
|
350
340
|
/// live upload is disabled or for local-only recordings.
|
|
@@ -645,6 +635,7 @@ pub(crate) fn prepare_shared_clip_sink(
|
|
|
645
635
|
request.height,
|
|
646
636
|
request.include_mic,
|
|
647
637
|
request.include_system_audio,
|
|
638
|
+
crate::config::feature_config(app).voice_cleanup_enabled,
|
|
648
639
|
) {
|
|
649
640
|
Ok(sink) => sink,
|
|
650
641
|
Err(error) => {
|
|
@@ -1047,6 +1038,9 @@ struct SavedNativeRecording {
|
|
|
1047
1038
|
retry_attempt_id: Option<String>,
|
|
1048
1039
|
#[serde(default)]
|
|
1049
1040
|
custom_pipeline: bool,
|
|
1041
|
+
/// True when native realtime microphone cleanup was already applied.
|
|
1042
|
+
#[serde(default)]
|
|
1043
|
+
audio_cleanup_applied: bool,
|
|
1050
1044
|
/// True when the SCK finalization callback reported an error, meaning the
|
|
1051
1045
|
/// MP4 is missing its moov atom and cannot be recovered by retrying.
|
|
1052
1046
|
#[serde(default)]
|
|
@@ -1070,6 +1064,8 @@ struct NativeRecordingIntent {
|
|
|
1070
1064
|
system_audio_captured: bool,
|
|
1071
1065
|
has_camera: bool,
|
|
1072
1066
|
custom_pipeline: bool,
|
|
1067
|
+
#[serde(default)]
|
|
1068
|
+
audio_cleanup_applied: bool,
|
|
1073
1069
|
saved_at: String,
|
|
1074
1070
|
}
|
|
1075
1071
|
|
|
@@ -1183,6 +1179,7 @@ pub(crate) struct FinalizedNativeArtifact {
|
|
|
1183
1179
|
pub system_audio_captured: bool,
|
|
1184
1180
|
pub has_camera: bool,
|
|
1185
1181
|
pub custom_pipeline: bool,
|
|
1182
|
+
pub audio_cleanup_applied: bool,
|
|
1186
1183
|
}
|
|
1187
1184
|
|
|
1188
1185
|
impl FinalizedNativeArtifact {
|
|
@@ -1197,6 +1194,7 @@ impl FinalizedNativeArtifact {
|
|
|
1197
1194
|
system_audio_captured: session.restart.capture_system_audio,
|
|
1198
1195
|
has_camera: false,
|
|
1199
1196
|
custom_pipeline: session.custom_pipeline,
|
|
1197
|
+
audio_cleanup_applied: session.audio_cleanup_applied,
|
|
1200
1198
|
}
|
|
1201
1199
|
}
|
|
1202
1200
|
|
|
@@ -1218,6 +1216,7 @@ impl FinalizedNativeArtifact {
|
|
|
1218
1216
|
system_audio_captured,
|
|
1219
1217
|
has_camera: false,
|
|
1220
1218
|
custom_pipeline: true,
|
|
1219
|
+
audio_cleanup_applied: false,
|
|
1221
1220
|
}
|
|
1222
1221
|
}
|
|
1223
1222
|
}
|
|
@@ -1427,6 +1426,7 @@ fn persist_active_recording_intent(
|
|
|
1427
1426
|
session.restart.capture_system_audio,
|
|
1428
1427
|
has_camera,
|
|
1429
1428
|
session.custom_pipeline,
|
|
1429
|
+
session.audio_cleanup_applied,
|
|
1430
1430
|
) {
|
|
1431
1431
|
eprintln!("[clips-tray] native recording recovery intent unavailable: {error}");
|
|
1432
1432
|
}
|
|
@@ -1783,14 +1783,20 @@ pub async fn native_fullscreen_recording_stop_and_upload(
|
|
|
1783
1783
|
|
|
1784
1784
|
if multi_segment {
|
|
1785
1785
|
if let Err(merge_err) = &consolidate_outcome {
|
|
1786
|
-
let mut saved = saved_recording_from_segments(
|
|
1786
|
+
let mut saved = match saved_recording_from_segments(
|
|
1787
1787
|
&session,
|
|
1788
1788
|
&server_url,
|
|
1789
1789
|
&recording_id,
|
|
1790
1790
|
duration_ms,
|
|
1791
1791
|
has_audio,
|
|
1792
1792
|
has_camera,
|
|
1793
|
-
)
|
|
1793
|
+
) {
|
|
1794
|
+
Ok(saved) => saved,
|
|
1795
|
+
Err(error) => {
|
|
1796
|
+
remove_empty_recording_artifacts(&session);
|
|
1797
|
+
return Err(error);
|
|
1798
|
+
}
|
|
1799
|
+
};
|
|
1794
1800
|
saved.last_error = Some(match &stop_outcome {
|
|
1795
1801
|
Err(stop_err) => {
|
|
1796
1802
|
format!("{stop_err}. Segment consolidation failed: {merge_err}")
|
|
@@ -1814,14 +1820,20 @@ pub async fn native_fullscreen_recording_stop_and_upload(
|
|
|
1814
1820
|
}
|
|
1815
1821
|
}
|
|
1816
1822
|
|
|
1817
|
-
let mut saved = saved_recording_from_session(
|
|
1823
|
+
let mut saved = match saved_recording_from_session(
|
|
1818
1824
|
&session,
|
|
1819
1825
|
&server_url,
|
|
1820
1826
|
&recording_id,
|
|
1821
1827
|
duration_ms,
|
|
1822
1828
|
has_audio,
|
|
1823
1829
|
has_camera,
|
|
1824
|
-
)
|
|
1830
|
+
) {
|
|
1831
|
+
Ok(saved) => saved,
|
|
1832
|
+
Err(error) => {
|
|
1833
|
+
remove_empty_recording_artifacts(&session);
|
|
1834
|
+
return Err(error);
|
|
1835
|
+
}
|
|
1836
|
+
};
|
|
1825
1837
|
let stop_error = stop_outcome.err();
|
|
1826
1838
|
if let Some(stop_err) = &stop_error {
|
|
1827
1839
|
// Capture-side finalize/write failure: the on-disk file is incomplete
|
|
@@ -3150,6 +3162,12 @@ pub async fn native_fullscreen_pending_uploads(
|
|
|
3150
3162
|
continue;
|
|
3151
3163
|
}
|
|
3152
3164
|
let Ok(saved) = read_saved_recording_metadata_path(&path) else {
|
|
3165
|
+
if std::fs::metadata(&path)
|
|
3166
|
+
.map(|metadata| metadata.len() == 0)
|
|
3167
|
+
.unwrap_or(false)
|
|
3168
|
+
{
|
|
3169
|
+
let _ = std::fs::remove_file(&path);
|
|
3170
|
+
}
|
|
3153
3171
|
continue;
|
|
3154
3172
|
};
|
|
3155
3173
|
if saved_recording_has_local_artifact(&saved) {
|
|
@@ -3316,6 +3334,7 @@ fn orphan_recording_intent(
|
|
|
3316
3334
|
system_audio_captured: false,
|
|
3317
3335
|
has_camera: false,
|
|
3318
3336
|
custom_pipeline: is_rewind,
|
|
3337
|
+
audio_cleanup_applied: false,
|
|
3319
3338
|
saved_at: now_iso(),
|
|
3320
3339
|
}
|
|
3321
3340
|
}
|
|
@@ -3418,6 +3437,7 @@ fn recover_orphaned_recording(
|
|
|
3418
3437
|
retry_count: 0,
|
|
3419
3438
|
retry_attempt_id: None,
|
|
3420
3439
|
custom_pipeline: intent.custom_pipeline,
|
|
3440
|
+
audio_cleanup_applied: intent.audio_cleanup_applied,
|
|
3421
3441
|
corrupt: false,
|
|
3422
3442
|
};
|
|
3423
3443
|
write_saved_recording_metadata(app, &saved)?;
|
|
@@ -3537,6 +3557,7 @@ mod orphan_recovery_tests {
|
|
|
3537
3557
|
false,
|
|
3538
3558
|
false,
|
|
3539
3559
|
true,
|
|
3560
|
+
false,
|
|
3540
3561
|
)
|
|
3541
3562
|
.unwrap();
|
|
3542
3563
|
let intent_path = recording_intent_path(&media);
|
|
@@ -3932,6 +3953,7 @@ pub(crate) fn persist_recording_intent(
|
|
|
3932
3953
|
system_audio_captured: bool,
|
|
3933
3954
|
has_camera: bool,
|
|
3934
3955
|
custom_pipeline: bool,
|
|
3956
|
+
audio_cleanup_applied: bool,
|
|
3935
3957
|
) -> Result<(), String> {
|
|
3936
3958
|
if recording_id.trim().is_empty() || server_url.trim().is_empty() {
|
|
3937
3959
|
return Err("recording recovery intent requires a recording ID and server URL".into());
|
|
@@ -3952,6 +3974,7 @@ pub(crate) fn persist_recording_intent(
|
|
|
3952
3974
|
system_audio_captured,
|
|
3953
3975
|
has_camera,
|
|
3954
3976
|
custom_pipeline,
|
|
3977
|
+
audio_cleanup_applied,
|
|
3955
3978
|
saved_at: now_iso(),
|
|
3956
3979
|
};
|
|
3957
3980
|
let data = serde_json::to_vec_pretty(&intent)
|
|
@@ -4018,7 +4041,41 @@ fn describe_recording_path(path: &Path) -> String {
|
|
|
4018
4041
|
}
|
|
4019
4042
|
|
|
4020
4043
|
fn saved_recording_has_local_artifact(saved: &SavedNativeRecording) -> bool {
|
|
4021
|
-
saved.file_path
|
|
4044
|
+
recording_file_has_bytes(&saved.file_path)
|
|
4045
|
+
|| saved
|
|
4046
|
+
.segment_paths
|
|
4047
|
+
.iter()
|
|
4048
|
+
.any(|path| recording_file_has_bytes(path))
|
|
4049
|
+
}
|
|
4050
|
+
|
|
4051
|
+
fn recording_file_has_bytes(path: &Path) -> bool {
|
|
4052
|
+
std::fs::metadata(path)
|
|
4053
|
+
.map(|metadata| metadata.len() > 0)
|
|
4054
|
+
.unwrap_or(false)
|
|
4055
|
+
}
|
|
4056
|
+
|
|
4057
|
+
fn remove_empty_recording_artifacts(session: &NativeFullscreenSession) {
|
|
4058
|
+
for path in session.segments.iter().chain(std::iter::once(&session.path)) {
|
|
4059
|
+
if !path.exists() {
|
|
4060
|
+
continue;
|
|
4061
|
+
}
|
|
4062
|
+
if std::fs::metadata(path)
|
|
4063
|
+
.map(|metadata| metadata.len() > 0)
|
|
4064
|
+
.unwrap_or(true)
|
|
4065
|
+
{
|
|
4066
|
+
continue;
|
|
4067
|
+
}
|
|
4068
|
+
if let Err(error) = std::fs::remove_file(path) {
|
|
4069
|
+
if error.kind() != ErrorKind::NotFound {
|
|
4070
|
+
eprintln!(
|
|
4071
|
+
"[clips-tray] empty native recording cleanup failed for {}: {error}",
|
|
4072
|
+
path.display()
|
|
4073
|
+
);
|
|
4074
|
+
}
|
|
4075
|
+
} else {
|
|
4076
|
+
remove_recording_intent(path);
|
|
4077
|
+
}
|
|
4078
|
+
}
|
|
4022
4079
|
}
|
|
4023
4080
|
|
|
4024
4081
|
fn pending_uploads_dir(app: &AppHandle) -> Result<PathBuf, String> {
|
|
@@ -4412,6 +4469,7 @@ fn saved_recording_from_path(
|
|
|
4412
4469
|
retry_count: 0,
|
|
4413
4470
|
retry_attempt_id: None,
|
|
4414
4471
|
custom_pipeline: session.custom_pipeline,
|
|
4472
|
+
audio_cleanup_applied: session.audio_cleanup_applied,
|
|
4415
4473
|
corrupt: false,
|
|
4416
4474
|
})
|
|
4417
4475
|
}
|
|
@@ -4423,8 +4481,7 @@ fn write_saved_recording_metadata(
|
|
|
4423
4481
|
let path = saved_recording_metadata_path(app, &saved.recording_id)?;
|
|
4424
4482
|
let data = serde_json::to_vec_pretty(saved)
|
|
4425
4483
|
.map_err(|e| format!("pending recording metadata encode failed: {e}"))?;
|
|
4426
|
-
|
|
4427
|
-
.map_err(|e| format!("pending recording metadata write failed: {e}"))?;
|
|
4484
|
+
write_bytes_atomically(&path, &data, "pending recording metadata")?;
|
|
4428
4485
|
remove_recording_intent(&saved.file_path);
|
|
4429
4486
|
for segment_path in &saved.segment_paths {
|
|
4430
4487
|
remove_recording_intent(segment_path);
|
|
@@ -4432,6 +4489,21 @@ fn write_saved_recording_metadata(
|
|
|
4432
4489
|
Ok(())
|
|
4433
4490
|
}
|
|
4434
4491
|
|
|
4492
|
+
fn write_bytes_atomically(path: &Path, data: &[u8], label: &str) -> Result<(), String> {
|
|
4493
|
+
let file_name = path
|
|
4494
|
+
.file_name()
|
|
4495
|
+
.and_then(|value| value.to_str())
|
|
4496
|
+
.unwrap_or("recording");
|
|
4497
|
+
let temporary = path.with_file_name(format!(".{file_name}.tmp"));
|
|
4498
|
+
std::fs::write(&temporary, data)
|
|
4499
|
+
.map_err(|error| format!("{label} write failed: {error}"))?;
|
|
4500
|
+
if let Err(error) = std::fs::rename(&temporary, path) {
|
|
4501
|
+
let _ = std::fs::remove_file(&temporary);
|
|
4502
|
+
return Err(format!("{label} finalize failed: {error}"));
|
|
4503
|
+
}
|
|
4504
|
+
Ok(())
|
|
4505
|
+
}
|
|
4506
|
+
|
|
4435
4507
|
fn read_saved_recording_metadata_path(path: &Path) -> Result<SavedNativeRecording, String> {
|
|
4436
4508
|
let data =
|
|
4437
4509
|
std::fs::read(path).map_err(|e| format!("pending recording metadata read failed: {e}"))?;
|
|
@@ -4506,6 +4578,7 @@ pub(crate) fn persist_shared_clip_recording(
|
|
|
4506
4578
|
include_mic: bool,
|
|
4507
4579
|
include_system_audio: bool,
|
|
4508
4580
|
has_camera: bool,
|
|
4581
|
+
audio_cleanup_applied: bool,
|
|
4509
4582
|
error: Option<&str>,
|
|
4510
4583
|
) -> Result<(), String> {
|
|
4511
4584
|
let bytes = std::fs::metadata(path)
|
|
@@ -4534,6 +4607,7 @@ pub(crate) fn persist_shared_clip_recording(
|
|
|
4534
4607
|
retry_count: u32::from(error.is_some()),
|
|
4535
4608
|
retry_attempt_id: None,
|
|
4536
4609
|
custom_pipeline: true,
|
|
4610
|
+
audio_cleanup_applied,
|
|
4537
4611
|
corrupt: false,
|
|
4538
4612
|
};
|
|
4539
4613
|
write_saved_recording_metadata(app, &saved)?;
|
|
@@ -4745,6 +4819,9 @@ fn start_screencapturekit_recording(
|
|
|
4745
4819
|
capture_region,
|
|
4746
4820
|
},
|
|
4747
4821
|
);
|
|
4822
|
+
session.audio_cleanup_applied = use_custom_sck_pipeline
|
|
4823
|
+
&& include_audio
|
|
4824
|
+
&& crate::config::feature_config(app).voice_cleanup_enabled;
|
|
4748
4825
|
session.pending_recording_output = defer_recording_output;
|
|
4749
4826
|
session.disk_monitor_stop = Some(spawn_disk_monitor(app.clone(), session.path.clone()));
|
|
4750
4827
|
Ok(session)
|
|
@@ -4807,6 +4884,7 @@ fn start_screencapture_recording(
|
|
|
4807
4884
|
capture_region,
|
|
4808
4885
|
},
|
|
4809
4886
|
);
|
|
4887
|
+
session.audio_cleanup_applied = false;
|
|
4810
4888
|
session.disk_monitor_stop = Some(spawn_disk_monitor(app.clone(), session.path.clone()));
|
|
4811
4889
|
Ok(session)
|
|
4812
4890
|
}
|
|
@@ -4846,6 +4924,7 @@ fn new_fullscreen_session(
|
|
|
4846
4924
|
restart,
|
|
4847
4925
|
pending_recording_output: false,
|
|
4848
4926
|
custom_pipeline,
|
|
4927
|
+
audio_cleanup_applied: false,
|
|
4849
4928
|
#[cfg(target_os = "macos")]
|
|
4850
4929
|
live_upload: None,
|
|
4851
4930
|
had_live_upload: false,
|
|
@@ -5167,6 +5246,7 @@ pub(crate) async fn upload_finalized_native_artifact(
|
|
|
5167
5246
|
artifact.mic_captured,
|
|
5168
5247
|
artifact.system_audio_captured,
|
|
5169
5248
|
artifact.custom_pipeline,
|
|
5249
|
+
artifact.audio_cleanup_applied,
|
|
5170
5250
|
)?;
|
|
5171
5251
|
let upload_result = upload_prepared_recording_file(
|
|
5172
5252
|
app,
|
|
@@ -5233,6 +5313,7 @@ fn prepare_saved_recording_file(
|
|
|
5233
5313
|
saved.mic_captured,
|
|
5234
5314
|
saved.system_audio_captured,
|
|
5235
5315
|
saved.custom_pipeline,
|
|
5316
|
+
saved.audio_cleanup_applied,
|
|
5236
5317
|
)?;
|
|
5237
5318
|
Ok((prepared, retry_combined_path))
|
|
5238
5319
|
}
|
|
@@ -6712,13 +6793,18 @@ fn prepare_recording_file(
|
|
|
6712
6793
|
// track written by the custom pipeline; the L/R downmix repair assumes
|
|
6713
6794
|
// mic and system on separate stereo channels and must not run on it.
|
|
6714
6795
|
audio_premixed: bool,
|
|
6796
|
+
audio_cleanup_applied: bool,
|
|
6715
6797
|
) -> Result<PreparedRecordingFile, String> {
|
|
6716
6798
|
// Downmix only repairs mic+system L/R split. Applying it to mic-only
|
|
6717
6799
|
// capture halves speech energy (~6 dB) when SCK puts the mic on one channel.
|
|
6718
6800
|
let downmix_audio = mic_captured_audio && system_audio_captured && !audio_premixed;
|
|
6719
|
-
let
|
|
6801
|
+
let voice_cleanup_enabled = crate::config::feature_config(app).voice_cleanup_enabled;
|
|
6802
|
+
let denoise_audio = mic_captured_audio && !audio_cleanup_applied && voice_cleanup_enabled;
|
|
6720
6803
|
// Pregain only for mic-only (no system audio compete) — SCK has no AGC.
|
|
6721
|
-
let mic_pregain = mic_captured_audio
|
|
6804
|
+
let mic_pregain = mic_captured_audio
|
|
6805
|
+
&& !system_audio_captured
|
|
6806
|
+
&& !audio_cleanup_applied
|
|
6807
|
+
&& voice_cleanup_enabled;
|
|
6722
6808
|
let metadata = std::fs::metadata(path).map_err(|e| {
|
|
6723
6809
|
let diag = describe_recording_path(path);
|
|
6724
6810
|
eprintln!("[clips-tray] native recording file missing at prepare: {e}; {diag}");
|
|
@@ -8176,8 +8262,7 @@ mod audio_track_probe_tests {
|
|
|
8176
8262
|
audio_filter_chain, decide_prepared_audio_signal, mp4_has_audio_track,
|
|
8177
8263
|
parse_ffmpeg_volume_db, AudioSignalProbe, PreparedAudioSignalDecision,
|
|
8178
8264
|
AUDIO_DENOISE_FILTER, AUDIO_DOWNMIX_FILTER, AUDIO_DOWNMIX_MAKEUP_FILTER,
|
|
8179
|
-
AUDIO_LOUDNESS_FILTER,
|
|
8180
|
-
AUDIO_MIXED_HUM_FILTER,
|
|
8265
|
+
AUDIO_LOUDNESS_FILTER, AUDIO_MIC_PREGAIN_FILTER,
|
|
8181
8266
|
};
|
|
8182
8267
|
use std::io::Write;
|
|
8183
8268
|
|
|
@@ -8230,14 +8315,12 @@ mod audio_track_probe_tests {
|
|
|
8230
8315
|
);
|
|
8231
8316
|
assert_eq!(
|
|
8232
8317
|
audio_filter_chain(false, true, true),
|
|
8233
|
-
format!(
|
|
8234
|
-
"{AUDIO_MIC_CLEANUP_FILTER},{AUDIO_DENOISE_FILTER},{AUDIO_MIC_PREGAIN_FILTER},{AUDIO_LOUDNESS_FILTER}"
|
|
8235
|
-
)
|
|
8318
|
+
format!("{AUDIO_DENOISE_FILTER},{AUDIO_MIC_PREGAIN_FILTER},{AUDIO_LOUDNESS_FILTER}")
|
|
8236
8319
|
);
|
|
8237
8320
|
assert_eq!(
|
|
8238
8321
|
audio_filter_chain(true, true, false),
|
|
8239
8322
|
format!(
|
|
8240
|
-
"{
|
|
8323
|
+
"{AUDIO_DOWNMIX_FILTER},{AUDIO_DOWNMIX_MAKEUP_FILTER},{AUDIO_DENOISE_FILTER},{AUDIO_LOUDNESS_FILTER}"
|
|
8241
8324
|
)
|
|
8242
8325
|
);
|
|
8243
8326
|
}
|
|
@@ -8450,6 +8533,7 @@ mod segment_recovery_tests {
|
|
|
8450
8533
|
},
|
|
8451
8534
|
pending_recording_output: false,
|
|
8452
8535
|
custom_pipeline: false,
|
|
8536
|
+
audio_cleanup_applied: false,
|
|
8453
8537
|
#[cfg(target_os = "macos")]
|
|
8454
8538
|
live_upload: None,
|
|
8455
8539
|
had_live_upload: false,
|
|
@@ -186,6 +186,7 @@ pub(crate) fn rewind_clip_prepare(
|
|
|
186
186
|
include_system_audio,
|
|
187
187
|
has_camera,
|
|
188
188
|
true,
|
|
189
|
+
crate::config::feature_config(&app).voice_cleanup_enabled && include_mic,
|
|
189
190
|
) {
|
|
190
191
|
shared_sink.cancel();
|
|
191
192
|
release_temporary_audio(&app, temporary_audio);
|
|
@@ -1129,6 +1130,7 @@ fn materialize(
|
|
|
1129
1130
|
include_system_audio,
|
|
1130
1131
|
has_camera,
|
|
1131
1132
|
true,
|
|
1133
|
+
false,
|
|
1132
1134
|
)?;
|
|
1133
1135
|
}
|
|
1134
1136
|
native_screen::materialize_mp4_slices_exact(&slices, &output, audio)?;
|
|
@@ -1220,6 +1222,7 @@ pub(crate) async fn rewind_clip_stop_and_upload(
|
|
|
1220
1222
|
include_mic,
|
|
1221
1223
|
include_system_audio,
|
|
1222
1224
|
has_camera,
|
|
1225
|
+
crate::config::feature_config(&app).voice_cleanup_enabled && include_mic,
|
|
1223
1226
|
Some(&error),
|
|
1224
1227
|
)
|
|
1225
1228
|
.err();
|
|
@@ -1251,6 +1254,7 @@ pub(crate) async fn rewind_clip_stop_and_upload(
|
|
|
1251
1254
|
include_mic,
|
|
1252
1255
|
include_system_audio,
|
|
1253
1256
|
has_camera,
|
|
1257
|
+
crate::config::feature_config(&app).voice_cleanup_enabled && include_mic,
|
|
1254
1258
|
None,
|
|
1255
1259
|
) {
|
|
1256
1260
|
sink.cancel_upload();
|
|
@@ -1278,6 +1282,7 @@ pub(crate) async fn rewind_clip_stop_and_upload(
|
|
|
1278
1282
|
include_mic,
|
|
1279
1283
|
include_system_audio,
|
|
1280
1284
|
has_camera,
|
|
1285
|
+
crate::config::feature_config(&app).voice_cleanup_enabled && include_mic,
|
|
1281
1286
|
Some(&error),
|
|
1282
1287
|
);
|
|
1283
1288
|
native_screen::emit_native_upload_finished(
|
|
@@ -1343,6 +1348,7 @@ pub(crate) async fn rewind_clip_stop_and_upload(
|
|
|
1343
1348
|
include_mic,
|
|
1344
1349
|
include_system_audio,
|
|
1345
1350
|
has_camera,
|
|
1351
|
+
artifact.audio_cleanup_applied,
|
|
1346
1352
|
None,
|
|
1347
1353
|
)?;
|
|
1348
1354
|
let recovery_server_url = server_url.clone();
|
|
@@ -1380,6 +1386,7 @@ pub(crate) async fn rewind_clip_stop_and_upload(
|
|
|
1380
1386
|
include_mic,
|
|
1381
1387
|
include_system_audio,
|
|
1382
1388
|
has_camera,
|
|
1389
|
+
artifact.audio_cleanup_applied,
|
|
1383
1390
|
Some(error),
|
|
1384
1391
|
);
|
|
1385
1392
|
}
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
import {
|
|
18
18
|
getActiveOrganizationId,
|
|
19
19
|
getCurrentOwnerEmail,
|
|
20
|
+
getDefaultRecordingVisibility,
|
|
20
21
|
nanoid,
|
|
21
22
|
} from "./recordings.js";
|
|
22
23
|
|
|
@@ -423,6 +424,10 @@ export async function materializeCalendarMeetingFromVirtualId(
|
|
|
423
424
|
const db = getDb();
|
|
424
425
|
const ownerEmail = getCurrentOwnerEmail();
|
|
425
426
|
const orgId = (await getActiveOrganizationId().catch(() => null)) ?? null;
|
|
427
|
+
const defaultVisibility = await getDefaultRecordingVisibility(
|
|
428
|
+
orgId ?? live.account.orgId,
|
|
429
|
+
ownerEmail,
|
|
430
|
+
);
|
|
426
431
|
const joinUrl = pickJoinUrl(live.event);
|
|
427
432
|
const meetingId = nanoid();
|
|
428
433
|
const nowIso = new Date().toISOString();
|
|
@@ -508,7 +513,7 @@ export async function materializeCalendarMeetingFromVirtualId(
|
|
|
508
513
|
updatedAt: nowIso,
|
|
509
514
|
ownerEmail,
|
|
510
515
|
orgId,
|
|
511
|
-
visibility:
|
|
516
|
+
visibility: defaultVisibility,
|
|
512
517
|
} as any);
|
|
513
518
|
|
|
514
519
|
const participants = calendarEventParticipants(live.event).filter(
|
|
@@ -12,6 +12,10 @@
|
|
|
12
12
|
import { defineTransactionalEmail } from "@agent-native/core/email-catalog";
|
|
13
13
|
|
|
14
14
|
import { renderClipsInviteEmail } from "../../actions/invite-member.js";
|
|
15
|
+
import {
|
|
16
|
+
CLIPS_ACCESS_REQUEST_EMAIL_ID,
|
|
17
|
+
renderRecordingAccessRequestEmail,
|
|
18
|
+
} from "../../actions/request-recording-access.js";
|
|
15
19
|
import {
|
|
16
20
|
CLIPS_ACTIVITY_COMMENT_EMAIL_ID,
|
|
17
21
|
CLIPS_ACTIVITY_REACTION_EMAIL_ID,
|
|
@@ -53,6 +57,25 @@ export function registerClipsEmails(): void {
|
|
|
53
57
|
if (registered) return;
|
|
54
58
|
registered = true;
|
|
55
59
|
|
|
60
|
+
defineTransactionalEmail({
|
|
61
|
+
id: CLIPS_ACCESS_REQUEST_EMAIL_ID,
|
|
62
|
+
name: "Clip access request",
|
|
63
|
+
trigger:
|
|
64
|
+
"A signed-in viewer requests access to a private Clip from its public share page. One request is recorded per viewer and Clip.",
|
|
65
|
+
recipientLabel: "Clip owner",
|
|
66
|
+
recipient:
|
|
67
|
+
"The owner of the private Clip. The in-app notification is stored even when email delivery is unavailable.",
|
|
68
|
+
senderLabel: "Agent-Native Clips",
|
|
69
|
+
sender: CLIPS_SENDER,
|
|
70
|
+
preview: () =>
|
|
71
|
+
renderRecordingAccessRequestEmail({
|
|
72
|
+
requesterName: "Sam Rivera",
|
|
73
|
+
requesterEmail: "sam.rivera@example.com",
|
|
74
|
+
recordingTitle: SAMPLE_TITLE,
|
|
75
|
+
url: "https://example.com/share/rec_sample",
|
|
76
|
+
}),
|
|
77
|
+
});
|
|
78
|
+
|
|
56
79
|
defineTransactionalEmail({
|
|
57
80
|
id: CLIPS_FIRST_VIEW_EMAIL_ID,
|
|
58
81
|
name: "First view on a Clip",
|
|
@@ -41,6 +41,25 @@ export type PublicAgentTranscript =
|
|
|
41
41
|
export type PublicAgentBugReport =
|
|
42
42
|
| typeof schema.recordingBugReports._.inferSelect
|
|
43
43
|
| null;
|
|
44
|
+
export type PublicAgentComment = {
|
|
45
|
+
id: string;
|
|
46
|
+
recordingId: string;
|
|
47
|
+
threadId: string;
|
|
48
|
+
parentId: string | null;
|
|
49
|
+
authorName: string | null;
|
|
50
|
+
content: string;
|
|
51
|
+
videoTimestampMs: number;
|
|
52
|
+
resolved: boolean;
|
|
53
|
+
createdAt: string;
|
|
54
|
+
updatedAt: string;
|
|
55
|
+
};
|
|
56
|
+
export type PublicAgentReaction = {
|
|
57
|
+
id: string;
|
|
58
|
+
emoji: string;
|
|
59
|
+
videoTimestampMs: number;
|
|
60
|
+
viewerName: string | null;
|
|
61
|
+
createdAt: string;
|
|
62
|
+
};
|
|
44
63
|
|
|
45
64
|
export interface PublicAgentAccess {
|
|
46
65
|
recording: PublicAgentRecording;
|
|
@@ -58,6 +77,7 @@ export type PublicAgentAccessResult =
|
|
|
58
77
|
| { ok: false; failure: PublicAgentFailure };
|
|
59
78
|
|
|
60
79
|
const DEFAULT_MAX_AGENT_FRAME_MEDIA_BYTES = 200 * 1024 * 1024;
|
|
80
|
+
export const MAX_PUBLIC_AGENT_HISTORY_ITEMS = 100;
|
|
61
81
|
export const CLIPS_AGENT_ACCESS_TTL_SECONDS = 2 * 60 * 60;
|
|
62
82
|
export { CLIPS_AGENT_ACCESS_PARAM };
|
|
63
83
|
|
|
@@ -525,6 +545,12 @@ export function buildPublicAgentContext({
|
|
|
525
545
|
transcript,
|
|
526
546
|
agentSegments,
|
|
527
547
|
chapters,
|
|
548
|
+
comments,
|
|
549
|
+
reactions,
|
|
550
|
+
commentCount = comments.length,
|
|
551
|
+
commentsTruncated = false,
|
|
552
|
+
reactionCount = reactions.length,
|
|
553
|
+
reactionsTruncated = false,
|
|
528
554
|
ctas,
|
|
529
555
|
browserDiagnostics,
|
|
530
556
|
bugReport,
|
|
@@ -534,6 +560,12 @@ export function buildPublicAgentContext({
|
|
|
534
560
|
transcript: PublicAgentTranscript;
|
|
535
561
|
agentSegments: ReturnType<typeof toAgentTranscriptSegments>;
|
|
536
562
|
chapters: ReturnType<typeof parseAgentChapters>;
|
|
563
|
+
comments: PublicAgentComment[];
|
|
564
|
+
reactions: PublicAgentReaction[];
|
|
565
|
+
commentCount?: number;
|
|
566
|
+
commentsTruncated?: boolean;
|
|
567
|
+
reactionCount?: number;
|
|
568
|
+
reactionsTruncated?: boolean;
|
|
537
569
|
ctas: Awaited<ReturnType<typeof loadAgentCtas>>;
|
|
538
570
|
browserDiagnostics?: BrowserDiagnosticsData | null;
|
|
539
571
|
bugReport?: PublicAgentBugReport;
|
|
@@ -639,6 +671,29 @@ export function buildPublicAgentContext({
|
|
|
639
671
|
segments: agentSegments,
|
|
640
672
|
segmentCount: agentSegments.length,
|
|
641
673
|
},
|
|
674
|
+
comments: comments.map((comment) => ({
|
|
675
|
+
id: comment.id,
|
|
676
|
+
recordingId: comment.recordingId,
|
|
677
|
+
threadId: comment.threadId,
|
|
678
|
+
parentId: comment.parentId,
|
|
679
|
+
authorName: comment.authorName,
|
|
680
|
+
content: comment.content,
|
|
681
|
+
videoTimestampMs: comment.videoTimestampMs,
|
|
682
|
+
resolved: comment.resolved,
|
|
683
|
+
createdAt: comment.createdAt,
|
|
684
|
+
updatedAt: comment.updatedAt,
|
|
685
|
+
})),
|
|
686
|
+
commentCount,
|
|
687
|
+
commentsTruncated,
|
|
688
|
+
reactions: reactions.map((reaction) => ({
|
|
689
|
+
id: reaction.id,
|
|
690
|
+
emoji: reaction.emoji,
|
|
691
|
+
videoTimestampMs: reaction.videoTimestampMs,
|
|
692
|
+
viewerName: reaction.viewerName,
|
|
693
|
+
createdAt: reaction.createdAt,
|
|
694
|
+
})),
|
|
695
|
+
reactionCount,
|
|
696
|
+
reactionsTruncated,
|
|
642
697
|
chapters,
|
|
643
698
|
recommendedFrames: suggestedFrames,
|
|
644
699
|
bugReport: compactBugReport(bugReport ?? null),
|
|
@@ -112,8 +112,9 @@ export async function getOrganizationDefaultVisibility(
|
|
|
112
112
|
*/
|
|
113
113
|
export async function getDefaultRecordingVisibility(
|
|
114
114
|
organizationId: string | null | undefined,
|
|
115
|
+
userEmail: string | null | undefined = getRequestUserEmail(),
|
|
115
116
|
): Promise<RecordingVisibility> {
|
|
116
|
-
const email =
|
|
117
|
+
const email = userEmail;
|
|
117
118
|
if (email) {
|
|
118
119
|
const prefs = (await getUserSetting(
|
|
119
120
|
normalizeOwnerEmail(email),
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { FileUploadProvider } from "@agent-native/core/file-upload";
|
|
2
|
+
|
|
3
|
+
import type { StoredResumableSession } from "./resumable-session.js";
|
|
4
|
+
import { resolveResumableUploadProvider } from "./resumable-upload-provider.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Best-effort provider cleanup for a resumable session.
|
|
8
|
+
*
|
|
9
|
+
* Callers decide when it is safe to delete the local session handle. Keeping
|
|
10
|
+
* that decision outside this helper matters for ambiguous completion errors:
|
|
11
|
+
* the provider may already have materialized the object while the response
|
|
12
|
+
* was in flight, so a later retry may still need the handle to reconcile it.
|
|
13
|
+
*/
|
|
14
|
+
export async function abortResumableUploadSession(
|
|
15
|
+
session: StoredResumableSession,
|
|
16
|
+
options: {
|
|
17
|
+
provider?: FileUploadProvider | null;
|
|
18
|
+
label?: string;
|
|
19
|
+
} = {},
|
|
20
|
+
): Promise<boolean> {
|
|
21
|
+
const label = options.label ?? "resumable upload";
|
|
22
|
+
let provider = options.provider;
|
|
23
|
+
if (provider === undefined) {
|
|
24
|
+
try {
|
|
25
|
+
provider = await resolveResumableUploadProvider(session.providerId);
|
|
26
|
+
} catch (error) {
|
|
27
|
+
console.warn(`[${label}] provider lookup failed:`, error);
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!provider?.resumable?.abortSession) {
|
|
33
|
+
console.warn(
|
|
34
|
+
`[${label}] provider ${session.providerId} cannot abort the resumable session`,
|
|
35
|
+
);
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
await provider.resumable.abortSession({
|
|
41
|
+
sessionId: session.sessionId,
|
|
42
|
+
meta: session.meta,
|
|
43
|
+
});
|
|
44
|
+
return true;
|
|
45
|
+
} catch (error) {
|
|
46
|
+
console.warn(`[${label}] provider session cleanup failed:`, error);
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|