@agent-native/core 0.70.3 → 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 +13 -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/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/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/changelog/2026-06-23-dragging-the-desktop-camera-bubble-now-glides-to-a-stop-at-t.md +6 -0
- package/corpus/templates/clips/chrome-extension/src/background.ts +130 -32
- package/corpus/templates/clips/chrome-extension/src/content-script.ts +163 -13
- package/corpus/templates/clips/chrome-extension/src/offscreen.ts +195 -34
- 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 +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/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
|
@@ -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();
|
|
962
|
+
|
|
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
|
+
};
|
|
892
972
|
|
|
893
|
-
//
|
|
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)
|
|
@@ -13,6 +13,128 @@
|
|
|
13
13
|
const ALL_PARTS: OverlayPart[] = ["bubble", "countdown", "toolbar", "saving"];
|
|
14
14
|
const flags = window as unknown as { __clipsOverlayHostReady?: boolean };
|
|
15
15
|
|
|
16
|
+
// ----- Draggable, resizable camera bubble ---------------------------------
|
|
17
|
+
// Size + position persist in storage so the bubble stays where the user put it
|
|
18
|
+
// across pages and recordings (like the desktop app). The iframe can't move or
|
|
19
|
+
// resize itself, so the content script owns its geometry.
|
|
20
|
+
const BUBBLE_SIZES: Record<string, number> = { sm: 184, lg: 280 };
|
|
21
|
+
const bubbleGeom: { size: string; left: number | null; top: number | null } =
|
|
22
|
+
{ size: "lg", left: null, top: null };
|
|
23
|
+
let bubbleDragLayer: HTMLDivElement | null = null;
|
|
24
|
+
let bubblePersistTimer: ReturnType<typeof setTimeout> | undefined;
|
|
25
|
+
|
|
26
|
+
function bubbleSizePx(): number {
|
|
27
|
+
return BUBBLE_SIZES[bubbleGeom.size] ?? BUBBLE_SIZES.lg;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function clampBubble(
|
|
31
|
+
left: number,
|
|
32
|
+
top: number,
|
|
33
|
+
size: number,
|
|
34
|
+
): { left: number; top: number } {
|
|
35
|
+
return {
|
|
36
|
+
left: Math.max(8, Math.min(left, window.innerWidth - size - 8)),
|
|
37
|
+
top: Math.max(8, Math.min(top, window.innerHeight - size - 8)),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function applyBubbleGeom(): void {
|
|
42
|
+
const frame = document.getElementById(
|
|
43
|
+
partFrameId("bubble"),
|
|
44
|
+
) as HTMLIFrameElement | null;
|
|
45
|
+
if (!frame) return;
|
|
46
|
+
const size = bubbleSizePx();
|
|
47
|
+
const margin = 24;
|
|
48
|
+
const base = clampBubble(
|
|
49
|
+
bubbleGeom.left ?? margin,
|
|
50
|
+
bubbleGeom.top ?? window.innerHeight - size - margin,
|
|
51
|
+
size,
|
|
52
|
+
);
|
|
53
|
+
Object.assign(frame.style, {
|
|
54
|
+
left: `${base.left}px`,
|
|
55
|
+
top: `${base.top}px`,
|
|
56
|
+
bottom: "auto",
|
|
57
|
+
width: `${size}px`,
|
|
58
|
+
height: `${size}px`,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function persistBubbleGeom(): void {
|
|
63
|
+
clearTimeout(bubblePersistTimer);
|
|
64
|
+
bubblePersistTimer = setTimeout(() => {
|
|
65
|
+
try {
|
|
66
|
+
chrome.storage.local.set({ bubbleGeom });
|
|
67
|
+
} catch {
|
|
68
|
+
/* ignore */
|
|
69
|
+
}
|
|
70
|
+
}, 200);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function startBubbleDrag(): void {
|
|
74
|
+
if (bubbleDragLayer) return;
|
|
75
|
+
const frame = document.getElementById(
|
|
76
|
+
partFrameId("bubble"),
|
|
77
|
+
) as HTMLIFrameElement | null;
|
|
78
|
+
if (!frame) return;
|
|
79
|
+
// Full-screen capture layer so the pointer keeps tracking after it leaves
|
|
80
|
+
// the small bubble iframe.
|
|
81
|
+
const layer = document.createElement("div");
|
|
82
|
+
Object.assign(layer.style, {
|
|
83
|
+
position: "fixed",
|
|
84
|
+
inset: "0",
|
|
85
|
+
zIndex: "2147483647",
|
|
86
|
+
cursor: "grabbing",
|
|
87
|
+
});
|
|
88
|
+
(document.documentElement || document.body).appendChild(layer);
|
|
89
|
+
bubbleDragLayer = layer;
|
|
90
|
+
const onMove = (e: PointerEvent): void => {
|
|
91
|
+
const size = bubbleSizePx();
|
|
92
|
+
const rect = frame.getBoundingClientRect();
|
|
93
|
+
const next = clampBubble(
|
|
94
|
+
rect.left + e.movementX,
|
|
95
|
+
rect.top + e.movementY,
|
|
96
|
+
size,
|
|
97
|
+
);
|
|
98
|
+
bubbleGeom.left = next.left;
|
|
99
|
+
bubbleGeom.top = next.top;
|
|
100
|
+
Object.assign(frame.style, {
|
|
101
|
+
left: `${next.left}px`,
|
|
102
|
+
top: `${next.top}px`,
|
|
103
|
+
bottom: "auto",
|
|
104
|
+
});
|
|
105
|
+
};
|
|
106
|
+
const onUp = (): void => {
|
|
107
|
+
layer.removeEventListener("pointermove", onMove);
|
|
108
|
+
layer.removeEventListener("pointerup", onUp);
|
|
109
|
+
layer.removeEventListener("pointercancel", onUp);
|
|
110
|
+
layer.remove();
|
|
111
|
+
bubbleDragLayer = null;
|
|
112
|
+
persistBubbleGeom();
|
|
113
|
+
};
|
|
114
|
+
layer.addEventListener("pointermove", onMove);
|
|
115
|
+
layer.addEventListener("pointerup", onUp);
|
|
116
|
+
layer.addEventListener("pointercancel", onUp);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
chrome.storage.local.get("bubbleGeom", (value) => {
|
|
121
|
+
if (chrome.runtime.lastError) return;
|
|
122
|
+
const g = value.bubbleGeom as
|
|
123
|
+
| { size?: unknown; left?: unknown; top?: unknown }
|
|
124
|
+
| undefined;
|
|
125
|
+
if (g && typeof g === "object") {
|
|
126
|
+
bubbleGeom.size = g.size === "sm" ? "sm" : "lg";
|
|
127
|
+
bubbleGeom.left = typeof g.left === "number" ? g.left : null;
|
|
128
|
+
bubbleGeom.top = typeof g.top === "number" ? g.top : null;
|
|
129
|
+
}
|
|
130
|
+
applyBubbleGeom();
|
|
131
|
+
});
|
|
132
|
+
} catch {
|
|
133
|
+
/* ignore */
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
window.addEventListener("resize", () => applyBubbleGeom());
|
|
137
|
+
|
|
16
138
|
function requestState(): void {
|
|
17
139
|
try {
|
|
18
140
|
chrome.runtime.sendMessage(
|
|
@@ -78,21 +200,25 @@
|
|
|
78
200
|
frame.setAttribute("allowtransparency", "true");
|
|
79
201
|
if (part === "bubble") {
|
|
80
202
|
frame.allow = "camera; microphone";
|
|
203
|
+
// Above the countdown so the face stays sharp over the dim/blur. Exact
|
|
204
|
+
// size/position are set by applyBubbleGeom() once mounted.
|
|
205
|
+
const size = bubbleSizePx();
|
|
81
206
|
Object.assign(frame.style, {
|
|
82
207
|
left: "24px",
|
|
83
208
|
bottom: "24px",
|
|
84
|
-
width:
|
|
85
|
-
height:
|
|
209
|
+
width: `${size}px`,
|
|
210
|
+
height: `${size}px`,
|
|
211
|
+
zIndex: "3",
|
|
86
212
|
});
|
|
87
213
|
} else if (part === "toolbar") {
|
|
88
214
|
// Left-edge vertical pill (desktop layout). Height grows on hover via the
|
|
89
215
|
// resize message below.
|
|
90
216
|
Object.assign(frame.style, {
|
|
91
217
|
left: "16px",
|
|
92
|
-
top: "50%",
|
|
93
|
-
transform: "translateY(-50%)",
|
|
218
|
+
top: "calc(50% - 77px)",
|
|
94
219
|
width: "68px",
|
|
95
220
|
height: "154px",
|
|
221
|
+
zIndex: "2",
|
|
96
222
|
});
|
|
97
223
|
} else if (part === "saving") {
|
|
98
224
|
Object.assign(frame.style, {
|
|
@@ -100,12 +226,15 @@
|
|
|
100
226
|
bottom: "24px",
|
|
101
227
|
width: "264px",
|
|
102
228
|
height: "96px",
|
|
229
|
+
zIndex: "2",
|
|
103
230
|
});
|
|
104
231
|
} else {
|
|
232
|
+
// countdown — full-screen dim/blur, below the bubble.
|
|
105
233
|
Object.assign(frame.style, {
|
|
106
234
|
inset: "0",
|
|
107
235
|
width: "100%",
|
|
108
236
|
height: "100%",
|
|
237
|
+
zIndex: "1",
|
|
109
238
|
});
|
|
110
239
|
}
|
|
111
240
|
}
|
|
@@ -114,15 +243,18 @@
|
|
|
114
243
|
if (document.getElementById(partFrameId(part))) return;
|
|
115
244
|
const frame = document.createElement("iframe");
|
|
116
245
|
frame.id = partFrameId(part);
|
|
246
|
+
if (part === "bubble") frame.allow = "camera; microphone";
|
|
117
247
|
const url = new URL(chrome.runtime.getURL("src/overlay.html"));
|
|
118
248
|
url.searchParams.set("part", part);
|
|
119
249
|
if (part === "countdown") url.searchParams.set("seconds", "3");
|
|
120
250
|
frame.src = url.toString();
|
|
121
251
|
styleFrame(frame, part);
|
|
122
252
|
container.appendChild(frame);
|
|
253
|
+
if (part === "bubble") applyBubbleGeom();
|
|
123
254
|
}
|
|
124
255
|
|
|
125
256
|
function reconcile(parts: OverlayPart[]): void {
|
|
257
|
+
console.log("[clips-cs] reconcile parts:", parts, "on", location.href);
|
|
126
258
|
const wanted = new Set(parts.filter((p) => ALL_PARTS.includes(p)));
|
|
127
259
|
if (wanted.size === 0) {
|
|
128
260
|
document.getElementById(CONTAINER_ID)?.remove();
|
|
@@ -157,20 +289,38 @@
|
|
|
157
289
|
}
|
|
158
290
|
});
|
|
159
291
|
|
|
160
|
-
//
|
|
161
|
-
//
|
|
292
|
+
// Overlay iframes post layout requests (toolbar hover-resize, bubble drag and
|
|
293
|
+
// size). Only trust messages from our own extension-origin frames.
|
|
162
294
|
window.addEventListener("message", (event) => {
|
|
163
295
|
const data = event.data as
|
|
164
|
-
| {
|
|
296
|
+
| {
|
|
297
|
+
source?: string;
|
|
298
|
+
kind?: string;
|
|
299
|
+
part?: string;
|
|
300
|
+
height?: number;
|
|
301
|
+
size?: string;
|
|
302
|
+
}
|
|
165
303
|
| undefined;
|
|
166
|
-
if (!data || data.source !== "clips-overlay"
|
|
304
|
+
if (!data || data.source !== "clips-overlay") return;
|
|
305
|
+
if (event.origin !== chrome.runtime.getURL("").replace(/\/$/, "")) return;
|
|
306
|
+
|
|
307
|
+
if (data.kind === "resize" && data.part === "toolbar") {
|
|
308
|
+
const frame = document.getElementById(partFrameId("toolbar"));
|
|
309
|
+
if (frame && typeof data.height === "number") {
|
|
310
|
+
frame.style.height = `${Math.round(data.height)}px`;
|
|
311
|
+
}
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
if (data.kind === "bubble-drag-start") {
|
|
315
|
+
startBubbleDrag();
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
if (data.kind === "bubble-size") {
|
|
319
|
+
bubbleGeom.size = data.size === "sm" ? "sm" : "lg";
|
|
320
|
+
applyBubbleGeom();
|
|
321
|
+
persistBubbleGeom();
|
|
167
322
|
return;
|
|
168
323
|
}
|
|
169
|
-
if (event.origin !== chrome.runtime.getURL("").replace(/\/$/, "")) return;
|
|
170
|
-
const part = data.part === "toolbar" ? "toolbar" : null;
|
|
171
|
-
if (!part || typeof data.height !== "number") return;
|
|
172
|
-
const frame = document.getElementById(partFrameId(part));
|
|
173
|
-
if (frame) frame.style.height = `${Math.round(data.height)}px`;
|
|
174
324
|
});
|
|
175
325
|
|
|
176
326
|
syncIfRecording();
|