bump version to 6.2.6 in package.json

This commit is contained in:
catlog22
2025-12-22 20:17:38 +08:00
parent 6a69af3bf1
commit cf58dc0dd3
17 changed files with 2075 additions and 69 deletions

8
ccw/package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "ccw", "name": "claude-code-workflow",
"version": "6.1.4", "version": "6.2.6",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "ccw", "name": "claude-code-workflow",
"version": "6.1.4", "version": "6.2.6",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@modelcontextprotocol/sdk": "^1.0.4", "@modelcontextprotocol/sdk": "^1.0.4",

View File

@@ -1,6 +1,6 @@
{ {
"name": "claude-code-workflow", "name": "claude-code-workflow",
"version": "6.2.4", "version": "6.2.6",
"description": "Claude Code Workflow CLI - Dashboard viewer for workflow sessions and reviews", "description": "Claude Code Workflow CLI - Dashboard viewer for workflow sessions and reviews",
"type": "module", "type": "module",
"main": "dist/index.js", "main": "dist/index.js",

View File

@@ -284,8 +284,12 @@ function normalizeTask(task: unknown): NormalizedTask | null {
const implementation = taskObj.implementation as unknown[] | undefined; const implementation = taskObj.implementation as unknown[] | undefined;
const modificationPoints = taskObj.modification_points as Array<{ file?: string }> | undefined; const modificationPoints = taskObj.modification_points as Array<{ file?: string }> | undefined;
// Ensure id is always a string (handle numeric IDs from JSON)
const rawId = taskObj.id ?? taskObj.task_id;
const stringId = rawId != null ? String(rawId) : 'unknown';
return { return {
id: (taskObj.id as string) || (taskObj.task_id as string) || 'unknown', id: stringId,
title: (taskObj.title as string) || (taskObj.name as string) || (taskObj.summary as string) || 'Untitled Task', title: (taskObj.title as string) || (taskObj.name as string) || (taskObj.summary as string) || 'Untitled Task',
status: (status as string).toLowerCase(), status: (status as string).toLowerCase(),
// Preserve original fields for flexible rendering // Preserve original fields for flexible rendering

View File

@@ -284,8 +284,12 @@ function normalizeTask(task: unknown): NormalizedTask | null {
const implementation = taskObj.implementation as unknown[] | undefined; const implementation = taskObj.implementation as unknown[] | undefined;
const modificationPoints = taskObj.modification_points as Array<{ file?: string }> | undefined; const modificationPoints = taskObj.modification_points as Array<{ file?: string }> | undefined;
// Ensure id is always a string (handle numeric IDs from JSON)
const rawId = taskObj.id ?? taskObj.task_id;
const stringId = rawId != null ? String(rawId) : 'unknown';
return { return {
id: (taskObj.id as string) || (taskObj.task_id as string) || 'unknown', id: stringId,
title: (taskObj.title as string) || (taskObj.name as string) || (taskObj.summary as string) || 'Untitled Task', title: (taskObj.title as string) || (taskObj.name as string) || (taskObj.summary as string) || 'Untitled Task',
status: (status as string).toLowerCase(), status: (status as string).toLowerCase(),
// Preserve original fields for flexible rendering // Preserve original fields for flexible rendering

View File

@@ -4,7 +4,7 @@
* Handles all CLAUDE.md memory rules management endpoints * Handles all CLAUDE.md memory rules management endpoints
*/ */
import type { IncomingMessage, ServerResponse } from 'http'; import type { IncomingMessage, ServerResponse } from 'http';
import { readFileSync, writeFileSync, existsSync, readdirSync, statSync } from 'fs'; import { readFileSync, writeFileSync, existsSync, readdirSync, statSync, unlinkSync, mkdirSync } from 'fs';
import { join, relative } from 'path'; import { join, relative } from 'path';
import { homedir } from 'os'; import { homedir } from 'os';
@@ -453,8 +453,7 @@ function deleteClaudeFile(filePath: string): { success: boolean; error?: string
writeFileSync(backupPath, content, 'utf8'); writeFileSync(backupPath, content, 'utf8');
// Delete original file // Delete original file
const fs = require('fs'); unlinkSync(filePath);
fs.unlinkSync(filePath);
return { success: true }; return { success: true };
} catch (error) { } catch (error) {
@@ -500,9 +499,8 @@ function createNewClaudeFile(level: 'user' | 'project' | 'module', template: str
// Ensure directory exists // Ensure directory exists
const dir = filePath.substring(0, filePath.lastIndexOf('/') || filePath.lastIndexOf('\\')); const dir = filePath.substring(0, filePath.lastIndexOf('/') || filePath.lastIndexOf('\\'));
const fs = require('fs');
if (!existsSync(dir)) { if (!existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true }); mkdirSync(dir, { recursive: true });
} }
// Write file // Write file

View File

@@ -362,8 +362,9 @@ export async function handleCliRoutes(ctx: RouteContext): Promise<boolean> {
const status = url.searchParams.get('status') || null; const status = url.searchParams.get('status') || null;
const category = url.searchParams.get('category') as 'user' | 'internal' | 'insight' | null; const category = url.searchParams.get('category') as 'user' | 'internal' | 'insight' | null;
const search = url.searchParams.get('search') || null; const search = url.searchParams.get('search') || null;
const recursive = url.searchParams.get('recursive') !== 'false';
getHistoryWithNativeInfo(projectPath, { limit, tool, status, category, search }) getHistoryWithNativeInfo(projectPath, { limit, tool, status, category, search, recursive })
.then(history => { .then(history => {
res.writeHead(200, { 'Content-Type': 'application/json' }); res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(history)); res.end(JSON.stringify(history));

View File

@@ -291,13 +291,14 @@ FOCUS AREAS: ${extractFocus || 'naming conventions, error handling, code structu
return { error: `Unknown generation type: ${generationType}` }; return { error: `Unknown generation type: ${generationType}` };
} }
// Execute CLI tool (Gemini) with at least 10 minutes timeout // Execute CLI tool (Claude) with at least 10 minutes timeout
const result = await executeCliTool({ const result = await executeCliTool({
tool: 'gemini', tool: 'claude',
prompt, prompt,
mode, mode,
cd: workingDir, cd: workingDir,
timeout: 600000 // 10 minutes timeout: 600000, // 10 minutes
category: 'internal'
}); });
if (!result.success) { if (!result.success) {

View File

@@ -123,6 +123,7 @@ function getSkillsConfig(projectPath) {
result.projectSkills.push({ result.projectSkills.push({
name: parsed.name || skill.name, name: parsed.name || skill.name,
folderName: skill.name, // Actual folder name for API queries
description: parsed.description, description: parsed.description,
version: parsed.version, version: parsed.version,
allowedTools: parsed.allowedTools, allowedTools: parsed.allowedTools,
@@ -152,6 +153,7 @@ function getSkillsConfig(projectPath) {
result.userSkills.push({ result.userSkills.push({
name: parsed.name || skill.name, name: parsed.name || skill.name,
folderName: skill.name, // Actual folder name for API queries
description: parsed.description, description: parsed.description,
version: parsed.version, version: parsed.version,
allowedTools: parsed.allowedTools, allowedTools: parsed.allowedTools,
@@ -197,6 +199,7 @@ function getSkillDetail(skillName, location, projectPath) {
return { return {
skill: { skill: {
name: parsed.name || skillName, name: parsed.name || skillName,
folderName: skillName, // Actual folder name for API queries
description: parsed.description, description: parsed.description,
version: parsed.version, version: parsed.version,
allowedTools: parsed.allowedTools, allowedTools: parsed.allowedTools,
@@ -390,7 +393,7 @@ async function importSkill(sourcePath, location, projectPath, customName) {
} }
/** /**
* Generate skill via CLI tool (Gemini) * Generate skill via CLI tool (Claude)
* @param {Object} params - Generation parameters * @param {Object} params - Generation parameters
* @param {string} params.generationType - 'description' or 'template' * @param {string} params.generationType - 'description' or 'template'
* @param {string} params.description - Skill description from user * @param {string} params.description - Skill description from user
@@ -455,9 +458,9 @@ REQUIREMENTS:
3. If the skill requires supporting files (e.g., templates, scripts), create them in the skill folder 3. If the skill requires supporting files (e.g., templates, scripts), create them in the skill folder
4. Ensure all files are properly formatted and follow best practices`; 4. Ensure all files are properly formatted and follow best practices`;
// Execute CLI tool (Gemini) with write mode // Execute CLI tool (Claude) with write mode
const result = await executeCliTool({ const result = await executeCliTool({
tool: 'gemini', tool: 'claude',
prompt, prompt,
mode: 'write', mode: 'write',
cd: baseDir, cd: baseDir,
@@ -515,8 +518,143 @@ export async function handleSkillsRoutes(ctx: RouteContext): Promise<boolean> {
return true; return true;
} }
// API: Get single skill detail // API: List skill directory contents
if (pathname.startsWith('/api/skills/') && req.method === 'GET' && !pathname.endsWith('/skills/')) { if (pathname.match(/^\/api\/skills\/[^/]+\/dir$/) && req.method === 'GET') {
const pathParts = pathname.split('/');
const skillName = decodeURIComponent(pathParts[3]);
const subPath = url.searchParams.get('subpath') || '';
const location = url.searchParams.get('location') || 'project';
const projectPathParam = url.searchParams.get('path') || initialPath;
const baseDir = location === 'project'
? join(projectPathParam, '.claude', 'skills')
: join(homedir(), '.claude', 'skills');
const dirPath = subPath
? join(baseDir, skillName, subPath)
: join(baseDir, skillName);
// Security check: ensure path is within skill folder
if (!dirPath.startsWith(join(baseDir, skillName))) {
res.writeHead(403, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Access denied' }));
return true;
}
if (!existsSync(dirPath)) {
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Directory not found' }));
return true;
}
try {
const stat = statSync(dirPath);
if (!stat.isDirectory()) {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Path is not a directory' }));
return true;
}
const entries = readdirSync(dirPath, { withFileTypes: true });
const files = entries.map(entry => ({
name: entry.name,
isDirectory: entry.isDirectory(),
path: subPath ? `${subPath}/${entry.name}` : entry.name
}));
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ files, subPath, skillName }));
} catch (error) {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: (error as Error).message }));
}
return true;
}
// API: Read skill file content
if (pathname.match(/^\/api\/skills\/[^/]+\/file$/) && req.method === 'GET') {
const pathParts = pathname.split('/');
const skillName = decodeURIComponent(pathParts[3]);
const fileName = url.searchParams.get('filename');
const location = url.searchParams.get('location') || 'project';
const projectPathParam = url.searchParams.get('path') || initialPath;
if (!fileName) {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'filename parameter is required' }));
return true;
}
const baseDir = location === 'project'
? join(projectPathParam, '.claude', 'skills')
: join(homedir(), '.claude', 'skills');
const filePath = join(baseDir, skillName, fileName);
// Security check: ensure file is within skill folder
if (!filePath.startsWith(join(baseDir, skillName))) {
res.writeHead(403, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Access denied' }));
return true;
}
if (!existsSync(filePath)) {
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'File not found' }));
return true;
}
try {
const content = readFileSync(filePath, 'utf8');
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ content, fileName, path: filePath }));
} catch (error) {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: (error as Error).message }));
}
return true;
}
// API: Write skill file content
if (pathname.match(/^\/api\/skills\/[^/]+\/file$/) && req.method === 'POST') {
const pathParts = pathname.split('/');
const skillName = decodeURIComponent(pathParts[3]);
handlePostRequest(req, res, async (body) => {
const { fileName, content, location, projectPath: projectPathParam } = body;
if (!fileName) {
return { error: 'fileName is required' };
}
if (content === undefined) {
return { error: 'content is required' };
}
const baseDir = location === 'project'
? join(projectPathParam || initialPath, '.claude', 'skills')
: join(homedir(), '.claude', 'skills');
const filePath = join(baseDir, skillName, fileName);
// Security check: ensure file is within skill folder
if (!filePath.startsWith(join(baseDir, skillName))) {
return { error: 'Access denied' };
}
try {
await fsPromises.writeFile(filePath, content, 'utf8');
return { success: true, fileName, path: filePath };
} catch (error) {
return { error: (error as Error).message };
}
});
return true;
}
// API: Get single skill detail (exclude /dir and /file sub-routes)
if (pathname.startsWith('/api/skills/') && req.method === 'GET' &&
!pathname.endsWith('/skills/') && !pathname.endsWith('/dir') && !pathname.endsWith('/file')) {
const skillName = decodeURIComponent(pathname.replace('/api/skills/', '')); const skillName = decodeURIComponent(pathname.replace('/api/skills/', ''));
const location = url.searchParams.get('location') || 'project'; const location = url.searchParams.get('location') || 'project';
const projectPathParam = url.searchParams.get('path') || initialPath; const projectPathParam = url.searchParams.get('path') || initialPath;
@@ -576,7 +714,7 @@ export async function handleSkillsRoutes(ctx: RouteContext): Promise<boolean> {
return await importSkill(sourcePath, location, projectPath, skillName); return await importSkill(sourcePath, location, projectPath, skillName);
} else if (mode === 'cli-generate') { } else if (mode === 'cli-generate') {
// CLI generate mode: use Gemini to generate skill // CLI generate mode: use Claude to generate skill
if (!skillName) { if (!skillName) {
return { error: 'Skill name is required for CLI generation mode' }; return { error: 'Skill name is required for CLI generation mode' };
} }

View File

@@ -411,9 +411,12 @@ const i18n = {
'index.fullDesc': 'FTS + Semantic search (recommended)', 'index.fullDesc': 'FTS + Semantic search (recommended)',
'index.selectModel': 'Select embedding model', 'index.selectModel': 'Select embedding model',
'index.modelCode': 'Code (768d)', 'index.modelCode': 'Code (768d)',
'index.modelBase': 'Base (768d)',
'index.modelFast': 'Fast (384d)', 'index.modelFast': 'Fast (384d)',
'index.modelMultilingual': 'Multilingual (1024d)', 'index.modelMinilm': 'MiniLM (384d)',
'index.modelBalanced': 'Balanced (1024d)', 'index.modelMultilingual': 'Multilingual (1024d) ⚠️',
'index.modelBalanced': 'Balanced (1024d) ⚠️',
'index.dimensionWarning': '1024d models require more resources',
// Semantic Search Configuration // Semantic Search Configuration
'semantic.settings': 'Semantic Search Settings', 'semantic.settings': 'Semantic Search Settings',
@@ -1824,9 +1827,12 @@ const i18n = {
'index.fullDesc': 'FTS + 语义搜索(推荐)', 'index.fullDesc': 'FTS + 语义搜索(推荐)',
'index.selectModel': '选择嵌入模型', 'index.selectModel': '选择嵌入模型',
'index.modelCode': '代码优化 (768维)', 'index.modelCode': '代码优化 (768维)',
'index.modelBase': '通用基础 (768维)',
'index.modelFast': '快速轻量 (384维)', 'index.modelFast': '快速轻量 (384维)',
'index.modelMultilingual': '多语言 (1024维)', 'index.modelMinilm': 'MiniLM (384维)',
'index.modelBalanced': '高精度 (1024维)', 'index.modelMultilingual': '多语言 (1024维) ⚠️',
'index.modelBalanced': '高精度 (1024维) ⚠️',
'index.dimensionWarning': '1024维模型需要更多资源',
// Semantic Search 配置 // Semantic Search 配置
'semantic.settings': '语义搜索设置', 'semantic.settings': '语义搜索设置',

View File

@@ -102,6 +102,7 @@ async function loadClaudeFiles() {
updateClaudeBadge(); // Update navigation badge updateClaudeBadge(); // Update navigation badge
} catch (error) { } catch (error) {
console.error('Error loading CLAUDE.md files:', error); console.error('Error loading CLAUDE.md files:', error);
showRefreshToast(t('claudeManager.loadError') || 'Failed to load files', 'error');
addGlobalNotification('error', t('claudeManager.loadError'), null, 'CLAUDE.md'); addGlobalNotification('error', t('claudeManager.loadError'), null, 'CLAUDE.md');
} }
} }
@@ -113,6 +114,7 @@ async function refreshClaudeFiles() {
renderFileViewer(); renderFileViewer();
renderFileMetadata(); renderFileMetadata();
if (window.lucide) lucide.createIcons(); if (window.lucide) lucide.createIcons();
showRefreshToast(t('claudeManager.refreshed') || 'Files refreshed', 'success');
addGlobalNotification('success', t('claudeManager.refreshed'), null, 'CLAUDE.md'); addGlobalNotification('success', t('claudeManager.refreshed'), null, 'CLAUDE.md');
// Load freshness data in background // Load freshness data in background
loadFreshnessDataAsync(); loadFreshnessDataAsync();
@@ -155,6 +157,7 @@ async function markFileAsUpdated() {
if (!res.ok) throw new Error('Failed to mark file as updated'); if (!res.ok) throw new Error('Failed to mark file as updated');
showRefreshToast(t('claudeManager.markedAsUpdated') || 'Marked as updated', 'success');
addGlobalNotification('success', t('claudeManager.markedAsUpdated') || 'Marked as updated', null, 'CLAUDE.md'); addGlobalNotification('success', t('claudeManager.markedAsUpdated') || 'Marked as updated', null, 'CLAUDE.md');
// Reload freshness data // Reload freshness data
@@ -163,6 +166,7 @@ async function markFileAsUpdated() {
renderFileMetadata(); renderFileMetadata();
} catch (error) { } catch (error) {
console.error('Error marking file as updated:', error); console.error('Error marking file as updated:', error);
showRefreshToast(t('claudeManager.markUpdateError') || 'Failed to mark as updated', 'error');
addGlobalNotification('error', t('claudeManager.markUpdateError') || 'Failed to mark as updated', null, 'CLAUDE.md'); addGlobalNotification('error', t('claudeManager.markUpdateError') || 'Failed to mark as updated', null, 'CLAUDE.md');
} }
} }
@@ -481,10 +485,12 @@ async function saveClaudeFile() {
selectedFile.stats = calculateFileStats(newContent); selectedFile.stats = calculateFileStats(newContent);
isDirty = false; isDirty = false;
showRefreshToast(t('claudeManager.saved') || 'File saved', 'success');
addGlobalNotification('success', t('claudeManager.saved'), null, 'CLAUDE.md'); addGlobalNotification('success', t('claudeManager.saved'), null, 'CLAUDE.md');
renderFileMetadata(); renderFileMetadata();
} catch (error) { } catch (error) {
console.error('Error saving file:', error); console.error('Error saving file:', error);
showRefreshToast(t('claudeManager.saveError') || 'Save failed', 'error');
addGlobalNotification('error', t('claudeManager.saveError'), null, 'CLAUDE.md'); addGlobalNotification('error', t('claudeManager.saveError'), null, 'CLAUDE.md');
} }
} }
@@ -733,12 +739,13 @@ async function loadFileContent(filePath) {
} }
function showClaudeNotification(type, message) { function showClaudeNotification(type, message) {
// Use global notification system if available // Show toast for immediate feedback
if (typeof showRefreshToast === 'function') {
showRefreshToast(message, type);
}
// Also add to global notification system if available
if (typeof addGlobalNotification === 'function') { if (typeof addGlobalNotification === 'function') {
addGlobalNotification(type, message, null, 'CLAUDE.md'); addGlobalNotification(type, message, null, 'CLAUDE.md');
} else {
// Fallback to simple alert
alert(message);
} }
} }
@@ -822,6 +829,7 @@ async function createNewFile() {
var modulePath = document.getElementById('modulePath').value; var modulePath = document.getElementById('modulePath').value;
if (level === 'module' && !modulePath) { if (level === 'module' && !modulePath) {
showRefreshToast(t('claude.modulePathRequired') || 'Module path is required', 'error');
addGlobalNotification('error', t('claude.modulePathRequired') || 'Module path is required', null, 'CLAUDE.md'); addGlobalNotification('error', t('claude.modulePathRequired') || 'Module path is required', null, 'CLAUDE.md');
return; return;
} }
@@ -841,12 +849,14 @@ async function createNewFile() {
var result = await res.json(); var result = await res.json();
closeCreateDialog(); closeCreateDialog();
showRefreshToast(t('claude.fileCreated') || 'File created successfully', 'success');
addGlobalNotification('success', t('claude.fileCreated') || 'File created successfully', null, 'CLAUDE.md'); addGlobalNotification('success', t('claude.fileCreated') || 'File created successfully', null, 'CLAUDE.md');
// Refresh file tree // Refresh file tree
await refreshClaudeFiles(); await refreshClaudeFiles();
} catch (error) { } catch (error) {
console.error('Error creating file:', error); console.error('Error creating file:', error);
showRefreshToast(t('claude.createFileError') || 'Failed to create file', 'error');
addGlobalNotification('error', t('claude.createFileError') || 'Failed to create file', null, 'CLAUDE.md'); addGlobalNotification('error', t('claude.createFileError') || 'Failed to create file', null, 'CLAUDE.md');
} }
} }
@@ -870,6 +880,7 @@ async function confirmDeleteFile() {
if (!res.ok) throw new Error('Failed to delete file'); if (!res.ok) throw new Error('Failed to delete file');
showRefreshToast(t('claude.fileDeleted') || 'File deleted successfully', 'success');
addGlobalNotification('success', t('claude.fileDeleted') || 'File deleted successfully', null, 'CLAUDE.md'); addGlobalNotification('success', t('claude.fileDeleted') || 'File deleted successfully', null, 'CLAUDE.md');
selectedFile = null; selectedFile = null;
@@ -877,6 +888,7 @@ async function confirmDeleteFile() {
await refreshClaudeFiles(); await refreshClaudeFiles();
} catch (error) { } catch (error) {
console.error('Error deleting file:', error); console.error('Error deleting file:', error);
showRefreshToast(t('claude.deleteFileError') || 'Failed to delete file', 'error');
addGlobalNotification('error', t('claude.deleteFileError') || 'Failed to delete file', null, 'CLAUDE.md'); addGlobalNotification('error', t('claude.deleteFileError') || 'Failed to delete file', null, 'CLAUDE.md');
} }
} }
@@ -886,9 +898,11 @@ function copyFileContent() {
if (!selectedFile || !selectedFile.content) return; if (!selectedFile || !selectedFile.content) return;
navigator.clipboard.writeText(selectedFile.content).then(function() { navigator.clipboard.writeText(selectedFile.content).then(function() {
showRefreshToast(t('claude.contentCopied') || 'Content copied to clipboard', 'success');
addGlobalNotification('success', t('claude.contentCopied') || 'Content copied to clipboard', null, 'CLAUDE.md'); addGlobalNotification('success', t('claude.contentCopied') || 'Content copied to clipboard', null, 'CLAUDE.md');
}).catch(function(error) { }).catch(function(error) {
console.error('Error copying content:', error); console.error('Error copying content:', error);
showRefreshToast(t('claude.copyError') || 'Failed to copy content', 'error');
addGlobalNotification('error', t('claude.copyError') || 'Failed to copy content', null, 'CLAUDE.md'); addGlobalNotification('error', t('claude.copyError') || 'Failed to copy content', null, 'CLAUDE.md');
}); });
} }

View File

@@ -404,10 +404,12 @@ function renderToolsSection() {
(codexLensStatus.ready (codexLensStatus.ready
? '<span class="tool-status-text success"><i data-lucide="check-circle" class="w-3.5 h-3.5"></i> v' + (codexLensStatus.version || 'installed') + '</span>' + ? '<span class="tool-status-text success"><i data-lucide="check-circle" class="w-3.5 h-3.5"></i> v' + (codexLensStatus.version || 'installed') + '</span>' +
'<select id="codexlensModelSelect" class="btn-sm bg-muted border border-border rounded text-xs" onclick="event.stopPropagation()" title="' + (t('index.selectModel') || 'Select embedding model') + '">' + '<select id="codexlensModelSelect" class="btn-sm bg-muted border border-border rounded text-xs" onclick="event.stopPropagation()" title="' + (t('index.selectModel') || 'Select embedding model') + '">' +
'<option value="code">' + (t('index.modelCode') || 'Code (768d)') + '</option>' + '<option value="code">' + (t('index.modelCode') || 'Code (768d)') + '</option>' +
'<option value="base">' + (t('index.modelBase') || 'Base (768d)') + '</option>' +
'<option value="fast">' + (t('index.modelFast') || 'Fast (384d)') + '</option>' + '<option value="fast">' + (t('index.modelFast') || 'Fast (384d)') + '</option>' +
'<option value="multilingual">' + (t('index.modelMultilingual') || 'Multilingual (1024d)') + '</option>' + '<option value="minilm">' + (t('index.modelMinilm') || 'MiniLM (384d)') + '</option>' +
'<option value="balanced">' + (t('index.modelBalanced') || 'Balanced (1024d)') + '</option>' + '<option value="multilingual" style="color: var(--muted-foreground)">' + (t('index.modelMultilingual') || 'Multilingual (1024d)') + ' ⚠️</option>' +
'<option value="balanced" style="color: var(--muted-foreground)">' + (t('index.modelBalanced') || 'Balanced (1024d)') + ' ⚠️</option>' +
'</select>' + '</select>' +
'<button class="btn-sm btn-primary" onclick="event.stopPropagation(); initCodexLensIndex(\'full\', getSelectedModel())" title="' + (t('index.fullDesc') || 'FTS + Semantic search (recommended)') + '"><i data-lucide="layers" class="w-3 h-3"></i> ' + (t('index.fullIndex') || '全部索引') + '</button>' + '<button class="btn-sm btn-primary" onclick="event.stopPropagation(); initCodexLensIndex(\'full\', getSelectedModel())" title="' + (t('index.fullDesc') || 'FTS + Semantic search (recommended)') + '"><i data-lucide="layers" class="w-3 h-3"></i> ' + (t('index.fullIndex') || '全部索引') + '</button>' +
'<button class="btn-sm btn-outline" onclick="event.stopPropagation(); initCodexLensIndex(\'vector\', getSelectedModel())" title="' + (t('index.vectorDesc') || 'Semantic search with embeddings') + '"><i data-lucide="sparkles" class="w-3 h-3"></i> ' + (t('index.vectorIndex') || '向量索引') + '</button>' + '<button class="btn-sm btn-outline" onclick="event.stopPropagation(); initCodexLensIndex(\'vector\', getSelectedModel())" title="' + (t('index.vectorDesc') || 'Semantic search with embeddings') + '"><i data-lucide="sparkles" class="w-3 h-3"></i> ' + (t('index.vectorIndex') || '向量索引') + '</button>' +

View File

@@ -638,9 +638,26 @@ function addRulePath() {
function removeRulePath(index) { function removeRulePath(index) {
ruleCreateState.paths.splice(index, 1); ruleCreateState.paths.splice(index, 1);
// Re-render paths list
closeRuleCreateModal(); // Re-render paths list without closing modal
openRuleCreateModal(); const pathsList = document.getElementById('rulePathsList');
if (pathsList) {
pathsList.innerHTML = ruleCreateState.paths.map((path, idx) => `
<div class="flex gap-2">
<input type="text" class="rule-path-input flex-1 px-3 py-2 bg-background border border-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary"
placeholder="src/**/*.ts"
value="${path}"
data-index="${idx}">
${idx > 0 ? `
<button class="px-3 py-2 text-destructive hover:bg-destructive/10 rounded-lg transition-colors"
onclick="removeRulePath(${idx})">
<i data-lucide="x" class="w-4 h-4"></i>
</button>
` : ''}
</div>
`).join('');
if (typeof lucide !== 'undefined') lucide.createIcons();
}
} }
function switchRuleCreateMode(mode) { function switchRuleCreateMode(mode) {
@@ -674,9 +691,21 @@ function switchRuleCreateMode(mode) {
if (contentSection) contentSection.style.display = 'block'; if (contentSection) contentSection.style.display = 'block';
} }
// Re-render modal to update button states // Update mode button styles without re-rendering
closeRuleCreateModal(); const modeButtons = document.querySelectorAll('#ruleCreateModal .mode-btn');
openRuleCreateModal(); modeButtons.forEach(btn => {
const btnText = btn.querySelector('.font-medium')?.textContent || '';
const isInput = btnText.includes(t('rules.manualInput'));
const isCliGenerate = btnText.includes(t('rules.cliGenerate'));
if ((isInput && mode === 'input') || (isCliGenerate && mode === 'cli-generate')) {
btn.classList.remove('border-border', 'hover:border-primary/50');
btn.classList.add('border-primary', 'bg-primary/10');
} else {
btn.classList.remove('border-primary', 'bg-primary/10');
btn.classList.add('border-border', 'hover:border-primary/50');
}
});
} }
function switchRuleGenerationType(type) { function switchRuleGenerationType(type) {

View File

@@ -153,10 +153,11 @@ function renderSkillCard(skill, location) {
const locationIcon = location === 'project' ? 'folder' : 'user'; const locationIcon = location === 'project' ? 'folder' : 'user';
const locationClass = location === 'project' ? 'text-primary' : 'text-indigo'; const locationClass = location === 'project' ? 'text-primary' : 'text-indigo';
const locationBg = location === 'project' ? 'bg-primary/10' : 'bg-indigo/10'; const locationBg = location === 'project' ? 'bg-primary/10' : 'bg-indigo/10';
const folderName = skill.folderName || skill.name;
return ` return `
<div class="skill-card bg-card border border-border rounded-lg p-4 hover:shadow-md transition-all cursor-pointer" <div class="skill-card bg-card border border-border rounded-lg p-4 hover:shadow-md transition-all cursor-pointer"
onclick="showSkillDetail('${escapeHtml(skill.name)}', '${location}')"> onclick="showSkillDetail('${escapeHtml(folderName)}', '${location}')">
<div class="flex items-start justify-between mb-3"> <div class="flex items-start justify-between mb-3">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<div class="w-10 h-10 ${locationBg} rounded-lg flex items-center justify-center"> <div class="w-10 h-10 ${locationBg} rounded-lg flex items-center justify-center">
@@ -198,6 +199,7 @@ function renderSkillCard(skill, location) {
function renderSkillDetailPanel(skill) { function renderSkillDetailPanel(skill) {
const hasAllowedTools = skill.allowedTools && skill.allowedTools.length > 0; const hasAllowedTools = skill.allowedTools && skill.allowedTools.length > 0;
const hasSupportingFiles = skill.supportingFiles && skill.supportingFiles.length > 0; const hasSupportingFiles = skill.supportingFiles && skill.supportingFiles.length > 0;
const folderName = skill.folderName || skill.name;
return ` return `
<div class="skill-detail-panel fixed top-0 right-0 w-1/2 max-w-xl h-full bg-card border-l border-border shadow-lg z-50 flex flex-col"> <div class="skill-detail-panel fixed top-0 right-0 w-1/2 max-w-xl h-full bg-card border-l border-border shadow-lg z-50 flex flex-col">
@@ -243,20 +245,54 @@ function renderSkillDetailPanel(skill) {
</div> </div>
` : ''} ` : ''}
<!-- Supporting Files --> <!-- Skill Files (SKILL.md + Supporting Files) -->
${hasSupportingFiles ? ` <div>
<div> <h4 class="text-sm font-semibold text-foreground mb-2">${t('skills.files') || 'Files'}</h4>
<h4 class="text-sm font-semibold text-foreground mb-2">${t('skills.supportingFiles')}</h4> <div class="space-y-2">
<div class="space-y-2"> <!-- SKILL.md (main file) -->
${skill.supportingFiles.map(file => ` <div class="flex items-center justify-between p-2 bg-primary/5 border border-primary/20 rounded-lg cursor-pointer hover:bg-primary/10 transition-colors"
<div class="flex items-center gap-2 p-2 bg-muted/50 rounded-lg"> onclick="viewSkillFile('${escapeHtml(folderName)}', 'SKILL.md', '${skill.location}')">
<i data-lucide="file-text" class="w-4 h-4 text-muted-foreground"></i> <div class="flex items-center gap-2">
<span class="text-sm font-mono text-foreground">${escapeHtml(file)}</span> <i data-lucide="file-text" class="w-4 h-4 text-primary"></i>
</div> <span class="text-sm font-mono text-foreground font-medium">SKILL.md</span>
`).join('')} </div>
<div class="flex items-center gap-1">
<button class="p-1 text-primary hover:bg-primary/20 rounded transition-colors"
onclick="event.stopPropagation(); editSkillFile('${escapeHtml(folderName)}', 'SKILL.md', '${skill.location}')"
title="${t('common.edit')}">
<i data-lucide="edit-2" class="w-3.5 h-3.5"></i>
</button>
</div>
</div> </div>
${hasSupportingFiles ? skill.supportingFiles.map(file => {
const isDir = file.endsWith('/');
const dirName = isDir ? file.slice(0, -1) : file;
return `
<!-- Supporting file: ${escapeHtml(file)} -->
<div class="skill-file-item" data-path="${escapeHtml(dirName)}">
<div class="flex items-center justify-between p-2 bg-muted/50 rounded-lg cursor-pointer hover:bg-muted transition-colors"
onclick="${isDir ? `toggleSkillFolder('${escapeHtml(folderName)}', '${escapeHtml(dirName)}', '${skill.location}', this)` : `viewSkillFile('${escapeHtml(folderName)}', '${escapeHtml(file)}', '${skill.location}')`}">
<div class="flex items-center gap-2">
<i data-lucide="${isDir ? 'folder' : 'file-text'}" class="w-4 h-4 text-muted-foreground ${isDir ? 'folder-icon' : ''}"></i>
<span class="text-sm font-mono text-foreground">${escapeHtml(isDir ? dirName : file)}</span>
${isDir ? '<i data-lucide="chevron-right" class="w-3 h-3 text-muted-foreground folder-chevron transition-transform"></i>' : ''}
</div>
${!isDir ? `
<div class="flex items-center gap-1">
<button class="p-1 text-muted-foreground hover:text-foreground hover:bg-muted rounded transition-colors"
onclick="event.stopPropagation(); editSkillFile('${escapeHtml(folderName)}', '${escapeHtml(file)}', '${skill.location}')"
title="${t('common.edit')}">
<i data-lucide="edit-2" class="w-3.5 h-3.5"></i>
</button>
</div>
` : ''}
</div>
<div class="folder-contents hidden ml-4 mt-1 space-y-1"></div>
</div>
`;
}).join('') : ''}
</div> </div>
` : ''} </div>
<!-- Path --> <!-- Path -->
<div> <div>
@@ -269,12 +305,12 @@ function renderSkillDetailPanel(skill) {
<!-- Actions --> <!-- Actions -->
<div class="px-5 py-4 border-t border-border flex justify-between"> <div class="px-5 py-4 border-t border-border flex justify-between">
<button class="px-4 py-2 text-sm text-destructive hover:bg-destructive/10 rounded-lg transition-colors flex items-center gap-2" <button class="px-4 py-2 text-sm text-destructive hover:bg-destructive/10 rounded-lg transition-colors flex items-center gap-2"
onclick="deleteSkill('${escapeHtml(skill.name)}', '${skill.location}')"> onclick="deleteSkill('${escapeHtml(folderName)}', '${skill.location}')">
<i data-lucide="trash-2" class="w-4 h-4"></i> <i data-lucide="trash-2" class="w-4 h-4"></i>
${t('common.delete')} ${t('common.delete')}
</button> </button>
<button class="px-4 py-2 text-sm bg-primary text-primary-foreground rounded-lg hover:opacity-90 transition-opacity flex items-center gap-2" <button class="px-4 py-2 text-sm bg-primary text-primary-foreground rounded-lg hover:opacity-90 transition-opacity flex items-center gap-2"
onclick="editSkill('${escapeHtml(skill.name)}', '${skill.location}')"> onclick="editSkill('${escapeHtml(folderName)}', '${skill.location}')">
<i data-lucide="edit" class="w-4 h-4"></i> <i data-lucide="edit" class="w-4 h-4"></i>
${t('common.edit')} ${t('common.edit')}
</button> </button>
@@ -525,7 +561,7 @@ function openSkillCreateModal() {
</div> </div>
<!-- Footer --> <!-- Footer -->
<div class="flex items-center justify-end gap-3 px-6 py-4 border-t border-border"> <div id="skillModalFooter" class="flex items-center justify-end gap-3 px-6 py-4 border-t border-border">
<button class="px-4 py-2 text-sm text-muted-foreground hover:text-foreground transition-colors" <button class="px-4 py-2 text-sm text-muted-foreground hover:text-foreground transition-colors"
onclick="closeSkillCreateModal()"> onclick="closeSkillCreateModal()">
${t('common.cancel')} ${t('common.cancel')}
@@ -588,16 +624,76 @@ function selectSkillLocation(location) {
function switchSkillCreateMode(mode) { function switchSkillCreateMode(mode) {
skillCreateState.mode = mode; skillCreateState.mode = mode;
// Re-render modal
closeSkillCreateModal(); // Toggle visibility of mode sections
openSkillCreateModal(); const importSection = document.getElementById('skillImportMode');
const cliGenerateSection = document.getElementById('skillCliGenerateMode');
const footerContainer = document.getElementById('skillModalFooter');
if (importSection) importSection.style.display = mode === 'import' ? 'block' : 'none';
if (cliGenerateSection) cliGenerateSection.style.display = mode === 'cli-generate' ? 'block' : 'none';
// Update mode button styles
const modeButtons = document.querySelectorAll('#skillCreateModal .mode-btn');
modeButtons.forEach(btn => {
const btnText = btn.querySelector('.font-medium')?.textContent || '';
const isImport = btnText.includes(t('skills.importFolder'));
const isCliGenerate = btnText.includes(t('skills.cliGenerate'));
if ((isImport && mode === 'import') || (isCliGenerate && mode === 'cli-generate')) {
btn.classList.remove('border-border', 'hover:border-primary/50');
btn.classList.add('border-primary', 'bg-primary/10');
} else {
btn.classList.remove('border-primary', 'bg-primary/10');
btn.classList.add('border-border', 'hover:border-primary/50');
}
});
// Update footer buttons
if (footerContainer) {
if (mode === 'import') {
footerContainer.innerHTML = `
<button class="px-4 py-2 text-sm text-muted-foreground hover:text-foreground transition-colors"
onclick="closeSkillCreateModal()">
${t('common.cancel')}
</button>
<button class="px-4 py-2 text-sm bg-primary/10 text-primary rounded-lg hover:bg-primary/20 transition-colors"
onclick="validateSkillImport()">
${t('skills.validate')}
</button>
<button class="px-4 py-2 text-sm bg-primary text-primary-foreground rounded-lg hover:opacity-90 transition-opacity"
onclick="createSkill()">
${t('skills.import')}
</button>
`;
} else {
footerContainer.innerHTML = `
<button class="px-4 py-2 text-sm text-muted-foreground hover:text-foreground transition-colors"
onclick="closeSkillCreateModal()">
${t('common.cancel')}
</button>
<button class="px-4 py-2 text-sm bg-primary text-primary-foreground rounded-lg hover:opacity-90 transition-opacity flex items-center gap-2"
onclick="createSkill()">
<i data-lucide="sparkles" class="w-4 h-4"></i>
${t('skills.generate')}
</button>
`;
}
if (typeof lucide !== 'undefined') lucide.createIcons();
}
} }
function switchSkillGenerationType(type) { function switchSkillGenerationType(type) {
skillCreateState.generationType = type; skillCreateState.generationType = type;
// Re-render modal
closeSkillCreateModal(); // Toggle visibility of description area
openSkillCreateModal(); const descriptionArea = document.getElementById('skillDescriptionArea');
if (descriptionArea) {
descriptionArea.style.display = type === 'description' ? 'block' : 'none';
}
// Update generation type button styles (only the description button is active, template is disabled)
// No need to update button styles since template button is disabled
} }
function browseSkillFolder() { function browseSkillFolder() {
@@ -817,3 +913,271 @@ async function createSkill() {
} }
} }
} }
// ========== Skill File View/Edit Functions ==========
var skillFileEditorState = {
skillName: '',
fileName: '',
location: '',
content: '',
isEditing: false
};
async function viewSkillFile(skillName, fileName, location) {
try {
const response = await fetch(
'/api/skills/' + encodeURIComponent(skillName) + '/file?filename=' + encodeURIComponent(fileName) +
'&location=' + location + '&path=' + encodeURIComponent(projectPath)
);
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Failed to load file');
}
const data = await response.json();
skillFileEditorState = {
skillName,
fileName,
location,
content: data.content,
isEditing: false
};
renderSkillFileModal();
} catch (err) {
console.error('Failed to load skill file:', err);
if (window.showToast) {
showToast(err.message || t('skills.fileLoadError') || 'Failed to load file', 'error');
}
}
}
function editSkillFile(skillName, fileName, location) {
viewSkillFile(skillName, fileName, location).then(() => {
skillFileEditorState.isEditing = true;
renderSkillFileModal();
});
}
function renderSkillFileModal() {
// Remove existing modal if any
const existingModal = document.getElementById('skillFileModal');
if (existingModal) existingModal.remove();
const { skillName, fileName, content, isEditing, location } = skillFileEditorState;
const modalHtml = `
<div class="modal-overlay fixed inset-0 bg-black/50 z-[60] flex items-center justify-center" onclick="closeSkillFileModal(event)">
<div class="modal-dialog bg-card rounded-lg shadow-lg w-full max-w-4xl max-h-[90vh] mx-4 flex flex-col" onclick="event.stopPropagation()">
<!-- Header -->
<div class="flex items-center justify-between px-6 py-4 border-b border-border">
<div class="flex items-center gap-3">
<i data-lucide="file-text" class="w-5 h-5 text-primary"></i>
<div>
<h3 class="text-lg font-semibold text-foreground font-mono">${escapeHtml(fileName)}</h3>
<p class="text-xs text-muted-foreground">${escapeHtml(skillName)} / ${location}</p>
</div>
</div>
<div class="flex items-center gap-2">
${!isEditing ? `
<button class="px-3 py-1.5 text-sm bg-primary/10 text-primary rounded-lg hover:bg-primary/20 transition-colors flex items-center gap-1"
onclick="toggleSkillFileEdit()">
<i data-lucide="edit-2" class="w-4 h-4"></i>
${t('common.edit')}
</button>
` : ''}
<button class="w-8 h-8 flex items-center justify-center text-xl text-muted-foreground hover:text-foreground hover:bg-hover rounded"
onclick="closeSkillFileModal()">&times;</button>
</div>
</div>
<!-- Content -->
<div class="flex-1 overflow-hidden p-4">
${isEditing ? `
<textarea id="skillFileContent"
class="w-full h-full min-h-[400px] px-4 py-3 bg-background border border-border rounded-lg text-sm font-mono focus:outline-none focus:ring-2 focus:ring-primary resize-none"
spellcheck="false">${escapeHtml(content)}</textarea>
` : `
<div class="w-full h-full min-h-[400px] overflow-auto">
<pre class="px-4 py-3 bg-muted/30 rounded-lg text-sm font-mono whitespace-pre-wrap break-words">${escapeHtml(content)}</pre>
</div>
`}
</div>
<!-- Footer -->
${isEditing ? `
<div class="flex items-center justify-end gap-3 px-6 py-4 border-t border-border">
<button class="px-4 py-2 text-sm text-muted-foreground hover:text-foreground transition-colors"
onclick="cancelSkillFileEdit()">
${t('common.cancel')}
</button>
<button class="px-4 py-2 text-sm bg-primary text-primary-foreground rounded-lg hover:opacity-90 transition-opacity flex items-center gap-2"
onclick="saveSkillFile()">
<i data-lucide="save" class="w-4 h-4"></i>
${t('common.save')}
</button>
</div>
` : ''}
</div>
</div>
`;
const modalContainer = document.createElement('div');
modalContainer.id = 'skillFileModal';
modalContainer.innerHTML = modalHtml;
document.body.appendChild(modalContainer);
if (typeof lucide !== 'undefined') lucide.createIcons();
}
function closeSkillFileModal(event) {
if (event && event.target !== event.currentTarget) return;
const modal = document.getElementById('skillFileModal');
if (modal) modal.remove();
skillFileEditorState = { skillName: '', fileName: '', location: '', content: '', isEditing: false };
}
function toggleSkillFileEdit() {
skillFileEditorState.isEditing = true;
renderSkillFileModal();
}
function cancelSkillFileEdit() {
skillFileEditorState.isEditing = false;
renderSkillFileModal();
}
async function saveSkillFile() {
const contentTextarea = document.getElementById('skillFileContent');
if (!contentTextarea) return;
const newContent = contentTextarea.value;
const { skillName, fileName, location } = skillFileEditorState;
try {
const response = await fetch('/api/skills/' + encodeURIComponent(skillName) + '/file', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
fileName,
content: newContent,
location,
projectPath
})
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Failed to save file');
}
// Update state and close edit mode
skillFileEditorState.content = newContent;
skillFileEditorState.isEditing = false;
renderSkillFileModal();
// Refresh skill detail if SKILL.md was edited
if (fileName === 'SKILL.md') {
await loadSkillsData();
// Reload current skill detail
if (selectedSkill) {
await showSkillDetail(skillName, location);
}
}
if (window.showToast) {
showToast(t('skills.fileSaved') || 'File saved successfully', 'success');
}
} catch (err) {
console.error('Failed to save skill file:', err);
if (window.showToast) {
showToast(err.message || t('skills.fileSaveError') || 'Failed to save file', 'error');
}
}
}
// ========== Skill Folder Expansion Functions ==========
var expandedFolders = new Set();
async function toggleSkillFolder(skillName, subPath, location, element) {
const fileItem = element.closest('.skill-file-item');
if (!fileItem) return;
const contentsDiv = fileItem.querySelector('.folder-contents');
const chevron = element.querySelector('.folder-chevron');
const folderIcon = element.querySelector('.folder-icon');
const folderKey = `${skillName}:${subPath}:${location}`;
if (expandedFolders.has(folderKey)) {
// Collapse folder
expandedFolders.delete(folderKey);
contentsDiv.classList.add('hidden');
contentsDiv.innerHTML = '';
if (chevron) chevron.style.transform = '';
if (folderIcon) folderIcon.setAttribute('data-lucide', 'folder');
if (typeof lucide !== 'undefined') lucide.createIcons();
} else {
// Expand folder
try {
const response = await fetch(
'/api/skills/' + encodeURIComponent(skillName) + '/dir?subpath=' + encodeURIComponent(subPath) +
'&location=' + location + '&path=' + encodeURIComponent(projectPath)
);
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Failed to load folder');
}
const data = await response.json();
expandedFolders.add(folderKey);
if (chevron) chevron.style.transform = 'rotate(90deg)';
if (folderIcon) folderIcon.setAttribute('data-lucide', 'folder-open');
// Render folder contents
contentsDiv.innerHTML = data.files.map(file => {
const filePath = file.path;
const isDir = file.isDirectory;
return `
<div class="skill-file-item" data-path="${escapeHtml(filePath)}">
<div class="flex items-center justify-between p-2 bg-muted/30 rounded-lg cursor-pointer hover:bg-muted/50 transition-colors"
onclick="${isDir ? `toggleSkillFolder('${escapeHtml(skillName)}', '${escapeHtml(filePath)}', '${location}', this)` : `viewSkillFile('${escapeHtml(skillName)}', '${escapeHtml(filePath)}', '${location}')`}">
<div class="flex items-center gap-2">
<i data-lucide="${isDir ? 'folder' : 'file-text'}" class="w-4 h-4 text-muted-foreground ${isDir ? 'folder-icon' : ''}"></i>
<span class="text-sm font-mono text-foreground">${escapeHtml(file.name)}</span>
${isDir ? '<i data-lucide="chevron-right" class="w-3 h-3 text-muted-foreground folder-chevron transition-transform"></i>' : ''}
</div>
${!isDir ? `
<div class="flex items-center gap-1">
<button class="p-1 text-muted-foreground hover:text-foreground hover:bg-muted rounded transition-colors"
onclick="event.stopPropagation(); editSkillFile('${escapeHtml(skillName)}', '${escapeHtml(filePath)}', '${location}')"
title="${t('common.edit')}">
<i data-lucide="edit-2" class="w-3.5 h-3.5"></i>
</button>
</div>
` : ''}
</div>
<div class="folder-contents hidden ml-4 mt-1 space-y-1"></div>
</div>
`;
}).join('');
contentsDiv.classList.remove('hidden');
if (typeof lucide !== 'undefined') lucide.createIcons();
} catch (err) {
console.error('Failed to load folder contents:', err);
if (window.showToast) {
showToast(err.message || 'Failed to load folder', 'error');
}
}
}
}

View File

@@ -5,6 +5,7 @@
import { z } from 'zod'; import { z } from 'zod';
import type { ToolSchema, ToolResult } from '../types/tool.js'; import type { ToolSchema, ToolResult } from '../types/tool.js';
import type { HistoryIndexEntry } from './cli-history-store.js';
import { spawn, ChildProcess } from 'child_process'; import { spawn, ChildProcess } from 'child_process';
import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync, readdirSync, statSync } from 'fs'; import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync, readdirSync, statSync } from 'fs';
import { join, relative } from 'path'; import { join, relative } from 'path';
@@ -1982,6 +1983,7 @@ export async function getEnrichedConversation(baseDir: string, ccwId: string) {
/** /**
* Get history with native session info * Get history with native session info
* Supports recursive querying of child projects
*/ */
export async function getHistoryWithNativeInfo(baseDir: string, options?: { export async function getHistoryWithNativeInfo(baseDir: string, options?: {
limit?: number; limit?: number;
@@ -1990,9 +1992,75 @@ export async function getHistoryWithNativeInfo(baseDir: string, options?: {
status?: string | null; status?: string | null;
category?: ExecutionCategory | null; category?: ExecutionCategory | null;
search?: string | null; search?: string | null;
recursive?: boolean;
}) { }) {
const store = await getSqliteStore(baseDir); const { limit = 50, recursive = false, ...queryOptions } = options || {};
return store.getHistoryWithNativeInfo(options || {});
// Non-recursive mode: query single project
if (!recursive) {
const store = await getSqliteStore(baseDir);
return store.getHistoryWithNativeInfo({ limit, ...queryOptions });
}
// Recursive mode: aggregate data from parent and all child projects
const { scanChildProjectsAsync } = await import('../config/storage-paths.js');
const childProjects = await scanChildProjectsAsync(baseDir);
// Use the same type as store.getHistoryWithNativeInfo returns
type ExecutionWithNativeAndSource = HistoryIndexEntry & {
hasNativeSession: boolean;
nativeSessionId?: string;
nativeSessionPath?: string;
};
const allExecutions: ExecutionWithNativeAndSource[] = [];
let totalCount = 0;
// Query parent project
try {
const parentStore = await getSqliteStore(baseDir);
const parentResult = parentStore.getHistoryWithNativeInfo({ limit, ...queryOptions });
totalCount += parentResult.total;
for (const exec of parentResult.executions) {
allExecutions.push({ ...exec, sourceDir: baseDir });
}
} catch (error) {
if (process.env.DEBUG) {
console.error(`[CLI History] Failed to query parent project ${baseDir}:`, error);
}
}
// Query all child projects
for (const child of childProjects) {
try {
const childStore = await getSqliteStore(child.projectPath);
const childResult = childStore.getHistoryWithNativeInfo({ limit, ...queryOptions });
totalCount += childResult.total;
for (const exec of childResult.executions) {
allExecutions.push({ ...exec, sourceDir: child.projectPath });
}
} catch (error) {
if (process.env.DEBUG) {
console.error(`[CLI History] Failed to query child project ${child.projectPath}:`, error);
}
}
}
// Sort by updated_at descending and apply limit
allExecutions.sort((a, b) => {
const timeA = a.updated_at ? new Date(a.updated_at).getTime() : new Date(a.timestamp).getTime();
const timeB = b.updated_at ? new Date(b.updated_at).getTime() : new Date(b.timestamp).getTime();
return timeB - timeA;
});
const limitedExecutions = allExecutions.slice(0, limit);
return {
total: totalCount,
count: limitedExecutions.length,
executions: limitedExecutions
};
} }
// Export types // Export types

View File

@@ -14,6 +14,8 @@ except ImportError:
# Model profiles with metadata # Model profiles with metadata
# Note: 768d is max recommended dimension for optimal performance/quality balance
# 1024d models are available but not recommended due to higher resource usage
MODEL_PROFILES = { MODEL_PROFILES = {
"fast": { "fast": {
"model_name": "BAAI/bge-small-en-v1.5", "model_name": "BAAI/bge-small-en-v1.5",
@@ -21,6 +23,15 @@ MODEL_PROFILES = {
"size_mb": 80, "size_mb": 80,
"description": "Fast, lightweight, English-optimized", "description": "Fast, lightweight, English-optimized",
"use_case": "Quick prototyping, resource-constrained environments", "use_case": "Quick prototyping, resource-constrained environments",
"recommended": True,
},
"base": {
"model_name": "BAAI/bge-base-en-v1.5",
"dimensions": 768,
"size_mb": 220,
"description": "General purpose, good balance of speed and quality",
"use_case": "General text search, documentation",
"recommended": True,
}, },
"code": { "code": {
"model_name": "jinaai/jina-embeddings-v2-base-code", "model_name": "jinaai/jina-embeddings-v2-base-code",
@@ -28,20 +39,31 @@ MODEL_PROFILES = {
"size_mb": 150, "size_mb": 150,
"description": "Code-optimized, best for programming languages", "description": "Code-optimized, best for programming languages",
"use_case": "Open source projects, code semantic search", "use_case": "Open source projects, code semantic search",
"recommended": True,
},
"minilm": {
"model_name": "sentence-transformers/all-MiniLM-L6-v2",
"dimensions": 384,
"size_mb": 90,
"description": "Popular lightweight model, good quality",
"use_case": "General purpose, low resource environments",
"recommended": True,
}, },
"multilingual": { "multilingual": {
"model_name": "intfloat/multilingual-e5-large", "model_name": "intfloat/multilingual-e5-large",
"dimensions": 1024, "dimensions": 1024,
"size_mb": 1000, "size_mb": 1000,
"description": "Multilingual + code support", "description": "Multilingual + code support (high resource usage)",
"use_case": "Enterprise multilingual projects", "use_case": "Enterprise multilingual projects",
"recommended": False, # 1024d not recommended
}, },
"balanced": { "balanced": {
"model_name": "mixedbread-ai/mxbai-embed-large-v1", "model_name": "mixedbread-ai/mxbai-embed-large-v1",
"dimensions": 1024, "dimensions": 1024,
"size_mb": 600, "size_mb": 600,
"description": "High accuracy, general purpose", "description": "High accuracy, general purpose (high resource usage)",
"use_case": "High-quality semantic search, balanced performance", "use_case": "High-quality semantic search, balanced performance",
"recommended": False, # 1024d not recommended
}, },
} }

1361
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "claude-code-workflow", "name": "claude-code-workflow",
"version": "6.2.4", "version": "6.2.6",
"description": "JSON-driven multi-agent development framework with intelligent CLI orchestration (Gemini/Qwen/Codex), context-first architecture, and automated workflow execution", "description": "JSON-driven multi-agent development framework with intelligent CLI orchestration (Gemini/Qwen/Codex), context-first architecture, and automated workflow execution",
"type": "module", "type": "module",
"main": "ccw/src/index.js", "main": "ccw/src/index.js",