//! `#[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 crate::dto::DismissEmbedderSuggestionRequestDto; use application::{ AppError, AssignSkillToAgentInput, CloseProjectInput, CreateAgentInput, CreateLayoutInput, CreateMemoryInput, CreateSkillInput, DeleteAgentInput, DeleteEmbedderProfileInput, DeleteLayoutInput, DeleteMemoryInput, DeleteSkillInput, DeleteTemplateInput, DetectAgentDriftInput, GetMemoryInput, GitBranchesInput, GitCheckoutInput, GitCommitInput, GitGraphInput, GitInitInput, GitLogInput, GitStagePathInput, GitStatusInput, InspectConversationInput, LaunchAgentInput, ListAgentsInput, ListLayoutsInput, ListMemoriesInput, ListSkillsInput, LoadLayoutInput, MutateLayoutInput, OpenProjectInput, ReadAgentContextInput, ReadMemoryIndexInput, ReadProjectContextInput, RecallMemoryInput, RenameLayoutInput, ResolveMemoryLinksInput, SetActiveLayoutInput, SnapshotRunningAgentsInput, SyncAgentWithTemplateInput, UnassignSkillFromAgentInput, UpdateAgentContextInput, UpdateMemoryInput, UpdateProjectContextInput, UpdateSkillInput, }; use domain::ports::PtyHandle; use crate::dto::{ parse_agent_id, parse_close_terminal, parse_delete_profile, parse_layout_id, parse_memory_slug, parse_node_id, parse_profile_id, parse_project_id, parse_session_id, parse_skill_id, parse_template_id, AgentDriftListDto, AgentDto, AgentListDto, AssignSkillRequestDto, AttachLiveAgentRequestDto, ConfigureProfilesRequestDto, ConversationDetailsDto, CreateAgentFromTemplateRequestDto, CreateAgentRequestDto, CreateLayoutRequestDto, CreateLayoutResultDto, CreateMemoryRequestDto, CreateProjectRequestDto, CreateSkillRequestDto, CreateTemplateRequestDto, DeleteLayoutRequestDto, DeleteLayoutResultDto, DetectProfilesRequestDto, DetectProfilesResponseDto, EmbedderEnginesDto, EmbedderProfileDto, EmbedderProfileListDto, ErrorDto, FirstRunStateDto, GitBranchesDto, GitCheckoutRequestDto, GitCommitDto, GitCommitListDto, GitCommitRequestDto, GitStageRequestDto, GitStatusListDto, GraphCommitListDto, HealthRequestDto, HealthResponseDto, InspectConversationRequestDto, LaunchAgentRequestDto, LayoutDto, LayoutOperationDto, ListLayoutsDto, LiveAgentListDto, MemoryDto, MemoryIndexDto, MemoryLinksDto, MemoryListDto, OpenTerminalRequestDto, ProfileDto, ProfileListDto, ProjectDto, ProjectListDto, ReadAgentContextResponseDto, ReattachResultDto, RecallMemoryRequestDto, RenameLayoutRequestDto, ResizeTerminalRequestDto, SaveEmbedderProfileRequestDto, SaveProfileRequestDto, SetActiveLayoutRequestDto, SkillDto, SkillListDto, SyncAgentWithTemplateRequestDto, SyncResultDto, TemplateDto, TemplateListDto, TerminalClosedDto, TerminalSessionDto, UnassignSkillRequestDto, UpdateAgentContextRequestDto, UpdateMemoryRequestDto, UpdateProjectContextRequestDto, UpdateSkillRequestDto, UpdateTemplateRequestDto, WriteTerminalRequestDto, }; use crate::pty::{PtyBridge, PtyChunk}; use crate::state::AppState; use domain::{SkillRef, SkillScope}; /// `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 { let output = state .create_project .execute(request.into()) .await .map_err(ErrorDto::from)?; // Start tailing this project's `.ideai/requests/` tree so an orchestrator // agent can delegate agent/skill creation to IdeA (§14.3). state.ensure_orchestrator_watch(&output.project); Ok(ProjectDto::from(output)) } /// `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)?; let output = state .open_project .execute(OpenProjectInput { project_id: id }) .await .map_err(ErrorDto::from)?; // (Re)start the orchestrator watcher for this project (idempotent, §14.3). state.ensure_orchestrator_watch(&output.project); Ok(ProjectDto::from(output)) } /// `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)?; // T5: freeze `agent_was_running` on every agent leaf BEFORE any PTY release, // reading the live registry as it stands now. Best-effort: a snapshot failure // must not block the close. let _ = state .snapshot_running_agents .execute(SnapshotRunningAgentsInput { project_id: id }) .await; // Stop tailing this project's `.ideai/requests/` tree (§14.3). state.stop_orchestrator_watch(&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) } /// `read_project_context` — read `.ideai/CONTEXT.md` for a project. /// /// Missing context is returned as an empty string so a project whose `.ideai/` /// was deleted can still open cleanly. /// /// # Errors /// Returns an [`ErrorDto`] (`INVALID` for a malformed id, `NOT_FOUND` if the /// project is unknown, `FILESYSTEM`/`STORE` on read or UTF-8 failure). #[tauri::command] pub async fn read_project_context( project_id: String, state: State<'_, AppState>, ) -> Result { let project = resolve_project(&project_id, &state).await?; state .read_project_context .execute(ReadProjectContextInput { project }) .await .map(|out| out.content) .map_err(ErrorDto::from) } /// `update_project_context` — overwrite `.ideai/CONTEXT.md` for a project. /// /// # Errors /// Returns an [`ErrorDto`] (`INVALID` for a malformed id, `NOT_FOUND` if the /// project is unknown, `FILESYSTEM` on write failure). #[tauri::command] pub async fn update_project_context( request: UpdateProjectContextRequestDto, state: State<'_, AppState>, ) -> Result<(), ErrorDto> { let project = resolve_project(&request.project_id, &state).await?; state .update_project_context .execute(UpdateProjectContextInput { project, content: request.content, }) .await .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. let gen = 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 this attach is superseded. 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: drop the channel only if still ours (a re-attach // may have superseded this generation — don't tear down its channel). bridge.unregister_if(&session_id, gen); }); } 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 } /// `reattach_terminal` — re-bind a view to a **still-living** PTY without /// re-spawning it. /// /// Navigation (switching layout/tab) tears the xterm view down but must NOT kill /// the backend PTY (the AI keeps running). When the view comes back it calls this /// command, which: /// 1. reads the session's retained **scrollback** so the terminal can repaint, /// 2. registers the new per-session [`Channel`] in the [`PtyBridge`], /// 3. starts a fresh output pump subscribed to the live PTY (re-subscribable /// broadcast), so new bytes flow to the new channel. /// /// Returns the scrollback bytes; the frontend writes them into xterm first, then /// receives subsequent output over `on_output`. /// /// # Errors /// Returns an [`ErrorDto`] (`INVALID` for a malformed id, `NOT_FOUND`/`PROCESS` /// if the session is no longer alive). #[tauri::command] pub fn reattach_terminal( session_id: String, on_output: Channel, state: State<'_, AppState>, ) -> Result { let sid = parse_session_id(&session_id)?; let handle = PtyHandle { session_id: sid }; // (1) Snapshot the scrollback. A NotFound here means the PTY is gone (was // explicitly closed or exited) — surfaced as an error so the caller falls // back to opening a fresh terminal. let scrollback = state .pty_port .scrollback(&handle) .map_err(|e| ErrorDto::from(AppError::from(e)))?; // (2) Register the new output channel for this session, replacing any stale // one from a previous attach (and bumping the generation). let gen = state.pty_bridge.register(sid, on_output); // (3) Subscribe afresh to the live byte stream and pump it to the channel. // The fresh subscription supersedes the previous attach's, so its pump thread // ends and stops double-delivering this session's bytes. 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(&sid, chunk) { break; } } bridge.unregister_if(&sid, gen); }); } Err(e) => { state.pty_bridge.unregister(&sid); return Err(ErrorDto::from(AppError::from(e))); } } Ok(ReattachResultDto { session_id, scrollback, }) } // --------------------------------------------------------------------------- // 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) } // --------------------------------------------------------------------------- // Embedder profiles & engines (LOT C2 — §14.5.3) // --------------------------------------------------------------------------- /// `list_embedder_profiles` — list the configured embedder profiles (empty when /// none configured ⇒ the default `none` posture). /// /// # Errors /// Returns an [`ErrorDto`] (`STORE` on `embedder.json` I/O failure). #[tauri::command] pub async fn list_embedder_profiles( state: State<'_, AppState>, ) -> Result { state .list_embedder_profiles .execute() .await .map(EmbedderProfileListDto::from) .map_err(ErrorDto::from) } /// `save_embedder_profile` — create or replace (by id) a single embedder profile. /// /// # Errors /// Returns an [`ErrorDto`] (`INVALID` for empty id/name or zero dimension, `STORE` /// on I/O failure). #[tauri::command] pub async fn save_embedder_profile( request: SaveEmbedderProfileRequestDto, state: State<'_, AppState>, ) -> Result { state .save_embedder_profile .execute(request.into()) .await .map(EmbedderProfileDto::from) .map_err(ErrorDto::from) } /// `delete_embedder_profile` — delete an embedder profile by id. /// /// # Errors /// Returns an [`ErrorDto`] (`NOT_FOUND` if absent, `STORE` on I/O failure). #[tauri::command] pub async fn delete_embedder_profile( embedder_id: String, state: State<'_, AppState>, ) -> Result<(), ErrorDto> { state .delete_embedder_profile .execute(DeleteEmbedderProfileInput { id: embedder_id }) .await .map_err(ErrorDto::from) } /// `describe_embedder_engines` — describe the engines available to the /// "configure an embedder?" UI: the recommended ONNX catalogue, a best-effort /// snapshot of the local environment, and which strategies are compiled in. /// /// # Errors /// Returns an [`ErrorDto`] — in practice never (the environment probe is best-effort). #[tauri::command] pub async fn describe_embedder_engines( state: State<'_, AppState>, ) -> Result { state .describe_embedder_engines .execute() .await .map(EmbedderEnginesDto::from) .map_err(ErrorDto::from) } /// `dismiss_embedder_suggestion` — persist the user's response to the one-time /// embedder suggestion (LOT C3 — §14.5.5): `later` (re-proposable next session) or /// `never` (silenced for good). Resolves the project root from `project_id`. /// /// # Errors /// Returns an [`ErrorDto`] (`INVALID` for a malformed id, `NOT_FOUND` if the project /// is unknown, `STORE` on `.embedder-prompt.json` I/O failure). #[tauri::command] pub async fn dismiss_embedder_suggestion( request: DismissEmbedderSuggestionRequestDto, state: State<'_, AppState>, ) -> Result<(), ErrorDto> { let project = resolve_project(&request.project_id, &state).await?; state .dismiss_embedder_suggestion .execute(request.into_input(project.root)) .await .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) } /// `list_live_agents` — list the agents that currently own a live PTY session /// and the cell hosting each, so the UI can disable an agent already running in /// another cell (the "one live session per agent" invariant). /// /// `project_id` is accepted for API symmetry and future per-project scoping; the /// session registry is process-wide today, so the full live set is returned (a /// project's agent ids are disjoint from other projects' by construction, so the /// frontend can filter by the agents it knows). /// /// # Errors /// Returns an [`ErrorDto`] (`INVALID` for a malformed project id). #[tauri::command] pub fn list_live_agents( project_id: String, state: State<'_, AppState>, ) -> Result { // Validate the id shape for a consistent contract, even though the registry // is not project-scoped yet. let _ = parse_project_id(&project_id)?; Ok(LiveAgentListDto::from_pairs( state.terminal_sessions.live_agents(), )) } /// `attach_live_agent` — rebind an already-running agent session to a visible /// layout cell without respawning the CLI process. /// /// This is the backend side of "a cell is a view": a closed cell can leave an /// agent running in the background, and opening the agent in a new cell updates /// the session's host node while preserving the PTY/session/scrollback. /// /// # Errors /// Returns an [`ErrorDto`] (`INVALID` for malformed ids, `NOT_FOUND` if the /// project/agent/live session is unknown). #[tauri::command] pub async fn attach_live_agent( request: AttachLiveAgentRequestDto, state: State<'_, AppState>, ) -> Result { let _project = resolve_project(&request.project_id, &state).await?; let agent_id = parse_agent_id(&request.agent_id)?; let node_id = parse_node_id(&request.node_id)?; let session = state .terminal_sessions .rebind_agent_node(&agent_id, node_id) .ok_or_else(|| AppError::NotFound(format!("running session for agent {agent_id}")))?; Ok(LiveAgentListDto::from_pairs(vec![( agent_id, session.node_id, session.id, )])) } /// `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) } /// `inspect_conversation` — best-effort enriched details (last topic + token /// indicator) for a resume popup (T7). /// /// Best-effort by contract: a missing/unsupported inspector or a missing /// transcript yields **empty details** (absent `lastTopic`/`tokenCount`), never /// an error. Only a genuine store failure (resolving the agent/profile) errors. /// /// # Errors /// Returns an [`ErrorDto`] (`INVALID` for a malformed id, `NOT_FOUND` if the /// project, agent or profile is unknown, `STORE` on a manifest/profile failure). #[tauri::command] pub async fn inspect_conversation( request: InspectConversationRequestDto, state: State<'_, AppState>, ) -> Result { let project = resolve_project(&request.project_id, &state).await?; let agent_id = parse_agent_id(&request.agent_id)?; state .inspect_conversation .execute(InspectConversationInput { project, agent_id, conversation_id: request.conversation_id, }) .await .map(ConversationDetailsDto::from) .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)?; // The hosting cell drives the singleton-invariant guard. Parse it when the // frontend supplies one; absent ⇒ `None` (a fresh node is minted, and an // already-live agent is refused). let node_id = request.node_id.as_deref().map(parse_node_id).transpose()?; let output = state .launch_agent .execute(LaunchAgentInput { project, agent_id, rows: request.rows, cols: request.cols, node_id, // Resume id is a property of the hosting cell; the frontend passes the // leaf's current conversation id here. conversation_id: request.conversation_id.clone(), }) .await .map_err(ErrorDto::from)?; let session_id = output.session.id; // Register the xterm output channel for this session. let gen = 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 this attach is superseded. 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_if(&session_id, gen); }); } 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 crate::dto::{parse_tab_id, MoveTabResultDto}; use application::MoveTabToNewWindowInput; /// `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)) } // --------------------------------------------------------------------------- // Skills (L12) // --------------------------------------------------------------------------- /// `create_skill` — create a skill in its scope's store. /// /// # Errors /// Returns an [`ErrorDto`] (`INVALID` for an empty name/content or malformed /// project id, `NOT_FOUND` if the project is unknown, `STORE` on failure). #[tauri::command] pub async fn create_skill( request: CreateSkillRequestDto, state: State<'_, AppState>, ) -> Result { let project = resolve_project(&request.project_id, &state).await?; state .create_skill .execute(CreateSkillInput { name: request.name, content: request.content, scope: request.scope, project_root: project.root, }) .await .map(SkillDto::from) .map_err(ErrorDto::from) } /// `update_skill` — replace a skill's Markdown content. /// /// # Errors /// Returns an [`ErrorDto`] (`INVALID` for malformed ids or empty content, /// `NOT_FOUND` if the project or skill is unknown, `STORE` on failure). #[tauri::command] pub async fn update_skill( request: UpdateSkillRequestDto, state: State<'_, AppState>, ) -> Result { let project = resolve_project(&request.project_id, &state).await?; let skill_id = parse_skill_id(&request.skill_id)?; state .update_skill .execute(UpdateSkillInput { scope: request.scope, skill_id, content: request.content, project_root: project.root, }) .await .map(SkillDto::from) .map_err(ErrorDto::from) } /// `list_skills` — list the skills in one scope. /// /// # Errors /// Returns an [`ErrorDto`] (`INVALID` for a malformed id, `NOT_FOUND` if the /// project is unknown, `STORE` on failure). #[tauri::command] pub async fn list_skills( project_id: String, scope: SkillScope, state: State<'_, AppState>, ) -> Result { let project = resolve_project(&project_id, &state).await?; state .list_skills .execute(ListSkillsInput { scope, project_root: project.root, }) .await .map(SkillListDto::from) .map_err(ErrorDto::from) } /// `delete_skill` — remove a skill from its scope's store. /// /// Agents that referenced it keep their `SkillRef`; injection simply skips the /// now-absent skill. /// /// # Errors /// Returns an [`ErrorDto`] (`INVALID` for malformed ids, `NOT_FOUND` if the /// project or skill is unknown, `STORE` on failure). #[tauri::command] pub async fn delete_skill( project_id: String, scope: SkillScope, skill_id: String, state: State<'_, AppState>, ) -> Result<(), ErrorDto> { let project = resolve_project(&project_id, &state).await?; let id = parse_skill_id(&skill_id)?; state .delete_skill .execute(DeleteSkillInput { scope, skill_id: id, project_root: project.root, }) .await .map_err(ErrorDto::from) } /// `assign_skill_to_agent` — record a `SkillRef` in the agent's manifest entry. /// Idempotent. /// /// # Errors /// Returns an [`ErrorDto`] (`INVALID` for malformed ids, `NOT_FOUND` if the /// project or agent is unknown, `STORE` on failure). #[tauri::command] pub async fn assign_skill_to_agent( request: AssignSkillRequestDto, state: State<'_, AppState>, ) -> Result<(), ErrorDto> { let project = resolve_project(&request.project_id, &state).await?; let agent_id = parse_agent_id(&request.agent_id)?; let skill_id = parse_skill_id(&request.skill_id)?; state .assign_skill .execute(AssignSkillToAgentInput { project, agent_id, skill: SkillRef::new(skill_id, request.scope), }) .await .map_err(ErrorDto::from) } /// `unassign_skill_from_agent` — drop a skill assignment from an agent. /// Idempotent. /// /// # Errors /// Returns an [`ErrorDto`] (`INVALID` for malformed ids, `NOT_FOUND` if the /// project or agent is unknown, `STORE` on failure). #[tauri::command] pub async fn unassign_skill_from_agent( request: UnassignSkillRequestDto, state: State<'_, AppState>, ) -> Result<(), ErrorDto> { let project = resolve_project(&request.project_id, &state).await?; let agent_id = parse_agent_id(&request.agent_id)?; let skill_id = parse_skill_id(&request.skill_id)?; state .unassign_skill .execute(UnassignSkillFromAgentInput { project, agent_id, skill_id, }) .await .map_err(ErrorDto::from) } // --------------------------------------------------------------------------- // Memory (LOT A — §14.5.1) // --------------------------------------------------------------------------- /// `create_memory` — create a memory note in the project's store. /// /// # Errors /// Returns an [`ErrorDto`] (`INVALID` for an invalid slug/empty fields or /// malformed project id, `NOT_FOUND` if the project is unknown, `STORE` on /// failure). #[tauri::command] pub async fn create_memory( request: CreateMemoryRequestDto, state: State<'_, AppState>, ) -> Result { let project = resolve_project(&request.project_id, &state).await?; state .create_memory .execute(CreateMemoryInput { project_root: project.root, name: request.name, description: request.description, r#type: request.r#type, content: request.content, }) .await .map(MemoryDto::from) .map_err(ErrorDto::from) } /// `update_memory` — replace an existing memory note (re-validates invariants). /// /// # Errors /// Returns an [`ErrorDto`] (`INVALID` for an invalid slug/empty fields, /// `NOT_FOUND` if the project is unknown, `STORE` on failure). #[tauri::command] pub async fn update_memory( request: UpdateMemoryRequestDto, state: State<'_, AppState>, ) -> Result { let project = resolve_project(&request.project_id, &state).await?; let slug = parse_memory_slug(&request.slug)?; state .update_memory .execute(UpdateMemoryInput { project_root: project.root, slug, description: request.description, r#type: request.r#type, content: request.content, }) .await .map(MemoryDto::from) .map_err(ErrorDto::from) } /// `list_memories` — list the project's memory notes. /// /// # Errors /// Returns an [`ErrorDto`] (`INVALID` for a malformed id, `NOT_FOUND` if the /// project is unknown, `STORE` on failure). #[tauri::command] pub async fn list_memories( project_id: String, state: State<'_, AppState>, ) -> Result { let project = resolve_project(&project_id, &state).await?; state .list_memories .execute(ListMemoriesInput { project_root: project.root, }) .await .map(MemoryListDto::from) .map_err(ErrorDto::from) } /// `get_memory` — read one memory note by slug. /// /// # Errors /// Returns an [`ErrorDto`] (`INVALID` for an invalid slug/id, `NOT_FOUND` if the /// project or note is unknown, `STORE` on failure). #[tauri::command] pub async fn get_memory( project_id: String, slug: String, state: State<'_, AppState>, ) -> Result { let project = resolve_project(&project_id, &state).await?; let slug = parse_memory_slug(&slug)?; state .get_memory .execute(GetMemoryInput { project_root: project.root, slug, }) .await .map(MemoryDto::from) .map_err(ErrorDto::from) } /// `delete_memory` — remove a memory note (and its index row). /// /// # Errors /// Returns an [`ErrorDto`] (`INVALID` for an invalid slug/id, `NOT_FOUND` if the /// project or note is unknown, `STORE` on failure). #[tauri::command] pub async fn delete_memory( project_id: String, slug: String, state: State<'_, AppState>, ) -> Result<(), ErrorDto> { let project = resolve_project(&project_id, &state).await?; let slug = parse_memory_slug(&slug)?; state .delete_memory .execute(DeleteMemoryInput { project_root: project.root, slug, }) .await .map_err(ErrorDto::from) } /// `read_memory_index` — read the structured `MEMORY.md` index. /// /// # Errors /// Returns an [`ErrorDto`] (`INVALID` for a malformed id, `NOT_FOUND` if the /// project is unknown, `STORE` on failure). #[tauri::command] pub async fn read_memory_index( project_id: String, state: State<'_, AppState>, ) -> Result { let project = resolve_project(&project_id, &state).await?; state .read_memory_index .execute(ReadMemoryIndexInput { project_root: project.root, }) .await .map(MemoryIndexDto::from) .map_err(ErrorDto::from) } /// `recall_memory` — recall the most relevant memory entries for a query within /// a token budget (LOT B — §14.5.2). /// /// # Errors /// Returns an [`ErrorDto`] (`INVALID` for a malformed id, `NOT_FOUND` if the /// project is unknown, `STORE` on failure). An empty or absent memory and a zero /// budget both yield an empty list, not an error. #[tauri::command] pub async fn recall_memory( request: RecallMemoryRequestDto, state: State<'_, AppState>, ) -> Result { let project = resolve_project(&request.project_id, &state).await?; state .recall_memory .execute(RecallMemoryInput { project_root: project.root, text: request.text, token_budget: request.token_budget, }) .await .map(MemoryIndexDto::from) .map_err(ErrorDto::from) } /// `resolve_memory_links` — resolve a note's outgoing `[[slug]]` links. /// /// # Errors /// Returns an [`ErrorDto`] (`INVALID` for an invalid slug/id, `NOT_FOUND` if the /// project or source note is unknown, `STORE` on failure). #[tauri::command] pub async fn resolve_memory_links( project_id: String, slug: String, state: State<'_, AppState>, ) -> Result { let project = resolve_project(&project_id, &state).await?; let slug = parse_memory_slug(&slug)?; state .resolve_memory_links .execute(ResolveMemoryLinksInput { project_root: project.root, slug, }) .await .map(MemoryLinksDto::from) .map_err(ErrorDto::from) }