@runloop/rl-cli 1.7.0 → 1.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -5,7 +5,6 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
5
5
|
*/
|
|
6
6
|
import React from "react";
|
|
7
7
|
import { Box, Text, useInput } from "ink";
|
|
8
|
-
import Spinner from "ink-spinner";
|
|
9
8
|
import figures from "figures";
|
|
10
9
|
import { Breadcrumb } from "./Breadcrumb.js";
|
|
11
10
|
import { NavigationTips } from "./NavigationTips.js";
|
|
@@ -14,6 +13,21 @@ import { useViewportHeight } from "../hooks/useViewportHeight.js";
|
|
|
14
13
|
import { useExitOnCtrlC } from "../hooks/useExitOnCtrlC.js";
|
|
15
14
|
import { parseAnyLogEntry } from "../utils/logFormatter.js";
|
|
16
15
|
import { getDevboxLogs } from "../services/devboxService.js";
|
|
16
|
+
// Color maps - defined outside component to avoid recreation on every render
|
|
17
|
+
const levelColorMap = {
|
|
18
|
+
red: colors.error,
|
|
19
|
+
yellow: colors.warning,
|
|
20
|
+
blue: colors.primary,
|
|
21
|
+
gray: colors.textDim,
|
|
22
|
+
};
|
|
23
|
+
const sourceColorMap = {
|
|
24
|
+
magenta: "#d33682",
|
|
25
|
+
cyan: colors.info,
|
|
26
|
+
green: colors.success,
|
|
27
|
+
yellow: colors.warning,
|
|
28
|
+
gray: colors.textDim,
|
|
29
|
+
white: colors.text,
|
|
30
|
+
};
|
|
17
31
|
export const StreamingLogsViewer = ({ devboxId, breadcrumbItems = [{ label: "Logs", active: true }], onBack, }) => {
|
|
18
32
|
const [logs, setLogs] = React.useState([]);
|
|
19
33
|
const [loading, setLoading] = React.useState(true);
|
|
@@ -25,15 +39,40 @@ export const StreamingLogsViewer = ({ devboxId, breadcrumbItems = [{ label: "Log
|
|
|
25
39
|
const [isPolling, setIsPolling] = React.useState(true);
|
|
26
40
|
// Refs for cleanup
|
|
27
41
|
const pollIntervalRef = React.useRef(null);
|
|
28
|
-
// Calculate viewport
|
|
29
|
-
const logsViewport = useViewportHeight({ overhead:
|
|
42
|
+
// Calculate viewport - overhead increased to reduce overdraw/flashing
|
|
43
|
+
const logsViewport = useViewportHeight({ overhead: 11, minHeight: 10 });
|
|
30
44
|
// Handle Ctrl+C
|
|
31
45
|
useExitOnCtrlC();
|
|
32
|
-
// Fetch logs function
|
|
46
|
+
// Fetch logs function - only update state if logs actually changed
|
|
33
47
|
const fetchLogs = React.useCallback(async () => {
|
|
34
48
|
try {
|
|
35
49
|
const newLogs = await getDevboxLogs(devboxId);
|
|
36
|
-
|
|
50
|
+
// Only update logs state if the logs have actually changed
|
|
51
|
+
// This prevents unnecessary re-renders that cause flashing in non-tmux terminals
|
|
52
|
+
setLogs((prevLogs) => {
|
|
53
|
+
// Quick length check first
|
|
54
|
+
if (prevLogs.length !== newLogs.length) {
|
|
55
|
+
return newLogs;
|
|
56
|
+
}
|
|
57
|
+
// If same length, check if last log entry is different (most common case for streaming)
|
|
58
|
+
if (newLogs.length > 0) {
|
|
59
|
+
const prevLast = prevLogs[prevLogs.length - 1];
|
|
60
|
+
const newLast = newLogs[newLogs.length - 1];
|
|
61
|
+
// Compare by timestamp and message for efficiency
|
|
62
|
+
if (prevLast &&
|
|
63
|
+
newLast &&
|
|
64
|
+
"timestamp" in prevLast &&
|
|
65
|
+
"timestamp" in newLast &&
|
|
66
|
+
"message" in prevLast &&
|
|
67
|
+
"message" in newLast &&
|
|
68
|
+
prevLast.timestamp === newLast.timestamp &&
|
|
69
|
+
prevLast.message === newLast.message) {
|
|
70
|
+
// Logs haven't changed, return previous state to avoid re-render
|
|
71
|
+
return prevLogs;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return newLogs;
|
|
75
|
+
});
|
|
37
76
|
setError(null);
|
|
38
77
|
if (loading)
|
|
39
78
|
setLoading(false);
|
|
@@ -216,57 +255,79 @@ export const StreamingLogsViewer = ({ devboxId, breadcrumbItems = [{ label: "Log
|
|
|
216
255
|
}
|
|
217
256
|
const hasMore = actualScroll + visibleLogs.length < logs.length;
|
|
218
257
|
const hasLess = actualScroll > 0;
|
|
219
|
-
//
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
258
|
+
// Build lines array - always render exactly viewportHeight lines for stable structure
|
|
259
|
+
// This prevents Ink from doing structural redraws that cause flashing
|
|
260
|
+
const renderLines = React.useMemo(() => {
|
|
261
|
+
const lines = [];
|
|
262
|
+
if (loading) {
|
|
263
|
+
// First line shows loading message
|
|
264
|
+
lines.push(_jsx(Box, { width: contentWidth, children: _jsx(Text, { color: colors.textDim, children: "\u25CF Loading logs..." }) }, "loading"));
|
|
265
|
+
}
|
|
266
|
+
else if (error) {
|
|
267
|
+
// First line shows error
|
|
268
|
+
lines.push(_jsx(Box, { width: contentWidth, children: _jsxs(Text, { color: colors.error, children: [figures.cross, " Error: ", error] }) }, "error"));
|
|
269
|
+
}
|
|
270
|
+
else if (logs.length === 0) {
|
|
271
|
+
// First line shows waiting message
|
|
272
|
+
lines.push(_jsx(Box, { width: contentWidth, children: _jsxs(Text, { color: colors.textDim, children: [isPolling ? "● " : "", "Waiting for logs..."] }) }, "waiting"));
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
// Render visible log entries
|
|
276
|
+
for (let i = 0; i < visibleLogs.length; i++) {
|
|
277
|
+
const log = visibleLogs[i];
|
|
278
|
+
const parts = parseAnyLogEntry(log);
|
|
279
|
+
const sanitizedMessage = sanitizeMessage(parts.message);
|
|
280
|
+
const MAX_MESSAGE_LENGTH = 1000;
|
|
281
|
+
const fullMessage = sanitizedMessage.length > MAX_MESSAGE_LENGTH
|
|
282
|
+
? sanitizedMessage.substring(0, MAX_MESSAGE_LENGTH) + "..."
|
|
283
|
+
: sanitizedMessage;
|
|
284
|
+
const cmd = parts.cmd
|
|
285
|
+
? `$ ${parts.cmd.substring(0, 40)}${parts.cmd.length > 40 ? "..." : ""} `
|
|
286
|
+
: "";
|
|
287
|
+
const exitCode = parts.exitCode !== null ? `exit=${parts.exitCode} ` : "";
|
|
288
|
+
const levelColor = levelColorMap[parts.levelColor] || colors.textDim;
|
|
289
|
+
const sourceColor = sourceColorMap[parts.sourceColor] || colors.textDim;
|
|
290
|
+
if (logsWrapMode) {
|
|
291
|
+
lines.push(_jsx(Box, { width: contentWidth, flexDirection: "column", children: _jsxs(Text, { wrap: "wrap", children: [_jsx(Text, { color: colors.textDim, dimColor: true, children: parts.timestamp }), _jsx(Text, { children: " " }), _jsx(Text, { color: levelColor, bold: parts.levelColor === "red", children: parts.level }), _jsx(Text, { children: " " }), _jsxs(Text, { color: sourceColor, children: ["[", parts.source, "]"] }), _jsx(Text, { children: " " }), parts.shellName && (_jsxs(Text, { color: colors.textDim, dimColor: true, children: ["(", parts.shellName, ")", " "] })), cmd && _jsx(Text, { color: colors.info, children: cmd }), _jsx(Text, { children: fullMessage }), exitCode && (_jsxs(Text, { color: parts.exitCode === 0 ? colors.success : colors.error, children: [" ", exitCode] }))] }) }, i));
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
const shellPart = parts.shellName ? `(${parts.shellName}) ` : "";
|
|
295
|
+
const exitPart = exitCode ? ` ${exitCode}` : "";
|
|
296
|
+
const prefix = `${parts.timestamp} ${parts.level} [${parts.source}] ${shellPart}${cmd}`;
|
|
297
|
+
const suffix = exitPart;
|
|
298
|
+
const availableForMessage = contentWidth - prefix.length - suffix.length;
|
|
299
|
+
let displayMessage;
|
|
300
|
+
if (availableForMessage <= 3) {
|
|
301
|
+
displayMessage = "";
|
|
302
|
+
}
|
|
303
|
+
else if (fullMessage.length <= availableForMessage) {
|
|
304
|
+
displayMessage = fullMessage;
|
|
249
305
|
}
|
|
250
306
|
else {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
const prefix = `${parts.timestamp} ${parts.level} [${parts.source}] ${shellPart}${cmd}`;
|
|
254
|
-
const suffix = exitPart;
|
|
255
|
-
const availableForMessage = contentWidth - prefix.length - suffix.length;
|
|
256
|
-
let displayMessage;
|
|
257
|
-
if (availableForMessage <= 3) {
|
|
258
|
-
displayMessage = "";
|
|
259
|
-
}
|
|
260
|
-
else if (fullMessage.length <= availableForMessage) {
|
|
261
|
-
displayMessage = fullMessage;
|
|
262
|
-
}
|
|
263
|
-
else {
|
|
264
|
-
displayMessage =
|
|
265
|
-
fullMessage.substring(0, availableForMessage - 3) + "...";
|
|
266
|
-
}
|
|
267
|
-
return (_jsx(Box, { width: contentWidth, children: _jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { color: colors.textDim, dimColor: true, children: parts.timestamp }), _jsx(Text, { children: " " }), _jsx(Text, { color: levelColor, bold: parts.levelColor === "red", children: parts.level }), _jsx(Text, { children: " " }), _jsxs(Text, { color: sourceColor, children: ["[", parts.source, "]"] }), _jsx(Text, { children: " " }), parts.shellName && (_jsxs(Text, { color: colors.textDim, dimColor: true, children: ["(", parts.shellName, ")", " "] })), cmd && _jsx(Text, { color: colors.info, children: cmd }), _jsx(Text, { children: displayMessage }), exitCode && (_jsxs(Text, { color: parts.exitCode === 0 ? colors.success : colors.error, children: [" ", exitCode] }))] }) }, index));
|
|
307
|
+
displayMessage =
|
|
308
|
+
fullMessage.substring(0, availableForMessage - 3) + "...";
|
|
268
309
|
}
|
|
269
|
-
|
|
310
|
+
lines.push(_jsx(Box, { width: contentWidth, children: _jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { color: colors.textDim, dimColor: true, children: parts.timestamp }), _jsx(Text, { children: " " }), _jsx(Text, { color: levelColor, bold: parts.levelColor === "red", children: parts.level }), _jsx(Text, { children: " " }), _jsxs(Text, { color: sourceColor, children: ["[", parts.source, "]"] }), _jsx(Text, { children: " " }), parts.shellName && (_jsxs(Text, { color: colors.textDim, dimColor: true, children: ["(", parts.shellName, ")", " "] })), cmd && _jsx(Text, { color: colors.info, children: cmd }), _jsx(Text, { children: displayMessage }), exitCode && (_jsxs(Text, { color: parts.exitCode === 0 ? colors.success : colors.error, children: [" ", exitCode] }))] }) }, i));
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
// Pad with empty lines to maintain consistent structure
|
|
315
|
+
// This prevents Ink from doing structural redraws
|
|
316
|
+
while (lines.length < viewportHeight) {
|
|
317
|
+
lines.push(_jsx(Box, { width: contentWidth, children: _jsx(Text, { children: " " }) }, `pad-${lines.length}`));
|
|
318
|
+
}
|
|
319
|
+
return lines;
|
|
320
|
+
}, [
|
|
321
|
+
loading,
|
|
322
|
+
error,
|
|
323
|
+
logs.length,
|
|
324
|
+
visibleLogs,
|
|
325
|
+
isPolling,
|
|
326
|
+
logsWrapMode,
|
|
327
|
+
contentWidth,
|
|
328
|
+
viewportHeight,
|
|
329
|
+
]);
|
|
330
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: breadcrumbItems }), _jsx(Box, { flexDirection: "column", borderStyle: "round", borderColor: colors.border, paddingX: 1, height: viewportHeight + 2, children: renderLines }), _jsxs(Box, { marginTop: 1, paddingX: 1, children: [_jsxs(Text, { color: colors.primary, bold: true, children: [figures.hamburger, " ", logs.length] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "logs"] }), logs.length > 0 && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [actualScroll + 1, "-", Math.min(actualScroll + visibleLogs.length, logs.length), " of", " ", logs.length] }), hasLess && _jsxs(Text, { color: colors.primary, children: [" ", figures.arrowUp] }), hasMore && (_jsxs(Text, { color: colors.primary, children: [" ", figures.arrowDown] }))] })), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), isPolling ? (_jsx(Text, { color: colors.success, children: "\u25CF Live" })) : (_jsx(Text, { color: colors.textDim, children: "\u25CB Paused" })), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsx(Text, { color: logsWrapMode ? colors.success : colors.textDim, bold: logsWrapMode, children: logsWrapMode ? "Wrap: ON" : "Wrap: OFF" }), copyStatus && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsx(Text, { color: colors.success, bold: true, children: copyStatus })] }))] }), _jsx(NavigationTips, { showArrows: true, tips: [
|
|
270
331
|
{ key: "g/G", label: "Top/Bottom" },
|
|
271
332
|
{ key: "p", label: isPolling ? "Pause" : "Resume" },
|
|
272
333
|
{ key: "w", label: "Wrap" },
|