fix(droid): restore tabbed layout and stabilize save state

This commit is contained in:
Tam Nhu Tran committed 2026-02-26 12:57:13 +07:00
1 parent eedb53b49e
commit a88fd68c34
2 files changed
+161 -74

No files matched your search

+40 -2
View File
@@ -97,6 +97,30 @@ interface SaveDroidRawSettingsResponse {
mtime: number; mtime: number;
} }
function parseDroidRawSettingsText(rawText: string): {
settings: Record<string, unknown> | null;
parseError: string | null;
} {
try {
const parsed = JSON.parse(rawText);
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
return {
settings: null,
parseError: 'JSON root must be an object.',
};
}
return {
settings: parsed as Record<string, unknown>,
parseError: null,
};
} catch (error) {
return {
settings: null,
parseError: (error as Error).message,
};
}
}
async function fetchDroidDiagnostics(): Promise<DroidDashboardDiagnostics> { async function fetchDroidDiagnostics(): Promise<DroidDashboardDiagnostics> {
const res = await fetch(withApiBase('/droid/diagnostics')); const res = await fetch(withApiBase('/droid/diagnostics'));
if (!res.ok) throw new Error('Failed to fetch Droid diagnostics'); if (!res.ok) throw new Error('Failed to fetch Droid diagnostics');
@@ -142,9 +166,23 @@ export function useDroid() {
const saveRawSettingsMutation = useMutation({ const saveRawSettingsMutation = useMutation({
mutationFn: saveDroidRawSettings, mutationFn: saveDroidRawSettings,
onSuccess: () => { onSuccess: (result, variables) => {
queryClient.setQueryData<DroidRawSettings>(['droid-raw-settings'], (current) => {
const path = current?.path ?? '~/.factory/settings.json';
const resolvedPath = current?.resolvedPath ?? path;
const parsed = parseDroidRawSettingsText(variables.rawText);
return {
path,
resolvedPath,
exists: true,
mtime: result.mtime,
rawText: variables.rawText,
settings: parsed.settings,
parseError: parsed.parseError,
};
});
queryClient.invalidateQueries({ queryKey: ['droid-diagnostics'] }); queryClient.invalidateQueries({ queryKey: ['droid-diagnostics'] });
queryClient.invalidateQueries({ queryKey: ['droid-raw-settings'] });
}, },
}); });
+121 -72
View File
@@ -25,6 +25,7 @@ import { Badge } from '@/components/ui/badge';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Separator } from '@/components/ui/separator'; import { Separator } from '@/components/ui/separator';
import { ScrollArea } from '@/components/ui/scroll-area'; import { ScrollArea } from '@/components/ui/scroll-area';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { import {
applyAnthropicBudgetTokensToDroidByokModel, applyAnthropicBudgetTokensToDroidByokModel,
@@ -257,7 +258,6 @@ export function DroidPage() {
expectedMtime: rawSettings?.exists ? rawSettings.mtime : undefined, expectedMtime: rawSettings?.exists ? rawSettings.mtime : undefined,
}); });
setRawDraftText(null); setRawDraftText(null);
await Promise.all([refetchDiagnostics(), refetchRawSettings()]);
toast.success('Droid settings saved'); toast.success('Droid settings saved');
} catch (error) { } catch (error) {
if (isApiConflictError(error)) { if (isApiConflictError(error)) {
@@ -308,9 +308,22 @@ export function DroidPage() {
const providerValues = docsReference.providerValues ?? []; const providerValues = docsReference.providerValues ?? [];
const settingsHierarchy = docsReference.settingsHierarchy ?? []; const settingsHierarchy = docsReference.settingsHierarchy ?? [];
const tabContentClassName = 'mt-0 h-full border-0 p-0 data-[state=inactive]:hidden';
return ( return (
<Tabs defaultValue="byok" className="h-full flex flex-col">
<div className="px-4 pt-4 shrink-0">
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="byok">BYOK</TabsTrigger>
<TabsTrigger value="docs">Docs</TabsTrigger>
</TabsList>
</div>
<div className="flex-1 min-h-0 overflow-hidden px-4 pb-4 pt-3">
<TabsContent value="overview" className={tabContentClassName}>
<ScrollArea className="h-full"> <ScrollArea className="h-full">
<div className="space-y-4 p-4"> <div className="space-y-4 pr-1">
<Card> <Card>
<CardHeader className="pb-2"> <CardHeader className="pb-2">
<CardTitle className="text-base flex items-center gap-2"> <CardTitle className="text-base flex items-center gap-2">
@@ -336,7 +349,11 @@ export function DroidPage() {
value={diagnostics.binary.installDir || 'N/A'} value={diagnostics.binary.installDir || 'N/A'}
mono mono
/> />
<DetailRow label="Version" value={diagnostics.binary.version || 'Unknown'} mono /> <DetailRow
label="Version"
value={diagnostics.binary.version || 'Unknown'}
mono
/>
<DetailRow <DetailRow
label="Override (CCS_DROID_PATH)" label="Override (CCS_DROID_PATH)"
value={diagnostics.binary.overridePath || 'Not set'} value={diagnostics.binary.overridePath || 'Not set'}
@@ -378,11 +395,37 @@ export function DroidPage() {
</CardContent> </CardContent>
</Card> </Card>
{diagnostics.warnings.length > 0 && (
<Card className="border-amber-200 bg-amber-50/50 dark:bg-amber-950/20">
<CardHeader className="pb-2">
<CardTitle className="text-base flex items-center gap-2">
<AlertTriangle className="h-4 w-4 text-amber-600" />
Warnings
</CardTitle>
</CardHeader>
<CardContent className="space-y-1.5">
{diagnostics.warnings.map((warning) => (
<p key={warning} className="text-sm text-amber-800 dark:text-amber-300">
- {warning}
</p>
))}
</CardContent>
</Card>
)}
</div>
</ScrollArea>
</TabsContent>
<TabsContent value="byok" className={tabContentClassName}>
<ScrollArea className="h-full">
<div className="space-y-4 pr-1">
<DroidSettingsQuickControlsCard <DroidSettingsQuickControlsCard
values={quickSettingsValues} values={quickSettingsValues}
disabled={rawSettingsLoading || !rawEditorParsed.valid} disabled={rawSettingsLoading || !rawEditorParsed.valid}
disabledReason={ disabledReason={
rawEditorParsed.valid ? null : `Quick settings disabled: ${rawEditorParsed.error}` rawEditorParsed.valid
? null
: `Quick settings disabled: ${rawEditorParsed.error}`
} }
onEnumSettingChange={(key, value) => { onEnumSettingChange={(key, value) => {
updateSettingsField(key, value); updateSettingsField(key, value);
@@ -454,9 +497,18 @@ export function DroidPage() {
value={diagnostics.byok.activeModelSelector || 'Not set'} value={diagnostics.byok.activeModelSelector || 'Not set'}
mono mono
/> />
<DetailRow label="Custom models" value={String(diagnostics.byok.customModelCount)} /> <DetailRow
<DetailRow label="CCS-managed" value={String(diagnostics.byok.ccsManagedCount)} /> label="Custom models"
<DetailRow label="User-managed" value={String(diagnostics.byok.userManagedCount)} /> value={String(diagnostics.byok.customModelCount)}
/>
<DetailRow
label="CCS-managed"
value={String(diagnostics.byok.ccsManagedCount)}
/>
<DetailRow
label="User-managed"
value={String(diagnostics.byok.userManagedCount)}
/>
<DetailRow <DetailRow
label="Malformed entries" label="Malformed entries"
value={String(diagnostics.byok.invalidModelEntryCount)} value={String(diagnostics.byok.invalidModelEntryCount)}
@@ -480,6 +532,63 @@ export function DroidPage() {
</CardContent> </CardContent>
</Card> </Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-base">Custom Models</CardTitle>
</CardHeader>
<CardContent>
<div className="rounded-md border overflow-hidden">
<div className="grid grid-cols-[2fr_1fr_2fr] bg-muted/40 px-3 py-2 text-xs font-medium">
<span>Name / Model</span>
<span>Provider</span>
<span>Base URL</span>
</div>
<ScrollArea className="h-52">
<div className="divide-y">
{customModels.length === 0 && (
<div className="px-3 py-4 text-xs text-muted-foreground">
No custom models
</div>
)}
{customModels.map((model) => (
<div
key={`${model.displayName}-${model.model}-${model.baseUrl}`}
className="grid grid-cols-[2fr_1fr_2fr] gap-2 px-3 py-2 text-xs"
>
<div className="min-w-0">
<p className="font-medium truncate">{model.displayName}</p>
<p className="text-muted-foreground font-mono truncate">
{model.model}
</p>
</div>
<div className="min-w-0">
<p className="truncate">{model.provider}</p>
<p className="text-muted-foreground">
{model.apiKeyPreview || 'no-key'}
</p>
</div>
<div className="min-w-0">
<p className="truncate" title={model.baseUrl}>
{model.host || model.baseUrl}
</p>
<p className="text-muted-foreground font-mono truncate">
{model.baseUrl}
</p>
</div>
</div>
))}
</div>
</ScrollArea>
</div>
</CardContent>
</Card>
</div>
</ScrollArea>
</TabsContent>
<TabsContent value="docs" className={tabContentClassName}>
<ScrollArea className="h-full">
<div className="space-y-4 pr-1">
<Card> <Card>
<CardHeader className="pb-2"> <CardHeader className="pb-2">
<CardTitle className="text-base flex items-center gap-2"> <CardTitle className="text-base flex items-center gap-2">
@@ -511,7 +620,9 @@ export function DroidPage() {
<span className="text-xs font-medium">{link.label}</span> <span className="text-xs font-medium">{link.label}</span>
<ExternalLink className="h-3.5 w-3.5 text-muted-foreground" /> <ExternalLink className="h-3.5 w-3.5 text-muted-foreground" />
</div> </div>
<p className="mt-0.5 text-[11px] text-muted-foreground">{link.description}</p> <p className="mt-0.5 text-[11px] text-muted-foreground">
{link.description}
</p>
<p className="mt-1 break-all font-mono text-[11px] text-muted-foreground/90 underline underline-offset-2"> <p className="mt-1 break-all font-mono text-[11px] text-muted-foreground/90 underline underline-offset-2">
{link.url} {link.url}
</p> </p>
@@ -556,73 +667,11 @@ export function DroidPage() {
</p> </p>
</CardContent> </CardContent>
</Card> </Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-base">Custom Models</CardTitle>
</CardHeader>
<CardContent>
<div className="rounded-md border overflow-hidden">
<div className="grid grid-cols-[2fr_1fr_2fr] bg-muted/40 px-3 py-2 text-xs font-medium">
<span>Name / Model</span>
<span>Provider</span>
<span>Base URL</span>
</div>
<ScrollArea className="h-52">
<div className="divide-y">
{customModels.length === 0 && (
<div className="px-3 py-4 text-xs text-muted-foreground">
No custom models
</div>
)}
{customModels.map((model) => (
<div
key={`${model.displayName}-${model.model}-${model.baseUrl}`}
className="grid grid-cols-[2fr_1fr_2fr] gap-2 px-3 py-2 text-xs"
>
<div className="min-w-0">
<p className="font-medium truncate">{model.displayName}</p>
<p className="text-muted-foreground font-mono truncate">{model.model}</p>
</div>
<div className="min-w-0">
<p className="truncate">{model.provider}</p>
<p className="text-muted-foreground">{model.apiKeyPreview || 'no-key'}</p>
</div>
<div className="min-w-0">
<p className="truncate" title={model.baseUrl}>
{model.host || model.baseUrl}
</p>
<p className="text-muted-foreground font-mono truncate">
{model.baseUrl}
</p>
</div>
</div>
))}
</div> </div>
</ScrollArea> </ScrollArea>
</TabsContent>
</div> </div>
</CardContent> </Tabs>
</Card>
{diagnostics.warnings.length > 0 && (
<Card className="border-amber-200 bg-amber-50/50 dark:bg-amber-950/20">
<CardHeader className="pb-2">
<CardTitle className="text-base flex items-center gap-2">
<AlertTriangle className="h-4 w-4 text-amber-600" />
Warnings
</CardTitle>
</CardHeader>
<CardContent className="space-y-1.5">
{diagnostics.warnings.map((warning) => (
<p key={warning} className="text-sm text-amber-800 dark:text-amber-300">
- {warning}
</p>
))}
</CardContent>
</Card>
)}
</div>
</ScrollArea>
); );
}; };