From e3df75cc38a2991e26858d77edd6756ca8b8795c Mon Sep 17 00:00:00 2001 From: Christoph Brandau Date: Thu, 2 Jul 2026 16:24:38 +0200 Subject: [PATCH] add new Context Menu for file Explorer fix blocking UI when select the file in Status also select in the file History --- src-tauri/Cargo.lock | 26 ++-- src-tauri/Cargo.toml | 2 +- src-tauri/src/git.rs | 186 +++++++++++++++++++++--- src-tauri/src/main.rs | 17 ++- src/App.svelte | 93 +++++++++--- src/app.css | 48 +++++- src/lib/TitleBar.svelte | 6 + src/lib/components/BranchPanel.svelte | 2 +- src/lib/components/ExplorerPanel.svelte | 59 +++++++- src/lib/components/StatusPanel.svelte | 18 ++- src/lib/git.ts | 17 ++- 11 files changed, 399 insertions(+), 75 deletions(-) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index e90ddc4..749251e 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1392,6 +1392,19 @@ dependencies = [ "winapi", ] +[[package]] +name = "git_lite" +version = "0.1.0" +dependencies = [ + "keyring", + "serde", + "serde_json", + "tauri", + "tauri-build", + "tauri-plugin-dialog", + "tauri-plugin-updater", +] + [[package]] name = "glib" version = "0.18.5" @@ -4121,19 +4134,6 @@ dependencies = [ "toml 1.1.2+spec-1.1.0", ] -[[package]] -name = "tauri_git_lite" -version = "0.1.0" -dependencies = [ - "keyring", - "serde", - "serde_json", - "tauri", - "tauri-build", - "tauri-plugin-dialog", - "tauri-plugin-updater", -] - [[package]] name = "tempfile" version = "3.27.0" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 486da8d..acc7789 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "tauri_git_lite" +name = "git_lite" version = "0.1.0" description = "Rust backend for a lightweight Git desktop client" edition = "2021" diff --git a/src-tauri/src/git.rs b/src-tauri/src/git.rs index 261d07e..4b8d8a2 100644 --- a/src-tauri/src/git.rs +++ b/src-tauri/src/git.rs @@ -214,6 +214,22 @@ pub fn open_repo_in_explorer(path: String) -> Result<(), String> { open_path_in_file_manager(&repo) } +#[tauri::command] +pub fn open_repository_file(path: String, file: String) -> Result<(), String> { + let repo = resolve_repo(&path)?; + validate_files(std::slice::from_ref(&file))?; + let file_path = resolve_repo_child_path(&repo, &file)?; + + if !file_path.exists() { + return Err(format!("Datei '{file}' existiert im Working Tree nicht.")); + } + if !file_path.is_file() { + return Err(format!("'{file}' ist keine Datei.")); + } + + reveal_path_in_file_manager(&file_path) +} + #[derive(Debug, Clone, Serialize)] pub struct RepositoryBundle { pub status: GitStatus, @@ -786,15 +802,60 @@ pub fn list_repository_files(path: String) -> Result, Str } #[tauri::command] -pub fn list_file_history( +pub async fn list_file_history( path: String, file: String, limit: Option, + request_id: Option, + state: tauri::State<'_, SearchCancellationState>, ) -> Result, String> { - let repo = resolve_repo(&path)?; + let state = state.inner().clone(); + + tauri::async_runtime::spawn_blocking(move || { + let repo = resolve_repo(&path)?; + let request_id = request_id + .map(|id| id.trim().to_string()) + .filter(|id| !id.is_empty()); + let cancellation = request_id.as_ref().map(|request_id| SearchCancellation { + state: state.clone(), + search_id: request_id.clone(), + }); + + let result = list_file_history_core(&repo, file, limit, cancellation.as_ref()); + + if let Some(request_id) = request_id.as_deref() { + let _ = state.clear(request_id); + } + + result + }) + .await + .map_err(|err| format!("Dateihistorie konnte nicht geladen werden: {err}"))? +} + +#[tauri::command] +pub fn cancel_file_history( + request_id: String, + state: tauri::State<'_, SearchCancellationState>, +) -> Result<(), String> { + let request_id = request_id.trim(); + if request_id.is_empty() { + return Ok(()); + } + + state.cancel(request_id) +} + +fn list_file_history_core( + repo: &Path, + file: String, + limit: Option, + cancellation: Option<&SearchCancellation>, +) -> Result, String> { + check_search_cancelled(cancellation)?; validate_files(std::slice::from_ref(&file))?; - if verify_commit(&repo, "HEAD").is_err() { + if verify_commit(repo, "HEAD").is_err() { return Ok(Vec::new()); } @@ -806,13 +867,14 @@ pub fn list_file_history( OsString::from("-n"), OsString::from(limit), ]; - if !is_repository_folder_path(&repo, &file)? { + if !is_repository_folder_path(repo, &file)? { args.push(OsString::from("--follow")); } args.extend([OsString::from("--"), OsString::from(file)]); - let output = run_git(&repo, args)?; + let output = run_git_cancellable(repo, args, cancellation, "Git-Dateihistorie fehlgeschlagen")?; + check_search_cancelled(cancellation)?; - parse_commit_log(&repo, &output) + parse_commit_log(repo, &output) } #[tauri::command] @@ -1416,6 +1478,69 @@ fn open_path_in_file_manager(path: &Path) -> Result<(), String> { Ok(()) } +fn resolve_repo_child_path(repo: &Path, child: &str) -> Result { + let child_path = Path::new(child); + if child_path.is_absolute() + || child_path.components().any(|component| { + matches!( + component, + std::path::Component::ParentDir + | std::path::Component::RootDir + | std::path::Component::Prefix(_) + ) + }) + { + return Err("Dateipfad muss innerhalb des Repositorys liegen.".to_string()); + } + + let candidate = repo.join(child_path); + let repo = repo + .canonicalize() + .map_err(|err| format!("Repository-Pfad konnte nicht aufgeloest werden: {err}"))?; + let candidate = candidate + .canonicalize() + .map_err(|err| format!("Dateipfad konnte nicht aufgeloest werden: {err}"))?; + + if !candidate.starts_with(&repo) { + return Err("Dateipfad liegt ausserhalb des Repositorys.".to_string()); + } + + Ok(candidate) +} + +#[cfg(windows)] +fn reveal_path_in_file_manager(path: &Path) -> Result<(), String> { + let native_path = path.to_string_lossy().replace('/', "\\"); + let mut command = Command::new("explorer.exe"); + command.arg(format!("/select,{native_path}")); + command.creation_flags(CREATE_NO_WINDOW); + command + .spawn() + .map_err(|err| format!("Explorer konnte nicht gestartet werden: {err}"))?; + Ok(()) +} + +#[cfg(target_os = "macos")] +fn reveal_path_in_file_manager(path: &Path) -> Result<(), String> { + Command::new("open") + .arg("-R") + .arg(path) + .spawn() + .map_err(|err| format!("Finder konnte nicht gestartet werden: {err}"))?; + Ok(()) +} + +#[cfg(all(unix, not(target_os = "macos")))] +fn reveal_path_in_file_manager(path: &Path) -> Result<(), String> { + // No universal "select this file" flag across Linux file managers; open its folder instead. + let target = path.parent().unwrap_or(path); + Command::new("xdg-open") + .arg(target) + .spawn() + .map_err(|err| format!("Dateimanager konnte nicht gestartet werden: {err}"))?; + Ok(()) +} + fn verify_commit(repo: &Path, commit: &str) -> Result { let commit = commit.trim(); if commit.is_empty() { @@ -1518,7 +1643,9 @@ fn detect_worktree_renames(repo: &Path, files: &mut Vec) { for (old_path, new_path) in renames { files.retain(|f| { - !(f.path == old_path && f.staged.is_none() && f.unstaged == Some(FileStatusKind::Deleted)) + !(f.path == old_path + && f.staged.is_none() + && f.unstaged == Some(FileStatusKind::Deleted)) && !(f.path == new_path && f.staged.is_none() && f.unstaged == Some(FileStatusKind::Untracked)) @@ -1573,11 +1700,7 @@ fn worktree_blob_hashes(repo: &Path, paths: &[String]) -> Result Result, String> { @@ -4052,12 +4175,8 @@ mod tests { run_git_test(&repo.path, ["add", "other.txt"]); run_git_test(&repo.path, ["commit", "-q", "-m", "touch other"]); - let commits = list_file_history( - repo.path.to_string_lossy().to_string(), - "old.txt".to_string(), - Some(10), - ) - .unwrap(); + let commits = + list_file_history_core(&repo.path, "old.txt".to_string(), Some(10), None).unwrap(); assert_eq!(commits.len(), 2); assert_eq!(commits[0].summary, "touch selected"); @@ -4080,15 +4199,34 @@ mod tests { run_git_test(&repo.path, ["add", "."]); run_git_test(&repo.path, ["commit", "-q", "-m", "src update"]); - let commits = list_file_history( - repo.path.to_string_lossy().to_string(), - "src".to_string(), - Some(10), - ) - .unwrap(); + let commits = + list_file_history_core(&repo.path, "src".to_string(), Some(10), None).unwrap(); assert_eq!(commits.len(), 2); assert_eq!(commits[0].summary, "src update"); assert_eq!(commits[1].summary, "src initial"); } + + #[test] + fn list_file_history_can_be_cancelled() { + let repo = init_temp_repo("file_history_cancelled"); + commit_initial_file(&repo.path); + + let state = SearchCancellationState::default(); + state + .cancel("file-history-test") + .expect("cancel flag should be set"); + + let result = list_file_history_core( + &repo.path, + "old.txt".to_string(), + Some(10), + Some(&SearchCancellation { + state, + search_id: "file-history-test".to_string(), + }), + ); + + assert_eq!(result.unwrap_err(), SEARCH_CANCELLED_MESSAGE); + } } diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 7db963e..20145bb 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -3,13 +3,14 @@ mod git; use git::{ - apply_file_patch, cancel_code_search, checkout_branch, commit, compare_commits, - compare_file_to_head, compare_file_to_parent, create_branch, cred_delete, cred_load, cred_save, - delete_branch, diff_file_against_working_tree, get_file_patch, get_remote_url, get_status, - list_branches, list_commits, list_file_history, list_repository_files, merge_branch, - open_repo_in_explorer, open_repository, open_repository_bundle, pull, push, read_conflict, - rename_branch, resolve_conflict, resolve_conflict_side, restore_file_from_commit, - restore_files, restore_to_commit, search_code_introductions, stage_files, unstage_files, + apply_file_patch, cancel_code_search, cancel_file_history, checkout_branch, commit, + compare_commits, compare_file_to_head, compare_file_to_parent, create_branch, cred_delete, + cred_load, cred_save, delete_branch, diff_file_against_working_tree, get_file_patch, + get_remote_url, get_status, list_branches, list_commits, list_file_history, + list_repository_files, merge_branch, open_repo_in_explorer, open_repository, + open_repository_bundle, open_repository_file, pull, push, read_conflict, rename_branch, + resolve_conflict, resolve_conflict_side, restore_file_from_commit, restore_files, + restore_to_commit, search_code_introductions, stage_files, unstage_files, SearchCancellationState, }; @@ -21,6 +22,7 @@ fn main() { .invoke_handler(tauri::generate_handler![ open_repository, open_repo_in_explorer, + open_repository_file, get_status, list_branches, checkout_branch, @@ -42,6 +44,7 @@ fn main() { list_repository_files, open_repository_bundle, list_file_history, + cancel_file_history, compare_commits, compare_file_to_head, compare_file_to_parent, diff --git a/src/App.svelte b/src/App.svelte index 253fcc3..8b3efd2 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -27,6 +27,7 @@ commit, compareCommits, cancelCodeSearch, + cancelFileHistory, applyFilePatch, createBranch, deleteBranch, @@ -39,6 +40,7 @@ listRepositoryFiles, mergeBranch, openRepoInExplorer, + openRepositoryFile, openRepositoryBundle, pull, push, @@ -119,6 +121,7 @@ let fileHistory: GitCommit[] = []; let fileHistoryLoading = false; let fileHistoryRequestId = 0; + let activeFileHistoryRequestId = ""; let commitMessage = ""; let errorMessage = ""; let operation = ""; @@ -207,6 +210,7 @@ onDestroy(() => { if (autoRefreshTimer) clearInterval(autoRefreshTimer); + if (activeFileHistoryRequestId) void cancelFileHistory(activeFileHistoryRequestId); }); // ── Auto-refresh ─────────────────────────────────────────────────────────── @@ -534,14 +538,56 @@ if (selectedExplorerPath && !explorerPathExists(repoFiles, selectedExplorerPath)) { selectedExplorerPath = ""; selectedExplorerKind = "file"; + cancelActiveFileHistoryLoad(); + activeFileHistoryRequestId = ""; + fileHistoryLoading = false; fileHistory = []; } } + function isCancellationMessage(message: string): boolean { + return message.toLowerCase().includes("abgebrochen"); + } + + function cancelActiveFileHistoryLoad() { + const requestId = activeFileHistoryRequestId; + if (!requestId) return; + void cancelFileHistory(requestId).catch(() => {}); + } + async function refreshFileHistory(path = activeRepoPath, file = selectedExplorerPath) { const requestId = ++fileHistoryRequestId; - const history = file ? await listFileHistory(path, file, 100) : []; - if (requestId === fileHistoryRequestId) fileHistory = history; + cancelActiveFileHistoryLoad(); + + if (!path || !file) { + activeFileHistoryRequestId = ""; + fileHistoryLoading = false; + fileHistory = []; + return; + } + + const historyRequestId = `file-history-${requestId}-${Date.now()}`; + activeFileHistoryRequestId = historyRequestId; + fileHistoryLoading = true; + fileHistory = []; + + try { + const history = await listFileHistory(path, file, 100, historyRequestId); + if (requestId === fileHistoryRequestId && activeFileHistoryRequestId === historyRequestId) { + fileHistory = history; + } + } catch (error) { + const message = errorToMessage(error); + if (requestId === fileHistoryRequestId && activeFileHistoryRequestId === historyRequestId) { + fileHistory = []; + if (!isCancellationMessage(message)) errorMessage = message; + } + } finally { + if (requestId === fileHistoryRequestId && activeFileHistoryRequestId === historyRequestId) { + activeFileHistoryRequestId = ""; + fileHistoryLoading = false; + } + } } // ── Repository operations ────────────────────────────────────────────────── @@ -1120,26 +1166,14 @@ // (isBusy/runOperation would disable every button in the app while this awaits). // A request id guards against a slower, stale request overwriting a newer selection. async function loadSelectedFileHistory(path: string, repo = activeRepoPath) { - const requestId = ++fileHistoryRequestId; - fileHistoryLoading = true; - try { - const history = await listFileHistory(repo, path, 100); - if (requestId === fileHistoryRequestId) fileHistory = history; - } catch (error) { - if (requestId === fileHistoryRequestId) { - fileHistory = []; - errorMessage = errorToMessage(error); - } - } finally { - if (requestId === fileHistoryRequestId) fileHistoryLoading = false; - } + await refreshFileHistory(repo, path); } async function selectExplorerNode(node: ExplorerNode) { if (!activeRepoPath || (selectedExplorerPath === node.path && selectedExplorerKind === node.kind)) return; selectedExplorerPath = node.path; selectedExplorerKind = node.kind; - await loadSelectedFileHistory(node.path); + void loadSelectedFileHistory(node.path); } async function selectFileFromSearch(file: GitRepositoryFile) { @@ -1148,7 +1182,29 @@ selectedExplorerKind = "file"; expandedExplorerPaths = new Set([...expandedExplorerPaths, ...explorerParentFolders(file.path)]); - await loadSelectedFileHistory(file.path); + void loadSelectedFileHistory(file.path); + } + + function selectFileFromStatus(file: GitFileStatus) { + if (!activeRepoPath) return; + selectedExplorerPath = file.path; + selectedExplorerKind = "file"; + expandedExplorerPaths = new Set([...expandedExplorerPaths, ...explorerParentFolders(file.path)]); + + void loadSelectedFileHistory(file.path); + } + + async function openFileFromExplorer(node: ExplorerNode) { + if (!activeRepoPath || node.kind !== "file") return; + selectedExplorerPath = node.path; + selectedExplorerKind = "file"; + void loadSelectedFileHistory(node.path); + + try { + await openRepositoryFile(activeRepoPath, node.path); + } catch (error) { + errorMessage = errorToMessage(error); + } } async function restoreSelectedFileFromCommit(target: GitCommit) { @@ -1590,6 +1646,7 @@ onExpandAllFolders={expandAllExplorerFolders} onCollapseAllFolders={collapseAllExplorerFolders} onSelectNode={selectExplorerNode} + onOpenFile={openFileFromExplorer} /> @@ -1618,6 +1675,8 @@ {hasRepository} {isBusy} {status} + selectedFilePath={selectedExplorerPath} + onSelectFile={selectFileFromStatus} onStage={stageFile} onUnstage={unstageFile} onDiscard={discardFile} diff --git a/src/app.css b/src/app.css index 9abef6b..9560379 100644 --- a/src/app.css +++ b/src/app.css @@ -12,6 +12,7 @@ --color-surface-dim: rgba(18, 18, 30, 0.9); --color-surface-hover: rgba(47, 48, 78, 0.76); --color-surface-raised: rgba(28, 29, 48, 0.88); + --color-surface-solid: #1c1d30; --color-border: rgba(100, 108, 255, 0.28); --color-border-subtle: rgba(255, 255, 255, 0.08); @@ -274,6 +275,15 @@ } .titlebar-brand svg { color: #ffd343; filter: drop-shadow(0 0 10px rgba(255,211,67,0.3)); flex-shrink: 0; } + .tb-version { + margin-left: 1px; + color: rgba(255,255,255,0.32); + font-size: 10px; + font-weight: 600; + font-family: var(--font-mono); + letter-spacing: 0.01em; + } + .titlebar-info { display: flex; align-items: center; @@ -943,6 +953,22 @@ .file-row { display: grid; gap: 8px; padding: 10px; border: 1px solid var(--color-border-subtle); border-radius: 8px; background: var(--color-surface-raised); } .file-row + .file-row { margin-top: 6px; } + .file-row.selected { border-color: rgba(90,140,248,0.36); background: rgba(90,140,248,0.1); } + + .file-title-button { + justify-content: flex-start; + width: 100%; + min-height: 24px; + padding: 0; + border: 0; + background: transparent; + color: inherit; + text-align: left; + } + .file-title-button:hover:not(:disabled) { + background: transparent; + color: var(--color-accent); + } .file-title strong { display: block; @@ -1175,8 +1201,8 @@ .branch-actions { display: flex; align-items: center; justify-content: flex-end; gap: 5px; } - .branch-context-menu { - position: absolute; + .branch-context-menu, + .explorer-context-menu { z-index: 120; display: grid; gap: 2px; @@ -1184,11 +1210,15 @@ padding: 5px; border: 1px solid var(--color-border); border-radius: 8px; - background: var(--color-surface); - box-shadow: 0 18px 50px rgba(0,0,0,0.35); + background: var(--color-surface-solid); + box-shadow: 0 18px 50px rgba(0,0,0,0.5); } - .branch-context-menu button { + .branch-context-menu { position: absolute; } + .explorer-context-menu { position: fixed; } + + .branch-context-menu button, + .explorer-context-menu button { display: flex; align-items: center; justify-content: flex-start; @@ -1205,7 +1235,8 @@ text-align: left; } - .branch-context-menu button:hover:not(:disabled) { + .branch-context-menu button:hover:not(:disabled), + .explorer-context-menu button:hover:not(:disabled) { border-color: var(--color-border-subtle); background: rgba(255,255,255,0.06); color: var(--color-ink); @@ -1221,13 +1252,16 @@ color: #ffd0d6; } - .branch-context-menu button:disabled { + .branch-context-menu button:disabled, + .explorer-context-menu button:disabled { cursor: not-allowed; opacity: 0.48; } /* --- Explorer --- */ + .explorer-panel { position: relative; } + .explorer-head-actions { display: flex; align-items: center; gap: 5px; flex: 0 0 auto; } .explorer-bulk-button { width: 26px; diff --git a/src/lib/TitleBar.svelte b/src/lib/TitleBar.svelte index 28df81b..2e6cea7 100644 --- a/src/lib/TitleBar.svelte +++ b/src/lib/TitleBar.svelte @@ -1,5 +1,6 @@ - +
diff --git a/src/lib/components/ExplorerPanel.svelte b/src/lib/components/ExplorerPanel.svelte index b801204..66db7f9 100644 --- a/src/lib/components/ExplorerPanel.svelte +++ b/src/lib/components/ExplorerPanel.svelte @@ -17,6 +17,7 @@ FileVideo, Folder, FolderOpen, + ExternalLink, Terminal, } from "@lucide/svelte"; import { languageIconForPath } from "../languageIcons"; @@ -34,6 +35,7 @@ onExpandAllFolders: () => void; onCollapseAllFolders: () => void; onSelectNode: (node: ExplorerNode) => void; + onOpenFile: (node: ExplorerNode) => void; } let { @@ -47,8 +49,13 @@ onExpandAllFolders = () => {}, onCollapseAllFolders = () => {}, onSelectNode = () => {}, + onOpenFile = () => {}, }: Props = $props(); + let contextNode = $state(null); + let contextMenuX = $state(0); + let contextMenuY = $state(0); + function mergeExplorerStatus(current: FileStatusKind | null, next: FileStatusKind | null): FileStatusKind | null { if (!next) return current; if (!current) return next; @@ -154,12 +161,39 @@ return "text"; } + function openFileContextMenu(event: MouseEvent, node: ExplorerNode) { + if (node.kind !== "file") return; + event.preventDefault(); + event.stopPropagation(); + + contextNode = node; + contextMenuX = Math.max(8, Math.min(event.clientX + 2, window.innerWidth - 192)); + contextMenuY = Math.max(8, Math.min(event.clientY + 2, window.innerHeight - 56)); + } + + function closeFileContextMenu() { + contextNode = null; + } + + function openContextFile() { + const node = contextNode; + if (!node || node.kind !== "file") return; + closeFileContextMenu(); + onOpenFile(node); + } + + function handleWindowKeydown(event: KeyboardEvent) { + if (event.key === "Escape") closeFileContextMenu(); + } + let explorerTree = $derived(buildExplorerTree(repoFiles)); let visibleNodes = $derived(flattenExplorerTree(explorerTree, expandedExplorerPaths)); let hasFolders = $derived(explorerTree.some((node) => node.kind === "folder")); -
+ + +
Explorer @@ -264,6 +298,7 @@ class="explorer-select" type="button" onclick={() => onSelectNode(node)} + oncontextmenu={(event) => openFileContextMenu(event, node)} disabled={isBusy} title={`Show history for ${node.path}`} > @@ -279,4 +314,26 @@ {/each}
{/if} +
+ +{#if contextNode} + +{/if} diff --git a/src/lib/components/StatusPanel.svelte b/src/lib/components/StatusPanel.svelte index fa4396f..d692be5 100644 --- a/src/lib/components/StatusPanel.svelte +++ b/src/lib/components/StatusPanel.svelte @@ -9,6 +9,8 @@ hasRepository: boolean; isBusy: boolean; status: GitStatus | null; + selectedFilePath: string; + onSelectFile: (file: GitFileStatus) => void; onStage: (file: GitFileStatus) => void; onUnstage: (file: GitFileStatus) => void; onDiscard: (file: GitFileStatus, staged: boolean) => void; @@ -24,6 +26,8 @@ hasRepository = false, isBusy = false, status = null, + selectedFilePath = "", + onSelectFile = () => {}, onStage = () => {}, onUnstage = () => {}, onDiscard = () => {}, @@ -102,9 +106,19 @@ {:else}
{#each changedFiles as file (`${file.old_path ?? ""}:${file.path}`)} -
+
- {fileName(file)} +
diff --git a/src/lib/git.ts b/src/lib/git.ts index 7f32cc3..870919b 100644 --- a/src/lib/git.ts +++ b/src/lib/git.ts @@ -21,6 +21,10 @@ export function openRepoInExplorer(path: string): Promise { return invoke("open_repo_in_explorer", { path }); } +export function openRepositoryFile(path: string, file: string): Promise { + return invoke("open_repository_file", { path, file }); +} + export function openRepositoryBundle(path: string, commitLimit = 100): Promise { return invoke("open_repository_bundle", { path, commitLimit }); } @@ -143,8 +147,17 @@ export function listRepositoryFiles(path: string): Promise return invoke("list_repository_files", { path }); } -export function listFileHistory(path: string, file: string, limit = 100): Promise { - return invoke("list_file_history", { path, file, limit }); +export function listFileHistory( + path: string, + file: string, + limit = 100, + requestId?: string, +): Promise { + return invoke("list_file_history", { path, file, limit, requestId: requestId ?? null }); +} + +export function cancelFileHistory(requestId: string): Promise { + return invoke("cancel_file_history", { requestId }); } export function compareCommits( -- 2.54.0