@agent-native/core 0.70.3 → 0.72.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 (221) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +51 -0
  3. package/corpus/core/docs/content/actions.md +23 -0
  4. package/corpus/core/docs/content/audit-log.md +111 -0
  5. package/corpus/core/docs/content/multi-app-workspace.md +2 -2
  6. package/corpus/core/docs/design/durable-agent-runs.md +458 -4
  7. package/corpus/core/package.json +2 -1
  8. package/corpus/core/src/action.ts +80 -1
  9. package/corpus/core/src/agent/durable-background.ts +192 -0
  10. package/corpus/core/src/agent/production-agent.ts +357 -13
  11. package/corpus/core/src/agent/run-manager.ts +66 -3
  12. package/corpus/core/src/agent/run-store.ts +143 -23
  13. package/corpus/core/src/agent/types.ts +20 -0
  14. package/corpus/core/src/application-state/store.ts +5 -0
  15. package/corpus/core/src/audit/actions/get-audit-event.ts +23 -0
  16. package/corpus/core/src/audit/actions/list-audit-events.ts +65 -0
  17. package/corpus/core/src/audit/cleanup-job.ts +100 -0
  18. package/corpus/core/src/audit/config.ts +91 -0
  19. package/corpus/core/src/audit/index.ts +43 -0
  20. package/corpus/core/src/audit/record.ts +143 -0
  21. package/corpus/core/src/audit/redact.ts +109 -0
  22. package/corpus/core/src/audit/store.ts +244 -0
  23. package/corpus/core/src/audit/types.ts +125 -0
  24. package/corpus/core/src/chat-threads/store.ts +10 -0
  25. package/corpus/core/src/client/AgentPanel.tsx +12 -9
  26. package/corpus/core/src/client/AssistantChat.tsx +6 -0
  27. package/corpus/core/src/client/blocks/library/FileTreeBlock.tsx +72 -14
  28. package/corpus/core/src/client/chat/repo-helpers.ts +38 -0
  29. package/corpus/core/src/db/client.ts +4 -0
  30. package/corpus/core/src/db/widen-columns.ts +75 -0
  31. package/corpus/core/src/deploy/build.ts +96 -0
  32. package/corpus/core/src/deploy/workspace-deploy.ts +121 -0
  33. package/corpus/core/src/mcp/build-server.ts +23 -13
  34. package/corpus/core/src/oauth-tokens/store.ts +6 -0
  35. package/corpus/core/src/provider-api/custom-registry.ts +8 -0
  36. package/corpus/core/src/resources/store.ts +10 -0
  37. package/corpus/core/src/scripts/runner.ts +6 -3
  38. package/corpus/core/src/server/action-discovery.ts +6 -0
  39. package/corpus/core/src/server/action-routes.ts +1 -0
  40. package/corpus/core/src/server/agent-chat-plugin.ts +133 -66
  41. package/corpus/core/src/server/auth.ts +5 -0
  42. package/corpus/core/src/server/core-routes-plugin.ts +14 -0
  43. package/corpus/core/src/server/security-headers.ts +9 -6
  44. package/corpus/core/src/settings/store.ts +6 -0
  45. package/corpus/core/src/templates/workspace-core/.agents/skills/audit-log/SKILL.md +93 -0
  46. package/corpus/core/src/usage/store.ts +6 -0
  47. package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +19 -0
  48. package/corpus/templates/analytics/AGENTS.md +8 -0
  49. package/corpus/templates/analytics/actions/compose-dashboard.ts +317 -0
  50. package/corpus/templates/analytics/changelog/2026-06-23-build-large-first-party-analytics-dashboards-in-one-fast-cal.md +6 -0
  51. package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +574 -0
  52. package/corpus/templates/calendar/app/components/calendar/GoogleConnectBanner.tsx +60 -6
  53. package/corpus/templates/calendar/changelog/2026-06-23-added-a-heads-up-explaining-google-s-app-not-verified-screen.md +6 -0
  54. package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +20 -43
  55. package/corpus/templates/clips/app/components/recorder/recording-toolbar.tsx +5 -1
  56. package/corpus/templates/clips/app/lib/countdown-audio-cue.ts +3 -51
  57. package/corpus/templates/clips/changelog/2026-06-23-dragging-the-desktop-camera-bubble-now-glides-to-a-stop-at-t.md +6 -0
  58. package/corpus/templates/clips/changelog/2026-06-23-public-clips-now-play-inline-in-slack-connect-a-workspac.md +6 -0
  59. package/corpus/templates/clips/chrome-extension/PERMISSIONS.md +72 -0
  60. package/corpus/templates/clips/chrome-extension/public/manifest.json +1 -10
  61. package/corpus/templates/clips/chrome-extension/src/background.ts +163 -32
  62. package/corpus/templates/clips/chrome-extension/src/content-script.ts +301 -24
  63. package/corpus/templates/clips/chrome-extension/src/offscreen.ts +245 -67
  64. package/corpus/templates/clips/chrome-extension/src/overlay.css +77 -43
  65. package/corpus/templates/clips/chrome-extension/src/overlay.html +7 -1
  66. package/corpus/templates/clips/chrome-extension/src/overlay.ts +102 -38
  67. package/corpus/templates/clips/chrome-extension/src/popup.html +62 -5
  68. package/corpus/templates/clips/chrome-extension/src/popup.ts +275 -4
  69. package/corpus/templates/clips/chrome-extension/src/styles.css +34 -0
  70. package/corpus/templates/clips/chrome-extension/vite.config.ts +5 -0
  71. package/corpus/templates/clips/desktop/src/lib/audio-cue.ts +21 -16
  72. package/corpus/templates/clips/desktop/src/lib/recorder.ts +10 -17
  73. package/corpus/templates/clips/desktop/src/overlays/bubble.tsx +73 -23
  74. package/corpus/templates/clips/desktop/src/overlays/countdown.tsx +0 -6
  75. package/corpus/templates/clips/desktop/src/overlays/toolbar.tsx +56 -46
  76. package/corpus/templates/clips/desktop/src/styles.css +33 -5
  77. package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +113 -0
  78. package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +3 -0
  79. package/corpus/templates/clips/server/lib/media-permissions.ts +5 -1
  80. package/corpus/templates/clips/shared/recording-audio.ts +62 -0
  81. package/corpus/templates/clips/shared/recording-core.ts +94 -0
  82. package/dist/action.d.ts +31 -0
  83. package/dist/action.d.ts.map +1 -1
  84. package/dist/action.js +45 -1
  85. package/dist/action.js.map +1 -1
  86. package/dist/agent/context-xray/schema.d.ts +1 -1
  87. package/dist/agent/durable-background.d.ts +60 -0
  88. package/dist/agent/durable-background.d.ts.map +1 -0
  89. package/dist/agent/durable-background.js +144 -0
  90. package/dist/agent/durable-background.js.map +1 -0
  91. package/dist/agent/observational-memory/schema.d.ts +1 -1
  92. package/dist/agent/production-agent.d.ts +20 -0
  93. package/dist/agent/production-agent.d.ts.map +1 -1
  94. package/dist/agent/production-agent.js +320 -14
  95. package/dist/agent/production-agent.js.map +1 -1
  96. package/dist/agent/run-manager.d.ts +48 -0
  97. package/dist/agent/run-manager.d.ts.map +1 -1
  98. package/dist/agent/run-manager.js +45 -3
  99. package/dist/agent/run-manager.js.map +1 -1
  100. package/dist/agent/run-store.d.ts +30 -1
  101. package/dist/agent/run-store.d.ts.map +1 -1
  102. package/dist/agent/run-store.js +138 -24
  103. package/dist/agent/run-store.js.map +1 -1
  104. package/dist/agent/types.d.ts +20 -0
  105. package/dist/agent/types.d.ts.map +1 -1
  106. package/dist/agent/types.js.map +1 -1
  107. package/dist/application-state/store.d.ts.map +1 -1
  108. package/dist/application-state/store.js +5 -0
  109. package/dist/application-state/store.js.map +1 -1
  110. package/dist/audit/actions/get-audit-event.d.ts +11 -0
  111. package/dist/audit/actions/get-audit-event.d.ts.map +1 -0
  112. package/dist/audit/actions/get-audit-event.js +22 -0
  113. package/dist/audit/actions/get-audit-event.js.map +1 -0
  114. package/dist/audit/actions/list-audit-events.d.ts +22 -0
  115. package/dist/audit/actions/list-audit-events.d.ts.map +1 -0
  116. package/dist/audit/actions/list-audit-events.js +61 -0
  117. package/dist/audit/actions/list-audit-events.js.map +1 -0
  118. package/dist/audit/cleanup-job.d.ts +12 -0
  119. package/dist/audit/cleanup-job.d.ts.map +1 -0
  120. package/dist/audit/cleanup-job.js +93 -0
  121. package/dist/audit/cleanup-job.js.map +1 -0
  122. package/dist/audit/config.d.ts +30 -0
  123. package/dist/audit/config.d.ts.map +1 -0
  124. package/dist/audit/config.js +65 -0
  125. package/dist/audit/config.js.map +1 -0
  126. package/dist/audit/index.d.ts +13 -0
  127. package/dist/audit/index.d.ts.map +1 -0
  128. package/dist/audit/index.js +6 -0
  129. package/dist/audit/index.js.map +1 -0
  130. package/dist/audit/record.d.ts +24 -0
  131. package/dist/audit/record.d.ts.map +1 -0
  132. package/dist/audit/record.js +106 -0
  133. package/dist/audit/record.js.map +1 -0
  134. package/dist/audit/redact.d.ts +28 -0
  135. package/dist/audit/redact.d.ts.map +1 -0
  136. package/dist/audit/redact.js +109 -0
  137. package/dist/audit/redact.js.map +1 -0
  138. package/dist/audit/store.d.ts +14 -0
  139. package/dist/audit/store.d.ts.map +1 -0
  140. package/dist/audit/store.js +219 -0
  141. package/dist/audit/store.js.map +1 -0
  142. package/dist/audit/types.d.ts +114 -0
  143. package/dist/audit/types.d.ts.map +1 -0
  144. package/dist/audit/types.js +15 -0
  145. package/dist/audit/types.js.map +1 -0
  146. package/dist/chat-threads/store.d.ts.map +1 -1
  147. package/dist/chat-threads/store.js +10 -0
  148. package/dist/chat-threads/store.js.map +1 -1
  149. package/dist/client/AgentPanel.d.ts.map +1 -1
  150. package/dist/client/AgentPanel.js +2 -2
  151. package/dist/client/AgentPanel.js.map +1 -1
  152. package/dist/client/AssistantChat.d.ts.map +1 -1
  153. package/dist/client/AssistantChat.js +6 -1
  154. package/dist/client/AssistantChat.js.map +1 -1
  155. package/dist/client/blocks/library/FileTreeBlock.d.ts.map +1 -1
  156. package/dist/client/blocks/library/FileTreeBlock.js +37 -4
  157. package/dist/client/blocks/library/FileTreeBlock.js.map +1 -1
  158. package/dist/client/chat/repo-helpers.d.ts +16 -0
  159. package/dist/client/chat/repo-helpers.d.ts.map +1 -1
  160. package/dist/client/chat/repo-helpers.js +40 -0
  161. package/dist/client/chat/repo-helpers.js.map +1 -1
  162. package/dist/db/client.d.ts.map +1 -1
  163. package/dist/db/client.js +3 -0
  164. package/dist/db/client.js.map +1 -1
  165. package/dist/db/widen-columns.d.ts +39 -0
  166. package/dist/db/widen-columns.d.ts.map +1 -0
  167. package/dist/db/widen-columns.js +73 -0
  168. package/dist/db/widen-columns.js.map +1 -0
  169. package/dist/deploy/build.d.ts +29 -0
  170. package/dist/deploy/build.d.ts.map +1 -1
  171. package/dist/deploy/build.js +85 -0
  172. package/dist/deploy/build.js.map +1 -1
  173. package/dist/deploy/workspace-deploy.d.ts.map +1 -1
  174. package/dist/deploy/workspace-deploy.js +108 -0
  175. package/dist/deploy/workspace-deploy.js.map +1 -1
  176. package/dist/mcp/build-server.d.ts.map +1 -1
  177. package/dist/mcp/build-server.js +24 -9
  178. package/dist/mcp/build-server.js.map +1 -1
  179. package/dist/oauth-tokens/store.d.ts.map +1 -1
  180. package/dist/oauth-tokens/store.js +6 -0
  181. package/dist/oauth-tokens/store.js.map +1 -1
  182. package/dist/provider-api/custom-registry.d.ts.map +1 -1
  183. package/dist/provider-api/custom-registry.js +8 -0
  184. package/dist/provider-api/custom-registry.js.map +1 -1
  185. package/dist/resources/store.d.ts.map +1 -1
  186. package/dist/resources/store.js +9 -0
  187. package/dist/resources/store.js.map +1 -1
  188. package/dist/scripts/runner.js +4 -3
  189. package/dist/scripts/runner.js.map +1 -1
  190. package/dist/server/action-discovery.d.ts.map +1 -1
  191. package/dist/server/action-discovery.js +6 -0
  192. package/dist/server/action-discovery.js.map +1 -1
  193. package/dist/server/action-routes.d.ts.map +1 -1
  194. package/dist/server/action-routes.js +1 -0
  195. package/dist/server/action-routes.js.map +1 -1
  196. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  197. package/dist/server/agent-chat-plugin.js +68 -13
  198. package/dist/server/agent-chat-plugin.js.map +1 -1
  199. package/dist/server/auth.d.ts.map +1 -1
  200. package/dist/server/auth.js +5 -0
  201. package/dist/server/auth.js.map +1 -1
  202. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  203. package/dist/server/core-routes-plugin.js +13 -0
  204. package/dist/server/core-routes-plugin.js.map +1 -1
  205. package/dist/server/security-headers.d.ts +8 -5
  206. package/dist/server/security-headers.d.ts.map +1 -1
  207. package/dist/server/security-headers.js +9 -6
  208. package/dist/server/security-headers.js.map +1 -1
  209. package/dist/settings/store.d.ts.map +1 -1
  210. package/dist/settings/store.js +6 -0
  211. package/dist/settings/store.js.map +1 -1
  212. package/dist/templates/workspace-core/.agents/skills/audit-log/SKILL.md +93 -0
  213. package/dist/usage/store.d.ts.map +1 -1
  214. package/dist/usage/store.js +5 -0
  215. package/dist/usage/store.js.map +1 -1
  216. package/docs/content/actions.md +23 -0
  217. package/docs/content/audit-log.md +111 -0
  218. package/docs/content/multi-app-workspace.md +2 -2
  219. package/docs/design/durable-agent-runs.md +458 -4
  220. package/package.json +2 -1
  221. package/src/templates/workspace-core/.agents/skills/audit-log/SKILL.md +93 -0
