feat(ui): cellule agent stoppé — état explicite + reopen from scratch (#197, QA verte)

Surface UI du LayoutGrid pour un agent de cellule stoppé:

- Affichage d'un état explicite quand runtimeState vaut « stopped » ou « failedStart », avec actions refresh / reopen from scratch (freshStart) et état restarting pendant la relance.

- theme.css: styles .stopped-agent-cell (boutons primary/secondary, états disabled).

Consomme le contrat runtime issu de #196 (AgentRuntimeState stopped/failedStart, freshStart) déjà présent dans develop; aucune modification runtime ici.

Tests QA verts: LayoutGrid.test.tsx, LayoutGrid.chat.test.tsx (rejoué aussi: agent_lifecycle launch_fresh_start_ignores_existing_conversation_id_and_mints_new_one).
This commit is contained in:
2026-08-12 15:34:53 +02:00
parent 7b7b2cec85
commit cdb395307b
4 changed files with 689 additions and 5 deletions

View File

@ -51,6 +51,7 @@ import {
MockProfileGateway,
MockSystemGateway,
MockTerminalGateway,
MockWorkStateGateway,
} from "@/adapters/mock";
import { DIProvider } from "@/app/di";
import { leaves } from "./layout";
@ -222,6 +223,75 @@ describe("LayoutGrid custom agent CLI (#147)", () => {
expect(screen.queryByText(/déjà actif dans une autre cellule/)).toBeNull();
});
it("relaunches a stopped custom CLI agent from scratch (#197)", async () => {
const layout = new MockLayoutGateway();
const agent = new MockAgentGateway();
const profileGateway = new MockProfileGateway();
const terminal = new MockTerminalGateway();
const system = new MockSystemGateway();
const workState = new MockWorkStateGateway();
await profileGateway.configureProfiles([structuredProfile]);
const created = await agent.createAgent("p1", {
name: "Worker",
profileId: structuredProfile.id,
});
const tree = await layout.loadLayout("p1");
const leafId = leaves(tree)[0].id;
await layout.mutateLayout("p1", {
type: "setCellAgent",
target: leafId,
agent: created.id,
});
await layout.mutateLayout("p1", {
type: "setSession",
target: leafId,
session: "dead-structured-session",
});
await layout.mutateLayout("p1", {
type: "setCellConversation",
target: leafId,
conversationId: "old-structured-conversation",
});
window.localStorage.setItem(`idea.agent-cell-mode.p1.${leafId}`, "custom");
workState._setProjectWorkState("p1", {
agents: [
{
agentId: created.id,
name: "Worker",
profileId: structuredProfile.id,
runtimeState: "stopped",
busy: { state: "idle" },
},
],
});
const launchChat = vi.spyOn(agent, "launchAgentChat");
renderGrid({
layout,
agent,
profile: profileGateway,
terminal,
system,
workState,
} as unknown as Gateways);
fireEvent.click(await screen.findByRole("button", { name: "Relancer" }));
await waitFor(() => expect(launchChat).toHaveBeenCalledTimes(1));
expect(launchChat.mock.calls[0]?.[2]).toMatchObject({
cwd: "/home/me/proj",
nodeId: leafId,
freshStart: true,
});
expect(launchChat.mock.calls[0]?.[2].conversationId).toBeUndefined();
await waitFor(async () => {
const updated = await layout.loadLayout("p1");
const leaf = leaves(updated)[0];
expect(leaf.session).toMatch(/^mock-agent-chat-/);
expect(leaf.conversationId).toMatch(/^mock-conversation-/);
});
});
it("closes the structured chat session by id before switching back to native TUI (#170)", async () => {
const layout = new MockLayoutGateway();
const agent = new MockAgentGateway();

View File

@ -12,7 +12,12 @@ import { describe, it, expect, vi } from "vitest";
import { render, screen, waitFor, fireEvent } from "@testing-library/react";
import type { Gateways } from "@/ports";
import { MockLayoutGateway, MockTerminalGateway } from "@/adapters/mock";
import {
MockAgentGateway,
MockLayoutGateway,
MockTerminalGateway,
MockWorkStateGateway,
} from "@/adapters/mock";
import { DIProvider } from "@/app/di";
import { leaves } from "./layout";
import { LayoutGrid } from "./LayoutGrid";
@ -181,4 +186,264 @@ describe("LayoutGrid (with MockLayoutGateway)", () => {
).not.toThrow();
expect(screen.getByTestId("layout-grid")).toBeTruthy();
});
it("renders an explicit stopped-agent cell without showing it as idle", async () => {
const layout = new MockLayoutGateway();
const agent = new MockAgentGateway();
const workState = new MockWorkStateGateway();
const created = await agent.createAgent("p1", {
name: "StoppedAgent",
profileId: "prof-1",
});
const tree = await layout.loadLayout("p1");
const leafId = leaves(tree)[0].id;
await layout.mutateLayout("p1", {
type: "setCellAgent",
target: leafId,
agent: created.id,
});
await layout.mutateLayout("p1", {
type: "setSession",
target: leafId,
session: "dead-session",
});
workState._setProjectWorkState("p1", {
agents: [
{
agentId: created.id,
name: "StoppedAgent",
profileId: "prof-1",
runtimeState: "stopped",
busy: { state: "idle" },
},
],
});
render(
<DIProvider gateways={{ layout, agent, workState } as unknown as Gateways}>
<LayoutGrid projectId="p1" cwd="/home/me/proj" />
</DIProvider>,
);
expect(await screen.findByLabelText("StoppedAgent arrêté")).toBeTruthy();
expect(screen.getByText("Arrêté")).toBeTruthy();
expect(screen.getByText("Cette session est terminée.")).toBeTruthy();
expect(screen.getByText("Ouvre une nouvelle session pour cet agent.")).toBeTruthy();
expect(screen.getByRole("button", { name: "Relancer" })).toBeTruthy();
expect(screen.getByRole("button", { name: "Actualiser" })).toBeTruthy();
expect(screen.queryByText("Idle")).toBeNull();
});
it("relaunches a stopped agent from scratch in the same cell", async () => {
const layout = new MockLayoutGateway();
const agent = new MockAgentGateway();
const workState = new MockWorkStateGateway();
const launchSpy = vi.spyOn(agent, "launchAgent");
const created = await agent.createAgent("p1", {
name: "StoppedAgent",
profileId: "prof-1",
});
const tree = await layout.loadLayout("p1");
const leafId = leaves(tree)[0].id;
await layout.mutateLayout("p1", {
type: "setCellAgent",
target: leafId,
agent: created.id,
});
await layout.mutateLayout("p1", {
type: "setSession",
target: leafId,
session: "dead-session",
});
await layout.mutateLayout("p1", {
type: "setCellConversation",
target: leafId,
conversationId: "old-conversation",
});
workState._setProjectWorkState("p1", {
agents: [
{
agentId: created.id,
name: "StoppedAgent",
profileId: "prof-1",
runtimeState: "stopped",
busy: { state: "idle" },
},
],
});
render(
<DIProvider gateways={{ layout, agent, workState } as unknown as Gateways}>
<LayoutGrid projectId="p1" cwd="/home/me/proj" />
</DIProvider>,
);
fireEvent.click(await screen.findByRole("button", { name: "Relancer" }));
await waitFor(() => expect(launchSpy).toHaveBeenCalledTimes(1));
expect(launchSpy.mock.calls[0]?.[0]).toBe("p1");
expect(launchSpy.mock.calls[0]?.[1]).toBe(created.id);
expect(launchSpy.mock.calls[0]?.[2]).toMatchObject({
cwd: "/home/me/proj",
nodeId: leafId,
freshStart: true,
});
expect(launchSpy.mock.calls[0]?.[2].conversationId).toBeUndefined();
await waitFor(async () => {
const fresh = await layout.loadLayout("p1");
const leaf = leaves(fresh)[0];
expect(leaf.agent).toBe(created.id);
expect(leaf.session).toMatch(/^mock-agent-session-/);
expect(leaf.conversationId).toMatch(/^mock-conversation-/);
});
});
it("refreshes a stopped agent state without relaunching", async () => {
const layout = new MockLayoutGateway();
const agent = new MockAgentGateway();
const workState = new MockWorkStateGateway();
const refreshSpy = vi.spyOn(workState, "getProjectWorkState");
const launchSpy = vi.spyOn(agent, "launchAgent");
const created = await agent.createAgent("p1", {
name: "StoppedAgent",
profileId: "prof-1",
});
const tree = await layout.loadLayout("p1");
const leafId = leaves(tree)[0].id;
await layout.mutateLayout("p1", {
type: "setCellAgent",
target: leafId,
agent: created.id,
});
await layout.mutateLayout("p1", {
type: "setSession",
target: leafId,
session: "dead-session",
});
workState._setProjectWorkState("p1", {
agents: [
{
agentId: created.id,
name: "StoppedAgent",
profileId: "prof-1",
runtimeState: "stopped",
busy: { state: "idle" },
},
],
});
render(
<DIProvider gateways={{ layout, agent, workState } as unknown as Gateways}>
<LayoutGrid projectId="p1" cwd="/home/me/proj" />
</DIProvider>,
);
fireEvent.click(await screen.findByRole("button", { name: "Actualiser" }));
await waitFor(() => expect(refreshSpy).toHaveBeenCalledTimes(2));
expect(launchSpy).not.toHaveBeenCalled();
});
it("shows the transient restart label while relaunching", async () => {
const layout = new MockLayoutGateway();
const agent = new MockAgentGateway();
const workState = new MockWorkStateGateway();
let resolveLaunch!: (value: Awaited<ReturnType<MockAgentGateway["launchAgent"]>>) => void;
const launchPromise = new Promise<Awaited<ReturnType<MockAgentGateway["launchAgent"]>>>(
(resolve) => {
resolveLaunch = resolve;
},
);
vi.spyOn(agent, "launchAgent").mockReturnValue(launchPromise);
const created = await agent.createAgent("p1", {
name: "StoppedAgent",
profileId: "prof-1",
});
const tree = await layout.loadLayout("p1");
const leafId = leaves(tree)[0].id;
await layout.mutateLayout("p1", {
type: "setCellAgent",
target: leafId,
agent: created.id,
});
workState._setProjectWorkState("p1", {
agents: [
{
agentId: created.id,
name: "StoppedAgent",
profileId: "prof-1",
runtimeState: "stopped",
busy: { state: "idle" },
},
],
});
render(
<DIProvider gateways={{ layout, agent, workState } as unknown as Gateways}>
<LayoutGrid projectId="p1" cwd="/home/me/proj" />
</DIProvider>,
);
fireEvent.click(await screen.findByRole("button", { name: "Relancer" }));
await waitFor(() => expect(screen.getAllByText("Relance…")).toHaveLength(2));
expect(
(screen.getByRole("button", { name: "Relance…" }) as HTMLButtonElement)
.disabled,
).toBe(true);
resolveLaunch({
sessionId: "mock-agent-session-deferred",
assignedConversationId: "mock-conversation-deferred",
write: async () => {},
resize: async () => {},
detach: () => {},
close: async () => {},
});
});
it("shows the stopped-cell restart error copy when relaunch fails", async () => {
const layout = new MockLayoutGateway();
const agent = new MockAgentGateway();
const workState = new MockWorkStateGateway();
const created = await agent.createAgent("p1", {
name: "StoppedAgent",
profileId: "prof-1",
});
vi.spyOn(agent, "launchAgent").mockRejectedValue(
new Error("launch failed"),
);
const tree = await layout.loadLayout("p1");
const leafId = leaves(tree)[0].id;
await layout.mutateLayout("p1", {
type: "setCellAgent",
target: leafId,
agent: created.id,
});
workState._setProjectWorkState("p1", {
agents: [
{
agentId: created.id,
name: "StoppedAgent",
profileId: "prof-1",
runtimeState: "stopped",
busy: { state: "idle" },
},
],
});
render(
<DIProvider gateways={{ layout, agent, workState } as unknown as Gateways}>
<LayoutGrid projectId="p1" cwd="/home/me/proj" />
</DIProvider>,
);
fireEvent.click(await screen.findByRole("button", { name: "Relancer" }));
expect(await screen.findByText("Erreur de relance")).toBeTruthy();
expect(
screen.getByText("Impossible de relancer l’agent. Réessayez."),
).toBeTruthy();
});
});

