Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions electron/audio/capture-preload-utils.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
function serializeError(error) {
return {
message: error instanceof Error ? error.message : String(error),
name: error && typeof error.name === "string" ? error.name : "",
};
}

function handleRecorderStop({ sendChain, id, getStopEpoch, cleanup, send }) {
return sendChain
.then(() => {
const stopEpoch = getStopEpoch();
cleanup();
send("audio:stopped", id, stopEpoch);
})
.catch((error) => {
const stopEpoch = getStopEpoch();
cleanup();
const detail = serializeError(error);
send("audio:error", id, detail.message, detail.name);
send("audio:stopped", id, stopEpoch);
});
}

module.exports = { handleRecorderStop, serializeError };
45 changes: 45 additions & 0 deletions electron/audio/capture-preload-utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import assert from "node:assert/strict";
import { createRequire } from "node:module";
import test from "node:test";

const require = createRequire(import.meta.url);
const { handleRecorderStop } = require("./capture-preload-utils.cjs");

test("audio stop reports a rejected chunk flush", async () => {
const events: unknown[][] = [];
let cleaned = false;
await handleRecorderStop({
sendChain: Promise.reject(new Error("chunk flush failed")),
id: "segment-0001",
getStopEpoch: () => 123,
cleanup: () => {
cleaned = true;
},
send: (...args: unknown[]) => {
events.push(args);
},
});
assert.equal(cleaned, true);
assert.deepEqual(events, [
["audio:error", "segment-0001", "chunk flush failed", "Error"],
["audio:stopped", "segment-0001", 123],
]);
});

test("audio stop acknowledges a successful chunk flush", async () => {
const events: unknown[][] = [];
let cleaned = false;
await handleRecorderStop({
sendChain: Promise.resolve(),
id: "segment-0001",
getStopEpoch: () => 123,
cleanup: () => {
cleaned = true;
},
send: (...args: unknown[]) => {
events.push(args);
},
});
assert.equal(cleaned, true);
assert.deepEqual(events, [["audio:stopped", "segment-0001", 123]]);
});
18 changes: 7 additions & 11 deletions electron/audio/capture-preload.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
// mirror electron/audio/recorder.ts.
const { ipcRenderer } = require("electron");
const { readFile } = require("node:fs/promises");
const { handleRecorderStop, serializeError } = require("./capture-preload-utils.cjs");

/** @type {MediaRecorder | null} */
let recorder = null;
Expand All @@ -29,13 +30,6 @@ function microphoneConstraints(deviceId) {
};
}

function serializeError(error) {
return {
message: error instanceof Error ? error.message : String(error),
name: error && typeof error.name === "string" ? error.name : "",
};
}

