@@ -279,22 +279,56 @@ function RuntimeHeader({
279279}
280280
281281function RuntimeRow ( {
282- installError,
283- installSuccess,
284- isInstalling,
285- onInstall,
282+ resetEpoch,
286283 runtime,
287284} : {
288- installError : string | null ;
289- installSuccess : boolean ;
290- isInstalling : boolean ;
291- onInstall : ( ) => void ;
285+ resetEpoch : number ;
292286 runtime : AcpRuntimeCatalogEntry ;
293287} ) {
294288 const [ terminalLaunchMethodId , setTerminalLaunchMethodId ] = React . useState <
295289 string | null
296290 > ( null ) ;
297291 const [ isUpdateWarningOpen , setIsUpdateWarningOpen ] = React . useState ( false ) ;
292+ // Each row owns its mutation instance so concurrent installs each track
293+ // their own isPending / result state independently.
294+ const installMutation = useInstallAcpRuntimeMutation ( ) ;
295+ const [ installResult , setInstallResult ] = React . useState < {
296+ success : boolean ;
297+ error : string | null ;
298+ } | null > ( null ) ;
299+ // Clear stale install results when the parent triggers a catalog refresh
300+ // (Check again) — the runtime may now be healthy and stale failure state
301+ // would linger because keyed rows don't remount on refetch.
302+ // biome-ignore lint/correctness/useExhaustiveDependencies: resetEpoch is an intentional trigger only; its value is not consumed in the effect body
303+ React . useEffect ( ( ) => {
304+ setInstallResult ( null ) ;
305+ } , [ resetEpoch ] ) ;
306+ const isInstalling = installMutation . isPending ;
307+ const installError = installResult ?. error ?? null ;
308+ const installSuccess = installResult ?. success ?? false ;
309+
310+ function handleInstall ( ) {
311+ setInstallResult ( null ) ;
312+ installMutation . mutate ( runtime . id , {
313+ onSuccess : ( result ) => {
314+ if ( result . success ) {
315+ setInstallResult ( { success : true , error : null } ) ;
316+ } else {
317+ setInstallResult ( {
318+ success : false ,
319+ error : getInstallErrorMessage ( result . steps ) ,
320+ } ) ;
321+ }
322+ } ,
323+ onError : ( error ) => {
324+ setInstallResult ( {
325+ success : false ,
326+ error : error instanceof Error ? error . message : "Install failed." ,
327+ } ) ;
328+ } ,
329+ } ) ;
330+ }
331+
298332 const canConnectAccount =
299333 runtime . availability === "available" &&
300334 runtime . authStatus . status === "logged_out" ;
@@ -352,7 +386,7 @@ function RuntimeRow({
352386 setIsUpdateWarningOpen ( true ) ;
353387 return ;
354388 }
355- onInstall ( ) ;
389+ handleInstall ( ) ;
356390 } }
357391 runtime = { runtime }
358392 />
@@ -372,7 +406,10 @@ function RuntimeRow({
372406 </ p >
373407 ) : null }
374408 { installError ? (
375- < p className = "mt-2 whitespace-pre-line rounded-lg border border-destructive/30 bg-destructive/10 px-3 py-1.5 text-sm text-destructive" >
409+ < p
410+ className = "mt-2 whitespace-pre-line rounded-lg border border-destructive/30 bg-destructive/10 px-3 py-1.5 text-sm text-destructive"
411+ data-testid = { `doctor-runtime-install-error-${ runtime . id } ` }
412+ >
376413 { installError }
377414 </ p >
378415 ) : null }
@@ -410,7 +447,7 @@ function RuntimeRow({
410447 < AlertDialogFooter >
411448 < AlertDialogCancel > Cancel</ AlertDialogCancel >
412449 < AlertDialogAction
413- onClick = { onInstall }
450+ onClick = { handleInstall }
414451 data-testid = { `doctor-runtime-confirm-update-${ runtime . id } ` }
415452 >
416453 Update
@@ -491,59 +528,9 @@ export function DoctorSettingsPanel() {
491528 [ runtimesQuery . data ] ,
492529 ) ;
493530 const isRefreshing = runtimesQuery . isFetching ;
494- const installMutation = useInstallAcpRuntimeMutation ( ) ;
495- const [ installResults , setInstallResults ] = React . useState <
496- Record < string , { success : boolean ; error : string | null } >
497- > ( { } ) ;
498- // Per-runtime installing state: tracks which runtime IDs have an in-flight
499- // install so concurrent installs each show their own spinner correctly.
500- const [ installingIds , setInstallingIds ] = React . useState < Set < string > > (
501- new Set ( ) ,
502- ) ;
503-
504- function handleInstall ( runtimeId : string ) {
505- // Clear any previous result for this runtime before retrying.
506- setInstallResults ( ( prev ) => ( {
507- ...prev ,
508- [ runtimeId ] : { success : false , error : null } ,
509- } ) ) ;
510- setInstallingIds ( ( prev ) => new Set ( prev ) . add ( runtimeId ) ) ;
511-
512- installMutation . mutate ( runtimeId , {
513- onSuccess : ( result ) => {
514- if ( result . success ) {
515- setInstallResults ( ( prev ) => ( {
516- ...prev ,
517- [ runtimeId ] : { success : true , error : null } ,
518- } ) ) ;
519- } else {
520- setInstallResults ( ( prev ) => ( {
521- ...prev ,
522- [ runtimeId ] : {
523- success : false ,
524- error : getInstallErrorMessage ( result . steps ) ,
525- } ,
526- } ) ) ;
527- }
528- } ,
529- onError : ( error ) => {
530- setInstallResults ( ( prev ) => ( {
531- ...prev ,
532- [ runtimeId ] : {
533- success : false ,
534- error : error instanceof Error ? error . message : "Install failed." ,
535- } ,
536- } ) ) ;
537- } ,
538- onSettled : ( ) => {
539- setInstallingIds ( ( prev ) => {
540- const next = new Set ( prev ) ;
541- next . delete ( runtimeId ) ;
542- return next ;
543- } ) ;
544- } ,
545- } ) ;
546- }
531+ // Incremented each time the user clicks "Check again" so RuntimeRow
532+ // useEffect clears stale install results from before the refresh.
533+ const [ resetEpoch , setResetEpoch ] = React . useState ( 0 ) ;
547534
548535 return (
549536 < section
@@ -558,7 +545,7 @@ export function DoctorSettingsPanel() {
558545 < Button
559546 disabled = { isRefreshing }
560547 onClick = { ( ) => {
561- setInstallResults ( { } ) ;
548+ setResetEpoch ( ( e ) => e + 1 ) ;
562549 void runtimesQuery . refetch ( ) ;
563550 void gitBashQuery . refetch ( ) ;
564551 } }
@@ -598,11 +585,8 @@ export function DoctorSettingsPanel() {
598585 < div className = "space-y-3" data-testid = "doctor-runtime-list" >
599586 { runtimes . map ( ( runtime ) => (
600587 < RuntimeRow
601- installError = { installResults [ runtime . id ] ?. error ?? null }
602- installSuccess = { installResults [ runtime . id ] ?. success ?? false }
603- isInstalling = { installingIds . has ( runtime . id ) }
604588 key = { runtime . id }
605- onInstall = { ( ) => handleInstall ( runtime . id ) }
589+ resetEpoch = { resetEpoch }
606590 runtime = { runtime }
607591 />
608592 ) ) }
0 commit comments