@@ -11,7 +11,7 @@ use std::{
1111 time:: { SystemTime , UNIX_EPOCH } ,
1212} ;
1313use walkdir:: WalkDir ;
14- use zip:: { write:: SimpleFileOptions , CompressionMethod , ZipWriter } ;
14+ use zip:: { read :: ZipArchive , write:: SimpleFileOptions , CompressionMethod , ZipWriter } ;
1515
1616const ARCHIVE_VERSION : u32 = 1 ;
1717const ARCHIVE_EXT : & str = "agentswitch-chat.json" ;
@@ -21,6 +21,7 @@ pub enum ChatProvider {
2121 Claude ,
2222 Codex ,
2323 Gemini ,
24+ Antigravity ,
2425 Kiro ,
2526}
2627
@@ -30,6 +31,7 @@ impl ChatProvider {
3031 Self :: Claude => "Claude Code" ,
3132 Self :: Codex => "Codex CLI" ,
3233 Self :: Gemini => "Gemini CLI" ,
34+ Self :: Antigravity => "Antigravity CLI" ,
3335 Self :: Kiro => "Kiro" ,
3436 }
3537 }
@@ -39,6 +41,7 @@ impl ChatProvider {
3941 Self :: Claude => "claude" ,
4042 Self :: Codex => "codex" ,
4143 Self :: Gemini => "gemini" ,
44+ Self :: Antigravity => "antigravity" ,
4245 Self :: Kiro => "kiro" ,
4346 }
4447 }
@@ -48,6 +51,7 @@ impl ChatProvider {
4851 Self :: Claude => ProviderId :: Claude . color ( ) ,
4952 Self :: Codex => ProviderId :: Codex . color ( ) ,
5053 Self :: Gemini => ProviderId :: Gemini . color ( ) ,
54+ Self :: Antigravity => ProviderId :: Antigravity . color ( ) ,
5155 Self :: Kiro => ProviderId :: Kiro . color ( ) ,
5256 }
5357 }
@@ -160,6 +164,7 @@ pub fn scan_all(workspace: &Path) -> Vec<ChatSession> {
160164 sessions. extend ( scan_claude ( ) ) ;
161165 sessions. extend ( scan_codex ( ) ) ;
162166 sessions. extend ( scan_gemini ( ) ) ;
167+ sessions. extend ( scan_antigravity ( ) ) ;
163168 sessions. extend ( scan_kiro ( workspace) ) ;
164169 sessions. extend ( scan_imported ( ) ) ;
165170 sessions. sort_by ( |a, b| {
@@ -263,9 +268,12 @@ pub fn export_sessions_zip(sessions: &[ChatSession], target: &Path) -> Result<Ba
263268 Ok ( report)
264269}
265270
266- pub fn import_archive ( path : & Path ) -> Result < PathBuf > {
267- let archive: ChatArchive = serde_json:: from_str ( & fs:: read_to_string ( path) ?) ?;
271+ pub fn import_archive ( path : & Path , project_dir : Option < & Path > ) -> Result < PathBuf > {
272+ let mut archive: ChatArchive = serde_json:: from_str ( & fs:: read_to_string ( path) ?) ?;
268273 validate_archive ( & archive) ?;
274+ if let Some ( dir) = project_dir {
275+ archive. project_path = dir. to_string_lossy ( ) . to_string ( ) ;
276+ }
269277 let dir = imports_dir ( ) ;
270278 fs:: create_dir_all ( & dir) ?;
271279 let base = safe_file_stem ( & format ! (
@@ -279,10 +287,59 @@ pub fn import_archive(path: &Path) -> Result<PathBuf> {
279287 target = dir. join ( format ! ( "{base}-{n}.{ARCHIVE_EXT}" ) ) ;
280288 n += 1 ;
281289 }
282- fs:: copy ( path , & target ) ?;
290+ fs:: write ( & target , serde_json :: to_string_pretty ( & archive ) ? ) ?;
283291 Ok ( target)
284292}
285293
294+ pub fn import_zip ( path : & Path , project_dir : Option < & Path > ) -> Result < BatchReport > {
295+ let file = File :: open ( path) ?;
296+ let mut zip = ZipArchive :: new ( file) ?;
297+ let dir = imports_dir ( ) ;
298+ fs:: create_dir_all ( & dir) ?;
299+ let mut report = BatchReport :: default ( ) ;
300+ for i in 0 ..zip. len ( ) {
301+ let mut entry = zip. by_index ( i) ?;
302+ let name = entry. name ( ) . to_string ( ) ;
303+ if !name. ends_with ( ARCHIVE_EXT ) {
304+ continue ;
305+ }
306+ let mut buf = String :: new ( ) ;
307+ std:: io:: Read :: read_to_string ( & mut entry, & mut buf) ?;
308+ let mut archive: ChatArchive = match serde_json:: from_str ( & buf) {
309+ Ok ( a) => a,
310+ Err ( _) => {
311+ report. failed += 1 ;
312+ continue ;
313+ }
314+ } ;
315+ if validate_archive ( & archive) . is_err ( ) {
316+ report. failed += 1 ;
317+ continue ;
318+ }
319+ if let Some ( d) = project_dir {
320+ archive. project_path = d. to_string_lossy ( ) . to_string ( ) ;
321+ }
322+ let base = safe_file_stem ( & format ! (
323+ "{}-{}" ,
324+ archive. source_provider. id( ) ,
325+ archive. title
326+ ) ) ;
327+ let mut target = dir. join ( format ! ( "{base}.{ARCHIVE_EXT}" ) ) ;
328+ let mut n = 2usize ;
329+ while target. exists ( ) {
330+ target = dir. join ( format ! ( "{base}-{n}.{ARCHIVE_EXT}" ) ) ;
331+ n += 1 ;
332+ }
333+ fs:: write ( & target, serde_json:: to_string_pretty ( & archive) ?) ?;
334+ report. ok += 1 ;
335+ }
336+ Ok ( report)
337+ }
338+
339+ pub fn exports_dir ( ) -> PathBuf {
340+ data_dir ( ) . join ( "chats" ) . join ( "exports" )
341+ }
342+
286343pub fn soft_delete ( session : & ChatSession , workspace : & Path ) -> Result < ( ) > {
287344 let _ = workspace;
288345 if session. source_kind == ChatSourceKind :: KiroCli {
@@ -593,6 +650,59 @@ fn scan_gemini() -> Vec<ChatSession> {
593650 out
594651}
595652
653+ fn scan_antigravity ( ) -> Vec < ChatSession > {
654+ let Some ( home) = dirs:: home_dir ( ) else {
655+ return vec ! [ ] ;
656+ } ;
657+ let tmp = home. join ( ".gemini" ) . join ( "antigravity-cli" ) . join ( "tmp" ) ;
658+ if !tmp. is_dir ( ) {
659+ return vec ! [ ] ;
660+ }
661+ let mut out = Vec :: new ( ) ;
662+ let Ok ( projects) = fs:: read_dir ( & tmp) else {
663+ return out;
664+ } ;
665+ for project in projects. flatten ( ) . filter ( |e| e. path ( ) . is_dir ( ) ) {
666+ let chats = project. path ( ) . join ( "chats" ) ;
667+ if let Ok ( entries) = fs:: read_dir ( & chats) {
668+ for entry in entries. flatten ( ) {
669+ let path = entry. path ( ) ;
670+ if path. is_file ( ) && is_gemini_session_file ( & path) {
671+ if let Ok ( session) = jsonl_session ( ChatProvider :: Antigravity , & tmp, & path) {
672+ if session. turn_count > 0 {
673+ out. push ( session) ;
674+ }
675+ }
676+ } else if path. is_dir ( ) {
677+ let files = jsonl_files_in ( & path, 1 ) ;
678+ if !files. is_empty ( ) {
679+ if let Ok ( session) =
680+ jsonl_dir_session ( ChatProvider :: Antigravity , & tmp, & path, files)
681+ {
682+ if session. turn_count > 0 {
683+ out. push ( session) ;
684+ }
685+ }
686+ }
687+ }
688+ }
689+ }
690+ if let Ok ( entries) = fs:: read_dir ( project. path ( ) ) {
691+ for entry in entries. flatten ( ) {
692+ let path = entry. path ( ) ;
693+ if path. is_file ( ) && is_gemini_checkpoint_file ( & path) {
694+ if let Ok ( session) = jsonl_session ( ChatProvider :: Antigravity , & tmp, & path) {
695+ if session. turn_count > 0 {
696+ out. push ( session) ;
697+ }
698+ }
699+ }
700+ }
701+ }
702+ }
703+ out
704+ }
705+
596706fn scan_kiro ( workspace : & Path ) -> Vec < ChatSession > {
597707 let _ = workspace;
598708 let Some ( home) = dirs:: home_dir ( ) else {
@@ -983,6 +1093,10 @@ fn update_meta_from_event(provider: ChatProvider, value: &Value, meta: &mut Sess
9831093 meta. project_path =
9841094 str_field ( value, & [ "projectHash" ] ) . map ( |s| format ! ( "Gemini project {s}" ) ) ;
9851095 }
1096+ if meta. project_path . is_none ( ) && provider == ChatProvider :: Antigravity {
1097+ meta. project_path =
1098+ str_field ( value, & [ "projectHash" ] ) . map ( |s| format ! ( "Antigravity project {s}" ) ) ;
1099+ }
9861100}
9871101
9881102fn tool_title ( value : & Value ) -> Option < & str > {
@@ -1184,6 +1298,12 @@ fn project_label_from_path(provider: ChatProvider, root: &Path, path: &Path) ->
11841298 . and_then ( |p| p. components ( ) . next ( ) )
11851299 . map ( |c| c. as_os_str ( ) . to_string_lossy ( ) . to_string ( ) )
11861300 . unwrap_or_else ( || "Gemini project" . into ( ) ) ,
1301+ ChatProvider :: Antigravity => path
1302+ . strip_prefix ( root)
1303+ . ok ( )
1304+ . and_then ( |p| p. components ( ) . next ( ) )
1305+ . map ( |c| c. as_os_str ( ) . to_string_lossy ( ) . to_string ( ) )
1306+ . unwrap_or_else ( || "Antigravity project" . into ( ) ) ,
11871307 _ => path
11881308 . parent ( )
11891309 . map ( |p| p. to_string_lossy ( ) . to_string ( ) )
0 commit comments