mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-12 02:37:45 +08:00
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
This commit is contained in:
@@ -7,7 +7,8 @@ import { createHash } from 'crypto';
|
|||||||
import { scanSessions } from './session-scanner.js';
|
import { scanSessions } from './session-scanner.js';
|
||||||
import { aggregateData } from './data-aggregator.js';
|
import { aggregateData } from './data-aggregator.js';
|
||||||
import { resolvePath, getRecentPaths, trackRecentPath, removeRecentPath, normalizePathForDisplay, getWorkflowDir } from '../utils/path-resolver.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
|
// Claude config file paths
|
||||||
const CLAUDE_CONFIG_PATH = join(homedir(), '.claude.json');
|
const CLAUDE_CONFIG_PATH = join(homedir(), '.claude.json');
|
||||||
@@ -47,7 +48,8 @@ const MODULE_CSS_FILES = [
|
|||||||
'06-cards.css',
|
'06-cards.css',
|
||||||
'07-managers.css',
|
'07-managers.css',
|
||||||
'08-review.css',
|
'08-review.css',
|
||||||
'09-explorer.css'
|
'09-explorer.css',
|
||||||
|
'10-cli.css'
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -449,6 +451,14 @@ export async function startServer(options = {}) {
|
|||||||
return;
|
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
|
// API: CLI Execution History
|
||||||
if (pathname === '/api/cli/history') {
|
if (pathname === '/api/cli/history') {
|
||||||
const projectPath = url.searchParams.get('path') || initialPath;
|
const projectPath = url.searchParams.get('path') || initialPath;
|
||||||
@@ -462,7 +472,7 @@ export async function startServer(options = {}) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// API: CLI Execution Detail
|
// API: CLI Execution Detail (GET) or Delete (DELETE)
|
||||||
if (pathname === '/api/cli/execution') {
|
if (pathname === '/api/cli/execution') {
|
||||||
const projectPath = url.searchParams.get('path') || initialPath;
|
const projectPath = url.searchParams.get('path') || initialPath;
|
||||||
const executionId = url.searchParams.get('id');
|
const executionId = url.searchParams.get('id');
|
||||||
@@ -473,6 +483,20 @@ export async function startServer(options = {}) {
|
|||||||
return;
|
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);
|
const detail = getExecutionDetail(projectPath, executionId);
|
||||||
if (!detail) {
|
if (!detail) {
|
||||||
res.writeHead(404, { 'Content-Type': 'application/json' });
|
res.writeHead(404, { 'Content-Type': 'application/json' });
|
||||||
|
|||||||
@@ -1319,11 +1319,20 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.notif-details {
|
.notif-details {
|
||||||
font-size: 12px;
|
font-size: 11px;
|
||||||
|
font-family: 'SF Mono', 'Consolas', 'Monaco', monospace;
|
||||||
color: hsl(var(--muted-foreground));
|
color: hsl(var(--muted-foreground));
|
||||||
margin-top: 6px;
|
margin-top: 8px;
|
||||||
padding-left: 22px;
|
margin-left: 22px;
|
||||||
line-height: 1.4;
|
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 {
|
.notif-meta {
|
||||||
|
|||||||
@@ -1,18 +1,20 @@
|
|||||||
/* ========================================
|
/* ========================================
|
||||||
* CLI Manager Styles
|
* CLI Manager Styles
|
||||||
|
* Unified font: system-ui for UI, monospace for code
|
||||||
* ======================================== */
|
* ======================================== */
|
||||||
|
|
||||||
/* Container */
|
/* Container */
|
||||||
.cli-manager-container {
|
.cli-manager-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1.5rem;
|
gap: 1.25rem;
|
||||||
|
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cli-manager-grid {
|
.cli-manager-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
gap: 1.5rem;
|
gap: 1.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
@@ -25,8 +27,14 @@
|
|||||||
.cli-panel {
|
.cli-panel {
|
||||||
background: hsl(var(--card));
|
background: hsl(var(--card));
|
||||||
border: 1px solid hsl(var(--border));
|
border: 1px solid hsl(var(--border));
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.75rem;
|
||||||
overflow: hidden;
|
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 {
|
.cli-panel-full {
|
||||||
@@ -38,38 +46,48 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 1rem;
|
padding: 0.875rem 1rem;
|
||||||
border-bottom: 1px solid hsl(var(--border));
|
border-bottom: 1px solid hsl(var(--border));
|
||||||
|
background: hsl(var(--muted) / 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.cli-status-header h3 {
|
.cli-status-header h3 {
|
||||||
font-size: 0.875rem;
|
font-size: 0.8125rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: hsl(var(--foreground));
|
color: hsl(var(--foreground));
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cli-tools-grid {
|
.cli-tools-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(3, 1fr);
|
grid-template-columns: repeat(3, 1fr);
|
||||||
gap: 0.75rem;
|
gap: 0.625rem;
|
||||||
padding: 1rem;
|
padding: 0.875rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cli-tool-card {
|
.cli-tool-card {
|
||||||
padding: 0.75rem;
|
padding: 0.875rem 0.75rem;
|
||||||
border-radius: 0.375rem;
|
border-radius: 0.5rem;
|
||||||
background: hsl(var(--muted));
|
background: hsl(var(--background));
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
border: 1px solid hsl(var(--border));
|
||||||
|
transition: all 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cli-tool-card.available {
|
.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 {
|
.cli-tool-card.unavailable {
|
||||||
border: 1px solid hsl(var(--border));
|
border-color: hsl(var(--border));
|
||||||
opacity: 0.7;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cli-tool-header {
|
.cli-tool-header {
|
||||||
@@ -77,40 +95,52 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
margin-bottom: 0.25rem;
|
margin-bottom: 0.375rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cli-tool-status {
|
.cli-tool-status {
|
||||||
width: 8px;
|
width: 8px;
|
||||||
height: 8px;
|
height: 8px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cli-tool-status.status-available {
|
.cli-tool-status.status-available {
|
||||||
background: hsl(var(--success));
|
background: hsl(var(--success));
|
||||||
|
box-shadow: 0 0 6px hsl(var(--success) / 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.cli-tool-status.status-unavailable {
|
.cli-tool-status.status-unavailable {
|
||||||
background: hsl(var(--muted-foreground));
|
background: hsl(var(--muted-foreground) / 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.cli-tool-name {
|
.cli-tool-name {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 0.875rem;
|
font-size: 0.8125rem;
|
||||||
color: hsl(var(--foreground));
|
color: hsl(var(--foreground));
|
||||||
|
letter-spacing: -0.01em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cli-tool-badge {
|
.cli-tool-badge {
|
||||||
font-size: 0.625rem;
|
font-size: 0.5625rem;
|
||||||
|
font-weight: 600;
|
||||||
padding: 0.125rem 0.375rem;
|
padding: 0.125rem 0.375rem;
|
||||||
background: hsl(var(--primary));
|
background: hsl(var(--primary));
|
||||||
color: hsl(var(--primary-foreground));
|
color: hsl(var(--primary-foreground));
|
||||||
border-radius: 9999px;
|
border-radius: 9999px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.03em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cli-tool-info {
|
.cli-tool-info {
|
||||||
font-size: 0.75rem;
|
font-size: 0.6875rem;
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
|
color: hsl(var(--muted-foreground));
|
||||||
|
}
|
||||||
|
|
||||||
|
.cli-tool-info .text-success {
|
||||||
|
color: hsl(var(--success));
|
||||||
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Execute Panel */
|
/* Execute Panel */
|
||||||
@@ -186,15 +216,17 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 1rem;
|
padding: 0.875rem 1rem;
|
||||||
border-bottom: 1px solid hsl(var(--border));
|
border-bottom: 1px solid hsl(var(--border));
|
||||||
|
background: hsl(var(--muted) / 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.cli-history-header h3 {
|
.cli-history-header h3 {
|
||||||
font-size: 0.875rem;
|
font-size: 0.8125rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: hsl(var(--foreground));
|
color: hsl(var(--foreground));
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cli-history-controls {
|
.cli-history-controls {
|
||||||
@@ -203,82 +235,160 @@
|
|||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cli-tool-filter {
|
/* Search Input for History */
|
||||||
padding: 0.375rem 0.5rem;
|
.cli-history-search {
|
||||||
|
padding: 0.375rem 0.625rem;
|
||||||
border: 1px solid hsl(var(--border));
|
border: 1px solid hsl(var(--border));
|
||||||
border-radius: 0.375rem;
|
border-radius: 0.375rem;
|
||||||
background: hsl(var(--background));
|
background: hsl(var(--background));
|
||||||
color: hsl(var(--foreground));
|
color: hsl(var(--foreground));
|
||||||
font-size: 0.75rem;
|
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 {
|
.cli-history-list {
|
||||||
max-height: 400px;
|
max-height: 450px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cli-history-item {
|
.cli-history-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
padding: 0.75rem 1rem;
|
padding: 0.75rem 1rem;
|
||||||
border-bottom: 1px solid hsl(var(--border));
|
border-bottom: 1px solid hsl(var(--border) / 0.5);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background 0.15s ease;
|
transition: all 0.15s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cli-history-item:hover {
|
.cli-history-item:hover {
|
||||||
background: hsl(var(--hover));
|
background: hsl(var(--hover));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cli-history-item:hover .cli-history-actions {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.cli-history-item:last-child {
|
.cli-history-item:last-child {
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cli-history-item-content {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.cli-history-item-header {
|
.cli-history-item-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
margin-bottom: 0.25rem;
|
margin-bottom: 0.375rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cli-tool-tag {
|
.cli-tool-tag {
|
||||||
font-size: 0.625rem;
|
font-size: 0.5625rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
padding: 0.125rem 0.375rem;
|
padding: 0.125rem 0.5rem;
|
||||||
border-radius: 0.25rem;
|
border-radius: 9999px;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cli-tool-gemini {
|
.cli-tool-gemini {
|
||||||
background: hsl(210 80% 55% / 0.15);
|
background: hsl(210 80% 55% / 0.12);
|
||||||
color: hsl(210 80% 50%);
|
color: hsl(210 80% 45%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.cli-tool-qwen {
|
.cli-tool-qwen {
|
||||||
background: hsl(280 70% 55% / 0.15);
|
background: hsl(280 70% 55% / 0.12);
|
||||||
color: hsl(280 70% 50%);
|
color: hsl(280 70% 45%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.cli-tool-codex {
|
.cli-tool-codex {
|
||||||
background: hsl(142 71% 45% / 0.15);
|
background: hsl(142 71% 45% / 0.12);
|
||||||
color: hsl(142 71% 40%);
|
color: hsl(142 71% 35%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.cli-history-time {
|
.cli-history-time {
|
||||||
font-size: 0.75rem;
|
font-size: 0.6875rem;
|
||||||
color: hsl(var(--muted-foreground));
|
color: hsl(var(--muted-foreground));
|
||||||
}
|
}
|
||||||
|
|
||||||
.cli-history-prompt {
|
.cli-history-prompt {
|
||||||
font-size: 0.8125rem;
|
font-size: 0.8125rem;
|
||||||
|
font-weight: 450;
|
||||||
color: hsl(var(--foreground));
|
color: hsl(var(--foreground));
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
line-height: 1.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cli-history-meta {
|
.cli-history-meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
font-size: 0.6875rem;
|
font-size: 0.6875rem;
|
||||||
margin-top: 0.25rem;
|
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 */
|
/* Output Panel */
|
||||||
@@ -342,92 +452,134 @@
|
|||||||
|
|
||||||
/* Detail Modal */
|
/* Detail Modal */
|
||||||
.cli-detail-header {
|
.cli-detail-header {
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1.25rem;
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
border-bottom: 1px solid hsl(var(--border));
|
||||||
}
|
}
|
||||||
|
|
||||||
.cli-detail-info {
|
.cli-detail-info {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.625rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cli-detail-status {
|
.cli-detail-status {
|
||||||
font-size: 0.75rem;
|
font-size: 0.6875rem;
|
||||||
font-weight: 500;
|
font-weight: 600;
|
||||||
padding: 0.125rem 0.5rem;
|
padding: 0.25rem 0.625rem;
|
||||||
border-radius: 9999px;
|
border-radius: 9999px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.03em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cli-detail-status.status-success {
|
.cli-detail-status.status-success {
|
||||||
background: hsl(var(--success-light));
|
background: hsl(var(--success) / 0.12);
|
||||||
color: hsl(var(--success));
|
color: hsl(var(--success));
|
||||||
}
|
}
|
||||||
|
|
||||||
.cli-detail-status.status-error {
|
.cli-detail-status.status-error {
|
||||||
background: hsl(var(--destructive) / 0.1);
|
background: hsl(var(--destructive) / 0.12);
|
||||||
color: hsl(var(--destructive));
|
color: hsl(var(--destructive));
|
||||||
}
|
}
|
||||||
|
|
||||||
.cli-detail-status.status-timeout {
|
.cli-detail-status.status-timeout {
|
||||||
background: hsl(var(--warning-light));
|
background: hsl(var(--warning) / 0.12);
|
||||||
color: hsl(var(--warning));
|
color: hsl(var(--warning));
|
||||||
}
|
}
|
||||||
|
|
||||||
.cli-detail-meta {
|
.cli-detail-meta {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 1rem;
|
gap: 1.25rem;
|
||||||
font-size: 0.75rem;
|
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 {
|
.cli-detail-section {
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cli-detail-section h4 {
|
.cli-detail-section h4 {
|
||||||
font-size: 0.8125rem;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: hsl(var(--foreground));
|
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 {
|
.cli-detail-prompt {
|
||||||
padding: 0.75rem;
|
padding: 1rem;
|
||||||
background: hsl(var(--muted));
|
background: hsl(var(--muted) / 0.5);
|
||||||
border-radius: 0.375rem;
|
border: 1px solid hsl(var(--border));
|
||||||
font-family: monospace;
|
border-radius: 0.5rem;
|
||||||
|
font-family: 'SF Mono', 'Consolas', 'Liberation Mono', monospace;
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
|
line-height: 1.6;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
max-height: 200px;
|
max-height: 200px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
color: hsl(var(--foreground));
|
||||||
}
|
}
|
||||||
|
|
||||||
.cli-detail-output {
|
.cli-detail-output {
|
||||||
padding: 0.75rem;
|
padding: 1rem;
|
||||||
background: hsl(var(--muted));
|
background: hsl(var(--muted) / 0.5);
|
||||||
border-radius: 0.375rem;
|
border: 1px solid hsl(var(--border));
|
||||||
font-family: monospace;
|
border-radius: 0.5rem;
|
||||||
|
font-family: 'SF Mono', 'Consolas', 'Liberation Mono', monospace;
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
|
line-height: 1.6;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
max-height: 300px;
|
max-height: 350px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
color: hsl(var(--foreground));
|
||||||
}
|
}
|
||||||
|
|
||||||
.cli-detail-error {
|
.cli-detail-error {
|
||||||
padding: 0.75rem;
|
padding: 1rem;
|
||||||
background: hsl(var(--destructive) / 0.1);
|
background: hsl(var(--destructive) / 0.08);
|
||||||
border-radius: 0.375rem;
|
border: 1px solid hsl(var(--destructive) / 0.2);
|
||||||
font-family: monospace;
|
border-radius: 0.5rem;
|
||||||
|
font-family: 'SF Mono', 'Consolas', 'Liberation Mono', monospace;
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
|
line-height: 1.6;
|
||||||
color: hsl(var(--destructive));
|
color: hsl(var(--destructive));
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
max-height: 150px;
|
max-height: 180px;
|
||||||
overflow-y: auto;
|
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 */
|
/* Button Styles */
|
||||||
.btn {
|
.btn {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
@@ -487,6 +639,16 @@
|
|||||||
background: hsl(var(--hover));
|
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 */
|
||||||
.empty-state {
|
.empty-state {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -507,3 +669,391 @@
|
|||||||
.empty-state p {
|
.empty-state p {
|
||||||
font-size: 0.875rem;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
// CLI History Component
|
// CLI History Component
|
||||||
// Displays execution history with filtering and search
|
// Displays execution history with filtering, search, and delete
|
||||||
|
|
||||||
// ========== CLI History State ==========
|
// ========== CLI History State ==========
|
||||||
let cliExecutionHistory = [];
|
let cliExecutionHistory = [];
|
||||||
let cliHistoryFilter = null; // Filter by tool
|
let cliHistoryFilter = null; // Filter by tool
|
||||||
|
let cliHistorySearch = ''; // Search query
|
||||||
let cliHistoryLimit = 50;
|
let cliHistoryLimit = 50;
|
||||||
|
|
||||||
// ========== Data Loading ==========
|
// ========== Data Loading ==========
|
||||||
@@ -44,19 +45,28 @@ function renderCliHistory() {
|
|||||||
const container = document.getElementById('cli-history-panel');
|
const container = document.getElementById('cli-history-panel');
|
||||||
if (!container) return;
|
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) {
|
if (cliExecutionHistory.length === 0) {
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
<div class="cli-history-header">
|
<div class="cli-history-header">
|
||||||
<h3>Execution History</h3>
|
<h3>Execution History</h3>
|
||||||
<div class="cli-history-controls">
|
<div class="cli-history-controls">
|
||||||
|
${renderHistorySearch()}
|
||||||
${renderToolFilter()}
|
${renderToolFilter()}
|
||||||
<button class="btn-icon" onclick="refreshCliHistory()" title="Refresh">
|
<button class="btn-icon" onclick="refreshCliHistory()" title="Refresh">
|
||||||
<i data-lucide="refresh-cw"></i>
|
<i data-lucide="refresh-cw" class="w-4 h-4"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
<i data-lucide="terminal"></i>
|
<i data-lucide="terminal" class="w-8 h-8"></i>
|
||||||
<p>No executions yet</p>
|
<p>No executions yet</p>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -65,36 +75,53 @@ function renderCliHistory() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const historyHtml = cliExecutionHistory.map(exec => {
|
const historyHtml = filteredHistory.length === 0
|
||||||
const statusIcon = exec.status === 'success' ? 'check-circle' :
|
? `<div class="empty-state">
|
||||||
exec.status === 'timeout' ? 'clock' : 'x-circle';
|
<i data-lucide="search-x" class="w-6 h-6"></i>
|
||||||
const statusClass = exec.status === 'success' ? 'text-success' :
|
<p>No matching results</p>
|
||||||
exec.status === 'timeout' ? 'text-warning' : 'text-destructive';
|
</div>`
|
||||||
const duration = formatDuration(exec.duration_ms);
|
: filteredHistory.map(exec => {
|
||||||
const timeAgo = getTimeAgo(new Date(exec.timestamp));
|
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 `
|
return `
|
||||||
<div class="cli-history-item" onclick="showExecutionDetail('${exec.id}')">
|
<div class="cli-history-item">
|
||||||
<div class="cli-history-item-header">
|
<div class="cli-history-item-content" onclick="showExecutionDetail('${exec.id}')">
|
||||||
<span class="cli-tool-tag cli-tool-${exec.tool}">${exec.tool}</span>
|
<div class="cli-history-item-header">
|
||||||
<span class="cli-history-time">${timeAgo}</span>
|
<span class="cli-tool-tag cli-tool-${exec.tool}">${exec.tool}</span>
|
||||||
<i data-lucide="${statusIcon}" class="${statusClass}"></i>
|
<span class="cli-history-time">${timeAgo}</span>
|
||||||
</div>
|
<i data-lucide="${statusIcon}" class="w-3.5 h-3.5 ${statusClass}"></i>
|
||||||
<div class="cli-history-prompt">${escapeHtml(exec.prompt_preview)}</div>
|
</div>
|
||||||
<div class="cli-history-meta">
|
<div class="cli-history-prompt">${escapeHtml(exec.prompt_preview)}</div>
|
||||||
<span class="text-muted-foreground">${duration}</span>
|
<div class="cli-history-meta">
|
||||||
</div>
|
<span>${duration}</span>
|
||||||
</div>
|
<span>${exec.mode || 'analysis'}</span>
|
||||||
`;
|
</div>
|
||||||
}).join('');
|
</div>
|
||||||
|
<div class="cli-history-actions">
|
||||||
|
<button class="btn-icon" onclick="event.stopPropagation(); showExecutionDetail('${exec.id}')" title="View Details">
|
||||||
|
<i data-lucide="eye" class="w-3.5 h-3.5"></i>
|
||||||
|
</button>
|
||||||
|
<button class="btn-icon btn-danger" onclick="event.stopPropagation(); confirmDeleteExecution('${exec.id}')" title="Delete">
|
||||||
|
<i data-lucide="trash-2" class="w-3.5 h-3.5"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
<div class="cli-history-header">
|
<div class="cli-history-header">
|
||||||
<h3>Execution History</h3>
|
<h3>Execution History</h3>
|
||||||
<div class="cli-history-controls">
|
<div class="cli-history-controls">
|
||||||
|
${renderHistorySearch()}
|
||||||
${renderToolFilter()}
|
${renderToolFilter()}
|
||||||
<button class="btn-icon" onclick="refreshCliHistory()" title="Refresh">
|
<button class="btn-icon" onclick="refreshCliHistory()" title="Refresh">
|
||||||
<i data-lucide="refresh-cw"></i>
|
<i data-lucide="refresh-cw" class="w-4 h-4"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -106,6 +133,17 @@ function renderCliHistory() {
|
|||||||
if (window.lucide) lucide.createIcons();
|
if (window.lucide) lucide.createIcons();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderHistorySearch() {
|
||||||
|
return `
|
||||||
|
<input type="text"
|
||||||
|
class="cli-history-search"
|
||||||
|
placeholder="Search history..."
|
||||||
|
value="${escapeHtml(cliHistorySearch)}"
|
||||||
|
onkeyup="searchCliHistory(this.value)"
|
||||||
|
oninput="searchCliHistory(this.value)">
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
function renderToolFilter() {
|
function renderToolFilter() {
|
||||||
const tools = ['all', 'gemini', 'qwen', 'codex'];
|
const tools = ['all', 'gemini', 'qwen', 'codex'];
|
||||||
return `
|
return `
|
||||||
@@ -135,30 +173,41 @@ async function showExecutionDetail(executionId) {
|
|||||||
<span class="text-muted-foreground">${formatDuration(detail.duration_ms)}</span>
|
<span class="text-muted-foreground">${formatDuration(detail.duration_ms)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="cli-detail-meta">
|
<div class="cli-detail-meta">
|
||||||
<span class="text-muted-foreground">Model: ${detail.model || 'default'}</span>
|
<span><i data-lucide="cpu" class="w-3 h-3"></i> ${detail.model || 'default'}</span>
|
||||||
<span class="text-muted-foreground">Mode: ${detail.mode}</span>
|
<span><i data-lucide="toggle-right" class="w-3 h-3"></i> ${detail.mode}</span>
|
||||||
<span class="text-muted-foreground">${new Date(detail.timestamp).toLocaleString()}</span>
|
<span><i data-lucide="calendar" class="w-3 h-3"></i> ${new Date(detail.timestamp).toLocaleString()}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="cli-detail-section">
|
<div class="cli-detail-section">
|
||||||
<h4>Prompt</h4>
|
<h4><i data-lucide="message-square"></i> Prompt</h4>
|
||||||
<pre class="cli-detail-prompt">${escapeHtml(detail.prompt)}</pre>
|
<pre class="cli-detail-prompt">${escapeHtml(detail.prompt)}</pre>
|
||||||
</div>
|
</div>
|
||||||
${detail.output.stdout ? `
|
${detail.output.stdout ? `
|
||||||
<div class="cli-detail-section">
|
<div class="cli-detail-section">
|
||||||
<h4>Output</h4>
|
<h4><i data-lucide="terminal"></i> Output</h4>
|
||||||
<pre class="cli-detail-output">${escapeHtml(detail.output.stdout)}</pre>
|
<pre class="cli-detail-output">${escapeHtml(detail.output.stdout)}</pre>
|
||||||
</div>
|
</div>
|
||||||
` : ''}
|
` : ''}
|
||||||
${detail.output.stderr ? `
|
${detail.output.stderr ? `
|
||||||
<div class="cli-detail-section">
|
<div class="cli-detail-section">
|
||||||
<h4>Errors</h4>
|
<h4><i data-lucide="alert-triangle"></i> Errors</h4>
|
||||||
<pre class="cli-detail-error">${escapeHtml(detail.output.stderr)}</pre>
|
<pre class="cli-detail-error">${escapeHtml(detail.output.stderr)}</pre>
|
||||||
</div>
|
</div>
|
||||||
` : ''}
|
` : ''}
|
||||||
${detail.output.truncated ? `
|
${detail.output.truncated ? `
|
||||||
<p class="text-warning">Output was truncated due to size.</p>
|
<p class="text-warning" style="font-size: 0.75rem; margin-top: 0.5rem;">
|
||||||
|
<i data-lucide="info" class="w-3 h-3" style="display: inline;"></i>
|
||||||
|
Output was truncated due to size.
|
||||||
|
</p>
|
||||||
` : ''}
|
` : ''}
|
||||||
|
<div class="cli-detail-actions">
|
||||||
|
<button class="btn btn-sm btn-outline" onclick="copyExecutionPrompt('${executionId}')">
|
||||||
|
<i data-lucide="copy" class="w-3.5 h-3.5"></i> Copy Prompt
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-sm btn-outline btn-danger" onclick="confirmDeleteExecution('${executionId}'); closeModal();">
|
||||||
|
<i data-lucide="trash-2" class="w-3.5 h-3.5"></i> Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
showModal('Execution Detail', modalContent);
|
showModal('Execution Detail', modalContent);
|
||||||
@@ -171,12 +220,69 @@ async function filterCliHistory(tool) {
|
|||||||
renderCliHistory();
|
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() {
|
async function refreshCliHistory() {
|
||||||
await loadCliHistory();
|
await loadCliHistory();
|
||||||
renderCliHistory();
|
renderCliHistory();
|
||||||
showRefreshToast('History refreshed', 'success');
|
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 ==========
|
// ========== Helpers ==========
|
||||||
function formatDuration(ms) {
|
function formatDuration(ms) {
|
||||||
if (ms >= 60000) {
|
if (ms >= 60000) {
|
||||||
|
|||||||
@@ -87,6 +87,11 @@ function addGlobalNotification(type, message, details = null, source = null) {
|
|||||||
globalNotificationQueue = globalNotificationQueue.slice(0, 100);
|
globalNotificationQueue = globalNotificationQueue.slice(0, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Persist to localStorage
|
||||||
|
if (typeof saveNotificationsToStorage === 'function') {
|
||||||
|
saveNotificationsToStorage();
|
||||||
|
}
|
||||||
|
|
||||||
renderGlobalNotifications();
|
renderGlobalNotifications();
|
||||||
updateGlobalNotifBadge();
|
updateGlobalNotifBadge();
|
||||||
|
|
||||||
@@ -204,6 +209,12 @@ function updateGlobalNotifBadge() {
|
|||||||
*/
|
*/
|
||||||
function clearGlobalNotifications() {
|
function clearGlobalNotifications() {
|
||||||
globalNotificationQueue = [];
|
globalNotificationQueue = [];
|
||||||
|
|
||||||
|
// Clear from localStorage
|
||||||
|
if (typeof saveNotificationsToStorage === 'function') {
|
||||||
|
saveNotificationsToStorage();
|
||||||
|
}
|
||||||
|
|
||||||
renderGlobalNotifications();
|
renderGlobalNotifications();
|
||||||
updateGlobalNotifBadge();
|
updateGlobalNotifBadge();
|
||||||
}
|
}
|
||||||
@@ -213,6 +224,12 @@ function clearGlobalNotifications() {
|
|||||||
*/
|
*/
|
||||||
function markAllNotificationsRead() {
|
function markAllNotificationsRead() {
|
||||||
globalNotificationQueue.forEach(n => n.read = true);
|
globalNotificationQueue.forEach(n => n.read = true);
|
||||||
|
|
||||||
|
// Save to localStorage
|
||||||
|
if (typeof saveNotificationsToStorage === 'function') {
|
||||||
|
saveNotificationsToStorage();
|
||||||
|
}
|
||||||
|
|
||||||
renderGlobalNotifications();
|
renderGlobalNotifications();
|
||||||
updateGlobalNotifBadge();
|
updateGlobalNotifBadge();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,56 @@
|
|||||||
// MODAL DIALOGS
|
// 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 = `
|
||||||
|
<div class="generic-modal ${options.size || ''}">
|
||||||
|
<div class="generic-modal-header">
|
||||||
|
<h3 class="generic-modal-title">${escapeHtml(title)}</h3>
|
||||||
|
<button class="generic-modal-close" onclick="closeModal()">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="generic-modal-body">
|
||||||
|
${content}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
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
|
// SVG Icons
|
||||||
const icons = {
|
const icons = {
|
||||||
folder: '<svg viewBox="0 0 24 24"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg>',
|
folder: '<svg viewBox="0 0 24 24"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg>',
|
||||||
|
|||||||
@@ -116,6 +116,8 @@ function updateContentTitle() {
|
|||||||
titleEl.textContent = 'MCP Server Management';
|
titleEl.textContent = 'MCP Server Management';
|
||||||
} else if (currentView === 'explorer') {
|
} else if (currentView === 'explorer') {
|
||||||
titleEl.textContent = 'File Explorer';
|
titleEl.textContent = 'File Explorer';
|
||||||
|
} else if (currentView === 'cli-manager') {
|
||||||
|
titleEl.textContent = 'CLI Tools & CCW';
|
||||||
} else if (currentView === 'liteTasks') {
|
} else if (currentView === 'liteTasks') {
|
||||||
const names = { 'lite-plan': 'Lite Plan Sessions', 'lite-fix': 'Lite Fix Sessions' };
|
const names = { 'lite-plan': 'Lite Plan Sessions', 'lite-fix': 'Lite Fix Sessions' };
|
||||||
titleEl.textContent = names[currentLiteType] || 'Lite Tasks';
|
titleEl.textContent = names[currentLiteType] || 'Lite Tasks';
|
||||||
|
|||||||
@@ -3,6 +3,42 @@
|
|||||||
// ==========================================
|
// ==========================================
|
||||||
// Real-time silent refresh (no notification bubbles)
|
// 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 wsConnection = null;
|
||||||
let autoRefreshInterval = null;
|
let autoRefreshInterval = null;
|
||||||
let lastDataHash = null;
|
let lastDataHash = null;
|
||||||
@@ -132,9 +168,7 @@ function handleToolExecutionNotification(payload) {
|
|||||||
notifType = 'info';
|
notifType = 'info';
|
||||||
message = `Executing ${toolName}...`;
|
message = `Executing ${toolName}...`;
|
||||||
if (params) {
|
if (params) {
|
||||||
// Show truncated params
|
details = formatJsonDetails(params, 150);
|
||||||
const paramStr = JSON.stringify(params);
|
|
||||||
details = paramStr.length > 100 ? paramStr.substring(0, 100) + '...' : paramStr;
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -142,12 +176,10 @@ function handleToolExecutionNotification(payload) {
|
|||||||
notifType = 'success';
|
notifType = 'success';
|
||||||
message = `${toolName} completed`;
|
message = `${toolName} completed`;
|
||||||
if (result) {
|
if (result) {
|
||||||
// Show truncated result
|
|
||||||
if (result._truncated) {
|
if (result._truncated) {
|
||||||
details = result.preview;
|
details = result.preview;
|
||||||
} else {
|
} else {
|
||||||
const resultStr = JSON.stringify(result);
|
details = formatJsonDetails(result, 200);
|
||||||
details = resultStr.length > 150 ? resultStr.substring(0, 150) + '...' : resultStr;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -37,9 +37,45 @@ const liteTaskDataStore = {};
|
|||||||
const taskJsonStore = {};
|
const taskJsonStore = {};
|
||||||
|
|
||||||
// ========== Global Notification Queue ==========
|
// ========== Global Notification Queue ==========
|
||||||
// Notification queue visible from any view
|
// Notification queue visible from any view (persisted to localStorage)
|
||||||
let globalNotificationQueue = [];
|
const NOTIFICATION_STORAGE_KEY = 'ccw_notifications';
|
||||||
|
const NOTIFICATION_MAX_STORED = 100;
|
||||||
|
|
||||||
|
// Load notifications from localStorage on init
|
||||||
|
let globalNotificationQueue = loadNotificationsFromStorage();
|
||||||
let isNotificationPanelVisible = false;
|
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 ==========
|
// ========== Event Handler ==========
|
||||||
/**
|
/**
|
||||||
* Handle granular workflow events from CLI
|
* Handle granular workflow events from CLI
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
// CLI Manager View
|
// CLI Manager View
|
||||||
// Main view combining CLI status and history panels
|
// Main view combining CLI status, CCW installations, and history panels
|
||||||
|
|
||||||
// ========== CLI Manager State ==========
|
// ========== CLI Manager State ==========
|
||||||
let currentCliExecution = null;
|
var currentCliExecution = null;
|
||||||
let cliExecutionOutput = '';
|
var cliExecutionOutput = '';
|
||||||
|
var ccwInstallations = [];
|
||||||
|
|
||||||
// ========== Initialization ==========
|
// ========== Initialization ==========
|
||||||
function initCliManager() {
|
function initCliManager() {
|
||||||
// Initialize CLI navigation
|
document.querySelectorAll('.nav-item[data-view="cli-manager"]').forEach(function(item) {
|
||||||
document.querySelectorAll('.nav-item[data-view="cli-manager"]').forEach(item => {
|
item.addEventListener('click', function() {
|
||||||
item.addEventListener('click', () => {
|
|
||||||
setActiveNavItem(item);
|
setActiveNavItem(item);
|
||||||
currentView = 'cli-manager';
|
currentView = 'cli-manager';
|
||||||
currentFilter = null;
|
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 ==========
|
// ========== Rendering ==========
|
||||||
async function renderCliManager() {
|
async function renderCliManager() {
|
||||||
const mainContent = document.querySelector('.main-content');
|
var container = document.getElementById('mainContent');
|
||||||
if (!mainContent) return;
|
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
|
// Load data
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
loadCliToolStatus(),
|
loadCliToolStatus(),
|
||||||
loadCliHistory()
|
loadCliHistory(),
|
||||||
|
loadCcwInstallations()
|
||||||
]);
|
]);
|
||||||
|
|
||||||
mainContent.innerHTML = `
|
container.innerHTML = '<div class="cli-manager-container">' +
|
||||||
<div class="cli-manager-container">
|
'<div class="cli-manager-grid">' +
|
||||||
<div class="cli-manager-grid">
|
'<div class="cli-panel"><div id="cli-status-panel"></div></div>' +
|
||||||
<!-- Status Panel -->
|
'<div class="cli-panel"><div id="ccw-install-panel"></div></div>' +
|
||||||
<div class="cli-panel">
|
'</div>' +
|
||||||
<div id="cli-status-panel"></div>
|
'<div class="cli-panel cli-panel-full"><div id="cli-history-panel"></div></div>' +
|
||||||
</div>
|
'</div>';
|
||||||
|
|
||||||
<!-- Quick Execute Panel -->
|
|
||||||
<div class="cli-panel">
|
|
||||||
<div id="cli-execute-panel"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- History Panel -->
|
|
||||||
<div class="cli-panel cli-panel-full">
|
|
||||||
<div id="cli-history-panel"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Live Output Panel (shown during execution) -->
|
|
||||||
<div class="cli-panel cli-panel-full ${currentCliExecution ? '' : 'hidden'}" id="cli-output-panel">
|
|
||||||
<div class="cli-output-header">
|
|
||||||
<h3>Execution Output</h3>
|
|
||||||
<div class="cli-output-status">
|
|
||||||
<span id="cli-output-status-indicator" class="status-indicator running"></span>
|
|
||||||
<span id="cli-output-status-text">Running...</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<pre class="cli-output-content" id="cli-output-content"></pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
// Render sub-panels
|
// Render sub-panels
|
||||||
renderCliStatus();
|
renderCliStatus();
|
||||||
renderCliExecutePanel();
|
renderCcwInstallPanel();
|
||||||
renderCliHistory();
|
renderCliHistory();
|
||||||
|
|
||||||
// Initialize Lucide icons
|
// Initialize Lucide icons
|
||||||
if (window.lucide) {
|
if (window.lucide) lucide.createIcons();
|
||||||
lucide.createIcons();
|
}
|
||||||
|
|
||||||
|
// CCW Install Carousel State
|
||||||
|
var ccwCarouselIndex = 0;
|
||||||
|
|
||||||
|
function renderCcwInstallPanel() {
|
||||||
|
var container = document.getElementById('ccw-install-panel');
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
var html = '<div class="cli-status-header"><h3>CCW Installations</h3>' +
|
||||||
|
'<div class="ccw-header-actions">' +
|
||||||
|
'<button class="btn-icon" onclick="showCcwInstallModal()" title="Add Installation">' +
|
||||||
|
'<i data-lucide="plus" class="w-4 h-4"></i></button>' +
|
||||||
|
'<button class="btn-icon" onclick="loadCcwInstallations().then(function() { renderCcwInstallPanel(); })" title="Refresh">' +
|
||||||
|
'<i data-lucide="refresh-cw" class="w-4 h-4"></i></button>' +
|
||||||
|
'</div></div>' +
|
||||||
|
'<div class="ccw-install-content">';
|
||||||
|
|
||||||
|
if (ccwInstallations.length === 0) {
|
||||||
|
html += '<div class="ccw-empty-state">' +
|
||||||
|
'<i data-lucide="package-x" class="w-8 h-8"></i>' +
|
||||||
|
'<p>No installations found</p>' +
|
||||||
|
'<button class="btn btn-sm btn-primary" onclick="showCcwInstallModal()">' +
|
||||||
|
'<i data-lucide="download" class="w-3 h-3"></i> Install CCW</button></div>';
|
||||||
|
} else {
|
||||||
|
// Carousel container
|
||||||
|
html += '<div class="ccw-carousel-wrapper">';
|
||||||
|
|
||||||
|
// Left arrow (show only if more than 1 installation)
|
||||||
|
if (ccwInstallations.length > 1) {
|
||||||
|
html += '<button class="ccw-carousel-btn ccw-carousel-prev" onclick="ccwCarouselPrev()" title="Previous">' +
|
||||||
|
'<i data-lucide="chevron-left" class="w-4 h-4"></i></button>';
|
||||||
|
}
|
||||||
|
|
||||||
|
html += '<div class="ccw-carousel-track" id="ccwCarouselTrack">';
|
||||||
|
|
||||||
|
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 += '<div class="ccw-carousel-card ' + activeClass + '" data-index="' + i + '">' +
|
||||||
|
'<div class="ccw-card-header">' +
|
||||||
|
'<div class="ccw-card-mode ' + (isGlobal ? 'global' : 'path') + '">' +
|
||||||
|
'<i data-lucide="' + modeIcon + '" class="w-4 h-4"></i>' +
|
||||||
|
'<span>' + inst.installation_mode + '</span>' +
|
||||||
|
'</div>' +
|
||||||
|
'<span class="ccw-version-tag">v' + version + '</span>' +
|
||||||
|
'</div>' +
|
||||||
|
'<div class="ccw-card-path" title="' + inst.installation_path + '">' + escapeHtml(inst.installation_path) + '</div>' +
|
||||||
|
'<div class="ccw-card-meta">' +
|
||||||
|
'<span><i data-lucide="calendar" class="w-3 h-3"></i> ' + installDate + '</span>' +
|
||||||
|
'<span><i data-lucide="file" class="w-3 h-3"></i> ' + (inst.files_count || 0) + ' files</span>' +
|
||||||
|
'</div>' +
|
||||||
|
'<div class="ccw-card-actions">' +
|
||||||
|
'<button class="btn-icon" onclick="runCcwUpgrade()" title="Upgrade">' +
|
||||||
|
'<i data-lucide="arrow-up-circle" class="w-4 h-4"></i></button>' +
|
||||||
|
'<button class="btn-icon btn-danger" onclick="confirmCcwUninstall(\'' + escapeHtml(inst.installation_path) + '\')" title="Uninstall">' +
|
||||||
|
'<i data-lucide="trash-2" class="w-4 h-4"></i></button>' +
|
||||||
|
'</div>' +
|
||||||
|
'</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
html += '</div>';
|
||||||
|
|
||||||
|
// Right arrow (show only if more than 1 installation)
|
||||||
|
if (ccwInstallations.length > 1) {
|
||||||
|
html += '<button class="ccw-carousel-btn ccw-carousel-next" onclick="ccwCarouselNext()" title="Next">' +
|
||||||
|
'<i data-lucide="chevron-right" class="w-4 h-4"></i></button>';
|
||||||
|
}
|
||||||
|
|
||||||
|
html += '</div>';
|
||||||
|
|
||||||
|
// Dots indicator (show only if more than 1 installation)
|
||||||
|
if (ccwInstallations.length > 1) {
|
||||||
|
html += '<div class="ccw-carousel-dots">';
|
||||||
|
for (var j = 0; j < ccwInstallations.length; j++) {
|
||||||
|
var dotActive = j === ccwCarouselIndex ? 'active' : '';
|
||||||
|
html += '<button class="ccw-carousel-dot ' + dotActive + '" onclick="ccwCarouselGoTo(' + j + ')"></button>';
|
||||||
|
}
|
||||||
|
html += '</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
html += '</div>';
|
||||||
|
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 = '<div class="ccw-install-modal">' +
|
||||||
|
'<div class="ccw-install-options">' +
|
||||||
|
'<div class="ccw-install-option" onclick="selectCcwInstallMode(\'Global\')">' +
|
||||||
|
'<div class="ccw-option-icon global"><i data-lucide="home" class="w-6 h-6"></i></div>' +
|
||||||
|
'<div class="ccw-option-info">' +
|
||||||
|
'<div class="ccw-option-title">Global Installation</div>' +
|
||||||
|
'<div class="ccw-option-desc">Install to user home directory (~/.claude)</div>' +
|
||||||
|
'</div>' +
|
||||||
|
'<i data-lucide="chevron-right" class="w-4 h-4 text-muted-foreground"></i>' +
|
||||||
|
'</div>' +
|
||||||
|
'<div class="ccw-install-option" onclick="toggleCcwPathInput()">' +
|
||||||
|
'<div class="ccw-option-icon path"><i data-lucide="folder" class="w-6 h-6"></i></div>' +
|
||||||
|
'<div class="ccw-option-info">' +
|
||||||
|
'<div class="ccw-option-title">Path Installation</div>' +
|
||||||
|
'<div class="ccw-option-desc">Install to a specific project folder</div>' +
|
||||||
|
'</div>' +
|
||||||
|
'<i data-lucide="chevron-right" class="w-4 h-4 text-muted-foreground"></i>' +
|
||||||
|
'</div>' +
|
||||||
|
'</div>' +
|
||||||
|
'<div class="ccw-path-input-section hidden" id="ccwPathInputSection">' +
|
||||||
|
'<div class="ccw-path-input-group">' +
|
||||||
|
'<label>Installation Path</label>' +
|
||||||
|
'<input type="text" id="ccwInstallPath" class="cli-textarea" placeholder="D:/projects/my-project" value="' + (projectPath || '') + '">' +
|
||||||
|
'</div>' +
|
||||||
|
'<div class="ccw-install-action">' +
|
||||||
|
'<button class="btn btn-primary" onclick="executeCcwInstall()">' +
|
||||||
|
'<i data-lucide="download" class="w-4 h-4"></i> Install to Path</button>' +
|
||||||
|
'</div>' +
|
||||||
|
'</div>' +
|
||||||
|
'</div>';
|
||||||
|
|
||||||
|
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() {
|
function renderCliExecutePanel() {
|
||||||
const container = document.getElementById('cli-execute-panel');
|
var container = document.getElementById('cli-execute-panel');
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
|
|
||||||
const tools = ['gemini', 'qwen', 'codex'];
|
var tools = ['gemini', 'qwen', 'codex'];
|
||||||
const modes = ['analysis', 'write', 'auto'];
|
var modes = ['analysis', 'write', 'auto'];
|
||||||
|
|
||||||
container.innerHTML = `
|
|
||||||
<div class="cli-execute-header">
|
|
||||||
<h3>Quick Execute</h3>
|
|
||||||
</div>
|
|
||||||
<div class="cli-execute-form">
|
|
||||||
<div class="cli-execute-row">
|
|
||||||
<div class="cli-form-group">
|
|
||||||
<label for="cli-exec-tool">Tool</label>
|
|
||||||
<select id="cli-exec-tool" class="cli-select">
|
|
||||||
${tools.map(tool => `
|
|
||||||
<option value="${tool}" ${tool === defaultCliTool ? 'selected' : ''}>
|
|
||||||
${tool.charAt(0).toUpperCase() + tool.slice(1)}
|
|
||||||
</option>
|
|
||||||
`).join('')}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="cli-form-group">
|
|
||||||
<label for="cli-exec-mode">Mode</label>
|
|
||||||
<select id="cli-exec-mode" class="cli-select">
|
|
||||||
${modes.map(mode => `
|
|
||||||
<option value="${mode}" ${mode === 'analysis' ? 'selected' : ''}>
|
|
||||||
${mode.charAt(0).toUpperCase() + mode.slice(1)}
|
|
||||||
</option>
|
|
||||||
`).join('')}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="cli-form-group">
|
|
||||||
<label for="cli-exec-prompt">Prompt</label>
|
|
||||||
<textarea id="cli-exec-prompt" class="cli-textarea" placeholder="Enter your prompt..."></textarea>
|
|
||||||
</div>
|
|
||||||
<div class="cli-execute-actions">
|
|
||||||
<button class="btn btn-primary" onclick="executeCliFromDashboard()" ${currentCliExecution ? 'disabled' : ''}>
|
|
||||||
<i data-lucide="play"></i>
|
|
||||||
Execute
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
|
var html = '<div class="cli-execute-header"><h3>Quick Execute</h3></div>' +
|
||||||
|
'<div class="cli-execute-form"><div class="cli-execute-row">' +
|
||||||
|
'<div class="cli-form-group"><label for="cli-exec-tool">Tool</label>' +
|
||||||
|
'<select id="cli-exec-tool" class="cli-select">';
|
||||||
|
for (var i = 0; i < tools.length; i++) {
|
||||||
|
var tool = tools[i];
|
||||||
|
var selected = tool === defaultCliTool ? 'selected' : '';
|
||||||
|
html += '<option value="' + tool + '" ' + selected + '>' + tool.charAt(0).toUpperCase() + tool.slice(1) + '</option>';
|
||||||
|
}
|
||||||
|
html += '</select></div>' +
|
||||||
|
'<div class="cli-form-group"><label for="cli-exec-mode">Mode</label>' +
|
||||||
|
'<select id="cli-exec-mode" class="cli-select">';
|
||||||
|
for (var j = 0; j < modes.length; j++) {
|
||||||
|
var mode = modes[j];
|
||||||
|
var sel = mode === 'analysis' ? 'selected' : '';
|
||||||
|
html += '<option value="' + mode + '" ' + sel + '>' + mode.charAt(0).toUpperCase() + mode.slice(1) + '</option>';
|
||||||
|
}
|
||||||
|
html += '</select></div></div>' +
|
||||||
|
'<div class="cli-form-group"><label for="cli-exec-prompt">Prompt</label>' +
|
||||||
|
'<textarea id="cli-exec-prompt" class="cli-textarea" placeholder="Enter your prompt..."></textarea></div>' +
|
||||||
|
'<div class="cli-execute-actions">' +
|
||||||
|
'<button class="btn btn-primary" onclick="executeCliFromDashboard()" ' + (currentCliExecution ? 'disabled' : '') + '>' +
|
||||||
|
'<i data-lucide="play" class="w-4 h-4"></i> Execute</button></div></div>';
|
||||||
|
container.innerHTML = html;
|
||||||
if (window.lucide) lucide.createIcons();
|
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 ==========
|
// ========== Execution ==========
|
||||||
async function executeCliFromDashboard() {
|
async function executeCliFromDashboard() {
|
||||||
const tool = document.getElementById('cli-exec-tool').value;
|
var toolEl = document.getElementById('cli-exec-tool');
|
||||||
const mode = document.getElementById('cli-exec-mode').value;
|
var modeEl = document.getElementById('cli-exec-mode');
|
||||||
const prompt = document.getElementById('cli-exec-prompt').value.trim();
|
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) {
|
if (!prompt) {
|
||||||
showRefreshToast('Please enter a prompt', 'error');
|
showRefreshToast('Please enter a prompt', 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show output panel
|
currentCliExecution = { tool: tool, mode: mode, prompt: prompt, startTime: Date.now() };
|
||||||
currentCliExecution = { tool, mode, prompt, startTime: Date.now() };
|
|
||||||
cliExecutionOutput = '';
|
cliExecutionOutput = '';
|
||||||
|
|
||||||
const outputPanel = document.getElementById('cli-output-panel');
|
var outputPanel = document.getElementById('cli-output-panel');
|
||||||
const outputContent = document.getElementById('cli-output-content');
|
var outputContent = document.getElementById('cli-output-content');
|
||||||
const statusIndicator = document.getElementById('cli-output-status-indicator');
|
var statusIndicator = document.getElementById('cli-output-status-indicator');
|
||||||
const statusText = document.getElementById('cli-output-status-text');
|
var statusText = document.getElementById('cli-output-status-text');
|
||||||
|
|
||||||
if (outputPanel) outputPanel.classList.remove('hidden');
|
if (outputPanel) outputPanel.classList.remove('hidden');
|
||||||
if (outputContent) outputContent.textContent = '';
|
if (outputContent) outputContent.textContent = '';
|
||||||
if (statusIndicator) {
|
if (statusIndicator) statusIndicator.className = 'status-indicator running';
|
||||||
statusIndicator.className = 'status-indicator running';
|
|
||||||
}
|
|
||||||
if (statusText) statusText.textContent = 'Running...';
|
if (statusText) statusText.textContent = 'Running...';
|
||||||
|
|
||||||
// Disable execute button
|
var execBtn = document.querySelector('.cli-execute-actions .btn-primary');
|
||||||
const execBtn = document.querySelector('.cli-execute-actions .btn-primary');
|
|
||||||
if (execBtn) execBtn.disabled = true;
|
if (execBtn) execBtn.disabled = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/cli/execute', {
|
var response = await fetch('/api/cli/execute', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({ tool: tool, mode: mode, prompt: prompt, dir: projectPath })
|
||||||
tool,
|
|
||||||
mode,
|
|
||||||
prompt,
|
|
||||||
dir: projectPath
|
|
||||||
})
|
|
||||||
});
|
});
|
||||||
|
var result = await response.json();
|
||||||
|
|
||||||
const result = await response.json();
|
if (statusIndicator) statusIndicator.className = 'status-indicator ' + (result.success ? 'success' : 'error');
|
||||||
|
|
||||||
// Update status
|
|
||||||
if (statusIndicator) {
|
|
||||||
statusIndicator.className = `status-indicator ${result.success ? 'success' : 'error'}`;
|
|
||||||
}
|
|
||||||
if (statusText) {
|
if (statusText) {
|
||||||
const duration = formatDuration(result.execution?.duration_ms || (Date.now() - currentCliExecution.startTime));
|
var duration = formatDuration(result.execution ? result.execution.duration_ms : (Date.now() - currentCliExecution.startTime));
|
||||||
statusText.textContent = result.success
|
statusText.textContent = result.success ? 'Completed in ' + duration : 'Failed: ' + (result.error || 'Unknown');
|
||||||
? `Completed in ${duration}`
|
|
||||||
: `Failed: ${result.error || 'Unknown error'}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refresh history
|
|
||||||
await loadCliHistory();
|
await loadCliHistory();
|
||||||
renderCliHistory();
|
renderCliHistory();
|
||||||
|
showRefreshToast(result.success ? 'Completed' : (result.error || 'Failed'), result.success ? 'success' : 'error');
|
||||||
if (result.success) {
|
|
||||||
showRefreshToast('Execution completed', 'success');
|
|
||||||
} else {
|
|
||||||
showRefreshToast(result.error || 'Execution failed', 'error');
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (statusIndicator) {
|
if (statusIndicator) statusIndicator.className = 'status-indicator error';
|
||||||
statusIndicator.className = 'status-indicator error';
|
if (statusText) statusText.textContent = 'Error: ' + error.message;
|
||||||
}
|
showRefreshToast('Error: ' + error.message, 'error');
|
||||||
if (statusText) {
|
|
||||||
statusText.textContent = `Error: ${error.message}`;
|
|
||||||
}
|
|
||||||
showRefreshToast(`Execution error: ${error.message}`, 'error');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
currentCliExecution = null;
|
currentCliExecution = null;
|
||||||
|
|
||||||
// Re-enable execute button
|
|
||||||
if (execBtn) execBtn.disabled = false;
|
if (execBtn) execBtn.disabled = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== WebSocket Event Handlers ==========
|
// ========== WebSocket Event Handlers ==========
|
||||||
function handleCliExecutionStarted(payload) {
|
function handleCliExecutionStarted(payload) {
|
||||||
const { executionId, tool, mode, timestamp } = payload;
|
currentCliExecution = {
|
||||||
currentCliExecution = { executionId, tool, mode, startTime: new Date(timestamp).getTime() };
|
executionId: payload.executionId,
|
||||||
|
tool: payload.tool,
|
||||||
|
mode: payload.mode,
|
||||||
|
startTime: new Date(payload.timestamp).getTime()
|
||||||
|
};
|
||||||
cliExecutionOutput = '';
|
cliExecutionOutput = '';
|
||||||
|
|
||||||
// Show output panel if in CLI manager view
|
|
||||||
if (currentView === 'cli-manager') {
|
if (currentView === 'cli-manager') {
|
||||||
const outputPanel = document.getElementById('cli-output-panel');
|
var outputPanel = document.getElementById('cli-output-panel');
|
||||||
const outputContent = document.getElementById('cli-output-content');
|
var outputContent = document.getElementById('cli-output-content');
|
||||||
const statusIndicator = document.getElementById('cli-output-status-indicator');
|
var statusIndicator = document.getElementById('cli-output-status-indicator');
|
||||||
const statusText = document.getElementById('cli-output-status-text');
|
var statusText = document.getElementById('cli-output-status-text');
|
||||||
|
|
||||||
if (outputPanel) outputPanel.classList.remove('hidden');
|
if (outputPanel) outputPanel.classList.remove('hidden');
|
||||||
if (outputContent) outputContent.textContent = '';
|
if (outputContent) outputContent.textContent = '';
|
||||||
if (statusIndicator) statusIndicator.className = 'status-indicator running';
|
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) {
|
function handleCliOutput(payload) {
|
||||||
const { data } = payload;
|
cliExecutionOutput += payload.data;
|
||||||
cliExecutionOutput += data;
|
var outputContent = document.getElementById('cli-output-content');
|
||||||
|
|
||||||
// Update output panel if visible
|
|
||||||
const outputContent = document.getElementById('cli-output-content');
|
|
||||||
if (outputContent) {
|
if (outputContent) {
|
||||||
outputContent.textContent = cliExecutionOutput;
|
outputContent.textContent = cliExecutionOutput;
|
||||||
// Auto-scroll to bottom
|
|
||||||
outputContent.scrollTop = outputContent.scrollHeight;
|
outputContent.scrollTop = outputContent.scrollHeight;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleCliExecutionCompleted(payload) {
|
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
|
if (statusIndicator) statusIndicator.className = 'status-indicator ' + (payload.success ? 'success' : 'error');
|
||||||
const statusIndicator = document.getElementById('cli-output-status-indicator');
|
if (statusText) statusText.textContent = payload.success ? 'Completed in ' + formatDuration(payload.duration_ms) : 'Failed: ' + payload.status;
|
||||||
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}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
currentCliExecution = null;
|
currentCliExecution = null;
|
||||||
|
|
||||||
// Refresh history
|
|
||||||
if (currentView === 'cli-manager') {
|
if (currentView === 'cli-manager') {
|
||||||
loadCliHistory().then(() => renderCliHistory());
|
loadCliHistory().then(function() { renderCliHistory(); });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleCliExecutionError(payload) {
|
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');
|
if (statusIndicator) statusIndicator.className = 'status-indicator error';
|
||||||
const statusText = document.getElementById('cli-output-status-text');
|
if (statusText) statusText.textContent = 'Error: ' + payload.error;
|
||||||
|
|
||||||
if (statusIndicator) {
|
|
||||||
statusIndicator.className = 'status-indicator error';
|
|
||||||
}
|
|
||||||
if (statusText) {
|
|
||||||
statusText.textContent = `Error: ${error}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
currentCliExecution = null;
|
currentCliExecution = null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,27 @@ let isTaskRunning = false;
|
|||||||
// Note: defaultCliTool is defined in components/cli-status.js
|
// 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
|
* Render the Explorer view
|
||||||
*/
|
*/
|
||||||
@@ -177,7 +198,7 @@ function renderTreeLevel(files, parentPath, depth) {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="tree-children ${isExpanded ? 'show' : ''}" id="children-${btoa(file.path).replace(/[^a-zA-Z0-9]/g, '')}">
|
<div class="tree-children ${isExpanded ? 'show' : ''}" id="children-${safeBase64Encode(file.path)}">
|
||||||
${isExpanded ? '' : ''}
|
${isExpanded ? '' : ''}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -298,35 +319,44 @@ function getFolderIcon(name, isExpanded, hasClaudeMd) {
|
|||||||
: '<i data-lucide="folder" class="w-4 h-4 text-warning"></i>';
|
: '<i data-lucide="folder" class="w-4 h-4 text-warning"></i>';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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() {
|
function attachTreeEventListeners() {
|
||||||
// Folder click - toggle expand
|
const treeContent = document.getElementById('explorerTreeContent');
|
||||||
document.querySelectorAll('.tree-folder > .tree-item-row').forEach(row => {
|
if (!treeContent || explorerEventsDelegated) return;
|
||||||
row.addEventListener('click', async (e) => {
|
|
||||||
const folder = row.closest('.tree-folder');
|
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;
|
const path = folder.dataset.path;
|
||||||
await toggleFolderExpand(path, folder);
|
await toggleFolderExpand(path, folder);
|
||||||
});
|
return;
|
||||||
});
|
}
|
||||||
|
|
||||||
// File click - preview
|
// Check if clicked on file
|
||||||
document.querySelectorAll('.tree-file').forEach(item => {
|
const fileItem = e.target.closest('.tree-file');
|
||||||
item.addEventListener('click', async () => {
|
if (fileItem) {
|
||||||
const path = item.dataset.path;
|
const path = fileItem.dataset.path;
|
||||||
await previewFile(path);
|
await previewFile(path);
|
||||||
|
|
||||||
// Update selection
|
// Update selection
|
||||||
document.querySelectorAll('.tree-item-row.selected, .tree-file.selected').forEach(el => {
|
document.querySelectorAll('.tree-item-row.selected, .tree-file.selected').forEach(el => {
|
||||||
el.classList.remove('selected');
|
el.classList.remove('selected');
|
||||||
});
|
});
|
||||||
item.classList.add('selected');
|
fileItem.classList.add('selected');
|
||||||
explorerSelectedFile = path;
|
explorerSelectedFile = path;
|
||||||
});
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Toggle folder expand/collapse
|
* Toggle folder expand/collapse
|
||||||
*/
|
*/
|
||||||
@@ -366,7 +396,6 @@ async function toggleFolderExpand(path, folderElement) {
|
|||||||
|
|
||||||
const depth = (path.match(/\//g) || []).length - (explorerCurrentPath.match(/\//g) || []).length + 1;
|
const depth = (path.match(/\//g) || []).length - (explorerCurrentPath.match(/\//g) || []).length + 1;
|
||||||
childrenContainer.innerHTML = renderTreeLevel(data.files, path, depth);
|
childrenContainer.innerHTML = renderTreeLevel(data.files, path, depth);
|
||||||
attachTreeEventListeners();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
childrenContainer.innerHTML = `<div class="tree-error">Failed to load</div>`;
|
childrenContainer.innerHTML = `<div class="tree-error">Failed to load</div>`;
|
||||||
}
|
}
|
||||||
@@ -483,6 +512,7 @@ async function refreshExplorerTree() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
explorerExpandedDirs.clear();
|
explorerExpandedDirs.clear();
|
||||||
|
explorerEventsDelegated = false;
|
||||||
await loadExplorerTree(explorerCurrentPath);
|
await loadExplorerTree(explorerCurrentPath);
|
||||||
|
|
||||||
if (btn) {
|
if (btn) {
|
||||||
|
|||||||
@@ -317,6 +317,11 @@
|
|||||||
<i data-lucide="folder-tree" class="nav-icon"></i>
|
<i data-lucide="folder-tree" class="nav-icon"></i>
|
||||||
<span class="nav-text flex-1">Explorer</span>
|
<span class="nav-text flex-1">Explorer</span>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="nav-item flex items-center gap-2 mx-2 px-3 py-2.5 text-sm text-muted-foreground hover:bg-hover hover:text-foreground rounded cursor-pointer transition-colors" data-view="cli-manager" data-tooltip="CLI Tools Status">
|
||||||
|
<i data-lucide="terminal" class="nav-icon"></i>
|
||||||
|
<span class="nav-text flex-1">Status</span>
|
||||||
|
<span class="badge px-2 py-0.5 text-xs font-semibold rounded-full bg-hover text-muted-foreground" id="badgeCliTools">0/3</span>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -395,20 +400,6 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- CLI Tools Section -->
|
|
||||||
<div class="mb-2" id="cliToolsNav">
|
|
||||||
<div class="flex items-center px-4 py-2 text-xs font-semibold text-muted-foreground uppercase tracking-wide">
|
|
||||||
<i data-lucide="terminal" class="nav-section-icon mr-2"></i>
|
|
||||||
<span class="nav-section-title">CLI Tools</span>
|
|
||||||
</div>
|
|
||||||
<ul class="space-y-0.5">
|
|
||||||
<li class="nav-item flex items-center gap-2 mx-2 px-3 py-2.5 text-sm text-muted-foreground hover:bg-hover hover:text-foreground rounded cursor-pointer transition-colors" data-view="cli-manager" data-tooltip="CLI Tools Management">
|
|
||||||
<i data-lucide="square-terminal" class="nav-icon"></i>
|
|
||||||
<span class="nav-text flex-1">Manage</span>
|
|
||||||
<span class="badge px-2 py-0.5 text-xs font-semibold rounded-full bg-hover text-muted-foreground" id="badgeCliTools">0/3</span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<!-- Sidebar Footer -->
|
<!-- Sidebar Footer -->
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { spawn } from 'child_process';
|
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 { join, dirname } from 'path';
|
||||||
import { homedir } from 'os';
|
import { homedir } from 'os';
|
||||||
|
|
||||||
@@ -436,6 +436,44 @@ export function getExecutionDetail(baseDir, executionId) {
|
|||||||
return null;
|
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
|
* CLI Executor Tool Definition
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user