@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.
Files changed (71) 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/agent/run-manager.d.ts +44 -1
  9. package/dist/agent/run-manager.js +70 -8
  10. package/dist/chat-threads/store.js +6 -4
  11. package/dist/client/use-db-sync.js +9 -1
  12. package/dist/collab/awareness.d.ts +2 -2
  13. package/dist/collab/struct-routes.d.ts +1 -1
  14. package/dist/db/ddl-guard.d.ts +8 -0
  15. package/dist/db/ddl-guard.js +148 -0
  16. package/dist/deploy/build.d.ts +36 -0
  17. package/dist/deploy/build.js +78 -5
  18. package/dist/framework-tools.d.ts +126 -0
  19. package/dist/framework-tools.js +114 -0
  20. package/dist/notifications/routes.d.ts +3 -3
  21. package/dist/org/accept-pending.js +2 -2
  22. package/dist/org/auth-policy.js +4 -0
  23. package/dist/org/auto-join-domain.d.ts +9 -0
  24. package/dist/org/auto-join-domain.js +53 -4
  25. package/dist/org/context.js +12 -3
  26. package/dist/org/handlers.js +17 -5
  27. package/dist/org/request-org-cache.d.ts +24 -2
  28. package/dist/org/request-org-cache.js +57 -2
  29. package/dist/provider-api/actions/custom-provider-registration.d.ts +6 -6
  30. package/dist/provider-api/actions/provider-api.d.ts +6 -6
  31. package/dist/resources/store.d.ts +1 -1
  32. package/dist/resources/store.js +96 -7
  33. package/dist/secrets/routes.d.ts +9 -9
  34. package/dist/server/action-discovery.d.ts +27 -0
  35. package/dist/server/action-discovery.js +89 -0
  36. package/dist/server/agent-chat/action-filters-a2a.d.ts +11 -0
  37. package/dist/server/agent-chat/action-filters-a2a.js +17 -1
  38. package/dist/server/agent-chat/framework-prompts.d.ts +2 -0
  39. package/dist/server/agent-chat/plugin-options.d.ts +30 -0
  40. package/dist/server/agent-chat/prompt-resources.d.ts +4 -1
  41. package/dist/server/agent-chat/prompt-resources.js +8 -2
  42. package/dist/server/agent-chat-plugin.js +138 -75
  43. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  44. package/dist/server/auth.d.ts +3 -0
  45. package/dist/server/auth.js +22 -1
  46. package/dist/server/collab-plugin.js +10 -4
  47. package/dist/server/core-routes-plugin.js +3 -0
  48. package/dist/server/prompts/framework-core-compact.d.ts +5 -0
  49. package/dist/server/prompts/framework-core-compact.js +28 -12
  50. package/dist/server/prompts/framework-core.d.ts +5 -0
  51. package/dist/server/prompts/framework-core.js +33 -16
  52. package/dist/server/prompts/index.d.ts +1 -1
  53. package/dist/server/prompts/index.js +1 -1
  54. package/dist/server/prompts/shared-rules.d.ts +12 -4
  55. package/dist/server/prompts/shared-rules.js +14 -4
  56. package/dist/server/session-email-cache.d.ts +14 -0
  57. package/dist/server/session-email-cache.js +48 -0
  58. package/dist/server/sweep-runtime.d.ts +22 -0
  59. package/dist/server/sweep-runtime.js +28 -0
  60. package/dist/settings/store.js +63 -7
  61. package/dist/shared/ttl-cache.d.ts +59 -0
  62. package/dist/shared/ttl-cache.js +43 -0
  63. package/dist/sharing/actions/list-resource-shares.d.ts +3 -3
  64. package/dist/sharing/actions/list-resource-shares.js +4 -1
  65. package/dist/templates/workspace-core/.agents/skills/extensions/SKILL.md +6 -3
  66. package/dist/triggers/condition-evaluator.js +11 -27
  67. package/docs/content/deployment.mdx +95 -13
  68. package/docs/content/environment-variables.mdx +5 -0
  69. package/docs/content/key-concepts.mdx +13 -6
  70. package/package.json +1 -1
  71. 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;
@@ -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. */