From 0e3f4824fd3e73dac4719eedc860f9c80d6e2c34 Mon Sep 17 00:00:00 2001 From: kccarlos Date: Tue, 10 Feb 2026 17:16:56 -0800 Subject: [PATCH 01/55] feat(desktop): add batch file selection from clipboard in file tree --- apps/desktop/src-tauri/tauri.conf.json | 1 + apps/desktop/src/App.tsx | 41 +++++++- apps/desktop/src/hooks/useFileTree.ts | 10 ++ .../src/utils/clipboardBatchSelect.test.ts | 41 ++++++++ .../desktop/src/utils/clipboardBatchSelect.ts | 98 +++++++++++++++++++ 5 files changed, 190 insertions(+), 1 deletion(-) create mode 100644 apps/desktop/src/utils/clipboardBatchSelect.test.ts create mode 100644 apps/desktop/src/utils/clipboardBatchSelect.ts diff --git a/apps/desktop/src-tauri/tauri.conf.json b/apps/desktop/src-tauri/tauri.conf.json index 75ef9d2..209ad62 100644 --- a/apps/desktop/src-tauri/tauri.conf.json +++ b/apps/desktop/src-tauri/tauri.conf.json @@ -49,6 +49,7 @@ "dialog:allow-open", "fs:allow-read-dir", "fs:allow-read-file", + "clipboard-manager:allow-read-text", "clipboard-manager:allow-write-text" ] } diff --git a/apps/desktop/src/App.tsx b/apps/desktop/src/App.tsx index 4cd80ea..6282cb9 100644 --- a/apps/desktop/src/App.tsx +++ b/apps/desktop/src/App.tsx @@ -3,7 +3,7 @@ import './App.css' import { ChevronsDown, ChevronsUp, CheckSquare, Square, Sun, Moon, Folder, FolderGit2, ListChecks, Copy, ArrowLeftRight } from 'lucide-react' import { FileTreeView, PreviewModal, GitHubStarIconButton, BugIconButton } from '@gitcontext/ui' import { type FileDiffStatus, isBinaryPath, MAX_CONCURRENT_READS } from '@gitcontext/core' -import { writeText } from '@tauri-apps/plugin-clipboard-manager' +import { readText, writeText } from '@tauri-apps/plugin-clipboard-manager' import { useGitRepository } from './hooks/useGitRepository' import { useFileTree } from './hooks/useFileTree' import { SelectedFilesPanel } from './components/SelectedFilesPanel' @@ -21,6 +21,12 @@ import { TokenCountsProvider } from './context/TokenCountsContext' import { mapWithConcurrency } from './utils/concurrency' import { logError } from './utils/logger' import { debounce } from './utils/debounce' +import { + INVALID_CLIPBOARD_FORMAT_MESSAGE, + NO_MATCHING_FILES_MESSAGE, + parseClipboardPathLines, + resolveSelectablePaths, +} from './utils/clipboardBatchSelect' function AppContent() { const [, setAppStatus] = useState({ state: 'IDLE' }) @@ -152,6 +158,7 @@ function AppContent() { collapseAll, selectAll, deselectAll, + addSelectedPaths, revealPath, } = useFileTree(setAppStatus) @@ -356,6 +363,37 @@ function AppContent() { } }, [gitClient, baseBranch, compareBranch, selectedPaths, diffContextLines, statusByPath, userInstructions, fileTree, includeFileTree, showChangedOnly, currentDir]) + const handleBatchSelectFromClipboard = useCallback(async () => { + if (!currentDir || !fileTree) return + + try { + const clipboardText = await readText() + const lines = parseClipboardPathLines(clipboardText) + if (lines.length === 0) { + setErrorMessage(INVALID_CLIPBOARD_FORMAT_MESSAGE) + return + } + + const selectableSet = new Set(statusByPath.keys()) + const { matched, invalidCount, outsideRepoCount } = resolveSelectablePaths(lines, currentDir, selectableSet) + + if (matched.length === 0) { + if (invalidCount === lines.length && outsideRepoCount === 0) { + setErrorMessage(INVALID_CLIPBOARD_FORMAT_MESSAGE) + } else { + setErrorMessage(NO_MATCHING_FILES_MESSAGE) + } + return + } + + addSelectedPaths(matched) + setErrorMessage(null) + } catch (err) { + logError('batchSelectFromClipboard', err) + setErrorMessage('Failed to read clipboard content.') + } + }, [currentDir, fileTree, statusByPath, addSelectedPaths]) + // Calculate file tree tokens useEffect(() => { if (!includeFileTree || !fileTree || selectedPaths.size === 0) { @@ -539,6 +577,7 @@ function AppContent() { +