import { Component, For, Show, createMemo, createEffect, createSignal, onMount, onCleanup } 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-shell2" import { RemoteAccessOverlay } from "./components/remote-access-overlay" import { InstanceMetadataProvider } from "./lib/contexts/instance-metadata-context" import { initMarkdown } from "./lib/markdown" import QwenOAuthCallback from "./pages/QwenOAuthCallback" import { useTheme } from "./lib/theme" import { useCommands } from "./lib/hooks/use-commands" import { useAppLifecycle } from "./lib/hooks/use-app-lifecycle" import { getLogger } from "./lib/logger" import { initReleaseNotifications } from "./stores/releases" import { runtimeEnv } from "./lib/runtime-env" import { hasInstances, isSelectingFolder, setIsSelectingFolder, showFolderSelection, setShowFolderSelection, showFolderSelectionOnStart, setShowFolderSelectionOnStart, } 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, flushSessionPersistence, updateSessionAgent, updateSessionModel, } from "./stores/sessions" const log = getLogger("actions") const App: Component = () => { const { isDark } = useTheme() const { preferences, recordWorkspaceLaunch, toggleShowThinkingBlocks, toggleShowTimelineTools, toggleAutoCleanupBlankSessions, toggleUsageMetrics, setDiffViewMode, setToolOutputExpansion, setDiagnosticsExpansion, setThinkingBlocksExpansion, } = useConfig() const [escapeInDebounce, setEscapeInDebounce] = createSignal(false) interface LaunchErrorState { message: string binaryPath: string missingBinary: boolean } const [launchError, setLaunchError] = createSignal(null) const [isAdvancedSettingsOpen, setIsAdvancedSettingsOpen] = createSignal(false) const [remoteAccessOpen, setRemoteAccessOpen] = createSignal(false) const [instanceTabBarHeight, setInstanceTabBarHeight] = createSignal(0) const shouldShowFolderSelection = () => !hasInstances() || showFolderSelectionOnStart() const updateInstanceTabBarHeight = () => { if (typeof document === "undefined") return const element = document.querySelector(".tab-bar-instance") setInstanceTabBarHeight(element?.offsetHeight ?? 0) } createEffect(() => { void initMarkdown(isDark()).catch((error) => log.error("Failed to initialize markdown", error)) }) createEffect(() => { initReleaseNotifications() }) createEffect(() => { instances() hasInstances() requestAnimationFrame(() => updateInstanceTabBarHeight()) }) onMount(() => { updateInstanceTabBarHeight() const handleResize = () => updateInstanceTabBarHeight() window.addEventListener("resize", handleResize) onCleanup(() => window.removeEventListener("resize", handleResize)) }) const activeInstance = createMemo(() => getActiveInstance()) const activeSessionIdForInstance = createMemo(() => { const instance = activeInstance() if (!instance) return null return activeSessionId().get(instance.id) || null }) const launchErrorPath = () => { const value = launchError()?.binaryPath if (!value) return "opencode" return value.trim() || "opencode" } 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") || normalized.includes("binary not found") || normalized.includes("no such file or directory") || normalized.includes("binary is not executable") || normalized.includes("enoent") ) } const clearLaunchError = () => setLaunchError(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) setShowFolderSelection(false) setShowFolderSelectionOnStart(false) setIsAdvancedSettingsOpen(false) log.info("Created instance", { instanceId, port: instances().get(instanceId)?.port, }) } catch (error) { const message = formatLaunchErrorMessage(error) const missingBinary = isMissingBinaryMessage(message) setLaunchError({ message, binaryPath: selectedBinary, missingBinary, }) log.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) { log.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 clearActiveParentSession(instanceId) await stopInstance(instanceId) } async function handleNewSession(instanceId: string) { try { const session = await createSession(instanceId) setActiveParentSession(instanceId, session.id) } catch (error) { log.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 } try { await flushSessionPersistence(instanceId) } catch (error) { log.error("Failed to flush session persistence before closing", error) } clearActiveParentSession(instanceId) try { await fetchSessions(instanceId) } catch (error) { log.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, toggleShowTimelineTools, 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, }) // Listen for Tauri menu events onMount(() => { if (runtimeEnv.host === "tauri") { const tauriBridge = (window as { __TAURI__?: { event?: { listen: (event: string, handler: (event: { payload: unknown }) => void) => Promise<() => void> } } }).__TAURI__ if (tauriBridge?.event) { let unlistenMenu: (() => void) | null = null tauriBridge.event.listen("menu:newInstance", () => { handleNewInstanceRequest() }).then((unlisten) => { unlistenMenu = unlisten }).catch((error) => { log.error("Failed to listen for menu:newInstance event", error) }) onCleanup(() => { unlistenMenu?.() }) } } }) // Check if this is OAuth callback const isOAuthCallback = window.location.pathname === '/auth/qwen/callback' if (isOAuthCallback) { return } return ( <>
Unable to launch OpenCode We couldn't start the selected OpenCode binary. Review the error output below or choose a different binary from Advanced Settings.

Binary path

{launchErrorPath()}

Error output

{launchErrorMessage()}
setRemoteAccessOpen(true)} /> {(instance) => { const isActiveInstance = () => activeInstanceId() === instance.id const isVisible = () => isActiveInstance() && !showFolderSelection() return (
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} tabBarOffset={instanceTabBarHeight()} />
) }}
} > setIsAdvancedSettingsOpen(true)} onAdvancedSettingsClose={() => setIsAdvancedSettingsOpen(false)} onOpenRemoteAccess={() => setRemoteAccessOpen(true)} />
setIsAdvancedSettingsOpen(true)} onAdvancedSettingsClose={() => setIsAdvancedSettingsOpen(false)} />
setRemoteAccessOpen(false)} />
) } export default App