@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
|
@@ -147,6 +147,33 @@ impl NativeUploadMode {
|
|
|
147
147
|
Self::Streaming => "streaming",
|
|
148
148
|
}
|
|
149
149
|
}
|
|
150
|
+
|
|
151
|
+
fn from_reset_response(body: &str) -> Self {
|
|
152
|
+
let value = serde_json::from_str::<serde_json::Value>(body).ok();
|
|
153
|
+
let upload_mode = value
|
|
154
|
+
.as_ref()
|
|
155
|
+
.and_then(|value| value.get("uploadMode"))
|
|
156
|
+
.and_then(serde_json::Value::as_str)
|
|
157
|
+
.map(str::to_string);
|
|
158
|
+
Self::from_option(upload_mode)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
#[cfg(test)]
|
|
163
|
+
mod native_upload_mode_tests {
|
|
164
|
+
use super::NativeUploadMode;
|
|
165
|
+
|
|
166
|
+
#[test]
|
|
167
|
+
fn uses_streaming_mode_when_retry_session_was_recreated() {
|
|
168
|
+
assert_eq!(
|
|
169
|
+
NativeUploadMode::from_reset_response(r#"{"uploadMode":"streaming"}"#),
|
|
170
|
+
NativeUploadMode::Streaming,
|
|
171
|
+
);
|
|
172
|
+
assert_eq!(
|
|
173
|
+
NativeUploadMode::from_reset_response(r#"{"ok":true}"#),
|
|
174
|
+
NativeUploadMode::Buffered,
|
|
175
|
+
);
|
|
176
|
+
}
|
|
150
177
|
}
|
|
151
178
|
|
|
152
179
|
#[derive(Default)]
|
|
@@ -2061,25 +2088,45 @@ pub async fn native_fullscreen_recording_retry_upload(
|
|
|
2061
2088
|
saved.last_error = None;
|
|
2062
2089
|
write_saved_recording_metadata(&app, &saved)?;
|
|
2063
2090
|
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2091
|
+
// Preparation can normalize or transcode the saved recording. The
|
|
2092
|
+
// resumable session must be created with the MIME type we will actually
|
|
2093
|
+
// upload, not the source file's potentially stale MIME type.
|
|
2094
|
+
let result = async {
|
|
2095
|
+
let (prepared, retry_combined_path) = prepare_saved_recording_file(&app, &saved)?;
|
|
2096
|
+
let upload_mode = match reset_upload_chunks(
|
|
2097
|
+
&saved.server_url,
|
|
2098
|
+
&saved.recording_id,
|
|
2099
|
+
&prepared.mime_type,
|
|
2100
|
+
auth_token.as_deref().unwrap_or(""),
|
|
2101
|
+
cookie.as_deref().unwrap_or(""),
|
|
2102
|
+
)
|
|
2103
|
+
.await
|
|
2104
|
+
{
|
|
2105
|
+
Ok(upload_mode) => upload_mode,
|
|
2106
|
+
Err(err) => {
|
|
2107
|
+
cleanup_prepared_saved_recording_files(&prepared, retry_combined_path);
|
|
2108
|
+
return Err(err);
|
|
2109
|
+
}
|
|
2110
|
+
};
|
|
2075
2111
|
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2112
|
+
let upload_result = upload_prepared_recording_file(
|
|
2113
|
+
&app,
|
|
2114
|
+
&prepared,
|
|
2115
|
+
saved.server_url.clone(),
|
|
2116
|
+
saved.recording_id.clone(),
|
|
2117
|
+
auth_token.unwrap_or_default(),
|
|
2118
|
+
cookie.unwrap_or_default(),
|
|
2119
|
+
upload_mode,
|
|
2120
|
+
saved.duration_ms,
|
|
2121
|
+
saved.width,
|
|
2122
|
+
saved.height,
|
|
2123
|
+
saved.has_audio,
|
|
2124
|
+
saved.has_camera,
|
|
2125
|
+
)
|
|
2126
|
+
.await;
|
|
2127
|
+
cleanup_prepared_saved_recording_files(&prepared, retry_combined_path);
|
|
2128
|
+
upload_result
|
|
2129
|
+
}
|
|
2083
2130
|
.await;
|
|
2084
2131
|
|
|
2085
2132
|
match result {
|
|
@@ -3236,29 +3283,10 @@ async fn upload_recording_file(
|
|
|
3236
3283
|
upload_result
|
|
3237
3284
|
}
|
|
3238
3285
|
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
auth_token: String,
|
|
3244
|
-
cookie: String,
|
|
3245
|
-
) -> Result<NativeFullscreenUploadResult, String> {
|
|
3246
|
-
let (prepared, retry_combined_path) = prepare_saved_recording_file(app, saved)?;
|
|
3247
|
-
let upload_result = upload_prepared_recording_file(
|
|
3248
|
-
app,
|
|
3249
|
-
&prepared,
|
|
3250
|
-
server_url,
|
|
3251
|
-
saved.recording_id.clone(),
|
|
3252
|
-
auth_token,
|
|
3253
|
-
cookie,
|
|
3254
|
-
NativeUploadMode::Buffered,
|
|
3255
|
-
saved.duration_ms,
|
|
3256
|
-
saved.width,
|
|
3257
|
-
saved.height,
|
|
3258
|
-
saved.has_audio,
|
|
3259
|
-
saved.has_camera,
|
|
3260
|
-
)
|
|
3261
|
-
.await;
|
|
3286
|
+
fn cleanup_prepared_saved_recording_files(
|
|
3287
|
+
prepared: &PreparedRecordingFile,
|
|
3288
|
+
retry_combined_path: Option<PathBuf>,
|
|
3289
|
+
) {
|
|
3262
3290
|
if prepared.temporary {
|
|
3263
3291
|
let _ = std::fs::remove_file(&prepared.path);
|
|
3264
3292
|
}
|
|
@@ -3267,7 +3295,6 @@ async fn upload_saved_recording_file(
|
|
|
3267
3295
|
let _ = std::fs::remove_file(path);
|
|
3268
3296
|
}
|
|
3269
3297
|
}
|
|
3270
|
-
upload_result
|
|
3271
3298
|
}
|
|
3272
3299
|
|
|
3273
3300
|
fn prepare_saved_recording_file(
|
|
@@ -3509,9 +3536,10 @@ async fn upload_prepared_recording_file(
|
|
|
3509
3536
|
async fn reset_upload_chunks(
|
|
3510
3537
|
server_url: &str,
|
|
3511
3538
|
recording_id: &str,
|
|
3539
|
+
mime_type: &str,
|
|
3512
3540
|
auth_token: &str,
|
|
3513
3541
|
cookie: &str,
|
|
3514
|
-
) -> Result<
|
|
3542
|
+
) -> Result<NativeUploadMode, String> {
|
|
3515
3543
|
let base = server_url.trim_end_matches('/');
|
|
3516
3544
|
let url = url::Url::parse(&format!("{base}/api/uploads/{recording_id}/reset-chunks"))
|
|
3517
3545
|
.map_err(|e| format!("invalid reset URL: {e}"))?;
|
|
@@ -3523,7 +3551,10 @@ async fn reset_upload_chunks(
|
|
|
3523
3551
|
.post(url)
|
|
3524
3552
|
.header("Content-Type", "application/json")
|
|
3525
3553
|
.header("X-Request-Source", "clips-desktop")
|
|
3526
|
-
.
|
|
3554
|
+
.json(&serde_json::json!({
|
|
3555
|
+
"requestStreaming": true,
|
|
3556
|
+
"mimeType": mime_type,
|
|
3557
|
+
}));
|
|
3527
3558
|
let trimmed_token = auth_token.trim();
|
|
3528
3559
|
if !trimmed_token.is_empty() {
|
|
3529
3560
|
request = request.bearer_auth(trimmed_token);
|
|
@@ -3545,7 +3576,7 @@ async fn reset_upload_chunks(
|
|
|
3545
3576
|
body.chars().take(400).collect::<String>()
|
|
3546
3577
|
));
|
|
3547
3578
|
}
|
|
3548
|
-
Ok(())
|
|
3579
|
+
Ok(NativeUploadMode::from_reset_response(&body))
|
|
3549
3580
|
}
|
|
3550
3581
|
|
|
3551
3582
|
async fn upload_thumbnail_bytes(
|
|
@@ -3912,6 +3943,27 @@ impl AudioSignalProbe {
|
|
|
3912
3943
|
}
|
|
3913
3944
|
}
|
|
3914
3945
|
|
|
3946
|
+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
3947
|
+
enum PreparedAudioSignalDecision {
|
|
3948
|
+
AcceptCandidate,
|
|
3949
|
+
UseOriginal,
|
|
3950
|
+
}
|
|
3951
|
+
|
|
3952
|
+
fn decide_prepared_audio_signal(
|
|
3953
|
+
candidate: AudioSignalProbe,
|
|
3954
|
+
source: Option<AudioSignalProbe>,
|
|
3955
|
+
) -> PreparedAudioSignalDecision {
|
|
3956
|
+
if !candidate.has_audible_signal()
|
|
3957
|
+
&& source
|
|
3958
|
+
.map(AudioSignalProbe::has_audible_signal)
|
|
3959
|
+
.unwrap_or(false)
|
|
3960
|
+
{
|
|
3961
|
+
PreparedAudioSignalDecision::UseOriginal
|
|
3962
|
+
} else {
|
|
3963
|
+
PreparedAudioSignalDecision::AcceptCandidate
|
|
3964
|
+
}
|
|
3965
|
+
}
|
|
3966
|
+
|
|
3915
3967
|
fn parse_ffmpeg_volume_db(stderr: &str, label: &str) -> Option<f64> {
|
|
3916
3968
|
stderr.lines().rev().find_map(|line| {
|
|
3917
3969
|
let (_, value) = line.split_once(label)?;
|
|
@@ -3988,14 +4040,11 @@ fn verify_prepared_audio_signal(
|
|
|
3988
4040
|
}
|
|
3989
4041
|
|
|
3990
4042
|
let source_probe = audio_signal_probe_with_ffmpeg(ffmpeg_path, source_path);
|
|
3991
|
-
let
|
|
3992
|
-
.as_ref()
|
|
3993
|
-
.ok()
|
|
3994
|
-
.copied()
|
|
3995
|
-
.map(AudioSignalProbe::has_audible_signal)
|
|
3996
|
-
.unwrap_or(false);
|
|
4043
|
+
let source_signal = source_probe.as_ref().ok().copied();
|
|
3997
4044
|
let source_summary = describe_audio_signal_probe(source_probe);
|
|
3998
|
-
if
|
|
4045
|
+
if decide_prepared_audio_signal(candidate_probe, source_signal)
|
|
4046
|
+
== PreparedAudioSignalDecision::UseOriginal
|
|
4047
|
+
{
|
|
3999
4048
|
eprintln!(
|
|
4000
4049
|
"[clips-tray] AUDIO QUIET AFTER PREPARE: {label} is effectively silent ({}) but original has signal ({source_summary}) — uploading original instead",
|
|
4001
4050
|
candidate_probe.summary()
|
|
@@ -4004,13 +4053,14 @@ fn verify_prepared_audio_signal(
|
|
|
4004
4053
|
}
|
|
4005
4054
|
|
|
4006
4055
|
eprintln!(
|
|
4007
|
-
"[clips-tray] AUDIO CAPTURE
|
|
4056
|
+
"[clips-tray] AUDIO CAPTURE QUIET: prepared {label} has an audio track but no usable signal ({}) and original was not usable either ({source_summary}) — publishing the playable candidate",
|
|
4008
4057
|
candidate_probe.summary()
|
|
4009
4058
|
);
|
|
4010
|
-
|
|
4011
|
-
|
|
4012
|
-
|
|
4013
|
-
|
|
4059
|
+
// Silence in both files is a capture outcome, not a processing regression.
|
|
4060
|
+
// Rejecting it strands the recording in `uploading`, and every retry fails
|
|
4061
|
+
// deterministically after re-running the same probes. Only fall back when
|
|
4062
|
+
// preparation removed signal that was present in the source.
|
|
4063
|
+
Ok(None)
|
|
4014
4064
|
}
|
|
4015
4065
|
|
|
4016
4066
|
fn prepare_recording_file(
|
|
@@ -4069,13 +4119,6 @@ fn prepare_recording_file(
|
|
|
4069
4119
|
|
|
4070
4120
|
let mut smallest_attempt_bytes: Option<u64> = None;
|
|
4071
4121
|
let ffmpeg_path = resolve_ffmpeg_path();
|
|
4072
|
-
if has_audio {
|
|
4073
|
-
if let Some(ffmpeg_path) = ffmpeg_path.as_deref() {
|
|
4074
|
-
let raw_summary =
|
|
4075
|
-
describe_audio_signal_probe(audio_signal_probe_with_ffmpeg(ffmpeg_path, path));
|
|
4076
|
-
eprintln!("[clips-tray] raw recording audio signal before prepare: {raw_summary}");
|
|
4077
|
-
}
|
|
4078
|
-
}
|
|
4079
4122
|
|
|
4080
4123
|
if !COMPRESSION_ENABLED || source_bytes < TRANSCODE_THRESHOLD_BYTES {
|
|
4081
4124
|
if has_audio {
|
|
@@ -5125,7 +5168,8 @@ fn concat_mp4_segments(segments: &[PathBuf], output: &Path) -> Result<(), String
|
|
|
5125
5168
|
#[cfg(test)]
|
|
5126
5169
|
mod audio_track_probe_tests {
|
|
5127
5170
|
use super::{
|
|
5128
|
-
audio_filter_chain,
|
|
5171
|
+
audio_filter_chain, decide_prepared_audio_signal, mp4_has_audio_track,
|
|
5172
|
+
parse_ffmpeg_volume_db, AudioSignalProbe, PreparedAudioSignalDecision,
|
|
5129
5173
|
AUDIO_DENOISE_FILTER, AUDIO_DOWNMIX_FILTER, AUDIO_DOWNMIX_MAKEUP_FILTER,
|
|
5130
5174
|
AUDIO_LOUDNESS_FILTER, AUDIO_MIC_PREGAIN_FILTER,
|
|
5131
5175
|
};
|
|
@@ -5301,6 +5345,27 @@ mod audio_track_probe_tests {
|
|
|
5301
5345
|
assert!(!quiet_noise_peak.has_audible_signal());
|
|
5302
5346
|
assert!(audible.has_audible_signal());
|
|
5303
5347
|
}
|
|
5348
|
+
|
|
5349
|
+
#[test]
|
|
5350
|
+
fn publishes_quiet_recordings_but_preserves_audible_source_audio() {
|
|
5351
|
+
let silent = AudioSignalProbe {
|
|
5352
|
+
mean_volume_db: Some(f64::NEG_INFINITY),
|
|
5353
|
+
max_volume_db: Some(f64::NEG_INFINITY),
|
|
5354
|
+
};
|
|
5355
|
+
let audible = AudioSignalProbe {
|
|
5356
|
+
mean_volume_db: Some(-21.5),
|
|
5357
|
+
max_volume_db: Some(-1.4),
|
|
5358
|
+
};
|
|
5359
|
+
|
|
5360
|
+
assert_eq!(
|
|
5361
|
+
decide_prepared_audio_signal(silent, Some(silent)),
|
|
5362
|
+
PreparedAudioSignalDecision::AcceptCandidate,
|
|
5363
|
+
);
|
|
5364
|
+
assert_eq!(
|
|
5365
|
+
decide_prepared_audio_signal(silent, Some(audible)),
|
|
5366
|
+
PreparedAudioSignalDecision::UseOriginal,
|
|
5367
|
+
);
|
|
5368
|
+
}
|
|
5304
5369
|
}
|
|
5305
5370
|
|
|
5306
5371
|
#[cfg(test)]
|
|
@@ -64,7 +64,8 @@ pub fn show_meeting_notification_window(app: &AppHandle) -> Result<(), String> {
|
|
|
64
64
|
return Ok(());
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
|
|
67
|
+
#[allow(unused_mut)]
|
|
68
|
+
let mut builder = WebviewWindowBuilder::new(
|
|
68
69
|
app,
|
|
69
70
|
MEETING_NOTIFICATION_LABEL,
|
|
70
71
|
build_overlay_url("meeting-notif"),
|
|
@@ -77,9 +78,14 @@ pub fn show_meeting_notification_window(app: &AppHandle) -> Result<(), String> {
|
|
|
77
78
|
.resizable(false)
|
|
78
79
|
.shadow(false)
|
|
79
80
|
.visible(false)
|
|
80
|
-
.focused(false)
|
|
81
|
-
|
|
82
|
-
.
|
|
81
|
+
.focused(false);
|
|
82
|
+
// The reminder remains non-activating, but its first click should still
|
|
83
|
+
// reach the webview instead of only activating the window.
|
|
84
|
+
#[cfg(target_os = "macos")]
|
|
85
|
+
{
|
|
86
|
+
builder = builder.accept_first_mouse(true);
|
|
87
|
+
}
|
|
88
|
+
let win = builder.build().map_err(|e| {
|
|
83
89
|
eprintln!("[clips-tray] meeting notification build failed: {e}");
|
|
84
90
|
e.to_string()
|
|
85
91
|
})?;
|
|
@@ -832,6 +832,57 @@ async function handleResumableChunk(
|
|
|
832
832
|
return { ok: true, finalized: true, ...result };
|
|
833
833
|
} catch (err) {
|
|
834
834
|
console.error(`[resumable-chunk-${recordingId}] finalize failed:`, err);
|
|
835
|
+
const db = getDb();
|
|
836
|
+
const [committed] = await db
|
|
837
|
+
.select({
|
|
838
|
+
id: schema.recordings.id,
|
|
839
|
+
status: schema.recordings.status,
|
|
840
|
+
videoUrl: schema.recordings.videoUrl,
|
|
841
|
+
durationMs: schema.recordings.durationMs,
|
|
842
|
+
width: schema.recordings.width,
|
|
843
|
+
height: schema.recordings.height,
|
|
844
|
+
hasAudio: schema.recordings.hasAudio,
|
|
845
|
+
hasCamera: schema.recordings.hasCamera,
|
|
846
|
+
})
|
|
847
|
+
.from(schema.recordings)
|
|
848
|
+
.where(
|
|
849
|
+
and(
|
|
850
|
+
eq(schema.recordings.id, recordingId),
|
|
851
|
+
ownerEmailMatches(schema.recordings.ownerEmail, ownerEmail),
|
|
852
|
+
),
|
|
853
|
+
);
|
|
854
|
+
if (committed?.status === "ready" && committed.videoUrl) {
|
|
855
|
+
console.warn(
|
|
856
|
+
`[resumable-chunk-${recordingId}] finalize reported an error after committing a ready recording; returning committed success.`,
|
|
857
|
+
{ error: err instanceof Error ? err.message : String(err) },
|
|
858
|
+
);
|
|
859
|
+
await writeAppState(`recording-upload-${recordingId}`, {
|
|
860
|
+
recordingId,
|
|
861
|
+
status: "ready",
|
|
862
|
+
progress: 100,
|
|
863
|
+
videoUrl: committed.videoUrl,
|
|
864
|
+
finishedAt: new Date().toISOString(),
|
|
865
|
+
}).catch((stateErr) =>
|
|
866
|
+
console.warn(
|
|
867
|
+
`[resumable-chunk-${recordingId}] committed-ready state repair failed:`,
|
|
868
|
+
stateErr,
|
|
869
|
+
),
|
|
870
|
+
);
|
|
871
|
+
return {
|
|
872
|
+
ok: true,
|
|
873
|
+
finalized: true,
|
|
874
|
+
recoveredAfterFinalizeError: true,
|
|
875
|
+
id: committed.id,
|
|
876
|
+
status: "ready",
|
|
877
|
+
videoUrl: committed.videoUrl,
|
|
878
|
+
durationMs: committed.durationMs,
|
|
879
|
+
width: committed.width,
|
|
880
|
+
height: committed.height,
|
|
881
|
+
hasAudio: committed.hasAudio,
|
|
882
|
+
hasCamera: committed.hasCamera,
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
|
|
835
886
|
trackUploadBlockingFailure(ownerEmail, {
|
|
836
887
|
stage: "finalize_recording",
|
|
837
888
|
failureKind: "finalize_error",
|
|
@@ -839,10 +890,40 @@ async function handleResumableChunk(
|
|
|
839
890
|
uploadMode: "resumable",
|
|
840
891
|
errorMessage: err instanceof Error ? err.message : String(err),
|
|
841
892
|
});
|
|
893
|
+
const failureReason =
|
|
894
|
+
err instanceof Error ? err.message : "Finalize failed";
|
|
895
|
+
const failedAt = new Date().toISOString();
|
|
896
|
+
await db
|
|
897
|
+
.update(schema.recordings)
|
|
898
|
+
.set({
|
|
899
|
+
status: "failed",
|
|
900
|
+
failureReason,
|
|
901
|
+
updatedAt: failedAt,
|
|
902
|
+
})
|
|
903
|
+
.where(
|
|
904
|
+
and(
|
|
905
|
+
eq(schema.recordings.id, recordingId),
|
|
906
|
+
ownerEmailMatches(schema.recordings.ownerEmail, ownerEmail),
|
|
907
|
+
),
|
|
908
|
+
);
|
|
909
|
+
const failedUploadStateRaw = await readAppState(
|
|
910
|
+
`recording-upload-${recordingId}`,
|
|
911
|
+
).catch(() => null);
|
|
912
|
+
const failedUploadState =
|
|
913
|
+
failedUploadStateRaw && typeof failedUploadStateRaw === "object"
|
|
914
|
+
? (failedUploadStateRaw as Record<string, unknown>)
|
|
915
|
+
: {};
|
|
916
|
+
await writeAppState(`recording-upload-${recordingId}`, {
|
|
917
|
+
...failedUploadState,
|
|
918
|
+
recordingId,
|
|
919
|
+
status: "failed",
|
|
920
|
+
failureReason,
|
|
921
|
+
updatedAt: failedAt,
|
|
922
|
+
});
|
|
842
923
|
setResponseStatus(event, 500);
|
|
843
924
|
return {
|
|
844
925
|
ok: false,
|
|
845
|
-
error:
|
|
926
|
+
error: failureReason,
|
|
846
927
|
};
|
|
847
928
|
}
|
|
848
929
|
}
|
|
@@ -28,7 +28,9 @@ import {
|
|
|
28
28
|
writeAppState,
|
|
29
29
|
deleteAppStateByPrefix,
|
|
30
30
|
} from "@agent-native/core/application-state";
|
|
31
|
+
import { getActiveFileUploadProviderForRequest } from "@agent-native/core/file-upload";
|
|
31
32
|
import { runWithRequestContext } from "@agent-native/core/server";
|
|
33
|
+
import type { UploadMode } from "@shared/recording-core.js";
|
|
32
34
|
import { MAX_UPLOAD_BYTES as MAX_RECORDING_UPLOAD_BYTES } from "@shared/upload-limits.js";
|
|
33
35
|
import { and, eq } from "drizzle-orm";
|
|
34
36
|
import {
|
|
@@ -44,7 +46,12 @@ import {
|
|
|
44
46
|
getEventOwnerContext,
|
|
45
47
|
ownerEmailMatches,
|
|
46
48
|
} from "../../../../lib/recordings.js";
|
|
47
|
-
import {
|
|
49
|
+
import {
|
|
50
|
+
deleteResumableSession,
|
|
51
|
+
setResumableSession,
|
|
52
|
+
} from "../../../../lib/resumable-session.js";
|
|
53
|
+
import { shouldEnableStreamingUpload } from "../../../../lib/streaming-upload-mode.js";
|
|
54
|
+
import { allowsSqlRecordingChunkScratch } from "../../../../lib/video-storage.js";
|
|
48
55
|
|
|
49
56
|
interface CompressionMeta {
|
|
50
57
|
originalBytes?: number;
|
|
@@ -54,6 +61,16 @@ interface CompressionMeta {
|
|
|
54
61
|
outputMimeType?: string;
|
|
55
62
|
}
|
|
56
63
|
|
|
64
|
+
function normalizeVideoMimeType(value: unknown): string | null {
|
|
65
|
+
if (typeof value !== "string") return null;
|
|
66
|
+
const mimeType = value.split(";")[0]?.trim().toLowerCase();
|
|
67
|
+
return mimeType === "video/mp4" ||
|
|
68
|
+
mimeType === "video/quicktime" ||
|
|
69
|
+
mimeType === "video/webm"
|
|
70
|
+
? mimeType
|
|
71
|
+
: null;
|
|
72
|
+
}
|
|
73
|
+
|
|
57
74
|
function pickNumber(value: unknown): number | undefined {
|
|
58
75
|
if (typeof value !== "number") return undefined;
|
|
59
76
|
if (!Number.isFinite(value) || value < 0) return undefined;
|
|
@@ -77,6 +94,8 @@ export default defineEventHandler(async (event: H3Event) => {
|
|
|
77
94
|
const { userEmail: ownerEmail, orgId } = await getEventOwnerContext(event);
|
|
78
95
|
const body = (await readBody(event).catch(() => null)) as {
|
|
79
96
|
compression?: CompressionMeta | null;
|
|
97
|
+
requestStreaming?: boolean;
|
|
98
|
+
mimeType?: string;
|
|
80
99
|
} | null;
|
|
81
100
|
|
|
82
101
|
// Sanitize compression metadata. The recorder is the only client we trust
|
|
@@ -117,6 +136,67 @@ export default defineEventHandler(async (event: H3Event) => {
|
|
|
117
136
|
// accidentally route through handleResumableChunk with stale offsets.
|
|
118
137
|
await deleteResumableSession(recordingId).catch(() => {});
|
|
119
138
|
|
|
139
|
+
let uploadMode: UploadMode = "buffered";
|
|
140
|
+
if (body?.requestStreaming === true) {
|
|
141
|
+
const mimeType = normalizeVideoMimeType(body.mimeType);
|
|
142
|
+
if (!mimeType) {
|
|
143
|
+
setResponseStatus(event, 400);
|
|
144
|
+
return { error: "A supported video mimeType is required for retry" };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const bufferedFallbackAvailable = allowsSqlRecordingChunkScratch();
|
|
148
|
+
const uploadProvider = await getActiveFileUploadProviderForRequest();
|
|
149
|
+
if (
|
|
150
|
+
shouldEnableStreamingUpload({
|
|
151
|
+
client: "desktop",
|
|
152
|
+
mimeType,
|
|
153
|
+
bufferedFallbackAvailable,
|
|
154
|
+
}) &&
|
|
155
|
+
uploadProvider?.resumable
|
|
156
|
+
) {
|
|
157
|
+
try {
|
|
158
|
+
const extension = /mp4|quicktime/.test(mimeType) ? "mp4" : "webm";
|
|
159
|
+
const filename = `${recordingId}.${extension}`;
|
|
160
|
+
const session = await uploadProvider.resumable.startSession(
|
|
161
|
+
filename,
|
|
162
|
+
mimeType,
|
|
163
|
+
MAX_RECORDING_UPLOAD_BYTES,
|
|
164
|
+
);
|
|
165
|
+
await setResumableSession(recordingId, {
|
|
166
|
+
providerId: uploadProvider.id,
|
|
167
|
+
sessionId: session.sessionId,
|
|
168
|
+
meta: {
|
|
169
|
+
...session.meta,
|
|
170
|
+
stableUrl: true,
|
|
171
|
+
recordAsset: false,
|
|
172
|
+
},
|
|
173
|
+
bytesUploaded: 0,
|
|
174
|
+
lastCommittedIndex: -1,
|
|
175
|
+
});
|
|
176
|
+
uploadMode = "streaming";
|
|
177
|
+
} catch (err) {
|
|
178
|
+
if (!bufferedFallbackAvailable) {
|
|
179
|
+
setResponseStatus(event, 502);
|
|
180
|
+
return {
|
|
181
|
+
error: `Could not restart recording upload: ${
|
|
182
|
+
err instanceof Error ? err.message : String(err)
|
|
183
|
+
}`,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
console.warn(
|
|
187
|
+
`[reset-chunks-${recordingId}] resumable restart failed; using buffered retry:`,
|
|
188
|
+
err,
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
} else if (!bufferedFallbackAvailable) {
|
|
192
|
+
setResponseStatus(event, 409);
|
|
193
|
+
return {
|
|
194
|
+
error:
|
|
195
|
+
"Recording upload storage could not start a resumable retry session.",
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
120
200
|
// Reset the per-recording upload progress so the UI poller sees the
|
|
121
201
|
// re-upload restart from 0 and doesn't briefly show "100% then
|
|
122
202
|
// re-running" on the post-compression chunked upload pass.
|
|
@@ -158,6 +238,7 @@ export default defineEventHandler(async (event: H3Event) => {
|
|
|
158
238
|
recordingId,
|
|
159
239
|
chunksCleared: cleared,
|
|
160
240
|
compressionRecorded: !!compression,
|
|
241
|
+
uploadMode,
|
|
161
242
|
};
|
|
162
243
|
});
|
|
163
244
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"artifact-response.d.ts","sourceRoot":"","sources":["../../src/a2a/artifact-response.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,0BAA0B;IACzC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,0BAA0B,CAAC,EAAE,OAAO,CAAC;CACtC;
|
|
1
|
+
{"version":3,"file":"artifact-response.d.ts","sourceRoot":"","sources":["../../src/a2a/artifact-response.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,0BAA0B;IACzC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,0BAA0B,CAAC,EAAE,OAAO,CAAC;CACtC;AA6lCD,wBAAgB,sBAAsB,CACpC,YAAY,EAAE,MAAM,EACpB,WAAW,EAAE,oBAAoB,EAAE,EACnC,OAAO,GAAE,0BAA+B,GACvC,MAAM,CAsIR;AAED,wBAAgB,kCAAkC,CAChD,WAAW,EAAE,oBAAoB,EAAE,EACnC,OAAO,GAAE,0BAA+B,GACvC,MAAM,GAAG,IAAI,CA8Bf"}
|
|
@@ -123,6 +123,110 @@ function contentDatabaseSubmissionArtifact(parsed) {
|
|
|
123
123
|
url,
|
|
124
124
|
};
|
|
125
125
|
}
|
|
126
|
+
function documentUrlForId(parsed, id, additionalCandidates = [], options = {}) {
|
|
127
|
+
const candidates = [
|
|
128
|
+
stringValue(parsed.url),
|
|
129
|
+
stringValue(parsed.urlPath),
|
|
130
|
+
stringValue(parsed.deepLink),
|
|
131
|
+
stringValue(parsed.pageUrl),
|
|
132
|
+
stringValue(parsed.documentUrl),
|
|
133
|
+
...additionalCandidates,
|
|
134
|
+
].filter((value) => !!value);
|
|
135
|
+
return candidates.find((candidate) => {
|
|
136
|
+
if (!artifactUrlReferencesId(candidate, "document", id))
|
|
137
|
+
return false;
|
|
138
|
+
return !options.requireContentOrigin || isContentDocumentUrl(candidate);
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
function isContentDocumentUrl(rawUrl) {
|
|
142
|
+
try {
|
|
143
|
+
return new URL(rawUrl).origin === "https://content.agent-native.com";
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
function addDocumentReadArtifact(documents, parsed, options) {
|
|
150
|
+
const id = stringValue(parsed.documentId) ?? stringValue(parsed.id);
|
|
151
|
+
if (!id)
|
|
152
|
+
return;
|
|
153
|
+
const url = documentUrlForId(parsed, id, options.additionalUrlCandidates, {
|
|
154
|
+
requireContentOrigin: options.requireContentOrigin,
|
|
155
|
+
});
|
|
156
|
+
if (!url && !options.allowWithoutUrl)
|
|
157
|
+
return;
|
|
158
|
+
documents.set(id, {
|
|
159
|
+
id,
|
|
160
|
+
title: stringValue(parsed.title) ?? stringValue(parsed.name),
|
|
161
|
+
url,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
function addContentDatabaseReadArtifacts(documents, parsed) {
|
|
165
|
+
const resultUrls = [
|
|
166
|
+
stringValue(parsed.url),
|
|
167
|
+
stringValue(parsed.urlPath),
|
|
168
|
+
stringValue(parsed.deepLink),
|
|
169
|
+
];
|
|
170
|
+
const database = asRecord(parsed.database);
|
|
171
|
+
if (database) {
|
|
172
|
+
addDocumentReadArtifact(documents, database, {
|
|
173
|
+
allowWithoutUrl: true,
|
|
174
|
+
requireContentOrigin: true,
|
|
175
|
+
additionalUrlCandidates: resultUrls,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
// Unavailable database reads still return a documentId, but they do not
|
|
180
|
+
// prove that the page exists and must not authorize an artifact URL.
|
|
181
|
+
if (parsed.available !== false) {
|
|
182
|
+
addDocumentReadArtifact(documents, parsed, {
|
|
183
|
+
allowWithoutUrl: true,
|
|
184
|
+
requireContentOrigin: true,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (!Array.isArray(parsed.items))
|
|
189
|
+
return;
|
|
190
|
+
for (const item of parsed.items) {
|
|
191
|
+
const itemRecord = asRecord(item);
|
|
192
|
+
const document = asRecord(itemRecord?.document);
|
|
193
|
+
if (!document)
|
|
194
|
+
continue;
|
|
195
|
+
addDocumentReadArtifact(documents, document, {
|
|
196
|
+
allowWithoutUrl: true,
|
|
197
|
+
requireContentOrigin: true,
|
|
198
|
+
additionalUrlCandidates: [
|
|
199
|
+
stringValue(itemRecord?.url),
|
|
200
|
+
stringValue(itemRecord?.urlPath),
|
|
201
|
+
stringValue(itemRecord?.deepLink),
|
|
202
|
+
],
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
function isGenericReadTool(tool) {
|
|
207
|
+
return /^(?:find|get|list|query|read|search)-/i.test(tool);
|
|
208
|
+
}
|
|
209
|
+
function addGenericDocumentReadArtifact(documents, parsed) {
|
|
210
|
+
// Unknown read actions are accepted only when their result pairs a document
|
|
211
|
+
// ID with a canonical page URL containing that exact ID. An ID by itself is
|
|
212
|
+
// insufficient, preserving the fabrication guard for unrelated actions.
|
|
213
|
+
addDocumentReadArtifact(documents, parsed, {
|
|
214
|
+
allowWithoutUrl: false,
|
|
215
|
+
requireContentOrigin: true,
|
|
216
|
+
});
|
|
217
|
+
const document = asRecord(parsed.document);
|
|
218
|
+
if (!document)
|
|
219
|
+
return;
|
|
220
|
+
addDocumentReadArtifact(documents, document, {
|
|
221
|
+
allowWithoutUrl: false,
|
|
222
|
+
requireContentOrigin: true,
|
|
223
|
+
additionalUrlCandidates: [
|
|
224
|
+
stringValue(parsed.url),
|
|
225
|
+
stringValue(parsed.urlPath),
|
|
226
|
+
stringValue(parsed.deepLink),
|
|
227
|
+
],
|
|
228
|
+
});
|
|
229
|
+
}
|
|
126
230
|
function addImageArtifact(images, parsed) {
|
|
127
231
|
const id = imageIdValue(parsed);
|
|
128
232
|
if (!id)
|
|
@@ -262,7 +366,6 @@ function collectArtifacts(results) {
|
|
|
262
366
|
continue;
|
|
263
367
|
}
|
|
264
368
|
if (toolResult.tool === "create-document" ||
|
|
265
|
-
toolResult.tool === "get-document" ||
|
|
266
369
|
toolResult.tool === "update-document") {
|
|
267
370
|
const id = stringValue(parsed.id);
|
|
268
371
|
if (id) {
|
|
@@ -274,6 +377,29 @@ function collectArtifacts(results) {
|
|
|
274
377
|
}
|
|
275
378
|
continue;
|
|
276
379
|
}
|
|
380
|
+
if (toolResult.tool === "get-document" ||
|
|
381
|
+
toolResult.tool === "get-content-document") {
|
|
382
|
+
const document = asRecord(parsed.document);
|
|
383
|
+
addDocumentReadArtifact(documents, document ?? parsed, {
|
|
384
|
+
allowWithoutUrl: true,
|
|
385
|
+
requireContentOrigin: true,
|
|
386
|
+
additionalUrlCandidates: document
|
|
387
|
+
? [
|
|
388
|
+
stringValue(parsed.url),
|
|
389
|
+
stringValue(parsed.urlPath),
|
|
390
|
+
stringValue(parsed.deepLink),
|
|
391
|
+
]
|
|
392
|
+
: [],
|
|
393
|
+
});
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
if (toolResult.tool === "get-content-database") {
|
|
397
|
+
addContentDatabaseReadArtifacts(documents, parsed);
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
if (isGenericReadTool(toolResult.tool)) {
|
|
401
|
+
addGenericDocumentReadArtifact(documents, parsed);
|
|
402
|
+
}
|
|
277
403
|
if (toolResult.tool === "create-deck" ||
|
|
278
404
|
toolResult.tool === "duplicate-deck") {
|
|
279
405
|
addDeckArtifact(decks, parsed, { requireReady: true });
|