feat: Implement association tree for LSP-based code relationship discovery

- Add `association_tree` module with components for building and processing call association trees using LSP call hierarchy capabilities.
- Introduce `AssociationTreeBuilder` for constructing call trees from seed locations with depth-first expansion.
- Create data structures: `TreeNode`, `CallTree`, and `UniqueNode` for representing nodes and relationships in the call tree.
- Implement `ResultDeduplicator` to extract unique nodes from call trees and assign relevance scores based on depth, frequency, and kind.
- Add unit tests for `AssociationTreeBuilder` and `ResultDeduplicator` to ensure functionality and correctness.
This commit is contained in:
catlog22
2026-01-20 22:09:04 +08:00
parent b85d9b9eb1
commit 261c98549d
21 changed files with 2826 additions and 94 deletions

View File

@@ -130,27 +130,62 @@
/* Archived Issue Card */
.issue-card.archived {
opacity: 0.85;
background: hsl(var(--muted) / 0.3);
opacity: 0.9;
background: linear-gradient(135deg, hsl(var(--muted) / 0.2), hsl(var(--muted) / 0.4));
border-style: dashed;
border-color: hsl(var(--border) / 0.7);
}
.issue-card.archived:hover {
opacity: 1;
border-color: hsl(var(--primary) / 0.5);
}
.issue-card.archived .issue-title {
color: hsl(var(--muted-foreground));
}
.issue-archived-badge {
display: inline-flex;
align-items: center;
padding: 0.125rem 0.375rem;
background: hsl(var(--muted));
color: hsl(var(--muted-foreground));
gap: 0.25rem;
padding: 0.125rem 0.5rem;
background: hsl(210 40% 96%);
color: hsl(215 16% 47%);
font-size: 0.625rem;
font-weight: 500;
border-radius: 0.25rem;
font-weight: 600;
border-radius: 9999px;
text-transform: uppercase;
letter-spacing: 0.025em;
}
.issue-archived-badge i {
opacity: 0.8;
}
/* Dark mode archived badge */
:root[data-theme="dark"] .issue-archived-badge,
.dark .issue-archived-badge {
background: hsl(217 33% 17%);
color: hsl(215 20% 65%);
}
/* Archived footer with timestamp */
.issue-archived-footer {
display: flex;
align-items: center;
gap: 0.375rem;
margin-top: 0.75rem;
padding-top: 0.625rem;
border-top: 1px dashed hsl(var(--border) / 0.5);
font-size: 0.6875rem;
color: hsl(var(--muted-foreground));
}
.issue-archived-footer i {
opacity: 0.7;
}
.issue-card-header {
display: flex;
align-items: flex-start;

View File

@@ -115,9 +115,12 @@ async function syncActiveExecutions() {
renderStreamTabs();
updateStreamBadge();
// If viewer is open, render content
// If viewer is open, render content. If not, and there's a running execution, open it.
if (isCliStreamViewerOpen) {
renderStreamContent(activeStreamTab);
} else if (executions.some(e => e.status === 'running')) {
// Automatically open the viewer if it's closed and we just synced a running task
toggleCliStreamViewer();
}
}

View File

@@ -1095,9 +1095,16 @@ function getCcwPathConfig() {
// Get CCW_DISABLE_SANDBOX checkbox status for Claude Code mode
function getCcwDisableSandbox() {
// Check if already installed and has the setting
const ccwToolsConfig = projectMcpServers?.['ccw-tools'] || globalServers?.['ccw-tools'];
return ccwToolsConfig?.env?.CCW_DISABLE_SANDBOX === '1' || ccwToolsConfig?.env?.CCW_DISABLE_SANDBOX === 'true';
// Try project config first, then global config
const currentPath = projectPath; // projectPath is from state.js
const projectData = mcpAllProjects[currentPath] || {};
const projectCcwConfig = projectData.mcpServers?.['ccw-tools'];
if (projectCcwConfig?.env?.CCW_DISABLE_SANDBOX) {
return projectCcwConfig.env.CCW_DISABLE_SANDBOX === '1' || projectCcwConfig.env.CCW_DISABLE_SANDBOX === 'true';
}
// Fallback to global config
const globalCcwConfig = mcpGlobalServers?.['ccw-tools'];
return globalCcwConfig?.env?.CCW_DISABLE_SANDBOX === '1' || globalCcwConfig?.env?.CCW_DISABLE_SANDBOX === 'true';
}
// Get CCW_DISABLE_SANDBOX checkbox status for Codex mode
@@ -1452,6 +1459,7 @@ const RECOMMENDED_MCP_SERVERS = [
descKey: 'mcp.codexLens.desc',
icon: 'code-2',
category: 'code-intelligence',
hidden: true, // Hide from recommended list (not ready for production)
fields: [
{
key: 'tools',
@@ -1476,9 +1484,9 @@ const RECOMMENDED_MCP_SERVERS = [
}
];
// Get recommended MCP servers list
// Get recommended MCP servers list (exclude hidden ones)
function getRecommendedMcpServers() {
return RECOMMENDED_MCP_SERVERS;
return RECOMMENDED_MCP_SERVERS.filter(mcp => !mcp.hidden);
}
// Check if a recommended MCP is already installed

View File

@@ -378,6 +378,7 @@ function renderIssueCard(issue) {
};
const isArchived = issue._isArchived;
const archivedDate = issue.archived_at ? new Date(issue.archived_at).toLocaleDateString() : null;
return `
<div class="issue-card ${isArchived ? 'archived' : ''}" onclick="openIssueDetail('${issue.id}'${isArchived ? ', true' : ''})">
@@ -385,7 +386,12 @@ function renderIssueCard(issue) {
<div class="flex items-center gap-2">
<span class="issue-id font-mono text-sm">${highlightMatch(issue.id, issueData.searchQuery)}</span>
<span class="issue-status ${statusColors[issue.status] || ''}">${issue.status || 'unknown'}</span>
${isArchived ? '<span class="issue-archived-badge">' + (t('issues.archived') || 'Archived') + '</span>' : ''}
${isArchived ? `
<span class="issue-archived-badge" title="Archived on ${archivedDate || 'Unknown'}">
<i data-lucide="archive" class="w-3 h-3"></i>
<span>${t('issues.archived') || 'Archived'}</span>
</span>
` : ''}
</div>
<span class="issue-priority" title="${t('issues.priority') || 'Priority'}: ${issue.priority || 3}">
${renderPriorityStars(issue.priority || 3)}
@@ -418,6 +424,13 @@ function renderIssueCard(issue) {
</a>
` : ''}
</div>
${isArchived && archivedDate ? `
<div class="issue-archived-footer">
<i data-lucide="clock" class="w-3 h-3"></i>
<span>Archived on ${archivedDate}</span>
</div>
` : ''}
</div>
`;
}