The AI listens to conversations and provides contextual assistance based on what it hears.
+
+
+
+ Troubleshooting
+
+
+ If you're experiencing issues with audio capture or other features, check the application logs for diagnostic information.
+
+
+
`;
}
+
+ async openLogsFolder() {
+ try {
+ const { ipcRenderer } = require('electron');
+ const result = await ipcRenderer.invoke('open-logs-folder');
+ if (!result.success) {
+ console.error('Failed to open logs folder:', result.error);
+ }
+ } catch (err) {
+ console.error('Error opening logs folder:', err);
+ }
+ }
}
customElements.define('help-view', HelpView);
diff --git a/src/index.js b/src/index.js
index fbf3152..89aa83f 100644
--- a/src/index.js
+++ b/src/index.js
@@ -6,6 +6,7 @@ const { app, BrowserWindow, shell, ipcMain } = require('electron');
const { createWindow, updateGlobalShortcuts } = require('./utils/window');
const { setupAIProviderIpcHandlers } = require('./utils/ai-provider-manager');
const { stopMacOSAudioCapture } = require('./utils/gemini');
+const { initLogger, closeLogger, getLogPath } = require('./utils/logger');
const storage = require('./storage');
const geminiSessionRef = { current: null };
@@ -24,6 +25,10 @@ function createMainWindow() {
}
app.whenReady().then(async () => {
+ // Initialize file logger first
+ const logPath = initLogger();
+ console.log('App starting, log file:', logPath);
+
// Initialize storage (checks version, resets if needed)
storage.initializeStorage();
@@ -31,10 +36,14 @@ app.whenReady().then(async () => {
setupAIProviderIpcHandlers(geminiSessionRef);
setupStorageIpcHandlers();
setupGeneralIpcHandlers();
+
+ // Add handler to get log path from renderer
+ ipcMain.handle('get-log-path', () => getLogPath());
});
app.on('window-all-closed', () => {
stopMacOSAudioCapture();
+ closeLogger();
if (process.platform !== 'darwin') {
app.quit();
}
@@ -42,6 +51,7 @@ app.on('window-all-closed', () => {
app.on('before-quit', () => {
stopMacOSAudioCapture();
+ closeLogger();
});
app.on('activate', () => {
@@ -284,6 +294,18 @@ function setupGeneralIpcHandlers() {
return app.getVersion();
});
+ ipcMain.handle('open-logs-folder', async () => {
+ try {
+ const logPath = getLogPath();
+ const logsDir = require('path').dirname(logPath);
+ await shell.openPath(logsDir);
+ return { success: true, path: logsDir };
+ } catch (error) {
+ console.error('Error opening logs folder:', error);
+ return { success: false, error: error.message };
+ }
+ });
+
ipcMain.handle('quit-application', async event => {
try {
stopMacOSAudioCapture();
diff --git a/src/utils/gemini.js b/src/utils/gemini.js
index 36ce507..e0f539e 100644
--- a/src/utils/gemini.js
+++ b/src/utils/gemini.js
@@ -418,10 +418,12 @@ async function startMacOSAudioCapture(geminiSessionRef) {
// Kill any existing SystemAudioDump processes first
await killExistingSystemAudioDump();
- console.log('Starting macOS audio capture with SystemAudioDump...');
+ console.log('=== Starting macOS audio capture ===');
+ sendToRenderer('update-status', 'Starting audio capture...');
const { app } = require('electron');
const path = require('path');
+ const fs = require('fs');
let systemAudioPath;
if (app.isPackaged) {
@@ -430,7 +432,35 @@ async function startMacOSAudioCapture(geminiSessionRef) {
systemAudioPath = path.join(__dirname, '../assets', 'SystemAudioDump');
}
- console.log('SystemAudioDump path:', systemAudioPath);
+ console.log('SystemAudioDump config:', {
+ path: systemAudioPath,
+ isPackaged: app.isPackaged,
+ resourcesPath: process.resourcesPath,
+ exists: fs.existsSync(systemAudioPath),
+ });
+
+ // Check if file exists
+ if (!fs.existsSync(systemAudioPath)) {
+ console.error('FATAL: SystemAudioDump not found at:', systemAudioPath);
+ sendToRenderer('update-status', 'Error: Audio binary not found');
+ return false;
+ }
+
+ // Check and fix executable permissions
+ try {
+ fs.accessSync(systemAudioPath, fs.constants.X_OK);
+ console.log('SystemAudioDump is executable');
+ } catch (err) {
+ console.warn('SystemAudioDump not executable, fixing permissions...');
+ try {
+ fs.chmodSync(systemAudioPath, 0o755);
+ console.log('Fixed executable permissions');
+ } catch (chmodErr) {
+ console.error('Failed to fix permissions:', chmodErr);
+ sendToRenderer('update-status', 'Error: Cannot execute audio binary');
+ return false;
+ }
+ }
const spawnOptions = {
stdio: ['ignore', 'pipe', 'pipe'],
@@ -439,10 +469,12 @@ async function startMacOSAudioCapture(geminiSessionRef) {
},
};
+ console.log('Spawning SystemAudioDump...');
systemAudioProc = spawn(systemAudioPath, [], spawnOptions);
if (!systemAudioProc.pid) {
- console.error('Failed to start SystemAudioDump');
+ console.error('FATAL: Failed to start SystemAudioDump - no PID');
+ sendToRenderer('update-status', 'Error: Audio capture failed to start');
return false;
}
@@ -455,8 +487,16 @@ async function startMacOSAudioCapture(geminiSessionRef) {
const CHUNK_SIZE = SAMPLE_RATE * BYTES_PER_SAMPLE * CHANNELS * CHUNK_DURATION;
let audioBuffer = Buffer.alloc(0);
+ let chunkCount = 0;
+ let firstDataReceived = false;
systemAudioProc.stdout.on('data', data => {
+ if (!firstDataReceived) {
+ firstDataReceived = true;
+ console.log('First audio data received! Size:', data.length);
+ sendToRenderer('update-status', 'Listening...');
+ }
+
audioBuffer = Buffer.concat([audioBuffer, data]);
while (audioBuffer.length >= CHUNK_SIZE) {
@@ -467,6 +507,11 @@ async function startMacOSAudioCapture(geminiSessionRef) {
const base64Data = monoChunk.toString('base64');
sendAudioToGemini(base64Data, geminiSessionRef);
+ chunkCount++;
+ if (chunkCount % 100 === 0) {
+ console.log(`Audio: ${chunkCount} chunks processed`);
+ }
+
if (process.env.DEBUG_AUDIO) {
console.log(`Processed audio chunk: ${chunk.length} bytes`);
saveDebugAudio(monoChunk, 'system_audio');
@@ -480,16 +525,24 @@ async function startMacOSAudioCapture(geminiSessionRef) {
});
systemAudioProc.stderr.on('data', data => {
- console.error('SystemAudioDump stderr:', data.toString());
+ const msg = data.toString();
+ console.error('SystemAudioDump stderr:', msg);
+ if (msg.toLowerCase().includes('error')) {
+ sendToRenderer('update-status', 'Audio error: ' + msg.substring(0, 50));
+ }
});
systemAudioProc.on('close', code => {
- console.log('SystemAudioDump process closed with code:', code);
+ console.log('SystemAudioDump closed with code:', code, 'chunks processed:', chunkCount);
+ if (code !== 0 && code !== null) {
+ sendToRenderer('update-status', `Audio stopped (exit: ${code})`);
+ }
systemAudioProc = null;
});
systemAudioProc.on('error', err => {
- console.error('SystemAudioDump process error:', err);
+ console.error('SystemAudioDump spawn error:', err.message);
+ sendToRenderer('update-status', 'Audio error: ' + err.message);
systemAudioProc = null;
});
diff --git a/src/utils/logger.js b/src/utils/logger.js
new file mode 100644
index 0000000..20f81c7
--- /dev/null
+++ b/src/utils/logger.js
@@ -0,0 +1,97 @@
+const fs = require('fs');
+const path = require('path');
+const { app } = require('electron');
+
+let logFile = null;
+let logPath = null;
+
+function getLogPath() {
+ if (logPath) return logPath;
+
+ const userDataPath = app.getPath('userData');
+ const logsDir = path.join(userDataPath, 'logs');
+
+ // Create logs directory if it doesn't exist
+ if (!fs.existsSync(logsDir)) {
+ fs.mkdirSync(logsDir, { recursive: true });
+ }
+
+ // Create log file with timestamp
+ const timestamp = new Date().toISOString().split('T')[0];
+ logPath = path.join(logsDir, `app-${timestamp}.log`);
+
+ return logPath;
+}
+
+function initLogger() {
+ try {
+ const filePath = getLogPath();
+ logFile = fs.createWriteStream(filePath, { flags: 'a' });
+
+ const startMsg = `\n${'='.repeat(60)}\nApp started at ${new Date().toISOString()}\nPlatform: ${process.platform}, Arch: ${process.arch}\nElectron: ${process.versions.electron}, Node: ${process.versions.node}\nPackaged: ${app.isPackaged}\n${'='.repeat(60)}\n`;
+ logFile.write(startMsg);
+
+ // Override console methods to also write to file
+ const originalLog = console.log;
+ const originalError = console.error;
+ const originalWarn = console.warn;
+
+ console.log = (...args) => {
+ originalLog.apply(console, args);
+ writeLog('INFO', args);
+ };
+
+ console.error = (...args) => {
+ originalError.apply(console, args);
+ writeLog('ERROR', args);
+ };
+
+ console.warn = (...args) => {
+ originalWarn.apply(console, args);
+ writeLog('WARN', args);
+ };
+
+ console.log('Logger initialized, writing to:', filePath);
+
+ return filePath;
+ } catch (err) {
+ console.error('Failed to initialize logger:', err);
+ return null;
+ }
+}
+
+function writeLog(level, args) {
+ if (!logFile) return;
+
+ try {
+ const timestamp = new Date().toISOString();
+ const message = args.map(arg => {
+ if (typeof arg === 'object') {
+ try {
+ return JSON.stringify(arg, null, 2);
+ } catch {
+ return String(arg);
+ }
+ }
+ return String(arg);
+ }).join(' ');
+
+ logFile.write(`[${timestamp}] [${level}] ${message}\n`);
+ } catch (err) {
+ // Silently fail - don't want logging errors to crash the app
+ }
+}
+
+function closeLogger() {
+ if (logFile) {
+ logFile.write(`\nApp closed at ${new Date().toISOString()}\n`);
+ logFile.end();
+ logFile = null;
+ }
+}
+
+module.exports = {
+ initLogger,
+ closeLogger,
+ getLogPath,
+};
diff --git a/src/utils/openai-sdk.js b/src/utils/openai-sdk.js
index 8ca6164..f230d40 100644
--- a/src/utils/openai-sdk.js
+++ b/src/utils/openai-sdk.js
@@ -441,9 +441,11 @@ async function startMacOSAudioCapture() {
// Kill any existing SystemAudioDump processes first
await killExistingSystemAudioDump();
- console.log('Starting macOS audio capture with SystemAudioDump for OpenAI SDK...');
+ console.log('=== Starting macOS audio capture (OpenAI SDK) ===');
+ sendToRenderer('update-status', 'Starting audio capture...');
const { app } = require('electron');
+ const fs = require('fs');
let systemAudioPath;
if (app.isPackaged) {
@@ -452,7 +454,35 @@ async function startMacOSAudioCapture() {
systemAudioPath = path.join(__dirname, '../assets', 'SystemAudioDump');
}
- console.log('SystemAudioDump path:', systemAudioPath);
+ console.log('SystemAudioDump config:', {
+ path: systemAudioPath,
+ isPackaged: app.isPackaged,
+ resourcesPath: process.resourcesPath,
+ exists: fs.existsSync(systemAudioPath),
+ });
+
+ // Check if file exists
+ if (!fs.existsSync(systemAudioPath)) {
+ console.error('FATAL: SystemAudioDump not found at:', systemAudioPath);
+ sendToRenderer('update-status', 'Error: Audio binary not found');
+ return false;
+ }
+
+ // Check and fix executable permissions
+ try {
+ fs.accessSync(systemAudioPath, fs.constants.X_OK);
+ console.log('SystemAudioDump is executable');
+ } catch (err) {
+ console.warn('SystemAudioDump not executable, fixing permissions...');
+ try {
+ fs.chmodSync(systemAudioPath, 0o755);
+ console.log('Fixed executable permissions');
+ } catch (chmodErr) {
+ console.error('Failed to fix permissions:', chmodErr);
+ sendToRenderer('update-status', 'Error: Cannot execute audio binary');
+ return false;
+ }
+ }
const spawnOptions = {
stdio: ['ignore', 'pipe', 'pipe'],
@@ -461,10 +491,12 @@ async function startMacOSAudioCapture() {
},
};
+ console.log('Spawning SystemAudioDump...');
systemAudioProc = spawn(systemAudioPath, [], spawnOptions);
if (!systemAudioProc.pid) {
- console.error('Failed to start SystemAudioDump');
+ console.error('FATAL: Failed to start SystemAudioDump - no PID');
+ sendToRenderer('update-status', 'Error: Audio capture failed to start');
return false;
}
@@ -476,8 +508,16 @@ async function startMacOSAudioCapture() {
const CHUNK_SIZE = SAMPLE_RATE * BYTES_PER_SAMPLE * CHANNELS * CHUNK_DURATION;
let tempBuffer = Buffer.alloc(0);
+ let chunkCount = 0;
+ let firstDataReceived = false;
systemAudioProc.stdout.on('data', data => {
+ if (!firstDataReceived) {
+ firstDataReceived = true;
+ console.log('First audio data received! Size:', data.length);
+ sendToRenderer('update-status', 'Listening...');
+ }
+
tempBuffer = Buffer.concat([tempBuffer, data]);
while (tempBuffer.length >= CHUNK_SIZE) {
@@ -489,6 +529,11 @@ async function startMacOSAudioCapture() {
// Add to audio buffer for transcription
audioBuffer = Buffer.concat([audioBuffer, monoChunk]);
+
+ chunkCount++;
+ if (chunkCount % 100 === 0) {
+ console.log(`Audio: ${chunkCount} chunks processed, buffer size: ${audioBuffer.length}`);
+ }
}
// Limit buffer size (max 30 seconds of audio)
@@ -499,21 +544,33 @@ async function startMacOSAudioCapture() {
});
systemAudioProc.stderr.on('data', data => {
- console.error('SystemAudioDump stderr:', data.toString());
+ const msg = data.toString();
+ console.error('SystemAudioDump stderr:', msg);
+ if (msg.toLowerCase().includes('error')) {
+ sendToRenderer('update-status', 'Audio error: ' + msg.substring(0, 50));
+ }
});
- systemAudioProc.on('close', code => {
- console.log('SystemAudioDump process closed with code:', code);
+ systemAudioProc.on('close', (code, signal) => {
+ console.log('SystemAudioDump closed:', { code, signal, chunksProcessed: chunkCount, tempBufferSize: tempBuffer.length });
+ if (code !== 0 && code !== null) {
+ sendToRenderer('update-status', `Audio stopped (exit: ${code}, signal: ${signal})`);
+ }
systemAudioProc = null;
stopTranscriptionTimer();
});
systemAudioProc.on('error', err => {
- console.error('SystemAudioDump process error:', err);
+ console.error('SystemAudioDump spawn error:', err.message, err.stack);
+ sendToRenderer('update-status', 'Audio error: ' + err.message);
systemAudioProc = null;
stopTranscriptionTimer();
});
+ systemAudioProc.on('exit', (code, signal) => {
+ console.log('SystemAudioDump exit event:', { code, signal });
+ });
+
// Start periodic transcription
startTranscriptionTimer();