diff --git a/src/web-server/services/compatible-cli-docs-registry.ts b/src/web-server/services/compatible-cli-docs-registry.ts
new file mode 100644
index 00000000..551fa978
--- /dev/null
+++ b/src/web-server/services/compatible-cli-docs-registry.ts
@@ -0,0 +1,105 @@
+export interface CompatibleCliDocLink {
+ id: string;
+ label: string;
+ url: string;
+ category: 'overview' | 'configuration' | 'byok' | 'reference';
+ source: 'factory' | 'provider';
+ description: string;
+}
+
+export interface CompatibleCliProviderDocLink {
+ provider: string;
+ label: string;
+ apiFormat: string;
+ url: string;
+}
+
+export interface CompatibleCliDocsReference {
+ providerValues: string[];
+ settingsHierarchy: string[];
+ notes: string[];
+ links: CompatibleCliDocLink[];
+ providerDocs: CompatibleCliProviderDocLink[];
+}
+
+interface CompatibleCliDocsRegistryEntry {
+ cliId: string;
+ displayName: string;
+ docsReference: CompatibleCliDocsReference;
+}
+
+const COMPATIBLE_CLI_DOCS_REGISTRY: Record = {
+ droid: {
+ cliId: 'droid',
+ displayName: 'Droid CLI',
+ docsReference: {
+ providerValues: ['anthropic', 'openai', 'generic-chat-completion-api'],
+ settingsHierarchy: [
+ 'project-level config',
+ 'user-level config',
+ 'home-level config',
+ 'CLI flags and env vars',
+ ],
+ notes: [
+ 'BYOK custom models are read from ~/.factory/settings.json customModels[]',
+ 'Factory docs mention legacy support for ~/.factory/config.json',
+ 'Interactive model selection uses settings.model (custom:)',
+ 'droid exec supports --model for one-off execution mode',
+ ],
+ links: [
+ {
+ id: 'droid-cli-overview',
+ label: 'Droid CLI Overview',
+ url: 'https://docs.factory.ai/cli/',
+ category: 'overview',
+ source: 'factory',
+ description: 'Primary entry docs for setup, auth, and core CLI usage.',
+ },
+ {
+ id: 'droid-byok-overview',
+ label: 'BYOK Overview',
+ url: 'https://docs.factory.ai/cli/byok/overview/',
+ category: 'byok',
+ source: 'factory',
+ description: 'BYOK model/provider shape, provider values, and migration notes.',
+ },
+ {
+ id: 'droid-settings-reference',
+ label: 'settings.json Reference',
+ url: 'https://docs.factory.ai/cli/configuration/settings/',
+ category: 'configuration',
+ source: 'factory',
+ description: 'Supported settings keys, defaults, and allowed values.',
+ },
+ ],
+ providerDocs: [
+ {
+ provider: 'anthropic',
+ label: 'Anthropic Messages API',
+ apiFormat: 'Messages API',
+ url: 'https://docs.anthropic.com/en/api/messages',
+ },
+ {
+ provider: 'openai',
+ label: 'OpenAI Responses API',
+ apiFormat: 'Responses API',
+ url: 'https://platform.openai.com/docs/api-reference/responses',
+ },
+ {
+ provider: 'generic-chat-completion-api',
+ label: 'OpenAI Chat Completions Spec',
+ apiFormat: 'Chat Completions API',
+ url: 'https://platform.openai.com/docs/api-reference/chat',
+ },
+ ],
+ },
+ },
+};
+
+export function getCompatibleCliDocsReference(cliId: string): CompatibleCliDocsReference {
+ const entry = COMPATIBLE_CLI_DOCS_REGISTRY[cliId];
+ if (!entry) {
+ throw new Error(`Unsupported compatible CLI docs reference: ${cliId}`);
+ }
+ return entry.docsReference;
+}
diff --git a/src/web-server/services/compatible-cli-types.ts b/src/web-server/services/compatible-cli-types.ts
index bb9898e2..6d93c1ce 100644
--- a/src/web-server/services/compatible-cli-types.ts
+++ b/src/web-server/services/compatible-cli-types.ts
@@ -44,6 +44,30 @@ export interface DroidByokDiagnostics {
customModels: DroidCustomModelDiagnostics[];
}
+export interface CompatibleCliDocLink {
+ id: string;
+ label: string;
+ url: string;
+ category: 'overview' | 'configuration' | 'byok' | 'reference';
+ source: 'factory' | 'provider';
+ description: string;
+}
+
+export interface CompatibleCliProviderDocLink {
+ provider: string;
+ label: string;
+ apiFormat: string;
+ url: string;
+}
+
+export interface CompatibleCliDocsReference {
+ providerValues: string[];
+ settingsHierarchy: string[];
+ notes: string[];
+ links: CompatibleCliDocLink[];
+ providerDocs: CompatibleCliProviderDocLink[];
+}
+
export interface DroidDashboardDiagnostics {
binary: DroidBinaryDiagnostics;
files: {
@@ -52,11 +76,7 @@ export interface DroidDashboardDiagnostics {
};
byok: DroidByokDiagnostics;
warnings: string[];
- docsReference: {
- providerValues: string[];
- settingsHierarchy: string[];
- notes: string[];
- };
+ docsReference: CompatibleCliDocsReference;
}
export interface DroidRawSettingsResponse {
diff --git a/src/web-server/services/droid-dashboard-service.ts b/src/web-server/services/droid-dashboard-service.ts
index 903c56ab..ad960f21 100644
--- a/src/web-server/services/droid-dashboard-service.ts
+++ b/src/web-server/services/droid-dashboard-service.ts
@@ -14,6 +14,7 @@ import {
probeJsonObjectFile,
writeJsonObjectFileAtomic,
} from './compatible-cli-json-file-service';
+import { getCompatibleCliDocsReference } from './compatible-cli-docs-registry';
interface DroidConfigPaths {
settingsPath: string;
@@ -160,6 +161,7 @@ export function summarizeDroidCustomModels(customModelsValue: unknown): DroidByo
export async function getDroidDashboardDiagnostics(): Promise {
const paths = resolveDroidConfigPaths();
const binaryPath = detectDroidCli();
+ const docsReference = getCompatibleCliDocsReference('droid');
const source = process.env.CCS_DROID_PATH ? 'CCS_DROID_PATH' : binaryPath ? 'PATH' : 'missing';
@@ -204,21 +206,7 @@ export async function getDroidDashboardDiagnostics(): Promise)',
- 'droid exec supports --model for one-off execution mode',
- ],
- },
+ docsReference,
};
}
diff --git a/tests/unit/web-server/droid-dashboard-service.test.ts b/tests/unit/web-server/droid-dashboard-service.test.ts
index c781785f..50ebbe2a 100644
--- a/tests/unit/web-server/droid-dashboard-service.test.ts
+++ b/tests/unit/web-server/droid-dashboard-service.test.ts
@@ -5,6 +5,7 @@ import * as path from 'path';
import {
DroidRawSettingsConflictError,
DroidRawSettingsValidationError,
+ getDroidDashboardDiagnostics,
getDroidRawSettings,
maskApiKeyPreview,
resolveDroidConfigPaths,
@@ -110,6 +111,19 @@ describe('droid-dashboard-service', () => {
expect(raw.rawText).toContain('invalid-json');
});
+ it('includes structured docs links for fact-checking providers', async () => {
+ const diagnostics = await getDroidDashboardDiagnostics();
+
+ expect(diagnostics.docsReference.links.length).toBeGreaterThan(0);
+ expect(diagnostics.docsReference.providerDocs.length).toBeGreaterThan(0);
+ expect(diagnostics.docsReference.links.every((link) => link.url.startsWith('https://'))).toBe(
+ true
+ );
+ expect(
+ diagnostics.docsReference.providerDocs.some((doc) => doc.provider === 'anthropic')
+ ).toBe(true);
+ });
+
it('saves valid raw settings content', async () => {
const result = await saveDroidRawSettings({
rawText: JSON.stringify({
diff --git a/ui/src/components/compatible-cli/droid-settings-quick-controls-card.tsx b/ui/src/components/compatible-cli/droid-settings-quick-controls-card.tsx
new file mode 100644
index 00000000..80f43ec7
--- /dev/null
+++ b/ui/src/components/compatible-cli/droid-settings-quick-controls-card.tsx
@@ -0,0 +1,214 @@
+import { SlidersHorizontal } from 'lucide-react';
+import { Badge } from '@/components/ui/badge';
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import { Input } from '@/components/ui/input';
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from '@/components/ui/select';
+
+const UNSET_VALUE = '__unset__';
+
+type DroidEnumSettingKey = 'reasoningEffort' | 'autonomyLevel' | 'diffMode';
+type DroidBooleanSettingKey =
+ | 'todoEnabled'
+ | 'todoAutoRefresh'
+ | 'autoCompactEnabled'
+ | 'soundEnabled';
+type DroidNumberSettingKey = 'maxTurns' | 'maxToolCalls' | 'autoCompactThreshold';
+
+export interface DroidQuickSettingsValues {
+ reasoningEffort: string | null;
+ autonomyLevel: string | null;
+ diffMode: string | null;
+ maxTurns: number | null;
+ maxToolCalls: number | null;
+ autoCompactThreshold: number | null;
+ todoEnabled: boolean | null;
+ todoAutoRefresh: boolean | null;
+ autoCompactEnabled: boolean | null;
+ soundEnabled: boolean | null;
+}
+
+interface DroidSettingsQuickControlsCardProps {
+ values: DroidQuickSettingsValues;
+ disabled: boolean;
+ disabledReason?: string | null;
+ onEnumSettingChange: (key: DroidEnumSettingKey, value: string | null) => void;
+ onBooleanSettingChange: (key: DroidBooleanSettingKey, value: boolean | null) => void;
+ onNumberSettingChange: (key: DroidNumberSettingKey, value: number | null) => void;
+}
+
+const enumFieldConfig: Array<{
+ key: DroidEnumSettingKey;
+ label: string;
+ description: string;
+ options: Array<{ value: string; label: string }>;
+}> = [
+ {
+ key: 'reasoningEffort',
+ label: 'Reasoning Effort',
+ description: 'none | medium | high | max',
+ options: [
+ { value: 'none', label: 'none' },
+ { value: 'medium', label: 'medium' },
+ { value: 'high', label: 'high' },
+ { value: 'max', label: 'max' },
+ ],
+ },
+ {
+ key: 'autonomyLevel',
+ label: 'Autonomy Level',
+ description: 'suggest | aggressive | full',
+ options: [
+ { value: 'suggest', label: 'suggest' },
+ { value: 'aggressive', label: 'aggressive' },
+ { value: 'full', label: 'full' },
+ ],
+ },
+ {
+ key: 'diffMode',
+ label: 'Diff Mode',
+ description: 'auto | none | inline | split',
+ options: [
+ { value: 'auto', label: 'auto' },
+ { value: 'none', label: 'none' },
+ { value: 'inline', label: 'inline' },
+ { value: 'split', label: 'split' },
+ ],
+ },
+];
+
+const booleanFieldConfig: Array<{
+ key: DroidBooleanSettingKey;
+ label: string;
+}> = [
+ { key: 'todoEnabled', label: 'Todo Enabled' },
+ { key: 'todoAutoRefresh', label: 'Todo Auto Refresh' },
+ { key: 'autoCompactEnabled', label: 'Auto Compact Enabled' },
+ { key: 'soundEnabled', label: 'Sound Enabled' },
+];
+
+const numberFieldConfig: Array<{
+ key: DroidNumberSettingKey;
+ label: string;
+ min: number;
+ step: number;
+}> = [
+ { key: 'maxTurns', label: 'Max Turns', min: 1, step: 1 },
+ { key: 'maxToolCalls', label: 'Max Tool Calls', min: 1, step: 1 },
+ { key: 'autoCompactThreshold', label: 'Auto Compact Threshold', min: 1000, step: 1000 },
+];
+
+function toBooleanSelectValue(value: boolean | null): string {
+ if (value === true) return 'true';
+ if (value === false) return 'false';
+ return UNSET_VALUE;
+}
+
+function toBooleanValue(value: string): boolean | null {
+ if (value === 'true') return true;
+ if (value === 'false') return false;
+ return null;
+}
+
+export function DroidSettingsQuickControlsCard({
+ values,
+ disabled,
+ disabledReason,
+ onEnumSettingChange,
+ onBooleanSettingChange,
+ onNumberSettingChange,
+}: DroidSettingsQuickControlsCardProps) {
+ return (
+
+
+
+
+ Quick Settings
+
+ settings.json
+
+
+
+
+ {disabledReason && {disabledReason}
}
+
+
+ {enumFieldConfig.map((field) => (
+
+
{field.label}
+
+
{field.description}
+
+ ))}
+
+ {numberFieldConfig.map((field) => (
+
+
{field.label}
+
{
+ const nextRaw = event.target.value.trim();
+ if (!nextRaw) {
+ onNumberSettingChange(field.key, null);
+ return;
+ }
+ const next = Number.parseInt(nextRaw, 10);
+ if (!Number.isFinite(next)) return;
+ onNumberSettingChange(field.key, Math.max(field.min, next));
+ }}
+ className="h-8 text-xs"
+ disabled={disabled}
+ />
+
+ ))}
+
+ {booleanFieldConfig.map((field) => (
+
+
{field.label}
+
+
+ ))}
+
+
+
+ );
+}
diff --git a/ui/src/hooks/use-droid.ts b/ui/src/hooks/use-droid.ts
index a4b2c062..7339829e 100644
--- a/ui/src/hooks/use-droid.ts
+++ b/ui/src/hooks/use-droid.ts
@@ -36,6 +36,22 @@ export interface DroidCustomModelDiagnostics {
apiKeyPreview: string | null;
}
+export interface CompatibleCliDocLink {
+ id: string;
+ label: string;
+ url: string;
+ category: 'overview' | 'configuration' | 'byok' | 'reference';
+ source: 'factory' | 'provider';
+ description: string;
+}
+
+export interface CompatibleCliProviderDocLink {
+ provider: string;
+ label: string;
+ apiFormat: string;
+ url: string;
+}
+
export interface DroidDashboardDiagnostics {
binary: DroidBinaryDiagnostics;
files: {
@@ -56,6 +72,8 @@ export interface DroidDashboardDiagnostics {
providerValues: string[];
settingsHierarchy: string[];
notes: string[];
+ links: CompatibleCliDocLink[];
+ providerDocs: CompatibleCliProviderDocLink[];
};
}
diff --git a/ui/src/pages/droid.tsx b/ui/src/pages/droid.tsx
index d53256cb..796eb075 100644
--- a/ui/src/pages/droid.tsx
+++ b/ui/src/pages/droid.tsx
@@ -4,6 +4,7 @@ import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels';
import {
AlertTriangle,
CheckCircle2,
+ ExternalLink,
Folder,
GripVertical,
Loader2,
@@ -15,6 +16,10 @@ import {
import { useDroid } from '@/hooks/use-droid';
import { isApiConflictError } from '@/lib/api-client';
import { RawJsonSettingsEditorPanel } from '@/components/compatible-cli/raw-json-settings-editor-panel';
+import {
+ DroidSettingsQuickControlsCard,
+ type DroidQuickSettingsValues,
+} from '@/components/compatible-cli/droid-settings-quick-controls-card';
import { Badge } from '@/components/ui/badge';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Separator } from '@/components/ui/separator';
@@ -33,18 +38,32 @@ function formatBytes(value: number | null | undefined): string {
return `${(value / (1024 * 1024)).toFixed(2)} MB`;
}
-function validateJsonObject(text: string): { valid: true } | { valid: false; error: string } {
+function parseJsonObjectText(
+ text: string
+): { valid: true; value: Record } | { valid: false; error: string } {
try {
const parsed = JSON.parse(text);
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
return { valid: false, error: 'JSON root must be an object.' };
}
- return { valid: true };
+ return { valid: true, value: parsed as Record };
} catch (error) {
return { valid: false, error: (error as Error).message };
}
}
+function asStringValue(value: unknown): string | null {
+ return typeof value === 'string' ? value : null;
+}
+
+function asNumberValue(value: unknown): number | null {
+ return typeof value === 'number' && Number.isFinite(value) ? value : null;
+}
+
+function asBooleanValue(value: unknown): boolean | null {
+ return typeof value === 'boolean' ? value : null;
+}
+
function DetailRow({
label,
value,
@@ -79,6 +98,59 @@ export function DroidPage() {
const rawBaseText = rawSettings?.rawText ?? '{}';
const rawEditorText = rawDraftText ?? rawBaseText;
const rawConfigDirty = rawDraftText !== null && rawDraftText !== rawBaseText;
+ const rawEditorParsed = parseJsonObjectText(rawEditorText);
+ const rawEditorValidation = rawEditorParsed.valid
+ ? { valid: true as const }
+ : { valid: false as const, error: rawEditorParsed.error };
+
+ const setRawEditorDraftText = (nextText: string) => {
+ if (nextText === rawBaseText) {
+ setRawDraftText(null);
+ return;
+ }
+ setRawDraftText(nextText);
+ };
+
+ const updateSettingsField = (key: string, value: unknown | null) => {
+ if (!rawEditorParsed.valid) {
+ toast.error('Fix JSON syntax before using quick settings controls.');
+ return;
+ }
+
+ const nextSettings = { ...rawEditorParsed.value };
+ if (value === null || value === undefined) {
+ delete nextSettings[key];
+ } else {
+ nextSettings[key] = value;
+ }
+ setRawEditorDraftText(JSON.stringify(nextSettings, null, 2) + '\n');
+ };
+
+ const quickSettingsValues: DroidQuickSettingsValues = rawEditorParsed.valid
+ ? {
+ reasoningEffort: asStringValue(rawEditorParsed.value.reasoningEffort),
+ autonomyLevel: asStringValue(rawEditorParsed.value.autonomyLevel),
+ diffMode: asStringValue(rawEditorParsed.value.diffMode),
+ maxTurns: asNumberValue(rawEditorParsed.value.maxTurns),
+ maxToolCalls: asNumberValue(rawEditorParsed.value.maxToolCalls),
+ autoCompactThreshold: asNumberValue(rawEditorParsed.value.autoCompactThreshold),
+ todoEnabled: asBooleanValue(rawEditorParsed.value.todoEnabled),
+ todoAutoRefresh: asBooleanValue(rawEditorParsed.value.todoAutoRefresh),
+ autoCompactEnabled: asBooleanValue(rawEditorParsed.value.autoCompactEnabled),
+ soundEnabled: asBooleanValue(rawEditorParsed.value.soundEnabled),
+ }
+ : {
+ reasoningEffort: null,
+ autonomyLevel: null,
+ diffMode: null,
+ maxTurns: null,
+ maxToolCalls: null,
+ autoCompactThreshold: null,
+ todoEnabled: null,
+ todoAutoRefresh: null,
+ autoCompactEnabled: null,
+ soundEnabled: null,
+ };
const refreshAll = async () => {
await Promise.all([refetchDiagnostics(), refetchRawSettings()]);
@@ -112,7 +184,6 @@ export function DroidPage() {
() => Object.entries(diagnostics?.byok.providerBreakdown ?? {}).sort((a, b) => b[1] - a[1]),
[diagnostics?.byok.providerBreakdown]
);
- const rawEditorValidation = validateJsonObject(rawEditorText);
const renderOverview = () => {
if (diagnosticsLoading) {
@@ -202,6 +273,23 @@ export function DroidPage() {
+ {
+ updateSettingsField(key, value);
+ }}
+ onBooleanSettingChange={(key, value) => {
+ updateSettingsField(key, value);
+ }}
+ onNumberSettingChange={(key, value) => {
+ updateSettingsField(key, value);
+ }}
+ />
+
@@ -255,6 +343,54 @@ export function DroidPage() {
))}
+
+
+
+
+ Provider Fact-Check Docs
+
+
+
+
Provider values: {diagnostics.docsReference.providerValues.join(', ')}
@@ -358,11 +494,7 @@ export function DroidPage() {
!rawEditorValidation.valid
}
onChange={(next) => {
- if (next === rawBaseText) {
- setRawDraftText(null);
- return;
- }
- setRawDraftText(next);
+ setRawEditorDraftText(next);
}}
onSave={handleSaveRawSettings}
onRefresh={refreshAll}