feat(domain,application,infrastructure,backend,web-server,app-tauri,frontend): persistance et reprise des conversations de CLI custom par agent — index durable <projet>/.ideai/conversations/custom-index.json borné aux conversations non vides par agent (conversation_id logique, titre dérivé de la première invite, created/updated_at_ms), port CustomConversationIndex + adaptateur FsCustomConversationIndex, persistence des messages utilisateur et finals agent dans le log de conversation existant, DTO/commandes app-tauri et routes web-server pour lister/rouvrir, CustomAgentChatView: ouverture par défaut sur une nouvelle conversation, menu reprise d'une conversation passée ou nouvelle conversation, replay de l'historique sauvegardé (user+finals) à la réouverture, affichage normal des nouveaux messages après reprise, /clean ouvre une nouvelle conversation, aucune entrée créée si la conversation courante est vide, injection de l'historique dans le contexte agent différée au premier nouveau message après reprise — #363 (QA verte: git diff --check, cargo fmt --all -- --check, cargo tests ciblés infrastructure/application/web-server/app-tauri/backend, npm run typecheck, tests frontend ciblés et CustomAgentChatView, npm run build)

This commit is contained in:
2026-09-16 23:20:31 +02:00
parent 83b78f438f
commit 943c4525d7
30 changed files with 1794 additions and 109 deletions

View File

@ -1054,6 +1054,9 @@ pub enum SlashCommandEffectDto {
/// Number of retained chunks removed by the backend bridge.
#[serde(rename = "clearedChunks")]
cleared_chunks: usize,
/// Fresh ephemeral logical conversation id; it is indexed only on first send.
#[serde(rename = "conversationId", skip_serializing_if = "Option::is_none")]
conversation_id: Option<String>,
},
/// Placeholder for ticket #164.
ProfileSwitch {
@ -1085,6 +1088,7 @@ impl From<SlashCommandEffect> for SlashCommandEffectDto {
SlashCommandEffect::CleanConversation { session_id } => Self::CleanConversation {
session_id: session_id.to_string(),
cleared_chunks: 0,
conversation_id: None,
},
SlashCommandEffect::ProfileSwitch { session_id } => Self::ProfileSwitch {
session_id: session_id.to_string(),
@ -1444,6 +1448,18 @@ pub struct TerminalSessionDto {
/// [`CellKind::Pty`] (the historical, non-breaking shape: a PTY session DTO
/// always serialises with `cellKind: "pty"`).
pub cell_kind: CellKind,
/// Durable human-visible history loaded for an explicitly resumed custom chat.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub history: Vec<TurnViewDto>,
/// Whether older history pages exist before `history`.
#[serde(default)]
pub history_has_more: bool,
/// Anchor for the next older history page.
#[serde(skip_serializing_if = "Option::is_none")]
pub history_next_anchor: Option<String>,
/// True when this launch explicitly reopened an indexed custom conversation.
#[serde(default)]
pub resumed: bool,
}
/// Whether a session's hosting cell renders as a structured chat view or a raw
@ -1469,6 +1485,10 @@ impl From<OpenTerminalOutput> for TerminalSessionDto {
assigned_conversation_id: None,
engine_session_id: None,
cell_kind: CellKind::Pty,
history: Vec::new(),
history_has_more: false,
history_next_anchor: None,
resumed: false,
}
}
}
@ -3569,10 +3589,60 @@ impl From<LaunchAgentOutput> for TerminalSessionDto {
assigned_conversation_id,
engine_session_id,
cell_kind,
history: Vec::new(),
history_has_more: false,
history_next_anchor: None,
resumed: false,
}
}
}
/// Request for the durable custom CLI conversation menu.
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ListCustomConversationsRequestDto {
/// Owning project.
pub project_id: String,
/// Owning agent.
pub agent_id: String,
/// Requested maximum, defaulting and clamped to 50.
#[serde(default)]
pub limit: Option<usize>,
}
/// One conversation menu row.
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CustomConversationSummaryDto {
/// Logical conversation id.
pub conversation_id: String,
/// First-prompt title.
pub title: String,
/// Creation timestamp.
pub created_at_ms: u64,
/// Last durable activity timestamp.
pub updated_at_ms: u64,
}
impl From<domain::CustomConversationEntry> for CustomConversationSummaryDto {
fn from(entry: domain::CustomConversationEntry) -> Self {
Self {
conversation_id: entry.conversation_id.to_string(),
title: entry.title,
created_at_ms: entry.created_at_ms,
updated_at_ms: entry.updated_at_ms,
}
}
}
/// Recent custom CLI conversations for one agent.
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CustomConversationListDto {
/// Newest-first, non-empty rows.
pub conversations: Vec<CustomConversationSummaryDto>,
}
/// Request DTO for `interrupt_agent` (cadrage C4 §4.2): the Interrompre path. The
/// frontend sends `{ request: { projectId, agentId } }`.
#[derive(Debug, Clone, Deserialize)]
@ -3708,6 +3778,10 @@ impl From<TerminalSession> for TerminalSessionDto {
assigned_conversation_id: None,
engine_session_id: None,
cell_kind: CellKind::Pty,
history: Vec::new(),
history_has_more: false,
history_next_anchor: None,
resumed: false,
}
}
}

