Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
27217f6
fix(macos): settle the native stop on the helper's exit, not its firs…
EtienneLescot Sep 14, 2026
186dd5a
fix(macos): keep a timed-out helper out of the next take, and say why…
EtienneLescot Sep 14, 2026
42115ad
fix(macos): listen for errors on the helper's output pipes too
EtienneLescot Sep 14, 2026
4457264
fix(macos): recover the take a dead writer or a killed helper left on…
EtienneLescot Sep 14, 2026
527b27e
fix(macos): keep a recovered take when the disk is still full, and sa…
EtienneLescot Sep 14, 2026
fe28daf
fix(wgc): read the config argument as UTF-16 so non-ASCII output path…
EtienneLescot Sep 14, 2026
5d4a10c
fix(timeline): coalesce scrub state updates per frame
arhxam Aug 28, 2026
ea01ad4
test(timeline): measure per-frame scrub commits against one update's …
EtienneLescot Sep 14, 2026
d108bdd
fix(windows): low-pass before decimating so 96/192 kHz sources stop a…
My-Denia Sep 10, 2026
f5a109d
docs(testing): add the anti-alias check, and log the A/B that confirm…
My-Denia Sep 10, 2026
60e839e
fix(ai): report every modifier removed with a pill
arhxam Aug 28, 2026
f07fa5a
fix(ai): enumerate audio pills too, now that RegionKind carries them
EtienneLescot Sep 14, 2026
ba4876b
ci(whisper-stt): drop the msvc-dev-cmd step the Windows build never used
My-Denia Sep 10, 2026
7d72881
docs(linux): bring the platform table back in line with what the code…
My-Denia Sep 10, 2026
7d0b507
docs(linux): the webcam is a separate file on Windows too, and VAAPI …
My-Denia Sep 10, 2026
03f9117
fix(capture-linux): re-apply the vaMapBuffer2 guard to the dmabuf enc…
EtienneLescot Sep 14, 2026
24c723e
fix(capture-linux): apply the vaMapBuffer2 guard to a forced VAAPI en…
sepion02 Sep 14, 2026
fe5d9f7
fix(hud): keep a dragged HUD inside the display work area
im10furry Sep 14, 2026
4004ae8
fix(hud): clamp the visible bar, not the window, into the work area
EtienneLescot Sep 14, 2026
edf7255
feat(editor): show what a trim keeps, while the handles are still moving
My-Denia Sep 10, 2026
f5a56c0
fix(editor): size the card timecode gate to the timecode it is gating
My-Denia Sep 10, 2026
8901928
fix(editor): measure the card timecode gate instead of guessing per c…
EtienneLescot Sep 14, 2026
35edf70
feat(launch): make post-recording auto-zoom systematic and remove tog…
EtienneLescot Sep 14, 2026
dbcf03e
fix(hud): keep open popovers on screen and harden the work-area clamp
EtienneLescot Sep 14, 2026
fe5f9dc
fix(build): re-fetch a staged ONNX Runtime that is the right version …
EtienneLescot Sep 14, 2026
dbe0f7c
feat(settings): persist recording setup and project appearance defaults
My-Denia Sep 14, 2026
e159ced
fix(settings): fail stale CLI sources and keep HUD stop/save in sync
My-Denia Sep 15, 2026
19abbd7
fix(settings): stop camera pref ping-pong and stale select-source writes
My-Denia Sep 15, 2026
0258890
feat: add Czech (cs) locale
Sep 8, 2026
a4e0bca
fix(cs): correct Czech grammar, terminology and typos in locale files
Sep 8, 2026
3033054
fix(cs): replace em dashes with natural Czech punctuation throughout
Sep 8, 2026
fc3722d
fix(cs): replace escaped inner quotes with proper Czech closing quota…
Sep 8, 2026
cf97efb
fix(cs): address CodeRabbit review: grammar, gender agreement and mis…
Sep 8, 2026
0c9aa77
fix(cs): add cs to tutorialHelpTranslations static locale map
Sep 13, 2026
b1d57bb
fix(cs): add missing i18n keys added in main merge (autoZoom, cursor)
Sep 13, 2026
2fb5691
fix(cs): register cs in main process and packaging, sync keys with main
EtienneLescot Sep 15, 2026
2f00a9d
fix(hud): label recording controls for assistive technology
CyberSparkx Sep 9, 2026
93f22d3
test(hud): pin the exact accessible name of every HUD control
EtienneLescot Sep 15, 2026
6157564
Merge remote-tracking branch 'origin/main' into pr/509-hud-session-prefs
EtienneLescot Sep 15, 2026
ac36aab
refactor(settings): drop the Settings dialog and new-project appearan…
EtienneLescot Sep 15, 2026
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
20 changes: 15 additions & 5 deletions .github/workflows/build-whisper-stt.yml
Original file line number Diff line number Diff line change
Expand Up @@ -91,11 +91,21 @@ jobs:
if: startsWith(matrix.os, 'macos')
run: brew install ninja

