fix(desktop): route composer context picking through remote-aware fs

Second pass on the remote-project flow: the project dialog and git cockpit were
remote-aware, but the composer's Add file/folder context picker still called the
native Electron picker directly. Route it through selectDesktopPaths so remote
sessions use the backend-aware picker instead of local disk paths; preserve local
multi-select behavior and keep remote folder selection single because the in-app
remote picker only supports one directory.

Also use readDesktopFileDataUrl for image previews so an already-known backend
image path can be read through /api/fs/read-data-url, and add focused coverage
for backend file-diff routing plus the plain-folder git init/worktree path.
This commit is contained in:
Brooklyn Nicholson 2026-06-28 14:35:23 -05:00
parent 9b71221187
commit 4e9439cc3b
3 changed files with 46 additions and 4 deletions

View file

@ -5,6 +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 {
addComposerAttachment,
type ComposerAttachment,
@ -262,10 +263,11 @@ export function useComposerActions({ activeSessionId, currentCwd, requestGateway
const pickContextPaths = useCallback(
async (kind: 'file' | 'folder') => {
const paths = await window.hermesDesktop?.selectPaths({
const paths = await selectDesktopPaths({
title: kind === 'file' ? 'Add files as context' : 'Add folders as context',
defaultPath: currentCwd || undefined,
directories: kind === 'folder'
directories: kind === 'folder',
multiple: kind === 'folder' && isDesktopFsRemoteMode() ? false : undefined
})
if (!paths?.length) {
@ -347,7 +349,7 @@ export function useComposerActions({ activeSessionId, currentCwd, requestGateway
attachToMain(baseAttachment)
try {
const previewUrl = await window.hermesDesktop?.readFileDataUrl(filePath)
const previewUrl = await readDesktopFileDataUrl(filePath)
if (previewUrl) {
addComposerAttachment({ ...baseAttachment, previewUrl })
@ -395,7 +397,7 @@ export function useComposerActions({ activeSessionId, currentCwd, requestGateway
)
const pickImages = useCallback(async () => {
const paths = await window.hermesDesktop?.selectPaths({
const paths = await selectDesktopPaths({
title: copy.attachImages,
defaultPath: currentCwd || undefined,
filters: [

View file

@ -4,6 +4,7 @@ import { $connection } from '@/store/session'
import {
desktopDefaultCwd,
desktopFileDiff,
desktopGitRoot,
readDesktopDir,
readDesktopFileDataUrl,
@ -39,6 +40,10 @@ const api = vi.fn(async ({ path }: { path: string }) => {
return { cwd: '/backend/project', branch: 'main' }
}
if (path.startsWith('/api/git/file-diff?')) {
return { diff: 'remote diff' }
}
throw new Error(`unexpected path ${path}`)
})
@ -107,6 +112,13 @@ describe('desktop filesystem facade', () => {
expect(gitRoot).not.toHaveBeenCalled()
})
it('routes file diffs through backend git in remote mode', async () => {
$connection.set({ mode: 'remote' } as never)
await expect(desktopFileDiff('/repo', 'src/a b.ts')).resolves.toBe('remote diff')
expect(api).toHaveBeenCalledWith({ path: '/api/git/file-diff?path=%2Frepo&file=src%2Fa%20b.ts' })
})
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)

View file

@ -106,6 +106,14 @@ def test_stage_commit_roundtrip_clears_changes(client, repo):
assert after["untracked"] == 1
def test_commit_with_nothing_staged_commits_all_changes(client, repo):
assert client.post(
"/api/git/review/commit", json={"path": str(repo), "message": "commit all", "push": False}
).json() == {"ok": True}
assert client.get("/api/git/status", params={"path": str(repo)}).json()["changed"] == 0
def test_worktrees_and_branch_lifecycle(client, repo):
worktrees = client.get("/api/git/worktrees", params={"path": str(repo)}).json()["worktrees"]
assert any(tree["isMain"] and tree["path"] == str(repo) for tree in worktrees)
@ -125,6 +133,26 @@ def test_worktrees_and_branch_lifecycle(client, repo):
assert removed["removed"]
def test_worktree_add_initializes_plain_folder(client, tmp_path):
folder = tmp_path / "plain-project"
folder.mkdir()
(folder / "notes.txt").write_text("not committed\n")
added = client.post(
"/api/git/worktree/add", json={"path": str(folder), "branch": "feature/plain"}
).json()
assert added["branch"] == "feature/plain"
assert Path(added["path"]).is_dir()
assert (folder / ".git").exists()
_git(folder, "rev-parse", "--verify", "HEAD")
status = client.get("/api/git/status", params={"path": str(folder)}).json()
assert status["branch"] == "main"
# Existing files are not silently committed by repo initialization.
assert any(file["path"] == "notes.txt" and file["untracked"] for file in status["files"])
def test_commit_context_includes_diff_and_untracked(client, repo):
body = client.get("/api/git/review/commit-context", params={"path": str(repo)}).json()