UNPKG

@notbnull/mcp-cursor-taskmaster

Version:

A project-scoped MCP server requiring explicit --project-dir configuration for task management, memory retrieval, and agentic workflows

521 lines (456 loc) 14.5 kB
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Cursor Taskmaster - Task Approval</title> <script src="/socket.io/socket.io.js"></script> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; background-color: #0a0a0a; color: #e0e0e0; line-height: 1.6; padding: 20px; } .container { max-width: 1200px; margin: 0 auto; } h1 { color: #fff; margin-bottom: 30px; font-size: 2rem; font-weight: 600; } .no-tasks { text-align: center; padding: 60px 20px; color: #666; font-size: 1.1rem; } .task-card { background-color: #1a1a1a; border: 1px solid #333; border-radius: 8px; padding: 24px; margin-bottom: 20px; transition: all 0.2s ease; } .task-card:hover { border-color: #555; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); } .task-header { display: flex; justify-content: space-between; align-items: start; margin-bottom: 16px; } .task-title { font-size: 1.3rem; font-weight: 600; color: #fff; margin-bottom: 8px; } .task-meta { display: flex; gap: 16px; font-size: 0.9rem; color: #999; } .priority { padding: 2px 8px; border-radius: 4px; font-size: 0.85rem; font-weight: 500; } .priority-low { background-color: #1a3a1a; color: #4ade80; } .priority-medium { background-color: #3a3a1a; color: #facc15; } .priority-high { background-color: #3a1a1a; color: #fb923c; } .priority-critical { background-color: #3a1a1a; color: #ef4444; } .task-description { color: #ccc; margin-bottom: 20px; white-space: pre-wrap; } .editable-section { background-color: #0a0a0a; border: 1px solid #333; border-radius: 6px; padding: 16px; margin-bottom: 20px; } .editable-section h3 { color: #fff; font-size: 1rem; margin-bottom: 12px; font-weight: 500; } .form-group { margin-bottom: 16px; } .form-group label { display: block; color: #999; font-size: 0.9rem; margin-bottom: 6px; } .form-group input, .form-group textarea, .form-group select { width: 100%; background-color: #1a1a1a; border: 1px solid #333; color: #e0e0e0; padding: 8px 12px; border-radius: 4px; font-size: 0.95rem; font-family: inherit; } .form-group input:focus, .form-group textarea:focus, .form-group select:focus { outline: none; border-color: #4a9eff; } .form-group textarea { min-height: 100px; resize: vertical; } .feedback-input { width: 100%; background-color: #1a1a1a; border: 1px solid #333; color: #e0e0e0; padding: 12px; border-radius: 6px; font-size: 0.95rem; margin-bottom: 20px; min-height: 80px; resize: vertical; } .action-buttons { display: flex; gap: 12px; justify-content: flex-end; } .btn { padding: 10px 20px; border: none; border-radius: 6px; font-size: 0.95rem; font-weight: 500; cursor: pointer; transition: all 0.2s ease; display: inline-flex; align-items: center; gap: 6px; } .btn-approve { background-color: #10b981; color: white; } .btn-approve:hover { background-color: #059669; } .btn-reject { background-color: #ef4444; color: white; } .btn-reject:hover { background-color: #dc2626; } .btn-edit { background-color: #3b82f6; color: white; } .btn-edit:hover { background-color: #2563eb; } .status-badge { position: fixed; top: 20px; right: 20px; padding: 8px 16px; border-radius: 20px; font-size: 0.9rem; font-weight: 500; display: flex; align-items: center; gap: 8px; } .status-connected { background-color: #10b981; color: white; } .status-disconnected { background-color: #ef4444; color: white; } .spinner { width: 12px; height: 12px; border: 2px solid #ffffff30; border-top-color: #fff; border-radius: 50%; animation: spin 1s linear infinite; } @keyframes spin { to { transform: rotate(360deg); } } .prompt-info { background-color: #1e293b; border: 1px solid #334155; border-radius: 6px; padding: 16px; margin-bottom: 20px; } .prompt-info h4 { color: #94a3b8; font-size: 0.9rem; margin-bottom: 8px; font-weight: 500; } .prompt-info p { color: #cbd5e1; } </style> </head> <body> <div class="container"> <h1>Cursor Taskmaster - Task Approval</h1> <div id="status" class="status-badge status-disconnected"> <span class="spinner"></span> Connecting... </div> <div id="tasks-container"> <div class="no-tasks">No tasks pending approval</div> </div> </div> <script> const socket = io(); const tasksContainer = document.getElementById("tasks-container"); const statusBadge = document.getElementById("status"); const pendingTasks = new Map(); socket.on("connect", () => { statusBadge.className = "status-badge status-connected"; statusBadge.innerHTML = "● Connected"; }); socket.on("disconnect", () => { statusBadge.className = "status-badge status-disconnected"; statusBadge.innerHTML = '<span class="spinner"></span> Disconnected'; }); socket.on("pending-approvals", (approvals) => { approvals.forEach(({ id, request }) => { if (!pendingTasks.has(id)) { addTaskCard(id, request); } }); updateEmptyState(); }); socket.on("new-approval", ({ id, request }) => { if (!pendingTasks.has(id)) { addTaskCard(id, request); updateEmptyState(); } }); socket.on("approval-resolved", (taskId) => { const card = pendingTasks.get(taskId); if (card) { card.remove(); pendingTasks.delete(taskId); updateEmptyState(); } }); function updateEmptyState() { if (pendingTasks.size === 0) { tasksContainer.innerHTML = '<div class="no-tasks">No tasks pending approval</div>'; } } function addTaskCard(taskId, request) { const { task, promptTitle, promptDescription } = request; const card = document.createElement("div"); card.className = "task-card"; card.innerHTML = ` ${ promptTitle || promptDescription ? ` <div class="prompt-info"> ${ promptTitle ? `<h4>Prompt: ${escapeHtml(promptTitle)}</h4>` : "" } ${ promptDescription ? `<p>${escapeHtml(promptDescription)}</p>` : "" } </div> ` : "" } <div class="task-header"> <div> <div class="task-title">${escapeHtml(task.title)}</div> <div class="task-meta"> <span class="priority priority-${ task.priority }">${task.priority.toUpperCase()}</span> ${ task.estimatedTime ? `<span>⏱ ${task.estimatedTime} min</span>` : "" } ${ task.dueDate ? `<span>📅 ${new Date( task.dueDate ).toLocaleDateString()}</span>` : "" } </div> </div> </div> <div class="task-description">${escapeHtml( task.description )}</div> <div class="editable-section" style="display: none;"> <h3>Edit Task</h3> <div class="form-group"> <label>Title</label> <input type="text" id="title-${taskId}" value="${escapeHtml( task.title )}"> </div> <div class="form-group"> <label>Description</label> <textarea id="desc-${taskId}">${escapeHtml( task.description )}</textarea> </div> <div class="form-group"> <label>Priority</label> <select id="priority-${taskId}"> <option value="low" ${ task.priority === "low" ? "selected" : "" }>Low</option> <option value="medium" ${ task.priority === "medium" ? "selected" : "" }>Medium</option> <option value="high" ${ task.priority === "high" ? "selected" : "" }>High</option> <option value="critical" ${ task.priority === "critical" ? "selected" : "" }>Critical</option> </select> </div> <div class="form-group"> <label>Estimated Time (minutes)</label> <input type="number" id="time-${taskId}" value="${ task.estimatedTime || "" }"> </div> </div> <textarea class="feedback-input" id="feedback-${taskId}" placeholder="Add feedback or comments (optional)"></textarea> <div class="action-buttons"> <button class="btn btn-edit" onclick="toggleEdit('${taskId}')"> ✏️ Edit </button> <button class="btn btn-reject" onclick="rejectTask('${taskId}')"> ❌ Reject </button> <button class="btn btn-approve" onclick="approveTask('${taskId}')"> ✅ Approve </button> </div> `; // Remove empty state if exists const emptyState = tasksContainer.querySelector(".no-tasks"); if (emptyState) { emptyState.remove(); } tasksContainer.appendChild(card); pendingTasks.set(taskId, card); } function escapeHtml(text) { const div = document.createElement("div"); div.textContent = text; return div.innerHTML; } function toggleEdit(taskId) { const card = pendingTasks.get(taskId); const editSection = card.querySelector(".editable-section"); const editBtn = card.querySelector(".btn-edit"); if (editSection.style.display === "none") { editSection.style.display = "block"; editBtn.textContent = "💾 Save Changes"; } else { editSection.style.display = "none"; editBtn.textContent = "✏️ Edit"; } } function getModifications(taskId) { const card = pendingTasks.get(taskId); const editSection = card.querySelector(".editable-section"); if (editSection.style.display === "none") { return null; } const modifications = {}; const title = document.getElementById(`title-${taskId}`).value; const description = document.getElementById(`desc-${taskId}`).value; const priority = document.getElementById(`priority-${taskId}`).value; const estimatedTime = document.getElementById(`time-${taskId}`).value; if (title) modifications.title = title; if (description) modifications.description = description; if (priority) modifications.priority = priority; if (estimatedTime) modifications.estimatedTime = parseInt(estimatedTime); return Object.keys(modifications).length > 0 ? modifications : null; } function approveTask(taskId) { const feedback = document.getElementById(`feedback-${taskId}`).value; const modifications = getModifications(taskId); socket.emit("approval-response", { taskId, approved: true, modifications, userFeedback: feedback || undefined, }); } function rejectTask(taskId) { const feedback = document.getElementById(`feedback-${taskId}`).value; const modifications = getModifications(taskId); socket.emit("approval-response", { taskId, approved: false, modifications, userFeedback: feedback || undefined, }); } </script> </body> </html>