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 } 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" 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" interface FolderSelectionViewProps { onSelectFolder: (folder: string, binaryPath?: string) => void isLoading?: boolean onClose?: () => void } const FolderSelectionView: Component = (props) => { const { recentFolders, removeRecentFolder, preferences, updatePreferences, serverSettings } = 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 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 isLoading = () => Boolean(props.isLoading) // 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-folder-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 } const folderList = folders() if (isBrowseShortcut) { e.preventDefault() void handleBrowse() return } if (folderList.length === 0) return if (e.key === "ArrowDown") { e.preventDefault() const newIndex = Math.min(selectedIndex() + 1, folderList.length - 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, folderList.length - 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 = folderList.length - 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 (folderList.length > 0 && focusMode() === "recent") { const folder = folderList[selectedIndex()] if (folder) { handleRemove(folder.path) } } } } function handleEnterKey() { if (isLoading()) return const folderList = folders() const index = selectedIndex() const folder = folderList[index] if (folder) { handleFolderSelect(folder.path) } } 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()) } 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.empty.title")}

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

} >

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

{t( folders().length === 1 ? "folderSelection.recent.subtitle.one" : "folderSelection.recent.subtitle.other", { count: folders().length }, )}

(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} /> ) } export default FolderSelectionView