@@ -7,6 +7,13 @@ type ExtensionSettings = {
7
7
  includeCamera: boolean;
8
8
  includeMicrophone: boolean;
9
9
  includeDeveloperLogs: boolean;
10
+ videoDeviceId: string;
11
+ audioDeviceId: string;
12
+ };
13
+
14
+ type InputDevice = {
15
+ deviceId: string;
16
+ label: string;
10
17
  };
11
18
 
12
19
  type PopupStartResponse = {
@@ -67,6 +74,8 @@ const DEFAULT_SETTINGS: ExtensionSettings = {
67
74
  includeCamera: true,
68
75
  includeMicrophone: true,
69
76
  includeDeveloperLogs: true,
77
+ videoDeviceId: "",
78
+ audioDeviceId: "",
70
79
  };
71
80
 
72
81
  const SOURCE_LABELS: Record<Exclude<CaptureSurface, "camera">, string> = {
@@ -167,6 +176,37 @@ async function loadFeedbackSchema(
167
176
  return pending;
168
177
  }
169
178
 
179
+ // Enumerate the user's input devices for the camera/mic pickers. Labels only
180
+ // populate after camera/mic permission is granted (the extension's permission
181
+ // onboarding page handles that), so fall back to a generic label otherwise.
182
+ async function enumerateInputDevices(): Promise<{
183
+ cameras: InputDevice[];
184
+ microphones: InputDevice[];
185
+ }> {
186
+ try {
187
+ const devices = await navigator.mediaDevices.enumerateDevices();
188
+ const cameras: InputDevice[] = [];
189
+ const microphones: InputDevice[] = [];
190
+ for (const device of devices) {
191
+ if (!device.deviceId) continue;
192
+ if (device.kind === "videoinput") {
193
+ cameras.push({
194
+ deviceId: device.deviceId,
195
+ label: device.label.trim() || `Camera ${cameras.length + 1}`,
196
+ });
197
+ } else if (device.kind === "audioinput") {
198
+ microphones.push({
199
+ deviceId: device.deviceId,
200
+ label: device.label.trim() || `Microphone ${microphones.length + 1}`,
201
+ });
202
+ }
203
+ }
204
+ return { cameras, microphones };
205
+ } catch {
206
+ return { cameras: [], microphones: [] };
207
+ }
208
+ }
209
+
170
210
  function readSettings(): Promise<ExtensionSettings> {
171
211
  return new Promise((resolve) => {
172
212
  chrome.storage.sync.get(DEFAULT_SETTINGS, (value) => {
@@ -188,6 +228,14 @@ function readSettings(): Promise<ExtensionSettings> {
188
228
  typeof value.includeDeveloperLogs === "boolean"
189
229
  ? value.includeDeveloperLogs
190
230
  : DEFAULT_SETTINGS.includeDeveloperLogs,
231
+ videoDeviceId:
232
+ typeof value.videoDeviceId === "string"
233
+ ? value.videoDeviceId
234
+ : DEFAULT_SETTINGS.videoDeviceId,
235
+ audioDeviceId:
236
+ typeof value.audioDeviceId === "string"
237
+ ? value.audioDeviceId
238
+ : DEFAULT_SETTINGS.audioDeviceId,
191
239
  });
192
240
  });
193
241
  });
@@ -401,6 +449,28 @@ function comparableLabel(value: string): string {
401
449
  .replace(/[^a-z0-9]+/g, "");
402
450
  }
403
451
 
452
+ // Pages where Chrome forbids content-script / overlay injection, so the on-page
453
+ // countdown + controls can't render. Recording the screen still works; we just
454
+ // warn the user in the popup instead of letting it fail silently.
455
+ function isUnsupportedPage(url: string | undefined | null): boolean {
456
+ if (!url) return true;
457
+ const u = url.toLowerCase();
458
+ return (
459
+ u.startsWith("chrome://") ||
460
+ u.startsWith("chrome-extension://") ||
461
+ u.startsWith("edge://") ||
462
+ u.startsWith("brave://") ||
463
+ u.startsWith("arc://") ||
464
+ u.startsWith("about:") ||
465
+ u.startsWith("view-source:") ||
466
+ u.startsWith("devtools://") ||
467
+ u.startsWith("chrome-search://") ||
468
+ u.startsWith("chrome-untrusted://") ||
469
+ u.startsWith("https://chromewebstore.google.com") ||
470
+ u.startsWith("https://chrome.google.com/webstore")
471
+ );
472
+ }
473
+
404
474
  function targetCopy(tab: chrome.tabs.Tab | null): {
405
475
  title: string;
406
476
  subtitle: string;
@@ -464,9 +534,111 @@ function renderSource(settings: ExtensionSettings): void {
464
534
  }
465
535
  }
466
536
 
537
+ // Holds the most recent device enumeration so the menus and labels can render
538
+ // without re-querying on every paint. Refreshed by refreshDevices().
539
+ let inputDevices: { cameras: InputDevice[]; microphones: InputDevice[] } = {
540
+ cameras: [],
541
+ microphones: [],
542
+ };
543
+
544
+ function deviceLabel(
545
+ devices: InputDevice[],
546
+ deviceId: string,
547
+ fallback: string,
548
+ ): string {
549
+ if (!deviceId) return fallback;
550
+ const match = devices.find((device) => device.deviceId === deviceId);
551
+ return match ? match.label : fallback;
552
+ }
553
+
554
+ function renderDeviceMenu(
555
+ menu: HTMLDivElement,
556
+ devices: InputDevice[],
557
+ selectedId: string,
558
+ defaultLabel: string,
559
+ ): void {
560
+ menu.replaceChildren();
561
+ const options: InputDevice[] = [
562
+ { deviceId: "", label: defaultLabel },
563
+ ...devices,
564
+ ];
565
+ for (const option of options) {
566
+ const item = document.createElement("button");
567
+ item.type = "button";
568
+ item.className = "row-menu-item";
569
+ item.dataset.deviceId = option.deviceId;
570
+ const selected = option.deviceId === selectedId;
571
+ if (selected) {
572
+ item.classList.add("selected");
573
+ item.setAttribute("aria-checked", "true");
574
+ } else {
575
+ item.setAttribute("aria-checked", "false");
576
+ }
577
+
578
+ const check = document.createElement("span");
579
+ check.className = "row-menu-check";
580
+ check.setAttribute("aria-hidden", "true");
581
+ check.textContent = selected ? "✓" : "";
582
+
583
+ const label = document.createElement("span");
584
+ label.className = "row-menu-label";
585
+ label.textContent = option.label;
586
+
587
+ item.append(check, label);
588
+ menu.append(item);
589
+ }
590
+ }
591
+
592
+ function renderDevicePickers(settings: ExtensionSettings): void {
593
+ const cameraButton = byId<HTMLButtonElement>("camera-device-button");
594
+ const cameraLabel = byId<HTMLSpanElement>("camera-device-label");
595
+ const cameraMenu = byId<HTMLDivElement>("camera-device-menu");
596
+ const showCamera =
597
+ settings.captureSurface === "camera" || settings.includeCamera;
598
+ cameraButton.hidden = !showCamera;
599
+ if (showCamera) {
600
+ cameraLabel.textContent = deviceLabel(
601
+ inputDevices.cameras,
602
+ settings.videoDeviceId,
603
+ "Default camera",
604
+ );
605
+ renderDeviceMenu(
606
+ cameraMenu,
607
+ inputDevices.cameras,
608
+ settings.videoDeviceId,
609
+ "Default camera",
610
+ );
611
+ } else {
612
+ cameraMenu.hidden = true;
613
+ }
614
+
615
+ const micRow = byId<HTMLDivElement>("microphone-row");
616
+ const micButton = byId<HTMLButtonElement>("microphone-device-button");
617
+ const micLabel = byId<HTMLSpanElement>("microphone-device-label");
618
+ const micMenu = byId<HTMLDivElement>("microphone-device-menu");
619
+ micButton.hidden = !settings.includeMicrophone;
620
+ if (settings.includeMicrophone) {
621
+ micLabel.textContent = deviceLabel(
622
+ inputDevices.microphones,
623
+ settings.audioDeviceId,
624
+ "Default mic",
625
+ );
626
+ renderDeviceMenu(
627
+ micMenu,
628
+ inputDevices.microphones,
629
+ settings.audioDeviceId,
630
+ "Default mic",
631
+ );
632
+ } else {
633
+ micMenu.hidden = true;
634
+ }
635
+ micRow.hidden = false;
636
+ }
637
+
467
638
  function render(settings: ExtensionSettings): void {
468
639
  renderMode(settings);
469
640
  renderSource(settings);
641
+ renderDevicePickers(settings);
470
642
  const includeCamera = byId<HTMLButtonElement>("include-camera");
471
643
  const includeMicrophone = byId<HTMLButtonElement>("include-microphone");
472
644
  const cameraRow = byId<HTMLDivElement>("camera-row");
@@ -550,6 +722,12 @@ async function init(): Promise<void> {
550
722
  }
551
723
  const sourceButton = byId<HTMLButtonElement>("source-button");
552
724
  const sourceMenu = byId<HTMLDivElement>("source-menu");
725
+ const cameraDeviceButton = byId<HTMLButtonElement>("camera-device-button");
726
+ const cameraDeviceMenu = byId<HTMLDivElement>("camera-device-menu");
727
+ const microphoneDeviceButton = byId<HTMLButtonElement>(
728
+ "microphone-device-button",
729
+ );
730
+ const microphoneDeviceMenu = byId<HTMLDivElement>("microphone-device-menu");
553
731
  const includeCamera = byId<HTMLButtonElement>("include-camera");
554
732
  const includeMicrophone = byId<HTMLButtonElement>("include-microphone");
555
733
  const start = byId<HTMLButtonElement>("start");
@@ -624,8 +802,30 @@ async function init(): Promise<void> {
624
802
  window.setTimeout(() => feedbackTextarea.focus(), 30);
625
803
  };
626
804
 
627
- await queryActiveTab();
805
+ // Re-enumerate devices and repaint the pickers. Labels only appear once the
806
+ // user has granted camera/mic access (via the permission onboarding page), so
807
+ // this is also re-run when the device list changes.
808
+ const refreshDevices = async (): Promise<void> => {
809
+ inputDevices = await enumerateInputDevices();
810
+ renderDevicePickers(settings);
811
+ };
812
+
813
+ const closeDeviceMenus = (): void => {
814
+ cameraDeviceMenu.hidden = true;
815
+ microphoneDeviceMenu.hidden = true;
816
+ };
817
+
818
+ const activeTab = await queryActiveTab();
819
+ byId<HTMLDivElement>("unsupported-notice").hidden = !isUnsupportedPage(
820
+ activeTab?.url,
821
+ );
628
822
  render(settings);
823
+ void refreshDevices();
824
+ if (navigator.mediaDevices) {
825
+ navigator.mediaDevices.addEventListener("devicechange", () => {
826
+ void refreshDevices();
827
+ });
828
+ }
629
829
  const status =
630
830
  await sendSimpleMessage<PopupStatusResponse>("CLIPS_POPUP_STATUS");
631
831
  activeRecording = status.activeRecording ?? null;
@@ -634,17 +834,26 @@ async function init(): Promise<void> {
634
834
  window.setInterval(() => renderActiveRecording(activeRecording), 1000);
635
835
  }
636
836
 
837
+ // No on-page pre-record preview. A Chrome action popup closes the instant you
838
+ // click the page, so an interactive on-page bubble before recording isn't
839
+ // possible — the face bubble appears only once recording starts. syncPreview
840
+ // is kept as a no-op so the settings handlers below stay unchanged.
841
+ const syncPreview = (): void => {};
842
+
637
843
  for (const button of document.querySelectorAll<HTMLButtonElement>(
638
844
  ".mode-option",
639
845
  )) {
640
846
  button.addEventListener("click", () => {
641
847
  applyMode(settings, button.dataset.mode as RecordingModeChoice);
848
+ closeDeviceMenus();
642
849
  render(settings);
643
850
  void saveSettings(settings);
851
+ syncPreview();
644
852
  });
645
853
  }
646
854
 
647
855
  sourceButton.addEventListener("click", () => {
856
+ closeDeviceMenus();
648
857
  sourceMenu.hidden = !sourceMenu.hidden;
649
858
  });
650
859
 
@@ -656,9 +865,70 @@ async function init(): Promise<void> {
656
865
  sourceMenu.hidden = true;
657
866
  render(settings);
658
867
  void saveSettings(settings);
868
+ syncPreview();
659
869
  });
660
870
  }
661
871
 
872
+ cameraDeviceButton.addEventListener("click", () => {
873
+ sourceMenu.hidden = true;
874
+ microphoneDeviceMenu.hidden = true;
875
+ cameraDeviceMenu.hidden = !cameraDeviceMenu.hidden;
876
+ });
877
+
878
+ cameraDeviceMenu.addEventListener("click", (event) => {
879
+ const item = (event.target as HTMLElement).closest<HTMLButtonElement>(
880
+ ".row-menu-item",
881
+ );
882
+ if (!item) return;
883
+ settings.videoDeviceId = item.dataset.deviceId ?? "";
884
+ cameraDeviceMenu.hidden = true;
885
+ renderDevicePickers(settings);
886
+ void saveSettings(settings);
887
+ });
888
+
889
+ microphoneDeviceButton.addEventListener("click", () => {
890
+ sourceMenu.hidden = true;
891
+ cameraDeviceMenu.hidden = true;
892
+ microphoneDeviceMenu.hidden = !microphoneDeviceMenu.hidden;
893
+ });
894
+
895
+ microphoneDeviceMenu.addEventListener("click", (event) => {
896
+ const item = (event.target as HTMLElement).closest<HTMLButtonElement>(
897
+ ".row-menu-item",
898
+ );
899
+ if (!item) return;
900
+ settings.audioDeviceId = item.dataset.deviceId ?? "";
901
+ microphoneDeviceMenu.hidden = true;
902
+ renderDevicePickers(settings);
903
+ void saveSettings(settings);
904
+ });
905
+
906
+ document.addEventListener("click", (event) => {
907
+ const target = event.target;
908
+ if (!(target instanceof Node)) return;
909
+ if (
910
+ !sourceMenu.hidden &&
911
+ !sourceMenu.contains(target) &&
912
+ !sourceButton.contains(target)
913
+ ) {
914
+ sourceMenu.hidden = true;
915
+ }
916
+ if (
917
+ !cameraDeviceMenu.hidden &&
918
+ !cameraDeviceMenu.contains(target) &&
919
+ !cameraDeviceButton.contains(target)
920
+ ) {
921
+ cameraDeviceMenu.hidden = true;
922
+ }
923
+ if (
924
+ !microphoneDeviceMenu.hidden &&
925
+ !microphoneDeviceMenu.contains(target) &&
926
+ !microphoneDeviceButton.contains(target)
927
+ ) {
928
+ microphoneDeviceMenu.hidden = true;
929
+ }
930
+ });
931
+
662
932
  includeCamera.addEventListener("click", () => {
663
933
  settings.includeCamera = !settings.includeCamera;
664
934
  if (settings.includeCamera && settings.captureSurface === "monitor") {
@@ -667,12 +937,15 @@ async function init(): Promise<void> {
667
937
  if (!settings.includeCamera && settings.captureSurface === "camera") {
668
938
  settings.captureSurface = "browser";
669
939
  }
940
+ closeDeviceMenus();
670
941
  render(settings);
671
942
  void saveSettings(settings);
943
+ syncPreview();
672
944
  });
673
945
 
674
946
  includeMicrophone.addEventListener("click", () => {
675
947
  settings.includeMicrophone = !settings.includeMicrophone;
948
+ closeDeviceMenus();
676
949
  render(settings);
677
950
  void saveSettings(settings);
678
951
  });
@@ -791,7 +1064,7 @@ async function init(): Promise<void> {
791
1064
  start.addEventListener("click", async () => {
792
1065
  start.disabled = true;
793
1066
  signIn.hidden = true;
794
- setStatus("Checking sign in...");
1067
+ setStatus(""); // no chatty "Checking…/Starting…" text — the disabled button is enough
795
1068
  authStatus = await readAuthStatus(settings);
796
1069
  if (authStatus === "signed-out") {
797
1070
  start.disabled = false;
@@ -800,7 +1073,6 @@ async function init(): Promise<void> {
800
1073
  setStatus("");
801
1074
  return;
802
1075
  }
803
- setStatus("Checking video storage...");
804
1076
  const storageConfigured = await readVideoStorageConfigured(settings);
805
1077
  if (!storageConfigured) {
806
1078
  start.disabled = false;
@@ -823,7 +1095,6 @@ async function init(): Promise<void> {
823
1095
  window.close();
824
1096
  return;
825
1097
  }
826
- setStatus("Starting recording...");
827
1098
  await saveSettings(settings);
828
1099
  const response = await sendStartMessage(settings);
829
1100
  if (response.ok) {
@@ -855,3 +855,37 @@ h1 {
855
855
  .primary-action:hover {
856
856
  opacity: 0.92;
857
857
  }
858
+
859
+ /* Unsupported-page notice (shown in the popup when the active tab can't host
860
+ the on-page overlay, e.g. chrome:// / New Tab / Web Store pages). */
861
+ .notice {
862
+ display: flex;
863
+ gap: 10px;
864
+ align-items: flex-start;
865
+ margin: 0 0 12px;
866
+ padding: 11px 12px;
867
+ border: 1px solid var(--border);
868
+ border-radius: var(--radius-sm);
869
+ background: var(--surface);
870
+ }
871
+
872
+ .notice-icon {
873
+ width: 18px;
874
+ height: 18px;
875
+ flex: none;
876
+ margin-top: 1px;
877
+ color: var(--status-warn);
878
+ }
879
+
880
+ .notice-title {
881
+ font-size: 12.5px;
882
+ font-weight: 600;
883
+ color: var(--fg);
884
+ margin-bottom: 2px;
885
+ }
886
+
887
+ .notice-text {
888
+ font-size: 11.5px;
889
+ line-height: 1.45;
890
+ color: var(--fg-muted);
891
+ }
@@ -8,6 +8,11 @@ export default defineConfig({
8
8
  root,
9
9
  base: "./",
10
10
  publicDir: "public",
11
+ // Share recording primitives with the web app recorder (templates/clips/shared)
12
+ // — matches the "@shared/*" tsconfig path so imports resolve in both builds.
13
+ resolve: {
14
+ alias: { "@shared": resolve(root, "../shared") },
15
+ },
11
16
  build: {
12
17
  outDir: "dist",
13
18
  emptyOutDir: true,
@@ -14,12 +14,11 @@
14
14
  */
15
15
 
16
16
  export interface AudioCue {
17
- /** Play the cue during the countdown once recording is about one second out. */
18
- playCountdownCue(): Promise<void>;
19
17
  /**
20
18
  * Play the cue (bounded by a timeout) and wait a short settle so the tone
21
- * sits just before capture rather than inside the recording. Safe to await
22
- * directly before starting the recorder.
19
+ * sits just before capture rather than inside the recording. Call this at the
20
+ * exact moment recording starts (right before the recorder/native capture is
21
+ * kicked off) so the chime lines up with the real start, not the countdown.
23
22
  */
24
23
  playBeforeCapture(): Promise<void>;
25
24
  cleanup(): void;
@@ -38,7 +37,6 @@ function wait(ms: number): Promise<void> {
38
37
 
39
38
  /** A cue that does nothing — used when Web Audio is unavailable. */
40
39
  const noopAudioCue: AudioCue = {
41
- async playCountdownCue() {},
42
40
  async playBeforeCapture() {},
43
41
  cleanup() {},
44
42
  };
@@ -68,19 +66,26 @@ async function playBeforeCapture(
68
66
  }
69
67
 
70
68
  /**
71
- * Schedule the recording-start chime on an already-running context: a soft
72
- * rising two-note (D5 → A5) with a faint high shimmer. Fast attack, smooth
73
- * exponential tail a gentle confirmation rather than a harsh beep. Kept under
74
- * ~380ms so it fits inside `CUE_PLAY_TIMEOUT_MS` and lands just before capture.
69
+ * Schedule the recording-start chime on an already-running context.
70
+ *
71
+ * A warm, bell-like rising two-note "ding-dong" (G5 C6, a perfect fourth)
72
+ * voiced on triangle oscillators for a rounder, less electronic timbre, with a
73
+ * quiet octave overtone (C7) sparkling on top of the second note. Each note has
74
+ * a soft 18ms attack and a long exponential decay so it rings out gently — a
75
+ * confident "you're rolling" confirmation that sounds nothing like a flat
76
+ * countdown blip. Kept under ~420ms so it still fits inside
77
+ * `CUE_PLAY_TIMEOUT_MS` and lands right as capture begins.
75
78
  */
76
79
  function scheduleTone(ctx: AudioContext): Promise<void> {
77
80
  return new Promise<void>((resolve) => {
78
81
  const t0 = ctx.currentTime + 0.005;
79
82
  const voices = [
80
- { freq: 587.33, at: 0.0, dur: 0.22, peak: 0.06 }, // D5
81
- { freq: 880.0, at: 0.06, dur: 0.26, peak: 0.075 }, // A5
82
- { freq: 1760.0, at: 0.065, dur: 0.14, peak: 0.02 }, // A6 shimmer
83
- ];
83
+ // Rising perfect fourth: a gentle "ding dong".
84
+ { freq: 783.99, at: 0.0, dur: 0.26, peak: 0.07, type: "triangle" }, // G5
85
+ { freq: 1046.5, at: 0.11, dur: 0.34, peak: 0.085, type: "triangle" }, // C6
86
+ // Faint octave overtone gives the second note a soft bell shimmer.
87
+ { freq: 2093.0, at: 0.115, dur: 0.18, peak: 0.022, type: "sine" }, // C7
88
+ ] as const;
84
89
 
85
90
  let lastStop = t0;
86
91
  for (const voice of voices) {
@@ -89,12 +94,13 @@ function scheduleTone(ctx: AudioContext): Promise<void> {
89
94
  lastStop = Math.max(lastStop, stopAt);
90
95
 
91
96
  const oscillator = ctx.createOscillator();
92
- oscillator.type = "sine";
97
+ oscillator.type = voice.type;
93
98
  oscillator.frequency.setValueAtTime(voice.freq, startAt);
94
99
 
95
100
  const gain = ctx.createGain();
101
+ // Smooth attack/decay envelope — ramps avoid the click of a hard gate.
96
102
  gain.gain.setValueAtTime(0.0001, startAt);
97
- gain.gain.exponentialRampToValueAtTime(voice.peak, startAt + 0.012);
103
+ gain.gain.exponentialRampToValueAtTime(voice.peak, startAt + 0.018);
98
104
  gain.gain.exponentialRampToValueAtTime(0.0001, stopAt);
99
105
 
100
106
  oscillator.connect(gain);
@@ -165,7 +171,6 @@ export function createAudioCue(): AudioCue {
165
171
  idleTimer = window.setTimeout(cleanup, CUE_IDLE_CLEANUP_MS);
166
172
 
167
173
  return {
168
- playCountdownCue: play,
169
174
  playBeforeCapture: () => playBeforeCapture(play, cleanup),
170
175
  cleanup,
171
176
  };
@@ -1398,10 +1398,7 @@ async function selectRegionForRecording(): Promise<RegionCaptureRect> {
1398
1398
  }
1399
1399
  }
1400
1400
 
1401
- function waitForCountdownEvent(
1402
- timeoutMs = 4000,
1403
- onOneSecond?: () => void,
1404
- ): Promise<void> {
1401
+ function waitForCountdownEvent(timeoutMs = 4000): Promise<void> {
1405
1402
  return new Promise((resolve, reject) => {
1406
1403
  let timer: ReturnType<typeof setTimeout> | null = null;
1407
1404
  const unlistens: UnlistenFn[] = [];
@@ -1457,9 +1454,6 @@ function waitForCountdownEvent(
1457
1454
 
1458
1455
  track(listen("clips:countdown-done", () => finish("done")));
1459
1456
  track(listen("clips:countdown-cancel", () => finish("cancel")));
1460
- if (onOneSecond) {
1461
- track(listen("clips:countdown-one", () => onOneSecond()));
1462
- }
1463
1457
 
1464
1458
  timer = setTimeout(() => {
1465
1459
  if (done) return;
@@ -1493,13 +1487,12 @@ async function showRegionRecordBorder(region: RegionCaptureRect | null) {
1493
1487
  });
1494
1488
  }
1495
1489
 
1496
- async function runRecordingCountdown(wantsScreen: boolean, audioCue: AudioCue) {
1497
- const countdownEvent = waitForCountdownEvent(
1498
- COUNTDOWN_EVENT_TIMEOUT_MS,
1499
- () => {
1500
- void audioCue.playCountdownCue();
1501
- },
1502
- );
1490
+ async function runRecordingCountdown(wantsScreen: boolean) {
1491
+ // The recording-start chime is intentionally NOT played here. It fires from
1492
+ // `audioCue.playBeforeCapture()` at the real capture-start (right before the
1493
+ // recorder/native capture is kicked off) so the beep lines up with the moment
1494
+ // recording actually begins — not one second early on the countdown's "1".
1495
+ const countdownEvent = waitForCountdownEvent(COUNTDOWN_EVENT_TIMEOUT_MS);
1503
1496
  await showRegionGuidesForRecording(wantsScreen);
1504
1497
  try {
1505
1498
  await invoke("show_countdown");
@@ -1680,7 +1673,7 @@ async function startNativeFullscreenRecording(
1680
1673
  }).catch((err) => {
1681
1674
  console.warn("[clips-recorder] mic warm failed:", err);
1682
1675
  });
1683
- const countdownPromise = runRecordingCountdown(true, audioCue);
1676
+ const countdownPromise = runRecordingCountdown(true);
1684
1677
  if (localOnly) {
1685
1678
  id = localFolderName;
1686
1679
  await Promise.all([countdownPromise, warmMic(id)]);
@@ -2460,7 +2453,7 @@ async function startRecordingInner(
2460
2453
  combined,
2461
2454
  });
2462
2455
 
2463
- const countdownPromise = runRecordingCountdown(wantsScreen, audioCue);
2456
+ const countdownPromise = runRecordingCountdown(wantsScreen);
2464
2457
  const localExportPromise = prepareLocalRecordingExport(targets);
2465
2458
  let localExport: Awaited<ReturnType<typeof prepareLocalRecordingExport>>;
2466
2459
  try {
@@ -2636,7 +2629,7 @@ async function startRecordingInner(
2636
2629
  console.log(
2637
2630
  "[clips-recorder] invoking show_countdown + createServerRecording",
2638
2631
  );
2639
- const countdownPromise = runRecordingCountdown(wantsScreen, audioCue);
2632
+ const countdownPromise = runRecordingCountdown(wantsScreen);
2640
2633
  console.time("[clips-recorder] createServerRecording duration");
2641
2634
  const recordingPromise = createServerRecording(
2642
2635
  params.serverUrl,