Files
IdeA/crates/app-tauri/src/lib.rs
Blomios b39c11a64d fix(agents): enforcer l'invariant « 1 session vivante par agent » (singleton)
Un agent ne peut tourner que dans une seule cellule à la fois. La garde dans
LaunchAgent refuse le spawn si l'agent est déjà vivant dans un autre node
(AGENT_ALREADY_RUNNING) ; idempotent sur le même node ; le chemin resume
(agent mort) reste inchangé. Le node_id est désormais plombé jusqu'au use case.

Corrige le reset asymétrique d'une cellule au changement d'onglet : deux leaves
partageant le même agent id rendaient session_for_agent/is_agent_live/stop_agent
ambigus (cible arbitraire). Le churn reset/reattach déclenchait aussi les accents
mélangés (FIFO intact, non touché).

- snapshot agentWasRunning calculé par node (is_node_live) et non par agent
- commande list_live_agents + live_agents()/node_for_agent()/is_node_live()
- UI : dropdown grise les agents déjà placés ailleurs ; 2e cellule en doublon
  affiche « disponible » au lieu d'une relance fantôme

Tests : cargo test (application + app-tauri) vert ; tsc + vitest vert.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 14:43:48 +02:00

157 lines
6.4 KiB
Rust

//! # IdeA — `app-tauri` (presentation / driving adapter + composition root)
//!
//! This crate is the **only** place that knows every other crate. It:
//! - builds the concrete adapters and injects them into use cases
//! ([`state::AppState`], the composition root),
//! - exposes `#[tauri::command]` handlers ([`commands`]) mapping DTOs ↔ use cases,
//! - relays domain events to the frontend ([`events::TauriEventRelay`]),
//! - hosts the generic PTY↔Channel bridge ([`pty::PtyBridge`]) for L3.
//!
//! The wiring lives in the library (testable) and `main.rs` is a thin shim.
#![forbid(unsafe_code)]
#![warn(missing_docs)]
pub mod commands;
pub mod dto;
pub mod events;
pub mod pty;
pub mod state;
use tauri::Manager;
use state::AppState;
/// Builds and runs the Tauri application.
///
/// Sets up the composition root (resolving the app-data directory via the Tauri
/// path API), registers commands, spawns the event relay, and starts the main
/// window.
///
/// # Panics
/// Panics if the Tauri application fails to build or run (no window/webview), or
/// if the app-data directory cannot be resolved.
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_dialog::init())
.setup(|app| {
// Resolve the machine-local IDE data directory (ARCHITECTURE §9.2)
// and build the composition root once the app handle exists, so the
// stores receive a concrete path without ever touching Tauri.
let app_data_dir = app
.path()
.app_data_dir()
.expect("failed to resolve the app data directory");
let app_state = AppState::build(app_data_dir);
// Wire the domain event bus → Tauri events relay.
events::spawn_relay(app.handle().clone(), &app_state.event_bus);
app.manage(app_state);
// Kill all live PTYs cleanly when the main window is closing. This is
// independent of the per-view (navigation/layout) lifecycle — those
// must NEVER kill a PTY — and only fires on a genuine app shutdown.
// A brutal crash is best-effort and out of scope.
if let Some(window) = app.get_webview_window("main") {
let handle = app.handle().clone();
window.on_window_event(move |event| {
if let tauri::WindowEvent::CloseRequested { .. } = event {
if let Some(state) = handle.try_state::<AppState>() {
let pty = std::sync::Arc::clone(&state.pty_port);
// ORDER IS CRITICAL: freeze `agent_was_running` on every
// agent leaf of every open project FIRST, reading the live
// PTY registry as it stands now; only THEN kill the PTYs.
// If we killed first, the registry would be empty and every
// agent would be persisted as "closed".
let snapshot = std::sync::Arc::clone(&state.snapshot_running_agents);
let open_projects = state.open_project_ids();
let handles = state.terminal_sessions.handles();
tauri::async_runtime::block_on(async move {
for project_id in open_projects {
let _ = snapshot
.execute(application::SnapshotRunningAgentsInput {
project_id,
})
.await;
}
for h in handles {
let _ = pty.kill(&h).await;
}
});
}
}
});
}
Ok(())
})
.invoke_handler(tauri::generate_handler![
commands::health,
commands::create_project,
commands::open_project,
commands::close_project,
commands::list_projects,
commands::open_terminal,
commands::write_terminal,
commands::resize_terminal,
commands::close_terminal,
commands::reattach_terminal,
commands::load_layout,
commands::mutate_layout,
commands::list_layouts,
commands::create_layout,
commands::rename_layout,
commands::delete_layout,
commands::set_active_layout,
commands::first_run_state,
commands::reference_profiles,
commands::detect_profiles,
commands::list_profiles,
commands::save_profile,
commands::delete_profile,
commands::configure_profiles,
commands::create_agent,
commands::list_agents,
commands::list_live_agents,
commands::read_agent_context,
commands::update_agent_context,
commands::delete_agent,
commands::launch_agent,
commands::inspect_conversation,
commands::create_template,
commands::update_template,
commands::list_templates,
commands::delete_template,
commands::create_agent_from_template,
commands::detect_agent_drift,
commands::sync_agent_with_template,
commands::git_status,
commands::git_stage,
commands::git_unstage,
commands::git_commit,
commands::git_branches,
commands::git_checkout,
commands::git_log,
commands::git_init,
commands::git_graph,
commands::create_skill,
commands::update_skill,
commands::list_skills,
commands::delete_skill,
commands::assign_skill_to_agent,
commands::unassign_skill_from_agent,
commands::create_memory,
commands::update_memory,
commands::list_memories,
commands::get_memory,
commands::delete_memory,
commands::read_memory_index,
commands::recall_memory,
commands::resolve_memory_links,
commands::move_tab_to_new_window,
])
.run(tauri::generate_context!())
.expect("error while running IdeA Tauri application");
}