feat: Add intelligent auto-router and enhanced integrations

- Add intelligent-router.sh hook for automatic agent routing
- Add AUTO-TRIGGER-SUMMARY.md documentation
- Add FINAL-INTEGRATION-SUMMARY.md documentation
- Complete Prometheus integration (6 commands + 4 tools)
- Complete Dexto integration (12 commands + 5 tools)
- Enhanced Ralph with access to all agents
- Fix /clawd command (removed disable-model-invocation)
- Update hooks.json to v5 with intelligent routing
- 291 total skills now available
- All 21 commands with automatic routing

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
admin
2026-01-28 00:27:56 +04:00
Unverified
parent 3b128ba3bd
commit b52318eeae
1724 changed files with 351216 additions and 0 deletions

View File

@@ -0,0 +1,304 @@
import React, { useState } from 'react';
import { Input } from '../../ui/input';
import { LabelWithTooltip } from '../../ui/label-with-tooltip';
import { Collapsible } from '../../ui/collapsible';
import { Eye, EyeOff } from 'lucide-react';
import { LLM_PROVIDERS, isReasoningCapableModel, type AgentConfig } from '@dexto/core';
type LLMConfig = AgentConfig['llm'];
interface LLMConfigSectionProps {
value: LLMConfig;
onChange: (value: LLMConfig) => void;
errors?: Record<string, string>;
open?: boolean;
onOpenChange?: (open: boolean) => void;
errorCount?: number;
sectionErrors?: string[];
}
export function LLMConfigSection({
value,
onChange,
errors = {},
open,
onOpenChange,
errorCount = 0,
sectionErrors = [],
}: LLMConfigSectionProps) {
const [showApiKey, setShowApiKey] = useState(false);
const handleChange = (field: keyof LLMConfig, newValue: string | number | undefined) => {
onChange({ ...value, [field]: newValue } as LLMConfig);
};
return (
<Collapsible
title="LLM Configuration"
defaultOpen={true}
open={open}
onOpenChange={onOpenChange}
errorCount={errorCount}
sectionErrors={sectionErrors}
>
<div className="space-y-4">
{/* Provider */}
<div>
<LabelWithTooltip
htmlFor="provider"
tooltip="The LLM provider to use (e.g., OpenAI, Anthropic)"
>
Provider *
</LabelWithTooltip>
<select
id="provider"
value={value.provider || ''}
onChange={(e) => handleChange('provider', e.target.value)}
aria-invalid={!!errors['llm.provider']}
className="flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive"
>
<option value="">Select provider...</option>
{LLM_PROVIDERS.map((p) => (
<option key={p} value={p}>
{p}
</option>
))}
</select>
{errors['llm.provider'] && (
<p className="text-xs text-destructive mt-1">{errors['llm.provider']}</p>
)}
</div>
{/* Model */}
<div>
<LabelWithTooltip
htmlFor="model"
tooltip="The specific model identifier (e.g., gpt-5, claude-sonnet-4-5-20250929)"
>
Model *
</LabelWithTooltip>
<Input
id="model"
value={value.model || ''}
onChange={(e) => handleChange('model', e.target.value)}
placeholder="e.g., gpt-5, claude-sonnet-4-5-20250929"
aria-invalid={!!errors['llm.model']}
/>
{errors['llm.model'] && (
<p className="text-xs text-destructive mt-1">{errors['llm.model']}</p>
)}
</div>
{/* API Key */}
<div>
<LabelWithTooltip
htmlFor="apiKey"
tooltip="Use $ENV_VAR for environment variables or enter the API key directly"
>
API Key *
</LabelWithTooltip>
<div className="relative">
<Input
id="apiKey"
type={showApiKey ? 'text' : 'password'}
value={value.apiKey ?? ''}
onChange={(e) => handleChange('apiKey', e.target.value)}
placeholder="$OPENAI_API_KEY or direct value"
aria-invalid={!!errors['llm.apiKey']}
className="pr-10"
/>
<button
type="button"
onClick={() => setShowApiKey(!showApiKey)}
className="absolute right-2 top-1/2 -translate-y-1/2 p-1 hover:bg-accent rounded transition-colors"
aria-label={showApiKey ? 'Hide API key' : 'Show API key'}
>
{showApiKey ? (
<EyeOff className="h-4 w-4 text-muted-foreground" />
) : (
<Eye className="h-4 w-4 text-muted-foreground" />
)}
</button>
</div>
{errors['llm.apiKey'] && (
<p className="text-xs text-destructive mt-1">{errors['llm.apiKey']}</p>
)}
</div>
{/* Max Iterations */}
<div>
<LabelWithTooltip
htmlFor="maxIterations"
tooltip="Maximum number of agent reasoning iterations per turn"
>
Max Iterations
</LabelWithTooltip>
<Input
id="maxIterations"
type="number"
value={value.maxIterations !== undefined ? value.maxIterations : ''}
onChange={(e) => {
const val = e.target.value;
if (val === '') {
handleChange('maxIterations', undefined);
} else {
const num = parseInt(val, 10);
if (!isNaN(num)) {
handleChange('maxIterations', num);
}
}
}}
min="1"
placeholder="50"
aria-invalid={!!errors['llm.maxIterations']}
/>
{errors['llm.maxIterations'] && (
<p className="text-xs text-destructive mt-1">
{errors['llm.maxIterations']}
</p>
)}
</div>
{/* Base URL */}
<div>
<LabelWithTooltip
htmlFor="baseURL"
tooltip="Custom base URL for the LLM provider (optional, for proxies or custom endpoints)"
>
Base URL
</LabelWithTooltip>
<Input
id="baseURL"
value={value.baseURL || ''}
onChange={(e) => handleChange('baseURL', e.target.value || undefined)}
placeholder="https://api.openai.com/v1"
aria-invalid={!!errors['llm.baseURL']}
/>
{errors['llm.baseURL'] && (
<p className="text-xs text-destructive mt-1">{errors['llm.baseURL']}</p>
)}
</div>
{/* Temperature */}
<div>
<LabelWithTooltip
htmlFor="temperature"
tooltip="Controls randomness in responses (0.0 = deterministic, 1.0 = creative)"
>
Temperature
</LabelWithTooltip>
<Input
id="temperature"
type="number"
value={value.temperature !== undefined ? value.temperature : ''}
onChange={(e) =>
handleChange(
'temperature',
e.target.value ? parseFloat(e.target.value) : undefined
)
}
min="0"
max="1"
step="0.1"
placeholder="0.0 - 1.0"
aria-invalid={!!errors['llm.temperature']}
/>
{errors['llm.temperature'] && (
<p className="text-xs text-destructive mt-1">{errors['llm.temperature']}</p>
)}
</div>
{/* Max Input/Output Tokens */}
<div className="grid grid-cols-2 gap-4">
<div>
<LabelWithTooltip
htmlFor="maxInputTokens"
tooltip="Maximum input tokens to send to the model. If not specified, defaults to model's limit from registry, or 128,000 tokens for custom endpoints"
>
Max Input Tokens
</LabelWithTooltip>
<Input
id="maxInputTokens"
type="number"
value={value.maxInputTokens || ''}
onChange={(e) =>
handleChange(
'maxInputTokens',
e.target.value ? parseInt(e.target.value, 10) : undefined
)
}
min="1"
placeholder="Auto (128k fallback)"
aria-invalid={!!errors['llm.maxInputTokens']}
/>
{errors['llm.maxInputTokens'] && (
<p className="text-xs text-destructive mt-1">
{errors['llm.maxInputTokens']}
</p>
)}
</div>
<div>
<LabelWithTooltip
htmlFor="maxOutputTokens"
tooltip="Maximum output tokens the model can generate. If not specified, uses provider's default (typically 4,096 tokens)"
>
Max Output Tokens
</LabelWithTooltip>
<Input
id="maxOutputTokens"
type="number"
value={value.maxOutputTokens || ''}
onChange={(e) =>
handleChange(
'maxOutputTokens',
e.target.value ? parseInt(e.target.value, 10) : undefined
)
}
min="1"
placeholder="Auto (provider default)"
aria-invalid={!!errors['llm.maxOutputTokens']}
/>
{errors['llm.maxOutputTokens'] && (
<p className="text-xs text-destructive mt-1">
{errors['llm.maxOutputTokens']}
</p>
)}
</div>
</div>
{/* Provider-Specific Options */}
{/* Reasoning Effort - Only for models that support it (o1, o3, codex, gpt-5.x) */}
{value.model && isReasoningCapableModel(value.model) && (
<div>
<LabelWithTooltip
htmlFor="reasoningEffort"
tooltip="Controls reasoning depth for OpenAI models (o1, o3, codex, gpt-5.x). Higher = more thorough but slower/costlier. 'medium' is recommended for most tasks."
>
Reasoning Effort
</LabelWithTooltip>
<select
id="reasoningEffort"
value={value.reasoningEffort || ''}
onChange={(e) =>
handleChange('reasoningEffort', e.target.value || undefined)
}
className="flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
>
<option value="">Auto (model default)</option>
<option value="none">None - No reasoning</option>
<option value="minimal">Minimal - Barely any reasoning</option>
<option value="low">Low - Light reasoning</option>
<option value="medium">Medium - Balanced (recommended)</option>
<option value="high">High - Thorough reasoning</option>
<option value="xhigh">Extra High - Maximum quality</option>
</select>
<p className="text-xs text-muted-foreground mt-1">
Only applies to reasoning models (o1, o3, codex, gpt-5.x)
</p>
</div>
)}
</div>
</Collapsible>
);
}

