feat(fusion): add Global → General settings pane and wire fn --version

Move the Star-on-GitHub toggle, CLI Binary panel, and update-check
controls out of project General into a new Global → General pane (with
an inline Updates subsection), matching their actual scope. Drop the
CLI Binary panel's outlined card background and apply the standard
horizontal indent so it doesn't bleed to the pane edges.

Wire --version / -v in the fn/fusion bin so the dashboard's CLI Binary
probe gets a real version string instead of booting the dashboard and
reporting "unknown".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-05-01 16:02:23 -07:00
parent e72eff47e9
commit 4231c4a151
4 changed files with 117 additions and 61 deletions

View File

@@ -0,0 +1,9 @@
---
"@runfusion/fusion": minor
---
Split the Star-on-GitHub toggle, CLI Binary panel, and update-check controls into a new **Global → General** settings pane (with an inline **Updates** subsection), separate from the project-scoped Project → General pane. All three are global by nature, and grouping them under Global avoids the impression that they apply only to the active project. The standalone Global → Updates entry has been folded into this pane.
The CLI Binary panel also drops its own outlined card background and adopts the standard `padding: 0 var(--space-xl)` indent every other top-level child of `.settings-content` uses, so it sits flush with adjacent form groups instead of bleeding to the pane edges.
Wire `--version` / `-v` in the `fn` / `fusion` bin so it prints the package version and exits before falling through to the default `dashboard` command. Without this, the dashboard's CLI Binary panel reported the installed version as "unknown" because its `<bin> --version` probe was booting the full server instead of getting a version string.

View File

