From a3231c3da565e2ecab64102632fb68ef88e2c3d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pascal=20Andr=C3=A9?= Date: Sun, 10 May 2026 19:36:49 +0200 Subject: [PATCH] fix(ui): return to active projects from home (#411) ## Summary - Return to an existing open project when users choose that from the already-open folder dialog. - When users click a recent folder that is already open, ask whether to switch to the open project or intentionally open another instance. - Surface a prominent Return to Active Project action so leaving the home screen no longer depends on the subtle top-right close affordance. - Keep multi-instance workflows available without requiring users to remember a separate row-level button. Fixes #281 ## Why Users can accidentally open the same workspace several times from the add-project screen. The app now detects that ambiguity when the already-open folder is clicked and asks what should happen, instead of making the user remember a special alternate button ahead of time. The dialog keeps the copy short: the title states that the project is already open, and the buttons carry the actual choices. ## Verification - `git diff --check` - `npm run typecheck --workspace @codenomad/ui` - `npm run build --workspace @codenomad/ui` - `npm run build:tauri` --- packages/ui/src/App.tsx | 68 +++++++++++++++++-- .../src/components/folder-selection-view.tsx | 43 ++++++++++-- .../lib/i18n/messages/en/folderSelection.ts | 7 ++ .../lib/i18n/messages/es/folderSelection.ts | 7 ++ .../lib/i18n/messages/fr/folderSelection.ts | 7 ++ .../lib/i18n/messages/he/folderSelection.ts | 7 ++ .../lib/i18n/messages/ja/folderSelection.ts | 7 ++ .../lib/i18n/messages/ru/folderSelection.ts | 7 ++ .../i18n/messages/zh-Hans/folderSelection.ts | 7 ++ packages/ui/src/stores/instances.ts | 25 +++++++ 10 files changed, 175 insertions(+), 10 deletions(-) diff --git a/packages/ui/src/App.tsx b/packages/ui/src/App.tsx index d32a3a80..44f0d23f 100644 --- a/packages/ui/src/App.tsx +++ b/packages/ui/src/App.tsx @@ -34,6 +34,7 @@ import { import { useConfig } from "./stores/preferences" import { createInstance, + getExistingInstanceForFolder, instances, stopInstance, disconnectedInstance, @@ -95,6 +96,11 @@ const App: Component = () => { const [escapeInDebounce, setEscapeInDebounce] = createSignal(false) const [instanceTabBarHeight, setInstanceTabBarHeight] = createSignal(0) const [sidecarPickerOpen, setSidecarPickerOpen] = createSignal(false) + const [alreadyOpenFolderChoice, setAlreadyOpenFolderChoice] = createSignal<{ + folderPath: string + binaryPath: string + instanceId: string + } | null>(null) const phoneQuery = useMediaQuery("(max-width: 767px)") const isPhoneLayout = createMemo(() => phoneQuery()) @@ -258,15 +264,24 @@ const App: Component = () => { const launchErrorMessage = () => launchError()?.message ?? "" - async function handleSelectFolder(folderPath: string, binaryPath?: string) { + async function handleSelectFolder(folderPath: string, binaryPath?: string, options?: { forceNew?: boolean }) { if (!folderPath) { return } - setIsSelectingFolder(true) const selectedBinary = binaryPath || serverSettings().opencodeBinary || "opencode" + recordWorkspaceLaunch(folderPath, selectedBinary) + clearLaunchError() + + if (!options?.forceNew) { + const existingInstance = getExistingInstanceForFolder(folderPath) + if (existingInstance) { + setAlreadyOpenFolderChoice({ folderPath, binaryPath: selectedBinary, instanceId: existingInstance.id }) + return + } + } + + setIsSelectingFolder(true) try { - recordWorkspaceLaunch(folderPath, selectedBinary) - clearLaunchError() const instanceId = await createInstance(folderPath, selectedBinary) selectInstanceTab(instanceId) setShowFolderSelection(false) @@ -285,6 +300,26 @@ const App: Component = () => { } } + function dismissAlreadyOpenFolderChoice() { + setAlreadyOpenFolderChoice(null) + } + + function switchToAlreadyOpenFolder() { + const choice = alreadyOpenFolderChoice() + if (!choice) return + setAlreadyOpenFolderChoice(null) + selectInstanceTab(choice.instanceId) + setShowFolderSelection(false) + log.info("Selected existing instance", { instanceId: choice.instanceId, folderPath: choice.folderPath }) + } + + function openAnotherFolderInstance() { + const choice = alreadyOpenFolderChoice() + if (!choice) return + setAlreadyOpenFolderChoice(null) + void handleSelectFolder(choice.folderPath, choice.binaryPath, { forceNew: true }) + } + function handleLaunchErrorClose() { clearLaunchError() } @@ -609,6 +644,7 @@ const App: Component = () => { onSelectFolder={handleSelectFolder} isLoading={isSelectingFolder()} onOpenSidecar={handleOpenSidecarPicker} + activeProjectLabel={activeInstance()?.folder ?? null} onClose={() => { setShowFolderSelection(false) clearLaunchError() @@ -620,6 +656,30 @@ const App: Component = () => { setSidecarPickerOpen(false)} onOpenSidecar={handleOpenSidecar} /> + + !open && dismissAlreadyOpenFolderChoice()}> + + + + + {t("folderSelection.recent.alreadyOpenTitle")} + + + {t("folderSelection.recent.alreadyOpenMessage")} + + +
+ + +
+
+
+
+
diff --git a/packages/ui/src/components/folder-selection-view.tsx b/packages/ui/src/components/folder-selection-view.tsx index 35c984de..286327b3 100644 --- a/packages/ui/src/components/folder-selection-view.tsx +++ b/packages/ui/src/components/folder-selection-view.tsx @@ -1,7 +1,7 @@ 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, GitBranch } from "lucide-solid" +import { Component, createMemo, createSignal, Show, For, onMount, onCleanup, createEffect } from "solid-js" +import { Folder, Clock, Trash2, FolderPlus, Settings, ChevronRight, MonitorUp, Star, Languages, ChevronDown, X, Globe, Loader2, GitBranch, ArrowLeft } from "lucide-solid" import { useConfig } from "../stores/preferences" import DirectoryBrowserDialog from "./directory-browser-dialog" import Kbd from "./kbd" @@ -18,6 +18,7 @@ import { openExternalUrl } from "../lib/external-url" import { serverApi } from "../lib/api-client" import { canOpenRemoteWindows, isTauriHost } from "../lib/runtime-env" import { openRemoteServerWindow } from "../lib/native/remote-window" +import { getExistingInstanceForFolder } from "../stores/instances" const codeNomadLogo = new URL("../images/CodeNomad-Icon.png", import.meta.url).href const GITHUB_URL = "https://github.com/NeuralNomadsAI/CodeNomad" @@ -27,10 +28,11 @@ type HomeTab = "local" | "servers" interface FolderSelectionViewProps { - onSelectFolder: (folder: string, binaryPath?: string) => void + onSelectFolder: (folder: string, binaryPath?: string, options?: { forceNew?: boolean }) => void onOpenSidecar?: () => void isLoading?: boolean onClose?: () => void + activeProjectLabel?: string | null } const FolderSelectionView: Component = (props) => { @@ -85,6 +87,11 @@ const FolderSelectionView: Component = (props) => { const serverList = () => remoteServers() const isLoading = () => Boolean(props.isLoading) const canUseRemoteServerWindows = () => canOpenRemoteWindows() + const activeProjectName = createMemo(() => { + const label = props.activeProjectLabel?.trim() + if (!label) return null + return splitFolderPath(label).baseName + }) function getActiveListLength() { return activeTab() === "local" ? folders().length : serverList().length @@ -862,8 +869,10 @@ const FolderSelectionView: Component = (props) => { ref={(el) => (recentListRef = el)} > - {(folder, index) => ( -
{ + const existingInstance = () => getExistingInstanceForFolder(folder.path) + + return
= (props) => { {splitFolderPath(folder.path).baseName} + + + {t("folderSelection.recent.openBadge")} + +
@@ -912,7 +926,7 @@ const FolderSelectionView: Component = (props) => {
- )} + }}
@@ -947,6 +961,23 @@ const FolderSelectionView: Component = (props) => { + + + + + + {(name) =>

{t("folderSelection.actions.returnToProjectSubtitle", { name: name() })}

} +
+