taskplane 0.27.0 → 0.28.0

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.
@@ -699,21 +699,30 @@ function renderLanesTasks(batch, sessions) {
699
699
  const showPacketHome = !!packetHomeRepo && packetHomeRepo !== (tRepo || lane.repoId || "");
700
700
 
701
701
  // Progress cell
702
+ // TP-174: Prefer V2 snapshot progress (segment-scoped when available)
703
+ // over full STATUS.md counts when the task is actively running on this lane.
704
+ // TP-176: Succeeded tasks always show 100% regardless of sidecar/statusData (#491).
702
705
  let progressHtml = "";
703
- if (sd) {
704
- const fillClass = pctClass(sd.progress);
706
+ const v2p = ls && ls._v2Progress;
707
+ const useV2 = v2p && v2p.total > 0 && ls.taskId === task.taskId;
708
+ if (task.status === "succeeded") {
709
+ // #491 fix: succeeded tasks always show 100%
705
710
  progressHtml = `
706
711
  <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>
712
+ <div class="task-progress-bar"><div class="task-progress-fill pct-hi" style="width:100%"></div></div>
713
+ <span class="task-progress-text">100%</span>
711
714
  </div>`;
712
- } else if (task.status === "succeeded") {
715
+ } else if (sd || useV2) {
716
+ const displayChecked = useV2 ? v2p.checked : (sd ? sd.checked : 0);
717
+ const displayTotal = useV2 ? v2p.total : (sd ? sd.total : 0);
718
+ const displayProgress = displayTotal > 0 ? Math.round((displayChecked / displayTotal) * 100) : 0;
719
+ const fillClass = pctClass(displayProgress);
713
720
  progressHtml = `
714
721
  <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>
722
+ <div class="task-progress-bar">
723
+ <div class="task-progress-fill ${fillClass}" style="width:${displayProgress}%"></div>
724
+ </div>
725
+ <span class="task-progress-text">${displayProgress}% ${displayChecked}/${displayTotal}</span>
717
726
  </div>`;
718
727
  } else if (task.status === "pending") {
719
728
  progressHtml = `
@@ -1881,6 +1890,127 @@ function renderV2Event(evt) {
1881
1890
  }
1882
1891
  }
1883
1892
 
1893
+ // ── Segment-Scoped STATUS.md Helpers (TP-176) ──────────────────────────────
1894
+
1895
+ /**
1896
+ * Resolve the active segment repoId for a given task.
1897
+ * Uses runtimeLaneSnapshots (active segment) and falls back to
1898
+ * taskSegmentProgress (batch state).
1899
+ * Returns { repoId, segmentInfo } or null if single-segment / unresolvable.
1900
+ */
1901
+ function resolveActiveSegmentForTask(taskId) {
1902
+ if (!currentData) return null;
1903
+ const batch = currentData.batch;
1904
+ if (!batch) return null;
1905
+ const task = (batch.tasks || []).find(t => t.taskId === taskId);
1906
+ if (!task) return null;
1907
+ const segmentIds = Array.isArray(task.segmentIds) ? task.segmentIds.filter(id => typeof id === "string") : [];
1908
+ if (segmentIds.length <= 1) return null; // single-segment or no segments
1909
+
1910
+ // Try to get active segment from runtime lane snapshots
1911
+ const v2Snapshots = currentData.runtimeLaneSnapshots || {};
1912
+ for (const snap of Object.values(v2Snapshots)) {
1913
+ if (snap && snap.taskId === taskId && snap.segmentId) {
1914
+ const parsed = parseSegmentId(snap.segmentId);
1915
+ if (parsed) {
1916
+ const idx = segmentIds.indexOf(snap.segmentId);
1917
+ return {
1918
+ repoId: parsed.repoId,
1919
+ segmentInfo: {
1920
+ index: idx >= 0 ? idx + 1 : null,
1921
+ total: segmentIds.length,
1922
+ repoId: parsed.repoId,
1923
+ segmentId: snap.segmentId,
1924
+ },
1925
+ };
1926
+ }
1927
+ }
1928
+ }
1929
+
1930
+ // Fallback: use taskSegmentProgress (batch state)
1931
+ const segmentStatusMap = buildSegmentStatusMap(batch);
1932
+ const info = taskSegmentProgress(task, segmentStatusMap, null);
1933
+ if (info && info.repoId) {
1934
+ return { repoId: info.repoId, segmentInfo: info };
1935
+ }
1936
+ return null;
1937
+ }
1938
+
1939
+ /**
1940
+ * Filter STATUS.md content to show only the active segment's blocks.
1941
+ * Within each `### Step N:` section, removes `#### Segment: <otherRepo>` blocks
1942
+ * and keeps only the block matching `activeRepoId`.
1943
+ * Non-step content (metadata, notes, reviews, etc.) is preserved.
1944
+ *
1945
+ * Returns the filtered markdown string, or the original if no segment markers found.
1946
+ */
1947
+ function filterStatusMdForSegment(markdown, activeRepoId) {
1948
+ if (!activeRepoId) return markdown;
1949
+ const lines = markdown.split('\n');
1950
+ const result = [];
1951
+ let inStep = false; // inside a ### Step section
1952
+ let inSegmentBlock = false; // inside a #### Segment: <repo> block
1953
+ let segmentMatch = false; // current segment block matches active repo
1954
+ let foundAnySegmentHeader = false;
1955
+
1956
+ for (let i = 0; i < lines.length; i++) {
1957
+ const line = lines[i];
1958
+
1959
+ // Detect step headers: ### Step N: ...
1960
+ if (/^###\s+Step\s+\d+/.test(line)) {
1961
+ inStep = true;
1962
+ inSegmentBlock = false;
1963
+ segmentMatch = false;
1964
+ result.push(line);
1965
+ continue;
1966
+ }
1967
+
1968
+ // Detect non-step ### headers (e.g., ### Reviews, ### Notes)
1969
+ if (/^###\s+/.test(line) && !/^###\s+Step\s+\d+/.test(line)) {
1970
+ inStep = false;
1971
+ inSegmentBlock = false;
1972
+ segmentMatch = false;
1973
+ result.push(line);
1974
+ continue;
1975
+ }
1976
+
1977
+ // Inside a step section, detect #### Segment: <repoId> headers
1978
+ if (inStep && /^####\s+Segment:\s*/.test(line)) {
1979
+ foundAnySegmentHeader = true;
1980
+ const segRepo = line.replace(/^####\s+Segment:\s*/, '').trim();
1981
+ inSegmentBlock = true;
1982
+ segmentMatch = (segRepo === activeRepoId);
1983
+ if (segmentMatch) {
1984
+ result.push(line);
1985
+ }
1986
+ continue;
1987
+ }
1988
+
1989
+ // Detect any other #### header (ends current segment block)
1990
+ if (/^####\s+/.test(line)) {
1991
+ inSegmentBlock = false;
1992
+ segmentMatch = false;
1993
+ result.push(line);
1994
+ continue;
1995
+ }
1996
+
1997
+ // If we're in a segment block, only include matching lines
1998
+ if (inSegmentBlock) {
1999
+ if (segmentMatch) {
2000
+ result.push(line);
2001
+ }
2002
+ continue;
2003
+ }
2004
+
2005
+ // Outside segment blocks: keep the line
2006
+ result.push(line);
2007
+ }
2008
+
2009
+ // If no segment headers were found, return original (fallback for single-segment)
2010
+ if (!foundAnySegmentHeader) return markdown;
2011
+ return result.join('\n');
2012
+ }
2013
+
1884
2014
  // ── Open STATUS.md viewer ───────────────────────────────────────────────────
1885
2015
 
1886
2016
  function viewStatusMd(taskId) {
@@ -1897,7 +2027,15 @@ function viewStatusMd(taskId) {
1897
2027
  autoScrollOn = false;
1898
2028
  lastStatusMdText = '';
1899
2029
 
1900
- $terminalTitle.textContent = `STATUS.md ${taskId}`;
2030
+ // TP-176: Include segment context in title for multi-segment tasks
2031
+ const segData = resolveActiveSegmentForTask(taskId);
2032
+ if (segData && segData.segmentInfo) {
2033
+ const label = segmentProgressText(segData.segmentInfo);
2034
+ $terminalTitle.textContent = `STATUS.md — ${taskId} · ${label}`;
2035
+ } else {
2036
+ $terminalTitle.textContent = `STATUS.md — ${taskId}`;
2037
+ }
2038
+
1901
2039
  $autoScrollText.textContent = 'Track progress';
1902
2040
  $autoScrollCheckbox.checked = false;
1903
2041
  $terminalPanel.style.display = '';
@@ -1916,11 +2054,22 @@ function pollStatusMd() {
1916
2054
  return r.text();
1917
2055
  })
1918
2056
  .then(text => {
2057
+ // TP-176: Apply segment-scoped filtering for multi-segment tasks.
2058
+ // Re-resolve on each poll since the active segment may change.
2059
+ const segData = resolveActiveSegmentForTask(viewerTarget);
2060
+ let displayText = text;
2061
+ if (segData && segData.repoId) {
2062
+ displayText = filterStatusMdForSegment(text, segData.repoId);
2063
+ // Update title with current segment context (may change between polls)
2064
+ const label = segmentProgressText(segData.segmentInfo);
2065
+ $terminalTitle.textContent = `STATUS.md \u2014 ${viewerTarget} \u00b7 ${label}`;
2066
+ }
2067
+
1919
2068
  // Diff-and-skip: no change, no DOM update
1920
- if (text === lastStatusMdText) return;
1921
- lastStatusMdText = text;
2069
+ if (displayText === lastStatusMdText) return;
2070
+ lastStatusMdText = displayText;
1922
2071
 
1923
- const { html, hasLastChecked } = renderStatusMd(text);
2072
+ const { html, hasLastChecked } = renderStatusMd(displayText);
1924
2073
  $terminalBody.innerHTML = html;
1925
2074
 
1926
2075
  // Update tracking highlight