mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-10 02:24:35 +08:00
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:
@@ -72,6 +72,44 @@ export function getActiveExecutions(): ActiveExecution[] {
|
||||
return Array.from(activeExecutions.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* Update active execution state from hook events
|
||||
* Called by hooks-routes when CLI events are received from terminal execution
|
||||
*/
|
||||
export function updateActiveExecution(event: {
|
||||
type: 'started' | 'output' | 'completed';
|
||||
executionId: string;
|
||||
tool?: string;
|
||||
mode?: string;
|
||||
prompt?: string;
|
||||
output?: string;
|
||||
success?: boolean;
|
||||
}): void {
|
||||
const { type, executionId, tool, mode, prompt, output, success } = event;
|
||||
|
||||
if (type === 'started') {
|
||||
// Create new active execution
|
||||
activeExecutions.set(executionId, {
|
||||
id: executionId,
|
||||
tool: tool || 'unknown',
|
||||
mode: mode || 'analysis',
|
||||
prompt: (prompt || '').substring(0, 500),
|
||||
startTime: Date.now(),
|
||||
output: '',
|
||||
status: 'running'
|
||||
});
|
||||
} else if (type === 'output') {
|
||||
// Append output to existing execution
|
||||
const activeExec = activeExecutions.get(executionId);
|
||||
if (activeExec && output) {
|
||||
activeExec.output += output;
|
||||
}
|
||||
} else if (type === 'completed') {
|
||||
// Remove from active executions
|
||||
activeExecutions.delete(executionId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle CLI routes
|
||||
* @returns true if route was handled, false otherwise
|
||||
|
||||
@@ -266,6 +266,37 @@ export async function handleHooksRoutes(ctx: HooksRouteContext): Promise<boolean
|
||||
}
|
||||
}
|
||||
|
||||
// Update active executions state for CLI streaming events (terminal execution)
|
||||
if (type === 'CLI_EXECUTION_STARTED' || type === 'CLI_OUTPUT' || type === 'CLI_EXECUTION_COMPLETED') {
|
||||
try {
|
||||
const { updateActiveExecution } = await import('./cli-routes.js');
|
||||
|
||||
if (type === 'CLI_EXECUTION_STARTED') {
|
||||
updateActiveExecution({
|
||||
type: 'started',
|
||||
executionId: String(extraData.executionId || ''),
|
||||
tool: String(extraData.tool || 'unknown'),
|
||||
mode: String(extraData.mode || 'analysis'),
|
||||
prompt: String(extraData.prompt_preview || '')
|
||||
});
|
||||
} else if (type === 'CLI_OUTPUT') {
|
||||
updateActiveExecution({
|
||||
type: 'output',
|
||||
executionId: String(extraData.executionId || ''),
|
||||
output: String(extraData.data || '')
|
||||
});
|
||||
} else if (type === 'CLI_EXECUTION_COMPLETED') {
|
||||
updateActiveExecution({
|
||||
type: 'completed',
|
||||
executionId: String(extraData.executionId || ''),
|
||||
success: Boolean(extraData.success)
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[Hooks] Failed to update active execution:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// Broadcast to all connected WebSocket clients
|
||||
const notification = {
|
||||
type: typeof type === 'string' && type.trim().length > 0 ? type : 'session_updated',
|
||||
|
||||
@@ -170,7 +170,13 @@ function getIssueDetail(issuesDir: string, issueId: string) {
|
||||
const issues = readIssuesJsonl(issuesDir);
|
||||
let issue = issues.find(i => i.id === issueId);
|
||||
|
||||
// Fallback: Reconstruct issue from solution file if issue not in issues.jsonl
|
||||
// Fix: Check history if not found in active issues
|
||||
if (!issue) {
|
||||
const historyIssues = readIssueHistoryJsonl(issuesDir);
|
||||
issue = historyIssues.find(i => i.id === issueId);
|
||||
}
|
||||
|
||||
// Fallback: Reconstruct issue from solution file if issue not in issues.jsonl or history
|
||||
if (!issue) {
|
||||
const solutionPath = join(issuesDir, 'solutions', `${issueId}.jsonl`);
|
||||
if (existsSync(solutionPath)) {
|
||||
@@ -948,7 +954,8 @@ export async function handleIssueRoutes(ctx: RouteContext): Promise<boolean> {
|
||||
|
||||
// GET /api/issues/history - List completed issues from history
|
||||
if (pathname === '/api/issues/history' && req.method === 'GET') {
|
||||
const history = readIssueHistoryJsonl(issuesDir);
|
||||
// Fix: Use enrichIssues to add solution/task counts to historical issues
|
||||
const history = enrichIssues(readIssueHistoryJsonl(issuesDir), issuesDir);
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
issues: history,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user