.prettierrc.json was missing "semi": false, so prettier wanted to add semicolons to a codebase written without them (763 semicolon-free statements vs. 150 with, in hand-written .ts; zero hand-written .svelte files use them at all). That's why prettier --check failed on 249 files — not because the code was unformatted, but because the config didn't match the actual house style. Added "semi": false; left printWidth/etc as configured (printWidth barely moves the failure count: 218/213/212 files at 100/120/140). Ran `prettier --write .` with the corrected config. Verified semantics-preserving before and after: - eslint: 142 problems both before and after, byte-identical - build passes, 38/38 tests pass - token-stream diff (whitespace/semicolons/quotes normalized) on all 218 changed files: only 52 had any remaining token change, all either trailing-comma removal (matching trailingComma: "none") or import/ ternary reflow — no semantic changes - live smoke test: Knowledge, Tasks, Fleet map, and a chat window (AgentTrace, markdown, Scope graph, activity rail) all render correctly, no console errors Most of the diff is shadcn/ui vendor files (lib/components/ui/) moving from the CLI's own style (double quotes, tabs, semicolons) to house style; re-running `shadcn-svelte add` on a component will need a follow-up format pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
48 lines
1.9 KiB
TypeScript
48 lines
1.9 KiB
TypeScript
import { clsx, type ClassValue } from 'clsx'
|
|
import { twMerge } from 'tailwind-merge'
|
|
|
|
export function cn(...inputs: ClassValue[]) {
|
|
return twMerge(clsx(inputs))
|
|
}
|
|
|
|
// relativeTime renders a compact "Xs/Xm/Xh/Xd ago" label for freshness
|
|
// indicators (health checks, live event timestamps, etc).
|
|
export function relativeTime(iso: string | null | undefined): string {
|
|
if (!iso) return 'never'
|
|
const ms = Date.now() - new Date(iso).getTime()
|
|
if (ms < 0) return 'just now'
|
|
const s = Math.floor(ms / 1000)
|
|
if (s < 60) return `${s}s ago`
|
|
const m = Math.floor(s / 60)
|
|
if (m < 60) return `${m}m ago`
|
|
const h = Math.floor(m / 60)
|
|
if (h < 24) return `${h}h ago`
|
|
const d = Math.floor(h / 24)
|
|
return `${d}d ago`
|
|
}
|
|
|
|
// Truncates long slugs/names in the middle (keeping the "type:" prefix and
|
|
// the tail visible) rather than at the end — for slugs the distinguishing
|
|
// part is often at both ends, e.g. "investigation:nomos/dragonfly-memlock-…"
|
|
// vs "…rlimit-type-8-in-unprivileged-lxcs".
|
|
export function truncateMiddle(s: string, maxLen = 36): string {
|
|
if (s.length <= maxLen) return s
|
|
const keep = Math.floor((maxLen - 1) / 2)
|
|
return `${s.slice(0, keep)}…${s.slice(-keep)}`
|
|
}
|
|
|
|
// debounce wraps fn so rapid calls (e.g. keystrokes in a filter input)
|
|
// collapse into one invocation after `wait`ms of silence.
|
|
export function debounce<T extends (...args: never[]) => void>(fn: T, wait = 300): T {
|
|
let timer: ReturnType<typeof setTimeout> | undefined
|
|
return ((...args: Parameters<T>) => {
|
|
clearTimeout(timer)
|
|
timer = setTimeout(() => fn(...args), wait)
|
|
}) as T
|
|
}
|
|
|
|
export type WithoutChild<T> = T extends { child?: unknown } ? Omit<T, 'child'> : T
|
|
export type WithoutChildren<T> = T extends { children?: unknown } ? Omit<T, 'children'> : T
|
|
export type WithoutChildrenOrChild<T> = WithoutChildren<WithoutChild<T>>
|
|
export type WithElementRef<T, U extends HTMLElement = HTMLElement> = T & { ref?: U | null }
|