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:
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
Reference in New Issue
Block a user