From 5f8f6643cb341f9543fc1abc2d77e73d341ee059 Mon Sep 17 00:00:00 2001 From: MrFadiAi Date: Mon, 16 Feb 2026 07:18:21 +0100 Subject: [PATCH] feat: add subagent configuration, default agent toggle, and agent path restructure (v0.0.6) - Add nested subagent config per agent (allowAgents) with global defaults (maxSpawnDepth, maxChildrenPerAgent, maxConcurrent) - Add Default Agent toggle in agent form and quick wizard - Restructure agent paths: workspace-{id}, agents/{id}/agent, agents/{id}/sessions - Add name/default fields to AgentInfo - Add Subagent Defaults card with save functionality - Bump version to 0.0.6 --- package.json | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/src/commands/config.rs | 83 ++++++++++++++++++- src-tauri/src/main.rs | 1 + src-tauri/tauri.conf.json | 2 +- src/components/Agents/index.tsx | 136 ++++++++++++++++++++++++++++++- 6 files changed, 221 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index a69b9b9..f329697 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "openclaw-manager", - "version": "0.0.5", + "version": "0.0.6", "description": "🦞 OpenClaw Cross-Platform Manager - High-Performance AI Assistant Configuration & Service Management", "author": "OpenClaw Team", "license": "MIT", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 9bad292..0b0282f 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "openclaw-manager" -version = "0.0.5" +version = "0.0.6" description = "OpenClaw Cross-Platform Manager" authors = ["OpenClaw Team"] edition = "2021" diff --git a/src-tauri/src/commands/config.rs b/src-tauri/src/commands/config.rs index 0704d39..5fc698e 100644 --- a/src-tauri/src/commands/config.rs +++ b/src-tauri/src/commands/config.rs @@ -2134,6 +2134,25 @@ pub struct AgentInfo { pub sandbox: Option, pub heartbeat: Option, pub default: Option, + pub subagents: Option, +} + +/// Per-agent subagent configuration +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct SubagentConfig { + #[serde(alias = "allowAgents", alias = "allow_agents")] + pub allow_agents: Option>, +} + +/// Global subagent defaults +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct SubagentDefaults { + #[serde(alias = "maxSpawnDepth", alias = "max_spawn_depth")] + pub max_spawn_depth: Option, + #[serde(alias = "maxChildrenPerAgent", alias = "max_children_per_agent")] + pub max_children_per_agent: Option, + #[serde(alias = "maxConcurrent", alias = "max_concurrent")] + pub max_concurrent: Option, } /// Agent binding rule @@ -2158,6 +2177,7 @@ pub struct MatchRule { pub struct AgentsConfigResponse { pub agents: Vec, pub bindings: Vec, + pub subagent_defaults: SubagentDefaults, } /// Get multi-agent routing configuration @@ -2182,6 +2202,12 @@ pub async fn get_agents_config() -> Result { sandbox: agent_val.get("sandbox").and_then(|v| v.as_bool()), heartbeat: agent_val.pointer("/heartbeat/every").and_then(|v| v.as_str()).map(|s| s.to_string()), default: agent_val.get("default").and_then(|v| v.as_bool()), + subagents: agent_val.get("subagents").and_then(|v| { + let allow = v.get("allowAgents").and_then(|a| a.as_array()).map(|arr| { + arr.iter().filter_map(|x| x.as_str().map(|s| s.to_string())).collect() + }); + Some(SubagentConfig { allow_agents: allow }) + }), }); } } else if let Some(list_obj) = config.pointer("/agents/list").and_then(|v| v.as_object()) { @@ -2196,6 +2222,12 @@ pub async fn get_agents_config() -> Result { sandbox: agent_val.get("sandbox").and_then(|v| v.as_bool()), heartbeat: agent_val.pointer("/heartbeat/every").and_then(|v| v.as_str()).map(|s| s.to_string()), default: agent_val.get("default").and_then(|v| v.as_bool()), + subagents: agent_val.get("subagents").and_then(|v| { + let allow = v.get("allowAgents").and_then(|a| a.as_array()).map(|arr| { + arr.iter().filter_map(|x| x.as_str().map(|s| s.to_string())).collect() + }); + Some(SubagentConfig { allow_agents: allow }) + }), }); } } @@ -2220,8 +2252,19 @@ pub async fn get_agents_config() -> Result { } } + // Read global subagent defaults from agents.defaults.subagents + let subagent_defaults = if let Some(sub_val) = config.pointer("/agents/defaults/subagents") { + SubagentDefaults { + max_spawn_depth: sub_val.get("maxSpawnDepth").and_then(|v| v.as_u64()).map(|v| v as u32), + max_children_per_agent: sub_val.get("maxChildrenPerAgent").and_then(|v| v.as_u64()).map(|v| v as u32), + max_concurrent: sub_val.get("maxConcurrent").and_then(|v| v.as_u64()).map(|v| v as u32), + } + } else { + SubagentDefaults::default() + }; + info!("[Agents] Found {} agents, {} bindings", agents.len(), bindings.len()); - Ok(AgentsConfigResponse { agents, bindings }) + Ok(AgentsConfigResponse { agents, bindings, subagent_defaults }) } /// Save (add/update) an agent @@ -2270,6 +2313,13 @@ pub async fn save_agent(agent: AgentInfo) -> Result { agent_obj["default"] = json!(true); } } + if let Some(sub) = &agent.subagents { + if let Some(allow) = &sub.allow_agents { + if !allow.is_empty() { + agent_obj["subagents"] = json!({ "allowAgents": allow }); + } + } + } // Migrate legacy object format to array if needed let mut list = if let Some(arr) = config["agents"].get("list").and_then(|v| v.as_array()) { @@ -2426,6 +2476,37 @@ pub async fn save_agent(agent: AgentInfo) -> Result { Ok(format!("Agent '{}' saved", agent.id)) } +/// Save global subagent defaults +#[command] +pub async fn save_subagent_defaults(defaults: SubagentDefaults) -> Result { + info!("[Agents] Saving subagent defaults"); + let mut config = load_openclaw_config()?; + + // Ensure agents.defaults exists + if config.get("agents").is_none() { + config["agents"] = json!({}); + } + if config["agents"].get("defaults").is_none() { + config["agents"]["defaults"] = json!({}); + } + + let mut sub_obj = json!({}); + if let Some(depth) = defaults.max_spawn_depth { + sub_obj["maxSpawnDepth"] = json!(depth); + } + if let Some(children) = defaults.max_children_per_agent { + sub_obj["maxChildrenPerAgent"] = json!(children); + } + if let Some(concurrent) = defaults.max_concurrent { + sub_obj["maxConcurrent"] = json!(concurrent); + } + + config["agents"]["defaults"]["subagents"] = sub_obj; + + save_openclaw_config(&config)?; + Ok("Subagent defaults saved".to_string()) +} + /// Delete an agent #[command] pub async fn delete_agent(agent_id: String) -> Result { diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index dc3c72b..130570a 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -97,6 +97,7 @@ fn main() { config::get_openclaw_home_dir, config::get_agents_config, config::save_agent, + config::save_subagent_defaults, config::delete_agent, config::save_agent_binding, config::delete_agent_binding, diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 9e304db..9562401 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "OpenClaw Manager", - "version": "0.0.5", + "version": "0.0.6", "identifier": "com.openclaw.manager", "build": { "beforeDevCommand": "npm run dev", diff --git a/src/components/Agents/index.tsx b/src/components/Agents/index.tsx index 9c98b31..b2854ae 100644 --- a/src/components/Agents/index.tsx +++ b/src/components/Agents/index.tsx @@ -25,6 +25,16 @@ import { import { appLogger } from '../../lib/logger'; // Types corresponding to Rust backend +interface SubagentConfig { + allow_agents: string[] | null; +} + +interface SubagentDefaults { + max_spawn_depth: number | null; + max_children_per_agent: number | null; + max_concurrent: number | null; +} + interface AgentInfo { id: string; name: string | null; @@ -34,6 +44,7 @@ interface AgentInfo { sandbox: boolean | null; heartbeat: string | null; default: boolean | null; + subagents: SubagentConfig | null; } interface MatchRule { @@ -50,6 +61,7 @@ interface AgentBinding { interface AgentsConfigResponse { agents: AgentInfo[]; bindings: AgentBinding[]; + subagent_defaults: SubagentDefaults; } interface TelegramAccount { @@ -103,8 +115,17 @@ export function Agents() { sandbox: null, heartbeat: null, default: null, + subagents: null, }); + // Subagent defaults state + const [subagentDefaults, setSubagentDefaults] = useState({ + max_spawn_depth: null, + max_children_per_agent: null, + max_concurrent: null, + }); + const [savingDefaults, setSavingDefaults] = useState(false); + const [bindingForm, setBindingForm] = useState({ agent_id: '', match_rule: { @@ -131,6 +152,7 @@ export function Agents() { const data = await invoke('get_agents_config'); setAgents(data.agents); setBindings(data.bindings); + setSubagentDefaults(data.subagent_defaults); } catch (e) { setError(String(e)); appLogger.error('Failed to fetch agents config', e); @@ -238,6 +260,7 @@ export function Agents() { sandbox: agent.sandbox, heartbeat: agent.heartbeat, default: null, + subagents: null, }); setSystemPrompt(''); // Load original system prompt for cloning @@ -284,6 +307,7 @@ export function Agents() { sandbox: null, heartbeat: null, default: wizardForm.isDefault || null, + subagents: null, }; await invoke('save_agent', { agent }); @@ -410,7 +434,7 @@ export function Agents() { + +
+
+ + setSubagentDefaults({ ...subagentDefaults, max_spawn_depth: e.target.value ? parseInt(e.target.value) : null })} + className="input-base text-center" + placeholder="2" + /> +

