mirror of
https://github.com/tiennm99/ccs.git
synced 2026-08-05 22:24:04 +00:00
feat(dashboard): enrich droid docs and quick settings controls
- add reusable compatible-CLI docs registry with external provider fact-check links - render docs links in Droid notes panel and keep future CLI extension points - add left-column quick controls for key settings.json fields (reasoning/autonomy/diff/automation)
This commit is contained in:
@@ -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<string, CompatibleCliDocsRegistryEntry> = {
|
||||||
|
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:<alias>)',
|
||||||
|
'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;
|
||||||
|
}
|
||||||
@@ -44,6 +44,30 @@ export interface DroidByokDiagnostics {
|
|||||||
customModels: DroidCustomModelDiagnostics[];
|
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 {
|
export interface DroidDashboardDiagnostics {
|
||||||
binary: DroidBinaryDiagnostics;
|
binary: DroidBinaryDiagnostics;
|
||||||
files: {
|
files: {
|
||||||
@@ -52,11 +76,7 @@ export interface DroidDashboardDiagnostics {
|
|||||||
};
|
};
|
||||||
byok: DroidByokDiagnostics;
|
byok: DroidByokDiagnostics;
|
||||||
warnings: string[];
|
warnings: string[];
|
||||||
docsReference: {
|
docsReference: CompatibleCliDocsReference;
|
||||||
providerValues: string[];
|
|
||||||
settingsHierarchy: string[];
|
|
||||||
notes: string[];
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DroidRawSettingsResponse {
|
export interface DroidRawSettingsResponse {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
probeJsonObjectFile,
|
probeJsonObjectFile,
|
||||||
writeJsonObjectFileAtomic,
|
writeJsonObjectFileAtomic,
|
||||||
} from './compatible-cli-json-file-service';
|
} from './compatible-cli-json-file-service';
|
||||||
|
import { getCompatibleCliDocsReference } from './compatible-cli-docs-registry';
|
||||||
|
|
||||||
interface DroidConfigPaths {
|
interface DroidConfigPaths {
|
||||||
settingsPath: string;
|
settingsPath: string;
|
||||||
@@ -160,6 +161,7 @@ export function summarizeDroidCustomModels(customModelsValue: unknown): DroidByo
|
|||||||
export async function getDroidDashboardDiagnostics(): Promise<DroidDashboardDiagnostics> {
|
export async function getDroidDashboardDiagnostics(): Promise<DroidDashboardDiagnostics> {
|
||||||
const paths = resolveDroidConfigPaths();
|
const paths = resolveDroidConfigPaths();
|
||||||
const binaryPath = detectDroidCli();
|
const binaryPath = detectDroidCli();
|
||||||
|
const docsReference = getCompatibleCliDocsReference('droid');
|
||||||
|
|
||||||
const source = process.env.CCS_DROID_PATH ? 'CCS_DROID_PATH' : binaryPath ? 'PATH' : 'missing';
|
const source = process.env.CCS_DROID_PATH ? 'CCS_DROID_PATH' : binaryPath ? 'PATH' : 'missing';
|
||||||
|
|
||||||
@@ -204,21 +206,7 @@ export async function getDroidDashboardDiagnostics(): Promise<DroidDashboardDiag
|
|||||||
},
|
},
|
||||||
byok,
|
byok,
|
||||||
warnings,
|
warnings,
|
||||||
docsReference: {
|
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:<alias>)',
|
|
||||||
'droid exec supports --model for one-off execution mode',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import * as path from 'path';
|
|||||||
import {
|
import {
|
||||||
DroidRawSettingsConflictError,
|
DroidRawSettingsConflictError,
|
||||||
DroidRawSettingsValidationError,
|
DroidRawSettingsValidationError,
|
||||||
|
getDroidDashboardDiagnostics,
|
||||||
getDroidRawSettings,
|
getDroidRawSettings,
|
||||||
maskApiKeyPreview,
|
maskApiKeyPreview,
|
||||||
resolveDroidConfigPaths,
|
resolveDroidConfigPaths,
|
||||||
@@ -110,6 +111,19 @@ describe('droid-dashboard-service', () => {
|
|||||||
expect(raw.rawText).toContain('invalid-json');
|
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 () => {
|
it('saves valid raw settings content', async () => {
|
||||||
const result = await saveDroidRawSettings({
|
const result = await saveDroidRawSettings({
|
||||||
rawText: JSON.stringify({
|
rawText: JSON.stringify({
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<CardTitle className="text-base flex items-center gap-2">
|
||||||
|
<SlidersHorizontal className="h-4 w-4" />
|
||||||
|
Quick Settings
|
||||||
|
<Badge variant="outline" className="text-[10px] font-normal">
|
||||||
|
settings.json
|
||||||
|
</Badge>
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
{disabledReason && <p className="text-xs text-amber-600">{disabledReason}</p>}
|
||||||
|
|
||||||
|
<div className="grid gap-3 sm:grid-cols-2">
|
||||||
|
{enumFieldConfig.map((field) => (
|
||||||
|
<div key={field.key} className="space-y-1">
|
||||||
|
<p className="text-xs font-medium">{field.label}</p>
|
||||||
|
<Select
|
||||||
|
value={values[field.key] ?? UNSET_VALUE}
|
||||||
|
onValueChange={(next) =>
|
||||||
|
onEnumSettingChange(field.key, next === UNSET_VALUE ? null : next)
|
||||||
|
}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="h-8 text-xs">
|
||||||
|
<SelectValue placeholder="Use default" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value={UNSET_VALUE}>Use default</SelectItem>
|
||||||
|
{field.options.map((option) => (
|
||||||
|
<SelectItem key={option.value} value={option.value}>
|
||||||
|
{option.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<p className="text-[11px] text-muted-foreground">{field.description}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{numberFieldConfig.map((field) => (
|
||||||
|
<div key={field.key} className="space-y-1">
|
||||||
|
<p className="text-xs font-medium">{field.label}</p>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min={field.min}
|
||||||
|
step={field.step}
|
||||||
|
value={values[field.key] ?? ''}
|
||||||
|
onChange={(event) => {
|
||||||
|
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}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{booleanFieldConfig.map((field) => (
|
||||||
|
<div key={field.key} className="space-y-1">
|
||||||
|
<p className="text-xs font-medium">{field.label}</p>
|
||||||
|
<Select
|
||||||
|
value={toBooleanSelectValue(values[field.key])}
|
||||||
|
onValueChange={(next) => onBooleanSettingChange(field.key, toBooleanValue(next))}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="h-8 text-xs">
|
||||||
|
<SelectValue placeholder="Use default" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value={UNSET_VALUE}>Use default</SelectItem>
|
||||||
|
<SelectItem value="true">true</SelectItem>
|
||||||
|
<SelectItem value="false">false</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -36,6 +36,22 @@ export interface DroidCustomModelDiagnostics {
|
|||||||
apiKeyPreview: string | null;
|
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 {
|
export interface DroidDashboardDiagnostics {
|
||||||
binary: DroidBinaryDiagnostics;
|
binary: DroidBinaryDiagnostics;
|
||||||
files: {
|
files: {
|
||||||
@@ -56,6 +72,8 @@ export interface DroidDashboardDiagnostics {
|
|||||||
providerValues: string[];
|
providerValues: string[];
|
||||||
settingsHierarchy: string[];
|
settingsHierarchy: string[];
|
||||||
notes: string[];
|
notes: string[];
|
||||||
|
links: CompatibleCliDocLink[];
|
||||||
|
providerDocs: CompatibleCliProviderDocLink[];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+140
-8
@@ -4,6 +4,7 @@ import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels';
|
|||||||
import {
|
import {
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
|
ExternalLink,
|
||||||
Folder,
|
Folder,
|
||||||
GripVertical,
|
GripVertical,
|
||||||
Loader2,
|
Loader2,
|
||||||
@@ -15,6 +16,10 @@ import {
|
|||||||
import { useDroid } from '@/hooks/use-droid';
|
import { useDroid } from '@/hooks/use-droid';
|
||||||
import { isApiConflictError } from '@/lib/api-client';
|
import { isApiConflictError } from '@/lib/api-client';
|
||||||
import { RawJsonSettingsEditorPanel } from '@/components/compatible-cli/raw-json-settings-editor-panel';
|
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 { 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';
|
||||||
@@ -33,18 +38,32 @@ function formatBytes(value: number | null | undefined): string {
|
|||||||
return `${(value / (1024 * 1024)).toFixed(2)} MB`;
|
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<string, unknown> } | { valid: false; error: string } {
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(text);
|
const parsed = JSON.parse(text);
|
||||||
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
||||||
return { valid: false, error: 'JSON root must be an object.' };
|
return { valid: false, error: 'JSON root must be an object.' };
|
||||||
}
|
}
|
||||||
return { valid: true };
|
return { valid: true, value: parsed as Record<string, unknown> };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return { valid: false, error: (error as Error).message };
|
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({
|
function DetailRow({
|
||||||
label,
|
label,
|
||||||
value,
|
value,
|
||||||
@@ -79,6 +98,59 @@ export function DroidPage() {
|
|||||||
const rawBaseText = rawSettings?.rawText ?? '{}';
|
const rawBaseText = rawSettings?.rawText ?? '{}';
|
||||||
const rawEditorText = rawDraftText ?? rawBaseText;
|
const rawEditorText = rawDraftText ?? rawBaseText;
|
||||||
const rawConfigDirty = rawDraftText !== null && 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 () => {
|
const refreshAll = async () => {
|
||||||
await Promise.all([refetchDiagnostics(), refetchRawSettings()]);
|
await Promise.all([refetchDiagnostics(), refetchRawSettings()]);
|
||||||
@@ -112,7 +184,6 @@ export function DroidPage() {
|
|||||||
() => Object.entries(diagnostics?.byok.providerBreakdown ?? {}).sort((a, b) => b[1] - a[1]),
|
() => Object.entries(diagnostics?.byok.providerBreakdown ?? {}).sort((a, b) => b[1] - a[1]),
|
||||||
[diagnostics?.byok.providerBreakdown]
|
[diagnostics?.byok.providerBreakdown]
|
||||||
);
|
);
|
||||||
const rawEditorValidation = validateJsonObject(rawEditorText);
|
|
||||||
|
|
||||||
const renderOverview = () => {
|
const renderOverview = () => {
|
||||||
if (diagnosticsLoading) {
|
if (diagnosticsLoading) {
|
||||||
@@ -202,6 +273,23 @@ export function DroidPage() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
<DroidSettingsQuickControlsCard
|
||||||
|
values={quickSettingsValues}
|
||||||
|
disabled={rawSettingsLoading || !rawEditorParsed.valid}
|
||||||
|
disabledReason={
|
||||||
|
rawEditorParsed.valid ? null : `Quick settings disabled: ${rawEditorParsed.error}`
|
||||||
|
}
|
||||||
|
onEnumSettingChange={(key, value) => {
|
||||||
|
updateSettingsField(key, value);
|
||||||
|
}}
|
||||||
|
onBooleanSettingChange={(key, value) => {
|
||||||
|
updateSettingsField(key, value);
|
||||||
|
}}
|
||||||
|
onNumberSettingChange={(key, value) => {
|
||||||
|
updateSettingsField(key, value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
<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">
|
||||||
@@ -255,6 +343,54 @@ export function DroidPage() {
|
|||||||
</p>
|
</p>
|
||||||
))}
|
))}
|
||||||
<Separator />
|
<Separator />
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p className="text-xs text-muted-foreground uppercase tracking-wide">
|
||||||
|
Factory Docs
|
||||||
|
</p>
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
{diagnostics.docsReference.links.map((link) => (
|
||||||
|
<a
|
||||||
|
key={link.id}
|
||||||
|
href={link.url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className="block rounded-md border px-2.5 py-2 transition-colors hover:bg-muted/50"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between gap-2">
|
||||||
|
<span className="text-xs font-medium">{link.label}</span>
|
||||||
|
<ExternalLink className="h-3.5 w-3.5 text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
<p className="mt-0.5 text-[11px] text-muted-foreground">{link.description}</p>
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Separator />
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p className="text-xs text-muted-foreground uppercase tracking-wide">
|
||||||
|
Provider Fact-Check Docs
|
||||||
|
</p>
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
{diagnostics.docsReference.providerDocs.map((providerDoc) => (
|
||||||
|
<a
|
||||||
|
key={`${providerDoc.provider}-${providerDoc.url}`}
|
||||||
|
href={providerDoc.url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className="block rounded-md border px-2.5 py-2 transition-colors hover:bg-muted/50"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between gap-2">
|
||||||
|
<span className="text-xs font-medium">{providerDoc.label}</span>
|
||||||
|
<ExternalLink className="h-3.5 w-3.5 text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
<p className="mt-0.5 text-[11px] text-muted-foreground">
|
||||||
|
provider: {providerDoc.provider} | format: {providerDoc.apiFormat}
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Separator />
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
Provider values: {diagnostics.docsReference.providerValues.join(', ')}
|
Provider values: {diagnostics.docsReference.providerValues.join(', ')}
|
||||||
</p>
|
</p>
|
||||||
@@ -358,11 +494,7 @@ export function DroidPage() {
|
|||||||
!rawEditorValidation.valid
|
!rawEditorValidation.valid
|
||||||
}
|
}
|
||||||
onChange={(next) => {
|
onChange={(next) => {
|
||||||
if (next === rawBaseText) {
|
setRawEditorDraftText(next);
|
||||||
setRawDraftText(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setRawDraftText(next);
|
|
||||||
}}
|
}}
|
||||||
onSave={handleSaveRawSettings}
|
onSave={handleSaveRawSettings}
|
||||||
onRefresh={refreshAll}
|
onRefresh={refreshAll}
|
||||||
|
|||||||
Reference in New Issue
Block a user