mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-11 14:00:29 +00:00
Centralize identifier label formatting
This commit is contained in:
@@ -26,6 +26,8 @@ runtime cost control, and shared AI transport surfaces.
|
||||
4. `internal/api/ai_intelligence_handlers.go`
|
||||
5. `frontend-modern/src/api/ai.ts`
|
||||
6. `frontend-modern/src/api/patrol.ts`
|
||||
7. `frontend-modern/src/components/AI/Chat/`
|
||||
8. `frontend-modern/src/utils/textPresentation.ts`
|
||||
|
||||
## Shared Boundaries
|
||||
|
||||
@@ -40,6 +42,7 @@ runtime cost control, and shared AI transport surfaces.
|
||||
1. Add or change chat runtime, Patrol orchestration, findings generation, or remediation behavior through `internal/ai/`
|
||||
2. Add or change Pulse Assistant request flow through `internal/api/ai_handler.go` and `frontend-modern/src/api/ai.ts`
|
||||
3. Add or change Patrol, alert-analysis, or remediation transport through `internal/api/ai_handlers.go`, `internal/api/ai_intelligence_handlers.go`, and `frontend-modern/src/api/patrol.ts`
|
||||
4. Keep AI chat presentation helpers aligned through `frontend-modern/src/components/AI/Chat/` and the shared `frontend-modern/src/utils/textPresentation.ts`
|
||||
|
||||
## Forbidden Paths
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ helpers.
|
||||
11. `frontend-modern/src/utils/patrolFormat.ts`
|
||||
12. `frontend-modern/src/utils/patrolRunPresentation.ts`
|
||||
13. `frontend-modern/src/utils/patrolSummaryPresentation.ts`
|
||||
14. `frontend-modern/src/utils/textPresentation.ts`
|
||||
|
||||
## Shared Boundaries
|
||||
|
||||
@@ -43,7 +44,8 @@ helpers.
|
||||
|
||||
1. Add or change Patrol page orchestration through `frontend-modern/src/pages/AIIntelligence.tsx` and `frontend-modern/src/stores/aiIntelligence.ts`
|
||||
2. Add or change Patrol findings, approvals, investigation, or run-history presentation through `frontend-modern/src/components/AI/FindingsPanel.tsx` and `frontend-modern/src/components/patrol/`
|
||||
3. Keep Patrol transport and payload changes aligned through the governed AI runtime and API contract transport surfaces
|
||||
3. Keep Patrol and chat identifier-label presentation aligned through the shared `frontend-modern/src/utils/textPresentation.ts`
|
||||
4. Keep Patrol transport and payload changes aligned through the governed AI runtime and API contract transport surfaces
|
||||
|
||||
## Forbidden Paths
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import { ToolExecutionBlock } from './ToolExecutionBlock';
|
||||
import { ApprovalCard } from './ApprovalCard';
|
||||
import { QuestionCard } from './QuestionCard';
|
||||
import type { ChatMessage, PendingApproval, PendingQuestion, StreamDisplayEvent } from './types';
|
||||
import { formatIdentifierLabel } from '@/utils/textPresentation';
|
||||
|
||||
interface MessageItemProps {
|
||||
message: ChatMessage;
|
||||
@@ -102,8 +103,6 @@ export const MessageItem: Component<MessageItemProps> = (props) => {
|
||||
return Array.from(names);
|
||||
});
|
||||
|
||||
const formatToolName = (name: string) => name.replace(/^pulse_/, '').replace(/_/g, ' ');
|
||||
|
||||
// Check if currently streaming content (no tools pending, still streaming)
|
||||
const isStreamingText = () =>
|
||||
props.message.isStreaming &&
|
||||
@@ -236,7 +235,7 @@ export const MessageItem: Component<MessageItemProps> = (props) => {
|
||||
<div class="flex flex-wrap gap-1.5">
|
||||
{contextTools().map((name) => (
|
||||
<span class="px-1.5 py-0.5 rounded text-[10px] bg-surface-hover text-muted border border-border font-medium">
|
||||
{formatToolName(name)}
|
||||
{formatIdentifierLabel(name, { stripPrefix: 'pulse_' })}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Component, Show, createSignal, createMemo, For } from 'solid-js';
|
||||
import type { ToolExecution, PendingTool } from './types';
|
||||
import { getToolCallResultTextClass } from '@/utils/patrolRunPresentation';
|
||||
import { formatIdentifierLabel } from '@/utils/textPresentation';
|
||||
|
||||
interface ToolExecutionBlockProps {
|
||||
tool: ToolExecution;
|
||||
@@ -28,10 +29,7 @@ export const ToolExecutionBlock: Component<ToolExecutionBlockProps> = (props) =>
|
||||
if (name === 'pulse_get_storage_config') return 'storage cfg';
|
||||
if (name === 'get_resource_details' || name === 'pulse_get_resource_details') return 'resource';
|
||||
if (name.includes('finding')) return 'finding';
|
||||
return name
|
||||
.replace(/^pulse_/, '')
|
||||
.replace(/_/g, ' ')
|
||||
.substring(0, 12);
|
||||
return formatIdentifierLabel(name, { stripPrefix: 'pulse_', maxLength: 12 });
|
||||
});
|
||||
|
||||
// Check if output is non-empty and interesting
|
||||
@@ -155,10 +153,7 @@ export const PendingToolBlock: Component<PendingToolBlockProps> = (props) => {
|
||||
if (name === 'get_storage' || name === 'pulse_get_storage') return 'storage';
|
||||
if (name === 'get_resource_details' || name === 'pulse_get_resource_details') return 'resource';
|
||||
if (name.includes('finding')) return 'finding';
|
||||
return name
|
||||
.replace(/^pulse_/, '')
|
||||
.replace(/_/g, ' ')
|
||||
.substring(0, 12);
|
||||
return formatIdentifierLabel(name, { stripPrefix: 'pulse_', maxLength: 12 });
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
@@ -678,7 +678,7 @@ describe('MessageItem', () => {
|
||||
));
|
||||
|
||||
expect(screen.getByText('Context used')).toBeInTheDocument();
|
||||
// formatToolName strips 'pulse_' and replaces underscores
|
||||
// Shared identifier formatter strips 'pulse_' and replaces underscores
|
||||
expect(screen.getByText('get nodes')).toBeInTheDocument();
|
||||
expect(screen.getByText('get metrics')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -36,6 +36,7 @@ import { ChatMessages } from './ChatMessages';
|
||||
import { ModelSelector } from './ModelSelector';
|
||||
import { MentionAutocomplete, type MentionResource } from './MentionAutocomplete';
|
||||
import type { PendingApproval, PendingQuestion, ModelInfo } from './types';
|
||||
import { formatIdentifierLabel } from '@/utils/textPresentation';
|
||||
|
||||
const MODEL_SESSION_STORAGE_KEY = 'pulse:ai_chat_models_by_session';
|
||||
const DEFAULT_SESSION_KEY = '__default__';
|
||||
@@ -276,7 +277,7 @@ export const AIChat: Component<AIChatProps> = (props) => {
|
||||
|
||||
if (lastMessage.pendingTools && lastMessage.pendingTools.length > 0) {
|
||||
const tool = lastMessage.pendingTools[0];
|
||||
const toolName = tool.name.replace(/^pulse_/, '').replace(/_/g, ' ');
|
||||
const toolName = formatIdentifierLabel(tool.name, { stripPrefix: 'pulse_' });
|
||||
return { type: 'tool', text: `Running ${toolName}...` };
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import { Component, createResource, createMemo, Show } from 'solid-js';
|
||||
import { getPatrolRunHistory, type PatrolRunRecord } from '@/api/patrol';
|
||||
import { aiIntelligenceStore } from '@/stores/aiIntelligence';
|
||||
import { formatRelativeTime } from '@/utils/format';
|
||||
import { formatTriggerReason } from '@/utils/patrolFormat';
|
||||
import { isPatrolRunHealthy } from '@/utils/patrolRunPresentation';
|
||||
import CheckCircleIcon from 'lucide-solid/icons/check-circle';
|
||||
import AlertCircleIcon from 'lucide-solid/icons/alert-circle';
|
||||
@@ -20,29 +21,6 @@ interface PatrolStatusBarProps {
|
||||
}
|
||||
|
||||
export const PatrolStatusBar: Component<PatrolStatusBarProps> = (props) => {
|
||||
const formatTrigger = (reason?: string) => {
|
||||
switch (reason) {
|
||||
case 'scheduled':
|
||||
return 'Scheduled';
|
||||
case 'manual':
|
||||
return 'Manual';
|
||||
case 'startup':
|
||||
return 'Startup';
|
||||
case 'alert_fired':
|
||||
return 'Alert fired';
|
||||
case 'alert_cleared':
|
||||
return 'Alert cleared';
|
||||
case 'anomaly':
|
||||
return 'Anomaly';
|
||||
case 'user_action':
|
||||
return 'User action';
|
||||
case 'config_changed':
|
||||
return 'Config change';
|
||||
default:
|
||||
return reason ? reason.replace(/_/g, ' ') : '';
|
||||
}
|
||||
};
|
||||
|
||||
const [runs] = createResource(
|
||||
() => props.refreshTrigger ?? 0,
|
||||
async (): Promise<PatrolRunRecord[]> => {
|
||||
@@ -68,7 +46,7 @@ export const PatrolStatusBar: Component<PatrolStatusBarProps> = (props) => {
|
||||
runsToday: todayRuns.length,
|
||||
newFindingsToday: todayRuns.reduce((sum, r) => sum + (r.new_findings || 0), 0),
|
||||
lastRunTime: lastRunTime ? formatRelativeTime(lastRunTime, { compact: true }) : null,
|
||||
lastRunTrigger: formatTrigger(lastRun?.trigger_reason),
|
||||
lastRunTrigger: formatTriggerReason(lastRun?.trigger_reason),
|
||||
isHealthy: isPatrolRunHealthy(lastRun?.status, lastRun?.error_count ?? 0),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -71,6 +71,12 @@ import resourceRelationshipPresentationSource from '@/utils/resourceRelationship
|
||||
import resourceCorrelationPresentationSource from '@/utils/resourceCorrelationPresentation.ts?raw';
|
||||
import confidencePresentationSource from '@/utils/confidencePresentation.ts?raw';
|
||||
import textPresentationSource from '@/utils/textPresentation.ts?raw';
|
||||
import messageItemSource from '@/components/AI/Chat/MessageItem.tsx?raw';
|
||||
import toolExecutionBlockSource from '@/components/AI/Chat/ToolExecutionBlock.tsx?raw';
|
||||
import aiChatSource from '@/components/AI/Chat/index.tsx?raw';
|
||||
import patrolStatusBarSource from '@/components/patrol/PatrolStatusBar.tsx?raw';
|
||||
import patrolFormatSource from '@/utils/patrolFormat.ts?raw';
|
||||
import aiFindingPresentationSource from '@/utils/aiFindingPresentation.ts?raw';
|
||||
import resourcePolicyNormalizationSource from '@/utils/resourcePolicyNormalization.ts?raw';
|
||||
import diskListSource from '@/components/Storage/DiskList.tsx?raw';
|
||||
import useDiskListModelSource from '@/components/Storage/useDiskListModel.ts?raw';
|
||||
@@ -148,7 +154,6 @@ import useUnifiedResourcesSource from '@/hooks/useUnifiedResources.ts?raw';
|
||||
import findingsPanelSource from '@/components/AI/FindingsPanel.tsx?raw';
|
||||
import exploreStatusBlockSource from '@/components/AI/Chat/ExploreStatusBlock.tsx?raw';
|
||||
import aiExplorePresentationSource from '@/utils/aiExplorePresentation.ts?raw';
|
||||
import aiFindingPresentationSource from '@/utils/aiFindingPresentation.ts?raw';
|
||||
import discoveryTabSource from '@/components/Discovery/DiscoveryTab.tsx?raw';
|
||||
import discoveryPresentationSource from '@/utils/discoveryPresentation.ts?raw';
|
||||
import mailGatewaySource from '@/components/PMG/MailGateway.tsx?raw';
|
||||
@@ -1867,6 +1872,22 @@ describe('frontend resource type boundaries', () => {
|
||||
expect(resourceCorrelationPresentationSource).toContain('humanizeToken');
|
||||
expect(resourceDetailDrawerSource).toContain('humanizeToken');
|
||||
expect(textPresentationSource).toContain('humanizeToken');
|
||||
expect(textPresentationSource).toContain('formatIdentifierLabel');
|
||||
expect(messageItemSource).toContain('formatIdentifierLabel');
|
||||
expect(toolExecutionBlockSource).toContain('formatIdentifierLabel');
|
||||
expect(aiChatSource).toContain('formatIdentifierLabel');
|
||||
expect(patrolStatusBarSource).toContain('formatTriggerReason');
|
||||
expect(patrolFormatSource).toContain('formatIdentifierLabel');
|
||||
expect(aiFindingPresentationSource).toContain('formatIdentifierLabel');
|
||||
expect(messageItemSource).not.toContain("replace(/^pulse_/, '').replace(/_/g, ' ')");
|
||||
expect(toolExecutionBlockSource).not.toContain("replace(/^pulse_/, '').replace(/_/g, ' ')");
|
||||
expect(aiChatSource).not.toContain("replace(/^pulse_/, '').replace(/_/g, ' ')");
|
||||
expect(patrolStatusBarSource).not.toContain("replace(/_/g, ' ') : ''");
|
||||
expect(patrolFormatSource).not.toContain("replace(/_/g, ' ') : 'Unknown'");
|
||||
expect(aiFindingPresentationSource).not.toContain("replace(/_/g, ' ')");
|
||||
expect(patrolRunPresentationSource).toContain('formatIdentifierLabel');
|
||||
expect(patrolRunPresentationSource).not.toContain("normalized.replace(/_/g, ' ')");
|
||||
expect(patrolRunPresentationSource).not.toContain("normalized ? normalized.replace(/_/g, ' ') : 'unknown'");
|
||||
expect(useUnifiedResourcesSource).toContain('normalizeResourcePolicyAISafeSummary(');
|
||||
expect(resourcePolicyNormalizationSource).toContain(
|
||||
'normalizeResourcePolicyAISafeSummary',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { humanizeToken } from '@/utils/textPresentation';
|
||||
import { formatIdentifierLabel, humanizeToken } from '@/utils/textPresentation';
|
||||
|
||||
describe('textPresentation', () => {
|
||||
it('humanizes underscore-separated tokens with a fallback', () => {
|
||||
@@ -13,4 +13,16 @@ describe('textPresentation', () => {
|
||||
expect(humanizeToken('IP', { preserveShortAllCaps: true })).toBe('IP');
|
||||
expect(humanizeToken('vm')).toBe('Vm');
|
||||
});
|
||||
|
||||
it('formats identifier labels without title-casing', () => {
|
||||
expect(formatIdentifierLabel('pulse_get_container_status', { stripPrefix: 'pulse_' })).toBe(
|
||||
'get container status',
|
||||
);
|
||||
expect(formatIdentifierLabel('some_reason')).toBe('some reason');
|
||||
expect(formatIdentifierLabel('pulse_very_long_unknown_tool_name', {
|
||||
stripPrefix: 'pulse_',
|
||||
maxLength: 12,
|
||||
})).toBe('very long un');
|
||||
expect(formatIdentifierLabel(undefined, { fallback: 'Unknown' })).toBe('Unknown');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { UnifiedFinding } from '@/stores/aiIntelligence';
|
||||
import type { ApprovalRequest } from '@/api/ai';
|
||||
import type { InvestigationOutcome, InvestigationStatus } from '@/api/patrol';
|
||||
import { isLivePendingApproval } from '@/utils/approvalState';
|
||||
import { formatIdentifierLabel } from '@/utils/textPresentation';
|
||||
|
||||
const DEFAULT_BADGE_CLASSES = 'border-border bg-surface-alt text-muted';
|
||||
const DEFAULT_LOOP_STATE_CLASSES = 'border-border bg-surface-alt text-muted';
|
||||
@@ -305,10 +306,11 @@ export const doesFindingNeedAttention = (
|
||||
export const getFindingLoopStateBadgeClasses = (loopState: string): string =>
|
||||
FINDING_LOOP_STATE_CLASSES[loopState] || DEFAULT_LOOP_STATE_CLASSES;
|
||||
|
||||
export const formatFindingLoopState = (loopState: string): string => loopState.replace(/_/g, ' ');
|
||||
export const formatFindingLoopState = (loopState: string): string =>
|
||||
formatIdentifierLabel(loopState);
|
||||
|
||||
export const formatFindingLifecycleType = (value: string): string =>
|
||||
FINDING_LIFECYCLE_LABELS[value] || value.replace(/_/g, ' ');
|
||||
FINDING_LIFECYCLE_LABELS[value] || formatIdentifierLabel(value);
|
||||
|
||||
export const getFindingResolutionReason = (
|
||||
finding: Pick<
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// Pure formatting helpers for Patrol run data — no SolidJS dependencies.
|
||||
import { formatIdentifierLabel } from '@/utils/textPresentation';
|
||||
|
||||
interface ModelInfo {
|
||||
id: string;
|
||||
@@ -52,7 +53,7 @@ export function formatTriggerReason(reason?: string): string {
|
||||
case 'config_changed':
|
||||
return 'Config change';
|
||||
default:
|
||||
return reason ? reason.replace(/_/g, ' ') : 'Unknown';
|
||||
return formatIdentifierLabel(reason, { fallback: 'Unknown' });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { PatrolRunStatus } from '@/api/patrol';
|
||||
import { formatIdentifierLabel } from '@/utils/textPresentation';
|
||||
|
||||
export interface PatrolRunStatusPresentation {
|
||||
badgeClass: string;
|
||||
@@ -29,7 +30,7 @@ export function getPatrolRunStatusPresentation(
|
||||
case 'error':
|
||||
return {
|
||||
badgeClass: 'bg-red-100 text-red-700 dark:bg-red-900 dark:text-red-300',
|
||||
label: normalized.replace(/_/g, ' '),
|
||||
label: formatIdentifierLabel(normalized),
|
||||
};
|
||||
case 'issues_found':
|
||||
return {
|
||||
@@ -44,7 +45,7 @@ export function getPatrolRunStatusPresentation(
|
||||
default:
|
||||
return {
|
||||
badgeClass: 'bg-surface-alt text-base-content',
|
||||
label: normalized ? normalized.replace(/_/g, ' ') : 'unknown',
|
||||
label: formatIdentifierLabel(normalized, { fallback: 'unknown' }),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,12 @@ export interface HumanizeTokenOptions {
|
||||
preserveShortAllCaps?: boolean;
|
||||
}
|
||||
|
||||
export interface IdentifierLabelOptions {
|
||||
fallback?: string;
|
||||
maxLength?: number;
|
||||
stripPrefix?: string;
|
||||
}
|
||||
|
||||
export function humanizeToken(value?: string, options?: HumanizeTokenOptions): string {
|
||||
const normalized = (value || '').trim();
|
||||
if (!normalized) {
|
||||
@@ -17,3 +23,28 @@ export function humanizeToken(value?: string, options?: HumanizeTokenOptions): s
|
||||
.replace(/_/g, ' ')
|
||||
.replace(/\b\w/g, (char) => char.toUpperCase());
|
||||
}
|
||||
|
||||
export function formatIdentifierLabel(
|
||||
value?: string,
|
||||
options?: IdentifierLabelOptions,
|
||||
): string {
|
||||
let normalized = (value || '').trim();
|
||||
if (!normalized) {
|
||||
return options?.fallback ?? '';
|
||||
}
|
||||
|
||||
if (options?.stripPrefix && normalized.startsWith(options.stripPrefix)) {
|
||||
normalized = normalized.slice(options.stripPrefix.length).trim();
|
||||
}
|
||||
|
||||
if (!normalized) {
|
||||
return options?.fallback ?? '';
|
||||
}
|
||||
|
||||
const label = normalized.replace(/_/g, ' ');
|
||||
if (options?.maxLength && options.maxLength > 0) {
|
||||
return label.substring(0, options.maxLength);
|
||||
}
|
||||
|
||||
return label;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user