@agent-native/core 0.90.4 → 0.90.7

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 (189) hide show
  1. package/README.md +34 -7
  2. package/corpus/README.md +1 -1
  3. package/corpus/core/CHANGELOG.md +21 -0
  4. package/corpus/core/docs/content/agent-native-toolkit.mdx +1 -1
  5. package/corpus/core/docs/content/locales/ar-SA/agent-native-toolkit.mdx +1 -1
  6. package/corpus/core/docs/content/locales/de-DE/agent-native-toolkit.mdx +1 -1
  7. package/corpus/core/docs/content/locales/es-ES/agent-native-toolkit.mdx +1 -1
  8. package/corpus/core/docs/content/locales/fr-FR/agent-native-toolkit.mdx +1 -1
  9. package/corpus/core/docs/content/locales/hi-IN/agent-native-toolkit.mdx +1 -1
  10. package/corpus/core/docs/content/locales/ja-JP/agent-native-toolkit.mdx +1 -1
  11. package/corpus/core/docs/content/locales/ko-KR/agent-native-toolkit.mdx +1 -1
  12. package/corpus/core/docs/content/locales/pt-BR/agent-native-toolkit.mdx +1 -1
  13. package/corpus/core/docs/content/locales/zh-CN/agent-native-toolkit.mdx +1 -1
  14. package/corpus/core/docs/content/locales/zh-TW/agent-native-toolkit.mdx +1 -1
  15. package/corpus/core/docs/content/toolkit-agent-ux.mdx +1 -1
  16. package/corpus/core/docs/content/toolkit-app-adapters.mdx +1 -1
  17. package/corpus/core/docs/content/toolkit-collaboration-ui.mdx +1 -1
  18. package/corpus/core/docs/content/toolkit-collaboration.mdx +1 -1
  19. package/corpus/core/docs/content/toolkit-command-navigation.mdx +2 -2
  20. package/corpus/core/docs/content/toolkit-comments-review.mdx +1 -1
  21. package/corpus/core/docs/content/toolkit-history.mdx +1 -1
  22. package/corpus/core/docs/content/toolkit-observability.mdx +1 -1
  23. package/corpus/core/docs/content/toolkit-org-team.mdx +1 -1
  24. package/corpus/core/docs/content/toolkit-resources.mdx +1 -1
  25. package/corpus/core/docs/content/toolkit-settings.mdx +1 -1
  26. package/corpus/core/docs/content/toolkit-setup-connections.mdx +1 -1
  27. package/corpus/core/docs/content/toolkit-sharing-ui.mdx +1 -1
  28. package/corpus/core/docs/content/toolkit-sharing.mdx +1 -1
  29. package/corpus/core/docs/content/toolkit-shell-hooks.mdx +1 -1
  30. package/corpus/core/docs/content/toolkit-ui.mdx +1 -1
  31. package/corpus/core/package.json +1 -1
  32. package/corpus/core/src/agent/durable-background.ts +18 -18
  33. package/corpus/core/src/agent/engine/ai-sdk-engine.ts +8 -2
  34. package/corpus/core/src/agent/engine/anthropic-engine.ts +11 -1
  35. package/corpus/core/src/agent/production-agent.ts +22 -11
  36. package/corpus/core/src/agent/run-manager.ts +17 -6
  37. package/corpus/core/src/agent/run-store.ts +16 -11
  38. package/corpus/core/src/cli/design-connect.ts +16 -2
  39. package/corpus/core/src/client/KeepTabOpenNotice.tsx +20 -15
  40. package/corpus/core/src/client/RunStuckBanner.tsx +14 -13
  41. package/corpus/core/src/client/agent-chat-adapter.ts +28 -17
  42. package/corpus/core/src/client/use-db-sync.ts +99 -1
  43. package/corpus/core/src/client/use-run-stuck-detection.ts +8 -5
  44. package/corpus/core/src/collab/client.ts +49 -4
  45. package/corpus/core/src/deploy/build.ts +103 -0
  46. package/corpus/core/src/server/core-routes-plugin.ts +137 -8
  47. package/corpus/templates/clips/changelog/2026-07-07-faster-local-whisper-dictation-start.md +6 -0
  48. package/corpus/templates/clips/desktop/src-tauri/src/native_speech.rs +311 -31
  49. package/corpus/templates/clips/desktop/src-tauri/src/whisper_speech.rs +2 -0
  50. package/corpus/templates/content/AGENTS.md +1 -0
  51. package/corpus/templates/content/actions/_database-source-utils.ts +73 -8
  52. package/corpus/templates/content/actions/_database-utils.ts +2 -1
  53. package/corpus/templates/content/actions/prepare-builder-source-review.ts +20 -1
  54. package/corpus/templates/content/actions/stage-builder-source-bulk-update.ts +663 -0
  55. package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +33 -7
  56. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +53 -4
  57. package/corpus/templates/content/app/components/editor/database/settings.tsx +24 -0
  58. package/corpus/templates/content/app/hooks/use-content-database.ts +70 -1
  59. package/corpus/templates/content/app/hooks/use-document-properties.ts +63 -5
  60. package/corpus/templates/content/app/i18n/zh-TW.ts +3 -0
  61. package/corpus/templates/content/app/i18n-data.ts +31 -0
  62. package/corpus/templates/content/changelog/2026-07-01-builder-source-rows-can-be-staged-in-bulk-with-a-reviewable.md +6 -0
  63. package/corpus/templates/content/changelog/2026-07-07-builder-backed-database-fields-now-edit-like-normal-content-fields.md +6 -0
  64. package/corpus/templates/content/parity/matrix.md +24 -24
  65. package/corpus/templates/content/parity/matrix.ts +2 -0
  66. package/corpus/templates/content/shared/api.ts +59 -4
  67. package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +9 -0
  68. package/corpus/templates/design/actions/connect-localhost.ts +26 -3
  69. package/corpus/templates/design/actions/open-visual-edit.ts +4 -37
  70. package/corpus/templates/design/actions/read-source-file.ts +28 -0
  71. package/corpus/templates/design/actions/update-file.ts +76 -7
  72. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +207 -11
  73. package/corpus/templates/design/app/components/design/EditPanel.tsx +4 -0
  74. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +387 -25
  75. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +1110 -88
  76. package/corpus/templates/design/app/components/design/bridge/hit-test.bridge.ts +368 -16
  77. package/corpus/templates/design/app/components/design/edit-panel/fill-gradient-helpers.ts +24 -0
  78. package/corpus/templates/design/app/components/design/edit-panel/panel-primitives.tsx +27 -6
  79. package/corpus/templates/design/app/components/design/edit-panel/position-helpers.ts +18 -0
  80. package/corpus/templates/design/app/components/design/edit-panel/stroke-properties.tsx +5 -4
  81. package/corpus/templates/design/app/components/design/multi-screen/handle-hit-zones.ts +124 -0
  82. package/corpus/templates/design/app/components/design/multi-screen/types.ts +23 -0
  83. package/corpus/templates/design/app/i18n/zh-TW.ts +1 -0
  84. package/corpus/templates/design/app/i18n-data.ts +19 -0
  85. package/corpus/templates/design/changelog/2026-07-07-dragging-a-free-floating-element-into-a-layout-column-or-row.md +6 -0
  86. package/corpus/templates/design/changelog/2026-07-07-dragging-a-repeated-list-item-like-a-to-do-card-generated-fr.md +6 -0
  87. package/corpus/templates/design/changelog/2026-07-07-dragging-an-element-onto-a-button-or-chip-s-middle-no-longer.md +6 -0
  88. package/corpus/templates/design/changelog/2026-07-07-elements-dropped-into-another-board-shape-now-stay-exactly-w.md +6 -0
  89. package/corpus/templates/design/changelog/2026-07-07-fixed-drag-reordering-elements-within-a-screen-no-longer-sil.md +6 -0
  90. package/corpus/templates/design/changelog/2026-07-07-fixed-dragging-an-element-from-a-screen-onto-the-canvas-now-.md +6 -0
  91. package/corpus/templates/design/changelog/2026-07-07-fixed-dragging-elements-between-screens-into-ai-generated-co.md +6 -0
  92. package/corpus/templates/design/changelog/2026-07-07-fixed-moving-elements-between-screens-no-longer-bakes-intern.md +6 -0
  93. package/corpus/templates/design/changelog/2026-07-07-fixed-the-cross-screen-drop-indicator-now-points-at-the-corr.md +6 -0
  94. package/corpus/templates/design/changelog/2026-07-07-fixed-the-zoom-menu-no-longer-leaves-the-canvas-unresponsive.md +6 -0
  95. package/corpus/templates/design/changelog/2026-07-07-local-visual-edit-keeps-bridge-access-when-opening-previousl.md +6 -0
  96. package/corpus/templates/design/changelog/2026-07-07-local-visual-edit-now-keeps-the-live-app-connection-authenti.md +6 -0
  97. package/corpus/templates/design/changelog/2026-07-07-text-drawn-on-the-canvas-is-now-readable-white-instead-of-in.md +6 -0
  98. package/corpus/templates/design/server/source-workspace.ts +27 -3
  99. package/corpus/templates/design/shared/board-file.ts +359 -0
  100. package/corpus/templates/design/shared/breakpoint-media.ts +21 -3
  101. package/corpus/templates/design/shared/code-layer.ts +145 -10
  102. package/dist/agent/durable-background.d.ts +12 -12
  103. package/dist/agent/durable-background.js +18 -18
  104. package/dist/agent/durable-background.js.map +1 -1
  105. package/dist/agent/engine/ai-sdk-engine.d.ts.map +1 -1
  106. package/dist/agent/engine/ai-sdk-engine.js +8 -2
  107. package/dist/agent/engine/ai-sdk-engine.js.map +1 -1
  108. package/dist/agent/engine/anthropic-engine.d.ts.map +1 -1
  109. package/dist/agent/engine/anthropic-engine.js +11 -1
  110. package/dist/agent/engine/anthropic-engine.js.map +1 -1
  111. package/dist/agent/production-agent.d.ts +2 -2
  112. package/dist/agent/production-agent.d.ts.map +1 -1
  113. package/dist/agent/production-agent.js +18 -11
  114. package/dist/agent/production-agent.js.map +1 -1
  115. package/dist/agent/run-manager.d.ts +7 -1
  116. package/dist/agent/run-manager.d.ts.map +1 -1
  117. package/dist/agent/run-manager.js +6 -1
  118. package/dist/agent/run-manager.js.map +1 -1
  119. package/dist/agent/run-store.d.ts +1 -1
  120. package/dist/agent/run-store.d.ts.map +1 -1
  121. package/dist/agent/run-store.js +15 -10
  122. package/dist/agent/run-store.js.map +1 -1
  123. package/dist/cli/design-connect.d.ts.map +1 -1
  124. package/dist/cli/design-connect.js +12 -2
  125. package/dist/cli/design-connect.js.map +1 -1
  126. package/dist/client/KeepTabOpenNotice.d.ts +9 -10
  127. package/dist/client/KeepTabOpenNotice.d.ts.map +1 -1
  128. package/dist/client/KeepTabOpenNotice.js +8 -4
  129. package/dist/client/KeepTabOpenNotice.js.map +1 -1
  130. package/dist/client/RunStuckBanner.d.ts.map +1 -1
  131. package/dist/client/RunStuckBanner.js +14 -13
  132. package/dist/client/RunStuckBanner.js.map +1 -1
  133. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  134. package/dist/client/agent-chat-adapter.js +25 -17
  135. package/dist/client/agent-chat-adapter.js.map +1 -1
  136. package/dist/client/use-db-sync.d.ts +18 -0
  137. package/dist/client/use-db-sync.d.ts.map +1 -1
  138. package/dist/client/use-db-sync.js +88 -1
  139. package/dist/client/use-db-sync.js.map +1 -1
  140. package/dist/client/use-run-stuck-detection.d.ts +1 -1
  141. package/dist/client/use-run-stuck-detection.d.ts.map +1 -1
  142. package/dist/client/use-run-stuck-detection.js +6 -4
  143. package/dist/client/use-run-stuck-detection.js.map +1 -1
  144. package/dist/collab/client.d.ts.map +1 -1
  145. package/dist/collab/client.js +43 -4
  146. package/dist/collab/client.js.map +1 -1
  147. package/dist/collab/routes.d.ts +1 -1
  148. package/dist/collab/struct-routes.d.ts +1 -1
  149. package/dist/deploy/build.d.ts +1 -0
  150. package/dist/deploy/build.d.ts.map +1 -1
  151. package/dist/deploy/build.js +57 -0
  152. package/dist/deploy/build.js.map +1 -1
  153. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  154. package/dist/notifications/routes.d.ts +2 -2
  155. package/dist/resources/handlers.d.ts +3 -3
  156. package/dist/secrets/routes.d.ts +9 -9
  157. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  158. package/dist/server/core-routes-plugin.d.ts +10 -0
  159. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  160. package/dist/server/core-routes-plugin.js +103 -9
  161. package/dist/server/core-routes-plugin.js.map +1 -1
  162. package/docs/content/agent-native-toolkit.mdx +1 -1
  163. package/docs/content/locales/ar-SA/agent-native-toolkit.mdx +1 -1
  164. package/docs/content/locales/de-DE/agent-native-toolkit.mdx +1 -1
  165. package/docs/content/locales/es-ES/agent-native-toolkit.mdx +1 -1
  166. package/docs/content/locales/fr-FR/agent-native-toolkit.mdx +1 -1
  167. package/docs/content/locales/hi-IN/agent-native-toolkit.mdx +1 -1
  168. package/docs/content/locales/ja-JP/agent-native-toolkit.mdx +1 -1
  169. package/docs/content/locales/ko-KR/agent-native-toolkit.mdx +1 -1
  170. package/docs/content/locales/pt-BR/agent-native-toolkit.mdx +1 -1
  171. package/docs/content/locales/zh-CN/agent-native-toolkit.mdx +1 -1
  172. package/docs/content/locales/zh-TW/agent-native-toolkit.mdx +1 -1
  173. package/docs/content/toolkit-agent-ux.mdx +1 -1
  174. package/docs/content/toolkit-app-adapters.mdx +1 -1
  175. package/docs/content/toolkit-collaboration-ui.mdx +1 -1
  176. package/docs/content/toolkit-collaboration.mdx +1 -1
  177. package/docs/content/toolkit-command-navigation.mdx +2 -2
  178. package/docs/content/toolkit-comments-review.mdx +1 -1
  179. package/docs/content/toolkit-history.mdx +1 -1
  180. package/docs/content/toolkit-observability.mdx +1 -1
  181. package/docs/content/toolkit-org-team.mdx +1 -1
  182. package/docs/content/toolkit-resources.mdx +1 -1
  183. package/docs/content/toolkit-settings.mdx +1 -1
  184. package/docs/content/toolkit-setup-connections.mdx +1 -1
  185. package/docs/content/toolkit-sharing-ui.mdx +1 -1
  186. package/docs/content/toolkit-sharing.mdx +1 -1
  187. package/docs/content/toolkit-shell-hooks.mdx +1 -1
  188. package/docs/content/toolkit-ui.mdx +1 -1
  189. package/package.json +1 -1