async function enumerateMicrophones() {
if (!navigator.mediaDevices || !navigator.mediaDevices.enumerateDevices) return [];
const devices = await navigator.mediaDevices.enumerateDevices();
Expand Down Expand Up @@ -136,10 +130,12 @@ ipcRenderer.on("audio:start", async (_event, opts) => {
};
recorder.onstop = () => {
// Wait for every queued chunk (including the final one) to be sent.
sendChain.then(() => {
const stopEpoch = requestedStopEpoch || epochNow();
cleanup();
ipcRenderer.send("audio:stopped", id, stopEpoch);
void handleRecorderStop({
sendChain,
id,
getStopEpoch: () => requestedStopEpoch || epochNow(),
cleanup,
send: (...args) => ipcRenderer.send(...args),
});
};
recorder.onerror = (e) => {
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"check:lockfile": "node scripts/check-lockfile-portability.mjs",
"typecheck": "tsc --noEmit",
"typecheck:evals": "tsc --noEmit -p evals/tsconfig.json",
"test": "node --experimental-transform-types --no-warnings --import ./evals/register.mjs --test evals/builder-imports.test.ts common/architecture-registry.test.ts electron/architectures/catalogue-registry.test.ts common/audio.test.ts common/microphone.test.ts common/screen.test.ts common/narration.test.ts common/sensitive.test.ts common/skill.test.ts electron/recording-controls-bounds.test.ts electron/recorder-window-sizing.test.ts electron/recording-privacy.test.ts electron/crash-guards.test.ts electron/copilot-signin-auth.test.ts electron/copilot-signin-flow.test.ts electron/copilot-signin-process.test.ts electron/microsoft-signin-hint.test.ts src/analysis-recovery.test.ts electron/recorder/controller.test.ts electron/recorder/session-store.test.ts electron/frames/extractor.test.ts electron/pipeline.test.ts electron/narration/audio-analysis.test.ts electron/narration/analyze-gate.test.ts electron/narration/transcribe.test.ts electron/narration/whisper.test.ts electron/sensitive/scanner.test.ts electron/sensitive/secrets.test.ts electron/sensitive/tessdata-source.test.ts electron/sensitive/ocr.test.ts electron/sensitive/frame-redact.test.ts electron/sensitive/frame-heuristics.test.ts electron/terminal/terminal.test.ts electron/sessions.test.ts electron/debug-bundle.test.ts electron/skillbuilder/plan-tools.test.ts electron/skillbuilder/placement.test.ts electron/skillbuilder/preview.test.ts src/skill-placement.test.ts src/skill-review-state.test.ts scripts/compliance.test.mjs scripts/install-windows-dependencies.test.mjs",
"test": "node --experimental-transform-types --no-warnings --import ./evals/register.mjs --test evals/builder-imports.test.ts common/architecture-registry.test.ts electron/architectures/catalogue-registry.test.ts electron/audio/capture-preload-utils.test.ts common/audio.test.ts common/microphone.test.ts common/screen.test.ts common/narration.test.ts common/sensitive.test.ts common/skill.test.ts electron/recording-controls-bounds.test.ts electron/recorder-window-sizing.test.ts electron/recording-privacy.test.ts electron/crash-guards.test.ts electron/copilot-signin-auth.test.ts electron/copilot-signin-flow.test.ts electron/copilot-signin-process.test.ts electron/microsoft-signin-hint.test.ts src/analysis-recovery.test.ts electron/recorder/controller.test.ts electron/recorder/session-store.test.ts electron/frames/extractor.test.ts electron/pipeline.test.ts electron/narration/audio-analysis.test.ts electron/narration/analyze-gate.test.ts electron/narration/transcribe.test.ts electron/narration/whisper.test.ts electron/sensitive/scanner.test.ts electron/sensitive/secrets.test.ts electron/sensitive/tessdata-source.test.ts electron/sensitive/ocr.test.ts electron/sensitive/frame-redact.test.ts electron/sensitive/frame-heuristics.test.ts electron/terminal/terminal.test.ts electron/sessions.test.ts electron/debug-bundle.test.ts electron/skillbuilder/plan-tools.test.ts electron/skillbuilder/placement.test.ts electron/skillbuilder/preview.test.ts src/skill-placement.test.ts src/skill-review-state.test.ts scripts/compliance.test.mjs scripts/install-windows-dependencies.test.mjs",
"eval": "node --experimental-transform-types --no-warnings --import ./evals/register.mjs evals/run.ts",
"eval:terminal": "node --experimental-transform-types --no-warnings --import ./evals/register.mjs evals/run.ts --only=terminal-output-required,terminal-failure-recovery,terminal-sensitive-output",
"eval:builder": "node --experimental-transform-types --no-warnings --import ./evals/register.mjs evals/builder/run.ts",
Expand Down
4 changes: 4 additions & 0 deletions vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ function copyStaticAssets(): void {
path.join(rootDir, "electron", "audio", "capture-preload.cjs"),
path.join(out, "audio", "capture-preload.cjs"),
);
copyFileSync(
path.join(rootDir, "electron", "audio", "capture-preload-utils.cjs"),
path.join(out, "audio", "capture-preload-utils.cjs"),
);
}

export default defineConfig({
Expand Down