@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.
Files changed (209) hide show
  1. package/blueprints/action/crud.md +1 -1
  2. package/blueprints/channel/discord.md +1 -1
  3. package/blueprints/provider/stripe.md +1 -1
  4. package/blueprints/sandbox/docker.md +1 -1
  5. package/corpus/README.md +1 -1
  6. package/corpus/core/CHANGELOG.md +14 -0
  7. package/corpus/core/blueprints/action/crud.md +1 -1
  8. package/corpus/core/blueprints/channel/discord.md +1 -1
  9. package/corpus/core/blueprints/provider/stripe.md +1 -1
  10. package/corpus/core/blueprints/sandbox/docker.md +1 -1
  11. package/corpus/core/package.json +6 -5
  12. package/corpus/core/src/agent/run-manager.ts +23 -2
  13. package/corpus/core/src/agent/run-store.ts +2 -2
  14. package/corpus/core/src/cli/add.ts +1 -1
  15. package/corpus/core/src/cli/create.ts +8 -3
  16. package/corpus/core/src/cli/index.ts +5 -4
  17. package/corpus/core/src/client/extensions/ExtensionSlot.tsx +7 -0
  18. package/corpus/core/src/client/session-replay.ts +97 -3
  19. package/corpus/core/src/jobs/scheduler.ts +30 -1
  20. package/corpus/core/src/localization/default-messages.ts +21 -0
  21. package/corpus/core/src/templates/default/app/i18n/ar-SA.ts +21 -0
  22. package/corpus/core/src/templates/default/app/i18n/de-DE.ts +21 -0
  23. package/corpus/core/src/templates/default/app/i18n/en-US.ts +21 -0
  24. package/corpus/core/src/templates/default/app/i18n/es-ES.ts +21 -0
  25. package/corpus/core/src/templates/default/app/i18n/fr-FR.ts +21 -0
  26. package/corpus/core/src/templates/default/app/i18n/hi-IN.ts +20 -0
  27. package/corpus/core/src/templates/default/app/i18n/ja-JP.ts +21 -0
  28. package/corpus/core/src/templates/default/app/i18n/ko-KR.ts +20 -0
  29. package/corpus/core/src/templates/default/app/i18n/pt-BR.ts +21 -0
  30. package/corpus/core/src/templates/default/app/i18n/zh-CN.ts +20 -0
  31. package/corpus/core/src/templates/default/app/i18n/zh-TW.ts +19 -0
  32. package/corpus/core/src/templates/default/package.json +2 -2
  33. package/corpus/core/src/templates/headless/package.json +3 -3
  34. package/corpus/core/src/templates/workspace-core/package.json +6 -6
  35. package/corpus/core/src/templates/workspace-root/package.json +2 -2
  36. package/corpus/core/src/triggers/dispatcher.ts +30 -2
  37. package/corpus/core/src/vite/client.ts +96 -1
  38. package/corpus/templates/analytics/.agents/skills/session-replay/SKILL.md +21 -2
  39. package/corpus/templates/analytics/app/global.css +26 -10
  40. package/corpus/templates/analytics/app/i18n/zh-TW.ts +2 -0
  41. package/corpus/templates/analytics/app/i18n-data.ts +25 -0
  42. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +548 -237
  43. package/corpus/templates/analytics/app/pages/sessions/SessionDevToolsPanel.tsx +285 -294
  44. package/corpus/templates/analytics/changelog/2026-07-08-console-details-expand-inline-under-each-row-and-the-event-t.md +6 -0
  45. package/corpus/templates/analytics/changelog/2026-07-08-session-replay-clamps-ultra-wide-viewports-while-keeping-the.md +6 -0
  46. package/corpus/templates/analytics/changelog/2026-07-08-session-replay-keeps-a-usable-stage-when-dev-tools-is-open-a.md +6 -0
  47. package/corpus/templates/analytics/changelog/2026-07-08-session-replay-resumes-after-scrubbing-and-expands-console-r.md +6 -0
  48. package/corpus/templates/analytics/changelog/2026-07-08-session-replays-wait-for-full-load-keep-a-normal-viewport-an.md +6 -0
  49. package/corpus/templates/analytics/package.json +2 -2
  50. package/corpus/templates/assets/package.json +2 -2
  51. package/corpus/templates/brain/package.json +2 -2
  52. package/corpus/templates/calendar/AGENTS.md +10 -0
  53. package/corpus/templates/calendar/actions/create-event.ts +5 -1
  54. package/corpus/templates/calendar/actions/event-action-helpers.ts +41 -0
  55. package/corpus/templates/calendar/actions/get-attendee-timezones.ts +28 -0
  56. package/corpus/templates/calendar/actions/set-attendee-timezone.ts +68 -0
  57. package/corpus/templates/calendar/app/components/calendar/EventAttendeesSection.tsx +150 -11
  58. package/corpus/templates/calendar/app/components/calendar/EventDetailPanel.tsx +36 -1
  59. package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +32 -0
  60. package/corpus/templates/calendar/app/hooks/use-attendee-timezones.ts +24 -0
  61. package/corpus/templates/calendar/app/i18n/zh-TW.ts +2 -0
  62. package/corpus/templates/calendar/app/i18n-data.ts +20 -0
  63. package/corpus/templates/calendar/app/lib/attendee-local-time.ts +122 -0
  64. package/corpus/templates/calendar/changelog/2026-07-08-event-details-now-support-extension-widgets-and-you-can-see-.md +6 -0
  65. package/corpus/templates/calendar/changelog/2026-07-08-when-you-invite-guests-to-a-new-event-you-now-appear-in.md +6 -0
  66. package/corpus/templates/calendar/package.json +2 -2
  67. package/corpus/templates/calendar/server/handlers/events.ts +5 -1
  68. package/corpus/templates/calendar/shared/api.ts +6 -0
  69. package/corpus/templates/chat/package.json +2 -2
  70. package/corpus/templates/clips/.agents/skills/meetings/SKILL.md +6 -2
  71. package/corpus/templates/clips/actions/create-meeting.ts +8 -3
  72. package/corpus/templates/clips/actions/list-meetings.ts +31 -3
  73. package/corpus/templates/clips/changelog/2026-07-08-after-finishing-a-desktop-recording-reopening-clips-restor.md +6 -0
  74. package/corpus/templates/clips/changelog/2026-07-08-clips-now-pops-up-to-take-notes-when-you-join-an-adhoc-zoom-.md +6 -0
  75. package/corpus/templates/clips/changelog/2026-07-08-desktop-clip-recordings-are-louder-after-capture-mic-only.md +6 -0
  76. package/corpus/templates/clips/changelog/2026-07-08-meeting-reminders-now-appear-1-minute-before-start-stay-unti.md +6 -0
  77. package/corpus/templates/clips/changelog/2026-07-08-the-desktop-meetings-panel-has-room-for-its-drop-shadow-and-.md +6 -0
  78. package/corpus/templates/clips/chrome-extension/package.json +2 -2
  79. package/corpus/templates/clips/desktop/package.json +2 -2
  80. package/corpus/templates/clips/desktop/src/app.tsx +91 -34
  81. package/corpus/templates/clips/desktop/src/hooks/useMeetingTranscription.ts +15 -5
  82. package/corpus/templates/clips/desktop/src/lib/meeting-notification-timing.ts +83 -0
  83. package/corpus/templates/clips/desktop/src/lib/native-recording-warm.ts +21 -0
  84. package/corpus/templates/clips/desktop/src/lib/recorder.ts +98 -20
  85. package/corpus/templates/clips/desktop/src/overlays/meeting-notification.tsx +174 -50
  86. package/corpus/templates/clips/desktop/src/styles.css +128 -53
  87. package/corpus/templates/clips/desktop/src-tauri/src/adhoc_meetings_watcher.rs +364 -0
  88. package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +1 -1
  89. package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +5 -0
  90. package/corpus/templates/clips/desktop/src-tauri/src/meetings_watcher.rs +91 -11
  91. package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +69 -19
  92. package/corpus/templates/clips/desktop/src-tauri/src/native_speech.rs +36 -1
  93. package/corpus/templates/clips/desktop/src-tauri/src/notifications.rs +62 -10
  94. package/corpus/templates/clips/desktop/src-tauri/src/silence_detector.rs +1 -24
  95. package/corpus/templates/clips/desktop/src-tauri/src/util.rs +36 -1
  96. package/corpus/templates/clips/package.json +2 -2
  97. package/corpus/templates/content/actions/_content-database-personal-view.ts +80 -0
  98. package/corpus/templates/content/actions/get-content-database-personal-view.ts +27 -0
  99. package/corpus/templates/content/actions/update-content-database-personal-view.ts +31 -0
  100. package/corpus/templates/content/actions/update-content-database-view.ts +3 -0
  101. package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +1 -1
  102. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +2584 -546
  103. package/corpus/templates/content/app/components/editor/database/filter-sort.ts +220 -18
  104. package/corpus/templates/content/app/hooks/use-content-database.ts +32 -0
  105. package/corpus/templates/content/app/i18n/zh-TW.ts +16 -0
  106. package/corpus/templates/content/app/i18n-data.ts +154 -0
  107. package/corpus/templates/content/changelog/2026-07-08-database-filters-now-support-notion-style-reset-save-for-eve.md +6 -0
  108. package/corpus/templates/content/changelog/2026-07-08-date-filters-can-now-target-a-between-range.md +6 -0
  109. package/corpus/templates/content/package.json +2 -2
  110. package/corpus/templates/content/parity/matrix.md +1 -1
  111. package/corpus/templates/content/parity/matrix.ts +2 -0
  112. package/corpus/templates/content/shared/api.ts +24 -0
  113. package/corpus/templates/design/package.json +2 -2
  114. package/corpus/templates/dispatch/app/i18n/zh-TW.ts +34 -1
  115. package/corpus/templates/dispatch/app/i18n-data.ts +387 -10
  116. package/corpus/templates/dispatch/app/routes/automations.tsx +1 -0
  117. package/corpus/templates/dispatch/changelog/2026-07-08-added-an-automations-page-to-review-pause-and-create.md +6 -0
  118. package/corpus/templates/dispatch/changelog/2026-07-08-destinations-now-shows-outbound-delivery-queue-health.md +6 -0
  119. package/corpus/templates/dispatch/package.json +2 -2
  120. package/corpus/templates/forms/package.json +2 -2
  121. package/corpus/templates/macros/package.json +2 -2
  122. package/corpus/templates/mail/app/hooks/use-emails.ts +56 -44
  123. package/corpus/templates/mail/app/lib/gmail-mutation-queue.ts +325 -0
  124. package/corpus/templates/mail/changelog/2026-07-08-rapid-archive-and-mark-read-now-batch-gmail-updates-so-the-i.md +6 -0
  125. package/corpus/templates/mail/package.json +2 -2
  126. package/corpus/templates/plan/package.json +2 -2
  127. package/corpus/templates/slides/app/components/editor/ImageDropPromptPopover.tsx +46 -29
  128. package/corpus/templates/slides/app/context/DeckContext.tsx +45 -18
  129. package/corpus/templates/slides/app/i18n/ar-SA.ts +4 -0
  130. package/corpus/templates/slides/app/i18n/de-DE.ts +4 -0
  131. package/corpus/templates/slides/app/i18n/en-US.ts +4 -0
  132. package/corpus/templates/slides/app/i18n/es-ES.ts +4 -0
  133. package/corpus/templates/slides/app/i18n/fr-FR.ts +4 -0
  134. package/corpus/templates/slides/app/i18n/hi-IN.ts +4 -0
  135. package/corpus/templates/slides/app/i18n/ja-JP.ts +4 -0
  136. package/corpus/templates/slides/app/i18n/ko-KR.ts +4 -0
  137. package/corpus/templates/slides/app/i18n/pt-BR.ts +4 -0
  138. package/corpus/templates/slides/app/i18n/zh-CN.ts +4 -0
  139. package/corpus/templates/slides/app/i18n/zh-TW.ts +4 -0
  140. package/corpus/templates/slides/app/lib/image-drop-to-agent.ts +115 -0
  141. package/corpus/templates/slides/app/pages/DeckEditor.tsx +22 -13
  142. package/corpus/templates/slides/changelog/2026-07-08-dropping-an-image-onto-a-slide-and-sending-it-to-the-agent-w.md +6 -0
  143. package/corpus/templates/slides/changelog/2026-07-08-undo-works-after-the-agent-edits-a-slide-so-you-can-reverse.md +6 -0
  144. package/corpus/templates/slides/package.json +2 -2
  145. package/dist/agent/run-manager.d.ts.map +1 -1
  146. package/dist/agent/run-manager.js +23 -2
  147. package/dist/agent/run-manager.js.map +1 -1
  148. package/dist/agent/run-store.js +2 -2
  149. package/dist/agent/run-store.js.map +1 -1
  150. package/dist/catalog.json +2 -2
  151. package/dist/cli/add.js +1 -1
  152. package/dist/cli/add.js.map +1 -1
  153. package/dist/cli/create.d.ts.map +1 -1
  154. package/dist/cli/create.js +8 -3
  155. package/dist/cli/create.js.map +1 -1
  156. package/dist/cli/index.js +5 -4
  157. package/dist/cli/index.js.map +1 -1
  158. package/dist/client/extensions/ExtensionSlot.js +6 -0
  159. package/dist/client/extensions/ExtensionSlot.js.map +1 -1
  160. package/dist/client/session-replay.d.ts +8 -0
  161. package/dist/client/session-replay.d.ts.map +1 -1
  162. package/dist/client/session-replay.js +80 -3
  163. package/dist/client/session-replay.js.map +1 -1
  164. package/dist/collab/struct-routes.d.ts +1 -1
  165. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  166. package/dist/jobs/scheduler.js +26 -1
  167. package/dist/jobs/scheduler.js.map +1 -1
  168. package/dist/localization/default-messages.d.ts +19 -0
  169. package/dist/localization/default-messages.d.ts.map +1 -1
  170. package/dist/localization/default-messages.js +19 -0
  171. package/dist/localization/default-messages.js.map +1 -1
  172. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  173. package/dist/resources/handlers.d.ts +2 -2
  174. package/dist/templates/default/app/i18n/ar-SA.ts +21 -0
  175. package/dist/templates/default/app/i18n/de-DE.ts +21 -0
  176. package/dist/templates/default/app/i18n/en-US.ts +21 -0
  177. package/dist/templates/default/app/i18n/es-ES.ts +21 -0
  178. package/dist/templates/default/app/i18n/fr-FR.ts +21 -0
  179. package/dist/templates/default/app/i18n/hi-IN.ts +20 -0
  180. package/dist/templates/default/app/i18n/ja-JP.ts +21 -0
  181. package/dist/templates/default/app/i18n/ko-KR.ts +20 -0
  182. package/dist/templates/default/app/i18n/pt-BR.ts +21 -0
  183. package/dist/templates/default/app/i18n/zh-CN.ts +20 -0
  184. package/dist/templates/default/app/i18n/zh-TW.ts +19 -0
  185. package/dist/templates/default/package.json +2 -2
  186. package/dist/templates/headless/package.json +3 -3
  187. package/dist/templates/workspace-core/package.json +6 -6
  188. package/dist/templates/workspace-root/package.json +2 -2
  189. package/dist/triggers/dispatcher.js +27 -2
  190. package/dist/triggers/dispatcher.js.map +1 -1
  191. package/dist/vite/client.d.ts.map +1 -1
  192. package/dist/vite/client.js +70 -1
  193. package/dist/vite/client.js.map +1 -1
  194. package/package.json +7 -6
  195. package/src/templates/default/app/i18n/ar-SA.ts +21 -0
  196. package/src/templates/default/app/i18n/de-DE.ts +21 -0
  197. package/src/templates/default/app/i18n/en-US.ts +21 -0
  198. package/src/templates/default/app/i18n/es-ES.ts +21 -0
  199. package/src/templates/default/app/i18n/fr-FR.ts +21 -0
  200. package/src/templates/default/app/i18n/hi-IN.ts +20 -0
  201. package/src/templates/default/app/i18n/ja-JP.ts +21 -0
  202. package/src/templates/default/app/i18n/ko-KR.ts +20 -0
  203. package/src/templates/default/app/i18n/pt-BR.ts +21 -0
  204. package/src/templates/default/app/i18n/zh-CN.ts +20 -0
  205. package/src/templates/default/app/i18n/zh-TW.ts +19 -0
  206. package/src/templates/default/package.json +2 -2
  207. package/src/templates/headless/package.json +3 -3
  208. package/src/templates/workspace-core/package.json +6 -6
  209. 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
