Unverified 提交 a711c99b authored 作者: Will Chen's avatar Will Chen 提交者: GitHub

Add fuzzy search to settings sidebar (#2449)

## Summary - Add a search bar with fuzzy matching (fuse.js) to the settings sidenav for quickly finding any setting - Create a searchable index of all ~23 settings with labels, descriptions, and keywords for forgiving matches - Clicking a search result scrolls to and briefly highlights the target setting with an animation ## Test plan - Navigate to Settings and type partial/approximate terms in the search bar (e.g. "aprv" for Auto-approve, "dark" for Theme, "zoom" for Zoom Level) - Verify results show matching settings grouped by section name - Click a result and verify it scrolls to and highlights the setting - Clear search and verify normal section navigation is restored - Verify "No settings found" appears for non-matching queries 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- devin-review-badge-begin --> --- <a href="https://app.devin.ai/review/dyad-sh/dyad/pull/2449"> <picture> <source media="(prefers-color-scheme: dark)" srcset="https://static.devin.ai/assets/gh-open-in-devin-review-dark.svg?v=1"> <img src="https://static.devin.ai/assets/gh-open-in-devin-review-light.svg?v=1" alt="Open with Devin"> </picture> </a> <!-- devin-review-badge-end --> <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Moderate UI/navigation change: expands settings DOM with new anchor IDs and changes `useScrollAndNavigateTo` signature/behavior, which could impact other scroll-to call sites if any were missed. Adds a new dependency (`fuse.js`) and client-side search logic but no security- or data-sensitive behavior. > > **Overview** > Adds a fuzzy-search input to the settings sidebar (powered by `fuse.js`) so users can search across a new `SETTINGS_SEARCH_INDEX` of settings metadata and jump directly to a specific setting. > > Updates settings navigation to use centralized `SECTION_IDS`/`SETTING_IDS`, adds per-setting DOM `id`s in `settings.tsx`, and extends `useScrollAndNavigateTo` with optional *highlight-on-scroll* behavior (with new `.settings-highlight` CSS animation) used when clicking search results. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 2734a219ca2d3ebe80571dede1ea88d36cca77cc. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> <!-- This is an auto-generated description by cubic. --> --- ## Summary by cubic Add a fuzzy search bar to the settings sidebar so you can quickly find any setting. Clicking a result smooth-scrolls to and briefly highlights the setting. - **New Features** - Fuzzy search across ~23 settings using a searchable index (labels, descriptions, keywords). - Smooth scroll-to with a highlight animation; added anchor IDs for precise navigation. - Clear button to reset the query and a "No settings found" state. - **Dependencies** - Added fuse.js ^7.1.0. <sup>Written for commit 2734a219ca2d3ebe80571dede1ea88d36cca77cc. Summary will update on new commits.</sup> <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: 's avatarClaude Opus 4.5 <noreply@anthropic.com>
上级 69e0b975
...@@ -52,6 +52,7 @@ ...@@ -52,6 +52,7 @@
"esbuild-register": "^3.6.0", "esbuild-register": "^3.6.0",
"fix-path": "^4.0.0", "fix-path": "^4.0.0",
"framer-motion": "^12.6.3", "framer-motion": "^12.6.3",
"fuse.js": "^7.1.0",
"geist": "^1.3.1", "geist": "^1.3.1",
"glob": "^11.0.2", "glob": "^11.0.2",
"html-to-image": "^1.11.13", "html-to-image": "^1.11.13",
...@@ -12529,6 +12530,15 @@ ...@@ -12529,6 +12530,15 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/fuse.js": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-7.1.0.tgz",
"integrity": "sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ==",
"license": "Apache-2.0",
"engines": {
"node": ">=10"
}
},
"node_modules/galactus": { "node_modules/galactus": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/galactus/-/galactus-1.0.0.tgz", "resolved": "https://registry.npmjs.org/galactus/-/galactus-1.0.0.tgz",
......
...@@ -87,6 +87,7 @@ ...@@ -87,6 +87,7 @@
"esbuild-register": "^3.6.0", "esbuild-register": "^3.6.0",
"fix-path": "^4.0.0", "fix-path": "^4.0.0",
"framer-motion": "^12.6.3", "framer-motion": "^12.6.3",
"fuse.js": "^7.1.0",
"geist": "^1.3.1", "geist": "^1.3.1",
"glob": "^11.0.2", "glob": "^11.0.2",
"html-to-image": "^1.11.13", "html-to-image": "^1.11.13",
......
import { atom } from "jotai"; import { atom } from "jotai";
import { SECTION_IDS } from "@/lib/settingsSearchIndex";
export const isPreviewOpenAtom = atom(true); export const isPreviewOpenAtom = atom(true);
export const selectedFileAtom = atom<{ export const selectedFileAtom = atom<{
...@@ -6,5 +7,5 @@ export const selectedFileAtom = atom<{ ...@@ -6,5 +7,5 @@ export const selectedFileAtom = atom<{
line?: number | null; line?: number | null;
} | null>(null); } | null>(null);
export const activeSettingsSectionAtom = atom<string | null>( export const activeSettingsSectionAtom = atom<string | null>(
"general-settings", SECTION_IDS.general,
); );
import { ScrollArea } from "@/components/ui/scroll-area"; import { ScrollArea } from "@/components/ui/scroll-area";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { useEffect } from "react"; import { useEffect, useMemo, useRef, useState } from "react";
import { useScrollAndNavigateTo } from "@/hooks/useScrollAndNavigateTo"; import { useScrollAndNavigateTo } from "@/hooks/useScrollAndNavigateTo";
import { useAtom } from "jotai"; import { useAtom } from "jotai";
import { activeSettingsSectionAtom } from "@/atoms/viewAtoms"; import { activeSettingsSectionAtom } from "@/atoms/viewAtoms";
import { SECTION_IDS, SETTINGS_SEARCH_INDEX } from "@/lib/settingsSearchIndex";
import Fuse from "fuse.js";
import { SearchIcon, XIcon } from "lucide-react";
type SettingsSection = { type SettingsSection = {
id: string; id: string;
...@@ -11,32 +14,54 @@ type SettingsSection = { ...@@ -11,32 +14,54 @@ type SettingsSection = {
}; };
const SETTINGS_SECTIONS: SettingsSection[] = [ const SETTINGS_SECTIONS: SettingsSection[] = [
{ id: "general-settings", label: "General" }, { id: SECTION_IDS.general, label: "General" },
{ id: "workflow-settings", label: "Workflow" }, { id: SECTION_IDS.workflow, label: "Workflow" },
{ id: "ai-settings", label: "AI" }, { id: SECTION_IDS.ai, label: "AI" },
{ id: "provider-settings", label: "Model Providers" }, { id: SECTION_IDS.providers, label: "Model Providers" },
{ id: "telemetry", label: "Telemetry" }, { id: SECTION_IDS.telemetry, label: "Telemetry" },
{ id: "integrations", label: "Integrations" }, { id: SECTION_IDS.integrations, label: "Integrations" },
{ { id: SECTION_IDS.agentPermissions, label: "Agent Permissions" },
id: "agent-permissions", { id: SECTION_IDS.toolsMcp, label: "Tools (MCP)" },
label: "Agent Permissions", { id: SECTION_IDS.experiments, label: "Experiments" },
}, { id: SECTION_IDS.dangerZone, label: "Danger Zone" },
{ id: "tools-mcp", label: "Tools (MCP)" },
{ id: "experiments", label: "Experiments" },
{ id: "danger-zone", label: "Danger Zone" },
]; ];
const fuse = new Fuse(SETTINGS_SEARCH_INDEX, {
keys: [
{ name: "label", weight: 2 },
{ name: "description", weight: 1 },
{ name: "keywords", weight: 1.5 },
{ name: "sectionLabel", weight: 0.5 },
],
threshold: 0.4,
includeScore: true,
ignoreLocation: true,
});
export function SettingsList({ show }: { show: boolean }) { export function SettingsList({ show }: { show: boolean }) {
const [activeSection, setActiveSection] = useAtom(activeSettingsSectionAtom); const [activeSection, setActiveSection] = useAtom(activeSettingsSectionAtom);
const [searchQuery, setSearchQuery] = useState("");
const inputRef = useRef<HTMLInputElement>(null);
const scrollAndNavigateTo = useScrollAndNavigateTo("/settings", { const scrollAndNavigateTo = useScrollAndNavigateTo("/settings", {
behavior: "smooth", behavior: "smooth",
block: "start", block: "start",
}); });
const settingsSections = SETTINGS_SECTIONS; const scrollAndNavigateToWithHighlight = useScrollAndNavigateTo("/settings", {
behavior: "smooth",
block: "start",
highlight: true,
});
const searchResults = useMemo(() => {
if (!searchQuery.trim()) return null;
return fuse.search(searchQuery.trim());
}, [searchQuery]);
useEffect(() => { useEffect(() => {
if (!show) return;
const observer = new IntersectionObserver( const observer = new IntersectionObserver(
(entries) => { (entries) => {
for (const entry of entries) { for (const entry of entries) {
...@@ -49,7 +74,7 @@ export function SettingsList({ show }: { show: boolean }) { ...@@ -49,7 +74,7 @@ export function SettingsList({ show }: { show: boolean }) {
{ rootMargin: "-20% 0px -80% 0px", threshold: 0 }, { rootMargin: "-20% 0px -80% 0px", threshold: 0 },
); );
for (const section of settingsSections) { for (const section of SETTINGS_SECTIONS) {
const el = document.getElementById(section.id); const el = document.getElementById(section.id);
if (el) { if (el) {
observer.observe(el); observer.observe(el);
...@@ -59,35 +84,86 @@ export function SettingsList({ show }: { show: boolean }) { ...@@ -59,35 +84,86 @@ export function SettingsList({ show }: { show: boolean }) {
return () => { return () => {
observer.disconnect(); observer.disconnect();
}; };
}, [settingsSections, setActiveSection]); }, [show, setActiveSection]);
if (!show) { if (!show) {
return null; return null;
} }
const handleScrollAndNavigateTo = scrollAndNavigateTo;
return ( return (
<div className="flex flex-col h-full"> <div className="flex flex-col h-full">
<div className="flex-shrink-0 p-4"> <div className="flex-shrink-0 p-4">
<h2 className="text-lg font-semibold tracking-tight">Settings</h2> <h2 className="text-lg font-semibold tracking-tight">Settings</h2>
</div> </div>
<ScrollArea className="flex-grow"> <div className="flex-shrink-0 px-4 pb-2">
<div className="space-y-1 p-4 pt-0"> <div className="relative">
{settingsSections.map((section) => ( <SearchIcon className="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" />
<input
ref={inputRef}
type="text"
placeholder="Search settings..."
aria-label="Search settings"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full rounded-md border border-input bg-transparent pl-8 pr-8 py-1.5 text-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
/>
{searchQuery && (
<button <button
key={section.id} onClick={() => {
onClick={() => handleScrollAndNavigateTo(section.id)} setSearchQuery("");
className={cn( inputRef.current?.focus();
"w-full text-left px-3 py-2 rounded-md text-sm transition-colors", }}
activeSection === section.id className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
? "bg-sidebar-accent text-sidebar-accent-foreground font-semibold" aria-label="Clear search"
: "hover:bg-sidebar-accent",
)}
> >
{section.label} <XIcon className="h-3.5 w-3.5" />
</button> </button>
))} )}
</div>
</div>
<ScrollArea className="flex-grow">
<div className="space-y-1 p-4 pt-0">
{searchResults !== null ? (
searchResults.length > 0 ? (
searchResults.map((result) => (
<button
key={`${result.item.id}-${result.refIndex}`}
onClick={() => {
scrollAndNavigateToWithHighlight(
result.item.id,
result.item.sectionId,
);
setSearchQuery("");
}}
className="w-full text-left px-3 py-2 rounded-md text-sm transition-colors hover:bg-sidebar-accent"
>
<div className="font-medium">{result.item.label}</div>
<div className="text-xs text-muted-foreground">
{result.item.sectionLabel}
</div>
</button>
))
) : (
<div className="px-3 py-4 text-sm text-muted-foreground text-center">
No settings found
</div>
)
) : (
SETTINGS_SECTIONS.map((section) => (
<button
key={section.id}
onClick={() => scrollAndNavigateTo(section.id)}
className={cn(
"w-full text-left px-3 py-2 rounded-md text-sm transition-colors",
activeSection === section.id
? "bg-sidebar-accent text-sidebar-accent-foreground font-semibold"
: "hover:bg-sidebar-accent",
)}
>
{section.label}
</button>
))
)}
</div> </div>
</ScrollArea> </ScrollArea>
</div> </div>
......
...@@ -10,6 +10,7 @@ import { ...@@ -10,6 +10,7 @@ import {
Folder, Folder,
} from "lucide-react"; } from "lucide-react";
import { providerSettingsRoute } from "@/routes/settings/providers/$provider"; import { providerSettingsRoute } from "@/routes/settings/providers/$provider";
import { SECTION_IDS } from "@/lib/settingsSearchIndex";
import SetupProviderCard from "@/components/SetupProviderCard"; import SetupProviderCard from "@/components/SetupProviderCard";
...@@ -125,7 +126,7 @@ export function SetupBanner() { ...@@ -125,7 +126,7 @@ export function SetupBanner() {
const handleOtherProvidersClick = () => { const handleOtherProvidersClick = () => {
posthog.capture("setup-flow:ai-provider-setup:other:click"); posthog.capture("setup-flow:ai-provider-setup:other:click");
settingsScrollAndNavigateTo("provider-settings"); settingsScrollAndNavigateTo(SECTION_IDS.providers);
}; };
const handleNodeInstallClick = useCallback(async () => { const handleNodeInstallClick = useCallback(async () => {
......
...@@ -2,6 +2,7 @@ import React, { createContext, useContext, useEffect, useState } from "react"; ...@@ -2,6 +2,7 @@ import React, { createContext, useContext, useEffect, useState } from "react";
import { useNavigate } from "@tanstack/react-router"; import { useNavigate } from "@tanstack/react-router";
import { ipc, DeepLinkData } from "../ipc/types"; import { ipc, DeepLinkData } from "../ipc/types";
import { useScrollAndNavigateTo } from "@/hooks/useScrollAndNavigateTo"; import { useScrollAndNavigateTo } from "@/hooks/useScrollAndNavigateTo";
import { SECTION_IDS } from "@/lib/settingsSearchIndex";
type DeepLinkContextType = { type DeepLinkContextType = {
lastDeepLink: (DeepLinkData & { timestamp: number }) | null; lastDeepLink: (DeepLinkData & { timestamp: number }) | null;
...@@ -28,7 +29,7 @@ export function DeepLinkProvider({ children }: { children: React.ReactNode }) { ...@@ -28,7 +29,7 @@ export function DeepLinkProvider({ children }: { children: React.ReactNode }) {
setLastDeepLink({ ...data, timestamp: Date.now() }); setLastDeepLink({ ...data, timestamp: Date.now() });
if (data.type === "add-mcp-server") { if (data.type === "add-mcp-server") {
// Navigate to tools-mcp section // Navigate to tools-mcp section
scrollAndNavigateTo("tools-mcp"); scrollAndNavigateTo(SECTION_IDS.toolsMcp);
} else if (data.type === "add-prompt") { } else if (data.type === "add-prompt") {
// Navigate to library page // Navigate to library page
navigate({ to: "/library" }); navigate({ to: "/library" });
......
...@@ -8,6 +8,7 @@ type ScrollOptions = { ...@@ -8,6 +8,7 @@ type ScrollOptions = {
block?: ScrollLogicalPosition; block?: ScrollLogicalPosition;
inline?: ScrollLogicalPosition; inline?: ScrollLogicalPosition;
onScrolled?: (id: string, element: HTMLElement) => void; onScrolled?: (id: string, element: HTMLElement) => void;
highlight?: boolean;
}; };
/** /**
...@@ -21,7 +22,7 @@ export function useScrollAndNavigateTo( ...@@ -21,7 +22,7 @@ export function useScrollAndNavigateTo(
const setActiveSection = useSetAtom(activeSettingsSectionAtom); const setActiveSection = useSetAtom(activeSettingsSectionAtom);
return useCallback( return useCallback(
async (id: string) => { async (id: string, sectionId?: string) => {
await navigate({ to }); await navigate({ to });
const element = document.getElementById(id); const element = document.getElementById(id);
if (element) { if (element) {
...@@ -30,8 +31,20 @@ export function useScrollAndNavigateTo( ...@@ -30,8 +31,20 @@ export function useScrollAndNavigateTo(
block: options?.block ?? "start", block: options?.block ?? "start",
inline: options?.inline, inline: options?.inline,
}); });
setActiveSection(id); setActiveSection(sectionId ?? id);
options?.onScrolled?.(id, element); options?.onScrolled?.(id, element);
if (options?.highlight) {
element.classList.remove("settings-highlight");
void element.offsetWidth; // force reflow to restart animation
element.classList.add("settings-highlight");
const onEnd = () => {
element.classList.remove("settings-highlight");
};
element.addEventListener("animationend", onEnd, { once: true });
element.addEventListener("animationcancel", onEnd, { once: true });
}
return true; return true;
} }
return false; return false;
...@@ -43,6 +56,7 @@ export function useScrollAndNavigateTo( ...@@ -43,6 +56,7 @@ export function useScrollAndNavigateTo(
options?.block, options?.block,
options?.inline, options?.inline,
options?.onScrolled, options?.onScrolled,
options?.highlight,
setActiveSection, setActiveSection,
], ],
); );
......
export const SECTION_IDS = {
general: "general-settings",
workflow: "workflow-settings",
ai: "ai-settings",
providers: "provider-settings",
telemetry: "telemetry",
integrations: "integrations",
agentPermissions: "agent-permissions",
toolsMcp: "tools-mcp",
experiments: "experiments",
dangerZone: "danger-zone",
} as const;
export const SETTING_IDS = {
theme: "setting-theme",
zoom: "setting-zoom",
autoUpdate: "setting-auto-update",
releaseChannel: "setting-release-channel",
runtimeMode: "setting-runtime-mode",
nodePath: "setting-node-path",
defaultChatMode: "setting-default-chat-mode",
autoApprove: "setting-auto-approve",
autoFix: "setting-auto-fix",
autoExpandPreview: "setting-auto-expand-preview",
chatCompletionNotification: "setting-chat-completion-notification",
thinkingBudget: "setting-thinking-budget",
maxChatTurns: "setting-max-chat-turns",
telemetry: "setting-telemetry",
github: "setting-github",
vercel: "setting-vercel",
supabase: "setting-supabase",
neon: "setting-neon",
nativeGit: "setting-native-git",
reset: "setting-reset",
} as const;
type SearchableSettingItem = {
id: string;
label: string;
description: string;
keywords: string[];
sectionId: string;
sectionLabel: string;
};
export const SETTINGS_SEARCH_INDEX: SearchableSettingItem[] = [
// General Settings
{
id: SETTING_IDS.theme,
label: "Theme",
description: "Switch between system, light, and dark mode",
keywords: ["dark mode", "light mode", "appearance", "color", "system"],
sectionId: SECTION_IDS.general,
sectionLabel: "General",
},
{
id: SETTING_IDS.zoom,
label: "Zoom Level",
description: "Adjust the zoom level to make content easier to read",
keywords: ["font size", "magnify", "scale", "accessibility", "zoom"],
sectionId: SECTION_IDS.general,
sectionLabel: "General",
},
{
id: SETTING_IDS.autoUpdate,
label: "Auto Update",
description: "Automatically update the app when new versions are available",
keywords: ["update", "automatic", "version", "upgrade"],
sectionId: SECTION_IDS.general,
sectionLabel: "General",
},
{
id: SETTING_IDS.releaseChannel,
label: "Release Channel",
description: "Choose between stable and beta release channels",
keywords: ["stable", "beta", "channel", "release", "version"],
sectionId: SECTION_IDS.general,
sectionLabel: "General",
},
{
id: SETTING_IDS.runtimeMode,
label: "Runtime Mode",
description: "Configure Node runtime settings",
keywords: ["node", "runtime", "bun", "environment"],
sectionId: SECTION_IDS.general,
sectionLabel: "General",
},
{
id: SETTING_IDS.nodePath,
label: "Node Path",
description: "Set a custom Node.js installation path",
keywords: ["node", "path", "nodejs", "binary", "executable"],
sectionId: SECTION_IDS.general,
sectionLabel: "General",
},
// Workflow Settings
{
id: SETTING_IDS.defaultChatMode,
label: "Default Chat Mode",
description: "Choose the default mode for new chats",
keywords: ["chat", "mode", "build", "agent", "mcp", "default"],
sectionId: SECTION_IDS.workflow,
sectionLabel: "Workflow",
},
{
id: SETTING_IDS.autoApprove,
label: "Auto-approve",
description: "Automatically approve code changes and run them",
keywords: ["approve", "automatic", "code changes", "auto"],
sectionId: SECTION_IDS.workflow,
sectionLabel: "Workflow",
},
{
id: SETTING_IDS.autoFix,
label: "Auto Fix Problems",
description: "Automatically fix TypeScript errors",
keywords: ["fix", "typescript", "errors", "automatic", "problems", "auto"],
sectionId: SECTION_IDS.workflow,
sectionLabel: "Workflow",
},
{
id: SETTING_IDS.autoExpandPreview,
label: "Auto Expand Preview",
description:
"Automatically expand the preview panel when code changes are made",
keywords: ["preview", "expand", "panel", "automatic", "auto"],
sectionId: SECTION_IDS.workflow,
sectionLabel: "Workflow",
},
{
id: SETTING_IDS.chatCompletionNotification,
label: "Chat Completion Notification",
description:
"Show a native notification when a chat response completes while the app is not focused",
keywords: ["notification", "chat", "complete", "alert", "background"],
sectionId: SECTION_IDS.workflow,
sectionLabel: "Workflow",
},
// AI Settings
{
id: SETTING_IDS.thinkingBudget,
label: "Thinking Budget",
description: "Set the AI thinking token budget",
keywords: ["thinking", "tokens", "budget", "reasoning", "ai"],
sectionId: SECTION_IDS.ai,
sectionLabel: "AI",
},
{
id: SETTING_IDS.maxChatTurns,
label: "Max Chat Turns",
description: "Set the maximum number of conversation turns",
keywords: ["turns", "max", "conversation", "limit", "chat"],
sectionId: SECTION_IDS.ai,
sectionLabel: "AI",
},
// Provider Settings
{
id: SECTION_IDS.providers,
label: "Model Providers",
description: "Configure AI model providers and API keys",
keywords: [
"provider",
"model",
"api key",
"openai",
"anthropic",
"claude",
"gpt",
"gemini",
"llm",
],
sectionId: SECTION_IDS.providers,
sectionLabel: "Model Providers",
},
// Telemetry
{
id: SETTING_IDS.telemetry,
label: "Telemetry",
description: "Enable or disable anonymous usage data collection",
keywords: [
"telemetry",
"analytics",
"usage",
"data",
"privacy",
"tracking",
],
sectionId: SECTION_IDS.telemetry,
sectionLabel: "Telemetry",
},
// Integrations
{
id: SETTING_IDS.github,
label: "GitHub Integration",
description: "Connect your GitHub account",
keywords: ["github", "git", "integration", "connect", "account"],
sectionId: SECTION_IDS.integrations,
sectionLabel: "Integrations",
},
{
id: SETTING_IDS.vercel,
label: "Vercel Integration",
description: "Connect your Vercel account for deployments",
keywords: ["vercel", "deploy", "integration", "hosting", "connect"],
sectionId: SECTION_IDS.integrations,
sectionLabel: "Integrations",
},
{
id: SETTING_IDS.supabase,
label: "Supabase Integration",
description: "Connect your Supabase project",
keywords: [
"supabase",
"database",
"integration",
"backend",
"connect",
"postgres",
],
sectionId: SECTION_IDS.integrations,
sectionLabel: "Integrations",
},
{
id: SETTING_IDS.neon,
label: "Neon Integration",
description: "Connect your Neon database",
keywords: [
"neon",
"database",
"integration",
"postgres",
"connect",
"serverless",
],
sectionId: SECTION_IDS.integrations,
sectionLabel: "Integrations",
},
// Agent Permissions
{
id: SECTION_IDS.agentPermissions,
label: "Agent Permissions",
description: "Configure permissions for agent built-in tools",
keywords: [
"agent",
"permissions",
"tools",
"approve",
"allow",
"consent",
"pro",
],
sectionId: SECTION_IDS.agentPermissions,
sectionLabel: "Agent Permissions",
},
// Tools (MCP)
{
id: SECTION_IDS.toolsMcp,
label: "Tools (MCP)",
description: "Configure MCP servers and environment variables",
keywords: [
"mcp",
"tools",
"server",
"model context protocol",
"environment",
],
sectionId: SECTION_IDS.toolsMcp,
sectionLabel: "Tools (MCP)",
},
// Experiments
{
id: SETTING_IDS.nativeGit,
label: "Enable Native Git",
description:
"Use native Git for faster performance without external installation",
keywords: ["git", "native", "experiment", "beta", "performance"],
sectionId: SECTION_IDS.experiments,
sectionLabel: "Experiments",
},
// Danger Zone
{
id: SETTING_IDS.reset,
label: "Reset Everything",
description:
"Delete all apps, chats, and settings. This action cannot be undone.",
keywords: ["reset", "delete", "clear", "wipe", "danger", "destructive"],
sectionId: SECTION_IDS.dangerZone,
sectionLabel: "Danger Zone",
},
];
...@@ -33,6 +33,7 @@ import { ZoomSelector } from "@/components/ZoomSelector"; ...@@ -33,6 +33,7 @@ import { ZoomSelector } from "@/components/ZoomSelector";
import { DefaultChatModeSelector } from "@/components/DefaultChatModeSelector"; import { DefaultChatModeSelector } from "@/components/DefaultChatModeSelector";
import { useSetAtom } from "jotai"; import { useSetAtom } from "jotai";
import { activeSettingsSectionAtom } from "@/atoms/viewAtoms"; import { activeSettingsSectionAtom } from "@/atoms/viewAtoms";
import { SECTION_IDS, SETTING_IDS } from "@/lib/settingsSearchIndex";
export default function SettingsPage() { export default function SettingsPage() {
const [isResetDialogOpen, setIsResetDialogOpen] = useState(false); const [isResetDialogOpen, setIsResetDialogOpen] = useState(false);
...@@ -43,7 +44,7 @@ export default function SettingsPage() { ...@@ -43,7 +44,7 @@ export default function SettingsPage() {
const setActiveSettingsSection = useSetAtom(activeSettingsSectionAtom); const setActiveSettingsSection = useSetAtom(activeSettingsSectionAtom);
useEffect(() => { useEffect(() => {
setActiveSettingsSection("general-settings"); setActiveSettingsSection(SECTION_IDS.general);
}, [setActiveSettingsSection]); }, [setActiveSettingsSection]);
const handleResetEverything = async () => { const handleResetEverything = async () => {
...@@ -86,7 +87,7 @@ export default function SettingsPage() { ...@@ -86,7 +87,7 @@ export default function SettingsPage() {
<AISettings /> <AISettings />
<div <div
id="provider-settings" id={SECTION_IDS.providers}
className="bg-white dark:bg-gray-800 rounded-xl shadow-sm" className="bg-white dark:bg-gray-800 rounded-xl shadow-sm"
> >
<ProviderSettingsGrid /> <ProviderSettingsGrid />
...@@ -94,13 +95,13 @@ export default function SettingsPage() { ...@@ -94,13 +95,13 @@ export default function SettingsPage() {
<div className="space-y-6"> <div className="space-y-6">
<div <div
id="telemetry" id={SECTION_IDS.telemetry}
className="bg-white dark:bg-gray-800 rounded-xl shadow-sm p-6" className="bg-white dark:bg-gray-800 rounded-xl shadow-sm p-6"
> >
<h2 className="text-lg font-medium text-gray-900 dark:text-white mb-4"> <h2 className="text-lg font-medium text-gray-900 dark:text-white mb-4">
Telemetry Telemetry
</h2> </h2>
<div className="space-y-2"> <div id={SETTING_IDS.telemetry} className="space-y-2">
<TelemetrySwitch /> <TelemetrySwitch />
<div className="text-sm text-gray-500 dark:text-gray-400"> <div className="text-sm text-gray-500 dark:text-gray-400">
This records anonymous usage data to improve the product. This records anonymous usage data to improve the product.
...@@ -118,24 +119,32 @@ export default function SettingsPage() { ...@@ -118,24 +119,32 @@ export default function SettingsPage() {
{/* Integrations Section */} {/* Integrations Section */}
<div <div
id="integrations" id={SECTION_IDS.integrations}
className="bg-white dark:bg-gray-800 rounded-xl shadow-sm p-6" className="bg-white dark:bg-gray-800 rounded-xl shadow-sm p-6"
> >
<h2 className="text-lg font-medium text-gray-900 dark:text-white mb-4"> <h2 className="text-lg font-medium text-gray-900 dark:text-white mb-4">
Integrations Integrations
</h2> </h2>
<div className="space-y-4"> <div className="space-y-4">
<GitHubIntegration /> <div id={SETTING_IDS.github}>
<VercelIntegration /> <GitHubIntegration />
<SupabaseIntegration /> </div>
<NeonIntegration /> <div id={SETTING_IDS.vercel}>
<VercelIntegration />
</div>
<div id={SETTING_IDS.supabase}>
<SupabaseIntegration />
</div>
<div id={SETTING_IDS.neon}>
<NeonIntegration />
</div>
</div> </div>
</div> </div>
{/* Agent v2 Permissions */} {/* Agent v2 Permissions */}
<div <div
id="agent-permissions" id={SECTION_IDS.agentPermissions}
className="bg-white dark:bg-gray-800 rounded-xl shadow-sm p-6" className="bg-white dark:bg-gray-800 rounded-xl shadow-sm p-6"
> >
<h2 className="text-lg font-medium text-gray-900 dark:text-white mb-4"> <h2 className="text-lg font-medium text-gray-900 dark:text-white mb-4">
...@@ -146,7 +155,7 @@ export default function SettingsPage() { ...@@ -146,7 +155,7 @@ export default function SettingsPage() {
{/* Tools (MCP) */} {/* Tools (MCP) */}
<div <div
id="tools-mcp" id={SECTION_IDS.toolsMcp}
className="bg-white dark:bg-gray-800 rounded-xl shadow-sm p-6" className="bg-white dark:bg-gray-800 rounded-xl shadow-sm p-6"
> >
<h2 className="text-lg font-medium text-gray-900 dark:text-white mb-4"> <h2 className="text-lg font-medium text-gray-900 dark:text-white mb-4">
...@@ -157,14 +166,14 @@ export default function SettingsPage() { ...@@ -157,14 +166,14 @@ export default function SettingsPage() {
{/* Experiments Section */} {/* Experiments Section */}
<div <div
id="experiments" id={SECTION_IDS.experiments}
className="bg-white dark:bg-gray-800 rounded-xl shadow-sm p-6" className="bg-white dark:bg-gray-800 rounded-xl shadow-sm p-6"
> >
<h2 className="text-lg font-medium text-gray-900 dark:text-white mb-4"> <h2 className="text-lg font-medium text-gray-900 dark:text-white mb-4">
Experiments Experiments
</h2> </h2>
<div className="space-y-4"> <div className="space-y-4">
<div className="space-y-1 mt-4"> <div id={SETTING_IDS.nativeGit} className="space-y-1 mt-4">
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<Switch <Switch
id="enable-native-git" id="enable-native-git"
...@@ -188,7 +197,7 @@ export default function SettingsPage() { ...@@ -188,7 +197,7 @@ export default function SettingsPage() {
{/* Danger Zone */} {/* Danger Zone */}
<div <div
id="danger-zone" id={SECTION_IDS.dangerZone}
className="bg-white dark:bg-gray-800 rounded-xl shadow-sm p-6 border border-red-200 dark:border-red-800" className="bg-white dark:bg-gray-800 rounded-xl shadow-sm p-6 border border-red-200 dark:border-red-800"
> >
<h2 className="text-lg font-medium text-red-600 dark:text-red-400 mb-4"> <h2 className="text-lg font-medium text-red-600 dark:text-red-400 mb-4">
...@@ -196,7 +205,10 @@ export default function SettingsPage() { ...@@ -196,7 +205,10 @@ export default function SettingsPage() {
</h2> </h2>
<div className="space-y-4"> <div className="space-y-4">
<div className="flex items-start justify-between flex-col sm:flex-row sm:items-center gap-4"> <div
id={SETTING_IDS.reset}
className="flex items-start justify-between flex-col sm:flex-row sm:items-center gap-4"
>
<div> <div>
<h3 className="text-sm font-medium text-gray-900 dark:text-white"> <h3 className="text-sm font-medium text-gray-900 dark:text-white">
Reset Everything Reset Everything
...@@ -237,7 +249,7 @@ export function GeneralSettings({ appVersion }: { appVersion: string | null }) { ...@@ -237,7 +249,7 @@ export function GeneralSettings({ appVersion }: { appVersion: string | null }) {
return ( return (
<div <div
id="general-settings" id={SECTION_IDS.general}
className="bg-white dark:bg-gray-800 rounded-xl shadow-sm p-6" className="bg-white dark:bg-gray-800 rounded-xl shadow-sm p-6"
> >
<h2 className="text-lg font-medium text-gray-900 dark:text-white mb-4"> <h2 className="text-lg font-medium text-gray-900 dark:text-white mb-4">
...@@ -245,7 +257,7 @@ export function GeneralSettings({ appVersion }: { appVersion: string | null }) { ...@@ -245,7 +257,7 @@ export function GeneralSettings({ appVersion }: { appVersion: string | null }) {
</h2> </h2>
<div className="space-y-4 mb-4"> <div className="space-y-4 mb-4">
<div className="flex items-center gap-4"> <div id={SETTING_IDS.theme} className="flex items-center gap-4">
<label className="text-sm font-medium text-gray-700 dark:text-gray-300"> <label className="text-sm font-medium text-gray-700 dark:text-gray-300">
Theme Theme
</label> </label>
...@@ -272,11 +284,11 @@ export function GeneralSettings({ appVersion }: { appVersion: string | null }) { ...@@ -272,11 +284,11 @@ export function GeneralSettings({ appVersion }: { appVersion: string | null }) {
</div> </div>
</div> </div>
<div className="mt-4"> <div id={SETTING_IDS.zoom} className="mt-4">
<ZoomSelector /> <ZoomSelector />
</div> </div>
<div className="space-y-1 mt-4"> <div id={SETTING_IDS.autoUpdate} className="space-y-1 mt-4">
<AutoUpdateSwitch /> <AutoUpdateSwitch />
<div className="text-sm text-gray-500 dark:text-gray-400"> <div className="text-sm text-gray-500 dark:text-gray-400">
This will automatically update the app when new versions are This will automatically update the app when new versions are
...@@ -284,14 +296,14 @@ export function GeneralSettings({ appVersion }: { appVersion: string | null }) { ...@@ -284,14 +296,14 @@ export function GeneralSettings({ appVersion }: { appVersion: string | null }) {
</div> </div>
</div> </div>
<div className="mt-4"> <div id={SETTING_IDS.releaseChannel} className="mt-4">
<ReleaseChannelSelector /> <ReleaseChannelSelector />
</div> </div>
<div className="mt-4"> <div id={SETTING_IDS.runtimeMode} className="mt-4">
<RuntimeModeSelector /> <RuntimeModeSelector />
</div> </div>
<div className="mt-4"> <div id={SETTING_IDS.nodePath} className="mt-4">
<NodePathSelector /> <NodePathSelector />
</div> </div>
...@@ -308,39 +320,42 @@ export function GeneralSettings({ appVersion }: { appVersion: string | null }) { ...@@ -308,39 +320,42 @@ export function GeneralSettings({ appVersion }: { appVersion: string | null }) {
export function WorkflowSettings() { export function WorkflowSettings() {
return ( return (
<div <div
id="workflow-settings" id={SECTION_IDS.workflow}
className="bg-white dark:bg-gray-800 rounded-xl shadow-sm p-6" className="bg-white dark:bg-gray-800 rounded-xl shadow-sm p-6"
> >
<h2 className="text-lg font-medium text-gray-900 dark:text-white mb-4"> <h2 className="text-lg font-medium text-gray-900 dark:text-white mb-4">
Workflow Settings Workflow Settings
</h2> </h2>
<div className="mt-4"> <div id={SETTING_IDS.defaultChatMode} className="mt-4">
<DefaultChatModeSelector /> <DefaultChatModeSelector />
</div> </div>
<div className="space-y-1 mt-4"> <div id={SETTING_IDS.autoApprove} className="space-y-1 mt-4">
<AutoApproveSwitch showToast={false} /> <AutoApproveSwitch showToast={false} />
<div className="text-sm text-gray-500 dark:text-gray-400"> <div className="text-sm text-gray-500 dark:text-gray-400">
This will automatically approve code changes and run them. This will automatically approve code changes and run them.
</div> </div>
</div> </div>
<div className="space-y-1 mt-4"> <div id={SETTING_IDS.autoFix} className="space-y-1 mt-4">
<AutoFixProblemsSwitch /> <AutoFixProblemsSwitch />
<div className="text-sm text-gray-500 dark:text-gray-400"> <div className="text-sm text-gray-500 dark:text-gray-400">
This will automatically fix TypeScript errors. This will automatically fix TypeScript errors.
</div> </div>
</div> </div>
<div className="space-y-1 mt-4"> <div id={SETTING_IDS.autoExpandPreview} className="space-y-1 mt-4">
<AutoExpandPreviewSwitch /> <AutoExpandPreviewSwitch />
<div className="text-sm text-gray-500 dark:text-gray-400"> <div className="text-sm text-gray-500 dark:text-gray-400">
Automatically expand the preview panel when code changes are made. Automatically expand the preview panel when code changes are made.
</div> </div>
</div> </div>
<div className="space-y-1 mt-4"> <div
id={SETTING_IDS.chatCompletionNotification}
className="space-y-1 mt-4"
>
<ChatCompletionNotificationSwitch /> <ChatCompletionNotificationSwitch />
<div className="text-sm text-gray-500 dark:text-gray-400"> <div className="text-sm text-gray-500 dark:text-gray-400">
Show a native notification when a chat response completes while the Show a native notification when a chat response completes while the
...@@ -353,18 +368,18 @@ export function WorkflowSettings() { ...@@ -353,18 +368,18 @@ export function WorkflowSettings() {
export function AISettings() { export function AISettings() {
return ( return (
<div <div
id="ai-settings" id={SECTION_IDS.ai}
className="bg-white dark:bg-gray-800 rounded-xl shadow-sm p-6" className="bg-white dark:bg-gray-800 rounded-xl shadow-sm p-6"
> >
<h2 className="text-lg font-medium text-gray-900 dark:text-white mb-4"> <h2 className="text-lg font-medium text-gray-900 dark:text-white mb-4">
AI Settings AI Settings
</h2> </h2>
<div className="mt-4"> <div id={SETTING_IDS.thinkingBudget} className="mt-4">
<ThinkingBudgetSelector /> <ThinkingBudgetSelector />
</div> </div>
<div className="mt-4"> <div id={SETTING_IDS.maxChatTurns} className="mt-4">
<MaxChatTurnsSelector /> <MaxChatTurnsSelector />
</div> </div>
</div> </div>
......
...@@ -317,6 +317,33 @@ body[data-scroll-locked] .app-region-drag { ...@@ -317,6 +317,33 @@ body[data-scroll-locked] .app-region-drag {
animation: marquee 2s linear infinite; animation: marquee 2s linear infinite;
} }
@keyframes settings-highlight {
0% {
background-color: oklch(0.85 0.1 290);
}
100% {
background-color: transparent;
}
}
.settings-highlight {
animation: settings-highlight 1.5s ease-out;
border-radius: 0.5rem;
}
.dark .settings-highlight {
animation: settings-highlight-dark 1.5s ease-out;
}
@keyframes settings-highlight-dark {
0% {
background-color: oklch(0.35 0.1 290);
}
100% {
background-color: transparent;
}
}
/* In-between text-xs and text-sm */ /* In-between text-xs and text-sm */
.text-xs-sm { .text-xs-sm {
font-size: 0.82rem; font-size: 0.82rem;
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论