@@ -124,7 +124,7 @@ pub(crate) mod macos {
124
124
  use std::ffi::c_void;
125
125
  use std::mem::size_of;
126
126
  use std::ptr::NonNull;
127
- use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
127
+ use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering};
128
128
  use std::sync::{Arc, Mutex, OnceLock};
129
129
 
130
130
  use block2::{RcBlock, StackBlock};
@@ -134,7 +134,7 @@ pub(crate) mod macos {
134
134
  kAudioOutputUnitProperty_CurrentDevice, kAudioUnitScope_Global, AudioUnitSetProperty,
135
135
  };
136
136
  use objc2_avf_audio::{
137
- AVAudioEngine, AVAudioPCMBuffer, AVAudioTime,
137
+ AVAudioEngine, AVAudioInputNode, AVAudioPCMBuffer, AVAudioTime,
138
138
  AVAudioVoiceProcessingOtherAudioDuckingConfiguration,
139
139
  AVAudioVoiceProcessingOtherAudioDuckingLevel,
140
140
  };
@@ -211,6 +211,51 @@ pub(crate) mod macos {
211
211
  // inner pointers across threads — the session is moved through a Mutex.
212
212
  unsafe impl Send for SpeechSession {}
213
213
 
214
+ #[derive(Clone, PartialEq, Eq)]
215
+ struct RawMicDeviceKey {
216
+ id: Option<String>,
217
+ label: Option<String>,
218
+ }
219
+
220
+ impl RawMicDeviceKey {
221
+ fn from_selection(device_id: Option<&str>, device_label: Option<&str>) -> Self {
222
+ Self {
223
+ id: device_id
224
+ .map(str::trim)
225
+ .filter(|value| !value.is_empty())
226
+ .map(ToOwned::to_owned),
227
+ label: device_label
228
+ .map(str::trim)
229
+ .filter(|value| !value.is_empty())
230
+ .map(ToOwned::to_owned),
231
+ }
232
+ }
233
+ }
234
+
235
+ #[derive(Clone)]
236
+ struct RawMicTapTarget {
237
+ app: AppHandle,
238
+ on_samples: Arc<dyn Fn(&[f32]) + Send + Sync>,
239
+ level_tick: Arc<AtomicU32>,
240
+ }
241
+
242
+ struct WarmedRawMicEngine {
243
+ engine: Retained<AVAudioEngine>,
244
+ input_node: Retained<AVAudioInputNode>,
245
+ device_key: RawMicDeviceKey,
246
+ target: Arc<Mutex<Option<RawMicTapTarget>>>,
247
+ }
248
+
249
+ // SAFETY: Same argument as `SpeechSession` / `RawMicCapture`: AVAudioEngine
250
+ // and AVAudioInputNode are refcounted ObjC objects that we move through
251
+ // Mutex ownership, without sharing Rust references across threads.
252
+ unsafe impl Send for WarmedRawMicEngine {}
253
+
254
+ struct RawMicWarmCache {
255
+ device_key: RawMicDeviceKey,
256
+ target: Arc<Mutex<Option<RawMicTapTarget>>>,
257
+ }
258
+
214
259
  /// Process-global session slot. We only allow one dictation at a time —
215
260
  /// starting a new one while another is in flight cancels the old one
216
261
  /// first.
@@ -219,6 +264,11 @@ pub(crate) mod macos {
219
264
  SLOT.get_or_init(|| Mutex::new(None))
220
265
  }
221
266
 
267
+ fn warmed_raw_mic_engine_slot() -> &'static Mutex<Option<WarmedRawMicEngine>> {
268
+ static SLOT: OnceLock<Mutex<Option<WarmedRawMicEngine>>> = OnceLock::new();
269
+ SLOT.get_or_init(|| Mutex::new(None))
270
+ }
271
+
222
272
  /// Bumped at the start of every `native_speech_start_impl` call. The
