@agent-native/core 0.136.3 → 0.136.5

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 (49) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/clips/.agents/skills/recording/SKILL.md +53 -0
  3. package/corpus/templates/clips/actions/import-loom-recording.ts +7 -0
  4. package/corpus/templates/clips/actions/lib/loom-import-job.ts +24 -4
  5. package/corpus/templates/clips/changelog/2026-08-03-restart-during-a-recording-now-immediately-starts-a-fresh-ta.md +6 -0
  6. package/corpus/templates/clips/desktop/src/app.tsx +81 -30
  7. package/corpus/templates/clips/desktop/src/lib/recorder.ts +436 -158
  8. package/corpus/templates/clips/server/lib/post-finalize-dispatch.ts +14 -1
  9. package/corpus/templates/clips/server/plugins/auth.ts +9 -0
  10. package/corpus/templates/clips/server/routes/api/_agent-native-background/post-finalize-worker.post.ts +13 -0
  11. package/dist/client/agent-page/AgentJobsTab.js +19 -4
  12. package/dist/client/agent-page/use-jobs.d.ts +10 -0
  13. package/dist/client/agent-page/use-jobs.js +21 -0
  14. package/dist/client/settings/SecretsSection.js +32 -8
  15. package/dist/collab/awareness.d.ts +2 -2
  16. package/dist/collab/struct-routes.d.ts +1 -1
  17. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  18. package/dist/jobs/actions/run-automation-now.d.ts +6 -0
  19. package/dist/jobs/actions/run-automation-now.js +22 -0
  20. package/dist/jobs/background-automation-runner.d.ts +2 -0
  21. package/dist/jobs/background-automation-runner.js +22 -16
  22. package/dist/jobs/run-history.d.ts +17 -3
  23. package/dist/jobs/run-history.js +73 -7
  24. package/dist/jobs/run-now.d.ts +20 -0
  25. package/dist/jobs/run-now.js +99 -0
  26. package/dist/jobs/scheduler.d.ts +15 -0
  27. package/dist/jobs/scheduler.js +76 -12
  28. package/dist/localization/default-messages.d.ts +8 -0
  29. package/dist/localization/default-messages.js +8 -0
  30. package/dist/notifications/routes.d.ts +4 -4
  31. package/dist/observability/routes.d.ts +3 -3
  32. package/dist/provider-api/actions/custom-provider-registration.d.ts +12 -12
  33. package/dist/provider-api/actions/provider-api.d.ts +4 -4
  34. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  35. package/dist/secrets/routes.d.ts +5 -5
  36. package/dist/secrets/routes.js +41 -17
  37. package/dist/server/action-discovery.js +4 -0
  38. package/dist/server/agent-chat-plugin.js +135 -68
  39. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  40. package/dist/server/email-actions.d.ts +6 -3
  41. package/dist/server/email-actions.js +17 -83
  42. package/dist/server/email-markdown.d.ts +4 -0
  43. package/dist/server/email-markdown.js +174 -0
  44. package/dist/server/transcribe-voice.d.ts +1 -1
  45. package/dist/templates/workspace-core/.agents/skills/automations/SKILL.md +1 -0
  46. package/dist/triggers/actions.d.ts +1 -1
  47. package/dist/triggers/actions.js +27 -5
  48. package/package.json +1 -1
  49. package/src/templates/workspace-core/.agents/skills/automations/SKILL.md +1 -0
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: 7515
34
+ - template files: 7516
@@ -189,6 +189,59 @@ When mode is `screen+camera`, "the bubble" is two different things:
189
189
 
190
190
  Because that draw loop runs continuously for the whole recording, keep its CPU/GPU cost bounded: a Worker-based timer drives the draw loop at the capture frame rate (`SCREEN_CAPTURE_FRAME_RATE`, 24fps — falling back to `requestAnimationFrame` only if Worker creation fails, e.g. under a strict CSP), the canvas is hard-capped to 1080p-class dimensions (1920px on its longest edge) even if the source display track is Retina/4K, and the bubble's drop shadow is pre-rendered into a small cached sprite (keyed by bubble size) instead of re-blurring with `shadowBlur` every frame.
