@agent-native/core 0.144.3 → 0.145.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/README.md +1 -1
- package/corpus/templates/clips/changelog/2026-08-06-desktop-full-screen-recordings-now-normalize-microphone-leve.md +6 -0
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen/custom_capture.rs +185 -69
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +47 -4
- package/corpus/templates/forms/server/plugins/agent-chat.ts +1 -1
- package/corpus/templates/slides/server/plugins/agent-chat.ts +1 -1
- package/dist/agent/production-agent.d.ts +6 -0
- package/dist/agent/run-manager.d.ts +44 -1
- package/dist/agent/run-manager.js +70 -8
- package/dist/chat-threads/store.js +6 -4
- package/dist/client/use-db-sync.js +9 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/db/ddl-guard.d.ts +8 -0
- package/dist/db/ddl-guard.js +148 -0
- package/dist/deploy/build.d.ts +36 -0
- package/dist/deploy/build.js +78 -5
- package/dist/framework-tools.d.ts +126 -0
- package/dist/framework-tools.js +114 -0
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/org/accept-pending.js +2 -2
- package/dist/org/auth-policy.js +4 -0
- package/dist/org/auto-join-domain.d.ts +9 -0
- package/dist/org/auto-join-domain.js +53 -4
- package/dist/org/context.js +12 -3
- package/dist/org/handlers.js +17 -5
- package/dist/org/request-org-cache.d.ts +24 -2
- package/dist/org/request-org-cache.js +57 -2
- package/dist/provider-api/actions/custom-provider-registration.d.ts +6 -6
- package/dist/provider-api/actions/provider-api.d.ts +6 -6
- package/dist/resources/store.d.ts +1 -1
- package/dist/resources/store.js +96 -7
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/action-discovery.d.ts +27 -0
- package/dist/server/action-discovery.js +89 -0
- package/dist/server/agent-chat/action-filters-a2a.d.ts +11 -0
- package/dist/server/agent-chat/action-filters-a2a.js +17 -1
- package/dist/server/agent-chat/framework-prompts.d.ts +2 -0
- package/dist/server/agent-chat/plugin-options.d.ts +30 -0
- package/dist/server/agent-chat/prompt-resources.d.ts +4 -1
- package/dist/server/agent-chat/prompt-resources.js +8 -2
- package/dist/server/agent-chat-plugin.js +138 -75
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/auth.d.ts +3 -0
- package/dist/server/auth.js +22 -1
- package/dist/server/collab-plugin.js +10 -4
- package/dist/server/core-routes-plugin.js +3 -0
- package/dist/server/prompts/framework-core-compact.d.ts +5 -0
- package/dist/server/prompts/framework-core-compact.js +28 -12
- package/dist/server/prompts/framework-core.d.ts +5 -0
- package/dist/server/prompts/framework-core.js +33 -16
- package/dist/server/prompts/index.d.ts +1 -1
- package/dist/server/prompts/index.js +1 -1
- package/dist/server/prompts/shared-rules.d.ts +12 -4
- package/dist/server/prompts/shared-rules.js +14 -4
- package/dist/server/session-email-cache.d.ts +14 -0
- package/dist/server/session-email-cache.js +48 -0
- package/dist/server/sweep-runtime.d.ts +22 -0
- package/dist/server/sweep-runtime.js +28 -0
- package/dist/settings/store.js +63 -7
- package/dist/shared/ttl-cache.d.ts +59 -0
- package/dist/shared/ttl-cache.js +43 -0
- package/dist/sharing/actions/list-resource-shares.d.ts +3 -3
- package/dist/sharing/actions/list-resource-shares.js +4 -1
- package/dist/templates/workspace-core/.agents/skills/extensions/SKILL.md +6 -3
- package/dist/triggers/condition-evaluator.js +11 -27
- package/docs/content/deployment.mdx +95 -13
- package/docs/content/environment-variables.mdx +5 -0
- package/docs/content/key-concepts.mdx +13 -6
- package/package.json +1 -1
- package/src/templates/workspace-core/.agents/skills/extensions/SKILL.md +6 -3
package/corpus/README.md
CHANGED
|
@@ -2145,6 +2145,175 @@ const MIX_SOURCE_GRACE: Duration = Duration::from_millis(2000);
|
|
|
2145
2145
|
const AUDIO_FORMAT_LPCM: u32 = 0x6C70_636D; // 'lpcm'
|
|
2146
2146
|
const AUDIO_FORMAT_FLAGS_FLOAT_PACKED: u32 = 1 | 8; // float + packed, interleaved, little-endian
|
|
2147
2147
|
|
|
2148
|
+
/// Speech RMS the mic auto-gain aims for (~-20 dBFS), and how far it may
|
|
2149
|
+
/// travel to get there. It never attenuates: a hot mic keeps its own level
|
|
2150
|
+
/// and the limiter below owns peak safety.
|
|
2151
|
+
const MIC_AGC_TARGET_RMS: f32 = 0.1;
|
|
2152
|
+
const MIC_AGC_MIN_GAIN: f32 = 1.0;
|
|
2153
|
+
const MIC_AGC_MAX_GAIN: f32 = 8.0; // +18 dB
|
|
2154
|
+
/// Below this RMS the mic is between words: hold the current gain rather than
|
|
2155
|
+
/// chasing the target, or every pause swells room tone to speech level.
|
|
2156
|
+
///
|
|
2157
|
+
/// Set at ~-60 dBFS, not the -50 dBFS that room tone alone would justify. A
|
|
2158
|
+
/// quiet built-in MacBook mic — the whole reason this stage exists — can carry
|
|
2159
|
+
/// speech at -45 dBFS RMS, and a floor set just under that would gate out the
|
|
2160
|
+
/// exact signal it is supposed to lift. Worst case here is room tone amplified
|
|
2161
|
+
/// to 0.001 × MIC_AGC_MAX_GAIN, still inaudible.
|
|
2162
|
+
const MIC_AGC_NOISE_FLOOR_RMS: f32 = 0.001;
|
|
2163
|
+
const MIC_AGC_ENVELOPE_SECONDS: f32 = 0.15;
|
|
2164
|
+
/// Asymmetric: back off quickly when the mic gets loud (the limiter should be
|
|
2165
|
+
/// a backstop, not the thing shaping the sound), return slowly so a boost
|
|
2166
|
+
/// doesn't audibly pump across sentences.
|
|
2167
|
+
const MIC_AGC_DUCK_SECONDS: f32 = 0.08;
|
|
2168
|
+
const MIC_AGC_BOOST_SECONDS: f32 = 1.5;
|
|
2169
|
+
/// Until the first speech has been levelled, converge at this rate instead of
|
|
2170
|
+
/// `MIC_AGC_BOOST_SECONDS`. Gain starts at unity, so a quiet mic needs to
|
|
2171
|
+
/// travel most of its range; at the steady-state rate that takes several
|
|
2172
|
+
/// seconds and the opening sentence audibly swells.
|
|
2173
|
+
const MIC_AGC_WARMUP_SECONDS: f32 = 0.2;
|
|
2174
|
+
/// How close to the target counts as levelled and ends the warmup.
|
|
2175
|
+
const MIC_AGC_CONVERGED_TOLERANCE: f32 = 0.05;
|
|
2176
|
+
/// -1.0 dBFS, near the true-peak ceiling the offline loudnorm chain targets.
|
|
2177
|
+
const MIX_LIMIT_CEILING: f32 = 0.891;
|
|
2178
|
+
const MIX_LIMIT_RELEASE_SECONDS: f32 = 0.15;
|
|
2179
|
+
|
|
2180
|
+
/// One-pole smoothing coefficient for a time constant at the mixer's rate.
|
|
2181
|
+
///
|
|
2182
|
+
/// `exp_m1`, not `1.0 - exp(x)`: at 48 kHz the exponent is ~1e-5, so the
|
|
2183
|
+
/// subtraction form cancels away most of the f32 mantissa and the longer time
|
|
2184
|
+
/// constants come out with about three significant digits.
|
|
2185
|
+
fn one_pole_coefficient(tau_seconds: f32, sample_rate: i32) -> f32 {
|
|
2186
|
+
if tau_seconds <= 0.0 || sample_rate <= 0 {
|
|
2187
|
+
return 1.0;
|
|
2188
|
+
}
|
|
2189
|
+
-(-1.0 / (tau_seconds * sample_rate as f32)).exp_m1()
|
|
2190
|
+
}
|
|
2191
|
+
|
|
2192
|
+
/// Causal mic auto-gain.
|
|
2193
|
+
///
|
|
2194
|
+
/// ScreenCaptureKit captures without AGC — unlike the browser path, which gets
|
|
2195
|
+
/// one from `autoGainControl` — so a MacBook mic lands far below the -16 LUFS
|
|
2196
|
+
/// the offline chain in `native_screen.rs` normalizes to. Live-uploaded clips
|
|
2197
|
+
/// never reach that chain: their bytes stream to the server while the writer
|
|
2198
|
+
/// is still producing them, so nothing downstream can re-read the file. Gain
|
|
2199
|
+
/// has to be decided here, from what has already been heard.
|
|
2200
|
+
struct MicAutoGain {
|
|
2201
|
+
mean_square: f32,
|
|
2202
|
+
gain: f32,
|
|
2203
|
+
/// False until the first speech has been levelled; see
|
|
2204
|
+
/// `MIC_AGC_WARMUP_SECONDS`.
|
|
2205
|
+
levelled: bool,
|
|
2206
|
+
envelope_coefficient: f32,
|
|
2207
|
+
warmup_coefficient: f32,
|
|
2208
|
+
duck_coefficient: f32,
|
|
2209
|
+
boost_coefficient: f32,
|
|
2210
|
+
}
|
|
2211
|
+
|
|
2212
|
+
impl MicAutoGain {
|
|
2213
|
+
fn new(sample_rate: i32) -> Self {
|
|
2214
|
+
Self {
|
|
2215
|
+
mean_square: 0.0,
|
|
2216
|
+
gain: 1.0,
|
|
2217
|
+
levelled: false,
|
|
2218
|
+
envelope_coefficient: one_pole_coefficient(MIC_AGC_ENVELOPE_SECONDS, sample_rate),
|
|
2219
|
+
warmup_coefficient: one_pole_coefficient(MIC_AGC_WARMUP_SECONDS, sample_rate),
|
|
2220
|
+
duck_coefficient: one_pole_coefficient(MIC_AGC_DUCK_SECONDS, sample_rate),
|
|
2221
|
+
boost_coefficient: one_pole_coefficient(MIC_AGC_BOOST_SECONDS, sample_rate),
|
|
2222
|
+
}
|
|
2223
|
+
}
|
|
2224
|
+
|
|
2225
|
+
fn next_gain(&mut self, left: f32, right: f32) -> f32 {
|
|
2226
|
+
let peak = left.abs().max(right.abs());
|
|
2227
|
+
self.mean_square += (peak * peak - self.mean_square) * self.envelope_coefficient;
|
|
2228
|
+
let rms = self.mean_square.max(0.0).sqrt();
|
|
2229
|
+
if rms >= MIC_AGC_NOISE_FLOOR_RMS {
|
|
2230
|
+
let target = (MIC_AGC_TARGET_RMS / rms).clamp(MIC_AGC_MIN_GAIN, MIC_AGC_MAX_GAIN);
|
|
2231
|
+
let coefficient = if !self.levelled {
|
|
2232
|
+
self.warmup_coefficient
|
|
2233
|
+
} else if target < self.gain {
|
|
2234
|
+
self.duck_coefficient
|
|
2235
|
+
} else {
|
|
2236
|
+
self.boost_coefficient
|
|
2237
|
+
};
|
|
2238
|
+
self.gain += (target - self.gain) * coefficient;
|
|
2239
|
+
if !self.levelled && (self.gain - target).abs() <= target * MIC_AGC_CONVERGED_TOLERANCE
|
|
2240
|
+
{
|
|
2241
|
+
self.levelled = true;
|
|
2242
|
+
// Once per recording. A "clip is too quiet" report is only
|
|
2243
|
+
// diagnosable if the log says what the mic measured and how
|
|
2244
|
+
// much was added; `gain` pinned at MIC_AGC_MAX_GAIN means the
|
|
2245
|
+
// cap, not the target, decided the level.
|
|
2246
|
+
crate::logfile::diagnostic(&format!(
|
|
2247
|
+
"[capture-health] mic auto-gain levelled: rms={rms:.6} gain={:.2} capped={}",
|
|
2248
|
+
self.gain,
|
|
2249
|
+
self.gain >= MIC_AGC_MAX_GAIN * 0.99
|
|
2250
|
+
));
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
self.gain
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
|
|
2257
|
+
/// Zero-latency peak limiter over the summed mix.
|
|
2258
|
+
///
|
|
2259
|
+
/// This replaces a flat 0.5x weight on each source, which bought clip safety
|
|
2260
|
+
/// by discarding 6 dB whether or not anything was near full scale. Lookahead
|
|
2261
|
+
/// would buy cleaner transients but costs an output delay, and the live
|
|
2262
|
+
/// uploader has already streamed the frames preceding it.
|
|
2263
|
+
struct PeakLimiter {
|
|
2264
|
+
gain: f32,
|
|
2265
|
+
release_coefficient: f32,
|
|
2266
|
+
}
|
|
2267
|
+
|
|
2268
|
+
impl PeakLimiter {
|
|
2269
|
+
fn new(sample_rate: i32) -> Self {
|
|
2270
|
+
Self {
|
|
2271
|
+
gain: 1.0,
|
|
2272
|
+
release_coefficient: one_pole_coefficient(MIX_LIMIT_RELEASE_SECONDS, sample_rate),
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2275
|
+
|
|
2276
|
+
fn next_gain(&mut self, peak: f32) -> f32 {
|
|
2277
|
+
self.gain += (1.0 - self.gain) * self.release_coefficient;
|
|
2278
|
+
if peak > MIX_LIMIT_CEILING {
|
|
2279
|
+
self.gain = self.gain.min(MIX_LIMIT_CEILING / peak);
|
|
2280
|
+
}
|
|
2281
|
+
self.gain
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2284
|
+
|
|
2285
|
+
/// The mixer's whole gain stage: auto-gain the mic, sum with system audio at
|
|
2286
|
+
/// unity, limit the result. Kept separate from `LiveAudioMixer` so the mix rule
|
|
2287
|
+
/// is reachable without constructing a `CMFormatDescription` — the reported bug
|
|
2288
|
+
/// was a wrong per-frame weight, so that arithmetic is what needs covering.
|
|
2289
|
+
struct MixGainStage {
|
|
2290
|
+
mic: MicAutoGain,
|
|
2291
|
+
limiter: PeakLimiter,
|
|
2292
|
+
}
|
|
2293
|
+
|
|
2294
|
+
impl MixGainStage {
|
|
2295
|
+
fn new(sample_rate: i32) -> Self {
|
|
2296
|
+
Self {
|
|
2297
|
+
mic: MicAutoGain::new(sample_rate),
|
|
2298
|
+
limiter: PeakLimiter::new(sample_rate),
|
|
2299
|
+
}
|
|
2300
|
+
}
|
|
2301
|
+
|
|
2302
|
+
/// One output frame. Neither source is pre-attenuated: system audio passes
|
|
2303
|
+
/// at unity and the mic is only ever boosted, with the limiter owning peak
|
|
2304
|
+
/// safety for the sum.
|
|
2305
|
+
fn frame(&mut self, system: (f32, f32), mic: (f32, f32)) -> (f32, f32) {
|
|
2306
|
+
let mic_gain = self.mic.next_gain(mic.0, mic.1);
|
|
2307
|
+
let left = system.0 + mic.0 * mic_gain;
|
|
2308
|
+
let right = system.1 + mic.1 * mic_gain;
|
|
2309
|
+
let limit = self.limiter.next_gain(left.abs().max(right.abs()));
|
|
2310
|
+
(
|
|
2311
|
+
(left * limit).clamp(-1.0, 1.0),
|
|
2312
|
+
(right * limit).clamp(-1.0, 1.0),
|
|
2313
|
+
)
|
|
2314
|
+
}
|
|
2315
|
+
}
|
|
2316
|
+
|
|
2148
2317
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
|
2149
2318
|
/// Which capture stream a pushed PCM chunk came from.
|
|
2150
2319
|
enum MixSource {
|
|
@@ -2224,63 +2393,6 @@ impl MixerTimeline {
|
|
|
2224
2393
|
}
|
|
2225
2394
|
}
|
|
2226
2395
|
|
|
2227
|
-
#[cfg(test)]
|
|
2228
|
-
mod mixer_timeline_tests {
|
|
2229
|
-
use super::MixerTimeline;
|
|
2230
|
-
|
|
2231
|
-
fn stereo_frames(values: &[f32]) -> Vec<f32> {
|
|
2232
|
-
values.iter().flat_map(|value| [*value, *value]).collect()
|
|
2233
|
-
}
|
|
2234
|
-
|
|
2235
|
-
#[test]
|
|
2236
|
-
fn trims_overlapping_frames_instead_of_extending_the_timeline() {
|
|
2237
|
-
let mut timeline = MixerTimeline::new();
|
|
2238
|
-
timeline.push_at(0, &stereo_frames(&[1.0, 2.0, 3.0, 4.0]), 0, 96_000);
|
|
2239
|
-
timeline.push_at(3, &stereo_frames(&[4.0, 5.0, 6.0, 7.0]), 0, 96_000);
|
|
2240
|
-
|
|
2241
|
-
assert_eq!(
|
|
2242
|
-
timeline.samples,
|
|
2243
|
-
stereo_frames(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0])
|
|
2244
|
-
);
|
|
2245
|
-
assert_eq!(timeline.end_frame(), 7);
|
|
2246
|
-
}
|
|
2247
|
-
|
|
2248
|
-
#[test]
|
|
2249
|
-
fn ignores_fully_duplicated_buffers() {
|
|
2250
|
-
let mut timeline = MixerTimeline::new();
|
|
2251
|
-
let frames = stereo_frames(&[1.0, 2.0, 3.0]);
|
|
2252
|
-
timeline.push_at(0, &frames, 0, 96_000);
|
|
2253
|
-
timeline.push_at(0, &frames, 0, 96_000);
|
|
2254
|
-
|
|
2255
|
-
assert_eq!(timeline.samples, frames);
|
|
2256
|
-
assert_eq!(timeline.end_frame(), 3);
|
|
2257
|
-
}
|
|
2258
|
-
|
|
2259
|
-
#[test]
|
|
2260
|
-
fn trims_input_that_starts_behind_already_emitted_output() {
|
|
2261
|
-
let mut timeline = MixerTimeline::new();
|
|
2262
|
-
timeline.started = true;
|
|
2263
|
-
timeline.base_frame = 10;
|
|
2264
|
-
timeline.push_at(8, &stereo_frames(&[1.0, 2.0, 3.0, 4.0]), 10, 96_000);
|
|
2265
|
-
|
|
2266
|
-
assert_eq!(timeline.samples, stereo_frames(&[3.0, 4.0]));
|
|
2267
|
-
assert_eq!(timeline.end_frame(), 12);
|
|
2268
|
-
}
|
|
2269
|
-
|
|
2270
|
-
#[test]
|
|
2271
|
-
fn preserves_forward_timestamp_gaps_as_silence() {
|
|
2272
|
-
let mut timeline = MixerTimeline::new();
|
|
2273
|
-
timeline.push_at(0, &stereo_frames(&[1.0, 2.0]), 0, 96_000);
|
|
2274
|
-
timeline.push_at(5, &stereo_frames(&[6.0]), 0, 96_000);
|
|
2275
|
-
|
|
2276
|
-
assert_eq!(
|
|
2277
|
-
timeline.samples,
|
|
2278
|
-
stereo_frames(&[1.0, 2.0, 0.0, 0.0, 0.0, 6.0])
|
|
2279
|
-
);
|
|
2280
|
-
assert_eq!(timeline.end_frame(), 6);
|
|
2281
|
-
}
|
|
2282
|
-
}
|
|
2283
|
-
|
|
2284
2396
|
enum SourceBound {
|
|
2285
2397
|
/// Started and recently fed; bounds output to `end_frame`.
|
|
2286
2398
|
Active(i64),
|
|
@@ -2313,6 +2425,9 @@ struct LiveAudioMixer {
|
|
|
2313
2425
|
out_pos: i64,
|
|
2314
2426
|
system: MixerTimeline,
|
|
2315
2427
|
mic: MixerTimeline,
|
|
2428
|
+
/// Gain stage, carried across chunks: emitted frames are already uploaded,
|
|
2429
|
+
/// so its state is the only memory the mix has.
|
|
2430
|
+
gain: MixGainStage,
|
|
2316
2431
|
created_at: Instant,
|
|
2317
2432
|
}
|
|
2318
2433
|
|
|
@@ -2360,6 +2475,7 @@ impl LiveAudioMixer {
|
|
|
2360
2475
|
out_pos: 0,
|
|
2361
2476
|
system: MixerTimeline::new(),
|
|
2362
2477
|
mic: MixerTimeline::new(),
|
|
2478
|
+
gain: MixGainStage::new(sample_rate),
|
|
2363
2479
|
created_at: Instant::now(),
|
|
2364
2480
|
})
|
|
2365
2481
|
}
|
|
@@ -2465,15 +2581,14 @@ impl LiveAudioMixer {
|
|
|
2465
2581
|
}
|
|
2466
2582
|
|
|
2467
2583
|
/// Emit mixed sample buffers covering `out_pos..safe_end` in
|
|
2468
|
-
/// `MIX_CHUNK_FRAMES` chunks:
|
|
2469
|
-
/// as LPCM `CMSampleBuffer`s with contiguous PTS.
|
|
2584
|
+
/// `MIX_CHUNK_FRAMES` chunks: auto-gain the mic, sum with system audio,
|
|
2585
|
+
/// limit, wrap as LPCM `CMSampleBuffer`s with contiguous PTS.
|
|
2470
2586
|
///
|
|
2471
|
-
///
|
|
2472
|
-
/// signals
|
|
2473
|
-
/// routes the mic back through system audio
|
|
2474
|
-
///
|
|
2475
|
-
///
|
|
2476
|
-
/// in post-processing.
|
|
2587
|
+
/// Clip safety belongs to `PeakLimiter`, not to a fixed weight per source.
|
|
2588
|
+
/// Two full-scale signals do occur — a USB interface with software
|
|
2589
|
+
/// monitoring routes the mic back through system audio — but attenuating
|
|
2590
|
+
/// every frame for that case is what left live-uploaded clips ~6 dB below
|
|
2591
|
+
/// the rest, with no post-processing stage left to make it back up.
|
|
2477
2592
|
fn drain_ready(
|
|
2478
2593
|
&mut self,
|
|
2479
2594
|
flush: bool,
|
|
@@ -2493,10 +2608,11 @@ impl LiveAudioMixer {
|
|
|
2493
2608
|
let mut interleaved = vec![0.0_f32; n * 2];
|
|
2494
2609
|
for f in 0..n {
|
|
2495
2610
|
let frame = a + f as i64;
|
|
2496
|
-
let (
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
interleaved[f * 2
|
|
2611
|
+
let (left, right) = self
|
|
2612
|
+
.gain
|
|
2613
|
+
.frame(self.system.sample_at(frame), self.mic.sample_at(frame));
|
|
2614
|
+
interleaved[f * 2] = left;
|
|
2615
|
+
interleaved[f * 2 + 1] = right;
|
|
2500
2616
|
}
|
|
2501
2617
|
emitted.push(self.build_sample_buffer(&interleaved, a)?);
|
|
2502
2618
|
a = b;
|
|
@@ -182,6 +182,19 @@ const MAX_WHEEL_PAN_DELTA = 140;
|
|
|
182
182
|
const CHROME_SCALE_CSS_VAR = "--an-chrome-scale";
|
|
183
183
|
const PIXEL_GRID_ZOOM = 800;
|
|
184
184
|
|
|
185
|
+
function hasScreenChildLayers(content: string): boolean {
|
|
186
|
+
const body = content.match(/<body\b[^>]*>([\s\S]*?)<\/body\s*>/i)?.[1] ?? "";
|
|
187
|
+
const editableMarkup = body
|
|
188
|
+
.replace(/<!--[\s\S]*?-->/g, "")
|
|
189
|
+
.replace(
|
|
190
|
+
/<(script|style|template)\b[^>]*>[\s\S]*?<\/\1\s*>|<(link|meta|base)\b[^>]*>/gi, // i18n-ignore regex removes non-rendered document elements
|
|
191
|
+
"",
|
|
192
|
+
)
|
|
193
|
+
.trim();
|
|
194
|
+
|
|
195
|
+
return editableMarkup.length > 0;
|
|
196
|
+
}
|
|
197
|
+
|
|
185
198
|
import {
|
|
186
199
|
BOARD_SURFACE_BACKGROUND,
|
|
187
200
|
getBoardContentKey,
|
|
@@ -3430,6 +3443,7 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
3430
3443
|
const originCanvas = getCanvasPoint(e.clientX, e.clientY);
|
|
3431
3444
|
let latestRect = normalizeRectFromPoints(originCanvas, originCanvas);
|
|
3432
3445
|
let layerCandidates: CanvasLayerMarqueeCandidate[] = [];
|
|
3446
|
+
let lastLayerSelectionSignature: string | null = null;
|
|
3433
3447
|
const marqueeState: MarqueeDragState = {
|
|
3434
3448
|
type: "marquee",
|
|
3435
3449
|
originClient: { x: e.clientX, y: e.clientY },
|
|
@@ -3469,6 +3483,14 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
3469
3483
|
screenId: candidate.screenId,
|
|
3470
3484
|
info: candidate.info,
|
|
3471
3485
|
}));
|
|
3486
|
+
const signature = selection
|
|
3487
|
+
.map(
|
|
3488
|
+
(item) =>
|
|
3489
|
+
`${item.screenId}:${item.info.sourceId ?? item.info.pendingNodeId ?? item.info.selector ?? item.info.id ?? ""}`,
|
|
3490
|
+
)
|
|
3491
|
+
.join("|");
|
|
3492
|
+
if (signature === lastLayerSelectionSignature) return;
|
|
3493
|
+
lastLayerSelectionSignature = signature;
|
|
3472
3494
|
onLayerMarqueeSelectionChange?.(selection, {
|
|
3473
3495
|
source: "marquee",
|
|
3474
3496
|
additive: state.additive,
|
|
@@ -3513,6 +3535,7 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
3513
3535
|
additive: false,
|
|
3514
3536
|
shiftKey: false,
|
|
3515
3537
|
});
|
|
3538
|
+
lastLayerSelectionSignature = "";
|
|
3516
3539
|
}
|
|
3517
3540
|
setIsDragging(true);
|
|
3518
3541
|
// Seed collection with whatever the zero-size origin rect already
|
|
@@ -8236,6 +8259,9 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
8236
8259
|
onStartRotate={(event) =>
|
|
8237
8260
|
beginRotate(singleSelectedFrame.id, event)
|
|
8238
8261
|
}
|
|
8262
|
+
onStartDrag={(event) =>
|
|
8263
|
+
beginFrameDrag(singleSelectedFrame.id, event)
|
|
8264
|
+
}
|
|
8239
8265
|
/>
|
|
8240
8266
|
) : null}
|
|
8241
8267
|
|
|
@@ -8259,6 +8285,9 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
8259
8285
|
chromeScale={chromeScale}
|
|
8260
8286
|
chromeSettling={chromeSettling}
|
|
8261
8287
|
handlesEnabled={!readOnly}
|
|
8288
|
+
onStartDrag={(event) =>
|
|
8289
|
+
beginFrameDrag(selectedFrameEntries[0]!.id, event)
|
|
8290
|
+
}
|
|
8262
8291
|
onStartResize={beginGroupResize}
|
|
8263
8292
|
onStartRotate={beginGroupRotate}
|
|
8264
8293
|
/>
|
|
@@ -8270,6 +8299,9 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
8270
8299
|
chromeScale={chromeScale}
|
|
8271
8300
|
chromeSettling={chromeSettling}
|
|
8272
8301
|
handlesEnabled={!readOnly}
|
|
8302
|
+
onStartDrag={(event) =>
|
|
8303
|
+
beginDraftDrag(selectedDraftEntries[0]!.id, event)
|
|
8304
|
+
}
|
|
8273
8305
|
onStartResize={beginDraftGroupResize}
|
|
8274
8306
|
/>
|
|
8275
8307
|
) : null}
|
|
@@ -9563,6 +9595,7 @@ const Screen = memo(function Screen({
|
|
|
9563
9595
|
!suppressFrameChromeForChild;
|
|
9564
9596
|
const screenContentInteractive =
|
|
9565
9597
|
Boolean(screenContent) &&
|
|
9598
|
+
(isSelected || hasScreenChildLayers(screen.content)) &&
|
|
9566
9599
|
!locked &&
|
|
9567
9600
|
!penActive &&
|
|
9568
9601
|
!creationToolActive &&
|
|
@@ -9771,16 +9804,14 @@ const Screen = memo(function Screen({
|
|
|
9771
9804
|
role="button"
|
|
9772
9805
|
tabIndex={0}
|
|
9773
9806
|
onClick={(e) => {
|
|
9774
|
-
|
|
9775
|
-
e.stopPropagation();
|
|
9776
|
-
return;
|
|
9777
|
-
}
|
|
9807
|
+
const interactiveContent = isInteractiveScreenContentTarget(e.target);
|
|
9778
9808
|
e.stopPropagation();
|
|
9779
9809
|
if (suppressNextClick.current) {
|
|
9780
9810
|
suppressNextClick.current = false;
|
|
9781
9811
|
return;
|
|
9782
9812
|
}
|
|
9783
9813
|
if (e.detail > 1) return;
|
|
9814
|
+
if (interactiveContent && isSelected) return;
|
|
9784
9815
|
onPick(screen.id, e);
|
|
9785
9816
|
}}
|
|
9786
9817
|
onDoubleClick={(e) => {
|
|
@@ -10672,6 +10703,7 @@ function GroupSelectionBox({
|
|
|
10672
10703
|
bounds,
|
|
10673
10704
|
chromeScale,
|
|
10674
10705
|
chromeSettling,
|
|
10706
|
+
onStartDrag,
|
|
10675
10707
|
onStartResize,
|
|
10676
10708
|
onStartRotate,
|
|
10677
10709
|
handlesEnabled = true,
|
|
@@ -10679,6 +10711,7 @@ function GroupSelectionBox({
|
|
|
10679
10711
|
bounds: NonNullable<ReturnType<typeof getFrameGroupBounds>>;
|
|
10680
10712
|
chromeScale: number;
|
|
10681
10713
|
chromeSettling: boolean;
|
|
10714
|
+
onStartDrag?: (e: React.MouseEvent) => void;
|
|
10682
10715
|
onStartResize: (handle: ResizeHandle, e: React.MouseEvent) => void;
|
|
10683
10716
|
/** Multi-selection rotate (CV14). Omit to keep the previous behavior (no
|
|
10684
10717
|
* rotate handle shown) — used by callers whose selection kind doesn't
|
|
@@ -10699,6 +10732,7 @@ function GroupSelectionBox({
|
|
|
10699
10732
|
showRotate={!!onStartRotate}
|
|
10700
10733
|
handlesEnabled={handlesEnabled}
|
|
10701
10734
|
filled
|
|
10735
|
+
onStartDrag={onStartDrag}
|
|
10702
10736
|
onStartResize={onStartResize}
|
|
10703
10737
|
onStartRotate={onStartRotate ?? (() => {})}
|
|
10704
10738
|
/>
|
|
@@ -10765,6 +10799,7 @@ function SelectionBox({
|
|
|
10765
10799
|
handlesEnabled = true,
|
|
10766
10800
|
onStartResize,
|
|
10767
10801
|
onStartRotate,
|
|
10802
|
+
onStartDrag,
|
|
10768
10803
|
}: {
|
|
10769
10804
|
geometry: FrameGeometry;
|
|
10770
10805
|
chromeScale: number;
|
|
@@ -10774,6 +10809,7 @@ function SelectionBox({
|
|
|
10774
10809
|
handlesEnabled?: boolean;
|
|
10775
10810
|
onStartResize: (handle: ResizeHandle, e: React.MouseEvent) => void;
|
|
10776
10811
|
onStartRotate: (e: React.MouseEvent) => void;
|
|
10812
|
+
onStartDrag?: (e: React.MouseEvent) => void;
|
|
10777
10813
|
}) {
|
|
10778
10814
|
return (
|
|
10779
10815
|
<div
|
|
@@ -10798,6 +10834,13 @@ function SelectionBox({
|
|
|
10798
10834
|
zIndex: 1_000_000,
|
|
10799
10835
|
}}
|
|
10800
10836
|
>
|
|
10837
|
+
{onStartDrag ? (
|
|
10838
|
+
<span
|
|
10839
|
+
data-frame-drag-surface
|
|
10840
|
+
className="pointer-events-auto absolute inset-0 z-[5] cursor-move"
|
|
10841
|
+
onMouseDown={onStartDrag}
|
|
10842
|
+
/>
|
|
10843
|
+
) : null}
|
|
10801
10844
|
<ResizeHandles
|
|
10802
10845
|
active
|
|
10803
10846
|
enabled={handlesEnabled}
|
|
@@ -56,7 +56,7 @@ export default createAgentChatPlugin({
|
|
|
56
56
|
actions: loadActionsFromStaticRegistry(actionsRegistry),
|
|
57
57
|
nativeActionsInDev: true,
|
|
58
58
|
skipFilesContext: true,
|
|
59
|
-
|
|
59
|
+
frameworkTools: { database: "off" },
|
|
60
60
|
resolveOrgId: async (event) => (await getOrgContext(event)).orgId,
|
|
61
61
|
mentionProviders: async () => {
|
|
62
62
|
const { getDb } = await import("../db/index.js");
|
|
@@ -43,7 +43,7 @@ export default createAgentChatPlugin({
|
|
|
43
43
|
// Customer and product activity data belongs to Analytics. Keep raw DB
|
|
44
44
|
// tools out of both the interactive and A2A Slides agent surfaces so the
|
|
45
45
|
// agent cannot bypass the Analytics data dictionary with local SQL.
|
|
46
|
-
|
|
46
|
+
frameworkTools: { database: "off" },
|
|
47
47
|
// Enable sandboxed JavaScript execution so Slides agents can fetch,
|
|
48
48
|
// paginate, and reduce provider data through providerFetch() without us
|
|
49
49
|
// hardcoding one action per Google Drive endpoint.
|
|
@@ -196,6 +196,12 @@ export interface ActionEntry {
|
|
|
196
196
|
* `needsApproval` option. See `packages/core/docs/content/actions.mdx`.
|
|
197
197
|
*/
|
|
198
198
|
needsApproval?: boolean | ((args: any, ctx?: import("../action.js").ActionRunContext) => boolean | Promise<boolean>);
|
|
199
|
+
/** Which framework tool group contributed this action. Set by the framework,
|
|
200
|
+
* never by an app: apps own their action names, and a tagged action is one
|
|
201
|
+
* the app can switch off wholesale through `frameworkTools`. Tagged actions
|
|
202
|
+
* are also excluded from the DEFAULT first-request tool list — they stay
|
|
203
|
+
* reachable through `tool-search` unless named in `initialToolNames`. */
|
|
204
|
+
frameworkGroup?: import("../framework-tools.js").FrameworkToolGroup;
|
|
199
205
|
}
|
|
200
206
|
/** @deprecated Use `ActionEntry` instead */
|
|
201
207
|
export type ScriptEntry = ActionEntry;
|
|
@@ -181,13 +181,56 @@ export declare const SQL_SUBSCRIPTION_IDLE_POLL_MS = 500;
|
|
|
181
181
|
export declare const SQL_SUBSCRIPTION_ACTIVE_GRACE_MS = 2000;
|
|
182
182
|
/** Keep terminal/status probes at the historical cadence to bound DB work. */
|
|
183
183
|
export declare const SQL_SUBSCRIPTION_STATUS_POLL_MS = 500;
|
|
184
|
+
/**
|
|
185
|
+
* Consecutive empty polls before the IDLE cadence starts decaying toward
|
|
186
|
+
* `SQL_SUBSCRIPTION_IDLE_MAX_POLL_MS`.
|
|
187
|
+
*
|
|
188
|
+
* The active grace already covers a streaming producer, so decay only ever
|
|
189
|
+
* applies to a subscriber watching a run that is producing nothing: a long tool
|
|
190
|
+
* call, a slow first token, or a wedged producer. Those cost one poll per
|
|
191
|
+
* 500ms each, forever, per subscriber — the single largest source of idle
|
|
192
|
+
* `agent_run_events` reads.
|
|
193
|
+
*/
|
|
194
|
+
export declare const SQL_SUBSCRIPTION_IDLE_DECAY_AFTER_POLLS = 4;
|
|
195
|
+
/**
|
|
196
|
+
* Ceiling for the decayed idle cadence. Bounds the WORST-CASE added latency to
|
|
197
|
+
* the next token a quiet run eventually produces; a row already waiting when the
|
|
198
|
+
* timer fires is delivered immediately, so this is not added to a streaming run.
|
|
199
|
+
*/
|
|
200
|
+
export declare const SQL_SUBSCRIPTION_IDLE_MAX_POLL_MS = 2000;
|
|
201
|
+
/**
|
|
202
|
+
* Cadence for the opportunistic `reapIfStale` probe inside the SSE poll loop,
|
|
203
|
+
* kept SEPARATE from (and much slower than) the status probe next to it.
|
|
204
|
+
*
|
|
205
|
+
* A reap can only ever act on a run whose liveness basis is older than the
|
|
206
|
+
* tightest staleness window any sweep enforces — `RUN_STALE_MS`, 15s — so
|
|
207
|
+
* running it on the 500ms status cadence issued ~30 rounds of
|
|
208
|
+
* `reconcileTerminalRunFromEvents` + reap-eligibility queries (2-4 round trips
|
|
209
|
+
* each) before the first one could possibly match a row. This cadence still
|
|
210
|
+
* detects a stale producer well inside the client's own idle timeout while
|
|
211
|
+
* cutting that probe traffic ~10x. `getRunById` deliberately stays on the fast
|
|
212
|
+
* status cadence: it is one indexed read and it is what closes the stream on a
|
|
213
|
+
* NORMAL finish, which must stay prompt.
|
|
214
|
+
*/
|
|
215
|
+
export declare const SQL_SUBSCRIPTION_REAP_POLL_MS = 5000;
|
|
184
216
|
/** Initial retry delay after a transient cross-isolate SQL polling failure. */
|
|
185
217
|
export declare const SQL_SUBSCRIPTION_RETRY_BASE_MS = 250;
|
|
186
218
|
/** Bound consecutive SQL polling failures so a dead subscription fails loud. */
|
|
187
219
|
export declare const SQL_SUBSCRIPTION_MAX_CONSECUTIVE_FAILURES = 4;
|
|
188
220
|
/** Cap retry delay even if the failure bound changes independently. */
|
|
189
221
|
export declare const SQL_SUBSCRIPTION_RETRY_MAX_MS = 2000;
|
|
190
|
-
export declare function resolveSqlSubscriptionPollMs(now: number, activePollUntil: number): number;
|
|
222
|
+
export declare function resolveSqlSubscriptionPollMs(now: number, activePollUntil: number, consecutiveEmptyPolls?: number): number;
|
|
223
|
+
/**
|
|
224
|
+
* Advance the empty-poll counter that drives `resolveSqlSubscriptionPollMs`'s
|
|
225
|
+
* decay.
|
|
226
|
+
*
|
|
227
|
+
* Only IDLE polls count. Counting the ~16 fast polls inside the active grace
|
|
228
|
+
* window would push the ladder to its cap the instant the grace expired, so a
|
|
229
|
+
* producer that pauses a few seconds between tokens would resume at the 2s cap
|
|
230
|
+
* instead of 500ms — a visible mid-stream stutter rather than the intended
|
|
231
|
+
* "this run has gone quiet" backoff.
|
|
232
|
+
*/
|
|
233
|
+
export declare function nextSqlSubscriptionEmptyPolls(current: number, hadEvents: boolean, now: number, activePollUntil: number): number;
|
|
191
234
|
export declare function resolveSqlSubscriptionRetryMs(consecutiveFailures: number): number;
|
|
192
235
|
export interface StartRunOptions {
|
|
193
236
|
/** Keep a request-scoped serverless invocation alive for this run. */
|