@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.
Files changed (141) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +6 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/cli/mcp-config-writers.ts +6 -4
  5. package/corpus/core/src/cli/mcp.ts +124 -0
  6. package/corpus/core/src/cli/skills-content/help.ts +4 -3
  7. package/corpus/core/src/cli/skills-content/index.ts +1 -0
  8. package/corpus/core/src/cli/skills-content/rewind-skill.ts +52 -0
  9. package/corpus/core/src/cli/skills.ts +115 -13
  10. package/corpus/core/src/mcp/screen-memory-stdio.ts +1360 -104
  11. package/corpus/core/src/mcp-client/index.ts +9 -0
  12. package/corpus/core/src/mcp-client/screen-memory-local.ts +847 -13
  13. package/corpus/templates/clips/actions/apply-rewind-extension.ts +195 -0
  14. package/corpus/templates/clips/actions/delete-agent-recording-if-unpromoted.ts +64 -0
  15. package/corpus/templates/clips/actions/get-rewind-extension-request.ts +22 -0
  16. package/corpus/templates/clips/actions/get-screen-memory-status.ts +18 -2
  17. package/corpus/templates/clips/actions/list-rewind-extension-requests.ts +52 -0
  18. package/corpus/templates/clips/actions/make-recording-private-for-rewind.ts +55 -0
  19. package/corpus/templates/clips/actions/query-screen-memory-context.ts +4 -4
  20. package/corpus/templates/clips/actions/request-rewind-extension.ts +81 -0
  21. package/corpus/templates/clips/actions/update-rewind-extension-request.ts +47 -0
  22. package/corpus/templates/clips/app/components/editor/editor-layout.tsx +25 -0
  23. package/corpus/templates/clips/app/components/editor/editor-toolbar.tsx +36 -0
  24. package/corpus/templates/clips/app/components/editor/rewind-extension-dialog.tsx +338 -0
  25. package/corpus/templates/clips/app/components/editor/stitch-manager.tsx +1 -16
  26. package/corpus/templates/clips/app/components/editor/timeline.tsx +26 -0
  27. package/corpus/templates/clips/app/i18n/en-US.ts +18 -0
  28. package/corpus/templates/clips/app/lib/ffmpeg-export.ts +19 -12
  29. package/corpus/templates/clips/app/lib/rewind-visibility.ts +31 -0
  30. package/corpus/templates/clips/app/lib/timestamp-mapping.ts +7 -0
  31. package/corpus/templates/clips/app/lib/upload-file-client.ts +17 -0
  32. package/corpus/templates/clips/changelog/2026-07-14-rewind-keeps-recent-screen-context-local-and-turns-it-into-clips.md +6 -0
  33. package/corpus/templates/clips/changelog/2026-07-17-active-clips-keep-rewind-stable-and-settings-clear.md +12 -0
  34. package/corpus/templates/clips/changelog/2026-07-17-clicking-the-clips-icon-during-a-recording-now-opens-the-app.md +6 -0
  35. package/corpus/templates/clips/changelog/2026-07-17-default-mic-now-uses-the-mac-default-and-transcription-shares-one-capture.md +6 -0
  36. package/corpus/templates/clips/changelog/2026-07-17-full-screen-clips-finalize-even-when-screencapturekit-stop-hangs.md +6 -0
  37. package/corpus/templates/clips/changelog/2026-07-17-normal-full-screen-clips-no-longer-stop-immediately-after-th.md +6 -0
  38. package/corpus/templates/clips/changelog/2026-07-17-rewind-can-hand-one-bounded-private-clip-to-your-agent.md +12 -0
  39. package/corpus/templates/clips/changelog/2026-07-17-rewind-home-can-copy-a-local-agent-prompt.md +6 -0
  40. package/corpus/templates/clips/changelog/2026-07-17-rewind-now-has-a-calm-home-status-focused-first-run-consent-.md +6 -0
  41. package/corpus/templates/clips/changelog/2026-07-17-rewind-settings-are-smaller-clearer-and-agent-first.md +6 -0
  42. package/corpus/templates/clips/changelog/2026-07-17-the-full-clips-interface-stays-available-while-recording.md +6 -0
  43. package/corpus/templates/clips/changelog/2026-07-18-rewind-organizes-recent-work-into-local-searchable-chapters.md +8 -0
  44. package/corpus/templates/clips/changelog/2026-07-18-searchable-rewind-chapters.md +6 -0
  45. package/corpus/templates/clips/changelog/2026-07-19-rewind-backed-recordings-now-start-immediately-after-the-cou.md +6 -0
  46. package/corpus/templates/clips/changelog/2026-07-20-rewind-can-set-up-your-local-agent-once-so-future-requests-w.md +6 -0
  47. package/corpus/templates/clips/desktop/package.json +1 -0
  48. package/corpus/templates/clips/desktop/scripts/sign-macos-local.ts +12 -1
  49. package/corpus/templates/clips/desktop/src/app.tsx +2498 -239
  50. package/corpus/templates/clips/desktop/src/components/ReadinessPanel.tsx +21 -1
  51. package/corpus/templates/clips/desktop/src/components/Switch.tsx +3 -0
  52. package/corpus/templates/clips/desktop/src/hooks/useMeetingTranscription.ts +156 -24
  53. package/corpus/templates/clips/desktop/src/lib/native-recording-warm.ts +6 -7
  54. package/corpus/templates/clips/desktop/src/lib/recorder.ts +1763 -1160
  55. package/corpus/templates/clips/desktop/src/lib/rewind-agent-prompt.ts +9 -0
  56. package/corpus/templates/clips/desktop/src/lib/rewind-recording-start.ts +20 -0
  57. package/corpus/templates/clips/desktop/src/main.tsx +3 -0
  58. package/corpus/templates/clips/desktop/src/overlays/countdown.tsx +14 -14
  59. package/corpus/templates/clips/desktop/src/overlays/onboarding.tsx +4 -4
  60. package/corpus/templates/clips/desktop/src/overlays/preparing.tsx +13 -0
  61. package/corpus/templates/clips/desktop/src/overlays/toolbar.tsx +27 -2
  62. package/corpus/templates/clips/desktop/src/shared/config.ts +16 -0
  63. package/corpus/templates/clips/desktop/src/styles.css +956 -134
  64. package/corpus/templates/clips/desktop/src-tauri/Cargo.toml +5 -0
  65. package/corpus/templates/clips/desktop/src-tauri/build.rs +69 -1
  66. package/corpus/templates/clips/desktop/src-tauri/src/accessibility.rs +386 -12
  67. package/corpus/templates/clips/desktop/src-tauri/src/capture_audio_bus.rs +382 -0
  68. package/corpus/templates/clips/desktop/src-tauri/src/capture_graph.rs +777 -0
  69. package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +249 -22
  70. package/corpus/templates/clips/desktop/src-tauri/src/config.rs +148 -1
  71. package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +58 -4
  72. package/corpus/templates/clips/desktop/src-tauri/src/logfile.rs +13 -1
  73. package/corpus/templates/clips/desktop/src-tauri/src/native_screen/custom_capture.rs +1505 -51
  74. package/corpus/templates/clips/desktop/src-tauri/src/native_screen/live_upload.rs +124 -0
  75. package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +1006 -95
  76. package/corpus/templates/clips/desktop/src-tauri/src/rewind_capture_suspension.rs +165 -0
  77. package/corpus/templates/clips/desktop/src-tauri/src/rewind_chapters.rs +2434 -0
  78. package/corpus/templates/clips/desktop/src-tauri/src/rewind_clip.rs +1650 -0
  79. package/corpus/templates/clips/desktop/src-tauri/src/rewind_egress.rs +643 -0
  80. package/corpus/templates/clips/desktop/src-tauri/src/rewind_local_ask.rs +803 -0
  81. package/corpus/templates/clips/desktop/src-tauri/src/rewind_meeting_history.rs +595 -0
  82. package/corpus/templates/clips/desktop/src-tauri/src/screen_memory.rs +2900 -91
  83. package/corpus/templates/clips/desktop/src-tauri/src/screen_memory_ocr.rs +360 -0
  84. package/corpus/templates/clips/desktop/src-tauri/src/screen_memory_transcript.rs +408 -0
  85. package/corpus/templates/clips/desktop/src-tauri/src/shortcuts.rs +114 -33
  86. package/corpus/templates/clips/desktop/src-tauri/src/state.rs +2 -2
  87. package/corpus/templates/clips/desktop/src-tauri/src/system_audio.rs +13 -3
  88. package/corpus/templates/clips/desktop/src-tauri/src/tray.rs +4 -1
  89. package/corpus/templates/clips/desktop/src-tauri/src/util.rs +7 -13
  90. package/corpus/templates/clips/desktop/src-tauri/src/whisper_speech.rs +198 -11
  91. package/corpus/templates/clips/desktop/src-tauri/tauri.alpha.conf.json +17 -0
  92. package/dist/cli/mcp-config-writers.d.ts +2 -2
  93. package/dist/cli/mcp-config-writers.d.ts.map +1 -1
  94. package/dist/cli/mcp-config-writers.js +6 -6
  95. package/dist/cli/mcp-config-writers.js.map +1 -1
  96. package/dist/cli/mcp.d.ts +14 -0
  97. package/dist/cli/mcp.d.ts.map +1 -1
  98. package/dist/cli/mcp.js +79 -0
  99. package/dist/cli/mcp.js.map +1 -1
  100. package/dist/cli/skills-content/help.d.ts +1 -1
  101. package/dist/cli/skills-content/help.d.ts.map +1 -1
  102. package/dist/cli/skills-content/help.js +4 -3
  103. package/dist/cli/skills-content/help.js.map +1 -1
  104. package/dist/cli/skills-content/index.d.ts +1 -0
  105. package/dist/cli/skills-content/index.d.ts.map +1 -1
  106. package/dist/cli/skills-content/index.js +1 -0
  107. package/dist/cli/skills-content/index.js.map +1 -1
  108. package/dist/cli/skills-content/rewind-skill.d.ts +2 -0
  109. package/dist/cli/skills-content/rewind-skill.d.ts.map +1 -0
  110. package/dist/cli/skills-content/rewind-skill.js +53 -0
  111. package/dist/cli/skills-content/rewind-skill.js.map +1 -0
  112. package/dist/cli/skills.d.ts +6 -0
  113. package/dist/cli/skills.d.ts.map +1 -1
  114. package/dist/cli/skills.js +90 -14
  115. package/dist/cli/skills.js.map +1 -1
  116. package/dist/mcp/screen-memory-stdio.d.ts +365 -0
  117. package/dist/mcp/screen-memory-stdio.d.ts.map +1 -1
  118. package/dist/mcp/screen-memory-stdio.js +1014 -91
  119. package/dist/mcp/screen-memory-stdio.js.map +1 -1
  120. package/dist/mcp-client/index.d.ts +1 -1
  121. package/dist/mcp-client/index.d.ts.map +1 -1
  122. package/dist/mcp-client/index.js +1 -1
  123. package/dist/mcp-client/index.js.map +1 -1
  124. package/dist/mcp-client/screen-memory-local.d.ts +90 -0
  125. package/dist/mcp-client/screen-memory-local.d.ts.map +1 -1
  126. package/dist/mcp-client/screen-memory-local.js +590 -12
  127. package/dist/mcp-client/screen-memory-local.js.map +1 -1
  128. package/dist/notifications/routes.d.ts +3 -3
  129. package/dist/observability/routes.d.ts +1 -1
  130. package/dist/provider-api/actions/provider-api.d.ts +4 -4
  131. package/dist/resources/handlers.d.ts +1 -1
  132. package/package.json +1 -1
  133. package/src/cli/mcp-config-writers.ts +6 -4
  134. package/src/cli/mcp.ts +124 -0
  135. package/src/cli/skills-content/help.ts +4 -3
  136. package/src/cli/skills-content/index.ts +1 -0
  137. package/src/cli/skills-content/rewind-skill.ts +52 -0
  138. package/src/cli/skills.ts +115 -13
  139. package/src/mcp/screen-memory-stdio.ts +1360 -104
  140. package/src/mcp-client/index.ts +9 -0
  141. 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 = "recorder" | "settings" | "meetings" | "dictation";
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: 24,
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.preference.security?Privacy_Camera",
522
+ "x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Privacy_Camera",
371
523
  microphone:
