From b0e636d7819bf172a253410f17e88b0beaac3ca1 Mon Sep 17 00:00:00 2001 From: Shantur Rathore Date: Fri, 24 Oct 2025 18:36:11 +0100 Subject: [PATCH] Add recent folders feature with localStorage persistence - Create FolderSelectionView component showing recent folders and browse option - Store up to 10 recent folders in localStorage with timestamps - Show folder selection view on app start when no instances exist - Display folder selection modal when creating new instance from existing instance - Add keyboard navigation (arrows, page up/down, home/end, enter, delete) - Add ability to remove folders from recent list - Track folder access time and display relative timestamps - Close modal with Escape key or close button - Update preferences store with recent folders management --- src/App.tsx | 72 +++++- src/components/folder-selection-view.tsx | 286 +++++++++++++++++++++++ src/stores/preferences.ts | 45 +++- src/stores/ui.ts | 3 + 4 files changed, 394 insertions(+), 12 deletions(-) create mode 100644 src/components/folder-selection-view.tsx diff --git a/src/App.tsx b/src/App.tsx index 6bc28dcd..72a15dfa 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,7 +1,7 @@ import { Component, onMount, onCleanup, Show, createMemo, createEffect, createSignal } from "solid-js" import type { Session } from "./types/session" import type { Attachment } from "./types/attachment" -import EmptyState from "./components/empty-state" +import FolderSelectionView from "./components/folder-selection-view" import InstanceWelcomeView from "./components/instance-welcome-view" import CommandPalette from "./components/command-palette" import InstanceTabs from "./components/instance-tabs" @@ -12,8 +12,15 @@ import LogsView from "./components/logs-view" import { initMarkdown } from "./lib/markdown" import { createCommandRegistry } from "./lib/commands" import type { Command } from "./lib/commands" -import { hasInstances, isSelectingFolder, setIsSelectingFolder, setHasInstances } from "./stores/ui" -import { toggleShowThinkingBlocks, preferences } from "./stores/preferences" +import { + hasInstances, + isSelectingFolder, + setIsSelectingFolder, + setHasInstances, + showFolderSelection, + setShowFolderSelection, +} from "./stores/ui" +import { toggleShowThinkingBlocks, preferences, addRecentFolder } from "./stores/preferences" import { createInstance, instances, @@ -176,16 +183,22 @@ const App: Component = () => { return activeSessionId().get(instance.id) || null }) - async function handleSelectFolder() { + async function handleSelectFolder(folderPath?: string) { setIsSelectingFolder(true) try { - const folder = await window.electronAPI.selectFolder() + let folder: string | null | undefined = folderPath + if (!folder) { - return + folder = await window.electronAPI.selectFolder() + if (!folder) { + return + } } + addRecentFolder(folder) const instanceId = await createInstance(folder) setHasInstances(true) + setShowFolderSelection(false) console.log("Created instance:", instanceId, "Port:", instances().get(instanceId)?.port) } catch (error) { @@ -195,6 +208,14 @@ const App: Component = () => { } } + function handleNewInstanceRequest() { + if (hasInstances()) { + setShowFolderSelection(true) + } else { + handleSelectFolder() + } + } + async function handleCloseInstance(instanceId: string) { if (confirm("Stop OpenCode instance? This will stop the server.")) { await stopInstance(instanceId) @@ -234,7 +255,7 @@ const App: Component = () => { category: "Instance", keywords: ["folder", "project", "workspace"], shortcut: { key: "N", meta: true }, - action: handleSelectFolder, + action: handleNewInstanceRequest, }) commandRegistry.register({ @@ -620,7 +641,7 @@ const App: Component = () => { setupCommands() setupTabKeyboardShortcuts( - handleSelectFolder, + handleNewInstanceRequest, handleCloseInstance, handleNewSession, handleCloseSession, @@ -676,6 +697,8 @@ const App: Component = () => { ) registerEscapeShortcut( () => { + if (showFolderSelection()) return true + const instance = activeInstance() if (!instance) return false @@ -697,6 +720,11 @@ const App: Component = () => { ) }, async () => { + if (showFolderSelection()) { + setShowFolderSelection(false) + return + } + const instance = activeInstance() const sessionId = activeSessionIdForInstance() if (!instance || !sessionId || sessionId === "logs") return @@ -740,7 +768,7 @@ const App: Component = () => { }) window.electronAPI.onNewInstance(() => { - handleSelectFolder() + handleNewInstanceRequest() }) window.electronAPI.onInstanceStarted(({ id, port, pid, binaryPath }) => { @@ -774,7 +802,7 @@ const App: Component = () => { activeInstanceId={activeInstanceId()} onSelect={setActiveInstanceId} onClose={handleCloseInstance} - onNew={handleSelectFolder} + onNew={handleNewInstanceRequest} /> @@ -825,7 +853,7 @@ const App: Component = () => { } > - + { commands={commandRegistry.getAll()} onExecute={handleExecuteCommand} /> + + +
+
+ + +
+
+
) } diff --git a/src/components/folder-selection-view.tsx b/src/components/folder-selection-view.tsx new file mode 100644 index 00000000..166f60c3 --- /dev/null +++ b/src/components/folder-selection-view.tsx @@ -0,0 +1,286 @@ +import { Component, createSignal, Show, For, onMount, onCleanup } from "solid-js" +import { Folder, Clock, Trash2, FolderPlus } from "lucide-solid" +import { recentFolders, removeRecentFolder } from "../stores/preferences" + +interface FolderSelectionViewProps { + onSelectFolder: (folder?: string) => void + isLoading?: boolean +} + +const FolderSelectionView: Component = (props) => { + const [selectedIndex, setSelectedIndex] = createSignal(0) + const [focusMode, setFocusMode] = createSignal<"recent" | "new" | null>("recent") + + const folders = () => recentFolders() + + function scrollToIndex(index: number) { + const element = document.querySelector(`[data-folder-index="${index}"]`) + if (element) { + element.scrollIntoView({ block: "nearest", behavior: "auto" }) + } + } + + function handleKeyDown(e: KeyboardEvent) { + const folderList = folders() + + if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) { + e.preventDefault() + 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() { + const folderList = folders() + const index = selectedIndex() + + if (index < folderList.length) { + props.onSelectFolder(folderList[index].path) + } + } + + onMount(() => { + window.addEventListener("keydown", handleKeyDown) + onCleanup(() => { + window.removeEventListener("keydown", handleKeyDown) + }) + }) + + 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 `${days}d ago` + if (hours > 0) return `${hours}h ago` + if (minutes > 0) return `${minutes}m ago` + return "just now" + } + + function handleFolderSelect(path: string) { + props.onSelectFolder(path) + } + + function handleBrowse() { + props.onSelectFolder() + } + + function handleRemove(path: string, e?: Event) { + 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.startsWith("/Users/")) { + return path.replace(/^\/Users\/[^/]+/, "~") + } + return path + } + + return ( +
+
+
+
+ +
+

Welcome to OpenCode

+

Select a folder to start coding with AI

+
+ +
+ 0} + fallback={ +
+
+ +
+

No Recent Folders

+

Browse for a folder to get started

+
+ } + > +
+
+

Recent Folders

+

+ {folders().length} {folders().length === 1 ? "folder" : "folders"} available +

+
+
+ + {(folder, index) => ( +
+ +
+ +
+ )} + +
+
+ + +
+
+

Browse for Folder

+

Select any folder on your computer

+
+
+ +
+
+
+ +
+
+ 0}> +
+ + ↑ + + + ↓ + + Navigate +
+
+ + Enter + + Select +
+
+ + Del + + Remove +
+
+
+ + Cmd+Enter + + Browse +
+
+
+
+ + ) +} + +export default FolderSelectionView diff --git a/src/stores/preferences.ts b/src/stores/preferences.ts index ff7b2abc..d4d8b497 100644 --- a/src/stores/preferences.ts +++ b/src/stores/preferences.ts @@ -1,11 +1,18 @@ import { createSignal } from "solid-js" const STORAGE_KEY = "opencode-preferences" +const RECENT_FOLDERS_KEY = "opencode-recent-folders" +const MAX_RECENT_FOLDERS = 10 interface Preferences { showThinkingBlocks: boolean } +interface RecentFolder { + path: string + lastAccessed: number +} + const defaultPreferences: Preferences = { showThinkingBlocks: false, } @@ -30,7 +37,28 @@ function savePreferences(prefs: Preferences): void { } } +function loadRecentFolders(): RecentFolder[] { + try { + const stored = localStorage.getItem(RECENT_FOLDERS_KEY) + if (stored) { + return JSON.parse(stored) + } + } catch (error) { + console.error("Failed to load recent folders:", error) + } + return [] +} + +function saveRecentFolders(folders: RecentFolder[]): void { + try { + localStorage.setItem(RECENT_FOLDERS_KEY, JSON.stringify(folders)) + } catch (error) { + console.error("Failed to save recent folders:", error) + } +} + const [preferences, setPreferences] = createSignal(loadPreferences()) +const [recentFolders, setRecentFolders] = createSignal(loadRecentFolders()) function updatePreferences(updates: Partial): void { const updated = { ...preferences(), ...updates } @@ -42,4 +70,19 @@ function toggleShowThinkingBlocks(): void { updatePreferences({ showThinkingBlocks: !preferences().showThinkingBlocks }) } -export { preferences, updatePreferences, toggleShowThinkingBlocks } +function addRecentFolder(path: string): void { + const folders = recentFolders().filter((f) => f.path !== path) + folders.unshift({ path, lastAccessed: Date.now() }) + + const trimmed = folders.slice(0, MAX_RECENT_FOLDERS) + setRecentFolders(trimmed) + saveRecentFolders(trimmed) +} + +function removeRecentFolder(path: string): void { + const folders = recentFolders().filter((f) => f.path !== path) + setRecentFolders(folders) + saveRecentFolders(folders) +} + +export { preferences, updatePreferences, toggleShowThinkingBlocks, recentFolders, addRecentFolder, removeRecentFolder } diff --git a/src/stores/ui.ts b/src/stores/ui.ts index f7867eeb..6cd1c6ba 100644 --- a/src/stores/ui.ts +++ b/src/stores/ui.ts @@ -3,6 +3,7 @@ import { createSignal } from "solid-js" const [hasInstances, setHasInstances] = createSignal(false) const [selectedFolder, setSelectedFolder] = createSignal(null) const [isSelectingFolder, setIsSelectingFolder] = createSignal(false) +const [showFolderSelection, setShowFolderSelection] = createSignal(false) const [instanceTabOrder, setInstanceTabOrder] = createSignal([]) const [sessionTabOrder, setSessionTabOrder] = createSignal>(new Map()) @@ -26,6 +27,8 @@ export { setSelectedFolder, isSelectingFolder, setIsSelectingFolder, + showFolderSelection, + setShowFolderSelection, instanceTabOrder, setInstanceTabOrder, sessionTabOrder,