@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.
- package/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +14 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/cli/mcp-config-writers.ts +6 -4
- package/corpus/core/src/cli/mcp.ts +124 -0
- package/corpus/core/src/cli/skills-content/help.ts +4 -3
- package/corpus/core/src/cli/skills-content/index.ts +1 -0
- package/corpus/core/src/cli/skills-content/rewind-skill.ts +52 -0
- package/corpus/core/src/cli/skills.ts +115 -13
- package/corpus/core/src/client/resources/ResourceTree.tsx +11 -5
- package/corpus/core/src/client/resources/ResourcesPanel.tsx +141 -18
- package/corpus/core/src/mcp/screen-memory-stdio.ts +1360 -104
- package/corpus/core/src/mcp-client/index.ts +9 -0
- package/corpus/core/src/mcp-client/screen-memory-local.ts +847 -13
- package/corpus/templates/clips/actions/apply-rewind-extension.ts +195 -0
- package/corpus/templates/clips/actions/delete-agent-recording-if-unpromoted.ts +64 -0
- package/corpus/templates/clips/actions/get-rewind-extension-request.ts +22 -0
- package/corpus/templates/clips/actions/get-screen-memory-status.ts +18 -2
- package/corpus/templates/clips/actions/list-rewind-extension-requests.ts +52 -0
- package/corpus/templates/clips/actions/make-recording-private-for-rewind.ts +55 -0
- package/corpus/templates/clips/actions/query-screen-memory-context.ts +4 -4
- package/corpus/templates/clips/actions/request-rewind-extension.ts +81 -0
- package/corpus/templates/clips/actions/update-rewind-extension-request.ts +47 -0
- package/corpus/templates/clips/app/components/editor/editor-layout.tsx +25 -0
- package/corpus/templates/clips/app/components/editor/editor-toolbar.tsx +36 -0
- package/corpus/templates/clips/app/components/editor/rewind-extension-dialog.tsx +338 -0
- package/corpus/templates/clips/app/components/editor/stitch-manager.tsx +1 -16
- package/corpus/templates/clips/app/components/editor/timeline.tsx +26 -0
- package/corpus/templates/clips/app/components/meetings/meeting-card.tsx +1 -2
- package/corpus/templates/clips/app/i18n/en-US.ts +18 -0
- package/corpus/templates/clips/app/lib/ffmpeg-export.ts +19 -12
- package/corpus/templates/clips/app/lib/rewind-visibility.ts +31 -0
- package/corpus/templates/clips/app/lib/timestamp-mapping.ts +7 -0
- package/corpus/templates/clips/app/lib/upload-file-client.ts +17 -0
- package/corpus/templates/clips/changelog/2026-07-14-rewind-keeps-recent-screen-context-local-and-turns-it-into-clips.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-17-active-clips-keep-rewind-stable-and-settings-clear.md +12 -0
- package/corpus/templates/clips/changelog/2026-07-17-clicking-the-clips-icon-during-a-recording-now-opens-the-app.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-17-default-mic-now-uses-the-mac-default-and-transcription-shares-one-capture.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-17-full-screen-clips-finalize-even-when-screencapturekit-stop-hangs.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-17-normal-full-screen-clips-no-longer-stop-immediately-after-th.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-17-rewind-can-hand-one-bounded-private-clip-to-your-agent.md +12 -0
- package/corpus/templates/clips/changelog/2026-07-17-rewind-home-can-copy-a-local-agent-prompt.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-17-rewind-now-has-a-calm-home-status-focused-first-run-consent-.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-17-rewind-settings-are-smaller-clearer-and-agent-first.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-17-the-full-clips-interface-stays-available-while-recording.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-18-rewind-organizes-recent-work-into-local-searchable-chapters.md +8 -0
- package/corpus/templates/clips/changelog/2026-07-18-searchable-rewind-chapters.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-19-rewind-backed-recordings-now-start-immediately-after-the-cou.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-20-opening-zoom-meeting-launches-native-app.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-20-rewind-can-set-up-your-local-agent-once-so-future-requests-w.md +6 -0
- package/corpus/templates/clips/desktop/package.json +1 -0
- package/corpus/templates/clips/desktop/scripts/sign-macos-local.ts +12 -1
- package/corpus/templates/clips/desktop/src/app.tsx +2478 -223
- package/corpus/templates/clips/desktop/src/components/ReadinessPanel.tsx +21 -1
- package/corpus/templates/clips/desktop/src/components/Switch.tsx +3 -0
- package/corpus/templates/clips/desktop/src/hooks/useMeetingTranscription.ts +156 -24
- package/corpus/templates/clips/desktop/src/lib/native-recording-warm.ts +6 -7
- package/corpus/templates/clips/desktop/src/lib/open-meeting-join-url.ts +22 -0
- package/corpus/templates/clips/desktop/src/lib/recorder.ts +1763 -1160
- package/corpus/templates/clips/desktop/src/lib/rewind-agent-prompt.ts +9 -0
- package/corpus/templates/clips/desktop/src/lib/rewind-recording-start.ts +20 -0
- package/corpus/templates/clips/desktop/src/main.tsx +3 -0
- package/corpus/templates/clips/desktop/src/overlays/countdown.tsx +14 -14
- package/corpus/templates/clips/desktop/src/overlays/meeting-notification.tsx +2 -3
- package/corpus/templates/clips/desktop/src/overlays/onboarding.tsx +4 -4
- package/corpus/templates/clips/desktop/src/overlays/preparing.tsx +13 -0
- package/corpus/templates/clips/desktop/src/overlays/toolbar.tsx +27 -2
- package/corpus/templates/clips/desktop/src/shared/config.ts +16 -0
- package/corpus/templates/clips/desktop/src/styles.css +956 -134
- package/corpus/templates/clips/desktop/src-tauri/Cargo.toml +5 -0
- package/corpus/templates/clips/desktop/src-tauri/build.rs +69 -1
- package/corpus/templates/clips/desktop/src-tauri/src/accessibility.rs +386 -12
- package/corpus/templates/clips/desktop/src-tauri/src/capture_audio_bus.rs +382 -0
- package/corpus/templates/clips/desktop/src-tauri/src/capture_graph.rs +777 -0
- package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +249 -22
- package/corpus/templates/clips/desktop/src-tauri/src/config.rs +148 -1
- package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +58 -4
- package/corpus/templates/clips/desktop/src-tauri/src/logfile.rs +13 -1
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen/custom_capture.rs +1505 -51
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen/live_upload.rs +124 -0
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +1006 -95
- package/corpus/templates/clips/desktop/src-tauri/src/rewind_capture_suspension.rs +165 -0
- package/corpus/templates/clips/desktop/src-tauri/src/rewind_chapters.rs +2434 -0
- package/corpus/templates/clips/desktop/src-tauri/src/rewind_clip.rs +1650 -0
- package/corpus/templates/clips/desktop/src-tauri/src/rewind_egress.rs +643 -0
- package/corpus/templates/clips/desktop/src-tauri/src/rewind_local_ask.rs +803 -0
- package/corpus/templates/clips/desktop/src-tauri/src/rewind_meeting_history.rs +595 -0
- package/corpus/templates/clips/desktop/src-tauri/src/screen_memory.rs +2900 -91
- package/corpus/templates/clips/desktop/src-tauri/src/screen_memory_ocr.rs +360 -0
- package/corpus/templates/clips/desktop/src-tauri/src/screen_memory_transcript.rs +408 -0
- package/corpus/templates/clips/desktop/src-tauri/src/shortcuts.rs +114 -33
- package/corpus/templates/clips/desktop/src-tauri/src/state.rs +2 -2
- package/corpus/templates/clips/desktop/src-tauri/src/system_audio.rs +13 -3
- package/corpus/templates/clips/desktop/src-tauri/src/tray.rs +4 -1
- package/corpus/templates/clips/desktop/src-tauri/src/util.rs +7 -13
- package/corpus/templates/clips/desktop/src-tauri/src/whisper_speech.rs +198 -11
- package/corpus/templates/clips/desktop/src-tauri/tauri.alpha.conf.json +17 -0
- package/corpus/templates/clips/shared/meeting-join-url.ts +0 -5
- package/corpus/templates/content/.agents/skills/document-editing/SKILL.md +9 -1
- package/corpus/templates/content/AGENTS.md +4 -1
- package/corpus/templates/content/actions/_content-spaces.ts +6 -1
- package/corpus/templates/content/actions/_database-row-batch.ts +15 -2
- package/corpus/templates/content/actions/_database-utils.ts +83 -15
- package/corpus/templates/content/actions/add-database-item.ts +3 -0
- package/corpus/templates/content/actions/connect-local-folder-source.ts +2 -1
- package/corpus/templates/content/actions/delete-content-database.ts +12 -9
- package/corpus/templates/content/actions/delete-database-items.ts +3 -2
- package/corpus/templates/content/actions/delete-document.ts +233 -6
- package/corpus/templates/content/actions/duplicate-database-item.ts +1 -0
- package/corpus/templates/content/actions/get-document.ts +4 -1
- package/corpus/templates/content/actions/list-content-databases.ts +1 -0
- package/corpus/templates/content/actions/list-documents.ts +1 -0
- package/corpus/templates/content/actions/list-trashed-content-databases.ts +8 -2
- package/corpus/templates/content/actions/list-trashed-documents.ts +50 -0
- package/corpus/templates/content/actions/permanently-delete-document.ts +28 -0
- package/corpus/templates/content/actions/pull-document.ts +15 -2
- package/corpus/templates/content/actions/restore-content-database.ts +48 -11
- package/corpus/templates/content/actions/restore-document.ts +28 -0
- package/corpus/templates/content/actions/search-documents.ts +2 -1
- package/corpus/templates/content/actions/view-screen.ts +2 -1
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +149 -55
- package/corpus/templates/content/app/components/editor/database/GalleryView.tsx +7 -2
- package/corpus/templates/content/app/components/editor/database/ListView.tsx +7 -2
- package/corpus/templates/content/app/components/editor/database/TimelineView.tsx +3 -0
- package/corpus/templates/content/app/components/editor/database/sidebar.tsx +26 -5
- package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +243 -163
- package/corpus/templates/content/app/components/sidebar/WorkspaceSourceMenu.tsx +154 -0
- package/corpus/templates/content/app/components/sidebar/select-content-space.ts +11 -0
- package/corpus/templates/content/app/hooks/use-content-database.ts +6 -0
- package/corpus/templates/content/app/hooks/use-documents.ts +57 -0
- package/corpus/templates/content/app/i18n-data.ts +83 -2
- package/corpus/templates/content/app/routes/_app.local-files.tsx +8 -1
- package/corpus/templates/content/changelog/2026-07-19-pages-now-move-to-a-reversible-trash-and-the-organization-pi.md +6 -0
- package/corpus/templates/content/changelog/2026-07-20-trash-actions-now-preserve-independent-archived-pages-enforc.md +6 -0
- package/corpus/templates/content/changelog/2026-07-20-workspace-toggles-now-stay-independently-open-or-closed-and-.md +6 -0
- package/corpus/templates/content/changelog/2026-07-20-workspaces-can-be-added-from-a-blank-workspace-or-a-connecte.md +6 -0
- package/corpus/templates/content/parity/matrix.md +1 -1
- package/corpus/templates/content/parity/matrix.ts +3 -0
- package/corpus/templates/content/server/db/schema.ts +2 -0
- package/corpus/templates/content/server/lib/public-documents.ts +8 -2
- package/corpus/templates/content/server/plugins/db.ts +52 -0
- package/corpus/templates/content/server/routes/api/document-agent-context.json.get.ts +2 -2
- package/corpus/templates/content/shared/api.ts +10 -0
- package/corpus/toolkit/CHANGELOG.md +6 -0
- package/corpus/toolkit/package.json +1 -1
- package/corpus/toolkit/src/ui/progress.tsx +3 -2
- package/dist/cli/mcp-config-writers.d.ts +2 -2
- package/dist/cli/mcp-config-writers.d.ts.map +1 -1
- package/dist/cli/mcp-config-writers.js +6 -6
- package/dist/cli/mcp-config-writers.js.map +1 -1
- package/dist/cli/mcp.d.ts +14 -0
- package/dist/cli/mcp.d.ts.map +1 -1
- package/dist/cli/mcp.js +79 -0
- package/dist/cli/mcp.js.map +1 -1
- package/dist/cli/skills-content/help.d.ts +1 -1
- package/dist/cli/skills-content/help.d.ts.map +1 -1
- package/dist/cli/skills-content/help.js +4 -3
- package/dist/cli/skills-content/help.js.map +1 -1
- package/dist/cli/skills-content/index.d.ts +1 -0
- package/dist/cli/skills-content/index.d.ts.map +1 -1
- package/dist/cli/skills-content/index.js +1 -0
- package/dist/cli/skills-content/index.js.map +1 -1
- package/dist/cli/skills-content/rewind-skill.d.ts +2 -0
- package/dist/cli/skills-content/rewind-skill.d.ts.map +1 -0
- package/dist/cli/skills-content/rewind-skill.js +53 -0
- package/dist/cli/skills-content/rewind-skill.js.map +1 -0
- package/dist/cli/skills.d.ts +6 -0
- package/dist/cli/skills.d.ts.map +1 -1
- package/dist/cli/skills.js +90 -14
- package/dist/cli/skills.js.map +1 -1
- package/dist/client/resources/ResourceTree.d.ts +3 -1
- package/dist/client/resources/ResourceTree.d.ts.map +1 -1
- package/dist/client/resources/ResourceTree.js +4 -4
- package/dist/client/resources/ResourceTree.js.map +1 -1
- package/dist/client/resources/ResourcesPanel.d.ts.map +1 -1
- package/dist/client/resources/ResourcesPanel.js +55 -6
- package/dist/client/resources/ResourcesPanel.js.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/mcp/screen-memory-stdio.d.ts +365 -0
- package/dist/mcp/screen-memory-stdio.d.ts.map +1 -1
- package/dist/mcp/screen-memory-stdio.js +1014 -91
- package/dist/mcp/screen-memory-stdio.js.map +1 -1
- package/dist/mcp-client/index.d.ts +1 -1
- package/dist/mcp-client/index.d.ts.map +1 -1
- package/dist/mcp-client/index.js +1 -1
- package/dist/mcp-client/index.js.map +1 -1
- package/dist/mcp-client/screen-memory-local.d.ts +90 -0
- package/dist/mcp-client/screen-memory-local.d.ts.map +1 -1
- package/dist/mcp-client/screen-memory-local.js +590 -12
- package/dist/mcp-client/screen-memory-local.js.map +1 -1
- package/dist/notifications/routes.d.ts +2 -2
- package/dist/observability/routes.d.ts +5 -5
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/actions/provider-api.d.ts +4 -4
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/package.json +2 -2
- package/src/cli/mcp-config-writers.ts +6 -4
- package/src/cli/mcp.ts +124 -0
- package/src/cli/skills-content/help.ts +4 -3
- package/src/cli/skills-content/index.ts +1 -0
- package/src/cli/skills-content/rewind-skill.ts +52 -0
- package/src/cli/skills.ts +115 -13
- package/src/client/resources/ResourceTree.tsx +11 -5
- package/src/client/resources/ResourcesPanel.tsx +141 -18
- package/src/mcp/screen-memory-stdio.ts +1360 -104
- package/src/mcp-client/index.ts +9 -0
- package/src/mcp-client/screen-memory-local.ts +847 -13
|
@@ -3,13 +3,18 @@ import {
|
|
|
3
3
|
IconArrowLeft,
|
|
4
4
|
IconCalendarEvent,
|
|
5
5
|
IconCircleCheck,
|
|
6
|
+
IconCopy,
|
|
6
7
|
IconDownload,
|
|
7
8
|
IconExternalLink,
|
|
8
9
|
IconFolderOpen,
|
|
9
10
|
IconPencil,
|
|
10
11
|
IconInfoCircle,
|
|
12
|
+
IconHistory,
|
|
11
13
|
IconMicrophone2,
|
|
14
|
+
IconPlayerPlay,
|
|
12
15
|
IconRefresh,
|
|
16
|
+
IconSearch,
|
|
17
|
+
IconShieldLock,
|
|
13
18
|
IconTrash,
|
|
14
19
|
IconUpload,
|
|
15
20
|
IconX,
|
|
@@ -55,7 +60,7 @@ import {
|
|
|
55
60
|
getCameraStreamWithFallback,
|
|
56
61
|
isMediaConstraintFailure,
|
|
57
62
|
} from "./lib/media-capture-constraints";
|
|
58
|
-
import {
|
|
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 =
|
|
138
|
+
type PopoverView =
|
|
139
|
+
| "recorder"
|
|
140
|
+
| "memory"
|
|
141
|
+
| "rewind-settings"
|
|
142
|
+
| "settings"
|
|
143
|
+
| "meetings"
|
|
144
|
+
| "dictation";
|
|
130
145
|
|
|
131
146
|
interface PopoverMeeting {
|
|
132
147
|
id: string;
|
|
@@ -138,6 +153,52 @@ interface PopoverMeeting {
|
|
|
138
153
|
transcriptStatus: string | null;
|
|
139
154
|
}
|
|
140
155
|
|
|
156
|
+
interface RewindMeetingHistoryAvailability {
|
|
157
|
+
available: boolean;
|
|
158
|
+
reason?: string | null;
|
|
159
|
+
coveredFrom?: string | null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
interface RewindAgentHandoffRequest {
|
|
163
|
+
requestId: string;
|
|
164
|
+
status: "pending" | "processing" | "ready" | "declined" | "failed";
|
|
165
|
+
requestedAt: string;
|
|
166
|
+
startAt: string;
|
|
167
|
+
endAt: string;
|
|
168
|
+
durationMs: number;
|
|
169
|
+
reason: string;
|
|
170
|
+
includeMicrophone: boolean;
|
|
171
|
+
includeSystemAudio: boolean;
|
|
172
|
+
reviewRequired: boolean;
|
|
173
|
+
agentClipRetention: "forever" | "24-hours" | "7-days" | "30-days";
|
|
174
|
+
recordingId?: string;
|
|
175
|
+
agentUrl?: string;
|
|
176
|
+
contextUrl?: string;
|
|
177
|
+
expiresAt?: string;
|
|
178
|
+
error?: string;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
interface RewindExtensionRequest {
|
|
182
|
+
requestId: string;
|
|
183
|
+
recordingId: string;
|
|
184
|
+
seconds: 30 | 300;
|
|
185
|
+
status: "pending" | "processing" | "ready" | "failed";
|
|
186
|
+
updatedAt: string;
|
|
187
|
+
preRollRecordingId?: string;
|
|
188
|
+
actualDurationMs?: number;
|
|
189
|
+
error?: string;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
interface NativeRewindUploadResult {
|
|
193
|
+
recordingId: string;
|
|
194
|
+
durationMs: number;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
interface DueRewindAgentHandoff {
|
|
198
|
+
requestId: string;
|
|
199
|
+
recordingId: string;
|
|
200
|
+
}
|
|
201
|
+
|
|
141
202
|
interface LocalRecordingNotice {
|
|
142
203
|
folderPath?: string;
|
|
143
204
|
files: LocalExportedFile[];
|
|
@@ -153,6 +214,74 @@ interface ScreenMemoryExportResult {
|
|
|
153
214
|
}>;
|
|
154
215
|
}
|
|
155
216
|
|
|
217
|
+
interface RewindEgressEvent {
|
|
218
|
+
requestId: string;
|
|
219
|
+
occurredAt: string;
|
|
220
|
+
state:
|
|
221
|
+
| "prepared"
|
|
222
|
+
| "completed"
|
|
223
|
+
| "failed"
|
|
224
|
+
| "local-evidence-read"
|
|
225
|
+
| "handoff-requested";
|
|
226
|
+
evidenceCount: number;
|
|
227
|
+
packetBytes: number;
|
|
228
|
+
error?: string | null;
|
|
229
|
+
operation?: string | null;
|
|
230
|
+
receipt?: {
|
|
231
|
+
evidence?: Array<{
|
|
232
|
+
id: string;
|
|
233
|
+
momentId: string;
|
|
234
|
+
sourceType: "app-context" | "transcript" | "ocr" | "chapter";
|
|
235
|
+
capturedAt?: string | null;
|
|
236
|
+
}>;
|
|
237
|
+
frames?: Array<{ timestamp: string; segmentId: string }>;
|
|
238
|
+
mediaInterval?: { startAt: string; endAt: string };
|
|
239
|
+
reviewRequired?: boolean;
|
|
240
|
+
} | null;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
interface RewindLocalAskResult {
|
|
244
|
+
query: string;
|
|
245
|
+
answerSummary: string;
|
|
246
|
+
evidence: Array<{
|
|
247
|
+
id: string;
|
|
248
|
+
sourceType: "app-context" | "transcript" | "ocr";
|
|
249
|
+
capturedAt: string;
|
|
250
|
+
excerpt: string;
|
|
251
|
+
confidence?: number | null;
|
|
252
|
+
segmentId: string;
|
|
253
|
+
offsetMs: number;
|
|
254
|
+
}>;
|
|
255
|
+
coverage: {
|
|
256
|
+
segmentsConsidered: number;
|
|
257
|
+
transcriptIndexesReady: number;
|
|
258
|
+
ocrIndexesReady: number;
|
|
259
|
+
gaps: Array<{
|
|
260
|
+
kind: string;
|
|
261
|
+
source: string;
|
|
262
|
+
startedAt?: string | null;
|
|
263
|
+
endedAt?: string | null;
|
|
264
|
+
detail: string;
|
|
265
|
+
}>;
|
|
266
|
+
};
|
|
267
|
+
confidence: string;
|
|
268
|
+
truncated: boolean;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
interface RewindExcludedApplication {
|
|
272
|
+
bundleId: string;
|
|
273
|
+
name: string;
|
|
274
|
+
path?: string | null;
|
|
275
|
+
installed: boolean;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
interface RewindAgentConnectionStatus {
|
|
279
|
+
client: "codex" | "claude-code";
|
|
280
|
+
configured: boolean;
|
|
281
|
+
configPath: string;
|
|
282
|
+
storeDir: string;
|
|
283
|
+
}
|
|
284
|
+
|
|
156
285
|
type MeetingTranscriptionMode = "manual" | "ask" | "auto";
|
|
157
286
|
|
|
158
287
|
type CaptureMode = "screen" | "screen-camera" | "camera";
|
|
@@ -165,12 +294,35 @@ const STORAGE_SETUP_FAILURE_RE =
|
|
|
165
294
|
const DEFAULT_SCREEN_MEMORY_CONFIG = {
|
|
166
295
|
enabled: false,
|
|
167
296
|
paused: false,
|
|
168
|
-
retentionHours:
|
|
297
|
+
retentionHours: 8,
|
|
169
298
|
maxBytes: 20 * 1024 * 1024 * 1024,
|
|
170
299
|
segmentSeconds: 5 * 60,
|
|
171
300
|
sampleIntervalSeconds: 10,
|
|
301
|
+
captureMode: "visuals" as const,
|
|
302
|
+
reviewBeforeSending: true,
|
|
303
|
+
autoPreviewBeforeSending: true,
|
|
304
|
+
agentClipRetention: "forever" as const,
|
|
305
|
+
excludedBundleIds: [
|
|
306
|
+
"com.1password.1password",
|
|
307
|
+
"com.agilebits.onepassword7",
|
|
308
|
+
"com.bitwarden.desktop",
|
|
309
|
+
"com.dashlane.dashlane",
|
|
310
|
+
"com.lastpass.lastpass",
|
|
311
|
+
],
|
|
312
|
+
excludePrivateWindows: false,
|
|
172
313
|
};
|
|
173
314
|
|
|
315
|
+
function parseExcludedBundleIds(value: string): string[] {
|
|
316
|
+
return [
|
|
317
|
+
...new Set(
|
|
318
|
+
value
|
|
319
|
+
.split(/[\n,]/)
|
|
320
|
+
.map((id) => id.trim())
|
|
321
|
+
.filter(Boolean),
|
|
322
|
+
),
|
|
323
|
+
];
|
|
324
|
+
}
|
|
325
|
+
|
|
174
326
|
function isStorageSetupFailureMessage(message: string | null | undefined) {
|
|
175
327
|
return STORAGE_SETUP_FAILURE_RE.test(message ?? "");
|
|
176
328
|
}
|
|
@@ -367,17 +519,17 @@ type MacosPrivacyPane =
|
|
|
367
519
|
|
|
368
520
|
const MACOS_PRIVACY_URLS: Record<MacosPrivacyPane, string> = {
|
|
369
521
|
camera:
|
|
370
|
-
"x-apple.systempreferences:com.apple.
|
|
522
|
+
"x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Privacy_Camera",
|
|
371
523
|
microphone:
|
|
372
|
-
"x-apple.systempreferences:com.apple.
|
|
524
|
+
"x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Privacy_Microphone",
|
|
373
525
|
screen:
|
|
374
|
-
"x-apple.systempreferences:com.apple.
|
|
526
|
+
"x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Privacy_ScreenCapture",
|
|
375
527
|
speech:
|
|
376
|
-
"x-apple.systempreferences:com.apple.
|
|
528
|
+
"x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Privacy_SpeechRecognition",
|
|
377
529
|
accessibility:
|
|
378
|
-
"x-apple.systempreferences:com.apple.
|
|
530
|
+
"x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Privacy_Accessibility",
|
|
379
531
|
"input-monitoring":
|
|
380
|
-
"x-apple.systempreferences:com.apple.
|
|
532
|
+
"x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Privacy_ListenEvent",
|
|
381
533
|
};
|
|
382
534
|
|
|
383
535
|
const WINDOWS_PRIVACY_URLS: Partial<Record<MacosPrivacyPane, string>> = {
|
|
@@ -552,6 +704,17 @@ function measurePopoverHeight(el: HTMLElement): number {
|
|
|
552
704
|
|
|
553
705
|
const candidates = [rect.height, el.scrollHeight + borderY];
|
|
554
706
|
|
|
707
|
+
// When a direct child is the scrollable content pane, the shell's own
|
|
708
|
+
// scrollHeight already includes the fixed footer but excludes content that
|
|
709
|
+
// is hidden inside that child. Add only that hidden delta to the shell
|
|
710
|
+
// baseline. Measuring the child's full height alone would omit the footer
|
|
711
|
+
// and leave the resumed recorder window partially clipped.
|
|
712
|
+
const directChildOverflow = Array.from(el.children).reduce((total, child) => {
|
|
713
|
+
if (!(child instanceof HTMLElement)) return total;
|
|
714
|
+
return total + Math.max(0, child.scrollHeight - child.clientHeight);
|
|
715
|
+
}, 0);
|
|
716
|
+
candidates.push(el.scrollHeight + borderY + directChildOverflow);
|
|
717
|
+
|
|
555
718
|
// ResizeObserver on `.app` alone misses scroll-only and absolutely
|
|
556
719
|
// positioned growth. Measure descendant bounds so menus, banners, and
|
|
557
720
|
// settings sections can grow the native window even when `.app` is capped
|
|
@@ -563,6 +726,19 @@ function measurePopoverHeight(el: HTMLElement): number {
|
|
|
563
726
|
const childRect = child.getBoundingClientRect();
|
|
564
727
|
if (childRect.width === 0 && childRect.height === 0) continue;
|
|
565
728
|
lowestBottom = Math.max(lowestBottom, childRect.bottom);
|
|
729
|
+
|
|
730
|
+
// Recorder Home deliberately keeps its footer fixed and lets the content
|
|
731
|
+
// region scroll when the native window is short. A newly inserted row can
|
|
732
|
+
// therefore grow `child.scrollHeight` without changing any descendant's
|
|
733
|
+
// visible bounding box. Include that hidden overflow in the desired
|
|
734
|
+
// window height so a state transition (for example paused -> remembering)
|
|
735
|
+
// can grow the popover back to its natural size.
|
|
736
|
+
const childBorderY =
|
|
737
|
+
Number.parseFloat(childStyle.borderTopWidth || "0") +
|
|
738
|
+
Number.parseFloat(childStyle.borderBottomWidth || "0");
|
|
739
|
+
candidates.push(
|
|
740
|
+
childRect.top - rect.top + child.scrollHeight + childBorderY,
|
|
741
|
+
);
|
|
566
742
|
}
|
|
567
743
|
candidates.push(lowestBottom - rect.top);
|
|
568
744
|
|
|
@@ -711,12 +887,33 @@ export function App() {
|
|
|
711
887
|
const [localRecordingNotice, setLocalRecordingNotice] =
|
|
712
888
|
useState<LocalRecordingNotice | null>(null);
|
|
713
889
|
const [popoverView, setPopoverView] = useState<PopoverView>("recorder");
|
|
890
|
+
const [rewindSettingsReturnView, setRewindSettingsReturnView] = useState<
|
|
891
|
+
"recorder" | "settings"
|
|
892
|
+
>("recorder");
|
|
893
|
+
const [homeScreenMemoryStatus, setHomeScreenMemoryStatus] =
|
|
894
|
+
useState<ScreenMemoryStatus | null>(null);
|
|
895
|
+
const [homeScreenMemoryBusy, setHomeScreenMemoryBusy] = useState(false);
|
|
896
|
+
const [rewindAgentPromptCopied, setRewindAgentPromptCopied] = useState(false);
|
|
897
|
+
const [agentHandoff, setAgentHandoff] =
|
|
898
|
+
useState<RewindAgentHandoffRequest | null>(null);
|
|
899
|
+
const agentHandoffProcessingRef = useRef<string | null>(null);
|
|
900
|
+
const agentHandoffPreviewedRef = useRef<Set<string>>(new Set());
|
|
901
|
+
const rewindExtensionProcessingRef = useRef<Set<string>>(new Set());
|
|
902
|
+
const [agentHandoffPreviewBusy, setAgentHandoffPreviewBusy] = useState(false);
|
|
903
|
+
const [agentHandoffPreviewError, setAgentHandoffPreviewError] = useState<
|
|
904
|
+
string | null
|
|
905
|
+
>(null);
|
|
906
|
+
const [promptRewindEnable, setPromptRewindEnable] = useState(false);
|
|
714
907
|
const [meetings, setMeetings] = useState<PopoverMeeting[]>([]);
|
|
715
908
|
const [meetingsLoading, setMeetingsLoading] = useState(false);
|
|
716
909
|
const [meetingsError, setMeetingsError] = useState<string | null>(null);
|
|
717
910
|
const [meetingStartMessage, setMeetingStartMessage] = useState<string | null>(
|
|
718
911
|
null,
|
|
719
912
|
);
|
|
913
|
+
const [
|
|
914
|
+
rewindMeetingHistoryAvailability,
|
|
915
|
+
setRewindMeetingHistoryAvailability,
|
|
916
|
+
] = useState<Record<string, RewindMeetingHistoryAvailability>>({});
|
|
720
917
|
const [activeMeetingId, setActiveMeetingId] = useState<string | null>(null);
|
|
721
918
|
const [readinessOpen, setReadinessOpen] = useState<boolean>(
|
|
722
919
|
() => !loadBool(READINESS_REVIEWED_KEY, false),
|
|
@@ -749,6 +946,26 @@ export function App() {
|
|
|
749
946
|
const isRecording = recorder !== null;
|
|
750
947
|
// Whether the popover window is shown; driven by the visibility effect below.
|
|
751
948
|
const [popoverVisible, setPopoverVisible] = useState(false);
|
|
949
|
+
useEffect(() => {
|
|
950
|
+
if (featureConfig?.screenMemory?.enabled !== true) {
|
|
951
|
+
setHomeScreenMemoryStatus(null);
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
954
|
+
let cancelled = false;
|
|
955
|
+
const refresh = () => {
|
|
956
|
+
invoke<ScreenMemoryStatus>("screen_memory_status")
|
|
957
|
+
.then((status) => {
|
|
958
|
+
if (!cancelled) setHomeScreenMemoryStatus(status);
|
|
959
|
+
})
|
|
960
|
+
.catch(() => {});
|
|
961
|
+
};
|
|
962
|
+
refresh();
|
|
963
|
+
const timer = window.setInterval(refresh, popoverVisible ? 5_000 : 30_000);
|
|
964
|
+
return () => {
|
|
965
|
+
cancelled = true;
|
|
966
|
+
window.clearInterval(timer);
|
|
967
|
+
};
|
|
968
|
+
}, [featureConfig?.screenMemory?.enabled, popoverVisible]);
|
|
752
969
|
const recordShortcutHandlerRef = useRef<() => void>(() => {});
|
|
753
970
|
// Mirrors `bubbleActive` (assigned below once it is computed) so device
|
|
754
971
|
// probes can synchronously tell whether the camera bubble owns the grant.
|
|
@@ -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
|
-
|
|
1034
|
-
.
|
|
1035
|
-
|
|
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
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
}
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
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
|
-
|
|
1166
|
-
(err)
|
|
1167
|
-
|
|
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
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 === "
|
|
3397
|
+
if (popoverView === "settings") {
|
|
2589
3398
|
return (
|
|
2590
|
-
<div className="app app-
|
|
3399
|
+
<div className="app app-settings" ref={appRef}>
|
|
2591
3400
|
{pendingUploadBanner}
|
|
2592
|
-
<
|
|
2593
|
-
|
|
2594
|
-
|
|
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
|
-
<
|
|
2695
|
-
|
|
2696
|
-
|
|
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
|
-
|
|
2699
|
-
<LocalRecordingModeBanner mode={localRecordingMode} />
|
|
2700
|
-
) : null}
|
|
3556
|
+
{pendingUploadBanner}
|
|
2701
3557
|
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
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
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
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
|
-
|
|
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="
|
|
2729
|
-
devices={
|
|
2730
|
-
selectedId={
|
|
2731
|
-
selectedLabel={
|
|
3605
|
+
kind="mic"
|
|
3606
|
+
devices={micDevices}
|
|
3607
|
+
selectedId={selectedMicId}
|
|
3608
|
+
selectedLabel={micLabel}
|
|
2732
3609
|
onSelect={(id, label) => {
|
|
2733
|
-
|
|
2734
|
-
|
|
3610
|
+
setMicId(id);
|
|
3611
|
+
setMicLabel(label);
|
|
2735
3612
|
}}
|
|
2736
|
-
onRefresh={() => requestDeviceAccess("
|
|
2737
|
-
on={
|
|
2738
|
-
onToggle={
|
|
3613
|
+
onRefresh={() => requestDeviceAccess("mic")}
|
|
3614
|
+
on={micOn}
|
|
3615
|
+
onToggle={setMicOn}
|
|
3616
|
+
systemAudio={systemAudioOn}
|
|
3617
|
+
onSystemAudioToggle={setSystemAudioOn}
|
|
3618
|
+
meterActive={popoverVisible && !isRecording && !recordingFlowActive}
|
|
2739
3619
|
/>
|
|
2740
|
-
|
|
3620
|
+
</div>
|
|
2741
3621
|
|
|
2742
|
-
<
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
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
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
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
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
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
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
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: (
|
|
3535
|
-
|
|
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(
|
|
3625
|
-
|
|
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
|
|
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<
|
|
5090
|
+
async function setScreenMemoryConfig(
|
|
5091
|
+
patch: Partial<ScreenMemoryStatus["config"]>,
|
|
3987
5092
|
) {
|
|
3988
5093
|
if (!featureConfig) return;
|
|
3989
5094
|
setScreenMemoryMessage(null);
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
|
|
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
|
|
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
|
-
|
|
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:
|
|
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: "
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
4464
|
-
|
|
4465
|
-
<div className="setup-
|
|
4466
|
-
|
|
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
|
|
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="
|
|
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="
|
|
4620
|
-
hint="
|
|
6592
|
+
label="Rewind"
|
|
6593
|
+
hint="A disabled-by-default rolling local archive for recent screen and app context. It never becomes a shared Clip on its own."
|
|
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
|
|
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
|
|
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
|
|
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
|
-
{
|
|
4685
|
-
?
|
|
4686
|
-
:
|
|
4687
|
-
?
|
|
4688
|
-
:
|
|
4689
|
-
?
|
|
4690
|
-
:
|
|
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
|
|
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
|
-
</
|
|
7446
|
+
</div>
|
|
5192
7447
|
);
|
|
5193
7448
|
}
|
|
5194
7449
|
|