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>
This commit is contained in:
2026-06-07 22:27:08 +02:00
parent d11eaaa8c0
commit 3ed0f6b45f
61 changed files with 5098 additions and 98 deletions

View File

@ -65,6 +65,23 @@ pub enum ContextInjectionPlan {
},
}
/// Intention de session pour un lancement d'agent donné.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SessionPlan {
/// Pas de reprise : profil sans bloc `session`, ou cellule neuve sans id.
None,
/// Premier lancement : IdeA a généré `conversation_id`, à assigner via assign_flag.
Assign {
/// Identifiant de conversation généré par IdeA pour ce lancement.
conversation_id: String,
},
/// Réouverture : reprendre la conversation existante via resume_flag.
Resume {
/// Identifiant de la conversation existante à reprendre.
conversation_id: String,
},
}
/// A fully-resolved process invocation: command, args, cwd, environment, and the
/// plan for delivering the agent context.
#[derive(Debug, Clone, PartialEq, Eq)]
@ -91,6 +108,22 @@ pub struct PreparedContext {
pub relative_path: String,
}
/// Enriched, **best-effort** details about a conversation, specific to a CLI's
/// on-disk transcript format (CONTEXT §T6).
///
/// Every field is optional: the core never *requires* this data, and a missing
/// piece (or a missing inspector) must never block a resume. The values exist
/// purely to enrich a resume popup (last topic, token indicator).
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ConversationDetails {
/// A short, best-effort label for the conversation (e.g. the last user
/// message, truncated). `None` when it cannot be extracted.
pub last_topic: Option<String>,
/// A best-effort cumulative token count for the conversation. `None` when no
/// usage information is available.
pub token_count: Option<u64>,
}
/// An opaque handle to a live PTY, owned by the adapter.
///
/// The domain only needs an identity to address the PTY in subsequent calls;
@ -235,6 +268,21 @@ pub enum RemoteError {
Auth(String),
}
/// Errors from a [`SessionInspector`].
///
/// Inspection is **best-effort and optional** (CONTEXT §T6/T7): these errors
/// must never block a conversation resume. A caller routing several inspectors
/// simply treats any error as "no enriched details available".
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum InspectError {
/// No transcript was found for the requested conversation.
#[error("conversation transcript not found")]
NotFound,
/// The transcript could not be read.
#[error("conversation transcript read failed: {0}")]
Read(String),
}
/// Errors from the git [`GitPort`].
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum GitError {
@ -309,6 +357,7 @@ pub trait AgentRuntime: Send + Sync {
profile: &AgentProfile,
ctx: &PreparedContext,
cwd: &ProjectPath,
session: &SessionPlan,
) -> Result<SpawnSpec, RuntimeError>;
}
@ -707,6 +756,39 @@ pub trait GitPort: Send + Sync {
async fn push(&self, root: &ProjectPath) -> Result<(), GitError>;
}
/// **Optional** capability to read enriched details from a CLI's conversation
/// transcript (CONTEXT §T6).
///
/// This port is optional *by construction*: it backs a best-effort resume popup
/// and is never wired as a hard dependency of the launch/resume flow. A caller
/// (the future `InspectConversation` use case, §T7) holds a
/// `Vec<Arc<dyn SessionInspector>>` and routes a profile to the first inspector
/// whose [`supports`](Self::supports) returns `true`; if none matches, or the
/// call errors, the resume proceeds with no enriched details.
///
/// All Claude/Codex/Gemini transcript shapes stay **inside the adapter**: no
/// CLI-specific type ever crosses this boundary — only [`ConversationDetails`].
#[async_trait]
pub trait SessionInspector: Send + Sync {
/// Returns whether this inspector knows how to read transcripts for the
/// given profile (e.g. by recognising its context-injection convention).
fn supports(&self, profile: &AgentProfile) -> bool;
/// Reads best-effort [`ConversationDetails`] for `conversation_id` whose
/// agent runs in `cwd`.
///
/// # Errors
/// [`InspectError::NotFound`] when no transcript exists for the conversation;
/// [`InspectError::Read`] on an I/O / decoding failure. Malformed *lines*
/// inside an otherwise-readable transcript are skipped, not surfaced.
async fn details(
&self,
profile: &AgentProfile,
conversation_id: &str,
cwd: &ProjectPath,
) -> Result<ConversationDetails, InspectError>;
}
/// Publish/subscribe domain events. Synchronous, in-process.
pub trait EventBus: Send + Sync {
/// Publishes an event.