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]>
45 lines
1.3 KiB
Rust
45 lines
1.3 KiB
Rust
//! Per-user saved terminal tab layout for an agent (named tabs + their tmux
|
|
//! sessions), so a user can restore their terminal after logging back in.
|
|
|
|
use cm_domain::{AgentId, UserId, WorkspaceId};
|
|
use sqlx::{PgPool, Row};
|
|
|
|
use crate::DbError;
|
|
|
|
/// The saved layout JSON for `(user, agent)`, or None if nothing saved.
|
|
pub async fn get(
|
|
pool: &PgPool,
|
|
user_id: UserId,
|
|
agent_id: AgentId,
|
|
) -> Result<Option<serde_json::Value>, DbError> {
|
|
let row = sqlx::query("SELECT layout FROM terminal_tabs WHERE user_id = $1 AND agent_id = $2")
|
|
.bind(user_id.as_uuid())
|
|
.bind(agent_id.as_uuid())
|
|
.fetch_optional(pool)
|
|
.await?;
|
|
Ok(row.map(|r| r.get::<serde_json::Value, _>("layout")))
|
|
}
|
|
|
|
/// Save (replace) the layout for `(user, agent)`.
|
|
pub async fn upsert(
|
|
pool: &PgPool,
|
|
workspace_id: WorkspaceId,
|
|
user_id: UserId,
|
|
agent_id: AgentId,
|
|
layout: &serde_json::Value,
|
|
) -> Result<(), DbError> {
|
|
sqlx::query(
|
|
"INSERT INTO terminal_tabs (user_id, agent_id, workspace_id, layout, updated_at)
|
|
VALUES ($1, $2, $3, $4, now())
|
|
ON CONFLICT (user_id, agent_id)
|
|
DO UPDATE SET layout = $4, workspace_id = $3, updated_at = now()",
|
|
)
|
|
.bind(user_id.as_uuid())
|
|
.bind(agent_id.as_uuid())
|
|
.bind(workspace_id.as_uuid())
|
|
.bind(layout)
|
|
.execute(pool)
|
|
.await?;
|
|
Ok(())
|
|
}
|