View File

@ -21,15 +21,15 @@ use application::{
ClearChatDraft, CloneOpenCodeProfileFromSeed, CloneProfileFromSeed, CloseProject, CloseTab,
CloseTerminal, CloseTicketAssistant, ConfigureProfiles, ContextGuardUseCases,
CreateAgentFromScratch, CreateAgentFromTemplate, CreateIssue, CreateLayout, CreateMemory,
CreateProject, CreateSkill, CreateSprint, CreateTemplate, DeleteAgent,
DeleteCustomProjectIconAsset, DeleteEmbedderProfile, DeleteIssue, DeleteLayout, DeleteMemory,
DeleteModelArtifact, DeleteModelServer, DeleteProfile, DeleteSkill, DeleteSprint,
DeleteTemplate, DescribeEmbedderEngines, DetectAgentDrift, DetectProfiles,
DismissEmbedderSuggestion, EnsureLocalModelServer, ExecuteSlashCommand, FirstRunState,
GcManagedTemp, GetAppExitWorkGuardState, GetCustomProjectIconAssetUsage, GetLiveStateLean,
GetMemory, GetProjectPermissions, GetProjectSystemPermissions, GetProjectWorkState,
GitBranches, GitCheckout, GitCommit, GitGraph, GitInit, GitLog, GitStage, GitStatus,
GitUnstage, HarvestMemoryFromTurn, HealthUseCase, ImportChatAttachments,
CreateProject, CreateSkill, CreateSprint, CreateTemplate, CustomConversationIndexProvider,
CustomConversationService, DeleteAgent, DeleteCustomProjectIconAsset, DeleteEmbedderProfile,
DeleteIssue, DeleteLayout, DeleteMemory, DeleteModelArtifact, DeleteModelServer, DeleteProfile,
DeleteSkill, DeleteSprint, DeleteTemplate, DescribeEmbedderEngines, DetectAgentDrift,
DetectProfiles, DismissEmbedderSuggestion, EnsureLocalModelServer, ExecuteSlashCommand,
FirstRunState, GcManagedTemp, GetAppExitWorkGuardState, GetCustomProjectIconAssetUsage,
GetLiveStateLean, GetMemory, GetProjectPermissions, GetProjectSystemPermissions,
GetProjectWorkState, GitBranches, GitCheckout, GitCommit, GitGraph, GitInit, GitLog, GitStage,
GitStatus, GitUnstage, HarvestMemoryFromTurn, HealthUseCase, ImportChatAttachments,
ImportCustomProjectIconAsset, InspectConversation, InstallPluginFromArchive,
InstallPluginFromDirectory, JsonPluginManifestValidator, LaunchAgent, LaunchAgentInput,
LinkIssues, ListAgents, ListAgentsInput, ListClaudeModels, ListCodexModels,
@ -107,20 +107,20 @@ use infrastructure::{
CliAgentRuntime, CodexPermissionProjector, CommandBackgroundRunner,
DiagnosticPluginWorkspaceExecAudit, EmbeddedCompatibilityMatrix, EmbedderEnvProbe,
ExternalMcpPluginSupervisor, FsAssistantContextStore, FsBackgroundTaskStore,
FsChatAttachmentStore, FsChatDraftStore, FsConversationLog, FsCustomProjectIconAssetStore,
FsDeviceSessionStore, FsEmbedderProfileStore, FsEmbedderPromptStore, FsHandoffStore,
FsIssueNumberAllocator, FsIssueStore, FsLiveStateStore, FsManagedTempStore,
FsMcpToolPermissionStore, FsMemoryStore, FsModelServerRegistry, FsOpenProjectRestoreStore,
FsOrchestratorWatcher, FsPermissionStore, FsPluginPackageStore, FsPluginRegistryStore,
FsPluginStorageStore, FsProfileStore, FsProjectPluginSettingsStore, FsProjectSettingsStore,
FsProjectStore, FsProviderSessionStore, FsScheduledProjectTaskStore, FsSecretStore,
FsSkillStore, FsSprintStore, FsSystemPermissionStore, FsTemplateStore, FsUserSettingsStore,
FsWindowStateStore, FsWorkspaceToolProvider, Git2Repository, HeuristicHandoffSummarizer,
HfModelArtifactDownloader, HttpOpenAiCompatibleProbe, HttpProviderModelCatalogue,
IdeaiContextStore, InMemoryConversationRegistry, InMemoryMailbox, InMemoryPairAttemptLimiter,
LlamaCppRuntime, LocalEnvironmentReader, LocalFileSystem, LocalManagedProcess,
LocalProcessSpawner, LocalWorkspaceToolRunner, ManagedTempToolProvider, McpServer,
MediatedInbox, NaiveMemoryRecall, OrchestratorWatchHandle, PortablePtyAdapter,
FsChatAttachmentStore, FsChatDraftStore, FsConversationLog, FsCustomConversationIndex,
FsCustomProjectIconAssetStore, FsDeviceSessionStore, FsEmbedderProfileStore,
FsEmbedderPromptStore, FsHandoffStore, FsIssueNumberAllocator, FsIssueStore, FsLiveStateStore,
FsManagedTempStore, FsMcpToolPermissionStore, FsMemoryStore, FsModelServerRegistry,
FsOpenProjectRestoreStore, FsOrchestratorWatcher, FsPermissionStore, FsPluginPackageStore,
FsPluginRegistryStore, FsPluginStorageStore, FsProfileStore, FsProjectPluginSettingsStore,
FsProjectSettingsStore, FsProjectStore, FsProviderSessionStore, FsScheduledProjectTaskStore,
FsSecretStore, FsSkillStore, FsSprintStore, FsSystemPermissionStore, FsTemplateStore,
FsUserSettingsStore, FsWindowStateStore, FsWorkspaceToolProvider, Git2Repository,
HeuristicHandoffSummarizer, HfModelArtifactDownloader, HttpOpenAiCompatibleProbe,
HttpProviderModelCatalogue, IdeaiContextStore, InMemoryConversationRegistry, InMemoryMailbox,
InMemoryPairAttemptLimiter, LlamaCppRuntime, LocalEnvironmentReader, LocalFileSystem,
LocalManagedProcess, LocalProcessSpawner, LocalWorkspaceToolRunner, ManagedTempToolProvider,
McpServer, MediatedInbox, NaiveMemoryRecall, OrchestratorWatchHandle, PortablePtyAdapter,
ProcessCliVersionReader, ReadOnlyRuntimePermissionProbe, RwFileGuard, StructuredSessionFactory,
SystemClock, SystemMillisClock, TemplateToolProvider, TicketAssistantEnvironmentPreparer,
TicketToolProvider, TokioBroadcastEventBus, TokioScheduler, ToolPolicyRegistry, UuidGenerator,
@ -496,6 +496,26 @@ impl RecordTurnProvider for AppRecordTurnProvider {
}
}
/// Resolves the durable custom CLI conversation catalogue for a project root.
#[derive(Default)]
struct AppCustomConversationIndexProvider {
indexes: Mutex<HashMap<String, Arc<FsCustomConversationIndex>>>,
}
impl CustomConversationIndexProvider for AppCustomConversationIndexProvider {
fn custom_conversation_index_for(
&self,
root: &domain::project::ProjectPath,
) -> Option<Arc<dyn domain::CustomConversationIndex>> {
let mut indexes = self.indexes.lock().ok()?;
let index = indexes
.entry(root.as_str().to_owned())
.or_insert_with(|| Arc::new(FsCustomConversationIndex::new(root)))
.clone();
Some(index as Arc<dyn domain::CustomConversationIndex>)
}
}
/// Implémente [`ConversationArchiveProvider`](application::ConversationArchiveProvider)
/// (lot LS6) en matérialisant un [`FsConversationLog`] (qui implémente aussi
/// [`domain::ConversationArchive`]) ciblant le **project root** courant.
@ -2317,6 +2337,8 @@ pub struct BackendCore {
pub get_app_exit_work_guard_state: Arc<GetAppExitWorkGuardState>,
/// Human paginated read of a conversation's full transcript (lot LS6).
pub read_conversation_page: Arc<ReadConversationPage>,
/// Durable listing, ownership and Prompt/Final checkpoints for custom CLI chats.
pub custom_conversations: Arc<CustomConversationService>,
/// Best-effort log rotation, triggered off the hot path at thread resume/open (lot LS6).
pub rotate_conversation_log: Arc<RotateConversationLog>,
/// Rebinds an already-running agent's live session to a visible cell (Lot D).
@ -4202,6 +4224,11 @@ impl BackendCore {
as Arc<dyn application::ConversationArchiveProvider>;
let read_conversation_page =
Arc::new(ReadConversationPage::new(Arc::clone(&archive_provider)));
let custom_conversations = Arc::new(CustomConversationService::new(
Arc::new(AppCustomConversationIndexProvider::default())
as Arc<dyn CustomConversationIndexProvider>,
Arc::new(AppRecordTurnProvider) as Arc<dyn RecordTurnProvider>,
));
let rotate_conversation_log = Arc::new(RotateConversationLog::new(
Arc::clone(&archive_provider),
Arc::new(AppHandoffProvider) as Arc<dyn application::HandoffProvider>,
@ -4617,6 +4644,7 @@ impl BackendCore {
get_project_work_state,
get_app_exit_work_guard_state,
read_conversation_page,
custom_conversations,
rotate_conversation_log,
attach_live_agent,
stop_live_agent,