@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.
- package/corpus/templates/analytics/actions/get-explorer-dashboard.ts +1 -0
- package/corpus/templates/analytics/app/components/auth/AuthProvider.tsx +8 -6
- package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +32 -0
- package/corpus/templates/analytics/app/components/layout/Layout.tsx +1 -1
- package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +66 -7
- package/corpus/templates/analytics/app/lib/prefetch-keys.ts +10 -0
- package/corpus/templates/analytics/app/pages/adhoc/explorer-dashboard/index.tsx +9 -7
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +10 -11
- package/corpus/templates/analytics/changelog/2026-08-24-feature-flag-changes-recover-from-temporary-workspace-direct.md +6 -0
- package/corpus/templates/analytics/changelog/2026-08-26-analytics-distinguishes-daily-from-weekly-activity-shows-sta.md +6 -0
- package/corpus/templates/analytics/changelog/2026-08-26-dashboard-metadata-now-shows-the-original-creator-separately.md +6 -0
- package/corpus/templates/analytics/changelog/2026-08-26-dashboard-names-stay-editable-when-the-analytics-sidebar-ref.md +6 -0
- package/corpus/templates/analytics/changelog/2026-08-26-fixed-the-agent-re-asking-dashboard-scope-questions-you-had-.md +6 -0
- package/corpus/templates/analytics/server/db/schema.ts +2 -0
- package/corpus/templates/analytics/server/lib/agent-readable-resource-context.ts +8 -2
- package/corpus/templates/analytics/server/lib/dashboards-store.ts +5 -0
- package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +6 -4
- package/corpus/templates/analytics/server/lib/migrate-analytics-artifacts.ts +19 -1
- package/corpus/templates/analytics/server/lib/workspace-feature-flags.ts +40 -9
- package/corpus/templates/analytics/server/plugins/db.ts +10 -0
- package/corpus/templates/analytics/server/routes/api/dashboard-agent-context.json.get.ts +1 -0
- package/corpus/templates/analytics/shared/feature-flags.ts +11 -1
- package/corpus/templates/calendar/app/components/calendar/GoogleConnectBanner.tsx +2 -9
- package/corpus/templates/clips/.agents/skills/ai-video-tools/SKILL.md +8 -7
- package/corpus/templates/clips/.agents/skills/recording/SKILL.md +9 -8
- package/corpus/templates/clips/actions/import-loom-recording.ts +1 -1
- package/corpus/templates/clips/actions/lib/loom-import-job.ts +52 -35
- package/corpus/templates/clips/actions/request-transcript.ts +6 -183
- package/corpus/templates/clips/app/components/clips-avatar.tsx +46 -0
- package/corpus/templates/clips/app/components/library/recording-card.tsx +8 -7
- package/corpus/templates/clips/app/components/library/space-card.tsx +8 -5
- package/corpus/templates/clips/app/components/meetings/attendee-stack.tsx +7 -8
- package/corpus/templates/clips/app/components/meetings/transcript-bubbles.tsx +19 -12
- package/corpus/templates/clips/app/components/player/transcript-panel.tsx +128 -94
- package/corpus/templates/clips/app/components/recorder/microphone-visualizer.tsx +25 -104
- package/corpus/templates/clips/app/components/workspace/members-list.tsx +8 -6
- package/corpus/templates/clips/app/components/workspace/notifications-list.tsx +8 -6
- package/corpus/templates/clips/app/components/workspace/top-creators-table.tsx +8 -6
- package/corpus/templates/clips/app/routes/_app.meetings.$meetingId.tsx +8 -7
- package/corpus/templates/clips/app/routes/_app.settings._index.tsx +0 -26
- package/corpus/templates/clips/app/routes/r.$recordingId.tsx +1 -4
- package/corpus/templates/clips/changelog/2026-08-26-detected-calls-no-longer-duplicate-meetings-or-strand-the-prompt.md +6 -0
- package/corpus/templates/clips/desktop/src/app.tsx +8 -10
- package/corpus/templates/clips/desktop/src/components/live-waveform.tsx +99 -0
- package/corpus/templates/clips/desktop/src/hooks/useMeetingTranscription.ts +248 -55
- package/corpus/templates/clips/desktop/src/lib/agent-steps.ts +477 -0
- package/corpus/templates/clips/desktop/src/lib/ask-sheet-layout.ts +71 -0
- package/corpus/templates/clips/desktop/src/lib/meeting-ask.ts +297 -0
- package/corpus/templates/clips/desktop/src/lib/url.ts +24 -0
- package/corpus/templates/clips/desktop/src/overlays/ask-steps.tsx +155 -0
- package/corpus/templates/clips/desktop/src/overlays/flow-bar.tsx +7 -81
- package/corpus/templates/clips/desktop/src/overlays/live-transcript.tsx +29 -2
- package/corpus/templates/clips/desktop/src/overlays/meeting-notification.tsx +80 -22
- package/corpus/templates/clips/desktop/src/overlays/record-pill.tsx +13 -53
- package/corpus/templates/clips/desktop/src/overlays/recording-pill.tsx +1017 -82
- package/corpus/templates/clips/desktop/src/styles.css +796 -57
- package/corpus/templates/clips/desktop/src/tailwind.css +23 -2
- package/corpus/templates/clips/desktop/src-tauri/src/adhoc_meetings_watcher.rs +1765 -219
- package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +6 -0
- package/corpus/templates/clips/desktop/src-tauri/src/meetings_watcher.rs +45 -30
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen/custom_capture.rs +5 -3
- package/corpus/templates/clips/desktop/src-tauri/src/notifications.rs +91 -0
- package/corpus/templates/clips/desktop/src-tauri/src/recording_indicator.rs +123 -27
- package/corpus/templates/clips/server/routes/_agent-native/google/callback.get.ts +16 -0
- package/corpus/templates/clips/shared/audio-meter.ts +145 -0
- package/corpus/templates/clips/shared/clips-ai-prefs.ts +0 -1
- package/corpus/templates/clips/shared/live-waveform.tsx +119 -0
- package/corpus/templates/content/app/components/editor/CommentsSidebar.tsx +44 -22
- package/corpus/templates/content/app/components/ui/avatar.tsx +1 -0
- package/corpus/templates/design/app/components/visual-editor/ReviewCanvasPins.tsx +4 -1
- package/corpus/templates/design/app/hooks/use-question-flow.ts +9 -2
- package/corpus/templates/design/app/pages/DesignSystemSetup.tsx +111 -37
- package/corpus/templates/design/changelog/2026-08-26-design-no-longer-asks-users-to-confirm-guided-questions-they-have-already-answered.md +6 -0
- package/corpus/templates/dispatch/changelog/2026-08-26-signing-out-goes-straight-to-the-sign-in-page.md +5 -0
- package/corpus/templates/dispatch/server/lib/org-apps-directory.ts +96 -47
- package/corpus/templates/dispatch/server/plugins/org-apps-directory.ts +73 -29
- package/corpus/templates/factory/.env.example +6 -2
- package/corpus/templates/factory/AGENTS.md +3 -3
- package/corpus/templates/factory/README.md +4 -2
- package/corpus/templates/factory/server/connectors/credentials.ts +70 -4
- package/corpus/templates/mail/actions/get-email.ts +68 -21
- package/corpus/templates/mail/actions/get-thread.ts +92 -51
- package/corpus/templates/mail/app/components/GoogleConnectBanner.tsx +2 -9
- package/corpus/templates/mail/app/hooks/use-emails.ts +7 -4
- package/corpus/templates/mail/server/lib/mail-connector-catalog.ts +7 -4
- package/corpus/templates/plan/app/pages/PlansPage.tsx +23 -15
- package/corpus/templates/slides/actions/add-deck.ts +20 -0
- package/corpus/templates/slides/actions/create-deck.ts +17 -7
- package/corpus/templates/slides/actions/delete-deck.ts +45 -5
- package/corpus/templates/slides/actions/get-deck.ts +77 -11
- package/corpus/templates/slides/actions/patch-deck.ts +14 -2
- package/corpus/templates/slides/actions/save-deck.ts +20 -0
- package/corpus/templates/slides/app/components/comments/SlideCommentsPanel.tsx +18 -7
- package/corpus/templates/slides/app/components/design-system/DesignSystemSetup.tsx +38 -14
- package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +2 -2
- package/corpus/templates/slides/app/components/ui/avatar.tsx +1 -0
- package/corpus/templates/slides/server/handlers/decks.ts +10 -0
- package/corpus/templates/slides/shared/slide-ids.ts +133 -0
- package/corpus/templates/tasks/DEVELOPING.md +8 -0
- package/corpus/templates/tasks/drizzle.config.ts +3 -1
- package/corpus/templates/tasks/drizzle.local.config.ts +6 -0
- package/corpus/templates/tasks/package.json +2 -1
- package/corpus/templates/tasks/server/db/migrations/meta/0000_snapshot.json +234 -0
- package/corpus/templates/tasks/server/db/migrations/meta/_journal.json +13 -0
- package/corpus/templates/tasks/server/db/migrations.ts +110 -0
- package/corpus/templates/tasks/server/plugins/db.ts +3 -87
- package/dist/client/AssistantChat.js +3 -10
- package/dist/client/guided-questions.d.ts +11 -1
- package/dist/client/guided-questions.js +24 -7
- package/dist/client/hooks/index.d.ts +2 -1
- package/dist/client/hooks/index.js +2 -1
- package/dist/client/org/OrgSwitcher.js +3 -17
- package/dist/client/review/ReviewThreadPanel.js +4 -2
- package/dist/client/sign-out.d.ts +15 -0
- package/dist/client/sign-out.js +82 -0
- package/dist/client/use-action.d.ts +2 -0
- package/dist/client/use-action.js +4 -2
- package/dist/client/use-session.d.ts +23 -1
- package/dist/client/use-session.js +46 -0
- package/dist/collab/client.js +20 -5
- package/dist/collab/presence.d.ts +3 -1
- package/dist/collab/presence.js +48 -45
- package/dist/collab/routes.d.ts +1 -1
- package/dist/db/drizzle-config.d.ts +7 -0
- package/dist/db/drizzle-config.js +35 -9
- package/dist/db/drizzle-migrations.d.ts +28 -0
- package/dist/db/drizzle-migrations.js +81 -0
- package/dist/db/index.d.ts +1 -1
- package/dist/db/migrations.d.ts +13 -1
- package/dist/db/migrations.js +60 -31
- package/dist/db/schema.js +4 -1
- package/dist/deploy/build.d.ts +6 -0
- package/dist/deploy/build.js +41 -0
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/mcp/index.d.ts +1 -1
- package/dist/mcp/index.js +1 -1
- package/dist/mcp/org-directory.d.ts +19 -7
- package/dist/mcp/org-directory.js +92 -68
- package/dist/mcp/screen-memory-stdio.d.ts +7 -7
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/org/context.d.ts +6 -0
- package/dist/org/context.js +22 -0
- package/dist/org/index.d.ts +1 -1
- package/dist/org/index.js +1 -1
- package/dist/resources/store.d.ts +12 -0
- package/dist/resources/store.js +55 -0
- package/dist/server/agent-discovery.d.ts +18 -2
- package/dist/server/agent-discovery.js +191 -22
- package/dist/server/auth.js +3 -2
- package/dist/server/builder-browser.d.ts +0 -4
- package/dist/server/builder-browser.js +23 -8
- package/dist/server/google-oauth.d.ts +18 -0
- package/dist/server/google-oauth.js +3 -3
- package/dist/server/onboarding-html.js +3 -3
- package/dist/server/poll.d.ts +4 -2
- package/dist/server/poll.js +8 -3
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/server/short-lived-token.d.ts +1 -1
- package/dist/server/short-lived-token.js +1 -1
- package/dist/shared/environment-lanes.d.ts +1 -0
- package/dist/shared/environment-lanes.js +1 -0
- package/dist/shared/google-profile-image.d.ts +2 -0
- package/dist/shared/google-profile-image.js +24 -0
- package/dist/shared/index.d.ts +1 -0
- package/dist/shared/index.js +1 -0
- package/dist/styles/agent-conversation.css +13 -0
- package/docs/content/creating-templates.mdx +10 -1
- package/docs/content/server-database.mdx +49 -3
- package/docs/content/server-plugins.mdx +39 -3
- package/docs/content/webmcp.mdx +2 -0
- package/package.json +2 -1
- package/corpus/templates/analytics/data/explorer-configs/_autosave.json +0 -18
- package/corpus/templates/analytics/data/explorer-configs/steve-chart.json +0 -18
- package/corpus/templates/analytics/data/explorer-dashboards/dashboard-1773260837897.json +0 -10
- package/corpus/templates/analytics/data/sync-config.json +0 -8
- package/corpus/templates/brain/data/sync-config.json +0 -1
- package/corpus/templates/calendar/data/availability.json +0 -32
- package/corpus/templates/calendar/data/settings.json +0 -6
- package/corpus/templates/calendar/data/sync-config.json +0 -10
- package/corpus/templates/chat/data/sync-config.json +0 -1
- package/corpus/templates/clips/data/sync-config.json +0 -1
- package/corpus/templates/clips/desktop/src/lib/audio-meter.ts +0 -80
- package/corpus/templates/clips/desktop/src/overlays/live-audio-bars.tsx +0 -113
- package/corpus/templates/design/data/sync-config.json +0 -1
- package/corpus/templates/factory/data/sync-config.json +0 -1
- package/corpus/templates/forms/data/settings.json +0 -8
- package/corpus/templates/forms/data/sync-config.json +0 -3
- package/corpus/templates/macros/data/sync-config.json +0 -4
- package/corpus/templates/mail/data/emails.defaults.json +0 -1
- package/corpus/templates/mail/data/labels.defaults.json +0 -59
- package/corpus/templates/mail/data/settings.defaults.json +0 -9
- package/corpus/templates/mail/data/sync-config.json +0 -8
- package/corpus/templates/slides/data/sync-config.json +0 -8
- package/corpus/templates/tasks/data/sync-config.json +0 -1
- package/dist/templates/chat/data/.gitkeep +0 -0
- package/dist/templates/chat/data/sync-config.json +0 -1
- package/dist/templates/default/data/.gitkeep +0 -0
- package/dist/templates/default/data/sync-config.json +0 -1
- package/src/templates/chat/data/.gitkeep +0 -0
- package/src/templates/chat/data/sync-config.json +0 -1
- package/src/templates/default/data/.gitkeep +0 -0
- 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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
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 =
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
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
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
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
|
-
|
|
738
|
-
|
|
739
|
-
|
|
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:
|
|
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,
|