refactor(mcp): strip sse fallback architecture in favor of pure websockets and fix nvim NDJSON bug
This commit is contained in:
1 parent
0e29b12ac8
commit
3716c3e698
33 files changed
+2082
-1756
No files matched your search
+107
-29
@@ -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);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user