mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-03-01 12:13:51 +08:00
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:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user