@agent-native/core 0.114.6 → 0.114.7
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 +2 -2
- package/corpus/core/CHANGELOG.md +6 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/cli/mcp-config-writers.ts +6 -4
- package/corpus/core/src/cli/mcp.ts +124 -0
- package/corpus/core/src/cli/skills-content/help.ts +4 -3
- package/corpus/core/src/cli/skills-content/index.ts +1 -0
- package/corpus/core/src/cli/skills-content/rewind-skill.ts +52 -0
- package/corpus/core/src/cli/skills.ts +115 -13
- package/corpus/core/src/mcp/screen-memory-stdio.ts +1360 -104
- package/corpus/core/src/mcp-client/index.ts +9 -0
- package/corpus/core/src/mcp-client/screen-memory-local.ts +847 -13
- package/corpus/templates/clips/actions/apply-rewind-extension.ts +195 -0
- package/corpus/templates/clips/actions/delete-agent-recording-if-unpromoted.ts +64 -0
- package/corpus/templates/clips/actions/get-rewind-extension-request.ts +22 -0
- package/corpus/templates/clips/actions/get-screen-memory-status.ts +18 -2
- package/corpus/templates/clips/actions/list-rewind-extension-requests.ts +52 -0
- package/corpus/templates/clips/actions/make-recording-private-for-rewind.ts +55 -0
- package/corpus/templates/clips/actions/query-screen-memory-context.ts +4 -4
- package/corpus/templates/clips/actions/request-rewind-extension.ts +81 -0
- package/corpus/templates/clips/actions/update-rewind-extension-request.ts +47 -0
- package/corpus/templates/clips/app/components/editor/editor-layout.tsx +25 -0
- package/corpus/templates/clips/app/components/editor/editor-toolbar.tsx +36 -0
- package/corpus/templates/clips/app/components/editor/rewind-extension-dialog.tsx +338 -0
- package/corpus/templates/clips/app/components/editor/stitch-manager.tsx +1 -16
- package/corpus/templates/clips/app/components/editor/timeline.tsx +26 -0
- package/corpus/templates/clips/app/i18n/en-US.ts +18 -0
- package/corpus/templates/clips/app/lib/ffmpeg-export.ts +19 -12
- package/corpus/templates/clips/app/lib/rewind-visibility.ts +31 -0
- package/corpus/templates/clips/app/lib/timestamp-mapping.ts +7 -0
- package/corpus/templates/clips/app/lib/upload-file-client.ts +17 -0
- package/corpus/templates/clips/changelog/2026-07-14-rewind-keeps-recent-screen-context-local-and-turns-it-into-clips.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-17-active-clips-keep-rewind-stable-and-settings-clear.md +12 -0
- package/corpus/templates/clips/changelog/2026-07-17-clicking-the-clips-icon-during-a-recording-now-opens-the-app.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-17-default-mic-now-uses-the-mac-default-and-transcription-shares-one-capture.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-17-full-screen-clips-finalize-even-when-screencapturekit-stop-hangs.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-17-normal-full-screen-clips-no-longer-stop-immediately-after-th.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-17-rewind-can-hand-one-bounded-private-clip-to-your-agent.md +12 -0
- package/corpus/templates/clips/changelog/2026-07-17-rewind-home-can-copy-a-local-agent-prompt.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-17-rewind-now-has-a-calm-home-status-focused-first-run-consent-.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-17-rewind-settings-are-smaller-clearer-and-agent-first.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-17-the-full-clips-interface-stays-available-while-recording.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-18-rewind-organizes-recent-work-into-local-searchable-chapters.md +8 -0
- package/corpus/templates/clips/changelog/2026-07-18-searchable-rewind-chapters.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-19-rewind-backed-recordings-now-start-immediately-after-the-cou.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-20-rewind-can-set-up-your-local-agent-once-so-future-requests-w.md +6 -0
- package/corpus/templates/clips/desktop/package.json +1 -0
- package/corpus/templates/clips/desktop/scripts/sign-macos-local.ts +12 -1
- package/corpus/templates/clips/desktop/src/app.tsx +2498 -239
- package/corpus/templates/clips/desktop/src/components/ReadinessPanel.tsx +21 -1
- package/corpus/templates/clips/desktop/src/components/Switch.tsx +3 -0
- package/corpus/templates/clips/desktop/src/hooks/useMeetingTranscription.ts +156 -24
- package/corpus/templates/clips/desktop/src/lib/native-recording-warm.ts +6 -7
- package/corpus/templates/clips/desktop/src/lib/recorder.ts +1763 -1160
- package/corpus/templates/clips/desktop/src/lib/rewind-agent-prompt.ts +9 -0
- package/corpus/templates/clips/desktop/src/lib/rewind-recording-start.ts +20 -0
- package/corpus/templates/clips/desktop/src/main.tsx +3 -0
- package/corpus/templates/clips/desktop/src/overlays/countdown.tsx +14 -14
- package/corpus/templates/clips/desktop/src/overlays/onboarding.tsx +4 -4
- package/corpus/templates/clips/desktop/src/overlays/preparing.tsx +13 -0
- package/corpus/templates/clips/desktop/src/overlays/toolbar.tsx +27 -2
- package/corpus/templates/clips/desktop/src/shared/config.ts +16 -0
- package/corpus/templates/clips/desktop/src/styles.css +956 -134
- package/corpus/templates/clips/desktop/src-tauri/Cargo.toml +5 -0
- package/corpus/templates/clips/desktop/src-tauri/build.rs +69 -1
- package/corpus/templates/clips/desktop/src-tauri/src/accessibility.rs +386 -12
- package/corpus/templates/clips/desktop/src-tauri/src/capture_audio_bus.rs +382 -0
- package/corpus/templates/clips/desktop/src-tauri/src/capture_graph.rs +777 -0
- package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +249 -22
- package/corpus/templates/clips/desktop/src-tauri/src/config.rs +148 -1
- package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +58 -4
- package/corpus/templates/clips/desktop/src-tauri/src/logfile.rs +13 -1
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen/custom_capture.rs +1505 -51
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen/live_upload.rs +124 -0
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +1006 -95
- package/corpus/templates/clips/desktop/src-tauri/src/rewind_capture_suspension.rs +165 -0
- package/corpus/templates/clips/desktop/src-tauri/src/rewind_chapters.rs +2434 -0
- package/corpus/templates/clips/desktop/src-tauri/src/rewind_clip.rs +1650 -0
- package/corpus/templates/clips/desktop/src-tauri/src/rewind_egress.rs +643 -0
- package/corpus/templates/clips/desktop/src-tauri/src/rewind_local_ask.rs +803 -0
- package/corpus/templates/clips/desktop/src-tauri/src/rewind_meeting_history.rs +595 -0
- package/corpus/templates/clips/desktop/src-tauri/src/screen_memory.rs +2900 -91
- package/corpus/templates/clips/desktop/src-tauri/src/screen_memory_ocr.rs +360 -0
- package/corpus/templates/clips/desktop/src-tauri/src/screen_memory_transcript.rs +408 -0
- package/corpus/templates/clips/desktop/src-tauri/src/shortcuts.rs +114 -33
- package/corpus/templates/clips/desktop/src-tauri/src/state.rs +2 -2
- package/corpus/templates/clips/desktop/src-tauri/src/system_audio.rs +13 -3
- package/corpus/templates/clips/desktop/src-tauri/src/tray.rs +4 -1
- package/corpus/templates/clips/desktop/src-tauri/src/util.rs +7 -13
- package/corpus/templates/clips/desktop/src-tauri/src/whisper_speech.rs +198 -11
- package/corpus/templates/clips/desktop/src-tauri/tauri.alpha.conf.json +17 -0
- package/dist/cli/mcp-config-writers.d.ts +2 -2
- package/dist/cli/mcp-config-writers.d.ts.map +1 -1
- package/dist/cli/mcp-config-writers.js +6 -6
- package/dist/cli/mcp-config-writers.js.map +1 -1
- package/dist/cli/mcp.d.ts +14 -0
- package/dist/cli/mcp.d.ts.map +1 -1
- package/dist/cli/mcp.js +79 -0
- package/dist/cli/mcp.js.map +1 -1
- package/dist/cli/skills-content/help.d.ts +1 -1
- package/dist/cli/skills-content/help.d.ts.map +1 -1
- package/dist/cli/skills-content/help.js +4 -3
- package/dist/cli/skills-content/help.js.map +1 -1
- package/dist/cli/skills-content/index.d.ts +1 -0
- package/dist/cli/skills-content/index.d.ts.map +1 -1
- package/dist/cli/skills-content/index.js +1 -0
- package/dist/cli/skills-content/index.js.map +1 -1
- package/dist/cli/skills-content/rewind-skill.d.ts +2 -0
- package/dist/cli/skills-content/rewind-skill.d.ts.map +1 -0
- package/dist/cli/skills-content/rewind-skill.js +53 -0
- package/dist/cli/skills-content/rewind-skill.js.map +1 -0
- package/dist/cli/skills.d.ts +6 -0
- package/dist/cli/skills.d.ts.map +1 -1
- package/dist/cli/skills.js +90 -14
- package/dist/cli/skills.js.map +1 -1
- package/dist/mcp/screen-memory-stdio.d.ts +365 -0
- package/dist/mcp/screen-memory-stdio.d.ts.map +1 -1
- package/dist/mcp/screen-memory-stdio.js +1014 -91
- package/dist/mcp/screen-memory-stdio.js.map +1 -1
- package/dist/mcp-client/index.d.ts +1 -1
- package/dist/mcp-client/index.d.ts.map +1 -1
- package/dist/mcp-client/index.js +1 -1
- package/dist/mcp-client/index.js.map +1 -1
- package/dist/mcp-client/screen-memory-local.d.ts +90 -0
- package/dist/mcp-client/screen-memory-local.d.ts.map +1 -1
- package/dist/mcp-client/screen-memory-local.js +590 -12
- package/dist/mcp-client/screen-memory-local.js.map +1 -1
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +1 -1
- package/dist/provider-api/actions/provider-api.d.ts +4 -4
- package/dist/resources/handlers.d.ts +1 -1
- package/package.json +1 -1
- package/src/cli/mcp-config-writers.ts +6 -4
- package/src/cli/mcp.ts +124 -0
- package/src/cli/skills-content/help.ts +4 -3
- package/src/cli/skills-content/index.ts +1 -0
- package/src/cli/skills-content/rewind-skill.ts +52 -0
- package/src/cli/skills.ts +115 -13
- package/src/mcp/screen-memory-stdio.ts +1360 -104
- package/src/mcp-client/index.ts +9 -0
- package/src/mcp-client/screen-memory-local.ts +847 -13
|
@@ -3,13 +3,18 @@ import {
|
|
|
3
3
|
IconArrowLeft,
|
|
4
4
|
IconCalendarEvent,
|
|
5
5
|
IconCircleCheck,
|
|
6
|
+
IconCopy,
|
|
6
7
|
IconDownload,
|
|
7
8
|
IconExternalLink,
|
|
8
9
|
IconFolderOpen,
|
|
9
10
|
IconPencil,
|
|
10
11
|
IconInfoCircle,
|
|
12
|
+
IconHistory,
|
|
11
13
|
IconMicrophone2,
|
|
14
|
+
IconPlayerPlay,
|
|
12
15
|
IconRefresh,
|
|
16
|
+
IconSearch,
|
|
17
|
+
IconShieldLock,
|
|
13
18
|
IconTrash,
|
|
14
19
|
IconUpload,
|
|
15
20
|
IconX,
|
|
@@ -67,7 +72,9 @@ import {
|
|
|
67
72
|
import { isMacPlatform, isWindowsPlatform } from "./lib/platform";
|
|
68
73
|
import {
|
|
69
74
|
dismissBrowserRecordingBackup,
|
|
75
|
+
createPrivateAgentRewindRecording,
|
|
70
76
|
exportBrowserRecordingBackup,
|
|
77
|
+
getRewindClipOrigin,
|
|
71
78
|
listBrowserRecordingBackups,
|
|
72
79
|
retryBrowserRecordingBackup,
|
|
73
80
|
scheduleNativeBackupCleanupAfterProcessing,
|
|
@@ -78,6 +85,7 @@ import {
|
|
|
78
85
|
type RecorderHandle,
|
|
79
86
|
type RecorderStopResult,
|
|
80
87
|
} from "./lib/recorder";
|
|
88
|
+
import { REWIND_AGENT_PROMPT } from "./lib/rewind-agent-prompt";
|
|
81
89
|
import {
|
|
82
90
|
loadBool,
|
|
83
91
|
loadString,
|
|
@@ -102,6 +110,7 @@ import {
|
|
|
102
110
|
} from "./lib/voice-dictation";
|
|
103
111
|
import {
|
|
104
112
|
useFeatureConfig,
|
|
113
|
+
type FeatureConfig,
|
|
105
114
|
type LocalRecordingMode,
|
|
106
115
|
type ScreenMemoryStatus,
|
|
107
116
|
} from "./shared/config";
|
|
@@ -126,7 +135,13 @@ interface PendingNativeUpload {
|
|
|
126
135
|
|
|
127
136
|
type PendingDesktopUpload = PendingNativeUpload | PendingBrowserRecordingUpload;
|
|
128
137
|
|
|
129
|
-
type PopoverView =
|
|
138
|
+
type PopoverView =
|
|
139
|
+
| "recorder"
|
|
140
|
+
| "memory"
|
|
141
|
+
| "rewind-settings"
|
|
142
|
+
| "settings"
|
|
143
|
+
| "meetings"
|
|
144
|
+
| "dictation";
|
|
130
145
|
|
|
131
146
|
interface PopoverMeeting {
|
|
132
147
|
id: string;
|
|
@@ -138,6 +153,52 @@ interface PopoverMeeting {
|
|
|
138
153
|
transcriptStatus: string | null;
|
|
139
154
|
}
|
|
140
155
|
|
|
156
|
+
interface RewindMeetingHistoryAvailability {
|
|
157
|
+
available: boolean;
|
|
158
|
+
reason?: string | null;
|
|
159
|
+
coveredFrom?: string | null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
interface RewindAgentHandoffRequest {
|
|
163
|
+
requestId: string;
|
|
164
|
+
status: "pending" | "processing" | "ready" | "declined" | "failed";
|
|
165
|
+
requestedAt: string;
|
|
166
|
+
startAt: string;
|
|
167
|
+
endAt: string;
|
|
168
|
+
durationMs: number;
|
|
169
|
+
reason: string;
|
|
170
|
+
includeMicrophone: boolean;
|
|
171
|
+
includeSystemAudio: boolean;
|
|
172
|
+
reviewRequired: boolean;
|
|
173
|
+
agentClipRetention: "forever" | "24-hours" | "7-days" | "30-days";
|
|
174
|
+
recordingId?: string;
|
|
175
|
+
agentUrl?: string;
|
|
176
|
+
contextUrl?: string;
|
|
177
|
+
expiresAt?: string;
|
|
178
|
+
error?: string;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
interface RewindExtensionRequest {
|
|
182
|
+
requestId: string;
|
|
183
|
+
recordingId: string;
|
|
184
|
+
seconds: 30 | 300;
|
|
185
|
+
status: "pending" | "processing" | "ready" | "failed";
|
|
186
|
+
updatedAt: string;
|
|
187
|
+
preRollRecordingId?: string;
|
|
188
|
+
actualDurationMs?: number;
|
|
189
|
+
error?: string;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
interface NativeRewindUploadResult {
|
|
193
|
+
recordingId: string;
|
|
194
|
+
durationMs: number;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
interface DueRewindAgentHandoff {
|
|
198
|
+
requestId: string;
|
|
199
|
+
recordingId: string;
|
|
200
|
+
}
|
|
201
|
+
|
|
141
202
|
interface LocalRecordingNotice {
|
|
142
203
|
folderPath?: string;
|
|
143
204
|
files: LocalExportedFile[];
|
|
@@ -153,6 +214,74 @@ interface ScreenMemoryExportResult {
|
|
|
153
214
|
}>;
|
|
154
215
|
}
|
|
155
216
|
|
|
217
|
+
interface RewindEgressEvent {
|
|
218
|
+
requestId: string;
|
|
219
|
+
occurredAt: string;
|
|
220
|
+
state:
|
|
221
|
+
| "prepared"
|
|
222
|
+
| "completed"
|
|
223
|
+
| "failed"
|
|
224
|
+
| "local-evidence-read"
|
|
225
|
+
| "handoff-requested";
|
|
226
|
+
evidenceCount: number;
|
|
227
|
+
packetBytes: number;
|
|
228
|
+
error?: string | null;
|
|
229
|
+
operation?: string | null;
|
|
230
|
+
receipt?: {
|
|
231
|
+
evidence?: Array<{
|
|
232
|
+
id: string;
|
|
233
|
+
momentId: string;
|
|
234
|
+
sourceType: "app-context" | "transcript" | "ocr" | "chapter";
|
|
235
|
+
capturedAt?: string | null;
|
|
236
|
+
}>;
|
|
237
|
+
frames?: Array<{ timestamp: string; segmentId: string }>;
|
|
238
|
+
mediaInterval?: { startAt: string; endAt: string };
|
|
239
|
+
reviewRequired?: boolean;
|
|
240
|
+
} | null;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
interface RewindLocalAskResult {
|
|
244
|
+
query: string;
|
|
245
|
+
answerSummary: string;
|
|
246
|
+
evidence: Array<{
|
|
247
|
+
id: string;
|
|
248
|
+
sourceType: "app-context" | "transcript" | "ocr";
|
|
249
|
+
capturedAt: string;
|
|
250
|
+
excerpt: string;
|
|
251
|
+
confidence?: number | null;
|
|
252
|
+
segmentId: string;
|
|
253
|
+
offsetMs: number;
|
|
254
|
+
}>;
|
|
255
|
+
coverage: {
|
|
256
|
+
segmentsConsidered: number;
|
|
257
|
+
transcriptIndexesReady: number;
|
|
258
|
+
ocrIndexesReady: number;
|
|
259
|
+
gaps: Array<{
|
|
260
|
+
kind: string;
|
|
261
|
+
source: string;
|
|
262
|
+
startedAt?: string | null;
|
|
263
|
+
endedAt?: string | null;
|
|
264
|
+
detail: string;
|
|
265
|
+
}>;
|
|
266
|
+
};
|
|
267
|
+
confidence: string;
|
|
268
|
+
truncated: boolean;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
interface RewindExcludedApplication {
|
|
272
|
+
bundleId: string;
|
|
273
|
+
name: string;
|
|
274
|
+
path?: string | null;
|
|
275
|
+
installed: boolean;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
interface RewindAgentConnectionStatus {
|
|
279
|
+
client: "codex" | "claude-code";
|
|
280
|
+
configured: boolean;
|
|
281
|
+
configPath: string;
|
|
282
|
+
storeDir: string;
|
|
283
|
+
}
|
|
284
|
+
|
|
156
285
|
type MeetingTranscriptionMode = "manual" | "ask" | "auto";
|
|
157
286
|
|
|
158
287
|
type CaptureMode = "screen" | "screen-camera" | "camera";
|
|
@@ -165,12 +294,35 @@ const STORAGE_SETUP_FAILURE_RE =
|
|
|
165
294
|
const DEFAULT_SCREEN_MEMORY_CONFIG = {
|
|
166
295
|
enabled: false,
|
|
167
296
|
paused: false,
|
|
168
|
-
retentionHours:
|
|
297
|
+
retentionHours: 8,
|
|
169
298
|
maxBytes: 20 * 1024 * 1024 * 1024,
|
|
170
299
|
segmentSeconds: 5 * 60,
|
|
171
300
|
sampleIntervalSeconds: 10,
|
|
301
|
+
captureMode: "visuals" as const,
|
|
302
|
+
reviewBeforeSending: true,
|
|
303
|
+
autoPreviewBeforeSending: true,
|
|
304
|
+
agentClipRetention: "forever" as const,
|
|
305
|
+
excludedBundleIds: [
|
|
306
|
+
"com.1password.1password",
|
|
307
|
+
"com.agilebits.onepassword7",
|
|
308
|
+
"com.bitwarden.desktop",
|
|
309
|
+
"com.dashlane.dashlane",
|
|
310
|
+
"com.lastpass.lastpass",
|
|
311
|
+
],
|
|
312
|
+
excludePrivateWindows: false,
|
|
172
313
|
};
|
|
173
314
|
|
|
315
|
+
function parseExcludedBundleIds(value: string): string[] {
|
|
316
|
+
return [
|
|
317
|
+
...new Set(
|
|
318
|
+
value
|
|
319
|
+
.split(/[\n,]/)
|
|
320
|
+
.map((id) => id.trim())
|
|
321
|
+
.filter(Boolean),
|
|
322
|
+
),
|
|
323
|
+
];
|
|
324
|
+
}
|
|
325
|
+
|
|
174
326
|
function isStorageSetupFailureMessage(message: string | null | undefined) {
|
|
175
327
|
return STORAGE_SETUP_FAILURE_RE.test(message ?? "");
|
|
176
328
|
}
|
|
@@ -367,17 +519,17 @@ type MacosPrivacyPane =
|
|
|
367
519
|
|
|
368
520
|
const MACOS_PRIVACY_URLS: Record<MacosPrivacyPane, string> = {
|
|
369
521
|
camera:
|
|
370
|
-
"x-apple.systempreferences:com.apple.
|
|
522
|
+
"x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Privacy_Camera",
|
|
371
523
|
microphone:
|
|
372
|
-
"x-apple.systempreferences:com.apple.
|
|
524
|
+
"x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Privacy_Microphone",
|
|
373
525
|
screen:
|
|
374
|
-
"x-apple.systempreferences:com.apple.
|
|
526
|
+
"x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Privacy_ScreenCapture",
|
|
375
527
|
speech:
|
|
376
|
-
"x-apple.systempreferences:com.apple.
|
|
528
|
+
"x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Privacy_SpeechRecognition",
|
|
377
529
|
accessibility:
|
|
378
|
-
"x-apple.systempreferences:com.apple.
|
|
530
|
+
"x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Privacy_Accessibility",
|
|
379
531
|
"input-monitoring":
|
|
380
|
-
"x-apple.systempreferences:com.apple.
|
|
532
|
+
"x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Privacy_ListenEvent",
|
|
381
533
|
};
|
|
382
534
|
|
|
383
535
|
const WINDOWS_PRIVACY_URLS: Partial<Record<MacosPrivacyPane, string>> = {
|
|
@@ -552,6 +704,17 @@ function measurePopoverHeight(el: HTMLElement): number {
|
|
|
552
704
|
|
|
553
705
|
const candidates = [rect.height, el.scrollHeight + borderY];
|
|
554
706
|
|
|
707
|
+
// When a direct child is the scrollable content pane, the shell's own
|
|
708
|
+
// scrollHeight already includes the fixed footer but excludes content that
|
|
709
|
+
// is hidden inside that child. Add only that hidden delta to the shell
|
|
710
|
+
// baseline. Measuring the child's full height alone would omit the footer
|
|
711
|
+
// and leave the resumed recorder window partially clipped.
|
|
712
|
+
const directChildOverflow = Array.from(el.children).reduce((total, child) => {
|
|
713
|
+
if (!(child instanceof HTMLElement)) return total;
|
|
714
|
+
return total + Math.max(0, child.scrollHeight - child.clientHeight);
|
|
715
|
+
}, 0);
|
|
716
|
+
candidates.push(el.scrollHeight + borderY + directChildOverflow);
|
|
717
|
+
|
|
555
718
|
// ResizeObserver on `.app` alone misses scroll-only and absolutely
|
|
556
719
|
// positioned growth. Measure descendant bounds so menus, banners, and
|
|
557
720
|
// settings sections can grow the native window even when `.app` is capped
|
|
@@ -563,6 +726,19 @@ function measurePopoverHeight(el: HTMLElement): number {
|
|
|
563
726
|
const childRect = child.getBoundingClientRect();
|
|
564
727
|
if (childRect.width === 0 && childRect.height === 0) continue;
|
|
565
728
|
lowestBottom = Math.max(lowestBottom, childRect.bottom);
|
|
729
|
+
|
|
730
|
+
// Recorder Home deliberately keeps its footer fixed and lets the content
|
|
731
|
+
// region scroll when the native window is short. A newly inserted row can
|
|
732
|
+
// therefore grow `child.scrollHeight` without changing any descendant's
|
|
733
|
+
// visible bounding box. Include that hidden overflow in the desired
|
|
734
|
+
// window height so a state transition (for example paused -> remembering)
|
|
735
|
+
// can grow the popover back to its natural size.
|
|
736
|
+
const childBorderY =
|
|
737
|
+
Number.parseFloat(childStyle.borderTopWidth || "0") +
|
|
738
|
+
Number.parseFloat(childStyle.borderBottomWidth || "0");
|
|
739
|
+
candidates.push(
|
|
740
|
+
childRect.top - rect.top + child.scrollHeight + childBorderY,
|
|
741
|
+
);
|
|
566
742
|
}
|
|
567
743
|
candidates.push(lowestBottom - rect.top);
|
|
568
744
|
|
|
@@ -711,12 +887,33 @@ export function App() {
|
|
|
711
887
|
const [localRecordingNotice, setLocalRecordingNotice] =
|
|
712
888
|
useState<LocalRecordingNotice | null>(null);
|
|
713
889
|
const [popoverView, setPopoverView] = useState<PopoverView>("recorder");
|
|
890
|
+
const [rewindSettingsReturnView, setRewindSettingsReturnView] = useState<
|
|
891
|
+
"recorder" | "settings"
|
|
892
|
+
>("recorder");
|
|
893
|
+
const [homeScreenMemoryStatus, setHomeScreenMemoryStatus] =
|
|
894
|
+
useState<ScreenMemoryStatus | null>(null);
|
|
895
|
+
const [homeScreenMemoryBusy, setHomeScreenMemoryBusy] = useState(false);
|
|
896
|
+
const [rewindAgentPromptCopied, setRewindAgentPromptCopied] = useState(false);
|
|
897
|
+
const [agentHandoff, setAgentHandoff] =
|
|
898
|
+
useState<RewindAgentHandoffRequest | null>(null);
|
|
899
|
+
const agentHandoffProcessingRef = useRef<string | null>(null);
|
|
900
|
+
const agentHandoffPreviewedRef = useRef<Set<string>>(new Set());
|
|
901
|
+
const rewindExtensionProcessingRef = useRef<Set<string>>(new Set());
|
|
902
|
+
const [agentHandoffPreviewBusy, setAgentHandoffPreviewBusy] = useState(false);
|
|
903
|
+
const [agentHandoffPreviewError, setAgentHandoffPreviewError] = useState<
|
|
904
|
+
string | null
|
|
905
|
+
>(null);
|
|
906
|
+
const [promptRewindEnable, setPromptRewindEnable] = useState(false);
|
|
714
907
|
const [meetings, setMeetings] = useState<PopoverMeeting[]>([]);
|
|
715
908
|
const [meetingsLoading, setMeetingsLoading] = useState(false);
|
|
716
909
|
const [meetingsError, setMeetingsError] = useState<string | null>(null);
|
|
717
910
|
const [meetingStartMessage, setMeetingStartMessage] = useState<string | null>(
|
|
718
911
|
null,
|
|
719
912
|
);
|
|
913
|
+
const [
|
|
914
|
+
rewindMeetingHistoryAvailability,
|
|
915
|
+
setRewindMeetingHistoryAvailability,
|
|
916
|
+
] = useState<Record<string, RewindMeetingHistoryAvailability>>({});
|
|
720
917
|
const [activeMeetingId, setActiveMeetingId] = useState<string | null>(null);
|
|
721
918
|
const [readinessOpen, setReadinessOpen] = useState<boolean>(
|
|
722
919
|
() => !loadBool(READINESS_REVIEWED_KEY, false),
|
|
@@ -749,6 +946,26 @@ export function App() {
|
|
|
749
946
|
const isRecording = recorder !== null;
|
|
750
947
|
// Whether the popover window is shown; driven by the visibility effect below.
|
|
751
948
|
const [popoverVisible, setPopoverVisible] = useState(false);
|
|
949
|
+
useEffect(() => {
|
|
950
|
+
if (featureConfig?.screenMemory?.enabled !== true) {
|
|
951
|
+
setHomeScreenMemoryStatus(null);
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
954
|
+
let cancelled = false;
|
|
955
|
+
const refresh = () => {
|
|
956
|
+
invoke<ScreenMemoryStatus>("screen_memory_status")
|
|
957
|
+
.then((status) => {
|
|
958
|
+
if (!cancelled) setHomeScreenMemoryStatus(status);
|
|
959
|
+
})
|
|
960
|
+
.catch(() => {});
|
|
961
|
+
};
|
|
962
|
+
refresh();
|
|
963
|
+
const timer = window.setInterval(refresh, popoverVisible ? 5_000 : 30_000);
|
|
964
|
+
return () => {
|
|
965
|
+
cancelled = true;
|
|
966
|
+
window.clearInterval(timer);
|
|
967
|
+
};
|
|
968
|
+
}, [featureConfig?.screenMemory?.enabled, popoverVisible]);
|
|
752
969
|
const recordShortcutHandlerRef = useRef<() => void>(() => {});
|
|
753
970
|
// Mirrors `bubbleActive` (assigned below once it is computed) so device
|
|
754
971
|
// probes can synchronously tell whether the camera bubble owns the grant.
|
|
@@ -1103,6 +1320,327 @@ export function App() {
|
|
|
1103
1320
|
[serverUrl],
|
|
1104
1321
|
);
|
|
1105
1322
|
|
|
1323
|
+
const updateAgentHandoff = useCallback(
|
|
1324
|
+
async (
|
|
1325
|
+
requestId: string,
|
|
1326
|
+
status: RewindAgentHandoffRequest["status"],
|
|
1327
|
+
result?: Record<string, unknown>,
|
|
1328
|
+
error?: string,
|
|
1329
|
+
) => {
|
|
1330
|
+
await invoke("screen_memory_update_agent_handoff", {
|
|
1331
|
+
requestId,
|
|
1332
|
+
status,
|
|
1333
|
+
result: result ?? null,
|
|
1334
|
+
error: error ?? null,
|
|
1335
|
+
});
|
|
1336
|
+
},
|
|
1337
|
+
[],
|
|
1338
|
+
);
|
|
1339
|
+
|
|
1340
|
+
const previewAgentHandoff = useCallback(
|
|
1341
|
+
async (request: RewindAgentHandoffRequest) => {
|
|
1342
|
+
setAgentHandoffPreviewBusy(true);
|
|
1343
|
+
setAgentHandoffPreviewError(null);
|
|
1344
|
+
try {
|
|
1345
|
+
await invoke("rewind_agent_handoff_preview", {
|
|
1346
|
+
requestId: request.requestId,
|
|
1347
|
+
startedAt: request.startAt,
|
|
1348
|
+
endedAt: request.endAt,
|
|
1349
|
+
includeMic: request.includeMicrophone,
|
|
1350
|
+
includeSystemAudio: request.includeSystemAudio,
|
|
1351
|
+
});
|
|
1352
|
+
} catch (error) {
|
|
1353
|
+
setAgentHandoffPreviewError(
|
|
1354
|
+
error instanceof Error ? error.message : String(error),
|
|
1355
|
+
);
|
|
1356
|
+
} finally {
|
|
1357
|
+
setAgentHandoffPreviewBusy(false);
|
|
1358
|
+
}
|
|
1359
|
+
},
|
|
1360
|
+
[],
|
|
1361
|
+
);
|
|
1362
|
+
|
|
1363
|
+
const processAgentHandoff = useCallback(
|
|
1364
|
+
async (request: RewindAgentHandoffRequest) => {
|
|
1365
|
+
if (agentHandoffProcessingRef.current === request.requestId) return;
|
|
1366
|
+
agentHandoffProcessingRef.current = request.requestId;
|
|
1367
|
+
const processing = { ...request, status: "processing" as const };
|
|
1368
|
+
setAgentHandoff(processing);
|
|
1369
|
+
let recordingId: string | null = null;
|
|
1370
|
+
try {
|
|
1371
|
+
await updateAgentHandoff(request.requestId, "processing");
|
|
1372
|
+
const hasAudio =
|
|
1373
|
+
request.includeMicrophone || request.includeSystemAudio;
|
|
1374
|
+
const recording = await createPrivateAgentRewindRecording(
|
|
1375
|
+
serverUrl,
|
|
1376
|
+
hasAudio,
|
|
1377
|
+
request.startAt,
|
|
1378
|
+
);
|
|
1379
|
+
recordingId = recording.id;
|
|
1380
|
+
await invoke("rewind_agent_handoff_upload", {
|
|
1381
|
+
requestId: request.requestId,
|
|
1382
|
+
startedAt: request.startAt,
|
|
1383
|
+
endedAt: request.endAt,
|
|
1384
|
+
serverUrl,
|
|
1385
|
+
recordingId,
|
|
1386
|
+
authToken: loadDesktopAuthToken(serverUrl),
|
|
1387
|
+
cookie: typeof document !== "undefined" ? document.cookie || "" : "",
|
|
1388
|
+
uploadMode: recording.uploadMode,
|
|
1389
|
+
includeMic: request.includeMicrophone,
|
|
1390
|
+
includeSystemAudio: request.includeSystemAudio,
|
|
1391
|
+
});
|
|
1392
|
+
|
|
1393
|
+
const retentionHours = {
|
|
1394
|
+
forever: null,
|
|
1395
|
+
"24-hours": 24,
|
|
1396
|
+
"7-days": 7 * 24,
|
|
1397
|
+
"30-days": 30 * 24,
|
|
1398
|
+
}[request.agentClipRetention];
|
|
1399
|
+
const autoDeleteAt = retentionHours
|
|
1400
|
+
? new Date(
|
|
1401
|
+
Date.now() + retentionHours * 60 * 60 * 1_000,
|
|
1402
|
+
).toISOString()
|
|
1403
|
+
: null;
|
|
1404
|
+
await callClipsAction("update-recording", {
|
|
1405
|
+
id: recordingId,
|
|
1406
|
+
...(autoDeleteAt ? { expiresAt: autoDeleteAt } : {}),
|
|
1407
|
+
});
|
|
1408
|
+
const link = await callClipsAction<{
|
|
1409
|
+
recordingId: string;
|
|
1410
|
+
url: string;
|
|
1411
|
+
contextUrl: string;
|
|
1412
|
+
expiresAt: string;
|
|
1413
|
+
}>("create-recording-agent-link", { recordingId });
|
|
1414
|
+
const ready: RewindAgentHandoffRequest = {
|
|
1415
|
+
...processing,
|
|
1416
|
+
status: "ready",
|
|
1417
|
+
recordingId,
|
|
1418
|
+
agentUrl: link.url,
|
|
1419
|
+
contextUrl: link.contextUrl,
|
|
1420
|
+
expiresAt: link.expiresAt,
|
|
1421
|
+
};
|
|
1422
|
+
await updateAgentHandoff(request.requestId, "ready", {
|
|
1423
|
+
recordingId,
|
|
1424
|
+
agentUrl: link.url,
|
|
1425
|
+
contextUrl: link.contextUrl,
|
|
1426
|
+
expiresAt: link.expiresAt,
|
|
1427
|
+
...(autoDeleteAt ? { autoDeleteAt } : {}),
|
|
1428
|
+
});
|
|
1429
|
+
setAgentHandoff(ready);
|
|
1430
|
+
} catch (error) {
|
|
1431
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1432
|
+
if (recordingId) {
|
|
1433
|
+
await callClipsAction("trash-recording", {
|
|
1434
|
+
id: recordingId,
|
|
1435
|
+
skipIfReady: true,
|
|
1436
|
+
}).catch(() => {});
|
|
1437
|
+
}
|
|
1438
|
+
await updateAgentHandoff(
|
|
1439
|
+
request.requestId,
|
|
1440
|
+
"failed",
|
|
1441
|
+
undefined,
|
|
1442
|
+
message,
|
|
1443
|
+
).catch(() => {});
|
|
1444
|
+
setAgentHandoff({ ...processing, status: "failed", error: message });
|
|
1445
|
+
} finally {
|
|
1446
|
+
agentHandoffProcessingRef.current = null;
|
|
1447
|
+
}
|
|
1448
|
+
},
|
|
1449
|
+
[callClipsAction, serverUrl, updateAgentHandoff],
|
|
1450
|
+
);
|
|
1451
|
+
|
|
1452
|
+
const processRewindExtension = useCallback(
|
|
1453
|
+
async (request: RewindExtensionRequest) => {
|
|
1454
|
+
if (rewindExtensionProcessingRef.current.has(request.requestId)) return;
|
|
1455
|
+
rewindExtensionProcessingRef.current.add(request.requestId);
|
|
1456
|
+
let preRollRecordingId: string | null = null;
|
|
1457
|
+
try {
|
|
1458
|
+
const origin = getRewindClipOrigin(request.recordingId);
|
|
1459
|
+
if (!origin) {
|
|
1460
|
+
throw new Error(
|
|
1461
|
+
"Clips Alpha no longer has the local start time for this Clip.",
|
|
1462
|
+
);
|
|
1463
|
+
}
|
|
1464
|
+
const endedAtMs = Date.parse(origin.startedAt);
|
|
1465
|
+
if (!Number.isFinite(endedAtMs)) {
|
|
1466
|
+
throw new Error("The original Clip start time is invalid.");
|
|
1467
|
+
}
|
|
1468
|
+
await callClipsAction("update-rewind-extension-request", {
|
|
1469
|
+
recordingId: request.recordingId,
|
|
1470
|
+
requestId: request.requestId,
|
|
1471
|
+
status: "processing",
|
|
1472
|
+
});
|
|
1473
|
+
const startedAt = new Date(
|
|
1474
|
+
endedAtMs - request.seconds * 1_000,
|
|
1475
|
+
).toISOString();
|
|
1476
|
+
const recording = await createPrivateAgentRewindRecording(
|
|
1477
|
+
serverUrl,
|
|
1478
|
+
origin.includeMicrophone || origin.includeSystemAudio,
|
|
1479
|
+
startedAt,
|
|
1480
|
+
);
|
|
1481
|
+
preRollRecordingId = recording.id;
|
|
1482
|
+
const upload = await invoke<NativeRewindUploadResult>(
|
|
1483
|
+
"rewind_agent_handoff_upload",
|
|
1484
|
+
{
|
|
1485
|
+
requestId: `handoff-${request.requestId}`,
|
|
1486
|
+
startedAt,
|
|
1487
|
+
endedAt: origin.startedAt,
|
|
1488
|
+
serverUrl,
|
|
1489
|
+
recordingId: recording.id,
|
|
1490
|
+
authToken: loadDesktopAuthToken(serverUrl),
|
|
1491
|
+
cookie:
|
|
1492
|
+
typeof document !== "undefined" ? document.cookie || "" : "",
|
|
1493
|
+
uploadMode: recording.uploadMode,
|
|
1494
|
+
includeMic: origin.includeMicrophone,
|
|
1495
|
+
includeSystemAudio: origin.includeSystemAudio,
|
|
1496
|
+
},
|
|
1497
|
+
);
|
|
1498
|
+
await callClipsAction("update-rewind-extension-request", {
|
|
1499
|
+
recordingId: request.recordingId,
|
|
1500
|
+
requestId: request.requestId,
|
|
1501
|
+
status: "ready",
|
|
1502
|
+
preRollRecordingId: recording.id,
|
|
1503
|
+
actualDurationMs: Math.round(upload.durationMs),
|
|
1504
|
+
});
|
|
1505
|
+
} catch (error) {
|
|
1506
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1507
|
+
if (preRollRecordingId) {
|
|
1508
|
+
await callClipsAction("trash-recording", {
|
|
1509
|
+
id: preRollRecordingId,
|
|
1510
|
+
skipIfReady: true,
|
|
1511
|
+
}).catch(() => {});
|
|
1512
|
+
}
|
|
1513
|
+
await callClipsAction("update-rewind-extension-request", {
|
|
1514
|
+
recordingId: request.recordingId,
|
|
1515
|
+
requestId: request.requestId,
|
|
1516
|
+
status: "failed",
|
|
1517
|
+
error: message,
|
|
1518
|
+
}).catch(() => {});
|
|
1519
|
+
} finally {
|
|
1520
|
+
rewindExtensionProcessingRef.current.delete(request.requestId);
|
|
1521
|
+
}
|
|
1522
|
+
},
|
|
1523
|
+
[callClipsAction, serverUrl],
|
|
1524
|
+
);
|
|
1525
|
+
|
|
1526
|
+
useEffect(() => {
|
|
1527
|
+
if (
|
|
1528
|
+
authStatus !== "authed" ||
|
|
1529
|
+
featureConfig?.screenMemory?.enabled !== true
|
|
1530
|
+
) {
|
|
1531
|
+
return;
|
|
1532
|
+
}
|
|
1533
|
+
let cancelled = false;
|
|
1534
|
+
const poll = async () => {
|
|
1535
|
+
const result = await callClipsAction<{
|
|
1536
|
+
requests?: RewindExtensionRequest[];
|
|
1537
|
+
}>("list-rewind-extension-requests", {}, { method: "GET" }).catch(
|
|
1538
|
+
() => null,
|
|
1539
|
+
);
|
|
1540
|
+
if (cancelled) return;
|
|
1541
|
+
for (const request of result?.requests ?? []) {
|
|
1542
|
+
void processRewindExtension(request);
|
|
1543
|
+
}
|
|
1544
|
+
};
|
|
1545
|
+
void poll();
|
|
1546
|
+
const timer = window.setInterval(() => void poll(), 3_000);
|
|
1547
|
+
return () => {
|
|
1548
|
+
cancelled = true;
|
|
1549
|
+
window.clearInterval(timer);
|
|
1550
|
+
};
|
|
1551
|
+
}, [
|
|
1552
|
+
authStatus,
|
|
1553
|
+
callClipsAction,
|
|
1554
|
+
featureConfig?.screenMemory?.enabled,
|
|
1555
|
+
processRewindExtension,
|
|
1556
|
+
]);
|
|
1557
|
+
|
|
1558
|
+
useEffect(() => {
|
|
1559
|
+
if (featureConfig?.screenMemory?.enabled !== true || agentHandoff) return;
|
|
1560
|
+
let cancelled = false;
|
|
1561
|
+
const poll = () => {
|
|
1562
|
+
invoke<RewindAgentHandoffRequest | null>(
|
|
1563
|
+
"screen_memory_next_agent_handoff",
|
|
1564
|
+
)
|
|
1565
|
+
.then((request) => {
|
|
1566
|
+
if (cancelled || !request) return;
|
|
1567
|
+
setAgentHandoff(request);
|
|
1568
|
+
getCurrentWindow()
|
|
1569
|
+
.show()
|
|
1570
|
+
.catch(() => {});
|
|
1571
|
+
getCurrentWindow()
|
|
1572
|
+
.setFocus()
|
|
1573
|
+
.catch(() => {});
|
|
1574
|
+
if (!request.reviewRequired) {
|
|
1575
|
+
void processAgentHandoff(request);
|
|
1576
|
+
} else if (
|
|
1577
|
+
featureConfig?.screenMemory?.autoPreviewBeforeSending !== false &&
|
|
1578
|
+
!agentHandoffPreviewedRef.current.has(request.requestId)
|
|
1579
|
+
) {
|
|
1580
|
+
// Polling and config refreshes can rerender this surface. Mark the
|
|
1581
|
+
// request before preparing QuickTime so one approval request opens
|
|
1582
|
+
// one local preview, while leaving the manual control available.
|
|
1583
|
+
agentHandoffPreviewedRef.current.add(request.requestId);
|
|
1584
|
+
void previewAgentHandoff(request);
|
|
1585
|
+
}
|
|
1586
|
+
})
|
|
1587
|
+
.catch(() => {});
|
|
1588
|
+
};
|
|
1589
|
+
poll();
|
|
1590
|
+
const timer = window.setInterval(poll, 2_000);
|
|
1591
|
+
return () => {
|
|
1592
|
+
cancelled = true;
|
|
1593
|
+
window.clearInterval(timer);
|
|
1594
|
+
};
|
|
1595
|
+
}, [
|
|
1596
|
+
agentHandoff,
|
|
1597
|
+
featureConfig?.screenMemory?.autoPreviewBeforeSending,
|
|
1598
|
+
featureConfig?.screenMemory?.enabled,
|
|
1599
|
+
previewAgentHandoff,
|
|
1600
|
+
processAgentHandoff,
|
|
1601
|
+
]);
|
|
1602
|
+
|
|
1603
|
+
useEffect(() => {
|
|
1604
|
+
if (featureConfig?.screenMemory?.enabled !== true) return;
|
|
1605
|
+
let cancelled = false;
|
|
1606
|
+
const sweep = async () => {
|
|
1607
|
+
const due = await invoke<DueRewindAgentHandoff[]>(
|
|
1608
|
+
"screen_memory_due_agent_handoffs",
|
|
1609
|
+
).catch(() => []);
|
|
1610
|
+
if (cancelled) return;
|
|
1611
|
+
for (const item of due) {
|
|
1612
|
+
try {
|
|
1613
|
+
const cleanup = await callClipsAction<{
|
|
1614
|
+
deleted: boolean;
|
|
1615
|
+
reason: string;
|
|
1616
|
+
}>("delete-agent-recording-if-unpromoted", {
|
|
1617
|
+
id: item.recordingId,
|
|
1618
|
+
});
|
|
1619
|
+
if (cleanup.deleted) {
|
|
1620
|
+
await invoke("screen_memory_mark_agent_handoff_deleted", {
|
|
1621
|
+
requestId: item.requestId,
|
|
1622
|
+
});
|
|
1623
|
+
} else if (cleanup.reason === "promoted") {
|
|
1624
|
+
await invoke("screen_memory_cancel_agent_handoff_cleanup", {
|
|
1625
|
+
requestId: item.requestId,
|
|
1626
|
+
});
|
|
1627
|
+
}
|
|
1628
|
+
} catch (error) {
|
|
1629
|
+
console.warn(
|
|
1630
|
+
"[clips-tray] agent-created Clip cleanup failed:",
|
|
1631
|
+
error,
|
|
1632
|
+
);
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
};
|
|
1636
|
+
void sweep();
|
|
1637
|
+
const timer = window.setInterval(() => void sweep(), 60_000);
|
|
1638
|
+
return () => {
|
|
1639
|
+
cancelled = true;
|
|
1640
|
+
window.clearInterval(timer);
|
|
1641
|
+
};
|
|
1642
|
+
}, [callClipsAction, featureConfig?.screenMemory?.enabled]);
|
|
1643
|
+
|
|
1106
1644
|
const fetchUpcomingMeetings = useCallback(async () => {
|
|
1107
1645
|
if (authStatus !== "authed") {
|
|
1108
1646
|
setMeetings([]);
|
|
@@ -1143,28 +1681,79 @@ export function App() {
|
|
|
1143
1681
|
}
|
|
1144
1682
|
}, [authStatus, callClipsAction]);
|
|
1145
1683
|
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
}
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1684
|
+
useEffect(() => {
|
|
1685
|
+
let cancelled = false;
|
|
1686
|
+
if (popoverView !== "meetings" || meetings.length === 0) {
|
|
1687
|
+
setRewindMeetingHistoryAvailability({});
|
|
1688
|
+
return () => {
|
|
1689
|
+
cancelled = true;
|
|
1690
|
+
};
|
|
1691
|
+
}
|
|
1692
|
+
Promise.all(
|
|
1693
|
+
meetings.map(async (meeting) => {
|
|
1694
|
+
if (!meeting.scheduledStart)
|
|
1695
|
+
return [meeting.id, { available: false }] as const;
|
|
1696
|
+
try {
|
|
1697
|
+
const availability = await invoke<RewindMeetingHistoryAvailability>(
|
|
1698
|
+
"rewind_meeting_history_status",
|
|
1699
|
+
{ scheduledStart: meeting.scheduledStart },
|
|
1700
|
+
);
|
|
1701
|
+
return [meeting.id, availability] as const;
|
|
1702
|
+
} catch (error) {
|
|
1703
|
+
return [
|
|
1704
|
+
meeting.id,
|
|
1705
|
+
{
|
|
1706
|
+
available: false,
|
|
1707
|
+
reason:
|
|
1708
|
+
error instanceof Error
|
|
1709
|
+
? error.message
|
|
1710
|
+
: "Earlier local meeting audio is unavailable.",
|
|
1711
|
+
},
|
|
1712
|
+
] as const;
|
|
1713
|
+
}
|
|
1714
|
+
}),
|
|
1715
|
+
).then((entries) => {
|
|
1716
|
+
if (!cancelled)
|
|
1717
|
+
setRewindMeetingHistoryAvailability(Object.fromEntries(entries));
|
|
1157
1718
|
});
|
|
1158
|
-
|
|
1719
|
+
return () => {
|
|
1720
|
+
cancelled = true;
|
|
1721
|
+
};
|
|
1722
|
+
}, [meetings, popoverView]);
|
|
1723
|
+
|
|
1724
|
+
const startMeetingNotes = useCallback(
|
|
1725
|
+
(meeting: PopoverMeeting, includeFromMeetingStart = false) => {
|
|
1726
|
+
setActiveMeetingId(meeting.id);
|
|
1727
|
+
setMeetingStartMessage(
|
|
1728
|
+
includeFromMeetingStart
|
|
1729
|
+
? `Including earlier local audio for ${meeting.title}…`
|
|
1730
|
+
: `Starting notes for ${meeting.title}…`,
|
|
1731
|
+
);
|
|
1732
|
+
emit("meetings:start-transcription", {
|
|
1733
|
+
meetingId: meeting.id,
|
|
1734
|
+
joinUrl: meeting.joinUrl,
|
|
1735
|
+
reason: "user",
|
|
1736
|
+
scheduledStart: meeting.scheduledStart,
|
|
1737
|
+
includeFromMeetingStart,
|
|
1738
|
+
}).catch((err) => {
|
|
1739
|
+
console.error("[clips-popover] start meeting notes failed:", err);
|
|
1740
|
+
setActiveMeetingId(null);
|
|
1741
|
+
setMeetingStartMessage(
|
|
1742
|
+
"Could not start notes. Try again from Meetings.",
|
|
1743
|
+
);
|
|
1744
|
+
});
|
|
1745
|
+
},
|
|
1746
|
+
[],
|
|
1747
|
+
);
|
|
1159
1748
|
|
|
1160
1749
|
const startMeetingNotesAndJoin = useCallback(
|
|
1161
|
-
(meeting: PopoverMeeting) => {
|
|
1750
|
+
(meeting: PopoverMeeting, includeFromMeetingStart = false) => {
|
|
1162
1751
|
if (meeting.joinUrl) {
|
|
1163
1752
|
openMeetingJoinUrl(meeting.joinUrl).catch((err) => {
|
|
1164
1753
|
console.error("[clips-popover] open meeting join url failed:", err);
|
|
1165
1754
|
});
|
|
1166
1755
|
}
|
|
1167
|
-
startMeetingNotes(meeting);
|
|
1756
|
+
startMeetingNotes(meeting, includeFromMeetingStart);
|
|
1168
1757
|
hidePopover();
|
|
1169
1758
|
},
|
|
1170
1759
|
[startMeetingNotes],
|
|
@@ -1182,7 +1771,7 @@ export function App() {
|
|
|
1182
1771
|
useEffect(() => {
|
|
1183
1772
|
invoke<string | null>("get_active_meeting_id")
|
|
1184
1773
|
.then((meetingId) => {
|
|
1185
|
-
if (meetingId) setActiveMeetingId(meetingId);
|
|
1774
|
+
if (meetingId) setActiveMeetingId((current) => current ?? meetingId);
|
|
1186
1775
|
})
|
|
1187
1776
|
.catch(() => {});
|
|
1188
1777
|
|
|
@@ -1205,6 +1794,7 @@ export function App() {
|
|
|
1205
1794
|
(event) => {
|
|
1206
1795
|
if (event.payload?.meetingId) {
|
|
1207
1796
|
setActiveMeetingId(event.payload.meetingId);
|
|
1797
|
+
setMeetingStartMessage("Meeting notes are live and staying local.");
|
|
1208
1798
|
}
|
|
1209
1799
|
},
|
|
1210
1800
|
),
|
|
@@ -1218,11 +1808,12 @@ export function App() {
|
|
|
1218
1808
|
? null
|
|
1219
1809
|
: current,
|
|
1220
1810
|
);
|
|
1811
|
+
setMeetingStartMessage(null);
|
|
1221
1812
|
},
|
|
1222
1813
|
),
|
|
1223
1814
|
);
|
|
1224
1815
|
track(
|
|
1225
|
-
listen<{ meetingId?: string | null }>(
|
|
1816
|
+
listen<{ meetingId?: string | null; error?: string }>(
|
|
1226
1817
|
"meetings:transcription-error",
|
|
1227
1818
|
(event) => {
|
|
1228
1819
|
setActiveMeetingId((current) =>
|
|
@@ -1230,6 +1821,20 @@ export function App() {
|
|
|
1230
1821
|
? null
|
|
1231
1822
|
: current,
|
|
1232
1823
|
);
|
|
1824
|
+
setMeetingStartMessage(
|
|
1825
|
+
event.payload?.error || "Could not start meeting notes.",
|
|
1826
|
+
);
|
|
1827
|
+
},
|
|
1828
|
+
),
|
|
1829
|
+
);
|
|
1830
|
+
track(
|
|
1831
|
+
listen<{ meetingId?: string | null; error?: string }>(
|
|
1832
|
+
"meetings:history-error",
|
|
1833
|
+
(event) => {
|
|
1834
|
+
setMeetingStartMessage(
|
|
1835
|
+
event.payload?.error ||
|
|
1836
|
+
"Meeting notes are live, but the earlier local audio could not be included.",
|
|
1837
|
+
);
|
|
1233
1838
|
},
|
|
1234
1839
|
),
|
|
1235
1840
|
);
|
|
@@ -1793,7 +2398,7 @@ export function App() {
|
|
|
1793
2398
|
const appRef = useRef<HTMLDivElement | null>(null);
|
|
1794
2399
|
usePopoverAutoSize(appRef, {
|
|
1795
2400
|
disabled: !popoverVisible || isRecording || recordingFlowActive,
|
|
1796
|
-
width: popoverView === "settings" ? 440 : 360,
|
|
2401
|
+
width: popoverView === "settings" || popoverView === "memory" ? 440 : 360,
|
|
1797
2402
|
});
|
|
1798
2403
|
|
|
1799
2404
|
const loadPendingUploads = useCallback(async () => {
|
|
@@ -2523,12 +3128,43 @@ export function App() {
|
|
|
2523
3128
|
const showCameraRow = mode !== "screen"; // screen-only has no camera
|
|
2524
3129
|
const showSourceRow = mode !== "camera"; // camera-only has no screen source
|
|
2525
3130
|
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
3131
|
+
async function setHomeRewindRemembering(remembering: boolean) {
|
|
3132
|
+
if (homeScreenMemoryBusy) return;
|
|
3133
|
+
if (remembering && featureConfig?.screenMemory?.enabled !== true) {
|
|
3134
|
+
setRewindSettingsReturnView("recorder");
|
|
3135
|
+
setPromptRewindEnable(true);
|
|
3136
|
+
setPopoverView("rewind-settings");
|
|
3137
|
+
return;
|
|
3138
|
+
}
|
|
3139
|
+
setHomeScreenMemoryBusy(true);
|
|
3140
|
+
try {
|
|
3141
|
+
const current = await invoke<FeatureConfig>("get_feature_config");
|
|
3142
|
+
await invoke("set_feature_config", {
|
|
3143
|
+
config: {
|
|
3144
|
+
...current,
|
|
3145
|
+
screenMemory: {
|
|
3146
|
+
...DEFAULT_SCREEN_MEMORY_CONFIG,
|
|
3147
|
+
...current.screenMemory,
|
|
3148
|
+
// Once Rewind has been set up, the everyday Home switch is a
|
|
3149
|
+
// pause/resume control. Full disable and capture permissions live
|
|
3150
|
+
// in Rewind Settings, so an accidental Home click never tears
|
|
3151
|
+
// down the configured memory system or asks for consent again.
|
|
3152
|
+
enabled: true,
|
|
3153
|
+
paused: !remembering,
|
|
3154
|
+
},
|
|
3155
|
+
},
|
|
3156
|
+
});
|
|
3157
|
+
const status = await invoke<ScreenMemoryStatus>("screen_memory_status");
|
|
3158
|
+
setHomeScreenMemoryStatus(status);
|
|
3159
|
+
} catch (err) {
|
|
3160
|
+
console.error(
|
|
3161
|
+
"[clips-tray] update Rewind remembering state failed:",
|
|
3162
|
+
err,
|
|
3163
|
+
);
|
|
3164
|
+
} finally {
|
|
3165
|
+
setHomeScreenMemoryBusy(false);
|
|
3166
|
+
}
|
|
3167
|
+
}
|
|
2532
3168
|
|
|
2533
3169
|
const pendingUploadBanner = recordingStopFinalizing ? (
|
|
2534
3170
|
<FinalizingUploadBanner />
|
|
@@ -2546,11 +3182,179 @@ export function App() {
|
|
|
2546
3182
|
/>
|
|
2547
3183
|
) : null;
|
|
2548
3184
|
|
|
2549
|
-
|
|
3185
|
+
async function copyRewindAgentPrompt() {
|
|
3186
|
+
try {
|
|
3187
|
+
await navigator.clipboard.writeText(REWIND_AGENT_PROMPT);
|
|
3188
|
+
setRewindAgentPromptCopied(true);
|
|
3189
|
+
window.setTimeout(() => setRewindAgentPromptCopied(false), 1_500);
|
|
3190
|
+
} catch (err) {
|
|
3191
|
+
console.error("[clips-tray] copy Rewind agent prompt failed:", err);
|
|
3192
|
+
}
|
|
3193
|
+
}
|
|
3194
|
+
|
|
3195
|
+
if (agentHandoff) {
|
|
3196
|
+
const durationSeconds = Math.max(
|
|
3197
|
+
1,
|
|
3198
|
+
Math.round(
|
|
3199
|
+
(new Date(agentHandoff.endAt).getTime() -
|
|
3200
|
+
new Date(agentHandoff.startAt).getTime()) /
|
|
3201
|
+
1_000,
|
|
3202
|
+
),
|
|
3203
|
+
);
|
|
3204
|
+
return (
|
|
3205
|
+
<div className="app app-settings" ref={appRef}>
|
|
3206
|
+
<div className="setup popover-view rewind-settings-surface">
|
|
3207
|
+
<div className="setup-header">
|
|
3208
|
+
<h2>
|
|
3209
|
+
{agentHandoff.status === "pending"
|
|
3210
|
+
? "Review before sending"
|
|
3211
|
+
: agentHandoff.status === "processing"
|
|
3212
|
+
? "Making a private Clip"
|
|
3213
|
+
: agentHandoff.status === "ready"
|
|
3214
|
+
? "Clip sent to your agent"
|
|
3215
|
+
: "Clip handoff needs attention"}
|
|
3216
|
+
</h2>
|
|
3217
|
+
</div>
|
|
3218
|
+
<div className="rewind-agent-guide">
|
|
3219
|
+
<div className="rewind-agent-guide-icon">
|
|
3220
|
+
<IconHistory size={17} stroke={1.8} />
|
|
3221
|
+
</div>
|
|
3222
|
+
<div>
|
|
3223
|
+
<strong>{agentHandoff.reason}</strong>
|
|
3224
|
+
<p>
|
|
3225
|
+
{new Date(agentHandoff.startAt).toLocaleString()} –{" "}
|
|
3226
|
+
{new Date(agentHandoff.endAt).toLocaleTimeString()} ·{" "}
|
|
3227
|
+
{durationSeconds} seconds
|
|
3228
|
+
</p>
|
|
3229
|
+
</div>
|
|
3230
|
+
</div>
|
|
3231
|
+
<div className="rewind-local-promise">
|
|
3232
|
+
<IconShieldLock size={17} stroke={1.8} />
|
|
3233
|
+
<p>
|
|
3234
|
+
<strong>Only this interval becomes a private Clip.</strong> The
|
|
3235
|
+
rolling Rewind archive and its local paths stay on this Mac.
|
|
3236
|
+
{agentHandoff.agentClipRetention === "forever"
|
|
3237
|
+
? " This Clip will be kept in your Library."
|
|
3238
|
+
: ` It uses your ${agentHandoff.agentClipRetention.replace("-", " ")} agent-Clip retention setting.`}
|
|
3239
|
+
</p>
|
|
3240
|
+
</div>
|
|
3241
|
+
{agentHandoff.status === "pending" ? (
|
|
3242
|
+
<>
|
|
3243
|
+
<div className="setup-grid">
|
|
3244
|
+
<div className="setup-mini-field">
|
|
3245
|
+
<span>Microphone</span>
|
|
3246
|
+
<Switch
|
|
3247
|
+
on={agentHandoff.includeMicrophone}
|
|
3248
|
+
onChange={(includeMicrophone) =>
|
|
3249
|
+
setAgentHandoff({ ...agentHandoff, includeMicrophone })
|
|
3250
|
+
}
|
|
3251
|
+
label="Include microphone audio"
|
|
3252
|
+
/>
|
|
3253
|
+
</div>
|
|
3254
|
+
<div className="setup-mini-field">
|
|
3255
|
+
<span>Mac audio</span>
|
|
3256
|
+
<Switch
|
|
3257
|
+
on={agentHandoff.includeSystemAudio}
|
|
3258
|
+
onChange={(includeSystemAudio) =>
|
|
3259
|
+
setAgentHandoff({ ...agentHandoff, includeSystemAudio })
|
|
3260
|
+
}
|
|
3261
|
+
label="Include Mac audio"
|
|
3262
|
+
/>
|
|
3263
|
+
</div>
|
|
3264
|
+
</div>
|
|
3265
|
+
<button
|
|
3266
|
+
type="button"
|
|
3267
|
+
className="secondary"
|
|
3268
|
+
disabled={agentHandoffPreviewBusy}
|
|
3269
|
+
onClick={() => void previewAgentHandoff(agentHandoff)}
|
|
3270
|
+
>
|
|
3271
|
+
{agentHandoffPreviewBusy
|
|
3272
|
+
? "Preparing preview…"
|
|
3273
|
+
: "Preview range"}
|
|
3274
|
+
</button>
|
|
3275
|
+
{agentHandoffPreviewError ? (
|
|
3276
|
+
<p className="setup-error" role="alert">
|
|
3277
|
+
{agentHandoffPreviewError}
|
|
3278
|
+
</p>
|
|
3279
|
+
) : null}
|
|
3280
|
+
<button
|
|
3281
|
+
type="button"
|
|
3282
|
+
className="primary rewind-consent-primary"
|
|
3283
|
+
onClick={() => void processAgentHandoff(agentHandoff)}
|
|
3284
|
+
>
|
|
3285
|
+
Send selected range to agent
|
|
3286
|
+
</button>
|
|
3287
|
+
<button
|
|
3288
|
+
type="button"
|
|
3289
|
+
className="rewind-quiet-button"
|
|
3290
|
+
onClick={async () => {
|
|
3291
|
+
await updateAgentHandoff(agentHandoff.requestId, "declined");
|
|
3292
|
+
setAgentHandoff(null);
|
|
3293
|
+
}}
|
|
3294
|
+
>
|
|
3295
|
+
Don’t send
|
|
3296
|
+
</button>
|
|
3297
|
+
</>
|
|
3298
|
+
) : agentHandoff.status === "processing" ? (
|
|
3299
|
+
<p className="setup-hint" role="status">
|
|
3300
|
+
Materializing the bounded range, uploading it privately, and
|
|
3301
|
+
preparing transcript and frame access for your agent…
|
|
3302
|
+
</p>
|
|
3303
|
+
) : agentHandoff.status === "ready" ? (
|
|
3304
|
+
<>
|
|
3305
|
+
<p className="setup-hint" role="status">
|
|
3306
|
+
The agent received a temporary access link. The private Clip
|
|
3307
|
+
itself remains in your Library according to your retention
|
|
3308
|
+
setting.
|
|
3309
|
+
</p>
|
|
3310
|
+
<button
|
|
3311
|
+
type="button"
|
|
3312
|
+
className="secondary"
|
|
3313
|
+
onClick={() => {
|
|
3314
|
+
if (!agentHandoff.agentUrl) return;
|
|
3315
|
+
import("@tauri-apps/plugin-shell")
|
|
3316
|
+
.then(({ open }) => open(agentHandoff.agentUrl!))
|
|
3317
|
+
.catch(() => {});
|
|
3318
|
+
}}
|
|
3319
|
+
>
|
|
3320
|
+
Open Clip
|
|
3321
|
+
</button>
|
|
3322
|
+
<button
|
|
3323
|
+
type="button"
|
|
3324
|
+
className="primary rewind-consent-primary"
|
|
3325
|
+
onClick={() => setAgentHandoff(null)}
|
|
3326
|
+
>
|
|
3327
|
+
Done
|
|
3328
|
+
</button>
|
|
3329
|
+
</>
|
|
3330
|
+
) : (
|
|
3331
|
+
<>
|
|
3332
|
+
<p className="setup-error" role="alert">
|
|
3333
|
+
{agentHandoff.error || "The bounded Clip could not be sent."}
|
|
3334
|
+
</p>
|
|
3335
|
+
<button
|
|
3336
|
+
type="button"
|
|
3337
|
+
className="secondary"
|
|
3338
|
+
onClick={() => setAgentHandoff(null)}
|
|
3339
|
+
>
|
|
3340
|
+
Close
|
|
3341
|
+
</button>
|
|
3342
|
+
</>
|
|
3343
|
+
)}
|
|
3344
|
+
</div>
|
|
3345
|
+
</div>
|
|
3346
|
+
);
|
|
3347
|
+
}
|
|
3348
|
+
|
|
3349
|
+
if (popoverView === "memory" || popoverView === "rewind-settings") {
|
|
2550
3350
|
return (
|
|
2551
3351
|
<div className="app app-settings" ref={appRef}>
|
|
2552
3352
|
{pendingUploadBanner}
|
|
3353
|
+
{isRecording ? <ActiveRecordingBanner /> : null}
|
|
2553
3354
|
<Setup
|
|
3355
|
+
surface={popoverView === "memory" ? "memory" : "rewind"}
|
|
3356
|
+
recordingActive={isRecording || recordingFlowActive}
|
|
3357
|
+
promptRewindEnable={promptRewindEnable}
|
|
2554
3358
|
initial={serverUrl}
|
|
2555
3359
|
serverUrl={serverUrl}
|
|
2556
3360
|
signedInAs={signedInAs}
|
|
@@ -2575,26 +3379,79 @@ export function App() {
|
|
|
2575
3379
|
setServerUrl(url.replace(/\/+$/, ""));
|
|
2576
3380
|
setPopoverView("recorder");
|
|
2577
3381
|
}}
|
|
2578
|
-
|
|
3382
|
+
onOpenRewind={() => setPopoverView("rewind-settings")}
|
|
3383
|
+
onOpenMemory={() => setPopoverView("memory")}
|
|
3384
|
+
onCancel={() => {
|
|
3385
|
+
setPromptRewindEnable(false);
|
|
3386
|
+
setPopoverView(
|
|
3387
|
+
popoverView === "memory"
|
|
3388
|
+
? "rewind-settings"
|
|
3389
|
+
: rewindSettingsReturnView,
|
|
3390
|
+
);
|
|
3391
|
+
}}
|
|
2579
3392
|
/>
|
|
2580
3393
|
</div>
|
|
2581
3394
|
);
|
|
2582
3395
|
}
|
|
2583
3396
|
|
|
2584
|
-
if (popoverView === "
|
|
3397
|
+
if (popoverView === "settings") {
|
|
2585
3398
|
return (
|
|
2586
|
-
<div className="app app-
|
|
3399
|
+
<div className="app app-settings" ref={appRef}>
|
|
2587
3400
|
{pendingUploadBanner}
|
|
2588
|
-
<
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
3401
|
+
{isRecording ? <ActiveRecordingBanner /> : null}
|
|
3402
|
+
<Setup
|
|
3403
|
+
recordingActive={isRecording || recordingFlowActive}
|
|
3404
|
+
initial={serverUrl}
|
|
3405
|
+
serverUrl={serverUrl}
|
|
3406
|
+
signedInAs={signedInAs}
|
|
3407
|
+
voiceShortcut={voiceShortcut}
|
|
3408
|
+
voiceCustomShortcut={voiceCustomShortcut}
|
|
3409
|
+
popoverCustomShortcut={popoverCustomShortcut}
|
|
3410
|
+
recordCustomShortcut={recordCustomShortcut}
|
|
3411
|
+
voiceMode={voiceMode}
|
|
3412
|
+
voiceProvider={voiceProvider}
|
|
3413
|
+
voiceInstructions={voiceInstructions}
|
|
3414
|
+
shortcutRegistrationError={shortcutRegistrationError}
|
|
3415
|
+
onVoiceShortcutChange={updateVoiceShortcut}
|
|
3416
|
+
onVoiceCustomShortcutChange={setVoiceCustomShortcut}
|
|
3417
|
+
onPopoverCustomShortcutChange={setPopoverCustomShortcut}
|
|
3418
|
+
onRecordCustomShortcutChange={setRecordCustomShortcut}
|
|
3419
|
+
onVoiceModeChange={setVoiceMode}
|
|
3420
|
+
onVoiceProviderChange={setVoiceProvider}
|
|
3421
|
+
onVoiceInstructionsChange={setVoiceInstructions}
|
|
3422
|
+
onSignOut={signOut}
|
|
3423
|
+
onConnect={(url) => {
|
|
3424
|
+
saveString(STORAGE_KEY, url.replace(/\/+$/, ""));
|
|
3425
|
+
setServerUrl(url.replace(/\/+$/, ""));
|
|
3426
|
+
setPopoverView("recorder");
|
|
3427
|
+
}}
|
|
3428
|
+
onOpenRewind={() => {
|
|
3429
|
+
setPromptRewindEnable(false);
|
|
3430
|
+
setRewindSettingsReturnView("settings");
|
|
3431
|
+
setPopoverView("rewind-settings");
|
|
3432
|
+
}}
|
|
3433
|
+
onCancel={() => setPopoverView("recorder")}
|
|
3434
|
+
/>
|
|
3435
|
+
</div>
|
|
3436
|
+
);
|
|
3437
|
+
}
|
|
3438
|
+
|
|
3439
|
+
if (popoverView === "meetings") {
|
|
3440
|
+
return (
|
|
3441
|
+
<div className="app app-popover-view" ref={appRef}>
|
|
3442
|
+
{pendingUploadBanner}
|
|
3443
|
+
{isRecording ? <ActiveRecordingBanner /> : null}
|
|
3444
|
+
<MeetingsPopoverView
|
|
3445
|
+
meetings={meetings}
|
|
3446
|
+
loading={meetingsLoading}
|
|
3447
|
+
error={meetingsError}
|
|
3448
|
+
startMessage={meetingStartMessage}
|
|
3449
|
+
activeMeetingId={activeMeetingId}
|
|
3450
|
+
meetingsEnabled={featureConfig?.meetingsEnabled !== false}
|
|
3451
|
+
rewindHistoryAvailability={rewindMeetingHistoryAvailability}
|
|
3452
|
+
onBack={() => setPopoverView("recorder")}
|
|
3453
|
+
onRefresh={fetchUpcomingMeetings}
|
|
3454
|
+
onOpenMeetings={() => openInBrowser("/meetings")}
|
|
2598
3455
|
onOpenMeeting={(meetingId) =>
|
|
2599
3456
|
openInBrowser(`/meetings/${encodeURIComponent(meetingId)}`)
|
|
2600
3457
|
}
|
|
@@ -2611,6 +3468,7 @@ export function App() {
|
|
|
2611
3468
|
return (
|
|
2612
3469
|
<div className="app app-popover-view" ref={appRef}>
|
|
2613
3470
|
{pendingUploadBanner}
|
|
3471
|
+
{isRecording ? <ActiveRecordingBanner /> : null}
|
|
2614
3472
|
<DictationPopoverView
|
|
2615
3473
|
voiceEnabled={voiceDictationEnabled}
|
|
2616
3474
|
voiceShortcut={voiceShortcut}
|
|
@@ -2686,141 +3544,230 @@ export function App() {
|
|
|
2686
3544
|
}
|
|
2687
3545
|
|
|
2688
3546
|
return (
|
|
2689
|
-
<div className="app" ref={appRef}>
|
|
2690
|
-
<
|
|
2691
|
-
|
|
2692
|
-
|
|
3547
|
+
<div className="app app-recorder" ref={appRef}>
|
|
3548
|
+
<div className="recorder-home-content">
|
|
3549
|
+
<Header
|
|
3550
|
+
mode={mode}
|
|
3551
|
+
onModeChange={setMode}
|
|
3552
|
+
submitterEmail={signedInAs}
|
|
3553
|
+
/>
|
|
3554
|
+
<UpdateBanner />
|
|
2693
3555
|
|
|
2694
|
-
|
|
2695
|
-
<LocalRecordingModeBanner mode={localRecordingMode} />
|
|
2696
|
-
) : null}
|
|
3556
|
+
{pendingUploadBanner}
|
|
2697
3557
|
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
if (!localRecordingNotice.folderPath) return;
|
|
2704
|
-
invoke("open_local_recording_folder", {
|
|
2705
|
-
path: localRecordingNotice.folderPath,
|
|
2706
|
-
}).catch((err) => {
|
|
2707
|
-
console.error("[clips-tray] open local folder failed:", err);
|
|
2708
|
-
});
|
|
2709
|
-
}}
|
|
2710
|
-
/>
|
|
2711
|
-
) : null}
|
|
3558
|
+
{isRecording ? <ActiveRecordingBanner /> : null}
|
|
3559
|
+
|
|
3560
|
+
{localRecordingMode !== "off" ? (
|
|
3561
|
+
<LocalRecordingModeBanner mode={localRecordingMode} />
|
|
3562
|
+
) : null}
|
|
2712
3563
|
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
3564
|
+
{localRecordingNotice ? (
|
|
3565
|
+
<LocalRecordingSavedBanner
|
|
3566
|
+
notice={localRecordingNotice}
|
|
3567
|
+
onDismiss={() => setLocalRecordingNotice(null)}
|
|
3568
|
+
onOpenFolder={() => {
|
|
3569
|
+
if (!localRecordingNotice.folderPath) return;
|
|
3570
|
+
invoke("open_local_recording_folder", {
|
|
3571
|
+
path: localRecordingNotice.folderPath,
|
|
3572
|
+
}).catch((err) => {
|
|
3573
|
+
console.error("[clips-tray] open local folder failed:", err);
|
|
3574
|
+
});
|
|
3575
|
+
}}
|
|
2719
3576
|
/>
|
|
2720
3577
|
) : null}
|
|
2721
3578
|
|
|
2722
|
-
|
|
3579
|
+
<div className="panel">
|
|
3580
|
+
{showSourceRow ? (
|
|
3581
|
+
<SourceRow
|
|
3582
|
+
value={source}
|
|
3583
|
+
onChange={setSource}
|
|
3584
|
+
includeRegion={isMacPlatform()}
|
|
3585
|
+
/>
|
|
3586
|
+
) : null}
|
|
3587
|
+
|
|
3588
|
+
{showCameraRow ? (
|
|
3589
|
+
<MediaDeviceRow
|
|
3590
|
+
kind="camera"
|
|
3591
|
+
devices={cameraDevices}
|
|
3592
|
+
selectedId={cameraId}
|
|
3593
|
+
selectedLabel={cameraLabel}
|
|
3594
|
+
onSelect={(id, label) => {
|
|
3595
|
+
setCameraId(id);
|
|
3596
|
+
setCameraLabel(label);
|
|
3597
|
+
}}
|
|
3598
|
+
onRefresh={() => requestDeviceAccess("camera")}
|
|
3599
|
+
on={cameraOn}
|
|
3600
|
+
onToggle={setCameraOn}
|
|
3601
|
+
/>
|
|
3602
|
+
) : null}
|
|
3603
|
+
|
|
2723
3604
|
<MediaDeviceRow
|
|
2724
|
-
kind="
|
|
2725
|
-
devices={
|
|
2726
|
-
selectedId={
|
|
2727
|
-
selectedLabel={
|
|
3605
|
+
kind="mic"
|
|
3606
|
+
devices={micDevices}
|
|
3607
|
+
selectedId={selectedMicId}
|
|
3608
|
+
selectedLabel={micLabel}
|
|
2728
3609
|
onSelect={(id, label) => {
|
|
2729
|
-
|
|
2730
|
-
|
|
3610
|
+
setMicId(id);
|
|
3611
|
+
setMicLabel(label);
|
|
2731
3612
|
}}
|
|
2732
|
-
onRefresh={() => requestDeviceAccess("
|
|
2733
|
-
on={
|
|
2734
|
-
onToggle={
|
|
3613
|
+
onRefresh={() => requestDeviceAccess("mic")}
|
|
3614
|
+
on={micOn}
|
|
3615
|
+
onToggle={setMicOn}
|
|
3616
|
+
systemAudio={systemAudioOn}
|
|
3617
|
+
onSystemAudioToggle={setSystemAudioOn}
|
|
3618
|
+
meterActive={popoverVisible && !isRecording && !recordingFlowActive}
|
|
2735
3619
|
/>
|
|
2736
|
-
|
|
3620
|
+
</div>
|
|
2737
3621
|
|
|
2738
|
-
<
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
}
|
|
2747
|
-
onRefresh={() => requestDeviceAccess("mic")}
|
|
2748
|
-
on={micOn}
|
|
2749
|
-
onToggle={setMicOn}
|
|
2750
|
-
systemAudio={systemAudioOn}
|
|
2751
|
-
onSystemAudioToggle={setSystemAudioOn}
|
|
2752
|
-
meterActive={popoverVisible && !isRecording && !recordingFlowActive}
|
|
3622
|
+
<ReadinessPanel
|
|
3623
|
+
mode={mode}
|
|
3624
|
+
cameraOn={cameraOn}
|
|
3625
|
+
micOn={micOn}
|
|
3626
|
+
includeVoicePaste={voiceDictationEnabled}
|
|
3627
|
+
includeFnMonitoring={fnShortcutEnabled}
|
|
3628
|
+
open={readinessOpen}
|
|
3629
|
+
onOpenChange={updateReadinessOpen}
|
|
3630
|
+
onOpenPermission={openPrivacySettings}
|
|
2753
3631
|
/>
|
|
2754
|
-
</div>
|
|
2755
3632
|
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
3633
|
+
<section className="rewind-home-card" aria-label="Rewind status">
|
|
3634
|
+
<div className="rewind-home-status">
|
|
3635
|
+
<span
|
|
3636
|
+
className={`rewind-home-dot ${
|
|
3637
|
+
featureConfig?.screenMemory?.enabled === true &&
|
|
3638
|
+
featureConfig.screenMemory.paused !== true &&
|
|
3639
|
+
homeScreenMemoryStatus?.state === "recording"
|
|
3640
|
+
? "is-live"
|
|
3641
|
+
: ""
|
|
3642
|
+
}`}
|
|
3643
|
+
/>
|
|
3644
|
+
<div>
|
|
3645
|
+
<strong>
|
|
3646
|
+
{featureConfig?.screenMemory?.enabled !== true
|
|
3647
|
+
? "Rewind is off"
|
|
3648
|
+
: featureConfig.screenMemory.paused === true
|
|
3649
|
+
? "Rewind is paused"
|
|
3650
|
+
: homeScreenMemoryStatus?.exclusionActive
|
|
3651
|
+
? "Rewind is protecting a private moment"
|
|
3652
|
+
: "Rewind is remembering"}
|
|
3653
|
+
</strong>
|
|
3654
|
+
{featureConfig?.screenMemory?.enabled !== true ||
|
|
3655
|
+
featureConfig.screenMemory.paused ||
|
|
3656
|
+
homeScreenMemoryStatus?.exclusionActive ? (
|
|
3657
|
+
<p>
|
|
3658
|
+
{featureConfig?.screenMemory?.enabled !== true
|
|
3659
|
+
? "Private memory for moments you may need later."
|
|
3660
|
+
: featureConfig.screenMemory.paused
|
|
3661
|
+
? "Existing local memory is still available."
|
|
3662
|
+
: "An excluded app is being skipped."}
|
|
3663
|
+
</p>
|
|
3664
|
+
) : null}
|
|
3665
|
+
</div>
|
|
3666
|
+
<div className="rewind-home-controls">
|
|
3667
|
+
<button
|
|
3668
|
+
type="button"
|
|
3669
|
+
className="rewind-agent-prompt-copy"
|
|
3670
|
+
onClick={() => void copyRewindAgentPrompt()}
|
|
3671
|
+
aria-label={
|
|
3672
|
+
rewindAgentPromptCopied
|
|
3673
|
+
? "Setup prompt copied — paste it into your agent once"
|
|
3674
|
+
: "Set up your agent"
|
|
3675
|
+
}
|
|
3676
|
+
title={
|
|
3677
|
+
rewindAgentPromptCopied
|
|
3678
|
+
? "Setup prompt copied — paste it into your agent once"
|
|
3679
|
+
: "Set up your agent"
|
|
3680
|
+
}
|
|
3681
|
+
>
|
|
3682
|
+
{rewindAgentPromptCopied ? (
|
|
3683
|
+
<IconCircleCheck size={15} stroke={2} />
|
|
3684
|
+
) : (
|
|
3685
|
+
<IconCopy size={15} stroke={1.9} />
|
|
3686
|
+
)}
|
|
3687
|
+
</button>
|
|
3688
|
+
<Switch
|
|
3689
|
+
on={
|
|
3690
|
+
featureConfig?.screenMemory?.enabled === true &&
|
|
3691
|
+
featureConfig.screenMemory.paused !== true
|
|
3692
|
+
}
|
|
3693
|
+
disabled={
|
|
3694
|
+
homeScreenMemoryBusy || isRecording || recordingFlowActive
|
|
3695
|
+
}
|
|
3696
|
+
onChange={(remembering) =>
|
|
3697
|
+
void setHomeRewindRemembering(remembering)
|
|
3698
|
+
}
|
|
3699
|
+
label={
|
|
3700
|
+
featureConfig?.screenMemory?.enabled === true
|
|
3701
|
+
? "Remember with Rewind"
|
|
3702
|
+
: "Set up Rewind"
|
|
3703
|
+
}
|
|
3704
|
+
/>
|
|
3705
|
+
</div>
|
|
3706
|
+
</div>
|
|
3707
|
+
</section>
|
|
2766
3708
|
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
onClick={() => {
|
|
2773
|
-
void handleStartRecording();
|
|
2774
|
-
}}
|
|
2775
|
-
>
|
|
2776
|
-
{localRecordingMode === "off" && videoStorageStatus === "checking"
|
|
2777
|
-
? "Checking storage..."
|
|
2778
|
-
: localRecordingMode === "off"
|
|
2779
|
-
? "Start recording"
|
|
2780
|
-
: "Start local recording"}
|
|
2781
|
-
</button>
|
|
2782
|
-
{recError ? (
|
|
2783
|
-
recError === MACOS_UPDATE_RESTART_MESSAGE ? (
|
|
2784
|
-
<UpdateRestartBanner message={recError} />
|
|
2785
|
-
) : recError === MACOS_CAPTURE_PERMISSION_MESSAGE ||
|
|
2786
|
-
recError === MACOS_SCREEN_PERMISSION_MESSAGE ||
|
|
2787
|
-
recError === DESKTOP_CAPTURE_PERMISSION_MESSAGE ? (
|
|
2788
|
-
<PermissionRecoveryBanner
|
|
2789
|
-
kind="recording"
|
|
2790
|
-
message={recError}
|
|
2791
|
-
panes={
|
|
2792
|
-
recError === MACOS_SCREEN_PERMISSION_MESSAGE
|
|
2793
|
-
? ["screen"]
|
|
2794
|
-
: permissionPanesForRecording(mode, cameraOn, micOn)
|
|
3709
|
+
{!isRecording ? (
|
|
3710
|
+
<button
|
|
3711
|
+
className="primary start"
|
|
3712
|
+
disabled={
|
|
3713
|
+
localRecordingMode === "off" && videoStorageStatus === "checking"
|
|
2795
3714
|
}
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
3715
|
+
onClick={() => {
|
|
3716
|
+
void handleStartRecording();
|
|
3717
|
+
}}
|
|
3718
|
+
>
|
|
3719
|
+
{localRecordingMode === "off" && videoStorageStatus === "checking"
|
|
3720
|
+
? "Checking storage..."
|
|
3721
|
+
: localRecordingMode === "off"
|
|
3722
|
+
? "Start recording"
|
|
3723
|
+
: "Start local recording"}
|
|
3724
|
+
</button>
|
|
3725
|
+
) : null}
|
|
3726
|
+
{recError ? (
|
|
3727
|
+
recError === MACOS_UPDATE_RESTART_MESSAGE ? (
|
|
3728
|
+
<UpdateRestartBanner message={recError} />
|
|
3729
|
+
) : recError === MACOS_CAPTURE_PERMISSION_MESSAGE ||
|
|
3730
|
+
recError === MACOS_SCREEN_PERMISSION_MESSAGE ||
|
|
3731
|
+
recError === DESKTOP_CAPTURE_PERMISSION_MESSAGE ? (
|
|
3732
|
+
<PermissionRecoveryBanner
|
|
3733
|
+
kind="recording"
|
|
3734
|
+
message={recError}
|
|
3735
|
+
panes={
|
|
3736
|
+
recError === MACOS_SCREEN_PERMISSION_MESSAGE
|
|
3737
|
+
? ["screen"]
|
|
3738
|
+
: permissionPanesForRecording(mode, cameraOn, micOn)
|
|
3739
|
+
}
|
|
3740
|
+
onRetry={handleStartRecording}
|
|
3741
|
+
/>
|
|
3742
|
+
) : recError === MACOS_SPEECH_PERMISSION_MESSAGE ? (
|
|
3743
|
+
<PermissionRecoveryBanner
|
|
3744
|
+
kind="speech"
|
|
3745
|
+
message={recError}
|
|
3746
|
+
panes={["speech", "microphone"]}
|
|
3747
|
+
onRetry={handleStartRecording}
|
|
3748
|
+
/>
|
|
3749
|
+
) : isStorageSetupFailureMessage(recError) ? (
|
|
3750
|
+
<StorageConnectionBanner
|
|
3751
|
+
onConnect={() => openVideoStorageSetup()}
|
|
3752
|
+
/>
|
|
3753
|
+
) : (
|
|
3754
|
+
<div className="error-banner">{recError}</div>
|
|
3755
|
+
)
|
|
3756
|
+
) : null}
|
|
3757
|
+
{cameraError && !recError ? (
|
|
3758
|
+
cameraError === MACOS_CAPTURE_PERMISSION_MESSAGE ||
|
|
3759
|
+
cameraError === DESKTOP_CAPTURE_PERMISSION_MESSAGE ? (
|
|
3760
|
+
<PermissionRecoveryBanner
|
|
3761
|
+
kind="camera"
|
|
3762
|
+
message={cameraError}
|
|
3763
|
+
panes={["camera"]}
|
|
3764
|
+
onRetry={retryCameraPreview}
|
|
3765
|
+
/>
|
|
3766
|
+
) : (
|
|
3767
|
+
<div className="error-banner">{cameraError}</div>
|
|
3768
|
+
)
|
|
3769
|
+
) : null}
|
|
3770
|
+
</div>
|
|
2824
3771
|
|
|
2825
3772
|
<div className="bottom-row">
|
|
2826
3773
|
<BottomButton
|
|
@@ -3507,6 +4454,7 @@ function MeetingsPopoverView({
|
|
|
3507
4454
|
startMessage,
|
|
3508
4455
|
activeMeetingId,
|
|
3509
4456
|
meetingsEnabled,
|
|
4457
|
+
rewindHistoryAvailability,
|
|
3510
4458
|
onBack,
|
|
3511
4459
|
onRefresh,
|
|
3512
4460
|
onOpenMeetings,
|
|
@@ -3522,15 +4470,38 @@ function MeetingsPopoverView({
|
|
|
3522
4470
|
startMessage: string | null;
|
|
3523
4471
|
activeMeetingId: string | null;
|
|
3524
4472
|
meetingsEnabled: boolean;
|
|
4473
|
+
rewindHistoryAvailability: Record<string, RewindMeetingHistoryAvailability>;
|
|
3525
4474
|
onBack: () => void;
|
|
3526
4475
|
onRefresh: () => void;
|
|
3527
4476
|
onOpenMeetings: () => void;
|
|
3528
4477
|
onOpenMeeting: (meetingId: string) => void;
|
|
3529
4478
|
onOpenSettings: () => void;
|
|
3530
|
-
onStartNotes: (
|
|
3531
|
-
|
|
4479
|
+
onStartNotes: (
|
|
4480
|
+
meeting: PopoverMeeting,
|
|
4481
|
+
includeFromMeetingStart?: boolean,
|
|
4482
|
+
) => void;
|
|
4483
|
+
onStartNotesAndJoin: (
|
|
4484
|
+
meeting: PopoverMeeting,
|
|
4485
|
+
includeFromMeetingStart?: boolean,
|
|
4486
|
+
) => void;
|
|
3532
4487
|
onShowActiveMeeting: (meetingId: string) => void;
|
|
3533
4488
|
}) {
|
|
4489
|
+
const [includeHistoryFor, setIncludeHistoryFor] = useState<Set<string>>(
|
|
4490
|
+
() => new Set(),
|
|
4491
|
+
);
|
|
4492
|
+
|
|
4493
|
+
const consumeIncludeHistoryChoice = (meeting: PopoverMeeting): boolean => {
|
|
4494
|
+
const include = includeHistoryFor.has(meeting.id);
|
|
4495
|
+
// This is intentionally a one-shot choice. It never follows the next
|
|
4496
|
+
// meeting, an automatic start, or a tray action around like a lost duck.
|
|
4497
|
+
setIncludeHistoryFor((current) => {
|
|
4498
|
+
const next = new Set(current);
|
|
4499
|
+
next.delete(meeting.id);
|
|
4500
|
+
return next;
|
|
4501
|
+
});
|
|
4502
|
+
return include;
|
|
4503
|
+
};
|
|
4504
|
+
|
|
3534
4505
|
return (
|
|
3535
4506
|
<div className="setup popover-view">
|
|
3536
4507
|
<PopoverSubViewHeader
|
|
@@ -3589,6 +4560,8 @@ function MeetingsPopoverView({
|
|
|
3589
4560
|
const canStart = meetingCanStartNotes(meeting);
|
|
3590
4561
|
const hasJoin = Boolean(meeting.joinUrl);
|
|
3591
4562
|
const isActive = activeMeetingId === meeting.id;
|
|
4563
|
+
const rewindHistory = rewindHistoryAvailability[meeting.id];
|
|
4564
|
+
const includeHistory = includeHistoryFor.has(meeting.id);
|
|
3592
4565
|
return (
|
|
3593
4566
|
<div className="popover-list-item" key={meeting.id}>
|
|
3594
4567
|
<div className="popover-list-icon">
|
|
@@ -3600,6 +4573,24 @@ function MeetingsPopoverView({
|
|
|
3600
4573
|
{formatMeetingWhen(meeting)}
|
|
3601
4574
|
{meeting.platform ? ` · ${meeting.platform}` : ""}
|
|
3602
4575
|
</div>
|
|
4576
|
+
{canStart && rewindHistory?.available ? (
|
|
4577
|
+
<label className="popover-list-sub">
|
|
4578
|
+
<input
|
|
4579
|
+
type="checkbox"
|
|
4580
|
+
checked={includeHistory}
|
|
4581
|
+
onChange={(event) => {
|
|
4582
|
+
const checked = event.currentTarget.checked;
|
|
4583
|
+
setIncludeHistoryFor((current) => {
|
|
4584
|
+
const next = new Set(current);
|
|
4585
|
+
if (checked) next.add(meeting.id);
|
|
4586
|
+
else next.delete(meeting.id);
|
|
4587
|
+
return next;
|
|
4588
|
+
});
|
|
4589
|
+
}}
|
|
4590
|
+
/>{" "}
|
|
4591
|
+
Include from meeting start
|
|
4592
|
+
</label>
|
|
4593
|
+
) : null}
|
|
3603
4594
|
</div>
|
|
3604
4595
|
{isActive ? (
|
|
3605
4596
|
<button
|
|
@@ -3617,8 +4608,14 @@ function MeetingsPopoverView({
|
|
|
3617
4608
|
className="popover-list-action popover-list-action-primary"
|
|
3618
4609
|
onClick={() =>
|
|
3619
4610
|
hasJoin
|
|
3620
|
-
? onStartNotesAndJoin(
|
|
3621
|
-
|
|
4611
|
+
? onStartNotesAndJoin(
|
|
4612
|
+
meeting,
|
|
4613
|
+
consumeIncludeHistoryChoice(meeting),
|
|
4614
|
+
)
|
|
4615
|
+
: onStartNotes(
|
|
4616
|
+
meeting,
|
|
4617
|
+
consumeIncludeHistoryChoice(meeting),
|
|
4618
|
+
)
|
|
3622
4619
|
}
|
|
3623
4620
|
title={
|
|
3624
4621
|
hasJoin
|
|
@@ -3754,6 +4751,30 @@ function BottomButton({
|
|
|
3754
4751
|
);
|
|
3755
4752
|
}
|
|
3756
4753
|
|
|
4754
|
+
function ActiveRecordingBanner() {
|
|
4755
|
+
return (
|
|
4756
|
+
<section
|
|
4757
|
+
className="active-recording-card active-recording-card-compact"
|
|
4758
|
+
aria-live="polite"
|
|
4759
|
+
>
|
|
4760
|
+
<span className="active-recording-live" aria-label="Live recording">
|
|
4761
|
+
<span className="active-recording-live-dot" aria-hidden="true" />
|
|
4762
|
+
REC
|
|
4763
|
+
</span>
|
|
4764
|
+
<div className="active-recording-copy">
|
|
4765
|
+
<strong>Recording in progress</strong>
|
|
4766
|
+
</div>
|
|
4767
|
+
<button
|
|
4768
|
+
type="button"
|
|
4769
|
+
className="primary rec-active active-recording-stop"
|
|
4770
|
+
onClick={() => emit("clips:recorder-stop").catch(() => {})}
|
|
4771
|
+
>
|
|
4772
|
+
Stop
|
|
4773
|
+
</button>
|
|
4774
|
+
</section>
|
|
4775
|
+
);
|
|
4776
|
+
}
|
|
4777
|
+
|
|
3757
4778
|
// ---- inline icons (Tabler-style, monochrome, stroke=1.75) -----------------
|
|
3758
4779
|
|
|
3759
4780
|
// ---------------------------------------------------------------------------
|
|
@@ -3811,6 +4832,9 @@ function desktopUpdateStatusText(status: UpdateStatus): string {
|
|
|
3811
4832
|
}
|
|
3812
4833
|
|
|
3813
4834
|
function Setup({
|
|
4835
|
+
surface = "settings",
|
|
4836
|
+
promptRewindEnable = false,
|
|
4837
|
+
recordingActive = false,
|
|
3814
4838
|
initial,
|
|
3815
4839
|
serverUrl,
|
|
3816
4840
|
signedInAs,
|
|
@@ -3830,9 +4854,14 @@ function Setup({
|
|
|
3830
4854
|
onVoiceProviderChange,
|
|
3831
4855
|
onVoiceInstructionsChange,
|
|
3832
4856
|
onConnect,
|
|
4857
|
+
onOpenRewind,
|
|
4858
|
+
onOpenMemory,
|
|
3833
4859
|
onCancel,
|
|
3834
4860
|
onSignOut,
|
|
3835
4861
|
}: {
|
|
4862
|
+
surface?: "settings" | "memory" | "rewind";
|
|
4863
|
+
promptRewindEnable?: boolean;
|
|
4864
|
+
recordingActive?: boolean;
|
|
3836
4865
|
initial?: string | null;
|
|
3837
4866
|
serverUrl?: string;
|
|
3838
4867
|
signedInAs?: string | null;
|
|
@@ -3852,6 +4881,8 @@ function Setup({
|
|
|
3852
4881
|
onVoiceProviderChange: (value: VoiceProvider) => void;
|
|
3853
4882
|
onVoiceInstructionsChange: (value: string) => void;
|
|
3854
4883
|
onConnect: (url: string) => void;
|
|
4884
|
+
onOpenRewind?: () => void;
|
|
4885
|
+
onOpenMemory?: () => void;
|
|
3855
4886
|
onCancel?: () => void;
|
|
3856
4887
|
onSignOut?: () => void;
|
|
3857
4888
|
}) {
|
|
@@ -3865,8 +4896,26 @@ function Setup({
|
|
|
3865
4896
|
const autoHidePopoverEnabled = featureConfig?.autoHidePopoverEnabled === true;
|
|
3866
4897
|
const showInScreenCapture = featureConfig?.showInScreenCapture === true;
|
|
3867
4898
|
const localRecordingMode = featureConfig?.localRecordingMode ?? "off";
|
|
3868
|
-
const
|
|
4899
|
+
const observedScreenMemory =
|
|
3869
4900
|
featureConfig?.screenMemory ?? DEFAULT_SCREEN_MEMORY_CONFIG;
|
|
4901
|
+
const [screenMemory, setScreenMemory] = useState(observedScreenMemory);
|
|
4902
|
+
const [rewindConsentOpen, setRewindConsentOpen] = useState(
|
|
4903
|
+
promptRewindEnable && observedScreenMemory.enabled !== true,
|
|
4904
|
+
);
|
|
4905
|
+
const [rewindConsentMode, setRewindConsentMode] = useState<
|
|
4906
|
+
"visuals" | "visuals-audio"
|
|
4907
|
+
>(observedScreenMemory.captureMode ?? "visuals");
|
|
4908
|
+
const screenMemoryRef = useRef(observedScreenMemory);
|
|
4909
|
+
const screenMemoryMutationRef = useRef(0);
|
|
4910
|
+
const screenMemoryMutationVersionRef = useRef(0);
|
|
4911
|
+
const screenMemoryMutationTailRef = useRef<Promise<void>>(Promise.resolve());
|
|
4912
|
+
const [screenMemoryConfigBusy, setScreenMemoryConfigBusy] = useState(false);
|
|
4913
|
+
|
|
4914
|
+
useEffect(() => {
|
|
4915
|
+
if (screenMemoryMutationRef.current > 0) return;
|
|
4916
|
+
screenMemoryRef.current = observedScreenMemory;
|
|
4917
|
+
setScreenMemory(observedScreenMemory);
|
|
4918
|
+
}, [observedScreenMemory]);
|
|
3870
4919
|
const regionGuides = featureConfig?.regionGuides ?? {
|
|
3871
4920
|
enabled: false,
|
|
3872
4921
|
rects: [],
|
|
@@ -3896,14 +4945,74 @@ function Setup({
|
|
|
3896
4945
|
kind: "ok" | "error";
|
|
3897
4946
|
text: string;
|
|
3898
4947
|
} | null>(null);
|
|
4948
|
+
const [screenMemoryExportResult, setScreenMemoryExportResult] =
|
|
4949
|
+
useState<ScreenMemoryExportResult | null>(null);
|
|
3899
4950
|
const [clipDraftsError, setClipDraftsError] = useState<string | null>(null);
|
|
3900
4951
|
const [screenMemoryBusy, setScreenMemoryBusy] = useState(false);
|
|
4952
|
+
const [rewindEgressEvents, setRewindEgressEvents] = useState<
|
|
4953
|
+
RewindEgressEvent[]
|
|
4954
|
+
>([]);
|
|
4955
|
+
const [rewindEgressOpen, setRewindEgressOpen] = useState(false);
|
|
4956
|
+
const [rewindLocalQuery, setRewindLocalQuery] = useState("");
|
|
4957
|
+
const [rewindLocalResult, setRewindLocalResult] =
|
|
4958
|
+
useState<RewindLocalAskResult | null>(null);
|
|
4959
|
+
const [rewindLocalBusy, setRewindLocalBusy] = useState(false);
|
|
4960
|
+
const [rewindLocalError, setRewindLocalError] = useState<string | null>(null);
|
|
4961
|
+
const [rewindReplayId, setRewindReplayId] = useState<string | null>(null);
|
|
4962
|
+
const [excludedBundleIdsInput, setExcludedBundleIdsInput] = useState("");
|
|
4963
|
+
const [excludedApps, setExcludedApps] = useState<RewindExcludedApplication[]>(
|
|
4964
|
+
[],
|
|
4965
|
+
);
|
|
4966
|
+
const [excludedAppsBusy, setExcludedAppsBusy] = useState(false);
|
|
4967
|
+
const [agentConnectionBusy, setAgentConnectionBusy] = useState<
|
|
4968
|
+
"codex" | "claude-code" | null
|
|
4969
|
+
>(null);
|
|
4970
|
+
const [agentConnectionMessage, setAgentConnectionMessage] = useState<{
|
|
4971
|
+
kind: "ok" | "error";
|
|
4972
|
+
text: string;
|
|
4973
|
+
} | null>(null);
|
|
3901
4974
|
const screenMemorySegments = screenMemoryStatus?.recentSegments ?? [];
|
|
3902
4975
|
const screenMemoryTotalBytes = screenMemorySegments.reduce(
|
|
3903
4976
|
(sum, segment) => sum + segment.bytes,
|
|
3904
4977
|
0,
|
|
3905
4978
|
);
|
|
3906
4979
|
const screenMemoryRecording = screenMemoryStatus?.state === "recording";
|
|
4980
|
+
const captureControlsLocked = recordingActive;
|
|
4981
|
+
|
|
4982
|
+
useEffect(() => {
|
|
4983
|
+
setExcludedBundleIdsInput(
|
|
4984
|
+
(screenMemory.excludedBundleIds ?? []).join(", "),
|
|
4985
|
+
);
|
|
4986
|
+
invoke<RewindExcludedApplication[]>("resolve_rewind_excluded_apps", {
|
|
4987
|
+
bundleIds: screenMemory.excludedBundleIds ?? [],
|
|
4988
|
+
})
|
|
4989
|
+
.then(setExcludedApps)
|
|
4990
|
+
.catch(() => {
|
|
4991
|
+
setExcludedApps(
|
|
4992
|
+
(screenMemory.excludedBundleIds ?? []).map((bundleId) => ({
|
|
4993
|
+
bundleId,
|
|
4994
|
+
name: bundleId.split(".").pop() || "Application",
|
|
4995
|
+
installed: false,
|
|
4996
|
+
})),
|
|
4997
|
+
);
|
|
4998
|
+
});
|
|
4999
|
+
}, [screenMemory.excludedBundleIds]);
|
|
5000
|
+
|
|
5001
|
+
const excludedAppGroups = excludedApps.reduce<
|
|
5002
|
+
Array<RewindExcludedApplication & { bundleIds: string[] }>
|
|
5003
|
+
>((groups, app) => {
|
|
5004
|
+
const existing = groups.find(
|
|
5005
|
+
(candidate) => candidate.name.toLowerCase() === app.name.toLowerCase(),
|
|
5006
|
+
);
|
|
5007
|
+
if (existing) {
|
|
5008
|
+
existing.bundleIds.push(app.bundleId);
|
|
5009
|
+
existing.installed ||= app.installed;
|
|
5010
|
+
existing.path ||= app.path;
|
|
5011
|
+
} else {
|
|
5012
|
+
groups.push({ ...app, bundleIds: [app.bundleId] });
|
|
5013
|
+
}
|
|
5014
|
+
return groups;
|
|
5015
|
+
}, []);
|
|
3907
5016
|
|
|
3908
5017
|
const [providerStatus, setProviderStatus] =
|
|
3909
5018
|
useState<VoiceProviderStatus | null>(null);
|
|
@@ -3978,27 +5087,89 @@ function Setup({
|
|
|
3978
5087
|
);
|
|
3979
5088
|
}
|
|
3980
5089
|
|
|
3981
|
-
function setScreenMemoryConfig(
|
|
3982
|
-
patch: Partial<
|
|
5090
|
+
async function setScreenMemoryConfig(
|
|
5091
|
+
patch: Partial<ScreenMemoryStatus["config"]>,
|
|
3983
5092
|
) {
|
|
3984
5093
|
if (!featureConfig) return;
|
|
3985
5094
|
setScreenMemoryMessage(null);
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
5095
|
+
const next = {
|
|
5096
|
+
...DEFAULT_SCREEN_MEMORY_CONFIG,
|
|
5097
|
+
...screenMemoryRef.current,
|
|
5098
|
+
...patch,
|
|
5099
|
+
};
|
|
5100
|
+
const version = ++screenMemoryMutationVersionRef.current;
|
|
5101
|
+
screenMemoryMutationRef.current += 1;
|
|
5102
|
+
screenMemoryRef.current = next;
|
|
5103
|
+
setScreenMemory(next);
|
|
5104
|
+
setScreenMemoryConfigBusy(true);
|
|
5105
|
+
let operation!: Promise<void>;
|
|
5106
|
+
operation = screenMemoryMutationTailRef.current
|
|
5107
|
+
.catch(() => {})
|
|
5108
|
+
.then(async () => {
|
|
5109
|
+
// Read the latest complete config at execution time so a queued Rewind
|
|
5110
|
+
// edit cannot overwrite an unrelated setting changed while it waited.
|
|
5111
|
+
const current = await invoke<FeatureConfig>("get_feature_config");
|
|
5112
|
+
await invoke("set_feature_config", {
|
|
5113
|
+
config: { ...current, screenMemory: next },
|
|
5114
|
+
});
|
|
5115
|
+
const committed = await invoke<FeatureConfig>("get_feature_config");
|
|
5116
|
+
if (version === screenMemoryMutationVersionRef.current) {
|
|
5117
|
+
screenMemoryRef.current = committed.screenMemory;
|
|
5118
|
+
setScreenMemory(committed.screenMemory);
|
|
5119
|
+
}
|
|
5120
|
+
});
|
|
5121
|
+
screenMemoryMutationTailRef.current = operation;
|
|
5122
|
+
try {
|
|
5123
|
+
await operation;
|
|
5124
|
+
} catch (err) {
|
|
3996
5125
|
console.error("[settings] set_feature_config failed", err);
|
|
5126
|
+
if (version === screenMemoryMutationVersionRef.current) {
|
|
5127
|
+
const committed = await invoke<FeatureConfig>(
|
|
5128
|
+
"get_feature_config",
|
|
5129
|
+
).catch(() => null);
|
|
5130
|
+
if (committed) {
|
|
5131
|
+
screenMemoryRef.current = committed.screenMemory;
|
|
5132
|
+
setScreenMemory(committed.screenMemory);
|
|
5133
|
+
}
|
|
5134
|
+
}
|
|
3997
5135
|
setScreenMemoryMessage({
|
|
3998
5136
|
kind: "error",
|
|
3999
|
-
text: (err as Error)?.message ?? "Could not update
|
|
5137
|
+
text: (err as Error)?.message ?? "Could not update Rewind.",
|
|
4000
5138
|
});
|
|
4001
|
-
}
|
|
5139
|
+
} finally {
|
|
5140
|
+
screenMemoryMutationRef.current = Math.max(
|
|
5141
|
+
0,
|
|
5142
|
+
screenMemoryMutationRef.current - 1,
|
|
5143
|
+
);
|
|
5144
|
+
if (screenMemoryMutationRef.current === 0) {
|
|
5145
|
+
setScreenMemoryConfigBusy(false);
|
|
5146
|
+
}
|
|
5147
|
+
}
|
|
5148
|
+
}
|
|
5149
|
+
|
|
5150
|
+
async function installRewindAgentConnection(client: "codex" | "claude-code") {
|
|
5151
|
+
setAgentConnectionBusy(client);
|
|
5152
|
+
setAgentConnectionMessage(null);
|
|
5153
|
+
try {
|
|
5154
|
+
const status = await invoke<RewindAgentConnectionStatus>(
|
|
5155
|
+
"screen_memory_install_agent_connection",
|
|
5156
|
+
{ client },
|
|
5157
|
+
);
|
|
5158
|
+
setAgentConnectionMessage({
|
|
5159
|
+
kind: "ok",
|
|
5160
|
+
text: `${client === "codex" ? "Codex" : "Claude Code"} is connected to this Rewind store. Restart the agent app once to load it.`,
|
|
5161
|
+
});
|
|
5162
|
+
console.info(
|
|
5163
|
+
`[clips-tray] configured ${status.client} Screen Memory MCP at ${status.configPath}`,
|
|
5164
|
+
);
|
|
5165
|
+
} catch (err) {
|
|
5166
|
+
setAgentConnectionMessage({
|
|
5167
|
+
kind: "error",
|
|
5168
|
+
text: err instanceof Error ? err.message : String(err),
|
|
5169
|
+
});
|
|
5170
|
+
} finally {
|
|
5171
|
+
setAgentConnectionBusy(null);
|
|
5172
|
+
}
|
|
4002
5173
|
}
|
|
4003
5174
|
|
|
4004
5175
|
function refreshScreenMemoryStatus() {
|
|
@@ -4007,22 +5178,71 @@ function Setup({
|
|
|
4007
5178
|
.catch(() => {});
|
|
4008
5179
|
}
|
|
4009
5180
|
|
|
5181
|
+
function refreshRewindEgressLog() {
|
|
5182
|
+
invoke<RewindEgressEvent[]>("rewind_list_evidence_egress", { limit: 20 })
|
|
5183
|
+
.then(setRewindEgressEvents)
|
|
5184
|
+
.catch((err) => {
|
|
5185
|
+
setScreenMemoryMessage({
|
|
5186
|
+
kind: "error",
|
|
5187
|
+
text:
|
|
5188
|
+
(err as Error)?.message ?? "Could not read the Rewind access log.",
|
|
5189
|
+
});
|
|
5190
|
+
});
|
|
5191
|
+
}
|
|
5192
|
+
|
|
5193
|
+
async function askRewindLocally() {
|
|
5194
|
+
const query = rewindLocalQuery.trim();
|
|
5195
|
+
if (!query) return;
|
|
5196
|
+
setRewindLocalBusy(true);
|
|
5197
|
+
setRewindLocalError(null);
|
|
5198
|
+
try {
|
|
5199
|
+
const result = await invoke<RewindLocalAskResult>("rewind_local_ask", {
|
|
5200
|
+
query,
|
|
5201
|
+
limit: 12,
|
|
5202
|
+
});
|
|
5203
|
+
setRewindLocalResult(result);
|
|
5204
|
+
} catch (err) {
|
|
5205
|
+
setRewindLocalError(
|
|
5206
|
+
(err as Error)?.message ?? "Could not search local Rewind evidence.",
|
|
5207
|
+
);
|
|
5208
|
+
} finally {
|
|
5209
|
+
setRewindLocalBusy(false);
|
|
5210
|
+
}
|
|
5211
|
+
}
|
|
5212
|
+
|
|
5213
|
+
async function replayRewindMoment(
|
|
5214
|
+
evidence: RewindLocalAskResult["evidence"][number],
|
|
5215
|
+
) {
|
|
5216
|
+
setRewindReplayId(evidence.id);
|
|
5217
|
+
setRewindLocalError(null);
|
|
5218
|
+
try {
|
|
5219
|
+
await invoke("rewind_replay_moment", {
|
|
5220
|
+
segmentId: evidence.segmentId,
|
|
5221
|
+
offsetMs: evidence.offsetMs,
|
|
5222
|
+
});
|
|
5223
|
+
} catch (err) {
|
|
5224
|
+
setRewindLocalError(
|
|
5225
|
+
(err as Error)?.message ?? "Could not replay this local moment.",
|
|
5226
|
+
);
|
|
5227
|
+
} finally {
|
|
5228
|
+
setRewindReplayId(null);
|
|
5229
|
+
}
|
|
5230
|
+
}
|
|
5231
|
+
|
|
4010
5232
|
async function exportScreenMemoryRecent() {
|
|
4011
5233
|
setScreenMemoryBusy(true);
|
|
4012
5234
|
setScreenMemoryMessage(null);
|
|
5235
|
+
setScreenMemoryExportResult(null);
|
|
4013
5236
|
try {
|
|
4014
5237
|
const result = await invoke<ScreenMemoryExportResult>(
|
|
4015
5238
|
"screen_memory_export_recent",
|
|
4016
5239
|
{ minutes: 5 },
|
|
4017
5240
|
);
|
|
4018
|
-
|
|
4019
|
-
kind: "ok",
|
|
4020
|
-
text: `Saved ${result.files.length} segment${result.files.length === 1 ? "" : "s"} to ${result.folderPath}.`,
|
|
4021
|
-
});
|
|
5241
|
+
setScreenMemoryExportResult(result);
|
|
4022
5242
|
} catch (err) {
|
|
4023
5243
|
setScreenMemoryMessage({
|
|
4024
5244
|
kind: "error",
|
|
4025
|
-
text:
|
|
5245
|
+
text: err instanceof Error ? err.message : String(err),
|
|
4026
5246
|
});
|
|
4027
5247
|
} finally {
|
|
4028
5248
|
setScreenMemoryBusy(false);
|
|
@@ -4038,11 +5258,11 @@ function Setup({
|
|
|
4038
5258
|
"screen_memory_delete_all",
|
|
4039
5259
|
);
|
|
4040
5260
|
setScreenMemoryStatus(status);
|
|
4041
|
-
setScreenMemoryMessage({ kind: "ok", text: "
|
|
5261
|
+
setScreenMemoryMessage({ kind: "ok", text: "Rewind cleared." });
|
|
4042
5262
|
} catch (err) {
|
|
4043
5263
|
setScreenMemoryMessage({
|
|
4044
5264
|
kind: "error",
|
|
4045
|
-
text: (err as Error)?.message ?? "Could not clear
|
|
5265
|
+
text: (err as Error)?.message ?? "Could not clear Rewind.",
|
|
4046
5266
|
});
|
|
4047
5267
|
} finally {
|
|
4048
5268
|
setScreenMemoryBusy(false);
|
|
@@ -4053,8 +5273,42 @@ function Setup({
|
|
|
4053
5273
|
invoke("screen_memory_open_folder").catch((err) => {
|
|
4054
5274
|
setScreenMemoryMessage({
|
|
4055
5275
|
kind: "error",
|
|
4056
|
-
text: (err as Error)?.message ?? "Could not open
|
|
5276
|
+
text: (err as Error)?.message ?? "Could not open Rewind folder.",
|
|
5277
|
+
});
|
|
5278
|
+
});
|
|
5279
|
+
}
|
|
5280
|
+
|
|
5281
|
+
async function chooseExcludedApplications() {
|
|
5282
|
+
setExcludedAppsBusy(true);
|
|
5283
|
+
setScreenMemoryMessage(null);
|
|
5284
|
+
try {
|
|
5285
|
+
const chosen = await invoke<RewindExcludedApplication[]>(
|
|
5286
|
+
"choose_rewind_excluded_apps",
|
|
5287
|
+
);
|
|
5288
|
+
if (chosen.length === 0) return;
|
|
5289
|
+
const bundleIds = [
|
|
5290
|
+
...new Set([
|
|
5291
|
+
...(screenMemory.excludedBundleIds ?? []),
|
|
5292
|
+
...chosen.map((app) => app.bundleId),
|
|
5293
|
+
]),
|
|
5294
|
+
];
|
|
5295
|
+
await setScreenMemoryConfig({ excludedBundleIds: bundleIds });
|
|
5296
|
+
} catch (err) {
|
|
5297
|
+
setScreenMemoryMessage({
|
|
5298
|
+
kind: "error",
|
|
5299
|
+
text: (err as Error)?.message ?? "Could not choose applications.",
|
|
4057
5300
|
});
|
|
5301
|
+
} finally {
|
|
5302
|
+
setExcludedAppsBusy(false);
|
|
5303
|
+
}
|
|
5304
|
+
}
|
|
5305
|
+
|
|
5306
|
+
function removeExcludedApplications(bundleIds: string[]) {
|
|
5307
|
+
const removed = new Set(bundleIds);
|
|
5308
|
+
void setScreenMemoryConfig({
|
|
5309
|
+
excludedBundleIds: (screenMemory.excludedBundleIds ?? []).filter(
|
|
5310
|
+
(candidate) => !removed.has(candidate),
|
|
5311
|
+
),
|
|
4058
5312
|
});
|
|
4059
5313
|
}
|
|
4060
5314
|
|
|
@@ -4291,8 +5545,7 @@ function Setup({
|
|
|
4291
5545
|
};
|
|
4292
5546
|
}, [serverUrl, initial]);
|
|
4293
5547
|
|
|
4294
|
-
function
|
|
4295
|
-
e.preventDefault();
|
|
5548
|
+
function handleConnect() {
|
|
4296
5549
|
const trimmed = url.trim();
|
|
4297
5550
|
if (!trimmed) return;
|
|
4298
5551
|
onConnect(trimmed);
|
|
@@ -4456,38 +5709,741 @@ function Setup({
|
|
|
4456
5709
|
});
|
|
4457
5710
|
}
|
|
4458
5711
|
|
|
4459
|
-
|
|
4460
|
-
|
|
4461
|
-
<div className="setup-
|
|
4462
|
-
|
|
5712
|
+
if (surface !== "settings" && rewindConsentOpen) {
|
|
5713
|
+
return (
|
|
5714
|
+
<div className="setup rewind-consent">
|
|
5715
|
+
<div className="rewind-consent-mark">
|
|
5716
|
+
<IconHistory size={24} stroke={1.8} />
|
|
5717
|
+
</div>
|
|
5718
|
+
<p className="rewind-kicker">Rewind</p>
|
|
5719
|
+
<h2>Turn on Rewind?</h2>
|
|
5720
|
+
<p className="rewind-consent-copy">
|
|
5721
|
+
Rewind remembers recent moments so you can ask an agent about them or
|
|
5722
|
+
add earlier context to a Clip.
|
|
5723
|
+
</p>
|
|
5724
|
+
<div className="rewind-consent-choices">
|
|
4463
5725
|
<button
|
|
4464
5726
|
type="button"
|
|
4465
|
-
className="
|
|
4466
|
-
onClick={
|
|
4467
|
-
aria-label="Back"
|
|
5727
|
+
className={rewindConsentMode === "visuals" ? "selected" : ""}
|
|
5728
|
+
onClick={() => setRewindConsentMode("visuals")}
|
|
4468
5729
|
>
|
|
4469
|
-
<
|
|
5730
|
+
<strong>Visuals</strong>
|
|
5731
|
+
<span>Screen, app, and readable text</span>
|
|
4470
5732
|
</button>
|
|
4471
|
-
|
|
4472
|
-
|
|
4473
|
-
|
|
4474
|
-
|
|
4475
|
-
|
|
4476
|
-
|
|
4477
|
-
|
|
4478
|
-
|
|
4479
|
-
|
|
4480
|
-
|
|
4481
|
-
|
|
4482
|
-
|
|
4483
|
-
|
|
4484
|
-
|
|
4485
|
-
|
|
4486
|
-
|
|
5733
|
+
<button
|
|
5734
|
+
type="button"
|
|
5735
|
+
className={rewindConsentMode === "visuals-audio" ? "selected" : ""}
|
|
5736
|
+
onClick={() => setRewindConsentMode("visuals-audio")}
|
|
5737
|
+
>
|
|
5738
|
+
<strong>Visuals + audio</strong>
|
|
5739
|
+
<span>Also remember your microphone and sound from the Mac</span>
|
|
5740
|
+
</button>
|
|
5741
|
+
</div>
|
|
5742
|
+
<div className="rewind-local-promise">
|
|
5743
|
+
<IconShieldLock size={17} stroke={1.8} />
|
|
5744
|
+
<p>
|
|
5745
|
+
<strong>Raw Rewind recordings stay on this Mac.</strong> When you
|
|
5746
|
+
ask an agent to search, Clips returns bounded matching context. A
|
|
5747
|
+
media range uploads only through a private Clip handoff.
|
|
5748
|
+
</p>
|
|
5749
|
+
</div>
|
|
5750
|
+
<button
|
|
5751
|
+
type="button"
|
|
5752
|
+
className="primary rewind-consent-primary"
|
|
5753
|
+
disabled={screenMemoryConfigBusy}
|
|
5754
|
+
onClick={async () => {
|
|
5755
|
+
await setScreenMemoryConfig({
|
|
5756
|
+
enabled: true,
|
|
5757
|
+
paused: false,
|
|
5758
|
+
captureMode: rewindConsentMode,
|
|
5759
|
+
retentionHours: 8,
|
|
5760
|
+
maxBytes: 20 * 1024 * 1024 * 1024,
|
|
5761
|
+
reviewBeforeSending: true,
|
|
5762
|
+
agentClipRetention: "forever",
|
|
5763
|
+
});
|
|
5764
|
+
setRewindConsentOpen(false);
|
|
5765
|
+
}}
|
|
5766
|
+
>
|
|
5767
|
+
{screenMemoryConfigBusy ? "Turning on…" : "Turn on Rewind"}
|
|
5768
|
+
</button>
|
|
5769
|
+
<button
|
|
5770
|
+
type="button"
|
|
5771
|
+
className="rewind-quiet-button"
|
|
5772
|
+
onClick={() => {
|
|
5773
|
+
setRewindConsentOpen(false);
|
|
5774
|
+
onCancel?.();
|
|
5775
|
+
}}
|
|
5776
|
+
>
|
|
5777
|
+
Not now
|
|
5778
|
+
</button>
|
|
5779
|
+
</div>
|
|
5780
|
+
);
|
|
5781
|
+
}
|
|
5782
|
+
|
|
5783
|
+
if (surface === "memory") {
|
|
5784
|
+
return (
|
|
5785
|
+
<div className="setup popover-view rewind-memory-surface">
|
|
5786
|
+
<div className="setup-header">
|
|
5787
|
+
<button
|
|
5788
|
+
type="button"
|
|
5789
|
+
className="setup-back"
|
|
5790
|
+
onClick={onCancel}
|
|
5791
|
+
aria-label="Back"
|
|
5792
|
+
>
|
|
5793
|
+
<IconArrowLeft size={18} stroke={1.75} />
|
|
5794
|
+
</button>
|
|
5795
|
+
<h2>Manual search</h2>
|
|
5796
|
+
</div>
|
|
5797
|
+
{screenMemory.enabled ? (
|
|
5798
|
+
<>
|
|
5799
|
+
<p className="rewind-surface-lede">
|
|
5800
|
+
This local fallback helps you verify what Rewind remembers. For
|
|
5801
|
+
everyday retrieval, ask Codex or your connected agent instead.
|
|
5802
|
+
</p>
|
|
5803
|
+
<form
|
|
5804
|
+
className="rewind-search-row"
|
|
5805
|
+
onSubmit={(event) => {
|
|
5806
|
+
event.preventDefault();
|
|
5807
|
+
void askRewindLocally();
|
|
5808
|
+
}}
|
|
5809
|
+
>
|
|
5810
|
+
<IconSearch size={17} stroke={1.8} />
|
|
5811
|
+
<input
|
|
5812
|
+
autoFocus
|
|
5813
|
+
value={rewindLocalQuery}
|
|
5814
|
+
onChange={(event) => setRewindLocalQuery(event.target.value)}
|
|
5815
|
+
placeholder="Search words you saw or heard…"
|
|
5816
|
+
aria-label="Search memory"
|
|
5817
|
+
maxLength={500}
|
|
5818
|
+
/>
|
|
5819
|
+
<button
|
|
5820
|
+
type="submit"
|
|
5821
|
+
disabled={rewindLocalBusy || !rewindLocalQuery.trim()}
|
|
5822
|
+
>
|
|
5823
|
+
{rewindLocalBusy ? "Searching…" : "Search"}
|
|
5824
|
+
</button>
|
|
5825
|
+
</form>
|
|
5826
|
+
{rewindLocalError ? (
|
|
5827
|
+
<p className="setup-warning">{rewindLocalError}</p>
|
|
5828
|
+
) : null}
|
|
5829
|
+
{rewindLocalResult ? (
|
|
5830
|
+
<div className="rewind-search-results" aria-live="polite">
|
|
5831
|
+
<div className="rewind-result-summary">
|
|
5832
|
+
<strong>
|
|
5833
|
+
{rewindLocalResult.evidence.length} moment
|
|
5834
|
+
{rewindLocalResult.evidence.length === 1 ? "" : "s"} found
|
|
5835
|
+
</strong>
|
|
5836
|
+
<span>
|
|
5837
|
+
{rewindLocalResult.coverage.segmentsConsidered} local
|
|
5838
|
+
segments searched
|
|
5839
|
+
</span>
|
|
5840
|
+
</div>
|
|
5841
|
+
{rewindLocalResult.evidence.length === 0 ? (
|
|
5842
|
+
<div className="popover-empty-card">
|
|
5843
|
+
<strong>No matching moments</strong>
|
|
5844
|
+
<p>
|
|
5845
|
+
Try an app name, a phrase you heard, or text that appeared
|
|
5846
|
+
on screen.
|
|
5847
|
+
</p>
|
|
5848
|
+
</div>
|
|
5849
|
+
) : (
|
|
5850
|
+
rewindLocalResult.evidence.map((evidence) => (
|
|
5851
|
+
<article className="rewind-evidence-card" key={evidence.id}>
|
|
5852
|
+
<div className="rewind-evidence-meta">
|
|
5853
|
+
{new Date(evidence.capturedAt).toLocaleString()} ·{" "}
|
|
5854
|
+
{evidence.sourceType === "ocr"
|
|
5855
|
+
? "On-screen text"
|
|
5856
|
+
: evidence.sourceType === "transcript"
|
|
5857
|
+
? "Audio"
|
|
5858
|
+
: "App context"}
|
|
5859
|
+
</div>
|
|
5860
|
+
<p>{evidence.excerpt}</p>
|
|
5861
|
+
<button
|
|
5862
|
+
type="button"
|
|
5863
|
+
className="secondary"
|
|
5864
|
+
onClick={() => void replayRewindMoment(evidence)}
|
|
5865
|
+
disabled={rewindReplayId === evidence.id}
|
|
5866
|
+
>
|
|
5867
|
+
<IconPlayerPlay size={14} stroke={1.9} />
|
|
5868
|
+
{rewindReplayId === evidence.id
|
|
5869
|
+
? "Preparing replay…"
|
|
5870
|
+
: "Replay this moment"}
|
|
5871
|
+
</button>
|
|
5872
|
+
</article>
|
|
5873
|
+
))
|
|
5874
|
+
)}
|
|
5875
|
+
<details className="setup-advanced rewind-coverage-details">
|
|
5876
|
+
<summary className="setup-advanced-summary">
|
|
5877
|
+
Coverage and gaps
|
|
5878
|
+
</summary>
|
|
5879
|
+
<div className="setup-advanced-body">
|
|
5880
|
+
<p className="setup-hint">
|
|
5881
|
+
{rewindLocalResult.coverage.transcriptIndexesReady} audio
|
|
5882
|
+
and {rewindLocalResult.coverage.ocrIndexesReady} visual
|
|
5883
|
+
indexes were ready. Confidence:{" "}
|
|
5884
|
+
{rewindLocalResult.confidence}.
|
|
5885
|
+
</p>
|
|
5886
|
+
{rewindLocalResult.coverage.gaps.length === 0 ? (
|
|
5887
|
+
<p className="setup-hint">
|
|
5888
|
+
No known capture or index gaps.
|
|
5889
|
+
</p>
|
|
5890
|
+
) : (
|
|
5891
|
+
rewindLocalResult.coverage.gaps.map((gap, index) => (
|
|
5892
|
+
<p
|
|
5893
|
+
className="setup-hint"
|
|
5894
|
+
key={`${gap.kind}-${gap.source}-${gap.startedAt ?? index}`}
|
|
5895
|
+
>
|
|
5896
|
+
<strong>{gap.source}</strong> · {gap.detail}
|
|
5897
|
+
</p>
|
|
5898
|
+
))
|
|
5899
|
+
)}
|
|
5900
|
+
</div>
|
|
5901
|
+
</details>
|
|
5902
|
+
</div>
|
|
5903
|
+
) : (
|
|
5904
|
+
<div className="popover-empty-card rewind-memory-empty">
|
|
5905
|
+
<IconSearch size={19} stroke={1.7} />
|
|
5906
|
+
<strong>Find the source moment</strong>
|
|
5907
|
+
<p>
|
|
5908
|
+
Results stay grounded in retained local evidence and always
|
|
5909
|
+
lead back to Replay.
|
|
5910
|
+
</p>
|
|
5911
|
+
</div>
|
|
5912
|
+
)}
|
|
5913
|
+
</>
|
|
5914
|
+
) : (
|
|
5915
|
+
<div className="popover-empty-card rewind-memory-empty">
|
|
5916
|
+
<IconHistory size={20} stroke={1.7} />
|
|
5917
|
+
<strong>Rewind is off</strong>
|
|
5918
|
+
<p>
|
|
5919
|
+
Turn on a private rolling memory before there is anything to
|
|
5920
|
+
search.
|
|
5921
|
+
</p>
|
|
5922
|
+
<button
|
|
5923
|
+
type="button"
|
|
5924
|
+
className="secondary"
|
|
5925
|
+
onClick={() => setRewindConsentOpen(true)}
|
|
5926
|
+
>
|
|
5927
|
+
Turn on Rewind
|
|
5928
|
+
</button>
|
|
5929
|
+
</div>
|
|
5930
|
+
)}
|
|
5931
|
+
</div>
|
|
5932
|
+
);
|
|
5933
|
+
}
|
|
5934
|
+
|
|
5935
|
+
if (surface === "rewind") {
|
|
5936
|
+
return (
|
|
5937
|
+
<div className="setup popover-view rewind-settings-surface">
|
|
5938
|
+
<div className="setup-header">
|
|
5939
|
+
<button
|
|
5940
|
+
type="button"
|
|
5941
|
+
className="setup-back"
|
|
5942
|
+
onClick={onCancel}
|
|
5943
|
+
aria-label="Back"
|
|
5944
|
+
>
|
|
5945
|
+
<IconArrowLeft size={18} stroke={1.75} />
|
|
5946
|
+
</button>
|
|
5947
|
+
<h2>Rewind settings</h2>
|
|
5948
|
+
</div>
|
|
5949
|
+
<div className="rewind-setting-row">
|
|
5950
|
+
<SettingLabel
|
|
5951
|
+
label="Rewind"
|
|
5952
|
+
hint={
|
|
5953
|
+
!screenMemory.enabled
|
|
5954
|
+
? "Off"
|
|
5955
|
+
: screenMemory.paused
|
|
5956
|
+
? "Paused · existing local memory is still available"
|
|
5957
|
+
: "Remembering locally"
|
|
5958
|
+
}
|
|
5959
|
+
/>
|
|
5960
|
+
<Switch
|
|
5961
|
+
on={screenMemory.enabled}
|
|
5962
|
+
disabled={screenMemoryConfigBusy || captureControlsLocked}
|
|
5963
|
+
onChange={(enabled) => {
|
|
5964
|
+
if (enabled) setRewindConsentOpen(true);
|
|
5965
|
+
else
|
|
5966
|
+
void setScreenMemoryConfig({ enabled: false, paused: false });
|
|
5967
|
+
}}
|
|
5968
|
+
label="Enable Rewind"
|
|
5969
|
+
/>
|
|
5970
|
+
</div>
|
|
5971
|
+
{captureControlsLocked ? (
|
|
5972
|
+
<p className="rewind-capture-lock-note" role="status">
|
|
5973
|
+
Rewind capture settings unlock when this Clip ends. This keeps one
|
|
5974
|
+
screen and audio recorder running at a time.
|
|
5975
|
+
</p>
|
|
5976
|
+
) : null}
|
|
5977
|
+
{screenMemory.enabled ? (
|
|
5978
|
+
<>
|
|
5979
|
+
<div className="rewind-setting-row">
|
|
5980
|
+
<SettingLabel
|
|
5981
|
+
label="Remember"
|
|
5982
|
+
hint="Choose whether audio joins the local screen memory."
|
|
5983
|
+
htmlFor="rewind-capture-mode"
|
|
5984
|
+
/>
|
|
5985
|
+
<select
|
|
5986
|
+
id="rewind-capture-mode"
|
|
5987
|
+
className="setup-select rewind-setting-control"
|
|
5988
|
+
disabled={screenMemoryConfigBusy || captureControlsLocked}
|
|
5989
|
+
value={screenMemory.captureMode}
|
|
5990
|
+
onChange={(event) =>
|
|
5991
|
+
void setScreenMemoryConfig({
|
|
5992
|
+
captureMode: event.target.value as
|
|
5993
|
+
| "visuals"
|
|
5994
|
+
| "visuals-audio",
|
|
5995
|
+
})
|
|
5996
|
+
}
|
|
5997
|
+
>
|
|
5998
|
+
<option value="visuals">Visuals</option>
|
|
5999
|
+
<option value="visuals-audio">Visuals + audio</option>
|
|
6000
|
+
</select>
|
|
6001
|
+
</div>
|
|
6002
|
+
<div className="rewind-setting-row">
|
|
6003
|
+
<SettingLabel
|
|
6004
|
+
label="Remember the last…"
|
|
6005
|
+
hint={`${formatStorageBytes(screenMemory.maxBytes)} maximum on disk`}
|
|
6006
|
+
htmlFor="rewind-retention"
|
|
6007
|
+
/>
|
|
6008
|
+
<select
|
|
6009
|
+
id="rewind-retention"
|
|
6010
|
+
className="setup-select rewind-setting-control"
|
|
6011
|
+
disabled={screenMemoryConfigBusy}
|
|
6012
|
+
value={screenMemory.retentionHours}
|
|
6013
|
+
onChange={(event) =>
|
|
6014
|
+
void setScreenMemoryConfig({
|
|
6015
|
+
retentionHours: Number(event.target.value),
|
|
6016
|
+
})
|
|
6017
|
+
}
|
|
6018
|
+
>
|
|
6019
|
+
<option value={8}>8 hours</option>
|
|
6020
|
+
<option value={24}>24 hours</option>
|
|
6021
|
+
</select>
|
|
6022
|
+
</div>
|
|
6023
|
+
<div className="rewind-setting-row">
|
|
6024
|
+
<SettingLabel
|
|
6025
|
+
label="Keep up to"
|
|
6026
|
+
hint="Older moments are removed automatically when this limit is reached."
|
|
6027
|
+
htmlFor="rewind-disk-cap"
|
|
6028
|
+
/>
|
|
6029
|
+
<select
|
|
6030
|
+
id="rewind-disk-cap"
|
|
6031
|
+
className="setup-select rewind-setting-control"
|
|
6032
|
+
disabled={screenMemoryConfigBusy}
|
|
6033
|
+
value={screenMemory.maxBytes}
|
|
6034
|
+
onChange={(event) =>
|
|
6035
|
+
void setScreenMemoryConfig({
|
|
6036
|
+
maxBytes: Number(event.target.value),
|
|
6037
|
+
})
|
|
6038
|
+
}
|
|
6039
|
+
>
|
|
6040
|
+
<option value={5 * 1024 * 1024 * 1024}>5 GB</option>
|
|
6041
|
+
<option value={20 * 1024 * 1024 * 1024}>20 GB</option>
|
|
6042
|
+
<option value={50 * 1024 * 1024 * 1024}>50 GB</option>
|
|
6043
|
+
</select>
|
|
6044
|
+
</div>
|
|
6045
|
+
<div className="rewind-settings-status">
|
|
6046
|
+
<span
|
|
6047
|
+
className={`rewind-home-dot ${screenMemoryRecording ? "is-live" : ""}`}
|
|
6048
|
+
/>
|
|
6049
|
+
<span>
|
|
6050
|
+
{screenMemoryStatus?.exclusionActive
|
|
6051
|
+
? screenMemoryStatus.coverage
|
|
6052
|
+
: screenMemoryRecording
|
|
6053
|
+
? `${screenMemorySegments.length} retained segment${screenMemorySegments.length === 1 ? "" : "s"} · ${formatStorageBytes(screenMemoryTotalBytes)}`
|
|
6054
|
+
: screenMemory.paused
|
|
6055
|
+
? "Paused. Existing memory remains available."
|
|
6056
|
+
: screenMemoryStatus?.lastError ||
|
|
6057
|
+
"Waiting for screen-recording permission."}
|
|
6058
|
+
</span>
|
|
6059
|
+
</div>
|
|
6060
|
+
<div className="setup-section-heading">Privacy</div>
|
|
6061
|
+
<div className="rewind-excluded-apps">
|
|
6062
|
+
<div className="rewind-excluded-apps-header">
|
|
6063
|
+
<SettingLabel
|
|
6064
|
+
label="Excluded apps"
|
|
6065
|
+
hint="Rewind never remembers these applications."
|
|
6066
|
+
/>
|
|
6067
|
+
<button
|
|
6068
|
+
type="button"
|
|
6069
|
+
className="secondary rewind-choose-apps"
|
|
6070
|
+
disabled={excludedAppsBusy || screenMemoryConfigBusy}
|
|
6071
|
+
onClick={() => void chooseExcludedApplications()}
|
|
6072
|
+
>
|
|
6073
|
+
<IconFolderOpen size={14} stroke={1.9} />
|
|
6074
|
+
{excludedAppsBusy ? "Choosing…" : "Choose applications…"}
|
|
6075
|
+
</button>
|
|
6076
|
+
</div>
|
|
6077
|
+
{excludedAppGroups.length > 0 ? (
|
|
6078
|
+
<div className="rewind-excluded-app-list">
|
|
6079
|
+
{excludedAppGroups.map((app) => (
|
|
6080
|
+
<div
|
|
6081
|
+
className={`rewind-excluded-app ${app.installed ? "" : "is-missing"}`}
|
|
6082
|
+
key={app.bundleId}
|
|
6083
|
+
>
|
|
6084
|
+
<div
|
|
6085
|
+
className="rewind-excluded-app-mark"
|
|
6086
|
+
aria-hidden="true"
|
|
6087
|
+
>
|
|
6088
|
+
{app.name.slice(0, 1).toUpperCase()}
|
|
6089
|
+
</div>
|
|
6090
|
+
<div className="rewind-excluded-app-copy">
|
|
6091
|
+
<strong>{app.name}</strong>
|
|
6092
|
+
<span>
|
|
6093
|
+
{app.installed
|
|
6094
|
+
? "Excluded"
|
|
6095
|
+
: "Not currently installed · still excluded"}
|
|
6096
|
+
</span>
|
|
6097
|
+
</div>
|
|
6098
|
+
<button
|
|
6099
|
+
type="button"
|
|
6100
|
+
className="rewind-excluded-app-remove"
|
|
6101
|
+
aria-label={`Remove ${app.name}`}
|
|
6102
|
+
disabled={screenMemoryConfigBusy}
|
|
6103
|
+
onClick={() =>
|
|
6104
|
+
removeExcludedApplications(app.bundleIds)
|
|
6105
|
+
}
|
|
6106
|
+
>
|
|
6107
|
+
<IconX size={14} stroke={2} />
|
|
6108
|
+
</button>
|
|
6109
|
+
</div>
|
|
6110
|
+
))}
|
|
6111
|
+
</div>
|
|
6112
|
+
) : (
|
|
6113
|
+
<p className="setup-hint">No additional apps are excluded.</p>
|
|
6114
|
+
)}
|
|
6115
|
+
</div>
|
|
6116
|
+
<div className="setup-section-heading">Agent handoff</div>
|
|
6117
|
+
<div className="rewind-agent-guide">
|
|
6118
|
+
<div className="rewind-agent-guide-icon">
|
|
6119
|
+
<IconHistory size={17} stroke={1.8} />
|
|
6120
|
+
</div>
|
|
6121
|
+
<div>
|
|
6122
|
+
<strong>Set up your agent once</strong>
|
|
6123
|
+
<p>
|
|
6124
|
+
Copy the setup prompt into any compatible agent. It installs
|
|
6125
|
+
Rewind's reusable instructions and repairs the local
|
|
6126
|
+
connection, so later you can simply say “Look at Rewind.”
|
|
6127
|
+
</p>
|
|
6128
|
+
</div>
|
|
6129
|
+
</div>
|
|
6130
|
+
<details className="setup-advanced rewind-agent-repair">
|
|
6131
|
+
<summary className="setup-advanced-summary">
|
|
6132
|
+
Repair an agent connection
|
|
6133
|
+
</summary>
|
|
6134
|
+
<div className="setup-advanced-body">
|
|
6135
|
+
<p className="setup-hint">
|
|
6136
|
+
Usually your agent can install Rewind from the copied prompt.
|
|
6137
|
+
These buttons are a manual repair for known clients.
|
|
6138
|
+
</p>
|
|
6139
|
+
<div className="rewind-memory-actions">
|
|
6140
|
+
<button
|
|
6141
|
+
type="button"
|
|
6142
|
+
className="secondary"
|
|
6143
|
+
disabled={agentConnectionBusy !== null}
|
|
6144
|
+
onClick={() => void installRewindAgentConnection("codex")}
|
|
6145
|
+
>
|
|
6146
|
+
{agentConnectionBusy === "codex"
|
|
6147
|
+
? "Repairing…"
|
|
6148
|
+
: "Repair Codex connection"}
|
|
6149
|
+
</button>
|
|
6150
|
+
<button
|
|
6151
|
+
type="button"
|
|
6152
|
+
className="secondary"
|
|
6153
|
+
disabled={agentConnectionBusy !== null}
|
|
6154
|
+
onClick={() =>
|
|
6155
|
+
void installRewindAgentConnection("claude-code")
|
|
6156
|
+
}
|
|
6157
|
+
>
|
|
6158
|
+
{agentConnectionBusy === "claude-code"
|
|
6159
|
+
? "Repairing…"
|
|
6160
|
+
: "Repair Claude Code connection"}
|
|
6161
|
+
</button>
|
|
6162
|
+
</div>
|
|
6163
|
+
{agentConnectionMessage ? (
|
|
6164
|
+
<p
|
|
6165
|
+
className={
|
|
6166
|
+
agentConnectionMessage.kind === "error"
|
|
6167
|
+
? "setup-error"
|
|
6168
|
+
: "setup-hint"
|
|
6169
|
+
}
|
|
6170
|
+
role="status"
|
|
6171
|
+
>
|
|
6172
|
+
{agentConnectionMessage.text}
|
|
6173
|
+
</p>
|
|
6174
|
+
) : null}
|
|
6175
|
+
</div>
|
|
6176
|
+
</details>
|
|
6177
|
+
<div className="rewind-setting-row rewind-agent-row">
|
|
6178
|
+
<SettingLabel
|
|
6179
|
+
label="Review before sending"
|
|
6180
|
+
hint="Preview and trim any visual or audio range before it becomes a private Clip."
|
|
6181
|
+
/>
|
|
6182
|
+
<Switch
|
|
6183
|
+
on={screenMemory.reviewBeforeSending}
|
|
6184
|
+
disabled={screenMemoryConfigBusy}
|
|
6185
|
+
onChange={(enabled) =>
|
|
6186
|
+
void setScreenMemoryConfig({
|
|
6187
|
+
reviewBeforeSending: enabled,
|
|
6188
|
+
})
|
|
6189
|
+
}
|
|
6190
|
+
label="Review visual and audio ranges before sending"
|
|
6191
|
+
/>
|
|
6192
|
+
</div>
|
|
6193
|
+
<div className="rewind-setting-row rewind-agent-row">
|
|
6194
|
+
<SettingLabel
|
|
6195
|
+
label="Open local preview automatically"
|
|
6196
|
+
hint="When review is on, prepare the selected range in QuickTime when an agent asks for it."
|
|
6197
|
+
/>
|
|
6198
|
+
<Switch
|
|
6199
|
+
on={screenMemory.autoPreviewBeforeSending}
|
|
6200
|
+
disabled={
|
|
6201
|
+
screenMemoryConfigBusy || !screenMemory.reviewBeforeSending
|
|
6202
|
+
}
|
|
6203
|
+
onChange={(enabled) =>
|
|
6204
|
+
void setScreenMemoryConfig({
|
|
6205
|
+
autoPreviewBeforeSending: enabled,
|
|
6206
|
+
})
|
|
6207
|
+
}
|
|
6208
|
+
label="Open a local preview automatically before sending"
|
|
6209
|
+
/>
|
|
6210
|
+
</div>
|
|
6211
|
+
<p className="rewind-boundary-note">
|
|
6212
|
+
Asking your agent authorizes bounded matching text. Raw Rewind
|
|
6213
|
+
files stay local. If this review is off, an agent-requested media
|
|
6214
|
+
range becomes a private Clip immediately and leaves a receipt.
|
|
6215
|
+
</p>
|
|
6216
|
+
<div className="rewind-setting-row">
|
|
6217
|
+
<SettingLabel
|
|
6218
|
+
label="Agent-created Clip retention"
|
|
6219
|
+
hint="Applies to future private Clips created for an agent."
|
|
6220
|
+
htmlFor="rewind-agent-clip-retention"
|
|
6221
|
+
/>
|
|
6222
|
+
<select
|
|
6223
|
+
id="rewind-agent-clip-retention"
|
|
6224
|
+
className="setup-select rewind-setting-control"
|
|
6225
|
+
disabled={screenMemoryConfigBusy}
|
|
6226
|
+
value={screenMemory.agentClipRetention}
|
|
6227
|
+
onChange={(event) =>
|
|
6228
|
+
void setScreenMemoryConfig({
|
|
6229
|
+
agentClipRetention: event.target.value as
|
|
6230
|
+
| "forever"
|
|
6231
|
+
| "24-hours"
|
|
6232
|
+
| "7-days"
|
|
6233
|
+
| "30-days",
|
|
6234
|
+
})
|
|
6235
|
+
}
|
|
6236
|
+
>
|
|
6237
|
+
<option value="forever">Keep forever</option>
|
|
6238
|
+
<option value="24-hours">Delete after 24 hours</option>
|
|
6239
|
+
<option value="7-days">Delete after 7 days</option>
|
|
6240
|
+
<option value="30-days">Delete after 30 days</option>
|
|
6241
|
+
</select>
|
|
6242
|
+
</div>
|
|
6243
|
+
<div className="rewind-agent-guide">
|
|
6244
|
+
<div className="rewind-agent-guide-icon">
|
|
6245
|
+
<IconHistory size={17} stroke={1.8} />
|
|
6246
|
+
</div>
|
|
6247
|
+
<div>
|
|
6248
|
+
<strong>Ask your agent</strong>
|
|
6249
|
+
<p>
|
|
6250
|
+
Try “What was the Terminal error?” or “Replay Tuesday’s design
|
|
6251
|
+
review.”
|
|
6252
|
+
</p>
|
|
6253
|
+
<button
|
|
6254
|
+
type="button"
|
|
6255
|
+
className="rewind-text-button"
|
|
6256
|
+
onClick={onOpenMemory}
|
|
6257
|
+
>
|
|
6258
|
+
Search manually
|
|
6259
|
+
</button>
|
|
6260
|
+
</div>
|
|
6261
|
+
</div>
|
|
6262
|
+
<details
|
|
6263
|
+
className="setup-advanced"
|
|
6264
|
+
open={rewindEgressOpen}
|
|
6265
|
+
onToggle={(event) => {
|
|
6266
|
+
const open = event.currentTarget.open;
|
|
6267
|
+
setRewindEgressOpen(open);
|
|
6268
|
+
if (open) refreshRewindEgressLog();
|
|
6269
|
+
}}
|
|
6270
|
+
>
|
|
6271
|
+
<summary className="setup-advanced-summary">
|
|
6272
|
+
Agent activity
|
|
6273
|
+
</summary>
|
|
6274
|
+
<div className="setup-advanced-body">
|
|
6275
|
+
<p className="setup-hint">
|
|
6276
|
+
See when an agent searched bounded local evidence. Raw Rewind
|
|
6277
|
+
media remains on this Mac unless you explicitly create a
|
|
6278
|
+
private Clip.
|
|
6279
|
+
</p>
|
|
6280
|
+
{rewindEgressEvents.length === 0 ? (
|
|
6281
|
+
<p className="setup-hint">No matching-text requests yet.</p>
|
|
6282
|
+
) : (
|
|
6283
|
+
rewindEgressEvents.slice(0, 10).map((event) => (
|
|
6284
|
+
<p
|
|
6285
|
+
className="setup-hint"
|
|
6286
|
+
key={`${event.requestId}-${event.state}`}
|
|
6287
|
+
>
|
|
6288
|
+
<strong>
|
|
6289
|
+
{new Date(event.occurredAt).toLocaleString()}
|
|
6290
|
+
</strong>
|
|
6291
|
+
{` · ${event.state} · ${event.evidenceCount} item${event.evidenceCount === 1 ? "" : "s"}`}
|
|
6292
|
+
</p>
|
|
6293
|
+
))
|
|
6294
|
+
)}
|
|
6295
|
+
</div>
|
|
6296
|
+
</details>
|
|
6297
|
+
<details className="setup-advanced">
|
|
6298
|
+
<summary className="setup-advanced-summary">
|
|
6299
|
+
Manage local memory
|
|
6300
|
+
</summary>
|
|
6301
|
+
<div className="setup-advanced-body">
|
|
6302
|
+
<div className="rewind-memory-actions">
|
|
6303
|
+
<div className="rewind-memory-action">
|
|
6304
|
+
<div className="rewind-memory-action-copy">
|
|
6305
|
+
<strong>Save a local Clip</strong>
|
|
6306
|
+
<p>
|
|
6307
|
+
Export the previous five minutes as a video on this Mac.
|
|
6308
|
+
Nothing is uploaded.
|
|
6309
|
+
</p>
|
|
6310
|
+
{screenMemoryExportResult ? (
|
|
6311
|
+
<div className="rewind-inline-receipt" role="status">
|
|
6312
|
+
<span>Saved locally</span>
|
|
6313
|
+
<button
|
|
6314
|
+
type="button"
|
|
6315
|
+
className="rewind-text-button"
|
|
6316
|
+
onClick={() =>
|
|
6317
|
+
void invoke("open_local_recording_folder", {
|
|
6318
|
+
path: screenMemoryExportResult.folderPath,
|
|
6319
|
+
})
|
|
6320
|
+
}
|
|
6321
|
+
>
|
|
6322
|
+
Show in Finder
|
|
6323
|
+
</button>
|
|
6324
|
+
</div>
|
|
6325
|
+
) : null}
|
|
6326
|
+
</div>
|
|
6327
|
+
<button
|
|
6328
|
+
type="button"
|
|
6329
|
+
className="secondary"
|
|
6330
|
+
disabled={
|
|
6331
|
+
screenMemoryBusy || screenMemorySegments.length === 0
|
|
6332
|
+
}
|
|
6333
|
+
onClick={exportScreenMemoryRecent}
|
|
6334
|
+
>
|
|
6335
|
+
<IconDownload size={15} stroke={1.9} /> Save previous 5
|
|
6336
|
+
minutes
|
|
6337
|
+
</button>
|
|
6338
|
+
</div>
|
|
6339
|
+
<div className="rewind-memory-action">
|
|
6340
|
+
<div>
|
|
6341
|
+
<strong>View Rewind files</strong>
|
|
6342
|
+
<p>
|
|
6343
|
+
Open the private folder where Rewind keeps its temporary
|
|
6344
|
+
local memory.
|
|
6345
|
+
</p>
|
|
6346
|
+
</div>
|
|
6347
|
+
<button
|
|
6348
|
+
type="button"
|
|
6349
|
+
className="secondary"
|
|
6350
|
+
onClick={openScreenMemoryFolder}
|
|
6351
|
+
>
|
|
6352
|
+
<IconFolderOpen size={15} stroke={1.9} /> Open Rewind
|
|
6353
|
+
folder
|
|
6354
|
+
</button>
|
|
6355
|
+
</div>
|
|
6356
|
+
<div className="rewind-memory-action is-danger">
|
|
6357
|
+
<div>
|
|
6358
|
+
<strong>Erase Rewind memory</strong>
|
|
6359
|
+
<p>
|
|
6360
|
+
Permanently delete all retained media and indexes from
|
|
6361
|
+
this Mac.
|
|
6362
|
+
</p>
|
|
6363
|
+
</div>
|
|
6364
|
+
<button
|
|
6365
|
+
type="button"
|
|
6366
|
+
className="secondary rewind-danger-button"
|
|
6367
|
+
disabled={
|
|
6368
|
+
screenMemoryBusy || screenMemorySegments.length === 0
|
|
6369
|
+
}
|
|
6370
|
+
onClick={clearScreenMemory}
|
|
6371
|
+
>
|
|
6372
|
+
<IconTrash size={15} stroke={1.9} /> Erase all memory…
|
|
6373
|
+
</button>
|
|
6374
|
+
</div>
|
|
6375
|
+
</div>
|
|
6376
|
+
</div>
|
|
6377
|
+
</details>
|
|
6378
|
+
{screenMemoryMessage ? (
|
|
6379
|
+
<p
|
|
6380
|
+
className={
|
|
6381
|
+
screenMemoryMessage.kind === "ok"
|
|
6382
|
+
? "setup-success"
|
|
6383
|
+
: "setup-warning"
|
|
6384
|
+
}
|
|
6385
|
+
>
|
|
6386
|
+
{screenMemoryMessage.text}
|
|
6387
|
+
</p>
|
|
6388
|
+
) : null}
|
|
6389
|
+
</>
|
|
6390
|
+
) : (
|
|
6391
|
+
<div className="popover-empty-card rewind-memory-empty">
|
|
6392
|
+
<IconHistory size={20} stroke={1.7} />
|
|
6393
|
+
<strong>Nothing is being remembered</strong>
|
|
6394
|
+
<p>
|
|
6395
|
+
Turning Rewind on begins a private rolling memory after you choose
|
|
6396
|
+
what it may remember.
|
|
6397
|
+
</p>
|
|
6398
|
+
<button
|
|
6399
|
+
type="button"
|
|
6400
|
+
className="secondary"
|
|
6401
|
+
onClick={() => setRewindConsentOpen(true)}
|
|
6402
|
+
>
|
|
6403
|
+
Choose what to remember
|
|
6404
|
+
</button>
|
|
6405
|
+
</div>
|
|
6406
|
+
)}
|
|
6407
|
+
</div>
|
|
6408
|
+
);
|
|
6409
|
+
}
|
|
6410
|
+
|
|
6411
|
+
return (
|
|
6412
|
+
<div className="setup">
|
|
6413
|
+
<div className="setup-header">
|
|
6414
|
+
{onCancel ? (
|
|
6415
|
+
<button
|
|
6416
|
+
type="button"
|
|
6417
|
+
className="setup-back"
|
|
6418
|
+
onClick={onCancel}
|
|
6419
|
+
aria-label="Back"
|
|
6420
|
+
>
|
|
6421
|
+
<IconArrowLeft size={18} stroke={1.75} />
|
|
6422
|
+
</button>
|
|
6423
|
+
) : null}
|
|
6424
|
+
<h2>Settings</h2>
|
|
6425
|
+
</div>
|
|
6426
|
+
|
|
6427
|
+
<div className="setup-section-heading">General</div>
|
|
6428
|
+
|
|
6429
|
+
<div className="setup-section">
|
|
6430
|
+
<SettingLabel
|
|
6431
|
+
label="Clips server URL"
|
|
6432
|
+
hint="The URL of the Clips backend this tray app connects to."
|
|
6433
|
+
htmlFor="clips-url"
|
|
6434
|
+
/>
|
|
6435
|
+
<input
|
|
6436
|
+
id="clips-url"
|
|
6437
|
+
type="url"
|
|
6438
|
+
value={url}
|
|
4487
6439
|
onChange={(e) => setUrl(e.target.value)}
|
|
4488
6440
|
placeholder="http://localhost:8080"
|
|
4489
6441
|
/>
|
|
4490
|
-
<button
|
|
6442
|
+
<button
|
|
6443
|
+
className="secondary setup-connect-button"
|
|
6444
|
+
type="button"
|
|
6445
|
+
onClick={handleConnect}
|
|
6446
|
+
>
|
|
4491
6447
|
Connect
|
|
4492
6448
|
</button>
|
|
4493
6449
|
</div>
|
|
@@ -4524,7 +6480,7 @@ function Setup({
|
|
|
4524
6480
|
<div className="setup-toggle-row">
|
|
4525
6481
|
<SettingLabel
|
|
4526
6482
|
label="Show Clips in screen captures"
|
|
4527
|
-
hint="
|
|
6483
|
+
hint="When off, Clips windows and recording overlays stay out of screenshots, normal screen recordings, and Rewind. Turn on only for debugging or demos."
|
|
4528
6484
|
/>
|
|
4529
6485
|
<Switch
|
|
4530
6486
|
on={showInScreenCapture}
|
|
@@ -4591,6 +6547,27 @@ function Setup({
|
|
|
4591
6547
|
|
|
4592
6548
|
<div className="setup-section-heading">Recording</div>
|
|
4593
6549
|
|
|
6550
|
+
<div className="setup-section rewind-settings-entry">
|
|
6551
|
+
<div className="rewind-settings-entry-main">
|
|
6552
|
+
<span
|
|
6553
|
+
className={`rewind-home-dot ${screenMemoryRecording ? "is-live" : ""}`}
|
|
6554
|
+
/>
|
|
6555
|
+
<div>
|
|
6556
|
+
<strong>Rewind</strong>
|
|
6557
|
+
<p className="setup-hint">
|
|
6558
|
+
{!screenMemory.enabled
|
|
6559
|
+
? "Off"
|
|
6560
|
+
: screenMemory.paused
|
|
6561
|
+
? "Paused · existing local memory is still available"
|
|
6562
|
+
: "Remembering locally"}
|
|
6563
|
+
</p>
|
|
6564
|
+
</div>
|
|
6565
|
+
</div>
|
|
6566
|
+
<button type="button" className="secondary" onClick={onOpenRewind}>
|
|
6567
|
+
Rewind settings
|
|
6568
|
+
</button>
|
|
6569
|
+
</div>
|
|
6570
|
+
|
|
4594
6571
|
<div className="setup-section">
|
|
4595
6572
|
<SettingLabel
|
|
4596
6573
|
label="Clip Drafts"
|
|
@@ -4609,31 +6586,37 @@ function Setup({
|
|
|
4609
6586
|
) : null}
|
|
4610
6587
|
</div>
|
|
4611
6588
|
|
|
4612
|
-
<div className="setup-section">
|
|
6589
|
+
<div className="setup-section setup-rewind-legacy">
|
|
4613
6590
|
<div className="setup-toggle-row">
|
|
4614
6591
|
<SettingLabel
|
|
4615
|
-
label="
|
|
4616
|
-
hint="
|
|
6592
|
+
label="Rewind"
|
|
6593
|
+
hint="A disabled-by-default rolling local archive for recent screen and app context. It never becomes a shared Clip on its own."
|
|
4617
6594
|
/>
|
|
4618
6595
|
<Switch
|
|
4619
6596
|
on={screenMemory.enabled}
|
|
4620
6597
|
onChange={(enabled) =>
|
|
4621
|
-
setScreenMemoryConfig({ enabled, paused: false })
|
|
6598
|
+
void setScreenMemoryConfig({ enabled, paused: false })
|
|
4622
6599
|
}
|
|
4623
|
-
label="Enable
|
|
6600
|
+
label="Enable Rewind"
|
|
6601
|
+
disabled={screenMemoryConfigBusy || captureControlsLocked}
|
|
4624
6602
|
/>
|
|
4625
6603
|
</div>
|
|
6604
|
+
<p className="setup-hint">
|
|
6605
|
+
Local-only by default. Media uploads and sharing happen only when you
|
|
6606
|
+
make a normal Clip.
|
|
6607
|
+
</p>
|
|
4626
6608
|
{screenMemory.enabled ? (
|
|
4627
6609
|
<>
|
|
4628
6610
|
<div className="setup-toggle-row">
|
|
4629
6611
|
<SettingLabel
|
|
4630
6612
|
label="Pause capture"
|
|
4631
|
-
hint="Stop
|
|
6613
|
+
hint="Stop retaining new Rewind segments without clearing the local archive."
|
|
4632
6614
|
/>
|
|
4633
6615
|
<Switch
|
|
4634
6616
|
on={screenMemory.paused}
|
|
4635
|
-
onChange={(paused) => setScreenMemoryConfig({ paused })}
|
|
4636
|
-
label="Pause
|
|
6617
|
+
onChange={(paused) => void setScreenMemoryConfig({ paused })}
|
|
6618
|
+
label="Pause Rewind"
|
|
6619
|
+
disabled={screenMemoryConfigBusy || captureControlsLocked}
|
|
4637
6620
|
/>
|
|
4638
6621
|
</div>
|
|
4639
6622
|
<div className="setup-grid">
|
|
@@ -4641,6 +6624,7 @@ function Setup({
|
|
|
4641
6624
|
<span>Retention</span>
|
|
4642
6625
|
<select
|
|
4643
6626
|
className="setup-select"
|
|
6627
|
+
disabled={screenMemoryConfigBusy || captureControlsLocked}
|
|
4644
6628
|
value={screenMemory.retentionHours}
|
|
4645
6629
|
onChange={(event) =>
|
|
4646
6630
|
setScreenMemoryConfig({
|
|
@@ -4650,13 +6634,13 @@ function Setup({
|
|
|
4650
6634
|
>
|
|
4651
6635
|
<option value={8}>8 hours</option>
|
|
4652
6636
|
<option value={24}>24 hours</option>
|
|
4653
|
-
<option value={72}>72 hours</option>
|
|
4654
6637
|
</select>
|
|
4655
6638
|
</label>
|
|
4656
6639
|
<label className="setup-mini-field">
|
|
4657
6640
|
<span>Disk cap</span>
|
|
4658
6641
|
<select
|
|
4659
6642
|
className="setup-select"
|
|
6643
|
+
disabled={screenMemoryConfigBusy}
|
|
4660
6644
|
value={screenMemory.maxBytes}
|
|
4661
6645
|
onChange={(event) =>
|
|
4662
6646
|
setScreenMemoryConfig({
|
|
@@ -4670,6 +6654,89 @@ function Setup({
|
|
|
4670
6654
|
</select>
|
|
4671
6655
|
</label>
|
|
4672
6656
|
</div>
|
|
6657
|
+
<div className="setup-grid">
|
|
6658
|
+
<label className="setup-mini-field">
|
|
6659
|
+
<span>Capture mode</span>
|
|
6660
|
+
<select
|
|
6661
|
+
className="setup-select"
|
|
6662
|
+
disabled={screenMemoryConfigBusy}
|
|
6663
|
+
value={screenMemory.captureMode}
|
|
6664
|
+
onChange={(event) =>
|
|
6665
|
+
setScreenMemoryConfig({
|
|
6666
|
+
captureMode: event.target.value as
|
|
6667
|
+
| "visuals"
|
|
6668
|
+
| "visuals-audio",
|
|
6669
|
+
})
|
|
6670
|
+
}
|
|
6671
|
+
>
|
|
6672
|
+
<option value="visuals">Visuals</option>
|
|
6673
|
+
<option value="visuals-audio">Visuals + audio</option>
|
|
6674
|
+
</select>
|
|
6675
|
+
</label>
|
|
6676
|
+
<label className="setup-mini-field">
|
|
6677
|
+
<span>Agent handoff review</span>
|
|
6678
|
+
<select
|
|
6679
|
+
className="setup-select"
|
|
6680
|
+
disabled={screenMemoryConfigBusy}
|
|
6681
|
+
value={screenMemory.reviewBeforeSending ? "review" : "direct"}
|
|
6682
|
+
onChange={(event) =>
|
|
6683
|
+
setScreenMemoryConfig({
|
|
6684
|
+
reviewBeforeSending: event.target.value === "review",
|
|
6685
|
+
})
|
|
6686
|
+
}
|
|
6687
|
+
>
|
|
6688
|
+
<option value="review">Review before sending</option>
|
|
6689
|
+
<option value="direct">Send requested range directly</option>
|
|
6690
|
+
</select>
|
|
6691
|
+
</label>
|
|
6692
|
+
</div>
|
|
6693
|
+
<p className="setup-hint">
|
|
6694
|
+
{screenMemory.captureMode === "visuals-audio"
|
|
6695
|
+
? "Visuals + audio is configured to retain microphone and system audio as separate local tracks."
|
|
6696
|
+
: "Visuals retains screen and app context without selecting audio capture."}
|
|
6697
|
+
</p>
|
|
6698
|
+
<p className="setup-hint">
|
|
6699
|
+
Agents receive bounded matching text when you ask. Raw Rewind
|
|
6700
|
+
media remains local unless a bounded range becomes a private Clip.
|
|
6701
|
+
</p>
|
|
6702
|
+
<label className="setup-mini-field">
|
|
6703
|
+
<span>Excluded apps</span>
|
|
6704
|
+
<input
|
|
6705
|
+
value={excludedBundleIdsInput}
|
|
6706
|
+
onChange={(event) =>
|
|
6707
|
+
setExcludedBundleIdsInput(event.target.value)
|
|
6708
|
+
}
|
|
6709
|
+
onBlur={() =>
|
|
6710
|
+
setScreenMemoryConfig({
|
|
6711
|
+
excludedBundleIds: parseExcludedBundleIds(
|
|
6712
|
+
excludedBundleIdsInput,
|
|
6713
|
+
),
|
|
6714
|
+
})
|
|
6715
|
+
}
|
|
6716
|
+
placeholder="com.example.private-app, com.example.vault"
|
|
6717
|
+
aria-describedby="rewind-excluded-apps-hint"
|
|
6718
|
+
/>
|
|
6719
|
+
</label>
|
|
6720
|
+
<button
|
|
6721
|
+
type="button"
|
|
6722
|
+
className="secondary"
|
|
6723
|
+
disabled={screenMemoryConfigBusy}
|
|
6724
|
+
onClick={() =>
|
|
6725
|
+
void setScreenMemoryConfig({
|
|
6726
|
+
excludedBundleIds: parseExcludedBundleIds(
|
|
6727
|
+
excludedBundleIdsInput,
|
|
6728
|
+
),
|
|
6729
|
+
})
|
|
6730
|
+
}
|
|
6731
|
+
>
|
|
6732
|
+
Apply exclusions
|
|
6733
|
+
</button>
|
|
6734
|
+
<p id="rewind-excluded-apps-hint" className="setup-hint">
|
|
6735
|
+
Bundle IDs, comma-separated. Password managers are excluded by
|
|
6736
|
+
default. Recognized bundle IDs stop media capture and discard the
|
|
6737
|
+
entire in-flight segment; apps that do not expose a recognized
|
|
6738
|
+
bundle ID cannot be detected.
|
|
6739
|
+
</p>
|
|
4673
6740
|
<div className="whisper-status">
|
|
4674
6741
|
{screenMemoryRecording ? (
|
|
4675
6742
|
<IconCircleCheck size={13} className="whisper-status-icon" />
|
|
@@ -4677,22 +6744,134 @@ function Setup({
|
|
|
4677
6744
|
<IconAlertTriangle size={13} className="whisper-status-icon" />
|
|
4678
6745
|
)}
|
|
4679
6746
|
<span>
|
|
4680
|
-
{
|
|
4681
|
-
?
|
|
4682
|
-
:
|
|
4683
|
-
?
|
|
4684
|
-
:
|
|
4685
|
-
?
|
|
4686
|
-
:
|
|
6747
|
+
{screenMemoryStatus?.exclusionActive
|
|
6748
|
+
? screenMemoryStatus.coverage
|
|
6749
|
+
: screenMemoryRecording
|
|
6750
|
+
? `Rewind is retaining local coverage: ${screenMemorySegments.length} segment${screenMemorySegments.length === 1 ? "" : "s"}, ${formatStorageBytes(screenMemoryTotalBytes)}.`
|
|
6751
|
+
: screenMemory.paused
|
|
6752
|
+
? "Capture paused. Existing local coverage remains available."
|
|
6753
|
+
: screenMemoryStatus?.lastError
|
|
6754
|
+
? screenMemoryStatus.lastError
|
|
6755
|
+
: "Capture status is waiting for screen-recording permission."}
|
|
4687
6756
|
</span>
|
|
4688
6757
|
</div>
|
|
4689
6758
|
{screenMemorySegments[0] ? (
|
|
4690
6759
|
<p className="setup-hint">
|
|
4691
|
-
Latest
|
|
6760
|
+
Latest local coverage:{" "}
|
|
4692
6761
|
{new Date(screenMemorySegments[0].endedAt).toLocaleTimeString()}
|
|
4693
6762
|
.
|
|
4694
6763
|
</p>
|
|
4695
6764
|
) : null}
|
|
6765
|
+
<div className="setup-advanced-body">
|
|
6766
|
+
<SettingLabel
|
|
6767
|
+
label="Ask Rewind"
|
|
6768
|
+
hint="Searches app context, local transcripts, and local visual text on this Mac. Private mode allows this because no model or network service is called."
|
|
6769
|
+
/>
|
|
6770
|
+
<div className="setup-button-row">
|
|
6771
|
+
<input
|
|
6772
|
+
value={rewindLocalQuery}
|
|
6773
|
+
onChange={(event) => setRewindLocalQuery(event.target.value)}
|
|
6774
|
+
placeholder="What was Lilian saying about the audience?"
|
|
6775
|
+
aria-label="Ask Rewind locally"
|
|
6776
|
+
maxLength={500}
|
|
6777
|
+
onKeyDown={(event) => {
|
|
6778
|
+
if (event.key !== "Enter") return;
|
|
6779
|
+
event.preventDefault();
|
|
6780
|
+
event.stopPropagation();
|
|
6781
|
+
void askRewindLocally();
|
|
6782
|
+
}}
|
|
6783
|
+
/>
|
|
6784
|
+
<button
|
|
6785
|
+
type="button"
|
|
6786
|
+
className="secondary"
|
|
6787
|
+
disabled={rewindLocalBusy || !rewindLocalQuery.trim()}
|
|
6788
|
+
onClick={() => void askRewindLocally()}
|
|
6789
|
+
>
|
|
6790
|
+
{rewindLocalBusy ? "Searching…" : "Search locally"}
|
|
6791
|
+
</button>
|
|
6792
|
+
</div>
|
|
6793
|
+
{rewindLocalError ? (
|
|
6794
|
+
<p className="setup-warning">{rewindLocalError}</p>
|
|
6795
|
+
) : null}
|
|
6796
|
+
{rewindLocalResult ? (
|
|
6797
|
+
<div aria-live="polite">
|
|
6798
|
+
<p className="setup-hint">
|
|
6799
|
+
<strong>Local answer:</strong>{" "}
|
|
6800
|
+
{rewindLocalResult.answerSummary}
|
|
6801
|
+
</p>
|
|
6802
|
+
<p className="setup-hint">
|
|
6803
|
+
<strong>Confidence:</strong> {rewindLocalResult.confidence}
|
|
6804
|
+
</p>
|
|
6805
|
+
<p className="setup-hint">
|
|
6806
|
+
<strong>Coverage:</strong>{" "}
|
|
6807
|
+
{rewindLocalResult.coverage.segmentsConsidered} retained
|
|
6808
|
+
segment
|
|
6809
|
+
{rewindLocalResult.coverage.segmentsConsidered === 1
|
|
6810
|
+
? ""
|
|
6811
|
+
: "s"}
|
|
6812
|
+
; {rewindLocalResult.coverage.transcriptIndexesReady}{" "}
|
|
6813
|
+
transcript and {rewindLocalResult.coverage.ocrIndexesReady}{" "}
|
|
6814
|
+
visual indexes ready.
|
|
6815
|
+
{rewindLocalResult.coverage.gaps.length > 0
|
|
6816
|
+
? ` ${rewindLocalResult.coverage.gaps.length} capture or index gap${rewindLocalResult.coverage.gaps.length === 1 ? "" : "s"} may hide matches.`
|
|
6817
|
+
: " No known capture or index gaps."}
|
|
6818
|
+
</p>
|
|
6819
|
+
{rewindLocalResult.coverage.gaps.length > 0 ? (
|
|
6820
|
+
<details className="setup-advanced">
|
|
6821
|
+
<summary className="setup-advanced-summary">
|
|
6822
|
+
Coverage gaps
|
|
6823
|
+
</summary>
|
|
6824
|
+
<div className="setup-advanced-body">
|
|
6825
|
+
{rewindLocalResult.coverage.gaps
|
|
6826
|
+
.slice(0, 10)
|
|
6827
|
+
.map((gap, index) => (
|
|
6828
|
+
<p
|
|
6829
|
+
className="setup-hint"
|
|
6830
|
+
key={`${gap.kind}-${gap.source}-${gap.startedAt ?? index}`}
|
|
6831
|
+
>
|
|
6832
|
+
<strong>{gap.source}</strong> · {gap.detail}
|
|
6833
|
+
</p>
|
|
6834
|
+
))}
|
|
6835
|
+
</div>
|
|
6836
|
+
</details>
|
|
6837
|
+
) : null}
|
|
6838
|
+
{rewindLocalResult.evidence.length === 0 ? (
|
|
6839
|
+
<p className="setup-hint">No matching evidence to show.</p>
|
|
6840
|
+
) : (
|
|
6841
|
+
rewindLocalResult.evidence.map((evidence) => (
|
|
6842
|
+
<div className="setup-section" key={evidence.id}>
|
|
6843
|
+
<p className="setup-hint">
|
|
6844
|
+
<strong>{evidence.sourceType}</strong> ·{" "}
|
|
6845
|
+
{new Date(evidence.capturedAt).toLocaleString()}
|
|
6846
|
+
{typeof evidence.confidence === "number"
|
|
6847
|
+
? ` · ${Math.round(evidence.confidence * 100)}% OCR confidence`
|
|
6848
|
+
: ""}
|
|
6849
|
+
<br />
|
|
6850
|
+
{evidence.excerpt}
|
|
6851
|
+
</p>
|
|
6852
|
+
<button
|
|
6853
|
+
type="button"
|
|
6854
|
+
className="secondary"
|
|
6855
|
+
onClick={() => void replayRewindMoment(evidence)}
|
|
6856
|
+
disabled={rewindReplayId === evidence.id}
|
|
6857
|
+
>
|
|
6858
|
+
<IconExternalLink size={14} stroke={1.9} />
|
|
6859
|
+
{rewindReplayId === evidence.id
|
|
6860
|
+
? "Preparing replay…"
|
|
6861
|
+
: "Replay moment"}
|
|
6862
|
+
</button>
|
|
6863
|
+
</div>
|
|
6864
|
+
))
|
|
6865
|
+
)}
|
|
6866
|
+
{rewindLocalResult.truncated ? (
|
|
6867
|
+
<p className="setup-hint">
|
|
6868
|
+
More local matches exist; results are bounded to the
|
|
6869
|
+
strongest 12.
|
|
6870
|
+
</p>
|
|
6871
|
+
) : null}
|
|
6872
|
+
</div>
|
|
6873
|
+
) : null}
|
|
6874
|
+
</div>
|
|
4696
6875
|
<div className="setup-button-row">
|
|
4697
6876
|
<button
|
|
4698
6877
|
type="button"
|
|
@@ -4718,9 +6897,89 @@ function Setup({
|
|
|
4718
6897
|
disabled={screenMemoryBusy || screenMemorySegments.length === 0}
|
|
4719
6898
|
>
|
|
4720
6899
|
<IconTrash size={15} stroke={1.9} />
|
|
4721
|
-
Clear
|
|
6900
|
+
Clear Rewind
|
|
4722
6901
|
</button>
|
|
4723
6902
|
</div>
|
|
6903
|
+
<details
|
|
6904
|
+
className="setup-advanced"
|
|
6905
|
+
open={rewindEgressOpen}
|
|
6906
|
+
onToggle={(event) => {
|
|
6907
|
+
const open = event.currentTarget.open;
|
|
6908
|
+
setRewindEgressOpen(open);
|
|
6909
|
+
if (open) refreshRewindEgressLog();
|
|
6910
|
+
}}
|
|
6911
|
+
>
|
|
6912
|
+
<summary className="setup-advanced-summary">
|
|
6913
|
+
Agent access log
|
|
6914
|
+
</summary>
|
|
6915
|
+
<div className="setup-advanced-body">
|
|
6916
|
+
<p className="setup-hint">
|
|
6917
|
+
Every bounded Rewind evidence request is recorded on this Mac
|
|
6918
|
+
before matching text is returned. Raw media appears only as a
|
|
6919
|
+
separate private Clip handoff.
|
|
6920
|
+
</p>
|
|
6921
|
+
{rewindEgressEvents.length === 0 ? (
|
|
6922
|
+
<p className="setup-hint">No evidence requests yet.</p>
|
|
6923
|
+
) : (
|
|
6924
|
+
rewindEgressEvents.slice(0, 10).map((event) => (
|
|
6925
|
+
<details
|
|
6926
|
+
className="setup-advanced"
|
|
6927
|
+
key={`${event.requestId}-${event.state}`}
|
|
6928
|
+
>
|
|
6929
|
+
<summary className="popover-kv">
|
|
6930
|
+
<span>
|
|
6931
|
+
{new Date(event.occurredAt).toLocaleString()} ·{" "}
|
|
6932
|
+
{event.state}
|
|
6933
|
+
</span>
|
|
6934
|
+
<strong>
|
|
6935
|
+
{event.evidenceCount} item
|
|
6936
|
+
{event.evidenceCount === 1 ? "" : "s"}
|
|
6937
|
+
</strong>
|
|
6938
|
+
</summary>
|
|
6939
|
+
<div className="setup-advanced-body">
|
|
6940
|
+
<p className="setup-hint">
|
|
6941
|
+
<strong>{event.operation ?? "Agent access"}</strong>
|
|
6942
|
+
{" · "}Request {event.requestId}
|
|
6943
|
+
</p>
|
|
6944
|
+
{event.receipt?.evidence?.map((evidence) => (
|
|
6945
|
+
<p className="setup-hint" key={evidence.id}>
|
|
6946
|
+
<strong>{evidence.sourceType}</strong>
|
|
6947
|
+
{evidence.capturedAt
|
|
6948
|
+
? ` · ${new Date(evidence.capturedAt).toLocaleTimeString()}`
|
|
6949
|
+
: ""}
|
|
6950
|
+
<br />
|
|
6951
|
+
Evidence {evidence.id} · moment {evidence.momentId}
|
|
6952
|
+
</p>
|
|
6953
|
+
))}
|
|
6954
|
+
{event.receipt?.frames?.map((frame) => (
|
|
6955
|
+
<p className="setup-hint" key={frame.timestamp}>
|
|
6956
|
+
<strong>Local frame</strong>
|
|
6957
|
+
{` · ${new Date(frame.timestamp).toLocaleTimeString()}`}
|
|
6958
|
+
<br />
|
|
6959
|
+
Segment {frame.segmentId}
|
|
6960
|
+
</p>
|
|
6961
|
+
))}
|
|
6962
|
+
{event.receipt?.mediaInterval ? (
|
|
6963
|
+
<p className="setup-hint">
|
|
6964
|
+
<strong>Private Clip range</strong>
|
|
6965
|
+
{` · ${new Date(event.receipt.mediaInterval.startAt).toLocaleTimeString()}–${new Date(event.receipt.mediaInterval.endAt).toLocaleTimeString()}`}
|
|
6966
|
+
</p>
|
|
6967
|
+
) : null}
|
|
6968
|
+
{!event.receipt ? (
|
|
6969
|
+
<p className="setup-hint">
|
|
6970
|
+
This completion record refers to the prepared
|
|
6971
|
+
receipt with request ID {event.requestId}.
|
|
6972
|
+
</p>
|
|
6973
|
+
) : null}
|
|
6974
|
+
{event.error ? (
|
|
6975
|
+
<p className="setup-warning">{event.error}</p>
|
|
6976
|
+
) : null}
|
|
6977
|
+
</div>
|
|
6978
|
+
</details>
|
|
6979
|
+
))
|
|
6980
|
+
)}
|
|
6981
|
+
</div>
|
|
6982
|
+
</details>
|
|
4724
6983
|
{screenMemoryMessage ? (
|
|
4725
6984
|
<p
|
|
4726
6985
|
className={
|
|
@@ -5184,7 +7443,7 @@ function Setup({
|
|
|
5184
7443
|
</button>
|
|
5185
7444
|
</div>
|
|
5186
7445
|
) : null}
|
|
5187
|
-
</
|
|
7446
|
+
</div>
|
|
5188
7447
|
);
|
|
5189
7448
|
}
|
|
5190
7449
|
|