feat(agent): UI hot-swap de profil (A2) — commande Tauri + sélecteur + dialog

- Tauri : commande change_agent_profile + ChangeAgentProfileRequestDto/Dto
  (camelCase, relaunchedSession omis si None), câblage state.rs par composition.
- Front : gateway changeAgentProfile (adapters Tauri+mock), sélecteur de profil
  par agent, dialog de confirmation FR (« Changer le moteur abandonne l'historique
  de conversation… »), refresh sur event agentProfileChanged.

Tests : app-tauri dto 5/5 ; vitest 314/314 (mapping, payload, dialog gating,
libellé FR, refresh). 0 régression.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-09 10:07:35 +02:00
parent 2433e173a1
commit b82e3e1a40
13 changed files with 731 additions and 10 deletions

View File

@ -32,6 +32,7 @@ import type {
Skill,
SkillScope,
Template,
TerminalSession,
Unsubscribe,
} from "@/domain";
import type {
@ -305,6 +306,61 @@ export class MockAgentGateway implements AgentGateway {
this.contexts.delete(this.contextKey(projectId, agentId));
}
async changeAgentProfile(
projectId: string,
agentId: string,
profileId: string,
rows: number,
cols: number,
): Promise<{ agent: Agent; relaunchedSession?: TerminalSession }> {
const list = this.getAgents(projectId);
const idx = list.findIndex((a) => a.id === agentId);
if (idx === -1) {
const err: GatewayError = {
code: "NOT_FOUND",
message: `agent ${agentId} not found in project ${projectId}`,
};
throw err;
}
// Mutate the agent's runtime profile in place (the rest of the record —
// origin, synchronized, skills — is untouched).
list[idx] = { ...list[idx], profileId };
const agent = structuredClone(list[idx]);
// Hot-swap path: when the agent owns a live session, the engine restarts —
// the old PTY is killed (history abandoned, per the product decision) and a
// fresh session is minted in the same cell, surfaced for the caller to
// rebind. No live session ⇒ nothing to relaunch.
const nodeId = this.liveByAgent.get(agentId);
const oldSessionId = this.liveSessionByAgent.get(agentId);
if (!nodeId || !oldSessionId) {
return { agent };
}
// Kill the old session.
const old = this.sessions.get(oldSessionId);
if (old) old.closed = true;
this.sessions.delete(oldSessionId);
// Mint a fresh, detached session (no view sink yet; the cell reattaches).
this.sessionSeq += 1;
const sessionId = `mock-agent-session-${this.sessionSeq}`;
const session = new MockPtySession(sessionId, () => {});
session.detach();
this.sessions.set(sessionId, session);
this.liveSessionByAgent.set(agentId, sessionId);
this.liveByAgent.set(agentId, nodeId);
return {
agent,
relaunchedSession: {
sessionId,
cwd: `/home/user/mock-project`,
rows,
cols,
},
};
}
// ── Internal helpers for MockTemplateGateway (same-package use only) ──
/**