Files
mcp-memory/server/src/dashboard.js
T

2376 lines
105 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// server/src/dashboard.ts
var currentTabId = "activity-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"));
const targetTab = document.getElementById(tabId);
if (targetTab)
targetTab.classList.add("active");
const targetBtn = btn || document.querySelector(`.tab-button[onclick*="'${tabId}'"]`);
if (targetBtn)
targetBtn.classList.add("active");
switch (tabId) {
case "graph-tab":
if (network) {
network.redraw();
} else {
loadGraph();
}
break;
case "activity-tab":
loadActivityHistory();
break;
case "task-tab":
loadTasks();
break;
case "techdebt-tab":
loadTechDebt();
break;
case "adrs-tab":
loadADRs();
break;
case "memos-tab":
loadMemos();
break;
case "snippets-tab":
loadSnippets();
break;
case "terminal-tab":
loadTerminal();
break;
case "ledger-tab":
loadLedger();
break;
case "env-tab":
loadEnvironment();
break;
}
}
function toggleTheme() {
const currentTheme = document.documentElement.getAttribute("data-theme");
const newTheme = currentTheme === "dark" ? "light" : "dark";
document.documentElement.setAttribute("data-theme", newTheme);
localStorage.setItem("theme", newTheme);
}
if (localStorage.getItem("theme") === "dark" || !localStorage.getItem("theme") && window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) {
document.documentElement.setAttribute("data-theme", "dark");
}
var network = null;
var nodesData = new vis.DataSet;
var edgesData = new vis.DataSet;
var rawEntities = {};
var rawRelations = [];
var activeFilters = new Set;
var allTypes = new Set;
var typeColors = {
Concept: { bg: "#9b59b6", border: "#8e44ad" },
Technology: { bg: "#3498db", border: "#2980b9" },
Infrastructure: { bg: "#e67e22", border: "#d35400" },
Database_Table: { bg: "#f1c40f", border: "#f39c12" },
Service: { bg: "#1abc9c", border: "#f39c12" },
Tool: { bg: "#e74c3c", border: "#c0392b" },
Integration: { bg: "#2ecc71", border: "#27ae60" },
System: { bg: "#34495e", border: "#2c3e50" },
Rule: { bg: "#fd79a8", border: "#e84393" }
};
function getColorForType(type) {
if (typeColors[type])
return typeColors[type];
let hash = 0;
for (let i = 0;i < type.length; i++)
hash = type.charCodeAt(i) + ((hash << 5) - hash);
const hue = Math.abs(hash) % 360;
return {
background: `hsl(${hue}, 70%, 60%)`,
border: `hsl(${hue}, 70%, 40%)`
};
}
function zoomGraph(step) {
if (!network)
return;
const currentScale = network.getScale();
network.moveTo({ scale: currentScale * (1 + step) });
}
function resetGraph() {
if (!network)
return;
network.fit({
animation: { duration: 500, easingFunction: "easeInOutQuad" }
});
}
function closeInspector() {
const inspector = document.getElementById("inspector-panel");
if (inspector)
inspector.classList.remove("open");
if (network)
network.unselectAll();
}
function showInspector(nodeId) {
const entity = rawEntities[nodeId];
if (!entity)
return;
const titleEl = document.getElementById("inspector-title");
if (titleEl)
titleEl.innerText = entity.name;
const metaEl = document.querySelector(".inspector-meta");
if (metaEl) {
const createdStr = entity.created_at ? new Date(entity.created_at * 1000).toISOString().replace("T", " ").substring(0, 19) : "";
const updatedStr = entity.updated_at ? new Date(entity.updated_at * 1000).toISOString().replace("T", " ").substring(0, 19) : "";
metaEl.innerHTML = `
<div><strong>Type:</strong> <span id="inspector-type">${escapeHtml(entity.entity_type)}</span></div>
<div><strong>Namespace:</strong> <span id="inspector-namespace">${escapeHtml(entity.namespace || "global")}</span></div>
${entity.repo_name ? `<div><strong>Repository:</strong> <span style="font-family:monospace;">${escapeHtml(entity.repo_name)}</span></div>` : ""}
${entity.file_path ? `<div><strong>File:</strong> <span style="font-family:monospace; word-break:break-all;">${escapeHtml(entity.file_path)}</span></div>` : ""}
${entity.git_branch ? `<div><strong>Branch:</strong> <span style="font-family:monospace;">${escapeHtml(entity.git_branch)}</span></div>` : ""}
${createdStr ? `<div><strong>Created:</strong> <span>${escapeHtml(createdStr)}</span></div>` : ""}
${updatedStr ? `<div><strong>Updated:</strong> <span>${escapeHtml(updatedStr)}</span></div>` : ""}
`;
} else {
const typeEl = document.getElementById("inspector-type");
if (typeEl)
typeEl.innerText = entity.entity_type;
const nsEl = document.getElementById("inspector-namespace");
if (nsEl)
nsEl.innerText = entity.namespace || "global";
}
const obsHtml = (entity.observations || []).map((o) => `<li>${escapeHtml(o)}</li>`).join("");
const obsEl = document.getElementById("inspector-observations");
if (obsEl)
obsEl.innerHTML = obsHtml || "<li><em>No observations recorded.</em></li>";
const rels = rawRelations.filter((r) => r.from === nodeId || r.to === nodeId);
const relHtml = rels.map((r) => {
if (r.from === nodeId)
return `<li>→ <em>${escapeHtml(r.relation_type)}</em> → <strong>${escapeHtml(r.to)}</strong></li>`;
return `<li>← <em>${escapeHtml(r.relation_type)}</em> ← <strong>${escapeHtml(r.from)}</strong></li>`;
}).join("");
const relEl = document.getElementById("inspector-relations");
if (relEl)
relEl.innerHTML = relHtml || "<li><em>No direct relations.</em></li>";
const inspector = document.getElementById("inspector-panel");
if (inspector)
inspector.classList.add("open");
}
function toggleFilter(type) {
if (activeFilters.has(type))
activeFilters.delete(type);
else
activeFilters.add(type);
renderFilters();
updateGraphData();
}
function renderFilters() {
const container = document.getElementById("graph-filters");
if (!container)
return;
container.innerHTML = Array.from(allTypes).sort().map((type) => {
const isActive = activeFilters.has(type);
const color = getColorForType(type).bg || getColorForType(type).background;
return `<button class="filter-chip ${isActive ? "active" : ""}" onclick="toggleFilter('${type}')">
<span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:${color};margin-right:4px;"></span>
${type}
</button>`;
}).join("");
}
var lastGraphJson = "";
function updateGraphData() {
const newNodes = [];
const newEdges = [];
const nodeIds = new Set;
for (const [name, entity] of Object.entries(rawEntities)) {
allTypes.add(entity.entity_type);
if (activeFilters.size > 0 && !activeFilters.has(entity.entity_type))
continue;
const color = getColorForType(entity.entity_type);
newNodes.push({
id: name,
label: name,
title: `${name}
Type: ${entity.entity_type}`,
color: { background: color.bg || color.background, border: color.border },
font: {
color: document.documentElement.getAttribute("data-theme") === "dark" ? "#eee" : "#333"
}
});
nodeIds.add(name);
}
rawRelations.forEach((r, idx) => {
if (nodeIds.has(r.from) && nodeIds.has(r.to)) {
newEdges.push({
id: `${r.from}-${r.to}-${r.relation_type}-${idx}`,
from: r.from,
to: r.to,
label: r.relation_type,
arrows: "to",
font: {
color: document.documentElement.getAttribute("data-theme") === "dark" ? "#aaa" : "#666",
strokeWidth: 0
}
});
}
});
nodesData.clear();
if (newNodes.length > 0)
nodesData.add(newNodes);
edgesData.clear();
if (newEdges.length > 0)
edgesData.add(newEdges);
renderFilters();
}
async function loadGraph() {
try {
const res = await fetch("/api/graph", { cache: "no-store" });
const dataText = await res.text();
if (dataText === lastGraphJson && network) {
return;
}
lastGraphJson = dataText;
const data = JSON.parse(dataText);
rawEntities = data.entities || {};
rawRelations = data.relations || [];
updateGraphData();
if (!network) {
const container = document.getElementById("network-container");
if (!container)
return;
const options = {
nodes: { shape: "dot", size: 16, font: { size: 12 } },
edges: {
color: { inherit: "from", opacity: 0.6 },
font: { size: 10, align: "middle" },
smooth: { type: "continuous" }
},
physics: {
enabled: true,
solver: "barnesHut",
barnesHut: {
gravitationalConstant: -3000,
centralGravity: 0.3,
springLength: 95,
springConstant: 0.04,
damping: 0.09,
avoidOverlap: 0.1
},
stabilization: {
enabled: true,
iterations: 150,
updateInterval: 25,
fit: true
}
},
interaction: { hover: true, tooltipDelay: 100, zoomView: false }
};
network = new vis.Network(container, { nodes: nodesData, edges: edgesData }, options);
network.once("stabilizationIterationsDone", function() {
network.setOptions({ physics: { enabled: false } });
});
network.on("dragStart", function() {
network.setOptions({ physics: { enabled: true } });
});
network.on("dragEnd", function() {
network.setOptions({ physics: { enabled: false } });
});
container.addEventListener("wheel", function(event) {
event.preventDefault();
const direction = event.deltaY > 0 ? -0.15 : 0.15;
zoomGraph(direction);
}, { passive: false });
network.on("selectNode", function(params) {
if (params.nodes.length > 0)
showInspector(params.nodes[0]);
});
network.on("deselectNode", function(params) {
if (params.nodes.length === 0)
closeInspector();
});
}
} catch (err) {
console.error("Failed to load graph", err);
const container = document.getElementById("network-container");
if (container) {
container.innerHTML = '<div style="color:#e74c3c;padding:20px;font-weight:bold;">Error loading graph: ' + err.message + "</div>";
}
}
}
async function completeTask(id) {
try {
if (activeWS && activeWS.readyState === WebSocket.OPEN) {
await sendWsRpc("tasks/complete", { id });
} else {
await fetch(`/api/tasks/${encodeURIComponent(id)}/complete`, { method: "POST" });
}
loadTasks();
} catch (e) {
console.error("Failed to complete task via WS, falling back to HTTP", e);
try {
await fetch(`/api/tasks/${encodeURIComponent(id)}/complete`, { method: "POST" });
loadTasks();
} catch (err) {
console.error("Failed to complete task via HTTP fallback", err);
}
}
}
function buildTaskTreeHTML(tasks, parentId, depth = 0) {
let html = "";
const children = tasks.filter((t) => {
const pid = t.parent_id || t.parentId;
if (!parentId)
return !pid;
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";
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);
}
});
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;
}
}
const prio = (t.priority || "").toLowerCase();
let prioBadge = "";
if (prio === "critical" || prio === "urgent") {
prioBadge = `<span style="background:rgba(231,76,60,0.18); color:#e74c3c; border:1px solid rgba(231,76,60,0.5); font-size:0.7em; font-weight:bold; padding:2px 6px; border-radius:4px; text-transform:uppercase;">CRITICAL</span>`;
} else if (prio === "high") {
prioBadge = `<span style="background:rgba(230,126,34,0.18); color:#e67e22; border:1px solid rgba(230,126,34,0.5); font-size:0.7em; font-weight:bold; padding:2px 6px; border-radius:4px; text-transform:uppercase;">HIGH</span>`;
} else if (prio === "medium" || prio === "med") {
prioBadge = `<span style="background:rgba(52,152,219,0.15); color:#3498db; border:1px solid rgba(52,152,219,0.4); font-size:0.7em; font-weight:bold; padding:2px 6px; border-radius:4px; text-transform:uppercase;">MEDIUM</span>`;
} else if (prio === "low") {
prioBadge = `<span style="background:var(--canvas-bg); color:var(--text-secondary); border:1px solid var(--border-color); font-size:0.7em; padding:2px 6px; border-radius:4px; text-transform:uppercase;">LOW</span>`;
}
html += `<div class="${cardClass}" style="margin-left: ${depth * 20}px; margin-bottom: 10px;">`;
html += `<div style="display:flex; justify-content:space-between; align-items:flex-start; gap:8px; margin-bottom:4px;">`;
html += `<div style="display:flex; align-items:center; gap:6px; flex-wrap:wrap;">`;
if (prioBadge)
html += prioBadge;
if (t.assigned_agent) {
html += `<span style="background:var(--canvas-bg); color:var(--text-secondary); border:1px solid var(--border-color); font-size:0.7em; padding:2px 6px; border-radius:4px;">\uD83E\uDD16 ${escapeHtml(t.assigned_agent)}</span>`;
}
if (isBlocked && !isCompleted && !isCancelled) {
html += `<span style="background:#e74c3c; color:white; font-size:0.7em; padding:2px 6px; border-radius:3px; font-weight:bold;">BLOCKED</span>`;
}
if (isCancelled) {
html += `<span style="background:#95a5a6; color:white; font-size:0.7em; padding:2px 6px; border-radius:3px; font-weight:bold;">CANCELLED</span>`;
}
html += `</div>`;
if (!isCompleted && !isCancelled && !isBlocked) {
html += `<button class="complete-btn" style="position:static;" onclick="completeTask('${escapeHtml(t.id)}')" title="Mark Completed">✓</button>`;
}
html += `</div>`;
if (isBlocked && blockers.length > 0 && !isCompleted && !isCancelled) {
html += `<div style="font-size:0.8em; color:#e74c3c; margin-bottom:6px;">Waiting on: ${escapeHtml(blockers.join(", "))}</div>`;
}
html += `<strong>${escapeHtml(t.title)}</strong>`;
if (t.description) {
html += `<div style="margin-top:5px; font-size:0.9em; color:var(--text-secondary); line-height:1.4;">${escapeHtml(t.description)}</div>`;
}
if (t.verification_command) {
html += `<div style="margin-top:6px; font-size:0.8em; font-family:monospace; background:var(--canvas-bg); padding:3px 8px; border-radius:4px; border:1px solid var(--border-color); color:var(--accent-color);">\uD83E\uDDEA Test: ${escapeHtml(t.verification_command)}</div>`;
}
const criteria = 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.is_met;
if (!isMet)
unmetCriteria = true;
const check = isMet ? "☑" : "☐";
const strike = isMet ? "text-decoration: line-through;" : "";
html += `<li style="${strike}">${check} ${escapeHtml(c.description)}${c.verification_command ? ` <code style="font-size:0.8em; background:var(--canvas-bg); padding:1px 4px; border-radius:3px;">(${escapeHtml(c.verification_command)})</code>` : ""}</li>`;
});
html += `</ul>`;
if (unmetCriteria && !isCompleted && !isCancelled)
isBlocked = true;
}
if (t.git_branch || t.repo_name) {
html += `<div style="margin-top:8px; font-size:0.75em; display:flex; gap:6px; flex-wrap:wrap; color:var(--text-secondary);">`;
if (t.repo_name)
html += `<span style="background:var(--canvas-bg); padding:1px 6px; border-radius:3px; border:1px solid var(--border-color);">\uD83D\uDCE6 ${escapeHtml(t.repo_name)}</span>`;
if (t.git_branch)
html += `<span style="background:var(--canvas-bg); padding:1px 6px; border-radius:3px; border:1px solid var(--border-color);">\uD83C\uDF3F ${escapeHtml(t.git_branch)}</span>`;
html += `</div>`;
}
html += progressHtml;
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);
html += `</div>`;
}
html += `</div>`;
});
return html;
}
var cachedTasks = [];
var taskFilterQuery = "";
var taskStatusFilter = "";
function handleTaskFilter(val) {
taskFilterQuery = val.toLowerCase().trim();
applyTaskFilters();
}
function handleTaskStatusFilter(val) {
taskStatusFilter = val.toLowerCase().trim();
applyTaskFilters();
}
function applyTaskFilters() {
const taskContainer = document.getElementById("task-tree-container");
if (!taskContainer)
return;
let filtered = cachedTasks;
if (taskFilterQuery) {
filtered = filtered.filter((t) => t.title && t.title.toLowerCase().includes(taskFilterQuery) || t.description && t.description.toLowerCase().includes(taskFilterQuery) || t.id && t.id.toLowerCase().includes(taskFilterQuery) || t.assigned_agent && t.assigned_agent.toLowerCase().includes(taskFilterQuery));
}
if (taskStatusFilter) {
if (taskStatusFilter === "blocked") {
filtered = filtered.filter((t) => {
const deps = t.dependencies || [];
return deps.some((dId) => {
const dt = cachedTasks.find((x) => x.id === dId);
return dt && dt.status !== "completed" && dt.status !== "done";
});
});
} else {
filtered = filtered.filter((t) => (t.status || "pending").toLowerCase() === taskStatusFilter);
}
}
const rootHtml = buildTaskTreeHTML(filtered, null, 0);
if (!rootHtml) {
if (cachedTasks.length === 0) {
taskContainer.innerHTML = '<div class="enterprise-empty-state">No active tasks.</div>';
} else {
taskContainer.innerHTML = '<div class="enterprise-empty-state">No tasks matching current filter.</div>';
}
} else {
taskContainer.innerHTML = rootHtml;
}
}
async function loadTasks() {
try {
const res = await fetch("/api/tasks", { cache: "no-store" });
cachedTasks = await res.json();
applyTaskFilters();
} catch (err) {
console.error("Failed to load tasks", err);
}
}
function highlightText(text, query) {
if (!query)
return text;
const regex = new RegExp(`(${query})`, "gi");
return text.replace(regex, '<span class="search-highlight">$1</span>');
}
var searchDebounceTimer = null;
var activeSearchAbortController = null;
async function handleSearch(e) {
const input = e.target;
const q = input.value.trim();
if (searchDebounceTimer)
clearTimeout(searchDebounceTimer);
if (!q) {
if (activeSearchAbortController)
activeSearchAbortController.abort();
const resultsEl = document.getElementById("search-results");
if (resultsEl)
resultsEl.innerHTML = "";
return;
}
const delay = e.key === "Enter" ? 0 : 250;
searchDebounceTimer = setTimeout(async () => {
if (activeSearchAbortController)
activeSearchAbortController.abort();
activeSearchAbortController = new AbortController;
try {
const res = await fetch(`/api/search?q=${encodeURIComponent(q)}`, {
signal: activeSearchAbortController.signal
});
const data = await res.json();
const container = document.getElementById("search-results");
if (!container)
return;
if (!data.results || data.results.length === 0) {
container.innerHTML = '<div style="padding:15px;color:var(--text-secondary);">No results found.</div>';
return;
}
container.innerHTML = data.results.map((r) => `
<div class="search-result-item" onclick="switchTab('graph-tab'); if(rawEntities['${r.id}']){ showInspector('${r.id}'); if(network){ network.selectNodes(['${r.id}']); network.focus('${r.id}', {scale:1.2, animation:true}); } }" style="cursor:pointer;">
<div style="display:flex; justify-content:space-between; align-items:flex-start;">
<div>
<strong>${highlightText(r.title, q)}</strong>
<span style="font-size: 0.7em; color: var(--text-secondary); background: var(--canvas-bg); padding: 2px 6px; border-radius: 4px; margin-left: 8px; text-transform: uppercase; border: 1px solid var(--border-color);">${r.type_name}</span>
</div>
<div style="font-size:0.75em; color:var(--text-secondary);">${r.score.toFixed(2)}</div>
</div>
<div style="margin-top:6px; font-size:0.9em; line-height:1.4;">${highlightText(r.content.substring(0, 150), q)}${r.content.length > 150 ? "..." : ""}</div>
<div style="font-size:0.75em; color:var(--text-secondary); margin-top:8px; font-family:monospace; background: var(--canvas-bg); padding: 2px 4px; border-radius: 3px; display: inline-block;">ID: ${r.id}</div>
</div>
`).join("");
} catch (err) {
if (err.name !== "AbortError") {
console.error("Search error:", err);
}
}
}, delay);
}
var MAX_ACTIVITY_HISTORY = 100;
function escapeHtml(str) {
if (str === null || str === undefined)
return "";
return String(str).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
}
function parseActivityPayload(item) {
let category = "TOOL";
let summary = "";
let details = "";
let timestamp = Date.now();
if (typeof item === "string") {
try {
const parsed = JSON.parse(item);
return parseActivityPayload(parsed);
} catch (e) {
summary = item;
}
} else if (typeof item === "object" && item !== null) {
if (item.method === "notifications/activity" && item.params) {
return parseActivityPayload(item.params);
}
category = (item.category || item.type || "TOOL").toUpperCase();
let rawSum = item.summary || item.message || item.description || item.data;
if (rawSum !== undefined && rawSum !== null) {
if (typeof rawSum === "object") {
summary = JSON.stringify(rawSum);
} else {
summary = String(rawSum);
}
}
if (!summary) {
if (item.title)
summary = String(item.title);
else if (item.name)
summary = String(item.name);
else if (item.action)
summary = String(item.action);
else if (item.content)
summary = String(item.content);
else {
const {
timestamp: _t,
time: _tm,
updated_at: _u,
category: _c,
type: _tp,
method: _m,
params: _p,
jsonrpc: _j,
...rest
} = item;
if (Object.keys(rest).length > 0) {
summary = JSON.stringify(rest);
} else {
summary = JSON.stringify(item);
}
}
}
let rawDet = item.details;
if (rawDet !== undefined && rawDet !== null) {
if (typeof rawDet === "object") {
details = JSON.stringify(rawDet);
} else {
details = String(rawDet);
}
}
timestamp = item.timestamp || item.time || item.updated_at || timestamp;
if (typeof timestamp === "number" && timestamp < 10000000000) {
timestamp = timestamp * 1000;
}
}
const catUpper = category.toUpperCase();
const colorMap = {
GRAPH: "#2ecc71",
DECISION: "#f39c12",
CODE: "#3498db",
CODE_CHANGE: "#3498db",
TASK: "#1abc9c",
TASK_CREATE: "#1abc9c",
TASK_UPDATE: "#1abc9c",
CLIPBOARD: "#9b59b6",
HANDOFF_MEMO: "#e67e22",
CHECKPOINT: "#e74c3c",
ERROR_FIX: "#e74c3c",
TECH_DEBT: "#d35400",
SUBAGENT: "#8e44ad",
AGENT_SIGNAL: "#8e44ad",
SNIPPET: "#16a085",
TOOL: "#3498db",
EDITOR: "#9b59b6",
TERMINAL: "#27ae60",
SNAPSHOT: "#f1c40f",
NAMESPACE: "#e67e22",
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"
});
let html = `<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;">${escapeHtml(catUpper)}</span> ${escapeHtml(summary)}`;
if (details) {
html += `<div style="font-size:0.85em; color:var(--text-secondary); margin-left:20px; margin-top:2px;">${escapeHtml(details)}</div>`;
}
return html;
}
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 = "";
if (!history || history.length === 0) {
feed.innerHTML = '<div style="padding:40px 20px; color:#a4b0be; text-align:center; font-family:sans-serif;">No recent activity recorded yet.<br/><span style="font-size:0.85em; opacity:0.8;">Live activity will stream here in real-time as MCP tools and system events execute.</span></div>';
return;
}
const getMillis = (item) => {
if (!item)
return 0;
if (typeof item === "string") {
try {
item = JSON.parse(item);
} catch (e) {
return 0;
}
}
if (item.method === "notifications/activity" && item.params) {
item = item.params;
}
let val = item.timestamp || item.time || item.updated_at || 0;
if (typeof val === "number" && val < 10000000000) {
val = val * 1000;
}
return val;
};
const sorted = [...history].sort((a, b) => getMillis(b) - getMillis(a));
sorted.forEach((item) => {
const div = document.createElement("div");
div.className = "feed-entry";
div.innerHTML = parseActivityPayload(item);
feed.appendChild(div);
});
if (sorted.length > 0) {
feed.scrollTop = 0;
}
} catch (e) {
console.error("Failed to load activity history", e);
}
}
var wsConnected = false;
var activeSSE = null;
var activeWS = null;
var wsRpcId = 1;
var pendingWsRequests = new Map;
function sendWsRpc(method, params = {}, timeoutMs = 5000) {
return new Promise((resolve, reject) => {
if (!activeWS || activeWS.readyState !== WebSocket.OPEN) {
return reject(new Error("WebSocket not open"));
}
const id = ++wsRpcId;
const timer = setTimeout(() => {
pendingWsRequests.delete(id);
reject(new Error(`WebSocket RPC timeout for ${method}`));
}, timeoutMs);
pendingWsRequests.set(id, { resolve, reject, timer });
try {
activeWS.send(JSON.stringify({
jsonrpc: "2.0",
id,
method,
params
}));
} catch (e) {
clearTimeout(timer);
pendingWsRequests.delete(id);
reject(e);
}
});
}
var tabRefreshDebounceTimers = {};
function requestDomainRefresh(domain) {
const tabMap = {
graph: "graph-tab",
task: "task-tab",
techdebt: "techdebt-tab",
adrs: "adrs-tab",
snippets: "snippets-tab",
terminal: "terminal-tab",
ledger: "ledger-tab",
memos: "memos-tab",
env: "env-tab",
gates: "env-tab"
};
const targetTab = tabMap[domain];
if (!targetTab || targetTab !== currentTabId) {
return;
}
if (tabRefreshDebounceTimers[domain]) {
clearTimeout(tabRefreshDebounceTimers[domain]);
}
tabRefreshDebounceTimers[domain] = setTimeout(() => {
delete tabRefreshDebounceTimers[domain];
switch (domain) {
case "graph":
loadGraph();
break;
case "task":
loadTasks();
break;
case "techdebt":
loadTechDebt();
break;
case "adrs":
loadADRs();
break;
case "snippets":
loadSnippets();
break;
case "terminal":
loadTerminal();
break;
case "ledger":
loadLedger();
break;
case "memos":
loadMemos();
break;
case "env":
case "gates":
loadEnvironment();
break;
}
}, 250);
}
function handleIncomingActivity(payload) {
let category = "";
let method = "";
if (typeof payload === "object" && payload !== null) {
method = payload.method || "";
if (payload.params) {
category = (payload.params.category || payload.params.type || "").toUpperCase();
} else {
category = (payload.category || payload.type || "").toUpperCase();
}
}
if (method === "notifications/resources/updated" || category === "GRAPH") {
requestDomainRefresh("graph");
} else if (category === "DECISION" || category === "ADR") {
requestDomainRefresh("adrs");
requestDomainRefresh("graph");
} else if (method === "notifications/task/completed" || category.startsWith("TASK")) {
requestDomainRefresh("task");
} else if (category === "GATE" || category === "QUALITY_GATE" || category === "ENVIRONMENT" || category === "ENV") {
requestDomainRefresh("gates");
requestDomainRefresh("env");
} else if (category === "TECH_DEBT" || category === "ERROR_FIX" || category === "CHECKPOINT") {
requestDomainRefresh("techdebt");
} else if (category === "SNIPPET") {
requestDomainRefresh("snippets");
} else if (category === "TERMINAL") {
requestDomainRefresh("terminal");
} else if (category === "CODE_CHANGE" || category === "LEDGER") {
requestDomainRefresh("ledger");
} else if (category === "MEMO" || category === "MILESTONE" || category === "HANDOFF_MEMO") {
requestDomainRefresh("memos");
}
const feed = document.getElementById("activity-feed");
if (feed) {
if (feed.querySelector(".feed-entry") === null) {
feed.innerHTML = "";
}
const isScrolledToTop = feed.scrollTop <= 20;
const div = document.createElement("div");
div.className = "feed-entry";
div.innerHTML = parseActivityPayload(payload);
feed.prepend(div);
while (feed.children.length > MAX_ACTIVITY_HISTORY && feed.lastChild) {
feed.removeChild(feed.lastChild);
}
if (isScrolledToTop)
feed.scrollTop = 0;
}
}
function setupWS() {
const protocol = location.protocol === "https:" ? "wss:" : "ws:";
let ws;
try {
ws = new WebSocket(`${protocol}//${location.host}/ws?client=ui`);
} catch (e) {
console.error("WebSocket creation error", e);
setupSSE();
return;
}
ws.onopen = function() {
wsConnected = true;
activeWS = ws;
if (activeSSE) {
activeSSE.close();
activeSSE = null;
}
};
ws.onmessage = function(event) {
try {
const data = JSON.parse(event.data);
if (data.id !== undefined && pendingWsRequests.has(data.id)) {
const handler = pendingWsRequests.get(data.id);
pendingWsRequests.delete(data.id);
clearTimeout(handler.timer);
if (data.error) {
handler.reject(new Error(data.error.message || JSON.stringify(data.error)));
} else {
handler.resolve(data.result);
}
return;
}
if (data.type === "activity" || data.type === "gate" || data.type === "task" || data.type === "tech_debt" || data.type === "nvim_telemetry" || data.method === "notifications/activity" || data.method === "notifications/task/completed" || data.method === "notifications/resources/updated") {
const payload = data.params || data.data || data;
handleIncomingActivity(payload);
}
} catch (e) {
console.error("WebSocket message parse error", e);
}
};
ws.onclose = function() {
wsConnected = false;
activeWS = null;
pendingWsRequests.forEach((handler) => {
clearTimeout(handler.timer);
handler.reject(new Error("WebSocket disconnected"));
});
pendingWsRequests.clear();
console.log("WebSocket closed, falling back to SSE and retrying WS in 5s...");
setupSSE();
setTimeout(setupWS, 5000);
};
ws.onerror = function() {
wsConnected = false;
activeWS = null;
pendingWsRequests.forEach((handler) => {
clearTimeout(handler.timer);
handler.reject(new Error("WebSocket error"));
});
pendingWsRequests.clear();
setupSSE();
};
}
var listControllers = new Map;
function applyListFiltering(containerId) {
const state = listControllers.get(containerId);
if (!state)
return;
let result = state.items.slice();
const q = (state.searchQuery || "").trim().toLowerCase();
if (q) {
result = result.filter((item) => {
if (state.options.searchFields) {
const fields = state.options.searchFields(item);
return fields.some((f) => f && String(f).toLowerCase().includes(q));
}
return JSON.stringify(item).toLowerCase().includes(q);
});
}
if (state.options.filters) {
for (const filterDef of state.options.filters) {
const activeVal = state.activeFilters[filterDef.id];
if (activeVal !== undefined && activeVal !== "") {
result = result.filter((item) => filterDef.predicate(item, activeVal));
}
}
}
const factor = state.sortOrder === "desc" ? -1 : 1;
const extractor = state.options.sortExtractor || ((item) => {
if (state.options.timestampExtractor) {
const ts = state.options.timestampExtractor(item);
if (ts != null)
return ts;
}
return item.timestamp || item.created_at || item.id || 0;
});
result.sort((a, b) => {
const valA = extractor(a);
const valB = extractor(b);
if (typeof valA === "number" && typeof valB === "number") {
return (valA - valB) * factor;
}
return String(valA).localeCompare(String(valB)) * factor;
});
state.filteredItems = result;
}
function renderListCardsAndPagination(containerId) {
const state = listControllers.get(containerId);
if (!state)
return;
const totalItems = state.filteredItems.length;
const pageSize = state.pageSize;
const totalPages = pageSize <= 0 ? 1 : Math.max(1, Math.ceil(totalItems / pageSize));
if (state.currentPage > totalPages) {
state.currentPage = totalPages;
}
if (state.currentPage < 1) {
state.currentPage = 1;
}
const startIndex = totalItems === 0 ? 0 : (state.currentPage - 1) * (pageSize > 0 ? pageSize : totalItems) + 1;
const endIndex = pageSize <= 0 ? totalItems : Math.min(state.currentPage * pageSize, totalItems);
const metaEl = document.getElementById(`${containerId}-meta`);
if (metaEl) {
const itemNoun = state.options.itemNoun || "items";
let summaryText = "";
if (totalItems === 0) {
summaryText = `0 ${itemNoun}`;
} else {
summaryText = `Showing <strong>${startIndex}–${endIndex}</strong> of <strong>${totalItems}</strong> ${itemNoun}`;
}
if (totalItems !== state.items.length) {
summaryText += ` <span style="color:var(--text-secondary);">(filtered from ${state.items.length} total)</span>`;
}
metaEl.innerHTML = `<div>${summaryText}</div><div>Page <strong>${state.currentPage}</strong> of <strong>${totalPages}</strong></div>`;
}
const cardsEl = document.getElementById(`${containerId}-cards`);
if (cardsEl) {
if (totalItems === 0) {
const itemNoun = state.options.itemNoun || "items";
if (state.items.length === 0) {
cardsEl.innerHTML = `<div class="enterprise-empty-state">No ${itemNoun} recorded.</div>`;
} else {
cardsEl.innerHTML = `
<div class="enterprise-empty-state">
<div>\uD83D\uDD0D No matching ${itemNoun} found for current filter.</div>
<button class="enterprise-btn" style="margin-top:12px;" onclick="onListResetFilters('${containerId}')">Reset Filters</button>
</div>`;
}
} else {
const pageSlice = pageSize <= 0 ? state.filteredItems : state.filteredItems.slice((state.currentPage - 1) * pageSize, state.currentPage * pageSize);
cardsEl.innerHTML = pageSlice.map((item) => {
const ts = state.options.timestampExtractor ? state.options.timestampExtractor(item) : item.timestamp || item.created_at || null;
const dateStr = ts ? new Date(ts * 1000).toLocaleString() : "";
const borderCol = state.options.cardBorderColor || "#9b59b6";
return `
<div class="task-card" style="border-left-color: ${borderCol};">
${dateStr ? `<div style="font-size:0.75em; color:var(--text-secondary); float:right;">${dateStr}</div>` : ""}
${state.formatter(item)}
</div>`;
}).join("");
}
}
const paginationEl = document.getElementById(`${containerId}-pagination`);
if (paginationEl) {
if (totalPages <= 1) {
paginationEl.style.display = "none";
paginationEl.innerHTML = "";
} else {
paginationEl.style.display = "flex";
let pagHtml = "";
pagHtml += `<button onclick="onListPageChange('${containerId}', 1)" ${state.currentPage === 1 ? "disabled" : ""} title="First Page">⏮</button>`;
pagHtml += `<button onclick="onListPageChange('${containerId}', ${state.currentPage - 1})" ${state.currentPage === 1 ? "disabled" : ""} title="Previous Page">◀</button>`;
const cur = state.currentPage;
const startP = Math.max(1, cur - 2);
const endP = Math.min(totalPages, cur + 2);
if (startP > 1) {
pagHtml += `<button onclick="onListPageChange('${containerId}', 1)">1</button>`;
if (startP > 2)
pagHtml += `<span style="padding:0 4px; color:var(--text-secondary);">…</span>`;
}
for (let p = startP;p <= endP; p++) {
pagHtml += `<button class="${p === cur ? "active" : ""}" onclick="onListPageChange('${containerId}', ${p})">${p}</button>`;
}
if (endP < totalPages) {
if (endP < totalPages - 1)
pagHtml += `<span style="padding:0 4px; color:var(--text-secondary);">…</span>`;
pagHtml += `<button onclick="onListPageChange('${containerId}', ${totalPages})">${totalPages}</button>`;
}
pagHtml += `<button onclick="onListPageChange('${containerId}', ${state.currentPage + 1})" ${state.currentPage === totalPages ? "disabled" : ""} title="Next Page">▶</button>`;
pagHtml += `<button onclick="onListPageChange('${containerId}', ${totalPages})" ${state.currentPage === totalPages ? "disabled" : ""} title="Last Page">⏭</button>`;
paginationEl.innerHTML = pagHtml;
}
}
const clearBtn = document.getElementById(`${containerId}-clear`);
if (clearBtn) {
clearBtn.style.display = state.searchQuery ? "block" : "none";
}
const sortBtn = document.getElementById(`${containerId}-sort-btn`);
if (sortBtn) {
sortBtn.innerHTML = state.sortOrder === "desc" ? "⬇️ Newest First" : "⬆️ Oldest First";
}
}
function renderListStructure(containerId) {
const state = listControllers.get(containerId);
if (!state)
return;
const container = document.getElementById(containerId);
if (!container)
return;
const itemNoun = state.options.itemNoun || "items";
let filtersHtml = "";
if (state.options.filters && state.options.filters.length > 0) {
for (const f of state.options.filters) {
const activeVal = state.activeFilters[f.id] || "";
filtersHtml += `
<select class="enterprise-select" id="${containerId}-filter-${f.id}" onchange="onListFilterChange('${containerId}', '${f.id}', this.value)" title="Filter by ${f.label}">
${f.options.map((opt) => `<option value="${escapeHtml(opt.value)}" ${opt.value === activeVal ? "selected" : ""}>${escapeHtml(opt.label)}</option>`).join("")}
</select>`;
}
}
const curPageSize = state.pageSize;
const pageSizeOptions = [
{ label: "10 / page", value: "10" },
{ label: "25 / page", value: "25" },
{ label: "50 / page", value: "50" },
{ label: "100 / page", value: "100" },
{ label: "All items", value: "-1" }
];
const pageSizeHtml = `
<select class="enterprise-select" id="${containerId}-pagesize" onchange="onListPageSizeChange('${containerId}', this.value)" title="Items per page">
${pageSizeOptions.map((opt) => `<option value="${opt.value}" ${parseInt(opt.value, 10) === curPageSize ? "selected" : ""}>${opt.label}</option>`).join("")}
</select>`;
container.innerHTML = `
<div class="enterprise-toolbar" id="${containerId}-toolbar">
<div class="enterprise-search-wrap">
<span class="enterprise-search-icon">\uD83D\uDD0D</span>
<input
type="text"
class="enterprise-search-input"
id="${containerId}-search"
placeholder="Filter ${itemNoun} (/)..."
value="${escapeHtml(state.searchQuery)}"
oninput="onListSearchInput('${containerId}', this.value)"
/>
<button
class="enterprise-clear-btn"
id="${containerId}-clear"
onclick="onListClearSearch('${containerId}')"
title="Clear search"
style="display:${state.searchQuery ? "block" : "none"};"
>✕</button>
</div>
<div class="enterprise-filters-group">
${filtersHtml}
<button
class="enterprise-btn"
id="${containerId}-sort-btn"
onclick="onListToggleSort('${containerId}')"
title="Toggle sort direction"
>
${state.sortOrder === "desc" ? "⬇️ Newest First" : "⬆️ Oldest First"}
</button>
${pageSizeHtml}
<button
class="enterprise-btn"
onclick="onListResetFilters('${containerId}')"
title="Reset all filters and sorting"
>
\uD83D\uDD04 Reset
</button>
</div>
</div>
<div class="enterprise-meta-bar" id="${containerId}-meta"></div>
<div class="enterprise-cards-list" id="${containerId}-cards"></div>
<div class="enterprise-pagination" id="${containerId}-pagination"></div>
`;
renderListCardsAndPagination(containerId);
}
function onListSearchInput(containerId, value) {
const state = listControllers.get(containerId);
if (!state)
return;
if (state.debounceTimer)
clearTimeout(state.debounceTimer);
state.searchQuery = value;
state.debounceTimer = setTimeout(() => {
state.currentPage = 1;
applyListFiltering(containerId);
renderListCardsAndPagination(containerId);
}, 150);
}
function onListClearSearch(containerId) {
const state = listControllers.get(containerId);
if (!state)
return;
state.searchQuery = "";
const inputEl = document.getElementById(`${containerId}-search`);
if (inputEl) {
inputEl.value = "";
inputEl.focus();
}
state.currentPage = 1;
applyListFiltering(containerId);
renderListCardsAndPagination(containerId);
}
function onListFilterChange(containerId, filterId, value) {
const state = listControllers.get(containerId);
if (!state)
return;
state.activeFilters[filterId] = value;
state.currentPage = 1;
applyListFiltering(containerId);
renderListCardsAndPagination(containerId);
}
function onListToggleSort(containerId) {
const state = listControllers.get(containerId);
if (!state)
return;
state.sortOrder = state.sortOrder === "desc" ? "asc" : "desc";
applyListFiltering(containerId);
renderListCardsAndPagination(containerId);
}
function onListPageSizeChange(containerId, value) {
const state = listControllers.get(containerId);
if (!state)
return;
state.pageSize = parseInt(value, 10);
state.currentPage = 1;
renderListCardsAndPagination(containerId);
}
function onListPageChange(containerId, page) {
const state = listControllers.get(containerId);
if (!state)
return;
state.currentPage = page;
renderListCardsAndPagination(containerId);
const container = document.getElementById(containerId);
if (container) {
container.scrollIntoView({ behavior: "smooth", block: "nearest" });
}
}
function onListResetFilters(containerId) {
const state = listControllers.get(containerId);
if (!state)
return;
state.searchQuery = "";
state.activeFilters = {};
state.sortOrder = state.options.defaultSort || "desc";
state.pageSize = state.options.pageSize || 25;
state.currentPage = 1;
applyListFiltering(containerId);
renderListStructure(containerId);
}
async function loadGenericList(endpoint, containerId, formatter, options) {
const container = document.getElementById(containerId);
if (!container)
return;
const resolvedOptions = options || {};
const existingState = listControllers.get(containerId);
const state = existingState || {
endpoint,
containerId,
formatter,
options: resolvedOptions,
items: [],
filteredItems: [],
searchQuery: "",
activeFilters: {},
sortOrder: resolvedOptions.defaultSort || "desc",
currentPage: 1,
pageSize: resolvedOptions.pageSize || 25,
debounceTimer: null
};
state.options = resolvedOptions;
state.formatter = formatter;
listControllers.set(containerId, state);
try {
const res = await fetch(endpoint, { cache: "no-store" });
if (!res.ok) {
container.innerHTML = `<div style="color:var(--error-color); padding:20px; text-align:center;">⚠️ Failed to load ${escapeHtml(endpoint)} (${res.status} ${escapeHtml(res.statusText)})</div>`;
return;
}
const data = await res.json();
state.items = Array.isArray(data) ? data : [];
applyListFiltering(containerId);
const toolbarExists = document.getElementById(`${containerId}-toolbar`);
if (!toolbarExists) {
renderListStructure(containerId);
} else {
renderListCardsAndPagination(containerId);
}
} catch (err) {
console.error(`Failed to load ${endpoint}`, err);
container.innerHTML = `<div style="color:var(--error-color); padding:20px; text-align:center;">⚠️ Failed to load ${escapeHtml(endpoint)} (${escapeHtml(String(err))})</div>`;
}
}
function loadLedger() {
loadGenericList("/api/ledger?limit=500", "ledger-container", (item) => {
const vcs = (item.vcs_type || (item.revision && item.revision.startsWith("r") ? "svn" : "git")).toUpperCase();
const vcsColor = vcs === "SVN" ? "#e67e22" : "#3498db";
const rev = item.revision || item.git_commit;
const revLabel = vcs === "SVN" ? "Rev" : "Commit";
const branch = item.branch || item.git_branch || "None";
const repo = item.repo_name || item.namespace || "global";
const changeKind = (item.change_kind || "modified").toLowerCase();
let kindColor = "var(--accent-color)";
let kindLabel = "Modified";
if (changeKind === "added") {
kindColor = "var(--success-color)";
kindLabel = "+ Added";
} else if (changeKind === "deleted") {
kindColor = "var(--error-color)";
kindLabel = "- Deleted";
} else if (changeKind === "renamed") {
kindColor = "#e67e22";
kindLabel = "→ Renamed";
}
const symbols = Array.isArray(item.symbols) && item.symbols.length > 0 ? item.symbols.map((s) => `<span style="background:var(--card-bg); padding:1px 6px; border-radius:3px; font-size:0.75em; border:1px solid var(--border-color); font-family:monospace;">${escapeHtml(s)}</span>`).join(" ") : "";
const authorBadge = item.author ? `<span style="background:var(--canvas-bg); padding:2px 8px; border-radius:4px; border:1px solid var(--border-color); font-size:0.8em; color:var(--text-secondary);">\uD83D\uDC64 ${escapeHtml(item.author)}</span>` : "";
const lineBadge = item.line_range ? `<span style="background:var(--canvas-bg); padding:2px 8px; border-radius:4px; border:1px solid var(--border-color); font-size:0.8em; font-family:monospace;">${escapeHtml(item.line_range)}</span>` : "";
return `
<div style="display:flex; justify-content:space-between; align-items:flex-start; gap:10px;">
<span style="font-weight:bold; font-size:1.05em; color:var(--text-primary); font-family:monospace;">${escapeHtml(item.file_path || "Unknown")}</span>
<div style="display:flex; gap:6px; align-items:center;">
<span style="background:${vcsColor}; color:#fff; font-size:0.75em; font-weight:bold; padding:2px 6px; border-radius:4px; text-transform:uppercase;">${vcs}</span>
<span style="background:var(--canvas-bg); color:${kindColor}; border:1px solid ${kindColor}; font-size:0.75em; font-weight:bold; padding:2px 6px; border-radius:4px;">${kindLabel}</span>
</div>
</div>
<div style="margin-top:6px; font-size:0.95em; color:var(--text-primary);">${escapeHtml(item.description || "")}</div>
${symbols ? `<div style="margin-top:8px; display:flex; gap:5px; flex-wrap:wrap; align-items:center;"><span style="font-size:0.8em; color:var(--text-secondary);">Symbols:</span> ${symbols}</div>` : ""}
<div style="margin-top:10px; font-size:0.85em; display:flex; gap:8px; flex-wrap:wrap; align-items:center;">
<span style="background:var(--canvas-bg); padding:2px 8px; border-radius:4px; font-weight:bold; border:1px solid var(--border-color); color:var(--text-primary);">\uD83D\uDCE6 ${escapeHtml(repo)}</span>
<span style="background:var(--canvas-bg); padding:2px 8px; border-radius:4px; font-family:monospace; border:1px solid var(--border-color);">${revLabel}: ${rev ? escapeHtml(rev.substring(0, 10)) : "None"}</span>
<span style="background:var(--canvas-bg); padding:2px 8px; border-radius:4px; border:1px solid var(--border-color);">\uD83C\uDF3F ${escapeHtml(branch)}</span>
${lineBadge}
${authorBadge}
</div>
`;
}, {
itemNoun: "Ledger Entries",
cardBorderColor: "#3498db",
defaultSort: "desc",
timestampExtractor: (item) => item.timestamp,
searchFields: (item) => [
item.file_path,
item.description,
item.author,
item.repo_name,
item.branch,
item.git_commit,
item.revision,
Array.isArray(item.symbols) ? item.symbols.join(" ") : ""
],
filters: [
{
id: "vcs",
label: "VCS",
options: [
{ label: "All VCS", value: "" },
{ label: "Git", value: "git" },
{ label: "SVN", value: "svn" }
],
predicate: (item, val) => {
const vcs = (item.vcs_type || (item.revision && item.revision.startsWith("r") ? "svn" : "git")).toLowerCase();
return vcs === val.toLowerCase();
}
},
{
id: "kind",
label: "Change Kind",
options: [
{ label: "All Types", value: "" },
{ label: "+ Added", value: "added" },
{ label: "Modified", value: "modified" },
{ label: "- Deleted", value: "deleted" },
{ label: "→ Renamed", value: "renamed" }
],
predicate: (item, val) => (item.change_kind || "modified").toLowerCase() === val.toLowerCase()
}
]
});
}
function loadTerminal() {
loadGenericList("/api/terminal/history", "terminal-container", (item) => {
const isSuccess = item.exit_code === 0;
const statusColor = isSuccess ? "var(--success-color)" : "var(--error-color)";
const statusBg = isSuccess ? "rgba(46, 204, 113, 0.12)" : "rgba(231, 76, 60, 0.12)";
const durationStr = item.duration_ms != null ? item.duration_ms < 1000 ? `${item.duration_ms}ms` : `${(item.duration_ms / 1000).toFixed(2)}s` : "";
return `
<div style="display:flex; justify-content:space-between; align-items:flex-start; gap:10px;">
<span style="font-family:monospace; font-weight:bold; font-size:1.05em; color:var(--text-primary); word-break:break-all;">$ ${escapeHtml(item.command)}</span>
<div style="display:flex; gap:6px; align-items:center; flex-shrink:0;">
${durationStr ? `<span style="background:var(--canvas-bg); padding:2px 6px; border-radius:4px; font-size:0.75em; border:1px solid var(--border-color); color:var(--text-secondary); font-family:monospace;">⏱️ ${escapeHtml(durationStr)}</span>` : ""}
<span style="background:${statusBg}; color:${statusColor}; border:1px solid ${statusColor}; font-size:0.75em; font-weight:bold; padding:2px 6px; border-radius:4px; font-family:monospace;">Exit ${item.exit_code}</span>
</div>
</div>
<div style="margin-top:8px; font-size:0.85em; display:flex; gap:8px; flex-wrap:wrap; align-items:center;">
${item.shell ? `<span style="background:var(--canvas-bg); padding:2px 6px; border-radius:4px; border:1px solid var(--border-color); font-weight:600;">\uD83D\uDC1A ${escapeHtml(item.shell)}</span>` : ""}
<span style="background:var(--canvas-bg); padding:2px 6px; border-radius:4px; border:1px solid var(--border-color);">OS: ${escapeHtml(item.os || "Unknown")}</span>
<span style="background:var(--canvas-bg); padding:2px 6px; border-radius:4px; font-family:monospace; border:1px solid var(--border-color); word-break:break-all;">\uD83D\uDCC2 ${escapeHtml(item.cwd || "Unknown")}</span>
${item.repo_name ? `<span style="background:var(--canvas-bg); padding:2px 6px; border-radius:4px; border:1px solid var(--border-color);">\uD83D\uDCE6 ${escapeHtml(item.repo_name)}</span>` : ""}
${item.invoker ? `<span style="background:var(--canvas-bg); padding:2px 6px; border-radius:4px; border:1px solid var(--border-color); color:var(--text-secondary);">\uD83D\uDC64 ${escapeHtml(item.invoker)}</span>` : ""}
</div>
${item.status_reason ? `<div style="margin-top:6px; font-size:0.85em; color:var(--error-color);"><strong>Reason:</strong> ${escapeHtml(item.status_reason)}</div>` : ""}
${item.stdout_summary ? `<div style="margin-top:6px; font-size:0.85em; color:var(--text-secondary);"><strong>Output:</strong> ${escapeHtml(item.stdout_summary)}</div>` : ""}
${item.error_output ? `
<details style="margin-top:8px;" ${!isSuccess ? "open" : ""}>
<summary style="font-size:0.8em; color:var(--error-color); cursor:pointer; font-weight:600;">Error Output</summary>
<pre style="background:#1a0f0f; color:#ff6b6b; padding:8px; border-radius:4px; border:1px solid rgba(231,76,60,0.3); font-size:0.8em; overflow-x:auto; margin-top:4px; white-space:pre-wrap;">${escapeHtml(item.error_output)}</pre>
</details>
` : ""}
`;
}, {
itemNoun: "Commands",
cardBorderColor: "#1abc9c",
defaultSort: "desc",
timestampExtractor: (item) => item.timestamp,
searchFields: (item) => [
item.command,
item.cwd,
item.repo_name,
item.invoker,
item.shell,
item.stdout_summary,
item.error_output
],
filters: [
{
id: "status",
label: "Exit Code",
options: [
{ label: "All Results", value: "" },
{ label: "Success (0)", value: "0" },
{ label: "Failed (!= 0)", value: "err" }
],
predicate: (item, val) => {
if (val === "0")
return item.exit_code === 0;
if (val === "err")
return item.exit_code !== 0;
return true;
}
}
]
});
}
function loadTechDebt() {
loadGenericList("/api/tech_debts", "techdebt-container", (item) => {
const sev = (item.severity || "medium").toLowerCase();
let sevBg = "rgba(241, 196, 15, 0.15)";
let sevColor = "#f1c40f";
let sevBorder = "rgba(241, 196, 15, 0.4)";
if (sev === "critical" || sev === "blocker") {
sevBg = "rgba(231, 76, 60, 0.18)";
sevColor = "#e74c3c";
sevBorder = "rgba(231, 76, 60, 0.5)";
} else if (sev === "high") {
sevBg = "rgba(230, 126, 34, 0.18)";
sevColor = "#e67e22";
sevBorder = "rgba(230, 126, 34, 0.5)";
} else if (sev === "low" || sev === "trivial") {
sevBg = "rgba(52, 152, 219, 0.15)";
sevColor = "#3498db";
sevBorder = "rgba(52, 152, 219, 0.4)";
}
const statusBadge = item.is_resolved ? '<span style="color:var(--success-color); font-weight:bold; background: rgba(46, 204, 113, 0.15); padding: 2px 6px; border-radius: 4px; border: 1px solid var(--success-color);">✓ Resolved</span>' : '<span style="color:var(--open-color); font-weight:bold; background: rgba(230, 126, 34, 0.15); padding: 2px 6px; border-radius: 4px; border: 1px solid var(--open-color);">● Open</span>';
const fileLoc = item.file_path ? `${item.file_path}${item.line_range ? `:${item.line_range}` : ""}` : "";
return `
<div style="display:flex; justify-content:space-between; align-items:center; gap:8px;">
<div style="display:flex; align-items:center; gap:8px; flex-wrap:wrap;">
<strong style="font-family:monospace; font-size:1em;">${escapeHtml(item.id)}</strong>
<span style="background:${sevBg}; color:${sevColor}; border:1px solid ${sevBorder}; font-size:0.75em; font-weight:bold; padding:2px 6px; border-radius:4px; text-transform:uppercase;">${escapeHtml(item.severity || "medium")}</span>
${item.effort_estimate ? `<span style="background:var(--canvas-bg); font-size:0.75em; padding:2px 6px; border-radius:4px; border:1px solid var(--border-color); color:var(--text-secondary);">⏳ ${escapeHtml(item.effort_estimate)}</span>` : ""}
</div>
<div style="display:flex; align-items:center; gap:8px;">
<div>${statusBadge}</div>
${!item.is_resolved ? `<button class="complete-btn" style="position:static;" onclick="resolveTechDebt('${escapeHtml(item.id)}')" title="Resolve Tech Debt">✓</button>` : ""}
</div>
</div>
${fileLoc ? `<div style="margin-top:6px; font-family:monospace; font-size:0.85em; color:var(--accent-color);">\uD83D\uDCC4 ${escapeHtml(fileLoc)}</div>` : ""}
<div style="margin-top:6px; font-size:0.9em; line-height:1.4;"><strong>Description:</strong> ${escapeHtml(item.description || "")}</div>
${item.workaround ? `<div style="margin-top:4px; font-size:0.85em; color:var(--text-secondary); background:var(--canvas-bg); padding:4px 8px; border-radius:4px; border-left:3px solid #f39c12;"><strong>Workaround:</strong> ${escapeHtml(item.workaround)}</div>` : ""}
<div style="margin-top:6px; font-size:0.9em; line-height:1.4; color:var(--text-secondary);"><strong>Ideal Solution:</strong> ${escapeHtml(item.ideal_solution || "")}</div>
<div style="margin-top:8px; font-size:0.8em; display:flex; gap:8px; flex-wrap:wrap; align-items:center;">
${item.repo_name ? `<span style="background:var(--canvas-bg); padding:2px 6px; border-radius:4px; border:1px solid var(--border-color);">\uD83D\uDCE6 ${escapeHtml(item.repo_name)}</span>` : ""}
<span style="background:var(--canvas-bg); padding:2px 6px; border-radius:4px; font-family:monospace; border:1px solid var(--border-color);">Commit: ${item.git_commit ? escapeHtml(item.git_commit.substring(0, 8)) : "None"}</span>
<span style="background:var(--canvas-bg); padding:2px 6px; border-radius:4px; border:1px solid var(--border-color);">\uD83C\uDF3F ${escapeHtml(item.git_branch || "None")}</span>
<span style="background:var(--canvas-bg); padding:2px 6px; border-radius:4px; border:1px solid var(--border-color); color:var(--text-secondary);">\uD83C\uDFF7️ ${escapeHtml(item.namespace || "global")}</span>
</div>
`;
}, {
itemNoun: "Tech Debts",
cardBorderColor: "#e67e22",
defaultSort: "desc",
timestampExtractor: (item) => item.timestamp,
searchFields: (item) => [
item.id,
item.description,
item.ideal_solution,
item.workaround,
item.file_path,
item.repo_name,
item.severity
],
filters: [
{
id: "status",
label: "Status",
options: [
{ label: "All Statuses", value: "" },
{ label: "Open", value: "open" },
{ label: "Resolved", value: "resolved" }
],
predicate: (item, val) => {
if (val === "resolved")
return !!item.is_resolved;
if (val === "open")
return !item.is_resolved;
return true;
}
},
{
id: "severity",
label: "Severity",
options: [
{ label: "All Severities", value: "" },
{ label: "Critical", value: "critical" },
{ label: "High", value: "high" },
{ label: "Medium", value: "medium" },
{ label: "Low", value: "low" }
],
predicate: (item, val) => String(item.severity || "").toLowerCase() === val.toLowerCase()
}
]
});
loadGenericList("/api/error_fixes", "errorfixes-container", (item) => `
<div style="display:flex; justify-content:space-between; align-items:flex-start; gap:8px;">
<div>
<strong style="color:var(--text-primary); font-family:monospace; font-size:1em;">${escapeHtml(item.signature || "Error")}</strong>
${item.error_category ? `<span style="background:rgba(231,76,60,0.15); color:var(--error-color); border:1px solid rgba(231,76,60,0.4); font-size:0.75em; font-weight:bold; padding:2px 6px; border-radius:4px; margin-left:6px;">${escapeHtml(item.error_category)}</span>` : ""}
${item.toolchain ? `<span style="background:var(--canvas-bg); color:var(--text-secondary); border:1px solid var(--border-color); font-size:0.75em; padding:2px 6px; border-radius:4px; margin-left:6px; font-family:monospace;">\uD83D\uDD27 ${escapeHtml(item.toolchain)}</span>` : ""}
</div>
<span style="color:var(--success-color); font-weight:bold; background: rgba(46, 204, 113, 0.15); padding: 2px 6px; border-radius: 4px; border: 1px solid var(--success-color); flex-shrink:0;">✓ Resolved</span>
</div>
<div style="margin-top:8px; font-size:0.9em; line-height:1.4;"><strong>Solution:</strong> ${escapeHtml(item.solution || "")}</div>
${item.stack_trace ? `
<details style="margin-top:8px;">
<summary style="font-size:0.8em; color:var(--text-secondary); cursor:pointer; font-weight:600;">Stack Trace</summary>
<pre style="background:#111417; color:#ff7b72; padding:8px; border-radius:4px; border:1px solid var(--border-color); font-size:0.8em; overflow-x:auto; margin-top:4px; white-space:pre-wrap;">${escapeHtml(item.stack_trace)}</pre>
</details>
` : ""}
<div style="margin-top:8px; font-size:0.8em; display:flex; gap:8px; flex-wrap:wrap; align-items:center;">
${item.repo_name ? `<span style="background:var(--canvas-bg); padding:2px 6px; border-radius:4px; border:1px solid var(--border-color);">\uD83D\uDCE6 ${escapeHtml(item.repo_name)}</span>` : ""}
<span style="background:var(--canvas-bg); padding:2px 6px; border-radius:4px; font-family:monospace; border:1px solid var(--border-color);">Commit: ${item.git_commit ? escapeHtml(item.git_commit.substring(0, 8)) : "None"}</span>
${item.git_branch ? `<span style="background:var(--canvas-bg); padding:2px 6px; border-radius:4px; border:1px solid var(--border-color);">\uD83C\uDF3F ${escapeHtml(item.git_branch)}</span>` : ""}
</div>
`, {
itemNoun: "Error Fixes",
cardBorderColor: "#2ecc71",
defaultSort: "desc",
timestampExtractor: (item) => item.timestamp,
searchFields: (item) => [
item.signature,
item.solution,
item.toolchain,
item.error_category,
item.repo_name,
item.stack_trace
],
filters: [
{
id: "category",
label: "Category",
options: [
{ label: "All Categories", value: "" },
{ label: "Compilation", value: "compilation" },
{ label: "Runtime", value: "runtime" },
{ label: "Test", value: "test" },
{ label: "Configuration", value: "configuration" }
],
predicate: (item, val) => String(item.error_category || "").toLowerCase() === val.toLowerCase()
}
]
});
}
function loadADRs() {
loadGenericList("/api/adrs", "adrs-container", (item) => {
const statusLower = (item.status || "").toLowerCase();
let badgeStyle = "color:var(--accent-color); border-color:rgba(88, 166, 255, 0.4); background:rgba(88, 166, 255, 0.12);";
let badgeText = `● ${escapeHtml(item.status || "accepted")}`;
if (statusLower === "implemented" || statusLower === "resolved") {
badgeStyle = "color:var(--success-color); border-color:rgba(46, 160, 67, 0.4); background:rgba(46, 160, 67, 0.15); font-weight:600;";
badgeText = `✓ ${escapeHtml(item.status)}`;
} else if (statusLower === "superseded") {
badgeStyle = "text-decoration: line-through; color:var(--error-color); border-color:rgba(248, 81, 73, 0.4); background:rgba(248, 81, 73, 0.12);";
badgeText = `✕ ${escapeHtml(item.status)}`;
} else if (statusLower === "proposed" || statusLower === "draft") {
badgeStyle = "color:var(--warning-color); border-color:rgba(210, 153, 34, 0.4); background:rgba(210, 153, 34, 0.12);";
badgeText = `○ ${escapeHtml(item.status)}`;
}
const resolvedDate = item.resolved_at ? new Date(item.resolved_at * 1000).toISOString().replace("T", " ").substring(0, 19) : "";
return `
<div style="display:flex; justify-content:space-between; align-items:center;">
<strong>${escapeHtml(item.id)} | ${escapeHtml(item.title)}</strong>
<span style="background:var(--canvas-bg); padding:2px 8px; border-radius:4px; font-size:0.8em; border:1px solid var(--border-color); ${badgeStyle}">${badgeText}</span>
</div>
${item.git_commit || item.git_branch || item.task_id || item.resolved_at ? `
<div style="font-size:0.8em; color:var(--text-secondary); margin-top:4px; font-family:monospace; display:flex; gap:12px; flex-wrap:wrap;">
${item.git_branch || item.git_commit ? `<span>\uD83D\uDCE6 ${escapeHtml(item.git_branch || "repo")} @ ${escapeHtml((item.git_commit || "").substring(0, 8))}</span>` : ""}
${item.task_id ? `<span>\uD83C\uDFAF Task: ${escapeHtml(item.task_id)}</span>` : ""}
${resolvedDate ? `<span>✓ Resolved: ${escapeHtml(resolvedDate)}</span>` : ""}
</div>
` : ""}
<div style="font-size:0.9em; margin-bottom:8px; margin-top:8px;"><em>Context:</em> ${escapeHtml(item.context || "")}</div>
<div style="font-size:0.9em; margin-bottom:8px;"><em>Decision:</em> ${escapeHtml(item.decision || "")}</div>
<div style="font-size:0.9em; margin-bottom:8px;"><em>Consequence:</em> ${escapeHtml(item.consequence || "")}</div>
${item.supersedes ? `<div style="font-size:0.9em; font-weight:bold; color:var(--warning-color);">Supersedes: ${escapeHtml(item.supersedes)}</div>` : ""}
`;
}, {
itemNoun: "ADRs",
cardBorderColor: "#3498db",
defaultSort: "desc",
sortExtractor: (item) => {
const match = String(item.id || "").match(/\d+/);
if (match)
return parseInt(match[0], 10);
return item.timestamp || 0;
},
searchFields: (item) => [
item.id,
item.title,
item.context,
item.decision,
item.consequence,
item.status,
item.git_branch,
item.task_id
],
filters: [
{
id: "status",
label: "Status",
options: [
{ label: "All Statuses", value: "" },
{ label: "Accepted", value: "accepted" },
{ label: "Implemented", value: "implemented" },
{ label: "Proposed", value: "proposed" },
{ label: "Draft", value: "draft" },
{ label: "Superseded", value: "superseded" }
],
predicate: (item, val) => String(item.status || "").toLowerCase() === val.toLowerCase()
}
]
});
}
function loadMemos() {
loadGenericList("/api/handoff_memos", "memos-container", (item) => {
const blockers = Array.isArray(item.blockers) && item.blockers.length > 0 ? item.blockers : [];
const actionItems = Array.isArray(item.action_items) && item.action_items.length > 0 ? item.action_items : [];
return `
<div style="display:flex; justify-content:space-between; align-items:center; gap:8px;">
<strong style="color:var(--text-primary); font-size:1em;">\uD83D\uDC64 Memo from ${escapeHtml(item.author || "System")}</strong>
<span style="font-size:0.75em; color:var(--text-secondary); background:var(--canvas-bg); padding:2px 6px; border-radius:4px; border:1px solid var(--border-color);">${escapeHtml(item.namespace || "global")}</span>
</div>
${item.git_branch || item.vcs_revision || item.session_id ? `
<div style="font-size:0.8em; color:var(--text-secondary); margin-top:3px; font-family:monospace; display:flex; gap:10px; flex-wrap:wrap;">
${item.git_branch ? `<span>\uD83C\uDF3F ${escapeHtml(item.git_branch)}</span>` : ""}
${item.vcs_revision ? `<span>Rev: ${escapeHtml(item.vcs_revision.substring(0, 8))}</span>` : ""}
${item.session_id ? `<span>Session: ${escapeHtml(item.session_id)}</span>` : ""}
</div>
` : ""}
<div style="white-space:pre-wrap; font-size:0.9em; line-height:1.4; margin-top:8px;">${escapeHtml(item.content || item.summary || "")}</div>
${blockers.length > 0 ? `
<div style="margin-top:8px; background:rgba(231,76,60,0.1); border:1px solid rgba(231,76,60,0.3); border-radius:4px; padding:6px 10px;">
<div style="color:var(--error-color); font-weight:bold; font-size:0.85em;">\uD83D\uDEAB Blockers:</div>
<ul style="margin:4px 0 0 16px; padding:0; font-size:0.85em; color:var(--text-primary);">
${blockers.map((b) => `<li>${escapeHtml(b)}</li>`).join("")}
</ul>
</div>
` : ""}
${actionItems.length > 0 ? `
<div style="margin-top:8px; background:rgba(52,152,219,0.1); border:1px solid rgba(52,152,219,0.3); border-radius:4px; padding:6px 10px;">
<div style="color:var(--accent-color); font-weight:bold; font-size:0.85em;">\uD83D\uDCCB Action Items:</div>
<ul style="margin:4px 0 0 16px; padding:0; font-size:0.85em; color:var(--text-primary);">
${actionItems.map((a) => `<li>${escapeHtml(a)}</li>`).join("")}
</ul>
</div>
` : ""}
`;
}, {
itemNoun: "Memos",
cardBorderColor: "#9b59b6",
defaultSort: "desc",
timestampExtractor: (item) => item.timestamp,
searchFields: (item) => [
item.author,
item.content,
item.summary,
item.namespace,
item.git_branch
]
});
loadGenericList("/api/session_summaries", "summaries-container", (item) => `
<div style="display:flex; justify-content:space-between; align-items:center;">
<strong>Session ${item.session_id ? escapeHtml(item.session_id) : "Summary"}</strong>
<span style="font-size:0.75em; color:var(--text-secondary); background:var(--canvas-bg); padding:2px 6px; border-radius:4px; border:1px solid var(--border-color);">${escapeHtml(item.namespace || "global")}</span>
</div>
${item.git_commit || item.git_branch ? `<div style="font-size:0.8em; color:var(--text-secondary); margin-top:3px; font-family:monospace;">${escapeHtml(item.git_branch || "")} @ ${escapeHtml((item.git_commit || "").substring(0, 8))}</div>` : ""}
<div style="white-space:pre-wrap; font-size:0.9em; line-height:1.4; margin-top:8px;">${escapeHtml(item.summary || "")}</div>
${item.tasks_completed && item.tasks_completed.length > 0 ? `<div style="margin-top:8px; font-size:0.85em;"><strong>Tasks Completed:</strong><ul style="margin:4px 0 0 16px; padding:0;">${item.tasks_completed.map((t) => `<li>${escapeHtml(t)}</li>`).join("")}</ul></div>` : ""}
${item.next_steps && item.next_steps.length > 0 ? `<div style="margin-top:6px; font-size:0.85em;"><strong>Next Steps:</strong><ul style="margin:4px 0 0 16px; padding:0;">${item.next_steps.map((s) => `<li>${escapeHtml(s)}</li>`).join("")}</ul></div>` : ""}
`, {
itemNoun: "Summaries",
cardBorderColor: "#34495e",
defaultSort: "desc",
timestampExtractor: (item) => item.timestamp,
searchFields: (item) => [
item.session_id,
item.summary,
item.namespace,
item.git_branch
]
});
loadGenericList("/api/milestones", "milestones-container", (item) => {
const statusLower = (item.status || "pending").toLowerCase();
let statusBg = "rgba(52, 152, 219, 0.15)";
let statusColor = "var(--accent-color)";
let statusLabel = item.status || "Pending";
if (statusLower === "completed" || statusLower === "done") {
statusBg = "rgba(46, 204, 113, 0.15)";
statusColor = "var(--success-color)";
statusLabel = "✓ Completed";
} else if (statusLower === "in_progress" || statusLower === "active") {
statusBg = "rgba(230, 126, 34, 0.15)";
statusColor = "var(--open-color)";
statusLabel = "● In Progress";
}
const targetDate = item.target_date ? new Date(item.target_date * 1000).toLocaleDateString() : "";
const completedDate = item.completed_at ? new Date(item.completed_at * 1000).toLocaleDateString() : "";
const deliverables = Array.isArray(item.deliverables) && item.deliverables.length > 0 ? item.deliverables : [];
const taskIds = Array.isArray(item.task_ids) && item.task_ids.length > 0 ? item.task_ids : [];
return `
<div style="display:flex; justify-content:space-between; align-items:center; gap:8px;">
<strong style="font-size:1.05em; color:var(--text-primary);">${escapeHtml(item.title || item.name || "Milestone")}</strong>
<span style="background:${statusBg}; color:${statusColor}; border:1px solid ${statusColor}; font-size:0.75em; font-weight:bold; padding:2px 8px; border-radius:4px;">${escapeHtml(statusLabel)}</span>
</div>
${targetDate || completedDate ? `
<div style="font-size:0.8em; color:var(--text-secondary); margin-top:3px; display:flex; gap:10px;">
${targetDate ? `<span>\uD83C\uDFAF Target: ${escapeHtml(targetDate)}</span>` : ""}
${completedDate ? `<span>✓ Finished: ${escapeHtml(completedDate)}</span>` : ""}
</div>
` : ""}
<div style="margin-top:6px; font-size:0.9em; line-height:1.4; color:var(--text-primary);">${escapeHtml(item.description || "")}</div>
${deliverables.length > 0 ? `
<div style="margin-top:8px; font-size:0.85em;">
<strong>Deliverables:</strong>
<ul style="margin:4px 0 0 16px; padding:0;">
${deliverables.map((d) => `<li>${escapeHtml(d)}</li>`).join("")}
</ul>
</div>
` : ""}
${taskIds.length > 0 ? `
<div style="margin-top:8px; display:flex; gap:6px; flex-wrap:wrap; align-items:center;">
<span style="font-size:0.8em; color:var(--text-secondary);">Linked Tasks:</span>
${taskIds.map((tid) => `<span style="background:var(--canvas-bg); padding:1px 6px; border-radius:3px; font-size:0.75em; font-family:monospace; border:1px solid var(--border-color);">${escapeHtml(tid)}</span>`).join("")}
</div>
` : ""}
`;
}, {
itemNoun: "Milestones",
cardBorderColor: "#f39c12",
defaultSort: "desc",
timestampExtractor: (item) => item.timestamp || item.target_date,
searchFields: (item) => [
item.title,
item.name,
item.description,
item.status
],
filters: [
{
id: "status",
label: "Status",
options: [
{ label: "All Statuses", value: "" },
{ label: "Pending", value: "pending" },
{ label: "In Progress", value: "in_progress" },
{ label: "Completed", value: "completed" }
],
predicate: (item, val) => String(item.status || "pending").toLowerCase() === val.toLowerCase()
}
]
});
}
function loadSnippets() {
loadGenericList("/api/snippets", "snippets-container", (item) => {
const code = item.code || item.content || "";
const tags = Array.isArray(item.tags) ? item.tags : [];
const origin = item.origin_file ? `${item.origin_file}${item.line_range ? `:${item.line_range}` : ""}` : "";
return `
<div style="display:flex; justify-content:space-between; align-items:flex-start; gap:8px;">
<div>
<strong style="font-size:1.05em; color:var(--text-primary);">${escapeHtml(item.name || item.description || "Snippet")}</strong>
${item.language ? `<span style="background:var(--canvas-bg); color:var(--accent-color); border:1px solid var(--border-color); font-size:0.75em; font-weight:bold; padding:2px 6px; border-radius:4px; margin-left:6px; font-family:monospace;">${escapeHtml(item.language)}</span>` : ""}
</div>
<button class="complete-btn" style="position:static;" onclick='navigator.clipboard.writeText(${JSON.stringify(code)}).then(() => { this.innerText="✓ Copied"; setTimeout(() => { this.innerText="\uD83D\uDCCB Copy"; }, 1500); })' title="Copy to Clipboard">\uD83D\uDCCB Copy</button>
</div>
${item.description && item.description !== item.name ? `<div style="margin-top:4px; font-size:0.85em; color:var(--text-secondary);">${escapeHtml(item.description)}</div>` : ""}
${origin ? `<div style="margin-top:6px; font-family:monospace; font-size:0.8em; color:var(--accent-color);">\uD83D\uDCC4 ${escapeHtml(origin)}</div>` : ""}
<pre style="background:#111417; color:#d1d8e0; padding:10px; border-radius:4px; overflow-x:auto; font-size:0.85em; margin-top:8px; border:1px solid var(--border-color);">${escapeHtml(code)}</pre>
<div style="margin-top:8px; font-size:0.8em; display:flex; gap:6px; flex-wrap:wrap; align-items:center;">
${item.repo_name ? `<span style="background:var(--canvas-bg); padding:2px 6px; border-radius:4px; border:1px solid var(--border-color);">\uD83D\uDCE6 ${escapeHtml(item.repo_name)}</span>` : ""}
${tags.map((t) => `<span style="background:var(--card-bg); padding:1px 6px; border-radius:3px; font-size:0.75em; border:1px solid var(--border-color); color:var(--text-secondary);">#${escapeHtml(t)}</span>`).join(" ")}
</div>
`;
}, {
itemNoun: "Snippets",
cardBorderColor: "#27ae60",
defaultSort: "desc",
timestampExtractor: (item) => item.timestamp,
searchFields: (item) => [
item.name,
item.description,
item.code,
item.language,
item.origin_file,
Array.isArray(item.tags) ? item.tags.join(" ") : ""
],
filters: [
{
id: "language",
label: "Language",
options: [
{ label: "All Languages", value: "" },
{ label: "Rust", value: "rust" },
{ label: "TypeScript / JS", value: "typescript" },
{ label: "Python", value: "python" },
{ label: "SQL", value: "sql" },
{ label: "Shell / Bash", value: "bash" },
{ label: "Markdown / Text", value: "markdown" }
],
predicate: (item, val) => {
const lang = String(item.language || "").toLowerCase();
if (val === "typescript")
return lang === "typescript" || lang === "javascript" || lang === "ts" || lang === "js";
if (val === "bash")
return lang === "bash" || lang === "sh" || lang === "pwsh" || lang === "powershell";
return lang === val.toLowerCase();
}
}
]
});
}
function loadEnvironment() {
loadGenericList("/api/environments", "environments-container", (item) => {
const isVpn = item.requires_vpn;
const envType = (item.env_type || "dev").toLowerCase();
let typeColor = "#3498db";
if (envType === "prod")
typeColor = "#e74c3c";
else if (envType === "staging")
typeColor = "#f39c12";
else if (envType === "qa")
typeColor = "#9b59b6";
return `
<div style="display:flex; justify-content:space-between; align-items:center; gap:8px;">
<div>
<strong style="font-size:1.05em; color:var(--text-primary);">${escapeHtml(item.name || "Environment")}</strong>
<span style="background:${typeColor}; color:#fff; font-size:0.75em; font-weight:bold; padding:2px 6px; border-radius:4px; margin-left:6px; text-transform:uppercase;">${escapeHtml(envType)}</span>
${isVpn ? '<span style="background:#e67e22; color:#fff; font-size:0.75em; font-weight:bold; padding:2px 6px; border-radius:4px; margin-left:6px;">\uD83D\uDD12 VPN Required</span>' : ""}
</div>
<span style="font-size:0.75em; color:var(--text-secondary); background:var(--canvas-bg); padding:2px 6px; border-radius:4px; border:1px solid var(--border-color);">${escapeHtml(item.namespace || "default")}</span>
</div>
${item.url ? `<div style="margin-top:6px; font-family:monospace; font-size:0.85em;"><a href="${escapeHtml(item.url)}" target="_blank" rel="noopener noreferrer" style="color:var(--accent-color); text-decoration:none;">\uD83C\uDF10 ${escapeHtml(item.url)}</a></div>` : ""}
${item.description ? `<div style="margin-top:4px; font-size:0.9em; color:var(--text-primary);">${escapeHtml(item.description)}</div>` : ""}
<div style="margin-top:8px; font-size:0.8em; display:flex; gap:8px; flex-wrap:wrap; align-items:center;">
${item.ssh_host ? `<span style="background:var(--canvas-bg); padding:2px 6px; border-radius:4px; border:1px solid var(--border-color); font-family:monospace;">SSH: ${escapeHtml(item.ssh_host)}</span>` : ""}
${item.healthcheck_endpoint ? `<span style="background:var(--canvas-bg); padding:2px 6px; border-radius:4px; border:1px solid var(--border-color); font-family:monospace;">Health: ${escapeHtml(item.healthcheck_endpoint)}</span>` : ""}
${item.repo_name ? `<span style="background:var(--canvas-bg); padding:2px 6px; border-radius:4px; border:1px solid var(--border-color);">\uD83D\uDCE6 ${escapeHtml(item.repo_name)}</span>` : ""}
</div>
`;
}, {
itemNoun: "Environments",
cardBorderColor: "#3498db",
defaultSort: "desc",
timestampExtractor: (item) => item.updated_at,
searchFields: (item) => [item.name, item.url, item.description, item.env_type, item.ssh_host, item.namespace]
});
loadGenericList("/api/env_fingerprints", "fingerprints-container", (item) => {
const toolVersions = item.tool_versions || {};
const tools = Object.entries(toolVersions).map(([k, v]) => `<span style="background:var(--canvas-bg); padding:2px 8px; border-radius:4px; font-size:0.8em; font-family:monospace; border:1px solid var(--border-color);"><strong>${escapeHtml(k)}:</strong> ${escapeHtml(String(v))}</span>`).join(" ");
return `
<div style="display:flex; justify-content:space-between; align-items:center; gap:8px;">
<div>
<strong style="font-size:1.05em; color:var(--text-primary);">\uD83D\uDCBB ${escapeHtml(item.os || "Unknown OS")} (${escapeHtml(item.shell || "shell")})</strong>
</div>
<span style="font-size:0.75em; color:var(--text-secondary); background:var(--canvas-bg); padding:2px 6px; border-radius:4px; border:1px solid var(--border-color);">${escapeHtml(item.namespace || "default")}</span>
</div>
${tools ? `<div style="margin-top:10px; display:flex; gap:6px; flex-wrap:wrap; align-items:center;">${tools}</div>` : '<div style="margin-top:6px; font-size:0.85em; color:var(--text-secondary);">No tool versions registered</div>'}
<div style="margin-top:8px; font-size:0.8em; color:var(--text-secondary);">
${item.repo_name ? `<span style="background:var(--canvas-bg); padding:2px 6px; border-radius:4px; border:1px solid var(--border-color); margin-right:8px;">\uD83D\uDCE6 ${escapeHtml(item.repo_name)}</span>` : ""}
</div>
`;
}, {
itemNoun: "Fingerprints",
cardBorderColor: "#1abc9c",
defaultSort: "desc",
timestampExtractor: (item) => item.updated_at,
searchFields: (item) => [item.namespace, item.os, item.shell, item.repo_name, JSON.stringify(item.tool_versions || {})]
});
loadGenericList("/api/env_requirements", "requirements-container", (item) => {
const isSecret = item.is_secret;
return `
<div style="display:flex; justify-content:space-between; align-items:center; gap:8px;">
<div>
<span style="font-family:monospace; font-size:1.05em; font-weight:bold; color:var(--text-primary);">${escapeHtml(item.key)}</span>
${isSecret ? '<span style="background:#e74c3c; color:#fff; font-size:0.75em; font-weight:bold; padding:2px 6px; border-radius:4px; margin-left:6px;">\uD83D\uDD12 Secret</span>' : '<span style="background:var(--canvas-bg); color:var(--text-secondary); border:1px solid var(--border-color); font-size:0.75em; padding:2px 6px; border-radius:4px; margin-left:6px;">Public</span>'}
</div>
<span style="font-size:0.75em; color:var(--text-secondary); background:var(--canvas-bg); padding:2px 6px; border-radius:4px; border:1px solid var(--border-color);">${escapeHtml(item.namespace || "default")}</span>
</div>
${item.description ? `<div style="margin-top:6px; font-size:0.9em; color:var(--text-primary);">${escapeHtml(item.description)}</div>` : ""}
<div style="margin-top:8px; font-size:0.8em; display:flex; gap:8px; flex-wrap:wrap; align-items:center;">
${item.default_value ? `<span style="background:var(--canvas-bg); padding:2px 6px; border-radius:4px; border:1px solid var(--border-color); font-family:monospace;">Default: ${escapeHtml(item.default_value)}</span>` : ""}
${item.validation_regex ? `<span style="background:var(--canvas-bg); padding:2px 6px; border-radius:4px; border:1px solid var(--border-color); font-family:monospace;">Regex: ${escapeHtml(item.validation_regex)}</span>` : ""}
${item.repo_name ? `<span style="background:var(--canvas-bg); padding:2px 6px; border-radius:4px; border:1px solid var(--border-color);">\uD83D\uDCE6 ${escapeHtml(item.repo_name)}</span>` : ""}
</div>
`;
}, {
itemNoun: "Requirements",
cardBorderColor: "#e67e22",
defaultSort: "desc",
searchFields: (item) => [item.key, item.description, item.default_value, item.namespace, item.repo_name]
});
loadGenericList("/api/gates", "gates-container", (item) => {
const status = (item.status || "pending").toLowerCase();
let statusColor = "var(--accent-color)";
if (status === "authorized")
statusColor = "var(--success-color)";
else if (status === "blocked")
statusColor = "var(--error-color)";
return `
<div style="display:flex; justify-content:space-between; align-items:center; gap:8px;">
<div>
<strong style="font-size:1.05em; color:var(--text-primary);">${escapeHtml(item.action)} &rarr; ${escapeHtml(item.target)}</strong>
<span style="background:${statusColor}; color:#fff; font-size:0.75em; font-weight:bold; padding:2px 6px; border-radius:4px; margin-left:6px; text-transform:uppercase;">${escapeHtml(status)}</span>
</div>
<div style="display:flex; align-items:center; gap:6px;">
<span style="font-size:0.75em; color:var(--text-secondary); background:var(--canvas-bg); padding:2px 6px; border-radius:4px; border:1px solid var(--border-color);">${escapeHtml(item.namespace || "global")}</span>
${status !== "authorized" ? `<button class="tab-button" style="padding:2px 8px; font-size:0.75em; background:rgba(46,204,113,0.15); color:var(--success-color); border:1px solid var(--success-color); cursor:pointer;" onclick="authorizeGate('${escapeHtml(item.id)}')" title="Authorize Gate">✓ Authorize</button>` : ""}
${status !== "blocked" ? `<button class="tab-button" style="padding:2px 8px; font-size:0.75em; background:rgba(231,76,60,0.15); color:var(--error-color); border:1px solid var(--error-color); cursor:pointer;" onclick="blockGate('${escapeHtml(item.id)}')" title="Block Gate">\uD83D\uDEAB Block</button>` : ""}
<button class="tab-button" style="padding:2px 6px; font-size:0.75em; background:var(--canvas-bg); color:var(--text-secondary); border:1px solid var(--border-color); cursor:pointer;" onclick="deleteGate('${escapeHtml(item.id)}')" title="Delete Gate">✕</button>
</div>
</div>
${item.reason ? `<div style="margin-top:6px; font-size:0.9em; color:var(--text-primary);">${escapeHtml(item.reason)}</div>` : ""}
<div style="margin-top:8px; font-size:0.8em; display:flex; gap:8px; flex-wrap:wrap; align-items:center;">
${item.gate_type ? `<span style="background:var(--canvas-bg); padding:2px 6px; border-radius:4px; border:1px solid var(--border-color);">Type: ${escapeHtml(item.gate_type)}</span>` : ""}
${item.enforcer ? `<span style="background:var(--canvas-bg); padding:2px 6px; border-radius:4px; border:1px solid var(--border-color);">Enforcer: ${escapeHtml(item.enforcer)}</span>` : ""}
${item.repo_name ? `<span style="background:var(--canvas-bg); padding:2px 6px; border-radius:4px; border:1px solid var(--border-color);">\uD83D\uDCE6 ${escapeHtml(item.repo_name)}</span>` : ""}
</div>
`;
}, {
itemNoun: "Gates",
cardBorderColor: "#e74c3c",
defaultSort: "desc",
timestampExtractor: (item) => item.timestamp,
searchFields: (item) => [item.action, item.target, item.status, item.reason, item.enforcer]
});
}
function loadAllExtras() {
loadTerminal();
loadTechDebt();
loadADRs();
loadMemos();
loadSnippets();
loadLedger();
}
async function testClipboard() {
const modal = document.getElementById("clipboard-modal");
const resultDiv = document.getElementById("clipboard-result");
if (modal)
modal.style.display = "flex";
if (resultDiv)
resultDiv.innerHTML = '<p id="clipboard-loading" style="color:var(--text-secondary);">Analyzing your clipboard...</p>';
try {
const res = await fetch("/api/clipboard/capture", { method: "POST" });
const data = await res.json();
if (resultDiv) {
if (data.success) {
resultDiv.innerHTML = `<img src="${data.image}" style="max-width:100%; max-height:60vh; border-radius:4px; border:1px solid var(--border-color); box-shadow: 0 5px 15px rgba(0,0,0,0.3);" />`;
} else {
resultDiv.innerHTML = `<p style="color:var(--error-color); font-weight:bold;">Failed to read clipboard: ${data.error}</p>`;
}
}
} catch (err) {
if (resultDiv) {
resultDiv.innerHTML = `<p style="color:var(--error-color); font-weight:bold;">Error calling endpoint: ${err.message}</p>`;
}
}
}
async function loadVersion() {
try {
const res = await fetch("/api/version");
const data = await res.json();
const verEl = document.getElementById("app-version");
if (verEl) {
verEl.innerHTML = `<span style="background:var(--card-bg); border:1px solid var(--border-color); color:var(--text-secondary); padding:3px 10px; border-radius:12px; font-size:0.85em; font-weight:600; font-family:monospace;">v${data.version}</span>`;
}
} catch (err) {
console.error("Failed to load version", err);
}
}
function setupSSE() {
if (wsConnected || activeSSE)
return;
try {
activeSSE = new EventSource("/api/activity/stream");
activeSSE.onmessage = function(event) {
if (event.data && !wsConnected) {
try {
const parsed = JSON.parse(event.data);
handleIncomingActivity(parsed);
} catch (e) {
handleIncomingActivity(event.data);
}
}
};
activeSSE.onerror = function() {
if (activeSSE) {
activeSSE.close();
activeSSE = null;
}
};
} catch (e) {
console.error("SSE initialization error", e);
}
}
async function resolveTechDebt(id) {
try {
if (activeWS && activeWS.readyState === WebSocket.OPEN) {
await sendWsRpc("tech_debts/resolve", { id });
} else {
const res = await fetch(`/api/tech_debts/${encodeURIComponent(id)}/resolve`, { method: "POST" });
if (!res.ok)
console.error("Failed to resolve tech debt", await res.text());
}
loadTechDebt();
} catch (err) {
console.error("Error resolving tech debt via WS, trying HTTP fallback", err);
try {
const res = await fetch(`/api/tech_debts/${encodeURIComponent(id)}/resolve`, { method: "POST" });
if (!res.ok)
console.error("Failed to resolve tech debt", await res.text());
loadTechDebt();
} catch (fallbackErr) {
console.error("Error resolving tech debt", fallbackErr);
}
}
}
async function authorizeGate(id) {
try {
if (activeWS && activeWS.readyState === WebSocket.OPEN) {
await sendWsRpc("gates/authorize", { id });
} else {
const res = await fetch(`/api/gates/${encodeURIComponent(id)}/authorize`, { method: "POST" });
if (!res.ok)
console.error("Failed to authorize gate", await res.text());
}
loadEnvironment();
} catch (err) {
console.error("Error authorizing gate via WS, trying HTTP fallback", err);
try {
const res = await fetch(`/api/gates/${encodeURIComponent(id)}/authorize`, { method: "POST" });
if (!res.ok)
console.error("Failed to authorize gate", await res.text());
loadEnvironment();
} catch (fallbackErr) {
console.error("Error authorizing gate", fallbackErr);
}
}
}
async function blockGate(id) {
try {
if (activeWS && activeWS.readyState === WebSocket.OPEN) {
await sendWsRpc("gates/block", { id });
} else {
const res = await fetch(`/api/gates/${encodeURIComponent(id)}/block`, { method: "POST" });
if (!res.ok)
console.error("Failed to block gate", await res.text());
}
loadEnvironment();
} catch (err) {
console.error("Error blocking gate via WS, trying HTTP fallback", err);
try {
const res = await fetch(`/api/gates/${encodeURIComponent(id)}/block`, { method: "POST" });
if (!res.ok)
console.error("Failed to block gate", await res.text());
loadEnvironment();
} catch (fallbackErr) {
console.error("Error blocking gate", fallbackErr);
}
}
}
async function deleteGate(id) {
try {
if (activeWS && activeWS.readyState === WebSocket.OPEN) {
await sendWsRpc("gates/delete", { id });
} else {
const res = await fetch(`/api/gates/${encodeURIComponent(id)}`, { method: "DELETE" });
if (!res.ok)
console.error("Failed to delete gate", await res.text());
}
loadEnvironment();
} catch (err) {
console.error("Error deleting gate via WS, trying HTTP fallback", err);
try {
const res = await fetch(`/api/gates/${encodeURIComponent(id)}`, { method: "DELETE" });
if (!res.ok)
console.error("Failed to delete gate", await res.text());
loadEnvironment();
} catch (fallbackErr) {
console.error("Error deleting gate", fallbackErr);
}
}
}
var paletteItems = [];
var filteredPaletteItems = [];
var paletteSelectedIndex = 0;
var paletteDataLoaded = false;
async function preloadPaletteData() {
try {
const [tasksRes, debtsRes, adrsRes, envsRes, gatesRes] = await Promise.all([
fetch("/api/tasks", { cache: "no-store" }).then((r) => r.json()).catch(() => []),
fetch("/api/tech_debts", { cache: "no-store" }).then((r) => r.json()).catch(() => []),
fetch("/api/adrs", { cache: "no-store" }).then((r) => r.json()).catch(() => []),
fetch("/api/environments", { cache: "no-store" }).then((r) => r.json()).catch(() => []),
fetch("/api/gates", { cache: "no-store" }).then((r) => r.json()).catch(() => [])
]);
const items = [
{ id: "nav-activity", category: "nav", icon: "⚡", title: "Live Activity Feed", sub: "Inspect real-time agent signals & SSE", badge: "VIEW", tabId: "activity-tab" },
{ id: "nav-ledger", category: "nav", icon: "\uD83D\uDCD6", title: "Code Ledger", sub: "View logged code changes & commits", badge: "VIEW", tabId: "ledger-tab" },
{ id: "nav-graph", category: "nav", icon: "\uD83D\uDD78️", title: "Knowledge Graph Explorer", sub: "Explore nodes, relationships & dependencies", badge: "VIEW", tabId: "graph-tab" },
{ id: "nav-search", category: "nav", icon: "\uD83D\uDD0D", title: "Omni-Search", sub: "Full-text cross-entity search", badge: "VIEW", tabId: "search-tab" },
{ id: "nav-tasks", category: "nav", icon: "\uD83D\uDCCB", title: "Task Board", sub: "HTN / DAG task hierarchies", badge: "VIEW", tabId: "task-tab" },
{ id: "nav-techdebt", category: "nav", icon: "⚡", title: "Tech Debt & Error Fixes", sub: "Known tech debt and error fixes", badge: "VIEW", tabId: "techdebt-tab" },
{ id: "nav-adrs", category: "nav", icon: "\uD83D\uDCDC", title: "Architectural Decision Records", sub: "ADRs and design choices", badge: "VIEW", tabId: "adrs-tab" },
{ id: "nav-memos", category: "nav", icon: "\uD83D\uDCDD", title: "Handoff Memos", sub: "Agent handoffs & session summaries", badge: "VIEW", tabId: "memos-tab" },
{ id: "nav-snippets", category: "nav", icon: "\uD83D\uDCCE", title: "Code Snippets", sub: "Indexed snippets & patterns", badge: "VIEW", tabId: "snippets-tab" },
{ id: "nav-terminal", category: "nav", icon: "\uD83D\uDCBB", title: "Terminal History", sub: "Command execution log & exit codes", badge: "VIEW", tabId: "terminal-tab" },
{ id: "nav-env", category: "nav", icon: "\uD83C\uDF10", title: "Environment & Quality Gates", sub: "Deploy targets, fingerprints, requirements & gates", badge: "VIEW", tabId: "env-tab" }
];
if (Array.isArray(tasksRes)) {
tasksRes.forEach((t) => {
items.push({
id: t.id,
category: "task",
icon: t.status === "completed" ? "✓" : "\uD83D\uDCCB",
title: t.title || t.id,
sub: t.description || `Task (${t.status || "pending"})`,
badge: t.status || "TASK",
tabId: "task-tab"
});
});
}
if (Array.isArray(debtsRes)) {
debtsRes.forEach((d) => {
items.push({
id: d.id,
category: "techdebt",
icon: "⚡",
title: `${d.id}: ${d.description || ""}`.trim(),
sub: d.file_path ? `${d.file_path} - Severity: ${d.severity || "medium"}` : `Severity: ${d.severity || "medium"}`,
badge: d.is_resolved ? "RESOLVED" : d.severity || "DEBT",
tabId: "techdebt-tab"
});
});
}
if (Array.isArray(adrsRes)) {
adrsRes.forEach((a) => {
items.push({
id: a.id,
category: "adr",
icon: "\uD83D\uDCDC",
title: `${a.id}: ${a.title || ""}`.trim(),
sub: a.decision || `Status: ${a.status || "accepted"}`,
badge: a.status || "ADR",
tabId: "adrs-tab"
});
});
}
if (Array.isArray(envsRes)) {
envsRes.forEach((e) => {
items.push({
id: e.name || e.url,
category: "env",
icon: "\uD83C\uDF10",
title: e.name || "Target Environment",
sub: `${e.url || ""} ${e.description ? "• " + e.description : ""}`.trim(),
badge: e.env_type || "ENV",
tabId: "env-tab"
});
});
}
if (Array.isArray(gatesRes)) {
gatesRes.forEach((g) => {
items.push({
id: g.id,
category: "gate",
icon: g.status === "authorized" ? "\uD83D\uDFE2" : g.status === "blocked" ? "\uD83D\uDD34" : "\uD83D\uDFE1",
title: `${g.action} → ${g.target}`,
sub: g.reason || `Enforcer: ${g.enforcer || "gate"}`,
badge: g.status || "GATE",
tabId: "env-tab"
});
});
}
paletteItems = items;
paletteDataLoaded = true;
} catch (err) {
console.error("Failed to preload palette data", err);
}
}
function openCommandPalette() {
const backdrop = document.getElementById("cmd-palette-backdrop");
const input = document.getElementById("palette-input");
if (!backdrop || !input)
return;
backdrop.style.display = "flex";
input.value = "";
paletteSelectedIndex = 0;
preloadPaletteData().then(() => {
onPaletteSearch("");
input.focus();
});
}
function closeCommandPalette() {
const backdrop = document.getElementById("cmd-palette-backdrop");
if (backdrop)
backdrop.style.display = "none";
}
function onPaletteBackdropClick(e) {
if (e.target === document.getElementById("cmd-palette-backdrop")) {
closeCommandPalette();
}
}
function onPaletteSearch(query) {
const q = query.toLowerCase().trim();
if (!q) {
filteredPaletteItems = paletteItems.slice(0, 30);
} else {
filteredPaletteItems = paletteItems.filter((item) => item.title.toLowerCase().includes(q) || item.sub.toLowerCase().includes(q) || item.badge.toLowerCase().includes(q) || item.category.toLowerCase().includes(q)).slice(0, 40);
}
paletteSelectedIndex = 0;
renderPaletteResults();
}
function renderPaletteResults() {
const container = document.getElementById("palette-results");
const countEl = document.getElementById("palette-count");
if (!container)
return;
if (countEl)
countEl.innerText = `${filteredPaletteItems.length} results`;
if (filteredPaletteItems.length === 0) {
container.innerHTML = '<li class="enterprise-empty-state" style="margin: 10px 18px;">No matching items found.</li>';
return;
}
container.innerHTML = filteredPaletteItems.map((item, idx) => {
const isSel = idx === paletteSelectedIndex;
return `
<li class="palette-item ${isSel ? "selected" : ""}" data-index="${idx}" onclick="selectPaletteItem(${idx})">
<div class="palette-item-left">
<span class="palette-item-icon">${escapeHtml(item.icon)}</span>
<div class="palette-item-text">
<span class="palette-item-title">${escapeHtml(item.title)}</span>
<span class="palette-item-sub">${escapeHtml(item.sub)}</span>
</div>
</div>
<span class="palette-item-badge">${escapeHtml(item.badge)}</span>
</li>
`;
}).join("");
const selectedEl = container.querySelector(`.palette-item[data-index="${paletteSelectedIndex}"]`);
if (selectedEl) {
selectedEl.scrollIntoView({ block: "nearest" });
}
}
function selectPaletteItem(idx) {
const item = filteredPaletteItems[idx];
if (!item)
return;
closeCommandPalette();
const tabBtn = document.querySelector(`button[onclick*="${item.tabId}"]`);
if (tabBtn) {
switchTab(item.tabId, tabBtn);
}
}
function onPaletteKeyDown(e) {
if (e.key === "ArrowDown") {
e.preventDefault();
if (filteredPaletteItems.length > 0) {
paletteSelectedIndex = (paletteSelectedIndex + 1) % filteredPaletteItems.length;
renderPaletteResults();
}
} else if (e.key === "ArrowUp") {
e.preventDefault();
if (filteredPaletteItems.length > 0) {
paletteSelectedIndex = (paletteSelectedIndex - 1 + filteredPaletteItems.length) % filteredPaletteItems.length;
renderPaletteResults();
}
} else if (e.key === "Enter") {
e.preventDefault();
selectPaletteItem(paletteSelectedIndex);
} else if (e.key === "Escape") {
e.preventDefault();
closeCommandPalette();
}
}
document.addEventListener("keydown", function(e) {
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "k") {
e.preventDefault();
const backdrop = document.getElementById("cmd-palette-backdrop");
if (backdrop && backdrop.style.display === "flex") {
closeCommandPalette();
} else {
openCommandPalette();
}
return;
}
if (e.key === "Escape") {
const backdrop = document.getElementById("cmd-palette-backdrop");
if (backdrop && backdrop.style.display === "flex") {
closeCommandPalette();
return;
}
}
if (e.key === "/" && document.activeElement?.tagName !== "INPUT" && document.activeElement?.tagName !== "TEXTAREA") {
const backdrop = document.getElementById("cmd-palette-backdrop");
if (backdrop && backdrop.style.display === "flex")
return;
const activeSearch = document.querySelector(".tab-content.active .enterprise-search-input, .tab-content.active #task-filter-input");
if (activeSearch) {
e.preventDefault();
activeSearch.focus();
activeSearch.select();
}
}
});
function refreshActiveTab() {
switch (currentTabId) {
case "graph-tab":
loadGraph();
break;
case "task-tab":
loadTasks();
break;
case "techdebt-tab":
loadTechDebt();
break;
case "adrs-tab":
loadADRs();
break;
case "memos-tab":
loadMemos();
break;
case "snippets-tab":
loadSnippets();
break;
case "terminal-tab":
loadTerminal();
break;
case "ledger-tab":
loadLedger();
break;
case "env-tab":
loadEnvironment();
break;
case "activity-tab":
loadActivityHistory();
break;
}
}
Object.assign(window, {
switchTab,
toggleTheme,
getColorForType,
zoomGraph,
resetGraph,
closeInspector,
showInspector,
toggleFilter,
renderFilters,
updateGraphData,
loadGraph,
completeTask,
buildTaskTreeHTML,
loadTasks,
handleTaskFilter,
handleTaskStatusFilter,
highlightText,
handleSearch,
escapeHtml,
parseActivityPayload,
loadActivityHistory,
requestDomainRefresh,
handleIncomingActivity,
setupWS,
sendWsRpc,
loadGenericList,
onListSearchInput,
onListClearSearch,
onListFilterChange,
onListToggleSort,
onListPageSizeChange,
onListPageChange,
onListResetFilters,
loadTerminal,
loadLedger,
loadTechDebt,
loadADRs,
loadMemos,
loadSnippets,
loadAllExtras,
loadEnvironment,
resolveTechDebt,
authorizeGate,
blockGate,
deleteGate,
openCommandPalette,
closeCommandPalette,
onPaletteBackdropClick,
onPaletteSearch,
onPaletteKeyDown,
selectPaletteItem,
testClipboard,
loadVersion,
setupSSE,
refreshActiveTab
});
loadVersion();
loadActivityHistory();
setupWS();
var observer = new MutationObserver(() => updateGraphData());
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ["data-theme"]
});