From 2d9c30dbedac2573d2e9a2ab3d6214101f3f9b08 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Wed, 15 Apr 2026 02:04:05 +0100 Subject: [PATCH] Clarify assistant and patrol settings ownership --- .../subsystems/frontend-primitives.md | 8 +++- .../Settings/AIChatMaintenanceSection.tsx | 13 +++--- .../Settings/AIModelSelectionSection.tsx | 30 ++++++++----- .../Settings/AIRuntimeControlsSection.tsx | 14 +++++-- .../components/Settings/AISettingsDialogs.tsx | 30 +++---------- .../Settings/__tests__/AISettings.test.tsx | 6 ++- .../__tests__/settingsArchitecture.test.ts | 10 +++-- .../components/Settings/useAISettingsState.ts | 4 +- .../__tests__/aiSettingsPresentation.test.ts | 20 +++++++++ .../src/utils/aiSettingsPresentation.ts | 42 +++++++++++++++++++ .../tests/51-quickstart-cross-surface.spec.ts | 4 +- .../52-ai-settings-provider-setup.spec.ts | 8 ++-- 12 files changed, 132 insertions(+), 57 deletions(-) diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index e37bd0ddc..860f11fd7 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -311,7 +311,13 @@ work extends shared components instead of creating new local variants. controls inside `frontend-modern/src/components/Settings/AIRuntimeControlsSection.tsx` must likewise describe discovery as workload discovery that supplies concrete service context to Pulse Assistant and Patrol, not as a generic - AI context feature. + AI context feature. Assistant-only controls inside the shared shell, such + as execution permissions and session maintenance, must stay explicitly + labeled as Pulse Assistant controls, while Patrol schedule and autonomy + continue to live on Patrol-owned surfaces rather than drifting back into + the shared settings shell. Shared/default model choices may remain on the + combined shell only when Assistant and Patrol overrides are presented as + explicit per-surface overrides instead of a generic advanced AI bucket. ## Forbidden Paths diff --git a/frontend-modern/src/components/Settings/AIChatMaintenanceSection.tsx b/frontend-modern/src/components/Settings/AIChatMaintenanceSection.tsx index 269ec3508..ec1746434 100644 --- a/frontend-modern/src/components/Settings/AIChatMaintenanceSection.tsx +++ b/frontend-modern/src/components/Settings/AIChatMaintenanceSection.tsx @@ -1,6 +1,7 @@ import { Component, For, Show } from 'solid-js'; import type { AISettingsState } from '@/components/Settings/useAISettingsState'; import { + AI_SETTINGS_ASSISTANT_SESSIONS_TITLE, getAIChatSessionsEmptyState, getAIChatSessionsLoadingState, } from '@/utils/aiSettingsPresentation'; @@ -34,7 +35,9 @@ export const AIChatMaintenanceSection: Component d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4" /> - Chat Session Maintenance + + {AI_SETTINGS_ASSISTANT_SESSIONS_TITLE} +

- Use this panel to summarize, inspect, or revert a specific chat session. It does not - change your default Pulse Assistant settings. + Summarize, inspect, or revert a specific Pulse Assistant session. It does not change + Patrol settings or your shared provider and model defaults.

@@ -101,7 +104,7 @@ export const AIChatMaintenanceSection: Component disabled={!state.selectedSessionId() || state.sessionActionLoading() !== null} class="w-full sm:w-auto min-h-10 sm:min-h-9 px-3 py-2 text-sm font-medium rounded border border-border bg-surface text-base-content hover:bg-surface-hover disabled:opacity-50" > - {state.sessionActionLoading() === 'summarize' ? 'Summarizing...' : 'Summarize context'} + {state.sessionActionLoading() === 'summarize' ? 'Summarizing...' : 'Summarize session'}
@@ -174,7 +178,7 @@ export const AIModelSelectionSection: Component = d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4" /> - Advanced Model Selection + {AI_SETTINGS_MODEL_OVERRIDES_TITLE} Customized @@ -193,12 +197,13 @@ export const AIModelSelectionSection: Component =

