Files
IdeA/crates/application/src/layout/snapshot.rs
Blomios 3ed0f6b45f feat(terminals): reprise de conversation par cellule + fix ordre d'écriture
Permet de recharger la conversation CLI précédente de chaque cellule à la
réouverture du projet, de façon universelle (indépendant du modèle/CLI).

- profil AgentRuntime: bloc déclaratif optionnel `session { assignFlag, resumeFlag }`
- LeafCell: `conversationId` (persistant, distinct du SessionId PTY) + `agentWasRunning`
- runtime: SessionPlan (None/Assign/Resume) + composition pure des args
- LaunchAgent: décide Assign vs Resume, génère l'UUID, remonte l'id assigné
  (persistance par l'appelant via setCellConversation — découplage SRP)
- close: SnapshotRunningAgents fige `agentWasRunning` avant le kill-all
  (statut clot/en cours universel, sans parsing CLI)
- SessionInspector: port optionnel best-effort + adapter ClaudeTranscriptInspector
- popup de reprise par cellule (statut + sujet/tokens si dispo), intercalée
  avant le Resume auto, jamais sur le chemin reattach

fix(terminals): sérialise les écritures PTY (file FIFO par handle) — corrige
les caractères mélangés/accents dus au réordonnancement des invoke Tauri concurrents

fix(layout): l'opération `move` préservait mal les champs du leaf (perdait `agent`)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 22:27:08 +02:00

114 lines
4.3 KiB
Rust

//! [`SnapshotRunningAgents`] — freeze, at close time, which agent cells still
//! held a live PTY (feature: "conversation resume", task T5).
//!
//! When the IDE (or a single project) closes, every live PTY is about to be
//! killed. *Before* that happens, we record on each agent-bearing leaf whether
//! its agent process was still running (`agent_was_running = true`) or had
//! already exited / never launched (`false`). On reopen, that flag is what tells
//! the resume popup "this conversation was still in progress" vs "it was closed".
//!
//! The decision is **universal**: it is derived purely from the process
//! lifecycle (the live-session registry), never from parsing CLI output. The use
//! case itself is a thin orchestrator over:
//! - the persisted layouts store ([`super::store`]),
//! - the pure domain operation [`domain::LayoutTree::set_agent_running`],
//! - a [`LiveAgentRegistry`] liveness query.
//!
//! **Ordering contract:** this snapshot reads the registry *as it is at call
//! time*. The composition root (app-tauri) is responsible for calling it
//! **before** the global PTY kill; if the kill ran first, every agent would look
//! "closed". See `app-tauri`'s `CloseRequested` hook and `close.rs` callers.
use std::sync::Arc;
use domain::ports::{FileSystem, ProjectStore};
use domain::ProjectId;
use crate::error::AppError;
use crate::terminal::LiveAgentRegistry;
use super::store::{persist_doc, resolve_doc};
/// Input for [`SnapshotRunningAgents::execute`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SnapshotRunningAgentsInput {
/// The project whose layouts must be frozen.
pub project_id: ProjectId,
}
/// Output of [`SnapshotRunningAgents::execute`].
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct SnapshotRunningAgentsOutput {
/// Number of agent-bearing leaves that were found running at snapshot time.
pub running: usize,
/// Number of agent-bearing leaves that were found stopped at snapshot time.
pub stopped: usize,
}
/// Freezes the `agent_was_running` flag on every agent leaf of a project's
/// layouts, from the live-session registry, then persists the layouts.
pub struct SnapshotRunningAgents {
store: Arc<dyn ProjectStore>,
fs: Arc<dyn FileSystem>,
live: Arc<dyn LiveAgentRegistry>,
}
impl SnapshotRunningAgents {
/// Builds the use case from its injected ports.
#[must_use]
pub fn new(
store: Arc<dyn ProjectStore>,
fs: Arc<dyn FileSystem>,
live: Arc<dyn LiveAgentRegistry>,
) -> Self {
Self { store, fs, live }
}
/// Executes the snapshot for one project.
///
/// Walks every named layout of the project; for each agent-bearing leaf it
/// applies [`domain::LayoutTree::set_agent_running`] with the agent's *current*
/// liveness, then persists the whole layouts store. A project with no
/// agent leaf is a no-op (the doc is still resolved/healed, never written if
/// unchanged).
///
/// # Errors
/// - [`AppError::NotFound`] if the project is unknown,
/// - [`AppError::FileSystem`] on persistence failure,
/// - [`AppError::Store`] on registry I/O failure.
pub async fn execute(
&self,
input: SnapshotRunningAgentsInput,
) -> Result<SnapshotRunningAgentsOutput, AppError> {
let project = self.store.load_project(input.project_id).await?;
let mut doc = resolve_doc(self.fs.as_ref(), &project).await?;
let mut out = SnapshotRunningAgentsOutput::default();
let mut changed = false;
for named in &mut doc.layouts {
for (leaf_id, agent_id) in named.tree.agent_leaves() {
let running = self.live.is_agent_live(&agent_id);
if running {
out.running += 1;
} else {
out.stopped += 1;
}
// Pure op: only NodeNotFound is possible, which cannot happen
// since `leaf_id` came from this very tree.
named.tree = named
.tree
.set_agent_running(leaf_id, running)
.map_err(|e| AppError::Invalid(e.to_string()))?;
changed = true;
}
}
if changed {
persist_doc(self.fs.as_ref(), &project, &doc).await?;
}
Ok(out)
}
}