feat: enhance RecommendedMcpWizard with icon mapping; improve ExecutionTab accessibility; refine NavGroup path matching; update fetchSkills to include enabled status; add loading and error messages to localization

This commit is contained in:
catlog22
2026-02-04 16:38:18 +08:00
parent ac95ee3161
commit 2bfce150ec
16 changed files with 385 additions and 71 deletions

View File

@@ -7,7 +7,7 @@
import { useState } from 'react';
import { useIntl } from 'react-intl';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { Download, Loader2, X } from 'lucide-react';
import { Download, Loader2, Search, Globe, Sparkles, Settings } from 'lucide-react';
import {
Dialog,
DialogContent,
@@ -18,7 +18,6 @@ import {
import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input';
import { Label } from '@/components/ui/Label';
import { Badge } from '@/components/ui/Badge';
import {
addGlobalMcpServer,
copyMcpServerToProject,
@@ -27,6 +26,14 @@ import { mcpServersKeys } from '@/hooks';
import { useNotifications } from '@/hooks/useNotifications';
import { cn } from '@/lib/utils';
// Icon map for MCP definitions
const ICON_MAP: Record<string, React.ComponentType<{ className?: string }>> = {
'search-code': Search,
'chrome': Globe,
'globe-2': Sparkles,
'code-2': Settings,
};
// ========== Types ==========
/**
@@ -207,6 +214,7 @@ export function RecommendedMcpWizard({
if (!mcpDefinition) return null;
const hasFields = mcpDefinition.fields.length > 0;
const Icon = ICON_MAP[mcpDefinition.icon] || Settings;
return (
<Dialog open={open} onOpenChange={handleOpenChange}>
@@ -215,7 +223,7 @@ export function RecommendedMcpWizard({
<DialogHeader>
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-primary/10 rounded-lg flex items-center justify-center">
<i className={cn('lucide w-5 h-5 text-primary', `lucide-${mcpDefinition.icon}`)} />
<Icon className="w-5 h-5 text-primary" />
</div>
<div>
<DialogTitle>

View File

@@ -3,7 +3,7 @@
// ========================================
// Redesigned CLI streaming monitor with smart parsing and message-based layout
import { useEffect, useState, useCallback, useMemo } from 'react';
import { useEffect, useState, useCallback, useMemo, useRef } from 'react';
import { useIntl } from 'react-intl';
import {
Terminal,
@@ -220,9 +220,14 @@ export function CliStreamMonitorNew({ isOpen, onClose }: CliStreamMonitorNewProp
// WebSocket last message
const lastMessage = useNotificationStore(selectWsLastMessage);
// Track last processed WebSocket message to prevent duplicate processing
const lastProcessedMsgRef = useRef<unknown>(null);
// Handle WebSocket messages (same as original)
useEffect(() => {
if (!lastMessage) return;
// Skip if no message or same message already processed (prevents React strict mode double-execution)
if (!lastMessage || lastMessage === lastProcessedMsgRef.current) return;
lastProcessedMsgRef.current = lastMessage;
const { type, payload } = lastMessage;

View File

@@ -58,13 +58,21 @@ export function ExecutionTab({ execution, isActive, onClick, onClose }: Executio
</span>
{/* Close button - show on hover */}
<button
<span
onClick={onClose}
className="ml-0.5 p-0.5 rounded hover:bg-rose-500/20 transition-opacity opacity-0 group-hover:opacity-100"
className="ml-0.5 p-0.5 rounded hover:bg-rose-500/20 transition-opacity opacity-0 group-hover:opacity-100 cursor-pointer"
aria-label="Close execution tab"
role="button"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
onClose(e as any);
}
}}
>
<X className="h-2.5 w-2.5 text-rose-600 dark:text-rose-400" />
</button>
</span>
</TabsTrigger>
);
}

View File

