feat: add workspace files to chat from Files and Changes

* feat(app): open changed files from context menu

* fix(app): preserve changed-file row behavior

* feat(app): add changed files to open chats

* feat(app): share file actions across explorer and changes

* feat(app): attach workspace files to focused chat

* feat(app): drag workspace files into chat

* refactor(app): use file actions menu for changed files

* fix(app): open changed-file actions on right click

* test(app): cover direct file attachment flow

* fix(app): enable add to chat on native

* fix(app): align workspace file pane rows

* refactor(app): simplify workspace file attachments

* fix(app): preserve uploaded file draft attachments

---------

Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
This commit is contained in:
nikuscs
2026-07-22 15:58:24 +02:00
committed by GitHub
co-authored by Mohamed Boudra
parent 42ee5a5949
commit bd937850b3
42 changed files with 1665 additions and 342 deletions
@@ -0,0 +1,56 @@
import { useCallback, useMemo } from "react";
import { useHosts } from "@/runtime/host-runtime";
import { useDownloadStore } from "@/stores/download-store";
import { useFileExplorerActions } from "@/hooks/use-file-explorer-actions";
interface UseFileDownloadParams {
serverId: string;
workspaceId?: string | null;
workspaceRoot: string;
}
/**
* Returns a stable callback that downloads a single workspace file by its
* workspace-relative path. Shared by the file explorer tree and the git diff
* pane so both surfaces download through the same host token + download-store
* pipeline instead of duplicating the plumbing.
*/
export function useFileDownload({
serverId,
workspaceId,
workspaceRoot,
}: UseFileDownloadParams): (input: { fileName: string; path: string }) => void {
const daemons = useHosts();
const daemonProfile = useMemo(
() => daemons.find((daemon) => daemon.serverId === serverId),
[daemons, serverId],
);
const normalizedWorkspaceRoot = useMemo(() => workspaceRoot.trim(), [workspaceRoot]);
const workspaceScopeId = useMemo(
() => workspaceId?.trim() || normalizedWorkspaceRoot,
[normalizedWorkspaceRoot, workspaceId],
);
const { requestFileDownloadToken } = useFileExplorerActions({
serverId,
workspaceId,
workspaceRoot: normalizedWorkspaceRoot,
});
const startDownload = useDownloadStore((state) => state.startDownload);
return useCallback(
({ fileName, path }) => {
if (!workspaceScopeId) {
return;
}
void startDownload({
serverId,
scopeId: workspaceScopeId,
fileName,
path,
daemonProfile,
requestFileDownloadToken: (targetPath) => requestFileDownloadToken(targetPath),
});
},
[daemonProfile, requestFileDownloadToken, serverId, startDownload, workspaceScopeId],
);
}