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

@ -9,7 +9,12 @@
import { useCallback, useEffect, useState } from "react";
import type { Agent, AgentProfile, GatewayError } from "@/domain";
import type {
Agent,
AgentProfile,
GatewayError,
TerminalSession,
} from "@/domain";
import type { LiveAgent, OpenTerminalOptions, TerminalHandle } from "@/ports";
import { useGateways } from "@/app/di";
@ -44,6 +49,19 @@ export interface AgentsViewModel {
selectAgent: (agentId: string) => Promise<void>;
/** Saves the context for the currently selected agent. */
saveContext: (content: string) => Promise<void>;
/**
* Hot-swaps an agent's runtime AI profile. The caller is expected to have
* confirmed with the user first (the conversation history is abandoned). On a
* live agent the backend relaunches the session and returns it so the cell can
* rebind; the agent list is refreshed locally. Returns the relaunched session,
* if any, so the panel can rebind the hosting cell.
*/
changeAgentProfile: (
agentId: string,
profileId: string,
rows: number,
cols: number,
) => Promise<TerminalSession | undefined>;
/** Deletes an agent; deselects if it was selected. */
deleteAgent: (agentId: string) => Promise<void>;
/**
@ -124,7 +142,11 @@ export function useAgents(projectId: string): AgentsViewModel {
let cancelled = false;
void system
.onDomainEvent((event) => {
if (event.type === "agentLaunched" || event.type === "agentExited") {
if (
event.type === "agentLaunched" ||
event.type === "agentExited" ||
event.type === "agentProfileChanged"
) {
void refresh();
void refreshLiveAgents();
}
@ -193,6 +215,38 @@ export function useAgents(projectId: string): AgentsViewModel {
[agent, projectId, selectedAgentId],
);
const changeAgentProfile = useCallback(
async (
agentId: string,
profileId: string,
rows: number,
cols: number,
): Promise<TerminalSession | undefined> => {
setBusy(true);
setError(null);
try {
const { agent: updated, relaunchedSession } =
await agent.changeAgentProfile(projectId, agentId, profileId, rows, cols);
// Reflect the new profile locally; the `agentProfileChanged` event (when
// wired) also triggers a full refresh, but updating here keeps the UI
// responsive offline / in tests.
setAgents((prev) =>
prev.map((a) => (a.id === updated.id ? updated : a)),
);
// A hot relaunch produces a fresh live session id — keep the live list
// in sync so the cell can rebind.
void refreshLiveAgents();
return relaunchedSession;
} catch (e) {
setError(describe(e));
return undefined;
} finally {
setBusy(false);
}
},
[agent, projectId, refreshLiveAgents],
);
const deleteAgent = useCallback(
async (agentId: string) => {
setBusy(true);
@ -271,6 +325,7 @@ export function useAgents(projectId: string): AgentsViewModel {
createAgent,
selectAgent,
saveContext,
changeAgentProfile,
deleteAgent,
launchAgent,
stopAgent,