feat(backend,application,frontend,domain,web-server,app-tauri): bouton Redémarrer l'agent dans la barre supérieure des cellules — #317 (QA verte)
La cellule expose un bouton Redémarrer l'agent valable pour les trois modes de présentation (headless, TUI native, CLI custom), sans jamais prendre la présentation pour vérité runtime. - Application: use case RestartCellAgentRuntime validant le binding exact cellule/runtime via les snapshots live (RestartRuntimeNotFound, ConflictAgentVisibleElsewhere, AgentNotBoundToCell) puis déléguant au scheduler self-restart existant (schedule_cell); CellAgentPresentation purement informationnelle. - Backend/app-tauri/web-server: commande restart_cell_agent sur contrat DTO identique Tauri/HTTP, stop borné par timeout typé (RestartStopTimeout), relance préservant routage TUI natif, géométrie de cellule (node/rows/cols) et conversation; évenements domaine enrichis. - Frontend: bouton dans la barre de cellule avec confirmation, overlay d'état Redémarrage…/échec (libellés par code d'erreur, Fermer/ Réessayer), gardes d'identité de cellule et de restarts externes, gateway/mock/http adapters et ports alignés. - Tests: dto_agents, workstate_actions, contrats web-server, LayoutGrid.cellControlsLayering, adapters agent/mock/http. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -194,6 +194,183 @@ function expectedOperation(action: CellAction, cellId: string) {
|
||||
}
|
||||
|
||||
describe("LayoutGrid — ticket #48 cell control layering", () => {
|
||||
it("shows restart only for an agent cell and disables its inputs while scheduled", async () => {
|
||||
const setup = await makeSplitAgentGrid();
|
||||
const restart = vi
|
||||
.spyOn(setup.gateways.agent, "restartCellAgentRuntime")
|
||||
.mockResolvedValue({
|
||||
restartId: "restart-cell-1",
|
||||
status: "scheduled",
|
||||
agentId: setup.agentId,
|
||||
nodeId: setup.bId,
|
||||
kind: "pty",
|
||||
presentation: "tui",
|
||||
layoutId: null,
|
||||
});
|
||||
renderGrid(setup.gateways);
|
||||
|
||||
const button = await screen.findByRole("button", { name: "Redémarrer l’agent" });
|
||||
expect(screen.getAllByRole("button", { name: "Redémarrer l’agent" })).toHaveLength(1);
|
||||
fireEvent.click(button);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(restart).toHaveBeenCalledWith({
|
||||
projectId: "p1",
|
||||
nodeId: setup.bId,
|
||||
agentId: setup.agentId,
|
||||
presentation: "tui",
|
||||
}),
|
||||
);
|
||||
expect(
|
||||
(screen.getByRole("button", {
|
||||
name: "Redémarrage de l’agent…",
|
||||
}) as HTMLButtonElement).disabled,
|
||||
).toBe(true);
|
||||
expect(
|
||||
(screen.getByLabelText(`agent selector ${setup.bId}`) as HTMLSelectElement)
|
||||
.disabled,
|
||||
).toBe(true);
|
||||
expect(screen.getByText("Redémarrage de l’agent…")).toBeTruthy();
|
||||
|
||||
await flush();
|
||||
act(() => {
|
||||
setup.system.emit({
|
||||
type: "agentRestartCompleted",
|
||||
agentId: setup.agentId,
|
||||
restartId: "restart-cell-1",
|
||||
});
|
||||
});
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
(screen.getByRole("button", {
|
||||
name: "Redémarrer l’agent",
|
||||
}) as HTMLButtonElement).disabled,
|
||||
).toBe(false),
|
||||
);
|
||||
});
|
||||
|
||||
it("confirms a busy restart and forwards the custom CLI presentation", async () => {
|
||||
const setup = await makeSplitAgentGrid();
|
||||
window.localStorage.setItem(`idea.agent-cell-mode.p1.${setup.bId}`, "custom");
|
||||
setup.workState._setProjectWorkState("p1", {
|
||||
agents: [
|
||||
{
|
||||
agentId: setup.agentId,
|
||||
name: "Worker",
|
||||
profileId: "mock-claude",
|
||||
busy: { state: "busy", sinceMs: 1 },
|
||||
},
|
||||
],
|
||||
});
|
||||
const restart = vi
|
||||
.spyOn(setup.gateways.agent, "restartCellAgentRuntime")
|
||||
.mockResolvedValue({
|
||||
restartId: "restart-cell-2",
|
||||
status: "scheduled",
|
||||
agentId: setup.agentId,
|
||||
nodeId: setup.bId,
|
||||
kind: "structured",
|
||||
presentation: "customCli",
|
||||
layoutId: null,
|
||||
});
|
||||
renderGrid(setup.gateways);
|
||||
|
||||
fireEvent.click(await screen.findByRole("button", { name: "Redémarrer l’agent" }));
|
||||
const dialog = await screen.findByRole("alertdialog", {
|
||||
name: "Redémarrer l’agent ?",
|
||||
});
|
||||
expect(restart).not.toHaveBeenCalled();
|
||||
fireEvent.click(screen.getByRole("button", { name: "Interrompre et redémarrer" }));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(restart).toHaveBeenCalledWith({
|
||||
projectId: "p1",
|
||||
nodeId: setup.bId,
|
||||
agentId: setup.agentId,
|
||||
presentation: "customCli",
|
||||
}),
|
||||
);
|
||||
expect(dialog.isConnected).toBe(false);
|
||||
window.localStorage.removeItem(`idea.agent-cell-mode.p1.${setup.bId}`);
|
||||
});
|
||||
|
||||
it("distinguishes an asynchronous relaunch failure and offers retry/close", async () => {
|
||||
const setup = await makeSplitAgentGrid();
|
||||
vi.spyOn(setup.gateways.agent, "restartCellAgentRuntime").mockResolvedValue({
|
||||
restartId: "restart-cell-3",
|
||||
status: "scheduled",
|
||||
agentId: setup.agentId,
|
||||
nodeId: setup.bId,
|
||||
kind: "pty",
|
||||
presentation: "tui",
|
||||
layoutId: null,
|
||||
});
|
||||
renderGrid(setup.gateways);
|
||||
fireEvent.click(await screen.findByRole("button", { name: "Redémarrer l’agent" }));
|
||||
await flush();
|
||||
|
||||
act(() => {
|
||||
setup.system.emit({
|
||||
type: "agentRestartFailed",
|
||||
agentId: setup.agentId,
|
||||
restartId: "restart-cell-3",
|
||||
phase: "launching",
|
||||
code: "LAUNCH_FAILED",
|
||||
message: "provider unavailable",
|
||||
});
|
||||
});
|
||||
|
||||
expect(await screen.findByText("Impossible de relancer l’agent.")).toBeTruthy();
|
||||
expect(screen.getByText("provider unavailable")).toBeTruthy();
|
||||
expect(screen.getByRole("button", { name: "Réessayer" })).toBeTruthy();
|
||||
fireEvent.click(screen.getByRole("button", { name: "Fermer" }));
|
||||
expect(screen.queryByTestId("cell-agent-restart-state")).toBeNull();
|
||||
});
|
||||
|
||||
it("distinguishes an asynchronous stop failure", async () => {
|
||||
const setup = await makeSplitAgentGrid();
|
||||
vi.spyOn(setup.gateways.agent, "restartCellAgentRuntime").mockResolvedValue({
|
||||
restartId: "restart-cell-stop",
|
||||
status: "scheduled",
|
||||
agentId: setup.agentId,
|
||||
nodeId: setup.bId,
|
||||
kind: "pty",
|
||||
presentation: "tui",
|
||||
layoutId: null,
|
||||
});
|
||||
renderGrid(setup.gateways);
|
||||
fireEvent.click(await screen.findByRole("button", { name: "Redémarrer l’agent" }));
|
||||
await flush();
|
||||
|
||||
act(() => {
|
||||
setup.system.emit({
|
||||
type: "agentRestartFailed",
|
||||
agentId: setup.agentId,
|
||||
restartId: "restart-cell-stop",
|
||||
phase: "stopping",
|
||||
code: "STOP_TIMEOUT",
|
||||
message: "stop timed out",
|
||||
});
|
||||
});
|
||||
|
||||
expect(await screen.findByText("Impossible d’arrêter l’agent.")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("surfaces a synchronous typed binding error", async () => {
|
||||
const setup = await makeSplitAgentGrid();
|
||||
vi.spyOn(setup.gateways.agent, "restartCellAgentRuntime").mockRejectedValue({
|
||||
code: "AGENT_NOT_BOUND_TO_CELL",
|
||||
message: "stale cell binding",
|
||||
});
|
||||
renderGrid(setup.gateways);
|
||||
fireEvent.click(await screen.findByRole("button", { name: "Redémarrer l’agent" }));
|
||||
|
||||
expect(
|
||||
await screen.findByText("L’agent n’est plus associé à cette cellule."),
|
||||
).toBeTruthy();
|
||||
expect(screen.getByText("stale cell binding")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("keeps custom CLI mode controls compact inside the cell toolbar (#151)", async () => {
|
||||
const setup = await makeSplitAgentGrid();
|
||||
|
||||
|
||||
@ -275,6 +275,13 @@ interface PendingModeSwitch {
|
||||
target: AgentCellMode;
|
||||
}
|
||||
|
||||
interface CellRestartState {
|
||||
status: "running" | "failed";
|
||||
restartId: string | null;
|
||||
code?: string;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
interface TrustedCustomCliBinding {
|
||||
agent: Agent;
|
||||
profile: AgentProfile;
|
||||
@ -382,6 +389,91 @@ function StoppedAgentCellPanel({
|
||||
);
|
||||
}
|
||||
|
||||
function restartFailureLabel(code?: string): string {
|
||||
switch (code) {
|
||||
case "STOP_TIMEOUT":
|
||||
case "STOP_FAILED":
|
||||
return "Impossible d’arrêter l’agent.";
|
||||
case "LAUNCH_FAILED":
|
||||
case "RESUME_FAILED":
|
||||
return "Impossible de relancer l’agent.";
|
||||
case "RUNTIME_NOT_FOUND":
|
||||
return "Runtime de l’agent introuvable.";
|
||||
case "AGENT_NOT_BOUND_TO_CELL":
|
||||
return "L’agent n’est plus associé à cette cellule.";
|
||||
case "CONFLICT_AGENT_VISIBLE_ELSEWHERE":
|
||||
return "Cet agent est visible dans une autre cellule.";
|
||||
case "CONFLICT":
|
||||
return "Un redémarrage est déjà en cours pour cet agent.";
|
||||
default:
|
||||
return "Impossible de redémarrer l’agent.";
|
||||
}
|
||||
}
|
||||
|
||||
function CellAgentRestartOverlay({
|
||||
state,
|
||||
onRetry,
|
||||
onClose,
|
||||
}: {
|
||||
state: CellRestartState;
|
||||
onRetry: () => void;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const failed = state.status === "failed";
|
||||
return (
|
||||
<div
|
||||
data-testid="cell-agent-restart-state"
|
||||
role={failed ? "alert" : "status"}
|
||||
aria-live="polite"
|
||||
style={{
|
||||
position: "absolute",
|
||||
inset: 0,
|
||||
zIndex: failed ? CELL_Z.controls + 1 : CELL_Z.veil,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
background: "rgba(0, 0, 0, 0.58)",
|
||||
color: "var(--color-content, #e0e0e0)",
|
||||
padding: 12,
|
||||
pointerEvents: failed ? "auto" : "none",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
maxWidth: 420,
|
||||
border: "1px solid var(--color-border, #3a3a3a)",
|
||||
borderRadius: 6,
|
||||
background: "var(--color-surface, #1e1e1e)",
|
||||
padding: "8px 12px",
|
||||
fontSize: 12,
|
||||
}}
|
||||
>
|
||||
<strong>
|
||||
{failed ? restartFailureLabel(state.code) : "Redémarrage de l’agent…"}
|
||||
</strong>
|
||||
{failed && state.message && (
|
||||
<p style={{ margin: "6px 0 0", color: "var(--color-muted, #9aa7b4)" }}>
|
||||
{state.message}
|
||||
</p>
|
||||
)}
|
||||
{failed && (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "flex-end",
|
||||
gap: 8,
|
||||
marginTop: 8,
|
||||
}}
|
||||
>
|
||||
<button type="button" onClick={onClose}>Fermer</button>
|
||||
<button type="button" onClick={onRetry}>Réessayer</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Local z-index scale for a single cell (ticket #48). Intra-cell layering only —
|
||||
* unrelated to the global {@link zIndex} scale used for app-level surfaces.
|
||||
@ -769,6 +861,76 @@ function LeafView({
|
||||
const busyByWorkState = agentWork?.busy.state === "busy" || Boolean(activeTicket);
|
||||
const delegatedTicket = activeTicket?.source.kind === "agent" ? activeTicket : undefined;
|
||||
const busyActive = announcementBusyActive || busyByWorkState;
|
||||
const [cellRestart, setCellRestart] = useState<CellRestartState | null>(null);
|
||||
const cellRestartRef = useRef<CellRestartState | null>(null);
|
||||
const cellRestartRequestRef = useRef(0);
|
||||
const cellRestartOwnedIdRef = useRef<string | null>(null);
|
||||
const [restartConfirmationOpen, setRestartConfirmationOpen] = useState(false);
|
||||
const cellRestartBusy = cellRestart?.status === "running";
|
||||
const otherRestartActive = Boolean(
|
||||
agentRestart && agentRestart.restartId !== cellRestartOwnedIdRef.current,
|
||||
);
|
||||
const cellIdentity = `${projectId}:${layoutId ?? ""}:${id}:${agentId ?? ""}`;
|
||||
const cellIdentityRef = useRef(cellIdentity);
|
||||
const previousCellIdentityRef = useRef(cellIdentity);
|
||||
cellIdentityRef.current = cellIdentity;
|
||||
|
||||
const commitCellRestart = (next: CellRestartState | null) => {
|
||||
cellRestartRef.current = next;
|
||||
setCellRestart(next);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (previousCellIdentityRef.current === cellIdentity) return;
|
||||
previousCellIdentityRef.current = cellIdentity;
|
||||
cellRestartRequestRef.current += 1;
|
||||
cellRestartOwnedIdRef.current = null;
|
||||
commitCellRestart(null);
|
||||
setRestartConfirmationOpen(false);
|
||||
}, [cellIdentity]);
|
||||
|
||||
async function restartCellAgent(): Promise<void> {
|
||||
if (!agentId || !agentGateway || cellRestartRef.current?.status === "running") return;
|
||||
const requestIdentity = cellIdentity;
|
||||
const sequence = ++cellRestartRequestRef.current;
|
||||
cellRestartOwnedIdRef.current = null;
|
||||
const running: CellRestartState = { status: "running", restartId: null };
|
||||
commitCellRestart(running);
|
||||
setRestartConfirmationOpen(false);
|
||||
try {
|
||||
const scheduled = await agentGateway.restartCellAgentRuntime({
|
||||
projectId,
|
||||
nodeId: id,
|
||||
agentId,
|
||||
presentation: cellMode === "custom" ? "customCli" : "tui",
|
||||
...(layoutId ? { layoutId } : {}),
|
||||
});
|
||||
if (
|
||||
cellRestartRequestRef.current === sequence &&
|
||||
cellIdentityRef.current === requestIdentity
|
||||
) {
|
||||
cellRestartOwnedIdRef.current = scheduled.restartId;
|
||||
commitCellRestart({ status: "running", restartId: scheduled.restartId });
|
||||
}
|
||||
} catch (error) {
|
||||
if (
|
||||
cellRestartRequestRef.current === sequence &&
|
||||
cellIdentityRef.current === requestIdentity
|
||||
) {
|
||||
commitCellRestart({
|
||||
status: "failed",
|
||||
restartId: null,
|
||||
code: gatewayErrorCode(error),
|
||||
message: describeNotice(error),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function requestCellAgentRestart(): void {
|
||||
if (busyActive) setRestartConfirmationOpen(true);
|
||||
else void restartCellAgent();
|
||||
}
|
||||
const targetAnnouncementsFallback = delegatedTicket?.requesterLabel
|
||||
? `Travaille sur une demande de ${delegatedTicket.requesterLabel}.`
|
||||
: "Travaille sur une demande en cours.";
|
||||
@ -1042,6 +1204,84 @@ function LeafView({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [liveAgents, agentId, session]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!system || !agentId) return;
|
||||
let cancelled = false;
|
||||
let unsubscribe: (() => void) | undefined;
|
||||
void system
|
||||
.onDomainEvent((event) => {
|
||||
if (
|
||||
event.type !== "agentRestartRequested" &&
|
||||
event.type !== "agentRestartPhaseChanged" &&
|
||||
event.type !== "agentRestartCompleted" &&
|
||||
event.type !== "agentRestartFailed"
|
||||
) return;
|
||||
if (event.agentId !== agentId) return;
|
||||
const current = cellRestartRef.current;
|
||||
if (!current || current.status !== "running") return;
|
||||
if (event.type === "agentRestartRequested") {
|
||||
if (!current.restartId) {
|
||||
cellRestartOwnedIdRef.current = event.restartId;
|
||||
commitCellRestart({ ...current, restartId: event.restartId });
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (
|
||||
event.type !== "agentRestartCompleted" &&
|
||||
event.type !== "agentRestartFailed"
|
||||
) return;
|
||||
if (current.restartId && current.restartId !== event.restartId) return;
|
||||
|
||||
if (event.type === "agentRestartFailed") {
|
||||
cellRestartOwnedIdRef.current = event.restartId;
|
||||
commitCellRestart({
|
||||
status: "failed",
|
||||
restartId: event.restartId,
|
||||
code: event.code,
|
||||
message: event.message,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
commitCellRestart(null);
|
||||
void (async () => {
|
||||
const live = await agentGateway?.listLiveAgents(projectId);
|
||||
if (cancelled || !live) return;
|
||||
setLiveAgents(live);
|
||||
const restarted = live.find((entry) => entry.agentId === agentId);
|
||||
if (
|
||||
restarted?.kind === "pty" &&
|
||||
restarted.sessionId &&
|
||||
restarted.sessionId !== session
|
||||
) {
|
||||
await vm.attachLiveAgentToCell(
|
||||
id,
|
||||
agentId,
|
||||
restarted.sessionId,
|
||||
);
|
||||
if (!cancelled) setAttachGen((generation) => generation + 1);
|
||||
}
|
||||
await refreshWorkState();
|
||||
})().catch(() => {
|
||||
// The restart itself completed; lifecycle refresh remains best-effort.
|
||||
});
|
||||
})
|
||||
.then((un) => {
|
||||
if (cancelled) un();
|
||||
else unsubscribe = un;
|
||||
})
|
||||
.catch(() => {
|
||||
// The synchronous command error remains visible if events are unavailable.
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
unsubscribe?.();
|
||||
};
|
||||
// The subscription follows the exact visible cell identity. Runtime refresh
|
||||
// functions are deliberately read from that render until the identity changes.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [system, agentGateway, projectId, layoutId, id, agentId]);
|
||||
|
||||
// ── Cell routing (Option 1 — Terminal + MCP) ──────────────────────────────
|
||||
// Every agent cell renders the raw {@link TerminalView}: the human view is the
|
||||
// native interactive PTY (live reasoning + Échap are native CLI behaviours,
|
||||
@ -1143,6 +1383,7 @@ function LeafView({
|
||||
<select
|
||||
aria-label={`agent selector ${id}`}
|
||||
value={agentId ?? ""}
|
||||
disabled={cellRestartBusy}
|
||||
onChange={(e) => {
|
||||
const val = e.target.value;
|
||||
if (val === "") {
|
||||
@ -1230,6 +1471,7 @@ function LeafView({
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
disabled={cellRestartBusy}
|
||||
aria-pressed={cellMode === "tui"}
|
||||
title="TUI native"
|
||||
onClick={() => requestMode("tui")}
|
||||
@ -1257,6 +1499,7 @@ function LeafView({
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={cellRestartBusy}
|
||||
aria-pressed={cellMode === "custom"}
|
||||
title="CLI custom"
|
||||
onClick={() => requestMode("custom")}
|
||||
@ -1284,6 +1527,18 @@ function LeafView({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{agentId && (
|
||||
<button
|
||||
type="button"
|
||||
title={cellRestartBusy ? "Redémarrage de l’agent…" : "Redémarrer l’agent"}
|
||||
aria-label={cellRestartBusy ? "Redémarrage de l’agent…" : "Redémarrer l’agent"}
|
||||
disabled={cellRestartBusy}
|
||||
onClick={requestCellAgentRestart}
|
||||
>
|
||||
↻
|
||||
</button>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
title="Split into columns"
|
||||
@ -1467,7 +1722,8 @@ function LeafView({
|
||||
is being injected into the agent's PTY, a grey veil with a centred
|
||||
message sits above the terminal. Only ever shown for an agent cell —
|
||||
and never together with the F3 overlay (exactly one veil, F3 first). */}
|
||||
{!agentRestart &&
|
||||
{!cellRestart &&
|
||||
!otherRestartActive &&
|
||||
!modelServerOverlay &&
|
||||
cellMode !== "custom" &&
|
||||
shouldShowWritePortalVeil(agentId != null, Boolean(overlay), busyActive) && (
|
||||
@ -1506,7 +1762,7 @@ function LeafView({
|
||||
an availability veil showing its live réflexions. Native TUI cells
|
||||
need the full-cell guard; custom CLI cells keep the canonical busy
|
||||
badge and composer lock only, matching their normal chat turn flow. */}
|
||||
{!agentRestart && !modelServerOverlay && cellMode !== "custom" && agentId != null && (
|
||||
{!cellRestart && !otherRestartActive && !modelServerOverlay && cellMode !== "custom" && agentId != null && (
|
||||
<TargetAnnouncementsOverlay
|
||||
projectId={projectId}
|
||||
agentId={agentId}
|
||||
@ -1519,7 +1775,7 @@ function LeafView({
|
||||
agent's bound local server is downloading its model / starting /
|
||||
probing, the empty terminal is covered rather than left to time out.
|
||||
Retracts at `ready`/`failed` (the predicate returns null then). */}
|
||||
{!agentRestart && modelServerOverlay && (
|
||||
{!cellRestart && !otherRestartActive && modelServerOverlay && (
|
||||
<div
|
||||
data-testid="model-server-overlay"
|
||||
role="status"
|
||||
@ -1636,9 +1892,19 @@ function LeafView({
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{cellMode !== "custom" && agentId != null && agentRestart && (
|
||||
{!cellRestart &&
|
||||
cellMode !== "custom" &&
|
||||
agentId != null &&
|
||||
otherRestartActive && (
|
||||
<AgentRestartOverlay agentId={agentId} />
|
||||
)}
|
||||
{cellRestart && (
|
||||
<CellAgentRestartOverlay
|
||||
state={cellRestart}
|
||||
onRetry={() => void restartCellAgent()}
|
||||
onClose={() => commitCellRestart(null)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{busyNotice && (
|
||||
<div
|
||||
@ -1769,6 +2035,58 @@ function LeafView({
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{restartConfirmationOpen && (
|
||||
<div
|
||||
role="alertdialog"
|
||||
aria-modal="true"
|
||||
aria-label="Redémarrer l’agent ?"
|
||||
style={{
|
||||
position: "absolute",
|
||||
inset: 0,
|
||||
zIndex: CELL_Z.controls + 1,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
background: "rgba(0, 0, 0, 0.62)",
|
||||
padding: 12,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: 360,
|
||||
maxWidth: "100%",
|
||||
border: "1px solid var(--color-border, #3a3a3a)",
|
||||
borderRadius: 6,
|
||||
background: "var(--color-surface, #1e1e1e)",
|
||||
color: "var(--color-content, #e0e0e0)",
|
||||
padding: 12,
|
||||
}}
|
||||
>
|
||||
<h2 style={{ margin: 0, fontSize: 14 }}>Redémarrer l’agent ?</h2>
|
||||
<p style={{ margin: "8px 0 0", fontSize: 12 }}>
|
||||
L’activité en cours sera interrompue avant la relance de l’agent.
|
||||
</p>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "flex-end",
|
||||
gap: 8,
|
||||
marginTop: 12,
|
||||
}}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setRestartConfirmationOpen(false)}
|
||||
>
|
||||
Annuler
|
||||
</button>
|
||||
<button type="button" onClick={() => void restartCellAgent()}>
|
||||
Interrompre et redémarrer
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -2022,6 +2340,13 @@ function describeNotice(e: unknown): string {
|
||||
return String(e);
|
||||
}
|
||||
|
||||
function gatewayErrorCode(e: unknown): string | undefined {
|
||||
if (e && typeof e === "object" && "code" in e) {
|
||||
return String((e as { code: unknown }).code);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** True when `e` is the backend singleton-invariant refusal (`GatewayError`). */
|
||||
function isAlreadyRunning(e: unknown): boolean {
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user