From 4744a3d333ed82399173b131ada3d429b181e67d Mon Sep 17 00:00:00 2001 From: Tushar Date: Sun, 14 Jun 2026 11:54:51 +0530 Subject: [PATCH 01/12] fix(todo_fmt): update cancelled status icon --- crates/forge_app/src/fmt/todo_fmt.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/forge_app/src/fmt/todo_fmt.rs b/crates/forge_app/src/fmt/todo_fmt.rs index cee5548184..7b676a21a2 100644 --- a/crates/forge_app/src/fmt/todo_fmt.rs +++ b/crates/forge_app/src/fmt/todo_fmt.rs @@ -21,7 +21,7 @@ fn format_todo_line(todo: &Todo, line_style: TodoLineStyle) -> String { TodoStatus::Completed => "󰄵", TodoStatus::InProgress => "󰄗", TodoStatus::Pending => "󰄱", - TodoStatus::Cancelled => "󰅙", + TodoStatus::Cancelled => "", }; let content = match todo.status { From 0761478df36d5fdb54edeba5a7e50b3e6bfeafa5 Mon Sep 17 00:00:00 2001 From: Tushar Date: Sun, 14 Jun 2026 14:56:16 +0530 Subject: [PATCH 02/12] feat(conversation): add tree command to display nested conversations --- crates/forge_main/src/cli.rs | 27 +++++++++ crates/forge_main/src/ui.rs | 71 ++++++++++++++++++++++- shell-plugin/lib/actions/conversation.zsh | 6 ++ shell-plugin/lib/dispatcher.zsh | 3 + 4 files changed, 106 insertions(+), 1 deletion(-) diff --git a/crates/forge_main/src/cli.rs b/crates/forge_main/src/cli.rs index a4c859bcd7..f3cb790f2d 100644 --- a/crates/forge_main/src/cli.rs +++ b/crates/forge_main/src/cli.rs @@ -757,6 +757,12 @@ pub enum ConversationCommand { md: bool, }, + /// Show nested conversations spawned by a conversation. + Tree { + /// Conversation ID. + id: ConversationId, + }, + /// Show conversation details. Info { /// Conversation ID. @@ -1085,6 +1091,27 @@ mod tests { assert_eq!(is_list, true); } + #[test] + fn test_conversation_tree() { + let fixture = Cli::parse_from([ + "forge", + "conversation", + "tree", + "550e8400-e29b-41d4-a716-446655440004", + ]); + let actual = match fixture.subcommands { + Some(TopLevelCommand::Conversation(conversation)) => match conversation.command { + ConversationCommand::Tree { id } => Some(id), + _ => None, + }, + _ => None, + }; + let expected = Some( + ConversationId::parse("550e8400-e29b-41d4-a716-446655440004").unwrap(), + ); + assert_eq!(actual, expected); + } + #[test] fn test_session_alias_list() { let fixture = Cli::parse_from(["forge", "session", "list"]); diff --git a/crates/forge_main/src/ui.rs b/crates/forge_main/src/ui.rs index 59a1057328..6adb3620b8 100644 --- a/crates/forge_main/src/ui.rs +++ b/crates/forge_main/src/ui.rs @@ -1,4 +1,4 @@ -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::path::PathBuf; use std::str::FromStr; use std::sync::Arc; @@ -839,6 +839,7 @@ impl A + Send + Sync> UI let max_conversations = self.config.max_conversations; let conversations = self.api.get_conversations(Some(max_conversations)).await?; + let conversations = Self::user_initiated_conversations(conversations); if !conversations.is_empty() && let Some(conversation) = ConversationSelector::select_conversation( @@ -922,6 +923,11 @@ impl A + Send + Sync> UI self.on_show_last_message(conversation, md).await?; } + ConversationCommand::Tree { id } => { + let conversation = self.validate_conversation_exists(&id).await?; + + self.on_show_conversation_tree(conversation).await?; + } ConversationCommand::Info { id } => { let conversation = self.validate_conversation_exists(&id).await?; @@ -2032,9 +2038,53 @@ impl A + Send + Sync> UI Ok(()) } + async fn on_show_conversation_tree(&mut self, root: Conversation) -> anyhow::Result<()> { + let mut lines = Vec::new(); + let mut stack = vec![(root, String::new(), true, true)]; + let mut visited = HashSet::new(); + + while let Some((conversation, prefix, is_last, is_root)) = stack.pop() { + if !visited.insert(conversation.id) { + continue; + } + + let title = conversation + .title + .as_deref() + .map(str::to_string) + .unwrap_or_else(|| markers::EMPTY.to_string()); + let connector = if is_root { + String::new() + } else if is_last { + "└── ".to_string() + } else { + "├── ".to_string() + }; + lines.push(format!("{prefix}{connector}{} {title}", conversation.id)); + + let child_prefix = if is_root { + String::new() + } else if is_last { + format!("{prefix} ") + } else { + format!("{prefix}│ ") + }; + + let children = self.fetch_related_conversations(&conversation).await; + let child_count = children.len(); + for (index, child) in children.into_iter().enumerate().rev() { + stack.push((child, child_prefix.clone(), index + 1 == child_count, false)); + } + } + + self.writeln(lines.join("\n"))?; + Ok(()) + } + async fn on_show_conversations(&mut self, porcelain: bool) -> anyhow::Result<()> { let max_conversations = self.config.max_conversations; let conversations = self.api.get_conversations(Some(max_conversations)).await?; + let conversations = Self::user_initiated_conversations(conversations); if conversations.is_empty() { return Ok(()); @@ -2086,6 +2136,25 @@ impl A + Send + Sync> UI Ok(()) } + fn user_initiated_conversations(conversations: Vec) -> Vec { + let related_ids: HashSet = conversations + .iter() + .flat_map(Conversation::related_conversation_ids) + .collect(); + + conversations + .into_iter() + .filter(|conversation| { + conversation + .context + .as_ref() + .and_then(|context| context.initiator.as_deref()) + .is_none_or(|initiator| initiator == "user") + && !related_ids.contains(&conversation.id) + }) + .collect() + } + async fn on_command(&mut self, command: AppCommand) -> anyhow::Result { match command { AppCommand::Conversations { id } => { diff --git a/shell-plugin/lib/actions/conversation.zsh b/shell-plugin/lib/actions/conversation.zsh index 4a31c8bbf1..05292d9418 100644 --- a/shell-plugin/lib/actions/conversation.zsh +++ b/shell-plugin/lib/actions/conversation.zsh @@ -6,6 +6,7 @@ # - :conversation - List and switch conversations (with interactive picker) # - :conversation - Switch to specific conversation by ID # - :conversation - - Toggle between current and previous conversation (like cd -) +# - :conversation-tree - Show nested conversations spawned by current conversation # - :clone - Clone current or selected conversation # - :clone - Clone specific conversation by ID # - :copy - Copy last assistant message to OS clipboard as raw markdown @@ -115,6 +116,11 @@ function _forge_action_conversation() { fi } +# Action handler: Show nested conversations spawned by current conversation +function _forge_action_conversation_tree() { + _forge_handle_conversation_command "tree" +} + # Action handler: Clone conversation function _forge_action_clone() { local input_text="$1" diff --git a/shell-plugin/lib/dispatcher.zsh b/shell-plugin/lib/dispatcher.zsh index 0be98eb808..1a0575c06a 100644 --- a/shell-plugin/lib/dispatcher.zsh +++ b/shell-plugin/lib/dispatcher.zsh @@ -169,6 +169,9 @@ function forge-accept-line() { conversation|c) _forge_action_conversation "$input_text" ;; + conversation-tree|ct) + _forge_action_conversation_tree + ;; config-model|cm) _forge_action_model "$input_text" ;; From d3980319a35ed4d8f24dc5dd7fb57b89a92ff2a1 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sun, 14 Jun 2026 09:32:54 +0000 Subject: [PATCH 03/12] [autofix.ci] apply automated fixes --- crates/forge_main/src/cli.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/forge_main/src/cli.rs b/crates/forge_main/src/cli.rs index f3cb790f2d..4ea8649d6a 100644 --- a/crates/forge_main/src/cli.rs +++ b/crates/forge_main/src/cli.rs @@ -1106,9 +1106,7 @@ mod tests { }, _ => None, }; - let expected = Some( - ConversationId::parse("550e8400-e29b-41d4-a716-446655440004").unwrap(), - ); + let expected = Some(ConversationId::parse("550e8400-e29b-41d4-a716-446655440004").unwrap()); assert_eq!(actual, expected); } From 312c71b36d8fc3e312d3cad549d3a36b766459d5 Mon Sep 17 00:00:00 2001 From: Tushar Date: Sun, 14 Jun 2026 15:15:26 +0530 Subject: [PATCH 04/12] feat(conversation): add parent flag to list child conversations --- crates/forge_main/src/cli.rs | 6 ++++- crates/forge_main/src/model.rs | 10 ++++++++ crates/forge_main/src/ui.rs | 45 ++++++++++++++++++++++++---------- 3 files changed, 47 insertions(+), 14 deletions(-) diff --git a/crates/forge_main/src/cli.rs b/crates/forge_main/src/cli.rs index 4ea8649d6a..d77a9ccee0 100644 --- a/crates/forge_main/src/cli.rs +++ b/crates/forge_main/src/cli.rs @@ -703,8 +703,12 @@ pub enum ConfigGetField { /// Command group for conversation management. #[derive(Parser, Debug, Clone)] pub struct ConversationCommandGroup { + /// List child conversations of a parent conversation. + #[arg(long)] + pub parent: Option, + #[command(subcommand)] - pub command: ConversationCommand, + pub command: Option, } #[derive(Subcommand, Debug, Clone)] diff --git a/crates/forge_main/src/model.rs b/crates/forge_main/src/model.rs index 5ee93c0405..aea647d14e 100644 --- a/crates/forge_main/src/model.rs +++ b/crates/forge_main/src/model.rs @@ -114,6 +114,8 @@ impl ForgeCommandManager { | "logout" | "retry" | "conversations" + | "conversation-tree" + | "ct" | "list" | "commit" | "rename" @@ -650,6 +652,13 @@ pub enum AppCommand { id: Option, }, + /// Show nested conversations spawned by the current conversation + #[strum(props( + usage = "Show nested conversations spawned by the current conversation [alias: ct]" + ))] + #[command(name = "conversation-tree", alias = "ct")] + ConversationTree, + /// Delete a conversation permanently #[strum(props(usage = "Delete a conversation permanently"))] #[command(skip)] @@ -716,6 +725,7 @@ impl AppCommand { AppCommand::Logout => "logout", AppCommand::Retry => "retry", AppCommand::Conversations { .. } => "conversation", + AppCommand::ConversationTree => "conversation-tree", AppCommand::Delete => "delete", AppCommand::Rename { .. } => "rename", AppCommand::AgentSwitch(agent_id) => agent_id, diff --git a/crates/forge_main/src/ui.rs b/crates/forge_main/src/ui.rs index 6adb3620b8..50e772c814 100644 --- a/crates/forge_main/src/ui.rs +++ b/crates/forge_main/src/ui.rs @@ -863,14 +863,21 @@ impl A + Send + Sync> UI &mut self, conversation_group: crate::cli::ConversationCommandGroup, ) -> anyhow::Result<()> { + // When --parent is provided, list child conversations of that parent + if let Some(parent_id) = conversation_group.parent { + let parent = self.validate_conversation_exists(&parent_id).await?; + self.on_show_conversation_children(parent).await?; + return Ok(()); + } + match conversation_group.command { - ConversationCommand::List { porcelain } => { + Some(ConversationCommand::List { porcelain }) => { self.on_show_conversations(porcelain).await?; } - ConversationCommand::New => { + Some(ConversationCommand::New) => { self.handle_generate_conversation_id().await?; } - ConversationCommand::Dump { id, html } => { + Some(ConversationCommand::Dump { id, html }) => { self.validate_conversation_exists(&id).await?; let original_id = self.state.conversation_id; @@ -881,7 +888,7 @@ impl A + Send + Sync> UI self.state.conversation_id = original_id; } - ConversationCommand::Compact { id } => { + Some(ConversationCommand::Compact { id }) => { self.validate_conversation_exists(&id).await?; let original_id = self.state.conversation_id; @@ -892,7 +899,7 @@ impl A + Send + Sync> UI self.state.conversation_id = original_id; } - ConversationCommand::Delete { id } => { + Some(ConversationCommand::Delete { id }) => { let conversation_id = ConversationId::parse(&id).context(format!("Invalid conversation ID: {id}"))?; @@ -900,7 +907,7 @@ impl A + Send + Sync> UI self.on_conversation_delete(conversation_id).await?; } - ConversationCommand::Retry { id } => { + Some(ConversationCommand::Retry { id }) => { self.validate_conversation_exists(&id).await?; let original_id = self.state.conversation_id; @@ -911,41 +918,41 @@ impl A + Send + Sync> UI self.state.conversation_id = original_id; } - ConversationCommand::Resume { id } => { + Some(ConversationCommand::Resume { id }) => { self.validate_conversation_exists(&id).await?; self.state.conversation_id = Some(id); self.writeln_title(TitleFormat::info(format!("Resumed conversation: {id}")))?; // Interactive mode will be handled by the main loop } - ConversationCommand::Show { id, md } => { + Some(ConversationCommand::Show { id, md }) => { let conversation = self.validate_conversation_exists(&id).await?; self.on_show_last_message(conversation, md).await?; } - ConversationCommand::Tree { id } => { + Some(ConversationCommand::Tree { id }) => { let conversation = self.validate_conversation_exists(&id).await?; self.on_show_conversation_tree(conversation).await?; } - ConversationCommand::Info { id } => { + Some(ConversationCommand::Info { id }) => { let conversation = self.validate_conversation_exists(&id).await?; self.on_show_conv_info(conversation).await?; } - ConversationCommand::Stats { id, porcelain } => { + Some(ConversationCommand::Stats { id, porcelain }) => { let conversation = self.validate_conversation_exists(&id).await?; self.on_show_conv_stats(conversation, porcelain).await?; } - ConversationCommand::Clone { id, porcelain } => { + Some(ConversationCommand::Clone { id, porcelain }) => { let conversation = self.validate_conversation_exists(&id).await?; self.spinner.start(Some("Cloning"))?; self.on_clone_conversation(conversation, porcelain).await?; self.spinner.stop(None)?; } - ConversationCommand::Rename { id, name } => { + Some(ConversationCommand::Rename { id, name }) => { self.validate_conversation_exists(&id).await?; let name = name.trim().to_string(); @@ -960,6 +967,9 @@ impl A + Send + Sync> UI name.bold() )))?; } + None => { + self.on_show_conversations(false).await?; + } } Ok(()) @@ -2004,6 +2014,7 @@ impl A + Send + Sync> UI self.spinner.start(Some("Loading Conversations"))?; let max_conversations = self.config.max_conversations; let conversations = self.api.get_conversations(Some(max_conversations)).await?; + let conversations = Self::user_initiated_conversations(conversations); self.spinner.stop(None)?; if conversations.is_empty() { @@ -2173,6 +2184,14 @@ impl A + Send + Sync> UI self.list_conversations().await?; } } + AppCommand::ConversationTree => { + let conversation_id = self + .state + .conversation_id + .ok_or_else(|| anyhow::anyhow!("No active conversation"))?; + let conversation = self.validate_conversation_exists(&conversation_id).await?; + self.on_show_conversation_tree(conversation).await?; + } AppCommand::Compact => { self.spinner.start(Some("Compacting"))?; self.on_compaction().await?; From 41486fb5c0e4208974bf4b31e067d21a4722cfa1 Mon Sep 17 00:00:00 2001 From: Tushar Date: Sun, 14 Jun 2026 15:15:41 +0530 Subject: [PATCH 05/12] feat(ui): add method to display child conversations in flat list view --- crates/forge_main/src/ui.rs | 43 +++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/crates/forge_main/src/ui.rs b/crates/forge_main/src/ui.rs index 50e772c814..f57eee794f 100644 --- a/crates/forge_main/src/ui.rs +++ b/crates/forge_main/src/ui.rs @@ -2092,6 +2092,49 @@ impl A + Send + Sync> UI Ok(()) } + /// Displays child conversations of a parent as a flat list + async fn on_show_conversation_children(&mut self, parent: Conversation) -> anyhow::Result<()> { + let children = self.fetch_related_conversations(&parent).await; + + if children.is_empty() { + self.writeln_title(TitleFormat::info("No child conversations found."))?; + return Ok(()); + } + + let mut info = Info::new(); + + for conv in children.into_iter() { + let title = conv + .title + .as_deref() + .map(|t| t.to_string()) + .unwrap_or_else(|| markers::EMPTY.to_string()); + + let duration = chrono::Utc::now().signed_duration_since( + conv.metadata.updated_at.unwrap_or(conv.metadata.created_at), + ); + let duration = + std::time::Duration::from_secs((duration.num_minutes() * 60).max(0) as u64); + let time_ago = if duration.is_zero() { + "now".to_string() + } else { + format!("{} ago", humantime::format_duration(duration)) + }; + + info = info + .add_title(conv.id) + .add_key_value("Title", title) + .add_key_value("Updated", time_ago); + } + + let porcelain = Porcelain::from(&info) + .drop_col(3) + .truncate(1, 60) + .uppercase_headers(); + self.writeln(porcelain)?; + Ok(()) + } + async fn on_show_conversations(&mut self, porcelain: bool) -> anyhow::Result<()> { let max_conversations = self.config.max_conversations; let conversations = self.api.get_conversations(Some(max_conversations)).await?; From b1c177e1bcecda1d0eb6fc92174ded3a5e4eabae Mon Sep 17 00:00:00 2001 From: Tushar Date: Sun, 14 Jun 2026 15:40:03 +0530 Subject: [PATCH 06/12] refactor(cli): make conversation command field optional --- crates/forge_main/src/cli.rs | 37 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/crates/forge_main/src/cli.rs b/crates/forge_main/src/cli.rs index d77a9ccee0..b0032e9dfc 100644 --- a/crates/forge_main/src/cli.rs +++ b/crates/forge_main/src/cli.rs @@ -710,7 +710,6 @@ pub struct ConversationCommandGroup { #[command(subcommand)] pub command: Option, } - #[derive(Subcommand, Debug, Clone)] pub enum ConversationCommand { /// List conversation history. @@ -1088,7 +1087,7 @@ mod tests { let fixture = Cli::parse_from(["forge", "conversation", "list"]); let is_list = match fixture.subcommands { Some(TopLevelCommand::Conversation(conversation)) => { - matches!(conversation.command, ConversationCommand::List { .. }) + matches!(conversation.command, Some(ConversationCommand::List { .. })) } _ => false, }; @@ -1105,7 +1104,7 @@ mod tests { ]); let actual = match fixture.subcommands { Some(TopLevelCommand::Conversation(conversation)) => match conversation.command { - ConversationCommand::Tree { id } => Some(id), + Some(ConversationCommand::Tree { id }) => Some(id), _ => None, }, _ => None, @@ -1119,7 +1118,7 @@ mod tests { let fixture = Cli::parse_from(["forge", "session", "list"]); let is_list = match fixture.subcommands { Some(TopLevelCommand::Conversation(conversation)) => { - matches!(conversation.command, ConversationCommand::List { .. }) + matches!(conversation.command, Some(ConversationCommand::List { .. })) } _ => false, }; @@ -1161,7 +1160,7 @@ mod tests { ]); let (id, html) = match fixture.subcommands { Some(TopLevelCommand::Conversation(conversation)) => match conversation.command { - ConversationCommand::Dump { id, html } => (id, html), + Some(ConversationCommand::Dump { id, html }) => (id, html), _ => (ConversationId::default(), true), }, _ => (ConversationId::default(), true), @@ -1184,7 +1183,7 @@ mod tests { ]); let (id, html) = match fixture.subcommands { Some(TopLevelCommand::Conversation(conversation)) => match conversation.command { - ConversationCommand::Dump { id, html } => (id, html), + Some(ConversationCommand::Dump { id, html }) => (id, html), _ => (ConversationId::default(), false), }, _ => (ConversationId::default(), false), @@ -1206,7 +1205,7 @@ mod tests { ]); let id = match fixture.subcommands { Some(TopLevelCommand::Conversation(conversation)) => match conversation.command { - ConversationCommand::Retry { id } => id, + Some(ConversationCommand::Retry { id }) => id, _ => ConversationId::default(), }, _ => ConversationId::default(), @@ -1227,7 +1226,7 @@ mod tests { ]); let id = match fixture.subcommands { Some(TopLevelCommand::Conversation(conversation)) => match conversation.command { - ConversationCommand::Compact { id } => id, + Some(ConversationCommand::Compact { id }) => id, _ => ConversationId::default(), }, _ => ConversationId::default(), @@ -1248,7 +1247,7 @@ mod tests { ]); let (id, md) = match fixture.subcommands { Some(TopLevelCommand::Conversation(conversation)) => match conversation.command { - ConversationCommand::Show { id, md } => (id, md), + Some(ConversationCommand::Show { id, md }) => (id, md), _ => (ConversationId::default(), false), }, _ => (ConversationId::default(), false), @@ -1271,7 +1270,7 @@ mod tests { ]); let (id, md) = match fixture.subcommands { Some(TopLevelCommand::Conversation(conversation)) => match conversation.command { - ConversationCommand::Show { id, md } => (id, md), + Some(ConversationCommand::Show { id, md }) => (id, md), _ => (ConversationId::default(), false), }, _ => (ConversationId::default(), false), @@ -1293,7 +1292,7 @@ mod tests { ]); let id = match fixture.subcommands { Some(TopLevelCommand::Conversation(conversation)) => match conversation.command { - ConversationCommand::Resume { id } => id, + Some(ConversationCommand::Resume { id }) => id, _ => ConversationId::default(), }, _ => ConversationId::default(), @@ -1508,7 +1507,7 @@ mod tests { let fixture = Cli::parse_from(["forge", "conversation", "list", "--porcelain"]); let actual = match fixture.subcommands { Some(TopLevelCommand::Conversation(conversation)) => match conversation.command { - ConversationCommand::List { porcelain } => porcelain, + Some(ConversationCommand::List { porcelain }) => porcelain, _ => false, }, _ => false, @@ -1549,7 +1548,7 @@ mod tests { ]); let id = match fixture.subcommands { Some(TopLevelCommand::Conversation(conversation)) => match conversation.command { - ConversationCommand::Info { id } => id, + Some(ConversationCommand::Info { id }) => id, _ => ConversationId::default(), }, _ => ConversationId::default(), @@ -1571,7 +1570,7 @@ mod tests { ]); let (id, porcelain) = match fixture.subcommands { Some(TopLevelCommand::Conversation(conversation)) => match conversation.command { - ConversationCommand::Stats { id, porcelain } => (id, porcelain), + Some(ConversationCommand::Stats { id, porcelain }) => (id, porcelain), _ => (ConversationId::default(), false), }, _ => (ConversationId::default(), false), @@ -1603,7 +1602,7 @@ mod tests { ]); let (id, html) = match fixture.subcommands { Some(TopLevelCommand::Conversation(conversation)) => match conversation.command { - ConversationCommand::Dump { id, html } => (id, html), + Some(ConversationCommand::Dump { id, html }) => (id, html), _ => (ConversationId::default(), true), }, _ => (ConversationId::default(), true), @@ -1625,7 +1624,7 @@ mod tests { ]); let id = match fixture.subcommands { Some(TopLevelCommand::Conversation(conversation)) => match conversation.command { - ConversationCommand::Retry { id } => id, + Some(ConversationCommand::Retry { id }) => id, _ => ConversationId::default(), }, _ => ConversationId::default(), @@ -1672,7 +1671,7 @@ mod tests { ]); let id = match fixture.subcommands { Some(TopLevelCommand::Conversation(conversation)) => match conversation.command { - ConversationCommand::Clone { id, .. } => id, + Some(ConversationCommand::Clone { id, .. }) => id, _ => ConversationId::default(), }, _ => ConversationId::default(), @@ -1694,7 +1693,7 @@ mod tests { ]); let (id, porcelain) = match fixture.subcommands { Some(TopLevelCommand::Conversation(conversation)) => match conversation.command { - ConversationCommand::Clone { id, porcelain } => (id, porcelain), + Some(ConversationCommand::Clone { id, porcelain }) => (id, porcelain), _ => (ConversationId::default(), false), }, _ => (ConversationId::default(), false), @@ -1812,7 +1811,7 @@ mod tests { ]); let is_delete_with_id = match fixture.subcommands { Some(TopLevelCommand::Conversation(conversation)) => { - matches!(conversation.command, ConversationCommand::Delete { id: _ }) + matches!(conversation.command, Some(ConversationCommand::Delete { id: _ })) } _ => false, }; From 05e6d522616461bb5803a81842c153acf1d44681 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sun, 14 Jun 2026 10:12:15 +0000 Subject: [PATCH 07/12] [autofix.ci] apply automated fixes --- crates/forge_main/src/cli.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/forge_main/src/cli.rs b/crates/forge_main/src/cli.rs index b0032e9dfc..174be9d1c5 100644 --- a/crates/forge_main/src/cli.rs +++ b/crates/forge_main/src/cli.rs @@ -1811,7 +1811,10 @@ mod tests { ]); let is_delete_with_id = match fixture.subcommands { Some(TopLevelCommand::Conversation(conversation)) => { - matches!(conversation.command, Some(ConversationCommand::Delete { id: _ })) + matches!( + conversation.command, + Some(ConversationCommand::Delete { id: _ }) + ) } _ => false, }; From c8d19463987d994bc28961a7b52eeff575a337a5 Mon Sep 17 00:00:00 2001 From: Tushar Date: Sun, 14 Jun 2026 18:31:17 +0530 Subject: [PATCH 08/12] refactor(cli): move parent flag from conversation group to list conversation command --- crates/forge_main/src/cli.rs | 38 +---- crates/forge_main/src/ui.rs | 195 +++++++++++----------- shell-plugin/lib/actions/conversation.zsh | 3 +- 3 files changed, 102 insertions(+), 134 deletions(-) diff --git a/crates/forge_main/src/cli.rs b/crates/forge_main/src/cli.rs index 174be9d1c5..f854bfc144 100644 --- a/crates/forge_main/src/cli.rs +++ b/crates/forge_main/src/cli.rs @@ -454,7 +454,11 @@ pub enum ListCommand { /// List conversation history. #[command(alias = "session")] - Conversation, + Conversation { + /// Show child conversations of a parent conversation. + #[arg(long)] + parent: Option, + }, /// List custom commands. #[command(alias = "cmds")] @@ -703,10 +707,6 @@ pub enum ConfigGetField { /// Command group for conversation management. #[derive(Parser, Debug, Clone)] pub struct ConversationCommandGroup { - /// List child conversations of a parent conversation. - #[arg(long)] - pub parent: Option, - #[command(subcommand)] pub command: Option, } @@ -760,11 +760,6 @@ pub enum ConversationCommand { md: bool, }, - /// Show nested conversations spawned by a conversation. - Tree { - /// Conversation ID. - id: ConversationId, - }, /// Show conversation details. Info { @@ -1094,25 +1089,6 @@ mod tests { assert_eq!(is_list, true); } - #[test] - fn test_conversation_tree() { - let fixture = Cli::parse_from([ - "forge", - "conversation", - "tree", - "550e8400-e29b-41d4-a716-446655440004", - ]); - let actual = match fixture.subcommands { - Some(TopLevelCommand::Conversation(conversation)) => match conversation.command { - Some(ConversationCommand::Tree { id }) => Some(id), - _ => None, - }, - _ => None, - }; - let expected = Some(ConversationId::parse("550e8400-e29b-41d4-a716-446655440004").unwrap()); - assert_eq!(actual, expected); - } - #[test] fn test_session_alias_list() { let fixture = Cli::parse_from(["forge", "session", "list"]); @@ -1321,7 +1297,7 @@ mod tests { fn test_list_conversation_command() { let fixture = Cli::parse_from(["forge", "list", "conversation"]); let is_conversation_list = match fixture.subcommands { - Some(TopLevelCommand::List(list)) => matches!(list.command, ListCommand::Conversation), + Some(TopLevelCommand::List(list)) => matches!(list.command, ListCommand::Conversation { .. }), _ => false, }; assert_eq!(is_conversation_list, true); @@ -1331,7 +1307,7 @@ mod tests { fn test_list_session_alias_command() { let fixture = Cli::parse_from(["forge", "list", "session"]); let is_conversation_list = match fixture.subcommands { - Some(TopLevelCommand::List(list)) => matches!(list.command, ListCommand::Conversation), + Some(TopLevelCommand::List(list)) => matches!(list.command, ListCommand::Conversation { .. }), _ => false, }; assert_eq!(is_conversation_list, true); diff --git a/crates/forge_main/src/ui.rs b/crates/forge_main/src/ui.rs index f57eee794f..a3ed8a61ef 100644 --- a/crates/forge_main/src/ui.rs +++ b/crates/forge_main/src/ui.rs @@ -501,8 +501,56 @@ impl A + Send + Sync> UI ListCommand::Mcp => { self.on_show_mcp_servers(porcelain).await?; } - ListCommand::Conversation => { - self.on_show_conversations(porcelain).await?; + ListCommand::Conversation { parent } => { + if let Some(parent_id) = parent { + let parent_conv = + self.validate_conversation_exists(&parent_id).await?; + let children = + self.fetch_related_conversations(&parent_conv).await; + + if children.is_empty() { + self.writeln_title(TitleFormat::info( + "No child conversations found.", + ))?; + } else { + let mut info = Info::new(); + for conv in children.into_iter() { + let title = conv + .title + .as_deref() + .map(|t| t.to_string()) + .unwrap_or_else(|| markers::EMPTY.to_string()); + + let duration = chrono::Utc::now() + .signed_duration_since( + conv.metadata + .updated_at + .unwrap_or(conv.metadata.created_at), + ); + let duration = std::time::Duration::from_secs( + (duration.num_minutes() * 60).max(0) as u64, + ); + let time_ago = if duration.is_zero() { + "now".to_string() + } else { + format!("{} ago", humantime::format_duration(duration)) + }; + + info = info + .add_title(conv.id) + .add_key_value("Title", title) + .add_key_value("Updated", time_ago); + } + + let porcelain = Porcelain::from(&info) + .drop_col(3) + .truncate(1, 60) + .uppercase_headers(); + self.writeln(porcelain)?; + } + } else { + self.on_show_conversations(porcelain).await?; + } } ListCommand::Cmd => { self.on_show_custom_commands(porcelain).await?; @@ -863,13 +911,6 @@ impl A + Send + Sync> UI &mut self, conversation_group: crate::cli::ConversationCommandGroup, ) -> anyhow::Result<()> { - // When --parent is provided, list child conversations of that parent - if let Some(parent_id) = conversation_group.parent { - let parent = self.validate_conversation_exists(&parent_id).await?; - self.on_show_conversation_children(parent).await?; - return Ok(()); - } - match conversation_group.command { Some(ConversationCommand::List { porcelain }) => { self.on_show_conversations(porcelain).await?; @@ -930,11 +971,6 @@ impl A + Send + Sync> UI self.on_show_last_message(conversation, md).await?; } - Some(ConversationCommand::Tree { id }) => { - let conversation = self.validate_conversation_exists(&id).await?; - - self.on_show_conversation_tree(conversation).await?; - } Some(ConversationCommand::Info { id }) => { let conversation = self.validate_conversation_exists(&id).await?; @@ -2049,92 +2085,6 @@ impl A + Send + Sync> UI Ok(()) } - async fn on_show_conversation_tree(&mut self, root: Conversation) -> anyhow::Result<()> { - let mut lines = Vec::new(); - let mut stack = vec![(root, String::new(), true, true)]; - let mut visited = HashSet::new(); - - while let Some((conversation, prefix, is_last, is_root)) = stack.pop() { - if !visited.insert(conversation.id) { - continue; - } - - let title = conversation - .title - .as_deref() - .map(str::to_string) - .unwrap_or_else(|| markers::EMPTY.to_string()); - let connector = if is_root { - String::new() - } else if is_last { - "└── ".to_string() - } else { - "├── ".to_string() - }; - lines.push(format!("{prefix}{connector}{} {title}", conversation.id)); - - let child_prefix = if is_root { - String::new() - } else if is_last { - format!("{prefix} ") - } else { - format!("{prefix}│ ") - }; - - let children = self.fetch_related_conversations(&conversation).await; - let child_count = children.len(); - for (index, child) in children.into_iter().enumerate().rev() { - stack.push((child, child_prefix.clone(), index + 1 == child_count, false)); - } - } - - self.writeln(lines.join("\n"))?; - Ok(()) - } - - /// Displays child conversations of a parent as a flat list - async fn on_show_conversation_children(&mut self, parent: Conversation) -> anyhow::Result<()> { - let children = self.fetch_related_conversations(&parent).await; - - if children.is_empty() { - self.writeln_title(TitleFormat::info("No child conversations found."))?; - return Ok(()); - } - - let mut info = Info::new(); - - for conv in children.into_iter() { - let title = conv - .title - .as_deref() - .map(|t| t.to_string()) - .unwrap_or_else(|| markers::EMPTY.to_string()); - - let duration = chrono::Utc::now().signed_duration_since( - conv.metadata.updated_at.unwrap_or(conv.metadata.created_at), - ); - let duration = - std::time::Duration::from_secs((duration.num_minutes() * 60).max(0) as u64); - let time_ago = if duration.is_zero() { - "now".to_string() - } else { - format!("{} ago", humantime::format_duration(duration)) - }; - - info = info - .add_title(conv.id) - .add_key_value("Title", title) - .add_key_value("Updated", time_ago); - } - - let porcelain = Porcelain::from(&info) - .drop_col(3) - .truncate(1, 60) - .uppercase_headers(); - self.writeln(porcelain)?; - Ok(()) - } - async fn on_show_conversations(&mut self, porcelain: bool) -> anyhow::Result<()> { let max_conversations = self.config.max_conversations; let conversations = self.api.get_conversations(Some(max_conversations)).await?; @@ -2232,8 +2182,49 @@ impl A + Send + Sync> UI .state .conversation_id .ok_or_else(|| anyhow::anyhow!("No active conversation"))?; - let conversation = self.validate_conversation_exists(&conversation_id).await?; - self.on_show_conversation_tree(conversation).await?; + let parent = self.validate_conversation_exists(&conversation_id).await?; + let children = self.fetch_related_conversations(&parent).await; + + if children.is_empty() { + self.writeln_title(TitleFormat::info( + "No child conversations found.", + ))?; + } else { + let mut info = Info::new(); + for conv in children.into_iter() { + let title = conv + .title + .as_deref() + .map(|t| t.to_string()) + .unwrap_or_else(|| markers::EMPTY.to_string()); + + let duration = chrono::Utc::now() + .signed_duration_since( + conv.metadata + .updated_at + .unwrap_or(conv.metadata.created_at), + ); + let duration = std::time::Duration::from_secs( + (duration.num_minutes() * 60).max(0) as u64, + ); + let time_ago = if duration.is_zero() { + "now".to_string() + } else { + format!("{} ago", humantime::format_duration(duration)) + }; + + info = info + .add_title(conv.id) + .add_key_value("Title", title) + .add_key_value("Updated", time_ago); + } + + let porcelain = Porcelain::from(&info) + .drop_col(3) + .truncate(1, 60) + .uppercase_headers(); + self.writeln(porcelain)?; + } } AppCommand::Compact => { self.spinner.start(Some("Compacting"))?; diff --git a/shell-plugin/lib/actions/conversation.zsh b/shell-plugin/lib/actions/conversation.zsh index 05292d9418..eb05d2510c 100644 --- a/shell-plugin/lib/actions/conversation.zsh +++ b/shell-plugin/lib/actions/conversation.zsh @@ -116,9 +116,10 @@ function _forge_action_conversation() { fi } + # Action handler: Show nested conversations spawned by current conversation function _forge_action_conversation_tree() { - _forge_handle_conversation_command "tree" + _forge_exec list conversation --parent "$_FORGE_CONVERSATION_ID" } # Action handler: Clone conversation From 221bfe841c4e4c4c46978deee84c755188cd4464 Mon Sep 17 00:00:00 2001 From: Tushar Date: Sun, 14 Jun 2026 18:36:03 +0530 Subject: [PATCH 09/12] feat(conversation): add parent flag to conversation selector for child list view --- crates/forge_main/src/cli.rs | 4 ++ crates/forge_main/src/ui.rs | 66 ++++++++++------------- shell-plugin/lib/actions/conversation.zsh | 2 +- 3 files changed, 32 insertions(+), 40 deletions(-) diff --git a/crates/forge_main/src/cli.rs b/crates/forge_main/src/cli.rs index f854bfc144..71288737b7 100644 --- a/crates/forge_main/src/cli.rs +++ b/crates/forge_main/src/cli.rs @@ -237,6 +237,10 @@ pub enum SelectCommand { /// Initial query text pre-filled in the search box. #[arg(long, short = 'q')] query: Option, + + /// Show child conversations of a parent conversation. + #[arg(long)] + parent: Option, }, /// Select a file interactively with a preview pane. diff --git a/crates/forge_main/src/ui.rs b/crates/forge_main/src/ui.rs index a3ed8a61ef..095475c9ca 100644 --- a/crates/forge_main/src/ui.rs +++ b/crates/forge_main/src/ui.rs @@ -883,11 +883,18 @@ impl A + Send + Sync> UI self.select_row_output("Command", query.clone(), rows)?; } } - SelectCommand::Conversation { query } => { - let max_conversations = self.config.max_conversations; + SelectCommand::Conversation { query, parent } => { let conversations = - self.api.get_conversations(Some(max_conversations)).await?; - let conversations = Self::user_initiated_conversations(conversations); + if let Some(parent_id) = parent { + let parent_conv = + self.validate_conversation_exists(parent_id).await?; + self.fetch_related_conversations(&parent_conv).await + } else { + let max_conversations = self.config.max_conversations; + let conversations = + self.api.get_conversations(Some(max_conversations)).await?; + Self::user_initiated_conversations(conversations) + }; if !conversations.is_empty() && let Some(conversation) = ConversationSelector::select_conversation( @@ -2189,41 +2196,22 @@ impl A + Send + Sync> UI self.writeln_title(TitleFormat::info( "No child conversations found.", ))?; - } else { - let mut info = Info::new(); - for conv in children.into_iter() { - let title = conv - .title - .as_deref() - .map(|t| t.to_string()) - .unwrap_or_else(|| markers::EMPTY.to_string()); - - let duration = chrono::Utc::now() - .signed_duration_since( - conv.metadata - .updated_at - .unwrap_or(conv.metadata.created_at), - ); - let duration = std::time::Duration::from_secs( - (duration.num_minutes() * 60).max(0) as u64, - ); - let time_ago = if duration.is_zero() { - "now".to_string() - } else { - format!("{} ago", humantime::format_duration(duration)) - }; - - info = info - .add_title(conv.id) - .add_key_value("Title", title) - .add_key_value("Updated", time_ago); - } - - let porcelain = Porcelain::from(&info) - .drop_col(3) - .truncate(1, 60) - .uppercase_headers(); - self.writeln(porcelain)?; + } else if let Some(conversation) = + ConversationSelector::select_conversation( + &children, + self.state.conversation_id, + None, + ) + .await? + { + let conversation_id = conversation.id; + self.state.conversation_id = Some(conversation_id); + self.on_show_last_message(conversation, false).await?; + self.writeln_title(TitleFormat::info(format!( + "Switched to conversation {}", + conversation_id.into_string().bold() + )))?; + self.on_info(false, Some(conversation_id)).await?; } } AppCommand::Compact => { diff --git a/shell-plugin/lib/actions/conversation.zsh b/shell-plugin/lib/actions/conversation.zsh index eb05d2510c..132497cbfd 100644 --- a/shell-plugin/lib/actions/conversation.zsh +++ b/shell-plugin/lib/actions/conversation.zsh @@ -119,7 +119,7 @@ function _forge_action_conversation() { # Action handler: Show nested conversations spawned by current conversation function _forge_action_conversation_tree() { - _forge_exec list conversation --parent "$_FORGE_CONVERSATION_ID" + _forge_select conversation --parent "$_FORGE_CONVERSATION_ID" } # Action handler: Clone conversation From c3ed89fa73ef9636fd9139bebf721f866f5c4f50 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sun, 14 Jun 2026 13:08:27 +0000 Subject: [PATCH 10/12] [autofix.ci] apply automated fixes --- crates/forge_main/src/cli.rs | 9 ++++-- crates/forge_main/src/ui.rs | 54 +++++++++++++++--------------------- 2 files changed, 29 insertions(+), 34 deletions(-) diff --git a/crates/forge_main/src/cli.rs b/crates/forge_main/src/cli.rs index 71288737b7..37f632ef1c 100644 --- a/crates/forge_main/src/cli.rs +++ b/crates/forge_main/src/cli.rs @@ -764,7 +764,6 @@ pub enum ConversationCommand { md: bool, }, - /// Show conversation details. Info { /// Conversation ID. @@ -1301,7 +1300,9 @@ mod tests { fn test_list_conversation_command() { let fixture = Cli::parse_from(["forge", "list", "conversation"]); let is_conversation_list = match fixture.subcommands { - Some(TopLevelCommand::List(list)) => matches!(list.command, ListCommand::Conversation { .. }), + Some(TopLevelCommand::List(list)) => { + matches!(list.command, ListCommand::Conversation { .. }) + } _ => false, }; assert_eq!(is_conversation_list, true); @@ -1311,7 +1312,9 @@ mod tests { fn test_list_session_alias_command() { let fixture = Cli::parse_from(["forge", "list", "session"]); let is_conversation_list = match fixture.subcommands { - Some(TopLevelCommand::List(list)) => matches!(list.command, ListCommand::Conversation { .. }), + Some(TopLevelCommand::List(list)) => { + matches!(list.command, ListCommand::Conversation { .. }) + } _ => false, }; assert_eq!(is_conversation_list, true); diff --git a/crates/forge_main/src/ui.rs b/crates/forge_main/src/ui.rs index 095475c9ca..dbf856db01 100644 --- a/crates/forge_main/src/ui.rs +++ b/crates/forge_main/src/ui.rs @@ -503,10 +503,8 @@ impl A + Send + Sync> UI } ListCommand::Conversation { parent } => { if let Some(parent_id) = parent { - let parent_conv = - self.validate_conversation_exists(&parent_id).await?; - let children = - self.fetch_related_conversations(&parent_conv).await; + let parent_conv = self.validate_conversation_exists(&parent_id).await?; + let children = self.fetch_related_conversations(&parent_conv).await; if children.is_empty() { self.writeln_title(TitleFormat::info( @@ -521,12 +519,11 @@ impl A + Send + Sync> UI .map(|t| t.to_string()) .unwrap_or_else(|| markers::EMPTY.to_string()); - let duration = chrono::Utc::now() - .signed_duration_since( - conv.metadata - .updated_at - .unwrap_or(conv.metadata.created_at), - ); + let duration = chrono::Utc::now().signed_duration_since( + conv.metadata + .updated_at + .unwrap_or(conv.metadata.created_at), + ); let duration = std::time::Duration::from_secs( (duration.num_minutes() * 60).max(0) as u64, ); @@ -884,17 +881,15 @@ impl A + Send + Sync> UI } } SelectCommand::Conversation { query, parent } => { - let conversations = - if let Some(parent_id) = parent { - let parent_conv = - self.validate_conversation_exists(parent_id).await?; - self.fetch_related_conversations(&parent_conv).await - } else { - let max_conversations = self.config.max_conversations; - let conversations = - self.api.get_conversations(Some(max_conversations)).await?; - Self::user_initiated_conversations(conversations) - }; + let conversations = if let Some(parent_id) = parent { + let parent_conv = self.validate_conversation_exists(parent_id).await?; + self.fetch_related_conversations(&parent_conv).await + } else { + let max_conversations = self.config.max_conversations; + let conversations = + self.api.get_conversations(Some(max_conversations)).await?; + Self::user_initiated_conversations(conversations) + }; if !conversations.is_empty() && let Some(conversation) = ConversationSelector::select_conversation( @@ -2193,16 +2188,13 @@ impl A + Send + Sync> UI let children = self.fetch_related_conversations(&parent).await; if children.is_empty() { - self.writeln_title(TitleFormat::info( - "No child conversations found.", - ))?; - } else if let Some(conversation) = - ConversationSelector::select_conversation( - &children, - self.state.conversation_id, - None, - ) - .await? + self.writeln_title(TitleFormat::info("No child conversations found."))?; + } else if let Some(conversation) = ConversationSelector::select_conversation( + &children, + self.state.conversation_id, + None, + ) + .await? { let conversation_id = conversation.id; self.state.conversation_id = Some(conversation_id); From 08a246a727626a4ab9b7e65497f7ed2e2c30f3e9 Mon Sep 17 00:00:00 2001 From: Tushar Date: Sun, 14 Jun 2026 18:41:30 +0530 Subject: [PATCH 11/12] refactor(cli): make conversation command non-optional in command group --- crates/forge_main/src/cli.rs | 36 ++++++++++++++++++------------------ crates/forge_main/src/ui.rs | 28 ++++++++++++---------------- 2 files changed, 30 insertions(+), 34 deletions(-) diff --git a/crates/forge_main/src/cli.rs b/crates/forge_main/src/cli.rs index 37f632ef1c..43f2b8af49 100644 --- a/crates/forge_main/src/cli.rs +++ b/crates/forge_main/src/cli.rs @@ -712,7 +712,7 @@ pub enum ConfigGetField { #[derive(Parser, Debug, Clone)] pub struct ConversationCommandGroup { #[command(subcommand)] - pub command: Option, + pub command: ConversationCommand, } #[derive(Subcommand, Debug, Clone)] pub enum ConversationCommand { @@ -1085,7 +1085,7 @@ mod tests { let fixture = Cli::parse_from(["forge", "conversation", "list"]); let is_list = match fixture.subcommands { Some(TopLevelCommand::Conversation(conversation)) => { - matches!(conversation.command, Some(ConversationCommand::List { .. })) + matches!(conversation.command, ConversationCommand::List { .. }) } _ => false, }; @@ -1097,7 +1097,7 @@ mod tests { let fixture = Cli::parse_from(["forge", "session", "list"]); let is_list = match fixture.subcommands { Some(TopLevelCommand::Conversation(conversation)) => { - matches!(conversation.command, Some(ConversationCommand::List { .. })) + matches!(conversation.command, ConversationCommand::List { .. }) } _ => false, }; @@ -1139,7 +1139,7 @@ mod tests { ]); let (id, html) = match fixture.subcommands { Some(TopLevelCommand::Conversation(conversation)) => match conversation.command { - Some(ConversationCommand::Dump { id, html }) => (id, html), + ConversationCommand::Dump { id, html } => (id, html), _ => (ConversationId::default(), true), }, _ => (ConversationId::default(), true), @@ -1162,7 +1162,7 @@ mod tests { ]); let (id, html) = match fixture.subcommands { Some(TopLevelCommand::Conversation(conversation)) => match conversation.command { - Some(ConversationCommand::Dump { id, html }) => (id, html), + ConversationCommand::Dump { id, html } => (id, html), _ => (ConversationId::default(), false), }, _ => (ConversationId::default(), false), @@ -1184,7 +1184,7 @@ mod tests { ]); let id = match fixture.subcommands { Some(TopLevelCommand::Conversation(conversation)) => match conversation.command { - Some(ConversationCommand::Retry { id }) => id, + ConversationCommand::Retry { id } => id, _ => ConversationId::default(), }, _ => ConversationId::default(), @@ -1205,7 +1205,7 @@ mod tests { ]); let id = match fixture.subcommands { Some(TopLevelCommand::Conversation(conversation)) => match conversation.command { - Some(ConversationCommand::Compact { id }) => id, + ConversationCommand::Compact { id } => id, _ => ConversationId::default(), }, _ => ConversationId::default(), @@ -1226,7 +1226,7 @@ mod tests { ]); let (id, md) = match fixture.subcommands { Some(TopLevelCommand::Conversation(conversation)) => match conversation.command { - Some(ConversationCommand::Show { id, md }) => (id, md), + ConversationCommand::Show { id, md } => (id, md), _ => (ConversationId::default(), false), }, _ => (ConversationId::default(), false), @@ -1249,7 +1249,7 @@ mod tests { ]); let (id, md) = match fixture.subcommands { Some(TopLevelCommand::Conversation(conversation)) => match conversation.command { - Some(ConversationCommand::Show { id, md }) => (id, md), + ConversationCommand::Show { id, md } => (id, md), _ => (ConversationId::default(), false), }, _ => (ConversationId::default(), false), @@ -1271,7 +1271,7 @@ mod tests { ]); let id = match fixture.subcommands { Some(TopLevelCommand::Conversation(conversation)) => match conversation.command { - Some(ConversationCommand::Resume { id }) => id, + ConversationCommand::Resume { id } => id, _ => ConversationId::default(), }, _ => ConversationId::default(), @@ -1490,7 +1490,7 @@ mod tests { let fixture = Cli::parse_from(["forge", "conversation", "list", "--porcelain"]); let actual = match fixture.subcommands { Some(TopLevelCommand::Conversation(conversation)) => match conversation.command { - Some(ConversationCommand::List { porcelain }) => porcelain, + ConversationCommand::List { porcelain } => porcelain, _ => false, }, _ => false, @@ -1531,7 +1531,7 @@ mod tests { ]); let id = match fixture.subcommands { Some(TopLevelCommand::Conversation(conversation)) => match conversation.command { - Some(ConversationCommand::Info { id }) => id, + ConversationCommand::Info { id } => id, _ => ConversationId::default(), }, _ => ConversationId::default(), @@ -1553,7 +1553,7 @@ mod tests { ]); let (id, porcelain) = match fixture.subcommands { Some(TopLevelCommand::Conversation(conversation)) => match conversation.command { - Some(ConversationCommand::Stats { id, porcelain }) => (id, porcelain), + ConversationCommand::Stats { id, porcelain } => (id, porcelain), _ => (ConversationId::default(), false), }, _ => (ConversationId::default(), false), @@ -1585,7 +1585,7 @@ mod tests { ]); let (id, html) = match fixture.subcommands { Some(TopLevelCommand::Conversation(conversation)) => match conversation.command { - Some(ConversationCommand::Dump { id, html }) => (id, html), + ConversationCommand::Dump { id, html } => (id, html), _ => (ConversationId::default(), true), }, _ => (ConversationId::default(), true), @@ -1607,7 +1607,7 @@ mod tests { ]); let id = match fixture.subcommands { Some(TopLevelCommand::Conversation(conversation)) => match conversation.command { - Some(ConversationCommand::Retry { id }) => id, + ConversationCommand::Retry { id } => id, _ => ConversationId::default(), }, _ => ConversationId::default(), @@ -1654,7 +1654,7 @@ mod tests { ]); let id = match fixture.subcommands { Some(TopLevelCommand::Conversation(conversation)) => match conversation.command { - Some(ConversationCommand::Clone { id, .. }) => id, + ConversationCommand::Clone { id, .. } => id, _ => ConversationId::default(), }, _ => ConversationId::default(), @@ -1676,7 +1676,7 @@ mod tests { ]); let (id, porcelain) = match fixture.subcommands { Some(TopLevelCommand::Conversation(conversation)) => match conversation.command { - Some(ConversationCommand::Clone { id, porcelain }) => (id, porcelain), + ConversationCommand::Clone { id, porcelain } => (id, porcelain), _ => (ConversationId::default(), false), }, _ => (ConversationId::default(), false), @@ -1796,7 +1796,7 @@ mod tests { Some(TopLevelCommand::Conversation(conversation)) => { matches!( conversation.command, - Some(ConversationCommand::Delete { id: _ }) + ConversationCommand::Delete { id: _ } ) } _ => false, diff --git a/crates/forge_main/src/ui.rs b/crates/forge_main/src/ui.rs index dbf856db01..a517907b24 100644 --- a/crates/forge_main/src/ui.rs +++ b/crates/forge_main/src/ui.rs @@ -914,13 +914,13 @@ impl A + Send + Sync> UI conversation_group: crate::cli::ConversationCommandGroup, ) -> anyhow::Result<()> { match conversation_group.command { - Some(ConversationCommand::List { porcelain }) => { + ConversationCommand::List { porcelain } => { self.on_show_conversations(porcelain).await?; } - Some(ConversationCommand::New) => { + ConversationCommand::New => { self.handle_generate_conversation_id().await?; } - Some(ConversationCommand::Dump { id, html }) => { + ConversationCommand::Dump { id, html } => { self.validate_conversation_exists(&id).await?; let original_id = self.state.conversation_id; @@ -931,7 +931,7 @@ impl A + Send + Sync> UI self.state.conversation_id = original_id; } - Some(ConversationCommand::Compact { id }) => { + ConversationCommand::Compact { id } => { self.validate_conversation_exists(&id).await?; let original_id = self.state.conversation_id; @@ -942,7 +942,7 @@ impl A + Send + Sync> UI self.state.conversation_id = original_id; } - Some(ConversationCommand::Delete { id }) => { + ConversationCommand::Delete { id } => { let conversation_id = ConversationId::parse(&id).context(format!("Invalid conversation ID: {id}"))?; @@ -950,7 +950,7 @@ impl A + Send + Sync> UI self.on_conversation_delete(conversation_id).await?; } - Some(ConversationCommand::Retry { id }) => { + ConversationCommand::Retry { id } => { self.validate_conversation_exists(&id).await?; let original_id = self.state.conversation_id; @@ -961,36 +961,36 @@ impl A + Send + Sync> UI self.state.conversation_id = original_id; } - Some(ConversationCommand::Resume { id }) => { + ConversationCommand::Resume { id } => { self.validate_conversation_exists(&id).await?; self.state.conversation_id = Some(id); self.writeln_title(TitleFormat::info(format!("Resumed conversation: {id}")))?; // Interactive mode will be handled by the main loop } - Some(ConversationCommand::Show { id, md }) => { + ConversationCommand::Show { id, md } => { let conversation = self.validate_conversation_exists(&id).await?; self.on_show_last_message(conversation, md).await?; } - Some(ConversationCommand::Info { id }) => { + ConversationCommand::Info { id } => { let conversation = self.validate_conversation_exists(&id).await?; self.on_show_conv_info(conversation).await?; } - Some(ConversationCommand::Stats { id, porcelain }) => { + ConversationCommand::Stats { id, porcelain } => { let conversation = self.validate_conversation_exists(&id).await?; self.on_show_conv_stats(conversation, porcelain).await?; } - Some(ConversationCommand::Clone { id, porcelain }) => { + ConversationCommand::Clone { id, porcelain } => { let conversation = self.validate_conversation_exists(&id).await?; self.spinner.start(Some("Cloning"))?; self.on_clone_conversation(conversation, porcelain).await?; self.spinner.stop(None)?; } - Some(ConversationCommand::Rename { id, name }) => { + ConversationCommand::Rename { id, name } => { self.validate_conversation_exists(&id).await?; let name = name.trim().to_string(); @@ -1005,11 +1005,7 @@ impl A + Send + Sync> UI name.bold() )))?; } - None => { - self.on_show_conversations(false).await?; - } } - Ok(()) } From 1ae93a1b79739974d6cc849b711a0cde89a5ca08 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sun, 14 Jun 2026 13:15:52 +0000 Subject: [PATCH 12/12] [autofix.ci] apply automated fixes --- crates/forge_main/src/cli.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/crates/forge_main/src/cli.rs b/crates/forge_main/src/cli.rs index 43f2b8af49..1068f77590 100644 --- a/crates/forge_main/src/cli.rs +++ b/crates/forge_main/src/cli.rs @@ -1794,10 +1794,7 @@ mod tests { ]); let is_delete_with_id = match fixture.subcommands { Some(TopLevelCommand::Conversation(conversation)) => { - matches!( - conversation.command, - ConversationCommand::Delete { id: _ } - ) + matches!(conversation.command, ConversationCommand::Delete { id: _ }) } _ => false, };