feat: fix git window and add pretty diff viewer (#178)
Security Scan and Upload / Security & DefectDojo Upload (push) Successful in 58s
CI / Lint & Test (push) Successful in 16m33s
CI / Build Linux (push) Successful in 20m56s
CI / Build Windows (cross-compile) (push) Successful in 31m1s

## Summary

- **Fix git window "Not a git repository" error** — The working directory received from Claude Code is a WSL Linux path (e.g. `/home/naomi/...`), but git commands were being run as native Windows processes with `.current_dir()`. Windows can't resolve WSL paths, causing `git rev-parse --git-dir` to fail. Fixed by routing git commands through `wsl -- git -C <path>` when the working directory starts with `/`.

- **Add syntax highlighting and line numbers to diff view** — Replaced the raw `<pre>` block with a proper `DiffViewer` component featuring:
  - Old/new line number columns with correct tracking across hunks
  - Colour-coded gutter (`+`/`-`) with green/red row backgrounds
  - Syntax highlighting via `highlight.js` using the detected file language, respecting all app themes via `--hljs-*` CSS variables
  - Styled hunk headers and file headers

## New files

- `src/lib/utils/diffParser.ts` — pure diff parsing logic
- `src/lib/utils/diffParser.test.ts` — 30 tests covering all line types, line number tracking, and language detection
- `src/lib/components/DiffViewer.svelte` — the pretty diff viewer component

 This pull request was created with help from Hikari~ 🌸

Reviewed-on: #178
Co-authored-by: Hikari <hikari@nhcarrigan.com>
Co-committed-by: Hikari <hikari@nhcarrigan.com>
This commit was merged in pull request #178.
This commit is contained in:
2026-03-06 09:19:16 -08:00
committed by Naomi Carrigan
parent 9af61a4a29
commit 1ae440659c
5 changed files with 668 additions and 7 deletions
+233
View File
@@ -0,0 +1,233 @@
<script lang="ts">
import hljs from "highlight.js";
import { parseDiff, detectLanguage } from "$lib/utils/diffParser";
export let diffContent: string;
export let filePath: string;
$: lines = diffContent ? parseDiff(diffContent) : [];
$: language = detectLanguage(filePath);
function highlightCode(code: string): string {
if (!code) return "";
try {
return hljs.highlight(code, { language }).value;
} catch {
return hljs.highlightAuto(code).value;
}
}
</script>
{#if lines.length === 0}
<div class="empty-diff">No changes</div>
{:else}
<table class="diff-table">
<tbody>
{#each lines as line, i (i)}
{#if line.type === "file-header"}
<tr class="line-file-header">
<td class="line-num" colspan="2"></td>
<td class="line-gutter"></td>
<td class="line-code">{line.content}</td>
</tr>
{:else if line.type === "hunk-header"}
<tr class="line-hunk-header">
<td class="line-num" colspan="2"></td>
<td class="line-gutter"></td>
<td class="line-code">{line.content}</td>
</tr>
{:else if line.type === "no-newline"}
<tr class="line-no-newline">
<td class="line-num" colspan="2"></td>
<td class="line-gutter"></td>
<td class="line-code">{line.content}</td>
</tr>
{:else}
<tr class="line-{line.type}">
<td class="line-num">{line.oldLineNumber ?? ""}</td>
<td class="line-num">{line.newLineNumber ?? ""}</td>
<td class="line-gutter">
{line.type === "added" ? "+" : line.type === "removed" ? "-" : ""}
</td>
<!-- eslint-disable-next-line svelte/no-at-html-tags -- Syntax highlighting requires @html; content is from trusted git diff output -->
<td class="line-code">{@html highlightCode(line.content)}</td>
</tr>
{/if}
{/each}
</tbody>
</table>
{/if}
<style>
.empty-diff {
padding: 2rem;
text-align: center;
color: var(--text-secondary);
font-family: var(--font-mono);
font-size: 0.85rem;
}
.diff-table {
border-collapse: collapse;
min-width: 100%;
width: max-content;
font-family: var(--font-mono);
font-size: 0.82rem;
line-height: 1.5;
}
.diff-table tr {
border: none;
}
.diff-table td {
padding: 0;
white-space: pre;
vertical-align: top;
border: none;
}
.line-num {
width: 3.5rem;
min-width: 3.5rem;
color: var(--text-secondary);
text-align: right;
user-select: none;
border-right: 1px solid var(--border-color);
opacity: 0.6;
font-size: 0.75rem;
padding: 0 0.4rem;
}
.line-gutter {
width: 1.5rem;
min-width: 1.5rem;
text-align: center;
user-select: none;
font-weight: bold;
padding: 0 0.25rem;
}
.line-code {
padding: 0 0.75rem;
}
/* Added lines */
.line-added {
background: rgba(34, 197, 94, 0.1);
}
.line-added .line-num {
background: rgba(34, 197, 94, 0.08);
color: rgba(34, 197, 94, 0.7);
}
.line-added .line-gutter {
color: #22c55e;
background: rgba(34, 197, 94, 0.18);
}
/* Removed lines */
.line-removed {
background: rgba(239, 68, 68, 0.1);
}
.line-removed .line-num {
background: rgba(239, 68, 68, 0.08);
color: rgba(239, 68, 68, 0.7);
}
.line-removed .line-gutter {
color: #ef4444;
background: rgba(239, 68, 68, 0.18);
}
/* Hunk header */
.line-hunk-header {
background: rgba(99, 102, 241, 0.12);
}
.line-hunk-header .line-code {
color: var(--text-secondary);
font-style: italic;
}
.line-hunk-header .line-gutter {
color: var(--text-secondary);
}
/* File header */
.line-file-header {
background: var(--bg-secondary);
border-bottom: 1px solid var(--border-color);
}
.line-file-header .line-code {
color: var(--text-secondary);
font-weight: 500;
padding: 0.15rem 0.75rem;
}
/* No newline */
.line-no-newline .line-code {
color: var(--text-secondary);
font-style: italic;
}
/* Syntax highlighting — scoped to this component's table */
.diff-table :global(.hljs-keyword),
.diff-table :global(.hljs-selector-tag),
.diff-table :global(.hljs-built_in),
.diff-table :global(.hljs-name) {
color: var(--hljs-keyword);
}
.diff-table :global(.hljs-string),
.diff-table :global(.hljs-attr),
.diff-table :global(.hljs-symbol),
.diff-table :global(.hljs-bullet) {
color: var(--hljs-string);
}
.diff-table :global(.hljs-number),
.diff-table :global(.hljs-literal) {
color: var(--hljs-number);
}
.diff-table :global(.hljs-comment),
.diff-table :global(.hljs-quote) {
color: var(--hljs-comment);
font-style: italic;
}
.diff-table :global(.hljs-function),
.diff-table :global(.hljs-title) {
color: var(--hljs-function);
}
.diff-table :global(.hljs-type),
.diff-table :global(.hljs-class) {
color: var(--hljs-type);
}
.diff-table :global(.hljs-variable),
.diff-table :global(.hljs-template-variable) {
color: var(--hljs-variable);
}
.diff-table :global(.hljs-meta) {
color: var(--hljs-meta);
}
.diff-table :global(.hljs-tag) {
color: var(--hljs-keyword);
}
.diff-table :global(.hljs-attribute) {
color: var(--hljs-function);
}
.diff-table :global(.hljs-params) {
color: var(--text-primary);
}
</style>
+4 -6
View File
@@ -2,6 +2,7 @@
import { invoke } from "@tauri-apps/api/core";
import { onMount, onDestroy } from "svelte";
import { claudeStore } from "$lib/stores/claude";
import DiffViewer from "$lib/components/DiffViewer.svelte";
interface GitFileChange {
path: string;
@@ -600,7 +601,9 @@
<h3>📄 {diffFile}</h3>
<button on:click={() => (showDiff = false)} title="Close"></button>
</div>
<pre class="diff-content">{diffContent || "(No changes)"}</pre>
<div class="diff-content">
<DiffViewer {diffContent} filePath={diffFile ?? ""} />
</div>
</div>
</div>
{/if}
@@ -1096,12 +1099,7 @@
.diff-content {
flex: 1;
overflow: auto;
padding: 1rem;
margin: 0;
font-family: var(--font-mono);
font-size: 0.85rem;
line-height: 1.4;
white-space: pre;
background: var(--bg-primary);
}
</style>