@agent-native/core 0.145.1 → 0.145.3

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 (42) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/brain/server/lib/capture-sanitization.ts +6 -2
  3. package/corpus/templates/clips/changelog/2026-08-07-full-screen-recordings-on-a-multi-monitor-mac-now-let-you-pi.md +6 -0
  4. package/corpus/templates/clips/chrome-extension/src/styles.css +13 -4
  5. package/corpus/templates/clips/desktop/src/app.tsx +38 -1
  6. package/corpus/templates/clips/desktop/src/components/MediaDeviceRow.tsx +123 -104
  7. package/corpus/templates/clips/desktop/src/lib/recorder.ts +165 -2
  8. package/corpus/templates/clips/desktop/src/main.tsx +3 -0
  9. package/corpus/templates/clips/desktop/src/overlays/monitor-picker.tsx +70 -0
  10. package/corpus/templates/clips/desktop/src/styles.css +70 -11
  11. package/corpus/templates/clips/desktop/src-tauri/capabilities/default.json +2 -1
  12. package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +163 -1
  13. package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +6 -1
  14. package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +141 -22
  15. package/corpus/templates/clips/desktop/src-tauri/src/state.rs +27 -1
  16. package/corpus/templates/clips/desktop/src-tauri/src/util.rs +12 -1
  17. package/dist/agent/engine/index.d.ts +1 -1
  18. package/dist/agent/engine/index.js +1 -1
  19. package/dist/agent/engine/registry.d.ts +15 -0
  20. package/dist/agent/engine/registry.js +39 -10
  21. package/dist/agent/production-agent.d.ts +40 -1
  22. package/dist/agent/production-agent.js +81 -40
  23. package/dist/collab/awareness.d.ts +2 -2
  24. package/dist/collab/struct-routes.d.ts +1 -1
  25. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  26. package/dist/integrations/webhook-handler.d.ts +3 -2
  27. package/dist/integrations/webhook-handler.js +8 -38
  28. package/dist/observability/routes.d.ts +3 -3
  29. package/dist/server/agent-chat-plugin.js +67 -33
  30. package/dist/server/complete-text.js +14 -7
  31. package/dist/server/index.d.ts +1 -1
  32. package/dist/server/index.js +1 -1
  33. package/dist/server/poll.d.ts +11 -0
  34. package/dist/server/poll.js +7 -4
  35. package/dist/server/realtime-token.d.ts +1 -1
  36. package/dist/server/realtime-token.js +8 -0
  37. package/dist/server/request-context.d.ts +7 -1
  38. package/dist/server/short-lived-token.d.ts +8 -0
  39. package/dist/server/short-lived-token.js +12 -0
  40. package/dist/server/transcribe-voice.d.ts +1 -1
  41. package/docs/content/environment-variables.mdx +6 -0
  42. package/package.json +3 -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: 7848
