@agent-native/core 0.79.7 → 0.79.9

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 (84) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +13 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/agent/run-manager.ts +9 -1
  5. package/corpus/core/src/client/agent-chat-adapter.ts +24 -16
  6. package/corpus/core/src/client/session-replay.ts +67 -13
  7. package/corpus/core/src/onboarding/default-steps.ts +3 -3
  8. package/corpus/core/src/server/agent-chat-plugin.ts +82 -162
  9. package/corpus/core/src/server/agent-run-context.ts +204 -0
  10. package/corpus/core/src/server/credential-provider.ts +20 -4
  11. package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +14 -4
  12. package/corpus/templates/analytics/changelog/2026-06-26-session-replay-uploads-now-accept-compressed-payloads-so-lar.md +6 -0
  13. package/corpus/templates/analytics/changelog/2026-06-26-sql-chart-tooltips-now-stay-stable-while-moving-across-dense.md +6 -0
  14. package/corpus/templates/analytics/server/handlers/session-replay.ts +56 -8
  15. package/corpus/templates/clips/app/components/player/transcript-panel.tsx +1 -1
  16. package/corpus/templates/clips/app/components/recorder/storage-setup-card.tsx +44 -16
  17. package/corpus/templates/clips/app/i18n/ar-SA.ts +12 -9
  18. package/corpus/templates/clips/app/i18n/de-DE.ts +12 -9
  19. package/corpus/templates/clips/app/i18n/en-US.ts +19 -16
  20. package/corpus/templates/clips/app/i18n/es-ES.ts +12 -9
  21. package/corpus/templates/clips/app/i18n/fr-FR.ts +12 -9
  22. package/corpus/templates/clips/app/i18n/hi-IN.ts +13 -9
  23. package/corpus/templates/clips/app/i18n/ja-JP.ts +12 -9
  24. package/corpus/templates/clips/app/i18n/ko-KR.ts +12 -9
  25. package/corpus/templates/clips/app/i18n/pt-BR.ts +12 -9
  26. package/corpus/templates/clips/app/i18n/zh-CN.ts +19 -14
  27. package/corpus/templates/clips/app/i18n/zh-TW.ts +19 -14
  28. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +7 -0
  29. package/corpus/templates/clips/app/routes/record.tsx +2 -2
  30. package/corpus/templates/clips/changelog/2026-06-26-desktop-recordings-recover-more-reliably-when-macos-reports-.md +6 -0
  31. package/corpus/templates/clips/changelog/2026-06-26-slack-unfurls-now-recognize-recording-dashboard-links-pasted.md +6 -0
  32. package/corpus/templates/clips/changelog/2026-06-26-storage-setup-now-explains-why-clips-needs-connected-storage.md +6 -0
  33. package/corpus/templates/clips/chrome-extension/public/manifest.json +1 -1
  34. package/corpus/templates/clips/chrome-extension/src/background.ts +24 -3
  35. package/corpus/templates/clips/chrome-extension/src/offscreen.ts +28 -2
  36. package/corpus/templates/clips/chrome-extension/src/popup.html +16 -0
  37. package/corpus/templates/clips/chrome-extension/src/popup.ts +59 -7
  38. package/corpus/templates/clips/chrome-extension/src/sentry.ts +28 -1
  39. package/corpus/templates/clips/chrome-extension/src/styles.css +74 -0
  40. package/corpus/templates/clips/desktop/src/app.tsx +85 -18
  41. package/corpus/templates/clips/desktop/src/styles.css +55 -0
  42. package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +88 -51
  43. package/corpus/templates/clips/desktop/src-tauri/src/native_speech.rs +12 -3
  44. package/corpus/templates/clips/server/lib/slack-unfurls.ts +5 -2
  45. package/dist/agent/run-manager.d.ts.map +1 -1
  46. package/dist/agent/run-manager.js +12 -1
  47. package/dist/agent/run-manager.js.map +1 -1
  48. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  49. package/dist/client/agent-chat-adapter.js +15 -8
  50. package/dist/client/agent-chat-adapter.js.map +1 -1
  51. package/dist/client/session-replay.d.ts.map +1 -1
  52. package/dist/client/session-replay.js +55 -14
  53. package/dist/client/session-replay.js.map +1 -1
  54. package/dist/collab/awareness.d.ts +2 -2
  55. package/dist/collab/awareness.d.ts.map +1 -1
  56. package/dist/notifications/routes.d.ts +3 -3
  57. package/dist/observability/routes.d.ts +6 -6
  58. package/dist/onboarding/default-steps.js +3 -3
  59. package/dist/onboarding/default-steps.js.map +1 -1
  60. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  61. package/dist/server/agent-chat-plugin.js +69 -154
  62. package/dist/server/agent-chat-plugin.js.map +1 -1
  63. package/dist/server/agent-engine-api-key-route.d.ts +2 -2
  64. package/dist/server/agent-run-context.d.ts +35 -0
  65. package/dist/server/agent-run-context.d.ts.map +1 -0
  66. package/dist/server/agent-run-context.js +137 -0
  67. package/dist/server/agent-run-context.js.map +1 -0
  68. package/dist/server/credential-provider.d.ts.map +1 -1
  69. package/dist/server/credential-provider.js +15 -4
  70. package/dist/server/credential-provider.js.map +1 -1
  71. package/package.json +1 -1
  72. package/corpus/templates/.retired/README.md +0 -9
  73. package/corpus/templates/.retired/calls/package.json +0 -8
  74. package/corpus/templates/.retired/code/package.json +0 -8
  75. package/corpus/templates/.retired/contracts/package.json +0 -8
  76. package/corpus/templates/.retired/images/package.json +0 -8
  77. package/corpus/templates/.retired/issues/package.json +0 -8
  78. package/corpus/templates/.retired/meeting-notes/package.json +0 -8
  79. package/corpus/templates/.retired/migration/package.json +0 -8
  80. package/corpus/templates/.retired/recruiting/package.json +0 -8
  81. package/corpus/templates/.retired/scheduling/package.json +0 -8
  82. package/corpus/templates/.retired/visual-plans/package.json +0 -8
  83. package/corpus/templates/.retired/voice/package.json +0 -8
  84. package/corpus/templates/.retired/workbench/package.json +0 -8