372
- "x-apple.systempreferences:com.apple.preference.security?Privacy_Microphone",
524
+ "x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Privacy_Microphone",
373
525
  screen:
374
- "x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture",
526
+ "x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Privacy_ScreenCapture",
375
527
  speech:
376
- "x-apple.systempreferences:com.apple.preference.security?Privacy_SpeechRecognition",
528
+ "x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Privacy_SpeechRecognition",
377
529
  accessibility:
378
- "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility",
530
+ "x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Privacy_Accessibility",
379
531
  "input-monitoring":
380
- "x-apple.systempreferences:com.apple.preference.security?Privacy_ListenEvent",
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
- const startMeetingNotes = useCallback((meeting: PopoverMeeting) => {
1147
- setActiveMeetingId(meeting.id);
1148
- setMeetingStartMessage(`Starting notes for ${meeting.title}…`);
1149
- emit("meetings:start-transcription", {
1150
- meetingId: meeting.id,
1151
- joinUrl: meeting.joinUrl,
1152
- reason: "user",
1153
- }).catch((err) => {
1154
- console.error("[clips-popover] start meeting notes failed:", err);
1155
- setActiveMeetingId(null);
1156
- setMeetingStartMessage("Could not start notes. Try again from Meetings.");
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
- // During recording the popover is normally hidden — the tray click and the
2527
- // global shortcut both emit `clips:recorder-stop` directly, and the
2528
- // floating left-edge toolbar has the canonical Stop button. If the popover
2529
- // does somehow end up visible (dock reopen, global-shortcut race, etc.),
2530
- // we just render the normal pre-record panel so the user at least knows
2531
- // where they are. No recording-only UI lives here.
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
- if (popoverView === "settings") {
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
- onCancel={() => setPopoverView("recorder")}
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 === "meetings") {
3397
+ if (popoverView === "settings") {
2585
3398
  return (
2586
- <div className="app app-popover-view" ref={appRef}>
3399
+ <div className="app app-settings" ref={appRef}>
2587
3400
  {pendingUploadBanner}
2588
- <MeetingsPopoverView
2589
- meetings={meetings}
2590
- loading={meetingsLoading}
2591
- error={meetingsError}
2592
- startMessage={meetingStartMessage}
2593
- activeMeetingId={activeMeetingId}
2594
- meetingsEnabled={featureConfig?.meetingsEnabled !== false}
2595
- onBack={() => setPopoverView("recorder")}
2596
- onRefresh={fetchUpcomingMeetings}
2597
- onOpenMeetings={() => openInBrowser("/meetings")}
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
- <Header mode={mode} onModeChange={setMode} submitterEmail={signedInAs} />
2691
- <UpdateBanner />
2692
- {pendingUploadBanner}
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
- {localRecordingMode !== "off" ? (
2695
- <LocalRecordingModeBanner mode={localRecordingMode} />
2696
- ) : null}
3556
+ {pendingUploadBanner}
2697
3557
 
2698
- {localRecordingNotice ? (
2699
- <LocalRecordingSavedBanner
2700
- notice={localRecordingNotice}
2701
- onDismiss={() => setLocalRecordingNotice(null)}
2702
- onOpenFolder={() => {
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
- <div className="panel">
2714
- {showSourceRow ? (
2715
- <SourceRow
2716
- value={source}
2717
- onChange={setSource}
2718
- includeRegion={isMacPlatform()}
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
- {showCameraRow ? (
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="camera"
2725
- devices={cameraDevices}
2726
- selectedId={cameraId}
2727
- selectedLabel={cameraLabel}
3605
+ kind="mic"
3606
+ devices={micDevices}
3607
+ selectedId={selectedMicId}
3608
+ selectedLabel={micLabel}
2728
3609
  onSelect={(id, label) => {
2729
- setCameraId(id);
2730
- setCameraLabel(label);
3610
+ setMicId(id);
3611
+ setMicLabel(label);
2731
3612
  }}
2732
- onRefresh={() => requestDeviceAccess("camera")}
2733
- on={cameraOn}
2734
- onToggle={setCameraOn}
3613
+ onRefresh={() => requestDeviceAccess("mic")}
3614
+ on={micOn}
3615
+ onToggle={setMicOn}
3616
+ systemAudio={systemAudioOn}
3617
+ onSystemAudioToggle={setSystemAudioOn}
3618
+ meterActive={popoverVisible && !isRecording && !recordingFlowActive}
2735
3619
  />
2736
- ) : null}
3620
+ </div>
2737
3621
 
2738
- <MediaDeviceRow
2739
- kind="mic"
2740
- devices={micDevices}
2741
- selectedId={selectedMicId}
2742
- selectedLabel={micLabel}
2743
- onSelect={(id, label) => {
2744
- setMicId(id);
2745
- setMicLabel(label);
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
- <ReadinessPanel
2757
- mode={mode}
2758
- cameraOn={cameraOn}
2759
- micOn={micOn}
2760
- includeVoicePaste={voiceDictationEnabled}
2761
- includeFnMonitoring={fnShortcutEnabled}
2762
- open={readinessOpen}
2763
- onOpenChange={updateReadinessOpen}
2764
- onOpenPermission={openPrivacySettings}
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
- <button
2768
- className="primary start"
2769
- disabled={
2770
- localRecordingMode === "off" && videoStorageStatus === "checking"
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
- onRetry={handleStartRecording}
2797
- />
2798
- ) : recError === MACOS_SPEECH_PERMISSION_MESSAGE ? (
2799
- <PermissionRecoveryBanner
2800
- kind="speech"
2801
- message={recError}
2802
- panes={["speech", "microphone"]}
2803
- onRetry={handleStartRecording}
2804
- />
2805
- ) : isStorageSetupFailureMessage(recError) ? (
2806
- <StorageConnectionBanner onConnect={() => openVideoStorageSetup()} />
2807
- ) : (
2808
- <div className="error-banner">{recError}</div>
2809
- )
2810
- ) : null}
2811
- {cameraError && !recError ? (
2812
- cameraError === MACOS_CAPTURE_PERMISSION_MESSAGE ||
2813
- cameraError === DESKTOP_CAPTURE_PERMISSION_MESSAGE ? (
2814
- <PermissionRecoveryBanner
2815
- kind="camera"
2816
- message={cameraError}
2817
- panes={["camera"]}
2818
- onRetry={retryCameraPreview}
2819
- />
2820
- ) : (
2821
- <div className="error-banner">{cameraError}</div>
2822
- )
2823
- ) : null}
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: (meeting: PopoverMeeting) => void;
3531
- onStartNotesAndJoin: (meeting: PopoverMeeting) => void;
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(meeting)
3621
- : onStartNotes(meeting)
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 screenMemory =
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<typeof DEFAULT_SCREEN_MEMORY_CONFIG>,
5090
+ async function setScreenMemoryConfig(
5091
+ patch: Partial<ScreenMemoryStatus["config"]>,
3983
5092
  ) {
3984
5093
  if (!featureConfig) return;
3985
5094
  setScreenMemoryMessage(null);
3986
- invoke("set_feature_config", {
3987
- config: {
3988
- ...featureConfig,
3989
- screenMemory: {
3990
- ...DEFAULT_SCREEN_MEMORY_CONFIG,
3991
- ...screenMemory,
3992
- ...patch,
3993
- },
3994
- },
3995
- }).catch((err) => {
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 Screen Memory.",
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
- setScreenMemoryMessage({
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: (err as Error)?.message ?? "Could not export Screen Memory.",
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: "Screen Memory cleared." });
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 Screen Memory.",
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 Screen Memory folder.",
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 handleSubmit(e: React.FormEvent) {
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
- return (
4460
- <form className="setup" onSubmit={handleSubmit}>
4461
- <div className="setup-header">
4462
- {onCancel ? (
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="setup-back"
4466
- onClick={onCancel}
4467
- aria-label="Back"
5727
+ className={rewindConsentMode === "visuals" ? "selected" : ""}
5728
+ onClick={() => setRewindConsentMode("visuals")}
4468
5729
  >
4469
- <IconArrowLeft size={18} stroke={1.75} />
5730
+ <strong>Visuals</strong>
5731
+ <span>Screen, app, and readable text</span>
4470
5732
  </button>
4471
- ) : null}
4472
- <h2>Settings</h2>
4473
- </div>
4474
-
4475
- <div className="setup-section-heading">General</div>
4476
-
4477
- <div className="setup-section">
4478
- <SettingLabel
4479
- label="Clips server URL"
4480
- hint="The URL of the Clips backend this tray app connects to."
4481
- htmlFor="clips-url"
4482
- />
4483
- <input
4484
- id="clips-url"
4485
- type="url"
4486
- value={url}
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 className="secondary setup-connect-button" type="submit">
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="By default the Clips window and recording overlays are hidden from screenshots and screen recordings so they never leak into your captured video. Turn on for debugging or demos."
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="Screen Memory"
4616
- hint="Keep a rolling local screen buffer and recent app/window context for local agents. Stored only on this Mac."
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 Screen Memory"
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 recording new Screen Memory segments without clearing the local history."
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 Screen Memory"
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
- {screenMemoryRecording
4681
- ? `Recording locally - ${screenMemorySegments.length} segment${screenMemorySegments.length === 1 ? "" : "s"}, ${formatStorageBytes(screenMemoryTotalBytes)}.`
4682
- : screenMemory.paused
4683
- ? "Paused. Existing local history is still available."
4684
- : screenMemoryStatus?.lastError
4685
- ? screenMemoryStatus.lastError
4686
- : "Starting when screen recording permission is available."}
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 segment:{" "}
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
- </form>
7446
+ </div>
5188
7447
  );
5189
7448
  }
5190
7449