fix(desktop): make project "Add folder" picker remote-gateway aware

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.
This commit is contained in:
Brooklyn Nicholson 2026-06-28 13:49:45 -05:00
parent b699d27a4a
commit 4526fccdbe
2 changed files with 58 additions and 11 deletions

View file

@ -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()
})
})

View file

@ -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<void> {
}
}
// 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<null | string> {
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
}