import { Dialog } from "@kobalte/core/dialog" import { Select } from "@kobalte/core/select" import { Component, createSignal, Show, For, onMount, onCleanup, createEffect } from "solid-js" import { Folder, Clock, Trash2, FolderPlus, Settings, ChevronRight, MonitorUp, Star, Languages, ChevronDown, X, Globe, Loader2 } from "lucide-solid" import { useConfig } from "../stores/preferences" import DirectoryBrowserDialog from "./directory-browser-dialog" import Kbd from "./kbd" import { openNativeFolderDialog, supportsNativeDialogs } from "../lib/native/native-functions" import { useFolderDrop } from "../lib/hooks/use-folder-drop" import VersionPill from "./version-pill" import { DiscordSymbolIcon, GitHubMarkIcon } from "./brand-icons" import { githubStars } from "../stores/github-stars" import { formatCompactCount } from "../lib/formatters" import { useI18n, type Locale } from "../lib/i18n" import { showAlertDialog } from "../stores/alerts" import { openSettings, settingsOpen } from "../stores/settings-screen" import { openExternalUrl } from "../lib/external-url" import { serverApi } from "../lib/api-client" import { openRemoteServerWindow } from "../lib/native/remote-window" const codeNomadLogo = new URL("../images/CodeNomad-Icon.png", import.meta.url).href const GITHUB_URL = "https://github.com/NeuralNomadsAI/CodeNomad" const DISCORD_URL = "https://discord.com/channels/1391832426048651334/1458412028325793887/1464701235683917945" type HomeTab = "local" | "servers" interface FolderSelectionViewProps { onSelectFolder: (folder: string, binaryPath?: string) => void onOpenSidecar?: () => void isLoading?: boolean onClose?: () => void } const FolderSelectionView: Component = (props) => { const { recentFolders, removeRecentFolder, preferences, updatePreferences, serverSettings, remoteServers, saveRemoteServerProfile, markRemoteServerConnected, removeRemoteServerProfile, } = useConfig() const { t, locale } = useI18n() const [selectedIndex, setSelectedIndex] = createSignal(0) const [focusMode, setFocusMode] = createSignal<"recent" | "new" | null>("recent") const [selectedBinary, setSelectedBinary] = createSignal(serverSettings().opencodeBinary || "opencode") const [isFolderBrowserOpen, setIsFolderBrowserOpen] = createSignal(false) const [activeTab, setActiveTab] = createSignal("local") const [isServerDialogOpen, setIsServerDialogOpen] = createSignal(false) const [serverName, setServerName] = createSignal("") const [serverUrl, setServerUrl] = createSignal("") const [skipTlsVerify, setSkipTlsVerify] = createSignal(false) const [serverDialogError, setServerDialogError] = createSignal(null) const [isSavingServer, setIsSavingServer] = createSignal(false) const [connectingServerId, setConnectingServerId] = createSignal(null) const nativeDialogsAvailable = supportsNativeDialogs() let recentListRef: HTMLDivElement | undefined type LanguageOption = { value: Locale; label: string } const languageOptions: LanguageOption[] = [ { value: "en", label: "English" }, { value: "es", label: "Español" }, { value: "fr", label: "Français" }, { value: "ru", label: "Русский" }, { value: "ja", label: "日本語" }, { value: "zh-Hans", label: "简体中文" }, { value: "he", label: "עברית" }, ] const selectedLanguageOption = () => languageOptions.find((opt) => opt.value === locale()) ?? languageOptions[0] const folders = () => recentFolders() const serverList = () => remoteServers() const isLoading = () => Boolean(props.isLoading) function getActiveListLength() { return activeTab() === "local" ? folders().length : serverList().length } // Update selected binary when preferences change createEffect(() => { const lastUsed = serverSettings().opencodeBinary if (!lastUsed) return setSelectedBinary((current) => (current === lastUsed ? current : lastUsed)) }) function scrollToIndex(index: number) { const container = recentListRef if (!container) return const element = container.querySelector(`[data-list-index="${index}"]`) as HTMLElement | null if (!element) return const containerRect = container.getBoundingClientRect() const elementRect = element.getBoundingClientRect() if (elementRect.top < containerRect.top) { container.scrollTop -= containerRect.top - elementRect.top } else if (elementRect.bottom > containerRect.bottom) { container.scrollTop += elementRect.bottom - containerRect.bottom } } function handleKeyDown(e: KeyboardEvent) { let activeElement: HTMLElement | null = null if (typeof document !== "undefined") { activeElement = document.activeElement as HTMLElement | null } const insideModal = activeElement?.closest(".modal-surface") || activeElement?.closest("[role='dialog']") const isEditingField = activeElement && (["INPUT", "TEXTAREA", "SELECT"].includes(activeElement.tagName) || activeElement.isContentEditable || Boolean(insideModal)) if (isEditingField) { return } const normalizedKey = e.key.toLowerCase() const isBrowseShortcut = (e.metaKey || e.ctrlKey) && !e.shiftKey && normalizedKey === "n" const blockedKeys = [ "ArrowDown", "ArrowUp", "PageDown", "PageUp", "Home", "End", "Enter", "Backspace", "Delete", ] if (isLoading()) { if (isBrowseShortcut || blockedKeys.includes(e.key)) { e.preventDefault() } return } if (isBrowseShortcut) { e.preventDefault() void handleBrowse() return } const listLength = getActiveListLength() if (listLength === 0) return if (e.key === "ArrowDown") { e.preventDefault() const newIndex = Math.min(selectedIndex() + 1, listLength - 1) setSelectedIndex(newIndex) setFocusMode("recent") scrollToIndex(newIndex) } else if (e.key === "ArrowUp") { e.preventDefault() const newIndex = Math.max(selectedIndex() - 1, 0) setSelectedIndex(newIndex) setFocusMode("recent") scrollToIndex(newIndex) } else if (e.key === "PageDown") { e.preventDefault() const pageSize = 5 const newIndex = Math.min(selectedIndex() + pageSize, listLength - 1) setSelectedIndex(newIndex) setFocusMode("recent") scrollToIndex(newIndex) } else if (e.key === "PageUp") { e.preventDefault() const pageSize = 5 const newIndex = Math.max(selectedIndex() - pageSize, 0) setSelectedIndex(newIndex) setFocusMode("recent") scrollToIndex(newIndex) } else if (e.key === "Home") { e.preventDefault() setSelectedIndex(0) setFocusMode("recent") scrollToIndex(0) } else if (e.key === "End") { e.preventDefault() const newIndex = listLength - 1 setSelectedIndex(newIndex) setFocusMode("recent") scrollToIndex(newIndex) } else if (e.key === "Enter") { e.preventDefault() handleEnterKey() } else if (e.key === "Backspace" || e.key === "Delete") { e.preventDefault() if (listLength > 0 && focusMode() === "recent") { if (activeTab() === "local") { const folder = folders()[selectedIndex()] if (folder) { handleRemove(folder.path) } } else { const server = serverList()[selectedIndex()] if (server) { removeRemoteServerProfile(server.id) } } } } } function handleEnterKey() { if (isLoading()) return const index = selectedIndex() if (activeTab() === "local") { const folder = folders()[index] if (folder) { handleFolderSelect(folder.path) } return } const server = serverList()[index] if (server) { void handleConnectSavedServer(server.id) } } createEffect(() => { activeTab() setSelectedIndex(0) setFocusMode("recent") }) createEffect(() => { const length = getActiveListLength() if (length === 0) { setSelectedIndex(0) return } if (selectedIndex() >= length) { setSelectedIndex(length - 1) } }) onMount(() => { window.addEventListener("keydown", handleKeyDown) onCleanup(() => { window.removeEventListener("keydown", handleKeyDown) }) }) function dropTargetBlocked() { return isLoading() || isFolderBrowserOpen() || settingsOpen() } function showInvalidFolderDropAlert() { showAlertDialog(t("folderSelection.drop.invalidMessage"), { title: t("folderSelection.drop.invalidTitle"), variant: "warning", }) } const folderDrop = useFolderDrop({ enabled: () => !dropTargetBlocked(), onInvalidDrop: showInvalidFolderDropAlert, onDrop: async (paths) => { const firstPath = paths[0] if (!firstPath) { showInvalidFolderDropAlert() return } handleFolderSelect(firstPath) }, }) function formatRelativeTime(timestamp: number): string { const seconds = Math.floor((Date.now() - timestamp) / 1000) const minutes = Math.floor(seconds / 60) const hours = Math.floor(minutes / 60) const days = Math.floor(hours / 24) if (days > 0) return t("time.relative.daysAgoShort", { count: days }) if (hours > 0) return t("time.relative.hoursAgoShort", { count: hours }) if (minutes > 0) return t("time.relative.minutesAgoShort", { count: minutes }) return t("time.relative.justNow") } function handleFolderSelect(path: string) { if (isLoading()) return props.onSelectFolder(path, selectedBinary()) } function resetServerDialog() { setServerName("") setServerUrl("") setSkipTlsVerify(false) setServerDialogError(null) } function openServerDialog() { resetServerDialog() setIsServerDialogOpen(true) } async function probeAndOpenServer(input: { id?: string; name: string; baseUrl: string; skipTlsVerify: boolean }, openWindow: boolean) { const trimmedName = input.name.trim() const trimmedUrl = input.baseUrl.trim() if (!trimmedName || !trimmedUrl) { throw new Error(t("folderSelection.servers.dialog.errorRequired")) } const probe = await serverApi.probeRemoteServer({ baseUrl: trimmedUrl, skipTlsVerify: input.skipTlsVerify, }) if (!probe.ok) { throw new Error(probe.error || t("folderSelection.servers.dialog.errorConnect")) } const profile = await saveRemoteServerProfile({ id: input.id, name: trimmedName, baseUrl: probe.normalizedUrl, skipTlsVerify: input.skipTlsVerify, }) if (openWindow) { await openRemoteServerWindow(profile) await markRemoteServerConnected(profile.id) } return profile } async function handleSaveServer(openWindow: boolean) { if (isSavingServer()) return setIsSavingServer(true) setServerDialogError(null) try { await probeAndOpenServer( { name: serverName(), baseUrl: serverUrl(), skipTlsVerify: skipTlsVerify(), }, openWindow, ) setIsServerDialogOpen(false) resetServerDialog() } catch (error) { setServerDialogError(error instanceof Error ? error.message : String(error)) } finally { setIsSavingServer(false) } } async function handleConnectSavedServer(id: string) { const target = remoteServers().find((entry) => entry.id === id) if (!target || connectingServerId()) return setConnectingServerId(id) try { await probeAndOpenServer(target, true) } catch (error) { showAlertDialog(error instanceof Error ? error.message : String(error), { title: t("folderSelection.servers.errorTitle"), variant: "warning", }) } finally { setConnectingServerId(null) } } async function handleBrowse() { if (isLoading()) return setFocusMode("new") if (nativeDialogsAvailable) { const fallbackPath = folders()[0]?.path const selected = await openNativeFolderDialog({ title: t("folderSelection.dialog.title"), defaultPath: fallbackPath, }) if (selected) { handleFolderSelect(selected) } return } setIsFolderBrowserOpen(true) } function handleBrowserSelect(path: string) { setIsFolderBrowserOpen(false) handleFolderSelect(path) } function handleRemove(path: string, e?: Event) { if (isLoading()) return e?.stopPropagation() removeRecentFolder(path) const folderList = folders() if (selectedIndex() >= folderList.length && folderList.length > 0) { setSelectedIndex(folderList.length - 1) } } function getDisplayPath(path: string): string { if (!path) return path // macOS: /Users//... if (path.startsWith("/Users/")) { return path.replace(/^\/Users\/[^/]+/, "~") } // Linux: /home//... if (path.startsWith("/home/")) { return path.replace(/^\/home\/[^/]+/, "~") } // Windows: C:\Users\\... (and the forward-slash variant) if (/^[A-Za-z]:\\Users\\/.test(path)) { return path.replace(/^[A-Za-z]:\\Users\\[^\\]+/, "~") } if (/^[A-Za-z]:\/Users\//.test(path)) { return path.replace(/^[A-Za-z]:\/Users\/[^/]+/, "~") } return path } function looksLikeWindowsPath(value: string): boolean { if (!value) return false // Drive letter (C:\...) or UNC (\\server\share\...) return /^[A-Za-z]:[\\/]/.test(value) || /^\\\\[^\\]+\\[^\\]+/.test(value) } function splitFolderPath(rawPath: string): { baseName: string; dirName: string } { if (!rawPath) return { baseName: "", dirName: "" } const isWindows = looksLikeWindowsPath(rawPath) const trimmed = rawPath.replace(/[\\/]+$/, "") // Root edge-cases ("/", "C:\\", "\\\\server\\share\\") if (!trimmed) { return { baseName: rawPath, dirName: "" } } if (isWindows && /^[A-Za-z]:$/.test(trimmed)) { return { baseName: `${trimmed}\\`, dirName: "" } } const lastSlash = trimmed.lastIndexOf("/") const lastBackslash = isWindows ? trimmed.lastIndexOf("\\") : -1 const lastSep = Math.max(lastSlash, lastBackslash) if (lastSep < 0) { return { baseName: trimmed, dirName: "" } } const baseName = trimmed.slice(lastSep + 1) || trimmed const dirName = trimmed.slice(0, lastSep) return { baseName, dirName } } return ( <>
value={selectedLanguageOption()} onChange={(value) => { if (!value) return if (value.value === locale()) return updatePreferences({ locale: value.value }) }} options={languageOptions} optionValue="value" optionTextValue="label" itemComponent={(itemProps) => ( {itemProps.item.rawValue.label} )} >
{/* Right column: recent folders */}
0} fallback={

{t("folderSelection.servers.empty.title")}

{t("folderSelection.servers.empty.description")}

} >
(recentListRef = el)} > {(server, index) => (
)}
} > 0} fallback={

{t("folderSelection.empty.title")}

{t("folderSelection.empty.description")}

} >
(recentListRef = el)} > {(folder, index) => (
)}
{/* Left column: version + browse + advanced settings */}
{/* OpenCode settings section */}

{t("folderSelection.loading.title")}

{t("folderSelection.loading.subtitle")}

setIsFolderBrowserOpen(false)} onSelect={handleBrowserSelect} /> !open && setIsServerDialogOpen(false)}>
{t("folderSelection.servers.dialog.title")} {t("folderSelection.servers.dialog.description")}
{(message) =>

{message()}

}
) } export default FolderSelectionView