34
+ - template files: 7850
@@ -442,9 +442,13 @@ async function classifyWithApprovedModel(
442
442
  if (!model || !engineName) return null;
443
443
 
444
444
  const core = await import("@agent-native/core/server");
445
- const userApiKey = await core.getOwnerActiveApiKey(input.source.ownerEmail);
445
+ const userApiKey = await core.resolveOwnerEngineApiKey({
446
+ engineOption: engineName,
447
+ ownerEmail: input.source.ownerEmail,
448
+ });
446
449
  const engine = await core.resolveEngine({
447
- apiKey: userApiKey ?? undefined,
450
+ apiKey: userApiKey.apiKey,
451
+ apiKeyEnvVar: userApiKey.apiKeyEnvVar,
448
452
  appId: "brain",
449
453
  engineOption: engineName,
450
454
  });
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: added
3
+ date: 2026-08-07
4
+ ---
5
+
6
+ Full-screen recordings on a multi-monitor Mac now let you pick which display to record before capture starts.
@@ -861,7 +861,7 @@ h1 {
861
861
 
862
862
  .field-label {
863
863
  color: var(--foreground);
864
- font-size: 12px;
864
+ font-size: 14px;
865
865
  font-weight: 700;
866
866
  }
867
867
 
@@ -873,6 +873,7 @@ h1 {
873
873
  padding: 0 11px;
874
874
  color: var(--fg);
875
875
  background: var(--bg);
876
+ font-size: 14px;
876
877
  }
877
878
 
878
879
  .form-actions {
@@ -899,14 +900,14 @@ h1 {
899
900
 
900
901
  .toggle-title {
901
902
  color: var(--foreground);
902
- font-size: 13px;
903
+ font-size: 14px;
903
904
  font-weight: 700;
904
905
  }
905
906
 
906
907
  .toggle-subtitle {
907
908
  margin-top: 3px;
908
909
  color: var(--muted-foreground);
909
- font-size: 12px;
910
+ font-size: 13px;
910
911
  line-height: 1.35;
911
912
  }
912
913
 
@@ -923,7 +924,7 @@ h1 {
923
924
  background: var(--primary);
924
925
  color: var(--primary-foreground);
925
926
  cursor: pointer;
926
- font-size: 13px;
927
+ font-size: 14px;
927
928
  font-weight: 700;
928
929
  }
929
930
 
@@ -931,6 +932,14 @@ h1 {
931
932
  opacity: 0.92;
932
933
  }
933
934
 
935
+ .options-page .secondary-action {
936
+ font-size: 14px;
937
+ }
938
+
939
+ .options-page .field-help {
940
+ font-size: 13px;
941
+ }
942
+
934
943
  /* Unsupported-page notice (shown in the popup when the active tab can't host
935
944
  the on-page overlay, e.g. chrome:// / New Tab / Web Store pages). */
936
945
  .notice {
@@ -92,6 +92,7 @@ import {
92
92
  exportBrowserRecordingBackup,
93
93
  getRewindClipOrigin,
94
94
  listBrowserRecordingBackups,
95
+ pickFullscreenRecordingDisplay,
95
96
  retryBrowserRecordingBackup,
96
97
  scheduleNativeBackupCleanupAfterProcessing,
97
98
  shouldUseNativeFullscreenRecording,
@@ -2899,7 +2900,10 @@ export function App() {
2899
2900
  /** Live capture inherited from the take a restart is replacing. */
2900
2901
  resumeCapture?: RestartHandoff;
2901
2902
  }): Promise<RecorderHandle | null> {
2902
- if (recorder && !options?.ignoreActiveRecorder) {
2903
+ if (
2904
+ (recorder || recordingFlowGateRef.current) &&
2905
+ !options?.ignoreActiveRecorder
2906
+ ) {
2903
2907
  console.warn(
2904
2908
  "[clips-popover] handleStartRecording ignored — recorder already active",
2905
2909
  );
@@ -2941,21 +2945,54 @@ export function App() {
2941
2945
  });
2942
2946
 
2943
2947
  if (mode !== "camera" && nativeFullscreenRecordingActive) {
2948
+ // Latch re-entry protection before these awaits, not after — both the
2949
+ // permission prompt and the monitor picker below can take a while (the
2950
+ // picker waits on the user), and without this a double-click while
2951
+ // either is pending passes the top-of-function guard and starts a
2952
+ // second, competing recording-start flow. The real flow-active latch
2953
+ // further below hasn't run yet at this point, so reset this on every
2954
+ // early return in this block.
2955
+ recordingFlowGateRef.current = true;
2944
2956
  try {
2945
2957
  const granted = await invoke<boolean>(
2946
2958
  "request_macos_screen_recording_access",
2947
2959
  );
2948
2960
  if (!granted) {
2961
+ recordingFlowGateRef.current = false;
2949
2962
  setReadinessOpen(true);
2950
2963
  setRecError(MACOS_SCREEN_PERMISSION_MESSAGE);
2951
2964
  openPrivacySettings("screen");
2952
2965
  return null;
2953
2966
  }
2954
2967
  } catch (err) {
2968
+ recordingFlowGateRef.current = false;
2955
2969
  setReadinessOpen(true);
2956
2970
  setRecError(err instanceof Error ? err.message : String(err));
2957
2971
  return null;
2958
2972
  }
2973
+ // A restart hands off the already-live display stream from the take
2974
+ // it's replacing (see `discardForRestart`/`preAcquiredDisplayStream`
2975
+ // below) — it must keep recording the same screen, not re-prompt.
2976
+ if (source === "full-screen" && !options?.resumeCapture) {
2977
+ try {
2978
+ // Must resolve before `recordingFlowActive` flips the toolbar on
2979
+ // below — the toolbar reads the pick to place itself on the
2980
+ // chosen screen the first time it's shown.
2981
+ await pickFullscreenRecordingDisplay();
2982
+ } catch (err) {
2983
+ recordingFlowGateRef.current = false;
2984
+ if (err instanceof Error && err.name === "AbortError") {
2985
+ // User cancelled the screen picker (Escape) — abort silently,
2986
+ // same as dismissing the native macOS screen picker.
2987
+ return null;
2988
+ }
2989
+ // A real failure (picker window construction, persisting the
2990
+ // pick, etc.) — surface it instead of silently aborting like a
2991
+ // cancel, or the user has no idea why nothing happened.
2992
+ setRecError(err instanceof Error ? err.message : String(err));
2993
+ return null;
2994
+ }
2995
+ }
2959
2996
  }
2960
2997
 
2961
2998
  stopAllMicMeters();
@@ -1,8 +1,10 @@
1
+ import { IconVinyl } from "@tabler/icons-react";
1
2
  import { useMemo } from "react";
2
3
 
3
4
  import { useMicMeter } from "../hooks/useMicMeter";
4
5
  import { CameraIcon, CheckIcon, ChevronDown, MicIcon } from "./Icons";
5
6
  import { Switch } from "./Switch";
7
+ import { Tooltip, TooltipContent, TooltipTrigger } from "./Tooltip";
6
8
  import { useRowMenu } from "./useRowMenu";
7
9
 
8
10
  function Toggle({
@@ -112,109 +114,71 @@ export function MediaDeviceRow({
112
114
  kind === "camera" ? "Refresh cameras" : "Refresh microphones";
113
115
 
114
116
  return (
115
- <div className="media-device-row" ref={rowRef}>
116
- <div className={`row ${on ? "row-on" : "row-off"}`}>
117
- <span className="row-icon">
118
- <Icon />
119
- </span>
120
- <button
121
- type="button"
122
- className="row-button"
123
- onClick={() => setOpen((v) => !v)}
124
- disabled={disabled}
125
- title={label}
126
- >
127
- <span className="row-label">{label}</span>
128
- {kind === "mic" && on ? (
129
- <MicWave deviceId={selectedId} active={on && meterActive} />
130
- ) : (
131
- <span className="row-flex" aria-hidden />
132
- )}
133
- </button>
134
- {canOpenMenu ? (
117
+ <div className="media-device-row">
118
+ <div className="media-device-picker" ref={rowRef}>
119
+ <div className={`row ${on ? "row-on" : "row-off"}`}>
120
+ <span className="row-icon">
121
+ <Icon />
122
+ </span>
135
123
  <button
136
124
  type="button"
137
- className="row-menu-trigger"
138
- onClick={() => setOpen((value) => !value)}
139
- aria-label={on ? `Choose ${kind}` : "System audio settings"}
140
- aria-expanded={open}
141
- aria-haspopup="menu"
125
+ className="row-button"
126
+ onClick={() => setOpen((v) => !v)}
127
+ disabled={disabled}
128
+ title={label}
142
129
  >
143
- <ChevronDown />
130
+ <span className="row-label">{label}</span>
131
+ {kind === "mic" && on ? (
132
+ <MicWave deviceId={selectedId} active={on && meterActive} />
133
+ ) : (
134
+ <span className="row-flex" aria-hidden />
135
+ )}
144
136
  </button>
145
- ) : (
146
- <span className="row-chev" aria-hidden>
147
- <ChevronDown />
148
- </span>
149
- )}
150
- <Toggle
151
- on={on}
152
- onChange={(v) => {
153
- if (!v) setOpen(false);
154
- onToggle(v);
155
- }}
156
- label={kind === "camera" ? "Camera" : "Microphone"}
157
- />
158
- </div>
159
- {open && canOpenMenu ? (
160
- <div className="row-menu" role="menu">
161
- {on ? (
162
- <>
163
- <button
164
- type="button"
165
- className={`row-menu-item ${!selectedId ? "selected" : ""}`}
166
- role="menuitemradio"
167
- aria-checked={!selectedId}
168
- onClick={() => {
169
- onSelect("", "");
170
- setOpen(false);
171
- }}
172
- >
173
- <span className="row-menu-check" aria-hidden>
174
- {!selectedId ? <CheckIcon /> : null}
175
- </span>
176
- <span className="row-menu-label">{defaultLabel}</span>
177
- </button>
178
- {devices.length === 0 ? (
137
+ {canOpenMenu ? (
138
+ <button
139
+ type="button"
140
+ className="row-menu-trigger"
141
+ onClick={() => setOpen((value) => !value)}
142
+ aria-label={on ? `Choose ${kind}` : "System audio settings"}
143
+ aria-expanded={open}
144
+ aria-haspopup="menu"
145
+ >
146
+ <ChevronDown />
147
+ </button>
148
+ ) : (
149
+ <span className="row-chev" aria-hidden>
150
+ <ChevronDown />
151
+ </span>
152
+ )}
153
+ <Toggle
154
+ on={on}
155
+ onChange={(v) => {
156
+ if (!v) setOpen(false);
157
+ onToggle(v);
158
+ }}
159
+ label={kind === "camera" ? "Camera" : "Microphone"}
160
+ />
161
+ </div>
162
+ {open && canOpenMenu ? (
163
+ <div className="row-menu" role="menu">
164
+ {on ? (
165
+ <>
179
166
  <button
180
167
  type="button"
181
- className="row-menu-item row-menu-action"
182
- role="menuitem"
168
+ className={`row-menu-item ${!selectedId ? "selected" : ""}`}
169
+ role="menuitemradio"
170
+ aria-checked={!selectedId}
183
171
  onClick={() => {
184
- onRefresh();
172
+ onSelect("", "");
185
173
  setOpen(false);
186
174
  }}
187
175
  >
188
- <span className="row-menu-check" aria-hidden />
189
- <span className="row-menu-label">{accessLabel}</span>
176
+ <span className="row-menu-check" aria-hidden>
177
+ {!selectedId ? <CheckIcon /> : null}
178
+ </span>
179
+ <span className="row-menu-label">{defaultLabel}</span>
190
180
  </button>
191
- ) : (
192
- <>
193
- {devices.map((d) => {
194
- const isSelected =
195
- !!selectedId && d.deviceId === selectedId;
196
- return (
197
- <button
198
- key={d.deviceId}
199
- type="button"
200
- className={`row-menu-item ${isSelected ? "selected" : ""}`}
201
- role="menuitemradio"
202
- aria-checked={isSelected}
203
- onClick={() => {
204
- onSelect(d.deviceId, d.label);
205
- setOpen(false);
206
- }}
207
- >
208
- <span className="row-menu-check" aria-hidden>
209
- {isSelected ? <CheckIcon /> : null}
210
- </span>
211
- <span className="row-menu-label">
212
- {d.label ||
213
- (kind === "camera" ? "Camera" : "Microphone")}
214
- </span>
215
- </button>
216
- );
217
- })}
181
+ {devices.length === 0 ? (
218
182
  <button
219
183
  type="button"
220
184
  className="row-menu-item row-menu-action"
@@ -225,23 +189,78 @@ export function MediaDeviceRow({
225
189
  }}
226
190
  >
227
191
  <span className="row-menu-check" aria-hidden />
228
- <span className="row-menu-label">{refreshLabel}</span>
192
+ <span className="row-menu-label">{accessLabel}</span>
229
193
  </button>
230
- </>
231
- )}
232
- </>
233
- ) : null}
234
- {kind === "mic" && onSystemAudioToggle ? (
235
- <div className="row-menu-toggle">
236
- <span className="row-menu-toggle-label">Record System audio</span>
237
- <Switch
194
+ ) : (
195
+ <>
196
+ {devices.map((d) => {
197
+ const isSelected =
198
+ !!selectedId && d.deviceId === selectedId;
199
+ return (
200
+ <button
201
+ key={d.deviceId}
202
+ type="button"
203
+ className={`row-menu-item ${isSelected ? "selected" : ""}`}
204
+ role="menuitemradio"
205
+ aria-checked={isSelected}
206
+ onClick={() => {
207
+ onSelect(d.deviceId, d.label);
208
+ setOpen(false);
209
+ }}
210
+ >
211
+ <span className="row-menu-check" aria-hidden>
212
+ {isSelected ? <CheckIcon /> : null}
213
+ </span>
214
+ <span className="row-menu-label">
215
+ {d.label ||
216
+ (kind === "camera" ? "Camera" : "Microphone")}
217
+ </span>
218
+ </button>
219
+ );
220
+ })}
221
+ <button
222
+ type="button"
223
+ className="row-menu-item row-menu-action"
224
+ role="menuitem"
225
+ onClick={() => {
226
+ onRefresh();
227
+ setOpen(false);
228
+ }}
229
+ >
230
+ <span className="row-menu-check" aria-hidden />
231
+ <span className="row-menu-label">{refreshLabel}</span>
232
+ </button>
233
+ </>
234
+ )}
235
+ </>
236
+ ) : null}
237
+ </div>
238
+ ) : null}
239
+ </div>
240
+ {kind === "mic" && onSystemAudioToggle ? (
241
+ <Tooltip>
242
+ <TooltipTrigger asChild>
243
+ <div
244
+ className={`row system-audio-option ${systemAudio ? "row-on" : "row-off"}`}
245
+ >
246
+ <span className="row-icon">
247
+ <IconVinyl size={18} stroke={1.75} />
248
+ </span>
249
+ <span className="system-audio-option-label">
250
+ Record System Audio
251
+ </span>
252
+ <span className="row-flex" aria-hidden />
253
+ <Toggle
238
254
  on={!!systemAudio}
239
255
  onChange={onSystemAudioToggle}
240
- label="Record system audio"
256
+ label="Record System Audio"
241
257
  />
242
258
  </div>
243
- ) : null}
244
- </div>
259
+ </TooltipTrigger>
260
+ <TooltipContent side="bottom">
261
+ Record audio from other applications
262
+ </TooltipContent>
263
+ </Tooltip>
245
264
  ) : null}
246
265
  </div>
247
266
  );
@@ -2111,6 +2111,155 @@ async function selectRegionForRecording(): Promise<RegionCaptureRect> {
2111
2111
  }
2112
2112
  }
2113
2113
 
2114
+ class MonitorPickerCancelledError extends Error {
2115
+ constructor() {
2116
+ super("Full-screen monitor selection cancelled");
2117
+ this.name = "AbortError";
2118
+ }
2119
+ }
2120
+
2121
+ /**
2122
+ * Safety net only — a normal pick takes as long as the user needs. This
2123
+ * guards against the picker windows being torn down by something other than
2124
+ * a click or Escape (a monitor disconnecting mid-pick, some other teardown)
2125
+ * with neither selection event ever firing, which would otherwise hang the
2126
+ * recording-start flow forever. Matches the "abandon after a while" window
2127
+ * used elsewhere in this file (see `CUE_IDLE_CLEANUP_MS` in audio-cue.ts).
2128
+ */
2129
+ const MONITOR_PICKER_SELECTION_TIMEOUT_MS = 5 * 60_000;
2130
+
2131
+ function waitForMonitorPickerSelection(): {
2132
+ promise: Promise<number | null>;
2133
+ cleanup: () => void;
2134
+ } {
2135
+ let settled = false;
2136
+ const unlistens: UnlistenFn[] = [];
2137
+ let timer: ReturnType<typeof setTimeout> | null = null;
2138
+
2139
+ const cleanup = () => {
2140
+ settled = true;
2141
+ if (timer) {
2142
+ clearTimeout(timer);
2143
+ timer = null;
2144
+ }
2145
+ for (const unlisten of unlistens.splice(0)) {
2146
+ try {
2147
+ unlisten();
2148
+ } catch {
2149
+ // ignore
2150
+ }
2151
+ }
2152
+ };
2153
+
2154
+ const promise = new Promise<number | null>((resolve, reject) => {
2155
+ const finish = (cancelled: boolean, displayId: number | null) => {
2156
+ if (settled) return;
2157
+ settled = true;
2158
+ cleanup();
2159
+ if (cancelled) reject(new MonitorPickerCancelledError());
2160
+ else resolve(displayId);
2161
+ };
2162
+
2163
+ timer = setTimeout(() => {
2164
+ finish(true, null);
2165
+ }, MONITOR_PICKER_SELECTION_TIMEOUT_MS);
2166
+
2167
+ const track = (listener: Promise<UnlistenFn>) => {
2168
+ listener
2169
+ .then((unlisten) => {
2170
+ if (settled) {
2171
+ try {
2172
+ unlisten();
2173
+ } catch {
2174
+ // ignore
2175
+ }
2176
+ return;
2177
+ }
2178
+ unlistens.push(unlisten);
2179
+ })
2180
+ .catch((err) => {
2181
+ if (settled) return;
2182
+ settled = true;
2183
+ cleanup();
2184
+ reject(err);
2185
+ });
2186
+ };
2187
+
2188
+ track(
2189
+ listen<{ displayId?: number | null }>(
2190
+ "clips:monitor-picker-selected",
2191
+ (event) => {
2192
+ const raw = event.payload?.displayId;
2193
+ const displayId =
2194
+ typeof raw === "number" && Number.isFinite(raw) && raw > 0
2195
+ ? raw
2196
+ : null;
2197
+ finish(false, displayId);
2198
+ },
2199
+ ),
2200
+ );
2201
+ track(
2202
+ listen("clips:monitor-picker-cancelled", () => {
2203
+ finish(true, null);
2204
+ }),
2205
+ );
2206
+ });
2207
+
2208
+ return { promise, cleanup };
2209
+ }
2210
+
2211
+ /**
2212
+ * When more than one monitor is connected, shows a small "record this
2213
+ * screen" popup centered on each one and waits for the user to pick one —
2214
+ * or cancel with Escape, which aborts the whole recording start. Resolves
2215
+ * immediately with no popup when there's only one monitor, matching the
2216
+ * prior single-monitor behavior.
2217
+ *
2218
+ * Callers must await this BEFORE flipping any "recording is starting" UI
2219
+ * state (toolbar, timer, etc.) — that chrome positions itself on whatever
2220
+ * `tray_monitor_physical_rect` returns at the moment it's shown, which
2221
+ * reads this pick. Called from `startNativeFullscreenRecording` instead,
2222
+ * the toolbar was already up (and pinned to the wrong screen) by the time
2223
+ * the user finished picking.
2224
+ */
2225
+ export async function pickFullscreenRecordingDisplay(): Promise<void> {
2226
+ // Registered BEFORE showing the picker windows: a fast click (or keyboard
2227
+ // activation) can otherwise fire the selection event before these
2228
+ // listeners exist, and Tauri does not replay events to a listener
2229
+ // registered after the fact — the wait would then sit until its timeout.
2230
+ const selection = waitForMonitorPickerSelection();
2231
+ let shown: boolean;
2232
+ try {
2233
+ shown = await invoke<boolean>("show_monitor_picker");
2234
+ } catch (err) {
2235
+ selection.cleanup();
2236
+ throw err;
2237
+ }
2238
+ if (!shown) {
2239
+ selection.cleanup();
2240
+ return;
2241
+ }
2242
+ let displayId: number | null = null;
2243
+ try {
2244
+ displayId = await selection.promise;
2245
+ } catch (err) {
2246
+ selection.cleanup();
2247
+ await invoke("close_monitor_picker").catch(() => {});
2248
+ throw err;
2249
+ }
2250
+ try {
2251
+ // Deliberately not swallowed: if this fails, `tray_display_id` falls
2252
+ // back to the tray-anchor screen with no way for the caller to tell
2253
+ // that happened, silently recording a different screen than the one
2254
+ // the user just picked. Propagate so the caller aborts the start
2255
+ // instead.
2256
+ await invoke("set_recording_display_override", { displayId });
2257
+ } finally {
2258
+ selection.cleanup();
2259
+ await invoke("close_monitor_picker").catch(() => {});
2260
+ }
2261
+ }
2262
+
2114
2263
  async function prepareCountdownEventWaiter(
2115
2264
  timeoutMs = 4000,
2116
2265
  signal?: AbortSignal,
@@ -3063,6 +3212,9 @@ async function startNativeFullscreenRecording(
3063
3212
  // down on stop, cancel, and the error paths below.
3064
3213
  await showRegionRecordBorder(captureRegion);
3065
3214
  }
3215
+ // Full-screen's monitor pick already happened in the caller, before
3216
+ // `recordingFlowActive`/the toolbar went up — see
3217
+ // `pickFullscreenRecordingDisplay`'s doc comment for why.
3066
3218
 
3067
3219
  if (localOnly && localRecordingMode === "separate" && wantsCamera) {
3068
3220
  localCameraStream =
@@ -3443,7 +3595,16 @@ async function startNativeFullscreenRecording(
3443
3595
  );
3444
3596
  });
3445
3597
  await localCameraExport?.cancel().catch(() => {});
3446
- await invoke("native_fullscreen_recording_cancel").catch((err) =>
3598
+ // A restart discards this take and immediately starts a replacement on
3599
+ // the SAME screen without re-running the monitor picker (native restarts
3600
+ // hand off the live capture, not a browser display stream — see
3601
+ // `pickFullscreenRecordingDisplay`'s skip condition in app.tsx). Without
3602
+ // `preserveDisplayOverride`, these calls would clear the pick before the
3603
+ // replacement recording ever reads it, silently falling back to the
3604
+ // tray-anchor screen.
3605
+ await invoke("native_fullscreen_recording_cancel", {
3606
+ preserveDisplayOverride: forRestart,
3607
+ }).catch((err) =>
3447
3608
  console.warn("[clips-recorder] native fullscreen cancel failed:", err),
3448
3609
  );
3449
3610
  if (bubbleCaptureExcluded) {
@@ -3457,7 +3618,9 @@ async function startNativeFullscreenRecording(
3457
3618
  }
3458
3619
  streamCleanups.forEach((cleanup) => cleanup());
3459
3620
  if (forRestart) {
3460
- await invoke("hide_recording_chrome").catch(() => {});
3621
+ await invoke("hide_recording_chrome", {
3622
+ preserveDisplayOverride: true,
3623
+ }).catch(() => {});
3461
3624
  } else {
3462
3625
  await endSession();
3463
3626
  }
@@ -10,6 +10,7 @@ import { Finalizing } from "./overlays/finalizing";
10
10
  import { FlowBar } from "./overlays/flow-bar";
11
11
  import { MeetingNotification } from "./overlays/meeting-notification";
12
12
  import { MeetingNub } from "./overlays/meeting-nub";
13
+ import { MonitorPicker } from "./overlays/monitor-picker";
13
14
  import { Onboarding } from "./overlays/onboarding";
14
15
  import { Preparing } from "./overlays/preparing";
15
16
  import { RecordingPill } from "./overlays/recording-pill";
@@ -64,6 +65,8 @@ function pickRoute(route: string): React.ReactElement {
64
65
  return <RegionGuideEditor mode="capture" />;
65
66
  case "region-record-border":
66
67
  return <RegionRecordBorder />;
68
+ case "monitor-picker":
69
+ return <MonitorPicker />;
67
70
  default:
68
71
  return <App />;
69
72
  }