surface launch failures with guided advanced settings

This commit is contained in:
Shantur Rathore
2025-11-14 16:04:04 +00:00
parent 541027c93e
commit 8431b9f8a2
2 changed files with 106 additions and 11 deletions

View File

@@ -1,4 +1,5 @@
import { Component, onMount, onCleanup, Show, createMemo, createEffect, createSignal } from "solid-js" import { Component, onMount, onCleanup, Show, createMemo, createEffect, createSignal } from "solid-js"
import { Dialog } from "@kobalte/core/dialog"
import { Toaster } from "solid-toast" import { Toaster } from "solid-toast"
import type { Session } from "./types/session" import type { Session } from "./types/session"
import type { Attachment } from "./types/attachment" import type { Attachment } from "./types/attachment"
@@ -352,6 +353,29 @@ const App: Component = () => {
const commandRegistry = createCommandRegistry() const commandRegistry = createCommandRegistry()
const [escapeInDebounce, setEscapeInDebounce] = createSignal(false) const [escapeInDebounce, setEscapeInDebounce] = createSignal(false)
const [paletteCommands, setPaletteCommands] = createSignal<Command[]>([]) const [paletteCommands, setPaletteCommands] = createSignal<Command[]>([])
const [launchErrorBinary, setLaunchErrorBinary] = createSignal<string | null>(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 = () => { const refreshCommandPalette = () => {
setPaletteCommands(commandRegistry.getAll()) setPaletteCommands(commandRegistry.getAll())
@@ -361,15 +385,12 @@ const App: Component = () => {
void initMarkdown(isDark()).catch(console.error) void initMarkdown(isDark()).catch(console.error)
}) })
const activeInstance = createMemo(() => getActiveInstance()) const activeInstance = createMemo(() => getActiveInstance())
const activeSessions = createMemo(() => { const activeSessions = createMemo(() => {
const instance = activeInstance() const instance = activeInstance()
if (!instance) return new Map() if (!instance) return new Map()
const instanceId = instance.id const instanceId = instance.id
const parentId = activeParentSessionId().get(instanceId) const parentId = activeParentSessionId().get(instanceId)
if (!parentId) return new Map() if (!parentId) return new Map()
@@ -383,6 +404,7 @@ const App: Component = () => {
return activeSessionId().get(instance.id) || null return activeSessionId().get(instance.id) || null
}) })
const activeSessionForInstance = createMemo(() => { const activeSessionForInstance = createMemo(() => {
const sessionId = activeSessionIdForInstance() const sessionId = activeSessionIdForInstance()
if (!sessionId || sessionId === "info") return null if (!sessionId || sessionId === "info") return null
@@ -408,6 +430,7 @@ const App: Component = () => {
async function handleSelectFolder(folderPath?: string, binaryPath?: string) { async function handleSelectFolder(folderPath?: string, binaryPath?: string) {
setIsSelectingFolder(true) setIsSelectingFolder(true)
const selectedBinary = binaryPath || preferences().lastUsedBinary || "opencode"
try { try {
let folder: string | null | undefined = folderPath let folder: string | null | undefined = folderPath
@@ -418,19 +441,38 @@ const App: Component = () => {
} }
} }
if (!folder) {
return
}
addRecentFolder(folder) addRecentFolder(folder)
const instanceId = await createInstance(folder, binaryPath) clearLaunchError()
const instanceId = await createInstance(folder, selectedBinary)
setHasInstances(true) setHasInstances(true)
setShowFolderSelection(false) setShowFolderSelection(false)
setIsAdvancedSettingsOpen(false)
console.log("Created instance:", instanceId, "Port:", instances().get(instanceId)?.port) console.log("Created instance:", instanceId, "Port:", instances().get(instanceId)?.port)
} catch (error) { } catch (error) {
clearLaunchError()
if (isMissingBinaryError(error)) {
setLaunchErrorBinary(selectedBinary)
}
console.error("Failed to create instance:", error) console.error("Failed to create instance:", error)
} finally { } finally {
setIsSelectingFolder(false) setIsSelectingFolder(false)
} }
} }
function handleLaunchErrorClose() {
clearLaunchError()
}
function handleLaunchErrorAdvanced() {
clearLaunchError()
setIsAdvancedSettingsOpen(true)
}
function handleNewInstanceRequest() { function handleNewInstanceRequest() {
if (hasInstances()) { if (hasInstances()) {
setShowFolderSelection(true) setShowFolderSelection(true)
@@ -1055,6 +1097,41 @@ const App: Component = () => {
reason={disconnectedInstance()?.reason} reason={disconnectedInstance()?.reason}
onClose={handleDisconnectedInstanceClose} onClose={handleDisconnectedInstanceClose}
/> />
<Dialog open={Boolean(launchErrorBinary())} modal>
<Dialog.Portal>
<Dialog.Overlay class="modal-overlay" />
<div class="fixed inset-0 z-50 flex items-center justify-center p-4">
<Dialog.Content class="modal-surface w-full max-w-md p-6 flex flex-col gap-6">
<div>
<Dialog.Title class="text-xl font-semibold text-primary">Unable to launch OpenCode</Dialog.Title>
<Dialog.Description class="text-sm text-secondary mt-2">
Install the OpenCode CLI and make sure it is available in your PATH, or pick a custom binary from
Advanced Settings.
</Dialog.Description>
</div>
<div class="rounded-lg border border-base bg-surface-secondary p-4">
<p class="text-xs font-medium text-muted uppercase tracking-wide mb-1">Binary path</p>
<p class="text-sm font-mono text-primary break-all">{launchErrorPath()}</p>
</div>
<div class="flex justify-end gap-2">
<button
type="button"
class="selector-button selector-button-secondary"
onClick={handleLaunchErrorAdvanced}
>
Open Advanced Settings
</button>
<button type="button" class="selector-button selector-button-primary" onClick={handleLaunchErrorClose}>
Close
</button>
</div>
</Dialog.Content>
</div>
</Dialog.Portal>
</Dialog>
<div class="h-screen w-screen flex flex-col"> <div class="h-screen w-screen flex flex-col">
<Show <Show
when={!hasInstances()} when={!hasInstances()}
@@ -1171,7 +1248,13 @@ const App: Component = () => {
</> </>
} }
> >
<FolderSelectionView onSelectFolder={handleSelectFolder} isLoading={isSelectingFolder()} /> <FolderSelectionView
onSelectFolder={handleSelectFolder}
isLoading={isSelectingFolder()}
advancedSettingsOpen={isAdvancedSettingsOpen()}
onAdvancedSettingsOpen={() => setIsAdvancedSettingsOpen(true)}
onAdvancedSettingsClose={() => setIsAdvancedSettingsOpen(false)}
/>
</Show> </Show>
<CommandPalette <CommandPalette
@@ -1185,7 +1268,11 @@ const App: Component = () => {
<div class="fixed inset-0 bg-black/50 z-50 flex items-center justify-center"> <div class="fixed inset-0 bg-black/50 z-50 flex items-center justify-center">
<div class="w-full h-full relative"> <div class="w-full h-full relative">
<button <button
onClick={() => setShowFolderSelection(false)} onClick={() => {
setShowFolderSelection(false)
setIsAdvancedSettingsOpen(false)
clearLaunchError()
}}
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" 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"
title="Close (Esc)" title="Close (Esc)"
> >
@@ -1198,7 +1285,13 @@ const App: Component = () => {
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg> </svg>
</button> </button>
<FolderSelectionView onSelectFolder={handleSelectFolder} isLoading={isSelectingFolder()} /> <FolderSelectionView
onSelectFolder={handleSelectFolder}
isLoading={isSelectingFolder()}
advancedSettingsOpen={isAdvancedSettingsOpen()}
onAdvancedSettingsOpen={() => setIsAdvancedSettingsOpen(true)}
onAdvancedSettingsClose={() => setIsAdvancedSettingsOpen(false)}
/>
</div> </div>
</div> </div>
</Show> </Show>

View File

@@ -9,12 +9,14 @@ const codeNomadLogo = new URL("../../images/CodeNomad-Icon.png", import.meta.url
interface FolderSelectionViewProps { interface FolderSelectionViewProps {
onSelectFolder: (folder?: string, binaryPath?: string) => void onSelectFolder: (folder?: string, binaryPath?: string) => void
isLoading?: boolean isLoading?: boolean
advancedSettingsOpen?: boolean
onAdvancedSettingsOpen?: () => void
onAdvancedSettingsClose?: () => void
} }
const FolderSelectionView: Component<FolderSelectionViewProps> = (props) => { const FolderSelectionView: Component<FolderSelectionViewProps> = (props) => {
const [selectedIndex, setSelectedIndex] = createSignal(0) const [selectedIndex, setSelectedIndex] = createSignal(0)
const [focusMode, setFocusMode] = createSignal<"recent" | "new" | null>("recent") const [focusMode, setFocusMode] = createSignal<"recent" | "new" | null>("recent")
const [isAdvancedModalOpen, setIsAdvancedModalOpen] = createSignal(false)
const [selectedBinary, setSelectedBinary] = createSignal(preferences().lastUsedBinary || "opencode") const [selectedBinary, setSelectedBinary] = createSignal(preferences().lastUsedBinary || "opencode")
let recentListRef: HTMLDivElement | undefined let recentListRef: HTMLDivElement | undefined
@@ -320,7 +322,7 @@ const FolderSelectionView: Component<FolderSelectionViewProps> = (props) => {
{/* Advanced settings section */} {/* Advanced settings section */}
<div class="panel-section w-full"> <div class="panel-section w-full">
<button <button
onClick={() => setIsAdvancedModalOpen(true)} onClick={() => props.onAdvancedSettingsOpen?.()}
class="panel-section-header w-full justify-between" class="panel-section-header w-full justify-between"
> >
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
@@ -369,8 +371,8 @@ const FolderSelectionView: Component<FolderSelectionViewProps> = (props) => {
</div> </div>
<AdvancedSettingsModal <AdvancedSettingsModal
open={isAdvancedModalOpen()} open={Boolean(props.advancedSettingsOpen)}
onClose={() => setIsAdvancedModalOpen(false)} onClose={() => props.onAdvancedSettingsClose?.()}
selectedBinary={selectedBinary()} selectedBinary={selectedBinary()}
onBinaryChange={handleBinaryChange} onBinaryChange={handleBinaryChange}
isLoading={props.isLoading} isLoading={props.isLoading}