@agent-native/core 0.70.3 → 0.72.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +51 -0
- package/corpus/core/docs/content/actions.md +23 -0
- package/corpus/core/docs/content/audit-log.md +111 -0
- package/corpus/core/docs/content/multi-app-workspace.md +2 -2
- package/corpus/core/docs/design/durable-agent-runs.md +458 -4
- package/corpus/core/package.json +2 -1
- package/corpus/core/src/action.ts +80 -1
- package/corpus/core/src/agent/durable-background.ts +192 -0
- package/corpus/core/src/agent/production-agent.ts +357 -13
- package/corpus/core/src/agent/run-manager.ts +66 -3
- package/corpus/core/src/agent/run-store.ts +143 -23
- package/corpus/core/src/agent/types.ts +20 -0
- package/corpus/core/src/application-state/store.ts +5 -0
- package/corpus/core/src/audit/actions/get-audit-event.ts +23 -0
- package/corpus/core/src/audit/actions/list-audit-events.ts +65 -0
- package/corpus/core/src/audit/cleanup-job.ts +100 -0
- package/corpus/core/src/audit/config.ts +91 -0
- package/corpus/core/src/audit/index.ts +43 -0
- package/corpus/core/src/audit/record.ts +143 -0
- package/corpus/core/src/audit/redact.ts +109 -0
- package/corpus/core/src/audit/store.ts +244 -0
- package/corpus/core/src/audit/types.ts +125 -0
- package/corpus/core/src/chat-threads/store.ts +10 -0
- package/corpus/core/src/client/AgentPanel.tsx +12 -9
- package/corpus/core/src/client/AssistantChat.tsx +6 -0
- package/corpus/core/src/client/blocks/library/FileTreeBlock.tsx +72 -14
- package/corpus/core/src/client/chat/repo-helpers.ts +38 -0
- package/corpus/core/src/db/client.ts +4 -0
- package/corpus/core/src/db/widen-columns.ts +75 -0
- package/corpus/core/src/deploy/build.ts +96 -0
- package/corpus/core/src/deploy/workspace-deploy.ts +121 -0
- package/corpus/core/src/mcp/build-server.ts +23 -13
- package/corpus/core/src/oauth-tokens/store.ts +6 -0
- package/corpus/core/src/provider-api/custom-registry.ts +8 -0
- package/corpus/core/src/resources/store.ts +10 -0
- package/corpus/core/src/scripts/runner.ts +6 -3
- package/corpus/core/src/server/action-discovery.ts +6 -0
- package/corpus/core/src/server/action-routes.ts +1 -0
- package/corpus/core/src/server/agent-chat-plugin.ts +133 -66
- package/corpus/core/src/server/auth.ts +5 -0
- package/corpus/core/src/server/core-routes-plugin.ts +14 -0
- package/corpus/core/src/server/security-headers.ts +9 -6
- package/corpus/core/src/settings/store.ts +6 -0
- package/corpus/core/src/templates/workspace-core/.agents/skills/audit-log/SKILL.md +93 -0
- package/corpus/core/src/usage/store.ts +6 -0
- package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +19 -0
- package/corpus/templates/analytics/AGENTS.md +8 -0
- package/corpus/templates/analytics/actions/compose-dashboard.ts +317 -0
- package/corpus/templates/analytics/changelog/2026-06-23-build-large-first-party-analytics-dashboards-in-one-fast-cal.md +6 -0
- package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +574 -0
- package/corpus/templates/calendar/app/components/calendar/GoogleConnectBanner.tsx +60 -6
- package/corpus/templates/calendar/changelog/2026-06-23-added-a-heads-up-explaining-google-s-app-not-verified-screen.md +6 -0
- package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +20 -43
- package/corpus/templates/clips/app/components/recorder/recording-toolbar.tsx +5 -1
- package/corpus/templates/clips/app/lib/countdown-audio-cue.ts +3 -51
- package/corpus/templates/clips/changelog/2026-06-23-dragging-the-desktop-camera-bubble-now-glides-to-a-stop-at-t.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-23-public-clips-now-play-inline-in-slack-connect-a-workspac.md +6 -0
- package/corpus/templates/clips/chrome-extension/PERMISSIONS.md +72 -0
- package/corpus/templates/clips/chrome-extension/public/manifest.json +1 -10
- package/corpus/templates/clips/chrome-extension/src/background.ts +163 -32
- package/corpus/templates/clips/chrome-extension/src/content-script.ts +301 -24
- package/corpus/templates/clips/chrome-extension/src/offscreen.ts +245 -67
- package/corpus/templates/clips/chrome-extension/src/overlay.css +77 -43
- package/corpus/templates/clips/chrome-extension/src/overlay.html +7 -1
- package/corpus/templates/clips/chrome-extension/src/overlay.ts +102 -38
- package/corpus/templates/clips/chrome-extension/src/popup.html +62 -5
- package/corpus/templates/clips/chrome-extension/src/popup.ts +275 -4
- package/corpus/templates/clips/chrome-extension/src/styles.css +34 -0
- package/corpus/templates/clips/chrome-extension/vite.config.ts +5 -0
- package/corpus/templates/clips/desktop/src/lib/audio-cue.ts +21 -16
- package/corpus/templates/clips/desktop/src/lib/recorder.ts +10 -17
- package/corpus/templates/clips/desktop/src/overlays/bubble.tsx +73 -23
- package/corpus/templates/clips/desktop/src/overlays/countdown.tsx +0 -6
- package/corpus/templates/clips/desktop/src/overlays/toolbar.tsx +56 -46
- package/corpus/templates/clips/desktop/src/styles.css +33 -5
- package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +113 -0
- package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +3 -0
- package/corpus/templates/clips/server/lib/media-permissions.ts +5 -1
- package/corpus/templates/clips/shared/recording-audio.ts +62 -0
- package/corpus/templates/clips/shared/recording-core.ts +94 -0
- package/dist/action.d.ts +31 -0
- package/dist/action.d.ts.map +1 -1
- package/dist/action.js +45 -1
- package/dist/action.js.map +1 -1
- package/dist/agent/context-xray/schema.d.ts +1 -1
- package/dist/agent/durable-background.d.ts +60 -0
- package/dist/agent/durable-background.d.ts.map +1 -0
- package/dist/agent/durable-background.js +144 -0
- package/dist/agent/durable-background.js.map +1 -0
- package/dist/agent/observational-memory/schema.d.ts +1 -1
- package/dist/agent/production-agent.d.ts +20 -0
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +320 -14
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/agent/run-manager.d.ts +48 -0
- package/dist/agent/run-manager.d.ts.map +1 -1
- package/dist/agent/run-manager.js +45 -3
- package/dist/agent/run-manager.js.map +1 -1
- package/dist/agent/run-store.d.ts +30 -1
- package/dist/agent/run-store.d.ts.map +1 -1
- package/dist/agent/run-store.js +138 -24
- package/dist/agent/run-store.js.map +1 -1
- package/dist/agent/types.d.ts +20 -0
- package/dist/agent/types.d.ts.map +1 -1
- package/dist/agent/types.js.map +1 -1
- package/dist/application-state/store.d.ts.map +1 -1
- package/dist/application-state/store.js +5 -0
- package/dist/application-state/store.js.map +1 -1
- package/dist/audit/actions/get-audit-event.d.ts +11 -0
- package/dist/audit/actions/get-audit-event.d.ts.map +1 -0
- package/dist/audit/actions/get-audit-event.js +22 -0
- package/dist/audit/actions/get-audit-event.js.map +1 -0
- package/dist/audit/actions/list-audit-events.d.ts +22 -0
- package/dist/audit/actions/list-audit-events.d.ts.map +1 -0
- package/dist/audit/actions/list-audit-events.js +61 -0
- package/dist/audit/actions/list-audit-events.js.map +1 -0
- package/dist/audit/cleanup-job.d.ts +12 -0
- package/dist/audit/cleanup-job.d.ts.map +1 -0
- package/dist/audit/cleanup-job.js +93 -0
- package/dist/audit/cleanup-job.js.map +1 -0
- package/dist/audit/config.d.ts +30 -0
- package/dist/audit/config.d.ts.map +1 -0
- package/dist/audit/config.js +65 -0
- package/dist/audit/config.js.map +1 -0
- package/dist/audit/index.d.ts +13 -0
- package/dist/audit/index.d.ts.map +1 -0
- package/dist/audit/index.js +6 -0
- package/dist/audit/index.js.map +1 -0
- package/dist/audit/record.d.ts +24 -0
- package/dist/audit/record.d.ts.map +1 -0
- package/dist/audit/record.js +106 -0
- package/dist/audit/record.js.map +1 -0
- package/dist/audit/redact.d.ts +28 -0
- package/dist/audit/redact.d.ts.map +1 -0
- package/dist/audit/redact.js +109 -0
- package/dist/audit/redact.js.map +1 -0
- package/dist/audit/store.d.ts +14 -0
- package/dist/audit/store.d.ts.map +1 -0
- package/dist/audit/store.js +219 -0
- package/dist/audit/store.js.map +1 -0
- package/dist/audit/types.d.ts +114 -0
- package/dist/audit/types.d.ts.map +1 -0
- package/dist/audit/types.js +15 -0
- package/dist/audit/types.js.map +1 -0
- package/dist/chat-threads/store.d.ts.map +1 -1
- package/dist/chat-threads/store.js +10 -0
- package/dist/chat-threads/store.js.map +1 -1
- package/dist/client/AgentPanel.d.ts.map +1 -1
- package/dist/client/AgentPanel.js +2 -2
- package/dist/client/AgentPanel.js.map +1 -1
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +6 -1
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/blocks/library/FileTreeBlock.d.ts.map +1 -1
- package/dist/client/blocks/library/FileTreeBlock.js +37 -4
- package/dist/client/blocks/library/FileTreeBlock.js.map +1 -1
- package/dist/client/chat/repo-helpers.d.ts +16 -0
- package/dist/client/chat/repo-helpers.d.ts.map +1 -1
- package/dist/client/chat/repo-helpers.js +40 -0
- package/dist/client/chat/repo-helpers.js.map +1 -1
- package/dist/db/client.d.ts.map +1 -1
- package/dist/db/client.js +3 -0
- package/dist/db/client.js.map +1 -1
- package/dist/db/widen-columns.d.ts +39 -0
- package/dist/db/widen-columns.d.ts.map +1 -0
- package/dist/db/widen-columns.js +73 -0
- package/dist/db/widen-columns.js.map +1 -0
- package/dist/deploy/build.d.ts +29 -0
- package/dist/deploy/build.d.ts.map +1 -1
- package/dist/deploy/build.js +85 -0
- package/dist/deploy/build.js.map +1 -1
- package/dist/deploy/workspace-deploy.d.ts.map +1 -1
- package/dist/deploy/workspace-deploy.js +108 -0
- package/dist/deploy/workspace-deploy.js.map +1 -1
- package/dist/mcp/build-server.d.ts.map +1 -1
- package/dist/mcp/build-server.js +24 -9
- package/dist/mcp/build-server.js.map +1 -1
- package/dist/oauth-tokens/store.d.ts.map +1 -1
- package/dist/oauth-tokens/store.js +6 -0
- package/dist/oauth-tokens/store.js.map +1 -1
- package/dist/provider-api/custom-registry.d.ts.map +1 -1
- package/dist/provider-api/custom-registry.js +8 -0
- package/dist/provider-api/custom-registry.js.map +1 -1
- package/dist/resources/store.d.ts.map +1 -1
- package/dist/resources/store.js +9 -0
- package/dist/resources/store.js.map +1 -1
- package/dist/scripts/runner.js +4 -3
- package/dist/scripts/runner.js.map +1 -1
- package/dist/server/action-discovery.d.ts.map +1 -1
- package/dist/server/action-discovery.js +6 -0
- package/dist/server/action-discovery.js.map +1 -1
- package/dist/server/action-routes.d.ts.map +1 -1
- package/dist/server/action-routes.js +1 -0
- package/dist/server/action-routes.js.map +1 -1
- package/dist/server/agent-chat-plugin.d.ts.map +1 -1
- package/dist/server/agent-chat-plugin.js +68 -13
- package/dist/server/agent-chat-plugin.js.map +1 -1
- package/dist/server/auth.d.ts.map +1 -1
- package/dist/server/auth.js +5 -0
- package/dist/server/auth.js.map +1 -1
- package/dist/server/core-routes-plugin.d.ts.map +1 -1
- package/dist/server/core-routes-plugin.js +13 -0
- package/dist/server/core-routes-plugin.js.map +1 -1
- package/dist/server/security-headers.d.ts +8 -5
- package/dist/server/security-headers.d.ts.map +1 -1
- package/dist/server/security-headers.js +9 -6
- package/dist/server/security-headers.js.map +1 -1
- package/dist/settings/store.d.ts.map +1 -1
- package/dist/settings/store.js +6 -0
- package/dist/settings/store.js.map +1 -1
- package/dist/templates/workspace-core/.agents/skills/audit-log/SKILL.md +93 -0
- package/dist/usage/store.d.ts.map +1 -1
- package/dist/usage/store.js +5 -0
- package/dist/usage/store.js.map +1 -1
- package/docs/content/actions.md +23 -0
- package/docs/content/audit-log.md +111 -0
- package/docs/content/multi-app-workspace.md +2 -2
- package/docs/design/durable-agent-runs.md +458 -4
- package/package.json +2 -1
- package/src/templates/workspace-core/.agents/skills/audit-log/SKILL.md +93 -0
|
@@ -7,6 +7,13 @@ type ExtensionSettings = {
|
|
|
7
7
|
includeCamera: boolean;
|
|
8
8
|
includeMicrophone: boolean;
|
|
9
9
|
includeDeveloperLogs: boolean;
|
|
10
|
+
videoDeviceId: string;
|
|
11
|
+
audioDeviceId: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
type InputDevice = {
|
|
15
|
+
deviceId: string;
|
|
16
|
+
label: string;
|
|
10
17
|
};
|
|
11
18
|
|
|
12
19
|
type PopupStartResponse = {
|
|
@@ -67,6 +74,8 @@ const DEFAULT_SETTINGS: ExtensionSettings = {
|
|
|
67
74
|
includeCamera: true,
|
|
68
75
|
includeMicrophone: true,
|
|
69
76
|
includeDeveloperLogs: true,
|
|
77
|
+
videoDeviceId: "",
|
|
78
|
+
audioDeviceId: "",
|
|
70
79
|
};
|
|
71
80
|
|
|
72
81
|
const SOURCE_LABELS: Record<Exclude<CaptureSurface, "camera">, string> = {
|
|
@@ -167,6 +176,37 @@ async function loadFeedbackSchema(
|
|
|
167
176
|
return pending;
|
|
168
177
|
}
|
|
169
178
|
|
|
179
|
+
// Enumerate the user's input devices for the camera/mic pickers. Labels only
|
|
180
|
+
// populate after camera/mic permission is granted (the extension's permission
|
|
181
|
+
// onboarding page handles that), so fall back to a generic label otherwise.
|
|
182
|
+
async function enumerateInputDevices(): Promise<{
|
|
183
|
+
cameras: InputDevice[];
|
|
184
|
+
microphones: InputDevice[];
|
|
185
|
+
}> {
|
|
186
|
+
try {
|
|
187
|
+
const devices = await navigator.mediaDevices.enumerateDevices();
|
|
188
|
+
const cameras: InputDevice[] = [];
|
|
189
|
+
const microphones: InputDevice[] = [];
|
|
190
|
+
for (const device of devices) {
|
|
191
|
+
if (!device.deviceId) continue;
|
|
192
|
+
if (device.kind === "videoinput") {
|
|
193
|
+
cameras.push({
|
|
194
|
+
deviceId: device.deviceId,
|
|
195
|
+
label: device.label.trim() || `Camera ${cameras.length + 1}`,
|
|
196
|
+
});
|
|
197
|
+
} else if (device.kind === "audioinput") {
|
|
198
|
+
microphones.push({
|
|
199
|
+
deviceId: device.deviceId,
|
|
200
|
+
label: device.label.trim() || `Microphone ${microphones.length + 1}`,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return { cameras, microphones };
|
|
205
|
+
} catch {
|
|
206
|
+
return { cameras: [], microphones: [] };
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
170
210
|
function readSettings(): Promise<ExtensionSettings> {
|
|
171
211
|
return new Promise((resolve) => {
|
|
172
212
|
chrome.storage.sync.get(DEFAULT_SETTINGS, (value) => {
|
|
@@ -188,6 +228,14 @@ function readSettings(): Promise<ExtensionSettings> {
|
|
|
188
228
|
typeof value.includeDeveloperLogs === "boolean"
|
|
189
229
|
? value.includeDeveloperLogs
|
|
190
230
|
: DEFAULT_SETTINGS.includeDeveloperLogs,
|
|
231
|
+
videoDeviceId:
|
|
232
|
+
typeof value.videoDeviceId === "string"
|
|
233
|
+
? value.videoDeviceId
|
|
234
|
+
: DEFAULT_SETTINGS.videoDeviceId,
|
|
235
|
+
audioDeviceId:
|
|
236
|
+
typeof value.audioDeviceId === "string"
|
|
237
|
+
? value.audioDeviceId
|
|
238
|
+
: DEFAULT_SETTINGS.audioDeviceId,
|
|
191
239
|
});
|
|
192
240
|
});
|
|
193
241
|
});
|
|
@@ -401,6 +449,28 @@ function comparableLabel(value: string): string {
|
|
|
401
449
|
.replace(/[^a-z0-9]+/g, "");
|
|
402
450
|
}
|
|
403
451
|
|
|
452
|
+
// Pages where Chrome forbids content-script / overlay injection, so the on-page
|
|
453
|
+
// countdown + controls can't render. Recording the screen still works; we just
|
|
454
|
+
// warn the user in the popup instead of letting it fail silently.
|
|
455
|
+
function isUnsupportedPage(url: string | undefined | null): boolean {
|
|
456
|
+
if (!url) return true;
|
|
457
|
+
const u = url.toLowerCase();
|
|
458
|
+
return (
|
|
459
|
+
u.startsWith("chrome://") ||
|
|
460
|
+
u.startsWith("chrome-extension://") ||
|
|
461
|
+
u.startsWith("edge://") ||
|
|
462
|
+
u.startsWith("brave://") ||
|
|
463
|
+
u.startsWith("arc://") ||
|
|
464
|
+
u.startsWith("about:") ||
|
|
465
|
+
u.startsWith("view-source:") ||
|
|
466
|
+
u.startsWith("devtools://") ||
|
|
467
|
+
u.startsWith("chrome-search://") ||
|
|
468
|
+
u.startsWith("chrome-untrusted://") ||
|
|
469
|
+
u.startsWith("https://chromewebstore.google.com") ||
|
|
470
|
+
u.startsWith("https://chrome.google.com/webstore")
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
|
|
404
474
|
function targetCopy(tab: chrome.tabs.Tab | null): {
|
|
405
475
|
title: string;
|
|
406
476
|
subtitle: string;
|
|
@@ -464,9 +534,111 @@ function renderSource(settings: ExtensionSettings): void {
|
|
|
464
534
|
}
|
|
465
535
|
}
|
|
466
536
|
|
|
537
|
+
// Holds the most recent device enumeration so the menus and labels can render
|
|
538
|
+
// without re-querying on every paint. Refreshed by refreshDevices().
|
|
539
|
+
let inputDevices: { cameras: InputDevice[]; microphones: InputDevice[] } = {
|
|
540
|
+
cameras: [],
|
|
541
|
+
microphones: [],
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
function deviceLabel(
|
|
545
|
+
devices: InputDevice[],
|
|
546
|
+
deviceId: string,
|
|
547
|
+
fallback: string,
|
|
548
|
+
): string {
|
|
549
|
+
if (!deviceId) return fallback;
|
|
550
|
+
const match = devices.find((device) => device.deviceId === deviceId);
|
|
551
|
+
return match ? match.label : fallback;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function renderDeviceMenu(
|
|
555
|
+
menu: HTMLDivElement,
|
|
556
|
+
devices: InputDevice[],
|
|
557
|
+
selectedId: string,
|
|
558
|
+
defaultLabel: string,
|
|
559
|
+
): void {
|
|
560
|
+
menu.replaceChildren();
|
|
561
|
+
const options: InputDevice[] = [
|
|
562
|
+
{ deviceId: "", label: defaultLabel },
|
|
563
|
+
...devices,
|
|
564
|
+
];
|
|
565
|
+
for (const option of options) {
|
|
566
|
+
const item = document.createElement("button");
|
|
567
|
+
item.type = "button";
|
|
568
|
+
item.className = "row-menu-item";
|
|
569
|
+
item.dataset.deviceId = option.deviceId;
|
|
570
|
+
const selected = option.deviceId === selectedId;
|
|
571
|
+
if (selected) {
|
|
572
|
+
item.classList.add("selected");
|
|
573
|
+
item.setAttribute("aria-checked", "true");
|
|
574
|
+
} else {
|
|
575
|
+
item.setAttribute("aria-checked", "false");
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
const check = document.createElement("span");
|
|
579
|
+
check.className = "row-menu-check";
|
|
580
|
+
check.setAttribute("aria-hidden", "true");
|
|
581
|
+
check.textContent = selected ? "✓" : "";
|
|
582
|
+
|
|
583
|
+
const label = document.createElement("span");
|
|
584
|
+
label.className = "row-menu-label";
|
|
585
|
+
label.textContent = option.label;
|
|
586
|
+
|
|
587
|
+
item.append(check, label);
|
|
588
|
+
menu.append(item);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function renderDevicePickers(settings: ExtensionSettings): void {
|
|
593
|
+
const cameraButton = byId<HTMLButtonElement>("camera-device-button");
|
|
594
|
+
const cameraLabel = byId<HTMLSpanElement>("camera-device-label");
|
|
595
|
+
const cameraMenu = byId<HTMLDivElement>("camera-device-menu");
|
|
596
|
+
const showCamera =
|
|
597
|
+
settings.captureSurface === "camera" || settings.includeCamera;
|
|
598
|
+
cameraButton.hidden = !showCamera;
|
|
599
|
+
if (showCamera) {
|
|
600
|
+
cameraLabel.textContent = deviceLabel(
|
|
601
|
+
inputDevices.cameras,
|
|
602
|
+
settings.videoDeviceId,
|
|
603
|
+
"Default camera",
|
|
604
|
+
);
|
|
605
|
+
renderDeviceMenu(
|
|
606
|
+
cameraMenu,
|
|
607
|
+
inputDevices.cameras,
|
|
608
|
+
settings.videoDeviceId,
|
|
609
|
+
"Default camera",
|
|
610
|
+
);
|
|
611
|
+
} else {
|
|
612
|
+
cameraMenu.hidden = true;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
const micRow = byId<HTMLDivElement>("microphone-row");
|
|
616
|
+
const micButton = byId<HTMLButtonElement>("microphone-device-button");
|
|
617
|
+
const micLabel = byId<HTMLSpanElement>("microphone-device-label");
|
|
618
|
+
const micMenu = byId<HTMLDivElement>("microphone-device-menu");
|
|
619
|
+
micButton.hidden = !settings.includeMicrophone;
|
|
620
|
+
if (settings.includeMicrophone) {
|
|
621
|
+
micLabel.textContent = deviceLabel(
|
|
622
|
+
inputDevices.microphones,
|
|
623
|
+
settings.audioDeviceId,
|
|
624
|
+
"Default mic",
|
|
625
|
+
);
|
|
626
|
+
renderDeviceMenu(
|
|
627
|
+
micMenu,
|
|
628
|
+
inputDevices.microphones,
|
|
629
|
+
settings.audioDeviceId,
|
|
630
|
+
"Default mic",
|
|
631
|
+
);
|
|
632
|
+
} else {
|
|
633
|
+
micMenu.hidden = true;
|
|
634
|
+
}
|
|
635
|
+
micRow.hidden = false;
|
|
636
|
+
}
|
|
637
|
+
|
|
467
638
|
function render(settings: ExtensionSettings): void {
|
|
468
639
|
renderMode(settings);
|
|
469
640
|
renderSource(settings);
|
|
641
|
+
renderDevicePickers(settings);
|
|
470
642
|
const includeCamera = byId<HTMLButtonElement>("include-camera");
|
|
471
643
|
const includeMicrophone = byId<HTMLButtonElement>("include-microphone");
|
|
472
644
|
const cameraRow = byId<HTMLDivElement>("camera-row");
|
|
@@ -550,6 +722,12 @@ async function init(): Promise<void> {
|
|
|
550
722
|
}
|
|
551
723
|
const sourceButton = byId<HTMLButtonElement>("source-button");
|
|
552
724
|
const sourceMenu = byId<HTMLDivElement>("source-menu");
|
|
725
|
+
const cameraDeviceButton = byId<HTMLButtonElement>("camera-device-button");
|
|
726
|
+
const cameraDeviceMenu = byId<HTMLDivElement>("camera-device-menu");
|
|
727
|
+
const microphoneDeviceButton = byId<HTMLButtonElement>(
|
|
728
|
+
"microphone-device-button",
|
|
729
|
+
);
|
|
730
|
+
const microphoneDeviceMenu = byId<HTMLDivElement>("microphone-device-menu");
|
|
553
731
|
const includeCamera = byId<HTMLButtonElement>("include-camera");
|
|
554
732
|
const includeMicrophone = byId<HTMLButtonElement>("include-microphone");
|
|
555
733
|
const start = byId<HTMLButtonElement>("start");
|
|
@@ -624,8 +802,30 @@ async function init(): Promise<void> {
|
|
|
624
802
|
window.setTimeout(() => feedbackTextarea.focus(), 30);
|
|
625
803
|
};
|
|
626
804
|
|
|
627
|
-
|
|
805
|
+
// Re-enumerate devices and repaint the pickers. Labels only appear once the
|
|
806
|
+
// user has granted camera/mic access (via the permission onboarding page), so
|
|
807
|
+
// this is also re-run when the device list changes.
|
|
808
|
+
const refreshDevices = async (): Promise<void> => {
|
|
809
|
+
inputDevices = await enumerateInputDevices();
|
|
810
|
+
renderDevicePickers(settings);
|
|
811
|
+
};
|
|
812
|
+
|
|
813
|
+
const closeDeviceMenus = (): void => {
|
|
814
|
+
cameraDeviceMenu.hidden = true;
|
|
815
|
+
microphoneDeviceMenu.hidden = true;
|
|
816
|
+
};
|
|
817
|
+
|
|
818
|
+
const activeTab = await queryActiveTab();
|
|
819
|
+
byId<HTMLDivElement>("unsupported-notice").hidden = !isUnsupportedPage(
|
|
820
|
+
activeTab?.url,
|
|
821
|
+
);
|
|
628
822
|
render(settings);
|
|
823
|
+
void refreshDevices();
|
|
824
|
+
if (navigator.mediaDevices) {
|
|
825
|
+
navigator.mediaDevices.addEventListener("devicechange", () => {
|
|
826
|
+
void refreshDevices();
|
|
827
|
+
});
|
|
828
|
+
}
|
|
629
829
|
const status =
|
|
630
830
|
await sendSimpleMessage<PopupStatusResponse>("CLIPS_POPUP_STATUS");
|
|
631
831
|
activeRecording = status.activeRecording ?? null;
|
|
@@ -634,17 +834,26 @@ async function init(): Promise<void> {
|
|
|
634
834
|
window.setInterval(() => renderActiveRecording(activeRecording), 1000);
|
|
635
835
|
}
|
|
636
836
|
|
|
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 => {};
|
|
842
|
+
|
|
637
843
|
for (const button of document.querySelectorAll<HTMLButtonElement>(
|
|
638
844
|
".mode-option",
|
|
639
845
|
)) {
|
|
640
846
|
button.addEventListener("click", () => {
|
|
641
847
|
applyMode(settings, button.dataset.mode as RecordingModeChoice);
|
|
848
|
+
closeDeviceMenus();
|
|
642
849
|
render(settings);
|
|
643
850
|
void saveSettings(settings);
|
|
851
|
+
syncPreview();
|
|
644
852
|
});
|
|
645
853
|
}
|
|
646
854
|
|
|
647
855
|
sourceButton.addEventListener("click", () => {
|
|
856
|
+
closeDeviceMenus();
|
|
648
857
|
sourceMenu.hidden = !sourceMenu.hidden;
|
|
649
858
|
});
|
|
650
859
|
|
|
@@ -656,9 +865,70 @@ async function init(): Promise<void> {
|
|
|
656
865
|
sourceMenu.hidden = true;
|
|
657
866
|
render(settings);
|
|
658
867
|
void saveSettings(settings);
|
|
868
|
+
syncPreview();
|
|
659
869
|
});
|
|
660
870
|
}
|
|
661
871
|
|
|
872
|
+
cameraDeviceButton.addEventListener("click", () => {
|
|
873
|
+
sourceMenu.hidden = true;
|
|
874
|
+
microphoneDeviceMenu.hidden = true;
|
|
875
|
+
cameraDeviceMenu.hidden = !cameraDeviceMenu.hidden;
|
|
876
|
+
});
|
|
877
|
+
|
|
878
|
+
cameraDeviceMenu.addEventListener("click", (event) => {
|
|
879
|
+
const item = (event.target as HTMLElement).closest<HTMLButtonElement>(
|
|
880
|
+
".row-menu-item",
|
|
881
|
+
);
|
|
882
|
+
if (!item) return;
|
|
883
|
+
settings.videoDeviceId = item.dataset.deviceId ?? "";
|
|
884
|
+
cameraDeviceMenu.hidden = true;
|
|
885
|
+
renderDevicePickers(settings);
|
|
886
|
+
void saveSettings(settings);
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
microphoneDeviceButton.addEventListener("click", () => {
|
|
890
|
+
sourceMenu.hidden = true;
|
|
891
|
+
cameraDeviceMenu.hidden = true;
|
|
892
|
+
microphoneDeviceMenu.hidden = !microphoneDeviceMenu.hidden;
|
|
893
|
+
});
|
|
894
|
+
|
|
895
|
+
microphoneDeviceMenu.addEventListener("click", (event) => {
|
|
896
|
+
const item = (event.target as HTMLElement).closest<HTMLButtonElement>(
|
|
897
|
+
".row-menu-item",
|
|
898
|
+
);
|
|
899
|
+
if (!item) return;
|
|
900
|
+
settings.audioDeviceId = item.dataset.deviceId ?? "";
|
|
901
|
+
microphoneDeviceMenu.hidden = true;
|
|
902
|
+
renderDevicePickers(settings);
|
|
903
|
+
void saveSettings(settings);
|
|
904
|
+
});
|
|
905
|
+
|
|
906
|
+
document.addEventListener("click", (event) => {
|
|
907
|
+
const target = event.target;
|
|
908
|
+
if (!(target instanceof Node)) return;
|
|
909
|
+
if (
|
|
910
|
+
!sourceMenu.hidden &&
|
|
911
|
+
!sourceMenu.contains(target) &&
|
|
912
|
+
!sourceButton.contains(target)
|
|
913
|
+
) {
|
|
914
|
+
sourceMenu.hidden = true;
|
|
915
|
+
}
|
|
916
|
+
if (
|
|
917
|
+
!cameraDeviceMenu.hidden &&
|
|
918
|
+
!cameraDeviceMenu.contains(target) &&
|
|
919
|
+
!cameraDeviceButton.contains(target)
|
|
920
|
+
) {
|
|
921
|
+
cameraDeviceMenu.hidden = true;
|
|
922
|
+
}
|
|
923
|
+
if (
|
|
924
|
+
!microphoneDeviceMenu.hidden &&
|
|
925
|
+
!microphoneDeviceMenu.contains(target) &&
|
|
926
|
+
!microphoneDeviceButton.contains(target)
|
|
927
|
+
) {
|
|
928
|
+
microphoneDeviceMenu.hidden = true;
|
|
929
|
+
}
|
|
930
|
+
});
|
|
931
|
+
|
|
662
932
|
includeCamera.addEventListener("click", () => {
|
|
663
933
|
settings.includeCamera = !settings.includeCamera;
|
|
664
934
|
if (settings.includeCamera && settings.captureSurface === "monitor") {
|
|
@@ -667,12 +937,15 @@ async function init(): Promise<void> {
|
|
|
667
937
|
if (!settings.includeCamera && settings.captureSurface === "camera") {
|
|
668
938
|
settings.captureSurface = "browser";
|
|
669
939
|
}
|
|
940
|
+
closeDeviceMenus();
|
|
670
941
|
render(settings);
|
|
671
942
|
void saveSettings(settings);
|
|
943
|
+
syncPreview();
|
|
672
944
|
});
|
|
673
945
|
|
|
674
946
|
includeMicrophone.addEventListener("click", () => {
|
|
675
947
|
settings.includeMicrophone = !settings.includeMicrophone;
|
|
948
|
+
closeDeviceMenus();
|
|
676
949
|
render(settings);
|
|
677
950
|
void saveSettings(settings);
|
|
678
951
|
});
|
|
@@ -791,7 +1064,7 @@ async function init(): Promise<void> {
|
|
|
791
1064
|
start.addEventListener("click", async () => {
|
|
792
1065
|
start.disabled = true;
|
|
793
1066
|
signIn.hidden = true;
|
|
794
|
-
setStatus("Checking
|
|
1067
|
+
setStatus(""); // no chatty "Checking…/Starting…" text — the disabled button is enough
|
|
795
1068
|
authStatus = await readAuthStatus(settings);
|
|
796
1069
|
if (authStatus === "signed-out") {
|
|
797
1070
|
start.disabled = false;
|
|
@@ -800,7 +1073,6 @@ async function init(): Promise<void> {
|
|
|
800
1073
|
setStatus("");
|
|
801
1074
|
return;
|
|
802
1075
|
}
|
|
803
|
-
setStatus("Checking video storage...");
|
|
804
1076
|
const storageConfigured = await readVideoStorageConfigured(settings);
|
|
805
1077
|
if (!storageConfigured) {
|
|
806
1078
|
start.disabled = false;
|
|
@@ -823,7 +1095,6 @@ async function init(): Promise<void> {
|
|
|
823
1095
|
window.close();
|
|
824
1096
|
return;
|
|
825
1097
|
}
|
|
826
|
-
setStatus("Starting recording...");
|
|
827
1098
|
await saveSettings(settings);
|
|
828
1099
|
const response = await sendStartMessage(settings);
|
|
829
1100
|
if (response.ok) {
|
|
@@ -855,3 +855,37 @@ h1 {
|
|
|
855
855
|
.primary-action:hover {
|
|
856
856
|
opacity: 0.92;
|
|
857
857
|
}
|
|
858
|
+
|
|
859
|
+
/* Unsupported-page notice (shown in the popup when the active tab can't host
|
|
860
|
+
the on-page overlay, e.g. chrome:// / New Tab / Web Store pages). */
|
|
861
|
+
.notice {
|
|
862
|
+
display: flex;
|
|
863
|
+
gap: 10px;
|
|
864
|
+
align-items: flex-start;
|
|
865
|
+
margin: 0 0 12px;
|
|
866
|
+
padding: 11px 12px;
|
|
867
|
+
border: 1px solid var(--border);
|
|
868
|
+
border-radius: var(--radius-sm);
|
|
869
|
+
background: var(--surface);
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
.notice-icon {
|
|
873
|
+
width: 18px;
|
|
874
|
+
height: 18px;
|
|
875
|
+
flex: none;
|
|
876
|
+
margin-top: 1px;
|
|
877
|
+
color: var(--status-warn);
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
.notice-title {
|
|
881
|
+
font-size: 12.5px;
|
|
882
|
+
font-weight: 600;
|
|
883
|
+
color: var(--fg);
|
|
884
|
+
margin-bottom: 2px;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
.notice-text {
|
|
888
|
+
font-size: 11.5px;
|
|
889
|
+
line-height: 1.45;
|
|
890
|
+
color: var(--fg-muted);
|
|
891
|
+
}
|
|
@@ -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,
|
|
@@ -14,12 +14,11 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
export interface AudioCue {
|
|
17
|
-
/** Play the cue during the countdown once recording is about one second out. */
|
|
18
|
-
playCountdownCue(): Promise<void>;
|
|
19
17
|
/**
|
|
20
18
|
* Play the cue (bounded by a timeout) and wait a short settle so the tone
|
|
21
|
-
* sits just before capture rather than inside the recording.
|
|
22
|
-
*
|
|
19
|
+
* sits just before capture rather than inside the recording. Call this at the
|
|
20
|
+
* exact moment recording starts (right before the recorder/native capture is
|
|
21
|
+
* kicked off) so the chime lines up with the real start, not the countdown.
|
|
23
22
|
*/
|
|
24
23
|
playBeforeCapture(): Promise<void>;
|
|
25
24
|
cleanup(): void;
|
|
@@ -38,7 +37,6 @@ function wait(ms: number): Promise<void> {
|
|
|
38
37
|
|
|
39
38
|
/** A cue that does nothing — used when Web Audio is unavailable. */
|
|
40
39
|
const noopAudioCue: AudioCue = {
|
|
41
|
-
async playCountdownCue() {},
|
|
42
40
|
async playBeforeCapture() {},
|
|
43
41
|
cleanup() {},
|
|
44
42
|
};
|
|
@@ -68,19 +66,26 @@ async function playBeforeCapture(
|
|
|
68
66
|
}
|
|
69
67
|
|
|
70
68
|
/**
|
|
71
|
-
* Schedule the recording-start chime on an already-running context
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
69
|
+
* Schedule the recording-start chime on an already-running context.
|
|
70
|
+
*
|
|
71
|
+
* A warm, bell-like rising two-note "ding-dong" (G5 → C6, a perfect fourth)
|
|
72
|
+
* voiced on triangle oscillators for a rounder, less electronic timbre, with a
|
|
73
|
+
* quiet octave overtone (C7) sparkling on top of the second note. Each note has
|
|
74
|
+
* a soft 18ms attack and a long exponential decay so it rings out gently — a
|
|
75
|
+
* confident "you're rolling" confirmation that sounds nothing like a flat
|
|
76
|
+
* countdown blip. Kept under ~420ms so it still fits inside
|
|
77
|
+
* `CUE_PLAY_TIMEOUT_MS` and lands right as capture begins.
|
|
75
78
|
*/
|
|
76
79
|
function scheduleTone(ctx: AudioContext): Promise<void> {
|
|
77
80
|
return new Promise<void>((resolve) => {
|
|
78
81
|
const t0 = ctx.currentTime + 0.005;
|
|
79
82
|
const voices = [
|
|
80
|
-
|
|
81
|
-
{ freq:
|
|
82
|
-
{ freq:
|
|
83
|
-
|
|
83
|
+
// Rising perfect fourth: a gentle "ding … dong".
|
|
84
|
+
{ freq: 783.99, at: 0.0, dur: 0.26, peak: 0.07, type: "triangle" }, // G5
|
|
85
|
+
{ freq: 1046.5, at: 0.11, dur: 0.34, peak: 0.085, type: "triangle" }, // C6
|
|
86
|
+
// Faint octave overtone gives the second note a soft bell shimmer.
|
|
87
|
+
{ freq: 2093.0, at: 0.115, dur: 0.18, peak: 0.022, type: "sine" }, // C7
|
|
88
|
+
] as const;
|
|
84
89
|
|
|
85
90
|
let lastStop = t0;
|
|
86
91
|
for (const voice of voices) {
|
|
@@ -89,12 +94,13 @@ function scheduleTone(ctx: AudioContext): Promise<void> {
|
|
|
89
94
|
lastStop = Math.max(lastStop, stopAt);
|
|
90
95
|
|
|
91
96
|
const oscillator = ctx.createOscillator();
|
|
92
|
-
oscillator.type =
|
|
97
|
+
oscillator.type = voice.type;
|
|
93
98
|
oscillator.frequency.setValueAtTime(voice.freq, startAt);
|
|
94
99
|
|
|
95
100
|
const gain = ctx.createGain();
|
|
101
|
+
// Smooth attack/decay envelope — ramps avoid the click of a hard gate.
|
|
96
102
|
gain.gain.setValueAtTime(0.0001, startAt);
|
|
97
|
-
gain.gain.exponentialRampToValueAtTime(voice.peak, startAt + 0.
|
|
103
|
+
gain.gain.exponentialRampToValueAtTime(voice.peak, startAt + 0.018);
|
|
98
104
|
gain.gain.exponentialRampToValueAtTime(0.0001, stopAt);
|
|
99
105
|
|
|
100
106
|
oscillator.connect(gain);
|
|
@@ -165,7 +171,6 @@ export function createAudioCue(): AudioCue {
|
|
|
165
171
|
idleTimer = window.setTimeout(cleanup, CUE_IDLE_CLEANUP_MS);
|
|
166
172
|
|
|
167
173
|
return {
|
|
168
|
-
playCountdownCue: play,
|
|
169
174
|
playBeforeCapture: () => playBeforeCapture(play, cleanup),
|
|
170
175
|
cleanup,
|
|
171
176
|
};
|
|
@@ -1398,10 +1398,7 @@ async function selectRegionForRecording(): Promise<RegionCaptureRect> {
|
|
|
1398
1398
|
}
|
|
1399
1399
|
}
|
|
1400
1400
|
|
|
1401
|
-
function waitForCountdownEvent(
|
|
1402
|
-
timeoutMs = 4000,
|
|
1403
|
-
onOneSecond?: () => void,
|
|
1404
|
-
): Promise<void> {
|
|
1401
|
+
function waitForCountdownEvent(timeoutMs = 4000): Promise<void> {
|
|
1405
1402
|
return new Promise((resolve, reject) => {
|
|
1406
1403
|
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
1407
1404
|
const unlistens: UnlistenFn[] = [];
|
|
@@ -1457,9 +1454,6 @@ function waitForCountdownEvent(
|
|
|
1457
1454
|
|
|
1458
1455
|
track(listen("clips:countdown-done", () => finish("done")));
|
|
1459
1456
|
track(listen("clips:countdown-cancel", () => finish("cancel")));
|
|
1460
|
-
if (onOneSecond) {
|
|
1461
|
-
track(listen("clips:countdown-one", () => onOneSecond()));
|
|
1462
|
-
}
|
|
1463
1457
|
|
|
1464
1458
|
timer = setTimeout(() => {
|
|
1465
1459
|
if (done) return;
|
|
@@ -1493,13 +1487,12 @@ async function showRegionRecordBorder(region: RegionCaptureRect | null) {
|
|
|
1493
1487
|
});
|
|
1494
1488
|
}
|
|
1495
1489
|
|
|
1496
|
-
async function runRecordingCountdown(wantsScreen: boolean
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
);
|
|
1490
|
+
async function runRecordingCountdown(wantsScreen: boolean) {
|
|
1491
|
+
// The recording-start chime is intentionally NOT played here. It fires from
|
|
1492
|
+
// `audioCue.playBeforeCapture()` at the real capture-start (right before the
|
|
1493
|
+
// recorder/native capture is kicked off) so the beep lines up with the moment
|
|
1494
|
+
// recording actually begins — not one second early on the countdown's "1".
|
|
1495
|
+
const countdownEvent = waitForCountdownEvent(COUNTDOWN_EVENT_TIMEOUT_MS);
|
|
1503
1496
|
await showRegionGuidesForRecording(wantsScreen);
|
|
1504
1497
|
try {
|
|
1505
1498
|
await invoke("show_countdown");
|
|
@@ -1680,7 +1673,7 @@ async function startNativeFullscreenRecording(
|
|
|
1680
1673
|
}).catch((err) => {
|
|
1681
1674
|
console.warn("[clips-recorder] mic warm failed:", err);
|
|
1682
1675
|
});
|
|
1683
|
-
const countdownPromise = runRecordingCountdown(true
|
|
1676
|
+
const countdownPromise = runRecordingCountdown(true);
|
|
1684
1677
|
if (localOnly) {
|
|
1685
1678
|
id = localFolderName;
|
|
1686
1679
|
await Promise.all([countdownPromise, warmMic(id)]);
|
|
@@ -2460,7 +2453,7 @@ async function startRecordingInner(
|
|
|
2460
2453
|
combined,
|
|
2461
2454
|
});
|
|
2462
2455
|
|
|
2463
|
-
const countdownPromise = runRecordingCountdown(wantsScreen
|
|
2456
|
+
const countdownPromise = runRecordingCountdown(wantsScreen);
|
|
2464
2457
|
const localExportPromise = prepareLocalRecordingExport(targets);
|
|
2465
2458
|
let localExport: Awaited<ReturnType<typeof prepareLocalRecordingExport>>;
|
|
2466
2459
|
try {
|
|
@@ -2636,7 +2629,7 @@ async function startRecordingInner(
|
|
|
2636
2629
|
console.log(
|
|
2637
2630
|
"[clips-recorder] invoking show_countdown + createServerRecording",
|
|
2638
2631
|
);
|
|
2639
|
-
const countdownPromise = runRecordingCountdown(wantsScreen
|
|
2632
|
+
const countdownPromise = runRecordingCountdown(wantsScreen);
|
|
2640
2633
|
console.time("[clips-recorder] createServerRecording duration");
|
|
2641
2634
|
const recordingPromise = createServerRecording(
|
|
2642
2635
|
params.serverUrl,
|