diff --git a/apps/desktop/src/app/chat/right-rail/preview-pane.test.tsx b/apps/desktop/src/app/chat/right-rail/preview-pane.test.tsx
index 163511b05bc..ba17b1322de 100644
--- a/apps/desktop/src/app/chat/right-rail/preview-pane.test.tsx
+++ b/apps/desktop/src/app/chat/right-rail/preview-pane.test.tsx
@@ -1,11 +1,50 @@
import { act, cleanup, render } from '@testing-library/react'
-import { afterEach, describe, expect, it, vi } from 'vitest'
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
+
+import { $connection } from '@/store/session'
import { PreviewPane } from './preview-pane'
describe('PreviewPane console state', () => {
+ beforeEach(() => {
+ vi.stubGlobal('requestAnimationFrame', (callback: FrameRequestCallback) => window.setTimeout(() => callback(Date.now()), 0))
+ vi.stubGlobal('cancelAnimationFrame', (id: number) => window.clearTimeout(id))
+ })
+
afterEach(() => {
cleanup()
+ $connection.set(null)
+ vi.unstubAllGlobals()
+ })
+
+ it('does not watch backend-only remote filesystem previews locally', () => {
+ const watchPreviewFile = vi.fn(async () => ({ id: 'watch-1', path: '/remote/file.txt' }))
+ const onPreviewFileChanged = vi.fn(() => vi.fn())
+ $connection.set({ mode: 'remote' } as never)
+ vi.stubGlobal('window', {
+ ...window,
+ hermesDesktop: {
+ onPreviewFileChanged,
+ watchPreviewFile
+ }
+ })
+
+ render(
+
+ )
+
+ expect(watchPreviewFile).not.toHaveBeenCalled()
+ expect(onPreviewFileChanged).not.toHaveBeenCalled()
})
it('does not rebuild the pane titlebar group for streamed console logs', () => {
diff --git a/apps/desktop/src/app/chat/right-rail/preview-pane.tsx b/apps/desktop/src/app/chat/right-rail/preview-pane.tsx
index 21cfbeb3ced..ae9d51d1e74 100644
--- a/apps/desktop/src/app/chat/right-rail/preview-pane.tsx
+++ b/apps/desktop/src/app/chat/right-rail/preview-pane.tsx
@@ -5,6 +5,7 @@ import { useCallback, useEffect, useRef, useState } from 'react'
import type { SetTitlebarToolGroup, TitlebarTool } from '@/app/shell/titlebar-controls'
import { Tip } from '@/components/ui/tooltip'
import { type Translations, useI18n } from '@/i18n'
+import { isDesktopFsRemoteMode } from '@/lib/desktop-fs'
import { Bug } from '@/lib/icons'
import { cn } from '@/lib/utils'
import { notify, notifyError } from '@/store/notifications'
@@ -406,6 +407,7 @@ export function PreviewPane({
useEffect(() => {
if (
target.kind !== 'file' ||
+ isDesktopFsRemoteMode() ||
!window.hermesDesktop?.watchPreviewFile ||
!window.hermesDesktop?.onPreviewFileChanged
) {
diff --git a/apps/desktop/src/app/right-sidebar/files/ipc.ts b/apps/desktop/src/app/right-sidebar/files/ipc.ts
index 466f38715f6..7ffed007d0a 100644
--- a/apps/desktop/src/app/right-sidebar/files/ipc.ts
+++ b/apps/desktop/src/app/right-sidebar/files/ipc.ts
@@ -97,7 +97,7 @@ async function gitignoreFor(dir: string) {
let cached = gitignoreCache.get(key)
if (!cached) {
- cached = readGitignore(key)
+ cached = readGitignore(clean(dir))
gitignoreCache.set(key, cached)
}
diff --git a/apps/desktop/src/app/right-sidebar/files/use-project-tree.test.ts b/apps/desktop/src/app/right-sidebar/files/use-project-tree.test.ts
index d1c0018bf2e..03027883781 100644
--- a/apps/desktop/src/app/right-sidebar/files/use-project-tree.test.ts
+++ b/apps/desktop/src/app/right-sidebar/files/use-project-tree.test.ts
@@ -1,19 +1,24 @@
-import { act, renderHook, waitFor } from '@testing-library/react'
+import { act, cleanup, renderHook, waitFor } from '@testing-library/react'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
+import { $connection } from '@/store/session'
import type { HermesReadDirResult } from '@/global'
+import { clearProjectDirCache, readProjectDir } from './ipc'
import { resetProjectTreeState, useProjectTree } from './use-project-tree'
const readDir = vi.fn<(path: string) => Promise>()
beforeEach(() => {
+ $connection.set(null)
resetProjectTreeState()
readDir.mockReset()
;(window as unknown as { hermesDesktop: { readDir: typeof readDir } }).hermesDesktop = { readDir }
})
afterEach(() => {
+ cleanup()
+ $connection.set(null)
resetProjectTreeState()
delete (window as unknown as { hermesDesktop?: unknown }).hermesDesktop
})
@@ -106,6 +111,36 @@ describe('useProjectTree', () => {
expect(readDir).toHaveBeenCalledTimes(1)
})
+ it('reads gitignore from the real path while caching per connection', async () => {
+ const readFileDataUrl = vi.fn(async () => `data:text/plain;base64,${btoa('ignored.log\n')}`)
+ const gitRoot = vi.fn(async () => '/repo')
+ readDir.mockImplementation(async path => {
+ if (path === '/repo') return ok([{ name: '.gitignore', path: '/repo/.gitignore', isDirectory: false }])
+ if (path === '/repo/src') {
+ return ok([
+ { name: 'app.ts', path: '/repo/src/app.ts', isDirectory: false },
+ { name: 'ignored.log', path: '/repo/src/ignored.log', isDirectory: false }
+ ])
+ }
+ throw new Error(`unexpected path ${path}`)
+ })
+ ;(window as unknown as { hermesDesktop: unknown }).hermesDesktop = { gitRoot, readDir, readFileDataUrl }
+
+ $connection.set({ baseUrl: 'local-a', mode: 'local' } as never)
+ await expect(readProjectDir('/repo/src', '/repo')).resolves.toMatchObject({
+ entries: [{ name: 'app.ts', path: '/repo/src/app.ts', isDirectory: false }]
+ })
+ expect(readDir).toHaveBeenCalledWith('/repo')
+ expect(readDir).not.toHaveBeenCalledWith(expect.stringContaining('local-a'))
+
+ $connection.set({ baseUrl: 'local-b', mode: 'local' } as never)
+ clearProjectDirCache()
+ await expect(readProjectDir('/repo/src', '/repo')).resolves.toMatchObject({
+ entries: [{ name: 'app.ts', path: '/repo/src/app.ts', isDirectory: false }]
+ })
+ expect(readDir.mock.calls.filter(([path]) => path === '/repo')).toHaveLength(2)
+ })
+
it('captures per-folder error code and shows an error placeholder child', async () => {
readDir.mockResolvedValueOnce(ok([{ name: 'priv', path: '/p/priv', isDirectory: true }]))
readDir.mockResolvedValueOnce({ entries: [], error: 'EACCES' })
diff --git a/apps/desktop/src/lib/desktop-fs.test.ts b/apps/desktop/src/lib/desktop-fs.test.ts
index 23c9ea8faf4..16be8790901 100644
--- a/apps/desktop/src/lib/desktop-fs.test.ts
+++ b/apps/desktop/src/lib/desktop-fs.test.ts
@@ -85,7 +85,7 @@ describe('desktop filesystem facade', () => {
expect(gitRoot).not.toHaveBeenCalled()
})
- it('uses the registered in-app picker in remote mode', async () => {
+ it('uses the registered in-app directory picker in remote mode', async () => {
const remoteSelect = vi.fn(async () => ['/remote/project'])
$connection.set({ mode: 'remote' } as never)
setDesktopFsRemotePicker({ selectPaths: remoteSelect })
@@ -97,4 +97,16 @@ describe('desktop filesystem facade', () => {
expect(remoteSelect).toHaveBeenCalledWith({ defaultPath: '/remote', directories: true, multiple: false })
expect(selectPaths).not.toHaveBeenCalled()
})
+
+ it('does not treat the remote directory picker as a general file picker', 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([])
+
+ expect(remoteSelect).not.toHaveBeenCalled()
+ expect(selectPaths).not.toHaveBeenCalled()
+ })
})
diff --git a/apps/desktop/src/lib/desktop-fs.ts b/apps/desktop/src/lib/desktop-fs.ts
index 95931b14530..99381bbca9b 100644
--- a/apps/desktop/src/lib/desktop-fs.ts
+++ b/apps/desktop/src/lib/desktop-fs.ts
@@ -80,5 +80,8 @@ export async function selectDesktopPaths(options?: HermesSelectPathsOptions): Pr
if (!isDesktopFsRemoteMode()) {
return desktop.selectPaths(options)
}
+ if (!options?.directories || options.multiple !== false) {
+ return []
+ }
return remotePicker ? remotePicker.selectPaths(options) : []
}