191
191
 
192
+ ## Restart (desktop)
193
+
194
+ Restart throws the current take away and immediately starts another one. It
195
+ must never re-acquire the screen: the toolbar click reaches the popover through
196
+ async Tauri IPC, which carries no user activation, so a second `getDisplayMedia`
197
+ would throw. Instead the dying session hands its live display and mic streams to
198
+ the replacement — `RecorderHandle.discardForRestart()` returns a `RestartHandoff`
199
+ that `startRecording` accepts as `preAcquiredDisplayStream` /
200
+ `preAcquiredAudioStream`.
201
+
202
+ Ownership is the opposite of the camera's. The popover owns the camera stream for
203
+ the whole session and re-hands it unchanged, so restart must not bump
204
+ `bubbleSessionEpoch` or clear `bubbleStreamTransferredToRecorder`. The handed-off
205
+ display and mic streams belong to the **recorder**, so the new session stops them
206
+ on its own stop/cancel, and whoever asked for the restart must stop them if the
207
+ new session never comes up.
208
+
209
+ `cancel()` and `discardForRestart()` share one `discardTake(forRestart)` per
210
+ backend, and the difference is which teardown they run. Cancel ends the
211
+ *session*: `hide_overlays` (which destroys the camera bubble window too) and
212
+ `clearRecordingState()`. A restart ends only the *take*, so it uses
213
+ `hide_recording_chrome`, which spares the bubble, and leaves the recording state
214
+ active.
215
+
216
+ Stop, cancel and restart are mutually exclusive terminal transitions, so each
217
+ gets its own promise slot — never reuse `cancelPromise` for a discard, or a
218
+ cancel arriving mid-restart returns the take-level teardown and the session
219
+ never ends. A cancel that lands after a discard still owes the session half,
220
+ plus stopping the capture the retake never took ownership of. On the app side
221
+ `restartInFlightRef` latches synchronously so stop and cancel events cannot act
222
+ on the recorder a restart is already tearing down.
223
+
224
+ Native full-screen backends re-acquire capture in Rust and hand off nothing.
225
+ `resolveRestartHandoff` vets the inherited streams before anything is acquired,
226
+ and it treats the two kinds of capture differently: an ended display share is
227
+ fatal and fails with `RESTART_CAPTURE_ENDED_MESSAGE`, because re-acquiring it
228
+ would surface an activation error that names the wrong cause, while an ended
229
+ microphone is just dropped and re-acquired normally. A restart also has to wait
230
+ for the old take's `transcriptionCapture.cancel()` — the engine is process-global
231
+ (`audio_transcription_stop` / `native_speech_stop`), so a late cancel would stop
232
+ the replacement's engine. And `stop()` after a discard must throw rather than
233
+ answer with the discarded take's id, or an aborted recording gets published as a
234
+ finished one. The
235
+ recording-flow latches (`recordingFlowGateRef`, `recordingFlowActive`,
236
+ `clipsForceAlive`, `set_recording_state`) stay held across the restart; releasing
237
+ them the way cancel does lets the popover blur auto-hide fire mid-restart.
238
+
239
+ Holding those latches has a catch. The `show_toolbar` effect is keyed on
240
+ `isRecording || recordingFlowActive`, so it does not re-run when the flow never
241
+ leaves — but the discard already closed the toolbar window. Restart therefore
242
+ bumps `recordingChromeEpoch` to rebuild it. The countdown needs no such nudge;
243
+ the recorder recreates it on every start.
244
+
192
245
  ## Error recovery
193
246
 
194
247
  | Failure | Handling |
