From fb1bd3e5fa330ad2b3c2c5afb65c5f5eecc52dce Mon Sep 17 00:00:00 2001 From: Mikael Hugo Date: Sun, 10 May 2026 22:41:03 +0200 Subject: [PATCH] refactor(shared): deduplicate shared/ utilities against coding-agent package exports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add packages/coding-agent/src/utils/format.ts as the canonical source for formatDuration, formatTokenCount, truncateWithEllipsis, sparkline, formatDateShort, fileLink, stripAnsi, normalizeStringArray — all already exported from @singularity-forge/coding-agent via index.ts. - Convert shared/format-utils.js to a compatibility shim that re-exports the 8 functions from @singularity-forge/coding-agent. All 13 importers continue to work with no import changes required. - Convert shared/path-display.js to a compatibility shim that re-exports toPosixPath from @singularity-forge/coding-agent. Implementation in packages/coding-agent/src/utils/path-display.ts was already canonical. - shared/frontmatter.js is intentionally NOT shimmed: splitFrontmatter/ parseFrontmatterMap have a different API from the package's parseFrontmatter/ stripFrontmatter (flat-map vs {frontmatter, body} object). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- packages/coding-agent/src/utils/format.ts | 118 ++++++++++++++++++ .../extensions/shared/format-utils.js | 105 +++------------- .../extensions/shared/path-display.js | 18 +-- 3 files changed, 140 insertions(+), 101 deletions(-) create mode 100644 packages/coding-agent/src/utils/format.ts diff --git a/packages/coding-agent/src/utils/format.ts b/packages/coding-agent/src/utils/format.ts new file mode 100644 index 000000000..e39110582 --- /dev/null +++ b/packages/coding-agent/src/utils/format.ts @@ -0,0 +1,118 @@ +/** + * Pure formatting utilities shared across extensions and the core agent. + * + * Purpose: centralise widely-used, dependency-free format helpers so extension + * shared/ shims can re-export from the package rather than duplicating source. + * No @singularity-forge/tui dependency — safe for any import context. + * + * Consumer: 35+ bundled extensions via src/resources/extensions/shared/format-utils.js. + */ + +// ─── Duration Formatting ────────────────────────────────────────────────────── + +/** Format a millisecond duration as a compact human-readable string. */ +export function formatDuration(ms: number): string { + if (ms > 0 && ms < 1000) return `${ms}ms`; + const s = Math.floor(ms / 1000); + if (s < 60) return `${s}s`; + const m = Math.floor(s / 60); + const rs = s % 60; + if (m < 60) return `${m}m ${rs}s`; + const h = Math.floor(m / 60); + const rm = m % 60; + return `${h}h ${rm}m`; +} + +// ─── Token Count Formatting ────────────────────────────────────────────────── + +/** Format a token count as a compact human-readable string (e.g. 1.5k, 1.50M). */ +export function formatTokenCount(count: number): string { + if (count < 1000) return `${count}`; + if (count < 1_000_000) return `${(count / 1000).toFixed(1)}k`; + return `${(count / 1_000_000).toFixed(2)}M`; +} + +// ─── Text Truncation ───────────────────────────────────────────────────────── + +/** Truncate a string to `maxLength` characters, replacing the last character with an ellipsis if needed. */ +export function truncateWithEllipsis(text: string, maxLength: number): string { + if (text.length <= maxLength) return text; + return text.slice(0, maxLength - 1) + "…"; +} + +// ─── Data Visualization ─────────────────────────────────────────────────────── + +/** + * Render a sparkline from numeric values using Unicode block characters. + * Uses loop-based max to avoid stack overflow on large arrays. + */ +export function sparkline(values: number[]): string { + if (values.length === 0) return ""; + const chars = "▁▂▃▄▅▆▇█"; + let max = 0; + for (const v of values) { + if (v > max) max = v; + } + if (max === 0) return chars[0].repeat(values.length); + return values + .map((v) => chars[Math.min(7, Math.floor((v / max) * 7))]) + .join(""); +} + +// ─── Date Formatting ───────────────────────────────────────────────────────── + +/** Format an ISO date string as a compact locale string (e.g. "Mar 17, 2025, 02:30 PM"). */ +export function formatDateShort(iso: string): string { + try { + const d = new Date(iso); + return d.toLocaleString("en-US", { + month: "short", + day: "numeric", + year: "numeric", + hour: "2-digit", + minute: "2-digit", + }); + } catch { + return iso; + } +} + +// ─── Hyperlinks ────────────────────────────────────────────────────────────── + +/** Wrap text in an OSC 8 hyperlink for terminals that support clickable links. */ +export function fileLink(filePath: string, displayText?: string): string { + const uri = `file://${filePath}`; + const label = displayText ?? filePath; + return `\x1b]8;;${uri}\x07${label}\x1b]8;;\x07`; +} + +// ─── ANSI Stripping ─────────────────────────────────────────────────────────── + +/** Strip ANSI escape sequences from a string. */ +export function stripAnsi(s: string): string { + return s.replace(/\x1b\[[0-9;]*m/g, ""); +} + +// ─── String Array Normalization ───────────────────────────────────────────── + +export interface NormalizeStringArrayOptions { + /** Deduplicate items after filtering and trimming. */ + dedupe?: boolean; +} + +/** + * Normalize an unknown value to a string array. + * Filters to string items, trims whitespace, removes empty strings. + * Optionally deduplicates. + */ +export function normalizeStringArray( + value: unknown, + options?: NormalizeStringArrayOptions, +): string[] { + if (!Array.isArray(value)) return []; + const items = (value as unknown[]) + .filter((item): item is string => typeof item === "string") + .map((item) => item.trim()) + .filter(Boolean); + return options?.dedupe ? [...new Set(items)] : items; +} diff --git a/src/resources/extensions/shared/format-utils.js b/src/resources/extensions/shared/format-utils.js index 4247d6fb9..608ba2768 100644 --- a/src/resources/extensions/shared/format-utils.js +++ b/src/resources/extensions/shared/format-utils.js @@ -1,93 +1,20 @@ /** - * Shared pure formatting utilities — no @singularity-forge/tui dependency. + * Compatibility shim — re-exports pure formatting utilities from the + * canonical implementation in @singularity-forge/coding-agent. + * + * All 13 importers of this module continue to work without any import changes. + * The implementations now live in packages/coding-agent/src/utils/format.ts. * * ANSI-aware layout helpers (padRight, joinColumns, centerLine, fitColumns) - * live in layout-utils.ts to avoid pulling @singularity-forge/tui into modules that - * run outside jiti's alias resolution (e.g. HTML report generation via - * dynamic import in auto-loop). + * still live in layout-utils.js (depend on @singularity-forge/tui). */ -// ─── Duration Formatting ────────────────────────────────────────────────────── -/** Format a millisecond duration as a compact human-readable string. */ -export function formatDuration(ms) { - if (ms > 0 && ms < 1000) return `${ms}ms`; - const s = Math.floor(ms / 1000); - if (s < 60) return `${s}s`; - const m = Math.floor(s / 60); - const rs = s % 60; - if (m < 60) return `${m}m ${rs}s`; - const h = Math.floor(m / 60); - const rm = m % 60; - return `${h}h ${rm}m`; -} -// ─── Token Count Formatting ────────────────────────────────────────────────── -/** Format a token count as a compact human-readable string (e.g. 1.5k, 1.50M). */ -export function formatTokenCount(count) { - if (count < 1000) return `${count}`; - if (count < 1_000_000) return `${(count / 1000).toFixed(1)}k`; - return `${(count / 1_000_000).toFixed(2)}M`; -} -// ─── Text Truncation ───────────────────────────────────────────────────────── -/** Truncate a string to `maxLength` characters, replacing the last character with an ellipsis if needed. */ -export function truncateWithEllipsis(text, maxLength) { - if (text.length <= maxLength) return text; - return text.slice(0, maxLength - 1) + "…"; -} -// ─── Data Visualization ─────────────────────────────────────────────────────── -/** - * Render a sparkline from numeric values using Unicode block characters. - * Uses loop-based max to avoid stack overflow on large arrays. - */ -export function sparkline(values) { - if (values.length === 0) return ""; - const chars = "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588"; - let max = 0; - for (const v of values) { - if (v > max) max = v; - } - if (max === 0) return chars[0].repeat(values.length); - return values - .map((v) => chars[Math.min(7, Math.floor((v / max) * 7))]) - .join(""); -} -// ─── Date Formatting ───────────────────────────────────────────────────────── -/** Format an ISO date string as a compact locale string (e.g. "Mar 17, 2025, 02:30 PM"). */ -export function formatDateShort(iso) { - try { - const d = new Date(iso); - return d.toLocaleString("en-US", { - month: "short", - day: "numeric", - year: "numeric", - hour: "2-digit", - minute: "2-digit", - }); - } catch { - return iso; - } -} -// ─── Hyperlinks ────────────────────────────────────────────────────────────── -/** Wrap text in an OSC 8 hyperlink for terminals that support clickable links. */ -export function fileLink(filePath, displayText) { - const uri = `file://${filePath}`; - const label = displayText ?? filePath; - return `\x1b]8;;${uri}\x07${label}\x1b]8;;\x07`; -} -// ─── ANSI Stripping ─────────────────────────────────────────────────────────── -/** Strip ANSI escape sequences from a string. */ -export function stripAnsi(s) { - return s.replace(/\x1b\[[0-9;]*m/g, ""); -} -// ─── String Array Normalization ───────────────────────────────────────────── -/** - * Normalize an unknown value to a string array. - * Filters to string items, trims whitespace, removes empty strings. - * Optionally deduplicates. - */ -export function normalizeStringArray(value, options) { - if (!Array.isArray(value)) return []; - const items = value - .filter((item) => typeof item === "string") - .map((item) => item.trim()) - .filter(Boolean); - return options?.dedupe ? [...new Set(items)] : items; -} +export { + fileLink, + formatDateShort, + formatDuration, + formatTokenCount, + normalizeStringArray, + sparkline, + stripAnsi, + truncateWithEllipsis, +} from "@singularity-forge/coding-agent"; diff --git a/src/resources/extensions/shared/path-display.js b/src/resources/extensions/shared/path-display.js index a6c3620e8..893e9489f 100644 --- a/src/resources/extensions/shared/path-display.js +++ b/src/resources/extensions/shared/path-display.js @@ -1,18 +1,12 @@ /** - * Cross-platform path display for LLM-visible text. + * Compatibility shim — re-exports toPosixPath from the canonical + * implementation in @singularity-forge/coding-agent. * - * Paths injected into prompts, tool results, or extension messages must use - * forward slashes. Windows backslash paths cause bash failures when the model - * copies them into shell commands — bash interprets backslashes as escape chars. + * All importers of this module continue to work without any import changes. + * The implementation now lives in packages/coding-agent/src/utils/path-display.ts. * - * Use this ONLY for paths entering text the LLM or shell sees. + * Use ONLY for paths entering text the LLM or shell sees. * Filesystem operations (fs.readFile, path.join, spawn cwd) handle native * separators correctly and should NOT be normalized. */ -/** - * Convert a filesystem path to forward-slash form for display in LLM text. - * No-op on Unix. On Windows converts `C:\Users\name` to `C:/Users/name`. - */ -export function toPosixPath(fsPath) { - return fsPath.replaceAll("\\", "/"); -} +export { toPosixPath } from "@singularity-forge/coding-agent";