diff --git a/frontend-modern/src/features/patrol/PatrolIntelligenceWorkspace.tsx b/frontend-modern/src/features/patrol/PatrolIntelligenceWorkspace.tsx
index 107830b0b..7e9421854 100644
--- a/frontend-modern/src/features/patrol/PatrolIntelligenceWorkspace.tsx
+++ b/frontend-modern/src/features/patrol/PatrolIntelligenceWorkspace.tsx
@@ -22,7 +22,7 @@ import {
import { Button, ButtonLink } from '@/components/shared/Button';
import { MetadataBadge } from '@/components/shared/MetadataBadge';
import { StatusIndicatorBadge } from '@/components/shared/StatusIndicatorBadge';
-import { getPatrolProviderSettingsAction } from '@/utils/patrolRuntimeActions';
+import { getPatrolSetupAction, getPatrolSetupHint } from '@/utils/patrolRuntimeActions';
import {
getPatrolProInvestigationHandoff,
getPatrolQueueBadgeLabel,
@@ -67,7 +67,8 @@ export function PatrolIntelligenceWorkspace(props: { state: PatrolIntelligenceSt
const isHistoryOpen = () => state.activeTab() === 'history';
const isSetupOnly = () =>
!isHistoryOpen() && !state.selectedRun() && state.shouldShowPatrolSetupOnly();
- const setupAction = getPatrolProviderSettingsAction();
+ const setupAction = () => getPatrolSetupAction(state.patrolReadiness()?.cause);
+ const setupHint = () => getPatrolSetupHint(state.patrolReadiness()?.cause);
const setupFinding = () => state.findingsTabBadgeFindings().find(isPatrolRuntimeFinding);
const setupReason = () => {
const finding = setupFinding();
@@ -318,18 +319,17 @@ export function PatrolIntelligenceWorkspace(props: { state: PatrolIntelligenceSt
Patrol cannot run yet
{setupReason()}
-
- Open Patrol settings and run the model check. Provider connectivity can be healthy
- even when the selected model cannot use Patrol tools.
-
+
+ {setupHint()}
+
- {setupAction.label}
+ {setupAction().label}
diff --git a/frontend-modern/src/features/patrol/__tests__/PatrolIntelligenceHeader.test.ts b/frontend-modern/src/features/patrol/__tests__/PatrolIntelligenceHeader.test.ts
index dbd2f8710..c81459533 100644
--- a/frontend-modern/src/features/patrol/__tests__/PatrolIntelligenceHeader.test.ts
+++ b/frontend-modern/src/features/patrol/__tests__/PatrolIntelligenceHeader.test.ts
@@ -75,10 +75,10 @@ describe('PatrolIntelligenceHeader', () => {
it('turns provider-blocked manual run controls into setup actions', () => {
expect(headerSource).toContain('runBlockedByProviderSetup');
expect(headerSource).toContain("state.patrolReadiness()?.status === 'not_ready'");
- expect(headerSource).toContain('getPatrolProviderSettingsAction');
- expect(headerSource).toContain('providerSetupAction.href');
+ expect(headerSource).toContain('getPatrolSetupAction');
+ expect(headerSource).toContain('providerSetupAction().href');
expect(headerSource).toContain('Fix setup');
- expect(headerSource).toContain('Check Patrol model:');
+ expect(headerSource).toContain("getPatrolSetupAction(state.patrolReadiness()?.cause)");
expect(headerSource).toContain('runButtonDisabled');
expect(headerSource).not.toContain('!state.canTriggerPatrol() ||');
});
diff --git a/frontend-modern/src/features/patrol/__tests__/PatrolIntelligenceWorkspace.test.ts b/frontend-modern/src/features/patrol/__tests__/PatrolIntelligenceWorkspace.test.ts
index b99e931a8..24b4e7221 100644
--- a/frontend-modern/src/features/patrol/__tests__/PatrolIntelligenceWorkspace.test.ts
+++ b/frontend-modern/src/features/patrol/__tests__/PatrolIntelligenceWorkspace.test.ts
@@ -67,7 +67,8 @@ describe('PatrolIntelligenceWorkspace trust strip', () => {
expect(workspaceSource).not.toContain(removedAllModeCopy);
expect(workspaceSource).toContain('Patrol cannot run yet');
expect(workspaceSource).toContain('Once ready');
- expect(workspaceSource).toContain('getPatrolProviderSettingsAction');
+ expect(workspaceSource).toContain('getPatrolSetupAction');
+ expect(workspaceSource).toContain('getPatrolSetupHint');
expect(workspaceSource).toContain('state.patrolRunHistory.value()?.length');
expect(workspaceSource).not.toContain('showControls={!state.selectedRun() && !isSetupOnly()}');
expect(PATROL_WORKSPACE_SETUP_TITLE).toBe('Patrol needs setup');
diff --git a/frontend-modern/src/utils/__tests__/patrolRuntimeActions.test.ts b/frontend-modern/src/utils/__tests__/patrolRuntimeActions.test.ts
index 05f875d54..d6e0a7005 100644
--- a/frontend-modern/src/utils/__tests__/patrolRuntimeActions.test.ts
+++ b/frontend-modern/src/utils/__tests__/patrolRuntimeActions.test.ts
@@ -1,6 +1,8 @@
import { describe, expect, it } from 'vitest';
import {
getPatrolProviderSettingsAction,
+ getPatrolSetupAction,
+ getPatrolSetupHint,
PATROL_PROVIDER_SETTINGS_ACTION,
} from '@/utils/patrolRuntimeActions';
@@ -15,4 +17,26 @@ describe('patrolRuntimeActions', () => {
expect(action).toEqual(PATROL_PROVIDER_SETTINGS_ACTION);
expect(action).not.toBe(PATROL_PROVIDER_SETTINGS_ACTION);
});
+
+ it('routes config-level causes to Provider & Models instead of the model check', () => {
+ for (const cause of ['assistant_disabled', 'provider_not_configured']) {
+ expect(getPatrolSetupAction(cause)).toEqual({
+ label: 'Open Provider & Models',
+ href: '/settings/pulse-intelligence/provider',
+ });
+ }
+ });
+
+ it('keeps the model check action for model-level and unknown causes', () => {
+ for (const cause of ['model_not_selected', 'model_unsupported_tools', undefined, '']) {
+ expect(getPatrolSetupAction(cause)).toEqual(PATROL_PROVIDER_SETTINGS_ACTION);
+ }
+ });
+
+ it('suppresses the tool-check hint for config-level causes', () => {
+ expect(getPatrolSetupHint('assistant_disabled')).toBe('');
+ expect(getPatrolSetupHint('provider_not_configured')).toBe('');
+ expect(getPatrolSetupHint('model_not_selected')).toContain('run the model check');
+ expect(getPatrolSetupHint(undefined)).toContain('run the model check');
+ });
});
diff --git a/frontend-modern/src/utils/patrolRuntimeActions.ts b/frontend-modern/src/utils/patrolRuntimeActions.ts
index c791e4bbc..93030420a 100644
--- a/frontend-modern/src/utils/patrolRuntimeActions.ts
+++ b/frontend-modern/src/utils/patrolRuntimeActions.ts
@@ -10,6 +10,27 @@ export const PATROL_PROVIDER_SETTINGS_ACTION: PatrolRuntimeActionPresentation =
href: settingsTabPath('system-ai-patrol'),
};
+// Config-level causes mean the install has no working provider at all, so
+// sending the user to the Patrol model check is a dead end. Route them to
+// Provider & Models, where the enable toggle and API key / Ollama fields live.
+const PATROL_CONFIG_LEVEL_CAUSES = new Set(['assistant_disabled', 'provider_not_configured']);
+
export const getPatrolProviderSettingsAction = (): PatrolRuntimeActionPresentation => ({
...PATROL_PROVIDER_SETTINGS_ACTION,
});
+
+export const getPatrolSetupAction = (cause?: string): PatrolRuntimeActionPresentation => {
+ if (cause && PATROL_CONFIG_LEVEL_CAUSES.has(cause)) {
+ return { label: 'Open Provider & Models', href: settingsTabPath('system-ai') };
+ }
+ return { ...PATROL_PROVIDER_SETTINGS_ACTION };
+};
+
+// The tool-check explainer only makes sense once a provider exists; for
+// config-level causes the readiness summary already says what to do.
+export const getPatrolSetupHint = (cause?: string): string => {
+ if (cause && PATROL_CONFIG_LEVEL_CAUSES.has(cause)) {
+ return '';
+ }
+ return 'Open Patrol settings and run the model check. Provider connectivity can be healthy even when the selected model cannot use Patrol tools.';
+};
diff --git a/internal/ai/patrol_preflight.go b/internal/ai/patrol_preflight.go
index 51b67caa2..7f3bc1c87 100644
--- a/internal/ai/patrol_preflight.go
+++ b/internal/ai/patrol_preflight.go
@@ -127,9 +127,9 @@ func (s *Service) RunPatrolToolPreflight(ctx context.Context, providerName, mode
}
if !cfg.Enabled {
result.Cause = PatrolFailureCauseAssistantDisabled
- result.Title = "Pulse Patrol: Assistant disabled"
- result.Summary = "Pulse Assistant is not enabled"
- result.Recommendation = "Enable Pulse Assistant in Assistant & Patrol settings, then re-run preflight."
+ result.Title = "Pulse Patrol: Pulse Intelligence turned off"
+ result.Summary = "Pulse Intelligence is turned off"
+ result.Recommendation = "Turn on Pulse Intelligence on the Provider & Models settings page, then run Check Patrol model again."
result.DurationMs = time.Since(started).Milliseconds()
s.recordPatrolPreflight(result, time.Now())
return result
@@ -143,7 +143,7 @@ func (s *Service) RunPatrolToolPreflight(ctx context.Context, providerName, mode
result.Cause = PatrolFailureCauseModelNotSelected
result.Title = "Pulse Patrol: No model selected"
result.Summary = "Patrol has no model selected"
- result.Recommendation = "Select a Patrol model in Assistant & Patrol settings, then re-run preflight."
+ result.Recommendation = "Select a Patrol model in Patrol settings. If no models are listed, add a provider API key or an Ollama server on the Provider & Models settings page first."
result.DurationMs = time.Since(started).Milliseconds()
s.recordPatrolPreflight(result, time.Now())
return result
diff --git a/internal/ai/patrol_readiness.go b/internal/ai/patrol_readiness.go
index 5ddf9a15e..60a652397 100644
--- a/internal/ai/patrol_readiness.go
+++ b/internal/ai/patrol_readiness.go
@@ -51,10 +51,10 @@ func EvaluatePatrolConfigReadiness(cfg *config.AIConfig) PatrolConfigReadiness {
return patrolConfigReadiness("", "", PatrolReadinessNotReady, PatrolFailureCauseSettingsPersistence, "Assistant & Patrol settings could not be loaded from persistence.")
}
if !cfg.Enabled {
- return patrolConfigReadiness("", "", PatrolReadinessNotReady, PatrolFailureCauseAssistantDisabled, "Pulse Assistant is disabled, so Patrol cannot run model-backed verification.")
+ return patrolConfigReadiness("", "", PatrolReadinessNotReady, PatrolFailureCauseAssistantDisabled, "Pulse Intelligence is turned off, so Patrol cannot run.")
}
if !cfg.IsConfigured() {
- return patrolConfigReadiness("", "", PatrolReadinessNotReady, PatrolFailureCauseProviderNotConfigured, "No AI provider is configured for Patrol.")
+ return patrolConfigReadiness("", "", PatrolReadinessNotReady, PatrolFailureCauseProviderNotConfigured, "No AI provider is configured yet. Add a provider API key or an Ollama server on the Provider & Models settings page.")
}
model := strings.TrimSpace(cfg.GetPatrolModel())
diff --git a/internal/api/ai_handlers.go b/internal/api/ai_handlers.go
index 02e700932..fc38a6486 100644
--- a/internal/api/ai_handlers.go
+++ b/internal/api/ai_handlers.go
@@ -5115,13 +5115,13 @@ func (h *AISettingsHandler) buildPatrolReadiness(ctx context.Context, aiService
addCheck("settings", patrolReadinessReady, ai.PatrolFailureCauseNone, "Settings persistence", "Assistant & Patrol settings are readable.", "")
if !cfg.Enabled {
- addCheck("enabled", patrolReadinessNotReady, ai.PatrolFailureCauseAssistantDisabled, "Assistant enabled", "Pulse Assistant is disabled, so Patrol cannot run model-backed verification.", "open_provider_settings")
+ addCheck("enabled", patrolReadinessNotReady, ai.PatrolFailureCauseAssistantDisabled, "Assistant enabled", "Pulse Intelligence is turned off, so Patrol cannot run.", "open_provider_settings")
} else {
addCheck("enabled", patrolReadinessReady, ai.PatrolFailureCauseNone, "Assistant enabled", "Pulse Assistant is enabled for Patrol verification.", "")
}
if !cfg.IsConfigured() {
- addCheck("provider", patrolReadinessNotReady, ai.PatrolFailureCauseProviderNotConfigured, "Provider configured", "No AI provider is configured for Patrol.", "open_provider_settings")
+ addCheck("provider", patrolReadinessNotReady, ai.PatrolFailureCauseProviderNotConfigured, "Provider configured", "No AI provider is configured yet. Add a provider API key or an Ollama server on the Provider & Models settings page.", "open_provider_settings")
return summarizePatrolReadiness("", "", checks)
}
addCheck("provider", patrolReadinessReady, ai.PatrolFailureCauseNone, "Provider configured", "At least one AI provider is configured.", "")