From d956164812ccc355846b0feae83b1a2cd79f1220 Mon Sep 17 00:00:00 2001 From: Hongming Wang Date: Wed, 22 Apr 2026 16:41:43 -0700 Subject: [PATCH] refactor(secrets): strip Service dropdown from Add-Key form MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Add-Key form used to open with a required Service dropdown (GitHub / Anthropic / OpenRouter / Other) that gated everything else. The dropdown did no persistent work — the secret store only cares about (key_name, value); the Service label was never saved anywhere. It also suffered registry drift: today we support ~22 hermes-dispatched providers (MiniMax, Gemini, DeepSeek, Kimi, Qwen, NVIDIA, etc.); only 3 had entries. Everyone else landed in "Other" with no downside beyond the mandatory click. Replaces it with: 1. Key-name autocomplete sourced from new KEY_NAME_SUGGESTIONS in lib/services.ts — 26 entries covering common infra keys + every hermes-supported provider. 2. inferGroup(keyName) derives classification at render time, matching what the store already does in getGrouped(). No behaviour change for list grouping. 3. Provider docs link renders inline only when inferGroup recognises the name. For 'custom' keys we stay quiet — no false-structure prompt. 4. Test-connection button still available when the inferred group supports it AND the value is format-valid. Same providers as before. SERVICES registry preserved for LIST rendering + test routing. Result: two fields instead of three. One fewer decision. Provider- agnostic by design — new providers work the moment someone types their canonical env var name; no UI code change per provider. Co-Authored-By: Claude Opus 4.7 (1M context) --- canvas/src/components/settings/AddKeyForm.tsx | 115 +++++++++--------- canvas/src/lib/services.ts | 52 +++++++- 2 files changed, 110 insertions(+), 57 deletions(-) diff --git a/canvas/src/components/settings/AddKeyForm.tsx b/canvas/src/components/settings/AddKeyForm.tsx index 97933ce1..8fb83120 100644 --- a/canvas/src/components/settings/AddKeyForm.tsx +++ b/canvas/src/components/settings/AddKeyForm.tsx @@ -1,6 +1,5 @@ 'use client'; -import { useState, useCallback, useEffect, useRef } from 'react'; -import type { SecretGroup } from '@/types/secrets'; +import { useState, useCallback, useEffect, useRef, useMemo } from 'react'; import { useSecretsStore } from '@/stores/secrets-store'; import { KeyValueField } from '@/components/ui/KeyValueField'; import { ValidationHint } from '@/components/ui/ValidationHint'; @@ -10,7 +9,7 @@ import { isValidKeyName, inferGroup, } from '@/lib/validation/secret-formats'; -import { SERVICES, SERVICE_GROUP_ORDER, getDefaultKeyName } from '@/lib/services'; +import { SERVICES, KEY_NAME_SUGGESTIONS } from '@/lib/services'; const VALIDATION_DEBOUNCE_MS = 400; @@ -23,9 +22,21 @@ interface AddKeyFormProps { /** * Inline-expanding form for adding a new API key. * - * Flow (from spec §4.2): - * Form Open → select service → key name auto-fills → type value → - * optional Test Connection → Save + * Design note (2026-04-22): the form used to open with a Service + * dropdown (GitHub / Anthropic / OpenRouter / Other) gating what to + * do next. That added friction — the storage layer only cares about + * (key_name, value), and the provider can always be inferred from the + * key name itself. We removed the dropdown and rely on: + * + * - A datalist of common key-name suggestions so autocomplete + * replaces "pick a provider then the name auto-fills" + * - inferGroup(keyName) to classify the secret for validation + + * list-view grouping + test-connection routing, derived at render + * time from what the user actually typed + * + * Result: fewer fields, provider-agnostic by design, no UI code change + * needed to onboard a new provider (MiniMax, DeepSeek, etc. just work + * as soon as you type their canonical env var name). */ export function AddKeyForm({ workspaceId, @@ -34,8 +45,7 @@ export function AddKeyForm({ }: AddKeyFormProps) { const createSecret = useSecretsStore((s) => s.createSecret); - const [selectedGroup, setSelectedGroup] = useState('github'); - const [keyName, setKeyName] = useState(getDefaultKeyName('github')); + const [keyName, setKeyName] = useState(''); const [value, setValue] = useState(''); const [validationError, setValidationError] = useState(null); const [keyNameError, setKeyNameError] = useState(null); @@ -43,23 +53,13 @@ export function AddKeyForm({ const [saveError, setSaveError] = useState(null); const debounceRef = useRef>(undefined); - const service = SERVICES[selectedGroup]; - // Auto-fill key name when service changes - const handleServiceChange = useCallback( - (group: SecretGroup) => { - setSelectedGroup(group); - const defaultName = getDefaultKeyName(group); - if (defaultName) { - setKeyName(defaultName); - } - // Reset validation - setValidationError(null); - setKeyNameError(null); - setSaveError(null); - }, - [], - ); + // Group is derived, not selected. Falls back to 'custom' for any + // key name that doesn't match a known provider pattern — validation + // and test-connection still work, just without provider-specific + // format hints. + const inferredGroup = useMemo(() => inferGroup(keyName || ''), [keyName]); + const service = SERVICES[inferredGroup]; // Validate key name useEffect(() => { @@ -78,7 +78,7 @@ export function AddKeyForm({ setKeyNameError(null); }, [keyName, existingNames]); - // Debounced value validation + // Debounced value validation against the inferred provider's format. useEffect(() => { if (!value) { setValidationError(null); @@ -86,18 +86,17 @@ export function AddKeyForm({ } clearTimeout(debounceRef.current); debounceRef.current = setTimeout(() => { - setValidationError(validateSecretValue(value, selectedGroup)); + setValidationError(validateSecretValue(value, inferredGroup)); }, VALIDATION_DEBOUNCE_MS); return () => clearTimeout(debounceRef.current); - }, [value, selectedGroup]); + }, [value, inferredGroup]); const handleSave = useCallback(async () => { - // Final validation pass if (!isValidKeyName(keyName)) { setKeyNameError('Key name must be UPPER_SNAKE_CASE'); return; } - const valErr = validateSecretValue(value, selectedGroup); + const valErr = validateSecretValue(value, inferredGroup); if (valErr) { setValidationError(valErr); return; @@ -114,32 +113,21 @@ export function AddKeyForm({ } finally { setIsSaving(false); } - }, [keyName, value, selectedGroup, createSecret, workspaceId]); + }, [keyName, value, inferredGroup, createSecret, workspaceId]); const canSave = keyName && value && !keyNameError && !validationError && !isSaving; + // Show the provider-specific docs hint only when the key name + // matches a known provider. For 'custom' (unknown key name) we stay + // quiet — no false-structure prompt. + const showProviderHint = inferredGroup !== 'custom' && service.docsUrl; + return (
Add New Key
- {/* Service selector */} - - - {/* Key name */} + {/* Key name — autocomplete replaces the old Service dropdown. + inferGroup(keyName) derives classification at render time. */} - {keyNameError && ( - + + {KEY_NAME_SUGGESTIONS.map((name) => ( + + {keyNameError && } + {showProviderHint && ( +
+ {service.label} + {' — '} + + get a key + +
)} {/* Key value */} @@ -172,22 +178,21 @@ export function AddKeyForm({ showValid={!validationError && value.length > 0} /> - {/* Test connection (only for supported services) */} + {/* Test connection (only when the inferred group supports it AND + value looks format-valid). */} {service.testSupported && value && !validationError && ( )} - {/* Save error */} {saveError && (
{saveError}
)} - {/* Actions */}