server settings keyed (352 -> 267)
ServerSettings reaches zero: OCR, STT, TTS, SMTP, maintenance and the AI harnesses. Two more hand-rolled plurals became real plural entries -- maintenance said "commit" with a ternary on length === 1, and both read correctly at 0, 1 and 2 in both locales now. The three Cancel buttons in AIHarnessesSection take ui.button.cancel rather than adding a fourth app-level copy of the word, which is what phase 1 put it there for. Scanner: a single token carrying digits and punctuation is a name, not a sentence -- the OCR placeholder is the example model file Qwen2.5-VL-7B-Instruct-q4_k_m.gguf. Prose has spaces, and requiring whitespace to be absent is what keeps this from eating real one-word labels. Verified the delta is exactly that one string. Note for the owner: another session has src/servers/api/sub-agents/router.ts in flight with three type errors. Untouched by me and excluded from my gate; the full tree will not typecheck clean until they land it.
This commit is contained in:
+44
-32
@@ -13,6 +13,7 @@ import {
|
||||
CommandList,
|
||||
} from '@/components/ui/command';
|
||||
import { useClient } from 'hooks/useClient';
|
||||
import { useTranslation } from '@/lib/i18n';
|
||||
|
||||
type VersionInfo = { version: string | null; path: string | null; globalPath: string | null };
|
||||
type StoredApiKeys = { keys: { provider: string; value: string }[] };
|
||||
@@ -38,6 +39,7 @@ type ChatProvider = { key: string; providerId: string };
|
||||
type ProbeState = { step: 'url' } | { step: 'probing' } | { step: 'auth'; probe: ProbeResult } | { step: 'saving' };
|
||||
|
||||
export const AIHarnessesSection = () => {
|
||||
const { t, tUi } = useTranslation();
|
||||
const client = useClient();
|
||||
const queryClient = useQueryClient();
|
||||
const [installing, setInstalling] = useState(false);
|
||||
@@ -168,7 +170,7 @@ export const AIHarnessesSection = () => {
|
||||
// No auth needed — save directly
|
||||
await saveLocalProvider(result, auth);
|
||||
} catch {
|
||||
toast.error('Failed to connect');
|
||||
toast.error(t('settings.server.harnesses.connectFailed'));
|
||||
setProbeState({ step: 'url' });
|
||||
}
|
||||
};
|
||||
@@ -192,13 +194,13 @@ export const AIHarnessesSection = () => {
|
||||
return;
|
||||
}
|
||||
if (result.needsAuth) {
|
||||
toast.error('Authentication failed');
|
||||
toast.error(t('settings.server.harnesses.authFailed'));
|
||||
setProbeState({ step: 'auth', probe });
|
||||
return;
|
||||
}
|
||||
await saveLocalProvider(result, auth);
|
||||
} catch {
|
||||
toast.error('Failed to connect');
|
||||
toast.error(t('settings.server.harnesses.connectFailed'));
|
||||
setProbeState({ step: 'auth', probe });
|
||||
}
|
||||
};
|
||||
@@ -218,10 +220,10 @@ export const AIHarnessesSection = () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['CHAT_PROVIDERS_LOCAL_PROVIDERS'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['CHAT_PROVIDERS_LOCAL_HEALTH'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['CHAT_MODELS'] });
|
||||
toast.success(`Connected to ${probe.name}`);
|
||||
toast.success(t('settings.server.harnesses.connectedTo', { name: probe.name ?? '' }));
|
||||
resetLocalForm();
|
||||
} catch {
|
||||
toast.error('Failed to save provider');
|
||||
toast.error(t('settings.server.harnesses.saveFailed'));
|
||||
setProbeState({ step: 'url' });
|
||||
}
|
||||
};
|
||||
@@ -254,9 +256,9 @@ export const AIHarnessesSection = () => {
|
||||
<div className="flex flex-col gap-4">
|
||||
<div>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-sm font-medium text-duck-dark">Pi Mono</span>
|
||||
<span className="text-sm font-medium text-duck-dark">{t('settings.server.harnesses.piMono')}</span>
|
||||
{piMonoLoading ? (
|
||||
<span className="text-xs text-duck-dark/50">Checking...</span>
|
||||
<span className="text-xs text-duck-dark/50">{t('settings.server.harnesses.checking')}</span>
|
||||
) : piMonoVersion?.version ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-duck-dark/50">{piMonoVersion.version}</span>
|
||||
@@ -265,7 +267,7 @@ export const AIHarnessesSection = () => {
|
||||
onClick={installAgent}
|
||||
disabled={installing}
|
||||
className="p-0.5 rounded hover:bg-duck-teal/10 cursor-pointer transition-colors disabled:opacity-30"
|
||||
title="Update Pi Mono"
|
||||
title={t('settings.server.harnesses.updatePiMono')}
|
||||
>
|
||||
<RefreshCw className={`h-3 w-3 text-duck-teal ${installing ? 'animate-spin' : ''}`} />
|
||||
</button>
|
||||
@@ -285,13 +287,21 @@ export const AIHarnessesSection = () => {
|
||||
<>
|
||||
{/* Local Providers */}
|
||||
<div className="mt-6 text-xs text-duck-dark/50">
|
||||
<span className="text-sm font-medium text-duck-dark dark:text-foreground">Local Providers</span>
|
||||
<span className="text-sm font-medium text-duck-dark dark:text-foreground">
|
||||
{t('settings.server.harnesses.localProviders')}
|
||||
</span>
|
||||
<div className="mt-3 flex flex-col gap-2">
|
||||
{localProviders.map((lp: LocalProviderEntry) => (
|
||||
<div key={lp.id} className="flex items-center gap-2">
|
||||
<span
|
||||
className={`inline-block h-2 w-2 shrink-0 rounded-full ${lp.id in localHealth ? (localHealth[lp.id] ? 'bg-success' : 'bg-red-500') : 'bg-duck-dark/20'}`}
|
||||
title={lp.id in localHealth ? (localHealth[lp.id] ? 'Online' : 'Offline') : 'Checking...'}
|
||||
title={
|
||||
lp.id in localHealth
|
||||
? localHealth[lp.id]
|
||||
? 'Online'
|
||||
: 'Offline'
|
||||
: t('settings.server.harnesses.checking')
|
||||
}
|
||||
/>
|
||||
<span className="text-duck-dark/70 dark:text-foreground/70 font-medium">{lp.name}</span>
|
||||
<span className="text-duck-dark/40 dark:text-foreground/40 truncate">{lp.url}</span>
|
||||
@@ -299,7 +309,7 @@ export const AIHarnessesSection = () => {
|
||||
type="button"
|
||||
onClick={() => removeLocalProvider(lp.id)}
|
||||
className="shrink-0 p-0.5 rounded hover:bg-red-500/10 cursor-pointer transition-colors"
|
||||
title="Remove provider"
|
||||
title={t('settings.server.harnesses.removeProvider')}
|
||||
>
|
||||
<Trash2 className="h-3 w-3 text-duck-dark/30 hover:text-red-500" />
|
||||
</button>
|
||||
@@ -312,7 +322,7 @@ export const AIHarnessesSection = () => {
|
||||
<Input
|
||||
type="text"
|
||||
className="h-8 text-xs"
|
||||
placeholder="Name (e.g. My Ollama)"
|
||||
placeholder={t('settings.server.harnesses.namePlaceholder')}
|
||||
value={localName}
|
||||
onChange={(ev) => setLocalName(ev.target.value)}
|
||||
onKeyDown={(ev) => {
|
||||
@@ -340,7 +350,7 @@ export const AIHarnessesSection = () => {
|
||||
className="h-7 text-xs bg-duck-dark text-white hover:bg-duck-dark/90 dark:bg-foreground dark:text-background dark:hover:bg-foreground/90 cursor-pointer disabled:opacity-40"
|
||||
disabled={!localUrl.trim()}
|
||||
>
|
||||
Connect
|
||||
{t('settings.server.harnesses.connect')}
|
||||
</Button>
|
||||
)}
|
||||
{(probeState.step === 'probing' || probeState.step === 'saving') && (
|
||||
@@ -351,7 +361,7 @@ export const AIHarnessesSection = () => {
|
||||
onClick={resetLocalForm}
|
||||
className="text-duck-dark/40 hover:text-duck-dark/70 dark:text-foreground/40 dark:hover:text-foreground/70 cursor-pointer transition-colors text-xs"
|
||||
>
|
||||
Cancel
|
||||
{tUi('button.cancel')}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -370,7 +380,7 @@ export const AIHarnessesSection = () => {
|
||||
<Input
|
||||
type="text"
|
||||
className="h-8 text-xs"
|
||||
placeholder="Username"
|
||||
placeholder={t('settings.server.harnesses.username')}
|
||||
value={authUsername}
|
||||
onChange={(ev) => setAuthUsername(ev.target.value)}
|
||||
autoFocus
|
||||
@@ -378,7 +388,7 @@ export const AIHarnessesSection = () => {
|
||||
<Input
|
||||
type="password"
|
||||
className="h-8 text-xs"
|
||||
placeholder="Password"
|
||||
placeholder={t('settings.server.harnesses.password')}
|
||||
value={authPassword}
|
||||
onChange={(ev) => setAuthPassword(ev.target.value)}
|
||||
/>
|
||||
@@ -388,7 +398,7 @@ export const AIHarnessesSection = () => {
|
||||
className="h-7 text-xs w-fit bg-duck-dark text-white hover:bg-duck-dark/90 dark:bg-foreground dark:text-background dark:hover:bg-foreground/90 cursor-pointer disabled:opacity-40"
|
||||
disabled={!authUsername || !authPassword}
|
||||
>
|
||||
Authenticate
|
||||
{t('settings.server.harnesses.authenticate')}
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
@@ -396,7 +406,7 @@ export const AIHarnessesSection = () => {
|
||||
<Input
|
||||
type="password"
|
||||
className="h-8 text-xs"
|
||||
placeholder="API Key"
|
||||
placeholder={t('settings.server.harnesses.apiKey')}
|
||||
value={authApiKey}
|
||||
onChange={(ev) => setAuthApiKey(ev.target.value)}
|
||||
autoFocus
|
||||
@@ -407,7 +417,7 @@ export const AIHarnessesSection = () => {
|
||||
className="h-7 text-xs w-fit bg-duck-dark text-white hover:bg-duck-dark/90 dark:bg-foreground dark:text-background dark:hover:bg-foreground/90 cursor-pointer disabled:opacity-40"
|
||||
disabled={!authApiKey}
|
||||
>
|
||||
Authenticate
|
||||
{t('settings.server.harnesses.authenticate')}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
@@ -423,7 +433,7 @@ export const AIHarnessesSection = () => {
|
||||
onClick={() => setAddingLocal(true)}
|
||||
>
|
||||
<Plus className="h-3.5 w-3.5 mr-1.5" />
|
||||
Connect Local Provider
|
||||
{t('settings.server.harnesses.connectLocal')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
@@ -431,7 +441,9 @@ export const AIHarnessesSection = () => {
|
||||
|
||||
{/* Remote Providers */}
|
||||
<div className="mt-6 text-xs text-duck-dark/50">
|
||||
<span className="text-sm font-medium text-duck-dark dark:text-foreground">Remote Providers</span>
|
||||
<span className="text-sm font-medium text-duck-dark dark:text-foreground">
|
||||
{t('settings.server.harnesses.remoteProviders')}
|
||||
</span>
|
||||
<div className="mt-3 flex flex-col gap-2">
|
||||
{connectedProviders.map((provider) => (
|
||||
<div key={provider.providerId} className="flex flex-col gap-2">
|
||||
@@ -457,7 +469,7 @@ export const AIHarnessesSection = () => {
|
||||
type="button"
|
||||
onClick={() => setEditingProvider(editingProvider === provider.key ? null : provider.key)}
|
||||
className="shrink-0 p-0.5 rounded hover:bg-duck-teal/10 cursor-pointer transition-colors"
|
||||
title="Edit key"
|
||||
title={t('settings.server.harnesses.editKey')}
|
||||
>
|
||||
<Pencil className="h-3 w-3 text-duck-dark/30 hover:text-duck-teal" />
|
||||
</button>
|
||||
@@ -465,7 +477,7 @@ export const AIHarnessesSection = () => {
|
||||
type="button"
|
||||
onClick={() => disconnectProvider(provider)}
|
||||
className="shrink-0 p-0.5 rounded hover:bg-red-500/10 cursor-pointer transition-colors"
|
||||
title="Disconnect provider"
|
||||
title={t('settings.server.harnesses.disconnect')}
|
||||
>
|
||||
<Trash2 className="h-3 w-3 text-duck-dark/30 hover:text-red-500" />
|
||||
</button>
|
||||
@@ -478,7 +490,7 @@ export const AIHarnessesSection = () => {
|
||||
<Input
|
||||
type="password"
|
||||
className="h-7 text-xs flex-1"
|
||||
placeholder={getStoredMasked(provider.providerId) || 'Enter API key'}
|
||||
placeholder={getStoredMasked(provider.providerId) || t('settings.server.harnesses.enterKey')}
|
||||
value={keyInputs[provider.providerId] ?? ''}
|
||||
onChange={(ev) =>
|
||||
setKeyInputs((prev) => ({ ...prev, [provider.providerId]: ev.target.value }))
|
||||
@@ -492,7 +504,7 @@ export const AIHarnessesSection = () => {
|
||||
type="submit"
|
||||
disabled={keyInputs[provider.providerId] === undefined || savingKey === provider.providerId}
|
||||
className="shrink-0 p-1 rounded hover:bg-duck-teal/10 cursor-pointer transition-colors disabled:opacity-30 disabled:cursor-default"
|
||||
title="Save key"
|
||||
title={t('settings.server.harnesses.saveKey')}
|
||||
>
|
||||
<Save className="h-3.5 w-3.5 text-duck-teal" />
|
||||
</button>
|
||||
@@ -501,7 +513,7 @@ export const AIHarnessesSection = () => {
|
||||
onClick={() => setEditingProvider(null)}
|
||||
className="text-duck-dark/40 hover:text-duck-dark/70 dark:text-foreground/40 dark:hover:text-foreground/70 cursor-pointer transition-colors text-xs"
|
||||
>
|
||||
Cancel
|
||||
{tUi('button.cancel')}
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
@@ -524,7 +536,7 @@ export const AIHarnessesSection = () => {
|
||||
<Input
|
||||
type="password"
|
||||
className="h-7 text-xs flex-1"
|
||||
placeholder="Enter API key"
|
||||
placeholder={t('settings.server.harnesses.enterKey')}
|
||||
value={keyInputs[provider.providerId] ?? ''}
|
||||
onChange={(ev) =>
|
||||
setKeyInputs((prev) => ({ ...prev, [provider.providerId]: ev.target.value }))
|
||||
@@ -538,7 +550,7 @@ export const AIHarnessesSection = () => {
|
||||
type="submit"
|
||||
disabled={keyInputs[provider.providerId] === undefined || savingKey === provider.providerId}
|
||||
className="shrink-0 p-1 rounded hover:bg-duck-teal/10 cursor-pointer transition-colors disabled:opacity-30 disabled:cursor-default"
|
||||
title="Save key"
|
||||
title={t('settings.server.harnesses.saveKey')}
|
||||
>
|
||||
<Save className="h-3.5 w-3.5 text-duck-teal" />
|
||||
</button>
|
||||
@@ -547,7 +559,7 @@ export const AIHarnessesSection = () => {
|
||||
onClick={() => setEditingProvider(null)}
|
||||
className="text-duck-dark/40 hover:text-duck-dark/70 dark:text-foreground/40 dark:hover:text-foreground/70 cursor-pointer transition-colors text-xs"
|
||||
>
|
||||
Cancel
|
||||
{tUi('button.cancel')}
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
@@ -561,14 +573,14 @@ export const AIHarnessesSection = () => {
|
||||
onClick={() => setCommandOpen(true)}
|
||||
>
|
||||
<Plus className="h-3.5 w-3.5 mr-1.5" />
|
||||
Connect Provider
|
||||
{t('settings.server.harnesses.connectProvider')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<CommandDialog open={commandOpen} onOpenChange={setCommandOpen}>
|
||||
<CommandInput placeholder="Search providers..." />
|
||||
<CommandInput placeholder={t('settings.server.harnesses.search')} />
|
||||
<CommandList>
|
||||
<CommandEmpty>No providers found.</CommandEmpty>
|
||||
<CommandEmpty>{t('settings.server.harnesses.none')}</CommandEmpty>
|
||||
<CommandGroup heading="Available Providers">
|
||||
{unconnectedProviders.map((provider) => (
|
||||
<CommandItem
|
||||
|
||||
Reference in New Issue
Block a user