merge(cli): intègre feature/164-profile-command — /profile + reset confirmé #164 (QA verte)
Commande /profile first-class avec reset de session explicite et confirmé, séparée de la palette slash générique. Dernier sous-ticket de l'épic #161 avant #165 (seam plugins). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -244,13 +244,17 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn profile_is_known_but_not_executable_yet() {
|
fn profile_returns_profile_switch_effect() {
|
||||||
let err = ExecuteSlashCommand::new(SlashCommandRegistry::new())
|
let sid = session();
|
||||||
|
let out = ExecuteSlashCommand::new(SlashCommandRegistry::new())
|
||||||
.execute(ExecuteSlashCommandInput {
|
.execute(ExecuteSlashCommandInput {
|
||||||
name: "/profile".to_owned(),
|
name: "/profile".to_owned(),
|
||||||
session_id: Some(session()),
|
session_id: Some(sid),
|
||||||
})
|
})
|
||||||
.unwrap_err();
|
.unwrap();
|
||||||
assert!(matches!(err, AppError::Invalid(message) if message.contains("unavailable")));
|
assert_eq!(
|
||||||
|
out.effect,
|
||||||
|
SlashCommandEffect::ProfileSwitch { session_id: sid }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -133,9 +133,7 @@ pub fn native_slash_commands() -> Vec<SlashCommand> {
|
|||||||
"/profile",
|
"/profile",
|
||||||
"Changer le profil de l'agent",
|
"Changer le profil de l'agent",
|
||||||
true,
|
true,
|
||||||
SlashCommandAvailability::Unavailable {
|
SlashCommandAvailability::Available,
|
||||||
reason: "La selection de profil est livree par le ticket #164".to_owned(),
|
|
||||||
},
|
|
||||||
SlashCommandSource::Native,
|
SlashCommandSource::Native,
|
||||||
Some(NativeSlashCommand::Profile),
|
Some(NativeSlashCommand::Profile),
|
||||||
)
|
)
|
||||||
@ -180,7 +178,7 @@ mod tests {
|
|||||||
assert_eq!(names, vec!["/help", "/clean", "/profile"]);
|
assert_eq!(names, vec!["/help", "/clean", "/profile"]);
|
||||||
assert!(commands[0].availability.is_available());
|
assert!(commands[0].availability.is_available());
|
||||||
assert!(commands[1].availability.is_available());
|
assert!(commands[1].availability.is_available());
|
||||||
assert!(!commands[2].availability.is_available());
|
assert!(commands[2].availability.is_available());
|
||||||
assert!(commands[2].requires_confirmation);
|
assert!(commands[2].requires_confirmation);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -422,10 +422,7 @@ export class MockAgentGateway implements AgentGateway {
|
|||||||
name: "/profile",
|
name: "/profile",
|
||||||
shortDescription: "Changer le profil de l'agent",
|
shortDescription: "Changer le profil de l'agent",
|
||||||
requiresConfirmation: true,
|
requiresConfirmation: true,
|
||||||
availability: {
|
availability: { status: "available" },
|
||||||
status: "unavailable",
|
|
||||||
reason: "La selection de profil est livree par le ticket #164",
|
|
||||||
},
|
|
||||||
source: "native",
|
source: "native",
|
||||||
native: "profile",
|
native: "profile",
|
||||||
},
|
},
|
||||||
@ -939,6 +936,21 @@ export class MockAgentGateway implements AgentGateway {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (command.native === "profile") {
|
||||||
|
if (!options.sessionId) {
|
||||||
|
throw {
|
||||||
|
code: "INVALID",
|
||||||
|
message: "/profile requires a current session id",
|
||||||
|
} as GatewayError;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
command: structuredClone(command),
|
||||||
|
effect: {
|
||||||
|
kind: "profileSwitch",
|
||||||
|
sessionId: options.sessionId,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
command: structuredClone(command),
|
command: structuredClone(command),
|
||||||
effect: {
|
effect: {
|
||||||
|
|||||||
@ -522,6 +522,129 @@ describe("CustomAgentChatView", () => {
|
|||||||
expect(screen.queryByRole("listbox", { name: "suggestions commandes slash" })).toBeNull();
|
expect(screen.queryByRole("listbox", { name: "suggestions commandes slash" })).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("executes /profile as a confirmed profile-switch flow that resets the current session", async () => {
|
||||||
|
const agent = {
|
||||||
|
launchAgentChat: vi.fn(async () => ({
|
||||||
|
sessionId: "chat-session-2",
|
||||||
|
cellKind: "chat" as const,
|
||||||
|
assignedConversationId: "conversation-2",
|
||||||
|
})),
|
||||||
|
reattachAgentChat: vi.fn(async (sessionId: string) => ({
|
||||||
|
sessionId,
|
||||||
|
scrollback: [],
|
||||||
|
})),
|
||||||
|
sendAgentChat: vi.fn(async () => {}),
|
||||||
|
executeSlashCommand: vi.fn(async () => ({
|
||||||
|
command: {
|
||||||
|
name: "/profile",
|
||||||
|
shortDescription: "Changer le profil de l'agent",
|
||||||
|
requiresConfirmation: true,
|
||||||
|
availability: { status: "available" as const },
|
||||||
|
source: "native" as const,
|
||||||
|
native: "profile" as const,
|
||||||
|
},
|
||||||
|
effect: {
|
||||||
|
kind: "profileSwitch" as const,
|
||||||
|
sessionId: "chat-session-1",
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
changeAgentProfile: vi.fn(async () => ({ agent: { id: "agent-1" } })),
|
||||||
|
cancelAgentChat: vi.fn(async () => {}),
|
||||||
|
closeAgentChat: vi.fn(async () => {}),
|
||||||
|
};
|
||||||
|
const profileGateway = {
|
||||||
|
listProfiles: vi.fn(async () => [
|
||||||
|
profile,
|
||||||
|
{
|
||||||
|
...profile,
|
||||||
|
id: "codex-high",
|
||||||
|
name: "Codex High",
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
};
|
||||||
|
const onSessionId = vi.fn();
|
||||||
|
const onConversationId = vi.fn();
|
||||||
|
|
||||||
|
render(
|
||||||
|
<DIProvider
|
||||||
|
gateways={{
|
||||||
|
agent,
|
||||||
|
profile: profileGateway,
|
||||||
|
system: { pickFile: vi.fn(async () => null) },
|
||||||
|
} as unknown as Gateways}
|
||||||
|
>
|
||||||
|
<CustomAgentChatView
|
||||||
|
projectId="project-1"
|
||||||
|
agentId="agent-1"
|
||||||
|
agentName="Worker"
|
||||||
|
profile={profile}
|
||||||
|
cwd="/repo"
|
||||||
|
nodeId="node-1"
|
||||||
|
sessionId="chat-session-1"
|
||||||
|
conversationId="conversation-1"
|
||||||
|
onSessionId={onSessionId}
|
||||||
|
onConversationId={onConversationId}
|
||||||
|
/>
|
||||||
|
</DIProvider>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(agent.reattachAgentChat).toHaveBeenCalledWith(
|
||||||
|
"chat-session-1",
|
||||||
|
expect.any(Function),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const composer = screen.getByLabelText(/message CLI custom/) as HTMLTextAreaElement;
|
||||||
|
fireEvent.change(composer, { target: { value: "/profile" } });
|
||||||
|
fireEvent.keyDown(composer, { key: "Enter" });
|
||||||
|
|
||||||
|
const dialog = await screen.findByRole("dialog", { name: "Changer de profil" });
|
||||||
|
expect(dialog.textContent).toContain("réinitialiser la session courante");
|
||||||
|
expect(dialog.textContent).toContain("L'historique de cette CLI custom sera perdu");
|
||||||
|
expect(agent.executeSlashCommand).toHaveBeenCalledWith("/profile", {
|
||||||
|
sessionId: "chat-session-1",
|
||||||
|
});
|
||||||
|
expect(agent.sendAgentChat).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
const confirmButton = screen.getByRole("button", {
|
||||||
|
name: "Reset et changer le profil",
|
||||||
|
}) as HTMLButtonElement;
|
||||||
|
expect(confirmButton.disabled).toBe(true);
|
||||||
|
|
||||||
|
fireEvent.change(screen.getByLabelText("profil cible"), {
|
||||||
|
target: { value: "codex-high" },
|
||||||
|
});
|
||||||
|
fireEvent.click(screen.getByLabelText("Je confirme que la session courante sera reset."));
|
||||||
|
expect(confirmButton.disabled).toBe(false);
|
||||||
|
fireEvent.click(confirmButton);
|
||||||
|
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(agent.changeAgentProfile).toHaveBeenCalledWith(
|
||||||
|
"project-1",
|
||||||
|
"agent-1",
|
||||||
|
"codex-high",
|
||||||
|
24,
|
||||||
|
80,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(agent.launchAgentChat).toHaveBeenCalledWith("project-1", "agent-1", {
|
||||||
|
cwd: "/repo",
|
||||||
|
rows: 24,
|
||||||
|
cols: 80,
|
||||||
|
conversationId: undefined,
|
||||||
|
nodeId: "node-1",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(onSessionId).toHaveBeenCalledWith(null);
|
||||||
|
expect(onConversationId).toHaveBeenCalledWith(null);
|
||||||
|
expect(onSessionId).toHaveBeenCalledWith("chat-session-2");
|
||||||
|
expect(onConversationId).toHaveBeenCalledWith("conversation-2");
|
||||||
|
expect(screen.getByText("Profil changé vers Codex High.")).toBeTruthy();
|
||||||
|
expect(screen.queryByRole("dialog", { name: "Changer de profil" })).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
it("pastes a clipboard image as a removable preview chip", async () => {
|
it("pastes a clipboard image as a removable preview chip", async () => {
|
||||||
const agent = {
|
const agent = {
|
||||||
launchAgentChat: vi.fn(),
|
launchAgentChat: vi.fn(),
|
||||||
|
|||||||
@ -58,6 +58,14 @@ interface AttachmentDraft {
|
|||||||
previewUrl?: string;
|
previewUrl?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ProfileCommandDialogState {
|
||||||
|
profiles: AgentProfile[];
|
||||||
|
selectedProfileId: string;
|
||||||
|
confirmedReset: boolean;
|
||||||
|
busy: boolean;
|
||||||
|
error: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
function describe(e: unknown): string {
|
function describe(e: unknown): string {
|
||||||
if (e && typeof e === "object" && "message" in e) {
|
if (e && typeof e === "object" && "message" in e) {
|
||||||
return String((e as GatewayError).message);
|
return String((e as GatewayError).message);
|
||||||
@ -319,7 +327,7 @@ export function CustomAgentChatView({
|
|||||||
onSessionId,
|
onSessionId,
|
||||||
onConversationId,
|
onConversationId,
|
||||||
}: CustomAgentChatViewProps) {
|
}: CustomAgentChatViewProps) {
|
||||||
const { agent, system } = useGateways();
|
const { agent, profile: profileGateway, system } = useGateways();
|
||||||
const [turns, setTurns] = useState<ChatTurn[]>([]);
|
const [turns, setTurns] = useState<ChatTurn[]>([]);
|
||||||
const [currentSession, setCurrentSession] = useState(sessionId);
|
const [currentSession, setCurrentSession] = useState(sessionId);
|
||||||
const [externalSessionId, setExternalSessionId] = useState(sessionId);
|
const [externalSessionId, setExternalSessionId] = useState(sessionId);
|
||||||
@ -328,6 +336,8 @@ export function CustomAgentChatView({
|
|||||||
const [slashCommands, setSlashCommands] = useState<SlashCommand[]>([]);
|
const [slashCommands, setSlashCommands] = useState<SlashCommand[]>([]);
|
||||||
const [slashMenuOpen, setSlashMenuOpen] = useState(false);
|
const [slashMenuOpen, setSlashMenuOpen] = useState(false);
|
||||||
const [slashActiveIndex, setSlashActiveIndex] = useState(0);
|
const [slashActiveIndex, setSlashActiveIndex] = useState(0);
|
||||||
|
const [profileDialog, setProfileDialog] =
|
||||||
|
useState<ProfileCommandDialogState | null>(null);
|
||||||
const [opening, setOpening] = useState(false);
|
const [opening, setOpening] = useState(false);
|
||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
@ -448,7 +458,13 @@ export function CustomAgentChatView({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const recoverStructuredSession = useCallback(
|
const recoverStructuredSession = useCallback(
|
||||||
async (options: { applyScrollback?: boolean; retryAttachNotFound?: boolean } = {}) => {
|
async (
|
||||||
|
options: {
|
||||||
|
applyScrollback?: boolean;
|
||||||
|
retryAttachNotFound?: boolean;
|
||||||
|
conversationId?: string | null;
|
||||||
|
} = {},
|
||||||
|
) => {
|
||||||
if (!agent.launchAgentChat) throw new Error("Structured launch unavailable");
|
if (!agent.launchAgentChat) throw new Error("Structured launch unavailable");
|
||||||
console.debug("[ticket149] recoverStructuredSession:start", {
|
console.debug("[ticket149] recoverStructuredSession:start", {
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
@ -462,7 +478,10 @@ export function CustomAgentChatView({
|
|||||||
cwd,
|
cwd,
|
||||||
rows: 24,
|
rows: 24,
|
||||||
cols: 80,
|
cols: 80,
|
||||||
conversationId: conversationId ?? undefined,
|
conversationId:
|
||||||
|
options.conversationId === undefined
|
||||||
|
? conversationId ?? undefined
|
||||||
|
: options.conversationId ?? undefined,
|
||||||
nodeId,
|
nodeId,
|
||||||
};
|
};
|
||||||
console.debug("[ticket149] recoverStructuredSession:launchAgentChat:start", {
|
console.debug("[ticket149] recoverStructuredSession:launchAgentChat:start", {
|
||||||
@ -641,6 +660,102 @@ export function CustomAgentChatView({
|
|||||||
[supported, draft, attachments.length, busy, opening],
|
[supported, draft, attachments.length, busy, opening],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
async function openProfileCommandFlow() {
|
||||||
|
if (!agent.executeSlashCommand) {
|
||||||
|
setError("Commande /profile indisponible dans ce runtime.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const sid =
|
||||||
|
currentSession ??
|
||||||
|
(await recoverStructuredSession({
|
||||||
|
applyScrollback: false,
|
||||||
|
retryAttachNotFound: true,
|
||||||
|
}));
|
||||||
|
const result = await agent.executeSlashCommand("/profile", { sessionId: sid });
|
||||||
|
if (result.effect.kind !== "profileSwitch") {
|
||||||
|
throw new Error("La commande /profile n'a pas renvoyé le flow profil.");
|
||||||
|
}
|
||||||
|
const availableProfiles = (await profileGateway.listProfiles()).filter(
|
||||||
|
(candidate) => Boolean(candidate.structuredAdapter),
|
||||||
|
);
|
||||||
|
if (availableProfiles.length === 0) {
|
||||||
|
throw new Error("Aucun profil compatible avec la CLI custom n'est configuré.");
|
||||||
|
}
|
||||||
|
setDraft("");
|
||||||
|
setProfileDialog({
|
||||||
|
profiles: availableProfiles,
|
||||||
|
selectedProfileId:
|
||||||
|
availableProfiles.find((candidate) => candidate.id !== profile.id)?.id ??
|
||||||
|
availableProfiles[0].id,
|
||||||
|
confirmedReset: false,
|
||||||
|
busy: false,
|
||||||
|
error: null,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
setError(describe(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirmProfileCommandChange() {
|
||||||
|
if (!profileDialog) return;
|
||||||
|
if (!agent.changeAgentProfile) {
|
||||||
|
setProfileDialog((prev) =>
|
||||||
|
prev ? { ...prev, error: "Changement de profil indisponible." } : prev,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const selected = profileDialog.profiles.find(
|
||||||
|
(candidate) => candidate.id === profileDialog.selectedProfileId,
|
||||||
|
);
|
||||||
|
if (!selected) return;
|
||||||
|
|
||||||
|
setProfileDialog((prev) => (prev ? { ...prev, busy: true, error: null } : prev));
|
||||||
|
try {
|
||||||
|
const { relaunchedSession } = await agent.changeAgentProfile(
|
||||||
|
projectId,
|
||||||
|
agentId,
|
||||||
|
selected.id,
|
||||||
|
24,
|
||||||
|
80,
|
||||||
|
);
|
||||||
|
setTurns([]);
|
||||||
|
setCurrentSession(null);
|
||||||
|
publishSessionId(null);
|
||||||
|
onConversationIdRef.current(null);
|
||||||
|
|
||||||
|
if (relaunchedSession?.sessionId) {
|
||||||
|
try {
|
||||||
|
setCurrentSession(relaunchedSession.sessionId);
|
||||||
|
publishSessionId(relaunchedSession.sessionId);
|
||||||
|
await reattachStructuredSession(relaunchedSession.sessionId, {
|
||||||
|
applyScrollback: false,
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
await recoverStructuredSession({
|
||||||
|
applyScrollback: false,
|
||||||
|
retryAttachNotFound: true,
|
||||||
|
conversationId: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await recoverStructuredSession({
|
||||||
|
applyScrollback: false,
|
||||||
|
retryAttachNotFound: true,
|
||||||
|
conversationId: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setTurns([{ role: "tool", label: `Profil changé vers ${selected.name}.` }]);
|
||||||
|
setProfileDialog(null);
|
||||||
|
} catch (e) {
|
||||||
|
setProfileDialog((prev) =>
|
||||||
|
prev ? { ...prev, busy: false, error: describe(e) } : prev,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function pickAttachment() {
|
async function pickAttachment() {
|
||||||
const path = await system.pickFile();
|
const path = await system.pickFile();
|
||||||
if (path) {
|
if (path) {
|
||||||
@ -685,6 +800,10 @@ export function CustomAgentChatView({
|
|||||||
async function send() {
|
async function send() {
|
||||||
const text = draft.trim();
|
const text = draft.trim();
|
||||||
if (!canSend || !agent.sendAgentChat) return;
|
if (!canSend || !agent.sendAgentChat) return;
|
||||||
|
if (text === "/profile") {
|
||||||
|
await openProfileCommandFlow();
|
||||||
|
return;
|
||||||
|
}
|
||||||
const outgoingAttachments = attachments;
|
const outgoingAttachments = attachments;
|
||||||
const attachmentInputs = outgoingAttachments.map((item) => item.input);
|
const attachmentInputs = outgoingAttachments.map((item) => item.input);
|
||||||
const attachmentLabels = outgoingAttachments.map((item) => item.label);
|
const attachmentLabels = outgoingAttachments.map((item) => item.label);
|
||||||
@ -797,6 +916,101 @@ export function CustomAgentChatView({
|
|||||||
{error}
|
{error}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
{profileDialog && (
|
||||||
|
<div
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-labelledby={`profile-command-title-${nodeId}`}
|
||||||
|
className="m-3 rounded-md border border-border bg-surface p-3 shadow-lg"
|
||||||
|
>
|
||||||
|
<div className="flex min-w-0 items-start justify-between gap-3">
|
||||||
|
<div className="min-w-0">
|
||||||
|
<h2
|
||||||
|
id={`profile-command-title-${nodeId}`}
|
||||||
|
className="text-sm font-semibold text-content"
|
||||||
|
>
|
||||||
|
Changer de profil
|
||||||
|
</h2>
|
||||||
|
<p className="mt-1 text-xs text-muted">
|
||||||
|
Valider ce changement va réinitialiser la session courante. L'historique de cette CLI custom sera perdu.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
disabled={profileDialog.busy}
|
||||||
|
onClick={() => setProfileDialog(null)}
|
||||||
|
>
|
||||||
|
Fermer
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="mt-3 flex min-w-0 flex-col gap-2">
|
||||||
|
<label className="flex min-w-0 flex-col gap-1 text-xs text-muted">
|
||||||
|
Profil
|
||||||
|
<select
|
||||||
|
aria-label="profil cible"
|
||||||
|
value={profileDialog.selectedProfileId}
|
||||||
|
disabled={profileDialog.busy}
|
||||||
|
className="rounded-md border border-border bg-raised px-2 py-1 text-sm text-content"
|
||||||
|
onChange={(event) =>
|
||||||
|
setProfileDialog((prev) =>
|
||||||
|
prev
|
||||||
|
? {
|
||||||
|
...prev,
|
||||||
|
selectedProfileId: event.target.value,
|
||||||
|
confirmedReset: false,
|
||||||
|
}
|
||||||
|
: prev,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{profileDialog.profiles.map((candidate) => (
|
||||||
|
<option key={candidate.id} value={candidate.id}>
|
||||||
|
{candidate.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label className="flex items-start gap-2 text-xs text-content">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={profileDialog.confirmedReset}
|
||||||
|
disabled={profileDialog.busy}
|
||||||
|
onChange={(event) =>
|
||||||
|
setProfileDialog((prev) =>
|
||||||
|
prev ? { ...prev, confirmedReset: event.target.checked } : prev,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<span>Je confirme que la session courante sera reset.</span>
|
||||||
|
</label>
|
||||||
|
{profileDialog.error && (
|
||||||
|
<p role="alert" className="text-xs text-danger">
|
||||||
|
{profileDialog.error}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<div className="flex justify-end gap-2">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
disabled={profileDialog.busy}
|
||||||
|
onClick={() => setProfileDialog(null)}
|
||||||
|
>
|
||||||
|
Annuler
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="danger"
|
||||||
|
disabled={!profileDialog.confirmedReset || profileDialog.busy}
|
||||||
|
loading={profileDialog.busy}
|
||||||
|
onClick={() => void confirmProfileCommandChange()}
|
||||||
|
>
|
||||||
|
Reset et changer le profil
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
ref={scrollRef}
|
ref={scrollRef}
|
||||||
|
|||||||
Reference in New Issue
Block a user