try to use local ai to generate commit message

This commit is contained in:
Christoph Brandau
2026-07-02 19:59:16 +02:00
parent 2a96e79d27
commit f2aa48d2ec
13 changed files with 3889 additions and 62 deletions
+54 -1
View File
@@ -25,6 +25,8 @@
import {
checkoutBranch,
commit,
commitAiGenerate,
commitAiStatus,
compareCommits,
cancelCodeSearch,
cancelFileHistory,
@@ -62,6 +64,7 @@
} from "./lib/git";
import type {
CommitAiPhase,
ConflictFile,
ExplorerNode,
ExplorerNodeKind,
@@ -122,7 +125,11 @@
let fileHistoryLoading = false;
let fileHistoryRequestId = 0;
let activeFileHistoryRequestId = "";
let lastFileHistoryHeadHash = "";
let commitMessage = "";
let commitAiPhase: CommitAiPhase = "idle";
let commitAiGenerating = false;
let commitAiPollTimer: ReturnType<typeof setInterval> | undefined;
let errorMessage = "";
let operation = "";
let compareFrom = "";
@@ -206,10 +213,12 @@
loadRepoLists();
autoRefreshTimer = setInterval(() => { void autoRefreshTick(); }, AUTO_REFRESH_INTERVAL);
void checkForUpdates();
startCommitAiPolling();
});
onDestroy(() => {
if (autoRefreshTimer) clearInterval(autoRefreshTimer);
if (commitAiPollTimer) clearInterval(commitAiPollTimer);
if (activeFileHistoryRequestId) void cancelFileHistory(activeFileHistoryRequestId);
});
@@ -229,15 +238,54 @@
applyStatus(nextStatus);
// Something changed — reload branches, commits and files in one bundled call.
const bundle = await openRepositoryBundle(activeRepoPath, 100);
const previousHeadHash = lastFileHistoryHeadHash;
await refreshBranchList(activeRepoPath, bundle.branches);
await refreshCommitHistory(activeRepoPath, bundle.commits);
await refreshExplorerFiles(activeRepoPath, bundle.files);
await refreshFileHistory(activeRepoPath);
// File history reflects `git log`, which only changes when HEAD actually moves
// (new commit, checkout, merge, ...) — skip the reload otherwise so a plain
// working-tree/status change (staging, edits) doesn't keep re-fetching and
// flickering the currently viewed file's history.
if (lastFileHistoryHeadHash !== previousHeadHash) {
await refreshFileHistory(activeRepoPath);
}
} catch { /* ignore transient errors */ } finally {
autoRefreshInFlight = false;
}
}
// ── Commit AI ──────────────────────────────────────────────────────────────
async function pollCommitAiStatus() {
try {
const result = await commitAiStatus();
commitAiPhase = result.phase;
} catch { /* ignore transient errors */ }
if (commitAiPhase === "ready" || commitAiPhase === "error") {
if (commitAiPollTimer) { clearInterval(commitAiPollTimer); commitAiPollTimer = undefined; }
}
}
function startCommitAiPolling() {
// The model downloads (first run only) and loads in the background on app start;
// poll until it's ready (or failed) so the "AI" button can enable itself.
void pollCommitAiStatus();
commitAiPollTimer = setInterval(() => { void pollCommitAiStatus(); }, 2000);
}
async function generateCommitMessageWithAi() {
if (!activeRepoPath || commitAiPhase !== "ready" || commitAiGenerating) return;
commitAiGenerating = true;
errorMessage = "";
try {
commitMessage = await commitAiGenerate(activeRepoPath, commitMessage.trim() || undefined);
} catch (error) {
errorMessage = errorToMessage(error);
} finally {
commitAiGenerating = false;
}
}
function toggleAutoRefresh() {
autoRefreshEnabled = !autoRefreshEnabled;
if (autoRefreshEnabled) void autoRefreshTick();
@@ -430,6 +478,7 @@
}
branches = [];
commits = [];
lastFileHistoryHeadHash = "";
repoFiles = [];
selectedExplorerPath = "";
selectedExplorerKind = "file";
@@ -520,6 +569,7 @@
async function refreshCommitHistory(path = activeRepoPath, prefetched?: GitCommit[]) {
commits = prefetched ?? (await listCommits(path, 100));
lastFileHistoryHeadHash = commits[0]?.hash ?? "";
const hashes = new Set(commits.map((c) => c.hash));
if (compareFrom && !hashes.has(compareFrom)) compareFrom = "";
if (compareTo && !hashes.has(compareTo)) compareTo = "";
@@ -1692,8 +1742,11 @@
{isBusy}
{operation}
{stagedCount}
{commitAiPhase}
{commitAiGenerating}
onCommit={commitChanges}
onCommitMessageChange={(msg) => { commitMessage = msg; }}
onGenerateCommitMessage={generateCommitMessageWithAi}
/>
</div>
</section>