//! Managed application state — the product of the **composition root**. //! //! The composition root ([`build_app_state`]) is the *single* place that //! constructs concrete adapters (`new ConcreteAdapter`) and injects them as //! `Arc` into the use cases (ARCHITECTURE §1.1, §10). The use cases //! are then exposed through `tauri::State` to the command handlers. use std::collections::HashMap; use std::path::PathBuf; use std::sync::{Arc, Mutex}; use application::{ CloseProject, CloseTab, CloseTerminal, ConfigureProfiles, CreateAgentFromScratch, CreateAgentFromTemplate, CreateLayout, CreateProject, CreateTemplate, DeleteAgent, DeleteLayout, DeleteProfile, DeleteTemplate, DetectAgentDrift, DetectProfiles, FirstRunState, GitBranches, GitCheckout, GitCommit, GitGraph, GitInit, GitLog, GitStage, GitStatus, GitUnstage, HealthUseCase, InspectConversation, LaunchAgent, ListAgents, ListLayouts, ListProfiles, ListProjects, ListSkills, ListTemplates, LiveAgentRegistry, LoadLayout, MoveTabToNewWindow, MutateLayout, OpenProject, OpenTerminal, OrchestratorService, ReadAgentContext, ReferenceProfiles, RenameLayout, ResizeTerminal, SaveProfile, SetActiveLayout, SnapshotRunningAgents, AssignSkillToAgent, CreateSkill, DeleteSkill, UnassignSkillFromAgent, UpdateSkill, CreateMemory, DeleteMemory, GetMemory, ListMemories, ReadMemoryIndex, RecallMemory, ResolveMemoryLinks, UpdateMemory, SyncAgentWithTemplate, TerminalSessions, UpdateAgentContext, UpdateTemplate, WriteToTerminal, }; use domain::ports::{ AgentContextStore, AgentRuntime, Clock, EventBus, FileSystem, GitPort, IdGenerator, MemoryRecall, MemoryStore, ProcessSpawner, ProfileStore, ProjectStore, PtyPort, SkillStore, TemplateStore, }; use domain::{DomainEvent, Project, ProjectId}; use infrastructure::{ ClaudeTranscriptInspector, CliAgentRuntime, FsOrchestratorWatcher, FsProfileStore, FsMemoryStore, FsProjectStore, FsSkillStore, FsTemplateStore, Git2Repository, IdeaiContextStore, LocalFileSystem, LocalProcessSpawner, NaiveMemoryRecall, OrchestratorWatchHandle, PortablePtyAdapter, SystemClock, TokioBroadcastEventBus, UuidGenerator, }; use crate::pty::PtyBridge; /// Everything the IPC layer needs at runtime, managed by Tauri. /// /// Use cases are stored behind `Arc` so handlers clone cheaply. The concrete /// adapters are owned here and never leak past the composition root as concrete /// types — downstream code only sees the `Arc` held inside the use /// cases. pub struct AppState { /// Trivial health use case validating the end-to-end wiring. pub health: Arc, /// Create a project (init `.ideai/`, register it). pub create_project: Arc, /// Open a project (load meta + manifest). pub open_project: Arc, /// Close a project (persist state). pub close_project: Arc, /// Close a tab. pub close_tab: Arc, /// List known projects. pub list_projects: Arc, /// Open a terminal (spawn PTY, register session). pub open_terminal: Arc, /// Write keystrokes to a terminal. pub write_terminal: Arc, /// Resize a terminal. pub resize_terminal: Arc, /// Close a terminal (kill PTY). pub close_terminal: Arc, /// Load a project's persisted layout tree. pub load_layout: Arc, /// Mutate + persist a project's layout tree. pub mutate_layout: Arc, /// List all named layouts for a project (#4). pub list_layouts: Arc, /// Create a new named layout (#4). pub create_layout: Arc, /// Rename a named layout (#4). pub rename_layout: Arc, /// Delete a named layout (#4). pub delete_layout: Arc, /// Set the active named layout (#4). pub set_active_layout: Arc, /// Freeze `agent_was_running` on every agent leaf before a PTY kill (T5). pub snapshot_running_agents: Arc, /// Detect which candidate profiles' CLIs are installed (first-run). pub detect_profiles: Arc, /// List configured profiles. pub list_profiles: Arc, /// Save (upsert) a profile. pub save_profile: Arc, /// Delete a profile. pub delete_profile: Arc, /// Persist the batch of chosen profiles (closes the first run). pub configure_profiles: Arc, /// Expose the pre-filled reference catalogue. pub reference_profiles: Arc, /// Whether the first-run wizard should show + the reference catalogue. pub first_run_state: Arc, /// The local PTY adapter, kept port-typed so the presentation layer can /// `subscribe_output` to pump bytes into the [`PtyBridge`] (it owns transport). pub pty_port: Arc, /// Active-terminal registry shared by the terminal use cases. pub terminal_sessions: Arc, /// The domain event bus (also handed to the event relay). pub event_bus: Arc, /// Generic PTY↔Channel bridge registry (consumed by L3). pub pty_bridge: Arc, // --- Agents (L6) --- /// Create a project agent from scratch. pub create_agent: Arc, /// List a project's agents. pub list_agents: Arc, /// Read an agent's Markdown context. pub read_agent_context: Arc, /// Overwrite an agent's Markdown context. pub update_agent_context: Arc, /// Delete an agent from the manifest. pub delete_agent: Arc, /// Launch an agent (spawn PTY, apply injection strategy). pub launch_agent: Arc, /// Best-effort inspection of a conversation (last topic + token indicator) /// for the resume popup (T7). Optional/extensible: backed by a `Vec` of /// [`domain::ports::SessionInspector`]s; an empty/missing match yields empty /// details, never an error. pub inspect_conversation: Arc, /// Project registry — used by agent commands to resolve a `Project` from an id. pub project_store: Arc, // --- Windows (L10) --- /// Detach a tab into a new OS window (persists the workspace topology). pub move_tab: Arc, // --- Templates & sync (L7) --- /// Create a template in the global store. pub create_template: Arc, /// Update a template's content (bumps version). pub update_template: Arc, /// List all templates in the global store. pub list_templates: Arc, /// Delete a template from the global store. pub delete_template: Arc, /// Create an agent from a template. pub create_agent_from_template: Arc, /// Detect which synchronized agents are behind their template. pub detect_agent_drift: Arc, /// Apply a template update to a synchronized agent. pub sync_agent_with_template: Arc, // --- Git (L8) --- /// Report the working-tree status of a repository. pub git_status: Arc, /// Stage a path. pub git_stage: Arc, /// Unstage a path. pub git_unstage: Arc, /// Create a commit. pub git_commit: Arc, /// List branches. pub git_branches: Arc, /// Check out a branch. pub git_checkout: Arc, /// Return the recent commit log. pub git_log: Arc, /// Initialise a repository. pub git_init: Arc, /// Return the commit graph for all local branches. pub git_graph: Arc, // --- Skills (L12) --- /// Create a skill in a scope's store. pub create_skill: Arc, /// Update a skill's content. pub update_skill: Arc, /// List skills in a scope. pub list_skills: Arc, /// Delete a skill from its scope's store. pub delete_skill: Arc, /// Assign a skill to an agent (records a `SkillRef`). pub assign_skill: Arc, /// Unassign a skill from an agent. pub unassign_skill: Arc, // --- Memory (LOT A — §14.5.1) --- /// Create a memory note in the project's store. pub create_memory: Arc, /// Replace an existing memory note. pub update_memory: Arc, /// List the project's memory notes. pub list_memories: Arc, /// Read one memory note by slug. pub get_memory: Arc, /// Delete a memory note. pub delete_memory: Arc, /// Read the structured `MEMORY.md` index. pub read_memory_index: Arc, /// Resolve a note's outgoing `[[slug]]` links. pub resolve_memory_links: Arc, /// Recall the most relevant memory entries for a query within a budget /// (LOT B — §14.5.2). pub recall_memory: Arc, // --- Orchestrator (§14.3) --- /// Dispatches validated orchestrator requests to the agent/skill use cases. /// Shared by every per-project filesystem watcher. pub orchestrator_service: Arc, /// Live orchestrator request watchers, keyed by project. One watcher per open /// project tails its `.ideai/requests/` tree; dropping the handle stops it. /// Guarded by a `Mutex` so the open/close commands can register/unregister /// watchers concurrently. pub orchestrator_watchers: Mutex>, } impl AppState { /// **Composition root.** Builds all adapters and use cases. /// /// `app_data_dir` is the machine-local IDE data directory (ARCHITECTURE /// §9.2), resolved by the caller via the Tauri path API and injected here so /// the stores never touch Tauri themselves (Dependency Inversion). /// /// This is the only function that constructs concrete adapters; every other /// layer depends on ports. Adapters added in later lots (PTY, git, remote) /// are wired in here. #[must_use] pub fn build(app_data_dir: PathBuf) -> Self { // --- Concrete adapters (driven adapters) --- let event_bus = Arc::new(TokioBroadcastEventBus::new()); let clock = Arc::new(SystemClock::new()); let ids = Arc::new(UuidGenerator::new()); let fs = Arc::new(LocalFileSystem::new()); let store = Arc::new(FsProjectStore::new( Arc::clone(&fs) as Arc, app_data_dir.to_string_lossy().into_owned(), )); // Port-typed handles for injection. let fs_port = Arc::clone(&fs) as Arc; let store_port = Arc::clone(&store) as Arc; let events_port = Arc::clone(&event_bus) as Arc; // --- Use cases (ports injected as Arc) --- let health = Arc::new(HealthUseCase::new( Arc::clone(&clock) as Arc, Arc::clone(&ids) as Arc, Arc::clone(&events_port), )); let create_project = Arc::new(CreateProject::new( Arc::clone(&store_port), Arc::clone(&fs_port), Arc::clone(&ids) as Arc, Arc::clone(&clock) as Arc, Arc::clone(&events_port), )); let open_project = Arc::new(OpenProject::new( Arc::clone(&store_port), Arc::clone(&fs_port), )); let close_project = Arc::new(CloseProject::new(Arc::clone(&store_port))); let close_tab = Arc::new(CloseTab::new(Arc::clone(&store_port))); let list_projects = Arc::new(ListProjects::new(Arc::clone(&store_port))); // --- PTY adapter + terminal use cases (L3) --- let pty = Arc::new(PortablePtyAdapter::new()); let pty_port = Arc::clone(&pty) as Arc; let terminal_sessions = Arc::new(TerminalSessions::new()); let open_terminal = Arc::new(OpenTerminal::new( Arc::clone(&pty_port), Arc::clone(&terminal_sessions), Arc::clone(&events_port), )); let write_terminal = Arc::new(WriteToTerminal::new( Arc::clone(&pty_port), Arc::clone(&terminal_sessions), )); let resize_terminal = Arc::new(ResizeTerminal::new( Arc::clone(&pty_port), Arc::clone(&terminal_sessions), )); let close_terminal = Arc::new(CloseTerminal::new( Arc::clone(&pty_port), Arc::clone(&terminal_sessions), )); // --- Layout use cases (L4 + #4) --- let load_layout = Arc::new(LoadLayout::new( Arc::clone(&store_port), Arc::clone(&fs_port), )); let mutate_layout = Arc::new(MutateLayout::new( Arc::clone(&store_port), Arc::clone(&fs_port), Arc::clone(&events_port), )); let list_layouts = Arc::new(ListLayouts::new( Arc::clone(&store_port), Arc::clone(&fs_port), )); let create_layout = Arc::new(CreateLayout::new( Arc::clone(&store_port), Arc::clone(&fs_port), Arc::clone(&ids) as Arc, Arc::clone(&events_port), )); let rename_layout = Arc::new(RenameLayout::new( Arc::clone(&store_port), Arc::clone(&fs_port), Arc::clone(&events_port), )); let delete_layout = Arc::new(DeleteLayout::new( Arc::clone(&store_port), Arc::clone(&fs_port), Arc::clone(&events_port), )); let set_active_layout = Arc::new(SetActiveLayout::new( Arc::clone(&store_port), Arc::clone(&fs_port), Arc::clone(&events_port), )); // Close-time snapshot of running agents (T5). Shares the SAME live-session // registry as the terminal/agent use cases, so its liveness check reflects // the very PTYs the shutdown hook is about to kill — it must run *before*. let snapshot_running_agents = Arc::new(SnapshotRunningAgents::new( Arc::clone(&store_port), Arc::clone(&fs_port), Arc::clone(&terminal_sessions) as Arc, )); // --- Profiles & AI runtime (L5) --- // One generic, profile-driven runtime adapter (Open/Closed): it holds the // process spawner used for detection. The profile store persists // `profiles.json` in the same machine-local app-data dir as the project // registry. let spawner = Arc::new(LocalProcessSpawner::new()); let spawner_port = Arc::clone(&spawner) as Arc; let runtime = Arc::new(CliAgentRuntime::new(Arc::clone(&spawner_port))); let runtime_port = Arc::clone(&runtime) as Arc; let profile_store = Arc::new(FsProfileStore::new( Arc::clone(&fs_port), app_data_dir.to_string_lossy().into_owned(), )); let profile_store_port = Arc::clone(&profile_store) as Arc; let detect_profiles = Arc::new(DetectProfiles::new(Arc::clone(&runtime_port))); let list_profiles = Arc::new(ListProfiles::new(Arc::clone(&profile_store_port))); let save_profile = Arc::new(SaveProfile::new(Arc::clone(&profile_store_port))); let delete_profile = Arc::new(DeleteProfile::new(Arc::clone(&profile_store_port))); let configure_profiles = Arc::new(ConfigureProfiles::new(Arc::clone(&profile_store_port))); let reference_profiles = Arc::new(ReferenceProfiles::new()); let first_run_state = Arc::new(FirstRunState::new(Arc::clone(&profile_store_port))); let pty_bridge = Arc::new(PtyBridge::new()); // --- Agent context store + use cases (L6) --- let contexts = Arc::new(IdeaiContextStore::new(Arc::clone(&fs_port))); let contexts_port = Arc::clone(&contexts) as Arc; // --- Skill store (L12) --- // Global skills live in the machine-local app-data dir; project skills are // resolved per call from each project's `.ideai/` (so one store serves all // open projects). Shared by the skill use cases and the agent launcher // (assigned-skill injection into the convention file, §14.2). let skill_store = Arc::new(FsSkillStore::new( Arc::clone(&fs_port), app_data_dir.to_string_lossy().into_owned(), )); let skill_store_port = Arc::clone(&skill_store) as Arc; // Memory store + naïve recall (LOT A/B — §14.5.1/§14.5.4). Built here so the // recall port can be injected into LaunchAgent below (it composes the project // memory recall into the convention file at activation); the memory use cases // are wired further down from these same instances. let memory_store = Arc::new(FsMemoryStore::new(Arc::clone(&fs_port))); let memory_store_port = Arc::clone(&memory_store) as Arc; let memory_recall_port = Arc::new(NaiveMemoryRecall::new(Arc::clone(&memory_store_port))) as Arc; let create_agent = Arc::new(CreateAgentFromScratch::new( Arc::clone(&contexts_port), Arc::clone(&ids) as Arc, Arc::clone(&events_port), )); let list_agents = Arc::new(ListAgents::new(Arc::clone(&contexts_port))); let read_agent_context = Arc::new(ReadAgentContext::new(Arc::clone(&contexts_port))); let update_agent_context = Arc::new(UpdateAgentContext::new(Arc::clone(&contexts_port))); let delete_agent = Arc::new(DeleteAgent::new( Arc::clone(&contexts_port), Arc::clone(&events_port), )); // LaunchAgent shares the SAME pty_port and terminal_sessions as the terminal // use cases — indispensable for the PtyBridge to work correctly. let launch_agent = Arc::new(LaunchAgent::new( Arc::clone(&contexts_port), Arc::clone(&profile_store_port), Arc::clone(&runtime_port), Arc::clone(&fs_port), Arc::clone(&pty_port), Arc::clone(&skill_store_port), Arc::clone(&terminal_sessions), Arc::clone(&events_port), Arc::clone(&ids) as Arc, Arc::clone(&memory_recall_port), )); // --- Conversation inspection (T7) --- // Best-effort, optional, extensible: a `Vec` of SessionInspectors routed // by profile. Adding an inspectable CLI = pushing one more adapter here. // The Claude inspector reads `/.claude/projects//.jsonl`; // `$HOME` is resolved from the environment (empty string if unset — the // inspector then simply finds nothing and yields empty details). let home_dir = std::env::var("HOME") .or_else(|_| std::env::var("USERPROFILE")) .unwrap_or_default(); let inspectors: Vec> = vec![Arc::new(ClaudeTranscriptInspector::new( Arc::clone(&fs_port), home_dir, ))]; let inspect_conversation = Arc::new(InspectConversation::new( Arc::clone(&contexts_port), Arc::clone(&profile_store_port), inspectors, )); let project_store = Arc::clone(&store_port); // --- Template store + use cases (L7) --- let template_store = Arc::new(FsTemplateStore::new( Arc::clone(&fs_port), app_data_dir.to_string_lossy().into_owned(), )); let template_store_port = Arc::clone(&template_store) as Arc; let create_template = Arc::new(CreateTemplate::new( Arc::clone(&template_store_port), Arc::clone(&ids) as Arc, )); let update_template = Arc::new(UpdateTemplate::new( Arc::clone(&template_store_port), Arc::clone(&events_port), )); let list_templates = Arc::new(ListTemplates::new(Arc::clone(&template_store_port))); let delete_template = Arc::new(DeleteTemplate::new(Arc::clone(&template_store_port))); let create_agent_from_template = Arc::new(CreateAgentFromTemplate::new( Arc::clone(&template_store_port), Arc::clone(&contexts_port), Arc::clone(&ids) as Arc, Arc::clone(&events_port), )); let detect_agent_drift = Arc::new(DetectAgentDrift::new( Arc::clone(&template_store_port), Arc::clone(&contexts_port), Arc::clone(&events_port), )); let sync_agent_with_template = Arc::new(SyncAgentWithTemplate::new( Arc::clone(&template_store_port), Arc::clone(&contexts_port), Arc::clone(&events_port), )); // --- Git adapter + use cases (L8) --- let git = Arc::new(Git2Repository::new()); let git_port = Arc::clone(&git) as Arc; let git_status = Arc::new(GitStatus::new(Arc::clone(&git_port))); let git_stage = Arc::new(GitStage::new(Arc::clone(&git_port))); let git_unstage = Arc::new(GitUnstage::new(Arc::clone(&git_port))); let git_commit = Arc::new(GitCommit::new(Arc::clone(&git_port), Arc::clone(&events_port))); let git_branches = Arc::new(GitBranches::new(Arc::clone(&git_port))); let git_checkout = Arc::new(GitCheckout::new( Arc::clone(&git_port), Arc::clone(&events_port), )); let git_log = Arc::new(GitLog::new(Arc::clone(&git_port))); let git_init = Arc::new(GitInit::new(Arc::clone(&git_port), Arc::clone(&events_port))); let git_graph = Arc::new(GitGraph::new(Arc::clone(&git_port))); // --- Skill use cases (L12) --- // Reuse the skill store (built above for the launcher) and the shared // agent context store for the agent↔skill assignment. let create_skill = Arc::new(CreateSkill::new( Arc::clone(&skill_store_port), Arc::clone(&ids) as Arc, )); let update_skill = Arc::new(UpdateSkill::new(Arc::clone(&skill_store_port))); let list_skills = Arc::new(ListSkills::new(Arc::clone(&skill_store_port))); let delete_skill = Arc::new(DeleteSkill::new(Arc::clone(&skill_store_port))); let assign_skill = Arc::new(AssignSkillToAgent::new( Arc::clone(&contexts_port), Arc::clone(&events_port), )); let unassign_skill = Arc::new(UnassignSkillFromAgent::new( Arc::clone(&contexts_port), Arc::clone(&events_port), )); // --- Memory use cases (LOT A — §14.5.1) --- // `memory_store` / `memory_store_port` / `memory_recall_port` are built // earlier (above LaunchAgent, which needs the recall port). A single // FsMemoryStore takes the project root per call (like the skill store), so // one instance serves every open project. The mutating use cases share the // event bus to announce Memory{Saved,Deleted}. let create_memory = Arc::new(CreateMemory::new( Arc::clone(&memory_store_port), Arc::clone(&events_port), )); let update_memory = Arc::new(UpdateMemory::new( Arc::clone(&memory_store_port), Arc::clone(&events_port), )); let list_memories = Arc::new(ListMemories::new(Arc::clone(&memory_store_port))); let get_memory = Arc::new(GetMemory::new(Arc::clone(&memory_store_port))); let delete_memory = Arc::new(DeleteMemory::new( Arc::clone(&memory_store_port), Arc::clone(&events_port), )); let read_memory_index = Arc::new(ReadMemoryIndex::new(Arc::clone(&memory_store_port))); let resolve_memory_links = Arc::new(ResolveMemoryLinks::new(Arc::clone(&memory_store_port))); // Naïve recall (LOT B): the same instance injected into LaunchAgent above — // composes the store, returns index entries in order truncated to the token // budget. The default, dependency-free MemoryRecall; substitutable by a // VectorMemoryRecall (LOT C). let recall_memory = Arc::new(RecallMemory::new(Arc::clone(&memory_recall_port))); // --- Orchestrator service (§14.3) --- // Dispatches file-based orchestrator requests through the *same* use cases // the UI drives (IdeA stays the single source of truth for the agent/skill // lifecycle). The per-project watcher that feeds it is started lazily when // a project is opened (see `ensure_orchestrator_watch`). let orchestrator_service = Arc::new(OrchestratorService::new( Arc::clone(&create_agent), Arc::clone(&launch_agent), Arc::clone(&list_agents), Arc::clone(&close_terminal), Arc::clone(&update_agent_context), Arc::clone(&create_skill), Arc::clone(&profile_store_port), Arc::clone(&terminal_sessions), )); // --- Windows (L10) --- let move_tab = Arc::new(MoveTabToNewWindow::new( Arc::clone(&store_port), Arc::clone(&ids) as Arc, )); Self { health, create_project, open_project, close_project, close_tab, list_projects, open_terminal, write_terminal, resize_terminal, close_terminal, load_layout, mutate_layout, list_layouts, create_layout, rename_layout, delete_layout, set_active_layout, snapshot_running_agents, detect_profiles, list_profiles, save_profile, delete_profile, configure_profiles, reference_profiles, first_run_state, pty_port, terminal_sessions, event_bus, pty_bridge, create_agent, list_agents, read_agent_context, update_agent_context, delete_agent, launch_agent, inspect_conversation, project_store, create_template, update_template, list_templates, delete_template, create_agent_from_template, detect_agent_drift, sync_agent_with_template, git_status, git_stage, git_unstage, git_commit, git_branches, git_checkout, git_log, git_init, git_graph, create_skill, update_skill, list_skills, delete_skill, assign_skill, unassign_skill, create_memory, update_memory, list_memories, get_memory, delete_memory, read_memory_index, resolve_memory_links, recall_memory, orchestrator_service, orchestrator_watchers: Mutex::new(HashMap::new()), move_tab, } } /// Starts a [`FsOrchestratorWatcher`] for `project` if one is not already /// running for it (idempotent). The watcher tails the project's /// `.ideai/requests/` tree and dispatches each request through the shared /// [`OrchestratorService`]; processed-request events are republished on the /// domain event bus so the relay surfaces them to the frontend. /// /// Called from the `open_project` / `create_project` commands. Must run inside /// the Tokio runtime (the watcher spawns a background task) — Tauri async /// commands satisfy this. pub fn ensure_orchestrator_watch(&self, project: &Project) { let mut watchers = self .orchestrator_watchers .lock() .expect("orchestrator watcher registry poisoned"); if watchers.contains_key(&project.id) { return; } let bus = Arc::clone(&self.event_bus); let events: Arc = Arc::new(move |event| bus.publish(event)); let handle = FsOrchestratorWatcher::start( project.clone(), Arc::clone(&self.orchestrator_service), events, ); watchers.insert(project.id, handle); } /// Returns the ids of every currently-open project. /// /// Derived from the orchestrator watcher registry, which holds exactly one /// entry per open project (started on open/create, dropped on close). Used by /// the shutdown hook to snapshot running agents across all open projects /// before the global PTY kill. #[must_use] pub fn open_project_ids(&self) -> Vec { self.orchestrator_watchers .lock() .map(|w| w.keys().copied().collect()) .unwrap_or_default() } /// Stops and removes the orchestrator watcher for `project_id`, if any. /// Called from `close_project` so a closed project stops consuming requests. pub fn stop_orchestrator_watch(&self, project_id: &ProjectId) { if let Some(handle) = self .orchestrator_watchers .lock() .expect("orchestrator watcher registry poisoned") .remove(project_id) { handle.stop(); } } }