223
273
  /// auto-restart thread captures the value at decision time and skips the
224
274
  /// restart if a newer session has since been requested.
@@ -574,6 +624,160 @@ pub(crate) mod macos {
574
624
  Ok(())
575
625
  }
576
626
 
627
+ fn enable_voice_processing(input_node: &AVAudioInputNode) {
628
+ if let Err(err) = unsafe { input_node.setVoiceProcessingEnabled_error(true) } {
629
+ eprintln!(
630
+ "[whisper-mic] voice processing enable failed: {} — continuing without AEC",
631
+ ns_error_message(&err)
632
+ );
633
+ }
634
+ }
635
+
636
+ fn disable_voice_processing_ducking(input_node: &AVAudioInputNode) {
637
+ // Disable other-audio ducking so the separately-captured system audio
638
+ // isn't dropped to near-silent while the mic VPIO runs.
639
+ unsafe {
640
+ let responds: bool = objc2::msg_send![
641
+ input_node,
642
+ respondsToSelector: objc2::sel!(setVoiceProcessingOtherAudioDuckingConfiguration:)
643
+ ];
644
+ if responds {
645
+ input_node.setVoiceProcessingOtherAudioDuckingConfiguration(
646
+ AVAudioVoiceProcessingOtherAudioDuckingConfiguration {
647
+ enableAdvancedDucking: objc2::runtime::Bool::NO,
648
+ duckingLevel: AVAudioVoiceProcessingOtherAudioDuckingLevel::Min,
649
+ },
650
+ );
651
+ }
652
+ }
653
+ }
654
+
655
+ fn take_warmed_raw_mic_engine(device_key: &RawMicDeviceKey) -> Option<WarmedRawMicEngine> {
656
+ let mut slot = warmed_raw_mic_engine_slot().lock().ok()?;
657
+ let should_reuse = slot
658
+ .as_ref()
659
+ .map(|cached| &cached.device_key == device_key)
660
+ .unwrap_or(false);
661
+ if should_reuse {
662
+ slot.take()
663
+ } else {
664
+ // A selected mic changed while the engine was warm. Drop the stale
665
+ // engine so the next start configures the requested device.
666
+ if let Some(stale) = slot.take() {
667
+ discard_warmed_raw_mic_engine(stale);
668
+ }
669
+ None
670
+ }
671
+ }
672
+
673
+ fn store_warmed_raw_mic_engine(engine: WarmedRawMicEngine) {
674
+ if let Ok(mut slot) = warmed_raw_mic_engine_slot().lock() {
675
+ if let Some(previous) = slot.replace(engine) {
676
+ discard_warmed_raw_mic_engine(previous);
677
+ }
678
+ }
679
+ }
680
+
681
+ fn clear_warmed_raw_mic_engine() {
682
+ if let Ok(mut slot) = warmed_raw_mic_engine_slot().lock() {
683
+ if let Some(engine) = slot.take() {
684
+ discard_warmed_raw_mic_engine(engine);
685
+ }
686
+ }
687
+ }
688
+
689
+ fn discard_warmed_raw_mic_engine(engine: WarmedRawMicEngine) {
690
+ let WarmedRawMicEngine {
691
+ engine,
692
+ input_node,
693
+ device_key: _,
694
+ target,
695
+ } = engine;
696
+ if let Ok(mut current) = target.lock() {
697
+ *current = None;
698
+ }
699
+ unsafe {
700
+ if engine.isRunning() {
701
+ engine.stop();
702
+ }
703
+ }
704
+ let _ = objc2::exception::catch(std::panic::AssertUnwindSafe(|| unsafe {
705
+ input_node.removeTapOnBus(0);
706
+ }));
707
+ }
708
+
709
+ fn install_reusable_raw_mic_tap(
710
+ input_node: &AVAudioInputNode,
711
+ target: Arc<Mutex<Option<RawMicTapTarget>>>,
712
+ ) -> Result<(), String> {
713
+ let target_for_tap = target.clone();
714
+ let tap_block = StackBlock::new(
715
+ move |buffer: std::ptr::NonNull<AVAudioPCMBuffer>,
716
+ _when: std::ptr::NonNull<AVAudioTime>| {
717
+ let Some(target) = target_for_tap
718
+ .try_lock()
719
+ .ok()
720
+ .and_then(|guard| guard.as_ref().cloned())
721
+ else {
722
+ return;
723
+ };
724
+ let buf = unsafe { buffer.as_ref() };
725
+ let mono = mono_mix_pcm(buf);
726
+ if !mono.is_empty() {
727
+ (target.on_samples)(&mono);
728
+ }
729
+ let n = target.level_tick.fetch_add(1, Ordering::Relaxed);
730
+ if n % 2 == 0 {
731
+ let level = peak_level_for_pcm(buf);
732
+ let _ = target.app.emit(
733
+ "voice:audio-level",
734
+ AudioLevelPayload {
735
+ level,
736
+ source: "mic",
737
+ },
738
+ );
739
+ }
740
+ },
741
+ )
742
+ .copy();
743
+ let block_ptr: *mut block2::Block<
744
+ dyn Fn(std::ptr::NonNull<AVAudioPCMBuffer>, std::ptr::NonNull<AVAudioTime>) + 'static,
745
+ > = (&*tap_block) as *const _ as *mut _;
746
+ objc2::exception::catch(std::panic::AssertUnwindSafe(|| unsafe {
747
+ input_node.installTapOnBus_bufferSize_format_block(
748
+ 0, 1024,
749
+ None, // use hardware's current format — avoids SCO↔A2DP stale-format race
750
+ block_ptr,
751
+ );
752
+ }))
753
+ .map_err(|e| format!("installTapOnBus threw: {e:?}"))
754
+ }
755
+
756
+ fn build_warmed_raw_mic_engine(
757
+ mic_device_id: Option<&str>,
758
+ mic_device_label: Option<&str>,
759
+ device_key: RawMicDeviceKey,
760
+ ) -> Result<WarmedRawMicEngine, String> {
761
+ let engine: Retained<AVAudioEngine> = unsafe { AVAudioEngine::new() };
762
+ configure_engine_input_device(&engine, mic_device_id, mic_device_label)?;
763
+ let input_node: Retained<AVAudioInputNode> = unsafe { engine.inputNode() };
764
+ enable_voice_processing(&input_node);
765
+ // Finalize input format negotiation once. Dictation-owned Whisper starts
766
+ // can then reuse this stopped VPIO engine instead of paying the
767
+ // per-press VoiceProcessingIO setup cost.
768
+ objc2::exception::catch(std::panic::AssertUnwindSafe(|| unsafe { engine.prepare() }))
769
+ .map_err(|e| format!("AVAudioEngine prepare threw: {e:?}"))?;
770
+ let target = Arc::new(Mutex::new(None));
771
+ install_reusable_raw_mic_tap(&input_node, target.clone())?;
772
+ disable_voice_processing_ducking(&input_node);
773
+ Ok(WarmedRawMicEngine {
774
+ engine,
775
+ input_node,
776
+ device_key,
777
+ target,
778
+ })
779
+ }
780
+
577
781
  /// Tear down whatever session is currently running. Called from `start`
