taskplane 0.24.7 → 0.24.8

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.
@@ -319,6 +319,89 @@ function repoBadgeHtml(repoId, extraClass) {
319
319
  return `<span class="repo-badge ${extraClass || ""}" title="Repo: ${escapeHtml(repoId)}">${escapeHtml(repoId)}</span>`;
320
320
  }
321
321
 
322
+ function parseSegmentId(segmentId) {
323
+ if (!segmentId || typeof segmentId !== "string") return null;
324
+ const sep = segmentId.indexOf("::");
325
+ if (sep <= 0 || sep >= segmentId.length - 2) return null;
326
+ return {
327
+ taskId: segmentId.slice(0, sep),
328
+ repoId: segmentId.slice(sep + 2),
329
+ };
330
+ }
331
+
332
+ function segmentProgressText(segmentInfo) {
333
+ if (!segmentInfo) return "";
334
+ const repo = segmentInfo.repoId || "unknown";
335
+ if (segmentInfo.index && segmentInfo.total) {
336
+ return `Segment ${segmentInfo.index}/${segmentInfo.total}: ${repo}`;
337
+ }
338
+ return `Segment: ${repo}`;
339
+ }
340
+
341
+ function buildSegmentStatusMap(batch) {
342
+ const map = new Map();
343
+ for (const seg of (batch?.segments || [])) {
344
+ if (seg && typeof seg.segmentId === "string") {
345
+ map.set(seg.segmentId, seg.status || "pending");
346
+ }
347
+ }
348
+ return map;
349
+ }
350
+
351
+ function taskSegmentProgress(task, segmentStatusMap, forcedActiveSegmentId) {
352
+ const segmentIds = Array.isArray(task?.segmentIds)
353
+ ? task.segmentIds.filter(id => typeof id === "string")
354
+ : [];
355
+ // Repo-singleton (or repo-mode) tasks should stay visually clean.
356
+ if (segmentIds.length <= 1) return null;
357
+
358
+ const activeSegmentId = forcedActiveSegmentId || task.activeSegmentId;
359
+ let currentSegmentId = activeSegmentId && segmentIds.includes(activeSegmentId)
360
+ ? activeSegmentId
361
+ : null;
362
+
363
+ if (!currentSegmentId) {
364
+ if (task.status === "pending" || task.status === "running") {
365
+ currentSegmentId = segmentIds.find((id) => {
366
+ const status = segmentStatusMap.get(id);
367
+ return !["succeeded", "failed", "stalled", "skipped"].includes(status);
368
+ }) || segmentIds[segmentIds.length - 1];
369
+ } else {
370
+ currentSegmentId = segmentIds[segmentIds.length - 1];
371
+ }
372
+ }
373
+
374
+ const idx = Math.max(0, segmentIds.indexOf(currentSegmentId));
375
+ const parsed = parseSegmentId(currentSegmentId);
376
+ return {
377
+ index: idx + 1,
378
+ total: segmentIds.length,
379
+ repoId: parsed?.repoId || taskRepoId(task) || undefined,
380
+ segmentId: currentSegmentId,
381
+ };
382
+ }
383
+
384
+ function laneActiveSegmentInfo(v2snap, laneTasks, segmentStatusMap) {
385
+ if (!v2snap || !v2snap.segmentId) return null;
386
+ const parsed = parseSegmentId(v2snap.segmentId);
387
+ if (!parsed) return null;
388
+
389
+ const ownerTaskId = v2snap.taskId || parsed.taskId;
390
+ const ownerTask = (laneTasks || []).find(t => t.taskId === ownerTaskId) || null;
391
+ if (ownerTask) {
392
+ const byTask = taskSegmentProgress(ownerTask, segmentStatusMap, v2snap.segmentId);
393
+ if (byTask) return byTask;
394
+ return null;
395
+ }
396
+
397
+ return {
398
+ index: null,
399
+ total: null,
400
+ repoId: parsed.repoId,
401
+ segmentId: v2snap.segmentId,
402
+ };
403
+ }
404
+
322
405
  // Repo filter change handler