View File

@@ -0,0 +1,538 @@
import React, { useState } from 'react';
import { Input } from '../../ui/input';
import { LabelWithTooltip } from '../../ui/label-with-tooltip';
import { Button } from '../../ui/button';
import { Collapsible } from '../../ui/collapsible';
import { Plus, Trash2, ChevronDown, ChevronUp } from 'lucide-react';
import type { AgentConfig } from '@dexto/core';
import { MCP_SERVER_TYPES, MCP_CONNECTION_MODES, DEFAULT_MCP_CONNECTION_MODE } from '@dexto/core';
type McpServersConfig = NonNullable<AgentConfig['mcpServers']>;
interface McpServersSectionProps {
value: McpServersConfig;
onChange: (value: McpServersConfig) => void;
errors?: Record<string, string>;
open?: boolean;
onOpenChange?: (open: boolean) => void;
errorCount?: number;
sectionErrors?: string[];
}
export function McpServersSection({
value,
onChange,
errors = {},
open,
onOpenChange,
errorCount = 0,
sectionErrors = [],
}: McpServersSectionProps) {
const [expandedServers, setExpandedServers] = useState<Set<string>>(new Set());
// Local state for text fields that need special parsing (args, env, headers)
// Key is "serverName:fieldName", value is the raw string being edited
const [editingFields, setEditingFields] = useState<Record<string, string>>({});
const servers = Object.entries(value || {});
const toggleServer = (name: string) => {
setExpandedServers((prev) => {
const next = new Set(prev);
if (next.has(name)) {
next.delete(name);
} else {
next.add(name);
}
return next;
});
};
const addServer = () => {
const newName = `server-${Object.keys(value || {}).length + 1}`;
onChange({
...value,
[newName]: {
type: 'stdio',
command: '',
connectionMode: 'strict',
},
});
setExpandedServers((prev) => new Set(prev).add(newName));
};
const removeServer = (name: string) => {
const newValue = { ...value };
delete newValue[name];
onChange(newValue);
setExpandedServers((prev) => {
const next = new Set(prev);
next.delete(name);
return next;
});
};
const updateServer = (
oldName: string,
updates: Partial<Record<string, unknown> & { name?: string }>
) => {
const server = value[oldName];
// Extract name from updates if present (it's not part of the server config, just used for the key)
const { name: newName, ...serverUpdates } = updates;
const newServer = { ...server, ...serverUpdates } as McpServersConfig[string];
// If name changed via updates, handle the name change
if (newName && typeof newName === 'string' && newName !== oldName) {
// Guard against collision: prevent overwriting an existing server
if (value[newName]) {
// TODO: Surface a user-facing error via onChange/errors map or toast notification
return; // No-op to avoid overwriting an existing server
}
const newValue = { ...value };
delete newValue[oldName];
newValue[newName] = newServer;
onChange(newValue);
// Update expanded state
setExpandedServers((prev) => {
const next = new Set(prev);
if (next.has(oldName)) {
next.delete(oldName);
next.add(newName);
}
return next;
});
} else {
onChange({ ...value, [oldName]: newServer });
}
};
// Get the current value for a field (either from editing state or from config)
const getFieldValue = (serverName: string, fieldName: string, fallback: string): string => {
const key = `${serverName}:${fieldName}`;
return editingFields[key] ?? fallback;
};
// Update local editing state while typing
const setFieldValue = (serverName: string, fieldName: string, value: string) => {
const key = `${serverName}:${fieldName}`;
setEditingFields((prev) => ({ ...prev, [key]: value }));
};
// Clear editing state for a field
const clearFieldValue = (serverName: string, fieldName: string) => {
const key = `${serverName}:${fieldName}`;
setEditingFields((prev) => {
const next = { ...prev };
delete next[key];
return next;
});
};
// Parse and commit args on blur
const commitArgs = (serverName: string, argsString: string) => {
clearFieldValue(serverName, 'args');
if (!argsString.trim()) {
updateServer(serverName, { args: undefined });
return;
}
const args = argsString
.split(',')
.map((arg) => arg.trim())
.filter(Boolean);
updateServer(serverName, { args: args.length > 0 ? args : undefined });
};
// Parse and commit env on blur
const commitEnv = (serverName: string, envString: string) => {
clearFieldValue(serverName, 'env');
if (!envString.trim()) {
updateServer(serverName, { env: undefined });
return;
}
const env: Record<string, string> = {};
envString
.split('\n')
.map((line) => line.trim())
.filter(Boolean)
.forEach((line) => {
const [key, ...valueParts] = line.split('=');
if (key && valueParts.length > 0) {
env[key.trim()] = valueParts.join('=').trim();
}
});
updateServer(serverName, { env: Object.keys(env).length > 0 ? env : undefined });
};
// Parse and commit headers on blur
const commitHeaders = (serverName: string, headersString: string) => {
clearFieldValue(serverName, 'headers');
if (!headersString.trim()) {
updateServer(serverName, { headers: undefined });
return;
}
const headers: Record<string, string> = {};
headersString
.split('\n')
.map((line) => line.trim())
.filter(Boolean)
.forEach((line) => {
const [key, ...valueParts] = line.split('=');
if (key && valueParts.length > 0) {
headers[key.trim()] = valueParts.join('=').trim();
}
});
updateServer(serverName, {
headers: Object.keys(headers).length > 0 ? headers : undefined,
});
};
return (
<Collapsible
title="MCP Servers"
defaultOpen={false}
open={open}
onOpenChange={onOpenChange}
errorCount={errorCount}
sectionErrors={sectionErrors}
>
<div className="space-y-4">
{servers.length === 0 ? (
<p className="text-sm text-muted-foreground">No MCP servers configured</p>
) : (
servers.map(([name, server]) => {
const isExpanded = expandedServers.has(name);
return (
<div
key={name}
className="border border-border rounded-lg overflow-hidden"
>
{/* Server Header */}
<div className="flex items-center justify-between px-3 py-2 bg-muted/30">
<button
onClick={() => toggleServer(name)}
className="flex items-center gap-2 flex-1 text-left hover:text-foreground transition-colors"
>
{isExpanded ? (
<ChevronUp className="h-4 w-4" />
) : (
<ChevronDown className="h-4 w-4" />
)}
<span className="font-medium text-sm">{name}</span>
{'command' in server && server.command && (
<span className="text-xs text-muted-foreground truncate">
({server.command})
</span>
)}
</button>
<Button
variant="ghost"
size="sm"
onClick={() => removeServer(name)}
className="h-7 w-7 p-0"
>
<Trash2 className="h-3.5 w-3.5 text-destructive" />
</Button>
</div>
{/* Server Details */}
{isExpanded && (
<div className="px-3 py-3 space-y-3">
{/* Server Name */}
<div>
<LabelWithTooltip
htmlFor={`server-name-${name}`}
tooltip="Unique identifier for this MCP server"
>
Server Name
</LabelWithTooltip>
<Input
id={`server-name-${name}`}
value={name}
onChange={(e) =>
updateServer(name, { name: e.target.value })
}
placeholder="e.g., filesystem"
/>
</div>
{/* Server Type */}
<div>
<LabelWithTooltip
htmlFor={`server-type-${name}`}
tooltip="MCP server connection type"
>
Connection Type *
</LabelWithTooltip>
<select
id={`server-type-${name}`}
value={server.type || 'stdio'}
onChange={(e) => {
const type = e.target.value as
| 'stdio'
| 'sse'
| 'http';
if (type === 'stdio') {
updateServer(name, {
type: 'stdio',
command: '',
args: undefined,
env: undefined,
});
} else {
updateServer(name, {
type,
url: '',
headers: undefined,
});
}
}}
className="flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
>
{MCP_SERVER_TYPES.map((type) => (
<option key={type} value={type}>
{type === 'stdio'
? 'Standard I/O (stdio)'
: type === 'sse'
? 'Server-Sent Events (SSE)'
: 'HTTP'}
</option>
))}
</select>
</div>
{/* stdio-specific fields */}
{server.type === 'stdio' && (
<>
{/* Command */}
<div>
<LabelWithTooltip
htmlFor={`server-command-${name}`}
tooltip="The command to execute (e.g., npx, node, python)"
>
Command *
</LabelWithTooltip>
<Input
id={`server-command-${name}`}
value={
'command' in server
? server.command
: ''
}
onChange={(e) =>
updateServer(name, {
command: e.target.value,
})
}
placeholder="e.g., npx, node, python"
aria-invalid={
!!errors[`mcpServers.${name}.command`]
}
/>
{errors[`mcpServers.${name}.command`] && (
<p className="text-xs text-destructive mt-1">
{errors[`mcpServers.${name}.command`]}
</p>
)}
</div>
{/* Arguments */}
<div>
<LabelWithTooltip
htmlFor={`server-args-${name}`}
tooltip="Command arguments, comma-separated"
>
Arguments
</LabelWithTooltip>
<Input
id={`server-args-${name}`}
value={getFieldValue(
name,
'args',
('args' in server && server.args
? server.args
: []
).join(', ')
)}
onChange={(e) =>
setFieldValue(
name,
'args',
e.target.value
)
}
onBlur={(e) =>
commitArgs(name, e.target.value)
}
placeholder="--port, 3000, --host, localhost"
className="font-mono"
/>
</div>
{/* Environment Variables */}
<div>
<LabelWithTooltip
htmlFor={`server-env-${name}`}
tooltip="Environment variables in KEY=value format, one per line"
>
Environment Variables
</LabelWithTooltip>
<textarea
id={`server-env-${name}`}
value={getFieldValue(
name,
'env',
Object.entries(
('env' in server && server.env) ||
{}
)
.map(([k, v]) => `${k}=${v}`)
.join('\n')
)}
onChange={(e) =>
setFieldValue(
name,
'env',
e.target.value
)
}
onBlur={(e) =>
commitEnv(name, e.target.value)
}
placeholder={`API_KEY=$MY_API_KEY\nPORT=3000`}
rows={4}
className="flex w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 font-mono"
/>
</div>
</>
)}
{/* sse/http-specific fields */}
{(server.type === 'sse' || server.type === 'http') && (
<>
{/* URL */}
<div>
<LabelWithTooltip
htmlFor={`server-url-${name}`}
tooltip="The URL endpoint for the MCP server"
>
URL *
</LabelWithTooltip>
<Input
id={`server-url-${name}`}
value={'url' in server ? server.url : ''}
onChange={(e) =>
updateServer(name, {
url: e.target.value,
})
}
placeholder="https://example.com/mcp"
aria-invalid={
!!errors[`mcpServers.${name}.url`]
}
/>
{errors[`mcpServers.${name}.url`] && (
<p className="text-xs text-destructive mt-1">
{errors[`mcpServers.${name}.url`]}
</p>
)}
</div>
{/* Headers */}
<div>
<LabelWithTooltip
htmlFor={`server-headers-${name}`}
tooltip="HTTP headers in KEY=value format, one per line"
>
Headers
</LabelWithTooltip>
<textarea
id={`server-headers-${name}`}
value={getFieldValue(
name,
'headers',
Object.entries(
('headers' in server &&
server.headers) ||
{}
)
.map(([k, v]) => `${k}=${v}`)
.join('\n')
)}
onChange={(e) =>
setFieldValue(
name,
'headers',
e.target.value
)
}
onBlur={(e) =>
commitHeaders(name, e.target.value)
}
placeholder={`Authorization=Bearer token\nContent-Type=application/json`}
rows={4}
className="flex w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 font-mono"
/>
</div>
</>
)}
{/* Connection Mode */}
<div>
<LabelWithTooltip
htmlFor={`server-mode-${name}`}
tooltip="Strict mode fails on any error; lenient mode continues despite errors"
>
Connection Mode
</LabelWithTooltip>
<select
id={`server-mode-${name}`}
value={
server.connectionMode ||
DEFAULT_MCP_CONNECTION_MODE
}
onChange={(e) =>
updateServer(name, {
connectionMode: e.target.value as
| 'strict'
| 'lenient',
})
}
className="flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
>
{MCP_CONNECTION_MODES.map((mode) => (
<option key={mode} value={mode}>
{mode.charAt(0).toUpperCase() +
mode.slice(1)}
</option>
))}
</select>
</div>
</div>
)}
</div>
);
})
)}
{/* Add Server Button */}
<Button onClick={addServer} variant="outline" size="sm" className="w-full">
<Plus className="h-4 w-4 mr-2" />
Add MCP Server
</Button>
{errors.mcpServers && (
<p className="text-xs text-destructive mt-1">{errors.mcpServers}</p>
)}
</div>
</Collapsible>
);
}