578
782
  /// to guarantee a fresh slate, and from `cancel` / `stop` for explicit
579
783
  /// teardown.
@@ -983,12 +1187,13 @@ pub(crate) mod macos {
983
1187
  Ok(())
984
1188
  }
985
1189
 
986
- /// Handle for a running raw mic capture. `stop()` removes the tap and stops
987
- /// the engine.
1190
+ /// Handle for a running raw mic capture. `stop()` either tears down the tap
1191
+ /// or parks a dictation-owned VPIO engine for the next press.
988
1192
  pub(crate) struct RawMicCapture {
989
1193
  engine: Retained<AVAudioEngine>,
990
- input_node: Retained<objc2_avf_audio::AVAudioInputNode>,
1194
+ input_node: Retained<AVAudioInputNode>,
991
1195
  sample_rate: f64,
1196
+ warm_cache: Option<RawMicWarmCache>,
992
1197
  }
993
1198
 
994
1199
  // SAFETY: same argument as `SpeechSession` — refcounted ObjC objects that
@@ -1003,15 +1208,103 @@ pub(crate) mod macos {
1003
1208
  }
1004
1209
 
1005
1210
  pub(crate) fn stop(self) {
1211
+ let RawMicCapture {
1212
+ engine,
1213
+ input_node,
1214
+ sample_rate: _,
1215
+ warm_cache,
1216
+ } = self;
1217
+ if let Some(cache) = warm_cache {
1218
+ unsafe {
1219
+ if engine.isRunning() {
1220
+ engine.stop();
1221
+ }
1222
+ }
1223
+ if let Ok(mut target) = cache.target.lock() {
1224
+ *target = None;
1225
+ }
1226
+ store_warmed_raw_mic_engine(WarmedRawMicEngine {
1227
+ engine,
1228
+ input_node,
1229
+ device_key: cache.device_key,
1230
+ target: cache.target,
1231
+ });
1232
+ return;
1233
+ }
1006
1234
  unsafe {
1007
- self.input_node.removeTapOnBus(0);
1008
- if self.engine.isRunning() {
1009
- self.engine.stop();
1235
+ input_node.removeTapOnBus(0);
1236
+ if engine.isRunning() {
1237
+ engine.stop();
1010
1238
  }
1011
1239
  }
1012
1240
  }
1013
1241
  }
