From 766a8d214522c54b8267d970dfa010c01f5863f6 Mon Sep 17 00:00:00 2001 From: catlog22 Date: Thu, 11 Dec 2025 21:18:28 +0800 Subject: [PATCH] feat: Enhance global notifications with localStorage persistence and clear functionality feat: Implement generic modal functions for better UI consistency feat: Update navigation titles for CLI Tools view feat: Add JSON formatting for notification details in CLI execution feat: Introduce localStorage handling for global notification queue feat: Expand CLI Manager view to include CCW installations with carousel feat: Add CCW installation management with modal for user interaction fix: Improve event delegation for explorer tree item interactions refactor: Clean up CLI Tools section in dashboard HTML feat: Add functionality to delete CLI execution history by ID --- ccw/src/core/server.js | 30 +- .../templates/dashboard-css/09-explorer.css | 17 +- ccw/src/templates/dashboard-css/10-cli.css | 674 ++++++++++++++++-- .../dashboard-js/components/cli-history.js | 170 ++++- .../components/global-notifications.js | 25 +- .../dashboard-js/components/modals.js | 50 ++ .../dashboard-js/components/navigation.js | 2 + .../dashboard-js/components/notifications.js | 44 +- ccw/src/templates/dashboard-js/state.js | 40 +- .../dashboard-js/views/cli-manager.js | 549 +++++++++----- .../templates/dashboard-js/views/explorer.js | 64 +- ccw/src/templates/dashboard.html | 19 +- ccw/src/tools/cli-executor.js | 40 +- 13 files changed, 1404 insertions(+), 320 deletions(-) diff --git a/ccw/src/core/server.js b/ccw/src/core/server.js index 59af670b..1e9b801c 100644 --- a/ccw/src/core/server.js +++ b/ccw/src/core/server.js @@ -7,7 +7,8 @@ import { createHash } from 'crypto'; import { scanSessions } from './session-scanner.js'; import { aggregateData } from './data-aggregator.js'; import { resolvePath, getRecentPaths, trackRecentPath, removeRecentPath, normalizePathForDisplay, getWorkflowDir } from '../utils/path-resolver.js'; -import { getCliToolsStatus, getExecutionHistory, getExecutionDetail, executeCliTool } from '../tools/cli-executor.js'; +import { getCliToolsStatus, getExecutionHistory, getExecutionDetail, deleteExecution, executeCliTool } from '../tools/cli-executor.js'; +import { getAllManifests } from './manifest.js'; // Claude config file paths const CLAUDE_CONFIG_PATH = join(homedir(), '.claude.json'); @@ -47,7 +48,8 @@ const MODULE_CSS_FILES = [ '06-cards.css', '07-managers.css', '08-review.css', - '09-explorer.css' + '09-explorer.css', + '10-cli.css' ]; /** @@ -449,6 +451,14 @@ export async function startServer(options = {}) { return; } + // API: CCW Installation Status + if (pathname === '/api/ccw/installations') { + const manifests = getAllManifests(); + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ installations: manifests })); + return; + } + // API: CLI Execution History if (pathname === '/api/cli/history') { const projectPath = url.searchParams.get('path') || initialPath; @@ -462,7 +472,7 @@ export async function startServer(options = {}) { return; } - // API: CLI Execution Detail + // API: CLI Execution Detail (GET) or Delete (DELETE) if (pathname === '/api/cli/execution') { const projectPath = url.searchParams.get('path') || initialPath; const executionId = url.searchParams.get('id'); @@ -473,6 +483,20 @@ export async function startServer(options = {}) { return; } + // Handle DELETE request + if (req.method === 'DELETE') { + const result = deleteExecution(projectPath, executionId); + if (result.success) { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: true, message: 'Execution deleted' })); + } else { + res.writeHead(500, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: result.error || 'Delete failed' })); + } + return; + } + + // Handle GET request const detail = getExecutionDetail(projectPath, executionId); if (!detail) { res.writeHead(404, { 'Content-Type': 'application/json' }); diff --git a/ccw/src/templates/dashboard-css/09-explorer.css b/ccw/src/templates/dashboard-css/09-explorer.css index a87222de..0ed00693 100644 --- a/ccw/src/templates/dashboard-css/09-explorer.css +++ b/ccw/src/templates/dashboard-css/09-explorer.css @@ -1319,11 +1319,20 @@ } .notif-details { - font-size: 12px; + font-size: 11px; + font-family: 'SF Mono', 'Consolas', 'Monaco', monospace; color: hsl(var(--muted-foreground)); - margin-top: 6px; - padding-left: 22px; - line-height: 1.4; + margin-top: 8px; + margin-left: 22px; + padding: 8px 10px; + line-height: 1.5; + background: hsl(var(--muted) / 0.5); + border-radius: 6px; + border-left: 2px solid hsl(var(--border)); + white-space: pre-wrap; + word-break: break-all; + max-height: 100px; + overflow-y: auto; } .notif-meta { diff --git a/ccw/src/templates/dashboard-css/10-cli.css b/ccw/src/templates/dashboard-css/10-cli.css index 4d05695b..2cfaad0c 100644 --- a/ccw/src/templates/dashboard-css/10-cli.css +++ b/ccw/src/templates/dashboard-css/10-cli.css @@ -1,18 +1,20 @@ /* ======================================== * CLI Manager Styles + * Unified font: system-ui for UI, monospace for code * ======================================== */ /* Container */ .cli-manager-container { display: flex; flex-direction: column; - gap: 1.5rem; + gap: 1.25rem; + font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; } .cli-manager-grid { display: grid; grid-template-columns: 1fr 1fr; - gap: 1.5rem; + gap: 1.25rem; } @media (max-width: 768px) { @@ -25,8 +27,14 @@ .cli-panel { background: hsl(var(--card)); border: 1px solid hsl(var(--border)); - border-radius: 0.5rem; + border-radius: 0.75rem; overflow: hidden; + box-shadow: 0 1px 3px hsl(var(--foreground) / 0.04); + transition: box-shadow 0.2s ease; +} + +.cli-panel:hover { + box-shadow: 0 4px 12px hsl(var(--foreground) / 0.08); } .cli-panel-full { @@ -38,38 +46,48 @@ display: flex; align-items: center; justify-content: space-between; - padding: 1rem; + padding: 0.875rem 1rem; border-bottom: 1px solid hsl(var(--border)); + background: hsl(var(--muted) / 0.3); } .cli-status-header h3 { - font-size: 0.875rem; + font-size: 0.8125rem; font-weight: 600; color: hsl(var(--foreground)); margin: 0; + letter-spacing: -0.01em; } .cli-tools-grid { display: grid; grid-template-columns: repeat(3, 1fr); - gap: 0.75rem; - padding: 1rem; + gap: 0.625rem; + padding: 0.875rem; } .cli-tool-card { - padding: 0.75rem; - border-radius: 0.375rem; - background: hsl(var(--muted)); + padding: 0.875rem 0.75rem; + border-radius: 0.5rem; + background: hsl(var(--background)); text-align: center; + border: 1px solid hsl(var(--border)); + transition: all 0.2s ease; } .cli-tool-card.available { - border: 1px solid hsl(var(--success) / 0.3); + border-color: hsl(var(--success) / 0.4); + background: hsl(var(--success) / 0.03); +} + +.cli-tool-card.available:hover { + border-color: hsl(var(--success) / 0.6); + background: hsl(var(--success) / 0.06); } .cli-tool-card.unavailable { - border: 1px solid hsl(var(--border)); - opacity: 0.7; + border-color: hsl(var(--border)); + opacity: 0.6; } .cli-tool-header { @@ -77,40 +95,52 @@ align-items: center; justify-content: center; gap: 0.5rem; - margin-bottom: 0.25rem; + margin-bottom: 0.375rem; } .cli-tool-status { width: 8px; height: 8px; border-radius: 50%; + flex-shrink: 0; } .cli-tool-status.status-available { background: hsl(var(--success)); + box-shadow: 0 0 6px hsl(var(--success) / 0.5); } .cli-tool-status.status-unavailable { - background: hsl(var(--muted-foreground)); + background: hsl(var(--muted-foreground) / 0.5); } .cli-tool-name { font-weight: 600; - font-size: 0.875rem; + font-size: 0.8125rem; color: hsl(var(--foreground)); + letter-spacing: -0.01em; } .cli-tool-badge { - font-size: 0.625rem; + 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; } .cli-tool-info { - font-size: 0.75rem; + font-size: 0.6875rem; margin-bottom: 0.5rem; + color: hsl(var(--muted-foreground)); +} + +.cli-tool-info .text-success { + color: hsl(var(--success)); + font-weight: 500; } /* Execute Panel */ @@ -186,15 +216,17 @@ display: flex; align-items: center; justify-content: space-between; - padding: 1rem; + padding: 0.875rem 1rem; border-bottom: 1px solid hsl(var(--border)); + background: hsl(var(--muted) / 0.3); } .cli-history-header h3 { - font-size: 0.875rem; + font-size: 0.8125rem; font-weight: 600; color: hsl(var(--foreground)); margin: 0; + letter-spacing: -0.01em; } .cli-history-controls { @@ -203,82 +235,160 @@ gap: 0.5rem; } -.cli-tool-filter { - padding: 0.375rem 0.5rem; +/* Search Input for History */ +.cli-history-search { + padding: 0.375rem 0.625rem; border: 1px solid hsl(var(--border)); border-radius: 0.375rem; background: hsl(var(--background)); color: hsl(var(--foreground)); font-size: 0.75rem; + width: 160px; + transition: all 0.2s ease; +} + +.cli-history-search:focus { + outline: none; + border-color: hsl(var(--primary)); + box-shadow: 0 0 0 2px hsl(var(--primary) / 0.15); + width: 200px; +} + +.cli-history-search::placeholder { + color: hsl(var(--muted-foreground) / 0.7); +} + +.cli-tool-filter { + padding: 0.375rem 0.625rem; + border: 1px solid hsl(var(--border)); + border-radius: 0.375rem; + background: hsl(var(--background)); + color: hsl(var(--foreground)); + font-size: 0.75rem; + cursor: pointer; + transition: all 0.2s ease; +} + +.cli-tool-filter:hover { + border-color: hsl(var(--primary) / 0.5); +} + +.cli-tool-filter:focus { + outline: none; + border-color: hsl(var(--primary)); } .cli-history-list { - max-height: 400px; + max-height: 450px; overflow-y: auto; } .cli-history-item { + display: flex; + align-items: flex-start; + justify-content: space-between; padding: 0.75rem 1rem; - border-bottom: 1px solid hsl(var(--border)); + border-bottom: 1px solid hsl(var(--border) / 0.5); cursor: pointer; - transition: background 0.15s ease; + transition: all 0.15s ease; } .cli-history-item:hover { background: hsl(var(--hover)); } +.cli-history-item:hover .cli-history-actions { + opacity: 1; +} + .cli-history-item:last-child { border-bottom: none; } +.cli-history-item-content { + flex: 1; + min-width: 0; +} + .cli-history-item-header { display: flex; align-items: center; gap: 0.5rem; - margin-bottom: 0.25rem; + margin-bottom: 0.375rem; } .cli-tool-tag { - font-size: 0.625rem; + font-size: 0.5625rem; font-weight: 600; - padding: 0.125rem 0.375rem; - border-radius: 0.25rem; + padding: 0.125rem 0.5rem; + border-radius: 9999px; text-transform: uppercase; + letter-spacing: 0.04em; } .cli-tool-gemini { - background: hsl(210 80% 55% / 0.15); - color: hsl(210 80% 50%); + background: hsl(210 80% 55% / 0.12); + color: hsl(210 80% 45%); } .cli-tool-qwen { - background: hsl(280 70% 55% / 0.15); - color: hsl(280 70% 50%); + background: hsl(280 70% 55% / 0.12); + color: hsl(280 70% 45%); } .cli-tool-codex { - background: hsl(142 71% 45% / 0.15); - color: hsl(142 71% 40%); + background: hsl(142 71% 45% / 0.12); + color: hsl(142 71% 35%); } .cli-history-time { - font-size: 0.75rem; + font-size: 0.6875rem; color: hsl(var(--muted-foreground)); } .cli-history-prompt { font-size: 0.8125rem; + font-weight: 450; color: hsl(var(--foreground)); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; + line-height: 1.4; } .cli-history-meta { + display: flex; + align-items: center; + gap: 0.75rem; font-size: 0.6875rem; margin-top: 0.25rem; + color: hsl(var(--muted-foreground)); +} + +/* History Item Actions */ +.cli-history-actions { + display: flex; + align-items: center; + gap: 0.25rem; + opacity: 0; + transition: opacity 0.15s ease; + margin-left: 0.5rem; +} + +.cli-history-actions .btn-icon { + padding: 0.25rem; + color: hsl(var(--muted-foreground)); +} + +.cli-history-actions .btn-icon:hover { + color: hsl(var(--foreground)); + background: hsl(var(--hover)); +} + +.cli-history-actions .btn-icon.btn-danger:hover { + color: hsl(var(--destructive)); + background: hsl(var(--destructive) / 0.1); } /* Output Panel */ @@ -342,92 +452,134 @@ /* Detail Modal */ .cli-detail-header { - margin-bottom: 1rem; + margin-bottom: 1.25rem; + padding-bottom: 1rem; + border-bottom: 1px solid hsl(var(--border)); } .cli-detail-info { display: flex; align-items: center; gap: 0.75rem; - margin-bottom: 0.5rem; + margin-bottom: 0.625rem; + flex-wrap: wrap; } .cli-detail-status { - font-size: 0.75rem; - font-weight: 500; - padding: 0.125rem 0.5rem; + font-size: 0.6875rem; + font-weight: 600; + padding: 0.25rem 0.625rem; border-radius: 9999px; + text-transform: uppercase; + letter-spacing: 0.03em; } .cli-detail-status.status-success { - background: hsl(var(--success-light)); + background: hsl(var(--success) / 0.12); color: hsl(var(--success)); } .cli-detail-status.status-error { - background: hsl(var(--destructive) / 0.1); + background: hsl(var(--destructive) / 0.12); color: hsl(var(--destructive)); } .cli-detail-status.status-timeout { - background: hsl(var(--warning-light)); + background: hsl(var(--warning) / 0.12); color: hsl(var(--warning)); } .cli-detail-meta { display: flex; - gap: 1rem; + gap: 1.25rem; font-size: 0.75rem; + color: hsl(var(--muted-foreground)); + flex-wrap: wrap; +} + +.cli-detail-meta span { + display: flex; + align-items: center; + gap: 0.375rem; } .cli-detail-section { - margin-bottom: 1rem; + margin-bottom: 1.25rem; } .cli-detail-section h4 { - font-size: 0.8125rem; + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 0.75rem; font-weight: 600; color: hsl(var(--foreground)); - margin-bottom: 0.5rem; + margin-bottom: 0.625rem; + text-transform: uppercase; + letter-spacing: 0.03em; +} + +.cli-detail-section h4 i { + width: 14px; + height: 14px; + color: hsl(var(--muted-foreground)); } .cli-detail-prompt { - padding: 0.75rem; - background: hsl(var(--muted)); - border-radius: 0.375rem; - font-family: monospace; + padding: 1rem; + background: hsl(var(--muted) / 0.5); + border: 1px solid hsl(var(--border)); + border-radius: 0.5rem; + font-family: 'SF Mono', 'Consolas', 'Liberation Mono', monospace; font-size: 0.75rem; + line-height: 1.6; white-space: pre-wrap; word-wrap: break-word; max-height: 200px; overflow-y: auto; + color: hsl(var(--foreground)); } .cli-detail-output { - padding: 0.75rem; - background: hsl(var(--muted)); - border-radius: 0.375rem; - font-family: monospace; + padding: 1rem; + background: hsl(var(--muted) / 0.5); + border: 1px solid hsl(var(--border)); + border-radius: 0.5rem; + font-family: 'SF Mono', 'Consolas', 'Liberation Mono', monospace; font-size: 0.75rem; + line-height: 1.6; white-space: pre-wrap; word-wrap: break-word; - max-height: 300px; + max-height: 350px; overflow-y: auto; + color: hsl(var(--foreground)); } .cli-detail-error { - padding: 0.75rem; - background: hsl(var(--destructive) / 0.1); - border-radius: 0.375rem; - font-family: monospace; + padding: 1rem; + background: hsl(var(--destructive) / 0.08); + border: 1px solid hsl(var(--destructive) / 0.2); + border-radius: 0.5rem; + font-family: 'SF Mono', 'Consolas', 'Liberation Mono', monospace; font-size: 0.75rem; + line-height: 1.6; color: hsl(var(--destructive)); white-space: pre-wrap; word-wrap: break-word; - max-height: 150px; + max-height: 180px; overflow-y: auto; } +/* Detail Actions */ +.cli-detail-actions { + display: flex; + justify-content: flex-end; + gap: 0.5rem; + margin-top: 1.5rem; + padding-top: 1rem; + border-top: 1px solid hsl(var(--border)); +} + /* Button Styles */ .btn { display: inline-flex; @@ -487,6 +639,16 @@ background: hsl(var(--hover)); } +.btn-outline.btn-danger { + border-color: hsl(var(--destructive) / 0.3); + color: hsl(var(--destructive)); +} + +.btn-outline.btn-danger:hover { + background: hsl(var(--destructive) / 0.1); + border-color: hsl(var(--destructive) / 0.5); +} + /* Empty State */ .empty-state { display: flex; @@ -507,3 +669,391 @@ .empty-state p { font-size: 0.875rem; } + +/* ======================================== + * CCW Installation Panel Styles + * ======================================== */ + +/* CCW Header Actions */ +.ccw-header-actions { + display: flex; + align-items: center; + gap: 0.25rem; +} + +/* CCW Install Content */ +.ccw-install-content { + padding: 0.875rem; +} + +/* CCW Empty State */ +.ccw-empty-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 2rem 1rem; + text-align: center; + color: hsl(var(--muted-foreground)); +} + +.ccw-empty-state i { + opacity: 0.4; + margin-bottom: 0.75rem; +} + +.ccw-empty-state p { + font-size: 0.8125rem; + margin-bottom: 1rem; +} + +/* ======================================== + * CCW Carousel Styles + * ======================================== */ +.ccw-carousel-wrapper { + position: relative; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.ccw-carousel-track { + display: flex; + flex: 1; + overflow: hidden; + transition: transform 0.3s ease; +} + +.ccw-carousel-card { + flex: 0 0 100%; + min-width: 0; + padding: 1rem; + background: hsl(var(--background)); + border: 1px solid hsl(var(--border)); + border-radius: 0.5rem; + transition: all 0.2s ease; +} + +.ccw-carousel-card.active { + border-color: hsl(var(--primary) / 0.4); + box-shadow: 0 2px 8px hsl(var(--primary) / 0.1); +} + +/* Carousel Card Header */ +.ccw-card-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 0.75rem; +} + +.ccw-card-mode { + display: flex; + align-items: center; + gap: 0.5rem; + font-weight: 600; + font-size: 0.875rem; + color: hsl(var(--foreground)); +} + +.ccw-card-mode.global { + color: hsl(var(--primary)); +} + +.ccw-card-mode.path { + color: hsl(var(--warning)); +} + +.ccw-version-tag { + font-size: 0.625rem; + font-weight: 600; + padding: 0.25rem 0.5rem; + background: hsl(var(--primary) / 0.1); + color: hsl(var(--primary)); + border-radius: 9999px; + letter-spacing: 0.02em; +} + +/* Carousel Card Path */ +.ccw-card-path { + font-size: 0.75rem; + color: hsl(var(--muted-foreground)); + font-family: 'SF Mono', 'Consolas', 'Liberation Mono', monospace; + background: hsl(var(--muted) / 0.5); + padding: 0.5rem 0.625rem; + border-radius: 0.375rem; + margin-bottom: 0.75rem; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +/* Carousel Card Meta */ +.ccw-card-meta { + display: flex; + gap: 1rem; + font-size: 0.6875rem; + color: hsl(var(--muted-foreground)); + margin-bottom: 0.75rem; +} + +.ccw-card-meta span { + display: flex; + align-items: center; + gap: 0.25rem; +} + +/* Carousel Card Actions */ +.ccw-card-actions { + display: flex; + justify-content: flex-end; + gap: 0.375rem; + padding-top: 0.75rem; + border-top: 1px solid hsl(var(--border)); +} + +/* Carousel Navigation Buttons */ +.ccw-carousel-btn { + display: flex; + align-items: center; + justify-content: center; + width: 2rem; + height: 2rem; + flex-shrink: 0; + background: hsl(var(--background)); + border: 1px solid hsl(var(--border)); + border-radius: 50%; + color: hsl(var(--muted-foreground)); + cursor: pointer; + transition: all 0.15s ease; +} + +.ccw-carousel-btn:hover { + background: hsl(var(--hover)); + color: hsl(var(--foreground)); + border-color: hsl(var(--primary) / 0.3); +} + +.ccw-carousel-btn:disabled { + opacity: 0.3; + cursor: not-allowed; +} + +/* Carousel Dots */ +.ccw-carousel-dots { + display: flex; + justify-content: center; + gap: 0.5rem; + margin-top: 0.75rem; +} + +.ccw-carousel-dot { + width: 8px; + height: 8px; + border-radius: 50%; + background: hsl(var(--muted-foreground) / 0.3); + border: none; + cursor: pointer; + transition: all 0.2s ease; +} + +.ccw-carousel-dot:hover { + background: hsl(var(--muted-foreground) / 0.5); +} + +.ccw-carousel-dot.active { + background: hsl(var(--primary)); + width: 20px; + border-radius: 4px; +} + +/* ======================================== + * CCW Install Modal Styles + * ======================================== */ +.ccw-install-modal { + padding: 0.5rem 0; +} + +.ccw-install-options { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.ccw-install-option { + display: flex; + align-items: center; + gap: 1rem; + padding: 1rem; + background: hsl(var(--muted) / 0.3); + border: 1px solid hsl(var(--border)); + border-radius: 0.5rem; + cursor: pointer; + transition: all 0.15s ease; +} + +.ccw-install-option:hover { + background: hsl(var(--hover)); + border-color: hsl(var(--primary) / 0.3); +} + +.ccw-option-icon { + display: flex; + align-items: center; + justify-content: center; + width: 3rem; + height: 3rem; + border-radius: 0.5rem; + flex-shrink: 0; +} + +.ccw-option-icon.global { + background: hsl(var(--primary) / 0.1); + color: hsl(var(--primary)); +} + +.ccw-option-icon.path { + background: hsl(var(--warning) / 0.1); + color: hsl(var(--warning)); +} + +.ccw-option-info { + flex: 1; +} + +.ccw-option-title { + font-weight: 600; + font-size: 0.875rem; + color: hsl(var(--foreground)); + margin-bottom: 0.25rem; +} + +.ccw-option-desc { + font-size: 0.75rem; + color: hsl(var(--muted-foreground)); +} + +/* Path Input Section */ +.ccw-path-input-section { + margin-top: 1rem; + padding-top: 1rem; + border-top: 1px solid hsl(var(--border)); +} + +.ccw-path-input-group { + margin-bottom: 1rem; +} + +.ccw-path-input-group label { + display: block; + font-size: 0.75rem; + font-weight: 500; + color: hsl(var(--muted-foreground)); + margin-bottom: 0.5rem; +} + +.ccw-path-input-group .cli-textarea { + width: 100%; + min-height: auto; + padding: 0.625rem 0.75rem; + font-size: 0.8125rem; +} + +.ccw-install-action { + display: flex; + justify-content: flex-end; +} + +/* Danger Button */ +.btn-icon.btn-danger { + color: hsl(var(--destructive)); +} + +.btn-icon.btn-danger:hover { + background: hsl(var(--destructive) / 0.1); +} + +/* ======================================== + * Generic Modal Styles + * ======================================== */ +.generic-modal-overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.5); + backdrop-filter: blur(2px); + display: flex; + align-items: center; + justify-content: center; + z-index: 100; + opacity: 0; + transition: opacity 0.2s ease; +} + +.generic-modal-overlay.active { + opacity: 1; +} + +.generic-modal { + background: hsl(var(--card)); + border: 1px solid hsl(var(--border)); + border-radius: 0.75rem; + width: 90%; + max-width: 600px; + max-height: 85vh; + display: flex; + flex-direction: column; + box-shadow: 0 8px 32px rgb(0 0 0 / 0.25); + transform: scale(0.95); + transition: transform 0.2s ease; +} + +.generic-modal-overlay.active .generic-modal { + transform: scale(1); +} + +.generic-modal.large { + max-width: 800px; +} + +.generic-modal-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 1rem 1.25rem; + border-bottom: 1px solid hsl(var(--border)); + background: hsl(var(--muted) / 0.3); + border-radius: 0.75rem 0.75rem 0 0; +} + +.generic-modal-title { + font-size: 0.9375rem; + font-weight: 600; + color: hsl(var(--foreground)); + margin: 0; +} + +.generic-modal-close { + width: 2rem; + height: 2rem; + display: flex; + align-items: center; + justify-content: center; + background: none; + border: none; + font-size: 1.25rem; + color: hsl(var(--muted-foreground)); + cursor: pointer; + border-radius: 0.375rem; + transition: all 0.15s ease; +} + +.generic-modal-close:hover { + background: hsl(var(--hover)); + color: hsl(var(--foreground)); +} + +.generic-modal-body { + flex: 1; + overflow-y: auto; + padding: 1.25rem; +} diff --git a/ccw/src/templates/dashboard-js/components/cli-history.js b/ccw/src/templates/dashboard-js/components/cli-history.js index e96390e2..f16d0b41 100644 --- a/ccw/src/templates/dashboard-js/components/cli-history.js +++ b/ccw/src/templates/dashboard-js/components/cli-history.js @@ -1,9 +1,10 @@ // CLI History Component -// Displays execution history with filtering and search +// Displays execution history with filtering, search, and delete // ========== CLI History State ========== let cliExecutionHistory = []; let cliHistoryFilter = null; // Filter by tool +let cliHistorySearch = ''; // Search query let cliHistoryLimit = 50; // ========== Data Loading ========== @@ -44,19 +45,28 @@ function renderCliHistory() { const container = document.getElementById('cli-history-panel'); if (!container) return; + // Filter by search query + const filteredHistory = cliHistorySearch + ? cliExecutionHistory.filter(exec => + exec.prompt_preview.toLowerCase().includes(cliHistorySearch.toLowerCase()) || + exec.tool.toLowerCase().includes(cliHistorySearch.toLowerCase()) + ) + : cliExecutionHistory; + if (cliExecutionHistory.length === 0) { container.innerHTML = `

