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:
@ -35,12 +35,14 @@ import type {
|
||||
ProfileAvailability,
|
||||
ResumableAgent,
|
||||
Skill,
|
||||
ReplyChunk,
|
||||
SkillScope,
|
||||
Sprint,
|
||||
Template,
|
||||
TerminalSession,
|
||||
Ticket,
|
||||
TicketCarnet,
|
||||
TicketChat,
|
||||
TicketLink,
|
||||
TicketLinkKind,
|
||||
TicketList,
|
||||
@ -1816,6 +1818,9 @@ export class MockTicketGateway implements TicketGateway {
|
||||
private counters = new Map<string, number>();
|
||||
private sprints = new Map<string, Map<string, Sprint>>();
|
||||
private sprintCounters = new Map<string, number>();
|
||||
/** Open assistant chats, keyed by `issueRef` → sessionId (ticket #8). */
|
||||
private chatSessions = new Map<string, string>();
|
||||
private chatCounter = 0;
|
||||
|
||||
constructor(private readonly system?: MockSystemGateway) {}
|
||||
|
||||
@ -2235,6 +2240,51 @@ export class MockTicketGateway implements TicketGateway {
|
||||
this.system?.emit({ type: "sprintDeleted", sprintId });
|
||||
}
|
||||
|
||||
async openTicketChat(
|
||||
projectId: string,
|
||||
issueRef: string,
|
||||
profileId: string,
|
||||
): Promise<TicketChat> {
|
||||
// Ensure the ticket exists (mirrors the backend resolving the ref).
|
||||
this.require(projectId, issueRef);
|
||||
const sessionId = `mock-ticket-chat-${++this.chatCounter}`;
|
||||
this.chatSessions.set(issueRef, sessionId);
|
||||
this.system?.emit({ type: "ticketAssistantOpened", issueRef, profileId });
|
||||
return { sessionId, requester: "ticket-assistant", issueRef };
|
||||
}
|
||||
|
||||
async closeTicketChat(_projectId: string, issueRef: string): Promise<void> {
|
||||
if (this.chatSessions.delete(issueRef)) {
|
||||
this.system?.emit({ type: "ticketAssistantClosed", issueRef });
|
||||
}
|
||||
}
|
||||
|
||||
async sendTicketChat(
|
||||
sessionId: string,
|
||||
message: string,
|
||||
onChunk: (chunk: ReplyChunk) => void,
|
||||
): Promise<void> {
|
||||
if (![...this.chatSessions.values()].includes(sessionId)) {
|
||||
throw {
|
||||
code: "NOT_FOUND",
|
||||
message: `chat session ${sessionId} not found`,
|
||||
} as GatewayError;
|
||||
}
|
||||
// Simulate a streamed assistant turn: a couple of text deltas, then the
|
||||
// deterministic `final` chunk. Delivered across microtasks (like a real
|
||||
// Channel), so a consumer that awaits sees the whole turn.
|
||||
const content = `Assistant: reçu « ${message} ».`;
|
||||
const chunks: ReplyChunk[] = [
|
||||
{ kind: "textDelta", text: "Assistant: reçu " },
|
||||
{ kind: "textDelta", text: `« ${message} ».` },
|
||||
{ kind: "final", content },
|
||||
];
|
||||
for (const chunk of chunks) {
|
||||
await Promise.resolve();
|
||||
onChunk(chunk);
|
||||
}
|
||||
}
|
||||
|
||||
private requireSprint(projectId: string, sprintId: string): Sprint {
|
||||
const sprint = this.projectSprints(projectId).get(sprintId);
|
||||
if (!sprint) {
|
||||
|
||||
Reference in New Issue
Block a user