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>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-09 00:13:06 +01:00
1 parent aa1f8cf016
commit 6dffcb501a
52 files changed
+6477 -765

No files matched your search

+57
View File
@@ -0,0 +1,57 @@
//! Version information and a daily check for new releases.
use crate::app::App;
use serde_json::{Value, json};
use std::sync::{Arc, Mutex};
use std::time::Duration;
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
/// Git commit the image was built from (Docker build arg GIT_COMMIT).
pub fn commit() -> &'static str {
option_env!("CODECRAFT_COMMIT").unwrap_or("")
}
#[derive(Default)]
pub struct UpdateState {
pub latest: Mutex<Option<Value>>,
}
fn newer(a: &str, b: &str) -> bool {
let p = |v: &str| v.trim_start_matches('v').split(['.', '-']).take(3).map(|x| x.parse::<u64>().unwrap_or(0)).collect::<Vec<_>>();
p(a) > p(b)
}
async fn check(app: &App) -> Option<Value> {
let url = app.config().server.update_url.clone();
if url.is_empty() {
return None;
}
let v: Value = crate::provider::http_client().get(&url).timeout(Duration::from_secs(20)).send().await.ok()?.json().await.ok()?;
let tag = v["tag_name"].as_str()?.to_string();
if !newer(&tag, VERSION) {
return None;
}
Some(json!({"version": tag.trim_start_matches('v'), "url": v["html_url"], "notes": v["body"].as_str().unwrap_or("").chars().take(4000).collect::<String>(), "published_at": v["published_at"]}))
}
pub fn spawn_checker(app: Arc<App>) {
tokio::spawn(async move {
tokio::time::sleep(Duration::from_secs(60)).await;
loop {
let found = check(&app).await;
*app.update.latest.lock().unwrap() = found;
tokio::time::sleep(Duration::from_secs(24 * 3600)).await;
}
});
}
#[cfg(test)]
mod tests {
#[test]
fn version_compare() {
assert!(super::newer("v0.2.0", "0.1.9"));
assert!(super::newer("1.0.0", "0.9.9"));
assert!(!super::newer("v0.1.0", "0.1.0"));
}
}