import { Component, Show, createMemo, createEffect, createSignal } from "solid-js" import { Dialog } from "@kobalte/core/dialog" import { Toaster } from "solid-toast" import AlertDialog from "./components/alert-dialog" import FolderSelectionView from "./components/folder-selection-view" import { showConfirmDialog } from "./stores/alerts" import InstanceTabs from "./components/instance-tabs" import InstanceDisconnectedModal from "./components/instance-disconnected-modal" import InstanceShell from "./components/instance/instance-shell" import { initMarkdown } from "./lib/markdown" import { useTheme } from "./lib/theme" import { useCommands } from "./lib/hooks/use-commands" import { useAppLifecycle } from "./lib/hooks/use-app-lifecycle" import { hasInstances, isSelectingFolder, setIsSelectingFolder, setHasInstances, showFolderSelection, setShowFolderSelection, } from "./stores/ui" import { useConfig } from "./stores/preferences" import { createInstance, instances, activeInstanceId, setActiveInstanceId, stopInstance, getActiveInstance, disconnectedInstance, acknowledgeDisconnectedInstance, } from "./stores/instances" import { getSessions, activeSessionId, setActiveParentSession, clearActiveParentSession, createSession, fetchSessions, updateSessionAgent, updateSessionModel, } from "./stores/sessions" const App: Component = () => { const { isDark } = useTheme() const { preferences, recordWorkspaceLaunch, toggleShowThinkingBlocks, toggleAutoCleanupBlankSessions, toggleUsageMetrics, setDiffViewMode, setToolOutputExpansion, setDiagnosticsExpansion, setThinkingBlocksExpansion, } = useConfig() const [escapeInDebounce, setEscapeInDebounce] = createSignal(false) const [launchErrorBinary, setLaunchErrorBinary] = createSignal(null) const [isAdvancedSettingsOpen, setIsAdvancedSettingsOpen] = createSignal(false) createEffect(() => { void initMarkdown(isDark()).catch(console.error) }) const activeInstance = createMemo(() => getActiveInstance()) const activeSessionIdForInstance = createMemo(() => { const instance = activeInstance() if (!instance) return null return activeSessionId().get(instance.id) || null }) 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) async function handleSelectFolder(folderPath: string, binaryPath?: string) { if (!folderPath) { return } setIsSelectingFolder(true) const selectedBinary = binaryPath || preferences().lastUsedBinary || "opencode" try { recordWorkspaceLaunch(folderPath, selectedBinary) clearLaunchError() const instanceId = await createInstance(folderPath, 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) } } async function handleDisconnectedInstanceClose() { try { await acknowledgeDisconnectedInstance() } catch (error) { console.error("Failed to finalize disconnected instance:", error) } } async function handleCloseInstance(instanceId: string) { const confirmed = await showConfirmDialog( "Stop OpenCode instance? This will stop the server.", { title: "Stop instance", variant: "warning", confirmLabel: "Stop", cancelLabel: "Keep running", }, ) if (!confirmed) return await stopInstance(instanceId) if (instances().size === 0) { setHasInstances(false) } } async function handleNewSession(instanceId: string) { try { const session = await createSession(instanceId) setActiveParentSession(instanceId, session.id) } catch (error) { console.error("Failed to create session:", error) } } async function handleCloseSession(instanceId: string, sessionId: string) { const sessions = getSessions(instanceId) const session = sessions.find((s) => s.id === sessionId) if (!session) { return } const parentSessionId = session.parentId ?? session.id const parentSession = sessions.find((s) => s.id === parentSessionId) if (!parentSession || parentSession.parentId !== null) { return } clearActiveParentSession(instanceId) try { await fetchSessions(instanceId) } catch (error) { console.error("Failed to refresh sessions after closing:", error) } } const handleSidebarAgentChange = async (instanceId: string, sessionId: string, agent: string) => { if (!instanceId || !sessionId || sessionId === "info") return await updateSessionAgent(instanceId, sessionId, agent) } const handleSidebarModelChange = async ( instanceId: string, sessionId: string, model: { providerId: string; modelId: string }, ) => { if (!instanceId || !sessionId || sessionId === "info") return await updateSessionModel(instanceId, sessionId, model) } const { commands: paletteCommands, executeCommand } = useCommands({ preferences, toggleAutoCleanupBlankSessions, toggleShowThinkingBlocks, toggleUsageMetrics, setDiffViewMode, setToolOutputExpansion, setDiagnosticsExpansion, setThinkingBlocksExpansion, handleNewInstanceRequest, handleCloseInstance, handleNewSession, handleCloseSession, getActiveInstance: activeInstance, getActiveSessionIdForInstance: activeSessionIdForInstance, }) useAppLifecycle({ setEscapeInDebounce, handleNewInstanceRequest, handleCloseInstance, handleNewSession, handleCloseSession, showFolderSelection, setShowFolderSelection, getActiveInstance: activeInstance, getActiveSessionIdForInstance: activeSessionIdForInstance, }) return ( <>
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()}

{(instance) => ( handleCloseSession(instance.id, sessionId)} onNewSession={() => handleNewSession(instance.id)} handleSidebarAgentChange={(sessionId, agent) => handleSidebarAgentChange(instance.id, sessionId, agent)} handleSidebarModelChange={(sessionId, model) => handleSidebarModelChange(instance.id, sessionId, model)} onExecuteCommand={executeCommand} /> )} } > setIsAdvancedSettingsOpen(true)} onAdvancedSettingsClose={() => setIsAdvancedSettingsOpen(false)} />
setIsAdvancedSettingsOpen(true)} onAdvancedSettingsClose={() => setIsAdvancedSettingsOpen(false)} />
) } export default App