feat(blame): add file blame dialog with git porcelain parsing

This change introduces a new backend command to fetch file blame using
git's line-porcelain output and returns structured per-line metadata.
The UI adds a BlameDialog that groups lines by commit, highlights
uncommitted changes, and styles the dialog to match the updated theme.

- Add get_file_blame command and parsing with uncommitted detection
- Wire BlameDialog into the explorer file node actions
- Add blame UI component and supporting types and styles
This commit is contained in:
Christoph Brandau
2026-07-07 15:09:25 +02:00
parent a67663f3c5
commit d4ac449074
10 changed files with 548 additions and 25 deletions
+47
View File
@@ -7,6 +7,7 @@
import TitleBar from "./lib/TitleBar.svelte";
import AiSettingsDialog from "./lib/components/AiSettingsDialog.svelte";
import BranchDeleteConfirmDialog from "./lib/components/BranchDeleteConfirmDialog.svelte";
import BlameDialog from "./lib/components/BlameDialog.svelte";
import BranchPanel from "./lib/components/BranchPanel.svelte";
import CloneRepositoryDialog from "./lib/components/CloneRepositoryDialog.svelte";
import CommitPanel from "./lib/components/CommitPanel.svelte";
@@ -50,6 +51,7 @@
diffFileAgainstWorkingTree,
compareFileToParent,
fetchRemote,
getFileBlame,
getStatus,
lastCommitMessage,
listBranches,
@@ -97,6 +99,7 @@
ConflictFile,
ExplorerNode,
ExplorerNodeKind,
GitBlameLine,
GitBranch as GitBranchInfo,
GitCommit,
GitCommitFile,
@@ -218,6 +221,11 @@
let linePatchText = "";
let linePatchLoading = false;
let linePatchError = "";
let blameOpen = false;
let blameFilePath = "";
let blameLines: GitBlameLine[] = [];
let blameLoading = false;
let blameError = "";
let pendingDiscard: PendingDiscard | null = null;
let globalSearchOpen = false;
let lastSearchQuery = "";
@@ -1921,6 +1929,33 @@
linePatchError = "";
}
async function openBlame(node: ExplorerNode) {
if (!activeRepoPath || node.kind !== "file") return;
blameOpen = true;
blameFilePath = node.path;
blameLines = [];
blameError = "";
blameLoading = true;
try {
const result = await getFileBlame(activeRepoPath, node.path);
blameLines = result.lines;
} catch (error) {
blameError = errorToMessage(error);
errorMessage = blameError;
} finally {
blameLoading = false;
}
}
function closeBlame() {
if (isBusy) return;
blameOpen = false;
blameFilePath = "";
blameLines = [];
blameError = "";
}
function patchOperationLabel(action: PatchApplyAction, file: GitFileStatus): string {
switch (action) {
case "stage":
@@ -2726,6 +2761,7 @@
onCollapseAllFolders={collapseAllExplorerFolders}
onSelectNode={selectExplorerNode}
onOpenFile={openFileFromExplorer}
onBlame={openBlame}
/>
</aside>
@@ -2886,6 +2922,17 @@
/>
{/if}
{#if blameOpen}
<BlameDialog
filePath={blameFilePath}
lines={blameLines}
{isBusy}
isLoading={blameLoading}
error={blameError}
onClose={closeBlame}
/>
{/if}
{#if pendingDiscard}
<DiscardConfirmDialog
file={pendingDiscard.file}