feat(tickets): création d'un assistant IA de ticket (backend + frontend)

Ajoute le chat assistant IA attaché à un ticket (#8).

Backend Rust :
- use cases OpenTicketAssistant/CloseTicketAssistant + tests
- politique d'outils par agent (domain/agent_tool_policy) et policy MCP
- store de contexte assistant + gabarit default_ticket_assistant.md + tests
- events TicketAssistantOpened/Closed
- commandes Tauri open_ticket_chat/close_ticket_chat et câblage state/lib/events

Frontend :
- gateway (ports, adapters ticket + mock, domain)
- hook useTicketAssistant + composant TicketAssistantPanel
- intégration dans TicketDetail

Tests verts : vitest tickets.test.tsx (23), cargo test application::ticket_assistant (1),
infrastructure::assistant_context_store (2).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-06 06:06:24 +02:00
parent f1803768fd
commit 1fdf62c089
29 changed files with 2173 additions and 49 deletions

View File

@ -0,0 +1,76 @@
//! Ticket assistant infrastructure adapters.
use std::sync::Arc;
use async_trait::async_trait;
use domain::{
AssistantContextError, AssistantContextProvider, FileSystem, FsError, Issue, MarkdownDoc,
PreparedContext, Project, RemotePath,
};
const ASSISTANT_DIR: &str = "assistant";
const TICKET_ASSISTANT_FILE: &str = "ticket-assistant.md";
const DEFAULT_TICKET_ASSISTANT_CONTEXT: &str = include_str!("default_ticket_assistant.md");
/// File-backed provider for the IdeA-owned ticket assistant context.
#[derive(Clone)]
pub struct FsAssistantContextStore {
fs: Arc<dyn FileSystem>,
app_data_dir: String,
}
impl FsAssistantContextStore {
/// Builds the store from an injected filesystem and app-data directory.
#[must_use]
pub fn new(fs: Arc<dyn FileSystem>, app_data_dir: impl Into<String>) -> Self {
Self {
fs,
app_data_dir: app_data_dir.into(),
}
}
fn join(&self, rel: &str) -> String {
let base = self.app_data_dir.trim_end_matches(['/', '\\']);
format!("{base}/{rel}")
}
fn context_path(&self) -> RemotePath {
RemotePath::new(self.join(&format!("{ASSISTANT_DIR}/{TICKET_ASSISTANT_FILE}")))
}
async fn read_base_context(&self) -> Result<String, AssistantContextError> {
let path = self.context_path();
match self.fs.read(&path).await {
Ok(bytes) => {
String::from_utf8(bytes).map_err(|e| AssistantContextError::Store(e.to_string()))
}
Err(FsError::NotFound(_)) => Ok(DEFAULT_TICKET_ASSISTANT_CONTEXT.to_owned()),
Err(e) => Err(AssistantContextError::Store(e.to_string())),
}
}
}
#[async_trait]
impl AssistantContextProvider for FsAssistantContextStore {
async fn prepare_ticket_assistant_context(
&self,
project: &Project,
issue: &Issue,
) -> Result<PreparedContext, AssistantContextError> {
let mut content = self.read_base_context().await?;
content.push_str("\n\n## Bound Ticket\n\n");
content.push_str(&format!("- Ref: {}\n", issue.reference()));
content.push_str(&format!("- Title: {}\n", issue.title));
content.push_str("\n### Description\n\n");
content.push_str(issue.description.as_str());
content.push_str("\n\n### Carnet\n\n");
content.push_str(issue.carnet.as_str());
content.push('\n');
Ok(PreparedContext {
content: MarkdownDoc::new(content),
relative_path: TICKET_ASSISTANT_FILE.to_owned(),
project_root: project.root.as_str().to_owned(),
})
}
}