1014
1242
 
1243
+ fn start_warmed_raw_mic_capture(
1244
+ app: AppHandle,
1245
+ mic_device_id: Option<String>,
1246
+ mic_device_label: Option<String>,
1247
+ on_samples: Arc<dyn Fn(&[f32]) + Send + Sync>,
1248
+ ) -> Result<RawMicCapture, String> {
1249
+ let device_key =
1250
+ RawMicDeviceKey::from_selection(mic_device_id.as_deref(), mic_device_label.as_deref());
1251
+ let warmed = match take_warmed_raw_mic_engine(&device_key) {
1252
+ Some(warmed) => warmed,
1253
+ None => build_warmed_raw_mic_engine(
1254
+ mic_device_id.as_deref(),
1255
+ mic_device_label.as_deref(),
1256
+ device_key.clone(),
1257
+ )?,
1258
+ };
1259
+ if let Ok(mut target) = warmed.target.lock() {
1260
+ *target = Some(RawMicTapTarget {
1261
+ app: app.clone(),
1262
+ on_samples,
1263
+ level_tick: Arc::new(AtomicU32::new(0)),
1264
+ });
1265
+ } else {
1266
+ return Err("warmed mic target lock poisoned".into());
1267
+ }
1268
+
1269
+ let start_result = objc2::exception::catch(std::panic::AssertUnwindSafe(|| unsafe {
1270
+ warmed.engine.startAndReturnError()
1271
+ }))
1272
+ .map_err(|e| format!("AVAudioEngine start threw: {e:?}"))
1273
+ .and_then(|r| {
1274
+ r.map_err(|e| format!("AVAudioEngine start failed: {}", ns_error_message(&e)))
1275
+ });
1276
+ if let Err(msg) = start_result {
1277
+ if let Ok(mut target) = warmed.target.lock() {
1278
+ *target = None;
1279
+ }
1280
+ return Err(msg);
1281
+ }
1282
+
1283
+ // Read the format only after the engine has started — at this point the
1284
+ // engine has committed to its I/O configuration and the sample rate
1285
+ // matches what the tap will actually deliver.
1286
+ let format = unsafe { warmed.input_node.outputFormatForBus(0) };
1287
+ let sample_rate = unsafe { format.sampleRate() };
1288
+ eprintln!(
1289
+ "[whisper-mic] reused warm VPIO tap format: {} Hz, {} ch",
1290
+ sample_rate as u32,
1291
+ unsafe { format.channelCount() }
1292
+ );
1293
+
1294
+ let WarmedRawMicEngine {
1295
+ engine,
1296
+ input_node,
1297
+ device_key,
1298
+ target,
1299
+ } = warmed;
1300
+ Ok(RawMicCapture {
1301
+ engine,
1302
+ input_node,
1303
+ sample_rate,
1304
+ warm_cache: Some(RawMicWarmCache { device_key, target }),
1305
+ })
1306
+ }
1307
+
1015
1308
  /// Start mic capture and forward a mono mix of every available channel to
