surface launch failures with guided advanced settings
This commit is contained in:
107
src/App.tsx
107
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<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 = () => {
|
||||
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}
|
||||
/>
|
||||
|
||||
<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">
|
||||
<Show
|
||||
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>
|
||||
|
||||
<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="w-full h-full relative">
|
||||
<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"
|
||||
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" />
|
||||
</svg>
|
||||
</button>
|
||||
<FolderSelectionView onSelectFolder={handleSelectFolder} isLoading={isSelectingFolder()} />
|
||||
<FolderSelectionView
|
||||
onSelectFolder={handleSelectFolder}
|
||||
isLoading={isSelectingFolder()}
|
||||
advancedSettingsOpen={isAdvancedSettingsOpen()}
|
||||
onAdvancedSettingsOpen={() => setIsAdvancedSettingsOpen(true)}
|
||||
onAdvancedSettingsClose={() => setIsAdvancedSettingsOpen(false)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
@@ -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<FolderSelectionViewProps> = (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<FolderSelectionViewProps> = (props) => {
|
||||
{/* Advanced settings section */}
|
||||
<div class="panel-section w-full">
|
||||
<button
|
||||
onClick={() => setIsAdvancedModalOpen(true)}
|
||||
onClick={() => props.onAdvancedSettingsOpen?.()}
|
||||
class="panel-section-header w-full justify-between"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
@@ -369,8 +371,8 @@ const FolderSelectionView: Component<FolderSelectionViewProps> = (props) => {
|
||||
</div>
|
||||
|
||||
<AdvancedSettingsModal
|
||||
open={isAdvancedModalOpen()}
|
||||
onClose={() => setIsAdvancedModalOpen(false)}
|
||||
open={Boolean(props.advancedSettingsOpen)}
|
||||
onClose={() => props.onAdvancedSettingsClose?.()}
|
||||
selectedBinary={selectedBinary()}
|
||||
onBinaryChange={handleBinaryChange}
|
||||
isLoading={props.isLoading}
|
||||
|
||||
Reference in New Issue
Block a user