fix: enforce subagent prompt files and refine webui

This commit is contained in:
lpf
2026-03-09 11:24:31 +08:00
parent ba3be33c91
commit acf8a22c0a
25 changed files with 257 additions and 211 deletions

View File

@@ -464,9 +464,6 @@ func (al *AgentLoop) buildSubagentTaskInput(task *tools.SubagentTask) string {
return fmt.Sprintf("Role Profile Policy (%s):\n%s\n\nTask:\n%s", promptFile, promptText, taskText)
}
}
if prompt := strings.TrimSpace(task.SystemPrompt); prompt != "" {
return fmt.Sprintf("Role Profile Prompt:\n%s\n\nTask:\n%s", prompt, taskText)
}
return taskText
}

View File

@@ -136,7 +136,6 @@ func (al *AgentLoop) HandleSubagentRuntime(ctx context.Context, action string, a
"display_name": subcfg.DisplayName,
"role": subcfg.Role,
"description": subcfg.Description,
"system_prompt": subcfg.SystemPrompt,
"system_prompt_file": subcfg.SystemPromptFile,
"prompt_file_found": promptFileFound,
"memory_namespace": subcfg.MemoryNamespace,
@@ -167,7 +166,6 @@ func (al *AgentLoop) HandleSubagentRuntime(ctx context.Context, action string, a
"display_name": profile.Name,
"role": profile.Role,
"description": "Node-registered remote main agent branch",
"system_prompt": profile.SystemPrompt,
"system_prompt_file": profile.SystemPromptFile,
"prompt_file_found": false,
"memory_namespace": profile.MemoryNamespace,

View File

@@ -78,7 +78,6 @@ func TestHandleSubagentRuntimeUpsertConfigSubagent(t *testing.T) {
"role": "testing",
"notify_main_policy": "internal_only",
"display_name": "Review Agent",
"system_prompt": "review changes",
"system_prompt_file": "agents/reviewer/AGENT.md",
"routing_keywords": []interface{}{"review", "regression"},
"tool_allowlist": []interface{}{"shell", "sessions"},
@@ -129,7 +128,6 @@ func TestHandleSubagentRuntimeRegistryAndToggleEnabled(t *testing.T) {
Type: "worker",
Role: "testing",
DisplayName: "Test Agent",
SystemPrompt: "run tests",
SystemPromptFile: "agents/tester/AGENT.md",
MemoryNamespace: "tester",
Tools: config.SubagentToolsConfig{

View File

@@ -20,7 +20,6 @@ func TestBuildSubagentTaskInputPrefersPromptFile(t *testing.T) {
loop := &AgentLoop{workspace: workspace}
input := loop.buildSubagentTaskInput(&tools.SubagentTask{
Task: "implement login flow",
SystemPrompt: "inline-fallback",
SystemPromptFile: "agents/coder/AGENT.md",
})
if !strings.Contains(input, "coder-file-policy") {
@@ -31,13 +30,15 @@ func TestBuildSubagentTaskInputPrefersPromptFile(t *testing.T) {
}
}
func TestBuildSubagentTaskInputFallsBackToInlinePrompt(t *testing.T) {
func TestBuildSubagentTaskInputWithoutPromptFileUsesTaskOnly(t *testing.T) {
loop := &AgentLoop{workspace: t.TempDir()}
input := loop.buildSubagentTaskInput(&tools.SubagentTask{
Task: "run regression",
SystemPrompt: "test inline prompt",
Task: "run regression",
})
if !strings.Contains(input, "test inline prompt") {
t.Fatalf("expected inline prompt in task input, got: %s", input)
if strings.Contains(input, "test inline prompt") {
t.Fatalf("did not expect inline prompt fallback, got: %s", input)
}
if !strings.Contains(input, "run regression") {
t.Fatalf("expected task input to contain task, got: %s", input)
}
}

View File

@@ -4007,19 +4007,19 @@ func (s *Server) handleWebUISubagentProfiles(w http.ResponseWriter, r *http.Requ
_ = json.NewEncoder(w).Encode(map[string]interface{}{"ok": true, "deleted": true, "agent_id": agentID})
case http.MethodPost:
var body struct {
Action string `json:"action"`
AgentID string `json:"agent_id"`
Name string `json:"name"`
Role string `json:"role"`
SystemPrompt string `json:"system_prompt"`
MemoryNamespace string `json:"memory_namespace"`
Status string `json:"status"`
ToolAllowlist []string `json:"tool_allowlist"`
MaxRetries *int `json:"max_retries"`
RetryBackoffMS *int `json:"retry_backoff_ms"`
TimeoutSec *int `json:"timeout_sec"`
MaxTaskChars *int `json:"max_task_chars"`
MaxResultChars *int `json:"max_result_chars"`
Action string `json:"action"`
AgentID string `json:"agent_id"`
Name string `json:"name"`
Role string `json:"role"`
SystemPromptFile string `json:"system_prompt_file"`
MemoryNamespace string `json:"memory_namespace"`
Status string `json:"status"`
ToolAllowlist []string `json:"tool_allowlist"`
MaxRetries *int `json:"max_retries"`
RetryBackoffMS *int `json:"retry_backoff_ms"`
TimeoutSec *int `json:"timeout_sec"`
MaxTaskChars *int `json:"max_task_chars"`
MaxResultChars *int `json:"max_result_chars"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
http.Error(w, "invalid json", http.StatusBadRequest)
@@ -4045,18 +4045,18 @@ func (s *Server) handleWebUISubagentProfiles(w http.ResponseWriter, r *http.Requ
return
}
profile, err := store.Upsert(tools.SubagentProfile{
AgentID: agentID,
Name: body.Name,
Role: body.Role,
SystemPrompt: body.SystemPrompt,
MemoryNamespace: body.MemoryNamespace,
Status: body.Status,
ToolAllowlist: body.ToolAllowlist,
MaxRetries: derefInt(body.MaxRetries),
RetryBackoff: derefInt(body.RetryBackoffMS),
TimeoutSec: derefInt(body.TimeoutSec),
MaxTaskChars: derefInt(body.MaxTaskChars),
MaxResultChars: derefInt(body.MaxResultChars),
AgentID: agentID,
Name: body.Name,
Role: body.Role,
SystemPromptFile: body.SystemPromptFile,
MemoryNamespace: body.MemoryNamespace,
Status: body.Status,
ToolAllowlist: body.ToolAllowlist,
MaxRetries: derefInt(body.MaxRetries),
RetryBackoff: derefInt(body.RetryBackoffMS),
TimeoutSec: derefInt(body.TimeoutSec),
MaxTaskChars: derefInt(body.MaxTaskChars),
MaxResultChars: derefInt(body.MaxResultChars),
})
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
@@ -4076,7 +4076,7 @@ func (s *Server) handleWebUISubagentProfiles(w http.ResponseWriter, r *http.Requ
next := *existing
next.Name = body.Name
next.Role = body.Role
next.SystemPrompt = body.SystemPrompt
next.SystemPromptFile = body.SystemPromptFile
next.MemoryNamespace = body.MemoryNamespace
if body.Status != "" {
next.Status = body.Status
@@ -4134,18 +4134,18 @@ func (s *Server) handleWebUISubagentProfiles(w http.ResponseWriter, r *http.Requ
_ = json.NewEncoder(w).Encode(map[string]interface{}{"ok": true, "deleted": true, "agent_id": agentID})
case "upsert":
profile, err := store.Upsert(tools.SubagentProfile{
AgentID: agentID,
Name: body.Name,
Role: body.Role,
SystemPrompt: body.SystemPrompt,
MemoryNamespace: body.MemoryNamespace,
Status: body.Status,
ToolAllowlist: body.ToolAllowlist,
MaxRetries: derefInt(body.MaxRetries),
RetryBackoff: derefInt(body.RetryBackoffMS),
TimeoutSec: derefInt(body.TimeoutSec),
MaxTaskChars: derefInt(body.MaxTaskChars),
MaxResultChars: derefInt(body.MaxResultChars),
AgentID: agentID,
Name: body.Name,
Role: body.Role,
SystemPromptFile: body.SystemPromptFile,
MemoryNamespace: body.MemoryNamespace,
Status: body.Status,
ToolAllowlist: body.ToolAllowlist,
MaxRetries: derefInt(body.MaxRetries),
RetryBackoff: derefInt(body.RetryBackoffMS),
TimeoutSec: derefInt(body.TimeoutSec),
MaxTaskChars: derefInt(body.MaxTaskChars),
MaxResultChars: derefInt(body.MaxResultChars),
})
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)

View File

@@ -77,7 +77,6 @@ type SubagentConfig struct {
DisplayName string `json:"display_name,omitempty"`
Role string `json:"role,omitempty"`
Description string `json:"description,omitempty"`
SystemPrompt string `json:"system_prompt,omitempty"`
SystemPromptFile string `json:"system_prompt_file,omitempty"`
MemoryNamespace string `json:"memory_namespace,omitempty"`
AcceptFrom []string `json:"accept_from,omitempty"`
@@ -88,6 +87,19 @@ type SubagentConfig struct {
Runtime SubagentRuntimeConfig `json:"runtime,omitempty"`
}
func (s *SubagentConfig) UnmarshalJSON(data []byte) error {
type alias SubagentConfig
var raw struct {
alias
LegacySystemPrompt string `json:"system_prompt"`
}
if err := json.Unmarshal(data, &raw); err != nil {
return err
}
*s = SubagentConfig(raw.alias)
return nil
}
type SubagentToolsConfig struct {
Allowlist []string `json:"allowlist,omitempty"`
Denylist []string `json:"denylist,omitempty"`

View File

@@ -27,7 +27,6 @@ type SubagentTask struct {
NotifyMainPolicy string `json:"notify_main_policy,omitempty"`
SessionKey string `json:"session_key"`
MemoryNS string `json:"memory_ns"`
SystemPrompt string `json:"system_prompt,omitempty"`
SystemPromptFile string `json:"system_prompt_file,omitempty"`
ToolAllowlist []string `json:"tool_allowlist,omitempty"`
MaxRetries int `json:"max_retries,omitempty"`
@@ -168,7 +167,6 @@ func (sm *SubagentManager) spawnTask(ctx context.Context, opts SubagentSpawnOpti
agentID = "default"
}
memoryNS := agentID
systemPrompt := ""
systemPromptFile := ""
transport := "local"
nodeID := ""
@@ -207,7 +205,6 @@ func (sm *SubagentManager) spawnTask(ctx context.Context, opts SubagentSpawnOpti
nodeID = strings.TrimSpace(profile.NodeID)
parentAgentID = strings.TrimSpace(profile.ParentAgentID)
notifyMainPolicy = normalizeNotifyMainPolicy(profile.NotifyMainPolicy)
systemPrompt = strings.TrimSpace(profile.SystemPrompt)
systemPromptFile = strings.TrimSpace(profile.SystemPromptFile)
toolAllowlist = append([]string(nil), profile.ToolAllowlist...)
maxRetries = profile.MaxRetries
@@ -288,7 +285,6 @@ func (sm *SubagentManager) spawnTask(ctx context.Context, opts SubagentSpawnOpti
NotifyMainPolicy: notifyMainPolicy,
SessionKey: sessionKey,
MemoryNS: memoryNS,
SystemPrompt: systemPrompt,
SystemPromptFile: systemPromptFile,
ToolAllowlist: toolAllowlist,
MaxRetries: maxRetries,
@@ -671,9 +667,6 @@ func (sm *SubagentManager) resolveSystemPrompt(task *SubagentTask) string {
return systemPrompt + "\n\nSubagent policy (" + promptFile + "):\n" + promptText
}
}
if rolePrompt := strings.TrimSpace(task.SystemPrompt); rolePrompt != "" {
return systemPrompt + "\n\nRole-specific profile prompt:\n" + rolePrompt
}
return systemPrompt
}

View File

@@ -21,14 +21,12 @@ func DraftConfigSubagent(description, agentIDHint string) map[string]interface{}
displayName := inferDraftDisplayName(role, agentID)
toolAllowlist := inferDraftToolAllowlist(role)
keywords := inferDraftKeywords(role, lower)
systemPrompt := inferDraftSystemPrompt(role, desc)
return map[string]interface{}{
"agent_id": agentID,
"role": role,
"display_name": displayName,
"description": desc,
"notify_main_policy": "final_only",
"system_prompt": systemPrompt,
"system_prompt_file": "agents/" + agentID + "/AGENT.md",
"memory_namespace": agentID,
"tool_allowlist": toolAllowlist,
@@ -87,9 +85,6 @@ func UpsertConfigSubagent(configPath string, args map[string]interface{}) (map[s
if v := stringArgFromMap(args, "description"); v != "" {
subcfg.Description = v
}
if v := stringArgFromMap(args, "system_prompt"); v != "" {
subcfg.SystemPrompt = v
}
if v := stringArgFromMap(args, "system_prompt_file"); v != "" {
subcfg.SystemPromptFile = v
}

View File

@@ -45,7 +45,6 @@ func (t *SubagentConfigTool) Parameters() map[string]interface{} {
"parent_agent_id": map[string]interface{}{"type": "string"},
"role": map[string]interface{}{"type": "string"},
"display_name": map[string]interface{}{"type": "string"},
"system_prompt": map[string]interface{}{"type": "string"},
"system_prompt_file": map[string]interface{}{"type": "string"},
"memory_namespace": map[string]interface{}{"type": "string"},
"type": map[string]interface{}{"type": "string"},

View File

@@ -35,7 +35,6 @@ func TestSubagentConfigToolUpsert(t *testing.T) {
"notify_main_policy": "internal_only",
"display_name": "Review Agent",
"description": "负责回归与评审",
"system_prompt": "review changes",
"system_prompt_file": "agents/reviewer/AGENT.md",
"routing_keywords": []interface{}{"review", "regression"},
"tool_allowlist": []interface{}{"shell", "sessions"},

View File

@@ -24,7 +24,6 @@ type SubagentProfile struct {
ParentAgentID string `json:"parent_agent_id,omitempty"`
NotifyMainPolicy string `json:"notify_main_policy,omitempty"`
Role string `json:"role,omitempty"`
SystemPrompt string `json:"system_prompt,omitempty"`
SystemPromptFile string `json:"system_prompt_file,omitempty"`
ToolAllowlist []string `json:"tool_allowlist,omitempty"`
MemoryNamespace string `json:"memory_namespace,omitempty"`
@@ -191,7 +190,6 @@ func normalizeSubagentProfile(in SubagentProfile) SubagentProfile {
p.ParentAgentID = normalizeSubagentIdentifier(p.ParentAgentID)
p.NotifyMainPolicy = normalizeNotifyMainPolicy(p.NotifyMainPolicy)
p.Role = strings.TrimSpace(p.Role)
p.SystemPrompt = strings.TrimSpace(p.SystemPrompt)
p.SystemPromptFile = strings.TrimSpace(p.SystemPromptFile)
p.MemoryNamespace = normalizeSubagentIdentifier(p.MemoryNamespace)
if p.MemoryNamespace == "" {
@@ -409,7 +407,6 @@ func profileFromConfig(agentID string, subcfg config.SubagentConfig) SubagentPro
ParentAgentID: strings.TrimSpace(subcfg.ParentAgentID),
NotifyMainPolicy: strings.TrimSpace(subcfg.NotifyMainPolicy),
Role: strings.TrimSpace(subcfg.Role),
SystemPrompt: strings.TrimSpace(subcfg.SystemPrompt),
SystemPromptFile: strings.TrimSpace(subcfg.SystemPromptFile),
ToolAllowlist: append([]string(nil), subcfg.Tools.Allowlist...),
MemoryNamespace: strings.TrimSpace(subcfg.MemoryNamespace),
@@ -554,7 +551,6 @@ func (t *SubagentProfileTool) Parameters() map[string]interface{} {
"name": map[string]interface{}{"type": "string"},
"notify_main_policy": map[string]interface{}{"type": "string", "description": "final_only|internal_only|milestone|on_blocked|always"},
"role": map[string]interface{}{"type": "string"},
"system_prompt": map[string]interface{}{"type": "string"},
"system_prompt_file": map[string]interface{}{"type": "string"},
"memory_namespace": map[string]interface{}{"type": "string"},
"status": map[string]interface{}{"type": "string", "description": "active|disabled"},
@@ -625,7 +621,6 @@ func (t *SubagentProfileTool) Execute(ctx context.Context, args map[string]inter
Name: stringArg(args, "name"),
NotifyMainPolicy: stringArg(args, "notify_main_policy"),
Role: stringArg(args, "role"),
SystemPrompt: stringArg(args, "system_prompt"),
SystemPromptFile: stringArg(args, "system_prompt_file"),
MemoryNamespace: stringArg(args, "memory_namespace"),
Status: stringArg(args, "status"),
@@ -662,9 +657,6 @@ func (t *SubagentProfileTool) Execute(ctx context.Context, args map[string]inter
if _, ok := args["notify_main_policy"]; ok {
next.NotifyMainPolicy = stringArg(args, "notify_main_policy")
}
if _, ok := args["system_prompt"]; ok {
next.SystemPrompt = stringArg(args, "system_prompt")
}
if _, ok := args["system_prompt_file"]; ok {
next.SystemPromptFile = stringArg(args, "system_prompt_file")
}

View File

@@ -126,7 +126,6 @@ func TestSubagentProfileStoreReadsProfilesFromRuntimeConfig(t *testing.T) {
Enabled: true,
DisplayName: "Code Agent",
Role: "coding",
SystemPrompt: "write code",
SystemPromptFile: "agents/coder/AGENT.md",
MemoryNamespace: "code-ns",
Tools: config.SubagentToolsConfig{

View File

@@ -719,7 +719,6 @@ func TestSubagentUsesConfiguredSystemPromptFile(t *testing.T) {
if _, err := manager.ProfileStore().Upsert(SubagentProfile{
AgentID: "coder",
Status: "active",
SystemPrompt: "inline-fallback",
SystemPromptFile: "agents/coder/AGENT.md",
}); err != nil {
t.Fatalf("profile upsert failed: %v", err)

View File

@@ -54,11 +54,10 @@ const Header: React.FC = () => {
<button
onClick={toggleTheme}
className="flex items-center gap-2 text-sm font-medium text-zinc-400 hover:text-zinc-200 transition-colors bg-zinc-900 hover:bg-zinc-800 border border-zinc-800 px-3 py-1.5 rounded-lg"
className="inline-flex h-9 w-9 items-center justify-center text-sm font-medium text-zinc-400 hover:text-zinc-200 transition-colors bg-zinc-900 hover:bg-zinc-800 border border-zinc-800 rounded-lg"
title={theme === 'dark' ? t('themeLight') : t('themeDark')}
>
{theme === 'dark' ? <SunMedium className="w-4 h-4" /> : <Moon className="w-4 h-4" />}
<span className="hidden sm:inline">{theme === 'dark' ? t('themeLight') : t('themeDark')}</span>
</button>
<button

View File

@@ -1,5 +1,6 @@
import React from 'react';
import { NavLink } from 'react-router-dom';
import { Check } from 'lucide-react';
interface NavItemProps {
icon: React.ReactNode;
@@ -9,17 +10,26 @@ interface NavItemProps {
}
const NavItem: React.FC<NavItemProps> = ({ icon, label, to, collapsed = false }) => (
<NavLink
<NavLink
to={to}
title={collapsed ? label : undefined}
className={({ isActive }) => `w-full flex items-center ${collapsed ? 'justify-center' : 'gap-3'} px-3 py-2.5 rounded-lg text-sm font-medium transition-all duration-200 ${
className={({ isActive }) => `w-full flex items-center ${collapsed ? 'justify-center' : 'gap-3'} px-3 py-2.5 rounded-lg text-sm font-medium transition-all duration-200 border ${
isActive
? 'nav-item-active text-indigo-700 border border-indigo-500/30'
: 'text-zinc-400 hover:bg-zinc-800/30 hover:text-zinc-200 border border-transparent'
? 'nav-item-active text-indigo-700 border-indigo-500/30'
: 'text-zinc-400 border-transparent'
}`}
>
{icon}
{!collapsed && label}
{({ isActive }) => (
<>
<span className="shrink-0">{icon}</span>
{!collapsed && <span className="min-w-0 flex-1 truncate">{label}</span>}
{!collapsed && isActive && (
<span className="ml-auto inline-flex h-5 w-5 items-center justify-center rounded-full bg-indigo-500/15 text-indigo-300">
<Check className="w-3.5 h-3.5" />
</span>
)}
</>
)}
</NavLink>
);

View File

@@ -14,17 +14,21 @@ const Sidebar: React.FC = () => {
items: [
{ icon: <LayoutDashboard className="w-5 h-5" />, label: t('dashboard'), to: '/' },
{ icon: <MessageSquare className="w-5 h-5" />, label: t('chat'), to: '/chat' },
{ icon: <Boxes className="w-5 h-5" />, label: t('subagentsRuntime'), to: '/subagents' },
],
},
{
title: t('sidebarRuntime'),
title: t('sidebarAgents'),
items: [
{ icon: <Boxes className="w-5 h-5" />, label: t('subagentsRuntime'), to: '/subagents' },
{ icon: <Bot className="w-5 h-5" />, label: t('subagentProfiles'), to: '/subagent-profiles' },
],
},
{
title: t('sidebarOps'),
items: [
{ icon: <Terminal className="w-5 h-5" />, label: t('nodes'), to: '/nodes' },
{ icon: <FolderOpen className="w-5 h-5" />, label: t('nodeArtifacts'), to: '/node-artifacts' },
{ icon: <ClipboardList className="w-5 h-5" />, label: t('taskAudit'), to: '/task-audit' },
{ icon: <Terminal className="w-5 h-5" />, label: t('logs'), to: '/logs' },
{ icon: <BrainCircuit className="w-5 h-5" />, label: t('ekg'), to: '/ekg' },
],
},
{
@@ -32,7 +36,6 @@ const Sidebar: React.FC = () => {
items: [
{ icon: <Settings className="w-5 h-5" />, label: t('config'), to: '/config' },
{ icon: <Plug className="w-5 h-5" />, label: t('mcpServices'), to: '/mcp' },
{ icon: <Bot className="w-5 h-5" />, label: t('subagentProfiles'), to: '/subagent-profiles' },
{ icon: <Clock className="w-5 h-5" />, label: t('cronJobs'), to: '/cron' },
],
},
@@ -41,6 +44,13 @@ const Sidebar: React.FC = () => {
items: [
{ icon: <FolderOpen className="w-5 h-5" />, label: t('memory'), to: '/memory' },
{ icon: <Zap className="w-5 h-5" />, label: t('skills'), to: '/skills' },
],
},
{
title: t('sidebarInsights'),
items: [
{ icon: <Terminal className="w-5 h-5" />, label: t('logs'), to: '/logs' },
{ icon: <BrainCircuit className="w-5 h-5" />, label: t('ekg'), to: '/ekg' },
{ icon: <Hash className="w-5 h-5" />, label: t('logCodes'), to: '/log-codes' },
],
},
@@ -80,15 +90,10 @@ const Sidebar: React.FC = () => {
<div className={`hidden md:flex border-t border-zinc-800 bg-zinc-900/20 ${sidebarCollapsed ? 'justify-center p-3' : 'p-3'}`}>
<button
onClick={() => setSidebarCollapsed((prev) => !prev)}
className={`flex items-center ${sidebarCollapsed ? 'justify-center' : 'justify-between'} gap-3 rounded-2xl border border-zinc-800 brand-card-subtle hover:bg-zinc-900/40 text-zinc-300 transition-colors ${sidebarCollapsed ? 'w-11 h-11' : 'w-full px-3 py-2.5'}`}
className="flex h-11 w-11 items-center justify-center rounded-2xl border border-zinc-800 brand-card-subtle hover:bg-zinc-900/40 text-zinc-300 transition-colors"
title={sidebarCollapsed ? t('expand') : t('collapse')}
>
{sidebarCollapsed ? <PanelLeftOpen className="w-4 h-4" /> : (
<>
<span className="text-sm font-medium">{t('collapse')}</span>
<PanelLeftClose className="w-4 h-4 shrink-0" />
</>
)}
{sidebarCollapsed ? <PanelLeftOpen className="w-4 h-4" /> : <PanelLeftClose className="w-4 h-4" />}
</button>
</div>
</aside>

View File

@@ -21,12 +21,9 @@ type UIContextType = {
};
const UIContext = createContext<UIContextType | undefined>(undefined);
const THEME_STORAGE_KEY = 'clawgo:webui:theme';
function getInitialTheme(): ThemeMode {
if (typeof window === 'undefined') return 'dark';
const saved = window.localStorage.getItem(THEME_STORAGE_KEY);
if (saved === 'light' || saved === 'dark') return saved;
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
@@ -45,9 +42,25 @@ export const UIProvider: React.FC<{ children: React.ReactNode }> = ({ children }
root.classList.remove('theme-light', 'theme-dark');
root.classList.add(theme === 'dark' ? 'theme-dark' : 'theme-light');
root.style.colorScheme = theme;
window.localStorage.setItem(THEME_STORAGE_KEY, theme);
}, [theme]);
useEffect(() => {
if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return;
const media = window.matchMedia('(prefers-color-scheme: dark)');
const applySystemTheme = (event?: MediaQueryList | MediaQueryListEvent) => {
const matches = 'matches' in (event || media) ? (event || media).matches : media.matches;
setTheme(matches ? 'dark' : 'light');
};
applySystemTheme(media);
const onChange = (event: MediaQueryListEvent) => applySystemTheme(event);
if (typeof media.addEventListener === 'function') {
media.addEventListener('change', onChange);
return () => media.removeEventListener('change', onChange);
}
media.addListener(onChange);
return () => media.removeListener(onChange);
}, []);
const value = useMemo<UIContextType>(() => ({
loading,
theme,

View File

@@ -108,6 +108,7 @@ const resources = {
subagentDeleteConfirmMessage: 'Delete subagent profile "{{id}}" permanently?',
sidebarCore: 'Core',
sidebarMain: 'Main',
sidebarAgents: 'Agents',
sidebarRuntime: 'Runtime',
sidebarConfig: 'Configuration',
sidebarKnowledge: 'Knowledge',
@@ -726,6 +727,7 @@ const resources = {
subagentDeleteConfirmMessage: '确认永久删除子代理档案 "{{id}}"',
sidebarCore: '核心',
sidebarMain: '主入口',
sidebarAgents: 'Agents',
sidebarRuntime: '运行态',
sidebarConfig: '配置',
sidebarKnowledge: '知识与调试',

View File

@@ -78,62 +78,62 @@ html {
html.theme-dark {
--color-zinc-50: #f8fafc;
--color-zinc-100: #e2e8f0;
--color-zinc-200: #cbd5e1;
--color-zinc-300: #94a3b8;
--color-zinc-400: #64748b;
--color-zinc-500: #475569;
--color-zinc-600: #334155;
--color-zinc-700: #1e293b;
--color-zinc-800: #111827;
--color-zinc-900: #0b1220;
--color-zinc-950: #070b13;
--color-indigo-300: #fdba74;
--color-indigo-400: #fb923c;
--color-indigo-500: #f97316;
--color-indigo-600: #ea580c;
--color-indigo-700: #c2410c;
--color-indigo-800: #9a3412;
--app-bg-spot-a: rgb(249 115 22 / 0.02);
--app-bg-spot-b: rgb(96 165 250 / 0.02);
--app-bg-base-top: rgb(3 7 12 / 0.998);
--app-bg-base-bottom: rgb(8 12 18 / 0.995);
--shell-spot-a: rgb(249 115 22 / 0.018);
--shell-spot-b: rgb(148 163 184 / 0.025);
--main-surface-top: rgb(255 255 255 / 0.012);
--main-surface-mid: rgb(255 255 255 / 0.008);
--main-surface-bottom: rgb(255 255 255 / 0.002);
--header-bg-a: rgb(10 14 20 / 0.86);
--header-bg-b: rgb(6 10 15 / 0.82);
--header-overlay-a: rgb(255 255 255 / 0.018);
--header-overlay-b: rgb(255 255 255 / 0.005);
--sidebar-bg-a: rgb(10 14 20 / 0.9);
--sidebar-bg-b: rgb(6 10 15 / 0.88);
--sidebar-overlay-a: rgb(255 255 255 / 0.012);
--sidebar-overlay-b: rgb(255 255 255 / 0.003);
--sidebar-edge: rgb(30 41 59 / 0.58);
--sidebar-section-a: rgb(15 23 42 / 0.52);
--sidebar-section-b: rgb(9 14 23 / 0.34);
--active-bg: rgb(249 115 22 / 0.09);
--active-ring: rgb(249 115 22 / 0.16);
--card-bg-a: rgb(15 23 42 / 0.56);
--card-bg-b: rgb(9 14 23 / 0.44);
--card-topline: rgb(255 255 255 / 0.035);
--card-inner-highlight: rgb(255 255 255 / 0.03);
--card-shadow: rgb(0 0 0 / 0.16);
--card-subtle-a: rgb(17 24 39 / 0.4);
--card-subtle-b: rgb(9 14 23 / 0.24);
--button-start: #fb923c;
--button-end: #ea580c;
--button-shadow: rgb(0 0 0 / 0.14);
--chip-bg: rgb(30 41 59 / 0.82);
--chip-bg-hover: rgb(51 65 85 / 0.92);
--chip-border: rgb(71 85 105 / 0.85);
--color-zinc-200: #d7e1ee;
--color-zinc-300: #b8c6d8;
--color-zinc-400: #90a4bc;
--color-zinc-500: #6f839b;
--color-zinc-600: #516278;
--color-zinc-700: #243244;
--color-zinc-800: #162131;
--color-zinc-900: #101827;
--color-zinc-950: #0d1522;
--color-indigo-300: #f8c58d;
--color-indigo-400: #f1a561;
--color-indigo-500: #e8843a;
--color-indigo-600: #d46a23;
--color-indigo-700: #af4f16;
--color-indigo-800: #8d3f13;
--app-bg-spot-a: rgb(249 115 22 / 0.035);
--app-bg-spot-b: rgb(56 189 248 / 0.05);
--app-bg-base-top: rgb(9 16 28 / 0.995);
--app-bg-base-bottom: rgb(14 22 36 / 0.992);
--shell-spot-a: rgb(249 115 22 / 0.03);
--shell-spot-b: rgb(96 165 250 / 0.04);
--main-surface-top: rgb(255 255 255 / 0.026);
--main-surface-mid: rgb(255 255 255 / 0.016);
--main-surface-bottom: rgb(255 255 255 / 0.008);
--header-bg-a: rgb(14 21 34 / 0.92);
--header-bg-b: rgb(11 18 29 / 0.9);
--header-overlay-a: rgb(255 255 255 / 0.03);
--header-overlay-b: rgb(255 255 255 / 0.01);
--sidebar-bg-a: rgb(13 20 33 / 0.93);
--sidebar-bg-b: rgb(10 16 27 / 0.9);
--sidebar-overlay-a: rgb(255 255 255 / 0.022);
--sidebar-overlay-b: rgb(255 255 255 / 0.008);
--sidebar-edge: rgb(71 85 105 / 0.64);
--sidebar-section-a: rgb(18 30 49 / 0.74);
--sidebar-section-b: rgb(12 20 34 / 0.58);
--active-bg: rgb(232 132 58 / 0.11);
--active-ring: rgb(241 165 97 / 0.22);
--card-bg-a: rgb(17 28 46 / 0.82);
--card-bg-b: rgb(11 20 34 / 0.72);
--card-topline: rgb(255 255 255 / 0.065);
--card-inner-highlight: rgb(255 255 255 / 0.045);
--card-shadow: rgb(0 0 0 / 0.24);
--card-subtle-a: rgb(22 32 50 / 0.62);
--card-subtle-b: rgb(13 21 35 / 0.48);
--button-start: #ee9852;
--button-end: #d96b25;
--button-shadow: rgb(217 107 37 / 0.18);
--chip-bg: rgb(36 49 69 / 0.9);
--chip-bg-hover: rgb(48 63 87 / 0.96);
--chip-border: rgb(93 109 135 / 0.82);
--chip-text: rgb(226 232 240 / 0.96);
--chip-active-bg: rgb(124 45 18 / 0.36);
--chip-active-border: rgb(251 146 60 / 0.35);
--chip-active-text: rgb(255 237 213 / 0.96);
--chip-group-bg: rgb(15 23 42 / 0.5);
--chip-group-border: rgb(51 65 85 / 0.7);
--chip-active-bg: rgb(123 58 24 / 0.28);
--chip-active-border: rgb(232 132 58 / 0.28);
--chip-active-text: rgb(255 237 213 / 0.92);
--chip-group-bg: rgb(19 31 49 / 0.68);
--chip-group-border: rgb(71 85 105 / 0.76);
--radius-card: 18px;
--radius-subtle: 12px;
--radius-panel: 16px;

View File

@@ -147,15 +147,11 @@ const Dashboard: React.FC = () => {
</div>
<div className="brand-card rounded-[28px] border border-zinc-800 p-5 min-h-[148px]">
<div className="flex items-center gap-2 text-zinc-200 mb-2">
<Workflow className="w-4 h-4 text-sky-400" />
<div className="text-sm font-medium">{t('nodeP2P')}</div>
</div>
<div className="text-2xl font-semibold text-zinc-100 truncate">
{p2pEnabled ? `${p2pConfiguredIce} ICE · ${p2pConfiguredStun} STUN` : t('disabled')}
</div>
<div className="mt-2 text-xs text-zinc-500">
{t('dashboardNodeP2PDetail', { transport: p2pTransport, sessions: p2pSessions, retries: p2pRetryCount })}
<Sparkles className="w-4 h-4 text-sky-400" />
<div className="text-sm font-medium">{t('ekgTopProvidersWorkload')}</div>
</div>
<div className="text-2xl font-semibold text-zinc-100 truncate">{ekgTopProvider}</div>
<div className="mt-2 text-xs text-zinc-500">{t('dashboardWorkloadSnapshot')}</div>
</div>
<div className="brand-card rounded-[28px] border border-zinc-800 p-5 min-h-[148px]">
<div className="flex items-center gap-2 text-zinc-200 mb-2">

View File

@@ -1,6 +1,7 @@
import React, { useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import { Check } from 'lucide-react';
import { useAppContext } from '../context/AppContext';
import { formatLocalDateTime } from '../utils/time';
@@ -247,20 +248,29 @@ const Nodes: React.FC = () => {
<button
key={nodeID}
onClick={() => setSelectedNodeID(nodeID)}
className={`w-full text-left px-3 py-3 border-b border-zinc-800/60 hover:bg-zinc-800/20 ${active ? 'bg-indigo-500/15' : ''}`}
className={`w-full text-left px-3 py-3 border-b border-zinc-800/60 transition-colors ${active ? 'bg-indigo-500/15' : ''}`}
>
<div className="text-sm font-medium text-zinc-100 truncate">{String(node?.name || nodeID)}</div>
<div className="text-xs text-zinc-400 truncate">{nodeID} · {String(node?.os || '-')} / {String(node?.arch || '-')}</div>
<div className="text-[11px] text-zinc-500 truncate">{String(node?.online ? t('online') : t('offline'))} · {String(node?.version || '-')}</div>
{tags.length > 0 && (
<div className="flex flex-wrap gap-1 mt-2">
{tags.slice(0, 4).map((tag: string) => (
<span key={`${nodeID}-${tag}`} className="rounded-full border border-zinc-700 bg-zinc-900/60 px-2 py-0.5 text-[10px] text-zinc-300">
{tag}
</span>
))}
<div className="flex items-start gap-3">
<div className="min-w-0 flex-1">
<div className="text-sm font-medium text-zinc-100 truncate">{String(node?.name || nodeID)}</div>
<div className="text-xs text-zinc-400 truncate">{nodeID} · {String(node?.os || '-')} / {String(node?.arch || '-')}</div>
<div className="text-[11px] text-zinc-500 truncate">{String(node?.online ? t('online') : t('offline'))} · {String(node?.version || '-')}</div>
{tags.length > 0 && (
<div className="flex flex-wrap gap-1 mt-2">
{tags.slice(0, 4).map((tag: string) => (
<span key={`${nodeID}-${tag}`} className="rounded-full border border-zinc-700 bg-zinc-900/60 px-2 py-0.5 text-[10px] text-zinc-300">
{tag}
</span>
))}
</div>
)}
</div>
)}
{active && (
<span className="mt-0.5 inline-flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-indigo-500/15 text-indigo-300">
<Check className="w-3.5 h-3.5" />
</span>
)}
</div>
</button>
);
})}
@@ -416,10 +426,19 @@ const Nodes: React.FC = () => {
<button
key={key || `dispatch-${index}`}
onClick={() => setSelectedDispatchKey(key)}
className={`w-full text-left px-3 py-2 border-b border-zinc-800/60 hover:bg-zinc-800/20 ${active ? 'bg-indigo-500/15' : ''}`}
className={`w-full text-left px-3 py-2 border-b border-zinc-800/60 transition-colors ${active ? 'bg-indigo-500/15' : ''}`}
>
<div className="text-sm font-medium text-zinc-100 truncate">{`${item?.action || '-'} · ${item?.used_transport || '-'}`}</div>
<div className="text-xs text-zinc-400 truncate">{formatLocalDateTime(item?.time)} · {Number(item?.duration_ms || 0)}ms · {Number(item?.artifact_count || 0)} {t('dashboardNodeDispatchArtifacts')}</div>
<div className="flex items-start gap-3">
<div className="min-w-0 flex-1">
<div className="text-sm font-medium text-zinc-100 truncate">{`${item?.action || '-'} · ${item?.used_transport || '-'}`}</div>
<div className="text-xs text-zinc-400 truncate">{formatLocalDateTime(item?.time)} · {Number(item?.duration_ms || 0)}ms · {Number(item?.artifact_count || 0)} {t('dashboardNodeDispatchArtifacts')}</div>
</div>
{active && (
<span className="mt-0.5 inline-flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-indigo-500/15 text-indigo-300">
<Check className="w-3.5 h-3.5" />
</span>
)}
</div>
</button>
);
})}

View File

@@ -202,13 +202,13 @@ const Skills: React.FC = () => {
</label>
</div>
<div className="flex items-center gap-3 flex-wrap">
<div className={`text-xs px-2 py-1 rounded-md border ${clawhubInstalled ? 'text-emerald-300 border-emerald-700/50 bg-emerald-900/20' : 'text-amber-300 border-amber-700/50 bg-amber-900/20'}`} title={clawhubPath || t('skillsClawhubNotFound')}>
<div className={`text-xs px-2 py-1 rounded-md border font-medium ${clawhubInstalled ? 'text-emerald-200 border-emerald-500/35 bg-emerald-500/12' : 'text-amber-100 border-amber-500/45 bg-amber-500/14 shadow-[inset_0_1px_0_rgba(255,255,255,0.04)]'}`} title={clawhubPath || t('skillsClawhubNotFound')}>
{t('skillsClawhubStatus')}: {clawhubInstalled ? t('installed') : t('notInstalled')}
</div>
{!clawhubInstalled && (
<button
onClick={installClawHubIfNeeded}
className="flex items-center gap-2 px-4 py-2 bg-amber-600 hover:bg-amber-500 text-white rounded-xl text-sm font-medium transition-colors shadow-sm"
className="flex items-center gap-2 px-4 py-2 rounded-xl text-sm font-medium transition-colors shadow-sm bg-cyan-500/15 text-cyan-200 border border-cyan-400/25 hover:bg-cyan-500/25 hover:border-cyan-300/35"
>
<Zap className="w-4 h-4" /> {t('skillsInstallNow')}
</button>
@@ -223,9 +223,16 @@ const Skills: React.FC = () => {
</div>
{!clawhubInstalled && (
<div className="rounded-2xl border border-amber-700/40 bg-amber-950/20 p-4 text-sm text-amber-100">
<div className="font-medium mb-1">{t('skillsClawhubMissingTitle')}</div>
<div className="text-amber-200/90">{t('skillsInstallPanelHint')}</div>
<div className="rounded-2xl border border-zinc-800/80 bg-zinc-950/45 p-4 text-sm shadow-sm">
<div className="flex items-start gap-3">
<div className="mt-0.5 flex h-9 w-9 shrink-0 items-center justify-center rounded-xl border border-amber-400/20 bg-amber-500/10 text-amber-300">
<Zap className="w-4 h-4" />
</div>
<div className="min-w-0">
<div className="font-medium text-zinc-100 mb-1">{t('skillsClawhubMissingTitle')}</div>
<div className="text-zinc-400 leading-6">{t('skillsInstallPanelHint')}</div>
</div>
</div>
</div>
)}

View File

@@ -1,5 +1,6 @@
import React, { useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Check } from 'lucide-react';
import { useAppContext } from '../context/AppContext';
import { useUI } from '../context/UIContext';
@@ -8,7 +9,6 @@ type SubagentProfile = {
name?: string;
notify_main_policy?: string;
role?: string;
system_prompt?: string;
system_prompt_file?: string;
tool_allowlist?: string[];
memory_namespace?: string;
@@ -33,7 +33,6 @@ const emptyDraft: SubagentProfile = {
name: '',
notify_main_policy: 'final_only',
role: '',
system_prompt: '',
system_prompt_file: '',
memory_namespace: '',
status: 'active',
@@ -81,7 +80,6 @@ const SubagentProfiles: React.FC = () => {
name: next.name || '',
notify_main_policy: next.notify_main_policy || 'final_only',
role: next.role || '',
system_prompt: next.system_prompt || '',
system_prompt_file: next.system_prompt_file || '',
memory_namespace: next.memory_namespace || '',
status: (next.status as string) || 'active',
@@ -142,7 +140,6 @@ const SubagentProfiles: React.FC = () => {
name: p.name || '',
notify_main_policy: p.notify_main_policy || 'final_only',
role: p.role || '',
system_prompt: p.system_prompt || '',
system_prompt_file: p.system_prompt_file || '',
memory_namespace: p.memory_namespace || '',
status: (p.status as string) || 'active',
@@ -195,7 +192,6 @@ const SubagentProfiles: React.FC = () => {
name: draft.name || '',
notify_main_policy: draft.notify_main_policy || 'final_only',
role: draft.role || '',
system_prompt: draft.system_prompt || '',
system_prompt_file: draft.system_prompt_file || '',
memory_namespace: draft.memory_namespace || '',
status: draft.status || 'active',
@@ -295,11 +291,20 @@ const SubagentProfiles: React.FC = () => {
<button
key={it.agent_id}
onClick={() => onSelect(it)}
className={`w-full text-left px-3 py-2 border-b border-zinc-800/50 hover:bg-zinc-800/20 ${selectedId === it.agent_id ? 'bg-indigo-500/15' : ''}`}
className={`w-full text-left px-3 py-2 border-b border-zinc-800/50 transition-colors ${selectedId === it.agent_id ? 'bg-indigo-500/15' : ''}`}
>
<div className="text-sm text-zinc-100 truncate">{it.agent_id || '-'}</div>
<div className="text-xs text-zinc-400 truncate">
{(it.status || 'active')} · {it.role || '-'} · {(it.memory_namespace || '-')}
<div className="flex items-start gap-3">
<div className="min-w-0 flex-1">
<div className="text-sm text-zinc-100 truncate">{it.agent_id || '-'}</div>
<div className="text-xs text-zinc-400 truncate">
{(it.status || 'active')} · {it.role || '-'} · {(it.memory_namespace || '-')}
</div>
</div>
{selectedId === it.agent_id && (
<span className="mt-0.5 inline-flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-indigo-500/15 text-indigo-300">
<Check className="w-3.5 h-3.5" />
</span>
)}
</div>
</button>
))}
@@ -409,15 +414,6 @@ const SubagentProfiles: React.FC = () => {
</div>
)}
</div>
<div className="md:col-span-2">
<div className="text-xs text-zinc-400 mb-1">System Prompt</div>
<textarea
value={draft.system_prompt || ''}
onChange={(e) => setDraft({ ...draft, system_prompt: e.target.value })}
className="w-full px-2 py-1 text-xs bg-zinc-900 border border-zinc-700 rounded min-h-[140px]"
placeholder="You are a coding specialist..."
/>
</div>
<div className="md:col-span-2">
<div className="flex items-center justify-between mb-1 gap-3">
<div className="text-xs text-zinc-400">system_prompt_file content</div>

View File

@@ -91,7 +91,6 @@ type RegistrySubagent = {
display_name?: string;
role?: string;
description?: string;
system_prompt?: string;
system_prompt_file?: string;
prompt_file_found?: boolean;
memory_namespace?: string;
@@ -392,7 +391,6 @@ const Subagents: React.FC = () => {
const [configAgentID, setConfigAgentID] = useState('');
const [configRole, setConfigRole] = useState('');
const [configDisplayName, setConfigDisplayName] = useState('');
const [configSystemPrompt, setConfigSystemPrompt] = useState('');
const [configSystemPromptFile, setConfigSystemPromptFile] = useState('');
const [configToolAllowlist, setConfigToolAllowlist] = useState('');
const [configRoutingKeywords, setConfigRoutingKeywords] = useState('');

View File

@@ -1,5 +1,6 @@
import React, { useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Check } from 'lucide-react';
import { useAppContext } from '../context/AppContext';
import { formatLocalDateTime } from '../utils/time';
@@ -143,11 +144,20 @@ const TaskAudit: React.FC = () => {
<button
key={`${it.task_id || idx}-${it.time || idx}`}
onClick={() => setSelected(it)}
className={`w-full text-left px-3 py-2 border-b border-zinc-800/60 hover:bg-zinc-800/20 ${active ? 'bg-indigo-500/15' : ''}`}
className={`w-full text-left px-3 py-2 border-b border-zinc-800/60 transition-colors ${active ? 'bg-indigo-500/15' : ''}`}
>
<div className="text-sm font-medium text-zinc-100 truncate">{it.task_id || `task-${idx + 1}`}</div>
<div className="text-xs text-zinc-400 truncate">{it.channel || '-'} · {it.status} · attempts:{it.attempts || 1} · {it.duration_ms || 0}ms · retry:{it.retry_count || 0} · {it.source || '-'} · {it.provider || '-'} / {it.model || '-'}</div>
<div className="text-[11px] text-zinc-500 truncate">{formatLocalDateTime(it.time)}</div>
<div className="flex items-start gap-3">
<div className="min-w-0 flex-1">
<div className="text-sm font-medium text-zinc-100 truncate">{it.task_id || `task-${idx + 1}`}</div>
<div className="text-xs text-zinc-400 truncate">{it.channel || '-'} · {it.status} · attempts:{it.attempts || 1} · {it.duration_ms || 0}ms · retry:{it.retry_count || 0} · {it.source || '-'} · {it.provider || '-'} / {it.model || '-'}</div>
<div className="text-[11px] text-zinc-500 truncate">{formatLocalDateTime(it.time)}</div>
</div>
{active && (
<span className="mt-0.5 inline-flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-indigo-500/15 text-indigo-300">
<Check className="w-3.5 h-3.5" />
</span>
)}
</div>
</button>
);
})}
@@ -240,11 +250,20 @@ const TaskAudit: React.FC = () => {
<button
key={`${it.time || idx}-${it.node || idx}-${it.action || idx}`}
onClick={() => setSelectedNode(it)}
className={`w-full text-left px-3 py-2 border-b border-zinc-800/60 hover:bg-zinc-800/20 ${active ? 'bg-indigo-500/15' : ''}`}
className={`w-full text-left px-3 py-2 border-b border-zinc-800/60 transition-colors ${active ? 'bg-indigo-500/15' : ''}`}
>
<div className="text-sm font-medium text-zinc-100 truncate">{`${it.node || '-'} · ${it.action || '-'}`}</div>
<div className="text-xs text-zinc-400 truncate">{it.used_transport || '-'} · {(it.duration_ms || 0)}ms · {(it.artifact_count || 0)} {t('dashboardNodeDispatchArtifacts')}</div>
<div className="text-[11px] text-zinc-500 truncate">{formatLocalDateTime(it.time)}</div>
<div className="flex items-start gap-3">
<div className="min-w-0 flex-1">
<div className="text-sm font-medium text-zinc-100 truncate">{`${it.node || '-'} · ${it.action || '-'}`}</div>
<div className="text-xs text-zinc-400 truncate">{it.used_transport || '-'} · {(it.duration_ms || 0)}ms · {(it.artifact_count || 0)} {t('dashboardNodeDispatchArtifacts')}</div>
<div className="text-[11px] text-zinc-500 truncate">{formatLocalDateTime(it.time)}</div>
</div>
{active && (
<span className="mt-0.5 inline-flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-indigo-500/15 text-indigo-300">
<Check className="w-3.5 h-3.5" />
</span>
)}
</div>
</button>
);
})}