refactor(mcp): strip sse fallback architecture in favor of pure websockets and fix nvim NDJSON bug

This commit is contained in:
Riz Ashraf committed 2026-09-17 15:26:22 +01:00
1 parent 0e29b12ac8
commit 3716c3e698
33 files changed
+2082 -1756

No files matched your search

+107 -29
View File
@@ -514,16 +514,8 @@
<div id="task-tab" class="tab-content">
<div class="panel kanban-panel" style="flex:1; display:flex; flex-direction:column;">
<div class="kanban-board" style="flex:1;">
<div class="kanban-column">
<h3>TODO / IN PROGRESS</h3>
<div class="kanban-items" id="tasks-active"></div>
</div>
<div class="kanban-column">
<h3>COMPLETED</h3>
<div class="kanban-items" id="tasks-done"></div>
</div>
</div>
<h3 style="margin-top:0;">Task Network (HTN)</h3>
<div class="kanban-items" id="task-tree-container" style="flex:1; border: 1px solid var(--border-color); padding:15px; border-radius:6px; background:var(--canvas-bg);"></div>
</div>
</div>
@@ -734,38 +726,124 @@
}
}
// --- Kanban Board ---
// --- Task Tree (HTN/DAG) ---
async function completeTask(id) {
try {
await fetch(`/api/tasks/${id}/complete`, { method: 'POST' });
loadTasks(); // Refresh UI instantly
} catch(e) { console.error("Failed to complete task", e); }
}
function buildTaskTreeHTML(tasks, parentId, depth = 0) {
let html = '';
const children = tasks.filter(t => {
const pid = t.parentId || t.parent_id;
if (!parentId) return !pid; // If looking for root, return tasks with no parent
return pid === parentId;
});
if (children.length === 0) return html;
children.forEach(t => {
const isCompleted = t.status === 'completed' || t.status === 'done';
const isCancelled = t.status === 'cancelled' || t.status === 'abandoned';
let cardClass = 'task-card';
if (isCompleted) cardClass += ' completed';
if (isCancelled) cardClass += ' cancelled';
// Find blockers
let isBlocked = false;
let blockers = [];
const deps = t.dependencies || [];
deps.forEach(depId => {
const depTask = tasks.find(dt => dt.id === depId);
if (depTask && depTask.status !== 'completed' && depTask.status !== 'done') {
isBlocked = true;
blockers.push(depTask.title);
}
});
// Child progress
const allChildren = tasks.filter(ct => (ct.parentId || ct.parent_id) === t.id);
const completedChildren = allChildren.filter(ct => ct.status === 'completed' || ct.status === 'done');
let progressHtml = '';
if (allChildren.length > 0) {
const pct = Math.round((completedChildren.length / allChildren.length) * 100);
progressHtml = `
<div style="margin-top:10px; background:#e1e8ed; border-radius:4px; height:8px; overflow:hidden;">
<div style="background:#3498db; width:${pct}%; height:100%; transition:width 0.3s;"></div>
</div>
<div style="font-size:0.8em; color:var(--text-secondary); text-align:right; margin-top:2px;">${pct}% (${completedChildren.length}/${allChildren.length} child tasks)</div>
`;
if (completedChildren.length < allChildren.length) {
isBlocked = true; // Implicitly blocked by children
}
}
html += `<div class="${cardClass}" style="margin-left: ${depth * 20}px; margin-bottom: 10px;">`;
if (isBlocked && !isCompleted && !isCancelled) {
html += `<div style="background:#e74c3c; color:white; font-size:0.75em; padding:2px 6px; border-radius:3px; display:inline-block; margin-bottom:6px; font-weight:bold;">[BLOCKED]</div>`;
if (blockers.length > 0) {
html += `<div style="font-size:0.8em; color:#e74c3c; margin-bottom:6px;">Waiting on: ${blockers.join(', ')}</div>`;
}
}
if (isCancelled) {
html += `<div style="background:#95a5a6; color:white; font-size:0.75em; padding:2px 6px; border-radius:3px; display:inline-block; margin-bottom:6px; font-weight:bold;">[CANCELLED]</div>`;
}
html += `<strong>${t.title}</strong>${t.description}`;
const criteria = t.acceptanceCriteria || t.acceptance_criteria || [];
if (criteria.length > 0) {
html += `<ul style="margin:8px 0 0 0; padding-left:20px; font-size: 0.9em; color: var(--text-secondary);">`;
let unmetCriteria = false;
criteria.forEach(c => {
const isMet = c.isMet || c.is_met;
if (!isMet) unmetCriteria = true;
const check = isMet ? '☑' : '☐';
const strike = isMet ? 'text-decoration: line-through;' : '';
html += `<li style="${strike}">${check} ${c.description}</li>`;
});
html += `</ul>`;
if (unmetCriteria && !isCompleted && !isCancelled) isBlocked = true;
}
html += progressHtml;
if (!isCompleted && !isCancelled && !isBlocked) {
html += `<button class="complete-btn" onclick="completeTask('${t.id}')" title="Mark Completed">✓</button>`;
}
// Recursively render children
if (allChildren.length > 0) {
html += `<div style="margin-top: 15px; border-left: 2px solid var(--border-color); padding-left: 10px;">`;
html += buildTaskTreeHTML(tasks, t.id, 0); // Reset depth since we use margin-left on wrapper
html += `</div>`;
}
html += `</div>`;
});
return html;
}
async function loadTasks() {
try {
const res = await fetch('/api/tasks');
const tasks = await res.json();
const activeContainer = document.getElementById('tasks-active');
const doneContainer = document.getElementById('tasks-done');
activeContainer.innerHTML = '';
doneContainer.innerHTML = '';
const taskContainer = document.getElementById('task-tree-container');
if (!taskContainer) return;
// Find root tasks (no parent)
const rootHtml = buildTaskTreeHTML(tasks, null, 0);
if (!rootHtml) {
taskContainer.innerHTML = '<div style="color:var(--text-secondary); padding:20px; text-align:center;">No active tasks.</div>';
} else {
taskContainer.innerHTML = rootHtml;
}
tasks.forEach(t => {
const card = document.createElement('div');
const isCompleted = t.status === 'completed';
card.className = `task-card ${isCompleted ? 'completed' : ''}`;
let html = `<strong>${t.title}</strong>${t.description}`;
if (!isCompleted) {
html += `<button class="complete-btn" onclick="completeTask('${t.id}')" title="Mark Completed">✓</button>`;
}
card.innerHTML = html;
if (isCompleted) doneContainer.appendChild(card);
else activeContainer.appendChild(card);
});
} catch (err) {
console.error("Failed to load tasks", err);
}