@agent-native/core 0.114.13 → 0.114.16
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/bin/agent-native.js +35 -14
- package/bin/launcher.js +34 -0
- package/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +21 -0
- package/corpus/core/bin/agent-native.js +35 -14
- package/corpus/core/bin/launcher.js +34 -0
- package/corpus/core/docs/content/deployment.mdx +1 -1
- package/corpus/core/docs/content/frames.mdx +1 -1
- package/corpus/core/docs/content/integrations.mdx +27 -27
- package/corpus/core/docs/content/pr-visual-recap.mdx +17 -13
- package/corpus/core/docs/content/workspace-management.mdx +1 -1
- package/corpus/core/package.json +2 -1
- package/corpus/core/src/agent/engine/builder-engine.ts +4 -0
- package/corpus/core/src/client/AgentPanel.tsx +10 -4
- package/corpus/core/src/client/ConnectBuilderCard.tsx +5 -1
- package/corpus/core/src/client/chat/markdown-renderer.tsx +3 -5
- package/corpus/core/src/client/error-format.ts +2 -1
- package/corpus/core/src/client/index.ts +1 -0
- package/corpus/core/src/client/integrations/IntegrationsPanel.tsx +2 -18
- package/corpus/core/src/client/resources/McpIntegrationDialog.tsx +43 -32
- package/corpus/core/src/client/resources/mcp-integration-catalog.ts +4 -2
- package/corpus/core/src/client/settings/BackgroundAgentSection.tsx +5 -1
- package/corpus/core/src/client/settings/SettingsPanel.tsx +4 -4
- package/corpus/core/src/client/settings/useBuilderStatus.ts +3 -0
- package/corpus/core/src/localization/default-messages.ts +1 -7
- package/corpus/core/src/server/builder-browser.ts +4 -0
- package/corpus/core/src/server/builder-design-systems.ts +6 -1
- package/corpus/core/src/server/fusion-app.ts +5 -1
- package/corpus/core/src/shared/builder-link-tracking.ts +41 -0
- package/corpus/core/src/shared/index.ts +1 -0
- package/corpus/templates/analytics/AGENTS.md +10 -1
- package/corpus/templates/analytics/actions/save-dashboard-report-subscription.ts +10 -2
- package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +5 -4
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +20 -32
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/report-panel-window.ts +62 -0
- package/corpus/templates/analytics/changelog/2026-07-21-chart-series-controls-stay-open-while-moving-from-the-legend.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-21-dashboard-email-captures-complete-large-dashboards-in-chunks.md +6 -0
- package/corpus/templates/analytics/seeds/dashboards/agent-native-templates-first-party.json +4 -4
- package/corpus/templates/analytics/server/jobs/dashboard-report.ts +1 -8
- package/corpus/templates/analytics/server/lib/dashboard-report-subscriptions.ts +39 -7
- package/corpus/templates/analytics/server/lib/dashboard-report.ts +341 -251
- package/corpus/templates/analytics/server/lib/first-party-dashboard-repair.ts +114 -0
- package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +84 -6
- package/corpus/templates/analytics/server/plugins/db.ts +13 -0
- package/corpus/templates/clips/app/components/library/bulk-action-toolbar.tsx +25 -8
- package/corpus/templates/clips/app/components/library/library-grid.tsx +118 -9
- package/corpus/templates/clips/app/components/library/library-layout.tsx +1 -1
- package/corpus/templates/clips/app/components/library/recording-card.tsx +10 -8
- package/corpus/templates/clips/app/components/player/video-player.tsx +10 -4
- package/corpus/templates/clips/app/i18n/en-US.ts +6 -0
- package/corpus/templates/clips/app/routes/_app.trash.tsx +16 -1
- package/corpus/templates/clips/changelog/2026-07-21-choose-which-whisper-model-to-use-in-settings.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-21-fixed-organization-recording-visibility-default.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-21-meeting-notes-stop-when-calls-end.md +6 -0
- package/corpus/templates/clips/desktop/src/app.tsx +90 -78
- package/corpus/templates/clips/desktop/src/hooks/useMeetingTranscription.ts +2 -0
- package/corpus/templates/clips/desktop/src/hooks/useWhisperSettings.ts +172 -0
- package/corpus/templates/clips/desktop/src/lib/meeting-call-app.ts +41 -0
- package/corpus/templates/clips/desktop/src/lib/silence-events.ts +1 -0
- package/corpus/templates/clips/desktop/src/shared/config.ts +1 -0
- package/corpus/templates/clips/desktop/src/styles.css +80 -7
- package/corpus/templates/clips/desktop/src-tauri/src/config.rs +32 -0
- package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +3 -0
- package/corpus/templates/clips/desktop/src-tauri/src/silence_detector.rs +268 -20
- package/corpus/templates/clips/desktop/src-tauri/src/whisper_model.rs +285 -164
- package/corpus/templates/clips/desktop/src-tauri/src/whisper_speech.rs +18 -7
- package/corpus/templates/clips/learnings.defaults.md +1 -1
- package/corpus/templates/clips/server/plugins/db.ts +11 -0
- package/corpus/templates/clips/shared/builder-credits.ts +1 -1
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +20 -38
- package/corpus/templates/content/app/components/editor/previewDocumentSaveController.ts +40 -1
- package/corpus/templates/design/app/components/design/FusionAppBanner.tsx +9 -1
- package/corpus/templates/design/app/components/design/edit-panel/component-section.tsx +5 -1
- package/corpus/templates/design/app/pages/DesignSystemSetup.tsx +9 -1
- package/corpus/templates/slides/app/components/design-system/DesignSystemSetup.tsx +9 -1
- package/dist/agent/engine/builder-engine.d.ts.map +1 -1
- package/dist/agent/engine/builder-engine.js +4 -0
- package/dist/agent/engine/builder-engine.js.map +1 -1
- package/dist/client/AgentPanel.d.ts.map +1 -1
- package/dist/client/AgentPanel.js +10 -3
- package/dist/client/AgentPanel.js.map +1 -1
- package/dist/client/ConnectBuilderCard.d.ts.map +1 -1
- package/dist/client/ConnectBuilderCard.js +5 -1
- package/dist/client/ConnectBuilderCard.js.map +1 -1
- package/dist/client/chat/markdown-renderer.d.ts.map +1 -1
- package/dist/client/chat/markdown-renderer.js +3 -2
- package/dist/client/chat/markdown-renderer.js.map +1 -1
- package/dist/client/error-format.d.ts +1 -1
- package/dist/client/error-format.d.ts.map +1 -1
- package/dist/client/error-format.js +1 -1
- package/dist/client/error-format.js.map +1 -1
- package/dist/client/index.d.ts +1 -0
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +1 -0
- package/dist/client/index.js.map +1 -1
- package/dist/client/integrations/IntegrationsPanel.d.ts.map +1 -1
- package/dist/client/integrations/IntegrationsPanel.js +3 -4
- package/dist/client/integrations/IntegrationsPanel.js.map +1 -1
- package/dist/client/resources/McpIntegrationDialog.d.ts.map +1 -1
- package/dist/client/resources/McpIntegrationDialog.js +21 -7
- package/dist/client/resources/McpIntegrationDialog.js.map +1 -1
- package/dist/client/resources/mcp-integration-catalog.d.ts.map +1 -1
- package/dist/client/resources/mcp-integration-catalog.js +2 -2
- package/dist/client/resources/mcp-integration-catalog.js.map +1 -1
- package/dist/client/settings/BackgroundAgentSection.d.ts.map +1 -1
- package/dist/client/settings/BackgroundAgentSection.js +5 -1
- package/dist/client/settings/BackgroundAgentSection.js.map +1 -1
- package/dist/client/settings/SettingsPanel.js +1 -1
- package/dist/client/settings/SettingsPanel.js.map +1 -1
- package/dist/client/settings/useBuilderStatus.d.ts.map +1 -1
- package/dist/client/settings/useBuilderStatus.js +2 -0
- package/dist/client/settings/useBuilderStatus.js.map +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/localization/default-messages.d.ts +0 -6
- package/dist/localization/default-messages.d.ts.map +1 -1
- package/dist/localization/default-messages.js +1 -7
- package/dist/localization/default-messages.js.map +1 -1
- package/dist/observability/routes.d.ts +5 -5
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/builder-browser.d.ts.map +1 -1
- package/dist/server/builder-browser.js +4 -0
- package/dist/server/builder-browser.js.map +1 -1
- package/dist/server/builder-design-systems.d.ts.map +1 -1
- package/dist/server/builder-design-systems.js +6 -1
- package/dist/server/builder-design-systems.js.map +1 -1
- package/dist/server/fusion-app.d.ts.map +1 -1
- package/dist/server/fusion-app.js +2 -1
- package/dist/server/fusion-app.js.map +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/shared/builder-link-tracking.d.ts +14 -0
- package/dist/shared/builder-link-tracking.d.ts.map +1 -0
- package/dist/shared/builder-link-tracking.js +26 -0
- package/dist/shared/builder-link-tracking.js.map +1 -0
- package/dist/shared/index.d.ts +1 -0
- package/dist/shared/index.d.ts.map +1 -1
- package/dist/shared/index.js +1 -0
- package/dist/shared/index.js.map +1 -1
- package/docs/content/deployment.mdx +1 -1
- package/docs/content/frames.mdx +1 -1
- package/docs/content/integrations.mdx +27 -27
- package/docs/content/pr-visual-recap.mdx +17 -13
- package/docs/content/workspace-management.mdx +1 -1
- package/package.json +3 -2
- package/src/agent/engine/builder-engine.ts +4 -0
- package/src/client/AgentPanel.tsx +10 -4
- package/src/client/ConnectBuilderCard.tsx +5 -1
- package/src/client/chat/markdown-renderer.tsx +3 -5
- package/src/client/error-format.ts +2 -1
- package/src/client/index.ts +1 -0
- package/src/client/integrations/IntegrationsPanel.tsx +2 -18
- package/src/client/resources/McpIntegrationDialog.tsx +43 -32
- package/src/client/resources/mcp-integration-catalog.ts +4 -2
- package/src/client/settings/BackgroundAgentSection.tsx +5 -1
- package/src/client/settings/SettingsPanel.tsx +4 -4
- package/src/client/settings/useBuilderStatus.ts +3 -0
- package/src/localization/default-messages.ts +1 -7
- package/src/server/builder-browser.ts +4 -0
- package/src/server/builder-design-systems.ts +6 -1
- package/src/server/fusion-app.ts +5 -1
- package/src/shared/builder-link-tracking.ts +41 -0
- package/src/shared/index.ts +1 -0
|
@@ -135,6 +135,8 @@ pub struct FeatureConfig {
|
|
|
135
135
|
pub onboarding_complete: bool,
|
|
136
136
|
#[serde(default = "default_whisper_model_enabled")]
|
|
137
137
|
pub whisper_model_enabled: bool,
|
|
138
|
+
#[serde(default = "default_whisper_model_id")]
|
|
139
|
+
pub whisper_model_id: String,
|
|
138
140
|
}
|
|
139
141
|
|
|
140
142
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
@@ -170,6 +172,10 @@ fn default_whisper_model_enabled() -> bool {
|
|
|
170
172
|
true
|
|
171
173
|
}
|
|
172
174
|
|
|
175
|
+
fn default_whisper_model_id() -> String {
|
|
176
|
+
"base".to_string()
|
|
177
|
+
}
|
|
178
|
+
|
|
173
179
|
fn default_screen_memory_retention_hours() -> u32 {
|
|
174
180
|
8
|
|
175
181
|
}
|
|
@@ -227,6 +233,7 @@ impl Default for FeatureConfig {
|
|
|
227
233
|
screen_memory: ScreenMemoryConfig::default(),
|
|
228
234
|
onboarding_complete: false,
|
|
229
235
|
whisper_model_enabled: default_whisper_model_enabled(),
|
|
236
|
+
whisper_model_id: default_whisper_model_id(),
|
|
230
237
|
}
|
|
231
238
|
}
|
|
232
239
|
}
|
|
@@ -333,6 +340,12 @@ pub async fn get_feature_config(app: AppHandle) -> Result<FeatureConfig, String>
|
|
|
333
340
|
/// Save feature config to disk and emit a change event.
|
|
334
341
|
#[tauri::command]
|
|
335
342
|
pub async fn set_feature_config(app: AppHandle, config: FeatureConfig) -> Result<(), String> {
|
|
343
|
+
if !crate::whisper_model::is_supported_model_id(&config.whisper_model_id) {
|
|
344
|
+
return Err(format!(
|
|
345
|
+
"unsupported Whisper model: {}",
|
|
346
|
+
config.whisper_model_id
|
|
347
|
+
));
|
|
348
|
+
}
|
|
336
349
|
let previous = load_config(&app);
|
|
337
350
|
if (crate::rewind_clip::is_active(&app) || crate::util::is_recording_active(&app))
|
|
338
351
|
&& rewind_capture_contract_changed(&previous.screen_memory, &config.screen_memory)
|
|
@@ -366,6 +379,12 @@ pub async fn set_feature_config(app: AppHandle, config: FeatureConfig) -> Result
|
|
|
366
379
|
serde_json::json!({ "enabled": config.whisper_model_enabled }),
|
|
367
380
|
);
|
|
368
381
|
}
|
|
382
|
+
if previous.whisper_model_id != config.whisper_model_id {
|
|
383
|
+
let _ = app.emit(
|
|
384
|
+
"whisper:model-selection-changed",
|
|
385
|
+
serde_json::json!({ "modelId": config.whisper_model_id }),
|
|
386
|
+
);
|
|
387
|
+
}
|
|
369
388
|
crate::screen_memory::sync_from_config(&app, &config);
|
|
370
389
|
let _ = app.emit("app:feature-config-changed", config);
|
|
371
390
|
Ok(())
|
|
@@ -406,6 +425,19 @@ mod tests {
|
|
|
406
425
|
.contains(&"com.1password.1password".to_string()));
|
|
407
426
|
}
|
|
408
427
|
|
|
428
|
+
#[test]
|
|
429
|
+
fn feature_config_defaults_to_base_whisper_model_for_legacy_json() {
|
|
430
|
+
let config: FeatureConfig = serde_json::from_value(serde_json::json!({
|
|
431
|
+
"clipsEnabled": true,
|
|
432
|
+
"meetingsEnabled": true,
|
|
433
|
+
"voiceEnabled": true,
|
|
434
|
+
"onboardingComplete": true
|
|
435
|
+
}))
|
|
436
|
+
.unwrap();
|
|
437
|
+
|
|
438
|
+
assert_eq!(config.whisper_model_id, "base");
|
|
439
|
+
}
|
|
440
|
+
|
|
409
441
|
#[test]
|
|
410
442
|
fn rewind_enums_use_stable_kebab_case_values() {
|
|
411
443
|
assert_eq!(
|
|
@@ -231,8 +231,11 @@ pub fn run() {
|
|
|
231
231
|
shortcuts::set_fn_shortcut_enabled,
|
|
232
232
|
shortcuts::set_dictation_escape_active,
|
|
233
233
|
// whisper model management
|
|
234
|
+
whisper_model::whisper_models,
|
|
234
235
|
whisper_model::whisper_model_status,
|
|
235
236
|
whisper_model::whisper_model_download,
|
|
237
|
+
whisper_model::whisper_downloaded_models,
|
|
238
|
+
whisper_model::whisper_model_delete,
|
|
236
239
|
// permission status (silent checks for all TCC permissions)
|
|
237
240
|
permission_status::check_permission_statuses,
|
|
238
241
|
// persistent log file (production debugging)
|
|
@@ -11,9 +11,11 @@
|
|
|
11
11
|
//!
|
|
12
12
|
//! * **System sleep** — `NSWorkspaceWillSleepNotification` via objc2.
|
|
13
13
|
//! Emits `meetings:sleep-stop`.
|
|
14
|
-
//! * **Call-end heuristic** — best-effort:
|
|
15
|
-
//!
|
|
16
|
-
//!
|
|
14
|
+
//! * **Call-end heuristic** — best-effort: when a known conferencing app
|
|
15
|
+
//! releases its microphone after using it for the active meeting, emit
|
|
16
|
+
//! `meetings:call-ended`. Falling back to a foreground-to-background
|
|
17
|
+
//! transition keeps the detector useful on macOS versions that do not
|
|
18
|
+
//! expose per-process input activity.
|
|
17
19
|
//! * **Calendar end** — when the scheduled meeting end has passed and both
|
|
18
20
|
//! audio sources have been quiet for the call-end window, emit the same
|
|
19
21
|
//! event even if the conferencing app remains frontmost.
|
|
@@ -69,6 +71,11 @@ pub struct SilenceConfig {
|
|
|
69
71
|
/// Whether to enable the call-ended heuristic.
|
|
70
72
|
#[serde(default = "default_true")]
|
|
71
73
|
pub watch_call_ended: bool,
|
|
74
|
+
/// Bundle IDs allowed to corroborate a call ending by releasing their
|
|
75
|
+
/// microphone input. Restricting this to the meeting provider prevents an
|
|
76
|
+
/// unrelated browser tab from affecting a live meeting session.
|
|
77
|
+
#[serde(default)]
|
|
78
|
+
pub call_app_bundle_ids: Option<Vec<String>>,
|
|
72
79
|
/// Unix epoch milliseconds for the calendar event's scheduled end.
|
|
73
80
|
/// Calendar-end stopping still requires quiet audio as confirmation.
|
|
74
81
|
#[serde(default)]
|
|
@@ -117,6 +124,9 @@ struct DetectorInner {
|
|
|
117
124
|
silence_fired: bool,
|
|
118
125
|
/// Calendar event end for the active session, if one is known.
|
|
119
126
|
scheduled_end_ms: Option<u64>,
|
|
127
|
+
/// Apps allowed to corroborate a call ending by releasing their microphone
|
|
128
|
+
/// input. This varies by the calendar join URL for each session.
|
|
129
|
+
call_app_bundle_ids: Vec<String>,
|
|
120
130
|
}
|
|
121
131
|
|
|
122
132
|
pub struct DetectorState {
|
|
@@ -151,6 +161,7 @@ pub fn silence_detector_start(app: AppHandle, config: Option<SilenceConfig>) ->
|
|
|
151
161
|
call_ended_ms: default_call_ended_ms(),
|
|
152
162
|
watch_sleep: true,
|
|
153
163
|
watch_call_ended: true,
|
|
164
|
+
call_app_bundle_ids: None,
|
|
154
165
|
scheduled_end_ms: None,
|
|
155
166
|
});
|
|
156
167
|
|
|
@@ -196,6 +207,13 @@ pub fn silence_detector_start(app: AppHandle, config: Option<SilenceConfig>) ->
|
|
|
196
207
|
g.silence_fired = false;
|
|
197
208
|
g.config = Some(cfg.clone());
|
|
198
209
|
g.scheduled_end_ms = cfg.scheduled_end_ms;
|
|
210
|
+
g.call_app_bundle_ids = cfg
|
|
211
|
+
.call_app_bundle_ids
|
|
212
|
+
.clone()
|
|
213
|
+
.unwrap_or_default()
|
|
214
|
+
.into_iter()
|
|
215
|
+
.map(|bundle_id| bundle_id.to_lowercase())
|
|
216
|
+
.collect();
|
|
199
217
|
// Seed both buckets with `now()` so we don't insta-fire on start
|
|
200
218
|
// before any audio has streamed in yet.
|
|
201
219
|
g.mic = Some(SourceState::fresh());
|
|
@@ -293,6 +311,7 @@ pub fn silence_detector_stop(app: AppHandle) -> Result<(), String> {
|
|
|
293
311
|
g.mic = None;
|
|
294
312
|
g.system = None;
|
|
295
313
|
g.scheduled_end_ms = None;
|
|
314
|
+
g.call_app_bundle_ids.clear();
|
|
296
315
|
Ok(())
|
|
297
316
|
}
|
|
298
317
|
|
|
@@ -334,6 +353,20 @@ fn install_sleep_watcher(_app: &AppHandle) {}
|
|
|
334
353
|
|
|
335
354
|
// --- call-ended heuristic --------------------------------------------------
|
|
336
355
|
|
|
356
|
+
const GENERIC_BROWSER_BUNDLE_IDS: &[&str] = &[
|
|
357
|
+
"com.google.chrome",
|
|
358
|
+
"company.thebrowser.browser",
|
|
359
|
+
"com.apple.safari",
|
|
360
|
+
"org.mozilla.firefox",
|
|
361
|
+
];
|
|
362
|
+
|
|
363
|
+
fn is_configured_generic_browser(bundle_id: &str, call_app_bundle_ids: &[String]) -> bool {
|
|
364
|
+
GENERIC_BROWSER_BUNDLE_IDS.contains(&bundle_id)
|
|
365
|
+
&& call_app_bundle_ids
|
|
366
|
+
.iter()
|
|
367
|
+
.any(|candidate| GENERIC_BROWSER_BUNDLE_IDS.contains(&candidate.as_str()))
|
|
368
|
+
}
|
|
369
|
+
|
|
337
370
|
#[cfg(target_os = "macos")]
|
|
338
371
|
fn install_call_ended_watcher(app: &AppHandle, threshold_ms: u64) {
|
|
339
372
|
static INSTALLED: OnceLock<()> = OnceLock::new();
|
|
@@ -356,39 +389,63 @@ fn install_call_ended_watcher(app: &AppHandle, threshold_ms: u64) {
|
|
|
356
389
|
// (DetectorInner.mic/system), never on the frontmost-app poll
|
|
357
390
|
// alone — matching the granola-ux.md "transcript length +
|
|
358
391
|
// calendar times" model instead of raw frontmost tracking.
|
|
359
|
-
let strong_vc_bundles: &[&str] = &[
|
|
360
|
-
"us.zoom.xos",
|
|
361
|
-
"us.zoom.ZoomClips",
|
|
362
|
-
"com.microsoft.teams2",
|
|
363
|
-
"com.microsoft.teams",
|
|
364
|
-
];
|
|
365
|
-
let generic_browser_bundles: &[&str] = &[
|
|
366
|
-
"com.google.Chrome",
|
|
367
|
-
"company.thebrowser.Browser", // Arc
|
|
368
|
-
];
|
|
369
392
|
let mut ever_seen_front = false;
|
|
370
393
|
let mut last_front_at: Option<Instant> = None;
|
|
371
394
|
let mut fired = false;
|
|
372
395
|
let mut last_front_was_generic_browser = false;
|
|
396
|
+
let mut call_app_used_microphone = false;
|
|
397
|
+
let mut microphone_released_at: Option<Instant> = None;
|
|
398
|
+
let mut generation: Option<u64> = None;
|
|
373
399
|
loop {
|
|
374
400
|
std::thread::sleep(Duration::from_secs(10));
|
|
375
401
|
let state = app.state::<DetectorState>();
|
|
376
|
-
let active = state
|
|
402
|
+
let (active, active_generation, configured_bundle_ids) = state
|
|
403
|
+
.inner
|
|
404
|
+
.lock()
|
|
405
|
+
.map(|g| (g.active, g.generation, g.call_app_bundle_ids.clone()))
|
|
406
|
+
.unwrap_or((false, 0, Vec::new()));
|
|
377
407
|
if !active {
|
|
378
408
|
ever_seen_front = false;
|
|
379
409
|
last_front_at = None;
|
|
380
410
|
fired = false;
|
|
381
411
|
last_front_was_generic_browser = false;
|
|
412
|
+
call_app_used_microphone = false;
|
|
413
|
+
microphone_released_at = None;
|
|
382
414
|
continue;
|
|
383
415
|
}
|
|
416
|
+
if generation != Some(active_generation) {
|
|
417
|
+
ever_seen_front = false;
|
|
418
|
+
last_front_at = None;
|
|
419
|
+
fired = false;
|
|
420
|
+
last_front_was_generic_browser = false;
|
|
421
|
+
call_app_used_microphone = false;
|
|
422
|
+
microphone_released_at = None;
|
|
423
|
+
generation = Some(active_generation);
|
|
424
|
+
}
|
|
425
|
+
let call_app_bundle_ids = if configured_bundle_ids.is_empty() {
|
|
426
|
+
default_call_app_bundle_ids()
|
|
427
|
+
} else {
|
|
428
|
+
configured_bundle_ids
|
|
429
|
+
};
|
|
384
430
|
let front = crate::util::frontmost_bundle_id();
|
|
385
|
-
let
|
|
431
|
+
let is_generic_browser = front
|
|
386
432
|
.as_ref()
|
|
387
|
-
.map(|
|
|
433
|
+
.map(|bundle_id| {
|
|
434
|
+
is_configured_generic_browser(
|
|
435
|
+
&bundle_id.to_lowercase(),
|
|
436
|
+
&call_app_bundle_ids,
|
|
437
|
+
)
|
|
438
|
+
})
|
|
388
439
|
.unwrap_or(false);
|
|
389
|
-
let
|
|
440
|
+
let is_strong_vc = front
|
|
390
441
|
.as_ref()
|
|
391
|
-
.map(|
|
|
442
|
+
.map(|bundle_id| {
|
|
443
|
+
let bundle_id = bundle_id.to_lowercase();
|
|
444
|
+
call_app_bundle_ids
|
|
445
|
+
.iter()
|
|
446
|
+
.any(|candidate| candidate == &bundle_id)
|
|
447
|
+
&& !is_generic_browser
|
|
448
|
+
})
|
|
392
449
|
.unwrap_or(false);
|
|
393
450
|
if is_strong_vc || is_generic_browser {
|
|
394
451
|
ever_seen_front = true;
|
|
@@ -399,6 +456,29 @@ fn install_call_ended_watcher(app: &AppHandle, threshold_ms: u64) {
|
|
|
399
456
|
continue;
|
|
400
457
|
}
|
|
401
458
|
|
|
459
|
+
// A native call application remains frontmost on its post-call
|
|
460
|
+
// screen, so foreground tracking alone cannot tell that the
|
|
461
|
+
// meeting ended. CoreAudio reports whether the provider still
|
|
462
|
+
// has an active microphone stream. Only accept a true -> false
|
|
463
|
+
// transition that stays stable for 30 seconds; this tolerates
|
|
464
|
+
// a device handoff while avoiding a stop before the call has
|
|
465
|
+
// actually acquired its microphone.
|
|
466
|
+
match call_app_uses_microphone(&call_app_bundle_ids) {
|
|
467
|
+
Some(true) => {
|
|
468
|
+
call_app_used_microphone = true;
|
|
469
|
+
microphone_released_at = None;
|
|
470
|
+
}
|
|
471
|
+
Some(false) if call_app_used_microphone => {
|
|
472
|
+
microphone_released_at.get_or_insert_with(Instant::now);
|
|
473
|
+
}
|
|
474
|
+
_ => {}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
let microphone_released = microphone_release_stop_ready(
|
|
478
|
+
call_app_used_microphone,
|
|
479
|
+
microphone_released_at.map(|at| Instant::now().duration_since(at)),
|
|
480
|
+
);
|
|
481
|
+
|
|
402
482
|
let frontmost_call_ended = ever_seen_front
|
|
403
483
|
&& last_front_at
|
|
404
484
|
.map(|t| {
|
|
@@ -412,7 +492,7 @@ fn install_call_ended_watcher(app: &AppHandle, threshold_ms: u64) {
|
|
|
412
492
|
&& (!last_front_was_generic_browser
|
|
413
493
|
|| audio_recently_silent(&state, threshold_ms));
|
|
414
494
|
|
|
415
|
-
if frontmost_call_ended {
|
|
495
|
+
if microphone_released || frontmost_call_ended {
|
|
416
496
|
let _ = app.emit("meetings:call-ended", ());
|
|
417
497
|
fired = true;
|
|
418
498
|
}
|
|
@@ -424,6 +504,136 @@ fn install_call_ended_watcher(app: &AppHandle, threshold_ms: u64) {
|
|
|
424
504
|
#[cfg(not(target_os = "macos"))]
|
|
425
505
|
fn install_call_ended_watcher(_app: &AppHandle, _threshold_ms: u64) {}
|
|
426
506
|
|
|
507
|
+
#[cfg(target_os = "macos")]
|
|
508
|
+
fn default_call_app_bundle_ids() -> Vec<String> {
|
|
509
|
+
[
|
|
510
|
+
"us.zoom.xos",
|
|
511
|
+
"us.zoom.ZoomClips",
|
|
512
|
+
"com.microsoft.teams2",
|
|
513
|
+
"com.microsoft.teams",
|
|
514
|
+
]
|
|
515
|
+
.into_iter()
|
|
516
|
+
.map(|bundle_id| bundle_id.to_lowercase())
|
|
517
|
+
.collect()
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
fn microphone_release_stop_ready(
|
|
521
|
+
app_used_microphone: bool,
|
|
522
|
+
released_for: Option<Duration>,
|
|
523
|
+
) -> bool {
|
|
524
|
+
app_used_microphone
|
|
525
|
+
&& released_for
|
|
526
|
+
.map(|elapsed| elapsed >= Duration::from_secs(30))
|
|
527
|
+
.unwrap_or(false)
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/// Returns whether one of the target conferencing apps currently has a live
|
|
531
|
+
/// CoreAudio input stream. `None` means the OS could not provide a reliable
|
|
532
|
+
/// answer, so callers must keep the existing conservative fallbacks.
|
|
533
|
+
#[cfg(target_os = "macos")]
|
|
534
|
+
fn call_app_uses_microphone(bundle_ids: &[String]) -> Option<bool> {
|
|
535
|
+
use core_foundation::base::TCFType;
|
|
536
|
+
use core_foundation::string::CFString;
|
|
537
|
+
use objc2_core_audio::{
|
|
538
|
+
kAudioHardwareNoError, kAudioHardwarePropertyProcessObjectList,
|
|
539
|
+
kAudioObjectPropertyElementMain, kAudioObjectPropertyScopeGlobal, kAudioObjectSystemObject,
|
|
540
|
+
kAudioProcessPropertyBundleID, kAudioProcessPropertyIsRunningInput,
|
|
541
|
+
AudioObjectGetPropertyData, AudioObjectGetPropertyDataSize, AudioObjectID,
|
|
542
|
+
AudioObjectPropertyAddress,
|
|
543
|
+
};
|
|
544
|
+
use std::ffi::c_void;
|
|
545
|
+
use std::mem::size_of;
|
|
546
|
+
use std::ptr::NonNull;
|
|
547
|
+
|
|
548
|
+
let mut list_address = AudioObjectPropertyAddress {
|
|
549
|
+
mSelector: kAudioHardwarePropertyProcessObjectList,
|
|
550
|
+
mScope: kAudioObjectPropertyScopeGlobal,
|
|
551
|
+
mElement: kAudioObjectPropertyElementMain,
|
|
552
|
+
};
|
|
553
|
+
let mut data_size = 0;
|
|
554
|
+
let list_status = unsafe {
|
|
555
|
+
AudioObjectGetPropertyDataSize(
|
|
556
|
+
kAudioObjectSystemObject as AudioObjectID,
|
|
557
|
+
NonNull::from(&mut list_address),
|
|
558
|
+
0,
|
|
559
|
+
std::ptr::null(),
|
|
560
|
+
NonNull::from(&mut data_size),
|
|
561
|
+
)
|
|
562
|
+
};
|
|
563
|
+
if list_status != kAudioHardwareNoError || data_size == 0 {
|
|
564
|
+
return None;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
let mut processes = vec![0 as AudioObjectID; data_size as usize / size_of::<AudioObjectID>()];
|
|
568
|
+
let list_status = unsafe {
|
|
569
|
+
AudioObjectGetPropertyData(
|
|
570
|
+
kAudioObjectSystemObject as AudioObjectID,
|
|
571
|
+
NonNull::from(&mut list_address),
|
|
572
|
+
0,
|
|
573
|
+
std::ptr::null(),
|
|
574
|
+
NonNull::from(&mut data_size),
|
|
575
|
+
NonNull::new(processes.as_mut_ptr().cast::<c_void>())?,
|
|
576
|
+
)
|
|
577
|
+
};
|
|
578
|
+
if list_status != kAudioHardwareNoError {
|
|
579
|
+
return None;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
for process in processes {
|
|
583
|
+
let mut bundle_address = AudioObjectPropertyAddress {
|
|
584
|
+
mSelector: kAudioProcessPropertyBundleID,
|
|
585
|
+
mScope: kAudioObjectPropertyScopeGlobal,
|
|
586
|
+
mElement: kAudioObjectPropertyElementMain,
|
|
587
|
+
};
|
|
588
|
+
let mut bundle_ref: *const c_void = std::ptr::null();
|
|
589
|
+
let mut bundle_size = size_of::<*const c_void>() as u32;
|
|
590
|
+
let bundle_status = unsafe {
|
|
591
|
+
AudioObjectGetPropertyData(
|
|
592
|
+
process,
|
|
593
|
+
NonNull::from(&mut bundle_address),
|
|
594
|
+
0,
|
|
595
|
+
std::ptr::null(),
|
|
596
|
+
NonNull::from(&mut bundle_size),
|
|
597
|
+
NonNull::new((&mut bundle_ref as *mut *const c_void).cast::<c_void>())?,
|
|
598
|
+
)
|
|
599
|
+
};
|
|
600
|
+
if bundle_status != kAudioHardwareNoError || bundle_ref.is_null() {
|
|
601
|
+
continue;
|
|
602
|
+
}
|
|
603
|
+
let bundle_id = unsafe {
|
|
604
|
+
CFString::wrap_under_get_rule(bundle_ref as core_foundation::string::CFStringRef)
|
|
605
|
+
}
|
|
606
|
+
.to_string()
|
|
607
|
+
.to_lowercase();
|
|
608
|
+
if !bundle_ids.iter().any(|candidate| candidate == &bundle_id) {
|
|
609
|
+
continue;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
let mut input_address = AudioObjectPropertyAddress {
|
|
613
|
+
mSelector: kAudioProcessPropertyIsRunningInput,
|
|
614
|
+
mScope: kAudioObjectPropertyScopeGlobal,
|
|
615
|
+
mElement: kAudioObjectPropertyElementMain,
|
|
616
|
+
};
|
|
617
|
+
let mut input_running: u32 = 0;
|
|
618
|
+
let mut input_size = size_of::<u32>() as u32;
|
|
619
|
+
let input_status = unsafe {
|
|
620
|
+
AudioObjectGetPropertyData(
|
|
621
|
+
process,
|
|
622
|
+
NonNull::from(&mut input_address),
|
|
623
|
+
0,
|
|
624
|
+
std::ptr::null(),
|
|
625
|
+
NonNull::from(&mut input_size),
|
|
626
|
+
NonNull::new((&mut input_running as *mut u32).cast::<c_void>())?,
|
|
627
|
+
)
|
|
628
|
+
};
|
|
629
|
+
if input_status == kAudioHardwareNoError && input_running != 0 {
|
|
630
|
+
return Some(true);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
Some(false)
|
|
635
|
+
}
|
|
636
|
+
|
|
427
637
|
fn scheduled_end_reached(scheduled_end_ms: Option<u64>, now_ms: u64) -> bool {
|
|
428
638
|
scheduled_end_ms
|
|
429
639
|
.map(|end_ms| now_ms >= end_ms)
|
|
@@ -470,7 +680,12 @@ fn audio_recently_silent(state: &tauri::State<'_, DetectorState>, threshold_ms:
|
|
|
470
680
|
|
|
471
681
|
#[cfg(test)]
|
|
472
682
|
mod tests {
|
|
473
|
-
use
|
|
683
|
+
use std::time::Duration;
|
|
684
|
+
|
|
685
|
+
use super::{
|
|
686
|
+
calendar_end_stop_ready, is_configured_generic_browser, microphone_release_stop_ready,
|
|
687
|
+
scheduled_end_reached,
|
|
688
|
+
};
|
|
474
689
|
|
|
475
690
|
#[test]
|
|
476
691
|
fn calendar_end_requires_a_known_end_and_allows_the_exact_boundary() {
|
|
@@ -486,4 +701,37 @@ mod tests {
|
|
|
486
701
|
assert!(calendar_end_stop_ready(Some(9_999), 10_000, true));
|
|
487
702
|
assert!(!calendar_end_stop_ready(Some(10_001), 10_000, true));
|
|
488
703
|
}
|
|
704
|
+
|
|
705
|
+
#[test]
|
|
706
|
+
fn microphone_release_only_stops_after_an_observed_call_input_ends() {
|
|
707
|
+
assert!(!microphone_release_stop_ready(
|
|
708
|
+
false,
|
|
709
|
+
Some(Duration::from_secs(60))
|
|
710
|
+
));
|
|
711
|
+
assert!(!microphone_release_stop_ready(true, None));
|
|
712
|
+
assert!(!microphone_release_stop_ready(
|
|
713
|
+
true,
|
|
714
|
+
Some(Duration::from_secs(29))
|
|
715
|
+
));
|
|
716
|
+
assert!(microphone_release_stop_ready(
|
|
717
|
+
true,
|
|
718
|
+
Some(Duration::from_secs(30))
|
|
719
|
+
));
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
#[test]
|
|
723
|
+
fn browser_calls_require_a_configured_browser_bundle() {
|
|
724
|
+
let browser_call = vec!["com.google.chrome".to_owned()];
|
|
725
|
+
let native_call = vec!["us.zoom.xos".to_owned()];
|
|
726
|
+
|
|
727
|
+
for browser in [
|
|
728
|
+
"com.google.chrome",
|
|
729
|
+
"company.thebrowser.browser",
|
|
730
|
+
"com.apple.safari",
|
|
731
|
+
"org.mozilla.firefox",
|
|
732
|
+
] {
|
|
733
|
+
assert!(is_configured_generic_browser(browser, &browser_call));
|
|
734
|
+
assert!(!is_configured_generic_browser(browser, &native_call));
|
|
735
|
+
}
|
|
736
|
+
}
|
|
489
737
|
}
|