feat(settings): make the external-agent (MCP) connector setup findable in sidebar search

Production telemetry showed 1,437 installs on the external-agent surface but
only 2 on the Pulse MCP adapter. Live verification proved the pulse_mcp
telemetry surface attribution is honest (adapter tools/call records
surface=pulse_mcp; header-less calls record agent_api), so the gap is real -
but the funnel comparison is also misleading: agent_api counts any scoped
API-token REST consumer (scripts, dashboards polling /api/config/nodes or
/api/ai/patrol/findings), not BYO AI agents. Recorded that interpretation
constraint on PulseIntelligenceExternalAgentEvidence.

The genuine product gap: the in-app connector setup (Settings -> Pulse
Intelligence -> Assistant -> External agents) was invisible to sidebar search.
Searching 'mcp', 'claude', 'opencode', or 'connector' returned 'No settings
found' because search matched only nav labels and header descriptions.

- Add search-only keywords to SettingsNavItem and match them in
  filteredTabGroups; the Assistant item now carries mcp / model context
  protocol / external agent(s) / claude / opencode / connector / pulse-mcp.
- Name external agent (MCP) connectors in the Assistant header description
  (EN/DE/ES kept in sync), so the page header states what the page contains.
- Pin the search behavior with parameterized useSettingsAccess tests plus a
  settingsArchitecture source proof; record the keyword channel in the
  frontend-primitives contract (obligation 41) and the product-copy allowance
  in the security-privacy i18n extension point.