- name: Setup MSVC (Windows)
if: matrix.os == 'windows-latest'
uses: ilammy/msvc-dev-cmd@v1
with:
arch: x64
# There is deliberately no "Setup MSVC" step for the Windows leg. It used to
# run ilammy/msvc-dev-cmd here, and nothing consumed what that action set up:
# scripts/build-whisper-stt.sh calls cmake without -G, so on Windows CMake
# picks its Visual Studio generator (the job log reads "Building for: Visual
# Studio 18 2026"), which locates MSVC through the Visual Studio installer
# and builds with MSBuild, whose VC targets set up the compiler environment
# themselves rather than inheriting the PATH/INCLUDE/LIB vcvarsall exported.
# vcpkg, in the SPIRV-Headers step below, finds the
# compiler on its own the same way, and windows-latest ships a single VS
# instance, so there is nothing for vcvars to disambiguate either. The
# action was also the last node20 action in the repo, with an upstream that
# stopped in 2024 (#317), so this drops a dependency rather than replacing
# it. Should the Windows build ever move to Ninja, that is the point where a
# vcvars step becomes necessary again; scripts/msvcEnv.mjs already has the
# vcvarsall discovery for it.

- name: Install Vulkan SDK
if: matrix.vulkan
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ Everything in the editor and export is the same on macOS, Windows, and Linux: zo

