@agent-native/core 0.77.4 → 0.77.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (192) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +31 -0
  3. package/corpus/core/package.json +3 -3
  4. package/corpus/core/src/agent/production-agent.ts +35 -0
  5. package/corpus/core/src/agent/run-store.ts +2 -0
  6. package/corpus/core/src/agent/thread-data-builder.ts +21 -2
  7. package/corpus/core/src/cli/create.ts +35 -1
  8. package/corpus/core/src/cli/skills.ts +8 -0
  9. package/corpus/core/src/client/AgentPanel.tsx +57 -25
  10. package/corpus/core/src/client/MultiTabAssistantChat.tsx +31 -1
  11. package/corpus/core/src/client/analytics.ts +24 -2
  12. package/corpus/core/src/client/changelog/Changelog.tsx +22 -10
  13. package/corpus/core/src/client/extensions/ExtensionsSidebarSection.tsx +405 -35
  14. package/corpus/core/src/client/use-agent-engine-configured.ts +29 -15
  15. package/corpus/core/src/client/use-chat-threads.ts +104 -55
  16. package/corpus/core/src/server/analytics.ts +27 -3
  17. package/corpus/core/src/sharing/access.ts +35 -13
  18. package/corpus/core/src/sharing/actions/share-resource.ts +32 -9
  19. package/corpus/core/src/sharing/actions/unshare-resource.ts +25 -2
  20. package/corpus/core/src/sharing/registry.ts +9 -0
  21. package/corpus/core/src/vite/client.ts +3 -0
  22. package/corpus/templates/analytics/actions/data-source-status.ts +25 -14
  23. package/corpus/templates/analytics/app/components/layout/NewAnalysisDialog.tsx +2 -2
  24. package/corpus/templates/analytics/app/lib/data-source-status.ts +47 -6
  25. package/corpus/templates/analytics/app/lib/data-sources.ts +14 -3
  26. package/corpus/templates/analytics/app/pages/DataSources.tsx +24 -5
  27. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/PanelEditorDialog.tsx +15 -43
  28. package/corpus/templates/analytics/changelog/2026-06-25-fixed-hubspot-data-source-status-so-private-app-tokens-legac.md +6 -0
  29. package/corpus/templates/analytics/netlify.toml +1 -0
  30. package/corpus/templates/analytics/server/lib/credential-keys.ts +3 -1
  31. package/corpus/templates/analytics/server/routes/api/test-connection.post.ts +12 -3
  32. package/corpus/templates/assets/app/components/layout/Sidebar.tsx +5 -2
  33. package/corpus/templates/assets/changelog/2026-06-25-create-now-returns-from-agent-settings-to-the-asset-chat-eve.md +6 -0
  34. package/corpus/templates/assets/netlify.toml +1 -0
  35. package/corpus/templates/brain/netlify.toml +1 -0
  36. package/corpus/templates/calendar/app/i18n-data.ts +12 -0
  37. package/corpus/templates/calendar/app/lib/clipboard.ts +58 -0
  38. package/corpus/templates/calendar/app/pages/AvailabilitySettings.tsx +15 -9
  39. package/corpus/templates/calendar/app/pages/BookingLinksPage.tsx +6 -2
  40. package/corpus/templates/calendar/changelog/2026-06-25-booking-link-copy-buttons-no-longer-fail-when-clipboard-perm.md +6 -0
  41. package/corpus/templates/calendar/netlify.toml +1 -0
  42. package/corpus/templates/chat/netlify.toml +1 -0
  43. package/corpus/templates/clips/AGENTS.md +9 -7
  44. package/corpus/templates/clips/actions/add-vocabulary-term.ts +2 -2
  45. package/corpus/templates/clips/actions/archive-recording.ts +5 -2
  46. package/corpus/templates/clips/actions/clear-edits.ts +5 -2
  47. package/corpus/templates/clips/actions/create-folder.ts +7 -2
  48. package/corpus/templates/clips/actions/delete-folder.ts +7 -4
  49. package/corpus/templates/clips/actions/delete-recording-permanent.ts +5 -2
  50. package/corpus/templates/clips/actions/finalize-recording.ts +5 -2
  51. package/corpus/templates/clips/actions/import-loom-recording.ts +2 -1
  52. package/corpus/templates/clips/actions/list-notifications.ts +9 -5
  53. package/corpus/templates/clips/actions/list-organization-state.ts +2 -1
  54. package/corpus/templates/clips/actions/list-recordings.ts +4 -1
  55. package/corpus/templates/clips/actions/move-recording.ts +5 -1
  56. package/corpus/templates/clips/actions/rename-folder.ts +6 -3
  57. package/corpus/templates/clips/actions/request-transcript.ts +5 -2
  58. package/corpus/templates/clips/actions/restore-recording.ts +5 -2
  59. package/corpus/templates/clips/actions/save-browser-diagnostics.ts +21 -8
  60. package/corpus/templates/clips/actions/set-chapters.ts +5 -2
  61. package/corpus/templates/clips/actions/set-thumbnail.ts +5 -2
  62. package/corpus/templates/clips/actions/split-recording.ts +5 -2
  63. package/corpus/templates/clips/actions/stitch-recordings.ts +6 -2
  64. package/corpus/templates/clips/actions/trash-recording.ts +5 -2
  65. package/corpus/templates/clips/actions/undo-edit.ts +5 -2
  66. package/corpus/templates/clips/actions/view-screen.ts +2 -1
  67. package/corpus/templates/clips/app/components/library/recording-card.tsx +24 -7
  68. package/corpus/templates/clips/app/i18n/ar-SA.ts +14 -0
  69. package/corpus/templates/clips/app/i18n/de-DE.ts +14 -0
  70. package/corpus/templates/clips/app/i18n/en-US.ts +14 -0
  71. package/corpus/templates/clips/app/i18n/es-ES.ts +14 -0
  72. package/corpus/templates/clips/app/i18n/fr-FR.ts +14 -0
  73. package/corpus/templates/clips/app/i18n/hi-IN.ts +14 -0
  74. package/corpus/templates/clips/app/i18n/ja-JP.ts +14 -0
  75. package/corpus/templates/clips/app/i18n/ko-KR.ts +14 -0
  76. package/corpus/templates/clips/app/i18n/pt-BR.ts +14 -0
  77. package/corpus/templates/clips/app/i18n/zh-CN.ts +14 -0
  78. package/corpus/templates/clips/app/lib/browser-diagnostics-capture.ts +5 -1
  79. package/corpus/templates/clips/app/root.tsx +2 -2
  80. package/corpus/templates/clips/app/routes/_app.extensions.$id.tsx +4 -1
  81. package/corpus/templates/clips/app/routes/_app.extensions._index.tsx +4 -1
  82. package/corpus/templates/clips/app/routes/_app.settings._index.tsx +12 -5
  83. package/corpus/templates/clips/changelog/2026-06-25-chrome-extension-recordings-now-upload-large-capture-bursts.md +6 -0
  84. package/corpus/templates/clips/changelog/2026-06-25-console-diagnostics-keep-more-useful-non-secret-details-when.md +6 -0
  85. package/corpus/templates/clips/changelog/2026-06-25-fixed-owner-recognition-so-clips-created-from-desktop-and-we.md +6 -0
  86. package/corpus/templates/clips/changelog/2026-06-25-localized-clips-settings-changelog-and-extension-sidebar-lab.md +6 -0
  87. package/corpus/templates/clips/chrome-extension/package.json +3 -0
  88. package/corpus/templates/clips/chrome-extension/public/manifest.json +1 -1
  89. package/corpus/templates/clips/chrome-extension/src/background.ts +85 -9
  90. package/corpus/templates/clips/chrome-extension/src/content-script.ts +55 -0
  91. package/corpus/templates/clips/chrome-extension/src/offscreen.ts +79 -3
  92. package/corpus/templates/clips/chrome-extension/src/options.ts +10 -0
  93. package/corpus/templates/clips/chrome-extension/src/overlay.ts +6 -0
  94. package/corpus/templates/clips/chrome-extension/src/permission.ts +8 -1
  95. package/corpus/templates/clips/chrome-extension/src/popup.ts +65 -39
  96. package/corpus/templates/clips/chrome-extension/src/sentry.ts +206 -0
  97. package/corpus/templates/clips/chrome-extension/vite.config.ts +47 -0
  98. package/corpus/templates/clips/desktop/src/overlays/countdown.tsx +4 -2
  99. package/corpus/templates/clips/desktop/src/styles.css +15 -1
  100. package/corpus/templates/clips/netlify.toml +1 -0
  101. package/corpus/templates/clips/server/db/index.ts +1 -0
  102. package/corpus/templates/clips/server/lib/google-calendar-oauth.ts +35 -23
  103. package/corpus/templates/clips/server/lib/public-agent-context.ts +8 -1
  104. package/corpus/templates/clips/server/lib/recordings.ts +18 -2
  105. package/corpus/templates/clips/server/lib/slack-oauth.ts +9 -6
  106. package/corpus/templates/clips/server/plugins/onboarding-calendar.ts +6 -2
  107. package/corpus/templates/clips/server/routes/api/public-recording.get.ts +2 -2
  108. package/corpus/templates/clips/server/routes/api/recordings/[recordingId]/thumbnail.post.ts +5 -2
  109. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/abort.post.ts +5 -2
  110. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +5 -2
  111. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/reset-chunks.post.ts +5 -2
  112. package/corpus/templates/clips/shared/browser-diagnostics.ts +14 -4
  113. package/corpus/templates/content/netlify.toml +1 -0
  114. package/corpus/templates/design/netlify.toml +1 -0
  115. package/corpus/templates/dispatch/netlify.toml +1 -0
  116. package/corpus/templates/forms/netlify.toml +1 -0
  117. package/corpus/templates/macros/netlify.toml +1 -0
  118. package/corpus/templates/mail/app/components/email/EmailList.tsx +7 -3
  119. package/corpus/templates/mail/app/components/email/EmailThread.tsx +4 -1
  120. package/corpus/templates/mail/app/components/layout/AppLayout.tsx +10 -2
  121. package/corpus/templates/mail/app/hooks/use-compose-state.ts +15 -5
  122. package/corpus/templates/mail/app/hooks/use-google-auth.ts +47 -9
  123. package/corpus/templates/mail/changelog/2026-06-25-account-avatars-stay-stable-while-mail-refreshes-google-acco.md +6 -0
  124. package/corpus/templates/mail/changelog/2026-06-25-mail-background-draft-saves-and-thread-prefetches-no-longer.md +6 -0
  125. package/corpus/templates/mail/netlify.toml +1 -0
  126. package/corpus/templates/plan/.agents/skills/visual-plan/references/canvas.md +8 -0
  127. package/corpus/templates/plan/netlify.toml +1 -0
  128. package/corpus/templates/slides/netlify.toml +1 -0
  129. package/corpus/templates/videos/netlify.toml +1 -0
  130. package/dist/agent/production-agent.d.ts.map +1 -1
  131. package/dist/agent/production-agent.js +31 -0
  132. package/dist/agent/production-agent.js.map +1 -1
  133. package/dist/agent/run-store.d.ts +2 -0
  134. package/dist/agent/run-store.d.ts.map +1 -1
  135. package/dist/agent/run-store.js +2 -0
  136. package/dist/agent/run-store.js.map +1 -1
  137. package/dist/agent/thread-data-builder.d.ts.map +1 -1
  138. package/dist/agent/thread-data-builder.js +23 -2
  139. package/dist/agent/thread-data-builder.js.map +1 -1
  140. package/dist/cli/create.d.ts.map +1 -1
  141. package/dist/cli/create.js +28 -1
  142. package/dist/cli/create.js.map +1 -1
  143. package/dist/cli/skills.d.ts +1 -1
  144. package/dist/cli/skills.d.ts.map +1 -1
  145. package/dist/cli/skills.js +8 -0
  146. package/dist/cli/skills.js.map +1 -1
  147. package/dist/client/AgentPanel.d.ts +8 -2
  148. package/dist/client/AgentPanel.d.ts.map +1 -1
  149. package/dist/client/AgentPanel.js +29 -15
  150. package/dist/client/AgentPanel.js.map +1 -1
  151. package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
  152. package/dist/client/MultiTabAssistantChat.js +4 -4
  153. package/dist/client/MultiTabAssistantChat.js.map +1 -1
  154. package/dist/client/analytics.d.ts.map +1 -1
  155. package/dist/client/analytics.js +22 -2
  156. package/dist/client/analytics.js.map +1 -1
  157. package/dist/client/changelog/Changelog.d.ts +7 -2
  158. package/dist/client/changelog/Changelog.d.ts.map +1 -1
  159. package/dist/client/changelog/Changelog.js +9 -7
  160. package/dist/client/changelog/Changelog.js.map +1 -1
  161. package/dist/client/extensions/ExtensionsSidebarSection.d.ts.map +1 -1
  162. package/dist/client/extensions/ExtensionsSidebarSection.js +339 -11
  163. package/dist/client/extensions/ExtensionsSidebarSection.js.map +1 -1
  164. package/dist/client/use-agent-engine-configured.d.ts.map +1 -1
  165. package/dist/client/use-agent-engine-configured.js +23 -13
  166. package/dist/client/use-agent-engine-configured.js.map +1 -1
  167. package/dist/client/use-chat-threads.d.ts +4 -0
  168. package/dist/client/use-chat-threads.d.ts.map +1 -1
  169. package/dist/client/use-chat-threads.js +49 -4
  170. package/dist/client/use-chat-threads.js.map +1 -1
  171. package/dist/collab/routes.d.ts +1 -1
  172. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  173. package/dist/observability/routes.d.ts +7 -7
  174. package/dist/server/agent-engine-api-key-route.d.ts +2 -2
  175. package/dist/server/analytics.d.ts +4 -0
  176. package/dist/server/analytics.d.ts.map +1 -1
  177. package/dist/server/analytics.js +19 -3
  178. package/dist/server/analytics.js.map +1 -1
  179. package/dist/sharing/access.d.ts.map +1 -1
  180. package/dist/sharing/access.js +26 -12
  181. package/dist/sharing/access.js.map +1 -1
  182. package/dist/sharing/actions/share-resource.js +20 -9
  183. package/dist/sharing/actions/share-resource.js.map +1 -1
  184. package/dist/sharing/actions/unshare-resource.js +13 -2
  185. package/dist/sharing/actions/unshare-resource.js.map +1 -1
  186. package/dist/sharing/registry.d.ts +9 -0
  187. package/dist/sharing/registry.d.ts.map +1 -1
  188. package/dist/sharing/registry.js.map +1 -1
  189. package/dist/vite/client.d.ts.map +1 -1
  190. package/dist/vite/client.js +1 -0
  191. package/dist/vite/client.js.map +1 -1
  192. package/package.json +3 -3
