diff --git a/ui/src/hooks/use-droid.ts b/ui/src/hooks/use-droid.ts index 7339829e..c35a3232 100644 --- a/ui/src/hooks/use-droid.ts +++ b/ui/src/hooks/use-droid.ts @@ -97,6 +97,30 @@ interface SaveDroidRawSettingsResponse { mtime: number; } +function parseDroidRawSettingsText(rawText: string): { + settings: Record | 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, + parseError: null, + }; + } catch (error) { + return { + settings: null, + parseError: (error as Error).message, + }; + } +} + async function fetchDroidDiagnostics(): Promise { const res = await fetch(withApiBase('/droid/diagnostics')); if (!res.ok) throw new Error('Failed to fetch Droid diagnostics'); @@ -142,9 +166,23 @@ export function useDroid() { const saveRawSettingsMutation = useMutation({ mutationFn: saveDroidRawSettings, - onSuccess: () => { + onSuccess: (result, variables) => { + queryClient.setQueryData(['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-raw-settings'] }); }, }); diff --git a/ui/src/pages/droid.tsx b/ui/src/pages/droid.tsx index b3ded797..0a1dea5d 100644 --- a/ui/src/pages/droid.tsx +++ b/ui/src/pages/droid.tsx @@ -25,6 +25,7 @@ import { Badge } from '@/components/ui/badge'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Separator } from '@/components/ui/separator'; import { ScrollArea } from '@/components/ui/scroll-area'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { cn } from '@/lib/utils'; import { applyAnthropicBudgetTokensToDroidByokModel, @@ -257,7 +258,6 @@ export function DroidPage() { expectedMtime: rawSettings?.exists ? rawSettings.mtime : undefined, }); setRawDraftText(null); - await Promise.all([refetchDiagnostics(), refetchRawSettings()]); toast.success('Droid settings saved'); } catch (error) { if (isApiConflictError(error)) { @@ -308,321 +308,370 @@ export function DroidPage() { const providerValues = docsReference.providerValues ?? []; const settingsHierarchy = docsReference.settingsHierarchy ?? []; + const tabContentClassName = 'mt-0 h-full border-0 p-0 data-[state=inactive]:hidden'; + return ( - -
- - - - - Runtime & Installation - - - -
- Status - - {diagnostics.binary.installed ? 'Detected' : 'Not Found'} - -
- - - - - -
-
+ +
+ + Overview + BYOK + Docs + +
- - - - - Config Files - - - - {[diagnostics.files.settings, diagnostics.files.legacyConfig].map((file) => ( -
-
- {file.label} - {file.exists ? ( - - ) : ( - - )} -
- - - - - {file.parseError && ( -

Parse warning: {file.parseError}

- )} - {file.readError && ( -

Read warning: {file.readError}

- )} -
- ))} -
-
+
+ + +
+ + + + + Runtime & Installation + + + +
+ Status + + {diagnostics.binary.installed ? 'Detected' : 'Not Found'} + +
+ + + + + +
+
- { - updateSettingsField(key, value); - }} - onBooleanSettingChange={(key, value) => { - updateSettingsField(key, value); - }} - onNumberSettingChange={(key, value) => { - updateSettingsField(key, value); - }} - /> - - { - if (!rawEditorParsed.valid) { - toast.error('Fix JSON syntax before updating BYOK reasoning settings.'); - return; - } - - const nextSettings = applyReasoningEffortToDroidByokModel( - rawEditorParsed.value, - modelId, - effort - ); - if (!nextSettings) { - toast.error('Unable to update selected BYOK model reasoning setting.'); - return; - } - - updateSettingsObject(nextSettings); - }} - onAnthropicBudgetChange={(modelId, budgetTokens) => { - if (!rawEditorParsed.valid) { - toast.error('Fix JSON syntax before updating thinking budget.'); - return; - } - - const nextSettings = applyAnthropicBudgetTokensToDroidByokModel( - rawEditorParsed.value, - modelId, - budgetTokens - ); - if (!nextSettings) { - toast.error('Thinking budget is only available for Anthropic BYOK models.'); - return; - } - - updateSettingsObject(nextSettings); - }} - /> - - - - - - BYOK Summary - - - - - - - - - -
-

Providers

-
- {providerRows.length === 0 && ( - - none - - )} - {providerRows.map(([provider, count]) => ( - - {provider}: {count} - - ))} -
-
-
-
- - - - - - Docs-Aligned Notes - - - - {docsNotes.map((note, index) => ( -

- - {renderTextWithLinks(note)} -

- ))} - -
-

- Factory Docs -

- -
- - - -

- Provider values: {providerValues.join(', ')} -

-

- Settings hierarchy: {settingsHierarchy.join(' -> ')} -

-
-
- - - - Custom Models - - -
-
- Name / Model - Provider - Base URL -
- -
- {customModels.length === 0 && ( -
- No custom models -
- )} - {customModels.map((model) => ( -
-
-

{model.displayName}

-

{model.model}

-
-
-

{model.provider}

-

{model.apiKeyPreview || 'no-key'}

-
-
-

- {model.host || model.baseUrl} -

-

- {model.baseUrl} -

+ + + + + Config Files + + + + {[diagnostics.files.settings, diagnostics.files.legacyConfig].map((file) => ( +
+
+ {file.label} + {file.exists ? ( + + ) : ( + + )}
+ + + + + {file.parseError && ( +

Parse warning: {file.parseError}

+ )} + {file.readError && ( +

Read warning: {file.readError}

+ )}
))} -
- -
- - + + - {diagnostics.warnings.length > 0 && ( - - - - - Warnings - - - - {diagnostics.warnings.map((warning) => ( -

- - {warning} -

- ))} -
-
- )} + {diagnostics.warnings.length > 0 && ( + + + + + Warnings + + + + {diagnostics.warnings.map((warning) => ( +

+ - {warning} +

+ ))} +
+
+ )} +
+
+ + + + +
+ { + updateSettingsField(key, value); + }} + onBooleanSettingChange={(key, value) => { + updateSettingsField(key, value); + }} + onNumberSettingChange={(key, value) => { + updateSettingsField(key, value); + }} + /> + + { + if (!rawEditorParsed.valid) { + toast.error('Fix JSON syntax before updating BYOK reasoning settings.'); + return; + } + + const nextSettings = applyReasoningEffortToDroidByokModel( + rawEditorParsed.value, + modelId, + effort + ); + if (!nextSettings) { + toast.error('Unable to update selected BYOK model reasoning setting.'); + return; + } + + updateSettingsObject(nextSettings); + }} + onAnthropicBudgetChange={(modelId, budgetTokens) => { + if (!rawEditorParsed.valid) { + toast.error('Fix JSON syntax before updating thinking budget.'); + return; + } + + const nextSettings = applyAnthropicBudgetTokensToDroidByokModel( + rawEditorParsed.value, + modelId, + budgetTokens + ); + if (!nextSettings) { + toast.error('Thinking budget is only available for Anthropic BYOK models.'); + return; + } + + updateSettingsObject(nextSettings); + }} + /> + + + + + + BYOK Summary + + + + + + + + + +
+

Providers

+
+ {providerRows.length === 0 && ( + + none + + )} + {providerRows.map(([provider, count]) => ( + + {provider}: {count} + + ))} +
+
+
+
+ + + + Custom Models + + +
+
+ Name / Model + Provider + Base URL +
+ +
+ {customModels.length === 0 && ( +
+ No custom models +
+ )} + {customModels.map((model) => ( +
+
+

{model.displayName}

+

+ {model.model} +

+
+
+

{model.provider}

+

+ {model.apiKeyPreview || 'no-key'} +

+
+
+

+ {model.host || model.baseUrl} +

+

+ {model.baseUrl} +

+
+
+ ))} +
+
+
+
+
+
+
+
+ + + +
+ + + + + Docs-Aligned Notes + + + + {docsNotes.map((note, index) => ( +

+ - {renderTextWithLinks(note)} +

+ ))} + +
+

+ Factory Docs +

+ +
+ + + +

+ Provider values: {providerValues.join(', ')} +

+

+ Settings hierarchy: {settingsHierarchy.join(' -> ')} +

+
+
+
+
+
- + ); };