5a027a0081
Splitting the god files made a pile of copy-paste helpers visible and, for the first time, fixable — sharing them previously meant importing a god file. Hashing function bodies through the TypeScript AST found twelve groups desktop-wide; production code is now at zero duplicates. Each helper went to the module that already owns its concern: firstStringField to lib/text, the two REST 404 predicates to lib/gateway-rpc beside isMissingRpcMethod, useDebounced and prefersReducedMotion to their hooks, the superseded-bootstrap guard to electron/ssh-connection, the composer keyup handler to the trigger hook that owns the rest of that state machine, and clampDataUrlReadMaxMb to apps/shared, replacing a "keep these in sync" comment between two copies. Only helpers with no existing owner got a new file: lib/mcp-servers, lib/audio-context, lib/keyed-timeouts, lib/pointer-drag, and the command palette's status row. Error-shape predicates are the worst thing to copy — when the backend changes how it reports a missing route, every copy has to be found.
30 lines
1.2 KiB
TypeScript
30 lines
1.2 KiB
TypeScript
// Canonical text micro-helpers. Do not redefine these per-page.
|
|
|
|
export const asText = (v: unknown): string => (typeof v === 'string' ? v : v == null ? '' : String(v))
|
|
|
|
export const includesQuery = (v: unknown, q: string) => asText(v).toLowerCase().includes(q)
|
|
|
|
export const prettyName = (v: string) => v.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase())
|
|
|
|
/** Search-key normalization: the exact `value.trim().toLowerCase()` idiom that
|
|
* was hand-written at ~30 filter/lookup sites. */
|
|
export const normalize = (v: unknown): string => asText(v).trim().toLowerCase()
|
|
|
|
/** Uppercase the first character, leave the rest. Matches the
|
|
* `s.charAt(0).toUpperCase() + s.slice(1)` idiom (empty-safe). */
|
|
export const capitalize = (v: string): string => (v ? v.charAt(0).toUpperCase() + v.slice(1) : v)
|
|
|
|
/** First non-empty string among `keys`, trimmed. For reading tool args and
|
|
* results, where the key carrying the interesting value varies by tool. */
|
|
export const firstStringField = (record: Record<string, unknown>, keys: readonly string[]): string => {
|
|
for (const key of keys) {
|
|
const value = record[key]
|
|
|
|
if (typeof value === 'string' && value.trim()) {
|
|
return value.trim()
|
|
}
|
|
}
|
|
|
|
return ''
|
|
}
|