Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 122 additions & 15 deletions VS Code/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -790,6 +790,44 @@ export class CodeMindMapPanel {
content: '✓';
color: #4caf50;
}

/* Child task completion progress */
.map-container me-tpc[data-child-progress-visible="true"] {
--child-progress-completed-ratio: 0%;
--child-progress-in-progress-ratio: 0%;
--child-progress-label-width: 30px;
background-image: linear-gradient(
to right,
#4caf50 var(--child-progress-completed-ratio),
#ff9800 var(--child-progress-completed-ratio),
#ff9800 var(--child-progress-in-progress-ratio),
rgba(255, 255, 255, 0.18) var(--child-progress-in-progress-ratio)
);
background-repeat: no-repeat;
background-size: calc(100% - 8px) 4px;
background-position: 4px calc(100% - 2px);
padding-bottom: 8px;
padding-inline-end: calc(var(--child-progress-label-width) + 16px);
}
.map-container me-tpc[data-child-progress-visible="true"]::after {
content: attr(data-child-progress-text);
position: absolute;
inset-inline-end: 6px;
top: 50%;
transform: translateY(-50%);
font-size: 10px;
font-weight: 600;
text-align: center;
color: rgba(255, 255, 255, 0.92);
background: rgba(0, 0, 0, 0.3);
border-radius: 10px;
width: var(--child-progress-label-width);
box-sizing: border-box;
padding: 0 4px;
line-height: 15px;
white-space: nowrap;
pointer-events: none;
}
</style>
</head>
<body>
Expand Down Expand Up @@ -843,6 +881,7 @@ export class CodeMindMapPanel {
let linkDivDebounceTimer = null; // debounce timer for the linkDiv bus event
let scheduleRafHandle = null;
let scheduleTimerHandle = null;
let applyingStatuses = false; // re-entry guard: prevents linkDiv→applyAllStatuses→linkDiv loop

function initMindMap() {
const options = {
Expand All @@ -859,6 +898,7 @@ export class CodeMindMapPanel {
node.data = node.data || {};
node.data.status = 'in-progress';
updateNodeStatus(node);
scheduleApplyAllStatuses();
vscode.postMessage({ action: 'mindMapOperation', operationName: 'updateNodeStatus' });
const cm = document.querySelector('.map-container > .context-menu'); if (cm) cm.hidden = true;
}
Expand All @@ -871,6 +911,7 @@ export class CodeMindMapPanel {
node.data = node.data || {};
node.data.status = 'completed';
updateNodeStatus(node);
scheduleApplyAllStatuses();
vscode.postMessage({ action: 'mindMapOperation', operationName: 'updateNodeStatus' });
const cm = document.querySelector('.map-container > .context-menu'); if (cm) cm.hidden = true;
}
Expand All @@ -883,6 +924,7 @@ export class CodeMindMapPanel {
node.data = node.data || {};
delete node.data.status;
updateNodeStatus(node);
scheduleApplyAllStatuses();
vscode.postMessage({ action: 'mindMapOperation', operationName: 'updateNodeStatus' });
const cm = document.querySelector('.map-container > .context-menu'); if (cm) cm.hidden = true;
}
Expand Down Expand Up @@ -1099,23 +1141,26 @@ export class CodeMindMapPanel {
});

// Helper function to update node visual status
function updateNodeStatus(nodeObj) {
if (!nodeObj || !nodeObj.id) return;
function getTopicElementByNode(nodeObj) {
if (!nodeObj || !nodeObj.id) return null;
const nodeElement = MindElixir.E(nodeObj.id);
if (!nodeElement) return;
if (!nodeElement) return null;

const status = nodeObj.data?.status || null;
const domEl = nodeElement.getEl?.() || nodeElement;
if (!domEl) return;
if (!domEl) return null;

const topicEl = (() => {
if (domEl.tagName === 'ME-TPC') return domEl;
const byQuery = domEl.querySelector?.('me-tpc');
if (byQuery) return byQuery;
const byTag = domEl.getElementsByTagName?.('me-tpc')?.[0];
if (byTag) return byTag;
return domEl;
})();
if (domEl.tagName === 'ME-TPC') return domEl;
const byQuery = domEl.querySelector?.('me-tpc');
if (byQuery) return byQuery;
const byTag = domEl.getElementsByTagName?.('me-tpc')?.[0];
if (byTag) return byTag;

return domEl;
}

function updateNodeStatus(nodeObj) {
const status = nodeObj.data?.status || null;
const topicEl = getTopicElementByNode(nodeObj);

if (!topicEl) return;

Expand All @@ -1127,6 +1172,61 @@ export class CodeMindMapPanel {
topicEl.setAttribute('data-status', status);
}

function updateNodeChildProgress(nodeObj) {
const topicEl = getTopicElementByNode(nodeObj);
if (!topicEl) return;

const children = Array.isArray(nodeObj.children) ? nodeObj.children : [];
if (children.length === 0) {
topicEl.removeAttribute('data-child-progress-visible');
topicEl.removeAttribute('data-child-progress-text');
topicEl.style.removeProperty('--child-progress-completed-ratio');
topicEl.style.removeProperty('--child-progress-in-progress-ratio');
topicEl.style.removeProperty('--child-progress-label-width');
return;
}

let completedChildren = 0;
let inProgressChildren = 0;
let statusMarkedChildren = 0;

for (const child of children) {
const childStatus = child?.data?.status;
if (childStatus === 'completed') {
completedChildren += 1;
statusMarkedChildren += 1;
} else if (childStatus === 'in-progress') {
inProgressChildren += 1;
statusMarkedChildren += 1;
}
}

// Show only when at least one child has in-progress or completed status.
if (statusMarkedChildren === 0) {
topicEl.removeAttribute('data-child-progress-visible');
topicEl.removeAttribute('data-child-progress-text');
topicEl.style.removeProperty('--child-progress-completed-ratio');
topicEl.style.removeProperty('--child-progress-in-progress-ratio');
topicEl.style.removeProperty('--child-progress-label-width');
return;
}

const completionRatio = children.length > 0
? Math.round((completedChildren / children.length) * 100)
: 0;
const inProgressRatio = children.length > 0
? Math.round(((completedChildren + inProgressChildren) / children.length) * 100)
: 0;
const progressText = completedChildren + '/' + children.length;
const labelWidth = Math.min(84, Math.max(30, progressText.length * 6 + 10));

topicEl.setAttribute('data-child-progress-visible', 'true');
topicEl.setAttribute('data-child-progress-text', progressText);
topicEl.style.setProperty('--child-progress-completed-ratio', completionRatio + '%');
topicEl.style.setProperty('--child-progress-in-progress-ratio', inProgressRatio + '%');
topicEl.style.setProperty('--child-progress-label-width', labelWidth + 'px');
}

function applyAllStatuses() {
const root = mind?.nodeData;
if (!root) return;
Expand All @@ -1135,14 +1235,18 @@ export class CodeMindMapPanel {
const node = stack.pop();
if (!node) continue;
updateNodeStatus(node);
updateNodeChildProgress(node);
if (Array.isArray(node.children)) {
for (const child of node.children) {
stack.push(child);
}
}
}
// linkDiv is called by MindElixir itself after layout; we must not call it here
// as that would create an infinite loop via the linkDiv bus listener
// Redraw branch lines to realign with resized nodes (padding changed by progress bars).
// The re-entry guard above prevents the resulting linkDiv event from re-triggering this.
applyingStatuses = true;
mind.linkDiv();
applyingStatuses = false;
}

function scheduleApplyAllStatuses() {
Expand Down Expand Up @@ -1174,7 +1278,9 @@ export class CodeMindMapPanel {
// Debounced linkDiv listener: MindElixir fires linkDiv after every layout pass
// (including multiple passes after refresh/changeTheme). Wait for 50ms of silence
// before applying statuses so we always run after the final DOM state.
// The applyingStatuses guard skips the event fired by our own mind.linkDiv() call.
mind.bus.addListener('linkDiv', () => {
if (applyingStatuses) return;
clearTimeout(linkDivDebounceTimer);
linkDivDebounceTimer = setTimeout(applyAllStatuses, 50);
});
Expand Down Expand Up @@ -1257,6 +1363,7 @@ export class CodeMindMapPanel {

// Update visual appearance
updateNodeStatus(currentNode);
scheduleApplyAllStatuses();

// Trigger autosave
vscode.postMessage({ action: 'mindMapOperation', operationName: 'updateNodeStatus' });
Expand Down
Loading