diff --git a/core/agents/agents.go b/core/agents/agents.go
index ac623951c..8ae3124ef 100644
--- a/core/agents/agents.go
+++ b/core/agents/agents.go
@@ -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)
diff --git a/core/agents/agents_test.go b/core/agents/agents_test.go
index 35ebf18d8..6163c7f3c 100644
--- a/core/agents/agents_test.go
+++ b/core/agents/agents_test.go
@@ -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"))
diff --git a/resources/i18n/pt-br.json b/resources/i18n/pt-br.json
index a4ad6bc8c..ccc5f872b 100644
--- a/resources/i18n/pt-br.json
+++ b/resources/i18n/pt-br.json
@@ -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",
diff --git a/ui/src/i18n/en.json b/ui/src/i18n/en.json
index 8823a6749..de96d47c0 100644
--- a/ui/src/i18n/en.json
+++ b/ui/src/i18n/en.json
@@ -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.",
diff --git a/ui/src/plugin/InfoCard.jsx b/ui/src/plugin/InfoCard.jsx
index 8fb6853fe..3a7bb24c7 100644
--- a/ui/src/plugin/InfoCard.jsx
+++ b/ui/src/plugin/InfoCard.jsx
@@ -123,6 +123,13 @@ export const InfoCard = ({ record, manifest, classes, translate, isSmall }) => (
isSmall={isSmall}
>
{record.id}
+
+ {translate('resources.plugin.messages.idHelp')}
+
{manifest?.name && (
@@ -201,7 +208,7 @@ export const InfoCard = ({ record, manifest, classes, translate, isSmall }) => (
{translate('resources.plugin.messages.clickPermissions')}
diff --git a/ui/src/plugin/InfoCard.test.jsx b/ui/src/plugin/InfoCard.test.jsx
new file mode 100644
index 000000000..24fa48caa
--- /dev/null
+++ b/ui/src/plugin/InfoCard.test.jsx
@@ -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 }) => ,
+}))
+
+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(
+ 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()
+ })
+})
diff --git a/ui/src/plugin/styles.js b/ui/src/plugin/styles.js
index 104d8bc0f..68c0be929 100644
--- a/ui/src/plugin/styles.js
+++ b/ui/src/plugin/styles.js
@@ -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',