@agent-native/core 0.175.4 → 0.175.5-nightly-20260827014436

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 (202) hide show
  1. package/corpus/templates/analytics/actions/get-explorer-dashboard.ts +1 -0
  2. package/corpus/templates/analytics/app/components/auth/AuthProvider.tsx +8 -6
  3. package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +32 -0
  4. package/corpus/templates/analytics/app/components/layout/Layout.tsx +1 -1
  5. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +66 -7
  6. package/corpus/templates/analytics/app/lib/prefetch-keys.ts +10 -0
  7. package/corpus/templates/analytics/app/pages/adhoc/explorer-dashboard/index.tsx +9 -7
  8. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +10 -11
  9. package/corpus/templates/analytics/changelog/2026-08-24-feature-flag-changes-recover-from-temporary-workspace-direct.md +6 -0
  10. package/corpus/templates/analytics/changelog/2026-08-26-analytics-distinguishes-daily-from-weekly-activity-shows-sta.md +6 -0
  11. package/corpus/templates/analytics/changelog/2026-08-26-dashboard-metadata-now-shows-the-original-creator-separately.md +6 -0
  12. package/corpus/templates/analytics/changelog/2026-08-26-dashboard-names-stay-editable-when-the-analytics-sidebar-ref.md +6 -0
  13. package/corpus/templates/analytics/changelog/2026-08-26-fixed-the-agent-re-asking-dashboard-scope-questions-you-had-.md +6 -0
  14. package/corpus/templates/analytics/server/db/schema.ts +2 -0
  15. package/corpus/templates/analytics/server/lib/agent-readable-resource-context.ts +8 -2
  16. package/corpus/templates/analytics/server/lib/dashboards-store.ts +5 -0
  17. package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +6 -4
  18. package/corpus/templates/analytics/server/lib/migrate-analytics-artifacts.ts +19 -1
  19. package/corpus/templates/analytics/server/lib/workspace-feature-flags.ts +40 -9
  20. package/corpus/templates/analytics/server/plugins/db.ts +10 -0
  21. package/corpus/templates/analytics/server/routes/api/dashboard-agent-context.json.get.ts +1 -0
  22. package/corpus/templates/analytics/shared/feature-flags.ts +11 -1
  23. package/corpus/templates/calendar/app/components/calendar/GoogleConnectBanner.tsx +2 -9
  24. package/corpus/templates/clips/.agents/skills/ai-video-tools/SKILL.md +8 -7
  25. package/corpus/templates/clips/.agents/skills/recording/SKILL.md +9 -8
  26. package/corpus/templates/clips/actions/import-loom-recording.ts +1 -1
  27. package/corpus/templates/clips/actions/lib/loom-import-job.ts +52 -35
  28. package/corpus/templates/clips/actions/request-transcript.ts +6 -183
  29. package/corpus/templates/clips/app/components/clips-avatar.tsx +46 -0
  30. package/corpus/templates/clips/app/components/library/recording-card.tsx +8 -7
  31. package/corpus/templates/clips/app/components/library/space-card.tsx +8 -5
  32. package/corpus/templates/clips/app/components/meetings/attendee-stack.tsx +7 -8
  33. package/corpus/templates/clips/app/components/meetings/transcript-bubbles.tsx +19 -12
  34. package/corpus/templates/clips/app/components/player/transcript-panel.tsx +128 -94
  35. package/corpus/templates/clips/app/components/recorder/microphone-visualizer.tsx +25 -104
  36. package/corpus/templates/clips/app/components/workspace/members-list.tsx +8 -6
  37. package/corpus/templates/clips/app/components/workspace/notifications-list.tsx +8 -6
  38. package/corpus/templates/clips/app/components/workspace/top-creators-table.tsx +8 -6
  39. package/corpus/templates/clips/app/routes/_app.meetings.$meetingId.tsx +8 -7
  40. package/corpus/templates/clips/app/routes/_app.settings._index.tsx +0 -26
  41. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +1 -4
  42. package/corpus/templates/clips/changelog/2026-08-26-detected-calls-no-longer-duplicate-meetings-or-strand-the-prompt.md +6 -0
  43. package/corpus/templates/clips/desktop/src/app.tsx +8 -10
  44. package/corpus/templates/clips/desktop/src/components/live-waveform.tsx +99 -0
  45. package/corpus/templates/clips/desktop/src/hooks/useMeetingTranscription.ts +248 -55
  46. package/corpus/templates/clips/desktop/src/lib/agent-steps.ts +477 -0
  47. package/corpus/templates/clips/desktop/src/lib/ask-sheet-layout.ts +71 -0
  48. package/corpus/templates/clips/desktop/src/lib/meeting-ask.ts +297 -0
  49. package/corpus/templates/clips/desktop/src/lib/url.ts +24 -0
  50. package/corpus/templates/clips/desktop/src/overlays/ask-steps.tsx +155 -0
  51. package/corpus/templates/clips/desktop/src/overlays/flow-bar.tsx +7 -81
  52. package/corpus/templates/clips/desktop/src/overlays/live-transcript.tsx +29 -2
  53. package/corpus/templates/clips/desktop/src/overlays/meeting-notification.tsx +80 -22
  54. package/corpus/templates/clips/desktop/src/overlays/record-pill.tsx +13 -53
  55. package/corpus/templates/clips/desktop/src/overlays/recording-pill.tsx +1017 -82
  56. package/corpus/templates/clips/desktop/src/styles.css +796 -57
  57. package/corpus/templates/clips/desktop/src/tailwind.css +23 -2
  58. package/corpus/templates/clips/desktop/src-tauri/src/adhoc_meetings_watcher.rs +1765 -219
  59. package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +6 -0
  60. package/corpus/templates/clips/desktop/src-tauri/src/meetings_watcher.rs +45 -30
  61. package/corpus/templates/clips/desktop/src-tauri/src/native_screen/custom_capture.rs +5 -3
  62. package/corpus/templates/clips/desktop/src-tauri/src/notifications.rs +91 -0
  63. package/corpus/templates/clips/desktop/src-tauri/src/recording_indicator.rs +123 -27
  64. package/corpus/templates/clips/server/routes/_agent-native/google/callback.get.ts +16 -0
  65. package/corpus/templates/clips/shared/audio-meter.ts +145 -0
  66. package/corpus/templates/clips/shared/clips-ai-prefs.ts +0 -1
  67. package/corpus/templates/clips/shared/live-waveform.tsx +119 -0
  68. package/corpus/templates/content/app/components/editor/CommentsSidebar.tsx +44 -22
  69. package/corpus/templates/content/app/components/ui/avatar.tsx +1 -0
  70. package/corpus/templates/design/app/components/visual-editor/ReviewCanvasPins.tsx +4 -1
  71. package/corpus/templates/design/app/hooks/use-question-flow.ts +9 -2
  72. package/corpus/templates/design/app/pages/DesignSystemSetup.tsx +111 -37
  73. package/corpus/templates/design/changelog/2026-08-26-design-no-longer-asks-users-to-confirm-guided-questions-they-have-already-answered.md +6 -0
  74. package/corpus/templates/dispatch/changelog/2026-08-26-signing-out-goes-straight-to-the-sign-in-page.md +5 -0
  75. package/corpus/templates/dispatch/server/lib/org-apps-directory.ts +96 -47
  76. package/corpus/templates/dispatch/server/plugins/org-apps-directory.ts +73 -29
  77. package/corpus/templates/factory/.env.example +6 -2
  78. package/corpus/templates/factory/AGENTS.md +3 -3
  79. package/corpus/templates/factory/README.md +4 -2
  80. package/corpus/templates/factory/server/connectors/credentials.ts +70 -4
  81. package/corpus/templates/mail/actions/get-email.ts +68 -21
  82. package/corpus/templates/mail/actions/get-thread.ts +92 -51
  83. package/corpus/templates/mail/app/components/GoogleConnectBanner.tsx +2 -9
  84. package/corpus/templates/mail/app/hooks/use-emails.ts +7 -4
  85. package/corpus/templates/mail/server/lib/mail-connector-catalog.ts +7 -4
  86. package/corpus/templates/plan/app/pages/PlansPage.tsx +23 -15
  87. package/corpus/templates/slides/actions/add-deck.ts +20 -0
  88. package/corpus/templates/slides/actions/create-deck.ts +17 -7
  89. package/corpus/templates/slides/actions/delete-deck.ts +45 -5
  90. package/corpus/templates/slides/actions/get-deck.ts +77 -11
  91. package/corpus/templates/slides/actions/patch-deck.ts +14 -2
  92. package/corpus/templates/slides/actions/save-deck.ts +20 -0
  93. package/corpus/templates/slides/app/components/comments/SlideCommentsPanel.tsx +18 -7
  94. package/corpus/templates/slides/app/components/design-system/DesignSystemSetup.tsx +38 -14
  95. package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +2 -2
  96. package/corpus/templates/slides/app/components/ui/avatar.tsx +1 -0
  97. package/corpus/templates/slides/server/handlers/decks.ts +10 -0
  98. package/corpus/templates/slides/shared/slide-ids.ts +133 -0
  99. package/corpus/templates/tasks/DEVELOPING.md +8 -0
  100. package/corpus/templates/tasks/drizzle.config.ts +3 -1
  101. package/corpus/templates/tasks/drizzle.local.config.ts +6 -0
  102. package/corpus/templates/tasks/package.json +2 -1
  103. package/corpus/templates/tasks/server/db/migrations/meta/0000_snapshot.json +234 -0
  104. package/corpus/templates/tasks/server/db/migrations/meta/_journal.json +13 -0
  105. package/corpus/templates/tasks/server/db/migrations.ts +110 -0
  106. package/corpus/templates/tasks/server/plugins/db.ts +3 -87
  107. package/dist/client/AssistantChat.js +3 -10
  108. package/dist/client/guided-questions.d.ts +11 -1
  109. package/dist/client/guided-questions.js +24 -7
  110. package/dist/client/hooks/index.d.ts +2 -1
  111. package/dist/client/hooks/index.js +2 -1
  112. package/dist/client/org/OrgSwitcher.js +3 -17
  113. package/dist/client/review/ReviewThreadPanel.js +4 -2
  114. package/dist/client/sign-out.d.ts +15 -0
  115. package/dist/client/sign-out.js +82 -0
  116. package/dist/client/use-action.d.ts +2 -0
  117. package/dist/client/use-action.js +4 -2
  118. package/dist/client/use-session.d.ts +23 -1
  119. package/dist/client/use-session.js +46 -0
  120. package/dist/collab/client.js +20 -5
  121. package/dist/collab/presence.d.ts +3 -1
  122. package/dist/collab/presence.js +48 -45
  123. package/dist/collab/routes.d.ts +1 -1
  124. package/dist/db/drizzle-config.d.ts +7 -0
  125. package/dist/db/drizzle-config.js +35 -9
  126. package/dist/db/drizzle-migrations.d.ts +28 -0
  127. package/dist/db/drizzle-migrations.js +81 -0
  128. package/dist/db/index.d.ts +1 -1
  129. package/dist/db/migrations.d.ts +13 -1
  130. package/dist/db/migrations.js +60 -31
  131. package/dist/db/schema.js +4 -1
  132. package/dist/deploy/build.d.ts +6 -0
  133. package/dist/deploy/build.js +41 -0
  134. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  135. package/dist/mcp/index.d.ts +1 -1
  136. package/dist/mcp/index.js +1 -1
  137. package/dist/mcp/org-directory.d.ts +19 -7
  138. package/dist/mcp/org-directory.js +92 -68
  139. package/dist/mcp/screen-memory-stdio.d.ts +7 -7
  140. package/dist/notifications/routes.d.ts +3 -3
  141. package/dist/org/context.d.ts +6 -0
  142. package/dist/org/context.js +22 -0
  143. package/dist/org/index.d.ts +1 -1
  144. package/dist/org/index.js +1 -1
  145. package/dist/resources/store.d.ts +12 -0
  146. package/dist/resources/store.js +55 -0
  147. package/dist/server/agent-discovery.d.ts +18 -2
  148. package/dist/server/agent-discovery.js +191 -22
  149. package/dist/server/auth.js +3 -2
  150. package/dist/server/builder-browser.d.ts +0 -4
  151. package/dist/server/builder-browser.js +23 -8
  152. package/dist/server/google-oauth.d.ts +18 -0
  153. package/dist/server/google-oauth.js +3 -3
  154. package/dist/server/onboarding-html.js +3 -3
  155. package/dist/server/poll.d.ts +4 -2
  156. package/dist/server/poll.js +8 -3
  157. package/dist/server/realtime-token.d.ts +1 -1
  158. package/dist/server/short-lived-token.d.ts +1 -1
  159. package/dist/server/short-lived-token.js +1 -1
  160. package/dist/shared/environment-lanes.d.ts +1 -0
  161. package/dist/shared/environment-lanes.js +1 -0
  162. package/dist/shared/google-profile-image.d.ts +2 -0
  163. package/dist/shared/google-profile-image.js +24 -0
  164. package/dist/shared/index.d.ts +1 -0
  165. package/dist/shared/index.js +1 -0
  166. package/dist/styles/agent-conversation.css +13 -0
  167. package/docs/content/creating-templates.mdx +10 -1
  168. package/docs/content/server-database.mdx +49 -3
  169. package/docs/content/server-plugins.mdx +39 -3
  170. package/docs/content/webmcp.mdx +2 -0
  171. package/package.json +2 -1
  172. package/corpus/templates/analytics/data/explorer-configs/_autosave.json +0 -18
  173. package/corpus/templates/analytics/data/explorer-configs/steve-chart.json +0 -18
  174. package/corpus/templates/analytics/data/explorer-dashboards/dashboard-1773260837897.json +0 -10
  175. package/corpus/templates/analytics/data/sync-config.json +0 -8
  176. package/corpus/templates/brain/data/sync-config.json +0 -1
  177. package/corpus/templates/calendar/data/availability.json +0 -32
  178. package/corpus/templates/calendar/data/settings.json +0 -6
  179. package/corpus/templates/calendar/data/sync-config.json +0 -10
  180. package/corpus/templates/chat/data/sync-config.json +0 -1
  181. package/corpus/templates/clips/data/sync-config.json +0 -1
  182. package/corpus/templates/clips/desktop/src/lib/audio-meter.ts +0 -80
  183. package/corpus/templates/clips/desktop/src/overlays/live-audio-bars.tsx +0 -113
  184. package/corpus/templates/design/data/sync-config.json +0 -1
  185. package/corpus/templates/factory/data/sync-config.json +0 -1
  186. package/corpus/templates/forms/data/settings.json +0 -8
  187. package/corpus/templates/forms/data/sync-config.json +0 -3
  188. package/corpus/templates/macros/data/sync-config.json +0 -4
  189. package/corpus/templates/mail/data/emails.defaults.json +0 -1
  190. package/corpus/templates/mail/data/labels.defaults.json +0 -59
  191. package/corpus/templates/mail/data/settings.defaults.json +0 -9
  192. package/corpus/templates/mail/data/sync-config.json +0 -8
  193. package/corpus/templates/slides/data/sync-config.json +0 -8
  194. package/corpus/templates/tasks/data/sync-config.json +0 -1
  195. package/dist/templates/chat/data/.gitkeep +0 -0
  196. package/dist/templates/chat/data/sync-config.json +0 -1
  197. package/dist/templates/default/data/.gitkeep +0 -0
  198. package/dist/templates/default/data/sync-config.json +0 -1
  199. package/src/templates/chat/data/.gitkeep +0 -0
  200. package/src/templates/chat/data/sync-config.json +0 -1
  201. package/src/templates/default/data/.gitkeep +0 -0
  202. package/src/templates/default/data/sync-config.json +0 -1
