@agent-native/core 0.91.1 → 0.91.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/blueprints/action/crud.md +1 -1
- package/blueprints/channel/discord.md +1 -1
- package/blueprints/provider/stripe.md +1 -1
- package/blueprints/sandbox/docker.md +1 -1
- package/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +14 -0
- package/corpus/core/blueprints/action/crud.md +1 -1
- package/corpus/core/blueprints/channel/discord.md +1 -1
- package/corpus/core/blueprints/provider/stripe.md +1 -1
- package/corpus/core/blueprints/sandbox/docker.md +1 -1
- package/corpus/core/package.json +6 -5
- package/corpus/core/src/agent/run-manager.ts +23 -2
- package/corpus/core/src/agent/run-store.ts +2 -2
- package/corpus/core/src/cli/add.ts +1 -1
- package/corpus/core/src/cli/create.ts +8 -3
- package/corpus/core/src/cli/index.ts +5 -4
- package/corpus/core/src/client/extensions/ExtensionSlot.tsx +7 -0
- package/corpus/core/src/client/session-replay.ts +97 -3
- package/corpus/core/src/jobs/scheduler.ts +30 -1
- package/corpus/core/src/localization/default-messages.ts +21 -0
- package/corpus/core/src/templates/default/app/i18n/ar-SA.ts +21 -0
- package/corpus/core/src/templates/default/app/i18n/de-DE.ts +21 -0
- package/corpus/core/src/templates/default/app/i18n/en-US.ts +21 -0
- package/corpus/core/src/templates/default/app/i18n/es-ES.ts +21 -0
- package/corpus/core/src/templates/default/app/i18n/fr-FR.ts +21 -0
- package/corpus/core/src/templates/default/app/i18n/hi-IN.ts +20 -0
- package/corpus/core/src/templates/default/app/i18n/ja-JP.ts +21 -0
- package/corpus/core/src/templates/default/app/i18n/ko-KR.ts +20 -0
- package/corpus/core/src/templates/default/app/i18n/pt-BR.ts +21 -0
- package/corpus/core/src/templates/default/app/i18n/zh-CN.ts +20 -0
- package/corpus/core/src/templates/default/app/i18n/zh-TW.ts +19 -0
- package/corpus/core/src/templates/default/package.json +2 -2
- package/corpus/core/src/templates/headless/package.json +3 -3
- package/corpus/core/src/templates/workspace-core/package.json +6 -6
- package/corpus/core/src/templates/workspace-root/package.json +2 -2
- package/corpus/core/src/triggers/dispatcher.ts +30 -2
- package/corpus/core/src/vite/client.ts +96 -1
- package/corpus/templates/analytics/.agents/skills/session-replay/SKILL.md +21 -2
- package/corpus/templates/analytics/app/global.css +26 -10
- package/corpus/templates/analytics/app/i18n/zh-TW.ts +2 -0
- package/corpus/templates/analytics/app/i18n-data.ts +25 -0
- package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +548 -237
- package/corpus/templates/analytics/app/pages/sessions/SessionDevToolsPanel.tsx +285 -294
- package/corpus/templates/analytics/changelog/2026-07-08-console-details-expand-inline-under-each-row-and-the-event-t.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-08-session-replay-clamps-ultra-wide-viewports-while-keeping-the.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-08-session-replay-keeps-a-usable-stage-when-dev-tools-is-open-a.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-08-session-replay-resumes-after-scrubbing-and-expands-console-r.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-08-session-replays-wait-for-full-load-keep-a-normal-viewport-an.md +6 -0
- package/corpus/templates/analytics/package.json +2 -2
- package/corpus/templates/assets/package.json +2 -2
- package/corpus/templates/brain/package.json +2 -2
- package/corpus/templates/calendar/AGENTS.md +10 -0
- package/corpus/templates/calendar/actions/create-event.ts +5 -1
- package/corpus/templates/calendar/actions/event-action-helpers.ts +41 -0
- package/corpus/templates/calendar/actions/get-attendee-timezones.ts +28 -0
- package/corpus/templates/calendar/actions/set-attendee-timezone.ts +68 -0
- package/corpus/templates/calendar/app/components/calendar/EventAttendeesSection.tsx +150 -11
- package/corpus/templates/calendar/app/components/calendar/EventDetailPanel.tsx +36 -1
- package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +32 -0
- package/corpus/templates/calendar/app/hooks/use-attendee-timezones.ts +24 -0
- package/corpus/templates/calendar/app/i18n/zh-TW.ts +2 -0
- package/corpus/templates/calendar/app/i18n-data.ts +20 -0
- package/corpus/templates/calendar/app/lib/attendee-local-time.ts +122 -0
- package/corpus/templates/calendar/changelog/2026-07-08-event-details-now-support-extension-widgets-and-you-can-see-.md +6 -0
- package/corpus/templates/calendar/changelog/2026-07-08-when-you-invite-guests-to-a-new-event-you-now-appear-in.md +6 -0
- package/corpus/templates/calendar/package.json +2 -2
- package/corpus/templates/calendar/server/handlers/events.ts +5 -1
- package/corpus/templates/calendar/shared/api.ts +6 -0
- package/corpus/templates/chat/package.json +2 -2
- package/corpus/templates/clips/.agents/skills/meetings/SKILL.md +6 -2
- package/corpus/templates/clips/actions/create-meeting.ts +8 -3
- package/corpus/templates/clips/actions/list-meetings.ts +31 -3
- package/corpus/templates/clips/changelog/2026-07-08-after-finishing-a-desktop-recording-reopening-clips-restor.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-08-clips-now-pops-up-to-take-notes-when-you-join-an-adhoc-zoom-.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-08-desktop-clip-recordings-are-louder-after-capture-mic-only.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-08-meeting-reminders-now-appear-1-minute-before-start-stay-unti.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-08-the-desktop-meetings-panel-has-room-for-its-drop-shadow-and-.md +6 -0
- package/corpus/templates/clips/chrome-extension/package.json +2 -2
- package/corpus/templates/clips/desktop/package.json +2 -2
- package/corpus/templates/clips/desktop/src/app.tsx +91 -34
- package/corpus/templates/clips/desktop/src/hooks/useMeetingTranscription.ts +15 -5
- package/corpus/templates/clips/desktop/src/lib/meeting-notification-timing.ts +83 -0
- package/corpus/templates/clips/desktop/src/lib/native-recording-warm.ts +21 -0
- package/corpus/templates/clips/desktop/src/lib/recorder.ts +98 -20
- package/corpus/templates/clips/desktop/src/overlays/meeting-notification.tsx +174 -50
- package/corpus/templates/clips/desktop/src/styles.css +128 -53
- package/corpus/templates/clips/desktop/src-tauri/src/adhoc_meetings_watcher.rs +364 -0
- package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +1 -1
- package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +5 -0
- package/corpus/templates/clips/desktop/src-tauri/src/meetings_watcher.rs +91 -11
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +69 -19
- package/corpus/templates/clips/desktop/src-tauri/src/native_speech.rs +36 -1
- package/corpus/templates/clips/desktop/src-tauri/src/notifications.rs +62 -10
- package/corpus/templates/clips/desktop/src-tauri/src/silence_detector.rs +1 -24
- package/corpus/templates/clips/desktop/src-tauri/src/util.rs +36 -1
- package/corpus/templates/clips/package.json +2 -2
- package/corpus/templates/content/actions/_content-database-personal-view.ts +80 -0
- package/corpus/templates/content/actions/get-content-database-personal-view.ts +27 -0
- package/corpus/templates/content/actions/update-content-database-personal-view.ts +31 -0
- package/corpus/templates/content/actions/update-content-database-view.ts +3 -0
- package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +1 -1
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +2584 -546
- package/corpus/templates/content/app/components/editor/database/filter-sort.ts +220 -18
- package/corpus/templates/content/app/hooks/use-content-database.ts +32 -0
- package/corpus/templates/content/app/i18n/zh-TW.ts +16 -0
- package/corpus/templates/content/app/i18n-data.ts +154 -0
- package/corpus/templates/content/changelog/2026-07-08-database-filters-now-support-notion-style-reset-save-for-eve.md +6 -0
- package/corpus/templates/content/changelog/2026-07-08-date-filters-can-now-target-a-between-range.md +6 -0
- package/corpus/templates/content/package.json +2 -2
- package/corpus/templates/content/parity/matrix.md +1 -1
- package/corpus/templates/content/parity/matrix.ts +2 -0
- package/corpus/templates/content/shared/api.ts +24 -0
- package/corpus/templates/design/package.json +2 -2
- package/corpus/templates/dispatch/app/i18n/zh-TW.ts +34 -1
- package/corpus/templates/dispatch/app/i18n-data.ts +387 -10
- package/corpus/templates/dispatch/app/routes/automations.tsx +1 -0
- package/corpus/templates/dispatch/changelog/2026-07-08-added-an-automations-page-to-review-pause-and-create.md +6 -0
- package/corpus/templates/dispatch/changelog/2026-07-08-destinations-now-shows-outbound-delivery-queue-health.md +6 -0
- package/corpus/templates/dispatch/package.json +2 -2
- package/corpus/templates/forms/package.json +2 -2
- package/corpus/templates/macros/package.json +2 -2
- package/corpus/templates/mail/app/hooks/use-emails.ts +56 -44
- package/corpus/templates/mail/app/lib/gmail-mutation-queue.ts +325 -0
- package/corpus/templates/mail/changelog/2026-07-08-rapid-archive-and-mark-read-now-batch-gmail-updates-so-the-i.md +6 -0
- package/corpus/templates/mail/package.json +2 -2
- package/corpus/templates/plan/package.json +2 -2
- package/corpus/templates/slides/app/components/editor/ImageDropPromptPopover.tsx +46 -29
- package/corpus/templates/slides/app/context/DeckContext.tsx +45 -18
- package/corpus/templates/slides/app/i18n/ar-SA.ts +4 -0
- package/corpus/templates/slides/app/i18n/de-DE.ts +4 -0
- package/corpus/templates/slides/app/i18n/en-US.ts +4 -0
- package/corpus/templates/slides/app/i18n/es-ES.ts +4 -0
- package/corpus/templates/slides/app/i18n/fr-FR.ts +4 -0
- package/corpus/templates/slides/app/i18n/hi-IN.ts +4 -0
- package/corpus/templates/slides/app/i18n/ja-JP.ts +4 -0
- package/corpus/templates/slides/app/i18n/ko-KR.ts +4 -0
- package/corpus/templates/slides/app/i18n/pt-BR.ts +4 -0
- package/corpus/templates/slides/app/i18n/zh-CN.ts +4 -0
- package/corpus/templates/slides/app/i18n/zh-TW.ts +4 -0
- package/corpus/templates/slides/app/lib/image-drop-to-agent.ts +115 -0
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +22 -13
- package/corpus/templates/slides/changelog/2026-07-08-dropping-an-image-onto-a-slide-and-sending-it-to-the-agent-w.md +6 -0
- package/corpus/templates/slides/changelog/2026-07-08-undo-works-after-the-agent-edits-a-slide-so-you-can-reverse.md +6 -0
- package/corpus/templates/slides/package.json +2 -2
- package/dist/agent/run-manager.d.ts.map +1 -1
- package/dist/agent/run-manager.js +23 -2
- package/dist/agent/run-manager.js.map +1 -1
- package/dist/agent/run-store.js +2 -2
- package/dist/agent/run-store.js.map +1 -1
- package/dist/catalog.json +2 -2
- package/dist/cli/add.js +1 -1
- package/dist/cli/add.js.map +1 -1
- package/dist/cli/create.d.ts.map +1 -1
- package/dist/cli/create.js +8 -3
- package/dist/cli/create.js.map +1 -1
- package/dist/cli/index.js +5 -4
- package/dist/cli/index.js.map +1 -1
- package/dist/client/extensions/ExtensionSlot.js +6 -0
- package/dist/client/extensions/ExtensionSlot.js.map +1 -1
- package/dist/client/session-replay.d.ts +8 -0
- package/dist/client/session-replay.d.ts.map +1 -1
- package/dist/client/session-replay.js +80 -3
- package/dist/client/session-replay.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/jobs/scheduler.js +26 -1
- package/dist/jobs/scheduler.js.map +1 -1
- package/dist/localization/default-messages.d.ts +19 -0
- package/dist/localization/default-messages.d.ts.map +1 -1
- package/dist/localization/default-messages.js +19 -0
- package/dist/localization/default-messages.js.map +1 -1
- package/dist/provider-api/corpus-jobs.d.ts +2 -2
- package/dist/resources/handlers.d.ts +2 -2
- package/dist/templates/default/app/i18n/ar-SA.ts +21 -0
- package/dist/templates/default/app/i18n/de-DE.ts +21 -0
- package/dist/templates/default/app/i18n/en-US.ts +21 -0
- package/dist/templates/default/app/i18n/es-ES.ts +21 -0
- package/dist/templates/default/app/i18n/fr-FR.ts +21 -0
- package/dist/templates/default/app/i18n/hi-IN.ts +20 -0
- package/dist/templates/default/app/i18n/ja-JP.ts +21 -0
- package/dist/templates/default/app/i18n/ko-KR.ts +20 -0
- package/dist/templates/default/app/i18n/pt-BR.ts +21 -0
- package/dist/templates/default/app/i18n/zh-CN.ts +20 -0
- package/dist/templates/default/app/i18n/zh-TW.ts +19 -0
- package/dist/templates/default/package.json +2 -2
- package/dist/templates/headless/package.json +3 -3
- package/dist/templates/workspace-core/package.json +6 -6
- package/dist/templates/workspace-root/package.json +2 -2
- package/dist/triggers/dispatcher.js +27 -2
- package/dist/triggers/dispatcher.js.map +1 -1
- package/dist/vite/client.d.ts.map +1 -1
- package/dist/vite/client.js +70 -1
- package/dist/vite/client.js.map +1 -1
- package/package.json +7 -6
- package/src/templates/default/app/i18n/ar-SA.ts +21 -0
- package/src/templates/default/app/i18n/de-DE.ts +21 -0
- package/src/templates/default/app/i18n/en-US.ts +21 -0
- package/src/templates/default/app/i18n/es-ES.ts +21 -0
- package/src/templates/default/app/i18n/fr-FR.ts +21 -0
- package/src/templates/default/app/i18n/hi-IN.ts +20 -0
- package/src/templates/default/app/i18n/ja-JP.ts +21 -0
- package/src/templates/default/app/i18n/ko-KR.ts +20 -0
- package/src/templates/default/app/i18n/pt-BR.ts +21 -0
- package/src/templates/default/app/i18n/zh-CN.ts +20 -0
- package/src/templates/default/app/i18n/zh-TW.ts +19 -0
- package/src/templates/default/package.json +2 -2
- package/src/templates/headless/package.json +3 -3
- package/src/templates/workspace-core/package.json +6 -6
- package/src/templates/workspace-root/package.json +2 -2
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
//! Granola-style adhoc Zoom / Teams detection.
|
|
2
|
+
//!
|
|
3
|
+
//! Polls the frontmost macOS app every few seconds. When Zoom or Teams stays
|
|
4
|
+
//! frontmost for a short dwell window, creates a meeting row via
|
|
5
|
+
//! `create-meeting` and shows the same meeting-notification overlay used for
|
|
6
|
+
//! calendar reminders — with `type: "adhoc"`.
|
|
7
|
+
//!
|
|
8
|
+
//! Reuses `MeetingsWatcherState` session (server URL + cookie + auth token)
|
|
9
|
+
//! so the popover only needs to push credentials once.
|
|
10
|
+
|
|
11
|
+
use std::collections::HashMap;
|
|
12
|
+
use std::sync::Mutex;
|
|
13
|
+
use std::time::{Duration, Instant};
|
|
14
|
+
|
|
15
|
+
use tauri::{AppHandle, Emitter, Manager};
|
|
16
|
+
|
|
17
|
+
use crate::config::{feature_config, MeetingTranscriptionMode};
|
|
18
|
+
use crate::dlog;
|
|
19
|
+
use crate::meetings_watcher::MeetingsWatcherState;
|
|
20
|
+
|
|
21
|
+
/// How often to sample the frontmost app.
|
|
22
|
+
const POLL_SECS: u64 = 4;
|
|
23
|
+
|
|
24
|
+
/// Require this many consecutive frontmost seconds before firing.
|
|
25
|
+
const DWELL_SECS: u64 = 9;
|
|
26
|
+
|
|
27
|
+
/// After dismiss / fire, suppress re-prompts for this platform until the
|
|
28
|
+
/// cooldown elapses (or the VC app leaves front — see tick logic).
|
|
29
|
+
const COOLDOWN_SECS: i64 = 30 * 60;
|
|
30
|
+
|
|
31
|
+
/// Soft guard: skip adhoc if a calendar reminder for the same platform fired
|
|
32
|
+
/// this recently.
|
|
33
|
+
const CALENDAR_SOFT_GUARD_SECS: i64 = 3 * 60;
|
|
34
|
+
|
|
35
|
+
const STRONG_VC_BUNDLES: &[(&str, &str, &str)] = &[
|
|
36
|
+
// (bundle_id, platform, display title)
|
|
37
|
+
("us.zoom.xos", "zoom", "Zoom meeting detected"),
|
|
38
|
+
("us.zoom.ZoomClips", "zoom", "Zoom meeting detected"),
|
|
39
|
+
("com.microsoft.teams2", "teams", "Teams meeting detected"),
|
|
40
|
+
("com.microsoft.teams", "teams", "Teams meeting detected"),
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
#[derive(Default)]
|
|
44
|
+
pub struct AdhocMeetingsWatcherState {
|
|
45
|
+
inner: Mutex<AdhocMeetingsWatcherInner>,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
#[derive(Default)]
|
|
49
|
+
struct AdhocMeetingsWatcherInner {
|
|
50
|
+
/// platform -> unix-seconds when we last fired (or dismissed via cooldown).
|
|
51
|
+
cooldown_until: HashMap<String, i64>,
|
|
52
|
+
/// Current dwell: which platform is accumulating frontmost time.
|
|
53
|
+
dwell_platform: Option<String>,
|
|
54
|
+
dwell_since: Option<Instant>,
|
|
55
|
+
/// Platforms already notified for the current continuous foreground session.
|
|
56
|
+
session_notified: HashMap<String, bool>,
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/// Spawn the long-running adhoc watcher. Idempotent — gated by OnceLock.
|
|
60
|
+
pub fn spawn_watcher(app: AppHandle) {
|
|
61
|
+
use std::sync::OnceLock;
|
|
62
|
+
static STARTED: OnceLock<()> = OnceLock::new();
|
|
63
|
+
if STARTED.set(()).is_err() {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
tauri::async_runtime::spawn(async move {
|
|
67
|
+
run_watcher(app).await;
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async fn run_watcher(app: AppHandle) {
|
|
72
|
+
let mut interval = tokio::time::interval(Duration::from_secs(POLL_SECS));
|
|
73
|
+
// Skip the first tick — give the frontend time to push session creds.
|
|
74
|
+
interval.tick().await;
|
|
75
|
+
let client = match reqwest::Client::builder()
|
|
76
|
+
.timeout(Duration::from_secs(10))
|
|
77
|
+
.build()
|
|
78
|
+
{
|
|
79
|
+
Ok(c) => c,
|
|
80
|
+
Err(err) => {
|
|
81
|
+
eprintln!("[clips-tray] adhoc_meetings_watcher: reqwest build failed: {err}");
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
loop {
|
|
86
|
+
interval.tick().await;
|
|
87
|
+
if let Err(err) = tick_once(&app, &client).await {
|
|
88
|
+
eprintln!("[clips-tray] adhoc_meetings_watcher tick failed: {err}");
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
fn match_vc_bundle(bundle: &str) -> Option<(&'static str, &'static str)> {
|
|
94
|
+
STRONG_VC_BUNDLES
|
|
95
|
+
.iter()
|
|
96
|
+
.find(|(id, _, _)| *id == bundle)
|
|
97
|
+
.map(|(_, platform, title)| (*platform, *title))
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async fn tick_once(app: &AppHandle, client: &reqwest::Client) -> Result<(), String> {
|
|
101
|
+
let config = feature_config(app);
|
|
102
|
+
if !config.meetings_enabled {
|
|
103
|
+
reset_dwell(app);
|
|
104
|
+
return Ok(());
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
#[cfg(not(target_os = "macos"))]
|
|
108
|
+
{
|
|
109
|
+
let _ = (app, client);
|
|
110
|
+
return Ok(());
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
#[cfg(target_os = "macos")]
|
|
114
|
+
{
|
|
115
|
+
tick_macos(app, client, &config).await
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
#[cfg(target_os = "macos")]
|
|
120
|
+
async fn tick_macos(
|
|
121
|
+
app: &AppHandle,
|
|
122
|
+
client: &reqwest::Client,
|
|
123
|
+
config: &crate::config::FeatureConfig,
|
|
124
|
+
) -> Result<(), String> {
|
|
125
|
+
let front = crate::util::frontmost_bundle_id();
|
|
126
|
+
let matched = front
|
|
127
|
+
.as_deref()
|
|
128
|
+
.and_then(match_vc_bundle);
|
|
129
|
+
|
|
130
|
+
let Some((platform, title)) = matched else {
|
|
131
|
+
// VC app left front — clear session dedupe so a later re-focus can
|
|
132
|
+
// fire again (cooldown still applies).
|
|
133
|
+
clear_session_if_left(app, None);
|
|
134
|
+
reset_dwell(app);
|
|
135
|
+
return Ok(());
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
clear_session_if_left(app, Some(platform));
|
|
139
|
+
|
|
140
|
+
// Skip while already transcribing a meeting.
|
|
141
|
+
if crate::util::is_meeting_active(app) {
|
|
142
|
+
reset_dwell(app);
|
|
143
|
+
return Ok(());
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if config.meeting_transcription_mode == MeetingTranscriptionMode::Manual
|
|
147
|
+
&& !config.show_meeting_widget_enabled
|
|
148
|
+
{
|
|
149
|
+
reset_dwell(app);
|
|
150
|
+
return Ok(());
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Soft guard against double-prompting after a calendar reminder.
|
|
154
|
+
if let Some(state) = app.try_state::<MeetingsWatcherState>() {
|
|
155
|
+
if state.recent_calendar_notify(platform, CALENDAR_SOFT_GUARD_SECS) {
|
|
156
|
+
dlog!(
|
|
157
|
+
"[clips-tray] adhoc skip: recent calendar notify for {}",
|
|
158
|
+
platform
|
|
159
|
+
);
|
|
160
|
+
reset_dwell(app);
|
|
161
|
+
return Ok(());
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
let now_ts = chrono::Utc::now().timestamp();
|
|
166
|
+
let should_fire = {
|
|
167
|
+
let state = app
|
|
168
|
+
.try_state::<AdhocMeetingsWatcherState>()
|
|
169
|
+
.ok_or_else(|| "no AdhocMeetingsWatcherState".to_string())?;
|
|
170
|
+
let mut g = state.inner.lock().map_err(|e| e.to_string())?;
|
|
171
|
+
|
|
172
|
+
// Prune expired cooldowns.
|
|
173
|
+
g.cooldown_until.retain(|_, until| *until > now_ts);
|
|
174
|
+
|
|
175
|
+
if g.cooldown_until.get(platform).copied().unwrap_or(0) > now_ts {
|
|
176
|
+
return Ok(());
|
|
177
|
+
}
|
|
178
|
+
if g.session_notified.get(platform).copied().unwrap_or(false) {
|
|
179
|
+
return Ok(());
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
match (g.dwell_platform.as_deref(), g.dwell_since) {
|
|
183
|
+
(Some(p), Some(since)) if p == platform => {
|
|
184
|
+
if since.elapsed() >= Duration::from_secs(DWELL_SECS) {
|
|
185
|
+
g.session_notified.insert(platform.to_string(), true);
|
|
186
|
+
g.cooldown_until
|
|
187
|
+
.insert(platform.to_string(), now_ts + COOLDOWN_SECS);
|
|
188
|
+
g.dwell_platform = None;
|
|
189
|
+
g.dwell_since = None;
|
|
190
|
+
true
|
|
191
|
+
} else {
|
|
192
|
+
false
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
_ => {
|
|
196
|
+
g.dwell_platform = Some(platform.to_string());
|
|
197
|
+
g.dwell_since = Some(Instant::now());
|
|
198
|
+
false
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
if !should_fire {
|
|
204
|
+
return Ok(());
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
dlog!(
|
|
208
|
+
"[clips-tray] adhoc dwell met for {} — creating meeting",
|
|
209
|
+
platform
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
let meeting_id = match create_adhoc_meeting(app, client, platform).await {
|
|
213
|
+
Ok(id) => id,
|
|
214
|
+
Err(err) => {
|
|
215
|
+
// Allow retry on next dwell if create failed.
|
|
216
|
+
if let Some(state) = app.try_state::<AdhocMeetingsWatcherState>() {
|
|
217
|
+
if let Ok(mut g) = state.inner.lock() {
|
|
218
|
+
g.session_notified.remove(platform);
|
|
219
|
+
g.cooldown_until.remove(platform);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return Err(err);
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
let auto_start = config.meeting_transcription_mode == MeetingTranscriptionMode::Auto;
|
|
227
|
+
let show_widget = config.show_meeting_widget_enabled
|
|
228
|
+
|| config.meeting_transcription_mode == MeetingTranscriptionMode::Ask
|
|
229
|
+
|| auto_start;
|
|
230
|
+
|
|
231
|
+
if show_widget {
|
|
232
|
+
let app_clone = app.clone();
|
|
233
|
+
let id_clone = meeting_id.clone();
|
|
234
|
+
let title_clone = title.to_string();
|
|
235
|
+
let platform_clone = platform.to_string();
|
|
236
|
+
let scheduled_start = chrono::Utc::now().to_rfc3339();
|
|
237
|
+
tauri::async_runtime::spawn(async move {
|
|
238
|
+
let _ = crate::notifications::notify_meeting_starting(
|
|
239
|
+
app_clone,
|
|
240
|
+
id_clone,
|
|
241
|
+
title_clone,
|
|
242
|
+
0,
|
|
243
|
+
None,
|
|
244
|
+
Some(scheduled_start),
|
|
245
|
+
None,
|
|
246
|
+
Some(platform_clone),
|
|
247
|
+
Some(auto_start),
|
|
248
|
+
Some("adhoc".to_string()),
|
|
249
|
+
)
|
|
250
|
+
.await;
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if auto_start {
|
|
255
|
+
let _ = app.emit(
|
|
256
|
+
"meetings:start-transcription",
|
|
257
|
+
serde_json::json!({
|
|
258
|
+
"meetingId": meeting_id,
|
|
259
|
+
"joinUrl": null,
|
|
260
|
+
"reason": "adhoc-auto",
|
|
261
|
+
}),
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
Ok(())
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
fn reset_dwell(app: &AppHandle) {
|
|
269
|
+
if let Some(state) = app.try_state::<AdhocMeetingsWatcherState>() {
|
|
270
|
+
if let Ok(mut g) = state.inner.lock() {
|
|
271
|
+
g.dwell_platform = None;
|
|
272
|
+
g.dwell_since = None;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/// When the frontmost VC platform changes (or clears), drop session_notified
|
|
278
|
+
/// for platforms that are no longer front. Keep `cooldown_until` so leaving
|
|
279
|
+
/// and re-focusing Zoom within the cooldown does not spam another popup.
|
|
280
|
+
fn clear_session_if_left(app: &AppHandle, current: Option<&str>) {
|
|
281
|
+
if let Some(state) = app.try_state::<AdhocMeetingsWatcherState>() {
|
|
282
|
+
if let Ok(mut g) = state.inner.lock() {
|
|
283
|
+
g.session_notified
|
|
284
|
+
.retain(|platform, _| current == Some(platform.as_str()));
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async fn create_adhoc_meeting(
|
|
290
|
+
app: &AppHandle,
|
|
291
|
+
client: &reqwest::Client,
|
|
292
|
+
platform: &str,
|
|
293
|
+
) -> Result<String, String> {
|
|
294
|
+
let session = app
|
|
295
|
+
.try_state::<MeetingsWatcherState>()
|
|
296
|
+
.map(|s| s.session_snapshot())
|
|
297
|
+
.unwrap_or_default();
|
|
298
|
+
let Some(server_url) = session.server_url else {
|
|
299
|
+
return Err("no server_url for create-meeting".to_string());
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
let url = format!("{}/_agent-native/actions/create-meeting", server_url);
|
|
303
|
+
let row_title = if platform == "zoom" {
|
|
304
|
+
"Zoom meeting"
|
|
305
|
+
} else {
|
|
306
|
+
"Teams meeting"
|
|
307
|
+
};
|
|
308
|
+
let body = serde_json::json!({
|
|
309
|
+
"title": row_title,
|
|
310
|
+
"platform": platform,
|
|
311
|
+
"source": "adhoc",
|
|
312
|
+
"scheduledStart": chrono::Utc::now().to_rfc3339(),
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
let mut req = client
|
|
316
|
+
.post(&url)
|
|
317
|
+
.header("Content-Type", "application/json")
|
|
318
|
+
.header("X-Request-Source", "clips-desktop")
|
|
319
|
+
.json(&body);
|
|
320
|
+
if let Some(c) = session.session_cookie.as_deref() {
|
|
321
|
+
req = req.header("Cookie", c);
|
|
322
|
+
}
|
|
323
|
+
if let Some(token) = session.auth_token.as_deref() {
|
|
324
|
+
req = req.bearer_auth(token);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
let resp = req
|
|
328
|
+
.send()
|
|
329
|
+
.await
|
|
330
|
+
.map_err(|e| format!("create-meeting fetch: {e}"))?;
|
|
331
|
+
let status = resp.status();
|
|
332
|
+
if status == reqwest::StatusCode::UNAUTHORIZED {
|
|
333
|
+
let _ = app.emit("meetings:auth-needed", serde_json::json!({}));
|
|
334
|
+
return Err("create-meeting http 401".to_string());
|
|
335
|
+
}
|
|
336
|
+
if !status.is_success() {
|
|
337
|
+
let text = resp.text().await.unwrap_or_default();
|
|
338
|
+
return Err(format!(
|
|
339
|
+
"create-meeting http {} — {}",
|
|
340
|
+
status,
|
|
341
|
+
text.chars().take(180).collect::<String>()
|
|
342
|
+
));
|
|
343
|
+
}
|
|
344
|
+
let body: serde_json::Value = resp.json().await.map_err(|e| e.to_string())?;
|
|
345
|
+
extract_meeting_id(&body).ok_or_else(|| {
|
|
346
|
+
format!(
|
|
347
|
+
"create-meeting response missing meeting id: {}",
|
|
348
|
+
body.to_string().chars().take(200).collect::<String>()
|
|
349
|
+
)
|
|
350
|
+
})
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
fn extract_meeting_id(body: &serde_json::Value) -> Option<String> {
|
|
354
|
+
// Framework wraps action returns as `{ result: { meeting, created } }`.
|
|
355
|
+
let meeting = body
|
|
356
|
+
.get("result")
|
|
357
|
+
.and_then(|r| r.get("meeting"))
|
|
358
|
+
.or_else(|| body.get("meeting"))
|
|
359
|
+
.or_else(|| body.get("result"));
|
|
360
|
+
meeting
|
|
361
|
+
.and_then(|m| m.get("id"))
|
|
362
|
+
.and_then(|id| id.as_str())
|
|
363
|
+
.map(|s| s.to_string())
|
|
364
|
+
}
|
|
@@ -56,7 +56,7 @@ const ONBOARDING_HEIGHT_LOGICAL: f64 = 640.0;
|
|
|
56
56
|
/// launch — this matches Loom's out-of-the-box behavior.
|
|
57
57
|
const BUBBLE_SIZE_SMALL: u32 = 360;
|
|
58
58
|
const BUBBLE_SIZE_MEDIUM: u32 = 504;
|
|
59
|
-
const POPOVER_SHADOW_GUTTER_LOGICAL: f64 =
|
|
59
|
+
const POPOVER_SHADOW_GUTTER_LOGICAL: f64 = 24.0;
|
|
60
60
|
const POPOVER_DEFAULT_WIDTH_LOGICAL: f64 = 360.0;
|
|
61
61
|
const POPOVER_DEFAULT_HEIGHT_LOGICAL: f64 = 520.0;
|
|
62
62
|
const OVERLAY_SHADOW_GUTTER_LOGICAL: f64 = 18.0;
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
//! is served by the Vite-built React UI (see `../dist`).
|
|
6
6
|
|
|
7
7
|
mod accessibility;
|
|
8
|
+
mod adhoc_meetings_watcher;
|
|
8
9
|
mod clips;
|
|
9
10
|
mod config;
|
|
10
11
|
mod debug;
|
|
@@ -211,6 +212,7 @@ pub fn run() {
|
|
|
211
212
|
.manage(native_screen::NativeFullscreenRecordingState::default())
|
|
212
213
|
.manage(screen_memory::ScreenMemoryState::default())
|
|
213
214
|
.manage(meetings_watcher::MeetingsWatcherState::default())
|
|
215
|
+
.manage(adhoc_meetings_watcher::AdhocMeetingsWatcherState::default())
|
|
214
216
|
.manage(notifications::MeetingNotificationState::default())
|
|
215
217
|
.manage(silence_detector::DetectorState::default())
|
|
216
218
|
.setup(|app| {
|
|
@@ -260,6 +262,9 @@ pub fn run() {
|
|
|
260
262
|
// server URL via `meetings_watcher_set_server_url` once the
|
|
261
263
|
// popover boots.
|
|
262
264
|
meetings_watcher::spawn_watcher(app.handle().clone());
|
|
265
|
+
// Granola-style adhoc Zoom/Teams detection — shares session
|
|
266
|
+
// credentials with the calendar watcher above.
|
|
267
|
+
adhoc_meetings_watcher::spawn_watcher(app.handle().clone());
|
|
263
268
|
|
|
264
269
|
// Pre-download the Whisper model in the background so the first
|
|
265
270
|
// meeting doesn't pay the ~142 MB download cost mid-call. Skipped
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
//!
|
|
3
3
|
//! Runs as a tokio task spawned from `lib.rs::run` setup. Every 30s it calls
|
|
4
4
|
//! the backend's `list-meetings` action for the next handful of live Google
|
|
5
|
-
//! Calendar meetings. For any meeting
|
|
6
|
-
//!
|
|
7
|
-
//! banner overlay.
|
|
5
|
+
//! Calendar meetings. For any meeting in the Granola-style reminder window
|
|
6
|
+
//! (1 minute before start through 5 minutes after) that we haven't already
|
|
7
|
+
//! alerted on, we fire the in-app banner overlay.
|
|
8
8
|
//!
|
|
9
9
|
//! ## Wire-up (from the popover renderer)
|
|
10
10
|
//!
|
|
@@ -39,8 +39,12 @@ use crate::tray_meetings::MeetingItem as TrayMeetingItem;
|
|
|
39
39
|
|
|
40
40
|
const MEETING_POLL_LIMIT: u8 = 10;
|
|
41
41
|
|
|
42
|
-
///
|
|
43
|
-
const NOTIFY_LEAD_SECS: i64 =
|
|
42
|
+
/// Show the reminder starting this many seconds before meeting start.
|
|
43
|
+
const NOTIFY_LEAD_SECS: i64 = 60;
|
|
44
|
+
|
|
45
|
+
/// Keep reminding eligible (until dismissed / acted on) this many seconds
|
|
46
|
+
/// after the scheduled start. Overlay auto-hide mirrors this hold window.
|
|
47
|
+
const NOTIFY_HOLD_AFTER_START_SECS: i64 = 5 * 60;
|
|
44
48
|
|
|
45
49
|
/// Forget de-dupe / snooze entries once a meeting's start is this far past, so
|
|
46
50
|
/// the maps don't grow unbounded across a long-running session.
|
|
@@ -80,6 +84,53 @@ struct MeetingsWatcherInner {
|
|
|
80
84
|
/// meetingId -> unix-seconds deadline. While now < deadline the meeting is
|
|
81
85
|
/// skipped; once it passes we re-fire the reminder exactly once.
|
|
82
86
|
snoozed_until: HashMap<String, i64>,
|
|
87
|
+
/// platform -> unix-seconds when a calendar reminder last fired. Soft
|
|
88
|
+
/// guard so adhoc Zoom/Teams detection doesn't double-prompt right after
|
|
89
|
+
/// a calendar banner for the same app.
|
|
90
|
+
last_calendar_notify_at: HashMap<String, i64>,
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/// Snapshot of auth fields the adhoc watcher needs to POST create-meeting.
|
|
94
|
+
#[derive(Clone, Default)]
|
|
95
|
+
pub struct MeetingsSessionSnapshot {
|
|
96
|
+
pub server_url: Option<String>,
|
|
97
|
+
pub session_cookie: Option<String>,
|
|
98
|
+
pub auth_token: Option<String>,
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
impl MeetingsWatcherState {
|
|
102
|
+
pub fn session_snapshot(&self) -> MeetingsSessionSnapshot {
|
|
103
|
+
let Ok(g) = self.inner.lock() else {
|
|
104
|
+
return MeetingsSessionSnapshot::default();
|
|
105
|
+
};
|
|
106
|
+
MeetingsSessionSnapshot {
|
|
107
|
+
server_url: g.server_url.clone(),
|
|
108
|
+
session_cookie: g.session_cookie.clone(),
|
|
109
|
+
auth_token: g.auth_token.clone(),
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
pub fn note_calendar_notify(&self, platform: Option<&str>) {
|
|
114
|
+
let Some(platform) = platform.map(str::trim).filter(|p| !p.is_empty()) else {
|
|
115
|
+
return;
|
|
116
|
+
};
|
|
117
|
+
if let Ok(mut g) = self.inner.lock() {
|
|
118
|
+
g.last_calendar_notify_at
|
|
119
|
+
.insert(platform.to_lowercase(), chrono::Utc::now().timestamp());
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/// True if a calendar reminder for `platform` fired within `within_secs`.
|
|
124
|
+
pub fn recent_calendar_notify(&self, platform: &str, within_secs: i64) -> bool {
|
|
125
|
+
let Ok(g) = self.inner.lock() else {
|
|
126
|
+
return false;
|
|
127
|
+
};
|
|
128
|
+
let key = platform.to_lowercase();
|
|
129
|
+
let Some(at) = g.last_calendar_notify_at.get(&key).copied() else {
|
|
130
|
+
return false;
|
|
131
|
+
};
|
|
132
|
+
chrono::Utc::now().timestamp() - at <= within_secs
|
|
133
|
+
}
|
|
83
134
|
}
|
|
84
135
|
|
|
85
136
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
|
@@ -88,8 +139,12 @@ struct MeetingItem {
|
|
|
88
139
|
title: Option<String>,
|
|
89
140
|
#[serde(default, alias = "scheduledStart")]
|
|
90
141
|
scheduled_start: Option<String>,
|
|
142
|
+
#[serde(default, alias = "scheduledEnd")]
|
|
143
|
+
scheduled_end: Option<String>,
|
|
91
144
|
#[serde(default, alias = "joinUrl")]
|
|
92
145
|
join_url: Option<String>,
|
|
146
|
+
#[serde(default)]
|
|
147
|
+
platform: Option<String>,
|
|
93
148
|
}
|
|
94
149
|
|
|
95
150
|
#[derive(Debug, Deserialize)]
|
|
@@ -232,9 +287,19 @@ async fn tick_once(app: &AppHandle, client: &reqwest::Client) -> Result<(), Stri
|
|
|
232
287
|
|
|
233
288
|
let url = format!("{}/_agent-native/actions/list-meetings", server_url);
|
|
234
289
|
let limit = MEETING_POLL_LIMIT.to_string();
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
290
|
+
// Include meetings that started within the hold window so a late-open
|
|
291
|
+
// desktop still surfaces the reminder until 5 minutes after start.
|
|
292
|
+
let within_min = ((NOTIFY_LEAD_SECS + NOTIFY_HOLD_AFTER_START_SECS) / 60
|
|
293
|
+
+ 1)
|
|
294
|
+
.to_string();
|
|
295
|
+
let mut req = client.get(&url).query(&[
|
|
296
|
+
("view", "upcoming"),
|
|
297
|
+
("limit", limit.as_str()),
|
|
298
|
+
("upcomingWithinMin", within_min.as_str()),
|
|
299
|
+
// list-meetings also uses this for the lower bound when we widen the
|
|
300
|
+
// upcoming window to include recently-started events (see action).
|
|
301
|
+
("includeStartedWithinMin", "5"),
|
|
302
|
+
]);
|
|
238
303
|
req = req.header("X-Request-Source", "clips-desktop");
|
|
239
304
|
if let Some(c) = cookie.as_deref() {
|
|
240
305
|
req = req.header("Cookie", c);
|
|
@@ -301,14 +366,19 @@ async fn tick_once(app: &AppHandle, client: &reqwest::Client) -> Result<(), Stri
|
|
|
301
366
|
g.snoozed_until
|
|
302
367
|
.retain(|_, until| *until > now_ts - STALE_AFTER_SECS);
|
|
303
368
|
|
|
369
|
+
// Eligible from 1 min before start through 5 min after start.
|
|
370
|
+
// secs_until > 0 => still upcoming; negative => already started.
|
|
371
|
+
let in_window = secs_until <= NOTIFY_LEAD_SECS
|
|
372
|
+
&& secs_until >= -NOTIFY_HOLD_AFTER_START_SECS;
|
|
373
|
+
|
|
304
374
|
let eligible = match g.snoozed_until.get(&m.id).copied() {
|
|
305
375
|
Some(until) if now_ts < until => false, // still snoozed
|
|
306
376
|
Some(_) => {
|
|
307
|
-
// Snooze elapsed — re-fire
|
|
377
|
+
// Snooze elapsed — re-fire if still inside the hold window.
|
|
308
378
|
g.snoozed_until.remove(&m.id);
|
|
309
|
-
|
|
379
|
+
in_window
|
|
310
380
|
}
|
|
311
|
-
None =>
|
|
381
|
+
None => in_window,
|
|
312
382
|
};
|
|
313
383
|
|
|
314
384
|
if !eligible {
|
|
@@ -333,10 +403,16 @@ async fn tick_once(app: &AppHandle, client: &reqwest::Client) -> Result<(), Stri
|
|
|
333
403
|
if config.show_meeting_widget_enabled
|
|
334
404
|
|| config.meeting_transcription_mode == MeetingTranscriptionMode::Auto
|
|
335
405
|
{
|
|
406
|
+
if let Some(state) = app.try_state::<MeetingsWatcherState>() {
|
|
407
|
+
state.note_calendar_notify(m.platform.as_deref());
|
|
408
|
+
}
|
|
336
409
|
let app_clone = app.clone();
|
|
337
410
|
let id_clone = m.id.clone();
|
|
338
411
|
let title_clone = title.clone();
|
|
339
412
|
let join_clone = join_url.clone();
|
|
413
|
+
let start_clone = m.scheduled_start.clone();
|
|
414
|
+
let end_clone = m.scheduled_end.clone();
|
|
415
|
+
let platform_clone = m.platform.clone();
|
|
340
416
|
let auto_start = config.meeting_transcription_mode == MeetingTranscriptionMode::Auto;
|
|
341
417
|
tauri::async_runtime::spawn(async move {
|
|
342
418
|
let _ = crate::notifications::notify_meeting_starting(
|
|
@@ -345,7 +421,11 @@ async fn tick_once(app: &AppHandle, client: &reqwest::Client) -> Result<(), Stri
|
|
|
345
421
|
title_clone,
|
|
346
422
|
secs_until,
|
|
347
423
|
join_clone,
|
|
424
|
+
start_clone,
|
|
425
|
+
end_clone,
|
|
426
|
+
platform_clone,
|
|
348
427
|
Some(auto_start),
|
|
428
|
+
None,
|
|
349
429
|
)
|
|
350
430
|
.await;
|
|
351
431
|
});
|