@@ -387,11 +387,18 @@ export default defineAction({
387
387
  await writeAppState("navigate", { view: "recording", recordingId: id });
388
388
 
389
389
  try {
390
+ console.log("[import-loom-recording] dispatching loom-import job", {
391
+ recordingId: id,
392
+ provider: providerId,
393
+ });
390
394
  await dispatchPostFinalizeJob({
391
395
  recordingId: id,
392
396
  kind: "loom-import",
393
397
  requireAccepted: true,
394
398
  });
399
+ console.log("[import-loom-recording] loom-import job accepted", {
400
+ recordingId: id,
401
+ });
395
402
  } catch (err) {
396
403
  const failureReason = `Could not start the Loom import: ${
397
404
  err instanceof Error ? err.message : String(err)
@@ -55,6 +55,11 @@ export async function failLoomImport(
55
55
  failureReason: string,
56
56
  claimId?: string,
57
57
  ): Promise<LoomImportJobResult> {
58
+ console.error("[loom-import] failed", {
59
+ recordingId,
60
+ claimId,
61
+ failureReason,
62
+ });
58
63
  const now = new Date().toISOString();
59
64
  const [updated] = await getDb()
60
65
  .update(schema.recordings)
@@ -140,9 +145,16 @@ export async function runLoomImportJob({
140
145
  );
141
146
  }
142
147
 
148
+ console.log("[loom-import] job started", { recordingId, claimId });
149
+
143
150
  let media: Awaited<ReturnType<typeof downloadLoomVideo>>;
144
151
  try {
145
152
  media = await downloadLoomVideo({ loomId, shareUrl });
153
+ console.log("[loom-import] download complete", {
154
+ recordingId,
155
+ bytes: media.sizeBytes,
156
+ mimeType: media.mimeType,
157
+ });
146
158
  } catch (err) {
147
159
  return failLoomImport(
148
160
  recordingId,
@@ -167,6 +179,10 @@ export async function runLoomImportJob({
167
179
  claimId,
168
180
  );
169
181
  }
182
+ console.log("[loom-import] reupload complete", {
183
+ recordingId,
184
+ videoUrl: upload.url,
185
+ });
170
186
 
171
187
  const now = new Date().toISOString();
172
188
  const [mediaReady] = await db
@@ -187,11 +203,15 @@ export async function runLoomImportJob({
187
203
  )
188
204
  .returning({ id: schema.recordings.id });
189
205
  if (!mediaReady) {
190
- return {
191
- status: "failed",
192
- failureReason: "The Loom import lease was lost before media was saved.",
193
- };
206
+ const failureReason =
207
+ "The Loom import lease was lost before media was saved.";
208
+ console.warn("[loom-import] lease lost before ready", {
209
+ recordingId,
210
+ claimId,
211
+ });
212
+ return { status: "failed", failureReason };
194
213
  }
214
+ console.log("[loom-import] recording ready", { recordingId });
195
215
 
196
216
  void queueBuilderMediaCompression({
197
217
  recordingId,
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-08-03
4
+ ---
5
+
6
+ Restart during a recording now immediately starts a fresh take instead of cancelling.
@@ -90,6 +90,7 @@ import {
90
90
  type PendingBrowserRecordingUpload,
91
91
  type RecorderHandle,
92
92
  type RecorderStopResult,
93
+ type RestartHandoff,
93
94
  } from "./lib/recorder";
94
95
  import {
95
96
  copyRecordingShareLink,
@@ -377,6 +378,12 @@ function normalizeCaptureSource(value: string): CaptureSource {
377
378
  return value === "window" ? "window" : "full-screen";
378
379
  }
379
380
 
381
+ function stopRestartHandoff(handoff: RestartHandoff): void {
382
+ [handoff.displayStream, handoff.audioStream].forEach((stream) =>
383
+ stream?.getTracks().forEach((track) => track.stop()),
384
+ );
385
+ }
386
+
380
387
  type FetchInput = Parameters<typeof fetch>[0];
381
388
  type FetchInit = Parameters<typeof fetch>[1];
382
389
 
@@ -1039,6 +1046,12 @@ export function App() {
1039
1046
  refreshHomeScreenMemoryStatus,
1040
1047
  ]);
1041
1048
  const recordShortcutHandlerRef = useRef<() => void>(() => {});
1049
+ const handleStartRecordingRef = useRef<
1050
+ (options?: {
1051
+ ignoreActiveRecorder?: boolean;
1052
+ resumeCapture?: RestartHandoff;
1053
+ }) => Promise<RecorderHandle | null>
1054
+ >(async () => null);
1042
1055
  // Mirrors `bubbleActive` (assigned below once it is computed) so device
1043
1056
  // probes can synchronously tell whether the camera bubble owns the grant.
1044
1057
  const bubbleActiveRef = useRef(false);
@@ -2197,6 +2210,11 @@ export function App() {
2197
2210
  // bubble effect re-acquires even if bubbleActive/cameraId are unchanged
2198
2211
  // (post-stop reopen with a blank "Default Camera" preview).
2199
2212
  const [bubbleSessionEpoch, setBubbleSessionEpoch] = useState(0);
2213
+ // Bumped when a session tears the recording chrome down without leaving the
2214
+ // recording flow, which only a restart does. `toolbarActive` stays true
2215
+ // across that handoff, so without an epoch the toolbar effect never re-runs
2216
+ // and the closed toolbar window is never rebuilt.
2217
+ const [recordingChromeEpoch, setRecordingChromeEpoch] = useState(0);
2200
2218
  const wantsCamera = mode !== "screen" && cameraOn;
2201
2219
  const nativeFullscreenRecordingActive =
2202
2220
  mode !== "camera" && shouldUseNativeFullscreenRecording(source);
@@ -2209,6 +2227,10 @@ export function App() {
2209
2227
  // fresh camera session can recover immediately. Keep that post-stop phase
2210
2228
  // separate so React cleanup does not close the finalizing progress window.
2211
2229
  const recordingStopFinalizingRef = useRef(false);
2230
+ // Held from the restart click until the replacement recorder is up (or has
2231
+ // failed). Stop and cancel are terminal transitions on the recorder a
2232
+ // restart is already tearing down, so they must not run against it.
2233
+ const restartInFlightRef = useRef(false);
2212
2234
  const recordingInFlight = isRecording || recordingFlowActive;
2213
2235
  useLayoutEffect(() => {
2214
2236
  recordingFlowGateRef.current = recordingInFlight;
@@ -2252,7 +2274,7 @@ export function App() {
2252
2274
  }).catch(() => {});
2253
2275
  }
2254
2276
  };
2255
- }, [toolbarActive]);
2277
+ }, [toolbarActive, recordingChromeEpoch]);
2256
2278
 
2257
2279
  useEffect(() => {
2258
2280
  if (!bubbleActive) return;
@@ -2759,7 +2781,9 @@ export function App() {
2759
2781
 
2760
2782
  async function handleStartRecording(options?: {
2761
2783
  ignoreActiveRecorder?: boolean;
2762
- }) {
2784
+ /** Live capture inherited from the take a restart is replacing. */
2785
+ resumeCapture?: RestartHandoff;
2786
+ }): Promise<RecorderHandle | null> {
2763
2787
  if (recorder && !options?.ignoreActiveRecorder) {
2764
2788
  console.warn(
2765
2789
  "[clips-popover] handleStartRecording ignored — recorder already active",
@@ -2767,7 +2791,7 @@ export function App() {
2767
2791
  setRecError(
2768
2792
  "Still finishing the last recording. Wait a moment, then try again.",
2769
2793
  );
2770
- return;
2794
+ return null;
2771
2795
  }
2772
2796
  const bubbleTracks = bubbleStreamRef.current?.getTracks() ?? [];
2773
2797
  const bubbleStreamDead =
@@ -2782,11 +2806,11 @@ export function App() {
2782
2806
  if (localRecordingMode === "off") {
2783
2807
  if (videoStorageStatus === "checking") {
2784
2808
  setRecError("Checking video storage. Try again in a moment.");
2785
- return;
2809
+ return null;
2786
2810
  }
2787
2811
  if (videoStorageStatus === "missing") {
2788
2812
  openVideoStorageSetup();
2789
- return;
2813
+ return null;
2790
2814
  }
2791
2815
  }
2792
2816
  setRecError(null);
@@ -2810,12 +2834,12 @@ export function App() {
2810
2834
  setReadinessOpen(true);
2811
2835
  setRecError(MACOS_SCREEN_PERMISSION_MESSAGE);
2812
2836
  openPrivacySettings("screen");
2813
- return;
2837
+ return null;
2814
2838
  }
2815
2839
  } catch (err) {
2816
2840
  setReadinessOpen(true);
2817
2841
  setRecError(err instanceof Error ? err.message : String(err));
2818
- return;
2842
+ return null;
2819
2843
  }
2820
2844
  }
2821
2845
 
@@ -2901,6 +2925,8 @@ export function App() {
2901
2925
  systemAudioOn,
2902
2926
  localRecordingMode,
2903
2927
  preAcquiredCameraStream,
2928
+ preAcquiredDisplayStream: options?.resumeCapture?.displayStream ?? null,
2929
+ preAcquiredAudioStream: options?.resumeCapture?.audioStream ?? null,
2904
2930
  });
2905
2931
  // macOS: park the popover to its 2×2 pinhole IMMEDIATELY so it
2906
2932
  // doesn't appear in the screen picker window list. The native
@@ -2967,7 +2993,7 @@ export function App() {
2967
2993
 
2968
2994
  if (handle) {
2969
2995
  setRecorder(handle);
2970
- return;
2996
+ return handle;
2971
2997
  }
2972
2998
 
2973
2999
  // Failure path — the recorder never came up. Side-effects (recording
@@ -2988,13 +3014,13 @@ export function App() {
2988
3014
  errName === "AbortError" ||
2989
3015
  /was cancelled|dismissed|region selection cancelled/i.test(message)
2990
3016
  ) {
2991
- return;
3017
+ return null;
2992
3018
  }
2993
3019
  if (
2994
3020
  errName === "NotAllowedError" &&
2995
3021
  !isHardCapturePermissionError(message)
2996
3022
  ) {
2997
- return;
3023
+ return null;
2998
3024
  }
2999
3025
  if (isHardCapturePermissionError(message)) {
3000
3026
  // If an update has finished downloading and is waiting to install, the
@@ -3009,16 +3035,22 @@ export function App() {
3009
3035
  ? MACOS_CAPTURE_PERMISSION_MESSAGE
3010
3036
  : DESKTOP_CAPTURE_PERMISSION_MESSAGE,
3011
3037
  );
3012
- return;
3038
+ return null;
3013
3039
  }
3014
3040
  if (isStorageSetupFailureMessage(message)) {
3015
3041
  setRecError(STORAGE_SETUP_HELP_TEXT);
3016
3042
  openVideoStorageSetup();
3017
- return;
3043
+ return null;
3018
3044
  }
3019
3045
  setRecError(message);
3046
+ return null;
3020
3047
  }
3021
3048
 
3049
+ // The restart listener lives in an effect keyed on `recorder`; calling the
3050
+ // start flow through this ref keeps that dependency list from having to
3051
+ // include a function that is recreated every render.
3052
+ handleStartRecordingRef.current = handleStartRecording;
3053
+
3022
3054
  recordShortcutHandlerRef.current = () => {
3023
3055
  if (recorder) {
3024
3056
  emit("clips:recorder-stop").catch(() => {});
@@ -3124,6 +3156,7 @@ export function App() {
3124
3156
  };
3125
3157
  track(
3126
3158
  listen("clips:recorder-stop", async () => {
3159
+ if (restartInFlightRef.current) return;
3127
3160
  // Detach the React Start/bubble gate immediately. The recorder keeps
3128
3161
  // Rust `is_recording_active` and the finalizing overlay guarded until
3129
3162
  // its durable backup/finalize boundary; keeping this React handle set
@@ -3185,6 +3218,7 @@ export function App() {
3185
3218
  );
3186
3219
  track(
3187
3220
  listen("clips:recorder-cancel", async () => {
3221
+ if (restartInFlightRef.current) return;
3188
3222
  try {
3189
3223
  await recorder.cancel();
3190
3224
  } finally {
@@ -3206,26 +3240,43 @@ export function App() {
3206
3240
  );
3207
3241
  track(
3208
3242
  listen("clips:recorder-restart", async () => {
3243
+ if (recordingStopFinalizingRef.current) return;
3244
+ // Latched synchronously: a restart is a terminal transition on this
3245
+ // recorder, and stop/cancel must not act on it while the replacement
3246
+ // is being brought up.
3247
+ if (restartInFlightRef.current) return;
3248
+ restartInFlightRef.current = true;
3249
+ let handoff: RestartHandoff | null = null;
3209
3250
  try {
3210
- await recorder.cancel();
3251
+ handoff = await recorder.discardForRestart();
3252
+ if (cancelled) return;
3253
+ // The recording flow stays latched across the restart. Releasing
3254
+ // `clipsForceAlive` / `recordingFlowGateRef` / `recordingFlowActive`
3255
+ // / `set_recording_state` the way cancel does would let the popover's
3256
+ // blur auto-hide fire and flicker the pill between the two takes. The
3257
+ // camera also stays owned by the popover, so the bubble session epoch
3258
+ // is deliberately not bumped and the stream is re-handed unchanged.
3259
+ //
3260
+ // The discard did close the countdown and toolbar windows. The
3261
+ // recorder rebuilds the countdown on every start, but the toolbar is
3262
+ // owned by an effect keyed on the flow latches we just kept held — so
3263
+ // it has to be told the chrome is gone.
3264
+ setRecordingChromeEpoch((epoch) => epoch + 1);
3265
+ setRecorder(null);
3266
+ const restarted = await handleStartRecordingRef.current({
3267
+ ignoreActiveRecorder: true,
3268
+ resumeCapture: handoff,
3269
+ });
3270
+ // The new session owns the handed-off capture only once it exists.
3271
+ if (restarted) handoff = null;
3272
+ } catch (err) {
3273
+ console.error("[clips-popover] restart failed:", err);
3274
+ setRecError(err instanceof Error ? err.message : String(err));
3211
3275
  } finally {
3212
- if (!cancelled) {
3213
- (
3214
- window as unknown as { clipsForceAlive?: boolean }
3215
- ).clipsForceAlive = false;
3216
- bubbleStreamTransferredToRecorder.current = false;
3217
- bubbleStreamRef.current = null;
3218
- recordingFlowGateRef.current = false;
3219
- setRecorder(null);
3220
- setRecordingFlowActive(false);
3221
- setBubbleSessionEpoch((epoch) => epoch + 1);
3222
- invoke("set_recording_state", { active: false }).catch(() => {});
3223
- // Starting a new browser capture must come from a fresh click in
3224
- // this webview. The toolbar click arrives here through async Tauri
3225
- // IPC, so reopen the popover and let the next Start click provide
3226
- // the required user activation.
3227
- invoke("show_popover").catch(() => {});
3228
- }
3276
+ // Anything still held here belongs to a retake that never came up.
3277
+ // Leaving it would keep the screen captured with nothing recording.
3278
+ if (handoff) stopRestartHandoff(handoff);
3279
+ restartInFlightRef.current = false;
3229
3280
  }
3230
3281
  }),
3231
3282
  );