feat: CCW Dashboard 增强 - 停止命令、浏览器修复和MCP多源配置

- 新增 ccw stop 命令支持优雅停止和强制终止 (--force)
- 修复 ccw view 服务器检测时浏览器无法打开的问题
- MCP 配置现在从多个源读取:
  - ~/.claude.json (项目级)
  - ~/.claude/settings.json 和 settings.local.json (全局)
  - 各工作空间的 .claude/settings.json (工作空间级)
- 新增全局 MCP 服务器显示区域
- 修复路径选择模态框样式问题

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
catlog22
2025-12-08 10:28:07 +08:00
parent f4299457fb
commit 27273405f7
8 changed files with 377 additions and 21 deletions

View File

@@ -4,6 +4,7 @@
// ========== MCP State ==========
let mcpConfig = null;
let mcpAllProjects = {};
let mcpGlobalServers = {};
let mcpCurrentProjectServers = {};
let mcpCreateMode = 'form'; // 'form' or 'json'
@@ -31,6 +32,7 @@ async function loadMcpConfig() {
const data = await response.json();
mcpConfig = data;
mcpAllProjects = data.projects || {};
mcpGlobalServers = data.globalServers || {};
// Get current project servers
const currentPath = projectPath.replace(/\//g, '\\');
@@ -150,6 +152,15 @@ function updateMcpBadge() {
function getAllAvailableMcpServers() {
const allServers = {};
// Collect global servers first
for (const [name, serverConfig] of Object.entries(mcpGlobalServers)) {
allServers[name] = {
config: serverConfig,
usedIn: [],
isGlobal: true
};
}
// Collect servers from all projects
for (const [path, config] of Object.entries(mcpAllProjects)) {
const servers = config.mcpServers || {};
@@ -157,7 +168,8 @@ function getAllAvailableMcpServers() {
if (!allServers[name]) {
allServers[name] = {
config: serverConfig,
usedIn: []
usedIn: [],
isGlobal: false
};
}
allServers[name].usedIn.push(path);

View File

@@ -26,8 +26,12 @@ async function renderMcpManager() {
// Separate current project servers and available servers
const currentProjectServerNames = Object.keys(projectServers);
const otherAvailableServers = Object.entries(allAvailableServers)
// Separate global servers and project servers that are not in current project
const globalServerEntries = Object.entries(mcpGlobalServers)
.filter(([name]) => !currentProjectServerNames.includes(name));
const otherProjectServers = Object.entries(allAvailableServers)
.filter(([name, info]) => !currentProjectServerNames.includes(name) && !info.isGlobal);
container.innerHTML = `
<div class="mcp-manager">
@@ -61,20 +65,39 @@ async function renderMcpManager() {
`}
</div>
<!-- Global MCP Servers -->
${globalServerEntries.length > 0 ? `
<div class="mcp-section mb-6">
<div class="flex items-center justify-between mb-4">
<div class="flex items-center gap-2">
<span class="text-lg">🌐</span>
<h3 class="text-lg font-semibold text-foreground">Global MCP Servers</h3>
</div>
<span class="text-sm text-muted-foreground">${globalServerEntries.length} servers from ~/.claude/settings</span>
</div>
<div class="mcp-server-grid grid gap-3">
${globalServerEntries.map(([serverName, serverConfig]) => {
return renderGlobalServerCard(serverName, serverConfig);
}).join('')}
</div>
</div>
` : ''}
<!-- Available MCP Servers from Other Projects -->
<div class="mcp-section">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-foreground">Available from Other Projects</h3>
<span class="text-sm text-muted-foreground">${otherAvailableServers.length} servers available</span>
<span class="text-sm text-muted-foreground">${otherProjectServers.length} servers available</span>
</div>
${otherAvailableServers.length === 0 ? `
${otherProjectServers.length === 0 ? `
<div class="mcp-empty-state bg-card border border-border rounded-lg p-6 text-center">
<p class="text-muted-foreground">No additional MCP servers found in other projects</p>
</div>
` : `
<div class="mcp-server-grid grid gap-3">
${otherAvailableServers.map(([serverName, serverInfo]) => {
${otherProjectServers.map(([serverName, serverInfo]) => {
return renderAvailableServerCard(serverName, serverInfo);
}).join('')}
</div>
@@ -240,6 +263,52 @@ function renderAvailableServerCard(serverName, serverInfo) {
`;
}
function renderGlobalServerCard(serverName, serverConfig) {
const command = serverConfig.command || 'N/A';
const args = serverConfig.args || [];
const hasEnv = serverConfig.env && Object.keys(serverConfig.env).length > 0;
return `
<div class="mcp-server-card mcp-server-global bg-card border border-primary/30 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">
<span class="text-xl">🌐</span>
<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">Global</span>
</div>
<button class="px-3 py-1 text-xs bg-primary text-primary-foreground rounded hover:opacity-90 transition-opacity"
data-server-name="${escapeHtml(serverName)}"
data-server-config='${JSON.stringify(serverConfig).replace(/'/g, "&#39;")}'
data-action="add">
Add to Project
</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">cmd</span>
<span class="truncate" title="${escapeHtml(command)}">${escapeHtml(command)}</span>
</div>
${args.length > 0 ? `
<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">args</span>
<span class="text-xs font-mono truncate" title="${escapeHtml(args.join(' '))}">${escapeHtml(args.slice(0, 3).join(' '))}${args.length > 3 ? '...' : ''}</span>
</div>
` : ''}
${hasEnv ? `
<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">env</span>
<span class="text-xs">${Object.keys(serverConfig.env).length} variables</span>
</div>
` : ''}
<div class="flex items-center gap-2 text-muted-foreground mt-1">
<span class="text-xs italic">Available to all projects from ~/.claude/settings</span>
</div>
</div>
</div>
`;
}
function attachMcpEventListeners() {
// Toggle switches
document.querySelectorAll('.mcp-server-card input[data-action="toggle"]').forEach(input => {

View File

@@ -8112,3 +8112,76 @@ code.ctx-meta-chip-value {
opacity: 0.5;
cursor: not-allowed;
}
/* Path Input Group */
.path-input-group {
display: flex;
align-items: center;
gap: 0.75rem;
flex-wrap: wrap;
}
.path-input-group label {
font-size: 0.875rem;
color: hsl(var(--muted-foreground));
white-space: nowrap;
}
.path-input-group input {
flex: 1;
min-width: 200px;
padding: 0.625rem 0.875rem;
background: hsl(var(--background));
border: 1px solid hsl(var(--border));
border-radius: 0.375rem;
font-size: 0.875rem;
font-family: var(--font-mono);
color: hsl(var(--foreground));
outline: none;
transition: border-color 0.15s, box-shadow 0.15s;
}
.path-input-group input:focus {
border-color: hsl(var(--primary));
box-shadow: 0 0 0 3px hsl(var(--primary) / 0.1);
}
.path-input-group input::placeholder {
color: hsl(var(--muted-foreground));
}
.path-go-btn {
padding: 0.625rem 1.25rem;
background: hsl(var(--primary));
color: white;
border: none;
border-radius: 0.375rem;
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: all 0.15s;
white-space: nowrap;
}
.path-go-btn:hover {
background: hsl(var(--primary) / 0.9);
transform: translateY(-1px);
}
.path-go-btn:active {
transform: translateY(0);
}
/* Selected Folder Display */
.selected-folder {
padding: 0.75rem 1rem;
background: hsl(var(--muted));
border-radius: 0.5rem;
margin-bottom: 0.75rem;
}
.selected-folder strong {
font-size: 1rem;
color: hsl(var(--foreground));
font-family: var(--font-mono);
}