@@ -146,7 +146,7 @@ const messages = {
146
146
  savingWentWrong: "保存此剪辑时出现问题。",
147
147
  finishingClip: "正在完成您的剪辑...",
148
148
  loomSourcePreserved:
149
- "Loom 源链接被保留。连接 Builder.io S3 存储,Clips 将重试保存其自己的副本。",
149
+ "Loom 源链接被保留。连接存储:Builder.io(免费套餐存储 + AI)或 S3 兼容存储,Clips 将重试保存自己的副本。",
150
150
  clipDataPreserved:
151
151
  "Your clip data is still preserved.连接Builder.io或S3存储,Clips将自动上传。",
152
152
  details: "细节",
@@ -223,12 +223,13 @@ const messages = {
223
223
  savingWentWrong: "保存此剪辑时出现问题。",
224
224
  finishingClip: "完成这个剪辑...",
225
225
  loomPreservedManage:
226
- "Loom 源链接被保留。连接 Builder.io S3 存储,然后重试导入。",
226
+ "Loom 源链接被保留。连接存储:Builder.io(免费套餐存储 + AI)或 S3 兼容存储,然后重试导入。",
227
227
  videoPreservedManage:
228
228
  "视频被保留。连接Builder.io或S3存储,Clips将完成上传。",
229
- creatorNeedsStorage: "创建者需要连接 Builder.io 或 S3 存储才能完成此剪辑。",
229
+ creatorNeedsStorage:
230
+ "创建者需要先连接存储才能完成此剪辑:Builder.io(免费套餐存储 + AI)或 S3 兼容存储。",
230
231
  signInStorage:
231
- "如果这是您的剪辑,请在此处登录以连接 Builder.io S3 存储并完成上传。",
232
+ "如果这是您的剪辑,请在此处登录以连接存储:Builder.io(免费套餐存储 + AI)或 S3 兼容存储,并完成上传。",
232
233
  uploadNotCompleteSession:
233
234
  "上传尚未完成。打开此剪辑的仪表板或要求创建者检查存储空间。",
234
235
  uploadNotCompleteSignIn:
@@ -330,7 +331,8 @@ const messages = {
330
331
  cleanupKept: "清理工作无法完成。保留了本机转录本。",
331
332
  cleanupBuilderFailed:
332
333
  "即使 Builder.io 已连接,清理也无法完成。保留了本机转录本。",
333
- cleanupPaused: "清理已暂停。在“设置”中连接 Builder.io 即可启用它。",
334
+ cleanupPaused:
335
+ "清理已暂停。请在“设置”中连接 AI:Builder.io(免费额度)或你自己的 LLM 密钥。",
334
336
  builderNoResponse: "没有收到Builder的回复。允许弹出窗口并重试。",
335
337
  saveFailed: "保存失败({{status}})",
336
338
  savedRetrying: "已保存。正在重试转录...",
@@ -562,7 +564,7 @@ Clips 中所有面向用户的重要更改都会记录在这里。你可以随
562
564
  "Builder.io 是 Clips 上传的主要存储路径。需要自带存储桶时,也可以使用 S3。",
563
565
  checkingBuilder: "正在检查 Builder.io",
564
566
  builderConnected: "Builder.io 已连接",
565
- connectBuilder: "连接 Builder.io",
567
+ connectBuilder: "使用 Builder.io(免费)",
566
568
  builderConnectedFor: "正在为 {{orgName}} 使用 Builder.io。",
567
569
  builderConnectedGeneric: "新的剪辑会使用已连接的 Builder.io 提供方。",
568
570
  builderIncludes: "包含对象存储、上传,以及新剪辑的托管转录。",
@@ -586,12 +588,11 @@ Clips 中所有面向用户的重要更改都会记录在这里。你可以随
586
588
  s3RegionLabel: "区域",
587
589
  s3PublicBaseUrlLabel: "公共基础 URL",
588
590
  apiSetup: "AI 设置",
589
- apiSetupDescription:
590
- "Builder.io 是托管 AI 额度的默认路径。也可以在这里添加可选的提供方密钥。",
591
- builderEasySetup: "Builder.io 是最简单的设置方式",
591
+ apiSetupDescription: "使用 Builder.io 免费额度或你自己的 LLM 密钥连接 AI。",
592
+ builderEasySetup: "Builder.io 免费额度",
592
593
  builderAiAvailable: "Clips 可使用包含的 AI 额度和托管转录。",
593
594
  builderAiDescription:
594
- "先连接 Builder,即可使用包含的 AI 额度、对象存储、上传和托管转录。",
595
+ "优先使用 Builder.io,可获得内置 AI 额度、对象存储、上传和托管转录。",
595
596
  providerKeyTitle: "使用自己的提供方密钥",
596
597
  providerKeyDescription:
597
598
  "添加 Anthropic、OpenAI、Gemini、Groq 或 OpenRouter 密钥,用于按提供方计费的使用。",
@@ -730,7 +731,8 @@ Clips 中所有面向用户的重要更改都会记录在这里。你可以随
730
731
  title: "拼接录制",
731
732
  pickAtLeastTwo: "请选择至少 2 个录制进行拼接",
732
733
  videoUrlMissing: "一个或多个录制还没有可用的视频 URL",
733
- connectStorage: "拼接录制前请连接 Builder.io 或 S3 兼容存储。",
734
+ connectStorage:
735
+ "拼接录制前请连接存储:Builder.io(免费套餐存储 + AI)或 S3 兼容存储。",
734
736
  created: "拼接录制已创建",
735
737
  failed: "拼接录制失败",
736
738
  noOtherRecordings: "没有其他可用录制。",
@@ -954,9 +956,12 @@ Clips 中所有面向用户的重要更改都会记录在这里。你可以随
954
956
  builderTimeout: "5 分钟内未收到 Builder 响应。请检查弹出窗口并重试。",
955
957
  builderConnected: "Builder.io 已连接",
956
958
  waitingForBuilder: "正在等待 Builder...",
957
- connectBuilder: "连接 Builder.io",
959
+ connectBuilder: "使用 Builder.io(免费)",
958
960
  free: "免费",
959
961
  configureS3: "配置 S3 兼容存储",
962
+ whyPrompt: "为什么会看到这个?",
963
+ whyDescription:
964
+ "Clips 是 100% 免费且开源的应用,所以你需要连接一种方式来存储剪辑。使用 Builder.io 可获得免费套餐存储和 AI,或使用 S3 兼容对象存储和你自己的 LLM 密钥。",
960
965
  },
961
966
  captureInstall: {
962
967
  title: "Choose your recorder (已本地化)",
@@ -1159,9 +1164,9 @@ Clips 中所有面向用户的重要更改都会记录在这里。你可以随
1159
1164
  downloadRecording: "Download recording (已本地化)",
1160
1165
  openRecorderInTab: "Open recorder in tab (已本地化)",
1161
1166
  connectStorageToFinish:
1162
- "Connect Builder.io or S3 storage on the next screen and Clips will finish saving it. (已本地化)",
1167
+ "在下一屏连接存储:Builder.io(免费套餐存储 + AI)或 S3 兼容存储。Clips 将完成保存。",
1163
1168
  connectStorageToRetryLoom:
1164
- "Connect Builder.io or S3 storage on the next screen and Clips will retry the import. (已本地化)",
1169
+ "在下一屏连接存储:Builder.io(免费套餐存储 + AI)或 S3 兼容存储。Clips 将重试导入。",
1165
1170
  },
1166
1171
  dictateRoute: {
1167
1172
  pageTitle: "Dictate · Clips (已本地化)",
@@ -146,7 +146,7 @@ const messages = {
146
146
  savingWentWrong: "儲存此剪輯時出現問題。",
147
147
  finishingClip: "正在完成您的剪輯...",
148
148
  loomSourcePreserved:
149
- "Loom 來源連結被保留。連線 Builder.io S3 存儲,Clips 將重試儲存其自己的副本。",
149
+ "Loom 來源連結被保留。連接儲存:Builder.io(免費方案儲存 + AI)或 S3 相容儲存,Clips 將重試儲存自己的副本。",
150
150
  clipDataPreserved:
151
151
  "Your clip data is still preserved.連線Builder.io或S3存儲,Clips將自動上傳。",
152
152
  details: "細節",
@@ -223,12 +223,13 @@ const messages = {
223
223
  savingWentWrong: "儲存此剪輯時出現問題。",
224
224
  finishingClip: "完成這個剪輯...",
225
225
  loomPreservedManage:
226
- "Loom 來源連結被保留。連線 Builder.io S3 存儲,然後重試匯入。",
226
+ "Loom 來源連結被保留。連接儲存:Builder.io(免費方案儲存 + AI)或 S3 相容儲存,然後重試匯入。",
227
227
  videoPreservedManage:
228
228
  "影片被保留。連線Builder.io或S3存儲,Clips將完成上傳。",
229
- creatorNeedsStorage: "建立者需要連線 Builder.io 或 S3 存儲才能完成此剪輯。",
229
+ creatorNeedsStorage:
230
+ "建立者需要先連接儲存才能完成此剪輯:Builder.io(免費方案儲存 + AI)或 S3 相容儲存。",
230
231
  signInStorage:
231
- "如果這是您的剪輯,請在此處登入以連線 Builder.io S3 存儲並完成上傳。",
232
+ "如果這是您的剪輯,請在此處登入以連接儲存:Builder.io(免費方案儲存 + AI)或 S3 相容儲存,並完成上傳。",
232
233
  uploadNotCompleteSession:
233
234
  "上傳尚未完成。開啟此剪輯的儀表板或要求建立者檢查存儲空間。",
234
235
  uploadNotCompleteSignIn:
@@ -330,7 +331,8 @@ const messages = {
330
331
  cleanupKept: "清理工作無法完成。保留了本機轉錄本。",
331
332
  cleanupBuilderFailed:
332
333
  "即使 Builder.io 已連線,清理也無法完成。保留了本機轉錄本。",
333
- cleanupPaused: "清理已暫停。在“設定”中連線 Builder.io 即可啟用它。",
334
+ cleanupPaused:
335
+ "清理已暫停。請在「設定」中連接 AI:Builder.io(免費額度)或你自己的 LLM 金鑰。",
334
336
  builderNoResponse: "沒有收到Builder的回複。允許快顯窗口並重試。",
335
337
  saveFailed: "儲存失敗({{status}})",
336
338
  savedRetrying: "已儲存。正在重試轉錄...",
@@ -555,7 +557,7 @@ const messages = {
555
557
  "Builder.io 是 Clips 上傳的主要存儲路徑。需要自帶存儲桶時,也可以使用 S3。",
556
558
  checkingBuilder: "正在檢查 Builder.io",
557
559
  builderConnected: "Builder.io 已連線",
558
- connectBuilder: "連線 Builder.io",
560
+ connectBuilder: "使用 Builder.io(免費)",
559
561
  builderConnectedFor: "正在為 {{orgName}} 使用 Builder.io。",
560
562
  builderConnectedGeneric: "新的剪輯會使用已連線的 Builder.io 提供方。",
561
563
  builderIncludes: "包含物件存儲、上傳,以及新剪輯的託管轉錄。",
@@ -579,12 +581,11 @@ const messages = {
579
581
  s3RegionLabel: "區域",
580
582
  s3PublicBaseUrlLabel: "公開基礎 URL",
581
583
  apiSetup: "AI 設定",
582
- apiSetupDescription:
583
- "Builder.io 是託管 AI 額度的預設路徑。也可以在這裡新增可選的提供方金鑰。",
584
- builderEasySetup: "Builder.io 是最簡單的設定方式",
584
+ apiSetupDescription: "使用 Builder.io 免費額度或你自己的 LLM 金鑰連接 AI。",
585
+ builderEasySetup: "Builder.io 免費額度",
585
586
  builderAiAvailable: "Clips 可使用包含的 AI 額度和託管轉錄。",
586
587
  builderAiDescription:
587
- "先連線 Builder,即可使用包含的 AI 額度、物件存儲、上傳和託管轉錄。",
588
+ "優先使用 Builder.io,可獲得內建 AI 額度、物件儲存、上傳和託管轉錄。",
588
589
  providerKeyTitle: "使用自己的提供方金鑰",
589
590
  providerKeyDescription:
590
591
  "新增 Anthropic、OpenAI、Gemini、Groq 或 OpenRouter 金鑰,用於按提供方計費的使用。",
@@ -723,7 +724,8 @@ const messages = {
723
724
  title: "拼接錄製",
724
725
  pickAtLeastTwo: "請選取至少 2 個錄製進行拼接",
725
726
  videoUrlMissing: "一個或多個錄製還沒有可用的影片 URL",
726
- connectStorage: "拼接錄製前請連線 Builder.io 或 S3 兼容存儲。",
727
+ connectStorage:
728
+ "拼接錄製前請連接儲存:Builder.io(免費方案儲存 + AI)或 S3 相容儲存。",
727
729
  created: "拼接錄製已建立",
728
730
  failed: "拼接錄製失敗",
729
731
  noOtherRecordings: "沒有其他可用錄製。",
@@ -947,9 +949,12 @@ const messages = {
947
949
  builderTimeout: "5 分鐘內未收到 Builder 回應。請檢查快顯窗口並重試。",
948
950
  builderConnected: "Builder.io 已連線",
949
951
  waitingForBuilder: "正在等待 Builder...",
950
- connectBuilder: "連線 Builder.io",
952
+ connectBuilder: "使用 Builder.io(免費)",
951
953
  free: "免費",
952
954
  configureS3: "設定 S3 兼容存儲",
955
+ whyPrompt: "為什麼我會看到這個?",
956
+ whyDescription:
957
+ "Clips 是 100% 免費且開源的,因此你需要連接一種儲存 clips 的方式。使用 Builder.io 可取得免費方案儲存和 AI,或使用 S3 相容物件儲存和你自己的 LLM 金鑰。",
953
958
  },
954
959
  captureInstall: {
955
960
  title: "Choose your recorder (已本機化)",
@@ -1152,9 +1157,9 @@ const messages = {
1152
1157
  downloadRecording: "Download recording (已本機化)",
1153
1158
  openRecorderInTab: "Open recorder in tab (已本機化)",
1154
1159
  connectStorageToFinish:
1155
- "Connect Builder.io or S3 storage on the next screen and Clips will finish saving it. (已本機化)",
1160
+ "在下一屏連接儲存:Builder.io(免費方案儲存 + AI)或 S3 相容儲存。Clips 將完成儲存。",
1156
1161
  connectStorageToRetryLoom:
1157
- "Connect Builder.io or S3 storage on the next screen and Clips will retry the import. (已本機化)",
1162
+ "在下一屏連接儲存:Builder.io(免費方案儲存 + AI)或 S3 相容儲存。Clips 將重試匯入。",
1158
1163
  },
1159
1164
  dictateRoute: {
1160
1165
  pageTitle: "Dictate · Clips (已本機化)",
@@ -114,6 +114,13 @@ function failureDetail(reason: string | null | undefined): string | null {
114
114
 
115
115
  function nativeSaveFailureMessage(reason: string | null | undefined): string {
116
116
  const text = reason ?? "";
117
+ if (
118
+ /finalization callback failed|missing required metadata|missing playback metadata|corrupted or incomplete|missing moov/i.test(
119
+ text,
120
+ )
121
+ ) {
122
+ return "macOS could not finish writing this desktop recording. The local file is incomplete, so discard it from the Clips menu and record again.";
123
+ }
117
124
  if (/too large|compression/i.test(text)) {
118
125
  return "Clips tried to compress this desktop recording, but it is still too large to upload. The original is saved locally and can be retried from the Clips menu.";
119
126
  }
@@ -1119,7 +1119,7 @@ export default function RecordRoute() {
1119
1119
  markStorageConfigured(status);
1120
1120
  if (!status.configured) {
1121
1121
  throw new Error(
1122
- "No video storage configured. Open Settings to connect Builder.io or S3-compatible storage.",
1122
+ "No video storage configured. Connect storage: Builder.io (free tier storage + AI) or S3-compatible storage.",
1123
1123
  );
1124
1124
  }
1125
1125
 
@@ -1329,7 +1329,7 @@ export default function RecordRoute() {
1329
1329
  markStorageConfigured(status);
1330
1330
  if (!status.configured) {
1331
1331
  throw new Error(
1332
- "No video storage configured. Open Settings to connect Builder.io or S3-compatible storage.",
1332
+ "No video storage configured. Connect storage: Builder.io (free tier storage + AI) or S3-compatible storage.",
1333
1333
  );
1334
1334
  }
1335
1335
 
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-06-26
4
+ ---
5
+
6
+ Desktop recordings recover more reliably when macOS reports a finalization error.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-06-26
4
+ ---
5
+
6
+ Slack unfurls now recognize recording dashboard links pasted from Clips.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-06-26
4
+ ---
5
+
6
+ Storage setup now explains why Clips needs connected storage and guides saved desktop or Chrome uploads back to Builder.io.
@@ -2,7 +2,7 @@
2
2
  "manifest_version": 3,
3
3
  "name": "Agent-Native Clips",
4
4
  "short_name": "Clips",
5
- "version": "0.1.4",
5
+ "version": "0.1.5",
6
6
  "description": "Start Clips recordings from Chrome, capture optional diagnostics, and preview Clips links on GitHub.",
7
7
  "minimum_chrome_version": "116",
8
8
  "action": {
@@ -8,6 +8,10 @@ const MAX_CONSOLE_LOGS = 400;
8
8
  const MAX_NETWORK_REQUESTS = 400;
9
9
  const MAX_MESSAGE_LENGTH = 2_000;
10
10
  const MAX_URL_LENGTH = 1_000;
11
+ const STORAGE_SETUP_REQUIRED_MESSAGE =
12
+ "Connect storage to finish saving this clip: Builder.io (free tier storage + AI) or S3-compatible storage.";
13
+ const STORAGE_SETUP_FAILURE_RE =
14
+ /video storage is not connected|no video storage configured|file upload provider|storage provider|connect builder|s3-compatible/i;
11
15
  const SECRET_KEY_FRAGMENT =
12
16
  "(?:authorization|cookie|set[-_]?cookie|token|secret|password|passwd|pwd|api[-_]?key|apikey|session|credential)";
13
17
  const AUTHORIZATION_SCHEME_RE =
@@ -151,6 +155,7 @@ type OffscreenStatusMessage = {
151
155
  recordingId?: string;
152
156
  result?: Record<string, unknown>;
153
157
  error?: string;
158
+ storageSetupRequired?: boolean;
154
159
  };
155
160
 
156
161
  type ExtensionErrorMessage = {
@@ -162,6 +167,16 @@ type ExtensionErrorMessage = {
162
167
  context?: Record<string, unknown>;
163
168
  };
164
169
 
170
+ function isStorageSetupFailureMessage(message: string | null | undefined) {
171
+ return STORAGE_SETUP_FAILURE_RE.test(message ?? "");
172
+ }
173
+
174
+ function friendlyRecordingError(message: string | null | undefined): string {
175
+ return isStorageSetupFailureMessage(message)
176
+ ? STORAGE_SETUP_REQUIRED_MESSAGE
177
+ : message || "Could not stop recording.";
178
+ }
179
+
165
180
  type PendingNetworkRequest = {
166
181
  requestId: string;
167
182
  timestampMs: number;
@@ -1369,8 +1384,9 @@ async function stopRecording() {
1369
1384
  };
1370
1385
  } catch (err) {
1371
1386
  recording.status = "error";
1372
- recording.error =
1373
- err instanceof Error ? err.message : "Could not stop recording.";
1387
+ recording.error = friendlyRecordingError(
1388
+ err instanceof Error ? err.message : null,
1389
+ );
1374
1390
  resetOverlay();
1375
1391
  await broadcastUnmount();
1376
1392
  broadcastOverlayState();
@@ -2032,7 +2048,12 @@ async function dispatchRuntimeMessage(message: unknown): Promise<unknown> {
2032
2048
  ) {
2033
2049
  activeNativeRecording.status = status.status;
2034
2050
  activeNativeRecording.error =
2035
- typeof status.error === "string" ? status.error : null;
2051
+ typeof status.error === "string"
2052
+ ? status.storageSetupRequired === true ||
2053
+ isStorageSetupFailureMessage(status.error)
2054
+ ? STORAGE_SETUP_REQUIRED_MESSAGE
2055
+ : status.error
2056
+ : null;
2036
2057
  if (typeof status.recordingId === "string") {
2037
2058
  activeNativeRecording.recordingId = status.recordingId;
2038
2059
  activeNativeRecording.recordingUrl = recordingUrl(
@@ -23,6 +23,15 @@ import { captureExtensionError, initExtensionSentry } from "./sentry";
23
23
 
24
24
  initExtensionSentry("offscreen");
25
25
 
26
+ const STORAGE_SETUP_REQUIRED_MESSAGE =
27
+ "Connect storage to finish saving this clip: Builder.io (free tier storage + AI) or S3-compatible storage.";
28
+ const STORAGE_SETUP_FAILURE_RE =
29
+ /video storage is not connected|no video storage configured|file upload provider|storage provider|connect builder|s3-compatible/i;
30
+
31
+ function isStorageSetupFailureMessage(message: string | null | undefined) {
32
+ return STORAGE_SETUP_FAILURE_RE.test(message ?? "");
33
+ }
34
+
26
35
  type CaptureMode = "screen" | "camera";
27
36
 
28
37
  type AcquireMessage = {
@@ -396,7 +405,14 @@ async function uploadChunk(
396
405
  body,
397
406
  });
398
407
  const text = await res.text().catch(() => "");
399
- const data = text ? (JSON.parse(text) as UploadResult) : {};
408
+ let data: UploadResult = {};
409
+ if (text) {
410
+ try {
411
+ data = JSON.parse(text) as UploadResult;
412
+ } catch {
413
+ data = {};
414
+ }
415
+ }
400
416
  if (!res.ok) {
401
417
  console.error(
402
418
  "[clips-offscreen] chunk upload failed",
@@ -405,9 +421,17 @@ async function uploadChunk(
405
421
  Boolean(recording.authToken),
406
422
  text.slice(0, 200),
407
423
  );
424
+ const storageSetupRequired =
425
+ data?.storageSetupRequired === true ||
426
+ isStorageSetupFailureMessage(data?.error || text);
408
427
  const error = new Error(
409
- data?.error || `Upload failed (${res.status}): ${text || res.statusText}`,
428
+ storageSetupRequired
429
+ ? STORAGE_SETUP_REQUIRED_MESSAGE
430
+ : data?.error ||
431
+ `Upload failed (${res.status}): ${text || res.statusText}`,
410
432
  );
433
+ (error as { storageSetupRequired?: boolean }).storageSetupRequired =
434
+ storageSetupRequired;
411
435
  captureExtensionError(error, {
412
436
  tags: {
413
437
  surface: "offscreen",
@@ -845,6 +869,8 @@ async function finalizeStop(recording: ActiveRecording): Promise<void> {
845
869
  reportStatus(recording.sessionId, "error", {
846
870
  recordingId: recording.recordingId,
847
871
  error: error.message,
872
+ storageSetupRequired: (error as { storageSetupRequired?: boolean })
873
+ .storageSetupRequired,
848
874
  });
849
875
  recording.rejectStopped(error);
850
876
  }
@@ -405,6 +405,22 @@
405
405
 
406
406
  <span id="status" class="status" role="status" aria-live="polite"></span>
407
407
 
408
+ <div id="storage-help" class="storage-help" hidden>
409
+ <button id="storage-help-open" class="storage-help-open" type="button">
410
+ Use Builder.io (free)
411
+ </button>
412
+ <span aria-hidden="true">·</span>
413
+ <span class="storage-help-why" tabindex="0">
414
+ Why am I seeing this?
415
+ <span class="storage-help-details" role="tooltip">
416
+ Clips is 100% free and open source, so you need to hook up a way to
417
+ store your clips. Connect storage with Builder.io for free-tier
418
+ storage and AI, or use S3-compatible object storage and your own LLM
419
+ keys.
420
+ </span>
421
+ </span>
422
+ </div>
423
+
408
424
  <nav class="bottom-row" aria-label="Clips shortcuts">
409
425
  <button id="open-library" class="bottom-button" type="button">
410
426
  <svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
@@ -93,9 +93,17 @@ const FEEDBACK_URL =
93
93
  const FEEDBACK_PLACEHOLDER = "Tell us what's on your mind...";
94
94
  const FEEDBACK_SUBMIT_TEXT = "Send feedback";
95
95
  const FEEDBACK_SUCCESS_MESSAGE = "Thanks for the feedback!";
96
+ const STORAGE_SETUP_REQUIRED_MESSAGE =
97
+ "Connect storage to finish saving this clip: Builder.io (free tier storage + AI) or S3-compatible storage.";
98
+ const STORAGE_SETUP_FAILURE_RE =
99
+ /video storage is not connected|no video storage configured|file upload provider|storage provider|connect builder|s3-compatible/i;
96
100
  const feedbackTarget = parseFeedbackTarget(FEEDBACK_URL);
97
101
  const feedbackSchemaCache = new Map<string, Promise<FeedbackFormSchema>>();
98
102
 
103
+ function isStorageSetupFailureMessage(message: string | null | undefined) {
104
+ return STORAGE_SETUP_FAILURE_RE.test(message ?? "");
105
+ }
106
+
99
107
  function screenSurface(
100
108
  value: CaptureSurface,
101
109
  ): Exclude<CaptureSurface, "camera"> {
@@ -507,6 +515,14 @@ function setStatus(message: string, kind: "info" | "error" = "info"): void {
507
515
  status.dataset.kind = kind;
508
516
  }
509
517
 
518
+ function setStorageHelp(visible: boolean): void {
519
+ byId<HTMLDivElement>("storage-help").hidden = !visible;
520
+ }
521
+
522
+ function storageSetupUrl(settings: ExtensionSettings): string {
523
+ return `${settings.clipsBaseUrl.replace(/\/+$/, "")}/record`;
524
+ }
525
+
510
526
  function renderMode(settings: ExtensionSettings): void {
511
527
  const mode = recordingMode(settings);
512
528
  for (const button of document.querySelectorAll<HTMLButtonElement>(
@@ -690,7 +706,10 @@ function renderActiveRecording(recording: NativeRecording | null): void {
690
706
  start.hidden = active;
691
707
  signIn.hidden = true;
692
708
  if (recordingActions) recordingActions.hidden = !active;
693
- if (!recording) return;
709
+ if (!recording) {
710
+ setStorageHelp(false);
711
+ return;
712
+ }
694
713
 
695
714
  recordingTitle.textContent = recording.targetTitle || "Current recording";
696
715
  const host = hostnameLabel(recording.targetUrl);
@@ -706,10 +725,16 @@ function renderActiveRecording(recording: NativeRecording | null): void {
706
725
  : recording.status === "stopping"
707
726
  ? "Stopping..."
708
727
  : recording.status === "error"
709
- ? recording.error || "Recording needs attention"
728
+ ? isStorageSetupFailureMessage(recording.error)
729
+ ? STORAGE_SETUP_REQUIRED_MESSAGE
730
+ : recording.error || "Recording needs attention"
710
731
  : `Recording ${formatDuration(recording.startedAtMs)}`;
711
732
  recordingStatus.dataset.kind =
712
733
  recording.status === "error" ? "error" : "info";
734
+ setStorageHelp(
735
+ recording.status === "error" &&
736
+ isStorageSetupFailureMessage(recording.error),
737
+ );
713
738
  }
714
739
 
715
740
  async function init(): Promise<void> {
@@ -751,6 +776,7 @@ async function init(): Promise<void> {
751
776
  const openSettings = byId<HTMLButtonElement>("open-settings");
752
777
  const openRecent = byId<HTMLButtonElement>("open-recent");
753
778
  const signIn = byId<HTMLButtonElement>("sign-in");
779
+ const storageHelpOpen = byId<HTMLButtonElement>("storage-help-open");
754
780
  let activeRecording: NativeRecording | null = null;
755
781
  let authStatus: AuthStatus = "checking";
756
782
  let feedbackOpenedAt = 0;
@@ -1071,6 +1097,7 @@ async function init(): Promise<void> {
1071
1097
  start.addEventListener("click", async () => {
1072
1098
  start.disabled = true;
1073
1099
  signIn.hidden = true;
1100
+ setStorageHelp(false);
1074
1101
  setStatus(""); // no chatty "Checking…/Starting…" text — the disabled button is enough
1075
1102
  try {
1076
1103
  authStatus = await readAuthStatus(settings);
@@ -1085,10 +1112,11 @@ async function init(): Promise<void> {
1085
1112
  if (!storageConfigured) {
1086
1113
  start.disabled = false;
1087
1114
  setStatus(
1088
- "Connect Builder.io or S3 storage in Clips, then start recording.",
1115
+ "Connect storage in Clips first: Builder.io (free tier storage + AI) or S3-compatible storage.",
1089
1116
  "error",
1090
1117
  );
1091
- await createTab(`${settings.clipsBaseUrl.replace(/\/+$/, "")}/record`);
1118
+ setStorageHelp(true);
1119
+ await createTab(storageSetupUrl(settings));
1092
1120
  window.close();
1093
1121
  return;
1094
1122
  }
@@ -1117,7 +1145,14 @@ async function init(): Promise<void> {
1117
1145
  setStatus("");
1118
1146
  return;
1119
1147
  }
1120
- setStatus(message, "error");
1148
+ const storageSetupFailure = isStorageSetupFailureMessage(message);
1149
+ setStorageHelp(storageSetupFailure);
1150
+ setStatus(
1151
+ storageSetupFailure
1152
+ ? "Connect storage in Clips first: Builder.io (free tier storage + AI) or S3-compatible storage."
1153
+ : message,
1154
+ "error",
1155
+ );
1121
1156
  } catch (err) {
1122
1157
  captureExtensionError(err, {
1123
1158
  tags: { surface: "popup", action: "start-recording" },
@@ -1128,13 +1163,24 @@ async function init(): Promise<void> {
1128
1163
  },
1129
1164
  });
1130
1165
  start.disabled = false;
1166
+ const message =
1167
+ err instanceof Error ? err.message : "Could not start Clips.";
1168
+ const storageSetupFailure = isStorageSetupFailureMessage(message);
1169
+ setStorageHelp(storageSetupFailure);
1131
1170
  setStatus(
1132
- err instanceof Error ? err.message : "Could not start Clips.",
1171
+ storageSetupFailure
1172
+ ? "Connect storage in Clips first: Builder.io (free tier storage + AI) or S3-compatible storage."
1173
+ : message,
1133
1174
  "error",
1134
1175
  );
1135
1176
  }
1136
1177
  });
1137
1178
 
1179
+ storageHelpOpen.addEventListener("click", async () => {
1180
+ await createTab(storageSetupUrl(settings));
1181
+ window.close();
1182
+ });
1183
+
1138
1184
  signIn.addEventListener("click", async () => {
1139
1185
  signIn.disabled = true;
1140
1186
  const response = await sendRuntimeMessage<PopupStartResponse>({
@@ -1161,7 +1207,13 @@ async function init(): Promise<void> {
1161
1207
  }
1162
1208
  stop.disabled = false;
1163
1209
  discard.disabled = false;
1164
- setStatus(response.error || "Could not stop recording.", "error");
1210
+ const message = response.error || "Could not stop recording.";
1211
+ const storageSetupFailure = isStorageSetupFailureMessage(message);
1212
+ setStorageHelp(storageSetupFailure);
1213
+ setStatus(
1214
+ storageSetupFailure ? STORAGE_SETUP_REQUIRED_MESSAGE : message,
1215
+ "error",
1216
+ );
1165
1217
  });
1166
1218
 
1167
1219
  discard.addEventListener("click", async () => {
@@ -67,6 +67,31 @@ function scrubEvent<T extends Sentry.Event>(event: T): T {
67
67
  return event;
68
68
  }
69
69
 
70
+ function shouldDropExpectedMediaPermissionError(
71
+ error: unknown,
72
+ context: CaptureContext,
73
+ ): boolean {
74
+ const tags = context.tags ?? {};
75
+ const isPermissionProbe =
76
+ tags.surface === "permission" &&
77
+ (tags.permission === "mic" || tags.permission === "cam");
78
+ const isOptionalBubbleCamera =
79
+ tags.surface === "overlay" && tags.overlayPart === "bubble";
80
+ if (!isPermissionProbe && !isOptionalBubbleCamera) return false;
81
+
82
+ const text =
83
+ error instanceof Error
84
+ ? `${error.name}: ${error.message}`
85
+ : String(error ?? "");
86
+
87
+ return (
88
+ /\bNotAllowedError\b/i.test(text) ||
89
+ /\bPermission (?:denied|dismissed)\b/i.test(text) ||
90
+ /\b(?:NotFoundError|DevicesNotFoundError)\b/i.test(text) ||
91
+ /\bRequested device not found\b/i.test(text)
92
+ );
93
+ }
94
+
70
95
  function scrubScopeString(value: string): string {
71
96
  const scrubbed = scrubUrl(value, { redactAllQueryValues: true });
72
97
  if (scrubbed !== value) return scrubbed;
@@ -175,7 +200,9 @@ export function captureExtensionError(
175
200
  error: unknown,
176
201
  context: CaptureContext = {},
177
202
  ): string | undefined {
178
- if (!initialized) return undefined;
203
+ if (!initialized || shouldDropExpectedMediaPermissionError(error, context)) {
204
+ return undefined;
205
+ }
179
206
  try {
180
207
  return Sentry.withScope((scope) => {
181
208
  if (context.tags) {