Split AI settings section owners

This commit is contained in:
rcourtman
2026-03-21 12:14:46 +00:00
parent 5a638644b1
commit 80a5fded28
12 changed files with 957 additions and 983 deletions
@@ -25,61 +25,66 @@ work extends shared components instead of creating new local variants.
3. `frontend-modern/src/components/Settings/SettingsPageShell.tsx`
4. `frontend-modern/src/components/Settings/settingsPanelRegistry.ts`
5. `frontend-modern/src/components/Settings/APIAccessPanel.tsx`
6. `frontend-modern/src/components/Settings/AISettings.tsx`
7. `frontend-modern/src/components/Settings/AIProviderConfigurationSection.tsx`
8. `frontend-modern/src/components/Settings/AISettingsDialogs.tsx`
9. `frontend-modern/src/components/Settings/aiSettingsModel.ts`
10. `frontend-modern/src/components/Settings/useAISettingsState.ts`
11. `frontend-modern/src/components/Settings/AuditLogPanel.tsx`
12. `frontend-modern/src/components/Settings/useAuditLogPanelState.ts`
13. `frontend-modern/src/components/Settings/AuditWebhookPanel.tsx`
14. `frontend-modern/src/components/Settings/useAuditWebhookPanelState.ts`
15. `frontend-modern/src/components/Settings/CopyCommandBlock.tsx`
16. `frontend-modern/src/components/Settings/diagnosticsModel.ts`
17. `frontend-modern/src/components/Settings/DiagnosticsPanel.tsx`
18. `frontend-modern/src/components/Settings/DiagnosticsResultsPanel.tsx`
19. `frontend-modern/src/components/Settings/GeneralSettingsPanel.tsx`
20. `frontend-modern/src/components/Settings/NetworkSettingsPanel.tsx`
21. `frontend-modern/src/components/Settings/RecoverySettingsPanel.tsx`
22. `frontend-modern/src/components/Settings/SecurityAuthPanel.tsx`
23. `frontend-modern/src/components/Settings/SecurityOverviewPanel.tsx`
24. `frontend-modern/src/components/Settings/settingsHeaderMeta.ts`
25. `frontend-modern/src/components/Settings/SettingsPageShell.tsx`
26. `frontend-modern/src/components/Settings/settingsPanelRegistry.ts`
27. `frontend-modern/src/components/Settings/SSOProvidersPanel.tsx`
28. `frontend-modern/src/components/Settings/useDiagnosticsPanelState.ts`
29. `frontend-modern/src/components/Settings/useSSOProvidersState.ts`
30. `frontend-modern/src/components/Settings/ssoProvidersModel.ts`
31. `frontend-modern/src/components/Settings/UpdateInstallGuide.tsx`
32. `frontend-modern/src/components/Settings/updatesSettingsModel.ts`
33. `frontend-modern/src/components/Settings/UpdatesSettingsPanel.tsx`
34. `frontend-modern/src/components/Settings/ReportingPanel.tsx`
35. `frontend-modern/src/components/Settings/reportingPanelModel.ts`
36. `frontend-modern/src/components/Settings/useReportingPanelState.ts`
37. `frontend-modern/src/utils/reportingPresentation.ts`
38. `frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts`
39. `tests/integration/tests/15-settings-shell-consistency.spec.ts`
40. `frontend-modern/src/components/shared/PageControls.guardrails.test.ts`
41. `frontend-modern/src/components/shared/TypeColumn.guardrails.test.ts`
42. `frontend-modern/src/features/`
43. `frontend-modern/src/components/SetupWizard/SetupWizard.tsx`
44. `frontend-modern/src/components/SetupWizard/SetupCompletionPreview.tsx`
45. `frontend-modern/src/components/SetupWizard/__tests__/SetupWizard.test.tsx`
46. `frontend-modern/src/components/SetupWizard/__tests__/SetupCompletionPreview.test.tsx`
47. `frontend-modern/src/components/shared/MonitoredSystemLimitWarningBanner.tsx`
48. `frontend-modern/src/components/Settings/SystemLogsPanel.tsx`
49. `frontend-modern/src/components/Settings/useSystemLogsPanelState.ts`
50. `frontend-modern/src/components/Settings/__tests__/SystemLogsPanel.test.tsx`
51. `frontend-modern/src/features/operations/OperationsPageSurface.tsx`
52. `frontend-modern/src/features/operations/operationsPageModel.ts`
53. `frontend-modern/src/pages/Operations.tsx`
54. `frontend-modern/src/pages/__tests__/Operations.helpers.test.ts`
55. `frontend-modern/src/components/Settings/NetworkDiscoverySection.tsx`
56. `frontend-modern/src/components/Settings/NetworkBoundarySettingsSection.tsx`
57. `frontend-modern/src/components/Settings/networkSettingsModel.ts`
58. `frontend-modern/src/components/Settings/useDiscoverySettingsState.ts`
59. `frontend-modern/src/components/Settings/useSettingsPanelRegistry.tsx`
60. `frontend-modern/src/components/Settings/useSettingsSystemPanels.tsx`
6. `frontend-modern/src/components/Settings/AIChatMaintenanceSection.tsx`
7. `frontend-modern/src/components/Settings/AIModelSelectionSection.tsx`
8. `frontend-modern/src/components/Settings/AIProviderConfigurationSection.tsx`
9. `frontend-modern/src/components/Settings/AIRuntimeControlsSection.tsx`
10. `frontend-modern/src/components/Settings/AISettings.tsx`
11. `frontend-modern/src/components/Settings/AISettingsDialogs.tsx`
12. `frontend-modern/src/components/Settings/AISettingsStatusAndActions.tsx`
13. `frontend-modern/src/components/Settings/aiSettingsModel.ts`
14. `frontend-modern/src/components/Settings/APIAccessPanel.tsx`
15. `frontend-modern/src/components/Settings/AuditLogPanel.tsx`
16. `frontend-modern/src/components/Settings/useAuditLogPanelState.ts`
17. `frontend-modern/src/components/Settings/AuditWebhookPanel.tsx`
18. `frontend-modern/src/components/Settings/useAuditWebhookPanelState.ts`
19. `frontend-modern/src/components/Settings/CopyCommandBlock.tsx`
20. `frontend-modern/src/components/Settings/diagnosticsModel.ts`
21. `frontend-modern/src/components/Settings/DiagnosticsPanel.tsx`
22. `frontend-modern/src/components/Settings/DiagnosticsResultsPanel.tsx`
23. `frontend-modern/src/components/Settings/GeneralSettingsPanel.tsx`
24. `frontend-modern/src/components/Settings/NetworkSettingsPanel.tsx`
25. `frontend-modern/src/components/Settings/RecoverySettingsPanel.tsx`
26. `frontend-modern/src/components/Settings/SecurityAuthPanel.tsx`
27. `frontend-modern/src/components/Settings/SecurityOverviewPanel.tsx`
28. `frontend-modern/src/components/Settings/settingsHeaderMeta.ts`
29. `frontend-modern/src/components/Settings/SettingsPageShell.tsx`
30. `frontend-modern/src/components/Settings/settingsPanelRegistry.ts`
31. `frontend-modern/src/components/Settings/SSOProvidersPanel.tsx`
32. `frontend-modern/src/components/Settings/useAISettingsState.ts`
33. `frontend-modern/src/components/Settings/useDiagnosticsPanelState.ts`
34. `frontend-modern/src/components/Settings/useSSOProvidersState.ts`
35. `frontend-modern/src/components/Settings/ssoProvidersModel.ts`
36. `frontend-modern/src/components/Settings/UpdateInstallGuide.tsx`
37. `frontend-modern/src/components/Settings/updatesSettingsModel.ts`
38. `frontend-modern/src/components/Settings/UpdatesSettingsPanel.tsx`
39. `frontend-modern/src/components/Settings/ReportingPanel.tsx`
40. `frontend-modern/src/components/Settings/reportingPanelModel.ts`
41. `frontend-modern/src/components/Settings/useReportingPanelState.ts`
42. `frontend-modern/src/utils/reportingPresentation.ts`
43. `frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts`
44. `tests/integration/tests/15-settings-shell-consistency.spec.ts`
45. `frontend-modern/src/components/shared/PageControls.guardrails.test.ts`
46. `frontend-modern/src/components/shared/TypeColumn.guardrails.test.ts`
47. `frontend-modern/src/features/`
48. `frontend-modern/src/components/SetupWizard/SetupWizard.tsx`
49. `frontend-modern/src/components/SetupWizard/SetupCompletionPreview.tsx`
50. `frontend-modern/src/components/SetupWizard/__tests__/SetupWizard.test.tsx`
51. `frontend-modern/src/components/SetupWizard/__tests__/SetupCompletionPreview.test.tsx`
52. `frontend-modern/src/components/shared/MonitoredSystemLimitWarningBanner.tsx`
53. `frontend-modern/src/components/Settings/SystemLogsPanel.tsx`
54. `frontend-modern/src/components/Settings/useSystemLogsPanelState.ts`
55. `frontend-modern/src/components/Settings/__tests__/SystemLogsPanel.test.tsx`
56. `frontend-modern/src/features/operations/OperationsPageSurface.tsx`
57. `frontend-modern/src/features/operations/operationsPageModel.ts`
58. `frontend-modern/src/pages/Operations.tsx`
59. `frontend-modern/src/pages/__tests__/Operations.helpers.test.ts`
60. `frontend-modern/src/components/Settings/NetworkDiscoverySection.tsx`
61. `frontend-modern/src/components/Settings/NetworkBoundarySettingsSection.tsx`
62. `frontend-modern/src/components/Settings/networkSettingsModel.ts`
63. `frontend-modern/src/components/Settings/useDiscoverySettingsState.ts`
64. `frontend-modern/src/components/Settings/useSettingsPanelRegistry.tsx`
65. `frontend-modern/src/components/Settings/useSettingsSystemPanels.tsx`
## Shared Boundaries
@@ -369,14 +374,22 @@ well: `frontend-modern/src/components/Settings/settingsHeaderMeta.ts` and
representative top-level panels such as
`frontend-modern/src/components/Settings/APIAccessPanel.tsx`,
`frontend-modern/src/components/Settings/AISettings.tsx`,
`frontend-modern/src/components/Settings/AIModelSelectionSection.tsx`,
`frontend-modern/src/components/Settings/AIRuntimeControlsSection.tsx`,
`frontend-modern/src/components/Settings/AIChatMaintenanceSection.tsx`,
`frontend-modern/src/components/Settings/AISettingsStatusAndActions.tsx`,
`frontend-modern/src/components/Settings/AIProviderConfigurationSection.tsx`,
`frontend-modern/src/components/Settings/AISettingsDialogs.tsx`, and
`frontend-modern/src/components/Settings/aiSettingsModel.ts` now also define
the canonical AI settings runtime boundary. `AISettings.tsx` is the shell,
`frontend-modern/src/components/Settings/useAISettingsState.ts` owns the
runtime lifecycle and persistence flow, and provider configuration, modal
flows, and provider metadata must extend those extracted owners instead of
being re-inlined into the shell.
runtime lifecycle and persistence flow, model/provider setup now routes
through `AIModelSelectionSection.tsx`, discovery, budget, timeout, and
permission controls route through `AIRuntimeControlsSection.tsx`, chat
maintenance routes through `AIChatMaintenanceSection.tsx`, and readiness plus
save/test actions route through `AISettingsStatusAndActions.tsx`. Future AI
settings work must extend those section owners instead of re-inlining large
runtime subsections into the shell.
`frontend-modern/src/components/Settings/AuditLogPanel.tsx`,
`frontend-modern/src/components/Settings/AuditWebhookPanel.tsx`,
`frontend-modern/src/components/Settings/GeneralSettingsPanel.tsx`,
@@ -1735,10 +1735,14 @@
"frontend-modern/src/components/shared/"
],
"owned_files": [
"frontend-modern/src/components/Settings/AIChatMaintenanceSection.tsx",
"frontend-modern/src/components/Settings/AIModelSelectionSection.tsx",
"frontend-modern/src/components/Settings/AIProviderConfigurationSection.tsx",
"frontend-modern/src/components/Settings/AIRuntimeControlsSection.tsx",
"frontend-modern/src/components/Settings/AISettings.tsx",
"frontend-modern/src/components/Settings/AISettingsDialogs.tsx",
"frontend-modern/src/components/Settings/aiSettingsModel.ts",
"frontend-modern/src/components/Settings/AISettingsStatusAndActions.tsx",
"frontend-modern/src/components/Settings/APIAccessPanel.tsx",
"frontend-modern/src/components/Settings/AuditLogPanel.tsx",
"frontend-modern/src/components/Settings/AuditWebhookPanel.tsx",
@@ -1817,10 +1821,14 @@
"label": "settings shell framing proof",
"match_prefixes": [],
"match_files": [
"frontend-modern/src/components/Settings/AIChatMaintenanceSection.tsx",
"frontend-modern/src/components/Settings/AIModelSelectionSection.tsx",
"frontend-modern/src/components/Settings/AIProviderConfigurationSection.tsx",
"frontend-modern/src/components/Settings/AIRuntimeControlsSection.tsx",
"frontend-modern/src/components/Settings/AISettings.tsx",
"frontend-modern/src/components/Settings/AISettingsDialogs.tsx",
"frontend-modern/src/components/Settings/aiSettingsModel.ts",
"frontend-modern/src/components/Settings/AISettingsStatusAndActions.tsx",
"frontend-modern/src/components/Settings/APIAccessPanel.tsx",
"frontend-modern/src/components/Settings/AuditLogPanel.tsx",
"frontend-modern/src/components/Settings/AuditWebhookPanel.tsx",
@@ -0,0 +1,128 @@
import { Component, For, Show } from 'solid-js';
import type { AISettingsState } from '@/components/Settings/useAISettingsState';
import {
getAIChatSessionsEmptyState,
getAIChatSessionsLoadingState,
} from '@/utils/aiSettingsPresentation';
interface AIChatMaintenanceSectionProps {
state: AISettingsState;
}
export const AIChatMaintenanceSection: Component<AIChatMaintenanceSectionProps> = (props) => {
const { state } = props;
return (
<div class="border border-border rounded-md overflow-hidden">
<button
type="button"
class="w-full min-h-10 sm:min-h-9 px-3 py-2.5 flex items-center justify-between bg-surface-alt hover:bg-surface-hover transition-colors text-left"
onClick={() => {
const next = !state.showChatMaintenance();
state.setShowChatMaintenance(next);
if (next) {
state.loadChatSessions();
}
}}
>
<div class="flex items-center gap-2">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
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>
</div>
<svg
class={`w-4 h-4 transition-transform ${state.showChatMaintenance() ? 'rotate-180' : ''}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
</button>
<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.
</p>
<div class="flex items-center justify-between">
<label class="text-xs font-medium text-muted">Session</label>
<button
type="button"
onClick={state.loadChatSessions}
disabled={state.chatSessionsLoading()}
class="inline-flex min-h-10 sm:min-h-9 items-center rounded-md px-2 py-1.5 text-sm text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 disabled:opacity-50"
>
{state.chatSessionsLoading() ? 'Refreshing...' : 'Refresh'}
</button>
</div>
<Show when={state.chatSessionsLoading()}>
<div class="text-xs text-muted">{getAIChatSessionsLoadingState().text}</div>
</Show>
<Show when={!state.chatSessionsLoading()}>
<Show when={state.chatSessionsError()}>
<div class="text-xs text-red-500">{state.chatSessionsError()}</div>
</Show>
<Show when={!state.chatSessionsError()}>
<Show
when={state.chatSessions().length > 0}
fallback={<div class="text-xs text-muted">{getAIChatSessionsEmptyState().text}</div>}
>
<select
value={state.selectedSessionId()}
onChange={(e) => state.setSelectedSessionId(e.currentTarget.value)}
class="w-full min-h-10 sm:min-h-9 px-2 py-2 text-sm border border-border rounded"
disabled={state.saving()}
>
<For each={state.chatSessions()}>
{(session) => <option value={session.id}>{state.formatSessionLabel(session)}</option>}
</For>
</select>
<Show when={state.selectedChatSession()}>
<p class="text-[10px] text-muted mt-1">
Last updated {new Date(state.selectedChatSession()!.updated_at).toLocaleString()}
</p>
</Show>
</Show>
</Show>
</Show>
<div class="flex flex-wrap gap-2 pt-1">
<button
type="button"
onClick={state.handleSessionSummarize}
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'}
</button>
<button
type="button"
onClick={state.handleSessionDiff}
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'}
</button>
<button
type="button"
onClick={state.handleSessionRevert}
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-red-200 dark:border-red-700 bg-red-50 dark:bg-red-900 text-red-700 dark:text-red-300 hover:bg-red-100 dark:hover:bg-red-900 disabled:opacity-50"
>
{state.sessionActionLoading() === 'revert' ? 'Reverting...' : 'Revert changes'}
</button>
</div>
<p class="text-[10px] text-muted">These actions apply to the selected chat session only.</p>
</div>
</Show>
</div>
);
};
@@ -0,0 +1,305 @@
import { Component, For, Show } from 'solid-js';
import { AIProviderConfigurationSection } from '@/components/Settings/AIProviderConfigurationSection';
import {
groupModelsByProvider,
isAIProviderConfigured,
isModelProviderConfigured,
} from '@/components/Settings/aiSettingsModel';
import type { AISettingsState } from '@/components/Settings/useAISettingsState';
import { formField, labelClass, controlClass } from '@/components/shared/Form';
import { getAIProviderDisplayName, getProviderFromModelId } from '@/utils/aiProviderPresentation';
interface AIModelSelectionSectionProps {
state: AISettingsState;
}
export const AIModelSelectionSection: Component<AIModelSelectionSectionProps> = (props) => {
const { state } = props;
const groupedModels = () => Array.from(groupModelsByProvider(state.availableModels()).entries());
return (
<>
<div class={formField}>
<div class="flex items-center justify-between mb-1">
<label class={labelClass()}>
Default Model
{state.modelsLoading() && <span class="ml-2 text-xs text-slate-500">(loading...)</span>}
</label>
<button
type="button"
onClick={state.loadModels}
disabled={state.modelsLoading()}
class="inline-flex min-h-10 sm:min-h-9 items-center gap-1 rounded-md px-2 py-1.5 text-sm text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 disabled:opacity-50"
title="Refresh model list from all configured providers"
>
<svg
class={`w-3 h-3 ${state.modelsLoading() ? 'animate-spin' : ''}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
/>
</svg>
Refresh
</button>
</div>
<Show
when={state.availableModels().length > 0}
fallback={
<input
type="text"
value={state.form.model}
onInput={(e) => state.setForm('model', e.currentTarget.value)}
placeholder="Configure a provider below to see available models"
class={controlClass()}
disabled={state.saving()}
/>
}
>
<select
value={state.form.model}
onChange={(e) => state.setForm('model', e.currentTarget.value)}
class={controlClass()}
disabled={state.saving()}
>
<Show
when={
!state.form.model ||
!state.availableModels().some((model) => model.id === state.form.model)
}
>
<option value={state.form.model}>{state.form.model || 'Select a model...'}</option>
</Show>
<For
each={groupedModels().filter(([provider]) =>
isAIProviderConfigured(provider, state.settings()),
)}
>
{([provider, models]) => (
<optgroup label={getAIProviderDisplayName(provider) || provider}>
<For each={models}>
{(model) => (
<option value={model.id} selected={model.id === state.form.model}>
{model.name || model.id.split(':').pop()}
</option>
)}
</For>
</optgroup>
)}
</For>
<For
each={groupedModels().filter(([provider]) =>
!isAIProviderConfigured(provider, state.settings()),
)}
>
{([provider, models]) => (
<optgroup
label={`${getAIProviderDisplayName(provider) || provider} (not configured)`}
>
<For each={models}>
{(model) => (
<option
value={model.id}
selected={model.id === state.form.model}
class="text-slate-400"
>
{model.name || model.id.split(':').pop()}
</option>
)}
</For>
</optgroup>
)}
</For>
</select>
</Show>
<Show when={state.modelsError()}>
<p class="text-xs text-amber-600 dark:text-amber-400 mt-1 flex items-center gap-1">
<svg
class="w-3.5 h-3.5 flex-shrink-0"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
/>
</svg>
Failed to load models: {state.modelsError()}. Enter a model ID manually (format:
provider:model-name) or click Refresh to retry.
</p>
</Show>
<Show when={state.form.model && !isModelProviderConfigured(state.form.model, state.settings())}>
<p class="text-xs text-amber-600 dark:text-amber-400 mt-1 flex items-center gap-1">
<svg
class="w-3.5 h-3.5 flex-shrink-0"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
/>
</svg>
This model requires{' '}
{getAIProviderDisplayName(getProviderFromModelId(state.form.model)) ||
getProviderFromModelId(state.form.model)}{' '}
to be configured. Add an API key below or select a different model.
</p>
</Show>
</div>
<div class="border border-border rounded-md overflow-hidden">
<button
type="button"
class="w-full min-h-10 sm:min-h-9 px-3 py-2.5 flex items-center justify-between bg-surface-alt hover:bg-surface-hover transition-colors text-left"
onClick={() => state.setShowAdvancedModels(!state.showAdvancedModels())}
>
<div class="flex items-center gap-2">
<svg class="w-4 h-4 text-slate-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
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>
<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
</span>
</Show>
</div>
<svg
class={`w-4 h-4 text-slate-500 transition-transform ${state.showAdvancedModels() ? 'rotate-180' : ''}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
</button>
<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.
</p>
<div>
<label class="block text-xs font-medium text-muted mb-0.5">Chat Model (Interactive)</label>
<p class="text-[11px] text-muted mb-1">
Used for chat and fix execution a more capable model is recommended.
</p>
<Show
when={state.availableModels().length > 0}
fallback={
<input
type="text"
value={state.form.chatModel}
onInput={(e) => state.setForm('chatModel', e.currentTarget.value)}
placeholder="Use default model"
class={controlClass()}
disabled={state.saving()}
/>
}
>
<select
value={state.form.chatModel}
onChange={(e) => state.setForm('chatModel', e.currentTarget.value)}
class={controlClass()}
disabled={state.saving()}
>
<option value="">
Use default ({state.form.model?.split(':').pop() || 'not set'})
</option>
<For each={groupedModels()}>
{([provider, models]) => (
<optgroup label={getAIProviderDisplayName(provider) || provider}>
<For each={models}>
{(model) => (
<option value={model.id}>{model.name || model.id.split(':').pop()}</option>
)}
</For>
</optgroup>
)}
</For>
</select>
</Show>
</div>
<div>
<label class="block text-xs font-medium text-muted mb-0.5">Patrol Model (Background)</label>
<p class="text-[11px] text-muted mb-1">
Runs frequently for detection a smaller, cheaper model keeps costs low.
</p>
<Show
when={state.availableModels().length > 0}
fallback={
<input
type="text"
value={state.form.patrolModel}
onInput={(e) => state.setForm('patrolModel', e.currentTarget.value)}
placeholder="Use default model"
class={controlClass()}
disabled={state.saving()}
/>
}
>
<select
value={state.form.patrolModel}
onChange={(e) => state.setForm('patrolModel', e.currentTarget.value)}
class={controlClass()}
disabled={state.saving()}
>
<option value="">
Use default ({state.form.model?.split(':').pop() || 'not set'})
</option>
<For each={groupedModels()}>
{([provider, models]) => (
<optgroup label={getAIProviderDisplayName(provider) || provider}>
<For each={models}>
{(model) => (
<option value={model.id}>{model.name || model.id.split(':').pop()}</option>
)}
</For>
</optgroup>
)}
</For>
</select>
</Show>
</div>
</div>
</Show>
</div>
<div class={formField}>
<AIProviderConfigurationSection
settings={state.settings}
form={state.form}
setForm={state.setForm}
expandedProviders={state.expandedProviders}
setExpandedProviders={state.setExpandedProviders}
providerHealth={state.providerHealth}
preflightRunning={state.preflightRunning}
preflightLastCheckedAt={state.preflightLastCheckedAt}
providerIssueCount={state.providerIssueCount}
testingProvider={state.testingProvider}
providerTestResult={state.providerTestResult}
saving={state.saving}
runProviderPreflight={() => state.runProviderPreflight()}
handleTestProvider={state.handleTestProvider}
handleClearProvider={state.handleClearProvider}
/>
</div>
</>
);
};
@@ -0,0 +1,281 @@
import { Component, Show } from 'solid-js';
import type { AIControlLevel } from '@/utils/aiControlLevelPresentation';
import type { AISettingsState } from '@/components/Settings/useAISettingsState';
import { HelpIcon } from '@/components/shared/HelpIcon';
import { Toggle } from '@/components/shared/Toggle';
import {
getAIControlLevelBadgeClass,
getAIControlLevelDescription,
getAIControlLevelPanelClass,
} from '@/utils/aiControlLevelPresentation';
import { trackUpgradeClicked } from '@/utils/upgradeMetrics';
interface AIRuntimeControlsSectionProps {
state: AISettingsState;
}
export const AIRuntimeControlsSection: Component<AIRuntimeControlsSectionProps> = (props) => {
const { state } = props;
return (
<>
<div class="rounded-md border border-blue-200 dark:border-blue-800 overflow-hidden">
<button
type="button"
class="w-full min-h-10 sm:min-h-9 px-3 py-2.5 flex items-center justify-between bg-blue-50 dark:bg-blue-900 hover:bg-blue-100 dark:hover:bg-blue-900 transition-colors text-left"
onClick={() => state.setShowDiscoverySettings(!state.showDiscoverySettings())}
>
<div class="flex items-center gap-2">
<svg class="w-4 h-4 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
/>
</svg>
<span class="text-sm font-medium text-base-content">Discovery Settings</span>
<Show when={state.form.discoveryEnabled}>
<span class="px-1.5 py-0.5 text-[10px] font-medium bg-blue-100 dark:bg-blue-800 text-blue-700 dark:text-blue-300 rounded">
{state.form.discoveryIntervalHours > 0
? `${state.form.discoveryIntervalHours}h`
: 'Manual'}
</span>
</Show>
<Show when={!state.form.discoveryEnabled}>
<span class="px-1.5 py-0.5 text-[10px] font-medium bg-surface-hover text-muted rounded">
Off
</span>
</Show>
</div>
<svg
class={`w-4 h-4 transition-transform ${state.showDiscoverySettings() ? 'rotate-180' : ''}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
</button>
<Show when={state.showDiscoverySettings()}>
<div class="px-3 py-3 bg-surface border-t border-border space-y-3">
<div class="flex items-center justify-between gap-2">
<label class="text-xs font-medium text-muted flex items-center gap-1.5">
Enable Discovery
<HelpIcon
inline={{
title: 'What is Discovery?',
description:
'Discovery scans your VMs, containers, and container runtimes to identify what services are running (databases, web servers, etc.), their versions, and how to access them. This information helps Pulse AI give you accurate troubleshooting commands and understand your infrastructure.',
}}
size="xs"
/>
</label>
<Toggle
checked={state.form.discoveryEnabled}
onChange={(event) => state.setForm('discoveryEnabled', event.currentTarget.checked)}
disabled={state.saving()}
/>
</div>
<Show when={state.form.discoveryEnabled}>
<div class="flex flex-col gap-1">
<div class="flex items-center gap-3">
<label class="text-xs font-medium text-muted w-32 flex-shrink-0">Scan Interval</label>
<select
class="flex-1 px-2 py-1 text-sm border border-border rounded bg-surface"
value={state.form.discoveryIntervalHours}
onChange={(e) =>
state.setForm('discoveryIntervalHours', parseInt(e.currentTarget.value, 10))
}
disabled={state.saving()}
>
<option value={0}>Manual only</option>
<option value={6}>Every 6 hours</option>
<option value={12}>Every 12 hours</option>
<option value={24}>Every 24 hours</option>
<option value={48}>Every 2 days</option>
<option value={168}>Every 7 days</option>
</select>
</div>
<p class="text-[10px] text-muted ml-32 pl-3">
{state.form.discoveryIntervalHours === 0
? 'Discovery runs only when you click "Update Discovery" on a resource'
: 'Discovery will automatically re-scan resources at this interval'}
</p>
</div>
</Show>
<p class="text-[10px] text-muted">
Discovery gives Pulse AI workload context, so responses can reference concrete
services and commands instead of generic advice.
</p>
</div>
</Show>
</div>
<div class="flex items-center gap-3 p-3 rounded-md border border-border bg-surface-alt">
<svg class="w-4 h-4 text-muted flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<label class="text-xs font-medium text-base-content">30-day Budget</label>
<div class="relative flex-shrink-0">
<span class="absolute left-2 top-1/2 -translate-y-1/2 text-muted text-xs">$</span>
<input
type="number"
class="w-24 min-h-10 sm:min-h-9 pl-5 pr-2 py-2 text-sm border border-border rounded bg-surface"
value={state.form.costBudgetUSD30d}
onInput={(e) => state.setForm('costBudgetUSD30d', e.currentTarget.value)}
min={0}
step={1}
placeholder="0"
disabled={state.saving()}
/>
</div>
<Show when={parseFloat(state.form.costBudgetUSD30d) > 0}>
<span class="text-xs"> ${(parseFloat(state.form.costBudgetUSD30d) / 30).toFixed(2)}/day</span>
</Show>
<Show when={!state.form.costBudgetUSD30d || parseFloat(state.form.costBudgetUSD30d) === 0}>
<span class="text-[10px] text-muted">Set a budget to receive usage alerts</span>
</Show>
</div>
<div class="flex items-center gap-3 p-3 rounded-md border border-border bg-surface-alt">
<svg class="w-4 h-4 text-muted flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<label class="text-xs font-medium text-base-content">Request Timeout</label>
<input
type="number"
class="w-20 min-h-10 sm:min-h-9 px-2 py-2 text-sm border border-border rounded bg-surface"
value={state.form.requestTimeoutSeconds}
onInput={(e) => {
const value = parseInt(e.currentTarget.value, 10);
if (!isNaN(value) && value > 0) {
state.setForm('requestTimeoutSeconds', value);
}
}}
min={30}
max={3600}
step={30}
disabled={state.saving()}
/>
<span class="text-xs">seconds</span>
<Show when={state.form.requestTimeoutSeconds !== 300}>
<span class="text-[10px] text-blue-600 dark:text-blue-400">Custom</span>
</Show>
<Show when={state.form.requestTimeoutSeconds === 300}>
<span class="text-[10px] text-muted">default</span>
</Show>
</div>
<p class="text-[10px] text-muted -mt-4 ml-1">
Increase for slower Ollama hardware (default: 300s / 5 min)
</p>
<div class={`space-y-3 p-4 rounded-md border ${getAIControlLevelPanelClass(state.form.controlLevel)}`}>
<div class="flex items-center gap-2">
<svg class="w-4 h-4 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
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>
<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}
</span>
</Show>
</div>
<div class="flex items-center gap-3">
<label class="text-xs font-medium text-muted w-28 flex-shrink-0">Permission</label>
<select
value={state.form.controlLevel}
onChange={(e) => state.setForm('controlLevel', e.currentTarget.value as AIControlLevel)}
class="flex-1 min-h-10 sm:min-h-9 px-2 py-2 text-sm border border-border rounded bg-surface"
disabled={state.saving()}
>
<option value="read_only">Read Only - Pulse Assistant can only observe</option>
<option value="controlled">Controlled - Pulse Assistant executes with your approval</option>
<option value="autonomous">Autonomous - Pulse Assistant executes without approval (Pro)</option>
</select>
</div>
<p class="text-[10px] text-muted ml-[7.5rem]">
{getAIControlLevelDescription(state.form.controlLevel)}
</p>
<Show when={state.form.controlLevel === 'autonomous'}>
<div class="p-2 bg-amber-100 dark:bg-amber-900 rounded border border-amber-200 dark:border-amber-800 text-[10px] text-amber-800 dark:text-amber-200">
<strong>Legal Disclaimer:</strong> Model-driven systems can hallucinate. You are
responsible for any damage caused by autonomous actions. See{' '}
<a
href="https://github.com/rcourtman/Pulse/blob/main/TERMS.md"
target="_blank"
rel="noopener noreferrer"
class="inline-flex min-h-10 sm:min-h-9 items-center rounded px-1 underline"
>
Terms of Service
</a>
.
</div>
</Show>
<Show when={state.form.controlLevel === 'autonomous' && state.autoFixLocked()}>
<p class="text-xs text-muted">
<a
class="text-blue-600 dark:text-blue-400 font-medium hover:underline"
href={state.upgradeAutofixUrl()}
target="_blank"
rel="noopener noreferrer"
onClick={() => trackUpgradeClicked('settings_ai_patrol_autofix', 'ai_autofix')}
>
Upgrade to Pro
</a>{' '}
to enable autonomous mode.
<Show when={state.canStartTrial()}>
{' '}
<button
type="button"
onClick={state.handleStartTrial}
disabled={state.startingTrial()}
class="text-indigo-500 hover:underline disabled:opacity-50"
>
Start free trial
</button>
</Show>
</p>
</Show>
<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>
<div class="flex-1">
<input
type="text"
value={state.form.protectedGuests}
onInput={(e) => state.setForm('protectedGuests', e.currentTarget.value)}
placeholder="e.g., 100, 101, prod-db"
class="w-full min-h-10 sm:min-h-9 px-2 py-2 text-sm border border-border rounded"
disabled={state.saving()}
/>
<p class="text-[10px] text-muted mt-1">
Comma-separated VMIDs or names that Pulse Assistant cannot control
</p>
</div>
</div>
</Show>
</div>
</>
);
};
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,62 @@
import { Component, Show } from 'solid-js';
import type { AISettingsState } from '@/components/Settings/useAISettingsState';
interface AISettingsStatusAndActionsProps {
state: AISettingsState;
}
export const AISettingsStatusAndActions: Component<AISettingsStatusAndActionsProps> = (props) => {
const { state } = props;
return (
<>
<Show when={state.settings()}>
<div class="p-4 sm:p-6">
<div
class={`flex items-center gap-2 p-3 rounded-md ${state.settingsReadiness().containerClassName}`}
>
<div class={`w-2 h-2 rounded-full ${state.settingsReadiness().dotClassName}`} />
<div class="flex-1 min-w-0">
<span class="text-xs font-medium">{state.settingsReadiness().summary}</span>
<Show when={state.settings()?.configured && state.settings()?.model}>
<span class="block sm:inline text-xs opacity-75 sm:ml-2">
Default: {state.settings()?.model?.split(':').pop() || state.settings()?.model}
</span>
</Show>
</div>
</div>
</div>
</Show>
<div class="sticky bottom-0 bg-surface px-4 sm:px-6 py-4 flex flex-col sm:flex-row sm:flex-wrap sm:items-center sm:justify-between gap-3">
<Show when={state.settings()?.configured}>
<button
type="button"
class="w-full sm:w-auto min-h-10 sm:min-h-9 px-4 py-2.5 text-sm border border-blue-300 dark:border-blue-700 text-blue-700 dark:text-blue-300 rounded-md hover:bg-blue-50 dark:hover:bg-blue-900 disabled:opacity-50 disabled:cursor-not-allowed"
onClick={state.handleTest}
disabled={state.testing() || state.saving() || state.loading()}
>
{state.testing() ? 'Testing...' : 'Test Connection'}
</button>
</Show>
<div class="grid grid-cols-1 sm:flex gap-3 w-full sm:w-auto sm:ml-auto">
<button
type="button"
class="w-full sm:w-auto min-h-10 sm:min-h-9 px-4 py-2.5 border border-border text-base-content rounded-md hover:bg-surface-hover"
onClick={() => state.resetForm(state.settings())}
disabled={state.saving() || state.loading()}
>
Reset
</button>
<button
type="submit"
class="w-full sm:w-auto min-h-10 sm:min-h-9 px-4 py-2.5 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
disabled={state.saving() || state.loading()}
>
{state.saving() ? 'Saving...' : 'Save changes'}
</button>
</div>
</div>
</>
);
};
@@ -38,9 +38,13 @@ import alertThresholdsSectionPresentationSource from '@/utils/alertThresholdsSec
import loginSource from '@/components/Login.tsx?raw';
import settingsSource from '../Settings.tsx?raw';
import aiSettingsShellSource from '../AISettings.tsx?raw';
import aiChatMaintenanceSectionSource from '../AIChatMaintenanceSection.tsx?raw';
import aiProviderConfigurationSectionSource from '../AIProviderConfigurationSection.tsx?raw';
import aiSettingsDialogsSource from '../AISettingsDialogs.tsx?raw';
import aiModelSelectionSectionSource from '../AIModelSelectionSection.tsx?raw';
import aiSettingsModelSource from '../aiSettingsModel.ts?raw';
import aiRuntimeControlsSectionSource from '../AIRuntimeControlsSection.tsx?raw';
import aiSettingsStatusAndActionsSource from '../AISettingsStatusAndActions.tsx?raw';
import aiSettingsStateSource from '../useAISettingsState.ts?raw';
import reportingPanelSource from '../ReportingPanel.tsx?raw';
import reportingPanelModelSource from '../reportingPanelModel.ts?raw';
@@ -111,6 +115,10 @@ import workloadTypePresentationSource from '@/utils/workloadTypePresentation.ts?
const aiSettingsSource = [
aiSettingsShellSource,
aiModelSelectionSectionSource,
aiRuntimeControlsSectionSource,
aiChatMaintenanceSectionSource,
aiSettingsStatusAndActionsSource,
aiProviderConfigurationSectionSource,
aiSettingsDialogsSource,
aiSettingsModelSource,
@@ -414,6 +422,10 @@ describe('monitored-system model guardrails', () => {
expect(aiSettingsSource).toContain('getAICredentialsClearErrorMessage');
expect(aiSettingsSource).toContain('getAISettingsToggleErrorMessage');
expect(aiSettingsShellSource).toContain('@/components/Settings/useAISettingsState');
expect(aiSettingsShellSource).toContain('@/components/Settings/AIModelSelectionSection');
expect(aiSettingsShellSource).toContain('@/components/Settings/AIRuntimeControlsSection');
expect(aiSettingsShellSource).toContain('@/components/Settings/AIChatMaintenanceSection');
expect(aiSettingsShellSource).toContain('@/components/Settings/AISettingsStatusAndActions');
expect(aiSettingsSource).not.toContain('Loading Pulse Assistant settings...');
expect(aiSettingsSource).not.toContain('Loading chat sessions...');
expect(aiSettingsSource).not.toContain('No chat sessions yet. Start a chat to create one.');
@@ -427,7 +439,11 @@ describe('monitored-system model guardrails', () => {
expect(aiSettingsSource).not.toContain('const errorMessages: Record<string, string> =');
expect(aiSettingsShellSource).not.toContain('const [loading, setLoading] = createSignal(false);');
expect(aiSettingsShellSource).not.toContain('const handleSave = async (event?: Event) =>');
expect(aiSettingsShellSource).not.toContain('Chat Session Maintenance');
expect(aiSettingsShellSource).not.toContain('Discovery Settings');
expect(aiSettingsShellSource).not.toContain('Pulse Permission Level');
expect(aiSettingsStateSource).toContain('export const useAISettingsState =');
expect(aiSettingsStateSource).toContain('export type AISettingsState =');
expect(aiSettingsStateSource).toContain('const [loading, setLoading] = createSignal(false);');
expect(aiSettingsStateSource).toContain('const handleSave = async (event?: Event) =>');
expect(aiSettingsStateSource).toContain('const handleEnabledToggle = async (newValue: boolean) =>');
@@ -32,9 +32,13 @@ import billingAdminPanelSource from '../BillingAdminPanel.tsx?raw';
import billingAdminPanelStateSource from '../useBillingAdminPanelState.ts?raw';
import generalSettingsPanelSource from '../GeneralSettingsPanel.tsx?raw';
import aiSettingsPanelSource from '../AISettings.tsx?raw';
import aiChatMaintenanceSectionSource from '../AIChatMaintenanceSection.tsx?raw';
import aiProviderConfigurationSectionSource from '../AIProviderConfigurationSection.tsx?raw';
import aiSettingsDialogsSource from '../AISettingsDialogs.tsx?raw';
import aiModelSelectionSectionSource from '../AIModelSelectionSection.tsx?raw';
import aiSettingsModelSource from '../aiSettingsModel.ts?raw';
import aiRuntimeControlsSectionSource from '../AIRuntimeControlsSection.tsx?raw';
import aiSettingsStatusAndActionsSource from '../AISettingsStatusAndActions.tsx?raw';
import aiSettingsStateSource from '../useAISettingsState.ts?raw';
import diagnosticsModelSource from '../diagnosticsModel.ts?raw';
import diagnosticsPanelSource from '../DiagnosticsPanel.tsx?raw';
@@ -135,6 +139,10 @@ const extractedModules = [
'../InfrastructureStopMonitoringDialog.tsx',
'../AIProviderConfigurationSection.tsx',
'../AISettingsDialogs.tsx',
'../AIChatMaintenanceSection.tsx',
'../AIModelSelectionSection.tsx',
'../AIRuntimeControlsSection.tsx',
'../AISettingsStatusAndActions.tsx',
'../aiSettingsModel.ts',
'../useAISettingsState.ts',
'../diagnosticsModel.ts',
@@ -734,13 +742,26 @@ describe('Settings architecture guardrails', () => {
});
it('keeps AI settings sub-surfaces behind extracted runtime owners', () => {
expect(aiSettingsPanelSource).toContain('@/components/Settings/AIProviderConfigurationSection');
expect(aiSettingsPanelSource).toContain('@/components/Settings/AIModelSelectionSection');
expect(aiSettingsPanelSource).toContain('@/components/Settings/AIRuntimeControlsSection');
expect(aiSettingsPanelSource).toContain('@/components/Settings/AIChatMaintenanceSection');
expect(aiSettingsPanelSource).toContain('@/components/Settings/AISettingsStatusAndActions');
expect(aiSettingsPanelSource).toContain('@/components/Settings/AISettingsDialogs');
expect(aiSettingsPanelSource).toContain('@/components/Settings/aiSettingsModel');
expect(aiSettingsPanelSource).toContain('@/components/Settings/useAISettingsState');
expect(aiSettingsPanelSource).not.toContain('const [loading, setLoading] = createSignal(false);');
expect(aiSettingsPanelSource).not.toContain('const handleSave = async (event?: Event) =>');
expect(aiSettingsPanelSource).not.toContain('AIAPI.getSettings()');
expect(aiSettingsPanelSource).not.toContain('Chat Session Maintenance');
expect(aiSettingsPanelSource).not.toContain('Discovery Settings');
expect(aiSettingsPanelSource).not.toContain('Pulse Permission Level');
expect(aiModelSelectionSectionSource).toContain('@/components/Settings/AIProviderConfigurationSection');
expect(aiModelSelectionSectionSource).toContain('@/components/Settings/aiSettingsModel');
expect(aiModelSelectionSectionSource).toContain('Advanced Model Selection');
expect(aiRuntimeControlsSectionSource).toContain('Discovery Settings');
expect(aiRuntimeControlsSectionSource).toContain('Pulse Permission Level');
expect(aiChatMaintenanceSectionSource).toContain('Chat Session Maintenance');
expect(aiSettingsStatusAndActionsSource).toContain('Save changes');
expect(aiSettingsStatusAndActionsSource).toContain('Test Connection');
expect(aiProviderConfigurationSectionSource).toContain('@/components/Settings/aiSettingsModel');
expect(aiSettingsDialogsSource).toContain('@/components/Settings/aiSettingsModel');
expect(aiSettingsModelSource).toContain('export const AI_PROVIDER_CONFIGS');
@@ -879,3 +879,5 @@ export const useAISettingsState = () => {
upgradeAutofixUrl,
};
};
export type AISettingsState = ReturnType<typeof useAISettingsState>;
@@ -343,9 +343,13 @@ import runToolCallTraceSource from '@/components/patrol/RunToolCallTrace.tsx?raw
import diagnosticsPanelSource from '@/components/Settings/DiagnosticsPanel.tsx?raw';
import diagnosticsPresentationSource from '@/utils/diagnosticsPresentation.ts?raw';
import aiSettingsShellSource from '@/components/Settings/AISettings.tsx?raw';
import aiChatMaintenanceSectionSource from '@/components/Settings/AIChatMaintenanceSection.tsx?raw';
import aiProviderConfigurationSectionSource from '@/components/Settings/AIProviderConfigurationSection.tsx?raw';
import aiSettingsDialogsSource from '@/components/Settings/AISettingsDialogs.tsx?raw';
import aiModelSelectionSectionSource from '@/components/Settings/AIModelSelectionSection.tsx?raw';
import aiSettingsModelSource from '@/components/Settings/aiSettingsModel.ts?raw';
import aiRuntimeControlsSectionSource from '@/components/Settings/AIRuntimeControlsSection.tsx?raw';
import aiSettingsStatusAndActionsSource from '@/components/Settings/AISettingsStatusAndActions.tsx?raw';
import aiSettingsStateSource from '@/components/Settings/useAISettingsState.ts?raw';
import aiIntelligenceSource from '@/pages/AIIntelligence.tsx?raw';
import patrolIntelligenceSurfaceSource from '@/features/patrol/PatrolIntelligenceSurface.tsx?raw';
@@ -371,6 +375,10 @@ import infrastructurePageStateSource from '@/features/infrastructure/useInfrastr
const aiSettingsSource = [
aiSettingsShellSource,
aiModelSelectionSectionSource,
aiRuntimeControlsSectionSource,
aiChatMaintenanceSectionSource,
aiSettingsStatusAndActionsSource,
aiProviderConfigurationSectionSource,
aiSettingsDialogsSource,
aiSettingsModelSource,
@@ -2907,6 +2915,10 @@ describe('frontend resource type boundaries', () => {
expect(aiSettingsSource).toContain('getAICredentialsClearErrorMessage');
expect(aiSettingsSource).toContain('getAISettingsToggleErrorMessage');
expect(aiSettingsShellSource).toContain('@/components/Settings/useAISettingsState');
expect(aiSettingsShellSource).toContain('@/components/Settings/AIModelSelectionSection');
expect(aiSettingsShellSource).toContain('@/components/Settings/AIRuntimeControlsSection');
expect(aiSettingsShellSource).toContain('@/components/Settings/AIChatMaintenanceSection');
expect(aiSettingsShellSource).toContain('@/components/Settings/AISettingsStatusAndActions');
expect(aiSettingsSource).not.toContain('Loading Pulse Assistant settings...');
expect(aiSettingsSource).not.toContain('Loading chat sessions...');
expect(aiSettingsSource).not.toContain('No chat sessions yet. Start a chat to create one.');
@@ -2923,10 +2935,14 @@ describe('frontend resource type boundaries', () => {
expect(aiSettingsSource).not.toContain("'Failed to update Pulse Assistant setting'");
expect(aiSettingsShellSource).not.toContain('const [loading, setLoading] = createSignal(false);');
expect(aiSettingsShellSource).not.toContain('const handleSave = async (event?: Event) =>');
expect(aiSettingsShellSource).not.toContain('Chat Session Maintenance');
expect(aiSettingsShellSource).not.toContain('Discovery Settings');
expect(aiSettingsShellSource).not.toContain('Pulse Permission Level');
expect(aiSettingsSource).not.toContain(
"providerTestResult()?.success ? 'text-green-600' : 'text-red-600'",
);
expect(aiSettingsStateSource).toContain('export const useAISettingsState =');
expect(aiSettingsStateSource).toContain('export type AISettingsState =');
expect(aiSettingsStateSource).toContain('const [loading, setLoading] = createSignal(false);');
expect(aiSettingsStateSource).toContain('const handleSave = async (event?: Event) =>');
expect(aiSettingsStateSource).toContain('const handleEnabledToggle = async (newValue: boolean) =>');
@@ -1856,7 +1856,11 @@ class SubsystemLookupTest(unittest.TestCase):
def test_lookup_paths_assigns_remaining_settings_shell_panels_to_frontend_primitives(self) -> None:
runtime_files = [
"frontend-modern/src/components/Settings/APIAccessPanel.tsx",
"frontend-modern/src/components/Settings/AIChatMaintenanceSection.tsx",
"frontend-modern/src/components/Settings/AIModelSelectionSection.tsx",
"frontend-modern/src/components/Settings/AIRuntimeControlsSection.tsx",
"frontend-modern/src/components/Settings/AISettings.tsx",
"frontend-modern/src/components/Settings/AISettingsStatusAndActions.tsx",
"frontend-modern/src/components/Settings/AuditLogPanel.tsx",
"frontend-modern/src/components/Settings/AuditWebhookPanel.tsx",
"frontend-modern/src/components/Settings/GeneralSettingsPanel.tsx",