@@ -13,9 +13,10 @@
*/
import { existsSync, mkdtempSync, readFileSync, symlinkSync, writeFileSync } from "node:fs";
import { createRequire } from "node:module";
import { join, dirname } from "node:path";
import { join, dirname, resolve } from "node:path";
import { tmpdir } from "node:os";
import { performance } from "node:perf_hooks";
import { fileURLToPath } from "node:url";
// @ts-expect-error -- Bun-only global; undefined in Node
const isBunBinary = typeof Bun !== "undefined" && !!Bun.embeddedFiles;
@@ -412,9 +413,53 @@ function getFlagValueNumber(args: string[], flag: string): number | undefined {
return Number.isFinite(parsed) ? parsed : undefined;
}
/**
* Locate `@runfusion/fusion`'s own version by walking up from the running
* `bin.js`. Mirrors `packages/dashboard/src/cli-package-version.ts` but is
* inlined here to avoid pulling the dashboard barrel into the bin's static
* import graph (bin keeps app imports dynamic until env bootstrap is done).
*/
function readOwnCliVersion(): string | undefined {
let currentDir: string;
try {
currentDir = dirname(fileURLToPath(import.meta.url));
} catch {
return undefined;
}
for (let i = 0; i < 8; i += 1) {
const pkgPath = resolve(currentDir, "package.json");
if (existsSync(pkgPath)) {
try {
const parsed = JSON.parse(readFileSync(pkgPath, "utf-8")) as {
name?: string;
version?: string;
};
if (parsed.name === "@runfusion/fusion" && typeof parsed.version === "string") {
return parsed.version;
}
} catch {
// Ignore malformed manifest and keep walking.
}
}
const parentDir = resolve(currentDir, "..");
if (parentDir === currentDir) break;
currentDir = parentDir;
}
return undefined;
}
async function main() {
const { cleanedArgs: args, projectName } = extractGlobalProjectFlag(process.argv.slice(2));
// Print version and exit before any application imports. This is what the
// dashboard's CLI Binary panel probes via `<bin> --version`; without an
// early exit, the flag falls through to the default `dashboard` command and
// boots the full server.
if (args.includes("--version") || args.includes("-v")) {
console.log(readOwnCliVersion() ?? "unknown");
process.exit(0);
}
if (args.includes("--help") || args.includes("-h")) {
console.log(HELP);
process.exit(0);

View File

@@ -2,10 +2,13 @@
display: flex;
flex-direction: column;
gap: 12px;
padding: 16px;
border: 1px solid var(--border-color, rgba(255, 255, 255, 0.08));
border-radius: 8px;
background: var(--surface-2, rgba(255, 255, 255, 0.02));
/* Match the horizontal indent every other top-level child of
.settings-content uses (.form-group, .settings-section-heading, etc.).
Top margin matches .form-group so consecutive sections breathe the same. */
padding: 0 var(--space-xl);
margin-top: var(--space-lg);
border: 0;
background: transparent;
}
.cli-binary-header {

View File

@@ -179,12 +179,12 @@ const SETTINGS_SECTIONS: SettingsSection[] = [
// Global group (shared across all Fusion projects)
{ id: "__global_header", label: "Global", scope: undefined, isGroupHeader: true },
{ id: "global-general", label: "General", scope: "global" },
{ id: "appearance", label: "Appearance", scope: "global" },
{ id: "notifications", label: "Notifications", scope: "global" },
{ id: "node-sync", label: "Node Sync", scope: "global" },
{ id: "global-models", label: "Models", scope: "global" },
{ id: "research-global", label: "Research Defaults", scope: "global" },
{ id: "updates", label: "Updates", scope: "global" },
{ id: "experimental", label: "Experimental Features", scope: "global" },
{ id: "remote", label: "Remote Access", scope: "global" },
@@ -1818,6 +1818,13 @@ export function SettingsModal({
</label>
<small>Show the floating chat button in the dashboard. Chat is still accessible from the Chat tab in the mobile navigation.</small>
</div>
</>
);
case "global-general":
return (
<>
{renderScopeBanner()}
<h4 className="settings-section-heading">General</h4>
<div className="form-group">
<label htmlFor="showGitHubStarButton" className="checkbox-label">
<input
@@ -1836,6 +1843,53 @@ export function SettingsModal({
</small>
</div>
<CliBinaryPanel />
<h4 className="settings-section-heading settings-section-heading--spaced">Updates</h4>
<div className="form-group">
<label htmlFor="updateCheckEnabled" className="checkbox-label">
<input
id="updateCheckEnabled"
type="checkbox"
checked={form.updateCheckEnabled !== false}
onChange={(e) =>
setForm((f) => ({ ...f, updateCheckEnabled: e.target.checked }))
}
/>
Check for updates automatically
</label>
<small>
When enabled, Fusion checks npm for new versions of{" "}
<code>@runfusion/fusion</code> and shows update notices in the CLI and dashboard.
Cadence is governed by the frequency below.
</small>
</div>
<div className="form-group">
<label htmlFor="updateCheckFrequency">Frequency</label>
<select
id="updateCheckFrequency"
value={form.updateCheckFrequency ?? "daily"}
onChange={(e) =>
setForm((f) => ({
...f,
updateCheckFrequency: e.target.value as
| "manual"
| "on-startup"
| "daily"
| "weekly",
}))
}
disabled={form.updateCheckEnabled === false}
>
<option value="manual">Manual only never auto-check</option>
<option value="on-startup">On startup once per server launch</option>
<option value="daily">Daily (recommended)</option>
<option value="weekly">Weekly</option>
</select>
<small>
Controls how often the dashboard re-fetches the npm registry.
Use the version + refresh control in the header to trigger an
immediate check at any time.
</small>
</div>
</>
);
case "global-models": {
@@ -2017,61 +2071,6 @@ export function SettingsModal({
);
}
case "updates": {
return (
<>
{renderScopeBanner()}
<h4 className="settings-section-heading">Updates</h4>
<div className="form-group">
<label htmlFor="updateCheckEnabled" className="checkbox-label">
<input
id="updateCheckEnabled"
type="checkbox"
checked={form.updateCheckEnabled !== false}
onChange={(e) =>
setForm((f) => ({ ...f, updateCheckEnabled: e.target.checked }))
}
/>
Check for updates automatically
</label>
<small>
When enabled, Fusion checks npm for new versions of{" "}
<code>@runfusion/fusion</code> and shows update notices in the CLI and dashboard.
Cadence is governed by the frequency below.
</small>
</div>
<div className="form-group">
<label htmlFor="updateCheckFrequency">Frequency</label>
<select
id="updateCheckFrequency"
value={form.updateCheckFrequency ?? "daily"}
onChange={(e) =>
setForm((f) => ({
...f,
updateCheckFrequency: e.target.value as
| "manual"
| "on-startup"
| "daily"
| "weekly",
}))
}
disabled={form.updateCheckEnabled === false}
>
<option value="manual">Manual only never auto-check</option>
<option value="on-startup">On startup once per server launch</option>
<option value="daily">Daily (recommended)</option>
<option value="weekly">Weekly</option>
</select>
<small>
Controls how often the dashboard re-fetches the npm registry.
Use the version + refresh control in the header to trigger an
immediate check at any time.
</small>
</div>
</>
);
}
case "project-models": {
const presets = form.modelPresets || [];
const presetOptions = presets.map((preset) => ({ id: preset.id, name: preset.name }));