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:
catlog22
2025-12-12 16:26:49 +08:00
parent a393601ec5
commit dfa8dbc52a
13 changed files with 2081 additions and 2161 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -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

View File

@@ -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

View File

@@ -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 {

View File

@@ -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();
}
}

View File

@@ -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 || []
});
}

View File

@@ -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';

View File

@@ -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); }

View File

@@ -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;

View 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');
}

View File

@@ -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">

View File

@@ -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()">&times;</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()">&times;</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>

View File

@@ -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 };