diff --git a/ccw/src/templates/dashboard-js/components/hook-manager.js b/ccw/src/templates/dashboard-js/components/hook-manager.js index 0e6c7f63..b14282f5 100644 --- a/ccw/src/templates/dashboard-js/components/hook-manager.js +++ b/ccw/src/templates/dashboard-js/components/hook-manager.js @@ -75,6 +75,19 @@ const HOOK_TEMPLATES = { interval: { type: 'number', default: 300, min: 60, max: 3600, label: 'Interval (seconds)', step: 60 } } }, + 'memory-update-count-based': { + event: 'PostToolUse', + matcher: 'Write|Edit', + command: 'bash', + args: ['-c', 'THRESHOLD=10; COUNT_FILE=~/.claude/.memory_update_count; INPUT=$(cat); FILE_PATH=$(echo "$INPUT" | jq -r ".tool_input.file_path // .tool_input.path // empty"); [ -z "$FILE_PATH" ] && exit 0; COUNT=0; [ -f "$COUNT_FILE" ] && COUNT=$(cat "$COUNT_FILE" 2>/dev/null || echo 0); COUNT=$((COUNT + 1)); echo $COUNT > "$COUNT_FILE"; if [ $COUNT -ge $THRESHOLD ]; then echo 0 > "$COUNT_FILE"; ccw tool exec update_module_claude \'{"strategy":"related","tool":"gemini"}\' & fi'], + description: 'Update CLAUDE.md when file changes reach threshold (default: 10 files)', + category: 'memory', + configurable: true, + config: { + tool: { type: 'select', options: ['gemini', 'qwen', 'codex'], default: 'gemini', label: 'CLI Tool' }, + threshold: { type: 'number', default: 10, min: 3, max: 50, label: 'File count threshold', step: 1 } + } + }, // SKILL Context Loader templates 'skill-context-keyword': { event: 'UserPromptSubmit', @@ -165,11 +178,18 @@ const WIZARD_TEMPLATES = { name: 'Periodic Update', description: 'Update documentation at regular intervals during session', templateId: 'memory-update-periodic' + }, + { + id: 'count-based', + name: 'Count-Based Update', + description: 'Update documentation when file changes reach threshold', + templateId: 'memory-update-count-based' } ], configFields: [ { key: 'tool', type: 'select', label: 'CLI Tool', options: ['gemini', 'qwen', 'codex'], default: 'gemini', description: 'Tool for documentation generation' }, { key: 'interval', type: 'number', label: 'Interval (seconds)', default: 300, min: 60, max: 3600, step: 60, showFor: ['periodic'], description: 'Time between updates' }, + { key: 'threshold', type: 'number', label: 'File Count Threshold', default: 10, min: 3, max: 50, step: 1, showFor: ['count-based'], description: 'Number of file changes to trigger update' }, { key: 'strategy', type: 'select', label: 'Update Strategy', options: ['related', 'single-layer'], default: 'related', description: 'Related: changed modules, Single-layer: current directory' } ] }, @@ -621,6 +641,7 @@ function renderWizardModalContent() { if (wizardId === 'memory-update') { if (optId === 'on-stop') return t('hook.wizard.onSessionEnd'); if (optId === 'periodic') return t('hook.wizard.periodicUpdate'); + if (optId === 'count-based') return t('hook.wizard.countBasedUpdate'); } if (wizardId === 'memory-setup') { if (optId === 'file-read') return t('hook.wizard.fileReadTracker'); @@ -638,6 +659,7 @@ function renderWizardModalContent() { if (wizardId === 'memory-update') { if (optId === 'on-stop') return t('hook.wizard.onSessionEndDesc'); if (optId === 'periodic') return t('hook.wizard.periodicUpdateDesc'); + if (optId === 'count-based') return t('hook.wizard.countBasedUpdateDesc'); } if (wizardId === 'memory-setup') { if (optId === 'file-read') return t('hook.wizard.fileReadTrackerDesc'); @@ -656,6 +678,7 @@ function renderWizardModalContent() { const labels = { 'tool': t('hook.wizard.cliTool'), 'interval': t('hook.wizard.intervalSeconds'), + 'threshold': t('hook.wizard.fileCountThreshold'), 'strategy': t('hook.wizard.updateStrategy') }; return labels[fieldKey] || wizard.configFields.find(f => f.key === fieldKey)?.label || fieldKey; @@ -665,6 +688,7 @@ function renderWizardModalContent() { const descs = { 'tool': t('hook.wizard.toolForDocGen'), 'interval': t('hook.wizard.timeBetweenUpdates'), + 'threshold': t('hook.wizard.fileCountThresholdDesc'), 'strategy': t('hook.wizard.relatedStrategy') }; return descs[fieldKey] || wizard.configFields.find(f => f.key === fieldKey)?.description || ''; @@ -999,12 +1023,15 @@ function generateWizardCommand() { const tool = wizardConfig.tool || 'gemini'; const strategy = wizardConfig.strategy || 'related'; const interval = wizardConfig.interval || 300; + const threshold = wizardConfig.threshold || 10; // 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 if (triggerType === 'count-based') { + return `THRESHOLD=${threshold}; COUNT_FILE=~/.claude/.memory_update_count; INPUT=$(cat); FILE_PATH=$(echo "$INPUT" | jq -r ".tool_input.file_path // .tool_input.path // empty"); [ -z "$FILE_PATH" ] && exit 0; COUNT=0; [ -f "$COUNT_FILE" ] && COUNT=$(cat "$COUNT_FILE" 2>/dev/null || echo 0); COUNT=$((COUNT + 1)); echo $COUNT > "$COUNT_FILE"; if [ $COUNT -ge $THRESHOLD ]; then echo 0 > "$COUNT_FILE"; ccw tool exec update_module_claude '${params}' & fi`; } else { return `ccw tool exec update_module_claude '${params}'`; } diff --git a/ccw/src/templates/dashboard-js/components/mcp-manager.js b/ccw/src/templates/dashboard-js/components/mcp-manager.js index af3666fa..89ef2531 100644 --- a/ccw/src/templates/dashboard-js/components/mcp-manager.js +++ b/ccw/src/templates/dashboard-js/components/mcp-manager.js @@ -930,8 +930,8 @@ function selectCcwTools(type) { // Build CCW Tools config with selected tools function buildCcwToolsConfig(selectedTools) { const config = { - command: "npx", - args: ["-y", "ccw-mcp"] + command: "cmd", + args: ["/c", "npx", "-y", "ccw-mcp"] }; // Add env if not all tools or not default 4 core tools diff --git a/ccw/src/templates/dashboard-js/i18n.js b/ccw/src/templates/dashboard-js/i18n.js index 8b52f5b6..a19dfa42 100644 --- a/ccw/src/templates/dashboard-js/i18n.js +++ b/ccw/src/templates/dashboard-js/i18n.js @@ -728,6 +728,10 @@ const i18n = { 'hook.wizard.onSessionEndDesc': 'Update documentation when Claude session ends', 'hook.wizard.periodicUpdate': 'Periodic Update', 'hook.wizard.periodicUpdateDesc': 'Update documentation at regular intervals during session', + 'hook.wizard.countBasedUpdate': 'Count-Based Update', + 'hook.wizard.countBasedUpdateDesc': 'Update documentation when file changes reach threshold', + 'hook.wizard.fileCountThreshold': 'File Count Threshold', + 'hook.wizard.fileCountThresholdDesc': 'Number of file changes to trigger update', 'hook.wizard.skillContext': 'SKILL Context Loader', 'hook.wizard.skillContextDesc': 'Automatically load SKILL packages based on keywords in user prompts', 'hook.wizard.keywordMatching': 'Keyword Matching', @@ -1977,6 +1981,10 @@ const i18n = { 'hook.wizard.onSessionEndDesc': 'Claude 会话结束时更新文档', 'hook.wizard.periodicUpdate': '定期更新', 'hook.wizard.periodicUpdateDesc': '会话期间定期更新文档', + 'hook.wizard.countBasedUpdate': '累计数量更新', + 'hook.wizard.countBasedUpdateDesc': '文件变动达到阈值时更新文档', + 'hook.wizard.fileCountThreshold': '文件数量阈值', + 'hook.wizard.fileCountThresholdDesc': '触发更新的文件变动数量', 'hook.wizard.skillContext': 'SKILL 上下文加载器', 'hook.wizard.skillContextDesc': '根据用户提示中的关键词自动加载 SKILL 包', 'hook.wizard.keywordMatching': '关键词匹配', diff --git a/ccw/src/templates/dashboard-js/utils.js b/ccw/src/templates/dashboard-js/utils.js index d37f4905..743dcd1e 100644 --- a/ccw/src/templates/dashboard-js/utils.js +++ b/ccw/src/templates/dashboard-js/utils.js @@ -20,6 +20,21 @@ function escapeHtml(str) { .replace(/'/g, '''); } +/** + * Unescape HTML entities back to original characters + * @param {string} str - String with HTML entities + * @returns {string} String with entities decoded + */ +function unescapeHtml(str) { + if (typeof str !== 'string') return str; + return str + .replace(/"/g, '"') + .replace(/'/g, "'") + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/&/g, '&'); +} + /** * Truncate text to specified maximum length * @param {string} text - Text to truncate diff --git a/ccw/src/templates/dashboard-js/views/hook-manager.js b/ccw/src/templates/dashboard-js/views/hook-manager.js index e7747afa..19293d13 100644 --- a/ccw/src/templates/dashboard-js/views/hook-manager.js +++ b/ccw/src/templates/dashboard-js/views/hook-manager.js @@ -221,6 +221,7 @@ function renderWizardCard(wizardId) { if (wizardId === 'memory-update') { if (optId === 'on-stop') return t('hook.wizard.onSessionEnd'); if (optId === 'periodic') return t('hook.wizard.periodicUpdate'); + if (optId === 'count-based') return t('hook.wizard.countBasedUpdate'); } if (wizardId === 'memory-setup') { if (optId === 'file-read') return t('hook.wizard.fileReadTracker'); @@ -238,6 +239,7 @@ function renderWizardCard(wizardId) { if (wizardId === 'memory-update') { if (optId === 'on-stop') return t('hook.wizard.onSessionEndDesc'); if (optId === 'periodic') return t('hook.wizard.periodicUpdateDesc'); + if (optId === 'count-based') return t('hook.wizard.countBasedUpdateDesc'); } if (wizardId === 'memory-setup') { if (optId === 'file-read') return t('hook.wizard.fileReadTrackerDesc'); diff --git a/ccw/src/templates/dashboard-js/views/mcp-manager.js b/ccw/src/templates/dashboard-js/views/mcp-manager.js index 3e144398..1e92cf48 100644 --- a/ccw/src/templates/dashboard-js/views/mcp-manager.js +++ b/ccw/src/templates/dashboard-js/views/mcp-manager.js @@ -863,12 +863,13 @@ function renderProjectAvailableServerCard(entry) { ` : ''} -
+
` : ''} @@ -929,10 +931,11 @@ function renderGlobalManagementCard(serverName, serverConfig) {
-
+
@@ -1337,6 +1340,11 @@ function openCodexMcpCreateModal() { } function attachMcpEventListeners() { + // Debug: Log event listener attachment + const viewDetailsCards = document.querySelectorAll('.mcp-server-card[data-action="view-details"]'); + const codexCards = document.querySelectorAll('.mcp-server-card[data-action="view-details-codex"]'); + console.log('[MCP] Attaching event listeners - Claude cards:', viewDetailsCards.length, 'Codex cards:', codexCards.length); + // Toggle switches document.querySelectorAll('.mcp-server-card input[data-action="toggle"]').forEach(input => { input.addEventListener('change', async (e) => { @@ -1553,19 +1561,41 @@ function attachMcpEventListeners() { // View details / Edit - click on Claude server card document.querySelectorAll('.mcp-server-card[data-action="view-details"]').forEach(card => { card.addEventListener('click', (e) => { - const serverName = card.dataset.serverName; - const serverConfig = JSON.parse(card.dataset.serverConfig); - const serverSource = card.dataset.serverSource; - showMcpEditModal(serverName, serverConfig, serverSource, 'claude'); + // Don't trigger if clicking on buttons or toggle + if (e.target.closest('button') || e.target.closest('label') || e.target.closest('input')) { + return; + } + try { + const serverName = card.dataset.serverName; + // Decode HTML entities before parsing JSON + const configStr = unescapeHtml(card.dataset.serverConfig); + const serverConfig = JSON.parse(configStr); + const serverSource = card.dataset.serverSource; + console.log('[MCP] Card clicked:', serverName, serverSource); + showMcpEditModal(serverName, serverConfig, serverSource, 'claude'); + } catch (err) { + console.error('[MCP] Error handling card click:', err, card.dataset.serverConfig); + } }); }); // View details / Edit - click on Codex server card document.querySelectorAll('.mcp-server-card[data-action="view-details-codex"]').forEach(card => { card.addEventListener('click', (e) => { - const serverName = card.dataset.serverName; - const serverConfig = JSON.parse(card.dataset.serverConfig); - showMcpEditModal(serverName, serverConfig, 'codex', 'codex'); + // Don't trigger if clicking on buttons or toggle + if (e.target.closest('button') || e.target.closest('label') || e.target.closest('input')) { + return; + } + try { + const serverName = card.dataset.serverName; + // Decode HTML entities before parsing JSON + const configStr = unescapeHtml(card.dataset.serverConfig); + const serverConfig = JSON.parse(configStr); + console.log('[MCP] Codex card clicked:', serverName); + showMcpEditModal(serverName, serverConfig, 'codex', 'codex'); + } catch (err) { + console.error('[MCP] Error handling Codex card click:', err, card.dataset.serverConfig); + } }); });