Skip to content
Merged
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
6 changes: 6 additions & 0 deletions electron/electron-env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,12 @@ interface Window {
setHasUnsavedChanges: (hasChanges: boolean) => void;
onRequestSaveBeforeClose: (callback: () => Promise<boolean> | boolean) => () => void;
setLocale: (locale: string) => Promise<void>;
saveDiagnostic: (payload: {
error: string;
stack?: string;
projectState: unknown;
logs: string[];
}) => Promise<{ success: boolean; path?: string; canceled?: boolean; error?: string }>;
};
}

Expand Down
42 changes: 42 additions & 0 deletions electron/ipc/handlers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import fs from "node:fs/promises";
import { createRequire } from "node:module";
import os from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";

Expand Down Expand Up @@ -1317,4 +1318,45 @@ export function registerIpcHandlers(
return { success: false, error: String(error) };
}
});

ipcMain.handle(
"save-diagnostic",
async (
_,
payload: { error: string; stack?: string; projectState: unknown; logs: string[] },
) => {
const { filePath, canceled } = await dialog.showSaveDialog({
title: "Save Diagnostic File",
defaultPath: `openscreen-diagnostic-${Date.now()}.json`,
filters: [{ name: "JSON", extensions: ["json"] }],
});

if (canceled || !filePath) return { success: false, canceled: true };

const diagnostic = {
timestamp: new Date().toISOString(),
appVersion: app.getVersion(),
platform: process.platform,
arch: process.arch,
osRelease: os.release(),
osVersion: os.version(),
totalMemoryMB: Math.round(os.totalmem() / 1024 / 1024),
nodeVersion: process.versions.node,
electronVersion: process.versions.electron,
chromeVersion: process.versions.chrome,
error: payload.error,
stack: payload.stack,
projectState: payload.projectState,
recentLogs: payload.logs,
};

try {
await fs.writeFile(filePath, JSON.stringify(diagnostic, null, 2), "utf-8");
return { success: true, path: filePath };
} catch (error) {
console.error("Failed to write diagnostic file:", error);
return { success: false, error: String(error) };
}
},
);
Comment on lines +1322 to +1361
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard the whole diagnostic IPC flow with a top-level try/catch.

Right now, failures from showSaveDialog can reject the IPC call instead of returning the expected object shape. that’s kinda cursed for caller handling paths.

💡 Suggested patch
  ipcMain.handle(
  	"save-diagnostic",
  	async (
  		_,
  		payload: { error: string; stack?: string; projectState: unknown; logs: string[] },
  	) => {
- 		const { filePath, canceled } = await dialog.showSaveDialog({
- 			title: "Save Diagnostic File",
- 			defaultPath: `openscreen-diagnostic-${Date.now()}.json`,
- 			filters: [{ name: "JSON", extensions: ["json"] }],
- 		});
-
- 		if (canceled || !filePath) return { success: false, canceled: true };
-
- 		const diagnostic = {
- 			timestamp: new Date().toISOString(),
- 			appVersion: app.getVersion(),
- 			platform: process.platform,
- 			arch: process.arch,
- 			osRelease: os.release(),
- 			osVersion: os.version(),
- 			totalMemoryMB: Math.round(os.totalmem() / 1024 / 1024),
- 			nodeVersion: process.versions.node,
- 			electronVersion: process.versions.electron,
- 			chromeVersion: process.versions.chrome,
- 			error: payload.error,
- 			stack: payload.stack,
- 			projectState: payload.projectState,
- 			recentLogs: payload.logs,
- 		};
-
  		try {
+ 			const { filePath, canceled } = await dialog.showSaveDialog({
+ 				title: "Save Diagnostic File",
+ 				defaultPath: `openscreen-diagnostic-${Date.now()}.json`,
+ 				filters: [{ name: "JSON", extensions: ["json"] }],
+ 			});
+
+ 			if (canceled || !filePath) return { success: false, canceled: true };
+
+ 			const diagnostic = {
+ 				timestamp: new Date().toISOString(),
+ 				appVersion: app.getVersion(),
+ 				platform: process.platform,
+ 				arch: process.arch,
+ 				osRelease: os.release(),
+ 				osVersion: os.version(),
+ 				totalMemoryMB: Math.round(os.totalmem() / 1024 / 1024),
+ 				nodeVersion: process.versions.node,
+ 				electronVersion: process.versions.electron,
+ 				chromeVersion: process.versions.chrome,
+ 				error: payload.error,
+ 				stack: payload.stack,
+ 				projectState: payload.projectState,
+ 				recentLogs: payload.logs,
+ 			};
+
  			await fs.writeFile(filePath, JSON.stringify(diagnostic, null, 2), "utf-8");
  			return { success: true, path: filePath };
  		} catch (error) {
  			console.error("Failed to write diagnostic file:", error);
  			return { success: false, error: String(error) };
  		}
  	},
  );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
