mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-28 09:23:08 +08:00
refactor(req-plan): streamline codebase exploration and decomposition guidelines
This commit is contained in:
@@ -14,6 +14,7 @@ import type { Locale } from './lib/i18n';
|
||||
import { useWorkflowStore } from '@/stores/workflowStore';
|
||||
import { useActiveCliExecutions } from '@/hooks/useActiveCliExecutions';
|
||||
import { DialogStyleProvider } from '@/contexts/DialogStyleContext';
|
||||
import { initializeCsrfToken } from './lib/api';
|
||||
|
||||
interface AppProps {
|
||||
locale: Locale;
|
||||
@@ -25,6 +26,11 @@ interface AppProps {
|
||||
* Provides routing and global providers
|
||||
*/
|
||||
function App({ locale, messages }: AppProps) {
|
||||
// Initialize CSRF token on app mount
|
||||
useEffect(() => {
|
||||
initializeCsrfToken().catch(console.error);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<IntlProvider locale={locale} messages={messages}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
Settings,
|
||||
Power,
|
||||
PowerOff,
|
||||
Tag,
|
||||
User,
|
||||
} from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
@@ -140,64 +139,93 @@ export function SkillCard({
|
||||
<Card
|
||||
onClick={handleClick}
|
||||
className={cn(
|
||||
'p-4 cursor-pointer hover:shadow-md transition-all hover-glow',
|
||||
skill.enabled ? 'hover:border-primary/50' : 'border-dashed border-muted-foreground/50 bg-muted/30 grayscale-[0.3]',
|
||||
'p-4 cursor-pointer transition-all hover-glow',
|
||||
skill.enabled
|
||||
? 'border-primary/20 bg-primary/[0.02] hover:border-primary/40 hover:shadow-md'
|
||||
: 'border-border/50 hover:border-border hover:shadow-sm',
|
||||
className
|
||||
)}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex items-start gap-3 min-w-0">
|
||||
{/* Header - Icon, Title, Version on left; Source Badge, Enable Button, Actions Menu on right */}
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2 min-w-0 flex-1">
|
||||
{/* Icon */}
|
||||
<div className={cn(
|
||||
'p-2 rounded-lg flex-shrink-0',
|
||||
skill.enabled ? 'bg-primary/10' : 'bg-muted'
|
||||
)}>
|
||||
<Sparkles className={cn('w-5 h-5', skill.enabled ? 'text-primary' : 'text-muted-foreground')} />
|
||||
</div>
|
||||
|
||||
{/* Title and Version */}
|
||||
<div className="min-w-0">
|
||||
<h3 className="text-sm font-medium text-foreground">{skill.name}</h3>
|
||||
<h3 className="text-sm font-medium text-foreground truncate">{skill.name}</h3>
|
||||
{skill.version && (
|
||||
<p className="text-xs text-muted-foreground">v{skill.version}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{showActions && (
|
||||
<DropdownMenu open={isMenuOpen} onOpenChange={setIsMenuOpen}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-8 w-8 p-0"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<MoreVertical className="w-4 h-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => onClick?.(skill)}>
|
||||
<Info className="w-4 h-4 mr-2" />
|
||||
{formatMessage({ id: 'skills.actions.viewDetails' })}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={handleConfigure}>
|
||||
<Settings className="w-4 h-4 mr-2" />
|
||||
{formatMessage({ id: 'skills.actions.configure' })}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={handleToggle}>
|
||||
{skill.enabled ? (
|
||||
<>
|
||||
<PowerOff className="w-4 h-4 mr-2" />
|
||||
{formatMessage({ id: 'skills.actions.disable' })}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Power className="w-4 h-4 mr-2" />
|
||||
{formatMessage({ id: 'skills.actions.enable' })}
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
|
||||
{/* Right side: Source Badge, Enable Icon Button, Actions Menu */}
|
||||
<div className="flex items-center gap-2 flex-shrink-0">
|
||||
<SourceBadge source={skill.source} />
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className={cn(
|
||||
"h-8 w-8 p-0",
|
||||
skill.enabled
|
||||
? "bg-primary hover:bg-primary/90"
|
||||
: "hover:bg-muted"
|
||||
)}
|
||||
onClick={handleToggle}
|
||||
disabled={isToggling}
|
||||
title={skill.enabled ? formatMessage({ id: 'skills.state.enabled' }) : formatMessage({ id: 'skills.state.disabled' })}
|
||||
>
|
||||
{skill.enabled ? (
|
||||
<Power className="w-4 h-4 text-white" />
|
||||
) : (
|
||||
<PowerOff className="w-4 h-4 text-foreground" />
|
||||
)}
|
||||
</Button>
|
||||
{showActions && (
|
||||
<DropdownMenu open={isMenuOpen} onOpenChange={setIsMenuOpen}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-8 w-8 p-0 flex-shrink-0"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<MoreVertical className="w-4 h-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => onClick?.(skill)}>
|
||||
<Info className="w-4 h-4 mr-2" />
|
||||
{formatMessage({ id: 'skills.actions.viewDetails' })}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={handleConfigure}>
|
||||
<Settings className="w-4 h-4 mr-2" />
|
||||
{formatMessage({ id: 'skills.actions.configure' })}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={handleToggle}>
|
||||
{skill.enabled ? (
|
||||
<>
|
||||
<PowerOff className="w-4 h-4 mr-2" />
|
||||
{formatMessage({ id: 'skills.actions.disable' })}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Power className="w-4 h-4 mr-2" />
|
||||
{formatMessage({ id: 'skills.actions.enable' })}
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
@@ -205,65 +233,35 @@ export function SkillCard({
|
||||
{skill.description}
|
||||
</p>
|
||||
|
||||
{/* Triggers */}
|
||||
{skill.triggers && skill.triggers.length > 0 && (
|
||||
<div className="mt-3">
|
||||
<div className="flex items-center gap-1 text-xs text-muted-foreground mb-1">
|
||||
<Tag className="w-3 h-3" />
|
||||
{formatMessage({ id: 'skills.card.triggers' })}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{skill.triggers.slice(0, 4).map((trigger) => (
|
||||
{/* Footer - Tags, Category, Author */}
|
||||
<div className="flex items-center gap-2 mt-4 pt-3 border-t border-border flex-wrap">
|
||||
{/* Tags (first 2 triggers) */}
|
||||
{skill.triggers && skill.triggers.length > 0 && (
|
||||
<>
|
||||
{skill.triggers.slice(0, 2).map((trigger) => (
|
||||
<Badge key={trigger} variant="outline" className="text-xs">
|
||||
{trigger}
|
||||
</Badge>
|
||||
))}
|
||||
{skill.triggers.length > 4 && (
|
||||
{skill.triggers.length > 2 && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
+{skill.triggers.length - 4}
|
||||
+{skill.triggers.length - 2}
|
||||
</Badge>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{skill.category && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{skill.category}
|
||||
</Badge>
|
||||
)}
|
||||
{skill.author && (
|
||||
<div className="flex items-center gap-1 text-xs text-muted-foreground">
|
||||
<User className="w-3 h-3" />
|
||||
{skill.author}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex items-center justify-between mt-4 pt-3 border-t border-border">
|
||||
<div className="flex items-center gap-2">
|
||||
<SourceBadge source={skill.source} />
|
||||
{skill.category && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{skill.category}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
variant={skill.enabled ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={handleToggle}
|
||||
disabled={isToggling}
|
||||
>
|
||||
{skill.enabled ? (
|
||||
<>
|
||||
<Power className="w-4 h-4 mr-1" />
|
||||
{formatMessage({ id: 'skills.state.enabled' })}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<PowerOff className="w-4 h-4 mr-1" />
|
||||
{formatMessage({ id: 'skills.state.disabled' })}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Author */}
|
||||
{skill.author && (
|
||||
<div className="flex items-center gap-1 mt-2 text-xs text-muted-foreground">
|
||||
<User className="w-3 h-3" />
|
||||
{skill.author}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -104,11 +104,42 @@ export interface ApiError {
|
||||
// ========== CSRF Token Handling ==========
|
||||
|
||||
/**
|
||||
* Get CSRF token from cookie
|
||||
* In-memory CSRF token storage
|
||||
* The token is obtained from X-CSRF-Token response header and stored here
|
||||
* because the XSRF-TOKEN cookie is HttpOnly and cannot be read by JavaScript
|
||||
*/
|
||||
let csrfToken: string | null = null;
|
||||
|
||||
/**
|
||||
* Get CSRF token from memory
|
||||
*/
|
||||
function getCsrfToken(): string | null {
|
||||
const match = document.cookie.match(/XSRF-TOKEN=([^;]+)/);
|
||||
return match ? decodeURIComponent(match[1]) : null;
|
||||
return csrfToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set CSRF token from response header
|
||||
*/
|
||||
function updateCsrfToken(response: Response): void {
|
||||
const token = response.headers.get('X-CSRF-Token');
|
||||
if (token) {
|
||||
csrfToken = token;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize CSRF token by fetching from server
|
||||
* Should be called once on app initialization
|
||||
*/
|
||||
export async function initializeCsrfToken(): Promise<void> {
|
||||
try {
|
||||
const response = await fetch('/api/csrf-token', {
|
||||
credentials: 'same-origin',
|
||||
});
|
||||
updateCsrfToken(response);
|
||||
} catch (error) {
|
||||
console.error('[CSRF] Failed to initialize CSRF token:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Base Fetch Wrapper ==========
|
||||
@@ -124,9 +155,9 @@ async function fetchApi<T>(
|
||||
|
||||
// Add CSRF token for mutating requests
|
||||
if (options.method && ['POST', 'PUT', 'PATCH', 'DELETE'].includes(options.method)) {
|
||||
const csrfToken = getCsrfToken();
|
||||
if (csrfToken) {
|
||||
headers.set('X-CSRF-Token', csrfToken);
|
||||
const token = getCsrfToken();
|
||||
if (token) {
|
||||
headers.set('X-CSRF-Token', token);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,6 +172,9 @@ async function fetchApi<T>(
|
||||
credentials: 'same-origin',
|
||||
});
|
||||
|
||||
// Update CSRF token from response header
|
||||
updateCsrfToken(response);
|
||||
|
||||
if (!response.ok) {
|
||||
const error: ApiError = {
|
||||
message: response.statusText || 'Request failed',
|
||||
|
||||
@@ -429,7 +429,6 @@ export function SkillsManagerPage() {
|
||||
value: 'hub',
|
||||
label: formatMessage({ id: 'skills.location.hub' }),
|
||||
icon: <Globe className="h-4 w-4" />,
|
||||
badge: <Badge variant="secondary" className="ml-2">{hubStats.data?.installedTotal || 0}</Badge>,
|
||||
disabled: isToggling,
|
||||
},
|
||||
]}
|
||||
|
||||
Reference in New Issue
Block a user