@agent-native/core 0.161.23 → 0.163.0
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/README.md +1 -1
- package/corpus/templates/analytics/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/analytics/.agents/skills/sharing/SKILL.md +27 -2
- package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +18 -2
- package/corpus/templates/assets/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/assets/.agents/skills/sharing/SKILL.md +27 -2
- package/corpus/templates/brain/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/brain/.agents/skills/sharing/SKILL.md +27 -2
- package/corpus/templates/calendar/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/calendar/.agents/skills/sharing/SKILL.md +27 -2
- package/corpus/templates/calendar/server/handlers/google-auth.ts +1 -8
- package/corpus/templates/chat/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/chat/.agents/skills/sharing/SKILL.md +27 -2
- package/corpus/templates/clips/app/components/recorder/pre-record-panel.tsx +88 -35
- package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +36 -1
- package/corpus/templates/clips/app/i18n/en-US.ts +4 -3
- package/corpus/templates/clips/app/lib/camera-composite.ts +15 -1
- package/corpus/templates/clips/changelog/2026-08-18-clips-no-longer-crashes-when-a-camera-bubble-starts-at-a-tin.md +6 -0
- package/corpus/templates/clips/desktop/src/app.tsx +30 -40
- package/corpus/templates/clips/desktop/src/components/MicOffConfirmation.tsx +47 -0
- package/corpus/templates/clips/desktop/src/styles.css +110 -0
- package/corpus/templates/clips/desktop/src-tauri/src/whisper_speech.rs +114 -7
- package/corpus/templates/clips/shared/recording-capture.ts +18 -13
- package/corpus/templates/content/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/content/.agents/skills/sharing/SKILL.md +27 -2
- package/corpus/templates/content/actions/create-content-database.ts +1 -1
- package/corpus/templates/content/actions/create-document.ts +1 -1
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +3 -5
- package/corpus/templates/content/app/components/editor/DocumentToolbar.tsx +0 -3
- package/corpus/templates/content/app/components/editor/NotionConflictBanner.tsx +0 -4
- package/corpus/templates/content/app/components/editor/NotionSyncBar.tsx +4 -1
- package/corpus/templates/content/app/hooks/use-notion.ts +17 -15
- package/corpus/templates/crm/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/crm/.agents/skills/sharing/SKILL.md +27 -2
- package/corpus/templates/design/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/design/.agents/skills/sharing/SKILL.md +27 -2
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +6 -1
- package/corpus/templates/dispatch/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/dispatch/.agents/skills/sharing/SKILL.md +27 -2
- package/corpus/templates/factory/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/factory/.agents/skills/sharing/SKILL.md +27 -2
- package/corpus/templates/forms/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/forms/.agents/skills/sharing/SKILL.md +27 -2
- package/corpus/templates/macros/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/macros/.agents/skills/sharing/SKILL.md +27 -2
- package/corpus/templates/mail/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/mail/.agents/skills/sharing/SKILL.md +27 -2
- package/corpus/templates/mail/app/components/layout/AppLayout.tsx +18 -3
- package/corpus/templates/mail/server/handlers/google-auth.ts +1 -8
- package/corpus/templates/plan/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/plan/.agents/skills/sharing/SKILL.md +27 -2
- package/corpus/templates/slides/.agents/skills/create-deck/SKILL.md +4 -0
- package/corpus/templates/slides/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/slides/.agents/skills/sharing/SKILL.md +27 -2
- package/corpus/templates/slides/actions/_await-fit-check.ts +30 -5
- package/corpus/templates/slides/actions/add-slide.ts +6 -2
- package/corpus/templates/slides/actions/create-deck.ts +2 -1
- package/corpus/templates/slides/app/components/editor/GoogleDriveConnectionCta.tsx +3 -3
- package/corpus/templates/slides/app/components/editor/GoogleSlidesReferenceImport.tsx +3 -3
- package/corpus/templates/slides/app/components/editor/PromptDialog.tsx +3 -1
- package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +1 -1
- package/corpus/templates/slides/app/components/editor/slide-object-interactions.ts +1 -1
- package/corpus/templates/slides/app/lib/create-deck-generation.ts +1 -0
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +35 -2
- package/corpus/templates/slides/app/routes/p.$id.tsx +3 -1
- package/corpus/templates/slides/changelog/2026-08-18-generated-slides-preserve-speaker-notes.md +6 -0
- package/corpus/templates/slides/server/handlers/assets.ts +23 -12
- package/corpus/templates/slides/server/handlers/design-system-decode-job-status.ts +15 -6
- package/corpus/templates/slides/server/handlers/design-system-upload-start.ts +15 -6
- package/corpus/templates/slides/server/handlers/index-design-system-sources.ts +34 -19
- package/corpus/templates/slides/server/handlers/request-auth-context.ts +1 -0
- package/corpus/templates/slides/server/handlers/share.ts +1 -1
- package/corpus/templates/slides/server/plugins/agent-chat.ts +1 -1
- package/corpus/templates/slides/server/routes/api/deck-agent-context.json.get.ts +1 -1
- package/corpus/templates/slides/server/routes/api/exports/[filename].get.ts +9 -3
- package/corpus/templates/slides/server/routes/api/exports/google-slides.post.ts +12 -5
- package/corpus/templates/slides/server/routes/api/exports/html.post.ts +9 -7
- package/corpus/templates/slides/server/routes/api/exports/pptx.post.ts +9 -7
- package/corpus/templates/slides/shared/api.ts +7 -1
- package/corpus/templates/tasks/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/tasks/.agents/skills/sharing/SKILL.md +27 -2
- package/dist/agent/engine/translate-anthropic.js +27 -2
- package/dist/agent/production-agent.d.ts +2 -2
- package/dist/agent/production-agent.js +16 -8
- package/dist/agent/thread-data-builder.d.ts +1 -0
- package/dist/agent/thread-data-builder.js +6 -4
- package/dist/agent/tool-approval-store.d.ts +10 -1
- package/dist/agent/tool-approval-store.js +19 -2
- package/dist/agent/types.d.ts +9 -0
- package/dist/app-config/schema.d.ts +1 -0
- package/dist/app-config/workspace.d.ts +1 -0
- package/dist/app-config/workspace.js +8 -0
- package/dist/chat-threads/schema.d.ts +2 -2
- package/dist/client/AssistantChat.js +14 -6
- package/dist/client/EnvironmentBadge.d.ts +15 -0
- package/dist/client/EnvironmentBadge.js +79 -0
- package/dist/client/MultiTabAssistantChat.js +73 -17
- package/dist/client/agent-chat-adapter.js +11 -1
- package/dist/client/api-path.js +14 -1
- package/dist/client/app-providers.js +2 -1
- package/dist/client/chat/tool-call-display.d.ts +12 -2
- package/dist/client/chat/tool-call-display.js +17 -7
- package/dist/client/chat/widgets/WorkspaceFileWidget.js +1 -1
- package/dist/client/chat/widgets/builtin-tool-renderers.d.ts +6 -0
- package/dist/client/chat/widgets/builtin-tool-renderers.js +19 -0
- package/dist/client/onboarding/FirstRunOnboarding.js +46 -32
- package/dist/client/onboarding/use-onboarding.d.ts +8 -1
- package/dist/client/onboarding/use-onboarding.js +22 -7
- package/dist/client/org/TeamPage.js +15 -2
- package/dist/client/org/hooks.d.ts +2 -0
- package/dist/client/org/hooks.js +12 -0
- package/dist/client/org/index.d.ts +2 -2
- package/dist/client/org/index.js +1 -1
- package/dist/client/session-replay.js +58 -15
- package/dist/client/settings/SettingsTabsPage.js +19 -6
- package/dist/client/sharing/ShareButton.js +43 -18
- package/dist/client/sharing/useShareButtonController.d.ts +12 -2
- package/dist/client/sharing/useShareButtonController.js +38 -8
- package/dist/client/sse-event-processor.d.ts +7 -0
- package/dist/client/sse-event-processor.js +4 -1
- package/dist/client/ui/index.d.ts +1 -0
- package/dist/client/ui/index.js +1 -0
- package/dist/client/use-action.js +26 -0
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/config.d.ts +15 -0
- package/dist/config.js +53 -0
- package/dist/dashboard-storage/schema.d.ts +2 -2
- package/dist/data-programs/schema.d.ts +2 -2
- package/dist/extensions/fetch-tool.js +8 -2
- package/dist/extensions/schema.d.ts +2 -2
- package/dist/extensions/store.js +3 -0
- package/dist/localization/core-messages/ar-SA.js +1 -0
- package/dist/localization/core-messages/de-DE.js +1 -0
- package/dist/localization/core-messages/en-US.d.ts +1 -0
- package/dist/localization/core-messages/en-US.js +1 -0
- package/dist/localization/core-messages/es-ES.js +1 -0
- package/dist/localization/core-messages/fr-FR.js +1 -0
- package/dist/localization/core-messages/hi-IN.js +1 -0
- package/dist/localization/core-messages/ja-JP.js +1 -0
- package/dist/localization/core-messages/ko-KR.js +1 -0
- package/dist/localization/core-messages/pt-BR.js +1 -0
- package/dist/localization/core-messages/zh-CN.js +1 -0
- package/dist/localization/core-messages/zh-TW.js +1 -0
- package/dist/localization/core-messages.d.ts +1 -0
- package/dist/localization/default-messages.d.ts +4 -0
- package/dist/localization/default-messages.js +4 -0
- package/dist/mcp/embed-app.js +1 -1
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/org/handlers.d.ts +6 -1
- package/dist/org/handlers.js +43 -0
- package/dist/org/index.d.ts +3 -3
- package/dist/org/index.js +2 -2
- package/dist/org/migrations.js +33 -0
- package/dist/org/plugin.js +10 -1
- package/dist/org/schema.d.ts +315 -0
- package/dist/org/schema.js +20 -1
- package/dist/org/types.d.ts +3 -0
- package/dist/org/workspace-app-access.d.ts +10 -0
- package/dist/org/workspace-app-access.js +179 -0
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +7 -7
- package/dist/provider-api/actions/provider-api.d.ts +7 -7
- package/dist/provider-api/index.d.ts +10 -0
- package/dist/provider-api/index.js +45 -9
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/auth.js +24 -4
- package/dist/server/poll.js +13 -3
- package/dist/server/sentry.d.ts +1 -1
- package/dist/sharing/access.d.ts +2 -2
- package/dist/sharing/access.js +56 -3
- package/dist/sharing/actions/list-resource-shares.d.ts +2 -0
- package/dist/sharing/actions/list-resource-shares.js +26 -1
- package/dist/sharing/actions/share-resource.d.ts +1 -1
- package/dist/sharing/actions/share-resource.js +23 -4
- package/dist/sharing/actions/unshare-resource.d.ts +1 -1
- package/dist/sharing/actions/unshare-resource.js +1 -1
- package/dist/sharing/registry.d.ts +12 -0
- package/dist/sharing/schema.d.ts +4 -4
- package/dist/sharing/schema.js +2 -2
- package/dist/templates/chat/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/dist/templates/chat/.agents/skills/sharing/SKILL.md +27 -2
- package/dist/templates/default/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/dist/templates/default/.agents/skills/sharing/SKILL.md +27 -2
- package/dist/templates/workspace-core/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/dist/templates/workspace-core/.agents/skills/sharing/SKILL.md +27 -2
- package/dist/vite/client.d.ts +5 -0
- package/dist/vite/client.js +64 -2
- package/dist/workspace-connections/actions/list-workspace-user-groups.d.ts +9 -1
- package/dist/workspace-connections/actions/list-workspace-user-groups.js +9 -4
- package/dist/workspace-connections/groups.d.ts +2 -0
- package/dist/workspace-connections/groups.js +16 -5
- package/dist/workspace-files/actions.d.ts +1 -8
- package/dist/workspace-files/actions.js +5 -21
- package/dist/workspace-files/index.d.ts +1 -1
- package/dist/workspace-files/index.js +1 -1
- package/dist/workspace-files/store.d.ts +18 -0
- package/dist/workspace-files/store.js +24 -3
- package/package.json +3 -3
- package/src/templates/chat/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/src/templates/chat/.agents/skills/sharing/SKILL.md +27 -2
- package/src/templates/default/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/src/templates/default/.agents/skills/sharing/SKILL.md +27 -2
- package/src/templates/workspace-core/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/src/templates/workspace-core/.agents/skills/sharing/SKILL.md +27 -2
|
@@ -505,6 +505,8 @@ export class RecorderEngine {
|
|
|
505
505
|
private cameraComposite: CameraCompositeHandle | null = null;
|
|
506
506
|
private audioMixCtx: AudioContext | null = null;
|
|
507
507
|
private audioMixSources: MediaStreamAudioSourceNode[] = [];
|
|
508
|
+
private wakeLock: WakeLockSentinel | null = null;
|
|
509
|
+
private wakeLockGeneration = 0;
|
|
508
510
|
private recorder: MediaRecorder | null = null;
|
|
509
511
|
private mimeType: string = "video/webm";
|
|
510
512
|
|
|
@@ -890,7 +892,7 @@ export class RecorderEngine {
|
|
|
890
892
|
this.opts.displaySurface ?? "window",
|
|
891
893
|
);
|
|
892
894
|
const displayOptions: ExtendedDisplayMediaOptions =
|
|
893
|
-
screenCaptureDisplayOptions(displaySurface);
|
|
895
|
+
screenCaptureDisplayOptions(displaySurface, wantsMic);
|
|
894
896
|
|
|
895
897
|
if (wantsMic || wantsDisplay) {
|
|
896
898
|
this.audioMixCtx?.close().catch(() => {});
|
|
@@ -904,6 +906,14 @@ export class RecorderEngine {
|
|
|
904
906
|
} catch (err) {
|
|
905
907
|
throw this.friendlyError(err, "screen");
|
|
906
908
|
}
|
|
909
|
+
// The OS is as free to sleep/lock the display during a pause as
|
|
910
|
+
// during active recording, which ends getDisplayMedia's video track
|
|
911
|
+
// outright (unlike the mic/camera, display capture requires an
|
|
912
|
+
// active compositor). Without this, a paused recording left idle
|
|
913
|
+
// finalizes early with no warning — see onDisplayTrackEnded below.
|
|
914
|
+
// Best-effort: unsupported browsers or a denied request must never
|
|
915
|
+
// block capture.
|
|
916
|
+
void this.acquireWakeLock();
|
|
907
917
|
}
|
|
908
918
|
|
|
909
919
|
if (wantsCamera) {
|
|
@@ -2360,9 +2370,34 @@ export class RecorderEngine {
|
|
|
2360
2370
|
);
|
|
2361
2371
|
}
|
|
2362
2372
|
|
|
2373
|
+
/** Best-effort — unsupported browsers or a denied request must never block capture. */
|
|
2374
|
+
private async acquireWakeLock(): Promise<void> {
|
|
2375
|
+
const generation = ++this.wakeLockGeneration;
|
|
2376
|
+
try {
|
|
2377
|
+
// coercion-ok: optional chaining yields undefined only when the Wake
|
|
2378
|
+
// Lock API itself is absent (older/other browsers) — a real rejection
|
|
2379
|
+
// (denied request) is caught below, not coerced here.
|
|
2380
|
+
const wakeLock = (await navigator.wakeLock?.request?.("screen")) ?? null;
|
|
2381
|
+
if (generation !== this.wakeLockGeneration || !this.displayStream) {
|
|
2382
|
+
wakeLock?.release().catch(() => {});
|
|
2383
|
+
return;
|
|
2384
|
+
}
|
|
2385
|
+
this.wakeLock = wakeLock;
|
|
2386
|
+
} catch {
|
|
2387
|
+
if (generation === this.wakeLockGeneration) this.wakeLock = null;
|
|
2388
|
+
}
|
|
2389
|
+
}
|
|
2390
|
+
|
|
2391
|
+
private releaseWakeLock(): void {
|
|
2392
|
+
this.wakeLockGeneration += 1;
|
|
2393
|
+
this.wakeLock?.release().catch(() => {});
|
|
2394
|
+
this.wakeLock = null;
|
|
2395
|
+
}
|
|
2396
|
+
|
|
2363
2397
|
private cleanupTracks(): void {
|
|
2364
2398
|
// Clear before stopping tracks so the `ended` events our own stop() fires
|
|
2365
2399
|
// aren't mistaken for a disconnect.
|
|
2400
|
+
this.releaseWakeLock();
|
|
2366
2401
|
this.cameraLive = false;
|
|
2367
2402
|
this.audioMixSources = [];
|
|
2368
2403
|
this.audioMixCtx?.close().catch(() => {});
|
|
@@ -1335,10 +1335,11 @@ All notable user-facing changes to Clips are documented here. Open it any time f
|
|
|
1335
1335
|
cameraOff: "Camera off",
|
|
1336
1336
|
includeCameraAria: "Include camera in this recording",
|
|
1337
1337
|
startRecording: "Start recording",
|
|
1338
|
-
micOffConfirmTitle: "
|
|
1338
|
+
micOffConfirmTitle: "Your mic is muted",
|
|
1339
1339
|
micOffConfirmDescription:
|
|
1340
|
-
"
|
|
1341
|
-
startWithoutMic: "
|
|
1340
|
+
"To have sound in your video, you’ll need to unmute your microphone.",
|
|
1341
|
+
startWithoutMic: "Continue",
|
|
1342
|
+
unmuteMicrophone: "Unmute",
|
|
1342
1343
|
uploadVideo: "Upload video",
|
|
1343
1344
|
importLoom: "Import Loom",
|
|
1344
1345
|
importing: "Importing...",
|
|
@@ -149,6 +149,13 @@ export function clampToMaxDimension(
|
|
|
149
149
|
};
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
+
export function cameraBubbleStrokeRadius(
|
|
153
|
+
size: number,
|
|
154
|
+
lineWidth: number,
|
|
155
|
+
): number {
|
|
156
|
+
return Math.max(0, size / 2 - lineWidth / 2);
|
|
157
|
+
}
|
|
158
|
+
|
|
152
159
|
function resizeCanvasToDisplay(
|
|
153
160
|
canvas: HTMLCanvasElement,
|
|
154
161
|
displayVideo: HTMLVideoElement,
|
|
@@ -225,6 +232,7 @@ function drawCameraBubble(
|
|
|
225
232
|
maxSize,
|
|
226
233
|
),
|
|
227
234
|
);
|
|
235
|
+
if (size <= 0) return;
|
|
228
236
|
const margin = Math.round(
|
|
229
237
|
clamp(minDimension * options.bubbleMarginRatio, 24, 80),
|
|
230
238
|
);
|
|
@@ -255,7 +263,13 @@ function drawCameraBubble(
|
|
|
255
263
|
ctx.strokeStyle = "rgba(255, 255, 255, 0.86)";
|
|
256
264
|
ctx.lineWidth = Math.max(4, Math.round(size * 0.025));
|
|
257
265
|
ctx.beginPath();
|
|
258
|
-
ctx.arc(
|
|
266
|
+
ctx.arc(
|
|
267
|
+
centerX,
|
|
268
|
+
centerY,
|
|
269
|
+
cameraBubbleStrokeRadius(size, ctx.lineWidth),
|
|
270
|
+
0,
|
|
271
|
+
Math.PI * 2,
|
|
272
|
+
);
|
|
259
273
|
ctx.stroke();
|
|
260
274
|
ctx.restore();
|
|
261
275
|
}
|
|
@@ -37,16 +37,6 @@ import {
|
|
|
37
37
|
useState,
|
|
38
38
|
} from "react";
|
|
39
39
|
|
|
40
|
-
import {
|
|
41
|
-
AlertDialog,
|
|
42
|
-
AlertDialogAction,
|
|
43
|
-
AlertDialogCancel,
|
|
44
|
-
AlertDialogContent,
|
|
45
|
-
AlertDialogDescription,
|
|
46
|
-
AlertDialogFooter,
|
|
47
|
-
AlertDialogHeader,
|
|
48
|
-
AlertDialogTitle,
|
|
49
|
-
} from "./components/AlertDialog";
|
|
50
40
|
import { FeedbackButton } from "./components/FeedbackButton";
|
|
51
41
|
import {
|
|
52
42
|
CamIcon,
|
|
@@ -58,6 +48,7 @@ import {
|
|
|
58
48
|
SettingsIcon,
|
|
59
49
|
} from "./components/Icons";
|
|
60
50
|
import { MediaDeviceRow } from "./components/MediaDeviceRow";
|
|
51
|
+
import { MicOffConfirmation } from "./components/MicOffConfirmation";
|
|
61
52
|
import { ReadinessPanel } from "./components/ReadinessPanel";
|
|
62
53
|
import { SourceRow, type CaptureSource } from "./components/SourceRow";
|
|
63
54
|
import { Switch } from "./components/Switch";
|
|
@@ -3333,6 +3324,22 @@ export function App() {
|
|
|
3333
3324
|
void handleStartRecording(options);
|
|
3334
3325
|
}
|
|
3335
3326
|
|
|
3327
|
+
function closeMicOffConfirmation() {
|
|
3328
|
+
pendingStartOptionsRef.current = undefined;
|
|
3329
|
+
setMicOffConfirmOpen(false);
|
|
3330
|
+
}
|
|
3331
|
+
|
|
3332
|
+
function unmuteFromConfirmation() {
|
|
3333
|
+
setMicOn(true);
|
|
3334
|
+
closeMicOffConfirmation();
|
|
3335
|
+
}
|
|
3336
|
+
|
|
3337
|
+
function continueWithoutMic() {
|
|
3338
|
+
const options = pendingStartOptionsRef.current;
|
|
3339
|
+
closeMicOffConfirmation();
|
|
3340
|
+
void handleStartRecording(options);
|
|
3341
|
+
}
|
|
3342
|
+
|
|
3336
3343
|
recordShortcutHandlerRef.current = () => {
|
|
3337
3344
|
if (recorder) {
|
|
3338
3345
|
emit("clips:recorder-stop").catch(() => {});
|
|
@@ -3960,7 +3967,19 @@ export function App() {
|
|
|
3960
3967
|
|
|
3961
3968
|
return (
|
|
3962
3969
|
<div className="app app-recorder" ref={appRef}>
|
|
3963
|
-
|
|
3970
|
+
{micOffConfirmOpen ? (
|
|
3971
|
+
<MicOffConfirmation
|
|
3972
|
+
onBack={closeMicOffConfirmation}
|
|
3973
|
+
onUnmute={unmuteFromConfirmation}
|
|
3974
|
+
onContinue={continueWithoutMic}
|
|
3975
|
+
/>
|
|
3976
|
+
) : null}
|
|
3977
|
+
|
|
3978
|
+
<div
|
|
3979
|
+
className="recorder-home-content"
|
|
3980
|
+
hidden={micOffConfirmOpen}
|
|
3981
|
+
aria-hidden={micOffConfirmOpen}
|
|
3982
|
+
>
|
|
3964
3983
|
<Header
|
|
3965
3984
|
mode={mode}
|
|
3966
3985
|
onModeChange={setMode}
|
|
@@ -4077,35 +4096,6 @@ export function App() {
|
|
|
4077
4096
|
</button>
|
|
4078
4097
|
) : null}
|
|
4079
4098
|
|
|
4080
|
-
<AlertDialog
|
|
4081
|
-
open={micOffConfirmOpen}
|
|
4082
|
-
onOpenChange={(open) => {
|
|
4083
|
-
setMicOffConfirmOpen(open);
|
|
4084
|
-
if (!open) pendingStartOptionsRef.current = undefined;
|
|
4085
|
-
}}
|
|
4086
|
-
>
|
|
4087
|
-
<AlertDialogContent>
|
|
4088
|
-
<AlertDialogHeader>
|
|
4089
|
-
<AlertDialogTitle>Record without a microphone?</AlertDialogTitle>
|
|
4090
|
-
<AlertDialogDescription>
|
|
4091
|
-
Your mic is off, so this recording won't capture any audio.
|
|
4092
|
-
Turn it on before starting if you want narration.
|
|
4093
|
-
</AlertDialogDescription>
|
|
4094
|
-
</AlertDialogHeader>
|
|
4095
|
-
<AlertDialogFooter>
|
|
4096
|
-
<AlertDialogAction
|
|
4097
|
-
onClick={() => {
|
|
4098
|
-
const options = pendingStartOptionsRef.current;
|
|
4099
|
-
pendingStartOptionsRef.current = undefined;
|
|
4100
|
-
void handleStartRecording(options);
|
|
4101
|
-
}}
|
|
4102
|
-
>
|
|
4103
|
-
Start anyway
|
|
4104
|
-
</AlertDialogAction>
|
|
4105
|
-
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
4106
|
-
</AlertDialogFooter>
|
|
4107
|
-
</AlertDialogContent>
|
|
4108
|
-
</AlertDialog>
|
|
4109
4099
|
{recError ? (
|
|
4110
4100
|
recError === MACOS_UPDATE_RESTART_MESSAGE ? (
|
|
4111
4101
|
<UpdateRestartBanner message={recError} />
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { IconArrowLeft, IconMicrophone2 } from "@tabler/icons-react";
|
|
2
|
+
|
|
3
|
+
export function MicOffConfirmation({
|
|
4
|
+
onBack,
|
|
5
|
+
onUnmute,
|
|
6
|
+
onContinue,
|
|
7
|
+
}: {
|
|
8
|
+
onBack: () => void;
|
|
9
|
+
onUnmute: () => void;
|
|
10
|
+
onContinue: () => void;
|
|
11
|
+
}) {
|
|
12
|
+
return (
|
|
13
|
+
<section className="mic-off-confirmation" aria-labelledby="mic-off-title">
|
|
14
|
+
<header className="mic-off-confirmation-header">
|
|
15
|
+
<button
|
|
16
|
+
type="button"
|
|
17
|
+
className="mic-off-confirmation-back"
|
|
18
|
+
onClick={onBack}
|
|
19
|
+
>
|
|
20
|
+
<IconArrowLeft size={18} aria-hidden="true" />
|
|
21
|
+
Back
|
|
22
|
+
</button>
|
|
23
|
+
</header>
|
|
24
|
+
|
|
25
|
+
<div className="mic-off-confirmation-body">
|
|
26
|
+
<div className="mic-off-confirmation-visual" aria-hidden="true">
|
|
27
|
+
<IconMicrophone2 size={42} strokeWidth={1.7} />
|
|
28
|
+
<span className="mic-off-confirmation-slash" />
|
|
29
|
+
</div>
|
|
30
|
+
<h2 id="mic-off-title">Your mic is muted</h2>
|
|
31
|
+
<p>
|
|
32
|
+
To have sound in your video, you'll need to unmute your
|
|
33
|
+
microphone.
|
|
34
|
+
</p>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<footer className="mic-off-confirmation-actions">
|
|
38
|
+
<button type="button" className="secondary" onClick={onUnmute}>
|
|
39
|
+
Unmute
|
|
40
|
+
</button>
|
|
41
|
+
<button type="button" className="primary" onClick={onContinue}>
|
|
42
|
+
Continue
|
|
43
|
+
</button>
|
|
44
|
+
</footer>
|
|
45
|
+
</section>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
@@ -3150,6 +3150,116 @@ body[data-clips-route="recording-pill"] #root {
|
|
|
3150
3150
|
}
|
|
3151
3151
|
}
|
|
3152
3152
|
|
|
3153
|
+
/* ------------------------------------------------------------------------- */
|
|
3154
|
+
/* Microphone-off confirmation */
|
|
3155
|
+
/* ------------------------------------------------------------------------- */
|
|
3156
|
+
|
|
3157
|
+
.mic-off-confirmation {
|
|
3158
|
+
min-height: 430px;
|
|
3159
|
+
display: flex;
|
|
3160
|
+
flex-direction: column;
|
|
3161
|
+
background: var(--bg);
|
|
3162
|
+
}
|
|
3163
|
+
|
|
3164
|
+
.mic-off-confirmation-header {
|
|
3165
|
+
display: flex;
|
|
3166
|
+
align-items: center;
|
|
3167
|
+
padding: 16px 18px 8px;
|
|
3168
|
+
}
|
|
3169
|
+
|
|
3170
|
+
.mic-off-confirmation-back {
|
|
3171
|
+
display: inline-flex;
|
|
3172
|
+
align-items: center;
|
|
3173
|
+
gap: 6px;
|
|
3174
|
+
min-height: 30px;
|
|
3175
|
+
padding: 4px 6px 4px 2px;
|
|
3176
|
+
border: 0;
|
|
3177
|
+
border-radius: var(--radius-sm);
|
|
3178
|
+
background: transparent;
|
|
3179
|
+
color: var(--fg);
|
|
3180
|
+
cursor: pointer;
|
|
3181
|
+
font-size: 15px;
|
|
3182
|
+
font-weight: 650;
|
|
3183
|
+
transition:
|
|
3184
|
+
background 120ms,
|
|
3185
|
+
color 120ms;
|
|
3186
|
+
}
|
|
3187
|
+
|
|
3188
|
+
.mic-off-confirmation-back:hover {
|
|
3189
|
+
background: var(--surface-hover);
|
|
3190
|
+
}
|
|
3191
|
+
|
|
3192
|
+
.mic-off-confirmation-back:focus-visible {
|
|
3193
|
+
outline: none;
|
|
3194
|
+
box-shadow: 0 0 0 3px var(--brand-ring);
|
|
3195
|
+
}
|
|
3196
|
+
|
|
3197
|
+
.mic-off-confirmation-body {
|
|
3198
|
+
display: flex;
|
|
3199
|
+
flex: 1 1 auto;
|
|
3200
|
+
flex-direction: column;
|
|
3201
|
+
align-items: center;
|
|
3202
|
+
justify-content: center;
|
|
3203
|
+
padding: 24px 28px 30px;
|
|
3204
|
+
text-align: center;
|
|
3205
|
+
}
|
|
3206
|
+
|
|
3207
|
+
.mic-off-confirmation-visual {
|
|
3208
|
+
position: relative;
|
|
3209
|
+
display: flex;
|
|
3210
|
+
align-items: center;
|
|
3211
|
+
justify-content: center;
|
|
3212
|
+
width: 88px;
|
|
3213
|
+
height: 88px;
|
|
3214
|
+
border-radius: 50%;
|
|
3215
|
+
background: var(--surface-strong);
|
|
3216
|
+
color: var(--status-warn);
|
|
3217
|
+
}
|
|
3218
|
+
|
|
3219
|
+
.mic-off-confirmation-slash {
|
|
3220
|
+
position: absolute;
|
|
3221
|
+
width: 58px;
|
|
3222
|
+
height: 3px;
|
|
3223
|
+
border-radius: var(--radius-pill);
|
|
3224
|
+
background: var(--status-warn);
|
|
3225
|
+
transform: rotate(45deg);
|
|
3226
|
+
}
|
|
3227
|
+
|
|
3228
|
+
.mic-off-confirmation-body h2 {
|
|
3229
|
+
margin: 24px 0 0;
|
|
3230
|
+
font-size: 22px;
|
|
3231
|
+
font-weight: 700;
|
|
3232
|
+
letter-spacing: -0.02em;
|
|
3233
|
+
}
|
|
3234
|
+
|
|
3235
|
+
.mic-off-confirmation-body p {
|
|
3236
|
+
max-width: 300px;
|
|
3237
|
+
margin: 8px 0 0;
|
|
3238
|
+
color: var(--fg-muted);
|
|
3239
|
+
font-size: 14px;
|
|
3240
|
+
line-height: 1.55;
|
|
3241
|
+
}
|
|
3242
|
+
|
|
3243
|
+
.mic-off-confirmation-actions {
|
|
3244
|
+
display: grid;
|
|
3245
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
3246
|
+
gap: 10px;
|
|
3247
|
+
padding: 16px 18px 18px;
|
|
3248
|
+
border-top: 1px solid var(--border);
|
|
3249
|
+
background: var(--surface);
|
|
3250
|
+
}
|
|
3251
|
+
|
|
3252
|
+
.mic-off-confirmation-actions .primary,
|
|
3253
|
+
.mic-off-confirmation-actions .secondary {
|
|
3254
|
+
width: auto;
|
|
3255
|
+
min-width: 0;
|
|
3256
|
+
height: 42px;
|
|
3257
|
+
padding: 0 12px;
|
|
3258
|
+
border-radius: var(--radius);
|
|
3259
|
+
font-size: 13px;
|
|
3260
|
+
white-space: nowrap;
|
|
3261
|
+
}
|
|
3262
|
+
|
|
3153
3263
|
/* ------------------------------------------------------------------------- */
|
|
3154
3264
|
/* Alert dialog */
|
|
3155
3265
|
/* ------------------------------------------------------------------------- */
|
|
@@ -630,6 +630,60 @@ mod macos {
|
|
|
630
630
|
}
|
|
631
631
|
}
|
|
632
632
|
|
|
633
|
+
/// RNNoise's voice-activity output is more useful than a fixed amplitude
|
|
634
|
+
/// threshold for the meeting microphone. A muted call still leaves Clips
|
|
635
|
+
/// with the local microphone, and fans, keyboard noise, or room noise can
|
|
636
|
+
/// be loud enough to cross the old RMS-only gate. Keep this state on the
|
|
637
|
+
/// worker thread so the realtime capture callback stays allocation-free.
|
|
638
|
+
struct MicVoiceActivity {
|
|
639
|
+
state: Box<nnnoiseless::DenoiseState<'static>>,
|
|
640
|
+
pending: Vec<f32>,
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
impl MicVoiceActivity {
|
|
644
|
+
const SAMPLE_RATE: f32 = 48_000.0;
|
|
645
|
+
const INPUT_SCALE: f32 = 32_768.0;
|
|
646
|
+
|
|
647
|
+
fn new() -> Self {
|
|
648
|
+
Self {
|
|
649
|
+
state: nnnoiseless::DenoiseState::new(),
|
|
650
|
+
pending: Vec::new(),
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
fn reset(&mut self) {
|
|
655
|
+
self.state = nnnoiseless::DenoiseState::new();
|
|
656
|
+
self.pending.clear();
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/// Return the highest RNNoise speech probability in `samples`.
|
|
660
|
+
/// `None` means this capture format is not the 48 kHz PCM that
|
|
661
|
+
/// RNNoise expects; callers retain the existing RMS fallback for
|
|
662
|
+
/// those legacy devices.
|
|
663
|
+
fn observe(&mut self, samples: &[f32], sample_rate: f32) -> Option<f32> {
|
|
664
|
+
if (sample_rate - Self::SAMPLE_RATE).abs() > 1.0 {
|
|
665
|
+
self.reset();
|
|
666
|
+
return None;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
self.pending.extend(samples.iter().map(|sample| {
|
|
670
|
+
(sample * Self::INPUT_SCALE).clamp(-Self::INPUT_SCALE, Self::INPUT_SCALE)
|
|
671
|
+
}));
|
|
672
|
+
|
|
673
|
+
let frame_size = nnnoiseless::DenoiseState::FRAME_SIZE;
|
|
674
|
+
let mut highest_probability: f32 = 0.0;
|
|
675
|
+
while self.pending.len() >= frame_size {
|
|
676
|
+
let mut denoised = [0.0_f32; nnnoiseless::DenoiseState::FRAME_SIZE];
|
|
677
|
+
let probability = self
|
|
678
|
+
.state
|
|
679
|
+
.process_frame(&mut denoised, &self.pending[..frame_size]);
|
|
680
|
+
highest_probability = highest_probability.max(probability);
|
|
681
|
+
self.pending.drain(..frame_size);
|
|
682
|
+
}
|
|
683
|
+
Some(highest_probability)
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
633
687
|
/// Run whisper over `samples` (16 kHz mono f32), returning each speech
|
|
634
688
|
/// segment as `(start_ms, end_ms, text)` with buffer-relative timestamps.
|
|
635
689
|
/// `language` is the forced language code (e.g. "en"); `None` lets whisper
|
|
@@ -698,6 +752,10 @@ mod macos {
|
|
|
698
752
|
const VOICE_RMS_THRESHOLD: f32 = 0.006;
|
|
699
753
|
/// A second, model-level gate for ambient/no-speech Whisper segments.
|
|
700
754
|
const MAX_NO_SPEECH_PROBABILITY: f32 = 0.72;
|
|
755
|
+
/// RNNoise speech probability required for a 48 kHz microphone frame to
|
|
756
|
+
/// count as voice. System audio and non-48 kHz legacy mic captures keep
|
|
757
|
+
/// the RMS path because this VAD is trained for 48 kHz microphone PCM.
|
|
758
|
+
const MIN_MIC_VAD_PROBABILITY: f32 = 0.6;
|
|
701
759
|
/// Minimum average per-token probability for a segment to count as real
|
|
702
760
|
/// speech. On noisy/near-silent audio whisper mis-detects the language and
|
|
703
761
|
/// decodes fluent-looking gibberish in another language — but those tokens
|
|
@@ -733,6 +791,11 @@ mod macos {
|
|
|
733
791
|
sum / count as f32
|
|
734
792
|
}
|
|
735
793
|
|
|
794
|
+
fn mic_voice_detected(rms: f32, vad_probability: Option<f32>) -> bool {
|
|
795
|
+
rms > VOICE_RMS_THRESHOLD
|
|
796
|
+
&& vad_probability.is_none_or(|probability| probability >= MIN_MIC_VAD_PROBABILITY)
|
|
797
|
+
}
|
|
798
|
+
|
|
736
799
|
fn buffer_start_after_drain(now: Instant, pending: Duration) -> Instant {
|
|
737
800
|
now.checked_sub(pending).unwrap_or(now)
|
|
738
801
|
}
|
|
@@ -788,6 +851,10 @@ mod macos {
|
|
|
788
851
|
let mut seen_reset_generation = stream.reset_generation.load(Ordering::SeqCst);
|
|
789
852
|
// Growing-utterance resample cache — see `IncrementalResample`.
|
|
790
853
|
let mut resample_state = IncrementalResample::new();
|
|
854
|
+
// Only the microphone stream needs a speech/noise classifier. The
|
|
855
|
+
// system stream must preserve remote speech even when it is mixed
|
|
856
|
+
// with music or other non-voice audio.
|
|
857
|
+
let mut mic_voice_activity = (stream.source == "mic").then(MicVoiceActivity::new);
|
|
791
858
|
|
|
792
859
|
while stream.running.load(Ordering::SeqCst) {
|
|
793
860
|
std::thread::sleep(Duration::from_millis(250));
|
|
@@ -800,6 +867,9 @@ mod macos {
|
|
|
800
867
|
last_infer = Instant::now() - Duration::from_secs(10);
|
|
801
868
|
had_voice = false;
|
|
802
869
|
resample_state.drop_all();
|
|
870
|
+
if let Some(vad) = mic_voice_activity.as_mut() {
|
|
871
|
+
vad.reset();
|
|
872
|
+
}
|
|
803
873
|
continue;
|
|
804
874
|
}
|
|
805
875
|
|
|
@@ -807,31 +877,40 @@ mod macos {
|
|
|
807
877
|
// poll. Inspect that tail in place instead of cloning + resampling
|
|
808
878
|
// the entire growing utterance four times per second. Full snapshots
|
|
809
879
|
// are reserved for an inference that is actually due.
|
|
810
|
-
let (raw_len, new_rms) = match stream.buf.lock() {
|
|
880
|
+
let (raw_len, new_rms, new_mic_samples) = match stream.buf.lock() {
|
|
811
881
|
Ok(b) => {
|
|
812
882
|
let raw_len = b.len();
|
|
813
|
-
let new_rms = if raw_len > last_raw_len {
|
|
883
|
+
let (new_rms, new_mic_samples) = if raw_len > last_raw_len {
|
|
814
884
|
let new = &b[last_raw_len..];
|
|
815
|
-
|
|
885
|
+
(
|
|
886
|
+
Some(
|
|
887
|
+
(new.iter().map(|x| x * x).sum::<f32>() / new.len() as f32).sqrt(),
|
|
888
|
+
),
|
|
889
|
+
(stream.source == "mic").then(|| new.to_vec()),
|
|
890
|
+
)
|
|
816
891
|
} else {
|
|
817
|
-
None
|
|
892
|
+
(None, None)
|
|
818
893
|
};
|
|
819
|
-
(raw_len, new_rms)
|
|
894
|
+
(raw_len, new_rms, new_mic_samples)
|
|
820
895
|
}
|
|
821
896
|
Err(_) => continue,
|
|
822
897
|
};
|
|
823
898
|
if stream.reset_generation.load(Ordering::SeqCst) != seen_reset_generation {
|
|
824
899
|
continue;
|
|
825
900
|
}
|
|
901
|
+
let src_rate = (stream.src_rate.load(Ordering::SeqCst) as f32).max(1.0);
|
|
826
902
|
if let Some(rms) = new_rms {
|
|
827
|
-
|
|
903
|
+
let vad_probability = mic_voice_activity
|
|
904
|
+
.as_mut()
|
|
905
|
+
.zip(new_mic_samples.as_deref())
|
|
906
|
+
.and_then(|(vad, samples)| vad.observe(samples, src_rate));
|
|
907
|
+
if mic_voice_detected(rms, vad_probability) {
|
|
828
908
|
last_voice = Instant::now();
|
|
829
909
|
had_voice = true;
|
|
830
910
|
}
|
|
831
911
|
}
|
|
832
912
|
last_raw_len = raw_len;
|
|
833
913
|
|
|
834
|
-
let src_rate = (stream.src_rate.load(Ordering::SeqCst) as f32).max(1.0);
|
|
835
914
|
let have_secs = raw_len as f32 / src_rate;
|
|
836
915
|
let silence = last_voice.elapsed();
|
|
837
916
|
|
|
@@ -878,6 +957,9 @@ mod macos {
|
|
|
878
957
|
// so the resample cache is invalid regardless of whether this
|
|
879
958
|
// utterance ran inference — rebuild fresh from whatever's left.
|
|
880
959
|
resample_state.drop_all();
|
|
960
|
+
if let Some(vad) = mic_voice_activity.as_mut() {
|
|
961
|
+
vad.reset();
|
|
962
|
+
}
|
|
881
963
|
// Advance the timeline offset so the next utterance's whisper
|
|
882
964
|
// timestamps map correctly. Inference can take seconds, and
|
|
883
965
|
// audio kept arriving throughout it, so the new buffer starts
|
|
@@ -1568,6 +1650,31 @@ mod macos {
|
|
|
1568
1650
|
);
|
|
1569
1651
|
}
|
|
1570
1652
|
|
|
1653
|
+
#[test]
|
|
1654
|
+
fn mic_voice_gate_requires_speech_probability_on_supported_captures() {
|
|
1655
|
+
assert!(!super::mic_voice_detected(0.02, Some(0.59)));
|
|
1656
|
+
assert!(!super::mic_voice_detected(0.004, Some(0.99)));
|
|
1657
|
+
assert!(super::mic_voice_detected(0.02, Some(0.6)));
|
|
1658
|
+
assert!(super::mic_voice_detected(0.02, None));
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
#[test]
|
|
1662
|
+
fn rnnoise_vad_rejects_steady_ambient_noise() {
|
|
1663
|
+
let mut vad = super::MicVoiceActivity::new();
|
|
1664
|
+
let samples: Vec<f32> = (0..(nnnoiseless::DenoiseState::FRAME_SIZE * 8))
|
|
1665
|
+
.map(|index| {
|
|
1666
|
+
let value = index.wrapping_mul(1_103_515_245usize).wrapping_add(12_345);
|
|
1667
|
+
(((value >> 16) & 0x7fff) as f32 / 16_384.0 - 1.0) * 0.012
|
|
1668
|
+
})
|
|
1669
|
+
.collect();
|
|
1670
|
+
|
|
1671
|
+
let probability = vad.observe(&samples, 48_000.0).unwrap();
|
|
1672
|
+
assert!(
|
|
1673
|
+
probability < super::MIN_MIC_VAD_PROBABILITY,
|
|
1674
|
+
"ambient noise was classified as speech: {probability:.3}"
|
|
1675
|
+
);
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1571
1678
|
#[test]
|
|
1572
1679
|
fn filters_multilingual_caption_hallucinations() {
|
|
1573
1680
|
assert_eq!(clean_transcript("ご視聴ありがとうございました"), None);
|
|
@@ -47,29 +47,34 @@ export type ScreenCaptureDisplayOptions = {
|
|
|
47
47
|
/**
|
|
48
48
|
* The `getDisplayMedia` options for a screen recording.
|
|
49
49
|
*
|
|
50
|
-
* Screen/tab audio is requested
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
* never had the problem.
|
|
50
|
+
* Screen/tab audio is requested whenever the microphone is enabled, and
|
|
51
|
+
* withheld when it's off. The mic toggle is the only audio control the
|
|
52
|
+
* recorder UI shows, so a user who turns it off reasonably expects the
|
|
53
|
+
* recording to capture no audio at all — not just their voice. Requesting
|
|
54
|
+
* system audio anyway relies on the user separately noticing and unchecking
|
|
55
|
+
* the browser's native "share tab audio" checkbox, which they may not even
|
|
56
|
+
* know exists. See Slack thread 1786086902028429: a user turned the mic off
|
|
57
|
+
* and still got system audio in the recording; the agreed fix is to gate
|
|
58
|
+
* system audio on the same toggle.
|
|
60
59
|
*
|
|
61
|
-
*
|
|
60
|
+
* When the mic IS on, audio is still requested unconditionally (not made
|
|
61
|
+
* conditional on some separate "include system audio" flag) — that used to be
|
|
62
|
+
* one toggle governing both, and with the mic off Chrome never even offered
|
|
63
|
+
* the checkbox, so 345 production recordings landed with `has_audio = false`
|
|
64
|
+
* while recording a call/demo. Declining the checkbox is still respected; the
|
|
65
|
+
* user simply gets the choice whenever the mic is on.
|
|
62
66
|
*/
|
|
63
67
|
export function screenCaptureDisplayOptions(
|
|
64
68
|
displaySurface: ScreenCaptureSurface,
|
|
69
|
+
wantsMic: boolean,
|
|
65
70
|
): ScreenCaptureDisplayOptions {
|
|
66
71
|
return {
|
|
67
72
|
video: screenCaptureVideoConstraints(displaySurface),
|
|
68
|
-
audio:
|
|
73
|
+
audio: wantsMic,
|
|
69
74
|
// Let "Browser tab" open the tab picker. preferCurrentTab turns it into a
|
|
70
75
|
// current-tab shortcut, which makes choosing another tab harder.
|
|
71
76
|
selfBrowserSurface: displaySurface === "browser" ? "include" : "exclude",
|
|
72
77
|
surfaceSwitching: "include",
|
|
73
|
-
systemAudio: "include",
|
|
78
|
+
systemAudio: wantsMic ? "include" : "exclude",
|
|
74
79
|
};
|
|
75
80
|
}
|
|
@@ -50,7 +50,7 @@ The agent modifies data in SQL, but the UI runs in the browser. SSE bridges same
|
|
|
50
50
|
|
|
51
51
|
For list/sidebar queries, use the same pattern — pass the counter into the queryKey of every list query you want to keep fresh.
|
|
52
52
|
|
|
53
|
-
4. **Fallback** polling calls `/_agent-native/poll?since=N`. It
|
|
53
|
+
4. **Fallback** polling calls `/_agent-native/poll?since=N`. It polls every 2 seconds only while an agent run is in flight; otherwise it idles at 60 seconds whether or not SSE is connected (`SSE_FALLBACK_INTERVAL_MS` and `IDLE_POLL_INTERVAL_MS`, both 60 s). A hidden tab polls no faster than every 10 seconds (`HIDDEN_POLL_INTERVAL_MS`). Polling is the universal serverless fallback: new framework writes are read from the durable `sync_events` log, while the older DB timestamp scan remains as a slower safety net for direct SQL writes and older processes.
|
|
54
54
|
|
|
55
55
|
5. When the agent writes to the database, the version increments, SSE/polling detects it, and React Query refetches the affected queries.
|
|
56
56
|
|