@agent-native/core 0.145.1 → 0.145.2
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/brain/server/lib/capture-sanitization.ts +6 -2
- package/corpus/templates/clips/changelog/2026-08-07-full-screen-recordings-on-a-multi-monitor-mac-now-let-you-pi.md +6 -0
- package/corpus/templates/clips/chrome-extension/src/styles.css +13 -4
- package/corpus/templates/clips/desktop/src/app.tsx +38 -1
- package/corpus/templates/clips/desktop/src/components/MediaDeviceRow.tsx +123 -104
- package/corpus/templates/clips/desktop/src/lib/recorder.ts +165 -2
- package/corpus/templates/clips/desktop/src/main.tsx +3 -0
- package/corpus/templates/clips/desktop/src/overlays/monitor-picker.tsx +70 -0
- package/corpus/templates/clips/desktop/src/styles.css +70 -11
- package/corpus/templates/clips/desktop/src-tauri/capabilities/default.json +2 -1
- package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +163 -1
- package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +6 -1
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +141 -22
- package/corpus/templates/clips/desktop/src-tauri/src/state.rs +27 -1
- package/corpus/templates/clips/desktop/src-tauri/src/util.rs +12 -1
- package/dist/agent/engine/index.d.ts +1 -1
- package/dist/agent/engine/index.js +1 -1
- package/dist/agent/engine/registry.d.ts +15 -0
- package/dist/agent/engine/registry.js +39 -10
- package/dist/agent/production-agent.d.ts +40 -1
- package/dist/agent/production-agent.js +81 -40
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/integrations/webhook-handler.d.ts +3 -2
- package/dist/integrations/webhook-handler.js +8 -38
- package/dist/notifications/routes.d.ts +2 -2
- package/dist/observability/routes.d.ts +3 -3
- package/dist/progress/routes.d.ts +1 -1
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/server/agent-chat-plugin.js +67 -33
- package/dist/server/complete-text.js +14 -7
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/request-context.d.ts +7 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/docs/content/environment-variables.mdx +6 -0
- package/package.json +3 -3
package/corpus/README.md
CHANGED
|
@@ -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.
|
|
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
|
|
450
|
+
apiKey: userApiKey.apiKey,
|
|
451
|
+
apiKeyEnvVar: userApiKey.apiKeyEnvVar,
|
|
448
452
|
appId: "brain",
|
|
449
453
|
engineOption: engineName,
|
|
450
454
|
});
|
|
@@ -861,7 +861,7 @@ h1 {
|
|
|
861
861
|
|
|
862
862
|
.field-label {
|
|
863
863
|
color: var(--foreground);
|
|
864
|
-
font-size:
|
|
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:
|
|
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:
|
|
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:
|
|
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 (
|
|
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"
|
|
116
|
-
<div className=
|
|
117
|
-
<
|
|
118
|
-
<
|
|
119
|
-
|
|
120
|
-
|
|
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-
|
|
138
|
-
onClick={() => setOpen((
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
aria-haspopup="menu"
|
|
125
|
+
className="row-button"
|
|
126
|
+
onClick={() => setOpen((v) => !v)}
|
|
127
|
+
disabled={disabled}
|
|
128
|
+
title={label}
|
|
142
129
|
>
|
|
143
|
-
<
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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=
|
|
182
|
-
role="
|
|
168
|
+
className={`row-menu-item ${!selectedId ? "selected" : ""}`}
|
|
169
|
+
role="menuitemradio"
|
|
170
|
+
aria-checked={!selectedId}
|
|
183
171
|
onClick={() => {
|
|
184
|
-
|
|
172
|
+
onSelect("", "");
|
|
185
173
|
setOpen(false);
|
|
186
174
|
}}
|
|
187
175
|
>
|
|
188
|
-
<span className="row-menu-check" aria-hidden
|
|
189
|
-
|
|
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">{
|
|
192
|
+
<span className="row-menu-label">{accessLabel}</span>
|
|
229
193
|
</button>
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
|
256
|
+
label="Record System Audio"
|
|
241
257
|
/>
|
|
242
258
|
</div>
|
|
243
|
-
|
|
244
|
-
|
|
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
|
-
|
|
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"
|
|
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
|
}
|