mirror of
https://github.com/YspCoder/clawgo.git
synced 2026-04-27 18:14:25 +08:00
fix: enforce subagent prompt files and refine webui
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"},
|
||||
|
||||
@@ -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"},
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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: '知识与调试',
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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('');
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
})}
|
||||
|
||||
Reference in New Issue
Block a user