@@ -18,6 +18,7 @@ import {
1818 LoaderIcon ,
1919 CircleIcon ,
2020 CopyIcon ,
21+ DownloadIcon ,
2122} from "lucide-react" ;
2223
2324interface InstallProgress {
@@ -40,22 +41,25 @@ interface InstallWizardProps {
4041
4142type WizardPhase =
4243 | "checking"
44+ | "confirm"
4345 | "already-installed"
4446 | "installing"
4547 | "success"
4648 | "failed" ;
4749
50+ interface PrereqResult {
51+ hasNode : boolean ;
52+ nodeVersion ?: string ;
53+ hasClaude : boolean ;
54+ claudeVersion ?: string ;
55+ }
56+
4857function getInstallAPI ( ) {
4958 if ( typeof window !== "undefined" ) {
5059 // eslint-disable-next-line @typescript-eslint/no-explicit-any
5160 return ( window as any ) . electronAPI ?. install as
5261 | {
53- checkPrerequisites : ( ) => Promise < {
54- hasNode : boolean ;
55- nodeVersion ?: string ;
56- hasClaude : boolean ;
57- claudeVersion ?: string ;
58- } > ;
62+ checkPrerequisites : ( ) => Promise < PrereqResult > ;
5963 start : ( options ?: { includeNode ?: boolean } ) => Promise < void > ;
6064 cancel : ( ) => Promise < void > ;
6165 getLogs : ( ) => Promise < string [ ] > ;
@@ -92,6 +96,7 @@ export function InstallWizard({
9296 const [ progress , setProgress ] = useState < InstallProgress | null > ( null ) ;
9397 const [ logs , setLogs ] = useState < string [ ] > ( [ ] ) ;
9498 const [ copied , setCopied ] = useState ( false ) ;
99+ const [ prereqs , setPrereqs ] = useState < PrereqResult | null > ( null ) ;
95100 const logEndRef = useRef < HTMLDivElement > ( null ) ;
96101 const cleanupRef = useRef < ( ( ) => void ) | null > ( null ) ;
97102
@@ -103,6 +108,17 @@ export function InstallWizard({
103108 scrollToBottom ( ) ;
104109 } , [ logs , scrollToBottom ] ) ;
105110
111+ // Cancel backend install and clean up listener
112+ const cancelInstall = useCallback ( async ( ) => {
113+ const api = getInstallAPI ( ) ;
114+ if ( ! api ) return ;
115+ try {
116+ await api . cancel ( ) ;
117+ } catch {
118+ // ignore cancel errors
119+ }
120+ } , [ ] ) ;
121+
106122 const startInstall = useCallback ( async ( options ?: { includeNode ?: boolean } ) => {
107123 const api = getInstallAPI ( ) ;
108124 if ( ! api ) return ;
@@ -138,9 +154,11 @@ export function InstallWizard({
138154 setPhase ( "checking" ) ;
139155 setLogs ( [ "Checking environment..." ] ) ;
140156 setProgress ( null ) ;
157+ setPrereqs ( null ) ;
141158
142159 try {
143160 const result = await api . checkPrerequisites ( ) ;
161+ setPrereqs ( result ) ;
144162
145163 if ( result . hasClaude ) {
146164 setLogs ( ( prev ) => [
@@ -152,37 +170,34 @@ export function InstallWizard({
152170 return ;
153171 }
154172
155- if ( ! result . hasNode ) {
173+ // Don't auto-install — show confirmation first
174+ if ( result . hasNode ) {
156175 setLogs ( ( prev ) => [
157176 ...prev ,
158- "Node.js not found. Will attempt to install Node.js and Claude Code..." ,
177+ `Node.js ${ result . nodeVersion } found.` ,
178+ "Claude Code CLI not detected." ,
159179 ] ) ;
160- startInstall ( { includeNode : true } ) ;
161180 } else {
162181 setLogs ( ( prev ) => [
163182 ...prev ,
164- ` Node.js ${ result . nodeVersion } found.` ,
165- "Claude Code not found. Starting installation.. ." ,
183+ " Node.js not found." ,
184+ "Claude Code CLI not detected ." ,
166185 ] ) ;
167- startInstall ( ) ;
168186 }
187+ setPhase ( "confirm" ) ;
169188 } catch ( err : unknown ) {
170189 setPhase ( "failed" ) ;
171190 const msg = err instanceof Error ? err . message : String ( err ) ;
172191 setLogs ( ( prev ) => [ ...prev , `Error checking prerequisites: ${ msg } ` ] ) ;
173192 }
174- } , [ startInstall ] ) ;
175-
176- const handleCancel = useCallback ( async ( ) => {
177- const api = getInstallAPI ( ) ;
178- if ( ! api ) return ;
179- try {
180- await api . cancel ( ) ;
181- } catch {
182- // ignore cancel errors
183- }
184193 } , [ ] ) ;
185194
195+ // User explicitly clicks "Install" — only then start the actual install
196+ const handleConfirmInstall = useCallback ( ( ) => {
197+ const needsNode = prereqs ? ! prereqs . hasNode : false ;
198+ startInstall ( { includeNode : needsNode } ) ;
199+ } , [ prereqs , startInstall ] ) ;
200+
186201 const handleCopyLogs = useCallback ( async ( ) => {
187202 try {
188203 await navigator . clipboard . writeText ( logs . join ( "\n" ) ) ;
@@ -198,13 +213,25 @@ export function InstallWizard({
198213 onInstallComplete ?.( ) ;
199214 } , [ onOpenChange , onInstallComplete ] ) ;
200215
216+ // [P1] Close dialog = cancel running install
217+ const handleOpenChange = useCallback (
218+ async ( nextOpen : boolean ) => {
219+ if ( ! nextOpen && phase === "installing" ) {
220+ await cancelInstall ( ) ;
221+ }
222+ onOpenChange ( nextOpen ) ;
223+ } ,
224+ [ phase , cancelInstall , onOpenChange ]
225+ ) ;
226+
201227 // Auto-check when dialog opens
202228 useEffect ( ( ) => {
203229 if ( open ) {
204230 setPhase ( "checking" ) ; // eslint-disable-line react-hooks/set-state-in-effect -- reset state before async check
205231 setLogs ( [ ] ) ; // eslint-disable-line react-hooks/set-state-in-effect
206232 setProgress ( null ) ; // eslint-disable-line react-hooks/set-state-in-effect
207233 setCopied ( false ) ; // eslint-disable-line react-hooks/set-state-in-effect
234+ setPrereqs ( null ) ; // eslint-disable-line react-hooks/set-state-in-effect
208235 checkPrereqs ( ) ;
209236 }
210237 return ( ) => {
@@ -218,17 +245,19 @@ export function InstallWizard({
218245 const steps = progress ?. steps ?? [ ] ;
219246
220247 return (
221- < Dialog open = { open } onOpenChange = { onOpenChange } >
248+ < Dialog open = { open } onOpenChange = { handleOpenChange } >
222249 < DialogContent className = "sm:max-w-lg" >
223250 < DialogHeader >
224251 < DialogTitle > Install Claude Code</ DialogTitle >
225252 < DialogDescription >
226- Automatically install Claude Code CLI
253+ { phase === "confirm"
254+ ? "Claude Code CLI was not detected. Install it now?"
255+ : "Automatically install Claude Code CLI" }
227256 </ DialogDescription >
228257 </ DialogHeader >
229258
230259 < div className = "space-y-4" >
231- { /* Step list */ }
260+ { /* Step list (only during/after install) */ }
232261 { steps . length > 0 && (
233262 < div className = "space-y-2" >
234263 { steps . map ( ( step ) => (
@@ -258,14 +287,40 @@ export function InstallWizard({
258287 </ div >
259288 ) }
260289
261- { /* Phase-specific messages */ }
290+ { /* Phase: checking */ }
262291 { phase === "checking" && steps . length === 0 && (
263292 < div className = "flex items-center gap-2.5 text-sm text-muted-foreground" >
264293 < LoaderIcon className = "size-4 animate-spin" />
265294 < span > Checking environment...</ span >
266295 </ div >
267296 ) }
268297
298+ { /* Phase: confirm — ask user before installing */ }
299+ { phase === "confirm" && (
300+ < div className = "space-y-3" >
301+ < div className = "rounded-lg bg-amber-500/10 px-4 py-3 text-sm space-y-1.5" >
302+ { prereqs && ! prereqs . hasNode && (
303+ < p className = "text-amber-700 dark:text-amber-400" >
304+ Node.js — not found (will be installed via { process . platform === "win32" ? "winget" : "Homebrew" } )
305+ </ p >
306+ ) }
307+ { prereqs ?. hasNode && (
308+ < p className = "text-emerald-700 dark:text-emerald-400" >
309+ Node.js { prereqs . nodeVersion } — found
310+ </ p >
311+ ) }
312+ < p className = "text-amber-700 dark:text-amber-400" >
313+ Claude Code CLI — not found
314+ </ p >
315+ </ div >
316+ < p className = "text-sm text-muted-foreground" >
317+ Click < strong > Install</ strong > to automatically set up{ " " }
318+ { prereqs && ! prereqs . hasNode ? "Node.js and " : "" } Claude Code CLI.
319+ </ p >
320+ </ div >
321+ ) }
322+
323+ { /* Phase: already-installed */ }
269324 { phase === "already-installed" && (
270325 < div className = "flex items-center gap-3 rounded-lg bg-emerald-500/10 px-4 py-3" >
271326 < CheckIcon className = "size-5 text-emerald-500 shrink-0" />
@@ -280,6 +335,7 @@ export function InstallWizard({
280335 </ div >
281336 ) }
282337
338+ { /* Phase: success */ }
283339 { phase === "success" && (
284340 < div className = "flex items-center gap-3 rounded-lg bg-emerald-500/10 px-4 py-3" >
285341 < CheckIcon className = "size-5 text-emerald-500 shrink-0" />
@@ -310,28 +366,40 @@ export function InstallWizard({
310366 </ div >
311367
312368 < DialogFooter >
313- < Button
314- variant = "outline"
315- size = "sm"
316- onClick = { handleCopyLogs }
317- disabled = { logs . length === 0 }
318- >
319- < CopyIcon />
320- { copied ? "Copied" : "Copy Logs" }
321- </ Button >
369+ { logs . length > 0 && (
370+ < Button
371+ variant = "outline"
372+ size = "sm"
373+ onClick = { handleCopyLogs }
374+ >
375+ < CopyIcon />
376+ { copied ? "Copied" : "Copy Logs" }
377+ </ Button >
378+ ) }
379+
380+ { /* Confirm phase: single "Install" button */ }
381+ { phase === "confirm" && (
382+ < Button size = "sm" onClick = { handleConfirmInstall } >
383+ < DownloadIcon />
384+ Install
385+ </ Button >
386+ ) }
322387
388+ { /* Installing: cancel button */ }
323389 { phase === "installing" && (
324- < Button variant = "destructive" size = "sm" onClick = { handleCancel } >
390+ < Button variant = "destructive" size = "sm" onClick = { cancelInstall } >
325391 Cancel
326392 </ Button >
327393 ) }
328394
395+ { /* Failed: retry */ }
329396 { phase === "failed" && (
330397 < Button size = "sm" onClick = { checkPrereqs } >
331398 Retry
332399 </ Button >
333400 ) }
334401
402+ { /* Success / already-installed: done */ }
335403 { ( phase === "success" || phase === "already-installed" ) && (
336404 < Button size = "sm" onClick = { handleDone } >
337405 Done
0 commit comments