Watch
1
0
Fork
You've already forked RedFlag
0

web: status/severity colors get one truth table

statusColors.ts in primitives — 9 domains, kept distinct on purpose. Eight files stop hand-rolling switches. Known discrepancy documented in place: CommandStatusBadge paints completed gray, both inline command timelines paint it green.
This commit is contained in:
Fimeg 2026-06-12 14:01:00 -04:00
commit d75939cb6a
12 changed files with 284 additions and 158 deletions

View file

@ -19,6 +19,7 @@ import { formatRelativeTime } from '@/lib/utils';
import { agentApi, securityApi } from '@/lib/api';
import toast from 'react-hot-toast';
import { cn } from '@/lib/utils';
import { securitySubsystemStatusColor } from '@/components/primitives/statusColors';
import { AgentSubsystem } from '@/types';
import { AgentUpdatesModal } from './AgentUpdatesModal';
@ -596,12 +597,6 @@ export function AgentHealth({ agentId }: AgentHealthProps) {
{/* Security Grid - 2x2 Layout */}
<div className="grid grid-cols-2 gap-3">
{Object.entries(securityOverview.subsystems).map(([key, subsystem]) => {
const statusColors = {
healthy: 'bg-green-100 text-green-700 border-green-200',
enforced: 'bg-blue-100 text-blue-700 border-blue-200',
degraded: 'bg-amber-100 text-amber-700 border-amber-200',
unhealthy: 'bg-red-100 text-red-700 border-red-200'
};
return (
<div key={key} className="group relative">
@ -633,7 +628,7 @@ export function AgentHealth({ agentId }: AgentHealthProps) {
</div>
<div className={cn(
'px-1.5 py-0.5 rounded text-[10px] font-medium border',
statusColors[subsystem.status as keyof typeof statusColors] || statusColors.unhealthy
securitySubsystemStatusColor(subsystem.status)
)}>
{subsystem.status === 'healthy' && <CheckCircle className="w-3 h-3 inline" />}
{subsystem.status === 'enforced' && <Shield className="w-3 h-3 inline" />}

View file

@ -16,7 +16,8 @@ import {
CheckCircle,
XCircle,
} from 'lucide-react';
import { formatRelativeTime, formatBytes, getSeverityColor } from '@/lib/utils';
import { formatRelativeTime, formatBytes } from '@/lib/utils';
import { packageSeverityColor, packageSeverityTextColor } from '@/components/primitives/statusColors';
import { updateApi } from '@/lib/api';
import toast from 'react-hot-toast';
import { cn } from '@/lib/utils';
@ -345,12 +346,7 @@ export function AgentUpdatesEnhanced({ agentId, onNavigateToHistory }: AgentUpda
if (count === 0) return null;
return (
<span key={severity} className="text-gray-500">
<span className={cn(
'font-medium',
severity === 'critical' ? 'text-red-600' :
severity === 'high' ? 'text-orange-600' :
severity === 'medium' ? 'text-yellow-600' : 'text-blue-600'
)}>{count}</span> {severity}
<span className={cn('font-medium', packageSeverityTextColor(severity))}>{count}</span> {severity}
</span>
);
})}
@ -461,7 +457,7 @@ export function AgentUpdatesEnhanced({ agentId, onNavigateToHistory }: AgentUpda
{statusMeta.label}
</span>
<span className={cn('chip', getSeverityColor(update.severity))}>
<span className={cn('chip', packageSeverityColor(update.severity))}>
{update.severity.toUpperCase()}
</span>
<span className="text-sm text-gray-900 truncate">{update.package_name}</span>

View file

@ -18,6 +18,7 @@ import { useQuery } from '@tanstack/react-query';
import { logApi } from '@/lib/api';
import { cn } from '@/lib/utils';
import { formatRelativeTime } from '@/lib/utils';
import { historyResultColor } from '@/components/primitives/statusColors';
import toast from 'react-hot-toast';
interface HistoryEntry {
@ -188,23 +189,6 @@ const HistoryTimeline: React.FC<HistoryTimelineProps> = ({ agentId, className })
}
};
// Get status color
const getStatusColor = (result: string) => {
switch (result) {
case 'success':
return 'text-green-700 bg-green-100 border-green-200';
case 'failed':
return 'text-red-700 bg-red-100 border-red-200';
case 'started':
return 'text-blue-700 bg-blue-100 border-blue-200';
case 'running':
return 'text-blue-700 bg-blue-100 border-blue-200';
case 'partial':
return 'text-amber-700 bg-amber-100 border-amber-200';
default:
return 'text-gray-700 bg-gray-100 border-gray-200';
}
};
// Format duration
const formatDuration = (seconds: number) => {
@ -377,7 +361,7 @@ const HistoryTimeline: React.FC<HistoryTimelineProps> = ({ agentId, className })
</span>
<span className={cn(
"badge border",
getStatusColor(entry.result)
historyResultColor(entry.result)
)}>
{entry.result}
</span>

View file

@ -2,9 +2,9 @@ import React from 'react';
import { cn, getStatusColor, getSeverityColor } from '@/lib/utils';
// State badge primitives — the one way to render a lifecycle status or a
// severity as a badge. Colour mapping lives in lib/utils (getStatusColor /
// getSeverityColor); these wrap the repeated `cn('badge', ...)` span so pages
// stop hand-rolling it (and drifting palettes while they're at it).
// severity as a badge. Colour mapping lives in primitives/statusColors.ts
// (packageStatusColor / packageSeverityColor); lib/utils re-exports them as
// getStatusColor / getSeverityColor for backwards compat.
interface BadgeProps {
/** Extra classes merged onto the badge (e.g. 'text-[10px]', 'ml-auto'). */

View file

@ -16,3 +16,15 @@ export { default as CommandStatusBadge, getCommandStatus } from './CommandStatus
export { default as SortableTable } from './SortableTable';
export type { Column } from './SortableTable';
export { StatusBadge, SeverityBadge } from './StateBadge';
export {
packageSeverityColor,
packageSeverityTextColor,
packageStatusColor,
lifecycleHistoryStatusColor,
commandStatusInlineColor,
tokenStatusColor,
historyResultColor,
securityEventSeverityColor,
securityHealthColor,
securitySubsystemStatusColor,
} from './statusColors';

View file

@ -0,0 +1,241 @@
// Single source of truth for status/severity -> Tailwind color class mappings.
//
// Each domain is kept separate even when two domains share identical colors
// today — they are semantically distinct and may diverge independently.
//
// Return value convention: all functions return a string of Tailwind classes
// suitable for direct use in className. Badge and border classes are included
// where the original code included them; text-only variants return only text
// classes.
// ---------------------------------------------------------------------------
// DOMAIN: package/vuln severity (critical / high / medium / low / unknown)
// Used by: StateBadge (SeverityBadge), AgentUpdatesEnhanced severity chips
// Source authority: lib/utils.ts getSeverityColor
// ---------------------------------------------------------------------------
export function packageSeverityColor(severity: string): string {
switch (severity) {
case 'critical':
return 'text-danger-600 bg-danger-100';
case 'important':
case 'high':
return 'text-warning-600 bg-warning-100';
case 'moderate':
case 'medium':
return 'text-info-600 bg-info-100';
case 'low':
case 'none':
return 'text-gray-600 bg-gray-100';
default:
return 'text-gray-600 bg-gray-100';
}
}
// Inline text-only severity accent used in AgentUpdatesEnhanced severity
// count chips (e.g. "3 critical"). Only a text color — no bg.
export function packageSeverityTextColor(severity: string): string {
switch (severity) {
case 'critical':
return 'text-red-600';
case 'high':
return 'text-orange-600';
case 'medium':
return 'text-yellow-600';
case 'low':
return 'text-blue-600';
default:
return 'text-gray-600';
}
}
// ---------------------------------------------------------------------------
// DOMAIN: package lifecycle status
// (online/offline/pending/approved/installing/installed/failed/ignored + docker variants)
// Used by: StateBadge (StatusBadge)
// Source authority: lib/utils.ts getStatusColor
// ---------------------------------------------------------------------------
export function packageStatusColor(status: string): string {
switch (status) {
case 'online':
return 'text-success-600 bg-success-100';
case 'offline':
return 'text-danger-600 bg-danger-100';
case 'pending':
return 'text-warning-600 bg-warning-100';
case 'checking_dependencies':
return 'text-info-500 bg-info-100';
case 'pending_dependencies':
return 'text-orange-600 bg-orange-100';
case 'approved':
return 'text-info-600 bg-info-100';
case 'installing':
return 'text-indigo-600 bg-indigo-100';
case 'installed':
return 'text-success-600 bg-success-100';
case 'failed':
return 'text-danger-600 bg-danger-100';
case 'ignored':
return 'text-gray-500 bg-gray-100';
// Docker image lifecycle (docker_images.status)
case 'up-to-date':
return 'text-success-600 bg-success-100';
case 'update-available':
return 'text-info-600 bg-info-100';
case 'update-approved':
return 'text-orange-600 bg-orange-100';
case 'update-scheduled':
return 'text-purple-600 bg-purple-100';
case 'update-installing':
return 'text-indigo-600 bg-indigo-100';
case 'update-failed':
return 'text-danger-600 bg-danger-100';
default:
return 'text-gray-600 bg-gray-100';
}
}
// ---------------------------------------------------------------------------
// DOMAIN: lifecycle history entry status
// (installed / failed / rollback — values from update_version_history rows)
// Used by: PackageDetail.tsx and Updates.tsx lifecycle history sections
// Includes border class — these are rendered as bordered mini-badges.
// ---------------------------------------------------------------------------
export function lifecycleHistoryStatusColor(status: string): string {
switch (status) {
case 'installed':
return 'bg-green-50 text-green-700 border-green-200';
case 'failed':
return 'bg-red-50 text-red-700 border-red-200';
case 'rollback':
return 'bg-amber-50 text-amber-700 border-amber-200';
default:
return 'bg-gray-50 text-gray-600 border-gray-200';
}
}
// ---------------------------------------------------------------------------
// DOMAIN: command status (inline badge, not CommandStatusBadge primitive)
// (completed / failed / timed_out / cancelled / pending / sent / running)
//
// DISCREPANCY: CommandStatusBadge (primitives/CommandStatusBadge.tsx) maps
// 'completed' -> text-gray-600 bg-gray-50 border-gray-200. Both inline sites
// (PackageDetail.tsx and Updates.tsx) map 'completed' -> bg-green-50
// text-green-700 border-green-200. The two inline sites agree with each
// other. The primitives/CommandStatusBadge.tsx is the odd one out.
// Kept as-is per task constraint: deduplication, not restyle.
// ---------------------------------------------------------------------------
export function commandStatusInlineColor(status: string): string {
switch (status) {
case 'completed':
return 'bg-green-50 text-green-700 border-green-200';
case 'failed':
case 'timed_out':
return 'bg-red-50 text-red-700 border-red-200';
case 'cancelled':
return 'bg-gray-50 text-gray-700 border-gray-200';
case 'pending':
case 'sent':
case 'running':
default:
return 'bg-blue-50 text-blue-700 border-blue-200';
}
}
// ---------------------------------------------------------------------------
// DOMAIN: registration token status (active / used / expired / revoked)
// Used by: TokenManagement.tsx
// Returns text-only color — used inside a badge element that supplies bg.
// ---------------------------------------------------------------------------
export function tokenStatusColor(status: string): string {
switch (status) {
case 'active':
return 'text-green-600';
case 'used':
return 'text-yellow-600';
case 'expired':
return 'text-red-600';
case 'revoked':
return 'text-gray-500';
default:
return 'text-gray-500';
}
}
// ---------------------------------------------------------------------------
// DOMAIN: history log result (success / failed / started / running / partial)
// Used by: HistoryTimeline.tsx
// Includes border class — these are rendered as bordered badge spans.
// ---------------------------------------------------------------------------
export function historyResultColor(result: string): string {
switch (result) {
case 'success':
return 'text-green-700 bg-green-100 border-green-200';
case 'failed':
return 'text-red-700 bg-red-100 border-red-200';
case 'started':
case 'running':
return 'text-blue-700 bg-blue-100 border-blue-200';
case 'partial':
return 'text-amber-700 bg-amber-100 border-amber-200';
default:
return 'text-gray-700 bg-gray-100 border-gray-200';
}
}
// ---------------------------------------------------------------------------
// DOMAIN: security event severity (critical / error / warn / info)
// Used by: SecurityEvents.tsx
// These are audit-log severity levels — semantically distinct from package
// vulnerability severity even though 'critical' appears in both.
// Includes border class — icon wrapper uses a bordered rounded-lg container.
// ---------------------------------------------------------------------------
export function securityEventSeverityColor(severity: string): string {
switch (severity) {
case 'critical':
case 'error':
return 'text-red-600 bg-red-50 border-red-200';
case 'warn':
return 'text-yellow-600 bg-yellow-50 border-yellow-200';
case 'info':
return 'text-blue-600 bg-blue-50 border-blue-200';
default:
return 'text-gray-600 bg-gray-50 border-gray-200';
}
}
// ---------------------------------------------------------------------------
// DOMAIN: security overall health (healthy / warning / critical / unknown)
// Used by: SecurityStatusCard.tsx
// Includes border and text — applied to a bordered container.
// ---------------------------------------------------------------------------
export function securityHealthColor(overall: string): string {
switch (overall) {
case 'healthy':
return 'bg-green-50 border-green-200 text-green-900';
case 'warning':
return 'bg-yellow-50 border-yellow-200 text-yellow-900';
case 'critical':
return 'bg-red-50 border-red-200 text-red-900';
default:
return 'bg-gray-50 border-gray-200 text-gray-900';
}
}
// ---------------------------------------------------------------------------
// DOMAIN: security subsystem status (healthy / enforced / degraded / unhealthy)
// Used by: AgentHealth.tsx security grid
// Includes border class — rendered as bordered pill badges.
// ---------------------------------------------------------------------------
export function securitySubsystemStatusColor(status: string): string {
switch (status) {
case 'healthy':
return 'bg-green-100 text-green-700 border-green-200';
case 'enforced':
return 'bg-blue-100 text-blue-700 border-blue-200';
case 'degraded':
return 'bg-amber-100 text-amber-700 border-amber-200';
case 'unhealthy':
default:
return 'bg-red-100 text-red-700 border-red-200';
}
}

View file

@ -20,6 +20,7 @@ import {
import { useSecurityEvents, useSecurityWebSocket } from '@/hooks/useSecuritySettings';
import { SecurityEvent, EventFilters } from '@/types/security';
import { clientLogger } from '@/lib/client-logger';
import { securityEventSeverityColor } from '@/components/primitives/statusColors';
const SecurityEvents: React.FC = () => {
const [filters, setFilters] = useState<EventFilters>({});
@ -52,21 +53,6 @@ const SecurityEvents: React.FC = () => {
return staticEvents;
}, [eventsData, liveEvents, liveUpdates, pageSize]);
// Severity color mapping
const getSeverityColor = (severity: string) => {
switch (severity) {
case 'critical':
return 'text-red-600 bg-red-50 border-red-200';
case 'error':
return 'text-red-600 bg-red-50 border-red-200';
case 'warn':
return 'text-yellow-600 bg-yellow-50 border-yellow-200';
case 'info':
return 'text-blue-600 bg-blue-50 border-blue-200';
default:
return 'text-gray-600 bg-gray-50 border-gray-200';
}
};
const getSeverityIcon = (severity: string) => {
switch (severity) {
@ -415,7 +401,7 @@ const SecurityEvents: React.FC = () => {
onClick={() => setSelectedEvent(event)}
>
<div className="flex items-start gap-4">
<div className={`p-2 rounded-lg border ${getSeverityColor(event.severity)}`}>
<div className={`p-2 rounded-lg border ${securityEventSeverityColor(event.severity)}`}>
{getSeverityIcon(event.severity)}
</div>
@ -514,7 +500,7 @@ const SecurityEvents: React.FC = () => {
<div className="space-y-4">
{/* Event Header */}
<div className="flex items-start gap-4 pb-4 border-b">
<div className={`p-2 rounded-lg border ${getSeverityColor(selectedEvent.severity)}`}>
<div className={`p-2 rounded-lg border ${securityEventSeverityColor(selectedEvent.severity)}`}>
{getSeverityIcon(selectedEvent.severity)}
</div>
<div className="flex-1">

View file

@ -12,6 +12,7 @@ import {
Info
} from 'lucide-react';
import { SecurityStatusCardProps } from '@/types/security';
import { securityHealthColor } from '@/components/primitives/statusColors';
const SecurityStatusCard: React.FC<SecurityStatusCardProps> = ({
status,
@ -31,18 +32,6 @@ const SecurityStatusCard: React.FC<SecurityStatusCardProps> = ({
}
};
const getStatusColor = () => {
switch (status.overall) {
case 'healthy':
return 'bg-green-50 border-green-200 text-green-900';
case 'warning':
return 'bg-yellow-50 border-yellow-200 text-yellow-900';
case 'critical':
return 'bg-red-50 border-red-200 text-red-900';
default:
return 'bg-gray-50 border-gray-200 text-gray-900';
}
};
const getStatusText = () => {
switch (status.overall) {
@ -75,7 +64,7 @@ const SecurityStatusCard: React.FC<SecurityStatusCardProps> = ({
<div className="bg-white border border-gray-200 rounded-lg p-6">
<div className="flex items-start justify-between mb-6">
<div className="flex items-start gap-4">
<div className={`p-3 rounded-lg border ${getStatusColor()}`}>
<div className={`p-3 rounded-lg border ${securityHealthColor(status.overall)}`}>
{getStatusIcon()}
</div>
<div>
@ -177,7 +166,7 @@ const SecurityStatusCard: React.FC<SecurityStatusCardProps> = ({
{/* Alert Section */}
{status.overall !== 'healthy' && (
<div className={`alert ${getStatusColor()}`}>
<div className={`alert ${securityHealthColor(status.overall)}`}>
<div className="flex items-start gap-3">
{status.overall === 'warning' ? (
<AlertTriangle className="w-5 h-5 text-yellow-600 flex-shrink-0 mt-0.5" />

View file

@ -116,64 +116,13 @@ export const versionCompare = (v1: string, v2: string): number => {
return 0;
};
// Status and severity utilities
export const getStatusColor = (status: string): string => {
switch (status) {
case 'online':
return 'text-success-600 bg-success-100';
case 'offline':
return 'text-danger-600 bg-danger-100';
case 'pending':
return 'text-warning-600 bg-warning-100';
case 'checking_dependencies':
return 'text-info-500 bg-info-100';
case 'pending_dependencies':
return 'text-orange-600 bg-orange-100';
case 'approved':
return 'text-info-600 bg-info-100';
case 'installing':
return 'text-indigo-600 bg-indigo-100';
case 'installed':
return 'text-success-600 bg-success-100';
case 'failed':
return 'text-danger-600 bg-danger-100';
case 'ignored':
return 'text-gray-500 bg-gray-100';
// Docker image lifecycle (docker_images.status)
case 'up-to-date':
return 'text-success-600 bg-success-100';
case 'update-available':
return 'text-info-600 bg-info-100';
case 'update-approved':
return 'text-orange-600 bg-orange-100';
case 'update-scheduled':
return 'text-purple-600 bg-purple-100';
case 'update-installing':
return 'text-indigo-600 bg-indigo-100';
case 'update-failed':
return 'text-danger-600 bg-danger-100';
default:
return 'text-gray-600 bg-gray-100';
}
};
// Status and severity utilities — thin re-exports delegating to the canonical
// truth table in components/primitives/statusColors.ts. New code should import
// from there directly; these shims keep existing callers working unchanged.
import { packageStatusColor, packageSeverityColor } from '@/components/primitives/statusColors';
export const getSeverityColor = (severity: string): string => {
switch (severity) {
case 'critical':
return 'text-danger-600 bg-danger-100';
case 'important':
case 'high':
return 'text-warning-600 bg-warning-100';
case 'moderate':
case 'medium':
return 'text-info-600 bg-info-100';
case 'low':
case 'none':
return 'text-gray-600 bg-gray-100';
default:
return 'text-gray-600 bg-gray-100';
}
};
export const getStatusColor = packageStatusColor;
export const getSeverityColor = packageSeverityColor;
export const getPackageTypeIcon = (type: string): string => {
switch (type) {

View file

@ -33,6 +33,7 @@ import {
formatRelativeTime,
} from '@/lib/utils';
import { cn } from '@/lib/utils';
import { lifecycleHistoryStatusColor, commandStatusInlineColor } from '@/components/primitives/statusColors';
import { StatusBadge, SeverityBadge } from '@/components/primitives';
import toast from 'react-hot-toast';
import DependencyClosureTree from '@/components/DependencyClosureTree';
@ -531,12 +532,7 @@ const AgentDetailPane: React.FC<AgentDetailPaneProps> = ({
</h2>
<ul className="divide-y divide-gray-100">
{lifecycleData.history.map((h: any) => {
const statusColors: Record<string, string> = {
installed: 'bg-green-50 text-green-700 border-green-200',
failed: 'bg-red-50 text-red-700 border-red-200',
rollback: 'bg-amber-50 text-amber-700 border-amber-200',
};
const cls = statusColors[h.update_status] || 'bg-gray-50 text-gray-600 border-gray-200';
const cls = lifecycleHistoryStatusColor(h.update_status);
const reason =
h.update_status === 'failed'
? h.failure_reason || h.metadata?.failure_reason
@ -589,14 +585,7 @@ const AgentDetailPane: React.FC<AgentDetailPaneProps> = ({
)}
<span className="font-mono text-gray-700 flex-shrink-0">{cmd.action}</span>
<span
className={cn(
'text-[10px] border rounded px-1 py-0.5 flex-shrink-0',
cmd.status === 'completed'
? 'bg-green-50 text-green-700 border-green-200'
: cmd.status === 'failed'
? 'bg-red-50 text-red-700 border-red-200'
: 'bg-gray-50 text-gray-600 border-gray-200'
)}
className={cn('text-[10px] border rounded px-1 py-0.5 flex-shrink-0', commandStatusInlineColor(cmd.status))}
>
{cmd.status}
</span>

View file

@ -22,6 +22,7 @@ import {
} from '../hooks/useRegistrationTokens';
import { RegistrationToken, CreateRegistrationTokenRequest } from '@/types';
import { formatDateTime } from '@/lib/utils';
import { tokenStatusColor } from '@/components/primitives/statusColors';
const TokenManagement: React.FC = () => {
const navigate = useNavigate();
@ -98,13 +99,6 @@ const TokenManagement: React.FC = () => {
};
const getStatusColor = (token: RegistrationToken) => {
if (token.status === 'revoked') return 'text-gray-500';
if (token.status === 'expired') return 'text-red-600';
if (token.status === 'used') return 'text-yellow-600';
if (token.status === 'active') return 'text-green-600';
return 'text-gray-500';
};
const getStatusText = (token: RegistrationToken) => {
if (token.status === 'revoked') return 'Revoked';
@ -453,7 +447,7 @@ const TokenManagement: React.FC = () => {
<div className="text-sm font-medium text-gray-900">{token.label}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className={`badge badge-lg ${getStatusColor(token)}`}>
<div className={`badge badge-lg ${tokenStatusColor(token.status)}`}>
{getStatusText(token)}
</div>
</td>

View file

@ -32,6 +32,7 @@ import { useRecentCommands } from '@/hooks/useCommands';
import type { UpdatePackage } from '@/types';
import { getPackageTypeIcon, formatBytes, formatRelativeTime } from '@/lib/utils';
import { cn } from '@/lib/utils';
import { lifecycleHistoryStatusColor, commandStatusInlineColor } from '@/components/primitives/statusColors';
import toast from 'react-hot-toast';
import { updateApi } from '@/lib/api';
import DependencyClosureTree from '@/components/DependencyClosureTree';
@ -852,12 +853,7 @@ const Updates: React.FC = () => {
</h2>
<ul className="divide-y divide-gray-100">
{lifecycleData.history.map((h: any) => {
const statusColors: Record<string, string> = {
installed: 'bg-green-50 text-green-700 border-green-200',
failed: 'bg-red-50 text-red-700 border-red-200',
rollback: 'bg-amber-50 text-amber-700 border-amber-200',
};
const cls = statusColors[h.update_status] || 'bg-gray-50 text-gray-600 border-gray-200';
const cls = lifecycleHistoryStatusColor(h.update_status);
const reason = h.update_status === 'failed'
? (h.failure_reason || h.metadata?.failure_reason)
: null;
@ -900,14 +896,9 @@ const Updates: React.FC = () => {
) : (
<ul className="space-y-2">
{updateCommands.map((cmd: any) => {
const statusColor =
cmd.status === 'completed' ? 'bg-green-50 text-green-700 border-green-200' :
cmd.status === 'failed' || cmd.status === 'timed_out' ? 'bg-red-50 text-red-700 border-red-200' :
cmd.status === 'cancelled' ? 'bg-gray-50 text-gray-700 border-gray-200' :
'bg-blue-50 text-blue-700 border-blue-200';
return (
<li key={cmd.id} className="flex items-center gap-3 py-2 px-3 bg-white border border-gray-100 rounded">
<span className={cn('text-xs font-medium border rounded px-2 py-0.5', statusColor)}>
<span className={cn('text-xs font-medium border rounded px-2 py-0.5', commandStatusInlineColor(cmd.status))}>
{cmd.status}
</span>
<span className="text-sm text-gray-900 font-mono truncate flex-1">