feat(plugins): surface the valid agent names in logs and the Plugins UI (#5910)

The agent name used in the `Agents` config option comes from the .ndp file
name, not from the manifest. The Plugins UI showed the ID but never said what
it was for, so renaming a plugin file silently breaks the config with only a
Debug-level "Unknown agent ignored" line to go on.

Add a caption under the ID in the Plugins UI, and list the accepted names
alongside the rejected one in that log line.

Related to navidrome/apple-music-plugin#14
This commit is contained in:
Deluan Quintão 2026-08-30 11:11:35 -04:00 committed by GitHub
parent 59448e9283
commit b134f16fd5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 81 additions and 2 deletions

View File

@ -4,6 +4,7 @@ import (
"cmp"
"context"
"errors"
"maps"
"slices"
"strings"
"sync"
@ -126,12 +127,19 @@ func (a *Agents) getEnabledAgentNames() []enabledAgent {
} else if isPlugin {
validAgents = append(validAgents, enabledAgent{name: name, isPlugin: true})
} else {
log.Debug("Unknown agent ignored", "name", name)
log.Debug("Unknown agent ignored", "name", name, "available", availableAgentNames(availablePlugins))
}
}
return validAgents
}
// availableAgentNames returns every name accepted by the Agents config option.
func availableAgentNames(plugins []string) []string {
names := append(slices.Collect(maps.Keys(Map)), plugins...)
slices.Sort(names)
return names
}
func (a *Agents) getAgent(ea enabledAgent) Interface {
if ea.isPlugin {
// Try to load WASM plugin agent (if plugin loader is available)

View File

@ -3,6 +3,7 @@ package agents
import (
"context"
"errors"
"slices"
"time"
"github.com/navidrome/navidrome/conf/configtest"
@ -91,6 +92,22 @@ var _ = Describe("Agents", func() {
Expect(ags).ToNot(ContainElement("disabled"))
})
Describe("availableAgentNames", func() {
It("combines built-in agents with the given plugins", func() {
names := availableAgentNames([]string{"apple-music"})
Expect(names).To(ContainElements("apple-music", LocalAgentName, "fake", "empty"))
})
It("returns the names sorted", func() {
names := availableAgentNames([]string{"zz-plugin", "aa-plugin"})
Expect(slices.IsSorted(names)).To(BeTrue())
})
It("works when there are no plugins", func() {
Expect(availableAgentNames(nil)).To(ContainElement(LocalAgentName))
})
})
Describe("GetArtistMBID", func() {
It("returns on first match", func() {
Expect(ag.GetArtistMBID(ctx, "123", "test")).To(Equal("mbid"))

View File

@ -394,6 +394,7 @@
"invalidJson": "A configuração deve ser um JSON válido"
},
"messages": {
"idHelp": "O ID do plugin, derivado do nome do arquivo. Use-o ao referenciar este plugin em opções de configuração, como Agents.",
"configHelp": "Configure o plugin usando pares chave-valor. Deixe vazio se o plugin não precisa de configuração.",
"clickPermissions": "Clique em uma permissão para ver detalhes",
"noConfig": "Nenhuma configuração definida",

View File

@ -397,6 +397,7 @@
"invalidJson": "Configuration must be valid JSON"
},
"messages": {
"idHelp": "The plugin ID, derived from its file name. Use it when referencing this plugin in configuration options, such as Agents.",
"configHelp": "Configure the plugin using key-value pairs. Leave empty if the plugin requires no configuration.",
"configValidationError": "Configuration validation failed:",
"schemaRenderError": "Unable to render configuration form. The plugin's schema may be invalid.",

View File

@ -123,6 +123,13 @@ export const InfoCard = ({ record, manifest, classes, translate, isSmall }) => (
isSmall={isSmall}
>
{record.id}
<Typography
variant="caption"
color="textSecondary"
className={classes.fieldHelp}
>
{translate('resources.plugin.messages.idHelp')}
</Typography>
</InfoRow>
{manifest?.name && (
@ -201,7 +208,7 @@ export const InfoCard = ({ record, manifest, classes, translate, isSmall }) => (
<Typography
variant="caption"
color="textSecondary"
style={{ marginTop: 4, display: 'block' }}
className={classes.fieldHelp}
>
{translate('resources.plugin.messages.clickPermissions')}
</Typography>

View File

@ -0,0 +1,41 @@
import React from 'react'
import { render, screen } from '@testing-library/react'
import { describe, it, expect, vi } from 'vitest'
vi.mock('../common', () => ({
DateField: ({ source }) => <span data-testid={`date-${source}`} />,
}))
const { InfoCard } = await import('./InfoCard')
const record = {
id: 'apple-music',
path: '/data/plugins/apple-music.ndp',
updatedAt: '2026-01-01T00:00:00Z',
createdAt: '2026-01-01T00:00:00Z',
}
const renderCard = () =>
render(
<InfoCard
record={record}
manifest={{ name: 'Apple Music Metadata Agent' }}
classes={{}}
translate={(key) => key}
isSmall={false}
/>,
)
describe('InfoCard', () => {
it('shows the plugin ID', () => {
renderCard()
expect(screen.getByText('apple-music')).toBeInTheDocument()
})
it('explains that the ID is the name used in config options', () => {
renderCard()
expect(
screen.getByText('resources.plugin.messages.idHelp'),
).toBeInTheDocument()
})
})

View File

@ -45,6 +45,10 @@ export const usePluginShowStyles = makeStyles(
fontSize: '0.85rem',
wordBreak: 'break-all',
},
fieldHelp: {
marginTop: theme.spacing(0.5),
display: 'block',
},
permissionsContainer: {
display: 'flex',
flexWrap: 'wrap',