refactor(req-plan): streamline codebase exploration and decomposition guidelines

This commit is contained in:
catlog22
2026-02-24 20:31:52 +08:00
parent 80ab955f8b
commit 6c9ad9a9f3
7 changed files with 695 additions and 298 deletions

View File

@@ -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}>

View File

@@ -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>
);
}

View File

@@ -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',

View File

@@ -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,
},
]}

View File

@@ -318,17 +318,34 @@ async function fetchRemoteSkillIndex(): Promise<RemoteSkillIndex> {
// Check cache
const now = Date.now();
if (remoteSkillsCache.data && (now - remoteSkillsCache.timestamp) < CACHE_TTL_MS) {
console.log('[SkillHub] Using cached remote index');
return remoteSkillsCache.data;
}
const indexUrl = `https://raw.githubusercontent.com/${GITHUB_CONFIG.owner}/${GITHUB_CONFIG.repo}/${GITHUB_CONFIG.branch}/${GITHUB_CONFIG.skillIndexPath}`;
console.log('[SkillHub] Fetching remote index from:', indexUrl);
try {
const response = await fetch(indexUrl);
// Add timeout to prevent hanging
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 second timeout
const response = await fetch(indexUrl, {
signal: controller.signal,
headers: {
'User-Agent': 'CCW-SkillHub/1.0'
}
});
clearTimeout(timeoutId);
console.log('[SkillHub] Fetch response status:', response.status, response.statusText);
if (!response.ok) {
// Try local fallback
console.log('[SkillHub] Fetch failed, trying local fallback');
const localIndex = loadLocalIndex();
if (localIndex) {
console.log('[SkillHub] Using local fallback index');
return localIndex;
}
throw new Error(`GitHub API error: ${response.status} ${response.statusText}`);
@@ -336,6 +353,7 @@ async function fetchRemoteSkillIndex(): Promise<RemoteSkillIndex> {
const index = await response.json() as RemoteSkillIndex;
index.source = 'github';
console.log('[SkillHub] Successfully fetched remote index with', index.skills.length, 'skills');
// Update cache
remoteSkillsCache = { data: index, timestamp: now };
@@ -345,17 +363,27 @@ async function fetchRemoteSkillIndex(): Promise<RemoteSkillIndex> {
return index;
} catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error);
console.error('[SkillHub] Error fetching remote index:', errorMsg);
// Return cached data if available, even if expired
if (remoteSkillsCache.data) {
console.log('[SkillHub] Using expired cache as fallback');
return remoteSkillsCache.data;
}
// Try local fallback
const localIndex = loadLocalIndex();
if (localIndex) {
console.log('[SkillHub] Using local fallback index after error');
return localIndex;
}
// If it's a timeout or network error, provide a more helpful message
if (errorMsg.includes('aborted') || errorMsg.includes('timeout')) {
throw new Error('Network timeout - please check your internet connection');
}
throw error;
}
}
@@ -395,11 +423,35 @@ function saveCachedIndex(index: RemoteSkillIndex): void {
* Fetch a single skill from remote URL
*/
async function fetchRemoteSkill(downloadUrl: string): Promise<string> {
const response = await fetch(downloadUrl);
if (!response.ok) {
throw new Error(`Failed to fetch skill: ${response.status} ${response.statusText}`);
console.log('[SkillHub] Fetching skill from:', downloadUrl);
try {
// Add timeout to prevent hanging
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 second timeout
const response = await fetch(downloadUrl, {
signal: controller.signal,
headers: {
'User-Agent': 'CCW-SkillHub/1.0'
}
});
clearTimeout(timeoutId);
console.log('[SkillHub] Fetch skill response status:', response.status, response.statusText);
if (!response.ok) {
throw new Error(`Failed to fetch skill: ${response.status} ${response.statusText}`);
}
const content = await response.text();
console.log('[SkillHub] Successfully fetched skill, size:', content.length, 'bytes');
return content;
} catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error);
console.error('[SkillHub] Error fetching skill:', errorMsg);
throw error;
}
return response.text();
}
/**