diff --git a/packages/ui/src/features/sessions/components/CloudInitializingView.test.tsx b/packages/ui/src/features/sessions/components/CloudInitializingView.test.tsx
new file mode 100644
index 000000000..6fc383aab
--- /dev/null
+++ b/packages/ui/src/features/sessions/components/CloudInitializingView.test.tsx
@@ -0,0 +1,72 @@
+import { Theme } from "@radix-ui/themes";
+import { act, render, screen } from "@testing-library/react";
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+import { CloudInitializingView } from "./CloudInitializingView";
+
+// The view hides everything behind a 2s reveal delay; fast-forward past it so
+// the heading and controls are mounted.
+function reveal() {
+ act(() => {
+ vi.advanceTimersByTime(2000);
+ });
+}
+
+describe("CloudInitializingView", () => {
+ beforeEach(() => {
+ vi.useFakeTimers();
+ });
+
+ afterEach(() => {
+ vi.useRealTimers();
+ });
+
+ it("renders a cancel control while provisioning when onCancel is provided", () => {
+ render(
+
+ {}} />
+ ,
+ );
+ reveal();
+ expect(screen.getByText("Getting things ready…")).toBeInTheDocument();
+ expect(
+ screen.getByRole("button", { name: "Cancel" }),
+ ).toBeInTheDocument();
+ });
+
+ it("omits the cancel control when no handler is provided", () => {
+ render(
+
+
+ ,
+ );
+ reveal();
+ expect(
+ screen.queryByRole("button", { name: "Cancel" }),
+ ).not.toBeInTheDocument();
+ });
+
+ it("invokes onCancel once and shows a pending label on click", () => {
+ const onCancel = vi.fn();
+ render(
+
+
+ ,
+ );
+ reveal();
+
+ const button = screen.getByRole("button", { name: "Cancel" });
+ act(() => {
+ button.click();
+ });
+
+ expect(onCancel).toHaveBeenCalledTimes(1);
+ const pending = screen.getByRole("button", { name: "Cancelling…" });
+ expect(pending).toBeDisabled();
+
+ // A second click is a no-op while the cancel is in flight.
+ act(() => {
+ pending.click();
+ });
+ expect(onCancel).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/packages/ui/src/features/sessions/components/CloudInitializingView.tsx b/packages/ui/src/features/sessions/components/CloudInitializingView.tsx
index b444721d5..82012f085 100644
--- a/packages/ui/src/features/sessions/components/CloudInitializingView.tsx
+++ b/packages/ui/src/features/sessions/components/CloudInitializingView.tsx
@@ -1,11 +1,13 @@
import { Spinner } from "@phosphor-icons/react";
import type { TaskRunStatus } from "@posthog/shared/domain-types";
-import { Flex, Text } from "@radix-ui/themes";
+import { Button, Flex, Text } from "@radix-ui/themes";
import { useEffect, useState } from "react";
import zenHedgehog from "../../../assets/images/zen.png";
interface CloudInitializingViewProps {
cloudStatus: TaskRunStatus | null;
+ /** Cancels the provisioning cloud run. When omitted, no cancel control is shown. */
+ onCancel?: () => void;
}
const REVEAL_DELAY_MS = 2000;
@@ -35,15 +37,23 @@ function copyFor(cloudStatus: TaskRunStatus | null): {
export function CloudInitializingView({
cloudStatus,
+ onCancel,
}: CloudInitializingViewProps) {
const { heading, subtitle } = copyFor(cloudStatus);
const [revealed, setRevealed] = useState(false);
+ const [cancelling, setCancelling] = useState(false);
useEffect(() => {
const timer = setTimeout(() => setRevealed(true), REVEAL_DELAY_MS);
return () => clearTimeout(timer);
}, []);
+ const handleCancel = () => {
+ if (cancelling) return;
+ setCancelling(true);
+ onCancel?.();
+ };
+
if (!revealed) {
return (
+ {onCancel && (
+
+ )}
);
}
diff --git a/packages/ui/src/features/sessions/components/SessionView.tsx b/packages/ui/src/features/sessions/components/SessionView.tsx
index fccb3f179..0b871c70c 100644
--- a/packages/ui/src/features/sessions/components/SessionView.tsx
+++ b/packages/ui/src/features/sessions/components/SessionView.tsx
@@ -458,7 +458,10 @@ export function SessionView({
>
) : isInitializing ? (
isCloud ? (
-
+
) : pendingTaskPrompt?.promptText ? (