Execution History

+ ${renderHistorySearch()} ${renderToolFilter()}
- +

No executions yet

`; @@ -65,36 +75,53 @@ function renderCliHistory() { return; } - const historyHtml = cliExecutionHistory.map(exec => { - const statusIcon = exec.status === 'success' ? 'check-circle' : - exec.status === 'timeout' ? 'clock' : 'x-circle'; - const statusClass = exec.status === 'success' ? 'text-success' : - exec.status === 'timeout' ? 'text-warning' : 'text-destructive'; - const duration = formatDuration(exec.duration_ms); - const timeAgo = getTimeAgo(new Date(exec.timestamp)); + const historyHtml = filteredHistory.length === 0 + ? `
+ +

No matching results

+
` + : filteredHistory.map(exec => { + const statusIcon = exec.status === 'success' ? 'check-circle' : + exec.status === 'timeout' ? 'clock' : 'x-circle'; + const statusClass = exec.status === 'success' ? 'text-success' : + exec.status === 'timeout' ? 'text-warning' : 'text-destructive'; + const duration = formatDuration(exec.duration_ms); + const timeAgo = getTimeAgo(new Date(exec.timestamp)); - return ` -
-
- ${exec.tool} - ${timeAgo} - -
-
${escapeHtml(exec.prompt_preview)}
-
- ${duration} -
-
- `; - }).join(''); + return ` +
+
+
+ ${exec.tool} + ${timeAgo} + +
+
${escapeHtml(exec.prompt_preview)}
+
+ ${duration} + ${exec.mode || 'analysis'} +
+
+
+ + +
+
+ `; + }).join(''); container.innerHTML = `