This commit is contained in:
rcourtman
2026-07-17 23:38:00 +01:00
parent 331758f32b
commit 3ffd622de0
14 changed files with 97 additions and 9 deletions
@@ -2307,6 +2307,21 @@ default` instead of fusing provider and badge text such as
`frontend-modern/src/components/shared/DiscoveryProvenanceMarker.tsx`, so
operators can distinguish opt-in Discovery context from API-owned resource
facts without reading a drawer-specific explanation.
41. Keep settings sidebar search able to find pages by the vocabulary users
actually type, not only by rendered copy. `SettingsNavItem.keywords` in
`frontend-modern/src/components/Settings/settingsNavigationModel.ts` is the
canonical search-only alias channel, matched alongside labels and header
descriptions in
`frontend-modern/src/components/Settings/useSettingsAccess.ts`; keywords
are never rendered and must not become a second copy surface. The
Assistant nav item must keep the external-agent connector aliases
(`mcp`, `model context protocol`, `external agent`, `claude`, `opencode`,
`connector`, `pulse-mcp`) so the pulse-mcp setup hosted on that page stays
reachable from search, and the Assistant header description must continue
to name external agent (MCP) connectors across locales. Proof lives in
`frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts`
and the parameterized search cases in
`frontend-modern/src/components/Settings/__tests__/useSettingsAccess.test.tsx`.
## Current State
@@ -299,7 +299,12 @@ the `white_label` branding entitlement.
Pulse Intelligence Provider & Models, Patrol, Assistant, and Service Context
settings labels may use the same localized catalog boundary, but those edits
must stay product-settings copy only and must not change token scope names,
preset ids, privacy disclosures, or security control terminology. Self-hosted
preset ids, privacy disclosures, or security control terminology. The
Assistant settings header naming external agent (MCP) connectors across
locales is such product-settings copy: it describes what the page hosts and
does not alter the scoped-token model, connector token handling, or any
security/privacy disclosure. `MCP` stays untranslated as a protocol
identifier. Self-hosted
Plans & Billing header and navigation localization may share that same catalog
boundary when it frames Pro setup as choosing Patrol autonomy; it must not
alter API Access, authentication, privacy, or token-management terminology in
@@ -291,6 +291,22 @@ describe('settings architecture guardrails', () => {
);
});
it('keeps the external-agent (MCP) connector setup findable from sidebar search', () => {
// The Assistant page hosts the pulse-mcp connector setup, but its label and
// copy can't carry every term users search for. The nav item's search-only
// keywords are the canonical bridge; losing them regresses "mcp"/"claude"
// searches back to "No settings found".
const assistantNavBlock = settingsNavCatalogSource.match(
/id: 'system-ai-assistant',[\s\S]*?},/,
);
for (const keyword of ['mcp', 'external agent', 'claude', 'opencode', 'connector']) {
expect(assistantNavBlock?.[0]).toContain(`'${keyword}'`);
}
expect(settingsHeaderMetaSource).toContain(
'Configure Assistant chat behavior, chat action permissions, sessions, and external agent (MCP) connectors.',
);
});
it('keeps resource privacy route-backed instead of sidebar-promoted', () => {
expect(settingsNavCatalogSource).toMatch(
/id: 'security-data-handling',[\s\S]*label: 'Resource Privacy',[\s\S]*hideFromSidebar: true/,
@@ -149,7 +149,8 @@ describe('getSettingsHeaderMeta', () => {
it('localizes system-ai-assistant to the English baseline (sibling only pinned es/de)', () => {
expect(en['system-ai-assistant']).toEqual({
title: 'Assistant',
description: 'Configure Assistant chat behavior, chat action permissions, and sessions.',
description:
'Configure Assistant chat behavior, chat action permissions, sessions, and external agent (MCP) connectors.',
});
});
@@ -81,7 +81,7 @@ describe('settings localization catalog', () => {
expect(spanishMeta['system-ai-assistant']).toEqual({
title: 'Assistant',
description:
'Configura el comportamiento del chat, los permisos de acciones y las sesiones del Assistant.',
'Configura el comportamiento del chat, los permisos de acciones, las sesiones y los conectores de agentes externos (MCP) del Assistant.',
});
expect(spanishMeta['system-billing']).toEqual({
title: 'Planes y facturacion',
@@ -99,7 +99,8 @@ describe('settings localization catalog', () => {
});
expect(getSettingsHeaderMeta('de')['system-ai-assistant']).toEqual({
title: 'Assistant',
description: 'Konfigurieren Sie Chatverhalten, Aktionsrechte und Sitzungen des Assistant.',
description:
'Konfigurieren Sie Chatverhalten, Aktionsrechte, Sitzungen und externe Agent-Verbindungen (MCP) des Assistant.',
});
expect(getSettingsHeaderMeta('de')['system-billing']).toEqual({
title: 'Plaene & Abrechnung',
@@ -130,4 +130,28 @@ describe('useSettingsAccess', () => {
expect(setActiveTabSpy).not.toHaveBeenCalled();
});
});
it.each(['mcp', 'claude', 'opencode', 'connector', 'external agent'])(
'sidebar search %j surfaces the Assistant page hosting the external-agent connector setup',
async (query) => {
shouldHideSettingsNavItemMock.mockReturnValue(false);
let access!: ReturnType<typeof useSettingsAccess>;
render(() => {
const [activeTab, setActiveTab] = createSignal('system-ai-assistant' as never);
access = useSettingsAccess({
activeTab,
setActiveTab: setActiveTab as never,
searchQuery: () => query,
});
return null;
});
await waitFor(() => {
const matchedIds = access
.filteredTabGroups()
.flatMap((group) => group.items.map((item) => item.id));
expect(matchedIds).toContain('system-ai-assistant');
});
},
);
});
@@ -42,7 +42,8 @@ export const SETTINGS_HEADER_META: SettingsHeaderMetaMap = {
},
'system-ai-assistant': {
title: 'Assistant',
description: 'Configure Assistant chat behavior, chat action permissions, and sessions.',
description:
'Configure Assistant chat behavior, chat action permissions, sessions, and external agent (MCP) connectors.',
},
'system-ai-discovery': {
title: 'Service Context',
@@ -78,6 +78,16 @@ export const SETTINGS_NAV_GROUPS: SettingsNavGroup[] = [
{
id: 'system-ai-assistant',
label: 'Assistant',
keywords: [
'mcp',
'model context protocol',
'external agent',
'external agents',
'claude',
'opencode',
'connector',
'pulse-mcp',
],
icon: Terminal,
iconProps: { strokeWidth: 2 },
},
@@ -71,6 +71,10 @@ export type SettingsNavGroupId =
export interface SettingsNavItem {
id: SettingsTab;
label: string;
// Search-only aliases (never rendered). Lets sidebar search find a page by
// terms its label/description can't carry, e.g. "mcp" or "claude" for the
// Assistant page's external-agent connector setup.
keywords?: string[];
icon: Component<{ class?: string; strokeWidth?: number }>;
iconProps?: { strokeWidth?: number };
hideFromSidebar?: boolean;
@@ -144,7 +144,10 @@ export function useSettingsAccess({
const matchLabel = item.label.toLowerCase().includes(q);
const description = settingsHeaderMeta()[item.id]?.description?.toLowerCase() || '';
const matchDesc = description.includes(q);
return matchLabel || matchDesc;
const matchKeyword = (item.keywords ?? []).some((keyword) =>
keyword.toLowerCase().includes(q),
);
return matchLabel || matchDesc || matchKeyword;
});
return { ...group, items: filteredItems };
})
+1 -1
View File
@@ -403,7 +403,7 @@ export const DE_MESSAGE_OVERRIDES = {
'Konfigurieren Sie Anbieter, Standardmodelle, Anbieterzustand, Budget und Nutzung fuer Pulse Intelligence.',
'settings.header.systemAi.title': 'Anbieter & Modelle',
'settings.header.systemAiAssistant.description':
'Konfigurieren Sie Chatverhalten, Aktionsrechte und Sitzungen des Assistant.',
'Konfigurieren Sie Chatverhalten, Aktionsrechte, Sitzungen und externe Agent-Verbindungen (MCP) des Assistant.',
'settings.header.systemAiAssistant.title': 'Assistant',
'settings.header.systemAiDiscovery.description':
'Konfigurieren Sie den KI-gestuetzten Service-Kontext fuer Assistant und Patrol. Infrastruktur-Erkennung und Onboarding bleiben unter Infrastruktur.',
+1 -1
View File
@@ -395,7 +395,7 @@ export const ES_MESSAGE_OVERRIDES = {
'Configura proveedores, modelos predeterminados, salud de proveedores, presupuesto y uso para Pulse Intelligence.',
'settings.header.systemAi.title': 'Proveedores y modelos',
'settings.header.systemAiAssistant.description':
'Configura el comportamiento del chat, los permisos de acciones y las sesiones del Assistant.',
'Configura el comportamiento del chat, los permisos de acciones, las sesiones y los conectores de agentes externos (MCP) del Assistant.',
'settings.header.systemAiAssistant.title': 'Assistant',
'settings.header.systemAiDiscovery.description':
'Configura el contexto de servicio asistido por IA que usan Assistant y Patrol. El descubrimiento y onboarding de infraestructura permanecen en Infraestructura.',
+1 -1
View File
@@ -388,7 +388,7 @@ export const EN_MESSAGES = {
'Configure providers, default models, provider health, budget, and usage for Pulse Intelligence.',
'settings.header.systemAi.title': 'Provider & Models',
'settings.header.systemAiAssistant.description':
'Configure Assistant chat behavior, chat action permissions, and sessions.',
'Configure Assistant chat behavior, chat action permissions, sessions, and external agent (MCP) connectors.',
'settings.header.systemAiAssistant.title': 'Assistant',
'settings.header.systemAiDiscovery.description':
'Configure the model-backed service context Assistant and Patrol use. Infrastructure discovery and onboarding stay under Infrastructure.',
@@ -55,6 +55,14 @@ func PulseIntelligenceAssistantContextEvent(event config.AIUsageEventRecord) boo
// PulseIntelligenceExternalAgentEvidence is the content-free subset of
// authenticated agent/MCP route activity used to prove external collaboration.
//
// Interpretation constraint: the agent_api surface counts any suitably scoped
// API-token request to manifest-listed routes — including plain REST consumers
// like scripts and dashboards hitting GET /api/config/nodes or
// /api/ai/patrol/findings — so Used does NOT imply a BYO AI agent. Only
// MCPAdapterUsed (the pulse_mcp surface header) certainly identifies an MCP
// client. Do not read Used-vs-MCPAdapterUsed as an adoption funnel of the
// same population.
type PulseIntelligenceExternalAgentEvidence struct {
Used bool
MCPAdapterUsed bool