//! 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, /// Conversation id **assigned** by this launch, when the agent's profile /// supports session assignment and the hosting cell had none yet (T4b). The /// front persists it on the leaf (`setCellConversation`) so the next open /// resumes. `None` for a plain terminal, a resume, or a degraded launch. #[serde(skip_serializing_if = "Option::is_none")] pub assigned_conversation_id: Option, /// How the frontend should render the cell hosting this session (§17.6): /// [`CellKind::Chat`] for a structured AI session (an `AgentChatView` driven by /// `agent_send`/`reattach_agent_chat`), [`CellKind::Pty`] for a raw terminal /// (xterm). **Derived**, not a layout field: it follows the presence of a /// structured session descriptor on the launch output — a single source of /// truth. Plain terminals and the non-launch construction paths default to /// [`CellKind::Pty`] (the historical, non-breaking shape: a PTY session DTO /// always serialises with `cellKind: "pty"`). pub cell_kind: CellKind, } /// Whether a session's hosting cell renders as a structured chat view or a raw /// terminal (ARCHITECTURE §17.6). Serialises as `"chat"` / `"pty"` on the wire; /// the frontend `LayoutGrid` switches `AgentChatView` vs `TerminalView` on it. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub enum CellKind { /// A raw PTY terminal cell (xterm) — the historical default. Pty, /// A structured AI chat cell (`AgentChatView`), backed by an `AgentSession`. Chat, } 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, assigned_conversation_id: None, cell_kind: CellKind::Pty, } } } /// Response DTO for `reattach_terminal`: the retained scrollback of a still-live /// session, repainted into the re-mounting xterm before the new output stream is /// wired. Bytes are serialised as a number array, matching the PTY output channel. #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct ReattachResultDto { /// The session that was re-attached (echoed back for the frontend). pub session_id: String, /// The most-recent retained output bytes (scrollback ring buffer). pub scrollback: Vec, } /// 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, }, /// Record/clear the persistent CLI conversation id on a leaf (T4b). #[serde(rename_all = "camelCase")] SetCellConversation { /// Hosting leaf. target: String, /// Conversation id, or `null` to clear. #[serde(default)] conversation_id: 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()?, }, Self::SetCellConversation { target, conversation_id, } => LayoutOperation::SetCellConversation { target: parse_node_id(&target)?, conversation_id, }, }) } } /// 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, } } } // --------------------------------------------------------------------------- // Embedder profiles & engines (LOT C2 — §14.5.3) // --------------------------------------------------------------------------- use application::{ DismissChoice, DismissEmbedderSuggestionInput, EmbedderEnginesView, ListEmbedderProfilesOutput, OnnxModelView, SaveEmbedderProfileInput, SaveEmbedderProfileOutput, }; use domain::profile::EmbedderProfile; /// An embedder profile crossing the wire. [`EmbedderProfile`] already serialises /// camelCase (`id, name, strategy, model?, endpoint?, apiKeyEnv?, dimension`), so we /// embed it directly — the TS mirror matches this shape. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(transparent)] pub struct EmbedderProfileDto(pub EmbedderProfile); /// A list of embedder profiles (camelCase array on the wire). #[derive(Debug, Clone, Serialize)] #[serde(transparent)] pub struct EmbedderProfileListDto(pub Vec); impl From> for EmbedderProfileListDto { fn from(v: Vec) -> Self { Self(v.into_iter().map(EmbedderProfileDto).collect()) } } impl From for EmbedderProfileListDto { fn from(out: ListEmbedderProfilesOutput) -> Self { out.profiles.into() } } impl From for EmbedderProfileDto { fn from(out: SaveEmbedderProfileOutput) -> Self { Self(out.profile) } } /// Request DTO for `save_embedder_profile`: the embedder profile to upsert. /// /// Carries the [`EmbedderProfile`] fields directly (the entity validates them in the /// use case). Deserialised camelCase to match the persisted/domain shape. #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SaveEmbedderProfileRequestDto { /// The embedder profile to upsert. pub profile: EmbedderProfile, } impl From for SaveEmbedderProfileInput { fn from(dto: SaveEmbedderProfileRequestDto) -> Self { let EmbedderProfile { id, name, strategy, model, endpoint, api_key_env, dimension, } = dto.profile; Self { id, name, strategy, model, endpoint, api_key_env, dimension, } } } /// One recommendable local ONNX model on the wire (camelCase). #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct OnnxModelInfoDto { /// Stable model id accepted by a `localOnnx` profile's `model` field. pub id: String, /// Human-readable name for the UI. pub display_name: String, /// Length of the vectors this model produces. pub dimension: usize, /// Approximate download/disk size in megabytes. pub approx_size_mb: u32, /// Whether this is the recommended default model. pub recommended: bool, } impl From for OnnxModelInfoDto { fn from(m: OnnxModelView) -> Self { Self { id: m.id, display_name: m.display_name, dimension: m.dimension, approx_size_mb: m.approx_size_mb, recommended: m.recommended, } } } /// Response DTO for `describe_embedder_engines` (drives the "configure an embedder?" /// UI). All-camelCase wire shape. #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct EmbedderEnginesDto { /// The curated catalogue of recommendable local ONNX models. pub recommended_onnx: Vec, /// Whether an Ollama-style local embedding server was detected (best-effort). pub ollama_detected: bool, /// Ids of the recommended ONNX models already present in the local cache. pub onnx_cached_models: Vec, /// Whether the HTTP capability (`localServer`/`api`) is compiled into this binary. pub vector_http_enabled: bool, /// Whether the in-process ONNX capability (`localOnnx`) is compiled into this binary. pub vector_onnx_enabled: bool, } impl From for EmbedderEnginesDto { fn from(v: EmbedderEnginesView) -> Self { Self { recommended_onnx: v.recommended_onnx.into_iter().map(Into::into).collect(), ollama_detected: v.ollama_detected, onnx_cached_models: v.onnx_cached_models, vector_http_enabled: v.vector_http_enabled, vector_onnx_enabled: v.vector_onnx_enabled, } } } // --------------------------------------------------------------------------- // Embedder suggestion (LOT C3 — §14.5.5) // --------------------------------------------------------------------------- /// The user's response to the embedder suggestion, on the wire (camelCase: /// `"later"` | `"never"`). #[derive(Debug, Clone, Copy, Deserialize)] #[serde(rename_all = "camelCase")] pub enum DismissChoiceDto { /// "Plus tard" — re-proposable next session. Later, /// "Ne plus demander" — never again. Never, } impl From for DismissChoice { fn from(c: DismissChoiceDto) -> Self { match c { DismissChoiceDto::Later => Self::Later, DismissChoiceDto::Never => Self::Never, } } } /// Request DTO for `dismiss_embedder_suggestion`. The `project_id` is resolved to a /// project root by the command; `choice` is the user's dismissal. #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct DismissEmbedderSuggestionRequestDto { /// The project the suggestion concerned (UUID string). pub project_id: String, /// The user's choice. pub choice: DismissChoiceDto, } impl DismissEmbedderSuggestionRequestDto { /// Builds the use-case input from a resolved project root + the DTO choice. #[must_use] pub fn into_input(self, project_root: domain::ProjectPath) -> DismissEmbedderSuggestionInput { DismissEmbedderSuggestionInput { project_root, choice: self.choice.into(), } } } /// 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::{ ChangeAgentProfileOutput, CreateAgentOutput, InspectConversationOutput, LaunchAgentOutput, ListAgentsOutput, ReadAgentContextOutput, }; use domain::{Agent, TerminalSession}; /// 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 `update_project_context`. #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct UpdateProjectContextRequestDto { /// Id of the owning project. pub project_id: String, /// New project-level Markdown context, stored under `.ideai/CONTEXT.md`. 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, /// Persistent CLI conversation id currently recorded on the hosting cell, if /// any. `Some` ⇒ the launch resumes it; absent/`None` ⇒ a fresh cell (the /// launch may assign a new id when the profile supports it). #[serde(default)] pub conversation_id: Option, /// The layout leaf (node) hosting this launch. Enforces the "one live session /// per agent" invariant: a launch into a node different from where the agent /// is already running is refused (`AGENT_ALREADY_RUNNING`); the same node is /// idempotent. Absent ⇒ a fresh node is minted (and any already-live agent is /// refused). #[serde(default)] pub node_id: Option, } impl From for TerminalSessionDto { fn from(out: LaunchAgentOutput) -> Self { let assigned_conversation_id = out.assigned_conversation_id; // §17.6: the cell kind is *derived* from the launch routing. A structured // descriptor (`structured: Some(..)`) means LaunchAgent routed to an // AgentSession ⇒ chat cell; its absence means the PTY/terminal path ⇒ // terminal cell. Single source of truth, no layout migration. let cell_kind = if out.structured.is_some() { CellKind::Chat } else { CellKind::Pty }; 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, assigned_conversation_id, cell_kind, } } } /// Request DTO for `submit_agent_input` (cadrage C4 §4.2): the human Envoyer path. /// The frontend's [`InputGateway`](../../../frontend/src/ports) sends /// `{ request: { projectId, agentId, text } }`. #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SubmitAgentInputRequestDto { /// Id of the owning project. pub project_id: String, /// Id of the agent to send the human input to. pub agent_id: String, /// The text the operator typed (enqueued as a `from_human` ticket). pub text: String, } /// Request DTO for `interrupt_agent` (cadrage C4 §4.2): the Interrompre path. The /// frontend sends `{ request: { projectId, agentId } }`. #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct InterruptAgentRequestDto { /// Id of the owning project. pub project_id: String, /// Id of the agent whose running turn to preempt. pub agent_id: String, } /// Request DTO for `change_agent_profile` (§15.1): hot-swap an agent's runtime /// profile, optionally relaunching its live session in place. #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ChangeAgentProfileRequestDto { /// Id of the owning project. pub project_id: String, /// Id of the agent whose runtime profile to change. pub agent_id: String, /// Id of the new runtime profile. pub profile_id: String, /// Terminal height in rows for a possible hot relaunch. pub rows: u16, /// Terminal width in columns for a possible hot relaunch. pub cols: u16, } /// Response DTO for `change_agent_profile`: the mutated agent plus the freshly /// relaunched session when a live session was hot-swapped (absent otherwise). #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct ChangeAgentProfileDto { /// The agent now carrying the new profile. pub agent: AgentDto, /// The relaunched session, present only when a live session was swapped. #[serde(skip_serializing_if = "Option::is_none")] pub relaunched_session: Option, } impl From for ChangeAgentProfileDto { fn from(out: ChangeAgentProfileOutput) -> Self { Self { agent: AgentDto(out.agent), relaunched_session: out.relaunched.map(TerminalSessionDto::from), } } } impl From for TerminalSessionDto { fn from(s: TerminalSession) -> Self { Self { session_id: s.id.to_string(), cwd: s.cwd.as_str().to_owned(), rows: s.pty_size.rows, cols: s.pty_size.cols, assigned_conversation_id: None, cell_kind: CellKind::Pty, } } } // --------------------------------------------------------------------------- // Structured chat sessions (§17 — D4) // --------------------------------------------------------------------------- /// One incremental chunk of a structured agent reply, streamed over the chat /// session's [`tauri::ipc::Channel`] (ARCHITECTURE §17.7). The serialised wire /// twin of a [`domain::ports::ReplyEvent`]: the `agent_send` pump maps each turn /// event to one of these and pushes it to the frontend `AgentChatView`. /// /// Tagged on `kind` (camelCase: `"textDelta"` | `"toolActivity"` | `"final"`), so /// the front branches without positional parsing. `Final` is the **deterministic /// terminal** chunk of a turn — after it the turn is frozen and the stream ends. /// `Deserialize` is derived too so tests (and a mock gateway) can round-trip it. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(tag = "kind", rename_all = "camelCase")] pub enum ReplyChunk { /// An assistant text fragment (incremental chat rendering). #[serde(rename_all = "camelCase")] TextDelta { /// The text fragment. text: String, }, /// A human-readable tool-activity badge (best-effort observability). #[serde(rename_all = "camelCase")] ToolActivity { /// The human-readable activity label. label: String, }, /// The deterministic end-of-turn chunk carrying the aggregated final content. #[serde(rename_all = "camelCase")] Final { /// The aggregated final content of the turn. content: String, }, } /// Response DTO for `reattach_agent_chat`: the retained **conversation /// scrollback** of a still-live structured session, replayed into the /// re-mounting `AgentChatView` before the new reply stream is wired (§17.6). The /// typed twin of [`ReattachResultDto`] (PTY scrollback bytes) — here the /// scrollback is the ordered list of [`ReplyChunk`]s already streamed. #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct ReattachChatDto { /// The session that was re-attached (echoed back for the frontend). pub session_id: String, /// The chunks already streamed for this conversation, in order. The frontend /// replays them to rebuild the visible turns, then receives subsequent chunks /// over the freshly-registered channel. pub scrollback: Vec, } /// Request DTO for `inspect_conversation` (T7). #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct InspectConversationRequestDto { /// Id of the owning project. pub project_id: String, /// Id of the agent whose conversation is inspected. pub agent_id: String, /// The conversation id recorded on the hosting cell. pub conversation_id: String, } /// Response DTO for `inspect_conversation` (T7): the best-effort enriched /// details for a resume popup. Both fields are optional and **omitted from the /// wire when `None`** (`skip_serializing_if`), so the TypeScript side sees an /// absent key — not `null` — for a degraded inspection. #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct ConversationDetailsDto { /// A short, best-effort label for the conversation (last user message, /// truncated). Absent when it could not be extracted. #[serde(skip_serializing_if = "Option::is_none")] pub last_topic: Option, /// A best-effort cumulative token count. Absent when no usage info exists. #[serde(skip_serializing_if = "Option::is_none")] pub token_count: Option, } impl From for ConversationDetailsDto { fn from(out: InspectConversationOutput) -> Self { Self { last_topic: out.details.last_topic, token_count: out.details.token_count, } } } /// One currently-live agent and the cell hosting it, for `list_live_agents`. /// /// Lets the UI disable an agent already running in another cell (it cannot be /// launched a second time — the "one live session per agent" invariant). #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct LiveAgentDto { /// The live agent's id (UUID string). pub agent_id: String, /// The hosting layout leaf's node id (UUID string). pub node_id: String, /// The live PTY session id, used to reattach a newly-opened cell without /// respawning the agent. pub session_id: String, } /// Response DTO for `list_live_agents` (transparent array on the wire). #[derive(Debug, Clone, Serialize)] #[serde(transparent)] pub struct LiveAgentListDto(pub Vec); impl LiveAgentListDto { /// Builds the wire list from the registry's `(AgentId, NodeId, SessionId)` tuples. /// /// De-duplicates by `agent_id`: the "one live session per agent" invariant /// guarantees an agent is live in at most one registry (PTY **or** /// structured), but the aggregated input could in theory carry the same agent /// twice; the UI contract is a dup-free set (each agent appears once), so we /// keep the first occurrence and drop any later one for the same agent. #[must_use] pub fn from_pairs(pairs: Vec<(AgentId, NodeId, domain::SessionId)>) -> Self { let mut seen = std::collections::HashSet::new(); Self( pairs .into_iter() .filter(|(agent_id, _, _)| seen.insert(*agent_id)) .map(|(agent_id, node_id, session_id)| LiveAgentDto { agent_id: agent_id.to_string(), node_id: node_id.to_string(), session_id: session_id.to_string(), }) .collect(), ) } } /// Request DTO for `attach_live_agent`: bind an already-running agent session to /// a visible layout cell without spawning a new process. #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AttachLiveAgentRequestDto { /// Id of the owning project (validated for symmetry and future scoping). pub project_id: String, /// Id of the already-running agent. pub agent_id: String, /// Layout leaf that should display the live session. pub node_id: String, } /// 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}"), }) } // --------------------------------------------------------------------------- // Resumable agents (§15.2 — Chantier B2) // --------------------------------------------------------------------------- use application::{ListResumableAgentsOutput, ResumableAgent}; /// One resumable agent cell, as seen by the frontend (§15.2). /// /// Mirrors [`ResumableAgent`]: the agent's identity + its host cell, the CLI /// conversation id to resume (absent ⇒ fresh relaunch), the `was_running` flag /// frozen at close, and whether the agent's profile can resume a CLI /// conversation. `conversationId` is **omitted from the wire when `None`** /// (`skip_serializing_if`), so the TypeScript side sees an absent key — not /// `null`. #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct ResumableAgentDto { /// The resumable agent's id (UUID string). pub agent_id: String, /// The agent's display name (resolved from the manifest). pub name: String, /// The host layout leaf where the agent is relaunched/resumed (UUID string). pub node_id: String, /// Persistent CLI conversation id carried by the cell. Absent ⇒ fresh /// relaunch (no history to resume). #[serde(skip_serializing_if = "Option::is_none")] pub conversation_id: Option, /// The `agent_was_running` flag frozen at the cell's close. pub was_running: bool, /// `true` when the agent's profile carries a usable resume strategy. pub resume_supported: bool, } impl From for ResumableAgentDto { fn from(r: ResumableAgent) -> Self { Self { agent_id: r.agent_id.to_string(), name: r.name, node_id: r.node_id.to_string(), conversation_id: r.conversation_id, was_running: r.was_running, resume_supported: r.resume_supported, } } } /// Response DTO for `list_resumable_agents` (§15.2). #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct ResumableAgentListDto { /// The resumable agent cells, in layout-traversal order. pub resumable: Vec, } impl From for ResumableAgentListDto { fn from(out: ListResumableAgentsOutput) -> Self { Self { resumable: out .resumable .into_iter() .map(ResumableAgentDto::from) .collect(), } } } // --------------------------------------------------------------------------- // 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}"), }) } // --------------------------------------------------------------------------- // Skills (L12) // --------------------------------------------------------------------------- use application::{CreateSkillOutput, ListSkillsOutput, UpdateSkillOutput}; use domain::{Skill, SkillId, SkillScope}; /// A skill crossing the wire. [`Skill`] already serialises camelCase /// (`id`, `name`, `contentMd`, `scope` as `"global"`/`"project"`), so we embed /// it directly — the TS mirror matches this shape. #[derive(Debug, Clone, Serialize)] #[serde(transparent)] pub struct SkillDto(pub Skill); /// A list of skills (transparent array on the wire). #[derive(Debug, Clone, Serialize)] #[serde(transparent)] pub struct SkillListDto(pub Vec); impl From for SkillListDto { fn from(out: ListSkillsOutput) -> Self { Self(out.skills.into_iter().map(SkillDto).collect()) } } impl From for SkillDto { fn from(out: CreateSkillOutput) -> Self { Self(out.skill) } } impl From for SkillDto { fn from(out: UpdateSkillOutput) -> Self { Self(out.skill) } } /// Request DTO for `create_skill`. #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CreateSkillRequestDto { /// Owning project (resolved to a root; ignored on disk for `Global`). pub project_id: String, /// Display name. pub name: String, /// Initial Markdown content. pub content: String, /// Scope the skill is created in. pub scope: SkillScope, } /// Request DTO for `update_skill`. #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct UpdateSkillRequestDto { /// Owning project (resolved to a root; ignored on disk for `Global`). pub project_id: String, /// Id of the skill to update. pub skill_id: String, /// Scope the skill lives in. pub scope: SkillScope, /// New Markdown content. pub content: String, } /// Request DTO for `assign_skill_to_agent`. #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AssignSkillRequestDto { /// Owning project. pub project_id: String, /// Agent receiving the skill. pub agent_id: String, /// Skill to assign. pub skill_id: String, /// Scope of the skill (recorded alongside the ref). pub scope: SkillScope, } /// Request DTO for `unassign_skill_from_agent`. #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct UnassignSkillRequestDto { /// Owning project. pub project_id: String, /// Agent losing the skill. pub agent_id: String, /// Skill to unassign. pub skill_id: String, } /// Parses a skill-id string (UUID) coming from the frontend. /// /// # Errors /// Returns an [`ErrorDto`] with code `INVALID` if the string is not a UUID. pub fn parse_skill_id(raw: &str) -> Result { uuid::Uuid::parse_str(raw) .map(SkillId::from_uuid) .map_err(|_| ErrorDto { code: "INVALID".to_owned(), message: format!("invalid skill id: {raw}"), }) } // --------------------------------------------------------------------------- // Memory (LOT A — §14.5.1) // --------------------------------------------------------------------------- use application::{ CreateMemoryOutput, GetMemoryOutput, ListMemoriesOutput, ReadMemoryIndexOutput, RecallMemoryOutput, ResolveMemoryLinksOutput, UpdateMemoryOutput, }; use domain::{Memory, MemoryIndexEntry, MemorySlug, MemoryType}; /// A memory note crossing the wire. /// /// Built explicitly from [`Memory`] (its `body` is [`domain::MarkdownDoc`], not /// directly a JSON string) so the TS mirror gets a flat camelCase shape. #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct MemoryDto { /// The note's slug (its identity, also the file stem). pub name: String, /// One-line description (the index hook). pub description: String, /// The note's kind. pub r#type: MemoryType, /// Markdown body of the note. pub content: String, } impl From for MemoryDto { fn from(memory: Memory) -> Self { Self { name: memory.frontmatter.name.as_str().to_owned(), description: memory.frontmatter.description, r#type: memory.frontmatter.r#type, content: memory.body.into_string(), } } } impl From for MemoryDto { fn from(out: CreateMemoryOutput) -> Self { Self::from(out.memory) } } impl From for MemoryDto { fn from(out: UpdateMemoryOutput) -> Self { Self::from(out.memory) } } impl From for MemoryDto { fn from(out: GetMemoryOutput) -> Self { Self::from(out.memory) } } /// A list of memory notes (transparent array on the wire). #[derive(Debug, Clone, Serialize)] #[serde(transparent)] pub struct MemoryListDto(pub Vec); impl From for MemoryListDto { fn from(out: ListMemoriesOutput) -> Self { Self(out.memories.into_iter().map(MemoryDto::from).collect()) } } /// One row of the structured `MEMORY.md` index crossing the wire. #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct MemoryIndexEntryDto { /// The note's slug. pub slug: String, /// The note's display title. pub title: String, /// The one-line hook (the frontmatter description). pub hook: String, /// The note's kind. pub r#type: MemoryType, } impl From for MemoryIndexEntryDto { fn from(entry: MemoryIndexEntry) -> Self { Self { slug: entry.slug.as_str().to_owned(), title: entry.title, hook: entry.hook, r#type: entry.r#type, } } } /// The structured memory index (transparent array on the wire). #[derive(Debug, Clone, Serialize)] #[serde(transparent)] pub struct MemoryIndexDto(pub Vec); impl From for MemoryIndexDto { fn from(out: ReadMemoryIndexOutput) -> Self { Self( out.entries .into_iter() .map(MemoryIndexEntryDto::from) .collect(), ) } } impl From for MemoryIndexDto { fn from(out: RecallMemoryOutput) -> Self { Self( out.entries .into_iter() .map(MemoryIndexEntryDto::from) .collect(), ) } } /// A note's resolved outgoing links — the target slugs (transparent array). #[derive(Debug, Clone, Serialize)] #[serde(transparent)] pub struct MemoryLinksDto(pub Vec); impl From for MemoryLinksDto { fn from(out: ResolveMemoryLinksOutput) -> Self { Self( out.links .into_iter() .map(|link| link.target.as_str().to_owned()) .collect(), ) } } /// Request DTO for `create_memory`. #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CreateMemoryRequestDto { /// Owning project (resolved to a root). pub project_id: String, /// Raw slug for the new note. pub name: String, /// One-line description (the index hook). pub description: String, /// The note's kind. pub r#type: MemoryType, /// Markdown body of the note. pub content: String, } /// Request DTO for `update_memory`. #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct UpdateMemoryRequestDto { /// Owning project (resolved to a root). pub project_id: String, /// Slug of the note to replace. pub slug: String, /// New description (the index hook). pub description: String, /// New kind. pub r#type: MemoryType, /// New Markdown body. pub content: String, } /// Request DTO for `recall_memory` (LOT B — §14.5.2). #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct RecallMemoryRequestDto { /// Owning project (resolved to a root). pub project_id: String, /// The recall query (often the agent's current working context). pub text: String, /// Approximate token budget bounding the recalled entries (`0` ⇒ empty). pub token_budget: usize, } /// Parses a memory slug string coming from the frontend. /// /// # Errors /// Returns an [`ErrorDto`] with code `INVALID` if the string is not a valid /// kebab-case slug. pub fn parse_memory_slug(raw: &str) -> Result { MemorySlug::new(raw).map_err(|_| ErrorDto { code: "INVALID".to_owned(), message: format!("invalid memory slug: {raw}"), }) }