Introduces a robust system for running automated, staged diff reviews against various large language models. This feature allows users to submit their changes to external AI services and receive structured feedback on potential bugs, security issues, and maintainability risks before committing. The implementation covers the entire stack: * Backend logic was added to handle API communication with OpenAI, Anthropic, and custom endpoints. * A dedicated parser ensures that complex JSON outputs from LLMs are reliably converted into structured findings (severity, title, description). * New components and UI elements provide a clear visualization of the AI's assessment and actionable suggestions. - Supports multiple major LLM providers (OpenAI, Anthropic) - Parses structured JSON output for consistent review results - Adds dedicated UI dialog to display AI findings and risk level
154 lines
5.4 KiB
Svelte
154 lines
5.4 KiB
Svelte
<script lang="ts">
|
|
import { Check, LoaderCircle, RotateCcw, Settings, ShieldCheck, Sparkles } from "@lucide/svelte";
|
|
import type { CommitAiPhase, CommitAiProvider } from "../types";
|
|
|
|
interface Props {
|
|
commitMessage: string;
|
|
canCommit: boolean;
|
|
commitBlockReason: string;
|
|
hasRepository: boolean;
|
|
isBusy: boolean;
|
|
operation: string;
|
|
stagedCount: number;
|
|
commitAiProvider: CommitAiProvider;
|
|
commitAiPhase: CommitAiPhase;
|
|
commitAiGenerating: boolean;
|
|
commitAiReviewing: boolean;
|
|
canAmend: boolean;
|
|
amendMode: boolean;
|
|
onCommit: () => void;
|
|
onCommitMessageChange: (msg: string) => void;
|
|
onGenerateCommitMessage: () => void;
|
|
onReviewStaged: () => void;
|
|
onOpenAiSettings: () => void;
|
|
onToggleAmend: (checked: boolean) => void;
|
|
onUndoLastCommit: () => void;
|
|
}
|
|
|
|
let {
|
|
commitMessage = "",
|
|
canCommit = false,
|
|
commitBlockReason = "",
|
|
hasRepository = false,
|
|
isBusy = false,
|
|
operation = "",
|
|
stagedCount = 0,
|
|
commitAiProvider = "local",
|
|
commitAiPhase = "idle",
|
|
commitAiGenerating = false,
|
|
commitAiReviewing = false,
|
|
canAmend = false,
|
|
amendMode = false,
|
|
onCommit = () => {},
|
|
onCommitMessageChange = () => {},
|
|
onGenerateCommitMessage = () => {},
|
|
onReviewStaged = () => {},
|
|
onOpenAiSettings = () => {},
|
|
onToggleAmend = () => {},
|
|
onUndoLastCommit = () => {},
|
|
}: Props = $props();
|
|
|
|
function handleSubmit(event: SubmitEvent) {
|
|
event.preventDefault();
|
|
onCommit();
|
|
}
|
|
|
|
function aiButtonTitle(provider: CommitAiProvider, phase: CommitAiPhase, staged: number): string {
|
|
if (staged === 0) return "Stage changes first";
|
|
if (provider === "local" && phase === "loading") return "AI model is downloading/loading — this happens once";
|
|
if (provider === "local" && phase === "error") return "AI model failed to load — check AI settings";
|
|
return "Generate commit message with AI from the staged diff";
|
|
}
|
|
|
|
let localModelLoading = $derived(commitAiProvider === "local" && commitAiPhase === "loading");
|
|
let canGenerate = $derived(
|
|
hasRepository &&
|
|
!isBusy &&
|
|
!commitAiGenerating &&
|
|
!commitAiReviewing &&
|
|
stagedCount > 0 &&
|
|
(commitAiProvider !== "local" || commitAiPhase === "ready"),
|
|
);
|
|
let canReview = $derived(canGenerate && commitAiProvider !== "local");
|
|
</script>
|
|
|
|
<section class="panel commit-panel" aria-label="Commit">
|
|
<div class="section-head">
|
|
<div>
|
|
<span class="eyebrow">Create revision</span>
|
|
<h2 class="mt-0.5 text-ink text-base font-bold leading-tight">Commit</h2>
|
|
</div>
|
|
<div class="commit-head-actions">
|
|
<span class="pill pill-count">{stagedCount} staged</span>
|
|
<button
|
|
class="commit-review-button"
|
|
type="button"
|
|
onclick={onReviewStaged}
|
|
disabled={!canReview}
|
|
title={commitAiProvider === "local" ? "Pre-commit review currently requires an API provider" : stagedCount === 0 ? "Stage changes first" : "Review staged changes for bugs and risks"}
|
|
>
|
|
{#if commitAiReviewing}<LoaderCircle class="spin" size={14} aria-hidden="true" />{:else}<ShieldCheck size={14} aria-hidden="true" />{/if}
|
|
Review
|
|
</button>
|
|
<button
|
|
class="commit-generate-button"
|
|
type="button"
|
|
onclick={onGenerateCommitMessage}
|
|
disabled={!canGenerate}
|
|
title={aiButtonTitle(commitAiProvider, commitAiPhase, stagedCount)}
|
|
>
|
|
{#if commitAiGenerating || localModelLoading}<LoaderCircle class="spin" size={14} aria-hidden="true" />{:else}<Sparkles size={14} aria-hidden="true" />{/if}
|
|
Generate
|
|
</button>
|
|
<button class="commit-settings-button" type="button" onclick={onOpenAiSettings} disabled={isBusy} title="AI settings" aria-label="AI settings">
|
|
<Settings size={14} aria-hidden="true" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<form class="commit-form" onsubmit={handleSubmit}>
|
|
<textarea
|
|
value={commitMessage}
|
|
oninput={(e) => onCommitMessageChange((e.target as HTMLTextAreaElement).value)}
|
|
placeholder="Commit message"
|
|
disabled={!hasRepository || isBusy}
|
|
></textarea>
|
|
{#if commitBlockReason}
|
|
<p class="commit-block-reason">{commitBlockReason}</p>
|
|
{/if}
|
|
{#if canAmend}
|
|
<div class="commit-amend-row">
|
|
<label class="commit-amend-toggle">
|
|
<input
|
|
type="checkbox"
|
|
checked={amendMode}
|
|
disabled={isBusy}
|
|
onchange={(e) => onToggleAmend((e.target as HTMLInputElement).checked)}
|
|
/>
|
|
Amend last commit
|
|
</label>
|
|
<button
|
|
class="btn-secondary commit-undo-button"
|
|
type="button"
|
|
onclick={onUndoLastCommit}
|
|
disabled={isBusy}
|
|
title="Undo the last commit — its changes come back as uncommitted changes (not pushed yet, so safe)"
|
|
>
|
|
<RotateCcw size={13} aria-hidden="true" />
|
|
Undo last commit
|
|
</button>
|
|
</div>
|
|
{/if}
|
|
<div class="commit-actions-row flex-shrink-0">
|
|
<button class="btn-primary flex-1" type="submit" disabled={!canCommit} title={commitBlockReason || (amendMode ? "Amend the last commit" : "Commit staged changes")}>
|
|
{#if operation === "Committing" || operation === "Amending"}
|
|
<LoaderCircle class="spin" size={16} aria-hidden="true" />
|
|
{:else}
|
|
<Check size={16} aria-hidden="true" />
|
|
{/if}
|
|
{amendMode ? "Amend" : "Commit"}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</section>
|