refactor(shared): deduplicate shared/ utilities against coding-agent package exports
- 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>
This commit is contained in:
parent
7227912a29
commit
fb1bd3e5fa
3 changed files with 140 additions and 101 deletions
118
packages/coding-agent/src/utils/format.ts
Normal file
118
packages/coding-agent/src/utils/format.ts
Normal file
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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)
|
* ANSI-aware layout helpers (padRight, joinColumns, centerLine, fitColumns)
|
||||||
* live in layout-utils.ts to avoid pulling @singularity-forge/tui into modules that
|
* still live in layout-utils.js (depend on @singularity-forge/tui).
|
||||||
* run outside jiti's alias resolution (e.g. HTML report generation via
|
|
||||||
* dynamic import in auto-loop).
|
|
||||||
*/
|
*/
|
||||||
// ─── Duration Formatting ──────────────────────────────────────────────────────
|
export {
|
||||||
/** Format a millisecond duration as a compact human-readable string. */
|
fileLink,
|
||||||
export function formatDuration(ms) {
|
formatDateShort,
|
||||||
if (ms > 0 && ms < 1000) return `${ms}ms`;
|
formatDuration,
|
||||||
const s = Math.floor(ms / 1000);
|
formatTokenCount,
|
||||||
if (s < 60) return `${s}s`;
|
normalizeStringArray,
|
||||||
const m = Math.floor(s / 60);
|
sparkline,
|
||||||
const rs = s % 60;
|
stripAnsi,
|
||||||
if (m < 60) return `${m}m ${rs}s`;
|
truncateWithEllipsis,
|
||||||
const h = Math.floor(m / 60);
|
} from "@singularity-forge/coding-agent";
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
* All importers of this module continue to work without any import changes.
|
||||||
* forward slashes. Windows backslash paths cause bash failures when the model
|
* The implementation now lives in packages/coding-agent/src/utils/path-display.ts.
|
||||||
* copies them into shell commands — bash interprets backslashes as escape chars.
|
|
||||||
*
|
*
|
||||||
* 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
|
* Filesystem operations (fs.readFile, path.join, spawn cwd) handle native
|
||||||
* separators correctly and should NOT be normalized.
|
* separators correctly and should NOT be normalized.
|
||||||
*/
|
*/
|
||||||
/**
|
export { toPosixPath } from "@singularity-forge/coding-agent";
|
||||||
* 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("\\", "/");
|
|
||||||
}
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue