Clarify assistant and patrol settings ownership

This commit is contained in:
rcourtman 2026-04-15 02:04:05 +01:00
parent 1175c1d2be
commit 2d9c30dbed
12 changed files with 132 additions and 57 deletions

View file

@ -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

View file

@ -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<AIChatMaintenanceSectionProps>
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"
/>
</svg>
<span class="text-sm font-medium text-base-content">Chat Session Maintenance</span>
<span class="text-sm font-medium text-base-content">
{AI_SETTINGS_ASSISTANT_SESSIONS_TITLE}
</span>
</div>
<svg
class={`w-4 h-4 transition-transform ${state.showChatMaintenance() ? 'rotate-180' : ''}`}
@ -48,8 +51,8 @@ export const AIChatMaintenanceSection: Component<AIChatMaintenanceSectionProps>
<Show when={state.showChatMaintenance()}>
<div class="px-3 py-3 bg-surface border-t border-border space-y-3">
<p class="text-xs text-muted">
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.
</p>
<div class="flex items-center justify-between">
<label class="text-xs font-medium text-muted">Session</label>
@ -101,7 +104,7 @@ export const AIChatMaintenanceSection: Component<AIChatMaintenanceSectionProps>
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'}
</button>
<button
type="button"
@ -109,7 +112,7 @@ export const AIChatMaintenanceSection: Component<AIChatMaintenanceSectionProps>
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() === 'diff' ? 'Loading...' : 'View file changes'}
{state.sessionActionLoading() === 'diff' ? 'Loading...' : 'View session changes'}
</button>
<button
type="button"

View file

