taskplane 0.27.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;
@@ -699,21 +706,30 @@ function renderLanesTasks(batch, sessions) {
699
706
  const showPacketHome = !!packetHomeRepo && packetHomeRepo !== (tRepo || lane.repoId || "");
700
707
 
701
708
  // Progress cell
709
+ // TP-174: Prefer V2 snapshot progress (segment-scoped when available)
710
+ // over full STATUS.md counts when the task is actively running on this lane.
711
+ // TP-176: Succeeded tasks always show 100% regardless of sidecar/statusData (#491).
702
712
  let progressHtml = "";
703
- if (sd) {
704
- const fillClass = pctClass(sd.progress);
713
+ const v2p = ls && ls._v2Progress;
714
+ const useV2 = v2p && v2p.total > 0 && ls.taskId === task.taskId;
715
+ if (task.status === "succeeded") {
716
+ // #491 fix: succeeded tasks always show 100%
705
717
  progressHtml = `
706
718
  <div class="task-progress">
707
- <div class="task-progress-bar">
708
- <div class="task-progress-fill ${fillClass}" style="width:${sd.progress}%"></div>
709
- </div>
710
- <span class="task-progress-text">${sd.progress}% ${sd.checked}/${sd.total}</span>
719
+ <div class="task-progress-bar"><div class="task-progress-fill pct-hi" style="width:100%"></div></div>
720
+ <span class="task-progress-text">100%</span>
711
721
  </div>`;
712
- } else if (task.status === "succeeded") {
722
+ } else if (sd || useV2) {
723
+ const displayChecked = useV2 ? v2p.checked : (sd ? sd.checked : 0);
724
+ const displayTotal = useV2 ? v2p.total : (sd ? sd.total : 0);
725
+ const displayProgress = displayTotal > 0 ? Math.round((displayChecked / displayTotal) * 100) : 0;
726
+ const fillClass = pctClass(displayProgress);
713
727
  progressHtml = `
714
728
  <div class="task-progress">
715
- <div class="task-progress-bar"><div class="task-progress-fill pct-hi" style="width:100%"></div></div>
716
- <span class="task-progress-text">100%</span>
729
+ <div class="task-progress-bar">
730
+ <div class="task-progress-fill ${fillClass}" style="width:${displayProgress}%"></div>
731
+ </div>
732
+ <span class="task-progress-text">${displayProgress}% ${displayChecked}/${displayTotal}</span>
717
733
  </div>`;
718
734
  } else if (task.status === "pending") {
719
735
  progressHtml = `
@@ -721,18 +737,32 @@ function renderLanesTasks(batch, sessions) {
721
737
  <div class="task-progress-bar"><div class="task-progress-fill pct-0" style="width:0%"></div></div>
722
738
  <span class="task-progress-text">0%</span>
723
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>`;
724
748
  } else {
725
749
  progressHtml = '<span style="color:var(--text-faint)">—</span>';
726
750
  }
727
751
 
728
752
  // Step cell
753
+ // TP-178: Prefer V2 snapshot currentStep (refreshed every sidecar poll) over
754
+ // server-parsed statusData which can lag behind (#488).
729
755
  let stepHtml = "";
730
- if (sd) {
731
- stepHtml = escapeHtml(sd.currentStep);
732
- if (sd.iteration > 0) stepHtml += `<span class="task-iter">i${sd.iteration}</span>`;
733
- if (sd.reviews > 0) stepHtml += `<span class="task-iter">r${sd.reviews}</span>`;
734
- } else if (task.status === "succeeded") {
756
+ if (task.status === "succeeded") {
757
+ // TP-178: Succeeded tasks always show "Complete" regardless of sidecar data (#491)
735
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>`;
736
766
  } else if (task.status === "pending") {
737
767
  stepHtml = '<span style="color:var(--text-faint)">Waiting</span>';
738
768
  } else {
@@ -978,15 +1008,27 @@ function renderMergeAgents(batch, sessions) {
978
1008
  const effectiveAlive = !!effectiveSession;
979
1009
  if (effectiveSession) shownSessions.add(effectiveSession);
980
1010
 
981
- // 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.
982
1015
  let mergeTel = null;
983
- for (const ln of waveLaneNums) {
984
- const candidate = getMergeSessionName(ln);
985
- 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
+ }
986
1029
  }
987
- // Fallback: effective session telemetry or any merge session
1030
+ // Priority 3: Effective session telemetry only (no catch-all fallback)
988
1031
  if (!mergeTel && effectiveSession) mergeTel = telemetry[effectiveSession] || null;
989
- if (!mergeTel) mergeTel = mergeSessions.reduce((found, ms) => found || telemetry[ms] || null, null);
990
1032
 
991
1033
  html += `<tr>`;
992
1034
  html += `<td class="merge-wave-cell">Wave ${mr.waveIndex + 1}</td>`;
@@ -1434,6 +1476,8 @@ function buildRecoveryTimeline(supervisor) {
1434
1476
  target: a.target || a.lane || a.taskId || "",
1435
1477
  outcome: a.outcome || a.result || "",
1436
1478
  reason: a.reason || "",
1479
+ context: a.context || "",
1480
+ detail: a.detail || "",
1437
1481
  source: "action"
1438
1482
  }));
1439
1483
 
@@ -1447,6 +1491,8 @@ function buildRecoveryTimeline(supervisor) {
1447
1491
  target: e.target || e.lane || e.taskId || "",
1448
1492
  outcome: e.outcome || e.result || "",
1449
1493
  reason: e.reason || e.message || "",
1494
+ context: e.context || "",
1495
+ detail: e.detail || "",
1450
1496
  source: "event"
1451
1497
  }));
1452
1498
 
@@ -1479,6 +1525,7 @@ function renderSupervisorActions(supervisor) {
1479
1525
  const target = entry.target;
1480
1526
  const outcome = entry.outcome;
1481
1527
  const reason = entry.reason;
1528
+ const description = entry.context || entry.detail || "";
1482
1529
 
1483
1530
  const outcomeCls = outcome === "success" || outcome === "recovered"
1484
1531
  ? "action-success"
@@ -1498,6 +1545,11 @@ function renderSupervisorActions(supervisor) {
1498
1545
  if (target) html += `<span class="supervisor-action-target">${escapeHtml(target)}</span>`;
1499
1546
  if (outcome) html += `<span class="supervisor-action-outcome ${outcomeCls}">${escapeHtml(outcome)}</span>`;
1500
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
+ }
1501
1553
  if (reason) {
1502
1554
  html += `<div class="supervisor-action-reason">${escapeHtml(reason)}</div>`;
1503
1555
  }
@@ -1558,6 +1610,9 @@ function render(data) {
1558
1610
  $lastUpdate.textContent = new Date().toLocaleTimeString();
1559
1611
 
1560
1612
  if (!batch) {
1613
+ // TP-178: Clear viewer when batch disappears (#487)
1614
+ if (lastBatchId && viewerMode) closeViewer();
1615
+ lastBatchId = null;
1561
1616
  renderHeader(null);
1562
1617
  renderSummary(null);
1563
1618
  renderSupervisor(data);
@@ -1567,6 +1622,12 @@ function render(data) {
1567
1622
  return;
1568
1623
  }
1569
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
+
1570
1631
  // Live batch is running — hide history panel, reset viewing state
1571
1632
  if (viewingHistoryId) {
1572
1633
  viewingHistoryId = null;
@@ -1881,6 +1942,127 @@ function renderV2Event(evt) {
1881
1942
  }
1882
1943
  }
1883
1944
 
1945
+ // ── Segment-Scoped STATUS.md Helpers (TP-176) ──────────────────────────────
1946
+
1947
+ /**
1948
+ * Resolve the active segment repoId for a given task.
1949
+ * Uses runtimeLaneSnapshots (active segment) and falls back to
1950
+ * taskSegmentProgress (batch state).
1951
+ * Returns { repoId, segmentInfo } or null if single-segment / unresolvable.
1952
+ */
1953
+ function resolveActiveSegmentForTask(taskId) {
1954
+ if (!currentData) return null;
1955
+ const batch = currentData.batch;
1956
+ if (!batch) return null;
1957
+ const task = (batch.tasks || []).find(t => t.taskId === taskId);
1958
+ if (!task) return null;
1959
+ const segmentIds = Array.isArray(task.segmentIds) ? task.segmentIds.filter(id => typeof id === "string") : [];
1960
+ if (segmentIds.length <= 1) return null; // single-segment or no segments
1961
+
1962
+ // Try to get active segment from runtime lane snapshots
1963
+ const v2Snapshots = currentData.runtimeLaneSnapshots || {};
1964
+ for (const snap of Object.values(v2Snapshots)) {
1965
+ if (snap && snap.taskId === taskId && snap.segmentId) {
1966
+ const parsed = parseSegmentId(snap.segmentId);
1967
+ if (parsed) {
1968
+ const idx = segmentIds.indexOf(snap.segmentId);
1969
+ return {
1970
+ repoId: parsed.repoId,
1971
+ segmentInfo: {
1972
+ index: idx >= 0 ? idx + 1 : null,
1973
+ total: segmentIds.length,
1974
+ repoId: parsed.repoId,
1975
+ segmentId: snap.segmentId,
1976
+ },
1977
+ };
1978
+ }
1979
+ }
1980
+ }
1981
+
1982
+ // Fallback: use taskSegmentProgress (batch state)
1983
+ const segmentStatusMap = buildSegmentStatusMap(batch);
1984
+ const info = taskSegmentProgress(task, segmentStatusMap, null);
1985
+ if (info && info.repoId) {
1986
+ return { repoId: info.repoId, segmentInfo: info };
1987
+ }
1988
+ return null;
1989
+ }
1990
+
1991
+ /**
1992
+ * Filter STATUS.md content to show only the active segment's blocks.
1993
+ * Within each `### Step N:` section, removes `#### Segment: <otherRepo>` blocks
1994
+ * and keeps only the block matching `activeRepoId`.
1995
+ * Non-step content (metadata, notes, reviews, etc.) is preserved.
1996
+ *
1997
+ * Returns the filtered markdown string, or the original if no segment markers found.
1998
+ */
1999
+ function filterStatusMdForSegment(markdown, activeRepoId) {
2000
+ if (!activeRepoId) return markdown;
2001
+ const lines = markdown.split('\n');
2002
+ const result = [];
2003
+ let inStep = false; // inside a ### Step section
2004
+ let inSegmentBlock = false; // inside a #### Segment: <repo> block
2005
+ let segmentMatch = false; // current segment block matches active repo
2006
+ let foundAnySegmentHeader = false;
2007
+
2008
+ for (let i = 0; i < lines.length; i++) {
2009
+ const line = lines[i];
2010
+
2011
+ // Detect step headers: ### Step N: ...
2012
+ if (/^###\s+Step\s+\d+/.test(line)) {
2013
+ inStep = true;
2014
+ inSegmentBlock = false;
2015
+ segmentMatch = false;
2016
+ result.push(line);
2017
+ continue;
2018
+ }
2019
+
2020
+ // Detect non-step ### headers (e.g., ### Reviews, ### Notes)
2021
+ if (/^###\s+/.test(line) && !/^###\s+Step\s+\d+/.test(line)) {
2022
+ inStep = false;
2023
+ inSegmentBlock = false;
2024
+ segmentMatch = false;
2025
+ result.push(line);
2026
+ continue;
2027
+ }
2028
+
2029
+ // Inside a step section, detect #### Segment: <repoId> headers
2030
+ if (inStep && /^####\s+Segment:\s*/.test(line)) {
2031
+ foundAnySegmentHeader = true;
2032
+ const segRepo = line.replace(/^####\s+Segment:\s*/, '').trim();
2033
+ inSegmentBlock = true;
2034
+ segmentMatch = (segRepo === activeRepoId);
2035
+ if (segmentMatch) {
2036
+ result.push(line);
2037
+ }
2038
+ continue;
2039
+ }
2040
+
2041
+ // Detect any other #### header (ends current segment block)
2042
+ if (/^####\s+/.test(line)) {
2043
+ inSegmentBlock = false;
2044
+ segmentMatch = false;
2045
+ result.push(line);
2046
+ continue;
2047
+ }
2048
+
2049
+ // If we're in a segment block, only include matching lines
2050
+ if (inSegmentBlock) {
2051
+ if (segmentMatch) {
2052
+ result.push(line);
2053
+ }
2054
+ continue;
2055
+ }
2056
+
2057
+ // Outside segment blocks: keep the line
2058
+ result.push(line);
2059
+ }
2060
+
2061
+ // If no segment headers were found, return original (fallback for single-segment)
2062
+ if (!foundAnySegmentHeader) return markdown;
2063
+ return result.join('\n');
2064
+ }
2065
+
1884
2066
  // ── Open STATUS.md viewer ───────────────────────────────────────────────────
1885
2067
 
1886
2068
  function viewStatusMd(taskId) {
@@ -1897,7 +2079,15 @@ function viewStatusMd(taskId) {
1897
2079
  autoScrollOn = false;
1898
2080
  lastStatusMdText = '';
1899
2081
 
1900
- $terminalTitle.textContent = `STATUS.md ${taskId}`;
2082
+ // TP-176: Include segment context in title for multi-segment tasks
2083
+ const segData = resolveActiveSegmentForTask(taskId);
2084
+ if (segData && segData.segmentInfo) {
2085
+ const label = segmentProgressText(segData.segmentInfo);
2086
+ $terminalTitle.textContent = `STATUS.md — ${taskId} · ${label}`;
2087
+ } else {
2088
+ $terminalTitle.textContent = `STATUS.md — ${taskId}`;
2089
+ }
2090
+
1901
2091
  $autoScrollText.textContent = 'Track progress';
1902
2092
  $autoScrollCheckbox.checked = false;
1903
2093
  $terminalPanel.style.display = '';
@@ -1916,11 +2106,22 @@ function pollStatusMd() {
1916
2106
  return r.text();
1917
2107
  })
1918
2108
  .then(text => {
2109
+ // TP-176: Apply segment-scoped filtering for multi-segment tasks.
2110
+ // Re-resolve on each poll since the active segment may change.
2111
+ const segData = resolveActiveSegmentForTask(viewerTarget);
2112
+ let displayText = text;
2113
+ if (segData && segData.repoId) {
2114
+ displayText = filterStatusMdForSegment(text, segData.repoId);
2115
+ // Update title with current segment context (may change between polls)
2116
+ const label = segmentProgressText(segData.segmentInfo);
2117
+ $terminalTitle.textContent = `STATUS.md \u2014 ${viewerTarget} \u00b7 ${label}`;
2118
+ }
2119
+
1919
2120
  // Diff-and-skip: no change, no DOM update
1920
- if (text === lastStatusMdText) return;
1921
- lastStatusMdText = text;
2121
+ if (displayText === lastStatusMdText) return;
2122
+ lastStatusMdText = displayText;
1922
2123
 
1923
- const { html, hasLastChecked } = renderStatusMd(text);
2124
+ const { html, hasLastChecked } = renderStatusMd(displayText);
1924
2125
  $terminalBody.innerHTML = html;
1925
2126
 
1926
2127
  // Update tracking highlight
@@ -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
  }