@agent-native/core 0.114.5 → 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 (210) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +14 -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/client/resources/ResourceTree.tsx +11 -5
  11. package/corpus/core/src/client/resources/ResourcesPanel.tsx +141 -18
  12. package/corpus/core/src/mcp/screen-memory-stdio.ts +1360 -104
  13. package/corpus/core/src/mcp-client/index.ts +9 -0
  14. package/corpus/core/src/mcp-client/screen-memory-local.ts +847 -13
  15. package/corpus/templates/clips/actions/apply-rewind-extension.ts +195 -0
  16. package/corpus/templates/clips/actions/delete-agent-recording-if-unpromoted.ts +64 -0
  17. package/corpus/templates/clips/actions/get-rewind-extension-request.ts +22 -0
  18. package/corpus/templates/clips/actions/get-screen-memory-status.ts +18 -2
  19. package/corpus/templates/clips/actions/list-rewind-extension-requests.ts +52 -0
  20. package/corpus/templates/clips/actions/make-recording-private-for-rewind.ts +55 -0
  21. package/corpus/templates/clips/actions/query-screen-memory-context.ts +4 -4
  22. package/corpus/templates/clips/actions/request-rewind-extension.ts +81 -0
  23. package/corpus/templates/clips/actions/update-rewind-extension-request.ts +47 -0
  24. package/corpus/templates/clips/app/components/editor/editor-layout.tsx +25 -0
  25. package/corpus/templates/clips/app/components/editor/editor-toolbar.tsx +36 -0
  26. package/corpus/templates/clips/app/components/editor/rewind-extension-dialog.tsx +338 -0
  27. package/corpus/templates/clips/app/components/editor/stitch-manager.tsx +1 -16
  28. package/corpus/templates/clips/app/components/editor/timeline.tsx +26 -0
  29. package/corpus/templates/clips/app/components/meetings/meeting-card.tsx +1 -2
  30. package/corpus/templates/clips/app/i18n/en-US.ts +18 -0
  31. package/corpus/templates/clips/app/lib/ffmpeg-export.ts +19 -12
  32. package/corpus/templates/clips/app/lib/rewind-visibility.ts +31 -0
  33. package/corpus/templates/clips/app/lib/timestamp-mapping.ts +7 -0
  34. package/corpus/templates/clips/app/lib/upload-file-client.ts +17 -0
  35. package/corpus/templates/clips/changelog/2026-07-14-rewind-keeps-recent-screen-context-local-and-turns-it-into-clips.md +6 -0
  36. package/corpus/templates/clips/changelog/2026-07-17-active-clips-keep-rewind-stable-and-settings-clear.md +12 -0
  37. package/corpus/templates/clips/changelog/2026-07-17-clicking-the-clips-icon-during-a-recording-now-opens-the-app.md +6 -0
  38. package/corpus/templates/clips/changelog/2026-07-17-default-mic-now-uses-the-mac-default-and-transcription-shares-one-capture.md +6 -0
  39. package/corpus/templates/clips/changelog/2026-07-17-full-screen-clips-finalize-even-when-screencapturekit-stop-hangs.md +6 -0
  40. package/corpus/templates/clips/changelog/2026-07-17-normal-full-screen-clips-no-longer-stop-immediately-after-th.md +6 -0
  41. package/corpus/templates/clips/changelog/2026-07-17-rewind-can-hand-one-bounded-private-clip-to-your-agent.md +12 -0
  42. package/corpus/templates/clips/changelog/2026-07-17-rewind-home-can-copy-a-local-agent-prompt.md +6 -0
  43. package/corpus/templates/clips/changelog/2026-07-17-rewind-now-has-a-calm-home-status-focused-first-run-consent-.md +6 -0
  44. package/corpus/templates/clips/changelog/2026-07-17-rewind-settings-are-smaller-clearer-and-agent-first.md +6 -0
  45. package/corpus/templates/clips/changelog/2026-07-17-the-full-clips-interface-stays-available-while-recording.md +6 -0
  46. package/corpus/templates/clips/changelog/2026-07-18-rewind-organizes-recent-work-into-local-searchable-chapters.md +8 -0
  47. package/corpus/templates/clips/changelog/2026-07-18-searchable-rewind-chapters.md +6 -0
  48. package/corpus/templates/clips/changelog/2026-07-19-rewind-backed-recordings-now-start-immediately-after-the-cou.md +6 -0
  49. package/corpus/templates/clips/changelog/2026-07-20-opening-zoom-meeting-launches-native-app.md +6 -0
  50. package/corpus/templates/clips/changelog/2026-07-20-rewind-can-set-up-your-local-agent-once-so-future-requests-w.md +6 -0
  51. package/corpus/templates/clips/desktop/package.json +1 -0
  52. package/corpus/templates/clips/desktop/scripts/sign-macos-local.ts +12 -1
  53. package/corpus/templates/clips/desktop/src/app.tsx +2478 -223
  54. package/corpus/templates/clips/desktop/src/components/ReadinessPanel.tsx +21 -1
  55. package/corpus/templates/clips/desktop/src/components/Switch.tsx +3 -0
  56. package/corpus/templates/clips/desktop/src/hooks/useMeetingTranscription.ts +156 -24
  57. package/corpus/templates/clips/desktop/src/lib/native-recording-warm.ts +6 -7
  58. package/corpus/templates/clips/desktop/src/lib/open-meeting-join-url.ts +22 -0
  59. package/corpus/templates/clips/desktop/src/lib/recorder.ts +1763 -1160
  60. package/corpus/templates/clips/desktop/src/lib/rewind-agent-prompt.ts +9 -0
  61. package/corpus/templates/clips/desktop/src/lib/rewind-recording-start.ts +20 -0
  62. package/corpus/templates/clips/desktop/src/main.tsx +3 -0
  63. package/corpus/templates/clips/desktop/src/overlays/countdown.tsx +14 -14
  64. package/corpus/templates/clips/desktop/src/overlays/meeting-notification.tsx +2 -3
  65. package/corpus/templates/clips/desktop/src/overlays/onboarding.tsx +4 -4
  66. package/corpus/templates/clips/desktop/src/overlays/preparing.tsx +13 -0
  67. package/corpus/templates/clips/desktop/src/overlays/toolbar.tsx +27 -2
  68. package/corpus/templates/clips/desktop/src/shared/config.ts +16 -0
  69. package/corpus/templates/clips/desktop/src/styles.css +956 -134
  70. package/corpus/templates/clips/desktop/src-tauri/Cargo.toml +5 -0
  71. package/corpus/templates/clips/desktop/src-tauri/build.rs +69 -1
  72. package/corpus/templates/clips/desktop/src-tauri/src/accessibility.rs +386 -12
  73. package/corpus/templates/clips/desktop/src-tauri/src/capture_audio_bus.rs +382 -0
  74. package/corpus/templates/clips/desktop/src-tauri/src/capture_graph.rs +777 -0
  75. package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +249 -22
  76. package/corpus/templates/clips/desktop/src-tauri/src/config.rs +148 -1
  77. package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +58 -4
  78. package/corpus/templates/clips/desktop/src-tauri/src/logfile.rs +13 -1
  79. package/corpus/templates/clips/desktop/src-tauri/src/native_screen/custom_capture.rs +1505 -51
  80. package/corpus/templates/clips/desktop/src-tauri/src/native_screen/live_upload.rs +124 -0
  81. package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +1006 -95
  82. package/corpus/templates/clips/desktop/src-tauri/src/rewind_capture_suspension.rs +165 -0
  83. package/corpus/templates/clips/desktop/src-tauri/src/rewind_chapters.rs +2434 -0
  84. package/corpus/templates/clips/desktop/src-tauri/src/rewind_clip.rs +1650 -0
  85. package/corpus/templates/clips/desktop/src-tauri/src/rewind_egress.rs +643 -0
  86. package/corpus/templates/clips/desktop/src-tauri/src/rewind_local_ask.rs +803 -0
  87. package/corpus/templates/clips/desktop/src-tauri/src/rewind_meeting_history.rs +595 -0
  88. package/corpus/templates/clips/desktop/src-tauri/src/screen_memory.rs +2900 -91
  89. package/corpus/templates/clips/desktop/src-tauri/src/screen_memory_ocr.rs +360 -0
  90. package/corpus/templates/clips/desktop/src-tauri/src/screen_memory_transcript.rs +408 -0
  91. package/corpus/templates/clips/desktop/src-tauri/src/shortcuts.rs +114 -33
  92. package/corpus/templates/clips/desktop/src-tauri/src/state.rs +2 -2
  93. package/corpus/templates/clips/desktop/src-tauri/src/system_audio.rs +13 -3
  94. package/corpus/templates/clips/desktop/src-tauri/src/tray.rs +4 -1
  95. package/corpus/templates/clips/desktop/src-tauri/src/util.rs +7 -13
  96. package/corpus/templates/clips/desktop/src-tauri/src/whisper_speech.rs +198 -11
  97. package/corpus/templates/clips/desktop/src-tauri/tauri.alpha.conf.json +17 -0
  98. package/corpus/templates/clips/shared/meeting-join-url.ts +0 -5
  99. package/corpus/templates/content/.agents/skills/document-editing/SKILL.md +9 -1
  100. package/corpus/templates/content/AGENTS.md +4 -1
  101. package/corpus/templates/content/actions/_content-spaces.ts +6 -1
  102. package/corpus/templates/content/actions/_database-row-batch.ts +15 -2
  103. package/corpus/templates/content/actions/_database-utils.ts +83 -15
  104. package/corpus/templates/content/actions/add-database-item.ts +3 -0
  105. package/corpus/templates/content/actions/connect-local-folder-source.ts +2 -1
  106. package/corpus/templates/content/actions/delete-content-database.ts +12 -9
  107. package/corpus/templates/content/actions/delete-database-items.ts +3 -2
  108. package/corpus/templates/content/actions/delete-document.ts +233 -6
  109. package/corpus/templates/content/actions/duplicate-database-item.ts +1 -0
  110. package/corpus/templates/content/actions/get-document.ts +4 -1
  111. package/corpus/templates/content/actions/list-content-databases.ts +1 -0
  112. package/corpus/templates/content/actions/list-documents.ts +1 -0
  113. package/corpus/templates/content/actions/list-trashed-content-databases.ts +8 -2
  114. package/corpus/templates/content/actions/list-trashed-documents.ts +50 -0
  115. package/corpus/templates/content/actions/permanently-delete-document.ts +28 -0
  116. package/corpus/templates/content/actions/pull-document.ts +15 -2
  117. package/corpus/templates/content/actions/restore-content-database.ts +48 -11
  118. package/corpus/templates/content/actions/restore-document.ts +28 -0
  119. package/corpus/templates/content/actions/search-documents.ts +2 -1
  120. package/corpus/templates/content/actions/view-screen.ts +2 -1
  121. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +149 -55
  122. package/corpus/templates/content/app/components/editor/database/GalleryView.tsx +7 -2
  123. package/corpus/templates/content/app/components/editor/database/ListView.tsx +7 -2
  124. package/corpus/templates/content/app/components/editor/database/TimelineView.tsx +3 -0
  125. package/corpus/templates/content/app/components/editor/database/sidebar.tsx +26 -5
  126. package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +243 -163
  127. package/corpus/templates/content/app/components/sidebar/WorkspaceSourceMenu.tsx +154 -0
  128. package/corpus/templates/content/app/components/sidebar/select-content-space.ts +11 -0
  129. package/corpus/templates/content/app/hooks/use-content-database.ts +6 -0
  130. package/corpus/templates/content/app/hooks/use-documents.ts +57 -0
  131. package/corpus/templates/content/app/i18n-data.ts +83 -2
  132. package/corpus/templates/content/app/routes/_app.local-files.tsx +8 -1
  133. package/corpus/templates/content/changelog/2026-07-19-pages-now-move-to-a-reversible-trash-and-the-organization-pi.md +6 -0
  134. package/corpus/templates/content/changelog/2026-07-20-trash-actions-now-preserve-independent-archived-pages-enforc.md +6 -0
  135. package/corpus/templates/content/changelog/2026-07-20-workspace-toggles-now-stay-independently-open-or-closed-and-.md +6 -0
  136. package/corpus/templates/content/changelog/2026-07-20-workspaces-can-be-added-from-a-blank-workspace-or-a-connecte.md +6 -0
  137. package/corpus/templates/content/parity/matrix.md +1 -1
  138. package/corpus/templates/content/parity/matrix.ts +3 -0
  139. package/corpus/templates/content/server/db/schema.ts +2 -0
  140. package/corpus/templates/content/server/lib/public-documents.ts +8 -2
  141. package/corpus/templates/content/server/plugins/db.ts +52 -0
  142. package/corpus/templates/content/server/routes/api/document-agent-context.json.get.ts +2 -2
  143. package/corpus/templates/content/shared/api.ts +10 -0
  144. package/corpus/toolkit/CHANGELOG.md +6 -0
  145. package/corpus/toolkit/package.json +1 -1
  146. package/corpus/toolkit/src/ui/progress.tsx +3 -2
  147. package/dist/cli/mcp-config-writers.d.ts +2 -2
  148. package/dist/cli/mcp-config-writers.d.ts.map +1 -1
  149. package/dist/cli/mcp-config-writers.js +6 -6
  150. package/dist/cli/mcp-config-writers.js.map +1 -1
  151. package/dist/cli/mcp.d.ts +14 -0
  152. package/dist/cli/mcp.d.ts.map +1 -1
  153. package/dist/cli/mcp.js +79 -0
  154. package/dist/cli/mcp.js.map +1 -1
  155. package/dist/cli/skills-content/help.d.ts +1 -1
  156. package/dist/cli/skills-content/help.d.ts.map +1 -1
  157. package/dist/cli/skills-content/help.js +4 -3
  158. package/dist/cli/skills-content/help.js.map +1 -1
  159. package/dist/cli/skills-content/index.d.ts +1 -0
  160. package/dist/cli/skills-content/index.d.ts.map +1 -1
  161. package/dist/cli/skills-content/index.js +1 -0
  162. package/dist/cli/skills-content/index.js.map +1 -1
  163. package/dist/cli/skills-content/rewind-skill.d.ts +2 -0
  164. package/dist/cli/skills-content/rewind-skill.d.ts.map +1 -0
  165. package/dist/cli/skills-content/rewind-skill.js +53 -0
  166. package/dist/cli/skills-content/rewind-skill.js.map +1 -0
  167. package/dist/cli/skills.d.ts +6 -0
  168. package/dist/cli/skills.d.ts.map +1 -1
  169. package/dist/cli/skills.js +90 -14
  170. package/dist/cli/skills.js.map +1 -1
  171. package/dist/client/resources/ResourceTree.d.ts +3 -1
  172. package/dist/client/resources/ResourceTree.d.ts.map +1 -1
  173. package/dist/client/resources/ResourceTree.js +4 -4
  174. package/dist/client/resources/ResourceTree.js.map +1 -1
  175. package/dist/client/resources/ResourcesPanel.d.ts.map +1 -1
  176. package/dist/client/resources/ResourcesPanel.js +55 -6
  177. package/dist/client/resources/ResourcesPanel.js.map +1 -1
  178. package/dist/collab/routes.d.ts +1 -1
  179. package/dist/collab/struct-routes.d.ts +1 -1
  180. package/dist/mcp/screen-memory-stdio.d.ts +365 -0
  181. package/dist/mcp/screen-memory-stdio.d.ts.map +1 -1
  182. package/dist/mcp/screen-memory-stdio.js +1014 -91
  183. package/dist/mcp/screen-memory-stdio.js.map +1 -1
  184. package/dist/mcp-client/index.d.ts +1 -1
  185. package/dist/mcp-client/index.d.ts.map +1 -1
  186. package/dist/mcp-client/index.js +1 -1
  187. package/dist/mcp-client/index.js.map +1 -1
  188. package/dist/mcp-client/screen-memory-local.d.ts +90 -0
  189. package/dist/mcp-client/screen-memory-local.d.ts.map +1 -1
  190. package/dist/mcp-client/screen-memory-local.js +590 -12
  191. package/dist/mcp-client/screen-memory-local.js.map +1 -1
  192. package/dist/notifications/routes.d.ts +2 -2
  193. package/dist/observability/routes.d.ts +5 -5
  194. package/dist/progress/routes.d.ts +1 -1
  195. package/dist/provider-api/actions/provider-api.d.ts +4 -4
  196. package/dist/resources/handlers.d.ts +1 -1
  197. package/dist/secrets/routes.d.ts +9 -9
  198. package/dist/server/transcribe-voice.d.ts +1 -1
  199. package/package.json +2 -2
  200. package/src/cli/mcp-config-writers.ts +6 -4
  201. package/src/cli/mcp.ts +124 -0
  202. package/src/cli/skills-content/help.ts +4 -3
  203. package/src/cli/skills-content/index.ts +1 -0
  204. package/src/cli/skills-content/rewind-skill.ts +52 -0
  205. package/src/cli/skills.ts +115 -13
  206. package/src/client/resources/ResourceTree.tsx +11 -5
  207. package/src/client/resources/ResourcesPanel.tsx +141 -18
  208. package/src/mcp/screen-memory-stdio.ts +1360 -104
  209. package/src/mcp-client/index.ts +9 -0
  210. 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,