@@ -14,7 +14,9 @@ import {
Search,
ArrowDownToLine,
Trash2,
ExternalLink,
} from 'lucide-react';
import { useNavigate } from 'react-router-dom';
import { cn } from '@/lib/utils';
import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input';
@@ -196,6 +198,7 @@ export interface CliStreamMonitorProps {
export function CliStreamMonitor({ isOpen, onClose }: CliStreamMonitorProps) {
const { formatMessage } = useIntl();
const navigate = useNavigate();
const logsEndRef = useRef<HTMLDivElement>(null);
const logsContainerRef = useRef<HTMLDivElement>(null);
const [searchQuery, setSearchQuery] = useState('');
@@ -206,6 +209,9 @@ export function CliStreamMonitor({ isOpen, onClose }: CliStreamMonitorProps) {
// Track last output length to detect new output
const lastOutputLengthRef = useRef<Record<string, number>>({});
// Track last processed WebSocket message to prevent duplicate processing
const lastProcessedMsgRef = useRef<unknown>(null);
// Store state
const executions = useCliStreamStore((state) => state.executions);
const currentExecutionId = useCliStreamStore((state) => state.currentExecutionId);
@@ -222,7 +228,9 @@ export function CliStreamMonitor({ isOpen, onClose }: CliStreamMonitorProps) {
// Handle WebSocket messages for CLI stream
useEffect(() => {
if (!lastMessage) return;
// Skip if no message or same message already processed (prevents React strict mode double-execution)
if (!lastMessage || lastMessage === lastProcessedMsgRef.current) return;
lastProcessedMsgRef.current = lastMessage;
const { type, payload } = lastMessage;
@@ -377,6 +385,15 @@ export function CliStreamMonitor({ isOpen, onClose }: CliStreamMonitorProps) {
setCurrentExecution(null);
}, [markExecutionClosedByUser, removeExecution, executions, setCurrentExecution]);
// Open in full page viewer
const handlePopOut = useCallback(() => {
const url = currentExecutionId
? `/cli-viewer?executionId=${currentExecutionId}`
: '/cli-viewer';
navigate(url);
onClose();
}, [currentExecutionId, navigate, onClose]);
// ESC key to close
useEffect(() => {
const handleEsc = (e: KeyboardEvent) => {
@@ -507,6 +524,14 @@ export function CliStreamMonitor({ isOpen, onClose }: CliStreamMonitorProps) {
<Trash2 className="h-4 w-4" />
</Button>
)}
<Button
variant="ghost"
size="icon"
onClick={handlePopOut}
title="Open in full page viewer"
>
<ExternalLink className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
@@ -636,17 +661,6 @@ export function CliStreamMonitor({ isOpen, onClose }: CliStreamMonitorProps) {
<div ref={logsEndRef} />
</div>
)}
{isUserScrolling && filteredOutput.length > 0 && (
<Button
size="sm"
variant="secondary"
className="absolute bottom-4 right-4"
onClick={scrollToBottom}
>
<ArrowDownToLine className="h-4 w-4 mr-1" />
Scroll to bottom
</Button>
)}
</div>
)}
</div>

View File

@@ -54,9 +54,10 @@ export function NavGroup({
{items.map((item) => {
const ItemIcon = item.icon;
const [basePath] = item.path.split('?');
// More precise matching: exact match or basePath followed by '/' to avoid parent/child conflicts
const isActive =
location.pathname === basePath ||
(basePath !== '/' && location.pathname.startsWith(basePath));
(basePath !== '/' && location.pathname.startsWith(basePath + '/'));
return (
<NavLink
@@ -93,9 +94,10 @@ export function NavGroup({
{items.map((item) => {
const ItemIcon = item.icon;
const [basePath, searchParams] = item.path.split('?');
// More precise matching: exact match or basePath followed by '/' to avoid parent/child conflicts
const isActive =
location.pathname === basePath ||
(basePath !== '/' && location.pathname.startsWith(basePath));
(basePath !== '/' && location.pathname.startsWith(basePath + '/'));
const isQueryParamActive =
searchParams && location.search.includes(searchParams);