fix: Resolve MCP installation issues and enhance path resolution

- Fixed API endpoint mismatches in mcp-manager.js to ensure global install/update buttons function correctly.
- Corrected undefined function references in mcp-manager.js for project installation.
- Refactored event handling to eliminate global scope pollution in mcp-manager.js.
- Added comprehensive debugging guide for MCP installation issues.
- Implemented a session path resolver to infer content types from filenames and paths, improving usability.
- Introduced tests for embeddings improvements in init and status commands to verify functionality.
This commit is contained in:
catlog22
2025-12-17 22:05:16 +08:00
parent 8b927f302c
commit b22839c99f
24 changed files with 2043 additions and 192 deletions

View File

@@ -135,7 +135,7 @@ export function run(argv: string[]): void {
program
.command('session [subcommand] [args...]')
.description('Workflow session lifecycle management')
.option('--location <loc>', 'Location filter: active|archived|both')
.option('--location <loc>', 'Session location: active|lite-plan|lite-fix (init); Filter: active|archived|both (list)')
.option('--type <type>', 'Content type or session type')
.option('--content <json>', 'Content for write/update')
.option('--task-id <id>', 'Task ID for task content')

View File

@@ -0,0 +1,372 @@
/**
* Session Path Resolver - Smart file path to content_type resolution
* Eliminates need for --type parameter by inferring from filenames and paths
*/
import { basename, dirname, join } from 'path';
// Supported content types (from session-manager.ts)
type ContentType =
| 'session' | 'plan' | 'task' | 'summary' | 'process' | 'chat' | 'brainstorm'
| 'review-dim' | 'review-iter' | 'review-fix' | 'todo' | 'context'
| 'lite-plan' | 'lite-fix-plan' | 'exploration' | 'explorations-manifest'
| 'diagnosis' | 'diagnoses-manifest' | 'clarifications' | 'execution-context' | 'session-metadata';
export interface ResolverResult {
contentType: ContentType;
pathParams?: Record<string, string>;
resolvedPath: string; // Relative path within session directory
}
export interface ResolverContext {
sessionPath: string;
sessionLocation: 'active' | 'archived' | 'lite-plan' | 'lite-fix';
}
export class PathResolutionError extends Error {
code: 'NOT_FOUND' | 'INVALID_PATH';
suggestions: string[];
constructor(code: 'NOT_FOUND' | 'INVALID_PATH', message: string, suggestions: string[] = []) {
super(message);
this.name = 'PathResolutionError';
this.code = code;
this.suggestions = suggestions;
}
}
/**
* Task ID patterns (IMPL-*, TEST-*, DOC-*, REFACTOR-*, TASK-*)
*/
const TASK_ID_PATTERNS = [
/^(IMPL|TEST|DOC|REFACTOR|TASK)-\d+\.json$/i,
];
/**
* Summary filename pattern (*-summary.md)
*/
const SUMMARY_PATTERN = /^(.+)-summary\.md$/i;
/**
* Path prefix to content_type mapping
*/
const PATH_PREFIX_TO_CONTENT_TYPE: Record<string, ContentType> = {
'.task/': 'task',
'.summaries/': 'summary',
'.process/': 'process',
'.chat/': 'chat',
'.brainstorming/': 'brainstorm',
'.review/dimensions/': 'review-dim',
'.review/iterations/': 'review-iter',
'.review/fixes/': 'review-fix',
};
/**
* Exact filename to content_type mapping
*/
const EXACT_FILENAME_TO_CONTENT_TYPE: Record<string, ContentType> = {
'IMPL_PLAN.md': 'plan',
'TODO_LIST.md': 'todo',
'workflow-session.json': 'session',
'context-package.json': 'context',
'plan.json': 'lite-plan', // Will be overridden by session location
'fix-plan.json': 'lite-fix-plan', // Specific to lite-fix sessions
'session-metadata.json': 'session-metadata',
'clarifications.json': 'clarifications',
'execution-context.json': 'execution-context',
'explorations-manifest.json': 'explorations-manifest',
'diagnoses-manifest.json': 'diagnoses-manifest',
};
/**
* Extract task ID from filename
* Examples: IMPL-001.json → IMPL-001
*/
function extractTaskId(filename: string): string | null {
const match = filename.match(/^([A-Z]+-\d+)\.json$/i);
return match ? match[1] : null;
}
/**
* Extract summary task ID from filename
* Examples: IMPL-001-summary.md → IMPL-001
*/
function extractSummaryTaskId(filename: string): string | null {
const match = filename.match(SUMMARY_PATTERN);
return match ? match[1] : null;
}
/**
* Extract review dimension from path
* Examples: .review/dimensions/security.json → security
*/
function extractReviewDimension(filepath: string): string | null {
const match = filepath.match(/\.review\/dimensions\/(.+)\.json$/);
return match ? match[1] : null;
}
/**
* Extract review iteration from path
* Examples: .review/iterations/1.json → 1
*/
function extractReviewIteration(filepath: string): string | null {
const match = filepath.match(/\.review\/iterations\/(\d+)\.json$/);
return match ? match[1] : null;
}
/**
* Extract filename from relative path within review fixes
* Examples: .review/fixes/finding-001.json → finding-001.json
*/
function extractReviewFixFilename(filepath: string): string | null {
const match = filepath.match(/\.review\/fixes\/(.+)$/);
return match ? match[1] : null;
}
/**
* Extract filename from process/chat/brainstorm paths
* Examples: .process/archiving.json → archiving.json
*/
function extractProcessFilename(filepath: string, prefix: string): string | null {
const pattern = new RegExp(`^${prefix.replace(/\//g, '\\/')}(.+)$`);
const match = filepath.match(pattern);
return match ? match[1] : null;
}
/**
* Resolve relative path (contains '/') to content_type
*/
function resolveRelativePath(filepath: string): ResolverResult | null {
// Security: Reject path traversal attempts
if (filepath.includes('..') || filepath.startsWith('/')) {
throw new PathResolutionError(
'INVALID_PATH',
'Path traversal and absolute paths are not allowed',
['Use relative paths within the session directory']
);
}
// Check path prefix matches
for (const [prefix, contentType] of Object.entries(PATH_PREFIX_TO_CONTENT_TYPE)) {
if (filepath.startsWith(prefix)) {
const pathParams: Record<string, string> = {};
// Extract specific parameters based on content type
if (contentType === 'task') {
const taskId = extractTaskId(basename(filepath));
if (taskId) {
pathParams.task_id = taskId;
}
} else if (contentType === 'summary') {
const taskId = extractSummaryTaskId(basename(filepath));
if (taskId) {
pathParams.task_id = taskId;
}
} else if (contentType === 'review-dim') {
const dimension = extractReviewDimension(filepath);
if (dimension) {
pathParams.dimension = dimension;
}
} else if (contentType === 'review-iter') {
const iteration = extractReviewIteration(filepath);
if (iteration) {
pathParams.iteration = iteration;
}
} else if (contentType === 'review-fix') {
const filename = extractReviewFixFilename(filepath);
if (filename) {
pathParams.filename = filename;
}
} else if (contentType === 'process' || contentType === 'chat' || contentType === 'brainstorm') {
const filename = extractProcessFilename(filepath, prefix);
if (filename) {
pathParams.filename = filename;
}
}
return {
contentType,
pathParams: Object.keys(pathParams).length > 0 ? pathParams : undefined,
resolvedPath: filepath,
};
}
}
return null;
}
/**
* Resolve simple filename (no '/') to content_type
*/
function resolveFilename(
filename: string,
context: ResolverContext
): ResolverResult | null {
// Check exact filename matches first
if (EXACT_FILENAME_TO_CONTENT_TYPE[filename]) {
let contentType = EXACT_FILENAME_TO_CONTENT_TYPE[filename];
// Override plan.json based on session location
if (filename === 'plan.json') {
if (context.sessionLocation === 'lite-plan') {
contentType = 'lite-plan';
} else if (context.sessionLocation === 'lite-fix') {
contentType = 'lite-fix-plan';
}
}
return {
contentType,
resolvedPath: filename,
};
}
// Check task ID patterns (IMPL-001.json, TEST-042.json, etc.)
for (const pattern of TASK_ID_PATTERNS) {
if (pattern.test(filename)) {
const taskId = extractTaskId(filename);
if (taskId) {
return {
contentType: 'task',
pathParams: { task_id: taskId },
resolvedPath: `.task/${filename}`,
};
}
}
}
// Check summary pattern (*-summary.md)
const summaryTaskId = extractSummaryTaskId(filename);
if (summaryTaskId) {
return {
contentType: 'summary',
pathParams: { task_id: summaryTaskId },
resolvedPath: `.summaries/${filename}`,
};
}
// Check exploration pattern (exploration-{angle}.json)
const explorationMatch = filename.match(/^exploration-(.+)\.json$/);
if (explorationMatch) {
return {
contentType: 'exploration',
pathParams: { angle: explorationMatch[1] },
resolvedPath: filename,
};
}
// Check diagnosis pattern (diagnosis-{angle}.json)
const diagnosisMatch = filename.match(/^diagnosis-(.+)\.json$/);
if (diagnosisMatch) {
return {
contentType: 'diagnosis',
pathParams: { angle: diagnosisMatch[1] },
resolvedPath: filename,
};
}
return null;
}
/**
* Main resolver function - resolves filename/path to content_type
*
* @param filename - Filename or relative path
* @param context - Session context (path and location)
* @returns Resolver result with content type, path params, and resolved path
* @throws PathResolutionError if path is invalid or cannot be resolved
*
* @example
* // Filename examples
* resolveFilePath('IMPL-001.json', context)
* // → { contentType: 'task', pathParams: {task_id: 'IMPL-001'}, resolvedPath: '.task/IMPL-001.json' }
*
* resolveFilePath('IMPL_PLAN.md', context)
* // → { contentType: 'plan', resolvedPath: 'IMPL_PLAN.md' }
*
* // Relative path examples
* resolveFilePath('.task/IMPL-001.json', context)
* // → { contentType: 'task', pathParams: {task_id: 'IMPL-001'}, resolvedPath: '.task/IMPL-001.json' }
*
* resolveFilePath('.review/dimensions/security.json', context)
* // → { contentType: 'review-dim', pathParams: {dimension: 'security'}, resolvedPath: '...' }
*/
export function resolveFilePath(
filename: string,
context: ResolverContext
): ResolverResult {
if (!filename) {
throw new PathResolutionError(
'INVALID_PATH',
'Filename is required',
['Usage: ccw session <session-id> read <filename|path>']
);
}
// Normalize path separators (handle Windows paths)
const normalizedFilename = filename.replace(/\\/g, '/');
// Security: Validate filename
if (normalizedFilename.includes('\0') || normalizedFilename.trim() === '') {
throw new PathResolutionError(
'INVALID_PATH',
'Invalid filename or path',
['Use valid filename or relative path']
);
}
// RULE 1: Relative path (contains '/')
if (normalizedFilename.includes('/')) {
const result = resolveRelativePath(normalizedFilename);
if (result) {
return result;
}
throw new PathResolutionError(
'NOT_FOUND',
`Unknown path pattern: ${normalizedFilename}`,
[
'Supported paths:',
' .task/*.json (tasks)',
' .summaries/*.md (summaries)',
' .process/* (process files)',
' .chat/* (chat files)',
' .brainstorming/* (brainstorm files)',
' .review/dimensions/*.json (review dimensions)',
' .review/iterations/*.json (review iterations)',
' .review/fixes/* (review fixes)',
]
);
}
// RULE 2: Simple filename
const result = resolveFilename(normalizedFilename, context);
if (result) {
return result;
}
// Resolution failed
throw new PathResolutionError(
'NOT_FOUND',
`Unknown file: ${normalizedFilename}`,
[
'Supported filenames:',
' IMPL-*.json, TEST-*.json, DOC-*.json, REFACTOR-*.json, TASK-*.json (tasks)',
' IMPL_PLAN.md (plan)',
' TODO_LIST.md (todo)',
' workflow-session.json (session metadata)',
' context-package.json (context)',
' session-metadata.json (session metadata for lite sessions)',
' plan.json (lite-plan or lite-fix-plan, depending on session location)',
' fix-plan.json (lite-fix-plan)',
' *-summary.md (summaries)',
' exploration-{angle}.json (explorations)',
' diagnosis-{angle}.json (diagnoses)',
' explorations-manifest.json (exploration manifest)',
' diagnoses-manifest.json (diagnosis manifest)',
' clarifications.json (clarifications)',
' execution-context.json (execution context)',
'Or use a relative path: .task/IMPL-001.json',
]
);
}

View File

@@ -53,10 +53,13 @@ interface SessionInput {
session_id?: string;
id?: string;
project?: string;
description?: string;
status?: string;
type?: string;
workflow_type?: string | null;
created_at?: string | null;
created_at?: string | null; // For backward compatibility
created?: string; // From SessionMetadata
updated?: string; // From SessionMetadata
archived_at?: string | null;
path: string;
}
@@ -249,11 +252,11 @@ export async function aggregateData(sessions: ScanSessionsResult, workflowDir: s
async function processSession(session: SessionInput, isActive: boolean): Promise<SessionData> {
const result: SessionData = {
session_id: session.session_id || session.id || '',
project: session.project || session.session_id || session.id || '',
project: session.project || session.description || session.session_id || session.id || '',
status: session.status || (isActive ? 'active' : 'archived'),
type: session.type || 'workflow', // Session type (workflow, review, test, docs)
workflow_type: session.workflow_type || null, // Original workflow_type for reference
created_at: session.created_at || null, // Raw ISO string - let frontend format
created_at: session.created || session.created_at || null, // Prefer 'created' from SessionMetadata, fallback to 'created_at'
archived_at: session.archived_at || null, // Raw ISO string - let frontend format
path: session.path,
tasks: [],

View File

@@ -192,9 +192,9 @@ export async function handleCodexLensRoutes(ctx: RouteContext): Promise<boolean>
const args = ['clean'];
if (all) {
args.push('--all');
}
if (path) {
args.push('--path', path);
} else if (path) {
// Path is passed as a positional argument, not as a flag
args.push(path);
}
args.push('--json');

View File

@@ -771,7 +771,8 @@ Return ONLY valid JSON in this exact format (no markdown, no code blocks, just p
}
try {
const configPath = join(projectPath, '.claude', 'CLAUDE.md');
const rulesDir = join(projectPath, '.claude', 'rules');
const configPath = join(rulesDir, 'active_memory.md');
const configJsonPath = join(projectPath, '.claude', 'active_memory_config.json');
const enabled = existsSync(configPath);
let lastSync: string | null = null;
@@ -823,7 +824,8 @@ Return ONLY valid JSON in this exact format (no markdown, no code blocks, just p
}
const claudeDir = join(projectPath, '.claude');
const configPath = join(claudeDir, 'CLAUDE.md');
const rulesDir = join(claudeDir, 'rules');
const configPath = join(rulesDir, 'active_memory.md');
const configJsonPath = join(claudeDir, 'active_memory_config.json');
if (enabled) {
@@ -831,14 +833,17 @@ Return ONLY valid JSON in this exact format (no markdown, no code blocks, just p
if (!existsSync(claudeDir)) {
mkdirSync(claudeDir, { recursive: true });
}
if (!existsSync(rulesDir)) {
mkdirSync(rulesDir, { recursive: true });
}
// Save config
if (config) {
writeFileSync(configJsonPath, JSON.stringify(config, null, 2), 'utf-8');
}
// Create initial CLAUDE.md with header
const initialContent = `# CLAUDE.md - Project Memory
// Create initial active_memory.md with header
const initialContent = `# Active Memory - Project Context
> Auto-generated understanding of frequently accessed files.
> Last updated: ${new Date().toISOString()}
@@ -901,7 +906,7 @@ Return ONLY valid JSON in this exact format (no markdown, no code blocks, just p
return true;
}
// API: Active Memory - Sync (analyze hot files using CLI and update CLAUDE.md)
// API: Active Memory - Sync (analyze hot files using CLI and update active_memory.md)
if (pathname === '/api/memory/active/sync' && req.method === 'POST') {
let body = '';
req.on('data', (chunk: Buffer) => { body += chunk.toString(); });
@@ -917,7 +922,8 @@ Return ONLY valid JSON in this exact format (no markdown, no code blocks, just p
}
const claudeDir = join(projectPath, '.claude');
const configPath = join(claudeDir, 'CLAUDE.md');
const rulesDir = join(claudeDir, 'rules');
const configPath = join(rulesDir, 'active_memory.md');
// Get hot files from memory store - with fallback
let hotFiles: any[] = [];
@@ -937,8 +943,8 @@ Return ONLY valid JSON in this exact format (no markdown, no code blocks, just p
return isAbsolute(filePath) ? filePath : join(projectPath, filePath);
}).filter((p: string) => existsSync(p));
// Build the CLAUDE.md content header
let content = `# CLAUDE.md - Project Memory
// Build the active_memory.md content header
let content = `# Active Memory - Project Context
> Auto-generated understanding of frequently accessed files using ${tool.toUpperCase()}.
> Last updated: ${new Date().toISOString()}
@@ -1055,10 +1061,13 @@ RULES: Be concise. Focus on practical understanding. Include function signatures
}
}
// Ensure directory exists
// Ensure directories exist
if (!existsSync(claudeDir)) {
mkdirSync(claudeDir, { recursive: true });
}
if (!existsSync(rulesDir)) {
mkdirSync(rulesDir, { recursive: true });
}
// Write the file
writeFileSync(configPath, content, 'utf-8');

View File

@@ -17,7 +17,7 @@ const SERVER_NAME = 'ccw-tools';
const SERVER_VERSION = '6.1.4';
// Default enabled tools (core set)
const DEFAULT_TOOLS: string[] = ['write_file', 'edit_file', 'read_file', 'codex_lens', 'smart_search'];
const DEFAULT_TOOLS: string[] = ['write_file', 'edit_file', 'read_file', 'smart_search'];
/**
* Get list of enabled tools from environment or defaults

View File

@@ -20,6 +20,11 @@ let currentCliMode = 'claude'; // 'claude' or 'codex'
let codexMcpConfig = null;
let codexMcpServers = {};
// ========== Project Config Type Preference ==========
// 'mcp' = .mcp.json (project root file, recommended)
// 'claude' = claude.json projects[path].mcpServers (shared config)
let preferredProjectConfigType = 'mcp';
// ========== Initialization ==========
function initMcpManager() {
// Initialize MCP navigation
@@ -229,11 +234,9 @@ async function toggleMcpServer(serverName, enable) {
async function copyMcpServerToProject(serverName, serverConfig, configType = null) {
try {
// If configType not specified, ask user to choose
// If configType not specified, use the preferred config type (toggle setting)
if (!configType) {
const choice = await showConfigTypeDialog();
if (!choice) return null; // User cancelled
configType = choice;
configType = preferredProjectConfigType;
}
const response = await fetch('/api/mcp-copy-server', {
@@ -982,14 +985,16 @@ async function installCcwToolsMcp(scope = 'workspace') {
showRefreshToast(result.error || 'Failed to install CCW Tools MCP globally', 'error');
}
} else {
// Install to workspace (.mcp.json)
// Install to workspace (use preferredProjectConfigType)
const configType = preferredProjectConfigType;
const response = await fetch('/api/mcp-copy-server', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
projectPath: projectPath,
serverName: 'ccw-tools',
serverConfig: ccwToolsConfig
serverConfig: ccwToolsConfig,
configType: configType
})
});
@@ -999,7 +1004,8 @@ async function installCcwToolsMcp(scope = 'workspace') {
if (result.success) {
await loadMcpConfig();
renderMcpManager();
showRefreshToast(`CCW Tools installed to workspace (${selectedTools.length} tools)`, 'success');
const location = configType === 'mcp' ? '.mcp.json' : 'claude.json';
showRefreshToast(`CCW Tools installed to ${location} (${selectedTools.length} tools)`, 'success');
} else {
showRefreshToast(result.error || 'Failed to install CCW Tools MCP to workspace', 'error');
}
@@ -1046,14 +1052,16 @@ async function updateCcwToolsMcp(scope = 'workspace') {
showRefreshToast(result.error || 'Failed to update CCW Tools MCP globally', 'error');
}
} else {
// Update workspace (.mcp.json)
// Update workspace (use preferredProjectConfigType)
const configType = preferredProjectConfigType;
const response = await fetch('/api/mcp-copy-server', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
projectPath: projectPath,
serverName: 'ccw-tools',
serverConfig: ccwToolsConfig
serverConfig: ccwToolsConfig,
configType: configType
})
});
@@ -1063,7 +1071,8 @@ async function updateCcwToolsMcp(scope = 'workspace') {
if (result.success) {
await loadMcpConfig();
renderMcpManager();
showRefreshToast(`CCW Tools updated in workspace (${selectedTools.length} tools)`, 'success');
const location = configType === 'mcp' ? '.mcp.json' : 'claude.json';
showRefreshToast(`CCW Tools updated in ${location} (${selectedTools.length} tools)`, 'success');
} else {
showRefreshToast(result.error || 'Failed to update CCW Tools MCP in workspace', 'error');
}
@@ -1130,6 +1139,25 @@ async function installCcwToolsMcpToCodex() {
}
}
// ========== Project Config Type Toggle ==========
function toggleProjectConfigType() {
preferredProjectConfigType = preferredProjectConfigType === 'mcp' ? 'claude' : 'mcp';
console.log('[MCP] Preferred project config type changed to:', preferredProjectConfigType);
// Re-render to update toggle display
renderMcpManager();
}
function getPreferredProjectConfigType() {
return preferredProjectConfigType;
}
function setPreferredProjectConfigType(type) {
if (type === 'mcp' || type === 'claude') {
preferredProjectConfigType = type;
console.log('[MCP] Preferred project config type set to:', preferredProjectConfigType);
}
}
// ========== Global Exports for onclick handlers ==========
// Expose functions to global scope to support inline onclick handlers
window.setCliMode = setCliMode;
@@ -1137,3 +1165,6 @@ window.getCliMode = getCliMode;
window.selectCcwTools = selectCcwTools;
window.selectCcwToolsCodex = selectCcwToolsCodex;
window.openMcpCreateModal = openMcpCreateModal;
window.toggleProjectConfigType = toggleProjectConfigType;
window.getPreferredProjectConfigType = getPreferredProjectConfigType;
window.setPreferredProjectConfigType = setPreferredProjectConfigType;

View File

@@ -3,6 +3,8 @@
// Internationalization for help page (Chinese translations)
// ==========================================
console.log('[Help i18n] File loading started');
var helpI18n = {
zh: {
// Page Headers
@@ -30,10 +32,13 @@ var helpI18n = {
// Workflow Diagrams
'help.diagrams.title': '常见工作流场景',
'help.diagrams.tdd': 'TDD 开发',
'help.diagrams.feature': '功能开发',
'help.diagrams.bugfix': 'Bug 调查',
'help.diagrams.review': '代码审查',
'help.diagrams.decision': '决策流程:选择规划方式',
'help.diagrams.brainstorm': '头脑风暴',
'help.diagrams.cliResume': 'CLI Resume机制',
'help.diagrams.bugFix': 'Bug修复流程',
'help.diagrams.lite': 'Lite轻量工作流',
'help.diagrams.planFull': 'Plan完整规划',
'help.diagrams.tdd': 'TDD测试驱动',
'help.diagrams.fit': '适应视图',
'help.diagrams.zoomIn': '放大',
'help.diagrams.zoomOut': '缩小',
@@ -43,6 +48,59 @@ var helpI18n = {
'help.diagrams.legend.alternatives': '替代方案',
'help.diagrams.notLoaded': 'Cytoscape.js 未加载',
// Workflow Steps - Decision
'help.workflows.decision.start': '任务开始',
'help.workflows.decision.cliAnalyze': 'CLI分析理解项目',
'help.workflows.decision.understand': '充分理解蓝图',
'help.workflows.decision.simple': '简单任务',
'help.workflows.decision.medium': '中等任务',
'help.workflows.decision.complex': '复杂任务',
'help.workflows.decision.claudeExec': 'Claude执行(优先)',
'help.workflows.decision.cliExec': 'CLI执行',
'help.workflows.decision.claudePlan': 'Claude自带Plan',
// Workflow Steps - Brainstorm
'help.workflows.brainstorm.start': '不确定方向',
'help.workflows.brainstorm.question': '知道做什么吗?',
'help.workflows.brainstorm.product': '不知道:探索产品',
'help.workflows.brainstorm.design': '知道但不知怎么做',
'help.workflows.brainstorm.next': '进入规划阶段',
// Workflow Steps - CLI Resume
'help.workflows.cliResume.firstExec': 'ccw cli exec "分析..."',
'help.workflows.cliResume.saveContext': '保存会话上下文',
'help.workflows.cliResume.resumeCmd': 'ccw cli exec --resume',
'help.workflows.cliResume.merge': '合并历史对话',
'help.workflows.cliResume.continue': '继续执行任务',
'help.workflows.cliResume.splitOutput': '拆分结果存储',
'help.workflows.cliResume.complete': '完成',
// Workflow Steps - Bug Fix
'help.workflows.bugFix.start': '发现Bug',
'help.workflows.bugFix.cliAnalyze': 'CLI分析定位Bug',
'help.workflows.bugFix.diagnosis': '诊断根因',
'help.workflows.bugFix.impact': '影响评估',
'help.workflows.bugFix.strategy': '修复策略',
'help.workflows.bugFix.execute': '执行修复',
'help.workflows.bugFix.complete': '完成',
// Workflow Steps - Plan Full
'help.workflows.planFull.start': '复杂项目开始',
'help.workflows.planFull.cliAnalyze': 'CLI深度分析项目',
'help.workflows.planFull.complete': '会话完成',
// Workflow Steps - Lite
'help.workflows.lite.start': '开始',
'help.workflows.lite.confirm': '三维确认',
'help.workflows.lite.complete': '完成',
// Workflow Steps - TDD
'help.workflows.tdd.start': '开始',
'help.workflows.tdd.red': 'Red: 编写失败测试',
'help.workflows.tdd.green': 'Green: 实现代码',
'help.workflows.tdd.refactor': 'Refactor: 重构优化',
'help.workflows.tdd.complete': '完成',
// CodexLens
'help.codexlens.title': 'CodexLens 快速入门',
'help.codexlens.subtitle': '强大的代码索引和语义搜索工具',
@@ -95,10 +153,13 @@ var helpI18n = {
// Workflow Diagrams
'help.diagrams.title': 'Common Workflow Scenarios',
'help.diagrams.decision': 'Decision: Choose Planning Approach',
'help.diagrams.brainstorm': 'Brainstorming',
'help.diagrams.cliResume': 'CLI Resume Mechanism',
'help.diagrams.bugFix': 'Bug Fix Workflow',
'help.diagrams.lite': 'Lite Workflow',
'help.diagrams.planFull': 'Full Planning',
'help.diagrams.tdd': 'TDD Development',
'help.diagrams.feature': 'Feature Development',
'help.diagrams.bugfix': 'Bug Investigation',
'help.diagrams.review': 'Code Review',
'help.diagrams.fit': 'Fit to View',
'help.diagrams.zoomIn': 'Zoom In',
'help.diagrams.zoomOut': 'Zoom Out',
@@ -108,6 +169,59 @@ var helpI18n = {
'help.diagrams.legend.alternatives': 'Alternatives',
'help.diagrams.notLoaded': 'Cytoscape.js not loaded',
// Workflow Steps - Decision
'help.workflows.decision.start': 'Task Start',
'help.workflows.decision.cliAnalyze': 'CLI Analyze Project',
'help.workflows.decision.understand': 'Understand Blueprint',
'help.workflows.decision.simple': 'Simple Task',
'help.workflows.decision.medium': 'Medium Task',
'help.workflows.decision.complex': 'Complex Task',
'help.workflows.decision.claudeExec': 'Claude Execute (Preferred)',
'help.workflows.decision.cliExec': 'CLI Execute',
'help.workflows.decision.claudePlan': 'Claude Built-in Plan',
// Workflow Steps - Brainstorm
'help.workflows.brainstorm.start': 'Uncertain Direction',
'help.workflows.brainstorm.question': 'Know What to Build?',
'help.workflows.brainstorm.product': 'No: Explore Product',
'help.workflows.brainstorm.design': 'Yes but Not How',
'help.workflows.brainstorm.next': 'Enter Planning Phase',
// Workflow Steps - CLI Resume
'help.workflows.cliResume.firstExec': 'ccw cli exec "analyze..."',
'help.workflows.cliResume.saveContext': 'Save Session Context',
'help.workflows.cliResume.resumeCmd': 'ccw cli exec --resume',
'help.workflows.cliResume.merge': 'Merge Conversation History',
'help.workflows.cliResume.continue': 'Continue Execution',
'help.workflows.cliResume.splitOutput': 'Split & Store Results',
'help.workflows.cliResume.complete': 'Complete',
// Workflow Steps - Bug Fix
'help.workflows.bugFix.start': 'Bug Discovered',
'help.workflows.bugFix.cliAnalyze': 'CLI Analyze & Locate Bug',
'help.workflows.bugFix.diagnosis': 'Root Cause Analysis',
'help.workflows.bugFix.impact': 'Impact Assessment',
'help.workflows.bugFix.strategy': 'Fix Strategy',
'help.workflows.bugFix.execute': 'Execute Fix',
'help.workflows.bugFix.complete': 'Complete',
// Workflow Steps - Plan Full
'help.workflows.planFull.start': 'Complex Project Start',
'help.workflows.planFull.cliAnalyze': 'CLI Deep Analysis',
'help.workflows.planFull.complete': 'Session Complete',
// Workflow Steps - Lite
'help.workflows.lite.start': 'Start',
'help.workflows.lite.confirm': '3D Confirmation',
'help.workflows.lite.complete': 'Complete',
// Workflow Steps - TDD
'help.workflows.tdd.start': 'Start',
'help.workflows.tdd.red': 'Red: Write Failing Test',
'help.workflows.tdd.green': 'Green: Implement Code',
'help.workflows.tdd.refactor': 'Refactor: Optimize',
'help.workflows.tdd.complete': 'Complete',
// CodexLens
'help.codexlens.title': 'CodexLens Quick Start',
'help.codexlens.subtitle': 'Powerful code indexing and semantic search tool',

View File

@@ -226,6 +226,7 @@ const i18n = {
'codexlens.migrationWarning': 'After changing the path, existing indexes will need to be re-initialized for each workspace.',
'codexlens.actions': 'Actions',
'codexlens.initializeIndex': 'Initialize Index',
'codexlens.cleanCurrentWorkspace': 'Clean Current Workspace',
'codexlens.cleanAllIndexes': 'Clean All Indexes',
'codexlens.installCodexLens': 'Install CodexLens',
'codexlens.testSearch': 'Test Search',
@@ -249,8 +250,10 @@ const i18n = {
'codexlens.configSaved': 'Configuration saved successfully',
'codexlens.pathEmpty': 'Index directory path cannot be empty',
'codexlens.cleanConfirm': 'Are you sure you want to clean all CodexLens indexes? This cannot be undone.',
'codexlens.cleanCurrentWorkspaceConfirm': 'Are you sure you want to clean the current workspace index? This cannot be undone.',
'codexlens.cleaning': 'Cleaning indexes...',
'codexlens.cleanSuccess': 'All indexes cleaned successfully',
'codexlens.cleanCurrentWorkspaceSuccess': 'Current workspace index cleaned successfully',
'codexlens.cleanFailed': 'Failed to clean indexes',
'codexlens.loadingConfig': 'Loading configuration...',
@@ -484,6 +487,10 @@ const i18n = {
'mcp.description': 'Description',
'mcp.category': 'Category',
'mcp.installTo': 'Install To',
'mcp.configTarget': 'Config Target',
'mcp.clickToSwitch': 'Click to switch',
'mcp.usingMcpJson': 'Using .mcp.json',
'mcp.usingClaudeJson': 'Using claude.json',
'mcp.cwd': 'Working Directory',
'mcp.httpHeaders': 'HTTP Headers',
'mcp.error': 'Error',
@@ -1335,6 +1342,7 @@ const i18n = {
'codexlens.migrationWarning': '更改路径后,需要为每个工作区重新初始化索引。',
'codexlens.actions': '操作',
'codexlens.initializeIndex': '初始化索引',
'codexlens.cleanCurrentWorkspace': '清理当前工作空间',
'codexlens.cleanAllIndexes': '清理所有索引',
'codexlens.installCodexLens': '安装 CodexLens',
'codexlens.testSearch': '测试搜索',
@@ -1358,8 +1366,10 @@ const i18n = {
'codexlens.configSaved': '配置保存成功',
'codexlens.pathEmpty': '索引目录路径不能为空',
'codexlens.cleanConfirm': '确定要清理所有 CodexLens 索引吗?此操作无法撤销。',
'codexlens.cleanCurrentWorkspaceConfirm': '确定要清理当前工作空间的索引吗?此操作无法撤销。',
'codexlens.cleaning': '清理索引中...',
'codexlens.cleanSuccess': '所有索引已成功清理',
'codexlens.cleanCurrentWorkspaceSuccess': '当前工作空间索引已成功清理',
'codexlens.cleanFailed': '清理索引失败',
'codexlens.loadingConfig': '加载配置中...',
@@ -1590,6 +1600,10 @@ const i18n = {
'mcp.description': '描述',
'mcp.category': '分类',
'mcp.installTo': '安装到',
'mcp.configTarget': '配置目标',
'mcp.clickToSwitch': '点击切换',
'mcp.usingMcpJson': '使用 .mcp.json',
'mcp.usingClaudeJson': '使用 claude.json',
'mcp.cwd': '工作目录',
'mcp.httpHeaders': 'HTTP 头',
'mcp.error': '错误',

View File

@@ -1613,6 +1613,9 @@ function buildCodexLensConfigContent(config) {
? '<button class="btn-sm btn-outline" onclick="event.stopPropagation(); initCodexLensIndex()">' +
'<i data-lucide="database" class="w-3 h-3"></i> ' + t('codexlens.initializeIndex') +
'</button>' +
'<button class="btn-sm btn-outline" onclick="event.stopPropagation(); cleanCurrentWorkspaceIndex()">' +
'<i data-lucide="folder-x" class="w-3 h-3"></i> ' + t('codexlens.cleanCurrentWorkspace') +
'</button>' +
'<button class="btn-sm btn-outline" onclick="event.stopPropagation(); cleanCodexLensIndexes()">' +
'<i data-lucide="trash" class="w-3 h-3"></i> ' + t('codexlens.cleanAllIndexes') +
'</button>' +
@@ -2014,7 +2017,48 @@ async function deleteModel(profile) {
if (window.lucide) lucide.createIcons();
}
}
/**
* Clean current workspace index
*/
async function cleanCurrentWorkspaceIndex() {
if (!confirm(t('codexlens.cleanCurrentWorkspaceConfirm'))) {
return;
}
try {
showRefreshToast(t('codexlens.cleaning'), 'info');
// Get current workspace path (projectPath is a global variable from state.js)
var workspacePath = projectPath;
var response = await fetch('/api/codexlens/clean', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ path: workspacePath })
});
var result = await response.json();
if (result.success) {
showRefreshToast(t('codexlens.cleanCurrentWorkspaceSuccess'), 'success');
// Refresh status
if (typeof loadCodexLensStatus === 'function') {
await loadCodexLensStatus();
renderToolsSection();
if (window.lucide) lucide.createIcons();
}
} else {
showRefreshToast(t('codexlens.cleanFailed') + ': ' + result.error, 'error');
}
} catch (err) {
showRefreshToast(t('common.error') + ': ' + err.message, 'error');
}
}
/**
* Clean all CodexLens indexes
*/
async function cleanCodexLensIndexes() {
if (!confirm(t('codexlens.cleanConfirm'))) {
return;

View File

@@ -109,6 +109,9 @@ function buildCodexLensConfigContent(config) {
? '<button class="btn-sm btn-outline" onclick="initCodexLensIndex()">' +
'<i data-lucide="database" class="w-3 h-3"></i> ' + t('codexlens.initializeIndex') +
'</button>' +
'<button class="btn-sm btn-outline" onclick="cleanCurrentWorkspaceIndex()">' +
'<i data-lucide="folder-x" class="w-3 h-3"></i> ' + t('codexlens.cleanCurrentWorkspace') +
'</button>' +
'<button class="btn-sm btn-outline" onclick="cleanCodexLensIndexes()">' +
'<i data-lucide="trash" class="w-3 h-3"></i> ' + t('codexlens.cleanAllIndexes') +
'</button>' +
@@ -559,6 +562,45 @@ function uninstallCodexLens() {
openCliUninstallWizard('codexlens');
}
/**
* Clean current workspace index
*/
async function cleanCurrentWorkspaceIndex() {
if (!confirm(t('codexlens.cleanCurrentWorkspaceConfirm'))) {
return;
}
try {
showRefreshToast(t('codexlens.cleaning'), 'info');
// Get current workspace path (projectPath is a global variable from state.js)
var workspacePath = projectPath;
var response = await fetch('/api/codexlens/clean', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ path: workspacePath })
});
var result = await response.json();
if (result.success) {
showRefreshToast(t('codexlens.cleanCurrentWorkspaceSuccess'), 'success');
// Refresh status
if (typeof loadCodexLensStatus === 'function') {
await loadCodexLensStatus();
renderToolsSection();
if (window.lucide) lucide.createIcons();
}
} else {
showRefreshToast(t('codexlens.cleanFailed') + ': ' + result.error, 'error');
}
} catch (err) {
showRefreshToast(t('common.error') + ': ' + err.message, 'error');
}
}
/**
* Clean all CodexLens indexes
*/

View File

@@ -14,7 +14,7 @@ var activeHelpTab = 'cli';
var helpSearchQuery = '';
var helpSearchTimeout = null;
var cytoscapeInstance = null;
var activeWorkflowDiagram = 'tdd';
var activeWorkflowDiagram = 'decision';
// ========== Main Render Function ==========
async function renderHelpView() {
@@ -377,18 +377,27 @@ function renderWorkflowDiagrams() {
<div class="mb-4">
<h3 class="text-lg font-semibold text-foreground mb-3">${ht('help.diagrams.title')}</h3>
<div class="flex gap-2 flex-wrap">
<button class="workflow-diagram-btn px-4 py-2 rounded-lg text-sm font-medium transition-colors" data-workflow="decision">
${ht('help.diagrams.decision')}
</button>
<button class="workflow-diagram-btn px-4 py-2 rounded-lg text-sm font-medium transition-colors" data-workflow="brainstorm">
${ht('help.diagrams.brainstorm')}
</button>
<button class="workflow-diagram-btn px-4 py-2 rounded-lg text-sm font-medium transition-colors" data-workflow="cli-resume">
${ht('help.diagrams.cliResume')}
</button>
<button class="workflow-diagram-btn px-4 py-2 rounded-lg text-sm font-medium transition-colors" data-workflow="bug-fix">
${ht('help.diagrams.bugFix')}
</button>
<button class="workflow-diagram-btn px-4 py-2 rounded-lg text-sm font-medium transition-colors" data-workflow="lite">
${ht('help.diagrams.lite')}
</button>
<button class="workflow-diagram-btn px-4 py-2 rounded-lg text-sm font-medium transition-colors" data-workflow="plan-full">
${ht('help.diagrams.planFull')}
</button>
<button class="workflow-diagram-btn px-4 py-2 rounded-lg text-sm font-medium transition-colors" data-workflow="tdd">
${ht('help.diagrams.tdd')}
</button>
<button class="workflow-diagram-btn px-4 py-2 rounded-lg text-sm font-medium transition-colors" data-workflow="feature">
${ht('help.diagrams.feature')}
</button>
<button class="workflow-diagram-btn px-4 py-2 rounded-lg text-sm font-medium transition-colors" data-workflow="bugfix">
${ht('help.diagrams.bugfix')}
</button>
<button class="workflow-diagram-btn px-4 py-2 rounded-lg text-sm font-medium transition-colors" data-workflow="review">
${ht('help.diagrams.review')}
</button>
</div>
</div>
@@ -507,6 +516,17 @@ function initializeCytoscapeDiagram(workflow) {
return;
}
// Get computed CSS variable values
var rootStyles = getComputedStyle(document.documentElement);
var primaryColor = rootStyles.getPropertyValue('--primary').trim();
var foregroundColor = rootStyles.getPropertyValue('--foreground').trim();
var mutedColor = rootStyles.getPropertyValue('--muted-foreground').trim();
// Convert HSL values to usable format
var primaryHsl = primaryColor ? 'hsl(' + primaryColor + ')' : '#3B82F6';
var foregroundHsl = foregroundColor ? 'hsl(' + foregroundColor + ')' : '#1F2937';
var mutedHsl = mutedColor ? 'hsl(' + mutedColor + ')' : '#6B7280';
// Initialize Cytoscape
cytoscapeInstance = cytoscape({
container: container,
@@ -515,43 +535,68 @@ function initializeCytoscapeDiagram(workflow) {
{
selector: 'node',
style: {
'background-color': 'hsl(var(--primary))',
'shape': 'roundrectangle',
'background-color': primaryHsl,
'background-opacity': 0.9,
'border-width': 2,
'border-color': primaryHsl,
'border-opacity': 1,
'label': 'data(label)',
'color': 'hsl(var(--foreground))',
'color': '#FFFFFF',
'text-valign': 'center',
'text-halign': 'center',
'font-size': '12px',
'width': '80px',
'height': '80px',
'font-size': '14px',
'font-weight': '600',
'width': '140px',
'height': '60px',
'text-wrap': 'wrap',
'text-max-width': '70px'
'text-max-width': '130px',
'padding': '8px',
'shadow-blur': 10,
'shadow-color': '#000000',
'shadow-opacity': 0.2,
'shadow-offset-x': 0,
'shadow-offset-y': 2
}
},
{
selector: 'edge',
style: {
'width': 2,
'line-color': 'hsl(var(--muted-foreground))',
'target-arrow-color': 'hsl(var(--muted-foreground))',
'width': 3,
'line-color': mutedHsl,
'target-arrow-color': mutedHsl,
'target-arrow-shape': 'triangle',
'target-arrow-fill': 'filled',
'arrow-scale': 1.5,
'curve-style': 'bezier',
'label': 'data(label)',
'font-size': '10px',
'color': 'hsl(var(--muted-foreground))'
'font-size': '12px',
'font-weight': '500',
'color': foregroundHsl,
'text-background-color': '#FFFFFF',
'text-background-opacity': 0.9,
'text-background-padding': '4px',
'text-background-shape': 'roundrectangle',
'text-border-width': 1,
'text-border-color': mutedHsl,
'text-border-opacity': 0.3
}
},
{
selector: 'edge.prerequisite',
style: {
'line-color': 'hsl(var(--primary))',
'target-arrow-color': 'hsl(var(--primary))'
'line-color': primaryHsl,
'target-arrow-color': primaryHsl,
'width': 3
}
},
{
selector: 'edge.next-step',
style: {
'line-color': '#10B981',
'target-arrow-color': '#10B981'
'target-arrow-color': '#10B981',
'width': 3,
'line-style': 'solid'
}
},
{
@@ -559,15 +604,20 @@ function initializeCytoscapeDiagram(workflow) {
style: {
'line-color': '#F59E0B',
'target-arrow-color': '#F59E0B',
'line-style': 'dashed'
'line-style': 'dashed',
'line-dash-pattern': [10, 5],
'width': 2.5
}
}
],
layout: {
name: 'dagre',
rankDir: 'TB',
nodeSep: 50,
rankSep: 80
name: 'breadthfirst',
directed: true,
padding: 80,
spacingFactor: 2,
avoidOverlap: true,
nodeDimensionsIncludeLabels: true,
animate: false
}
});
@@ -583,83 +633,156 @@ function initializeCytoscapeDiagram(workflow) {
}
function getWorkflowGraphData(workflow) {
var nodes = [];
var edges = [];
var workflows = {
'tdd': ['workflow:tdd-plan', 'workflow:execute', 'workflow:tdd-verify'],
'feature': ['workflow:plan', 'workflow:action-plan-verify', 'workflow:execute', 'workflow:review'],
'bugfix': ['workflow:lite-fix', 'workflow:lite-execute', 'workflow:test-cycle-execute'],
'review': ['workflow:review-session-cycle', 'workflow:review-fix', 'workflow:test-cycle-execute']
'decision': {
nodes: [
{ data: { id: 'start', label: ht('help.workflows.decision.start') } },
{ data: { id: 'cli-analyze', label: ht('help.workflows.decision.cliAnalyze') } },
{ data: { id: 'understand', label: ht('help.workflows.decision.understand') } },
{ data: { id: 'simple', label: ht('help.workflows.decision.simple') } },
{ data: { id: 'medium', label: ht('help.workflows.decision.medium') } },
{ data: { id: 'complex', label: ht('help.workflows.decision.complex') } },
{ data: { id: 'claude-exec', label: ht('help.workflows.decision.claudeExec') } },
{ data: { id: 'cli-exec', label: ht('help.workflows.decision.cliExec') } },
{ data: { id: 'claude-plan', label: ht('help.workflows.decision.claudePlan') } },
{ data: { id: 'lite-plan', label: '/workflow:lite-plan' } },
{ data: { id: 'full-plan', label: '/workflow:plan' } }
],
edges: [
{ data: { source: 'start', target: 'cli-analyze' }, classes: 'next-step' },
{ data: { source: 'cli-analyze', target: 'understand' }, classes: 'next-step' },
{ data: { source: 'understand', target: 'simple' }, classes: 'alternative' },
{ data: { source: 'understand', target: 'medium' }, classes: 'alternative' },
{ data: { source: 'understand', target: 'complex' }, classes: 'alternative' },
{ data: { source: 'simple', target: 'claude-exec', label: '优先' }, classes: 'next-step' },
{ data: { source: 'simple', target: 'cli-exec' }, classes: 'alternative' },
{ data: { source: 'medium', target: 'claude-plan' }, classes: 'next-step' },
{ data: { source: 'medium', target: 'lite-plan' }, classes: 'alternative' },
{ data: { source: 'complex', target: 'full-plan' }, classes: 'next-step' }
]
},
'brainstorm': {
nodes: [
{ data: { id: 'start', label: ht('help.workflows.brainstorm.start') } },
{ data: { id: 'question', label: ht('help.workflows.brainstorm.question') } },
{ data: { id: 'product', label: ht('help.workflows.brainstorm.product') } },
{ data: { id: 'design', label: ht('help.workflows.brainstorm.design') } },
{ data: { id: 'brainstorm-product', label: '/workflow:brainstorm:auto-parallel' } },
{ data: { id: 'brainstorm-design', label: '/workflow:brainstorm:auto-parallel' } },
{ data: { id: 'next', label: ht('help.workflows.brainstorm.next') } }
],
edges: [
{ data: { source: 'start', target: 'question' }, classes: 'next-step' },
{ data: { source: 'question', target: 'product' }, classes: 'alternative' },
{ data: { source: 'question', target: 'design' }, classes: 'alternative' },
{ data: { source: 'product', target: 'brainstorm-product' }, classes: 'next-step' },
{ data: { source: 'design', target: 'brainstorm-design' }, classes: 'next-step' },
{ data: { source: 'brainstorm-product', target: 'next' }, classes: 'next-step' },
{ data: { source: 'brainstorm-design', target: 'next' }, classes: 'next-step' }
]
},
'cli-resume': {
nodes: [
{ data: { id: 'first-exec', label: ht('help.workflows.cliResume.firstExec') } },
{ data: { id: 'save-context', label: ht('help.workflows.cliResume.saveContext') } },
{ data: { id: 'resume-cmd', label: ht('help.workflows.cliResume.resumeCmd') } },
{ data: { id: 'merge', label: ht('help.workflows.cliResume.merge') } },
{ data: { id: 'continue', label: ht('help.workflows.cliResume.continue') } },
{ data: { id: 'split-output', label: ht('help.workflows.cliResume.splitOutput') } },
{ data: { id: 'complete', label: ht('help.workflows.cliResume.complete') } }
],
edges: [
{ data: { source: 'first-exec', target: 'save-context' }, classes: 'next-step' },
{ data: { source: 'save-context', target: 'resume-cmd' }, classes: 'next-step' },
{ data: { source: 'resume-cmd', target: 'merge' }, classes: 'next-step' },
{ data: { source: 'merge', target: 'continue' }, classes: 'next-step' },
{ data: { source: 'continue', target: 'split-output' }, classes: 'next-step' },
{ data: { source: 'split-output', target: 'complete' }, classes: 'next-step' }
]
},
'bug-fix': {
nodes: [
{ data: { id: 'start', label: ht('help.workflows.bugFix.start') } },
{ data: { id: 'cli-analyze', label: ht('help.workflows.bugFix.cliAnalyze') } },
{ data: { id: 'lite-fix', label: '/workflow:lite-fix' } },
{ data: { id: 'diagnosis', label: ht('help.workflows.bugFix.diagnosis') } },
{ data: { id: 'impact', label: ht('help.workflows.bugFix.impact') } },
{ data: { id: 'strategy', label: ht('help.workflows.bugFix.strategy') } },
{ data: { id: 'execute', label: ht('help.workflows.bugFix.execute') } },
{ data: { id: 'complete', label: ht('help.workflows.bugFix.complete') } }
],
edges: [
{ data: { source: 'start', target: 'cli-analyze' }, classes: 'next-step' },
{ data: { source: 'cli-analyze', target: 'lite-fix' }, classes: 'next-step' },
{ data: { source: 'lite-fix', target: 'diagnosis' }, classes: 'next-step' },
{ data: { source: 'diagnosis', target: 'impact' }, classes: 'next-step' },
{ data: { source: 'impact', target: 'strategy' }, classes: 'next-step' },
{ data: { source: 'strategy', target: 'execute' }, classes: 'next-step' },
{ data: { source: 'execute', target: 'complete' }, classes: 'next-step' }
]
},
'plan-full': {
nodes: [
{ data: { id: 'start', label: ht('help.workflows.planFull.start') } },
{ data: { id: 'cli-analyze', label: ht('help.workflows.planFull.cliAnalyze') } },
{ data: { id: 'plan', label: '/workflow:plan' } },
{ data: { id: 'verify', label: '/workflow:action-plan-verify' } },
{ data: { id: 'execute', label: '/workflow:execute' } },
{ data: { id: 'test', label: '/workflow:test-gen' } },
{ data: { id: 'review', label: '/workflow:review' } },
{ data: { id: 'complete', label: '/workflow:session:complete' } }
],
edges: [
{ data: { source: 'start', target: 'cli-analyze' }, classes: 'next-step' },
{ data: { source: 'cli-analyze', target: 'plan' }, classes: 'next-step' },
{ data: { source: 'plan', target: 'verify' }, classes: 'next-step' },
{ data: { source: 'verify', target: 'execute' }, classes: 'next-step' },
{ data: { source: 'execute', target: 'test' }, classes: 'next-step' },
{ data: { source: 'test', target: 'review' }, classes: 'next-step' },
{ data: { source: 'review', target: 'complete' }, classes: 'next-step' }
]
},
'lite': {
nodes: [
{ data: { id: 'start', label: ht('help.workflows.lite.start') } },
{ data: { id: 'lite-plan', label: '/workflow:lite-plan' } },
{ data: { id: 'confirm', label: ht('help.workflows.lite.confirm') } },
{ data: { id: 'lite-execute', label: '/workflow:lite-execute' } },
{ data: { id: 'complete', label: ht('help.workflows.lite.complete') } }
],
edges: [
{ data: { source: 'start', target: 'lite-plan' }, classes: 'next-step' },
{ data: { source: 'lite-plan', target: 'confirm' }, classes: 'next-step' },
{ data: { source: 'confirm', target: 'lite-execute' }, classes: 'next-step' },
{ data: { source: 'lite-execute', target: 'complete' }, classes: 'next-step' }
]
},
'tdd': {
nodes: [
{ data: { id: 'start', label: ht('help.workflows.tdd.start') } },
{ data: { id: 'tdd-plan', label: '/workflow:tdd-plan' } },
{ data: { id: 'red', label: ht('help.workflows.tdd.red') } },
{ data: { id: 'green', label: ht('help.workflows.tdd.green') } },
{ data: { id: 'refactor', label: ht('help.workflows.tdd.refactor') } },
{ data: { id: 'verify', label: '/workflow:tdd-verify' } },
{ data: { id: 'complete', label: ht('help.workflows.tdd.complete') } }
],
edges: [
{ data: { source: 'start', target: 'tdd-plan' }, classes: 'next-step' },
{ data: { source: 'tdd-plan', target: 'red' }, classes: 'next-step' },
{ data: { source: 'red', target: 'green' }, classes: 'next-step' },
{ data: { source: 'green', target: 'refactor' }, classes: 'next-step' },
{ data: { source: 'refactor', target: 'verify' }, classes: 'next-step' },
{ data: { source: 'verify', target: 'complete' }, classes: 'next-step' }
]
}
};
var workflowCommands = workflows[workflow] || workflows['tdd'];
var workflowData = workflows[workflow] || workflows['decision'];
console.log('Building workflow diagram for:', workflow);
console.log('Commands:', workflowCommands);
console.log('Available workflows data:', helpData.workflows ? Object.keys(helpData.workflows).length + ' commands' : 'no data');
console.log('Generated graph:', workflowData.nodes.length, 'nodes,', workflowData.edges.length, 'edges');
// Build graph from workflow relationships
workflowCommands.forEach(function(cmd) {
nodes.push({ data: { id: cmd, label: cmd.replace('workflow:', '').replace('task:', '') } });
var relationships = helpData.workflows ? helpData.workflows[cmd] : null;
if (relationships) {
// Add prerequisites
if (relationships.prerequisites) {
relationships.prerequisites.forEach(function(prereq) {
if (!nodes.find(n => n.data.id === prereq)) {
nodes.push({ data: { id: prereq, label: prereq.replace('workflow:', '').replace('task:', '') } });
}
edges.push({
data: { source: prereq, target: cmd, label: 'requires' },
classes: 'prerequisite'
});
});
}
// Add next steps
if (relationships.next_steps) {
relationships.next_steps.forEach(function(next) {
if (!nodes.find(n => n.data.id === next)) {
nodes.push({ data: { id: next, label: next.replace('workflow:', '').replace('task:', '') } });
}
edges.push({
data: { source: cmd, target: next, label: 'then' },
classes: 'next-step'
});
});
}
// Add alternatives
if (relationships.alternatives) {
relationships.alternatives.forEach(function(alt) {
if (!nodes.find(n => n.data.id === alt)) {
nodes.push({ data: { id: alt, label: alt.replace('workflow:', '').replace('task:', '') } });
}
edges.push({
data: { source: cmd, target: alt, label: 'or' },
classes: 'alternative'
});
});
}
}
});
console.log('Generated graph:', nodes.length, 'nodes,', edges.length, 'edges');
// If no edges but we have nodes, create a simple chain
if (edges.length === 0 && nodes.length > 1) {
console.log('No relationships found, creating simple chain');
for (var i = 0; i < nodes.length - 1; i++) {
edges.push({
data: { source: nodes[i].data.id, target: nodes[i + 1].data.id },
classes: 'next-step'
});
}
}
return nodes.concat(edges);
return workflowData.nodes.concat(workflowData.edges);
}
function showCommandTooltip(commandName, node) {

View File

@@ -461,17 +461,23 @@ async function renderMcpManager() {
onclick="openMcpCreateModal('project')">
<span>+</span> ${t('mcp.newProjectServer')}
</button>
<!-- Project Config Type Toggle -->
<button class="project-config-toggle inline-flex items-center gap-1.5 px-3 py-1.5 text-xs rounded-lg border cursor-pointer transition-all hover:shadow-md"
onclick="toggleProjectConfigType()"
title="${t('mcp.clickToSwitch')}"
style="${getPreferredProjectConfigType() === 'mcp'
? 'background: rgba(34, 197, 94, 0.1); border-color: rgba(34, 197, 94, 0.3); color: rgb(34, 197, 94);'
: 'background: rgba(59, 130, 246, 0.1); border-color: rgba(59, 130, 246, 0.3); color: rgb(59, 130, 246);'}">
<i data-lucide="${getPreferredProjectConfigType() === 'mcp' ? 'file-json' : 'settings'}" class="w-3.5 h-3.5"></i>
<span>${getPreferredProjectConfigType() === 'mcp' ? '.mcp.json' : 'claude.json'}</span>
<i data-lucide="chevrons-up-down" class="w-3 h-3 opacity-50"></i>
</button>
${hasMcpJson ? `
<span class="inline-flex items-center gap-1.5 px-2 py-1 text-xs bg-success/10 text-success rounded-md border border-success/20">
<i data-lucide="file-check" class="w-3.5 h-3.5"></i>
.mcp.json
<span class="inline-flex items-center gap-1 px-1.5 py-0.5 text-[10px] bg-success/10 text-success rounded border border-success/20">
<i data-lucide="check" class="w-2.5 h-2.5"></i>
exists
</span>
` : `
<span class="inline-flex items-center gap-1.5 px-2 py-1 text-xs bg-muted text-muted-foreground rounded-md border border-border" title="New servers will create .mcp.json">
<i data-lucide="file-plus" class="w-3.5 h-3.5"></i>
Will use .mcp.json
</span>
`}
` : ''}
</div>
<span class="text-sm text-muted-foreground">${projectAvailableEntries.length} ${t('mcp.serversAvailable')}</span>
</div>
@@ -569,30 +575,46 @@ async function renderMcpManager() {
<span class="text-sm text-muted-foreground">${mcpTemplates.length} ${t('mcp.savedTemplates')}</span>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<div class="mcp-server-grid grid gap-3">
${mcpTemplates.map(template => `
<div class="mcp-template-card bg-card border border-border rounded-lg p-4 hover:shadow-md transition-all">
<div class="mcp-template-card mcp-server-card bg-card border border-border border-dashed rounded-lg p-4 hover:shadow-md hover:border-solid transition-all">
<div class="flex items-start justify-between mb-3">
<div class="flex-1 min-w-0">
<h4 class="font-semibold text-foreground truncate flex items-center gap-2">
<i data-lucide="layout-template" class="w-4 h-4 shrink-0"></i>
<span class="truncate">${escapeHtml(template.name)}</span>
</h4>
<div class="flex items-center gap-2 flex-wrap">
<span><i data-lucide="layout-template" class="w-5 h-5 text-muted-foreground"></i></span>
<h4 class="font-semibold text-foreground">${escapeHtml(template.name)}</h4>
${template.description ? `
<p class="text-xs text-muted-foreground mt-1 line-clamp-2">${escapeHtml(template.description)}</p>
<span class="text-xs px-2 py-0.5 bg-muted text-muted-foreground rounded-full truncate max-w-32" title="${escapeHtml(template.description)}">
${escapeHtml(template.description)}
</span>
` : ''}
</div>
<div class="flex gap-2">
<button class="px-3 py-1 text-xs bg-primary text-primary-foreground rounded hover:opacity-90 transition-opacity"
data-template-name="${escapeHtml(template.name)}"
data-scope="project"
data-action="install-template"
title="${t('mcp.installToProject')}">
<i data-lucide="folder-plus" class="w-3.5 h-3.5 inline"></i>
</button>
<button class="px-3 py-1 text-xs bg-success text-success-foreground rounded hover:opacity-90 transition-opacity"
data-template-name="${escapeHtml(template.name)}"
data-scope="global"
data-action="install-template"
title="${t('mcp.installToGlobal')}">
<i data-lucide="globe" class="w-3.5 h-3.5 inline"></i>
</button>
</div>
</div>
<div class="mcp-server-details text-sm space-y-1 mb-3">
<div class="mcp-server-details text-sm space-y-1">
<div class="flex items-center gap-2 text-muted-foreground">
<span class="font-mono text-xs bg-muted px-1.5 py-0.5 rounded">${t('mcp.cmd')}</span>
<span class="truncate text-xs" title="${escapeHtml(template.serverConfig.command)}">${escapeHtml(template.serverConfig.command)}</span>
<span class="truncate" title="${escapeHtml(template.serverConfig.command)}">${escapeHtml(template.serverConfig.command)}</span>
</div>
${template.serverConfig.args && template.serverConfig.args.length > 0 ? `
<div class="flex items-start gap-2 text-muted-foreground">
<span class="font-mono text-xs bg-muted px-1.5 py-0.5 rounded shrink-0">${t('mcp.args')}</span>
<span class="text-xs font-mono truncate" title="${escapeHtml(template.serverConfig.args.join(' '))}">${escapeHtml(template.serverConfig.args.slice(0, 2).join(' '))}${template.serverConfig.args.length > 2 ? '...' : ''}</span>
<span class="text-xs font-mono truncate" title="${escapeHtml(template.serverConfig.args.join(' '))}">${escapeHtml(template.serverConfig.args.slice(0, 3).join(' '))}${template.serverConfig.args.length > 3 ? '...' : ''}</span>
</div>
` : ''}
</div>

View File

@@ -45,6 +45,12 @@ const ParamsSchema = z.object({
mode: z.enum(['auto', 'text', 'semantic', 'exact', 'fuzzy', 'hybrid', 'vector', 'pure-vector']).default('auto'),
languages: z.array(z.string()).optional(),
limit: z.number().default(20),
// Additional fields for internal functions
file: z.string().optional(),
key: z.string().optional(),
value: z.string().optional(),
newPath: z.string().optional(),
all: z.boolean().optional(),
});
type Params = z.infer<typeof ParamsSchema>;

View File

@@ -18,7 +18,7 @@ import * as convertTokensToCssMod from './convert-tokens-to-css.js';
import * as sessionManagerMod from './session-manager.js';
import * as cliExecutorMod from './cli-executor.js';
import * as smartSearchMod from './smart-search.js';
import * as codexLensMod from './codex-lens.js';
// codex_lens removed - functionality integrated into smart_search
import * as readFileMod from './read-file.js';
// Import legacy JS tools
@@ -297,7 +297,7 @@ registerTool(toLegacyTool(convertTokensToCssMod));
registerTool(toLegacyTool(sessionManagerMod));
registerTool(toLegacyTool(cliExecutorMod));
registerTool(toLegacyTool(smartSearchMod));
registerTool(toLegacyTool(codexLensMod));
// codex_lens removed - functionality integrated into smart_search
registerTool(toLegacyTool(readFileMod));
// Register legacy JS tools