@@ -55,7 +60,7 @@ import {
55
60
  getCameraStreamWithFallback,
56
61
  isMediaConstraintFailure,
57
62
  } from "./lib/media-capture-constraints";
58
- import { resolveDesktopMeetingJoinUrl } from "./lib/meeting-join-url";
63
+ import { openMeetingJoinUrl } from "./lib/open-meeting-join-url";
59
64
  import {
60
65
  DESKTOP_CAPTURE_PERMISSION_MESSAGE,
61
66
  isHardCapturePermissionError,
@@ -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.
@@ -1030,11 +1247,9 @@ export function App() {
1030
1247
  listen<{ joinUrl?: string | null }>("meetings:open-join-url", (ev) => {
1031
1248
  const url = ev.payload?.joinUrl;
1032
1249
  if (!url) return;
1033
- import("@tauri-apps/plugin-shell")
1034
- .then(({ open }) => open(resolveDesktopMeetingJoinUrl(url)))
1035
- .catch((err) => {
1036
- console.error("[clips-popover] open join url failed:", err);
1037
- });
1250
+ openMeetingJoinUrl(url).catch((err) => {
1251
+ console.error("[clips-popover] open join url failed:", err);
1252
+ });
1038
1253
  })
1039
1254
  .then((u) => {
1040
1255
  unlisten = u;
@@ -1105,6 +1320,327 @@ export function App() {
1105
1320
  [serverUrl],
1106
1321
  );
1107
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
+
1108
1644
  const fetchUpcomingMeetings = useCallback(async () => {
1109
1645
  if (authStatus !== "authed") {
1110
1646
  setMeetings([]);
@@ -1145,30 +1681,79 @@ export function App() {
1145
1681
  }
1146
1682
  }, [authStatus, callClipsAction]);
1147
1683
 
1148
- const startMeetingNotes = useCallback((meeting: PopoverMeeting) => {
1149
- setActiveMeetingId(meeting.id);
1150
- setMeetingStartMessage(`Starting notes for ${meeting.title}…`);
1151
- emit("meetings:start-transcription", {
1152
- meetingId: meeting.id,
1153
- joinUrl: meeting.joinUrl,
1154
- reason: "user",
1155
- }).catch((err) => {
1156
- console.error("[clips-popover] start meeting notes failed:", err);
1157
- setActiveMeetingId(null);
1158
- 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));
1159
1718
  });
1160
- }, []);
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
+ );
1161
1748
 
1162
1749
  const startMeetingNotesAndJoin = useCallback(
1163
- (meeting: PopoverMeeting) => {
1750
+ (meeting: PopoverMeeting, includeFromMeetingStart = false) => {
1164
1751
  if (meeting.joinUrl) {
1165
- openExternal(resolveDesktopMeetingJoinUrl(meeting.joinUrl)).catch(
1166
- (err) => {
1167
- console.error("[clips-popover] open meeting join url failed:", err);
1168
- },
1169
- );
1752
+ openMeetingJoinUrl(meeting.joinUrl).catch((err) => {
1753
+ console.error("[clips-popover] open meeting join url failed:", err);
1754
+ });
1170
1755
  }
1171
- startMeetingNotes(meeting);
1756
+ startMeetingNotes(meeting, includeFromMeetingStart);
1172
1757
  hidePopover();
1173
1758
  },
1174
1759
  [startMeetingNotes],
@@ -1186,7 +1771,7 @@ export function App() {
1186
1771
  useEffect(() => {
1187
1772
  invoke<string | null>("get_active_meeting_id")
1188
1773
  .then((meetingId) => {
1189
- if (meetingId) setActiveMeetingId(meetingId);
1774
+ if (meetingId) setActiveMeetingId((current) => current ?? meetingId);
1190
1775
  })
1191
1776
  .catch(() => {});
1192
1777
 
@@ -1209,6 +1794,7 @@ export function App() {
1209
1794
  (event) => {
1210
1795
  if (event.payload?.meetingId) {
1211
1796
  setActiveMeetingId(event.payload.meetingId);
1797
+ setMeetingStartMessage("Meeting notes are live and staying local.");
1212
1798
  }
1213
1799
  },
1214
1800
  ),
@@ -1222,11 +1808,12 @@ export function App() {
1222
1808
  ? null
1223
1809
  : current,
1224
1810
  );
1811
+ setMeetingStartMessage(null);
1225
1812
  },
1226
1813
  ),
1227
1814
  );
1228
1815
  track(
1229
- listen<{ meetingId?: string | null }>(
1816
+ listen<{ meetingId?: string | null; error?: string }>(
1230
1817
  "meetings:transcription-error",
1231
1818
  (event) => {
1232
1819
  setActiveMeetingId((current) =>
@@ -1234,6 +1821,20 @@ export function App() {
1234
1821
  ? null
1235
1822
  : current,
1236
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
+ );
1237
1838
  },
1238
1839
  ),
1239
1840
  );
@@ -1797,7 +2398,7 @@ export function App() {
1797
2398
  const appRef = useRef<HTMLDivElement | null>(null);
1798
2399
  usePopoverAutoSize(appRef, {
1799
2400
  disabled: !popoverVisible || isRecording || recordingFlowActive,
1800
- width: popoverView === "settings" ? 440 : 360,
2401
+ width: popoverView === "settings" || popoverView === "memory" ? 440 : 360,
1801
2402
  });
1802
2403
 
1803
2404
  const loadPendingUploads = useCallback(async () => {
@@ -2527,12 +3128,43 @@ export function App() {
2527
3128
  const showCameraRow = mode !== "screen"; // screen-only has no camera
2528
3129
  const showSourceRow = mode !== "camera"; // camera-only has no screen source
2529
3130
 
2530
- // During recording the popover is normally hidden — the tray click and the
2531
- // global shortcut both emit `clips:recorder-stop` directly, and the
2532
- // floating left-edge toolbar has the canonical Stop button. If the popover
2533
- // does somehow end up visible (dock reopen, global-shortcut race, etc.),
2534
- // we just render the normal pre-record panel so the user at least knows
2535
- // 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
+ }
2536
3168
 
2537
3169
  const pendingUploadBanner = recordingStopFinalizing ? (
2538
3170
  <FinalizingUploadBanner />
@@ -2550,11 +3182,179 @@ export function App() {
2550
3182
  />
2551
3183
  ) : null;
2552
3184
 
2553
- 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") {
2554
3350
  return (
2555
3351
  <div className="app app-settings" ref={appRef}>
2556
3352
  {pendingUploadBanner}
3353
+ {isRecording ? <ActiveRecordingBanner /> : null}
2557
3354
  <Setup
3355
+ surface={popoverView === "memory" ? "memory" : "rewind"}
3356
+ recordingActive={isRecording || recordingFlowActive}
3357
+ promptRewindEnable={promptRewindEnable}
2558
3358
  initial={serverUrl}
2559
3359
  serverUrl={serverUrl}
2560
3360
  signedInAs={signedInAs}
@@ -2579,23 +3379,76 @@ export function App() {
2579
3379
  setServerUrl(url.replace(/\/+$/, ""));
2580
3380
  setPopoverView("recorder");
2581
3381
  }}
2582
- 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
+ }}
2583
3392
  />
2584
3393
  </div>
2585
3394
  );
2586
3395
  }
2587
3396
 
2588
- if (popoverView === "meetings") {
3397
+ if (popoverView === "settings") {
2589
3398
  return (
2590
- <div className="app app-popover-view" ref={appRef}>
3399
+ <div className="app app-settings" ref={appRef}>
2591
3400
  {pendingUploadBanner}
2592
- <MeetingsPopoverView
2593
- meetings={meetings}
2594
- loading={meetingsLoading}
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}
2595
3447
  error={meetingsError}
2596
3448
  startMessage={meetingStartMessage}
2597
3449
  activeMeetingId={activeMeetingId}
2598
3450
  meetingsEnabled={featureConfig?.meetingsEnabled !== false}
3451
+ rewindHistoryAvailability={rewindMeetingHistoryAvailability}
2599
3452
  onBack={() => setPopoverView("recorder")}
2600
3453
  onRefresh={fetchUpcomingMeetings}
2601
3454
  onOpenMeetings={() => openInBrowser("/meetings")}
@@ -2615,6 +3468,7 @@ export function App() {
2615
3468
  return (
2616
3469
  <div className="app app-popover-view" ref={appRef}>
2617
3470
  {pendingUploadBanner}
3471
+ {isRecording ? <ActiveRecordingBanner /> : null}
2618
3472
  <DictationPopoverView
2619
3473
  voiceEnabled={voiceDictationEnabled}
2620
3474
  voiceShortcut={voiceShortcut}
@@ -2690,141 +3544,230 @@ export function App() {
2690
3544
  }
2691
3545
 
2692
3546
  return (
2693
- <div className="app" ref={appRef}>
2694
- <Header mode={mode} onModeChange={setMode} submitterEmail={signedInAs} />
2695
- <UpdateBanner />
2696
- {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 />
2697
3555
 
2698
- {localRecordingMode !== "off" ? (
2699
- <LocalRecordingModeBanner mode={localRecordingMode} />
2700
- ) : null}
3556
+ {pendingUploadBanner}
2701
3557
 
2702
- {localRecordingNotice ? (
2703
- <LocalRecordingSavedBanner
2704
- notice={localRecordingNotice}
2705
- onDismiss={() => setLocalRecordingNotice(null)}
2706
- onOpenFolder={() => {
2707
- if (!localRecordingNotice.folderPath) return;
2708
- invoke("open_local_recording_folder", {
2709
- path: localRecordingNotice.folderPath,
2710
- }).catch((err) => {
2711
- console.error("[clips-tray] open local folder failed:", err);
2712
- });
2713
- }}
2714
- />
2715
- ) : null}
3558
+ {isRecording ? <ActiveRecordingBanner /> : null}
3559
+
3560
+ {localRecordingMode !== "off" ? (
3561
+ <LocalRecordingModeBanner mode={localRecordingMode} />
3562
+ ) : null}
2716
3563
 
2717
- <div className="panel">
2718
- {showSourceRow ? (
2719
- <SourceRow
2720
- value={source}
2721
- onChange={setSource}
2722
- 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
+ }}
2723
3576
  />
2724
3577
  ) : null}
2725
3578
 
2726
- {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
+
2727
3604
  <MediaDeviceRow
2728
- kind="camera"
2729
- devices={cameraDevices}
2730
- selectedId={cameraId}
2731
- selectedLabel={cameraLabel}
3605
+ kind="mic"
3606
+ devices={micDevices}
3607
+ selectedId={selectedMicId}
3608
+ selectedLabel={micLabel}
2732
3609
  onSelect={(id, label) => {
2733
- setCameraId(id);
2734
- setCameraLabel(label);
3610
+ setMicId(id);
3611
+ setMicLabel(label);
2735
3612
  }}
2736
- onRefresh={() => requestDeviceAccess("camera")}
2737
- on={cameraOn}
2738
- onToggle={setCameraOn}
3613
+ onRefresh={() => requestDeviceAccess("mic")}
3614
+ on={micOn}
3615
+ onToggle={setMicOn}
3616
+ systemAudio={systemAudioOn}
3617
+ onSystemAudioToggle={setSystemAudioOn}
3618
+ meterActive={popoverVisible && !isRecording && !recordingFlowActive}
2739
3619
  />
2740
- ) : null}
3620
+ </div>
2741
3621
 
2742
- <MediaDeviceRow
2743
- kind="mic"
2744
- devices={micDevices}
2745
- selectedId={selectedMicId}
2746
- selectedLabel={micLabel}
2747
- onSelect={(id, label) => {
2748
- setMicId(id);
2749
- setMicLabel(label);
2750
- }}
2751
- onRefresh={() => requestDeviceAccess("mic")}
2752
- on={micOn}
2753
- onToggle={setMicOn}
2754
- systemAudio={systemAudioOn}
2755
- onSystemAudioToggle={setSystemAudioOn}
2756
- 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}
2757
3631
  />
2758
- </div>
2759
3632
 
2760
- <ReadinessPanel
2761
- mode={mode}
2762
- cameraOn={cameraOn}
2763
- micOn={micOn}
2764
- includeVoicePaste={voiceDictationEnabled}
2765
- includeFnMonitoring={fnShortcutEnabled}
2766
- open={readinessOpen}
2767
- onOpenChange={updateReadinessOpen}
2768
- onOpenPermission={openPrivacySettings}
2769
- />
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>
2770
3708
 
2771
- <button
2772
- className="primary start"
2773
- disabled={
2774
- localRecordingMode === "off" && videoStorageStatus === "checking"
2775
- }
2776
- onClick={() => {
2777
- void handleStartRecording();
2778
- }}
2779
- >
2780
- {localRecordingMode === "off" && videoStorageStatus === "checking"
2781
- ? "Checking storage..."
2782
- : localRecordingMode === "off"
2783
- ? "Start recording"
2784
- : "Start local recording"}
2785
- </button>
2786
- {recError ? (
2787
- recError === MACOS_UPDATE_RESTART_MESSAGE ? (
2788
- <UpdateRestartBanner message={recError} />
2789
- ) : recError === MACOS_CAPTURE_PERMISSION_MESSAGE ||
2790
- recError === MACOS_SCREEN_PERMISSION_MESSAGE ||
2791
- recError === DESKTOP_CAPTURE_PERMISSION_MESSAGE ? (
2792
- <PermissionRecoveryBanner
2793
- kind="recording"
2794
- message={recError}
2795
- panes={
2796
- recError === MACOS_SCREEN_PERMISSION_MESSAGE
2797
- ? ["screen"]
2798
- : permissionPanesForRecording(mode, cameraOn, micOn)
3709
+ {!isRecording ? (
3710
+ <button
3711
+ className="primary start"
3712
+ disabled={
3713
+ localRecordingMode === "off" && videoStorageStatus === "checking"
2799
3714
  }
2800
- onRetry={handleStartRecording}
2801
- />
2802
- ) : recError === MACOS_SPEECH_PERMISSION_MESSAGE ? (
2803
- <PermissionRecoveryBanner
2804
- kind="speech"
2805
- message={recError}
2806
- panes={["speech", "microphone"]}
2807
- onRetry={handleStartRecording}
2808
- />
2809
- ) : isStorageSetupFailureMessage(recError) ? (
2810
- <StorageConnectionBanner onConnect={() => openVideoStorageSetup()} />
2811
- ) : (
2812
- <div className="error-banner">{recError}</div>
2813
- )
2814
- ) : null}
2815
- {cameraError && !recError ? (
2816
- cameraError === MACOS_CAPTURE_PERMISSION_MESSAGE ||
2817
- cameraError === DESKTOP_CAPTURE_PERMISSION_MESSAGE ? (
2818
- <PermissionRecoveryBanner
2819
- kind="camera"
2820
- message={cameraError}
2821
- panes={["camera"]}
2822
- onRetry={retryCameraPreview}
2823
- />
2824
- ) : (
2825
- <div className="error-banner">{cameraError}</div>
2826
- )
2827
- ) : 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>
2828
3771
 
2829
3772
  <div className="bottom-row">
2830
3773
  <BottomButton
@@ -3511,6 +4454,7 @@ function MeetingsPopoverView({
3511
4454
  startMessage,
3512
4455
  activeMeetingId,
3513
4456
  meetingsEnabled,
4457
+ rewindHistoryAvailability,
3514
4458
  onBack,
3515
4459
  onRefresh,
3516
4460
  onOpenMeetings,
@@ -3526,15 +4470,38 @@ function MeetingsPopoverView({
3526
4470
  startMessage: string | null;
3527
4471
  activeMeetingId: string | null;
3528
4472
  meetingsEnabled: boolean;
4473
+ rewindHistoryAvailability: Record<string, RewindMeetingHistoryAvailability>;
3529
4474
  onBack: () => void;
3530
4475
  onRefresh: () => void;
3531
4476
  onOpenMeetings: () => void;
3532
4477
  onOpenMeeting: (meetingId: string) => void;
3533
4478
  onOpenSettings: () => void;
3534
- onStartNotes: (meeting: PopoverMeeting) => void;
3535
- onStartNotesAndJoin: (meeting: PopoverMeeting) => void;
4479
+ onStartNotes: (
4480
+ meeting: PopoverMeeting,
4481
+ includeFromMeetingStart?: boolean,
4482
+ ) => void;
4483
+ onStartNotesAndJoin: (
4484
+ meeting: PopoverMeeting,
4485
+ includeFromMeetingStart?: boolean,
4486
+ ) => void;
3536
4487
  onShowActiveMeeting: (meetingId: string) => void;
3537
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
+
3538
4505
  return (
3539
4506
  <div className="setup popover-view">
3540
4507
  <PopoverSubViewHeader
@@ -3593,6 +4560,8 @@ function MeetingsPopoverView({
3593
4560
  const canStart = meetingCanStartNotes(meeting);
3594
4561
  const hasJoin = Boolean(meeting.joinUrl);
3595
4562
  const isActive = activeMeetingId === meeting.id;
4563
+ const rewindHistory = rewindHistoryAvailability[meeting.id];
4564
+ const includeHistory = includeHistoryFor.has(meeting.id);
3596
4565
  return (
3597
4566
  <div className="popover-list-item" key={meeting.id}>
3598
4567
  <div className="popover-list-icon">
@@ -3604,6 +4573,24 @@ function MeetingsPopoverView({
3604
4573
  {formatMeetingWhen(meeting)}
3605
4574
  {meeting.platform ? ` · ${meeting.platform}` : ""}
3606
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}
3607
4594
  </div>
3608
4595
  {isActive ? (
3609
4596
  <button
@@ -3621,8 +4608,14 @@ function MeetingsPopoverView({
3621
4608
  className="popover-list-action popover-list-action-primary"
3622
4609
  onClick={() =>
3623
4610
  hasJoin
3624
- ? onStartNotesAndJoin(meeting)
3625
- : onStartNotes(meeting)
4611
+ ? onStartNotesAndJoin(
4612
+ meeting,
4613
+ consumeIncludeHistoryChoice(meeting),
4614
+ )
4615
+ : onStartNotes(
4616
+ meeting,
4617
+ consumeIncludeHistoryChoice(meeting),
4618
+ )
3626
4619
  }
3627
4620
  title={
3628
4621
  hasJoin
@@ -3758,6 +4751,30 @@ function BottomButton({
3758
4751
  );
3759
4752
  }
3760
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
+
3761
4778
  // ---- inline icons (Tabler-style, monochrome, stroke=1.75) -----------------
3762
4779
 
3763
4780
  // ---------------------------------------------------------------------------
@@ -3815,6 +4832,9 @@ function desktopUpdateStatusText(status: UpdateStatus): string {
3815
4832
  }
3816
4833
 
3817
4834
  function Setup({
4835
+ surface = "settings",
4836
+ promptRewindEnable = false,
4837
+ recordingActive = false,
3818
4838
  initial,
3819
4839
  serverUrl,
3820
4840
  signedInAs,
@@ -3834,9 +4854,14 @@ function Setup({
3834
4854
  onVoiceProviderChange,
3835
4855
  onVoiceInstructionsChange,
3836
4856
  onConnect,
4857
+ onOpenRewind,
4858
+ onOpenMemory,
3837
4859
  onCancel,
3838
4860
  onSignOut,
3839
4861
  }: {
4862
+ surface?: "settings" | "memory" | "rewind";
4863
+ promptRewindEnable?: boolean;
4864
+ recordingActive?: boolean;
3840
4865
  initial?: string | null;
3841
4866
  serverUrl?: string;
3842
4867
  signedInAs?: string | null;
@@ -3856,6 +4881,8 @@ function Setup({
3856
4881
  onVoiceProviderChange: (value: VoiceProvider) => void;
3857
4882
  onVoiceInstructionsChange: (value: string) => void;
3858
4883
  onConnect: (url: string) => void;
4884
+ onOpenRewind?: () => void;
4885
+ onOpenMemory?: () => void;
3859
4886
  onCancel?: () => void;
3860
4887
  onSignOut?: () => void;
3861
4888
  }) {
@@ -3869,8 +4896,26 @@ function Setup({
3869
4896
  const autoHidePopoverEnabled = featureConfig?.autoHidePopoverEnabled === true;
3870
4897
  const showInScreenCapture = featureConfig?.showInScreenCapture === true;
3871
4898
  const localRecordingMode = featureConfig?.localRecordingMode ?? "off";
3872
- const screenMemory =
4899
+ const observedScreenMemory =
3873
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]);
3874
4919
  const regionGuides = featureConfig?.regionGuides ?? {
3875
4920
  enabled: false,
3876
4921
  rects: [],
@@ -3900,14 +4945,74 @@ function Setup({
3900
4945
  kind: "ok" | "error";
3901
4946
  text: string;
3902
4947
  } | null>(null);
4948
+ const [screenMemoryExportResult, setScreenMemoryExportResult] =
4949
+ useState<ScreenMemoryExportResult | null>(null);
3903
4950
  const [clipDraftsError, setClipDraftsError] = useState<string | null>(null);
3904
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);
3905
4974
  const screenMemorySegments = screenMemoryStatus?.recentSegments ?? [];
3906
4975
  const screenMemoryTotalBytes = screenMemorySegments.reduce(
3907
4976
  (sum, segment) => sum + segment.bytes,
3908
4977
  0,
3909
4978
  );
3910
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
+ }, []);
3911
5016
 
3912
5017
  const [providerStatus, setProviderStatus] =
3913
5018
  useState<VoiceProviderStatus | null>(null);
@@ -3982,27 +5087,89 @@ function Setup({
3982
5087
  );
3983
5088
  }
3984
5089
 
3985
- function setScreenMemoryConfig(
3986
- patch: Partial<typeof DEFAULT_SCREEN_MEMORY_CONFIG>,
5090
+ async function setScreenMemoryConfig(
5091
+ patch: Partial<ScreenMemoryStatus["config"]>,
3987
5092
  ) {
3988
5093
  if (!featureConfig) return;
3989
5094
  setScreenMemoryMessage(null);
3990
- invoke("set_feature_config", {
3991
- config: {
3992
- ...featureConfig,
3993
- screenMemory: {
3994
- ...DEFAULT_SCREEN_MEMORY_CONFIG,
3995
- ...screenMemory,
3996
- ...patch,
3997
- },
3998
- },
3999
- }).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) {
4000
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
+ }
4001
5135
  setScreenMemoryMessage({
4002
5136
  kind: "error",
4003
- text: (err as Error)?.message ?? "Could not update Screen Memory.",
5137
+ text: (err as Error)?.message ?? "Could not update Rewind.",
4004
5138
  });
4005
- });
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
+ }
4006
5173
  }
4007
5174
 
4008
5175
  function refreshScreenMemoryStatus() {
@@ -4011,22 +5178,71 @@ function Setup({
4011
5178
  .catch(() => {});
4012
5179
  }
4013
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
+
4014
5232
  async function exportScreenMemoryRecent() {
4015
5233
  setScreenMemoryBusy(true);
4016
5234
  setScreenMemoryMessage(null);
5235
+ setScreenMemoryExportResult(null);
4017
5236
  try {
4018
5237
  const result = await invoke<ScreenMemoryExportResult>(
4019
5238
  "screen_memory_export_recent",
4020
5239
  { minutes: 5 },
4021
5240
  );
4022
- setScreenMemoryMessage({
4023
- kind: "ok",
4024
- text: `Saved ${result.files.length} segment${result.files.length === 1 ? "" : "s"} to ${result.folderPath}.`,
4025
- });
5241
+ setScreenMemoryExportResult(result);
4026
5242
  } catch (err) {
4027
5243
  setScreenMemoryMessage({
4028
5244
  kind: "error",
4029
- text: (err as Error)?.message ?? "Could not export Screen Memory.",
5245
+ text: err instanceof Error ? err.message : String(err),
4030
5246
  });
4031
5247
  } finally {
4032
5248
  setScreenMemoryBusy(false);
@@ -4042,11 +5258,11 @@ function Setup({
4042
5258
  "screen_memory_delete_all",
4043
5259
  );
4044
5260
  setScreenMemoryStatus(status);
4045
- setScreenMemoryMessage({ kind: "ok", text: "Screen Memory cleared." });
5261
+ setScreenMemoryMessage({ kind: "ok", text: "Rewind cleared." });
4046
5262
  } catch (err) {
4047
5263
  setScreenMemoryMessage({
4048
5264
  kind: "error",
4049
- text: (err as Error)?.message ?? "Could not clear Screen Memory.",
5265
+ text: (err as Error)?.message ?? "Could not clear Rewind.",
4050
5266
  });
4051
5267
  } finally {
4052
5268
  setScreenMemoryBusy(false);
@@ -4057,8 +5273,42 @@ function Setup({
4057
5273
  invoke("screen_memory_open_folder").catch((err) => {
4058
5274
  setScreenMemoryMessage({
4059
5275
  kind: "error",
4060
- 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.",
4061
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
+ ),
4062
5312
  });
4063
5313
  }
4064
5314
 
@@ -4295,8 +5545,7 @@ function Setup({
4295
5545
  };
4296
5546
  }, [serverUrl, initial]);
4297
5547
 
4298
- function handleSubmit(e: React.FormEvent) {
4299
- e.preventDefault();
5548
+ function handleConnect() {
4300
5549
  const trimmed = url.trim();
4301
5550
  if (!trimmed) return;
4302
5551
  onConnect(trimmed);
@@ -4460,10 +5709,709 @@ function Setup({
4460
5709
  });
4461
5710
  }
4462
5711
 
4463
- return (
4464
- <form className="setup" onSubmit={handleSubmit}>
4465
- <div className="setup-header">
4466
- {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">
5725
+ <button
5726
+ type="button"
5727
+ className={rewindConsentMode === "visuals" ? "selected" : ""}
5728
+ onClick={() => setRewindConsentMode("visuals")}
5729
+ >
5730
+ <strong>Visuals</strong>
5731
+ <span>Screen, app, and readable text</span>
5732
+ </button>
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 ? (
4467
6415
  <button
4468
6416
  type="button"
4469
6417
  className="setup-back"
@@ -4491,7 +6439,11 @@ function Setup({
4491
6439
  onChange={(e) => setUrl(e.target.value)}
4492
6440
  placeholder="http://localhost:8080"
4493
6441
  />
4494
- <button className="secondary setup-connect-button" type="submit">
6442
+ <button
6443
+ className="secondary setup-connect-button"
6444
+ type="button"
6445
+ onClick={handleConnect}
6446
+ >
4495
6447
  Connect
4496
6448
  </button>
4497
6449
  </div>
@@ -4528,7 +6480,7 @@ function Setup({
4528
6480
  <div className="setup-toggle-row">
4529
6481
  <SettingLabel
4530
6482
  label="Show Clips in screen captures"
4531
- 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."
4532
6484
  />
4533
6485
  <Switch
4534
6486
  on={showInScreenCapture}
@@ -4595,6 +6547,27 @@ function Setup({
4595
6547
 
4596
6548
  <div className="setup-section-heading">Recording</div>
4597
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
+
4598
6571
  <div className="setup-section">
4599
6572
  <SettingLabel
4600
6573
  label="Clip Drafts"
@@ -4613,31 +6586,37 @@ function Setup({
4613
6586
  ) : null}
4614
6587
  </div>
4615
6588
 
4616
- <div className="setup-section">
6589
+ <div className="setup-section setup-rewind-legacy">
4617
6590
  <div className="setup-toggle-row">
4618
6591
  <SettingLabel
4619
- label="Screen Memory"
4620
- 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."
4621
6594
  />
4622
6595
  <Switch
4623
6596
  on={screenMemory.enabled}
4624
6597
  onChange={(enabled) =>
4625
- setScreenMemoryConfig({ enabled, paused: false })
6598
+ void setScreenMemoryConfig({ enabled, paused: false })
4626
6599
  }
4627
- label="Enable Screen Memory"
6600
+ label="Enable Rewind"
6601
+ disabled={screenMemoryConfigBusy || captureControlsLocked}
4628
6602
  />
4629
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>
4630
6608
  {screenMemory.enabled ? (
4631
6609
  <>
4632
6610
  <div className="setup-toggle-row">
4633
6611
  <SettingLabel
4634
6612
  label="Pause capture"
4635
- hint="Stop recording new Screen Memory segments without clearing the local history."
6613
+ hint="Stop retaining new Rewind segments without clearing the local archive."
4636
6614
  />
4637
6615
  <Switch
4638
6616
  on={screenMemory.paused}
4639
- onChange={(paused) => setScreenMemoryConfig({ paused })}
4640
- label="Pause Screen Memory"
6617
+ onChange={(paused) => void setScreenMemoryConfig({ paused })}
6618
+ label="Pause Rewind"
6619
+ disabled={screenMemoryConfigBusy || captureControlsLocked}
4641
6620
  />
4642
6621
  </div>
4643
6622
  <div className="setup-grid">
@@ -4645,6 +6624,7 @@ function Setup({
4645
6624
  <span>Retention</span>
4646
6625
  <select
4647
6626
  className="setup-select"
6627
+ disabled={screenMemoryConfigBusy || captureControlsLocked}
4648
6628
  value={screenMemory.retentionHours}
4649
6629
  onChange={(event) =>
4650
6630
  setScreenMemoryConfig({
@@ -4654,13 +6634,13 @@ function Setup({
4654
6634
  >
4655
6635
  <option value={8}>8 hours</option>
4656
6636
  <option value={24}>24 hours</option>
4657
- <option value={72}>72 hours</option>
4658
6637
  </select>
4659
6638
  </label>
4660
6639
  <label className="setup-mini-field">
4661
6640
  <span>Disk cap</span>
4662
6641
  <select
4663
6642
  className="setup-select"
6643
+ disabled={screenMemoryConfigBusy}
4664
6644
  value={screenMemory.maxBytes}
4665
6645
  onChange={(event) =>
4666
6646
  setScreenMemoryConfig({
@@ -4674,6 +6654,89 @@ function Setup({
4674
6654
  </select>
4675
6655
  </label>
4676
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>
4677
6740
  <div className="whisper-status">
4678
6741
  {screenMemoryRecording ? (
4679
6742
  <IconCircleCheck size={13} className="whisper-status-icon" />
@@ -4681,22 +6744,134 @@ function Setup({
4681
6744
  <IconAlertTriangle size={13} className="whisper-status-icon" />
4682
6745
  )}
4683
6746
  <span>
4684
- {screenMemoryRecording
4685
- ? `Recording locally - ${screenMemorySegments.length} segment${screenMemorySegments.length === 1 ? "" : "s"}, ${formatStorageBytes(screenMemoryTotalBytes)}.`
4686
- : screenMemory.paused
4687
- ? "Paused. Existing local history is still available."
4688
- : screenMemoryStatus?.lastError
4689
- ? screenMemoryStatus.lastError
4690
- : "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."}
4691
6756
  </span>
4692
6757
  </div>
4693
6758
  {screenMemorySegments[0] ? (
4694
6759
  <p className="setup-hint">
4695
- Latest segment:{" "}
6760
+ Latest local coverage:{" "}
4696
6761
  {new Date(screenMemorySegments[0].endedAt).toLocaleTimeString()}
4697
6762
  .
4698
6763
  </p>
4699
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>
4700
6875
  <div className="setup-button-row">
4701
6876
  <button
4702
6877
  type="button"
@@ -4722,9 +6897,89 @@ function Setup({
4722
6897
  disabled={screenMemoryBusy || screenMemorySegments.length === 0}
4723
6898
  >
4724
6899
  <IconTrash size={15} stroke={1.9} />
4725
- Clear
6900
+ Clear Rewind
4726
6901
  </button>
4727
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>
4728
6983
  {screenMemoryMessage ? (
4729
6984
  <p
4730
6985
  className={
@@ -5188,7 +7443,7 @@ function Setup({
5188
7443
  </button>
5189
7444
  </div>
5190
7445
  ) : null}
5191
- </form>
7446
+ </div>
5192
7447
  );
5193
7448
  }
5194
7449