@@ -108,6 +108,23 @@ impl AgentInfo {
108108 }
109109}
110110
111+ /// Full sub-agent definition for editing (user/project custom agents only)
112+ #[ derive( Debug , Clone , Serialize , Deserialize ) ]
113+ #[ serde( rename_all = "camelCase" ) ]
114+ pub struct CustomSubagentDetail {
115+ pub subagent_id : String ,
116+ pub name : String ,
117+ pub description : String ,
118+ pub prompt : String ,
119+ pub tools : Vec < String > ,
120+ pub readonly : bool ,
121+ pub enabled : bool ,
122+ pub model : String ,
123+ pub path : String ,
124+ /// `"user"` or `"project"`
125+ pub level : String ,
126+ }
127+
111128fn default_model_id_for_builtin_agent ( agent_type : & str ) -> & ' static str {
112129 match agent_type {
113130 "agentic" | "Cowork" | "Plan" | "debug" | "Claw" => "auto" ,
@@ -674,6 +691,215 @@ impl AgentRegistry {
674691 Ok ( ( ) )
675692 }
676693
694+ /// Load custom subagents if needed, then return full definition for the editor UI
695+ pub async fn get_custom_subagent_detail (
696+ & self ,
697+ agent_id : & str ,
698+ workspace_root : Option < & Path > ,
699+ ) -> BitFunResult < CustomSubagentDetail > {
700+ if let Some ( root) = workspace_root {
701+ self . load_custom_subagents ( root) . await ;
702+ }
703+ self . get_custom_subagent_detail_inner ( agent_id, workspace_root)
704+ }
705+
706+ fn get_custom_subagent_detail_inner (
707+ & self ,
708+ agent_id : & str ,
709+ workspace_root : Option < & Path > ,
710+ ) -> BitFunResult < CustomSubagentDetail > {
711+ let entry = self . find_agent_entry ( agent_id, workspace_root) . ok_or_else ( || {
712+ BitFunError :: agent ( format ! ( "Subagent not found: {}" , agent_id) )
713+ } ) ?;
714+ if entry. category != AgentCategory :: SubAgent {
715+ return Err ( BitFunError :: agent ( format ! (
716+ "Agent '{}' is not a subagent" ,
717+ agent_id
718+ ) ) ) ;
719+ }
720+ if entry. subagent_source == Some ( SubAgentSource :: Builtin ) {
721+ return Err ( BitFunError :: agent (
722+ "Built-in subagents cannot be edited here" . to_string ( ) ,
723+ ) ) ;
724+ }
725+ let custom = entry
726+ . agent
727+ . as_any ( )
728+ . downcast_ref :: < CustomSubagent > ( )
729+ . ok_or_else ( || {
730+ BitFunError :: agent ( format ! (
731+ "Subagent '{}' is not a custom subagent file" ,
732+ agent_id
733+ ) )
734+ } ) ?;
735+ let ( enabled, model) = match & entry. custom_config {
736+ Some ( c) => ( c. enabled , c. model . clone ( ) ) ,
737+ None => ( custom. enabled , custom. model . clone ( ) ) ,
738+ } ;
739+ let level = match custom. kind {
740+ CustomSubagentKind :: User => "user" ,
741+ CustomSubagentKind :: Project => "project" ,
742+ } ;
743+ Ok ( CustomSubagentDetail {
744+ subagent_id : agent_id. to_string ( ) ,
745+ name : custom. name . clone ( ) ,
746+ description : custom. description . clone ( ) ,
747+ prompt : custom. prompt . clone ( ) ,
748+ tools : custom. tools . clone ( ) ,
749+ readonly : custom. readonly ,
750+ enabled,
751+ model,
752+ path : custom. path . clone ( ) ,
753+ level : level. to_string ( ) ,
754+ } )
755+ }
756+
757+ /// Update description, prompt, tools, and readonly for a custom sub-agent (id and file path unchanged)
758+ pub async fn update_custom_subagent_definition (
759+ & self ,
760+ agent_id : & str ,
761+ workspace_root : Option < & Path > ,
762+ description : String ,
763+ prompt : String ,
764+ tools : Option < Vec < String > > ,
765+ readonly : Option < bool > ,
766+ ) -> BitFunResult < ( ) > {
767+ if let Some ( root) = workspace_root {
768+ self . load_custom_subagents ( root) . await ;
769+ }
770+ let entry = self . find_agent_entry ( agent_id, workspace_root) . ok_or_else ( || {
771+ BitFunError :: agent ( format ! ( "Subagent not found: {}" , agent_id) )
772+ } ) ?;
773+ if entry. category != AgentCategory :: SubAgent {
774+ return Err ( BitFunError :: agent ( format ! (
775+ "Agent '{}' is not a subagent" ,
776+ agent_id
777+ ) ) ) ;
778+ }
779+ if entry. subagent_source == Some ( SubAgentSource :: Builtin ) {
780+ return Err ( BitFunError :: agent (
781+ "Built-in subagents cannot be edited" . to_string ( ) ,
782+ ) ) ;
783+ }
784+ let old = entry
785+ . agent
786+ . as_any ( )
787+ . downcast_ref :: < CustomSubagent > ( )
788+ . ok_or_else ( || {
789+ BitFunError :: agent ( format ! (
790+ "Subagent '{}' is not a custom subagent file" ,
791+ agent_id
792+ ) )
793+ } ) ?;
794+ let tools = tools
795+ . filter ( |t| !t. is_empty ( ) )
796+ . unwrap_or_else ( || {
797+ vec ! [
798+ "LS" . to_string( ) ,
799+ "Read" . to_string( ) ,
800+ "Glob" . to_string( ) ,
801+ "Grep" . to_string( ) ,
802+ ]
803+ } ) ;
804+ let mut new_subagent = CustomSubagent :: new (
805+ old. name . clone ( ) ,
806+ description,
807+ tools,
808+ prompt,
809+ readonly. unwrap_or ( old. readonly ) ,
810+ old. path . clone ( ) ,
811+ old. kind ,
812+ ) ;
813+ new_subagent. enabled = old. enabled ;
814+ new_subagent. model = old. model . clone ( ) ;
815+
816+ let valid_tools = get_all_registered_tool_names ( ) . await ;
817+ let valid_models = Self :: get_valid_model_ids ( ) . await ;
818+ Self :: validate_custom_subagent ( & mut new_subagent, & valid_tools, & valid_models) ;
819+
820+ new_subagent. save_to_file ( None , None ) ?;
821+
822+ self . replace_custom_subagent_entry ( agent_id, workspace_root, new_subagent)
823+ }
824+
825+ fn replace_custom_subagent_entry (
826+ & self ,
827+ agent_id : & str ,
828+ workspace_root : Option < & Path > ,
829+ new_subagent : CustomSubagent ,
830+ ) -> BitFunResult < ( ) > {
831+ let mut map = self . write_agents ( ) ;
832+ if map. contains_key ( agent_id) {
833+ let old_entry = map. get ( agent_id) . ok_or_else ( || {
834+ BitFunError :: agent ( format ! ( "Subagent not found: {}" , agent_id) )
835+ } ) ?;
836+ if old_entry. category != AgentCategory :: SubAgent {
837+ return Err ( BitFunError :: agent ( format ! (
838+ "Agent '{}' is not a subagent" ,
839+ agent_id
840+ ) ) ) ;
841+ }
842+ if old_entry. subagent_source == Some ( SubAgentSource :: Builtin ) {
843+ return Err ( BitFunError :: agent (
844+ "Cannot replace built-in subagent" . to_string ( ) ,
845+ ) ) ;
846+ }
847+ let subagent_source = old_entry. subagent_source ;
848+ let cfg = CustomSubagentConfig {
849+ enabled : new_subagent. enabled ,
850+ model : new_subagent. model . clone ( ) ,
851+ } ;
852+ map. insert (
853+ agent_id. to_string ( ) ,
854+ AgentEntry {
855+ category : AgentCategory :: SubAgent ,
856+ subagent_source,
857+ agent : Arc :: new ( new_subagent) ,
858+ custom_config : Some ( cfg) ,
859+ } ,
860+ ) ;
861+ return Ok ( ( ) ) ;
862+ }
863+ drop ( map) ;
864+
865+ let root = workspace_root. ok_or_else ( || {
866+ BitFunError :: agent ( "Workspace path is required to update project subagent" . to_string ( ) )
867+ } ) ?;
868+ let mut pm = self . write_project_subagents ( ) ;
869+ let entries = pm. get_mut ( root) . ok_or_else ( || {
870+ BitFunError :: agent ( "Project subagent cache not loaded for this workspace" . to_string ( ) )
871+ } ) ?;
872+ let old_entry = entries. get ( agent_id) . ok_or_else ( || {
873+ BitFunError :: agent ( format ! ( "Subagent not found: {}" , agent_id) )
874+ } ) ?;
875+ if old_entry. category != AgentCategory :: SubAgent {
876+ return Err ( BitFunError :: agent ( format ! (
877+ "Agent '{}' is not a subagent" ,
878+ agent_id
879+ ) ) ) ;
880+ }
881+ if old_entry. subagent_source == Some ( SubAgentSource :: Builtin ) {
882+ return Err ( BitFunError :: agent (
883+ "Cannot replace built-in subagent" . to_string ( ) ,
884+ ) ) ;
885+ }
886+ let subagent_source = old_entry. subagent_source ;
887+ let cfg = CustomSubagentConfig {
888+ enabled : new_subagent. enabled ,
889+ model : new_subagent. model . clone ( ) ,
890+ } ;
891+ entries. insert (
892+ agent_id. to_string ( ) ,
893+ AgentEntry {
894+ category : AgentCategory :: SubAgent ,
895+ subagent_source,
896+ agent : Arc :: new ( new_subagent) ,
897+ custom_config : Some ( cfg) ,
898+ } ,
899+ ) ;
900+ Ok ( ( ) )
901+ }
902+
677903 /// remove single non-built-in subagent, return its file path (used for caller to delete file)
678904 /// only allow removing entries that are SubAgent and not Builtin
679905 pub fn remove_subagent ( & self , agent_id : & str ) -> BitFunResult < Option < String > > {
0 commit comments