refactor(desktop): keep remote fs routing inside the fs facade

Let UI callers ask for folders/files without knowing remote-picker limits:
selectDesktopPaths now normalizes remote directory selection to a single folder
inside the facade. Project creation and composer context picking no longer branch
on remote mode; they route through desktop-fs helpers just like git callers route
through desktopGit(). Behavior unchanged except remote folder context now works
through the same backend picker path.
This commit is contained in:
Brooklyn Nicholson 2026-06-28 14:39:33 -05:00
parent 453f134b3b
commit 8d8c7111d9
5 changed files with 15 additions and 14 deletions

View File

@ -5,7 +5,7 @@ import { droppedFileInlineRef } from '@/app/chat/composer/inline-refs'
import { formatRefValue } from '@/components/assistant-ui/directive-text'
import { useI18n } from '@/i18n'
import { attachmentId, contextPath, pathLabel } from '@/lib/chat-runtime'
import { isDesktopFsRemoteMode, readDesktopFileDataUrl, selectDesktopPaths } from '@/lib/desktop-fs'
import { readDesktopFileDataUrl, selectDesktopPaths } from '@/lib/desktop-fs'
import {
addComposerAttachment,
type ComposerAttachment,
@ -266,8 +266,7 @@ export function useComposerActions({ activeSessionId, currentCwd, requestGateway
const paths = await selectDesktopPaths({
title: kind === 'file' ? 'Add files as context' : 'Add folders as context',
defaultPath: currentCwd || undefined,
directories: kind === 'folder',
multiple: kind === 'folder' && isDesktopFsRemoteMode() ? false : undefined
directories: kind === 'folder'
})
if (!paths?.length) {

View File

@ -132,15 +132,15 @@ describe('desktop filesystem facade', () => {
expect(selectPaths).not.toHaveBeenCalled()
})
it('does not treat the remote directory picker as a general file picker', async () => {
it('limits the remote picker to single-directory selection', async () => {
const remoteSelect = vi.fn(async () => ['/remote/project'])
$connection.set({ mode: 'remote' } as never)
setDesktopFsRemotePicker({ selectPaths: remoteSelect })
await expect(selectDesktopPaths({ directories: false, multiple: false })).resolves.toEqual([])
await expect(selectDesktopPaths({ directories: true, multiple: true })).resolves.toEqual([])
await expect(selectDesktopPaths({ directories: true })).resolves.toEqual(['/remote/project'])
expect(remoteSelect).not.toHaveBeenCalled()
expect(remoteSelect).toHaveBeenCalledWith({ directories: true, multiple: false })
expect(selectPaths).not.toHaveBeenCalled()
})
})

View File

@ -178,9 +178,9 @@ export async function selectDesktopPaths(options?: HermesSelectPathsOptions): Pr
return desktop.selectPaths(options)
}
if (!options?.directories || options.multiple !== false) {
if (!options?.directories) {
return []
}
return remotePicker ? remotePicker.selectPaths(options) : []
return remotePicker ? remotePicker.selectPaths({ ...options, multiple: false }) : []
}

View File

@ -68,13 +68,12 @@ describe('pickProjectFolder', () => {
vi.clearAllMocks()
})
it('uses the remote-aware directory picker locally (no backend default cwd probe)', async () => {
it('uses the remote-aware directory picker locally', 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 () => {

View File

@ -2,7 +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, writeDesktopFileText } from '@/lib/desktop-fs'
import { desktopDefaultCwd, selectDesktopPaths, writeDesktopFileText } from '@/lib/desktop-fs'
import { desktopGit } from '@/lib/desktop-git'
import { persistentAtom } from '@/lib/persisted'
import { activeGateway, ensureActiveGatewayOpen } from '@/store/gateway'
@ -282,7 +282,7 @@ export async function fetchProjectSessions(projectId: string): Promise<SidebarPr
let didScanRepos = false
export async function scanAndRecordRepos(force = false): Promise<void> {
const scan = window.hermesDesktop?.git?.scanRepos
const scan = desktopGit()?.scanRepos
if (!scan || (didScanRepos && !force)) {
return
@ -738,8 +738,11 @@ export async function copyPath(path: null | string): Promise<void> {
// mode opens the native dialog. Returns the absolute path, or null if cancelled.
export async function pickProjectFolder(): Promise<null | string> {
try {
const defaultPath = isDesktopFsRemoteMode() ? (await desktopDefaultCwd())?.cwd : undefined
const [dir] = await selectDesktopPaths({ defaultPath, directories: true, multiple: false })
const [dir] = await selectDesktopPaths({
defaultPath: (await desktopDefaultCwd())?.cwd,
directories: true,
multiple: false
})
return dir || null
} catch {