feat: add category and scope to specs for enhanced filtering and organization

- Introduced SpecCategory and SpecScope types to categorize specs by workflow stage and scope (global/project).
- Updated Spec interface to include category and scope properties.
- Enhanced SpecCard component to display category and scope badges.
- Implemented category and scope filtering in SpecsSettingsPage.
- Updated localization files to support new category and scope labels.
- Modified spec loading commands to utilize category instead of keywords.
- Adjusted spec index builder to handle category and scope during spec parsing.
- Updated seed documents to include category information.
This commit is contained in:
catlog22
2026-02-26 23:43:55 +08:00
parent 052e25dddb
commit dfa8e0d9f5
47 changed files with 619 additions and 179 deletions

View File

@@ -5,6 +5,7 @@
import { useState, useEffect, useCallback, useMemo } from 'react';
import { useIntl } from 'react-intl';
import { Link } from 'react-router-dom';
import { toast } from 'sonner';
import { cn } from '@/lib/utils';
import {
@@ -28,7 +29,7 @@ import {
Plug,
Download,
CheckCircle2,
ExternalLink,
Settings,
} from 'lucide-react';
import { useInstallRecommendedHooks } from '@/hooks/useSystemSettings';
@@ -325,32 +326,34 @@ export function InjectionControlTab({ className }: InjectionControlTabProps) {
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center gap-4">
<Button
onClick={handleInstallAllHooks}
disabled={allHooksInstalled || installingHookIds.length > 0}
>
{allHooksInstalled ? (
<>
<CheckCircle2 className="h-4 w-4 mr-2" />
{formatMessage({ id: 'specs.allHooksInstalled', defaultMessage: 'All Hooks Installed' })}
</>
) : (
<>
<Download className="h-4 w-4 mr-2" />
{formatMessage({ id: 'specs.installAllHooks', defaultMessage: 'Install All Hooks' })}
</>
)}
</Button>
<div className="text-sm text-muted-foreground">
{installedCount} / {RECOMMENDED_HOOKS.length}{' '}
{formatMessage({ id: 'specs.hooksInstalled', defaultMessage: 'installed' })}
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<Button
onClick={handleInstallAllHooks}
disabled={allHooksInstalled || installingHookIds.length > 0}
>
{allHooksInstalled ? (
<>
<CheckCircle2 className="h-4 w-4 mr-2" />
{formatMessage({ id: 'specs.allHooksInstalled', defaultMessage: 'All Hooks Installed' })}
</>
) : (
<>
<Download className="h-4 w-4 mr-2" />
{formatMessage({ id: 'specs.installAllHooks', defaultMessage: 'Install All Hooks' })}
</>
)}
</Button>
<div className="text-sm text-muted-foreground">
{installedCount} / {RECOMMENDED_HOOKS.length}{' '}
{formatMessage({ id: 'specs.hooksInstalled', defaultMessage: 'installed' })}
</div>
</div>
<Button variant="ghost" size="sm" asChild>
<a href="/hooks" target="_blank" rel="noopener noreferrer">
<ExternalLink className="h-4 w-4 mr-1" />
<Link to="/hooks">
<Settings className="h-4 w-4 mr-1" />
{formatMessage({ id: 'specs.manageHooks', defaultMessage: 'Manage Hooks' })}
</a>
</Link>
</Button>
</div>

View File

@@ -23,6 +23,10 @@ import {
Trash2,
FileText,
Tag,
Eye,
Globe,
Folder,
Layers,
} from 'lucide-react';
// ========== Types ==========
@@ -32,6 +36,11 @@ import {
*/
export type SpecDimension = 'specs' | 'personal';
/**
* Spec scope type
*/
export type SpecScope = 'global' | 'project';
/**
* Spec read mode type
*/
@@ -42,6 +51,11 @@ export type SpecReadMode = 'required' | 'optional';
*/
export type SpecPriority = 'critical' | 'high' | 'medium' | 'low';
/**
* Spec category type for workflow stage-based loading
*/
export type SpecCategory = 'general' | 'exploration' | 'planning' | 'execution';
/**
* Spec data structure
*/
@@ -54,6 +68,10 @@ export interface Spec {
file: string;
/** Spec dimension/category */
dimension: SpecDimension;
/** Scope: global (from ~/.ccw/) or project (from .ccw/) */
scope: SpecScope;
/** Workflow stage category for system-level loading */
category?: SpecCategory;
/** Read mode: required (always inject) or optional (keyword match) */
readMode: SpecReadMode;
/** Priority level */
@@ -72,6 +90,8 @@ export interface Spec {
export interface SpecCardProps {
/** Spec data */
spec: Spec;
/** Called when view content action is triggered */
onView?: (spec: Spec) => void;
/** Called when edit action is triggered */
onEdit?: (spec: Spec) => void;
/** Called when delete action is triggered */
@@ -108,6 +128,17 @@ const priorityConfig: Record<
low: { variant: 'secondary', labelKey: 'specs.priority.low' },
};
// Category badge configuration for workflow stage
const categoryConfig: Record<
SpecCategory,
{ variant: 'default' | 'secondary' | 'outline'; labelKey: string }
> = {
general: { variant: 'secondary', labelKey: 'specs.category.general' },
exploration: { variant: 'outline', labelKey: 'specs.category.exploration' },
planning: { variant: 'outline', labelKey: 'specs.category.planning' },
execution: { variant: 'outline', labelKey: 'specs.category.execution' },
};
// ========== Component ==========
/**
@@ -115,6 +146,7 @@ const priorityConfig: Record<
*/
export function SpecCard({
spec,
onView,
onEdit,
onDelete,
onToggle,
@@ -181,6 +213,10 @@ export function SpecCard({
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={(e) => { e.stopPropagation(); onView?.(spec); }}>
<Eye className="mr-2 h-4 w-4" />
{formatMessage({ id: 'specs.actions.view', defaultMessage: 'View Content' })}
</DropdownMenuItem>
<DropdownMenuItem onClick={(e) => handleAction(e, 'edit')}>
<Edit className="mr-2 h-4 w-4" />
{formatMessage({ id: 'specs.actions.edit' })}
@@ -201,6 +237,29 @@ export function SpecCard({
{/* Badges */}
<div className="mt-3 flex flex-wrap items-center gap-2">
{/* Category badge - workflow stage */}
{spec.category && (
<Badge variant={categoryConfig[spec.category].variant} className="text-xs gap-1">
<Layers className="h-3 w-3" />
{formatMessage({ id: categoryConfig[spec.category].labelKey, defaultMessage: spec.category })}
</Badge>
)}
{/* Scope badge - only show for personal specs */}
{spec.dimension === 'personal' && (
<Badge variant="outline" className="text-xs gap-1">
{spec.scope === 'global' ? (
<>
<Globe className="h-3 w-3" />
{formatMessage({ id: 'specs.scope.global', defaultMessage: 'Global' })}
</>
) : (
<>
<Folder className="h-3 w-3" />
{formatMessage({ id: 'specs.scope.project', defaultMessage: 'Project' })}
</>
)}
</Badge>
)}
<Badge variant={readMode.variant} className="text-xs">
{formatMessage({ id: readMode.labelKey })}
</Badge>

View File

@@ -11,8 +11,10 @@ export {
export type {
Spec,
SpecDimension,
SpecScope,
SpecReadMode,
SpecPriority,
SpecCategory,
SpecCardProps,
} from './SpecCard';