View File

@@ -0,0 +1,183 @@
import React from 'react';
import { Input } from '../../ui/input';
import { LabelWithTooltip } from '../../ui/label-with-tooltip';
import { Collapsible } from '../../ui/collapsible';
import type { AgentConfig, CacheType, DatabaseType } from '@dexto/core';
import { CACHE_TYPES, DATABASE_TYPES } from '@dexto/core';
type StorageConfig = NonNullable<AgentConfig['storage']>;
interface StorageSectionProps {
value: StorageConfig;
onChange: (value: StorageConfig) => void;
errors?: Record<string, string>;
open?: boolean;
onOpenChange?: (open: boolean) => void;
errorCount?: number;
sectionErrors?: string[];
}
export function StorageSection({
value,
onChange,
errors = {},
open,
onOpenChange,
errorCount = 0,
sectionErrors = [],
}: StorageSectionProps) {
const updateCache = (updates: Partial<Record<string, unknown>>) => {
onChange({
...value,
cache: { ...value.cache, ...updates } as StorageConfig['cache'],
});
};
const updateDatabase = (updates: Partial<Record<string, unknown>>) => {
onChange({
...value,
database: { ...value.database, ...updates } as StorageConfig['database'],
});
};
const showCacheUrl = value.cache.type === 'redis';
const showDatabaseUrl = value.database.type === 'sqlite' || value.database.type === 'postgres';
return (
<Collapsible
title="Storage Configuration"
defaultOpen={false}
open={open}
onOpenChange={onOpenChange}
errorCount={errorCount}
sectionErrors={sectionErrors}
>
<div className="space-y-6">
{/* Cache Configuration */}
<div className="space-y-3">
<h4 className="text-sm font-medium">Cache</h4>
<div>
<LabelWithTooltip
htmlFor="cache-type"
tooltip="Storage backend for caching data (in-memory or Redis)"
>
Cache Type
</LabelWithTooltip>
<select
id="cache-type"
value={value.cache.type}
onChange={(e) => updateCache({ type: e.target.value as CacheType })}
className="flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
>
{CACHE_TYPES.map((type) => (
<option key={type} value={type}>
{type}
</option>
))}
</select>
</div>
{showCacheUrl && 'url' in value.cache && (
<div>
<LabelWithTooltip
htmlFor="cache-url"
tooltip="Redis connection URL (e.g., redis://localhost:6379)"
>
Redis URL
</LabelWithTooltip>
<Input
id="cache-url"
value={value.cache.url || ''}
onChange={(e) => updateCache({ url: e.target.value || undefined })}
placeholder="redis://localhost:6379"
aria-invalid={!!errors['storage.cache.url']}
/>
{errors['storage.cache.url'] && (
<p className="text-xs text-destructive mt-1">
{errors['storage.cache.url']}
</p>
)}
</div>
)}
</div>
{/* Database Configuration */}
<div className="space-y-3">
<h4 className="text-sm font-medium">Database</h4>
<div>
<LabelWithTooltip
htmlFor="database-type"
tooltip="Storage backend for persistent data (in-memory, SQLite, or PostgreSQL)"
>
Database Type
</LabelWithTooltip>
<select
id="database-type"
value={value.database.type}
onChange={(e) =>
updateDatabase({ type: e.target.value as DatabaseType })
}
className="flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
>
{DATABASE_TYPES.map((type) => (
<option key={type} value={type}>
{type}
</option>
))}
</select>
</div>
{showDatabaseUrl && (
<div>
<LabelWithTooltip
htmlFor="database-url"
tooltip={
value.database.type === 'sqlite'
? 'File path for SQLite database'
: 'PostgreSQL connection string'
}
>
{value.database.type === 'sqlite'
? 'SQLite Path'
: 'PostgreSQL URL'}
</LabelWithTooltip>
<Input
id="database-url"
value={
('url' in value.database && value.database.url) ||
('path' in value.database && value.database.path) ||
''
}
onChange={(e) => {
if (value.database.type === 'sqlite') {
updateDatabase({ path: e.target.value || undefined });
} else {
updateDatabase({ url: e.target.value || undefined });
}
}}
placeholder={
value.database.type === 'sqlite'
? './dexto.db'
: 'postgresql://user:pass@localhost:5432/dexto'
}
aria-invalid={
!!(
errors['storage.database.url'] ||
errors['storage.database.path']
)
}
/>
{(errors['storage.database.url'] ||
errors['storage.database.path']) && (
<p className="text-xs text-destructive mt-1">
{errors['storage.database.url'] ||
errors['storage.database.path']}
</p>
)}
</div>
)}
</div>
</div>
</Collapsible>
);
}

