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

@ -183,6 +183,18 @@ pub enum DomainEvent {
/// Exit code.
code: i32,
},
/// A ticket assistant session was opened for an issue.
TicketAssistantOpened {
/// Bound issue.
issue_ref: IssueRef,
/// Runtime profile used by the assistant.
profile_id: ProfileId,
},
/// A ticket assistant session was closed for an issue.
TicketAssistantClosed {
/// Bound issue.
issue_ref: IssueRef,
},
/// An agent's **busy/idle** state changed (cadrage C4 §4.2): it went `Busy` on
/// the enqueue that started a turn, or `Idle` on `mark_idle` (prompt-ready or an
/// explicit signal). Discrete, low-frequency beacon relayed to the front so the
@ -531,6 +543,14 @@ mod tests {
AgentId::from_uuid(uuid::Uuid::from_u128(n))
}
fn profile(n: u128) -> ProfileId {
ProfileId::from_uuid(uuid::Uuid::from_u128(n))
}
fn issue_ref(n: u64) -> IssueRef {
IssueRef::from(crate::IssueNumber::new(n).unwrap())
}
// -- §21 : constructibilité + égalité PartialEq des 5 variantes --------------
#[test]
@ -638,6 +658,47 @@ mod tests {
);
}
#[test]
fn ticket_assistant_opened_constructs_and_compares() {
let ev = DomainEvent::TicketAssistantOpened {
issue_ref: issue_ref(7),
profile_id: profile(9),
};
assert_eq!(
ev,
DomainEvent::TicketAssistantOpened {
issue_ref: issue_ref(7),
profile_id: profile(9),
}
);
assert_ne!(
ev,
DomainEvent::TicketAssistantOpened {
issue_ref: issue_ref(8),
profile_id: profile(9),
}
);
}
#[test]
fn ticket_assistant_closed_constructs_and_compares() {
let ev = DomainEvent::TicketAssistantClosed {
issue_ref: issue_ref(7),
};
assert_eq!(
ev,
DomainEvent::TicketAssistantClosed {
issue_ref: issue_ref(7),
}
);
assert_ne!(
ev,
DomainEvent::TicketAssistantClosed {
issue_ref: issue_ref(8),
}
);
}
#[test]
fn distinct_session_limit_variants_are_not_equal() {
// Les variantes ne se confondent pas entre elles malgré des champs proches.