Files
lam-agent-vicinae/src/lib/utils.ts
T

105 lines
3.8 KiB
TypeScript

/** Small pure helpers shared across the extension. Kept dependency-free so they are trivial to test. */
/**
* Collapses whitespace and clips to `max` characters, adding an ellipsis when clipped.
* Used for list rows and session titles, where a stray newline would break the layout.
*/
export function truncate(text: string, max: number): string {
const collapsed = text.replace(/\s+/g, " ").trim();
if (collapsed.length <= max) return collapsed;
return `${collapsed.slice(0, Math.max(0, max - 1)).trimEnd()}…`;
}
/** "just now", "12m ago", "3h ago", then an absolute date once it is older than a day. */
export function formatRelativeTime(timestamp: number, now: number = Date.now()): string {
const deltaSeconds = Math.round((now - timestamp) / 1000);
if (!Number.isFinite(deltaSeconds) || deltaSeconds < 0) return "just now";
if (deltaSeconds < 45) return "just now";
const minutes = Math.round(deltaSeconds / 60);
if (minutes < 60) return `${minutes}m ago`;
const hours = Math.round(minutes / 60);
if (hours < 24) return `${hours}h ago`;
const days = Math.round(hours / 24);
if (days < 7) return `${days}d ago`;
return new Date(timestamp).toLocaleDateString(undefined, { month: "short", day: "numeric" });
}
export type RecencyGroup = "Today" | "Yesterday" | "This week" | "Older";
/**
* Buckets a timestamp for the conversation sidebar.
*
* Raycast groups its sidebar this way, and it buys the grouping that per-row
* timestamps were trying to convey, without putting a timestamp on every row.
*/
export function recencyGroup(timestamp: number, now: number = Date.now()): RecencyGroup {
const startOfToday = new Date(now);
startOfToday.setHours(0, 0, 0, 0);
const start = startOfToday.getTime();
const DAY = 86_400_000;
if (timestamp >= start) return "Today";
if (timestamp >= start - DAY) return "Yesterday";
if (timestamp >= start - 7 * DAY) return "This week";
return "Older";
}
/**
* A byte count for a person to read: "6.6 GB", "326 B".
*
* Decimal units, not the 1024-based ones a memory figure would use, because
* that is what every other tool in this ecosystem prints: `ollama list` shows
* 6550813657 bytes as 6.6 GB, and a number that disagreed with the terminal the
* user just looked at would read as a bug in this extension.
*
* `undefined` for a size that is absent or nonsense, so a caller can leave the
* field out rather than print "NaN B".
*/
export function formatBytes(bytes: number | undefined): string | undefined {
if (bytes === undefined || !Number.isFinite(bytes) || bytes < 0) return undefined;
if (bytes < 1000) return `${Math.round(bytes)} B`;
const units = ["kB", "MB", "GB", "TB"];
let value = bytes / 1000;
let unit = 0;
while (value >= 1000 && unit < units.length - 1) {
value /= 1000;
unit += 1;
}
return `${value.toFixed(1)} ${units[unit]}`;
}
/** Pulls a human-readable message out of anything that can be thrown. */
export function errorMessage(error: unknown): string {
if (error instanceof Error) return error.message;
if (typeof error === "string") return error;
try {
return JSON.stringify(error);
} catch {
return String(error);
}
}
/** True when a rejected promise came from an AbortController rather than a real failure. */
export function isAbortError(error: unknown): boolean {
return error instanceof Error && (error.name === "AbortError" || error.message.includes("aborted"));
}
/**
* Builds the first line of a prompt into a session title.
* Prompt text often starts with markdown decoration, which reads badly in a list row.
*/
export function titleFromPrompt(prompt: string, max: number): string {
const firstLine = prompt.split("\n").find((line) => line.trim().length > 0) ?? prompt;
const cleaned = firstLine.replace(/^[#>*\-\s]+/, "").trim();
return truncate(cleaned || "New chat", max);
}