- Override the default model for specific tasks. Leave empty to use the default. + Override the shared default for Pulse Assistant or Patrol. Leave empty to use the + shared default model.

- +

- Used for chat and fix execution — a more capable model is recommended. + Used for live chat and approved fix execution — a more capable model is recommended.

0} @@ -207,7 +212,7 @@ export const AIModelSelectionSection: Component = type="text" value={state.form.chatModel} onInput={(e) => state.setForm('chatModel', e.currentTarget.value)} - placeholder="Use default model" + placeholder="Use shared default model" class={controlClass()} disabled={state.saving()} /> @@ -220,7 +225,7 @@ export const AIModelSelectionSection: Component = disabled={state.saving()} > {([provider, models]) => ( @@ -237,9 +242,12 @@ export const AIModelSelectionSection: Component =
- +

- Runs frequently for detection — a smaller, cheaper model keeps costs low. + Used for recurring verification and finding generation — a smaller, cheaper model + keeps costs low.

0} @@ -248,7 +256,7 @@ export const AIModelSelectionSection: Component = type="text" value={state.form.patrolModel} onInput={(e) => state.setForm('patrolModel', e.currentTarget.value)} - placeholder="Use default model" + placeholder="Use shared default model" class={controlClass()} disabled={state.saving()} /> @@ -261,7 +269,7 @@ export const AIModelSelectionSection: Component = disabled={state.saving()} > {([provider, models]) => ( diff --git a/frontend-modern/src/components/Settings/AIRuntimeControlsSection.tsx b/frontend-modern/src/components/Settings/AIRuntimeControlsSection.tsx index 14057f5ee..9ab017175 100644 --- a/frontend-modern/src/components/Settings/AIRuntimeControlsSection.tsx +++ b/frontend-modern/src/components/Settings/AIRuntimeControlsSection.tsx @@ -11,6 +11,7 @@ import { getAIControlLevelPanelClass, } from '@/utils/aiControlLevelPresentation'; import { + AI_SETTINGS_ASSISTANT_PERMISSIONS_TITLE, getAISettingsWorkloadDiscoveryHelpContent, getAISettingsWorkloadDiscoverySummary, } from '@/utils/aiSettingsPresentation'; @@ -198,7 +199,9 @@ export const AIRuntimeControlsSection: Component d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" /> - Pulse Permission Level + + {AI_SETTINGS_ASSISTANT_PERMISSIONS_TITLE} + {state.form.controlLevel} @@ -207,7 +210,7 @@ export const AIRuntimeControlsSection: Component
- + disabled={state.saving()} />

- Comma-separated VMIDs or names that Pulse Assistant cannot control + Comma-separated VMIDs or names that Pulse Assistant cannot control, even when + command execution is enabled.

diff --git a/frontend-modern/src/components/Settings/AISettingsDialogs.tsx b/frontend-modern/src/components/Settings/AISettingsDialogs.tsx index 2d2cc3188..69378b5a0 100644 --- a/frontend-modern/src/components/Settings/AISettingsDialogs.tsx +++ b/frontend-modern/src/components/Settings/AISettingsDialogs.tsx @@ -2,6 +2,7 @@ import { For, Show, type Accessor, type Component, type Setter } from 'solid-js' import { Dialog } from '@/components/shared/Dialog'; import { SelectionCardGroup } from '@/components/shared/SelectionCardGroup'; import { getAISessionDiffStatusPresentation } from '@/utils/aiSessionDiffPresentation'; +import { getAISettingsSetupDialogPresentation } from '@/utils/aiSettingsPresentation'; import { RELAY_ONBOARDING_TRIAL_STARTING_LABEL } from '@/utils/relayPresentation'; import type { FileChange } from '@/api/aiChat'; import type { AIProvider } from '@/types/ai'; @@ -36,26 +37,7 @@ export interface AISettingsDialogsProps { export const AISettingsDialogs: Component = (props) => { const setupProviderConfig = () => getAIProviderConfig(props.setupProvider()); - const setupTitle = () => { - switch (props.setupMode()) { - case 'activation-or-provider': - return 'Activate quickstart or connect a provider'; - case 'provider-required': - return 'Connect a provider to continue'; - default: - return 'Set Up Pulse Assistant'; - } - }; - const setupDescription = () => { - switch (props.setupMode()) { - case 'activation-or-provider': - return 'Start a trial to unlock Patrol quickstart, or connect your own provider below.'; - case 'provider-required': - return 'Patrol quickstart is not currently available. Connect a provider to continue.'; - default: - return 'Choose a provider to get started'; - } - }; + const setupPresentation = () => getAISettingsSetupDialogPresentation(props.setupMode()); return ( <> @@ -120,12 +102,12 @@ export const AISettingsDialogs: Component = (props) => { onClose={props.handleCloseSetupModal} panelClass="max-w-md" closeOnBackdrop={false} - ariaLabel="Set up Pulse Assistant" + ariaLabel={setupPresentation().ariaLabel} >
-

{setupTitle()}

-

{setupDescription()}

+

{setupPresentation().title}

+

{setupPresentation().description}

@@ -233,7 +215,7 @@ export const AISettingsDialogs: Component = (props) => { {props.setupSaving() && ( )} - Enable Pulse Assistant + {setupPresentation().submitLabel}
diff --git a/frontend-modern/src/components/Settings/__tests__/AISettings.test.tsx b/frontend-modern/src/components/Settings/__tests__/AISettings.test.tsx index 275863c43..5310a01ff 100644 --- a/frontend-modern/src/components/Settings/__tests__/AISettings.test.tsx +++ b/frontend-modern/src/components/Settings/__tests__/AISettings.test.tsx @@ -477,8 +477,10 @@ describe('AISettings quickstart enablement flow', () => { expect(updateSettingsMock).toHaveBeenCalledWith({ enabled: true }); }); - expect(screen.queryByText('Choose a provider to get started')).not.toBeInTheDocument(); - expect(notificationSuccessMock).toHaveBeenCalledWith('Pulse Assistant enabled'); + expect( + screen.queryByText('Connect a provider to power Pulse Assistant and Patrol.'), + ).not.toBeInTheDocument(); + expect(notificationSuccessMock).toHaveBeenCalledWith('Assistant & Patrol enabled'); }); it('shows activation-aware setup guidance instead of generic provider setup when quickstart is blocked', async () => { diff --git a/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts b/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts index 68f7da1c8..b5ea29260 100644 --- a/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts +++ b/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts @@ -1115,7 +1115,8 @@ describe('Settings architecture guardrails', () => { '@/components/Settings/AIProviderConfigurationSection', ); expect(aiModelSelectionSectionSource).toContain('@/components/Settings/aiSettingsModel'); - expect(aiModelSelectionSectionSource).toContain('Advanced Model Selection'); + expect(aiModelSelectionSectionSource).toContain('@/utils/aiSettingsPresentation'); + expect(aiModelSelectionSectionSource).toContain('AI_SETTINGS_MODEL_OVERRIDES_TITLE'); expect(aiRuntimeControlsSectionSource).toContain('@/components/shared/UpgradeLink'); expect(aiRuntimeControlsSectionSource).toContain('@/utils/upgradePresentation'); expect(aiRuntimeControlsSectionSource).toContain('UPGRADE_ACTION_LABEL'); @@ -1123,17 +1124,20 @@ describe('Settings architecture guardrails', () => { expect(aiRuntimeControlsSectionSource).toContain('Workload Discovery'); expect(aiRuntimeControlsSectionSource).toContain('getAISettingsWorkloadDiscoveryHelpContent'); expect(aiRuntimeControlsSectionSource).toContain('getAISettingsWorkloadDiscoverySummary'); - expect(aiRuntimeControlsSectionSource).toContain('Pulse Permission Level'); + expect(aiRuntimeControlsSectionSource).toContain('@/utils/aiSettingsPresentation'); + expect(aiRuntimeControlsSectionSource).toContain('AI_SETTINGS_ASSISTANT_PERMISSIONS_TITLE'); expect(aiRuntimeControlsSectionSource).toContain('destination={state.upgradeAutofixDestination()}'); expect(aiRuntimeControlsSectionSource).not.toContain('href={state.upgradeAutofixDestination().href}'); expect(aiRuntimeControlsSectionSource).not.toContain('window.open(state.upgradeAutofixDestination().href'); expect(aiRuntimeControlsSectionSource).not.toContain('>Upgrade to Pro<'); expect(aiRuntimeControlsSectionSource).not.toContain('>Start free trial<'); - expect(aiChatMaintenanceSectionSource).toContain('Chat Session Maintenance'); + expect(aiChatMaintenanceSectionSource).toContain('@/utils/aiSettingsPresentation'); + expect(aiChatMaintenanceSectionSource).toContain('AI_SETTINGS_ASSISTANT_SESSIONS_TITLE'); expect(aiSettingsStatusAndActionsSource).toContain('Save changes'); expect(aiSettingsStatusAndActionsSource).toContain('Test Connection'); expect(aiProviderConfigurationSectionSource).toContain('@/components/Settings/aiSettingsModel'); expect(aiSettingsDialogsSource).toContain('@/components/Settings/aiSettingsModel'); + expect(aiSettingsDialogsSource).toContain('getAISettingsSetupDialogPresentation'); expect(aiSettingsModelSource).toContain('export const AI_PROVIDER_CONFIGS'); expect(aiSettingsModelSource).toContain('export const AI_SETUP_PROVIDER_OPTIONS'); expect(aiSettingsStateSource).toContain('export const useAISettingsState ='); diff --git a/frontend-modern/src/components/Settings/useAISettingsState.ts b/frontend-modern/src/components/Settings/useAISettingsState.ts index 1042ef1c7..6e0bffccb 100644 --- a/frontend-modern/src/components/Settings/useAISettingsState.ts +++ b/frontend-modern/src/components/Settings/useAISettingsState.ts @@ -489,7 +489,7 @@ export const useAISettingsState = () => { syncModelCatalogForSettings(updated); void runProviderPreflight(updated); handleCloseSetupModal(); - notificationStore.success('Pulse Assistant enabled! You can customize settings below.'); + notificationStore.success('Assistant & Patrol enabled! You can customize settings below.'); } catch (error) { logger.error('[AISettings] Setup failed:', error); notificationStore.error(error instanceof Error ? error.message : 'Setup failed'); @@ -811,7 +811,7 @@ export const useAISettingsState = () => { setSettings(updated); syncModelCatalogForSettings(updated); void runProviderPreflight(updated); - notificationStore.success(newValue ? 'Pulse Assistant enabled' : 'Pulse Assistant disabled'); + notificationStore.success(newValue ? 'Assistant & Patrol enabled' : 'Assistant & Patrol disabled'); } catch (error) { setForm('enabled', !newValue); logger.error('[AISettings] Failed to toggle AI:', error); diff --git a/frontend-modern/src/utils/__tests__/aiSettingsPresentation.test.ts b/frontend-modern/src/utils/__tests__/aiSettingsPresentation.test.ts index 98842b38d..a182de59a 100644 --- a/frontend-modern/src/utils/__tests__/aiSettingsPresentation.test.ts +++ b/frontend-modern/src/utils/__tests__/aiSettingsPresentation.test.ts @@ -1,5 +1,8 @@ import { describe, expect, it } from 'vitest'; import { + AI_SETTINGS_ASSISTANT_PERMISSIONS_TITLE, + AI_SETTINGS_ASSISTANT_SESSIONS_TITLE, + AI_SETTINGS_MODEL_OVERRIDES_TITLE, AI_SETTINGS_PANEL_DESCRIPTION, AI_SETTINGS_PANEL_TITLE, getAICredentialsClearErrorMessage, @@ -17,6 +20,7 @@ import { getAISettingsReadinessPresentation, getAISettingsRetryLabel, getAISettingsSaveErrorMessage, + getAISettingsSetupDialogPresentation, getAISettingsToggleErrorMessage, getAISettingsWorkloadDiscoveryHelpContent, getAISettingsWorkloadDiscoverySummary, @@ -28,6 +32,9 @@ describe('aiSettingsPresentation', () => { expect(AI_SETTINGS_PANEL_DESCRIPTION).toBe( 'Configure providers and models for Pulse Assistant and Patrol.', ); + expect(AI_SETTINGS_MODEL_OVERRIDES_TITLE).toBe('Assistant & Patrol Model Overrides'); + expect(AI_SETTINGS_ASSISTANT_SESSIONS_TITLE).toBe('Pulse Assistant Sessions'); + expect(AI_SETTINGS_ASSISTANT_PERMISSIONS_TITLE).toBe('Pulse Assistant Permissions'); expect(getAISettingsWorkloadDiscoveryHelpContent()).toEqual({ title: 'What is workload discovery?', description: @@ -36,6 +43,19 @@ describe('aiSettingsPresentation', () => { expect(getAISettingsWorkloadDiscoverySummary()).toEqual({ text: 'Workload discovery gives Pulse Assistant and Patrol concrete service context, so chat responses and verification findings can reference real services and commands instead of generic advice.', }); + expect(getAISettingsSetupDialogPresentation('provider')).toEqual({ + ariaLabel: 'Set up Assistant and Patrol', + title: 'Set Up Assistant & Patrol', + description: 'Connect a provider to power Pulse Assistant and Patrol.', + submitLabel: 'Enable Assistant & Patrol', + }); + expect(getAISettingsSetupDialogPresentation('activation-or-provider')).toEqual({ + ariaLabel: 'Activate quickstart or connect a provider', + title: 'Activate quickstart or connect a provider', + description: + 'Start a trial to unlock Patrol quickstart, or connect your own provider for Pulse Assistant and Patrol.', + submitLabel: 'Enable Assistant & Patrol', + }); }); it('returns the canonical provider-backed ready presentation', () => { diff --git a/frontend-modern/src/utils/aiSettingsPresentation.ts b/frontend-modern/src/utils/aiSettingsPresentation.ts index 386fdc851..22ab7b334 100644 --- a/frontend-modern/src/utils/aiSettingsPresentation.ts +++ b/frontend-modern/src/utils/aiSettingsPresentation.ts @@ -24,12 +24,24 @@ const AI_OAUTH_ERROR_MESSAGES: Record = { export const AI_SETTINGS_PANEL_TITLE = 'Assistant & Patrol'; export const AI_SETTINGS_PANEL_DESCRIPTION = 'Configure providers and models for Pulse Assistant and Patrol.'; +export const AI_SETTINGS_MODEL_OVERRIDES_TITLE = 'Assistant & Patrol Model Overrides'; +export const AI_SETTINGS_ASSISTANT_SESSIONS_TITLE = 'Pulse Assistant Sessions'; +export const AI_SETTINGS_ASSISTANT_PERMISSIONS_TITLE = 'Pulse Assistant Permissions'; export const AI_SETTINGS_LOAD_MODELS_ERROR = 'Unable to load models.'; export const AI_SETTINGS_LOAD_CHAT_SESSIONS_ERROR = 'Unable to load chat sessions.'; export const AI_SETTINGS_LOAD_FAILURE_MESSAGE = 'Unable to load Assistant & Patrol settings. Your configuration could not be retrieved.'; export const AI_SETTINGS_LOAD_RETRY_LABEL = 'Retry'; +export type AISettingsSetupMode = 'provider' | 'activation-or-provider' | 'provider-required'; + +export interface AISettingsSetupDialogPresentation { + ariaLabel: string; + description: string; + submitLabel: string; + title: string; +} + export function getAIProviderTestResultTextClass(success: boolean): string { return success ? 'text-green-600' : 'text-red-600'; } @@ -48,6 +60,36 @@ export function getAISettingsWorkloadDiscoverySummary() { } as const; } +export function getAISettingsSetupDialogPresentation( + mode: AISettingsSetupMode, +): AISettingsSetupDialogPresentation { + switch (mode) { + case 'activation-or-provider': + return { + ariaLabel: 'Activate quickstart or connect a provider', + title: 'Activate quickstart or connect a provider', + description: + 'Start a trial to unlock Patrol quickstart, or connect your own provider for Pulse Assistant and Patrol.', + submitLabel: 'Enable Assistant & Patrol', + }; + case 'provider-required': + return { + ariaLabel: 'Connect a provider to continue', + title: 'Connect a provider to continue', + description: + 'Patrol quickstart is not currently available. Connect a provider for Pulse Assistant and Patrol.', + submitLabel: 'Enable Assistant & Patrol', + }; + default: + return { + ariaLabel: 'Set up Assistant and Patrol', + title: 'Set Up Assistant & Patrol', + description: 'Connect a provider to power Pulse Assistant and Patrol.', + submitLabel: 'Enable Assistant & Patrol', + }; + } +} + export function getAISettingsReadinessPresentation( input: AISettingsReadinessInput, ): AISettingsReadinessPresentation { diff --git a/tests/integration/tests/51-quickstart-cross-surface.spec.ts b/tests/integration/tests/51-quickstart-cross-surface.spec.ts index b7b4e957e..807fe9f4f 100644 --- a/tests/integration/tests/51-quickstart-cross-surface.spec.ts +++ b/tests/integration/tests/51-quickstart-cross-surface.spec.ts @@ -658,6 +658,8 @@ test.describe("Quickstart cross-surface browser contract", () => { await expect.poll(() => surface.updateRequests.length).toBe(1); expect(surface.updateRequests[0]).toMatchObject({ enabled: true }); - await expect(page.getByText("Choose a provider to get started")).toHaveCount(0); + await expect( + page.getByText("Connect a provider to power Pulse Assistant and Patrol."), + ).toHaveCount(0); }); }); diff --git a/tests/integration/tests/52-ai-settings-provider-setup.spec.ts b/tests/integration/tests/52-ai-settings-provider-setup.spec.ts index e67bdbe81..0799b5ef8 100644 --- a/tests/integration/tests/52-ai-settings-provider-setup.spec.ts +++ b/tests/integration/tests/52-ai-settings-provider-setup.spec.ts @@ -113,12 +113,12 @@ test.describe("Assistant & Patrol settings provider setup", () => { ).toBeVisible(); await page.getByRole("button", { name: /enable assistant and patrol/i }).click(); - const setupDialog = page.getByRole("dialog", { name: "Set up Pulse Assistant" }); - await expect(setupDialog.getByText("Set Up Pulse Assistant")).toBeVisible(); + const setupDialog = page.getByRole("dialog", { name: "Set up Assistant and Patrol" }); + await expect(setupDialog.getByText("Set Up Assistant & Patrol")).toBeVisible(); await setupDialog.getByRole("button", { name: /OpenRouter/i }).click(); await setupDialog.getByPlaceholder("sk-or-...").fill("sk-or-runtime-selected"); - await setupDialog.getByRole("button", { name: "Enable Pulse Assistant" }).click(); + await setupDialog.getByRole("button", { name: "Enable Assistant & Patrol" }).click(); await expect.poll(() => updateRequests.length).toBe(1); expect(updateRequests[0]).toEqual({ @@ -127,7 +127,7 @@ test.describe("Assistant & Patrol settings provider setup", () => { }); expect(updateRequests[0]).not.toHaveProperty("model"); - await expect(page.getByText("Advanced Model Selection")).toBeVisible(); + await expect(page.getByText("Assistant & Patrol Model Overrides")).toBeVisible(); const workloadDiscoveryToggle = page.getByRole("button", { name: /workload discovery/i }); await expect(workloadDiscoveryToggle).toBeVisible(); await workloadDiscoveryToggle.click();