Files
codecraft/cli/src/session.rs
T
Richard ZinkandClaude Opus 5.5 6dffcb501a Add terminal client, memory, model fallback, version and install improvements
- codecraft: full-screen terminal client (ratatui) – local tools and files,
  models via the CodeCraft server gateway or own providers (standalone with a
  guided setup), slash commands, @files, permission dialogs, resume, update
- Static Linux CLI builds (x86_64/aarch64, cargo-zigbuild) served by the
  server: /cli/install.sh and /cli/download/<target>
- Project memory (remember tool, .codecraft/memory.md) and personal memory
- Model fallback when a host is unreachable or a budget is used up
- Version/commit in UI and health, daily release check, "What's new" dialog
- PWA: install button, maskable icon, manifest id and screenshots
- Case-insensitive detection of connection errors for retries

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 00:13:06 +01:00

91 lines
2.7 KiB
Rust

//! Local sessions (~/.local/share/codecraft/sessions/<id>.json).
use anyhow::Result;
use codecraft_core::types::{Message, Part, Usage, new_id, now_ms};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::path::PathBuf;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LocalSession {
pub id: String,
pub title: String,
pub cwd: String,
pub model: String,
pub mode: String,
pub permission_mode: String,
pub created_at: i64,
pub updated_at: i64,
pub messages: Vec<Message>,
#[serde(default)]
pub usage: Usage,
#[serde(default)]
pub cost: f64,
#[serde(default)]
pub todos: Value,
/// Tokens of the last request (context fill).
#[serde(default)]
pub context_tokens: u64,
}
fn dir() -> PathBuf {
crate::conf::data_dir().join("sessions")
}
impl LocalSession {
pub fn new(cwd: &str, model: &str, mode: &str, permission_mode: &str) -> Self {
let now = now_ms();
Self {
id: new_id(),
title: String::new(),
cwd: cwd.into(),
model: model.into(),
mode: mode.into(),
permission_mode: permission_mode.into(),
created_at: now,
updated_at: now,
messages: vec![],
usage: Usage::default(),
cost: 0.0,
todos: Value::Null,
context_tokens: 0,
}
}
pub fn save(&mut self) -> Result<()> {
if self.messages.is_empty() {
return Ok(());
}
self.updated_at = now_ms();
std::fs::create_dir_all(dir())?;
let tmp = dir().join(format!("{}.json.tmp", self.id));
std::fs::write(&tmp, serde_json::to_vec(self)?)?;
std::fs::rename(tmp, dir().join(format!("{}.json", self.id)))?;
Ok(())
}
pub fn load(id: &str) -> Result<Self> {
Ok(serde_json::from_slice(&std::fs::read(dir().join(format!("{id}.json")))?)?)
}
/// Messages the model sees: everything from the last summary on.
pub fn active(&self) -> Vec<Message> {
let start = self.messages.iter().rposition(|m| m.parts.iter().any(|p| matches!(p, Part::Summary { .. }))).unwrap_or(0);
self.messages[start..].to_vec()
}
}
/// Sessions (newest first), optionally only those of one folder.
pub fn list(cwd: Option<&str>) -> Vec<LocalSession> {
let mut out: Vec<LocalSession> = std::fs::read_dir(dir())
.into_iter()
.flatten()
.flatten()
.filter(|e| e.path().extension().is_some_and(|x| x == "json"))
.filter_map(|e| serde_json::from_slice::<LocalSession>(&std::fs::read(e.path()).ok()?).ok())
.filter(|s| cwd.is_none_or(|c| s.cwd == c))
.collect();
out.sort_by_key(|s| std::cmp::Reverse(s.updated_at));
out
}