Skip to content

Commit 303facb

Browse files
7418claude
andcommitted
fix: v0.38 regression fixes — stale state, perf, status, model chain
Bucket 1: Windows stale localStorage crash (#300) - chat/page.tsx: add one-time migration (codepilot:migration-038) that clears stale last-model/last-provider-id on upgrade from <0.38 - chat/page.tsx: startup useEffect validates restored provider/model against actual available data, falls back gracefully - chat/page.tsx: provider-changed handler re-validates from API Bucket 2: Mac chat slowness — Generative UI overhead (#299) - widget-guidelines.ts: add WIDGET_SYSTEM_PROMPT_HINT (~180 chars) to replace full WIDGET_SYSTEM_PROMPT (~1000 chars) in system prompt - chat/route.ts: inject hint instead of full prompt — model loads full guidelines on-demand via codepilot-widget MCP tool - widget-guidelines.ts: MCP tool response now includes format rules so nothing is lost when system prompt is shortened - ~85% reduction in per-request system prompt overhead Bucket 3: Windows Claude status false negative - claude-status/route.ts: connected = !!version (not gated on Git Bash) - claude-status/route.ts: add warnings[] array for non-blocking issues - ConnectionStatus.tsx: Git Bash missing shows warning-yellow badge instead of error-red disconnected - ClaudeCodeCard.tsx: updated type for warnings field Bucket 4: Provider/model chain gaps (#301, #296) - providers/models/route.ts: synthesize model entries from all role_models_json fields (default/reasoning/small/haiku/sonnet/opus) so they appear in the chat picker - provider-doctor.ts: new finding provider.no-models warns when a provider has zero available models Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 0ae3485 commit 303facb

File tree

9 files changed

+232
-61
lines changed

9 files changed

+232
-61
lines changed

src/__tests__/unit/widget-system.test.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import {
2727
} from '../../components/chat/MessageItem';
2828

2929
import { WIDGET_CSS_BRIDGE } from '../../lib/widget-css-bridge';
30-
import { WIDGET_SYSTEM_PROMPT, getGuidelines, createWidgetMcpServer } from '../../lib/widget-guidelines';
30+
import { WIDGET_SYSTEM_PROMPT, WIDGET_SYSTEM_PROMPT_HINT, getGuidelines, createWidgetMcpServer } from '../../lib/widget-guidelines';
3131

3232
// ── Sanitization ────────────────────────────────────────────────────────
3333

@@ -297,7 +297,23 @@ describe('WIDGET_CSS_BRIDGE', () => {
297297
});
298298
});
299299

300-
// ── System prompt ───────────────────────────────────────────────────────
300+
// ── System prompt hint (injected into system prompt) ────────────────────
301+
302+
describe('WIDGET_SYSTEM_PROMPT_HINT', () => {
303+
it('references the codepilot_load_widget_guidelines tool', () => {
304+
assert.ok(WIDGET_SYSTEM_PROMPT_HINT.includes('codepilot_load_widget_guidelines'));
305+
});
306+
307+
it('is ultra-minimal to reduce per-request token overhead', () => {
308+
assert.ok(WIDGET_SYSTEM_PROMPT_HINT.length < 250, `hint should be <250 chars, got ${WIDGET_SYSTEM_PROMPT_HINT.length}`);
309+
});
310+
311+
it('mentions widget capability', () => {
312+
assert.ok(WIDGET_SYSTEM_PROMPT_HINT.includes('widget-capability'));
313+
});
314+
});
315+
316+
// ── Full system prompt (loaded on demand via MCP tool) ──────────────────
301317

