@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.
Files changed (40) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/clips/changelog/2026-08-06-desktop-full-screen-recordings-now-normalize-microphone-leve.md +6 -0
  3. package/corpus/templates/clips/desktop/src-tauri/src/native_screen/custom_capture.rs +185 -69
  4. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +47 -4
  5. package/corpus/templates/forms/server/plugins/agent-chat.ts +1 -1
  6. package/corpus/templates/slides/server/plugins/agent-chat.ts +1 -1
  7. package/dist/agent/production-agent.d.ts +6 -0
  8. package/dist/collab/routes.d.ts +1 -1
  9. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  10. package/dist/framework-tools.d.ts +126 -0
  11. package/dist/framework-tools.js +114 -0
  12. package/dist/mcp/screen-memory-stdio.d.ts +7 -7
  13. package/dist/notifications/routes.d.ts +4 -4
  14. package/dist/observability/routes.d.ts +3 -3
  15. package/dist/progress/routes.d.ts +1 -1
  16. package/dist/provider-api/actions/custom-provider-registration.d.ts +2 -2
  17. package/dist/resources/handlers.d.ts +1 -1
  18. package/dist/secrets/routes.d.ts +3 -3
  19. package/dist/server/action-discovery.d.ts +27 -0
  20. package/dist/server/action-discovery.js +89 -0
  21. package/dist/server/agent-chat/action-filters-a2a.d.ts +11 -0
  22. package/dist/server/agent-chat/action-filters-a2a.js +17 -1
  23. package/dist/server/agent-chat/framework-prompts.d.ts +2 -0
  24. package/dist/server/agent-chat/plugin-options.d.ts +30 -0
  25. package/dist/server/agent-chat/prompt-resources.d.ts +4 -1
  26. package/dist/server/agent-chat/prompt-resources.js +8 -2
  27. package/dist/server/agent-chat-plugin.js +122 -71
  28. package/dist/server/prompts/framework-core-compact.d.ts +5 -0
  29. package/dist/server/prompts/framework-core-compact.js +28 -12
  30. package/dist/server/prompts/framework-core.d.ts +5 -0
  31. package/dist/server/prompts/framework-core.js +33 -16
  32. package/dist/server/prompts/index.d.ts +1 -1
  33. package/dist/server/prompts/index.js +1 -1
  34. package/dist/server/prompts/shared-rules.d.ts +12 -4
  35. package/dist/server/prompts/shared-rules.js +14 -4
  36. package/dist/templates/workspace-core/.agents/skills/extensions/SKILL.md +6 -3
  37. package/docs/content/deployment.mdx +47 -13
  38. package/docs/content/key-concepts.mdx +13 -6
  39. package/package.json +3 -3
  40. package/src/templates/workspace-core/.agents/skills/extensions/SKILL.md +6 -3
package/corpus/README.md CHANGED
@@ -31,4 +31,4 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
31
31
 
32
32
  ## Generated Counts
33
33
 
34
- - template files: 7847
34
+ - template files: 7848
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-08-06
4
+ ---
5
+
6
+ Desktop full-screen recordings now normalize microphone level while recording, so clips are no longer quieter than the rest of your library.
@@ -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: average system + mic per frame, clamp, wrap
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
- /// Each source is weighted at 0.5 before summing so that two full-scale
2472
- /// signals (which occurs when a USB audio interface with software monitoring
2473
- /// routes the mic back through system audio) can never exceed ±1.0 and
2474
- /// hard-clip. The standard SCK pipeline applies the same 0.5×L + 0.5×R
2475
- /// pan-downmix for the same reason; loudnorm restores the target loudness
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 (sl, sr) = self.system.sample_at(frame);
2497
- let (ml, mr) = self.mic.sample_at(frame);
2498
- interleaved[f * 2] = (sl * 0.5 + ml * 0.5).clamp(-1.0, 1.0);
2499
- interleaved[f * 2 + 1] = (sr * 0.5 + mr * 0.5).clamp(-1.0, 1.0);
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
- if (isInteractiveScreenContentTarget(e.target)) {
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
- databaseTools: false,
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
- databaseTools: "off",
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;
@@ -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 {};