fix(ui): add provider indicator, retry button, and optimistic locking

- U4: add blue info box showing supported providers

- W1: add inline retry button in error alert

- W4: track lastModified, handle 409 conflict with auto-refresh
This commit is contained in:
kaitranntt
2026-01-08 16:56:05 -05:00
parent ba19e1fcda
commit 35f28a6e77
2 changed files with 54 additions and 6 deletions
@@ -1,9 +1,10 @@
/** /**
* Thinking Config Hook * Thinking Config Hook
* Manages thinking budget configuration with direct API calls * Manages thinking budget configuration with direct API calls
* Includes W4 optimistic locking via lastModified timestamps
*/ */
import { useCallback, useState } from 'react'; import { useCallback, useState, useRef } from 'react';
import type { ThinkingConfig, ThinkingMode } from '../types'; import type { ThinkingConfig, ThinkingMode } from '../types';
const DEFAULT_THINKING_CONFIG: ThinkingConfig = { const DEFAULT_THINKING_CONFIG: ThinkingConfig = {
@@ -24,6 +25,8 @@ export function useThinkingConfig() {
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState(false); const [success, setSuccess] = useState(false);
// W4: Track lastModified for optimistic locking
const lastModifiedRef = useRef<number | undefined>(undefined);
const fetchConfig = useCallback(async () => { const fetchConfig = useCallback(async () => {
const controller = new AbortController(); const controller = new AbortController();
@@ -37,6 +40,8 @@ export function useThinkingConfig() {
if (!res.ok) throw new Error('Failed to load Thinking config'); if (!res.ok) throw new Error('Failed to load Thinking config');
const data = await res.json(); const data = await res.json();
setConfig(data.config || DEFAULT_THINKING_CONFIG); setConfig(data.config || DEFAULT_THINKING_CONFIG);
// W4: Store lastModified for optimistic locking
lastModifiedRef.current = data.lastModified;
} catch (err) { } catch (err) {
clearTimeout(timeoutId); clearTimeout(timeoutId);
if ((err as Error).name === 'AbortError') { if ((err as Error).name === 'AbortError') {
@@ -63,10 +68,16 @@ export function useThinkingConfig() {
setSaving(true); setSaving(true);
setError(null); setError(null);
// W4: Include lastModified for optimistic locking
const payload = {
...optimisticConfig,
lastModified: lastModifiedRef.current,
};
const res = await fetch('/api/thinking', { const res = await fetch('/api/thinking', {
method: 'PUT', method: 'PUT',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(optimisticConfig), body: JSON.stringify(payload),
signal: controller.signal, signal: controller.signal,
}); });
@@ -74,11 +85,17 @@ export function useThinkingConfig() {
if (!res.ok) { if (!res.ok) {
const data = await res.json(); const data = await res.json();
// W4: Handle conflict (409) with user-friendly message
if (res.status === 409) {
throw new Error('Config changed by another session. Refreshing...');
}
throw new Error(data.error || 'Failed to save'); throw new Error(data.error || 'Failed to save');
} }
const data = await res.json(); const data = await res.json();
setConfig(data.config); setConfig(data.config);
// W4: Update lastModified after successful save
lastModifiedRef.current = data.lastModified;
setSuccess(true); setSuccess(true);
setTimeout(() => setSuccess(false), 1500); setTimeout(() => setSuccess(false), 1500);
} catch (err) { } catch (err) {
@@ -88,12 +105,16 @@ export function useThinkingConfig() {
setError('Request timeout - please try again'); setError('Request timeout - please try again');
} else { } else {
setError((err as Error).message); setError((err as Error).message);
// W4: On conflict, auto-refresh to get latest
if ((err as Error).message.includes('another session')) {
setTimeout(() => fetchConfig(), 1000);
}
} }
} finally { } finally {
setSaving(false); setSaving(false);
} }
}, },
[config] [config, fetchConfig]
); );
const setMode = useCallback( const setMode = useCallback(
@@ -16,7 +16,7 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from '@/components/ui/select'; } from '@/components/ui/select';
import { RefreshCw, CheckCircle2, AlertCircle, Brain } from 'lucide-react'; import { RefreshCw, CheckCircle2, AlertCircle, Brain, Info } from 'lucide-react';
import { useThinkingConfig } from '../../hooks'; import { useThinkingConfig } from '../../hooks';
import type { ThinkingMode } from '../../types'; import type { ThinkingMode } from '../../types';
@@ -69,8 +69,22 @@ export default function ThinkingSection() {
> >
{error && ( {error && (
<Alert variant="destructive" className="py-2 shadow-lg"> <Alert variant="destructive" className="py-2 shadow-lg">
<AlertCircle className="h-4 w-4" /> <div className="flex items-center justify-between w-full">
<AlertDescription>{error}</AlertDescription> <div className="flex items-center gap-2">
<AlertCircle className="h-4 w-4" />
<AlertDescription>{error}</AlertDescription>
</div>
{/* W1: Retry button on error */}
<Button
variant="outline"
size="sm"
onClick={fetchConfig}
className="h-7 px-2 text-xs border-destructive/50 hover:bg-destructive/10"
>
<RefreshCw className="w-3 h-3 mr-1" />
Retry
</Button>
</div>
</Alert> </Alert>
)} )}
{success && ( {success && (
@@ -91,6 +105,19 @@ export default function ThinkingSection() {
</p> </p>
</div> </div>
{/* U4: Provider support indicator */}
<div className="flex items-start gap-2 p-3 rounded-lg bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800">
<Info className="w-4 h-4 text-blue-600 dark:text-blue-400 shrink-0 mt-0.5" />
<div className="text-sm text-blue-700 dark:text-blue-300">
<p className="font-medium">Supported Providers</p>
<p className="mt-1 text-blue-600 dark:text-blue-400">
Thinking budget works with: <strong>agy</strong> (Antigravity),{' '}
<strong>gemini</strong> (with thinking models). Other providers may ignore this
setting.
</p>
</div>
</div>
{/* Mode Selection */} {/* Mode Selection */}
<div className="space-y-3"> <div className="space-y-3">
<h3 className="text-base font-medium">Thinking Mode</h3> <h3 className="text-base font-medium">Thinking Mode</h3>