@agent-native/core 0.136.4 → 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.
- package/corpus/README.md +1 -1
- package/corpus/templates/clips/.agents/skills/recording/SKILL.md +53 -0
- package/corpus/templates/clips/actions/import-loom-recording.ts +7 -0
- package/corpus/templates/clips/actions/lib/loom-import-job.ts +24 -4
- package/corpus/templates/clips/changelog/2026-08-03-restart-during-a-recording-now-immediately-starts-a-fresh-ta.md +6 -0
- package/corpus/templates/clips/desktop/src/app.tsx +81 -30
- package/corpus/templates/clips/desktop/src/lib/recorder.ts +436 -158
- package/corpus/templates/clips/server/lib/post-finalize-dispatch.ts +14 -1
- package/corpus/templates/clips/server/plugins/auth.ts +9 -0
- package/corpus/templates/clips/server/routes/api/_agent-native-background/post-finalize-worker.post.ts +13 -0
- package/dist/client/settings/SecretsSection.js +32 -8
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/localization/default-messages.d.ts +5 -0
- package/dist/localization/default-messages.js +5 -0
- package/dist/mcp/screen-memory-stdio.d.ts +7 -7
- package/dist/notifications/routes.d.ts +5 -5
- package/dist/observability/routes.d.ts +1 -1
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/secrets/routes.d.ts +2 -2
- package/dist/secrets/routes.js +41 -17
- package/package.json +1 -1
package/corpus/README.md
CHANGED
|
@@ -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
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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,
|
|
@@ -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.
|
|
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
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
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
|
);
|