1- use std:: collections:: HashMap ;
1+ use std:: collections:: { HashMap , HashSet } ;
22use std:: path:: PathBuf ;
33use std:: str:: FromStr ;
44use std:: sync:: Arc ;
@@ -501,8 +501,53 @@ impl<A: API + ConsoleWriter + 'static, F: Fn(ForgeConfig) -> A + Send + Sync> UI
501501 ListCommand :: Mcp => {
502502 self . on_show_mcp_servers ( porcelain) . await ?;
503503 }
504- ListCommand :: Conversation => {
505- self . on_show_conversations ( porcelain) . await ?;
504+ ListCommand :: Conversation { parent } => {
505+ if let Some ( parent_id) = parent {
506+ let parent_conv = self . validate_conversation_exists ( & parent_id) . await ?;
507+ let children = self . fetch_related_conversations ( & parent_conv) . await ;
508+
509+ if children. is_empty ( ) {
510+ self . writeln_title ( TitleFormat :: info (
511+ "No child conversations found." ,
512+ ) ) ?;
513+ } else {
514+ let mut info = Info :: new ( ) ;
515+ for conv in children. into_iter ( ) {
516+ let title = conv
517+ . title
518+ . as_deref ( )
519+ . map ( |t| t. to_string ( ) )
520+ . unwrap_or_else ( || markers:: EMPTY . to_string ( ) ) ;
521+
522+ let duration = chrono:: Utc :: now ( ) . signed_duration_since (
523+ conv. metadata
524+ . updated_at
525+ . unwrap_or ( conv. metadata . created_at ) ,
526+ ) ;
527+ let duration = std:: time:: Duration :: from_secs (
528+ ( duration. num_minutes ( ) * 60 ) . max ( 0 ) as u64 ,
529+ ) ;
530+ let time_ago = if duration. is_zero ( ) {
531+ "now" . to_string ( )
532+ } else {
533+ format ! ( "{} ago" , humantime:: format_duration( duration) )
534+ } ;
535+
536+ info = info
537+ . add_title ( conv. id )
538+ . add_key_value ( "Title" , title)
539+ . add_key_value ( "Updated" , time_ago) ;
540+ }
541+
542+ let porcelain = Porcelain :: from ( & info)
543+ . drop_col ( 3 )
544+ . truncate ( 1 , 60 )
545+ . uppercase_headers ( ) ;
546+ self . writeln ( porcelain) ?;
547+ }
548+ } else {
549+ self . on_show_conversations ( porcelain) . await ?;
550+ }
506551 }
507552 ListCommand :: Cmd => {
508553 self . on_show_custom_commands ( porcelain) . await ?;
@@ -835,10 +880,16 @@ impl<A: API + ConsoleWriter + 'static, F: Fn(ForgeConfig) -> A + Send + Sync> UI
835880 self . select_row_output ( "Command" , query. clone ( ) , rows) ?;
836881 }
837882 }
838- SelectCommand :: Conversation { query } => {
839- let max_conversations = self . config . max_conversations ;
840- let conversations =
841- self . api . get_conversations ( Some ( max_conversations) ) . await ?;
883+ SelectCommand :: Conversation { query, parent } => {
884+ let conversations = if let Some ( parent_id) = parent {
885+ let parent_conv = self . validate_conversation_exists ( parent_id) . await ?;
886+ self . fetch_related_conversations ( & parent_conv) . await
887+ } else {
888+ let max_conversations = self . config . max_conversations ;
889+ let conversations =
890+ self . api . get_conversations ( Some ( max_conversations) ) . await ?;
891+ Self :: user_initiated_conversations ( conversations)
892+ } ;
842893
843894 if !conversations. is_empty ( )
844895 && let Some ( conversation) = ConversationSelector :: select_conversation (
@@ -955,7 +1006,6 @@ impl<A: API + ConsoleWriter + 'static, F: Fn(ForgeConfig) -> A + Send + Sync> UI
9551006 ) ) ) ?;
9561007 }
9571008 }
958-
9591009 Ok ( ( ) )
9601010 }
9611011
@@ -1998,6 +2048,7 @@ impl<A: API + ConsoleWriter + 'static, F: Fn(ForgeConfig) -> A + Send + Sync> UI
19982048 self . spinner . start ( Some ( "Loading Conversations" ) ) ?;
19992049 let max_conversations = self . config . max_conversations ;
20002050 let conversations = self . api . get_conversations ( Some ( max_conversations) ) . await ?;
2051+ let conversations = Self :: user_initiated_conversations ( conversations) ;
20012052 self . spinner . stop ( None ) ?;
20022053
20032054 if conversations. is_empty ( ) {
@@ -2035,6 +2086,7 @@ impl<A: API + ConsoleWriter + 'static, F: Fn(ForgeConfig) -> A + Send + Sync> UI
20352086 async fn on_show_conversations ( & mut self , porcelain : bool ) -> anyhow:: Result < ( ) > {
20362087 let max_conversations = self . config . max_conversations ;
20372088 let conversations = self . api . get_conversations ( Some ( max_conversations) ) . await ?;
2089+ let conversations = Self :: user_initiated_conversations ( conversations) ;
20382090
20392091 if conversations. is_empty ( ) {
20402092 return Ok ( ( ) ) ;
@@ -2086,6 +2138,25 @@ impl<A: API + ConsoleWriter + 'static, F: Fn(ForgeConfig) -> A + Send + Sync> UI
20862138 Ok ( ( ) )
20872139 }
20882140
2141+ fn user_initiated_conversations ( conversations : Vec < Conversation > ) -> Vec < Conversation > {
2142+ let related_ids: HashSet < ConversationId > = conversations
2143+ . iter ( )
2144+ . flat_map ( Conversation :: related_conversation_ids)
2145+ . collect ( ) ;
2146+
2147+ conversations
2148+ . into_iter ( )
2149+ . filter ( |conversation| {
2150+ conversation
2151+ . context
2152+ . as_ref ( )
2153+ . and_then ( |context| context. initiator . as_deref ( ) )
2154+ . is_none_or ( |initiator| initiator == "user" )
2155+ && !related_ids. contains ( & conversation. id )
2156+ } )
2157+ . collect ( )
2158+ }
2159+
20892160 async fn on_command ( & mut self , command : AppCommand ) -> anyhow:: Result < bool > {
20902161 match command {
20912162 AppCommand :: Conversations { id } => {
@@ -2104,6 +2175,33 @@ impl<A: API + ConsoleWriter + 'static, F: Fn(ForgeConfig) -> A + Send + Sync> UI
21042175 self . list_conversations ( ) . await ?;
21052176 }
21062177 }
2178+ AppCommand :: ConversationTree => {
2179+ let conversation_id = self
2180+ . state
2181+ . conversation_id
2182+ . ok_or_else ( || anyhow:: anyhow!( "No active conversation" ) ) ?;
2183+ let parent = self . validate_conversation_exists ( & conversation_id) . await ?;
2184+ let children = self . fetch_related_conversations ( & parent) . await ;
2185+
2186+ if children. is_empty ( ) {
2187+ self . writeln_title ( TitleFormat :: info ( "No child conversations found." ) ) ?;
2188+ } else if let Some ( conversation) = ConversationSelector :: select_conversation (
2189+ & children,
2190+ self . state . conversation_id ,
2191+ None ,
2192+ )
2193+ . await ?
2194+ {
2195+ let conversation_id = conversation. id ;
2196+ self . state . conversation_id = Some ( conversation_id) ;
2197+ self . on_show_last_message ( conversation, false ) . await ?;
2198+ self . writeln_title ( TitleFormat :: info ( format ! (
2199+ "Switched to conversation {}" ,
2200+ conversation_id. into_string( ) . bold( )
2201+ ) ) ) ?;
2202+ self . on_info ( false , Some ( conversation_id) ) . await ?;
2203+ }
2204+ }
21072205 AppCommand :: Compact => {
21082206 self . spinner . start ( Some ( "Compacting" ) ) ?;
21092207 self . on_compaction ( ) . await ?;
0 commit comments