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

+102
View File
@@ -0,0 +1,102 @@
//! Local configuration (~/.config/codecraft/config.toml, readable only by the user).
use anyhow::{Context, Result};
use codecraft_core::config::ProviderConfig;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::path::PathBuf;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServerLogin {
pub url: String,
pub token: String,
#[serde(default)]
pub username: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Config {
/// Signed-in CodeCraft server (models, modes and rules come from there).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub server: Option<ServerLogin>,
/// Model used for new sessions ("provider/model", server models as "server:provider/model").
#[serde(default, skip_serializing_if = "Option::is_none")]
pub default_model: Option<String>,
/// Permission mode for new sessions (default: auto).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub permission_mode: Option<String>,
/// UI language (en, de); default from the server or $LANG.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub lang: Option<String>,
/// Theme override, e.g. "nord" and "dark" (default: your server theme).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub theme: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub theme_mode: Option<String>,
/// Own model providers (standalone mode), same format as the server config.
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub providers: BTreeMap<String, ProviderConfig>,
}
pub fn config_dir() -> PathBuf {
if let Some(x) = std::env::var_os("CODECRAFT_CONFIG_DIR") {
return PathBuf::from(x);
}
#[cfg(windows)]
if let Some(x) = std::env::var_os("APPDATA") {
return PathBuf::from(x).join("codecraft");
}
let base = std::env::var_os("XDG_CONFIG_HOME").map(PathBuf::from).unwrap_or_else(|| home().join(".config"));
base.join("codecraft")
}
pub fn data_dir() -> PathBuf {
if let Some(x) = std::env::var_os("CODECRAFT_DATA_DIR") {
return PathBuf::from(x);
}
#[cfg(windows)]
if let Some(x) = std::env::var_os("LOCALAPPDATA") {
return PathBuf::from(x).join("codecraft");
}
let base = std::env::var_os("XDG_DATA_HOME").map(PathBuf::from).unwrap_or_else(|| home().join(".local").join("share"));
base.join("codecraft")
}
pub fn home() -> PathBuf {
std::env::var_os("HOME").or_else(|| std::env::var_os("USERPROFILE")).map(PathBuf::from).unwrap_or_else(|| PathBuf::from("."))
}
fn path() -> PathBuf {
config_dir().join("config.toml")
}
impl Config {
pub fn load() -> Result<Self> {
let p = path();
match std::fs::read_to_string(&p) {
Ok(text) => toml::from_str(&text).with_context(|| format!("invalid {}", p.display())),
Err(_) => Ok(Self::default()),
}
}
pub fn save(&self) -> Result<()> {
let p = path();
std::fs::create_dir_all(p.parent().unwrap())?;
let text = format!(
"# CodeCraft terminal client – edit with `codecraft setup` or by hand.\n# API keys may be written as \"${{ENV_VAR}}\".\n\n{}",
toml::to_string_pretty(self)?
);
std::fs::write(&p, text)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&p, std::fs::Permissions::from_mode(0o600))?;
}
Ok(())
}
/// Nothing set up yet: the setup wizard runs first.
pub fn is_empty(&self) -> bool {
self.server.is_none() && self.providers.is_empty()
}
}