@agent-native/core 0.71.0 → 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.
- package/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +38 -0
- package/corpus/core/docs/content/actions.md +23 -0
- package/corpus/core/docs/content/audit-log.md +111 -0
- package/corpus/core/package.json +2 -1
- package/corpus/core/src/action.ts +80 -1
- package/corpus/core/src/agent/production-agent.ts +27 -0
- package/corpus/core/src/agent/run-store.ts +14 -0
- package/corpus/core/src/application-state/store.ts +5 -0
- package/corpus/core/src/audit/actions/get-audit-event.ts +23 -0
- package/corpus/core/src/audit/actions/list-audit-events.ts +65 -0
- package/corpus/core/src/audit/cleanup-job.ts +100 -0
- package/corpus/core/src/audit/config.ts +91 -0
- package/corpus/core/src/audit/index.ts +43 -0
- package/corpus/core/src/audit/record.ts +143 -0
- package/corpus/core/src/audit/redact.ts +109 -0
- package/corpus/core/src/audit/store.ts +244 -0
- package/corpus/core/src/audit/types.ts +125 -0
- package/corpus/core/src/chat-threads/store.ts +10 -0
- package/corpus/core/src/client/AssistantChat.tsx +6 -0
- package/corpus/core/src/client/chat/repo-helpers.ts +38 -0
- package/corpus/core/src/db/client.ts +4 -0
- package/corpus/core/src/db/widen-columns.ts +75 -0
- package/corpus/core/src/mcp/build-server.ts +1 -0
- package/corpus/core/src/oauth-tokens/store.ts +6 -0
- package/corpus/core/src/provider-api/custom-registry.ts +8 -0
- package/corpus/core/src/resources/store.ts +10 -0
- package/corpus/core/src/scripts/runner.ts +6 -3
- package/corpus/core/src/server/action-discovery.ts +6 -0
- package/corpus/core/src/server/action-routes.ts +1 -0
- package/corpus/core/src/server/auth.ts +5 -0
- package/corpus/core/src/server/core-routes-plugin.ts +14 -0
- package/corpus/core/src/server/security-headers.ts +9 -6
- package/corpus/core/src/settings/store.ts +6 -0
- package/corpus/core/src/templates/workspace-core/.agents/skills/audit-log/SKILL.md +93 -0
- package/corpus/core/src/usage/store.ts +6 -0
- package/corpus/templates/calendar/app/components/calendar/GoogleConnectBanner.tsx +60 -6
- package/corpus/templates/calendar/changelog/2026-06-23-added-a-heads-up-explaining-google-s-app-not-verified-screen.md +6 -0
- package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +20 -43
- package/corpus/templates/clips/app/components/recorder/recording-toolbar.tsx +5 -1
- package/corpus/templates/clips/app/lib/countdown-audio-cue.ts +3 -51
- package/corpus/templates/clips/changelog/2026-06-23-public-clips-now-play-inline-in-slack-connect-a-workspac.md +6 -0
- package/corpus/templates/clips/chrome-extension/PERMISSIONS.md +72 -0
- package/corpus/templates/clips/chrome-extension/public/manifest.json +1 -10
- package/corpus/templates/clips/chrome-extension/src/background.ts +34 -1
- package/corpus/templates/clips/chrome-extension/src/content-script.ts +138 -11
- package/corpus/templates/clips/chrome-extension/src/offscreen.ts +50 -33
- package/corpus/templates/clips/chrome-extension/src/overlay.css +11 -27
- package/corpus/templates/clips/chrome-extension/src/overlay.html +7 -1
- package/corpus/templates/clips/chrome-extension/src/overlay.ts +33 -38
- package/corpus/templates/clips/chrome-extension/src/popup.ts +6 -24
- package/corpus/templates/clips/chrome-extension/vite.config.ts +5 -0
- package/corpus/templates/clips/server/lib/media-permissions.ts +5 -1
- package/corpus/templates/clips/shared/recording-audio.ts +62 -0
- package/corpus/templates/clips/shared/recording-core.ts +94 -0
- package/dist/action.d.ts +31 -0
- package/dist/action.d.ts.map +1 -1
- package/dist/action.js +45 -1
- package/dist/action.js.map +1 -1
- package/dist/agent/context-xray/schema.d.ts +1 -1
- package/dist/agent/observational-memory/schema.d.ts +1 -1
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +28 -0
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/agent/run-store.d.ts.map +1 -1
- package/dist/agent/run-store.js +14 -0
- package/dist/agent/run-store.js.map +1 -1
- package/dist/application-state/store.d.ts.map +1 -1
- package/dist/application-state/store.js +5 -0
- package/dist/application-state/store.js.map +1 -1
- package/dist/audit/actions/get-audit-event.d.ts +11 -0
- package/dist/audit/actions/get-audit-event.d.ts.map +1 -0
- package/dist/audit/actions/get-audit-event.js +22 -0
- package/dist/audit/actions/get-audit-event.js.map +1 -0
- package/dist/audit/actions/list-audit-events.d.ts +22 -0
- package/dist/audit/actions/list-audit-events.d.ts.map +1 -0
- package/dist/audit/actions/list-audit-events.js +61 -0
- package/dist/audit/actions/list-audit-events.js.map +1 -0
- package/dist/audit/cleanup-job.d.ts +12 -0
- package/dist/audit/cleanup-job.d.ts.map +1 -0
- package/dist/audit/cleanup-job.js +93 -0
- package/dist/audit/cleanup-job.js.map +1 -0
- package/dist/audit/config.d.ts +30 -0
- package/dist/audit/config.d.ts.map +1 -0
- package/dist/audit/config.js +65 -0
- package/dist/audit/config.js.map +1 -0
- package/dist/audit/index.d.ts +13 -0
- package/dist/audit/index.d.ts.map +1 -0
- package/dist/audit/index.js +6 -0
- package/dist/audit/index.js.map +1 -0
- package/dist/audit/record.d.ts +24 -0
- package/dist/audit/record.d.ts.map +1 -0
- package/dist/audit/record.js +106 -0
- package/dist/audit/record.js.map +1 -0
- package/dist/audit/redact.d.ts +28 -0
- package/dist/audit/redact.d.ts.map +1 -0
- package/dist/audit/redact.js +109 -0
- package/dist/audit/redact.js.map +1 -0
- package/dist/audit/store.d.ts +14 -0
- package/dist/audit/store.d.ts.map +1 -0
- package/dist/audit/store.js +219 -0
- package/dist/audit/store.js.map +1 -0
- package/dist/audit/types.d.ts +114 -0
- package/dist/audit/types.d.ts.map +1 -0
- package/dist/audit/types.js +15 -0
- package/dist/audit/types.js.map +1 -0
- package/dist/chat-threads/store.d.ts.map +1 -1
- package/dist/chat-threads/store.js +10 -0
- package/dist/chat-threads/store.js.map +1 -1
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +6 -1
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/chat/repo-helpers.d.ts +16 -0
- package/dist/client/chat/repo-helpers.d.ts.map +1 -1
- package/dist/client/chat/repo-helpers.js +40 -0
- package/dist/client/chat/repo-helpers.js.map +1 -1
- package/dist/db/client.d.ts.map +1 -1
- package/dist/db/client.js +3 -0
- package/dist/db/client.js.map +1 -1
- package/dist/db/widen-columns.d.ts +39 -0
- package/dist/db/widen-columns.d.ts.map +1 -0
- package/dist/db/widen-columns.js +73 -0
- package/dist/db/widen-columns.js.map +1 -0
- package/dist/mcp/build-server.d.ts.map +1 -1
- package/dist/mcp/build-server.js +1 -0
- package/dist/mcp/build-server.js.map +1 -1
- package/dist/oauth-tokens/store.d.ts.map +1 -1
- package/dist/oauth-tokens/store.js +6 -0
- package/dist/oauth-tokens/store.js.map +1 -1
- package/dist/provider-api/custom-registry.d.ts.map +1 -1
- package/dist/provider-api/custom-registry.js +8 -0
- package/dist/provider-api/custom-registry.js.map +1 -1
- package/dist/resources/store.d.ts.map +1 -1
- package/dist/resources/store.js +9 -0
- package/dist/resources/store.js.map +1 -1
- package/dist/scripts/runner.js +4 -3
- package/dist/scripts/runner.js.map +1 -1
- package/dist/server/action-discovery.d.ts.map +1 -1
- package/dist/server/action-discovery.js +6 -0
- package/dist/server/action-discovery.js.map +1 -1
- package/dist/server/action-routes.d.ts.map +1 -1
- package/dist/server/action-routes.js +1 -0
- package/dist/server/action-routes.js.map +1 -1
- package/dist/server/auth.d.ts.map +1 -1
- package/dist/server/auth.js +5 -0
- package/dist/server/auth.js.map +1 -1
- package/dist/server/core-routes-plugin.d.ts.map +1 -1
- package/dist/server/core-routes-plugin.js +13 -0
- package/dist/server/core-routes-plugin.js.map +1 -1
- package/dist/server/security-headers.d.ts +8 -5
- package/dist/server/security-headers.d.ts.map +1 -1
- package/dist/server/security-headers.js +9 -6
- package/dist/server/security-headers.js.map +1 -1
- package/dist/settings/store.d.ts.map +1 -1
- package/dist/settings/store.js +6 -0
- package/dist/settings/store.js.map +1 -1
- package/dist/templates/workspace-core/.agents/skills/audit-log/SKILL.md +93 -0
- package/dist/usage/store.d.ts.map +1 -1
- package/dist/usage/store.js +5 -0
- package/dist/usage/store.js.map +1 -1
- package/docs/content/actions.md +23 -0
- package/docs/content/audit-log.md +111 -0
- package/package.json +2 -1
- 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
|
-
<
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
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,
|
|
@@ -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
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
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,
|
|
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
|
|
42
|
+
await scheduleReadyChime(ctx);
|
|
91
43
|
} catch (err) {
|
|
92
44
|
console.warn("[recorder] countdown cue unavailable:", err);
|
|
93
45
|
cleanup();
|
|
@@ -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
|
-
|
|
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;
|