@agent-native/core 0.70.2 → 0.71.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 +19 -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 +1 -1
- package/corpus/core/src/action.ts +21 -10
- package/corpus/core/src/agent/durable-background.ts +192 -0
- package/corpus/core/src/agent/production-agent.ts +330 -13
- package/corpus/core/src/agent/run-manager.ts +66 -3
- package/corpus/core/src/agent/run-store.ts +129 -23
- package/corpus/core/src/agent/types.ts +20 -0
- package/corpus/core/src/client/AgentPanel.tsx +12 -9
- package/corpus/core/src/client/blocks/library/FileTreeBlock.tsx +72 -14
- 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 +22 -13
- package/corpus/core/src/server/agent-chat-plugin.ts +133 -66
- 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/app/components/dashboard/SqlChart.tsx +33 -6
- 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/clips/app/components/recorder/recorder-engine.ts +8 -2
- package/corpus/templates/clips/app/routes/r.$recordingId.tsx +1 -9
- 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-screen-recordings-now-capture-at-a-crisp-1080p-bitrate-inste.md +6 -0
- package/corpus/templates/clips/chrome-extension/package.json +1 -0
- package/corpus/templates/clips/chrome-extension/scripts/dev.ts +102 -0
- package/corpus/templates/clips/chrome-extension/src/background.ts +164 -32
- package/corpus/templates/clips/chrome-extension/src/content-script.ts +163 -13
- package/corpus/templates/clips/chrome-extension/src/offscreen.ts +198 -35
- package/corpus/templates/clips/chrome-extension/src/overlay.css +73 -23
- package/corpus/templates/clips/chrome-extension/src/overlay.ts +82 -13
- package/corpus/templates/clips/chrome-extension/src/popup.html +62 -5
- package/corpus/templates/clips/chrome-extension/src/popup.ts +290 -1
- package/corpus/templates/clips/chrome-extension/src/styles.css +34 -0
- package/corpus/templates/clips/desktop/src/lib/audio-cue.ts +21 -16
- package/corpus/templates/clips/desktop/src/lib/recorder.ts +17 -20
- 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/desktop/src-tauri/src/native_screen.rs +2 -2
- package/corpus/templates/clips/server/routes/api/video/[recordingId].get.ts +9 -1
- package/corpus/templates/clips/shared/upload-limits.ts +5 -2
- package/corpus/templates/plan/app/pages/PlansPage.tsx +11 -7
- package/corpus/templates/plan/changelog/2026-06-23-plan-loading-skeleton-no-longer-cuts-off-the-canvas-preview-.md +6 -0
- package/dist/action.js +21 -10
- package/dist/action.js.map +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/production-agent.d.ts +20 -0
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +292 -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 +124 -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/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/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/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 +23 -9
- package/dist/mcp/build-server.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/docs/content/multi-app-workspace.md +2 -2
- package/docs/design/durable-agent-runs.md +458 -4
- package/package.json +1 -1
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dev loop with auto-reload for the unpacked extension.
|
|
3
|
+
*
|
|
4
|
+
* MV3 has no built-in hot reload for unpacked extensions, so this script:
|
|
5
|
+
* 1. runs `vite build --watch` (rebuilds dist/ on every source change), and
|
|
6
|
+
* 2. serves a tiny localhost stream that emits "reload" after each rebuild.
|
|
7
|
+
*
|
|
8
|
+
* The background service worker (in dev / unpacked only) holds that stream open
|
|
9
|
+
* and calls chrome.runtime.reload() when it sees "reload" — so saving a file
|
|
10
|
+
* rebuilds AND reloads the extension with no clicking in chrome://extensions.
|
|
11
|
+
* The open fetch also keeps the worker alive while you iterate.
|
|
12
|
+
*/
|
|
13
|
+
import { spawn } from "node:child_process";
|
|
14
|
+
import http from "node:http";
|
|
15
|
+
import { watch } from "node:fs";
|
|
16
|
+
import { dirname, resolve } from "node:path";
|
|
17
|
+
import { fileURLToPath } from "node:url";
|
|
18
|
+
|
|
19
|
+
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
20
|
+
const distDir = resolve(root, "dist");
|
|
21
|
+
const PORT = 8123;
|
|
22
|
+
|
|
23
|
+
const clients = new Set<http.ServerResponse>();
|
|
24
|
+
|
|
25
|
+
const server = http.createServer((req, res) => {
|
|
26
|
+
if (req.url === "/dev-reload-stream") {
|
|
27
|
+
res.writeHead(200, {
|
|
28
|
+
"Content-Type": "text/plain; charset=utf-8",
|
|
29
|
+
"Cache-Control": "no-store",
|
|
30
|
+
"Access-Control-Allow-Origin": "*",
|
|
31
|
+
Connection: "keep-alive",
|
|
32
|
+
});
|
|
33
|
+
res.write("connected\n");
|
|
34
|
+
clients.add(res);
|
|
35
|
+
req.on("close", () => clients.delete(res));
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
res.writeHead(404);
|
|
39
|
+
res.end();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
server.on("error", (err) => {
|
|
43
|
+
console.error(`[clips-dev] reload server error:`, err);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
server.listen(PORT, () => {
|
|
47
|
+
console.log(`[clips-dev] reload server → http://localhost:${PORT}`);
|
|
48
|
+
console.log(
|
|
49
|
+
`[clips-dev] load ${distDir} as an unpacked extension; it auto-reloads on rebuild.`,
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
let debounce: ReturnType<typeof setTimeout> | undefined;
|
|
54
|
+
function notifyReload(): void {
|
|
55
|
+
clearTimeout(debounce);
|
|
56
|
+
debounce = setTimeout(() => {
|
|
57
|
+
if (clients.size === 0) return;
|
|
58
|
+
for (const res of clients) {
|
|
59
|
+
try {
|
|
60
|
+
res.write("reload\n");
|
|
61
|
+
} catch {
|
|
62
|
+
/* client went away */
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
console.log(
|
|
66
|
+
`[clips-dev] rebuild → reload sent to ${clients.size} client(s)`,
|
|
67
|
+
);
|
|
68
|
+
}, 250);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Vite owns the actual build; we watch its output so the reload fires only after
|
|
72
|
+
// dist/ is fully rewritten.
|
|
73
|
+
const vite = spawn("pnpm", ["exec", "vite", "build", "--watch"], {
|
|
74
|
+
cwd: root,
|
|
75
|
+
stdio: "inherit",
|
|
76
|
+
});
|
|
77
|
+
vite.on("exit", (code) => {
|
|
78
|
+
console.log(`[clips-dev] vite exited (${code ?? 0})`);
|
|
79
|
+
server.close();
|
|
80
|
+
process.exit(code ?? 0);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
let watching = false;
|
|
84
|
+
function startWatchingDist(): void {
|
|
85
|
+
if (watching) return;
|
|
86
|
+
try {
|
|
87
|
+
watch(distDir, { recursive: true }, () => notifyReload());
|
|
88
|
+
watching = true;
|
|
89
|
+
} catch {
|
|
90
|
+
// dist may not exist yet on the very first run — retry shortly.
|
|
91
|
+
setTimeout(startWatchingDist, 500);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
startWatchingDist();
|
|
95
|
+
|
|
96
|
+
for (const signal of ["SIGINT", "SIGTERM"] as const) {
|
|
97
|
+
process.on(signal, () => {
|
|
98
|
+
vite.kill();
|
|
99
|
+
server.close();
|
|
100
|
+
process.exit(0);
|
|
101
|
+
});
|
|
102
|
+
}
|
|
@@ -210,7 +210,11 @@ 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;
|
|
214
218
|
let countdownEndsAtMs = 0;
|
|
215
219
|
|
|
216
220
|
function desiredParts(): OverlayPart[] {
|
|
@@ -221,7 +225,7 @@ function desiredParts(): OverlayPart[] {
|
|
|
221
225
|
return overlayShowsBubble ? ["bubble", "countdown"] : ["countdown"];
|
|
222
226
|
}
|
|
223
227
|
if (overlayPhase === "recording" || overlayPhase === "paused") {
|
|
224
|
-
return
|
|
228
|
+
return recordingShowsBubble ? ["bubble", "toolbar"] : ["toolbar"];
|
|
225
229
|
}
|
|
226
230
|
if (overlayPhase === "saving") {
|
|
227
231
|
return ["saving"];
|
|
@@ -286,6 +290,7 @@ function persistOverlay(): Promise<void> {
|
|
|
286
290
|
baseElapsedMs: overlayBaseElapsedMs,
|
|
287
291
|
baseEpochMs: overlayBaseEpochMs,
|
|
288
292
|
showsBubble: overlayShowsBubble,
|
|
293
|
+
recordingShowsBubble,
|
|
289
294
|
countdownEndsAtMs,
|
|
290
295
|
},
|
|
291
296
|
}).catch(() => undefined);
|
|
@@ -306,6 +311,7 @@ async function restoreRuntimeState(): Promise<void> {
|
|
|
306
311
|
baseElapsedMs?: number;
|
|
307
312
|
baseEpochMs?: number;
|
|
308
313
|
showsBubble?: boolean;
|
|
314
|
+
recordingShowsBubble?: boolean;
|
|
309
315
|
countdownEndsAtMs?: number;
|
|
310
316
|
}
|
|
311
317
|
| undefined;
|
|
@@ -316,6 +322,7 @@ async function restoreRuntimeState(): Promise<void> {
|
|
|
316
322
|
overlayBaseEpochMs =
|
|
317
323
|
typeof rt.baseEpochMs === "number" ? rt.baseEpochMs : 0;
|
|
318
324
|
overlayShowsBubble = Boolean(rt.showsBubble);
|
|
325
|
+
recordingShowsBubble = Boolean(rt.recordingShowsBubble);
|
|
319
326
|
countdownEndsAtMs =
|
|
320
327
|
typeof rt.countdownEndsAtMs === "number" ? rt.countdownEndsAtMs : 0;
|
|
321
328
|
}
|
|
@@ -413,6 +420,7 @@ function resetOverlay(): void {
|
|
|
413
420
|
overlayBaseElapsedMs = 0;
|
|
414
421
|
overlayBaseEpochMs = 0;
|
|
415
422
|
overlayShowsBubble = false;
|
|
423
|
+
recordingShowsBubble = false;
|
|
416
424
|
clearCountdownTimer();
|
|
417
425
|
setRecordingFlag(false);
|
|
418
426
|
// Bring back the toolbar popup now that we're idle again.
|
|
@@ -420,6 +428,41 @@ function resetOverlay(): void {
|
|
|
420
428
|
void persistOverlay();
|
|
421
429
|
}
|
|
422
430
|
|
|
431
|
+
// ----- Pre-record camera preview --------------------------------------------
|
|
432
|
+
// While the popup is open and idle, show the live face bubble on the active tab
|
|
433
|
+
// (like the desktop app's pre-record bubble). The popup holds a runtime port
|
|
434
|
+
// open; its disconnect (popup closed) tears the preview down.
|
|
435
|
+
let previewTabId: number | null = null;
|
|
436
|
+
|
|
437
|
+
async function startPreview(wantsCamera: boolean): Promise<void> {
|
|
438
|
+
await ensureRestored();
|
|
439
|
+
// Never show a preview over an active recording (the recording overlay owns
|
|
440
|
+
// the tab then). If the camera is off, make sure any preview is removed.
|
|
441
|
+
if (overlayPhase !== "idle" || !wantsCamera) {
|
|
442
|
+
await stopPreview();
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
const tab = await queryActiveTab();
|
|
446
|
+
if (!tab || typeof tab.id !== "number") return;
|
|
447
|
+
if (previewTabId !== null && previewTabId !== tab.id) await stopPreview();
|
|
448
|
+
previewTabId = tab.id;
|
|
449
|
+
await ensureContentScript(tab.id);
|
|
450
|
+
// Injection / send fail silently on unsupported pages (chrome://, etc.).
|
|
451
|
+
await sendTabMessage(tab.id, {
|
|
452
|
+
type: "CLIPS_OVERLAY_MOUNT",
|
|
453
|
+
parts: ["bubble"],
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
async function stopPreview(): Promise<void> {
|
|
458
|
+
const tabId = previewTabId;
|
|
459
|
+
previewTabId = null;
|
|
460
|
+
if (tabId === null) return;
|
|
461
|
+
// Don't tear down a recording overlay that took over this tab.
|
|
462
|
+
if (overlayPhase !== "idle") return;
|
|
463
|
+
await sendTabMessage(tabId, { type: "CLIPS_OVERLAY_UNMOUNT" });
|
|
464
|
+
}
|
|
465
|
+
|
|
423
466
|
// Mirrored into chrome.storage.local so content scripts can cheaply tell whether
|
|
424
467
|
// a recording is in progress without waking the service worker on every page
|
|
425
468
|
// load. Content scripts can read storage.local by default; storage.session
|
|
@@ -875,6 +918,7 @@ async function armRecording(args: {
|
|
|
875
918
|
settings.captureSurface === "camera" ? "camera" : "screen";
|
|
876
919
|
const surface: "browser" | "window" | "monitor" =
|
|
877
920
|
settings.captureSurface === "camera" ? "browser" : settings.captureSurface;
|
|
921
|
+
console.log("[clips-bg] arm: start", { mode, surface, tabId: tab.id });
|
|
878
922
|
|
|
879
923
|
// Pre-warmed on popup open, so this is effectively instant and keeps the
|
|
880
924
|
// getDisplayMedia() call close to the user's click.
|
|
@@ -882,15 +926,51 @@ async function armRecording(args: {
|
|
|
882
926
|
|
|
883
927
|
// 1) Native "Choose what to share" picker. Do this before any network round
|
|
884
928
|
// trip so the picker stays tied to the user gesture. Throws if cancelled.
|
|
885
|
-
await sendOffscreenMessage<{
|
|
929
|
+
const acq = await sendOffscreenMessage<{
|
|
930
|
+
ok: boolean;
|
|
931
|
+
width: number;
|
|
932
|
+
height: number;
|
|
933
|
+
}>({
|
|
886
934
|
type: "CLIPS_OFFSCREEN_ACQUIRE",
|
|
887
935
|
sessionId,
|
|
888
936
|
mode,
|
|
889
937
|
surface,
|
|
890
938
|
includeMicrophone: settings.includeMicrophone,
|
|
939
|
+
includeCamera: settings.includeCamera,
|
|
891
940
|
});
|
|
941
|
+
console.log("[clips-bg] arm: acquired stream", acq);
|
|
942
|
+
|
|
943
|
+
// 2) Show the countdown overlay IMMEDIATELY — before the network round-trip —
|
|
944
|
+
// so the 3-2-1 runs full-length and the dim/blur clears exactly when the
|
|
945
|
+
// recorder starts. The on-page bubble shows during countdown AND recording
|
|
946
|
+
// (the face is captured in the display; we do not composite).
|
|
947
|
+
const cameraInvolved = mode === "camera" || settings.includeCamera;
|
|
948
|
+
overlayPhase = "countdown";
|
|
949
|
+
overlayBaseElapsedMs = 0;
|
|
950
|
+
overlayBaseEpochMs = nowMs();
|
|
951
|
+
overlayShowsBubble = cameraInvolved;
|
|
952
|
+
recordingShowsBubble = cameraInvolved;
|
|
953
|
+
countdownEndsAtMs = nowMs() + COUNTDOWN_SECONDS * 1000;
|
|
954
|
+
// The recording overlay now owns this tab's bubble — hand off from any preview
|
|
955
|
+
// so the popup-close disconnect won't tear down the live recording overlay.
|
|
956
|
+
previewTabId = null;
|
|
957
|
+
setRecordingFlag(true);
|
|
958
|
+
// Clicking the icon now stops & saves immediately instead of opening the popup.
|
|
959
|
+
setActionPopup("");
|
|
960
|
+
await mountOverlayOnTab(tab.id as number);
|
|
961
|
+
broadcastOverlayState();
|
|
892
962
|
|
|
893
|
-
|
|
963
|
+
const abortArming = async (): Promise<void> => {
|
|
964
|
+
await sendOffscreenMessage({
|
|
965
|
+
type: "CLIPS_OFFSCREEN_CANCEL",
|
|
966
|
+
sessionId,
|
|
967
|
+
}).catch(() => undefined);
|
|
968
|
+
resetOverlay();
|
|
969
|
+
await broadcastUnmount();
|
|
970
|
+
await chrome.action.setBadgeText({ text: "" });
|
|
971
|
+
};
|
|
972
|
+
|
|
973
|
+
// 3) Create the recording row (happens during the countdown).
|
|
894
974
|
type CreatedRecording = {
|
|
895
975
|
id?: string;
|
|
896
976
|
uploadChunkUrl?: string;
|
|
@@ -903,23 +983,17 @@ async function armRecording(args: {
|
|
|
903
983
|
titleSource: tab.title ? "context" : "default",
|
|
904
984
|
sourceAppName: "Chrome",
|
|
905
985
|
sourceWindowTitle: tab.title ?? null,
|
|
906
|
-
hasCamera:
|
|
986
|
+
hasCamera: cameraInvolved,
|
|
907
987
|
hasAudio:
|
|
908
988
|
settings.includeMicrophone || settings.captureSurface !== "camera",
|
|
909
989
|
visibility: "public",
|
|
910
990
|
});
|
|
911
991
|
} catch (err) {
|
|
912
|
-
await
|
|
913
|
-
type: "CLIPS_OFFSCREEN_CANCEL",
|
|
914
|
-
sessionId,
|
|
915
|
-
}).catch(() => undefined);
|
|
992
|
+
await abortArming();
|
|
916
993
|
throw err;
|
|
917
994
|
}
|
|
918
995
|
if (!created.id || !created.uploadChunkUrl) {
|
|
919
|
-
await
|
|
920
|
-
type: "CLIPS_OFFSCREEN_CANCEL",
|
|
921
|
-
sessionId,
|
|
922
|
-
}).catch(() => undefined);
|
|
996
|
+
await abortArming();
|
|
923
997
|
throw new Error("Clips did not create a recording target.");
|
|
924
998
|
}
|
|
925
999
|
|
|
@@ -946,35 +1020,24 @@ async function armRecording(args: {
|
|
|
946
1020
|
error: null,
|
|
947
1021
|
};
|
|
948
1022
|
|
|
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.).
|
|
1023
|
+
// 4) Start the recorder when the (already-running) countdown ends. The
|
|
1024
|
+
// offscreen owns the pre-roll timer (a reliable context, unlike the
|
|
1025
|
+
// suspendable worker) and reports "recording" back when it actually starts.
|
|
966
1026
|
const authToken = (await readAuthSession(settings))?.token;
|
|
1027
|
+
console.log("[clips-bg] arm: created row", created.id, "auth?", !!authToken);
|
|
1028
|
+
const startDelayMs = Math.max(0, countdownEndsAtMs - nowMs());
|
|
967
1029
|
try {
|
|
968
1030
|
await sendOffscreenMessage({
|
|
969
1031
|
type: "CLIPS_OFFSCREEN_BEGIN",
|
|
970
1032
|
sessionId,
|
|
971
1033
|
recordingId: created.id,
|
|
972
1034
|
uploadUrl,
|
|
973
|
-
hasCamera:
|
|
974
|
-
startDelayMs
|
|
1035
|
+
hasCamera: cameraInvolved,
|
|
1036
|
+
startDelayMs,
|
|
975
1037
|
authToken,
|
|
976
1038
|
});
|
|
977
1039
|
} catch (err) {
|
|
1040
|
+
console.error("[clips-bg] arm: BEGIN failed", err);
|
|
978
1041
|
await cancelRecording();
|
|
979
1042
|
throw err;
|
|
980
1043
|
}
|
|
@@ -985,10 +1048,10 @@ async function armRecording(args: {
|
|
|
985
1048
|
await attachSession(session);
|
|
986
1049
|
}
|
|
987
1050
|
|
|
988
|
-
await mountOverlayOnTab(tab.id as number);
|
|
989
1051
|
broadcastOverlayState();
|
|
990
1052
|
await chrome.action.setBadgeBackgroundColor({ color: "#e11d48" });
|
|
991
1053
|
await chrome.action.setBadgeText({ text: "REC" });
|
|
1054
|
+
console.log("[clips-bg] arm: done — countdown, parts:", desiredParts());
|
|
992
1055
|
|
|
993
1056
|
return { ok: true, sessionId, recordingId: created.id, native: true };
|
|
994
1057
|
}
|
|
@@ -998,6 +1061,7 @@ async function armRecording(args: {
|
|
|
998
1061
|
// on-page timer. Idempotent.
|
|
999
1062
|
async function markRecordingStarted() {
|
|
1000
1063
|
if (overlayPhase !== "countdown") return;
|
|
1064
|
+
console.log("[clips-bg] recorder started → phase=recording");
|
|
1001
1065
|
clearCountdownTimer();
|
|
1002
1066
|
overlayPhase = "recording";
|
|
1003
1067
|
overlayBaseElapsedMs = 0;
|
|
@@ -1178,6 +1242,7 @@ async function finishSaving(
|
|
|
1178
1242
|
|
|
1179
1243
|
async function stopRecording() {
|
|
1180
1244
|
const recording = activeNativeRecording;
|
|
1245
|
+
console.log("[clips-bg] stopRecording — active:", !!recording);
|
|
1181
1246
|
if (!recording) return { ok: false, error: "No active Clips recording." };
|
|
1182
1247
|
recording.status = "stopping";
|
|
1183
1248
|
// Keep an overlay up: swap the recording controls/bubble for a "Saving…" card
|
|
@@ -1821,6 +1886,11 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
|
|
1821
1886
|
try {
|
|
1822
1887
|
response = await dispatchRuntimeMessage(message);
|
|
1823
1888
|
} catch (err) {
|
|
1889
|
+
console.error(
|
|
1890
|
+
"[clips-bg] message failed:",
|
|
1891
|
+
(message as { type?: unknown })?.type,
|
|
1892
|
+
err,
|
|
1893
|
+
);
|
|
1824
1894
|
response = {
|
|
1825
1895
|
ok: false,
|
|
1826
1896
|
error: err instanceof Error ? err.message : "Could not use Clips.",
|
|
@@ -1841,6 +1911,13 @@ async function dispatchRuntimeMessage(message: unknown): Promise<unknown> {
|
|
|
1841
1911
|
// Status updates streamed from the offscreen recorder.
|
|
1842
1912
|
if (type === "CLIPS_NATIVE_STATUS") {
|
|
1843
1913
|
const status = message as OffscreenStatusMessage;
|
|
1914
|
+
console.log(
|
|
1915
|
+
"[clips-bg] status:",
|
|
1916
|
+
status.status,
|
|
1917
|
+
status.error ?? "",
|
|
1918
|
+
"phase:",
|
|
1919
|
+
overlayPhase,
|
|
1920
|
+
);
|
|
1844
1921
|
if (
|
|
1845
1922
|
activeNativeRecording &&
|
|
1846
1923
|
activeNativeRecording.sessionId === status.sessionId
|
|
@@ -1909,6 +1986,11 @@ async function dispatchRuntimeMessage(message: unknown): Promise<unknown> {
|
|
|
1909
1986
|
return { ok: true };
|
|
1910
1987
|
}
|
|
1911
1988
|
|
|
1989
|
+
if (type === "CLIPS_PREVIEW") {
|
|
1990
|
+
await startPreview(Boolean((message as { camera?: unknown }).camera));
|
|
1991
|
+
return { ok: true };
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1912
1994
|
switch (type) {
|
|
1913
1995
|
case "CLIPS_POPUP_START":
|
|
1914
1996
|
return handlePopupStart(message as PopupStartMessage);
|
|
@@ -1961,6 +2043,12 @@ chrome.action.onClicked.addListener(() => {
|
|
|
1961
2043
|
// Await restore — the click may have woken the worker, leaving the module
|
|
1962
2044
|
// globals empty until this resolves. Without it, Stop silently does nothing.
|
|
1963
2045
|
await ensureRestored();
|
|
2046
|
+
console.log(
|
|
2047
|
+
"[clips-bg] icon clicked — phase:",
|
|
2048
|
+
overlayPhase,
|
|
2049
|
+
"active:",
|
|
2050
|
+
!!activeNativeRecording,
|
|
2051
|
+
);
|
|
1964
2052
|
if (!activeNativeRecording) return;
|
|
1965
2053
|
if (overlayPhase === "countdown") await cancelRecording();
|
|
1966
2054
|
else if (overlayPhase === "recording" || overlayPhase === "paused") {
|
|
@@ -1969,6 +2057,16 @@ chrome.action.onClicked.addListener(() => {
|
|
|
1969
2057
|
})();
|
|
1970
2058
|
});
|
|
1971
2059
|
|
|
2060
|
+
// The popup opens a port for as long as it is visible; its disconnect (popup
|
|
2061
|
+
// closed) is our reliable signal to remove the pre-record preview bubble.
|
|
2062
|
+
chrome.runtime.onConnect.addListener((port) => {
|
|
2063
|
+
if (port.name !== "clips-preview") return;
|
|
2064
|
+
port.onDisconnect.addListener(() => {
|
|
2065
|
+
void chrome.runtime.lastError;
|
|
2066
|
+
void stopPreview();
|
|
2067
|
+
});
|
|
2068
|
+
});
|
|
2069
|
+
|
|
1972
2070
|
chrome.runtime.onMessageExternal.addListener(
|
|
1973
2071
|
(message, sender, sendResponse) => {
|
|
1974
2072
|
void handleExternalMessage(message as ExternalMessage, sender)
|
|
@@ -2027,3 +2125,37 @@ chrome.debugger.onDetach.addListener((source) => {
|
|
|
2027
2125
|
if (session) session.attached = false;
|
|
2028
2126
|
tabToSession.delete(tabId);
|
|
2029
2127
|
});
|
|
2128
|
+
|
|
2129
|
+
// ---- Dev auto-reload (unpacked installs only) ------------------------------
|
|
2130
|
+
// `pnpm dev:hot` runs a localhost server that streams "reload" after each
|
|
2131
|
+
// rebuild; we hold the stream open and reload the extension when it fires, so
|
|
2132
|
+
// edits land without touching chrome://extensions. The open fetch also keeps
|
|
2133
|
+
// this worker alive while iterating. No-ops for packed/Web Store installs
|
|
2134
|
+
// (they have an update_url) and quietly retries when no dev server is running.
|
|
2135
|
+
function startDevHotReload(): void {
|
|
2136
|
+
if ("update_url" in chrome.runtime.getManifest()) return;
|
|
2137
|
+
const url = "http://localhost:8123/dev-reload-stream";
|
|
2138
|
+
const connect = (): void => {
|
|
2139
|
+
fetch(url, { cache: "no-store" })
|
|
2140
|
+
.then(async (res) => {
|
|
2141
|
+
const reader = res.body?.getReader();
|
|
2142
|
+
if (!reader) {
|
|
2143
|
+
setTimeout(connect, 2500);
|
|
2144
|
+
return;
|
|
2145
|
+
}
|
|
2146
|
+
const decoder = new TextDecoder();
|
|
2147
|
+
for (;;) {
|
|
2148
|
+
const { value, done } = await reader.read();
|
|
2149
|
+
if (done) break;
|
|
2150
|
+
if (decoder.decode(value, { stream: true }).includes("reload")) {
|
|
2151
|
+
chrome.runtime.reload();
|
|
2152
|
+
return;
|
|
2153
|
+
}
|
|
2154
|
+
}
|
|
2155
|
+
setTimeout(connect, 1000);
|
|
2156
|
+
})
|
|
2157
|
+
.catch(() => setTimeout(connect, 2500));
|
|
2158
|
+
};
|
|
2159
|
+
connect();
|
|
2160
|
+
}
|
|
2161
|
+
startDevHotReload();
|