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

+25 -3
View File
@@ -300,7 +300,18 @@ pub async fn run_session(app: Arc<App>, session_id: &str, cancel: CancellationTo
reasoning: true,
model: model.clone(),
};
let step = stream_step(&app, &session, &agent.name, &model_spec, provider, req, &cancel).await?;
let step = match stream_step(&app, &session, &agent.name, &model_spec, provider, req.clone(), &cancel).await {
Ok(step) => step,
// Host down or budget used up: switch to the model's fallback for this step.
Err(e) if model.fallback.is_some() && !cancel.is_cancelled() && needs_fallback(&e) => {
let fb = model.fallback.clone().unwrap_or_default();
let (fb_provider, fb_model) = app.registry().resolve(&fb)?;
app.emit(Event::Error { session_id: session_id.into(), message: format!("{model_spec} is not available ({}) – using the fallback model {fb}", short_error(&e)) });
let req = ChatRequest { model: fb_model.clone(), max_tokens: fb_model.max_output.unwrap_or(16_000), ..req };
stream_step(&app, &session, &agent.name, &fb, fb_provider, req, &cancel).await?
}
Err(e) => return Err(e),
};
final_text = step
.message
.parts
@@ -747,9 +758,20 @@ fn estimate_prompt_tokens(req: &ChatRequest) -> u64 {
(chars as f64 / 3.8) as u64 + images * 1000
}
/// Errors after which the fallback model takes over: the host is unreachable
/// (connection, timeout, gateway errors) or the provider's budget is used up.
fn needs_fallback(e: &anyhow::Error) -> bool {
let s = format!("{e:#}").to_lowercase();
["connect", "timed out", "timeout", "http 502", "http 503", "http 504", "dns", "budget"].iter().any(|k| s.contains(k))
}
fn short_error(e: &anyhow::Error) -> String {
format!("{e:#}").lines().next().unwrap_or("").chars().take(120).collect()
}
fn is_retryable(e: &anyhow::Error) -> bool {
let s = format!("{e:#}");
s.contains("HTTP 429") || s.contains("HTTP 5") || s.contains("overloaded") || s.contains("connection") || s.contains("timed out")
let s = format!("{e:#}").to_lowercase();
["http 429", "http 5", "overloaded", "connect", "timed out", "timeout"].iter().any(|k| s.contains(k))
}
/// One model request with streaming to the UI, usage recording and retries.