11import { Component , onMount , onCleanup , Show , createMemo , createEffect , createSignal } from "solid-js"
2+ import { Dialog } from "@kobalte/core/dialog"
23import { Toaster } from "solid-toast"
34import type { Session } from "./types/session"
45import type { Attachment } from "./types/attachment"
@@ -352,6 +353,29 @@ const App: Component = () => {
352353 const commandRegistry = createCommandRegistry ( )
353354 const [ escapeInDebounce , setEscapeInDebounce ] = createSignal ( false )
354355 const [ paletteCommands , setPaletteCommands ] = createSignal < Command [ ] > ( [ ] )
356+ const [ launchErrorBinary , setLaunchErrorBinary ] = createSignal < string | null > ( null )
357+ const [ isAdvancedSettingsOpen , setIsAdvancedSettingsOpen ] = createSignal ( false )
358+
359+ const launchErrorPath = ( ) => {
360+ const value = launchErrorBinary ( )
361+ if ( ! value ) return "opencode"
362+ return value . trim ( ) || "opencode"
363+ }
364+
365+ const isMissingBinaryError = ( error : unknown ) : boolean => {
366+ if ( ! error ) return false
367+ const message = typeof error === "string" ? error : error instanceof Error ? error . message : String ( error )
368+ const normalized = message . toLowerCase ( )
369+ return (
370+ normalized . includes ( "opencode binary not found" ) ||
371+ normalized . includes ( "binary not found" ) ||
372+ normalized . includes ( "no such file or directory" ) ||
373+ normalized . includes ( "binary is not executable" ) ||
374+ normalized . includes ( "enoent" )
375+ )
376+ }
377+
378+ const clearLaunchError = ( ) => setLaunchErrorBinary ( null )
355379
356380 const refreshCommandPalette = ( ) => {
357381 setPaletteCommands ( commandRegistry . getAll ( ) )
@@ -361,15 +385,12 @@ const App: Component = () => {
361385 void initMarkdown ( isDark ( ) ) . catch ( console . error )
362386 } )
363387
364-
365-
366388 const activeInstance = createMemo ( ( ) => getActiveInstance ( ) )
367389
368390 const activeSessions = createMemo ( ( ) => {
369391 const instance = activeInstance ( )
370392 if ( ! instance ) return new Map ( )
371393 const instanceId = instance . id
372-
373394 const parentId = activeParentSessionId ( ) . get ( instanceId )
374395 if ( ! parentId ) return new Map ( )
375396
@@ -383,6 +404,7 @@ const App: Component = () => {
383404 return activeSessionId ( ) . get ( instance . id ) || null
384405 } )
385406
407+
386408 const activeSessionForInstance = createMemo ( ( ) => {
387409 const sessionId = activeSessionIdForInstance ( )
388410 if ( ! sessionId || sessionId === "info" ) return null
@@ -408,6 +430,7 @@ const App: Component = () => {
408430
409431 async function handleSelectFolder ( folderPath ?: string , binaryPath ?: string ) {
410432 setIsSelectingFolder ( true )
433+ const selectedBinary = binaryPath || preferences ( ) . lastUsedBinary || "opencode"
411434 try {
412435 let folder : string | null | undefined = folderPath
413436
@@ -418,19 +441,38 @@ const App: Component = () => {
418441 }
419442 }
420443
444+ if ( ! folder ) {
445+ return
446+ }
447+
421448 addRecentFolder ( folder )
422- const instanceId = await createInstance ( folder , binaryPath )
449+ clearLaunchError ( )
450+ const instanceId = await createInstance ( folder , selectedBinary )
423451 setHasInstances ( true )
424452 setShowFolderSelection ( false )
453+ setIsAdvancedSettingsOpen ( false )
425454
426455 console . log ( "Created instance:" , instanceId , "Port:" , instances ( ) . get ( instanceId ) ?. port )
427456 } catch ( error ) {
457+ clearLaunchError ( )
458+ if ( isMissingBinaryError ( error ) ) {
459+ setLaunchErrorBinary ( selectedBinary )
460+ }
428461 console . error ( "Failed to create instance:" , error )
429462 } finally {
430463 setIsSelectingFolder ( false )
431464 }
432465 }
433466
467+ function handleLaunchErrorClose ( ) {
468+ clearLaunchError ( )
469+ }
470+
471+ function handleLaunchErrorAdvanced ( ) {
472+ clearLaunchError ( )
473+ setIsAdvancedSettingsOpen ( true )
474+ }
475+
434476 function handleNewInstanceRequest ( ) {
435477 if ( hasInstances ( ) ) {
436478 setShowFolderSelection ( true )
@@ -1055,6 +1097,41 @@ const App: Component = () => {
10551097 reason = { disconnectedInstance ( ) ?. reason }
10561098 onClose = { handleDisconnectedInstanceClose }
10571099 />
1100+
1101+ < Dialog open = { Boolean ( launchErrorBinary ( ) ) } modal >
1102+ < Dialog . Portal >
1103+ < Dialog . Overlay class = "modal-overlay" />
1104+ < div class = "fixed inset-0 z-50 flex items-center justify-center p-4" >
1105+ < Dialog . Content class = "modal-surface w-full max-w-md p-6 flex flex-col gap-6" >
1106+ < div >
1107+ < Dialog . Title class = "text-xl font-semibold text-primary" > Unable to launch OpenCode</ Dialog . Title >
1108+ < Dialog . Description class = "text-sm text-secondary mt-2" >
1109+ Install the OpenCode CLI and make sure it is available in your PATH, or pick a custom binary from
1110+ Advanced Settings.
1111+ </ Dialog . Description >
1112+ </ div >
1113+
1114+ < div class = "rounded-lg border border-base bg-surface-secondary p-4" >
1115+ < p class = "text-xs font-medium text-muted uppercase tracking-wide mb-1" > Binary path</ p >
1116+ < p class = "text-sm font-mono text-primary break-all" > { launchErrorPath ( ) } </ p >
1117+ </ div >
1118+
1119+ < div class = "flex justify-end gap-2" >
1120+ < button
1121+ type = "button"
1122+ class = "selector-button selector-button-secondary"
1123+ onClick = { handleLaunchErrorAdvanced }
1124+ >
1125+ Open Advanced Settings
1126+ </ button >
1127+ < button type = "button" class = "selector-button selector-button-primary" onClick = { handleLaunchErrorClose } >
1128+ Close
1129+ </ button >
1130+ </ div >
1131+ </ Dialog . Content >
1132+ </ div >
1133+ </ Dialog . Portal >
1134+ </ Dialog >
10581135 < div class = "h-screen w-screen flex flex-col" >
10591136 < Show
10601137 when = { ! hasInstances ( ) }
@@ -1171,7 +1248,13 @@ const App: Component = () => {
11711248 </ >
11721249 }
11731250 >
1174- < FolderSelectionView onSelectFolder = { handleSelectFolder } isLoading = { isSelectingFolder ( ) } />
1251+ < FolderSelectionView
1252+ onSelectFolder = { handleSelectFolder }
1253+ isLoading = { isSelectingFolder ( ) }
1254+ advancedSettingsOpen = { isAdvancedSettingsOpen ( ) }
1255+ onAdvancedSettingsOpen = { ( ) => setIsAdvancedSettingsOpen ( true ) }
1256+ onAdvancedSettingsClose = { ( ) => setIsAdvancedSettingsOpen ( false ) }
1257+ />
11751258 </ Show >
11761259
11771260 < CommandPalette
@@ -1185,7 +1268,11 @@ const App: Component = () => {
11851268 < div class = "fixed inset-0 bg-black/50 z-50 flex items-center justify-center" >
11861269 < div class = "w-full h-full relative" >
11871270 < button
1188- onClick = { ( ) => setShowFolderSelection ( false ) }
1271+ onClick = { ( ) => {
1272+ setShowFolderSelection ( false )
1273+ setIsAdvancedSettingsOpen ( false )
1274+ clearLaunchError ( )
1275+ } }
11891276 class = "absolute top-4 right-4 z-10 p-2 bg-white dark:bg-gray-800 rounded-lg shadow-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
11901277 title = "Close (Esc)"
11911278 >
@@ -1198,7 +1285,13 @@ const App: Component = () => {
11981285 < path stroke-linecap = "round" stroke-linejoin = "round" stroke-width = "2" d = "M6 18L18 6M6 6l12 12" />
11991286 </ svg >
12001287 </ button >
1201- < FolderSelectionView onSelectFolder = { handleSelectFolder } isLoading = { isSelectingFolder ( ) } />
1288+ < FolderSelectionView
1289+ onSelectFolder = { handleSelectFolder }
1290+ isLoading = { isSelectingFolder ( ) }
1291+ advancedSettingsOpen = { isAdvancedSettingsOpen ( ) }
1292+ onAdvancedSettingsOpen = { ( ) => setIsAdvancedSettingsOpen ( true ) }
1293+ onAdvancedSettingsClose = { ( ) => setIsAdvancedSettingsOpen ( false ) }
1294+ />
12021295 </ div >
12031296 </ div >
12041297 </ Show >
0 commit comments