mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-28 09:23:08 +08:00
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:
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -11,8 +11,10 @@ export {
|
||||
export type {
|
||||
Spec,
|
||||
SpecDimension,
|
||||
SpecScope,
|
||||
SpecReadMode,
|
||||
SpecPriority,
|
||||
SpecCategory,
|
||||
SpecCardProps,
|
||||
} from './SpecCard';
|
||||
|
||||
|
||||
@@ -350,6 +350,7 @@ export const specsSettingsKeys = {
|
||||
all: ['specsSettings'] as const,
|
||||
systemSettings: () => [...specsSettingsKeys.all, 'systemSettings'] as const,
|
||||
specStats: (projectPath?: string) => [...specsSettingsKeys.all, 'specStats', projectPath] as const,
|
||||
specsList: (projectPath?: string) => [...specsSettingsKeys.all, 'specsList', projectPath] as const,
|
||||
};
|
||||
|
||||
// ========================================
|
||||
@@ -492,7 +493,7 @@ export function useSpecsList(options: UseSpecsListOptions = {}): UseSpecsListRet
|
||||
const { projectPath, enabled = true, staleTime = STALE_TIME } = options;
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: specsSettingsKeys.specStats(projectPath), // Reuse for specs list
|
||||
queryKey: specsSettingsKeys.specsList(projectPath),
|
||||
queryFn: () => getSpecsList(projectPath),
|
||||
staleTime,
|
||||
enabled,
|
||||
@@ -528,6 +529,7 @@ export function useRebuildSpecIndex(options: UseRebuildSpecIndexOptions = {}) {
|
||||
onSuccess: () => {
|
||||
// Invalidate specs list and stats queries to refresh data
|
||||
queryClient.invalidateQueries({ queryKey: specsSettingsKeys.specStats(projectPath) });
|
||||
queryClient.invalidateQueries({ queryKey: specsSettingsKeys.specsList(projectPath) });
|
||||
},
|
||||
});
|
||||
|
||||
@@ -560,8 +562,9 @@ export function useUpdateSpecFrontmatter(options: UseUpdateSpecFrontmatterOption
|
||||
mutationFn: ({ file, readMode }: { file: string; readMode: string }) =>
|
||||
updateSpecFrontmatter(file, readMode, projectPath),
|
||||
onSuccess: () => {
|
||||
// Invalidate specs list to refresh data
|
||||
// Invalidate specs list and stats to refresh data
|
||||
queryClient.invalidateQueries({ queryKey: specsSettingsKeys.specStats(projectPath) });
|
||||
queryClient.invalidateQueries({ queryKey: specsSettingsKeys.specsList(projectPath) });
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -7268,9 +7268,11 @@ export interface SpecEntry {
|
||||
file: string;
|
||||
title: string;
|
||||
dimension: string;
|
||||
category?: 'general' | 'exploration' | 'planning' | 'execution';
|
||||
readMode: 'required' | 'optional' | 'keywords';
|
||||
priority: 'critical' | 'high' | 'medium' | 'low';
|
||||
keywords: string[];
|
||||
scope: 'global' | 'project';
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -10,36 +10,67 @@
|
||||
"rebuildIndex": "Rebuild Index",
|
||||
"loading": "Loading...",
|
||||
"noSpecs": "No specs found. Create specs in .ccw/ directory.",
|
||||
"required": "required",
|
||||
|
||||
"dimension": {
|
||||
"specs": "Project Specs",
|
||||
"personal": "Personal"
|
||||
},
|
||||
|
||||
"scope": {
|
||||
"all": "All",
|
||||
"global": "Global",
|
||||
"project": "Project"
|
||||
},
|
||||
"filterByScope": "Filter by scope:",
|
||||
|
||||
"category": {
|
||||
"general": "General",
|
||||
"exploration": "Exploration",
|
||||
"planning": "Planning",
|
||||
"execution": "Execution"
|
||||
},
|
||||
|
||||
"recommendedHooks": "Recommended Hooks",
|
||||
"recommendedHooksDesc": "One-click install system-preset spec injection hooks",
|
||||
"installAll": "Install All Recommended Hooks",
|
||||
"installAllHooks": "Install All Hooks",
|
||||
"allHooksInstalled": "All Hooks Installed",
|
||||
"hooksInstalled": "installed",
|
||||
"manageHooks": "Manage Hooks",
|
||||
"hookEvent": "Event",
|
||||
"hookScope": "Scope",
|
||||
"install": "Install",
|
||||
"installed": "Installed",
|
||||
"installing": "Installing...",
|
||||
"installedHooks": "Installed Hooks",
|
||||
"installedHooksDesc": "Manage your installed hooks configuration",
|
||||
"searchHooks": "Search hooks...",
|
||||
"noHooks": "No hooks installed. Install recommended hooks above.",
|
||||
|
||||
"spec": {
|
||||
"edit": "Edit",
|
||||
"toggle": "Toggle",
|
||||
"delete": "Delete",
|
||||
"required": "Required",
|
||||
"optional": "Optional",
|
||||
"priority": {
|
||||
"critical": "Critical",
|
||||
"high": "High",
|
||||
"medium": "Medium",
|
||||
"low": "Low"
|
||||
}
|
||||
"edit": "Edit Spec",
|
||||
"toggle": "Toggle Status",
|
||||
"delete": "Delete Spec",
|
||||
"deleteConfirm": "Are you sure you want to delete this spec?",
|
||||
"title": "Spec Title",
|
||||
"keywords": "Keywords",
|
||||
"keywordsPlaceholder": "Enter keywords, separated by commas",
|
||||
"readMode": "Read Mode",
|
||||
"priority": "Priority",
|
||||
"file": "File Path"
|
||||
},
|
||||
|
||||
"hook": {
|
||||
"install": "Install",
|
||||
"edit": "Edit",
|
||||
"toggle": "Toggle",
|
||||
"delete": "Delete",
|
||||
"uninstall": "Uninstall",
|
||||
"edit": "Edit Hook",
|
||||
"toggle": "Toggle Status",
|
||||
"delete": "Delete Hook",
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled",
|
||||
"installed": "Installed",
|
||||
"notInstalled": "Not Installed",
|
||||
"scope": {
|
||||
"global": "Global",
|
||||
"project": "Project"
|
||||
@@ -48,21 +79,142 @@
|
||||
"SessionStart": "Session Start",
|
||||
"UserPromptSubmit": "Prompt Submit",
|
||||
"SessionEnd": "Session End"
|
||||
},
|
||||
"name": "Hook Name",
|
||||
"eventLabel": "Trigger Event",
|
||||
"command": "Command",
|
||||
"scopeLabel": "Scope",
|
||||
"timeout": "Timeout (ms)",
|
||||
"failMode": "Fail Mode",
|
||||
"failModeContinue": "Continue",
|
||||
"failModeBlock": "Block",
|
||||
"failModeWarn": "Warn"
|
||||
},
|
||||
|
||||
"actions": {
|
||||
"edit": "Edit",
|
||||
"delete": "Delete",
|
||||
"reset": "Reset",
|
||||
"save": "Save",
|
||||
"saving": "Saving...",
|
||||
"view": "View Content"
|
||||
},
|
||||
|
||||
"status": {
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled"
|
||||
},
|
||||
|
||||
"readMode": {
|
||||
"required": "Required",
|
||||
"optional": "Optional"
|
||||
},
|
||||
|
||||
"priority": {
|
||||
"critical": "Critical",
|
||||
"high": "High",
|
||||
"medium": "Medium",
|
||||
"low": "Low"
|
||||
},
|
||||
|
||||
"hooks": {
|
||||
"dialog": {
|
||||
"createTitle": "Create Hook",
|
||||
"editTitle": "Edit Hook",
|
||||
"description": "Configure the hook trigger event, command, and other settings."
|
||||
},
|
||||
"fields": {
|
||||
"name": "Hook Name",
|
||||
"event": "Trigger Event",
|
||||
"scope": "Scope",
|
||||
"command": "Command",
|
||||
"description": "Description",
|
||||
"timeout": "Timeout",
|
||||
"timeoutUnit": "ms",
|
||||
"failMode": "Failure Mode"
|
||||
},
|
||||
"placeholders": {
|
||||
"name": "Enter hook name",
|
||||
"event": "Select event",
|
||||
"command": "Enter command to execute",
|
||||
"description": "Enter description (optional)"
|
||||
},
|
||||
"events": {
|
||||
"sessionStart": "Session Start",
|
||||
"userPromptSubmit": "Prompt Submit",
|
||||
"sessionEnd": "Session End"
|
||||
},
|
||||
"scope": {
|
||||
"global": "Global",
|
||||
"project": "Project"
|
||||
},
|
||||
"failModes": {
|
||||
"continue": "Continue",
|
||||
"warn": "Show Warning",
|
||||
"block": "Block Operation"
|
||||
},
|
||||
"validation": {
|
||||
"nameRequired": "Name is required",
|
||||
"commandRequired": "Command is required",
|
||||
"timeoutMin": "Minimum timeout is 1000ms",
|
||||
"timeoutMax": "Maximum timeout is 300000ms"
|
||||
}
|
||||
},
|
||||
|
||||
"hints": {
|
||||
"hookEvents": "Select when this hook should be triggered",
|
||||
"hookScope": "Global hooks apply to all projects, project hooks only to current project",
|
||||
"hookCommand": "Command to execute, can use environment variables",
|
||||
"hookTimeout": "Timeout for command execution",
|
||||
"hookFailMode": "How to handle command execution failure"
|
||||
},
|
||||
|
||||
"common": {
|
||||
"cancel": "Cancel",
|
||||
"save": "Save",
|
||||
"delete": "Delete",
|
||||
"edit": "Edit",
|
||||
"reset": "Reset",
|
||||
"confirm": "Confirm",
|
||||
"close": "Close"
|
||||
},
|
||||
|
||||
"content": {
|
||||
"edit": "Edit",
|
||||
"view": "View",
|
||||
"metadata": "Metadata",
|
||||
"markdownContent": "Markdown Content",
|
||||
"noContent": "No content available",
|
||||
"editHint": "Edit the full markdown content including frontmatter. Changes to frontmatter will be reflected in the spec metadata.",
|
||||
"placeholder": "# Spec Title\n\nContent here..."
|
||||
},
|
||||
|
||||
"injection": {
|
||||
"title": "Injection Control",
|
||||
"description": "Monitor and manage spec injection length",
|
||||
"statusTitle": "Current Injection Status",
|
||||
"settingsTitle": "Injection Control Settings",
|
||||
"settingsDescription": "Configure how spec content is injected into AI context.",
|
||||
"currentLength": "Current Length",
|
||||
"maxLength": "Max Length",
|
||||
"maxLength": "Max Injection Length (characters)",
|
||||
"maxLengthHelp": "Recommended: 4000-10000. Too large may consume too much context; too small may truncate important specs.",
|
||||
"warnThreshold": "Warn Threshold",
|
||||
"warnThresholdLabel": "Warning Threshold (characters)",
|
||||
"warnThresholdHelp": "A warning will be displayed when injection length exceeds this value.",
|
||||
"percentage": "Usage",
|
||||
"truncateOnExceed": "Truncate on Exceed",
|
||||
"truncateDescription": "Automatically truncate when injection exceeds max length",
|
||||
"truncateHelp": "Automatically truncate content when it exceeds the maximum length.",
|
||||
"overLimit": "Over Limit",
|
||||
"warning": "Warning",
|
||||
"normal": "Normal"
|
||||
"overLimitDescription": "Current injection content exceeds maximum length of {max} characters. Excess content will be truncated.",
|
||||
"warning": "Approaching Limit",
|
||||
"normal": "Normal",
|
||||
"characters": "characters",
|
||||
"statsInfo": "Statistics",
|
||||
"requiredLength": "Required specs length:",
|
||||
"matchedLength": "Keyword-matched length:",
|
||||
"remaining": "Remaining space:",
|
||||
"loadError": "Failed to load stats",
|
||||
"saveSuccess": "Settings saved successfully",
|
||||
"saveError": "Failed to save settings"
|
||||
},
|
||||
|
||||
"settings": {
|
||||
@@ -70,6 +222,7 @@
|
||||
"description": "Configure personal spec defaults and system settings",
|
||||
"personalSpecDefaults": "Personal Spec Defaults",
|
||||
"defaultReadMode": "Default Read Mode",
|
||||
"defaultReadModeHelp": "Default read mode for newly created personal specs",
|
||||
"autoEnable": "Auto Enable",
|
||||
"autoEnableDescription": "Automatically enable newly created personal specs"
|
||||
},
|
||||
@@ -77,17 +230,25 @@
|
||||
"dialog": {
|
||||
"cancel": "Cancel",
|
||||
"save": "Save",
|
||||
"close": "Close",
|
||||
"editSpec": "Edit Spec",
|
||||
"editHook": "Edit Hook",
|
||||
"confirmDelete": "Confirm Delete",
|
||||
"specTitle": "Spec Title",
|
||||
"keywords": "Keywords",
|
||||
"readMode": "Read Mode",
|
||||
"priority": "Priority",
|
||||
"hookName": "Hook Name",
|
||||
"hookEvent": "Event",
|
||||
"hookEvent": "Trigger Event",
|
||||
"hookCommand": "Command",
|
||||
"hookScope": "Scope",
|
||||
"hookTimeout": "Timeout (ms)",
|
||||
"hookFailMode": "Fail Mode"
|
||||
},
|
||||
|
||||
"form": {
|
||||
"readMode": "Read Mode",
|
||||
"priority": "Priority",
|
||||
"keywords": "Keywords"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,16 +10,47 @@
|
||||
"rebuildIndex": "重建索引",
|
||||
"loading": "加载中...",
|
||||
"noSpecs": "未找到规范。请在 .ccw/ 目录中创建规范文件。",
|
||||
"required": "必读",
|
||||
|
||||
"dimension": {
|
||||
"specs": "项目规范",
|
||||
"personal": "个人规范"
|
||||
},
|
||||
|
||||
"scope": {
|
||||
"all": "全部",
|
||||
"global": "全局",
|
||||
"project": "项目"
|
||||
},
|
||||
"filterByScope": "按范围筛选:",
|
||||
|
||||
"category": {
|
||||
"general": "通用",
|
||||
"exploration": "探索",
|
||||
"planning": "规划",
|
||||
"execution": "执行"
|
||||
},
|
||||
|
||||
"recommendedHooks": "推荐钩子",
|
||||
"recommendedHooksDesc": "一键安装系统预设的规范注入钩子",
|
||||
"installAll": "安装所有推荐钩子",
|
||||
"installAllHooks": "安装所有钩子",
|
||||
"allHooksInstalled": "已安装所有钩子",
|
||||
"hooksInstalled": "已安装",
|
||||
"manageHooks": "管理钩子",
|
||||
"hookEvent": "事件",
|
||||
"hookScope": "范围",
|
||||
"install": "安装",
|
||||
"installed": "已安装",
|
||||
"installing": "安装中...",
|
||||
|
||||
"installedHooks": "已安装钩子",
|
||||
"installedHooksDesc": "管理已安装的钩子配置",
|
||||
"searchHooks": "搜索钩子...",
|
||||
"noHooks": "未安装钩子。请安装上方的推荐钩子。",
|
||||
|
||||
"actions": {
|
||||
"view": "查看内容",
|
||||
"edit": "编辑",
|
||||
"delete": "删除",
|
||||
"reset": "重置",
|
||||
@@ -45,6 +76,7 @@
|
||||
},
|
||||
|
||||
"spec": {
|
||||
"view": "查看内容",
|
||||
"edit": "编辑规范",
|
||||
"toggle": "切换状态",
|
||||
"delete": "删除规范",
|
||||
@@ -57,6 +89,23 @@
|
||||
"file": "文件路径"
|
||||
},
|
||||
|
||||
"content": {
|
||||
"edit": "编辑",
|
||||
"view": "查看",
|
||||
"metadata": "元数据",
|
||||
"markdownContent": "Markdown 内容",
|
||||
"noContent": "无内容",
|
||||
"editHint": "编辑完整的 Markdown 内容(包括 frontmatter)。frontmatter 的更改将反映到规范元数据中。",
|
||||
"placeholder": "# 规范标题\n\n内容..."
|
||||
},
|
||||
|
||||
"common": {
|
||||
"cancel": "取消",
|
||||
"save": "保存",
|
||||
"saving": "保存中...",
|
||||
"close": "关闭"
|
||||
},
|
||||
|
||||
"hook": {
|
||||
"install": "安装",
|
||||
"uninstall": "卸载",
|
||||
@@ -88,6 +137,9 @@
|
||||
},
|
||||
|
||||
"hooks": {
|
||||
"installSuccess": "钩子安装成功",
|
||||
"installError": "钩子安装失败",
|
||||
"installAllSuccess": "所有钩子安装成功",
|
||||
"dialog": {
|
||||
"createTitle": "创建钩子",
|
||||
"editTitle": "编辑钩子",
|
||||
@@ -122,6 +174,12 @@
|
||||
"continue": "继续执行",
|
||||
"warn": "显示警告",
|
||||
"block": "阻止操作"
|
||||
},
|
||||
"validation": {
|
||||
"nameRequired": "名称为必填项",
|
||||
"commandRequired": "命令为必填项",
|
||||
"timeoutMin": "最小超时时间为 1000ms",
|
||||
"timeoutMax": "最大超时时间为 300000ms"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -133,18 +191,8 @@
|
||||
"hookFailMode": "命令执行失败时的处理方式"
|
||||
},
|
||||
|
||||
"common": {
|
||||
"cancel": "取消",
|
||||
"save": "保存",
|
||||
"delete": "删除",
|
||||
"edit": "编辑",
|
||||
"reset": "重置",
|
||||
"confirm": "确认"
|
||||
},
|
||||
|
||||
"injection": {
|
||||
"title": "注入控制",
|
||||
"description": "监控和管理规范注入长度",
|
||||
"statusTitle": "当前注入状态",
|
||||
"settingsTitle": "注入控制设置",
|
||||
"settingsDescription": "配置如何将规范内容注入到 AI 上下文中。",
|
||||
@@ -198,5 +246,11 @@
|
||||
"hookScope": "作用域",
|
||||
"hookTimeout": "超时时间(ms)",
|
||||
"hookFailMode": "失败模式"
|
||||
},
|
||||
|
||||
"form": {
|
||||
"readMode": "读取模式",
|
||||
"priority": "优先级",
|
||||
"keywords": "关键词"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
*
|
||||
* Main page for managing spec settings, injection control, and global settings.
|
||||
* Uses 4 tabs: Project Specs | Personal Specs | Injection | Settings
|
||||
* Supports category filtering (workflow stage) and scope filtering (personal only)
|
||||
*/
|
||||
import { useState, useMemo } from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
@@ -10,8 +11,8 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/Tabs';
|
||||
import { Card, CardContent } from '@/components/ui/Card';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Input } from '@/components/ui/Input';
|
||||
import { ScrollText, User, Gauge, Settings, RefreshCw, Search } from 'lucide-react';
|
||||
import { SpecCard, SpecDialog, SpecContentDialog, type Spec, type SpecFormData } from '@/components/specs';
|
||||
import { ScrollText, User, Gauge, Settings, RefreshCw, Search, Globe, Folder, Filter, Layers } from 'lucide-react';
|
||||
import { SpecCard, SpecDialog, SpecContentDialog, type Spec, type SpecFormData, type SpecCategory } from '@/components/specs';
|
||||
import { InjectionControlTab } from '@/components/specs/InjectionControlTab';
|
||||
import { GlobalSettingsTab } from '@/components/specs/GlobalSettingsTab';
|
||||
import { useSpecStats, useSpecsList, useRebuildSpecIndex } from '@/hooks/useSystemSettings';
|
||||
@@ -19,6 +20,11 @@ import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore';
|
||||
import type { SpecEntry } from '@/lib/api';
|
||||
|
||||
type SettingsTab = 'project-specs' | 'personal-specs' | 'injection' | 'settings';
|
||||
type PersonalScopeFilter = 'all' | 'global' | 'project';
|
||||
type CategoryFilter = 'all' | SpecCategory;
|
||||
|
||||
// All available categories
|
||||
const SPEC_CATEGORIES: SpecCategory[] = ['general', 'exploration', 'planning', 'execution'];
|
||||
|
||||
// Convert SpecEntry to Spec for display
|
||||
function specEntryToSpec(entry: SpecEntry, dimension: string): Spec {
|
||||
@@ -26,6 +32,8 @@ function specEntryToSpec(entry: SpecEntry, dimension: string): Spec {
|
||||
id: entry.file,
|
||||
title: entry.title,
|
||||
dimension: dimension as Spec['dimension'],
|
||||
scope: entry.scope || 'project', // Default to project if not specified
|
||||
category: entry.category || 'general', // Default to general if not specified
|
||||
keywords: entry.keywords,
|
||||
readMode: entry.readMode as Spec['readMode'],
|
||||
priority: entry.priority as Spec['priority'],
|
||||
@@ -39,8 +47,12 @@ export function SpecsSettingsPage() {
|
||||
const projectPath = useWorkflowStore(selectProjectPath);
|
||||
const [activeTab, setActiveTab] = useState<SettingsTab>('project-specs');
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [personalScopeFilter, setPersonalScopeFilter] = useState<PersonalScopeFilter>('all');
|
||||
const [categoryFilter, setCategoryFilter] = useState<CategoryFilter>('all');
|
||||
const [editDialogOpen, setEditDialogOpen] = useState(false);
|
||||
const [contentDialogOpen, setContentDialogOpen] = useState(false);
|
||||
const [editingSpec, setEditingSpec] = useState<Spec | null>(null);
|
||||
const [viewingSpec, setViewingSpec] = useState<Spec | null>(null);
|
||||
|
||||
// Fetch real data
|
||||
const { data: specsListData, isLoading: specsLoading, refetch: refetchSpecs } = useSpecsList({ projectPath });
|
||||
@@ -48,26 +60,50 @@ export function SpecsSettingsPage() {
|
||||
const rebuildMutation = useRebuildSpecIndex();
|
||||
|
||||
// Convert specs data to display format
|
||||
const { projectSpecs, personalSpecs } = useMemo(() => {
|
||||
const { projectSpecs, personalSpecs, globalPersonalSpecs, projectPersonalSpecs, categoryCounts } = useMemo(() => {
|
||||
if (!specsListData?.specs) {
|
||||
return { projectSpecs: [], personalSpecs: [] };
|
||||
return {
|
||||
projectSpecs: [],
|
||||
personalSpecs: [],
|
||||
globalPersonalSpecs: [],
|
||||
projectPersonalSpecs: [],
|
||||
categoryCounts: { general: 0, exploration: 0, planning: 0, execution: 0 }
|
||||
};
|
||||
}
|
||||
|
||||
const specs: Spec[] = [];
|
||||
const personal: Spec[] = [];
|
||||
const globalPersonal: Spec[] = [];
|
||||
const projectPersonal: Spec[] = [];
|
||||
const counts: Record<SpecCategory, number> = { general: 0, exploration: 0, planning: 0, execution: 0 };
|
||||
|
||||
for (const [dimension, entries] of Object.entries(specsListData.specs)) {
|
||||
for (const entry of entries) {
|
||||
const spec = specEntryToSpec(entry, dimension);
|
||||
// Count by category
|
||||
if (spec.category) {
|
||||
counts[spec.category]++;
|
||||
}
|
||||
if (dimension === 'personal') {
|
||||
personal.push(spec);
|
||||
if (spec.scope === 'global') {
|
||||
globalPersonal.push(spec);
|
||||
} else {
|
||||
projectPersonal.push(spec);
|
||||
}
|
||||
} else {
|
||||
specs.push(spec);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { projectSpecs: specs, personalSpecs: personal };
|
||||
return {
|
||||
projectSpecs: specs,
|
||||
personalSpecs: personal,
|
||||
globalPersonalSpecs: globalPersonal,
|
||||
projectPersonalSpecs: projectPersonal,
|
||||
categoryCounts: counts
|
||||
};
|
||||
}, [specsListData]);
|
||||
|
||||
const isLoading = specsLoading;
|
||||
@@ -113,16 +149,34 @@ export function SpecsSettingsPage() {
|
||||
};
|
||||
|
||||
const filterSpecs = (specs: Spec[]) => {
|
||||
if (!searchQuery.trim()) return specs;
|
||||
const query = searchQuery.toLowerCase();
|
||||
return specs.filter(spec =>
|
||||
spec.title.toLowerCase().includes(query) ||
|
||||
spec.keywords.some(k => k.toLowerCase().includes(query))
|
||||
);
|
||||
let result = specs;
|
||||
// Filter by category
|
||||
if (categoryFilter !== 'all') {
|
||||
result = result.filter(spec => spec.category === categoryFilter);
|
||||
}
|
||||
// Filter by search query
|
||||
if (searchQuery.trim()) {
|
||||
const query = searchQuery.toLowerCase();
|
||||
result = result.filter(spec =>
|
||||
spec.title.toLowerCase().includes(query) ||
|
||||
spec.keywords.some(k => k.toLowerCase().includes(query))
|
||||
);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
const renderSpecsTab = (dimension: 'project' | 'personal') => {
|
||||
const specs = dimension === 'project' ? projectSpecs : personalSpecs;
|
||||
let specs = dimension === 'project' ? projectSpecs : personalSpecs;
|
||||
|
||||
// Apply scope filter for personal specs
|
||||
if (dimension === 'personal') {
|
||||
if (personalScopeFilter === 'global') {
|
||||
specs = globalPersonalSpecs;
|
||||
} else if (personalScopeFilter === 'project') {
|
||||
specs = projectPersonalSpecs;
|
||||
}
|
||||
}
|
||||
|
||||
const filteredSpecs = filterSpecs(specs);
|
||||
|
||||
return (
|
||||
@@ -144,13 +198,77 @@ export function SpecsSettingsPage() {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Scope filter for personal specs */}
|
||||
{dimension === 'personal' && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Filter className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{formatMessage({ id: 'specs.filterByScope', defaultMessage: 'Filter by scope:' })}
|
||||
</span>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant={personalScopeFilter === 'all' ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => setPersonalScopeFilter('all')}
|
||||
>
|
||||
{formatMessage({ id: 'specs.scope.all', defaultMessage: 'All' })} ({personalSpecs.length})
|
||||
</Button>
|
||||
<Button
|
||||
variant={personalScopeFilter === 'global' ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => setPersonalScopeFilter('global')}
|
||||
>
|
||||
<Globe className="h-3 w-3 mr-1" />
|
||||
{formatMessage({ id: 'specs.scope.global', defaultMessage: 'Global' })} ({globalPersonalSpecs.length})
|
||||
</Button>
|
||||
<Button
|
||||
variant={personalScopeFilter === 'project' ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => setPersonalScopeFilter('project')}
|
||||
>
|
||||
<Folder className="h-3 w-3 mr-1" />
|
||||
{formatMessage({ id: 'specs.scope.project', defaultMessage: 'Project' })} ({projectPersonalSpecs.length})
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Category filter for workflow stage */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Layers className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{formatMessage({ id: 'specs.filterByCategory', defaultMessage: 'Workflow stage:' })}
|
||||
</span>
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
<Button
|
||||
variant={categoryFilter === 'all' ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => setCategoryFilter('all')}
|
||||
>
|
||||
{formatMessage({ id: 'specs.category.all', defaultMessage: 'All' })}
|
||||
</Button>
|
||||
{SPEC_CATEGORIES.map(cat => (
|
||||
<Button
|
||||
key={cat}
|
||||
variant={categoryFilter === cat ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => setCategoryFilter(cat)}
|
||||
>
|
||||
{formatMessage({ id: `specs.category.${cat}`, defaultMessage: cat })} ({categoryCounts[cat]})
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats Summary */}
|
||||
{statsData?.dimensions && (
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
{Object.entries(statsData.dimensions).map(([dim, data]) => (
|
||||
<Card key={dim}>
|
||||
<CardContent className="pt-4">
|
||||
<div className="text-sm text-muted-foreground capitalize">{dim}</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{formatMessage({ id: `specs.dimension.${dim}`, defaultMessage: dim })}
|
||||
</div>
|
||||
<div className="text-2xl font-bold">{(data as { count: number }).count}</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{(data as { requiredCount: number }).requiredCount} {formatMessage({ id: 'specs.required', defaultMessage: 'required' })}
|
||||
@@ -167,7 +285,7 @@ export function SpecsSettingsPage() {
|
||||
<CardContent className="py-8 text-center text-muted-foreground">
|
||||
{isLoading
|
||||
? formatMessage({ id: 'specs.loading', defaultMessage: 'Loading specs...' })
|
||||
: formatMessage({ id: 'specs.noSpecs', defaultMessage: 'No specs found. Create specs in .workflow/ directory.' })
|
||||
: formatMessage({ id: 'specs.noSpecs', defaultMessage: 'No specs found. Create specs in .ccw/ directory.' })
|
||||
}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
Reference in New Issue
Block a user