FN-5779: restore Star on GitHub header button

Restore the Settings header GitHub star CTA while removing its configurability.

- keep the Star on GitHub link in Settings header with refreshed split-pill styling and alignment
- reintroduce GitHub star count fetching/caching and click-tracking helpers used by the header control
- update Settings modal header-action test expectations to include the GitHub star link
- revise the changeset note to reflect removing only the showGitHubStarButton setting/toggle

Files changed:
 .changeset/FN-5777-removal.md                      |   4 +-
 packages/dashboard/app/components/SettingsModal.css     |  51 ++++++++-
 packages/dashboard/app/components/SettingsModal.tsx     | 118 ++++++++++++++++++++-
 packages/dashboard/app/components/__tests__/SettingsModal.test.tsx    |   4 +-
 4 files changed, 170 insertions(+), 7 deletions(-)

Fusion-Task-Id: FN-5779

Fusion-Task-Lineage: f485a062-6110-4a79-b42d-70710e3ce5da
This commit is contained in:
gsxdsm
2026-05-31 09:43:38 -07:00
parent 793da2c749
commit b6b902485e
4 changed files with 170 additions and 7 deletions

View File

@@ -2,6 +2,6 @@
"@runfusion/fusion": patch
---
Removed the Settings modal "Star on GitHub" header button and deleted its `showGitHubStarButton` setting.
Removed the `showGitHubStarButton` setting and its Project General toggle from Settings.
This simplifies the Settings surface by removing a promotional control and all of its related client-side logic/styles (including star-count fetch/cache behavior).
The Settings header "Star on GitHub" button remains available (always shown) while the dedicated visibility setting is no longer configurable.

View File

