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 = `
`;
@@ -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 `
-
-
-
${escapeHtml(exec.prompt_preview)}
-
- ${duration}
-
-
- `;
- }).join('');
+ return `
+
+
+
+
${escapeHtml(exec.prompt_preview)}
+
+ ${duration}
+ ${exec.mode || 'analysis'}
+
+
+
+
+
+
+
+
+
+
+
+ `;
+ }).join('');
container.innerHTML = `
@@ -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.
+
` : ''}
+
+
+ Copy Prompt
+
+
+ Delete
+
+
`;
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 = `
+
+ `;
+
+ 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 = `
-
- `;
+ 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 = '' +
+ '';
+
+ if (ccwInstallations.length === 0) {
+ html += '
' +
+ '
' +
+ '
No installations found
' +
+ '
' +
+ ' Install CCW ';
+ } else {
+ // Carousel container
+ html += '
';
+
+ // Left arrow (show only if more than 1 installation)
+ if (ccwInstallations.length > 1) {
+ html += '
' +
+ ' ';
+ }
+
+ html += '
';
+
+ 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();
+ var activeClass = i === ccwCarouselIndex ? 'active' : '';
+
+ html += '
' +
+ '' +
+ '
' + escapeHtml(inst.installation_path) + '
' +
+ '
' +
+ ' ' + installDate + ' ' +
+ ' ' + (inst.files_count || 0) + ' files ' +
+ '
' +
+ '
' +
+ '' +
+ ' ' +
+ '' +
+ ' ' +
+ '
' +
+ '
';
+ }
+
+ html += '
';
+
+ // Right arrow (show only if more than 1 installation)
+ if (ccwInstallations.length > 1) {
+ html += '
' +
+ ' ';
+ }
+
+ html += '
';
+
+ // Dots indicator (show only if more than 1 installation)
+ if (ccwInstallations.length > 1) {
+ html += '
';
+ for (var j = 0; j < ccwInstallations.length; j++) {
+ var dotActive = j === ccwCarouselIndex ? 'active' : '';
+ html += ' ';
+ }
+ 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 = `
-
-
- `;
+ var tools = ['gemini', 'qwen', 'codex'];
+ var modes = ['analysis', 'write', 'auto'];
+ var html = '' +
+ '';
+ 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
+
+
+ Status
+ 0/3
+
@@ -395,20 +400,6 @@
-
-
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
*/