//! Data Transfer Objects crossing the IPC boundary. //! //! Convention (frozen here for L1, see L1-ipc-bridge.md "points d'attention"): //! **all IPC payloads are `camelCase`** via `#[serde(rename_all = "camelCase")]`. //! Rust uses `snake_case` fields; serde renames them on the wire so the //! TypeScript side sees idiomatic camelCase. This matches the persisted-domain //! JSON convention already used in the domain (`agents.json` etc.). use serde::{Deserialize, Serialize}; use application::{ AppError, CreateProjectInput, CreateProjectOutput, GitGraphOutput, HealthInput, HealthReport, LayoutKind, ListProjectsOutput, OpenProjectOutput, }; use domain::{Project, ProjectId}; /// Request DTO for the `health` command. #[derive(Debug, Clone, Default, Deserialize)] #[serde(rename_all = "camelCase")] pub struct HealthRequestDto { /// Optional note echoed back by the use case. #[serde(default)] pub note: Option, } impl From for HealthInput { fn from(dto: HealthRequestDto) -> Self { Self { note: dto.note } } } /// Response DTO for the `health` command. #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct HealthResponseDto { /// Application version. pub version: String, /// Liveness flag. pub alive: bool, /// Server time in epoch milliseconds. pub time_millis: i64, /// Correlation id for this call. pub correlation_id: String, /// Echoed note, if any. pub note: Option, } impl From for HealthResponseDto { fn from(r: HealthReport) -> Self { Self { version: r.version, alive: r.alive, time_millis: r.time_millis, correlation_id: r.correlation_id, note: r.note, } } } /// Error DTO returned to the frontend in the `Err` arm of every command. /// /// `code` is a stable machine-readable string (see [`AppError::code`]); the /// frontend branches on it without parsing `message`. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ErrorDto { /// Stable error code, e.g. `NOT_FOUND`, `INVALID`. pub code: String, /// Human-readable message. pub message: String, } impl From for ErrorDto { fn from(e: AppError) -> Self { Self { code: e.code().to_owned(), message: e.to_string(), } } } // --------------------------------------------------------------------------- // Projects (L2) // --------------------------------------------------------------------------- /// A project as seen by the frontend (camelCase wire shape). /// /// `remote` is the domain [`domain::RemoteRef`], which already serialises /// camelCase + tagged (`kind`), so we embed it directly. #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct ProjectDto { /// Stable project id (UUID string). pub id: String, /// Display name. pub name: String, /// Absolute project root. pub root: String, /// Where the project lives (`{ "kind": "local" }`, `ssh`, `wsl`). pub remote: domain::RemoteRef, /// Creation timestamp, epoch milliseconds. pub created_at: i64, } impl From for ProjectDto { fn from(p: Project) -> Self { Self { id: p.id.to_string(), name: p.name, root: p.root.as_str().to_owned(), remote: p.remote, created_at: p.created_at, } } } /// Request DTO for `create_project`. #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CreateProjectRequestDto { /// Display name. pub name: String, /// Absolute project root. pub root: String, /// Optional remote reference; defaults to local when omitted. #[serde(default)] pub remote: Option, /// Optional default profile id. #[serde(default)] pub default_profile_id: Option, } impl From for CreateProjectInput { fn from(dto: CreateProjectRequestDto) -> Self { Self { name: dto.name, root: dto.root, remote: dto.remote, default_profile_id: dto.default_profile_id, } } } impl From for ProjectDto { fn from(out: CreateProjectOutput) -> Self { out.project.into() } } impl From for ProjectDto { fn from(out: OpenProjectOutput) -> Self { out.project.into() } } /// Parses a project-id string (UUID) coming from the frontend. /// /// # Errors /// Returns an [`ErrorDto`] with code `INVALID` if the string is not a UUID. pub fn parse_project_id(raw: &str) -> Result { uuid::Uuid::parse_str(raw) .map(ProjectId::from_uuid) .map_err(|_| ErrorDto { code: "INVALID".to_owned(), message: format!("invalid project id: {raw}"), }) } /// Response DTO for `list_projects`. #[derive(Debug, Clone, Serialize)] #[serde(transparent)] pub struct ProjectListDto(pub Vec); impl From for ProjectListDto { fn from(out: ListProjectsOutput) -> Self { Self(out.projects.into_iter().map(ProjectDto::from).collect()) } } // --------------------------------------------------------------------------- // Terminals (L3) // --------------------------------------------------------------------------- use application::{ CloseTerminalInput, CloseTerminalOutput, OpenTerminalInput, OpenTerminalOutput, ResizeTerminalInput, WriteToTerminalInput, }; use domain::SessionId; /// Request DTO for `open_terminal`. #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct OpenTerminalRequestDto { /// Working directory (typically the project root). pub cwd: String, /// Initial terminal height in rows. pub rows: u16, /// Initial terminal width in columns. pub cols: u16, /// Optional explicit command; defaults to the platform shell when omitted. #[serde(default)] pub command: Option, /// Optional arguments for the command. #[serde(default)] pub args: Vec, } impl From for OpenTerminalInput { fn from(dto: OpenTerminalRequestDto) -> Self { Self { cwd: dto.cwd, rows: dto.rows, cols: dto.cols, command: dto.command, args: dto.args, node_id: None, } } } /// Response DTO for `open_terminal`: the freshly-opened session. #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct TerminalSessionDto { /// Stable session id (UUID string) — used for write/resize/close + the /// output channel. pub session_id: String, /// Working directory the shell runs in. pub cwd: String, /// Current rows. pub rows: u16, /// Current cols. pub cols: u16, } impl From for TerminalSessionDto { fn from(out: OpenTerminalOutput) -> Self { let s = out.session; Self { session_id: s.id.to_string(), cwd: s.cwd.as_str().to_owned(), rows: s.pty_size.rows, cols: s.pty_size.cols, } } } /// Request DTO for `write_terminal`. #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct WriteTerminalRequestDto { /// Target session id. pub session_id: String, /// Bytes to write (xterm keystrokes). pub data: Vec, } impl WriteTerminalRequestDto { /// Converts to the use-case input, parsing the session id. /// /// # Errors /// [`ErrorDto`] with code `INVALID` if the id is malformed. pub fn into_input(self) -> Result { Ok(WriteToTerminalInput { session_id: parse_session_id(&self.session_id)?, data: self.data, }) } } /// Request DTO for `resize_terminal`. #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ResizeTerminalRequestDto { /// Target session id. pub session_id: String, /// New rows. pub rows: u16, /// New cols. pub cols: u16, } impl ResizeTerminalRequestDto { /// Converts to the use-case input, parsing the session id. /// /// # Errors /// [`ErrorDto`] with code `INVALID` if the id is malformed. pub fn into_input(self) -> Result { Ok(ResizeTerminalInput { session_id: parse_session_id(&self.session_id)?, rows: self.rows, cols: self.cols, }) } } /// Response DTO for `close_terminal`. #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct TerminalClosedDto { /// Exit code, if the process reported one. pub code: Option, } impl From for TerminalClosedDto { fn from(out: CloseTerminalOutput) -> Self { Self { code: out.code } } } /// Builds a [`CloseTerminalInput`] from a raw session-id string. /// /// # Errors /// [`ErrorDto`] with code `INVALID` if the id is malformed. pub fn parse_close_terminal(raw: &str) -> Result { Ok(CloseTerminalInput { session_id: parse_session_id(raw)?, }) } /// Parses a session-id string (UUID) coming from the frontend. /// /// # Errors /// Returns an [`ErrorDto`] with code `INVALID` if the string is not a UUID. pub fn parse_session_id(raw: &str) -> Result { uuid::Uuid::parse_str(raw) .map(SessionId::from_uuid) .map_err(|_| ErrorDto { code: "INVALID".to_owned(), message: format!("invalid session id: {raw}"), }) } // --------------------------------------------------------------------------- // Layout (L4 + #4 management + #3 per-cell agent) // --------------------------------------------------------------------------- use application::{ CreateLayoutOutput, DeleteLayoutOutput, LayoutInfo, LayoutOperation, ListLayoutsOutput, LoadLayoutOutput, MutateLayoutOutput, }; use domain::{AgentId, Direction, LayoutId, LayoutTree, NodeId}; /// Response DTO carrying a layout tree. /// /// [`LayoutTree`] already serialises camelCase + tagged (its enum uses /// `#[serde(tag = "type", content = "node")]`), so we embed it directly; the /// TypeScript mirror in `@/domain` matches this shape. #[derive(Debug, Clone, Serialize)] #[serde(transparent)] pub struct LayoutDto(pub LayoutTree); impl From for LayoutDto { fn from(out: LoadLayoutOutput) -> Self { Self(out.layout) } } impl From for LayoutDto { fn from(out: MutateLayoutOutput) -> Self { Self(out.layout) } } /// A layout operation as sent by the frontend (tagged on `type`, camelCase). /// /// Mirrors [`LayoutOperation`]; node/session ids cross the wire as UUID strings /// and are parsed here. `direction` reuses the domain [`Direction`] (which /// already serialises `"row"`/`"column"`). #[derive(Debug, Clone, Deserialize)] #[serde(tag = "type", rename_all = "camelCase")] pub enum LayoutOperationDto { /// Split a leaf into a two-child split. #[serde(rename_all = "camelCase")] Split { /// Leaf to split. target: String, /// Row (columns) or Column (rows). direction: Direction, /// Id for the new sibling leaf. new_leaf: String, /// Id for the wrapping split container. container: String, }, /// Collapse a split container back to one child. #[serde(rename_all = "camelCase")] Merge { /// Split container to collapse. container: String, /// Index of the child to keep. keep_index: usize, }, /// Reassign a split's child weights. #[serde(rename_all = "camelCase")] Resize { /// Split container to resize. container: String, /// New weights (one per child). weights: Vec, }, /// Move a session from one leaf to another. #[serde(rename_all = "camelCase")] Move { /// Source leaf. from: String, /// Target (empty) leaf. to: String, }, /// Attach/detach a session to/from a leaf. #[serde(rename_all = "camelCase")] SetSession { /// Hosting leaf. target: String, /// Session id, or `null` to clear. #[serde(default)] session: Option, }, /// Attach/detach an agent to/from a leaf (#3 per-cell agent). #[serde(rename_all = "camelCase")] SetCellAgent { /// Hosting leaf. target: String, /// Agent id, or `null` to clear. #[serde(default)] agent: Option, }, } impl LayoutOperationDto { /// Converts to the use-case operation, parsing all ids. /// /// # Errors /// [`ErrorDto`] with code `INVALID` if any id is malformed. pub fn into_operation(self) -> Result { Ok(match self { Self::Split { target, direction, new_leaf, container, } => LayoutOperation::Split { target: parse_node_id(&target)?, direction, new_leaf: parse_node_id(&new_leaf)?, container: parse_node_id(&container)?, }, Self::Merge { container, keep_index, } => LayoutOperation::Merge { container: parse_node_id(&container)?, keep_index, }, Self::Resize { container, weights } => LayoutOperation::Resize { container: parse_node_id(&container)?, weights, }, Self::Move { from, to } => LayoutOperation::Move { from: parse_node_id(&from)?, to: parse_node_id(&to)?, }, Self::SetSession { target, session } => LayoutOperation::SetSession { target: parse_node_id(&target)?, session: session.as_deref().map(parse_session_id).transpose()?, }, Self::SetCellAgent { target, agent } => LayoutOperation::SetCellAgent { target: parse_node_id(&target)?, agent: agent.as_deref().map(parse_agent_id).transpose()?, }, }) } } /// Parses a node-id string (UUID) coming from the frontend. /// /// # Errors /// Returns an [`ErrorDto`] with code `INVALID` if the string is not a UUID. pub fn parse_node_id(raw: &str) -> Result { uuid::Uuid::parse_str(raw) .map(NodeId::from_uuid) .map_err(|_| ErrorDto { code: "INVALID".to_owned(), message: format!("invalid node id: {raw}"), }) } /// Parses a layout-id string (UUID) coming from the frontend. /// /// # Errors /// Returns an [`ErrorDto`] with code `INVALID` if the string is not a UUID. pub fn parse_layout_id(raw: &str) -> Result { uuid::Uuid::parse_str(raw) .map(LayoutId::from_uuid) .map_err(|_| ErrorDto { code: "INVALID".to_owned(), message: format!("invalid layout id: {raw}"), }) } // --------------------------------------------------------------------------- // Layouts (#4) — management DTOs // --------------------------------------------------------------------------- /// Lightweight layout descriptor (id + name + kind), for the tab bar. #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct LayoutInfoDto { /// Stable layout id (UUID string). pub id: String, /// Display name. pub name: String, /// Layout kind: `"terminal"` or `"gitGraph"`. pub kind: String, } impl From for LayoutInfoDto { fn from(info: LayoutInfo) -> Self { let kind = match info.kind { LayoutKind::Terminal => "terminal", LayoutKind::GitGraph => "gitGraph", } .to_owned(); Self { id: info.id.to_string(), name: info.name, kind, } } } /// Response DTO for `list_layouts`. #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct ListLayoutsDto { /// All named layouts (id + name), in order. pub layouts: Vec, /// The id of the currently active layout. pub active_id: String, } impl From for ListLayoutsDto { fn from(out: ListLayoutsOutput) -> Self { Self { layouts: out.layouts.into_iter().map(LayoutInfoDto::from).collect(), active_id: out.active_id.to_string(), } } } /// Response DTO for `create_layout`. #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct CreateLayoutResultDto { /// The id minted for the new layout. pub layout_id: String, } impl From for CreateLayoutResultDto { fn from(out: CreateLayoutOutput) -> Self { Self { layout_id: out.layout_id.to_string(), } } } /// Response DTO for `delete_layout`. #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct DeleteLayoutResultDto { /// The active layout after the deletion. pub active_id: String, } impl From for DeleteLayoutResultDto { fn from(out: DeleteLayoutOutput) -> Self { Self { active_id: out.active_id.to_string(), } } } /// Request DTO for `create_layout`. #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CreateLayoutRequestDto { /// Owning project id. pub project_id: String, /// Display name for the new layout. pub name: String, /// Optional layout kind: `"terminal"` (default) or `"gitGraph"`. #[serde(default)] pub kind: Option, } impl CreateLayoutRequestDto { /// Parses the optional `kind` string into a [`LayoutKind`]. /// /// # Errors /// [`ErrorDto`] with code `INVALID` if the value is not a known kind. pub fn parse_kind(&self) -> Result { match self.kind.as_deref() { None | Some("terminal") => Ok(LayoutKind::Terminal), Some("gitGraph") => Ok(LayoutKind::GitGraph), Some(other) => Err(ErrorDto { code: "INVALID".to_owned(), message: format!("unknown layout kind: {other}"), }), } } } /// Request DTO for `rename_layout`. #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct RenameLayoutRequestDto { /// Owning project id. pub project_id: String, /// Layout to rename. pub layout_id: String, /// New display name. pub name: String, } /// Request DTO for `delete_layout`. #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct DeleteLayoutRequestDto { /// Owning project id. pub project_id: String, /// Layout to delete. pub layout_id: String, } /// Request DTO for `set_active_layout`. #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SetActiveLayoutRequestDto { /// Owning project id. pub project_id: String, /// Layout to make active. pub layout_id: String, } // --------------------------------------------------------------------------- // Profiles & first-run (L5) // --------------------------------------------------------------------------- use application::{ ConfigureProfilesInput, ConfigureProfilesOutput, DeleteProfileInput, DetectProfilesInput, DetectProfilesOutput, FirstRunStateOutput, ListProfilesOutput, ProfileAvailability, ReferenceProfilesOutput, SaveProfileInput, SaveProfileOutput, }; use domain::profile::AgentProfile; use domain::ProfileId; /// A profile crossing the wire. [`AgentProfile`] already serialises camelCase /// (id, name, command, args, `contextInjection{strategy,…}`, detect, /// `cwdTemplate`), so we embed it directly — the TS mirror matches this shape. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(transparent)] pub struct ProfileDto(pub AgentProfile); /// A list of profiles (camelCase array on the wire). #[derive(Debug, Clone, Serialize)] #[serde(transparent)] pub struct ProfileListDto(pub Vec); impl From> for ProfileListDto { fn from(v: Vec) -> Self { Self(v.into_iter().map(ProfileDto).collect()) } } impl From for ProfileListDto { fn from(out: ListProfilesOutput) -> Self { out.profiles.into() } } impl From for ProfileListDto { fn from(out: ReferenceProfilesOutput) -> Self { out.profiles.into() } } impl From for ProfileDto { fn from(out: SaveProfileOutput) -> Self { Self(out.profile) } } /// Request DTO for `detect_profiles`: the candidate profiles to probe. #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct DetectProfilesRequestDto { /// Candidate profiles whose `detect` command should be run. pub candidates: Vec, } impl From for DetectProfilesInput { fn from(dto: DetectProfilesRequestDto) -> Self { Self { candidates: dto.candidates, } } } /// One availability result (`profile` + whether its CLI is installed). #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct ProfileAvailabilityDto { /// The probed profile. pub profile: AgentProfile, /// Whether the CLI was detected (exit code 0). pub available: bool, } impl From for ProfileAvailabilityDto { fn from(a: ProfileAvailability) -> Self { Self { profile: a.profile, available: a.available, } } } /// Response DTO for `detect_profiles`. #[derive(Debug, Clone, Serialize)] #[serde(transparent)] pub struct DetectProfilesResponseDto(pub Vec); impl From for DetectProfilesResponseDto { fn from(out: DetectProfilesOutput) -> Self { Self(out.results.into_iter().map(Into::into).collect()) } } /// Request DTO for `save_profile`. #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SaveProfileRequestDto { /// The profile to upsert. pub profile: AgentProfile, } impl From for SaveProfileInput { fn from(dto: SaveProfileRequestDto) -> Self { Self { profile: dto.profile, } } } /// Request DTO for `configure_profiles` (closes the first run). #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ConfigureProfilesRequestDto { /// All profiles the user chose to keep. pub profiles: Vec, } impl From for ConfigureProfilesInput { fn from(dto: ConfigureProfilesRequestDto) -> Self { Self { profiles: dto.profiles, } } } impl From for ProfileListDto { fn from(out: ConfigureProfilesOutput) -> Self { out.profiles.into() } } /// Response DTO for `first_run_state`. #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct FirstRunStateDto { /// `true` when the first-run wizard should be shown. pub is_first_run: bool, /// Pre-filled reference catalogue to seed the wizard. pub reference_profiles: Vec, } impl From for FirstRunStateDto { fn from(out: FirstRunStateOutput) -> Self { Self { is_first_run: out.is_first_run, reference_profiles: out.reference_profiles, } } } /// Builds a [`DeleteProfileInput`] from a raw profile-id string. /// /// # Errors /// [`ErrorDto`] with code `INVALID` if the id is malformed. pub fn parse_delete_profile(raw: &str) -> Result { Ok(DeleteProfileInput { id: parse_profile_id(raw)?, }) } /// Parses a profile-id string (UUID) coming from the frontend. /// /// # Errors /// Returns an [`ErrorDto`] with code `INVALID` if the string is not a UUID. pub fn parse_profile_id(raw: &str) -> Result { uuid::Uuid::parse_str(raw) .map(ProfileId::from_uuid) .map_err(|_| ErrorDto { code: "INVALID".to_owned(), message: format!("invalid profile id: {raw}"), }) } // --------------------------------------------------------------------------- // Agents (L6) // --------------------------------------------------------------------------- use application::{ CreateAgentOutput, LaunchAgentOutput, ListAgentsOutput, ReadAgentContextOutput, }; use domain::Agent; /// An agent crossing the wire. [`Agent`] already serialises camelCase /// (`id`, `name`, `contextPath`, `profileId`, `origin` tagged, `synchronized`), /// so we embed it directly — the TS mirror matches this shape. #[derive(Debug, Clone, Serialize)] #[serde(transparent)] pub struct AgentDto(pub Agent); /// A list of agents (camelCase array on the wire). #[derive(Debug, Clone, Serialize)] #[serde(transparent)] pub struct AgentListDto(pub Vec); impl From for AgentListDto { fn from(out: ListAgentsOutput) -> Self { Self(out.agents.into_iter().map(AgentDto).collect()) } } impl From for AgentDto { fn from(out: CreateAgentOutput) -> Self { Self(out.agent) } } /// Request DTO for `create_agent`. #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CreateAgentRequestDto { /// Id of the owning project. pub project_id: String, /// Display name for the new agent. pub name: String, /// Runtime profile id. pub profile_id: String, /// Initial Markdown content (empty when absent). #[serde(default)] pub initial_content: Option, } /// Response DTO for `read_agent_context`. #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct ReadAgentContextResponseDto { /// The agent's Markdown context content. pub content: String, } impl From for ReadAgentContextResponseDto { fn from(out: ReadAgentContextOutput) -> Self { Self { content: out.content.into_string(), } } } /// Request DTO for `update_agent_context`. #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct UpdateAgentContextRequestDto { /// Id of the owning project. pub project_id: String, /// Id of the agent to update. pub agent_id: String, /// New Markdown content. pub content: String, } /// Request DTO for `launch_agent`. #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct LaunchAgentRequestDto { /// Id of the owning project. pub project_id: String, /// Id of the agent to launch. pub agent_id: String, /// Initial terminal height in rows. pub rows: u16, /// Initial terminal width in columns. pub cols: u16, } impl From for TerminalSessionDto { fn from(out: LaunchAgentOutput) -> Self { let s = out.session; Self { session_id: s.id.to_string(), cwd: s.cwd.as_str().to_owned(), rows: s.pty_size.rows, cols: s.pty_size.cols, } } } /// Parses an agent-id string (UUID) coming from the frontend. /// /// # Errors /// Returns an [`ErrorDto`] with code `INVALID` if the string is not a UUID. pub fn parse_agent_id(raw: &str) -> Result { uuid::Uuid::parse_str(raw) .map(AgentId::from_uuid) .map_err(|_| ErrorDto { code: "INVALID".to_owned(), message: format!("invalid agent id: {raw}"), }) } // --------------------------------------------------------------------------- // Templates & sync (L7) // --------------------------------------------------------------------------- use application::{ AgentDrift, CreateAgentFromTemplateInput, CreateTemplateInput, CreateTemplateOutput, DetectAgentDriftOutput, ListTemplatesOutput, SyncAgentWithTemplateOutput, UpdateTemplateInput, UpdateTemplateOutput, }; use domain::{AgentTemplate, TemplateId}; /// A template crossing the wire. [`AgentTemplate`] already serialises camelCase /// (`id`, `name`, `contentMd`, `version` as a number, `defaultProfileId`), /// so we embed it directly — the TS mirror matches this shape. #[derive(Debug, Clone, Serialize)] #[serde(transparent)] pub struct TemplateDto(pub AgentTemplate); /// A list of templates (transparent array on the wire). #[derive(Debug, Clone, Serialize)] #[serde(transparent)] pub struct TemplateListDto(pub Vec); impl From for TemplateListDto { fn from(out: ListTemplatesOutput) -> Self { Self(out.templates.into_iter().map(TemplateDto).collect()) } } impl From for TemplateDto { fn from(out: CreateTemplateOutput) -> Self { Self(out.template) } } impl From for TemplateDto { fn from(out: UpdateTemplateOutput) -> Self { Self(out.template) } } /// Request DTO for `create_template`. #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CreateTemplateRequestDto { /// Display name. pub name: String, /// Initial Markdown content. pub content: String, /// Default runtime profile id for agents created from this template. pub default_profile_id: String, } impl CreateTemplateRequestDto { /// Converts to the use-case input, parsing the profile id. /// /// # Errors /// [`ErrorDto`] with code `INVALID` if the profile id is malformed. pub fn into_input(self) -> Result { Ok(CreateTemplateInput { name: self.name, content: self.content, default_profile_id: parse_profile_id(&self.default_profile_id)?, }) } } /// Request DTO for `update_template`. #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct UpdateTemplateRequestDto { /// Id of the template to update. pub template_id: String, /// New Markdown content. pub content: String, } impl UpdateTemplateRequestDto { /// Converts to the use-case input, parsing the template id. /// /// # Errors /// [`ErrorDto`] with code `INVALID` if the template id is malformed. pub fn into_input(self) -> Result { Ok(UpdateTemplateInput { template_id: parse_template_id(&self.template_id)?, content: self.content, }) } } /// Parses a template-id string (UUID) coming from the frontend. /// /// # Errors /// Returns an [`ErrorDto`] with code `INVALID` if the string is not a UUID. pub fn parse_template_id(raw: &str) -> Result { uuid::Uuid::parse_str(raw) .map(TemplateId::from_uuid) .map_err(|_| ErrorDto { code: "INVALID".to_owned(), message: format!("invalid template id: {raw}"), }) } /// Request DTO for `create_agent_from_template`. #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CreateAgentFromTemplateRequestDto { /// Id of the owning project. pub project_id: String, /// Source template id. pub template_id: String, /// Optional agent name; defaults to the template's name when absent. #[serde(default)] pub name: Option, /// Whether the agent tracks the template for future syncs. pub synchronized: bool, } impl CreateAgentFromTemplateRequestDto { /// Converts to the use-case input, given the resolved project. /// /// # Errors /// [`ErrorDto`] with code `INVALID` if the template id is malformed. pub fn into_input( self, project: domain::Project, ) -> Result { Ok(CreateAgentFromTemplateInput { project, template_id: parse_template_id(&self.template_id)?, name: self.name, synchronized: self.synchronized, }) } } /// One drifting agent, as seen by the frontend. #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct AgentDriftDto { /// The drifting agent id (UUID string). pub agent_id: String, /// Version the agent is currently synced to. pub from: u64, /// Version available from the template. pub to: u64, } impl From for AgentDriftDto { fn from(d: AgentDrift) -> Self { Self { agent_id: d.agent_id.to_string(), from: d.from.get(), to: d.to.get(), } } } /// Response DTO for `detect_agent_drift` (transparent array on the wire). #[derive(Debug, Clone, Serialize)] #[serde(transparent)] pub struct AgentDriftListDto(pub Vec); impl From for AgentDriftListDto { fn from(out: DetectAgentDriftOutput) -> Self { Self(out.drifts.into_iter().map(AgentDriftDto::from).collect()) } } /// Response DTO for `sync_agent_with_template`. #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct SyncResultDto { /// Whether a sync was actually applied. pub synced: bool, /// The version the agent is now at (`null` when no sync happened). pub version: Option, } impl From for SyncResultDto { fn from(out: SyncAgentWithTemplateOutput) -> Self { Self { synced: out.synced, version: out.version.map(|v| v.get()), } } } /// Request DTO for `sync_agent_with_template`. #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SyncAgentWithTemplateRequestDto { /// Id of the owning project. pub project_id: String, /// Id of the agent to sync. pub agent_id: String, } // --------------------------------------------------------------------------- // Git (L8) // --------------------------------------------------------------------------- use application::{ GitBranchesOutput, GitCommitOutput, GitLogOutput, GitStatusOutput, }; use domain::ports::{GitCommitInfo, GitFileStatus, GraphCommit}; /// One changed path returned by `git_status`. #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct GitFileStatusDto { /// Repo-relative path. pub path: String, /// Whether the change is staged. pub staged: bool, } impl From for GitFileStatusDto { fn from(s: GitFileStatus) -> Self { Self { path: s.path, staged: s.staged, } } } /// Response DTO for `git_status` (transparent array on the wire). #[derive(Debug, Clone, Serialize)] #[serde(transparent)] pub struct GitStatusListDto(pub Vec); impl From for GitStatusListDto { fn from(out: GitStatusOutput) -> Self { Self(out.entries.into_iter().map(GitFileStatusDto::from).collect()) } } /// A single commit summary crossing the wire. #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct GitCommitDto { /// Commit hash. pub hash: String, /// Commit message summary. pub summary: String, } impl From for GitCommitDto { fn from(c: GitCommitInfo) -> Self { Self { hash: c.hash, summary: c.summary, } } } impl From for GitCommitDto { fn from(out: GitCommitOutput) -> Self { Self::from(out.commit) } } /// Response DTO for `git_log` (transparent array on the wire). #[derive(Debug, Clone, Serialize)] #[serde(transparent)] pub struct GitCommitListDto(pub Vec); impl From for GitCommitListDto { fn from(out: GitLogOutput) -> Self { Self(out.commits.into_iter().map(GitCommitDto::from).collect()) } } /// Response DTO for `git_branches`. #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct GitBranchesDto { /// All local branches. pub branches: Vec, /// The current branch (`null` when detached or unborn). pub current: Option, } impl From for GitBranchesDto { fn from(out: GitBranchesOutput) -> Self { Self { branches: out.branches, current: out.current, } } } /// A single commit enriched for graph display. #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct GraphCommitDto { /// Full commit hash. pub hash: String, /// First line of the commit message. pub summary: String, /// Parent commit hashes. pub parents: Vec, /// Ref labels pointing at this commit (e.g. `"main"`, `"tag: v1.0"`). pub refs: Vec, /// Author name. pub author: String, /// Author timestamp in Unix seconds. pub timestamp: i64, } impl From for GraphCommitDto { fn from(c: GraphCommit) -> Self { Self { hash: c.hash, summary: c.summary, parents: c.parents, refs: c.refs, author: c.author, timestamp: c.timestamp, } } } /// Response DTO for `git_graph` (transparent array on the wire). #[derive(Debug, Clone, Serialize)] #[serde(transparent)] pub struct GraphCommitListDto(pub Vec); impl From for GraphCommitListDto { fn from(out: GitGraphOutput) -> Self { Self(out.commits.into_iter().map(GraphCommitDto::from).collect()) } } /// Request DTO for `git_stage` / `git_unstage`. #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct GitStageRequestDto { /// Id of the owning project. pub project_id: String, /// Repo-relative path to (un)stage. pub path: String, } /// Request DTO for `git_commit`. #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct GitCommitRequestDto { /// Id of the owning project. pub project_id: String, /// Commit message. pub message: String, } /// Request DTO for `git_checkout`. #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct GitCheckoutRequestDto { /// Id of the owning project. pub project_id: String, /// Branch to check out. pub branch: String, } // --------------------------------------------------------------------------- // Windows (L10) // --------------------------------------------------------------------------- use application::MoveTabToNewWindowOutput; use domain::ids::TabId; /// Response DTO for `move_tab_to_new_window`: the id minted for the new window /// (used as the new OS window's label). #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct MoveTabResultDto { /// The new window id (UUID string). pub new_window_id: String, } impl From for MoveTabResultDto { fn from(out: MoveTabToNewWindowOutput) -> Self { Self { new_window_id: out.new_window_id.to_string(), } } } /// Parses a tab-id string (UUID) coming from the frontend. /// /// # Errors /// Returns an [`ErrorDto`] with code `INVALID` if the string is not a UUID. pub fn parse_tab_id(raw: &str) -> Result { uuid::Uuid::parse_str(raw) .map(TabId::from_uuid) .map_err(|_| ErrorDto { code: "INVALID".to_owned(), message: format!("invalid tab id: {raw}"), }) }