@@ -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" || ! / ^ h t t p s ? : \/ \/ / 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