/** * Visual preview of the auto-mode dashboard widget. * Run: npx tsx scripts/preview-dashboard.ts [width] [--no-milestone] [--narrow] [--unhealthy] * * Renders the two-column layout with mock data so you can see * exactly how it looks at any terminal width. * * Examples: * npx tsx scripts/preview-dashboard.ts # default 120 cols, with milestone * npx tsx scripts/preview-dashboard.ts 80 # narrow single-column * npx tsx scripts/preview-dashboard.ts --no-milestone # compact no-milestone view * npx tsx scripts/preview-dashboard.ts --unhealthy # yellow/red health states * npx tsx scripts/preview-dashboard.ts --narrow # force 80 cols */ import { truncateToWidth, visibleWidth } from "@singularity-forge/pi-tui"; import { GLYPH, INDENT, makeUI, } 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`; }, }; // ── CLI args ──────────────────────────────────────────────────────────── const args = process.argv.slice(2); const noMilestone = args.includes("--no-milestone"); const forceNarrow = args.includes("--narrow"); const unhealthy = args.includes("--unhealthy"); const modeArg = args.find((a) => ["--small", "--min"].includes(a)); const widgetMode = modeArg === "--small" ? "small" : modeArg === "--min" ? "min" : "full"; const widthArg = args.find((a) => /^\d+$/.test(a)); const width = forceNarrow ? 80 : parseInt(widthArg ?? "", 10) || process.stdout.columns || 120; // ── 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 = noMilestone ? "some-unit-id" : "M001-07dqzj/S04"; const verb = noMilestone ? "executing" : "completing"; const phaseLabel = noMilestone ? "EXECUTE" : "COMPLETE"; const modeTag = "AUTO"; const elapsed = "1h 23m"; const slicesDone = 3; const slicesTotal = 6; const taskNum = 4; const taskTotal = 6; const etaShort = "~47m left"; const pwd = noMilestone ? "my-project (main)" : "worktrees/M001 (\u2387 M001-07dqzj)"; // Mock token/cost stats — simplified 3 items const mockHitRate = 85; const mockCost = "$18.67"; const mockCtxUsage = "35%/200k"; const modelDisplay = "anthropic/claude-opus-4-6"; // Mock last commit const lastCommitTimeAgo = "3m"; const lastCommitMessage = "fix auth middleware"; // Health states const healthStates = unhealthy ? [ { icon: "!", color: "warning", summary: "Struggling — 2 consecutive error unit(s)", }, { icon: "x", color: "error", summary: "Stuck — 4 consecutive error units", }, ] : [{ icon: "o", color: "success", summary: "Progressing well" }]; // ── Render helpers ────────────────────────────────────────────────────── function rightAlign(left: string, right: string, w: number): string { const leftVis = visibleWidth(left); const rightVis = visibleWidth(right); const gap = Math.max(1, w - leftVis - rightVis); return truncateToWidth(left + " ".repeat(gap) + right, w); } 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( w: number, healthState: { icon: string; color: string; summary: string }, ): string[] { const ui = makeUI(theme as any, w); const lines: string[] = []; const pad = INDENT.base; // Top bar lines.push(...ui.bar()); // Header: SF AUTO + health ... elapsed + ETA const dot = theme.fg("accent", GLYPH.statusActive); const healthIcon = healthState.color === "success" ? "o" : healthState.color === "warning" ? "!" : "x"; const healthStr = ` ${theme.fg(healthState.color, healthIcon)} ${theme.fg(healthState.color, healthState.summary)}`; const headerLeft = `${pad}${dot} ${theme.fg("accent", theme.bold("SF"))} ${theme.fg("success", modeTag)}${healthStr}`; const headerRight = `${theme.fg("dim", elapsed)} ${theme.fg("dim", "·")} ${theme.fg("dim", etaShort)}`; lines.push(rightAlign(headerLeft, headerRight, w)); // ── min mode: header only ────────────────────────────────────────── if (widgetMode === "min") { lines.push(...ui.bar()); return lines; } // ── small mode: header + action + progress + compact stats ───────── if (widgetMode === "small") { lines.push(""); const target = noMilestone ? unitId : `${currentTaskId}: ${mockTasks.find((t) => t.id === currentTaskId)!.title}`; const actionLeft = `${pad}${theme.fg("accent", "▸")} ${theme.fg("accent", verb)} ${theme.fg("text", target)}`; lines.push(rightAlign(actionLeft, theme.fg("dim", phaseLabel), w)); if (!noMilestone) { const barWidth = Math.max(6, Math.min(18, Math.floor(w * 0.25))); 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("text", `${slicesDone}`)}${theme.fg("dim", `/${slicesTotal} slices`)}` + `${theme.fg("dim", " · task ")}${theme.fg("accent", `${taskNum}`)}${theme.fg("dim", `/${taskTotal}`)}`; lines.push(`${pad}${bar} ${meta}`); } const smallStats = [ theme.fg("warning", "$18.67"), theme.fg("dim", "35.2%ctx"), ]; lines.push(rightAlign("", smallStats.join(theme.fg("dim", " ")), w)); lines.push(...ui.bar()); return lines; } // ── full mode ────────────────────────────────────────────────────── lines.push(""); // Context section: milestone + slice + model if (!noMilestone) { const modelTag = theme.fg("muted", ` ${modelDisplay}`); lines.push( truncateToWidth(`${pad}${theme.fg("dim", milestoneTitle)}${modelTag}`, w), ); lines.push( truncateToWidth( `${pad}${theme.fg("text", theme.bold(`${sliceId}: ${sliceTitle}`))}`, w, ), ); lines.push(""); } // Action line const target = noMilestone ? unitId : `${currentTaskId}: ${mockTasks.find((t) => t.id === currentTaskId)!.title}`; const actionLeft = `${pad}${theme.fg("accent", "▸")} ${theme.fg("accent", verb)} ${theme.fg("text", target)}`; const phaseBadge = theme.fg("dim", phaseLabel); lines.push(rightAlign(actionLeft, phaseBadge, w)); lines.push(""); // Two-column body — pad left to fixed width, concatenate right const minTwoColWidth = 76; const hasTasks = !noMilestone; const useTwoCol = w >= minTwoColWidth && hasTasks; const leftColWidth = useTwoCol ? Math.floor(w * (w >= 100 ? 0.45 : 0.5)) : w; // Left column const leftLines: string[] = []; if (!noMilestone) { 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("text", `${slicesDone}`)}${theme.fg("dim", `/${slicesTotal} slices`)}` + `${theme.fg("dim", " · task ")}${theme.fg("accent", `${taskNum}`)}${theme.fg("dim", `/${taskTotal}`)}`; leftLines.push(`${pad}${bar} ${meta}`); } // Right column: task checklist — ASCII glyphs only (* > .) const rightLines: string[] = []; function fmtTask(t: (typeof mockTasks)[0]): string { const isCurrent = t.id === currentTaskId; const glyph = t.done ? theme.fg("success", "*") : isCurrent ? theme.fg("accent", ">") : theme.fg("dim", "."); const id = isCurrent ? theme.fg("accent", t.id) : t.done ? theme.fg("muted", t.id) : theme.fg("dim", t.id); const title = isCurrent ? theme.fg("text", t.title) : t.done ? theme.fg("muted", t.title) : theme.fg("text", t.title); return `${glyph} ${id}: ${title}`; } if (useTwoCol) { for (const t of mockTasks) rightLines.push(fmtTask(t)); } else if (hasTasks) { for (const t of mockTasks) leftLines.push(`${pad}${fmtTask(t)}`); } // Compose columns — pad left to fixed width, concatenate right if (useTwoCol) { const maxRows = Math.max(leftLines.length, rightLines.length); lines.push(""); for (let i = 0; i < maxRows; i++) { const left = padToWidth( truncateToWidth(leftLines[i] ?? "", leftColWidth), leftColWidth, ); const right = rightLines[i] ?? ""; lines.push(`${left}${right}`); } } else { lines.push(""); for (const l of leftLines) lines.push(truncateToWidth(l, w)); } // Footer: simplified stats + pwd + last commit + hints lines.push(""); const hitColor = mockHitRate >= 70 ? "success" : mockHitRate >= 40 ? "warning" : "error"; const statsParts = [ theme.fg(hitColor, `${mockHitRate}%hit`), theme.fg("warning", mockCost), theme.fg("dim", mockCtxUsage), ]; const statsStr = statsParts.join(theme.fg("dim", " ")); lines.push(rightAlign("", statsStr, w)); // PWD + last commit const pwdStr = theme.fg("dim", pwd); const commitStr = theme.fg( "dim", `${lastCommitTimeAgo} ago: ${lastCommitMessage}`, ); lines.push( rightAlign( `${pad}${pwdStr}`, truncateToWidth(commitStr, Math.floor(w * 0.45)), w, ), ); // Hints const hintStr = theme.fg("dim", "esc pause | ⌃⌥G dashboard"); lines.push(rightAlign("", hintStr, w)); lines.push(...ui.bar()); return lines; } // ── Main ──────────────────────────────────────────────────────────────── for (const healthState of healthStates) { const label = noMilestone ? "no-milestone" : `${width} cols`; console.log(`\n Preview: ${label}, health=${healthState.color}\n`); for (const line of render(width, healthState)) { console.log(line); } } console.log();