@@ -19,6 +19,10 @@
19
19
  import { scheduleReadyChime } from "@shared/recording-audio";
20
20
  import { chunkUploadUrl, pickMimeType } from "@shared/recording-core";
21
21
 
22
+ import { captureExtensionError, initExtensionSentry } from "./sentry";
23
+
24
+ initExtensionSentry("offscreen");
25
+
22
26
  type CaptureMode = "screen" | "camera";
23
27
 
24
28
  type AcquireMessage = {
@@ -118,6 +122,8 @@ type ActiveRecording = {
118
122
  rejectStopped: (error: Error) => void;
119
123
  };
120
124
 
125
+ const UPLOAD_SLICE_BYTES = 3 * 1024 * 1024;
126
+
121
127
  let prepared: PreparedStreams | null = null;
122
128
  let activeRecording: ActiveRecording | null = null;
123
129
 
@@ -399,13 +405,46 @@ async function uploadChunk(
399
405
  Boolean(recording.authToken),
400
406
  text.slice(0, 200),
401
407
  );
402
- throw new Error(
408
+ const error = new Error(
403
409
  data?.error || `Upload failed (${res.status}): ${text || res.statusText}`,
404
410
  );
411
+ captureExtensionError(error, {
412
+ tags: {
413
+ surface: "offscreen",
414
+ recordingStep: "chunk-upload",
415
+ httpStatus: String(res.status),
416
+ isFinal: extra.isFinal ? "true" : "false",
417
+ },
418
+ extra: {
419
+ recordingId: recording.recordingId,
420
+ chunkIndex: index,
421
+ chunkBytes: blob.size,
422
+ total: extra.total,
423
+ mimeType: blob.type || recording.mimeType,
424
+ responseBodyTail: text.slice(0, 2000),
425
+ hadAuth: Boolean(recording.authToken),
426
+ },
427
+ });
428
+ throw error;
405
429
  }
406
430
  return data;
407
431
  }
