//! `#[tauri::command]` handlers — the **driving adapters** (frontend → backend). //! //! Each handler is a thin shell: deserialise the DTO, call the use case from //! [`AppState`], map `Result` to `Result`. No business logic lives here. use tauri::ipc::Channel; use tauri::State; use application::{ AppError, CloseProjectInput, CreateAgentInput, CreateLayoutInput, DeleteAgentInput, DeleteLayoutInput, DeleteTemplateInput, DetectAgentDriftInput, GitBranchesInput, GitCheckoutInput, GitCommitInput, GitGraphInput, GitInitInput, GitLogInput, GitStagePathInput, GitStatusInput, LaunchAgentInput, ListAgentsInput, ListLayoutsInput, LoadLayoutInput, MutateLayoutInput, OpenProjectInput, ReadAgentContextInput, RenameLayoutInput, SetActiveLayoutInput, SyncAgentWithTemplateInput, UpdateAgentContextInput, }; use domain::ports::PtyHandle; use crate::dto::{ parse_agent_id, parse_close_terminal, parse_delete_profile, parse_layout_id, parse_profile_id, parse_project_id, parse_session_id, parse_template_id, AgentDriftListDto, AgentDto, AgentListDto, ConfigureProfilesRequestDto, CreateAgentFromTemplateRequestDto, CreateAgentRequestDto, CreateLayoutRequestDto, CreateLayoutResultDto, CreateProjectRequestDto, CreateTemplateRequestDto, DeleteLayoutRequestDto, DeleteLayoutResultDto, DetectProfilesRequestDto, DetectProfilesResponseDto, ErrorDto, FirstRunStateDto, GitBranchesDto, GitCheckoutRequestDto, GitCommitDto, GitCommitListDto, GitCommitRequestDto, GitStageRequestDto, GitStatusListDto, GraphCommitListDto, HealthRequestDto, HealthResponseDto, LaunchAgentRequestDto, LayoutDto, LayoutOperationDto, ListLayoutsDto, OpenTerminalRequestDto, ProfileDto, ProfileListDto, ProjectDto, ProjectListDto, ReadAgentContextResponseDto, RenameLayoutRequestDto, ResizeTerminalRequestDto, SaveProfileRequestDto, SetActiveLayoutRequestDto, SyncAgentWithTemplateRequestDto, SyncResultDto, TemplateDto, TemplateListDto, TerminalClosedDto, TerminalSessionDto, UpdateAgentContextRequestDto, UpdateTemplateRequestDto, WriteTerminalRequestDto, }; use crate::pty::{PtyBridge, PtyChunk}; use crate::state::AppState; /// `health` — trivial command validating the full IPC pipeline /// (frontend gateway → invoke → command → use case → ports → event relay). /// /// # Errors /// Returns an [`ErrorDto`] if the use case fails. #[tauri::command] pub fn health( request: Option, state: State<'_, AppState>, ) -> Result { let input = request.unwrap_or_default().into(); state .health .execute(input) .map(HealthResponseDto::from) .map_err(ErrorDto::from) } /// `create_project` — create a project from a root: init `.ideai/`, register it. /// /// # Errors /// Returns an [`ErrorDto`] (`INVALID` for a bad root/name or a duplicate /// `(remote, root)`, `FILESYSTEM`/`STORE` on I/O failure). #[tauri::command] pub async fn create_project( request: CreateProjectRequestDto, state: State<'_, AppState>, ) -> Result { state .create_project .execute(request.into()) .await .map(ProjectDto::from) .map_err(ErrorDto::from) } /// `open_project` — load a project and its `.ideai/` meta/manifest. /// /// # Errors /// Returns an [`ErrorDto`] (`INVALID` for a malformed id, `NOT_FOUND` if the /// project is unknown, `STORE` on registry I/O failure). #[tauri::command] pub async fn open_project( project_id: String, state: State<'_, AppState>, ) -> Result { let id = parse_project_id(&project_id)?; state .open_project .execute(OpenProjectInput { project_id: id }) .await .map(ProjectDto::from) .map_err(ErrorDto::from) } /// `close_project` — persist state and release resources for a project. /// /// # Errors /// Returns an [`ErrorDto`] (`INVALID` for a malformed id, `STORE` on failure). #[tauri::command] pub async fn close_project( project_id: String, state: State<'_, AppState>, ) -> Result<(), ErrorDto> { let id = parse_project_id(&project_id)?; state .close_project .execute(CloseProjectInput { project_id: id, // L2 has no UI-side workspace mutations to persist yet. workspace: None, }) .await .map(|_| ()) .map_err(ErrorDto::from) } /// `list_projects` — list the projects known to the registry. /// /// # Errors /// Returns an [`ErrorDto`] (`STORE` on registry I/O failure). #[tauri::command] pub async fn list_projects(state: State<'_, AppState>) -> Result { state .list_projects .execute() .await .map(ProjectListDto::from) .map_err(ErrorDto::from) } // --------------------------------------------------------------------------- // Terminals (L3) // --------------------------------------------------------------------------- /// `open_terminal` — spawn a PTY and wire its byte stream to the frontend. /// /// The frontend passes a per-session [`Channel`] (xterm's output sink). We: /// 1. run [`application::OpenTerminal`] (spawn the PTY, register the session), /// 2. register the channel in the [`PtyBridge`] keyed by the new session id, /// 3. start a pump that drains the PTY's blocking output stream and forwards /// each chunk through the bridge to that channel. /// /// Returns the [`TerminalSessionDto`] (its `sessionId` is what `write`/`resize`/ /// `close` reference). /// /// # Errors /// Returns an [`ErrorDto`] (`INVALID` for a bad cwd/size, `PROCESS` if the PTY /// fails to spawn or its output cannot be subscribed). #[tauri::command] pub async fn open_terminal( request: OpenTerminalRequestDto, on_output: Channel, state: State<'_, AppState>, ) -> Result { let output = state .open_terminal .execute(request.into()) .await .map_err(ErrorDto::from)?; let session_id = output.session.id; // (2) Register the xterm output channel for this session. state.pty_bridge.register(session_id, on_output); // (3) Subscribe to the PTY's byte stream and pump it to the channel. The // stream is a blocking iterator, so it runs on a dedicated OS thread; it // ends when the PTY hits EOF (process exit) or the bridge channel is gone. let handle = PtyHandle { session_id }; match state.pty_port.subscribe_output(&handle) { Ok(stream) => { let bridge: std::sync::Arc = std::sync::Arc::clone(&state.pty_bridge); std::thread::spawn(move || { for chunk in stream { if !bridge.send_output(&session_id, chunk) { break; } } // Stream ended (process exited): drop the channel registration. bridge.unregister(&session_id); }); } Err(e) => { state.pty_bridge.unregister(&session_id); return Err(ErrorDto::from(application::AppError::from(e))); } } Ok(TerminalSessionDto::from(output)) } /// `write_terminal` — forward bytes (xterm keystrokes) to a live PTY. /// /// # Errors /// Returns an [`ErrorDto`] (`INVALID` for a malformed id, `NOT_FOUND` if the /// session is unknown, `PROCESS` on PTY I/O failure). #[tauri::command] pub fn write_terminal( request: WriteTerminalRequestDto, state: State<'_, AppState>, ) -> Result<(), ErrorDto> { let input = request.into_input()?; state .write_terminal .execute(input) .map_err(ErrorDto::from) } /// `resize_terminal` — resize a live PTY. /// /// # Errors /// Returns an [`ErrorDto`] (`INVALID` for a malformed id/size, `NOT_FOUND` if the /// session is unknown, `PROCESS` on failure). #[tauri::command] pub fn resize_terminal( request: ResizeTerminalRequestDto, state: State<'_, AppState>, ) -> Result<(), ErrorDto> { let input = request.into_input()?; state .resize_terminal .execute(input) .map_err(ErrorDto::from) } /// `close_terminal` — kill a live PTY and tear down its channel. /// /// # Errors /// Returns an [`ErrorDto`] (`INVALID` for a malformed id, `NOT_FOUND` if the /// session is unknown, `PROCESS` if the kill fails). #[tauri::command] pub async fn close_terminal( session_id: String, state: State<'_, AppState>, ) -> Result { let input = parse_close_terminal(&session_id)?; let sid = parse_session_id(&session_id)?; let result = state .close_terminal .execute(input) .await .map(TerminalClosedDto::from) .map_err(ErrorDto::from); // Tear down the channel regardless of kill outcome. state.pty_bridge.unregister(&sid); result } // --------------------------------------------------------------------------- // Layout (L4) // --------------------------------------------------------------------------- /// `load_layout` — read a project's named layout (the active one when `layout_id` /// is omitted). /// /// # Errors /// Returns an [`ErrorDto`] (`INVALID` for a malformed id, `NOT_FOUND` if the /// project or layout is unknown, `STORE` on registry I/O failure). #[tauri::command] pub async fn load_layout( project_id: String, layout_id: Option, state: State<'_, AppState>, ) -> Result { let id = parse_project_id(&project_id)?; let lid = layout_id.as_deref().map(parse_layout_id).transpose()?; state .load_layout .execute(LoadLayoutInput { project_id: id, layout_id: lid, }) .await .map(LayoutDto::from) .map_err(ErrorDto::from) } /// `mutate_layout` — apply a split/merge/resize/move/setSession/setCellAgent /// operation, persist the result and announce `LayoutChanged`. /// /// # Errors /// Returns an [`ErrorDto`] (`INVALID` for a malformed id/operation or an invariant /// violation, `NOT_FOUND` for an unknown project/node, `FILESYSTEM`/`STORE` on I/O /// failure). #[tauri::command] pub async fn mutate_layout( project_id: String, layout_id: Option, operation: LayoutOperationDto, state: State<'_, AppState>, ) -> Result { let id = parse_project_id(&project_id)?; let lid = layout_id.as_deref().map(parse_layout_id).transpose()?; let operation = operation.into_operation()?; state .mutate_layout .execute(MutateLayoutInput { project_id: id, layout_id: lid, operation, }) .await .map(LayoutDto::from) .map_err(ErrorDto::from) } // --------------------------------------------------------------------------- // Named-layout management (#4) // --------------------------------------------------------------------------- /// `list_layouts` — list all named layouts of a project and the active one. /// /// # Errors /// Returns an [`ErrorDto`] (`INVALID` for a malformed id, `NOT_FOUND` if the /// project is unknown, `STORE`/`FILESYSTEM` on I/O failure). #[tauri::command] pub async fn list_layouts( project_id: String, state: State<'_, AppState>, ) -> Result { let id = parse_project_id(&project_id)?; state .list_layouts .execute(ListLayoutsInput { project_id: id }) .await .map(ListLayoutsDto::from) .map_err(ErrorDto::from) } /// `create_layout` — create a new empty named layout and make it active. /// /// # Errors /// Returns an [`ErrorDto`] (`INVALID` for empty name or malformed id, `NOT_FOUND` /// if the project is unknown, `STORE`/`FILESYSTEM` on I/O failure). #[tauri::command] pub async fn create_layout( request: CreateLayoutRequestDto, state: State<'_, AppState>, ) -> Result { let project_id = parse_project_id(&request.project_id)?; let kind = request.parse_kind()?; state .create_layout .execute(CreateLayoutInput { project_id, name: request.name, kind, }) .await .map(CreateLayoutResultDto::from) .map_err(ErrorDto::from) } /// `rename_layout` — rename a named layout. /// /// # Errors /// Returns an [`ErrorDto`] (`INVALID` for empty name or malformed id, `NOT_FOUND` /// if the project or layout is unknown). #[tauri::command] pub async fn rename_layout( request: RenameLayoutRequestDto, state: State<'_, AppState>, ) -> Result<(), ErrorDto> { let project_id = parse_project_id(&request.project_id)?; let layout_id = parse_layout_id(&request.layout_id)?; state .rename_layout .execute(RenameLayoutInput { project_id, layout_id, name: request.name, }) .await .map_err(ErrorDto::from) } /// `delete_layout` — delete a named layout (cannot be the last one). /// /// Returns the active layout id after the deletion. /// /// # Errors /// Returns an [`ErrorDto`] (`INVALID` for malformed id or last layout attempt, /// `NOT_FOUND` if the project or layout is unknown). #[tauri::command] pub async fn delete_layout( request: DeleteLayoutRequestDto, state: State<'_, AppState>, ) -> Result { let project_id = parse_project_id(&request.project_id)?; let layout_id = parse_layout_id(&request.layout_id)?; state .delete_layout .execute(DeleteLayoutInput { project_id, layout_id, }) .await .map(DeleteLayoutResultDto::from) .map_err(ErrorDto::from) } /// `set_active_layout` — switch the active named layout of a project. /// /// # Errors /// Returns an [`ErrorDto`] (`INVALID` for malformed id, `NOT_FOUND` if the /// project or layout is unknown). #[tauri::command] pub async fn set_active_layout( request: SetActiveLayoutRequestDto, state: State<'_, AppState>, ) -> Result<(), ErrorDto> { let project_id = parse_project_id(&request.project_id)?; let layout_id = parse_layout_id(&request.layout_id)?; state .set_active_layout .execute(SetActiveLayoutInput { project_id, layout_id, }) .await .map_err(ErrorDto::from) } // --------------------------------------------------------------------------- // Profiles & first-run (L5) // --------------------------------------------------------------------------- /// `first_run_state` — whether the first-run wizard should show (no /// `profiles.json` yet) plus the pre-filled reference catalogue to seed it. /// /// # Errors /// Returns an [`ErrorDto`] (`STORE` on profiles I/O failure). #[tauri::command] pub async fn first_run_state(state: State<'_, AppState>) -> Result { state .first_run_state .execute() .await .map(FirstRunStateDto::from) .map_err(ErrorDto::from) } /// `reference_profiles` — the pre-filled, editable reference catalogue /// (Claude/Codex/Gemini/Aider). /// /// # Errors /// Returns an [`ErrorDto`] (never in practice; the catalogue is in-memory). #[tauri::command] pub async fn reference_profiles(state: State<'_, AppState>) -> Result { state .reference_profiles .execute() .await .map(ProfileListDto::from) .map_err(ErrorDto::from) } /// `detect_profiles` — probe each candidate profile's detection command and /// report which CLIs are installed (✓/✗). /// /// # Errors /// Returns an [`ErrorDto`] (detection failures degrade to `available: false`). #[tauri::command] pub async fn detect_profiles( request: DetectProfilesRequestDto, state: State<'_, AppState>, ) -> Result { state .detect_profiles .execute(request.into()) .await .map(DetectProfilesResponseDto::from) .map_err(ErrorDto::from) } /// `list_profiles` — list the configured profiles. /// /// # Errors /// Returns an [`ErrorDto`] (`STORE` on profiles I/O failure). #[tauri::command] pub async fn list_profiles(state: State<'_, AppState>) -> Result { state .list_profiles .execute() .await .map(ProfileListDto::from) .map_err(ErrorDto::from) } /// `save_profile` — create or replace (by id) a single profile. /// /// # Errors /// Returns an [`ErrorDto`] (`STORE` on profiles I/O failure). #[tauri::command] pub async fn save_profile( request: SaveProfileRequestDto, state: State<'_, AppState>, ) -> Result { state .save_profile .execute(request.into()) .await .map(ProfileDto::from) .map_err(ErrorDto::from) } /// `delete_profile` — delete a profile by id. /// /// # Errors /// Returns an [`ErrorDto`] (`INVALID` for a malformed id, `NOT_FOUND` if absent, /// `STORE` on failure). #[tauri::command] pub async fn delete_profile( profile_id: String, state: State<'_, AppState>, ) -> Result<(), ErrorDto> { let input = parse_delete_profile(&profile_id)?; state .delete_profile .execute(input) .await .map_err(ErrorDto::from) } /// `configure_profiles` — persist the batch of chosen/edited/custom profiles, /// closing the first run. /// /// # Errors /// Returns an [`ErrorDto`] (`STORE` on profiles I/O failure). #[tauri::command] pub async fn configure_profiles( request: ConfigureProfilesRequestDto, state: State<'_, AppState>, ) -> Result { state .configure_profiles .execute(request.into()) .await .map(ProfileListDto::from) .map_err(ErrorDto::from) } // --------------------------------------------------------------------------- // Agents (L6) // --------------------------------------------------------------------------- /// Resolves a [`domain::Project`] by id, mapping `StoreError` → `AppError` → `ErrorDto`. async fn resolve_project( project_id: &str, state: &State<'_, AppState>, ) -> Result { let id = parse_project_id(project_id)?; state .project_store .load_project(id) .await .map_err(|e| ErrorDto::from(AppError::from(e))) } /// `create_agent` — create a new project agent from scratch. /// /// # Errors /// Returns an [`ErrorDto`] (`INVALID` for a bad id/name, `NOT_FOUND` if the /// project is unknown, `STORE` on I/O failure). #[tauri::command] pub async fn create_agent( request: CreateAgentRequestDto, state: State<'_, AppState>, ) -> Result { let project = resolve_project(&request.project_id, &state).await?; let profile_id = parse_profile_id(&request.profile_id)?; state .create_agent .execute(CreateAgentInput { project, name: request.name, profile_id, initial_content: request.initial_content, }) .await .map(AgentDto::from) .map_err(ErrorDto::from) } /// `list_agents` — list the agents of a project. /// /// # Errors /// Returns an [`ErrorDto`] (`INVALID` for a malformed id, `NOT_FOUND` if the /// project is unknown, `STORE` on I/O failure). #[tauri::command] pub async fn list_agents( project_id: String, state: State<'_, AppState>, ) -> Result { let project = resolve_project(&project_id, &state).await?; state .list_agents .execute(ListAgentsInput { project }) .await .map(AgentListDto::from) .map_err(ErrorDto::from) } /// `read_agent_context` — read an agent's Markdown context. /// /// # Errors /// Returns an [`ErrorDto`] (`INVALID` for a malformed id, `NOT_FOUND` if the /// project or agent is unknown, `STORE` on I/O failure). #[tauri::command] pub async fn read_agent_context( project_id: String, agent_id: String, state: State<'_, AppState>, ) -> Result { let project = resolve_project(&project_id, &state).await?; let agent_id = parse_agent_id(&agent_id)?; state .read_agent_context .execute(ReadAgentContextInput { project, agent_id }) .await .map(ReadAgentContextResponseDto::from) .map_err(ErrorDto::from) } /// `update_agent_context` — overwrite an agent's Markdown context. /// /// # Errors /// Returns an [`ErrorDto`] (`INVALID` for a malformed id, `NOT_FOUND` if the /// project or agent is unknown, `STORE` on I/O failure). #[tauri::command] pub async fn update_agent_context( request: UpdateAgentContextRequestDto, state: State<'_, AppState>, ) -> Result<(), ErrorDto> { let project = resolve_project(&request.project_id, &state).await?; let agent_id = parse_agent_id(&request.agent_id)?; state .update_agent_context .execute(UpdateAgentContextInput { project, agent_id, content: request.content, }) .await .map_err(ErrorDto::from) } /// `delete_agent` — remove an agent from the project manifest. /// /// # Errors /// Returns an [`ErrorDto`] (`INVALID` for a malformed id, `NOT_FOUND` if the /// project or agent is unknown, `STORE` on I/O failure). #[tauri::command] pub async fn delete_agent( project_id: String, agent_id: String, state: State<'_, AppState>, ) -> Result<(), ErrorDto> { let project = resolve_project(&project_id, &state).await?; let agent_id = parse_agent_id(&agent_id)?; state .delete_agent .execute(DeleteAgentInput { project, agent_id }) .await .map_err(ErrorDto::from) } /// `launch_agent` — spawn an agent's CLI in a PTY and wire its byte stream to /// the frontend via a [`Channel`]. /// /// Mirrors `open_terminal`: execute the use case (spawn + register session), /// register the xterm channel in the [`PtyBridge`], then pump PTY output to /// that channel on a dedicated OS thread. /// /// Returns the [`TerminalSessionDto`] (its `sessionId` is what /// `write`/`resize`/`close` reference). /// /// # Errors /// Returns an [`ErrorDto`] (`INVALID` for a bad id/size, `NOT_FOUND` if the /// project, agent or profile is unknown, `PROCESS` if the PTY fails to spawn). #[tauri::command] pub async fn launch_agent( request: LaunchAgentRequestDto, on_output: Channel, state: State<'_, AppState>, ) -> Result { let project = resolve_project(&request.project_id, &state).await?; let agent_id = parse_agent_id(&request.agent_id)?; let output = state .launch_agent .execute(LaunchAgentInput { project, agent_id, rows: request.rows, cols: request.cols, node_id: None, }) .await .map_err(ErrorDto::from)?; let session_id = output.session.id; // Register the xterm output channel for this session. state.pty_bridge.register(session_id, on_output); // Subscribe to the PTY's byte stream and pump it to the channel. // The stream is a blocking iterator; it runs on a dedicated OS thread and // ends when the PTY hits EOF or the bridge channel is gone. let handle = PtyHandle { session_id }; match state.pty_port.subscribe_output(&handle) { Ok(stream) => { let bridge: std::sync::Arc = std::sync::Arc::clone(&state.pty_bridge); std::thread::spawn(move || { for chunk in stream { if !bridge.send_output(&session_id, chunk) { break; } } bridge.unregister(&session_id); }); } Err(e) => { state.pty_bridge.unregister(&session_id); return Err(ErrorDto::from(AppError::from(e))); } } Ok(TerminalSessionDto::from(output)) } // --------------------------------------------------------------------------- // Templates & sync (L7) // --------------------------------------------------------------------------- /// `create_template` — create a template in the global IDE store. /// /// # Errors /// Returns an [`ErrorDto`] (`INVALID` for an empty name or malformed profile id, /// `STORE` on persistence failure). #[tauri::command] pub async fn create_template( request: CreateTemplateRequestDto, state: State<'_, AppState>, ) -> Result { let input = request.into_input()?; state .create_template .execute(input) .await .map(TemplateDto::from) .map_err(ErrorDto::from) } /// `update_template` — update a template's content (bumps version, fires /// `TemplateUpdated` event). /// /// # Errors /// Returns an [`ErrorDto`] (`INVALID` for a malformed id, `NOT_FOUND` if the /// template is unknown, `STORE` on persistence failure). #[tauri::command] pub async fn update_template( request: UpdateTemplateRequestDto, state: State<'_, AppState>, ) -> Result { let input = request.into_input()?; state .update_template .execute(input) .await .map(TemplateDto::from) .map_err(ErrorDto::from) } /// `list_templates` — list all templates in the global IDE store. /// /// # Errors /// Returns an [`ErrorDto`] (`STORE` on persistence failure). #[tauri::command] pub async fn list_templates(state: State<'_, AppState>) -> Result { state .list_templates .execute() .await .map(TemplateListDto::from) .map_err(ErrorDto::from) } /// `delete_template` — remove a template from the global IDE store. /// /// Agents previously created from it keep their `.md`; drift detection simply /// finds nothing to compare against afterwards. /// /// # Errors /// Returns an [`ErrorDto`] (`INVALID` for a malformed id, `NOT_FOUND` if the /// template is unknown, `STORE` on failure). #[tauri::command] pub async fn delete_template( template_id: String, state: State<'_, AppState>, ) -> Result<(), ErrorDto> { let id = parse_template_id(&template_id)?; state .delete_template .execute(DeleteTemplateInput { template_id: id }) .await .map_err(ErrorDto::from) } /// `create_agent_from_template` — instantiate a project agent from a template. /// /// Copies the template's Markdown content, links the agent origin and version, /// and records the manifest entry. /// /// # Errors /// Returns an [`ErrorDto`] (`INVALID` for a malformed id, `NOT_FOUND` if the /// project or template is unknown, `STORE` on I/O failure). #[tauri::command] pub async fn create_agent_from_template( request: CreateAgentFromTemplateRequestDto, state: State<'_, AppState>, ) -> Result { let project = resolve_project(&request.project_id, &state).await?; let input = request.into_input(project)?; state .create_agent_from_template .execute(input) .await .map(|out| AgentDto(out.agent)) .map_err(ErrorDto::from) } /// `detect_agent_drift` — list which synchronized agents are behind their /// template (version drift). /// /// # Errors /// Returns an [`ErrorDto`] (`INVALID` for a malformed id, `NOT_FOUND` if the /// project is unknown, `STORE` on I/O failure). #[tauri::command] pub async fn detect_agent_drift( project_id: String, state: State<'_, AppState>, ) -> Result { let project = resolve_project(&project_id, &state).await?; state .detect_agent_drift .execute(DetectAgentDriftInput { project }) .await .map(AgentDriftListDto::from) .map_err(ErrorDto::from) } /// `sync_agent_with_template` — apply the latest template content to a /// synchronized agent. /// /// Returns whether a sync was applied and the resulting version. /// /// # Errors /// Returns an [`ErrorDto`] (`INVALID` for a malformed id, `NOT_FOUND` if the /// project, agent or template is unknown, `STORE` on I/O failure). #[tauri::command] pub async fn sync_agent_with_template( request: SyncAgentWithTemplateRequestDto, state: State<'_, AppState>, ) -> Result { let project = resolve_project(&request.project_id, &state).await?; let agent_id = parse_agent_id(&request.agent_id)?; state .sync_agent_with_template .execute(SyncAgentWithTemplateInput { project, agent_id }) .await .map(SyncResultDto::from) .map_err(ErrorDto::from) } // --------------------------------------------------------------------------- // Git (L8) // --------------------------------------------------------------------------- /// `git_status` — report the working-tree status of a project's repository. /// /// # Errors /// Returns an [`ErrorDto`] (`INVALID` for a bad project id or root, `GIT` if /// the repo is missing or the operation fails). #[tauri::command] pub async fn git_status( project_id: String, state: State<'_, AppState>, ) -> Result { let project = resolve_project(&project_id, &state).await?; let root = project.root.as_str().to_owned(); state .git_status .execute(GitStatusInput { root }) .await .map(GitStatusListDto::from) .map_err(ErrorDto::from) } /// `git_stage` — stage a path in a project's repository. /// /// # Errors /// Returns an [`ErrorDto`] (`INVALID` for a bad project id or root, `GIT` on /// failure). #[tauri::command] pub async fn git_stage( request: GitStageRequestDto, state: State<'_, AppState>, ) -> Result<(), ErrorDto> { let project = resolve_project(&request.project_id, &state).await?; let root = project.root.as_str().to_owned(); state .git_stage .execute(GitStagePathInput { root, path: request.path, }) .await .map_err(ErrorDto::from) } /// `git_unstage` — unstage a path in a project's repository. /// /// # Errors /// Returns an [`ErrorDto`] (`INVALID` for a bad project id or root, `GIT` on /// failure). #[tauri::command] pub async fn git_unstage( request: GitStageRequestDto, state: State<'_, AppState>, ) -> Result<(), ErrorDto> { let project = resolve_project(&request.project_id, &state).await?; let root = project.root.as_str().to_owned(); state .git_unstage .execute(GitStagePathInput { root, path: request.path, }) .await .map_err(ErrorDto::from) } /// `git_commit` — create a commit in a project's repository. /// /// Announces [`DomainEvent::GitStateChanged`]. /// /// # Errors /// Returns an [`ErrorDto`] (`INVALID` for a bad id or an empty message, `GIT` /// on failure). #[tauri::command] pub async fn git_commit( request: GitCommitRequestDto, state: State<'_, AppState>, ) -> Result { let project = resolve_project(&request.project_id, &state).await?; let root = project.root.as_str().to_owned(); state .git_commit .execute(GitCommitInput { project_id: project.id, root, message: request.message, }) .await .map(GitCommitDto::from) .map_err(ErrorDto::from) } /// `git_branches` — list branches and the current one for a project's repository. /// /// # Errors /// Returns an [`ErrorDto`] (`INVALID` for a bad project id or root, `GIT` on /// failure). #[tauri::command] pub async fn git_branches( project_id: String, state: State<'_, AppState>, ) -> Result { let project = resolve_project(&project_id, &state).await?; let root = project.root.as_str().to_owned(); state .git_branches .execute(GitBranchesInput { root }) .await .map(GitBranchesDto::from) .map_err(ErrorDto::from) } /// `git_checkout` — check out a branch in a project's repository. /// /// Announces [`DomainEvent::GitStateChanged`]. /// /// # Errors /// Returns an [`ErrorDto`] (`INVALID` for a bad project id or root, `GIT` on /// failure). #[tauri::command] pub async fn git_checkout( request: GitCheckoutRequestDto, state: State<'_, AppState>, ) -> Result<(), ErrorDto> { let project = resolve_project(&request.project_id, &state).await?; let root = project.root.as_str().to_owned(); state .git_checkout .execute(GitCheckoutInput { project_id: project.id, root, branch: request.branch, }) .await .map_err(ErrorDto::from) } /// `git_log` — return the recent commit log for a project's repository. /// /// # Errors /// Returns an [`ErrorDto`] (`INVALID` for a bad project id or root, `GIT` on /// failure). #[tauri::command] pub async fn git_log( project_id: String, limit: usize, state: State<'_, AppState>, ) -> Result { let project = resolve_project(&project_id, &state).await?; let root = project.root.as_str().to_owned(); state .git_log .execute(GitLogInput { root, limit }) .await .map(GitCommitListDto::from) .map_err(ErrorDto::from) } /// `git_init` — initialise a git repository at a project's root. /// /// Announces [`DomainEvent::GitStateChanged`]. /// /// # Errors /// Returns an [`ErrorDto`] (`INVALID` for a bad project id or root, `GIT` on /// failure). #[tauri::command] pub async fn git_init( project_id: String, state: State<'_, AppState>, ) -> Result<(), ErrorDto> { let project = resolve_project(&project_id, &state).await?; let root = project.root.as_str().to_owned(); state .git_init .execute(GitInitInput { project_id: project.id, root, }) .await .map_err(ErrorDto::from) } /// `git_graph` — return the commit graph for all local branches of a project. /// /// # Errors /// Returns an [`ErrorDto`] (`INVALID` for a bad project id or root, `GIT` on /// failure). #[tauri::command] pub async fn git_graph( project_id: String, limit: usize, state: State<'_, AppState>, ) -> Result { let project = resolve_project(&project_id, &state).await?; let root = project.root.as_str().to_owned(); state .git_graph .execute(GitGraphInput { root, limit }) .await .map(GraphCommitListDto::from) .map_err(ErrorDto::from) } // --------------------------------------------------------------------------- // Windows (L10) // --------------------------------------------------------------------------- use application::MoveTabToNewWindowInput; use crate::dto::{parse_tab_id, MoveTabResultDto}; /// `move_tab_to_new_window` — detach a tab into a brand-new OS window. /// /// Applies the workspace topology change (the tab is *moved*, not duplicated) /// and opens a fresh [`tauri::WebviewWindow`]. The session-state handoff to that /// window (rendering the detached tab) is the L11 multi-window UI work; this /// command provides the backend primitive and the new OS window. /// /// # Errors /// Returns an [`ErrorDto`] (`INVALID` for a malformed tab id, `NOT_FOUND` if the /// tab is unknown to the persisted workspace, `INTERNAL` if the window fails to /// open). #[tauri::command] pub async fn move_tab_to_new_window( app: tauri::AppHandle, tab_id: String, state: State<'_, AppState>, ) -> Result { let tid = parse_tab_id(&tab_id)?; let out = state .move_tab .execute(MoveTabToNewWindowInput { tab_id: tid }) .await .map_err(ErrorDto::from)?; let label = format!("win-{}", out.new_window_id); tauri::WebviewWindowBuilder::new(&app, &label, tauri::WebviewUrl::App("index.html".into())) .title("IdeA") .inner_size(1280.0, 800.0) .build() .map_err(|e| ErrorDto { code: "INTERNAL".to_owned(), message: e.to_string(), })?; Ok(MoveTabResultDto::from(out)) }