Skip to content

Commit 5232e98

Browse files
committed
Fix install bugs: friendly Gemini errors, install button states, mic button after install
1 parent 201c2b6 commit 5232e98

3 files changed

Lines changed: 87 additions & 8 deletions

File tree

main.js

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1366,14 +1366,35 @@ class ApplicationController {
13661366
}
13671367
}
13681368

1369-
// If provider changed, reinitialize speech service so the new provider
1370-
// is picked up immediately without a restart.
1371-
if (settings.speechProvider && speechService.provider !== settings.speechProvider) {
1369+
// Reinitialize speech service when provider OR whisper command
1370+
// changes. Without the second check, the install flow (which
1371+
// writes a new whisperCommand after install but keeps the same
1372+
// provider) would leave the speech service pointing at a stale
1373+
// (or non-existent) binary, and the main overlay's mic button
1374+
// would stay hidden / non-functional.
1375+
const providerChanged = settings.speechProvider && speechService.provider !== settings.speechProvider;
1376+
const whisperCommandChanged = settings.whisperCommand !== undefined &&
1377+
(process.env.WHISPER_COMMAND || '') !== String(settings.whisperCommand || '');
1378+
if (providerChanged || whisperCommandChanged) {
13721379
try {
13731380
speechService.initializeClient();
13741381
this.speechAvailable = speechService.isAvailable
13751382
? speechService.isAvailable()
13761383
: false;
1384+
// Broadcast so any open window (settings, overlay, chat)
1385+
// can react immediately — especially the main overlay's
1386+
// mic button, which queries availability on load.
1387+
const { BrowserWindow } = require("electron");
1388+
BrowserWindow.getAllWindows().forEach((win) => {
1389+
if (!win.isDestroyed()) {
1390+
win.webContents.send("speech-availability", { available: this.speechAvailable });
1391+
}
1392+
});
1393+
logger.info('Speech service reinitialized after settings change', {
1394+
providerChanged,
1395+
whisperCommandChanged,
1396+
speechAvailable: this.speechAvailable,
1397+
});
13771398
} catch (e) {
13781399
logger.warn("Failed to reinitialize speech service after settings change", {
13791400
error: e.message

onboarding.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,10 +265,20 @@
265265
}
266266

267267
async function runWhisperInstall() {
268+
const btn = document.getElementById('installWhisperBtn');
268269
installLog.textContent = '';
269270
setDetectStatus('testing', 'Installing');
270271
appendLog('Starting install…');
271272

273+
// Lock the button while installing so the user can't double-click
274+
// and spawn parallel installs. Change the label to "Installing…"
275+
// with a spinner so they see real progress.
276+
if (btn) {
277+
btn.disabled = true;
278+
btn.dataset.originalHtml = btn.dataset.originalHtml || btn.innerHTML;
279+
btn.innerHTML = '<span class="spinner"></span> Installing…';
280+
}
281+
272282
// Subscribe to streamed progress lines from the main process.
273283
// `installWhisper()` only returns once install completes; live
274284
// output comes through `onInstallProgress` events.
@@ -286,13 +296,29 @@
286296
detectCmd.textContent = r.command;
287297
setDetectStatus('success', 'Installed');
288298
appendLog(`\n✓ ${r.message}`);
299+
if (btn) {
300+
// Keep button disabled — install is done. Show a checkmark
301+
// so the user sees the final state at a glance.
302+
btn.innerHTML = '<i class="fas fa-check-circle"></i> Installed';
303+
btn.classList.remove('primary');
304+
btn.classList.add('success');
305+
}
289306
} else {
290307
setDetectStatus('error', 'Install failed');
291308
appendLog(`\n✗ ${r.message}`);
309+
// Restore the button so the user can retry.
310+
if (btn) {
311+
btn.disabled = false;
312+
btn.innerHTML = btn.dataset.originalHtml || '<i class="fas fa-download"></i> Install Whisper now';
313+
}
292314
}
293315
} catch (e) {
294316
setDetectStatus('error', 'Install error');
295317
appendLog(`\n! ${e.message || e}`);
318+
if (btn) {
319+
btn.disabled = false;
320+
btn.innerHTML = btn.dataset.originalHtml || '<i class="fas fa-download"></i> Install Whisper now';
321+
}
296322
} finally {
297323
if (progressHandler && window.electronAPI.removeAllListeners) {
298324
try { window.electronAPI.removeAllListeners('install-progress'); } catch (_) { /* ignore */ }

src/services/llm.service.js

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1054,20 +1054,52 @@ Remember: Be intelligent about filtering - only provide detailed responses when
10541054
};
10551055
} catch (error) {
10561056
const errorAnalysis = this.analyzeError(error);
1057-
logger.error('Connection test failed', {
1057+
logger.error('Connection test failed', {
10581058
error: error.message,
10591059
errorAnalysis
10601060
});
1061-
1062-
return {
1063-
success: false,
1064-
error: error.message,
1061+
1062+
// Map raw SDK errors to user-friendly messages. The wizard only
1063+
// surfaces `error`, so any raw "[GoogleGenerativeAI Error]: ..."
1064+
// string would land in the UI verbatim.
1065+
const friendlyError = this._friendlyTestError(error, errorAnalysis);
1066+
1067+
return {
1068+
success: false,
1069+
error: friendlyError,
1070+
errorType: errorAnalysis?.type || 'UNKNOWN',
10651071
errorAnalysis,
10661072
networkConnectivity: await this.checkNetworkConnectivity().catch(() => null)
10671073
};
10681074
}
10691075
}
10701076

1077+
/**
1078+
* Translate raw SDK / network errors into something a user can act on.
1079+
*/
1080+
_friendlyTestError(error, analysis) {
1081+
const type = analysis?.type;
1082+
const raw = (error?.message || '').toLowerCase();
1083+
1084+
if (type === 'NETWORK_ERROR' || raw.includes('fetch failed') || raw.includes('enotfound')) {
1085+
return 'Cannot reach Google servers. Check your internet connection, firewall, or VPN settings.';
1086+
}
1087+
if (type === 'AUTH_ERROR' || raw.includes('api key') || raw.includes('401') || raw.includes('403')) {
1088+
return 'Invalid API key or insufficient permissions. Double-check the key at aistudio.google.com/apikey.';
1089+
}
1090+
if (type === 'RATE_LIMIT_ERROR' || raw.includes('429') || raw.includes('quota')) {
1091+
return 'Rate limit or quota exceeded. Wait a moment or check your Google Cloud billing.';
1092+
}
1093+
if (type === 'TIMEOUT_ERROR') {
1094+
return 'Request timed out. The Google API may be slow or unreachable right now.';
1095+
}
1096+
if (type === 'MODEL_ERROR' || raw.includes('model') || raw.includes('404')) {
1097+
return 'The configured Gemini model is unavailable. Try a different model in Settings.';
1098+
}
1099+
// Fall back to a stripped-down raw message (no SDK prefix noise)
1100+
return (error?.message || 'Connection failed').replace(/^\[GoogleGenerativeAI Error\]:\s*/i, '');
1101+
}
1102+
10711103
updateApiKey(newApiKey) {
10721104
process.env.GEMINI_API_KEY = newApiKey;
10731105
this.isInitialized = false;

0 commit comments

Comments
 (0)