@agent-native/core 0.70.3 → 0.71.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 (79) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +13 -0
  3. package/corpus/core/docs/content/multi-app-workspace.md +2 -2
  4. package/corpus/core/docs/design/durable-agent-runs.md +458 -4
  5. package/corpus/core/package.json +1 -1
  6. package/corpus/core/src/agent/durable-background.ts +192 -0
  7. package/corpus/core/src/agent/production-agent.ts +330 -13
  8. package/corpus/core/src/agent/run-manager.ts +66 -3
  9. package/corpus/core/src/agent/run-store.ts +129 -23
  10. package/corpus/core/src/agent/types.ts +20 -0
  11. package/corpus/core/src/client/AgentPanel.tsx +12 -9
  12. package/corpus/core/src/client/blocks/library/FileTreeBlock.tsx +72 -14
  13. package/corpus/core/src/deploy/build.ts +96 -0
  14. package/corpus/core/src/deploy/workspace-deploy.ts +121 -0
  15. package/corpus/core/src/mcp/build-server.ts +22 -13
  16. package/corpus/core/src/server/agent-chat-plugin.ts +133 -66
  17. package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +19 -0
  18. package/corpus/templates/analytics/AGENTS.md +8 -0
  19. package/corpus/templates/analytics/actions/compose-dashboard.ts +317 -0
  20. package/corpus/templates/analytics/changelog/2026-06-23-build-large-first-party-analytics-dashboards-in-one-fast-cal.md +6 -0
  21. package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +574 -0
  22. package/corpus/templates/clips/changelog/2026-06-23-dragging-the-desktop-camera-bubble-now-glides-to-a-stop-at-t.md +6 -0
  23. package/corpus/templates/clips/chrome-extension/src/background.ts +130 -32
  24. package/corpus/templates/clips/chrome-extension/src/content-script.ts +163 -13
  25. package/corpus/templates/clips/chrome-extension/src/offscreen.ts +195 -34
  26. package/corpus/templates/clips/chrome-extension/src/overlay.css +73 -23
  27. package/corpus/templates/clips/chrome-extension/src/overlay.ts +82 -13
  28. package/corpus/templates/clips/chrome-extension/src/popup.html +62 -5
  29. package/corpus/templates/clips/chrome-extension/src/popup.ts +290 -1
  30. package/corpus/templates/clips/chrome-extension/src/styles.css +34 -0
  31. package/corpus/templates/clips/desktop/src/lib/audio-cue.ts +21 -16
  32. package/corpus/templates/clips/desktop/src/lib/recorder.ts +10 -17
  33. package/corpus/templates/clips/desktop/src/overlays/bubble.tsx +73 -23
  34. package/corpus/templates/clips/desktop/src/overlays/countdown.tsx +0 -6
  35. package/corpus/templates/clips/desktop/src/overlays/toolbar.tsx +56 -46
  36. package/corpus/templates/clips/desktop/src/styles.css +33 -5
  37. package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +113 -0
  38. package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +3 -0
  39. package/dist/agent/durable-background.d.ts +60 -0
  40. package/dist/agent/durable-background.d.ts.map +1 -0
  41. package/dist/agent/durable-background.js +144 -0
  42. package/dist/agent/durable-background.js.map +1 -0
  43. package/dist/agent/production-agent.d.ts +20 -0
  44. package/dist/agent/production-agent.d.ts.map +1 -1
  45. package/dist/agent/production-agent.js +292 -14
  46. package/dist/agent/production-agent.js.map +1 -1
  47. package/dist/agent/run-manager.d.ts +48 -0
  48. package/dist/agent/run-manager.d.ts.map +1 -1
  49. package/dist/agent/run-manager.js +45 -3
  50. package/dist/agent/run-manager.js.map +1 -1
  51. package/dist/agent/run-store.d.ts +30 -1
  52. package/dist/agent/run-store.d.ts.map +1 -1
  53. package/dist/agent/run-store.js +124 -24
  54. package/dist/agent/run-store.js.map +1 -1
  55. package/dist/agent/types.d.ts +20 -0
  56. package/dist/agent/types.d.ts.map +1 -1
  57. package/dist/agent/types.js.map +1 -1
  58. package/dist/client/AgentPanel.d.ts.map +1 -1
  59. package/dist/client/AgentPanel.js +2 -2
  60. package/dist/client/AgentPanel.js.map +1 -1
  61. package/dist/client/blocks/library/FileTreeBlock.d.ts.map +1 -1
  62. package/dist/client/blocks/library/FileTreeBlock.js +37 -4
  63. package/dist/client/blocks/library/FileTreeBlock.js.map +1 -1
  64. package/dist/deploy/build.d.ts +29 -0
  65. package/dist/deploy/build.d.ts.map +1 -1
  66. package/dist/deploy/build.js +85 -0
  67. package/dist/deploy/build.js.map +1 -1
  68. package/dist/deploy/workspace-deploy.d.ts.map +1 -1
  69. package/dist/deploy/workspace-deploy.js +108 -0
  70. package/dist/deploy/workspace-deploy.js.map +1 -1
  71. package/dist/mcp/build-server.d.ts.map +1 -1
  72. package/dist/mcp/build-server.js +23 -9
  73. package/dist/mcp/build-server.js.map +1 -1
  74. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  75. package/dist/server/agent-chat-plugin.js +68 -13
  76. package/dist/server/agent-chat-plugin.js.map +1 -1
  77. package/docs/content/multi-app-workspace.md +2 -2
  78. package/docs/design/durable-agent-runs.md +458 -4
  79. package/package.json +1 -1
