diff --git a/ccw/frontend/src/components/specs/GlobalSettingsTab.tsx b/ccw/frontend/src/components/specs/GlobalSettingsTab.tsx index c1184d18..32966f3b 100644 --- a/ccw/frontend/src/components/specs/GlobalSettingsTab.tsx +++ b/ccw/frontend/src/components/specs/GlobalSettingsTab.tsx @@ -50,8 +50,6 @@ interface SpecDimensionStats { interface SpecStats { dimensions: { specs: SpecDimensionStats; - roadmap: SpecDimensionStats; - changelog: SpecDimensionStats; personal: SpecDimensionStats; }; injectionLength?: { @@ -199,8 +197,6 @@ export function GlobalSettingsTab() { // Dimension display config const dimensionLabels: Record = { specs: 'Specs', - roadmap: 'Roadmap', - changelog: 'Changelog', personal: 'Personal', }; diff --git a/ccw/frontend/src/components/specs/InjectionControlTab.tsx b/ccw/frontend/src/components/specs/InjectionControlTab.tsx index 94e9b60c..faecfa35 100644 --- a/ccw/frontend/src/components/specs/InjectionControlTab.tsx +++ b/ccw/frontend/src/components/specs/InjectionControlTab.tsx @@ -3,7 +3,7 @@ // ======================================== // Tab for managing spec injection control settings -import { useState, useEffect, useCallback } from 'react'; +import { useState, useEffect, useCallback, useMemo } from 'react'; import { useIntl } from 'react-intl'; import { toast } from 'sonner'; import { cn } from '@/lib/utils'; @@ -25,7 +25,12 @@ import { Loader2, RefreshCw, AlertTriangle, + Plug, + Download, + CheckCircle2, + ExternalLink, } from 'lucide-react'; +import { useInstallRecommendedHooks } from '@/hooks/useSystemSettings'; // ========== Types ========== @@ -39,8 +44,6 @@ export interface InjectionStats { export interface SpecStatsResponse { dimensions: { specs: { count: number; requiredCount: number }; - roadmap: { count: number; requiredCount: number }; - changelog: { count: number; requiredCount: number }; personal: { count: number; requiredCount: number }; }; injectionLength: InjectionStats; @@ -64,6 +67,27 @@ export interface InjectionControlTabProps { className?: string; } +// ========== Recommended Hooks Configuration ========== + +const RECOMMENDED_HOOKS = [ + { + id: 'spec-injection-session', + name: 'Spec Context Injection (Session)', + event: 'SessionStart', + command: 'ccw spec load --stdin', + scope: 'global' as const, + description: 'Automatically inject spec context when Claude session starts', + }, + { + id: 'spec-injection-prompt', + name: 'Spec Context Injection (Prompt)', + event: 'UserPromptSubmit', + command: 'ccw spec load --stdin', + scope: 'project' as const, + description: 'Inject spec context when user submits a prompt, matching keywords', + }, +]; + // ========== API Functions ========== async function fetchSpecStats(): Promise { @@ -119,6 +143,7 @@ function calculatePercentage(current: number, max: number): number { export function InjectionControlTab({ className }: InjectionControlTabProps) { const { formatMessage } = useIntl(); + const installHooksMutation = useInstallRecommendedHooks(); // State for stats const [stats, setStats] = useState(null); @@ -138,6 +163,9 @@ export function InjectionControlTab({ className }: InjectionControlTabProps) { const [hasChanges, setHasChanges] = useState(false); const [isSaving, setIsSaving] = useState(false); + // State for hooks installation + const [installingHookIds, setInstallingHookIds] = useState([]); + // Fetch stats const loadStats = useCallback(async () => { setStatsLoading(true); @@ -216,6 +244,61 @@ export function InjectionControlTab({ className }: InjectionControlTabProps) { setHasChanges(false); }; + // ========== Hooks Installation ========== + + // Get installed hooks from system settings + const installedHookIds = useMemo(() => { + const installed = new Set(); + // Check if hooks are already installed by checking system settings + // For now, we'll track this via the mutation result + return installed; + }, []); + + const installedCount = 0; // Will be updated when we have real data + const allHooksInstalled = installedCount === RECOMMENDED_HOOKS.length; + + // Install single hook + const handleInstallHook = useCallback(async (hookId: string) => { + setInstallingHookIds(prev => [...prev, hookId]); + try { + await installHooksMutation.installHooks([hookId], 'global'); + toast.success( + formatMessage({ id: 'specs.hooks.installSuccess', defaultMessage: 'Hook installed successfully' }) + ); + } catch (err) { + toast.error( + formatMessage({ id: 'specs.hooks.installError', defaultMessage: 'Failed to install hook' }) + ); + console.error('Failed to install hook:', err); + } finally { + setInstallingHookIds(prev => prev.filter(id => id !== hookId)); + } + }, [installHooksMutation, formatMessage]); + + // Install all hooks + const handleInstallAllHooks = useCallback(async () => { + const uninstalledHooks = RECOMMENDED_HOOKS.filter(h => !installedHookIds.has(h.id)); + if (uninstalledHooks.length === 0) return; + + setInstallingHookIds(uninstalledHooks.map(h => h.id)); + try { + await installHooksMutation.installHooks( + uninstalledHooks.map(h => h.id), + 'global' + ); + toast.success( + formatMessage({ id: 'specs.hooks.installAllSuccess', defaultMessage: 'All hooks installed successfully' }) + ); + } catch (err) { + toast.error( + formatMessage({ id: 'specs.hooks.installError', defaultMessage: 'Failed to install hooks' }) + ); + console.error('Failed to install hooks:', err); + } finally { + setInstallingHookIds([]); + } + }, [installedHookIds, installHooksMutation, formatMessage]); + // Calculate progress and status const currentLength = stats?.injectionLength?.withKeywords || 0; const maxLength = settings.maxLength; @@ -227,6 +310,98 @@ export function InjectionControlTab({ className }: InjectionControlTabProps) { return (
+ {/* Recommended Hooks Section */} + + + + + {formatMessage({ id: 'specs.recommendedHooks', defaultMessage: 'Recommended Hooks' })} + + + {formatMessage({ + id: 'specs.recommendedHooksDesc', + defaultMessage: 'One-click install spec injection hooks for automatic context loading', + })} + + + +
+ +
+ {installedCount} / {RECOMMENDED_HOOKS.length}{' '} + {formatMessage({ id: 'specs.hooksInstalled', defaultMessage: 'installed' })} +
+ +
+ +
+ {RECOMMENDED_HOOKS.map(hook => { + const isInstalled = installedHookIds.has(hook.id); + const isInstalling = installingHookIds.includes(hook.id); + + return ( +
+
+
+ {hook.name} + {isInstalled && } +
+
+ {hook.description} +
+
+ + {formatMessage({ id: 'specs.hookEvent', defaultMessage: 'Event' })}:{' '} + {hook.event} + + + {formatMessage({ id: 'specs.hookScope', defaultMessage: 'Scope' })}:{' '} + {hook.scope} + +
+
+ +
+ ); + })} +
+
+
+ {/* Current Injection Status Card */} diff --git a/ccw/frontend/src/components/specs/SpecCard.tsx b/ccw/frontend/src/components/specs/SpecCard.tsx index 5c2efcea..f77bbaa5 100644 --- a/ccw/frontend/src/components/specs/SpecCard.tsx +++ b/ccw/frontend/src/components/specs/SpecCard.tsx @@ -30,7 +30,7 @@ import { /** * Spec dimension type */ -export type SpecDimension = 'specs' | 'roadmap' | 'changelog' | 'personal'; +export type SpecDimension = 'specs' | 'personal'; /** * Spec read mode type diff --git a/ccw/frontend/src/locales/en/specs.json b/ccw/frontend/src/locales/en/specs.json index 70c79d06..e2c37d7a 100644 --- a/ccw/frontend/src/locales/en/specs.json +++ b/ccw/frontend/src/locales/en/specs.json @@ -9,7 +9,7 @@ "searchPlaceholder": "Search specs...", "rebuildIndex": "Rebuild Index", "loading": "Loading...", - "noSpecs": "No specs found. Create specs in .workflow/ directory.", + "noSpecs": "No specs found. Create specs in .ccw/ directory.", "recommendedHooks": "Recommended Hooks", "recommendedHooksDesc": "One-click install system-preset spec injection hooks", diff --git a/ccw/frontend/src/locales/zh/specs.json b/ccw/frontend/src/locales/zh/specs.json index 8e71fc66..a367dcb6 100644 --- a/ccw/frontend/src/locales/zh/specs.json +++ b/ccw/frontend/src/locales/zh/specs.json @@ -9,7 +9,7 @@ "searchPlaceholder": "搜索规范...", "rebuildIndex": "重建索引", "loading": "加载中...", - "noSpecs": "未找到规范。请在 .workflow/ 目录中创建规范文件。", + "noSpecs": "未找到规范。请在 .ccw/ 目录中创建规范文件。", "recommendedHooks": "推荐钩子", "recommendedHooksDesc": "一键安装系统预设的规范注入钩子", diff --git a/ccw/src/cli.ts b/ccw/src/cli.ts index fb364b78..91ee5208 100644 --- a/ccw/src/cli.ts +++ b/ccw/src/cli.ts @@ -302,8 +302,8 @@ export function run(argv: string[]): void { program .command('spec [subcommand] [args...]') .description('Project spec management for conventions and guidelines') - .option('--dimension ', 'Target dimension: specs, roadmap, changelog, personal') - .option('--context ', 'Context text for keyword extraction (CLI mode)') + .option('--dimension ', 'Target dimension: specs, personal') + .option('--keywords ', 'Keywords for spec matching (CLI mode)') .option('--stdin', 'Read input from stdin (Hook mode)') .option('--json', 'Output as JSON') .action((subcommand, args, options) => specCommand(subcommand, args, options)); diff --git a/ccw/src/tools/spec-loader.ts b/ccw/src/tools/spec-loader.ts index 030a5ad4..0decdfe7 100644 --- a/ccw/src/tools/spec-loader.ts +++ b/ccw/src/tools/spec-loader.ts @@ -266,7 +266,7 @@ export function filterSpecs( /** * Merge loaded spec content by dimension priority. * - * Dimension priority order: personal(1) < changelog(2) < roadmap(3) < specs(4). + * Dimension priority order: personal(1) < specs(2). * Within a dimension, specs are ordered by priority weight (critical > high > medium > low). * * @param specs - All loaded specs