From 78b224dd74b3b9a295a1d983b5d1f52d1b88f8d3 Mon Sep 17 00:00:00 2001 From: WhoamiI00 Date: Wed, 12 Aug 2026 13:25:08 +0530 Subject: [PATCH] fix(gui): allow the onboarding card to be reopened after dismissal Closing the onboarding card writes `hasDismissedOnboardingCard` to localStorage, but nothing ever clears it. The redux `onboardingCard.show` flag is not persisted (the `ui` slice only persists tool/rule settings), so the existing "Quickstart" row in Help re-showed the card for the current session only - after a reload the localStorage flag suppressed it again, leaving no way to bring the card back. `open` now clears the flag, mirroring `close` which sets it. Every call site of `open` is an explicit request to show the card, so clearing the dismissal there is safe. The Quickstart row now goes through `open` instead of dispatching `setOnboardingCard` directly, so it benefits from the same handling; `activeTab: undefined` was already equivalent to API_KEY, which the card defaults to. Fixes #12582 --- .../hooks/useOnboardingCard.test.tsx | 74 +++++++++++++++++++ .../OnboardingCard/hooks/useOnboardingCard.ts | 3 + gui/src/pages/config/sections/HelpSection.tsx | 10 +-- 3 files changed, 80 insertions(+), 7 deletions(-) create mode 100644 gui/src/components/OnboardingCard/hooks/useOnboardingCard.test.tsx diff --git a/gui/src/components/OnboardingCard/hooks/useOnboardingCard.test.tsx b/gui/src/components/OnboardingCard/hooks/useOnboardingCard.test.tsx new file mode 100644 index 00000000000..456599b4733 --- /dev/null +++ b/gui/src/components/OnboardingCard/hooks/useOnboardingCard.test.tsx @@ -0,0 +1,74 @@ +import { screen } from "@testing-library/react"; +import { beforeEach, describe, expect, it } from "vitest"; +import { getLocalStorage, setLocalStorage } from "../../../util/localStorage"; +import { renderWithProviders } from "../../../util/test/render"; +import { useOnboardingCard } from "./useOnboardingCard"; + +/** + * Minimal probe that surfaces the hook's state and actions to the DOM, so the + * hook can be exercised through the same providers the real card renders in. + */ +function OnboardingCardProbe() { + const { show, open, close } = useOnboardingCard(); + + return ( +
+ {String(show)} + + +
+ ); +} + +describe("useOnboardingCard", () => { + beforeEach(() => { + localStorage.clear(); + }); + + it("shows the card by default for a user who has not onboarded", async () => { + await renderWithProviders(); + + expect(screen.getByTestId("show").textContent).toBe("true"); + }); + + it("persists the dismissal when the card is closed", async () => { + const { user } = await renderWithProviders(); + + await user.click(screen.getByTestId("close")); + + expect(screen.getByTestId("show").textContent).toBe("false"); + expect(getLocalStorage("hasDismissedOnboardingCard")).toBe(true); + }); + + // Regression test for https://github.com/continuedev/continue/issues/12582 + it("clears the persisted dismissal when the card is reopened", async () => { + setLocalStorage("hasDismissedOnboardingCard", true); + + const { user } = await renderWithProviders(); + expect(screen.getByTestId("show").textContent).toBe("false"); + + await user.click(screen.getByTestId("open")); + + expect(screen.getByTestId("show").textContent).toBe("true"); + expect(getLocalStorage("hasDismissedOnboardingCard")).toBe(false); + }); + + // The redux `show` flag is not persisted by redux-persist, so reopening has + // to clear the localStorage flag or the card vanishes again on next load. + it("keeps the card visible after a reload once it has been reopened", async () => { + setLocalStorage("hasDismissedOnboardingCard", true); + + const first = await renderWithProviders(); + await first.user.click(screen.getByTestId("open")); + first.unmount(); + + // A fresh store stands in for a webview reload: only localStorage carries over. + await renderWithProviders(); + + expect(screen.getByTestId("show").textContent).toBe("true"); + }); +}); diff --git a/gui/src/components/OnboardingCard/hooks/useOnboardingCard.ts b/gui/src/components/OnboardingCard/hooks/useOnboardingCard.ts index 75bc28adcfa..253839e83ab 100644 --- a/gui/src/components/OnboardingCard/hooks/useOnboardingCard.ts +++ b/gui/src/components/OnboardingCard/hooks/useOnboardingCard.ts @@ -38,6 +38,9 @@ export function useOnboardingCard(): UseOnboardingCard { } async function open(tab?: OnboardingModes) { + // Clear the dismissal flag set by `close`. The redux `show` flag is not + // persisted, so without this the card disappears again on the next reload. + setLocalStorage("hasDismissedOnboardingCard", false); navigate("/"); dispatch( setOnboardingCard({ diff --git a/gui/src/pages/config/sections/HelpSection.tsx b/gui/src/pages/config/sections/HelpSection.tsx index 1d1b2febc0f..1d16cafe285 100644 --- a/gui/src/pages/config/sections/HelpSection.tsx +++ b/gui/src/pages/config/sections/HelpSection.tsx @@ -8,10 +8,10 @@ import { import { useContext, useMemo } from "react"; import { useNavigate } from "react-router-dom"; import Shortcut from "../../../components/gui/Shortcut"; +import { useOnboardingCard } from "../../../components/OnboardingCard/hooks/useOnboardingCard"; import { Card } from "../../../components/ui"; import { IdeMessengerContext } from "../../../context/IdeMessenger"; import { useAppDispatch, useAppSelector } from "../../../redux/hooks"; -import { setOnboardingCard } from "../../../redux/slices/uiSlice"; import { saveCurrentSession } from "../../../redux/thunks/session"; import { isJetBrains } from "../../../util"; import { ROUTES } from "../../../util/navigation"; @@ -150,6 +150,7 @@ export function HelpSection() { const ideMessenger = useContext(IdeMessengerContext); const navigate = useNavigate(); const dispatch = useAppDispatch(); + const onboardingCard = useOnboardingCard(); const currentSession = useAppSelector((state) => state.session); @@ -226,12 +227,7 @@ export function HelpSection() { generateTitle: true, }), ); - dispatch( - setOnboardingCard({ - show: true, - activeTab: undefined, - }), - ); + void onboardingCard.open(); ideMessenger.post("showTutorial", undefined); }} />