@agent-native/core 0.91.0 → 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 +26 -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/design-connect.ts +53 -9
- 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/AGENTS.md +9 -0
- package/corpus/templates/design/actions/request-localhost-write-consent.ts +141 -0
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +13 -19
- package/corpus/templates/design/package.json +2 -2
- package/corpus/templates/design/server/lib/verify-write-grant.ts +4 -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/design-connect.d.ts.map +1 -1
- package/dist/cli/design-connect.js +40 -10
- package/dist/cli/design-connect.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/routes.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/notifications/routes.d.ts +1 -1
- package/dist/observability/routes.d.ts +1 -1
- package/dist/provider-api/corpus-jobs.d.ts +2 -2
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- 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
|
@@ -65,18 +65,36 @@ const AUDIO_DENOISE_FILTER: &str = "afftdn=nr=10:nf=-50:tn=1";
|
|
|
65
65
|
// explicit output rate the AAC track ends up at 192 kHz and plays back slow.
|
|
66
66
|
const AUDIO_OUTPUT_SAMPLE_RATE: u32 = 48000;
|
|
67
67
|
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
|
|
68
|
+
// Pre-gain for mic-only native captures. ScreenCaptureKit records without
|
|
69
|
+
// WebRTC AGC (unlike Loom / the browser path), so speech often lands well
|
|
70
|
+
// below -16 LUFS. 12 dB before loudnorm matches Loom-ish perceptual loudness
|
|
71
|
+
// for MacBook mics; loudnorm still clamps true peaks at TP=-1.5.
|
|
72
|
+
const AUDIO_MIC_PREGAIN_FILTER: &str = "volume=12dB";
|
|
73
|
+
// After the mic+system centered downmix (0.5*L+0.5*R) each source is ~
|
|
74
|
+
// 6 dB quieter. Restore energy before loudnorm so dual-capture clips are not
|
|
75
|
+
// systematically quieter than mic-only. Dual still mixes two sources so peaks
|
|
76
|
+
// may compress more under loudnorm than mic-only — by design when both sides
|
|
77
|
+
// compete for the same LUFS budget.
|
|
78
|
+
const AUDIO_DOWNMIX_MAKEUP_FILTER: &str = "volume=6dB";
|
|
79
|
+
|
|
80
|
+
// Loudness normalization, optionally preceded by the centered-stereo downmix
|
|
81
|
+
// that repairs the mic+system L/R split and denoise for native mic captures.
|
|
82
|
+
// Pair with `-ar AUDIO_OUTPUT_SAMPLE_RATE` so loudnorm's 192 kHz output is
|
|
83
|
+
// resampled back.
|
|
84
|
+
fn audio_filter_chain(downmix: bool, denoise: bool, mic_pregain: bool) -> String {
|
|
73
85
|
let mut filters = Vec::new();
|
|
74
86
|
if downmix {
|
|
75
87
|
filters.push(AUDIO_DOWNMIX_FILTER);
|
|
88
|
+
// Undo pan attenuation; do not also apply mic-only pregain here —
|
|
89
|
+
// system audio would get a double boost.
|
|
90
|
+
filters.push(AUDIO_DOWNMIX_MAKEUP_FILTER);
|
|
76
91
|
}
|
|
77
92
|
if denoise {
|
|
78
93
|
filters.push(AUDIO_DENOISE_FILTER);
|
|
79
94
|
}
|
|
95
|
+
if mic_pregain {
|
|
96
|
+
filters.push(AUDIO_MIC_PREGAIN_FILTER);
|
|
97
|
+
}
|
|
80
98
|
filters.push(AUDIO_LOUDNESS_FILTER);
|
|
81
99
|
filters.join(",")
|
|
82
100
|
}
|
|
@@ -522,11 +540,17 @@ struct SavedNativeRecording {
|
|
|
522
540
|
height: Option<u32>,
|
|
523
541
|
bytes: u64,
|
|
524
542
|
has_audio: bool,
|
|
525
|
-
// Whether the mic was captured. Drives
|
|
526
|
-
// for the mic+system L/R split. Defaults to
|
|
527
|
-
// before this field existed so their audio is
|
|
543
|
+
// Whether the mic was captured. Drives denoise + (with system audio) the
|
|
544
|
+
// centered-stereo downmix repair for the mic+system L/R split. Defaults to
|
|
545
|
+
// false for recordings queued before this field existed so their audio is
|
|
546
|
+
// left untouched.
|
|
528
547
|
#[serde(default)]
|
|
529
548
|
mic_captured: bool,
|
|
549
|
+
// Whether system audio was captured alongside the mic. Needed to decide
|
|
550
|
+
// downmix vs mic-only pregain on retry uploads. Defaults to false for
|
|
551
|
+
// older pending files (safe: skips downmix that would attenuate mic-only).
|
|
552
|
+
#[serde(default)]
|
|
553
|
+
system_audio_captured: bool,
|
|
530
554
|
has_camera: bool,
|
|
531
555
|
saved_at: String,
|
|
532
556
|
last_attempt_at: Option<String>,
|
|
@@ -2577,6 +2601,7 @@ fn saved_recording_from_path(
|
|
|
2577
2601
|
bytes,
|
|
2578
2602
|
has_audio,
|
|
2579
2603
|
mic_captured: session.restart.mic_captured_in_file,
|
|
2604
|
+
system_audio_captured: session.restart.capture_system_audio,
|
|
2580
2605
|
has_camera,
|
|
2581
2606
|
saved_at: now_iso(),
|
|
2582
2607
|
last_attempt_at: None,
|
|
@@ -3071,6 +3096,7 @@ async fn upload_recording_file(
|
|
|
3071
3096
|
Some(duration_ms),
|
|
3072
3097
|
has_audio,
|
|
3073
3098
|
session.restart.mic_captured_in_file,
|
|
3099
|
+
session.restart.capture_system_audio,
|
|
3074
3100
|
)?;
|
|
3075
3101
|
let upload_result = upload_prepared_recording_file(
|
|
3076
3102
|
app,
|
|
@@ -3152,6 +3178,7 @@ fn prepare_saved_recording_file(
|
|
|
3152
3178
|
Some(saved.duration_ms),
|
|
3153
3179
|
saved.has_audio,
|
|
3154
3180
|
saved.mic_captured,
|
|
3181
|
+
saved.system_audio_captured,
|
|
3155
3182
|
)?;
|
|
3156
3183
|
Ok((prepared, retry_combined_path))
|
|
3157
3184
|
}
|
|
@@ -3878,7 +3905,14 @@ fn prepare_recording_file(
|
|
|
3878
3905
|
duration_ms: Option<u128>,
|
|
3879
3906
|
has_audio: bool,
|
|
3880
3907
|
mic_captured_audio: bool,
|
|
3908
|
+
system_audio_captured: bool,
|
|
3881
3909
|
) -> Result<PreparedRecordingFile, String> {
|
|
3910
|
+
// Downmix only repairs mic+system L/R split. Applying it to mic-only
|
|
3911
|
+
// capture halves speech energy (~6 dB) when SCK puts the mic on one channel.
|
|
3912
|
+
let downmix_audio = mic_captured_audio && system_audio_captured;
|
|
3913
|
+
let denoise_audio = mic_captured_audio;
|
|
3914
|
+
// Pregain only for mic-only (no system audio compete) — SCK has no AGC.
|
|
3915
|
+
let mic_pregain = mic_captured_audio && !system_audio_captured;
|
|
3882
3916
|
let metadata = std::fs::metadata(path).map_err(|e| {
|
|
3883
3917
|
let diag = describe_recording_path(path);
|
|
3884
3918
|
eprintln!("[clips-tray] native recording file missing at prepare: {e}; {diag}");
|
|
@@ -3935,8 +3969,9 @@ fn prepare_recording_file(
|
|
|
3935
3969
|
ffmpeg_path,
|
|
3936
3970
|
path,
|
|
3937
3971
|
&normalized_path,
|
|
3938
|
-
|
|
3939
|
-
|
|
3972
|
+
downmix_audio,
|
|
3973
|
+
denoise_audio,
|
|
3974
|
+
mic_pregain,
|
|
3940
3975
|
) {
|
|
3941
3976
|
Ok(()) => {
|
|
3942
3977
|
let normalized_bytes = std::fs::metadata(&normalized_path)
|
|
@@ -4022,8 +4057,9 @@ fn prepare_recording_file(
|
|
|
4022
4057
|
height,
|
|
4023
4058
|
duration_ms,
|
|
4024
4059
|
has_audio,
|
|
4025
|
-
|
|
4026
|
-
|
|
4060
|
+
downmix_audio,
|
|
4061
|
+
denoise_audio,
|
|
4062
|
+
mic_pregain,
|
|
4027
4063
|
) {
|
|
4028
4064
|
Ok(()) => {
|
|
4029
4065
|
let compressed_bytes = std::fs::metadata(&compressed_path)
|
|
@@ -4421,6 +4457,7 @@ fn normalize_audio_with_ffmpeg(
|
|
|
4421
4457
|
output: &Path,
|
|
4422
4458
|
downmix_audio: bool,
|
|
4423
4459
|
denoise_audio: bool,
|
|
4460
|
+
mic_pregain: bool,
|
|
4424
4461
|
) -> Result<(), String> {
|
|
4425
4462
|
let audio_bitrate = format!("{NORMALIZED_AUDIO_BITRATE_KBPS}k");
|
|
4426
4463
|
let mut command = Command::new(ffmpeg_path);
|
|
@@ -4442,7 +4479,7 @@ fn normalize_audio_with_ffmpeg(
|
|
|
4442
4479
|
.arg("-b:a")
|
|
4443
4480
|
.arg(audio_bitrate)
|
|
4444
4481
|
.arg("-af")
|
|
4445
|
-
.arg(audio_filter_chain(downmix_audio, denoise_audio))
|
|
4482
|
+
.arg(audio_filter_chain(downmix_audio, denoise_audio, mic_pregain))
|
|
4446
4483
|
.arg("-ac")
|
|
4447
4484
|
.arg("2")
|
|
4448
4485
|
.arg("-ar")
|
|
@@ -4507,6 +4544,7 @@ fn transcode_with_ffmpeg(
|
|
|
4507
4544
|
normalize_audio: bool,
|
|
4508
4545
|
downmix_audio: bool,
|
|
4509
4546
|
denoise_audio: bool,
|
|
4547
|
+
mic_pregain: bool,
|
|
4510
4548
|
) -> Result<(), String> {
|
|
4511
4549
|
let mut command = Command::new(ffmpeg_path);
|
|
4512
4550
|
command
|
|
@@ -4532,7 +4570,7 @@ fn transcode_with_ffmpeg(
|
|
|
4532
4570
|
if normalize_audio {
|
|
4533
4571
|
command
|
|
4534
4572
|
.arg("-af")
|
|
4535
|
-
.arg(audio_filter_chain(downmix_audio, denoise_audio));
|
|
4573
|
+
.arg(audio_filter_chain(downmix_audio, denoise_audio, mic_pregain));
|
|
4536
4574
|
}
|
|
4537
4575
|
|
|
4538
4576
|
let duration_rate_limit =
|
|
@@ -4965,7 +5003,8 @@ fn concat_mp4_segments(segments: &[PathBuf], output: &Path) -> Result<(), String
|
|
|
4965
5003
|
mod audio_track_probe_tests {
|
|
4966
5004
|
use super::{
|
|
4967
5005
|
audio_filter_chain, mp4_has_audio_track, parse_ffmpeg_volume_db, AudioSignalProbe,
|
|
4968
|
-
AUDIO_DENOISE_FILTER, AUDIO_DOWNMIX_FILTER,
|
|
5006
|
+
AUDIO_DENOISE_FILTER, AUDIO_DOWNMIX_FILTER, AUDIO_DOWNMIX_MAKEUP_FILTER,
|
|
5007
|
+
AUDIO_LOUDNESS_FILTER, AUDIO_MIC_PREGAIN_FILTER,
|
|
4969
5008
|
};
|
|
4970
5009
|
use std::io::Write;
|
|
4971
5010
|
|
|
@@ -5006,14 +5045,25 @@ mod audio_track_probe_tests {
|
|
|
5006
5045
|
|
|
5007
5046
|
#[test]
|
|
5008
5047
|
fn builds_native_audio_filter_chain_for_mic_noise_reduction() {
|
|
5009
|
-
assert_eq!(audio_filter_chain(false, false), AUDIO_LOUDNESS_FILTER);
|
|
5010
5048
|
assert_eq!(
|
|
5011
|
-
audio_filter_chain(false,
|
|
5049
|
+
audio_filter_chain(false, false, false),
|
|
5050
|
+
AUDIO_LOUDNESS_FILTER
|
|
5051
|
+
);
|
|
5052
|
+
assert_eq!(
|
|
5053
|
+
audio_filter_chain(false, true, false),
|
|
5012
5054
|
format!("{AUDIO_DENOISE_FILTER},{AUDIO_LOUDNESS_FILTER}")
|
|
5013
5055
|
);
|
|
5014
5056
|
assert_eq!(
|
|
5015
|
-
audio_filter_chain(true, true),
|
|
5016
|
-
format!(
|
|
5057
|
+
audio_filter_chain(false, true, true),
|
|
5058
|
+
format!(
|
|
5059
|
+
"{AUDIO_DENOISE_FILTER},{AUDIO_MIC_PREGAIN_FILTER},{AUDIO_LOUDNESS_FILTER}"
|
|
5060
|
+
)
|
|
5061
|
+
);
|
|
5062
|
+
assert_eq!(
|
|
5063
|
+
audio_filter_chain(true, true, false),
|
|
5064
|
+
format!(
|
|
5065
|
+
"{AUDIO_DOWNMIX_FILTER},{AUDIO_DOWNMIX_MAKEUP_FILTER},{AUDIO_DENOISE_FILTER},{AUDIO_LOUDNESS_FILTER}"
|
|
5066
|
+
)
|
|
5017
5067
|
);
|
|
5018
5068
|
}
|
|
5019
5069
|
|
|
@@ -134,7 +134,8 @@ pub(crate) mod macos {
|
|
|
134
134
|
kAudioOutputUnitProperty_CurrentDevice, kAudioUnitScope_Global, AudioUnitSetProperty,
|
|
135
135
|
};
|
|
136
136
|
use objc2_avf_audio::{
|
|
137
|
-
AVAudioEngine, AVAudioInputNode, AVAudioPCMBuffer,
|
|
137
|
+
AVAudioEngine, AVAudioInputNode, AVAudioPCMBuffer, AVAudioSession,
|
|
138
|
+
AVAudioSessionCategoryOptions, AVAudioSessionCategoryPlayAndRecord, AVAudioTime,
|
|
138
139
|
AVAudioVoiceProcessingOtherAudioDuckingConfiguration,
|
|
139
140
|
AVAudioVoiceProcessingOtherAudioDuckingLevel,
|
|
140
141
|
};
|
|
@@ -652,6 +653,36 @@ pub(crate) mod macos {
|
|
|
652
653
|
}
|
|
653
654
|
}
|
|
654
655
|
|
|
656
|
+
/// Keep Zoom/Meet/Teams' own mic uplink alive while Clips opens a parallel
|
|
657
|
+
/// AVAudioEngine tap. Without MixWithOthers, PlayAndRecord takes exclusive
|
|
658
|
+
/// session ownership and other apps' live calls get ducked or starved even
|
|
659
|
+
/// when VoiceProcessingIO is off.
|
|
660
|
+
fn configure_shared_mic_audio_session() {
|
|
661
|
+
unsafe {
|
|
662
|
+
let session = AVAudioSession::sharedInstance();
|
|
663
|
+
let Some(category) = AVAudioSessionCategoryPlayAndRecord else {
|
|
664
|
+
eprintln!("[whisper-mic] PlayAndRecord category missing — skipping MixWithOthers");
|
|
665
|
+
return;
|
|
666
|
+
};
|
|
667
|
+
let options = AVAudioSessionCategoryOptions::MixWithOthers;
|
|
668
|
+
if let Err(err) = session.setCategory_withOptions_error(category, options) {
|
|
669
|
+
eprintln!(
|
|
670
|
+
"[whisper-mic] setCategory MixWithOthers failed: {} — continuing",
|
|
671
|
+
ns_error_message(&err)
|
|
672
|
+
);
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
if let Err(err) = session.setActive_error(true) {
|
|
676
|
+
eprintln!(
|
|
677
|
+
"[whisper-mic] AVAudioSession setActive failed: {} — continuing",
|
|
678
|
+
ns_error_message(&err)
|
|
679
|
+
);
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
eprintln!("[whisper-mic] AVAudioSession PlayAndRecord + MixWithOthers active");
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
655
686
|
fn take_warmed_raw_mic_engine(device_key: &RawMicDeviceKey) -> Option<WarmedRawMicEngine> {
|
|
656
687
|
let mut slot = warmed_raw_mic_engine_slot().lock().ok()?;
|
|
657
688
|
let should_reuse = slot
|
|
@@ -758,6 +789,7 @@ pub(crate) mod macos {
|
|
|
758
789
|
mic_device_label: Option<&str>,
|
|
759
790
|
device_key: RawMicDeviceKey,
|
|
760
791
|
) -> Result<WarmedRawMicEngine, String> {
|
|
792
|
+
configure_shared_mic_audio_session();
|
|
761
793
|
let engine: Retained<AVAudioEngine> = unsafe { AVAudioEngine::new() };
|
|
762
794
|
configure_engine_input_device(&engine, mic_device_id, mic_device_label)?;
|
|
763
795
|
let input_node: Retained<AVAudioInputNode> = unsafe { engine.inputNode() };
|
|
@@ -955,6 +987,7 @@ pub(crate) mod macos {
|
|
|
955
987
|
// Spin up the engine and grab its input node + native format.
|
|
956
988
|
// SAFETY: `AVAudioEngine::new()` returns a retained engine.
|
|
957
989
|
// `inputNode` is the engine's singleton input — also retained.
|
|
990
|
+
configure_shared_mic_audio_session();
|
|
958
991
|
let engine: Retained<AVAudioEngine> = unsafe { AVAudioEngine::new() };
|
|
959
992
|
configure_engine_input_device(
|
|
960
993
|
&engine,
|
|
@@ -1321,6 +1354,8 @@ pub(crate) mod macos {
|
|
|
1321
1354
|
}
|
|
1322
1355
|
clear_warmed_raw_mic_engine();
|
|
1323
1356
|
|
|
1357
|
+
configure_shared_mic_audio_session();
|
|
1358
|
+
|
|
1324
1359
|
let engine: Retained<AVAudioEngine> = unsafe { AVAudioEngine::new() };
|
|
1325
1360
|
configure_engine_input_device(
|
|
1326
1361
|
&engine,
|
|
@@ -16,8 +16,8 @@ use crate::util::{
|
|
|
16
16
|
};
|
|
17
17
|
|
|
18
18
|
const MEETING_NOTIFICATION_LABEL: &str = "meeting-notif";
|
|
19
|
-
const NOTIFICATION_W_LOGICAL: u32 =
|
|
20
|
-
const NOTIFICATION_H_LOGICAL: u32 =
|
|
19
|
+
const NOTIFICATION_W_LOGICAL: u32 = 440;
|
|
20
|
+
const NOTIFICATION_H_LOGICAL: u32 = 120;
|
|
21
21
|
const NOTIFICATION_TOP_MARGIN_LOGICAL: u32 = 44;
|
|
22
22
|
const NOTIFICATION_RIGHT_MARGIN_LOGICAL: u32 = 24;
|
|
23
23
|
|
|
@@ -109,6 +109,38 @@ pub fn take_pending_meeting_notification(app: AppHandle) -> Result<Option<Value>
|
|
|
109
109
|
Ok(pending.take())
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
fn format_time_range_subtitle(
|
|
113
|
+
scheduled_start: Option<&str>,
|
|
114
|
+
scheduled_end: Option<&str>,
|
|
115
|
+
starts_in_secs: i64,
|
|
116
|
+
) -> String {
|
|
117
|
+
let Some(start_str) = scheduled_start else {
|
|
118
|
+
return fallback_starts_subtitle(starts_in_secs);
|
|
119
|
+
};
|
|
120
|
+
let Ok(start) = chrono::DateTime::parse_from_rfc3339(start_str) else {
|
|
121
|
+
return fallback_starts_subtitle(starts_in_secs);
|
|
122
|
+
};
|
|
123
|
+
let local_start = start.with_timezone(&chrono::Local);
|
|
124
|
+
let start_label = local_start.format("%-I:%M %p").to_string();
|
|
125
|
+
if let Some(end_str) = scheduled_end {
|
|
126
|
+
if let Ok(end) = chrono::DateTime::parse_from_rfc3339(end_str) {
|
|
127
|
+
let end_label = end.with_timezone(&chrono::Local).format("%-I:%M %p");
|
|
128
|
+
return format!("{start_label} - {end_label}");
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
start_label
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
fn fallback_starts_subtitle(starts_in_secs: i64) -> String {
|
|
135
|
+
if starts_in_secs <= 0 {
|
|
136
|
+
"Started".to_string()
|
|
137
|
+
} else if starts_in_secs < 90 {
|
|
138
|
+
format!("Starts in {}s", starts_in_secs)
|
|
139
|
+
} else {
|
|
140
|
+
format!("Starts in {} min", (starts_in_secs / 60).max(1))
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
112
144
|
#[tauri::command]
|
|
113
145
|
pub async fn notify_meeting_starting(
|
|
114
146
|
app: AppHandle,
|
|
@@ -116,18 +148,30 @@ pub async fn notify_meeting_starting(
|
|
|
116
148
|
title: String,
|
|
117
149
|
starts_in_secs: i64,
|
|
118
150
|
join_url: Option<String>,
|
|
151
|
+
scheduled_start: Option<String>,
|
|
152
|
+
scheduled_end: Option<String>,
|
|
153
|
+
platform: Option<String>,
|
|
119
154
|
auto_start: Option<bool>,
|
|
155
|
+
notification_type: Option<String>,
|
|
120
156
|
) -> Result<(), String> {
|
|
121
|
-
let
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
157
|
+
let kind = notification_type
|
|
158
|
+
.as_deref()
|
|
159
|
+
.map(str::trim)
|
|
160
|
+
.filter(|s| !s.is_empty())
|
|
161
|
+
.unwrap_or("calendar");
|
|
162
|
+
let is_adhoc = kind == "adhoc";
|
|
163
|
+
let body = if is_adhoc {
|
|
164
|
+
"Take notes?".to_string()
|
|
125
165
|
} else {
|
|
126
|
-
|
|
166
|
+
format_time_range_subtitle(
|
|
167
|
+
scheduled_start.as_deref(),
|
|
168
|
+
scheduled_end.as_deref(),
|
|
169
|
+
starts_in_secs,
|
|
170
|
+
)
|
|
127
171
|
};
|
|
128
|
-
let body = format!("Starts {}", pretty_in);
|
|
129
172
|
dlog!(
|
|
130
|
-
"[clips-tray] notify_meeting_starting id={} title={} body={}",
|
|
173
|
+
"[clips-tray] notify_meeting_starting type={} id={} title={} body={}",
|
|
174
|
+
kind,
|
|
131
175
|
meeting_id,
|
|
132
176
|
title,
|
|
133
177
|
body
|
|
@@ -136,15 +180,23 @@ pub async fn notify_meeting_starting(
|
|
|
136
180
|
// Keep the latest payload available for cold overlay windows, then emit
|
|
137
181
|
// for already-mounted listeners.
|
|
138
182
|
let payload = serde_json::json!({
|
|
139
|
-
"type":
|
|
183
|
+
"type": kind,
|
|
140
184
|
"title": title,
|
|
141
185
|
"subtitle": body,
|
|
142
186
|
"meetingId": meeting_id,
|
|
143
187
|
"joinUrl": join_url,
|
|
188
|
+
"platform": platform,
|
|
189
|
+
"scheduledStart": scheduled_start,
|
|
190
|
+
"scheduledEnd": scheduled_end,
|
|
144
191
|
"autoStart": auto_start.unwrap_or(false),
|
|
145
192
|
});
|
|
146
193
|
store_pending_meeting_notification(&app, &payload);
|
|
147
194
|
let _ = app.emit("meetings:show-notification", payload.clone());
|
|
148
195
|
|
|
196
|
+
// Ensure the overlay window exists / is visible for cold starts.
|
|
197
|
+
if let Err(err) = show_meeting_notification_window(&app) {
|
|
198
|
+
eprintln!("[clips-tray] show meeting notification failed: {err}");
|
|
199
|
+
}
|
|
200
|
+
|
|
149
201
|
Ok(())
|
|
150
202
|
}
|
|
@@ -341,7 +341,7 @@ fn install_call_ended_watcher(app: &AppHandle, threshold_ms: u64) {
|
|
|
341
341
|
fired = false;
|
|
342
342
|
continue;
|
|
343
343
|
}
|
|
344
|
-
let front = frontmost_bundle_id();
|
|
344
|
+
let front = crate::util::frontmost_bundle_id();
|
|
345
345
|
let is_strong_vc = front
|
|
346
346
|
.as_ref()
|
|
347
347
|
.map(|b| strong_vc_bundles.iter().any(|k| k == b))
|
|
@@ -409,26 +409,3 @@ fn audio_recently_silent(state: &tauri::State<'_, DetectorState>, threshold_ms:
|
|
|
409
409
|
mic_silent && system_silent
|
|
410
410
|
}
|
|
411
411
|
|
|
412
|
-
#[cfg(target_os = "macos")]
|
|
413
|
-
fn frontmost_bundle_id() -> Option<String> {
|
|
414
|
-
// Lightweight shell-out to avoid extra crates: AppleScript via `osascript`
|
|
415
|
-
// returns the bundle id of the frontmost app. This avoids pulling in a
|
|
416
|
-
// full objc2 dependency just for one read.
|
|
417
|
-
use std::process::Command;
|
|
418
|
-
let out = Command::new("osascript")
|
|
419
|
-
.args([
|
|
420
|
-
"-e",
|
|
421
|
-
"tell application \"System Events\" to get bundle identifier of (first process whose frontmost is true)",
|
|
422
|
-
])
|
|
423
|
-
.output()
|
|
424
|
-
.ok()?;
|
|
425
|
-
if !out.status.success() {
|
|
426
|
-
return None;
|
|
427
|
-
}
|
|
428
|
-
let s = String::from_utf8(out.stdout).ok()?.trim().to_string();
|
|
429
|
-
if s.is_empty() {
|
|
430
|
-
None
|
|
431
|
-
} else {
|
|
432
|
-
Some(s)
|
|
433
|
-
}
|
|
434
|
-
}
|
|
@@ -5,7 +5,7 @@ use crate::state::{
|
|
|
5
5
|
DictationActive, PopoverShownAt, RecordingActive, TrayAnchor, VoiceWakePopover,
|
|
6
6
|
};
|
|
7
7
|
|
|
8
|
-
const POPOVER_SHADOW_GUTTER_LOGICAL: f64 =
|
|
8
|
+
const POPOVER_SHADOW_GUTTER_LOGICAL: f64 = 24.0;
|
|
9
9
|
const POPOVER_DEFAULT_WIDTH_LOGICAL: f64 = 360.0;
|
|
10
10
|
const POPOVER_DEFAULT_HEIGHT_LOGICAL: f64 = 520.0;
|
|
11
11
|
|
|
@@ -425,6 +425,41 @@ pub fn is_recording_active(app: &AppHandle) -> bool {
|
|
|
425
425
|
.unwrap_or(false)
|
|
426
426
|
}
|
|
427
427
|
|
|
428
|
+
pub fn is_meeting_active(app: &AppHandle) -> bool {
|
|
429
|
+
use crate::state::MeetingActive;
|
|
430
|
+
app.try_state::<MeetingActive>()
|
|
431
|
+
.and_then(|s| s.0.lock().ok().map(|g| *g))
|
|
432
|
+
.unwrap_or(false)
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/// Bundle id of the frontmost macOS app, or `None` on failure / non-macOS.
|
|
436
|
+
/// Uses a lightweight `osascript` shell-out so callers don't need objc2.
|
|
437
|
+
#[cfg(target_os = "macos")]
|
|
438
|
+
pub fn frontmost_bundle_id() -> Option<String> {
|
|
439
|
+
use std::process::Command;
|
|
440
|
+
let out = Command::new("osascript")
|
|
441
|
+
.args([
|
|
442
|
+
"-e",
|
|
443
|
+
"tell application \"System Events\" to get bundle identifier of (first process whose frontmost is true)",
|
|
444
|
+
])
|
|
445
|
+
.output()
|
|
446
|
+
.ok()?;
|
|
447
|
+
if !out.status.success() {
|
|
448
|
+
return None;
|
|
449
|
+
}
|
|
450
|
+
let s = String::from_utf8(out.stdout).ok()?.trim().to_string();
|
|
451
|
+
if s.is_empty() {
|
|
452
|
+
None
|
|
453
|
+
} else {
|
|
454
|
+
Some(s)
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
#[cfg(not(target_os = "macos"))]
|
|
459
|
+
pub fn frontmost_bundle_id() -> Option<String> {
|
|
460
|
+
None
|
|
461
|
+
}
|
|
462
|
+
|
|
428
463
|
pub fn set_dictation_active(app: &AppHandle, active: bool) {
|
|
429
464
|
if let Some(state) = app.try_state::<DictationActive>() {
|
|
430
465
|
if let Ok(mut g) = state.0.lock() {
|
|
@@ -71,7 +71,6 @@
|
|
|
71
71
|
"@types/node": "^24.2.1",
|
|
72
72
|
"@types/react": "catalog:",
|
|
73
73
|
"@types/react-dom": "catalog:",
|
|
74
|
-
"@typescript/native-preview": "catalog:",
|
|
75
74
|
"@xterm/addon-fit": "^0.11.0",
|
|
76
75
|
"@xterm/addon-web-links": "^0.12.0",
|
|
77
76
|
"@xterm/xterm": "^6.0.0",
|
|
@@ -93,7 +92,8 @@
|
|
|
93
92
|
"tailwind-merge": "^3.5.0",
|
|
94
93
|
"tailwindcss": "catalog:",
|
|
95
94
|
"tsx": "^4.20.3",
|
|
96
|
-
"typescript": "
|
|
95
|
+
"typescript": "catalog:",
|
|
96
|
+
"typescript-7": "catalog:",
|
|
97
97
|
"vaul": "^1.1.2",
|
|
98
98
|
"vite": "catalog:",
|
|
99
99
|
"vitest": "catalog:"
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { getUserSetting } from "@agent-native/core/settings";
|
|
2
|
+
import { assertAccess } from "@agent-native/core/sharing";
|
|
3
|
+
import { and, eq, isNull } from "drizzle-orm";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
|
|
6
|
+
import { getDb, schema } from "../server/db/index.js";
|
|
7
|
+
|
|
8
|
+
export const PERSONAL_DATABASE_VIEW_OVERRIDES_VERSION = 1;
|
|
9
|
+
|
|
10
|
+
export const personalDatabaseViewSettingKey = (databaseId: string) =>
|
|
11
|
+
`content-database-personal-view:${databaseId}`;
|
|
12
|
+
|
|
13
|
+
export const sortSchema = z.object({
|
|
14
|
+
key: z.string(),
|
|
15
|
+
label: z.string(),
|
|
16
|
+
direction: z.enum(["asc", "desc"]),
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
export const filterSchema = z.object({
|
|
20
|
+
key: z.string(),
|
|
21
|
+
label: z.string(),
|
|
22
|
+
operator: z.enum([
|
|
23
|
+
"contains",
|
|
24
|
+
"equals",
|
|
25
|
+
"does_not_equal",
|
|
26
|
+
"greater_than",
|
|
27
|
+
"less_than",
|
|
28
|
+
"before",
|
|
29
|
+
"after",
|
|
30
|
+
"between",
|
|
31
|
+
"is_checked",
|
|
32
|
+
"is_unchecked",
|
|
33
|
+
"is_empty",
|
|
34
|
+
"is_not_empty",
|
|
35
|
+
]),
|
|
36
|
+
value: z.string(),
|
|
37
|
+
filterGroupId: z.string().optional(),
|
|
38
|
+
parentFilterGroupId: z.string().optional(),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
export const personalViewOverridesSchema = z.object({
|
|
42
|
+
version: z.literal(PERSONAL_DATABASE_VIEW_OVERRIDES_VERSION),
|
|
43
|
+
activeViewId: z.string().optional(),
|
|
44
|
+
views: z.array(
|
|
45
|
+
z.object({
|
|
46
|
+
id: z.string(),
|
|
47
|
+
sorts: z.array(sortSchema).default([]),
|
|
48
|
+
filters: z.array(filterSchema).default([]),
|
|
49
|
+
filterMode: z.enum(["and", "or"]).default("and"),
|
|
50
|
+
}),
|
|
51
|
+
),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
export async function assertContentDatabaseViewerAccess(databaseId: string) {
|
|
55
|
+
const db = getDb();
|
|
56
|
+
const [database] = await db
|
|
57
|
+
.select()
|
|
58
|
+
.from(schema.contentDatabases)
|
|
59
|
+
.where(
|
|
60
|
+
and(
|
|
61
|
+
eq(schema.contentDatabases.id, databaseId),
|
|
62
|
+
isNull(schema.contentDatabases.deletedAt),
|
|
63
|
+
),
|
|
64
|
+
);
|
|
65
|
+
if (!database) throw new Error(`Database "${databaseId}" not found`);
|
|
66
|
+
|
|
67
|
+
await assertAccess("document", database.documentId, "viewer");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function readPersonalDatabaseViewOverrides(
|
|
71
|
+
userEmail: string,
|
|
72
|
+
databaseId: string,
|
|
73
|
+
) {
|
|
74
|
+
const stored = await getUserSetting(
|
|
75
|
+
userEmail,
|
|
76
|
+
personalDatabaseViewSettingKey(databaseId),
|
|
77
|
+
);
|
|
78
|
+
const parsed = personalViewOverridesSchema.safeParse(stored);
|
|
79
|
+
return parsed.success ? parsed.data : null;
|
|
80
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { defineAction } from "@agent-native/core";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
assertContentDatabaseViewerAccess,
|
|
6
|
+
readPersonalDatabaseViewOverrides,
|
|
7
|
+
} from "./_content-database-personal-view.js";
|
|
8
|
+
|
|
9
|
+
export default defineAction({
|
|
10
|
+
description:
|
|
11
|
+
"Get the current user's personal saved filter, sort, and active view overrides for a content database.",
|
|
12
|
+
schema: z.object({
|
|
13
|
+
databaseId: z.string().describe("Database ID"),
|
|
14
|
+
}),
|
|
15
|
+
http: { method: "GET" },
|
|
16
|
+
run: async ({ databaseId }, ctx) => {
|
|
17
|
+
if (!ctx?.userEmail) throw new Error("Not authenticated.");
|
|
18
|
+
await assertContentDatabaseViewerAccess(databaseId);
|
|
19
|
+
return {
|
|
20
|
+
databaseId,
|
|
21
|
+
overrides: await readPersonalDatabaseViewOverrides(
|
|
22
|
+
ctx.userEmail,
|
|
23
|
+
databaseId,
|
|
24
|
+
),
|
|
25
|
+
};
|
|
26
|
+
},
|
|
27
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { defineAction } from "@agent-native/core";
|
|
2
|
+
import { deleteUserSetting, putUserSetting } from "@agent-native/core/settings";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
assertContentDatabaseViewerAccess,
|
|
7
|
+
personalDatabaseViewSettingKey,
|
|
8
|
+
personalViewOverridesSchema,
|
|
9
|
+
} from "./_content-database-personal-view.js";
|
|
10
|
+
|
|
11
|
+
export default defineAction({
|
|
12
|
+
description:
|
|
13
|
+
"Update or clear the current user's personal saved filter, sort, and active view overrides for a content database.",
|
|
14
|
+
schema: z.object({
|
|
15
|
+
databaseId: z.string().describe("Database ID"),
|
|
16
|
+
overrides: personalViewOverridesSchema.nullable(),
|
|
17
|
+
}),
|
|
18
|
+
run: async ({ databaseId, overrides }, ctx) => {
|
|
19
|
+
if (!ctx?.userEmail) throw new Error("Not authenticated.");
|
|
20
|
+
await assertContentDatabaseViewerAccess(databaseId);
|
|
21
|
+
|
|
22
|
+
const key = personalDatabaseViewSettingKey(databaseId);
|
|
23
|
+
if (overrides) {
|
|
24
|
+
await putUserSetting(ctx.userEmail, key, overrides);
|
|
25
|
+
} else {
|
|
26
|
+
await deleteUserSetting(ctx.userEmail, key);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return { databaseId, overrides };
|
|
30
|
+
},
|
|
31
|
+
});
|
|
@@ -25,12 +25,15 @@ const filterSchema = z.object({
|
|
|
25
25
|
"less_than",
|
|
26
26
|
"before",
|
|
27
27
|
"after",
|
|
28
|
+
"between",
|
|
28
29
|
"is_checked",
|
|
29
30
|
"is_unchecked",
|
|
30
31
|
"is_empty",
|
|
31
32
|
"is_not_empty",
|
|
32
33
|
]),
|
|
33
34
|
value: z.string(),
|
|
35
|
+
filterGroupId: z.string().optional(),
|
|
36
|
+
parentFilterGroupId: z.string().optional(),
|
|
34
37
|
});
|
|
35
38
|
|
|
36
39
|
const columnCalculationSchema = z.enum([
|
|
@@ -440,7 +440,7 @@ function personInitials(value: string) {
|
|
|
440
440
|
);
|
|
441
441
|
}
|
|
442
442
|
|
|
443
|
-
function PersonPill({ value }: { value: string }) {
|
|
443
|
+
export function PersonPill({ value }: { value: string }) {
|
|
444
444
|
return (
|
|
445
445
|
<span className="inline-flex max-w-full items-center gap-1.5 rounded bg-muted px-1.5 py-0.5 text-xs font-medium text-foreground">
|
|
446
446
|
<span className="flex size-4 shrink-0 items-center justify-center rounded-full bg-background text-[9px] font-semibold text-muted-foreground">
|