@agent-native/core 0.157.0 → 0.157.1

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 (111) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/server/jobs/analytics-bigquery-backfill.ts +19 -4
  3. package/corpus/templates/analytics/server/lib/first-party-analytics-backend.ts +7 -2
  4. package/corpus/templates/assets/app/global.css +4 -0
  5. package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +1 -2
  6. package/corpus/templates/brain/app/routes/sources.tsx +1 -2
  7. package/corpus/templates/calendar/app/hooks/use-bookings.ts +12 -2
  8. package/corpus/templates/calendar/app/pages/BookingLinksPage.tsx +64 -2
  9. package/corpus/templates/calendar/server/handlers/bookings.ts +219 -93
  10. package/corpus/templates/calendar/server/plugins/auth.ts +0 -2
  11. package/corpus/templates/clips/actions/create-folder.ts +7 -1
  12. package/corpus/templates/clips/actions/create-meeting.ts +6 -2
  13. package/corpus/templates/clips/actions/create-recording.ts +5 -3
  14. package/corpus/templates/clips/actions/finalize-recording.ts +97 -65
  15. package/corpus/templates/clips/actions/import-loom-recording.ts +5 -3
  16. package/corpus/templates/clips/actions/request-recording-access.ts +272 -0
  17. package/corpus/templates/clips/actions/stitch-recordings.ts +5 -3
  18. package/corpus/templates/clips/app/components/meetings/share-meeting-dialog.tsx +38 -63
  19. package/corpus/templates/clips/app/components/player/share-dialog.tsx +75 -75
  20. package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +3 -21
  21. package/corpus/templates/clips/app/components/sharing/share-ui.tsx +13 -30
  22. package/corpus/templates/clips/app/i18n/en-US.ts +10 -0
  23. package/corpus/templates/clips/app/routes/_app.meetings.$meetingId.tsx +5 -5
  24. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +9 -16
  25. package/corpus/templates/clips/app/routes/share.$shareId.tsx +135 -18
  26. package/corpus/templates/clips/changelog/2026-08-14-long-resumable-uploads-now-abort-cleanly-and-show-errors.md +6 -0
  27. package/corpus/templates/clips/changelog/2026-08-14-standardized-share-popovers.md +6 -0
  28. package/corpus/templates/clips/chrome-extension/public/manifest.json +11 -3
  29. package/corpus/templates/clips/chrome-extension/src/github-preview-content.ts +37 -8
  30. package/corpus/templates/clips/chrome-extension/src/github-preview.ts +3 -3
  31. package/corpus/templates/clips/chrome-extension/src/offscreen.ts +8 -25
  32. package/corpus/templates/clips/desktop/src/app.tsx +24 -0
  33. package/corpus/templates/clips/desktop/src/lib/media-capture-constraints.ts +42 -13
  34. package/corpus/templates/clips/desktop/src/lib/recorder.ts +11 -18
  35. package/corpus/templates/clips/desktop/src/overlays/onboarding.tsx +1 -0
  36. package/corpus/templates/clips/desktop/src/shared/config.ts +1 -0
  37. package/corpus/templates/clips/desktop/src/styles.css +24 -9
  38. package/corpus/templates/clips/desktop/src-tauri/Cargo.toml +3 -0
  39. package/corpus/templates/clips/desktop/src-tauri/src/config.rs +8 -0
  40. package/corpus/templates/clips/desktop/src-tauri/src/native_screen/custom_capture.rs +154 -176
  41. package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +115 -31
  42. package/corpus/templates/clips/desktop/src-tauri/src/rewind_clip.rs +7 -0
  43. package/corpus/templates/clips/server/db/index.ts +1 -0
  44. package/corpus/templates/clips/server/db/schema.ts +1 -0
  45. package/corpus/templates/clips/server/lib/calendar-event-meetings.ts +6 -1
  46. package/corpus/templates/clips/server/lib/emails.ts +23 -0
  47. package/corpus/templates/clips/server/lib/public-agent-context.ts +55 -0
  48. package/corpus/templates/clips/server/lib/recordings.ts +2 -1
  49. package/corpus/templates/clips/server/lib/resumable-upload-cleanup.ts +49 -0
  50. package/corpus/templates/clips/server/lib/s3-upload-provider.ts +34 -7
  51. package/corpus/templates/clips/server/lib/upload-interruption.ts +15 -1
  52. package/corpus/templates/clips/server/lib/upload-lease.ts +94 -9
  53. package/corpus/templates/clips/server/routes/api/agent-context.json.get.ts +82 -7
  54. package/corpus/templates/clips/server/routes/api/public-recording.get.ts +1 -0
  55. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +71 -12
  56. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/interrupt.post.ts +4 -3
  57. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/reset-chunks.post.ts +19 -0
  58. package/corpus/templates/clips/server/routes/api/uploads/reap.post.ts +8 -1
  59. package/corpus/templates/clips/shared/recording-link.ts +4 -0
  60. package/corpus/templates/clips/shared/share-loader-response.ts +2 -1
  61. package/corpus/templates/content/app/components/editor/DocumentToolbar.tsx +7 -16
  62. package/corpus/templates/factory/AGENTS.md +5 -1
  63. package/corpus/templates/factory/README.md +6 -2
  64. package/corpus/templates/factory/actions/view-screen.ts +15 -0
  65. package/corpus/templates/factory/app/components/factory/FactoryAgentsView.tsx +58 -189
  66. package/corpus/templates/factory/app/components/layout/Header.tsx +1 -0
  67. package/corpus/templates/factory/app/components/layout/Layout.tsx +1 -1
  68. package/corpus/templates/factory/app/components/layout/Sidebar.tsx +7 -0
  69. package/corpus/templates/factory/app/hooks/use-navigation-state.ts +3 -0
  70. package/corpus/templates/factory/app/i18n/en-US.ts +3 -1
  71. package/corpus/templates/factory/app/routes/agents.tsx +9 -0
  72. package/corpus/templates/mail/server/plugins/auth.ts +0 -2
  73. package/corpus/templates/plan/app/components/plan/LocalCodebasePicker.tsx +50 -14
  74. package/corpus/templates/plan/app/pages/PlansPage.tsx +9 -19
  75. package/corpus/templates/slides/app/components/editor/HistoryPanel.tsx +7 -1
  76. package/corpus/templates/slides/app/context/DeckContext.tsx +71 -22
  77. package/corpus/templates/slides/app/lib/editor-shortcuts.ts +46 -0
  78. package/corpus/templates/slides/app/pages/DeckEditor.tsx +13 -0
  79. package/dist/client/AgentPanel.d.ts +13 -1
  80. package/dist/client/AgentPanel.js +12 -10
  81. package/dist/client/AssistantChat.js +25 -5
  82. package/dist/client/HighlightedCodeBlock.js +12 -2
  83. package/dist/client/agent-chat/index.d.ts +1 -0
  84. package/dist/client/agent-chat/index.js +1 -0
  85. package/dist/client/chat/markdown-renderer.js +16 -3
  86. package/dist/client/chat-first/apps-rail.js +1 -1
  87. package/dist/client/components/ui/message-scroller.js +1 -1
  88. package/dist/client/settings/AccountSettingsCard.js +48 -19
  89. package/dist/client/sharing/ShareButton.d.ts +1 -2
  90. package/dist/client/sharing/ShareButton.js +5 -41
  91. package/dist/client/sharing/ShareDialog.js +5 -4
  92. package/dist/client/use-chat-threads.js +10 -14
  93. package/dist/collab/struct-routes.d.ts +1 -1
  94. package/dist/localization/default-messages.d.ts +1 -0
  95. package/dist/localization/default-messages.js +1 -0
  96. package/dist/progress/routes.d.ts +1 -1
  97. package/dist/provider-api/actions/custom-provider-registration.d.ts +11 -11
  98. package/dist/provider-api/actions/provider-api.d.ts +15 -15
  99. package/dist/server/auth-marketing.d.ts +1 -0
  100. package/dist/server/auth-marketing.js +0 -2
  101. package/dist/server/auth.d.ts +1 -0
  102. package/dist/server/auth.js +18 -0
  103. package/dist/server/onboarding-html.d.ts +1 -0
  104. package/dist/server/onboarding-html.js +2 -88
  105. package/dist/server/realtime-token.d.ts +1 -1
  106. package/dist/shared/markdown-block-split.d.ts +16 -0
  107. package/dist/shared/markdown-block-split.js +81 -5
  108. package/dist/styles/agent-native.css +13 -0
  109. package/dist/voice/voice-cleanup-prompt.js +1 -0
  110. package/package.json +2 -2
  111. package/corpus/templates/clips/shared/mic-audio-cleanup.ts +0 -350
