Chat tabs, chat mode, own modes, compaction notice, CLI binaries again

- Terminals and file managers as tabs of a chat; chat terminals keep running on
  the server (no idle timeout) and re-attach from any device
- "Just chat" in New session and a built-in chat mode with a lean prompt and only
  web tools; custom modes with kind (coding/conversation), read-only and tool picker
- Mode selector as a dropdown
- The chat shows running compactions (automatic: tokens and share of the limit)
  and labels summaries
- Docker image builds and serves the terminal client binaries again
- Fix: atomic session updates; background title/usage writes no longer undo each
  other (auto compaction triggered one turn late)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-09 11:24:58 +01:00
1 parent 242b80605c
commit c850586a81
29 files changed
+831 -187

No files matched your search

+29 -7
View File
@@ -23,6 +23,8 @@ pub struct Agent {
/// Allowed tools; None = all.
pub tools: Option<Vec<String>>,
pub read_only: bool,
/// "coding" or "chat" (plain conversation prompt).
pub style: String,
pub model: Option<String>,
/// "builtin", "config" or "file".
pub source: String,
@@ -41,11 +43,12 @@ pub use codecraft_core::prompts::*;
pub fn builtin_agents(clean_room: bool) -> Vec<Agent> {
let craft = craft_prompt(clean_room);
vec![
Agent { name: "build".into(), description: "Full access: implement, edit, run".into(), mode: "primary".into(), prompt: BUILD_PROMPT.into(), tools: None, read_only: false, model: None, source: "builtin".into(), max_parallel: None, refine: None, path: None },
Agent { name: "plan".into(), description: "Read-only: analyse and propose a plan".into(), mode: "primary".into(), prompt: PLAN_PROMPT.into(), tools: None, read_only: true, model: None, source: "builtin".into(), max_parallel: None, refine: None, path: None },
Agent { name: "craft".into(), description: "Study a target, build your own version, check and refine".into(), mode: "primary".into(), prompt: craft, tools: None, read_only: false, model: None, source: "builtin".into(), max_parallel: None, refine: None, path: None },
Agent { name: "general".into(), description: "General-purpose subagent for multi-step tasks".into(), mode: "subagent".into(), prompt: GENERAL_PROMPT.into(), tools: None, read_only: false, model: None, source: "builtin".into(), max_parallel: None, refine: None, path: None },
Agent { name: "explore".into(), description: "Read-only subagent for fast codebase searches".into(), mode: "subagent".into(), prompt: EXPLORE_PROMPT.into(), tools: None, read_only: true, model: None, source: "builtin".into(), max_parallel: None, refine: None, path: None },
Agent { name: "build".into(), description: "Full access: implement, edit, run".into(), mode: "primary".into(), prompt: BUILD_PROMPT.into(), tools: None, read_only: false, style: "coding".into(), model: None, source: "builtin".into(), max_parallel: None, refine: None, path: None },
Agent { name: "plan".into(), description: "Read-only: analyse and propose a plan".into(), mode: "primary".into(), prompt: PLAN_PROMPT.into(), tools: None, read_only: true, style: "coding".into(), model: None, source: "builtin".into(), max_parallel: None, refine: None, path: None },
Agent { name: "chat".into(), description: "Just talk: answers, explanations, web search – no files or shell".into(), mode: "primary".into(), prompt: CHAT_PROMPT.into(), tools: Some(vec!["web_search".into(), "web_fetch".into()]), read_only: true, style: "chat".into(), model: None, source: "builtin".into(), max_parallel: None, refine: None, path: None },
Agent { name: "craft".into(), description: "Study a target, build your own version, check and refine".into(), mode: "primary".into(), prompt: craft, tools: None, read_only: false, style: "coding".into(), model: None, source: "builtin".into(), max_parallel: None, refine: None, path: None },
Agent { name: "general".into(), description: "General-purpose subagent for multi-step tasks".into(), mode: "subagent".into(), prompt: GENERAL_PROMPT.into(), tools: None, read_only: false, style: "coding".into(), model: None, source: "builtin".into(), max_parallel: None, refine: None, path: None },
Agent { name: "explore".into(), description: "Read-only subagent for fast codebase searches".into(), mode: "subagent".into(), prompt: EXPLORE_PROMPT.into(), tools: None, read_only: true, style: "coding".into(), model: None, source: "builtin".into(), max_parallel: None, refine: None, path: None },
]
}
@@ -113,7 +116,8 @@ pub fn agents(app: &App, root: &Path, cwd: &Path) -> Vec<Agent> {
mode: meta.get("mode").cloned().unwrap_or_else(|| "subagent".into()),
prompt: body,
tools,
read_only: false,
read_only: meta.get("read_only").is_some_and(|v| v == "true"),
style: meta.get("style").filter(|v| *v == "chat").cloned().unwrap_or_else(|| "coding".into()),
model: meta.get("model").filter(|m| m.contains('/')).cloned(),
source: "file".into(),
max_parallel: meta.get("max_parallel").and_then(|v| v.parse().ok()),
@@ -131,7 +135,8 @@ pub fn agents(app: &App, root: &Path, cwd: &Path) -> Vec<Agent> {
mode: a.mode.clone().or(base.as_ref().map(|b| b.mode.clone())).unwrap_or_else(|| "subagent".into()),
prompt: a.prompt.clone().or(base.as_ref().map(|b| b.prompt.clone())).unwrap_or_default(),
tools: a.tools.clone().or(base.as_ref().and_then(|b| b.tools.clone())),
read_only: base.as_ref().is_some_and(|b| b.read_only),
read_only: a.read_only.unwrap_or_else(|| base.as_ref().is_some_and(|b| b.read_only)),
style: a.style.clone().filter(|s| s == "chat" || s == "coding").or(base.as_ref().map(|b| b.style.clone())).unwrap_or_else(|| "coding".into()),
model: a.model.clone().filter(|m| !m.is_empty()).or(base.as_ref().and_then(|b| b.model.clone())),
source: "config".into(),
max_parallel: a.max_parallel.or(base.as_ref().and_then(|b| b.max_parallel)),
@@ -177,7 +182,24 @@ fn instruction_files(app: &App, root: &Path, cwd: &Path) -> Vec<(PathBuf, String
out
}
/// The plain chat mode: no coding instructions, project files or memory – only the
/// date, the model and what the user keeps in their personal memory.
fn chat_prompt(app: &App, agent: &Agent, model: &str, owner: Option<&crate::store::User>) -> String {
let mut s = agent.prompt.clone();
s.push_str(&format!("\n\n# Context\n- Date: {}\n- Model: {model}\n", chrono::Local::now().format("%A, %Y-%m-%d")));
if let Some(u) = owner {
let memory: String = app.store.user_setting(&u.id, "memory").unwrap_or_default();
if !memory.trim().is_empty() {
s.push_str(&format!("\n# About the user (personal memory)\n{}\n", memory.trim()));
}
}
s
}
pub fn system_prompt(app: &App, agent: &Agent, root: &Path, cwd: &Path, model: &str, permission_mode: &str, owner: Option<&crate::store::User>) -> String {
if agent.style == "chat" {
return chat_prompt(app, agent, model, owner);
}
let mut s = String::from(BASE_PROMPT);
s.push_str("\n\n");
s.push_str(&agent.prompt);