@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
|
@@ -1286,6 +1286,9 @@ export function App() {
|
|
|
1286
1286
|
if (e.key === "Escape") {
|
|
1287
1287
|
// Don't close mid-recording — user would lose the recorder handle.
|
|
1288
1288
|
if (isRecording) return;
|
|
1289
|
+
// Reset nested views before hide so the next tray open lands on the
|
|
1290
|
+
// main recorder UI instead of resuming scrolled settings/meetings.
|
|
1291
|
+
setPopoverView("recorder");
|
|
1289
1292
|
hidePopover();
|
|
1290
1293
|
}
|
|
1291
1294
|
}
|
|
@@ -1328,7 +1331,11 @@ export function App() {
|
|
|
1328
1331
|
track(
|
|
1329
1332
|
listen<boolean>("clips:popover-visible", (ev) => {
|
|
1330
1333
|
console.log("[clips-popover] popover-visible =", ev.payload);
|
|
1331
|
-
|
|
1334
|
+
const visible = !!ev.payload;
|
|
1335
|
+
setPopoverVisible(visible);
|
|
1336
|
+
// Leaving settings/meetings/dictation mid-scroll should not resume on
|
|
1337
|
+
// the next open — always return to the main recorder surface.
|
|
1338
|
+
if (!visible) setPopoverView("recorder");
|
|
1332
1339
|
}),
|
|
1333
1340
|
);
|
|
1334
1341
|
// The bubble window emits `clips:bubble-closed` when the user clicks
|
|
@@ -1415,6 +1422,10 @@ export function App() {
|
|
|
1415
1422
|
// tracks (which causes the laggy / black / silently-failing recording
|
|
1416
1423
|
// symptoms). Reset to false once the recording is fully torn down.
|
|
1417
1424
|
const bubbleStreamTransferredToRecorder = useRef(false);
|
|
1425
|
+
// Bumped when the native stop path releases the camera mid-session so the
|
|
1426
|
+
// bubble effect re-acquires even if bubbleActive/cameraId are unchanged
|
|
1427
|
+
// (post-stop reopen with a blank "Default Camera" preview).
|
|
1428
|
+
const [bubbleSessionEpoch, setBubbleSessionEpoch] = useState(0);
|
|
1418
1429
|
const wantsCamera = mode !== "screen" && cameraOn;
|
|
1419
1430
|
const nativeFullscreenRecordingActive =
|
|
1420
1431
|
mode !== "camera" && shouldUseNativeFullscreenRecording(source);
|
|
@@ -1623,14 +1634,21 @@ export function App() {
|
|
|
1623
1634
|
invoke("hide_overlays").catch(() => {});
|
|
1624
1635
|
}
|
|
1625
1636
|
};
|
|
1626
|
-
}, [bubbleActive, cameraId]);
|
|
1637
|
+
}, [bubbleActive, cameraId, bubbleSessionEpoch]);
|
|
1627
1638
|
|
|
1628
1639
|
useEffect(() => {
|
|
1629
1640
|
let unlisten: (() => void) | null = null;
|
|
1630
1641
|
let cancelled = false;
|
|
1631
1642
|
listen("clips:release-camera", () => {
|
|
1632
1643
|
console.log(`[popover] releasing camera`);
|
|
1644
|
+
// Native stop closes the bubble and kills tracks while React may still
|
|
1645
|
+
// hold a live RecorderHandle during finalize/upload. Clear ownership so
|
|
1646
|
+
// the bubble session can re-acquire a fresh preview, and so Start is not
|
|
1647
|
+
// stuck on an ended stream.
|
|
1648
|
+
bubbleStreamTransferredToRecorder.current = false;
|
|
1633
1649
|
bubbleStreamRef.current?.getTracks().forEach((t) => t.stop());
|
|
1650
|
+
bubbleStreamRef.current = null;
|
|
1651
|
+
setBubbleSessionEpoch((epoch) => epoch + 1);
|
|
1634
1652
|
})
|
|
1635
1653
|
.then((u) => {
|
|
1636
1654
|
if (cancelled) u();
|
|
@@ -1643,6 +1661,24 @@ export function App() {
|
|
|
1643
1661
|
};
|
|
1644
1662
|
}, []);
|
|
1645
1663
|
|
|
1664
|
+
// If the popover reopens while camera is still wanted, and the previous
|
|
1665
|
+
// bubble stream is gone or all tracks have ended (common after native stop
|
|
1666
|
+
// + mid-upload reopen), bump the session epoch so the bubble effect
|
|
1667
|
+
// re-acquires getUserMedia + WebRTC instead of showing a blank "Default
|
|
1668
|
+
// Camera" label with a silent Start.
|
|
1669
|
+
useEffect(() => {
|
|
1670
|
+
if (!popoverVisible || !wantsCamera) return;
|
|
1671
|
+
if (recordingFlowGateRef.current || recordingFlowActive) return;
|
|
1672
|
+
const tracks = bubbleStreamRef.current?.getTracks() ?? [];
|
|
1673
|
+
const needsFreshBubble =
|
|
1674
|
+
tracks.length === 0 ||
|
|
1675
|
+
tracks.every((track) => track.readyState === "ended");
|
|
1676
|
+
if (!needsFreshBubble) return;
|
|
1677
|
+
bubbleStreamTransferredToRecorder.current = false;
|
|
1678
|
+
bubbleStreamRef.current = null;
|
|
1679
|
+
setBubbleSessionEpoch((epoch) => epoch + 1);
|
|
1680
|
+
}, [popoverVisible, wantsCamera, recordingFlowActive]);
|
|
1681
|
+
|
|
1646
1682
|
// ---- auto-size popover to content --------------------------------------
|
|
1647
1683
|
// The Tauri window is fixed-size via tauri.conf.json, but our content
|
|
1648
1684
|
// height varies (more rows when a camera is on, Recent list toggle, etc.).
|
|
@@ -1859,7 +1895,25 @@ export function App() {
|
|
|
1859
1895
|
async function handleStartRecording(options?: {
|
|
1860
1896
|
ignoreActiveRecorder?: boolean;
|
|
1861
1897
|
}) {
|
|
1862
|
-
if (recorder && !options?.ignoreActiveRecorder)
|
|
1898
|
+
if (recorder && !options?.ignoreActiveRecorder) {
|
|
1899
|
+
console.warn(
|
|
1900
|
+
"[clips-popover] handleStartRecording ignored — recorder already active",
|
|
1901
|
+
);
|
|
1902
|
+
setRecError(
|
|
1903
|
+
"Still finishing the last recording. Wait a moment, then try again.",
|
|
1904
|
+
);
|
|
1905
|
+
return;
|
|
1906
|
+
}
|
|
1907
|
+
const bubbleTracks = bubbleStreamRef.current?.getTracks() ?? [];
|
|
1908
|
+
const bubbleStreamDead =
|
|
1909
|
+
bubbleTracks.length > 0 &&
|
|
1910
|
+
bubbleTracks.every((track) => track.readyState === "ended");
|
|
1911
|
+
if (bubbleStreamDead) {
|
|
1912
|
+
console.warn("[clips-popover] clearing ended bubble stream before start");
|
|
1913
|
+
bubbleStreamTransferredToRecorder.current = false;
|
|
1914
|
+
bubbleStreamRef.current = null;
|
|
1915
|
+
setBubbleSessionEpoch((epoch) => epoch + 1);
|
|
1916
|
+
}
|
|
1863
1917
|
if (localRecordingMode === "off") {
|
|
1864
1918
|
if (videoStorageStatus === "checking") {
|
|
1865
1919
|
setRecError("Checking video storage. Try again in a moment.");
|
|
@@ -2202,11 +2256,28 @@ export function App() {
|
|
|
2202
2256
|
};
|
|
2203
2257
|
track(
|
|
2204
2258
|
listen("clips:recorder-stop", async () => {
|
|
2259
|
+
// Detach the React Start/bubble gate immediately. Native stop already
|
|
2260
|
+
// released the camera and cleared Rust `is_recording_active` mid-
|
|
2261
|
+
// finalize; keeping `recorder` set through the whole upload made
|
|
2262
|
+
// reopen show a blank preview and made Start a silent no-op.
|
|
2263
|
+
const handle = recorder;
|
|
2264
|
+
bubbleStreamTransferredToRecorder.current = false;
|
|
2265
|
+
bubbleStreamRef.current = null;
|
|
2266
|
+
recordingFlowGateRef.current = false;
|
|
2267
|
+
(window as unknown as { clipsForceAlive?: boolean }).clipsForceAlive =
|
|
2268
|
+
false;
|
|
2269
|
+
setRecordingFlowActive(false);
|
|
2270
|
+
setRecorder(null);
|
|
2271
|
+
// Force a fresh bubble session even when bubbleActive stays true
|
|
2272
|
+
// (popover still open / camera still on). Without this epoch bump the
|
|
2273
|
+
// effect does not re-run and reopen shows ended tracks until Start
|
|
2274
|
+
// discovers them.
|
|
2275
|
+
setBubbleSessionEpoch((epoch) => epoch + 1);
|
|
2276
|
+
|
|
2205
2277
|
let stopFailed = false;
|
|
2206
2278
|
let stopResult: RecorderStopResult | null = null;
|
|
2207
2279
|
try {
|
|
2208
|
-
stopResult = await
|
|
2209
|
-
if (cancelled) return;
|
|
2280
|
+
stopResult = await handle.stop();
|
|
2210
2281
|
if (stopResult.localOnly) {
|
|
2211
2282
|
setLocalRecordingNotice({
|
|
2212
2283
|
folderPath: stopResult.localFolder,
|
|
@@ -2217,36 +2288,20 @@ export function App() {
|
|
|
2217
2288
|
}
|
|
2218
2289
|
} catch (err) {
|
|
2219
2290
|
stopFailed = true;
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
await loadPendingUploads();
|
|
2223
|
-
}
|
|
2291
|
+
setRecError(err instanceof Error ? err.message : String(err));
|
|
2292
|
+
await loadPendingUploads();
|
|
2224
2293
|
} finally {
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
//
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
setRecorder(null);
|
|
2237
|
-
setRecordingFlowActive(false);
|
|
2238
|
-
invoke("set_recording_state", { active: false }).catch(() => {});
|
|
2239
|
-
if (stopFailed || stopResult?.localOnly) {
|
|
2240
|
-
invoke("show_popover").catch(() => {});
|
|
2241
|
-
} else {
|
|
2242
|
-
// Close the popover — recorder.stop() already opened the
|
|
2243
|
-
// recording's page in the default browser. The popover doesn't
|
|
2244
|
-
// need to hang around.
|
|
2245
|
-
getCurrentWindow()
|
|
2246
|
-
.hide()
|
|
2247
|
-
.catch(() => {});
|
|
2248
|
-
emit("clips:popover-visible", false).catch(() => {});
|
|
2249
|
-
}
|
|
2294
|
+
invoke("set_recording_state", { active: false }).catch(() => {});
|
|
2295
|
+
if (stopFailed || stopResult?.localOnly) {
|
|
2296
|
+
invoke("show_popover").catch(() => {});
|
|
2297
|
+
} else {
|
|
2298
|
+
// Close the popover — recorder.stop() already opened the
|
|
2299
|
+
// recording's page in the default browser. The popover doesn't
|
|
2300
|
+
// need to hang around.
|
|
2301
|
+
getCurrentWindow()
|
|
2302
|
+
.hide()
|
|
2303
|
+
.catch(() => {});
|
|
2304
|
+
emit("clips:popover-visible", false).catch(() => {});
|
|
2250
2305
|
}
|
|
2251
2306
|
}
|
|
2252
2307
|
}),
|
|
@@ -2265,6 +2320,7 @@ export function App() {
|
|
|
2265
2320
|
recordingFlowGateRef.current = false;
|
|
2266
2321
|
setRecorder(null);
|
|
2267
2322
|
setRecordingFlowActive(false);
|
|
2323
|
+
setBubbleSessionEpoch((epoch) => epoch + 1);
|
|
2268
2324
|
invoke("set_recording_state", { active: false }).catch(() => {});
|
|
2269
2325
|
invoke("show_popover").catch(() => {});
|
|
2270
2326
|
}
|
|
@@ -2285,6 +2341,7 @@ export function App() {
|
|
|
2285
2341
|
recordingFlowGateRef.current = false;
|
|
2286
2342
|
setRecorder(null);
|
|
2287
2343
|
setRecordingFlowActive(false);
|
|
2344
|
+
setBubbleSessionEpoch((epoch) => epoch + 1);
|
|
2288
2345
|
invoke("set_recording_state", { active: false }).catch(() => {});
|
|
2289
2346
|
// Starting a new browser capture must come from a fresh click in
|
|
2290
2347
|
// this webview. The toolbar click arrives here through async Tauri
|
|
@@ -363,12 +363,19 @@ export function useMeetingTranscription({
|
|
|
363
363
|
};
|
|
364
364
|
|
|
365
365
|
// Resume the engine that initial start settled on (no fallback here —
|
|
366
|
-
// the engine choice was already made below).
|
|
366
|
+
// the engine choice was already made below). Keep voice processing
|
|
367
|
+
// off: AEC/ducking on the shared mic can make Zoom/Meet callers hear
|
|
368
|
+
// the local user at a whisper while Clips is transcribed.
|
|
367
369
|
const startAudio = async () => {
|
|
368
|
-
await restartTranscriptionEngine(
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
370
|
+
await restartTranscriptionEngine(
|
|
371
|
+
session.engine,
|
|
372
|
+
{
|
|
373
|
+
deviceId: selectedMicId,
|
|
374
|
+
label: selectedMicLabel,
|
|
375
|
+
},
|
|
376
|
+
true,
|
|
377
|
+
false,
|
|
378
|
+
);
|
|
372
379
|
};
|
|
373
380
|
|
|
374
381
|
// Pause/resume state machine — see app.tsx for full explanation.
|
|
@@ -538,6 +545,9 @@ export function useMeetingTranscription({
|
|
|
538
545
|
|
|
539
546
|
session.engine = await startTranscriptionEngine({
|
|
540
547
|
mic: { deviceId: selectedMicId, label: selectedMicLabel },
|
|
548
|
+
// Match clip recordings: VoiceProcessingIO AEC/ducking on the shared
|
|
549
|
+
// mic can tank live meeting volume for remote participants.
|
|
550
|
+
voiceProcessing: false,
|
|
541
551
|
});
|
|
542
552
|
|
|
543
553
|
await invoke("silence_detector_start", {
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Meeting reminder visibility policy (Granola-aligned).
|
|
3
|
+
*
|
|
4
|
+
* Show the overlay from 1 minute before start through 5 minutes after start,
|
|
5
|
+
* unless the user dismisses it or takes an action.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export const MEETING_NOTIFY_LEAD_MS = 60_000;
|
|
9
|
+
export const MEETING_NOTIFY_HOLD_AFTER_START_MS = 5 * 60_000;
|
|
10
|
+
|
|
11
|
+
/** True when `now` is inside the show window for a meeting that starts at `startMs`. */
|
|
12
|
+
export function isMeetingNotificationWindowOpen(
|
|
13
|
+
startMs: number,
|
|
14
|
+
nowMs: number = Date.now(),
|
|
15
|
+
): boolean {
|
|
16
|
+
if (!Number.isFinite(startMs)) return false;
|
|
17
|
+
const earliest = startMs - MEETING_NOTIFY_LEAD_MS;
|
|
18
|
+
const latest = startMs + MEETING_NOTIFY_HOLD_AFTER_START_MS;
|
|
19
|
+
return nowMs >= earliest && nowMs <= latest;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Milliseconds until the overlay should auto-hide, assuming it is currently
|
|
24
|
+
* visible. Returns 0 when the window has already closed.
|
|
25
|
+
*/
|
|
26
|
+
export function meetingNotificationAutoHideMs(
|
|
27
|
+
startMs: number,
|
|
28
|
+
nowMs: number = Date.now(),
|
|
29
|
+
): number {
|
|
30
|
+
if (!Number.isFinite(startMs)) return 0;
|
|
31
|
+
const latest = startMs + MEETING_NOTIFY_HOLD_AFTER_START_MS;
|
|
32
|
+
return Math.max(0, latest - nowMs);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type MeetingJoinProvider = "zoom" | "meet" | "teams" | "webex" | "other";
|
|
36
|
+
|
|
37
|
+
export function detectMeetingJoinProvider(
|
|
38
|
+
joinUrl: string | null | undefined,
|
|
39
|
+
platform?: string | null,
|
|
40
|
+
): MeetingJoinProvider {
|
|
41
|
+
const haystack = `${platform ?? ""} ${joinUrl ?? ""}`.toLowerCase();
|
|
42
|
+
if (haystack.includes("zoom")) return "zoom";
|
|
43
|
+
if (haystack.includes("meet.google") || /\bmeet\b/.test(haystack))
|
|
44
|
+
return "meet";
|
|
45
|
+
if (haystack.includes("teams.microsoft") || haystack.includes("teams"))
|
|
46
|
+
return "teams";
|
|
47
|
+
if (haystack.includes("webex")) return "webex";
|
|
48
|
+
return "other";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function joinProviderLabel(provider: MeetingJoinProvider): string {
|
|
52
|
+
switch (provider) {
|
|
53
|
+
case "zoom":
|
|
54
|
+
return "Zoom";
|
|
55
|
+
case "meet":
|
|
56
|
+
return "Meet";
|
|
57
|
+
case "teams":
|
|
58
|
+
return "Teams";
|
|
59
|
+
case "webex":
|
|
60
|
+
return "Webex";
|
|
61
|
+
default:
|
|
62
|
+
return "meeting";
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Format a local time range like "9:00 AM - 9:30 AM". */
|
|
67
|
+
export function formatMeetingTimeRange(
|
|
68
|
+
startIso: string | null | undefined,
|
|
69
|
+
endIso: string | null | undefined,
|
|
70
|
+
locale?: string,
|
|
71
|
+
): string | null {
|
|
72
|
+
if (!startIso) return null;
|
|
73
|
+
const start = Date.parse(startIso);
|
|
74
|
+
if (!Number.isFinite(start)) return null;
|
|
75
|
+
const end = endIso ? Date.parse(endIso) : NaN;
|
|
76
|
+
const fmt = new Intl.DateTimeFormat(locale, {
|
|
77
|
+
hour: "numeric",
|
|
78
|
+
minute: "2-digit",
|
|
79
|
+
});
|
|
80
|
+
const startLabel = fmt.format(new Date(start));
|
|
81
|
+
if (!Number.isFinite(end)) return startLabel;
|
|
82
|
+
return `${startLabel} - ${fmt.format(new Date(end))}`;
|
|
83
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hosted native start sequencing: overlap Whisper start, create-recording, and
|
|
3
|
+
* deferred SCK warm so Skip no longer waits serially on Whisper then warm.
|
|
4
|
+
* Begin/attach must still wait for this promise (transcription settled).
|
|
5
|
+
*/
|
|
6
|
+
export function planNativeFullscreenWarmOverlap<
|
|
7
|
+
TRecording extends { id: string },
|
|
8
|
+
>(input: {
|
|
9
|
+
createRecording: () => Promise<TRecording>;
|
|
10
|
+
startTranscription: () => Promise<unknown>;
|
|
11
|
+
warmMic: (recordingId: string) => Promise<unknown>;
|
|
12
|
+
}): Promise<TRecording> {
|
|
13
|
+
const transcriptionPromise = input.startTranscription();
|
|
14
|
+
return (async () => {
|
|
15
|
+
const created = await input.createRecording();
|
|
16
|
+
// Deferred-output warm may overlap remaining Whisper startup; frames are
|
|
17
|
+
// not written until begin attaches the recording output.
|
|
18
|
+
await Promise.all([transcriptionPromise, input.warmMic(created.id)]);
|
|
19
|
+
return created;
|
|
20
|
+
})();
|
|
21
|
+
}
|
|
@@ -69,6 +69,7 @@ import {
|
|
|
69
69
|
getAudioStreamWithFallback,
|
|
70
70
|
getCameraStreamWithFallback,
|
|
71
71
|
} from "./media-capture-constraints";
|
|
72
|
+
import { planNativeFullscreenWarmOverlap } from "./native-recording-warm";
|
|
72
73
|
import { buildCaptureTitle, type CaptureTitleResult } from "./recording-title";
|
|
73
74
|
import {
|
|
74
75
|
startTranscriptionCapture,
|
|
@@ -77,6 +78,7 @@ import {
|
|
|
77
78
|
} from "./transcription-capture";
|
|
78
79
|
|
|
79
80
|
export type { LocalExportedFile } from "./local-export";
|
|
81
|
+
export { planNativeFullscreenWarmOverlap } from "./native-recording-warm";
|
|
80
82
|
|
|
81
83
|
export type CaptureMode = "screen" | "screen-camera" | "camera";
|
|
82
84
|
export type CaptureSource = "full-screen" | "window" | "region";
|
|
@@ -1677,6 +1679,11 @@ async function openNativeUploadUrl(
|
|
|
1677
1679
|
}
|
|
1678
1680
|
}
|
|
1679
1681
|
|
|
1682
|
+
/**
|
|
1683
|
+
* Hosted native start sequencing helper: overlap Whisper start, create-recording,
|
|
1684
|
+
* and deferred SCK warm so Skip no longer waits serially on Whisper then warm.
|
|
1685
|
+
* `begin` / attach still waits for transcription to settle first.
|
|
1686
|
+
*/
|
|
1680
1687
|
function abortCreatedRecordingOnCountdownCancel(
|
|
1681
1688
|
err: unknown,
|
|
1682
1689
|
recordingPromise: Promise<{ id: string }>,
|
|
@@ -1847,19 +1854,42 @@ async function startNativeFullscreenRecording(
|
|
|
1847
1854
|
}).catch((err) => {
|
|
1848
1855
|
console.warn("[clips-recorder] mic warm failed:", err);
|
|
1849
1856
|
});
|
|
1857
|
+
const clickStartedAt = Date.now();
|
|
1850
1858
|
if (localOnly) {
|
|
1859
|
+
// Local recordings have no create-recording round-trip; still overlap
|
|
1860
|
+
// Whisper startup with countdown + deferred SCK warm.
|
|
1851
1861
|
const countdownPromise = runRecordingCountdown(true);
|
|
1852
1862
|
id = localFolderName;
|
|
1853
|
-
|
|
1863
|
+
const transcriptionPromise = startNativeTranscriptionBeforeRecording();
|
|
1864
|
+
const warmPromise = (async () => {
|
|
1865
|
+
const warmStartedAt = Date.now();
|
|
1866
|
+
await warmMic(id);
|
|
1867
|
+
console.log(
|
|
1868
|
+
`[clips-recorder] native warm durations: warmMs=${Date.now() - warmStartedAt}`,
|
|
1869
|
+
);
|
|
1870
|
+
})();
|
|
1871
|
+
try {
|
|
1872
|
+
await Promise.all([
|
|
1873
|
+
countdownPromise,
|
|
1874
|
+
transcriptionPromise,
|
|
1875
|
+
warmPromise,
|
|
1876
|
+
]);
|
|
1877
|
+
} catch (err) {
|
|
1878
|
+
await transcriptionPromise.catch(() => {});
|
|
1879
|
+
throw err;
|
|
1880
|
+
}
|
|
1854
1881
|
} else {
|
|
1855
1882
|
const captureTitlePromise = captureTitleForRecording({
|
|
1856
1883
|
mode: params.mode,
|
|
1857
1884
|
source: params.source,
|
|
1858
1885
|
});
|
|
1859
1886
|
const countdownPromise = runRecordingCountdown(true);
|
|
1887
|
+
// Kick Whisper off immediately (no recording id needed) so Skip no longer
|
|
1888
|
+
// serializes create → Whisper → SCK warm on the critical path.
|
|
1889
|
+
const transcriptionPromise = startNativeTranscriptionBeforeRecording();
|
|
1860
1890
|
const recordingPromise = (async () => {
|
|
1861
1891
|
const captureTitle = await captureTitlePromise;
|
|
1862
|
-
|
|
1892
|
+
const createStartedAt = Date.now();
|
|
1863
1893
|
try {
|
|
1864
1894
|
return await createServerRecording(
|
|
1865
1895
|
params.serverUrl,
|
|
@@ -1873,26 +1903,47 @@ async function startNativeFullscreenRecording(
|
|
|
1873
1903
|
},
|
|
1874
1904
|
);
|
|
1875
1905
|
} finally {
|
|
1876
|
-
console.
|
|
1906
|
+
console.log(
|
|
1907
|
+
`[clips-recorder] createServerRecording durationMs=${Date.now() - createStartedAt}`,
|
|
1908
|
+
);
|
|
1877
1909
|
}
|
|
1878
1910
|
})();
|
|
1879
|
-
//
|
|
1880
|
-
//
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1911
|
+
// Once create returns an id, warm SCK with deferred_output in parallel
|
|
1912
|
+
// with any remaining Whisper startup. Begin/attach still waits on
|
|
1913
|
+
// transcription settling first (AVAudioEngine after SCK writing can mute
|
|
1914
|
+
// the SCK mic leg).
|
|
1915
|
+
const warmAndId = planNativeFullscreenWarmOverlap({
|
|
1916
|
+
createRecording: async () => {
|
|
1917
|
+
const createRes = await recordingPromise;
|
|
1918
|
+
uploadMode = createRes.uploadMode;
|
|
1919
|
+
id = createRes.id;
|
|
1920
|
+
return createRes;
|
|
1921
|
+
},
|
|
1922
|
+
startTranscription: async () => {
|
|
1923
|
+
const transcriptionStartedAt = Date.now();
|
|
1924
|
+
try {
|
|
1925
|
+
await transcriptionPromise;
|
|
1926
|
+
} finally {
|
|
1927
|
+
console.log(
|
|
1928
|
+
`[clips-recorder] transcription warm durationMs=${Date.now() - transcriptionStartedAt}`,
|
|
1929
|
+
);
|
|
1930
|
+
}
|
|
1931
|
+
},
|
|
1932
|
+
warmMic: async (recordingId) => {
|
|
1933
|
+
const warmStartedAt = Date.now();
|
|
1934
|
+
try {
|
|
1935
|
+
await warmMic(recordingId);
|
|
1936
|
+
} finally {
|
|
1937
|
+
console.log(
|
|
1938
|
+
`[clips-recorder] native warm durationMs=${Date.now() - warmStartedAt}`,
|
|
1939
|
+
);
|
|
1940
|
+
}
|
|
1941
|
+
},
|
|
1942
|
+
});
|
|
1893
1943
|
try {
|
|
1894
|
-
const [,
|
|
1895
|
-
id =
|
|
1944
|
+
const [, createRes] = await Promise.all([countdownPromise, warmAndId]);
|
|
1945
|
+
id = createRes.id;
|
|
1946
|
+
uploadMode = createRes.uploadMode ?? uploadMode;
|
|
1896
1947
|
} catch (err) {
|
|
1897
1948
|
abortCreatedRecordingOnCountdownCancel(
|
|
1898
1949
|
err,
|
|
@@ -1905,11 +1956,16 @@ async function startNativeFullscreenRecording(
|
|
|
1905
1956
|
|
|
1906
1957
|
await audioCue.playBeforeCapture();
|
|
1907
1958
|
// Phase 2: attach the recording output now that the mic is warm (or do a
|
|
1908
|
-
// normal immediate start if warming was skipped/failed).
|
|
1959
|
+
// normal immediate start if warming was skipped/failed). Transcription has
|
|
1960
|
+
// already been awaited above so AVAudioEngine won't reconfigure mid-write.
|
|
1961
|
+
const beginStartedAt = Date.now();
|
|
1909
1962
|
await invoke("native_fullscreen_recording_begin", {
|
|
1910
1963
|
recordingId: id,
|
|
1911
1964
|
...captureAudioParams,
|
|
1912
1965
|
});
|
|
1966
|
+
console.log(
|
|
1967
|
+
`[clips-recorder] native begin durationMs=${Date.now() - beginStartedAt} clickToLiveMs=${Date.now() - clickStartedAt}`,
|
|
1968
|
+
);
|
|
1913
1969
|
await transcriptionCapture?.resetTimeline().catch((err) => {
|
|
1914
1970
|
console.warn(
|
|
1915
1971
|
"[clips-recorder] transcription timeline reset failed:",
|
|
@@ -2021,7 +2077,26 @@ async function startNativeFullscreenRecording(
|
|
|
2021
2077
|
stopPromise = (async () => {
|
|
2022
2078
|
stopped = true;
|
|
2023
2079
|
console.log("[clips-recorder] native full-screen stop requested");
|
|
2080
|
+
// Tear chrome down immediately with finalizing so the live camera bubble /
|
|
2081
|
+
// toolbar don't linger while ScreencaptureKit finalize + upload run.
|
|
2082
|
+
// hide_recording_chrome leaves the bubble; close_bubble destroys it.
|
|
2083
|
+
// Order matters: show finalizing first, and never call hide_overlays here
|
|
2084
|
+
// because that also closes the finalizing window.
|
|
2024
2085
|
if (!localOnly) showFinalizingFeedback();
|
|
2086
|
+
await invoke("hide_recording_chrome").catch((err) =>
|
|
2087
|
+
console.error(
|
|
2088
|
+
"[clips-recorder] immediate hide_recording_chrome after stop failed:",
|
|
2089
|
+
err,
|
|
2090
|
+
),
|
|
2091
|
+
);
|
|
2092
|
+
if (wantsCamera) {
|
|
2093
|
+
await invoke("close_bubble").catch((err) =>
|
|
2094
|
+
console.error(
|
|
2095
|
+
"[clips-recorder] immediate close_bubble after stop failed:",
|
|
2096
|
+
err,
|
|
2097
|
+
),
|
|
2098
|
+
);
|
|
2099
|
+
}
|
|
2025
2100
|
clearSegmentRotator();
|
|
2026
2101
|
if (tickHandle) {
|
|
2027
2102
|
clearInterval(tickHandle);
|
|
@@ -3226,6 +3301,9 @@ async function startRecordingInner(
|
|
|
3226
3301
|
label: params.micLabel,
|
|
3227
3302
|
},
|
|
3228
3303
|
wantsSystemAudio,
|
|
3304
|
+
// Match native path: VoiceProcessingIO AEC/ducking on a shared mic
|
|
3305
|
+
// can tank live call volume and attenuate the recorded mic leg.
|
|
3306
|
+
{ voiceProcessing: false },
|
|
3229
3307
|
)
|
|
3230
3308
|
: null;
|
|
3231
3309
|
// Stop/Cancel can fire during the await above — at that point stop()/cancel()
|