1016
1309
  /// `on_samples`. Recording transcription can disable VPIO so the tap does
1017
1310
  /// not precondition the shared mic before ScreenCaptureKit opens it.
@@ -1020,23 +1313,24 @@ pub(crate) mod macos {
1020
1313
  mic_device_id: Option<String>,
1021
1314
  mic_device_label: Option<String>,
1022
1315
  voice_processing: bool,
1316
+ reuse_voice_processing_engine: bool,
1023
1317
  on_samples: Arc<dyn Fn(&[f32]) + Send + Sync>,
1024
1318
  ) -> Result<RawMicCapture, String> {
1319
+ if voice_processing && reuse_voice_processing_engine {
1320
+ return start_warmed_raw_mic_capture(app, mic_device_id, mic_device_label, on_samples);
1321
+ }
1322
+ clear_warmed_raw_mic_engine();
1323
+
1025
1324
  let engine: Retained<AVAudioEngine> = unsafe { AVAudioEngine::new() };
1026
1325
  configure_engine_input_device(
1027
1326
  &engine,
1028
1327
  mic_device_id.as_deref(),
1029
1328
  mic_device_label.as_deref(),
1030
1329
  )?;
1031
- let input_node: Retained<objc2_avf_audio::AVAudioInputNode> = unsafe { engine.inputNode() };
1330
+ let input_node: Retained<AVAudioInputNode> = unsafe { engine.inputNode() };
1032
1331
 
1033
1332
  if voice_processing {
1034
- if let Err(err) = unsafe { input_node.setVoiceProcessingEnabled_error(true) } {
1035
- eprintln!(
1036
- "[whisper-mic] voice processing enable failed: {} — continuing without AEC",
1037
- ns_error_message(&err)
1038
- );
1039
- }
1333
+ enable_voice_processing(&input_node);
1040
1334
  } else {
1041
1335
  eprintln!("[whisper-mic] voice processing disabled for shared mic capture");
1042
1336
  }
@@ -1102,28 +1396,14 @@ pub(crate) mod macos {
1102
1396
  );
1103
1397
 
1104
1398
  if voice_processing {
1105
- // Disable other-audio ducking so the separately-captured system audio
1106
- // isn't dropped to near-silent while the mic VPIO runs.
1107
- unsafe {
1108
- let responds: bool = objc2::msg_send![
1109
- &*input_node,
1110
- respondsToSelector: objc2::sel!(setVoiceProcessingOtherAudioDuckingConfiguration:)
1111
- ];
1112
- if responds {
1113
- input_node.setVoiceProcessingOtherAudioDuckingConfiguration(
1114
- AVAudioVoiceProcessingOtherAudioDuckingConfiguration {
1115
- enableAdvancedDucking: objc2::runtime::Bool::NO,
1116
- duckingLevel: AVAudioVoiceProcessingOtherAudioDuckingLevel::Min,
1117
- },
1118
- );
1119
- }
1120
- }
1399
+ disable_voice_processing_ducking(&input_node);
1121
1400
  }
1122
1401
 
1123
1402
  Ok(RawMicCapture {
1124
1403
  engine,
1125
1404
  input_node,
1126
1405
  sample_rate,
1406
+ warm_cache: None,
1127
1407
  })
1128
1408
  }
1129
1409
 
@@ -656,11 +656,13 @@ mod macos {
656
656
  session_start,
657
657
  );
658
658
  let mic_for_cb = mic_stream.clone();
659
+ let reuse_voice_processing_engine = owner == SessionOwner::Dictation && !capture_system;
659
660
  let mic_cap = start_raw_mic_capture(
660
661
  app.clone(),
661
662
  mic_device_id,
662
663
  mic_device_label,
663
664
  voice_processing,
665
+ reuse_voice_processing_engine,
664
666
  Arc::new(move |s: &[f32]| mic_for_cb.push(s)),
665
667
  )
