Austin Pickett acce1a2452
feat(desktop): polish credentials settings and messaging env routing (#39217)
* feat(desktop): polish credentials settings and messaging env routing

Align Provider API Keys and Tools & Keys with Advanced ListRow inputs,
add Tools & Keys sidebar subnav, move platform env vars to Messaging via
channel_managed discovery, strip toolset emojis, and condense cron actions.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(desktop): align Messaging credential inputs with settings ListRow style

Remove monospace inputs and use CREDENTIAL_CONTROL_CLASS + ListRow layout
to match Provider API Keys and Tools & Keys.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-04 14:01:15 -04:00

152 lines
4.5 KiB
TypeScript

import type { HermesConfigRecord, ToolsetInfo } from '@/types/hermes'
import { BUILTIN_PERSONALITIES, ENUM_OPTIONS, PROVIDER_GROUPS } from './constants'
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())
/** Strip leading emoji from toolset titles (CLI registry prefixes labels with icons). */
export const stripToolsetLabel = (label: string): string =>
label.replace(/^[\p{Emoji}\p{Extended_Pictographic}\s]+/u, '').trim() || label
export const toolsetDisplayLabel = (toolset: Pick<ToolsetInfo, 'label' | 'name'>): string =>
stripToolsetLabel(asText(toolset.label || toolset.name))
export const toolNames = (t: ToolsetInfo) => (Array.isArray(t.tools) ? t.tools.map(asText).filter(Boolean) : [])
export const withoutKey = <T>(record: Record<string, T>, key: string) => {
const next = { ...record }
delete next[key]
return next
}
export const redactedValue = (v: string) => (v.length <= 8 ? '••••' : `${v.slice(0, 4)}...${v.slice(-4)}`)
// Longest-prefix match so a more specific group like ``MINIMAX_CN_`` is
// chosen over its shorter parent ``MINIMAX_``. Falls back to the bucket
// "Other" used by the Keys settings view for un-grouped env vars.
export const providerGroup = (key: string) => {
let best: (typeof PROVIDER_GROUPS)[number] | undefined
for (const candidate of PROVIDER_GROUPS) {
if (!key.startsWith(candidate.prefix)) {
continue
}
if (!best || candidate.prefix.length > best.prefix.length) {
best = candidate
}
}
return best?.name ?? 'Other'
}
export const providerMeta = (name: string) =>
PROVIDER_GROUPS.find(g => g.name === name && (g.description || g.docsUrl)) ??
PROVIDER_GROUPS.find(g => g.name === name)
export const providerPriority = (name: string) => providerMeta(name)?.priority ?? 99
const POLLUTING_PATH_PARTS = new Set(['__proto__', 'constructor', 'prototype'])
function isSafePart(part: string): boolean {
return part.length > 0 && !POLLUTING_PATH_PARTS.has(part)
}
function configPathParts(path: string): string[] {
const parts = path.split('.')
if (!parts.every(isSafePart)) {
throw new Error(`Unsafe config path: ${path}`)
}
return parts
}
function safeSet(target: Record<string, unknown>, key: string, value: unknown): void {
if (key === '__proto__' || key === 'constructor' || key === 'prototype' || !key) {
throw new Error(`Unsafe config key: ${key}`)
}
Object.defineProperty(target, key, {
value,
writable: true,
enumerable: true,
configurable: true
})
}
export function getNested(obj: HermesConfigRecord, path: string): unknown {
let cur: unknown = obj
for (const part of configPathParts(path)) {
if (cur == null || typeof cur !== 'object') {
return undefined
}
if (!Object.prototype.hasOwnProperty.call(cur, part)) {
return undefined
}
cur = (cur as Record<string, unknown>)[part]
}
return cur
}
export function setNested(obj: HermesConfigRecord, path: string, value: unknown): HermesConfigRecord {
const clone = structuredClone(obj)
const parts = configPathParts(path)
let cur: Record<string, unknown> = clone
for (let i = 0; i < parts.length - 1; i += 1) {
const part = parts[i]
if (!isSafePart(part)) {
throw new Error(`Unsafe config path part: ${part}`)
}
const existing = Object.prototype.hasOwnProperty.call(cur, part) ? cur[part] : undefined
if (existing == null || typeof existing !== 'object') {
safeSet(cur, part, {})
}
cur = cur[part] as Record<string, unknown>
}
safeSet(cur, parts[parts.length - 1], value)
return clone
}
function personalityOptions(config: HermesConfigRecord): string[] {
const custom = getNested(config, 'agent.personalities')
const customNames =
custom && typeof custom === 'object' && !Array.isArray(custom) ? Object.keys(custom as Record<string, unknown>) : []
return [...new Set(['', ...BUILTIN_PERSONALITIES, ...customNames])]
}
export function enumOptionsFor(
key: string,
value: unknown,
config: HermesConfigRecord,
dynamicOptions?: string[]
): string[] | undefined {
const opts = dynamicOptions ?? (key === 'display.personality' ? personalityOptions(config) : ENUM_OPTIONS[key])
if (!opts) {
return undefined
}
const current = asText(value)
return current && !opts.includes(current) ? [...opts, current] : opts
}