diff --git a/scripts/preview-dashboard.ts b/scripts/preview-dashboard.ts deleted file mode 100644 index 5cf34428f..000000000 --- a/scripts/preview-dashboard.ts +++ /dev/null @@ -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 = { - 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(); diff --git a/src/resources/extensions/gsd/auto-dashboard.ts b/src/resources/extensions/gsd/auto-dashboard.ts index b738dfa71..d3da5502d 100644 --- a/src/resources/extensions/gsd/auto-dashboard.ts +++ b/src/resources/extensions/gsd/auto-dashboard.ts @@ -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); -}