View File

@@ -0,0 +1,582 @@
import React, { useState } from 'react';
import { Input } from '../../ui/input';
import { Textarea } from '../../ui/textarea';
import { LabelWithTooltip } from '../../ui/label-with-tooltip';
import { Button } from '../../ui/button';
import { Collapsible } from '../../ui/collapsible';
import { Plus, Trash2, ChevronDown, ChevronUp } from 'lucide-react';
import { PROMPT_GENERATOR_SOURCES } from '@dexto/core';
import type { ContributorConfig } from '@dexto/core';
// Component works with the object form of SystemPromptConfig (not the string form)
type SystemPromptConfigObject = {
contributors: ContributorConfig[];
};
interface SystemPromptSectionProps {
value: SystemPromptConfigObject;
onChange: (value: SystemPromptConfigObject) => void;
errors?: Record<string, string>;
open?: boolean;
onOpenChange?: (open: boolean) => void;
errorCount?: number;
sectionErrors?: string[];
}
export function SystemPromptSection({
value,
onChange,
errors = {},
open,
onOpenChange,
errorCount = 0,
sectionErrors = [],
}: SystemPromptSectionProps) {
const [expandedContributors, setExpandedContributors] = useState<Set<string>>(new Set());
// Local state for file paths (comma-separated editing)
const [editingFiles, setEditingFiles] = useState<Record<string, string>>({});
const contributors = value.contributors || [];
const toggleContributor = (id: string) => {
setExpandedContributors((prev) => {
const next = new Set(prev);
if (next.has(id)) {
next.delete(id);
} else {
next.add(id);
}
return next;
});
};
const addContributor = () => {
const newId = `contributor-${Date.now()}`;
const newContributor: ContributorConfig = {
id: newId,
type: 'static',
priority: contributors.length * 10,
enabled: true,
content: '',
};
onChange({
contributors: [...contributors, newContributor],
});
setExpandedContributors((prev) => new Set(prev).add(newId));
};
const removeContributor = (id: string) => {
onChange({
contributors: contributors.filter((c) => c.id !== id),
});
setExpandedContributors((prev) => {
const next = new Set(prev);
next.delete(id);
return next;
});
};
const updateContributor = (id: string, updates: Partial<ContributorConfig>) => {
onChange({
contributors: contributors.map((c) => {
if (c.id === id) {
// If ID is changing, handle the ID change
if (updates.id && updates.id !== id) {
// Update expanded state
setExpandedContributors((prev) => {
const next = new Set(prev);
if (next.has(id)) {
next.delete(id);
next.add(updates.id!);
}
return next;
});
}
// If type is changing, create a new contributor with the new type
if (updates.type && updates.type !== c.type) {
const baseFields = {
id: updates.id || c.id,
priority:
updates.priority !== undefined ? updates.priority : c.priority,
enabled: updates.enabled !== undefined ? updates.enabled : c.enabled,
};
if (updates.type === 'static') {
return {
...baseFields,
type: 'static',
content: '',
} as ContributorConfig;
} else if (updates.type === 'dynamic') {
return {
...baseFields,
type: 'dynamic',
source: 'date',
} as ContributorConfig;
} else if (updates.type === 'file') {
return { ...baseFields, type: 'file', files: [] } as ContributorConfig;
}
}
return { ...c, ...updates } as ContributorConfig;
}
return c;
}),
});
};
// Get the current value for file paths (either from editing state or from config)
const getFilesValue = (id: string, files: string[]): string => {
return editingFiles[id] ?? files.join(', ');
};
// Update local editing state while typing
const setFilesValue = (id: string, value: string) => {
setEditingFiles((prev) => ({ ...prev, [id]: value }));
};
// Parse and commit files on blur
const commitFiles = (id: string, filesString: string) => {
setEditingFiles((prev) => {
const next = { ...prev };
delete next[id];
return next;
});
const files = filesString
.split(',')
.map((f) => f.trim())
.filter(Boolean);
updateContributor(id, { files: files.length > 0 ? files : [] });
};
return (
<Collapsible
title="System Prompt"
defaultOpen={true}
open={open}
onOpenChange={onOpenChange}
errorCount={errorCount}
sectionErrors={sectionErrors}
>
<div className="space-y-4">
<p className="text-sm text-muted-foreground">
Define how the agent should behave using multiple contributors with different
priorities.
</p>
{contributors.length === 0 ? (
<p className="text-sm text-muted-foreground">No contributors configured</p>
) : (
contributors.map((contributor) => {
const isExpanded = expandedContributors.has(contributor.id);
return (
<div
key={contributor.id}
className="border border-border rounded-lg overflow-hidden"
>
{/* Contributor Header */}
<div className="flex items-center justify-between px-3 py-2 bg-muted/30">
<button
onClick={() => toggleContributor(contributor.id)}
className="flex items-center gap-2 flex-1 text-left hover:text-foreground transition-colors"
>
{isExpanded ? (
<ChevronUp className="h-4 w-4" />
) : (
<ChevronDown className="h-4 w-4" />
)}
<span className="font-medium text-sm">
{contributor.id}
</span>
<span className="text-xs text-muted-foreground">
({contributor.type}, priority: {contributor.priority})
</span>
{contributor.enabled === false && (
<span className="text-xs text-destructive">
(disabled)
</span>
)}
</button>
<Button
variant="ghost"
size="sm"
onClick={() => removeContributor(contributor.id)}
className="h-7 w-7 p-0"
>
<Trash2 className="h-3.5 w-3.5 text-destructive" />
</Button>
</div>
{/* Contributor Details */}
{isExpanded && (
<div className="px-3 py-3 space-y-3">
{/* Common Fields */}
<div>
<LabelWithTooltip
htmlFor={`contributor-id-${contributor.id}`}
tooltip="Unique identifier for this contributor"
>
ID *
</LabelWithTooltip>
<Input
id={`contributor-id-${contributor.id}`}
value={contributor.id}
onChange={(e) =>
updateContributor(contributor.id, {
id: e.target.value,
})
}
placeholder="e.g., primary, date"
/>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<LabelWithTooltip
htmlFor={`contributor-type-${contributor.id}`}
tooltip="Type of contributor: static (fixed text), dynamic (runtime generated), or file (from files)"
>
Type *
</LabelWithTooltip>
<select
id={`contributor-type-${contributor.id}`}
value={contributor.type}
onChange={(e) =>
updateContributor(contributor.id, {
type: e.target.value as
| 'static'
| 'dynamic'
| 'file',
})
}
className="flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
>
<option value="static">Static</option>
<option value="dynamic">Dynamic</option>
<option value="file">File</option>
</select>
</div>
<div>
<LabelWithTooltip
htmlFor={`contributor-priority-${contributor.id}`}
tooltip="Execution priority (lower numbers run first)"
>
Priority *
</LabelWithTooltip>
<Input
id={`contributor-priority-${contributor.id}`}
type="number"
value={contributor.priority}
onChange={(e) => {
const val = e.target.value;
const num = Number.parseInt(val, 10);
updateContributor(contributor.id, {
priority: Number.isNaN(num) ? 0 : num,
});
}}
placeholder="0"
min="0"
/>
</div>
</div>
<div>
<label className="flex items-center gap-2 text-sm">
<input
type="checkbox"
checked={contributor.enabled !== false}
onChange={(e) =>
updateContributor(contributor.id, {
enabled: e.target.checked,
})
}
className="rounded border-input"
/>
<span>Enabled</span>
</label>
</div>
{/* Type-specific Fields */}
{contributor.type === 'static' && (
<div>
<LabelWithTooltip
htmlFor={`contributor-content-${contributor.id}`}
tooltip="Static content for the system prompt"
>
Content *
</LabelWithTooltip>
<Textarea
id={`contributor-content-${contributor.id}`}
value={contributor.content}
onChange={(e) =>
updateContributor(contributor.id, {
content: e.target.value,
})
}
placeholder="You are a helpful assistant..."
rows={8}
className="font-mono text-sm"
/>
</div>
)}
{contributor.type === 'dynamic' && (
<div>
<LabelWithTooltip
htmlFor={`contributor-source-${contributor.id}`}
tooltip="Source for dynamic content generation"
>
Source *
</LabelWithTooltip>
<select
id={`contributor-source-${contributor.id}`}
value={contributor.source}
onChange={(e) =>
updateContributor(contributor.id, {
source: e.target.value as Extract<
ContributorConfig,
{ type: 'dynamic' }
>['source'],
})
}
className="flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
>
{PROMPT_GENERATOR_SOURCES.map((source) => (
<option key={source} value={source}>
{source}
</option>
))}
</select>
</div>
)}
{contributor.type === 'file' && (
<>
<div>
<LabelWithTooltip
htmlFor={`contributor-files-${contributor.id}`}
tooltip="File paths to include, comma-separated"
>
Files *
</LabelWithTooltip>
<Input
id={`contributor-files-${contributor.id}`}
value={getFilesValue(
contributor.id,
contributor.files
)}
onChange={(e) =>
setFilesValue(
contributor.id,
e.target.value
)
}
onBlur={(e) =>
commitFiles(
contributor.id,
e.target.value
)
}
placeholder="./commands/context.md, ./commands/rules.txt"
className="font-mono"
/>
</div>
{/* File Options */}
<details className="border border-border rounded-md p-2">
<summary className="text-sm font-medium cursor-pointer">
File Options
</summary>
<div className="mt-3 space-y-3">
<label className="flex items-center gap-2 text-sm">
<input
type="checkbox"
checked={
contributor.options
?.includeFilenames !== false
}
onChange={(e) =>
updateContributor(
contributor.id,
{
options: {
...(contributor.options ??
{}),
includeFilenames:
e.target
.checked,
},
}
)
}
className="rounded border-input"
/>
<span>
Include filenames as headers
</span>
</label>
<label className="flex items-center gap-2 text-sm">
<input
type="checkbox"
checked={
contributor.options
?.includeMetadata === true
}
onChange={(e) =>
updateContributor(
contributor.id,
{
options: {
...(contributor.options ??
{}),
includeMetadata:
e.target
.checked,
},
}
)
}
className="rounded border-input"
/>
<span>Include file metadata</span>
</label>
<div>
<LabelWithTooltip
htmlFor={`contributor-separator-${contributor.id}`}
tooltip="Separator between multiple files"
>
Separator
</LabelWithTooltip>
<Input
id={`contributor-separator-${contributor.id}`}
value={
contributor.options
?.separator ?? '\n\n---\n\n'
}
onChange={(e) =>
updateContributor(
contributor.id,
{
options: {
...(contributor.options ??
{}),
separator:
e.target.value,
},
}
)
}
placeholder="\n\n---\n\n"
/>
</div>
<div>
<LabelWithTooltip
htmlFor={`contributor-error-handling-${contributor.id}`}
tooltip="How to handle missing or unreadable files"
>
Error Handling
</LabelWithTooltip>
<select
id={`contributor-error-handling-${contributor.id}`}
value={
contributor.options
?.errorHandling || 'skip'
}
onChange={(e) =>
updateContributor(
contributor.id,
{
options: {
...(contributor.options ??
{}),
errorHandling: e
.target
.value as
| 'skip'
| 'error',
},
}
)
}
className="flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
>
<option value="skip">
Skip missing files
</option>
<option value="error">
Error on missing files
</option>
</select>
</div>
<div>
<LabelWithTooltip
htmlFor={`contributor-max-file-size-${contributor.id}`}
tooltip="Maximum file size in bytes"
>
Max File Size (bytes)
</LabelWithTooltip>
<Input
id={`contributor-max-file-size-${contributor.id}`}
type="number"
value={
contributor.options
?.maxFileSize || 100000
}
onChange={(e) => {
const val = e.target.value;
const num = Number.parseInt(
val,
10
);
updateContributor(
contributor.id,
{
options: {
...(contributor.options ??
{}),
maxFileSize:
Number.isNaN(
num
)
? undefined
: num,
},
}
);
}}
placeholder="100000"
min="1"
/>
</div>
</div>
</details>
</>
)}
</div>
)}
</div>
);
})
)}
{/* Add Contributor Button */}
<Button onClick={addContributor} variant="outline" size="sm" className="w-full">
<Plus className="h-4 w-4 mr-2" />
Add Contributor
</Button>
{errors.systemPrompt && (
<p className="text-xs text-destructive mt-1">{errors.systemPrompt}</p>
)}
</div>
</Collapsible>
);
}

