refactor(agent): 将 Summary Model 改造为内置 Sub Agent
- 扩展 AgentMode 类型添加 'internal' 模式 - 新增 summary agent preset (claude-3-5-haiku) - AgentRegistry 添加 getInternal/listInternalAgents 方法 - CompressionManager 添加 setSummaryModelFromAgentConfig - Agent 构造函数改用 Registry 配置初始化 Summary 模型 - 清理旧的 SummaryConfig 配置系统 - UI AgentsPanel 分离显示 System/Preset/Custom agents - UI AgentEditor 为 internal agent 显示简化编辑界面
This commit is contained in:
@@ -45,11 +45,9 @@ import type {
|
||||
CustomProviderDefinition,
|
||||
ProviderConfig,
|
||||
ConnectionTestResult,
|
||||
// Context & Summary types
|
||||
// Context types
|
||||
ContextUsageInfo,
|
||||
CompressionResult,
|
||||
SummaryConfigInfo,
|
||||
SummaryConfigInput,
|
||||
} from './types.js';
|
||||
|
||||
// Re-export types
|
||||
@@ -124,8 +122,6 @@ export type {
|
||||
CompressionStatus,
|
||||
CompressionType,
|
||||
CompressionResult,
|
||||
SummaryConfigInfo,
|
||||
SummaryConfigInput,
|
||||
} from './types.js';
|
||||
|
||||
// API Configuration
|
||||
@@ -976,27 +972,3 @@ export async function compressContext(
|
||||
}> {
|
||||
return request('POST', `/sessions/${encodeURIComponent(sessionId)}/compress`, options || {});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取摘要模型配置
|
||||
*/
|
||||
export async function getSummaryConfig(): Promise<{
|
||||
success: boolean;
|
||||
data?: SummaryConfigInfo;
|
||||
error?: string;
|
||||
}> {
|
||||
return request('GET', '/config/summary');
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新摘要模型配置
|
||||
*/
|
||||
export async function updateSummaryConfig(
|
||||
config: SummaryConfigInput
|
||||
): Promise<{
|
||||
success: boolean;
|
||||
data?: SummaryConfigInfo;
|
||||
error?: string;
|
||||
}> {
|
||||
return request('PUT', '/config/summary', config);
|
||||
}
|
||||
|
||||
@@ -280,7 +280,7 @@ export interface HookTestResult {
|
||||
// ============ Agent 相关 ============
|
||||
|
||||
/** Agent 运行模式 */
|
||||
export type AgentMode = 'primary' | 'subagent' | 'all';
|
||||
export type AgentMode = 'primary' | 'subagent' | 'all' | 'internal';
|
||||
|
||||
/** Agent 模型配置 */
|
||||
export interface AgentModelConfig {
|
||||
@@ -756,26 +756,3 @@ export interface CompressionResult {
|
||||
summaryTokens?: number;
|
||||
}
|
||||
|
||||
/** 摘要模型配置信息(不含 API Key 明文) */
|
||||
export interface SummaryConfigInfo {
|
||||
/** 提供商类型 */
|
||||
provider?: string;
|
||||
/** 模型名称 */
|
||||
model?: string;
|
||||
/** 是否已配置 API Key */
|
||||
hasApiKey: boolean;
|
||||
/** 服务地址 */
|
||||
baseUrl?: string;
|
||||
}
|
||||
|
||||
/** 摘要模型配置输入 */
|
||||
export interface SummaryConfigInput {
|
||||
/** 提供商类型 */
|
||||
provider?: string;
|
||||
/** 模型名称 */
|
||||
model?: string;
|
||||
/** API Key */
|
||||
apiKey?: string;
|
||||
/** 服务地址 */
|
||||
baseUrl?: string;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
AlertCircle,
|
||||
Lock,
|
||||
} from 'lucide-react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { toast } from 'sonner';
|
||||
@@ -93,6 +94,9 @@ export function AgentEditor({
|
||||
const [name, setName] = useState(defaultName);
|
||||
const [description, setDescription] = useState('');
|
||||
const [mode, setMode] = useState<AgentMode>('primary');
|
||||
|
||||
// 是否为内部 Agent(Internal Agent 只能编辑模型配置)
|
||||
const isInternalAgent = mode === 'internal';
|
||||
const [prompt, setPrompt] = useState('');
|
||||
const [maxSteps, setMaxSteps] = useState<number | undefined>(undefined);
|
||||
|
||||
@@ -298,12 +302,26 @@ export function AgentEditor({
|
||||
)}
|
||||
<div className={cn(responsive && 'mt-2 md:mt-0')}>
|
||||
<h2 className="text-lg font-semibold flex items-center gap-2">
|
||||
<Bot size={20} className="text-primary-400" />
|
||||
{isInternalAgent ? (
|
||||
<Lock size={20} className="text-slate-400" />
|
||||
) : (
|
||||
<Bot size={20} className="text-primary-400" />
|
||||
)}
|
||||
{isNewAgent ? 'Create Agent' : `Edit: ${agentName}`}
|
||||
{isInternalAgent && (
|
||||
<span className="text-xs px-2 py-0.5 bg-slate-500/20 text-slate-400 rounded">
|
||||
System
|
||||
</span>
|
||||
)}
|
||||
</h2>
|
||||
{copyFrom && (
|
||||
<p className="text-xs text-gray-500">Copying from: {copyFrom}</p>
|
||||
)}
|
||||
{isInternalAgent && (
|
||||
<p className="text-xs text-slate-400 mt-1">
|
||||
System agents can only modify model configuration
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
@@ -354,12 +372,12 @@ export function AgentEditor({
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
disabled={!isNewAgent}
|
||||
disabled={!isNewAgent || isInternalAgent}
|
||||
placeholder="my-agent"
|
||||
className={cn(
|
||||
'w-full px-3 py-2 bg-gray-900 border border-gray-700 rounded-lg text-sm',
|
||||
'focus:outline-none focus:border-primary-500',
|
||||
!isNewAgent && 'opacity-50 cursor-not-allowed'
|
||||
(!isNewAgent || isInternalAgent) && 'opacity-50 cursor-not-allowed'
|
||||
)}
|
||||
/>
|
||||
{!isNewAgent && (
|
||||
@@ -374,59 +392,81 @@ export function AgentEditor({
|
||||
type="text"
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
disabled={isInternalAgent}
|
||||
placeholder="A helpful coding assistant"
|
||||
className="w-full px-3 py-2 bg-gray-900 border border-gray-700 rounded-lg text-sm focus:outline-none focus:border-primary-500"
|
||||
className={cn(
|
||||
'w-full px-3 py-2 bg-gray-900 border border-gray-700 rounded-lg text-sm focus:outline-none focus:border-primary-500',
|
||||
isInternalAgent && 'opacity-50 cursor-not-allowed'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Mode */}
|
||||
<div>
|
||||
<label className="block text-xs text-gray-400 mb-1">Mode *</label>
|
||||
<div className="flex gap-2">
|
||||
{(['primary', 'subagent', 'all'] as const).map((m) => (
|
||||
<button
|
||||
key={m}
|
||||
type="button"
|
||||
onClick={() => setMode(m)}
|
||||
className={cn(
|
||||
'px-3 py-2 rounded-lg text-sm transition-colors',
|
||||
mode === m
|
||||
? 'bg-primary-500 text-white'
|
||||
: 'bg-gray-900 text-gray-400 hover:bg-gray-800'
|
||||
)}
|
||||
>
|
||||
{m === 'primary' ? 'Primary' : m === 'subagent' ? 'Subagent' : 'Both'}
|
||||
</button>
|
||||
))}
|
||||
{/* Mode - 不为内部 Agent 显示 */}
|
||||
{!isInternalAgent && (
|
||||
<div>
|
||||
<label className="block text-xs text-gray-400 mb-1">Mode *</label>
|
||||
<div className="flex gap-2">
|
||||
{(['primary', 'subagent', 'all'] as const).map((m) => (
|
||||
<button
|
||||
key={m}
|
||||
type="button"
|
||||
onClick={() => setMode(m)}
|
||||
className={cn(
|
||||
'px-3 py-2 rounded-lg text-sm transition-colors',
|
||||
mode === m
|
||||
? 'bg-primary-500 text-white'
|
||||
: 'bg-gray-900 text-gray-400 hover:bg-gray-800'
|
||||
)}
|
||||
>
|
||||
{m === 'primary' ? 'Primary' : m === 'subagent' ? 'Subagent' : 'Both'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
{mode === 'primary'
|
||||
? 'Can be used as the main agent'
|
||||
: mode === 'subagent'
|
||||
? 'Can only be spawned by other agents'
|
||||
: 'Can be used in both modes'}
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
{mode === 'primary'
|
||||
? 'Can be used as the main agent'
|
||||
: mode === 'subagent'
|
||||
? 'Can only be spawned by other agents'
|
||||
: 'Can be used in both modes'}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Internal Mode 显示只读标签 */}
|
||||
{isInternalAgent && (
|
||||
<div>
|
||||
<label className="block text-xs text-gray-400 mb-1">Mode</label>
|
||||
<div className="px-3 py-2 bg-slate-500/10 border border-slate-600/30 rounded-lg text-sm text-slate-400 inline-flex items-center gap-2">
|
||||
<Lock size={14} />
|
||||
Internal (System Agent)
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
System agents are used internally and cannot be called directly
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* System Prompt */}
|
||||
<CollapsibleSection title="System Prompt" defaultOpen={!!prompt}>
|
||||
<div>
|
||||
<textarea
|
||||
value={prompt}
|
||||
onChange={(e) => setPrompt(e.target.value)}
|
||||
placeholder="You are a helpful assistant..."
|
||||
rows={8}
|
||||
className="w-full px-3 py-2 bg-gray-900 border border-gray-700 rounded-lg text-sm font-mono focus:outline-none focus:border-primary-500 resize-y"
|
||||
/>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
Custom system prompt for this agent. Leave empty to use defaults.
|
||||
</p>
|
||||
</div>
|
||||
</CollapsibleSection>
|
||||
{/* System Prompt - 不为内部 Agent 显示 */}
|
||||
{!isInternalAgent && (
|
||||
<CollapsibleSection title="System Prompt" defaultOpen={!!prompt}>
|
||||
<div>
|
||||
<textarea
|
||||
value={prompt}
|
||||
onChange={(e) => setPrompt(e.target.value)}
|
||||
placeholder="You are a helpful assistant..."
|
||||
rows={8}
|
||||
className="w-full px-3 py-2 bg-gray-900 border border-gray-700 rounded-lg text-sm font-mono focus:outline-none focus:border-primary-500 resize-y"
|
||||
/>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
Custom system prompt for this agent. Leave empty to use defaults.
|
||||
</p>
|
||||
</div>
|
||||
</CollapsibleSection>
|
||||
)}
|
||||
|
||||
{/* Model Configuration */}
|
||||
<CollapsibleSection title="Model Configuration" defaultOpen={!!modelProvider || !!modelName}>
|
||||
{/* Model Configuration - 内部 Agent 默认展开 */}
|
||||
<CollapsibleSection title="Model Configuration" defaultOpen={isInternalAgent || !!modelProvider || !!modelName}>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{/* Provider */}
|
||||
<div>
|
||||
@@ -489,83 +529,87 @@ export function AgentEditor({
|
||||
</div>
|
||||
</CollapsibleSection>
|
||||
|
||||
{/* Tool Configuration */}
|
||||
<CollapsibleSection title="Tool Configuration" defaultOpen={toolMode !== 'all' || noTask}>
|
||||
<div className="space-y-4">
|
||||
{/* Tool Mode */}
|
||||
<div>
|
||||
<label className="block text-xs text-gray-400 mb-1">Tool Access</label>
|
||||
<div className="flex gap-2">
|
||||
{(['all', 'enabled', 'disabled'] as const).map((m) => (
|
||||
<button
|
||||
key={m}
|
||||
type="button"
|
||||
onClick={() => setToolMode(m)}
|
||||
className={cn(
|
||||
'px-3 py-2 rounded-lg text-sm transition-colors',
|
||||
toolMode === m
|
||||
? 'bg-primary-500 text-white'
|
||||
: 'bg-gray-900 text-gray-400 hover:bg-gray-800'
|
||||
)}
|
||||
>
|
||||
{m === 'all' ? 'All Tools' : m === 'enabled' ? 'Only These' : 'Except These'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tool List */}
|
||||
{toolMode !== 'all' && (
|
||||
{/* Tool Configuration - 不为内部 Agent 显示 */}
|
||||
{!isInternalAgent && (
|
||||
<CollapsibleSection title="Tool Configuration" defaultOpen={toolMode !== 'all' || noTask}>
|
||||
<div className="space-y-4">
|
||||
{/* Tool Mode */}
|
||||
<div>
|
||||
<label className="block text-xs text-gray-400 mb-1">
|
||||
{toolMode === 'enabled' ? 'Enabled Tools' : 'Disabled Tools'}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={toolList}
|
||||
onChange={(e) => setToolList(e.target.value)}
|
||||
placeholder="bash, read_file, write_file"
|
||||
className="w-full px-3 py-2 bg-gray-900 border border-gray-700 rounded-lg text-sm focus:outline-none focus:border-primary-500"
|
||||
/>
|
||||
<p className="text-xs text-gray-500 mt-1">Comma-separated tool names</p>
|
||||
<label className="block text-xs text-gray-400 mb-1">Tool Access</label>
|
||||
<div className="flex gap-2">
|
||||
{(['all', 'enabled', 'disabled'] as const).map((m) => (
|
||||
<button
|
||||
key={m}
|
||||
type="button"
|
||||
onClick={() => setToolMode(m)}
|
||||
className={cn(
|
||||
'px-3 py-2 rounded-lg text-sm transition-colors',
|
||||
toolMode === m
|
||||
? 'bg-primary-500 text-white'
|
||||
: 'bg-gray-900 text-gray-400 hover:bg-gray-800'
|
||||
)}
|
||||
>
|
||||
{m === 'all' ? 'All Tools' : m === 'enabled' ? 'Only These' : 'Except These'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* No Task */}
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="noTask"
|
||||
checked={noTask}
|
||||
onChange={(e) => setNoTask(e.target.checked)}
|
||||
className="w-4 h-4 rounded border-gray-600 bg-gray-900 text-primary-500 focus:ring-primary-500"
|
||||
/>
|
||||
<label htmlFor="noTask" className="text-sm text-gray-300">
|
||||
Disable nested Task calls
|
||||
</label>
|
||||
{/* Tool List */}
|
||||
{toolMode !== 'all' && (
|
||||
<div>
|
||||
<label className="block text-xs text-gray-400 mb-1">
|
||||
{toolMode === 'enabled' ? 'Enabled Tools' : 'Disabled Tools'}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={toolList}
|
||||
onChange={(e) => setToolList(e.target.value)}
|
||||
placeholder="bash, read_file, write_file"
|
||||
className="w-full px-3 py-2 bg-gray-900 border border-gray-700 rounded-lg text-sm focus:outline-none focus:border-primary-500"
|
||||
/>
|
||||
<p className="text-xs text-gray-500 mt-1">Comma-separated tool names</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* No Task */}
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="noTask"
|
||||
checked={noTask}
|
||||
onChange={(e) => setNoTask(e.target.checked)}
|
||||
className="w-4 h-4 rounded border-gray-600 bg-gray-900 text-primary-500 focus:ring-primary-500"
|
||||
/>
|
||||
<label htmlFor="noTask" className="text-sm text-gray-300">
|
||||
Disable nested Task calls
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CollapsibleSection>
|
||||
</CollapsibleSection>
|
||||
)}
|
||||
|
||||
{/* Execution Limits */}
|
||||
<CollapsibleSection title="Execution Limits" defaultOpen={maxSteps !== undefined}>
|
||||
<div>
|
||||
<label className="block text-xs text-gray-400 mb-1">Max Steps</label>
|
||||
<input
|
||||
type="number"
|
||||
value={maxSteps ?? ''}
|
||||
onChange={(e) =>
|
||||
setMaxSteps(e.target.value ? parseInt(e.target.value) : undefined)
|
||||
}
|
||||
placeholder="15"
|
||||
min="1"
|
||||
className="w-full px-3 py-2 bg-gray-900 border border-gray-700 rounded-lg text-sm focus:outline-none focus:border-primary-500"
|
||||
/>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
Maximum number of tool call steps. Leave empty for default.
|
||||
</p>
|
||||
</div>
|
||||
</CollapsibleSection>
|
||||
{/* Execution Limits - 不为内部 Agent 显示 */}
|
||||
{!isInternalAgent && (
|
||||
<CollapsibleSection title="Execution Limits" defaultOpen={maxSteps !== undefined}>
|
||||
<div>
|
||||
<label className="block text-xs text-gray-400 mb-1">Max Steps</label>
|
||||
<input
|
||||
type="number"
|
||||
value={maxSteps ?? ''}
|
||||
onChange={(e) =>
|
||||
setMaxSteps(e.target.value ? parseInt(e.target.value) : undefined)
|
||||
}
|
||||
placeholder="15"
|
||||
min="1"
|
||||
className="w-full px-3 py-2 bg-gray-900 border border-gray-700 rounded-lg text-sm focus:outline-none focus:border-primary-500"
|
||||
/>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
Maximum number of tool call steps. Leave empty for default.
|
||||
</p>
|
||||
</div>
|
||||
</CollapsibleSection>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
Sparkles,
|
||||
Cpu,
|
||||
Layers,
|
||||
Lock,
|
||||
} from 'lucide-react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { toast } from 'sonner';
|
||||
@@ -52,6 +53,8 @@ function getModeColor(mode: AgentListItem['mode']) {
|
||||
return 'bg-purple-500/20 text-purple-400';
|
||||
case 'all':
|
||||
return 'bg-green-500/20 text-green-400';
|
||||
case 'internal':
|
||||
return 'bg-slate-500/20 text-slate-400';
|
||||
default:
|
||||
return 'bg-gray-500/20 text-gray-400';
|
||||
}
|
||||
@@ -66,6 +69,8 @@ function getModeText(mode: AgentListItem['mode']) {
|
||||
return 'Subagent';
|
||||
case 'all':
|
||||
return 'Both';
|
||||
case 'internal':
|
||||
return 'Internal';
|
||||
default:
|
||||
return mode;
|
||||
}
|
||||
@@ -189,9 +194,10 @@ export function AgentsPanel({ onClose, responsive = false }: AgentsPanelProps) {
|
||||
setAgentDetails({});
|
||||
};
|
||||
|
||||
// 统计
|
||||
const presetAgents = agents.filter((a) => a.isPreset);
|
||||
const customAgents = agents.filter((a) => !a.isPreset);
|
||||
// 统计 - 按分类过滤
|
||||
const internalAgents = agents.filter((a) => a.mode === 'internal');
|
||||
const presetAgents = agents.filter((a) => a.isPreset && a.mode !== 'internal');
|
||||
const customAgents = agents.filter((a) => !a.isPreset && a.mode !== 'internal');
|
||||
|
||||
// Loading 骨架屏
|
||||
const LoadingSkeleton = () => (
|
||||
@@ -214,6 +220,7 @@ export function AgentsPanel({ onClose, responsive = false }: AgentsPanelProps) {
|
||||
const isExpanded = expandedAgents.has(agent.name);
|
||||
const isLoading = actionLoading === agent.name;
|
||||
const detail = agentDetails[agent.name];
|
||||
const isInternal = agent.mode === 'internal';
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
@@ -236,7 +243,9 @@ export function AgentsPanel({ onClose, responsive = false }: AgentsPanelProps) {
|
||||
</button>
|
||||
|
||||
{/* Icon */}
|
||||
{agent.isPreset ? (
|
||||
{isInternal ? (
|
||||
<Lock size={16} className="text-slate-400" />
|
||||
) : agent.isPreset ? (
|
||||
<Sparkles size={16} className="text-yellow-400" />
|
||||
) : (
|
||||
<Bot size={16} className="text-primary-400" />
|
||||
@@ -264,8 +273,18 @@ export function AgentsPanel({ onClose, responsive = false }: AgentsPanelProps) {
|
||||
<div className="animate-spin rounded-full h-5 w-5 border-t-2 border-b-2 border-primary-500" />
|
||||
) : (
|
||||
<>
|
||||
{/* View/Edit */}
|
||||
{agent.isPreset && !agent.isCustomized ? (
|
||||
{/* View/Edit - Internal agents can be edited (model config) */}
|
||||
{isInternal ? (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setEditingAgent({ name: agent.name, isNew: false })}
|
||||
className="text-blue-400 hover:text-blue-300"
|
||||
title="Edit Model Config"
|
||||
>
|
||||
<Edit3 size={14} />
|
||||
</Button>
|
||||
) : agent.isPreset && !agent.isCustomized ? (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
@@ -287,19 +306,21 @@ export function AgentsPanel({ onClose, responsive = false }: AgentsPanelProps) {
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Copy */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleCopy(agent.name)}
|
||||
className="text-gray-400 hover:text-gray-300"
|
||||
title="Copy"
|
||||
>
|
||||
<Copy size={14} />
|
||||
</Button>
|
||||
{/* Copy - not for internal agents */}
|
||||
{!isInternal && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleCopy(agent.name)}
|
||||
className="text-gray-400 hover:text-gray-300"
|
||||
title="Copy"
|
||||
>
|
||||
<Copy size={14} />
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Delete (only for custom agents) */}
|
||||
{!agent.isPreset && (
|
||||
{/* Delete (only for custom agents, not internal) */}
|
||||
{!agent.isPreset && !isInternal && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
@@ -475,7 +496,7 @@ export function AgentsPanel({ onClose, responsive = false }: AgentsPanelProps) {
|
||||
Agent Presets
|
||||
</h2>
|
||||
<p className="text-xs text-gray-500">
|
||||
{agents.length} agents ({presetAgents.length} preset, {customAgents.length} custom)
|
||||
{agents.length} agents ({internalAgents.length} system, {presetAgents.length} preset, {customAgents.length} custom)
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -541,6 +562,21 @@ export function AgentsPanel({ onClose, responsive = false }: AgentsPanelProps) {
|
||||
animate={{ opacity: 1 }}
|
||||
className={cn('space-y-4', responsive ? 'p-4' : 'p-4')}
|
||||
>
|
||||
{/* System Agents (Internal) */}
|
||||
{internalAgents.length > 0 && (
|
||||
<div>
|
||||
<h3 className="text-xs font-medium text-slate-400 uppercase tracking-wide mb-2 flex items-center gap-2">
|
||||
<Lock size={12} />
|
||||
System Agents
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
{internalAgents.map((agent) => (
|
||||
<AgentItem key={agent.name} agent={agent} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Preset Agents */}
|
||||
{presetAgents.length > 0 && (
|
||||
<div>
|
||||
|
||||
@@ -12,15 +12,11 @@ import { cn } from '../utils/cn';
|
||||
import { modalOverlay, modalContent, smoothTransition } from '../utils/animations';
|
||||
import { Button } from '../primitives/Button';
|
||||
import { Input } from '../primitives/Input';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../primitives/Select';
|
||||
import { Skeleton } from './Skeleton';
|
||||
import {
|
||||
getConfig,
|
||||
updateConfig,
|
||||
getSummaryConfig,
|
||||
updateSummaryConfig,
|
||||
type ServerConfig,
|
||||
type SummaryConfigInfo,
|
||||
} from '../api/client.js';
|
||||
|
||||
interface ConfigPanelProps {
|
||||
@@ -29,58 +25,26 @@ interface ConfigPanelProps {
|
||||
responsive?: boolean;
|
||||
}
|
||||
|
||||
// 摘要模型提供商列表
|
||||
const SUMMARY_PROVIDERS = [
|
||||
{ id: 'openai', name: 'OpenAI' },
|
||||
{ id: 'deepseek', name: 'DeepSeek' },
|
||||
{ id: 'anthropic', name: 'Anthropic' },
|
||||
{ id: 'openai-compatible', name: 'OpenAI Compatible' },
|
||||
];
|
||||
|
||||
export function ConfigPanel({ onClose, responsive = false }: ConfigPanelProps) {
|
||||
const [config, setConfig] = useState<ServerConfig | null>(null);
|
||||
const [summaryConfig, setSummaryConfig] = useState<SummaryConfigInfo | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [savingSummary, setSavingSummary] = useState(false);
|
||||
|
||||
// 表单状态
|
||||
const [formData, setFormData] = useState({
|
||||
workdir: '',
|
||||
});
|
||||
|
||||
// 摘要模型表单状态
|
||||
const [summaryFormData, setSummaryFormData] = useState({
|
||||
provider: '',
|
||||
model: '',
|
||||
apiKey: '',
|
||||
baseUrl: '',
|
||||
});
|
||||
|
||||
// 加载配置
|
||||
useEffect(() => {
|
||||
async function loadConfig() {
|
||||
try {
|
||||
// 并行加载主配置和摘要配置
|
||||
const [configResponse, summaryResponse] = await Promise.all([
|
||||
getConfig(),
|
||||
getSummaryConfig(),
|
||||
]);
|
||||
const configResponse = await getConfig();
|
||||
|
||||
setConfig(configResponse.data);
|
||||
setFormData({
|
||||
workdir: configResponse.data.workdir,
|
||||
});
|
||||
|
||||
if (summaryResponse.success && summaryResponse.data) {
|
||||
setSummaryConfig(summaryResponse.data);
|
||||
setSummaryFormData({
|
||||
provider: summaryResponse.data.provider || '',
|
||||
model: summaryResponse.data.model || '',
|
||||
apiKey: '', // 不显示已保存的 API Key
|
||||
baseUrl: summaryResponse.data.baseUrl || '',
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
toast.error(err instanceof Error ? err.message : 'Failed to load config');
|
||||
} finally {
|
||||
@@ -105,33 +69,6 @@ export function ConfigPanel({ onClose, responsive = false }: ConfigPanelProps) {
|
||||
}
|
||||
};
|
||||
|
||||
// 保存摘要配置
|
||||
const handleSaveSummary = async () => {
|
||||
setSavingSummary(true);
|
||||
try {
|
||||
// 只发送有变化的字段
|
||||
const updateData: { provider?: string; model?: string; apiKey?: string; baseUrl?: string } = {};
|
||||
if (summaryFormData.provider) updateData.provider = summaryFormData.provider;
|
||||
if (summaryFormData.model) updateData.model = summaryFormData.model;
|
||||
if (summaryFormData.apiKey) updateData.apiKey = summaryFormData.apiKey;
|
||||
if (summaryFormData.baseUrl) updateData.baseUrl = summaryFormData.baseUrl;
|
||||
|
||||
const response = await updateSummaryConfig(updateData);
|
||||
if (response.success && response.data) {
|
||||
setSummaryConfig(response.data);
|
||||
// 清空 API Key 输入框
|
||||
setSummaryFormData((prev) => ({ ...prev, apiKey: '' }));
|
||||
toast.success('Summary model config saved');
|
||||
} else {
|
||||
toast.error(response.error || 'Failed to save summary config');
|
||||
}
|
||||
} catch (err) {
|
||||
toast.error(err instanceof Error ? err.message : 'Failed to save summary config');
|
||||
} finally {
|
||||
setSavingSummary(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 重置为默认值
|
||||
const handleReset = () => {
|
||||
if (config) {
|
||||
@@ -228,96 +165,6 @@ export function ConfigPanel({ onClose, responsive = false }: ConfigPanelProps) {
|
||||
/>
|
||||
<p className="text-xs text-gray-500">Root directory for file operations</p>
|
||||
</div>
|
||||
|
||||
{/* Summary Model Config */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.15 }}
|
||||
className="pt-4 border-t border-gray-700"
|
||||
>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-sm font-medium text-gray-400">Summary Model</h3>
|
||||
{summaryConfig?.hasApiKey && (
|
||||
<span className="text-xs text-green-400">✓ Configured</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 mb-4">
|
||||
Configure a separate model for context compression. Uses a smaller, faster model to summarize conversation history.
|
||||
</p>
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* Provider */}
|
||||
<div className="space-y-2">
|
||||
<label className="block text-xs font-medium text-gray-400">Provider</label>
|
||||
<Select
|
||||
value={summaryFormData.provider}
|
||||
onValueChange={(value) => setSummaryFormData({ ...summaryFormData, provider: value })}
|
||||
>
|
||||
<SelectTrigger className="h-9">
|
||||
<SelectValue placeholder="Select provider" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{SUMMARY_PROVIDERS.map((provider) => (
|
||||
<SelectItem key={provider.id} value={provider.id}>
|
||||
{provider.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* Model Name */}
|
||||
<div className="space-y-2">
|
||||
<label className="block text-xs font-medium text-gray-400">Model</label>
|
||||
<Input
|
||||
value={summaryFormData.model}
|
||||
onChange={(e) => setSummaryFormData({ ...summaryFormData, model: e.target.value })}
|
||||
className="h-9"
|
||||
placeholder="e.g., deepseek-chat, gpt-4o-mini"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* API Key */}
|
||||
<div className="space-y-2">
|
||||
<label className="block text-xs font-medium text-gray-400">
|
||||
API Key {summaryConfig?.hasApiKey && <span className="text-gray-500">(saved)</span>}
|
||||
</label>
|
||||
<Input
|
||||
type="password"
|
||||
value={summaryFormData.apiKey}
|
||||
onChange={(e) => setSummaryFormData({ ...summaryFormData, apiKey: e.target.value })}
|
||||
className="h-9 font-mono"
|
||||
placeholder={summaryConfig?.hasApiKey ? '••••••••' : 'sk-...'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Base URL (optional) */}
|
||||
{(summaryFormData.provider === 'openai-compatible' || summaryFormData.baseUrl) && (
|
||||
<div className="space-y-2">
|
||||
<label className="block text-xs font-medium text-gray-400">Base URL (optional)</label>
|
||||
<Input
|
||||
value={summaryFormData.baseUrl}
|
||||
onChange={(e) => setSummaryFormData({ ...summaryFormData, baseUrl: e.target.value })}
|
||||
className="h-9 font-mono"
|
||||
placeholder="https://api.example.com/v1"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Save Button */}
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={handleSaveSummary}
|
||||
disabled={savingSummary || (!summaryFormData.provider && !summaryFormData.model && !summaryFormData.apiKey)}
|
||||
className="w-full"
|
||||
>
|
||||
{savingSummary ? 'Saving...' : 'Save Summary Config'}
|
||||
</Button>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -90,11 +90,9 @@ export {
|
||||
deleteProvider,
|
||||
addProviderModel,
|
||||
deleteProviderModel,
|
||||
// Context & Summary API
|
||||
// Context API
|
||||
getContextUsage,
|
||||
compressContext,
|
||||
getSummaryConfig,
|
||||
updateSummaryConfig,
|
||||
} from './api/client.js';
|
||||
|
||||
// Types
|
||||
@@ -166,13 +164,11 @@ export type {
|
||||
CustomProviderDefinition,
|
||||
ProviderConfig,
|
||||
ConnectionTestResult,
|
||||
// Context & Summary types
|
||||
// Context types
|
||||
TokenUsage,
|
||||
ContextUsageInfo,
|
||||
CompressionStatus,
|
||||
CompressionResult,
|
||||
SummaryConfigInfo,
|
||||
SummaryConfigInput,
|
||||
} from './api/client.js';
|
||||
|
||||
// Primitives (shadcn/ui style)
|
||||
|
||||
Reference in New Issue
Block a user