feat: allow manual Node.js path configuration (#1577)
Add ability to manually configure Node.js path for users who have Node.js installed but not in their system PATH. Features: - Browse and select custom Node.js installation folder - Visual status indicator showing Node.js version or "Not found" - Reset to system default PATH option - Manual configuration option in setup banner - Real-time Node.js status checking closes #1050 <!-- This is an auto-generated description by cubic. --> --- ## Summary by cubic Adds manual Node.js path configuration so the app works even when Node isn’t on PATH, fulfilling #1050. Users can browse to their install, reset to default, and see real-time status in Settings and during setup. - New Features - Settings: NodePathSelector to browse a Node.js folder, show version/“Not found” status, and reset to system PATH (persists customNodePath). - Setup banner: manual config flow with a folder picker if Node is already installed. - IPC: select-node-folder, set-node-path, get-node-path; reloads env and prepends custom path to PATH. - Real-time Node.js status check with visual indicator (CheckCircle on valid). - E2E tests for browse, reset, and valid-status display. <!-- End of auto-generated description by cubic. -->
This commit is contained in:
committed by
GitHub
parent
cc435d1ed4
commit
b1095b7951
58
e2e-tests/nodejs_path_configuration.spec.ts
Normal file
58
e2e-tests/nodejs_path_configuration.spec.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { test } from "./helpers/test_helper";
|
||||||
|
import { expect } from "@playwright/test";
|
||||||
|
|
||||||
|
test.describe("Node.js Path Configuration", () => {
|
||||||
|
test("should browse and set custom Node.js path", async ({ po }) => {
|
||||||
|
await po.setUp();
|
||||||
|
await po.goToSettingsTab();
|
||||||
|
|
||||||
|
const browseButton = po.page.getByRole("button", {
|
||||||
|
name: /Browse for Node\.js/i,
|
||||||
|
});
|
||||||
|
await browseButton.click();
|
||||||
|
|
||||||
|
// Should show selecting state
|
||||||
|
await expect(
|
||||||
|
po.page.getByRole("button", { name: /Selecting\.\.\./i }),
|
||||||
|
).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should reset custom path to system default", async ({ po }) => {
|
||||||
|
await po.setUp();
|
||||||
|
await po.goToSettingsTab();
|
||||||
|
|
||||||
|
const resetButton = po.page.getByRole("button", {
|
||||||
|
name: /Reset to Default/i,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (await resetButton.isVisible()) {
|
||||||
|
await resetButton.click();
|
||||||
|
|
||||||
|
// Should show system PATH after reset
|
||||||
|
await expect(po.page.getByText("System PATH:")).toBeVisible();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should show CheckCircle when Node.js is valid", async ({ po }) => {
|
||||||
|
await po.setUp();
|
||||||
|
await po.goToSettingsTab();
|
||||||
|
|
||||||
|
// Wait for status check
|
||||||
|
await po.page.waitForTimeout(2000);
|
||||||
|
|
||||||
|
// Target the specific valid status container with CheckCircle
|
||||||
|
const validStatus = po.page.locator(
|
||||||
|
"div.flex.items-center.gap-1.text-green-600, div.flex.items-center.gap-1.text-green-400",
|
||||||
|
);
|
||||||
|
|
||||||
|
// Skip test if Node.js is not installed
|
||||||
|
if (!(await validStatus.isVisible())) {
|
||||||
|
test.skip();
|
||||||
|
}
|
||||||
|
|
||||||
|
// If visible, check for CheckCircle icon
|
||||||
|
await expect(validStatus).toBeVisible();
|
||||||
|
const checkIcon = validStatus.locator("svg").first();
|
||||||
|
await expect(checkIcon).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
186
src/components/NodePathSelector.tsx
Normal file
186
src/components/NodePathSelector.tsx
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { useSettings } from "@/hooks/useSettings";
|
||||||
|
import { showError, showSuccess } from "@/lib/toast";
|
||||||
|
import { IpcClient } from "@/ipc/ipc_client";
|
||||||
|
import { FolderOpen, RotateCcw, CheckCircle, AlertCircle } from "lucide-react";
|
||||||
|
|
||||||
|
export function NodePathSelector() {
|
||||||
|
const { settings, updateSettings } = useSettings();
|
||||||
|
const [isSelectingPath, setIsSelectingPath] = useState(false);
|
||||||
|
const [nodeStatus, setNodeStatus] = useState<{
|
||||||
|
version: string | null;
|
||||||
|
isValid: boolean;
|
||||||
|
}>({
|
||||||
|
version: null,
|
||||||
|
isValid: false,
|
||||||
|
});
|
||||||
|
const [isCheckingNode, setIsCheckingNode] = useState(false);
|
||||||
|
const [systemPath, setSystemPath] = useState<string>("Loading...");
|
||||||
|
|
||||||
|
// Check Node.js status when component mounts or path changes
|
||||||
|
useEffect(() => {
|
||||||
|
checkNodeStatus();
|
||||||
|
}, [settings?.customNodePath]);
|
||||||
|
|
||||||
|
const fetchSystemPath = async () => {
|
||||||
|
try {
|
||||||
|
const debugInfo = await IpcClient.getInstance().getSystemDebugInfo();
|
||||||
|
setSystemPath(debugInfo.nodePath || "System PATH (not available)");
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to fetch system path:", err);
|
||||||
|
setSystemPath("System PATH (not available)");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Fetch system path on mount
|
||||||
|
fetchSystemPath();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const checkNodeStatus = async () => {
|
||||||
|
if (!settings) return;
|
||||||
|
setIsCheckingNode(true);
|
||||||
|
try {
|
||||||
|
const status = await IpcClient.getInstance().getNodejsStatus();
|
||||||
|
setNodeStatus({
|
||||||
|
version: status.nodeVersion,
|
||||||
|
isValid: !!status.nodeVersion,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to check Node.js status:", error);
|
||||||
|
setNodeStatus({ version: null, isValid: false });
|
||||||
|
} finally {
|
||||||
|
setIsCheckingNode(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const handleSelectNodePath = async () => {
|
||||||
|
setIsSelectingPath(true);
|
||||||
|
try {
|
||||||
|
// Call the IPC method to select folder
|
||||||
|
const result = await IpcClient.getInstance().selectNodeFolder();
|
||||||
|
if (result.path) {
|
||||||
|
// Save the custom path to settings
|
||||||
|
await updateSettings({ customNodePath: result.path });
|
||||||
|
// Update the environment PATH
|
||||||
|
await IpcClient.getInstance().reloadEnvPath();
|
||||||
|
// Recheck Node.js status
|
||||||
|
await checkNodeStatus();
|
||||||
|
showSuccess("Node.js path updated successfully");
|
||||||
|
} else if (result.path === null && result.canceled === false) {
|
||||||
|
showError(
|
||||||
|
`Could not find Node.js at the path "${result.selectedPath}"`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
showError(`Failed to set Node.js path: ${error.message}`);
|
||||||
|
} finally {
|
||||||
|
setIsSelectingPath(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const handleResetToDefault = async () => {
|
||||||
|
try {
|
||||||
|
// Clear the custom path
|
||||||
|
await updateSettings({ customNodePath: null });
|
||||||
|
// Reload environment to use system PATH
|
||||||
|
await IpcClient.getInstance().reloadEnvPath();
|
||||||
|
// Recheck Node.js status
|
||||||
|
await fetchSystemPath();
|
||||||
|
await checkNodeStatus();
|
||||||
|
showSuccess("Reset to system Node.js path");
|
||||||
|
} catch (error: any) {
|
||||||
|
showError(`Failed to reset Node.js path: ${error.message}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!settings) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const currentPath = settings.customNodePath || systemPath;
|
||||||
|
const isCustomPath = !!settings.customNodePath;
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Label className="text-sm font-medium">
|
||||||
|
Node.js Path Configuration
|
||||||
|
</Label>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={handleSelectNodePath}
|
||||||
|
disabled={isSelectingPath}
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<FolderOpen className="w-4 h-4" />
|
||||||
|
{isSelectingPath ? "Selecting..." : "Browse for Node.js"}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{isCustomPath && (
|
||||||
|
<Button
|
||||||
|
onClick={handleResetToDefault}
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<RotateCcw className="w-4 h-4" />
|
||||||
|
Reset to Default
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="p-3 bg-gray-50 dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700">
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-center gap-2 mb-1">
|
||||||
|
<span className="text-xs font-medium text-gray-500 dark:text-gray-400">
|
||||||
|
{isCustomPath ? "Custom Path:" : "System PATH:"}
|
||||||
|
</span>
|
||||||
|
{isCustomPath && (
|
||||||
|
<span className="px-2 py-0.5 text-xs bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300 rounded">
|
||||||
|
Custom
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="text-sm font-mono text-gray-700 dark:text-gray-300 break-all max-h-32 overflow-y-auto">
|
||||||
|
{currentPath}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Status Indicator */}
|
||||||
|
<div className="ml-3 flex items-center">
|
||||||
|
{isCheckingNode ? (
|
||||||
|
<div className="animate-spin rounded-full h-4 w-4 border-2 border-gray-300 border-t-blue-500" />
|
||||||
|
) : nodeStatus.isValid ? (
|
||||||
|
<div className="flex items-center gap-1 text-green-600 dark:text-green-400">
|
||||||
|
<CheckCircle className="w-4 h-4" />
|
||||||
|
<span className="text-xs">{nodeStatus.version}</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center gap-1 text-yellow-600 dark:text-yellow-400">
|
||||||
|
<AlertCircle className="w-4 h-4" />
|
||||||
|
<span className="text-xs">Not found</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Help Text */}
|
||||||
|
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
{nodeStatus.isValid ? (
|
||||||
|
<p>Node.js is properly configured and ready to use.</p>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<p>
|
||||||
|
Select the folder where Node.js is installed if it's not in your
|
||||||
|
system PATH.
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
XCircle,
|
XCircle,
|
||||||
Loader2,
|
Loader2,
|
||||||
Settings,
|
Settings,
|
||||||
|
Folder,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { providerSettingsRoute } from "@/routes/settings/providers/$provider";
|
import { providerSettingsRoute } from "@/routes/settings/providers/$provider";
|
||||||
|
|
||||||
@@ -30,6 +31,8 @@ import { useScrollAndNavigateTo } from "@/hooks/useScrollAndNavigateTo";
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import logo from "../../assets/logo.svg";
|
import logo from "../../assets/logo.svg";
|
||||||
import { OnboardingBanner } from "./home/OnboardingBanner";
|
import { OnboardingBanner } from "./home/OnboardingBanner";
|
||||||
|
import { showError } from "@/lib/toast";
|
||||||
|
import { useSettings } from "@/hooks/useSettings";
|
||||||
|
|
||||||
type NodeInstallStep =
|
type NodeInstallStep =
|
||||||
| "install"
|
| "install"
|
||||||
@@ -60,6 +63,32 @@ export function SetupBanner() {
|
|||||||
setNodeCheckError(true);
|
setNodeCheckError(true);
|
||||||
}
|
}
|
||||||
}, [setNodeSystemInfo, setNodeCheckError]);
|
}, [setNodeSystemInfo, setNodeCheckError]);
|
||||||
|
const [showManualConfig, setShowManualConfig] = useState(false);
|
||||||
|
const [isSelectingPath, setIsSelectingPath] = useState(false);
|
||||||
|
const { updateSettings } = useSettings();
|
||||||
|
|
||||||
|
// Add handler for manual path selection
|
||||||
|
const handleManualNodeConfig = useCallback(async () => {
|
||||||
|
setIsSelectingPath(true);
|
||||||
|
try {
|
||||||
|
const result = await IpcClient.getInstance().selectNodeFolder();
|
||||||
|
if (result.path) {
|
||||||
|
await updateSettings({ customNodePath: result.path });
|
||||||
|
await IpcClient.getInstance().reloadEnvPath();
|
||||||
|
await checkNode();
|
||||||
|
setNodeInstallStep("finished-checking");
|
||||||
|
setShowManualConfig(false);
|
||||||
|
} else if (result.path === null && result.canceled === false) {
|
||||||
|
showError(
|
||||||
|
`Could not find Node.js at the path "${result.selectedPath}"`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showError("Error setting Node.js path:" + error);
|
||||||
|
} finally {
|
||||||
|
setIsSelectingPath(false);
|
||||||
|
}
|
||||||
|
}, [checkNode]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
checkNode();
|
checkNode();
|
||||||
@@ -222,6 +251,38 @@ export function SetupBanner() {
|
|||||||
handleNodeInstallClick={handleNodeInstallClick}
|
handleNodeInstallClick={handleNodeInstallClick}
|
||||||
finishNodeInstall={finishNodeInstall}
|
finishNodeInstall={finishNodeInstall}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<div className="mt-3 pt-3 border-t border-gray-200 dark:border-gray-700">
|
||||||
|
<button
|
||||||
|
onClick={() => setShowManualConfig(!showManualConfig)}
|
||||||
|
className="text-sm text-blue-600 dark:text-blue-400 hover:underline"
|
||||||
|
>
|
||||||
|
Node.js already installed? Configure path manually →
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{showManualConfig && (
|
||||||
|
<div className="mt-3 p-3 bg-gray-50 dark:bg-gray-800 rounded-lg">
|
||||||
|
<Button
|
||||||
|
onClick={handleManualNodeConfig}
|
||||||
|
disabled={isSelectingPath}
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
{isSelectingPath ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
|
Selecting...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Folder className="mr-2 h-4 w-4" />
|
||||||
|
Browse for Node.js folder
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<NodeJsHelpCallout />
|
<NodeJsHelpCallout />
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import { ipcMain } from "electron";
|
import { ipcMain, dialog } from "electron";
|
||||||
import { execSync } from "child_process";
|
import { execSync } from "child_process";
|
||||||
import { platform, arch } from "os";
|
import { platform, arch } from "os";
|
||||||
import { NodeSystemInfo } from "../ipc_types";
|
import { NodeSystemInfo } from "../ipc_types";
|
||||||
import fixPath from "fix-path";
|
import fixPath from "fix-path";
|
||||||
import { runShellCommand } from "../utils/runShellCommand";
|
import { runShellCommand } from "../utils/runShellCommand";
|
||||||
import log from "electron-log";
|
import log from "electron-log";
|
||||||
|
import { existsSync } from "fs";
|
||||||
|
import { join } from "path";
|
||||||
|
import { readSettings } from "../../main/settings";
|
||||||
|
|
||||||
const logger = log.scope("node_handlers");
|
const logger = log.scope("node_handlers");
|
||||||
|
|
||||||
@@ -52,6 +55,50 @@ export function registerNodeHandlers() {
|
|||||||
} else {
|
} else {
|
||||||
fixPath();
|
fixPath();
|
||||||
}
|
}
|
||||||
|
const settings = readSettings();
|
||||||
|
if (settings.customNodePath) {
|
||||||
|
const separator = platform() === "win32" ? ";" : ":";
|
||||||
|
process.env.PATH = `${settings.customNodePath}${separator}${process.env.PATH}`;
|
||||||
|
logger.debug(
|
||||||
|
"Added custom Node.js path to PATH:",
|
||||||
|
settings.customNodePath,
|
||||||
|
);
|
||||||
|
}
|
||||||
logger.debug("Reloaded env path, now:", process.env.PATH);
|
logger.debug("Reloaded env path, now:", process.env.PATH);
|
||||||
});
|
});
|
||||||
|
ipcMain.handle("select-node-folder", async () => {
|
||||||
|
const result = await dialog.showOpenDialog({
|
||||||
|
title: "Select Node.js Installation Folder",
|
||||||
|
properties: ["openDirectory"],
|
||||||
|
message: "Select the folder where Node.js is installed",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.canceled) {
|
||||||
|
return { path: null, canceled: true, selectedPath: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result.filePaths[0]) {
|
||||||
|
return { path: null, canceled: false, selectedPath: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedPath = result.filePaths[0];
|
||||||
|
|
||||||
|
// Verify Node.js exists in selected path
|
||||||
|
const nodeBinary = platform() === "win32" ? "node.exe" : "node";
|
||||||
|
const nodePath = join(selectedPath, nodeBinary);
|
||||||
|
|
||||||
|
if (!existsSync(nodePath)) {
|
||||||
|
// Check bin subdirectory (common on Unix systems)
|
||||||
|
const binPath = join(selectedPath, "bin", nodeBinary);
|
||||||
|
if (existsSync(binPath)) {
|
||||||
|
return {
|
||||||
|
path: join(selectedPath, "bin"),
|
||||||
|
canceled: false,
|
||||||
|
selectedPath,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return { path: null, canceled: false, selectedPath };
|
||||||
|
}
|
||||||
|
return { path: selectedPath, canceled: false, selectedPath };
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ import type {
|
|||||||
CloneRepoParams,
|
CloneRepoParams,
|
||||||
SupabaseBranch,
|
SupabaseBranch,
|
||||||
SetSupabaseAppProjectParams,
|
SetSupabaseAppProjectParams,
|
||||||
|
SelectNodeFolderResult,
|
||||||
} from "./ipc_types";
|
} from "./ipc_types";
|
||||||
import type { Template } from "../shared/templates";
|
import type { Template } from "../shared/templates";
|
||||||
import type {
|
import type {
|
||||||
@@ -1180,6 +1181,16 @@ export class IpcClient {
|
|||||||
return this.ipcRenderer.invoke("select-app-folder");
|
return this.ipcRenderer.invoke("select-app-folder");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add these methods to IpcClient class
|
||||||
|
|
||||||
|
public async selectNodeFolder(): Promise<SelectNodeFolderResult> {
|
||||||
|
return this.ipcRenderer.invoke("select-node-folder");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getNodePath(): Promise<string | null> {
|
||||||
|
return this.ipcRenderer.invoke("get-node-path");
|
||||||
|
}
|
||||||
|
|
||||||
public async checkAiRules(params: {
|
public async checkAiRules(params: {
|
||||||
path: string;
|
path: string;
|
||||||
}): Promise<{ exists: boolean }> {
|
}): Promise<{ exists: boolean }> {
|
||||||
|
|||||||
@@ -523,3 +523,12 @@ export interface SetSupabaseAppProjectParams {
|
|||||||
parentProjectId?: string;
|
parentProjectId?: string;
|
||||||
appId: number;
|
appId: number;
|
||||||
}
|
}
|
||||||
|
export interface SetNodePathParams {
|
||||||
|
nodePath: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SelectNodeFolderResult {
|
||||||
|
path: string | null;
|
||||||
|
canceled?: boolean;
|
||||||
|
selectedPath: string | null;
|
||||||
|
}
|
||||||
|
|||||||
@@ -245,6 +245,7 @@ export const UserSettingsSchema = z.object({
|
|||||||
enableAutoUpdate: z.boolean(),
|
enableAutoUpdate: z.boolean(),
|
||||||
releaseChannel: ReleaseChannelSchema,
|
releaseChannel: ReleaseChannelSchema,
|
||||||
runtimeMode2: RuntimeMode2Schema.optional(),
|
runtimeMode2: RuntimeMode2Schema.optional(),
|
||||||
|
customNodePath: z.string().optional().nullable(),
|
||||||
|
|
||||||
////////////////////////////////
|
////////////////////////////////
|
||||||
// E2E TESTING ONLY.
|
// E2E TESTING ONLY.
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import { AutoUpdateSwitch } from "@/components/AutoUpdateSwitch";
|
|||||||
import { ReleaseChannelSelector } from "@/components/ReleaseChannelSelector";
|
import { ReleaseChannelSelector } from "@/components/ReleaseChannelSelector";
|
||||||
import { NeonIntegration } from "@/components/NeonIntegration";
|
import { NeonIntegration } from "@/components/NeonIntegration";
|
||||||
import { RuntimeModeSelector } from "@/components/RuntimeModeSelector";
|
import { RuntimeModeSelector } from "@/components/RuntimeModeSelector";
|
||||||
|
import { NodePathSelector } from "@/components/NodePathSelector";
|
||||||
import { ToolsMcpSettings } from "@/components/settings/ToolsMcpSettings";
|
import { ToolsMcpSettings } from "@/components/settings/ToolsMcpSettings";
|
||||||
|
|
||||||
export default function SettingsPage() {
|
export default function SettingsPage() {
|
||||||
@@ -272,6 +273,9 @@ export function GeneralSettings({ appVersion }: { appVersion: string | null }) {
|
|||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<RuntimeModeSelector />
|
<RuntimeModeSelector />
|
||||||
</div>
|
</div>
|
||||||
|
<div className="mt-4">
|
||||||
|
<NodePathSelector />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center text-sm text-gray-500 dark:text-gray-400 mt-4">
|
<div className="flex items-center text-sm text-gray-500 dark:text-gray-400 mt-4">
|
||||||
<span className="mr-2 font-medium">App Version:</span>
|
<span className="mr-2 font-medium">App Version:</span>
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ const validInvokeChannels = [
|
|||||||
"reset-all",
|
"reset-all",
|
||||||
"nodejs-status",
|
"nodejs-status",
|
||||||
"install-node",
|
"install-node",
|
||||||
|
"select-node-folder",
|
||||||
"github:start-flow",
|
"github:start-flow",
|
||||||
"github:list-repos",
|
"github:list-repos",
|
||||||
"github:get-repo-branches",
|
"github:get-repo-branches",
|
||||||
|
|||||||
Reference in New Issue
Block a user