View File

@ -130,6 +130,7 @@ export function LayoutGrid({
projectId={projectId}
layoutId={layoutId}
workState={work.state}
workStateBusy={work.busy}
refreshWorkState={work.refresh}
refitSignal={refitEpoch}
onOpenConversation={onOpenConversation}
@ -148,6 +149,7 @@ interface NodeViewProps {
projectId: string;
layoutId?: string;
workState: ProjectWorkState | null;
workStateBusy: boolean;
refreshWorkState: () => Promise<void>;
refitSignal: number;
onOpenConversation?: (conversationId: string) => void;
@ -162,6 +164,7 @@ function NodeView({
projectId,
layoutId,
workState,
workStateBusy,
refreshWorkState,
refitSignal,
onOpenConversation,
@ -196,6 +199,7 @@ function NodeView({
projectId={projectId}
layoutId={layoutId}
workState={workState}
workStateBusy={workStateBusy}
refreshWorkState={refreshWorkState}
refitSignal={refitSignal}
/>
@ -209,6 +213,7 @@ function NodeView({
projectId={projectId}
layoutId={layoutId}
workState={workState}
workStateBusy={workStateBusy}
refreshWorkState={refreshWorkState}
refitSignal={refitSignal}
onOpenConversation={onOpenConversation}
@ -224,6 +229,7 @@ function NodeView({
projectId={projectId}
layoutId={layoutId}
workState={workState}
workStateBusy={workStateBusy}
refreshWorkState={refreshWorkState}
refitSignal={refitSignal}
onOpenConversation={onOpenConversation}
@ -244,6 +250,7 @@ interface LeafViewProps {
projectId: string;
layoutId?: string;
workState: ProjectWorkState | null;
workStateBusy: boolean;
refreshWorkState: () => Promise<void>;
refitSignal: number;
}
@ -270,6 +277,11 @@ interface TrustedCustomCliBinding {
profile: AgentProfile;
}
interface FreshRestartedSession {
agentId: string;
sessionId: string;
}
export function shouldFallbackCustomCliMode(input: {
agentsLoaded: boolean;
profilesLoaded: boolean;
@ -309,6 +321,64 @@ function goToCell(nodeId: string): void {
}, 1200);
}
function StoppedAgentCellPanel({
agentName,
restarting,
error,
onRestart,
onRefresh,
}: {
agentName: string;
restarting: boolean;
error: boolean;
onRestart: () => void;
onRefresh: () => void;
}) {
return (
<section
className="stopped-agent-cell"
aria-label={`${agentName} arrêté`}
>
<div className="stopped-agent-cell__header">
<h3 className="stopped-agent-cell__name">{agentName}</h3>
<span className="stopped-agent-cell__badge">Arrêté</span>
</div>
<div className="stopped-agent-cell__content">
<p className="stopped-agent-cell__message">
{error ? "Erreur de relance" : restarting ? "Relance…" : "Cette session est terminée."}
</p>
{error ? (
<p className="stopped-agent-cell__help">
Impossible de relancer l’agent. Réessayez.
</p>
) : (
<p className="stopped-agent-cell__help">
Ouvre une nouvelle session pour cet agent.
</p>
)}
</div>
<div className="stopped-agent-cell__actions">
<button
type="button"
className="stopped-agent-cell__button stopped-agent-cell__button--primary"
onClick={onRestart}
disabled={restarting}
>
{restarting ? "Relance…" : "Relancer"}
</button>
<button
type="button"
className="stopped-agent-cell__button"
onClick={onRefresh}
disabled={restarting}
>
Actualiser
</button>
</div>
</section>
);
}
/**
* 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.
@ -348,6 +418,7 @@ function LeafView({
projectId,
layoutId,
workState,
workStateBusy,
refreshWorkState,
refitSignal,
}: LeafViewProps) {
@ -471,10 +542,15 @@ function LeafView({
// backend refuses such a launch (`AGENT_ALREADY_RUNNING`); disabling it here is
// the matching UI affordance.
const [liveAgents, setLiveAgents] = useState<LiveAgent[]>([]);
const refreshLive = () => {
if (!agentGateway?.listLiveAgents) return;
agentGateway.listLiveAgents(projectId).then(setLiveAgents).catch(() => {
const refreshLive = async (): Promise<LiveAgent[]> => {
if (!agentGateway?.listLiveAgents) return [];
return agentGateway.listLiveAgents(projectId).then((list) => {
setLiveAgents(list);
return list;
}).catch(() => {
/* ignore — treat as none live */
setLiveAgents([]);
return [];
});
};
useEffect(() => {
@ -669,6 +745,46 @@ function LeafView({
) ?? [];
const busyByWorkState = agentWork?.busy.state === "busy" || Boolean(activeTicket);
const delegatedTicket = activeTicket?.source.kind === "agent" ? activeTicket : undefined;
const [restartPending, setRestartPending] = useState(false);
const [restartError, setRestartError] = useState(false);
const [freshRestartedSession, setFreshRestartedSession] =
useState<FreshRestartedSession | null>(null);
const runtimeState = agentWork?.runtimeState;
const livePinnedAgent = agentId
? liveAgents.find((la) => la.agentId === agentId)
: undefined;
const hasFreshRestartedLiveSession = Boolean(
agentId &&
session &&
freshRestartedSession?.agentId === agentId &&
freshRestartedSession.sessionId === session &&
livePinnedAgent?.sessionId === session,
);
const stoppedAgentCell = Boolean(
agentId &&
(runtimeState === "stopped" || runtimeState === "failedStart") &&
!hasFreshRestartedLiveSession,
);
const agentCellHydrating = Boolean(
agentId &&
session &&
(!agentsLoaded ||
!profilesLoaded ||
(workState === null && workStateBusy)),
);
useEffect(() => {
if (!freshRestartedSession || !agentId) return;
if (freshRestartedSession.agentId !== agentId) {
setFreshRestartedSession(null);
return;
}
const live = liveAgents.find((la) => la.agentId === agentId);
if (!live || live.sessionId !== freshRestartedSession.sessionId) {
setFreshRestartedSession(null);
}
}, [agentId, freshRestartedSession, liveAgents]);
async function interruptCurrentTurn(): Promise<void> {
if (!agentId || !input) return;
try {
@ -738,6 +854,74 @@ function LeafView({
const liveFor = (candidate: string): LiveAgent | undefined =>
liveAgents.find((la) => la.agentId === candidate);
async function refreshStoppedAgentState(): Promise<void> {
setRestartError(false);
await Promise.all([refreshWorkState(), refreshLive()]);
}
async function restartStoppedAgent(): Promise<void> {
if (!agentId || !agentGateway) return;
setRestartPending(true);
setRestartError(false);
try {
await vm.setSession(id, null);
await vm.setCellConversation(id, null);
if (
cellMode === "custom" &&
effectiveCustomCliAvailable &&
agentGateway.launchAgentChat
) {
const handle = await agentGateway.launchAgentChat(projectId, agentId, {
cwd,
rows: 24,
cols: 80,
nodeId: id,
freshStart: true,
});
await setSessionWithInstrumentation("custom", handle.sessionId);
await vm.setCellConversation(id, handle.assignedConversationId ?? null);
const live = await refreshLive();
setFreshRestartedSession({
agentId,
sessionId:
live.find((entry) => entry.agentId === agentId)?.sessionId ??
handle.sessionId,
});
await refreshWorkState();
return;
}
if (!agentGateway.launchAgent) throw new Error("Agent launch unavailable");
const handle = await agentGateway.launchAgent(
projectId,
agentId,
{
cwd,
rows: 24,
cols: 80,
nodeId: id,
freshStart: true,
},
() => {},
);
await setSessionWithInstrumentation("plain", handle.sessionId);
await vm.setCellConversation(id, handle.assignedConversationId ?? null);
handle.detach();
const live = await refreshLive();
setFreshRestartedSession({
agentId,
sessionId:
live.find((entry) => entry.agentId === agentId)?.sessionId ??
handle.sessionId,
});
await refreshWorkState();
} catch {
setRestartError(true);
setFreshRestartedSession(null);
} finally {
setRestartPending(false);
}
}
/**
* Maps an error into a {@link CellNotice}. For the singleton-invariant refusal
* (`AGENT_ALREADY_RUNNING`) it resolves the host cell from the live-agents set
@ -1252,7 +1436,34 @@ function LeafView({
minWidth: 0,
}}
>
{agentId && cellMode === "custom" && effectiveCustomCliAvailable && customCliAgent && customCliProfile ? (
{agentId && agentCellHydrating ? (
<div
data-testid="agent-cell-runtime-loading"
role="status"
aria-live="polite"
style={{
position: "absolute",
inset: 0,
zIndex: 1,
display: "flex",
alignItems: "center",
justifyContent: "center",
background: "var(--color-canvas, #0e1116)",
color: "var(--color-muted, #9aa7b4)",
fontSize: 12,
}}
>
Chargement…
</div>
) : agentId && stoppedAgentCell ? (
<StoppedAgentCellPanel
agentName={pinnedAgent?.name ?? "Agent"}
restarting={restartPending}
error={restartError}
onRestart={() => void restartStoppedAgent()}
onRefresh={() => void refreshStoppedAgentState()}
/>
) : agentId && cellMode === "custom" && effectiveCustomCliAvailable && customCliAgent && customCliProfile ? (
<CustomAgentChatView
key={`${id}-${agentId}-custom`}
projectId={projectId}
@ -1589,6 +1800,7 @@ interface SplitViewProps {
projectId: string;
layoutId?: string;
workState: ProjectWorkState | null;
workStateBusy: boolean;
refreshWorkState: () => Promise<void>;
refitSignal: number;
onOpenConversation?: (conversationId: string) => void;
@ -1602,6 +1814,7 @@ function SplitView({
projectId,
layoutId,
workState,
workStateBusy,
refreshWorkState,
refitSignal,
onOpenConversation,
@ -1649,6 +1862,7 @@ function SplitView({
projectId={projectId}
layoutId={layoutId}
workState={workState}
workStateBusy={workStateBusy}
refreshWorkState={refreshWorkState}
refitSignal={refitSignal}
onOpenConversation={onOpenConversation}
@ -1746,6 +1960,7 @@ interface GridViewProps {
projectId: string;
layoutId?: string;
workState: ProjectWorkState | null;
workStateBusy: boolean;
refreshWorkState: () => Promise<void>;
refitSignal: number;
onOpenConversation?: (conversationId: string) => void;
@ -1759,6 +1974,7 @@ function GridView({
projectId,
layoutId,
workState,
workStateBusy,
refreshWorkState,
refitSignal,
onOpenConversation,
@ -1802,6 +2018,7 @@ function GridView({
projectId={projectId}
layoutId={layoutId}
workState={workState}
workStateBusy={workStateBusy}
refreshWorkState={refreshWorkState}
refitSignal={refitSignal}
onOpenConversation={onOpenConversation}

View File

@ -101,3 +101,135 @@
left: 100%;
}
}
@layer components {
.stopped-agent-cell {
position: absolute;
inset: 0;
z-index: 1;
display: flex;
flex-direction: column;
min-width: 0;
min-height: 0;
padding: 28px 12px 12px;
background: var(--color-canvas);
color: var(--color-content);
}
.stopped-agent-cell__header {
display: flex;
min-width: 0;
align-items: flex-start;
gap: 8px;
}
.stopped-agent-cell__name {
min-width: 0;
margin: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 13px;
font-weight: 650;
line-height: 1.35;
}
.stopped-agent-cell__badge {
margin-left: auto;
flex-shrink: 0;
border: 1px solid var(--color-warning);
border-radius: 3px;
background: rgb(210 153 34 / 16%);
color: var(--color-warning);
padding: 1px 6px;
font-size: 11px;
font-weight: 650;
line-height: 1.35;
}
.stopped-agent-cell__content {
min-width: 0;
margin-top: 18px;
}
.stopped-agent-cell__message {
margin: 0;
font-size: 13px;
font-weight: 600;
line-height: 1.35;
}
.stopped-agent-cell__help {
margin: 4px 0 0;
color: var(--color-muted);
font-size: 12px;
line-height: 1.4;
}
.stopped-agent-cell__actions {
display: flex;
gap: 8px;
margin-top: auto;
}
.stopped-agent-cell__button {
min-height: 28px;
border: 1px solid var(--color-border-strong);
border-radius: 3px;
background: var(--color-surface);
color: var(--color-content);
padding: 3px 10px;
font-size: 12px;
font-weight: 600;
cursor: pointer;
}
.stopped-agent-cell__button:hover:not(:disabled) {
background: var(--color-raised);
}
.stopped-agent-cell__button:disabled {
color: var(--color-faint);
cursor: default;
opacity: 0.75;
}
.stopped-agent-cell__button--primary {
border-color: var(--color-primary);
background: var(--color-primary);
color: var(--color-on-primary);
}
.stopped-agent-cell__button--primary:hover:not(:disabled) {
background: var(--color-primary-hover);
}
@media (max-width: 640px) {
.stopped-agent-cell {
padding: 34px 10px 10px;
}
.stopped-agent-cell__header {
flex-direction: column;
gap: 4px;
}
.stopped-agent-cell__badge {
margin-left: 0;
align-self: flex-start;
}
.stopped-agent-cell__content {
margin-top: 12px;
}
.stopped-agent-cell__actions {
flex-direction: column;
gap: 6px;
}
.stopped-agent-cell__button {
width: 100%;
}
}
}