mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-28 09:23:08 +08:00
feat: add injection preview functionality and enhance specs management
- Implemented injection preview feature in InjectionControlTab with file listing and content preview. - Added new API endpoint for fetching injection preview data. - Introduced content length caching for performance optimization. - Enhanced spec loading to support category filtering. - Updated localization files for new features and terms. - Created new personal and project specs for coding style and architecture constraints. - Improved CLI options for category selection in spec commands.
This commit is contained in:
27
.ccw/personal/coding-style.md
Normal file
27
.ccw/personal/coding-style.md
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
---
|
||||||
|
title: "Personal Coding Style"
|
||||||
|
dimension: personal
|
||||||
|
category: general
|
||||||
|
keywords:
|
||||||
|
- style
|
||||||
|
- preference
|
||||||
|
readMode: optional
|
||||||
|
priority: medium
|
||||||
|
---
|
||||||
|
|
||||||
|
# Personal Coding Style
|
||||||
|
|
||||||
|
## Preferences
|
||||||
|
|
||||||
|
- Describe your preferred coding style here
|
||||||
|
- Example: verbose variable names vs terse, functional vs imperative
|
||||||
|
|
||||||
|
## Patterns I Prefer
|
||||||
|
|
||||||
|
- List patterns you reach for most often
|
||||||
|
- Example: builder pattern, factory functions, tagged unions
|
||||||
|
|
||||||
|
## Things I Avoid
|
||||||
|
|
||||||
|
- List anti-patterns or approaches you dislike
|
||||||
|
- Example: deep inheritance hierarchies, magic strings
|
||||||
25
.ccw/personal/tool-preferences.md
Normal file
25
.ccw/personal/tool-preferences.md
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
---
|
||||||
|
title: "Tool Preferences"
|
||||||
|
dimension: personal
|
||||||
|
category: general
|
||||||
|
keywords:
|
||||||
|
- tool
|
||||||
|
- cli
|
||||||
|
- editor
|
||||||
|
readMode: optional
|
||||||
|
priority: low
|
||||||
|
---
|
||||||
|
|
||||||
|
# Tool Preferences
|
||||||
|
|
||||||
|
## Editor
|
||||||
|
|
||||||
|
- Preferred editor and key extensions/plugins
|
||||||
|
|
||||||
|
## CLI Tools
|
||||||
|
|
||||||
|
- Preferred shell, package manager, build tools
|
||||||
|
|
||||||
|
## Debugging
|
||||||
|
|
||||||
|
- Preferred debugging approach and tools
|
||||||
32
.ccw/specs/architecture-constraints.md
Normal file
32
.ccw/specs/architecture-constraints.md
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
---
|
||||||
|
title: "Architecture Constraints"
|
||||||
|
dimension: specs
|
||||||
|
category: planning
|
||||||
|
keywords:
|
||||||
|
- architecture
|
||||||
|
- module
|
||||||
|
- layer
|
||||||
|
- pattern
|
||||||
|
readMode: required
|
||||||
|
priority: high
|
||||||
|
---
|
||||||
|
|
||||||
|
# Architecture Constraints
|
||||||
|
|
||||||
|
## Module Boundaries
|
||||||
|
|
||||||
|
- Each module owns its data and exposes a public API
|
||||||
|
- No circular dependencies between modules
|
||||||
|
- Shared utilities live in a dedicated shared layer
|
||||||
|
|
||||||
|
## Layer Separation
|
||||||
|
|
||||||
|
- Presentation layer must not import data layer directly
|
||||||
|
- Business logic must be independent of framework specifics
|
||||||
|
- Configuration must be externalized, not hardcoded
|
||||||
|
|
||||||
|
## Dependency Rules
|
||||||
|
|
||||||
|
- External dependencies require justification
|
||||||
|
- Prefer standard library when available
|
||||||
|
- Pin dependency versions for reproducibility
|
||||||
38
.ccw/specs/coding-conventions.md
Normal file
38
.ccw/specs/coding-conventions.md
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
---
|
||||||
|
title: "Coding Conventions"
|
||||||
|
dimension: specs
|
||||||
|
category: general
|
||||||
|
keywords:
|
||||||
|
- typescript
|
||||||
|
- naming
|
||||||
|
- style
|
||||||
|
- convention
|
||||||
|
readMode: required
|
||||||
|
priority: high
|
||||||
|
---
|
||||||
|
|
||||||
|
# Coding Conventions
|
||||||
|
|
||||||
|
## Naming
|
||||||
|
|
||||||
|
- Use camelCase for variables and functions
|
||||||
|
- Use PascalCase for classes and interfaces
|
||||||
|
- Use UPPER_SNAKE_CASE for constants
|
||||||
|
|
||||||
|
## Formatting
|
||||||
|
|
||||||
|
- 2-space indentation
|
||||||
|
- Single quotes for strings
|
||||||
|
- Trailing commas in multi-line constructs
|
||||||
|
|
||||||
|
## Patterns
|
||||||
|
|
||||||
|
- Prefer composition over inheritance
|
||||||
|
- Use early returns to reduce nesting
|
||||||
|
- Keep functions under 30 lines when practical
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
- Always handle errors explicitly
|
||||||
|
- Prefer typed errors over generic catch-all
|
||||||
|
- Log errors with sufficient context
|
||||||
@@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { useIntl } from 'react-intl';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { Settings, RefreshCw } from 'lucide-react';
|
import { Settings, RefreshCw } from 'lucide-react';
|
||||||
import {
|
import {
|
||||||
@@ -113,6 +114,7 @@ const settingsKeys = {
|
|||||||
// ========== Component ==========
|
// ========== Component ==========
|
||||||
|
|
||||||
export function GlobalSettingsTab() {
|
export function GlobalSettingsTab() {
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
// Local state for immediate UI feedback
|
// Local state for immediate UI feedback
|
||||||
@@ -149,10 +151,13 @@ export function GlobalSettingsTab() {
|
|||||||
mutationFn: updateSystemSettings,
|
mutationFn: updateSystemSettings,
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
queryClient.setQueryData(settingsKeys.settings(), data.settings);
|
queryClient.setQueryData(settingsKeys.settings(), data.settings);
|
||||||
toast.success('Settings saved successfully');
|
toast.success(formatMessage({ id: 'specs.injection.saveSuccess', defaultMessage: 'Settings saved successfully' }));
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
toast.error(`Failed to save settings: ${error.message}`);
|
toast.error(formatMessage(
|
||||||
|
{ id: 'specs.injection.saveError', defaultMessage: 'Failed to save settings: {error}' },
|
||||||
|
{ error: error.message }
|
||||||
|
));
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -194,12 +199,6 @@ export function GlobalSettingsTab() {
|
|||||||
const isLoading = isLoadingSettings || isLoadingStats;
|
const isLoading = isLoadingSettings || isLoadingStats;
|
||||||
const hasError = settingsError || statsError;
|
const hasError = settingsError || statsError;
|
||||||
|
|
||||||
// Dimension display config
|
|
||||||
const dimensionLabels: Record<string, string> = {
|
|
||||||
specs: 'Specs',
|
|
||||||
personal: 'Personal',
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Personal Spec Defaults Card */}
|
{/* Personal Spec Defaults Card */}
|
||||||
@@ -207,16 +206,20 @@ export function GlobalSettingsTab() {
|
|||||||
<CardHeader>
|
<CardHeader>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Settings className="h-5 w-5 text-muted-foreground" />
|
<Settings className="h-5 w-5 text-muted-foreground" />
|
||||||
<CardTitle>Personal Spec Defaults</CardTitle>
|
<CardTitle>
|
||||||
|
{formatMessage({ id: 'specs.settings.personalSpecDefaults', defaultMessage: 'Personal Spec Defaults' })}
|
||||||
|
</CardTitle>
|
||||||
</div>
|
</div>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
These settings will be applied when creating new personal specs
|
{formatMessage({ id: 'specs.settings.personalSpecDefaultsDesc', defaultMessage: 'These settings will be applied when creating new personal specs' })}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-6">
|
<CardContent className="space-y-6">
|
||||||
{/* Default Read Mode */}
|
{/* Default Read Mode */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="default-read-mode">Default Read Mode</Label>
|
<Label htmlFor="default-read-mode">
|
||||||
|
{formatMessage({ id: 'specs.settings.defaultReadMode', defaultMessage: 'Default Read Mode' })}
|
||||||
|
</Label>
|
||||||
<Select
|
<Select
|
||||||
value={localDefaults.defaultReadMode}
|
value={localDefaults.defaultReadMode}
|
||||||
onValueChange={(value) =>
|
onValueChange={(value) =>
|
||||||
@@ -224,28 +227,30 @@ export function GlobalSettingsTab() {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SelectTrigger id="default-read-mode" className="w-full">
|
<SelectTrigger id="default-read-mode" className="w-full">
|
||||||
<SelectValue placeholder="Select read mode" />
|
<SelectValue placeholder={formatMessage({ id: 'specs.settings.selectReadMode', defaultMessage: 'Select read mode' })} />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="required">
|
<SelectItem value="required">
|
||||||
Required (Always inject)
|
{formatMessage({ id: 'specs.readMode.required', defaultMessage: 'Required' })}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value="optional">
|
<SelectItem value="optional">
|
||||||
Optional (Inject on keyword match)
|
{formatMessage({ id: 'specs.readMode.optional', defaultMessage: 'Optional' })}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
The default read mode for newly created personal specs
|
{formatMessage({ id: 'specs.settings.defaultReadModeHelp', defaultMessage: 'The default read mode for newly created personal specs' })}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Auto Enable */}
|
{/* Auto Enable */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
<Label htmlFor="auto-enable">Auto Enable New Specs</Label>
|
<Label htmlFor="auto-enable">
|
||||||
|
{formatMessage({ id: 'specs.settings.autoEnable', defaultMessage: 'Auto Enable New Specs' })}
|
||||||
|
</Label>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
Automatically enable newly created personal specs
|
{formatMessage({ id: 'specs.settings.autoEnableDescription', defaultMessage: 'Automatically enable newly created personal specs' })}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Switch
|
<Switch
|
||||||
@@ -262,7 +267,9 @@ export function GlobalSettingsTab() {
|
|||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<CardTitle>Spec Statistics</CardTitle>
|
<CardTitle>
|
||||||
|
{formatMessage({ id: 'specs.settings.specStatistics', defaultMessage: 'Spec Statistics' })}
|
||||||
|
</CardTitle>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -280,7 +287,7 @@ export function GlobalSettingsTab() {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="grid grid-cols-4 gap-4">
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||||
{[1, 2, 3, 4].map((i) => (
|
{[1, 2, 3, 4].map((i) => (
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={i}
|
||||||
@@ -293,11 +300,11 @@ export function GlobalSettingsTab() {
|
|||||||
</div>
|
</div>
|
||||||
) : hasError ? (
|
) : hasError ? (
|
||||||
<div className="text-center py-8 text-muted-foreground">
|
<div className="text-center py-8 text-muted-foreground">
|
||||||
Failed to load statistics
|
{formatMessage({ id: 'specs.injection.loadError', defaultMessage: 'Failed to load statistics' })}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className="grid grid-cols-4 gap-4">
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||||
{dimensionEntries.map(([dim, data]) => (
|
{dimensionEntries.map(([dim, data]) => (
|
||||||
<div
|
<div
|
||||||
key={dim}
|
key={dim}
|
||||||
@@ -306,11 +313,11 @@ export function GlobalSettingsTab() {
|
|||||||
<div className="text-2xl font-bold text-foreground">
|
<div className="text-2xl font-bold text-foreground">
|
||||||
{data.count}
|
{data.count}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm text-muted-foreground capitalize">
|
<div className="text-sm text-muted-foreground">
|
||||||
{dimensionLabels[dim] || dim}
|
{formatMessage({ id: `specs.dimension.${dim}`, defaultMessage: dim })}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-muted-foreground mt-1">
|
<div className="text-xs text-muted-foreground mt-1">
|
||||||
{data.requiredCount} required
|
{data.requiredCount} {formatMessage({ id: 'specs.required', defaultMessage: 'required' })}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -319,10 +326,15 @@ export function GlobalSettingsTab() {
|
|||||||
{/* Summary */}
|
{/* Summary */}
|
||||||
<div className="mt-4 pt-4 border-t border-border">
|
<div className="mt-4 pt-4 border-t border-border">
|
||||||
<div className="flex justify-between text-sm text-muted-foreground">
|
<div className="flex justify-between text-sm text-muted-foreground">
|
||||||
<span>Total: {totalCount} spec files</span>
|
|
||||||
<span>
|
<span>
|
||||||
{totalRequired} required | {totalCount - totalRequired}{' '}
|
{formatMessage(
|
||||||
optional
|
{ id: 'specs.settings.totalSpecs', defaultMessage: 'Total: {count} spec files' },
|
||||||
|
{ count: totalCount }
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
{totalRequired} {formatMessage({ id: 'specs.readMode.required', defaultMessage: 'required' })} | {totalCount - totalRequired}{' '}
|
||||||
|
{formatMessage({ id: 'specs.readMode.optional', defaultMessage: 'optional' })}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,6 +8,12 @@ import { useIntl } from 'react-intl';
|
|||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@/components/ui/Dialog';
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
@@ -20,6 +26,7 @@ import { Label } from '@/components/ui/Label';
|
|||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
import { Switch } from '@/components/ui/Switch';
|
import { Switch } from '@/components/ui/Switch';
|
||||||
import { Progress } from '@/components/ui/Progress';
|
import { Progress } from '@/components/ui/Progress';
|
||||||
|
import { Badge } from '@/components/ui/Badge';
|
||||||
import {
|
import {
|
||||||
AlertCircle,
|
AlertCircle,
|
||||||
Info,
|
Info,
|
||||||
@@ -30,8 +37,16 @@ import {
|
|||||||
Download,
|
Download,
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
Settings,
|
Settings,
|
||||||
|
FileText,
|
||||||
|
Eye,
|
||||||
|
Globe,
|
||||||
|
Folder,
|
||||||
|
ChevronDown,
|
||||||
|
ChevronRight,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { useInstallRecommendedHooks } from '@/hooks/useSystemSettings';
|
import { useInstallRecommendedHooks } from '@/hooks/useSystemSettings';
|
||||||
|
import type { InjectionPreviewFile, InjectionPreviewResponse } from '@/lib/api';
|
||||||
|
import { getInjectionPreview } from '@/lib/api';
|
||||||
|
|
||||||
// ========== Types ==========
|
// ========== Types ==========
|
||||||
|
|
||||||
@@ -167,6 +182,17 @@ export function InjectionControlTab({ className }: InjectionControlTabProps) {
|
|||||||
// State for hooks installation
|
// State for hooks installation
|
||||||
const [installingHookIds, setInstallingHookIds] = useState<string[]>([]);
|
const [installingHookIds, setInstallingHookIds] = useState<string[]>([]);
|
||||||
|
|
||||||
|
// State for injection preview
|
||||||
|
const [previewMode, setPreviewMode] = useState<'required' | 'all'>('required');
|
||||||
|
const [previewData, setPreviewData] = useState<InjectionPreviewResponse | null>(null);
|
||||||
|
const [previewLoading, setPreviewLoading] = useState(false);
|
||||||
|
const [expandedDimensions, setExpandedDimensions] = useState<Record<string, boolean>>({
|
||||||
|
specs: true,
|
||||||
|
personal: true,
|
||||||
|
});
|
||||||
|
const [previewDialogOpen, setPreviewDialogOpen] = useState(false);
|
||||||
|
const [previewFile, setPreviewFile] = useState<InjectionPreviewFile | null>(null);
|
||||||
|
|
||||||
// Fetch stats
|
// Fetch stats
|
||||||
const loadStats = useCallback(async () => {
|
const loadStats = useCallback(async () => {
|
||||||
setStatsLoading(true);
|
setStatsLoading(true);
|
||||||
@@ -195,12 +221,44 @@ export function InjectionControlTab({ className }: InjectionControlTabProps) {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Load injection preview
|
||||||
|
const loadPreview = useCallback(async () => {
|
||||||
|
setPreviewLoading(true);
|
||||||
|
try {
|
||||||
|
const data = await getInjectionPreview(previewMode, false);
|
||||||
|
setPreviewData(data);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to load injection preview:', err);
|
||||||
|
} finally {
|
||||||
|
setPreviewLoading(false);
|
||||||
|
}
|
||||||
|
}, [previewMode]);
|
||||||
|
|
||||||
|
// Load file content for preview
|
||||||
|
const loadFilePreview = useCallback(async (file: InjectionPreviewFile) => {
|
||||||
|
try {
|
||||||
|
const data = await getInjectionPreview(previewMode, true);
|
||||||
|
const fileWithData = data.files.find(f => f.file === file.file);
|
||||||
|
if (fileWithData) {
|
||||||
|
setPreviewFile(fileWithData);
|
||||||
|
setPreviewDialogOpen(true);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to load file preview:', err);
|
||||||
|
}
|
||||||
|
}, [previewMode]);
|
||||||
|
|
||||||
// Initial load
|
// Initial load
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadStats();
|
loadStats();
|
||||||
loadSettings();
|
loadSettings();
|
||||||
}, [loadStats, loadSettings]);
|
}, [loadStats, loadSettings]);
|
||||||
|
|
||||||
|
// Load preview when mode changes
|
||||||
|
useEffect(() => {
|
||||||
|
loadPreview();
|
||||||
|
}, [loadPreview]);
|
||||||
|
|
||||||
// Check for changes
|
// Check for changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const changed =
|
const changed =
|
||||||
@@ -245,20 +303,21 @@ export function InjectionControlTab({ className }: InjectionControlTabProps) {
|
|||||||
setHasChanges(false);
|
setHasChanges(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Toggle dimension expansion
|
||||||
|
const toggleDimension = (dim: string) => {
|
||||||
|
setExpandedDimensions(prev => ({ ...prev, [dim]: !prev[dim] }));
|
||||||
|
};
|
||||||
|
|
||||||
// ========== Hooks Installation ==========
|
// ========== Hooks Installation ==========
|
||||||
|
|
||||||
// Get installed hooks from system settings
|
|
||||||
const installedHookIds = useMemo(() => {
|
const installedHookIds = useMemo(() => {
|
||||||
const installed = new Set<string>();
|
const installed = new Set<string>();
|
||||||
// Check if hooks are already installed by checking system settings
|
|
||||||
// For now, we'll track this via the mutation result
|
|
||||||
return installed;
|
return installed;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const installedCount = 0; // Will be updated when we have real data
|
const installedCount = 0;
|
||||||
const allHooksInstalled = installedCount === RECOMMENDED_HOOKS.length;
|
const allHooksInstalled = installedCount === RECOMMENDED_HOOKS.length;
|
||||||
|
|
||||||
// Install single hook
|
|
||||||
const handleInstallHook = useCallback(async (hookId: string) => {
|
const handleInstallHook = useCallback(async (hookId: string) => {
|
||||||
setInstallingHookIds(prev => [...prev, hookId]);
|
setInstallingHookIds(prev => [...prev, hookId]);
|
||||||
try {
|
try {
|
||||||
@@ -276,7 +335,6 @@ export function InjectionControlTab({ className }: InjectionControlTabProps) {
|
|||||||
}
|
}
|
||||||
}, [installHooksMutation, formatMessage]);
|
}, [installHooksMutation, formatMessage]);
|
||||||
|
|
||||||
// Install all hooks
|
|
||||||
const handleInstallAllHooks = useCallback(async () => {
|
const handleInstallAllHooks = useCallback(async () => {
|
||||||
const uninstalledHooks = RECOMMENDED_HOOKS.filter(h => !installedHookIds.has(h.id));
|
const uninstalledHooks = RECOMMENDED_HOOKS.filter(h => !installedHookIds.has(h.id));
|
||||||
if (uninstalledHooks.length === 0) return;
|
if (uninstalledHooks.length === 0) return;
|
||||||
@@ -300,6 +358,19 @@ export function InjectionControlTab({ className }: InjectionControlTabProps) {
|
|||||||
}
|
}
|
||||||
}, [installedHookIds, installHooksMutation, formatMessage]);
|
}, [installedHookIds, installHooksMutation, formatMessage]);
|
||||||
|
|
||||||
|
// Group files by dimension
|
||||||
|
const filesByDimension = useMemo(() => {
|
||||||
|
if (!previewData) return {};
|
||||||
|
const grouped: Record<string, InjectionPreviewFile[]> = {};
|
||||||
|
for (const file of previewData.files) {
|
||||||
|
if (!grouped[file.dimension]) {
|
||||||
|
grouped[file.dimension] = [];
|
||||||
|
}
|
||||||
|
grouped[file.dimension].push(file);
|
||||||
|
}
|
||||||
|
return grouped;
|
||||||
|
}, [previewData]);
|
||||||
|
|
||||||
// Calculate progress and status
|
// Calculate progress and status
|
||||||
const currentLength = stats?.injectionLength?.withKeywords || 0;
|
const currentLength = stats?.injectionLength?.withKeywords || 0;
|
||||||
const maxLength = settings.maxLength;
|
const maxLength = settings.maxLength;
|
||||||
@@ -416,7 +487,7 @@ export function InjectionControlTab({ className }: InjectionControlTabProps) {
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="h-6 w-6"
|
className="h-6 w-6"
|
||||||
onClick={loadStats}
|
onClick={() => { loadStats(); loadPreview(); }}
|
||||||
disabled={statsLoading}
|
disabled={statsLoading}
|
||||||
>
|
>
|
||||||
<RefreshCw className={cn('h-4 w-4', statsLoading && 'animate-spin')} />
|
<RefreshCw className={cn('h-4 w-4', statsLoading && 'animate-spin')} />
|
||||||
@@ -537,6 +608,116 @@ export function InjectionControlTab({ className }: InjectionControlTabProps) {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
{/* Injection Files List Card */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<FileText className="h-5 w-5" />
|
||||||
|
{formatMessage({ id: 'specs.injection.filesList', defaultMessage: 'Injection Files' })}
|
||||||
|
</CardTitle>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button
|
||||||
|
variant={previewMode === 'required' ? 'default' : 'outline'}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setPreviewMode('required')}
|
||||||
|
>
|
||||||
|
{formatMessage({ id: 'specs.readMode.required', defaultMessage: 'Required' })}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant={previewMode === 'all' ? 'default' : 'outline'}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setPreviewMode('all')}
|
||||||
|
>
|
||||||
|
{formatMessage({ id: 'specs.scope.all', defaultMessage: 'All' })}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<CardDescription>
|
||||||
|
{previewData && (
|
||||||
|
<span>
|
||||||
|
{previewData.stats.count} {formatMessage({ id: 'specs.injection.files', defaultMessage: 'files' })} • {formatNumber(previewData.stats.totalLength)} {formatMessage({ id: 'specs.injection.characters', defaultMessage: 'characters' })}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{previewLoading ? (
|
||||||
|
<div className="flex items-center justify-center py-8">
|
||||||
|
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="h-[300px] overflow-auto">
|
||||||
|
<div className="space-y-2">
|
||||||
|
{Object.entries(filesByDimension).map(([dim, files]) => (
|
||||||
|
<div key={dim} className="border rounded-lg">
|
||||||
|
<button
|
||||||
|
className="w-full flex items-center justify-between p-3 hover:bg-muted/50 transition-colors"
|
||||||
|
onClick={() => toggleDimension(dim)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{expandedDimensions[dim] ? (
|
||||||
|
<ChevronDown className="h-4 w-4" />
|
||||||
|
) : (
|
||||||
|
<ChevronRight className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
<span className="font-medium capitalize">
|
||||||
|
{formatMessage({ id: `specs.dimension.${dim}`, defaultMessage: dim })}
|
||||||
|
</span>
|
||||||
|
<Badge variant="secondary" className="text-xs">
|
||||||
|
{files.length}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
{formatNumber(files.reduce((sum, f) => sum + f.contentLength, 0))} {formatMessage({ id: 'specs.injection.characters', defaultMessage: 'chars' })}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
{expandedDimensions[dim] && (
|
||||||
|
<div className="border-t">
|
||||||
|
{files.map((file) => (
|
||||||
|
<div
|
||||||
|
key={file.file}
|
||||||
|
className="flex items-center justify-between p-3 border-b last:border-b-0 hover:bg-muted/30"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3 min-w-0">
|
||||||
|
{file.scope === 'global' ? (
|
||||||
|
<Globe className="h-4 w-4 text-blue-500 flex-shrink-0" />
|
||||||
|
) : (
|
||||||
|
<Folder className="h-4 w-4 text-green-500 flex-shrink-0" />
|
||||||
|
)}
|
||||||
|
<div className="min-w-0">
|
||||||
|
<div className="font-medium truncate">{file.title}</div>
|
||||||
|
<div className="text-xs text-muted-foreground truncate">{file.file}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Badge variant="outline" className="text-xs">
|
||||||
|
{formatMessage({ id: `specs.priority.${file.priority}`, defaultMessage: file.priority })}
|
||||||
|
</Badge>
|
||||||
|
<span className="text-xs text-muted-foreground whitespace-nowrap">
|
||||||
|
{formatNumber(file.contentLength)}
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-7 w-7"
|
||||||
|
onClick={() => loadFilePreview(file)}
|
||||||
|
>
|
||||||
|
<Eye className="h-3.5 w-3.5" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
{/* Settings Card */}
|
{/* Settings Card */}
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
@@ -645,6 +826,23 @@ export function InjectionControlTab({ className }: InjectionControlTabProps) {
|
|||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
{/* File Preview Dialog */}
|
||||||
|
<Dialog open={previewDialogOpen} onOpenChange={setPreviewDialogOpen}>
|
||||||
|
<DialogContent className="max-w-3xl max-h-[80vh]">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="flex items-center gap-2">
|
||||||
|
<FileText className="h-5 w-5" />
|
||||||
|
{previewFile?.title}
|
||||||
|
</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="flex-1 overflow-auto">
|
||||||
|
<pre className="text-sm whitespace-pre-wrap p-4 bg-muted rounded-lg">
|
||||||
|
{previewFile?.content || formatMessage({ id: 'specs.content.noContent', defaultMessage: 'No content available' })}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7273,6 +7273,8 @@ export interface SpecEntry {
|
|||||||
priority: 'critical' | 'high' | 'medium' | 'low';
|
priority: 'critical' | 'high' | 'medium' | 'low';
|
||||||
keywords: string[];
|
keywords: string[];
|
||||||
scope: 'global' | 'project';
|
scope: 'global' | 'project';
|
||||||
|
/** Content length (body only, cached for performance) */
|
||||||
|
contentLength: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -7305,6 +7307,54 @@ export async function rebuildSpecIndex(projectPath?: string): Promise<{ success:
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injection preview file info
|
||||||
|
*/
|
||||||
|
export interface InjectionPreviewFile {
|
||||||
|
file: string;
|
||||||
|
title: string;
|
||||||
|
dimension: string;
|
||||||
|
category: string;
|
||||||
|
scope: string;
|
||||||
|
readMode: string;
|
||||||
|
priority: string;
|
||||||
|
contentLength: number;
|
||||||
|
content?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injection preview response
|
||||||
|
*/
|
||||||
|
export interface InjectionPreviewResponse {
|
||||||
|
files: InjectionPreviewFile[];
|
||||||
|
stats: {
|
||||||
|
count: number;
|
||||||
|
totalLength: number;
|
||||||
|
maxLength: number;
|
||||||
|
percentage: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get injection preview with file list
|
||||||
|
* @param mode - 'required' | 'all' | 'keywords'
|
||||||
|
* @param preview - Include content preview
|
||||||
|
* @param projectPath - Optional project path
|
||||||
|
*/
|
||||||
|
export async function getInjectionPreview(
|
||||||
|
mode: 'required' | 'all' | 'keywords' = 'required',
|
||||||
|
preview: boolean = false,
|
||||||
|
projectPath?: string
|
||||||
|
): Promise<InjectionPreviewResponse> {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
params.set('mode', mode);
|
||||||
|
params.set('preview', String(preview));
|
||||||
|
if (projectPath) {
|
||||||
|
params.set('path', projectPath);
|
||||||
|
}
|
||||||
|
return fetchApi<InjectionPreviewResponse>(`/api/specs/injection-preview?${params.toString()}`);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update spec frontmatter (toggle readMode)
|
* Update spec frontmatter (toggle readMode)
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -14,7 +14,9 @@
|
|||||||
|
|
||||||
"dimension": {
|
"dimension": {
|
||||||
"specs": "Project Specs",
|
"specs": "Project Specs",
|
||||||
"personal": "Personal"
|
"personal": "Personal",
|
||||||
|
"roadmap": "Roadmap",
|
||||||
|
"changelog": "Changelog"
|
||||||
},
|
},
|
||||||
|
|
||||||
"scope": {
|
"scope": {
|
||||||
@@ -23,8 +25,10 @@
|
|||||||
"project": "Project"
|
"project": "Project"
|
||||||
},
|
},
|
||||||
"filterByScope": "Filter by scope:",
|
"filterByScope": "Filter by scope:",
|
||||||
|
"filterByCategory": "Workflow stage:",
|
||||||
|
|
||||||
"category": {
|
"category": {
|
||||||
|
"all": "All",
|
||||||
"general": "General",
|
"general": "General",
|
||||||
"exploration": "Exploration",
|
"exploration": "Exploration",
|
||||||
"planning": "Planning",
|
"planning": "Planning",
|
||||||
@@ -43,11 +47,38 @@
|
|||||||
"install": "Install",
|
"install": "Install",
|
||||||
"installed": "Installed",
|
"installed": "Installed",
|
||||||
"installing": "Installing...",
|
"installing": "Installing...",
|
||||||
|
|
||||||
"installedHooks": "Installed Hooks",
|
"installedHooks": "Installed Hooks",
|
||||||
"installedHooksDesc": "Manage your installed hooks configuration",
|
"installedHooksDesc": "Manage your installed hooks configuration",
|
||||||
"searchHooks": "Search hooks...",
|
"searchHooks": "Search hooks...",
|
||||||
"noHooks": "No hooks installed. Install recommended hooks above.",
|
"noHooks": "No hooks installed. Install recommended hooks above.",
|
||||||
|
|
||||||
|
"actions": {
|
||||||
|
"view": "View Content",
|
||||||
|
"edit": "Edit",
|
||||||
|
"delete": "Delete",
|
||||||
|
"reset": "Reset",
|
||||||
|
"save": "Save",
|
||||||
|
"saving": "Saving..."
|
||||||
|
},
|
||||||
|
|
||||||
|
"status": {
|
||||||
|
"enabled": "Enabled",
|
||||||
|
"disabled": "Disabled"
|
||||||
|
},
|
||||||
|
|
||||||
|
"readMode": {
|
||||||
|
"required": "Required",
|
||||||
|
"optional": "Optional"
|
||||||
|
},
|
||||||
|
|
||||||
|
"priority": {
|
||||||
|
"critical": "Critical",
|
||||||
|
"high": "High",
|
||||||
|
"medium": "Medium",
|
||||||
|
"low": "Low"
|
||||||
|
},
|
||||||
|
|
||||||
"spec": {
|
"spec": {
|
||||||
"edit": "Edit Spec",
|
"edit": "Edit Spec",
|
||||||
"toggle": "Toggle Status",
|
"toggle": "Toggle Status",
|
||||||
@@ -91,37 +122,11 @@
|
|||||||
"failModeWarn": "Warn"
|
"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": {
|
"hooks": {
|
||||||
"dialog": {
|
"dialog": {
|
||||||
"createTitle": "Create Hook",
|
"createTitle": "Create Hook",
|
||||||
"editTitle": "Edit Hook",
|
"editTitle": "Edit Hook",
|
||||||
"description": "Configure the hook trigger event, command, and other settings."
|
"description": "Configure hook trigger event, command, and other settings."
|
||||||
},
|
},
|
||||||
"fields": {
|
"fields": {
|
||||||
"name": "Hook Name",
|
"name": "Hook Name",
|
||||||
@@ -149,9 +154,9 @@
|
|||||||
"project": "Project"
|
"project": "Project"
|
||||||
},
|
},
|
||||||
"failModes": {
|
"failModes": {
|
||||||
"continue": "Continue",
|
"continue": "Continue execution",
|
||||||
"warn": "Show Warning",
|
"warn": "Show warning",
|
||||||
"block": "Block Operation"
|
"block": "Block operation"
|
||||||
},
|
},
|
||||||
"validation": {
|
"validation": {
|
||||||
"nameRequired": "Name is required",
|
"nameRequired": "Name is required",
|
||||||
@@ -185,7 +190,7 @@
|
|||||||
"metadata": "Metadata",
|
"metadata": "Metadata",
|
||||||
"markdownContent": "Markdown Content",
|
"markdownContent": "Markdown Content",
|
||||||
"noContent": "No content available",
|
"noContent": "No content available",
|
||||||
"editHint": "Edit the full markdown content including frontmatter. Changes to frontmatter will be reflected in the spec metadata.",
|
"editHint": "Edit the full markdown content including frontmatter. Changes to frontmatter will be reflected in spec metadata.",
|
||||||
"placeholder": "# Spec Title\n\nContent here..."
|
"placeholder": "# Spec Title\n\nContent here..."
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -221,10 +226,14 @@
|
|||||||
"title": "Global Settings",
|
"title": "Global Settings",
|
||||||
"description": "Configure personal spec defaults and system settings",
|
"description": "Configure personal spec defaults and system settings",
|
||||||
"personalSpecDefaults": "Personal Spec Defaults",
|
"personalSpecDefaults": "Personal Spec Defaults",
|
||||||
|
"personalSpecDefaultsDesc": "These settings will be applied when creating new personal specs",
|
||||||
"defaultReadMode": "Default Read Mode",
|
"defaultReadMode": "Default Read Mode",
|
||||||
"defaultReadModeHelp": "Default read mode for newly created personal specs",
|
"defaultReadModeHelp": "The default read mode for newly created personal specs",
|
||||||
"autoEnable": "Auto Enable",
|
"selectReadMode": "Select read mode",
|
||||||
"autoEnableDescription": "Automatically enable newly created personal specs"
|
"autoEnable": "Auto Enable New Specs",
|
||||||
|
"autoEnableDescription": "Automatically enable newly created personal specs",
|
||||||
|
"specStatistics": "Spec Statistics",
|
||||||
|
"totalSpecs": "Total: {count} spec files"
|
||||||
},
|
},
|
||||||
|
|
||||||
"dialog": {
|
"dialog": {
|
||||||
|
|||||||
@@ -14,7 +14,9 @@
|
|||||||
|
|
||||||
"dimension": {
|
"dimension": {
|
||||||
"specs": "项目规范",
|
"specs": "项目规范",
|
||||||
"personal": "个人规范"
|
"personal": "个人规范",
|
||||||
|
"roadmap": "路线图",
|
||||||
|
"changelog": "变更日志"
|
||||||
},
|
},
|
||||||
|
|
||||||
"scope": {
|
"scope": {
|
||||||
@@ -23,8 +25,10 @@
|
|||||||
"project": "项目"
|
"project": "项目"
|
||||||
},
|
},
|
||||||
"filterByScope": "按范围筛选:",
|
"filterByScope": "按范围筛选:",
|
||||||
|
"filterByCategory": "工作流阶段:",
|
||||||
|
|
||||||
"category": {
|
"category": {
|
||||||
|
"all": "全部",
|
||||||
"general": "通用",
|
"general": "通用",
|
||||||
"exploration": "探索",
|
"exploration": "探索",
|
||||||
"planning": "规划",
|
"planning": "规划",
|
||||||
@@ -76,7 +80,6 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
"spec": {
|
"spec": {
|
||||||
"view": "查看内容",
|
|
||||||
"edit": "编辑规范",
|
"edit": "编辑规范",
|
||||||
"toggle": "切换状态",
|
"toggle": "切换状态",
|
||||||
"delete": "删除规范",
|
"delete": "删除规范",
|
||||||
@@ -89,23 +92,6 @@
|
|||||||
"file": "文件路径"
|
"file": "文件路径"
|
||||||
},
|
},
|
||||||
|
|
||||||
"content": {
|
|
||||||
"edit": "编辑",
|
|
||||||
"view": "查看",
|
|
||||||
"metadata": "元数据",
|
|
||||||
"markdownContent": "Markdown 内容",
|
|
||||||
"noContent": "无内容",
|
|
||||||
"editHint": "编辑完整的 Markdown 内容(包括 frontmatter)。frontmatter 的更改将反映到规范元数据中。",
|
|
||||||
"placeholder": "# 规范标题\n\n内容..."
|
|
||||||
},
|
|
||||||
|
|
||||||
"common": {
|
|
||||||
"cancel": "取消",
|
|
||||||
"save": "保存",
|
|
||||||
"saving": "保存中...",
|
|
||||||
"close": "关闭"
|
|
||||||
},
|
|
||||||
|
|
||||||
"hook": {
|
"hook": {
|
||||||
"install": "安装",
|
"install": "安装",
|
||||||
"uninstall": "卸载",
|
"uninstall": "卸载",
|
||||||
@@ -137,9 +123,6 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
"hooks": {
|
"hooks": {
|
||||||
"installSuccess": "钩子安装成功",
|
|
||||||
"installError": "钩子安装失败",
|
|
||||||
"installAllSuccess": "所有钩子安装成功",
|
|
||||||
"dialog": {
|
"dialog": {
|
||||||
"createTitle": "创建钩子",
|
"createTitle": "创建钩子",
|
||||||
"editTitle": "编辑钩子",
|
"editTitle": "编辑钩子",
|
||||||
@@ -191,6 +174,26 @@
|
|||||||
"hookFailMode": "命令执行失败时的处理方式"
|
"hookFailMode": "命令执行失败时的处理方式"
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"common": {
|
||||||
|
"cancel": "取消",
|
||||||
|
"save": "保存",
|
||||||
|
"delete": "删除",
|
||||||
|
"edit": "编辑",
|
||||||
|
"reset": "重置",
|
||||||
|
"confirm": "确认",
|
||||||
|
"close": "关闭"
|
||||||
|
},
|
||||||
|
|
||||||
|
"content": {
|
||||||
|
"edit": "编辑",
|
||||||
|
"view": "查看",
|
||||||
|
"metadata": "元数据",
|
||||||
|
"markdownContent": "Markdown 内容",
|
||||||
|
"noContent": "无可用内容",
|
||||||
|
"editHint": "编辑完整的 markdown 内容包括 frontmatter。对 frontmatter 的更改将反映在规范元数据中。",
|
||||||
|
"placeholder": "# 规范标题\n\n内容在这里..."
|
||||||
|
},
|
||||||
|
|
||||||
"injection": {
|
"injection": {
|
||||||
"title": "注入控制",
|
"title": "注入控制",
|
||||||
"statusTitle": "当前注入状态",
|
"statusTitle": "当前注入状态",
|
||||||
@@ -210,23 +213,37 @@
|
|||||||
"warning": "接近限制",
|
"warning": "接近限制",
|
||||||
"normal": "正常",
|
"normal": "正常",
|
||||||
"characters": "字符",
|
"characters": "字符",
|
||||||
|
"chars": "字符",
|
||||||
"statsInfo": "统计信息",
|
"statsInfo": "统计信息",
|
||||||
"requiredLength": "必读规范长度:",
|
"requiredLength": "必读规范长度:",
|
||||||
"matchedLength": "关键词匹配长度:",
|
"matchedLength": "关键词匹配长度:",
|
||||||
"remaining": "剩余空间:",
|
"remaining": "剩余空间:",
|
||||||
"loadError": "加载统计数据失败",
|
"loadError": "加载统计数据失败",
|
||||||
"saveSuccess": "设置已保存",
|
"saveSuccess": "设置已保存",
|
||||||
"saveError": "保存设置失败"
|
"saveError": "保存设置失败",
|
||||||
|
"filesList": "注入文件列表",
|
||||||
|
"files": "个文件"
|
||||||
|
},
|
||||||
|
|
||||||
|
"priority": {
|
||||||
|
"critical": "关键",
|
||||||
|
"high": "高",
|
||||||
|
"medium": "中",
|
||||||
|
"low": "低"
|
||||||
},
|
},
|
||||||
|
|
||||||
"settings": {
|
"settings": {
|
||||||
"title": "全局设置",
|
"title": "全局设置",
|
||||||
"description": "配置个人规范默认值和系统设置",
|
"description": "配置个人规范默认值和系统设置",
|
||||||
"personalSpecDefaults": "个人规范默认值",
|
"personalSpecDefaults": "个人规范默认值",
|
||||||
|
"personalSpecDefaultsDesc": "创建新的个人规范时将应用这些设置",
|
||||||
"defaultReadMode": "默认读取模式",
|
"defaultReadMode": "默认读取模式",
|
||||||
"defaultReadModeHelp": "新创建的个人规范的默认读取模式",
|
"defaultReadModeHelp": "新创建的个人规范的默认读取模式",
|
||||||
"autoEnable": "自动启用",
|
"selectReadMode": "选择读取模式",
|
||||||
"autoEnableDescription": "新创建的个人规范自动启用"
|
"autoEnable": "自动启用新规范",
|
||||||
|
"autoEnableDescription": "自动启用新创建的个人规范",
|
||||||
|
"specStatistics": "规范统计",
|
||||||
|
"totalSpecs": "总计:{count} 个规范文件"
|
||||||
},
|
},
|
||||||
|
|
||||||
"dialog": {
|
"dialog": {
|
||||||
|
|||||||
@@ -303,6 +303,7 @@ export function run(argv: string[]): void {
|
|||||||
.command('spec [subcommand] [args...]')
|
.command('spec [subcommand] [args...]')
|
||||||
.description('Project spec management for conventions and guidelines')
|
.description('Project spec management for conventions and guidelines')
|
||||||
.option('--dimension <dim>', 'Target dimension: specs, personal')
|
.option('--dimension <dim>', 'Target dimension: specs, personal')
|
||||||
|
.option('--category <cat>', 'Workflow stage: general, exploration, planning, execution')
|
||||||
.option('--keywords <text>', 'Keywords for spec matching (CLI mode)')
|
.option('--keywords <text>', 'Keywords for spec matching (CLI mode)')
|
||||||
.option('--stdin', 'Read input from stdin (Hook mode)')
|
.option('--stdin', 'Read input from stdin (Hook mode)')
|
||||||
.option('--json', 'Output as JSON')
|
.option('--json', 'Output as JSON')
|
||||||
@@ -374,3 +375,6 @@ export function run(argv: string[]): void {
|
|||||||
|
|
||||||
program.parse(argv);
|
program.parse(argv);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Invoke CLI when run directly
|
||||||
|
run(process.argv);
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import chalk from 'chalk';
|
|||||||
|
|
||||||
interface SpecOptions {
|
interface SpecOptions {
|
||||||
dimension?: string;
|
dimension?: string;
|
||||||
|
category?: string;
|
||||||
keywords?: string;
|
keywords?: string;
|
||||||
stdin?: boolean;
|
stdin?: boolean;
|
||||||
json?: boolean;
|
json?: boolean;
|
||||||
@@ -58,13 +59,13 @@ function getProjectPath(hookCwd?: string): string {
|
|||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load action - load specs matching dimension/keywords.
|
* Load action - load specs matching dimension/category/keywords.
|
||||||
*
|
*
|
||||||
* CLI mode: --dimension and --keywords options, outputs formatted markdown.
|
* CLI mode: --dimension, --category, --keywords options, outputs formatted markdown.
|
||||||
* Hook mode: --stdin reads JSON {session_id, cwd, user_prompt}, outputs JSON {continue, systemMessage}.
|
* Hook mode: --stdin reads JSON {session_id, cwd, user_prompt}, outputs JSON {continue, systemMessage}.
|
||||||
*/
|
*/
|
||||||
async function loadAction(options: SpecOptions): Promise<void> {
|
async function loadAction(options: SpecOptions): Promise<void> {
|
||||||
const { stdin, dimension, keywords: keywordsInput } = options;
|
const { stdin, dimension, category, keywords: keywordsInput } = options;
|
||||||
let projectPath: string;
|
let projectPath: string;
|
||||||
let stdinData: StdinData | undefined;
|
let stdinData: StdinData | undefined;
|
||||||
|
|
||||||
@@ -96,6 +97,7 @@ async function loadAction(options: SpecOptions): Promise<void> {
|
|||||||
const result = await loadSpecs({
|
const result = await loadSpecs({
|
||||||
projectPath,
|
projectPath,
|
||||||
dimension: dimension as 'specs' | 'personal' | undefined,
|
dimension: dimension as 'specs' | 'personal' | undefined,
|
||||||
|
category: category as 'general' | 'exploration' | 'planning' | 'execution' | undefined,
|
||||||
keywords,
|
keywords,
|
||||||
outputFormat: stdin ? 'hook' : 'cli',
|
outputFormat: stdin ? 'hook' : 'cli',
|
||||||
stdinData,
|
stdinData,
|
||||||
|
|||||||
@@ -151,7 +151,7 @@ export async function handleSpecRoutes(ctx: RouteContext): Promise<boolean> {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// API: Get spec stats (dimensions count + injection length info)
|
// API: Get spec stats (optimized - uses cached contentLength)
|
||||||
if (pathname === '/api/specs/stats' && req.method === 'GET') {
|
if (pathname === '/api/specs/stats' && req.method === 'GET') {
|
||||||
const projectPath = url.searchParams.get('path') || initialPath;
|
const projectPath = url.searchParams.get('path') || initialPath;
|
||||||
const resolvedPath = resolvePath(projectPath);
|
const resolvedPath = resolvePath(projectPath);
|
||||||
@@ -186,18 +186,8 @@ export async function handleSpecRoutes(ctx: RouteContext): Promise<boolean> {
|
|||||||
|
|
||||||
for (const entry of index.entries) {
|
for (const entry of index.entries) {
|
||||||
count++;
|
count++;
|
||||||
// Calculate content length by reading the file
|
// Use cached contentLength instead of re-reading file
|
||||||
const filePath = join(resolvedPath, entry.file);
|
const contentLength = entry.contentLength || 0;
|
||||||
let contentLength = 0;
|
|
||||||
try {
|
|
||||||
if (existsSync(filePath)) {
|
|
||||||
const rawContent = readFileSync(filePath, 'utf-8');
|
|
||||||
// Strip frontmatter to get actual content length
|
|
||||||
const matter = (await import('gray-matter')).default;
|
|
||||||
const parsed = matter(rawContent);
|
|
||||||
contentLength = parsed.content.length;
|
|
||||||
}
|
|
||||||
} catch { /* ignore */ }
|
|
||||||
|
|
||||||
if (entry.readMode === 'required') {
|
if (entry.readMode === 'required') {
|
||||||
requiredCount++;
|
requiredCount++;
|
||||||
@@ -228,5 +218,109 @@ export async function handleSpecRoutes(ctx: RouteContext): Promise<boolean> {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// API: Get injection preview (files list and content preview)
|
||||||
|
if (pathname === '/api/specs/injection-preview' && req.method === 'GET') {
|
||||||
|
const projectPath = url.searchParams.get('path') || initialPath;
|
||||||
|
const resolvedPath = resolvePath(projectPath);
|
||||||
|
const mode = url.searchParams.get('mode') || 'required'; // required | all | keywords
|
||||||
|
const preview = url.searchParams.get('preview') === 'true';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { getDimensionIndex, SPEC_DIMENSIONS } = await import(
|
||||||
|
'../../tools/spec-index-builder.js'
|
||||||
|
);
|
||||||
|
|
||||||
|
interface InjectionFile {
|
||||||
|
file: string;
|
||||||
|
title: string;
|
||||||
|
dimension: string;
|
||||||
|
category: string;
|
||||||
|
scope: string;
|
||||||
|
readMode: string;
|
||||||
|
priority: string;
|
||||||
|
contentLength: number;
|
||||||
|
content?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const files: InjectionFile[] = [];
|
||||||
|
let totalLength = 0;
|
||||||
|
|
||||||
|
for (const dim of SPEC_DIMENSIONS) {
|
||||||
|
const index = await getDimensionIndex(resolvedPath, dim);
|
||||||
|
|
||||||
|
for (const entry of index.entries) {
|
||||||
|
// Filter by mode
|
||||||
|
if (mode === 'required' && entry.readMode !== 'required') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileData: InjectionFile = {
|
||||||
|
file: entry.file,
|
||||||
|
title: entry.title,
|
||||||
|
dimension: entry.dimension,
|
||||||
|
category: entry.category || 'general',
|
||||||
|
scope: entry.scope,
|
||||||
|
readMode: entry.readMode,
|
||||||
|
priority: entry.priority,
|
||||||
|
contentLength: entry.contentLength || 0
|
||||||
|
};
|
||||||
|
|
||||||
|
// Include content if preview requested
|
||||||
|
if (preview) {
|
||||||
|
const filePath = join(resolvedPath, entry.file);
|
||||||
|
if (existsSync(filePath)) {
|
||||||
|
try {
|
||||||
|
const rawContent = readFileSync(filePath, 'utf-8');
|
||||||
|
const matter = (await import('gray-matter')).default;
|
||||||
|
const parsed = matter(rawContent);
|
||||||
|
fileData.content = parsed.content.trim();
|
||||||
|
} catch {
|
||||||
|
fileData.content = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
files.push(fileData);
|
||||||
|
totalLength += fileData.contentLength;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by priority
|
||||||
|
const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
|
||||||
|
files.sort((a, b) =>
|
||||||
|
(priorityOrder[a.priority as keyof typeof priorityOrder] || 2) -
|
||||||
|
(priorityOrder[b.priority as keyof typeof priorityOrder] || 2)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get maxLength for percentage calculation
|
||||||
|
let maxLength = 8000;
|
||||||
|
const settingsPath = join(homedir(), '.claude', 'settings.json');
|
||||||
|
if (existsSync(settingsPath)) {
|
||||||
|
try {
|
||||||
|
const rawSettings = readFileSync(settingsPath, 'utf-8');
|
||||||
|
const settings = JSON.parse(rawSettings) as {
|
||||||
|
system?: { injectionControl?: { maxLength?: number } };
|
||||||
|
};
|
||||||
|
maxLength = settings?.system?.injectionControl?.maxLength || 8000;
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({
|
||||||
|
files,
|
||||||
|
stats: {
|
||||||
|
count: files.length,
|
||||||
|
totalLength,
|
||||||
|
maxLength,
|
||||||
|
percentage: Math.round((totalLength / maxLength) * 100)
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
} catch (err) {
|
||||||
|
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ error: (err as Error).message }));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -626,11 +626,12 @@ export async function startServer(options: ServerOptions = {}): Promise<http.Ser
|
|||||||
if (await handleFilesRoutes(routeContext)) return;
|
if (await handleFilesRoutes(routeContext)) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// System routes (data, health, version, paths, shutdown, notify, storage, dialog, a2ui answer broker)
|
// System routes (data, health, version, paths, shutdown, notify, storage, dialog, a2ui answer broker, system settings)
|
||||||
if (pathname === '/api/data' || pathname === '/api/health' ||
|
if (pathname === '/api/data' || pathname === '/api/health' ||
|
||||||
pathname === '/api/version-check' || pathname === '/api/shutdown' ||
|
pathname === '/api/version-check' || pathname === '/api/shutdown' ||
|
||||||
pathname === '/api/recent-paths' || pathname === '/api/switch-path' ||
|
pathname === '/api/recent-paths' || pathname === '/api/switch-path' ||
|
||||||
pathname === '/api/remove-recent-path' || pathname === '/api/system/notify' ||
|
pathname === '/api/remove-recent-path' || pathname === '/api/system/notify' ||
|
||||||
|
pathname === '/api/system/settings' || pathname === '/api/system/hooks/install-recommended' ||
|
||||||
pathname === '/api/a2ui/answer' ||
|
pathname === '/api/a2ui/answer' ||
|
||||||
pathname.startsWith('/api/storage/') || pathname.startsWith('/api/dialog/')) {
|
pathname.startsWith('/api/storage/') || pathname.startsWith('/api/dialog/')) {
|
||||||
if (await handleSystemRoutes(routeContext)) return;
|
if (await handleSystemRoutes(routeContext)) return;
|
||||||
|
|||||||
@@ -74,6 +74,8 @@ export interface SpecIndexEntry {
|
|||||||
priority: 'critical' | 'high' | 'medium' | 'low';
|
priority: 'critical' | 'high' | 'medium' | 'low';
|
||||||
/** Scope: global (from ~/.ccw/) or project (from .ccw/) */
|
/** Scope: global (from ~/.ccw/) or project (from .ccw/) */
|
||||||
scope: 'global' | 'project';
|
scope: 'global' | 'project';
|
||||||
|
/** Content length (body only, without frontmatter) - cached for performance */
|
||||||
|
contentLength: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -347,16 +349,18 @@ function parseSpecFile(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const data = parsed.data as Record<string, unknown>;
|
const data = parsed.data as Record<string, unknown>;
|
||||||
|
// Calculate content length (body only, without frontmatter)
|
||||||
|
const contentLength = parsed.content.length;
|
||||||
|
|
||||||
// Extract and validate frontmatter fields
|
// Extract and validate frontmatter fields
|
||||||
const title = extractString(data, 'title');
|
const title = extractString(data, 'title');
|
||||||
if (!title) {
|
if (!title) {
|
||||||
// Title is required - use filename as fallback
|
// Title is required - use filename as fallback
|
||||||
const fallbackTitle = basename(filePath, extname(filePath));
|
const fallbackTitle = basename(filePath, extname(filePath));
|
||||||
return buildEntry(fallbackTitle, filePath, dimension, projectPath, data, scope);
|
return buildEntry(fallbackTitle, filePath, dimension, projectPath, data, scope, contentLength);
|
||||||
}
|
}
|
||||||
|
|
||||||
return buildEntry(title, filePath, dimension, projectPath, data, scope);
|
return buildEntry(title, filePath, dimension, projectPath, data, scope, contentLength);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -368,7 +372,8 @@ function buildEntry(
|
|||||||
dimension: string,
|
dimension: string,
|
||||||
projectPath: string,
|
projectPath: string,
|
||||||
data: Record<string, unknown>,
|
data: Record<string, unknown>,
|
||||||
scope: 'global' | 'project' = 'project'
|
scope: 'global' | 'project' = 'project',
|
||||||
|
contentLength: number = 0
|
||||||
): SpecIndexEntry {
|
): SpecIndexEntry {
|
||||||
// Compute relative file path from project root using path.relative
|
// Compute relative file path from project root using path.relative
|
||||||
// Normalize to forward slashes for cross-platform consistency
|
// Normalize to forward slashes for cross-platform consistency
|
||||||
@@ -398,6 +403,7 @@ function buildEntry(
|
|||||||
readMode,
|
readMode,
|
||||||
priority,
|
priority,
|
||||||
scope,
|
scope,
|
||||||
|
contentLength,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import {
|
|||||||
SPEC_DIMENSIONS,
|
SPEC_DIMENSIONS,
|
||||||
SPEC_CATEGORIES,
|
SPEC_CATEGORIES,
|
||||||
type SpecDimension,
|
type SpecDimension,
|
||||||
|
type SpecCategory,
|
||||||
} from './spec-index-builder.js';
|
} from './spec-index-builder.js';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -43,6 +44,8 @@ export interface SpecLoadOptions {
|
|||||||
projectPath: string;
|
projectPath: string;
|
||||||
/** Specific dimension to load (loads all if omitted) */
|
/** Specific dimension to load (loads all if omitted) */
|
||||||
dimension?: SpecDimension;
|
dimension?: SpecDimension;
|
||||||
|
/** Workflow stage category filter (loads matching category specs) */
|
||||||
|
category?: SpecCategory;
|
||||||
/** Pre-extracted keywords (skips extraction if provided) */
|
/** Pre-extracted keywords (skips extraction if provided) */
|
||||||
keywords?: string[];
|
keywords?: string[];
|
||||||
/** Output format: 'cli' for markdown, 'hook' for JSON */
|
/** Output format: 'cli' for markdown, 'hook' for JSON */
|
||||||
@@ -138,7 +141,7 @@ const SPEC_PRIORITY_WEIGHT: Record<string, number> = {
|
|||||||
* @returns SpecLoadResult with formatted content
|
* @returns SpecLoadResult with formatted content
|
||||||
*/
|
*/
|
||||||
export async function loadSpecs(options: SpecLoadOptions): Promise<SpecLoadResult> {
|
export async function loadSpecs(options: SpecLoadOptions): Promise<SpecLoadResult> {
|
||||||
const { projectPath, outputFormat, debug } = options;
|
const { projectPath, outputFormat, debug, category } = options;
|
||||||
|
|
||||||
// Get injection control settings
|
// Get injection control settings
|
||||||
const maxLength = options.maxLength ?? 8000;
|
const maxLength = options.maxLength ?? 8000;
|
||||||
@@ -149,6 +152,9 @@ export async function loadSpecs(options: SpecLoadOptions): Promise<SpecLoadResul
|
|||||||
|
|
||||||
if (debug) {
|
if (debug) {
|
||||||
debugLog(`Extracted ${keywords.length} keywords: [${keywords.join(', ')}]`);
|
debugLog(`Extracted ${keywords.length} keywords: [${keywords.join(', ')}]`);
|
||||||
|
if (category) {
|
||||||
|
debugLog(`Category filter: ${category}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 2: Determine which dimensions to process
|
// Step 2: Determine which dimensions to process
|
||||||
@@ -164,7 +170,7 @@ export async function loadSpecs(options: SpecLoadOptions): Promise<SpecLoadResul
|
|||||||
const index = await getDimensionIndex(projectPath, dim);
|
const index = await getDimensionIndex(projectPath, dim);
|
||||||
totalScanned += index.entries.length;
|
totalScanned += index.entries.length;
|
||||||
|
|
||||||
const { required, matched } = filterSpecs(index, keywords);
|
const { required, matched } = filterSpecs(index, keywords, category);
|
||||||
|
|
||||||
if (debug) {
|
if (debug) {
|
||||||
debugLog(
|
debugLog(
|
||||||
@@ -229,23 +235,30 @@ export async function loadSpecs(options: SpecLoadOptions): Promise<SpecLoadResul
|
|||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Filter specs by readMode and keyword match.
|
* Filter specs by readMode, category, and keyword match.
|
||||||
*
|
*
|
||||||
* - required: all entries with readMode === 'required'
|
* - required: all entries with readMode === 'required' (and matching category if specified)
|
||||||
* - matched: entries with readMode === 'optional' that have keyword intersection
|
* - matched: entries with readMode === 'optional' that have keyword intersection (and matching category if specified)
|
||||||
*
|
*
|
||||||
* @param index - The dimension index to filter
|
* @param index - The dimension index to filter
|
||||||
* @param keywords - Extracted prompt keywords
|
* @param keywords - Extracted prompt keywords
|
||||||
|
* @param category - Optional category filter for workflow stage
|
||||||
* @returns Separated required and matched entries (deduplicated)
|
* @returns Separated required and matched entries (deduplicated)
|
||||||
*/
|
*/
|
||||||
export function filterSpecs(
|
export function filterSpecs(
|
||||||
index: DimensionIndex,
|
index: DimensionIndex,
|
||||||
keywords: string[]
|
keywords: string[],
|
||||||
|
category?: SpecCategory
|
||||||
): { required: SpecIndexEntry[]; matched: SpecIndexEntry[] } {
|
): { required: SpecIndexEntry[]; matched: SpecIndexEntry[] } {
|
||||||
const required: SpecIndexEntry[] = [];
|
const required: SpecIndexEntry[] = [];
|
||||||
const matched: SpecIndexEntry[] = [];
|
const matched: SpecIndexEntry[] = [];
|
||||||
|
|
||||||
for (const entry of index.entries) {
|
for (const entry of index.entries) {
|
||||||
|
// Category filter: skip if category specified and doesn't match
|
||||||
|
if (category && entry.category !== category) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (entry.readMode === 'required') {
|
if (entry.readMode === 'required') {
|
||||||
required.push(entry);
|
required.push(entry);
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
Reference in New Issue
Block a user