Show error if opencode fails to launch
This commit is contained in:
@@ -21,11 +21,9 @@ import {
|
||||
hasInstances,
|
||||
isSelectingFolder,
|
||||
setIsSelectingFolder,
|
||||
setHasInstances,
|
||||
showFolderSelection,
|
||||
setShowFolderSelection,
|
||||
} from "./stores/ui"
|
||||
import { instances as instanceStore } from "./stores/instances"
|
||||
import { useConfig } from "./stores/preferences"
|
||||
import {
|
||||
createInstance,
|
||||
@@ -65,7 +63,12 @@ const App: Component = () => {
|
||||
setThinkingBlocksExpansion,
|
||||
} = useConfig()
|
||||
const [escapeInDebounce, setEscapeInDebounce] = createSignal(false)
|
||||
const [launchErrorBinary, setLaunchErrorBinary] = createSignal<string | null>(null)
|
||||
interface LaunchErrorState {
|
||||
message: string
|
||||
binaryPath: string
|
||||
missingBinary: boolean
|
||||
}
|
||||
const [launchError, setLaunchError] = createSignal<LaunchErrorState | null>(null)
|
||||
const [isAdvancedSettingsOpen, setIsAdvancedSettingsOpen] = createSignal(false)
|
||||
const [remoteAccessOpen, setRemoteAccessOpen] = createSignal(false)
|
||||
const [instanceTabBarHeight, setInstanceTabBarHeight] = createSignal(0)
|
||||
@@ -105,14 +108,30 @@ const App: Component = () => {
|
||||
})
|
||||
|
||||
const launchErrorPath = () => {
|
||||
const value = launchErrorBinary()
|
||||
const value = launchError()?.binaryPath
|
||||
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 launchErrorMessage = () => launchError()?.message ?? ""
|
||||
|
||||
const formatLaunchErrorMessage = (error: unknown): string => {
|
||||
if (!error) {
|
||||
return "Failed to launch workspace"
|
||||
}
|
||||
const raw = typeof error === "string" ? error : error instanceof Error ? error.message : String(error)
|
||||
try {
|
||||
const parsed = JSON.parse(raw)
|
||||
if (parsed && typeof parsed.error === "string") {
|
||||
return parsed.error
|
||||
}
|
||||
} catch {
|
||||
// ignore JSON parse errors
|
||||
}
|
||||
return raw
|
||||
}
|
||||
|
||||
const isMissingBinaryMessage = (message: string): boolean => {
|
||||
const normalized = message.toLowerCase()
|
||||
return (
|
||||
normalized.includes("opencode binary not found") ||
|
||||
@@ -123,7 +142,7 @@ const App: Component = () => {
|
||||
)
|
||||
}
|
||||
|
||||
const clearLaunchError = () => setLaunchErrorBinary(null)
|
||||
const clearLaunchError = () => setLaunchError(null)
|
||||
|
||||
async function handleSelectFolder(folderPath: string, binaryPath?: string) {
|
||||
if (!folderPath) {
|
||||
@@ -135,7 +154,6 @@ const App: Component = () => {
|
||||
recordWorkspaceLaunch(folderPath, selectedBinary)
|
||||
clearLaunchError()
|
||||
const instanceId = await createInstance(folderPath, selectedBinary)
|
||||
setHasInstances(true)
|
||||
setShowFolderSelection(false)
|
||||
setIsAdvancedSettingsOpen(false)
|
||||
|
||||
@@ -144,10 +162,13 @@ const App: Component = () => {
|
||||
port: instances().get(instanceId)?.port,
|
||||
})
|
||||
} catch (error) {
|
||||
clearLaunchError()
|
||||
if (isMissingBinaryError(error)) {
|
||||
setLaunchErrorBinary(selectedBinary)
|
||||
}
|
||||
const message = formatLaunchErrorMessage(error)
|
||||
const missingBinary = isMissingBinaryMessage(message)
|
||||
setLaunchError({
|
||||
message,
|
||||
binaryPath: selectedBinary,
|
||||
missingBinary,
|
||||
})
|
||||
log.error("Failed to create instance", error)
|
||||
} finally {
|
||||
setIsSelectingFolder(false)
|
||||
@@ -191,9 +212,6 @@ const App: Component = () => {
|
||||
if (!confirmed) return
|
||||
|
||||
await stopInstance(instanceId)
|
||||
if (instances().size === 0) {
|
||||
setHasInstances(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleNewSession(instanceId: string) {
|
||||
@@ -304,7 +322,7 @@ const App: Component = () => {
|
||||
onClose={handleDisconnectedInstanceClose}
|
||||
/>
|
||||
|
||||
<Dialog open={Boolean(launchErrorBinary())} modal>
|
||||
<Dialog open={Boolean(launchError())} modal>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Overlay class="modal-overlay" />
|
||||
<div class="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||
@@ -312,8 +330,8 @@ const App: Component = () => {
|
||||
<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 break-words">
|
||||
Install the OpenCode CLI and make sure it is available in your PATH, or pick a custom binary from
|
||||
Advanced Settings.
|
||||
We couldn't start the selected OpenCode binary. Review the error output below or choose a different
|
||||
binary from Advanced Settings.
|
||||
</Dialog.Description>
|
||||
</div>
|
||||
|
||||
@@ -322,10 +340,23 @@ const App: Component = () => {
|
||||
<p class="text-sm font-mono text-primary break-all">{launchErrorPath()}</p>
|
||||
</div>
|
||||
|
||||
<Show when={launchErrorMessage()}>
|
||||
<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">Error output</p>
|
||||
<pre class="text-sm font-mono text-primary whitespace-pre-wrap break-words max-h-48 overflow-y-auto">{launchErrorMessage()}</pre>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<div class="flex justify-end gap-2">
|
||||
<button type="button" class="selector-button selector-button-secondary" onClick={handleLaunchErrorAdvanced}>
|
||||
Open Advanced Settings
|
||||
</button>
|
||||
<Show when={launchError()?.missingBinary}>
|
||||
<button
|
||||
type="button"
|
||||
class="selector-button selector-button-secondary"
|
||||
onClick={handleLaunchErrorAdvanced}
|
||||
>
|
||||
Open Advanced Settings
|
||||
</button>
|
||||
</Show>
|
||||
<button type="button" class="selector-button selector-button-primary" onClick={handleLaunchErrorClose}>
|
||||
Close
|
||||
</button>
|
||||
|
||||
@@ -34,6 +34,11 @@ const [logStreamingState, setLogStreamingState] = createSignal<Map<string, boole
|
||||
const [permissionQueues, setPermissionQueues] = createSignal<Map<string, Permission[]>>(new Map())
|
||||
const [activePermissionId, setActivePermissionId] = createSignal<Map<string, string | null>>(new Map())
|
||||
const permissionSessionCounts = new Map<string, Map<string, number>>()
|
||||
|
||||
function syncHasInstancesFlag() {
|
||||
const readyExists = Array.from(instances().values()).some((instance) => instance.status === "ready")
|
||||
setHasInstances(readyExists)
|
||||
}
|
||||
interface DisconnectedInstanceInfo {
|
||||
id: string
|
||||
folder: string
|
||||
@@ -68,7 +73,6 @@ function upsertWorkspace(descriptor: WorkspaceDescriptor) {
|
||||
updateInstance(descriptor.id, mapped)
|
||||
} else {
|
||||
addInstance(mapped)
|
||||
setHasInstances(true)
|
||||
}
|
||||
|
||||
if (descriptor.status === "ready") {
|
||||
@@ -135,9 +139,6 @@ void (async function initializeWorkspaces() {
|
||||
try {
|
||||
const workspaces = await serverApi.fetchWorkspaces()
|
||||
workspaces.forEach((workspace) => upsertWorkspace(workspace))
|
||||
if (workspaces.length === 0) {
|
||||
setHasInstances(false)
|
||||
}
|
||||
} catch (error) {
|
||||
log.error("Failed to load workspaces", error)
|
||||
}
|
||||
@@ -159,9 +160,6 @@ function handleWorkspaceEvent(event: WorkspaceEventPayload) {
|
||||
case "workspace.stopped":
|
||||
releaseInstanceResources(event.workspaceId)
|
||||
removeInstance(event.workspaceId)
|
||||
if (instances().size === 0) {
|
||||
setHasInstances(false)
|
||||
}
|
||||
break
|
||||
case "workspace.log":
|
||||
handleWorkspaceLog(event.entry)
|
||||
@@ -249,6 +247,7 @@ function addInstance(instance: Instance) {
|
||||
})
|
||||
ensureLogContainer(instance.id)
|
||||
ensureLogStreamingState(instance.id)
|
||||
syncHasInstancesFlag()
|
||||
}
|
||||
|
||||
function updateInstance(id: string, updates: Partial<Instance>) {
|
||||
@@ -260,6 +259,7 @@ function updateInstance(id: string, updates: Partial<Instance>) {
|
||||
}
|
||||
return next
|
||||
})
|
||||
syncHasInstancesFlag()
|
||||
}
|
||||
|
||||
function removeInstance(id: string) {
|
||||
@@ -301,6 +301,7 @@ function removeInstance(id: string) {
|
||||
clearCacheForInstance(id)
|
||||
messageStoreBus.unregisterInstance(id)
|
||||
clearInstanceDraftPrompts(id)
|
||||
syncHasInstancesFlag()
|
||||
}
|
||||
|
||||
async function createInstance(folder: string, _binaryPath?: string): Promise<string> {
|
||||
@@ -328,9 +329,6 @@ async function stopInstance(id: string) {
|
||||
}
|
||||
|
||||
removeInstance(id)
|
||||
if (instances().size === 0) {
|
||||
setHasInstances(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchLspStatus(instanceId: string): Promise<LspStatus[] | undefined> {
|
||||
@@ -590,9 +588,6 @@ async function acknowledgeDisconnectedInstance(): Promise<void> {
|
||||
log.error("Failed to stop disconnected instance", error)
|
||||
} finally {
|
||||
setDisconnectedInstance(null)
|
||||
if (instances().size === 0) {
|
||||
setHasInstances(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user