From 9c371ec5205faa23acd925e463f9a40ec07e75cc Mon Sep 17 00:00:00 2001 From: Christoph Brandau Date: Fri, 3 Jul 2026 23:35:46 +0200 Subject: [PATCH] feat(history-ui): add resizable commit history aside The commit history panel is now resizable with a draggable handle and keyboard support. The chosen width is persisted in local storage so the layout stays consistent across sessions. Git commit loading was also adjusted to include all branch tips for a more complete graph. - Add width persistence and pointer/keyboard resizing for history panel - Update commit graph query to use topo-order across all refs - Extend tests to ensure branch tips are included in commit results --- src-tauri/src/git.rs | 29 ++ src/App.svelte | 82 ++++- src/app.css | 463 ++++++++++++++++++++----- src/lib/components/HistoryPanel.svelte | 347 ++++++++++++++++-- 4 files changed, 821 insertions(+), 100 deletions(-) diff --git a/src-tauri/src/git.rs b/src-tauri/src/git.rs index 2e2486c..b14c74a 100644 --- a/src-tauri/src/git.rs +++ b/src-tauri/src/git.rs @@ -996,6 +996,8 @@ fn commits_for_repo(repo: &Path, limit: Option) -> Result, S repo, [ "log", + "--all", + "--topo-order", "--decorate=short", "--name-status", "-M", @@ -3869,6 +3871,33 @@ mod tests { assert!(comparison.patch.contains("+original")); } + #[test] + fn commits_for_repo_includes_all_branch_tips_for_graph() { + let repo = init_temp_repo("commits_all_branches"); + commit_initial_file(&repo.path); + let base_branch = git_output_test(&repo.path, ["rev-parse", "--abbrev-ref", "HEAD"]); + + run_git_test(&repo.path, ["checkout", "-q", "-b", "feature/graph"]); + fs::write(repo.path.join("feature.txt"), "feature\n") + .expect("feature file should be written"); + run_git_test(&repo.path, ["add", "feature.txt"]); + run_git_test(&repo.path, ["commit", "-q", "-m", "feature graph"]); + let feature_commit = git_output_test(&repo.path, ["rev-parse", "HEAD"]); + + run_git_test(&repo.path, ["checkout", "-q", base_branch.as_str()]); + fs::write(repo.path.join("main.txt"), "main\n").expect("main file should be written"); + run_git_test(&repo.path, ["add", "main.txt"]); + run_git_test(&repo.path, ["commit", "-q", "-m", "main graph"]); + let main_commit = git_output_test(&repo.path, ["rev-parse", "HEAD"]); + + let commits = commits_for_repo(&repo.path, Some(10)).expect("commits should load"); + + assert!(commits.iter().any(|commit| commit.hash == main_commit)); + assert!(commits.iter().any(|commit| { + commit.hash == feature_commit && commit.refs.iter().any(|r| r.contains("feature/graph")) + })); + } + #[test] #[cfg_attr( windows, diff --git a/src/App.svelte b/src/App.svelte index 206f403..ef27d3f 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -117,9 +117,13 @@ const RECENT_REPOS_KEY = "gitlite.recentRepos.v1"; const AI_SETTINGS_KEY = "gitlite.aiSettings.v1"; const COMMIT_PANEL_HEIGHT_KEY = "gitlite.commitPanelHeight.v1"; + const HISTORY_ASIDE_WIDTH_KEY = "gitlite.historyAsideWidth.v1"; const COMMIT_PANEL_DEFAULT_HEIGHT = 220; const COMMIT_PANEL_MIN_HEIGHT = COMMIT_PANEL_DEFAULT_HEIGHT; const COMMIT_PANEL_MAX_HEIGHT = 640; + const HISTORY_ASIDE_DEFAULT_WIDTH = 620; + const HISTORY_ASIDE_MIN_WIDTH = 560; + const HISTORY_ASIDE_MAX_WIDTH = 920; // ── State ────────────────────────────────────────────────────────────────── @@ -207,6 +211,10 @@ let resizingCommitPanel = false; let resizeStartY = 0; let resizeStartHeight = 0; + let historyAsideWidth = loadHistoryAsideWidth(); + let resizingHistoryAside = false; + let historyResizeStartX = 0; + let historyResizeStartWidth = 0; // ── Derived ──────────────────────────────────────────────────────────────── @@ -226,6 +234,7 @@ : ""; $: canCompare = hasRepository && compareFrom.length > 0 && compareTo.length > 0 && compareFrom !== compareTo && !isBusy; $: localBranches = branches.filter((b) => !b.remote); + $: localBranchNames = localBranches.map((b) => b.name); $: remoteBranches = branches.filter((b) => b.remote); $: repoSearchTerm = repoSearch.trim().toLowerCase(); $: openRepoRows = repoTabs.filter(repoMatchesSearch); @@ -625,6 +634,28 @@ } } + function clampHistoryAsideWidth(value: number): number { + return Math.min(HISTORY_ASIDE_MAX_WIDTH, Math.max(HISTORY_ASIDE_MIN_WIDTH, Math.round(value))); + } + + function loadHistoryAsideWidth(): number { + try { + const stored = Number(localStorage.getItem(HISTORY_ASIDE_WIDTH_KEY)); + if (Number.isFinite(stored) && stored > 0) return clampHistoryAsideWidth(stored); + } catch { + // Fall through to the default below. + } + return HISTORY_ASIDE_DEFAULT_WIDTH; + } + + function persistHistoryAsideWidth(value: number) { + try { + localStorage.setItem(HISTORY_ASIDE_WIDTH_KEY, String(value)); + } catch { + // Local storage is best-effort only; resizing must keep working without it. + } + } + function startCommitPanelResize(event: PointerEvent) { event.preventDefault(); resizingCommitPanel = true; @@ -653,6 +684,34 @@ persistCommitPanelHeight(commitPanelHeight); } + function startHistoryAsideResize(event: PointerEvent) { + event.preventDefault(); + resizingHistoryAside = true; + historyResizeStartX = event.clientX; + historyResizeStartWidth = historyAsideWidth; + (event.currentTarget as HTMLElement).setPointerCapture(event.pointerId); + } + + function onHistoryAsideResizeMove(event: PointerEvent) { + if (!resizingHistoryAside) return; + historyAsideWidth = clampHistoryAsideWidth(historyResizeStartWidth + (historyResizeStartX - event.clientX)); + } + + function endHistoryAsideResize(event: PointerEvent) { + if (!resizingHistoryAside) return; + resizingHistoryAside = false; + persistHistoryAsideWidth(historyAsideWidth); + const target = event.currentTarget as HTMLElement; + if (target.hasPointerCapture(event.pointerId)) target.releasePointerCapture(event.pointerId); + } + + function onHistoryAsideResizeKeydown(event: KeyboardEvent) { + if (event.key !== "ArrowLeft" && event.key !== "ArrowRight") return; + event.preventDefault(); + historyAsideWidth = clampHistoryAsideWidth(historyAsideWidth + (event.key === "ArrowLeft" ? 24 : -24)); + persistHistoryAsideWidth(historyAsideWidth); + } + function rememberRecentRepo(path: string) { recentRepoPaths = uniqueRepoPaths([path, ...recentRepoPaths]).slice(0, 40); persistRepoLists(); @@ -1945,7 +2004,7 @@ {:else} -
+