taskplane 0.7.2 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -17,9 +17,11 @@ function formatDuration(ms) {
17
17
  return `${m}m ${String(s).padStart(2, "0")}s`;
18
18
  }
19
19
 
20
- function relativeTime(epochMs) {
21
- if (!epochMs) return "";
22
- const diff = Date.now() - epochMs;
20
+ function relativeTime(epochOrIso) {
21
+ if (!epochOrIso) return "";
22
+ const ts = typeof epochOrIso === "string" ? new Date(epochOrIso).getTime() : epochOrIso;
23
+ if (isNaN(ts)) return "";
24
+ const diff = Date.now() - ts;
23
25
  if (diff < 60000) return `${Math.floor(diff / 1000)}s ago`;
24
26
  if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago`;
25
27
  return `${Math.floor(diff / 3600000)}h ago`;
@@ -587,6 +589,7 @@ function renderMergeAgents(batch, tmuxSessions) {
587
589
  const mergeResults = batch?.mergeResults || [];
588
590
  const tmuxSet = new Set(tmuxSessions || []);
589
591
  const showRepos = knownRepos.length >= 2;
592
+ const telemetry = currentData?.telemetry || {};
590
593
 
591
594
  // Check for active merge sessions (convention: orch-merge-*)
592
595
  const mergeSessions = (tmuxSessions || []).filter(s => s.startsWith("orch-merge"));
@@ -597,7 +600,7 @@ function renderMergeAgents(batch, tmuxSessions) {
597
600
  }
598
601
 
599
602
  let html = '<table class="merge-table"><thead><tr>';
600
- html += '<th>Wave</th><th>Status</th><th>Session</th><th>Attach</th><th>Details</th>';
603
+ html += '<th>Wave</th><th>Status</th><th>Session</th><th>Telemetry</th><th>Attach</th><th>Details</th>';
601
604
  html += '</tr></thead><tbody>';
602
605
 
603
606
  // Show merge results
@@ -618,10 +621,29 @@ function renderMergeAgents(batch, tmuxSessions) {
618
621
  const sessionName = `orch-merge-w${mr.waveIndex + 1}`;
619
622
  const alive = tmuxSet.has(sessionName);
620
623
 
624
+ // Look for merge telemetry data
625
+ const mergeTel = telemetry[sessionName] || telemetry[`orch-merge-${mr.waveIndex + 1}`] || null;
626
+
621
627
  html += `<tr>`;
622
628
  html += `<td style="font-family:var(--font-mono);">Wave ${mr.waveIndex + 1}</td>`;
623
629
  html += `<td><span class="status-badge ${statusCls}">${mr.status}</span></td>`;
624
630
  html += `<td style="font-family:var(--font-mono);font-size:0.8rem;">${alive ? escapeHtml(sessionName) : "—"}</td>`;
631
+ // Telemetry cell
632
+ html += `<td style="font-size:0.75rem;">`;
633
+ if (mergeTel) {
634
+ const totalTok = (mergeTel.inputTokens || 0) + (mergeTel.outputTokens || 0);
635
+ const cost = mergeTel.cost || 0;
636
+ if (totalTok > 0 || cost > 0) {
637
+ html += `<span style="color:var(--text-muted);">${totalTok > 0 ? totalTok.toLocaleString() + " tok" : ""}`;
638
+ if (cost > 0) html += ` · $${cost.toFixed(4)}`;
639
+ html += `</span>`;
640
+ } else {
641
+ html += '<span style="color:var(--text-faint);">—</span>';
642
+ }
643
+ } else {
644
+ html += '<span style="color:var(--text-faint);">—</span>';
645
+ }
646
+ html += `</td>`;
625
647
  html += `<td>`;
626
648
  if (alive) {
627
649
  const cmd = `tmux attach -t ${sessionName}`;
@@ -650,7 +672,8 @@ function renderMergeAgents(batch, tmuxSessions) {
650
672
  html += `<td>${repoBadgeHtml(rr.repoId)}</td>`;
651
673
  html += `<td><span class="status-badge ${rrStatusCls}">${rr.status}</span></td>`;
652
674
  html += `<td style="font-family:var(--font-mono);font-size:0.75rem;color:var(--text-faint);">${rrLanes}</td>`;
653
- html += `<td></td>`;
675
+ html += `<td></td>`; /* telemetry placeholder */
676
+ html += `<td></td>`; /* attach placeholder */
654
677
  html += `<td style="font-size:0.75rem;color:var(--text-faint);">${rrDetail}</td>`;
655
678
  html += `</tr>`;
656
679
  }
@@ -662,11 +685,28 @@ function renderMergeAgents(batch, tmuxSessions) {
662
685
  const alreadyShown = mergeResults.some((mr) => `orch-merge-w${mr.waveIndex + 1}` === sess);
663
686
  if (alreadyShown) continue;
664
687
 
688
+ const sessTel = telemetry[sess] || null;
665
689
  const cmd = `tmux attach -t ${sess}`;
666
690
  html += `<tr>`;
667
691
  html += `<td style="font-family:var(--font-mono);">—</td>`;
668
692
  html += `<td><span class="status-badge status-running"><span class="status-dot running"></span> merging</span></td>`;
669
693
  html += `<td style="font-family:var(--font-mono);font-size:0.8rem;">${escapeHtml(sess)}</td>`;
694
+ // Telemetry cell for active merge session
695
+ html += `<td style="font-size:0.75rem;">`;
696
+ if (sessTel) {
697
+ const totalTok = (sessTel.inputTokens || 0) + (sessTel.outputTokens || 0);
698
+ const cost = sessTel.cost || 0;
699
+ if (totalTok > 0 || cost > 0) {
700
+ html += `<span style="color:var(--text-muted);">${totalTok > 0 ? totalTok.toLocaleString() + " tok" : ""}`;
701
+ if (cost > 0) html += ` · $${cost.toFixed(4)}`;
702
+ html += `</span>`;
703
+ } else {
704
+ html += '<span style="color:var(--text-faint);">—</span>';
705
+ }
706
+ } else {
707
+ html += '<span style="color:var(--text-faint);">—</span>';
708
+ }
709
+ html += `</td>`;
670
710
  html += `<td><span class="tmux-cmd" data-tmux="${escapeHtml(sess)}" onclick="copyTmuxCmd('${escapeHtml(sess)}')" title="Click to copy">${escapeHtml(cmd)}</span></td>`;
671
711
  html += `<td>—</td>`;
672
712
  html += `</tr>`;
@@ -711,17 +751,16 @@ function renderNoBatch() {
711
751
  if ($mergePanel) $mergePanel.style.display = "none";
712
752
  if ($errorsPanel) $errorsPanel.style.display = "none";
713
753
 
714
- // Try to show the latest history entry
715
- if (historyList.length > 0 && !viewingHistoryId) {
716
- viewHistoryEntry(historyList[0].batchId);
717
- $historySelect.value = historyList[0].batchId;
718
- } else if (historyList.length === 0) {
719
- // No history yet — show placeholder in the history panel
754
+ // Show a placeholder while history loads. loadHistoryList() (called in
755
+ // render() just before this) is async — the fresh list may not be
756
+ // available yet. The loadHistoryList callback will replace this with
757
+ // the actual latest entry once it resolves.
758
+ if (!viewingHistoryId) {
720
759
  $historyBody.innerHTML = `
721
760
  <div class="no-batch">
722
761
  <div class="no-batch-icon">⏳</div>
723
- <div class="no-batch-title">No batch running</div>
724
- <div class="no-batch-hint">.pi/batch-state.json not found<br>Start an orchestrator batch to see the dashboard.</div>
762
+ <div class="no-batch-title">Batch complete</div>
763
+ <div class="no-batch-hint">Loading history…</div>
725
764
  </div>`;
726
765
  $historyPanel.style.display = "";
727
766
  }
@@ -1405,10 +1444,22 @@ function loadHistoryList() {
1405
1444
  .then(list => {
1406
1445
  historyList = list || [];
1407
1446
  renderHistoryDropdown();
1408
- // If no live batch and no history shown yet, auto-select latest
1409
- if (noBatchRendered && !viewingHistoryId && historyList.length > 0) {
1447
+ // Auto-select the latest history entry when no live batch is running.
1448
+ // Always update the view here renderNoBatch() shows a placeholder
1449
+ // while this async fetch completes, so we need to replace it with
1450
+ // the actual latest entry. This fixes #20 where the stale cached
1451
+ // historyList caused the previous batch to be shown.
1452
+ if (noBatchRendered && historyList.length > 0) {
1410
1453
  viewHistoryEntry(historyList[0].batchId);
1411
1454
  $historySelect.value = historyList[0].batchId;
1455
+ } else if (noBatchRendered && historyList.length === 0) {
1456
+ $historyBody.innerHTML = `
1457
+ <div class="no-batch">
1458
+ <div class="no-batch-icon">⏳</div>
1459
+ <div class="no-batch-title">No batch running</div>
1460
+ <div class="no-batch-hint">.pi/batch-state.json not found<br>Start an orchestrator batch to see the dashboard.</div>
1461
+ </div>`;
1462
+ $historyPanel.style.display = "";
1412
1463
  }
1413
1464
  })
1414
1465
  .catch(() => {});
@@ -239,7 +239,8 @@ const telemetryPrefixFiles = new Map();
239
239
  /**
240
240
  * Parse a telemetry JSONL filename to extract lane number and role.
241
241
  * Pattern: {opId}-{batchId}-{repoId}[-{taskId}][-lane-{N}]-{role}.jsonl
242
- * Returns { laneNumber: number|null, role: string } or null if unparseable.
242
+ * Roles: worker, reviewer, merger
243
+ * Returns { laneNumber: number|null, role: string, mergeNumber: number|null } or null if unparseable.
243
244
  */
244
245
  function parseTelemetryFilename(filename) {
245
246
  // Remove .jsonl extension
@@ -248,13 +249,17 @@ function parseTelemetryFilename(filename) {
248
249
  const lastDash = base.lastIndexOf("-");
249
250
  if (lastDash < 0) return null;
250
251
  const role = base.slice(lastDash + 1);
251
- if (role !== "worker" && role !== "reviewer") return null;
252
+ if (role !== "worker" && role !== "reviewer" && role !== "merger") return null;
252
253
 
253
254
  // Extract lane number from -lane-{N}- pattern
254
255
  const laneMatch = base.match(/-lane-(\d+)-/);
255
256
  const laneNumber = laneMatch ? parseInt(laneMatch[1], 10) : null;
256
257
 
257
- return { laneNumber, role };
258
+ // Extract merge number from -merge-{N}- pattern (merge agents)
259
+ const mergeMatch = base.match(/-merge-(\d+)-/);
260
+ const mergeNumber = mergeMatch ? parseInt(mergeMatch[1], 10) : null;
261
+
262
+ return { laneNumber, role, mergeNumber };
258
263
  }
259
264
 
260
265
  /**
@@ -375,7 +380,10 @@ function loadTelemetryData(batchState) {
375
380
 
376
381
  // Determine the key (tmux prefix)
377
382
  let prefix;
378
- if (parsed.laneNumber != null && laneToPrefix[parsed.laneNumber]) {
383
+ if (parsed.role === "merger") {
384
+ // Merge agent — key by merge session number or generic "merge"
385
+ prefix = parsed.mergeNumber != null ? `orch-merge-${parsed.mergeNumber}` : "orch-merge";
386
+ } else if (parsed.laneNumber != null && laneToPrefix[parsed.laneNumber]) {
379
387
  prefix = laneToPrefix[parsed.laneNumber];
380
388
  } else if (parsed.laneNumber != null) {
381
389
  // Lane number found but no batch-state mapping — use heuristic