Skip to content

Commit 8431b9f

Browse files
committed
surface launch failures with guided advanced settings
1 parent 541027c commit 8431b9f

2 files changed

Lines changed: 106 additions & 11 deletions

File tree

‎src/App.tsx‎

Lines changed: 100 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Component, onMount, onCleanup, Show, createMemo, createEffect, createSignal } from "solid-js"
2+
import { Dialog } from "@kobalte/core/dialog"
23
import { Toaster } from "solid-toast"
34
import type { Session } from "./types/session"
45
import 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>

‎src/components/folder-selection-view.tsx‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,14 @@ const codeNomadLogo = new URL("../../images/CodeNomad-Icon.png", import.meta.url
99
interface FolderSelectionViewProps {
1010
onSelectFolder: (folder?: string, binaryPath?: string) => void
1111
isLoading?: boolean
12+
advancedSettingsOpen?: boolean
13+
onAdvancedSettingsOpen?: () => void
14+
onAdvancedSettingsClose?: () => void
1215
}
1316

1417
const FolderSelectionView: Component<FolderSelectionViewProps> = (props) => {
1518
const [selectedIndex, setSelectedIndex] = createSignal(0)
1619
const [focusMode, setFocusMode] = createSignal<"recent" | "new" | null>("recent")
17-
const [isAdvancedModalOpen, setIsAdvancedModalOpen] = createSignal(false)
1820
const [selectedBinary, setSelectedBinary] = createSignal(preferences().lastUsedBinary || "opencode")
1921
let recentListRef: HTMLDivElement | undefined
2022

@@ -320,7 +322,7 @@ const FolderSelectionView: Component<FolderSelectionViewProps> = (props) => {
320322
{/* Advanced settings section */}
321323
<div class="panel-section w-full">
322324
<button
323-
onClick={() => setIsAdvancedModalOpen(true)}
325+
onClick={() => props.onAdvancedSettingsOpen?.()}
324326
class="panel-section-header w-full justify-between"
325327
>
326328
<div class="flex items-center gap-2">
@@ -369,8 +371,8 @@ const FolderSelectionView: Component<FolderSelectionViewProps> = (props) => {
369371
</div>
370372

371373
<AdvancedSettingsModal
372-
open={isAdvancedModalOpen()}
373-
onClose={() => setIsAdvancedModalOpen(false)}
374+
open={Boolean(props.advancedSettingsOpen)}
375+
onClose={() => props.onAdvancedSettingsClose?.()}
374376
selectedBinary={selectedBinary()}
375377
onBinaryChange={handleBinaryChange}
376378
isLoading={props.isLoading}

0 commit comments

Comments
 (0)