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:
catlog22
2026-02-27 09:45:28 +08:00
parent dfa8e0d9f5
commit 3f25dbb11b
15 changed files with 648 additions and 120 deletions

View File

@@ -74,6 +74,8 @@ export interface SpecIndexEntry {
priority: 'critical' | 'high' | 'medium' | 'low';
/** Scope: global (from ~/.ccw/) or project (from .ccw/) */
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>;
// Calculate content length (body only, without frontmatter)
const contentLength = parsed.content.length;
// Extract and validate frontmatter fields
const title = extractString(data, 'title');
if (!title) {
// Title is required - use filename as fallback
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,
projectPath: string,
data: Record<string, unknown>,
scope: 'global' | 'project' = 'project'
scope: 'global' | 'project' = 'project',
contentLength: number = 0
): SpecIndexEntry {
// Compute relative file path from project root using path.relative
// Normalize to forward slashes for cross-platform consistency
@@ -398,6 +403,7 @@ function buildEntry(
readMode,
priority,
scope,
contentLength,
};
}

View File

@@ -24,6 +24,7 @@ import {
SPEC_DIMENSIONS,
SPEC_CATEGORIES,
type SpecDimension,
type SpecCategory,
} from './spec-index-builder.js';
import {
@@ -43,6 +44,8 @@ export interface SpecLoadOptions {
projectPath: string;
/** Specific dimension to load (loads all if omitted) */
dimension?: SpecDimension;
/** Workflow stage category filter (loads matching category specs) */
category?: SpecCategory;
/** Pre-extracted keywords (skips extraction if provided) */
keywords?: string[];
/** Output format: 'cli' for markdown, 'hook' for JSON */
@@ -138,7 +141,7 @@ const SPEC_PRIORITY_WEIGHT: Record<string, number> = {
* @returns SpecLoadResult with formatted content
*/
export async function loadSpecs(options: SpecLoadOptions): Promise<SpecLoadResult> {
const { projectPath, outputFormat, debug } = options;
const { projectPath, outputFormat, debug, category } = options;
// Get injection control settings
const maxLength = options.maxLength ?? 8000;
@@ -149,6 +152,9 @@ export async function loadSpecs(options: SpecLoadOptions): Promise<SpecLoadResul
if (debug) {
debugLog(`Extracted ${keywords.length} keywords: [${keywords.join(', ')}]`);
if (category) {
debugLog(`Category filter: ${category}`);
}
}
// 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);
totalScanned += index.entries.length;
const { required, matched } = filterSpecs(index, keywords);
const { required, matched } = filterSpecs(index, keywords, category);
if (debug) {
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'
* - matched: entries with readMode === 'optional' that have keyword intersection
* - required: all entries with readMode === 'required' (and matching category if specified)
* - matched: entries with readMode === 'optional' that have keyword intersection (and matching category if specified)
*
* @param index - The dimension index to filter
* @param keywords - Extracted prompt keywords
* @param category - Optional category filter for workflow stage
* @returns Separated required and matched entries (deduplicated)
*/
export function filterSpecs(
index: DimensionIndex,
keywords: string[]
keywords: string[],
category?: SpecCategory
): { required: SpecIndexEntry[]; matched: SpecIndexEntry[] } {
const required: SpecIndexEntry[] = [];
const matched: SpecIndexEntry[] = [];
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') {
required.push(entry);
continue;