From cdb395307b6f6534c949e57bd2b3f0b7feac58af Mon Sep 17 00:00:00 2001 From: Blomios Date: Wed, 12 Aug 2026 15:34:53 +0200 Subject: [PATCH] =?UTF-8?q?feat(ui):=20cellule=20agent=20stopp=C3=A9=20?= =?UTF-8?q?=E2=80=94=20=C3=A9tat=20explicite=20+=20reopen=20from=20scratch?= =?UTF-8?q?=20(#197,=20QA=20verte)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- .../features/layout/LayoutGrid.chat.test.tsx | 70 +++++ .../src/features/layout/LayoutGrid.test.tsx | 267 +++++++++++++++++- frontend/src/features/layout/LayoutGrid.tsx | 225 ++++++++++++++- frontend/src/shared/styles/theme.css | 132 +++++++++ 4 files changed, 689 insertions(+), 5 deletions(-) diff --git a/frontend/src/features/layout/LayoutGrid.chat.test.tsx b/frontend/src/features/layout/LayoutGrid.chat.test.tsx index 31742442..47490955 100644 --- a/frontend/src/features/layout/LayoutGrid.chat.test.tsx +++ b/frontend/src/features/layout/LayoutGrid.chat.test.tsx @@ -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(); diff --git a/frontend/src/features/layout/LayoutGrid.test.tsx b/frontend/src/features/layout/LayoutGrid.test.tsx index 9586ffbc..aab68f44 100644 --- a/frontend/src/features/layout/LayoutGrid.test.tsx +++ b/frontend/src/features/layout/LayoutGrid.test.tsx @@ -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( + + + , + ); + + 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( + + + , + ); + + 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( + + + , + ); + + 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>) => void; + const launchPromise = new Promise>>( + (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( + + + , + ); + + 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( + + + , + ); + + 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(); + }); }); diff --git a/frontend/src/features/layout/LayoutGrid.tsx b/frontend/src/features/layout/LayoutGrid.tsx index 1302f887..abaaab00 100644 --- a/frontend/src/features/layout/LayoutGrid.tsx +++ b/frontend/src/features/layout/LayoutGrid.tsx @@ -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; 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; 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 ( +
+
+

{agentName}

+ Arrêté +
+
+

+ {error ? "Erreur de relance" : restarting ? "Relance…" : "Cette session est terminée."} +

+ {error ? ( +

+ Impossible de relancer l’agent. Réessayez. +

+ ) : ( +

+ Ouvre une nouvelle session pour cet agent. +

+ )} +
+
+ + +
+
+ ); +} + /** * 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([]); - const refreshLive = () => { - if (!agentGateway?.listLiveAgents) return; - agentGateway.listLiveAgents(projectId).then(setLiveAgents).catch(() => { + const refreshLive = async (): Promise => { + 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(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 { 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 { + setRestartError(false); + await Promise.all([refreshWorkState(), refreshLive()]); + } + + async function restartStoppedAgent(): Promise { + 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 ? ( +
+ Chargement… +
+ ) : agentId && stoppedAgentCell ? ( + void restartStoppedAgent()} + onRefresh={() => void refreshStoppedAgentState()} + /> + ) : agentId && cellMode === "custom" && effectiveCustomCliAvailable && customCliAgent && customCliProfile ? ( Promise; 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; 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} diff --git a/frontend/src/shared/styles/theme.css b/frontend/src/shared/styles/theme.css index 8775763a..11406731 100644 --- a/frontend/src/shared/styles/theme.css +++ b/frontend/src/shared/styles/theme.css @@ -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%; + } + } +}