@agent-native/core 0.71.0 → 0.72.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +38 -0
- package/corpus/core/docs/content/actions.md +23 -0
- package/corpus/core/docs/content/audit-log.md +111 -0
- package/corpus/core/package.json +2 -1
- package/corpus/core/src/action.ts +80 -1
- package/corpus/core/src/agent/production-agent.ts +27 -0
- package/corpus/core/src/agent/run-store.ts +14 -0
- package/corpus/core/src/application-state/store.ts +5 -0
- package/corpus/core/src/audit/actions/get-audit-event.ts +23 -0
- package/corpus/core/src/audit/actions/list-audit-events.ts +65 -0
- package/corpus/core/src/audit/cleanup-job.ts +100 -0
- package/corpus/core/src/audit/config.ts +91 -0
- package/corpus/core/src/audit/index.ts +43 -0
- package/corpus/core/src/audit/record.ts +143 -0
- package/corpus/core/src/audit/redact.ts +109 -0
- package/corpus/core/src/audit/store.ts +244 -0
- package/corpus/core/src/audit/types.ts +125 -0
- package/corpus/core/src/chat-threads/store.ts +10 -0
- package/corpus/core/src/client/AssistantChat.tsx +6 -0
- package/corpus/core/src/client/chat/repo-helpers.ts +38 -0
- package/corpus/core/src/db/client.ts +4 -0
- package/corpus/core/src/db/widen-columns.ts +75 -0
- package/corpus/core/src/mcp/build-server.ts +1 -0
- package/corpus/core/src/oauth-tokens/store.ts +6 -0
- package/corpus/core/src/provider-api/custom-registry.ts +8 -0
- package/corpus/core/src/resources/store.ts +10 -0
- package/corpus/core/src/scripts/runner.ts +6 -3
- package/corpus/core/src/server/action-discovery.ts +6 -0
- package/corpus/core/src/server/action-routes.ts +1 -0
- package/corpus/core/src/server/auth.ts +5 -0
- package/corpus/core/src/server/core-routes-plugin.ts +14 -0
- package/corpus/core/src/server/security-headers.ts +9 -6
- package/corpus/core/src/settings/store.ts +6 -0
- package/corpus/core/src/templates/workspace-core/.agents/skills/audit-log/SKILL.md +93 -0
- package/corpus/core/src/usage/store.ts +6 -0
- package/corpus/templates/calendar/app/components/calendar/GoogleConnectBanner.tsx +60 -6
- package/corpus/templates/calendar/changelog/2026-06-23-added-a-heads-up-explaining-google-s-app-not-verified-screen.md +6 -0
- package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +20 -43
- package/corpus/templates/clips/app/components/recorder/recording-toolbar.tsx +5 -1
- package/corpus/templates/clips/app/lib/countdown-audio-cue.ts +3 -51
- package/corpus/templates/clips/changelog/2026-06-23-public-clips-now-play-inline-in-slack-connect-a-workspac.md +6 -0
- package/corpus/templates/clips/chrome-extension/PERMISSIONS.md +72 -0
- package/corpus/templates/clips/chrome-extension/public/manifest.json +1 -10
- package/corpus/templates/clips/chrome-extension/src/background.ts +34 -1
- package/corpus/templates/clips/chrome-extension/src/content-script.ts +138 -11
- package/corpus/templates/clips/chrome-extension/src/offscreen.ts +50 -33
- package/corpus/templates/clips/chrome-extension/src/overlay.css +11 -27
- package/corpus/templates/clips/chrome-extension/src/overlay.html +7 -1
- package/corpus/templates/clips/chrome-extension/src/overlay.ts +33 -38
- package/corpus/templates/clips/chrome-extension/src/popup.ts +6 -24
- package/corpus/templates/clips/chrome-extension/vite.config.ts +5 -0
- package/corpus/templates/clips/server/lib/media-permissions.ts +5 -1
- package/corpus/templates/clips/shared/recording-audio.ts +62 -0
- package/corpus/templates/clips/shared/recording-core.ts +94 -0
- package/dist/action.d.ts +31 -0
- package/dist/action.d.ts.map +1 -1
- package/dist/action.js +45 -1
- package/dist/action.js.map +1 -1
- package/dist/agent/context-xray/schema.d.ts +1 -1
- package/dist/agent/observational-memory/schema.d.ts +1 -1
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +28 -0
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/agent/run-store.d.ts.map +1 -1
- package/dist/agent/run-store.js +14 -0
- package/dist/agent/run-store.js.map +1 -1
- package/dist/application-state/store.d.ts.map +1 -1
- package/dist/application-state/store.js +5 -0
- package/dist/application-state/store.js.map +1 -1
- package/dist/audit/actions/get-audit-event.d.ts +11 -0
- package/dist/audit/actions/get-audit-event.d.ts.map +1 -0
- package/dist/audit/actions/get-audit-event.js +22 -0
- package/dist/audit/actions/get-audit-event.js.map +1 -0
- package/dist/audit/actions/list-audit-events.d.ts +22 -0
- package/dist/audit/actions/list-audit-events.d.ts.map +1 -0
- package/dist/audit/actions/list-audit-events.js +61 -0
- package/dist/audit/actions/list-audit-events.js.map +1 -0
- package/dist/audit/cleanup-job.d.ts +12 -0
- package/dist/audit/cleanup-job.d.ts.map +1 -0
- package/dist/audit/cleanup-job.js +93 -0
- package/dist/audit/cleanup-job.js.map +1 -0
- package/dist/audit/config.d.ts +30 -0
- package/dist/audit/config.d.ts.map +1 -0
- package/dist/audit/config.js +65 -0
- package/dist/audit/config.js.map +1 -0
- package/dist/audit/index.d.ts +13 -0
- package/dist/audit/index.d.ts.map +1 -0
- package/dist/audit/index.js +6 -0
- package/dist/audit/index.js.map +1 -0
- package/dist/audit/record.d.ts +24 -0
- package/dist/audit/record.d.ts.map +1 -0
- package/dist/audit/record.js +106 -0
- package/dist/audit/record.js.map +1 -0
- package/dist/audit/redact.d.ts +28 -0
- package/dist/audit/redact.d.ts.map +1 -0
- package/dist/audit/redact.js +109 -0
- package/dist/audit/redact.js.map +1 -0
- package/dist/audit/store.d.ts +14 -0
- package/dist/audit/store.d.ts.map +1 -0
- package/dist/audit/store.js +219 -0
- package/dist/audit/store.js.map +1 -0
- package/dist/audit/types.d.ts +114 -0
- package/dist/audit/types.d.ts.map +1 -0
- package/dist/audit/types.js +15 -0
- package/dist/audit/types.js.map +1 -0
- package/dist/chat-threads/store.d.ts.map +1 -1
- package/dist/chat-threads/store.js +10 -0
- package/dist/chat-threads/store.js.map +1 -1
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +6 -1
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/chat/repo-helpers.d.ts +16 -0
- package/dist/client/chat/repo-helpers.d.ts.map +1 -1
- package/dist/client/chat/repo-helpers.js +40 -0
- package/dist/client/chat/repo-helpers.js.map +1 -1
- package/dist/db/client.d.ts.map +1 -1
- package/dist/db/client.js +3 -0
- package/dist/db/client.js.map +1 -1
- package/dist/db/widen-columns.d.ts +39 -0
- package/dist/db/widen-columns.d.ts.map +1 -0
- package/dist/db/widen-columns.js +73 -0
- package/dist/db/widen-columns.js.map +1 -0
- package/dist/mcp/build-server.d.ts.map +1 -1
- package/dist/mcp/build-server.js +1 -0
- package/dist/mcp/build-server.js.map +1 -1
- package/dist/oauth-tokens/store.d.ts.map +1 -1
- package/dist/oauth-tokens/store.js +6 -0
- package/dist/oauth-tokens/store.js.map +1 -1
- package/dist/provider-api/custom-registry.d.ts.map +1 -1
- package/dist/provider-api/custom-registry.js +8 -0
- package/dist/provider-api/custom-registry.js.map +1 -1
- package/dist/resources/store.d.ts.map +1 -1
- package/dist/resources/store.js +9 -0
- package/dist/resources/store.js.map +1 -1
- package/dist/scripts/runner.js +4 -3
- package/dist/scripts/runner.js.map +1 -1
- package/dist/server/action-discovery.d.ts.map +1 -1
- package/dist/server/action-discovery.js +6 -0
- package/dist/server/action-discovery.js.map +1 -1
- package/dist/server/action-routes.d.ts.map +1 -1
- package/dist/server/action-routes.js +1 -0
- package/dist/server/action-routes.js.map +1 -1
- package/dist/server/auth.d.ts.map +1 -1
- package/dist/server/auth.js +5 -0
- package/dist/server/auth.js.map +1 -1
- package/dist/server/core-routes-plugin.d.ts.map +1 -1
- package/dist/server/core-routes-plugin.js +13 -0
- package/dist/server/core-routes-plugin.js.map +1 -1
- package/dist/server/security-headers.d.ts +8 -5
- package/dist/server/security-headers.d.ts.map +1 -1
- package/dist/server/security-headers.js +9 -6
- package/dist/server/security-headers.js.map +1 -1
- package/dist/settings/store.d.ts.map +1 -1
- package/dist/settings/store.js +6 -0
- package/dist/settings/store.js.map +1 -1
- package/dist/templates/workspace-core/.agents/skills/audit-log/SKILL.md +93 -0
- package/dist/usage/store.d.ts.map +1 -1
- package/dist/usage/store.js +5 -0
- package/dist/usage/store.js.map +1 -1
- package/docs/content/actions.md +23 -0
- package/docs/content/audit-log.md +111 -0
- package/package.json +2 -1
- package/src/templates/workspace-core/.agents/skills/audit-log/SKILL.md +93 -0
|
@@ -1,10 +1,15 @@
|
|
|
1
|
-
// Loom-style in-page overlay host.
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
1
|
+
// Loom-style in-page overlay host. The background service worker injects this
|
|
2
|
+
// into the LAUNCH TAB on demand via chrome.scripting.executeScript (covered by
|
|
3
|
+
// the activeTab permission the user grants when they click the extension), NOT
|
|
4
|
+
// declaratively on every page — that would need broad "<all_urls>" host access
|
|
5
|
+
// and Chrome's in-depth review. So the overlay lives on the tab the recording
|
|
6
|
+
// was started from; it does not follow across tabs unless CROSS_TAB_FOLLOW is
|
|
7
|
+
// re-enabled in background.ts (see PERMISSIONS.md). Wrapped in an IIFE so it
|
|
8
|
+
// emits a single self-contained classic script with no module imports/exports
|
|
9
|
+
// and leaks no names into the shared global scope. Its only job is to
|
|
10
|
+
// mount/unmount the overlay iframes; all UI and control logic lives inside the
|
|
11
|
+
// extension-origin overlay pages (src/overlay.html). The worker is the source of
|
|
12
|
+
// truth for which "parts" are visible and pushes them here.
|
|
8
13
|
|
|
9
14
|
(function clipsOverlayHost() {
|
|
10
15
|
type OverlayPart = "bubble" | "countdown" | "toolbar" | "saving";
|
|
@@ -194,7 +199,7 @@
|
|
|
194
199
|
position: "absolute",
|
|
195
200
|
border: "none",
|
|
196
201
|
background: "transparent",
|
|
197
|
-
colorScheme: "
|
|
202
|
+
colorScheme: "dark",
|
|
198
203
|
pointerEvents: "auto",
|
|
199
204
|
});
|
|
200
205
|
frame.setAttribute("allowtransparency", "true");
|
|
@@ -208,24 +213,39 @@
|
|
|
208
213
|
bottom: "24px",
|
|
209
214
|
width: `${size}px`,
|
|
210
215
|
height: `${size}px`,
|
|
216
|
+
// Clip the IFRAME itself to a circle so the iframe's opaque canvas (which
|
|
217
|
+
// a declared color-scheme always paints, dark or white) can never show as
|
|
218
|
+
// a square box around the bubble.
|
|
219
|
+
borderRadius: "50%",
|
|
220
|
+
overflow: "hidden",
|
|
211
221
|
zIndex: "3",
|
|
212
222
|
});
|
|
213
223
|
} else if (part === "toolbar") {
|
|
214
224
|
// Left-edge vertical pill (desktop layout). Height grows on hover via the
|
|
215
|
-
// resize message below.
|
|
225
|
+
// resize message below. Clipped to the pill's radius (the pill fills the
|
|
226
|
+
// iframe) so the opaque canvas can't show as a box; shadow on the iframe so
|
|
227
|
+
// the clip doesn't cut it.
|
|
216
228
|
Object.assign(frame.style, {
|
|
217
229
|
left: "16px",
|
|
218
230
|
top: "calc(50% - 77px)",
|
|
219
231
|
width: "68px",
|
|
220
232
|
height: "154px",
|
|
233
|
+
borderRadius: "20px",
|
|
234
|
+
overflow: "hidden",
|
|
235
|
+
boxShadow: "0 10px 28px rgba(9, 9, 11, 0.45)",
|
|
221
236
|
zIndex: "2",
|
|
222
237
|
});
|
|
223
238
|
} else if (part === "saving") {
|
|
239
|
+
// Compact card: caption + a single indeterminate bar (no circular spinner).
|
|
240
|
+
// Clipped to the card radius (card fills the iframe) so no canvas box shows.
|
|
224
241
|
Object.assign(frame.style, {
|
|
225
242
|
left: "24px",
|
|
226
243
|
bottom: "24px",
|
|
227
|
-
width: "
|
|
228
|
-
height: "
|
|
244
|
+
width: "240px",
|
|
245
|
+
height: "64px",
|
|
246
|
+
borderRadius: "14px",
|
|
247
|
+
overflow: "hidden",
|
|
248
|
+
boxShadow: "0 10px 28px rgba(9, 9, 11, 0.45)",
|
|
229
249
|
zIndex: "2",
|
|
230
250
|
});
|
|
231
251
|
} else {
|
|
@@ -253,22 +273,124 @@
|
|
|
253
273
|
if (part === "bubble") applyBubbleGeom();
|
|
254
274
|
}
|
|
255
275
|
|
|
276
|
+
// Camera-ready gating + connecting spinner: while the camera connects we show a
|
|
277
|
+
// simple centered spinner and keep the bubble hidden, then reveal the bubble and
|
|
278
|
+
// start the countdown once the feed is live — so the "3" never hangs and there's
|
|
279
|
+
// no half-loaded, un-draggable bubble during the wait. The bubble posts
|
|
280
|
+
// "camera-ready" when its video plays (or fails); a fallback timer proceeds
|
|
281
|
+
// anyway if the camera never connects.
|
|
282
|
+
const CONNECTING_ID = `${CONTAINER_ID}-connecting`;
|
|
283
|
+
let cameraReady = false;
|
|
284
|
+
let countdownDeferred = false;
|
|
285
|
+
let countdownFallbackTimer: ReturnType<typeof setTimeout> | undefined;
|
|
286
|
+
|
|
287
|
+
function showConnecting(container: HTMLDivElement): void {
|
|
288
|
+
if (document.getElementById(CONNECTING_ID)) return;
|
|
289
|
+
const wrap = document.createElement("div");
|
|
290
|
+
wrap.id = CONNECTING_ID;
|
|
291
|
+
Object.assign(wrap.style, {
|
|
292
|
+
position: "absolute",
|
|
293
|
+
inset: "0",
|
|
294
|
+
display: "flex",
|
|
295
|
+
alignItems: "center",
|
|
296
|
+
justifyContent: "center",
|
|
297
|
+
pointerEvents: "none",
|
|
298
|
+
zIndex: "4",
|
|
299
|
+
});
|
|
300
|
+
const chip = document.createElement("div");
|
|
301
|
+
Object.assign(chip.style, {
|
|
302
|
+
display: "flex",
|
|
303
|
+
alignItems: "center",
|
|
304
|
+
justifyContent: "center",
|
|
305
|
+
width: "84px",
|
|
306
|
+
height: "84px",
|
|
307
|
+
borderRadius: "20px",
|
|
308
|
+
background: "rgba(24, 24, 27, 0.92)",
|
|
309
|
+
boxShadow: "0 10px 28px rgba(9, 9, 11, 0.45)",
|
|
310
|
+
});
|
|
311
|
+
const spinner = document.createElement("div");
|
|
312
|
+
Object.assign(spinner.style, {
|
|
313
|
+
width: "40px",
|
|
314
|
+
height: "40px",
|
|
315
|
+
borderRadius: "50%",
|
|
316
|
+
border: "4px solid rgba(255, 255, 255, 0.18)",
|
|
317
|
+
borderTopColor: "rgba(255, 255, 255, 0.92)",
|
|
318
|
+
});
|
|
319
|
+
chip.appendChild(spinner);
|
|
320
|
+
wrap.appendChild(chip);
|
|
321
|
+
container.appendChild(wrap);
|
|
322
|
+
try {
|
|
323
|
+
spinner.animate(
|
|
324
|
+
[{ transform: "rotate(0deg)" }, { transform: "rotate(360deg)" }],
|
|
325
|
+
{ duration: 800, iterations: Infinity },
|
|
326
|
+
);
|
|
327
|
+
} catch {
|
|
328
|
+
/* Web Animations unavailable — a static ring is fine */
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function hideConnecting(): void {
|
|
333
|
+
document.getElementById(CONNECTING_ID)?.remove();
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function setBubbleHidden(hidden: boolean): void {
|
|
337
|
+
const frame = document.getElementById(partFrameId("bubble"));
|
|
338
|
+
if (frame) (frame as HTMLElement).style.visibility = hidden ? "hidden" : "";
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function mountDeferredCountdown(): void {
|
|
342
|
+
countdownDeferred = false;
|
|
343
|
+
clearTimeout(countdownFallbackTimer);
|
|
344
|
+
hideConnecting();
|
|
345
|
+
setBubbleHidden(false);
|
|
346
|
+
const container = document.getElementById(
|
|
347
|
+
CONTAINER_ID,
|
|
348
|
+
) as HTMLDivElement | null;
|
|
349
|
+
if (container && !document.getElementById(partFrameId("countdown"))) {
|
|
350
|
+
mountPart(container, "countdown");
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
256
354
|
function reconcile(parts: OverlayPart[]): void {
|
|
257
355
|
console.log("[clips-cs] reconcile parts:", parts, "on", location.href);
|
|
258
356
|
const wanted = new Set(parts.filter((p) => ALL_PARTS.includes(p)));
|
|
357
|
+
// Leaving the countdown phase (recording / saving / idle): cancel any pending
|
|
358
|
+
// deferred countdown + spinner so a late fallback timer can't pop the "3-2-1"
|
|
359
|
+
// back up over a later overlay (this was the "countdown over the saving card"
|
|
360
|
+
// bug).
|
|
361
|
+
if (!wanted.has("countdown")) {
|
|
362
|
+
countdownDeferred = false;
|
|
363
|
+
clearTimeout(countdownFallbackTimer);
|
|
364
|
+
hideConnecting();
|
|
365
|
+
}
|
|
259
366
|
if (wanted.size === 0) {
|
|
260
367
|
document.getElementById(CONTAINER_ID)?.remove();
|
|
368
|
+
cameraReady = false;
|
|
261
369
|
return;
|
|
262
370
|
}
|
|
263
371
|
const container = ensureContainer();
|
|
372
|
+
const gateCountdown =
|
|
373
|
+
wanted.has("countdown") && wanted.has("bubble") && !cameraReady;
|
|
264
374
|
for (const part of ALL_PARTS) {
|
|
265
375
|
const existing = document.getElementById(partFrameId(part));
|
|
266
376
|
if (wanted.has(part)) {
|
|
377
|
+
// Hold the countdown (showing the spinner) until the camera feed is live.
|
|
378
|
+
if (part === "countdown" && gateCountdown) {
|
|
379
|
+
if (!existing && !countdownDeferred) {
|
|
380
|
+
countdownDeferred = true;
|
|
381
|
+
clearTimeout(countdownFallbackTimer);
|
|
382
|
+
countdownFallbackTimer = setTimeout(mountDeferredCountdown, 12000);
|
|
383
|
+
}
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
267
386
|
if (!existing) mountPart(container, part);
|
|
387
|
+
// Keep the bubble hidden behind the spinner until the feed is live.
|
|
388
|
+
if (part === "bubble" && gateCountdown) setBubbleHidden(true);
|
|
268
389
|
} else if (existing) {
|
|
269
390
|
existing.remove();
|
|
270
391
|
}
|
|
271
392
|
}
|
|
393
|
+
if (gateCountdown) showConnecting(container);
|
|
272
394
|
}
|
|
273
395
|
|
|
274
396
|
// Guard against rare double-injection (SPA soft-reloads re-running the script).
|
|
@@ -311,6 +433,11 @@
|
|
|
311
433
|
}
|
|
312
434
|
return;
|
|
313
435
|
}
|
|
436
|
+
if (data.kind === "camera-ready") {
|
|
437
|
+
cameraReady = true;
|
|
438
|
+
if (countdownDeferred) mountDeferredCountdown();
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
314
441
|
if (data.kind === "bubble-drag-start") {
|
|
315
442
|
startBubbleDrag();
|
|
316
443
|
return;
|
|
@@ -12,6 +12,12 @@
|
|
|
12
12
|
// Lifecycle: ACQUIRE (show picker, hold stream) → BEGIN (start recorder after
|
|
13
13
|
// the countdown) → PAUSE/RESUME → STOP/CANCEL, plus RESTART (discard and start
|
|
14
14
|
// over on the same stream).
|
|
15
|
+
//
|
|
16
|
+
// MIME selection and the chunk-upload URL/param protocol are shared with the web
|
|
17
|
+
// app recorder via @shared/recording-core so the server contract can't drift.
|
|
18
|
+
|
|
19
|
+
import { chunkUploadUrl, pickMimeType } from "@shared/recording-core";
|
|
20
|
+
import { scheduleReadyChime } from "@shared/recording-audio";
|
|
15
21
|
|
|
16
22
|
type CaptureMode = "screen" | "camera";
|
|
17
23
|
|
|
@@ -128,20 +134,6 @@ function reportStatus(
|
|
|
128
134
|
});
|
|
129
135
|
}
|
|
130
136
|
|
|
131
|
-
function chooseMimeType(): string {
|
|
132
|
-
const preferred = [
|
|
133
|
-
"video/webm;codecs=vp9,opus",
|
|
134
|
-
"video/webm;codecs=vp8,opus",
|
|
135
|
-
"video/webm;codecs=opus",
|
|
136
|
-
"video/webm",
|
|
137
|
-
];
|
|
138
|
-
if (typeof MediaRecorder === "undefined") return "video/webm";
|
|
139
|
-
return (
|
|
140
|
-
preferred.find((type) => MediaRecorder.isTypeSupported(type)) ??
|
|
141
|
-
"video/webm"
|
|
142
|
-
);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
137
|
function waitForMetadata(video: HTMLVideoElement): Promise<void> {
|
|
146
138
|
if (video.readyState >= HTMLMediaElement.HAVE_METADATA) {
|
|
147
139
|
return Promise.resolve();
|
|
@@ -358,21 +350,6 @@ async function createMixedAudio(
|
|
|
358
350
|
return { audioContext, tracks: destination.stream.getAudioTracks() };
|
|
359
351
|
}
|
|
360
352
|
|
|
361
|
-
function appendUploadParams(
|
|
362
|
-
uploadUrl: string,
|
|
363
|
-
params: Record<string, string | number | boolean | undefined>,
|
|
364
|
-
): string {
|
|
365
|
-
const url = new URL(uploadUrl);
|
|
366
|
-
for (const [key, value] of Object.entries(params)) {
|
|
367
|
-
if (value === undefined) continue;
|
|
368
|
-
url.searchParams.set(
|
|
369
|
-
key,
|
|
370
|
-
typeof value === "boolean" ? (value ? "1" : "0") : String(value),
|
|
371
|
-
);
|
|
372
|
-
}
|
|
373
|
-
return url.toString();
|
|
374
|
-
}
|
|
375
|
-
|
|
376
353
|
async function uploadChunk(
|
|
377
354
|
recording: ActiveRecording,
|
|
378
355
|
blob: Blob,
|
|
@@ -387,10 +364,10 @@ async function uploadChunk(
|
|
|
387
364
|
hasCamera?: boolean;
|
|
388
365
|
} = {},
|
|
389
366
|
): Promise<UploadResult> {
|
|
390
|
-
const url =
|
|
367
|
+
const url = chunkUploadUrl(recording.uploadUrl, {
|
|
391
368
|
index,
|
|
392
369
|
total: extra.total,
|
|
393
|
-
isFinal: extra.isFinal
|
|
370
|
+
isFinal: extra.isFinal,
|
|
394
371
|
mimeType: recording.mimeType,
|
|
395
372
|
durationMs: extra.durationMs,
|
|
396
373
|
width: extra.width,
|
|
@@ -573,7 +550,7 @@ async function begin(message: BeginMessage): Promise<{
|
|
|
573
550
|
const mixedAudio = await createMixedAudio(audioInputs);
|
|
574
551
|
|
|
575
552
|
const outputStream = new MediaStream([videoTrack, ...mixedAudio.tracks]);
|
|
576
|
-
const mimeType =
|
|
553
|
+
const mimeType = pickMimeType() || "video/webm";
|
|
577
554
|
const recorder = new MediaRecorder(outputStream, {
|
|
578
555
|
mimeType,
|
|
579
556
|
// Crisp 1080p capture — matches the web/desktop recorders. Files upload
|
|
@@ -640,6 +617,10 @@ async function begin(message: BeginMessage): Promise<{
|
|
|
640
617
|
return;
|
|
641
618
|
}
|
|
642
619
|
const index = recording.chunkIndex++;
|
|
620
|
+
// Record the failure and stop, but do NOT re-throw: re-throwing leaves a
|
|
621
|
+
// rejected promise that surfaces as an "Uncaught (in promise)" error (bad
|
|
622
|
+
// look in a Chrome Web Store review). finalizeStop reads recording.upload-
|
|
623
|
+
// Failure and surfaces it through the normal error path instead.
|
|
643
624
|
const upload = uploadChunk(recording, event.data, index).catch((err) => {
|
|
644
625
|
recording.uploadFailure =
|
|
645
626
|
err instanceof Error ? err : new Error(String(err));
|
|
@@ -647,7 +628,6 @@ async function begin(message: BeginMessage): Promise<{
|
|
|
647
628
|
error: recording.uploadFailure.message,
|
|
648
629
|
});
|
|
649
630
|
if (recorder.state !== "inactive") recorder.stop();
|
|
650
|
-
throw recording.uploadFailure;
|
|
651
631
|
});
|
|
652
632
|
recording.uploadPromises.push(upload);
|
|
653
633
|
});
|
|
@@ -677,9 +657,25 @@ async function begin(message: BeginMessage): Promise<{
|
|
|
677
657
|
};
|
|
678
658
|
}
|
|
679
659
|
|
|
660
|
+
// The canonical Clips "ready" chime when recording starts — shared with the web
|
|
661
|
+
// app recorder (and matching the desktop app) so every surface sounds the same.
|
|
662
|
+
function playStartChime(): void {
|
|
663
|
+
try {
|
|
664
|
+
const ctx = new AudioContext();
|
|
665
|
+
void ctx.resume().catch(() => undefined);
|
|
666
|
+
void scheduleReadyChime(ctx).finally(() => {
|
|
667
|
+
void ctx.close().catch(() => undefined);
|
|
668
|
+
});
|
|
669
|
+
} catch {
|
|
670
|
+
/* audio unavailable */
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
680
674
|
function startRecorderNow(recording: ActiveRecording): void {
|
|
681
675
|
if (recording.cancelled) return;
|
|
682
676
|
try {
|
|
677
|
+
// Chime first (on "Go") so it mostly lands before the recording begins.
|
|
678
|
+
playStartChime();
|
|
683
679
|
recording.recorder.start(2000);
|
|
684
680
|
recording.startedAtMs = Date.now();
|
|
685
681
|
console.log("[clips-offscreen] recorder.start ok");
|
|
@@ -724,6 +720,27 @@ async function finalizeStop(recording: ActiveRecording): Promise<void> {
|
|
|
724
720
|
: new Error(String(rejected.reason));
|
|
725
721
|
}
|
|
726
722
|
const durationMs = Math.max(0, Date.now() - recording.startedAtMs);
|
|
723
|
+
// Surface WHY a finalize might fail before the server's cryptic "No chunks
|
|
724
|
+
// found": 0 chunks means the recorder emitted no non-empty data (empty
|
|
725
|
+
// capture / never started), which is a different problem than an auth 401.
|
|
726
|
+
if (recording.chunkIndex === 0) {
|
|
727
|
+
console.warn(
|
|
728
|
+
"[clips-offscreen] finalizing with 0 chunks — empty recording.",
|
|
729
|
+
"recorderStarted:",
|
|
730
|
+
recording.startedAtMs > 0,
|
|
731
|
+
"durationMs:",
|
|
732
|
+
durationMs,
|
|
733
|
+
"hadAuth:",
|
|
734
|
+
Boolean(recording.authToken),
|
|
735
|
+
);
|
|
736
|
+
} else {
|
|
737
|
+
console.log(
|
|
738
|
+
"[clips-offscreen] finalizing",
|
|
739
|
+
recording.chunkIndex,
|
|
740
|
+
"chunk(s), durationMs:",
|
|
741
|
+
durationMs,
|
|
742
|
+
);
|
|
743
|
+
}
|
|
727
744
|
const result = await uploadChunk(
|
|
728
745
|
recording,
|
|
729
746
|
new Blob([], { type: recording.mimeType }),
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
:root {
|
|
2
|
-
color-scheme
|
|
2
|
+
/* "dark" to match the dark overlay: a declared color-scheme always paints an
|
|
3
|
+
opaque iframe canvas, so we keep it dark rather than white. Shaped parts clip
|
|
4
|
+
the canvas away (see content-script styleFrame); the countdown uses it. */
|
|
5
|
+
color-scheme: dark;
|
|
3
6
|
}
|
|
4
7
|
|
|
5
8
|
* {
|
|
@@ -41,7 +44,6 @@ body {
|
|
|
41
44
|
overflow: hidden;
|
|
42
45
|
background: #18181b;
|
|
43
46
|
border: 3px solid rgba(255, 255, 255, 0.92);
|
|
44
|
-
box-shadow: 0 6px 18px rgba(9, 9, 11, 0.4);
|
|
45
47
|
display: flex;
|
|
46
48
|
align-items: center;
|
|
47
49
|
justify-content: center;
|
|
@@ -176,21 +178,22 @@ body {
|
|
|
176
178
|
}
|
|
177
179
|
|
|
178
180
|
.toolbar-v {
|
|
179
|
-
/*
|
|
180
|
-
|
|
181
|
+
/* Fill the iframe so the (opaque) iframe canvas is fully covered by the pill —
|
|
182
|
+
the iframe is clipped to this same 20px radius in styleFrame, so no canvas
|
|
183
|
+
box shows. Top-anchored content (justify-content:flex-start) keeps Stop/time/
|
|
184
|
+
pause put as the pill grows DOWNWARD on hover (centering caused misclicks).
|
|
185
|
+
Shadow lives on the iframe element so the clip doesn't cut it off. */
|
|
181
186
|
position: absolute;
|
|
182
|
-
|
|
183
|
-
left: 50%;
|
|
184
|
-
transform: translateX(-50%);
|
|
187
|
+
inset: 0;
|
|
185
188
|
display: flex;
|
|
186
189
|
flex-direction: column;
|
|
187
190
|
align-items: center;
|
|
191
|
+
justify-content: flex-start;
|
|
188
192
|
gap: 8px;
|
|
189
193
|
padding: 9px;
|
|
190
194
|
border-radius: 20px;
|
|
191
195
|
background: rgba(24, 24, 27, 0.94);
|
|
192
196
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
193
|
-
box-shadow: 0 10px 28px rgba(9, 9, 11, 0.45);
|
|
194
197
|
color: #fff;
|
|
195
198
|
-webkit-user-select: none;
|
|
196
199
|
user-select: none;
|
|
@@ -322,28 +325,9 @@ body {
|
|
|
322
325
|
border-radius: 14px;
|
|
323
326
|
background: rgba(24, 24, 27, 0.95);
|
|
324
327
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
325
|
-
box-shadow: 0 10px 28px rgba(9, 9, 11, 0.45);
|
|
326
328
|
color: #fff;
|
|
327
329
|
}
|
|
328
330
|
|
|
329
|
-
.saving-card .saving-spinner {
|
|
330
|
-
position: absolute;
|
|
331
|
-
top: 14px;
|
|
332
|
-
right: 14px;
|
|
333
|
-
width: 16px;
|
|
334
|
-
height: 16px;
|
|
335
|
-
border-radius: 50%;
|
|
336
|
-
border: 2px solid rgba(255, 255, 255, 0.25);
|
|
337
|
-
border-top-color: #fff;
|
|
338
|
-
animation: saving-spin 0.8s linear infinite;
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
@keyframes saving-spin {
|
|
342
|
-
to {
|
|
343
|
-
transform: rotate(360deg);
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
|
|
347
331
|
.saving-caption {
|
|
348
332
|
font-size: 14px;
|
|
349
333
|
font-weight: 600;
|
|
@@ -2,7 +2,13 @@
|
|
|
2
2
|
<html lang="en">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
|
-
|
|
5
|
+
<!-- A declared color-scheme always makes Chrome paint an OPAQUE iframe canvas
|
|
6
|
+
(white for light, dark for dark) — it can't be made transparent. The
|
|
7
|
+
overlay is dark-themed, so we use "dark" to keep that canvas dark rather
|
|
8
|
+
than a jarring white. The shaped parts (bubble/toolbar/saving) clip their
|
|
9
|
+
iframe to the content shape so the canvas never shows; only the
|
|
10
|
+
full-screen countdown relies on this dark value. -->
|
|
11
|
+
<meta name="color-scheme" content="dark" />
|
|
6
12
|
<title>Clips overlay</title>
|
|
7
13
|
</head>
|
|
8
14
|
<body>
|
|
@@ -161,12 +161,17 @@ async function initBubble(): Promise<void> {
|
|
|
161
161
|
ring.appendChild(video);
|
|
162
162
|
await video.play().catch(() => undefined);
|
|
163
163
|
console.log("[clips-overlay] camera bubble live");
|
|
164
|
+
// Tell the host the feed is live so it can start the countdown — the "3"
|
|
165
|
+
// shouldn't appear until the camera is actually showing.
|
|
166
|
+
postBubble("camera-ready");
|
|
164
167
|
} catch (err) {
|
|
165
168
|
console.warn("[clips-overlay] camera getUserMedia failed:", err);
|
|
166
169
|
const empty = document.createElement("div");
|
|
167
170
|
empty.className = "bubble-empty";
|
|
168
171
|
empty.innerHTML = ICONS.cameraOff;
|
|
169
172
|
ring.appendChild(empty);
|
|
173
|
+
// Still release the countdown — a blocked/failed camera must not hang it.
|
|
174
|
+
postBubble("camera-ready");
|
|
170
175
|
}
|
|
171
176
|
}
|
|
172
177
|
|
|
@@ -210,43 +215,36 @@ function initCountdown(): void {
|
|
|
210
215
|
wrap.append(controls, hint);
|
|
211
216
|
root.appendChild(wrap);
|
|
212
217
|
|
|
213
|
-
|
|
214
|
-
//
|
|
215
|
-
//
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
218
|
+
// Each number is shown via a CHAINED setTimeout — the next step is scheduled
|
|
219
|
+
// one second after the current one actually renders, not on a fixed interval.
|
|
220
|
+
// This is deliberate: when the camera is slow to connect (e.g. an iPhone
|
|
221
|
+
// Continuity Camera) it can hog the main thread and stall a tick. A setInterval
|
|
222
|
+
// would then fire all the missed ticks back-to-back ("3"… then "2 1 Go" in a
|
|
223
|
+
// burst); chaining means a stall only delays the next number, it never bursts.
|
|
224
|
+
// At "Go" we tell the worker to start the recorder; the worker's own timer is
|
|
225
|
+
// just a fallback for pages where no overlay can be injected.
|
|
226
|
+
const STEP_MS = 1000;
|
|
227
|
+
const steps = ["3", "2", "1", "Go"];
|
|
228
|
+
let doneSent = false;
|
|
229
|
+
|
|
230
|
+
const showStep = (index: number): void => {
|
|
231
|
+
const text = steps[index];
|
|
232
|
+
number.textContent = text;
|
|
233
|
+
number.classList.toggle("countdown-go", text === "Go");
|
|
234
|
+
number.style.animation = "none";
|
|
235
|
+
void number.offsetWidth;
|
|
236
|
+
number.style.animation = "";
|
|
237
|
+
if (text === "Go") {
|
|
238
|
+
if (!doneSent) {
|
|
239
|
+
doneSent = true;
|
|
240
|
+
send("CLIPS_OVERLAY_COUNTDOWN_DONE");
|
|
234
241
|
}
|
|
235
242
|
return;
|
|
236
243
|
}
|
|
237
|
-
|
|
238
|
-
if (text !== lastShown) {
|
|
239
|
-
number.textContent = text;
|
|
240
|
-
number.classList.remove("countdown-go");
|
|
241
|
-
number.style.animation = "none";
|
|
242
|
-
void number.offsetWidth;
|
|
243
|
-
number.style.animation = "";
|
|
244
|
-
lastShown = text;
|
|
245
|
-
}
|
|
244
|
+
window.setTimeout(() => showStep(index + 1), STEP_MS);
|
|
246
245
|
};
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
render();
|
|
246
|
+
|
|
247
|
+
showStep(0); // "3" immediately, then chain "2" → "1" → "Go"
|
|
250
248
|
}
|
|
251
249
|
|
|
252
250
|
/* --------------------------------------------------------------- toolbar --- */
|
|
@@ -341,8 +339,6 @@ function initToolbar(): void {
|
|
|
341
339
|
function initSaving(): void {
|
|
342
340
|
const card = document.createElement("div");
|
|
343
341
|
card.className = "saving-card";
|
|
344
|
-
const spinner = document.createElement("div");
|
|
345
|
-
spinner.className = "saving-spinner";
|
|
346
342
|
const caption = document.createElement("div");
|
|
347
343
|
caption.className = "saving-caption";
|
|
348
344
|
caption.textContent = "Saving clip…";
|
|
@@ -351,7 +347,8 @@ function initSaving(): void {
|
|
|
351
347
|
const fill = document.createElement("div");
|
|
352
348
|
fill.className = "saving-bar-fill";
|
|
353
349
|
bar.appendChild(fill);
|
|
354
|
-
|
|
350
|
+
// One progress indicator only — the indeterminate bar (no circular spinner).
|
|
351
|
+
card.append(caption, bar);
|
|
355
352
|
root.appendChild(card);
|
|
356
353
|
}
|
|
357
354
|
|
|
@@ -371,7 +368,6 @@ const state: OverlayState = {
|
|
|
371
368
|
countdownEndsAtMs: 0,
|
|
372
369
|
};
|
|
373
370
|
let toolbarRender: (() => void) | null = null;
|
|
374
|
-
let countdownRender: (() => void) | null = null;
|
|
375
371
|
|
|
376
372
|
chrome.runtime.onMessage.addListener((message) => {
|
|
377
373
|
if (!message || typeof message !== "object") return;
|
|
@@ -386,7 +382,6 @@ chrome.runtime.onMessage.addListener((message) => {
|
|
|
386
382
|
if (typeof next.countdownEndsAtMs === "number")
|
|
387
383
|
state.countdownEndsAtMs = next.countdownEndsAtMs;
|
|
388
384
|
toolbarRender?.();
|
|
389
|
-
countdownRender?.();
|
|
390
385
|
});
|
|
391
386
|
|
|
392
387
|
if (part === "bubble") void initBubble();
|
|
@@ -834,27 +834,11 @@ async function init(): Promise<void> {
|
|
|
834
834
|
window.setInterval(() => renderActiveRecording(activeRecording), 1000);
|
|
835
835
|
}
|
|
836
836
|
|
|
837
|
-
//
|
|
838
|
-
//
|
|
839
|
-
//
|
|
840
|
-
//
|
|
841
|
-
const
|
|
842
|
-
const syncPreview = (): void => {
|
|
843
|
-
const wantsCamera =
|
|
844
|
-
!activeRecording &&
|
|
845
|
-
!isUnsupportedPage(activeTab?.url) &&
|
|
846
|
-
(settings.captureSurface === "camera" || settings.includeCamera);
|
|
847
|
-
try {
|
|
848
|
-
chrome.runtime.sendMessage(
|
|
849
|
-
{ type: "CLIPS_PREVIEW", camera: wantsCamera },
|
|
850
|
-
() => void chrome.runtime.lastError,
|
|
851
|
-
);
|
|
852
|
-
} catch {
|
|
853
|
-
/* worker asleep; will re-sync on next interaction */
|
|
854
|
-
}
|
|
855
|
-
};
|
|
856
|
-
void previewPort;
|
|
857
|
-
syncPreview();
|
|
837
|
+
// No on-page pre-record preview. A Chrome action popup closes the instant you
|
|
838
|
+
// click the page, so an interactive on-page bubble before recording isn't
|
|
839
|
+
// possible — the face bubble appears only once recording starts. syncPreview
|
|
840
|
+
// is kept as a no-op so the settings handlers below stay unchanged.
|
|
841
|
+
const syncPreview = (): void => {};
|
|
858
842
|
|
|
859
843
|
for (const button of document.querySelectorAll<HTMLButtonElement>(
|
|
860
844
|
".mode-option",
|
|
@@ -1080,7 +1064,7 @@ async function init(): Promise<void> {
|
|
|
1080
1064
|
start.addEventListener("click", async () => {
|
|
1081
1065
|
start.disabled = true;
|
|
1082
1066
|
signIn.hidden = true;
|
|
1083
|
-
setStatus("Checking
|
|
1067
|
+
setStatus(""); // no chatty "Checking…/Starting…" text — the disabled button is enough
|
|
1084
1068
|
authStatus = await readAuthStatus(settings);
|
|
1085
1069
|
if (authStatus === "signed-out") {
|
|
1086
1070
|
start.disabled = false;
|
|
@@ -1089,7 +1073,6 @@ async function init(): Promise<void> {
|
|
|
1089
1073
|
setStatus("");
|
|
1090
1074
|
return;
|
|
1091
1075
|
}
|
|
1092
|
-
setStatus("Checking video storage...");
|
|
1093
1076
|
const storageConfigured = await readVideoStorageConfigured(settings);
|
|
1094
1077
|
if (!storageConfigured) {
|
|
1095
1078
|
start.disabled = false;
|
|
@@ -1112,7 +1095,6 @@ async function init(): Promise<void> {
|
|
|
1112
1095
|
window.close();
|
|
1113
1096
|
return;
|
|
1114
1097
|
}
|
|
1115
|
-
setStatus("Starting recording...");
|
|
1116
1098
|
await saveSettings(settings);
|
|
1117
1099
|
const response = await sendStartMessage(settings);
|
|
1118
1100
|
if (response.ok) {
|
|
@@ -8,6 +8,11 @@ export default defineConfig({
|
|
|
8
8
|
root,
|
|
9
9
|
base: "./",
|
|
10
10
|
publicDir: "public",
|
|
11
|
+
// Share recording primitives with the web app recorder (templates/clips/shared)
|
|
12
|
+
// — matches the "@shared/*" tsconfig path so imports resolve in both builds.
|
|
13
|
+
resolve: {
|
|
14
|
+
alias: { "@shared": resolve(root, "../shared") },
|
|
15
|
+
},
|
|
11
16
|
build: {
|
|
12
17
|
outDir: "dist",
|
|
13
18
|
emptyOutDir: true,
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
// camera=* (not (self)) so the Clips browser extension's camera bubble — injected
|
|
2
|
+
// as a cross-origin iframe — can run on Clips pages too; (self) only allows the
|
|
3
|
+
// app's own same-origin camera. Display-capture stays same-origin. The browser
|
|
4
|
+
// still prompts for camera/mic permission per origin.
|
|
1
5
|
export const MEDIA_CAPTURE_PERMISSIONS_POLICY =
|
|
2
|
-
"camera
|
|
6
|
+
"camera=*, microphone=(self), display-capture=(self), geolocation=(), screen-wake-lock=()";
|
|
3
7
|
|
|
4
8
|
export function withMediaCapturePermissions(response: Response): Response {
|
|
5
9
|
const headers = new Headers(response.headers);
|