feat(ai): disable local provider and improve commit file display

Local AI is now treated as unavailable in the settings UI, with a
migration to ensure any previously saved "local" selection switches
back to OpenAI. The history panel also improves how commit file
names are presented, including clearer old->new path formatting.

- Migrate stored AI provider away from local to prevent dead state
- Disable local provider option with an "in development" badge
- Refine history panel filename rendering and tooltip context
This commit is contained in:
Christoph Brandau
2026-07-03 07:26:23 +02:00
parent 791d686c48
commit 979d5aed80
5 changed files with 54 additions and 13 deletions
+15 -8
View File
@@ -413,20 +413,27 @@ do not include lines starting with 'diff --git', '@@', '+', '-', 'index ', 'Stag
or 'Diff stat:' anywhere in your answer. \ or 'Diff stat:' anywhere in your answer. \
Format: a Conventional Commits header (<type>(<scope>): <subject>) in imperative mood, \ Format: a Conventional Commits header (<type>(<scope>): <subject>) in imperative mood, \
max. 72 characters, then a blank line, then a body. \ max. 72 characters, then a blank line, then a body. \
The body is required: one short paragraph explaining what changed and why, \ The body is required: a short, general paragraph (2-4 sentences) summarizing what changed \
then bullet points (- ...) of the key changes grouped by affected area/file. \ and why at a high level — do NOT enumerate every changed file individually. \
You may optionally add up to 3 bullet points (- ...) afterward, but only for the most \
significant changes overall, never one bullet or heading per file. \
Never use bold text, backticks, or markdown headings for file names. \
Lines in the body max. 72 characters. \ Lines in the body max. 72 characters. \
No preamble, no explanation, no code fences, no markdown headings, answer in English.\n\n\ No preamble, no explanation, no code fences, answer in English.\n\n\
Example:\n\ Example:\n\
Diff:\n\ Diff:\n\
diff --git a/src/auth.py b/src/auth.py\n\ diff --git a/src/auth.py b/src/auth.py\n\
+def hash_password(pw):\n\ +def hash_password(pw):\n\
+ return bcrypt.hash(pw)\n\n\ + return bcrypt.hash(pw)\n\
diff --git a/src/routes.py b/src/routes.py\n\
-if password == stored_password:\n\
+if bcrypt.check(password, stored_password):\n\n\
Commit message:\n\ Commit message:\n\
feat(auth): add password hashing helper\n\n\ feat(auth): hash and verify passwords with bcrypt\n\n\
Add a bcrypt-based helper so passwords are never stored or compared in\n\ Passwords were previously compared as plain text. This adds a bcrypt-based\n\
plain text.\n\n\ hashing helper and updates the login check to verify against the hash\n\
- auth.py: add hash_password() using bcrypt" instead of a direct string comparison.\n\n\
- Hash passwords on write, verify with bcrypt on login"
.to_string(); .to_string();
let mut user = String::new(); let mut user = String::new();
+6 -2
View File
@@ -543,7 +543,7 @@
function defaultAiSettings(): AiSettings { function defaultAiSettings(): AiSettings {
return { return {
provider: "local", provider: "openai",
localModelId: "qwen2.5-0.5b", localModelId: "qwen2.5-0.5b",
localProfile: "fast", localProfile: "fast",
openaiModel: "gpt-4o-mini", openaiModel: "gpt-4o-mini",
@@ -557,7 +557,11 @@
try { try {
const stored = JSON.parse(localStorage.getItem(AI_SETTINGS_KEY) ?? "null") as unknown; const stored = JSON.parse(localStorage.getItem(AI_SETTINGS_KEY) ?? "null") as unknown;
if (stored && typeof stored === "object") { if (stored && typeof stored === "object") {
return { ...defaultAiSettings(), ...(stored as Partial<AiSettings>) }; 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;
} }
} catch { } catch {
// Fall through to defaults below. // Fall through to defaults below.
+13
View File
@@ -1721,6 +1721,19 @@
color: #f5f7ff; color: #f5f7ff;
background: linear-gradient(180deg, rgba(100,108,255,0.22), rgba(65,209,255,0.1)); background: linear-gradient(180deg, rgba(100,108,255,0.22), rgba(65,209,255,0.1));
} }
.ai-provider-option-local {
flex-wrap: wrap;
row-gap: 2px;
}
.ai-provider-badge {
flex-basis: 100%;
text-align: center;
font-size: 9.5px;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--color-ink-faint);
}
.ai-local-profile-options { .ai-local-profile-options {
display: grid; display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr)); grid-template-columns: repeat(3, minmax(0, 1fr));
+8 -1
View File
@@ -141,9 +141,16 @@
<form class="ai-settings-form" onsubmit={(e) => { e.preventDefault(); void handleSave(); }}> <form class="ai-settings-form" onsubmit={(e) => { e.preventDefault(); void handleSave(); }}>
<div class="ai-provider-options" role="radiogroup" aria-label="AI provider"> <div class="ai-provider-options" role="radiogroup" aria-label="AI provider">
<button type="button" class="ai-provider-option" class:active={provider === "local"} onclick={() => { provider = "local"; }}> <button
type="button"
class="ai-provider-option ai-provider-option-local"
class:active={provider === "local"}
disabled
title="Local AI is still in development and not yet available"
>
<Cpu size={16} aria-hidden="true" /> <Cpu size={16} aria-hidden="true" />
Local AI Local AI
<span class="ai-provider-badge">In development</span>
</button> </button>
<button type="button" class="ai-provider-option" class:active={provider === "openai"} onclick={() => { provider = "openai"; }}> <button type="button" class="ai-provider-option" class:active={provider === "openai"} onclick={() => { provider = "openai"; }}>
<Bot size={16} aria-hidden="true" /> <Bot size={16} aria-hidden="true" />
+12 -2
View File
@@ -113,6 +113,16 @@
return file.old_path ? `${file.old_path} -> ${file.path}` : file.path; return file.old_path ? `${file.old_path} -> ${file.path}` : file.path;
} }
function baseName(path: string): string {
return path.split(/[\\/]/).filter(Boolean).pop() ?? path;
}
function commitFileName(file: GitCommitFile): string {
return file.old_path
? `${baseName(file.old_path)} -> ${baseName(file.path)}`
: baseName(file.path);
}
function formatCommitDate(value: string): string { function formatCommitDate(value: string): string {
const date = new Date(value); const date = new Date(value);
if (Number.isNaN(date.getTime())) return value; if (Number.isNaN(date.getTime())) return value;
@@ -208,10 +218,10 @@
type="button" type="button"
onclick={() => onPreviewCommitFile(item, file)} onclick={() => onPreviewCommitFile(item, file)}
disabled={isBusy} disabled={isBusy}
title="Show differences before restoring" title={`Show differences before restoring - ${displayCommitFile(file)}`}
> >
<span class={`status-badge ${file.status}`}>{statusLabel(file.status)}</span> <span class={`status-badge ${file.status}`}>{statusLabel(file.status)}</span>
<strong>{displayCommitFile(file)}</strong> <strong>{commitFileName(file)}</strong>
</button> </button>
{/each} {/each}
</div> </div>