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:
2025-12-14 22:12:36 +08:00
parent e97daaa0eb
commit c307cd3a7c
20 changed files with 339 additions and 594 deletions
+3 -1
View File
@@ -5,6 +5,7 @@ import { codeReviewerAgent } from './code-reviewer.js';
import { buildAgent } from './build.js'; import { buildAgent } from './build.js';
import { planAgent } from './plan.js'; import { planAgent } from './plan.js';
import { visionAgent } from './vision.js'; import { visionAgent } from './vision.js';
import { summaryAgent } from './summary.js';
/** /**
* 预设 Agent 集合 * 预设 Agent 集合
@@ -16,6 +17,7 @@ export const presetAgents: Record<string, Omit<AgentInfo, 'name'>> = {
build: buildAgent, build: buildAgent,
plan: planAgent, plan: planAgent,
vision: visionAgent, vision: visionAgent,
summary: summaryAgent,
}; };
/** /**
@@ -32,4 +34,4 @@ export function isPresetAgent(name: string): boolean {
return name in presetAgents; return name in presetAgents;
} }
export { generalAgent, exploreAgent, codeReviewerAgent, buildAgent, planAgent, visionAgent }; export { generalAgent, exploreAgent, codeReviewerAgent, buildAgent, planAgent, visionAgent, summaryAgent };
@@ -0,0 +1,20 @@
import type { AgentInfo } from '../types.js';
/**
* Summary Agent
* 内部 Agent,用于对话压缩时生成摘要
* 推荐使用成本较低的模型
*/
export const summaryAgent: Omit<AgentInfo, 'name'> = {
description: '对话压缩摘要生成(内部使用)',
mode: 'internal',
model: {
provider: 'anthropic',
model: 'claude-3-5-haiku-20241022',
},
tools: {
enabled: [], // 无工具,纯文本生成
noTask: true,
},
maxSteps: 1,
};
+22 -2
View File
@@ -61,16 +61,36 @@ export class AgentRegistry {
/** /**
* 列出可作为子 Agent 的 Agent(供 Task 工具使用) * 列出可作为子 Agent 的 Agent(供 Task 工具使用)
* 排除 primary 和 internal 模式
*/ */
listSubagents(): AgentInfo[] { listSubagents(): AgentInfo[] {
return this.list().filter((a) => a.mode !== 'primary'); return this.list().filter((a) => a.mode !== 'primary' && a.mode !== 'internal');
} }
/** /**
* 列出可作为主交互 Agent 的 Agent(供 /agent 命令使用) * 列出可作为主交互 Agent 的 Agent(供 /agent 命令使用)
* 排除 internal 模式
*/ */
listPrimaryAgents(): AgentInfo[] { listPrimaryAgents(): AgentInfo[] {
return this.list().filter((a) => a.mode !== 'subagent'); return this.list().filter((a) => a.mode !== 'subagent' && a.mode !== 'internal');
}
/**
* 获取内部 Agentinternal 模式)
*/
getInternal(name: string): AgentInfo | undefined {
const agent = this.get(name);
if (agent?.mode === 'internal') {
return agent;
}
return undefined;
}
/**
* 列出所有内部 Agent
*/
listInternalAgents(): AgentInfo[] {
return this.list().filter((a) => a.mode === 'internal');
} }
/** /**
+2 -1
View File
@@ -9,8 +9,9 @@ export type { PermissionAction, PermissionRule };
* - primary: 主 Agent,用户直接使用 * - primary: 主 Agent,用户直接使用
* - subagent: 子 Agent,由 Task 工具调用 * - subagent: 子 Agent,由 Task 工具调用
* - all: 两种方式都可以 * - all: 两种方式都可以
* - internal: 内部使用,不对外暴露(如 Summary Agent
*/ */
export type AgentMode = 'primary' | 'subagent' | 'all'; export type AgentMode = 'primary' | 'subagent' | 'all' | 'internal';
/** /**
* Agent Bash 权限配置 * Agent Bash 权限配置
+28
View File
@@ -9,6 +9,8 @@ import {
CompressionStatus, CompressionStatus,
DEFAULT_COMPRESSION_CONFIG, DEFAULT_COMPRESSION_CONFIG,
} from './types.js'; } from './types.js';
import type { AgentModelConfig } from '../agent/types.js';
import { getProviderRegistry } from '../provider/index.js';
/** /**
* 压缩管理器 * 压缩管理器
@@ -41,6 +43,32 @@ export class CompressionManager {
this.summaryModel = model; this.summaryModel = model;
} }
/**
* 从 Agent 配置设置摘要模型
* @param config Agent 的模型配置
* @param apiKey API Key
* @param baseUrl 可选的 Base URL
*/
setSummaryModelFromAgentConfig(
config: AgentModelConfig,
apiKey: string,
baseUrl?: string
): void {
const registry = getProviderRegistry();
const provider = config.provider || 'anthropic';
const modelName = config.model || 'claude-3-5-haiku-20241022';
try {
const getModel = registry.getModelFactory(provider, {
apiKey,
baseUrl,
});
this.summaryModel = getModel(modelName);
} catch (error) {
console.warn('[CompressionManager] Failed to create summary model:', error);
}
}
/** /**
* 获取用于摘要生成的模型 * 获取用于摘要生成的模型
* 优先使用专用摘要模型,无则使用主模型 * 优先使用专用摘要模型,无则使用主模型
+33 -10
View File
@@ -18,8 +18,8 @@ import {
} from '../context/index.js'; } from '../context/index.js';
import type { AgentInfo, ImageData } from '../agent/types.js'; import type { AgentInfo, ImageData } from '../agent/types.js';
import { agentRegistry, AgentExecutor } from '../agent/index.js'; import { agentRegistry, AgentExecutor } from '../agent/index.js';
import { loadVisionConfig, loadSummaryConfig } from '../utils/config.js'; import { loadVisionConfig } from '../utils/config.js';
import { getProviderRegistry } from '../provider/index.js'; import { getProviderRegistry, resolveApiKey } from '../provider/index.js';
import { getHookManager } from '../hooks/index.js'; import { getHookManager } from '../hooks/index.js';
import { getGitManager } from '../git/index.js'; import { getGitManager } from '../git/index.js';
@@ -62,15 +62,38 @@ export class Agent {
// 设置主模型(作为摘要模型的后备) // 设置主模型(作为摘要模型的后备)
this.compressionManager.setModel(this.getModel(config.model)); this.compressionManager.setModel(this.getModel(config.model));
// 加载摘要模型配置(可选,用于降低压缩成本) // 从 Agent Registry 加载 Summary Agent 配置
const summaryConfig = loadSummaryConfig(); this.initSummaryModel(config, providerRegistry);
if (summaryConfig) { }
const summaryModelFactory = providerRegistry.getModelFactory(summaryConfig.provider, {
apiKey: summaryConfig.apiKey, /**
baseUrl: summaryConfig.baseUrl, * 从 Agent Registry 初始化 Summary 模型
}); */
this.compressionManager.setSummaryModel(summaryModelFactory(summaryConfig.model)); private initSummaryModel(
config: AgentConfig,
providerRegistry: ReturnType<typeof getProviderRegistry>
): void {
// 获取 Summary Agentinternal 模式)
const summaryAgentInfo = agentRegistry.getInternal('summary');
if (!summaryAgentInfo?.model) {
return;
} }
const modelConfig = summaryAgentInfo.model;
// 确定 provider(默认使用主配置的 provider
const provider = modelConfig.provider || config.provider;
// 从 ProviderRegistry 获取 API Key
const providerConfig = providerRegistry.getConfig(provider);
const apiKey = resolveApiKey(providerConfig) || config.apiKey;
if (!apiKey) {
return;
}
// 设置 Summary 模型
const baseUrl = providerConfig?.baseUrl;
this.compressionManager.setSummaryModelFromAgentConfig(modelConfig, apiKey, baseUrl);
} }
/** /**
+2 -2
View File
@@ -1,7 +1,7 @@
export { Agent } from './core/agent.js'; export { Agent } from './core/agent.js';
export { toolRegistry, todoManager, initTaskContext, updateTaskDescription, updateSkillDescription } from './tools/index.js'; export { toolRegistry, todoManager, initTaskContext, updateTaskDescription, updateSkillDescription } from './tools/index.js';
export { loadConfig, saveConfig, getConfig, loadVisionConfig, loadSummaryConfig } from './utils/config.js'; export { loadConfig, saveConfig, getConfig, loadVisionConfig } from './utils/config.js';
export type { VisionConfig, SummaryConfig } from './utils/config.js'; export type { VisionConfig } from './utils/config.js';
// Context compression // Context compression
export { export {
-64
View File
@@ -19,11 +19,6 @@ interface StoredConfig {
visionModel?: string; visionModel?: string;
/** Vision 专用的 Base URL(用于 OpenAI 兼容的 Vision 服务) */ /** Vision 专用的 Base URL(用于 OpenAI 兼容的 Vision 服务) */
visionBaseUrl?: string; visionBaseUrl?: string;
// Summary 配置(用于对话压缩摘要生成)
summaryProvider?: ProviderType;
summaryModel?: string;
/** Summary 专用的 Base URL(用于 OpenAI 兼容的 Summary 服务) */
summaryBaseUrl?: string;
} }
// Vision 配置接口 // Vision 配置接口
@@ -35,15 +30,6 @@ export interface VisionConfig {
baseUrl?: string; baseUrl?: string;
} }
// Summary 配置接口(用于对话压缩摘要生成)
export interface SummaryConfig {
provider: ProviderType;
apiKey: string;
model: string;
/** 自定义 Base URL(用于 OpenAI 兼容的 Summary 服务) */
baseUrl?: string;
}
// 默认模型配置 // 默认模型配置
const DEFAULT_MODELS: Record<ProviderType, string> = { const DEFAULT_MODELS: Record<ProviderType, string> = {
anthropic: 'claude-sonnet-4-20250514', anthropic: 'claude-sonnet-4-20250514',
@@ -58,13 +44,6 @@ const DEFAULT_VISION_MODELS: Record<ProviderType, string> = {
openai: 'gpt-4o', openai: 'gpt-4o',
}; };
// 默认 Summary 模型(推荐使用成本较低的模型)
const DEFAULT_SUMMARY_MODELS: Record<ProviderType, string> = {
anthropic: 'claude-3-5-haiku-20241022',
deepseek: 'deepseek-chat',
openai: 'gpt-4o-mini',
};
// 默认系统提示词 // 默认系统提示词
const DEFAULT_SYSTEM_PROMPT = `你是一个运行在终端中的 AI 编程助手。你可以帮助用户: const DEFAULT_SYSTEM_PROMPT = `你是一个运行在终端中的 AI 编程助手。你可以帮助用户:
- 读取和写入文件 - 读取和写入文件
@@ -167,49 +146,6 @@ export function loadVisionConfig(): VisionConfig | null {
}; };
} }
/**
* 加载 Summary 配置
* Summary 用于对话压缩时生成摘要,推荐使用成本较低的小模型
* 通过 ProviderRegistry 获取 API Key
*/
export function loadSummaryConfig(): SummaryConfig | null {
// 从配置文件读取
const storedConfig = getConfig();
// 如果没有任何 summary 相关配置,返回 null(使用主模型)
const hasSummaryConfig = storedConfig.summaryProvider || storedConfig.summaryModel;
if (!hasSummaryConfig) {
return null;
}
// 确定 summary provider(默认使用主配置的 provider
const mainProvider = storedConfig.provider || 'anthropic';
const finalProvider = storedConfig.summaryProvider || mainProvider;
// 通过 ProviderRegistry 获取 API Key
const providerConfig = providerRegistry.getConfig(finalProvider);
const finalApiKey = resolveApiKey(providerConfig);
// 如果没有 API Key,返回 null
if (!finalApiKey) {
return null;
}
// 确定模型
const finalModel = storedConfig.summaryModel || DEFAULT_SUMMARY_MODELS[finalProvider];
// 确定 baseUrl
const finalBaseUrl = storedConfig.summaryBaseUrl || providerConfig?.baseUrl;
return {
provider: finalProvider,
apiKey: finalApiKey,
model: finalModel,
baseUrl: finalBaseUrl,
};
}
// 保存配置 // 保存配置
export function saveConfig(config: Partial<StoredConfig>): void { export function saveConfig(config: Partial<StoredConfig>): void {
// 确保目录存在 // 确保目录存在
@@ -14,7 +14,7 @@ import {
describe('Agent Presets - 预设 Agent', () => { describe('Agent Presets - 预设 Agent', () => {
describe('presetAgents 集合', () => { describe('presetAgents 集合', () => {
it('包含所有预设 Agent', () => { it('包含所有预设 Agent', () => {
expect(Object.keys(presetAgents)).toHaveLength(6); expect(Object.keys(presetAgents)).toHaveLength(7); // includes summary agent
}); });
it('包含 general Agent', () => { it('包含 general Agent', () => {
@@ -67,7 +67,7 @@ describe('Agent Presets - 预设 Agent', () => {
it('返回正确数量', () => { it('返回正确数量', () => {
const names = getPresetAgentNames(); const names = getPresetAgentNames();
expect(names).toHaveLength(6); expect(names).toHaveLength(7); // general, explore, code-review, frontend, backend, vision, summary
}); });
}); });
@@ -192,9 +192,9 @@ describe('Agent Presets - 预设 Agent', () => {
}); });
}); });
it('mode 值是 subagent 或 primary', () => { it('mode 值是有效的 AgentMode', () => {
Object.entries(presetAgents).forEach(([name, agent]) => { Object.entries(presetAgents).forEach(([name, agent]) => {
expect(['subagent', 'primary'], `${name} mode 无效`).toContain(agent.mode); expect(['subagent', 'primary', 'all', 'internal'], `${name} mode 无效`).toContain(agent.mode);
}); });
}); });
}); });
@@ -38,6 +38,7 @@ vi.mock('ai', () => ({
vi.mock('../../../src/agent/index.js', () => ({ vi.mock('../../../src/agent/index.js', () => ({
agentRegistry: { agentRegistry: {
get: vi.fn(), get: vi.fn(),
getInternal: vi.fn(() => null), // Returns null by default - no summary agent configured
}, },
AgentExecutor: vi.fn().mockImplementation(() => ({ AgentExecutor: vi.fn().mockImplementation(() => ({
execute: vi.fn().mockResolvedValue({ execute: vi.fn().mockResolvedValue({
-79
View File
@@ -76,16 +76,6 @@ interface PermissionManager {
setAskCallback(callback: (ctx: unknown) => Promise<{ allow: boolean; remember?: boolean }>): void; setAskCallback(callback: (ctx: unknown) => Promise<{ allow: boolean; remember?: boolean }>): void;
} }
/**
* Summary 配置接口
*/
export interface SummaryConfig {
provider: string;
apiKey: string;
model: string;
baseUrl?: string;
}
/** /**
* Core 模块接口 * Core 模块接口
*/ */
@@ -94,7 +84,6 @@ interface CoreModule {
toolRegistry: ToolRegistry; toolRegistry: ToolRegistry;
loadConfig: () => unknown; loadConfig: () => unknown;
saveConfig: (config: Record<string, unknown>) => void; saveConfig: (config: Record<string, unknown>) => void;
loadSummaryConfig: () => SummaryConfig | null;
getPermissionManager: (projectRoot?: string) => PermissionManager; getPermissionManager: (projectRoot?: string) => PermissionManager;
} }
@@ -443,71 +432,3 @@ export async function compressContext(
} }
} }
// ============================================================================
// 摘要配置 API
// ============================================================================
/**
* 摘要配置(不含 API Key 明文)
*/
export interface SummaryConfigInfo {
provider?: string;
model?: string;
hasApiKey: boolean;
baseUrl?: string;
}
/**
* 获取摘要配置
*/
export function getSummaryConfig(): SummaryConfigInfo | null {
if (!coreModule) {
return null;
}
const config = coreModule.loadSummaryConfig();
if (!config) {
return {
hasApiKey: false,
};
}
return {
provider: config.provider,
model: config.model,
hasApiKey: !!config.apiKey,
baseUrl: config.baseUrl,
};
}
/**
* 更新摘要配置
*/
export function updateSummaryConfig(config: {
provider?: string;
model?: string;
apiKey?: string;
baseUrl?: string;
}): boolean {
if (!coreModule) {
return false;
}
// 构建要保存的配置
const saveData: Record<string, unknown> = {};
if (config.provider !== undefined) {
saveData.summaryProvider = config.provider;
}
if (config.model !== undefined) {
saveData.summaryModel = config.model;
}
if (config.apiKey !== undefined) {
saveData.summaryApiKey = config.apiKey;
}
if (config.baseUrl !== undefined) {
saveData.summaryBaseUrl = config.baseUrl;
}
coreModule.saveConfig(saveData);
return true;
}
-4
View File
@@ -15,12 +15,8 @@ export {
// 上下文压缩相关 // 上下文压缩相关
getContextUsage, getContextUsage,
compressContext, compressContext,
getSummaryConfig,
updateSummaryConfig,
// 类型导出 // 类型导出
type TokenUsage, type TokenUsage,
type CompressionResult, type CompressionResult,
type ContextUsageInfo, type ContextUsageInfo,
type SummaryConfigInfo,
type SummaryConfig,
} from './adapter.js'; } from './adapter.js';
-3
View File
@@ -258,12 +258,9 @@ export {
cancelProcessing, cancelProcessing,
getContextUsage, getContextUsage,
compressContext, compressContext,
getSummaryConfig,
updateSummaryConfig,
type TokenUsage, type TokenUsage,
type CompressionResult, type CompressionResult,
type ContextUsageInfo, type ContextUsageInfo,
type SummaryConfigInfo,
} from './agent/index.js'; } from './agent/index.js';
export { export {
initAuth, initAuth,
-72
View File
@@ -5,11 +5,6 @@
*/ */
import { Hono } from 'hono'; import { Hono } from 'hono';
import {
getSummaryConfig,
updateSummaryConfig,
type SummaryConfigInfo,
} from '../agent/adapter.js';
export const configRouter = new Hono(); export const configRouter = new Hono();
@@ -102,70 +97,3 @@ export function getConfig(): ServerConfig {
export function setConfig(config: Partial<ServerConfig>): void { export function setConfig(config: Partial<ServerConfig>): void {
serverConfig = { ...serverConfig, ...config }; serverConfig = { ...serverConfig, ...config };
} }
// ============================================================================
// 摘要配置 API
// ============================================================================
/**
* GET /config/summary - 获取摘要模型配置
*/
configRouter.get('/summary', (c) => {
const config = getSummaryConfig();
if (!config) {
// Core 模块不可用
return c.json({
success: true,
data: {
hasApiKey: false,
} as SummaryConfigInfo,
});
}
return c.json({
success: true,
data: config,
});
});
/**
* PUT /config/summary - 更新摘要模型配置
*/
configRouter.put('/summary', async (c) => {
try {
const body = await c.req.json();
const success = updateSummaryConfig({
provider: body.provider,
model: body.model,
apiKey: body.apiKey,
baseUrl: body.baseUrl,
});
if (!success) {
return c.json(
{
success: false,
error: 'Core module not available',
},
500
);
}
// 返回更新后的配置(不含 API Key)
const config = getSummaryConfig();
return c.json({
success: true,
data: config,
});
} catch (error) {
return c.json(
{
success: false,
error: error instanceof Error ? error.message : 'Invalid input',
},
400
);
}
});
+1 -29
View File
@@ -45,11 +45,9 @@ import type {
CustomProviderDefinition, CustomProviderDefinition,
ProviderConfig, ProviderConfig,
ConnectionTestResult, ConnectionTestResult,
// Context & Summary types // Context types
ContextUsageInfo, ContextUsageInfo,
CompressionResult, CompressionResult,
SummaryConfigInfo,
SummaryConfigInput,
} from './types.js'; } from './types.js';
// Re-export types // Re-export types
@@ -124,8 +122,6 @@ export type {
CompressionStatus, CompressionStatus,
CompressionType, CompressionType,
CompressionResult, CompressionResult,
SummaryConfigInfo,
SummaryConfigInput,
} from './types.js'; } from './types.js';
// API Configuration // API Configuration
@@ -976,27 +972,3 @@ export async function compressContext(
}> { }> {
return request('POST', `/sessions/${encodeURIComponent(sessionId)}/compress`, options || {}); 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);
}
+1 -24
View File
@@ -280,7 +280,7 @@ export interface HookTestResult {
// ============ Agent 相关 ============ // ============ Agent 相关 ============
/** Agent 运行模式 */ /** Agent 运行模式 */
export type AgentMode = 'primary' | 'subagent' | 'all'; export type AgentMode = 'primary' | 'subagent' | 'all' | 'internal';
/** Agent 模型配置 */ /** Agent 模型配置 */
export interface AgentModelConfig { export interface AgentModelConfig {
@@ -756,26 +756,3 @@ export interface CompressionResult {
summaryTokens?: number; 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;
}
+164 -120
View File
@@ -12,6 +12,7 @@ import {
ChevronDown, ChevronDown,
ChevronRight, ChevronRight,
AlertCircle, AlertCircle,
Lock,
} from 'lucide-react'; } from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion'; import { motion, AnimatePresence } from 'framer-motion';
import { toast } from 'sonner'; import { toast } from 'sonner';
@@ -93,6 +94,9 @@ export function AgentEditor({
const [name, setName] = useState(defaultName); const [name, setName] = useState(defaultName);
const [description, setDescription] = useState(''); const [description, setDescription] = useState('');
const [mode, setMode] = useState<AgentMode>('primary'); const [mode, setMode] = useState<AgentMode>('primary');
// 是否为内部 AgentInternal Agent 只能编辑模型配置)
const isInternalAgent = mode === 'internal';
const [prompt, setPrompt] = useState(''); const [prompt, setPrompt] = useState('');
const [maxSteps, setMaxSteps] = useState<number | undefined>(undefined); const [maxSteps, setMaxSteps] = useState<number | undefined>(undefined);
@@ -298,12 +302,26 @@ export function AgentEditor({
)} )}
<div className={cn(responsive && 'mt-2 md:mt-0')}> <div className={cn(responsive && 'mt-2 md:mt-0')}>
<h2 className="text-lg font-semibold flex items-center gap-2"> <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}`} {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> </h2>
{copyFrom && ( {copyFrom && (
<p className="text-xs text-gray-500">Copying from: {copyFrom}</p> <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>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Button <Button
@@ -354,12 +372,12 @@ export function AgentEditor({
type="text" type="text"
value={name} value={name}
onChange={(e) => setName(e.target.value)} onChange={(e) => setName(e.target.value)}
disabled={!isNewAgent} disabled={!isNewAgent || isInternalAgent}
placeholder="my-agent" placeholder="my-agent"
className={cn( className={cn(
'w-full px-3 py-2 bg-gray-900 border border-gray-700 rounded-lg text-sm', 'w-full px-3 py-2 bg-gray-900 border border-gray-700 rounded-lg text-sm',
'focus:outline-none focus:border-primary-500', 'focus:outline-none focus:border-primary-500',
!isNewAgent && 'opacity-50 cursor-not-allowed' (!isNewAgent || isInternalAgent) && 'opacity-50 cursor-not-allowed'
)} )}
/> />
{!isNewAgent && ( {!isNewAgent && (
@@ -374,59 +392,81 @@ export function AgentEditor({
type="text" type="text"
value={description} value={description}
onChange={(e) => setDescription(e.target.value)} onChange={(e) => setDescription(e.target.value)}
disabled={isInternalAgent}
placeholder="A helpful coding assistant" 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> </div>
{/* Mode */} {/* Mode - 不为内部 Agent 显示 */}
<div> {!isInternalAgent && (
<label className="block text-xs text-gray-400 mb-1">Mode *</label> <div>
<div className="flex gap-2"> <label className="block text-xs text-gray-400 mb-1">Mode *</label>
{(['primary', 'subagent', 'all'] as const).map((m) => ( <div className="flex gap-2">
<button {(['primary', 'subagent', 'all'] as const).map((m) => (
key={m} <button
type="button" key={m}
onClick={() => setMode(m)} type="button"
className={cn( onClick={() => setMode(m)}
'px-3 py-2 rounded-lg text-sm transition-colors', className={cn(
mode === m 'px-3 py-2 rounded-lg text-sm transition-colors',
? 'bg-primary-500 text-white' mode === m
: 'bg-gray-900 text-gray-400 hover:bg-gray-800' ? 'bg-primary-500 text-white'
)} : 'bg-gray-900 text-gray-400 hover:bg-gray-800'
> )}
{m === 'primary' ? 'Primary' : m === 'subagent' ? 'Subagent' : 'Both'} >
</button> {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> </div>
<p className="text-xs text-gray-500 mt-1"> )}
{mode === 'primary'
? 'Can be used as the main agent' {/* Internal Mode 显示只读标签 */}
: mode === 'subagent' {isInternalAgent && (
? 'Can only be spawned by other agents' <div>
: 'Can be used in both modes'} <label className="block text-xs text-gray-400 mb-1">Mode</label>
</p> <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">
</div> <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> </div>
{/* System Prompt */} {/* System Prompt - 不为内部 Agent 显示 */}
<CollapsibleSection title="System Prompt" defaultOpen={!!prompt}> {!isInternalAgent && (
<div> <CollapsibleSection title="System Prompt" defaultOpen={!!prompt}>
<textarea <div>
value={prompt} <textarea
onChange={(e) => setPrompt(e.target.value)} value={prompt}
placeholder="You are a helpful assistant..." onChange={(e) => setPrompt(e.target.value)}
rows={8} placeholder="You are a helpful assistant..."
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" 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 className="text-xs text-gray-500 mt-1">
</p> Custom system prompt for this agent. Leave empty to use defaults.
</div> </p>
</CollapsibleSection> </div>
</CollapsibleSection>
)}
{/* Model Configuration */} {/* Model Configuration - 内部 Agent 默认展开 */}
<CollapsibleSection title="Model Configuration" defaultOpen={!!modelProvider || !!modelName}> <CollapsibleSection title="Model Configuration" defaultOpen={isInternalAgent || !!modelProvider || !!modelName}>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
{/* Provider */} {/* Provider */}
<div> <div>
@@ -489,83 +529,87 @@ export function AgentEditor({
</div> </div>
</CollapsibleSection> </CollapsibleSection>
{/* Tool Configuration */} {/* Tool Configuration - 不为内部 Agent 显示 */}
<CollapsibleSection title="Tool Configuration" defaultOpen={toolMode !== 'all' || noTask}> {!isInternalAgent && (
<div className="space-y-4"> <CollapsibleSection title="Tool Configuration" defaultOpen={toolMode !== 'all' || noTask}>
{/* Tool Mode */} <div className="space-y-4">
<div> {/* Tool Mode */}
<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' && (
<div> <div>
<label className="block text-xs text-gray-400 mb-1"> <label className="block text-xs text-gray-400 mb-1">Tool Access</label>
{toolMode === 'enabled' ? 'Enabled Tools' : 'Disabled Tools'} <div className="flex gap-2">
</label> {(['all', 'enabled', 'disabled'] as const).map((m) => (
<input <button
type="text" key={m}
value={toolList} type="button"
onChange={(e) => setToolList(e.target.value)} onClick={() => setToolMode(m)}
placeholder="bash, read_file, write_file" className={cn(
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" 'px-3 py-2 rounded-lg text-sm transition-colors',
/> toolMode === m
<p className="text-xs text-gray-500 mt-1">Comma-separated tool names</p> ? '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> </div>
)}
{/* No Task */} {/* Tool List */}
<div className="flex items-center gap-2"> {toolMode !== 'all' && (
<input <div>
type="checkbox" <label className="block text-xs text-gray-400 mb-1">
id="noTask" {toolMode === 'enabled' ? 'Enabled Tools' : 'Disabled Tools'}
checked={noTask} </label>
onChange={(e) => setNoTask(e.target.checked)} <input
className="w-4 h-4 rounded border-gray-600 bg-gray-900 text-primary-500 focus:ring-primary-500" type="text"
/> value={toolList}
<label htmlFor="noTask" className="text-sm text-gray-300"> onChange={(e) => setToolList(e.target.value)}
Disable nested Task calls placeholder="bash, read_file, write_file"
</label> 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>
</div> </CollapsibleSection>
</CollapsibleSection> )}
{/* Execution Limits */} {/* Execution Limits - 不为内部 Agent 显示 */}
<CollapsibleSection title="Execution Limits" defaultOpen={maxSteps !== undefined}> {!isInternalAgent && (
<div> <CollapsibleSection title="Execution Limits" defaultOpen={maxSteps !== undefined}>
<label className="block text-xs text-gray-400 mb-1">Max Steps</label> <div>
<input <label className="block text-xs text-gray-400 mb-1">Max Steps</label>
type="number" <input
value={maxSteps ?? ''} type="number"
onChange={(e) => value={maxSteps ?? ''}
setMaxSteps(e.target.value ? parseInt(e.target.value) : undefined) onChange={(e) =>
} setMaxSteps(e.target.value ? parseInt(e.target.value) : undefined)
placeholder="15" }
min="1" placeholder="15"
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" 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 className="text-xs text-gray-500 mt-1">
</p> Maximum number of tool call steps. Leave empty for default.
</div> </p>
</CollapsibleSection> </div>
</CollapsibleSection>
)}
</> </>
)} )}
</div> </div>
+55 -19
View File
@@ -20,6 +20,7 @@ import {
Sparkles, Sparkles,
Cpu, Cpu,
Layers, Layers,
Lock,
} from 'lucide-react'; } from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion'; import { motion, AnimatePresence } from 'framer-motion';
import { toast } from 'sonner'; import { toast } from 'sonner';
@@ -52,6 +53,8 @@ function getModeColor(mode: AgentListItem['mode']) {
return 'bg-purple-500/20 text-purple-400'; return 'bg-purple-500/20 text-purple-400';
case 'all': case 'all':
return 'bg-green-500/20 text-green-400'; return 'bg-green-500/20 text-green-400';
case 'internal':
return 'bg-slate-500/20 text-slate-400';
default: default:
return 'bg-gray-500/20 text-gray-400'; return 'bg-gray-500/20 text-gray-400';
} }
@@ -66,6 +69,8 @@ function getModeText(mode: AgentListItem['mode']) {
return 'Subagent'; return 'Subagent';
case 'all': case 'all':
return 'Both'; return 'Both';
case 'internal':
return 'Internal';
default: default:
return mode; return mode;
} }
@@ -189,9 +194,10 @@ export function AgentsPanel({ onClose, responsive = false }: AgentsPanelProps) {
setAgentDetails({}); setAgentDetails({});
}; };
// 统计 // 统计 - 按分类过滤
const presetAgents = agents.filter((a) => a.isPreset); const internalAgents = agents.filter((a) => a.mode === 'internal');
const customAgents = agents.filter((a) => !a.isPreset); const presetAgents = agents.filter((a) => a.isPreset && a.mode !== 'internal');
const customAgents = agents.filter((a) => !a.isPreset && a.mode !== 'internal');
// Loading 骨架屏 // Loading 骨架屏
const LoadingSkeleton = () => ( const LoadingSkeleton = () => (
@@ -214,6 +220,7 @@ export function AgentsPanel({ onClose, responsive = false }: AgentsPanelProps) {
const isExpanded = expandedAgents.has(agent.name); const isExpanded = expandedAgents.has(agent.name);
const isLoading = actionLoading === agent.name; const isLoading = actionLoading === agent.name;
const detail = agentDetails[agent.name]; const detail = agentDetails[agent.name];
const isInternal = agent.mode === 'internal';
return ( return (
<motion.div <motion.div
@@ -236,7 +243,9 @@ export function AgentsPanel({ onClose, responsive = false }: AgentsPanelProps) {
</button> </button>
{/* Icon */} {/* Icon */}
{agent.isPreset ? ( {isInternal ? (
<Lock size={16} className="text-slate-400" />
) : agent.isPreset ? (
<Sparkles size={16} className="text-yellow-400" /> <Sparkles size={16} className="text-yellow-400" />
) : ( ) : (
<Bot size={16} className="text-primary-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" /> <div className="animate-spin rounded-full h-5 w-5 border-t-2 border-b-2 border-primary-500" />
) : ( ) : (
<> <>
{/* View/Edit */} {/* View/Edit - Internal agents can be edited (model config) */}
{agent.isPreset && !agent.isCustomized ? ( {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 <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
@@ -287,19 +306,21 @@ export function AgentsPanel({ onClose, responsive = false }: AgentsPanelProps) {
</Button> </Button>
)} )}
{/* Copy */} {/* Copy - not for internal agents */}
<Button {!isInternal && (
variant="ghost" <Button
size="sm" variant="ghost"
onClick={() => handleCopy(agent.name)} size="sm"
className="text-gray-400 hover:text-gray-300" onClick={() => handleCopy(agent.name)}
title="Copy" className="text-gray-400 hover:text-gray-300"
> title="Copy"
<Copy size={14} /> >
</Button> <Copy size={14} />
</Button>
)}
{/* Delete (only for custom agents) */} {/* Delete (only for custom agents, not internal) */}
{!agent.isPreset && ( {!agent.isPreset && !isInternal && (
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
@@ -475,7 +496,7 @@ export function AgentsPanel({ onClose, responsive = false }: AgentsPanelProps) {
Agent Presets Agent Presets
</h2> </h2>
<p className="text-xs text-gray-500"> <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> </p>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@@ -541,6 +562,21 @@ export function AgentsPanel({ onClose, responsive = false }: AgentsPanelProps) {
animate={{ opacity: 1 }} animate={{ opacity: 1 }}
className={cn('space-y-4', responsive ? 'p-4' : 'p-4')} 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 */} {/* Preset Agents */}
{presetAgents.length > 0 && ( {presetAgents.length > 0 && (
<div> <div>
+1 -154
View File
@@ -12,15 +12,11 @@ import { cn } from '../utils/cn';
import { modalOverlay, modalContent, smoothTransition } from '../utils/animations'; import { modalOverlay, modalContent, smoothTransition } from '../utils/animations';
import { Button } from '../primitives/Button'; import { Button } from '../primitives/Button';
import { Input } from '../primitives/Input'; import { Input } from '../primitives/Input';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../primitives/Select';
import { Skeleton } from './Skeleton'; import { Skeleton } from './Skeleton';
import { import {
getConfig, getConfig,
updateConfig, updateConfig,
getSummaryConfig,
updateSummaryConfig,
type ServerConfig, type ServerConfig,
type SummaryConfigInfo,
} from '../api/client.js'; } from '../api/client.js';
interface ConfigPanelProps { interface ConfigPanelProps {
@@ -29,58 +25,26 @@ interface ConfigPanelProps {
responsive?: boolean; 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) { export function ConfigPanel({ onClose, responsive = false }: ConfigPanelProps) {
const [config, setConfig] = useState<ServerConfig | null>(null); const [config, setConfig] = useState<ServerConfig | null>(null);
const [summaryConfig, setSummaryConfig] = useState<SummaryConfigInfo | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
const [savingSummary, setSavingSummary] = useState(false);
// 表单状态 // 表单状态
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
workdir: '', workdir: '',
}); });
// 摘要模型表单状态
const [summaryFormData, setSummaryFormData] = useState({
provider: '',
model: '',
apiKey: '',
baseUrl: '',
});
// 加载配置 // 加载配置
useEffect(() => { useEffect(() => {
async function loadConfig() { async function loadConfig() {
try { try {
// 并行加载主配置和摘要配置 const configResponse = await getConfig();
const [configResponse, summaryResponse] = await Promise.all([
getConfig(),
getSummaryConfig(),
]);
setConfig(configResponse.data); setConfig(configResponse.data);
setFormData({ setFormData({
workdir: configResponse.data.workdir, 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) { } catch (err) {
toast.error(err instanceof Error ? err.message : 'Failed to load config'); toast.error(err instanceof Error ? err.message : 'Failed to load config');
} finally { } 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 = () => { const handleReset = () => {
if (config) { 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> <p className="text-xs text-gray-500">Root directory for file operations</p>
</div> </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> </motion.div>
)} )}
+2 -6
View File
@@ -90,11 +90,9 @@ export {
deleteProvider, deleteProvider,
addProviderModel, addProviderModel,
deleteProviderModel, deleteProviderModel,
// Context & Summary API // Context API
getContextUsage, getContextUsage,
compressContext, compressContext,
getSummaryConfig,
updateSummaryConfig,
} from './api/client.js'; } from './api/client.js';
// Types // Types
@@ -166,13 +164,11 @@ export type {
CustomProviderDefinition, CustomProviderDefinition,
ProviderConfig, ProviderConfig,
ConnectionTestResult, ConnectionTestResult,
// Context & Summary types // Context types
TokenUsage, TokenUsage,
ContextUsageInfo, ContextUsageInfo,
CompressionStatus, CompressionStatus,
CompressionResult, CompressionResult,
SummaryConfigInfo,
SummaryConfigInput,
} from './api/client.js'; } from './api/client.js';
// Primitives (shadcn/ui style) // Primitives (shadcn/ui style)