666
668
  .map_err(|e| {
@@ -121,6 +121,7 @@ cd templates/content && pnpm action <name> [args]
121
121
  | `set-content-database-source-write-mode` | `--databaseId <id>` or `--documentId <id> --liveWritesEnabled true\|false [--allowedWriteModes <json>]` | Enable/disable per-source Builder live writes; enabling is allowed only for `agent-native-blog-article-test` with explicit modes |
122
122
  | `stage-builder-revision` | `--databaseId <id>` or `--documentId <id>` | Stage pending local Builder CMS changes as a local-only save-revision record; never calls Builder |
123
123
  | `review-content-database-source-change-set` | `--databaseId <id>` or `--documentId <id> --changeSetId <id> --decision approve\|reject [--note]` | Approve or reject a local source change-set review record without provider writes |
124
+ | `stage-builder-source-bulk-update` | `--databaseId <id>` or `--documentId <id> --itemIds <json-array>` or `--documentIds <json-array> --field <json> [--sourceId <id>] [--dryRun true\|false]` | Preview or stage one bounded field update across selected source-backed Builder rows; apply still goes through Builder review/execution |
124
125
  | `prepare-builder-source-execution` | `--databaseId <id>` or `--documentId <id> --changeSetId <id> [--pushModeConfirmation autosave\|draft\|publish]` | Prepare a dry-run Builder execution gate for approved field/body changes with request semantics/idempotency key; never calls Builder |
125
126
  | `validate-builder-source-execution` | `--databaseId <id>` or `--documentId <id> --changeSetId <id> [--idempotencyKey <key>]` | Validate/replay a prepared Builder execution gate locally as a dry run; never calls Builder |
126
127
  | `execute-builder-source-execution` | `--databaseId <id>` or `--documentId <id> --changeSetId <id> [--idempotencyKey <key>] [--pushModeConfirmation autosave\|draft\|publish]` | Execute a guarded live Builder write only when approved, validated, enabled, idempotent, and targeting `agent-native-blog-article-test` |
@@ -302,6 +302,7 @@ export function serializeSourceField(
302
302
 
303
303
  function serializeSourceRowRecord(
304
304
  row: ContentDatabaseSourceRecordRowDb,
305
+ options: { includeHeavyBuilderBodyValues?: boolean } = {},
305
306
  ): ContentDatabaseSourceRow {
306
307
  return {
307
308
  id: row.id,
@@ -310,10 +311,12 @@ function serializeSourceRowRecord(
310
311
  sourceRowId: row.sourceRowId,
311
312
  sourceQualifiedId: row.sourceQualifiedId,
312
313
  sourceDisplayKey: row.sourceDisplayKey,
313
- sourceValues:
314
+ sourceValues: sourceValuesForSnapshot(
314
315
  parseObject<Record<string, DocumentPropertyValue>>(
315
316
  row.sourceValuesJson,
316
317
  ) ?? {},
318
+ options,
319
+ ),
317
320
  provenance: row.provenance,
318
321
  syncState: normalizeSourceSyncState(row.syncState),
319
322
  freshness: normalizeSourceFreshness(row.freshness),
@@ -322,6 +325,27 @@ function serializeSourceRowRecord(
322
325
  };
323
326
  }
324
327
 
328
+ const HEAVY_BUILDER_BODY_SOURCE_VALUE_KEYS = new Set([
329
+ BUILDER_CMS_BODY_CONTENT_KEY,
330
+ BUILDER_CMS_BODY_LOSSLESS_CONTENT_KEY,
331
+ BUILDER_CMS_BODY_READABLE_MAP_KEY,
332
+ BUILDER_CMS_BODY_SIDECARS_KEY,
333
+ ]);
334
+
335
+ export function sourceValuesForSnapshot(
336
+ sourceValues: Record<string, DocumentPropertyValue>,
337
+ options: { includeHeavyBuilderBodyValues?: boolean } = {},
338
+ ): Record<string, DocumentPropertyValue> {
339
+ if (options.includeHeavyBuilderBodyValues === true) return sourceValues;
340
+ let next: Record<string, DocumentPropertyValue> | null = null;
341
+ for (const key of HEAVY_BUILDER_BODY_SOURCE_VALUE_KEYS) {
342
+ if (!Object.prototype.hasOwnProperty.call(sourceValues, key)) continue;
343
+ next ??= { ...sourceValues };
344
+ delete next[key];
345
+ }
346
+ return next ?? sourceValues;
347
+ }
348
+
325
349
  function serializeSourceChangeSet(
326
350
  row: ContentDatabaseSourceChangeSetRowDb,
327
351
  ): ContentDatabaseSourceChangeSet {
@@ -1919,7 +1943,9 @@ export async function getContentDatabaseSourceSnapshot(
1919
1943
  asc(schema.contentDatabaseSources.id),
1920
1944
  );
1921
1945
  if (!source) return null;
1922
- return loadSourceSnapshot(source, database);
1946
+ return loadSourceSnapshot(source, database, {
1947
+ includeHeavyBuilderBodyValues: false,
1948
+ });
1923
1949
  }
1924
1950
 
1925
1951
  /**
@@ -1942,7 +1968,9 @@ export async function getContentDatabaseSourceSnapshotById(
1942
1968
  ),
1943
1969
  );
1944
1970
  if (!source) return null;
1945
- return loadSourceSnapshot(source, database);
1971
+ return loadSourceSnapshot(source, database, {
1972
+ includeHeavyBuilderBodyValues: false,
1973
+ });
1946
1974
  }
1947
1975
 
1948
1976
  /**
@@ -1955,9 +1983,37 @@ export async function getContentDatabaseSourceSnapshotForWrite(
1955
1983
  database: ContentDatabaseRow | ContentDatabase,
1956
1984
  sourceId?: string | null,
1957
1985
  ): Promise<ContentDatabaseSource | null> {
1958
- return sourceId
1959
- ? getContentDatabaseSourceSnapshotById(database, sourceId)
1960
- : getContentDatabaseSourceSnapshot(database);
1986
+ if (sourceId) {
1987
+ const db = getDb();
1988
+ const [source] = await db
1989
+ .select()
1990
+ .from(schema.contentDatabaseSources)
1991
+ .where(
1992
+ and(
1993
+ eq(schema.contentDatabaseSources.id, sourceId),
1994
+ eq(schema.contentDatabaseSources.databaseId, database.id),
1995
+ ),
1996
+ );
1997
+ return source
1998
+ ? loadSourceSnapshot(source, database, {
1999
+ includeHeavyBuilderBodyValues: true,
2000
+ })
2001
+ : null;
2002
+ }
2003
+ const db = getDb();
2004
+ const [source] = await db
2005
+ .select()
2006
+ .from(schema.contentDatabaseSources)
2007
+ .where(eq(schema.contentDatabaseSources.databaseId, database.id))
2008
+ .orderBy(
2009
+ asc(schema.contentDatabaseSources.createdAt),
2010
+ asc(schema.contentDatabaseSources.id),
2011
+ );
2012
+ return source
2013
+ ? loadSourceSnapshot(source, database, {
2014
+ includeHeavyBuilderBodyValues: true,
2015
+ })
2016
+ : null;
1961
2017
  }
1962
2018
 
1963
2019
  /**
@@ -1982,7 +2038,11 @@ export async function getAllContentDatabaseSourceSnapshots(
1982
2038
  );
1983
2039
  const snapshots: ContentDatabaseSource[] = [];
1984
2040
  for (const source of sources) {
1985
- snapshots.push(await loadSourceSnapshot(source, database));
2041
+ snapshots.push(
2042
+ await loadSourceSnapshot(source, database, {
2043
+ includeHeavyBuilderBodyValues: false,
2044
+ }),
2045
+ );
1986
2046
  }
1987
2047
  return snapshots;
1988
2048
  }
@@ -2135,6 +2195,7 @@ async function loadSourceSnapshotRowsOptimistically(args: {
2135
2195
  async function loadSourceSnapshot(
2136
2196
  source: ContentDatabaseSourceRowDb,
2137
2197
  database: ContentDatabaseRow | ContentDatabase,
2198
+ options: { includeHeavyBuilderBodyValues: boolean },
2138
2199
  ): Promise<ContentDatabaseSource> {
2139
2200
  const db = getDb();
2140
2201
  const [fieldRows, changeRows, reviewRows, executionRows, propertyDefs] =
@@ -2218,7 +2279,11 @@ async function loadSourceSnapshot(
2218
2279
  database,
2219
2280
  isBuilderSource,
2220
2281
  });
2221
- const rows = rowRows.map(serializeSourceRowRecord);
2282
+ const rows = rowRows.map((row) =>
2283
+ serializeSourceRowRecord(row, {
2284
+ includeHeavyBuilderBodyValues: options.includeHeavyBuilderBodyValues,
2285
+ }),
2286
+ );
2222
2287
  const documentTitleById = new Map(
2223
2288
  rowDocuments.map((document) => [document.id, document.title]),
2224
2289
  );
@@ -123,12 +123,13 @@ export function normalizeContentDatabasePageOptions(options: {
123
123
  function serializeDocument(
124
124
  doc: typeof schema.documents.$inferSelect,
125
125
  membership?: DatabaseMembershipRow,
126
+ options: { includeContent?: boolean } = {},
126
127
  ) {
127
128
  return {
128
129
  id: doc.id,
129
130
  parentId: doc.parentId,
130
131
  title: doc.title,
131
- content: doc.content,
132
+ content: options.includeContent === true ? doc.content : "",
132
133
  icon: doc.icon,
133
134
  position: doc.position,
134
135
  isFavorite: parseDocumentFavorite(doc.isFavorite),
@@ -382,6 +382,11 @@ export default defineAction({
382
382
  .string()
383
383
  .optional()
384
384
  .describe("Target source ID (defaults to the primary source)"),
385
+ changeSetIds: z
386
+ .array(z.string())
387
+ .max(BUILDER_SOURCE_REVIEW_PREPARE_LIMIT)
388
+ .optional()
389
+ .describe("Optional bounded set of Builder change-set IDs to prepare"),
385
390
  pushModeConfirmation: z
386
391
  .enum(["autosave", "draft", "publish"])
387
392
  .optional()
@@ -409,13 +414,27 @@ export default defineAction({
409
414
  if (!snapshot || snapshot.sourceType !== "builder-cms") {
410
415
  throw new Error("Attach a Builder CMS source before reviewing updates.");
411
416
  }
417
+ const requestedIds = new Set(args.changeSetIds ?? []);
412
418
  const allReviewableChanges = snapshot.changeSets.filter(
413
419
  (changeSet) =>
414
420
  changeSet.direction === "outbound" &&
415
421
  (changeSet.state === "pending_push" ||
416
422
  changeSet.state === "staged_revision" ||
417
- changeSet.state === "approved"),
423
+ changeSet.state === "approved") &&
424
+ (requestedIds.size === 0 || requestedIds.has(changeSet.id)),
418
425
  );
426
+ if (
427
+ requestedIds.size > 0 &&
428
+ allReviewableChanges.length !== requestedIds.size
429
+ ) {
430
+ const foundIds = new Set(
431
+ allReviewableChanges.map((changeSet) => changeSet.id),
432
+ );
433
+ const missingIds = [...requestedIds].filter((id) => !foundIds.has(id));
434
+ throw new Error(
435
+ `Requested Builder change-set is not reviewable: ${missingIds.join(", ")}.`,
436
+ );
437
+ }
419
438
  if (allReviewableChanges.length === 0) {
420
439
  throw new Error("No pending local Builder changes to review.");
421
440
  }