@@ -22,7 +22,6 @@ import {
22
22
  isLikelyPhoneMicLabel,
23
23
  type AudioInputFallback,
24
24
  } from "@shared/media-device-selection";
25
- import { createMicAudioCleanup } from "@shared/mic-audio-cleanup";
26
25
  import { scheduleReadyChime } from "@shared/recording-audio";
27
26
  import {
28
27
  SCREEN_CAPTURE_FRAME_RATE,
@@ -435,14 +434,9 @@ async function getCameraStream(deviceId: string): Promise<MediaStream> {
435
434
  }
436
435
  }
437
436
 
438
- // Phones/Continuity-style mics already apply their own echo cancellation,
439
- // noise suppression, and gain control before the audio ever reaches Chrome.
440
- // Stacking Chrome's versions of the same processing on top double-processes
441
- // the signal and audibly degrades it. We can't detect "is this device doing
442
- // its own processing" directly, so we use the device label as a heuristic and
443
- // ask the browser to SKIP its own noise suppression / AGC for those devices
444
- // (`{ ideal: false }` — a best-effort request, not a hard requirement). Echo
445
- // cancellation stays on for everything.
437
+ // Let the browser apply its built-in voice processing. Device labels are not a
438
+ // reliable signal for whether a hardware path already processed its audio, so
439
+ // do not branch capture quality on a label heuristic.
446
440
  async function chooseFallbackMicDevice(
447
441
  requestedLabel: string,
448
442
  avoidDeviceIds: string[] = [],
@@ -459,13 +453,12 @@ async function chooseFallbackMicDevice(
459
453
 
460
454
  function voiceFocusedAudioConstraints(
461
455
  deviceId: string,
462
- deviceLabel = "",
456
+ _deviceLabel = "",
463
457
  ): MediaTrackConstraints {
464
- const skipRedundantProcessing = isLikelyPhoneMicLabel(deviceLabel);
465
458
  const audio: MediaTrackConstraints = {
466
459
  echoCancellation: true,
467
- noiseSuppression: skipRedundantProcessing ? { ideal: false } : true,
468
- autoGainControl: skipRedundantProcessing ? { ideal: false } : true,
460
+ noiseSuppression: true,
461
+ autoGainControl: true,
469
462
  channelCount: 1,
470
463
  };
471
464
  if (deviceId) audio.deviceId = { exact: deviceId };
@@ -811,20 +804,11 @@ async function createMixedAudio(
811
804
  const audioContext = new AudioContext();
812
805
  await audioContext.resume().catch(() => undefined);
813
806
  const destination = audioContext.createMediaStreamDestination();
814
- const micCleanup: Array<{ stop: () => void }> = [];
815
807
  for (const input of audioInputs) {
816
- let track = input.track;
817
- if (input.isMicrophone) {
818
- const cleanup = createMicAudioCleanup(new MediaStream([track]), {
819
- audioContext,
820
- });
821
- micCleanup.push(cleanup);
822
- track = cleanup.stream.getAudioTracks()[0] ?? track;
823
- }
824
808
  // One source per track (not per stream) so each input is isolated in the
825
809
  // mix graph and can be detached independently.
826
810
  const source = audioContext.createMediaStreamSource(
827
- new MediaStream([track]),
811
+ new MediaStream([input.track]),
828
812
  );
829
813
  source.connect(destination);
830
814
  // A mixed input track can end mid-recording — most commonly the shared
@@ -832,7 +816,7 @@ async function createMixedAudio(
832
816
  // just that dead source so the mixed output keeps carrying the surviving
833
817
  // inputs (the microphone). Without this the whole mixed destination can go
834
818
  // silent, dropping both the tab audio and the mic for the rest of the clip.
835
- track.addEventListener("ended", () => {
819
+ input.track.addEventListener("ended", () => {
836
820
  try {
837
821
  source.disconnect();
838
822
  } catch {
@@ -847,7 +831,6 @@ async function createMixedAudio(
847
831
  cleanup() {
848
832
  if (cleanedUp) return;
849
833
  cleanedUp = true;
850
- for (const cleanup of micCleanup) cleanup.stop();
851
834
  },
852
835
  };
853
836
  }
@@ -977,6 +977,7 @@ export function App() {
977
977
  );
978
978
  const localRecordingMode: LocalRecordingMode =
979
979
  featureConfig?.localRecordingMode ?? "off";
980
+ const voiceCleanupEnabled = featureConfig?.voiceCleanupEnabled !== false;
980
981
 
981
982
  const [pendingUploads, setPendingUploads] = useState<PendingDesktopUpload[]>(
982
983
  [],
@@ -3160,6 +3161,7 @@ export function App() {
3160
3161
  cameraOn,
3161
3162
  micOn,
3162
3163
  systemAudioOn,
3164
+ voiceCleanupEnabled,
3163
3165
  localRecordingMode,
3164
3166
  preAcquiredCameraStream,
3165
3167
  preAcquiredDisplayStream: options?.resumeCapture?.displayStream ?? null,
@@ -5391,6 +5393,7 @@ function Setup({
5391
5393
  const featureConfig = useFeatureConfig();
5392
5394
  const updateStatus = useUpdateStatus();
5393
5395
  const voiceEnabled = featureConfig?.voiceEnabled !== false;
5396
+ const voiceCleanupEnabled = featureConfig?.voiceCleanupEnabled !== false;
5394
5397
  const meetingsEnabled = featureConfig?.meetingsEnabled !== false;
5395
5398
  const launchAtLoginEnabled = featureConfig?.launchAtLoginEnabled !== false;
5396
5399
  const autoHidePopoverEnabled = featureConfig?.autoHidePopoverEnabled === true;
@@ -5540,6 +5543,15 @@ function Setup({
5540
5543
  );
5541
5544
  }
5542
5545
 
5546
+ function setVoiceCleanupEnabled(enabled: boolean) {
5547
+ if (!featureConfig) return;
5548
+ invoke("set_feature_config", {
5549
+ config: { ...featureConfig, voiceCleanupEnabled: enabled },
5550
+ }).catch((err) =>
5551
+ console.error("[settings] set_feature_config failed", err),
5552
+ );
5553
+ }
5554
+
5543
5555
  function setMeetingsEnabled(enabled: boolean) {
5544
5556
  if (!featureConfig) return;
5545
5557
  invoke("set_feature_config", {
@@ -7258,6 +7270,18 @@ function Setup({
7258
7270
  title="Capture"
7259
7271
  description="Choose how recordings are saved and how the tray opens them."
7260
7272
  >
7273
+ <DesktopSettingsRow
7274
+ label="Voice cleanup"
7275
+ description="Reduce steady background noise in microphone recordings."
7276
+ control={
7277
+ <Switch
7278
+ on={voiceCleanupEnabled}
7279
+ onChange={setVoiceCleanupEnabled}
7280
+ disabled={captureControlsLocked}
7281
+ label="Use voice cleanup"
7282
+ />
7283
+ }
7284
+ />
7261
7285
  <DesktopSettingsRow
7262
7286
  label="Rewind"
7263
7287
  description={rewindStatusPresentation.title}
@@ -13,12 +13,22 @@ export const VOICE_FOCUSED_AUDIO_CONSTRAINTS: MediaTrackConstraints = {
13
13
  channelCount: { ideal: 1 },
14
14
  };
15
15
 
16
+ const RAW_AUDIO_CONSTRAINTS: MediaTrackConstraints = {
17
+ echoCancellation: false,
18
+ noiseSuppression: false,
19
+ autoGainControl: false,
20
+ channelCount: { ideal: 1 },
21
+ };
22
+
16
23
  export function voiceFocusedAudioConstraints(
17
24
  deviceId?: string | null,
25
+ voiceCleanupEnabled = true,
18
26
  ): MediaTrackConstraints {
19
27
  const id = deviceId?.trim();
20
28
  return {
21
- ...VOICE_FOCUSED_AUDIO_CONSTRAINTS,
29
+ ...(voiceCleanupEnabled
30
+ ? VOICE_FOCUSED_AUDIO_CONSTRAINTS
31
+ : RAW_AUDIO_CONSTRAINTS),
22
32
  ...(id ? { deviceId: { exact: id } } : {}),
23
33
  };
24
34
  }
@@ -73,9 +83,10 @@ function stopStream(stream: MediaStream): void {
73
83
 
74
84
  async function getVoiceFocusedAudioStream(
75
85
  deviceId?: string | null,
86
+ voiceCleanupEnabled = true,
76
87
  ): Promise<MediaStream> {
77
88
  return navigator.mediaDevices.getUserMedia({
78
- audio: voiceFocusedAudioConstraints(deviceId),
89
+ audio: voiceFocusedAudioConstraints(deviceId, voiceCleanupEnabled),
79
90
  video: false,
80
91
  });
81
92
  }
@@ -101,6 +112,7 @@ async function fallbackAudioInput(
101
112
  async function tryFallbackAudioInput(
102
113
  savedLabel: string | null | undefined,
103
114
  avoidDeviceIds: Array<string | null | undefined>,
115
+ voiceCleanupEnabled = true,
104
116
  ): Promise<MediaStream | null> {
105
117
  const fallback = await fallbackAudioInput(savedLabel, avoidDeviceIds);
106
118
  if (!fallback) return null;
@@ -109,7 +121,10 @@ async function tryFallbackAudioInput(
109
121
  `[clips-recorder] selected mic unavailable; retrying ${fallback.reason} microphone`,
110
122
  { deviceId: fallback.deviceId, label: fallback.label },
111
123
  );
112
- return await getVoiceFocusedAudioStream(fallback.deviceId);
124
+ return await getVoiceFocusedAudioStream(
125
+ fallback.deviceId,
126
+ voiceCleanupEnabled,
127
+ );
113
128
  } catch (fallbackErr) {
114
129
  if (!isMediaConstraintFailure(fallbackErr)) throw fallbackErr;
115
130
  console.warn(
@@ -122,9 +137,10 @@ async function tryFallbackAudioInput(
122
137
 
123
138
  async function getDefaultAudioStreamWithBasicFallback(
124
139
  reason: unknown,
140
+ voiceCleanupEnabled = true,
125
141
  ): Promise<MediaStream> {
126
142
  try {
127
- return await getVoiceFocusedAudioStream();
143
+ return await getVoiceFocusedAudioStream(undefined, voiceCleanupEnabled);
128
144
  } catch (fallbackErr) {
129
145
  if (!isMediaConstraintFailure(fallbackErr)) throw fallbackErr;
130
146
  console.warn(
@@ -132,7 +148,7 @@ async function getDefaultAudioStreamWithBasicFallback(
132
148
  fallbackErr,
133
149
  );
134
150
  return navigator.mediaDevices.getUserMedia({
135
- audio: true,
151
+ audio: voiceCleanupEnabled ? true : RAW_AUDIO_CONSTRAINTS,
136
152
  video: false,
137
153
  });
138
154
  } finally {
@@ -147,13 +163,16 @@ async function getDefaultAudioStreamWithBasicFallback(
147
163
 
148
164
  async function replacePhoneDefaultMicIfPossible(
149
165
  stream: MediaStream,
166
+ voiceCleanupEnabled = true,
150
167
  ): Promise<MediaStream> {
151
168
  const track = stream.getAudioTracks()[0];
152
169
  if (!track || !isLikelyPhoneMicLabel(track.label)) return stream;
153
170
  const settings = track.getSettings?.();
154
- const replacement = await tryFallbackAudioInput(null, [
155
- settings?.deviceId ?? null,
156
- ]);
171
+ const replacement = await tryFallbackAudioInput(
172
+ null,
173
+ [settings?.deviceId ?? null],
174
+ voiceCleanupEnabled,
175
+ );
157
176
  if (!replacement) return stream;
158
177
  console.warn(
159
178
  "[clips-recorder] default mic resolved to a phone-like input; using explicit fallback mic",
@@ -198,12 +217,15 @@ export async function getCameraStreamWithFallback(
198
217
  export async function getAudioStreamWithFallback(
199
218
  deviceId?: string | null,
200
219
  savedLabel?: string | null,
220
+ voiceCleanupEnabled = true,
201
221
  ): Promise<MediaStream> {
202
222
  const id = deviceId?.trim();
203
223
 
204
224
  try {
205
- const stream = await getVoiceFocusedAudioStream(id);
206
- return id ? stream : replacePhoneDefaultMicIfPossible(stream);
225
+ const stream = await getVoiceFocusedAudioStream(id, voiceCleanupEnabled);
226
+ return id
227
+ ? stream
228
+ : replacePhoneDefaultMicIfPossible(stream, voiceCleanupEnabled);
207
229
  } catch (err) {
208
230
  let deviceGone = false;
209
231
  if (id && err instanceof DOMException && err.name === "NotAllowedError") {
@@ -227,11 +249,18 @@ export async function getAudioStreamWithFallback(
227
249
  }
228
250
  if (!isMediaConstraintFailure(err) && !deviceGone) throw err;
229
251
  if (id) {
230
- const fallback = await tryFallbackAudioInput(savedLabel, [id]);
252
+ const fallback = await tryFallbackAudioInput(
253
+ savedLabel,
254
+ [id],
255
+ voiceCleanupEnabled,
256
+ );
231
257
  if (fallback) return fallback;
232
258
  }
233
259
 
234
- const defaultStream = await getDefaultAudioStreamWithBasicFallback(err);
235
- return replacePhoneDefaultMicIfPossible(defaultStream);
260
+ const defaultStream = await getDefaultAudioStreamWithBasicFallback(
261
+ err,
262
+ voiceCleanupEnabled,
263
+ );
264
+ return replacePhoneDefaultMicIfPossible(defaultStream, voiceCleanupEnabled);
236
265
  }
237
266
  }
@@ -55,10 +55,6 @@ import {
55
55
  waitForAcceptedRecordingAfterFinalizeError,
56
56
  waitForReadyRecordingAfterFinalizeError,
57
57
  } from "../../../shared/finalize-recovery";
58
- import {
59
- createMicAudioCleanup,
60
- type MicAudioCleanupHandle,
61
- } from "../../../shared/mic-audio-cleanup";
62
58
  import type { LocalRecordingMode } from "../shared/config";
63
59
  import { createAudioCue, type AudioCue } from "./audio-cue";
64
60
  import { createCameraCompositeStream } from "./camera-composite";
@@ -239,6 +235,8 @@ export interface StartParams {
239
235
  cameraOn: boolean;
240
236
  /** Record + transcribe system/desktop audio. Default true. */
241
237
  systemAudioOn?: boolean;
238
+ /** Apply the browser/native voice cleanup path to microphone audio. */
239
+ voiceCleanupEnabled?: boolean;
242
240
  /** Cancels or bounds the pre-record capture setup. */
243
241
  signal?: AbortSignal;
244
242
  localRecordingMode?: LocalRecordingMode;
@@ -548,9 +546,9 @@ interface RecordingAudio {
548
546
  }
549
547
 
550
548
  /**
551
- * Build the audio track(s) for the recording. Microphone tracks pass through
552
- * the cleanup graph whether or not system/display audio is also present, then
553
- * both sources are mixed into one track when needed.
549
+ * Build the audio track(s) for the recording. Browser capture requests its
550
+ * built-in voice processing; this graph only combines raw source tracks when
551
+ * MediaRecorder needs one mixed track.
554
552
  */
555
553
  function buildRecordingAudio(
556
554
  micTracks: MediaStreamTrack[],
@@ -571,22 +569,13 @@ function buildRecordingAudio(
571
569
  }
572
570
  const ctx: AudioContext = new AudioCtx();
573
571
  const destination = ctx.createMediaStreamDestination();
574
- const micCleanup: MicAudioCleanupHandle[] = [];
575
- const cleanedMicTracks = micTracks.map((track) => {
576
- const cleanup = createMicAudioCleanup(new MediaStream([track]), {
577
- audioContext: ctx,
578
- });
579
- micCleanup.push(cleanup);
580
- return cleanup.stream.getAudioTracks()[0] ?? track;
581
- });
582
- for (const tracks of [cleanedMicTracks, systemTracks]) {
572
+ for (const tracks of [micTracks, systemTracks]) {
583
573
  if (!tracks.length) continue;
584
574
  ctx.createMediaStreamSource(new MediaStream(tracks)).connect(destination);
585
575
  }
586
576
  return {
587
577
  tracks: destination.stream.getAudioTracks(),
588
578
  cleanup() {
589
- for (const cleanup of micCleanup) cleanup.stop();
590
579
  ctx.close().catch(() => {});
591
580
  },
592
581
  };
@@ -4307,7 +4296,11 @@ async function startRecordingInner(
4307
4296
  const audioStreamPromise: Promise<MediaStream> | null = resumedAudioStream
4308
4297
  ? Promise.resolve(resumedAudioStream)
4309
4298
  : wantsAudio
4310
- ? getAudioStreamWithFallback(params.micId, params.micLabel)
4299
+ ? getAudioStreamWithFallback(
4300
+ params.micId,
4301
+ params.micLabel,
4302
+ params.voiceCleanupEnabled,
4303
+ )
4311
4304
  : null;
4312
4305
 
4313
4306
  // getDisplayMedia can remain pending in a long-lived WebKit tray webview.
@@ -127,6 +127,7 @@ export function Onboarding() {
127
127
  clipsEnabled: clips,
128
128
  meetingsEnabled: meetings,
129
129
  voiceEnabled: voice,
130
+ voiceCleanupEnabled: true,
130
131
  launchAtLoginEnabled: true,
131
132
  autoHidePopoverEnabled: false,
132
133
  meetingTranscriptionMode: "ask",
@@ -120,6 +120,7 @@ export interface FeatureConfig {
120
120
  clipsEnabled: boolean;
121
121
  meetingsEnabled: boolean;
122
122
  voiceEnabled: boolean;
123
+ voiceCleanupEnabled: boolean;
123
124
  launchAtLoginEnabled: boolean;
124
125
  autoHidePopoverEnabled: boolean;
125
126
  meetingTranscriptionMode: "manual" | "ask" | "auto";
@@ -6074,9 +6074,14 @@ body[data-clips-route="recording-pill"] #root {
6074
6074
  /* No fill, and the stroke is painted OUTWARD with an outset box-shadow so it
6075
6075
  lands just outside the captured rect — never inside the recorded video. */
6076
6076
  box-shadow:
6077
- 0 0 0 2px rgba(239, 68, 68, 0.95),
6078
- 0 0 0 4px rgba(239, 68, 68, 0.32),
6079
- 0 0 26px 6px rgba(239, 68, 68, 0.26);
6077
+ /* guard:allow-raw-color — recording frame uses fixed capture-safe contrast. */
6078
+ 0 0 0 1px rgba(255, 255, 255, 0.96),
6079
+ /* guard:allow-raw-color — recording frame uses fixed capture-safe contrast. */
6080
+ 0 0 0 3px rgba(220, 38, 38, 0.98),
6081
+ /* guard:allow-raw-color — recording frame uses fixed capture-safe contrast. */
6082
+ 0 0 0 6px rgba(17, 24, 39, 0.78),
6083
+ /* guard:allow-raw-color — recording frame uses fixed capture-safe contrast. */
6084
+ 0 0 30px 8px rgba(220, 38, 38, 0.28);
6080
6085
  animation: region-record-border-pulse 1.8s ease-in-out infinite;
6081
6086
  }
6082
6087
 
@@ -6086,15 +6091,25 @@ body[data-clips-route="recording-pill"] #root {
6086
6091
  0%,
6087
6092
  100% {
6088
6093
  box-shadow:
6089
- 0 0 0 2px rgba(239, 68, 68, 0.9),
6090
- 0 0 0 4px rgba(239, 68, 68, 0.28),
6091
- 0 0 22px 5px rgba(239, 68, 68, 0.2);
6094
+ /* guard:allow-raw-color — recording frame uses fixed capture-safe contrast. */
6095
+ 0 0 0 1px rgba(255, 255, 255, 0.98),
6096
+ /* guard:allow-raw-color — recording frame uses fixed capture-safe contrast. */
6097
+ 0 0 0 3px rgba(220, 38, 38, 1),
6098
+ /* guard:allow-raw-color — recording frame uses fixed capture-safe contrast. */
6099
+ 0 0 0 6px rgba(17, 24, 39, 0.76),
6100
+ /* guard:allow-raw-color — recording frame uses fixed capture-safe contrast. */
6101
+ 0 0 24px 7px rgba(220, 38, 38, 0.22);
6092
6102
  }
6093
6103
  50% {
6094
6104
  box-shadow:
6095
- 0 0 0 2px rgba(239, 68, 68, 1),
6096
- 0 0 0 4px rgba(239, 68, 68, 0.44),
6097
- 0 0 30px 8px rgba(239, 68, 68, 0.34);
6105
+ /* guard:allow-raw-color — recording frame uses fixed capture-safe contrast. */
6106
+ 0 0 0 1px rgba(255, 255, 255, 1),
6107
+ /* guard:allow-raw-color — recording frame uses fixed capture-safe contrast. */
6108
+ 0 0 0 3px rgba(220, 38, 38, 1),
6109
+ /* guard:allow-raw-color — recording frame uses fixed capture-safe contrast. */
6110
+ 0 0 0 6px rgba(17, 24, 39, 0.88),
6111
+ /* guard:allow-raw-color — recording frame uses fixed capture-safe contrast. */
6112
+ 0 0 34px 10px rgba(220, 38, 38, 0.36);
6098
6113
  }
6099
6114
  }
6100
6115
 
@@ -143,6 +143,9 @@ screencapturekit = { version = "2.0.0", features = ["macos_15_0"] }
143
143
  # AudioBufferList type so we can hand SCK's CMSampleBuffer audio bytes to
144
144
  # AVAudioPCMBuffer for the speech recognizer.
145
145
  objc2-core-audio-types = "0.3"
146
+ # Pure-Rust RNNoise-derived microphone denoising for the realtime
147
+ # ScreenCaptureKit mixer. Keep the CLI/dasp features out of the desktop bundle.
148
+ nnnoiseless = { version = "0.5.2", default-features = false }
146
149
  # `metal` builds whisper.cpp's ggml-metal backend and links it in (the sys
147
150
  # crate's build script embeds the compiled metallib, so no app-bundle/runtime
148
151
  # asset changes are needed). Without it, whisper.cpp decode runs CPU-only.
@@ -111,6 +111,8 @@ pub struct FeatureConfig {
111
111
  pub clips_enabled: bool,
112
112
  pub meetings_enabled: bool,
113
113
  pub voice_enabled: bool,
114
+ #[serde(default = "default_voice_cleanup_enabled")]
115
+ pub voice_cleanup_enabled: bool,
114
116
  #[serde(default = "default_launch_at_login_enabled")]
115
117
  pub launch_at_login_enabled: bool,
116
118
  #[serde(default)]
@@ -160,6 +162,10 @@ fn default_launch_at_login_enabled() -> bool {
160
162
  true
161
163
  }
162
164
 
165
+ fn default_voice_cleanup_enabled() -> bool {
166
+ true
167
+ }
168
+
163
169
  fn default_meeting_transcription_mode() -> MeetingTranscriptionMode {
164
170
  MeetingTranscriptionMode::Ask
165
171
  }
@@ -223,6 +229,7 @@ impl Default for FeatureConfig {
223
229
  clips_enabled: true,
224
230
  meetings_enabled: true,
225
231
  voice_enabled: true,
232
+ voice_cleanup_enabled: default_voice_cleanup_enabled(),
226
233
  launch_at_login_enabled: true,
227
234
  auto_hide_popover_enabled: false,
228
235
  meeting_transcription_mode: default_meeting_transcription_mode(),
@@ -436,6 +443,7 @@ mod tests {
436
443
  .unwrap();
437
444
 
438
445
  assert_eq!(config.whisper_model_id, "base");
446
+ assert!(config.voice_cleanup_enabled);
439
447
  }
440
448
 
441
449
  #[test]