ipcMain.handle(
"save-diagnostic",
async (
_,
payload: { error: string; stack?: string; projectState: unknown; logs: string[] },
) => {
const { filePath, canceled } = await dialog.showSaveDialog({
title: "Save Diagnostic File",
defaultPath: `openscreen-diagnostic-${Date.now()}.json`,
filters: [{ name: "JSON", extensions: ["json"] }],
});
if (canceled || !filePath) return { success: false, canceled: true };
const diagnostic = {
timestamp: new Date().toISOString(),
appVersion: app.getVersion(),
platform: process.platform,
arch: process.arch,
osRelease: os.release(),
osVersion: os.version(),
totalMemoryMB: Math.round(os.totalmem() / 1024 / 1024),
nodeVersion: process.versions.node,
electronVersion: process.versions.electron,
chromeVersion: process.versions.chrome,
error: payload.error,
stack: payload.stack,
projectState: payload.projectState,
recentLogs: payload.logs,
};
try {
await fs.writeFile(filePath, JSON.stringify(diagnostic, null, 2), "utf-8");
return { success: true, path: filePath };
} catch (error) {
console.error("Failed to write diagnostic file:", error);
return { success: false, error: String(error) };
}
},
);
ipcMain.handle(
"save-diagnostic",
async (
_,
payload: { error: string; stack?: string; projectState: unknown; logs: string[] },
) => {
try {
const { filePath, canceled } = await dialog.showSaveDialog({
title: "Save Diagnostic File",
defaultPath: `openscreen-diagnostic-${Date.now()}.json`,
filters: [{ name: "JSON", extensions: ["json"] }],
});
if (canceled || !filePath) return { success: false, canceled: true };
const diagnostic = {
timestamp: new Date().toISOString(),
appVersion: app.getVersion(),
platform: process.platform,
arch: process.arch,
osRelease: os.release(),
osVersion: os.version(),
totalMemoryMB: Math.round(os.totalmem() / 1024 / 1024),
nodeVersion: process.versions.node,
electronVersion: process.versions.electron,
chromeVersion: process.versions.chrome,
error: payload.error,
stack: payload.stack,
projectState: payload.projectState,
recentLogs: payload.logs,
};
await fs.writeFile(filePath, JSON.stringify(diagnostic, null, 2), "utf-8");
return { success: true, path: filePath };
} catch (error) {
console.error("Failed to write diagnostic file:", error);
return { success: false, error: String(error) };
}
},
);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@electron/ipc/handlers.ts` around lines 1322 - 1361, Wrap the entire
"save-diagnostic" ipcMain.handle handler body in a top-level try/catch so any
exceptions (e.g. from dialog.showSaveDialog) are caught and the IPC always
returns the expected object shape; inside the catch, log the error
(console.error) and return an object like { success: false, error:
String(error), canceled: false } (or include canceled if relevant) instead of
letting the promise reject. Keep the existing inner try/catch for fs.writeFile
but move/encapsulate everything (dialog.showSaveDialog, diagnostic construction,
and writeFile call) into the outer try block in the "save-diagnostic" handler to
ensure callers always receive a consistent response.

}
8 changes: 8 additions & 0 deletions electron/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,14 @@ contextBridge.exposeInMainWorld("electronAPI", {
setLocale: (locale: string) => {
return ipcRenderer.invoke("set-locale", locale);
},
saveDiagnostic: (payload: {
error: string;
stack?: string;
projectState: unknown;
logs: string[];
}) => {
return ipcRenderer.invoke("save-diagnostic", payload);
},
setMicrophoneExpanded: (expanded: boolean) => {
ipcRenderer.send("hud:setMicrophoneExpanded", expanded);
},
Expand Down
13 changes: 13 additions & 0 deletions src/components/video-editor/SettingsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
ChevronDown,
Crop,
Download,
FileDown,
Film,
Image,
Lock,
Expand Down Expand Up @@ -240,6 +241,7 @@ interface SettingsPanelProps {
webcamSizePreset?: WebcamSizePreset;
onWebcamSizePresetChange?: (size: WebcamSizePreset) => void;
onWebcamSizePresetCommit?: () => void;
onSaveDiagnostic?: () => Promise<void>;
}

export default SettingsPanel;
Expand Down Expand Up @@ -327,6 +329,7 @@ export function SettingsPanel({
webcamSizePreset = DEFAULT_WEBCAM_SIZE_PRESET,
onWebcamSizePresetChange,
onWebcamSizePresetCommit,
onSaveDiagnostic,
}: SettingsPanelProps) {
const t = useScopedT("settings");
// Resolved URLs are for DOM rendering only (backgroundImage). The canonical
Expand Down Expand Up @@ -1682,6 +1685,16 @@ export function SettingsPanel({
<Bug className="w-3 h-3 text-[#34B27B]" />
{t("links.reportBug")}
</button>
{onSaveDiagnostic && (
<button
type="button"
onClick={onSaveDiagnostic}
className="flex-1 flex items-center justify-center gap-1.5 text-[10px] text-slate-500 hover:text-slate-300 py-1.5 transition-colors"
>
<FileDown className="w-3 h-3 text-slate-400" />
Save Diagnostics
</button>
)}
Comment on lines +1688 to +1697
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Button label should go through i18n.

Save Diagnostics is currently hardcoded, so this one string won’t localize with the rest of the panel.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/video-editor/SettingsPanel.tsx` around lines 1688 - 1697, The
hardcoded button label "Save Diagnostics" in SettingsPanel should be replaced
with a localized string; locate the button rendered when onSaveDiagnostic is
present (the <button> using FileDown and onSaveDiagnostic) and call the
project's i18n helper (e.g., t('...') or useTranslation/Trans) instead of the
literal text, and add a matching locale key like "settings.saveDiagnostics" to
the translation files so the label is translated consistently across locales.

