Split view, personal modes, budget warnings, server move script

- Up to three chats side by side (#/s/a+b+c), "Open beside" in the sidebar menu
- File manager tabs of a chat stored per user on the server (/sessions/{id}/tabs)
- Personal modes for every user (Settings → My modes, /account/modes)
- Budget meters (total, provider, images, opencode Go windows) with push warnings
  for admins at the warning share and 100 %, shown in the status overlay
- scripts/move-server.sh: move to another Docker host with bind mounts (Unraid);
  scripts/smoke-test.sh

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-09 12:06:20 +01:00
1 parent 81ee084ff2
commit d152ad836d
26 files changed
+836 -52

No files matched your search

+34 -2
View File
@@ -147,8 +147,40 @@ pub fn agents(app: &App, root: &Path, cwd: &Path) -> Vec<Agent> {
map.into_values().collect()
}
pub fn find_agent(app: &App, root: &Path, cwd: &Path, name: &str) -> Agent {
agents(app, root, cwd)
/// Personal modes a user created for themselves (user setting "modes").
pub fn personal_modes(app: &App, user_id: &str) -> BTreeMap<String, crate::config::AgentConfig> {
app.store.user_setting(user_id, "modes").unwrap_or_default()
}
/// All agents plus the user's personal modes. Personal modes never replace built-in,
/// admin or project ones of the same name.
pub fn agents_for(app: &App, root: &Path, cwd: &Path, user_id: Option<&str>) -> Vec<Agent> {
let mut list = agents(app, root, cwd);
let Some(uid) = user_id else { return list };
for (name, a) in personal_modes(app, uid) {
if list.iter().any(|x| x.name == name) {
continue;
}
list.push(Agent {
name,
description: a.description.unwrap_or_default(),
mode: "primary".into(),
prompt: a.prompt.unwrap_or_default(),
tools: a.tools,
read_only: a.read_only.unwrap_or(false),
style: a.style.filter(|s| s == "chat").unwrap_or_else(|| "coding".into()),
model: a.model.filter(|m| !m.is_empty()),
source: "personal".into(),
max_parallel: None,
refine: a.refine,
path: None,
});
}
list
}
pub fn find_agent_for(app: &App, root: &Path, cwd: &Path, name: &str, user_id: Option<&str>) -> Agent {
agents_for(app, root, cwd, user_id)
.into_iter()
.find(|a| a.name == name)
.unwrap_or_else(|| builtin_agents(true).remove(0))