mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-05 01:50:27 +08:00
feat: Enhance CLI tools and history management
- Added CLI Manager and CLI History views to the navigation. - Implemented rendering for CLI tools with detailed status and actions. - Introduced a new CLI History view to display execution history with search and filter capabilities. - Added hooks for managing and displaying available SKILLs in the Hook Manager. - Created modals for Hook Wizards and Template View for better user interaction. - Implemented semantic search dependency checks and installation functions in CodexLens. - Updated dashboard layout to accommodate new features and improve user experience.
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -31,34 +31,46 @@ ccw tool exec classify_folders '{"path": "./src"}'
|
||||
**When to Use**: Edit tool fails 1+ times on same file
|
||||
|
||||
```bash
|
||||
# CLI shorthand
|
||||
# Basic edit
|
||||
ccw tool exec edit_file --path "file.py" --old "old code" --new "new code"
|
||||
|
||||
# JSON (recommended)
|
||||
ccw tool exec edit_file '{"path": "file.py", "oldText": "old", "newText": "new"}'
|
||||
# Preview without modifying (dry run)
|
||||
ccw tool exec edit_file --path "file.py" --old "old" --new "new" --dry-run
|
||||
|
||||
# dryRun - preview without modifying
|
||||
ccw tool exec edit_file '{"path": "file.py", "oldText": "old", "newText": "new", "dryRun": true}'
|
||||
# Replace all occurrences
|
||||
ccw tool exec edit_file --path "file.py" --old "old" --new "new" --replace-all
|
||||
|
||||
# Multiple edits
|
||||
ccw tool exec edit_file '{"path": "file.py", "edits": [{"oldText": "a", "newText": "b"}, {"oldText": "c", "newText": "d"}]}'
|
||||
# Line mode - insert after line
|
||||
ccw tool exec edit_file --path "file.py" --mode line --operation insert_after --line 10 --text "new line"
|
||||
|
||||
# Line mode
|
||||
ccw tool exec edit_file '{"path": "file.py", "mode": "line", "operation": "insert_after", "line": 10, "text": "new"}'
|
||||
# Line mode - insert before line
|
||||
ccw tool exec edit_file --path "file.py" --mode line --operation insert_before --line 5 --text "new line"
|
||||
|
||||
# Line mode - replace line
|
||||
ccw tool exec edit_file --path "file.py" --mode line --operation replace --line 3 --text "replacement"
|
||||
|
||||
# Line mode - delete line
|
||||
ccw tool exec edit_file --path "file.py" --mode line --operation delete --line 3
|
||||
```
|
||||
|
||||
**Parameters**: `path`*, `oldText`, `newText`, `edits[]`, `dryRun`, `replaceAll`, `mode` (update|line)
|
||||
**Parameters**: `--path`*, `--old`, `--new`, `--dry-run`, `--replace-all`, `--mode` (update|line), `--operation`, `--line`, `--text`
|
||||
|
||||
### write_file Tool
|
||||
|
||||
**When to Use**: Create new files or overwrite existing content
|
||||
|
||||
```bash
|
||||
ccw tool exec write_file '{"path": "file.txt", "content": "Hello"}'
|
||||
ccw tool exec write_file '{"path": "file.txt", "content": "new", "backup": true}'
|
||||
# Basic write
|
||||
ccw tool exec write_file --path "file.txt" --content "Hello"
|
||||
|
||||
# With backup
|
||||
ccw tool exec write_file --path "file.txt" --content "new content" --backup
|
||||
|
||||
# Create directories if needed
|
||||
ccw tool exec write_file --path "new/path/file.txt" --content "content" --create-directories
|
||||
```
|
||||
|
||||
**Parameters**: `path`*, `content`*, `createDirectories`, `backup`, `encoding`
|
||||
**Parameters**: `--path`*, `--content`*, `--create-directories`, `--backup`, `--encoding`
|
||||
|
||||
### Fallback Strategy
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import { aggregateData } from './data-aggregator.js';
|
||||
import { resolvePath, getRecentPaths, trackRecentPath, removeRecentPath, normalizePathForDisplay, getWorkflowDir } from '../utils/path-resolver.js';
|
||||
import { getCliToolsStatus, getExecutionHistory, getExecutionDetail, deleteExecution, executeCliTool } from '../tools/cli-executor.js';
|
||||
import { getAllManifests } from './manifest.js';
|
||||
import { checkVenvStatus, bootstrapVenv, executeCodexLens } from '../tools/codex-lens.js';
|
||||
import { checkVenvStatus, bootstrapVenv, executeCodexLens, checkSemanticStatus, installSemantic } from '../tools/codex-lens.js';
|
||||
|
||||
// Claude config file paths
|
||||
const CLAUDE_CONFIG_PATH = join(homedir(), '.claude.json');
|
||||
@@ -113,6 +113,7 @@ const MODULE_FILES = [
|
||||
'views/mcp-manager.js',
|
||||
'views/hook-manager.js',
|
||||
'views/cli-manager.js',
|
||||
'views/history.js',
|
||||
'views/explorer.js',
|
||||
'main.js'
|
||||
];
|
||||
@@ -430,6 +431,15 @@ export async function startServer(options = {}) {
|
||||
return;
|
||||
}
|
||||
|
||||
// API: Discover SKILL packages in project
|
||||
if (pathname === '/api/skills') {
|
||||
const projectPathParam = url.searchParams.get('path') || initialPath;
|
||||
const skills = await discoverSkillPackages(projectPathParam);
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(skills));
|
||||
return;
|
||||
}
|
||||
|
||||
// API: Get file content for preview (Explorer view)
|
||||
if (pathname === '/api/file-content') {
|
||||
const filePath = url.searchParams.get('path');
|
||||
@@ -503,6 +513,32 @@ export async function startServer(options = {}) {
|
||||
return;
|
||||
}
|
||||
|
||||
// API: CodexLens Semantic Search Status
|
||||
if (pathname === '/api/codexlens/semantic/status') {
|
||||
const status = await checkSemanticStatus();
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(status));
|
||||
return;
|
||||
}
|
||||
|
||||
// API: CodexLens Semantic Search Install
|
||||
if (pathname === '/api/codexlens/semantic/install' && req.method === 'POST') {
|
||||
handlePostRequest(req, res, async () => {
|
||||
try {
|
||||
const result = await installSemantic();
|
||||
if (result.success) {
|
||||
const status = await checkSemanticStatus();
|
||||
return { success: true, message: 'Semantic search installed successfully', ...status };
|
||||
} else {
|
||||
return { success: false, error: result.error, status: 500 };
|
||||
}
|
||||
} catch (err) {
|
||||
return { success: false, error: err.message, status: 500 };
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// API: CCW Installation Status
|
||||
if (pathname === '/api/ccw/installations') {
|
||||
const manifests = getAllManifests();
|
||||
@@ -1757,6 +1793,64 @@ function writeSettingsFile(filePath, settings) {
|
||||
writeFileSync(filePath, JSON.stringify(settings, null, 2), 'utf8');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Discover SKILL packages in project
|
||||
* @param {string} projectPath - Project root path
|
||||
* @returns {Object} - List of discovered SKILL packages
|
||||
*/
|
||||
async function discoverSkillPackages(projectPath) {
|
||||
const skills = [];
|
||||
const skillsDir = join(projectPath, '.claude', 'skills');
|
||||
|
||||
try {
|
||||
// Check if skills directory exists
|
||||
if (!existsSync(skillsDir)) {
|
||||
return { skills: [], skillsDir: null };
|
||||
}
|
||||
|
||||
// Read all subdirectories in skills folder
|
||||
const entries = readdirSync(skillsDir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
if (entry.isDirectory()) {
|
||||
const skillPath = join(skillsDir, entry.name);
|
||||
const skillMdPath = join(skillPath, 'SKILL.md');
|
||||
|
||||
// Check if SKILL.md exists
|
||||
if (existsSync(skillMdPath)) {
|
||||
const skillContent = readFileSync(skillMdPath, 'utf8');
|
||||
|
||||
// Parse YAML frontmatter
|
||||
let metadata = { name: entry.name, description: '' };
|
||||
const frontmatterRegex = /^---\n([\s\S]*?)\n---/;
|
||||
const frontmatterMatch = skillContent.match(frontmatterRegex);
|
||||
if (frontmatterMatch) {
|
||||
const yaml = frontmatterMatch[1];
|
||||
const nameMatch = yaml.match(/name:\s*(.+)/);
|
||||
const descMatch = yaml.match(/description:\s*(.+)/);
|
||||
if (nameMatch) metadata.name = nameMatch[1].trim();
|
||||
if (descMatch) metadata.description = descMatch[1].trim();
|
||||
}
|
||||
|
||||
skills.push({
|
||||
id: entry.name,
|
||||
name: metadata.name,
|
||||
description: metadata.description,
|
||||
path: skillPath,
|
||||
skillMdPath: skillMdPath
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { skills, skillsDir };
|
||||
} catch (err) {
|
||||
console.error('Error discovering SKILL packages:', err);
|
||||
return { skills: [], skillsDir: null, error: err.message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get hooks configuration from both global and project settings
|
||||
* @param {string} projectPath
|
||||
|
||||
@@ -3,6 +3,559 @@
|
||||
* Unified font: system-ui for UI, monospace for code
|
||||
* ======================================== */
|
||||
|
||||
/* ========================================
|
||||
* Status Manager - Two Column Layout
|
||||
* ======================================== */
|
||||
.status-manager {
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
}
|
||||
|
||||
.status-two-column {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1.5rem;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.status-two-column {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* Section Container */
|
||||
.status-section {
|
||||
background: hsl(var(--card));
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 0.75rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Section Header */
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.875rem 1rem;
|
||||
border-bottom: 1px solid hsl(var(--border));
|
||||
background: hsl(var(--muted) / 0.3);
|
||||
}
|
||||
|
||||
.section-header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.section-header h3 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: hsl(var(--foreground));
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.section-header h3 i {
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
.section-count {
|
||||
font-size: 0.75rem;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
.section-header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
/* Tools List */
|
||||
.tools-list {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.tool-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.75rem;
|
||||
border-radius: 0.5rem;
|
||||
margin-bottom: 0.375rem;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.tool-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.tool-item:hover {
|
||||
background: hsl(var(--hover));
|
||||
}
|
||||
|
||||
.tool-item.available {
|
||||
border-left: 3px solid hsl(var(--success));
|
||||
}
|
||||
|
||||
.tool-item.unavailable {
|
||||
border-left: 3px solid hsl(var(--muted-foreground) / 0.3);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.tool-item-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.tool-status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tool-status-dot.status-available {
|
||||
background: hsl(var(--success));
|
||||
box-shadow: 0 0 6px hsl(var(--success) / 0.5);
|
||||
}
|
||||
|
||||
.tool-status-dot.status-unavailable {
|
||||
background: hsl(var(--muted-foreground) / 0.4);
|
||||
}
|
||||
|
||||
.tool-item-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.125rem;
|
||||
}
|
||||
|
||||
.tool-item-name {
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
color: hsl(var(--foreground));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.tool-item-desc {
|
||||
font-size: 0.6875rem;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
.tool-default-badge {
|
||||
font-size: 0.5625rem;
|
||||
font-weight: 600;
|
||||
padding: 0.125rem 0.375rem;
|
||||
background: hsl(var(--primary));
|
||||
color: hsl(var(--primary-foreground));
|
||||
border-radius: 9999px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
|
||||
.tool-type-badge {
|
||||
font-size: 0.5625rem;
|
||||
font-weight: 500;
|
||||
padding: 0.125rem 0.375rem;
|
||||
background: hsl(var(--muted));
|
||||
color: hsl(var(--muted-foreground));
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.tool-type-badge.ai {
|
||||
background: hsl(var(--primary) / 0.15);
|
||||
color: hsl(var(--primary));
|
||||
}
|
||||
|
||||
.tool-item-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.tool-status-text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.tool-status-text.success {
|
||||
color: hsl(var(--success));
|
||||
}
|
||||
|
||||
.tool-status-text.muted {
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
/* CCW List */
|
||||
.ccw-list {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.ccw-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
padding: 0.75rem;
|
||||
border-radius: 0.5rem;
|
||||
margin-bottom: 0.375rem;
|
||||
border: 1px solid hsl(var(--border));
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.ccw-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.ccw-item:hover {
|
||||
background: hsl(var(--hover));
|
||||
border-color: hsl(var(--primary) / 0.3);
|
||||
}
|
||||
|
||||
.ccw-item-left {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.75rem;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.ccw-item-mode {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 2.25rem;
|
||||
height: 2.25rem;
|
||||
border-radius: 0.5rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.ccw-item-mode.global {
|
||||
background: hsl(var(--primary) / 0.1);
|
||||
color: hsl(var(--primary));
|
||||
}
|
||||
|
||||
.ccw-item-mode.path {
|
||||
background: hsl(var(--warning) / 0.1);
|
||||
color: hsl(var(--warning));
|
||||
}
|
||||
|
||||
.ccw-item-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.ccw-item-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.ccw-item-name {
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
.ccw-item-path {
|
||||
font-size: 0.6875rem;
|
||||
color: hsl(var(--muted-foreground));
|
||||
font-family: 'SF Mono', 'Consolas', 'Liberation Mono', monospace;
|
||||
background: hsl(var(--muted) / 0.5);
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
margin-bottom: 0.375rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ccw-item-meta {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
font-size: 0.625rem;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
.ccw-item-meta span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.ccw-item-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s ease;
|
||||
}
|
||||
|
||||
.ccw-item:hover .ccw-item-actions {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
* History View Styles
|
||||
* ======================================== */
|
||||
.history-view {
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
}
|
||||
|
||||
.history-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.75rem 0;
|
||||
margin-bottom: 1rem;
|
||||
border-bottom: 1px solid hsl(var(--border));
|
||||
}
|
||||
|
||||
.history-header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.history-count {
|
||||
font-size: 0.8125rem;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
.history-header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.history-search-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.history-search-wrapper i {
|
||||
position: absolute;
|
||||
left: 0.625rem;
|
||||
color: hsl(var(--muted-foreground));
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.history-search-input {
|
||||
padding: 0.5rem 0.75rem 0.5rem 2rem;
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 0.5rem;
|
||||
background: hsl(var(--background));
|
||||
color: hsl(var(--foreground));
|
||||
font-size: 0.8125rem;
|
||||
width: 220px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.history-search-input:focus {
|
||||
outline: none;
|
||||
border-color: hsl(var(--primary));
|
||||
box-shadow: 0 0 0 2px hsl(var(--primary) / 0.15);
|
||||
width: 260px;
|
||||
}
|
||||
|
||||
.history-search-input::placeholder {
|
||||
color: hsl(var(--muted-foreground) / 0.7);
|
||||
}
|
||||
|
||||
.history-filter-select {
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 0.5rem;
|
||||
background: hsl(var(--background));
|
||||
color: hsl(var(--foreground));
|
||||
font-size: 0.8125rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.history-filter-select:hover {
|
||||
border-color: hsl(var(--primary) / 0.5);
|
||||
}
|
||||
|
||||
.history-filter-select:focus {
|
||||
outline: none;
|
||||
border-color: hsl(var(--primary));
|
||||
}
|
||||
|
||||
/* History List */
|
||||
.history-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.history-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
padding: 1rem;
|
||||
background: hsl(var(--card));
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 0.5rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.history-item:hover {
|
||||
background: hsl(var(--hover));
|
||||
border-color: hsl(var(--primary) / 0.3);
|
||||
box-shadow: 0 2px 8px hsl(var(--foreground) / 0.05);
|
||||
}
|
||||
|
||||
.history-item:hover .history-item-actions {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.history-item-main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.history-item-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.history-tool-tag {
|
||||
font-size: 0.625rem;
|
||||
font-weight: 600;
|
||||
padding: 0.1875rem 0.5rem;
|
||||
border-radius: 9999px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.history-tool-tag.tool-gemini {
|
||||
background: hsl(210 80% 55% / 0.12);
|
||||
color: hsl(210 80% 45%);
|
||||
}
|
||||
|
||||
.history-tool-tag.tool-qwen {
|
||||
background: hsl(280 70% 55% / 0.12);
|
||||
color: hsl(280 70% 45%);
|
||||
}
|
||||
|
||||
.history-tool-tag.tool-codex {
|
||||
background: hsl(142 71% 45% / 0.12);
|
||||
color: hsl(142 71% 35%);
|
||||
}
|
||||
|
||||
.history-mode-tag {
|
||||
font-size: 0.625rem;
|
||||
font-weight: 500;
|
||||
padding: 0.1875rem 0.5rem;
|
||||
background: hsl(var(--muted));
|
||||
color: hsl(var(--muted-foreground));
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.history-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
font-size: 0.625rem;
|
||||
font-weight: 500;
|
||||
padding: 0.1875rem 0.5rem;
|
||||
border-radius: 9999px;
|
||||
}
|
||||
|
||||
.history-status.success {
|
||||
background: hsl(var(--success) / 0.12);
|
||||
color: hsl(var(--success));
|
||||
}
|
||||
|
||||
.history-status.warning {
|
||||
background: hsl(var(--warning) / 0.12);
|
||||
color: hsl(var(--warning));
|
||||
}
|
||||
|
||||
.history-status.error {
|
||||
background: hsl(var(--destructive) / 0.12);
|
||||
color: hsl(var(--destructive));
|
||||
}
|
||||
|
||||
.history-item-prompt {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 450;
|
||||
color: hsl(var(--foreground));
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 100%;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.history-item-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
font-size: 0.6875rem;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
.history-item-meta span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.history-item-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s ease;
|
||||
margin-left: 0.75rem;
|
||||
}
|
||||
|
||||
/* History Empty State */
|
||||
.history-empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 4rem 2rem;
|
||||
text-align: center;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
.history-empty-state i {
|
||||
opacity: 0.3;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.history-empty-state h3 {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: hsl(var(--foreground));
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.history-empty-state p {
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
* Legacy Container Styles (kept for compatibility)
|
||||
* ======================================== */
|
||||
|
||||
/* Container */
|
||||
.cli-manager-container {
|
||||
display: flex;
|
||||
@@ -69,7 +622,7 @@
|
||||
|
||||
.cli-tools-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 0.625rem;
|
||||
}
|
||||
@@ -174,6 +727,36 @@
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* CLI Tool Description */
|
||||
.cli-tool-desc {
|
||||
font-size: 0.625rem;
|
||||
color: hsl(var(--muted-foreground) / 0.8);
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
/* CLI Tool Actions */
|
||||
.cli-tool-actions {
|
||||
min-height: 1.75rem;
|
||||
}
|
||||
|
||||
/* CodexLens specific styles */
|
||||
.cli-tool-card.tool-codexlens.available {
|
||||
border-color: hsl(35 90% 50% / 0.5);
|
||||
}
|
||||
|
||||
.cli-tool-card.tool-codexlens.available:hover {
|
||||
border-color: hsl(35 90% 50% / 0.7);
|
||||
}
|
||||
|
||||
/* Semantic Search specific styles */
|
||||
.cli-tool-card.tool-semantic.available {
|
||||
border-color: hsl(260 80% 60% / 0.5);
|
||||
}
|
||||
|
||||
.cli-tool-card.tool-semantic.available:hover {
|
||||
border-color: hsl(260 80% 60% / 0.7);
|
||||
}
|
||||
|
||||
/* Execute Panel */
|
||||
.cli-execute-header {
|
||||
display: flex;
|
||||
@@ -662,9 +1245,44 @@
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: 0.25rem 0.5rem;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.25rem;
|
||||
padding: 0.375rem 0.625rem;
|
||||
font-size: 0.75rem;
|
||||
border-radius: 0.25rem;
|
||||
font-weight: 500;
|
||||
border-radius: 0.375rem;
|
||||
border: 1px solid hsl(var(--border));
|
||||
background: hsl(var(--background));
|
||||
color: hsl(var(--foreground));
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.btn-sm:hover {
|
||||
background: hsl(var(--hover));
|
||||
border-color: hsl(var(--primary) / 0.3);
|
||||
}
|
||||
|
||||
.btn-sm.btn-primary {
|
||||
background: hsl(var(--primary));
|
||||
color: hsl(var(--primary-foreground));
|
||||
border-color: hsl(var(--primary));
|
||||
}
|
||||
|
||||
.btn-sm.btn-primary:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.btn-sm.btn-outline {
|
||||
background: transparent;
|
||||
border-color: hsl(var(--border));
|
||||
}
|
||||
|
||||
.btn-sm.btn-outline:hover {
|
||||
background: hsl(var(--hover));
|
||||
}
|
||||
|
||||
.btn-outline {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
// ========== CLI State ==========
|
||||
let cliToolStatus = { gemini: {}, qwen: {}, codex: {} };
|
||||
let codexLensStatus = { ready: false };
|
||||
let semanticStatus = { available: false };
|
||||
let defaultCliTool = 'gemini';
|
||||
|
||||
// ========== Initialization ==========
|
||||
@@ -41,6 +42,11 @@ async function loadCodexLensStatus() {
|
||||
// Update CodexLens badge
|
||||
updateCodexLensBadge();
|
||||
|
||||
// If CodexLens is ready, also check semantic status
|
||||
if (data.ready) {
|
||||
await loadSemanticStatus();
|
||||
}
|
||||
|
||||
return data;
|
||||
} catch (err) {
|
||||
console.error('Failed to load CodexLens status:', err);
|
||||
@@ -48,6 +54,19 @@ async function loadCodexLensStatus() {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSemanticStatus() {
|
||||
try {
|
||||
const response = await fetch('/api/codexlens/semantic/status');
|
||||
if (!response.ok) throw new Error('Failed to load semantic status');
|
||||
const data = await response.json();
|
||||
semanticStatus = data;
|
||||
return data;
|
||||
} catch (err) {
|
||||
console.error('Failed to load semantic status:', err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Badge Update ==========
|
||||
function updateCliBadge() {
|
||||
const badge = document.getElementById('badgeCliTools');
|
||||
@@ -75,6 +94,18 @@ function renderCliStatus() {
|
||||
const container = document.getElementById('cli-status-panel');
|
||||
if (!container) return;
|
||||
|
||||
const toolDescriptions = {
|
||||
gemini: 'Google AI for code analysis',
|
||||
qwen: 'Alibaba AI assistant',
|
||||
codex: 'OpenAI code generation'
|
||||
};
|
||||
|
||||
const toolIcons = {
|
||||
gemini: 'sparkle',
|
||||
qwen: 'bot',
|
||||
codex: 'code-2'
|
||||
};
|
||||
|
||||
const tools = ['gemini', 'qwen', 'codex'];
|
||||
|
||||
const toolsHtml = tools.map(tool => {
|
||||
@@ -89,21 +120,28 @@ function renderCliStatus() {
|
||||
<span class="cli-tool-name">${tool.charAt(0).toUpperCase() + tool.slice(1)}</span>
|
||||
${isDefault ? '<span class="cli-tool-badge">Default</span>' : ''}
|
||||
</div>
|
||||
<div class="cli-tool-info">
|
||||
<div class="cli-tool-desc text-xs text-muted-foreground mt-1">
|
||||
${toolDescriptions[tool]}
|
||||
</div>
|
||||
<div class="cli-tool-info mt-2">
|
||||
${isAvailable
|
||||
? `<span class="text-success">Ready</span>`
|
||||
: `<span class="text-muted-foreground">Not Installed</span>`
|
||||
? `<span class="text-success flex items-center gap-1"><i data-lucide="check-circle" class="w-3 h-3"></i> Ready</span>`
|
||||
: `<span class="text-muted-foreground flex items-center gap-1"><i data-lucide="circle-dashed" class="w-3 h-3"></i> Not Installed</span>`
|
||||
}
|
||||
</div>
|
||||
<div class="cli-tool-actions mt-3">
|
||||
${isAvailable && !isDefault
|
||||
? `<button class="btn-sm btn-outline flex items-center gap-1" onclick="setDefaultCliTool('${tool}')">
|
||||
<i data-lucide="star" class="w-3 h-3"></i> Set Default
|
||||
</button>`
|
||||
: ''
|
||||
}
|
||||
</div>
|
||||
${isAvailable && !isDefault
|
||||
? `<button class="btn-sm btn-outline" onclick="setDefaultCliTool('${tool}')">Set Default</button>`
|
||||
: ''
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
// CodexLens card
|
||||
// CodexLens card with semantic search info
|
||||
const codexLensHtml = `
|
||||
<div class="cli-tool-card tool-codexlens ${codexLensStatus.ready ? 'available' : 'unavailable'}">
|
||||
<div class="cli-tool-header">
|
||||
@@ -111,21 +149,64 @@ function renderCliStatus() {
|
||||
<span class="cli-tool-name">CodexLens</span>
|
||||
<span class="badge px-1.5 py-0.5 text-xs rounded bg-muted text-muted-foreground">Index</span>
|
||||
</div>
|
||||
<div class="cli-tool-info">
|
||||
<div class="cli-tool-desc text-xs text-muted-foreground mt-1">
|
||||
${codexLensStatus.ready ? 'Code indexing & FTS search' : 'Full-text code search engine'}
|
||||
</div>
|
||||
<div class="cli-tool-info mt-2">
|
||||
${codexLensStatus.ready
|
||||
? `<span class="text-success">v${codexLensStatus.version || 'installed'}</span>`
|
||||
: `<span class="text-muted-foreground">Not Installed</span>`
|
||||
? `<span class="text-success flex items-center gap-1"><i data-lucide="check-circle" class="w-3 h-3"></i> v${codexLensStatus.version || 'installed'}</span>`
|
||||
: `<span class="text-muted-foreground flex items-center gap-1"><i data-lucide="circle-dashed" class="w-3 h-3"></i> Not Installed</span>`
|
||||
}
|
||||
</div>
|
||||
<div class="cli-tool-actions flex gap-2 mt-2">
|
||||
<div class="cli-tool-actions flex gap-2 mt-3">
|
||||
${!codexLensStatus.ready
|
||||
? `<button class="btn-sm btn-primary" onclick="installCodexLens()">Install</button>`
|
||||
: `<button class="btn-sm btn-outline" onclick="initCodexLensIndex()">Init Index</button>`
|
||||
? `<button class="btn-sm btn-primary flex items-center gap-1" onclick="installCodexLens()">
|
||||
<i data-lucide="download" class="w-3 h-3"></i> Install
|
||||
</button>`
|
||||
: `<button class="btn-sm btn-outline flex items-center gap-1" onclick="initCodexLensIndex()">
|
||||
<i data-lucide="database" class="w-3 h-3"></i> Init Index
|
||||
</button>`
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Semantic Search card (only show if CodexLens is installed)
|
||||
const semanticHtml = codexLensStatus.ready ? `
|
||||
<div class="cli-tool-card tool-semantic ${semanticStatus.available ? 'available' : 'unavailable'}">
|
||||
<div class="cli-tool-header">
|
||||
<span class="cli-tool-status ${semanticStatus.available ? 'status-available' : 'status-unavailable'}"></span>
|
||||
<span class="cli-tool-name">Semantic Search</span>
|
||||
<span class="badge px-1.5 py-0.5 text-xs rounded ${semanticStatus.available ? 'bg-primary/20 text-primary' : 'bg-muted text-muted-foreground'}">AI</span>
|
||||
</div>
|
||||
<div class="cli-tool-desc text-xs text-muted-foreground mt-1">
|
||||
${semanticStatus.available ? 'AI-powered code understanding' : 'Natural language code search'}
|
||||
</div>
|
||||
<div class="cli-tool-info mt-2">
|
||||
${semanticStatus.available
|
||||
? `<span class="text-success flex items-center gap-1"><i data-lucide="sparkles" class="w-3 h-3"></i> ${semanticStatus.backend || 'Ready'}</span>`
|
||||
: `<span class="text-muted-foreground flex items-center gap-1"><i data-lucide="circle-dashed" class="w-3 h-3"></i> Not Installed</span>`
|
||||
}
|
||||
</div>
|
||||
<div class="cli-tool-actions flex flex-col gap-2 mt-3">
|
||||
${!semanticStatus.available ? `
|
||||
<button class="btn-sm btn-primary w-full flex items-center justify-center gap-1" onclick="openSemanticInstallWizard()">
|
||||
<i data-lucide="brain" class="w-3 h-3"></i> Install AI Model
|
||||
</button>
|
||||
<div class="flex items-center justify-center gap-1 text-xs text-muted-foreground">
|
||||
<i data-lucide="hard-drive" class="w-3 h-3"></i>
|
||||
<span>~500MB download</span>
|
||||
</div>
|
||||
` : `
|
||||
<div class="flex items-center gap-1 text-xs text-muted-foreground">
|
||||
<i data-lucide="cpu" class="w-3 h-3"></i>
|
||||
<span>bge-small-en-v1.5</span>
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
</div>
|
||||
` : '';
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="cli-status-header">
|
||||
<h3><i data-lucide="terminal" class="w-4 h-4"></i> CLI Tools</h3>
|
||||
@@ -136,6 +217,7 @@ function renderCliStatus() {
|
||||
<div class="cli-tools-grid">
|
||||
${toolsHtml}
|
||||
${codexLensHtml}
|
||||
${semanticHtml}
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -203,3 +285,152 @@ async function initCodexLensIndex() {
|
||||
showRefreshToast(`Init error: ${err.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Semantic Search Installation Wizard ==========
|
||||
function openSemanticInstallWizard() {
|
||||
const modal = document.createElement('div');
|
||||
modal.id = 'semanticInstallModal';
|
||||
modal.className = 'fixed inset-0 bg-black/50 flex items-center justify-center z-50';
|
||||
modal.innerHTML = `
|
||||
<div class="bg-card rounded-lg shadow-xl w-full max-w-md mx-4 overflow-hidden">
|
||||
<div class="p-6">
|
||||
<div class="flex items-center gap-3 mb-4">
|
||||
<div class="w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center">
|
||||
<i data-lucide="brain" class="w-5 h-5 text-primary"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold">Install Semantic Search</h3>
|
||||
<p class="text-sm text-muted-foreground">AI-powered code understanding</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="bg-muted/50 rounded-lg p-4">
|
||||
<h4 class="font-medium mb-2">What will be installed:</h4>
|
||||
<ul class="text-sm space-y-2 text-muted-foreground">
|
||||
<li class="flex items-start gap-2">
|
||||
<i data-lucide="check" class="w-4 h-4 text-success mt-0.5"></i>
|
||||
<span><strong>sentence-transformers</strong> - ML framework</span>
|
||||
</li>
|
||||
<li class="flex items-start gap-2">
|
||||
<i data-lucide="check" class="w-4 h-4 text-success mt-0.5"></i>
|
||||
<span><strong>bge-small-en-v1.5</strong> - Embedding model (~130MB)</span>
|
||||
</li>
|
||||
<li class="flex items-start gap-2">
|
||||
<i data-lucide="check" class="w-4 h-4 text-success mt-0.5"></i>
|
||||
<span><strong>PyTorch</strong> - Deep learning backend (~300MB)</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="bg-warning/10 border border-warning/20 rounded-lg p-3">
|
||||
<div class="flex items-start gap-2">
|
||||
<i data-lucide="alert-triangle" class="w-4 h-4 text-warning mt-0.5"></i>
|
||||
<div class="text-sm">
|
||||
<p class="font-medium text-warning">Large Download</p>
|
||||
<p class="text-muted-foreground">Total size: ~500MB. First-time model loading may take a few minutes.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="semanticInstallProgress" class="hidden">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="animate-spin w-5 h-5 border-2 border-primary border-t-transparent rounded-full"></div>
|
||||
<span class="text-sm" id="semanticInstallStatus">Installing dependencies...</span>
|
||||
</div>
|
||||
<div class="mt-2 h-2 bg-muted rounded-full overflow-hidden">
|
||||
<div id="semanticProgressBar" class="h-full bg-primary transition-all duration-300" style="width: 0%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border-t border-border p-4 flex justify-end gap-3 bg-muted/30">
|
||||
<button class="btn-outline px-4 py-2" onclick="closeSemanticInstallWizard()">Cancel</button>
|
||||
<button id="semanticInstallBtn" class="btn-primary px-4 py-2" onclick="startSemanticInstall()">
|
||||
<i data-lucide="download" class="w-4 h-4 mr-2"></i>
|
||||
Install Now
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(modal);
|
||||
|
||||
// Initialize Lucide icons in modal
|
||||
if (window.lucide) {
|
||||
lucide.createIcons();
|
||||
}
|
||||
}
|
||||
|
||||
function closeSemanticInstallWizard() {
|
||||
const modal = document.getElementById('semanticInstallModal');
|
||||
if (modal) {
|
||||
modal.remove();
|
||||
}
|
||||
}
|
||||
|
||||
async function startSemanticInstall() {
|
||||
const progressDiv = document.getElementById('semanticInstallProgress');
|
||||
const installBtn = document.getElementById('semanticInstallBtn');
|
||||
const statusText = document.getElementById('semanticInstallStatus');
|
||||
const progressBar = document.getElementById('semanticProgressBar');
|
||||
|
||||
// Show progress, disable button
|
||||
progressDiv.classList.remove('hidden');
|
||||
installBtn.disabled = true;
|
||||
installBtn.innerHTML = '<span class="animate-pulse">Installing...</span>';
|
||||
|
||||
// Simulate progress stages
|
||||
const stages = [
|
||||
{ progress: 10, text: 'Installing numpy...' },
|
||||
{ progress: 30, text: 'Installing sentence-transformers...' },
|
||||
{ progress: 50, text: 'Installing PyTorch dependencies...' },
|
||||
{ progress: 70, text: 'Downloading embedding model...' },
|
||||
{ progress: 90, text: 'Finalizing installation...' }
|
||||
];
|
||||
|
||||
let currentStage = 0;
|
||||
const progressInterval = setInterval(() => {
|
||||
if (currentStage < stages.length) {
|
||||
statusText.textContent = stages[currentStage].text;
|
||||
progressBar.style.width = `${stages[currentStage].progress}%`;
|
||||
currentStage++;
|
||||
}
|
||||
}, 2000);
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/codexlens/semantic/install', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({})
|
||||
});
|
||||
|
||||
clearInterval(progressInterval);
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
progressBar.style.width = '100%';
|
||||
statusText.textContent = 'Installation complete!';
|
||||
|
||||
setTimeout(() => {
|
||||
closeSemanticInstallWizard();
|
||||
showRefreshToast('Semantic search installed successfully!', 'success');
|
||||
loadSemanticStatus().then(() => renderCliStatus());
|
||||
}, 1000);
|
||||
} else {
|
||||
statusText.textContent = `Error: ${result.error}`;
|
||||
progressBar.classList.add('bg-destructive');
|
||||
installBtn.disabled = false;
|
||||
installBtn.innerHTML = '<i data-lucide="refresh-cw" class="w-4 h-4 mr-2"></i> Retry';
|
||||
if (window.lucide) lucide.createIcons();
|
||||
}
|
||||
} catch (err) {
|
||||
clearInterval(progressInterval);
|
||||
statusText.textContent = `Error: ${err.message}`;
|
||||
progressBar.classList.add('bg-destructive');
|
||||
installBtn.disabled = false;
|
||||
installBtn.innerHTML = '<i data-lucide="refresh-cw" class="w-4 h-4 mr-2"></i> Retry';
|
||||
if (window.lucide) lucide.createIcons();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,6 +74,29 @@ const HOOK_TEMPLATES = {
|
||||
tool: { type: 'select', options: ['gemini', 'qwen', 'codex'], default: 'gemini', label: 'CLI Tool' },
|
||||
interval: { type: 'number', default: 300, min: 60, max: 3600, label: 'Interval (seconds)', step: 60 }
|
||||
}
|
||||
},
|
||||
// SKILL Context Loader templates
|
||||
'skill-context-keyword': {
|
||||
event: 'UserPromptSubmit',
|
||||
matcher: '',
|
||||
command: 'bash',
|
||||
args: ['-c', 'ccw tool exec skill_context_loader \'{"keywords":"$SKILL_KEYWORDS","skills":"$SKILL_NAMES","prompt":"$CLAUDE_PROMPT"}\''],
|
||||
description: 'Load SKILL context based on keyword matching in user prompt',
|
||||
category: 'skill',
|
||||
configurable: true,
|
||||
config: {
|
||||
keywords: { type: 'text', default: '', label: 'Keywords (comma-separated)', placeholder: 'react,workflow,api' },
|
||||
skills: { type: 'text', default: '', label: 'SKILL Names (comma-separated)', placeholder: 'prompt-enhancer,command-guide' }
|
||||
}
|
||||
},
|
||||
'skill-context-auto': {
|
||||
event: 'UserPromptSubmit',
|
||||
matcher: '',
|
||||
command: 'bash',
|
||||
args: ['-c', 'ccw tool exec skill_context_loader \'{"mode":"auto","prompt":"$CLAUDE_PROMPT"}\''],
|
||||
description: 'Auto-detect and load SKILL based on skill name in prompt',
|
||||
category: 'skill',
|
||||
configurable: false
|
||||
}
|
||||
};
|
||||
|
||||
@@ -102,6 +125,28 @@ const WIZARD_TEMPLATES = {
|
||||
{ key: 'interval', type: 'number', label: 'Interval (seconds)', default: 300, min: 60, max: 3600, step: 60, showFor: ['periodic'], description: 'Time between updates' },
|
||||
{ key: 'strategy', type: 'select', label: 'Update Strategy', options: ['related', 'single-layer'], default: 'related', description: 'Related: changed modules, Single-layer: current directory' }
|
||||
]
|
||||
},
|
||||
'skill-context': {
|
||||
name: 'SKILL Context Loader',
|
||||
description: 'Automatically load SKILL packages based on keywords in user prompts',
|
||||
icon: 'sparkles',
|
||||
options: [
|
||||
{
|
||||
id: 'keyword',
|
||||
name: 'Keyword Matching',
|
||||
description: 'Load specific SKILLs when keywords are detected in prompt',
|
||||
templateId: 'skill-context-keyword'
|
||||
},
|
||||
{
|
||||
id: 'auto',
|
||||
name: 'Auto Detection',
|
||||
description: 'Automatically detect and load SKILLs by name in prompt',
|
||||
templateId: 'skill-context-auto'
|
||||
}
|
||||
],
|
||||
configFields: [],
|
||||
requiresSkillDiscovery: true,
|
||||
customRenderer: 'renderSkillContextConfig'
|
||||
}
|
||||
};
|
||||
|
||||
@@ -327,7 +372,8 @@ function getHookEventDescription(event) {
|
||||
'PreToolUse': 'Runs before a tool is executed',
|
||||
'PostToolUse': 'Runs after a tool completes',
|
||||
'Notification': 'Runs when a notification is triggered',
|
||||
'Stop': 'Runs when the agent stops'
|
||||
'Stop': 'Runs when the agent stops',
|
||||
'UserPromptSubmit': 'Runs when user submits a prompt'
|
||||
};
|
||||
return descriptions[event] || event;
|
||||
}
|
||||
@@ -337,7 +383,8 @@ function getHookEventIcon(event) {
|
||||
'PreToolUse': '⏳',
|
||||
'PostToolUse': '✅',
|
||||
'Notification': '🔔',
|
||||
'Stop': '🛑'
|
||||
'Stop': '🛑',
|
||||
'UserPromptSubmit': '💬'
|
||||
};
|
||||
return icons[event] || '🪝';
|
||||
}
|
||||
@@ -347,7 +394,468 @@ function getHookEventIconLucide(event) {
|
||||
'PreToolUse': '<i data-lucide="clock" class="w-5 h-5"></i>',
|
||||
'PostToolUse': '<i data-lucide="check-circle" class="w-5 h-5"></i>',
|
||||
'Notification': '<i data-lucide="bell" class="w-5 h-5"></i>',
|
||||
'Stop': '<i data-lucide="octagon-x" class="w-5 h-5"></i>'
|
||||
'Stop': '<i data-lucide="octagon-x" class="w-5 h-5"></i>',
|
||||
'UserPromptSubmit': '<i data-lucide="message-square" class="w-5 h-5"></i>'
|
||||
};
|
||||
return icons[event] || '<i data-lucide="webhook" class="w-5 h-5"></i>';
|
||||
}
|
||||
|
||||
// ========== Wizard Modal Functions ==========
|
||||
let currentWizardTemplate = null;
|
||||
let wizardConfig = {};
|
||||
|
||||
function openHookWizardModal(wizardId) {
|
||||
const wizard = WIZARD_TEMPLATES[wizardId];
|
||||
if (!wizard) {
|
||||
showRefreshToast('Wizard template not found', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
currentWizardTemplate = { id: wizardId, ...wizard };
|
||||
wizardConfig = {};
|
||||
|
||||
// Set defaults
|
||||
wizard.configFields.forEach(field => {
|
||||
wizardConfig[field.key] = field.default;
|
||||
});
|
||||
|
||||
const modal = document.getElementById('hookWizardModal');
|
||||
if (modal) {
|
||||
renderWizardModalContent();
|
||||
modal.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function closeHookWizardModal() {
|
||||
const modal = document.getElementById('hookWizardModal');
|
||||
if (modal) {
|
||||
modal.classList.add('hidden');
|
||||
currentWizardTemplate = null;
|
||||
wizardConfig = {};
|
||||
}
|
||||
}
|
||||
|
||||
function renderWizardModalContent() {
|
||||
const container = document.getElementById('wizardModalContent');
|
||||
if (!container || !currentWizardTemplate) return;
|
||||
|
||||
const wizard = currentWizardTemplate;
|
||||
const selectedOption = wizardConfig.triggerType || wizard.options[0].id;
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="space-y-6">
|
||||
<!-- Wizard Header -->
|
||||
<div class="flex items-center gap-3 pb-4 border-b border-border">
|
||||
<div class="p-2 bg-primary/10 rounded-lg">
|
||||
<i data-lucide="${wizard.icon}" class="w-6 h-6 text-primary"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-foreground">${escapeHtml(wizard.name)}</h3>
|
||||
<p class="text-sm text-muted-foreground">${escapeHtml(wizard.description)}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Trigger Type Selection -->
|
||||
<div class="space-y-3">
|
||||
<label class="block text-sm font-medium text-foreground">When to Trigger</label>
|
||||
<div class="grid grid-cols-1 gap-3">
|
||||
${wizard.options.map(opt => `
|
||||
<label class="flex items-start gap-3 p-3 border rounded-lg cursor-pointer transition-all ${selectedOption === opt.id ? 'border-primary bg-primary/5' : 'border-border hover:border-muted-foreground'}">
|
||||
<input type="radio" name="wizardTrigger" value="${opt.id}"
|
||||
${selectedOption === opt.id ? 'checked' : ''}
|
||||
onchange="updateWizardTrigger('${opt.id}')"
|
||||
class="mt-1">
|
||||
<div class="flex-1">
|
||||
<span class="font-medium text-foreground">${escapeHtml(opt.name)}</span>
|
||||
<p class="text-sm text-muted-foreground">${escapeHtml(opt.description)}</p>
|
||||
</div>
|
||||
</label>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Configuration Fields -->
|
||||
<div class="space-y-4">
|
||||
<label class="block text-sm font-medium text-foreground">Configuration</label>
|
||||
${wizard.customRenderer ? window[wizard.customRenderer]() : wizard.configFields.map(field => {
|
||||
// Check if field should be shown for current trigger type
|
||||
const shouldShow = !field.showFor || field.showFor.includes(selectedOption);
|
||||
if (!shouldShow) return '';
|
||||
|
||||
const value = wizardConfig[field.key] ?? field.default;
|
||||
|
||||
if (field.type === 'select') {
|
||||
return `
|
||||
<div class="space-y-1">
|
||||
<label class="block text-sm text-muted-foreground">${escapeHtml(field.label)}</label>
|
||||
<select id="wizard_${field.key}"
|
||||
onchange="updateWizardConfig('${field.key}', this.value)"
|
||||
class="w-full px-3 py-2 bg-background border border-border rounded-lg text-foreground focus:outline-none focus:ring-2 focus:ring-primary">
|
||||
${field.options.map(opt => `
|
||||
<option value="${opt}" ${value === opt ? 'selected' : ''}>${opt}</option>
|
||||
`).join('')}
|
||||
</select>
|
||||
${field.description ? `<p class="text-xs text-muted-foreground">${escapeHtml(field.description)}</p>` : ''}
|
||||
</div>
|
||||
`;
|
||||
} else if (field.type === 'number') {
|
||||
return `
|
||||
<div class="space-y-1">
|
||||
<label class="block text-sm text-muted-foreground">${escapeHtml(field.label)}</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<input type="number" id="wizard_${field.key}"
|
||||
value="${value}"
|
||||
min="${field.min || 0}"
|
||||
max="${field.max || 9999}"
|
||||
step="${field.step || 1}"
|
||||
onchange="updateWizardConfig('${field.key}', parseInt(this.value))"
|
||||
class="flex-1 px-3 py-2 bg-background border border-border rounded-lg text-foreground focus:outline-none focus:ring-2 focus:ring-primary">
|
||||
<span class="text-sm text-muted-foreground">${formatIntervalDisplay(value)}</span>
|
||||
</div>
|
||||
${field.description ? `<p class="text-xs text-muted-foreground">${escapeHtml(field.description)}</p>` : ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
return '';
|
||||
}).join('')}
|
||||
</div>
|
||||
|
||||
<!-- Preview -->
|
||||
<div class="space-y-2">
|
||||
<label class="block text-sm font-medium text-foreground">Generated Command Preview</label>
|
||||
<div class="bg-muted/50 rounded-lg p-3 font-mono text-xs overflow-x-auto">
|
||||
<pre id="wizardCommandPreview" class="whitespace-pre-wrap text-muted-foreground">${escapeHtml(generateWizardCommand())}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scope Selection -->
|
||||
<div class="space-y-3">
|
||||
<label class="block text-sm font-medium text-foreground">Install To</label>
|
||||
<div class="flex gap-4">
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="radio" name="wizardScope" value="project" checked>
|
||||
<span class="text-sm text-foreground">Project</span>
|
||||
<span class="text-xs text-muted-foreground">(.claude/settings.json)</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="radio" name="wizardScope" value="global">
|
||||
<span class="text-sm text-foreground">Global</span>
|
||||
<span class="text-xs text-muted-foreground">(~/.claude/settings.json)</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Initialize Lucide icons
|
||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||
}
|
||||
|
||||
function updateWizardTrigger(triggerId) {
|
||||
wizardConfig.triggerType = triggerId;
|
||||
renderWizardModalContent();
|
||||
}
|
||||
|
||||
function updateWizardConfig(key, value) {
|
||||
wizardConfig[key] = value;
|
||||
// Update command preview
|
||||
const preview = document.getElementById('wizardCommandPreview');
|
||||
if (preview) {
|
||||
preview.textContent = generateWizardCommand();
|
||||
}
|
||||
// Re-render if interval changed (to update display)
|
||||
if (key === 'interval') {
|
||||
const displaySpan = document.querySelector(`#wizard_${key}`)?.parentElement?.querySelector('.text-muted-foreground:last-child');
|
||||
if (displaySpan) {
|
||||
displaySpan.textContent = formatIntervalDisplay(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function formatIntervalDisplay(seconds) {
|
||||
if (seconds < 60) return `${seconds}s`;
|
||||
const mins = Math.floor(seconds / 60);
|
||||
const secs = seconds % 60;
|
||||
if (secs === 0) return `${mins}min`;
|
||||
return `${mins}min ${secs}s`;
|
||||
}
|
||||
|
||||
// ========== SKILL Context Wizard Custom Functions ==========
|
||||
function renderSkillContextConfig() {
|
||||
const selectedOption = wizardConfig.triggerType || 'keyword';
|
||||
const skillConfigs = wizardConfig.skillConfigs || [];
|
||||
const availableSkills = window.availableSkills || [];
|
||||
|
||||
if (selectedOption === 'auto') {
|
||||
return `
|
||||
<div class="bg-muted/30 rounded-lg p-4 text-sm text-muted-foreground">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<i data-lucide="info" class="w-4 h-4"></i>
|
||||
<span class="font-medium">Auto Detection Mode</span>
|
||||
</div>
|
||||
<p>SKILLs will be automatically loaded when their name appears in your prompt.</p>
|
||||
<p class="mt-2">Available SKILLs: ${availableSkills.map(s => \`<span class="px-1.5 py-0.5 bg-emerald-500/10 text-emerald-500 rounded text-xs">${escapeHtml(s.name)}</span>\`).join(' ')}</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm font-medium text-foreground">Configure SKILLs</span>
|
||||
<button type="button" onclick="addSkillConfig()"
|
||||
class="px-3 py-1.5 text-xs bg-primary text-primary-foreground rounded-lg hover:opacity-90 flex items-center gap-1">
|
||||
<i data-lucide="plus" class="w-3 h-3"></i> Add SKILL
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="skillConfigsList" class="space-y-3">
|
||||
${skillConfigs.length === 0 ? \`
|
||||
<div class="text-center py-6 text-muted-foreground text-sm border border-dashed border-border rounded-lg">
|
||||
<i data-lucide="package" class="w-8 h-8 mx-auto mb-2 opacity-50"></i>
|
||||
<p>No SKILLs configured yet</p>
|
||||
<p class="text-xs mt-1">Click "Add SKILL" to configure keyword triggers</p>
|
||||
</div>
|
||||
\` : skillConfigs.map((config, idx) => \`
|
||||
<div class="border border-border rounded-lg p-3 bg-card">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<select onchange="updateSkillConfig(${idx}, 'skill', this.value)"
|
||||
class="px-2 py-1 text-sm bg-background border border-border rounded text-foreground">
|
||||
<option value="">Select SKILL...</option>
|
||||
${availableSkills.map(s => \`
|
||||
<option value="${s.id}" ${config.skill === s.id ? 'selected' : ''}>${escapeHtml(s.name)}</option>
|
||||
\`).join('')}
|
||||
</select>
|
||||
<button onclick="removeSkillConfig(${idx})"
|
||||
class="p-1 text-muted-foreground hover:text-destructive rounded">
|
||||
<i data-lucide="trash-2" class="w-4 h-4"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<label class="text-xs text-muted-foreground">Trigger Keywords (comma-separated)</label>
|
||||
<input type="text"
|
||||
value="${config.keywords || ''}"
|
||||
onchange="updateSkillConfig(${idx}, 'keywords', this.value)"
|
||||
placeholder="e.g., react, hooks, component"
|
||||
class="w-full px-2 py-1.5 text-sm bg-background border border-border rounded text-foreground">
|
||||
</div>
|
||||
</div>
|
||||
\`).join('')}
|
||||
</div>
|
||||
|
||||
${availableSkills.length === 0 ? \`
|
||||
<div class="text-xs text-amber-500 flex items-center gap-1">
|
||||
<i data-lucide="alert-triangle" class="w-3 h-3"></i>
|
||||
No SKILLs found. Create SKILL packages in .claude/skills/
|
||||
</div>
|
||||
\` : ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function addSkillConfig() {
|
||||
if (!wizardConfig.skillConfigs) {
|
||||
wizardConfig.skillConfigs = [];
|
||||
}
|
||||
wizardConfig.skillConfigs.push({ skill: '', keywords: '' });
|
||||
renderWizardModalContent();
|
||||
}
|
||||
|
||||
function removeSkillConfig(index) {
|
||||
if (wizardConfig.skillConfigs) {
|
||||
wizardConfig.skillConfigs.splice(index, 1);
|
||||
renderWizardModalContent();
|
||||
}
|
||||
}
|
||||
|
||||
function updateSkillConfig(index, key, value) {
|
||||
if (wizardConfig.skillConfigs && wizardConfig.skillConfigs[index]) {
|
||||
wizardConfig.skillConfigs[index][key] = value;
|
||||
const preview = document.getElementById('wizardCommandPreview');
|
||||
if (preview) {
|
||||
preview.textContent = generateWizardCommand();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
function generateWizardCommand() {
|
||||
if (!currentWizardTemplate) return '';
|
||||
|
||||
const wizard = currentWizardTemplate;
|
||||
const wizardId = wizard.id;
|
||||
const triggerType = wizardConfig.triggerType || wizard.options[0].id;
|
||||
const selectedOption = wizard.options.find(o => o.id === triggerType);
|
||||
if (!selectedOption) return '';
|
||||
|
||||
const baseTemplate = HOOK_TEMPLATES[selectedOption.templateId];
|
||||
if (!baseTemplate) return '';
|
||||
|
||||
// Handle skill-context wizard
|
||||
if (wizardId === 'skill-context') {
|
||||
const keywords = wizardConfig.keywords || '';
|
||||
const skills = wizardConfig.skills || '';
|
||||
|
||||
if (triggerType === 'keyword') {
|
||||
const params = JSON.stringify({ keywords, skills, prompt: '$CLAUDE_PROMPT' });
|
||||
return `ccw tool exec skill_context_loader '${params}'`;
|
||||
} else {
|
||||
// auto mode
|
||||
const params = JSON.stringify({ mode: 'auto', prompt: '$CLAUDE_PROMPT' });
|
||||
return `ccw tool exec skill_context_loader '${params}'`;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle memory-update wizard (default)
|
||||
const tool = wizardConfig.tool || 'gemini';
|
||||
const strategy = wizardConfig.strategy || 'related';
|
||||
const interval = wizardConfig.interval || 300;
|
||||
|
||||
// Build the ccw tool command based on configuration
|
||||
const params = JSON.stringify({ strategy, tool });
|
||||
|
||||
if (triggerType === 'periodic') {
|
||||
return `INTERVAL=${interval}; LAST_FILE=~/.claude/.last_memory_update; NOW=$(date +%s); LAST=0; [ -f "$LAST_FILE" ] && LAST=$(cat "$LAST_FILE"); if [ $((NOW - LAST)) -ge $INTERVAL ]; then echo $NOW > "$LAST_FILE"; ccw tool exec update_module_claude '${params}' & fi`;
|
||||
} else {
|
||||
return `ccw tool exec update_module_claude '${params}'`;
|
||||
}
|
||||
}
|
||||
|
||||
async function submitHookWizard() {
|
||||
if (!currentWizardTemplate) return;
|
||||
|
||||
const wizard = currentWizardTemplate;
|
||||
const triggerType = wizardConfig.triggerType || wizard.options[0].id;
|
||||
const selectedOption = wizard.options.find(o => o.id === triggerType);
|
||||
if (!selectedOption) return;
|
||||
|
||||
const baseTemplate = HOOK_TEMPLATES[selectedOption.templateId];
|
||||
if (!baseTemplate) return;
|
||||
|
||||
const scope = document.querySelector('input[name="wizardScope"]:checked')?.value || 'project';
|
||||
const command = generateWizardCommand();
|
||||
|
||||
const hookData = {
|
||||
command: 'bash',
|
||||
args: ['-c', command]
|
||||
};
|
||||
|
||||
if (baseTemplate.matcher) {
|
||||
hookData.matcher = baseTemplate.matcher;
|
||||
}
|
||||
|
||||
await saveHook(scope, baseTemplate.event, hookData);
|
||||
closeHookWizardModal();
|
||||
}
|
||||
|
||||
// ========== Template View/Copy Functions ==========
|
||||
function viewTemplateDetails(templateId) {
|
||||
const template = HOOK_TEMPLATES[templateId];
|
||||
if (!template) return;
|
||||
|
||||
const modal = document.getElementById('templateViewModal');
|
||||
const content = document.getElementById('templateViewContent');
|
||||
|
||||
if (modal && content) {
|
||||
const args = template.args || [];
|
||||
content.innerHTML = `
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center gap-3 pb-3 border-b border-border">
|
||||
<i data-lucide="webhook" class="w-5 h-5 text-primary"></i>
|
||||
<div>
|
||||
<h4 class="font-semibold text-foreground">${escapeHtml(templateId)}</h4>
|
||||
<p class="text-sm text-muted-foreground">${escapeHtml(template.description || 'No description')}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3 text-sm">
|
||||
<div class="flex items-start gap-2">
|
||||
<span class="font-mono text-xs bg-muted px-1.5 py-0.5 rounded shrink-0 w-16">Event</span>
|
||||
<span class="font-medium text-foreground">${escapeHtml(template.event)}</span>
|
||||
</div>
|
||||
<div class="flex items-start gap-2">
|
||||
<span class="font-mono text-xs bg-muted px-1.5 py-0.5 rounded shrink-0 w-16">Matcher</span>
|
||||
<span class="text-muted-foreground">${escapeHtml(template.matcher || 'All tools')}</span>
|
||||
</div>
|
||||
<div class="flex items-start gap-2">
|
||||
<span class="font-mono text-xs bg-muted px-1.5 py-0.5 rounded shrink-0 w-16">Command</span>
|
||||
<code class="font-mono text-xs text-foreground">${escapeHtml(template.command)}</code>
|
||||
</div>
|
||||
${args.length > 0 ? `
|
||||
<div class="flex items-start gap-2">
|
||||
<span class="font-mono text-xs bg-muted px-1.5 py-0.5 rounded shrink-0 w-16">Args</span>
|
||||
<div class="flex-1">
|
||||
<pre class="font-mono text-xs text-muted-foreground bg-muted/50 rounded p-2 overflow-x-auto whitespace-pre-wrap">${escapeHtml(args.join('\n'))}</pre>
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
${template.category ? `
|
||||
<div class="flex items-start gap-2">
|
||||
<span class="font-mono text-xs bg-muted px-1.5 py-0.5 rounded shrink-0 w-16">Category</span>
|
||||
<span class="px-2 py-0.5 text-xs rounded-full bg-primary/10 text-primary">${escapeHtml(template.category)}</span>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2 pt-3 border-t border-border">
|
||||
<button class="flex-1 px-3 py-2 text-sm bg-primary text-primary-foreground rounded-lg hover:opacity-90 transition-opacity"
|
||||
onclick="copyTemplateToClipboard('${templateId}')">
|
||||
<i data-lucide="copy" class="w-4 h-4 inline mr-1"></i> Copy JSON
|
||||
</button>
|
||||
<button class="flex-1 px-3 py-2 text-sm bg-muted text-foreground rounded-lg hover:bg-hover transition-colors"
|
||||
onclick="editTemplateAsNew('${templateId}')">
|
||||
<i data-lucide="pencil" class="w-4 h-4 inline mr-1"></i> Edit as New
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
modal.classList.remove('hidden');
|
||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||
}
|
||||
}
|
||||
|
||||
function closeTemplateViewModal() {
|
||||
const modal = document.getElementById('templateViewModal');
|
||||
if (modal) {
|
||||
modal.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function copyTemplateToClipboard(templateId) {
|
||||
const template = HOOK_TEMPLATES[templateId];
|
||||
if (!template) return;
|
||||
|
||||
const hookJson = {
|
||||
matcher: template.matcher || undefined,
|
||||
command: template.command,
|
||||
args: template.args
|
||||
};
|
||||
|
||||
// Clean up undefined values
|
||||
Object.keys(hookJson).forEach(key => {
|
||||
if (hookJson[key] === undefined || hookJson[key] === '') {
|
||||
delete hookJson[key];
|
||||
}
|
||||
});
|
||||
|
||||
navigator.clipboard.writeText(JSON.stringify(hookJson, null, 2))
|
||||
.then(() => showRefreshToast('Template copied to clipboard', 'success'))
|
||||
.catch(() => showRefreshToast('Failed to copy', 'error'));
|
||||
}
|
||||
|
||||
function editTemplateAsNew(templateId) {
|
||||
const template = HOOK_TEMPLATES[templateId];
|
||||
if (!template) return;
|
||||
|
||||
closeTemplateViewModal();
|
||||
|
||||
// Open create modal with template data
|
||||
openHookCreateModal({
|
||||
event: template.event,
|
||||
matcher: template.matcher || '',
|
||||
command: template.command,
|
||||
args: template.args || []
|
||||
});
|
||||
}
|
||||
@@ -98,6 +98,12 @@ function initNavigation() {
|
||||
renderProjectOverview();
|
||||
} else if (currentView === 'explorer') {
|
||||
renderExplorer();
|
||||
} else if (currentView === 'cli-manager') {
|
||||
renderCliManager();
|
||||
} else if (currentView === 'cli-history') {
|
||||
renderCliHistoryView();
|
||||
} else if (currentView === 'hook-manager') {
|
||||
renderHookManager();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -118,6 +124,8 @@ function updateContentTitle() {
|
||||
titleEl.textContent = 'File Explorer';
|
||||
} else if (currentView === 'cli-manager') {
|
||||
titleEl.textContent = 'CLI Tools & CCW';
|
||||
} else if (currentView === 'cli-history') {
|
||||
titleEl.textContent = 'CLI Execution History';
|
||||
} else if (currentView === 'liteTasks') {
|
||||
const names = { 'lite-plan': 'Lite Plan Sessions', 'lite-fix': 'Lite Fix Sessions' };
|
||||
titleEl.textContent = names[currentLiteType] || 'Lite Tasks';
|
||||
|
||||
@@ -15,7 +15,6 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
try { initCarousel(); } catch (e) { console.error('Carousel init failed:', e); }
|
||||
try { initMcpManager(); } catch (e) { console.error('MCP Manager init failed:', e); }
|
||||
try { initHookManager(); } catch (e) { console.error('Hook Manager init failed:', e); }
|
||||
try { initCliManager(); } catch (e) { console.error('CLI Manager init failed:', e); }
|
||||
try { initCliStatus(); } catch (e) { console.error('CLI Status init failed:', e); }
|
||||
try { initGlobalNotifications(); } catch (e) { console.error('Global notifications init failed:', e); }
|
||||
try { initVersionCheck(); } catch (e) { console.error('Version check init failed:', e); }
|
||||
|
||||
@@ -1,26 +1,11 @@
|
||||
// CLI Manager View
|
||||
// Main view combining CLI status, CCW installations, and history panels
|
||||
// Main view combining CLI status and CCW installations panels (two-column layout)
|
||||
|
||||
// ========== CLI Manager State ==========
|
||||
var currentCliExecution = null;
|
||||
var cliExecutionOutput = '';
|
||||
var ccwInstallations = [];
|
||||
|
||||
// ========== Initialization ==========
|
||||
function initCliManager() {
|
||||
document.querySelectorAll('.nav-item[data-view="cli-manager"]').forEach(function(item) {
|
||||
item.addEventListener('click', function() {
|
||||
setActiveNavItem(item);
|
||||
currentView = 'cli-manager';
|
||||
currentFilter = null;
|
||||
currentLiteType = null;
|
||||
currentSessionDetailKey = null;
|
||||
updateContentTitle();
|
||||
renderCliManager();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ========== CCW Installations ==========
|
||||
async function loadCcwInstallations() {
|
||||
try {
|
||||
@@ -50,27 +35,192 @@ async function renderCliManager() {
|
||||
// Load data
|
||||
await Promise.all([
|
||||
loadCliToolStatus(),
|
||||
loadCliHistory(),
|
||||
loadCcwInstallations()
|
||||
]);
|
||||
|
||||
container.innerHTML = '<div class="cli-manager-container">' +
|
||||
'<div class="cli-manager-grid">' +
|
||||
'<div class="cli-panel"><div id="cli-status-panel"></div></div>' +
|
||||
'<div class="cli-panel"><div id="ccw-install-panel"></div></div>' +
|
||||
container.innerHTML = '<div class="status-manager">' +
|
||||
'<div class="status-two-column">' +
|
||||
'<div class="status-section" id="tools-section"></div>' +
|
||||
'<div class="status-section" id="ccw-section"></div>' +
|
||||
'</div>' +
|
||||
'<div class="cli-panel cli-panel-full"><div id="cli-history-panel"></div></div>' +
|
||||
'</div>';
|
||||
|
||||
// Render sub-panels
|
||||
renderCliStatus();
|
||||
renderCcwInstallPanel();
|
||||
renderCliHistory();
|
||||
renderToolsSection();
|
||||
renderCcwSection();
|
||||
|
||||
// Initialize Lucide icons
|
||||
if (window.lucide) lucide.createIcons();
|
||||
}
|
||||
|
||||
// ========== Tools Section (Left Column) ==========
|
||||
function renderToolsSection() {
|
||||
var container = document.getElementById('tools-section');
|
||||
if (!container) return;
|
||||
|
||||
var toolDescriptions = {
|
||||
gemini: 'Google AI for code analysis',
|
||||
qwen: 'Alibaba AI assistant',
|
||||
codex: 'OpenAI code generation'
|
||||
};
|
||||
|
||||
var tools = ['gemini', 'qwen', 'codex'];
|
||||
var available = Object.values(cliToolStatus).filter(function(t) { return t.available; }).length;
|
||||
|
||||
var toolsHtml = tools.map(function(tool) {
|
||||
var status = cliToolStatus[tool] || {};
|
||||
var isAvailable = status.available;
|
||||
var isDefault = defaultCliTool === tool;
|
||||
|
||||
return '<div class="tool-item ' + (isAvailable ? 'available' : 'unavailable') + '">' +
|
||||
'<div class="tool-item-left">' +
|
||||
'<span class="tool-status-dot ' + (isAvailable ? 'status-available' : 'status-unavailable') + '"></span>' +
|
||||
'<div class="tool-item-info">' +
|
||||
'<div class="tool-item-name">' + tool.charAt(0).toUpperCase() + tool.slice(1) +
|
||||
(isDefault ? '<span class="tool-default-badge">Default</span>' : '') +
|
||||
'</div>' +
|
||||
'<div class="tool-item-desc">' + toolDescriptions[tool] + '</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="tool-item-right">' +
|
||||
(isAvailable
|
||||
? '<span class="tool-status-text success"><i data-lucide="check-circle" class="w-3.5 h-3.5"></i> Ready</span>'
|
||||
: '<span class="tool-status-text muted"><i data-lucide="circle-dashed" class="w-3.5 h-3.5"></i> Not Installed</span>') +
|
||||
(isAvailable && !isDefault
|
||||
? '<button class="btn-sm btn-outline" onclick="setDefaultCliTool(\'' + tool + '\')"><i data-lucide="star" class="w-3 h-3"></i> Set Default</button>'
|
||||
: '') +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}).join('');
|
||||
|
||||
// CodexLens item
|
||||
var codexLensHtml = '<div class="tool-item ' + (codexLensStatus.ready ? 'available' : 'unavailable') + '">' +
|
||||
'<div class="tool-item-left">' +
|
||||
'<span class="tool-status-dot ' + (codexLensStatus.ready ? 'status-available' : 'status-unavailable') + '"></span>' +
|
||||
'<div class="tool-item-info">' +
|
||||
'<div class="tool-item-name">CodexLens <span class="tool-type-badge">Index</span></div>' +
|
||||
'<div class="tool-item-desc">' + (codexLensStatus.ready ? 'Code indexing & FTS search' : 'Full-text code search engine') + '</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="tool-item-right">' +
|
||||
(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>' +
|
||||
'<button class="btn-sm btn-outline" onclick="initCodexLensIndex()"><i data-lucide="database" class="w-3 h-3"></i> Init Index</button>'
|
||||
: '<span class="tool-status-text muted"><i data-lucide="circle-dashed" class="w-3.5 h-3.5"></i> Not Installed</span>' +
|
||||
'<button class="btn-sm btn-primary" onclick="installCodexLens()"><i data-lucide="download" class="w-3 h-3"></i> Install</button>') +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
|
||||
// Semantic Search item (only show if CodexLens is installed)
|
||||
var semanticHtml = '';
|
||||
if (codexLensStatus.ready) {
|
||||
semanticHtml = '<div class="tool-item ' + (semanticStatus.available ? 'available' : 'unavailable') + '">' +
|
||||
'<div class="tool-item-left">' +
|
||||
'<span class="tool-status-dot ' + (semanticStatus.available ? 'status-available' : 'status-unavailable') + '"></span>' +
|
||||
'<div class="tool-item-info">' +
|
||||
'<div class="tool-item-name">Semantic Search <span class="tool-type-badge ai">AI</span></div>' +
|
||||
'<div class="tool-item-desc">' + (semanticStatus.available ? 'AI-powered code understanding' : 'Natural language code search') + '</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="tool-item-right">' +
|
||||
(semanticStatus.available
|
||||
? '<span class="tool-status-text success"><i data-lucide="sparkles" class="w-3.5 h-3.5"></i> ' + (semanticStatus.backend || 'Ready') + '</span>'
|
||||
: '<span class="tool-status-text muted"><i data-lucide="circle-dashed" class="w-3.5 h-3.5"></i> Not Installed</span>' +
|
||||
'<button class="btn-sm btn-primary" onclick="openSemanticInstallWizard()"><i data-lucide="brain" class="w-3 h-3"></i> Install</button>') +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
container.innerHTML = '<div class="section-header">' +
|
||||
'<div class="section-header-left">' +
|
||||
'<h3><i data-lucide="terminal" class="w-4 h-4"></i> CLI Tools</h3>' +
|
||||
'<span class="section-count">' + available + '/' + tools.length + ' available</span>' +
|
||||
'</div>' +
|
||||
'<button class="btn-icon" onclick="refreshAllCliStatus()" title="Refresh Status">' +
|
||||
'<i data-lucide="refresh-cw" class="w-4 h-4"></i>' +
|
||||
'</button>' +
|
||||
'</div>' +
|
||||
'<div class="tools-list">' +
|
||||
toolsHtml +
|
||||
codexLensHtml +
|
||||
semanticHtml +
|
||||
'</div>';
|
||||
|
||||
if (window.lucide) lucide.createIcons();
|
||||
}
|
||||
|
||||
// ========== CCW Section (Right Column) ==========
|
||||
function renderCcwSection() {
|
||||
var container = document.getElementById('ccw-section');
|
||||
if (!container) return;
|
||||
|
||||
var installationsHtml = '';
|
||||
|
||||
if (ccwInstallations.length === 0) {
|
||||
installationsHtml = '<div class="ccw-empty-state">' +
|
||||
'<i data-lucide="package-x" class="w-8 h-8"></i>' +
|
||||
'<p>No installations found</p>' +
|
||||
'<button class="btn btn-sm btn-primary" onclick="showCcwInstallModal()">' +
|
||||
'<i data-lucide="download" class="w-3 h-3"></i> Install CCW</button>' +
|
||||
'</div>';
|
||||
} else {
|
||||
installationsHtml = '<div class="ccw-list">';
|
||||
for (var i = 0; i < ccwInstallations.length; i++) {
|
||||
var inst = ccwInstallations[i];
|
||||
var isGlobal = inst.installation_mode === 'Global';
|
||||
var modeIcon = isGlobal ? 'home' : 'folder';
|
||||
var version = inst.application_version || 'unknown';
|
||||
var installDate = new Date(inst.installation_date).toLocaleDateString();
|
||||
|
||||
installationsHtml += '<div class="ccw-item">' +
|
||||
'<div class="ccw-item-left">' +
|
||||
'<div class="ccw-item-mode ' + (isGlobal ? 'global' : 'path') + '">' +
|
||||
'<i data-lucide="' + modeIcon + '" class="w-4 h-4"></i>' +
|
||||
'</div>' +
|
||||
'<div class="ccw-item-info">' +
|
||||
'<div class="ccw-item-header">' +
|
||||
'<span class="ccw-item-name">' + inst.installation_mode + '</span>' +
|
||||
'<span class="ccw-version-tag">v' + version + '</span>' +
|
||||
'</div>' +
|
||||
'<div class="ccw-item-path" title="' + inst.installation_path + '">' + escapeHtml(inst.installation_path) + '</div>' +
|
||||
'<div class="ccw-item-meta">' +
|
||||
'<span><i data-lucide="calendar" class="w-3 h-3"></i> ' + installDate + '</span>' +
|
||||
'<span><i data-lucide="file" class="w-3 h-3"></i> ' + (inst.files_count || 0) + ' files</span>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="ccw-item-actions">' +
|
||||
'<button class="btn-icon btn-icon-sm" onclick="runCcwUpgrade()" title="Upgrade">' +
|
||||
'<i data-lucide="arrow-up-circle" class="w-4 h-4"></i>' +
|
||||
'</button>' +
|
||||
'<button class="btn-icon btn-icon-sm btn-danger" onclick="confirmCcwUninstall(\'' + escapeHtml(inst.installation_path) + '\')" title="Uninstall">' +
|
||||
'<i data-lucide="trash-2" class="w-4 h-4"></i>' +
|
||||
'</button>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}
|
||||
installationsHtml += '</div>';
|
||||
}
|
||||
|
||||
container.innerHTML = '<div class="section-header">' +
|
||||
'<div class="section-header-left">' +
|
||||
'<h3><i data-lucide="package" class="w-4 h-4"></i> CCW Install</h3>' +
|
||||
'<span class="section-count">' + ccwInstallations.length + ' installation' + (ccwInstallations.length !== 1 ? 's' : '') + '</span>' +
|
||||
'</div>' +
|
||||
'<div class="section-header-actions">' +
|
||||
'<button class="btn-icon" onclick="showCcwInstallModal()" title="Add Installation">' +
|
||||
'<i data-lucide="plus" class="w-4 h-4"></i>' +
|
||||
'</button>' +
|
||||
'<button class="btn-icon" onclick="loadCcwInstallations().then(function() { renderCcwSection(); if (window.lucide) lucide.createIcons(); })" title="Refresh">' +
|
||||
'<i data-lucide="refresh-cw" class="w-4 h-4"></i>' +
|
||||
'</button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
installationsHtml;
|
||||
|
||||
if (window.lucide) lucide.createIcons();
|
||||
}
|
||||
|
||||
// CCW Install Carousel State
|
||||
var ccwCarouselIndex = 0;
|
||||
|
||||
|
||||
132
ccw/src/templates/dashboard-js/views/history.js
Normal file
132
ccw/src/templates/dashboard-js/views/history.js
Normal file
@@ -0,0 +1,132 @@
|
||||
// CLI History View
|
||||
// Standalone view for CLI execution history
|
||||
|
||||
// ========== Rendering ==========
|
||||
async function renderCliHistoryView() {
|
||||
var container = document.getElementById('mainContent');
|
||||
if (!container) return;
|
||||
|
||||
// Hide stats grid and search for History view
|
||||
var statsGrid = document.getElementById('statsGrid');
|
||||
var searchInput = document.getElementById('searchInput');
|
||||
if (statsGrid) statsGrid.style.display = 'none';
|
||||
if (searchInput) searchInput.parentElement.style.display = 'none';
|
||||
|
||||
// Load history data
|
||||
await loadCliHistory();
|
||||
|
||||
// Filter by search query
|
||||
var filteredHistory = cliHistorySearch
|
||||
? cliExecutionHistory.filter(function(exec) {
|
||||
return exec.prompt_preview.toLowerCase().includes(cliHistorySearch.toLowerCase()) ||
|
||||
exec.tool.toLowerCase().includes(cliHistorySearch.toLowerCase());
|
||||
})
|
||||
: cliExecutionHistory;
|
||||
|
||||
var historyHtml = '';
|
||||
|
||||
if (cliExecutionHistory.length === 0) {
|
||||
historyHtml = '<div class="history-empty-state">' +
|
||||
'<i data-lucide="terminal" class="w-12 h-12"></i>' +
|
||||
'<h3>No executions yet</h3>' +
|
||||
'<p>CLI execution history will appear here</p>' +
|
||||
'</div>';
|
||||
} else if (filteredHistory.length === 0) {
|
||||
historyHtml = '<div class="history-empty-state">' +
|
||||
'<i data-lucide="search-x" class="w-10 h-10"></i>' +
|
||||
'<h3>No matching results</h3>' +
|
||||
'<p>Try adjusting your search or filter</p>' +
|
||||
'</div>';
|
||||
} else {
|
||||
historyHtml = '<div class="history-list">';
|
||||
for (var i = 0; i < filteredHistory.length; i++) {
|
||||
var exec = filteredHistory[i];
|
||||
var statusIcon = exec.status === 'success' ? 'check-circle' :
|
||||
exec.status === 'timeout' ? 'clock' : 'x-circle';
|
||||
var statusClass = exec.status === 'success' ? 'success' :
|
||||
exec.status === 'timeout' ? 'warning' : 'error';
|
||||
var duration = formatDuration(exec.duration_ms);
|
||||
var timeAgo = getTimeAgo(new Date(exec.timestamp));
|
||||
|
||||
historyHtml += '<div class="history-item" onclick="showExecutionDetail(\'' + exec.id + '\')">' +
|
||||
'<div class="history-item-main">' +
|
||||
'<div class="history-item-header">' +
|
||||
'<span class="history-tool-tag tool-' + exec.tool + '">' + exec.tool + '</span>' +
|
||||
'<span class="history-mode-tag">' + (exec.mode || 'analysis') + '</span>' +
|
||||
'<span class="history-status ' + statusClass + '">' +
|
||||
'<i data-lucide="' + statusIcon + '" class="w-3.5 h-3.5"></i>' +
|
||||
exec.status +
|
||||
'</span>' +
|
||||
'</div>' +
|
||||
'<div class="history-item-prompt">' + escapeHtml(exec.prompt_preview) + '</div>' +
|
||||
'<div class="history-item-meta">' +
|
||||
'<span class="history-time"><i data-lucide="clock" class="w-3 h-3"></i> ' + timeAgo + '</span>' +
|
||||
'<span class="history-duration"><i data-lucide="timer" class="w-3 h-3"></i> ' + duration + '</span>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="history-item-actions">' +
|
||||
'<button class="btn-icon" onclick="event.stopPropagation(); showExecutionDetail(\'' + exec.id + '\')" title="View Details">' +
|
||||
'<i data-lucide="eye" class="w-4 h-4"></i>' +
|
||||
'</button>' +
|
||||
'<button class="btn-icon btn-danger" onclick="event.stopPropagation(); confirmDeleteExecution(\'' + exec.id + '\')" title="Delete">' +
|
||||
'<i data-lucide="trash-2" class="w-4 h-4"></i>' +
|
||||
'</button>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}
|
||||
historyHtml += '</div>';
|
||||
}
|
||||
|
||||
container.innerHTML = '<div class="history-view">' +
|
||||
'<div class="history-header">' +
|
||||
'<div class="history-header-left">' +
|
||||
'<span class="history-count">' + cliExecutionHistory.length + ' execution' + (cliExecutionHistory.length !== 1 ? 's' : '') + '</span>' +
|
||||
'</div>' +
|
||||
'<div class="history-header-right">' +
|
||||
'<div class="history-search-wrapper">' +
|
||||
'<i data-lucide="search" class="w-4 h-4"></i>' +
|
||||
'<input type="text" class="history-search-input" placeholder="Search executions..." ' +
|
||||
'value="' + escapeHtml(cliHistorySearch) + '" ' +
|
||||
'onkeyup="searchCliHistoryView(this.value)" oninput="searchCliHistoryView(this.value)">' +
|
||||
'</div>' +
|
||||
'<select class="history-filter-select" onchange="filterCliHistoryView(this.value)">' +
|
||||
'<option value=""' + (cliHistoryFilter === null ? ' selected' : '') + '>All Tools</option>' +
|
||||
'<option value="gemini"' + (cliHistoryFilter === 'gemini' ? ' selected' : '') + '>Gemini</option>' +
|
||||
'<option value="qwen"' + (cliHistoryFilter === 'qwen' ? ' selected' : '') + '>Qwen</option>' +
|
||||
'<option value="codex"' + (cliHistoryFilter === 'codex' ? ' selected' : '') + '>Codex</option>' +
|
||||
'</select>' +
|
||||
'<button class="btn-icon" onclick="refreshCliHistoryView()" title="Refresh">' +
|
||||
'<i data-lucide="refresh-cw" class="w-4 h-4"></i>' +
|
||||
'</button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
historyHtml +
|
||||
'</div>';
|
||||
|
||||
// Initialize Lucide icons
|
||||
if (window.lucide) lucide.createIcons();
|
||||
}
|
||||
|
||||
// ========== Actions ==========
|
||||
async function filterCliHistoryView(tool) {
|
||||
cliHistoryFilter = tool || null;
|
||||
await loadCliHistory();
|
||||
renderCliHistoryView();
|
||||
}
|
||||
|
||||
function searchCliHistoryView(query) {
|
||||
cliHistorySearch = query;
|
||||
renderCliHistoryView();
|
||||
// Preserve focus and cursor position
|
||||
var searchInput = document.querySelector('.history-search-input');
|
||||
if (searchInput) {
|
||||
searchInput.focus();
|
||||
searchInput.setSelectionRange(query.length, query.length);
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshCliHistoryView() {
|
||||
await loadCliHistory();
|
||||
renderCliHistoryView();
|
||||
showRefreshToast('History refreshed', 'success');
|
||||
}
|
||||
@@ -74,6 +74,22 @@ async function renderHookManager() {
|
||||
`}
|
||||
</div>
|
||||
|
||||
<!-- Hook Wizards -->
|
||||
<div class="hook-section mb-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<h3 class="text-lg font-semibold text-foreground">Hook Wizards</h3>
|
||||
<span class="badge px-2 py-0.5 text-xs font-semibold rounded-full bg-success/20 text-success">Guided Setup</span>
|
||||
</div>
|
||||
<span class="text-sm text-muted-foreground">Configure complex hooks with guided wizards</span>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
${renderWizardCard('memory-update')}
|
||||
${renderWizardCard('skill-context')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Install Templates -->
|
||||
<div class="hook-section">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
@@ -134,11 +150,112 @@ async function renderHookManager() {
|
||||
|
||||
// Attach event listeners
|
||||
attachHookEventListeners();
|
||||
|
||||
|
||||
// Initialize Lucide icons
|
||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||
}
|
||||
|
||||
// Load available SKILLs for skill-context wizard
|
||||
async function loadAvailableSkills() {
|
||||
try {
|
||||
const response = await fetch(`/api/skills?path=${encodeURIComponent(projectPath)}`);
|
||||
if (!response.ok) throw new Error('Failed to load skills');
|
||||
const data = await response.json();
|
||||
|
||||
const container = document.getElementById('skill-discovery-skill-context');
|
||||
if (container && data.skills) {
|
||||
if (data.skills.length === 0) {
|
||||
container.innerHTML = `
|
||||
<span class="font-mono bg-muted px-1.5 py-0.5 rounded">Available SKILLs:</span>
|
||||
<span class="text-muted-foreground ml-2">No SKILLs found in .claude/skills/</span>
|
||||
`;
|
||||
} else {
|
||||
const skillBadges = data.skills.map(skill => `
|
||||
<span class="px-2 py-0.5 bg-emerald-500/10 text-emerald-500 rounded" title="${escapeHtml(skill.description)}">${escapeHtml(skill.name)}</span>
|
||||
`).join('');
|
||||
container.innerHTML = `
|
||||
<span class="font-mono bg-muted px-1.5 py-0.5 rounded">Available SKILLs:</span>
|
||||
<div class="flex flex-wrap gap-1 mt-1">${skillBadges}</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
// Store skills for wizard use
|
||||
window.availableSkills = data.skills || [];
|
||||
} catch (err) {
|
||||
console.error('Failed to load skills:', err);
|
||||
const container = document.getElementById('skill-discovery-skill-context');
|
||||
if (container) {
|
||||
container.innerHTML = `
|
||||
<span class="font-mono bg-muted px-1.5 py-0.5 rounded">Available SKILLs:</span>
|
||||
<span class="text-destructive ml-2">Error loading skills</span>
|
||||
`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Call loadAvailableSkills after rendering hook manager
|
||||
const originalRenderHookManager = typeof renderHookManager === 'function' ? renderHookManager : null;
|
||||
|
||||
function renderWizardCard(wizardId) {
|
||||
const wizard = WIZARD_TEMPLATES[wizardId];
|
||||
if (!wizard) return '';
|
||||
|
||||
// Determine what to show in the tools/skills section
|
||||
const toolsSection = wizard.requiresSkillDiscovery
|
||||
? `
|
||||
<div class="flex items-center gap-2 text-xs text-muted-foreground mb-4">
|
||||
<span class="font-mono bg-muted px-1.5 py-0.5 rounded">Event:</span>
|
||||
<span class="px-2 py-0.5 bg-amber-500/10 text-amber-500 rounded">UserPromptSubmit</span>
|
||||
</div>
|
||||
<div id="skill-discovery-${wizardId}" class="text-xs text-muted-foreground mb-4">
|
||||
<span class="font-mono bg-muted px-1.5 py-0.5 rounded">Available SKILLs:</span>
|
||||
<span class="text-muted-foreground ml-2">Loading...</span>
|
||||
</div>
|
||||
`
|
||||
: `
|
||||
<div class="flex items-center gap-2 text-xs text-muted-foreground mb-4">
|
||||
<span class="font-mono bg-muted px-1.5 py-0.5 rounded">CLI Tools:</span>
|
||||
<span class="px-2 py-0.5 bg-blue-500/10 text-blue-500 rounded">gemini</span>
|
||||
<span class="px-2 py-0.5 bg-purple-500/10 text-purple-500 rounded">qwen</span>
|
||||
<span class="px-2 py-0.5 bg-green-500/10 text-green-500 rounded">codex</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
return `
|
||||
<div class="hook-wizard-card bg-gradient-to-br from-primary/5 to-primary/10 border border-primary/20 rounded-lg p-5 hover:shadow-lg transition-all">
|
||||
<div class="flex items-start justify-between mb-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="p-2.5 bg-primary/10 rounded-lg">
|
||||
<i data-lucide="${wizard.icon}" class="w-6 h-6 text-primary"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="font-semibold text-foreground">${escapeHtml(wizard.name)}</h4>
|
||||
<p class="text-sm text-muted-foreground">${escapeHtml(wizard.description)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2 mb-4">
|
||||
${wizard.options.map(opt => `
|
||||
<div class="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<i data-lucide="check" class="w-4 h-4 text-success"></i>
|
||||
<span>${escapeHtml(opt.name)}: ${escapeHtml(opt.description)}</span>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
|
||||
${toolsSection}
|
||||
|
||||
<button class="w-full px-4 py-2.5 text-sm bg-primary text-primary-foreground rounded-lg hover:opacity-90 transition-opacity flex items-center justify-center gap-2"
|
||||
onclick="openHookWizardModal('${wizardId}')">
|
||||
<i data-lucide="wand-2" class="w-4 h-4"></i>
|
||||
Open Wizard
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function countHooks(hooks) {
|
||||
let count = 0;
|
||||
for (const event of Object.keys(hooks)) {
|
||||
@@ -214,6 +331,8 @@ function renderHooksByEvent(hooks, scope) {
|
||||
|
||||
function renderQuickInstallCard(templateId, title, description, event, matcher) {
|
||||
const isInstalled = isHookTemplateInstalled(templateId);
|
||||
const template = HOOK_TEMPLATES[templateId];
|
||||
const category = template?.category || 'general';
|
||||
|
||||
return `
|
||||
<div class="hook-template-card bg-card border border-border rounded-lg p-4 hover:shadow-md transition-all ${isInstalled ? 'border-success bg-success-light/30' : ''}">
|
||||
@@ -225,6 +344,11 @@ function renderQuickInstallCard(templateId, title, description, event, matcher)
|
||||
<p class="text-xs text-muted-foreground">${escapeHtml(description)}</p>
|
||||
</div>
|
||||
</div>
|
||||
<button class="p-1.5 text-muted-foreground hover:text-foreground hover:bg-hover rounded transition-colors"
|
||||
onclick="viewTemplateDetails('${templateId}')"
|
||||
title="View template details">
|
||||
<i data-lucide="eye" class="w-4 h-4"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="hook-template-meta text-xs text-muted-foreground mb-3 flex items-center gap-3">
|
||||
@@ -234,6 +358,7 @@ function renderQuickInstallCard(templateId, title, description, event, matcher)
|
||||
<span class="flex items-center gap-1">
|
||||
Matches: <span class="font-medium">${matcher}</span>
|
||||
</span>
|
||||
<span class="px-1.5 py-0.5 bg-primary/10 text-primary rounded text-xs">${category}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
|
||||
@@ -322,6 +322,10 @@
|
||||
<span class="nav-text flex-1">Status</span>
|
||||
<span class="badge px-2 py-0.5 text-xs font-semibold rounded-full bg-hover text-muted-foreground" id="badgeCliTools">0/3</span>
|
||||
</li>
|
||||
<li class="nav-item flex items-center gap-2 mx-2 px-3 py-2.5 text-sm text-muted-foreground hover:bg-hover hover:text-foreground rounded cursor-pointer transition-colors" data-view="cli-history" data-tooltip="CLI Execution History">
|
||||
<i data-lucide="history" class="nav-icon"></i>
|
||||
<span class="nav-text flex-1">History</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -678,6 +682,38 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hook Wizard Modal -->
|
||||
<div id="hookWizardModal" class="hook-modal hidden fixed inset-0 z-[100] flex items-center justify-center">
|
||||
<div class="hook-modal-backdrop absolute inset-0 bg-black/60" onclick="closeHookWizardModal()"></div>
|
||||
<div class="hook-modal-content relative bg-card border border-border rounded-lg shadow-2xl w-[90vw] max-w-xl flex flex-col max-h-[90vh]">
|
||||
<div class="hook-modal-header flex items-center justify-between px-4 py-3 border-b border-border">
|
||||
<h3 class="text-lg font-semibold text-foreground">Hook Wizard</h3>
|
||||
<button class="w-8 h-8 flex items-center justify-center text-xl text-muted-foreground hover:text-foreground hover:bg-hover rounded" onclick="closeHookWizardModal()">×</button>
|
||||
</div>
|
||||
<div class="hook-modal-body p-4 overflow-y-auto" id="wizardModalContent">
|
||||
<!-- Dynamic wizard content -->
|
||||
</div>
|
||||
<div class="hook-modal-footer flex justify-end gap-2 px-4 py-3 border-t border-border">
|
||||
<button class="px-4 py-2 text-sm bg-muted text-foreground rounded-lg hover:bg-hover transition-colors" onclick="closeHookWizardModal()">Cancel</button>
|
||||
<button class="px-4 py-2 text-sm bg-primary text-primary-foreground rounded-lg hover:opacity-90 transition-opacity" onclick="submitHookWizard()">Install Hook</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Template View Modal -->
|
||||
<div id="templateViewModal" class="hook-modal hidden fixed inset-0 z-[100] flex items-center justify-center">
|
||||
<div class="hook-modal-backdrop absolute inset-0 bg-black/60" onclick="closeTemplateViewModal()"></div>
|
||||
<div class="hook-modal-content relative bg-card border border-border rounded-lg shadow-2xl w-[90vw] max-w-md flex flex-col max-h-[80vh]">
|
||||
<div class="hook-modal-header flex items-center justify-between px-4 py-3 border-b border-border">
|
||||
<h3 class="text-lg font-semibold text-foreground">Template Details</h3>
|
||||
<button class="w-8 h-8 flex items-center justify-center text-xl text-muted-foreground hover:text-foreground hover:bg-hover rounded" onclick="closeTemplateViewModal()">×</button>
|
||||
</div>
|
||||
<div class="hook-modal-body p-4 overflow-y-auto" id="templateViewContent">
|
||||
<!-- Dynamic template content -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Update CLAUDE.md Modal -->
|
||||
<div id="updateClaudeMdModal" class="claude-md-modal hidden fixed inset-0 z-[100] flex items-center justify-center">
|
||||
<div class="claude-md-modal-backdrop absolute inset-0 bg-black/60" onclick="closeUpdateClaudeMdModal()"></div>
|
||||
|
||||
@@ -94,6 +94,112 @@ async function checkVenvStatus() {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if semantic search dependencies are installed
|
||||
* @returns {Promise<{available: boolean, backend?: string, error?: string}>}
|
||||
*/
|
||||
async function checkSemanticStatus() {
|
||||
// First check if CodexLens is installed
|
||||
const venvStatus = await checkVenvStatus();
|
||||
if (!venvStatus.ready) {
|
||||
return { available: false, error: 'CodexLens not installed' };
|
||||
}
|
||||
|
||||
// Check semantic module availability
|
||||
return new Promise((resolve) => {
|
||||
const checkCode = `
|
||||
import sys
|
||||
try:
|
||||
from codexlens.semantic import SEMANTIC_AVAILABLE, SEMANTIC_BACKEND
|
||||
if SEMANTIC_AVAILABLE:
|
||||
print(f"available:{SEMANTIC_BACKEND}")
|
||||
else:
|
||||
print("unavailable")
|
||||
except Exception as e:
|
||||
print(f"error:{e}")
|
||||
`;
|
||||
const child = spawn(VENV_PYTHON, ['-c', checkCode], {
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
timeout: 15000
|
||||
});
|
||||
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
|
||||
child.stdout.on('data', (data) => { stdout += data.toString(); });
|
||||
child.stderr.on('data', (data) => { stderr += data.toString(); });
|
||||
|
||||
child.on('close', (code) => {
|
||||
const output = stdout.trim();
|
||||
if (output.startsWith('available:')) {
|
||||
const backend = output.split(':')[1];
|
||||
resolve({ available: true, backend });
|
||||
} else if (output === 'unavailable') {
|
||||
resolve({ available: false, error: 'Semantic dependencies not installed' });
|
||||
} else {
|
||||
resolve({ available: false, error: output || stderr || 'Unknown error' });
|
||||
}
|
||||
});
|
||||
|
||||
child.on('error', (err) => {
|
||||
resolve({ available: false, error: `Check failed: ${err.message}` });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Install semantic search dependencies
|
||||
* @returns {Promise<{success: boolean, error?: string}>}
|
||||
*/
|
||||
async function installSemantic() {
|
||||
// First ensure CodexLens is installed
|
||||
const venvStatus = await checkVenvStatus();
|
||||
if (!venvStatus.ready) {
|
||||
return { success: false, error: 'CodexLens not installed. Install CodexLens first.' };
|
||||
}
|
||||
|
||||
const pipPath = process.platform === 'win32'
|
||||
? join(CODEXLENS_VENV, 'Scripts', 'pip.exe')
|
||||
: join(CODEXLENS_VENV, 'bin', 'pip');
|
||||
|
||||
return new Promise((resolve) => {
|
||||
console.log('[CodexLens] Installing semantic search dependencies...');
|
||||
|
||||
// Install sentence-transformers and numpy
|
||||
const child = spawn(pipPath, ['install', 'numpy>=1.24', 'sentence-transformers>=2.2'], {
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
timeout: 300000 // 5 minutes for model download
|
||||
});
|
||||
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
|
||||
child.stdout.on('data', (data) => {
|
||||
stdout += data.toString();
|
||||
// Log progress
|
||||
const line = data.toString().trim();
|
||||
if (line.includes('Downloading') || line.includes('Installing')) {
|
||||
console.log(`[CodexLens] ${line}`);
|
||||
}
|
||||
});
|
||||
|
||||
child.stderr.on('data', (data) => { stderr += data.toString(); });
|
||||
|
||||
child.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
console.log('[CodexLens] Semantic dependencies installed successfully');
|
||||
resolve({ success: true });
|
||||
} else {
|
||||
resolve({ success: false, error: `Installation failed: ${stderr || stdout}` });
|
||||
}
|
||||
});
|
||||
|
||||
child.on('error', (err) => {
|
||||
resolve({ success: false, error: `Failed to run pip: ${err.message}` });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap CodexLens venv with required packages
|
||||
* @returns {Promise<{success: boolean, error?: string}>}
|
||||
@@ -471,4 +577,4 @@ Features:
|
||||
};
|
||||
|
||||
// Export for direct usage
|
||||
export { ensureReady, executeCodexLens, checkVenvStatus, bootstrapVenv };
|
||||
export { ensureReady, executeCodexLens, checkVenvStatus, bootstrapVenv, checkSemanticStatus, installSemantic };
|
||||
|
||||
Reference in New Issue
Block a user