diff --git a/apps/desktop/src/renderer/components/GitHubStarPill/GitHubStarPill.tsx b/apps/desktop/src/renderer/components/GitHubStarPill/GitHubStarPill.tsx index e334180d667..7ad1e08199f 100644 --- a/apps/desktop/src/renderer/components/GitHubStarPill/GitHubStarPill.tsx +++ b/apps/desktop/src/renderer/components/GitHubStarPill/GitHubStarPill.tsx @@ -11,21 +11,36 @@ import { track } from "renderer/lib/analytics"; interface GitHubStarPillProps { className?: string; + /** Analytics surface tag; defaults to "empty_state" for the original callers. */ + surface?: "empty_state" | "new_workspace"; + /** + * Keep the pill's layout box mounted (just faded to invisible) instead of + * unmounting it once starred. The empty-state screens sit at the bottom of + * a plain block, so a height collapse there is harmless; the new-workspace + * screen centers its content with `justify-center`, where that same + * collapse re-centers everything above it. Off by default so the two + * existing callers keep their original unmount-on-hide behavior. + */ + reserveSpace?: boolean; } /** * Small, always-optional "Star Superset on GitHub" pill for the empty - * "no pane open" screens (v1 EmptyTabView and v2 WorkspaceEmptyState). - * Renders straight from live `state`, with no nag-suppression layer — unlike - * the sidebar card/toast, this is a low-key status indicator, not an - * interruptive campaign, so it's allowed to be fully truthful: it hides the - * instant `state` is "starred" and reappears the instant a later unstar is - * confirmed, without waiting on any mute grace window. It briefly stays - * mounted past that point so the confetti/label animation on a fresh star - * has time to play, then dissolves out (fade + soft blur) instead of - * vanishing instantly. + * "no pane open" screens (v1 EmptyTabView and v2 WorkspaceEmptyState) and + * the new-workspace screen. Renders straight from live `state`, with no + * nag-suppression layer — unlike the sidebar card/toast, this is a low-key + * status indicator, not an interruptive campaign, so it's allowed to be + * fully truthful: it hides the instant `state` is "starred" and reappears + * the instant a later unstar is confirmed, without waiting on any mute + * grace window. It briefly stays mounted past that point so the + * confetti/label animation on a fresh star has time to play, then + * dissolves out (fade + soft blur) instead of vanishing instantly. */ -export function GitHubStarPill({ className }: GitHubStarPillProps) { +export function GitHubStarPill({ + className, + surface = "empty_state", + reserveSpace = false, +}: GitHubStarPillProps) { const { state, activate, isBusy } = useGithubStarAction(); const prevStateRef = useRef(null); @@ -69,36 +84,59 @@ export function GitHubStarPill({ className }: GitHubStarPillProps) { } }, [state]); - // Fire at most once per showing — reset once starred so a later unstar - // that re-shows the pill tracks a fresh "shown" impression instead of - // staying silent forever after the first one. - const trackedShownRef = useRef(false); + // Fire at most once per showing per surface — reset once starred so a + // later unstar re-shows the pill and tracks a fresh "shown" impression, + // and reset again if `surface` itself changes so a re-purposed mounted + // instance still gets its own impression instead of inheriting the prior + // surface's guard. + const trackedShownSurfaceRef = useRef | null>(null); useEffect(() => { if (state === "starred") { - trackedShownRef.current = false; + trackedShownSurfaceRef.current = null; return; } - if (trackedShownRef.current) return; + if (trackedShownSurfaceRef.current === surface) return; if (state !== "not_starred" && state !== "unknown") return; - trackedShownRef.current = true; - track("star_nag_shown", { surface: "empty_state" }); - }, [state]); + trackedShownSurfaceRef.current = surface; + track("star_nag_shown", { surface }); + }, [state, surface]); - if (state === "loading") return null; + if (state === "loading" && !reserveSpace) return null; - const isVisible = !( - state === "starred" && - !justStarred && - !staysVisibleForAnimation - ); + const isVisible = + state !== "loading" && + !(state === "starred" && !justStarred && !staysVisibleForAnimation); const handleClick = () => { track(state === "unknown" ? "star_nag_opened_web" : "star_nag_starred", { - surface: "empty_state", + surface, }); activate(); }; + if (reserveSpace) { + // Always mounted so the button's box keeps occupying its slot — only + // opacity/interactivity change, never the layout. + return ( + + + + ); + } + return ( {isVisible && ( diff --git a/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/NewWorkspaceScreen/NewWorkspaceScreen.tsx b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/NewWorkspaceScreen/NewWorkspaceScreen.tsx index 538fd454504..68b2d9a0816 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/NewWorkspaceScreen/NewWorkspaceScreen.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/NewWorkspaceScreen/NewWorkspaceScreen.tsx @@ -29,6 +29,7 @@ import { LuGitPullRequest } from "react-icons/lu"; import { SiLinear } from "react-icons/si"; import { AgentModelSelect } from "renderer/components/AgentModelSelect"; import { AgentSelect } from "renderer/components/AgentSelect"; +import { GitHubStarPill } from "renderer/components/GitHubStarPill"; import { IssueLinkCommand } from "renderer/components/IssueLinkCommand"; import { LinkedIssuePill } from "renderer/components/LinkedIssuePill"; import { MarkdownEditor } from "renderer/components/MarkdownEditor"; @@ -655,6 +656,7 @@ export function NewWorkspaceScreen({

What should we build next?

+