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:
@ -231,6 +231,12 @@ pub struct TerminalSessionDto {
|
||||
pub rows: u16,
|
||||
/// Current cols.
|
||||
pub cols: u16,
|
||||
/// Conversation id **assigned** by this launch, when the agent's profile
|
||||
/// supports session assignment and the hosting cell had none yet (T4b). The
|
||||
/// front persists it on the leaf (`setCellConversation`) so the next open
|
||||
/// resumes. `None` for a plain terminal, a resume, or a degraded launch.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub assigned_conversation_id: Option<String>,
|
||||
}
|
||||
|
||||
impl From<OpenTerminalOutput> for TerminalSessionDto {
|
||||
@ -241,6 +247,7 @@ impl From<OpenTerminalOutput> for TerminalSessionDto {
|
||||
cwd: s.cwd.as_str().to_owned(),
|
||||
rows: s.pty_size.rows,
|
||||
cols: s.pty_size.cols,
|
||||
assigned_conversation_id: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -436,6 +443,15 @@ pub enum LayoutOperationDto {
|
||||
#[serde(default)]
|
||||
agent: Option<String>,
|
||||
},
|
||||
/// Record/clear the persistent CLI conversation id on a leaf (T4b).
|
||||
#[serde(rename_all = "camelCase")]
|
||||
SetCellConversation {
|
||||
/// Hosting leaf.
|
||||
target: String,
|
||||
/// Conversation id, or `null` to clear.
|
||||
#[serde(default)]
|
||||
conversation_id: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
impl LayoutOperationDto {
|
||||
@ -479,6 +495,13 @@ impl LayoutOperationDto {
|
||||
target: parse_node_id(&target)?,
|
||||
agent: agent.as_deref().map(parse_agent_id).transpose()?,
|
||||
},
|
||||
Self::SetCellConversation {
|
||||
target,
|
||||
conversation_id,
|
||||
} => LayoutOperation::SetCellConversation {
|
||||
target: parse_node_id(&target)?,
|
||||
conversation_id,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -832,7 +855,8 @@ pub fn parse_profile_id(raw: &str) -> Result<ProfileId, ErrorDto> {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
use application::{
|
||||
CreateAgentOutput, LaunchAgentOutput, ListAgentsOutput, ReadAgentContextOutput,
|
||||
CreateAgentOutput, InspectConversationOutput, LaunchAgentOutput, ListAgentsOutput,
|
||||
ReadAgentContextOutput,
|
||||
};
|
||||
use domain::Agent;
|
||||
|
||||
@ -915,16 +939,60 @@ pub struct LaunchAgentRequestDto {
|
||||
pub rows: u16,
|
||||
/// Initial terminal width in columns.
|
||||
pub cols: u16,
|
||||
/// Persistent CLI conversation id currently recorded on the hosting cell, if
|
||||
/// any. `Some` ⇒ the launch resumes it; absent/`None` ⇒ a fresh cell (the
|
||||
/// launch may assign a new id when the profile supports it).
|
||||
#[serde(default)]
|
||||
pub conversation_id: Option<String>,
|
||||
}
|
||||
|
||||
impl From<LaunchAgentOutput> for TerminalSessionDto {
|
||||
fn from(out: LaunchAgentOutput) -> Self {
|
||||
let assigned_conversation_id = out.assigned_conversation_id;
|
||||
let s = out.session;
|
||||
Self {
|
||||
session_id: s.id.to_string(),
|
||||
cwd: s.cwd.as_str().to_owned(),
|
||||
rows: s.pty_size.rows,
|
||||
cols: s.pty_size.cols,
|
||||
assigned_conversation_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Request DTO for `inspect_conversation` (T7).
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct InspectConversationRequestDto {
|
||||
/// Id of the owning project.
|
||||
pub project_id: String,
|
||||
/// Id of the agent whose conversation is inspected.
|
||||
pub agent_id: String,
|
||||
/// The conversation id recorded on the hosting cell.
|
||||
pub conversation_id: String,
|
||||
}
|
||||
|
||||
/// Response DTO for `inspect_conversation` (T7): the best-effort enriched
|
||||
/// details for a resume popup. Both fields are optional and **omitted from the
|
||||
/// wire when `None`** (`skip_serializing_if`), so the TypeScript side sees an
|
||||
/// absent key — not `null` — for a degraded inspection.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ConversationDetailsDto {
|
||||
/// A short, best-effort label for the conversation (last user message,
|
||||
/// truncated). Absent when it could not be extracted.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub last_topic: Option<String>,
|
||||
/// A best-effort cumulative token count. Absent when no usage info exists.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub token_count: Option<u64>,
|
||||
}
|
||||
|
||||
impl From<InspectConversationOutput> for ConversationDetailsDto {
|
||||
fn from(out: InspectConversationOutput) -> Self {
|
||||
Self {
|
||||
last_topic: out.details.last_topic,
|
||||
token_count: out.details.token_count,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user