Nesting levels

+
+
+ + setSubagentDefaults({ ...subagentDefaults, max_children_per_agent: e.target.value ? parseInt(e.target.value) : null })} + className="input-base text-center" + placeholder="5" + /> +

Per parent

+
+
+ + setSubagentDefaults({ ...subagentDefaults, max_concurrent: e.target.value ? parseInt(e.target.value) : null })} + className="input-base text-center" + placeholder="8" + /> +

System-wide

+
+
+ + {/* Bindings Section */}
@@ -767,6 +862,45 @@ export function Agents() { placeholder='e.g. "1h" or "0m" to disable' />
+ + {/* Subagents — Allow Agents */} +
+ +

Select which other agents this agent can spawn as subagents.

+ {agents.filter(a => a.id !== agentForm.id).length === 0 ? ( +

No other agents available. Create more agents first.

+ ) : ( +
+ {agents.filter(a => a.id !== agentForm.id).map(a => { + const allowed = agentForm.subagents?.allow_agents || []; + const isChecked = allowed.includes(a.id); + return ( +
+ { + const newList = e.target.checked + ? [...allowed, a.id] + : allowed.filter(x => x !== a.id); + setAgentForm({ + ...agentForm, + subagents: { allow_agents: newList.length > 0 ? newList : null } + }); + }} + className="w-4 h-4 rounded bg-dark-600 border-dark-500 text-claw-500 focus:ring-claw-500/50" + /> + +
+ ); + })} +
+ )} +