105 lines
3.8 KiB
TypeScript
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);
|
|
}
|