<button
type="button"
onClick={() => {
Expand Down
14 changes: 14 additions & 0 deletions src/components/video-editor/VideoEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1730,6 +1730,19 @@ export default function VideoEditor() {
}
}, []);

const handleSaveDiagnostic = useCallback(async () => {
const result = await window.electronAPI.saveDiagnostic({
error: exportError ?? "Manual diagnostic export",
projectState: editorState,
logs: [],
});
if (result.success) {
toast.success("Diagnostic file saved");
} else if (!result.canceled) {
toast.error("Failed to save diagnostic file");
}
}, [exportError, editorState]);
Comment on lines +1733 to +1744
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Please localize the new diagnostic strings.

Right now "Manual diagnostic export" and the success/failure toasts are hardcoded English. lowkey breaks localization consistency in an otherwise fully translated screen.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/video-editor/VideoEditor.tsx` around lines 1733 - 1744, The
hardcoded strings in handleSaveDiagnostic (the error fallback "Manual diagnostic
export" and the toast messages passed to toast.success/toast.error) must be
localized: import or use the project's translation helper (e.g.,
useTranslation()/t) and replace the literal strings passed to
window.electronAPI.saveDiagnostic (error), toast.success, and toast.error with
translated keys (e.g., t('diagnostics.manualExport'),
t('diagnostics.saveSuccess'), t('diagnostics.saveFailure')); keep exportError
fallback logic but use t(...) for the default message and update any existing
translation files with the new keys.


if (loading) {
return (
<div className="flex items-center justify-center h-screen bg-background">
Expand Down Expand Up @@ -2100,6 +2113,7 @@ export default function VideoEditor() {
onSpeedDelete={handleSpeedDelete}
unsavedExport={unsavedExport}
onSaveUnsavedExport={handleSaveUnsavedExport}
onSaveDiagnostic={handleSaveDiagnostic}
/>
</div>
</div>
Expand Down
Loading