@@ -606,40 +606,90 @@ export function Bubble() {
606
606
  }, []);
607
607
 
608
608
  // ---- explicit drag handler --------------------------------------------
609
- // We bypass `data-tauri-drag-region` entirely it was unreliable across
610
- // three iterations (see PR history). Tauri's attribute hook watches for
611
- // elements with the attribute at page-load time, and also has gotchas
612
- // with pointer-events, unfocused-window first-click swallowing, and
613
- // WKWebView's latched event target. Calling `startDragging()` explicitly
614
- // on mousedown is the canonical robust path (per Tauri's own docs for
615
- // "customize drag behavior") and skips all of those footguns.
609
+ // Loom-style manual drag. We deliberately do NOT use Tauri's native
610
+ // `startDragging()` (or `data-tauri-drag-region`): with a native drag the OS
611
+ // window server owns the position, so the only way to keep the bubble on
612
+ // screen is to clamp AFTER each move and `set_position` it back — and the OS
613
+ // immediately shoves it back out toward the cursor on the next frame. That
614
+ // fight is exactly the jitter/snap-back we're killing here.
616
615
  //
617
- // Interactive children (close X, size dots) are marked `data-no-drag`
618
- // so their clicks land on their onClick handlers instead of starting
619
- // a window drag.
620
- const handleBubbleMouseDown = (e: React.MouseEvent) => {
621
- // Only left mouse button initiates drag.
616
+ // Instead we drive the move ourselves. On pointer-down Rust snapshots the
617
+ // cursor + window anchor; each frame `bubble_drag_move` reads the live cursor,
618
+ // offsets the window by the cursor delta, and clamps BEFORE moving. The
619
+ // window therefore stops dead at the edge like a puck against a wall — the
620
+ // cursor keeps going, the bubble stays pinned, nothing to snap back from.
621
+ //
622
+ // Interactive children (close X, size dots) are marked `data-no-drag` so
623
+ // their clicks land on their onClick handlers instead of starting a drag.
624
+ const draggingRef = useRef(false);
625
+ const moveFrameRef = useRef<number | null>(null);
626
+
627
+ const handleBubblePointerDown = (e: React.PointerEvent) => {
628
+ // Only the left button initiates a drag.
622
629
  if (e.button !== 0) return;
623
630
  const target = e.target as HTMLElement;
624
- // Walk up from the target — any ancestor marked `data-no-drag`
625
- // means we're over a real control, not the draggable surface.
631
+ // Any ancestor marked `data-no-drag` means we're over a real control.
626
632
  if (target.closest("[data-no-drag]")) return;
627
- console.log("[bubble] startDragging");
628
- getCurrentWindow()
629
- .startDragging()
630
- .catch((err) => {
631
- console.warn("[bubble] startDragging failed", err);
633
+ e.preventDefault();
634
+ draggingRef.current = true;
635
+ // Pointer capture keeps pointermove/up flowing even after the cursor
636
+ // leaves the (small) bubble window — which it will the moment the bubble
637
+ // hits an edge and the cursor outruns it.
638
+ try {
639
+ (e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
640
+ } catch {
641
+ // capture is best-effort
642
+ }
643
+ void invoke("bubble_drag_start").catch((err) => {
644
+ console.warn("[bubble] bubble_drag_start failed", err);
645
+ });
646
+ };
647
+
648
+ const handleBubblePointerMove = (e: React.PointerEvent) => {
649
+ if (!draggingRef.current) return;
650
+ // Coalesce a burst of pointermove events into at most one reposition per
651
+ // animation frame, no matter how fast the OS delivers them. Rust reads the
652
+ // live cursor itself, so the frame callback needs no coordinates.
653
+ if (moveFrameRef.current != null) return;
654
+ moveFrameRef.current = requestAnimationFrame(() => {
655
+ moveFrameRef.current = null;
656
+ if (!draggingRef.current) return;
657
+ void invoke("bubble_drag_move").catch(() => {
658
+ // Transient failures (e.g. window mid-teardown) are non-fatal; the
659
+ // next pointermove schedules another frame.
632
660
  });
661
+ });
662
+ };
663
+
664
+ const endBubbleDrag = (e: React.PointerEvent) => {
665
+ if (!draggingRef.current) return;
666
+ draggingRef.current = false;
667
+ if (moveFrameRef.current != null) {
668
+ cancelAnimationFrame(moveFrameRef.current);
669
+ moveFrameRef.current = null;
670
+ }
671
+ try {
672
+ (e.currentTarget as HTMLElement).releasePointerCapture(e.pointerId);
673
+ } catch {
674
+ // already released
675
+ }
676
+ void invoke("bubble_drag_end").catch((err) => {
677
+ console.warn("[bubble] bubble_drag_end failed", err);
678
+ });
633
679
  };
634
680
 
635
681
  return (
636
- // The ENTIRE wrapper catches mousedown and calls `startDragging()`
637
- // directly. No `data-tauri-drag-region` see `handleBubbleMouseDown`.
682
+ // The ENTIRE wrapper is the drag surface — pointer-down drives the manual
683
+ // clamp-before-move drag loop. No `startDragging()`, no
684
+ // `data-tauri-drag-region` — see `handleBubblePointerDown` above.
638
685
  <div
639
686
  className={`bubble-wrapper bubble-${size}`}
640
687
  onMouseEnter={handleMouseEnter}
641
688
  onMouseLeave={handleMouseLeave}
642
- onMouseDown={handleBubbleMouseDown}
689
+ onPointerDown={handleBubblePointerDown}
690
+ onPointerMove={handleBubblePointerMove}
691
+ onPointerUp={endBubbleDrag}
692
+ onPointerCancel={endBubbleDrag}
643
693
  data-path={activePath}
644
694
  >
645
695
  <div className="bubble-root">
@@ -676,7 +726,7 @@ export function Bubble() {
676
726
  }
677
727
  />
678
728
  {/* Close X — top-right of bubble, only visible on hover. Marked
679
- `data-no-drag` so mousedown here does NOT call startDragging;
729
+ `data-no-drag` so pointer-down here does NOT start a drag;
680
730
  onClick fires normally. */}
681
731
  <button
682
732
  type="button"
@@ -46,12 +46,6 @@ export function Countdown() {
46
46
  };
47
47
  }, [closeWithEvent]);
48
48
 
49
- useEffect(() => {
50
- if (n === 1) {
51
- emit("clips:countdown-one").catch(() => {});
52
- }
53
- }, [n]);
54
-
55
49
  useEffect(() => {
56
50
  if (n <= 0) {
57
51
  closeWithEvent("clips:countdown-done");
@@ -14,7 +14,10 @@ import {
14
14
  const OVERLAY_SHADOW_GUTTER = 18;
15
15
  const TOOLBAR_CONTENT_WIDTH = 72;
16
16
  const TOOLBAR_COLLAPSED_HEIGHT = 150;
17
- const TOOLBAR_EXPANDED_HEIGHT = 234;
17
+ // Collapsed content box (150 − 20 padding = 130) holds the centered primary
18
+ // zone; the expanded height must fit that fixed 130 zone + the 88px hover
19
+ // actions (+2 margin) + 20 vertical padding so nothing clips on hover.
20
+ const TOOLBAR_EXPANDED_HEIGHT = 240;
18
21
  const TOOLBAR_WINDOW_WIDTH = TOOLBAR_CONTENT_WIDTH + OVERLAY_SHADOW_GUTTER * 2;
19
22
  const TOOLBAR_COLLAPSED_WINDOW_HEIGHT =
20
23
  TOOLBAR_COLLAPSED_HEIGHT + OVERLAY_SHADOW_GUTTER * 2;
@@ -225,51 +228,58 @@ export function Toolbar() {
225
228
  onFocusCapture={() => resizeToolbarWindow(true)}
226
229
  onBlurCapture={handleToolbarBlur}
227
230
  >
228
- <button
229
- className="toolbar-v-stop"
230
- onClick={stop}
231
- disabled={!!pendingAction || !enabled}
232
- aria-label={
233
- pendingAction === "stop" ? "Stopping recording" : "Stop recording"
234
- }
235
- title={
236
- pendingAction === "stop"
237
- ? pendingActionLabel
238
- : enabled
239
- ? "Stop recording"
240
- : "Recording not started yet"
241
- }
242
- data-no-drag
243
- >
244
- {pendingAction === "stop" ? (
245
- <IconLoader2 className="toolbar-v-spinner" size={18} />
246
- ) : (
247
- <span className="toolbar-v-stop-square" />
248
- )}
249
- </button>
250
- <div className="toolbar-v-time">{formatTime(elapsed)}</div>
251
- <button
252
- className="toolbar-v-pause"
253
- onClick={togglePause}
254
- disabled={!enabled || !!pendingAction}
255
- aria-label={paused ? "Resume" : "Pause"}
256
- title={
257
- pendingAction
258
- ? pendingActionLabel
259
- : enabled
260
- ? paused
261
- ? "Resume"
262
- : "Pause"
263
- : "Recording not started yet"
264
- }
265
- data-no-drag
266
- >
267
- {paused ? (
268
- <IconPlayerPlayFilled size={18} />
269
- ) : (
270
- <IconPlayerPauseFilled size={18} />
271
- )}
272
- </button>
231
+ {/* Primary controls live in a fixed-height zone so they stay pinned
232
+ to the same vertical position whether or not the pill is hovered.
233
+ Centering happens INSIDE this zone (not on the pill), so the
234
+ collapsed→expanded `justify-content` change can't nudge the Stop
235
+ button up — only the hover actions below grow into the new space. */}
236
+ <div className="toolbar-v-primary">
237
+ <button
238
+ className="toolbar-v-stop"
239
+ onClick={stop}
240
+ disabled={!!pendingAction || !enabled}
241
+ aria-label={
242
+ pendingAction === "stop" ? "Stopping recording" : "Stop recording"
243
+ }
244
+ title={
245
+ pendingAction === "stop"
246
+ ? pendingActionLabel
247
+ : enabled
248
+ ? "Stop recording"
249
+ : "Recording not started yet"
250
+ }
251
+ data-no-drag
252
+ >
253
+ {pendingAction === "stop" ? (
254
+ <IconLoader2 className="toolbar-v-spinner" size={18} />
255
+ ) : (
256
+ <span className="toolbar-v-stop-square" />
257
+ )}
258
+ </button>
259
+ <div className="toolbar-v-time">{formatTime(elapsed)}</div>
260
+ <button
261
+ className="toolbar-v-pause"
262
+ onClick={togglePause}
263
+ disabled={!enabled || !!pendingAction}
264
+ aria-label={paused ? "Resume" : "Pause"}
265
+ title={
266
+ pendingAction
267
+ ? pendingActionLabel
268
+ : enabled
269
+ ? paused
270
+ ? "Resume"
271
+ : "Pause"
272
+ : "Recording not started yet"
273
+ }
274
+ data-no-drag
275
+ >
276
+ {paused ? (
277
+ <IconPlayerPlayFilled size={18} />
278
+ ) : (
279
+ <IconPlayerPauseFilled size={18} />
280
+ )}
281
+ </button>
282
+ </div>
273
283
  <div
274
284
  className="toolbar-v-hover-actions"
275
285
  role="group"
@@ -2106,8 +2106,18 @@ body[data-clips-route="recording-pill"] #root {
2106
2106
  font-weight: 800;
2107
2107
  font-variant-numeric: tabular-nums;
2108
2108
  color: #ffffff;
2109
- -webkit-text-stroke: 1px rgba(17, 24, 39, 0.22);
2109
+ /*
2110
+ * Do NOT use -webkit-text-stroke here: it joins with a hard miter and no way
2111
+ * to set stroke-linejoin, so at this size/weight the sharp concave corner of
2112
+ * the "2" (where the curved top meets the flat bottom bar) sprouts star-shaped
2113
+ * miter spikes. We fake a soft dark edge with a 4-direction 1px shadow halo
2114
+ * instead, which has no join geometry and cannot spike.
2115
+ */
2110
2116
  text-shadow:
2117
+ 1px 0 1px rgba(17, 24, 39, 0.22),
2118
+ -1px 0 1px rgba(17, 24, 39, 0.22),
2119
+ 0 1px 1px rgba(17, 24, 39, 0.22),
2120
+ 0 -1px 1px rgba(17, 24, 39, 0.22),
2111
2121
  0 2px 4px rgba(0, 0, 0, 0.48),
2112
2122
  0 12px 34px rgba(0, 0, 0, 0.36),
2113
2123
  0 0 2px rgba(0, 0, 0, 0.72);
@@ -2420,8 +2430,12 @@ body[data-clips-route="recording-pill"] #root {
2420
2430
  display: flex;
2421
2431
  flex-direction: column;
2422
2432
  align-items: center;
2423
- justify-content: center;
2424
- gap: 10px;
2433
+ /* Pin content to the top in BOTH states. The primary controls are
2434
+ centered inside their own fixed-height zone (.toolbar-v-primary), so
2435
+ the collapsed→expanded growth only adds room BELOW them. Without this,
2436
+ flipping justify-content center→flex-start on hover yanked the Stop
2437
+ button up toward the bar's top edge. */
2438
+ justify-content: flex-start;
2425
2439
  /* Equal inset on all four sides from the pill edge to the buttons. */
2426
2440
  padding: 10px;
2427
2441
  overflow: hidden;
@@ -2440,8 +2454,9 @@ body[data-clips-route="recording-pill"] #root {
2440
2454
 
2441
2455
  .toolbar-v:hover,
2442
2456
  .toolbar-v:focus-within {
2443
- height: 234px;
2444
- justify-content: flex-start;
2457
+ /* Matches TOOLBAR_EXPANDED_HEIGHT in toolbar.tsx — the fixed 130px primary
2458
+ zone + 88px hover actions (+2 margin) + 20px vertical padding. */
2459
+ height: 240px;
2445
2460
  border-radius: 26px;
2446
2461
  }
2447
2462
 
@@ -2449,6 +2464,19 @@ body[data-clips-route="recording-pill"] #root {
2449
2464
  cursor: grabbing;
2450
2465
  }
2451
2466
 
2467
+ /* Fixed-height home for Stop / time / Pause. Its height equals the collapsed
2468
+ pill's content box (150px − 20px vertical padding = 130px), so the trio is
2469
+ centered exactly as before when collapsed and never shifts when the pill
2470
+ grows on hover — the extra height flows into .toolbar-v-hover-actions below. */
2471
+ .toolbar-v-primary {
2472
+ flex: 0 0 130px;
2473
+ display: flex;
2474
+ flex-direction: column;
2475
+ align-items: center;
2476
+ justify-content: center;
2477
+ gap: 10px;
2478
+ }
2479
+
2452
2480
  .toolbar-v-stop {
2453
2481
  width: 30px;
2454
2482
  height: 30px;
@@ -5,6 +5,7 @@ use std::process::Command;
5
5
  #[cfg(target_os = "macos")]
6
6
  use std::process::Stdio;
7
7
  use std::sync::atomic::{AtomicBool, Ordering};
8
+ use std::sync::{Mutex, OnceLock};
8
9
  use std::thread;
9
10
  use std::time::Duration;
10
11
  use tauri::{
@@ -216,6 +217,36 @@ fn clamp_existing_bubble_window(app: &AppHandle, window: &WebviewWindow) {
216
217
  }
217
218
  }
218
219
 
220
+ /// True while the user is hand-dragging the bubble through the JS pointer
221
+ /// handlers (`bubble_drag_start` / `bubble_drag_move` / `bubble_drag_end`).
222
+ ///
223
+ /// While this is set, the bubble's `Moved` event handler skips its own clamp.
224
+ /// That handler used to re-clamp on EVERY move — including the OS-native drag's
225
+ /// interpolated moves — calling `set_position` to snap the window back inside
226
+ /// the screen. During a drag the OS keeps shoving the window back out toward the
227
+ /// cursor, so the snap-back and the OS fought each frame and the bubble
228
+ /// visibly jittered. Now the drag-move command is the sole authority on
229
+ /// position during a drag and clamps every frame itself, so the handler must
230
+ /// yield to it.
231
+ static BUBBLE_DRAGGING: AtomicBool = AtomicBool::new(false);
232
+
233
+ /// Anchor captured at the start of a hand-drag: the global cursor position and
234
+ /// the bubble window's top-left, both in physical px with a desktop top-left
235
+ /// origin (the same space `cursor_position()` / `outer_position()` report in).
236
+ /// Each move computes `win_start + (cursor_now - cursor_start)` so the bubble
237
+ /// tracks the cursor 1:1, then clamps to the monitor BEFORE moving.
238
+ struct BubbleDragAnchor {
239
+ cursor_x: i32,
240
+ cursor_y: i32,
241
+ win_x: i32,
242
+ win_y: i32,
243
+ }
244
+
245
+ fn bubble_drag_anchor() -> &'static Mutex<Option<BubbleDragAnchor>> {
246
+ static ANCHOR: OnceLock<Mutex<Option<BubbleDragAnchor>>> = OnceLock::new();
247
+ ANCHOR.get_or_init(|| Mutex::new(None))
248
+ }
249
+
219
250
  /// Path to the JSON blob that stores the last-known bubble position on disk.
220
251
  /// Lives in the Tauri app-data dir (platform-specific — `~/Library/Application
221
252
  /// Support/<bundle-id>/` on macOS). Returns None if the app-data dir cannot be
@@ -842,6 +873,13 @@ pub async fn show_bubble(app: AppHandle) -> Result<(), String> {
842
873
  | tauri::WindowEvent::Resized(_)
843
874
  | tauri::WindowEvent::ScaleFactorChanged { .. }
844
875
  ) {
876
+ // During a hand-drag the move command owns the position and clamps
877
+ // every frame; re-clamping here would race that loop and bring back
878
+ // the edge jitter, so yield until the drag ends (which runs a final
879
+ // clamp of its own).
880
+ if BUBBLE_DRAGGING.load(Ordering::SeqCst) {
881
+ return;
882
+ }
845
883
  clamp_existing_bubble_window(&app_for_bounds, &win_for_bounds);
846
884
  }
847
885
  });
@@ -1689,6 +1727,81 @@ pub async fn save_bubble_position(app: AppHandle, x: i32, y: i32) -> Result<(),
1689
1727
  Ok(())
1690
1728
  }
1691
1729
 
1730
+ /// Begin a Loom-style hand-drag of the bubble. The JS pointer handler calls
1731
+ /// this on pointer-down. We snapshot the current cursor and window position as
1732
+ /// the drag anchor and flip `BUBBLE_DRAGGING` so the bounds handler yields to
1733
+ /// the drag loop.
1734
+ ///
1735
+ /// We deliberately do NOT use Tauri's native `startDragging()`: the OS window
1736
+ /// server owns the position during a native drag, so clamping it to the screen
1737
+ /// edge means fighting the OS every frame (the jitter/snap-back the user saw).
1738
+ /// Driving the move ourselves lets us clamp BEFORE moving, so the bubble stops
1739
+ /// dead at the edge like a puck hitting a wall.
1740
+ #[tauri::command]
1741
+ pub async fn bubble_drag_start(app: AppHandle) -> Result<(), String> {
1742
+ let Some(window) = app.get_webview_window(BUBBLE_LABEL) else {
1743
+ return Ok(());
1744
+ };
1745
+ let (Ok(cursor), Ok(pos)) = (window.cursor_position(), window.outer_position()) else {
1746
+ return Ok(());
1747
+ };
1748
+ *bubble_drag_anchor().lock().unwrap_or_else(|e| e.into_inner()) = Some(BubbleDragAnchor {
1749
+ cursor_x: cursor.x.round() as i32,
1750
+ cursor_y: cursor.y.round() as i32,
1751
+ win_x: pos.x,
1752
+ win_y: pos.y,
1753
+ });
1754
+ BUBBLE_DRAGGING.store(true, Ordering::SeqCst);
1755
+ Ok(())
1756
+ }
1757
+
1758
+ /// Move the bubble to follow the cursor for the active hand-drag. The JS
1759
+ /// pointer handler calls this once per animation frame while dragging.
1760
+ ///
1761
+ /// The new top-left is `win_start + (cursor_now - cursor_start)` — a 1:1
1762
+ /// follow in physical px — then clamped to the target monitor BEFORE the move.
1763
+ /// Because we clamp first, the window never overshoots the edge, so there is
1764
+ /// nothing to snap back from: the cursor can keep travelling past the edge
1765
+ /// while the bubble sits pinned against it. No-op if no drag is in progress.
1766
+ #[tauri::command]
1767
+ pub async fn bubble_drag_move(app: AppHandle) -> Result<(), String> {
1768
+ let Some(window) = app.get_webview_window(BUBBLE_LABEL) else {
1769
+ return Ok(());
1770
+ };
1771
+ let Ok(cursor) = window.cursor_position() else {
1772
+ return Ok(());
1773
+ };
1774
+ let target = {
1775
+ let guard = bubble_drag_anchor().lock().unwrap_or_else(|e| e.into_inner());
1776
+ let Some(anchor) = guard.as_ref() else {
1777
+ return Ok(());
1778
+ };
1779
+ (
1780
+ anchor.win_x + (cursor.x.round() as i32 - anchor.cursor_x),
1781
+ anchor.win_y + (cursor.y.round() as i32 - anchor.cursor_y),
1782
+ )
1783
+ };
1784
+ let Ok(size) = window.outer_size() else {
1785
+ return Ok(());
1786
+ };
1787
+ let (x, y) = clamp_bubble_window_position(&app, target.0, target.1, size.width, size.height);
1788
+ let _ = window.set_position(PhysicalPosition::new(x, y));
1789
+ Ok(())
1790
+ }
1791
+
1792
+ /// End the hand-drag: clear the anchor, drop the dragging flag, and run one
1793
+ /// final clamp so the resting position is guaranteed in-bounds. The JS
1794
+ /// `onMoved` listener persists the final spot via `save_bubble_position`.
1795
+ #[tauri::command]
1796
+ pub async fn bubble_drag_end(app: AppHandle) -> Result<(), String> {
1797
+ *bubble_drag_anchor().lock().unwrap_or_else(|e| e.into_inner()) = None;
1798
+ BUBBLE_DRAGGING.store(false, Ordering::SeqCst);
1799
+ if let Some(window) = app.get_webview_window(BUBBLE_LABEL) {
1800
+ clamp_existing_bubble_window(&app, &window);
1801
+ }
1802
+ Ok(())
1803
+ }
1804
+
1692
1805
  #[tauri::command]
1693
1806
  pub async fn show_popover(app: AppHandle) -> Result<(), String> {
1694
1807
  if let Some(window) = app.get_webview_window("popover") {
@@ -87,6 +87,9 @@ pub fn run() {
87
87
  clips::set_recording_state,
88
88
  clips::reset_state,
89
89
  clips::save_bubble_position,
90
+ clips::bubble_drag_start,
91
+ clips::bubble_drag_move,
92
+ clips::bubble_drag_end,
90
93
  clips::set_bubble_size,
91
94
  clips::load_bubble_size,
92
95
  // config commands
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Framework route the background function actually runs — sibling to
3
+ * `AGENT_TEAM_PROCESS_RUN_PATH`. Reached *through* the Netlify background
4
+ * function, so it inherits the 15-min budget.
5
+ */
6
+ export declare const AGENT_CHAT_PROCESS_RUN_PATH = "/_agent-native/agent-chat/_process-run";
7
+ /** Env flag (off by default) that opts an app into durable background runs. */
8
+ export declare const AGENT_CHAT_DURABLE_BACKGROUND_ENV = "AGENT_CHAT_DURABLE_BACKGROUND";
9
+ /**
10
+ * Body field the foreground handler injects when self-dispatching to the
11
+ * background processor. Its presence is how the re-entered handler knows it is
12
+ * the background worker (run inline with the background soft-timeout; do NOT
13
+ * re-claim the slot or re-dispatch). Untrusted on its own — the route also
14
+ * verifies the HMAC token before invoking the handler.
15
+ */
16
+ export declare const AGENT_CHAT_BACKGROUND_RUN_FIELD = "__backgroundRun";
17
+ /**
18
+ * Mirror of run-manager's private `isHostedRuntime`. Kept in sync deliberately:
19
+ * the durable-background gate must agree with the soft-timeout regime about
20
+ * what "hosted" means.
21
+ */
22
+ export declare function isHostedRuntimeForDurableBackground(): boolean;
23
+ /**
24
+ * The single gate. True only when the flag is on AND the runtime is hosted AND
25
+ * A2A_SECRET is configured. False otherwise — and false means the current
26
+ * synchronous behavior is used, unchanged.
27
+ */
28
+ export declare function isAgentChatDurableBackgroundEnabled(): boolean;
29
+ /** Decision returned by `prepareProcessRunRequest`. */
30
+ export type ProcessRunPreparation = {
31
+ ok: true;
32
+ /** The pre-claimed run id the background worker must reuse. */
33
+ runId: string;
34
+ /** Body to stash for the re-entered handler (marker guaranteed present). */
35
+ body: Record<string, unknown>;
36
+ } | {
37
+ ok: false;
38
+ /** HTTP status the route should return. */
39
+ status: number;
40
+ /** Error payload. */
41
+ error: string;
42
+ };
43
+ /**
44
+ * Pure, transport-agnostic core of the `_process-run` route: validate the body,
45
+ * authenticate the HMAC self-dispatch, and produce the body the re-entered
46
+ * agent-chat handler should run as the background worker.
47
+ *
48
+ * Auth policy mirrors the agent-teams processor exactly:
49
+ * - `A2A_SECRET` set → require a valid `verifyInternalToken(runId, token)`.
50
+ * - no secret but a production runtime → refuse (503) — never run unsigned in
51
+ * prod.
52
+ * - no secret + non-prod (local dev) → allow unsigned; the SQL atomic claim
53
+ * in the worker still prevents double-processing.
54
+ *
55
+ * Extracted from the route handler so the auth + marker-prep decision is unit
56
+ * testable without booting the whole Nitro plugin. The route only adds body
57
+ * reading and the final handler invocation around this.
58
+ */
59
+ export declare function prepareProcessRunRequest(body: unknown, authHeader: string | undefined): ProcessRunPreparation;
60
+ //# sourceMappingURL=durable-background.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"durable-background.d.ts","sourceRoot":"","sources":["../../src/agent/durable-background.ts"],"names":[],"mappings":"AAmCA;;;;GAIG;AACH,eAAO,MAAM,2BAA2B,2CACE,CAAC;AAE3C,+EAA+E;AAC/E,eAAO,MAAM,iCAAiC,kCACb,CAAC;AAElC;;;;;;GAMG;AACH,eAAO,MAAM,+BAA+B,oBAAoB,CAAC;AAEjE;;;;GAIG;AACH,wBAAgB,mCAAmC,IAAI,OAAO,CAsB7D;AAiBD;;;;GAIG;AACH,wBAAgB,mCAAmC,IAAI,OAAO,CAM7D;AAED,uDAAuD;AACvD,MAAM,MAAM,qBAAqB,GAC7B;IACE,EAAE,EAAE,IAAI,CAAC;IACT,+DAA+D;IAC/D,KAAK,EAAE,MAAM,CAAC;IACd,4EAA4E;IAC5E,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC/B,GACD;IACE,EAAE,EAAE,KAAK,CAAC;IACV,2CAA2C;IAC3C,MAAM,EAAE,MAAM,CAAC;IACf,qBAAqB;IACrB,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEN;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,OAAO,EACb,UAAU,EAAE,MAAM,GAAG,SAAS,GAC7B,qBAAqB,CA0CvB"}