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>
114 lines
4.3 KiB
Rust
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)
|
|
}
|
|
}
|