@ -7,6 +7,7 @@ import {
} from '@/components/Settings/aiSettingsModel';
import type { AISettingsState } from '@/components/Settings/useAISettingsState';
import { formField, labelClass, controlClass } from '@/components/shared/Form';
import { AI_SETTINGS_MODEL_OVERRIDES_TITLE } from '@/utils/aiSettingsPresentation';
import { getAIProviderDisplayName, getProviderFromModelId } from '@/utils/aiProviderPresentation';
interface AIModelSelectionSectionProps {
@ -22,7 +23,7 @@ export const AIModelSelectionSection: Component<AIModelSelectionSectionProps> =
<div class={formField}>
<div class="flex items-center justify-between mb-1">
<label class={labelClass()}>
Default Model
Shared Default Model
{state.modelsLoading() && <span class="ml-2 text-xs text-slate-500">(loading...)</span>}
</label>
<button
@ -157,6 +158,9 @@ export const AIModelSelectionSection: Component<AIModelSelectionSectionProps> =
to be configured. Add an API key below or select a different model.
</p>
</Show>
<p class="text-[11px] text-muted mt-1">
Used by both Pulse Assistant and Patrol unless you set a section-specific override below.
</p>
</div>
<div class="border border-border rounded-md overflow-hidden">
@ -174,7 +178,7 @@ export const AIModelSelectionSection: Component<AIModelSelectionSectionProps> =
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"
/>
</svg>
<span class="text-sm font-medium text-base-content">Advanced Model Selection</span>
<span class="text-sm font-medium text-base-content">{AI_SETTINGS_MODEL_OVERRIDES_TITLE}</span>
<Show when={state.form.chatModel || state.form.patrolModel}>
<span class="px-1.5 py-0.5 text-[10px] font-semibold bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300 rounded">
Customized
@ -193,12 +197,13 @@ export const AIModelSelectionSection: Component<AIModelSelectionSectionProps> =
<Show when={state.showAdvancedModels()}>
<div class="px-3 py-3 bg-surface border-t border-border space-y-3">
<p class="text-xs text-muted">
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.
</p>
<div>
<label class="block text-xs font-medium text-muted mb-0.5">Chat Model (Interactive)</label>
<label class="block text-xs font-medium text-muted mb-0.5">Pulse Assistant Model</label>
<p class="text-[11px] text-muted mb-1">
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.
</p>
<Show
when={state.availableModels().length > 0}
@ -207,7 +212,7 @@ export const AIModelSelectionSection: Component<AIModelSelectionSectionProps> =
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<AIModelSelectionSectionProps> =
disabled={state.saving()}
>
<option value="">
Use default ({state.form.model?.split(':').pop() || 'not set'})
Use shared default ({state.form.model?.split(':').pop() || 'not set'})
</option>
<For each={groupedModels()}>
{([provider, models]) => (
@ -237,9 +242,12 @@ export const AIModelSelectionSection: Component<AIModelSelectionSectionProps> =
</Show>
</div>
<div>
<label class="block text-xs font-medium text-muted mb-0.5">Patrol Model (Background)</label>
<label class="block text-xs font-medium text-muted mb-0.5">
Patrol Verification Model
</label>
<p class="text-[11px] text-muted mb-1">
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.
</p>
<Show
when={state.availableModels().length > 0}
@ -248,7 +256,7 @@ export const AIModelSelectionSection: Component<AIModelSelectionSectionProps> =
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<AIModelSelectionSectionProps> =
disabled={state.saving()}
>
<option value="">
Use default ({state.form.model?.split(':').pop() || 'not set'})
Use shared default ({state.form.model?.split(':').pop() || 'not set'})
</option>
<For each={groupedModels()}>
{([provider, models]) => (

View file

@ -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<AIRuntimeControlsSectionProps>
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"
/>
</svg>
<span class="text-sm font-medium text-base-content">Pulse Permission Level</span>
<span class="text-sm font-medium text-base-content">
{AI_SETTINGS_ASSISTANT_PERMISSIONS_TITLE}
</span>
<Show when={state.form.controlLevel !== 'read_only'}>
<span class={`px-1.5 py-0.5 text-[10px] font-medium rounded ${getAIControlLevelBadgeClass(state.form.controlLevel)}`}>
{state.form.controlLevel}
@ -207,7 +210,7 @@ export const AIRuntimeControlsSection: Component<AIRuntimeControlsSectionProps>
</div>
<div class="flex items-center gap-3">
<label class="text-xs font-medium text-muted w-28 flex-shrink-0">Permission</label>
<label class="text-xs font-medium text-muted w-28 flex-shrink-0">Control mode</label>
<select
value={state.form.controlLevel}
onChange={(e) => state.setForm('controlLevel', e.currentTarget.value as AIControlLevel)}
@ -263,7 +266,9 @@ export const AIRuntimeControlsSection: Component<AIRuntimeControlsSectionProps>
<Show when={state.form.controlLevel !== 'read_only'}>
<div class="flex items-start gap-3 pt-2 border-t border-blue-200 dark:border-blue-700">
<label class="text-xs font-medium text-muted w-28 flex-shrink-0 pt-1">Protected</label>
<label class="text-xs font-medium text-muted w-28 flex-shrink-0 pt-1">
Protected guests
</label>
<div class="flex-1">
<input
type="text"
@ -274,7 +279,8 @@ export const AIRuntimeControlsSection: Component<AIRuntimeControlsSectionProps>
disabled={state.saving()}
/>
<p class="text-[10px] text-muted mt-1">
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.
</p>
</div>
</div>

View file

@ -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<AISettingsDialogsProps> = (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<AISettingsDialogsProps> = (props) => {
onClose={props.handleCloseSetupModal}
panelClass="max-w-md"
closeOnBackdrop={false}
ariaLabel="Set up Pulse Assistant"
ariaLabel={setupPresentation().ariaLabel}
>
<div class="w-full overflow-hidden">
<div class="bg-blue-600 px-6 py-4">
<h3 class="text-lg font-semibold text-white">{setupTitle()}</h3>
<p class="text-blue-100 text-sm mt-1">{setupDescription()}</p>
<h3 class="text-lg font-semibold text-white">{setupPresentation().title}</h3>
<p class="text-blue-100 text-sm mt-1">{setupPresentation().description}</p>
</div>
<div class="p-6 space-y-4">
@ -233,7 +215,7 @@ export const AISettingsDialogs: Component<AISettingsDialogsProps> = (props) => {
{props.setupSaving() && (
<span class="h-4 w-4 border-2 border-white border-t-transparent rounded-full animate-spin" />
)}
Enable Pulse Assistant
{setupPresentation().submitLabel}
</button>
</div>
</div>

View file

@ -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 () => {

View file

@ -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 =');

View file

@ -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);

View file

@ -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', () => {

View file

@ -24,12 +24,24 @@ const AI_OAUTH_ERROR_MESSAGES: Record<string, string> = {
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 {

View file

@ -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);
});
});

View file

@ -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();