View File

@@ -0,0 +1,150 @@
import React from 'react';
import { Input } from '../../ui/input';
import { LabelWithTooltip } from '../../ui/label-with-tooltip';
import { Collapsible } from '../../ui/collapsible';
import type { AgentConfig } from '@dexto/core';
import {
TOOL_CONFIRMATION_MODES,
ALLOWED_TOOLS_STORAGE_TYPES,
DEFAULT_TOOL_CONFIRMATION_MODE,
DEFAULT_ALLOWED_TOOLS_STORAGE,
} from '@dexto/core';
type ToolConfirmationConfig = NonNullable<AgentConfig['toolConfirmation']>;
interface ToolConfirmationSectionProps {
value: ToolConfirmationConfig;
onChange: (value: ToolConfirmationConfig) => void;
errors?: Record<string, string>;
open?: boolean;
onOpenChange?: (open: boolean) => void;
errorCount?: number;
sectionErrors?: string[];
}
export function ToolConfirmationSection({
value,
onChange,
errors = {},
open,
onOpenChange,
errorCount = 0,
sectionErrors = [],
}: ToolConfirmationSectionProps) {
const handleChange = (updates: Partial<ToolConfirmationConfig>) => {
onChange({ ...value, ...updates });
};
const updateAllowedToolsStorage = (type: 'memory' | 'storage') => {
onChange({
...value,
allowedToolsStorage: type,
});
};
return (
<Collapsible
title="Tool Confirmation"
defaultOpen={false}
open={open}
onOpenChange={onOpenChange}
errorCount={errorCount}
sectionErrors={sectionErrors}
>
<div className="space-y-4">
{/* Confirmation Mode */}
<div>
<LabelWithTooltip
htmlFor="confirmation-mode"
tooltip="How the agent handles tool execution requests"
>
Confirmation Mode
</LabelWithTooltip>
<select
id="confirmation-mode"
value={value.mode || DEFAULT_TOOL_CONFIRMATION_MODE}
onChange={(e) =>
handleChange({
mode: e.target.value as 'auto-approve' | 'manual' | 'auto-deny',
})
}
className="flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
>
{TOOL_CONFIRMATION_MODES.map((mode) => (
<option key={mode} value={mode}>
{mode === 'auto-approve'
? 'Auto-approve'
: mode === 'manual'
? 'Manual'
: 'Auto-deny'}
</option>
))}
</select>
<p className="text-xs text-muted-foreground mt-1">
{value.mode === 'manual'
? 'Require explicit approval before executing tools'
: value.mode === 'auto-deny'
? 'Automatically deny all tool executions'
: 'Automatically approve tool executions'}
</p>
</div>
{/* Timeout */}
{value.mode === 'manual' && (
<div>
<LabelWithTooltip
htmlFor="confirmation-timeout"
tooltip="How long to wait for approval before timing out"
>
Timeout (seconds)
</LabelWithTooltip>
<Input
id="confirmation-timeout"
type="number"
value={value.timeout || ''}
onChange={(e) =>
handleChange({
timeout: e.target.value
? parseInt(e.target.value, 10)
: undefined,
})
}
min="1"
placeholder="e.g., 60"
aria-invalid={!!errors['toolConfirmation.timeout']}
/>
{errors['toolConfirmation.timeout'] && (
<p className="text-xs text-destructive mt-1">
{errors['toolConfirmation.timeout']}
</p>
)}
</div>
)}
{/* Allowed Tools Storage */}
<div>
<LabelWithTooltip
htmlFor="allowed-tools-storage"
tooltip="Where to store the list of pre-approved tools (memory or persistent storage)"
>
Allowed Tools Storage
</LabelWithTooltip>
<select
id="allowed-tools-storage"
value={value.allowedToolsStorage || DEFAULT_ALLOWED_TOOLS_STORAGE}
onChange={(e) =>
updateAllowedToolsStorage(e.target.value as 'memory' | 'storage')
}
className="flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
>
{ALLOWED_TOOLS_STORAGE_TYPES.map((type) => (
<option key={type} value={type}>
{type}
</option>
))}
</select>
</div>
</div>
</Collapsible>
);
}