diff --git a/crates/forge_main/src/model.rs b/crates/forge_main/src/model.rs index 6852225a34..c0faa48830 100644 --- a/crates/forge_main/src/model.rs +++ b/crates/forge_main/src/model.rs @@ -759,6 +759,18 @@ impl AppCommand { | AppCommand::Rename { .. } ) } + + /// Returns true for variants that are pure agent-switch shorthands whose + /// canonical name matches a built-in agent (forge, muse, sage). These + /// commands are already emitted as AGENT rows by the agent-info loop in + /// `on_show_commands`, so they must be excluded from the COMMAND loop to + /// avoid duplicate entries in `list commands --porcelain`. + pub fn is_agent_switch(&self) -> bool { + matches!( + self, + AppCommand::Forge | AppCommand::Muse | AppCommand::Sage + ) + } } #[cfg(test)] diff --git a/crates/forge_main/src/ui.rs b/crates/forge_main/src/ui.rs index a14c495183..9967c7e317 100644 --- a/crates/forge_main/src/ui.rs +++ b/crates/forge_main/src/ui.rs @@ -1333,7 +1333,10 @@ impl A + Send + Sync> UI // the list always stays in sync with what the REPL actually supports. // Internal/meta variants (Message, Custom, Shell, AgentSwitch, Rename) // are excluded via is_internal(). - for cmd in AppCommand::iter().filter(|c| !c.is_internal()) { + // Agent-switch shorthands (forge, muse, sage) are excluded via + // is_agent_switch() because they are already emitted as AGENT rows + // by the agent-info loop below, and must not appear twice. + for cmd in AppCommand::iter().filter(|c| !c.is_internal() && !c.is_agent_switch()) { info = info .add_title(cmd.name()) .add_key_value("type", CommandType::Command)