mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-08-24 08:23:31 +00:00
Route keyless Patrol setup to Provider & Models instead of a dead-end model check
A fresh install with no AI provider looped: the Patrol zero state sent users to the Patrol model check, the check told them to enable Pulse Assistant in a settings page that does not exist by that name, and the Patrol model field silently degraded to a bare text input with no hint that a provider key or Ollama server was the missing step (#1463, #847). - Preflight and readiness copy now names the real surfaces (Provider & Models, Patrol settings) and the real first step: add an API key or an Ollama server. - The Patrol zero-state CTA, header Fix setup link, and readiness banners route config-level causes (assistant_disabled, provider_not_configured) to Provider & Models; model-level causes keep the Check Patrol model action. - The Patrol model field shows a linked zero-provider notice instead of a bare text input when no provider is configured. - The preflight result box no longer renders the same failure message twice.
This commit is contained in:
parent
0e40ec07cb
commit
cc948b022c
11 changed files with 107 additions and 40 deletions
|
|
@ -1,6 +1,8 @@
|
|||
import { A } from '@solidjs/router';
|
||||
import { Component, Show } from 'solid-js';
|
||||
import { AIProviderConfigurationSection } from '@/components/Settings/AIProviderConfigurationSection';
|
||||
import { isModelProviderConfigured } from '@/components/Settings/aiSettingsModel';
|
||||
import { settingsTabPath } from '@/components/Settings/settingsNavigationModel';
|
||||
import type { AISettingsState } from '@/components/Settings/useAISettingsState';
|
||||
import { AIModelPicker } from '@/components/shared/AIModelPicker';
|
||||
import { formField, labelClass, controlClass } from '@/components/shared/Form';
|
||||
|
|
@ -122,7 +124,10 @@ export const PatrolPreflightControl: Component<{ state: AISettingsState }> = (co
|
|||
if (r.cause === 'model_tool_support_unverified') {
|
||||
return 'Run Patrol once to confirm this model works correctly in practice.';
|
||||
}
|
||||
return r.summary || r.message || '';
|
||||
// Summary and message are often the same string on config-level
|
||||
// failures; don't render the headline twice.
|
||||
const fallback = r.summary || r.message || '';
|
||||
return fallback === headline() ? '' : fallback;
|
||||
};
|
||||
|
||||
const formatDuration = (ms: number) => {
|
||||
|
|
@ -230,15 +235,31 @@ export const AIModelOverrideField: Component<{
|
|||
<Show
|
||||
when={selectableModels().length > 0}
|
||||
fallback={
|
||||
<input
|
||||
type="text"
|
||||
value={selectedModel()}
|
||||
onInput={(e) => setSelectedModel(e.currentTarget.value)}
|
||||
placeholder="Use shared default model"
|
||||
aria-label={config().ariaLabel}
|
||||
class={controlClass()}
|
||||
disabled={state.saving()}
|
||||
/>
|
||||
<Show
|
||||
when={state.hasConfiguredProvider()}
|
||||
fallback={
|
||||
<p class="rounded-md border border-border bg-surface-alt px-3 py-2 text-xs text-base-content">
|
||||
No AI provider is configured yet. Add an API key or an Ollama server on{' '}
|
||||
<A
|
||||
href={settingsTabPath('system-ai')}
|
||||
class="font-medium text-blue-600 underline dark:text-blue-400"
|
||||
>
|
||||
Provider & Models
|
||||
</A>
|
||||
, then pick a model here.
|
||||
</p>
|
||||
}
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
value={selectedModel()}
|
||||
onInput={(e) => setSelectedModel(e.currentTarget.value)}
|
||||
placeholder="Use shared default model"
|
||||
aria-label={config().ariaLabel}
|
||||
class={controlClass()}
|
||||
disabled={state.saving()}
|
||||
/>
|
||||
</Show>
|
||||
}
|
||||
>
|
||||
<AIModelPicker
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import { getUpgradeActionDestination } from '@/stores/licenseCommercial';
|
|||
import { presentationPolicyHidesUpgradePrompts } from '@/stores/sessionPresentationPolicy';
|
||||
import { formatRelativeTime } from '@/utils/format';
|
||||
import { getAIProviderDisplayName } from '@/utils/aiProviderPresentation';
|
||||
import { PATROL_PROVIDER_SETTINGS_ACTION } from '@/utils/patrolRuntimeActions';
|
||||
import { getPatrolSetupAction } from '@/utils/patrolRuntimeActions';
|
||||
import { getPatrolAutonomyAvailabilityPresentation } from './patrolAutonomyAvailability';
|
||||
import type { PatrolIntelligenceState } from './usePatrolIntelligenceState';
|
||||
|
||||
|
|
@ -229,11 +229,11 @@ export function PatrolIntelligenceBanners(props: { state: PatrolIntelligenceStat
|
|||
</div>
|
||||
<Show when={shouldShowReadinessAction()}>
|
||||
<a
|
||||
href={PATROL_PROVIDER_SETTINGS_ACTION.href}
|
||||
href={getPatrolSetupAction(state.patrolReadiness()?.cause).href}
|
||||
class="inline-flex items-center justify-center gap-2 px-3 py-1.5 text-xs font-semibold rounded-md border border-amber-200 bg-amber-100 text-amber-900 transition-colors hover:bg-amber-200 dark:border-amber-700 dark:bg-amber-900 dark:text-amber-100 dark:hover:bg-amber-900"
|
||||
>
|
||||
<SettingsIcon class="w-3.5 h-3.5" />
|
||||
{PATROL_PROVIDER_SETTINGS_ACTION.label}
|
||||
{getPatrolSetupAction(state.patrolReadiness()?.cause).label}
|
||||
</a>
|
||||
</Show>
|
||||
</div>
|
||||
|
|
@ -261,11 +261,11 @@ export function PatrolIntelligenceBanners(props: { state: PatrolIntelligenceStat
|
|||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<a
|
||||
href={PATROL_PROVIDER_SETTINGS_ACTION.href}
|
||||
href={getPatrolSetupAction(state.patrolReadiness()?.cause).href}
|
||||
class="inline-flex items-center justify-center gap-2 px-3 py-1.5 text-xs font-semibold text-amber-900 dark:text-amber-100 bg-amber-100 dark:bg-amber-900 border border-amber-200 dark:border-amber-700 rounded-md hover:bg-amber-200 dark:hover:bg-amber-900 transition-colors"
|
||||
>
|
||||
<SettingsIcon class="w-3.5 h-3.5" />
|
||||
{PATROL_PROVIDER_SETTINGS_ACTION.label}
|
||||
{getPatrolSetupAction(state.patrolReadiness()?.cause).label}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import { formatRelativeTime } from '@/utils/format';
|
|||
import { getPatrolPageHeaderMeta } from '@/utils/patrolPagePresentation';
|
||||
import { getPatrolTriggerStatusSummary } from '@/utils/patrolRunPresentation';
|
||||
import { getPatrolRuntimePresentation } from '@/utils/patrolRuntimePresentation';
|
||||
import { getPatrolProviderSettingsAction } from '@/utils/patrolRuntimeActions';
|
||||
import { getPatrolSetupAction } from '@/utils/patrolRuntimeActions';
|
||||
import { getPatrolRecencyPresentation } from '@/utils/patrolSummaryPresentation';
|
||||
import { PATROL_CONTROL_ANCHOR, PATROL_OPERATIONS_LOOP_ANCHOR } from '@/routing/resourceLinks';
|
||||
import type { PatrolConfigurationFailureInput } from './patrolInvestigationContextModel';
|
||||
|
|
@ -74,7 +74,7 @@ export function PatrolIntelligenceHeader(props: { state: PatrolIntelligenceState
|
|||
manualRunBlockedReason: state.triggerPatrolDisabledReason(),
|
||||
}),
|
||||
);
|
||||
const providerSetupAction = getPatrolProviderSettingsAction();
|
||||
const providerSetupAction = () => getPatrolSetupAction(state.patrolReadiness()?.cause);
|
||||
const runControlBusy = createMemo(
|
||||
() =>
|
||||
state.isTriggeringPatrol() || state.manualRunRequested() || state.patrolStream.isStreaming(),
|
||||
|
|
@ -99,9 +99,9 @@ export function PatrolIntelligenceHeader(props: { state: PatrolIntelligenceState
|
|||
when={!runBlockedByProviderSetup()}
|
||||
fallback={
|
||||
<A
|
||||
href={providerSetupAction.href}
|
||||
aria-label={`Check Patrol model: ${state.triggerPatrolDisabledReason() || 'Patrol model needs attention'}`}
|
||||
title={state.triggerPatrolDisabledReason() || 'Open Patrol settings'}
|
||||
href={providerSetupAction().href}
|
||||
aria-label={`${providerSetupAction().label}: ${state.triggerPatrolDisabledReason() || 'Patrol setup needs attention'}`}
|
||||
title={state.triggerPatrolDisabledReason() || providerSetupAction().label}
|
||||
class={className}
|
||||
>
|
||||
<SettingsIcon class="w-4 h-4" />
|
||||
|
|
|
|||
|
|
@ -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
|
|||
<h3 class="text-sm font-semibold">Patrol cannot run yet</h3>
|
||||
</div>
|
||||
<p class="mt-2 text-sm leading-6 text-base-content">{setupReason()}</p>
|
||||
<p class="mt-1 text-xs leading-5 text-muted">
|
||||
Open Patrol settings and run the model check. Provider connectivity can be healthy
|
||||
even when the selected model cannot use Patrol tools.
|
||||
</p>
|
||||
<Show when={setupHint()}>
|
||||
<p class="mt-1 text-xs leading-5 text-muted">{setupHint()}</p>
|
||||
</Show>
|
||||
<ButtonLink
|
||||
href={setupAction.href}
|
||||
href={setupAction().href}
|
||||
variant="primary"
|
||||
size="sm"
|
||||
class="mt-4 gap-1.5"
|
||||
>
|
||||
<SettingsIcon class="h-4 w-4" aria-hidden="true" />
|
||||
{setupAction.label}
|
||||
{setupAction().label}
|
||||
</ButtonLink>
|
||||
</div>
|
||||
<div class="rounded-md border border-amber-200 bg-surface/80 p-3 dark:border-amber-900">
|
||||
|
|
|
|||
|
|
@ -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() ||');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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.';
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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.", "")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue