@agent-native/core 0.71.0 → 0.72.1

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 (165) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +60 -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/package.json +2 -1
  6. package/corpus/core/src/action.ts +80 -1
  7. package/corpus/core/src/agent/production-agent.ts +27 -0
  8. package/corpus/core/src/agent/run-store.ts +18 -1
  9. package/corpus/core/src/application-state/store.ts +5 -0
  10. package/corpus/core/src/audit/actions/get-audit-event.ts +23 -0
  11. package/corpus/core/src/audit/actions/list-audit-events.ts +65 -0
  12. package/corpus/core/src/audit/cleanup-job.ts +100 -0
  13. package/corpus/core/src/audit/config.ts +91 -0
  14. package/corpus/core/src/audit/index.ts +43 -0
  15. package/corpus/core/src/audit/record.ts +143 -0
  16. package/corpus/core/src/audit/redact.ts +109 -0
  17. package/corpus/core/src/audit/store.ts +244 -0
  18. package/corpus/core/src/audit/types.ts +125 -0
  19. package/corpus/core/src/chat-threads/store.ts +10 -0
  20. package/corpus/core/src/client/AssistantChat.tsx +6 -0
  21. package/corpus/core/src/client/chat/repo-helpers.ts +38 -0
  22. package/corpus/core/src/db/client.ts +4 -0
  23. package/corpus/core/src/db/widen-columns.ts +75 -0
  24. package/corpus/core/src/mcp/build-server.ts +1 -0
  25. package/corpus/core/src/oauth-tokens/store.ts +6 -0
  26. package/corpus/core/src/provider-api/custom-registry.ts +8 -0
  27. package/corpus/core/src/resources/store.ts +10 -0
  28. package/corpus/core/src/scripts/runner.ts +6 -3
  29. package/corpus/core/src/server/action-discovery.ts +6 -0
  30. package/corpus/core/src/server/action-routes.ts +1 -0
  31. package/corpus/core/src/server/auth.ts +5 -0
  32. package/corpus/core/src/server/core-routes-plugin.ts +14 -0
  33. package/corpus/core/src/server/security-headers.ts +9 -6
  34. package/corpus/core/src/settings/store.ts +6 -0
  35. package/corpus/core/src/templates/workspace-core/.agents/skills/audit-log/SKILL.md +93 -0
  36. package/corpus/core/src/usage/store.ts +6 -0
  37. package/corpus/templates/calendar/app/components/calendar/GoogleConnectBanner.tsx +60 -6
  38. package/corpus/templates/calendar/changelog/2026-06-23-added-a-heads-up-explaining-google-s-app-not-verified-screen.md +6 -0
  39. package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +20 -43
  40. package/corpus/templates/clips/app/components/recorder/recording-toolbar.tsx +5 -1
  41. package/corpus/templates/clips/app/lib/countdown-audio-cue.ts +3 -51
  42. package/corpus/templates/clips/changelog/2026-06-23-public-clips-now-play-inline-in-slack-connect-a-workspac.md +6 -0
  43. package/corpus/templates/clips/chrome-extension/PERMISSIONS.md +72 -0
  44. package/corpus/templates/clips/chrome-extension/public/manifest.json +1 -10
  45. package/corpus/templates/clips/chrome-extension/src/background.ts +34 -1
  46. package/corpus/templates/clips/chrome-extension/src/content-script.ts +138 -11
  47. package/corpus/templates/clips/chrome-extension/src/offscreen.ts +50 -33
  48. package/corpus/templates/clips/chrome-extension/src/overlay.css +11 -27
  49. package/corpus/templates/clips/chrome-extension/src/overlay.html +7 -1
  50. package/corpus/templates/clips/chrome-extension/src/overlay.ts +33 -38
  51. package/corpus/templates/clips/chrome-extension/src/popup.ts +6 -24
  52. package/corpus/templates/clips/chrome-extension/vite.config.ts +5 -0
  53. package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +43 -3
  54. package/corpus/templates/clips/server/lib/media-permissions.ts +5 -1
  55. package/corpus/templates/clips/shared/recording-audio.ts +62 -0
  56. package/corpus/templates/clips/shared/recording-core.ts +94 -0
  57. package/dist/action.d.ts +31 -0
  58. package/dist/action.d.ts.map +1 -1
  59. package/dist/action.js +45 -1
  60. package/dist/action.js.map +1 -1
  61. package/dist/agent/context-xray/schema.d.ts +1 -1
  62. package/dist/agent/observational-memory/schema.d.ts +1 -1
  63. package/dist/agent/production-agent.d.ts.map +1 -1
  64. package/dist/agent/production-agent.js +28 -0
  65. package/dist/agent/production-agent.js.map +1 -1
  66. package/dist/agent/run-store.d.ts.map +1 -1
  67. package/dist/agent/run-store.js +18 -1
  68. package/dist/agent/run-store.js.map +1 -1
  69. package/dist/application-state/store.d.ts.map +1 -1
  70. package/dist/application-state/store.js +5 -0
  71. package/dist/application-state/store.js.map +1 -1
  72. package/dist/audit/actions/get-audit-event.d.ts +11 -0
  73. package/dist/audit/actions/get-audit-event.d.ts.map +1 -0
  74. package/dist/audit/actions/get-audit-event.js +22 -0
  75. package/dist/audit/actions/get-audit-event.js.map +1 -0
  76. package/dist/audit/actions/list-audit-events.d.ts +22 -0
  77. package/dist/audit/actions/list-audit-events.d.ts.map +1 -0
  78. package/dist/audit/actions/list-audit-events.js +61 -0
  79. package/dist/audit/actions/list-audit-events.js.map +1 -0
  80. package/dist/audit/cleanup-job.d.ts +12 -0
  81. package/dist/audit/cleanup-job.d.ts.map +1 -0
  82. package/dist/audit/cleanup-job.js +93 -0
  83. package/dist/audit/cleanup-job.js.map +1 -0
  84. package/dist/audit/config.d.ts +30 -0
  85. package/dist/audit/config.d.ts.map +1 -0
  86. package/dist/audit/config.js +65 -0
  87. package/dist/audit/config.js.map +1 -0
  88. package/dist/audit/index.d.ts +13 -0
  89. package/dist/audit/index.d.ts.map +1 -0
  90. package/dist/audit/index.js +6 -0
  91. package/dist/audit/index.js.map +1 -0
  92. package/dist/audit/record.d.ts +24 -0
  93. package/dist/audit/record.d.ts.map +1 -0
  94. package/dist/audit/record.js +106 -0
  95. package/dist/audit/record.js.map +1 -0
  96. package/dist/audit/redact.d.ts +28 -0
  97. package/dist/audit/redact.d.ts.map +1 -0
  98. package/dist/audit/redact.js +109 -0
  99. package/dist/audit/redact.js.map +1 -0
  100. package/dist/audit/store.d.ts +14 -0
  101. package/dist/audit/store.d.ts.map +1 -0
  102. package/dist/audit/store.js +219 -0
  103. package/dist/audit/store.js.map +1 -0
  104. package/dist/audit/types.d.ts +114 -0
  105. package/dist/audit/types.d.ts.map +1 -0
  106. package/dist/audit/types.js +15 -0
  107. package/dist/audit/types.js.map +1 -0
  108. package/dist/chat-threads/store.d.ts.map +1 -1
  109. package/dist/chat-threads/store.js +10 -0
  110. package/dist/chat-threads/store.js.map +1 -1
  111. package/dist/client/AssistantChat.d.ts.map +1 -1
  112. package/dist/client/AssistantChat.js +6 -1
  113. package/dist/client/AssistantChat.js.map +1 -1
  114. package/dist/client/chat/repo-helpers.d.ts +16 -0
  115. package/dist/client/chat/repo-helpers.d.ts.map +1 -1
  116. package/dist/client/chat/repo-helpers.js +40 -0
  117. package/dist/client/chat/repo-helpers.js.map +1 -1
  118. package/dist/db/client.d.ts.map +1 -1
  119. package/dist/db/client.js +3 -0
  120. package/dist/db/client.js.map +1 -1
  121. package/dist/db/widen-columns.d.ts +39 -0
  122. package/dist/db/widen-columns.d.ts.map +1 -0
  123. package/dist/db/widen-columns.js +73 -0
  124. package/dist/db/widen-columns.js.map +1 -0
  125. package/dist/mcp/build-server.d.ts.map +1 -1
  126. package/dist/mcp/build-server.js +1 -0
  127. package/dist/mcp/build-server.js.map +1 -1
  128. package/dist/oauth-tokens/store.d.ts.map +1 -1
  129. package/dist/oauth-tokens/store.js +6 -0
  130. package/dist/oauth-tokens/store.js.map +1 -1
  131. package/dist/provider-api/custom-registry.d.ts.map +1 -1
  132. package/dist/provider-api/custom-registry.js +8 -0
  133. package/dist/provider-api/custom-registry.js.map +1 -1
  134. package/dist/resources/store.d.ts.map +1 -1
  135. package/dist/resources/store.js +9 -0
  136. package/dist/resources/store.js.map +1 -1
  137. package/dist/scripts/runner.js +4 -3
  138. package/dist/scripts/runner.js.map +1 -1
  139. package/dist/server/action-discovery.d.ts.map +1 -1
  140. package/dist/server/action-discovery.js +6 -0
  141. package/dist/server/action-discovery.js.map +1 -1
  142. package/dist/server/action-routes.d.ts.map +1 -1
  143. package/dist/server/action-routes.js +1 -0
  144. package/dist/server/action-routes.js.map +1 -1
  145. package/dist/server/auth.d.ts.map +1 -1
  146. package/dist/server/auth.js +5 -0
  147. package/dist/server/auth.js.map +1 -1
  148. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  149. package/dist/server/core-routes-plugin.js +13 -0
  150. package/dist/server/core-routes-plugin.js.map +1 -1
  151. package/dist/server/security-headers.d.ts +8 -5
  152. package/dist/server/security-headers.d.ts.map +1 -1
  153. package/dist/server/security-headers.js +9 -6
  154. package/dist/server/security-headers.js.map +1 -1
  155. package/dist/settings/store.d.ts.map +1 -1
  156. package/dist/settings/store.js +6 -0
  157. package/dist/settings/store.js.map +1 -1
  158. package/dist/templates/workspace-core/.agents/skills/audit-log/SKILL.md +93 -0
  159. package/dist/usage/store.d.ts.map +1 -1
  160. package/dist/usage/store.js +5 -0
  161. package/dist/usage/store.js.map +1 -1
  162. package/docs/content/actions.md +23 -0
  163. package/docs/content/audit-log.md +111 -0
  164. package/package.json +2 -1
  165. package/src/templates/workspace-core/.agents/skills/audit-log/SKILL.md +93 -0
@@ -10,8 +10,14 @@ import {
10
10
  IconUpload,
11
11
  IconAlertTriangle,
12
12
  IconLogout,
13
+ IconInfoCircle,
13
14
  } from "@tabler/icons-react";
14
15
  import { Button } from "@/components/ui/button";
16
+ import {
17
+ Popover,
18
+ PopoverContent,
19
+ PopoverTrigger,
20
+ } from "@/components/ui/popover";
15
21
  import {
16
22
  agentNativePath,
17
23
  isInBuilderFrame,
@@ -336,6 +342,8 @@ export function GoogleConnectBanner({
336
342
  : "Connect Google"}
337
343
  </Button>
338
344
 
345
+ <GoogleVerificationNotice className="mt-3" />
346
+
339
347
  <GoogleAuthIssuePanel
340
348
  issue={desktopAuthIssue}
341
349
  onSignOut={handleSignOutForGoogle}
@@ -439,15 +447,18 @@ export function GoogleConnectBanner({
439
447
  <div className="border-b border-border/30 bg-card">
440
448
  {/* Compact banner row */}
441
449
  <div className="flex items-center justify-between gap-3 px-4 py-2">
442
- <div className="flex items-center gap-2.5">
450
+ <div className="flex items-center gap-2.5 min-w-0">
443
451
  <div className="flex h-6 w-6 shrink-0 items-center justify-center rounded bg-white/[0.06]">
444
452
  <IconCalendarCheck className="h-3 w-3 text-white/40" />
445
453
  </div>
446
- <p className="text-[13px] font-medium leading-tight text-foreground/80">
447
- {allConfigured
448
- ? "Ready to connect — sign in with your Google account"
449
- : "Connect Google to sync your calendar"}
450
- </p>
454
+ <div className="flex min-w-0 flex-col">
455
+ <p className="text-[13px] font-medium leading-tight text-foreground/80">
456
+ {allConfigured
457
+ ? "Ready to connect — sign in with your Google account"
458
+ : "Connect Google to sync your calendar"}
459
+ </p>
460
+ <GoogleVerificationNotice className="mt-0.5" />
461
+ </div>
451
462
  </div>
452
463
 
453
464
  <div className="flex items-center gap-1.5 shrink-0">
@@ -534,6 +545,49 @@ export function GoogleConnectBanner({
534
545
  );
535
546
  }
536
547
 
548
+ // Heads-up popover: Google shows a "hasn't verified this app" warning during
549
+ // the OAuth consent flow because the connection runs through the user's own
550
+ // Google Cloud project (External + Testing), not a Google-reviewed public app.
551
+ // This explains that the warning is expected and how to safely continue.
552
+ function GoogleVerificationNotice({ className = "" }: { className?: string }) {
553
+ return (
554
+ <Popover>
555
+ <PopoverTrigger asChild>
556
+ <button
557
+ type="button"
558
+ className={`inline-flex items-center gap-1 text-[11px] text-muted-foreground/70 transition-colors hover:text-muted-foreground ${className}`}
559
+ >
560
+ <IconInfoCircle className="h-3 w-3" />
561
+ Google may show a warning
562
+ </button>
563
+ </PopoverTrigger>
564
+ <PopoverContent align="center" className="w-72 text-left">
565
+ <div className="flex items-start gap-2.5">
566
+ <div className="mt-0.5 flex h-6 w-6 shrink-0 items-center justify-center rounded-md bg-amber-500/15 text-amber-300">
567
+ <IconAlertTriangle className="h-3.5 w-3.5" />
568
+ </div>
569
+ <div className="space-y-1.5">
570
+ <p className="text-[13px] font-medium text-foreground">
571
+ “Google hasn’t verified this app”
572
+ </p>
573
+ <p className="text-xs leading-relaxed text-muted-foreground">
574
+ You’ll see this screen because the calendar connects through your
575
+ own Google Cloud project, not a Google-reviewed public app. It’s
576
+ safe to continue: click{" "}
577
+ <span className="font-medium text-foreground">Advanced</span>,
578
+ then{" "}
579
+ <span className="font-medium text-foreground">
580
+ “Go to … (unsafe)”
581
+ </span>{" "}
582
+ to finish connecting.
583
+ </p>
584
+ </div>
585
+ </div>
586
+ </PopoverContent>
587
+ </Popover>
588
+ );
589
+ }
590
+
537
591
  function GoogleAuthIssuePanel({
538
592
  issue,
539
593
  onSignOut,
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: added
3
+ date: 2026-06-23
4
+ ---
5
+
6
+ Added a heads-up explaining Google's 'app not verified' screen when connecting your calendar, so you know it's safe to continue
@@ -19,6 +19,15 @@ import {
19
19
  createCameraCompositeStream,
20
20
  type CameraCompositeHandle,
21
21
  } from "@/lib/camera-composite";
22
+ import {
23
+ chunkUploadUrl,
24
+ pickMimeType,
25
+ pickMimeTypeCandidates,
26
+ } from "@shared/recording-core";
27
+
28
+ // Re-exported for existing callers; the canonical impls live in
29
+ // @shared/recording-core and are shared with the Chrome extension recorder.
30
+ export { pickMimeType, pickMimeTypeCandidates };
22
31
 
23
32
  export type RecordingMode = "screen" | "camera" | "screen+camera";
24
33
  export type DisplaySurface = "monitor" | "window" | "browser";
@@ -281,34 +290,6 @@ function isScreenPickerDismissal(err: unknown): boolean {
281
290
  return false;
282
291
  }
283
292
 
284
- /** Pick a MediaRecorder mimeType the current browser actually supports. */
285
- export function pickMimeTypeCandidates(): string[] {
286
- // Chrome can report MP4 support but still reject the encoder configuration
287
- // for some display-capture streams. Prefer the WebM combinations that are
288
- // broadly supported by MediaRecorder, then fall back to MP4/Safari.
289
- return [
290
- "video/webm;codecs=vp8,opus",
291
- "video/webm;codecs=vp9,opus",
292
- "video/webm;codecs=vp8",
293
- "video/webm;codecs=vp9",
294
- "video/webm",
295
- "video/mp4;codecs=avc1",
296
- "video/mp4",
297
- ];
298
- }
299
-
300
- export function pickMimeType(): string {
301
- if (typeof MediaRecorder === "undefined") return "video/webm";
302
- for (const type of pickMimeTypeCandidates()) {
303
- try {
304
- if (MediaRecorder.isTypeSupported(type)) return type;
305
- } catch {
306
- // continue
307
- }
308
- }
309
- return "";
310
- }
311
-
312
293
  function canUseTimeslicedRecorderChunks(mimeType: string): boolean {
313
294
  // WebM chunks emitted by MediaRecorder are safe to concatenate locally before
314
295
  // compression/upload. Browser MP4 chunks are not reliably self-contained; in
@@ -1589,21 +1570,17 @@ export class RecorderEngine {
1589
1570
  signal?: AbortSignal;
1590
1571
  } = {},
1591
1572
  ): Promise<Record<string, unknown> | undefined> {
1592
- const params = new URLSearchParams();
1593
- params.set("index", String(index));
1594
- if (extra.total !== undefined) params.set("total", String(extra.total));
1595
- params.set("isFinal", extra.isFinal ? "1" : "0");
1596
- if (extra.mimeType) params.set("mimeType", extra.mimeType);
1597
- if (extra.durationMs !== undefined)
1598
- params.set("durationMs", String(Math.round(extra.durationMs)));
1599
- if (extra.width !== undefined) params.set("width", String(extra.width));
1600
- if (extra.height !== undefined) params.set("height", String(extra.height));
1601
- if (extra.hasAudio !== undefined)
1602
- params.set("hasAudio", extra.hasAudio ? "1" : "0");
1603
- if (extra.hasCamera !== undefined)
1604
- params.set("hasCamera", extra.hasCamera ? "1" : "0");
1605
-
1606
- const url = `${this.opts.uploadUrl}?${params.toString()}`;
1573
+ const url = chunkUploadUrl(this.opts.uploadUrl, {
1574
+ index,
1575
+ total: extra.total,
1576
+ isFinal: extra.isFinal,
1577
+ mimeType: extra.mimeType,
1578
+ durationMs: extra.durationMs,
1579
+ width: extra.width,
1580
+ height: extra.height,
1581
+ hasAudio: extra.hasAudio,
1582
+ hasCamera: extra.hasCamera,
1583
+ });
1607
1584
 
1608
1585
  const body = await blob.arrayBuffer();
1609
1586
  let res: Response | null = null;
@@ -25,6 +25,10 @@ export interface RecordingToolbarProps {
25
25
 
26
26
  const TOOLBAR_WIDTH = 276;
27
27
  const TOOLBAR_HEIGHT = 56;
28
+ // Drop the toolbar just below the centered "Recording your screen…" status
29
+ // text (which sits at the viewport's vertical center) so the controls don't
30
+ // overlap it.
31
+ const TOOLBAR_TOP_OFFSET = 48;
28
32
 
29
33
  function formatElapsed(ms: number): string {
30
34
  const totalSeconds = Math.max(0, Math.floor(ms / 1000));
@@ -47,7 +51,7 @@ export function RecordingToolbar({
47
51
  ? { left: 16, top: 16, corner: "tl" }
48
52
  : {
49
53
  left: Math.max(16, (window.innerWidth - TOOLBAR_WIDTH) / 2),
50
- top: Math.max(16, (window.innerHeight - TOOLBAR_HEIGHT) / 2),
54
+ top: Math.max(16, window.innerHeight / 2 + TOOLBAR_TOP_OFFSET),
51
55
  corner: "tl",
52
56
  },
53
57
  );
@@ -1,3 +1,5 @@
1
+ import { scheduleReadyChime } from "@shared/recording-audio";
2
+
1
3
  export interface CountdownAudioCue {
2
4
  play(): Promise<void>;
3
5
  cleanup(): void;
@@ -8,56 +10,6 @@ const noopCountdownAudioCue: CountdownAudioCue = {
8
10
  cleanup() {},
9
11
  };
10
12
 
11
- /**
12
- * A soft, modern "ready" chime: a rising two-note (D5 → A5) with a faint high
13
- * shimmer. Each voice has a fast attack and a smooth exponential tail so it
14
- * reads as a gentle confirmation rather than a harsh beep. Kept under ~380ms so
15
- * it lands cleanly just before capture.
16
- */
17
- function scheduleCountdownTone(ctx: AudioContext): Promise<void> {
18
- return new Promise<void>((resolve) => {
19
- const t0 = ctx.currentTime + 0.005;
20
- const voices = [
21
- { freq: 587.33, at: 0.0, dur: 0.22, peak: 0.06 }, // D5
22
- { freq: 880.0, at: 0.06, dur: 0.26, peak: 0.075 }, // A5
23
- { freq: 1760.0, at: 0.065, dur: 0.14, peak: 0.02 }, // A6 shimmer
24
- ];
25
-
26
- let lastStop = t0;
27
- for (const voice of voices) {
28
- const startAt = t0 + voice.at;
29
- const stopAt = startAt + voice.dur;
30
- lastStop = Math.max(lastStop, stopAt);
31
-
32
- const oscillator = ctx.createOscillator();
33
- oscillator.type = "sine";
34
- oscillator.frequency.setValueAtTime(voice.freq, startAt);
35
-
36
- const gain = ctx.createGain();
37
- gain.gain.setValueAtTime(0.0001, startAt);
38
- gain.gain.exponentialRampToValueAtTime(voice.peak, startAt + 0.012);
39
- gain.gain.exponentialRampToValueAtTime(0.0001, stopAt);
40
-
41
- oscillator.connect(gain);
42
- gain.connect(ctx.destination);
43
-
44
- oscillator.start(startAt);
45
- oscillator.stop(stopAt + 0.02);
46
- }
47
-
48
- let resolved = false;
49
- const finish = () => {
50
- if (resolved) return;
51
- resolved = true;
52
- resolve();
53
- };
54
- window.setTimeout(
55
- finish,
56
- Math.ceil((lastStop - ctx.currentTime) * 1000) + 60,
57
- );
58
- });
59
- }
60
-
61
13
  export function createCountdownAudioCue(): CountdownAudioCue {
62
14
  try {
63
15
  const AudioCtx =
@@ -87,7 +39,7 @@ export function createCountdownAudioCue(): CountdownAudioCue {
87
39
  try {
88
40
  if (ctx.state !== "running") await ctx.resume();
89
41
  if (closed) return;
90
- await scheduleCountdownTone(ctx);
42
+ await scheduleReadyChime(ctx);
91
43
  } catch (err) {
92
44
  console.warn("[recorder] countdown cue unavailable:", err);
93
45
  cleanup();
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: added
3
+ date: 2026-06-23
4
+ ---
5
+
6
+ Clips for Slack is here — connect a workspace under Settings, then paste any public clip link in Slack and it unfurls as a playable video
@@ -0,0 +1,72 @@
1
+ # Permissions model
2
+
3
+ This extension ships **activeTab-only**: it deliberately does **not** request the
4
+ broad `<all_urls>` host permission, and it has **no declarative `content_scripts`**.
5
+
6
+ ## Why
7
+
8
+ A declarative content script on `<all_urls>` (and the matching `<all_urls>` host
9
+ permission) triggers Chrome Web Store's **broad host permission in-depth review**,
10
+ which significantly delays publishing and updates. We avoid that.
11
+
12
+ ## How the overlay gets on the page
13
+
14
+ When the user clicks the extension and starts a recording, the background service
15
+ worker injects the content script into the **launch tab** with
16
+ `chrome.scripting.executeScript({ target: { tabId }, files: ["assets/content-script.js"] })`.
17
+ That call is authorized by the **`activeTab`** permission, which Chrome grants for
18
+ the tab that was active when the user invoked the extension — no broad host access
19
+ needed. The content script then mounts the overlay iframes (countdown, camera
20
+ bubble, controls). `web_accessible_resources` stays `<all_urls>` — that is **not**
21
+ a host permission and does not trigger the review.
22
+
23
+ Declared permissions: `activeTab`, `debugger`, `offscreen`, `scripting`,
24
+ `storage`. Host permissions: only the configured Clips app + `forms.agent-native.com`
25
+
26
+ - `localhost`/`127.0.0.1`.
27
+
28
+ ## What this costs
29
+
30
+ - The overlay (countdown, camera bubble, recording controls) lives **only on the
31
+ tab the recording was launched from**. It does **not** follow the user to other
32
+ tabs during a full-screen or multi-tab recording.
33
+ - **Recording itself is unaffected.** Capture runs in the offscreen document via
34
+ `getDisplayMedia`, independent of any tab's content script — full-screen,
35
+ window, and other-tab content are all still captured normally. Only the on-page
36
+ _overlay UI_ is scoped to the launch tab.
37
+
38
+ ## Re-enabling cross-tab follow
39
+
40
+ The full cross-tab behavior is gated behind a single flag in
41
+ `src/background.ts`:
42
+
43
+ ```ts
44
+ const CROSS_TAB_FOLLOW = false; // flip to true
45
+ ```
46
+
47
+ Flipping it to `true` restores the all-tabs broadcast in `broadcastMount()` /
48
+ `broadcastUnmount()` and the `chrome.tabs.onActivated` follow listener. To make
49
+ that actually work you must **also** restore broad host access so the worker can
50
+ inject into arbitrary tabs. Two options:
51
+
52
+ 1. **Static (simplest, but in-depth review):** re-add to `public/manifest.json`:
53
+ - `"<all_urls>"` in `host_permissions`, and
54
+ - the `content_scripts` block:
55
+ ```json
56
+ "content_scripts": [
57
+ {
58
+ "matches": ["<all_urls>"],
59
+ "js": ["assets/content-script.js"],
60
+ "run_at": "document_idle",
61
+ "all_frames": false
62
+ }
63
+ ],
64
+ ```
65
+
66
+ 2. **Optional (preferred — keeps the default install lean):** declare
67
+ `"optional_host_permissions": ["<all_urls>"]` in the manifest and call
68
+ `chrome.permissions.request({ origins: ["<all_urls>"] })` at runtime (e.g. from
69
+ a settings toggle) before enabling follow. The user grants the broad access
70
+ explicitly, only if they want cross-tab overlays.
71
+
72
+ After any change: `pnpm build`, then re-zip `dist/`.
@@ -23,16 +23,7 @@
23
23
  "https://clips.agent-native.com/*",
24
24
  "https://forms.agent-native.com/*",
25
25
  "http://localhost/*",
26
- "http://127.0.0.1/*",
27
- "<all_urls>"
28
- ],
29
- "content_scripts": [
30
- {
31
- "matches": ["<all_urls>"],
32
- "js": ["assets/content-script.js"],
33
- "run_at": "document_idle",
34
- "all_frames": false
35
- }
26
+ "http://127.0.0.1/*"
36
27
  ],
37
28
  "web_accessible_resources": [
38
29
  {
@@ -215,6 +215,17 @@ let overlayBaseEpochMs = 0;
215
215
  // show the on-page bubble for camera-only mode (recordingShowsBubble).
216
216
  let overlayShowsBubble = false;
217
217
  let recordingShowsBubble = false;
218
+ // Cross-tab follow: when true, the overlay is pushed to whatever tab the user
219
+ // switches to mid-recording. That requires "<all_urls>" + a declarative content
220
+ // script, which triggers Chrome's broad-host in-depth review. Shipped OFF
221
+ // (activeTab-only: the overlay lives on the launch tab the user invoked the
222
+ // extension from). Capture is unaffected — it runs in the offscreen document via
223
+ // getDisplayMedia regardless. See PERMISSIONS.md to re-enable.
224
+ // Typed `boolean` (not inferred literal `false`) so the cross-tab branches below
225
+ // don't read as unreachable code; flip the value to re-enable. See PERMISSIONS.md.
226
+ const CROSS_TAB_FOLLOW: boolean = false;
227
+ // The tab the recording was launched from — the only tab activeTab lets us touch.
228
+ let overlayTabId: number | null = null;
218
229
  let countdownEndsAtMs = 0;
219
230
 
220
231
  function desiredParts(): OverlayPart[] {
@@ -292,6 +303,7 @@ function persistOverlay(): Promise<void> {
292
303
  showsBubble: overlayShowsBubble,
293
304
  recordingShowsBubble,
294
305
  countdownEndsAtMs,
306
+ overlayTabId,
295
307
  },
296
308
  }).catch(() => undefined);
297
309
  }
@@ -313,6 +325,7 @@ async function restoreRuntimeState(): Promise<void> {
313
325
  showsBubble?: boolean;
314
326
  recordingShowsBubble?: boolean;
315
327
  countdownEndsAtMs?: number;
328
+ overlayTabId?: number;
316
329
  }
317
330
  | undefined;
318
331
  if (rt && typeof rt.phase === "string" && overlayPhase === "idle") {
@@ -325,6 +338,7 @@ async function restoreRuntimeState(): Promise<void> {
325
338
  recordingShowsBubble = Boolean(rt.recordingShowsBubble);
326
339
  countdownEndsAtMs =
327
340
  typeof rt.countdownEndsAtMs === "number" ? rt.countdownEndsAtMs : 0;
341
+ overlayTabId = typeof rt.overlayTabId === "number" ? rt.overlayTabId : null;
328
342
  }
329
343
 
330
344
  if (overlayPhase !== "idle" && activeNativeRecording) {
@@ -393,6 +407,13 @@ function allTabs(): Promise<chrome.tabs.Tab[]> {
393
407
  }
394
408
 
395
409
  async function broadcastMount(): Promise<void> {
410
+ // activeTab-only: keep the overlay on the launch tab (re-ensures the injected
411
+ // content script too, so it survives a worker suspend). Cross-tab broadcast is
412
+ // gated behind CROSS_TAB_FOLLOW because it needs broad host access.
413
+ if (!CROSS_TAB_FOLLOW) {
414
+ if (overlayTabId !== null) await mountOverlayOnTab(overlayTabId);
415
+ return;
416
+ }
396
417
  const parts = desiredParts();
397
418
  const tabs = await allTabs();
398
419
  await Promise.all(
@@ -405,6 +426,11 @@ async function broadcastMount(): Promise<void> {
405
426
  }
406
427
 
407
428
  async function broadcastUnmount(): Promise<void> {
429
+ if (!CROSS_TAB_FOLLOW) {
430
+ if (overlayTabId !== null)
431
+ await sendTabMessage(overlayTabId, { type: "CLIPS_OVERLAY_UNMOUNT" });
432
+ return;
433
+ }
408
434
  const tabs = await allTabs();
409
435
  await Promise.all(
410
436
  tabs.map((tab) =>
@@ -957,6 +983,7 @@ async function armRecording(args: {
957
983
  setRecordingFlag(true);
958
984
  // Clicking the icon now stops & saves immediately instead of opening the popup.
959
985
  setActionPopup("");
986
+ overlayTabId = tab.id as number;
960
987
  await mountOverlayOnTab(tab.id as number);
961
988
  broadcastOverlayState();
962
989
 
@@ -1025,7 +1052,12 @@ async function armRecording(args: {
1025
1052
  // suspendable worker) and reports "recording" back when it actually starts.
1026
1053
  const authToken = (await readAuthSession(settings))?.token;
1027
1054
  console.log("[clips-bg] arm: created row", created.id, "auth?", !!authToken);
1028
- const startDelayMs = Math.max(0, countdownEndsAtMs - nowMs());
1055
+ // The on-page countdown drives the actual start (it sends COUNTDOWN_DONE at
1056
+ // "Go"). This offscreen timer is only a FALLBACK. When a camera is involved the
1057
+ // countdown waits for the camera feed before it even shows "3" (the content
1058
+ // script holds it, with its own 12s cap), so the fallback must be generous
1059
+ // enough not to start the recorder mid-connect; otherwise a short pre-roll.
1060
+ const startDelayMs = cameraInvolved ? 20000 : COUNTDOWN_SECONDS * 1000 + 1000;
1029
1061
  try {
1030
1062
  await sendOffscreenMessage({
1031
1063
  type: "CLIPS_OFFSCREEN_BEGIN",
@@ -2028,6 +2060,7 @@ async function dispatchRuntimeMessage(message: unknown): Promise<unknown> {
2028
2060
  // switch tabs (programmatic injection covers tabs opened before the extension
2029
2061
  // loaded; declared content scripts cover navigations).
2030
2062
  chrome.tabs.onActivated.addListener((info) => {
2063
+ if (!CROSS_TAB_FOLLOW) return; // activeTab-only: overlay stays on the launch tab
2031
2064
  void (async () => {
2032
2065
  await ensureRestored();
2033
2066
  if (overlayPhase === "idle" || !activeNativeRecording) return;