@@ -7,6 +7,7 @@ use std::io::Read;
77use std:: path:: Path ;
88use uuid:: Uuid ;
99
10+ use crate :: models:: message:: MessageType ;
1011use crate :: models:: { ChatSession , Message , MessageRole , ToolResult , ToolUse } ;
1112use crate :: models:: { Provider , SessionState } ;
1213use crate :: parsers:: project_inference:: ProjectInference ;
@@ -279,15 +280,19 @@ impl GeminiCLIParser {
279280 // Convert messages
280281 let mut converted_messages = Vec :: new ( ) ;
281282 let mut total_tokens = 0u32 ;
283+ let mut sequence = 1usize ;
282284
283- for ( index, session_message) in messages. iter ( ) . enumerate ( ) {
284- let message = self . convert_session_message ( session_message, session_id, index + 1 ) ?;
285+ for session_message in messages. iter ( ) {
286+ let new_messages =
287+ self . convert_session_message ( session_message, session_id, sequence) ?;
285288
286- if let Some ( token_count) = message. token_count {
287- total_tokens += token_count;
289+ for message in new_messages {
290+ if let Some ( token_count) = message. token_count {
291+ total_tokens += token_count;
292+ }
293+ sequence += 1 ;
294+ converted_messages. push ( message) ;
288295 }
289-
290- converted_messages. push ( message) ;
291296 }
292297
293298 chat_session. message_count = converted_messages. len ( ) as u32 ;
@@ -342,15 +347,19 @@ impl GeminiCLIParser {
342347
343348 let mut messages = Vec :: new ( ) ;
344349 let mut total_tokens = 0u32 ;
350+ let mut sequence = 1usize ;
345351
346- for ( index, session_message) in session. messages . iter ( ) . enumerate ( ) {
347- let message = self . convert_session_message ( session_message, session_id, index + 1 ) ?;
352+ for session_message in session. messages . iter ( ) {
353+ let new_messages =
354+ self . convert_session_message ( session_message, session_id, sequence) ?;
348355
349- if let Some ( token_count) = message. token_count {
350- total_tokens += token_count;
356+ for message in new_messages {
357+ if let Some ( token_count) = message. token_count {
358+ total_tokens += token_count;
359+ }
360+ sequence += 1 ;
361+ messages. push ( message) ;
351362 }
352-
353- messages. push ( message) ;
354363 }
355364
356365 chat_session. message_count = messages. len ( ) as u32 ;
@@ -555,12 +564,14 @@ impl GeminiCLIParser {
555564 ( tool_uses, tool_results)
556565 }
557566
567+ /// Convert a Gemini session message to one or more Messages.
568+ /// Returns multiple messages when thoughts are present (thinking messages + main message).
558569 fn convert_session_message (
559570 & self ,
560571 session_message : & GeminiSessionMessage ,
561572 session_id : Uuid ,
562- sequence : usize ,
563- ) -> Result < Message > {
573+ start_sequence : usize ,
574+ ) -> Result < Vec < Message > > {
564575 let role = match session_message. message_type . as_str ( ) {
565576 "user" => MessageRole :: User ,
566577 "gemini" => MessageRole :: Assistant ,
@@ -579,7 +590,49 @@ impl GeminiCLIParser {
579590
580591 let timestamp = self . parse_timestamp ( & session_message. timestamp ) ?;
581592
582- // Generate a deterministic UUID for the message
593+ let mut messages = Vec :: new ( ) ;
594+ let mut current_sequence = start_sequence;
595+
596+ // Create separate thinking messages for each thought (only for assistant messages)
597+ if role == MessageRole :: Assistant {
598+ if let Some ( thoughts) = & session_message. thoughts {
599+ for ( idx, thought) in thoughts. iter ( ) . enumerate ( ) {
600+ let thinking_content =
601+ format ! ( "**{}**\n \n {}" , thought. subject, thought. description) ;
602+ let thinking_timestamp = self
603+ . parse_timestamp ( & thought. timestamp )
604+ . unwrap_or ( timestamp) ;
605+
606+ // Generate deterministic UUID for thinking message using index to avoid collision
607+ let thinking_id = self . generate_uuid_from_string ( & format ! (
608+ "{session_id}-thought-{}-{}" ,
609+ session_message. id, idx
610+ ) ) ;
611+
612+ let mut thinking_message = Message :: new (
613+ session_id,
614+ MessageRole :: Assistant ,
615+ thinking_content,
616+ thinking_timestamp,
617+ current_sequence as u32 ,
618+ )
619+ . with_message_type ( MessageType :: Thinking ) ;
620+
621+ thinking_message. id = thinking_id;
622+
623+ // Token estimation for thinking content
624+ let thinking_tokens = ( thinking_message. content . len ( ) / 4 ) as u32 ;
625+ if thinking_tokens > 0 {
626+ thinking_message = thinking_message. with_token_count ( thinking_tokens) ;
627+ }
628+
629+ messages. push ( thinking_message) ;
630+ current_sequence += 1 ;
631+ }
632+ }
633+ }
634+
635+ // Generate a deterministic UUID for the main message
583636 let message_id = if let Ok ( uuid) = Uuid :: parse_str ( & session_message. id ) {
584637 uuid
585638 } else {
@@ -591,7 +644,7 @@ impl GeminiCLIParser {
591644 role,
592645 session_message. content . clone ( ) ,
593646 timestamp,
594- sequence as u32 ,
647+ current_sequence as u32 ,
595648 ) ;
596649
597650 message. id = message_id;
@@ -608,13 +661,15 @@ impl GeminiCLIParser {
608661 message = message. with_token_count ( tokens. total ) ;
609662 } else {
610663 // Estimate token count based on content length
611- let estimated_tokens = ( message. content . len ( ) / 4 ) as u32 ; // Rough estimate: 4 chars per token
664+ let estimated_tokens = ( message. content . len ( ) / 4 ) as u32 ;
612665 if estimated_tokens > 0 {
613666 message = message. with_token_count ( estimated_tokens) ;
614667 }
615668 }
616669
617- Ok ( message)
670+ messages. push ( message) ;
671+
672+ Ok ( messages)
618673 }
619674
620675 async fn convert_conversation (
@@ -1027,4 +1082,228 @@ mod tests {
10271082
10281083 assert_eq ! ( count, 2 ) ;
10291084 }
1085+
1086+ #[ tokio:: test]
1087+ async fn test_parse_gemini_session_with_thoughts ( ) {
1088+ use std:: fs;
1089+ use tempfile:: TempDir ;
1090+
1091+ let temp_dir = TempDir :: new ( ) . unwrap ( ) ;
1092+ let file_path = temp_dir. path ( ) . join ( "session-test-thoughts.json" ) ;
1093+
1094+ let sample_data = r#"{
1095+ "sessionId": "test-session-123",
1096+ "projectHash": "abc123",
1097+ "startTime": "2024-01-01T10:00:00Z",
1098+ "lastUpdated": "2024-01-01T10:05:00Z",
1099+ "messages": [
1100+ {
1101+ "id": "msg-1",
1102+ "timestamp": "2024-01-01T10:00:00Z",
1103+ "type": "user",
1104+ "content": "Hello"
1105+ },
1106+ {
1107+ "id": "msg-2",
1108+ "timestamp": "2024-01-01T10:01:00Z",
1109+ "type": "gemini",
1110+ "content": "Hi there!",
1111+ "thoughts": [
1112+ {
1113+ "subject": "Analyzing Request",
1114+ "description": "I'm analyzing the user's greeting.",
1115+ "timestamp": "2024-01-01T10:00:30Z"
1116+ },
1117+ {
1118+ "subject": "Formulating Response",
1119+ "description": "I'll respond with a friendly greeting.",
1120+ "timestamp": "2024-01-01T10:00:45Z"
1121+ }
1122+ ]
1123+ }
1124+ ]
1125+ }"# ;
1126+
1127+ fs:: write ( & file_path, sample_data) . unwrap ( ) ;
1128+
1129+ let parser = GeminiCLIParser :: new ( & file_path) ;
1130+ let result = parser. parse ( ) . await ;
1131+
1132+ assert ! ( result. is_ok( ) ) ;
1133+ let sessions = result. unwrap ( ) ;
1134+ assert_eq ! ( sessions. len( ) , 1 ) ;
1135+
1136+ let ( session, messages) = & sessions[ 0 ] ;
1137+
1138+ // Should have 4 messages: 1 user + 2 thinking + 1 main assistant
1139+ assert_eq ! ( messages. len( ) , 4 ) ;
1140+ assert_eq ! ( session. message_count, 4 ) ;
1141+
1142+ // First message: user
1143+ assert_eq ! ( messages[ 0 ] . role, MessageRole :: User ) ;
1144+ assert_eq ! ( messages[ 0 ] . content, "Hello" ) ;
1145+ assert_eq ! ( messages[ 0 ] . sequence_number, 1 ) ;
1146+
1147+ // Second message: thinking 1
1148+ assert_eq ! ( messages[ 1 ] . role, MessageRole :: Assistant ) ;
1149+ assert_eq ! ( messages[ 1 ] . message_type, MessageType :: Thinking ) ;
1150+ assert ! ( messages[ 1 ] . content. contains( "**Analyzing Request**" ) ) ;
1151+ assert ! ( messages[ 1 ]
1152+ . content
1153+ . contains( "analyzing the user's greeting" ) ) ;
1154+ assert_eq ! ( messages[ 1 ] . sequence_number, 2 ) ;
1155+
1156+ // Third message: thinking 2
1157+ assert_eq ! ( messages[ 2 ] . role, MessageRole :: Assistant ) ;
1158+ assert_eq ! ( messages[ 2 ] . message_type, MessageType :: Thinking ) ;
1159+ assert ! ( messages[ 2 ] . content. contains( "**Formulating Response**" ) ) ;
1160+ assert_eq ! ( messages[ 2 ] . sequence_number, 3 ) ;
1161+
1162+ // Fourth message: main assistant response
1163+ assert_eq ! ( messages[ 3 ] . role, MessageRole :: Assistant ) ;
1164+ assert_eq ! ( messages[ 3 ] . message_type, MessageType :: SimpleMessage ) ;
1165+ assert_eq ! ( messages[ 3 ] . content, "Hi there!" ) ;
1166+ assert_eq ! ( messages[ 3 ] . sequence_number, 4 ) ;
1167+ }
1168+
1169+ #[ tokio:: test]
1170+ async fn test_parse_gemini_session_no_thoughts ( ) {
1171+ use std:: fs;
1172+ use tempfile:: TempDir ;
1173+
1174+ let temp_dir = TempDir :: new ( ) . unwrap ( ) ;
1175+ let file_path = temp_dir. path ( ) . join ( "session-no-thoughts.json" ) ;
1176+
1177+ let sample_data = r#"{
1178+ "sessionId": "test-session-456",
1179+ "projectHash": "def456",
1180+ "startTime": "2024-01-01T10:00:00Z",
1181+ "lastUpdated": "2024-01-01T10:02:00Z",
1182+ "messages": [
1183+ {
1184+ "id": "msg-1",
1185+ "timestamp": "2024-01-01T10:00:00Z",
1186+ "type": "user",
1187+ "content": "Hello"
1188+ },
1189+ {
1190+ "id": "msg-2",
1191+ "timestamp": "2024-01-01T10:01:00Z",
1192+ "type": "gemini",
1193+ "content": "Hi there!"
1194+ }
1195+ ]
1196+ }"# ;
1197+
1198+ fs:: write ( & file_path, sample_data) . unwrap ( ) ;
1199+
1200+ let parser = GeminiCLIParser :: new ( & file_path) ;
1201+ let result = parser. parse ( ) . await ;
1202+
1203+ assert ! ( result. is_ok( ) ) ;
1204+ let sessions = result. unwrap ( ) ;
1205+ let ( session, messages) = & sessions[ 0 ] ;
1206+
1207+ // Should have 2 messages: 1 user + 1 assistant (no thinking)
1208+ assert_eq ! ( messages. len( ) , 2 ) ;
1209+ assert_eq ! ( session. message_count, 2 ) ;
1210+
1211+ assert_eq ! ( messages[ 0 ] . role, MessageRole :: User ) ;
1212+ assert_eq ! ( messages[ 1 ] . role, MessageRole :: Assistant ) ;
1213+ assert_eq ! ( messages[ 1 ] . message_type, MessageType :: SimpleMessage ) ;
1214+ }
1215+
1216+ #[ tokio:: test]
1217+ async fn test_parse_gemini_session_user_message_ignores_thoughts ( ) {
1218+ use std:: fs;
1219+ use tempfile:: TempDir ;
1220+
1221+ let temp_dir = TempDir :: new ( ) . unwrap ( ) ;
1222+ let file_path = temp_dir. path ( ) . join ( "session-user-thoughts.json" ) ;
1223+
1224+ // Even if user messages somehow have thoughts, they should be ignored
1225+ let sample_data = r#"{
1226+ "sessionId": "test-session-789",
1227+ "projectHash": "ghi789",
1228+ "startTime": "2024-01-01T10:00:00Z",
1229+ "lastUpdated": "2024-01-01T10:01:00Z",
1230+ "messages": [
1231+ {
1232+ "id": "msg-1",
1233+ "timestamp": "2024-01-01T10:00:00Z",
1234+ "type": "user",
1235+ "content": "Hello",
1236+ "thoughts": [
1237+ {
1238+ "subject": "Should Be Ignored",
1239+ "description": "This thought should not create a message.",
1240+ "timestamp": "2024-01-01T10:00:00Z"
1241+ }
1242+ ]
1243+ }
1244+ ]
1245+ }"# ;
1246+
1247+ fs:: write ( & file_path, sample_data) . unwrap ( ) ;
1248+
1249+ let parser = GeminiCLIParser :: new ( & file_path) ;
1250+ let result = parser. parse ( ) . await ;
1251+
1252+ assert ! ( result. is_ok( ) ) ;
1253+ let sessions = result. unwrap ( ) ;
1254+ let ( session, messages) = & sessions[ 0 ] ;
1255+
1256+ // Should have only 1 message (user thoughts are ignored)
1257+ assert_eq ! ( messages. len( ) , 1 ) ;
1258+ assert_eq ! ( session. message_count, 1 ) ;
1259+ assert_eq ! ( messages[ 0 ] . role, MessageRole :: User ) ;
1260+ }
1261+
1262+ #[ tokio:: test]
1263+ async fn test_parse_gemini_session_empty_thoughts_array ( ) {
1264+ use std:: fs;
1265+ use tempfile:: TempDir ;
1266+
1267+ let temp_dir = TempDir :: new ( ) . unwrap ( ) ;
1268+ let file_path = temp_dir. path ( ) . join ( "session-empty-thoughts.json" ) ;
1269+
1270+ // Empty thoughts array should not create any thinking messages
1271+ let sample_data = r#"{
1272+ "sessionId": "test-session-empty",
1273+ "projectHash": "empty123",
1274+ "startTime": "2024-01-01T10:00:00Z",
1275+ "lastUpdated": "2024-01-01T10:01:00Z",
1276+ "messages": [
1277+ {
1278+ "id": "msg-1",
1279+ "timestamp": "2024-01-01T10:00:00Z",
1280+ "type": "user",
1281+ "content": "Hello"
1282+ },
1283+ {
1284+ "id": "msg-2",
1285+ "timestamp": "2024-01-01T10:01:00Z",
1286+ "type": "gemini",
1287+ "content": "Hi there!",
1288+ "thoughts": []
1289+ }
1290+ ]
1291+ }"# ;
1292+
1293+ fs:: write ( & file_path, sample_data) . unwrap ( ) ;
1294+
1295+ let parser = GeminiCLIParser :: new ( & file_path) ;
1296+ let result = parser. parse ( ) . await ;
1297+
1298+ assert ! ( result. is_ok( ) ) ;
1299+ let sessions = result. unwrap ( ) ;
1300+ let ( session, messages) = & sessions[ 0 ] ;
1301+
1302+ // Should have only 2 messages (no thinking for empty array)
1303+ assert_eq ! ( messages. len( ) , 2 ) ;
1304+ assert_eq ! ( session. message_count, 2 ) ;
1305+ assert_eq ! ( messages[ 0 ] . role, MessageRole :: User ) ;
1306+ assert_eq ! ( messages[ 1 ] . role, MessageRole :: Assistant ) ;
1307+ assert_eq ! ( messages[ 1 ] . message_type, MessageType :: SimpleMessage ) ;
1308+ }
10301309}
0 commit comments