diff --git a/apps/desktop/src/app/chat/hooks/use-composer-actions.ts b/apps/desktop/src/app/chat/hooks/use-composer-actions.ts index ecc13808413..f72f9782398 100644 --- a/apps/desktop/src/app/chat/hooks/use-composer-actions.ts +++ b/apps/desktop/src/app/chat/hooks/use-composer-actions.ts @@ -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: [ diff --git a/apps/desktop/src/lib/desktop-fs.test.ts b/apps/desktop/src/lib/desktop-fs.test.ts index d9c999773f4..0d5955313c6 100644 --- a/apps/desktop/src/lib/desktop-fs.test.ts +++ b/apps/desktop/src/lib/desktop-fs.test.ts @@ -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) diff --git a/tests/hermes_cli/test_web_server_git.py b/tests/hermes_cli/test_web_server_git.py index 25879388076..a3f98c670f6 100644 --- a/tests/hermes_cli/test_web_server_git.py +++ b/tests/hermes_cli/test_web_server_git.py @@ -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()