323
406
  $repoFilter.addEventListener("change", (e) => {
324
407
  selectedRepo = e.target.value;
@@ -523,12 +606,16 @@ function renderLanesTasks(batch, sessions) {
523
606
  // TP-107: V2 lane snapshots take precedence over legacy lane states when present
524
607
  const v2Snapshots = currentData?.runtimeLaneSnapshots || {};
525
608
  const showRepos = knownRepos.length >= 2;
609
+ const segmentStatusMap = buildSegmentStatusMap(batch);
526
610
  let html = "";
527
611
 
528
612
  for (const lane of batch.lanes) {
613
+ const laneTasks = (lane.taskIds || []).map(tid => tasks.find(t => t.taskId === tid)).filter(Boolean);
614
+ const v2snap = v2Snapshots[lane.laneNumber] || null;
615
+ const laneActiveSegment = laneActiveSegmentInfo(v2snap, laneTasks, segmentStatusMap);
616
+
529
617
  // Repo filtering: if a repo is selected, skip lanes that don't match
530
618
  if (selectedRepo && showRepos) {
531
- const laneTasks = (lane.taskIds || []).map(tid => tasks.find(t => t.taskId === tid)).filter(Boolean);
532
619
  const laneMatchesRepo = (lane.repoId === selectedRepo) ||
533
620
  laneTasks.some(t => (taskRepoId(t) || lane.repoId) === selectedRepo);
534
621
  if (!laneMatchesRepo) continue;
@@ -550,6 +637,9 @@ function renderLanesTasks(batch, sessions) {
550
637
  if (showRepos && lane.repoId) {
551
638
  html += ` ${repoBadgeHtml(lane.repoId, "repo-badge-lane")}`;
552
639
  }
640
+ if (laneActiveSegment) {
641
+ html += ` <span class="lane-segment" title="${escapeHtml(laneActiveSegment.segmentId || segmentProgressText(laneActiveSegment))}">${escapeHtml(segmentProgressText(laneActiveSegment))}</span>`;
642
+ }
553
643
  html += ` </div>`;
554
644
  html += ` <div class="lane-right">`;
555
645
  html += ` <span class="session-dot ${alive ? "alive" : "dead"}" title="${alive ? "session alive" : "session not active"}"></span>`;
@@ -565,15 +655,12 @@ function renderLanesTasks(batch, sessions) {
565
655
  html += `</div>`;
566
656
 
567
657
  // Task rows for this lane
568
- const laneTasks = (lane.taskIds || []).map(tid => tasks.find(t => t.taskId === tid)).filter(Boolean);
569
-
570
658
  if (laneTasks.length === 0) {
571
659
  html += `<div class="task-row"><span class="task-icon"></span><span style="color:var(--text-faint);grid-column:2/-1;">No tasks assigned</span></div>`;
572
660
  }
573
661
 
574
662
  // Get lane state and telemetry for worker stats
575
663
  // TP-107: V2 lane snapshots take precedence when present
576
- const v2snap = v2Snapshots[lane.laneNumber] || null;
577
664
  const legacyLs = laneStates[laneSessionId] || null;
578
665
  const ls = v2snap ? mergeV2LaneSnapshot(legacyLs, v2snap) : legacyLs;
579
666
  const tel = telemetry[laneSessionId] || null;
@@ -587,6 +674,9 @@ function renderLanesTasks(batch, sessions) {
587
674
  const dur = task.startedAt
588
675
  ? formatDuration((task.endedAt || Date.now()) - task.startedAt)
589
676
  : "—";
677
+ const segmentInfo = taskSegmentProgress(task, segmentStatusMap, null);
678
+ const packetHomeRepo = typeof task.packetRepoId === "string" ? task.packetRepoId : "";
679
+ const showPacketHome = !!packetHomeRepo && packetHomeRepo !== (tRepo || lane.repoId || "");
590
680
 
591
681
  // Progress cell
592
682
  let progressHtml = "";
@@ -629,6 +719,17 @@ function renderLanesTasks(batch, sessions) {
629
719
  stepHtml = `<span style="color:var(--text-faint)">${escapeHtml(task.exitReason || "—")}</span>`;
630
720
  }
631
721
 
722
+ const detailBits = [];
723
+ if (segmentInfo) {
724
+ detailBits.push(`<span class="task-segment-progress" title="${escapeHtml(segmentInfo.segmentId || segmentProgressText(segmentInfo))}">${escapeHtml(segmentProgressText(segmentInfo))}</span>`);
725
+ }
726
+ if (showPacketHome) {
727
+ detailBits.push(`<span class="task-packet-home" title="Task packet home repo">packet: ${escapeHtml(packetHomeRepo)}</span>`);
728
+ }
729
+ if (detailBits.length > 0) {
730
+ stepHtml = `${detailBits.join('<span class="task-detail-sep"> · </span>')}<span class="task-detail-sep"> · </span><span class="task-step-main">${stepHtml}</span>`;
731
+ }
732
+
632
733
  // Worker stats from lane state sidecar + telemetry badges
633
734
  let workerHtml = "";
634
735
  // Reviewer sub-row should only appear under the active running task in this lane.
@@ -507,6 +507,23 @@ body {
507
507
  text-overflow: ellipsis;
508
508
  }
509
509
 
510
+ .lane-segment {
511
+ display: inline-flex;
512
+ align-items: center;
513
+ gap: 4px;
514
+ font-family: var(--font-mono);
515
+ font-size: 0.68rem;
516
+ font-weight: 600;
517
+ padding: 2px 7px;
518
+ border-radius: 8px;
519
+ background: var(--badge-running-bg);
520
+ color: var(--accent);
521
+ max-width: 220px;
522
+ overflow: hidden;
523
+ text-overflow: ellipsis;
524
+ white-space: nowrap;
525
+ }
526
+
510
527
  .lane-right {
511
528
  display: flex;
512
529
  align-items: center;
@@ -610,6 +627,25 @@ body {
610
627
  text-overflow: ellipsis;
611
628
  }
612
629
 
630
+ .task-segment-progress {
631
+ font-family: var(--font-mono);
632
+ color: var(--accent);
633
+ }
634
+
635
+ .task-packet-home {
636
+ font-family: var(--font-mono);
637
+ color: var(--magenta);
638
+ }
639
+
640
+ .task-detail-sep {
641
+ color: var(--text-faint);
642
+ margin: 0 2px;
643
+ }
644
+
645
+ .task-step-main {
646
+ color: var(--text-muted);
647
+ }
648
+
613
649
  .task-iter {
614
650
  font-family: var(--font-mono);
615
651
  font-size: 0.7rem;
@@ -217,7 +217,7 @@ export default function (pi: ExtensionAPI) {
217
217
  }
218
218
 
219
219
  function reviewerStatePath(taskFolder: string): string {
220
- return join(taskFolder, ".reviewer-state.json");
220
+ return process.env.TASKPLANE_REVIEWER_STATE_PATH || join(taskFolder, ".reviewer-state.json");
221
221
  }
222
222
 
223
223
  function writeReviewerState(taskFolder: string, state: {
@@ -421,8 +421,9 @@ export default function (pi: ExtensionAPI) {
421
421
 
422
422
  // Find task folder and paths
423
423
  const taskFolder = process.env.TASKPLANE_TASK_FOLDER || cwd;
424
- const statusPath = join(taskFolder, "STATUS.md");
425
- const reviewsDir = join(taskFolder, ".reviews");
424
+ const statusPath = process.env.TASKPLANE_STATUS_PATH || join(taskFolder, "STATUS.md");
425
+ const promptPath = process.env.TASKPLANE_PROMPT_PATH || join(taskFolder, "PROMPT.md");
426
+ const reviewsDir = process.env.TASKPLANE_REVIEWS_DIR || join(taskFolder, ".reviews");
426
427
  if (!existsSync(reviewsDir)) mkdirSync(reviewsDir, { recursive: true });
427
428
 
428
429
  // Read review counter from STATUS.md
@@ -450,7 +451,6 @@ export default function (pi: ExtensionAPI) {
450
451
  } catch { /* use default */ }
451
452
 
452
453
  // Generate review request prompt
453
- const promptPath = join(taskFolder, "PROMPT.md");
454
454
  const projectName = process.env.TASKPLANE_PROJECT_NAME || "project";
455
455
  const diffCmd = baseline ? `git diff ${baseline}..HEAD` : `git diff`;
456
456
  const diffNamesCmd = baseline ? `git diff ${baseline}..HEAD --name-only` : `git diff --name-only`;