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

+3
View File
@@ -34,6 +34,9 @@ Rules: ask the user (ask_user) when a decision is theirs (stack, scope, which pa
pub const CLEAN_ROOM_ON: &str = "Clean room is ON: only use what a visitor can see (screenshots, visible text, measured layout). Do not read the target's page source, scripts or files (web_fetch with raw=true is refused).";
pub const CLEAN_ROOM_OFF: &str = "Deep mode is ON: you may also read the target's page source and scripts, and analyse programs (binary_info, decompile with Ghidra) for the user's private use. The goal is a faithful RE-CREATION, not a copy. For software:\n1. STUDY: binary_info, then decompile. Find the program's structure: entry point and main loop, UI (windows, menus, dialogs, resources), input handling, file formats, algorithms and data structures, timing.\n2. SPECIFY: write .codecraft/spec.md – behaviour, screens, data and file formats, algorithms in your own words (pseudocode allowed), edge cases. This spec is the only input for the build.\n3. BUILD: implement from the spec in a modern stack (the user's choice, or a sensible default), with clean, idiomatic, documented code. Never paste or mechanically translate decompiled code; names, structure and code must be your own. Reuse only facts (file formats, constants needed for compatibility).\n4. CHECK: compare with the original – same files read/written, same outputs for the same inputs, same look where it matters – and refine.";
/// Plain conversation (mode "chat"): no project, no coding workflow.
pub const CHAT_PROMPT: &str = "You are a helpful, knowledgeable assistant having a conversation with the user. Answer directly and naturally in the user's language; keep answers as short as the question allows and go into depth when asked. Use Markdown only where it helps (lists, code, tables). You can search the web (web_search) and read pages (web_fetch) for current or specific facts – say where information comes from. You have no access to files or a shell in this mode; if the user wants something built or changed in a project, suggest switching to build mode.";
pub const GENERAL_PROMPT: &str = "You are a subagent. Complete the delegated task autonomously and end with a concise, complete report of what you found or did – it is the only thing the calling agent sees.";
pub const EXPLORE_PROMPT: &str = "You are a fast, read-only exploration subagent. Search broadly (glob, grep, read excerpts) and report the conclusion with precise file:line references. Do not modify anything.";
+1 -1
View File
@@ -48,7 +48,7 @@ pub fn to_wire_messages(messages: &[Message]) -> Vec<Value> {
Part::Text { text } if !text.is_empty() => {
content.push(json!({"type": "text", "text": text}))
}
Part::Summary { text } => content.push(json!({
Part::Summary { text, .. } => content.push(json!({
"type": "text",
"text": format!("<conversation-summary>\n{text}\n</conversation-summary>")
})),
+1 -1
View File
@@ -40,7 +40,7 @@ pub fn to_wire_messages(system: &str, messages: &[Message]) -> Vec<Value> {
Part::Text { text } if !text.is_empty() => {
parts.push(json!({"type": "text", "text": text}))
}
Part::Summary { text } => parts.push(json!({
Part::Summary { text, .. } => parts.push(json!({
"type": "text",
"text": format!("<conversation-summary>\n{text}\n</conversation-summary>")
})),
+11 -1
View File
@@ -57,6 +57,12 @@ pub enum Part {
/// Marks the start of a compacted history; the text is the summary.
Summary {
text: String,
/// Started automatically because the context was nearly full.
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
auto: bool,
/// Context size (tokens) when it was compacted.
#[serde(default, skip_serializing_if = "Option::is_none")]
tokens: Option<u64>,
},
}
@@ -213,6 +219,9 @@ pub enum Event {
StepStarted { session_id: String, message_id: String, started_at: i64, prompt_tokens: u64, estimated: bool },
/// Messages waiting for a busy session (full list).
QueueUpdated { session_id: String, items: Value },
/// Compaction of the history: state "started", "done" or "failed"; auto = triggered by
/// a nearly full context (tokens of limit).
Compaction { session_id: String, state: String, auto: bool, tokens: Option<u64>, limit: Option<u64> },
}
impl Event {
@@ -234,7 +243,8 @@ impl Event {
| Error { session_id, .. }
| Waiting { session_id, .. }
| QueueUpdated { session_id, .. }
| StepStarted { session_id, .. } => Some(session_id),
| StepStarted { session_id, .. }
| Compaction { session_id, .. } => Some(session_id),
McpStatus { .. } => None,
}
}