From 3ffd622de00c79bc6e802a7615174086b7307d0e Mon Sep 17 00:00:00 2001 From: rcourtman Date: Fri, 17 Jul 2026 23:38:00 +0100 Subject: [PATCH] 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. --- .../subsystems/frontend-primitives.md | 15 ++++++++++++ .../internal/subsystems/security-privacy.md | 7 +++++- .../__tests__/settingsArchitecture.test.ts | 16 +++++++++++++ .../settingsHeaderMeta.branchcov0713.test.ts | 3 ++- .../__tests__/settingsLocalization.test.ts | 5 ++-- .../__tests__/useSettingsAccess.test.tsx | 24 +++++++++++++++++++ .../components/Settings/settingsHeaderMeta.ts | 3 ++- .../components/Settings/settingsNavCatalog.ts | 10 ++++++++ .../Settings/settingsNavigationModel.ts | 4 ++++ .../components/Settings/useSettingsAccess.ts | 5 +++- frontend-modern/src/i18n/messages.de.ts | 2 +- frontend-modern/src/i18n/messages.es.ts | 2 +- frontend-modern/src/i18n/messages.ts | 2 +- .../telemetry/pulse_intelligence_evidence.go | 8 +++++++ 14 files changed, 97 insertions(+), 9 deletions(-) diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index cda0945c5..a246de7d2 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -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 diff --git a/docs/release-control/v6/internal/subsystems/security-privacy.md b/docs/release-control/v6/internal/subsystems/security-privacy.md index cd408a9d5..16169ff30 100644 --- a/docs/release-control/v6/internal/subsystems/security-privacy.md +++ b/docs/release-control/v6/internal/subsystems/security-privacy.md @@ -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 diff --git a/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts b/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts index 536a866fc..2a030c9b8 100644 --- a/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts +++ b/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts @@ -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/, diff --git a/frontend-modern/src/components/Settings/__tests__/settingsHeaderMeta.branchcov0713.test.ts b/frontend-modern/src/components/Settings/__tests__/settingsHeaderMeta.branchcov0713.test.ts index 20b267fda..e08e82060 100644 --- a/frontend-modern/src/components/Settings/__tests__/settingsHeaderMeta.branchcov0713.test.ts +++ b/frontend-modern/src/components/Settings/__tests__/settingsHeaderMeta.branchcov0713.test.ts @@ -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.', }); }); diff --git a/frontend-modern/src/components/Settings/__tests__/settingsLocalization.test.ts b/frontend-modern/src/components/Settings/__tests__/settingsLocalization.test.ts index b9f917b15..b175ad8f9 100644 --- a/frontend-modern/src/components/Settings/__tests__/settingsLocalization.test.ts +++ b/frontend-modern/src/components/Settings/__tests__/settingsLocalization.test.ts @@ -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', diff --git a/frontend-modern/src/components/Settings/__tests__/useSettingsAccess.test.tsx b/frontend-modern/src/components/Settings/__tests__/useSettingsAccess.test.tsx index d84ef40c9..f9893b4fe 100644 --- a/frontend-modern/src/components/Settings/__tests__/useSettingsAccess.test.tsx +++ b/frontend-modern/src/components/Settings/__tests__/useSettingsAccess.test.tsx @@ -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; + 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'); + }); + }, + ); }); diff --git a/frontend-modern/src/components/Settings/settingsHeaderMeta.ts b/frontend-modern/src/components/Settings/settingsHeaderMeta.ts index a2d35fb9d..d425d7af3 100644 --- a/frontend-modern/src/components/Settings/settingsHeaderMeta.ts +++ b/frontend-modern/src/components/Settings/settingsHeaderMeta.ts @@ -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', diff --git a/frontend-modern/src/components/Settings/settingsNavCatalog.ts b/frontend-modern/src/components/Settings/settingsNavCatalog.ts index a944ac00b..224528581 100644 --- a/frontend-modern/src/components/Settings/settingsNavCatalog.ts +++ b/frontend-modern/src/components/Settings/settingsNavCatalog.ts @@ -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 }, }, diff --git a/frontend-modern/src/components/Settings/settingsNavigationModel.ts b/frontend-modern/src/components/Settings/settingsNavigationModel.ts index d3eb52727..b1f3d536c 100644 --- a/frontend-modern/src/components/Settings/settingsNavigationModel.ts +++ b/frontend-modern/src/components/Settings/settingsNavigationModel.ts @@ -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; diff --git a/frontend-modern/src/components/Settings/useSettingsAccess.ts b/frontend-modern/src/components/Settings/useSettingsAccess.ts index df04c462d..d39e76fdb 100644 --- a/frontend-modern/src/components/Settings/useSettingsAccess.ts +++ b/frontend-modern/src/components/Settings/useSettingsAccess.ts @@ -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 }; }) diff --git a/frontend-modern/src/i18n/messages.de.ts b/frontend-modern/src/i18n/messages.de.ts index bb45d5524..f36d345b3 100644 --- a/frontend-modern/src/i18n/messages.de.ts +++ b/frontend-modern/src/i18n/messages.de.ts @@ -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.', diff --git a/frontend-modern/src/i18n/messages.es.ts b/frontend-modern/src/i18n/messages.es.ts index c7ca68010..7ad736dc5 100644 --- a/frontend-modern/src/i18n/messages.es.ts +++ b/frontend-modern/src/i18n/messages.es.ts @@ -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.', diff --git a/frontend-modern/src/i18n/messages.ts b/frontend-modern/src/i18n/messages.ts index a530322f4..aefea5cfe 100644 --- a/frontend-modern/src/i18n/messages.ts +++ b/frontend-modern/src/i18n/messages.ts @@ -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.', diff --git a/internal/telemetry/pulse_intelligence_evidence.go b/internal/telemetry/pulse_intelligence_evidence.go index f1e6d6591..655ede4c8 100644 --- a/internal/telemetry/pulse_intelligence_evidence.go +++ b/internal/telemetry/pulse_intelligence_evidence.go @@ -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