diff --git a/src/resources/extensions/gsd/history.ts b/src/resources/extensions/gsd/history.ts index 79da6782e..a3d1c3fc6 100644 --- a/src/resources/extensions/gsd/history.ts +++ b/src/resources/extensions/gsd/history.ts @@ -2,7 +2,8 @@ // Human-readable display of past auto-mode unit executions. import type { ExtensionCommandContext } from "@gsd/pi-coding-agent"; -import { formatDuration, padRight, truncateWithEllipsis } from "../shared/format-utils.js"; +import { formatDuration, truncateWithEllipsis } from "../shared/format-utils.js"; +import { padRight } from "../shared/layout-utils.js"; import { getLedger, getProjectTotals, formatCost, formatTokenCount, aggregateBySlice, aggregateByPhase, aggregateByModel, loadLedgerFromDisk, diff --git a/src/resources/extensions/gsd/metrics.ts b/src/resources/extensions/gsd/metrics.ts index f17195a2b..d3090ae44 100644 --- a/src/resources/extensions/gsd/metrics.ts +++ b/src/resources/extensions/gsd/metrics.ts @@ -20,8 +20,10 @@ import { getAndClearSkills } from "./skill-telemetry.js"; import { loadJsonFile, loadJsonFileOrNull, saveJsonFile } from "./json-persistence.js"; import { parseUnitId } from "./unit-id.js"; -// Re-export from shared — canonical implementation lives in format-utils. -export { formatTokenCount } from "../shared/mod.js"; +// Re-export from shared — import directly from format-utils to avoid pulling +// in the full barrel (mod.js → ui.js → @gsd/pi-tui) which breaks when loaded +// outside jiti's alias resolution (e.g. dynamic import in auto-loop reports). +export { formatTokenCount } from "../shared/format-utils.js"; // ─── Types ──────────────────────────────────────────────────────────────────── diff --git a/src/resources/extensions/shared/format-utils.ts b/src/resources/extensions/shared/format-utils.ts index 62e18abf1..122d122bd 100644 --- a/src/resources/extensions/shared/format-utils.ts +++ b/src/resources/extensions/shared/format-utils.ts @@ -1,12 +1,12 @@ /** - * Shared formatting and layout utilities for TUI dashboard components. + * Shared pure formatting utilities — no @gsd/pi-tui dependency. * - * Consolidates helpers that were previously duplicated across - * auto-dashboard.ts, dashboard-overlay.ts, and visualizer-views.ts. + * ANSI-aware layout helpers (padRight, joinColumns, centerLine, fitColumns) + * live in layout-utils.ts to avoid pulling @gsd/pi-tui into modules that + * run outside jiti's alias resolution (e.g. HTML report generation via + * dynamic import in auto-loop). */ -import { truncateToWidth, visibleWidth } from "@gsd/pi-tui"; - // ─── Duration Formatting ────────────────────────────────────────────────────── /** Format a millisecond duration as a compact human-readable string. */ @@ -31,45 +31,6 @@ export function formatTokenCount(count: number): string { return `${(count / 1_000_000).toFixed(2)}M`; } -// ─── Layout Helpers ─────────────────────────────────────────────────────────── - -/** Pad a string with trailing spaces to fill `width` (ANSI-aware). */ -export function padRight(content: string, width: number): string { - const vis = visibleWidth(content); - return content + " ".repeat(Math.max(0, width - vis)); -} - -/** Build a line with left-aligned and right-aligned content. */ -export function joinColumns(left: string, right: string, width: number): string { - const leftW = visibleWidth(left); - const rightW = visibleWidth(right); - if (leftW + rightW + 2 > width) { - return truncateToWidth(`${left} ${right}`, width); - } - return left + " ".repeat(width - leftW - rightW) + right; -} - -/** Center content within `width` (ANSI-aware). */ -export function centerLine(content: string, width: number): string { - const vis = visibleWidth(content); - if (vis >= width) return truncateToWidth(content, width); - const leftPad = Math.floor((width - vis) / 2); - return " ".repeat(leftPad) + content; -} - -/** Join as many parts as fit within `width`, separated by `separator`. */ -export function fitColumns(parts: string[], width: number, separator = " "): string { - const filtered = parts.filter(Boolean); - if (filtered.length === 0) return ""; - let result = filtered[0]; - for (let i = 1; i < filtered.length; i++) { - const candidate = `${result}${separator}${filtered[i]}`; - if (visibleWidth(candidate) > width) break; - result = candidate; - } - return truncateToWidth(result, width); -} - // ─── Text Truncation ───────────────────────────────────────────────────────── /** Truncate a string to `maxLength` characters, replacing the last character with an ellipsis if needed. */ diff --git a/src/resources/extensions/shared/layout-utils.ts b/src/resources/extensions/shared/layout-utils.ts new file mode 100644 index 000000000..c18695563 --- /dev/null +++ b/src/resources/extensions/shared/layout-utils.ts @@ -0,0 +1,49 @@ +/** + * ANSI-aware TUI layout utilities that depend on @gsd/pi-tui. + * + * Separated from format-utils.ts so that modules needing only pure + * formatting (e.g. HTML report generation) can import format-utils + * without pulling in the @gsd/pi-tui dependency — which fails when + * loaded outside jiti's alias resolution context. + */ + +import { truncateToWidth, visibleWidth } from "@gsd/pi-tui"; + +// ─── Layout Helpers ─────────────────────────────────────────────────────────── + +/** Pad a string with trailing spaces to fill `width` (ANSI-aware). */ +export function padRight(content: string, width: number): string { + const vis = visibleWidth(content); + return content + " ".repeat(Math.max(0, width - vis)); +} + +/** Build a line with left-aligned and right-aligned content. */ +export function joinColumns(left: string, right: string, width: number): string { + const leftW = visibleWidth(left); + const rightW = visibleWidth(right); + if (leftW + rightW + 2 > width) { + return truncateToWidth(`${left} ${right}`, width); + } + return left + " ".repeat(width - leftW - rightW) + right; +} + +/** Center content within `width` (ANSI-aware). */ +export function centerLine(content: string, width: number): string { + const vis = visibleWidth(content); + if (vis >= width) return truncateToWidth(content, width); + const leftPad = Math.floor((width - vis) / 2); + return " ".repeat(leftPad) + content; +} + +/** Join as many parts as fit within `width`, separated by `separator`. */ +export function fitColumns(parts: string[], width: number, separator = " "): string { + const filtered = parts.filter(Boolean); + if (filtered.length === 0) return ""; + let result = filtered[0]; + for (let i = 1; i < filtered.length; i++) { + const candidate = `${result}${separator}${filtered[i]}`; + if (visibleWidth(candidate) > width) break; + result = candidate; + } + return truncateToWidth(result, width); +} diff --git a/src/resources/extensions/shared/mod.ts b/src/resources/extensions/shared/mod.ts index b51e9b69f..44a2706ae 100644 --- a/src/resources/extensions/shared/mod.ts +++ b/src/resources/extensions/shared/mod.ts @@ -13,15 +13,18 @@ export { stripAnsi, formatTokenCount, formatDuration, - padRight, - joinColumns, - centerLine, - fitColumns, sparkline, normalizeStringArray, fileLink, } from "./format-utils.js"; +export { + padRight, + joinColumns, + centerLine, + fitColumns, +} from "./layout-utils.js"; + export { shortcutDesc } from "./terminal.js"; export { toPosixPath } from "./path-display.js"; export { showInterviewRound } from "./interview-ui.js"; diff --git a/src/resources/extensions/shared/tests/format-utils.test.ts b/src/resources/extensions/shared/tests/format-utils.test.ts index e6b8ddde0..94705e4e2 100644 --- a/src/resources/extensions/shared/tests/format-utils.test.ts +++ b/src/resources/extensions/shared/tests/format-utils.test.ts @@ -2,13 +2,15 @@ import { describe, it } from "node:test"; import assert from "node:assert/strict"; import { formatDuration, + sparkline, + stripAnsi, +} from "../format-utils.js"; +import { padRight, joinColumns, centerLine, fitColumns, - sparkline, - stripAnsi, -} from "../format-utils.js"; +} from "../layout-utils.js"; describe("formatDuration", () => { it("formats seconds", () => {