mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-05 01:50:27 +08:00
bump version to 6.2.6 in package.json
This commit is contained in:
8
ccw/package-lock.json
generated
8
ccw/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "ccw",
|
||||
"version": "6.1.4",
|
||||
"name": "claude-code-workflow",
|
||||
"version": "6.2.6",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "ccw",
|
||||
"version": "6.1.4",
|
||||
"name": "claude-code-workflow",
|
||||
"version": "6.2.6",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.0.4",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "claude-code-workflow",
|
||||
"version": "6.2.4",
|
||||
"version": "6.2.6",
|
||||
"description": "Claude Code Workflow CLI - Dashboard viewer for workflow sessions and reviews",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
|
||||
@@ -284,8 +284,12 @@ function normalizeTask(task: unknown): NormalizedTask | null {
|
||||
const implementation = taskObj.implementation as unknown[] | 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 {
|
||||
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',
|
||||
status: (status as string).toLowerCase(),
|
||||
// Preserve original fields for flexible rendering
|
||||
|
||||
@@ -284,8 +284,12 @@ function normalizeTask(task: unknown): NormalizedTask | null {
|
||||
const implementation = taskObj.implementation as unknown[] | 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 {
|
||||
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',
|
||||
status: (status as string).toLowerCase(),
|
||||
// Preserve original fields for flexible rendering
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* Handles all CLAUDE.md memory rules management endpoints
|
||||
*/
|
||||
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 { homedir } from 'os';
|
||||
|
||||
@@ -453,8 +453,7 @@ function deleteClaudeFile(filePath: string): { success: boolean; error?: string
|
||||
writeFileSync(backupPath, content, 'utf8');
|
||||
|
||||
// Delete original file
|
||||
const fs = require('fs');
|
||||
fs.unlinkSync(filePath);
|
||||
unlinkSync(filePath);
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
@@ -500,9 +499,8 @@ function createNewClaudeFile(level: 'user' | 'project' | 'module', template: str
|
||||
|
||||
// Ensure directory exists
|
||||
const dir = filePath.substring(0, filePath.lastIndexOf('/') || filePath.lastIndexOf('\\'));
|
||||
const fs = require('fs');
|
||||
if (!existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
|
||||
// Write file
|
||||
|
||||
@@ -362,8 +362,9 @@ export async function handleCliRoutes(ctx: RouteContext): Promise<boolean> {
|
||||
const status = url.searchParams.get('status') || null;
|
||||
const category = url.searchParams.get('category') as 'user' | 'internal' | 'insight' | 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 => {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(history));
|
||||
|
||||
@@ -291,13 +291,14 @@ FOCUS AREAS: ${extractFocus || 'naming conventions, error handling, code structu
|
||||
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({
|
||||
tool: 'gemini',
|
||||
tool: 'claude',
|
||||
prompt,
|
||||
mode,
|
||||
cd: workingDir,
|
||||
timeout: 600000 // 10 minutes
|
||||
timeout: 600000, // 10 minutes
|
||||
category: 'internal'
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
|
||||
@@ -123,6 +123,7 @@ function getSkillsConfig(projectPath) {
|
||||
|
||||
result.projectSkills.push({
|
||||
name: parsed.name || skill.name,
|
||||
folderName: skill.name, // Actual folder name for API queries
|
||||
description: parsed.description,
|
||||
version: parsed.version,
|
||||
allowedTools: parsed.allowedTools,
|
||||
@@ -152,6 +153,7 @@ function getSkillsConfig(projectPath) {
|
||||
|
||||
result.userSkills.push({
|
||||
name: parsed.name || skill.name,
|
||||
folderName: skill.name, // Actual folder name for API queries
|
||||
description: parsed.description,
|
||||
version: parsed.version,
|
||||
allowedTools: parsed.allowedTools,
|
||||
@@ -197,6 +199,7 @@ function getSkillDetail(skillName, location, projectPath) {
|
||||
return {
|
||||
skill: {
|
||||
name: parsed.name || skillName,
|
||||
folderName: skillName, // Actual folder name for API queries
|
||||
description: parsed.description,
|
||||
version: parsed.version,
|
||||
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 {string} params.generationType - 'description' or 'template'
|
||||
* @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
|
||||
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({
|
||||
tool: 'gemini',
|
||||
tool: 'claude',
|
||||
prompt,
|
||||
mode: 'write',
|
||||
cd: baseDir,
|
||||
@@ -515,8 +518,143 @@ export async function handleSkillsRoutes(ctx: RouteContext): Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
|
||||
// API: Get single skill detail
|
||||
if (pathname.startsWith('/api/skills/') && req.method === 'GET' && !pathname.endsWith('/skills/')) {
|
||||
// API: List skill directory contents
|
||||
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 location = url.searchParams.get('location') || 'project';
|
||||
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);
|
||||
} else if (mode === 'cli-generate') {
|
||||
// CLI generate mode: use Gemini to generate skill
|
||||
// CLI generate mode: use Claude to generate skill
|
||||
if (!skillName) {
|
||||
return { error: 'Skill name is required for CLI generation mode' };
|
||||
}
|
||||
|
||||
@@ -411,9 +411,12 @@ const i18n = {
|
||||
'index.fullDesc': 'FTS + Semantic search (recommended)',
|
||||
'index.selectModel': 'Select embedding model',
|
||||
'index.modelCode': 'Code (768d)',
|
||||
'index.modelBase': 'Base (768d)',
|
||||
'index.modelFast': 'Fast (384d)',
|
||||
'index.modelMultilingual': 'Multilingual (1024d)',
|
||||
'index.modelBalanced': 'Balanced (1024d)',
|
||||
'index.modelMinilm': 'MiniLM (384d)',
|
||||
'index.modelMultilingual': 'Multilingual (1024d) ⚠️',
|
||||
'index.modelBalanced': 'Balanced (1024d) ⚠️',
|
||||
'index.dimensionWarning': '1024d models require more resources',
|
||||
|
||||
// Semantic Search Configuration
|
||||
'semantic.settings': 'Semantic Search Settings',
|
||||
@@ -1824,9 +1827,12 @@ const i18n = {
|
||||
'index.fullDesc': 'FTS + 语义搜索(推荐)',
|
||||
'index.selectModel': '选择嵌入模型',
|
||||
'index.modelCode': '代码优化 (768维)',
|
||||
'index.modelBase': '通用基础 (768维)',
|
||||
'index.modelFast': '快速轻量 (384维)',
|
||||
'index.modelMultilingual': '多语言 (1024维)',
|
||||
'index.modelBalanced': '高精度 (1024维)',
|
||||
'index.modelMinilm': 'MiniLM (384维)',
|
||||
'index.modelMultilingual': '多语言 (1024维) ⚠️',
|
||||
'index.modelBalanced': '高精度 (1024维) ⚠️',
|
||||
'index.dimensionWarning': '1024维模型需要更多资源',
|
||||
|
||||
// Semantic Search 配置
|
||||
'semantic.settings': '语义搜索设置',
|
||||
|
||||
@@ -102,6 +102,7 @@ async function loadClaudeFiles() {
|
||||
updateClaudeBadge(); // Update navigation badge
|
||||
} catch (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');
|
||||
}
|
||||
}
|
||||
@@ -113,6 +114,7 @@ async function refreshClaudeFiles() {
|
||||
renderFileViewer();
|
||||
renderFileMetadata();
|
||||
if (window.lucide) lucide.createIcons();
|
||||
showRefreshToast(t('claudeManager.refreshed') || 'Files refreshed', 'success');
|
||||
addGlobalNotification('success', t('claudeManager.refreshed'), null, 'CLAUDE.md');
|
||||
// Load freshness data in background
|
||||
loadFreshnessDataAsync();
|
||||
@@ -155,6 +157,7 @@ async function markFileAsUpdated() {
|
||||
|
||||
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');
|
||||
|
||||
// Reload freshness data
|
||||
@@ -163,6 +166,7 @@ async function markFileAsUpdated() {
|
||||
renderFileMetadata();
|
||||
} catch (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');
|
||||
}
|
||||
}
|
||||
@@ -481,10 +485,12 @@ async function saveClaudeFile() {
|
||||
selectedFile.stats = calculateFileStats(newContent);
|
||||
isDirty = false;
|
||||
|
||||
showRefreshToast(t('claudeManager.saved') || 'File saved', 'success');
|
||||
addGlobalNotification('success', t('claudeManager.saved'), null, 'CLAUDE.md');
|
||||
renderFileMetadata();
|
||||
} catch (error) {
|
||||
console.error('Error saving file:', error);
|
||||
showRefreshToast(t('claudeManager.saveError') || 'Save failed', 'error');
|
||||
addGlobalNotification('error', t('claudeManager.saveError'), null, 'CLAUDE.md');
|
||||
}
|
||||
}
|
||||
@@ -733,12 +739,13 @@ async function loadFileContent(filePath) {
|
||||
}
|
||||
|
||||
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') {
|
||||
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;
|
||||
|
||||
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');
|
||||
return;
|
||||
}
|
||||
@@ -841,12 +849,14 @@ async function createNewFile() {
|
||||
|
||||
var result = await res.json();
|
||||
closeCreateDialog();
|
||||
showRefreshToast(t('claude.fileCreated') || 'File created successfully', 'success');
|
||||
addGlobalNotification('success', t('claude.fileCreated') || 'File created successfully', null, 'CLAUDE.md');
|
||||
|
||||
// Refresh file tree
|
||||
await refreshClaudeFiles();
|
||||
} catch (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');
|
||||
}
|
||||
}
|
||||
@@ -870,6 +880,7 @@ async function confirmDeleteFile() {
|
||||
|
||||
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');
|
||||
selectedFile = null;
|
||||
|
||||
@@ -877,6 +888,7 @@ async function confirmDeleteFile() {
|
||||
await refreshClaudeFiles();
|
||||
} catch (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');
|
||||
}
|
||||
}
|
||||
@@ -886,9 +898,11 @@ function copyFileContent() {
|
||||
if (!selectedFile || !selectedFile.content) return;
|
||||
|
||||
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');
|
||||
}).catch(function(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');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -404,10 +404,12 @@ function renderToolsSection() {
|
||||
(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>' +
|
||||
'<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="multilingual">' + (t('index.modelMultilingual') || 'Multilingual (1024d)') + '</option>' +
|
||||
'<option value="balanced">' + (t('index.modelBalanced') || 'Balanced (1024d)') + '</option>' +
|
||||
'<option value="minilm">' + (t('index.modelMinilm') || 'MiniLM (384d)') + '</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>' +
|
||||
'<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>' +
|
||||
|
||||
@@ -638,9 +638,26 @@ function addRulePath() {
|
||||
|
||||
function removeRulePath(index) {
|
||||
ruleCreateState.paths.splice(index, 1);
|
||||
// Re-render paths list
|
||||
closeRuleCreateModal();
|
||||
openRuleCreateModal();
|
||||
|
||||
// Re-render paths list without closing modal
|
||||
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) {
|
||||
@@ -674,9 +691,21 @@ function switchRuleCreateMode(mode) {
|
||||
if (contentSection) contentSection.style.display = 'block';
|
||||
}
|
||||
|
||||
// Re-render modal to update button states
|
||||
closeRuleCreateModal();
|
||||
openRuleCreateModal();
|
||||
// Update mode button styles without re-rendering
|
||||
const modeButtons = document.querySelectorAll('#ruleCreateModal .mode-btn');
|
||||
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) {
|
||||
|
||||
@@ -153,10 +153,11 @@ function renderSkillCard(skill, location) {
|
||||
const locationIcon = location === 'project' ? 'folder' : 'user';
|
||||
const locationClass = location === 'project' ? 'text-primary' : 'text-indigo';
|
||||
const locationBg = location === 'project' ? 'bg-primary/10' : 'bg-indigo/10';
|
||||
const folderName = skill.folderName || skill.name;
|
||||
|
||||
return `
|
||||
<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-center gap-3">
|
||||
<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) {
|
||||
const hasAllowedTools = skill.allowedTools && skill.allowedTools.length > 0;
|
||||
const hasSupportingFiles = skill.supportingFiles && skill.supportingFiles.length > 0;
|
||||
const folderName = skill.folderName || skill.name;
|
||||
|
||||
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">
|
||||
@@ -243,20 +245,54 @@ function renderSkillDetailPanel(skill) {
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<!-- Supporting Files -->
|
||||
${hasSupportingFiles ? `
|
||||
<div>
|
||||
<h4 class="text-sm font-semibold text-foreground mb-2">${t('skills.supportingFiles')}</h4>
|
||||
<div class="space-y-2">
|
||||
${skill.supportingFiles.map(file => `
|
||||
<div class="flex items-center gap-2 p-2 bg-muted/50 rounded-lg">
|
||||
<i data-lucide="file-text" class="w-4 h-4 text-muted-foreground"></i>
|
||||
<span class="text-sm font-mono text-foreground">${escapeHtml(file)}</span>
|
||||
</div>
|
||||
`).join('')}
|
||||
<!-- Skill Files (SKILL.md + Supporting Files) -->
|
||||
<div>
|
||||
<h4 class="text-sm font-semibold text-foreground mb-2">${t('skills.files') || 'Files'}</h4>
|
||||
<div class="space-y-2">
|
||||
<!-- SKILL.md (main 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"
|
||||
onclick="viewSkillFile('${escapeHtml(folderName)}', 'SKILL.md', '${skill.location}')">
|
||||
<div class="flex items-center gap-2">
|
||||
<i data-lucide="file-text" class="w-4 h-4 text-primary"></i>
|
||||
<span class="text-sm font-mono text-foreground font-medium">SKILL.md</span>
|
||||
</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>
|
||||
${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>
|
||||
|
||||
<!-- Path -->
|
||||
<div>
|
||||
@@ -269,12 +305,12 @@ function renderSkillDetailPanel(skill) {
|
||||
<!-- Actions -->
|
||||
<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"
|
||||
onclick="deleteSkill('${escapeHtml(skill.name)}', '${skill.location}')">
|
||||
onclick="deleteSkill('${escapeHtml(folderName)}', '${skill.location}')">
|
||||
<i data-lucide="trash-2" class="w-4 h-4"></i>
|
||||
${t('common.delete')}
|
||||
</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="editSkill('${escapeHtml(skill.name)}', '${skill.location}')">
|
||||
onclick="editSkill('${escapeHtml(folderName)}', '${skill.location}')">
|
||||
<i data-lucide="edit" class="w-4 h-4"></i>
|
||||
${t('common.edit')}
|
||||
</button>
|
||||
@@ -525,7 +561,7 @@ function openSkillCreateModal() {
|
||||
</div>
|
||||
|
||||
<!-- 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"
|
||||
onclick="closeSkillCreateModal()">
|
||||
${t('common.cancel')}
|
||||
@@ -588,16 +624,76 @@ function selectSkillLocation(location) {
|
||||
|
||||
function switchSkillCreateMode(mode) {
|
||||
skillCreateState.mode = mode;
|
||||
// Re-render modal
|
||||
closeSkillCreateModal();
|
||||
openSkillCreateModal();
|
||||
|
||||
// Toggle visibility of mode sections
|
||||
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) {
|
||||
skillCreateState.generationType = type;
|
||||
// Re-render modal
|
||||
closeSkillCreateModal();
|
||||
openSkillCreateModal();
|
||||
|
||||
// Toggle visibility of description area
|
||||
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() {
|
||||
@@ -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()">×</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');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
import { z } from 'zod';
|
||||
import type { ToolSchema, ToolResult } from '../types/tool.js';
|
||||
import type { HistoryIndexEntry } from './cli-history-store.js';
|
||||
import { spawn, ChildProcess } from 'child_process';
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync, readdirSync, statSync } from 'fs';
|
||||
import { join, relative } from 'path';
|
||||
@@ -1982,6 +1983,7 @@ export async function getEnrichedConversation(baseDir: string, ccwId: string) {
|
||||
|
||||
/**
|
||||
* Get history with native session info
|
||||
* Supports recursive querying of child projects
|
||||
*/
|
||||
export async function getHistoryWithNativeInfo(baseDir: string, options?: {
|
||||
limit?: number;
|
||||
@@ -1990,9 +1992,75 @@ export async function getHistoryWithNativeInfo(baseDir: string, options?: {
|
||||
status?: string | null;
|
||||
category?: ExecutionCategory | null;
|
||||
search?: string | null;
|
||||
recursive?: boolean;
|
||||
}) {
|
||||
const store = await getSqliteStore(baseDir);
|
||||
return store.getHistoryWithNativeInfo(options || {});
|
||||
const { limit = 50, recursive = false, ...queryOptions } = 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
|
||||
|
||||
@@ -14,6 +14,8 @@ except ImportError:
|
||||
|
||||
|
||||
# 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 = {
|
||||
"fast": {
|
||||
"model_name": "BAAI/bge-small-en-v1.5",
|
||||
@@ -21,6 +23,15 @@ MODEL_PROFILES = {
|
||||
"size_mb": 80,
|
||||
"description": "Fast, lightweight, English-optimized",
|
||||
"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": {
|
||||
"model_name": "jinaai/jina-embeddings-v2-base-code",
|
||||
@@ -28,20 +39,31 @@ MODEL_PROFILES = {
|
||||
"size_mb": 150,
|
||||
"description": "Code-optimized, best for programming languages",
|
||||
"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": {
|
||||
"model_name": "intfloat/multilingual-e5-large",
|
||||
"dimensions": 1024,
|
||||
"size_mb": 1000,
|
||||
"description": "Multilingual + code support",
|
||||
"description": "Multilingual + code support (high resource usage)",
|
||||
"use_case": "Enterprise multilingual projects",
|
||||
"recommended": False, # 1024d not recommended
|
||||
},
|
||||
"balanced": {
|
||||
"model_name": "mixedbread-ai/mxbai-embed-large-v1",
|
||||
"dimensions": 1024,
|
||||
"size_mb": 600,
|
||||
"description": "High accuracy, general purpose",
|
||||
"description": "High accuracy, general purpose (high resource usage)",
|
||||
"use_case": "High-quality semantic search, balanced performance",
|
||||
"recommended": False, # 1024d not recommended
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
1361
package-lock.json
generated
1361
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"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",
|
||||
"type": "module",
|
||||
"main": "ccw/src/index.js",
|
||||
|
||||
Reference in New Issue
Block a user