Centralize identifier label formatting

This commit is contained in:
rcourtman
2026-03-19 06:22:48 +00:00
parent 79c7d59613
commit d5ba511e7b
13 changed files with 91 additions and 45 deletions
@@ -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<
+2 -1
View File
@@ -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;
}