feat: add quick install templates and index status to CLI hooks and home locales

feat: enhance MCP manager with interactive question feature and update locales

feat: implement tags and available models management in settings page

fix: improve process termination logic in stop command for React frontend

fix: update view command to default to 'js' frontend

feat: add Recommended MCP Wizard component for dynamic server configuration
This commit is contained in:
catlog22
2026-02-04 15:24:34 +08:00
parent 341331325c
commit 8454ae4f41
24 changed files with 1186 additions and 727 deletions

View File

@@ -17,11 +17,7 @@ import {
ChevronDown,
ChevronUp,
Languages,
GitFork,
Scale,
Search,
Power,
PowerOff,
Plus,
} from 'lucide-react';
import { Card } from '@/components/ui/Card';
import { Button } from '@/components/ui/Button';
@@ -29,12 +25,10 @@ import { Input } from '@/components/ui/Input';
import { Badge } from '@/components/ui/Badge';
import { ThemeSelector } from '@/components/shared/ThemeSelector';
import { useTheme } from '@/hooks';
import { useHooks, useRules, useToggleHook, useToggleRule } from '@/hooks';
import { useConfigStore, selectCliTools, selectDefaultCliTool, selectUserPreferences } from '@/stores/configStore';
import type { CliToolConfig, UserPreferences } from '@/types/store';
import { cn } from '@/lib/utils';
import { LanguageSwitcher } from '@/components/layout/LanguageSwitcher';
import { IndexManager } from '@/components/shared/IndexManager';
// ========== CLI Tool Card Component ==========
@@ -47,6 +41,9 @@ interface CliToolCardProps {
onToggleEnabled: () => void;
onSetDefault: () => void;
onUpdateModel: (field: 'primaryModel' | 'secondaryModel', value: string) => void;
onUpdateTags: (tags: string[]) => void;
onUpdateAvailableModels: (models: string[]) => void;
onUpdateSettingsFile: (settingsFile: string | undefined) => void;
}
function CliToolCard({
@@ -58,9 +55,49 @@ function CliToolCard({
onToggleEnabled,
onSetDefault,
onUpdateModel,
onUpdateTags,
onUpdateAvailableModels,
onUpdateSettingsFile,
}: CliToolCardProps) {
const { formatMessage } = useIntl();
// Local state for tag and model input
const [tagInput, setTagInput] = useState('');
const [modelInput, setModelInput] = useState('');
// Handler for adding tags
const handleAddTag = () => {
const newTag = tagInput.trim();
if (newTag && !config.tags.includes(newTag)) {
onUpdateTags([...config.tags, newTag]);
setTagInput('');
}
};
// Handler for removing tags
const handleRemoveTag = (tagToRemove: string) => {
onUpdateTags(config.tags.filter((t) => t !== tagToRemove));
};
// Handler for adding available models
const handleAddModel = () => {
const newModel = modelInput.trim();
const currentModels = config.availableModels || [];
if (newModel && !currentModels.includes(newModel)) {
onUpdateAvailableModels([...currentModels, newModel]);
setModelInput('');
}
};
// Handler for removing available models
const handleRemoveModel = (modelToRemove: string) => {
const currentModels = config.availableModels || [];
onUpdateAvailableModels(currentModels.filter((m) => m !== modelToRemove));
};
// Predefined tags
const predefinedTags = ['分析', 'Debug', 'implementation', 'refactoring', 'testing'];
return (
<Card className={cn('overflow-hidden', !config.enabled && 'opacity-60')}>
{/* Header */}
@@ -157,6 +194,146 @@ function CliToolCard({
/>
</div>
</div>
{/* Tags Section */}
<div className="space-y-2">
<label className="text-sm font-medium text-foreground">
{formatMessage({ id: 'apiSettings.cliSettings.tags' })}
</label>
<p className="text-xs text-muted-foreground">
{formatMessage({ id: 'apiSettings.cliSettings.tagsDescription' })}
</p>
<div className="flex gap-2">
<div className="flex-1 flex flex-wrap gap-1.5 p-2 border border-input bg-background rounded-md min-h-[38px] focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2">
{config.tags.map((tag) => (
<span
key={tag}
className="inline-flex items-center gap-1 px-2 py-0.5 bg-primary/10 text-primary rounded text-xs h-6"
>
{tag}
<button
type="button"
onClick={() => handleRemoveTag(tag)}
className="hover:text-destructive transition-colors"
aria-label={formatMessage({ id: 'apiSettings.cliSettings.removeTag' })}
>
<X className="w-3 h-3" />
</button>
</span>
))}
<input
type="text"
value={tagInput}
onChange={(e) => setTagInput(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
handleAddTag();
}
}}
placeholder={config.tags.length === 0 ? formatMessage({ id: 'apiSettings.cliSettings.tagInputPlaceholder' }) : ''}
className="flex-1 min-w-[120px] bg-transparent border-0 outline-none text-sm placeholder:text-muted-foreground"
/>
</div>
<Button
type="button"
size="sm"
onClick={handleAddTag}
variant="outline"
className="shrink-0"
>
<Plus className="w-4 h-4" />
</Button>
</div>
{/* Predefined Tags */}
<div className="flex flex-wrap gap-1">
<span className="text-xs text-muted-foreground">
{formatMessage({ id: 'apiSettings.cliSettings.predefinedTags' })}:
</span>
{predefinedTags.map((predefinedTag) => (
<button
key={predefinedTag}
type="button"
onClick={() => {
if (!config.tags.includes(predefinedTag)) {
onUpdateTags([...config.tags, predefinedTag]);
}
}}
disabled={config.tags.includes(predefinedTag)}
className="text-xs px-2 py-0.5 rounded border border-border hover:bg-muted disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
{predefinedTag}
</button>
))}
</div>
</div>
{/* Available Models Section */}
<div className="space-y-2">
<label className="text-sm font-medium text-foreground">
{formatMessage({ id: 'apiSettings.cliSettings.availableModels' })}
</label>
<div className="flex gap-2">
<div className="flex-1 flex flex-wrap gap-1.5 p-2 border border-input bg-background rounded-md min-h-[38px] focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2">
{(config.availableModels || []).map((model) => (
<span
key={model}
className="inline-flex items-center gap-1 px-2 py-0.5 bg-primary/10 text-primary rounded text-xs h-6"
>
{model}
<button
type="button"
onClick={() => handleRemoveModel(model)}
className="hover:text-destructive transition-colors"
>
<X className="w-3 h-3" />
</button>
</span>
))}
<input
type="text"
value={modelInput}
onChange={(e) => setModelInput(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
handleAddModel();
}
}}
placeholder={(config.availableModels || []).length === 0 ? formatMessage({ id: 'apiSettings.cliSettings.availableModelsPlaceholder' }) : ''}
className="flex-1 min-w-[120px] bg-transparent border-0 outline-none text-sm placeholder:text-muted-foreground"
/>
</div>
<Button
type="button"
size="sm"
onClick={handleAddModel}
variant="outline"
className="shrink-0"
>
<Plus className="w-4 h-4" />
</Button>
</div>
<p className="text-xs text-muted-foreground">
{formatMessage({ id: 'apiSettings.cliSettings.availableModelsHint' })}
</p>
</div>
{/* Settings File */}
<div className="space-y-2">
<label className="text-sm font-medium text-foreground">
{formatMessage({ id: 'apiSettings.cliSettings.settingsFile' })}
</label>
<Input
value={config.settingsFile || ''}
onChange={(e) => onUpdateSettingsFile(e.target.value || undefined)}
placeholder={formatMessage({ id: 'apiSettings.cliSettings.settingsFilePlaceholder' })}
/>
<p className="text-xs text-muted-foreground">
{formatMessage({ id: 'apiSettings.cliSettings.settingsFileHint' })}
</p>
</div>
{!isDefault && config.enabled && (
<Button variant="outline" size="sm" onClick={onSetDefault}>
{formatMessage({ id: 'settings.cliTools.setDefault' })}
@@ -168,210 +345,6 @@ function CliToolCard({
);
}
// ========== Hooks Section Component ==========
function HooksSection() {
const { formatMessage } = useIntl();
const [searchQuery, setSearchQuery] = useState('');
const { hooks, enabledCount, totalCount, isLoading } = useHooks();
const { toggleHook, isToggling } = useToggleHook();
const filteredHooks = hooks.filter(h =>
h.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
(h.description && h.description.toLowerCase().includes(searchQuery.toLowerCase()))
);
return (
<Card className="p-6">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold text-foreground flex items-center gap-2">
<GitFork className="w-5 h-5" />
{formatMessage({ id: 'settings.sections.hooks' })}
</h2>
<div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground">
{enabledCount}/{totalCount} {formatMessage({ id: 'cliHooks.stats.enabled' })}
</span>
</div>
</div>
<div className="relative mb-4">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
<Input
placeholder={formatMessage({ id: 'cliHooks.filters.searchPlaceholder' })}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-9"
/>
</div>
<div className="space-y-2">
{isLoading ? (
<div className="space-y-2">
{[1, 2, 3].map((i) => (
<div key={i} className="h-16 bg-muted animate-pulse rounded-lg" />
))}
</div>
) : filteredHooks.length === 0 ? (
<div className="text-center py-8 text-muted-foreground">
<GitFork className="w-8 h-8 mx-auto mb-2 opacity-50" />
<p>{formatMessage({ id: 'cliHooks.emptyState.title' })}</p>
</div>
) : (
filteredHooks.map((hook) => (
<div
key={hook.name}
className="flex items-center justify-between p-3 rounded-lg border border-border hover:bg-muted/50 transition-colors"
>
<div className="flex items-center gap-3">
<div className={cn(
'p-2 rounded-lg',
hook.enabled ? 'bg-primary/10' : 'bg-muted'
)}>
<GitFork className={cn(
'w-4 h-4',
hook.enabled ? 'text-primary' : 'text-muted-foreground'
)} />
</div>
<div>
<p className="text-sm font-medium text-foreground">{hook.name}</p>
{hook.description && (
<p className="text-xs text-muted-foreground">{hook.description}</p>
)}
<Badge variant="outline" className="text-xs mt-1">
{formatMessage({ id: `cliHooks.trigger.${hook.trigger}` })}
</Badge>
</div>
</div>
<Button
variant={hook.enabled ? 'default' : 'outline'}
size="sm"
className="h-8"
onClick={() => toggleHook(hook.name, !hook.enabled)}
disabled={isToggling}
>
{hook.enabled ? (
<><Power className="w-4 h-4 mr-1" />{formatMessage({ id: 'settings.cliTools.enabled' })}</>
) : (
<><PowerOff className="w-4 h-4 mr-1" />{formatMessage({ id: 'settings.cliTools.disabled' })}</>
)}
</Button>
</div>
))
)}
</div>
</Card>
);
}
// ========== Rules Section Component ==========
function RulesSection() {
const { formatMessage } = useIntl();
const [searchQuery, setSearchQuery] = useState('');
const { rules, enabledCount, totalCount, isLoading } = useRules();
const { toggleRule, isToggling } = useToggleRule();
const filteredRules = rules.filter(r =>
r.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
(r.description && r.description.toLowerCase().includes(searchQuery.toLowerCase()))
);
return (
<Card className="p-6">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold text-foreground flex items-center gap-2">
<Scale className="w-5 h-5" />
{formatMessage({ id: 'settings.sections.rules' })}
</h2>
<div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground">
{enabledCount}/{totalCount} {formatMessage({ id: 'cliRules.stats.enabled' })}
</span>
</div>
</div>
<div className="relative mb-4">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
<Input
placeholder={formatMessage({ id: 'cliRules.filters.searchPlaceholder' })}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-9"
/>
</div>
<div className="space-y-2">
{isLoading ? (
<div className="space-y-2">
{[1, 2, 3].map((i) => (
<div key={i} className="h-16 bg-muted animate-pulse rounded-lg" />
))}
</div>
) : filteredRules.length === 0 ? (
<div className="text-center py-8 text-muted-foreground">
<Scale className="w-8 h-8 mx-auto mb-2 opacity-50" />
<p>{formatMessage({ id: 'cliRules.emptyState.title' })}</p>
</div>
) : (
filteredRules.map((rule) => (
<div
key={rule.id}
className="flex items-center justify-between p-3 rounded-lg border border-border hover:bg-muted/50 transition-colors"
>
<div className="flex items-center gap-3">
<div className={cn(
'p-2 rounded-lg',
rule.enabled ? 'bg-primary/10' : 'bg-muted'
)}>
<Scale className={cn(
'w-4 h-4',
rule.enabled ? 'text-primary' : 'text-muted-foreground'
)} />
</div>
<div>
<p className="text-sm font-medium text-foreground">{rule.name}</p>
{rule.description && (
<p className="text-xs text-muted-foreground">{rule.description}</p>
)}
<div className="flex items-center gap-2 mt-1">
{rule.category && (
<Badge variant="secondary" className="text-xs">{rule.category}</Badge>
)}
{rule.severity && (
<Badge
variant={rule.severity === 'error' ? 'destructive' : 'outline'}
className="text-xs"
>
{formatMessage({ id: `cliRules.severity.${rule.severity}` })}
</Badge>
)}
</div>
</div>
</div>
<Button
variant={rule.enabled ? 'default' : 'outline'}
size="sm"
className="h-8"
onClick={() => toggleRule(rule.id, !rule.enabled)}
disabled={isToggling}
>
{rule.enabled ? (
<><Power className="w-4 h-4 mr-1" />{formatMessage({ id: 'settings.cliTools.enabled' })}</>
) : (
<><PowerOff className="w-4 h-4 mr-1" />{formatMessage({ id: 'settings.cliTools.disabled' })}</>
)}
</Button>
</div>
))
)}
</div>
</Card>
);
}
// ========== Main Page Component ==========
export function SettingsPage() {
@@ -408,6 +381,18 @@ export function SettingsPage() {
updateCliTool(toolId, { [field]: value });
};
const handleUpdateTags = (toolId: string, tags: string[]) => {
updateCliTool(toolId, { tags });
};
const handleUpdateAvailableModels = (toolId: string, availableModels: string[]) => {
updateCliTool(toolId, { availableModels });
};
const handleUpdateSettingsFile = (toolId: string, settingsFile: string | undefined) => {
updateCliTool(toolId, { settingsFile });
};
const handlePreferenceChange = (key: keyof UserPreferences, value: unknown) => {
setUserPreferences({ [key]: value });
};
@@ -502,6 +487,9 @@ export function SettingsPage() {
onToggleEnabled={() => handleToggleToolEnabled(toolId)}
onSetDefault={() => handleSetDefaultTool(toolId)}
onUpdateModel={(field, value) => handleUpdateModel(toolId, field, value)}
onUpdateTags={(tags) => handleUpdateTags(toolId, tags)}
onUpdateAvailableModels={(models) => handleUpdateAvailableModels(toolId, models)}
onUpdateSettingsFile={(settingsFile) => handleUpdateSettingsFile(toolId, settingsFile)}
/>
))}
</div>
@@ -621,15 +609,6 @@ export function SettingsPage() {
</div>
</Card>
{/* Git Hooks */}
<HooksSection />
{/* Rules */}
<RulesSection />
{/* Index Manager */}
<IndexManager />
{/* Reset Settings */}
<Card className="p-6 border-destructive/50">
<h2 className="text-lg font-semibold text-foreground flex items-center gap-2 mb-4">