408
432
 
433
+ async function uploadBlobInSlices(
434
+ recording: ActiveRecording,
435
+ blob: Blob,
436
+ ): Promise<void> {
437
+ const totalSlices = Math.max(1, Math.ceil(blob.size / UPLOAD_SLICE_BYTES));
438
+ for (let sliceIndex = 0; sliceIndex < totalSlices; sliceIndex += 1) {
439
+ if (recording.cancelled || recording.uploadFailure) return;
440
+ const start = sliceIndex * UPLOAD_SLICE_BYTES;
441
+ const end = Math.min(start + UPLOAD_SLICE_BYTES, blob.size);
442
+ const slice = blob.slice(start, end, blob.type || recording.mimeType);
443
+ const index = recording.chunkIndex++;
444
+ await uploadChunk(recording, slice, index);
445
+ }
446
+ }
447
+
409
448
  function stopStreams(streams: (MediaStream | null)[]): void {
410
449
  for (const stream of streams) {
411
450
  if (!stream) continue;
@@ -616,14 +655,25 @@ async function begin(message: BeginMessage): Promise<{
616
655
  ) {
617
656
  return;
618
657
  }
619
- const index = recording.chunkIndex++;
620
658
  // Record the failure and stop, but do NOT re-throw: re-throwing leaves a
621
659
  // rejected promise that surfaces as an "Uncaught (in promise)" error (bad
622
660
  // look in a Chrome Web Store review). finalizeStop reads recording.upload-
623
661
  // Failure and surfaces it through the normal error path instead.
624
- const upload = uploadChunk(recording, event.data, index).catch((err) => {
662
+ const upload = uploadBlobInSlices(recording, event.data).catch((err) => {
625
663
  recording.uploadFailure =
626
664
  err instanceof Error ? err : new Error(String(err));
665
+ captureExtensionError(recording.uploadFailure, {
666
+ tags: {
667
+ surface: "offscreen",
668
+ recordingStep: "dataavailable-upload",
669
+ },
670
+ extra: {
671
+ recordingId: recording.recordingId,
672
+ blobBytes: event.data.size,
673
+ chunkIndex: recording.chunkIndex,
674
+ mimeType: event.data.type || recording.mimeType,
675
+ },
676
+ });
627
677
  reportStatus(recording.sessionId, "error", {
628
678
  error: recording.uploadFailure.message,
629
679
  });
@@ -687,6 +737,20 @@ function startRecorderNow(recording: ActiveRecording): void {
687
737
  hasCamera: recording.hasCamera,
688
738
  });
689
739
  } catch (err) {
740
+ captureExtensionError(err, {
741
+ tags: {
742
+ surface: "offscreen",
743
+ recordingStep: "recorder-start",
744
+ },
745
+ extra: {
746
+ recordingId: recording.recordingId,
747
+ mimeType: recording.mimeType,
748
+ width: recording.dimensions.width,
749
+ height: recording.dimensions.height,
750
+ hasAudio: recording.hasAudio,
751
+ hasCamera: recording.hasCamera,
752
+ },
753
+ });
690
754
  reportStatus(recording.sessionId, "error", {
691
755
  recordingId: recording.recordingId,
692
756
  error: err instanceof Error ? err.message : "Could not start recording.",
@@ -766,6 +830,18 @@ async function finalizeStop(recording: ActiveRecording): Promise<void> {
766
830
  cleanup(recording);
767
831
  if (activeRecording === recording) activeRecording = null;
768
832
  const error = err instanceof Error ? err : new Error(String(err));
833
+ captureExtensionError(error, {
834
+ tags: {
835
+ surface: "offscreen",
836
+ recordingStep: "finalize-stop",
837
+ },
838
+ extra: {
839
+ recordingId: recording.recordingId,
840
+ chunkCount: recording.chunkIndex,
841
+ mimeType: recording.mimeType,
842
+ durationMs: Math.max(0, Date.now() - recording.startedAtMs),
843
+ },
844
+ });
769
845
  reportStatus(recording.sessionId, "error", {
770
846
  recordingId: recording.recordingId,
771
847
  error: error.message,
@@ -1,3 +1,7 @@
1
+ import { captureExtensionError, initExtensionSentry } from "./sentry";
2
+
3
+ initExtensionSentry("options");
4
+
1
5
  type CaptureSurface = "browser" | "window" | "monitor" | "camera";
2
6
 
3
7
  type ExtensionSettings = {
@@ -116,6 +120,9 @@ async function init(): Promise<void> {
116
120
  await saveSettings(settings);
117
121
  setStatus("Settings saved.");
118
122
  } catch (err) {
123
+ captureExtensionError(err, {
124
+ tags: { surface: "options", action: "save-settings" },
125
+ });
119
126
  setStatus(
120
127
  err instanceof Error ? err.message : "Could not save settings.",
121
128
  "error",
@@ -132,6 +139,9 @@ async function init(): Promise<void> {
132
139
  }
133
140
 
134
141
  void init().catch((err) => {
142
+ captureExtensionError(err, {
143
+ tags: { surface: "options", action: "init" },
144
+ });
135
145
  setStatus(
136
146
  err instanceof Error ? err.message : "Could not load settings.",
137
147
  "error",
@@ -1,4 +1,7 @@
1
1
  import "./overlay.css";
2
+ import { captureExtensionError, initExtensionSentry } from "./sentry";
3
+
4
+ initExtensionSentry("overlay");
2
5
 
3
6
  // The overlay runs as an extension-origin iframe injected into the page by the
4
7
  // content script. Each iframe renders one "part" of the Loom-style recording UI
@@ -166,6 +169,9 @@ async function initBubble(): Promise<void> {
166
169
  postBubble("camera-ready");
167
170
  } catch (err) {
168
171
  console.warn("[clips-overlay] camera getUserMedia failed:", err);
172
+ captureExtensionError(err, {
173
+ tags: { surface: "overlay", overlayPart: "bubble" },
174
+ });
169
175
  const empty = document.createElement("div");
170
176
  empty.className = "bubble-empty";
171
177
  empty.innerHTML = ICONS.cameraOff;
@@ -5,6 +5,10 @@
5
5
  // the grant for the whole chrome-extension:// origin — so the offscreen recorder
6
6
  // (mic) and the camera-bubble iframe both work afterward.
7
7
 
8
+ import { captureExtensionError, initExtensionSentry } from "./sentry";
9
+
10
+ initExtensionSentry("permission");
11
+
8
12
  const enableBtn = document.getElementById("enable") as HTMLButtonElement;
9
13
  const statusEl = document.getElementById("status") as HTMLDivElement;
10
14
  const successEl = document.getElementById("success-state") as HTMLDivElement;
@@ -71,7 +75,10 @@ async function requestOne(kind: "mic" | "cam"): Promise<boolean> {
71
75
  const stream = await navigator.mediaDevices.getUserMedia(constraints);
72
76
  for (const track of stream.getTracks()) track.stop();
73
77
  return true;
74
- } catch {
78
+ } catch (err) {
79
+ captureExtensionError(err, {
80
+ tags: { surface: "permission", permission: kind },
81
+ });
75
82
  return false;
76
83
  }
77
84
  }
@@ -1,3 +1,7 @@
1
+ import { captureExtensionError, initExtensionSentry } from "./sentry";
2
+
3
+ initExtensionSentry("popup");
4
+
1
5
  type CaptureSurface = "browser" | "window" | "monitor" | "camera";
2
6
  type RecordingModeChoice = "screen-camera" | "screen" | "camera";
3
7
 
@@ -1032,6 +1036,9 @@ async function init(): Promise<void> {
1032
1036
  1400,
1033
1037
  );
1034
1038
  } catch (err) {
1039
+ captureExtensionError(err, {
1040
+ tags: { surface: "popup", action: "submit-feedback" },
1041
+ });
1035
1042
  feedbackSubmit.disabled = !feedbackTextarea.value.trim();
1036
1043
  feedbackSubmit.textContent = FEEDBACK_SUBMIT_TEXT;
1037
1044
  feedbackHint.textContent =
@@ -1065,51 +1072,67 @@ async function init(): Promise<void> {
1065
1072
  start.disabled = true;
1066
1073
  signIn.hidden = true;
1067
1074
  setStatus(""); // no chatty "Checking…/Starting…" text — the disabled button is enough
1068
- authStatus = await readAuthStatus(settings);
1069
- if (authStatus === "signed-out") {
1075
+ try {
1076
+ authStatus = await readAuthStatus(settings);
1077
+ if (authStatus === "signed-out") {
1078
+ start.disabled = false;
1079
+ start.hidden = true;
1080
+ signIn.hidden = false;
1081
+ setStatus("");
1082
+ return;
1083
+ }
1084
+ const storageConfigured = await readVideoStorageConfigured(settings);
1085
+ if (!storageConfigured) {
1086
+ start.disabled = false;
1087
+ setStatus(
1088
+ "Connect Builder.io or S3 storage in Clips, then start recording.",
1089
+ "error",
1090
+ );
1091
+ await createTab(`${settings.clipsBaseUrl.replace(/\/+$/, "")}/record`);
1092
+ window.close();
1093
+ return;
1094
+ }
1095
+ // Gate at record time: if the user wants camera/mic but hasn't granted the
1096
+ // extension access yet, send them to the onboarding page first. Requesting
1097
+ // there (a real extension page) is the only place Chrome reliably shows the
1098
+ // permission dialog and persists the grant for the offscreen recorder + bubble.
1099
+ if (!(await ensureMediaPermission(settings))) {
1100
+ start.disabled = false;
1101
+ setStatus("Allow camera & microphone, then start recording.", "error");
1102
+ await createTab(chrome.runtime.getURL("src/permission.html"));
1103
+ window.close();
1104
+ return;
1105
+ }
1106
+ await saveSettings(settings);
1107
+ const response = await sendStartMessage(settings);
1108
+ if (response.ok) {
1109
+ window.close();
1110
+ return;
1111
+ }
1070
1112
  start.disabled = false;
1071
- start.hidden = true;
1072
- signIn.hidden = false;
1073
- setStatus("");
1074
- return;
1075
- }
1076
- const storageConfigured = await readVideoStorageConfigured(settings);
1077
- if (!storageConfigured) {
1113
+ const message = response.error || "Could not start Clips.";
1114
+ if (isSignInError(message)) {
1115
+ start.hidden = true;
1116
+ signIn.hidden = false;
1117
+ setStatus("");
1118
+ return;
1119
+ }
1120
+ setStatus(message, "error");
1121
+ } catch (err) {
1122
+ captureExtensionError(err, {
1123
+ tags: { surface: "popup", action: "start-recording" },
1124
+ extra: {
1125
+ captureSurface: settings.captureSurface,
1126
+ includeCamera: settings.includeCamera,
1127
+ includeMicrophone: settings.includeMicrophone,
1128
+ },
1129
+ });
1078
1130
  start.disabled = false;
1079
1131
  setStatus(
1080
- "Connect Builder.io or S3 storage in Clips, then start recording.",
1132
+ err instanceof Error ? err.message : "Could not start Clips.",
1081
1133
  "error",
1082
1134
  );
1083
- await createTab(`${settings.clipsBaseUrl.replace(/\/+$/, "")}/record`);
1084
- window.close();
1085
- return;
1086
- }
1087
- // Gate at record time: if the user wants camera/mic but hasn't granted the
1088
- // extension access yet, send them to the onboarding page first. Requesting
1089
- // there (a real extension page) is the only place Chrome reliably shows the
1090
- // permission dialog and persists the grant for the offscreen recorder + bubble.
1091
- if (!(await ensureMediaPermission(settings))) {
1092
- start.disabled = false;
1093
- setStatus("Allow camera & microphone, then start recording.", "error");
1094
- await createTab(chrome.runtime.getURL("src/permission.html"));
1095
- window.close();
1096
- return;
1097
1135
  }
1098
- await saveSettings(settings);
1099
- const response = await sendStartMessage(settings);
1100
- if (response.ok) {
1101
- window.close();
1102
- return;
1103
- }
1104
- start.disabled = false;
1105
- const message = response.error || "Could not start Clips.";
1106
- if (isSignInError(message)) {
1107
- start.hidden = true;
1108
- signIn.hidden = false;
1109
- setStatus("");
1110
- return;
1111
- }
1112
- setStatus(message, "error");
1113
1136
  });
1114
1137
 
1115
1138
  signIn.addEventListener("click", async () => {
@@ -1173,6 +1196,9 @@ async function init(): Promise<void> {
1173
1196
  }
1174
1197
 
1175
1198
  void init().catch((err) => {
1199
+ captureExtensionError(err, {
1200
+ tags: { surface: "popup", action: "init" },
1201
+ });
1176
1202
  setStatus(
1177
1203
  err instanceof Error ? err.message : "Could not load popup.",
1178
1204
  "error",
@@ -0,0 +1,206 @@
1
+ import * as Sentry from "@sentry/browser";
2
+
3
+ declare const __CLIPS_SENTRY_DSN__: string;
4
+ declare const __CLIPS_SENTRY_ENVIRONMENT__: string;
5
+
6
+ type CaptureContext = {
7
+ tags?: Record<string, string | undefined>;
8
+ extra?: Record<string, unknown>;
9
+ contexts?: Record<string, Record<string, unknown>>;
10
+ };
11
+
12
+ const SENSITIVE_QUERY_PARAM_RE =
13
+ /(?:token|secret|password|passwd|pwd|key|api[-_]?key|code|state|session|auth|credential)/i;
14
+
15
+ let initialized = false;
16
+ let globalErrorCaptureInstalled = false;
17
+
18
+ function configuredDsn(): string {
19
+ return typeof __CLIPS_SENTRY_DSN__ === "string"
20
+ ? __CLIPS_SENTRY_DSN__.trim()
21
+ : "";
22
+ }
23
+
24
+ function extensionVersion(): string {
25
+ try {
26
+ return chrome.runtime.getManifest().version || "unknown";
27
+ } catch {
28
+ return "unknown";
29
+ }
30
+ }
31
+
32
+ function scrubUrl(
33
+ value: string,
34
+ options: { redactAllQueryValues?: boolean } = {},
35
+ ): string {
36
+ try {
37
+ const url = new URL(value);
38
+ for (const key of [...url.searchParams.keys()]) {
39
+ if (options.redactAllQueryValues || SENSITIVE_QUERY_PARAM_RE.test(key)) {
40
+ url.searchParams.set(key, "<redacted>");
41
+ }
42
+ }
43
+ return url.toString();
44
+ } catch {
45
+ return value;
46
+ }
47
+ }
48
+
49
+ function scrubEvent<T extends Sentry.Event>(event: T): T {
50
+ if (event.request?.url) {
51
+ event.request.url = scrubUrl(event.request.url);
52
+ }
53
+ if (Array.isArray(event.breadcrumbs)) {
54
+ for (const breadcrumb of event.breadcrumbs) {
55
+ const data = breadcrumb.data as Record<string, unknown> | undefined;
56
+ if (!data) continue;
57
+ for (const key of ["url", "from", "to"]) {
58
+ if (typeof data[key] === "string") {
59
+ data[key] = scrubUrl(data[key]);
60
+ }
61
+ }
62
+ }
63
+ }
64
+ if (event.user) {
65
+ delete (event.user as Record<string, unknown>).ip_address;
66
+ }
67
+ return event;
68
+ }
69
+
70
+ function scrubScopeString(value: string): string {
71
+ const scrubbed = scrubUrl(value, { redactAllQueryValues: true });
72
+ if (scrubbed !== value) return scrubbed;
73
+ return value.replace(/https?:\/\/[^\s"'<>]+/g, (url) =>
74
+ scrubUrl(url, { redactAllQueryValues: true }),
75
+ );
76
+ }
77
+
78
+ function scrubScopeValue(
79
+ value: unknown,
80
+ seen = new WeakSet<object>(),
81
+ ): unknown {
82
+ if (typeof value === "string") return scrubScopeString(value);
83
+ if (Array.isArray(value)) {
84
+ return value.map((item) => scrubScopeValue(item, seen));
85
+ }
86
+ if (!value || typeof value !== "object") return value;
87
+ if (seen.has(value)) return "[Circular]";
88
+ if (Object.prototype.toString.call(value) !== "[object Object]") {
89
+ return value;
90
+ }
91
+ seen.add(value);
92
+ return Object.fromEntries(
93
+ Object.entries(value as Record<string, unknown>).map(([key, entry]) => [
94
+ key,
95
+ scrubScopeValue(entry, seen),
96
+ ]),
97
+ );
98
+ }
99
+
100
+ function installGlobalErrorCapture(surface: string): void {
101
+ if (globalErrorCaptureInstalled) return;
102
+ globalErrorCaptureInstalled = true;
103
+ const target = globalThis as typeof globalThis & {
104
+ addEventListener?: (
105
+ type: string,
106
+ listener: EventListenerOrEventListenerObject,
107
+ ) => void;
108
+ };
109
+ if (typeof target.addEventListener !== "function") return;
110
+
111
+ target.addEventListener("error", (event) => {
112
+ const errorEvent = event as ErrorEvent;
113
+ const error =
114
+ errorEvent.error instanceof Error
115
+ ? errorEvent.error
116
+ : new Error(errorEvent.message || "Unhandled extension error");
117
+ captureExtensionError(error, {
118
+ tags: { surface, mechanism: "global-error" },
119
+ extra: {
120
+ filename: errorEvent.filename,
121
+ lineno: errorEvent.lineno,
122
+ colno: errorEvent.colno,
123
+ },
124
+ });
125
+ });
126
+
127
+ target.addEventListener("unhandledrejection", (event) => {
128
+ const rejection = event as PromiseRejectionEvent;
129
+ const error =
130
+ rejection.reason instanceof Error
131
+ ? rejection.reason
132
+ : new Error(String(rejection.reason ?? "Unhandled promise rejection"));
133
+ captureExtensionError(error, {
134
+ tags: { surface, mechanism: "unhandled-rejection" },
135
+ });
136
+ });
137
+ }
138
+
139
+ export function initExtensionSentry(surface: string): void {
140
+ if (initialized) return;
141
+ const dsn = configuredDsn();
142
+ if (!dsn) return;
143
+ initialized = true;
144
+
145
+ Sentry.init({
146
+ dsn,
147
+ environment:
148
+ typeof __CLIPS_SENTRY_ENVIRONMENT__ === "string" &&
149
+ __CLIPS_SENTRY_ENVIRONMENT__
150
+ ? __CLIPS_SENTRY_ENVIRONMENT__
151
+ : "production",
152
+ release: `clips-chrome-extension@${extensionVersion()}`,
153
+ sendDefaultPii: false,
154
+ skipBrowserExtensionCheck: true,
155
+ beforeSend(event) {
156
+ event.tags = {
157
+ ...event.tags,
158
+ app: "agent-native-clips",
159
+ template: "clips",
160
+ runtime: "chrome-extension",
161
+ surface,
162
+ };
163
+ return scrubEvent(event);
164
+ },
165
+ });
166
+
167
+ Sentry.setTag("app", "agent-native-clips");
168
+ Sentry.setTag("template", "clips");
169
+ Sentry.setTag("runtime", "chrome-extension");
170
+ Sentry.setTag("surface", surface);
171
+ installGlobalErrorCapture(surface);
172
+ }
173
+
174
+ export function captureExtensionError(
175
+ error: unknown,
176
+ context: CaptureContext = {},
177
+ ): string | undefined {
178
+ if (!initialized) return undefined;
179
+ try {
180
+ return Sentry.withScope((scope) => {
181
+ if (context.tags) {
182
+ for (const [key, value] of Object.entries(context.tags)) {
183
+ if (typeof value === "string") {
184
+ scope.setTag(key, scrubScopeString(value));
185
+ }
186
+ }
187
+ }
188
+ if (context.extra) {
189
+ for (const [key, value] of Object.entries(context.extra)) {
190
+ if (value !== undefined) scope.setExtra(key, scrubScopeValue(value));
191
+ }
192
+ }
193
+ if (context.contexts) {
194
+ for (const [key, value] of Object.entries(context.contexts)) {
195
+ scope.setContext(
196
+ key,
197
+ scrubScopeValue(value) as Record<string, unknown>,
198
+ );
199
+ }
200
+ }
201
+ return Sentry.captureException(error);
202
+ });
203
+ } catch {
204
+ return undefined;
205
+ }
206
+ }
@@ -5,10 +5,57 @@ import { defineConfig } from "vite";
5
5
 
6
6
  const root = dirname(fileURLToPath(import.meta.url));
7
7
 
8
+ function firstNonEmpty(...values: Array<string | undefined>): string {
9
+ for (const value of values) {
10
+ const trimmed = value?.trim();
11
+ if (trimmed) return trimmed;
12
+ }
13
+ return "";
14
+ }
15
+
16
+ function resolveSentryDsn(): string {
17
+ const direct = firstNonEmpty(
18
+ process.env.SENTRY_CLIENT_DSN,
19
+ process.env.VITE_SENTRY_CLIENT_DSN,
20
+ process.env.VITE_SENTRY_DSN,
21
+ process.env.SENTRY_DSN,
22
+ );
23
+ if (direct) return direct;
24
+
25
+ const key = firstNonEmpty(
26
+ process.env.SENTRY_CLIENT_KEY,
27
+ process.env.VITE_SENTRY_CLIENT_KEY,
28
+ );
29
+ const projectId = firstNonEmpty(
30
+ process.env.SENTRY_PROJECT_ID,
31
+ process.env.VITE_SENTRY_PROJECT_ID,
32
+ );
33
+ const host = firstNonEmpty(
34
+ process.env.SENTRY_INGEST_HOST,
35
+ process.env.VITE_SENTRY_INGEST_HOST,
36
+ );
37
+ return key && projectId && host ? `https://${key}@${host}/${projectId}` : "";
38
+ }
39
+
40
+ function resolveSentryEnvironment(): string {
41
+ return (
42
+ firstNonEmpty(
43
+ process.env.SENTRY_ENVIRONMENT,
44
+ process.env.NETLIFY_CONTEXT,
45
+ process.env.VERCEL_ENV,
46
+ process.env.NODE_ENV,
47
+ ) || "production"
48
+ );
49
+ }
50
+
8
51
  export default defineConfig({
9
52
  root,
10
53
  base: "./",
11
54
  publicDir: "public",
55
+ define: {
56
+ __CLIPS_SENTRY_DSN__: JSON.stringify(resolveSentryDsn()),
57
+ __CLIPS_SENTRY_ENVIRONMENT__: JSON.stringify(resolveSentryEnvironment()),
58
+ },
12
59
  // Share recording primitives with the web app recorder (templates/clips/shared)
13
60
  // — matches the "@shared/*" tsconfig path so imports resolve in both builds.
14
61
  resolve: {
@@ -66,8 +66,10 @@ export function Countdown() {
66
66
  >
67
67
  <IconX size={30} stroke={2.4} />
68
68
  </button>
69
- <div className="countdown-number" key={n} aria-live="polite">
70
- {n > 0 ? n : ""}
69
+ <div className="countdown-number-wrap">
70
+ <div className="countdown-number" key={n} aria-live="polite">
71
+ {n > 0 ? n : ""}
72
+ </div>
71
73
  </div>
72
74
  <button
73
75
  type="button"
@@ -2099,7 +2099,7 @@ body[data-clips-route="recording-pill"] #root {
2099
2099
 
2100
2100
  .countdown-control-skip {
2101
2101
  /* center at +200px from row center: 200 - (64 / 2) = 168px */
2102
- right: -232px;
2102
+ left: 168px;
2103
2103
  }
2104
2104
 
2105
2105
  .countdown-control:hover {
@@ -2117,6 +2117,20 @@ body[data-clips-route="recording-pill"] #root {
2117
2117
  outline-offset: 2px;
2118
2118
  }
2119
2119
 
2120
+ /*
2121
+ * Absolutely centers the number without contributing width to .countdown-controls.
2122
+ * This keeps the controls div at zero width so the cancel/skip button positions
2123
+ * (left: -232px, left: 168px) line up exactly with the Rust cursor-poll hit-rects
2124
+ * (CONTROL_OFFSET_X = 200.0) measured from the window center.
2125
+ */
2126
+ .countdown-number-wrap {
2127
+ position: absolute;
2128
+ top: 50%;
2129
+ left: 0;
2130
+ transform: translate(-50%, -50%);
2131
+ pointer-events: none;
2132
+ }
2133
+
2120
2134
  .countdown-number {
2121
2135
  font-size: 240px;
2122
2136
  font-weight: 800;
@@ -5,6 +5,7 @@ publish = "templates/clips/dist"
5
5
  functions = "templates/clips/.netlify/functions-internal"
6
6
 
7
7
  [build.environment]
8
+ GA_MEASUREMENT_ID = "G-ESF7FYXGN9"
8
9
  NITRO_PRESET = "netlify"
9
10
  NPM_CONFIG_PRODUCTION = "false"
10
11
  AUTO_CREATE_DEFAULT_ORG = "1"
@@ -14,6 +14,7 @@ registerShareableResource({
14
14
  titleColumn: "title",
15
15
  getResourcePath: (recording) => `/r/${recording.id}`,
16
16
  getDb,
17
+ ownerAccessIgnoresOrg: true,
17
18
  });
18
19
 
19
20
  registerShareableResource({