feat(external-tools): add cross-platform external tool discovery

Adds a new external tools subsystem to detect and launch
diff and editor tools across Windows, macOS, and Linux.
It exposes data models for tools, commands, and results to the UI
and serializes them for consumption by the app.

- Implement cross-platform discovery of editors and diff tools
- Expose serialized results to the UI for user selection
- Centralize per-OS known tool lists and overrides
This commit is contained in:
Christoph Brandau
2026-08-13 14:08:02 +02:00
parent 3eb554fee7
commit 15d1f2bfd6
15 changed files with 3721 additions and 125 deletions
+18 -1
View File
@@ -1,5 +1,5 @@
<script lang="ts">
import { Check, LoaderCircle, MousePointer2, X } from "@lucide/svelte";
import { Check, ExternalLink, FileDiff, LoaderCircle, MousePointer2, X } from "@lucide/svelte";
import type { GitFileStatus, PatchApplyAction } from "../types";
type PatchLineKind = "context" | "add" | "delete" | "meta";
@@ -34,9 +34,12 @@
isBusy: boolean;
isLoading: boolean;
error: string;
language?: "en" | "de";
diffName?: string;
onClose: () => void;
onRefresh: () => void | Promise<void>;
onApply: (action: PatchApplyAction, patch: string, scope: "hunk" | "lines") => void | Promise<void>;
onExternalDiff: () => void | Promise<void>;
}
let {
@@ -46,11 +49,16 @@
isBusy = false,
isLoading = false,
error = "",
language = "en",
diffName = "diff tool",
onClose = () => {},
onRefresh = () => {},
onApply = () => {},
onExternalDiff = () => {},
}: Props = $props();
const isGerman = $derived(language === "de");
let parsed = $state<ParsedPatch>({ headerLines: [], hunks: [], binary: false });
let patchScroll = $state<HTMLDivElement | null>(null);
let selectedLineIds = $state<Set<string>>(new Set());
@@ -270,6 +278,15 @@
<p class="dialog-title" title={displayPath}>{displayPath}</p>
</div>
<div class="dialog-header-actions">
<div class="tool-surface-choice" aria-label={isGerman ? "Diff öffnen mit" : "Open diff with"}>
<span>{isGerman ? "Öffnen mit" : "Open with"}</span>
<button class="active" type="button" aria-pressed="true" title={isGerman ? "In Gitty anzeigen" : "Show in Gitty"}>
<FileDiff size={13} aria-hidden="true" />Gitty
</button>
<button type="button" onclick={onExternalDiff} disabled={isBusy || isLoading} title={isGerman ? `In ${diffName} öffnen` : `Open in ${diffName}`}>
<ExternalLink size={13} aria-hidden="true" /><span>{diffName}</span>
</button>
</div>
<button class="btn-sm" type="button" onclick={onRefresh} disabled={isBusy || isLoading}>Refresh</button>
<button class="btn-sm dialog-close" type="button" onclick={onClose} disabled={isBusy} aria-label="Close">
<X size={16} aria-hidden="true" />