mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-10 02:24:35 +08:00
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:
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user