@agent-native/core-corpus 0.210.2 → 0.211.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (192) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/.agents/skills/actions/references/action-fields.md +5 -3
  3. package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +41 -2
  4. package/corpus/templates/analytics/.agents/skills/data-querying/SKILL.md +4 -2
  5. package/corpus/templates/analytics/CHANGELOG.md +18 -0
  6. package/corpus/templates/analytics/actions/build-data-index.ts +11 -2
  7. package/corpus/templates/analytics/actions/configure-content-collection.ts +29 -4
  8. package/corpus/templates/analytics/actions/configure-first-party-events-model.ts +9 -7
  9. package/corpus/templates/analytics/actions/create-analytics-public-key.ts +4 -0
  10. package/corpus/templates/analytics/actions/estimate-content-signups-cost.ts +72 -0
  11. package/corpus/templates/analytics/actions/estimate-query-cost.ts +16 -134
  12. package/corpus/templates/analytics/actions/list-sigma-workbooks.ts +123 -42
  13. package/corpus/templates/analytics/actions/migrate-sigma-workbook.ts +20 -17
  14. package/corpus/templates/analytics/actions/revoke-analytics-public-key.ts +4 -0
  15. package/corpus/templates/analytics/app/components/dashboard/SigmaMigrationDialog.tsx +246 -680
  16. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +1 -1
  17. package/corpus/templates/analytics/app/i18n/en-US.ts +54 -55
  18. package/corpus/templates/analytics/app/pages/DashboardOverview.tsx +0 -1
  19. package/corpus/templates/analytics/app/pages/DataSources.tsx +56 -0
  20. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/filter-vars.ts +4 -3
  21. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +26 -8
  22. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/interpolate.ts +3 -1
  23. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/panel-cost-notice.ts +172 -0
  24. package/corpus/templates/analytics/app/pages/data-sources/SearchConsoleDetail.tsx +441 -0
  25. package/corpus/templates/analytics/changelog/2026-10-15-a-content-dashboard-lists-every-blog-article-with-its-author.md +6 -0
  26. package/corpus/templates/analytics/changelog/2026-10-15-data-sources-has-a-google-search-console-card-that-shows-the.md +6 -0
  27. package/corpus/templates/analytics/changelog/2026-10-16-the-content-dashboard-can-show-signups-per-blog-article.md +6 -0
  28. package/corpus/templates/analytics/changelog/2026-10-19-creating-or-revoking-a-public-analytics-key-now-asks-you-to-.md +6 -0
  29. package/corpus/templates/analytics/changelog/2026-10-19-search-sigma-workbooks-by-name-and-open-ones-a-teammate-alre.md +6 -0
  30. package/corpus/templates/analytics/changelog/2026-10-20-blog-signup-attribution-excludes-signups-before-the-first-vi.md +6 -0
  31. package/corpus/templates/analytics/changelog/2026-10-20-collapsed-analytics-navigation-starts-at-the-top.md +6 -0
  32. package/corpus/templates/analytics/changelog/2026-10-20-content-keeps-successful-coverage-dates-and-missing-day-gaps.md +6 -0
  33. package/corpus/templates/analytics/changelog/2026-10-20-private-github-repositories-can-be-indexed-with-your-connect.md +6 -0
  34. package/corpus/templates/analytics/changelog/2026-10-20-search-console-preserves-backfill-and-paused-collection.md +6 -0
  35. package/corpus/templates/analytics/changelog/2026-10-20-search-console-refreshes-historical-coverage-when-properties.md +6 -0
  36. package/corpus/templates/analytics/changelog/2026-10-20-search-console-setup-keeps-saved-properties-readable-and-sup.md +6 -0
  37. package/corpus/templates/analytics/seeds/dashboards/content-view.json +182 -0
  38. package/corpus/templates/analytics/server/db/schema.ts +1 -0
  39. package/corpus/templates/analytics/server/lib/bi-migration/external-dashboards-store.ts +56 -18
  40. package/corpus/templates/analytics/server/lib/bi-migration/unfinished-import.ts +64 -0
  41. package/corpus/templates/analytics/server/lib/builtin-dashboard-pass.ts +17 -2
  42. package/corpus/templates/analytics/server/lib/content-metrics/collect.ts +2 -0
  43. package/corpus/templates/analytics/server/lib/content-metrics/config.ts +92 -3
  44. package/corpus/templates/analytics/server/lib/content-metrics/legacy-content-view.json +172 -0
  45. package/corpus/templates/analytics/server/lib/content-metrics/migration.ts +2 -0
  46. package/corpus/templates/analytics/server/lib/content-metrics/steps/daily-metrics.ts +8 -3
  47. package/corpus/templates/analytics/server/lib/content-metrics/steps/search-console.ts +16 -3
  48. package/corpus/templates/analytics/server/lib/content-metrics/steps/signups.ts +240 -0
  49. package/corpus/templates/analytics/server/lib/content-metrics/store.ts +5 -0
  50. package/corpus/templates/analytics/server/lib/dashboard-catalog.ts +21 -0
  51. package/corpus/templates/analytics/server/lib/dashboard-panel-verification.ts +300 -5
  52. package/corpus/templates/analytics/server/lib/dashboard-seeds.ts +2 -0
  53. package/corpus/templates/analytics/server/lib/dashboards-store.ts +28 -0
  54. package/corpus/templates/analytics/server/lib/first-party-analytics-backend.ts +128 -24
  55. package/corpus/templates/analytics/server/lib/google-analytics.ts +56 -16
  56. package/corpus/templates/analytics/server/lib/index-build-github.ts +5 -0
  57. package/corpus/templates/analytics/server/lib/provider-credentials.ts +47 -2
  58. package/corpus/templates/analytics/server/lib/query-cost-estimate.ts +184 -5
  59. package/corpus/templates/analytics/server/plugins/db.ts +9 -1
  60. package/corpus/templates/analytics/shared/builtin-dashboard-ids.ts +2 -0
  61. package/corpus/templates/analytics/shared/search-console-property.ts +78 -0
  62. package/corpus/templates/assets/.agents/skills/actions/references/action-fields.md +5 -3
  63. package/corpus/templates/brain/.agents/skills/actions/references/action-fields.md +5 -3
  64. package/corpus/templates/calendar/.agents/skills/actions/references/action-fields.md +5 -3
  65. package/corpus/templates/calendar/actions/delete-event.ts +1 -3
  66. package/corpus/templates/calendar/actions/delete-events.ts +1 -2
  67. package/corpus/templates/calendar/actions/event-action-helpers.ts +0 -4
  68. package/corpus/templates/calendar/actions/update-event.ts +2 -6
  69. package/corpus/templates/chat/.agents/skills/actions/references/action-fields.md +5 -3
  70. package/corpus/templates/clips/.agents/skills/actions/references/action-fields.md +5 -3
  71. package/corpus/templates/clips/CHANGELOG.md +11 -24
  72. package/corpus/templates/clips/actions/create-recording-upload.ts +5 -1
  73. package/corpus/templates/clips/app/components/player/chapter-list.tsx +7 -6
  74. package/corpus/templates/clips/app/components/player/comment-composer.tsx +5 -1
  75. package/corpus/templates/clips/app/components/player/player-controls.tsx +1 -0
  76. package/corpus/templates/clips/app/components/player/scrubber.tsx +4 -2
  77. package/corpus/templates/clips/app/i18n/en-US.ts +3 -3
  78. package/corpus/templates/clips/app/lib/extension-auth.ts +70 -0
  79. package/corpus/templates/clips/app/root.tsx +22 -10
  80. package/corpus/templates/clips/app/routes/_app.notifications.tsx +3 -0
  81. package/corpus/templates/clips/app/routes/import.tsx +115 -110
  82. package/corpus/templates/clips/changelog/2026-10-16-agent-uploads-can-use-free-clips-storage.md +6 -0
  83. package/corpus/templates/clips/changelog/2026-10-19-agents-see-a-clip-s-frames-as-soon-as-it-finishes-processing.md +6 -0
  84. package/corpus/templates/clips/changelog/2026-10-19-chapter-list-times-now-match-the-player-s-clock-and-each-cha.md +6 -0
  85. package/corpus/templates/clips/changelog/2026-10-19-editing-a-comment-puts-the-cursor-after-its-text-and-reply-o.md +6 -0
  86. package/corpus/templates/clips/changelog/2026-10-19-youtube-import-followups.md +6 -0
  87. package/corpus/templates/clips/changelog/2026-10-20-locally-saved-desktop-recordings-now-appear-in-the-recovery-.md +6 -0
  88. package/corpus/templates/clips/desktop/src/app.tsx +29 -186
  89. package/corpus/templates/clips/desktop/src/components/RecordingRecovery.tsx +171 -16
  90. package/corpus/templates/clips/desktop/src/lib/record-first.ts +9 -32
  91. package/corpus/templates/clips/desktop/src/lib/recording-recovery.ts +30 -1
  92. package/corpus/templates/clips/server/lib/agent-frame-request.ts +5 -2
  93. package/corpus/templates/clips/server/lib/recording-upload-ticket.ts +8 -0
  94. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +2 -0
  95. package/corpus/templates/content/.agents/skills/actions/references/action-fields.md +5 -3
  96. package/corpus/templates/content/CHANGELOG.md +12 -0
  97. package/corpus/templates/content/actions/_content-database-personal-view.ts +5 -15
  98. package/corpus/templates/content/actions/_database-navigation.ts +3 -3
  99. package/corpus/templates/content/actions/_database-utils.ts +10 -22
  100. package/corpus/templates/content/actions/_database-view-setup.ts +16 -76
  101. package/corpus/templates/content/actions/create-comment-ai-suggestion.ts +93 -44
  102. package/corpus/templates/content/actions/export-document.ts +2 -14
  103. package/corpus/templates/content/actions/suggest-document-edit.ts +5 -7
  104. package/corpus/templates/content/actions/update-content-database-view.ts +2 -14
  105. package/corpus/templates/content/app/components/editor/CommentsSidebar.tsx +86 -30
  106. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +64 -28
  107. package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +6 -46
  108. package/corpus/templates/content/app/components/editor/PageDraftRecovery.tsx +72 -7
  109. package/corpus/templates/content/app/components/editor/TableHoverControls.tsx +292 -103
  110. package/corpus/templates/content/app/components/editor/ThreadSuggestion.tsx +147 -40
  111. package/corpus/templates/content/app/components/editor/VisualEditor.tsx +108 -11
  112. package/corpus/templates/content/app/components/editor/database/ContentTable.tsx +2 -1
  113. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +150 -166
  114. package/corpus/templates/content/app/components/editor/database/database-cell-comments.tsx +515 -0
  115. package/corpus/templates/content/app/components/editor/database/database-item-comments.tsx +5 -43
  116. package/corpus/templates/content/app/components/editor/database/filter-sort.ts +8 -783
  117. package/corpus/templates/content/app/components/editor/live-editor-session.ts +5 -1
  118. package/corpus/templates/content/app/components/editor/page-draft-journal.ts +61 -1
  119. package/corpus/templates/content/app/components/editor/property-comments.tsx +35 -4
  120. package/corpus/templates/content/app/components/editor/property-date-format.ts +45 -0
  121. package/corpus/templates/content/app/components/editor/thread-suggestions.ts +30 -0
  122. package/corpus/templates/content/app/components/editor/useDocumentReconcileRecovery.ts +23 -4
  123. package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +36 -10
  124. package/corpus/templates/content/app/global.css +23 -0
  125. package/corpus/templates/content/app/i18n-data.ts +1 -0
  126. package/corpus/templates/content/changelog/2026-10-16-opening-a-page-in-a-new-tab-while-another-tab-is-still-savin.md +6 -0
  127. package/corpus/templates/content/changelog/2026-10-16-typing-steadily-in-one-tab-no-longer-makes-another-open-tab-.md +6 -0
  128. package/corpus/templates/content/changelog/2026-10-19-database-views-can-filter-the-same-property-more-than-once-n.md +6 -0
  129. package/corpus/templates/content/changelog/2026-10-19-edits-made-from-a-block-s-menu-or-a-table-s-row-and-column-m.md +6 -0
  130. package/corpus/templates/content/changelog/2026-10-19-starting-a-comment-puts-the-cursor-in-the-comment-box-ready-.md +6 -0
  131. package/corpus/templates/content/changelog/2026-10-19-table-row-and-column-options-now-appear-for-the-row-or-colum.md +6 -0
  132. package/corpus/templates/content/docs/product/capabilities/content.comment.page-owned.md +5 -1
  133. package/corpus/templates/content/docs/product/capabilities/content.revision.suggestions.md +3 -1
  134. package/corpus/templates/content/e2e/helpers.ts +10 -0
  135. package/corpus/templates/content/shared/api.ts +18 -12
  136. package/corpus/templates/content/shared/database-query.ts +107 -13
  137. package/corpus/templates/content/shared/suggestion-diff.ts +35 -0
  138. package/corpus/templates/crm/.agents/skills/actions/references/action-fields.md +5 -3
  139. package/corpus/templates/design/.agents/skills/actions/references/action-fields.md +5 -3
  140. package/corpus/templates/design/CHANGELOG.md +2 -0
  141. package/corpus/templates/design/actions/delete-file.ts +1 -4
  142. package/corpus/templates/design/app/components/design/LayersPanel.tsx +10 -1
  143. package/corpus/templates/design/app/components/visual-editor/ReviewCanvasPins.tsx +24 -4
  144. package/corpus/templates/design/app/pages/design-editor/commands/mode-change.ts +14 -9
  145. package/corpus/templates/design/app/pages/design-editor/domains/use-editor-modes.ts +23 -2
  146. package/corpus/templates/design/app/pages/design-editor/tool-state.ts +29 -0
  147. package/corpus/templates/design/changelog/2026-10-19-reply-and-edit-on-a-comment-in-the-sidebar-or-on-a-canvas-pi.md +6 -0
  148. package/corpus/templates/design/changelog/2026-10-20-comments-return-to-the-mode-they-came-from-and-layers-search.md +6 -0
  149. package/corpus/templates/design/e2e/global-setup.ts +185 -85
  150. package/corpus/templates/dispatch/.agents/skills/actions/references/action-fields.md +5 -3
  151. package/corpus/templates/dispatch/CHANGELOG.md +4 -0
  152. package/corpus/templates/dispatch/actions/apply-workspace-connection-setup.ts +4 -0
  153. package/corpus/templates/dispatch/actions/delete-workspace-connection.ts +4 -0
  154. package/corpus/templates/dispatch/actions/set-workspace-connection-grant.ts +4 -0
  155. package/corpus/templates/dispatch/actions/upsert-workspace-connection.ts +4 -0
  156. package/corpus/templates/dispatch/changelog/2026-10-19-vault-secrets-grants-link-tokens-app-access-and-workspace-co.md +6 -0
  157. package/corpus/templates/factory/.agents/skills/actions/references/action-fields.md +5 -3
  158. package/corpus/templates/factory/.agents/skills/review-latest-feedback/SKILL.md +105 -33
  159. package/corpus/templates/factory/.agents/skills/review-latest-feedback/references/channel-inventory.md +128 -74
  160. package/corpus/templates/factory/.agents/skills/review-latest-feedback/references/slack-reaction-replies.md +39 -11
  161. package/corpus/templates/forms/.agents/skills/actions/references/action-fields.md +5 -3
  162. package/corpus/templates/mail/.agents/skills/actions/references/action-fields.md +5 -3
  163. package/corpus/templates/mail/.agents/skills/contacts-and-crm/SKILL.md +22 -17
  164. package/corpus/templates/mail/AGENTS.md +1 -1
  165. package/corpus/templates/mail/CHANGELOG.md +14 -0
  166. package/corpus/templates/mail/actions/provider-api-request.ts +3 -3
  167. package/corpus/templates/mail/app/components/email/IntegrationsSidebar.tsx +211 -54
  168. package/corpus/templates/mail/app/hooks/use-inbox-threads.ts +26 -2
  169. package/corpus/templates/mail/app/i18n/en-US.ts +5 -1
  170. package/corpus/templates/mail/app/lib/gong-connection.ts +27 -0
  171. package/corpus/templates/mail/app/lib/thread-cache.ts +110 -9
  172. package/corpus/templates/mail/app/root.tsx +10 -2
  173. package/corpus/templates/mail/changelog/2026-10-20-connected-gong-settings-now-open-the-shared-integration-mana.md +6 -0
  174. package/corpus/templates/mail/changelog/2026-10-20-gong-connection-details-now-match-the-call-data-available-in.md +6 -0
  175. package/corpus/templates/mail/changelog/2026-10-20-improve-gong-setup-with-a-click-to-connect-flow-and-permissi.md +6 -0
  176. package/corpus/templates/mail/changelog/2026-10-20-mail-lets-you-retry-when-it-cannot-check-your-gong-access.md +6 -0
  177. package/corpus/templates/mail/changelog/2026-10-20-mail-thread-cache-stays-isolated-when-mailbox-scope-changes.md +6 -0
  178. package/corpus/templates/mail/changelog/2026-10-20-the-inbox-and-recently-opened-threads-now-open-instantly-fro.md +6 -0
  179. package/corpus/templates/mail/server/handlers/emails.ts +9 -0
  180. package/corpus/templates/plan/.agents/skills/actions/references/action-fields.md +5 -3
  181. package/corpus/templates/plan/CHANGELOG.md +4 -0
  182. package/corpus/templates/plan/app/pages/PlansPage.tsx +173 -33
  183. package/corpus/templates/plan/changelog/2026-10-19-opening-a-comment-thread-puts-the-cursor-in-the-reply-box-an.md +6 -0
  184. package/corpus/templates/slides/.agents/skills/actions/references/action-fields.md +5 -3
  185. package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +6 -42
  186. package/corpus/templates/slides/app/components/editor/FloatingToolbar.tsx +3 -7
  187. package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +5 -14
  188. package/corpus/templates/slides/app/i18n/en-US.ts +0 -2
  189. package/corpus/templates/slides/app/pages/DeckEditor.tsx +27 -33
  190. package/corpus/templates/slides/scripts/edit-fidelity/run.ts +1 -3
  191. package/corpus/templates/tasks/.agents/skills/actions/references/action-fields.md +5 -3
  192. package/package.json +1 -1
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-10-19
4
+ ---
5
+
6
+ Chapter list times now match the player's clock, and each chapter shows a focus ring when you tab to it.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-10-19
4
+ ---
5
+
6
+ Editing a comment puts the cursor after its text, and Reply on another notification moves the cursor to its reply box.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-10-19
4
+ ---
5
+
6
+ Video imports use clearer labels, YouTube links work without connected storage, and embedded players stay clear of timeline hover previews.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-10-20
4
+ ---
5
+
6
+ Locally saved desktop recordings now appear in the recovery list with upload and storage setup actions.
@@ -165,8 +165,7 @@ import {
165
165
  loadRecordFirstFiles,
166
166
  RecordFirstFileMissingError,
167
167
  recordFirstFilesKey,
168
- recordFirstFilesToQueue,
169
- recordFirstMediaFlags,
168
+ shouldPollVideoStorageStatus,
170
169
  stagedIdAfterFailure,
171
170
  transferRecordFirstFiles,
172
171
  type RecordFirstFile,
@@ -209,11 +208,13 @@ import {
209
208
  ScreenRecordingPermissionError,
210
209
  } from "./lib/recording-preflight";
211
210
  import {
211
+ mapRecordFirstFilesForRecovery,
212
212
  reconcileRecordingRecovery,
213
213
  recordingRecoveryKey,
214
214
  shouldShowRecordingRecoveryBanner,
215
215
  type PendingDesktopUpload,
216
216
  type PendingNativeUpload,
217
+ type RecordFirstFileUploadError,
217
218
  type RecoverySnapshot,
218
219
  } from "./lib/recording-recovery";
219
220
  import {
@@ -1391,7 +1392,7 @@ export function App({
1391
1392
  }, [videoStorageIdentity, serverUrl, signedInAs]);
1392
1393
 
1393
1394
  const refreshVideoStorageStatus = useCallback(async () => {
1394
- if (authStatus !== "authed" || localRecordingMode !== "off") {
1395
+ if (authStatus !== "authed") {
1395
1396
  hostedClipLimitMsRef.current = null;
1396
1397
  setVideoStorageStatus("configured");
1397
1398
  return true;
@@ -1410,13 +1411,7 @@ export function App({
1410
1411
  hostedClipLimitMsRef.current = maxClipDurationMs;
1411
1412
  setVideoStorageStatus(probe);
1412
1413
  return probe === "configured";
1413
- }, [
1414
- authStatus,
1415
- localRecordingMode,
1416
- serverUrl,
1417
- signedInAs,
1418
- videoStorageIdentity,
1419
- ]);
1414
+ }, [authStatus, serverUrl, signedInAs, videoStorageIdentity]);
1420
1415
 
1421
1416
  useEffect(() => {
1422
1417
  void refreshVideoStorageStatus();
@@ -1424,14 +1419,9 @@ export function App({
1424
1419
 
1425
1420
  useEffect(() => {
1426
1421
  if (
1427
- authStatus !== "authed" ||
1428
- localRecordingMode !== "off" ||
1429
- (videoStorageStatus !== "missing" &&
1430
- videoStorageStatus !== "checking" &&
1431
- videoStorageStatus !== "unavailable")
1432
- ) {
1422
+ !shouldPollVideoStorageStatus(authStatus === "authed", videoStorageStatus)
1423
+ )
1433
1424
  return;
1434
- }
1435
1425
 
1436
1426
  let inFlight = false;
1437
1427
  const tick = () => {
@@ -1450,12 +1440,7 @@ export function App({
1450
1440
  window.clearInterval(interval);
1451
1441
  document.removeEventListener("visibilitychange", onVisibilityChange);
1452
1442
  };
1453
- }, [
1454
- authStatus,
1455
- localRecordingMode,
1456
- refreshVideoStorageStatus,
1457
- videoStorageStatus,
1458
- ]);
1443
+ }, [authStatus, refreshVideoStorageStatus, videoStorageStatus]);
1459
1444
 
1460
1445
  // Mirrors the web recorder: a hosted take stops itself at the clip cap
1461
1446
  // instead of failing the upload past the server's size limit. The cap the
@@ -3201,9 +3186,6 @@ export function App({
3201
3186
  const recorderStopListenerPromiseRef = useRef<Promise<() => void> | null>(
3202
3187
  null,
3203
3188
  );
3204
- const recordedWithoutStorageRef = useRef(false);
3205
- // The record-first list a capture belongs to, fixed when capture starts.
3206
- const recordFirstKeyAtStartRef = useRef<string | null>(null);
3207
3189
  const finishRecordingStopRef = useRef<
3208
3190
  (handle: RecorderHandle, recordingId?: string | null) => Promise<void>
3209
3191
  >(async () => {});
@@ -3597,7 +3579,7 @@ export function App({
3597
3579
  >([]);
3598
3580
  const [recordFirstError, setRecordFirstError] = useState<string | null>(null);
3599
3581
  const [recordFirstFileErrors, setRecordFirstFileErrors] = useState<
3600
- Record<string, { message: string; missing: boolean }>
3582
+ Record<string, RecordFirstFileUploadError>
3601
3583
  >({});
3602
3584
  const [recordFirstUploading, setRecordFirstUploading] = useState(false);
3603
3585
  useEffect(() => {
@@ -3636,17 +3618,6 @@ export function App({
3636
3618
  },
3637
3619
  [recordFirstKey, unclaimedRecordFirstKey],
3638
3620
  );
3639
- const rememberRecordFirstFile = useCallback(
3640
- (file: RecordFirstFile, key: string) => {
3641
- // `key` is the account signed in when capture started, never whoever
3642
- // is signed in at Stop, so a switch mid-recording cannot reassign it.
3643
- changeQueuedFiles(key, (files) => [
3644
- ...files.filter((f) => f.path !== file.path),
3645
- file,
3646
- ]);
3647
- },
3648
- [changeQueuedFiles],
3649
- );
3650
3621
  const forgetRecordFirstFile = useCallback(
3651
3622
  (path: string) => {
3652
3623
  // Removes the list entry only; the file on disk is never touched.
@@ -3828,24 +3799,6 @@ export function App({
3828
3799
  folderPath: stopResult.localFolder,
3829
3800
  files: stopResult.localFiles ?? [],
3830
3801
  });
3831
- const queueKey = recordFirstKeyAtStartRef.current;
3832
- if (recordedWithoutStorageRef.current && queueKey) {
3833
- for (const file of recordFirstFilesToQueue(
3834
- stopResult.localFiles ?? [],
3835
- )) {
3836
- rememberRecordFirstFile(
3837
- {
3838
- ...file,
3839
- ...recordFirstMediaFlags(file.role, {
3840
- hasAudio: micOn || systemAudioOn,
3841
- hasCamera: mode !== "screen",
3842
- }),
3843
- savedAt: new Date().toISOString(),
3844
- },
3845
- queueKey,
3846
- );
3847
- }
3848
- }
3849
3802
  emit("clips:native-upload-finished", {
3850
3803
  recordingId: stopResult.recordingId,
3851
3804
  ok: true,
@@ -3867,7 +3820,7 @@ export function App({
3867
3820
  ok: false,
3868
3821
  error: err instanceof Error ? err.message : String(err),
3869
3822
  },
3870
- localRecordingMode !== "off" || recordedWithoutStorageRef.current,
3823
+ localRecordingMode !== "off",
3871
3824
  );
3872
3825
  emit("clips:native-upload-finished", {
3873
3826
  recordingId: stoppingRecordingId ?? undefined,
@@ -4242,19 +4195,7 @@ export function App({
4242
4195
  bubbleStreamRef.current = null;
4243
4196
  setBubbleSessionEpoch((epoch) => epoch + 1);
4244
4197
  }
4245
- // Recording never waits on storage: until storage reads as connected the
4246
- // clip is written to Movies/Clips as it records, then uploads once
4247
- // storage connects.
4248
- const recordingLocalMode = effectiveLocalRecordingMode(
4249
- localRecordingMode,
4250
- videoStorageStatus,
4251
- );
4252
- const recordLocallyUntilStorage = recordingLocalMode !== localRecordingMode;
4253
- recordedWithoutStorageRef.current = recordLocallyUntilStorage;
4254
- recordFirstKeyAtStartRef.current = recordFirstFilesKey(
4255
- originForServer(serverUrl),
4256
- signedInAs,
4257
- );
4198
+ const recordingLocalMode = effectiveLocalRecordingMode(localRecordingMode);
4258
4199
  setRecError(null);
4259
4200
  setLocalRecordingNotice(null);
4260
4201
  setShareLinkNotice(null);
@@ -4264,6 +4205,7 @@ export function App({
4264
4205
  mode,
4265
4206
  source,
4266
4207
  localRecordingMode,
4208
+ videoStorageStatus,
4267
4209
  cameraOn,
4268
4210
  micOn,
4269
4211
  });
@@ -4875,8 +4817,16 @@ export function App({
4875
4817
  ? "Start recording"
4876
4818
  : "Start local recording";
4877
4819
 
4820
+ const localRecoveryFiles = mapRecordFirstFilesForRecovery(
4821
+ [...recordFirstFiles, ...unclaimedRecordFirstFiles],
4822
+ recordFirstFileErrors,
4823
+ );
4878
4824
  const recoveryProps: RecordingRecoveryProps = {
4879
4825
  uploads: pendingUploads,
4826
+ localFiles: localRecoveryFiles,
4827
+ localFilesError: recordFirstError,
4828
+ localFilesUploading: recordFirstUploading,
4829
+ localFilesStorageStatus: videoStorageStatus,
4880
4830
  lookupErrors: recoverySnapshot.errors,
4881
4831
  actionErrors: recoveryActionErrors,
4882
4832
  refreshing: recoveryRefreshing,
@@ -4897,6 +4847,11 @@ export function App({
4897
4847
  onRetry: retryPendingUpload,
4898
4848
  onCancelRetry: cancelPendingUploadRetry,
4899
4849
  onOpenFolder: openPendingUploadFolder,
4850
+ onRetryLocalFiles: () =>
4851
+ void uploadRecordFirstFilesRef.current({ includeUnclaimed: true }),
4852
+ onRefreshStorage: () => void refreshVideoStorageStatus(),
4853
+ onConnectLocalStorage: () => openVideoStorageSetup(serverUrl),
4854
+ onForgetLocalFile: forgetRecordFirstFile,
4900
4855
  onReviewFiles: (key) => {
4901
4856
  void invoke("native_fullscreen_open_drafts_folder").catch((error) => {
4902
4857
  setRecoveryActionErrors((errors) => ({
@@ -4921,7 +4876,9 @@ export function App({
4921
4876
  delete next[key];
4922
4877
  return next;
4923
4878
  });
4924
- void openExternal(`${targetServerUrl}/record`).catch((error) => {
4879
+ void openExternal(
4880
+ `${targetServerUrl.replace(/\/+$/, "")}/record?connectStorage=1`,
4881
+ ).catch((error) => {
4925
4882
  setRecoveryActionErrors((errors) => ({
4926
4883
  ...errors,
4927
4884
  [key]: error instanceof Error ? error.message : String(error),
@@ -4932,6 +4889,7 @@ export function App({
4932
4889
  const pendingUploadBanner = shouldShowRecordingRecoveryBanner(
4933
4890
  popoverView,
4934
4891
  authStatus === "authed",
4892
+ localRecoveryFiles.length > 0 || !!recordFirstError,
4935
4893
  ) ? (
4936
4894
  <RecordingRecovery
4937
4895
  {...recoveryProps}
@@ -5492,31 +5450,6 @@ export function App({
5492
5450
  </button>
5493
5451
  ) : null}
5494
5452
 
5495
- {recordFirstFiles.length + unclaimedRecordFirstFiles.length > 0 ||
5496
- recordFirstError ? (
5497
- <RecordFirstUploadsBanner
5498
- count={recordFirstFiles.length + unclaimedRecordFirstFiles.length}
5499
- storageStatus={videoStorageStatus}
5500
- signedIn={!!signedInAs}
5501
- uploading={recordFirstUploading}
5502
- error={recordFirstError}
5503
- failedFiles={[
5504
- ...recordFirstFiles,
5505
- ...unclaimedRecordFirstFiles,
5506
- ].flatMap((file) =>
5507
- recordFirstFileErrors[file.path]
5508
- ? [{ file, ...recordFirstFileErrors[file.path]! }]
5509
- : [],
5510
- )}
5511
- onUpload={() =>
5512
- void uploadRecordFirstFilesRef.current({
5513
- includeUnclaimed: true,
5514
- })
5515
- }
5516
- onCheckStorage={() => void refreshVideoStorageStatus()}
5517
- onForget={forgetRecordFirstFile}
5518
- />
5519
- ) : null}
5520
5453
  {recError ? (
5521
5454
  recError === MACOS_UPDATE_RESTART_MESSAGE ? (
5522
5455
  <UpdateRestartBanner message={recError} />
@@ -5719,96 +5652,6 @@ function UpdateRestartBanner({ message }: { message: string }) {
5719
5652
  const RECORD_FIRST_LIST_UNREADABLE =
5720
5653
  "Clips couldn't read its list of recordings waiting to upload. They are still in Movies/Clips.";
5721
5654
 
5722
- function RecordFirstUploadsBanner({
5723
- count,
5724
- storageStatus,
5725
- signedIn,
5726
- uploading,
5727
- error,
5728
- failedFiles,
5729
- onUpload,
5730
- onCheckStorage,
5731
- onForget,
5732
- }: {
5733
- count: number;
5734
- storageStatus: VideoStorageStatus;
5735
- signedIn: boolean;
5736
- uploading: boolean;
5737
- error: string | null;
5738
- failedFiles: Array<{
5739
- file: RecordFirstFile;
5740
- message: string;
5741
- missing: boolean;
5742
- }>;
5743
- onUpload: () => void;
5744
- onCheckStorage: () => void;
5745
- onForget: (path: string) => void;
5746
- }) {
5747
- const recordings = count === 1 ? "1 recording" : `${count} recordings`;
5748
- const storageCopy =
5749
- storageStatus === "configured"
5750
- ? "Saved in Movies/Clips. Upload them to get share links."
5751
- : storageStatus === "missing"
5752
- ? "Saved in Movies/Clips. Connect storage and they upload."
5753
- : storageStatus === "unavailable"
5754
- ? "Storage authorization could not be checked. Retry to check again; recordings remain in Movies/Clips."
5755
- : "Checking whether storage can upload clips.";
5756
- const checkingStorage =
5757
- storageStatus === "checking" || storageStatus === "unavailable";
5758
- return (
5759
- <>
5760
- <div className="storage-flow-banner" role="status">
5761
- <div className="storage-flow-icon" aria-hidden>
5762
- <IconUpload size={17} stroke={1.8} />
5763
- </div>
5764
- <div className="storage-flow-copy">
5765
- <div className="storage-flow-title">
5766
- {uploading
5767
- ? `Uploading ${recordings}…`
5768
- : `${recordings} saved on this Mac, not uploaded`}
5769
- </div>
5770
- <div className="storage-flow-sub">
5771
- {error ? <div>{error}</div> : null}
5772
- <div>{storageCopy}</div>
5773
- </div>
5774
- </div>
5775
- {count > 0 ? (
5776
- <button
5777
- type="button"
5778
- className="storage-flow-connect"
5779
- disabled={uploading || !signedIn}
5780
- title={signedIn ? undefined : "Sign in to upload"}
5781
- onClick={checkingStorage ? onCheckStorage : onUpload}
5782
- >
5783
- <IconUpload size={14} stroke={2} />
5784
- {storageStatus === "configured"
5785
- ? "Upload now"
5786
- : storageStatus === "missing"
5787
- ? "Connect"
5788
- : storageStatus === "unavailable"
5789
- ? "Retry"
5790
- : "Check storage"}
5791
- </button>
5792
- ) : null}
5793
- </div>
5794
- {failedFiles.map(({ file, message, missing }) => (
5795
- <div key={file.path} className="error-banner" role="alert">
5796
- {file.fileName}: {message}
5797
- {missing ? (
5798
- <button
5799
- type="button"
5800
- className="storage-flow-connect"
5801
- onClick={() => onForget(file.path)}
5802
- >
5803
- Remove from list
5804
- </button>
5805
- ) : null}
5806
- </div>
5807
- ))}
5808
- </>
5809
- );
5810
- }
5811
-
5812
5655
  function StorageConnectionBanner({ onConnect }: { onConnect: () => void }) {
5813
5656
  return (
5814
5657
  <div className="storage-flow-banner">
@@ -3,11 +3,13 @@ import {
3
3
  IconChevronRight,
4
4
  IconDots,
5
5
  IconRefresh,
6
+ IconUpload,
6
7
  IconX,
7
8
  } from "@tabler/icons-react";
8
9
  import { useLayoutEffect, useRef, type RefObject, type ReactNode } from "react";
9
10
 
10
11
  import { desktopRecoveryCopy as copy } from "../i18n/en-US";
12
+ import type { VideoStorageStatus } from "../lib/record-first";
11
13
  import {
12
14
  classifyRecordingRecoveryError,
13
15
  recordingRecoveryKey,
@@ -39,6 +41,16 @@ import { Spinner } from "./ui/spinner";
39
41
 
40
42
  export interface RecordingRecoveryState {
41
43
  uploads: PendingDesktopUpload[];
44
+ localFiles: Array<{
45
+ path: string;
46
+ fileName: string;
47
+ savedAt: string;
48
+ error?: string;
49
+ missing?: boolean;
50
+ }>;
51
+ localFilesError: string | null;
52
+ localFilesUploading: boolean;
53
+ localFilesStorageStatus: VideoStorageStatus;
42
54
  lookupErrors: RecoveryLookupError[];
43
55
  actionErrors: Record<string, string>;
44
56
  refreshing: boolean;
@@ -64,6 +76,10 @@ export interface RecordingRecoveryProps extends RecordingRecoveryState {
64
76
  onExport: (upload: PendingDesktopUpload) => void;
65
77
  onOpenFolder: (upload: PendingDesktopUpload) => void;
66
78
  onConnectStorage: (upload: PendingDesktopUpload) => void;
79
+ onRetryLocalFiles: () => void;
80
+ onRefreshStorage: () => void;
81
+ onConnectLocalStorage: () => void;
82
+ onForgetLocalFile: (path: string) => void;
67
83
  onReviewFiles: (key: string) => void;
68
84
  onOpenLogs: (key: string) => void;
69
85
  }
@@ -117,19 +133,28 @@ function recoveryPresentation(props: RecordingRecoveryState) {
117
133
  messages: [...(orphanErrors.get(id)?.messages || []), message],
118
134
  });
119
135
  }
120
- const attentionCount = uploads.filter(
121
- (upload) => uploadSeverity(upload, props) !== "neutral",
122
- ).length;
136
+ const attentionCount =
137
+ uploads.filter((upload) => uploadSeverity(upload, props) !== "neutral")
138
+ .length + (props.localFilesUploading ? 0 : props.localFiles.length);
123
139
  const unknownFailures =
124
- props.lookupErrors.length > 0 || orphanErrors.size > 0;
140
+ props.lookupErrors.length > 0 ||
141
+ orphanErrors.size > 0 ||
142
+ !!props.localFilesError;
143
+ const localFileSeverities = props.localFiles.map((file) =>
144
+ props.localFilesUploading ? "neutral" : file.missing ? "error" : "warning",
145
+ );
125
146
  const severities = [
126
147
  ...uploads.map((upload) => uploadSeverity(upload, props)),
148
+ ...localFileSeverities,
127
149
  ...[...orphanErrors.values()].map((error) =>
128
150
  recordingRecoverySeverity(error.messages),
129
151
  ),
130
152
  ...(props.lookupErrors.length
131
153
  ? [recordingRecoverySeverity([lookupDiagnostic(props.lookupErrors)])]
132
154
  : []),
155
+ ...(props.localFilesError
156
+ ? [recordingRecoverySeverity([props.localFilesError])]
157
+ : []),
133
158
  ];
134
159
  const severity = severities.includes("error")
135
160
  ? "error"
@@ -138,6 +163,7 @@ function recoveryPresentation(props: RecordingRecoveryState) {
138
163
  : "neutral";
139
164
  return {
140
165
  uploads,
166
+ localFiles: props.localFiles,
141
167
  orphanErrors: [...orphanErrors.values()],
142
168
  attentionCount,
143
169
  unknownFailures,
@@ -151,21 +177,29 @@ export function RecordingRecovery(
151
177
  triggerRef?: RefObject<HTMLButtonElement | null>;
152
178
  },
153
179
  ) {
154
- const { uploads, attentionCount, unknownFailures, severity } =
180
+ const { uploads, localFiles, attentionCount, unknownFailures, severity } =
155
181
  recoveryPresentation(props);
156
182
  const showFinalizing = props.finalizing && props.showFinalizing !== false;
157
- if (uploads.length === 0 && !unknownFailures && !showFinalizing) return null;
183
+ if (
184
+ uploads.length === 0 &&
185
+ localFiles.length === 0 &&
186
+ !unknownFailures &&
187
+ !showFinalizing
188
+ )
189
+ return null;
158
190
  const label = unknownFailures
159
191
  ? copy.recordingsNeedAttention
160
- : attentionCount === 1
161
- ? copy.attentionOne
162
- : attentionCount > 1
163
- ? copy.attentionMany.replace("{count}", String(attentionCount))
164
- : showFinalizing
165
- ? copy.finishing
166
- : props.exportingUploadId
167
- ? copy.exporting
168
- : copy.retrying;
192
+ : props.localFilesUploading
193
+ ? copy.retrying
194
+ : attentionCount === 1
195
+ ? copy.attentionOne
196
+ : attentionCount > 1
197
+ ? copy.attentionMany.replace("{count}", String(attentionCount))
198
+ : showFinalizing
199
+ ? copy.finishing
200
+ : props.exportingUploadId
201
+ ? copy.exporting
202
+ : copy.retrying;
169
203
  return (
170
204
  <div data-tw-surface className="recording-recovery-entry">
171
205
  <Button
@@ -293,7 +327,7 @@ export function RecordingRecoveryPage(
293
327
  useLayoutEffect(() => {
294
328
  backRef.current?.focus();
295
329
  }, []);
296
- const { uploads, orphanErrors } = recoveryPresentation(props);
330
+ const { uploads, localFiles, orphanErrors } = recoveryPresentation(props);
297
331
  const busy = !!props.retryingUploadId || !!props.exportingUploadId;
298
332
  return (
299
333
  <section
@@ -344,6 +378,125 @@ export function RecordingRecoveryPage(
344
378
  </ItemActions>
345
379
  </Item>
346
380
  ) : null}
381
+ {props.localFilesError ? (
382
+ <Item size="recovery" role="listitem">
383
+ <ItemMedia data-recovery-tone="warning">
384
+ <IconAlertTriangle size={20} stroke={1.75} aria-hidden />
385
+ </ItemMedia>
386
+ <ItemContent>
387
+ <ItemTitle>{copy.localFiles}</ItemTitle>
388
+ <RecoveryDescription
389
+ text={copy.lookupFailed}
390
+ diagnostic={props.localFilesError}
391
+ />
392
+ </ItemContent>
393
+ <ItemActions>
394
+ <RecoveryMore
395
+ onOpenLogs={() => props.onOpenLogs("record-first-list")}
396
+ />
397
+ </ItemActions>
398
+ </Item>
399
+ ) : null}
400
+ {localFiles.map((file) => {
401
+ const failureKind = file.error
402
+ ? classifyRecordingRecoveryError(file.error, {
403
+ storageRequired: props.needsStorage(file.error),
404
+ incomplete: file.missing,
405
+ actionError: true,
406
+ })
407
+ : null;
408
+ const status = props.localFilesUploading
409
+ ? copy.retrying
410
+ : !props.authenticated
411
+ ? copy.signInToRetry
412
+ : file.error
413
+ ? copy[failureKind!]
414
+ : file.missing
415
+ ? copy.localSaveFailed
416
+ : props.localFilesStorageStatus === "missing"
417
+ ? copy.storageRequired
418
+ : props.localFilesStorageStatus === "unavailable"
419
+ ? copy.storageStatusUnavailable
420
+ : copy.uploadPending;
421
+ const severity = props.localFilesUploading
422
+ ? "neutral"
423
+ : file.missing
424
+ ? "error"
425
+ : "warning";
426
+ return (
427
+ <Item
428
+ key={`local:${file.path}`}
429
+ size="recovery"
430
+ role="listitem"
431
+ aria-label={file.fileName}
432
+ >
433
+ <ItemMedia data-recovery-tone={severity}>
434
+ {props.localFilesUploading ? (
435
+ <Spinner />
436
+ ) : (
437
+ <IconAlertTriangle size={16} stroke={1.75} aria-hidden />
438
+ )}
439
+ </ItemMedia>
440
+ <ItemContent>
441
+ <ItemTitle title={file.path}>{file.fileName}</ItemTitle>
442
+ <RecoveryDescription text={status} diagnostic={file.error} />
443
+ </ItemContent>
444
+ <ItemActions>
445
+ {!file.missing ? (
446
+ <RecoveryAction
447
+ label={
448
+ !props.authenticated
449
+ ? copy.signInToRetry
450
+ : props.localFilesStorageStatus === "configured"
451
+ ? copy.retry
452
+ : props.localFilesStorageStatus === "missing"
453
+ ? copy.connectStorage
454
+ : copy.refresh
455
+ }
456
+ disabled={
457
+ !props.authenticated ||
458
+ props.localFilesUploading ||
459
+ props.finalizing ||
460
+ props.localFilesStorageStatus === "checking"
461
+ }
462
+ busy={
463
+ props.localFilesUploading ||
464
+ props.localFilesStorageStatus === "checking"
465
+ }
466
+ onClick={() => {
467
+ if (props.localFilesStorageStatus === "configured") {
468
+ props.onRetryLocalFiles();
469
+ } else if (props.localFilesStorageStatus === "missing") {
470
+ props.onConnectLocalStorage();
471
+ } else {
472
+ props.onRefreshStorage();
473
+ }
474
+ }}
475
+ >
476
+ {props.localFilesStorageStatus === "missing" ? (
477
+ <IconUpload aria-hidden />
478
+ ) : (
479
+ <IconRefresh aria-hidden />
480
+ )}
481
+ </RecoveryAction>
482
+ ) : null}
483
+ <RecoveryMore
484
+ localAction={
485
+ file.missing
486
+ ? {
487
+ label: "Remove from list",
488
+ onSelect: () => props.onForgetLocalFile(file.path),
489
+ }
490
+ : undefined
491
+ }
492
+ onOpenLogs={() =>
493
+ props.onOpenLogs(`record-first:${file.path}`)
494
+ }
495
+ />
496
+ </ItemActions>
497
+ </Item>
498
+ );
499
+ })}
347
500
  {uploads.map((upload) => {
348
501
  const key = recordingRecoveryKey(upload);
349
502
  const retrying = props.retryingUploadId === key;
@@ -486,6 +639,8 @@ export function RecordingRecoveryPage(
486
639
  })}
487
640
  </ItemGroup>
488
641
  {!uploads.length &&
642
+ !localFiles.length &&
643
+ !props.localFilesError &&
489
644
  !orphanErrors.length &&
490
645
  !props.lookupErrors.length &&
491
646
  !props.finalizing ? (