Agent computer: terminal (tmux + drives + tabs), Obsidian vault, UI polish
Terminal app (xterm ⇄ WebSocket ⇄ per-agent themed container):
- zsh + oh-my-zsh + powerlevel10k image (agent-terminal), runs as uid 65532 to
share read-write ownership of the file-drive volume with the server.
- Interactive PTY in cm-sandbox (bollard exec tty/attach + resize) + a
TerminalManager; ticket-authed WS bridge routed straight to the backend via a
Traefik PathRegexp(/ws) rule. MOTD greets the user by name.
- tmux resumable sessions; multi-tab (one tmux session per tab, same container),
drag-to-reorder, rename, and a Save that persists named tabs to the server
(terminal_tabs, migration 0014) so they survive logout / a new device.
- Files drives mounted per-agent (subpath) at ~/drives/{documents,received,
shared}; a reconciler keeps the Files app's index in sync with terminal writes.
Storage moved to a shared `filedata` volume (CLAWMATES_STORAGE__DATA_DIR).
Obsidian vault (a markdown "second brain" per agent):
- New `vault` FileDrive (migration 0015) mounted into the terminal at ~/obsidian;
a file-content read route; a purple Obsidian tile + a vault viewer app.
Computer UI:
- Draggable computer-panel width (min = phone preset) keeping the size presets.
- Green Terminal glyph, "Claw Chat" → "Chat", colored gradient-outline app icons.
- Agent page: avatar↔activity-grid spacing + larger, uniform section fonts with
colored section-tinted tag chips.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
671df7c622
commit
e61724ff82
@@ -146,7 +146,7 @@ async fn run() -> Result<(), String> {
|
||||
};
|
||||
// Environment tools need a container engine; absence is tolerated
|
||||
// (shell.exec reports it per-call) so the API still serves.
|
||||
let (sandboxes, browser) = if config.sandbox.enabled {
|
||||
let (sandboxes, browser, terminals) = if config.sandbox.enabled {
|
||||
match cm_sandbox::DockerDriver::connect() {
|
||||
Ok(driver) => {
|
||||
let driver: std::sync::Arc<dyn cm_sandbox::SandboxDriver> =
|
||||
@@ -156,14 +156,32 @@ async fn run() -> Result<(), String> {
|
||||
&config.sandbox.image,
|
||||
));
|
||||
let browser = std::sync::Arc::new(
|
||||
cm_runtime::SandboxManager::new(driver, &config.sandbox.browser_image)
|
||||
cm_runtime::SandboxManager::new(driver.clone(), &config.sandbox.browser_image)
|
||||
.with_egress(),
|
||||
);
|
||||
// Themed interactive terminal containers (zsh + oh-my-zsh + p10k)
|
||||
// for the Terminal computer app. On the Local storage backend the
|
||||
// file-drive volume is mounted (per-agent subpath) at ~/drives.
|
||||
let drives = if config.storage.backend == cm_config::StorageBackend::Local {
|
||||
Some(cm_runtime::DriveConfig {
|
||||
volume: config.sandbox.terminal_drive_volume.clone(),
|
||||
data_dir: PathBuf::from(&config.storage.data_dir),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let terminals = std::sync::Arc::new(cm_runtime::TerminalManager::new(
|
||||
driver,
|
||||
&config.sandbox.terminal_image,
|
||||
config.sandbox.terminal_egress,
|
||||
drives,
|
||||
));
|
||||
// Boot reconciliation: any sandbox the engine still holds is an
|
||||
// orphan from a dead process (we track none yet) — remove them
|
||||
// before warming so a crash/redeploy can't leak containers.
|
||||
agents.reconcile_orphans(std::time::Duration::ZERO).await;
|
||||
browser.reconcile_orphans(std::time::Duration::ZERO).await;
|
||||
terminals.reconcile_orphans().await;
|
||||
let agents = if config.sandbox.warm_pool > 0 {
|
||||
agents.warm(config.sandbox.warm_pool)
|
||||
} else {
|
||||
@@ -179,20 +197,27 @@ async fn run() -> Result<(), String> {
|
||||
std::time::Duration::from_secs(300),
|
||||
std::time::Duration::from_secs(600),
|
||||
);
|
||||
(Some(agents), Some(browser))
|
||||
// Terminals: reap idle (no live session for ~2 h, so a resumable
|
||||
// tmux session survives normal navigation gaps) + orphans.
|
||||
terminals.clone().spawn_reaper(
|
||||
std::time::Duration::from_secs(300),
|
||||
std::time::Duration::from_secs(7200),
|
||||
);
|
||||
(Some(agents), Some(browser), Some(terminals))
|
||||
}
|
||||
Err(error) => {
|
||||
eprintln!("clawmates-server: sandbox engine unavailable: {error}");
|
||||
(None, None)
|
||||
(None, None, None)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
(None, None)
|
||||
(None, None, None)
|
||||
};
|
||||
// Keep handles for the graceful-shutdown drain (SIGTERM) — the managers
|
||||
// themselves are moved into the runtime config below.
|
||||
let drain_agents = sandboxes.clone();
|
||||
let drain_browser = browser.clone();
|
||||
let drain_terminals = terminals.clone();
|
||||
let runtime = Runtime::with_blob_store(
|
||||
pool.clone(),
|
||||
provider,
|
||||
@@ -203,6 +228,7 @@ async fn run() -> Result<(), String> {
|
||||
slack_base_url: config.slack.base_url.clone(),
|
||||
sandboxes,
|
||||
browser,
|
||||
terminals,
|
||||
providers: provider_registry,
|
||||
},
|
||||
blob,
|
||||
@@ -246,6 +272,10 @@ async fn run() -> Result<(), String> {
|
||||
.with_broker(PathBuf::from(&config.broker.socket_path))
|
||||
.with_oauth(config.oauth.clone())
|
||||
.with_billing(config.billing.clone())
|
||||
.with_file_root(
|
||||
(config.storage.backend == cm_config::StorageBackend::Local)
|
||||
.then(|| PathBuf::from(&config.storage.data_dir)),
|
||||
)
|
||||
.pipe_auth_verifier(auth_verifier),
|
||||
);
|
||||
if e2e::enabled() {
|
||||
@@ -277,5 +307,8 @@ async fn run() -> Result<(), String> {
|
||||
if let Some(browser) = drain_browser {
|
||||
browser.shutdown().await;
|
||||
}
|
||||
if let Some(terminals) = drain_terminals {
|
||||
terminals.shutdown().await;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user