Skip to content

Commit 4c7654a

Browse files
committed
Add multi-step onboarding wizard with Whisper detect/install
1 parent 444171d commit 4c7654a

6 files changed

Lines changed: 1915 additions & 7 deletions

File tree

main.js

Lines changed: 78 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@ class ApplicationController {
6060
this.firstRunManager = new FirstRunManager({
6161
logger: logger
6262
});
63+
// Lazily-initialised in getWhisperInstaller() so tests can mock
64+
// the constructor without polluting main-process startup.
65+
this._whisperInstaller = null;
6366
this.isFirstRun = false;
6467

6568
// Window configurations for reference
@@ -172,26 +175,28 @@ class ApplicationController {
172175
this.starting = false;
173176
this.isReady = true;
174177

175-
// First-run onboarding: ensure .env exists and prompt the user to
176-
// set their Gemini API key via the Settings window if it isn't
177-
// configured yet. Non-blocking — failure here just logs.
178+
// First-run onboarding: ensure .env exists and launch the
179+
// multi-step onboarding wizard if the user hasn't completed it.
180+
// Non-blocking — failure here just logs.
178181
try {
179182
this.firstRunManager.ensureEnv();
180183
const status = this.firstRunManager.getStatus();
181184
this.isFirstRun = status.needsOnboarding;
182185
logger.info("First-run status", status);
183186
if (this.isFirstRun) {
184187
// Defer slightly so all windows finish loading before we pop
185-
// the settings dialog on top of them.
188+
// the wizard on top of them.
186189
setTimeout(() => {
187190
try {
188-
this.showSettings();
191+
windowManager.showOnboarding();
189192
windowManager.broadcastToAllWindows("first-run", status);
190-
logger.info("First-run onboarding: settings window opened");
193+
logger.info("First-run onboarding: wizard opened");
191194
} catch (e) {
192-
logger.warn("Could not open first-run settings window", {
195+
logger.warn("Could not open first-run onboarding window", {
193196
error: e.message
194197
});
198+
// Fallback to legacy settings prompt
199+
try { this.showSettings(); } catch (_) { /* ignore */ }
195200
}
196201
}, 800);
197202
} else {
@@ -620,6 +625,61 @@ class ApplicationController {
620625
}
621626
});
622627

628+
// Open a URL in the system browser (used by the GitHub star button
629+
// in onboarding).
630+
ipcMain.handle("open-external", async (_event, url) => {
631+
try {
632+
if (typeof url !== "string" || !/^https?:\/\//i.test(url)) {
633+
return { ok: false, error: "Invalid URL" };
634+
}
635+
const { shell } = require("electron");
636+
await shell.openExternal(url);
637+
return { ok: true };
638+
} catch (e) {
639+
logger.warn("Failed to open external URL", { url, error: e.message });
640+
return { ok: false, error: e.message };
641+
}
642+
});
643+
644+
// Close the onboarding wizard window.
645+
ipcMain.handle("close-onboarding", () => {
646+
try {
647+
windowManager.closeOnboarding();
648+
return { success: true };
649+
} catch (e) {
650+
return { success: false, error: e.message };
651+
}
652+
});
653+
654+
// Detect an installed Whisper CLI across common locations.
655+
ipcMain.handle("detect-whisper", async () => {
656+
try {
657+
const installer = this.getWhisperInstaller();
658+
return await installer.detect();
659+
} catch (e) {
660+
logger.warn("Whisper detection failed", { error: e.message });
661+
return { found: false, command: null, version: null, error: e.message };
662+
}
663+
});
664+
665+
// Install Whisper. Streams progress lines back via `webContents.send`
666+
// so the renderer can paint them as they arrive.
667+
ipcMain.handle("install-whisper", async (event) => {
668+
try {
669+
const installer = this.getWhisperInstaller();
670+
const sender = event.sender;
671+
const result = await installer.install({
672+
onProgress: (line) => {
673+
try { sender.send("install-progress", line); } catch (_) { /* ignore */ }
674+
},
675+
});
676+
return result;
677+
} catch (e) {
678+
logger.error("Whisper install failed", { error: e.message });
679+
return { ok: false, command: null, message: e.message, logs: "" };
680+
}
681+
});
682+
623683
ipcMain.handle("save-settings", (event, settings) => {
624684
return this.saveSettings(settings);
625685
});
@@ -1182,6 +1242,17 @@ class ApplicationController {
11821242
});
11831243
}
11841244

1245+
getWhisperInstaller() {
1246+
if (!this._whisperInstaller) {
1247+
const WhisperInstaller = require("./src/core/whisper-installer");
1248+
this._whisperInstaller = new WhisperInstaller({
1249+
cwd: process.cwd(),
1250+
platform: process.platform,
1251+
});
1252+
}
1253+
return this._whisperInstaller;
1254+
}
1255+
11851256
getSettings() {
11861257
// Surface every value the settings UI can edit, reading the live source
11871258
// of truth (process.env) so the UI shows exactly what the running app is

0 commit comments

Comments
 (0)