- **Native recording**: macOS (ScreenCaptureKit), Windows (Windows Graphics Capture), and Linux (PipeWire via the ScreenCast portal) all record through a native pipeline for higher quality and clean window-level capture. On Linux the browser pipeline stays as an automatic fallback if the helper isn't available.
- **Custom cursors**: on macOS and Windows the real cursor is captured with shape, type, and clicks. Linux captures position and cursor shape through the portal, so cursor themes and the editable cursor overlay work there too. Click effects work on Linux as well, but not through the portal — Wayland exposes no portal for mouse buttons, so the capture helper reads the left button from evdev, which needs your user in the `input` group. Without that, recording is unaffected and every cursor sample is simply a move.
- **Webcam**: Windows muxes the webcam natively into the recording; macOS and Linux record it alongside as a separate file. It works as a picture-in-picture overlay on all three.
- **Webcam**: every platform saves the webcam as a separate file next to the screen recording. Windows captures it natively in the recording helper; macOS and Linux record it in the app. It works as a picture-in-picture overlay on all three.
- **System audio** support varies by OS:
- **macOS**: works on every supported version. On macOS 14.2+ you'll be prompted to grant audio capture permission.
- **Windows**: works out of the box.
Expand Down
3 changes: 2 additions & 1 deletion electron-builder.json5
Original file line number Diff line number Diff line change
Expand Up @@ -405,12 +405,13 @@
// Mirrors SUPPORTED_LOCALES in src/i18n/config.ts. This list is what the Store
// shows as "supported languages" on the product page and what lets the listing
// surface in each language's Store search — declaring only en-US/fr-FR advertised
// 2 of the 13 languages the app actually ships. Bare tags (ar, es, ...) match every
// 2 of the 14 languages the app actually ships. Bare tags (ar, es, ...) match every
// region of that language, which is what the renderer's locale resolution does too.
"languages": [
"en-US",
"fr-FR",
"ar",
"cs",
"es",
"it",
"ja-JP",
Expand Down
45 changes: 43 additions & 2 deletions electron/ai-edition/agent-tools.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -759,7 +759,11 @@ describe("executeAgentTool", () => {
const zoomId = withZoom.zoomRanges[0].id;
const removed = executeAgentTool(withZoom, "removeModifier", JSON.stringify({ id: zoomId }));
expect(removed.ok).toBe(true);
expect(JSON.parse(removed.resultJson)).toMatchObject({ kind: "zoom" });
expect(JSON.parse(removed.resultJson)).toMatchObject({
kind: "zoom",
removed: zoomId,
removedIds: [zoomId],
});
expect(removed.document?.zoomRanges).toHaveLength(0);

const withSpeed = executeAgentTool(
Expand All @@ -775,7 +779,11 @@ describe("executeAgentTool", () => {
"removeModifier",
JSON.stringify({ id: speedId }),
);
expect(JSON.parse(removedSpeed.resultJson)).toMatchObject({ kind: "speed" });
expect(JSON.parse(removedSpeed.resultJson)).toMatchObject({
kind: "speed",
removed: speedId,
removedIds: [speedId],
});
expect(
(removedSpeed.document?.legacyEditor as Record<string, unknown>).speedRegions,
).toHaveLength(0);
Expand All @@ -790,6 +798,39 @@ describe("executeAgentTool", () => {
expect(wrong.resultJson).toMatch(/removeTrim/);
});

it("removeModifier reports every touching modifier row removed with the pill", () => {
const added = executeAgentTool(
fixtureDocument(),
"addZooms",
JSON.stringify({
regions: [
{ startSec: 0, endSec: 10, depth: 3 },
{ startSec: 10, endSec: 20, depth: 3 },
{ startSec: 20, endSec: 30, depth: 3 },
],
}),
);
expect(added.ok).toBe(true);
const ids = (JSON.parse(added.resultJson).applied as Array<{ zoomId: string }>).map(
(entry) => entry.zoomId,
);
expect(ids).toHaveLength(3);

const removed = executeAgentTool(
added.document as AxcutDocument,
"removeModifier",
JSON.stringify({ id: ids[0] }),
);

expect(removed.ok).toBe(true);
expect(removed.document?.zoomRanges).toHaveLength(0);
expect(JSON.parse(removed.resultJson)).toMatchObject({
removed: ids[0],
removedIds: ids,
kind: "zoom",
});
});

it("addZoom reports the CLAMPED span, not the one it was asked for", () => {
// ponytail: the exact shape of D-HONEST. Ventilation trims the span to the
// clip; the tool used to echo back 20–40 while the document held 20–24.704
Expand Down
38 changes: 35 additions & 3 deletions electron/ai-edition/agent-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,32 @@ function toMs(sec: number): number {
return Math.max(0, Math.round(sec * 1000));
}

type ModifierKind = Exclude<RegionKind, "trim">;

function modifierIds(document: AxcutDocument, kind: ModifierKind): string[] {
const legacy = (document.legacyEditor as Record<string, unknown>) ?? {};
switch (kind) {
case "zoom":
return document.zoomRanges.map((region) => region.id);
case "annotation":
return document.annotations.map((region) => region.id);
case "speed":
return ((legacy.speedRegions as Array<{ id: string }> | undefined) ?? []).map(
(region) => region.id,
);
case "cameraFullscreen":
return ((legacy.cameraFullscreenRegions as Array<{ id: string }> | undefined) ?? []).map(
(region) => region.id,
);
case "audio":
// Fragments of one user-visible track share `trackId` and render as a single pill,
// so the id that disappears is the group key once, not one per fragment.
// `removeRegion` applies the same grouping by routing this kind through
// `removeAudioTrack`.
return [...new Set(document.audioTracks.map(trackGroupId))];
}
}

// For the effect set* tools: keep the stored span unless the caller passes new
// edges, and normalise so start ≤ end. Input seconds are virtual-timeline time.
function resolveSpanMs(
Expand Down Expand Up @@ -2203,7 +2229,7 @@ export function executeAgentTool(
const speedRegions = (legacy.speedRegions as Array<{ id: string }> | undefined) ?? [];
const cameraFullscreenRegions =
(legacy.cameraFullscreenRegions as Array<{ id: string }> | undefined) ?? [];
let kind: RegionKind | null = null;
let kind: ModifierKind | null = null;
if (document.zoomRanges.some((z) => z.id === id)) kind = "zoom";
else if (document.annotations.some((a) => a.id === id)) kind = "annotation";
else if (speedRegions.some((s) => s.id === id)) kind = "speed";
Expand All @@ -2215,12 +2241,18 @@ export function executeAgentTool(
`For a trim use removeTrim; for a clip use removeClip.`,
);
}
const beforeIds = modifierIds(document, kind);
const next = removeRegion(document, kind, id);
const remainingIds = new Set(modifierIds(next, kind));
const removedIds = beforeIds.filter((candidateId) => !remainingIds.has(candidateId));
return {
ok: true,
document: next,
resultJson: JSON.stringify({ removed: id, kind }),
summary: `removed ${kind} ${id}`,
resultJson: JSON.stringify({ removed: id, removedIds, kind }),
summary:
removedIds.length === 1
? `removed ${kind} ${id}`
: `removed ${removedIds.length} ${kind} rows: ${removedIds.join(", ")}`,
};
}

Expand Down
2 changes: 1 addition & 1 deletion electron/ai-edition/deep-agent/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ export const TOOL_DESCRIPTIONS: Record<string, string> = {
removeTrim:
"Delete a trim range by id — the cut is undone and that span plays/exports again. This is how you 'remove a trim'; never re-add a trim to undo one.",
removeModifier:
"Delete a modifier (zoom / speed / annotation / camera-fullscreen / audio) by id; the kind is resolved from the id. This is how you 'remove'/'delete' one — never neutralise it (span 0, speed 1×), which leaves it in the document. For a trim use removeTrim; for a clip use removeClip.",
"Delete a modifier (zoom / speed / annotation / camera-fullscreen / audio) by id; the kind is resolved from the id. Touching rows with the same styling render as one pill and are deleted together, so read removedIds in the result for the complete set that disappeared. This is how you 'remove'/'delete' one — never neutralise it (span 0, speed 1×), which leaves it in the document. For a trim use removeTrim; for a clip use removeClip.",
removeClip:
"Delete a placed clip by id; remaining clips close the gap and effects anchored to it are dropped. Use only when the user asks to remove a clip — to shorten one, use setClipRange.",
};
Expand Down
72 changes: 72 additions & 0 deletions electron/app-settings.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { AppSettingsStore, DEFAULT_RECORDING_PREFERENCES } from "./app-settings";

const dirs: string[] = [];
const temp = () => {
const dir = mkdtempSync(path.join(os.tmpdir(), "openscreen-app-settings-"));
dirs.push(dir);
return dir;
};
afterEach(() => {
for (const dir of dirs.splice(0)) rmSync(dir, { recursive: true, force: true });
});

describe("app settings store", () => {
it("uses defaults for absent fields and preserves unknown keys", () => {
const dir = temp();
const file = path.join(dir, "recording-settings.json");
writeFileSync(file, JSON.stringify({ future: { keep: true } }));
const store = new AppSettingsStore(dir);
expect(store.getSnapshot().recording).toEqual(DEFAULT_RECORDING_PREFERENCES);
store.setRecordingPreferences({ micEnabled: true, camDeviceName: "Camera A" });
expect(JSON.parse(readFileSync(file, "utf8"))).toMatchObject({
future: { keep: true },
micEnabled: true,
camDeviceName: "Camera A",
});
});

it("uses validated defaults for absent, corrupt, and invalid fields", () => {
const dir = temp();
const file = path.join(dir, "recording-settings.json");
const store = new AppSettingsStore(dir);
expect(store.getSnapshot().recording).toEqual(DEFAULT_RECORDING_PREFERENCES);
for (const raw of ["{broken", "[]", JSON.stringify({ micEnabled: "yes" })]) {
writeFileSync(file, raw);
expect(store.getSnapshot().recording.micEnabled).toBe(false);
}
});

it("stores the last source beside the recording preferences", () => {
const dir = temp();
const store = new AppSettingsStore(dir);
const source = {
platform: "win32",
kind: "screen",
id: "screen:1",
name: "Display",
displayId: "1",
} as const;
store.setRecordingPreferences({ micEnabled: true, micDeviceId: "mic" });
expect(store.setLastSource(source).lastSource).toEqual(source);
expect(
JSON.parse(readFileSync(path.join(dir, "recording-settings.json"), "utf8")),
).toMatchObject({ micEnabled: true, micDeviceId: "mic", lastSource: source });
expect(store.setLastSource(null).lastSource).toBeNull();
expect(store.getSnapshot().recording).toMatchObject({ micEnabled: true, micDeviceId: "mic" });
});

it("rejects invalid or failed writes without changing the published durable value", () => {
const dir = temp();
const store = new AppSettingsStore(dir);
store.setRecordingPreferences({ micEnabled: true });
expect(() => store.setRecordingPreferences({ micEnabled: "yes" as never })).toThrow(TypeError);
expect(store.getSnapshot().recording.micEnabled).toBe(true);
const missing = new AppSettingsStore(path.join(dir, "missing"));
expect(() => missing.setRecordingPreferences({ micEnabled: true })).toThrow();
expect(missing.getSnapshot().recording.micEnabled).toBe(false);
});
});
Loading
Loading