@@ -489,6 +489,7 @@ export function AssistantPage() {
489489 const [ deletingThreadId , setDeletingThreadId ] = useState < string | null > ( null )
490490
491491 const sendLockRef = useRef ( false )
492+ const setupDialogPinnedRef = useRef ( false )
492493 const shimmerVisibleSinceRef = useRef < number | null > ( null )
493494 const shimmerHideTimerRef = useRef < number | null > ( null )
494495 const fileInputRef = useRef < HTMLInputElement | null > ( null )
@@ -568,25 +569,41 @@ export function AssistantPage() {
568569 }
569570 } , [ shouldShowPreResponseShimmer , showPreResponseShimmer ] )
570571
572+ const openSetupDialog = useCallback ( ( pinned = false ) => {
573+ setupDialogPinnedRef . current = pinned
574+ setSetupDialogOpen ( true )
575+ setModelDialogOpen ( false )
576+ } , [ ] )
577+
578+ const closeSetupDialog = useCallback ( ( ) => {
579+ setupDialogPinnedRef . current = false
580+ setSetupDialogOpen ( false )
581+ } , [ ] )
582+
583+ const closeSetupDialogIfAutoManaged = useCallback ( ( ) => {
584+ if ( ! setupDialogPinnedRef . current ) {
585+ setSetupDialogOpen ( false )
586+ }
587+ } , [ ] )
588+
571589 const refreshRuntimeStatus = useCallback ( async ( threadId ?: string ) => {
572590 const status = await window . electron . assistant . getRuntimeStatus ( threadId )
573591 setRuntimeStatus ( status )
574592
575593 if ( ! status . daemonReachable ) {
576- setSetupDialogOpen ( true )
577- setModelDialogOpen ( false )
594+ openSetupDialog ( false )
578595 return status
579596 }
580597
581- setSetupDialogOpen ( false )
598+ closeSetupDialogIfAutoManaged ( )
582599
583600 if ( status . daemonReachable && status . needsModelSelection ) {
584601 setModelDialogOpen ( true )
585602 setSelectedModel ( status . installedModels . find ( isLikelyVisionModel ) ?? '' )
586603 }
587604
588605 return status
589- } , [ ] )
606+ } , [ closeSetupDialogIfAutoManaged , openSetupDialog ] )
590607
591608 const refreshThreads = async ( ) => {
592609 const nextThreads = await window . electron . assistant . listThreads ( )
@@ -641,13 +658,13 @@ export function AssistantPage() {
641658 useEffect ( ( ) => {
642659 const unsubscribeStatus = window . electron . assistant . onRuntimeStatusChanged ( ( status ) => {
643660 setRuntimeStatus ( status )
661+
644662 if ( ! status . daemonReachable ) {
645- setSetupDialogOpen ( true )
646- setModelDialogOpen ( false )
663+ openSetupDialog ( false )
647664 return
648665 }
649666
650- setSetupDialogOpen ( false )
667+ closeSetupDialogIfAutoManaged ( )
651668
652669 if ( status . daemonReachable && status . needsModelSelection ) {
653670 setModelDialogOpen ( true )
@@ -730,7 +747,7 @@ export function AssistantPage() {
730747 unsubscribeRunEvent ( )
731748 unsubscribeApproval ( )
732749 }
733- } , [ activeThreadId , permissionMode , refreshRuntimeStatus ] )
750+ } , [ activeThreadId , closeSetupDialogIfAutoManaged , openSetupDialog , permissionMode , refreshRuntimeStatus ] )
734751
735752 useEffect ( ( ) => {
736753 if ( ! modelDialogOpen ) return
@@ -776,6 +793,20 @@ export function AssistantPage() {
776793 }
777794 } , [ ] )
778795
796+ useEffect ( ( ) => {
797+ const handleOpenSetup = ( ) => {
798+ openSetupDialog ( true )
799+ void window . electron . assistant . getRuntimeStatus ( activeThreadId ?? undefined ) . then ( ( status ) => {
800+ setRuntimeStatus ( status )
801+ } )
802+ }
803+
804+ window . addEventListener ( 'assistant:open-setup' , handleOpenSetup )
805+ return ( ) => {
806+ window . removeEventListener ( 'assistant:open-setup' , handleOpenSetup )
807+ }
808+ } , [ activeThreadId , openSetupDialog ] )
809+
779810 const handleCreateThread = async ( ) => {
780811 const created = await window . electron . assistant . createThread ( )
781812 await refreshThreads ( )
@@ -824,7 +855,7 @@ export function AssistantPage() {
824855 }
825856
826857 if ( appState === 'needs-setup' ) {
827- setSetupDialogOpen ( true )
858+ openSetupDialog ( false )
828859 return
829860 }
830861
@@ -998,11 +1029,11 @@ export function AssistantPage() {
9981029 const status = await refreshRuntimeStatus ( activeThreadId ?? undefined )
9991030
10001031 if ( ! status . daemonReachable ) {
1001- setSetupDialogOpen ( true )
1032+ openSetupDialog ( false )
10021033 return
10031034 }
10041035
1005- setSetupDialogOpen ( false )
1036+ closeSetupDialog ( )
10061037 if ( status . daemonReachable && status . needsModelSelection ) {
10071038 setModelDialogOpen ( true )
10081039 }
@@ -1219,7 +1250,7 @@ export function AssistantPage() {
12191250 < DropdownMenuItem
12201251 onSelect = { ( ) => {
12211252 if ( ! runtimeStatus ?. daemonReachable ) {
1222- setSetupDialogOpen ( true )
1253+ openSetupDialog ( false )
12231254 return
12241255 }
12251256 setModelDialogOpen ( true )
@@ -1325,12 +1356,21 @@ export function AssistantPage() {
13251356 </ SheetContent >
13261357 </ Sheet >
13271358
1328- < Dialog open = { setupDialogOpen } onOpenChange = { setSetupDialogOpen } >
1359+ < Dialog
1360+ open = { setupDialogOpen }
1361+ onOpenChange = { ( open ) => {
1362+ if ( open ) {
1363+ setSetupDialogOpen ( true )
1364+ return
1365+ }
1366+ closeSetupDialog ( )
1367+ } }
1368+ >
13291369 < DialogContent >
13301370 < DialogHeader >
1331- < DialogTitle > Set up Ollama</ DialogTitle >
1371+ < DialogTitle > Set up local Ollama</ DialogTitle >
13321372 < DialogDescription >
1333- The local Ollama API is not reachable. Start Ollama and make sure the endpoint responds before selecting a model .
1373+ Ollama is required for the local assistant. Complete this onboarding: install Ollama, run it, then install < b > gemma4:e4b </ b > .
13341374 </ DialogDescription >
13351375 </ DialogHeader >
13361376 < div className = "space-y-3 text-sm" >
@@ -1341,19 +1381,31 @@ export function AssistantPage() {
13411381 </ div >
13421382 </ div >
13431383
1344- { setupGuide ?. steps ?. length ? (
1345- < ol className = "space-y-1.5 pl-5" >
1346- { setupGuide . steps . map ( ( step ) => (
1347- < li key = { step } > { step } </ li >
1348- ) ) }
1349- </ ol >
1350- ) : null }
1351-
13521384 < div className = "space-y-1.5 rounded-md border p-3" >
1353- < p className = "text-muted-foreground text-xs" > Recommended multimodal model</ p >
1354- < code className = "block rounded bg-muted px-2 py-1 text-xs" > ollama pull qwen2.5-vl</ code >
1355- < code className = "block rounded bg-muted px-2 py-1 text-xs" > ollama serve</ code >
1356- < code className = "block rounded bg-muted px-2 py-1 text-xs" > ollama list</ code >
1385+ < p className = "font-medium" > Onboarding</ p >
1386+ < ol className = "space-y-2 pl-5" >
1387+ < li >
1388+ < p className = "font-medium" > Install Ollama</ p >
1389+ < p className = "text-muted-foreground text-xs" >
1390+ { setupGuide ?. steps ?. [ 0 ] ?? 'Install Ollama from the official website.' }
1391+ </ p >
1392+ </ li >
1393+ < li >
1394+ < p className = "font-medium" > Run Ollama</ p >
1395+ < code className = "mt-1 block rounded bg-muted px-2 py-1 text-xs" >
1396+ { setupGuide ?. serveCommand ?? 'ollama serve' }
1397+ </ code >
1398+ </ li >
1399+ < li >
1400+ < p className = "font-medium" > Install default model</ p >
1401+ < code className = "mt-1 block rounded bg-muted px-2 py-1 text-xs" >
1402+ { setupGuide ?. pullCommand ?? 'ollama pull gemma4:e4b' }
1403+ </ code >
1404+ < code className = "mt-1 block rounded bg-muted px-2 py-1 text-xs" >
1405+ { setupGuide ?. verifyCommand ?? 'ollama list' }
1406+ </ code >
1407+ </ li >
1408+ </ ol >
13571409 </ div >
13581410 </ div >
13591411 < DialogFooter >
0 commit comments