taskplane 0.28.0 → 0.28.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.
@@ -241,6 +241,7 @@ let viewingHistoryId = null; // batchId if viewing history, null if live
241
241
 
242
242
  let viewerMode = null; // "conversation" | "status-md" | null
243
243
  let viewerTarget = null; // session name (conversation) or taskId (status-md)
244
+ let lastBatchId = null; // TP-178: track batchId for stale viewer detection (#487)
244
245
 
245
246
  // ─── Repo Helpers ───────────────────────────────────────────────────────────
246
247
 
@@ -515,14 +516,18 @@ function renderSummary(batch) {
515
516
  const fillPct = ws.total > 0 ? (ws.checked / ws.total) * 100 : 0;
516
517
  const checkboxDone = ws.checked === ws.total && ws.total > 0;
517
518
  const pastWave = ws.waveIdx < currentWaveIdx;
518
- const batchDone = batch.phase === "completed" || batch.phase === "merging";
519
+ const batchDone = batch.phase === "completed";
520
+ // TP-178: During merging, only past waves are done; current wave is merging, future waves are pending (#493)
521
+ const isMerging = batch.phase === "merging";
519
522
  const isDone = checkboxDone || pastWave || batchDone || ws.allSucceeded;
520
- const isCurrent = ws.waveIdx === currentWaveIdx && (batch.phase === "executing" || batch.phase === "merging");
521
- const isFuture = ws.waveIdx > currentWaveIdx && batch.phase === "executing";
523
+ const isMergingWave = isMerging && ws.waveIdx === currentWaveIdx;
524
+ const isCurrent = ws.waveIdx === currentWaveIdx && (batch.phase === "executing" || isMerging);
525
+ const isFuture = ws.waveIdx > currentWaveIdx && (batch.phase === "executing" || isMerging);
522
526
 
523
527
  const fillClass = isDone ? "pct-hi" : fillPct > 50 ? "pct-mid" : fillPct > 0 ? "pct-low" : "pct-0";
524
528
  const fillWidth = isDone ? 100 : fillPct;
525
- const segClass = isCurrent ? "wave-seg-current" : isFuture ? "wave-seg-future" : "";
529
+ // TP-178: Add merging visual state for the wave currently being merged (#493)
530
+ const segClass = isMergingWave ? "wave-seg-current wave-seg-merging" : isCurrent ? "wave-seg-current" : isFuture ? "wave-seg-future" : "";
526
531
 
527
532
  // TP-148: Use segment-aware labels in tooltip when available
528
533
  const segLabels = waveSegmentLabels[ws.waveIdx] || new Map();
@@ -604,9 +609,11 @@ function renderSummary(batch) {
604
609
  const waveIdx = batch.currentWaveIndex || 0;
605
610
  let wavesHtml = '<span style="color:var(--text-muted); font-weight:600; margin-right:4px;">Waves</span>';
606
611
  batch.wavePlan.forEach((taskIds, i) => {
607
- const isDone = i < waveIdx || batch.phase === "completed" || batch.phase === "merging";
608
- const isCurrent = i === waveIdx && batch.phase === "executing";
609
- const cls = isDone ? "done" : isCurrent ? "current" : "";
612
+ // TP-178: During merging, only past waves are done; current wave shows merging state (#493)
613
+ const isDone = i < waveIdx || batch.phase === "completed";
614
+ const isCurrent = i === waveIdx && (batch.phase === "executing" || batch.phase === "merging");
615
+ const isMergingChip = i === waveIdx && batch.phase === "merging";
616
+ const cls = isDone ? "done" : isMergingChip ? "current merging" : isCurrent ? "current" : "";
610
617
  wavesHtml += `<span class="wave-chip ${cls}">W${i + 1} [${taskIds.join(", ")}]</span>`;
611
618
  });
612
619
  $summaryWaves.innerHTML = wavesHtml;
@@ -730,18 +737,32 @@ function renderLanesTasks(batch, sessions) {
730
737
  <div class="task-progress-bar"><div class="task-progress-fill pct-0" style="width:0%"></div></div>
731
738
  <span class="task-progress-text">0%</span>
732
739
  </div>`;
740
+ } else if (task.status === "running") {
741
+ // TP-178: Show executing indicator for running tasks without sidecar data (#494)
742
+ // This covers non-final segment execution where the sidecar hasn't started yet.
743
+ progressHtml = `
744
+ <div class="task-progress">
745
+ <div class="task-progress-bar"><div class="task-progress-fill pct-low task-progress-executing" style="width:100%"></div></div>
746
+ <span class="task-progress-text">executing…</span>
747
+ </div>`;
733
748
  } else {
734
749
  progressHtml = '<span style="color:var(--text-faint)">—</span>';
735
750
  }
736
751
 
737
752
  // Step cell
753
+ // TP-178: Prefer V2 snapshot currentStep (refreshed every sidecar poll) over
754
+ // server-parsed statusData which can lag behind (#488).
738
755
  let stepHtml = "";
739
- if (sd) {
740
- stepHtml = escapeHtml(sd.currentStep);
741
- if (sd.iteration > 0) stepHtml += `<span class="task-iter">i${sd.iteration}</span>`;
742
- if (sd.reviews > 0) stepHtml += `<span class="task-iter">r${sd.reviews}</span>`;
743
- } else if (task.status === "succeeded") {
756
+ if (task.status === "succeeded") {
757
+ // TP-178: Succeeded tasks always show "Complete" regardless of sidecar data (#491)
744
758
  stepHtml = '<span style="color:var(--green)">Complete</span>';
759
+ } else if (sd || (useV2 && v2p)) {
760
+ const stepName = (useV2 && v2p && v2p.currentStep) ? v2p.currentStep : (sd ? sd.currentStep : "Unknown");
761
+ const iter = (useV2 && v2p && v2p.iteration != null) ? v2p.iteration : (sd ? sd.iteration : 0);
762
+ const revs = (useV2 && v2p && v2p.reviews != null) ? v2p.reviews : (sd ? sd.reviews : 0);
763
+ stepHtml = escapeHtml(stepName);
764
+ if (iter > 0) stepHtml += `<span class="task-iter">i${iter}</span>`;
765
+ if (revs > 0) stepHtml += `<span class="task-iter">r${revs}</span>`;
745
766
  } else if (task.status === "pending") {
746
767
  stepHtml = '<span style="color:var(--text-faint)">Waiting</span>';
747
768
  } else {
@@ -987,15 +1008,27 @@ function renderMergeAgents(batch, sessions) {
987
1008
  const effectiveAlive = !!effectiveSession;
988
1009
  if (effectiveSession) shownSessions.add(effectiveSession);
989
1010
 
990
- // Find merge telemetry: try sessions by lane number first
1011
+ // TP-178: Find merge telemetry precisely using waveIndex (#498).
1012
+ // First try matching by waveIndex from the telemetry entries (injected from merge snapshots).
1013
+ // Then fall back to session-based matching (lane numbers), but never use a
1014
+ // catch-all fallback that grabs any merge session's telemetry.
991
1015
  let mergeTel = null;
992
- for (const ln of waveLaneNums) {
993
- const candidate = getMergeSessionName(ln);
994
- if (telemetry[candidate]) { mergeTel = telemetry[candidate]; break; }
1016
+ // Priority 1: Match by waveIndex in telemetry entries
1017
+ for (const [telKey, tel] of Object.entries(telemetry)) {
1018
+ if (tel._source === "merge-snapshot" && tel.waveIndex === mr.waveIndex) {
1019
+ mergeTel = tel;
1020
+ break;
1021
+ }
1022
+ }
1023
+ // Priority 2: Match by lane number session
1024
+ if (!mergeTel) {
1025
+ for (const ln of waveLaneNums) {
1026
+ const candidate = getMergeSessionName(ln);
1027
+ if (telemetry[candidate]) { mergeTel = telemetry[candidate]; break; }
1028
+ }
995
1029
  }
996
- // Fallback: effective session telemetry or any merge session
1030
+ // Priority 3: Effective session telemetry only (no catch-all fallback)
997
1031
  if (!mergeTel && effectiveSession) mergeTel = telemetry[effectiveSession] || null;
998
- if (!mergeTel) mergeTel = mergeSessions.reduce((found, ms) => found || telemetry[ms] || null, null);
999
1032
 
1000
1033
  html += `<tr>`;
1001
1034
  html += `<td class="merge-wave-cell">Wave ${mr.waveIndex + 1}</td>`;
@@ -1443,6 +1476,8 @@ function buildRecoveryTimeline(supervisor) {
1443
1476
  target: a.target || a.lane || a.taskId || "",
1444
1477
  outcome: a.outcome || a.result || "",
1445
1478
  reason: a.reason || "",
1479
+ context: a.context || "",
1480
+ detail: a.detail || "",
1446
1481
  source: "action"
1447
1482
  }));
1448
1483
 
@@ -1456,6 +1491,8 @@ function buildRecoveryTimeline(supervisor) {
1456
1491
  target: e.target || e.lane || e.taskId || "",
1457
1492
  outcome: e.outcome || e.result || "",
1458
1493
  reason: e.reason || e.message || "",
1494
+ context: e.context || "",
1495
+ detail: e.detail || "",
1459
1496
  source: "event"
1460
1497
  }));
1461
1498
 
@@ -1488,6 +1525,7 @@ function renderSupervisorActions(supervisor) {
1488
1525
  const target = entry.target;
1489
1526
  const outcome = entry.outcome;
1490
1527
  const reason = entry.reason;
1528
+ const description = entry.context || entry.detail || "";
1491
1529
 
1492
1530
  const outcomeCls = outcome === "success" || outcome === "recovered"
1493
1531
  ? "action-success"
@@ -1507,6 +1545,11 @@ function renderSupervisorActions(supervisor) {
1507
1545
  if (target) html += `<span class="supervisor-action-target">${escapeHtml(target)}</span>`;
1508
1546
  if (outcome) html += `<span class="supervisor-action-outcome ${outcomeCls}">${escapeHtml(outcome)}</span>`;
1509
1547
  html += ` </div>`;
1548
+ if (description) {
1549
+ const fullDesc = escapeHtml(description);
1550
+ const truncated = description.length > 100 ? escapeHtml(description.slice(0, 100)) + "\u2026" : fullDesc;
1551
+ html += `<div class="supervisor-action-description" title="${fullDesc}">${truncated}</div>`;
1552
+ }
1510
1553
  if (reason) {
1511
1554
  html += `<div class="supervisor-action-reason">${escapeHtml(reason)}</div>`;
1512
1555
  }
@@ -1567,6 +1610,9 @@ function render(data) {
1567
1610
  $lastUpdate.textContent = new Date().toLocaleTimeString();
1568
1611
 
1569
1612
  if (!batch) {
1613
+ // TP-178: Clear viewer when batch disappears (#487)
1614
+ if (lastBatchId && viewerMode) closeViewer();
1615
+ lastBatchId = null;
1570
1616
  renderHeader(null);
1571
1617
  renderSummary(null);
1572
1618
  renderSupervisor(data);
@@ -1576,6 +1622,12 @@ function render(data) {
1576
1622
  return;
1577
1623
  }
1578
1624
 
1625
+ // TP-178: Detect batchId change — clear stale viewer state (#487)
1626
+ if (batch.batchId && lastBatchId && batch.batchId !== lastBatchId && viewerMode) {
1627
+ closeViewer();
1628
+ }
1629
+ lastBatchId = batch.batchId || null;
1630
+
1579
1631
  // Live batch is running — hide history panel, reset viewing state
1580
1632
  if (viewingHistoryId) {
1581
1633
  viewingHistoryId = null;
@@ -415,6 +415,20 @@ body {
415
415
  color: var(--green);
416
416
  }
417
417
 
418
+ /* TP-178: Merging wave indicator — pulsing accent to distinguish from executing (#493) */
419
+ .wave-chip.merging {
420
+ animation: merge-pulse 1.5s ease-in-out infinite;
421
+ }
422
+
423
+ .wave-seg-merging {
424
+ animation: merge-pulse 1.5s ease-in-out infinite;
425
+ }
426
+
427
+ @keyframes merge-pulse {
428
+ 0%, 100% { opacity: 1; }
429
+ 50% { opacity: 0.5; }
430
+ }
431
+
418
432
  /* ─── Content ──────────────────────────────────────────────────────────── */
419
433
 
420
434
  .content {
@@ -713,6 +727,16 @@ body {
713
727
  .task-progress-fill.pct-mid { background: var(--cyan); }
714
728
  .task-progress-fill.pct-hi { background: var(--green); }
715
729
 
730
+ /* TP-178: Pulsing indicator for executing tasks without sidecar progress (#494) */
731
+ .task-progress-executing {
732
+ opacity: 0.4;
733
+ animation: executing-pulse 2s ease-in-out infinite;
734
+ }
735
+ @keyframes executing-pulse {
736
+ 0%, 100% { opacity: 0.25; }
737
+ 50% { opacity: 0.55; }
738
+ }
739
+
716
740
  .task-progress-text {
717
741
  font-family: var(--font-mono);
718
742
  font-size: 0.75rem;
@@ -1555,6 +1579,18 @@ body {
1555
1579
  color: var(--yellow);
1556
1580
  }
1557
1581
 
1582
+ .supervisor-action-description {
1583
+ font-size: 0.8rem;
1584
+ color: var(--text-secondary, #b0b0b0);
1585
+ margin-top: 3px;
1586
+ line-height: 1.4;
1587
+ overflow: hidden;
1588
+ text-overflow: ellipsis;
1589
+ white-space: nowrap;
1590
+ max-width: 600px;
1591
+ cursor: default;
1592
+ }
1593
+
1558
1594
  .supervisor-action-reason {
1559
1595
  font-size: 0.78rem;
1560
1596
  color: var(--text-muted);
@@ -1153,6 +1153,8 @@ function buildDashboardState() {
1153
1153
  latestTotalTokens: (agent.inputTokens || 0) + (agent.outputTokens || 0),
1154
1154
  _updatedAt: snap.updatedAt,
1155
1155
  _source: "merge-snapshot",
1156
+ // TP-178: Include waveIndex for precise wave-telemetry association (#498)
1157
+ waveIndex: snap.waveIndex != null ? snap.waveIndex : undefined,
1156
1158
  };
1157
1159
  }
1158
1160
  }
@@ -659,11 +659,11 @@ export function parsePromptForOrchestrator(
659
659
 
660
660
  // Check for "None" variants
661
661
  if (!/\*?\*?None\*?\*?/i.test(depBody) && depBody.length > 0) {
662
- // Pattern 1: "**Requires:** COMP-005 ..." or "**Requires:** time-off/TO-014 ..."
663
- const requiresMatches = depBody.matchAll(
664
- /\*?\*?Requires:?\*?\*?\s*((?:[a-z0-9-]+\/)?[A-Z]+-\d+)/gi,
662
+ // Pattern 1: "**Requires:** COMP-005 ..." or "**Task:** TO-014 ..."
663
+ const labeledMatches = depBody.matchAll(
664
+ /\*?\*?(?:Requires|Task):?\*?\*?\s*((?:[a-z0-9-]+\/)?[A-Z]+-\d+)/gi,
665
665
  );
666
- for (const m of requiresMatches) {
666
+ for (const m of labeledMatches) {
667
667
  const dep = normalizeDependencyReference(m[1]);
668
668
  if (!dependencies.includes(dep)) dependencies.push(dep);
669
669
  }