@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.
- package/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +51 -0
- package/corpus/core/docs/content/actions.md +23 -0
- package/corpus/core/docs/content/audit-log.md +111 -0
- package/corpus/core/docs/content/multi-app-workspace.md +2 -2
- package/corpus/core/docs/design/durable-agent-runs.md +458 -4
- package/corpus/core/package.json +2 -1
- package/corpus/core/src/action.ts +80 -1
- package/corpus/core/src/agent/durable-background.ts +192 -0
- package/corpus/core/src/agent/production-agent.ts +357 -13
- package/corpus/core/src/agent/run-manager.ts +66 -3
- package/corpus/core/src/agent/run-store.ts +143 -23
- package/corpus/core/src/agent/types.ts +20 -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/AgentPanel.tsx +12 -9
- package/corpus/core/src/client/AssistantChat.tsx +6 -0
- package/corpus/core/src/client/blocks/library/FileTreeBlock.tsx +72 -14
- 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/deploy/build.ts +96 -0
- package/corpus/core/src/deploy/workspace-deploy.ts +121 -0
- package/corpus/core/src/mcp/build-server.ts +23 -13
- 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/agent-chat-plugin.ts +133 -66
- 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/analytics/.agents/skills/dashboard-management/SKILL.md +19 -0
- package/corpus/templates/analytics/AGENTS.md +8 -0
- package/corpus/templates/analytics/actions/compose-dashboard.ts +317 -0
- package/corpus/templates/analytics/changelog/2026-06-23-build-large-first-party-analytics-dashboards-in-one-fast-cal.md +6 -0
- package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +574 -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-dragging-the-desktop-camera-bubble-now-glides-to-a-stop-at-t.md +6 -0
- 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 +163 -32
- package/corpus/templates/clips/chrome-extension/src/content-script.ts +301 -24
- package/corpus/templates/clips/chrome-extension/src/offscreen.ts +245 -67
- package/corpus/templates/clips/chrome-extension/src/overlay.css +77 -43
- package/corpus/templates/clips/chrome-extension/src/overlay.html +7 -1
- package/corpus/templates/clips/chrome-extension/src/overlay.ts +102 -38
- package/corpus/templates/clips/chrome-extension/src/popup.html +62 -5
- package/corpus/templates/clips/chrome-extension/src/popup.ts +275 -4
- package/corpus/templates/clips/chrome-extension/src/styles.css +34 -0
- package/corpus/templates/clips/chrome-extension/vite.config.ts +5 -0
- package/corpus/templates/clips/desktop/src/lib/audio-cue.ts +21 -16
- package/corpus/templates/clips/desktop/src/lib/recorder.ts +10 -17
- package/corpus/templates/clips/desktop/src/overlays/bubble.tsx +73 -23
- package/corpus/templates/clips/desktop/src/overlays/countdown.tsx +0 -6
- package/corpus/templates/clips/desktop/src/overlays/toolbar.tsx +56 -46
- package/corpus/templates/clips/desktop/src/styles.css +33 -5
- package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +113 -0
- package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +3 -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/durable-background.d.ts +60 -0
- package/dist/agent/durable-background.d.ts.map +1 -0
- package/dist/agent/durable-background.js +144 -0
- package/dist/agent/durable-background.js.map +1 -0
- package/dist/agent/observational-memory/schema.d.ts +1 -1
- package/dist/agent/production-agent.d.ts +20 -0
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +320 -14
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/agent/run-manager.d.ts +48 -0
- package/dist/agent/run-manager.d.ts.map +1 -1
- package/dist/agent/run-manager.js +45 -3
- package/dist/agent/run-manager.js.map +1 -1
- package/dist/agent/run-store.d.ts +30 -1
- package/dist/agent/run-store.d.ts.map +1 -1
- package/dist/agent/run-store.js +138 -24
- package/dist/agent/run-store.js.map +1 -1
- package/dist/agent/types.d.ts +20 -0
- package/dist/agent/types.d.ts.map +1 -1
- package/dist/agent/types.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/AgentPanel.d.ts.map +1 -1
- package/dist/client/AgentPanel.js +2 -2
- package/dist/client/AgentPanel.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/blocks/library/FileTreeBlock.d.ts.map +1 -1
- package/dist/client/blocks/library/FileTreeBlock.js +37 -4
- package/dist/client/blocks/library/FileTreeBlock.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/deploy/build.d.ts +29 -0
- package/dist/deploy/build.d.ts.map +1 -1
- package/dist/deploy/build.js +85 -0
- package/dist/deploy/build.js.map +1 -1
- package/dist/deploy/workspace-deploy.d.ts.map +1 -1
- package/dist/deploy/workspace-deploy.js +108 -0
- package/dist/deploy/workspace-deploy.js.map +1 -1
- package/dist/mcp/build-server.d.ts.map +1 -1
- package/dist/mcp/build-server.js +24 -9
- 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/agent-chat-plugin.d.ts.map +1 -1
- package/dist/server/agent-chat-plugin.js +68 -13
- package/dist/server/agent-chat-plugin.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/docs/content/multi-app-workspace.md +2 -2
- package/docs/design/durable-agent-runs.md +458 -4
- package/package.json +2 -1
- package/src/templates/workspace-core/.agents/skills/audit-log/SKILL.md +93 -0
|
@@ -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
|
{
|
|
@@ -210,7 +210,22 @@ type OverlayPart = "bubble" | "countdown" | "toolbar" | "saving";
|
|
|
210
210
|
let overlayPhase: OverlayPhase = "idle";
|
|
211
211
|
let overlayBaseElapsedMs = 0;
|
|
212
212
|
let overlayBaseEpochMs = 0;
|
|
213
|
+
// Bubble during the countdown preview (any camera). During recording, the face
|
|
214
|
+
// is composited into the video by the offscreen for screen+camera, so we only
|
|
215
|
+
// show the on-page bubble for camera-only mode (recordingShowsBubble).
|
|
213
216
|
let overlayShowsBubble = false;
|
|
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;
|
|
214
229
|
let countdownEndsAtMs = 0;
|
|
215
230
|
|
|
216
231
|
function desiredParts(): OverlayPart[] {
|
|
@@ -221,7 +236,7 @@ function desiredParts(): OverlayPart[] {
|
|
|
221
236
|
return overlayShowsBubble ? ["bubble", "countdown"] : ["countdown"];
|
|
222
237
|
}
|
|
223
238
|
if (overlayPhase === "recording" || overlayPhase === "paused") {
|
|
224
|
-
return
|
|
239
|
+
return recordingShowsBubble ? ["bubble", "toolbar"] : ["toolbar"];
|
|
225
240
|
}
|
|
226
241
|
if (overlayPhase === "saving") {
|
|
227
242
|
return ["saving"];
|
|
@@ -286,7 +301,9 @@ function persistOverlay(): Promise<void> {
|
|
|
286
301
|
baseElapsedMs: overlayBaseElapsedMs,
|
|
287
302
|
baseEpochMs: overlayBaseEpochMs,
|
|
288
303
|
showsBubble: overlayShowsBubble,
|
|
304
|
+
recordingShowsBubble,
|
|
289
305
|
countdownEndsAtMs,
|
|
306
|
+
overlayTabId,
|
|
290
307
|
},
|
|
291
308
|
}).catch(() => undefined);
|
|
292
309
|
}
|
|
@@ -306,7 +323,9 @@ async function restoreRuntimeState(): Promise<void> {
|
|
|
306
323
|
baseElapsedMs?: number;
|
|
307
324
|
baseEpochMs?: number;
|
|
308
325
|
showsBubble?: boolean;
|
|
326
|
+
recordingShowsBubble?: boolean;
|
|
309
327
|
countdownEndsAtMs?: number;
|
|
328
|
+
overlayTabId?: number;
|
|
310
329
|
}
|
|
311
330
|
| undefined;
|
|
312
331
|
if (rt && typeof rt.phase === "string" && overlayPhase === "idle") {
|
|
@@ -316,8 +335,10 @@ async function restoreRuntimeState(): Promise<void> {
|
|
|
316
335
|
overlayBaseEpochMs =
|
|
317
336
|
typeof rt.baseEpochMs === "number" ? rt.baseEpochMs : 0;
|
|
318
337
|
overlayShowsBubble = Boolean(rt.showsBubble);
|
|
338
|
+
recordingShowsBubble = Boolean(rt.recordingShowsBubble);
|
|
319
339
|
countdownEndsAtMs =
|
|
320
340
|
typeof rt.countdownEndsAtMs === "number" ? rt.countdownEndsAtMs : 0;
|
|
341
|
+
overlayTabId = typeof rt.overlayTabId === "number" ? rt.overlayTabId : null;
|
|
321
342
|
}
|
|
322
343
|
|
|
323
344
|
if (overlayPhase !== "idle" && activeNativeRecording) {
|
|
@@ -386,6 +407,13 @@ function allTabs(): Promise<chrome.tabs.Tab[]> {
|
|
|
386
407
|
}
|
|
387
408
|
|
|
388
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
|
+
}
|
|
389
417
|
const parts = desiredParts();
|
|
390
418
|
const tabs = await allTabs();
|
|
391
419
|
await Promise.all(
|
|
@@ -398,6 +426,11 @@ async function broadcastMount(): Promise<void> {
|
|
|
398
426
|
}
|
|
399
427
|
|
|
400
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
|
+
}
|
|
401
434
|
const tabs = await allTabs();
|
|
402
435
|
await Promise.all(
|
|
403
436
|
tabs.map((tab) =>
|
|
@@ -413,6 +446,7 @@ function resetOverlay(): void {
|
|
|
413
446
|
overlayBaseElapsedMs = 0;
|
|
414
447
|
overlayBaseEpochMs = 0;
|
|
415
448
|
overlayShowsBubble = false;
|
|
449
|
+
recordingShowsBubble = false;
|
|
416
450
|
clearCountdownTimer();
|
|
417
451
|
setRecordingFlag(false);
|
|
418
452
|
// Bring back the toolbar popup now that we're idle again.
|
|
@@ -420,6 +454,41 @@ function resetOverlay(): void {
|
|
|
420
454
|
void persistOverlay();
|
|
421
455
|
}
|
|
422
456
|
|
|
457
|
+
// ----- Pre-record camera preview --------------------------------------------
|
|
458
|
+
// While the popup is open and idle, show the live face bubble on the active tab
|
|
459
|
+
// (like the desktop app's pre-record bubble). The popup holds a runtime port
|
|
460
|
+
// open; its disconnect (popup closed) tears the preview down.
|
|
461
|
+
let previewTabId: number | null = null;
|
|
462
|
+
|
|
463
|
+
async function startPreview(wantsCamera: boolean): Promise<void> {
|
|
464
|
+
await ensureRestored();
|
|
465
|
+
// Never show a preview over an active recording (the recording overlay owns
|
|
466
|
+
// the tab then). If the camera is off, make sure any preview is removed.
|
|
467
|
+
if (overlayPhase !== "idle" || !wantsCamera) {
|
|
468
|
+
await stopPreview();
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
const tab = await queryActiveTab();
|
|
472
|
+
if (!tab || typeof tab.id !== "number") return;
|
|
473
|
+
if (previewTabId !== null && previewTabId !== tab.id) await stopPreview();
|
|
474
|
+
previewTabId = tab.id;
|
|
475
|
+
await ensureContentScript(tab.id);
|
|
476
|
+
// Injection / send fail silently on unsupported pages (chrome://, etc.).
|
|
477
|
+
await sendTabMessage(tab.id, {
|
|
478
|
+
type: "CLIPS_OVERLAY_MOUNT",
|
|
479
|
+
parts: ["bubble"],
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
async function stopPreview(): Promise<void> {
|
|
484
|
+
const tabId = previewTabId;
|
|
485
|
+
previewTabId = null;
|
|
486
|
+
if (tabId === null) return;
|
|
487
|
+
// Don't tear down a recording overlay that took over this tab.
|
|
488
|
+
if (overlayPhase !== "idle") return;
|
|
489
|
+
await sendTabMessage(tabId, { type: "CLIPS_OVERLAY_UNMOUNT" });
|
|
490
|
+
}
|
|
491
|
+
|
|
423
492
|
// Mirrored into chrome.storage.local so content scripts can cheaply tell whether
|
|
424
493
|
// a recording is in progress without waking the service worker on every page
|
|
425
494
|
// load. Content scripts can read storage.local by default; storage.session
|
|
@@ -875,6 +944,7 @@ async function armRecording(args: {
|
|
|
875
944
|
settings.captureSurface === "camera" ? "camera" : "screen";
|
|
876
945
|
const surface: "browser" | "window" | "monitor" =
|
|
877
946
|
settings.captureSurface === "camera" ? "browser" : settings.captureSurface;
|
|
947
|
+
console.log("[clips-bg] arm: start", { mode, surface, tabId: tab.id });
|
|
878
948
|
|
|
879
949
|
// Pre-warmed on popup open, so this is effectively instant and keeps the
|
|
880
950
|
// getDisplayMedia() call close to the user's click.
|
|
@@ -882,15 +952,52 @@ async function armRecording(args: {
|
|
|
882
952
|
|
|
883
953
|
// 1) Native "Choose what to share" picker. Do this before any network round
|
|
884
954
|
// trip so the picker stays tied to the user gesture. Throws if cancelled.
|
|
885
|
-
await sendOffscreenMessage<{
|
|
955
|
+
const acq = await sendOffscreenMessage<{
|
|
956
|
+
ok: boolean;
|
|
957
|
+
width: number;
|
|
958
|
+
height: number;
|
|
959
|
+
}>({
|
|
886
960
|
type: "CLIPS_OFFSCREEN_ACQUIRE",
|
|
887
961
|
sessionId,
|
|
888
962
|
mode,
|
|
889
963
|
surface,
|
|
890
964
|
includeMicrophone: settings.includeMicrophone,
|
|
965
|
+
includeCamera: settings.includeCamera,
|
|
891
966
|
});
|
|
967
|
+
console.log("[clips-bg] arm: acquired stream", acq);
|
|
968
|
+
|
|
969
|
+
// 2) Show the countdown overlay IMMEDIATELY — before the network round-trip —
|
|
970
|
+
// so the 3-2-1 runs full-length and the dim/blur clears exactly when the
|
|
971
|
+
// recorder starts. The on-page bubble shows during countdown AND recording
|
|
972
|
+
// (the face is captured in the display; we do not composite).
|
|
973
|
+
const cameraInvolved = mode === "camera" || settings.includeCamera;
|
|
974
|
+
overlayPhase = "countdown";
|
|
975
|
+
overlayBaseElapsedMs = 0;
|
|
976
|
+
overlayBaseEpochMs = nowMs();
|
|
977
|
+
overlayShowsBubble = cameraInvolved;
|
|
978
|
+
recordingShowsBubble = cameraInvolved;
|
|
979
|
+
countdownEndsAtMs = nowMs() + COUNTDOWN_SECONDS * 1000;
|
|
980
|
+
// The recording overlay now owns this tab's bubble — hand off from any preview
|
|
981
|
+
// so the popup-close disconnect won't tear down the live recording overlay.
|
|
982
|
+
previewTabId = null;
|
|
983
|
+
setRecordingFlag(true);
|
|
984
|
+
// Clicking the icon now stops & saves immediately instead of opening the popup.
|
|
985
|
+
setActionPopup("");
|
|
986
|
+
overlayTabId = tab.id as number;
|
|
987
|
+
await mountOverlayOnTab(tab.id as number);
|
|
988
|
+
broadcastOverlayState();
|
|
989
|
+
|
|
990
|
+
const abortArming = async (): Promise<void> => {
|
|
991
|
+
await sendOffscreenMessage({
|
|
992
|
+
type: "CLIPS_OFFSCREEN_CANCEL",
|
|
993
|
+
sessionId,
|
|
994
|
+
}).catch(() => undefined);
|
|
995
|
+
resetOverlay();
|
|
996
|
+
await broadcastUnmount();
|
|
997
|
+
await chrome.action.setBadgeText({ text: "" });
|
|
998
|
+
};
|
|
892
999
|
|
|
893
|
-
//
|
|
1000
|
+
// 3) Create the recording row (happens during the countdown).
|
|
894
1001
|
type CreatedRecording = {
|
|
895
1002
|
id?: string;
|
|
896
1003
|
uploadChunkUrl?: string;
|
|
@@ -903,23 +1010,17 @@ async function armRecording(args: {
|
|
|
903
1010
|
titleSource: tab.title ? "context" : "default",
|
|
904
1011
|
sourceAppName: "Chrome",
|
|
905
1012
|
sourceWindowTitle: tab.title ?? null,
|
|
906
|
-
hasCamera:
|
|
1013
|
+
hasCamera: cameraInvolved,
|
|
907
1014
|
hasAudio:
|
|
908
1015
|
settings.includeMicrophone || settings.captureSurface !== "camera",
|
|
909
1016
|
visibility: "public",
|
|
910
1017
|
});
|
|
911
1018
|
} catch (err) {
|
|
912
|
-
await
|
|
913
|
-
type: "CLIPS_OFFSCREEN_CANCEL",
|
|
914
|
-
sessionId,
|
|
915
|
-
}).catch(() => undefined);
|
|
1019
|
+
await abortArming();
|
|
916
1020
|
throw err;
|
|
917
1021
|
}
|
|
918
1022
|
if (!created.id || !created.uploadChunkUrl) {
|
|
919
|
-
await
|
|
920
|
-
type: "CLIPS_OFFSCREEN_CANCEL",
|
|
921
|
-
sessionId,
|
|
922
|
-
}).catch(() => undefined);
|
|
1023
|
+
await abortArming();
|
|
923
1024
|
throw new Error("Clips did not create a recording target.");
|
|
924
1025
|
}
|
|
925
1026
|
|
|
@@ -946,35 +1047,29 @@ async function armRecording(args: {
|
|
|
946
1047
|
error: null,
|
|
947
1048
|
};
|
|
948
1049
|
|
|
949
|
-
//
|
|
950
|
-
//
|
|
951
|
-
//
|
|
952
|
-
overlayPhase = "countdown";
|
|
953
|
-
overlayBaseElapsedMs = 0;
|
|
954
|
-
overlayBaseEpochMs = nowMs();
|
|
955
|
-
overlayShowsBubble = mode === "camera" || settings.includeCamera;
|
|
956
|
-
countdownEndsAtMs = nowMs() + COUNTDOWN_SECONDS * 1000;
|
|
957
|
-
setRecordingFlag(true);
|
|
958
|
-
// Clicking the icon now stops & saves immediately instead of opening the popup.
|
|
959
|
-
setActionPopup("");
|
|
960
|
-
|
|
961
|
-
// Hand the recorder its targets plus the pre-roll delay. The offscreen document
|
|
962
|
-
// (a reliable context, unlike the suspendable worker) owns the countdown timer
|
|
963
|
-
// and reports "recording" back when the recorder actually starts — that status
|
|
964
|
-
// flip (markRecordingStarted) advances our phase, so recording begins even on
|
|
965
|
-
// pages where no overlay can be injected (chrome://, the New Tab page, etc.).
|
|
1050
|
+
// 4) Start the recorder when the (already-running) countdown ends. The
|
|
1051
|
+
// offscreen owns the pre-roll timer (a reliable context, unlike the
|
|
1052
|
+
// suspendable worker) and reports "recording" back when it actually starts.
|
|
966
1053
|
const authToken = (await readAuthSession(settings))?.token;
|
|
1054
|
+
console.log("[clips-bg] arm: created row", created.id, "auth?", !!authToken);
|
|
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;
|
|
967
1061
|
try {
|
|
968
1062
|
await sendOffscreenMessage({
|
|
969
1063
|
type: "CLIPS_OFFSCREEN_BEGIN",
|
|
970
1064
|
sessionId,
|
|
971
1065
|
recordingId: created.id,
|
|
972
1066
|
uploadUrl,
|
|
973
|
-
hasCamera:
|
|
974
|
-
startDelayMs
|
|
1067
|
+
hasCamera: cameraInvolved,
|
|
1068
|
+
startDelayMs,
|
|
975
1069
|
authToken,
|
|
976
1070
|
});
|
|
977
1071
|
} catch (err) {
|
|
1072
|
+
console.error("[clips-bg] arm: BEGIN failed", err);
|
|
978
1073
|
await cancelRecording();
|
|
979
1074
|
throw err;
|
|
980
1075
|
}
|
|
@@ -985,10 +1080,10 @@ async function armRecording(args: {
|
|
|
985
1080
|
await attachSession(session);
|
|
986
1081
|
}
|
|
987
1082
|
|
|
988
|
-
await mountOverlayOnTab(tab.id as number);
|
|
989
1083
|
broadcastOverlayState();
|
|
990
1084
|
await chrome.action.setBadgeBackgroundColor({ color: "#e11d48" });
|
|
991
1085
|
await chrome.action.setBadgeText({ text: "REC" });
|
|
1086
|
+
console.log("[clips-bg] arm: done — countdown, parts:", desiredParts());
|
|
992
1087
|
|
|
993
1088
|
return { ok: true, sessionId, recordingId: created.id, native: true };
|
|
994
1089
|
}
|
|
@@ -998,6 +1093,7 @@ async function armRecording(args: {
|
|
|
998
1093
|
// on-page timer. Idempotent.
|
|
999
1094
|
async function markRecordingStarted() {
|
|
1000
1095
|
if (overlayPhase !== "countdown") return;
|
|
1096
|
+
console.log("[clips-bg] recorder started → phase=recording");
|
|
1001
1097
|
clearCountdownTimer();
|
|
1002
1098
|
overlayPhase = "recording";
|
|
1003
1099
|
overlayBaseElapsedMs = 0;
|
|
@@ -1178,6 +1274,7 @@ async function finishSaving(
|
|
|
1178
1274
|
|
|
1179
1275
|
async function stopRecording() {
|
|
1180
1276
|
const recording = activeNativeRecording;
|
|
1277
|
+
console.log("[clips-bg] stopRecording — active:", !!recording);
|
|
1181
1278
|
if (!recording) return { ok: false, error: "No active Clips recording." };
|
|
1182
1279
|
recording.status = "stopping";
|
|
1183
1280
|
// Keep an overlay up: swap the recording controls/bubble for a "Saving…" card
|
|
@@ -1821,6 +1918,11 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
|
|
1821
1918
|
try {
|
|
1822
1919
|
response = await dispatchRuntimeMessage(message);
|
|
1823
1920
|
} catch (err) {
|
|
1921
|
+
console.error(
|
|
1922
|
+
"[clips-bg] message failed:",
|
|
1923
|
+
(message as { type?: unknown })?.type,
|
|
1924
|
+
err,
|
|
1925
|
+
);
|
|
1824
1926
|
response = {
|
|
1825
1927
|
ok: false,
|
|
1826
1928
|
error: err instanceof Error ? err.message : "Could not use Clips.",
|
|
@@ -1841,6 +1943,13 @@ async function dispatchRuntimeMessage(message: unknown): Promise<unknown> {
|
|
|
1841
1943
|
// Status updates streamed from the offscreen recorder.
|
|
1842
1944
|
if (type === "CLIPS_NATIVE_STATUS") {
|
|
1843
1945
|
const status = message as OffscreenStatusMessage;
|
|
1946
|
+
console.log(
|
|
1947
|
+
"[clips-bg] status:",
|
|
1948
|
+
status.status,
|
|
1949
|
+
status.error ?? "",
|
|
1950
|
+
"phase:",
|
|
1951
|
+
overlayPhase,
|
|
1952
|
+
);
|
|
1844
1953
|
if (
|
|
1845
1954
|
activeNativeRecording &&
|
|
1846
1955
|
activeNativeRecording.sessionId === status.sessionId
|
|
@@ -1909,6 +2018,11 @@ async function dispatchRuntimeMessage(message: unknown): Promise<unknown> {
|
|
|
1909
2018
|
return { ok: true };
|
|
1910
2019
|
}
|
|
1911
2020
|
|
|
2021
|
+
if (type === "CLIPS_PREVIEW") {
|
|
2022
|
+
await startPreview(Boolean((message as { camera?: unknown }).camera));
|
|
2023
|
+
return { ok: true };
|
|
2024
|
+
}
|
|
2025
|
+
|
|
1912
2026
|
switch (type) {
|
|
1913
2027
|
case "CLIPS_POPUP_START":
|
|
1914
2028
|
return handlePopupStart(message as PopupStartMessage);
|
|
@@ -1946,6 +2060,7 @@ async function dispatchRuntimeMessage(message: unknown): Promise<unknown> {
|
|
|
1946
2060
|
// switch tabs (programmatic injection covers tabs opened before the extension
|
|
1947
2061
|
// loaded; declared content scripts cover navigations).
|
|
1948
2062
|
chrome.tabs.onActivated.addListener((info) => {
|
|
2063
|
+
if (!CROSS_TAB_FOLLOW) return; // activeTab-only: overlay stays on the launch tab
|
|
1949
2064
|
void (async () => {
|
|
1950
2065
|
await ensureRestored();
|
|
1951
2066
|
if (overlayPhase === "idle" || !activeNativeRecording) return;
|
|
@@ -1961,6 +2076,12 @@ chrome.action.onClicked.addListener(() => {
|
|
|
1961
2076
|
// Await restore — the click may have woken the worker, leaving the module
|
|
1962
2077
|
// globals empty until this resolves. Without it, Stop silently does nothing.
|
|
1963
2078
|
await ensureRestored();
|
|
2079
|
+
console.log(
|
|
2080
|
+
"[clips-bg] icon clicked — phase:",
|
|
2081
|
+
overlayPhase,
|
|
2082
|
+
"active:",
|
|
2083
|
+
!!activeNativeRecording,
|
|
2084
|
+
);
|
|
1964
2085
|
if (!activeNativeRecording) return;
|
|
1965
2086
|
if (overlayPhase === "countdown") await cancelRecording();
|
|
1966
2087
|
else if (overlayPhase === "recording" || overlayPhase === "paused") {
|
|
@@ -1969,6 +2090,16 @@ chrome.action.onClicked.addListener(() => {
|
|
|
1969
2090
|
})();
|
|
1970
2091
|
});
|
|
1971
2092
|
|
|
2093
|
+
// The popup opens a port for as long as it is visible; its disconnect (popup
|
|
2094
|
+
// closed) is our reliable signal to remove the pre-record preview bubble.
|
|
2095
|
+
chrome.runtime.onConnect.addListener((port) => {
|
|
2096
|
+
if (port.name !== "clips-preview") return;
|
|
2097
|
+
port.onDisconnect.addListener(() => {
|
|
2098
|
+
void chrome.runtime.lastError;
|
|
2099
|
+
void stopPreview();
|
|
2100
|
+
});
|
|
2101
|
+
});
|
|
2102
|
+
|
|
1972
2103
|
chrome.runtime.onMessageExternal.addListener(
|
|
1973
2104
|
(message, sender, sendResponse) => {
|
|
1974
2105
|
void handleExternalMessage(message as ExternalMessage, sender)
|