Introduce a shared dialog header style (.unified-dialog-header) in app.css and opt dialog components into the new chrome by updating their header markup. Headers now use unified-dialog-icon and unified-dialog-text elements (and import the matching Lucide icons where needed), which standardizes icon placement, title/eyebrow layout, close button styling and responsive behavior. The Command Palette layout was adjusted to include the new header and its grid rows. The CSS is explicitly opt-in (so page/section headers remain unchanged) and includes hover/focus styles and a theme-sensitive close color variable. This commit is a UI refactor only — no API or behavior logic changes.
95 lines
4.4 KiB
Svelte
95 lines
4.4 KiB
Svelte
<script lang="ts">
|
|
import {Bot, AlertTriangle, CircleAlert, FileCode, Info, LoaderCircle, RotateCw, ShieldCheck, X } from "@lucide/svelte";
|
|
import type { AiReviewFinding, AiReviewResult, CommitAiProvider } from "../types";
|
|
|
|
interface Props {
|
|
result: AiReviewResult;
|
|
provider: CommitAiProvider;
|
|
isReviewing: boolean;
|
|
onRerun: () => void;
|
|
onClose: () => void;
|
|
}
|
|
|
|
let { result, provider, isReviewing = false, onRerun, onClose }: Props = $props();
|
|
|
|
function providerLabel(value: CommitAiProvider): string {
|
|
if (value === "openai") return "OpenAI";
|
|
if (value === "anthropic") return "Anthropic";
|
|
return "Custom endpoint";
|
|
}
|
|
|
|
function locationLabel(finding: AiReviewFinding): string {
|
|
if (!finding.file) return "Repository-wide";
|
|
return finding.line ? `${finding.file}:${finding.line}` : finding.file;
|
|
}
|
|
</script>
|
|
|
|
<svelte:window onkeydown={(event) => { if (event.key === "Escape" && !isReviewing) onClose(); }} />
|
|
|
|
<div class="dialog-backdrop" role="presentation">
|
|
<div class="dialog ai-review-dialog" role="dialog" aria-modal="true" aria-label="AI pre-commit review" tabindex="-1">
|
|
<header class="dialog-header ai-review-header unified-dialog-header">
|
|
<span class="unified-dialog-icon" aria-hidden="true"><Bot size={18} /></span>
|
|
<div class="unified-dialog-text">
|
|
<span class="eyebrow">Staged changes</span>
|
|
<h2>AI pre-commit review</h2>
|
|
</div>
|
|
<div class="dialog-header-actions">
|
|
<span class="ai-review-provider">{providerLabel(provider)}</span>
|
|
<button class="dialog-close" type="button" onclick={onClose} disabled={isReviewing} aria-label="Close review"><X size={18} aria-hidden="true" /></button>
|
|
</div>
|
|
</header>
|
|
|
|
<div class="ai-review-summary">
|
|
<div class="ai-review-summary-icon" class:clean={result.findings.length === 0}>
|
|
{#if result.findings.length === 0}<ShieldCheck size={22} aria-hidden="true" />{:else}<AlertTriangle size={22} aria-hidden="true" />{/if}
|
|
</div>
|
|
<div>
|
|
<div class="ai-review-summary-line">
|
|
<strong>{result.findings.length === 0 ? "No actionable issues found" : `${result.findings.length} review ${result.findings.length === 1 ? "finding" : "findings"}`}</strong>
|
|
<span class="ai-review-risk {result.risk}">{result.risk} risk</span>
|
|
</div>
|
|
<p>{result.summary}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="ai-review-findings">
|
|
{#if result.findings.length === 0}
|
|
<div class="ai-review-clean-state">
|
|
<ShieldCheck size={30} aria-hidden="true" />
|
|
<strong>The staged diff looks ready for human verification.</strong>
|
|
<span>AI reviews can miss issues. Run the relevant tests before committing.</span>
|
|
</div>
|
|
{:else}
|
|
{#each result.findings as finding, index (`${finding.file ?? "repo"}:${finding.line ?? 0}:${finding.title}:${index}`)}
|
|
<article class="ai-review-finding {finding.severity}">
|
|
<div class="ai-review-finding-icon">
|
|
{#if finding.severity === "critical"}<CircleAlert size={17} aria-hidden="true" />{:else if finding.severity === "warning"}<AlertTriangle size={17} aria-hidden="true" />{:else}<Info size={17} aria-hidden="true" />{/if}
|
|
</div>
|
|
<div class="ai-review-finding-body">
|
|
<div class="ai-review-finding-title">
|
|
<span>{finding.severity}</span>
|
|
<strong>{finding.title}</strong>
|
|
</div>
|
|
<p>{finding.description}</p>
|
|
<div class="ai-review-location"><FileCode size={13} aria-hidden="true" /><code>{locationLabel(finding)}</code></div>
|
|
{#if finding.suggestion}<div class="ai-review-suggestion"><strong>Suggested next step</strong><span>{finding.suggestion}</span></div>{/if}
|
|
</div>
|
|
</article>
|
|
{/each}
|
|
{/if}
|
|
</div>
|
|
|
|
<footer class="dialog-footer ai-review-footer">
|
|
<p>Review suggestions are advisory and never modify files automatically.</p>
|
|
<div>
|
|
<button class="btn-secondary" type="button" onclick={onRerun} disabled={isReviewing}>
|
|
{#if isReviewing}<LoaderCircle class="spin" size={14} aria-hidden="true" />{:else}<RotateCw size={14} aria-hidden="true" />{/if}
|
|
Review again
|
|
</button>
|
|
<button class="btn-primary" type="button" onclick={onClose} disabled={isReviewing}>Done</button>
|
|
</div>
|
|
</footer>
|
|
</div>
|
|
</div>
|