302318
describe('WIDGET_SYSTEM_PROMPT', () => {
303319
it('includes show-widget fence format', () => {

src/app/api/chat/route.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -330,13 +330,15 @@ Start by greeting the user and asking the first question.
330330
// CLI tools context injection failed — don't block chat
331331
}
332332

333-
// Inject widget (generative UI) system prompt — gated by user setting (default: enabled)
333+
// Generative UI: gated by user setting (default: enabled).
334+
// Only a short hint is injected into the system prompt — full guidelines
335+
// are loaded on-demand via the codepilot-widget MCP tool to save tokens.
334336
const generativeUISetting = getSetting('generative_ui_enabled');
335337
const generativeUIEnabled = generativeUISetting !== 'false';
336338
if (generativeUIEnabled) {
337339
try {
338-
const { WIDGET_SYSTEM_PROMPT } = await import('@/lib/widget-guidelines');
339-
finalSystemPrompt = (finalSystemPrompt || '') + '\n\n' + WIDGET_SYSTEM_PROMPT;
340+
const { WIDGET_SYSTEM_PROMPT_HINT } = await import('@/lib/widget-guidelines');
341+
finalSystemPrompt = (finalSystemPrompt || '') + '\n\n' + WIDGET_SYSTEM_PROMPT_HINT;
340342
} catch {
341343
// Widget prompt injection failed — don't block chat
342344
}

src/app/api/claude-status/route.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,9 @@ export async function GET() {
3131
const missingGit = isWindows && findGitBash() === null;
3232

3333
if (!claudePath) {
34-
return NextResponse.json({ connected: false, version: null, binaryPath: null, installType: null, otherInstalls: [], missingGit, features: {} });
34+
const w: string[] = [];
35+
if (missingGit) w.push('Git Bash not found — some features may not work');
36+
return NextResponse.json({ connected: false, version: null, binaryPath: null, installType: null, otherInstalls: [], missingGit, warnings: w, features: {} });
3537
}
3638
const version = await getClaudeVersion(claudePath);
3739
const installType = classifyClaudePath(claudePath);
@@ -53,17 +55,28 @@ export async function GET() {
5355
}
5456
}
5557

58+
// Build warnings array for non-blocking issues
59+
const warnings: string[] = [];
60+
if (missingGit) {
61+
warnings.push('Git Bash not found — some features may not work');
62+
}
63+
if (otherInstalls.length > 0) {
64+
warnings.push(`${otherInstalls.length} other Claude CLI installation(s) detected`);
65+
}
66+
5667
return NextResponse.json({
57-
// If Git Bash is missing on Windows, Claude is installed but not usable
58-
connected: !!version && !missingGit,
68+
// connected = CLI found and returns a version. Git Bash missing is a
69+
// warning, not a blocker — the CLI itself is still usable for basic ops.
70+
connected: !!version,
5971
version,
6072
binaryPath: claudePath,
6173
installType,
6274
otherInstalls,
6375
missingGit,
76+
warnings,
6477
features,
6578
});
6679
} catch {
67-
return NextResponse.json({ connected: false, version: null, binaryPath: null, installType: null, otherInstalls: [], missingGit: false, features: {} });
80+
return NextResponse.json({ connected: false, version: null, binaryPath: null, installType: null, otherInstalls: [], missingGit: false, warnings: [], features: {} });
6881
}
6982
}

src/app/api/providers/models/route.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -138,12 +138,23 @@ export async function GET() {
138138
rawModels = [...catalogRaw];
139139
}
140140

141-
// Inject role_models_json.default into the list if not already present
141+
// Inject models from role_models_json into the list if not already present
142142
// (e.g. user configured "ark-code-latest" for a Volcengine or anthropic-thirdparty provider)
143143
try {
144144
const rm = JSON.parse(provider.role_models_json || '{}');
145-
if (rm.default && !rawModels.some(m => m.value === rm.default)) {
146-
rawModels.unshift({ value: rm.default, label: rm.default });
145+
// Collect unique model IDs from all role fields (default, reasoning, small, haiku, sonnet, opus)
146+
const roleEntries: { id: string; role: string }[] = [];
147+
for (const role of ['default', 'reasoning', 'small', 'haiku', 'sonnet', 'opus'] as const) {
148+
if (rm[role] && !roleEntries.some(e => e.id === rm[role])) {
149+
roleEntries.push({ id: rm[role], role });
150+
}
151+
}
152+
// Add each role model to the list (default role first, so it appears at the top)
153+
for (const entry of roleEntries) {
154+
if (!rawModels.some(m => m.value === entry.id)) {
155+
const label = entry.role === 'default' ? entry.id : `${entry.id} (${entry.role})`;
156+
rawModels.unshift({ value: entry.id, label });
157+
}
147158
}
148159
} catch { /* ignore */ }
149160

src/app/chat/page.tsx

Lines changed: 90 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -44,16 +44,25 @@ export default function NewChatPage() {
4444
const [recentProjects, setRecentProjects] = useState<string[]>([]);
4545
const [hasProvider, setHasProvider] = useState(true); // assume true until checked
4646
const [mode] = useState('code');
47-
const [currentModel, setCurrentModel] = useState(() =>
48-
typeof window !== 'undefined'
49-
? localStorage.getItem('codepilot:last-model') || 'sonnet'
50-
: 'sonnet'
51-
);
52-
const [currentProviderId, setCurrentProviderId] = useState(() =>
53-
typeof window !== 'undefined'
54-
? localStorage.getItem('codepilot:last-provider-id') || ''
55-
: ''
56-
);
47+
const [currentModel, setCurrentModel] = useState(() => {
48+
if (typeof window === 'undefined') return 'sonnet';
49+
// One-time migration: clear stale model/provider from pre-0.38 installs
50+
if (!localStorage.getItem('codepilot:migration-038')) {
51+
localStorage.removeItem('codepilot:last-model');
52+
localStorage.removeItem('codepilot:last-provider-id');
53+
localStorage.setItem('codepilot:migration-038', '1');
54+
return 'sonnet';
55+
}
56+
return localStorage.getItem('codepilot:last-model') || 'sonnet';
57+
});
58+
const [currentProviderId, setCurrentProviderId] = useState(() => {
59+
if (typeof window === 'undefined') return '';
60+
// Migration already ran above (or was already done), just read
61+
if (!localStorage.getItem('codepilot:migration-038')) {
62+
return '';
63+
}
64+
return localStorage.getItem('codepilot:last-provider-id') || '';
65+
});
5766
const [pendingPermission, setPendingPermission] = useState<PermissionRequestEvent | null>(null);
5867
const [permissionResolved, setPermissionResolved] = useState<'allow' | 'deny' | null>(null);
5968
const [streamingToolOutput, setStreamingToolOutput] = useState('');
@@ -82,6 +91,38 @@ export default function NewChatPage() {
8291
return () => controller.abort();
8392
}, [currentProviderId]);
8493

94+
// Validate restored model/provider against actual available providers/models
95+
useEffect(() => {
96+
let cancelled = false;
97+
fetch('/api/providers/models')
98+
.then(r => r.ok ? r.json() : null)
99+
.then(data => {
100+
if (cancelled || !data?.groups || data.groups.length === 0) return;
101+
const groups = data.groups as Array<{ provider_id: string; models: Array<{ value: string }> }>;
102+
103+
// Validate provider
104+
const validProvider = groups.find(g => g.provider_id === currentProviderId);
105+
if (currentProviderId && !validProvider) {
106+
setCurrentProviderId('');
107+
localStorage.removeItem('codepilot:last-provider-id');
108+
}
109+
110+
// Validate model against the resolved provider's model list
111+
const resolvedGroup = validProvider || groups[0];
112+
if (resolvedGroup?.models && resolvedGroup.models.length > 0) {
113+
const validModel = resolvedGroup.models.find(m => m.value === currentModel);
114+
if (!validModel) {
115+
const fallback = resolvedGroup.models[0].value;
116+
setCurrentModel(fallback);
117+
localStorage.setItem('codepilot:last-model', fallback);
118+
}
119+
}
120+
})
121+
.catch(() => {});
122+
return () => { cancelled = true; };
123+
// eslint-disable-next-line react-hooks/exhaustive-deps
124+
}, []); // Run once on mount to validate initial values
125+
85126
// Initialize workingDir from localStorage (or setup default), validating the path exists
86127
useEffect(() => {
87128
let cancelled = false;
@@ -155,11 +196,47 @@ export default function NewChatPage() {
155196
}
156197
})
157198
.catch(() => {});
158-
// Sync provider/model from localStorage when provider changes
199+
// Sync provider/model from localStorage, validating against available providers
159200
const savedProviderId = localStorage.getItem('codepilot:last-provider-id');
160201
const savedModel = localStorage.getItem('codepilot:last-model');
161-
if (savedProviderId !== null) setCurrentProviderId(savedProviderId);
162-
if (savedModel) setCurrentModel(savedModel);
202+
fetch('/api/providers/models')
203+
.then(r => r.ok ? r.json() : null)
204+
.then(data => {
205+
if (!data?.groups || data.groups.length === 0) return;
206+
const groups = data.groups as Array<{ provider_id: string; models: Array<{ value: string }> }>;
207+
208+
// Validate and apply provider
209+
if (savedProviderId !== null) {
210+
const validProvider = groups.find(g => g.provider_id === savedProviderId);
211+
if (validProvider) {
212+
setCurrentProviderId(savedProviderId);
213+
} else {
214+
setCurrentProviderId('');
215+
localStorage.removeItem('codepilot:last-provider-id');
216+
}
217+
}
218+
219+
// Validate and apply model
220+
const resolvedPid = savedProviderId && groups.find(g => g.provider_id === savedProviderId)
221+
? savedProviderId
222+
: groups[0]?.provider_id || '';
223+
const resolvedGroup = groups.find(g => g.provider_id === resolvedPid) || groups[0];
224+
if (savedModel && resolvedGroup?.models?.length > 0) {
225+
const validModel = resolvedGroup.models.find((m: { value: string }) => m.value === savedModel);
226+
if (validModel) {
227+
setCurrentModel(savedModel);
228+
} else {
229+
const fallback = resolvedGroup.models[0].value;
230+
setCurrentModel(fallback);
231+
localStorage.setItem('codepilot:last-model', fallback);
232+
}
233+
}
234+
})
235+
.catch(() => {
236+
// On fetch failure, still apply localStorage values as-is (best effort)
237+
if (savedProviderId !== null) setCurrentProviderId(savedProviderId);
238+
if (savedModel) setCurrentModel(savedModel);
239+
});
163240
};
164241
checkProvider();
165242

src/components/layout/ConnectionStatus.tsx

Lines changed: 33 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ interface ClaudeStatus {
2929
installType?: string | null;
3030
otherInstalls?: ClaudeInstallInfo[];
3131
missingGit?: boolean;
32+
warnings?: string[];
3233
}
3334

3435
const BASE_INTERVAL = 30_000; // 30s
@@ -155,6 +156,7 @@ export function ConnectionStatus() {
155156
const connected = status?.connected ?? false;
156157
const hasConflicts = (status?.otherInstalls?.length ?? 0) > 0;
157158
const missingGit = status?.missingGit ?? false;
159+
const hasWarnings = hasConflicts || missingGit;
158160

159161
return (
160162
<>
@@ -167,7 +169,7 @@ export function ConnectionStatus() {
167169
status === null
168170
? "bg-muted text-muted-foreground"
169171
: connected
170-
? hasConflicts
172+
? hasWarnings
171173
? "bg-status-warning-muted text-status-warning-foreground"
172174
: "bg-status-success-muted text-status-success-foreground"
173175
: "bg-status-error-muted text-status-error-foreground"
@@ -179,7 +181,7 @@ export function ConnectionStatus() {
179181
status === null
180182
? "bg-muted-foreground/40"
181183
: connected
182-
? hasConflicts
184+
? hasWarnings
183185
? "bg-status-warning"
184186
: "bg-status-success"
185187
: "bg-status-error"
@@ -188,30 +190,30 @@ export function ConnectionStatus() {
188190
{status === null
189191
? t('connection.checking')
190192
: connected
191-
? hasConflicts
192-
? t('connection.conflict')
193-
: t('connection.connected')
194-
: missingGit
193+
? missingGit
195194
? t('connection.missingGit')
196-
: t('connection.disconnected')}
195+
: hasConflicts
196+
? t('connection.conflict')
197+
: t('connection.connected')
198+
: t('connection.disconnected')}
197199
</Button>
198200

199201
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
200202
<DialogContent className="sm:max-w-md">
201203
<DialogHeader>
202204
<DialogTitle>
203205
{connected
204-
? t('connection.installed')
205-
: missingGit
206+
? missingGit
206207
? t('connection.missingGitTitle')
207-
: t('connection.notInstalled')}
208+
: t('connection.installed')
209+
: t('connection.notInstalled')}
208210
</DialogTitle>
209211
<DialogDescription>
210212
{connected
211-
? `Claude Code CLI v${status?.version} is running and ready.`
212-
: missingGit
213+
? missingGit
213214
? t('connection.missingGitDesc')
214-
: "Claude Code CLI is required to use this application."}
215+
: `Claude Code CLI v${status?.version} is running and ready.`
216+
: "Claude Code CLI is required to use this application."}
215217
</DialogDescription>
216218
</DialogHeader>
217219

@@ -258,25 +260,25 @@ export function ConnectionStatus() {
258260
</div>
259261
</div>
260262
)}
261-
</div>
262-
) : missingGit ? (
263-
<div className="space-y-4 text-sm">
264-
<div className="flex items-center gap-3 rounded-lg bg-status-warning-muted px-4 py-3">
265-
<Warning size={16} className="text-status-warning-foreground shrink-0" />
266-
<div>
267-
<p className="font-medium text-status-warning-foreground">{t('connection.missingGitTitle')}</p>
268-
{status?.version && (
269-
<p className="text-xs text-muted-foreground">Claude Code v{status.version} is installed but cannot run without Git.</p>
270-
)}
263+
264+
{/* Git Bash missing warning (Windows only) */}
265+
{missingGit && (
266+
<div className="rounded-lg bg-status-warning-muted px-4 py-3 space-y-2">
267+
<div className="flex items-center gap-2">
268+
<Warning size={16} className="text-status-warning-foreground shrink-0" />
269+
<p className="font-medium text-status-warning-foreground text-xs">
270+
{t('connection.missingGitTitle')}
271+
</p>
272+
</div>
273+
<div className="text-xs text-muted-foreground space-y-1">
274+
<ol className="list-decimal list-inside space-y-0.5">
275+
<li>{t('install.gitStep1')}</li>
276+
<li>{t('install.gitStep2')}</li>
277+
<li>{t('install.gitStep3')}</li>
278+
</ol>
279+
</div>
271280
</div>
272-
</div>
273-
<div className="text-sm text-muted-foreground space-y-1">
274-
<ol className="list-decimal list-inside space-y-0.5">
275-
<li>{t('install.gitStep1')}</li>
276-
<li>{t('install.gitStep2')}</li>
277-
<li>{t('install.gitStep3')}</li>
278-
</ol>
279-
</div>
281+
)}
280282
</div>
281283
) : (
282284
<div className="space-y-4 text-sm">

src/components/setup/ClaudeCodeCard.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ interface ClaudeStatus {
3131
installType?: string | null;
3232
otherInstalls?: Array<{ path: string; version: string | null; type: string }>;
3333
missingGit?: boolean;
34+
warnings?: string[];
3435
}
3536

3637
interface ClaudeCodeCardProps {

0 commit comments

Comments
 (0)