diff --git a/src/App.tsx b/src/App.tsx index 26f454bc..36cea53e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,5 @@ import { Component, onMount, onCleanup, Show, createMemo, createEffect, createSignal } from "solid-js" +import { Dialog } from "@kobalte/core/dialog" import { Toaster } from "solid-toast" import type { Session } from "./types/session" import type { Attachment } from "./types/attachment" @@ -352,6 +353,29 @@ const App: Component = () => { const commandRegistry = createCommandRegistry() const [escapeInDebounce, setEscapeInDebounce] = createSignal(false) const [paletteCommands, setPaletteCommands] = createSignal([]) + const [launchErrorBinary, setLaunchErrorBinary] = createSignal(null) + const [isAdvancedSettingsOpen, setIsAdvancedSettingsOpen] = createSignal(false) + + const launchErrorPath = () => { + const value = launchErrorBinary() + if (!value) return "opencode" + return value.trim() || "opencode" + } + + const isMissingBinaryError = (error: unknown): boolean => { + if (!error) return false + const message = typeof error === "string" ? error : error instanceof Error ? error.message : String(error) + const normalized = message.toLowerCase() + return ( + normalized.includes("opencode binary not found") || + normalized.includes("binary not found") || + normalized.includes("no such file or directory") || + normalized.includes("binary is not executable") || + normalized.includes("enoent") + ) + } + + const clearLaunchError = () => setLaunchErrorBinary(null) const refreshCommandPalette = () => { setPaletteCommands(commandRegistry.getAll()) @@ -361,15 +385,12 @@ const App: Component = () => { void initMarkdown(isDark()).catch(console.error) }) - - const activeInstance = createMemo(() => getActiveInstance()) const activeSessions = createMemo(() => { const instance = activeInstance() if (!instance) return new Map() const instanceId = instance.id - const parentId = activeParentSessionId().get(instanceId) if (!parentId) return new Map() @@ -383,6 +404,7 @@ const App: Component = () => { return activeSessionId().get(instance.id) || null }) + const activeSessionForInstance = createMemo(() => { const sessionId = activeSessionIdForInstance() if (!sessionId || sessionId === "info") return null @@ -408,6 +430,7 @@ const App: Component = () => { async function handleSelectFolder(folderPath?: string, binaryPath?: string) { setIsSelectingFolder(true) + const selectedBinary = binaryPath || preferences().lastUsedBinary || "opencode" try { let folder: string | null | undefined = folderPath @@ -418,19 +441,38 @@ const App: Component = () => { } } + if (!folder) { + return + } + addRecentFolder(folder) - const instanceId = await createInstance(folder, binaryPath) + clearLaunchError() + const instanceId = await createInstance(folder, selectedBinary) setHasInstances(true) setShowFolderSelection(false) + setIsAdvancedSettingsOpen(false) console.log("Created instance:", instanceId, "Port:", instances().get(instanceId)?.port) } catch (error) { + clearLaunchError() + if (isMissingBinaryError(error)) { + setLaunchErrorBinary(selectedBinary) + } console.error("Failed to create instance:", error) } finally { setIsSelectingFolder(false) } } + function handleLaunchErrorClose() { + clearLaunchError() + } + + function handleLaunchErrorAdvanced() { + clearLaunchError() + setIsAdvancedSettingsOpen(true) + } + function handleNewInstanceRequest() { if (hasInstances()) { setShowFolderSelection(true) @@ -1055,6 +1097,41 @@ const App: Component = () => { reason={disconnectedInstance()?.reason} onClose={handleDisconnectedInstanceClose} /> + + + + +
+ +
+ Unable to launch OpenCode + + Install the OpenCode CLI and make sure it is available in your PATH, or pick a custom binary from + Advanced Settings. + +
+ +
+

Binary path

+

{launchErrorPath()}

+
+ +
+ + +
+
+
+
+
{ } > - + setIsAdvancedSettingsOpen(true)} + onAdvancedSettingsClose={() => setIsAdvancedSettingsOpen(false)} + /> {
- + setIsAdvancedSettingsOpen(true)} + onAdvancedSettingsClose={() => setIsAdvancedSettingsOpen(false)} + />
diff --git a/src/components/folder-selection-view.tsx b/src/components/folder-selection-view.tsx index 14542ce0..deae688f 100644 --- a/src/components/folder-selection-view.tsx +++ b/src/components/folder-selection-view.tsx @@ -9,12 +9,14 @@ const codeNomadLogo = new URL("../../images/CodeNomad-Icon.png", import.meta.url interface FolderSelectionViewProps { onSelectFolder: (folder?: string, binaryPath?: string) => void isLoading?: boolean + advancedSettingsOpen?: boolean + onAdvancedSettingsOpen?: () => void + onAdvancedSettingsClose?: () => void } const FolderSelectionView: Component = (props) => { const [selectedIndex, setSelectedIndex] = createSignal(0) const [focusMode, setFocusMode] = createSignal<"recent" | "new" | null>("recent") - const [isAdvancedModalOpen, setIsAdvancedModalOpen] = createSignal(false) const [selectedBinary, setSelectedBinary] = createSignal(preferences().lastUsedBinary || "opencode") let recentListRef: HTMLDivElement | undefined @@ -320,7 +322,7 @@ const FolderSelectionView: Component = (props) => { {/* Advanced settings section */}