refactor(commit-ai): remove local models and simplify AI flow

Remove local on-device model support and related IPC commands,
consolidating commit-generation to cloud providers and simplifying the
AI crate surface. Local-specific types, generation profiles, caching,
and the local prompt builder were removed while message sanitization and
diff-echo detection were preserved. Also harden repository handling and
runtime: unborn HEADs are handled gracefully so empty repos still report
files, Git LFS sync is skipped for repositories without commits, and
tokio runtime features were enabled.

- Remove local model engine, load/status commands, and local profile code
- Handle unborn HEAD and skip LFS sync for repos without commits
- Enable tokio runtime features and route AI generation to cloud only
This commit is contained in:
2026-08-30 19:16:32 +02:00
parent 9c93d5a978
commit 4db6f30461
13 changed files with 161 additions and 3959 deletions
+13 -85
View File
@@ -49,9 +49,6 @@
commitAiGenerate,
commitAiReview,
commitAiSplit,
commitAiLoad,
commitAiLocalModels,
commitAiStatus,
compareCommits,
cancelCodeSearch,
cancelFileHistory,
@@ -151,7 +148,6 @@
AppLanguage,
AppTheme,
AnalyticsSettings,
CommitAiPhase,
CustomThemeColors,
ConflictFile,
DetectedExternalTool,
@@ -179,7 +175,6 @@
GitStatus,
GitTag,
GitWorktree,
LocalModelOption,
PatchApplyAction,
PreparedResolution,
RebaseCommit,
@@ -360,8 +355,6 @@
let commitMessage = "";
let amendMode = false;
let preAmendDraftMessage = "";
let lastLocalAiGeneratedMessage = "";
let commitAiPhase: CommitAiPhase = "idle";
let commitAiGenerating = false;
let commitAiReviewing = false;
let commitAiSplitting = false;
@@ -369,7 +362,6 @@
let aiCommitSplitOpen = false;
let aiReviewResult: AiReviewResult | null = null;
let aiReviewOpen = false;
let commitAiPollTimer: ReturnType<typeof setInterval> | undefined;
let aiSettings: AiSettings = defaultAiSettings();
let aiSettingsOpen = false;
let appSettingsOpen = false;
@@ -387,7 +379,6 @@
let detectedExternalTools: DetectedExternalTool[] = [];
let externalToolsDetectionPending = true;
let externalToolsDetectionUnavailable = false;
let localModelOptions: LocalModelOption[] = [];
let errorMessage = "";
let operation = "";
let compareFrom = "";
@@ -628,7 +619,6 @@
if (autoRefreshTimer) { clearInterval(autoRefreshTimer); autoRefreshTimer = undefined; }
if (backgroundRepoStatusTimer) { clearInterval(backgroundRepoStatusTimer); backgroundRepoStatusTimer = undefined; }
if (backgroundFetchTimer) { clearInterval(backgroundFetchTimer); backgroundFetchTimer = undefined; }
if (commitAiPollTimer) { clearInterval(commitAiPollTimer); commitAiPollTimer = undefined; }
}
function wait(ms: number): Promise<void> {
@@ -681,7 +671,7 @@
loadRepoLists();
void checkForUpdates();
void initCommitAi();
aiSettings = loadAiSettings();
try {
await waitForStartupPaint();
@@ -1029,48 +1019,10 @@
// ── Commit AI ──────────────────────────────────────────────────────────────
function stopCommitAiPolling() {
if (commitAiPollTimer) { clearInterval(commitAiPollTimer); commitAiPollTimer = undefined; }
}
async function pollCommitAiStatus() {
try {
const result = await commitAiStatus();
commitAiPhase = result.phase;
} catch { /* ignore transient errors */ }
if (commitAiPhase === "ready" || commitAiPhase === "error") stopCommitAiPolling();
}
function startCommitAiPolling() {
// Only the local model has a download/load phase worth polling — cloud providers are
// plain API calls with nothing to wait for.
stopCommitAiPolling();
if (aiSettings.provider !== "local") return;
void pollCommitAiStatus();
commitAiPollTimer = setInterval(() => { void pollCommitAiStatus(); }, 2000);
}
async function initCommitAi() {
aiSettings = loadAiSettings();
try {
localModelOptions = await commitAiLocalModels();
} catch { /* AI features stay disabled if this fails; not fatal to the app */ }
if (aiSettings.provider === "local") {
try { await commitAiLoad(aiSettings.localModelId); } catch { /* surfaced via status polling */ }
}
startCommitAiPolling();
}
function saveAiSettings(next: AiSettings) {
const modelChanged = next.provider === "local" && next.localModelId !== aiSettings.localModelId;
aiSettings = next;
persistAiSettings(next);
aiSettingsOpen = false;
if (next.provider === "local" && (modelChanged || commitAiPhase === "idle")) {
commitAiPhase = "idle";
void commitAiLoad(next.localModelId);
}
startCommitAiPolling();
}
function defaultAnalyticsSettings(): AnalyticsSettings {
@@ -1324,27 +1276,15 @@
function updateCommitMessage(message: string) {
commitMessage = message;
if (message !== lastLocalAiGeneratedMessage) {
lastLocalAiGeneratedMessage = "";
}
}
async function generateCommitMessageWithAi() {
if (!activeRepoPath || commitAiGenerating) return;
if (aiSettings.provider === "local" && commitAiPhase !== "ready") return;
commitAiGenerating = true;
errorMessage = "";
try {
const notes = commitMessage.trim() || undefined;
if (aiSettings.provider === "local") {
const localNotes = notes && notes !== lastLocalAiGeneratedMessage ? notes : undefined;
commitMessage = await commitAiGenerate(activeRepoPath, {
provider: "local",
notes: localNotes,
localProfile: aiSettings.localProfile,
});
lastLocalAiGeneratedMessage = commitMessage;
} else if (aiSettings.provider === "openai") {
if (aiSettings.provider === "openai") {
const cred = await credLoad("ai:openai");
commitMessage = await commitAiGenerate(activeRepoPath, {
provider: "openai",
@@ -1352,7 +1292,6 @@
model: aiSettings.openaiModel,
apiKey: cred?.password,
});
lastLocalAiGeneratedMessage = "";
} else if (aiSettings.provider === "anthropic") {
const cred = await credLoad("ai:anthropic");
commitMessage = await commitAiGenerate(activeRepoPath, {
@@ -1361,7 +1300,6 @@
model: aiSettings.anthropicModel,
apiKey: cred?.password,
});
lastLocalAiGeneratedMessage = "";
} else {
const cred = await credLoad("ai:custom");
commitMessage = await commitAiGenerate(activeRepoPath, {
@@ -1371,7 +1309,6 @@
baseUrl: aiSettings.customBaseUrl,
apiKey: cred?.password,
});
lastLocalAiGeneratedMessage = "";
}
} catch (error) {
errorMessage = errorToMessage(error);
@@ -1382,10 +1319,6 @@
async function reviewStagedWithAi() {
if (!activeRepoPath || commitAiReviewing || stagedCount === 0) return;
if (aiSettings.provider === "local") {
errorMessage = "Pre-commit review currently requires OpenAI, Anthropic, or a custom endpoint.";
return;
}
commitAiReviewing = true;
errorMessage = "";
try {
@@ -1427,10 +1360,6 @@
async function splitStagedWithAi() {
if (!activeRepoPath || commitAiSplitting || stagedCount < 2) return;
if (aiSettings.provider === "local") {
errorMessage = "Commit splitting currently requires OpenAI, Anthropic, or a custom endpoint.";
return;
}
commitAiSplitting = true;
errorMessage = "";
try {
@@ -1735,8 +1664,6 @@
function defaultAiSettings(): AiSettings {
return {
provider: "openai",
localModelId: "qwen2.5-0.5b",
localProfile: "fast",
openaiModel: "gpt-4o-mini",
anthropicModel: "claude-3-5-haiku-latest",
customBaseUrl: "",
@@ -1748,11 +1675,17 @@
try {
const stored = JSON.parse(localStorage.getItem(AI_SETTINGS_KEY) ?? "null") as unknown;
if (stored && typeof stored === "object") {
const merged = { ...defaultAiSettings(), ...(stored as Partial<AiSettings>) };
// Local AI is still in development and disabled in the settings UI — migrate any
// previously saved selection away from it so nobody gets stuck on a dead option.
if (merged.provider === "local") merged.provider = "openai";
return merged;
const candidate = stored as Partial<AiSettings> & { provider?: unknown };
const provider = candidate.provider === "anthropic" || candidate.provider === "custom"
? candidate.provider
: "openai";
return {
provider,
openaiModel: typeof candidate.openaiModel === "string" ? candidate.openaiModel : "gpt-4o-mini",
anthropicModel: typeof candidate.anthropicModel === "string" ? candidate.anthropicModel : "claude-3-5-haiku-latest",
customBaseUrl: typeof candidate.customBaseUrl === "string" ? candidate.customBaseUrl : "",
customModel: typeof candidate.customModel === "string" ? candidate.customModel : "",
};
}
} catch {
// Fall through to defaults below.
@@ -4385,7 +4318,6 @@
commitMessage = "";
amendMode = false;
preAmendDraftMessage = "";
lastLocalAiGeneratedMessage = "";
await refreshRepositoryViews(activeRepoPath);
trackEvent("commit_created", { amend: 1 });
});
@@ -4396,7 +4328,6 @@
await runOperation("Committing", async () => {
applyStatus(await commit(activeRepoPath, message));
commitMessage = "";
lastLocalAiGeneratedMessage = "";
await refreshRepositoryViews(activeRepoPath);
trackEvent("commit_created", { amend: 0, staged_files: trackedStagedCount });
});
@@ -5617,8 +5548,6 @@
{isBusy}
{operation}
{stagedCount}
commitAiProvider={aiSettings.provider}
{commitAiPhase}
{commitAiGenerating}
{commitAiReviewing}
{commitAiSplitting}
@@ -5983,7 +5912,6 @@
{#await import("./lib/components/AiSettingsDialog.svelte") then module}
<module.default
settings={aiSettings}
localModels={localModelOptions}
onSave={saveAiSettings}
onClose={() => { aiSettingsOpen = false; }}
/>