- setPopoverVisible(!!ev.payload);
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) return;
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 recorder.stop();
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
- if (!cancelled) {
2221
- setRecError(err instanceof Error ? err.message : String(err));
2222
- await loadPendingUploads();
2223
- }
2291
+ setRecError(err instanceof Error ? err.message : String(err));
2292
+ await loadPendingUploads();
2224
2293
  } finally {
2225
- if (!cancelled) {
2226
- // Clear the force-alive flag — recording is done, the pump
2227
- // can honor document.hidden normally again.
2228
- (
2229
- window as unknown as { clipsForceAlive?: boolean }
2230
- ).clipsForceAlive = false;
2231
- // Recorder has stopped its tracks; next popover session can
2232
- // acquire the camera cleanly again.
2233
- bubbleStreamTransferredToRecorder.current = false;
2234
- bubbleStreamRef.current = null;
2235
- recordingFlowGateRef.current = false;
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(session.engine, {
369
- deviceId: selectedMicId,
370
- label: selectedMicLabel,
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
- await Promise.all([countdownPromise, warmMic(id)]);
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
- console.time("[clips-recorder] createServerRecording duration");
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.timeEnd("[clips-recorder] createServerRecording duration");
1906
+ console.log(
1907
+ `[clips-recorder] createServerRecording durationMs=${Date.now() - createStartedAt}`,
1908
+ );
1877
1909
  }
1878
1910
  })();
1879
- // The recording id usually lands well before the countdown ends — warm
1880
- // native capture as soon as it does so setup overlaps the 3-2-1.
1881
- const warmAndId = (async () => {
1882
- const createRes = await recordingPromise;
1883
- uploadMode = createRes.uploadMode;
1884
- id = createRes.id;
1885
- // Start the live transcription mic tap before SCK attaches its
1886
- // recording output. On macOS, opening AVAudioEngine after SCK has
1887
- // started writing can reconfigure the shared input device and leave
1888
- // SCK's microphone leg near-silent while Whisper still hears audio.
1889
- await startNativeTranscriptionBeforeRecording();
1890
- await warmMic(createRes.id);
1891
- return createRes.id;
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 [, warmedId] = await Promise.all([countdownPromise, warmAndId]);
1895
- id = warmedId;
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()