Files
mcpctl/src/cli/src/commands/console/components/timeline.tsx
Michal 5d859ca7d8 feat: audit console TUI, system prompt management, and CLI improvements
Audit Console Phase 1: tool_call_trace emission from mcplocal router,
session_bind/rbac_decision event kinds, GET /audit/sessions endpoint,
full Ink TUI with session sidebar, event timeline, and detail view
(mcpctl console --audit).

System prompts: move 6 hardcoded LLM prompts to mcpctl-system project
with extensible ResourceRuleRegistry validation framework, template
variable enforcement ({{maxTokens}}, {{pageCount}}), and delete-resets-
to-default behavior. All consumers fetch via SystemPromptFetcher with
hardcoded fallbacks.

CLI: -p shorthand for --project across get/create/delete/config commands,
console auto-scroll improvements, shell completions regenerated.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 23:50:54 +00:00

96 lines
3.5 KiB
TypeScript

/**
* Unified timeline — renders all events (interactive, observed)
* with a lane-colored gutter, windowed rendering, and auto-scroll.
*/
import { Box, Text } from 'ink';
import type { TimelineEvent, EventLane } from '../unified-types.js';
import { formatTime, formatEventSummary, trunc } from '../format-event.js';
const LANE_COLORS: Record<EventLane, string> = {
interactive: 'green',
observed: 'yellow',
};
const LANE_MARKERS: Record<EventLane, string> = {
interactive: '\u2502',
observed: '\u2502',
};
interface TimelineProps {
events: TimelineEvent[];
height: number;
focusedIdx: number; // -1 = auto-scroll to bottom
showProject: boolean;
}
export function Timeline({ events, height, focusedIdx, showProject }: TimelineProps) {
const maxVisible = Math.max(1, height - 2); // header + spacing
let startIdx: number;
if (focusedIdx >= 0) {
startIdx = Math.max(0, Math.min(focusedIdx - Math.floor(maxVisible / 2), events.length - maxVisible));
} else {
startIdx = Math.max(0, events.length - maxVisible);
}
const visible = events.slice(startIdx, startIdx + maxVisible);
return (
<Box flexDirection="column" flexGrow={1} paddingLeft={1}>
<Text bold>
Timeline <Text dimColor>({events.length} events{focusedIdx >= 0 ? ` \u00B7 #${focusedIdx + 1}` : ' \u00B7 following'})</Text>
</Text>
{visible.length === 0 && (
<Box marginTop={1}>
<Text dimColor>{' waiting for traffic\u2026'}</Text>
</Box>
)}
{visible.map((event, vi) => {
const absIdx = startIdx + vi;
const isFocused = absIdx === focusedIdx;
const { arrow, color, label, detail, detailColor } = formatEventSummary(
event.eventType,
event.method,
event.body,
event.upstreamName,
event.durationMs,
);
const isLifecycle = event.eventType === 'session_created' || event.eventType === 'session_closed';
const laneColor = LANE_COLORS[event.lane];
const laneMarker = LANE_MARKERS[event.lane];
const focusMarker = isFocused ? '\u25B8' : ' ';
const hasCorrelation = event.correlationId !== undefined;
if (isLifecycle) {
return (
<Text key={event.id} wrap="truncate">
<Text color={laneColor}>{laneMarker}</Text>
<Text color={isFocused ? 'cyan' : undefined}>{focusMarker}</Text>
<Text dimColor>{formatTime(event.timestamp)} </Text>
<Text color={color} bold>{arrow} {label}</Text>
{showProject && <Text color="gray"> [{trunc(event.projectName, 12)}]</Text>}
<Text dimColor> {event.sessionId.slice(0, 8)}</Text>
</Text>
);
}
const isUpstream = event.eventType.startsWith('upstream_');
return (
<Text key={event.id} wrap="truncate">
<Text color={laneColor}>{laneMarker}</Text>
<Text color={isFocused ? 'cyan' : undefined}>{focusMarker}</Text>
<Text dimColor>{formatTime(event.timestamp)} </Text>
{showProject && <Text color="gray">[{trunc(event.projectName, 12)}] </Text>}
<Text color={color}>{arrow} </Text>
<Text bold={!isUpstream} color={color}>{label}</Text>
{detail ? (
<Text color={detailColor} dimColor={!detailColor}> {detail}</Text>
) : null}
{hasCorrelation && <Text dimColor>{' \u26D3'}</Text>}
</Text>
);
})}
</Box>
);
}