Introduce a split-planning path for staged changes that asks supported AI providers to group files into ordered Conventional Commit messages. The plan is validated before execution so every staged file is assigned once and unsafe states are rejected. A new dialog lets users review and adjust the proposed groups before creating the commits in sequence, with safeguards to preserve remaining changes if something fails.
169 lines
6.1 KiB
Svelte
169 lines
6.1 KiB
Svelte
<script lang="ts">
|
|
import { Check, GitCommitHorizontal, 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;
|
|
commitAiSplitting: boolean;
|
|
canAmend: boolean;
|
|
amendMode: boolean;
|
|
onCommit: () => void;
|
|
onCommitMessageChange: (msg: string) => void;
|
|
onGenerateCommitMessage: () => void;
|
|
onReviewStaged: () => void;
|
|
onSplitStaged: () => 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,
|
|
commitAiSplitting = false,
|
|
canAmend = false,
|
|
amendMode = false,
|
|
onCommit = () => {},
|
|
onCommitMessageChange = () => {},
|
|
onGenerateCommitMessage = () => {},
|
|
onReviewStaged = () => {},
|
|
onSplitStaged = () => {},
|
|
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");
|
|
let canSplit = $derived(canReview && stagedCount > 1 && !commitAiSplitting);
|
|
</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={onSplitStaged}
|
|
disabled={!canSplit}
|
|
title={commitAiProvider === "local" ? "Commit splitting currently requires an API provider" : "Suggest logical commits for the staged files"}
|
|
>
|
|
{#if commitAiSplitting}<LoaderCircle class="spin" size={14} aria-hidden="true" />{:else}<GitCommitHorizontal size={14} aria-hidden="true" />{/if}
|
|
Split
|
|
</button>
|
|
<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>
|