Execution History

+ ${renderHistorySearch()} ${renderToolFilter()}
@@ -106,6 +133,17 @@ function renderCliHistory() { if (window.lucide) lucide.createIcons(); } +function renderHistorySearch() { + return ` + + `; +} + function renderToolFilter() { const tools = ['all', 'gemini', 'qwen', 'codex']; return ` @@ -135,30 +173,41 @@ async function showExecutionDetail(executionId) { ${formatDuration(detail.duration_ms)}
- Model: ${detail.model || 'default'} - Mode: ${detail.mode} - ${new Date(detail.timestamp).toLocaleString()} + ${detail.model || 'default'} + ${detail.mode} + ${new Date(detail.timestamp).toLocaleString()}
-

Prompt

+

Prompt

${escapeHtml(detail.prompt)}
${detail.output.stdout ? `
-

Output

+

Output

${escapeHtml(detail.output.stdout)}
` : ''} ${detail.output.stderr ? `
-

Errors

+

Errors

${escapeHtml(detail.output.stderr)}
` : ''} ${detail.output.truncated ? ` -

Output was truncated due to size.

+

+ + Output was truncated due to size. +

` : ''} +
+ + +
`; showModal('Execution Detail', modalContent); @@ -171,12 +220,69 @@ async function filterCliHistory(tool) { renderCliHistory(); } +function searchCliHistory(query) { + cliHistorySearch = query; + renderCliHistory(); + // Preserve focus and cursor position + const searchInput = document.querySelector('.cli-history-search'); + if (searchInput) { + searchInput.focus(); + searchInput.setSelectionRange(query.length, query.length); + } +} + async function refreshCliHistory() { await loadCliHistory(); renderCliHistory(); showRefreshToast('History refreshed', 'success'); } +// ========== Delete Execution ========== +function confirmDeleteExecution(executionId) { + if (confirm('Delete this execution record? This action cannot be undone.')) { + deleteExecution(executionId); + } +} + +async function deleteExecution(executionId) { + try { + const response = await fetch(`/api/cli/execution?path=${encodeURIComponent(projectPath)}&id=${encodeURIComponent(executionId)}`, { + method: 'DELETE' + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || 'Failed to delete'); + } + + // Remove from local state + cliExecutionHistory = cliExecutionHistory.filter(exec => exec.id !== executionId); + renderCliHistory(); + showRefreshToast('Execution deleted', 'success'); + } catch (err) { + console.error('Failed to delete execution:', err); + showRefreshToast('Delete failed: ' + err.message, 'error'); + } +} + +// ========== Copy Prompt ========== +async function copyExecutionPrompt(executionId) { + const detail = await loadExecutionDetail(executionId); + if (!detail) { + showRefreshToast('Execution not found', 'error'); + return; + } + + if (navigator.clipboard) { + try { + await navigator.clipboard.writeText(detail.prompt); + showRefreshToast('Prompt copied to clipboard', 'success'); + } catch (err) { + showRefreshToast('Failed to copy', 'error'); + } + } +} + // ========== Helpers ========== function formatDuration(ms) { if (ms >= 60000) { diff --git a/ccw/src/templates/dashboard-js/components/global-notifications.js b/ccw/src/templates/dashboard-js/components/global-notifications.js index 2fbdb5a8..03c8b057 100644 --- a/ccw/src/templates/dashboard-js/components/global-notifications.js +++ b/ccw/src/templates/dashboard-js/components/global-notifications.js @@ -79,17 +79,22 @@ function addGlobalNotification(type, message, details = null, source = null) { timestamp: new Date().toISOString(), read: false }; - + globalNotificationQueue.unshift(notification); - + // Keep only last 100 notifications if (globalNotificationQueue.length > 100) { globalNotificationQueue = globalNotificationQueue.slice(0, 100); } - + + // Persist to localStorage + if (typeof saveNotificationsToStorage === 'function') { + saveNotificationsToStorage(); + } + renderGlobalNotifications(); updateGlobalNotifBadge(); - + // Show toast for important notifications if (type === 'error' || type === 'success') { showNotificationToast(notification); @@ -204,6 +209,12 @@ function updateGlobalNotifBadge() { */ function clearGlobalNotifications() { globalNotificationQueue = []; + + // Clear from localStorage + if (typeof saveNotificationsToStorage === 'function') { + saveNotificationsToStorage(); + } + renderGlobalNotifications(); updateGlobalNotifBadge(); } @@ -213,6 +224,12 @@ function clearGlobalNotifications() { */ function markAllNotificationsRead() { globalNotificationQueue.forEach(n => n.read = true); + + // Save to localStorage + if (typeof saveNotificationsToStorage === 'function') { + saveNotificationsToStorage(); + } + renderGlobalNotifications(); updateGlobalNotifBadge(); } diff --git a/ccw/src/templates/dashboard-js/components/modals.js b/ccw/src/templates/dashboard-js/components/modals.js index c67e63a7..2238189e 100644 --- a/ccw/src/templates/dashboard-js/components/modals.js +++ b/ccw/src/templates/dashboard-js/components/modals.js @@ -2,6 +2,56 @@ // MODAL DIALOGS // ========================================== +// Generic Modal Functions +function showModal(title, content, options = {}) { + // Remove existing modal if any + closeModal(); + + const overlay = document.createElement('div'); + overlay.className = 'generic-modal-overlay'; + overlay.innerHTML = ` +
+
+

${escapeHtml(title)}

+ +
+
+ ${content} +
+
+ `; + + document.body.appendChild(overlay); + + // Trigger animation + requestAnimationFrame(() => overlay.classList.add('active')); + + // Initialize Lucide icons in modal + if (window.lucide) lucide.createIcons(); + + // Close on overlay click + overlay.addEventListener('click', (e) => { + if (e.target === overlay) closeModal(); + }); + + // Close on Escape key + const escHandler = (e) => { + if (e.key === 'Escape') { + closeModal(); + document.removeEventListener('keydown', escHandler); + } + }; + document.addEventListener('keydown', escHandler); +} + +function closeModal() { + const overlay = document.querySelector('.generic-modal-overlay'); + if (overlay) { + overlay.classList.remove('active'); + setTimeout(() => overlay.remove(), 200); + } +} + // SVG Icons const icons = { folder: '', diff --git a/ccw/src/templates/dashboard-js/components/navigation.js b/ccw/src/templates/dashboard-js/components/navigation.js index 3af5597f..f324281e 100644 --- a/ccw/src/templates/dashboard-js/components/navigation.js +++ b/ccw/src/templates/dashboard-js/components/navigation.js @@ -116,6 +116,8 @@ function updateContentTitle() { titleEl.textContent = 'MCP Server Management'; } else if (currentView === 'explorer') { titleEl.textContent = 'File Explorer'; + } else if (currentView === 'cli-manager') { + titleEl.textContent = 'CLI Tools & CCW'; } else if (currentView === 'liteTasks') { const names = { 'lite-plan': 'Lite Plan Sessions', 'lite-fix': 'Lite Fix Sessions' }; titleEl.textContent = names[currentLiteType] || 'Lite Tasks'; diff --git a/ccw/src/templates/dashboard-js/components/notifications.js b/ccw/src/templates/dashboard-js/components/notifications.js index dadb2f71..c412b7a5 100644 --- a/ccw/src/templates/dashboard-js/components/notifications.js +++ b/ccw/src/templates/dashboard-js/components/notifications.js @@ -3,6 +3,42 @@ // ========================================== // Real-time silent refresh (no notification bubbles) +/** + * Format JSON object for display in notifications + * @param {Object} obj - Object to format + * @param {number} maxLen - Max string length + * @returns {string} Formatted string + */ +function formatJsonDetails(obj, maxLen = 150) { + if (!obj || typeof obj !== 'object') return String(obj); + + // Try pretty format first + try { + const formatted = JSON.stringify(obj, null, 2); + if (formatted.length <= maxLen) { + return formatted; + } + + // For longer content, show key-value pairs on separate lines + const entries = Object.entries(obj); + if (entries.length === 0) return '{}'; + + const lines = entries.slice(0, 5).map(([key, val]) => { + let valStr = typeof val === 'object' ? JSON.stringify(val) : String(val); + if (valStr.length > 50) valStr = valStr.substring(0, 47) + '...'; + return `${key}: ${valStr}`; + }); + + if (entries.length > 5) { + lines.push(`... +${entries.length - 5} more`); + } + + return lines.join('\n'); + } catch (e) { + return JSON.stringify(obj).substring(0, maxLen) + '...'; + } +} + let wsConnection = null; let autoRefreshInterval = null; let lastDataHash = null; @@ -132,9 +168,7 @@ function handleToolExecutionNotification(payload) { notifType = 'info'; message = `Executing ${toolName}...`; if (params) { - // Show truncated params - const paramStr = JSON.stringify(params); - details = paramStr.length > 100 ? paramStr.substring(0, 100) + '...' : paramStr; + details = formatJsonDetails(params, 150); } break; @@ -142,12 +176,10 @@ function handleToolExecutionNotification(payload) { notifType = 'success'; message = `${toolName} completed`; if (result) { - // Show truncated result if (result._truncated) { details = result.preview; } else { - const resultStr = JSON.stringify(result); - details = resultStr.length > 150 ? resultStr.substring(0, 150) + '...' : resultStr; + details = formatJsonDetails(result, 200); } } break; diff --git a/ccw/src/templates/dashboard-js/state.js b/ccw/src/templates/dashboard-js/state.js index 3fbe372e..b42abe68 100644 --- a/ccw/src/templates/dashboard-js/state.js +++ b/ccw/src/templates/dashboard-js/state.js @@ -37,9 +37,45 @@ const liteTaskDataStore = {}; const taskJsonStore = {}; // ========== Global Notification Queue ========== -// Notification queue visible from any view -let globalNotificationQueue = []; +// Notification queue visible from any view (persisted to localStorage) +const NOTIFICATION_STORAGE_KEY = 'ccw_notifications'; +const NOTIFICATION_MAX_STORED = 100; + +// Load notifications from localStorage on init +let globalNotificationQueue = loadNotificationsFromStorage(); let isNotificationPanelVisible = false; + +/** + * Load notifications from localStorage + * @returns {Array} Notification array + */ +function loadNotificationsFromStorage() { + try { + const stored = localStorage.getItem(NOTIFICATION_STORAGE_KEY); + if (stored) { + const parsed = JSON.parse(stored); + // Filter out notifications older than 7 days + const sevenDaysAgo = Date.now() - (7 * 24 * 60 * 60 * 1000); + return parsed.filter(n => new Date(n.timestamp).getTime() > sevenDaysAgo); + } + } catch (e) { + console.error('[Notifications] Failed to load from storage:', e); + } + return []; +} + +/** + * Save notifications to localStorage + */ +function saveNotificationsToStorage() { + try { + // Keep only the last N notifications + const toSave = globalNotificationQueue.slice(0, NOTIFICATION_MAX_STORED); + localStorage.setItem(NOTIFICATION_STORAGE_KEY, JSON.stringify(toSave)); + } catch (e) { + console.error('[Notifications] Failed to save to storage:', e); + } +} // ========== Event Handler ========== /** * Handle granular workflow events from CLI diff --git a/ccw/src/templates/dashboard-js/views/cli-manager.js b/ccw/src/templates/dashboard-js/views/cli-manager.js index cc37b3c9..6e966f91 100644 --- a/ccw/src/templates/dashboard-js/views/cli-manager.js +++ b/ccw/src/templates/dashboard-js/views/cli-manager.js @@ -1,15 +1,15 @@ // CLI Manager View -// Main view combining CLI status and history panels +// Main view combining CLI status, CCW installations, and history panels // ========== CLI Manager State ========== -let currentCliExecution = null; -let cliExecutionOutput = ''; +var currentCliExecution = null; +var cliExecutionOutput = ''; +var ccwInstallations = []; // ========== Initialization ========== function initCliManager() { - // Initialize CLI navigation - document.querySelectorAll('.nav-item[data-view="cli-manager"]').forEach(item => { - item.addEventListener('click', () => { + document.querySelectorAll('.nav-item[data-view="cli-manager"]').forEach(function(item) { + item.addEventListener('click', function() { setActiveNavItem(item); currentView = 'cli-manager'; currentFilter = null; @@ -21,262 +21,461 @@ function initCliManager() { }); } +// ========== CCW Installations ========== +async function loadCcwInstallations() { + try { + var response = await fetch('/api/ccw/installations'); + if (!response.ok) throw new Error('Failed to load CCW installations'); + var data = await response.json(); + ccwInstallations = data.installations || []; + return ccwInstallations; + } catch (err) { + console.error('Failed to load CCW installations:', err); + ccwInstallations = []; + return []; + } +} + // ========== Rendering ========== async function renderCliManager() { - const mainContent = document.querySelector('.main-content'); - if (!mainContent) return; + var container = document.getElementById('mainContent'); + if (!container) return; + + // Hide stats grid and search for CLI 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 data await Promise.all([ loadCliToolStatus(), - loadCliHistory() + loadCliHistory(), + loadCcwInstallations() ]); - mainContent.innerHTML = ` -
-
- -
-
-
- - -
-
-
-
- - -
-
-
- - -
-
-

Execution Output

-
- - Running... -
-
-

-      
-
- `; + container.innerHTML = '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
'; // Render sub-panels renderCliStatus(); - renderCliExecutePanel(); + renderCcwInstallPanel(); renderCliHistory(); // Initialize Lucide icons - if (window.lucide) { - lucide.createIcons(); + if (window.lucide) lucide.createIcons(); +} + +// CCW Install Carousel State +var ccwCarouselIndex = 0; + +function renderCcwInstallPanel() { + var container = document.getElementById('ccw-install-panel'); + if (!container) return; + + var html = '

CCW Installations

' + + '
' + + '' + + '' + + '
' + + '
'; + + if (ccwInstallations.length === 0) { + html += '
' + + '' + + '

No installations found

' + + '
'; + } else { + // Carousel container + html += ''; + + // Dots indicator (show only if more than 1 installation) + if (ccwInstallations.length > 1) { + html += ''; + } + } + + html += '
'; + container.innerHTML = html; + if (window.lucide) lucide.createIcons(); + + // Update carousel position + updateCcwCarouselPosition(); +} + +function ccwCarouselPrev() { + if (ccwCarouselIndex > 0) { + ccwCarouselIndex--; + updateCcwCarouselPosition(); + updateCcwCarouselDots(); } } +function ccwCarouselNext() { + if (ccwCarouselIndex < ccwInstallations.length - 1) { + ccwCarouselIndex++; + updateCcwCarouselPosition(); + updateCcwCarouselDots(); + } +} + +function ccwCarouselGoTo(index) { + ccwCarouselIndex = index; + updateCcwCarouselPosition(); + updateCcwCarouselDots(); +} + +function updateCcwCarouselPosition() { + var track = document.getElementById('ccwCarouselTrack'); + if (track) { + track.style.transform = 'translateX(-' + (ccwCarouselIndex * 100) + '%)'; + } + + // Update card active states + var cards = document.querySelectorAll('.ccw-carousel-card'); + cards.forEach(function(card, idx) { + card.classList.toggle('active', idx === ccwCarouselIndex); + }); +} + +function updateCcwCarouselDots() { + var dots = document.querySelectorAll('.ccw-carousel-dot'); + dots.forEach(function(dot, idx) { + dot.classList.toggle('active', idx === ccwCarouselIndex); + }); +} + +// CCW Install Modal +function showCcwInstallModal() { + var modalContent = '
' + + '
' + + '
' + + '
' + + '
' + + '
Global Installation
' + + '
Install to user home directory (~/.claude)
' + + '
' + + '' + + '
' + + '
' + + '
' + + '
' + + '
Path Installation
' + + '
Install to a specific project folder
' + + '
' + + '' + + '
' + + '
' + + '' + + '
'; + + showModal('Install CCW', modalContent); +} + +function selectCcwInstallMode(mode) { + if (mode === 'Global') { + closeModal(); + runCcwInstall('Global'); + } +} + +function toggleCcwPathInput() { + var section = document.getElementById('ccwPathInputSection'); + if (section) { + section.classList.toggle('hidden'); + if (!section.classList.contains('hidden')) { + var input = document.getElementById('ccwInstallPath'); + if (input) input.focus(); + } + } +} + +function executeCcwInstall() { + var input = document.getElementById('ccwInstallPath'); + var path = input ? input.value.trim() : ''; + + if (!path) { + showRefreshToast('Please enter a path', 'error'); + return; + } + + closeModal(); + runCcwInstall('Path', path); +} + +function truncatePath(path) { + if (!path) return ''; + var maxLen = 35; + if (path.length <= maxLen) return path; + return '...' + path.slice(-maxLen + 3); +} + function renderCliExecutePanel() { - const container = document.getElementById('cli-execute-panel'); + var container = document.getElementById('cli-execute-panel'); if (!container) return; - const tools = ['gemini', 'qwen', 'codex']; - const modes = ['analysis', 'write', 'auto']; - - container.innerHTML = ` -
-

Quick Execute

-
-
-
-
- - -
-
- - -
-
-
- - -
-
- -
-
- `; + var tools = ['gemini', 'qwen', 'codex']; + var modes = ['analysis', 'write', 'auto']; + var html = '

Quick Execute

' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
'; + container.innerHTML = html; if (window.lucide) lucide.createIcons(); } +// ========== CCW Actions ========== +function runCcwInstall(mode, customPath) { + var command; + if (mode === 'Global') { + command = 'ccw install --mode Global'; + } else { + var installPath = customPath || projectPath; + command = 'ccw install --mode Path --path "' + installPath + '"'; + } + + // Copy command to clipboard + if (navigator.clipboard) { + navigator.clipboard.writeText(command).then(function() { + showRefreshToast('Command copied: ' + command, 'success'); + }).catch(function() { + showRefreshToast('Run: ' + command, 'info'); + }); + } else { + showRefreshToast('Run: ' + command, 'info'); + } +} + +function runCcwUpgrade() { + var command = 'ccw upgrade'; + if (navigator.clipboard) { + navigator.clipboard.writeText(command).then(function() { + showRefreshToast('Command copied: ' + command, 'success'); + }).catch(function() { + showRefreshToast('Run: ' + command, 'info'); + }); + } else { + showRefreshToast('Run: ' + command, 'info'); + } +} + +function confirmCcwUninstall(installPath) { + if (confirm('Uninstall CCW from this location?\n' + (installPath || 'Current installation'))) { + var command = installPath + ? 'ccw uninstall --path "' + installPath + '"' + : 'ccw uninstall'; + + if (navigator.clipboard) { + navigator.clipboard.writeText(command).then(function() { + showRefreshToast('Command copied: ' + command, 'success'); + }).catch(function() { + showRefreshToast('Run: ' + command, 'info'); + }); + } else { + showRefreshToast('Run: ' + command, 'info'); + } + } +} + // ========== Execution ========== async function executeCliFromDashboard() { - const tool = document.getElementById('cli-exec-tool').value; - const mode = document.getElementById('cli-exec-mode').value; - const prompt = document.getElementById('cli-exec-prompt').value.trim(); + var toolEl = document.getElementById('cli-exec-tool'); + var modeEl = document.getElementById('cli-exec-mode'); + var promptEl = document.getElementById('cli-exec-prompt'); + + var tool = toolEl ? toolEl.value : 'gemini'; + var mode = modeEl ? modeEl.value : 'analysis'; + var prompt = promptEl ? promptEl.value.trim() : ''; if (!prompt) { showRefreshToast('Please enter a prompt', 'error'); return; } - // Show output panel - currentCliExecution = { tool, mode, prompt, startTime: Date.now() }; + currentCliExecution = { tool: tool, mode: mode, prompt: prompt, startTime: Date.now() }; cliExecutionOutput = ''; - const outputPanel = document.getElementById('cli-output-panel'); - const outputContent = document.getElementById('cli-output-content'); - const statusIndicator = document.getElementById('cli-output-status-indicator'); - const statusText = document.getElementById('cli-output-status-text'); + var outputPanel = document.getElementById('cli-output-panel'); + var outputContent = document.getElementById('cli-output-content'); + var statusIndicator = document.getElementById('cli-output-status-indicator'); + var statusText = document.getElementById('cli-output-status-text'); if (outputPanel) outputPanel.classList.remove('hidden'); if (outputContent) outputContent.textContent = ''; - if (statusIndicator) { - statusIndicator.className = 'status-indicator running'; - } + if (statusIndicator) statusIndicator.className = 'status-indicator running'; if (statusText) statusText.textContent = 'Running...'; - // Disable execute button - const execBtn = document.querySelector('.cli-execute-actions .btn-primary'); + var execBtn = document.querySelector('.cli-execute-actions .btn-primary'); if (execBtn) execBtn.disabled = true; try { - const response = await fetch('/api/cli/execute', { + var response = await fetch('/api/cli/execute', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - tool, - mode, - prompt, - dir: projectPath - }) + body: JSON.stringify({ tool: tool, mode: mode, prompt: prompt, dir: projectPath }) }); + var result = await response.json(); - const result = await response.json(); - - // Update status - if (statusIndicator) { - statusIndicator.className = `status-indicator ${result.success ? 'success' : 'error'}`; - } + if (statusIndicator) statusIndicator.className = 'status-indicator ' + (result.success ? 'success' : 'error'); if (statusText) { - const duration = formatDuration(result.execution?.duration_ms || (Date.now() - currentCliExecution.startTime)); - statusText.textContent = result.success - ? `Completed in ${duration}` - : `Failed: ${result.error || 'Unknown error'}`; + var duration = formatDuration(result.execution ? result.execution.duration_ms : (Date.now() - currentCliExecution.startTime)); + statusText.textContent = result.success ? 'Completed in ' + duration : 'Failed: ' + (result.error || 'Unknown'); } - // Refresh history await loadCliHistory(); renderCliHistory(); - - if (result.success) { - showRefreshToast('Execution completed', 'success'); - } else { - showRefreshToast(result.error || 'Execution failed', 'error'); - } - + showRefreshToast(result.success ? 'Completed' : (result.error || 'Failed'), result.success ? 'success' : 'error'); } catch (error) { - if (statusIndicator) { - statusIndicator.className = 'status-indicator error'; - } - if (statusText) { - statusText.textContent = `Error: ${error.message}`; - } - showRefreshToast(`Execution error: ${error.message}`, 'error'); + if (statusIndicator) statusIndicator.className = 'status-indicator error'; + if (statusText) statusText.textContent = 'Error: ' + error.message; + showRefreshToast('Error: ' + error.message, 'error'); } currentCliExecution = null; - - // Re-enable execute button if (execBtn) execBtn.disabled = false; } // ========== WebSocket Event Handlers ========== function handleCliExecutionStarted(payload) { - const { executionId, tool, mode, timestamp } = payload; - currentCliExecution = { executionId, tool, mode, startTime: new Date(timestamp).getTime() }; + currentCliExecution = { + executionId: payload.executionId, + tool: payload.tool, + mode: payload.mode, + startTime: new Date(payload.timestamp).getTime() + }; cliExecutionOutput = ''; - // Show output panel if in CLI manager view if (currentView === 'cli-manager') { - const outputPanel = document.getElementById('cli-output-panel'); - const outputContent = document.getElementById('cli-output-content'); - const statusIndicator = document.getElementById('cli-output-status-indicator'); - const statusText = document.getElementById('cli-output-status-text'); + var outputPanel = document.getElementById('cli-output-panel'); + var outputContent = document.getElementById('cli-output-content'); + var statusIndicator = document.getElementById('cli-output-status-indicator'); + var statusText = document.getElementById('cli-output-status-text'); if (outputPanel) outputPanel.classList.remove('hidden'); if (outputContent) outputContent.textContent = ''; if (statusIndicator) statusIndicator.className = 'status-indicator running'; - if (statusText) statusText.textContent = `Running ${tool} (${mode})...`; + if (statusText) statusText.textContent = 'Running ' + payload.tool + ' (' + payload.mode + ')...'; } } function handleCliOutput(payload) { - const { data } = payload; - cliExecutionOutput += data; - - // Update output panel if visible - const outputContent = document.getElementById('cli-output-content'); + cliExecutionOutput += payload.data; + var outputContent = document.getElementById('cli-output-content'); if (outputContent) { outputContent.textContent = cliExecutionOutput; - // Auto-scroll to bottom outputContent.scrollTop = outputContent.scrollHeight; } } function handleCliExecutionCompleted(payload) { - const { executionId, success, status, duration_ms } = payload; + var statusIndicator = document.getElementById('cli-output-status-indicator'); + var statusText = document.getElementById('cli-output-status-text'); - // Update status - const statusIndicator = document.getElementById('cli-output-status-indicator'); - const statusText = document.getElementById('cli-output-status-text'); - - if (statusIndicator) { - statusIndicator.className = `status-indicator ${success ? 'success' : 'error'}`; - } - if (statusText) { - statusText.textContent = success - ? `Completed in ${formatDuration(duration_ms)}` - : `Failed: ${status}`; - } + if (statusIndicator) statusIndicator.className = 'status-indicator ' + (payload.success ? 'success' : 'error'); + if (statusText) statusText.textContent = payload.success ? 'Completed in ' + formatDuration(payload.duration_ms) : 'Failed: ' + payload.status; currentCliExecution = null; - - // Refresh history if (currentView === 'cli-manager') { - loadCliHistory().then(() => renderCliHistory()); + loadCliHistory().then(function() { renderCliHistory(); }); } } function handleCliExecutionError(payload) { - const { executionId, error } = payload; + var statusIndicator = document.getElementById('cli-output-status-indicator'); + var statusText = document.getElementById('cli-output-status-text'); - const statusIndicator = document.getElementById('cli-output-status-indicator'); - const statusText = document.getElementById('cli-output-status-text'); - - if (statusIndicator) { - statusIndicator.className = 'status-indicator error'; - } - if (statusText) { - statusText.textContent = `Error: ${error}`; - } + if (statusIndicator) statusIndicator.className = 'status-indicator error'; + if (statusText) statusText.textContent = 'Error: ' + payload.error; currentCliExecution = null; } diff --git a/ccw/src/templates/dashboard-js/views/explorer.js b/ccw/src/templates/dashboard-js/views/explorer.js index a0245989..7188cfe0 100644 --- a/ccw/src/templates/dashboard-js/views/explorer.js +++ b/ccw/src/templates/dashboard-js/views/explorer.js @@ -16,6 +16,27 @@ let isTaskRunning = false; // Note: defaultCliTool is defined in components/cli-status.js +/** + * Safe base64 encode that handles Unicode characters + * Returns alphanumeric-only string suitable for HTML IDs + */ +function safeBase64Encode(str) { + try { + // Encode Unicode string to UTF-8 bytes, then to base64 + const encoded = btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, (_, p1) => String.fromCharCode(parseInt(p1, 16)))); + return encoded.replace(/[^a-zA-Z0-9]/g, ''); + } catch (e) { + // Fallback: use simple hash if encoding fails + let hash = 0; + for (let i = 0; i < str.length; i++) { + const char = str.charCodeAt(i); + hash = ((hash << 5) - hash) + char; + hash = hash & hash; + } + return 'path' + Math.abs(hash).toString(36); + } +} + /** * Render the Explorer view */ @@ -177,7 +198,7 @@ function renderTreeLevel(files, parentPath, depth) { -
+
${isExpanded ? '' : ''}
@@ -298,35 +319,44 @@ function getFolderIcon(name, isExpanded, hasClaudeMd) { : ''; } +// Flag to track if event delegation is already set up +let explorerEventsDelegated = false; + /** - * Attach event listeners to tree items + * Attach event listeners using event delegation (only once on container) */ function attachTreeEventListeners() { - // Folder click - toggle expand - document.querySelectorAll('.tree-folder > .tree-item-row').forEach(row => { - row.addEventListener('click', async (e) => { - const folder = row.closest('.tree-folder'); + const treeContent = document.getElementById('explorerTreeContent'); + if (!treeContent || explorerEventsDelegated) return; + + explorerEventsDelegated = true; + + // Use event delegation - single listener on container handles all clicks + treeContent.addEventListener('click', async (e) => { + // Check if clicked on folder row (but not on action buttons) + const folderRow = e.target.closest('.tree-folder > .tree-item-row'); + if (folderRow && !e.target.closest('.tree-folder-actions')) { + const folder = folderRow.closest('.tree-folder'); const path = folder.dataset.path; await toggleFolderExpand(path, folder); - }); - }); + return; + } - // File click - preview - document.querySelectorAll('.tree-file').forEach(item => { - item.addEventListener('click', async () => { - const path = item.dataset.path; + // Check if clicked on file + const fileItem = e.target.closest('.tree-file'); + if (fileItem) { + const path = fileItem.dataset.path; await previewFile(path); - + // Update selection document.querySelectorAll('.tree-item-row.selected, .tree-file.selected').forEach(el => { el.classList.remove('selected'); }); - item.classList.add('selected'); + fileItem.classList.add('selected'); explorerSelectedFile = path; - }); + } }); } - /** * Toggle folder expand/collapse */ @@ -366,7 +396,6 @@ async function toggleFolderExpand(path, folderElement) { const depth = (path.match(/\//g) || []).length - (explorerCurrentPath.match(/\//g) || []).length + 1; childrenContainer.innerHTML = renderTreeLevel(data.files, path, depth); - attachTreeEventListeners(); } catch (error) { childrenContainer.innerHTML = `
Failed to load
`; } @@ -483,6 +512,7 @@ async function refreshExplorerTree() { } explorerExpandedDirs.clear(); + explorerEventsDelegated = false; await loadExplorerTree(explorerCurrentPath); if (btn) { diff --git a/ccw/src/templates/dashboard.html b/ccw/src/templates/dashboard.html index 4c91fbb6..fc32162b 100644 --- a/ccw/src/templates/dashboard.html +++ b/ccw/src/templates/dashboard.html @@ -317,6 +317,11 @@ Explorer + @@ -395,20 +400,6 @@ - -
-
- - CLI Tools -
- -
diff --git a/ccw/src/tools/cli-executor.js b/ccw/src/tools/cli-executor.js index e5f3f1ad..855837fd 100644 --- a/ccw/src/tools/cli-executor.js +++ b/ccw/src/tools/cli-executor.js @@ -4,7 +4,7 @@ */ import { spawn } from 'child_process'; -import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'; +import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from 'fs'; import { join, dirname } from 'path'; import { homedir } from 'os'; @@ -436,6 +436,44 @@ export function getExecutionDetail(baseDir, executionId) { return null; } +/** + * Delete execution by ID + * @param {string} baseDir - Base directory + * @param {string} executionId - Execution ID + * @returns {{success: boolean, error?: string}} + */ +export function deleteExecution(baseDir, executionId) { + const historyDir = join(baseDir, '.workflow', '.cli-history'); + + // Parse date from execution ID + const timestamp = parseInt(executionId.split('-')[0], 10); + const date = new Date(timestamp); + const dateStr = date.toISOString().split('T')[0]; + + const filePath = join(historyDir, dateStr, `${executionId}.json`); + + // Delete the execution file + if (existsSync(filePath)) { + try { + unlinkSync(filePath); + } catch (err) { + return { success: false, error: `Failed to delete file: ${err.message}` }; + } + } + + // Update index + try { + const index = loadHistoryIndex(historyDir); + index.executions = index.executions.filter(e => e.id !== executionId); + index.total_executions = Math.max(0, index.total_executions - 1); + writeFileSync(join(historyDir, 'index.json'), JSON.stringify(index, null, 2), 'utf8'); + } catch (err) { + return { success: false, error: `Failed to update index: ${err.message}` }; + } + + return { success: true }; +} + /** * CLI Executor Tool Definition */