Feature: Implement session management for listing and loading saved sessions

This commit is contained in:
2026-05-08 01:21:48 +02:00
parent 56f6613537
commit fe2a15afca

View File

@@ -1,5 +1,7 @@
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::io::Write;
use std::fs::File;
use std::io::{BufRead, BufReader, Write};
use std::path::PathBuf;
use std::process::{Command, Stdio};
use tauri::{
@@ -12,6 +14,93 @@ fn repo_root() -> PathBuf {
manifest_dir.parent().unwrap_or(&manifest_dir).to_path_buf()
}
fn sessions_file() -> PathBuf {
repo_root().join(".idea-hole").join("sessions.jsonl")
}
#[derive(Deserialize)]
struct SessionFields {
title: Option<String>,
description: Option<String>,
saved_at: Option<i64>,
}
#[derive(Serialize)]
struct SessionSummary {
title: String,
description: String,
saved_at: i64,
}
fn open_sessions_file() -> Result<Option<File>, String> {
match File::open(sessions_file()) {
Ok(file) => Ok(Some(file)),
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(err) => Err(format!("Failed to open sessions file: {err}")),
}
}
fn session_summary_from_line(line: &str) -> Option<SessionSummary> {
let fields: SessionFields = serde_json::from_str(line).ok()?;
let title = fields.title.unwrap_or_default().trim().to_string();
if title.is_empty() {
return None;
}
Some(SessionSummary {
title,
description: fields.description.unwrap_or_default(),
saved_at: fields.saved_at.unwrap_or_default(),
})
}
fn list_sessions_fast() -> Result<Vec<SessionSummary>, String> {
let Some(file) = open_sessions_file()? else {
return Ok(Vec::new());
};
let mut sessions = Vec::new();
for line in BufReader::new(file).lines() {
let line = line.map_err(|err| format!("Failed to read sessions file: {err}"))?;
if line.trim().is_empty() {
continue;
}
if let Some(summary) = session_summary_from_line(&line) {
sessions.push(summary);
}
}
Ok(sessions)
}
fn load_session_fast(title: &str) -> Result<Value, String> {
let target = title.trim();
if target.is_empty() {
return Ok(Value::Null);
}
let Some(file) = open_sessions_file()? else {
return Ok(Value::Null);
};
for line in BufReader::new(file).lines() {
let line = line.map_err(|err| format!("Failed to read sessions file: {err}"))?;
if line.trim().is_empty() {
continue;
}
let Some(summary) = session_summary_from_line(&line) else {
continue;
};
if summary.title == target {
return serde_json::from_str(&line)
.map_err(|err| format!("Failed to parse saved session: {err}"));
}
}
Ok(Value::Null)
}
fn run_python(script: &str, input: &str) -> Result<Vec<u8>, String> {
let cwd = repo_root();
let mut last_err: Option<String> = None;
@@ -58,6 +147,17 @@ fn run_python(script: &str, input: &str) -> Result<Vec<u8>, String> {
#[tauri::command]
fn run_python_action(action: String, payload: Value) -> Result<Value, String> {
match action.as_str() {
"list_sessions" => {
return serde_json::to_value(list_sessions_fast()?).map_err(|err| err.to_string());
}
"load_session" => {
let title = payload.get("title").and_then(Value::as_str).unwrap_or("");
return load_session_fast(title);
}
_ => {}
}
let script = repo_root().join("concept_api.py");
if !script.exists() {
return Err("concept_api.py not found".to_string());