mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-10 02:24:35 +08:00
feat: Enhance navigation and cleanup for graph explorer view
- Added a cleanup function to reset the state when navigating away from the graph explorer. - Updated navigation logic to call the cleanup function before switching views. - Improved internationalization by adding new translations for graph-related terms. - Adjusted icon sizes for better UI consistency in the graph explorer. - Implemented impact analysis button functionality in the graph explorer. - Refactored CLI tool configuration to use updated model names. - Enhanced CLI executor to handle prompts correctly for codex commands. - Introduced code relationship storage for better visualization in the index tree. - Added support for parsing Markdown and plain text files in the symbol parser. - Updated tests to reflect changes in language detection logic.
This commit is contained in:
@@ -2098,6 +2098,7 @@
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 50vw;
|
||||
min-width: 600px;
|
||||
max-width: 100vw;
|
||||
height: 100vh;
|
||||
background: hsl(var(--card));
|
||||
|
||||
@@ -34,13 +34,7 @@
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.graph-explorer-header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.graph-explorer-header-left h2 {
|
||||
.graph-explorer-header h2 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
@@ -50,6 +44,12 @@
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.graph-explorer-header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.graph-explorer-header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -190,27 +190,7 @@
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
* Graph Main Content
|
||||
* ======================================== */
|
||||
.graph-main {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
gap: 0;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Cytoscape Graph Canvas */
|
||||
.cytoscape-container {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
background: hsl(var(--card));
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 0.75rem;
|
||||
overflow: hidden;
|
||||
min-height: 400px;
|
||||
}
|
||||
/* Note: .graph-main and .cytoscape-container styles are defined in the Additional Classes section below */
|
||||
|
||||
#cy {
|
||||
width: 100%;
|
||||
@@ -1134,3 +1114,444 @@
|
||||
font-size: 0.875rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
* Additional Classes for JS Compatibility
|
||||
* ======================================== */
|
||||
|
||||
/* Explorer Tabs */
|
||||
.graph-explorer-tabs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: hsl(var(--muted-foreground));
|
||||
background: transparent;
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 0.375rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.tab-btn:hover {
|
||||
background: hsl(var(--hover));
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
.tab-btn.active {
|
||||
background: hsl(var(--primary));
|
||||
color: hsl(var(--primary-foreground));
|
||||
border-color: hsl(var(--primary));
|
||||
}
|
||||
|
||||
/* Tab Content */
|
||||
.graph-explorer-content {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
display: none;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.tab-content.active {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
/* Graph View Layout */
|
||||
.graph-view {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
gap: 1rem;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.graph-sidebar {
|
||||
width: 240px;
|
||||
min-width: 240px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
overflow-y: auto;
|
||||
padding: 0.5rem 1rem 0.5rem 0;
|
||||
border-right: 1px solid hsl(var(--border));
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.graph-main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.graph-controls-section,
|
||||
.graph-legend-section {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.graph-controls-section h3,
|
||||
.graph-legend-section h3 {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: hsl(var(--muted-foreground));
|
||||
margin: 0 0 0.75rem 0;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.graph-legend-section {
|
||||
border-top: 1px solid hsl(var(--border));
|
||||
padding-top: 1rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
/* Graph Legend */
|
||||
.graph-legend {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.legend-title {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: hsl(var(--muted-foreground));
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.legend-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.legend-line {
|
||||
width: 20px;
|
||||
height: 3px;
|
||||
border-radius: 2px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.filter-color {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Graph Toolbar */
|
||||
.graph-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.5rem 0.75rem;
|
||||
background: hsl(var(--card));
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 0.5rem 0.5rem 0 0;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.graph-toolbar-left,
|
||||
.graph-toolbar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.graph-stats {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
font-size: 0.8125rem;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 0.375rem;
|
||||
color: hsl(var(--muted-foreground));
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.btn-icon:hover {
|
||||
background: hsl(var(--hover));
|
||||
color: hsl(var(--foreground));
|
||||
border-color: hsl(var(--primary) / 0.3);
|
||||
}
|
||||
|
||||
/* Filter Checkboxes */
|
||||
.filter-dropdowns {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.filter-group > label {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: hsl(var(--muted-foreground));
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.filter-checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.8125rem;
|
||||
color: hsl(var(--foreground));
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.filter-checkbox input[type="checkbox"] {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
cursor: pointer;
|
||||
accent-color: hsl(var(--primary));
|
||||
}
|
||||
|
||||
/* Legend Items */
|
||||
.legend-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.legend-section-title {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: hsl(var(--muted-foreground));
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.8125rem;
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
.legend-color {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Search Process View */
|
||||
.search-process-view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.search-process-header {
|
||||
text-align: center;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.search-process-header h3 {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: hsl(var(--foreground));
|
||||
margin: 0 0 0.5rem 0;
|
||||
}
|
||||
|
||||
.search-process-header p {
|
||||
font-size: 0.875rem;
|
||||
color: hsl(var(--muted-foreground));
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.search-empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
.search-empty-state i {
|
||||
margin-bottom: 1rem;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* Search Process Empty State */
|
||||
.search-process-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
color: hsl(var(--muted-foreground));
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.search-process-empty i {
|
||||
margin-bottom: 1rem;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.search-process-empty p {
|
||||
font-size: 0.875rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Search Process Timeline */
|
||||
.search-process-timeline {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.search-step {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
background: hsl(var(--card));
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.search-step-number {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background: hsl(var(--primary));
|
||||
color: hsl(var(--primary-foreground));
|
||||
border-radius: 50%;
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.search-step-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.search-step-content h4 {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: hsl(var(--foreground));
|
||||
margin: 0 0 0.25rem 0;
|
||||
}
|
||||
|
||||
.search-step-content p {
|
||||
font-size: 0.8125rem;
|
||||
color: hsl(var(--muted-foreground));
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.search-step-results {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.result-count {
|
||||
font-size: 0.75rem;
|
||||
color: hsl(var(--primary));
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Cytoscape Container */
|
||||
.cytoscape-container {
|
||||
flex: 1;
|
||||
min-height: 400px;
|
||||
background: hsl(var(--card));
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 0.5rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Cytoscape Empty State */
|
||||
.cytoscape-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 400px;
|
||||
text-align: center;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
.cytoscape-empty i {
|
||||
margin-bottom: 1rem;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.cytoscape-empty p {
|
||||
font-size: 0.875rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Graph Empty State */
|
||||
.graph-empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
.graph-empty-state i {
|
||||
margin-bottom: 1rem;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.graph-empty-state p {
|
||||
font-size: 0.875rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Hidden class */
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
@@ -52,10 +52,25 @@ function initPathSelector() {
|
||||
});
|
||||
}
|
||||
|
||||
// Cleanup function for view transitions
|
||||
function cleanupPreviousView() {
|
||||
// Cleanup graph explorer
|
||||
if (currentView === 'graph-explorer' && typeof window.cleanupGraphExplorer === 'function') {
|
||||
window.cleanupGraphExplorer();
|
||||
}
|
||||
// Hide storage card when leaving cli-manager
|
||||
var storageCard = document.getElementById('storageCard');
|
||||
if (storageCard) {
|
||||
storageCard.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// Navigation
|
||||
function initNavigation() {
|
||||
document.querySelectorAll('.nav-item[data-filter]').forEach(item => {
|
||||
item.addEventListener('click', () => {
|
||||
cleanupPreviousView();
|
||||
|
||||
setActiveNavItem(item);
|
||||
currentFilter = item.dataset.filter;
|
||||
currentLiteType = null;
|
||||
@@ -70,6 +85,8 @@ function initNavigation() {
|
||||
// Lite Tasks Navigation
|
||||
document.querySelectorAll('.nav-item[data-lite]').forEach(item => {
|
||||
item.addEventListener('click', () => {
|
||||
cleanupPreviousView();
|
||||
|
||||
setActiveNavItem(item);
|
||||
currentLiteType = item.dataset.lite;
|
||||
currentFilter = null;
|
||||
@@ -84,6 +101,8 @@ function initNavigation() {
|
||||
// View Navigation (Project Overview, MCP Manager, etc.)
|
||||
document.querySelectorAll('.nav-item[data-view]').forEach(item => {
|
||||
item.addEventListener('click', () => {
|
||||
cleanupPreviousView();
|
||||
|
||||
setActiveNavItem(item);
|
||||
currentView = item.dataset.view;
|
||||
currentFilter = null;
|
||||
|
||||
@@ -59,40 +59,22 @@ function renderStorageCard() {
|
||||
return date.toLocaleDateString();
|
||||
};
|
||||
|
||||
// Build project rows
|
||||
// Build project tree (hierarchical view)
|
||||
let projectRows = '';
|
||||
if (projects && projects.length > 0) {
|
||||
projects.slice(0, 5).forEach(p => {
|
||||
const historyBadge = p.historyRecords > 0
|
||||
? '<span class="text-xs px-1.5 py-0.5 bg-primary/10 text-primary rounded">' + p.historyRecords + '</span>'
|
||||
: '<span class="text-xs text-muted-foreground">-</span>';
|
||||
const tree = buildProjectTree(projects);
|
||||
projectRows = renderProjectTree(tree, 0, formatTimeAgo);
|
||||
|
||||
projectRows += '\
|
||||
<tr class="border-b border-border/50 hover:bg-muted/30">\
|
||||
<td class="py-2 px-2 font-mono text-xs text-muted-foreground">' + escapeHtml(p.id.substring(0, 8)) + '...</td>\
|
||||
<td class="py-2 px-2 text-sm text-right">' + escapeHtml(p.totalSizeFormatted) + '</td>\
|
||||
<td class="py-2 px-2 text-center">' + historyBadge + '</td>\
|
||||
<td class="py-2 px-2 text-xs text-muted-foreground text-right">' + formatTimeAgo(p.lastModified) + '</td>\
|
||||
<td class="py-2 px-1 text-right">\
|
||||
<button onclick="cleanProjectStorage(\'' + escapeHtml(p.id) + '\')" \
|
||||
class="text-xs px-2 py-1 text-destructive hover:bg-destructive/10 rounded transition-colors" \
|
||||
title="Clean this project storage">\
|
||||
<i data-lucide="trash-2" class="w-3 h-3"></i>\
|
||||
</button>\
|
||||
</td>\
|
||||
</tr>\
|
||||
';
|
||||
});
|
||||
|
||||
if (projects.length > 5) {
|
||||
projectRows += '\
|
||||
<tr>\
|
||||
<td colspan="5" class="py-2 px-2 text-xs text-muted-foreground text-center">\
|
||||
... and ' + (projects.length - 5) + ' more projects\
|
||||
</td>\
|
||||
</tr>\
|
||||
';
|
||||
}
|
||||
// Initially hide all child rows (level > 0)
|
||||
setTimeout(() => {
|
||||
const allRows = document.querySelectorAll('.project-row');
|
||||
allRows.forEach(row => {
|
||||
const level = parseInt(row.getAttribute('data-level'));
|
||||
if (level > 0) {
|
||||
row.style.display = 'none';
|
||||
}
|
||||
});
|
||||
}, 0);
|
||||
} else {
|
||||
projectRows = '\
|
||||
<tr>\
|
||||
@@ -178,6 +160,162 @@ function getTotalRecords() {
|
||||
return storageData.projects.reduce((sum, p) => sum + (p.historyRecords || 0), 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build project tree from flat list
|
||||
* Converts flat project list to hierarchical tree structure
|
||||
*/
|
||||
function buildProjectTree(projects) {
|
||||
const tree = [];
|
||||
const map = new Map();
|
||||
|
||||
// Sort by path depth (shallowest first)
|
||||
const sorted = projects.slice().sort((a, b) => {
|
||||
const depthA = (a.id.match(/\//g) || []).length;
|
||||
const depthB = (b.id.match(/\//g) || []).length;
|
||||
return depthA - depthB;
|
||||
});
|
||||
|
||||
for (const project of sorted) {
|
||||
const segments = project.id.split('/');
|
||||
|
||||
if (segments.length === 1) {
|
||||
// Root level project
|
||||
const node = {
|
||||
...project,
|
||||
children: [],
|
||||
isExpanded: false
|
||||
};
|
||||
tree.push(node);
|
||||
map.set(project.id, node);
|
||||
} else {
|
||||
// Sub-project
|
||||
const parentId = segments.slice(0, -1).join('/');
|
||||
const parent = map.get(parentId);
|
||||
|
||||
if (parent) {
|
||||
const node = {
|
||||
...project,
|
||||
children: [],
|
||||
isExpanded: false
|
||||
};
|
||||
parent.children.push(node);
|
||||
map.set(project.id, node);
|
||||
} else {
|
||||
// Orphaned project (parent not found) - add to root
|
||||
const node = {
|
||||
...project,
|
||||
children: [],
|
||||
isExpanded: false
|
||||
};
|
||||
tree.push(node);
|
||||
map.set(project.id, node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return tree;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render project tree recursively
|
||||
*/
|
||||
function renderProjectTree(tree, level = 0, formatTimeAgo) {
|
||||
if (!tree || tree.length === 0) return '';
|
||||
|
||||
let html = '';
|
||||
|
||||
for (const node of tree) {
|
||||
const hasChildren = node.children && node.children.length > 0;
|
||||
const indent = level * 20;
|
||||
const projectName = node.id.split('/').pop();
|
||||
|
||||
const historyBadge = node.historyRecords > 0
|
||||
? '<span class="text-xs px-1.5 py-0.5 bg-primary/10 text-primary rounded">' + node.historyRecords + '</span>'
|
||||
: '<span class="text-xs text-muted-foreground">-</span>';
|
||||
|
||||
const toggleIcon = hasChildren
|
||||
? '<i data-lucide="chevron-right" class="w-3 h-3 transition-transform duration-200 toggle-icon"></i>'
|
||||
: '<span class="w-3 h-3 inline-block"></span>';
|
||||
|
||||
html += '\
|
||||
<tr class="border-b border-border/50 hover:bg-muted/30 project-row" data-project-id="' + escapeHtml(node.id) + '" data-level="' + level + '">\
|
||||
<td class="py-2 px-2 font-mono text-xs text-muted-foreground">\
|
||||
<div class="flex items-center gap-1" style="padding-left: ' + indent + 'px">\
|
||||
' + (hasChildren ? '<button class="toggle-btn hover:bg-muted/50 rounded p-0.5" onclick="toggleProjectNode(\'' + escapeHtml(node.id) + '\')">' + toggleIcon + '</button>' : '<span class="w-3 h-3 inline-block"></span>') + '\
|
||||
<span class="truncate max-w-[150px]" title="' + escapeHtml(node.id) + '">' + escapeHtml(projectName) + '</span>\
|
||||
</div>\
|
||||
</td>\
|
||||
<td class="py-2 px-2 text-sm text-right">' + escapeHtml(node.totalSizeFormatted) + '</td>\
|
||||
<td class="py-2 px-2 text-center">' + historyBadge + '</td>\
|
||||
<td class="py-2 px-2 text-xs text-muted-foreground text-right">' + formatTimeAgo(node.lastModified) + '</td>\
|
||||
<td class="py-2 px-1 text-right">\
|
||||
<button onclick="cleanProjectStorage(\'' + escapeHtml(node.id) + '\')" \
|
||||
class="text-xs px-2 py-1 text-destructive hover:bg-destructive/10 rounded transition-colors" \
|
||||
title="Clean this project storage">\
|
||||
<i data-lucide="trash-2" class="w-3 h-3"></i>\
|
||||
</button>\
|
||||
</td>\
|
||||
</tr>\
|
||||
';
|
||||
|
||||
// Render children (initially hidden)
|
||||
if (hasChildren) {
|
||||
html += renderProjectTree(node.children, level + 1, formatTimeAgo);
|
||||
}
|
||||
}
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle project node expansion
|
||||
*/
|
||||
function toggleProjectNode(projectId) {
|
||||
const row = document.querySelector('[data-project-id="' + projectId + '"]');
|
||||
if (!row) return;
|
||||
|
||||
const icon = row.querySelector('.toggle-icon');
|
||||
const level = parseInt(row.getAttribute('data-level'));
|
||||
|
||||
// Find all child rows
|
||||
let nextRow = row.nextElementSibling;
|
||||
const childRows = [];
|
||||
|
||||
while (nextRow && nextRow.classList.contains('project-row')) {
|
||||
const nextLevel = parseInt(nextRow.getAttribute('data-level'));
|
||||
if (nextLevel <= level) break;
|
||||
childRows.push(nextRow);
|
||||
nextRow = nextRow.nextElementSibling;
|
||||
}
|
||||
|
||||
// Toggle visibility
|
||||
const isExpanded = row.classList.contains('expanded');
|
||||
|
||||
if (isExpanded) {
|
||||
// Collapse
|
||||
row.classList.remove('expanded');
|
||||
if (icon) icon.style.transform = 'rotate(0deg)';
|
||||
childRows.forEach(child => {
|
||||
child.style.display = 'none';
|
||||
});
|
||||
} else {
|
||||
// Expand (only immediate children)
|
||||
row.classList.add('expanded');
|
||||
if (icon) icon.style.transform = 'rotate(90deg)';
|
||||
childRows.forEach(child => {
|
||||
const childLevel = parseInt(child.getAttribute('data-level'));
|
||||
if (childLevel === level + 1) {
|
||||
child.style.display = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Reinitialize Lucide icons
|
||||
if (typeof lucide !== 'undefined') {
|
||||
lucide.createIcons();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render error state for storage card
|
||||
*/
|
||||
|
||||
@@ -506,6 +506,27 @@ const i18n = {
|
||||
'mcp.codex.enabledTools': 'Tools',
|
||||
'mcp.codex.tools': 'tools enabled',
|
||||
|
||||
// Claude to Codex copy
|
||||
'mcp.claude.copyFromCodex': 'Copy Codex Servers to Claude',
|
||||
'mcp.claude.alreadyAdded': 'Already in Claude',
|
||||
'mcp.claude.copyToClaude': 'Copy to Claude Global',
|
||||
|
||||
// MCP Edit Modal
|
||||
'mcp.editModal.title': 'Edit MCP Server',
|
||||
'mcp.editModal.serverNamePlaceholder': 'server-name',
|
||||
'mcp.editModal.onePerLine': 'one per line',
|
||||
'mcp.editModal.save': 'Save Changes',
|
||||
'mcp.editModal.delete': 'Delete',
|
||||
'mcp.editModal.nameRequired': 'Server name is required',
|
||||
'mcp.editModal.commandRequired': 'Command or URL is required',
|
||||
'mcp.editModal.saved': 'MCP server "{name}" updated',
|
||||
'mcp.editModal.saveFailed': 'Failed to save MCP server',
|
||||
'mcp.editModal.deleteConfirm': 'Delete MCP server "{name}"?',
|
||||
'mcp.editModal.deleted': 'MCP server "{name}" deleted',
|
||||
'mcp.editModal.deleteFailed': 'Failed to delete MCP server',
|
||||
'mcp.clickToEdit': 'Click to edit',
|
||||
'mcp.clickToViewDetails': 'Click to view details',
|
||||
|
||||
// Hook Manager
|
||||
'hook.projectHooks': 'Project Hooks',
|
||||
'hook.projectFile': '.claude/settings.json',
|
||||
@@ -729,6 +750,7 @@ const i18n = {
|
||||
'memory.contextHotspots': 'Context Hotspots',
|
||||
'memory.mostRead': 'Most Read Files',
|
||||
'memory.mostEdited': 'Most Edited Files',
|
||||
'memory.mostMentioned': 'Most Mentioned Topics',
|
||||
'memory.today': 'Today',
|
||||
'memory.week': 'Week',
|
||||
'memory.allTime': 'All Time',
|
||||
@@ -958,6 +980,29 @@ const i18n = {
|
||||
'graph.zoomIn': 'Zoom In',
|
||||
'graph.zoomOut': 'Zoom Out',
|
||||
'graph.resetLayout': 'Reset Layout',
|
||||
'graph.title': 'Code Graph',
|
||||
'graph.filters': 'Filters',
|
||||
'graph.legend': 'Legend',
|
||||
'graph.nodes': 'nodes',
|
||||
'graph.edges': 'edges',
|
||||
'graph.noGraphData': 'No graph data available. Index this project with codex-lens first.',
|
||||
'graph.noSearchData': 'No search process data available.',
|
||||
'graph.center': 'Center',
|
||||
'graph.resetFilters': 'Reset Filters',
|
||||
'graph.cytoscapeNotLoaded': 'Graph library not loaded',
|
||||
'graph.impactAnalysisError': 'Failed to load impact analysis',
|
||||
'graph.searchProcessDesc': 'Visualize how search queries flow through the system',
|
||||
'graph.searchProcessTitle': 'Search Pipeline',
|
||||
'graph.resultsFound': 'results found',
|
||||
'graph.type': 'Type',
|
||||
'graph.line': 'Line',
|
||||
'graph.path': 'Path',
|
||||
'graph.depth': 'Depth',
|
||||
'graph.exports': 'Exports',
|
||||
'graph.imports': 'Imports',
|
||||
'graph.references': 'References',
|
||||
'graph.symbolType': 'Symbol Type',
|
||||
'graph.affectedSymbols': 'Affected Symbols',
|
||||
|
||||
// CLI Sync (used in claude-manager.js)
|
||||
'claude.cliSync': 'CLI Auto-Sync',
|
||||
@@ -1025,7 +1070,7 @@ const i18n = {
|
||||
zh: {
|
||||
// App title and brand
|
||||
'app.title': 'CCW 控制面板',
|
||||
'app.brand': 'Claude Code 工作流',
|
||||
'app.brand': 'Claude Code Workflow',
|
||||
|
||||
// Header
|
||||
'header.project': '项目:',
|
||||
@@ -1498,6 +1543,27 @@ const i18n = {
|
||||
'mcp.codex.enabledTools': '工具',
|
||||
'mcp.codex.tools': '个工具已启用',
|
||||
|
||||
// Claude to Codex copy
|
||||
'mcp.claude.copyFromCodex': '从 Codex 复制服务器到 Claude',
|
||||
'mcp.claude.alreadyAdded': '已在 Claude 中',
|
||||
'mcp.claude.copyToClaude': '复制到 Claude 全局',
|
||||
|
||||
// MCP Edit Modal
|
||||
'mcp.editModal.title': '编辑 MCP 服务器',
|
||||
'mcp.editModal.serverNamePlaceholder': 'server-name',
|
||||
'mcp.editModal.onePerLine': '每行一个',
|
||||
'mcp.editModal.save': '保存更改',
|
||||
'mcp.editModal.delete': '删除',
|
||||
'mcp.editModal.nameRequired': '服务器名称必填',
|
||||
'mcp.editModal.commandRequired': '命令或 URL 必填',
|
||||
'mcp.editModal.saved': 'MCP 服务器 "{name}" 已更新',
|
||||
'mcp.editModal.saveFailed': '保存 MCP 服务器失败',
|
||||
'mcp.editModal.deleteConfirm': '删除 MCP 服务器 "{name}"?',
|
||||
'mcp.editModal.deleted': 'MCP 服务器 "{name}" 已删除',
|
||||
'mcp.editModal.deleteFailed': '删除 MCP 服务器失败',
|
||||
'mcp.clickToEdit': '点击编辑',
|
||||
'mcp.clickToViewDetails': '点击查看详情',
|
||||
|
||||
// Hook Manager
|
||||
'hook.projectHooks': '项目钩子',
|
||||
'hook.projectFile': '.claude/settings.json',
|
||||
@@ -1721,6 +1787,7 @@ const i18n = {
|
||||
'memory.contextHotspots': '上下文热点',
|
||||
'memory.mostRead': '最常读取的文件',
|
||||
'memory.mostEdited': '最常编辑的文件',
|
||||
'memory.mostMentioned': '最常提及的话题',
|
||||
'memory.today': '今天',
|
||||
'memory.week': '本周',
|
||||
'memory.allTime': '全部时间',
|
||||
@@ -1950,6 +2017,29 @@ const i18n = {
|
||||
'graph.zoomIn': '放大',
|
||||
'graph.zoomOut': '缩小',
|
||||
'graph.resetLayout': '重置布局',
|
||||
'graph.title': '代码图谱',
|
||||
'graph.filters': '筛选器',
|
||||
'graph.legend': '图例',
|
||||
'graph.nodes': '个节点',
|
||||
'graph.edges': '条边',
|
||||
'graph.noGraphData': '无图谱数据。请先使用 codex-lens 为此项目建立索引。',
|
||||
'graph.noSearchData': '无搜索过程数据。',
|
||||
'graph.center': '居中',
|
||||
'graph.resetFilters': '重置筛选',
|
||||
'graph.cytoscapeNotLoaded': '图谱库未加载',
|
||||
'graph.impactAnalysisError': '加载影响分析失败',
|
||||
'graph.searchProcessDesc': '可视化搜索查询在系统中的流转过程',
|
||||
'graph.searchProcessTitle': '搜索管道',
|
||||
'graph.resultsFound': '个结果',
|
||||
'graph.type': '类型',
|
||||
'graph.line': '行号',
|
||||
'graph.path': '路径',
|
||||
'graph.depth': '深度',
|
||||
'graph.exports': '导出',
|
||||
'graph.imports': '导入',
|
||||
'graph.references': '引用',
|
||||
'graph.symbolType': '符号类型',
|
||||
'graph.affectedSymbols': '受影响符号',
|
||||
|
||||
// CLI Sync (used in claude-manager.js)
|
||||
'claude.cliSync': 'CLI 自动同步',
|
||||
|
||||
@@ -222,7 +222,7 @@ function renderGraphLegend() {
|
||||
function renderSearchProcessView() {
|
||||
if (!searchProcessData) {
|
||||
return '<div class="search-process-empty">' +
|
||||
'<i data-lucide="search-x" class="w-12 h-12"></i>' +
|
||||
'<i data-lucide="search-x" class="w-8 h-8"></i>' +
|
||||
'<p>' + t('graph.noSearchData') + '</p>' +
|
||||
'</div>';
|
||||
}
|
||||
@@ -280,7 +280,7 @@ function initializeCytoscape() {
|
||||
// Check if Cytoscape.js is loaded
|
||||
if (typeof cytoscape === 'undefined') {
|
||||
container.innerHTML = '<div class="cytoscape-error">' +
|
||||
'<i data-lucide="alert-triangle" class="w-8 h-8"></i>' +
|
||||
'<i data-lucide="alert-triangle" class="w-6 h-6"></i>' +
|
||||
'<p>' + t('graph.cytoscapeNotLoaded') + '</p>' +
|
||||
'</div>';
|
||||
if (window.lucide) lucide.createIcons();
|
||||
@@ -289,7 +289,7 @@ function initializeCytoscape() {
|
||||
|
||||
if (graphData.nodes.length === 0) {
|
||||
container.innerHTML = '<div class="cytoscape-empty">' +
|
||||
'<i data-lucide="network" class="w-12 h-12"></i>' +
|
||||
'<i data-lucide="network" class="w-8 h-8"></i>' +
|
||||
'<p>' + t('graph.noGraphData') + '</p>' +
|
||||
'</div>';
|
||||
if (window.lucide) lucide.createIcons();
|
||||
@@ -493,6 +493,15 @@ function selectNode(nodeData) {
|
||||
panel.classList.remove('hidden');
|
||||
panel.innerHTML = renderNodeDetails(nodeData);
|
||||
if (window.lucide) lucide.createIcons();
|
||||
|
||||
// Attach event listener for impact analysis button (prevents XSS)
|
||||
var impactBtn = document.getElementById('impactAnalysisBtn');
|
||||
if (impactBtn) {
|
||||
impactBtn.addEventListener('click', function() {
|
||||
var nodeId = this.getAttribute('data-node-id');
|
||||
if (nodeId) showImpactAnalysis(nodeId);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -559,7 +568,7 @@ function renderNodeDetails(node) {
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="node-details-actions">' +
|
||||
'<button class="btn btn-sm btn-primary" onclick="showImpactAnalysis(\'' + escapeHtml(node.id) + '\')">' +
|
||||
'<button class="btn btn-sm btn-primary" id="impactAnalysisBtn" data-node-id="' + escapeHtml(node.id) + '">' +
|
||||
'<i data-lucide="target" class="w-3 h-3"></i> ' + t('graph.impactAnalysis') +
|
||||
'</button>' +
|
||||
'</div>' +
|
||||
@@ -629,7 +638,7 @@ function centerCytoscape() {
|
||||
// ========== Impact Analysis ==========
|
||||
async function showImpactAnalysis(symbolId) {
|
||||
try {
|
||||
var response = await fetch('/api/graph/impact/' + encodeURIComponent(symbolId));
|
||||
var response = await fetch('/api/graph/impact?symbol=' + encodeURIComponent(symbolId));
|
||||
if (!response.ok) throw new Error('Failed to fetch impact analysis');
|
||||
var data = await response.json();
|
||||
|
||||
@@ -727,3 +736,22 @@ function hideStatsAndCarousel() {
|
||||
if (statsGrid) statsGrid.style.display = 'none';
|
||||
if (carousel) carousel.style.display = 'none';
|
||||
}
|
||||
|
||||
// ========== Cleanup Function ==========
|
||||
/**
|
||||
* Clean up Cytoscape instance to prevent memory leaks
|
||||
* Should be called when navigating away from the graph explorer view
|
||||
*/
|
||||
function cleanupGraphExplorer() {
|
||||
if (cyInstance) {
|
||||
cyInstance.destroy();
|
||||
cyInstance = null;
|
||||
}
|
||||
selectedNode = null;
|
||||
searchProcessData = null;
|
||||
}
|
||||
|
||||
// Register cleanup on navigation (called by navigation.js before switching views)
|
||||
if (typeof window !== 'undefined') {
|
||||
window.cleanupGraphExplorer = cleanupGraphExplorer;
|
||||
}
|
||||
|
||||
@@ -139,8 +139,9 @@ async function renderMcpManager() {
|
||||
<i data-lucide="bot" class="w-4 h-4 inline mr-1.5"></i>
|
||||
Claude
|
||||
</button>
|
||||
<button class="cli-mode-btn px-4 py-2 text-sm font-medium rounded-md transition-all ${currentCliMode === 'codex' ? 'bg-orange-500 text-white shadow-sm' : 'text-muted-foreground hover:text-foreground'}"
|
||||
onclick="setCliMode('codex')">
|
||||
<button class="cli-mode-btn px-4 py-2 text-sm font-medium rounded-md transition-all ${currentCliMode === 'codex' ? 'shadow-sm' : 'text-muted-foreground hover:text-foreground'}"
|
||||
onclick="setCliMode('codex')"
|
||||
style="${currentCliMode === 'codex' ? 'background-color: #f97316; color: white;' : ''}">
|
||||
<i data-lucide="code-2" class="w-4 h-4 inline mr-1.5"></i>
|
||||
Codex
|
||||
</button>
|
||||
@@ -228,6 +229,7 @@ async function renderMcpManager() {
|
||||
<div class="flex items-center gap-2">
|
||||
<i data-lucide="bot" class="w-5 h-5 text-primary"></i>
|
||||
<h4 class="font-semibold text-foreground">${escapeHtml(serverName)}</h4>
|
||||
<span class="text-xs px-2 py-0.5 bg-primary/10 text-primary rounded-full">Claude</span>
|
||||
${alreadyInCodex ? `<span class="text-xs px-2 py-0.5 bg-success/10 text-success rounded-full">${t('mcp.codex.alreadyAdded')}</span>` : ''}
|
||||
</div>
|
||||
${!alreadyInCodex ? `
|
||||
@@ -250,6 +252,26 @@ async function renderMcpManager() {
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<!-- Available MCP Servers from Other Projects (Codex mode) -->
|
||||
<div class="mcp-section">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-semibold text-foreground">${t('mcp.availableOther')}</h3>
|
||||
<span class="text-sm text-muted-foreground">${otherProjectServers.length} ${t('mcp.serversAvailable')}</span>
|
||||
</div>
|
||||
|
||||
${otherProjectServers.length === 0 ? `
|
||||
<div class="mcp-empty-state bg-card border border-border rounded-lg p-6 text-center">
|
||||
<p class="text-muted-foreground">${t('empty.noAdditionalMcp')}</p>
|
||||
</div>
|
||||
` : `
|
||||
<div class="mcp-server-grid grid gap-3">
|
||||
${otherProjectServers.map(([serverName, serverInfo]) => {
|
||||
return renderAvailableServerCardForCodex(serverName, serverInfo);
|
||||
}).join('')}
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
` : `
|
||||
<!-- CCW Tools MCP Server Card -->
|
||||
<div class="mcp-section mb-6">
|
||||
@@ -486,6 +508,55 @@ async function renderMcpManager() {
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<!-- Copy Codex Servers to Claude (Claude mode only) -->
|
||||
${currentCliMode === 'claude' && Object.keys(codexMcpServers || {}).length > 0 ? `
|
||||
<div class="mcp-section mb-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-semibold text-foreground flex items-center gap-2">
|
||||
<i data-lucide="copy" class="w-5 h-5"></i>
|
||||
${t('mcp.claude.copyFromCodex')}
|
||||
</h3>
|
||||
<span class="text-sm text-muted-foreground">${Object.keys(codexMcpServers || {}).length} ${t('mcp.serversAvailable')}</span>
|
||||
</div>
|
||||
<div class="mcp-server-grid grid gap-3">
|
||||
${Object.entries(codexMcpServers || {}).map(([serverName, serverConfig]) => {
|
||||
const alreadyInClaude = mcpUserServers && mcpUserServers[serverName];
|
||||
const isStdio = !!serverConfig.command;
|
||||
const isHttp = !!serverConfig.url;
|
||||
return `
|
||||
<div class="mcp-server-card bg-card border ${alreadyInClaude ? 'border-success/50' : 'border-orange-200 dark:border-orange-800'} border-dashed rounded-lg p-4 hover:shadow-md transition-all">
|
||||
<div class="flex items-start justify-between mb-3">
|
||||
<div class="flex items-center gap-2 flex-wrap">
|
||||
<i data-lucide="code-2" class="w-5 h-5 text-orange-500"></i>
|
||||
<h4 class="font-semibold text-foreground">${escapeHtml(serverName)}</h4>
|
||||
<span class="text-xs px-2 py-0.5 bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-300 rounded-full">Codex</span>
|
||||
${isHttp
|
||||
? '<span class="text-xs px-2 py-0.5 bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300 rounded-full">HTTP</span>'
|
||||
: '<span class="text-xs px-2 py-0.5 bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300 rounded-full">STDIO</span>'
|
||||
}
|
||||
${alreadyInClaude ? '<span class="text-xs px-2 py-0.5 bg-success/10 text-success rounded-full">' + t('mcp.claude.alreadyAdded') + '</span>' : ''}
|
||||
</div>
|
||||
${!alreadyInClaude ? `
|
||||
<button class="px-3 py-1 text-xs bg-primary text-primary-foreground rounded hover:opacity-90 transition-opacity"
|
||||
onclick="copyCodexServerToClaude('${escapeHtml(serverName)}', ${JSON.stringify(serverConfig).replace(/'/g, "'")})"
|
||||
title="${t('mcp.claude.copyToClaude')}">
|
||||
<i data-lucide="arrow-right" class="w-3.5 h-3.5 inline"></i> Claude
|
||||
</button>
|
||||
` : ''}
|
||||
</div>
|
||||
<div class="mcp-server-details text-sm space-y-1">
|
||||
<div class="flex items-center gap-2 text-muted-foreground">
|
||||
<span class="font-mono text-xs bg-muted px-1.5 py-0.5 rounded">${isHttp ? t('mcp.url') : t('mcp.cmd')}</span>
|
||||
<span class="truncate" title="${escapeHtml(serverConfig.command || serverConfig.url || 'N/A')}">${escapeHtml(serverConfig.command || serverConfig.url || 'N/A')}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('')}
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<!-- All Projects MCP Overview Table (Claude mode only) -->
|
||||
${currentCliMode === 'claude' ? `
|
||||
<div class="mcp-section mt-6">
|
||||
@@ -676,7 +747,12 @@ function renderGlobalManagementCard(serverName, serverConfig) {
|
||||
const serverType = serverConfig.type || 'stdio';
|
||||
|
||||
return `
|
||||
<div class="mcp-server-card mcp-server-global bg-card border border-success/30 rounded-lg p-4 hover:shadow-md transition-all">
|
||||
<div class="mcp-server-card mcp-server-global bg-card border border-success/30 rounded-lg p-4 hover:shadow-md transition-all cursor-pointer"
|
||||
data-server-name="${escapeHtml(serverName)}"
|
||||
data-server-config="${escapeHtml(JSON.stringify(serverConfig))}"
|
||||
data-server-source="global"
|
||||
data-action="view-details"
|
||||
title="${t('mcp.clickToEdit')}">
|
||||
<div class="flex items-start justify-between mb-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<i data-lucide="globe" class="w-5 h-5 text-success"></i>
|
||||
@@ -706,7 +782,7 @@ function renderGlobalManagementCard(serverName, serverConfig) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 pt-3 border-t border-border flex items-center justify-end">
|
||||
<div class="mt-3 pt-3 border-t border-border flex items-center justify-end" onclick="event.stopPropagation()">
|
||||
<button class="text-xs text-destructive hover:text-destructive/80 transition-colors"
|
||||
data-server-name="${escapeHtml(serverName)}"
|
||||
data-action="remove-global">
|
||||
@@ -807,6 +883,70 @@ function renderAvailableServerCard(serverName, serverInfo) {
|
||||
`;
|
||||
}
|
||||
|
||||
// Render available server card for Codex mode (with Claude badge and copy to Codex button)
|
||||
function renderAvailableServerCardForCodex(serverName, serverInfo) {
|
||||
const serverConfig = serverInfo.config;
|
||||
const usedIn = serverInfo.usedIn || [];
|
||||
const command = serverConfig.command || serverConfig.url || 'N/A';
|
||||
const args = serverConfig.args || [];
|
||||
|
||||
// Get the actual name to use when adding
|
||||
const originalName = serverInfo.originalName || serverName;
|
||||
const hasVariant = serverInfo.originalName && serverInfo.originalName !== serverName;
|
||||
|
||||
// Get source project info
|
||||
const sourceProject = serverInfo.sourceProject;
|
||||
const sourceProjectName = sourceProject ? (sourceProject.split('\\').pop() || sourceProject.split('/').pop()) : null;
|
||||
|
||||
// Generate args preview
|
||||
const argsPreview = args.length > 0 ? args.slice(0, 3).join(' ') + (args.length > 3 ? '...' : '') : '';
|
||||
|
||||
// Check if already in Codex
|
||||
const alreadyInCodex = codexMcpServers && codexMcpServers[originalName];
|
||||
|
||||
return `
|
||||
<div class="mcp-server-card mcp-server-available bg-card border ${alreadyInCodex ? 'border-success/50' : 'border-border'} border-dashed rounded-lg p-4 hover:shadow-md hover:border-solid transition-all">
|
||||
<div class="flex items-start justify-between mb-3">
|
||||
<div class="flex items-center gap-2 flex-wrap">
|
||||
<span><i data-lucide="circle-dashed" class="w-5 h-5 text-muted-foreground"></i></span>
|
||||
<h4 class="font-semibold text-foreground">${escapeHtml(originalName)}</h4>
|
||||
<span class="text-xs px-2 py-0.5 bg-primary/10 text-primary rounded-full">Claude</span>
|
||||
${hasVariant ? `
|
||||
<span class="text-xs px-2 py-0.5 bg-warning/20 text-warning rounded-full" title="Different config from: ${escapeHtml(sourceProject || '')}">
|
||||
${escapeHtml(sourceProjectName || 'variant')}
|
||||
</span>
|
||||
` : ''}
|
||||
${alreadyInCodex ? `<span class="text-xs px-2 py-0.5 bg-success/10 text-success rounded-full">${t('mcp.codex.alreadyAdded')}</span>` : ''}
|
||||
</div>
|
||||
${!alreadyInCodex ? `
|
||||
<button class="px-3 py-1 text-xs bg-orange-500 text-white rounded hover:opacity-90 transition-opacity"
|
||||
onclick="copyClaudeServerToCodex('${escapeHtml(originalName)}', ${JSON.stringify(serverConfig).replace(/'/g, "'")})"
|
||||
title="${t('mcp.codex.copyToCodex')}">
|
||||
<i data-lucide="arrow-right" class="w-3.5 h-3.5 inline"></i> Codex
|
||||
</button>
|
||||
` : ''}
|
||||
</div>
|
||||
|
||||
<div class="mcp-server-details text-sm space-y-1">
|
||||
<div class="flex items-center gap-2 text-muted-foreground">
|
||||
<span class="font-mono text-xs bg-muted px-1.5 py-0.5 rounded">${t('mcp.cmd')}</span>
|
||||
<span class="truncate" title="${escapeHtml(command)}">${escapeHtml(command)}</span>
|
||||
</div>
|
||||
${argsPreview ? `
|
||||
<div class="flex items-start gap-2 text-muted-foreground">
|
||||
<span class="font-mono text-xs bg-muted px-1.5 py-0.5 rounded shrink-0">${t('mcp.args')}</span>
|
||||
<span class="text-xs font-mono truncate" title="${escapeHtml(args.join(' '))}">${escapeHtml(argsPreview)}</span>
|
||||
</div>
|
||||
` : ''}
|
||||
<div class="flex items-center gap-2 text-muted-foreground">
|
||||
<span class="text-xs">${t('mcp.usedInCount').replace('{count}', usedIn.length).replace('{s}', usedIn.length !== 1 ? 's' : '')}</span>
|
||||
${sourceProjectName ? `<span class="text-xs text-muted-foreground/70">• ${t('mcp.from')} ${escapeHtml(sourceProjectName)}</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Codex MCP Server Card Renderer
|
||||
// ========================================
|
||||
@@ -825,14 +965,17 @@ function renderCodexServerCard(serverName, serverConfig) {
|
||||
: `<span class="text-xs px-2 py-0.5 bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300 rounded-full">STDIO</span>`;
|
||||
|
||||
return `
|
||||
<div class="mcp-server-card bg-card border border-orange-200 dark:border-orange-800 rounded-lg p-4 hover:shadow-md transition-all ${!isEnabled ? 'opacity-60' : ''}"
|
||||
<div class="mcp-server-card bg-card border border-orange-200 dark:border-orange-800 rounded-lg p-4 hover:shadow-md transition-all cursor-pointer ${!isEnabled ? 'opacity-60' : ''}"
|
||||
data-server-name="${escapeHtml(serverName)}"
|
||||
data-server-config="${escapeHtml(JSON.stringify(serverConfig))}"
|
||||
data-cli-type="codex">
|
||||
data-cli-type="codex"
|
||||
data-action="view-details-codex"
|
||||
title="${t('mcp.clickToEdit')}">
|
||||
<div class="flex items-start justify-between mb-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex items-center gap-2 flex-wrap">
|
||||
<span>${isEnabled ? '<i data-lucide="check-circle" class="w-5 h-5 text-orange-500"></i>' : '<i data-lucide="circle" class="w-5 h-5 text-muted-foreground"></i>'}</span>
|
||||
<h4 class="font-semibold text-foreground">${escapeHtml(serverName)}</h4>
|
||||
<span class="text-xs px-2 py-0.5 bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-300 rounded-full">Codex</span>
|
||||
${typeBadge}
|
||||
</div>
|
||||
<label class="mcp-toggle relative inline-flex items-center cursor-pointer" onclick="event.stopPropagation()">
|
||||
@@ -1041,13 +1184,22 @@ function attachMcpEventListeners() {
|
||||
});
|
||||
});
|
||||
|
||||
// View details - click on server card
|
||||
// View details / Edit - click on Claude server card
|
||||
document.querySelectorAll('.mcp-server-card[data-action="view-details"]').forEach(card => {
|
||||
card.addEventListener('click', (e) => {
|
||||
const serverName = card.dataset.serverName;
|
||||
const serverConfig = JSON.parse(card.dataset.serverConfig);
|
||||
const serverSource = card.dataset.serverSource;
|
||||
showMcpDetails(serverName, serverConfig, serverSource);
|
||||
showMcpEditModal(serverName, serverConfig, serverSource, 'claude');
|
||||
});
|
||||
});
|
||||
|
||||
// View details / Edit - click on Codex server card
|
||||
document.querySelectorAll('.mcp-server-card[data-action="view-details-codex"]').forEach(card => {
|
||||
card.addEventListener('click', (e) => {
|
||||
const serverName = card.dataset.serverName;
|
||||
const serverConfig = JSON.parse(card.dataset.serverConfig);
|
||||
showMcpEditModal(serverName, serverConfig, 'codex', 'codex');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1068,15 +1220,39 @@ function attachMcpEventListeners() {
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// MCP Details Modal
|
||||
// MCP Edit Modal (replaces Details Modal)
|
||||
// ========================================
|
||||
|
||||
function showMcpDetails(serverName, serverConfig, serverSource) {
|
||||
// Store current editing context
|
||||
let mcpEditContext = {
|
||||
serverName: null,
|
||||
serverConfig: null,
|
||||
serverSource: null,
|
||||
cliType: 'claude'
|
||||
};
|
||||
|
||||
function showMcpDetails(serverName, serverConfig, serverSource, cliType = 'claude') {
|
||||
showMcpEditModal(serverName, serverConfig, serverSource, cliType);
|
||||
}
|
||||
|
||||
function showMcpEditModal(serverName, serverConfig, serverSource, cliType = 'claude') {
|
||||
const modal = document.getElementById('mcpDetailsModal');
|
||||
const modalBody = document.getElementById('mcpDetailsModalBody');
|
||||
|
||||
if (!modal || !modalBody) return;
|
||||
|
||||
// Store editing context
|
||||
mcpEditContext = {
|
||||
serverName,
|
||||
serverConfig: JSON.parse(JSON.stringify(serverConfig)), // Deep clone
|
||||
serverSource,
|
||||
cliType
|
||||
};
|
||||
|
||||
// Check if editable (enterprise is read-only)
|
||||
const isReadOnly = serverSource === 'enterprise';
|
||||
const isCodex = cliType === 'codex';
|
||||
|
||||
// Build source badge
|
||||
let sourceBadge = '';
|
||||
if (serverSource === 'enterprise') {
|
||||
@@ -1085,74 +1261,271 @@ function showMcpDetails(serverName, serverConfig, serverSource) {
|
||||
sourceBadge = `<span class="inline-flex items-center px-2 py-1 text-xs font-semibold rounded-full bg-success/10 text-success">${t('mcp.sourceGlobal')}</span>`;
|
||||
} else if (serverSource === 'project') {
|
||||
sourceBadge = `<span class="inline-flex items-center px-2 py-1 text-xs font-semibold rounded-full bg-primary/10 text-primary">${t('mcp.sourceProject')}</span>`;
|
||||
} else if (isCodex) {
|
||||
sourceBadge = `<span class="inline-flex items-center px-2 py-1 text-xs font-semibold rounded-full bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-300">Codex</span>`;
|
||||
}
|
||||
|
||||
// Build environment variables display
|
||||
let envHtml = '';
|
||||
if (serverConfig.env && Object.keys(serverConfig.env).length > 0) {
|
||||
envHtml = '<div class="mt-4"><h4 class="font-semibold text-sm text-foreground mb-2">' + t('mcp.env') + '</h4><div class="bg-muted rounded-lg p-3 space-y-1 font-mono text-xs">';
|
||||
for (const [key, value] of Object.entries(serverConfig.env)) {
|
||||
envHtml += `<div class="flex items-start gap-2"><span class="text-muted-foreground shrink-0">${escapeHtml(key)}:</span><span class="text-foreground break-all">${escapeHtml(value)}</span></div>`;
|
||||
}
|
||||
envHtml += '</div></div>';
|
||||
} else {
|
||||
envHtml = '<div class="mt-4"><h4 class="font-semibold text-sm text-foreground mb-2">' + t('mcp.env') + '</h4><p class="text-sm text-muted-foreground">' + t('mcp.detailsModal.noEnv') + '</p></div>';
|
||||
}
|
||||
// Format args and env for textarea
|
||||
const argsText = (serverConfig.args || []).join('\n');
|
||||
const envText = Object.entries(serverConfig.env || {}).map(([k, v]) => `${k}=${v}`).join('\n');
|
||||
|
||||
// Build edit form HTML
|
||||
modalBody.innerHTML = `
|
||||
<div class="space-y-4">
|
||||
<!-- Server Name and Source -->
|
||||
<div>
|
||||
<label class="text-xs font-semibold text-muted-foreground uppercase tracking-wide">${t('mcp.detailsModal.serverName')}</label>
|
||||
<div class="mt-1 flex items-center gap-2">
|
||||
<h3 class="text-xl font-bold text-foreground">${escapeHtml(serverName)}</h3>
|
||||
<input type="text" id="mcpEditName" value="${escapeHtml(serverName)}"
|
||||
class="text-lg font-bold text-foreground bg-transparent border-b border-border focus:border-primary outline-none px-1 py-0.5 flex-1"
|
||||
${isReadOnly ? 'disabled' : ''}
|
||||
placeholder="${t('mcp.editModal.serverNamePlaceholder')}">
|
||||
${sourceBadge}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Configuration -->
|
||||
<!-- Command/URL -->
|
||||
<div>
|
||||
<h4 class="font-semibold text-sm text-foreground mb-2">${t('mcp.detailsModal.configuration')}</h4>
|
||||
<div class="space-y-2">
|
||||
<!-- Command -->
|
||||
<div class="flex items-start gap-3">
|
||||
<span class="font-mono text-xs bg-muted px-2 py-1 rounded shrink-0">${t('mcp.cmd')}</span>
|
||||
<code class="text-sm font-mono text-foreground break-all">${escapeHtml(serverConfig.command || serverConfig.url || 'N/A')}</code>
|
||||
</div>
|
||||
<label class="text-xs font-semibold text-muted-foreground uppercase tracking-wide mb-1 block">
|
||||
${serverConfig.url ? t('mcp.url') : t('mcp.cmd')}
|
||||
</label>
|
||||
<input type="text" id="mcpEditCommand" value="${escapeHtml(serverConfig.command || serverConfig.url || '')}"
|
||||
class="w-full px-3 py-2 text-sm font-mono bg-muted border border-border rounded-lg focus:border-primary outline-none"
|
||||
${isReadOnly ? 'disabled' : ''}
|
||||
placeholder="${serverConfig.url ? 'https://...' : 'npx, node, python...'}">
|
||||
</div>
|
||||
|
||||
<!-- Arguments -->
|
||||
${serverConfig.args && serverConfig.args.length > 0 ? `
|
||||
<div class="flex items-start gap-3">
|
||||
<span class="font-mono text-xs bg-muted px-2 py-1 rounded shrink-0">${t('mcp.args')}</span>
|
||||
<div class="flex-1 space-y-1">
|
||||
${serverConfig.args.map((arg, index) => `
|
||||
<div class="text-sm font-mono text-foreground flex items-center gap-2">
|
||||
<span class="text-muted-foreground">[${index}]</span>
|
||||
<code class="break-all">${escapeHtml(arg)}</code>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
<!-- Arguments -->
|
||||
<div>
|
||||
<label class="text-xs font-semibold text-muted-foreground uppercase tracking-wide mb-1 block">
|
||||
${t('mcp.args')} <span class="font-normal">(${t('mcp.editModal.onePerLine')})</span>
|
||||
</label>
|
||||
<textarea id="mcpEditArgs" rows="3"
|
||||
class="w-full px-3 py-2 text-sm font-mono bg-muted border border-border rounded-lg focus:border-primary outline-none resize-none"
|
||||
${isReadOnly ? 'disabled' : ''}
|
||||
placeholder="-y package-name">${escapeHtml(argsText)}</textarea>
|
||||
</div>
|
||||
|
||||
<!-- Environment Variables -->
|
||||
${envHtml}
|
||||
|
||||
<!-- Raw JSON -->
|
||||
<div>
|
||||
<h4 class="font-semibold text-sm text-foreground mb-2">Raw JSON</h4>
|
||||
<pre class="bg-muted rounded-lg p-3 text-xs font-mono overflow-x-auto">${escapeHtml(JSON.stringify(serverConfig, null, 2))}</pre>
|
||||
<label class="text-xs font-semibold text-muted-foreground uppercase tracking-wide mb-1 block">
|
||||
${t('mcp.env')} <span class="font-normal">(KEY=VALUE ${t('mcp.editModal.onePerLine')})</span>
|
||||
</label>
|
||||
<textarea id="mcpEditEnv" rows="3"
|
||||
class="w-full px-3 py-2 text-sm font-mono bg-muted border border-border rounded-lg focus:border-primary outline-none resize-none"
|
||||
${isReadOnly ? 'disabled' : ''}
|
||||
placeholder="API_KEY=your-key DEBUG=true">${escapeHtml(envText)}</textarea>
|
||||
</div>
|
||||
|
||||
${isCodex ? `
|
||||
<!-- Codex-specific: enabled_tools -->
|
||||
<div>
|
||||
<label class="text-xs font-semibold text-muted-foreground uppercase tracking-wide mb-1 block">
|
||||
${t('mcp.codex.enabledTools')} <span class="font-normal">(${t('mcp.editModal.onePerLine')})</span>
|
||||
</label>
|
||||
<textarea id="mcpEditEnabledTools" rows="2"
|
||||
class="w-full px-3 py-2 text-sm font-mono bg-muted border border-border rounded-lg focus:border-primary outline-none resize-none"
|
||||
${isReadOnly ? 'disabled' : ''}
|
||||
placeholder="tool1 tool2">${escapeHtml((serverConfig.enabled_tools || []).join('\n'))}</textarea>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<!-- Raw JSON Preview (collapsible) -->
|
||||
<details class="group">
|
||||
<summary class="text-xs font-semibold text-muted-foreground uppercase tracking-wide cursor-pointer flex items-center gap-1">
|
||||
<i data-lucide="chevron-right" class="w-3 h-3 transition-transform group-open:rotate-90"></i>
|
||||
Raw JSON
|
||||
</summary>
|
||||
<pre id="mcpEditJsonPreview" class="mt-2 bg-muted rounded-lg p-3 text-xs font-mono overflow-x-auto">${escapeHtml(JSON.stringify(serverConfig, null, 2))}</pre>
|
||||
</details>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
${!isReadOnly ? `
|
||||
<div class="flex items-center justify-between pt-4 border-t border-border">
|
||||
<div class="flex items-center gap-2">
|
||||
${serverSource === 'project' || isCodex ? `
|
||||
<button onclick="deleteMcpFromEdit()" class="px-4 py-2 text-sm text-destructive hover:bg-destructive/10 rounded-lg transition-colors flex items-center gap-1.5">
|
||||
<i data-lucide="trash-2" class="w-4 h-4"></i>
|
||||
${t('mcp.editModal.delete')}
|
||||
</button>
|
||||
` : ''}
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<button onclick="closeMcpEditModal()" class="px-4 py-2 text-sm text-muted-foreground hover:bg-muted rounded-lg transition-colors">
|
||||
${t('common.cancel')}
|
||||
</button>
|
||||
<button onclick="saveMcpEdit()" class="px-4 py-2 text-sm bg-primary text-primary-foreground rounded-lg hover:opacity-90 transition-opacity flex items-center gap-1.5">
|
||||
<i data-lucide="check" class="w-4 h-4"></i>
|
||||
${t('mcp.editModal.save')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
` : `
|
||||
<div class="flex items-center justify-end pt-4 border-t border-border">
|
||||
<button onclick="closeMcpEditModal()" class="px-4 py-2 text-sm bg-muted text-foreground rounded-lg hover:bg-muted/80 transition-colors">
|
||||
${t('common.close')}
|
||||
</button>
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Update modal title
|
||||
const modalTitle = modal.querySelector('h2');
|
||||
if (modalTitle) {
|
||||
modalTitle.textContent = isReadOnly ? t('mcp.detailsModal.title') : t('mcp.editModal.title');
|
||||
}
|
||||
|
||||
// Show modal
|
||||
modal.classList.remove('hidden');
|
||||
|
||||
// Re-initialize Lucide icons in modal
|
||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||
|
||||
// Add input listeners to update JSON preview
|
||||
if (!isReadOnly) {
|
||||
['mcpEditCommand', 'mcpEditArgs', 'mcpEditEnv', 'mcpEditEnabledTools'].forEach(id => {
|
||||
const el = document.getElementById(id);
|
||||
if (el) {
|
||||
el.addEventListener('input', updateMcpEditJsonPreview);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function closeMcpEditModal() {
|
||||
const modal = document.getElementById('mcpDetailsModal');
|
||||
if (modal) {
|
||||
modal.classList.add('hidden');
|
||||
}
|
||||
mcpEditContext = { serverName: null, serverConfig: null, serverSource: null, cliType: 'claude' };
|
||||
}
|
||||
|
||||
function updateMcpEditJsonPreview() {
|
||||
const preview = document.getElementById('mcpEditJsonPreview');
|
||||
if (!preview) return;
|
||||
|
||||
const config = buildConfigFromEditForm();
|
||||
preview.textContent = JSON.stringify(config, null, 2);
|
||||
}
|
||||
|
||||
function buildConfigFromEditForm() {
|
||||
const command = document.getElementById('mcpEditCommand')?.value.trim() || '';
|
||||
const argsText = document.getElementById('mcpEditArgs')?.value.trim() || '';
|
||||
const envText = document.getElementById('mcpEditEnv')?.value.trim() || '';
|
||||
const enabledToolsEl = document.getElementById('mcpEditEnabledTools');
|
||||
|
||||
// Build config
|
||||
const config = {};
|
||||
|
||||
// Command or URL
|
||||
if (mcpEditContext.serverConfig?.url) {
|
||||
config.url = command;
|
||||
} else {
|
||||
config.command = command;
|
||||
}
|
||||
|
||||
// Args
|
||||
if (argsText) {
|
||||
config.args = argsText.split('\n').map(a => a.trim()).filter(a => a);
|
||||
}
|
||||
|
||||
// Env
|
||||
if (envText) {
|
||||
config.env = {};
|
||||
envText.split('\n').forEach(line => {
|
||||
const trimmed = line.trim();
|
||||
if (trimmed && trimmed.includes('=')) {
|
||||
const eqIndex = trimmed.indexOf('=');
|
||||
const key = trimmed.substring(0, eqIndex).trim();
|
||||
const value = trimmed.substring(eqIndex + 1).trim();
|
||||
if (key) {
|
||||
config.env[key] = value;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Codex-specific: enabled_tools
|
||||
if (enabledToolsEl) {
|
||||
const toolsText = enabledToolsEl.value.trim();
|
||||
if (toolsText) {
|
||||
config.enabled_tools = toolsText.split('\n').map(t => t.trim()).filter(t => t);
|
||||
}
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
async function saveMcpEdit() {
|
||||
const newName = document.getElementById('mcpEditName')?.value.trim();
|
||||
if (!newName) {
|
||||
showRefreshToast(t('mcp.editModal.nameRequired'), 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const newConfig = buildConfigFromEditForm();
|
||||
|
||||
if (!newConfig.command && !newConfig.url) {
|
||||
showRefreshToast(t('mcp.editModal.commandRequired'), 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const { serverName, serverSource, cliType } = mcpEditContext;
|
||||
const nameChanged = newName !== serverName;
|
||||
|
||||
try {
|
||||
if (cliType === 'codex') {
|
||||
// Codex MCP update
|
||||
// If name changed, remove old and add new
|
||||
if (nameChanged) {
|
||||
await removeCodexMcpServer(serverName);
|
||||
}
|
||||
await addCodexMcpServer(newName, newConfig);
|
||||
} else if (serverSource === 'global') {
|
||||
// Global MCP update
|
||||
if (nameChanged) {
|
||||
await removeGlobalMcpServer(serverName);
|
||||
}
|
||||
await addGlobalMcpServer(newName, newConfig);
|
||||
} else if (serverSource === 'project') {
|
||||
// Project MCP update
|
||||
if (nameChanged) {
|
||||
await removeMcpServerFromProject(serverName);
|
||||
}
|
||||
await copyMcpServerToProject(newName, newConfig, 'mcp');
|
||||
}
|
||||
|
||||
closeMcpEditModal();
|
||||
showRefreshToast(t('mcp.editModal.saved', { name: newName }), 'success');
|
||||
} catch (err) {
|
||||
console.error('Failed to save MCP edit:', err);
|
||||
showRefreshToast(t('mcp.editModal.saveFailed') + ': ' + err.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteMcpFromEdit() {
|
||||
const { serverName, serverSource, cliType } = mcpEditContext;
|
||||
|
||||
if (!confirm(t('mcp.editModal.deleteConfirm', { name: serverName }))) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (cliType === 'codex') {
|
||||
await removeCodexMcpServer(serverName);
|
||||
} else if (serverSource === 'global') {
|
||||
await removeGlobalMcpServer(serverName);
|
||||
} else if (serverSource === 'project') {
|
||||
await removeMcpServerFromProject(serverName);
|
||||
}
|
||||
|
||||
closeMcpEditModal();
|
||||
showRefreshToast(t('mcp.editModal.deleted', { name: serverName }), 'success');
|
||||
} catch (err) {
|
||||
console.error('Failed to delete MCP:', err);
|
||||
showRefreshToast(t('mcp.editModal.deleteFailed') + ': ' + err.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
|
||||
@@ -345,6 +345,7 @@ function renderHotspotsColumn() {
|
||||
|
||||
var mostRead = memoryStats.mostRead || [];
|
||||
var mostEdited = memoryStats.mostEdited || [];
|
||||
var mostMentioned = memoryStats.mostMentioned || [];
|
||||
|
||||
container.innerHTML = '<div class="memory-section">' +
|
||||
'<div class="section-header">' +
|
||||
@@ -371,6 +372,10 @@ function renderHotspotsColumn() {
|
||||
'<h4 class="hotspot-list-title"><i data-lucide="pencil" class="w-3.5 h-3.5"></i> ' + t('memory.mostEdited') + '</h4>' +
|
||||
renderHotspotList(mostEdited, 'edit') +
|
||||
'</div>' +
|
||||
'<div class="hotspot-list-container">' +
|
||||
'<h4 class="hotspot-list-title"><i data-lucide="message-circle" class="w-3.5 h-3.5"></i> ' + t('memory.mostMentioned') + '</h4>' +
|
||||
renderTopicList(mostMentioned) +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
|
||||
@@ -380,7 +385,7 @@ function renderHotspotsColumn() {
|
||||
function renderHotspotList(items, type) {
|
||||
if (!items || items.length === 0) {
|
||||
return '<div class="hotspot-empty">' +
|
||||
'<i data-lucide="inbox" class="w-6 h-6"></i>' +
|
||||
'<i data-lucide="inbox" class="w-5 h-5"></i>' +
|
||||
'<p>' + t('memory.noData') + '</p>' +
|
||||
'</div>';
|
||||
}
|
||||
@@ -407,6 +412,34 @@ function renderHotspotList(items, type) {
|
||||
'</div>';
|
||||
}
|
||||
|
||||
function renderTopicList(items) {
|
||||
if (!items || items.length === 0) {
|
||||
return '<div class="hotspot-empty">' +
|
||||
'<i data-lucide="inbox" class="w-5 h-5"></i>' +
|
||||
'<p>' + t('memory.noData') + '</p>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
return '<div class="hotspot-list topic-list">' +
|
||||
items.map(function(item, index) {
|
||||
var heat = item.heat || item.count || 0;
|
||||
var heatClass = heat > 10 ? 'high' : heat > 5 ? 'medium' : 'low';
|
||||
var preview = item.preview || item.topic || 'Unknown';
|
||||
|
||||
return '<div class="hotspot-item topic-item">' +
|
||||
'<div class="hotspot-rank">' + (index + 1) + '</div>' +
|
||||
'<div class="hotspot-info">' +
|
||||
'<div class="hotspot-name topic-preview" title="' + escapeHtml(item.topic || '') + '">' + escapeHtml(preview) + '</div>' +
|
||||
'</div>' +
|
||||
'<div class="hotspot-heat ' + heatClass + '">' +
|
||||
'<span class="heat-badge">' + heat + '</span>' +
|
||||
'<i data-lucide="message-circle" class="w-3 h-3"></i>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}).join('') +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
// ========== Center Column: Memory Graph ==========
|
||||
// Store graph state for zoom/pan
|
||||
var graphZoom = null;
|
||||
@@ -458,7 +491,7 @@ function renderMemoryGraph(graphData) {
|
||||
var container = document.getElementById('memoryGraphSvg');
|
||||
if (container) {
|
||||
container.innerHTML = '<div class="graph-empty-state">' +
|
||||
'<i data-lucide="network" class="w-12 h-12"></i>' +
|
||||
'<i data-lucide="network" class="w-8 h-8"></i>' +
|
||||
'<p>' + t('memory.noGraphData') + '</p>' +
|
||||
'</div>';
|
||||
if (window.lucide) lucide.createIcons();
|
||||
@@ -471,7 +504,7 @@ function renderMemoryGraph(graphData) {
|
||||
var container = document.getElementById('memoryGraphSvg');
|
||||
if (container) {
|
||||
container.innerHTML = '<div class="graph-error">' +
|
||||
'<i data-lucide="alert-triangle" class="w-8 h-8"></i>' +
|
||||
'<i data-lucide="alert-triangle" class="w-6 h-6"></i>' +
|
||||
'<p>' + t('memory.d3NotLoaded') + '</p>' +
|
||||
'</div>';
|
||||
if (window.lucide) lucide.createIcons();
|
||||
@@ -767,7 +800,7 @@ function renderContextColumn() {
|
||||
function renderContextTimeline(prompts) {
|
||||
if (!prompts || prompts.length === 0) {
|
||||
return '<div class="context-empty">' +
|
||||
'<i data-lucide="inbox" class="w-8 h-8"></i>' +
|
||||
'<i data-lucide="inbox" class="w-6 h-6"></i>' +
|
||||
'<p>' + t('memory.noRecentActivity') + '</p>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
@@ -248,7 +248,7 @@
|
||||
</button>
|
||||
<div class="flex items-center gap-2 text-lg font-semibold text-primary">
|
||||
<i data-lucide="workflow" class="w-6 h-6"></i>
|
||||
<span class="hidden sm:inline">Claude Code Workflow</span>
|
||||
<span data-i18n="app.brand">Claude Code Workflow</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user