forked from aws/agentcore-cli
-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathLogPanel.tsx
More file actions
184 lines (164 loc) · 5.32 KB
/
Copy pathLogPanel.tsx
File metadata and controls
184 lines (164 loc) · 5.32 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
import { Box, Text, useInput } from 'ink';
import { useMemo, useState } from 'react';
export interface LogEntry {
level: 'info' | 'warn' | 'error' | 'system' | 'response';
message: string;
}
interface LogPanelProps {
logs: LogEntry[];
maxLines?: number;
/**
* If true, only show important logs (system, response, simple errors).
* Hides JSON debug logs. Default true.
*/
minimal?: boolean;
/**
* Whether this component should handle scroll input. Default true.
*/
isActive?: boolean;
}
const LOG_COLORS: Record<LogEntry['level'], string | undefined> = {
error: 'red',
warn: 'yellow',
system: 'cyan',
info: 'gray',
response: 'green',
};
/**
* Check if a log message is a JSON debug log that should be hidden in minimal mode.
*/
function isJsonDebugLog(log: LogEntry): boolean {
// Response and system logs are always shown
if (log.level === 'response' || log.level === 'system') {
return false;
}
// Check if message contains JSON (starts with { after trimming)
const trimmed = log.message.trim();
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
return true;
}
// Check for JSON embedded in the message (common pattern: "level": "INFO")
if (trimmed.includes('"timestamp"') || trimmed.includes('"level"')) {
return true;
}
return false;
}
/**
* Filter logs to only show important ones for the minimal TUI view.
* Only shows: system messages, responses, and warnings.
* Hides: all INFO logs, JSON debug logs.
*/
function filterForMinimalView(logs: LogEntry[]): LogEntry[] {
return logs.filter(log => {
// Always show system and response logs
if (log.level === 'system' || log.level === 'response') {
return true;
}
// Show warn logs (but not if they're JSON debug)
if (log.level === 'warn' && !isJsonDebugLog(log)) {
return true;
}
// Show error logs only if they're not JSON debug logs
if (log.level === 'error' && !isJsonDebugLog(log)) {
return true;
}
// Hide all info logs and everything else
return false;
});
}
/**
* Renders log entries with scrolling support.
* Auto-scrolls to bottom when new logs arrive, allows manual scroll with ↑↓.
*/
export function LogPanel({ logs, maxLines = 15, minimal = true, isActive = true }: LogPanelProps) {
const [scrollOffset, setScrollOffset] = useState(0);
const [userScrolled, setUserScrolled] = useState(false);
// Filter logs for minimal view
const filteredLogs = minimal ? filterForMinimalView(logs) : logs;
const totalLogs = filteredLogs.length;
const maxScroll = Math.max(0, totalLogs - maxLines);
const needsScroll = totalLogs > maxLines;
// Compute effective scroll offset - auto-scroll to bottom unless user scrolled up
const effectiveScrollOffset = useMemo(() => {
if (!userScrolled) {
return maxScroll;
}
return Math.min(scrollOffset, maxScroll);
}, [userScrolled, scrollOffset, maxScroll]);
// Handle scroll input
useInput(
(input, key) => {
if (!needsScroll) return;
if (key.upArrow || input === 'k') {
setUserScrolled(true);
setScrollOffset(prev => Math.max(0, prev - 1));
} else if (key.downArrow || input === 'j') {
setScrollOffset(prev => {
const next = Math.min(maxScroll, prev + 1);
// Reset user scroll flag when reaching bottom
if (next >= maxScroll) {
setUserScrolled(false);
}
return next;
});
} else if (key.pageUp) {
setUserScrolled(true);
setScrollOffset(prev => Math.max(0, prev - maxLines));
} else if (key.pageDown) {
setScrollOffset(prev => {
const next = Math.min(maxScroll, prev + maxLines);
if (next >= maxScroll) {
setUserScrolled(false);
}
return next;
});
}
},
{ isActive: isActive && needsScroll }
);
// Calculate visible window
const visibleLogs = filteredLogs.slice(effectiveScrollOffset, effectiveScrollOffset + maxLines);
const hasLogs = visibleLogs.length > 0;
if (!hasLogs) {
return <Text dimColor>No output yet</Text>;
}
return (
<Box flexDirection="column">
{needsScroll && effectiveScrollOffset > 0 && (
<Box>
<Text dimColor>↑ {effectiveScrollOffset} more above</Text>
</Box>
)}
{visibleLogs.map((log, idx) => {
if (log.level === 'response') {
return (
<Box key={effectiveScrollOffset + idx} flexDirection="column" marginTop={1}>
<Text color="green" bold>
─── Response ───
</Text>
<Text wrap="wrap">{log.message}</Text>
</Box>
);
}
return (
<Box key={effectiveScrollOffset + idx} flexDirection="row">
{log.level !== 'system' && <Text color={LOG_COLORS[log.level]}>{log.level.toUpperCase().padEnd(6)} </Text>}
<Text color={LOG_COLORS[log.level]} wrap="wrap">
{log.message}
</Text>
</Box>
);
})}
{needsScroll && effectiveScrollOffset < maxScroll && (
<Box>
<Text dimColor>↓ {maxScroll - effectiveScrollOffset} more below</Text>
</Box>
)}
{needsScroll && (
<Box marginTop={1}>
<Text dimColor>↑↓ scroll</Text>
</Box>
)}
</Box>
);
}