(nodes: TreeNode[], phase: 'initial' | 'background')
| 251 | } |
| 252 | |
| 253 | async function loadTaskProgressForNodes(nodes: TreeNode[], phase: 'initial' | 'background') { |
| 254 | if (!projectPath.value || !isElectron()) return |
| 255 | if (nodes.length === 0) return |
| 256 | |
| 257 | const perfEnabled = isPerfEnabled() |
| 258 | const perfStart = perfEnabled ? perfNow() : 0 |
| 259 | let attempts = 0 |
| 260 | let successes = 0 |
| 261 | |
| 262 | const queue = nodes.filter(node => { |
| 263 | const key = normalizePath(node.path || '') |
| 264 | if (!key) return false |
| 265 | if (taskProgressCache.has(key)) return false |
| 266 | if (taskProgressPending.has(key)) return false |
| 267 | taskProgressPending.add(key) |
| 268 | return true |
| 269 | }) |
| 270 | |
| 271 | const workers = Array.from({ length: TASK_PROGRESS_CONCURRENCY }, async () => { |
| 272 | while (queue.length > 0) { |
| 273 | const node = queue.shift() |
| 274 | if (!node || !node.path) continue |
| 275 | const tasksPath = `${node.path}/tasks.md` |
| 276 | attempts += 1 |
| 277 | try { |
| 278 | const fullPath = `${projectPath.value}/${tasksPath}` |
| 279 | const result = await window.electronAPI!.readFile(fullPath) |
| 280 | if (result.success && result.content) { |
| 281 | const progress = parseTaskProgress(result.content) |
| 282 | queueTaskProgressUpdate(node, progress) |
| 283 | successes += 1 |
| 284 | } |
| 285 | } catch { |
| 286 | // ignore missing tasks.md |
| 287 | } finally { |
| 288 | taskProgressPending.delete(normalizePath(node.path)) |
| 289 | } |
| 290 | } |
| 291 | }) |
| 292 | |
| 293 | await Promise.all(workers) |
| 294 | |
| 295 | if (perfEnabled) { |
| 296 | const durationMs = perfNow() - perfStart |
| 297 | perfLog('tasks.load', { |
| 298 | phase, |
| 299 | durationMs: Math.round(durationMs * 1000) / 1000, |
| 300 | changeCount: nodes.length, |
| 301 | attempts, |
| 302 | successes |
| 303 | }) |
| 304 | } |
| 305 | } |
| 306 | |
| 307 | function queueTaskProgressLoad(token: number) { |
| 308 | const activeChanges = changesTree.value.filter(n => n.name !== 'archive') |
no test coverage detected