From 4526fccdbe6bd804eb26aa131f82d6e319be101b Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Sun, 28 Jun 2026 13:49:45 -0500 Subject: [PATCH] fix(desktop): make project "Add folder" picker remote-gateway aware MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The new-project / add-folder dialog (PR #49037) picked folders via the native Electron dialog (pickDefaultProjectDir), which only browses the LOCAL machine. On a remote gateway that picks a path that doesn't exist on the backend where sessions actually run. Route pickProjectFolder() through selectDesktopPaths({directories, multiple:false}) — the same remote-aware path the retired right-sidebar picker used: local mode opens the native directory dialog, remote mode browses the backend filesystem via the in-app RemoteFolderPicker. Seed it with the backend's default cwd on remote so it opens somewhere useful. --- apps/desktop/src/store/projects.test.ts | 49 ++++++++++++++++++++++++- apps/desktop/src/store/projects.ts | 20 +++++----- 2 files changed, 58 insertions(+), 11 deletions(-) diff --git a/apps/desktop/src/store/projects.test.ts b/apps/desktop/src/store/projects.test.ts index 3e379f8fafa..32749f2b19c 100644 --- a/apps/desktop/src/store/projects.test.ts +++ b/apps/desktop/src/store/projects.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it } from 'vitest' +import { beforeEach, describe, expect, it, vi } from 'vitest' import { $projectScope, @@ -6,9 +6,21 @@ import { ALL_PROJECTS, enterProject, exitProjectScope, + pickProjectFolder, refreshWorktrees } from './projects' +vi.mock('@/lib/desktop-fs', () => ({ + desktopDefaultCwd: vi.fn(), + isDesktopFsRemoteMode: vi.fn(), + selectDesktopPaths: vi.fn() +})) + +const fs = await import('@/lib/desktop-fs') +const desktopDefaultCwd = vi.mocked(fs.desktopDefaultCwd) +const isDesktopFsRemoteMode = vi.mocked(fs.isDesktopFsRemoteMode) +const selectDesktopPaths = vi.mocked(fs.selectDesktopPaths) + describe('project scope', () => { beforeEach(() => { window.localStorage.clear() @@ -50,3 +62,38 @@ describe('worktree refresh', () => { expect($worktreeRefreshToken.get()).toBe(before + 1) }) }) + +describe('pickProjectFolder', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('uses the remote-aware directory picker locally (no backend default cwd probe)', async () => { + isDesktopFsRemoteMode.mockReturnValue(false) + selectDesktopPaths.mockResolvedValue(['/local/repo']) + + await expect(pickProjectFolder()).resolves.toBe('/local/repo') + expect(selectDesktopPaths).toHaveBeenCalledWith({ defaultPath: undefined, directories: true, multiple: false }) + expect(desktopDefaultCwd).not.toHaveBeenCalled() + }) + + it('seeds the picker with the backend cwd on a remote gateway', async () => { + isDesktopFsRemoteMode.mockReturnValue(true) + desktopDefaultCwd.mockResolvedValue({ branch: 'main', cwd: '/backend/work' }) + selectDesktopPaths.mockResolvedValue(['/backend/work/repo']) + + await expect(pickProjectFolder()).resolves.toBe('/backend/work/repo') + expect(selectDesktopPaths).toHaveBeenCalledWith({ + defaultPath: '/backend/work', + directories: true, + multiple: false + }) + }) + + it('returns null when the picker is cancelled (empty selection)', async () => { + isDesktopFsRemoteMode.mockReturnValue(false) + selectDesktopPaths.mockResolvedValue([]) + + await expect(pickProjectFolder()).resolves.toBeNull() + }) +}) diff --git a/apps/desktop/src/store/projects.ts b/apps/desktop/src/store/projects.ts index d30ed4c1972..241898cc11a 100644 --- a/apps/desktop/src/store/projects.ts +++ b/apps/desktop/src/store/projects.ts @@ -2,6 +2,7 @@ import { atom } from 'nanostores' import { liveSessionProjectId, type SidebarProjectTree } from '@/app/chat/sidebar/projects/workspace-groups' import type { HermesGitBranch } from '@/global' +import { desktopDefaultCwd, isDesktopFsRemoteMode, selectDesktopPaths } from '@/lib/desktop-fs' import { persistentAtom } from '@/lib/persisted' import { activeGateway, ensureActiveGatewayOpen } from '@/store/gateway' import { setSidebarAgentsGrouped } from '@/store/layout' @@ -732,19 +733,18 @@ export async function copyPath(path: null | string): Promise { } } -// Open the native directory picker (reuses the Electron default-project-dir -// chooser). Returns the chosen absolute path, or null when cancelled. +// Pick a folder for a project. Routes through the remote-aware folder picker +// (selectDesktopPaths): a remote gateway browses the BACKEND filesystem via the +// in-app RemoteFolderPicker — where sessions actually run — while local mode +// uses the native directory dialog. Returns the chosen absolute path, or null +// when cancelled. Seeded with the backend's default cwd on remote so the picker +// opens somewhere useful instead of "/". export async function pickProjectFolder(): Promise { - const pick = window.hermesDesktop?.settings?.pickDefaultProjectDir - - if (!pick) { - return null - } - try { - const result = await pick() + const defaultPath = isDesktopFsRemoteMode() ? (await desktopDefaultCwd())?.cwd : undefined + const [dir] = await selectDesktopPaths({ defaultPath, directories: true, multiple: false }) - return result.canceled ? null : result.dir + return dir || null } catch { return null }