feat(sessions): add session browsing for Gemini CLI

This commit is contained in:
Jason
2026-02-21 19:17:38 +08:00
parent c3f29a62d1
commit 2e676e5f53
8 changed files with 145 additions and 7 deletions
+3 -1
View File
@@ -4,7 +4,7 @@ pub mod terminal;
use serde::Serialize;
use std::path::Path;
use providers::{claude, codex, openclaw, opencode};
use providers::{claude, codex, gemini, openclaw, opencode};
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
@@ -42,6 +42,7 @@ pub fn scan_sessions() -> Vec<SessionMeta> {
sessions.extend(claude::scan_sessions());
sessions.extend(opencode::scan_sessions());
sessions.extend(openclaw::scan_sessions());
sessions.extend(gemini::scan_sessions());
sessions.sort_by(|a, b| {
let a_ts = a.last_active_at.or(a.created_at).unwrap_or(0);
@@ -59,6 +60,7 @@ pub fn load_messages(provider_id: &str, source_path: &str) -> Result<Vec<Session
"claude" => claude::load_messages(path),
"opencode" => opencode::load_messages(path),
"openclaw" => openclaw::load_messages(path),
"gemini" => gemini::load_messages(path),
_ => Err(format!("Unsupported provider: {provider_id}")),
}
}
@@ -0,0 +1,117 @@
use std::path::Path;
use serde_json::Value;
use crate::session_manager::{SessionMessage, SessionMeta};
use super::utils::{parse_timestamp_to_ms, truncate_summary};
const PROVIDER_ID: &str = "gemini";
pub fn scan_sessions() -> Vec<SessionMeta> {
let gemini_dir = crate::gemini_config::get_gemini_dir();
let tmp_dir = gemini_dir.join("tmp");
if !tmp_dir.exists() {
return Vec::new();
}
let mut sessions = Vec::new();
// Iterate over project hash directories: tmp/<project_hash>/chats/session-*.json
let project_dirs = match std::fs::read_dir(&tmp_dir) {
Ok(entries) => entries,
Err(_) => return Vec::new(),
};
for entry in project_dirs.flatten() {
let chats_dir = entry.path().join("chats");
if !chats_dir.is_dir() {
continue;
}
let chat_files = match std::fs::read_dir(&chats_dir) {
Ok(entries) => entries,
Err(_) => continue,
};
for file_entry in chat_files.flatten() {
let path = file_entry.path();
if path.extension().and_then(|e| e.to_str()) != Some("json") {
continue;
}
if let Some(meta) = parse_session(&path) {
sessions.push(meta);
}
}
}
sessions
}
pub fn load_messages(path: &Path) -> Result<Vec<SessionMessage>, String> {
let data = std::fs::read_to_string(path).map_err(|e| format!("Failed to read session: {e}"))?;
let value: Value =
serde_json::from_str(&data).map_err(|e| format!("Failed to parse session JSON: {e}"))?;
let messages = value
.get("messages")
.and_then(Value::as_array)
.ok_or_else(|| "No messages array found".to_string())?;
let mut result = Vec::new();
for msg in messages {
let content = match msg.get("content").and_then(Value::as_str) {
Some(c) if !c.trim().is_empty() => c.to_string(),
_ => continue,
};
let role = match msg.get("type").and_then(Value::as_str) {
Some("gemini") => "assistant".to_string(),
Some("user") => "user".to_string(),
Some(other) => other.to_string(),
None => continue,
};
let ts = msg.get("timestamp").and_then(parse_timestamp_to_ms);
result.push(SessionMessage { role, content, ts });
}
Ok(result)
}
fn parse_session(path: &Path) -> Option<SessionMeta> {
let data = std::fs::read_to_string(path).ok()?;
let value: Value = serde_json::from_str(&data).ok()?;
let session_id = value.get("sessionId").and_then(Value::as_str)?.to_string();
let created_at = value.get("startTime").and_then(parse_timestamp_to_ms);
let last_active_at = value.get("lastUpdated").and_then(parse_timestamp_to_ms);
// Derive title from first user message
let title = value
.get("messages")
.and_then(Value::as_array)
.and_then(|msgs| {
msgs.iter()
.find(|m| m.get("type").and_then(Value::as_str) == Some("user"))
.and_then(|m| m.get("content").and_then(Value::as_str))
.filter(|s| !s.trim().is_empty())
.map(|s| truncate_summary(s, 160))
});
let source_path = path.to_string_lossy().to_string();
Some(SessionMeta {
provider_id: PROVIDER_ID.to_string(),
session_id: session_id.clone(),
title: title.clone(),
summary: title,
project_dir: None, // project hash is not reversible
created_at,
last_active_at: last_active_at.or(created_at),
source_path: Some(source_path),
resume_command: Some(format!("gemini --resume {session_id}")),
})
}
@@ -1,5 +1,6 @@
pub mod claude;
pub mod codex;
pub mod gemini;
pub mod openclaw;
pub mod opencode;
mod utils;