@agent-native/core 0.144.3 → 0.145.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/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/collab/routes.d.ts +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/framework-tools.d.ts +126 -0
- package/dist/framework-tools.js +114 -0
- package/dist/mcp/screen-memory-stdio.d.ts +7 -7
- package/dist/notifications/routes.d.ts +4 -4
- package/dist/observability/routes.d.ts +3 -3
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +2 -2
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/secrets/routes.d.ts +3 -3
- 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 +122 -71
- 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/templates/workspace-core/.agents/skills/extensions/SKILL.md +6 -3
- package/docs/content/deployment.mdx +47 -13
- package/docs/content/key-concepts.mdx +13 -6
- package/package.json +3 -3
- 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;
|
package/dist/collab/routes.d.ts
CHANGED
|
@@ -26,8 +26,8 @@ export declare const getCollabState: import("h3").EventHandlerWithFetch<import("
|
|
|
26
26
|
* Body: { update: string (base64), requestSource?: string }
|
|
27
27
|
*/
|
|
28
28
|
export declare const postCollabUpdate: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
|
|
29
|
-
error: string;
|
|
30
29
|
ok?: undefined;
|
|
30
|
+
error: string;
|
|
31
31
|
} | {
|
|
32
32
|
error?: undefined;
|
|
33
33
|
ok: boolean;
|
|
@@ -17,11 +17,11 @@ declare const _default: import("../../action.js").ActionDefinition<{
|
|
|
17
17
|
id?: undefined;
|
|
18
18
|
provider?: undefined;
|
|
19
19
|
} | {
|
|
20
|
+
error?: undefined;
|
|
20
21
|
configured?: undefined;
|
|
21
22
|
connectPath?: undefined;
|
|
22
23
|
url: string;
|
|
23
24
|
id: string;
|
|
24
25
|
provider: string;
|
|
25
|
-
error?: undefined;
|
|
26
26
|
}>;
|
|
27
27
|
export default _default;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { type DatabaseToolsOption } from "./scripts/db/tool-mode.js";
|
|
2
|
+
/**
|
|
3
|
+
* Optional framework tool groups an app can turn off through `frameworkTools`.
|
|
4
|
+
*
|
|
5
|
+
* Each name is also stamped onto the group's actions as
|
|
6
|
+
* `ActionEntry.frameworkGroup`. That tag is what lets a single filter subtract a
|
|
7
|
+
* group from every agent tool surface at once: `agent-chat-plugin.ts` spreads
|
|
8
|
+
* these registries at thirteen separate composition sites (interactive chat,
|
|
9
|
+
* dev, MCP-full, A2A, background jobs, triggers), and a per-site condition would
|
|
10
|
+
* be the same omission waiting to happen thirteen times.
|
|
11
|
+
*
|
|
12
|
+
* The tag has a second job: framework-tagged actions are excluded from the
|
|
13
|
+
* DEFAULT first-request tool list, so an app pays for the ~40 kit schemas only
|
|
14
|
+
* when it names them in `initialToolNames`. They stay in the searchable registry
|
|
15
|
+
* either way.
|
|
16
|
+
*/
|
|
17
|
+
export declare const FRAMEWORK_TOOL_GROUPS: readonly ["sharing", "review", "history", "featureFlags", "localization", "audit", "contextXray", "userProfile", "automation", "docs", "resources", "web", "workspaceApps", "chat", "email"];
|
|
18
|
+
export type FrameworkToolGroup = (typeof FRAMEWORK_TOOL_GROUPS)[number];
|
|
19
|
+
/**
|
|
20
|
+
* Per-group switches for the framework's own agent tools. Every group defaults
|
|
21
|
+
* to today's behavior, so omitting this option leaves the tool surface
|
|
22
|
+
* byte-identical.
|
|
23
|
+
*
|
|
24
|
+
* Turning a group off removes it from the agent surfaces only — interactive
|
|
25
|
+
* chat, MCP, A2A, and background runs. The matching HTTP action routes stay
|
|
26
|
+
* mounted (see `httpActions` in `agent-chat-plugin.ts`) because the UI reaches
|
|
27
|
+
* them through client hooks; a flag meant to trim the model's tool list must not
|
|
28
|
+
* 404 the share dialog.
|
|
29
|
+
*
|
|
30
|
+
* Note the parity cost before disabling one: the framework's contract is that
|
|
31
|
+
* anything the UI can do, the agent can do. `sharing: false` on an app that
|
|
32
|
+
* still renders `ShareButton` breaks that on purpose, and only makes sense when
|
|
33
|
+
* the app has no such surface at all.
|
|
34
|
+
*/
|
|
35
|
+
export interface FrameworkToolsOption {
|
|
36
|
+
/** Raw SQL tools. `"read"` (default) exposes `db-schema`/`db-query`;
|
|
37
|
+
* `"write"` adds `db-exec`/`db-patch`; `"off"` exposes neither. */
|
|
38
|
+
database?: DatabaseToolsOption;
|
|
39
|
+
/** Extension management (`create-extension`, `update-extension`, …).
|
|
40
|
+
* Defaults to false — only apps that intentionally let the agent build
|
|
41
|
+
* sandboxed mini-apps should enable it. */
|
|
42
|
+
extensions?: boolean;
|
|
43
|
+
/** `share-resource`, `unshare-resource`, `list-resource-shares`,
|
|
44
|
+
* `set-resource-visibility`, `create-agent-resource-link`. */
|
|
45
|
+
sharing?: boolean;
|
|
46
|
+
/** The inline review/comment kit (`list-review-comments`,
|
|
47
|
+
* `create-review-comment`, `resolve-review-thread`, …). */
|
|
48
|
+
review?: boolean;
|
|
49
|
+
/** Version snapshots and restore (`create-resource-version`,
|
|
50
|
+
* `list-resource-versions`, `restore-resource-version`, …). */
|
|
51
|
+
history?: boolean;
|
|
52
|
+
/** `get-feature-flags`, `list-feature-flags`, `set-feature-flag`. */
|
|
53
|
+
featureFlags?: boolean;
|
|
54
|
+
/** `get-localization-preference`, `set-localization-preference`. */
|
|
55
|
+
localization?: boolean;
|
|
56
|
+
/** `list-audit-events`, `get-audit-event`, `export-audit-events`. */
|
|
57
|
+
audit?: boolean;
|
|
58
|
+
/** Context X-Ray (`context-manifest-get`, `context-pin`, `context-evict`, …). */
|
|
59
|
+
contextXray?: boolean;
|
|
60
|
+
/** `get-user-profile`, `update-user-profile`, `change-appearance`. */
|
|
61
|
+
userProfile?: boolean;
|
|
62
|
+
/** Recurring jobs, automations, notifications, and progress runs. */
|
|
63
|
+
automation?: boolean;
|
|
64
|
+
/** `docs-search` over the bundled framework documentation. */
|
|
65
|
+
docs?: boolean;
|
|
66
|
+
/** The `resources` tool — workspace notes, memory, and context files. */
|
|
67
|
+
resources?: boolean;
|
|
68
|
+
/** `web-request` and `web-search`. */
|
|
69
|
+
web?: boolean;
|
|
70
|
+
/** `describe-workspace-apps` and `call-agent` for cross-app delegation. */
|
|
71
|
+
workspaceApps?: boolean;
|
|
72
|
+
/** `chat-history`, `manage-agent-engine`, `manage-agent-loop-settings`. */
|
|
73
|
+
chat?: boolean;
|
|
74
|
+
/** `core-send-email`. */
|
|
75
|
+
email?: boolean;
|
|
76
|
+
/** `"minimal"` turns every group above off, for voice-first and
|
|
77
|
+
* single-purpose apps that want the template's own actions and nothing else.
|
|
78
|
+
* Any explicit group key wins over the preset, so
|
|
79
|
+
* `{ preset: "minimal", resources: true }` keeps exactly one group. */
|
|
80
|
+
preset?: "minimal";
|
|
81
|
+
}
|
|
82
|
+
export type FrameworkToolsConfig = FrameworkToolsOption | "minimal";
|
|
83
|
+
export interface FrameworkToolsInput {
|
|
84
|
+
frameworkTools?: FrameworkToolsConfig;
|
|
85
|
+
/** @deprecated Use `frameworkTools.database`. */
|
|
86
|
+
databaseTools?: DatabaseToolsOption;
|
|
87
|
+
/** @deprecated Use `frameworkTools.extensions`. */
|
|
88
|
+
extensionTools?: boolean;
|
|
89
|
+
}
|
|
90
|
+
export interface ResolvedFrameworkTools {
|
|
91
|
+
/** Pass-through for `normalizeDatabaseToolsMode`; `undefined` means default. */
|
|
92
|
+
database: DatabaseToolsOption | undefined;
|
|
93
|
+
extensions: boolean;
|
|
94
|
+
disabledGroups: ReadonlySet<FrameworkToolGroup>;
|
|
95
|
+
isEnabled(group: FrameworkToolGroup): boolean;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Resolve the public option surface into one shape the plugin threads inward.
|
|
99
|
+
*
|
|
100
|
+
* Legacy `databaseTools` / `extensionTools` remain accepted for one minor. When
|
|
101
|
+
* both forms are present and disagree we throw at plugin init rather than
|
|
102
|
+
* picking a winner: an app that boots with a tool surface nobody chose is how a
|
|
103
|
+
* "why can't the agent see db-query" report ends up unexplainable.
|
|
104
|
+
*/
|
|
105
|
+
export declare function resolveFrameworkTools(input: FrameworkToolsInput | undefined): ResolvedFrameworkTools;
|
|
106
|
+
/** Structural view of the one field these helpers read, so tagging utilities
|
|
107
|
+
* stay free of an import cycle with `ActionEntry`. */
|
|
108
|
+
interface FrameworkGrouped {
|
|
109
|
+
frameworkGroup?: FrameworkToolGroup;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Drop every action belonging to a disabled group. Returns the input untouched
|
|
113
|
+
* when nothing is disabled so the default path allocates nothing.
|
|
114
|
+
*/
|
|
115
|
+
export declare function filterFrameworkToolGroups<T extends FrameworkGrouped>(actions: Record<string, T>, disabledGroups: ReadonlySet<FrameworkToolGroup>): Record<string, T>;
|
|
116
|
+
/** True for actions the framework contributes rather than the app's own. */
|
|
117
|
+
export declare function isFrameworkGroupedAction(entry: FrameworkGrouped): boolean;
|
|
118
|
+
/**
|
|
119
|
+
* Whether a group is live, for prompt builders that only receive the disabled
|
|
120
|
+
* set. Prompt text and tool schemas must agree: a prompt that names a tool the
|
|
121
|
+
* request does not carry makes the model try it, fail, and often report the
|
|
122
|
+
* capability as nonexistent — so every block naming a group's tool by name has
|
|
123
|
+
* to be gated on the same set that gates the registry.
|
|
124
|
+
*/
|
|
125
|
+
export declare function frameworkGroupEnabled(disabledGroups: ReadonlySet<FrameworkToolGroup> | undefined, group: FrameworkToolGroup): boolean;
|
|
126
|
+
export {};
|