Revert "fix: Two-column dashboard layout with task checklist (#1195)" (#1254)

This reverts commit d3780c9bdb.
This commit is contained in:
TÂCHES 2026-03-18 15:02:01 -06:00 committed by GitHub
parent 35cee7b05f
commit 4d9aef5705
2 changed files with 62 additions and 391 deletions

View file

@ -1,208 +0,0 @@
/**
* Visual preview of the auto-mode dashboard widget.
* Run: npx tsx scripts/preview-dashboard.ts [width]
*
* Renders the two-column layout with mock data so you can see
* exactly how it looks at any terminal width.
*/
import { truncateToWidth, visibleWidth } from "@gsd/pi-tui";
import { makeUI, GLYPH, INDENT } from "../src/resources/extensions/shared/mod.js";
// ── Minimal ANSI color theme (no Theme class dependency) ────────────────
const COLORS: Record<string, string> = {
accent: "\x1b[36m", // cyan
dim: "\x1b[2m", // dim
text: "\x1b[37m", // white
success: "\x1b[32m", // green
error: "\x1b[31m", // red
warning: "\x1b[33m", // yellow
muted: "\x1b[90m", // gray
};
const RESET_FG = "\x1b[22m\x1b[39m";
const theme = {
fg(color: string, text: string): string {
const ansi = COLORS[color] ?? COLORS.text;
return `${ansi}${text}${RESET_FG}`;
},
bold(text: string): string {
return `\x1b[1m${text}\x1b[22m`;
},
};
// ── Mock data ───────────────────────────────────────────────────────────
const mockTasks = [
{ id: "T01", title: "Core type definitions & interfaces", done: true },
{ id: "T02", title: "Database schema migration", done: true },
{ id: "T03", title: "API route handlers", done: true },
{ id: "T04", title: "Authentication middleware", done: false },
{ id: "T05", title: "Unit & integration tests", done: false },
{ id: "T06", title: "Documentation updates", done: false },
];
const currentTaskId = "T04";
const milestoneTitle = "Core Patching Daemon";
const sliceId = "S04";
const sliceTitle = "CI gate";
const unitId = "M001-07dqzj/S04";
const verb = "completing";
const phaseLabel = "COMPLETE";
const modeTag = "AUTO";
const elapsed = "1h 23m";
const slicesDone = 3;
const slicesTotal = 6;
const taskNum = 4;
const taskTotal = 6;
const eta = "~1h 47m remaining";
const nextStep = "reassess roadmap";
const pwd = "~/Github/git-patcher/.gsd/worktrees/M001-07dqzj (milestone/M001-07dqzj)";
const tokenStats = "↑22 ↓11k R1.1M W38k ⚡85% $18.668";
const contextStats = "35.2%/200k";
const modelDisplay = "anthropic/claude-opus-4-6";
// ── Render helpers ──────────────────────────────────────────────────────
function rightAlign(left: string, right: string, width: number): string {
const leftVis = visibleWidth(left);
const rightVis = visibleWidth(right);
const gap = Math.max(1, width - leftVis - rightVis);
return truncateToWidth(left + " ".repeat(gap) + right, width);
}
function padToWidth(s: string, colWidth: number): string {
const vis = visibleWidth(s);
if (vis >= colWidth) return truncateToWidth(s, colWidth);
return s + " ".repeat(colWidth - vis);
}
// ── Render ──────────────────────────────────────────────────────────────
function render(width: number): string[] {
const ui = makeUI(theme as any, width);
const lines: string[] = [];
const pad = INDENT.base;
// Top bar
lines.push(...ui.bar());
// Header: GSD AUTO ... elapsed
const dot = theme.fg("accent", GLYPH.statusActive);
const headerLeft = `${pad}${dot} ${theme.fg("accent", theme.bold("GSD"))} ${theme.fg("success", modeTag)}`;
const headerRight = theme.fg("dim", elapsed);
lines.push(rightAlign(headerLeft, headerRight, width));
// Context line: project · slice · action
const contextParts = [
theme.fg("dim", milestoneTitle),
theme.fg("text", theme.bold(`${sliceId}: ${sliceTitle}`)),
`${theme.fg("accent", "▸")} ${theme.fg("accent", verb)} ${theme.fg("text", unitId)}`,
];
const phaseBadge = theme.fg("dim", phaseLabel);
const contextLine = contextParts.join(theme.fg("dim", " · "));
lines.push(rightAlign(`${pad}${contextLine}`, phaseBadge, width));
// Column sizing: left flexes, right fixed. Task list sits center-right.
const minTwoColWidth = 100;
const rightColFixed = 44;
const colGap = 5;
const useTwoCol = width >= minTwoColWidth;
const rightColWidth = useTwoCol ? rightColFixed : 0;
const leftColWidth = useTwoCol ? width - rightColWidth - colGap : width;
// Left column: progress, ETA, next, stats
const leftLines: string[] = [];
const barWidth = Math.max(6, Math.min(18, Math.floor(leftColWidth * 0.4)));
const pct = slicesDone / slicesTotal;
const filled = Math.round(pct * barWidth);
const bar = theme.fg("success", "█".repeat(filled))
+ theme.fg("dim", "░".repeat(barWidth - filled));
const meta = theme.fg("dim", `${slicesDone}/${slicesTotal} slices`)
+ theme.fg("dim", ` · task ${taskNum}/${taskTotal}`);
leftLines.push(truncateToWidth(`${pad}${bar} ${meta}`, leftColWidth));
leftLines.push(truncateToWidth(`${pad}${theme.fg("dim", eta)}`, leftColWidth));
leftLines.push(truncateToWidth(
`${pad}${theme.fg("dim", "→")} ${theme.fg("dim", `then ${nextStep}`)}`,
leftColWidth,
));
leftLines.push(truncateToWidth(
`${pad}${theme.fg("dim", tokenStats)} ${theme.fg("dim", contextStats)}`,
leftColWidth,
));
leftLines.push(truncateToWidth(`${pad}${theme.fg("dim", modelDisplay)}`, leftColWidth));
// Right column: task checklist (pegged to right edge)
const rightLines: string[] = [];
const rpad = " ";
if (useTwoCol) {
for (const t of mockTasks) {
const isCurrent = t.id === currentTaskId;
const glyph = t.done
? theme.fg("success", GLYPH.statusDone)
: isCurrent
? theme.fg("accent", "▸")
: theme.fg("dim", " ");
const label = isCurrent
? theme.fg("text", `${t.id}: ${t.title}`)
: t.done
? theme.fg("dim", `${t.id}: ${t.title}`)
: theme.fg("text", `${t.id}: ${t.title}`);
rightLines.push(truncateToWidth(`${rpad}${glyph} ${label}`, rightColWidth));
}
} else {
// Narrow: tasks + progress inline
for (const t of mockTasks) {
const isCurrent = t.id === currentTaskId;
const glyph = t.done
? theme.fg("success", GLYPH.statusDone)
: isCurrent
? theme.fg("accent", "▸")
: theme.fg("dim", " ");
const label = isCurrent
? theme.fg("text", `${t.id}: ${t.title}`)
: t.done
? theme.fg("dim", `${t.id}: ${t.title}`)
: theme.fg("text", `${t.id}: ${t.title}`);
leftLines.push(truncateToWidth(`${pad}${glyph} ${label}`, leftColWidth));
}
}
// Compose columns
if (useTwoCol) {
const divider = theme.fg("dim", "│");
const maxRows = Math.max(leftLines.length, rightLines.length);
lines.push("");
for (let i = 0; i < maxRows; i++) {
const left = padToWidth(leftLines[i] ?? "", leftColWidth);
const gap = " ".repeat(colGap - 2);
const right = rightLines[i] ?? "";
lines.push(truncateToWidth(`${left}${gap}${divider} ${right}`, width));
}
} else {
lines.push("");
for (const l of leftLines) lines.push(l);
}
// Footer
lines.push("");
const hintStr = theme.fg("dim", "esc pause | ⌃⌥G dashboard");
const pwdStr = theme.fg("dim", pwd);
lines.push(rightAlign(`${pad}${pwdStr}`, hintStr, width));
lines.push(...ui.bar());
return lines;
}
// ── Main ────────────────────────────────────────────────────────────────
const width = parseInt(process.argv[2] ?? "", 10) || process.stdout.columns || 100;
console.log(`\nPreview at width=${width}:\n`);
for (const line of render(width)) {
console.log(line);
}
console.log();

View file

@ -205,13 +205,6 @@ export function estimateTimeRemaining(): string | null {
// ─── Slice Progress Cache ─────────────────────────────────────────────────────
/** Cached task detail for the widget task checklist */
interface CachedTaskDetail {
id: string;
title: string;
done: boolean;
}
/** Cached slice progress for the widget — avoid async in render */
let cachedSliceProgress: {
done: number;
@ -219,8 +212,6 @@ let cachedSliceProgress: {
milestoneId: string;
/** Real task progress for the active slice, if its plan file exists */
activeSliceTasks: { done: number; total: number } | null;
/** Full task list for the active slice checklist */
taskDetails: CachedTaskDetail[] | null;
} | null = null;
export function updateSliceProgressCache(base: string, mid: string, activeSid?: string): void {
@ -231,7 +222,6 @@ export function updateSliceProgressCache(base: string, mid: string, activeSid?:
const roadmap = parseRoadmap(content);
let activeSliceTasks: { done: number; total: number } | null = null;
let taskDetails: CachedTaskDetail[] | null = null;
if (activeSid) {
try {
const planFile = resolveSliceFile(base, mid, activeSid, "PLAN");
@ -242,7 +232,6 @@ export function updateSliceProgressCache(base: string, mid: string, activeSid?:
done: plan.tasks.filter(t => t.done).length,
total: plan.tasks.length,
};
taskDetails = plan.tasks.map(t => ({ id: t.id, title: t.title, done: t.done }));
}
} catch {
// Non-fatal — just omit task count
@ -254,19 +243,13 @@ export function updateSliceProgressCache(base: string, mid: string, activeSid?:
total: roadmap.slices.length,
milestoneId: mid,
activeSliceTasks,
taskDetails,
};
} catch {
// Non-fatal — widget just won't show progress bar
}
}
export function getRoadmapSlicesSync(): {
done: number;
total: number;
activeSliceTasks: { done: number; total: number } | null;
taskDetails: CachedTaskDetail[] | null;
} | null {
export function getRoadmapSlicesSync(): { done: number; total: number; activeSliceTasks: { done: number; total: number } | null } | null {
return cachedSliceProgress;
}
@ -367,84 +350,87 @@ export function updateProgressWidget(
const lines: string[] = [];
const pad = INDENT.base;
// ── Top bar ─────────────────────────────────────────────────────
// ── Line 1: Top bar ───────────────────────────────────────────────
lines.push(...ui.bar());
// ── Header: GSD AUTO ... elapsed ────────────────────────────────
const dot = pulseBright
? theme.fg("accent", GLYPH.statusActive)
: theme.fg("dim", GLYPH.statusPending);
const elapsed = formatAutoElapsed(accessors.getAutoStartTime());
const modeTag = accessors.isStepMode() ? "NEXT" : "AUTO";
const headerLeft = `${pad}${dot} ${theme.fg("accent", theme.bold("GSD"))} ${theme.fg("success", modeTag)}`;
const headerLeft = `${pad}${dot} ${theme.fg("accent", theme.bold("GSD"))} ${theme.fg("success", modeTag)}`;
const headerRight = elapsed ? theme.fg("dim", elapsed) : "";
lines.push(rightAlign(headerLeft, headerRight, width));
// ── Context: project · slice · action (merged into one line) ────
const contextParts: string[] = [];
if (mid) contextParts.push(theme.fg("dim", mid.title));
if (slice && unitType !== "research-milestone" && unitType !== "plan-milestone") {
contextParts.push(theme.fg("text", theme.bold(`${slice.id}: ${slice.title}`)));
lines.push("");
if (mid) {
lines.push(truncateToWidth(`${pad}${theme.fg("dim", mid.title)}`, width));
}
if (slice && unitType !== "research-milestone" && unitType !== "plan-milestone") {
lines.push(truncateToWidth(
`${pad}${theme.fg("text", theme.bold(`${slice.id}: ${slice.title}`))}`,
width,
));
}
lines.push("");
const isHook = unitType.startsWith("hook/");
const target = isHook
? (unitId.split("/").pop() ?? unitId)
: (task ? `${task.id}: ${task.title}` : unitId);
contextParts.push(`${theme.fg("accent", "▸")} ${theme.fg("accent", verb)} ${theme.fg("text", target)}`);
const actionLeft = `${pad}${theme.fg("accent", "▸")} ${theme.fg("accent", verb)} ${theme.fg("text", target)}`;
const tierTag = tierBadge ? theme.fg("dim", `[${tierBadge}] `) : "";
const phaseBadge = `${tierTag}${theme.fg("dim", phaseLabel)}`;
const contextLine = contextParts.join(theme.fg("dim", " · "));
lines.push(rightAlign(`${pad}${contextLine}`, phaseBadge, width));
lines.push(rightAlign(actionLeft, phaseBadge, width));
lines.push("");
// ── Two-column body ─────────────────────────────────────────────
// Left: progress, ETA, next, stats (fixed) | Right: task checklist (fixed, adjacent)
// Both columns sit left-to-center; empty space is on the right.
const divider = theme.fg("dim", "│");
const minTwoColWidth = 100;
const rightColFixed = 44;
const colGap = 5; // breathing room between columns
// Left column takes remaining space — no truncation on wide terminals
const useTwoCol = width >= minTwoColWidth;
const rightColWidth = useTwoCol ? rightColFixed : 0;
const leftColWidth = useTwoCol ? width - rightColWidth - colGap : width;
if (mid) {
const roadmapSlices = getRoadmapSlicesSync();
if (roadmapSlices) {
const { done, total, activeSliceTasks } = roadmapSlices;
const barWidth = Math.max(8, Math.min(24, Math.floor(width * 0.3)));
const pct = total > 0 ? done / total : 0;
const filled = Math.round(pct * barWidth);
const bar = theme.fg("success", "█".repeat(filled))
+ theme.fg("dim", "░".repeat(barWidth - filled));
const roadmapSlices = mid ? getRoadmapSlicesSync() : null;
let meta = theme.fg("dim", `${done}/${total} slices`);
// Build left column: progress bar, ETA, next step, token stats
const leftLines: string[] = [];
if (activeSliceTasks && activeSliceTasks.total > 0) {
// For hooks, show the trigger task number (done), not the next task (done + 1)
const taskNum = isHook
? Math.max(activeSliceTasks.done, 1)
: Math.min(activeSliceTasks.done + 1, activeSliceTasks.total);
meta += theme.fg("dim", ` · task ${taskNum}/${activeSliceTasks.total}`);
}
if (roadmapSlices) {
const { done, total, activeSliceTasks } = roadmapSlices;
const barWidth = Math.max(6, Math.min(18, Math.floor(leftColWidth * 0.4)));
const pct = total > 0 ? done / total : 0;
const filled = Math.round(pct * barWidth);
const bar = theme.fg("success", "█".repeat(filled))
+ theme.fg("dim", "░".repeat(barWidth - filled));
// ETA estimate
const eta = estimateTimeRemaining();
if (eta) {
meta += theme.fg("dim", ` · ${eta}`);
}
let meta = theme.fg("dim", `${done}/${total} slices`);
if (activeSliceTasks && activeSliceTasks.total > 0) {
const taskNum = isHook
? Math.max(activeSliceTasks.done, 1)
: Math.min(activeSliceTasks.done + 1, activeSliceTasks.total);
meta += theme.fg("dim", ` · task ${taskNum}/${activeSliceTasks.total}`);
}
leftLines.push(truncateToWidth(`${pad}${bar} ${meta}`, leftColWidth));
const eta = estimateTimeRemaining();
if (eta) {
leftLines.push(truncateToWidth(`${pad}${theme.fg("dim", eta)}`, leftColWidth));
lines.push(truncateToWidth(`${pad}${bar} ${meta}`, width));
}
}
lines.push("");
if (next) {
leftLines.push(truncateToWidth(
lines.push(truncateToWidth(
`${pad}${theme.fg("dim", "→")} ${theme.fg("dim", `then ${next}`)}`,
leftColWidth,
width,
));
}
// Token stats
// ── Footer info (pwd, tokens, cost, context, model) ──────────────
lines.push("");
lines.push(truncateToWidth(theme.fg("dim", `${pad}${widgetPwd}`), width, theme.fg("dim", "…")));
// Token stats from current unit session + cumulative cost from metrics
{
const cmdCtx = accessors.getCmdCtx();
let totalInput = 0, totalOutput = 0;
@ -479,6 +465,7 @@ export function updateProgressWidget(
if (totalOutput) sp.push(`${formatWidgetTokens(totalOutput)}`);
if (totalCacheRead) sp.push(`R${formatWidgetTokens(totalCacheRead)}`);
if (totalCacheWrite) sp.push(`W${formatWidgetTokens(totalCacheWrite)}`);
// Cache hit rate for current unit
if (totalCacheRead + totalInput > 0) {
const hitRate = Math.round((totalCacheRead / (totalCacheRead + totalInput)) * 100);
sp.push(`\u26A1${hitRate}%`);
@ -497,134 +484,33 @@ export function updateProgressWidget(
sp.push(cxDisplay);
}
const tokenLine = sp.map(p => p.includes("\x1b[") ? p : theme.fg("dim", p))
const sLeft = sp.map(p => p.includes("\x1b[") ? p : theme.fg("dim", p))
.join(theme.fg("dim", " "));
leftLines.push(truncateToWidth(`${pad}${tokenLine}`, leftColWidth));
const modelId = cmdCtx?.model?.id ?? "";
const modelProvider = cmdCtx?.model?.provider ?? "";
const modelPhase = phaseLabel ? theme.fg("dim", `[${phaseLabel}] `) : "";
const modelDisplay = modelProvider && modelId
? `${modelProvider}/${modelId}`
: modelId;
if (modelDisplay) {
leftLines.push(truncateToWidth(`${pad}${theme.fg("dim", modelDisplay)}`, leftColWidth));
}
const sRight = modelDisplay
? `${modelPhase}${theme.fg("dim", modelDisplay)}`
: "";
lines.push(rightAlign(`${pad}${sLeft}`, sRight, width));
// Dynamic routing savings
// Dynamic routing savings summary
if (mLedger && mLedger.units.some(u => u.tier)) {
const savings = formatTierSavings(mLedger.units);
if (savings) {
leftLines.push(truncateToWidth(`${pad}${theme.fg("dim", savings)}`, leftColWidth));
lines.push(truncateToWidth(theme.fg("dim", `${pad}${savings}`), width));
}
}
}
// Build right column: task checklist (pegged to right edge)
const rightLines: string[] = [];
const taskDetails = roadmapSlices?.taskDetails ?? null;
const maxVisibleTasks = 8;
const rpad = " ";
if (useTwoCol) {
if (taskDetails && taskDetails.length > 0) {
const visibleTasks = taskDetails.slice(0, maxVisibleTasks);
for (const t of visibleTasks) {
const isCurrent = task && t.id === task.id;
const glyph = t.done
? theme.fg("success", GLYPH.statusDone)
: isCurrent
? theme.fg("accent", "▸")
: theme.fg("dim", " ");
const label = isCurrent
? theme.fg("text", `${t.id}: ${t.title}`)
: t.done
? theme.fg("dim", `${t.id}: ${t.title}`)
: theme.fg("text", `${t.id}: ${t.title}`);
rightLines.push(truncateToWidth(`${rpad}${glyph} ${label}`, rightColWidth));
}
if (taskDetails.length > maxVisibleTasks) {
rightLines.push(truncateToWidth(
`${rpad}${theme.fg("dim", ` …+${taskDetails.length - maxVisibleTasks} more`)}`,
rightColWidth,
));
}
} else if (roadmapSlices?.activeSliceTasks) {
const { done: tDone, total: tTotal } = roadmapSlices.activeSliceTasks;
rightLines.push(`${rpad}${theme.fg("dim", `${tDone}/${tTotal} tasks`)}`);
}
} else {
// Narrow single-column: task list goes into left column
if (taskDetails && taskDetails.length > 0) {
for (const t of taskDetails.slice(0, maxVisibleTasks)) {
const isCurrent = task && t.id === task.id;
const glyph = t.done
? theme.fg("success", GLYPH.statusDone)
: isCurrent
? theme.fg("accent", "▸")
: theme.fg("dim", " ");
const label = isCurrent
? theme.fg("text", `${t.id}: ${t.title}`)
: t.done
? theme.fg("dim", `${t.id}: ${t.title}`)
: theme.fg("text", `${t.id}: ${t.title}`);
leftLines.push(truncateToWidth(`${pad}${glyph} ${label}`, leftColWidth));
}
}
// Add progress bar inline
if (roadmapSlices) {
const { done, total, activeSliceTasks } = roadmapSlices;
const barWidth = Math.max(6, Math.min(18, Math.floor(leftColWidth * 0.4)));
const pct = total > 0 ? done / total : 0;
const filled = Math.round(pct * barWidth);
const bar = theme.fg("success", "█".repeat(filled))
+ theme.fg("dim", "░".repeat(barWidth - filled));
let meta = theme.fg("dim", `${done}/${total} slices`);
if (activeSliceTasks && activeSliceTasks.total > 0) {
const taskNum = isHook
? Math.max(activeSliceTasks.done, 1)
: Math.min(activeSliceTasks.done + 1, activeSliceTasks.total);
meta += theme.fg("dim", ` · task ${taskNum}/${activeSliceTasks.total}`);
}
const eta = estimateTimeRemaining();
if (eta) meta += theme.fg("dim", ` · ${eta}`);
leftLines.push(truncateToWidth(`${pad}${bar} ${meta}`, leftColWidth));
}
if (next) {
leftLines.push(truncateToWidth(
`${pad}${theme.fg("dim", "→")} ${theme.fg("dim", `then ${next}`)}`,
leftColWidth,
));
}
}
// Compose columns
if (useTwoCol) {
const maxRows = Math.max(leftLines.length, rightLines.length);
if (maxRows > 0) {
lines.push(""); // spacer before columns
for (let i = 0; i < maxRows; i++) {
const left = padToWidth(leftLines[i] ?? "", leftColWidth);
const gap = " ".repeat(colGap - 2); // colGap minus divider and its trailing space
const right = rightLines[i] ?? "";
lines.push(truncateToWidth(`${left}${gap}${divider} ${right}`, width));
}
}
} else {
// Narrow single-column: just stack
if (leftLines.length > 0) {
lines.push("");
for (const l of leftLines) lines.push(l);
}
}
// ── Footer: pwd + hints ─────────────────────────────────────────
lines.push("");
const hintParts: string[] = [];
hintParts.push("esc pause");
hintParts.push(process.platform === "darwin" ? "⌃⌥G dashboard" : "Ctrl+Alt+G dashboard");
const hintStr = theme.fg("dim", hintParts.join(" | "));
const pwdStr = theme.fg("dim", widgetPwd);
lines.push(rightAlign(`${pad}${pwdStr}`, hintStr, width));
lines.push(...ui.hints(hintParts));
lines.push(...ui.bar());
@ -742,10 +628,3 @@ function rightAlign(left: string, right: string, width: number): string {
const gap = Math.max(1, width - leftVis - rightVis);
return truncateToWidth(left + " ".repeat(gap) + right, width);
}
/** Pad a string with trailing spaces to fill exactly `colWidth` (ANSI-aware). */
function padToWidth(s: string, colWidth: number): string {
const vis = visibleWidth(s);
if (vis >= colWidth) return truncateToWidth(s, colWidth);
return s + " ".repeat(colWidth - vis);
}