@agent-native/core 0.161.22 → 0.162.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/README.md +1 -1
- package/corpus/templates/analytics/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +18 -2
- package/corpus/templates/assets/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/brain/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/calendar/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/calendar/server/handlers/google-auth.ts +1 -8
- package/corpus/templates/chat/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/clips/app/components/recorder/pre-record-panel.tsx +88 -35
- package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +36 -1
- package/corpus/templates/clips/app/i18n/en-US.ts +4 -3
- package/corpus/templates/clips/desktop/src/app.tsx +30 -40
- package/corpus/templates/clips/desktop/src/components/MicOffConfirmation.tsx +47 -0
- package/corpus/templates/clips/desktop/src/styles.css +110 -0
- package/corpus/templates/clips/desktop/src-tauri/src/whisper_speech.rs +114 -7
- package/corpus/templates/clips/shared/recording-capture.ts +18 -13
- package/corpus/templates/content/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +3 -5
- package/corpus/templates/content/app/components/editor/DocumentToolbar.tsx +0 -3
- package/corpus/templates/content/app/components/editor/NotionConflictBanner.tsx +0 -4
- package/corpus/templates/content/app/components/editor/NotionSyncBar.tsx +4 -1
- package/corpus/templates/content/app/hooks/use-notion.ts +17 -15
- package/corpus/templates/crm/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/design/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/design/app/components/design/keyboard-shortcuts.ts +13 -7
- package/corpus/templates/design/app/hooks/use-navigation-state.ts +18 -21
- package/corpus/templates/design/app/pages/design-editor/editor-state.ts +2 -5
- package/corpus/templates/design/app/pages/design-editor/tool-state.ts +4 -12
- package/corpus/templates/design/app/pages/design-editor/types.ts +38 -1
- package/corpus/templates/design/changelog/2026-08-18-advanced-design-editor-panels-hidden-by-default.md +6 -0
- package/corpus/templates/design/playwright.config.ts +19 -2
- package/corpus/templates/dispatch/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/factory/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/forms/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/macros/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/mail/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/mail/app/components/layout/AppLayout.tsx +18 -3
- package/corpus/templates/mail/server/handlers/google-auth.ts +1 -8
- package/corpus/templates/plan/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/slides/.agents/skills/create-deck/SKILL.md +4 -0
- package/corpus/templates/slides/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/slides/actions/_await-fit-check.ts +30 -5
- package/corpus/templates/slides/actions/add-slide.ts +6 -2
- package/corpus/templates/slides/actions/create-deck.ts +2 -1
- package/corpus/templates/slides/app/lib/create-deck-generation.ts +1 -0
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +34 -1
- package/corpus/templates/slides/app/routes/p.$id.tsx +3 -1
- package/corpus/templates/slides/changelog/2026-08-18-generated-slides-preserve-speaker-notes.md +6 -0
- package/corpus/templates/slides/server/handlers/assets.ts +23 -12
- package/corpus/templates/slides/server/handlers/design-system-decode-job-status.ts +15 -6
- package/corpus/templates/slides/server/handlers/design-system-upload-start.ts +15 -6
- package/corpus/templates/slides/server/handlers/index-design-system-sources.ts +34 -19
- package/corpus/templates/slides/server/handlers/share.ts +1 -1
- package/corpus/templates/slides/server/plugins/agent-chat.ts +1 -1
- package/corpus/templates/slides/server/routes/api/deck-agent-context.json.get.ts +1 -1
- package/corpus/templates/slides/server/routes/api/exports/[filename].get.ts +9 -3
- package/corpus/templates/slides/server/routes/api/exports/google-slides.post.ts +12 -5
- package/corpus/templates/slides/server/routes/api/exports/html.post.ts +9 -7
- package/corpus/templates/slides/server/routes/api/exports/pptx.post.ts +9 -7
- package/corpus/templates/slides/shared/api.ts +7 -1
- package/corpus/templates/tasks/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/dist/agent/engine/builder-engine.js +15 -8
- package/dist/agent/engine/translate-anthropic.js +27 -2
- package/dist/agent/production-agent.d.ts +17 -2
- package/dist/agent/production-agent.js +48 -17
- package/dist/agent/thread-data-builder.d.ts +1 -0
- package/dist/agent/thread-data-builder.js +6 -4
- package/dist/agent/tool-approval-store.d.ts +10 -1
- package/dist/agent/tool-approval-store.js +19 -2
- package/dist/agent/types.d.ts +9 -0
- package/dist/client/AssistantChat.js +14 -6
- package/dist/client/EnvironmentBadge.d.ts +15 -0
- package/dist/client/EnvironmentBadge.js +79 -0
- package/dist/client/MultiTabAssistantChat.js +73 -17
- package/dist/client/agent-chat-adapter.js +11 -1
- package/dist/client/api-path.js +14 -1
- package/dist/client/app-providers.js +2 -1
- package/dist/client/chat/tool-call-display.d.ts +12 -2
- package/dist/client/chat/tool-call-display.js +17 -7
- package/dist/client/chat/widgets/WorkspaceFileWidget.js +1 -1
- package/dist/client/chat/widgets/builtin-tool-renderers.d.ts +6 -0
- package/dist/client/chat/widgets/builtin-tool-renderers.js +19 -0
- package/dist/client/onboarding/FirstRunOnboarding.js +46 -32
- package/dist/client/onboarding/use-onboarding.d.ts +8 -1
- package/dist/client/onboarding/use-onboarding.js +22 -7
- package/dist/client/session-replay.js +58 -15
- package/dist/client/settings/SettingsTabsPage.js +19 -6
- package/dist/client/sse-event-processor.d.ts +7 -0
- package/dist/client/sse-event-processor.js +4 -1
- package/dist/client/ui/index.d.ts +1 -0
- package/dist/client/ui/index.js +1 -0
- package/dist/client/use-action.js +26 -0
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/routes.d.ts +1 -1
- package/dist/config.d.ts +15 -0
- package/dist/config.js +53 -0
- package/dist/extensions/fetch-tool.js +8 -2
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/mcp/embed-app.js +1 -1
- package/dist/observability/routes.d.ts +5 -5
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +13 -13
- package/dist/provider-api/actions/provider-api.d.ts +11 -11
- package/dist/provider-api/index.d.ts +10 -0
- package/dist/provider-api/index.js +45 -9
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/auth.js +10 -3
- package/dist/server/poll.js +7 -3
- package/dist/server/sentry.d.ts +1 -1
- package/dist/templates/chat/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/dist/templates/default/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/dist/templates/workspace-core/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/dist/vite/client.d.ts +5 -0
- package/dist/vite/client.js +64 -2
- package/dist/workspace-files/actions.d.ts +1 -8
- package/dist/workspace-files/actions.js +5 -21
- package/dist/workspace-files/index.d.ts +1 -1
- package/dist/workspace-files/index.js +1 -1
- package/dist/workspace-files/store.d.ts +18 -0
- package/dist/workspace-files/store.js +24 -3
- package/package.json +3 -3
- package/src/templates/chat/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/src/templates/default/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/src/templates/workspace-core/.agents/skills/real-time-sync/SKILL.md +1 -1
|
@@ -630,6 +630,60 @@ mod macos {
|
|
|
630
630
|
}
|
|
631
631
|
}
|
|
632
632
|
|
|
633
|
+
/// RNNoise's voice-activity output is more useful than a fixed amplitude
|
|
634
|
+
/// threshold for the meeting microphone. A muted call still leaves Clips
|
|
635
|
+
/// with the local microphone, and fans, keyboard noise, or room noise can
|
|
636
|
+
/// be loud enough to cross the old RMS-only gate. Keep this state on the
|
|
637
|
+
/// worker thread so the realtime capture callback stays allocation-free.
|
|
638
|
+
struct MicVoiceActivity {
|
|
639
|
+
state: Box<nnnoiseless::DenoiseState<'static>>,
|
|
640
|
+
pending: Vec<f32>,
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
impl MicVoiceActivity {
|
|
644
|
+
const SAMPLE_RATE: f32 = 48_000.0;
|
|
645
|
+
const INPUT_SCALE: f32 = 32_768.0;
|
|
646
|
+
|
|
647
|
+
fn new() -> Self {
|
|
648
|
+
Self {
|
|
649
|
+
state: nnnoiseless::DenoiseState::new(),
|
|
650
|
+
pending: Vec::new(),
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
fn reset(&mut self) {
|
|
655
|
+
self.state = nnnoiseless::DenoiseState::new();
|
|
656
|
+
self.pending.clear();
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/// Return the highest RNNoise speech probability in `samples`.
|
|
660
|
+
/// `None` means this capture format is not the 48 kHz PCM that
|
|
661
|
+
/// RNNoise expects; callers retain the existing RMS fallback for
|
|
662
|
+
/// those legacy devices.
|
|
663
|
+
fn observe(&mut self, samples: &[f32], sample_rate: f32) -> Option<f32> {
|
|
664
|
+
if (sample_rate - Self::SAMPLE_RATE).abs() > 1.0 {
|
|
665
|
+
self.reset();
|
|
666
|
+
return None;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
self.pending.extend(samples.iter().map(|sample| {
|
|
670
|
+
(sample * Self::INPUT_SCALE).clamp(-Self::INPUT_SCALE, Self::INPUT_SCALE)
|
|
671
|
+
}));
|
|
672
|
+
|
|
673
|
+
let frame_size = nnnoiseless::DenoiseState::FRAME_SIZE;
|
|
674
|
+
let mut highest_probability: f32 = 0.0;
|
|
675
|
+
while self.pending.len() >= frame_size {
|
|
676
|
+
let mut denoised = [0.0_f32; nnnoiseless::DenoiseState::FRAME_SIZE];
|
|
677
|
+
let probability = self
|
|
678
|
+
.state
|
|
679
|
+
.process_frame(&mut denoised, &self.pending[..frame_size]);
|
|
680
|
+
highest_probability = highest_probability.max(probability);
|
|
681
|
+
self.pending.drain(..frame_size);
|
|
682
|
+
}
|
|
683
|
+
Some(highest_probability)
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
633
687
|
/// Run whisper over `samples` (16 kHz mono f32), returning each speech
|
|
634
688
|
/// segment as `(start_ms, end_ms, text)` with buffer-relative timestamps.
|
|
635
689
|
/// `language` is the forced language code (e.g. "en"); `None` lets whisper
|
|
@@ -698,6 +752,10 @@ mod macos {
|
|
|
698
752
|
const VOICE_RMS_THRESHOLD: f32 = 0.006;
|
|
699
753
|
/// A second, model-level gate for ambient/no-speech Whisper segments.
|
|
700
754
|
const MAX_NO_SPEECH_PROBABILITY: f32 = 0.72;
|
|
755
|
+
/// RNNoise speech probability required for a 48 kHz microphone frame to
|
|
756
|
+
/// count as voice. System audio and non-48 kHz legacy mic captures keep
|
|
757
|
+
/// the RMS path because this VAD is trained for 48 kHz microphone PCM.
|
|
758
|
+
const MIN_MIC_VAD_PROBABILITY: f32 = 0.6;
|
|
701
759
|
/// Minimum average per-token probability for a segment to count as real
|
|
702
760
|
/// speech. On noisy/near-silent audio whisper mis-detects the language and
|
|
703
761
|
/// decodes fluent-looking gibberish in another language — but those tokens
|
|
@@ -733,6 +791,11 @@ mod macos {
|
|
|
733
791
|
sum / count as f32
|
|
734
792
|
}
|
|
735
793
|
|
|
794
|
+
fn mic_voice_detected(rms: f32, vad_probability: Option<f32>) -> bool {
|
|
795
|
+
rms > VOICE_RMS_THRESHOLD
|
|
796
|
+
&& vad_probability.is_none_or(|probability| probability >= MIN_MIC_VAD_PROBABILITY)
|
|
797
|
+
}
|
|
798
|
+
|
|
736
799
|
fn buffer_start_after_drain(now: Instant, pending: Duration) -> Instant {
|
|
737
800
|
now.checked_sub(pending).unwrap_or(now)
|
|
738
801
|
}
|
|
@@ -788,6 +851,10 @@ mod macos {
|
|
|
788
851
|
let mut seen_reset_generation = stream.reset_generation.load(Ordering::SeqCst);
|
|
789
852
|
// Growing-utterance resample cache — see `IncrementalResample`.
|
|
790
853
|
let mut resample_state = IncrementalResample::new();
|
|
854
|
+
// Only the microphone stream needs a speech/noise classifier. The
|
|
855
|
+
// system stream must preserve remote speech even when it is mixed
|
|
856
|
+
// with music or other non-voice audio.
|
|
857
|
+
let mut mic_voice_activity = (stream.source == "mic").then(MicVoiceActivity::new);
|
|
791
858
|
|
|
792
859
|
while stream.running.load(Ordering::SeqCst) {
|
|
793
860
|
std::thread::sleep(Duration::from_millis(250));
|
|
@@ -800,6 +867,9 @@ mod macos {
|
|
|
800
867
|
last_infer = Instant::now() - Duration::from_secs(10);
|
|
801
868
|
had_voice = false;
|
|
802
869
|
resample_state.drop_all();
|
|
870
|
+
if let Some(vad) = mic_voice_activity.as_mut() {
|
|
871
|
+
vad.reset();
|
|
872
|
+
}
|
|
803
873
|
continue;
|
|
804
874
|
}
|
|
805
875
|
|
|
@@ -807,31 +877,40 @@ mod macos {
|
|
|
807
877
|
// poll. Inspect that tail in place instead of cloning + resampling
|
|
808
878
|
// the entire growing utterance four times per second. Full snapshots
|
|
809
879
|
// are reserved for an inference that is actually due.
|
|
810
|
-
let (raw_len, new_rms) = match stream.buf.lock() {
|
|
880
|
+
let (raw_len, new_rms, new_mic_samples) = match stream.buf.lock() {
|
|
811
881
|
Ok(b) => {
|
|
812
882
|
let raw_len = b.len();
|
|
813
|
-
let new_rms = if raw_len > last_raw_len {
|
|
883
|
+
let (new_rms, new_mic_samples) = if raw_len > last_raw_len {
|
|
814
884
|
let new = &b[last_raw_len..];
|
|
815
|
-
|
|
885
|
+
(
|
|
886
|
+
Some(
|
|
887
|
+
(new.iter().map(|x| x * x).sum::<f32>() / new.len() as f32).sqrt(),
|
|
888
|
+
),
|
|
889
|
+
(stream.source == "mic").then(|| new.to_vec()),
|
|
890
|
+
)
|
|
816
891
|
} else {
|
|
817
|
-
None
|
|
892
|
+
(None, None)
|
|
818
893
|
};
|
|
819
|
-
(raw_len, new_rms)
|
|
894
|
+
(raw_len, new_rms, new_mic_samples)
|
|
820
895
|
}
|
|
821
896
|
Err(_) => continue,
|
|
822
897
|
};
|
|
823
898
|
if stream.reset_generation.load(Ordering::SeqCst) != seen_reset_generation {
|
|
824
899
|
continue;
|
|
825
900
|
}
|
|
901
|
+
let src_rate = (stream.src_rate.load(Ordering::SeqCst) as f32).max(1.0);
|
|
826
902
|
if let Some(rms) = new_rms {
|
|
827
|
-
|
|
903
|
+
let vad_probability = mic_voice_activity
|
|
904
|
+
.as_mut()
|
|
905
|
+
.zip(new_mic_samples.as_deref())
|
|
906
|
+
.and_then(|(vad, samples)| vad.observe(samples, src_rate));
|
|
907
|
+
if mic_voice_detected(rms, vad_probability) {
|
|
828
908
|
last_voice = Instant::now();
|
|
829
909
|
had_voice = true;
|
|
830
910
|
}
|
|
831
911
|
}
|
|
832
912
|
last_raw_len = raw_len;
|
|
833
913
|
|
|
834
|
-
let src_rate = (stream.src_rate.load(Ordering::SeqCst) as f32).max(1.0);
|
|
835
914
|
let have_secs = raw_len as f32 / src_rate;
|
|
836
915
|
let silence = last_voice.elapsed();
|
|
837
916
|
|
|
@@ -878,6 +957,9 @@ mod macos {
|
|
|
878
957
|
// so the resample cache is invalid regardless of whether this
|
|
879
958
|
// utterance ran inference — rebuild fresh from whatever's left.
|
|
880
959
|
resample_state.drop_all();
|
|
960
|
+
if let Some(vad) = mic_voice_activity.as_mut() {
|
|
961
|
+
vad.reset();
|
|
962
|
+
}
|
|
881
963
|
// Advance the timeline offset so the next utterance's whisper
|
|
882
964
|
// timestamps map correctly. Inference can take seconds, and
|
|
883
965
|
// audio kept arriving throughout it, so the new buffer starts
|
|
@@ -1568,6 +1650,31 @@ mod macos {
|
|
|
1568
1650
|
);
|
|
1569
1651
|
}
|
|
1570
1652
|
|
|
1653
|
+
#[test]
|
|
1654
|
+
fn mic_voice_gate_requires_speech_probability_on_supported_captures() {
|
|
1655
|
+
assert!(!super::mic_voice_detected(0.02, Some(0.59)));
|
|
1656
|
+
assert!(!super::mic_voice_detected(0.004, Some(0.99)));
|
|
1657
|
+
assert!(super::mic_voice_detected(0.02, Some(0.6)));
|
|
1658
|
+
assert!(super::mic_voice_detected(0.02, None));
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
#[test]
|
|
1662
|
+
fn rnnoise_vad_rejects_steady_ambient_noise() {
|
|
1663
|
+
let mut vad = super::MicVoiceActivity::new();
|
|
1664
|
+
let samples: Vec<f32> = (0..(nnnoiseless::DenoiseState::FRAME_SIZE * 8))
|
|
1665
|
+
.map(|index| {
|
|
1666
|
+
let value = index.wrapping_mul(1_103_515_245usize).wrapping_add(12_345);
|
|
1667
|
+
(((value >> 16) & 0x7fff) as f32 / 16_384.0 - 1.0) * 0.012
|
|
1668
|
+
})
|
|
1669
|
+
.collect();
|
|
1670
|
+
|
|
1671
|
+
let probability = vad.observe(&samples, 48_000.0).unwrap();
|
|
1672
|
+
assert!(
|
|
1673
|
+
probability < super::MIN_MIC_VAD_PROBABILITY,
|
|
1674
|
+
"ambient noise was classified as speech: {probability:.3}"
|
|
1675
|
+
);
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1571
1678
|
#[test]
|
|
1572
1679
|
fn filters_multilingual_caption_hallucinations() {
|
|
1573
1680
|
assert_eq!(clean_transcript("ご視聴ありがとうございました"), None);
|
|
@@ -47,29 +47,34 @@ export type ScreenCaptureDisplayOptions = {
|
|
|
47
47
|
/**
|
|
48
48
|
* The `getDisplayMedia` options for a screen recording.
|
|
49
49
|
*
|
|
50
|
-
* Screen/tab audio is requested
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
* never had the problem.
|
|
50
|
+
* Screen/tab audio is requested whenever the microphone is enabled, and
|
|
51
|
+
* withheld when it's off. The mic toggle is the only audio control the
|
|
52
|
+
* recorder UI shows, so a user who turns it off reasonably expects the
|
|
53
|
+
* recording to capture no audio at all — not just their voice. Requesting
|
|
54
|
+
* system audio anyway relies on the user separately noticing and unchecking
|
|
55
|
+
* the browser's native "share tab audio" checkbox, which they may not even
|
|
56
|
+
* know exists. See Slack thread 1786086902028429: a user turned the mic off
|
|
57
|
+
* and still got system audio in the recording; the agreed fix is to gate
|
|
58
|
+
* system audio on the same toggle.
|
|
60
59
|
*
|
|
61
|
-
*
|
|
60
|
+
* When the mic IS on, audio is still requested unconditionally (not made
|
|
61
|
+
* conditional on some separate "include system audio" flag) — that used to be
|
|
62
|
+
* one toggle governing both, and with the mic off Chrome never even offered
|
|
63
|
+
* the checkbox, so 345 production recordings landed with `has_audio = false`
|
|
64
|
+
* while recording a call/demo. Declining the checkbox is still respected; the
|
|
65
|
+
* user simply gets the choice whenever the mic is on.
|
|
62
66
|
*/
|
|
63
67
|
export function screenCaptureDisplayOptions(
|
|
64
68
|
displaySurface: ScreenCaptureSurface,
|
|
69
|
+
wantsMic: boolean,
|
|
65
70
|
): ScreenCaptureDisplayOptions {
|
|
66
71
|
return {
|
|
67
72
|
video: screenCaptureVideoConstraints(displaySurface),
|
|
68
|
-
audio:
|
|
73
|
+
audio: wantsMic,
|
|
69
74
|
// Let "Browser tab" open the tab picker. preferCurrentTab turns it into a
|
|
70
75
|
// current-tab shortcut, which makes choosing another tab harder.
|
|
71
76
|
selfBrowserSurface: displaySurface === "browser" ? "include" : "exclude",
|
|
72
77
|
surfaceSwitching: "include",
|
|
73
|
-
systemAudio: "include",
|
|
78
|
+
systemAudio: wantsMic ? "include" : "exclude",
|
|
74
79
|
};
|
|
75
80
|
}
|
|
@@ -50,7 +50,7 @@ The agent modifies data in SQL, but the UI runs in the browser. SSE bridges same
|
|
|
50
50
|
|
|
51
51
|
For list/sidebar queries, use the same pattern — pass the counter into the queryKey of every list query you want to keep fresh.
|
|
52
52
|
|
|
53
|
-
4. **Fallback** polling calls `/_agent-native/poll?since=N`. It
|
|
53
|
+
4. **Fallback** polling calls `/_agent-native/poll?since=N`. It polls every 2 seconds only while an agent run is in flight; otherwise it idles at 60 seconds whether or not SSE is connected (`SSE_FALLBACK_INTERVAL_MS` and `IDLE_POLL_INTERVAL_MS`, both 60 s). A hidden tab polls no faster than every 10 seconds (`HIDDEN_POLL_INTERVAL_MS`). Polling is the universal serverless fallback: new framework writes are read from the durable `sync_events` log, while the older DB timestamp scan remains as a slower safety net for direct SQL writes and older processes.
|
|
54
54
|
|
|
55
55
|
5. When the agent writes to the database, the version increments, SSE/polling detects it, and React Query refetches the affected queries.
|
|
56
56
|
|
|
@@ -597,9 +597,7 @@ function DocumentEditorBody({
|
|
|
597
597
|
);
|
|
598
598
|
// Polls Notion sync status to drive the conflict banner / sync bar and the
|
|
599
599
|
// push-on-save path below (read via the query cache, not this return value).
|
|
600
|
-
useDocumentSyncStatus(canEdit && !isLocalFileDocument ? documentId : null
|
|
601
|
-
autoSync,
|
|
602
|
-
});
|
|
600
|
+
useDocumentSyncStatus(canEdit && !isLocalFileDocument ? documentId : null);
|
|
603
601
|
const pushDocumentToNotion = usePushDocumentToNotion(documentId);
|
|
604
602
|
const [localTitle, setLocalTitle] = useState("");
|
|
605
603
|
const [localContent, setLocalContent] = useState("");
|
|
@@ -1088,7 +1086,7 @@ function DocumentEditorBody({
|
|
|
1088
1086
|
// Pulls remain driven by the polling refetch in useDocumentSyncStatus.
|
|
1089
1087
|
if (autoSync) {
|
|
1090
1088
|
const status = queryClient.getQueryData<DocumentSyncStatus>(
|
|
1091
|
-
documentSyncStatusQueryKey(documentId
|
|
1089
|
+
documentSyncStatusQueryKey(documentId),
|
|
1092
1090
|
);
|
|
1093
1091
|
if (status?.pageId && !status.hasConflict) {
|
|
1094
1092
|
try {
|
|
@@ -1100,7 +1098,7 @@ function DocumentEditorBody({
|
|
|
1100
1098
|
flushOpenEditor: false,
|
|
1101
1099
|
});
|
|
1102
1100
|
queryClient.setQueryData(
|
|
1103
|
-
documentSyncStatusQueryKey(documentId
|
|
1101
|
+
documentSyncStatusQueryKey(documentId),
|
|
1104
1102
|
next,
|
|
1105
1103
|
);
|
|
1106
1104
|
} catch {
|
|
@@ -510,9 +510,6 @@ export function DocumentToolbar({
|
|
|
510
510
|
const { data: connection } = useNotionConnection();
|
|
511
511
|
const { data: syncStatus } = useDocumentSyncStatus(
|
|
512
512
|
canEdit && !isLocalFileDocument ? documentId : null,
|
|
513
|
-
{
|
|
514
|
-
autoSync,
|
|
515
|
-
},
|
|
516
513
|
);
|
|
517
514
|
const linkDocument = useLinkDocumentToNotion(documentId);
|
|
518
515
|
const unlinkDocument = useUnlinkDocumentFromNotion(documentId);
|
|
@@ -4,7 +4,6 @@ import { useState } from "react";
|
|
|
4
4
|
import { toast } from "sonner";
|
|
5
5
|
|
|
6
6
|
import { Button } from "@/components/ui/button";
|
|
7
|
-
import { useLocalStorage } from "@/hooks/use-local-storage";
|
|
8
7
|
import {
|
|
9
8
|
useDocumentSyncStatus,
|
|
10
9
|
useResolveDocumentSyncConflict,
|
|
@@ -20,11 +19,8 @@ export function NotionConflictBanner({
|
|
|
20
19
|
canEdit = true,
|
|
21
20
|
}: NotionConflictBannerProps) {
|
|
22
21
|
const t = useT();
|
|
23
|
-
// Share autoSync state (and React Query cache) with DocumentToolbar.
|
|
24
|
-
const [autoSync] = useLocalStorage(`notion-auto-sync:${documentId}`, false);
|
|
25
22
|
const { data: syncStatus } = useDocumentSyncStatus(
|
|
26
23
|
canEdit ? documentId : null,
|
|
27
|
-
{ autoSync },
|
|
28
24
|
);
|
|
29
25
|
const resolveConflict = useResolveDocumentSyncConflict(documentId);
|
|
30
26
|
const [direction, setDirection] = useState<"pull" | "push" | null>(null);
|
|
@@ -15,6 +15,7 @@ import { toast } from "sonner";
|
|
|
15
15
|
import { Button } from "@/components/ui/button";
|
|
16
16
|
import { Input } from "@/components/ui/input";
|
|
17
17
|
import {
|
|
18
|
+
invalidateDocumentQueries,
|
|
18
19
|
useDisconnectNotion,
|
|
19
20
|
useDocumentSyncStatus,
|
|
20
21
|
useLinkDocumentToNotion,
|
|
@@ -51,7 +52,9 @@ export function NotionSyncBar({ documentId }: NotionSyncBarProps) {
|
|
|
51
52
|
lastSyncedRef.current &&
|
|
52
53
|
lastSyncedRef.current !== syncStatus.lastSyncedAt
|
|
53
54
|
) {
|
|
54
|
-
|
|
55
|
+
// Bare ["action"] refetches every mounted query app-wide (sidebar tree,
|
|
56
|
+
// comments, database views, search, connection status) on each sync tick.
|
|
57
|
+
invalidateDocumentQueries(queryClient, documentId);
|
|
55
58
|
}
|
|
56
59
|
lastSyncedRef.current = syncStatus.lastSyncedAt;
|
|
57
60
|
}, [syncStatus?.lastSyncedAt, queryClient, documentId]);
|
|
@@ -16,6 +16,8 @@ import type {
|
|
|
16
16
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|
17
17
|
import { useEffect, useRef } from "react";
|
|
18
18
|
|
|
19
|
+
import { useLocalStorage } from "@/hooks/use-local-storage";
|
|
20
|
+
|
|
19
21
|
import { documentQueryFilter } from "./use-documents";
|
|
20
22
|
|
|
21
23
|
// The server signs a `redirect` query param into the OAuth `state` and the
|
|
@@ -64,20 +66,16 @@ export function invalidateDocumentQueries(
|
|
|
64
66
|
queryClient.invalidateQueries({
|
|
65
67
|
queryKey: documentSyncStatusQueryKey(documentId),
|
|
66
68
|
});
|
|
67
|
-
queryClient.invalidateQueries({
|
|
68
|
-
queryKey: documentSyncStatusQueryKey(documentId, { autoSync: true }),
|
|
69
|
-
});
|
|
70
69
|
}
|
|
71
70
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
) {
|
|
76
|
-
const normalizedDocumentId = documentId.trim();
|
|
71
|
+
// `autoSync` only decides how often to refetch, so keying on it split one
|
|
72
|
+
// document's status across two cache entries and two independent poll loops —
|
|
73
|
+
// the toolbar polling at 2s while the sync bar polled the same action at 30s.
|
|
74
|
+
export function documentSyncStatusQueryKey(documentId: string) {
|
|
77
75
|
return [
|
|
78
76
|
"action",
|
|
79
77
|
"refresh-notion-sync-status",
|
|
80
|
-
{ documentId:
|
|
78
|
+
{ documentId: documentId.trim() },
|
|
81
79
|
] as const;
|
|
82
80
|
}
|
|
83
81
|
|
|
@@ -119,17 +117,21 @@ export function documentSyncRefetchIntervalMs(
|
|
|
119
117
|
return autoSync ? 2_000 : 30_000;
|
|
120
118
|
}
|
|
121
119
|
|
|
122
|
-
export function useDocumentSyncStatus(
|
|
123
|
-
documentId: string | null,
|
|
124
|
-
options?: { autoSync?: boolean },
|
|
125
|
-
) {
|
|
120
|
+
export function useDocumentSyncStatus(documentId: string | null) {
|
|
126
121
|
const queryClient = useQueryClient();
|
|
127
122
|
const lastObservedSyncedAtRef = useRef<string | null>(null);
|
|
128
123
|
const normalizedDocumentId = documentId?.trim() || null;
|
|
129
|
-
|
|
124
|
+
// `autoSync` changes what the server does, so every observer of the shared
|
|
125
|
+
// query key has to agree on it. Read the same per-document toggle the
|
|
126
|
+
// toolbar writes instead of taking it per mount, where one component could
|
|
127
|
+
// suppress or enable another's auto-sync through the shared query function.
|
|
128
|
+
const [autoSync] = useLocalStorage(
|
|
129
|
+
`notion-auto-sync:${normalizedDocumentId ?? ""}`,
|
|
130
|
+
false,
|
|
131
|
+
);
|
|
130
132
|
const query = useQuery<DocumentSyncStatus>({
|
|
131
133
|
queryKey: normalizedDocumentId
|
|
132
|
-
? documentSyncStatusQueryKey(normalizedDocumentId
|
|
134
|
+
? documentSyncStatusQueryKey(normalizedDocumentId)
|
|
133
135
|
: ["action", "refresh-notion-sync-status", null],
|
|
134
136
|
queryFn: () => {
|
|
135
137
|
if (!normalizedDocumentId) throw new Error("documentId is required");
|
|
@@ -50,7 +50,7 @@ The agent modifies data in SQL, but the UI runs in the browser. SSE bridges same
|
|
|
50
50
|
|
|
51
51
|
For list/sidebar queries, use the same pattern — pass the counter into the queryKey of every list query you want to keep fresh.
|
|
52
52
|
|
|
53
|
-
4. **Fallback** polling calls `/_agent-native/poll?since=N`. It
|
|
53
|
+
4. **Fallback** polling calls `/_agent-native/poll?since=N`. It polls every 2 seconds only while an agent run is in flight; otherwise it idles at 60 seconds whether or not SSE is connected (`SSE_FALLBACK_INTERVAL_MS` and `IDLE_POLL_INTERVAL_MS`, both 60 s). A hidden tab polls no faster than every 10 seconds (`HIDDEN_POLL_INTERVAL_MS`). Polling is the universal serverless fallback: new framework writes are read from the durable `sync_events` log, while the older DB timestamp scan remains as a slower safety net for direct SQL writes and older processes.
|
|
54
54
|
|
|
55
55
|
5. When the agent writes to the database, the version increments, SSE/polling detects it, and React Query refetches the affected queries.
|
|
56
56
|
|
|
@@ -50,7 +50,7 @@ The agent modifies data in SQL, but the UI runs in the browser. SSE bridges same
|
|
|
50
50
|
|
|
51
51
|
For list/sidebar queries, use the same pattern — pass the counter into the queryKey of every list query you want to keep fresh.
|
|
52
52
|
|
|
53
|
-
4. **Fallback** polling calls `/_agent-native/poll?since=N`. It
|
|
53
|
+
4. **Fallback** polling calls `/_agent-native/poll?since=N`. It polls every 2 seconds only while an agent run is in flight; otherwise it idles at 60 seconds whether or not SSE is connected (`SSE_FALLBACK_INTERVAL_MS` and `IDLE_POLL_INTERVAL_MS`, both 60 s). A hidden tab polls no faster than every 10 seconds (`HIDDEN_POLL_INTERVAL_MS`). Polling is the universal serverless fallback: new framework writes are read from the durable `sync_events` log, while the older DB timestamp scan remains as a slower safety net for direct SQL writes and older processes.
|
|
54
54
|
|
|
55
55
|
5. When the agent writes to the database, the version increments, SSE/polling detects it, and React Query refetches the affected queries.
|
|
56
56
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { UseDesignHotkeysProps } from "@/hooks/useDesignHotkeys";
|
|
2
|
+
import { SHOW_DESIGN_SECONDARY_LEFT_PANELS } from "@/pages/design-editor/types";
|
|
2
3
|
|
|
3
4
|
export const DESIGN_SHORTCUT_CATEGORIES = [
|
|
4
5
|
"essential",
|
|
@@ -136,13 +137,18 @@ export const DESIGN_SHORTCUTS: readonly DesignShortcutDefinition[] = [
|
|
|
136
137
|
labelKey: "designEditor.keyboardShortcuts.commands.showLayers",
|
|
137
138
|
handler: "onShowLayersPanel",
|
|
138
139
|
}),
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
140
|
+
...(SHOW_DESIGN_SECONDARY_LEFT_PANELS
|
|
141
|
+
? [
|
|
142
|
+
shortcut({
|
|
143
|
+
id: "show-assets",
|
|
144
|
+
category: "view" as const,
|
|
145
|
+
bindings: ["alt+2"],
|
|
146
|
+
labelKey:
|
|
147
|
+
"designEditor.keyboardShortcuts.commands.showAssets" as const,
|
|
148
|
+
handler: "onShowAssetsPanel" as const,
|
|
149
|
+
}),
|
|
150
|
+
]
|
|
151
|
+
: []),
|
|
146
152
|
shortcut({
|
|
147
153
|
id: "toggle-ui",
|
|
148
154
|
category: "view",
|
|
@@ -6,6 +6,8 @@ import { useAgentRouteState } from "@agent-native/core/client/navigation";
|
|
|
6
6
|
import { useEffect } from "react";
|
|
7
7
|
import { useLocation, useParams } from "react-router";
|
|
8
8
|
|
|
9
|
+
import { normalizeDesignLeftPanel } from "@/pages/design-editor/tool-state";
|
|
10
|
+
|
|
9
11
|
export interface NavigationState {
|
|
10
12
|
view: string;
|
|
11
13
|
designId?: string;
|
|
@@ -120,12 +122,11 @@ function normalizeEditorView(
|
|
|
120
122
|
|
|
121
123
|
function normalizeInspectorTab(
|
|
122
124
|
value: unknown,
|
|
123
|
-
): "design" | "comments" | "tweaks" | "code" |
|
|
125
|
+
): "design" | "comments" | "tweaks" | "code" | undefined {
|
|
124
126
|
return value === "design" ||
|
|
125
127
|
value === "comments" ||
|
|
126
128
|
value === "tweaks" ||
|
|
127
|
-
value === "code"
|
|
128
|
-
value === "extensions"
|
|
129
|
+
value === "code"
|
|
129
130
|
? value
|
|
130
131
|
: undefined;
|
|
131
132
|
}
|
|
@@ -141,16 +142,7 @@ function normalizeLeftPanel(
|
|
|
141
142
|
| "import"
|
|
142
143
|
| "code"
|
|
143
144
|
| undefined {
|
|
144
|
-
|
|
145
|
-
return value === "file" ||
|
|
146
|
-
value === "agent" ||
|
|
147
|
-
value === "assets" ||
|
|
148
|
-
value === "tools" ||
|
|
149
|
-
value === "tokens" ||
|
|
150
|
-
value === "import" ||
|
|
151
|
-
value === "code"
|
|
152
|
-
? value
|
|
153
|
-
: undefined;
|
|
145
|
+
return normalizeDesignLeftPanel(value);
|
|
154
146
|
}
|
|
155
147
|
|
|
156
148
|
function normalizeEditorMode(
|
|
@@ -176,9 +168,12 @@ export function editorPathFromCommand(cmd: NavigationState): string | null {
|
|
|
176
168
|
const editorView = normalizeEditorView(cmd.editorView);
|
|
177
169
|
if (editorView) params.set("view", editorView);
|
|
178
170
|
if (editorView === "single") params.set("mode", cmd.mode ?? "interact");
|
|
179
|
-
const
|
|
171
|
+
const rawInspectorTab = cmd.inspectorTab ?? cmd.inspector;
|
|
172
|
+
const inspectorTab = normalizeInspectorTab(rawInspectorTab);
|
|
180
173
|
if (inspectorTab) params.set("inspector", inspectorTab);
|
|
181
|
-
const leftPanel = normalizeLeftPanel(
|
|
174
|
+
const leftPanel = normalizeLeftPanel(
|
|
175
|
+
cmd.leftPanel ?? cmd.panel ?? rawInspectorTab,
|
|
176
|
+
);
|
|
182
177
|
if (leftPanel) params.set("panel", leftPanel);
|
|
183
178
|
const screen = cmd.fileId ?? cmd.screenId ?? cmd.filename ?? cmd.screen;
|
|
184
179
|
if (screen) params.set("screen", screen);
|
|
@@ -201,10 +196,11 @@ export function editorCommandFromNavigate(
|
|
|
201
196
|
): DesignEditorCommand | null {
|
|
202
197
|
if (cmd.view !== "editor" || !cmd.designId) return null;
|
|
203
198
|
const editorView = normalizeEditorView(cmd.editorView);
|
|
204
|
-
const
|
|
199
|
+
const rawInspectorTab = cmd.inspectorTab ?? cmd.inspector;
|
|
200
|
+
const inspectorTab = normalizeInspectorTab(rawInspectorTab);
|
|
205
201
|
const leftPanel =
|
|
206
202
|
normalizeLeftPanel(cmd.leftPanel ?? cmd.panel) ??
|
|
207
|
-
normalizeLeftPanel(
|
|
203
|
+
normalizeLeftPanel(rawInspectorTab);
|
|
208
204
|
const command: DesignEditorCommand = {
|
|
209
205
|
designId: cmd.designId,
|
|
210
206
|
issuedAt: Date.now(),
|
|
@@ -255,11 +251,12 @@ export function useNavigationState(enabled = true) {
|
|
|
255
251
|
if (editorView) state.editorView = editorView;
|
|
256
252
|
const mode = normalizeEditorMode(searchParams.get("mode"));
|
|
257
253
|
if (mode) state.mode = mode;
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
);
|
|
254
|
+
const rawInspectorTab = searchParams.get("inspector");
|
|
255
|
+
const inspectorTab = normalizeInspectorTab(rawInspectorTab);
|
|
261
256
|
if (inspectorTab) state.inspectorTab = inspectorTab;
|
|
262
|
-
const leftPanel = normalizeLeftPanel(
|
|
257
|
+
const leftPanel = normalizeLeftPanel(
|
|
258
|
+
searchParams.get("panel") ?? rawInspectorTab,
|
|
259
|
+
);
|
|
263
260
|
if (leftPanel) state.leftPanel = leftPanel;
|
|
264
261
|
const screen = searchParams.get("screen");
|
|
265
262
|
if (screen) state.screen = screen;
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { isStandaloneHttpUrl } from "@shared/html-content";
|
|
2
2
|
|
|
3
|
+
import { normalizeDesignLeftPanel } from "./tool-state";
|
|
3
4
|
import {
|
|
4
5
|
type DesignFile,
|
|
5
6
|
type DesignLeftPanel,
|
|
6
7
|
type DesignTool,
|
|
7
8
|
type EditorMode,
|
|
8
|
-
SHOW_DESIGN_CODE_LEFT_PANEL,
|
|
9
9
|
} from "./types";
|
|
10
10
|
|
|
11
11
|
// Definition moved to shared/ so the server write path and code-layer's
|
|
@@ -127,10 +127,7 @@ export function getDesignEditorStateUrlSearch(args: {
|
|
|
127
127
|
mode?: EditorMode | null;
|
|
128
128
|
}) {
|
|
129
129
|
const params = new URLSearchParams(args.currentSearch);
|
|
130
|
-
const leftPanel =
|
|
131
|
-
args.leftPanel === "code" && !SHOW_DESIGN_CODE_LEFT_PANEL
|
|
132
|
-
? null
|
|
133
|
-
: args.leftPanel;
|
|
130
|
+
const leftPanel = normalizeDesignLeftPanel(args.leftPanel) ?? null;
|
|
134
131
|
params.set("view", args.viewMode);
|
|
135
132
|
if (leftPanel && leftPanel !== "file") {
|
|
136
133
|
params.set("panel", leftPanel);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { CreationTool } from "@/components/design/design-canvas/creation";
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
-
|
|
4
|
+
isDesignLeftPanelEnabled,
|
|
5
5
|
type DesignLeftPanel,
|
|
6
6
|
type DesignTool,
|
|
7
7
|
type EditorMode,
|
|
@@ -10,18 +10,10 @@ import {
|
|
|
10
10
|
export function normalizeDesignLeftPanel(
|
|
11
11
|
value: unknown,
|
|
12
12
|
): DesignLeftPanel | undefined {
|
|
13
|
-
if (value === "extensions")
|
|
14
|
-
|
|
15
|
-
return SHOW_DESIGN_CODE_LEFT_PANEL ? "code" : undefined;
|
|
13
|
+
if (value === "extensions") {
|
|
14
|
+
return isDesignLeftPanelEnabled("tools") ? "tools" : undefined;
|
|
16
15
|
}
|
|
17
|
-
return value
|
|
18
|
-
value === "agent" ||
|
|
19
|
-
value === "assets" ||
|
|
20
|
-
value === "tools" ||
|
|
21
|
-
value === "tokens" ||
|
|
22
|
-
value === "import"
|
|
23
|
-
? value
|
|
24
|
-
: undefined;
|
|
16
|
+
return isDesignLeftPanelEnabled(value) ? value : undefined;
|
|
25
17
|
}
|
|
26
18
|
|
|
27
19
|
export const MOVE_GROUP_TOOL_PRESENTATIONS = {
|
|
@@ -33,7 +33,44 @@ export type DesignLeftPanel =
|
|
|
33
33
|
| "import"
|
|
34
34
|
| "code";
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
/**
|
|
37
|
+
* Keep the still-evolving Design editor side panels out of the default
|
|
38
|
+
* surface until their Figma parity work is ready. Flip this switch to expose
|
|
39
|
+
* Assets, Tools, Tokens, and Code together while iterating on them. The E2E
|
|
40
|
+
* harness sets the Vite flag so those existing advanced-surface tests keep
|
|
41
|
+
* exercising the gated panels.
|
|
42
|
+
*/
|
|
43
|
+
const designSecondaryLeftPanelsSetting =
|
|
44
|
+
typeof import.meta !== "undefined"
|
|
45
|
+
? (import.meta as { env?: Record<string, string> }).env
|
|
46
|
+
?.VITE_SHOW_DESIGN_SECONDARY_LEFT_PANELS
|
|
47
|
+
: undefined;
|
|
48
|
+
export const SHOW_DESIGN_SECONDARY_LEFT_PANELS =
|
|
49
|
+
designSecondaryLeftPanelsSetting === "1" ||
|
|
50
|
+
designSecondaryLeftPanelsSetting === "true";
|
|
51
|
+
export const SHOW_DESIGN_CODE_LEFT_PANEL = SHOW_DESIGN_SECONDARY_LEFT_PANELS;
|
|
52
|
+
|
|
53
|
+
export function isDesignLeftPanelEnabled(
|
|
54
|
+
value: unknown,
|
|
55
|
+
): value is DesignLeftPanel {
|
|
56
|
+
if (
|
|
57
|
+
value !== "file" &&
|
|
58
|
+
value !== "agent" &&
|
|
59
|
+
value !== "assets" &&
|
|
60
|
+
value !== "tools" &&
|
|
61
|
+
value !== "tokens" &&
|
|
62
|
+
value !== "import" &&
|
|
63
|
+
value !== "code"
|
|
64
|
+
) {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (value === "code") return SHOW_DESIGN_CODE_LEFT_PANEL;
|
|
69
|
+
if (value === "assets" || value === "tools" || value === "tokens") {
|
|
70
|
+
return SHOW_DESIGN_SECONDARY_LEFT_PANELS;
|
|
71
|
+
}
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
37
74
|
|
|
38
75
|
/** Zoom percentage applied when entering single-screen (focused) editor mode. */
|
|
39
76
|
export const FOCUSED_SCREEN_ZOOM = 100;
|