@@ -34,7 +34,10 @@ export interface MeetingTranscriptionPayload {
34
34
 
35
35
  interface MeetingTranscriptionSession {
36
36
  meetingId: string;
37
- recordingId: string;
37
+ /** Null until `start-meeting-recording` answers. Live audio starts in
38
+ * parallel with that call, so early lines buffer until there is a row to
39
+ * save them to — see the flush guard. */
40
+ recordingId: string | null;
38
41
  lines: TranscriptLine[];
39
42
  unlisten: Array<() => void>;
40
43
  flushTimer: ReturnType<typeof setTimeout> | null;
@@ -98,7 +101,10 @@ export function useMeetingTranscription({
98
101
  const pendingPillInitRef = useRef<{
99
102
  meetingId: string;
100
103
  initialNotes: string;
104
+ title?: string;
101
105
  preloadedLines?: PillTranscriptLine[];
106
+ /** The pill is on screen but capture has not attached yet. */
107
+ starting?: boolean;
102
108
  } | null>(null);
103
109
 
104
110
  const normalizedServerUrl = useMemo(
@@ -119,6 +125,9 @@ export function useMeetingTranscription({
119
125
  const flushTranscript = useCallback(async (): Promise<void> => {
120
126
  const session = sessionRef.current;
121
127
  if (!session) return;
128
+ // No row yet: lines keep accumulating and the flush that follows the row's
129
+ // arrival writes all of them. Saving against a null id would 400.
130
+ if (!session.recordingId) return;
122
131
  session.dirtySeq = session.flushSeq + 1;
123
132
  if (session.flushInFlight) {
124
133
  // A flush is already outstanding — wait for it (and any chained
@@ -273,7 +282,7 @@ export function useMeetingTranscription({
273
282
  // Start
274
283
  // -------------------------------------------------------------------------
275
284
 
276
- const startTranscription = useCallback(
285
+ const runStartTranscription = useCallback(
277
286
  async (payload: MeetingTranscriptionPayload) => {
278
287
  const meetingId = payload.meetingId;
279
288
  if (!meetingId) return;
@@ -299,6 +308,42 @@ export function useMeetingTranscription({
299
308
  await stopTranscription("replaced");
300
309
  }
301
310
 
311
+ // Put the pill on screen before any of the work behind it — the row
312
+ // creation, the ScreenCaptureKit query, the engine start. Every one of
313
+ // those used to run with nothing on screen, which is the whole of the
314
+ // delay between the click and the note taker appearing.
315
+ //
316
+ // It shows as `starting`, not recording: the indicator, the tray state,
317
+ // and the meeting-active flag still wait for capture to actually attach,
318
+ // so a failed start never leaves a pill claiming it recorded anything.
319
+ pendingPillInitRef.current = {
320
+ meetingId,
321
+ initialNotes: "",
322
+ starting: true,
323
+ };
324
+ invoke("recording_pill_show", { meetingId, mode: "meeting" }).catch(
325
+ () => {},
326
+ );
327
+ emit("clips:pill-context", {
328
+ meetingId,
329
+ mode: "meeting",
330
+ starting: true,
331
+ }).catch(() => {});
332
+ // The pill has taken over the feedback; the dialog has nothing left to
333
+ // report and used to sit there for the whole startup.
334
+ emit("meetings:hide-notification", { meetingId }).catch(() => {});
335
+
336
+ // Held so a failure downstream can tear the engine back down. Two
337
+ // variables because startup keeps going after the engine is up: while it
338
+ // is still coming up only the promise exists, and once it has resolved
339
+ // the promise is no longer the thing to stop. A failure at any point
340
+ // after this must close whichever one is live, or the microphone stays
341
+ // open for a meeting that never started.
342
+ let engineStarting: Promise<TranscriptionEngine> | null = null;
343
+ let liveEngine: TranscriptionEngine | null = null;
344
+ // Held outside the try so the failure path can tell "my session" from
345
+ // "whatever session is current now", which a newer start may own.
346
+ let startedSession: MeetingTranscriptionSession | null = null;
302
347
  let historyPreparedRef: {
303
348
  current: {
304
349
  token: string;
@@ -341,27 +386,9 @@ export function useMeetingTranscription({
341
386
  });
342
387
  }
343
388
 
344
- const result = await callClipsAction<{
345
- meetingId?: string;
346
- scheduledEnd?: string | null;
347
- recording?: { id?: string | null } | null;
348
- }>("start-meeting-recording", { meetingId });
349
- const resolvedMeetingId = result.meetingId ?? meetingId;
350
- const recordingId = result.recording?.id;
351
- if (!recordingId) {
352
- throw new Error("Could not create a transcript session.");
353
- }
354
-
355
- const parsedScheduledEndMs = result.scheduledEnd
356
- ? Date.parse(result.scheduledEnd)
357
- : Number.NaN;
358
- const scheduledEndMs = Number.isFinite(parsedScheduledEndMs)
359
- ? parsedScheduledEndMs
360
- : null;
361
-
362
389
  const session: MeetingTranscriptionSession = {
363
- meetingId: resolvedMeetingId,
364
- recordingId,
390
+ meetingId,
391
+ recordingId: null,
365
392
  lines: [],
366
393
  unlisten: [],
367
394
  flushTimer: null,
@@ -375,6 +402,7 @@ export function useMeetingTranscription({
375
402
  dirtySeq: 0,
376
403
  };
377
404
  sessionRef.current = session;
405
+ startedSession = session;
378
406
 
379
407
  const scheduleFlush = () => {
380
408
  if (session.flushTimer) window.clearTimeout(session.flushTimer);
@@ -456,6 +484,52 @@ export function useMeetingTranscription({
456
484
  }),
457
485
  );
458
486
 
487
+ // Creating the row and starting the audio engine need nothing from each
488
+ // other — only the flush needs a recording id. Kicking the engine here
489
+ // takes the network round trip off the critical path; the transcript
490
+ // listeners above are already attached, so a line spoken during the
491
+ // overlap is buffered rather than dropped.
492
+ // Set before capture can produce anything. The final-transcript
493
+ // listener stamps each event with this offset as it arrives, so an
494
+ // event that lands while the offset is still 0 is written on the local
495
+ // clock while its neighbours are written on the meeting's — one
496
+ // transcript on two timelines. Anchoring here costs the engine's warmup
497
+ // in accuracy and is what the pre-parallel code did.
498
+ if (includeFromMeetingStart && payload.scheduledStart) {
499
+ session.liveTimelineOffsetMs = Math.max(
500
+ 0,
501
+ Date.now() - Date.parse(payload.scheduledStart),
502
+ );
503
+ }
504
+ const enginePromise = startTranscriptionEngine({
505
+ mic: { deviceId: selectedMicId, label: selectedMicLabel },
506
+ // macOS 15+ uses ScreenCaptureKit's independent microphone output.
507
+ // Rust upgrades only the legacy/failure fallback to bypassed VPIO so
508
+ // call apps cannot starve Clips of mic buffers or lose call volume.
509
+ voiceProcessing: false,
510
+ });
511
+ engineStarting = enginePromise;
512
+
513
+ const result = await callClipsAction<{
514
+ meetingId?: string;
515
+ scheduledEnd?: string | null;
516
+ recording?: { id?: string | null } | null;
517
+ }>("start-meeting-recording", { meetingId });
518
+ const resolvedMeetingId = result.meetingId ?? meetingId;
519
+ const recordingId = result.recording?.id;
520
+ if (!recordingId) {
521
+ throw new Error("Could not create a transcript session.");
522
+ }
523
+ session.meetingId = resolvedMeetingId;
524
+ session.recordingId = recordingId;
525
+
526
+ const parsedScheduledEndMs = result.scheduledEnd
527
+ ? Date.parse(result.scheduledEnd)
528
+ : Number.NaN;
529
+ const scheduledEndMs = Number.isFinite(parsedScheduledEndMs)
530
+ ? parsedScheduledEndMs
531
+ : null;
532
+
459
533
  const silenceDetectorConfig = {
460
534
  silenceThreshold: 0.05,
461
535
  silenceMs: 15 * 60 * 1000,
@@ -565,10 +639,11 @@ export function useMeetingTranscription({
565
639
  pendingPillInitRef.current = {
566
640
  meetingId: resolvedMeetingId,
567
641
  initialNotes: "",
642
+ starting: true,
568
643
  };
569
644
 
570
645
  callClipsAction<{
571
- meeting?: { userNotesMd?: string };
646
+ meeting?: { userNotesMd?: string; title?: string | null };
572
647
  transcript?: { segmentsJson?: string | null } | null;
573
648
  }>("get-meeting", { id: resolvedMeetingId }, { method: "GET" })
574
649
  .then((data) => {
@@ -578,11 +653,28 @@ export function useMeetingTranscription({
578
653
  if (pendingPillInitRef.current?.meetingId !== resolvedMeetingId)
579
654
  return;
580
655
  const initialNotes = data?.meeting?.userNotesMd ?? "";
656
+ const title = data?.meeting?.title ?? undefined;
657
+ // Spread rather than rebuild: this ref is what `clips:pill-ready`
658
+ // replays into a window that mounts later, and only the engine-start
659
+ // path is allowed to clear `starting`. Rebuilding it here dropped
660
+ // that flag, so a pill reloaded during startup came up showing a
661
+ // live meter and transport for capture that had not attached.
581
662
  pendingPillInitRef.current = {
663
+ ...pendingPillInitRef.current,
582
664
  meetingId: resolvedMeetingId,
583
665
  initialNotes,
666
+ title,
584
667
  preloadedLines: pillTranscriptLines(session.lines),
585
668
  };
669
+ // A pill that mounted before this fetch resolved learns the
670
+ // meeting title here; same meetingId+mode means the pill applies
671
+ // it without resetting its session state.
672
+ emit("clips:pill-context", {
673
+ meetingId: resolvedMeetingId,
674
+ mode: "meeting",
675
+ title,
676
+ starting: pendingPillInitRef.current.starting,
677
+ }).catch(() => {});
586
678
  emit("clips:meeting-notes-init", {
587
679
  meetingId: resolvedMeetingId,
588
680
  initialNotes,
@@ -630,36 +722,69 @@ export function useMeetingTranscription({
630
722
  })
631
723
  .catch(() => {});
632
724
 
633
- // The local index may need a moment after the fragment fence. Anchor
634
- // the live engine where it actually begins, not at the earlier click,
635
- // so every stored segment remains on one honest meeting timeline.
636
- if (includeFromMeetingStart && payload.scheduledStart) {
637
- session.liveTimelineOffsetMs = Math.max(
638
- 0,
639
- Date.now() - Date.parse(payload.scheduledStart),
640
- );
725
+ const startedEngine = await enginePromise;
726
+ engineStarting = null;
727
+ liveEngine = startedEngine;
728
+ // A newer start can have replaced this session while the row creation
729
+ // and the engine were in flight — `stopTranscription` tore this one
730
+ // down against `session.engine`, which was still the placeholder,
731
+ // so the engine that just came up belongs to nobody. Publishing the
732
+ // tray, indicator, and pill state below would repoint all three at the
733
+ // meeting the user just left. Every other continuation in this function
734
+ // guards the same way.
735
+ if (sessionRef.current !== session) {
736
+ await stopTranscriptionEngine(startedEngine).catch(() => {});
737
+ liveEngine = null;
738
+ if (historyPreparedRef.current) {
739
+ invoke("rewind_meeting_history_cancel", {
740
+ token: historyPreparedRef.current.token,
741
+ }).catch(() => {});
742
+ }
743
+ // The teardown that replaced this session ran while the row was
744
+ // still being created, so its own `stop-meeting-recording` either
745
+ // hit nothing or stopped a row that did not exist yet. Closing the
746
+ // row here is the only chance left — otherwise the meeting stays
747
+ // recording server-side with no client attached to it.
748
+ if (session.recordingId) {
749
+ await callClipsAction("stop-meeting-recording", {
750
+ meetingId: session.meetingId,
751
+ }).catch((err) => {
752
+ console.warn(
753
+ "[clips-popover] could not close a superseded meeting row:",
754
+ err,
755
+ );
756
+ });
757
+ }
758
+ return;
641
759
  }
642
- session.engine = await startTranscriptionEngine({
643
- mic: { deviceId: selectedMicId, label: selectedMicLabel },
644
- // macOS 15+ uses ScreenCaptureKit's independent microphone output.
645
- // Rust upgrades only the legacy/failure fallback to bypassed VPIO so
646
- // call apps cannot starve Clips of mic buffers or lose call volume.
647
- voiceProcessing: false,
648
- });
760
+ session.engine = startedEngine;
761
+ // Anything captured while the row was still being created has a home
762
+ // now, so write it before the first idle flush would have.
763
+ if (session.lines.length) scheduleFlush();
649
764
 
650
- await invoke("set_recording_state", { active: true }).catch(() => {});
651
- await invoke("set_meeting_active", {
652
- active: true,
653
- meetingId: resolvedMeetingId,
654
- }).catch(() => {});
655
- await invoke("recording_pill_show", {
656
- meetingId: resolvedMeetingId,
657
- mode: "meeting",
658
- });
765
+ await Promise.all([
766
+ invoke("set_recording_state", { active: true }).catch(() => {}),
767
+ invoke("set_meeting_active", {
768
+ active: true,
769
+ meetingId: resolvedMeetingId,
770
+ }).catch(() => {}),
771
+ invoke("recording_pill_show", {
772
+ meetingId: resolvedMeetingId,
773
+ mode: "meeting",
774
+ }),
775
+ ]);
776
+ if (pendingPillInitRef.current?.meetingId === resolvedMeetingId) {
777
+ pendingPillInitRef.current = {
778
+ ...pendingPillInitRef.current,
779
+ starting: false,
780
+ };
781
+ }
659
782
  // Immediate emit covers the reused-window case (pill already mounted).
783
+ // `starting: false` is what flips the pill from waiting to recording.
660
784
  emit("clips:pill-context", {
661
785
  meetingId: resolvedMeetingId,
662
786
  mode: "meeting",
787
+ starting: false,
663
788
  }).catch(() => {});
664
789
  emit("meetings:transcription-started", {
665
790
  meetingId: resolvedMeetingId,
@@ -729,22 +854,54 @@ export function useMeetingTranscription({
729
854
 
730
855
  emit("meetings:hide-notification", { meetingId }).catch(() => {});
731
856
  } catch (err) {
857
+ // The engine can be live even though the start failed: it runs in
858
+ // parallel with the row creation, and startup continues for a while
859
+ // after it comes up. Leaving it would hold the microphone for a session
860
+ // that does not exist. Whichever of the two is live gets stopped —
861
+ // `liveEngine` once the promise resolved, the promise itself before
862
+ // that. Checking only the promise let every failure after the engine
863
+ // was up orphan it.
864
+ if (liveEngine) {
865
+ await stopTranscriptionEngine(liveEngine).catch(() => {});
866
+ } else if (engineStarting) {
867
+ // coercion-ok: an engine that never started has nothing to tear
868
+ // down, and null is distinguishable from a started one below. The
869
+ // start failure itself is already being reported by this catch.
870
+ const engine = await engineStarting.catch(() => null);
871
+ if (engine) await stopTranscriptionEngine(engine).catch(() => {});
872
+ }
732
873
  if (historyPreparedRef.current) {
733
874
  invoke("rewind_meeting_history_cancel", {
734
875
  token: historyPreparedRef.current.token,
735
876
  }).catch(() => {});
736
877
  }
737
- const failedSession = sessionRef.current;
738
- sessionRef.current = null;
739
- if (failedSession?.meetingId) {
878
+ // A newer start may have taken over while this one was failing. It owns
879
+ // the session ref, the pill, and the tray state now, so tearing those
880
+ // down here would stop a meeting that is recording fine — the same
881
+ // identity guard `stopTranscription` uses for its shared writes.
882
+ const superseded =
883
+ sessionRef.current !== null && sessionRef.current !== startedSession;
884
+ if (!superseded) {
885
+ const failedSession = startedSession;
886
+ sessionRef.current = null;
887
+ if (failedSession?.meetingId) {
888
+ await callClipsAction("stop-meeting-recording", {
889
+ meetingId: failedSession.meetingId,
890
+ }).catch(() => {});
891
+ }
892
+ pendingPillInitRef.current = null;
893
+ await invoke("recording_pill_hide").catch(() => {});
894
+ await invoke("set_recording_state", { active: false }).catch(
895
+ () => {},
896
+ );
897
+ await invoke("set_meeting_active", { active: false }).catch(() => {});
898
+ } else if (startedSession?.meetingId) {
899
+ // This session's row still needs closing even though the globals
900
+ // belong to the newer one.
740
901
  await callClipsAction("stop-meeting-recording", {
741
- meetingId: failedSession.meetingId,
902
+ meetingId: startedSession.meetingId,
742
903
  }).catch(() => {});
743
904
  }
744
- pendingPillInitRef.current = null;
745
- await invoke("recording_pill_hide").catch(() => {});
746
- await invoke("set_recording_state", { active: false }).catch(() => {});
747
- await invoke("set_meeting_active", { active: false }).catch(() => {});
748
905
  const message =
749
906
  err instanceof Error ? err.message : "Could not start notes.";
750
907
  emit("meetings:transcription-error", {
@@ -762,6 +919,40 @@ export function useMeetingTranscription({
762
919
  ],
763
920
  );
764
921
 
922
+ /**
923
+ * One start at a time.
924
+ *
925
+ * `sessionRef` is only claimed part-way through the sequence above, so two
926
+ * starts arriving close together — a reminder click racing the calendar
927
+ * auto-start, or a double-fired event — both saw no existing session, and
928
+ * each created a recording row and an audio engine. Only one could own
929
+ * `sessionRef`; the other's capture ran with nothing left to stop it, and its
930
+ * row was never closed.
931
+ *
932
+ * Serializing is what makes the existing checks mean anything: the second
933
+ * start now sees the first's session and either adopts it (same meeting) or
934
+ * replaces it through `stopTranscription`, which is the path that tears the
935
+ * first one down properly.
936
+ */
937
+ const startInFlightRef = useRef<Promise<void> | null>(null);
938
+ const startTranscription = useCallback(
939
+ async (payload: MeetingTranscriptionPayload) => {
940
+ const previous = startInFlightRef.current;
941
+ const run = (async () => {
942
+ // A failed start must not stop the next one from running.
943
+ if (previous) await previous.catch(() => {});
944
+ await runStartTranscription(payload);
945
+ })();
946
+ startInFlightRef.current = run;
947
+ try {
948
+ await run;
949
+ } finally {
950
+ if (startInFlightRef.current === run) startInFlightRef.current = null;
951
+ }
952
+ },
953
+ [runStartTranscription],
954
+ );
955
+
765
956
  // -------------------------------------------------------------------------
766
957
  // Event listeners
767
958
  // -------------------------------------------------------------------------
@@ -843,6 +1034,8 @@ export function useMeetingTranscription({
843
1034
  emit("clips:pill-context", {
844
1035
  meetingId: pending.meetingId,
845
1036
  mode: "meeting",
1037
+ title: pending.title,
1038
+ starting: pending.starting === true,
846
1039
  }).catch(() => {});
847
1040
  emit("clips:meeting-notes-init", {
848
1041
  meetingId: pending.meetingId,