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

@ -50,6 +50,20 @@ pub enum DomainEventDto {
/// Exit code.
code: i32,
},
/// A ticket assistant chat was opened.
#[serde(rename_all = "camelCase")]
TicketAssistantOpened {
/// Bound ticket reference.
issue_ref: String,
/// Runtime profile id.
profile_id: String,
},
/// A ticket assistant chat was closed.
#[serde(rename_all = "camelCase")]
TicketAssistantClosed {
/// Bound ticket reference.
issue_ref: String,
},
/// An agent's busy/idle state changed (cadrage C4 §4.2). The frontend dims
/// "Envoyer" while `busy` is `true`.
#[serde(rename_all = "camelCase")]
@ -551,6 +565,16 @@ impl From<&DomainEvent> for DomainEventDto {
agent_id: agent_id.to_string(),
code: *code,
},
DomainEvent::TicketAssistantOpened {
issue_ref,
profile_id,
} => Self::TicketAssistantOpened {
issue_ref: issue_ref.to_string(),
profile_id: profile_id.to_string(),
},
DomainEvent::TicketAssistantClosed { issue_ref } => Self::TicketAssistantClosed {
issue_ref: issue_ref.to_string(),
},
DomainEvent::AgentBusyChanged { agent_id, busy } => Self::AgentBusyChanged {
agent_id: agent_id.to_string(),
busy: *busy,
@ -1081,4 +1105,24 @@ mod tests {
assert_eq!(json["to"], to.to_string());
assert_eq!(json["version"], 3);
}
#[test]
fn ticket_assistant_events_relay_to_dto_and_wire() {
let profile_id = domain::ProfileId::from_uuid(uuid::Uuid::from_u128(9));
let opened = DomainEventDto::from(&DomainEvent::TicketAssistantOpened {
issue_ref: domain::IssueRef::from(domain::IssueNumber::new(7).unwrap()),
profile_id,
});
let opened = serde_json::to_value(&opened).expect("serialisable");
assert_eq!(opened["type"], "ticketAssistantOpened");
assert_eq!(opened["issueRef"], "#7");
assert_eq!(opened["profileId"], profile_id.to_string());
let closed = DomainEventDto::from(&DomainEvent::TicketAssistantClosed {
issue_ref: domain::IssueRef::from(domain::IssueNumber::new(7).unwrap()),
});
let closed = serde_json::to_value(&closed).expect("serialisable");
assert_eq!(closed["type"], "ticketAssistantClosed");
assert_eq!(closed["issueRef"], "#7");
}
}