@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.
- package/README.md +34 -7
- package/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +21 -0
- package/corpus/core/docs/content/agent-native-toolkit.mdx +1 -1
- package/corpus/core/docs/content/locales/ar-SA/agent-native-toolkit.mdx +1 -1
- package/corpus/core/docs/content/locales/de-DE/agent-native-toolkit.mdx +1 -1
- package/corpus/core/docs/content/locales/es-ES/agent-native-toolkit.mdx +1 -1
- package/corpus/core/docs/content/locales/fr-FR/agent-native-toolkit.mdx +1 -1
- package/corpus/core/docs/content/locales/hi-IN/agent-native-toolkit.mdx +1 -1
- package/corpus/core/docs/content/locales/ja-JP/agent-native-toolkit.mdx +1 -1
- package/corpus/core/docs/content/locales/ko-KR/agent-native-toolkit.mdx +1 -1
- package/corpus/core/docs/content/locales/pt-BR/agent-native-toolkit.mdx +1 -1
- package/corpus/core/docs/content/locales/zh-CN/agent-native-toolkit.mdx +1 -1
- package/corpus/core/docs/content/locales/zh-TW/agent-native-toolkit.mdx +1 -1
- package/corpus/core/docs/content/toolkit-agent-ux.mdx +1 -1
- package/corpus/core/docs/content/toolkit-app-adapters.mdx +1 -1
- package/corpus/core/docs/content/toolkit-collaboration-ui.mdx +1 -1
- package/corpus/core/docs/content/toolkit-collaboration.mdx +1 -1
- package/corpus/core/docs/content/toolkit-command-navigation.mdx +2 -2
- package/corpus/core/docs/content/toolkit-comments-review.mdx +1 -1
- package/corpus/core/docs/content/toolkit-history.mdx +1 -1
- package/corpus/core/docs/content/toolkit-observability.mdx +1 -1
- package/corpus/core/docs/content/toolkit-org-team.mdx +1 -1
- package/corpus/core/docs/content/toolkit-resources.mdx +1 -1
- package/corpus/core/docs/content/toolkit-settings.mdx +1 -1
- package/corpus/core/docs/content/toolkit-setup-connections.mdx +1 -1
- package/corpus/core/docs/content/toolkit-sharing-ui.mdx +1 -1
- package/corpus/core/docs/content/toolkit-sharing.mdx +1 -1
- package/corpus/core/docs/content/toolkit-shell-hooks.mdx +1 -1
- package/corpus/core/docs/content/toolkit-ui.mdx +1 -1
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/agent/durable-background.ts +18 -18
- package/corpus/core/src/agent/engine/ai-sdk-engine.ts +8 -2
- package/corpus/core/src/agent/engine/anthropic-engine.ts +11 -1
- package/corpus/core/src/agent/production-agent.ts +22 -11
- package/corpus/core/src/agent/run-manager.ts +17 -6
- package/corpus/core/src/agent/run-store.ts +16 -11
- package/corpus/core/src/cli/design-connect.ts +16 -2
- package/corpus/core/src/client/KeepTabOpenNotice.tsx +20 -15
- package/corpus/core/src/client/RunStuckBanner.tsx +14 -13
- package/corpus/core/src/client/agent-chat-adapter.ts +28 -17
- package/corpus/core/src/client/use-db-sync.ts +99 -1
- package/corpus/core/src/client/use-run-stuck-detection.ts +8 -5
- package/corpus/core/src/collab/client.ts +49 -4
- package/corpus/core/src/deploy/build.ts +103 -0
- package/corpus/core/src/server/core-routes-plugin.ts +137 -8
- package/corpus/templates/clips/changelog/2026-07-07-faster-local-whisper-dictation-start.md +6 -0
- package/corpus/templates/clips/desktop/src-tauri/src/native_speech.rs +311 -31
- package/corpus/templates/clips/desktop/src-tauri/src/whisper_speech.rs +2 -0
- package/corpus/templates/content/AGENTS.md +1 -0
- package/corpus/templates/content/actions/_database-source-utils.ts +73 -8
- package/corpus/templates/content/actions/_database-utils.ts +2 -1
- package/corpus/templates/content/actions/prepare-builder-source-review.ts +20 -1
- package/corpus/templates/content/actions/stage-builder-source-bulk-update.ts +663 -0
- package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +33 -7
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +53 -4
- package/corpus/templates/content/app/components/editor/database/settings.tsx +24 -0
- package/corpus/templates/content/app/hooks/use-content-database.ts +70 -1
- package/corpus/templates/content/app/hooks/use-document-properties.ts +63 -5
- package/corpus/templates/content/app/i18n/zh-TW.ts +3 -0
- package/corpus/templates/content/app/i18n-data.ts +31 -0
- package/corpus/templates/content/changelog/2026-07-01-builder-source-rows-can-be-staged-in-bulk-with-a-reviewable.md +6 -0
- package/corpus/templates/content/changelog/2026-07-07-builder-backed-database-fields-now-edit-like-normal-content-fields.md +6 -0
- package/corpus/templates/content/parity/matrix.md +24 -24
- package/corpus/templates/content/parity/matrix.ts +2 -0
- package/corpus/templates/content/shared/api.ts +59 -4
- package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +9 -0
- package/corpus/templates/design/actions/connect-localhost.ts +26 -3
- package/corpus/templates/design/actions/open-visual-edit.ts +4 -37
- package/corpus/templates/design/actions/read-source-file.ts +28 -0
- package/corpus/templates/design/actions/update-file.ts +76 -7
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +207 -11
- package/corpus/templates/design/app/components/design/EditPanel.tsx +4 -0
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +387 -25
- package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +1110 -88
- package/corpus/templates/design/app/components/design/bridge/hit-test.bridge.ts +368 -16
- package/corpus/templates/design/app/components/design/edit-panel/fill-gradient-helpers.ts +24 -0
- package/corpus/templates/design/app/components/design/edit-panel/panel-primitives.tsx +27 -6
- package/corpus/templates/design/app/components/design/edit-panel/position-helpers.ts +18 -0
- package/corpus/templates/design/app/components/design/edit-panel/stroke-properties.tsx +5 -4
- package/corpus/templates/design/app/components/design/multi-screen/handle-hit-zones.ts +124 -0
- package/corpus/templates/design/app/components/design/multi-screen/types.ts +23 -0
- package/corpus/templates/design/app/i18n/zh-TW.ts +1 -0
- package/corpus/templates/design/app/i18n-data.ts +19 -0
- package/corpus/templates/design/changelog/2026-07-07-dragging-a-free-floating-element-into-a-layout-column-or-row.md +6 -0
- package/corpus/templates/design/changelog/2026-07-07-dragging-a-repeated-list-item-like-a-to-do-card-generated-fr.md +6 -0
- package/corpus/templates/design/changelog/2026-07-07-dragging-an-element-onto-a-button-or-chip-s-middle-no-longer.md +6 -0
- package/corpus/templates/design/changelog/2026-07-07-elements-dropped-into-another-board-shape-now-stay-exactly-w.md +6 -0
- package/corpus/templates/design/changelog/2026-07-07-fixed-drag-reordering-elements-within-a-screen-no-longer-sil.md +6 -0
- package/corpus/templates/design/changelog/2026-07-07-fixed-dragging-an-element-from-a-screen-onto-the-canvas-now-.md +6 -0
- package/corpus/templates/design/changelog/2026-07-07-fixed-dragging-elements-between-screens-into-ai-generated-co.md +6 -0
- package/corpus/templates/design/changelog/2026-07-07-fixed-moving-elements-between-screens-no-longer-bakes-intern.md +6 -0
- package/corpus/templates/design/changelog/2026-07-07-fixed-the-cross-screen-drop-indicator-now-points-at-the-corr.md +6 -0
- package/corpus/templates/design/changelog/2026-07-07-fixed-the-zoom-menu-no-longer-leaves-the-canvas-unresponsive.md +6 -0
- package/corpus/templates/design/changelog/2026-07-07-local-visual-edit-keeps-bridge-access-when-opening-previousl.md +6 -0
- package/corpus/templates/design/changelog/2026-07-07-local-visual-edit-now-keeps-the-live-app-connection-authenti.md +6 -0
- package/corpus/templates/design/changelog/2026-07-07-text-drawn-on-the-canvas-is-now-readable-white-instead-of-in.md +6 -0
- package/corpus/templates/design/server/source-workspace.ts +27 -3
- package/corpus/templates/design/shared/board-file.ts +359 -0
- package/corpus/templates/design/shared/breakpoint-media.ts +21 -3
- package/corpus/templates/design/shared/code-layer.ts +145 -10
- package/dist/agent/durable-background.d.ts +12 -12
- package/dist/agent/durable-background.js +18 -18
- package/dist/agent/durable-background.js.map +1 -1
- package/dist/agent/engine/ai-sdk-engine.d.ts.map +1 -1
- package/dist/agent/engine/ai-sdk-engine.js +8 -2
- package/dist/agent/engine/ai-sdk-engine.js.map +1 -1
- package/dist/agent/engine/anthropic-engine.d.ts.map +1 -1
- package/dist/agent/engine/anthropic-engine.js +11 -1
- package/dist/agent/engine/anthropic-engine.js.map +1 -1
- package/dist/agent/production-agent.d.ts +2 -2
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +18 -11
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/agent/run-manager.d.ts +7 -1
- package/dist/agent/run-manager.d.ts.map +1 -1
- package/dist/agent/run-manager.js +6 -1
- package/dist/agent/run-manager.js.map +1 -1
- package/dist/agent/run-store.d.ts +1 -1
- package/dist/agent/run-store.d.ts.map +1 -1
- package/dist/agent/run-store.js +15 -10
- package/dist/agent/run-store.js.map +1 -1
- package/dist/cli/design-connect.d.ts.map +1 -1
- package/dist/cli/design-connect.js +12 -2
- package/dist/cli/design-connect.js.map +1 -1
- package/dist/client/KeepTabOpenNotice.d.ts +9 -10
- package/dist/client/KeepTabOpenNotice.d.ts.map +1 -1
- package/dist/client/KeepTabOpenNotice.js +8 -4
- package/dist/client/KeepTabOpenNotice.js.map +1 -1
- package/dist/client/RunStuckBanner.d.ts.map +1 -1
- package/dist/client/RunStuckBanner.js +14 -13
- package/dist/client/RunStuckBanner.js.map +1 -1
- package/dist/client/agent-chat-adapter.d.ts.map +1 -1
- package/dist/client/agent-chat-adapter.js +25 -17
- package/dist/client/agent-chat-adapter.js.map +1 -1
- package/dist/client/use-db-sync.d.ts +18 -0
- package/dist/client/use-db-sync.d.ts.map +1 -1
- package/dist/client/use-db-sync.js +88 -1
- package/dist/client/use-db-sync.js.map +1 -1
- package/dist/client/use-run-stuck-detection.d.ts +1 -1
- package/dist/client/use-run-stuck-detection.d.ts.map +1 -1
- package/dist/client/use-run-stuck-detection.js +6 -4
- package/dist/client/use-run-stuck-detection.js.map +1 -1
- package/dist/collab/client.d.ts.map +1 -1
- package/dist/collab/client.js +43 -4
- package/dist/collab/client.js.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/deploy/build.d.ts +1 -0
- package/dist/deploy/build.d.ts.map +1 -1
- package/dist/deploy/build.js +57 -0
- package/dist/deploy/build.js.map +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/notifications/routes.d.ts +2 -2
- package/dist/resources/handlers.d.ts +3 -3
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/core-routes-plugin.d.ts +10 -0
- package/dist/server/core-routes-plugin.d.ts.map +1 -1
- package/dist/server/core-routes-plugin.js +103 -9
- package/dist/server/core-routes-plugin.js.map +1 -1
- package/docs/content/agent-native-toolkit.mdx +1 -1
- package/docs/content/locales/ar-SA/agent-native-toolkit.mdx +1 -1
- package/docs/content/locales/de-DE/agent-native-toolkit.mdx +1 -1
- package/docs/content/locales/es-ES/agent-native-toolkit.mdx +1 -1
- package/docs/content/locales/fr-FR/agent-native-toolkit.mdx +1 -1
- package/docs/content/locales/hi-IN/agent-native-toolkit.mdx +1 -1
- package/docs/content/locales/ja-JP/agent-native-toolkit.mdx +1 -1
- package/docs/content/locales/ko-KR/agent-native-toolkit.mdx +1 -1
- package/docs/content/locales/pt-BR/agent-native-toolkit.mdx +1 -1
- package/docs/content/locales/zh-CN/agent-native-toolkit.mdx +1 -1
- package/docs/content/locales/zh-TW/agent-native-toolkit.mdx +1 -1
- package/docs/content/toolkit-agent-ux.mdx +1 -1
- package/docs/content/toolkit-app-adapters.mdx +1 -1
- package/docs/content/toolkit-collaboration-ui.mdx +1 -1
- package/docs/content/toolkit-collaboration.mdx +1 -1
- package/docs/content/toolkit-command-navigation.mdx +2 -2
- package/docs/content/toolkit-comments-review.mdx +1 -1
- package/docs/content/toolkit-history.mdx +1 -1
- package/docs/content/toolkit-observability.mdx +1 -1
- package/docs/content/toolkit-org-team.mdx +1 -1
- package/docs/content/toolkit-resources.mdx +1 -1
- package/docs/content/toolkit-settings.mdx +1 -1
- package/docs/content/toolkit-setup-connections.mdx +1 -1
- package/docs/content/toolkit-sharing-ui.mdx +1 -1
- package/docs/content/toolkit-sharing.mdx +1 -1
- package/docs/content/toolkit-shell-hooks.mdx +1 -1
- package/docs/content/toolkit-ui.mdx +1 -1
- 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()`
|
|
987
|
-
/// the
|
|
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<
|
|
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
|
-
|
|
1008
|
-
if
|
|
1009
|
-
|
|
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<
|
|
1330
|
+
let input_node: Retained<AVAudioInputNode> = unsafe { engine.inputNode() };
|
|
1032
1331
|
|
|
1033
1332
|
if voice_processing {
|
|
1034
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
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(
|
|
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(
|
|
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
|
}
|