@@ -126,6 +126,19 @@ pub struct ConversationState {
126126 pub file_line_tracker : HashMap < String , FileLineTracker > ,
127127 #[ serde( default = "default_true" ) ]
128128 pub mcp_enabled : bool ,
129+ /// Tangent mode checkpoint - stores main conversation when in tangent mode
130+ #[ serde( default , skip_serializing_if = "Option::is_none" ) ]
131+ tangent_state : Option < ConversationCheckpoint > ,
132+ }
133+
134+ #[ derive( Debug , Clone , Serialize , Deserialize ) ]
135+ struct ConversationCheckpoint {
136+ /// Main conversation history stored while in tangent mode
137+ main_history : VecDeque < HistoryEntry > ,
138+ /// Main conversation next message
139+ main_next_message : Option < UserMessage > ,
140+ /// Main conversation transcript
141+ main_transcript : VecDeque < String > ,
129142}
130143
131144impl ConversationState {
@@ -184,6 +197,7 @@ impl ConversationState {
184197 model_info : model,
185198 file_line_tracker : HashMap :: new ( ) ,
186199 mcp_enabled,
200+ tangent_state : None ,
187201 }
188202 }
189203
@@ -204,6 +218,42 @@ impl ConversationState {
204218 }
205219 }
206220
221+ /// Check if currently in tangent mode
222+ pub fn is_in_tangent_mode ( & self ) -> bool {
223+ self . tangent_state . is_some ( )
224+ }
225+
226+ /// Create a checkpoint of current conversation state
227+ fn create_checkpoint ( & self ) -> ConversationCheckpoint {
228+ ConversationCheckpoint {
229+ main_history : self . history . clone ( ) ,
230+ main_next_message : self . next_message . clone ( ) ,
231+ main_transcript : self . transcript . clone ( ) ,
232+ }
233+ }
234+
235+ /// Restore conversation state from checkpoint
236+ fn restore_from_checkpoint ( & mut self , checkpoint : ConversationCheckpoint ) {
237+ self . history = checkpoint. main_history ;
238+ self . next_message = checkpoint. main_next_message ;
239+ self . transcript = checkpoint. main_transcript ;
240+ self . valid_history_range = ( 0 , self . history . len ( ) ) ;
241+ }
242+
243+ /// Enter tangent mode - creates checkpoint of current state
244+ pub fn enter_tangent_mode ( & mut self ) {
245+ if self . tangent_state . is_none ( ) {
246+ self . tangent_state = Some ( self . create_checkpoint ( ) ) ;
247+ }
248+ }
249+
250+ /// Exit tangent mode - restore from checkpoint
251+ pub fn exit_tangent_mode ( & mut self ) {
252+ if let Some ( checkpoint) = self . tangent_state . take ( ) {
253+ self . restore_from_checkpoint ( checkpoint) ;
254+ }
255+ }
256+
207257 /// Appends a collection prompts into history and returns the last message in the collection.
208258 /// It asserts that the collection ends with a prompt that assumes the role of user.
209259 pub fn append_prompts ( & mut self , mut prompts : VecDeque < Prompt > ) -> Option < String > {
@@ -1289,4 +1339,65 @@ mod tests {
12891339 conversation. set_next_user_message ( i. to_string ( ) ) . await ;
12901340 }
12911341 }
1342+
1343+ #[ tokio:: test]
1344+ async fn test_tangent_mode ( ) {
1345+ let mut os = Os :: new ( ) . await . unwrap ( ) ;
1346+ let agents = Agents :: default ( ) ;
1347+ let mut tool_manager = ToolManager :: default ( ) ;
1348+ let mut conversation = ConversationState :: new (
1349+ "fake_conv_id" ,
1350+ agents,
1351+ tool_manager. load_tools ( & mut os, & mut vec ! [ ] ) . await . unwrap ( ) ,
1352+ tool_manager,
1353+ None ,
1354+ & os,
1355+ false , // mcp_enabled
1356+ )
1357+ . await ;
1358+
1359+ // Initially not in tangent mode
1360+ assert ! ( !conversation. is_in_tangent_mode( ) ) ;
1361+
1362+ // Add some main conversation history
1363+ conversation. set_next_user_message ( "main conversation" . to_string ( ) ) . await ;
1364+ conversation. push_assistant_message ( & mut os, AssistantMessage :: new_response ( None , "main response" . to_string ( ) ) , None ) ;
1365+ conversation. transcript . push_back ( "main transcript" . to_string ( ) ) ;
1366+
1367+ let main_history_len = conversation. history . len ( ) ;
1368+ let main_transcript_len = conversation. transcript . len ( ) ;
1369+
1370+ // Enter tangent mode (toggle from normal to tangent)
1371+ conversation. enter_tangent_mode ( ) ;
1372+ assert ! ( conversation. is_in_tangent_mode( ) ) ;
1373+
1374+ // History should be preserved for tangent (not cleared)
1375+ assert_eq ! ( conversation. history. len( ) , main_history_len) ;
1376+ assert_eq ! ( conversation. transcript. len( ) , main_transcript_len) ;
1377+ assert ! ( conversation. next_message. is_none( ) ) ;
1378+
1379+ // Add tangent conversation
1380+ conversation. set_next_user_message ( "tangent conversation" . to_string ( ) ) . await ;
1381+ conversation. push_assistant_message ( & mut os, AssistantMessage :: new_response ( None , "tangent response" . to_string ( ) ) , None ) ;
1382+
1383+ // During tangent mode, history should have grown
1384+ assert_eq ! ( conversation. history. len( ) , main_history_len + 1 ) ;
1385+ assert_eq ! ( conversation. transcript. len( ) , main_transcript_len + 1 ) ;
1386+
1387+ // Exit tangent mode (toggle from tangent to normal)
1388+ conversation. exit_tangent_mode ( ) ;
1389+ assert ! ( !conversation. is_in_tangent_mode( ) ) ;
1390+
1391+ // Main conversation should be restored (tangent additions discarded)
1392+ assert_eq ! ( conversation. history. len( ) , main_history_len) ; // Back to original length
1393+ assert_eq ! ( conversation. transcript. len( ) , main_transcript_len) ; // Back to original length
1394+ assert ! ( conversation. transcript. contains( & "main transcript" . to_string( ) ) ) ;
1395+ assert ! ( !conversation. transcript. iter( ) . any( |t| t. contains( "tangent" ) ) ) ;
1396+
1397+ // Test multiple toggles
1398+ conversation. enter_tangent_mode ( ) ;
1399+ assert ! ( conversation. is_in_tangent_mode( ) ) ;
1400+ conversation. exit_tangent_mode ( ) ;
1401+ assert ! ( !conversation. is_in_tangent_mode( ) ) ;
1402+ }
12921403}
0 commit comments