feat: Enhance spec management with new hooks and settings features

- Updated test cycle execution steps to streamline agent execution.
- Improved HookDialog component with enhanced validation messages and localization.
- Introduced SpecDialog component for better spec management.
- Added new hooks for fetching and updating specs list and frontmatter.
- Implemented API functions for specs list retrieval and index rebuilding.
- Added localization support for new specs settings and hooks.
- Enhanced SpecsSettingsPage to manage project and personal specs effectively.
- Updated CLI commands to support keyword-based spec loading.
- Improved spec index builder to categorize specs by workflow stages.
This commit is contained in:
catlog22
2026-02-26 22:52:33 +08:00
parent 6155fcc7b8
commit 151b81ee4a
51 changed files with 731 additions and 690 deletions

View File

@@ -111,19 +111,19 @@ export function HookDialog({
const newErrors: Record<string, string> = {};
if (!formData.name.trim()) {
newErrors.name = formatMessage({ id: 'hooks.validation.nameRequired' });
newErrors.name = formatMessage({ id: 'specs.hooks.validation.nameRequired', defaultMessage: 'Name is required' });
}
if (!formData.command.trim()) {
newErrors.command = formatMessage({ id: 'hooks.validation.commandRequired' });
newErrors.command = formatMessage({ id: 'specs.hooks.validation.commandRequired', defaultMessage: 'Command is required' });
}
if (formData.timeout && formData.timeout < 1000) {
newErrors.timeout = formatMessage({ id: 'hooks.validation.timeoutMin' });
newErrors.timeout = formatMessage({ id: 'specs.hooks.validation.timeoutMin', defaultMessage: 'Minimum timeout is 1000ms' });
}
if (formData.timeout && formData.timeout > 300000) {
newErrors.timeout = formatMessage({ id: 'hooks.validation.timeoutMax' });
newErrors.timeout = formatMessage({ id: 'specs.hooks.validation.timeoutMax', defaultMessage: 'Maximum timeout is 300000ms' });
}
setErrors(newErrors);
@@ -153,11 +153,11 @@ export function HookDialog({
<DialogHeader>
<DialogTitle>
{isEditing
? formatMessage({ id: 'hooks.dialog.editTitle' })
: formatMessage({ id: 'hooks.dialog.createTitle' })}
? formatMessage({ id: 'specs.hooks.dialog.editTitle', defaultMessage: 'Edit Hook' })
: formatMessage({ id: 'specs.hooks.dialog.createTitle', defaultMessage: 'Create Hook' })}
</DialogTitle>
<DialogDescription>
{formatMessage({ id: 'hooks.dialog.description' })}
{formatMessage({ id: 'specs.hooks.dialog.description', defaultMessage: 'Configure the hook trigger event, command, and other settings.' })}
</DialogDescription>
</DialogHeader>
@@ -165,13 +165,13 @@ export function HookDialog({
{/* Name field */}
<div className="space-y-2">
<Label htmlFor="name" className="required">
{formatMessage({ id: 'hooks.fields.name' })}
{formatMessage({ id: 'specs.hooks.fields.name', defaultMessage: 'Hook Name' })}
</Label>
<Input
id="name"
value={formData.name}
onChange={(e) => updateField('name', e.target.value)}
placeholder={formatMessage({ id: 'hooks.placeholders.name' })}
placeholder={formatMessage({ id: 'specs.hooks.placeholders.name', defaultMessage: 'Enter hook name' })}
className={errors.name ? 'border-destructive' : ''}
disabled={isLoading}
/>
@@ -183,7 +183,7 @@ export function HookDialog({
{/* Event type field */}
<div className="space-y-2">
<Label htmlFor="event" className="required">
{formatMessage({ id: 'hooks.fields.event' })}
{formatMessage({ id: 'specs.hooks.fields.event', defaultMessage: 'Trigger Event' })}
</Label>
<Select
value={formData.event}
@@ -191,29 +191,29 @@ export function HookDialog({
disabled={isLoading}
>
<SelectTrigger id="event">
<SelectValue placeholder={formatMessage({ id: 'hooks.placeholders.event' })} />
<SelectValue placeholder={formatMessage({ id: 'specs.hooks.placeholders.event', defaultMessage: 'Select event' })} />
</SelectTrigger>
<SelectContent>
<SelectItem value="SessionStart">
{formatMessage({ id: 'hooks.events.sessionStart' })}
{formatMessage({ id: 'specs.hooks.events.sessionStart', defaultMessage: 'Session Start' })}
</SelectItem>
<SelectItem value="UserPromptSubmit">
{formatMessage({ id: 'hooks.events.userPromptSubmit' })}
{formatMessage({ id: 'specs.hooks.events.userPromptSubmit', defaultMessage: 'User Prompt Submit' })}
</SelectItem>
<SelectItem value="SessionEnd">
{formatMessage({ id: 'hooks.events.sessionEnd' })}
{formatMessage({ id: 'specs.hooks.events.sessionEnd', defaultMessage: 'Session End' })}
</SelectItem>
</SelectContent>
</Select>
<p className="text-xs text-muted-foreground">
{formatMessage({ id: 'hints.hookEvents' })}
{formatMessage({ id: 'specs.hints.hookEvents', defaultMessage: 'Select when this hook should be triggered' })}
</p>
</div>
{/* Scope field */}
<div className="space-y-2">
<Label className="required">
{formatMessage({ id: 'hooks.fields.scope' })}
{formatMessage({ id: 'specs.hooks.fields.scope', defaultMessage: 'Scope' })}
</Label>
<RadioGroup
value={formData.scope}
@@ -225,32 +225,32 @@ export function HookDialog({
<RadioGroupItem value="global" id="scope-global" />
<Label htmlFor="scope-global" className="flex items-center gap-1.5 cursor-pointer">
<Globe className="h-4 w-4" />
{formatMessage({ id: 'hooks.scope.global' })}
{formatMessage({ id: 'specs.hooks.scope.global', defaultMessage: 'Global' })}
</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="project" id="scope-project" />
<Label htmlFor="scope-project" className="flex items-center gap-1.5 cursor-pointer">
<Folder className="h-4 w-4" />
{formatMessage({ id: 'hooks.scope.project' })}
{formatMessage({ id: 'specs.hooks.scope.project', defaultMessage: 'Project' })}
</Label>
</div>
</RadioGroup>
<p className="text-xs text-muted-foreground">
{formatMessage({ id: 'hints.hookScope' })}
{formatMessage({ id: 'specs.hints.hookScope', defaultMessage: 'Global hooks apply to all projects, project hooks only to current project' })}
</p>
</div>
{/* Command field */}
<div className="space-y-2">
<Label htmlFor="command" className="required">
{formatMessage({ id: 'hooks.fields.command' })}
{formatMessage({ id: 'specs.hooks.fields.command', defaultMessage: 'Command' })}
</Label>
<Input
id="command"
value={formData.command}
onChange={(e) => updateField('command', e.target.value)}
placeholder={formatMessage({ id: 'hooks.placeholders.command' })}
placeholder={formatMessage({ id: 'specs.hooks.placeholders.command', defaultMessage: 'Enter command to execute' })}
className={cn('font-mono', errors.command ? 'border-destructive' : '')}
disabled={isLoading}
/>
@@ -258,20 +258,20 @@ export function HookDialog({
<p className="text-xs text-destructive">{errors.command}</p>
)}
<p className="text-xs text-muted-foreground">
{formatMessage({ id: 'hints.hookCommand' })}
{formatMessage({ id: 'specs.hints.hookCommand', defaultMessage: 'Command to execute, can use environment variables' })}
</p>
</div>
{/* Description field */}
<div className="space-y-2">
<Label htmlFor="description">
{formatMessage({ id: 'hooks.fields.description' })}
{formatMessage({ id: 'specs.hooks.fields.description', defaultMessage: 'Description' })}
</Label>
<Textarea
id="description"
value={formData.description || ''}
onChange={(e) => updateField('description', e.target.value)}
placeholder={formatMessage({ id: 'hooks.placeholders.description' })}
placeholder={formatMessage({ id: 'specs.hooks.placeholders.description', defaultMessage: 'Enter description (optional)' })}
rows={2}
disabled={isLoading}
/>
@@ -280,9 +280,9 @@ export function HookDialog({
{/* Timeout field */}
<div className="space-y-2">
<Label htmlFor="timeout" className="flex items-center gap-1">
{formatMessage({ id: 'hooks.fields.timeout' })}
{formatMessage({ id: 'specs.hooks.fields.timeout', defaultMessage: 'Timeout' })}
<span className="text-xs text-muted-foreground">
({formatMessage({ id: 'hooks.fields.timeoutUnit' })})
({formatMessage({ id: 'specs.hooks.fields.timeoutUnit', defaultMessage: 'ms' })})
</span>
</Label>
<Input
@@ -300,14 +300,14 @@ export function HookDialog({
<p className="text-xs text-destructive">{errors.timeout}</p>
)}
<p className="text-xs text-muted-foreground">
{formatMessage({ id: 'hints.hookTimeout' })}
{formatMessage({ id: 'specs.hints.hookTimeout', defaultMessage: 'Timeout for command execution' })}
</p>
</div>
{/* Fail mode field */}
<div className="space-y-2">
<Label htmlFor="failMode" className="flex items-center gap-1">
{formatMessage({ id: 'hooks.fields.failMode' })}
{formatMessage({ id: 'specs.hooks.fields.failMode', defaultMessage: 'Failure Mode' })}
<HelpCircle className="h-3.5 w-3.5 text-muted-foreground" />
</Label>
<Select
@@ -320,30 +320,30 @@ export function HookDialog({
</SelectTrigger>
<SelectContent>
<SelectItem value="continue">
{formatMessage({ id: 'hooks.failModes.continue' })}
{formatMessage({ id: 'specs.hooks.failModes.continue', defaultMessage: 'Continue' })}
</SelectItem>
<SelectItem value="warn">
{formatMessage({ id: 'hooks.failModes.warn' })}
{formatMessage({ id: 'specs.hooks.failModes.warn', defaultMessage: 'Warn' })}
</SelectItem>
<SelectItem value="block">
{formatMessage({ id: 'hooks.failModes.block' })}
{formatMessage({ id: 'specs.hooks.failModes.block', defaultMessage: 'Block' })}
</SelectItem>
</SelectContent>
</Select>
<p className="text-xs text-muted-foreground">
{formatMessage({ id: 'hints.hookFailMode' })}
{formatMessage({ id: 'specs.hints.hookFailMode', defaultMessage: 'How to handle command execution failure' })}
</p>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={handleCancel} disabled={isLoading}>
{formatMessage({ id: 'common.cancel' })}
{formatMessage({ id: 'specs.common.cancel', defaultMessage: 'Cancel' })}
</Button>
<Button onClick={handleSave} disabled={isLoading}>
{isLoading
? formatMessage({ id: 'common.saving' })
: formatMessage({ id: 'common.save' })}
? formatMessage({ id: 'specs.common.saving', defaultMessage: 'Saving...' })
: formatMessage({ id: 'specs.common.save', defaultMessage: 'Save' })}
</Button>
</DialogFooter>
</DialogContent>

View File

@@ -5,7 +5,6 @@
import * as React from 'react';
import { useIntl } from 'react-intl';
import { cn } from '@/lib/utils';
import {
Dialog,
DialogContent,

View File

@@ -336,10 +336,13 @@ import {
updateSystemSettings,
installRecommendedHooks,
getSpecStats,
getSpecsList,
rebuildSpecIndex,
updateSpecFrontmatter,
type SystemSettings,
type UpdateSystemSettingsInput,
type InstallRecommendedHooksResponse,
type SpecStats,
type SpecsListResponse,
} from '../lib/api';
// Query keys for specs settings
@@ -463,3 +466,110 @@ export function useSpecStats(options: UseSpecStatsOptions = {}): UseSpecStatsRet
refetch: () => { query.refetch(); },
};
}
// ========================================
// Specs List Hook
// ========================================
export interface UseSpecsListOptions {
projectPath?: string;
enabled?: boolean;
staleTime?: number;
}
export interface UseSpecsListReturn {
data: SpecsListResponse | undefined;
isLoading: boolean;
error: Error | null;
refetch: () => void;
}
/**
* Hook to fetch specs list for all dimensions
* @param options - Options including projectPath for workspace isolation
*/
export function useSpecsList(options: UseSpecsListOptions = {}): UseSpecsListReturn {
const { projectPath, enabled = true, staleTime = STALE_TIME } = options;
const query = useQuery({
queryKey: specsSettingsKeys.specStats(projectPath), // Reuse for specs list
queryFn: () => getSpecsList(projectPath),
staleTime,
enabled,
retry: 1,
});
return {
data: query.data,
isLoading: query.isLoading,
error: query.error,
refetch: () => { query.refetch(); },
};
}
// ========================================
// Rebuild Spec Index Mutation Hook
// ========================================
export interface UseRebuildSpecIndexOptions {
projectPath?: string;
}
/**
* Hook to rebuild spec index
* @param options - Options including projectPath for workspace isolation
*/
export function useRebuildSpecIndex(options: UseRebuildSpecIndexOptions = {}) {
const { projectPath } = options;
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: () => rebuildSpecIndex(projectPath),
onSuccess: () => {
// Invalidate specs list and stats queries to refresh data
queryClient.invalidateQueries({ queryKey: specsSettingsKeys.specStats(projectPath) });
},
});
return {
mutate: mutation.mutate,
mutateAsync: mutation.mutateAsync,
isPending: mutation.isPending,
error: mutation.error,
data: mutation.data,
};
}
// ========================================
// Update Spec Frontmatter Mutation Hook
// ========================================
export interface UseUpdateSpecFrontmatterOptions {
projectPath?: string;
}
/**
* Hook to update spec frontmatter (e.g., toggle readMode)
* @param options - Options including projectPath for workspace isolation
*/
export function useUpdateSpecFrontmatter(options: UseUpdateSpecFrontmatterOptions = {}) {
const { projectPath } = options;
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: ({ file, readMode }: { file: string; readMode: string }) =>
updateSpecFrontmatter(file, readMode, projectPath),
onSuccess: () => {
// Invalidate specs list to refresh data
queryClient.invalidateQueries({ queryKey: specsSettingsKeys.specStats(projectPath) });
},
});
return {
mutate: mutation.mutate,
mutateAsync: mutation.mutateAsync,
isPending: mutation.isPending,
error: mutation.error,
data: mutation.data,
};
}

View File

@@ -7261,6 +7261,65 @@ export async function getSpecStats(projectPath?: string): Promise<SpecStats> {
return fetchApi<SpecStats>(url);
}
/**
* Spec entry from index
*/
export interface SpecEntry {
file: string;
title: string;
dimension: string;
readMode: 'required' | 'optional' | 'keywords';
priority: 'critical' | 'high' | 'medium' | 'low';
keywords: string[];
}
/**
* Specs list response from /api/specs/list
*/
export interface SpecsListResponse {
specs: Record<string, SpecEntry[]>;
}
/**
* Fetch specs list for all dimensions
* @param projectPath - Optional project path
*/
export async function getSpecsList(projectPath?: string): Promise<SpecsListResponse> {
const url = projectPath
? `/api/specs/list?path=${encodeURIComponent(projectPath)}`
: '/api/specs/list';
return fetchApi<SpecsListResponse>(url);
}
/**
* Rebuild spec index
*/
export async function rebuildSpecIndex(projectPath?: string): Promise<{ success: boolean; stats?: Record<string, number> }> {
const url = projectPath
? `/api/specs/rebuild?path=${encodeURIComponent(projectPath)}`
: '/api/specs/rebuild';
return fetchApi<{ success: boolean; stats?: Record<string, number> }>(url, {
method: 'POST',
});
}
/**
* Update spec frontmatter (toggle readMode)
*/
export async function updateSpecFrontmatter(
file: string,
readMode: string,
projectPath?: string
): Promise<{ success: boolean; readMode?: string }> {
const url = projectPath
? `/api/specs/update-frontmatter?path=${encodeURIComponent(projectPath)}`
: '/api/specs/update-frontmatter';
return fetchApi<{ success: boolean; readMode?: string }>(url, {
method: 'PUT',
body: JSON.stringify({ file, readMode }),
});
}
// ========== Analysis API ==========
import type { AnalysisSessionSummary, AnalysisSessionDetail } from '../types/analysis';

View File

@@ -42,6 +42,7 @@ import team from './team.json';
import terminalDashboard from './terminal-dashboard.json';
import skillHub from './skill-hub.json';
import nativeSession from './native-session.json';
import specs from './specs.json';
/**
* Flattens nested JSON object to dot-separated keys
@@ -107,4 +108,5 @@ export default {
...flattenMessages(terminalDashboard, 'terminalDashboard'),
...flattenMessages(skillHub, 'skillHub'),
...flattenMessages(nativeSession, 'nativeSession'),
...flattenMessages(specs, 'specs'),
} as Record<string, string>;

View File

@@ -0,0 +1,93 @@
{
"pageTitle": "Spec Settings",
"pageDescription": "Manage specification injection, hooks, and system settings",
"tabProjectSpecs": "Project Specs",
"tabPersonalSpecs": "Personal",
"tabHooks": "Hooks",
"tabInjection": "Injection",
"tabSettings": "Settings",
"searchPlaceholder": "Search specs...",
"rebuildIndex": "Rebuild Index",
"loading": "Loading...",
"noSpecs": "No specs found. Create specs in .workflow/ directory.",
"recommendedHooks": "Recommended Hooks",
"recommendedHooksDesc": "One-click install system-preset spec injection hooks",
"installAll": "Install All Recommended Hooks",
"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"
}
},
"hook": {
"install": "Install",
"edit": "Edit",
"toggle": "Toggle",
"delete": "Delete",
"enabled": "Enabled",
"disabled": "Disabled",
"scope": {
"global": "Global",
"project": "Project"
},
"event": {
"SessionStart": "Session Start",
"UserPromptSubmit": "Prompt Submit",
"SessionEnd": "Session End"
}
},
"injection": {
"title": "Injection Control",
"description": "Monitor and manage spec injection length",
"currentLength": "Current Length",
"maxLength": "Max Length",
"warnThreshold": "Warn Threshold",
"percentage": "Usage",
"truncateOnExceed": "Truncate on Exceed",
"truncateDescription": "Automatically truncate when injection exceeds max length",
"overLimit": "Over Limit",
"warning": "Warning",
"normal": "Normal"
},
"settings": {
"title": "Global Settings",
"description": "Configure personal spec defaults and system settings",
"personalSpecDefaults": "Personal Spec Defaults",
"defaultReadMode": "Default Read Mode",
"autoEnable": "Auto Enable",
"autoEnableDescription": "Automatically enable newly created personal specs"
},
"dialog": {
"cancel": "Cancel",
"save": "Save",
"editSpec": "Edit Spec",
"editHook": "Edit Hook",
"specTitle": "Spec Title",
"keywords": "Keywords",
"readMode": "Read Mode",
"priority": "Priority",
"hookName": "Hook Name",
"hookEvent": "Event",
"hookCommand": "Command",
"hookScope": "Scope",
"hookTimeout": "Timeout (ms)",
"hookFailMode": "Fail Mode"
}
}

View File

@@ -42,6 +42,7 @@ import team from './team.json';
import terminalDashboard from './terminal-dashboard.json';
import skillHub from './skill-hub.json';
import nativeSession from './native-session.json';
import specs from './specs.json';
/**
* Flattens nested JSON object to dot-separated keys
@@ -107,4 +108,5 @@ export default {
...flattenMessages(terminalDashboard, 'terminalDashboard'),
...flattenMessages(skillHub, 'skillHub'),
...flattenMessages(nativeSession, 'nativeSession'),
...flattenMessages(specs, 'specs'),
} as Record<string, string>;

View File

@@ -0,0 +1,202 @@
{
"pageTitle": "规范设置",
"pageDescription": "管理规范注入、钩子和系统设置",
"tabProjectSpecs": "项目规范",
"tabPersonalSpecs": "个人",
"tabHooks": "钩子",
"tabInjection": "注入控制",
"tabSettings": "设置",
"searchPlaceholder": "搜索规范...",
"rebuildIndex": "重建索引",
"loading": "加载中...",
"noSpecs": "未找到规范。请在 .workflow/ 目录中创建规范文件。",
"recommendedHooks": "推荐钩子",
"recommendedHooksDesc": "一键安装系统预设的规范注入钩子",
"installAll": "安装所有推荐钩子",
"installedHooks": "已安装钩子",
"installedHooksDesc": "管理已安装的钩子配置",
"searchHooks": "搜索钩子...",
"noHooks": "未安装钩子。请安装上方的推荐钩子。",
"actions": {
"edit": "编辑",
"delete": "删除",
"reset": "重置",
"save": "保存",
"saving": "保存中..."
},
"status": {
"enabled": "已启用",
"disabled": "已禁用"
},
"readMode": {
"required": "必读",
"optional": "选读"
},
"priority": {
"critical": "关键",
"high": "高",
"medium": "中",
"low": "低"
},
"spec": {
"edit": "编辑规范",
"toggle": "切换状态",
"delete": "删除规范",
"deleteConfirm": "确定要删除此规范吗?",
"title": "规范标题",
"keywords": "关键词",
"keywordsPlaceholder": "输入关键词,用逗号分隔",
"readMode": "读取模式",
"priority": "优先级",
"file": "文件路径"
},
"hook": {
"install": "安装",
"uninstall": "卸载",
"edit": "编辑钩子",
"toggle": "切换状态",
"delete": "删除钩子",
"enabled": "已启用",
"disabled": "已禁用",
"installed": "已安装",
"notInstalled": "未安装",
"scope": {
"global": "全局",
"project": "项目"
},
"event": {
"SessionStart": "会话开始",
"UserPromptSubmit": "提示词提交",
"SessionEnd": "会话结束"
},
"name": "钩子名称",
"eventLabel": "触发事件",
"command": "执行命令",
"scopeLabel": "作用域",
"timeout": "超时时间(ms)",
"failMode": "失败模式",
"failModeContinue": "继续",
"failModeBlock": "阻止",
"failModeWarn": "警告"
},
"hooks": {
"dialog": {
"createTitle": "创建钩子",
"editTitle": "编辑钩子",
"description": "配置钩子的触发事件、执行命令和其他参数。"
},
"fields": {
"name": "钩子名称",
"event": "触发事件",
"scope": "作用域",
"command": "执行命令",
"description": "描述",
"timeout": "超时时间",
"timeoutUnit": "毫秒",
"failMode": "失败处理模式"
},
"placeholders": {
"name": "输入钩子名称",
"event": "选择触发事件",
"command": "输入要执行的命令",
"description": "输入钩子描述(可选)"
},
"events": {
"sessionStart": "会话开始",
"userPromptSubmit": "提示词提交",
"sessionEnd": "会话结束"
},
"scope": {
"global": "全局",
"project": "项目"
},
"failModes": {
"continue": "继续执行",
"warn": "显示警告",
"block": "阻止操作"
}
},
"hints": {
"hookEvents": "选择钩子触发的事件类型",
"hookScope": "全局钩子应用于所有项目,项目钩子仅当前项目",
"hookCommand": "执行的命令,可使用环境变量",
"hookTimeout": "命令执行的超时时间",
"hookFailMode": "命令执行失败时的处理方式"
},
"common": {
"cancel": "取消",
"save": "保存",
"delete": "删除",
"edit": "编辑",
"reset": "重置",
"confirm": "确认"
},
"injection": {
"title": "注入控制",
"description": "监控和管理规范注入长度",
"statusTitle": "当前注入状态",
"settingsTitle": "注入控制设置",
"settingsDescription": "配置如何将规范内容注入到 AI 上下文中。",
"currentLength": "当前长度",
"maxLength": "最大注入长度(字符)",
"maxLengthHelp": "推荐值4000-10000。过大会消耗过多上下文过小可能截断重要规范。",
"warnThreshold": "警告阈值",
"warnThresholdLabel": "警告阈值(字符)",
"warnThresholdHelp": "当注入长度超过此值时显示警告。",
"percentage": "使用率",
"truncateOnExceed": "超出时截断",
"truncateHelp": "当内容超出最大长度时自动截断。",
"overLimit": "已超出限制",
"overLimitDescription": "当前注入内容已超出最大长度限制 {max} 字符,超出部分将被截断。",
"warning": "接近限制",
"normal": "正常",
"characters": "字符",
"statsInfo": "统计信息",
"requiredLength": "必读规范长度:",
"matchedLength": "关键词匹配长度:",
"remaining": "剩余空间:",
"loadError": "加载统计数据失败",
"saveSuccess": "设置已保存",
"saveError": "保存设置失败"
},
"settings": {
"title": "全局设置",
"description": "配置个人规范默认值和系统设置",
"personalSpecDefaults": "个人规范默认值",
"defaultReadMode": "默认读取模式",
"defaultReadModeHelp": "新创建的个人规范的默认读取模式",
"autoEnable": "自动启用",
"autoEnableDescription": "新创建的个人规范自动启用"
},
"dialog": {
"cancel": "取消",
"save": "保存",
"close": "关闭",
"editSpec": "编辑规范",
"editHook": "编辑钩子",
"confirmDelete": "确认删除",
"specTitle": "规范标题",
"keywords": "关键词",
"readMode": "读取模式",
"priority": "优先级",
"hookName": "钩子名称",
"hookEvent": "触发事件",
"hookCommand": "执行命令",
"hookScope": "作用域",
"hookTimeout": "超时时间(ms)",
"hookFailMode": "失败模式"
}
}

View File

@@ -4,24 +4,40 @@
* Main page for managing spec settings, hooks, injection control, and global settings.
* Uses 5 tabs: Project Specs | Personal Specs | Hooks | Injection | Settings
*/
import { useState } from 'react';
import { useState, useMemo } from 'react';
import { useIntl } from 'react-intl';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/Tabs';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/Card';
import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input';
import { ScrollText, User, Plug, Gauge, Settings, RefreshCw, Search } from 'lucide-react';
import { SpecCard, SpecDialog, type Spec, type SpecFormData } from '@/components/specs';
import { HookCard, HookDialog, type HookConfig } from '@/components/specs';
import { InjectionControlTab } from '@/components/specs/InjectionControlTab';
import { GlobalSettingsTab } from '@/components/specs/GlobalSettingsTab';
import { useSpecStats } from '@/hooks/useSystemSettings';
import { useSpecStats, useSpecsList, useSystemSettings, useRebuildSpecIndex } from '@/hooks/useSystemSettings';
import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore';
import type { SpecEntry } from '@/lib/api';
type SettingsTab = 'project-specs' | 'personal-specs' | 'hooks' | 'injection' | 'settings';
// Convert SpecEntry to Spec for display
function specEntryToSpec(entry: SpecEntry, dimension: string): Spec {
return {
id: entry.file,
title: entry.title,
dimension: dimension as Spec['dimension'],
keywords: entry.keywords,
readMode: entry.readMode as Spec['readMode'],
priority: entry.priority as Spec['priority'],
file: entry.file,
enabled: true, // Default to enabled
};
}
export function SpecsSettingsPage() {
const { formatMessage } = useIntl();
const projectPath = useWorkflowStore(selectProjectPath);
const [activeTab, setActiveTab] = useState<SettingsTab>('project-specs');
const [searchQuery, setSearchQuery] = useState('');
const [editDialogOpen, setEditDialogOpen] = useState(false);
@@ -29,13 +45,50 @@ export function SpecsSettingsPage() {
const [editingSpec, setEditingSpec] = useState<Spec | null>(null);
const [editingHook, setEditingHook] = useState<HookConfig | null>(null);
// Mock data for demonstration - will be replaced with real API calls
const [projectSpecs] = useState<Spec[]>([]);
const [personalSpecs] = useState<Spec[]>([]);
const [hooks] = useState<HookConfig[]>([]);
const [isLoading] = useState(false);
// Fetch real data
const { data: specsListData, isLoading: specsLoading, refetch: refetchSpecs } = useSpecsList({ projectPath });
const { data: statsData } = useSpecStats({ projectPath });
const { data: systemSettings } = useSystemSettings();
const rebuildMutation = useRebuildSpecIndex();
const { data: statsData, refetch: refetchStats } = useSpecStats();
// Convert specs data to display format
const { projectSpecs, personalSpecs } = useMemo(() => {
if (!specsListData?.specs) {
return { projectSpecs: [], personalSpecs: [] };
}
const specs: Spec[] = [];
const personal: Spec[] = [];
for (const [dimension, entries] of Object.entries(specsListData.specs)) {
for (const entry of entries) {
const spec = specEntryToSpec(entry, dimension);
if (dimension === 'personal') {
personal.push(spec);
} else {
specs.push(spec);
}
}
}
return { projectSpecs: specs, personalSpecs: personal };
}, [specsListData]);
// Get hooks from system settings
const hooks: HookConfig[] = useMemo(() => {
return systemSettings?.recommendedHooks?.map(h => ({
id: h.id,
name: h.name,
event: h.event as HookConfig['event'],
command: h.command,
description: h.description,
scope: h.scope as HookConfig['scope'],
enabled: h.autoInstall ?? false,
installed: h.autoInstall ?? false,
})) ?? [];
}, [systemSettings]);
const isLoading = specsLoading;
const handleSpecEdit = (spec: Spec) => {
setEditingSpec(spec);
@@ -81,7 +134,11 @@ export function SpecsSettingsPage() {
const handleRebuildIndex = async () => {
console.log('Rebuilding index...');
// TODO: Implement rebuild logic
rebuildMutation.mutate(undefined, {
onSuccess: () => {
refetchSpecs();
}
});
};
const filterSpecs = (specs: Spec[]) => {
@@ -117,7 +174,7 @@ export function SpecsSettingsPage() {
</div>
{/* Stats Summary */}
{statsData && (
{statsData?.dimensions && (
<div className="grid grid-cols-4 gap-4">
{Object.entries(statsData.dimensions).map(([dim, data]) => (
<Card key={dim}>
@@ -178,7 +235,7 @@ export function SpecsSettingsPage() {
scope: 'global',
enabled: true,
timeout: 5000,
failMode: 'silent'
failMode: 'continue'
},
{
id: 'spec-injection-prompt',
@@ -188,7 +245,7 @@ export function SpecsSettingsPage() {
scope: 'project',
enabled: true,
timeout: 5000,
failMode: 'silent'
failMode: 'continue'
}
];
@@ -219,11 +276,11 @@ export function SpecsSettingsPage() {
<HookCard
key={hook.id}
hook={hook}
isRecommended={true}
isRecommendedCard={true}
onInstall={() => console.log('Install:', hook.id)}
onEdit={handleHookEdit}
onToggle={handleHookToggle}
onDelete={handleHookDelete}
onUninstall={handleHookDelete}
/>
))}
</div>
@@ -261,7 +318,7 @@ export function SpecsSettingsPage() {
hook={hook}
onEdit={handleHookEdit}
onToggle={handleHookToggle}
onDelete={handleHookDelete}
onUninstall={handleHookDelete}
/>
))}
</div>
@@ -273,7 +330,7 @@ export function SpecsSettingsPage() {
};
return (
<div className="container py-6 max-w-6xl">
<div className="max-w-6xl mx-auto">
{/* Page Header */}
<div className="mb-6">
<h1 className="text-2xl font-bold flex items-center gap-2">
@@ -343,8 +400,10 @@ export function SpecsSettingsPage() {
<HookDialog
open={hookDialogOpen}
onOpenChange={setHookDialogOpen}
hook={editingHook}
onSave={handleHookSave}
hook={editingHook ?? undefined}
onSave={(hookData) => {
handleHookSave(editingHook?.id ?? null, hookData);
}}
/>
</div>
);