feat(ai): generate PR drafts and consolidate AI settings UI
Add pull request draft generation to the commit_ai crate and expose it
via a new Tauri command. The backend builds a branch-range context from
published remote-tracking refs only, calls the chosen AI provider, and
parses a JSON {"title","description"} draft (with validation). Also
register the command in the app and add unit tests for parsing and the
branch-context behavior.
Consolidate AI settings in the frontend by renaming the dialog to an
AiSettingsPage and integrating AI options into the main AppSettings
dialog. Persisted AI preferences are merged with existing localStorage
rather than replacing it, and the settings UI now supports opening the
app settings to a specific initial page ("integrations" or "ai").
Other changes:
- Replace the commit-message system prompt used by build_messages with
the updated, more detailed guidance text.
This commit is contained in:
+15
-38
@@ -1,16 +1,15 @@
|
||||
<script lang="ts">
|
||||
import { onDestroy, onMount } from "svelte";
|
||||
import { Bot, Check, Eye, EyeOff, Globe, Key, LoaderCircle, X } from "@lucide/svelte";
|
||||
import { Bot, Eye, EyeOff, Globe, Key } from "@lucide/svelte";
|
||||
import { credDelete, credLoad, credSave } from "../git";
|
||||
import type { AiSettings, CommitAiProvider } from "../types";
|
||||
|
||||
interface Props {
|
||||
settings: AiSettings;
|
||||
onSave: (settings: AiSettings) => void;
|
||||
onClose: () => void;
|
||||
|
||||
}
|
||||
|
||||
let { settings, onSave, onClose }: Props = $props();
|
||||
let { settings }: Props = $props();
|
||||
|
||||
type CloudProvider = CommitAiProvider;
|
||||
|
||||
@@ -29,6 +28,8 @@
|
||||
let openaiApiKey = $state("");
|
||||
let anthropicApiKey = $state("");
|
||||
let customApiKey = $state("");
|
||||
let originalKeys = { openai: "", anthropic: "", custom: "" };
|
||||
let keysLoaded = false;
|
||||
let showKey = $state(false);
|
||||
let loadingKeys = $state(true);
|
||||
let saving = $state(false);
|
||||
@@ -64,6 +65,8 @@
|
||||
openaiApiKey = openai?.password ?? "";
|
||||
anthropicApiKey = anthropic?.password ?? "";
|
||||
customApiKey = custom?.password ?? "";
|
||||
originalKeys = { openai: openaiApiKey, anthropic: anthropicApiKey, custom: customApiKey };
|
||||
keysLoaded = true;
|
||||
} catch (err) {
|
||||
error = err instanceof Error ? err.message : String(err);
|
||||
} finally {
|
||||
@@ -77,6 +80,8 @@
|
||||
});
|
||||
|
||||
async function persistKey(target: CloudProvider, value: string) {
|
||||
if (value === originalKeys[target]) return;
|
||||
if (!keysLoaded) throw new Error("API keys could not be loaded. Existing credentials have been preserved.");
|
||||
const key = CRED_KEYS[target];
|
||||
const trimmed = value.trim();
|
||||
if (trimmed) {
|
||||
@@ -86,7 +91,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
export async function saveSettings(): Promise<AiSettings> {
|
||||
if (loadingKeys) throw new Error("Please wait for AI settings to load.");
|
||||
saving = true;
|
||||
error = "";
|
||||
try {
|
||||
@@ -95,15 +101,16 @@
|
||||
persistKey("anthropic", anthropicApiKey),
|
||||
persistKey("custom", customApiKey),
|
||||
]);
|
||||
onSave({
|
||||
return {
|
||||
provider,
|
||||
openaiModel: openaiModel.trim() || "gpt-4o-mini",
|
||||
anthropicModel: anthropicModel.trim() || "claude-3-5-haiku-latest",
|
||||
customBaseUrl: customBaseUrl.trim(),
|
||||
customModel: customModel.trim(),
|
||||
});
|
||||
};
|
||||
} catch (err) {
|
||||
error = err instanceof Error ? err.message : String(err);
|
||||
throw err;
|
||||
} finally {
|
||||
saving = false;
|
||||
}
|
||||
@@ -111,22 +118,7 @@
|
||||
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="dialog-backdrop"
|
||||
role="presentation"
|
||||
>
|
||||
<div class="dialog ai-settings-dialog" role="dialog" aria-modal="true" aria-label="AI settings" tabindex="-1">
|
||||
<header class="dialog-header">
|
||||
<div>
|
||||
<span class="eyebrow">Commit AI</span>
|
||||
<h2 class="mt-0.5 text-ink text-base font-bold leading-tight">AI settings</h2>
|
||||
</div>
|
||||
<button class="dialog-close" type="button" onclick={onClose} title="Close">
|
||||
<X size={18} aria-hidden="true" />
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<form class="ai-settings-form" onsubmit={(e) => { e.preventDefault(); void handleSave(); }}>
|
||||
<div class="ai-settings-form">
|
||||
<div class="ai-provider-options" role="radiogroup" aria-label="AI provider">
|
||||
<button type="button" class="ai-provider-option" class:active={provider === "openai"} onclick={() => { provider = "openai"; }}>
|
||||
<Bot size={16} aria-hidden="true" />
|
||||
@@ -222,19 +214,4 @@
|
||||
<p class="commit-block-reason">{error}</p>
|
||||
{/if}
|
||||
|
||||
<div class="new-branch-actions">
|
||||
<button class="btn-secondary" type="button" onclick={onClose} disabled={saving}>
|
||||
Cancel
|
||||
</button>
|
||||
<button class="btn-primary" type="submit" disabled={saving || loadingKeys}>
|
||||
{#if saving}
|
||||
<LoaderCircle class="spin" size={16} aria-hidden="true" />
|
||||
{:else}
|
||||
<Check size={16} aria-hidden="true" />
|
||||
{/if}
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,4 +1,7 @@
|
||||
<script lang="ts">
|
||||
import AiSettingsPage from "./AiSettingsPage.svelte";
|
||||
import type { AiSettings } from "../types";
|
||||
import { untrack } from "svelte";
|
||||
import { open } from "@tauri-apps/plugin-dialog";
|
||||
import {
|
||||
Check,
|
||||
@@ -47,9 +50,12 @@
|
||||
import IntegrationSettingsPage from "./IntegrationSettingsPage.svelte";
|
||||
import SelectMenu from "./SelectMenu.svelte";
|
||||
|
||||
type SettingsPage = "general" | "integrations" | "tools";
|
||||
type SettingsPage = "general" | "integrations" | "tools" | "ai";
|
||||
|
||||
interface Props {
|
||||
aiSettings: AiSettings;
|
||||
initialPage?: SettingsPage;
|
||||
onSaveAiSettings: (settings: AiSettings) => void;
|
||||
analytics: AnalyticsSettings;
|
||||
theme: AppTheme;
|
||||
appearance: AppAppearance;
|
||||
@@ -67,6 +73,7 @@
|
||||
}
|
||||
|
||||
let {
|
||||
aiSettings, initialPage = "integrations", onSaveAiSettings,
|
||||
analytics,
|
||||
theme = "system",
|
||||
appearance = "modern",
|
||||
@@ -85,7 +92,7 @@
|
||||
|
||||
const toolKinds: ExternalToolKind[] = ["editor", "diff", "merge", "terminal", "fileManager"];
|
||||
|
||||
let activePage = $state<SettingsPage>("integrations");
|
||||
let activePage = $state<SettingsPage>(untrack(() => initialPage));
|
||||
let activeToolKind = $state<ExternalToolKind>("editor");
|
||||
let advancedOpen = $state(false);
|
||||
let analyticsEnabled = $state(true);
|
||||
@@ -98,6 +105,8 @@
|
||||
let integrationDraft = $state<GitIntegrationSettings>(defaultGitIntegrationSettings());
|
||||
let integrationSecretUpdates = $state<GitIntegrationSecretUpdate[]>([]);
|
||||
let saving = $state(false);
|
||||
let saveError = $state("");
|
||||
let aiPage: AiSettingsPage;
|
||||
const isGerman = $derived(selectedLanguage === "de");
|
||||
|
||||
$effect(() => {
|
||||
@@ -114,12 +123,17 @@
|
||||
async function save() {
|
||||
if (saving) return;
|
||||
saving = true;
|
||||
saveError = "";
|
||||
try {
|
||||
const nextAi = await aiPage.saveSettings();
|
||||
onSaveAiSettings(nextAi);
|
||||
await onSave({
|
||||
...analytics,
|
||||
enabled: analyticsEnabled,
|
||||
noticeSeen: true,
|
||||
}, selectedTheme, selectedAppearance, $state.snapshot(customColors), selectedLanguage, autoRefreshEnabled, $state.snapshot(tools), $state.snapshot(integrationDraft), $state.snapshot(integrationSecretUpdates));
|
||||
} catch (cause) {
|
||||
saveError = String(cause);
|
||||
} finally {
|
||||
saving = false;
|
||||
}
|
||||
@@ -342,10 +356,13 @@
|
||||
<em>{configuredIntegrationCount(integrationDraft)}</em>
|
||||
</button>
|
||||
|
||||
<button type="button" class:active={activePage === "ai"} onclick={() => { activePage = "ai"; }}>
|
||||
<Code2 size={16}/><span><strong>{isGerman ? "Künstliche Intelligenz" : "Artificial intelligence"}</strong><small>Commits, Reviews & Pull Requests</small></span>
|
||||
</button>
|
||||
<div class="settings-nav-note">
|
||||
{#if activePage === "integrations"}<KeyRound size={15} aria-hidden="true" />{:else}<ShieldCheck size={15} aria-hidden="true" />{/if}
|
||||
{#if activePage === "integrations" || activePage === "ai"}<KeyRound size={15} aria-hidden="true" />{:else}<ShieldCheck size={15} aria-hidden="true" />{/if}
|
||||
<p>
|
||||
{activePage === "integrations"
|
||||
{activePage === "integrations" || activePage === "ai"
|
||||
? (isGerman ? "Tokens werden sicher im Schlüsselbund des Betriebssystems gespeichert." : "Tokens are stored securely in the operating system keychain.")
|
||||
: (isGerman ? "Tool-Argumente werden direkt und ohne zusätzliche Shell übergeben." : "Tool arguments are passed directly without an extra shell.")}
|
||||
</p>
|
||||
@@ -353,6 +370,10 @@
|
||||
</nav>
|
||||
|
||||
<div class="settings-content">
|
||||
<div hidden={activePage !== "ai"}>
|
||||
<div class="settings-page-head"><div><h3>{isGerman ? "KI-Einstellungen" : "AI settings"}</h3><p>{isGerman ? "Gemeinsamer Anbieter für Commits, Reviews und PR-Beschreibungen." : "Shared provider for commits, reviews and PR descriptions."}</p></div></div>
|
||||
<AiSettingsPage bind:this={aiPage} settings={aiSettings}/>
|
||||
</div>
|
||||
{#if activePage === "general"}
|
||||
<div class="settings-page-head">
|
||||
<div>
|
||||
@@ -548,7 +569,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
{:else}
|
||||
{:else if activePage === "integrations"}
|
||||
<div class="settings-page-head">
|
||||
<div>
|
||||
<h3>{isGerman ? "Integrationen" : "Integrations"}</h3>
|
||||
@@ -565,6 +586,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if saveError}<p role="alert">{saveError}</p>{/if}
|
||||
<footer class="app-settings-footer">
|
||||
<span>{isGerman ? "Änderungen werden erst beim Speichern übernommen." : "Changes are applied only after saving."}</span>
|
||||
<div>
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
<script lang="ts">
|
||||
import SelectMenu from "./SelectMenu.svelte";
|
||||
import { onMount } from "svelte";
|
||||
import { GitPullRequest, X, LoaderCircle, ArrowRight } from "@lucide/svelte";
|
||||
import { createIntegrationReviewRequest, listIntegrationRepositories, listIntegrationRepositoryBranches, listRemotes, listBranches } from "../git";
|
||||
import { GitPullRequest, X, LoaderCircle, ArrowRight, Sparkles } from "@lucide/svelte";
|
||||
import { credLoad, pullRequestAiGenerate, createIntegrationReviewRequest, listIntegrationRepositories, listIntegrationRepositoryBranches, listRemotes, listBranches } from "../git";
|
||||
import { integrationCredentialKey } from "../integrations";
|
||||
import type { GitIntegrationSource, GitIntegrationRepository, IntegrationReviewRequest, StoredCredential } from "../types";
|
||||
import type { AiSettings, GitIntegrationSource, GitIntegrationRepository, IntegrationReviewRequest, StoredCredential } from "../types";
|
||||
|
||||
let { source, de, localRepositoryPath = "", loadCredential, onClose, onCreated }: {
|
||||
let { aiSettings, source, de, localRepositoryPath = "", loadCredential, onClose, onCreated }: {
|
||||
aiSettings: AiSettings;
|
||||
source: GitIntegrationSource; de: boolean; localRepositoryPath?: string;
|
||||
loadCredential: (key: string) => Promise<StoredCredential | null>;
|
||||
onClose: () => void; onCreated: (request: IntegrationReviewRequest) => void;
|
||||
} = $props();
|
||||
let dialog: HTMLDialogElement;
|
||||
let titleInput: HTMLInputElement;
|
||||
let repositories = $state<GitIntegrationRepository[]>([]);
|
||||
let repositoryId = $state("");
|
||||
let sourceBranch = $state("");
|
||||
@@ -59,6 +61,7 @@
|
||||
if (generation === branchGeneration) branchError = String(cause);
|
||||
} finally { if (generation === branchGeneration) branchesLoading = false; }
|
||||
}
|
||||
let generating = $state(false);
|
||||
let title = $state("");
|
||||
let description = $state("");
|
||||
let loading = $state(true);
|
||||
@@ -70,7 +73,7 @@
|
||||
const sameBranch = $derived(!!sourceBranch.trim() && normalizeBranch(sourceBranch) === normalizeBranch(targetBranch));
|
||||
const valid = $derived(!branchesLoading && !branchError && branches.includes(sourceBranch) && branches.includes(targetBranch) && repositoryId && title.trim() && sourceBranch.trim() && targetBranch.trim() && !sameBranch);
|
||||
|
||||
onMount(() => { dialog.showModal(); void loadRepositories(); });
|
||||
onMount(() => { dialog.showModal(); titleInput.focus({ preventScroll: true }); void loadRepositories(); });
|
||||
async function loadRepositories() {
|
||||
loading = true; error = "";
|
||||
try {
|
||||
@@ -79,9 +82,34 @@
|
||||
} catch (cause) { error = String(cause); }
|
||||
finally { loading = false; }
|
||||
}
|
||||
async function generateDraft() {
|
||||
if (busy || generating || branchesLoading || !sourceBranch || !targetBranch || sameBranch) return;
|
||||
const repository = repositories.find(item => item.id === repositoryId);
|
||||
if (!repository) return;
|
||||
generating = true; error = "";
|
||||
const context = `${repositoryId}:${sourceBranch}:${targetBranch}`;
|
||||
try {
|
||||
if (!localRepositoryPath) throw new Error(de ? "Öffne zuerst das passende lokale Repository und führe Fetch aus." : "Open the matching local repository and fetch it first.");
|
||||
const remotes = await listRemotes(localRepositoryPath);
|
||||
const clean = (url: string) => url.trim().replace(/\.git\/?$/, "").replace(/\/$/, "");
|
||||
const remote = remotes.find(remote => [repository.cloneUrl, repository.sshUrl].filter(Boolean).some(url => clean(url) === clean(remote.fetch_url)));
|
||||
if (!remote) throw new Error(de ? "Das offene lokale Repository passt nicht zum ausgewählten PR-Repository." : "The open local repository does not match the selected PR repository.");
|
||||
const settings = { ...aiSettings };
|
||||
const credential = await credLoad(`ai:${settings.provider}`);
|
||||
const draft = await pullRequestAiGenerate(localRepositoryPath, remote.name, sourceBranch, targetBranch, {
|
||||
provider: settings.provider,
|
||||
model: settings.provider === "openai" ? settings.openaiModel : settings.provider === "anthropic" ? settings.anthropicModel : settings.customModel,
|
||||
baseUrl: settings.provider === "custom" ? settings.customBaseUrl : undefined,
|
||||
apiKey: credential?.password, language: de ? "de" : "en",
|
||||
});
|
||||
if (context !== `${repositoryId}:${sourceBranch}:${targetBranch}`) return;
|
||||
title = draft.title; description = draft.description;
|
||||
} catch (cause) { error = cause instanceof Error ? cause.message : String(cause); }
|
||||
finally { generating = false; }
|
||||
}
|
||||
async function submit(event: SubmitEvent) {
|
||||
event.preventDefault();
|
||||
if (busy || !valid) return;
|
||||
if (busy || generating || !valid) return;
|
||||
const repository = repositories.find(item => item.id === repositoryId);
|
||||
if (!repository) return;
|
||||
busy = true; error = "";
|
||||
@@ -95,9 +123,9 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<dialog bind:this={dialog} aria-labelledby="create-review-title" oncancel={(event) => { event.preventDefault(); if (!busy) onClose(); }} onclick={(event) => { if (event.target === dialog && !busy) { const rect = dialog.getBoundingClientRect(); if (event.clientX < rect.left || event.clientX > rect.right || event.clientY < rect.top || event.clientY > rect.bottom) onClose(); } }}>
|
||||
<dialog bind:this={dialog} aria-labelledby="create-review-title" oncancel={(event) => { event.preventDefault(); if (!busy && !generating) onClose(); }} onclick={(event) => { if (event.target === dialog && !busy && !generating) { const rect = dialog.getBoundingClientRect(); if (event.clientX < rect.left || event.clientX > rect.right || event.clientY < rect.top || event.clientY > rect.bottom) onClose(); } }}>
|
||||
<form onsubmit={submit}>
|
||||
<header><div class="heading-icon"><GitPullRequest size={19} /></div><div><h2 id="create-review-title">{heading}</h2><p>{source.label}</p></div><button data-dialog-close class="close" type="button" aria-label={de ? "Schließen" : "Close"} disabled={busy} onclick={onClose}><X size={18}/></button></header>
|
||||
<header><div class="heading-icon"><GitPullRequest size={19} /></div><div><h2 id="create-review-title">{heading}</h2><p>{source.label}</p></div><button data-dialog-close class="close" type="button" aria-label={de ? "Schließen" : "Close"} disabled={generating || busy} onclick={onClose}><X size={18}/></button></header>
|
||||
<div class="body">
|
||||
{#if error}<div class="error" role="alert">{error}{#if !repositories.length && !loading}<button type="button" onclick={loadRepositories}>{de ? "Erneut laden" : "Retry"}</button>{/if}</div>{/if}
|
||||
<div class="repository-field"><div class="field-heading"><span>Repository</span><small>{loading ? "…" : `${repositories.length} ${de ? "verfügbar" : "available"}`}</small></div>
|
||||
@@ -105,21 +133,23 @@
|
||||
</div>
|
||||
{#if !loading && !error && !repositories.length}<p>{de ? "Keine Repositories für diese Integration gefunden." : "No repositories found for this integration."}</p>{/if}
|
||||
<div class="branches">
|
||||
<div class="repository-field"><span>{de ? "Quellbranch" : "Source branch"}</span><SelectMenu value={sourceBranch} options={branchOptions.map(option => ({...option,disabled:option.value === targetBranch}))} disabled={busy || branchesLoading || !repositoryId} ariaLabel={de ? "Quellbranch" : "Source branch"} placeholder={branchesLoading ? (de ? "Lädt …" : "Loading…") : (de ? "Quellbranch auswählen" : "Select source branch")} searchable searchPlaceholder={de ? "Branches durchsuchen …" : "Search branches…"} emptyText={de ? "Keine Branches gefunden" : "No branches found"} onChange={value => sourceBranch = value}/></div>
|
||||
<div class="repository-field"><span>{de ? "Quellbranch" : "Source branch"}</span><SelectMenu value={sourceBranch} options={branchOptions.map(option => ({...option,disabled:option.value === targetBranch}))} disabled={generating || busy || branchesLoading || !repositoryId} ariaLabel={de ? "Quellbranch" : "Source branch"} placeholder={branchesLoading ? (de ? "Lädt …" : "Loading…") : (de ? "Quellbranch auswählen" : "Select source branch")} searchable searchPlaceholder={de ? "Branches durchsuchen …" : "Search branches…"} emptyText={de ? "Keine Branches gefunden" : "No branches found"} onChange={value => sourceBranch = value}/></div>
|
||||
<ArrowRight size={16}/>
|
||||
<div class="repository-field"><span>{de ? "Zielbranch" : "Target branch"}</span><SelectMenu value={targetBranch} options={branchOptions.map(option => ({...option,disabled:option.value === sourceBranch}))} disabled={busy || branchesLoading || !repositoryId} ariaLabel={de ? "Zielbranch" : "Target branch"} placeholder={branchesLoading ? (de ? "Lädt …" : "Loading…") : (de ? "Zielbranch auswählen" : "Select target branch")} searchable searchPlaceholder={de ? "Branches durchsuchen …" : "Search branches…"} emptyText={de ? "Keine Branches gefunden" : "No branches found"} onChange={value => targetBranch = value}/></div>
|
||||
<div class="repository-field"><span>{de ? "Zielbranch" : "Target branch"}</span><SelectMenu value={targetBranch} options={branchOptions.map(option => ({...option,disabled:option.value === sourceBranch}))} disabled={generating || busy || branchesLoading || !repositoryId} ariaLabel={de ? "Zielbranch" : "Target branch"} placeholder={branchesLoading ? (de ? "Lädt …" : "Loading…") : (de ? "Zielbranch auswählen" : "Select target branch")} searchable searchPlaceholder={de ? "Branches durchsuchen …" : "Search branches…"} emptyText={de ? "Keine Branches gefunden" : "No branches found"} onChange={value => targetBranch = value}/></div>
|
||||
</div>
|
||||
{#if branchError}<div class="error" role="alert">{branchError}<button type="button" onclick={() => loadRepositoryBranches(repositories.find(item => item.id === repositoryId))}>{de ? "Erneut laden" : "Retry"}</button></div>{:else if repositoryId && !branchesLoading && !branches.length}<p>{de ? "Dieses Repository hat noch keine Branches." : "This repository has no branches yet."}</p>{/if}
|
||||
{#if sameBranch}<p class="validation">{de ? "Quell- und Zielbranch müssen unterschiedlich sein." : "Source and target branches must be different."}</p>{/if}
|
||||
<p class="hint">{de ? "Beide Branches müssen bereits in diesem Repository gepusht sein." : "Both branches must already be pushed to this repository."}</p>
|
||||
<label>{de ? "Titel" : "Title"}<input bind:value={title} disabled={busy} placeholder={de ? "Was ändert sich?" : "What is changing?"} required /></label>
|
||||
<label>{de ? "Beschreibung" : "Description"}<textarea bind:value={description} disabled={busy} rows="7" placeholder={de ? "Beschreibe deine Änderungen … (Markdown unterstützt)" : "Describe your changes… (Markdown supported)"}></textarea></label>
|
||||
<div class="ai-draft-action"><button type="button" disabled={generating || busy || branchesLoading || !sourceBranch || !targetBranch || sameBranch} onclick={generateDraft}>{#if generating}<LoaderCircle class="spin" size={15}/>{:else}<Sparkles size={15}/>{/if}{generating ? (de ? "Wird generiert …" : "Generating…") : (de ? "Mit KI erstellen" : "Generate with AI")}</button><small>{de ? "Erstellt Titel und Beschreibung aus dem lokalen Stand der Remote-Branches. Vorher Fetch ausführen." : "Creates a title and description from locally fetched remote branches. Fetch first."}</small></div>
|
||||
<label>{de ? "Titel" : "Title"}<input bind:this={titleInput} bind:value={title} disabled={generating || busy} placeholder={de ? "Was ändert sich?" : "What is changing?"} required /></label>
|
||||
<label>{de ? "Beschreibung" : "Description"}<textarea bind:value={description} disabled={generating || busy} rows="7" placeholder={de ? "Beschreibe deine Änderungen … (Markdown unterstützt)" : "Describe your changes… (Markdown supported)"}></textarea></label>
|
||||
</div>
|
||||
<footer><button type="button" disabled={busy} onclick={onClose}>{de ? "Abbrechen" : "Cancel"}</button><button class="primary" type="submit" disabled={busy || loading || !valid}>{#if busy}<LoaderCircle class="spin" size={15}/>{:else}<GitPullRequest size={15}/>{/if}{busy ? (de ? "Wird erstellt …" : "Creating…") : heading}</button></footer>
|
||||
<footer><button type="button" disabled={generating || busy} onclick={onClose}>{de ? "Abbrechen" : "Cancel"}</button><button class="primary" type="submit" disabled={generating || busy || loading || !valid}>{#if busy}<LoaderCircle class="spin" size={15}/>{:else}<GitPullRequest size={15}/>{/if}{busy ? (de ? "Wird erstellt …" : "Creating…") : heading}</button></footer>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<style>
|
||||
.ai-draft-action{display:flex;align-items:center;gap:12px}.ai-draft-action small{color:var(--color-ink-muted);line-height:1.5}.ai-draft-action button{flex-shrink:0}
|
||||
.repository-field{min-width:0;display:grid;gap:9px}.field-heading{display:flex;align-items:center;justify-content:space-between;font-weight:600}.field-heading small{font-size:10px;font-weight:400;color:var(--color-ink-faint)}
|
||||
dialog{margin:auto;width:min(640px,calc(100vw - 40px));max-height:calc(100vh - 48px);padding:0;border:1px solid var(--color-border-subtle);border-radius:14px;background:var(--app-dialog-bg);color:var(--color-ink);box-shadow:var(--app-dialog-shadow);font-family:inherit;font-size:12px;overflow:auto}dialog::backdrop{background:#0007;backdrop-filter:blur(3px)}header{display:flex;align-items:center;gap:12px;padding:22px 26px;background:var(--app-dialog-chrome);border-bottom:1px solid var(--color-border-subtle)}h2{margin:0;font-size:16px;font-weight:650}p{margin:5px 0 0;color:var(--color-ink-muted)}.heading-icon{display:grid;place-items:center;width:38px;height:38px;border-radius:10px;background:color-mix(in srgb,var(--color-accent) 12%,transparent);color:var(--color-accent)}button,input,textarea{font:inherit}button{display:inline-flex;justify-content:center;align-items:center;gap:8px;border:1px solid var(--color-border-subtle);border-radius:7px;padding:9px 13px;background:var(--color-surface);color:var(--color-ink);cursor:pointer}button:disabled{opacity:.5;cursor:default}.close{margin-left:auto;border:0;padding:6px}.body{display:grid;gap:20px;padding:24px 26px}label{display:grid;gap:8px;font-weight:600;min-width:0}input,textarea{box-sizing:border-box;width:100%;padding:10px 11px;border:1px solid var(--color-border-subtle);border-radius:7px;background:var(--app-bg);color:var(--color-ink);font-weight:400}input:focus,textarea:focus{outline:2px solid var(--color-accent);outline-offset:1px}textarea{resize:vertical;min-height:110px;line-height:1.6}.branches{padding:16px;background:color-mix(in srgb,var(--color-accent) 3%,var(--app-bg));border:1px solid var(--color-border-subtle);display:grid;grid-template-columns:minmax(0,1fr) 16px minmax(0,1fr);gap:12px;align-items:end}.branches>:global(svg){margin-bottom:12px;color:var(--color-accent)}.hint{margin-top:-8px;font-size:11px;line-height:1.5}.error,.validation{color:var(--color-danger,#e76767);line-height:1.5}.error{padding:12px;border-radius:7px;background:color-mix(in srgb,var(--color-danger,#e76767) 9%,transparent);overflow-wrap:anywhere}.error button{margin-left:8px}footer{display:flex;justify-content:flex-end;gap:9px;padding:16px 26px;background:var(--app-dialog-chrome);border-top:1px solid var(--color-border-subtle)}.primary{background:var(--color-accent);border-color:var(--color-accent);color:white}.primary:enabled:hover{filter:brightness(1.08)}:global(.spin){animation:rotate 1s linear infinite}@keyframes rotate{to{transform:rotate(360deg)}}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import type { AiSettings } from "../types";
|
||||
import CreateReviewDialog from "./CreateReviewDialog.svelte";
|
||||
import CommentEditor from "./CommentEditor.svelte";
|
||||
import SelectMenu from "./SelectMenu.svelte";
|
||||
@@ -17,6 +18,7 @@
|
||||
type LocalResolutionPhase = "idle" | "preparing" | "conflicts" | "ready-to-continue" | "ready-to-push" | "complete" | "error";
|
||||
|
||||
interface Props {
|
||||
aiSettings: AiSettings;
|
||||
language: AppLanguage;
|
||||
localRepositoryPath?: string;
|
||||
integrations: GitIntegrationSettings;
|
||||
@@ -34,7 +36,7 @@
|
||||
onPushLocalResolution?: () => void | Promise<void>;
|
||||
}
|
||||
|
||||
let { localRepositoryPath = "", language = "en", integrations, initialQuery = "", initialSourceId = "", localResolutionRequestId = "", localResolutionPhase = "idle", localResolutionMessage = "", loadCredential, onOpenSettings = () => {}, onStartLocalResolution = () => {}, onOpenLocalResolver = () => {}, onContinueLocalResolution = () => {}, onAbortLocalResolution = () => {}, onPushLocalResolution = () => {} }: Props = $props();
|
||||
let { aiSettings, localRepositoryPath = "", language = "en", integrations, initialQuery = "", initialSourceId = "", localResolutionRequestId = "", localResolutionPhase = "idle", localResolutionMessage = "", loadCredential, onOpenSettings = () => {}, onStartLocalResolution = () => {}, onOpenLocalResolver = () => {}, onContinueLocalResolution = () => {}, onAbortLocalResolution = () => {}, onPushLocalResolution = () => {} }: Props = $props();
|
||||
let createOpen = $state(false);
|
||||
let requests = $state<IntegrationReviewRequest[]>([]);
|
||||
let loading = $state(false);
|
||||
@@ -388,7 +390,7 @@
|
||||
</script>
|
||||
|
||||
{#if createOpen && activeSource}
|
||||
<CreateReviewDialog {localRepositoryPath} source={activeSource} {de} {loadCredential} onClose={() => createOpen = false} onCreated={(request) => {
|
||||
<CreateReviewDialog {aiSettings} {localRepositoryPath} source={activeSource} {de} {loadCredential} onClose={() => createOpen = false} onCreated={(request) => {
|
||||
++loadGeneration;
|
||||
loading = false;
|
||||
requests = [request, ...requests.filter(item => item.id !== request.id)];
|
||||
|
||||
@@ -728,3 +728,7 @@ export function listAzureIssueTypes(baseUrl: string, username: string, token: st
|
||||
export function createIntegrationIssue(provider: GitIntegrationProvider, baseUrl: string, username: string, token: string, repository: string, title: string, description: string, workItemType: string): Promise<import("./types").IntegrationIssue> {
|
||||
return invoke("create_integration_issue", { provider, baseUrl, username, token, repository, title, description, workItemType });
|
||||
}
|
||||
|
||||
export function pullRequestAiGenerate(path: string, remote: string, sourceBranch: string, targetBranch: string, options: { provider: string; model: string; apiKey?: string; baseUrl?: string; language: string }): Promise<{title: string; description: string}> {
|
||||
return invoke("pull_request_ai_generate", { path, remote, sourceBranch, targetBranch, ...options });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user