@@ -2,7 +2,7 @@
Extracted from styles.css as part of the Sweep-3 CSS extraction effort.
Imported by SettingsModal.tsx (and MemoryView.tsx for shared classes). */
/* === Settings Modal header action buttons === */
/* === Settings Modal header action buttons (Star on GitHub, Help) === */
.settings-header-actions {
--settings-header-action-height: calc(var(--space-md) * 2 + var(--space-xs) / 2);
@@ -13,7 +13,9 @@
margin-right: var(--space-sm);
}
/* Keep header action buttons at matching heights. */
/* Keep the GitHub Star and Help buttons at matching heights regardless of
their differing icon sizes (provider icon 16px vs. HelpCircle 13px). */
.settings-header-actions > .settings-github-star-btn,
.settings-header-actions > .settings-header-discord-btn,
.settings-header-actions > .settings-header-help-btn {
height: var(--settings-header-action-height);
@@ -45,6 +47,51 @@
}
}
/* GitHub star button — split-pill layout: [Star half | Count half] */
.settings-github-star-btn {
display: inline-flex;
align-items: stretch;
border: var(--btn-border-width) solid var(--border);
border-radius: var(--radius-pill);
background: var(--card);
color: var(--text);
font-size: 12px;
font-weight: 500;
text-decoration: none;
overflow: hidden;
transition:
border-color var(--transition-fast),
background var(--transition-fast);
}
.settings-github-star-btn:hover {
border-color: var(--text-muted);
background: var(--card-hover);
}
.settings-github-star-btn:focus-visible {
outline: none;
box-shadow: var(--focus-ring-strong);
border-color: var(--todo);
}
.settings-github-star-btn__action {
display: inline-flex;
align-items: center;
gap: var(--space-xs);
padding: var(--space-xs) calc(var(--space-sm) + var(--space-xs) / 2);
}
.settings-github-star-btn__count {
display: inline-flex;
align-items: center;
padding: var(--space-xs) calc(var(--space-sm) + var(--space-xs) / 4);
border-left: var(--btn-border-width) solid var(--border);
background: color-mix(in srgb, var(--surface) 60%, var(--card));
color: var(--text-muted);
font-variant-numeric: tabular-nums;
}
/* === Settings Modal: sizing + resizability === */
.settings-modal {
width: min(95vw, 1100px);

View File

@@ -1,5 +1,5 @@
import { useState, useEffect, useCallback, useRef, lazy, Suspense, type CSSProperties, type MouseEvent } from "react";
import { Globe, Folder, RefreshCw, HelpCircle, MessageCircle, Loader2, CheckCircle, AlertTriangle } from "lucide-react";
import { Globe, Folder, RefreshCw, Star, HelpCircle, MessageCircle, Loader2, CheckCircle, AlertTriangle } from "lucide-react";
import {
AGENT_PERMISSION_POLICY_ACTION_CATEGORIES,
THINKING_LEVELS,
@@ -58,6 +58,13 @@ import { NodeHealthDot } from "./NodeHealthDot";
import { TrackingRepoSelect, type TrackingRepoOption } from "./TrackingRepoSelect";
import { filterVisibleOnboardingAndSettingsProviders } from "./providerVisibility";
// ---------------------------------------------------------------------------
// GitHub star count — fetched once per session, cached in localStorage (1 h).
// ---------------------------------------------------------------------------
const GITHUB_STAR_CACHE_KEY = "fusion_github_star_count";
const GITHUB_STAR_CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
const GITHUB_STAR_CLICKED_KEY = "fusion:github-star-clicked";
function toCompleteAgentPermissionRules(rules?: Partial<AgentPermissionPolicyRules>): AgentPermissionPolicyRules {
return AGENT_PERMISSION_POLICY_ACTION_CATEGORIES.reduce((acc, category) => {
acc[category] = rules?.[category] ?? "allow";
@@ -83,6 +90,90 @@ function toTrackingRepoOptions(remotes: GitRemote[]): TrackingRepoOption[] {
return [...byValue.values()].sort((a, b) => a.value.localeCompare(b.value));
}
/**
* Has the user already clicked the "Star on GitHub" button at any point in
* the past? Used to permanently hide the button afterward — clicking opens
* the repo where the actual star happens, so we treat that click as intent
* to star and stop nagging.
*/
function useStarClickedFlag(): [boolean, () => void] {
const [clicked, setClicked] = useState<boolean>(() => {
try {
return localStorage.getItem(GITHUB_STAR_CLICKED_KEY) === "true";
} catch {
return false;
}
});
const markClicked = useCallback(() => {
setClicked(true);
try {
localStorage.setItem(GITHUB_STAR_CLICKED_KEY, "true");
} catch {
// quota / private mode — best-effort
}
}, []);
return [clicked, markClicked];
}
interface StarCache {
count: number;
fetchedAt: number;
}
function useGitHubStarCount(): number | null {
const [count, setCount] = useState<number | null>(() => {
try {
const raw = localStorage.getItem(GITHUB_STAR_CACHE_KEY);
if (raw) {
const parsed: StarCache = JSON.parse(raw) as StarCache;
if (Date.now() - parsed.fetchedAt < GITHUB_STAR_CACHE_TTL_MS) {
return parsed.count;
}
}
} catch {
// ignore malformed cache
}
return null;
});
useEffect(() => {
// If we already have a fresh count from the initial state, skip the fetch.
try {
const raw = localStorage.getItem(GITHUB_STAR_CACHE_KEY);
if (raw) {
const parsed: StarCache = JSON.parse(raw) as StarCache;
if (Date.now() - parsed.fetchedAt < GITHUB_STAR_CACHE_TTL_MS) {
return;
}
}
} catch {
// ignore
}
fetch("https://api.github.com/repos/Runfusion/Fusion")
.then((res) => {
if (!res.ok) return;
return res.json() as Promise<{ stargazers_count?: number }>;
})
.then((data) => {
if (data && typeof data.stargazers_count === "number") {
const cache: StarCache = { count: data.stargazers_count, fetchedAt: Date.now() };
try {
localStorage.setItem(GITHUB_STAR_CACHE_KEY, JSON.stringify(cache));
} catch {
// quota exceeded — just skip
}
setCount(data.stargazers_count);
}
})
.catch(() => {
// Network failure — hide count gracefully, no update
});
}, []);
return count;
}
/**
* Settings sections configuration.
*
@@ -403,6 +494,8 @@ export function SettingsModal({
const [appVersion, setAppVersion] = useState<string | null>(null);
const [updateCheckLoading, setUpdateCheckLoading] = useState(false);
const [updateCheckResult, setUpdateCheckResult] = useState<UpdateCheckResponse | null>(null);
const gitHubStarCount = useGitHubStarCount();
const [starClicked, markStarClicked] = useStarClickedFlag();
const [prefixError, setPrefixError] = useState<string | null>(null);
const [researchLimitError, setResearchLimitError] = useState<string | null>(null);
const [overlapPathPickerIndex, setOverlapPathPickerIndex] = useState<number | null>(null);
@@ -7335,6 +7428,29 @@ export function SettingsModal({
<h3>Settings</h3>
</div>
<div className="settings-header-actions">
<a
href="https://github.com/Runfusion/Fusion"
target="_blank"
rel="noopener noreferrer"
className="settings-github-star-btn"
aria-label="Star Fusion on GitHub"
title="Star Fusion on GitHub"
onClick={markStarClicked}
data-clicked={starClicked ? "true" : "false"}
>
<span className="settings-github-star-btn__action">
<ProviderIcon provider="github" size="sm" />
<Star size={11} aria-hidden="true" />
Star
</span>
{gitHubStarCount !== null && (
<span className="settings-github-star-btn__count" aria-label={`${gitHubStarCount.toLocaleString()} stars`}>
{gitHubStarCount >= 1000
? `${(gitHubStarCount / 1000).toFixed(1)}k`
: gitHubStarCount.toLocaleString()}
</span>
)}
</a>
<a
href="https://discord.gg/ksrfuy7WYR"
target="_blank"

View File

@@ -1529,14 +1529,14 @@ describe("SettingsModal", () => {
});
describe("settings header actions", () => {
it("renders Help and Discord controls, without GitHub star", async () => {
it("renders Help, Discord, and GitHub star controls", async () => {
renderModal();
await waitForSettingsModalReady();
const headerActions = document.querySelector(".settings-header-actions");
expect(headerActions).toBeInTheDocument();
expect(within(headerActions as HTMLElement).queryByRole("link", { name: "Star Fusion on GitHub" })).toBeNull();
expect(within(headerActions as HTMLElement).getByRole("link", { name: "Star Fusion on GitHub" })).toBeInTheDocument();
expect(within(headerActions as HTMLElement).getByRole("link", { name: "Join our Discord" })).toBeInTheDocument();
expect(within(headerActions as HTMLElement).getByRole("link", { name: "Help and discussions" })).toBeInTheDocument();
});