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:
1 parent
242b80605c
commit
c850586a81
29 files changed
+831
-187
No files matched your search
+33
-4
@@ -18,13 +18,16 @@ use tokio::sync::broadcast;
|
||||
|
||||
/// Output kept for re-attaching clients.
|
||||
const SCROLLBACK: usize = 256 * 1024;
|
||||
/// Detached terminals are stopped after this time.
|
||||
/// Detached terminals outside a chat are stopped after this time; terminals that
|
||||
/// belong to a chat stay open until they are closed, exit or the chat is deleted.
|
||||
const DETACHED_TTL: Duration = Duration::from_secs(30 * 60);
|
||||
const MAX_PER_USER: usize = 8;
|
||||
const MAX_PER_USER: usize = 16;
|
||||
|
||||
pub struct Term {
|
||||
pub id: String,
|
||||
pub owner_id: String,
|
||||
/// Chat the terminal is a tab of.
|
||||
pub session_id: Option<String>,
|
||||
pub cwd: PathBuf,
|
||||
pub created_at: i64,
|
||||
writer: tokio::sync::Mutex<pty_process::OwnedWritePty>,
|
||||
@@ -57,6 +60,7 @@ impl Term {
|
||||
json!({
|
||||
"id": self.id,
|
||||
"cwd": self.cwd,
|
||||
"session_id": self.session_id,
|
||||
"created_at": self.created_at,
|
||||
"alive": self.alive(),
|
||||
"attached": *self.attached.lock().unwrap() > 0,
|
||||
@@ -76,6 +80,30 @@ impl Terminals {
|
||||
v.into_iter().map(|(_, i)| i).collect()
|
||||
}
|
||||
|
||||
/// Terminals of one chat (oldest first).
|
||||
pub fn for_session(&self, session_id: &str, owner_id: &str) -> Vec<Value> {
|
||||
let mut v: Vec<_> = self
|
||||
.map
|
||||
.lock()
|
||||
.unwrap()
|
||||
.values()
|
||||
.filter(|t| t.owner_id == owner_id && t.session_id.as_deref() == Some(session_id))
|
||||
.map(|t| (t.created_at, t.info()))
|
||||
.collect();
|
||||
v.sort_by_key(|(c, _)| *c);
|
||||
v.into_iter().map(|(_, i)| i).collect()
|
||||
}
|
||||
|
||||
/// Close all terminals of a deleted chat.
|
||||
pub fn kill_session(&self, session_id: &str) {
|
||||
let gone: Vec<Arc<Term>> = {
|
||||
let mut map = self.map.lock().unwrap();
|
||||
let ids: Vec<String> = map.values().filter(|t| t.session_id.as_deref() == Some(session_id)).map(|t| t.id.clone()).collect();
|
||||
ids.iter().filter_map(|id| map.remove(id)).collect()
|
||||
};
|
||||
gone.iter().for_each(|t| t.kill());
|
||||
}
|
||||
|
||||
pub fn get(&self, id: &str, owner_id: &str) -> Option<Arc<Term>> {
|
||||
self.map.lock().unwrap().get(id).filter(|t| t.owner_id == owner_id).cloned()
|
||||
}
|
||||
@@ -100,7 +128,7 @@ impl Terminals {
|
||||
pub fn reap(&self) {
|
||||
let mut map = self.map.lock().unwrap();
|
||||
map.retain(|_, t| {
|
||||
let stale = t.detached_since.lock().unwrap().is_some_and(|d| d.elapsed() > DETACHED_TTL);
|
||||
let stale = t.session_id.is_none() && t.detached_since.lock().unwrap().is_some_and(|d| d.elapsed() > DETACHED_TTL);
|
||||
let gone = !t.alive() && *t.attached.lock().unwrap() == 0;
|
||||
if stale || gone {
|
||||
t.kill();
|
||||
@@ -113,7 +141,7 @@ impl Terminals {
|
||||
}
|
||||
|
||||
/// Start a shell for `user` in `cwd` (already access-checked).
|
||||
pub fn spawn(app: &Arc<App>, user: &User, cwd: &Path, cols: u16, rows: u16) -> Result<Arc<Term>> {
|
||||
pub fn spawn(app: &Arc<App>, user: &User, cwd: &Path, session_id: Option<String>, cols: u16, rows: u16) -> Result<Arc<Term>> {
|
||||
if app.terminals.list(&user.id).iter().filter(|t| t["alive"] == true).count() >= MAX_PER_USER {
|
||||
anyhow::bail!("too many open terminals (max {MAX_PER_USER}) – close one first");
|
||||
}
|
||||
@@ -142,6 +170,7 @@ pub fn spawn(app: &Arc<App>, user: &User, cwd: &Path, cols: u16, rows: u16) -> R
|
||||
let term = Arc::new(Term {
|
||||
id: crate::types::new_id(),
|
||||
owner_id: user.id.clone(),
|
||||
session_id,
|
||||
cwd: cwd.to_path_buf(),
|
||||
created_at: crate::types::now_ms(),
|
||||
writer: tokio::sync::Mutex::new(writer),
|
||||
|
||||
Reference in new issue
Block a user