fix(desktop): reflect active toolset provider in config panel

The toolset config panel highlighted the first keyless provider (e.g.
Nous Portal) on load instead of the provider actually written to config.
The /api/tools/toolsets/{name}/config endpoint never reported which
provider was active, so the GUI's default-expand logic fell back to
"first configured" — and keyless providers are always "configured".

Backend now annotates each provider with is_active (via the same
_is_provider_active helper the CLI 'hermes tools' picker uses) plus a
top-level active_provider summary. The panel prefers that signal before
falling back to first-configured/first.

Adds a frontend regression test (active provider is expanded on load)
and backend coverage (config reports is_active/active_provider; selecting
a provider round-trips into the next config read).
This commit is contained in:
emozilla 2026-06-02 03:25:46 -04:00
parent 59510d7b44
commit 134643a2fa
5 changed files with 122 additions and 7 deletions

View file

@ -26,6 +26,7 @@ function config(overrides: Partial<ToolsetConfig> = {}): ToolsetConfig {
return {
name: 'tts',
has_category: true,
active_provider: null,
providers: [
{
name: 'Microsoft Edge TTS',
@ -33,7 +34,8 @@ function config(overrides: Partial<ToolsetConfig> = {}): ToolsetConfig {
tag: 'No API key needed',
env_vars: [],
post_setup: null,
requires_nous_auth: false
requires_nous_auth: false,
is_active: false
},
{
name: 'ElevenLabs',
@ -43,7 +45,8 @@ function config(overrides: Partial<ToolsetConfig> = {}): ToolsetConfig {
{ key: 'ELEVENLABS_API_KEY', prompt: 'ElevenLabs API key', url: 'https://x', default: null, is_set: false }
],
post_setup: null,
requires_nous_auth: false
requires_nous_auth: false,
is_active: false
}
],
...overrides
@ -99,4 +102,54 @@ describe('ToolsetConfigPanel', () => {
await waitFor(() => expect(setEnvVar).toHaveBeenCalledWith('ELEVENLABS_API_KEY', 'sk-test-123'))
})
it('expands the active provider on load, not just the first configured one', async () => {
// ElevenLabs is the active provider per config, even though the keyless
// Edge TTS provider sorts first and is also "configured". The panel must
// honor is_active and expand ElevenLabs (so its API-key field renders)
// rather than defaulting to the first keyless provider. Regression test
// for the GUI showing the wrong provider selected after relaunch.
getToolsetConfig.mockResolvedValue(
config({
active_provider: 'ElevenLabs',
providers: [
{
name: 'Microsoft Edge TTS',
badge: 'free',
tag: 'No API key needed',
env_vars: [],
post_setup: null,
requires_nous_auth: false,
is_active: false
},
{
name: 'ElevenLabs',
badge: 'paid',
tag: 'Most natural voices',
env_vars: [
{
key: 'ELEVENLABS_API_KEY',
prompt: 'ElevenLabs API key',
url: 'https://x',
default: null,
is_set: true
}
],
post_setup: null,
requires_nous_auth: false,
is_active: true
}
]
})
)
const { ToolsetConfigPanel } = await import('./toolset-config-panel')
render(<ToolsetConfigPanel onConfiguredChange={vi.fn()} toolset="tts" />)
// The active provider's env-var field only renders when it's the expanded
// one — so finding it proves ElevenLabs (not Edge TTS) was auto-expanded.
expect(await screen.findByText('ELEVENLABS_API_KEY')).toBeTruthy()
// No provider selection was triggered — this is purely reflecting state.
expect(selectToolsetProvider).not.toHaveBeenCalled()
})
})

View file

@ -195,16 +195,23 @@ export function ToolsetConfigPanel({ toolset, onConfiguredChange }: ToolsetConfi
const providers = useMemo(() => cfg?.providers ?? [], [cfg])
// Default the expanded provider to the first one that is fully configured,
// else the first provider.
// Default the expanded provider to the one actually active in config
// (`is_active` / `cfg.active_provider`, mirroring the CLI picker), then the
// first fully-configured provider, else the first provider. Without this the
// panel highlighted the first keyless provider (e.g. Nous Portal) even when
// the user had already selected another (e.g. DuckDuckGo).
useEffect(() => {
if (activeProvider || providers.length === 0) {
return
}
const configured = providers.find(p => providerConfigured(p, envState))
setActiveProvider((configured ?? providers[0]).name)
}, [activeProvider, providers, envState])
const selected =
providers.find(p => p.is_active) ??
(cfg?.active_provider ? providers.find(p => p.name === cfg.active_provider) : undefined) ??
providers.find(p => providerConfigured(p, envState)) ??
providers[0]
setActiveProvider(selected.name)
}, [activeProvider, providers, envState, cfg])
async function handleSelect(provider: ToolProvider) {
setActiveProvider(provider.name)

View file

@ -471,12 +471,17 @@ export interface ToolProvider {
env_vars: ToolEnvVar[]
post_setup: string | null
requires_nous_auth: boolean
/** True when this is the provider currently written to config (mirrors the
* CLI `hermes tools` active-provider detection). */
is_active: boolean
}
export interface ToolsetConfig {
name: string
has_category: boolean
providers: ToolProvider[]
/** Name of the currently active provider, or null if none is configured. */
active_provider: string | null
}
export interface SessionSearchResult {