Merge pull request #1689 from Molecule-AI/refactor/strip-secret-service-dropdown

refactor(secrets): strip Service dropdown from Add-Key form
This commit is contained in:
Hongming Wang 2026-04-22 18:46:02 -07:00 committed by GitHub
commit 66de81fbfa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 110 additions and 57 deletions

View File

@ -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<SecretGroup>('github');
const [keyName, setKeyName] = useState(getDefaultKeyName('github'));
const [keyName, setKeyName] = useState('');
const [value, setValue] = useState('');
const [validationError, setValidationError] = useState<string | null>(null);
const [keyNameError, setKeyNameError] = useState<string | null>(null);
@ -43,23 +53,13 @@ export function AddKeyForm({
const [saveError, setSaveError] = useState<string | null>(null);
const debounceRef = useRef<ReturnType<typeof setTimeout>>(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 (
<div className="add-key-form">
<div className="add-key-form__header">Add New Key</div>
{/* Service selector */}
<label className="add-key-form__label">
Service
<select
value={selectedGroup}
onChange={(e) => handleServiceChange(e.target.value as SecretGroup)}
disabled={isSaving}
className="add-key-form__select"
>
{SERVICE_GROUP_ORDER.map((group) => (
<option key={group} value={group}>
{SERVICES[group].label}
</option>
))}
</select>
</label>
{/* Key name */}
{/* Key name autocomplete replaces the old Service dropdown.
inferGroup(keyName) derives classification at render time. */}
<label className="add-key-form__label">
Key name
<input
@ -147,14 +135,32 @@ export function AddKeyForm({
value={keyName}
onChange={(e) => setKeyName(e.target.value.toUpperCase())}
disabled={isSaving}
placeholder="MY_API_KEY"
placeholder="e.g. ANTHROPIC_API_KEY, MINIMAX_API_KEY, GITHUB_TOKEN"
className="add-key-form__input"
autoComplete="off"
spellCheck={false}
list="add-key-name-suggestions"
/>
</label>
{keyNameError && (
<ValidationHint error={keyNameError} />
<datalist id="add-key-name-suggestions">
{KEY_NAME_SUGGESTIONS.map((name) => (
<option key={name} value={name} />
))}
</datalist>
{keyNameError && <ValidationHint error={keyNameError} />}
{showProviderHint && (
<div className="add-key-form__hint" data-testid="provider-hint">
<span className="add-key-form__hint-label">{service.label}</span>
{' — '}
<a
href={service.docsUrl}
target="_blank"
rel="noopener noreferrer"
className="add-key-form__hint-link"
>
get a key
</a>
</div>
)}
{/* 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 && (
<TestConnectionButton
provider={selectedGroup}
provider={inferredGroup}
secretValue={value}
/>
)}
{/* Save error */}
{saveError && (
<div className="add-key-form__error" role="alert">
{saveError}
</div>
)}
{/* Actions */}
<div className="add-key-form__actions">
<button
type="button"

View File

@ -1,10 +1,18 @@
import type { ServiceConfig, SecretGroup } from '@/types/secrets';
/**
* Static service registry. Each known provider maps to its display
* properties, expected key names, and whether test-connection is supported.
* Static service registry used for LIST-view rendering: the
* per-group icon, the "get a key" docs link shown as a hint once
* the user types a matching key name, and the test-connection
* routing for the 3 providers with backend test endpoints.
*
* Keys not matching any known service fall into the "custom" catch-all.
*
* Note (2026-04-22): the Add-Key form no longer uses this as a
* user-facing dropdown. It reads keyNames[0] via getDefaultKeyName
* still referenced by a couple of legacy call sites and the
* Add form's autocomplete source lives in KEY_NAME_SUGGESTIONS
* below. SERVICES is purely for post-save display + test routing.
*/
export const SERVICES: Record<SecretGroup, ServiceConfig> = {
github: {
@ -49,3 +57,43 @@ export const SERVICE_GROUP_ORDER: SecretGroup[] = [
export function getDefaultKeyName(group: SecretGroup): string {
return SERVICES[group].keyNames[0] ?? '';
}
/**
* Autocomplete suggestions for the Add-Key form's key-name input.
*
* Covers the providers hermes-agent supports natively + the common
* infra keys (GitHub, platform-side). Adding a new provider here is
* a one-line change the Add form picks it up via <datalist>, and
* classification (for validation + list grouping) comes from
* inferGroup in lib/validation/secret-formats.ts.
*
* Order: alphabetical for stable display in autocomplete popups.
*/
export const KEY_NAME_SUGGESTIONS: readonly string[] = [
'AI_GATEWAY_API_KEY',
'ANTHROPIC_API_KEY',
'ARCEEAI_API_KEY',
'COPILOT_GITHUB_TOKEN',
'DASHSCOPE_API_KEY',
'DEEPSEEK_API_KEY',
'GEMINI_API_KEY',
'GH_TOKEN',
'GITHUB_TOKEN',
'GLM_API_KEY',
'GOOGLE_API_KEY',
'HERMES_API_KEY',
'HF_TOKEN',
'KILOCODE_API_KEY',
'KIMI_API_KEY',
'KIMI_CN_API_KEY',
'MINIMAX_API_KEY',
'MINIMAX_CN_API_KEY',
'NOUS_API_KEY',
'NVIDIA_API_KEY',
'OLLAMA_API_KEY',
'OPENAI_API_KEY',
'OPENCODE_GO_API_KEY',
'OPENCODE_ZEN_API_KEY',
'OPENROUTER_API_KEY',
'XIAOMI_API_KEY',
] as const;