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 { 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>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
Reference in New Issue
Block a user