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]>
130 lines
3.6 KiB
Rust
130 lines
3.6 KiB
Rust
use serde::{Deserialize, Serialize};
|
|
use time::OffsetDateTime;
|
|
|
|
use crate::ids::{AgentId, UserId, WorkspaceId};
|
|
use crate::role::Role;
|
|
|
|
/// A tenant team (spec §14 Workspace/Team).
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
pub struct Workspace {
|
|
pub id: WorkspaceId,
|
|
pub name: String,
|
|
pub plan: String,
|
|
}
|
|
|
|
/// A human member of a workspace (spec §14 User).
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
pub struct User {
|
|
pub id: UserId,
|
|
pub workspace_id: WorkspaceId,
|
|
pub email: String,
|
|
pub role: Role,
|
|
pub display_name: String,
|
|
#[serde(with = "time::serde::rfc3339")]
|
|
pub created_at: OffsetDateTime,
|
|
}
|
|
|
|
/// Lifecycle of an agent; `Online` renders the green roster dot (§4).
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum AgentStatus {
|
|
Provisioning,
|
|
Online,
|
|
Offline,
|
|
}
|
|
|
|
impl AgentStatus {
|
|
pub fn as_str(&self) -> &'static str {
|
|
match self {
|
|
AgentStatus::Provisioning => "provisioning",
|
|
AgentStatus::Online => "online",
|
|
AgentStatus::Offline => "offline",
|
|
}
|
|
}
|
|
}
|
|
|
|
impl std::str::FromStr for AgentStatus {
|
|
type Err = String;
|
|
|
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
match s {
|
|
"provisioning" => Ok(AgentStatus::Provisioning),
|
|
"online" => Ok(AgentStatus::Online),
|
|
"offline" => Ok(AgentStatus::Offline),
|
|
other => Err(format!("unknown agent status: {other}")),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The three file drives (spec §7.4): per-agent documents and received
|
|
/// files, plus the team-wide shared ClawDrive.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum FileDrive {
|
|
Documents,
|
|
Received,
|
|
Shared,
|
|
/// The agent's Obsidian-style markdown vault — a "second brain" the agent
|
|
/// writes notes/references into (per-agent scoped).
|
|
Vault,
|
|
}
|
|
|
|
impl FileDrive {
|
|
pub fn as_str(&self) -> &'static str {
|
|
match self {
|
|
FileDrive::Documents => "documents",
|
|
FileDrive::Received => "received",
|
|
FileDrive::Shared => "shared",
|
|
FileDrive::Vault => "vault",
|
|
}
|
|
}
|
|
|
|
/// Whether nodes on this drive belong to one agent or the whole team.
|
|
pub fn is_agent_scoped(&self) -> bool {
|
|
!matches!(self, FileDrive::Shared)
|
|
}
|
|
}
|
|
|
|
impl std::str::FromStr for FileDrive {
|
|
type Err = String;
|
|
|
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
match s {
|
|
"documents" => Ok(FileDrive::Documents),
|
|
"received" => Ok(FileDrive::Received),
|
|
"shared" => Ok(FileDrive::Shared),
|
|
"vault" => Ok(FileDrive::Vault),
|
|
other => Err(format!("unknown drive: {other}")),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// One entry in a drive (spec §14 FileNode).
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
pub struct FileNode {
|
|
pub id: uuid::Uuid,
|
|
pub workspace_id: WorkspaceId,
|
|
/// `None` on the shared drive.
|
|
pub agent_id: Option<AgentId>,
|
|
pub drive: FileDrive,
|
|
pub path: String,
|
|
pub size: i64,
|
|
pub blob_ref: String,
|
|
}
|
|
|
|
/// An AI coworker (spec §14 Agent). The `system_prompt` is the Settings
|
|
/// "Job Description" textarea verbatim (§7.7).
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
pub struct Agent {
|
|
pub id: AgentId,
|
|
pub workspace_id: WorkspaceId,
|
|
pub name: String,
|
|
pub job_title: String,
|
|
pub system_prompt: String,
|
|
pub avatar: String,
|
|
pub accent: String,
|
|
pub wallpaper: String,
|
|
pub managed_by: UserId,
|
|
pub status: AgentStatus,
|
|
}
|