From fd8ed37f8fdf8f1b3e1f2ed31dc23760900b103d Mon Sep 17 00:00:00 2001 From: "posthog[bot]" <206114724+posthog[bot]@users.noreply.github.com> Date: Thu, 11 Jun 2026 21:01:32 +0000 Subject: [PATCH] fix(sessions): add cancel control to cloud initializing screen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A provisioning cloud task renders CloudInitializingView as a full-screen spinner with no interactive controls. If the SSE stream never delivers a status transition, the task is stuck on "Getting things ready…" with no way to stop, cancel, or resume. Surface a Cancel button during the initializing render path, wired to the existing onCancelPrompt callback (cancelPrompt -> cancelCloudPrompt), which already sends a `cancel` command over tRPC for null/queued/in_progress runs and reuses the TASK_RUN_CANCELLED analytics. Generated-By: PostHog Code Task-Id: 677a6fa9-0f43-4d72-aa8a-12dd3a008619 --- .../components/CloudInitializingView.test.tsx | 72 +++++++++++++++++++ .../components/CloudInitializingView.tsx | 23 +++++- .../sessions/components/SessionView.tsx | 5 +- 3 files changed, 98 insertions(+), 2 deletions(-) create mode 100644 packages/ui/src/features/sessions/components/CloudInitializingView.test.tsx 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 ? (