refactor: consolidate nvim crates, extract server library, and update workspace dependencies
This commit is contained in:
1 parent
a083719cf1
commit
533adfd41b
53 files changed
+5967
-1230
No files matched your search
+226
-54
@@ -633,29 +633,61 @@
|
||||
|
||||
<script>
|
||||
// --- Tabs ---
|
||||
let currentTabId = 'graph-tab';
|
||||
|
||||
function switchTab(tabId, btn) {
|
||||
currentTabId = tabId;
|
||||
|
||||
document.querySelectorAll('.tab-content').forEach(el => el.classList.remove('active'));
|
||||
document.querySelectorAll('.tab-button').forEach(el => el.classList.remove('active'));
|
||||
|
||||
document.getElementById(tabId).classList.add('active');
|
||||
btn.classList.add('active');
|
||||
if (btn) btn.classList.add('active');
|
||||
|
||||
if (tabId === 'graph-tab' && network) {
|
||||
// Resize graph when becoming visible again
|
||||
network.redraw();
|
||||
network.fit();
|
||||
loadGraph();
|
||||
} else if (tabId === 'activity-tab') {
|
||||
const feed = document.getElementById('activity-feed');
|
||||
feed.scrollTop = 0;
|
||||
} else if (tabId === 'task-tab') {
|
||||
loadTasks();
|
||||
} else if (tabId === 'preferences-tab') {
|
||||
loadPreferences();
|
||||
} else if (tabId === 'sticky-tab') {
|
||||
loadStickyNotes();
|
||||
} else {
|
||||
loadAllExtras();
|
||||
switch (tabId) {
|
||||
case 'graph-tab':
|
||||
if (network) {
|
||||
network.redraw();
|
||||
} else {
|
||||
loadGraph();
|
||||
}
|
||||
break;
|
||||
case 'activity-tab':
|
||||
loadActivityHistory();
|
||||
break;
|
||||
case 'task-tab':
|
||||
loadTasks();
|
||||
break;
|
||||
case 'preferences-tab':
|
||||
loadPreferences();
|
||||
break;
|
||||
case 'sticky-tab':
|
||||
loadStickyNotes();
|
||||
break;
|
||||
case 'techdebt-tab':
|
||||
loadTechDebt();
|
||||
break;
|
||||
case 'adrs-tab':
|
||||
loadADRs();
|
||||
break;
|
||||
case 'workspaces-tab':
|
||||
loadWorkspaces();
|
||||
break;
|
||||
case 'pinned-tab':
|
||||
loadPinned();
|
||||
break;
|
||||
case 'memos-tab':
|
||||
loadMemos();
|
||||
break;
|
||||
case 'snippets-tab':
|
||||
loadSnippets();
|
||||
break;
|
||||
case 'pr-tab':
|
||||
loadPRs();
|
||||
break;
|
||||
case 'terminal-tab':
|
||||
loadTerminal();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1028,23 +1060,99 @@
|
||||
// --- WebSocket Activity Feed ---
|
||||
const MAX_ACTIVITY_HISTORY = 100;
|
||||
|
||||
function parseActivityPayload(item) {
|
||||
let category = 'activity';
|
||||
let message = '';
|
||||
let timestamp = Date.now();
|
||||
|
||||
if (typeof item === 'string') {
|
||||
try {
|
||||
const parsed = JSON.parse(item);
|
||||
return parseActivityPayload(parsed);
|
||||
} catch (e) {
|
||||
message = item;
|
||||
}
|
||||
} else if (typeof item === 'object' && item !== null) {
|
||||
if (item.method === 'notifications/activity' && item.params) {
|
||||
message = item.params.message || JSON.stringify(item.params);
|
||||
timestamp = item.params.time || item.params.timestamp || timestamp;
|
||||
category = item.params.category || item.params.type || 'tool';
|
||||
} else {
|
||||
category = item.category || item.type || category;
|
||||
message = item.summary || item.message || item.description || item.data || JSON.stringify(item);
|
||||
timestamp = item.timestamp || item.time || item.updated_at || timestamp;
|
||||
if (typeof timestamp === 'number' && timestamp < 10000000000) {
|
||||
timestamp = timestamp * 1000;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback inference from tool name if category is generic
|
||||
const lowerCategory = category.toLowerCase();
|
||||
if (lowerCategory === 'activity' || lowerCategory === 'mcp_tool' || lowerCategory === 'tool') {
|
||||
const match = message.match(/Agent executed tool:\s*([a-z0-9_]+)/i);
|
||||
if (match && match[1]) {
|
||||
const tool = match[1].toLowerCase();
|
||||
if (tool.includes('clipboard')) category = 'CLIPBOARD';
|
||||
else if (tool.includes('graph') || tool.includes('entities') || tool.includes('relations') || tool.includes('schema')) category = 'GRAPH';
|
||||
else if (tool.includes('decision')) category = 'DECISION';
|
||||
else if (tool.includes('code_change') || tool.includes('ast')) category = 'CODE';
|
||||
else if (tool.includes('task') || tool.includes('milestone')) category = 'TASK';
|
||||
else if (tool.includes('note')) category = 'STICKY_NOTE';
|
||||
else if (tool.includes('checkpoint') || tool.includes('snapshot')) category = 'CHECKPOINT';
|
||||
else if (tool.includes('subagent') || tool.includes('namespace')) category = 'SUBAGENT';
|
||||
else if (tool.includes('snippet')) category = 'SNIPPET';
|
||||
else if (tool.includes('error')) category = 'ERROR_FIX';
|
||||
else category = tool.toUpperCase();
|
||||
}
|
||||
}
|
||||
|
||||
const catUpper = category.toUpperCase();
|
||||
const colorMap = {
|
||||
'GRAPH': '#2ecc71',
|
||||
'DECISION': '#f39c12',
|
||||
'CODE': '#3498db',
|
||||
'TASK': '#1abc9c',
|
||||
'CLIPBOARD': '#9b59b6',
|
||||
'STICKY_NOTE': '#e67e22',
|
||||
'CHECKPOINT': '#e74c3c',
|
||||
'ERROR_FIX': '#e74c3c',
|
||||
'TECH_DEBT': '#d35400',
|
||||
'SUBAGENT': '#8e44ad',
|
||||
'SNIPPET': '#16a085',
|
||||
'TOOL': '#3498db',
|
||||
'SYSTEM': '#95a5a6'
|
||||
};
|
||||
const badgeColor = colorMap[catUpper] || '#3498db';
|
||||
|
||||
const dateObj = new Date(timestamp);
|
||||
const validDate = isNaN(dateObj.getTime()) ? new Date() : dateObj;
|
||||
const timeStr = validDate.toLocaleString([], {
|
||||
month: '2-digit', day: '2-digit', hour: '2-digit', minute:'2-digit', second:'2-digit'
|
||||
});
|
||||
|
||||
return `<span class="time">[${timeStr}]</span> <span style="background:var(--canvas-bg); color:${badgeColor}; padding:2px 6px; border-radius:3px; font-weight:bold; margin-right:6px; border:1px solid ${badgeColor}60; text-transform:uppercase; font-size:0.75em;">${catUpper}</span> ${message}`;
|
||||
}
|
||||
|
||||
async function loadActivityHistory() {
|
||||
try {
|
||||
const response = await fetch('/api/activity');
|
||||
const history = await response.json();
|
||||
const feed = document.getElementById('activity-feed');
|
||||
if (!feed) return;
|
||||
feed.innerHTML = '';
|
||||
[...history].reverse().forEach(item => {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'feed-entry';
|
||||
const timeStr = new Date(item.time).toLocaleString([], {month: '2-digit', day: '2-digit', hour: '2-digit', minute:'2-digit', second:'2-digit'});
|
||||
div.innerHTML = `<span class="time">[${timeStr}]</span> ${item.message || item.data}`;
|
||||
div.innerHTML = parseActivityPayload(item);
|
||||
feed.appendChild(div);
|
||||
});
|
||||
if (history.length > 0) {
|
||||
feed.scrollTop = 0;
|
||||
}
|
||||
} catch(e) {}
|
||||
} catch(e) {
|
||||
console.error("Failed to load activity history", e);
|
||||
}
|
||||
}
|
||||
|
||||
function setupWS() {
|
||||
@@ -1056,27 +1164,21 @@
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
if (data.type === 'activity') {
|
||||
// Check scroll state before appending
|
||||
const isScrolledToTop = feed.scrollTop <= 20;
|
||||
const isScrolledToTop = feed ? feed.scrollTop <= 20 : true;
|
||||
|
||||
const div = document.createElement('div');
|
||||
div.className = 'feed-entry';
|
||||
const timeStr = new Date(data.data.time).toLocaleString([], {month: '2-digit', day: '2-digit', hour: '2-digit', minute:'2-digit', second:'2-digit'});
|
||||
div.innerHTML = `<span class="time">[${timeStr}]</span> ${data.data.message || data.data.data || data.data}`;
|
||||
feed.prepend(div);
|
||||
|
||||
// Enforce max history
|
||||
while (feed.children.length > MAX_ACTIVITY_HISTORY) {
|
||||
feed.removeChild(feed.lastChild);
|
||||
}
|
||||
|
||||
// Auto-scroll logic
|
||||
if (isScrolledToTop) {
|
||||
feed.scrollTop = 0;
|
||||
div.innerHTML = parseActivityPayload(data.data);
|
||||
if (feed) {
|
||||
feed.prepend(div);
|
||||
while (feed.children.length > MAX_ACTIVITY_HISTORY) {
|
||||
feed.removeChild(feed.lastChild);
|
||||
}
|
||||
if (isScrolledToTop) feed.scrollTop = 0;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore non-JSON or other messages for now
|
||||
console.error("WebSocket message parse error", e);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1161,7 +1263,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
function loadAllExtras() {
|
||||
function loadTerminal() {
|
||||
loadGenericList('/api/terminal/history', 'terminal-container', item => `
|
||||
<span style="font-weight:bold; font-size:1.05em; color:var(--text-primary);">${item.command}</span>
|
||||
<span style="margin-left:15px; font-size:0.85em;"><span style="font-weight:bold; color:var(--text-secondary);">Exit Code:</span> <span style="color:${item.exit_code === 0 ? 'var(--success-color)' : 'var(--error-color)'}; font-weight:bold;">${item.exit_code}</span></span>
|
||||
@@ -1170,7 +1272,9 @@
|
||||
<span style="background:var(--canvas-bg); padding:2px 6px; border-radius:4px; font-family:monospace; border:1px solid var(--border-color);">CWD: ${item.cwd || 'Unknown'}</span>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
function loadTechDebt() {
|
||||
loadGenericList('/api/tech_debts', 'techdebt-container', item => `
|
||||
<strong>${item.id}</strong> ${item.is_resolved ? '<span style="color:var(--success-color); font-weight:bold; background: rgba(46, 204, 113, 0.1); padding: 2px 6px; border-radius: 4px; border: 1px solid var(--success-color); margin-left: 8px;">(Resolved)</span>' : '<span style="color:var(--open-color); font-weight:bold; background: rgba(230, 126, 34, 0.1); padding: 2px 6px; border-radius: 4px; border: 1px solid var(--open-color); margin-left: 8px;">(Open)</span>'}
|
||||
<div style="margin-top:5px;"><strong>Description:</strong> ${item.description}</div>
|
||||
@@ -1186,7 +1290,9 @@
|
||||
<div style="font-size:0.9em; margin-bottom:8px; margin-top:5px;"><em>Solution:</em> ${item.solution || ''}</div>
|
||||
<div style="font-family:monospace; font-size:0.85em; color:#e74c3c;">Commit: ${item.git_commit || 'None'}</div>
|
||||
`);
|
||||
|
||||
}
|
||||
|
||||
function loadADRs() {
|
||||
loadGenericList('/api/adrs', 'adrs-container', item => `
|
||||
<strong>${item.id} | ${item.title}</strong>
|
||||
<span style="float:right; background:var(--canvas-bg); padding:2px 6px; border-radius:4px; font-size:0.8em; border:1px solid var(--border-color); ${item.status === 'superseded' ? 'text-decoration: line-through; color:var(--error-color);' : 'color:var(--success-color);'}">${item.status}</span>
|
||||
@@ -1195,24 +1301,44 @@
|
||||
<div style="font-size:0.9em; margin-bottom:8px;"><em>Consequence:</em> ${item.consequence || ''}</div>
|
||||
${item.supersedes ? `<div style="font-size:0.9em; font-weight:bold; color:var(--warning-color);">Supersedes: ${item.supersedes}</div>` : ''}
|
||||
`);
|
||||
}
|
||||
|
||||
function loadWorkspaces() {
|
||||
loadGenericList('/api/context_workspaces', 'workspaces-container', item => `
|
||||
<strong>${item.name}</strong>
|
||||
<div style="margin-bottom:8px;">${item.description || ''}</div>
|
||||
<div style="font-family:monospace; font-size:0.85em; color:#3498db;">${(item.paths || []).join(', ')}</div>
|
||||
`);
|
||||
}
|
||||
|
||||
function loadPinned() {
|
||||
loadGenericList('/api/pinned_files', 'pinned-container', item => `
|
||||
<strong>${item.path || item.file_path || item.id}</strong>
|
||||
<div style="margin-top:5px; font-size:0.85em; color:var(--text-secondary);">${item.reason || item.description || 'Pinned'}</div>
|
||||
`);
|
||||
}
|
||||
|
||||
function loadMemos() {
|
||||
loadGenericList('/api/handoff_memos', 'memos-container', item => `
|
||||
<strong>Memo from ${item.author || 'System'}</strong>
|
||||
<div style="white-space:pre-wrap; font-size:0.9em; line-height:1.4;">${item.content || item.summary || ''}</div>
|
||||
`);
|
||||
|
||||
loadGenericList('/api/milestones', 'milestones-container', item => `
|
||||
<strong>${item.title || item.name}</strong>
|
||||
<div style="margin-top:5px; font-size:0.85em;">${item.description || ''}</div>
|
||||
`);
|
||||
}
|
||||
|
||||
function loadSnippets() {
|
||||
loadGenericList('/api/snippets', 'snippets-container', item => `
|
||||
<strong>${item.description || 'Snippet'}</strong>
|
||||
<div style="margin-bottom:8px; font-size:0.8em;">Language: ${item.language || 'txt'} | Tags: ${(item.tags || []).join(', ')}</div>
|
||||
<pre style="background:#111417; color:#d1d8e0; padding:10px; border-radius:4px; overflow-x:auto; font-size:0.85em;">${item.content || item.code || ''}</pre>
|
||||
`);
|
||||
}
|
||||
|
||||
function loadPRs() {
|
||||
loadGenericList('/api/pr_checklists', 'pr-container', item => `
|
||||
<strong>${item.name || 'Checklist'}</strong>
|
||||
<ul style="padding-left:20px; font-size:0.9em;">
|
||||
@@ -1225,6 +1351,17 @@
|
||||
`);
|
||||
}
|
||||
|
||||
function loadAllExtras() {
|
||||
loadTerminal();
|
||||
loadTechDebt();
|
||||
loadADRs();
|
||||
loadWorkspaces();
|
||||
loadPinned();
|
||||
loadMemos();
|
||||
loadSnippets();
|
||||
loadPRs();
|
||||
}
|
||||
|
||||
async function testClipboard() {
|
||||
const modal = document.getElementById('clipboard-modal');
|
||||
const resultDiv = document.getElementById('clipboard-result');
|
||||
@@ -1264,16 +1401,17 @@
|
||||
sse.onmessage = function(event) {
|
||||
if (event.data) {
|
||||
try {
|
||||
const rec = JSON.parse(event.data);
|
||||
const feed = document.getElementById('activity-feed');
|
||||
if (feed) {
|
||||
const item = document.createElement('div');
|
||||
item.style.padding = '8px 12px';
|
||||
item.style.borderBottom = '1px solid var(--border-color)';
|
||||
item.style.fontSize = '0.85em';
|
||||
item.style.background = 'rgba(39, 174, 96, 0.08)';
|
||||
item.innerHTML = `<strong>${rec.category || 'activity'}</strong>: ${rec.summary || event.data}`;
|
||||
feed.insertBefore(item, feed.firstChild);
|
||||
const isScrolledToTop = feed.scrollTop <= 20;
|
||||
const div = document.createElement('div');
|
||||
div.className = 'feed-entry';
|
||||
div.innerHTML = parseActivityPayload(event.data);
|
||||
feed.prepend(div);
|
||||
while (feed.children.length > MAX_ACTIVITY_HISTORY) {
|
||||
feed.removeChild(feed.lastChild);
|
||||
}
|
||||
if (isScrolledToTop) feed.scrollTop = 0;
|
||||
}
|
||||
} catch(e) {}
|
||||
}
|
||||
@@ -1300,10 +1438,6 @@
|
||||
// --- Start ---
|
||||
loadVersion();
|
||||
loadGraph();
|
||||
loadTasks();
|
||||
loadStickyNotes();
|
||||
loadActivityHistory();
|
||||
loadAllExtras();
|
||||
setupWS();
|
||||
setupSSE();
|
||||
|
||||
@@ -1311,12 +1445,50 @@
|
||||
const observer = new MutationObserver(() => updateGraphData());
|
||||
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });
|
||||
|
||||
// Smart background polling: refresh only the currently active tab
|
||||
setInterval(() => {
|
||||
loadGraph();
|
||||
loadTasks();
|
||||
loadStickyNotes();
|
||||
loadAllExtras();
|
||||
}, 5000);
|
||||
switch (currentTabId) {
|
||||
case 'graph-tab':
|
||||
loadGraph();
|
||||
break;
|
||||
case 'task-tab':
|
||||
loadTasks();
|
||||
break;
|
||||
case 'sticky-tab':
|
||||
loadStickyNotes();
|
||||
break;
|
||||
case 'techdebt-tab':
|
||||
loadTechDebt();
|
||||
break;
|
||||
case 'adrs-tab':
|
||||
loadADRs();
|
||||
break;
|
||||
case 'workspaces-tab':
|
||||
loadWorkspaces();
|
||||
break;
|
||||
case 'pinned-tab':
|
||||
loadPinned();
|
||||
break;
|
||||
case 'memos-tab':
|
||||
loadMemos();
|
||||
break;
|
||||
case 'snippets-tab':
|
||||
loadSnippets();
|
||||
break;
|
||||
case 'pr-tab':
|
||||
loadPRs();
|
||||
break;
|
||||
case 'terminal-tab':
|
||||
loadTerminal();
|
||||
break;
|
||||
case 'preferences-tab':
|
||||
loadPreferences();
|
||||
break;
|
||||
case 'activity-tab':
|
||||
loadActivityHistory();
|
||||
break;
|
||||
}
|
||||
}, 10000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in new issue
Block a user