oncall-cli 2.0.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.
- package/LICENSE +21 -0
- package/README.md +172 -0
- package/bin/oncall.js +303 -0
- package/dist/api.d.ts +1 -0
- package/dist/api.js +23 -0
- package/dist/api.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +91 -0
- package/dist/cli.js.map +1 -0
- package/dist/code_hierarchy.d.ts +4 -0
- package/dist/code_hierarchy.js +92 -0
- package/dist/code_hierarchy.js.map +1 -0
- package/dist/config.d.ts +9 -0
- package/dist/config.js +12 -0
- package/dist/config.js.map +1 -0
- package/dist/helpers/cli-helpers.d.ts +22 -0
- package/dist/helpers/cli-helpers.js +261 -0
- package/dist/helpers/cli-helpers.js.map +1 -0
- package/dist/helpers/config-helpers.js +161 -0
- package/dist/helpers/ripgrep-tool.d.ts +15 -0
- package/dist/helpers/ripgrep-tool.js +110 -0
- package/dist/helpers/ripgrep-tool.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +545 -0
- package/dist/index.js.map +1 -0
- package/dist/logsManager.d.ts +31 -0
- package/dist/logsManager.js +90 -0
- package/dist/logsManager.js.map +1 -0
- package/dist/tools/ripgrep.d.ts +15 -0
- package/dist/tools/ripgrep.js +110 -0
- package/dist/tools/ripgrep.js.map +1 -0
- package/dist/ui-graph.d.ts +1 -0
- package/dist/ui-graph.js +125 -0
- package/dist/ui-graph.js.map +1 -0
- package/dist/useWebSocket.d.ts +21 -0
- package/dist/useWebSocket.js +411 -0
- package/dist/useWebSocket.js.map +1 -0
- package/dist/utils/version-check.d.ts +2 -0
- package/dist/utils/version-check.js +124 -0
- package/dist/utils/version-check.js.map +1 -0
- package/dist/utils.d.ts +1 -0
- package/dist/utils.js +22 -0
- package/dist/utils.js.map +1 -0
- package/dist/websocket-server.d.ts +24 -0
- package/dist/websocket-server.js +221 -0
- package/dist/websocket-server.js.map +1 -0
- package/package.json +46 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { RipGrep } from "ripgrep-node";
|
|
2
|
+
export async function ripgrepSearch(query, options = {}) {
|
|
3
|
+
const defaultExcludePatterns = [
|
|
4
|
+
"node_modules",
|
|
5
|
+
".git",
|
|
6
|
+
"dist",
|
|
7
|
+
"build",
|
|
8
|
+
".next",
|
|
9
|
+
".cache",
|
|
10
|
+
"coverage",
|
|
11
|
+
".nyc_output",
|
|
12
|
+
".vscode",
|
|
13
|
+
".idea",
|
|
14
|
+
"*.log",
|
|
15
|
+
"*.lock",
|
|
16
|
+
"package-lock.json",
|
|
17
|
+
"yarn.lock",
|
|
18
|
+
"pnpm-lock.yaml",
|
|
19
|
+
".env",
|
|
20
|
+
".env.*",
|
|
21
|
+
"*.min.js",
|
|
22
|
+
"*.min.css",
|
|
23
|
+
".DS_Store",
|
|
24
|
+
"Thumbs.db",
|
|
25
|
+
];
|
|
26
|
+
const { maxResults = 20, caseSensitive = false, fileTypes = [], excludePatterns = defaultExcludePatterns, workingDirectory, } = options;
|
|
27
|
+
if (!query || query.trim().length === 0) {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
const searchDir = workingDirectory || (typeof process !== "undefined" ? process.cwd() : undefined);
|
|
31
|
+
if (!searchDir) {
|
|
32
|
+
console.error("[ripgrep] workingDirectory is required for client-side usage");
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
let rg = new RipGrep(query, searchDir);
|
|
37
|
+
rg.withFilename().lineNumber();
|
|
38
|
+
if (!caseSensitive) {
|
|
39
|
+
rg.ignoreCase();
|
|
40
|
+
}
|
|
41
|
+
if (fileTypes.length > 0) {
|
|
42
|
+
for (const ext of fileTypes) {
|
|
43
|
+
rg.glob(`*.${ext}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
for (const pattern of excludePatterns) {
|
|
47
|
+
const hasFileExtension = /\.(json|lock|yaml|js|css|log)$/.test(pattern) ||
|
|
48
|
+
pattern.startsWith("*.") ||
|
|
49
|
+
pattern === ".env" ||
|
|
50
|
+
pattern.startsWith(".env.") ||
|
|
51
|
+
pattern === ".DS_Store" ||
|
|
52
|
+
pattern === "Thumbs.db";
|
|
53
|
+
const isFilePattern = pattern.includes("*") || hasFileExtension;
|
|
54
|
+
const excludePattern = isFilePattern ? `!${pattern}` : `!${pattern}/**`;
|
|
55
|
+
rg.glob(excludePattern);
|
|
56
|
+
}
|
|
57
|
+
const output = await rg.run().asString();
|
|
58
|
+
if (!output || output.trim().length === 0) {
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
const lines = output.trim().split("\n");
|
|
62
|
+
const results = [];
|
|
63
|
+
for (const line of lines.slice(0, maxResults)) {
|
|
64
|
+
const match = line.match(/^(.+?):(\d+):(.+)$/);
|
|
65
|
+
if (match) {
|
|
66
|
+
const [, filePath, lineNum, content] = match;
|
|
67
|
+
const lineNumber = lineNum ? parseInt(lineNum, 10) : null;
|
|
68
|
+
const preview = content ? content.trim().slice(0, 200) : undefined;
|
|
69
|
+
results.push({
|
|
70
|
+
filePath: filePath.trim(),
|
|
71
|
+
line: lineNumber ?? null,
|
|
72
|
+
preview: preview || undefined,
|
|
73
|
+
score: 1.0,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return results;
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
if (error.message?.includes("No matches") ||
|
|
81
|
+
error.message?.includes("not found") ||
|
|
82
|
+
error.code === 1) {
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
console.error("[ripgrep] Error executing ripgrep:", error.message);
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
export async function ripgrepSearchMultiple(queries, options = {}) {
|
|
90
|
+
const allResults = [];
|
|
91
|
+
for (const query of queries) {
|
|
92
|
+
const results = await ripgrepSearch(query, {
|
|
93
|
+
...options,
|
|
94
|
+
maxResults: Math.ceil((options.maxResults || 20) / queries.length),
|
|
95
|
+
});
|
|
96
|
+
allResults.push(...results);
|
|
97
|
+
}
|
|
98
|
+
// Deduplicate by filePath:line
|
|
99
|
+
const seen = new Set();
|
|
100
|
+
const unique = [];
|
|
101
|
+
for (const result of allResults) {
|
|
102
|
+
const key = `${result.filePath}:${result.line}`;
|
|
103
|
+
if (!seen.has(key)) {
|
|
104
|
+
seen.add(key);
|
|
105
|
+
unique.push(result);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return unique.slice(0, options.maxResults || 20);
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=ripgrep-tool.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ripgrep-tool.js","sourceRoot":"","sources":["../../helpers/ripgrep-tool.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,545 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useState, useMemo, useRef, useCallback, } from "react";
|
|
4
|
+
import { useInput, useApp, render, Box, Text, useStdout } from "ink";
|
|
5
|
+
import stringWidth from "string-width";
|
|
6
|
+
import sliceAnsi from "slice-ansi";
|
|
7
|
+
import { spawn as ptySpawn } from "node-pty";
|
|
8
|
+
import TextInput from "ink-text-input";
|
|
9
|
+
import * as dotenv from "dotenv";
|
|
10
|
+
import { marked } from "marked";
|
|
11
|
+
import { markedTerminal } from "marked-terminal";
|
|
12
|
+
import { useWebSocket } from "./useWebSocket.js";
|
|
13
|
+
import { config } from "./config.js";
|
|
14
|
+
import Spinner from "ink-spinner";
|
|
15
|
+
import { HumanMessage } from "langchain";
|
|
16
|
+
import { loadProjectMetadata, fetchProjectsFromCluster } from "./helpers/cli-helpers.js";
|
|
17
|
+
import logsManager from "./logsManager.js";
|
|
18
|
+
dotenv.config();
|
|
19
|
+
if (typeof process !== "undefined" && process.on) {
|
|
20
|
+
process.on("SIGINT", () => {
|
|
21
|
+
console.log("\nCtrl+C detected! Exiting...");
|
|
22
|
+
process.exit();
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
marked.use(markedTerminal({
|
|
26
|
+
reflowText: false,
|
|
27
|
+
showSectionPrefix: false,
|
|
28
|
+
unescape: false,
|
|
29
|
+
}));
|
|
30
|
+
// Override just the 'text' renderer to handle inline tokens:
|
|
31
|
+
marked.use({
|
|
32
|
+
renderer: {
|
|
33
|
+
text(tokenOrString) {
|
|
34
|
+
if (typeof tokenOrString === "object" && tokenOrString?.tokens) {
|
|
35
|
+
// @ts-ignore - 'this' is the renderer context with a parser
|
|
36
|
+
return this.parser.parseInline(tokenOrString.tokens);
|
|
37
|
+
}
|
|
38
|
+
return typeof tokenOrString === "string"
|
|
39
|
+
? tokenOrString
|
|
40
|
+
: tokenOrString?.text ?? "";
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
//get last 50 lines of logs
|
|
45
|
+
function getLast50Lines(str) {
|
|
46
|
+
const lines = str.split("\n");
|
|
47
|
+
return lines.slice(-50).join("\n");
|
|
48
|
+
}
|
|
49
|
+
// Helper function to wrap text to a specific width, accounting for ANSI codes
|
|
50
|
+
function wrapText(text, maxWidth) {
|
|
51
|
+
if (maxWidth <= 0)
|
|
52
|
+
return [text];
|
|
53
|
+
const words = text.split(" ");
|
|
54
|
+
const lines = [];
|
|
55
|
+
let currentLine = "";
|
|
56
|
+
for (const word of words) {
|
|
57
|
+
const testLine = currentLine ? `${currentLine} ${word}` : word;
|
|
58
|
+
const width = stringWidth(testLine);
|
|
59
|
+
if (width <= maxWidth) {
|
|
60
|
+
currentLine = testLine;
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
if (currentLine) {
|
|
64
|
+
lines.push(currentLine);
|
|
65
|
+
}
|
|
66
|
+
currentLine = word;
|
|
67
|
+
// If a single word is too long, it will overflow - keep it as one line
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (currentLine) {
|
|
71
|
+
lines.push(currentLine);
|
|
72
|
+
}
|
|
73
|
+
return lines.length > 0 ? lines : [""];
|
|
74
|
+
}
|
|
75
|
+
//scrollable content component
|
|
76
|
+
const ScrollableContent = ({ lines, maxHeight, isFocused, onScrollChange, scrollOffset, }) => {
|
|
77
|
+
const totalLines = lines.length;
|
|
78
|
+
const visibleLines = Math.max(0, Math.min(maxHeight, totalLines));
|
|
79
|
+
const maxOffset = Math.max(0, totalLines - visibleLines);
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
if (maxOffset === 0) {
|
|
82
|
+
if (scrollOffset !== 0) {
|
|
83
|
+
onScrollChange(0);
|
|
84
|
+
}
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
// Auto-scroll to bottom when not focused
|
|
88
|
+
if (!isFocused) {
|
|
89
|
+
onScrollChange(maxOffset);
|
|
90
|
+
}
|
|
91
|
+
}, [totalLines, maxOffset, isFocused]);
|
|
92
|
+
const effectiveOffset = Math.min(maxOffset, Math.max(0, scrollOffset));
|
|
93
|
+
const displayedLines = lines.slice(effectiveOffset, effectiveOffset + visibleLines);
|
|
94
|
+
let scrollPosition = 0;
|
|
95
|
+
if (totalLines > visibleLines) {
|
|
96
|
+
scrollPosition = Math.floor((effectiveOffset / maxOffset) * 100);
|
|
97
|
+
}
|
|
98
|
+
const scrollBarIndicator = totalLines > visibleLines ? `[${scrollPosition}%]` : "";
|
|
99
|
+
return (_jsxs(Box, { flexDirection: "column", height: maxHeight, overflow: "hidden", children: [displayedLines.map((line) => {
|
|
100
|
+
const rendered = marked.parseInline(line.text);
|
|
101
|
+
return _jsx(Text, { children: rendered }, line.key);
|
|
102
|
+
}), _jsx(Box, { position: "absolute", children: _jsx(Text, { color: "yellowBright", bold: true, children: scrollBarIndicator }) })] }));
|
|
103
|
+
};
|
|
104
|
+
//AI chat
|
|
105
|
+
const ScrollableContentChat = ({ lines, maxHeight, isFocused, onScrollChange, scrollOffset, isLoading, showControlR, }) => {
|
|
106
|
+
const totalLines = lines.length;
|
|
107
|
+
const visibleLines = Math.max(0, Math.min(maxHeight, totalLines));
|
|
108
|
+
const maxOffset = Math.max(0, totalLines - visibleLines);
|
|
109
|
+
const animationRef = useRef(null);
|
|
110
|
+
useEffect(() => {
|
|
111
|
+
if (maxOffset === 0) {
|
|
112
|
+
if (scrollOffset !== 0) {
|
|
113
|
+
onScrollChange(0);
|
|
114
|
+
}
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
// Auto-scroll to bottom when not focused
|
|
118
|
+
if (!isFocused) {
|
|
119
|
+
// Cancel any ongoing animation
|
|
120
|
+
if (animationRef.current) {
|
|
121
|
+
clearInterval(animationRef.current);
|
|
122
|
+
}
|
|
123
|
+
// Smooth scroll animation
|
|
124
|
+
const start = scrollOffset;
|
|
125
|
+
const distance = maxOffset - start;
|
|
126
|
+
const duration = 500; // 500ms animation
|
|
127
|
+
const startTime = Date.now();
|
|
128
|
+
const animate = () => {
|
|
129
|
+
const elapsed = Date.now() - startTime;
|
|
130
|
+
const progress = Math.min(elapsed / duration, 1);
|
|
131
|
+
// Easing function (ease-out)
|
|
132
|
+
const easeOut = 1 - Math.pow(1 - progress, 3);
|
|
133
|
+
const newOffset = Math.round(start + distance * easeOut);
|
|
134
|
+
onScrollChange(newOffset);
|
|
135
|
+
if (progress < 1) {
|
|
136
|
+
animationRef.current = setTimeout(animate, 16); // ~60fps
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
animationRef.current = null;
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
animate();
|
|
143
|
+
}
|
|
144
|
+
return () => {
|
|
145
|
+
if (animationRef.current) {
|
|
146
|
+
clearInterval(animationRef.current);
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
}, [totalLines, maxOffset, isFocused]);
|
|
150
|
+
const effectiveOffset = Math.min(maxOffset, Math.max(0, scrollOffset));
|
|
151
|
+
const displayedLines = lines.slice(effectiveOffset, effectiveOffset + visibleLines);
|
|
152
|
+
let scrollPosition = 0;
|
|
153
|
+
if (totalLines > visibleLines) {
|
|
154
|
+
scrollPosition = Math.floor((effectiveOffset / maxOffset) * 100);
|
|
155
|
+
}
|
|
156
|
+
const scrollBarIndicator = totalLines > visibleLines ? `[${scrollPosition}%]` : "";
|
|
157
|
+
return (_jsxs(Box, { flexDirection: "column", height: maxHeight, overflow: "hidden", children: [displayedLines.map((line) => {
|
|
158
|
+
if (line?.text === "" || line?.text === undefined)
|
|
159
|
+
return null;
|
|
160
|
+
const rendered = marked.parseInline(line?.text);
|
|
161
|
+
return _jsx(Text, { children: rendered }, line.key);
|
|
162
|
+
}), isLoading && (_jsx(_Fragment, { children: _jsxs(Text, { color: "grey", children: [_jsx(Spinner, { type: "dots" }), "\u00A0 Analyzing..."] }) })), showControlR && (_jsxs(Text, { color: "#949494", children: ["\n\n", " If the issue is resolved hit [Ctrl-r] to start new issue. If not continue chatting."] })), _jsx(Box, { position: "absolute", children: _jsx(Text, { color: "yellowBright", bold: true, children: scrollBarIndicator }) })] }));
|
|
163
|
+
};
|
|
164
|
+
//border for the content
|
|
165
|
+
const BorderBox = ({ title, children, isFocused, width, }) => (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: isFocused ? "greenBright" : "gray", paddingX: 1, paddingY: 0, marginRight: 1, width: width, overflow: "hidden", children: [_jsx(Box, { marginBottom: 1, borderBottom: isFocused ? true : undefined, borderBottomColor: isFocused ? "greenBright" : "gray", children: _jsxs(Text, { color: "cyan", bold: isFocused, children: [title, " ", isFocused ? " (FOCUSED)" : ""] }) }), children] }));
|
|
166
|
+
const BorderBoxNoBorder = ({ title, children, isFocused, width, }) => (_jsxs(Box, { flexDirection: "column", borderColor: isFocused ? "greenBright" : "gray", paddingX: 1, paddingY: 0, marginRight: 1, width: width, overflow: "hidden", children: [_jsx(Box, { marginBottom: 1, borderBottom: isFocused ? true : undefined, borderBottomColor: isFocused ? "greenBright" : "gray", children: _jsxs(Text, { color: "cyan", bold: isFocused, children: [title, " ", isFocused ? " (FOCUSED)" : ""] }) }), children] }));
|
|
167
|
+
const ShortcutBadge = ({ label }) => (_jsxs(Text, { backgroundColor: "#1f2937", color: "#f8fafc", bold: true, children: [" ", label, " "] }));
|
|
168
|
+
const ShortcutItem = ({ shortcut, description, showDivider }) => (_jsxs(Box, { alignItems: "center", marginBottom: 0, marginX: 1, children: [showDivider && (_jsxs(Text, { color: "#4b5563", dimColor: true, children: ["\u2502", " "] })), _jsx(ShortcutBadge, { label: shortcut }), _jsx(Text, { color: "#b0b0b0", children: ` ${description}` })] }));
|
|
169
|
+
const ShortcutsFooter = ({ shortcuts, }) => {
|
|
170
|
+
const firstRow = shortcuts.slice(0, 3);
|
|
171
|
+
const secondRow = shortcuts.slice(3);
|
|
172
|
+
return (_jsxs(Box, { marginTop: 1, width: "100%", flexDirection: "column", alignItems: "center", paddingX: 1, children: [_jsx(Text, { color: "#2d3748", children: "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" }), _jsx(Box, { flexDirection: "column", marginTop: 0, children: [firstRow, secondRow]
|
|
173
|
+
.filter((row) => row.length > 0)
|
|
174
|
+
.map((row, rowIndex) => (_jsx(Box, { flexDirection: "row", justifyContent: "center", marginTop: rowIndex === 0 ? 0 : 1, children: row.map((item, index) => (_jsx(ShortcutItem, { shortcut: item.shortcut, description: item.description, showDivider: index !== 0 }, `${item.shortcut}-${item.description}`))) }, `shortcut-row-${rowIndex}`))) })] }));
|
|
175
|
+
};
|
|
176
|
+
const getShortcutsForMode = (mode) => {
|
|
177
|
+
const ctrlDAction = mode === "COPY"
|
|
178
|
+
? "Expand Logs"
|
|
179
|
+
: mode === "LOGS"
|
|
180
|
+
? "Collapse Logs"
|
|
181
|
+
: "Toggle chat";
|
|
182
|
+
return [
|
|
183
|
+
{ shortcut: "[Tab]", description: "Switch Focus laude" },
|
|
184
|
+
{ shortcut: "[ ⬆ / ⬇ ]", description: "Scroll (Keyboard Only)" },
|
|
185
|
+
{ shortcut: "[Enter]", description: "Send" },
|
|
186
|
+
{ shortcut: "[Ctrl+D]", description: ctrlDAction },
|
|
187
|
+
{ shortcut: "[Ctrl+C]", description: "Exit" },
|
|
188
|
+
{ shortcut: "[Ctrl+R]", description: "Reload AI chat" },
|
|
189
|
+
];
|
|
190
|
+
};
|
|
191
|
+
export const App = () => {
|
|
192
|
+
const { stdout } = useStdout();
|
|
193
|
+
const { exit } = useApp();
|
|
194
|
+
const [chatInput, setChatInput] = useState("");
|
|
195
|
+
const [rawLogData, setRawLogData] = useState([]);
|
|
196
|
+
const [planningDoc, setPlanningDoc] = useState("");
|
|
197
|
+
const partialLine = useRef("");
|
|
198
|
+
const logKeyCounter = useRef(0);
|
|
199
|
+
const [activePane, setActivePane] = useState("input");
|
|
200
|
+
const [logScroll, setLogScroll] = useState(0);
|
|
201
|
+
const [chatScroll, setChatScroll] = useState(0);
|
|
202
|
+
const [terminalRows, setTerminalRows] = useState(stdout?.rows || 20);
|
|
203
|
+
const [terminalCols, setTerminalCols] = useState(stdout?.columns || 80);
|
|
204
|
+
const [mode, setMode] = useState("NORMAL");
|
|
205
|
+
const ctrlPressedRef = useRef(false);
|
|
206
|
+
const shortcuts = useMemo(() => getShortcutsForMode(mode), [mode]);
|
|
207
|
+
const [unTamperedLogs, setUnTamperedLogs] = useState("");
|
|
208
|
+
// refs for current dims (used by stable callbacks)
|
|
209
|
+
const terminalColsRef = useRef(terminalCols);
|
|
210
|
+
const terminalRowsRef = useRef(terminalRows);
|
|
211
|
+
const logsHeightRef = useRef(Math.max(5, terminalRows - 6));
|
|
212
|
+
useEffect(() => {
|
|
213
|
+
terminalColsRef.current = terminalCols;
|
|
214
|
+
}, [terminalCols]);
|
|
215
|
+
useEffect(() => {
|
|
216
|
+
terminalRowsRef.current = terminalRows;
|
|
217
|
+
logsHeightRef.current = Math.max(5, terminalRows - 6);
|
|
218
|
+
}, [terminalRows]);
|
|
219
|
+
//websocket hook
|
|
220
|
+
const { connectWebSocket, sendQuery, chatResponseMessages, setChatResponseMessages, isConnected, isLoading, setIsLoading, setTrimmedChats, API_KEY, connectionError, setSocketId, setIsConnected, socket, setShowControlR, showControlR, setCompleteChatHistory, } = useWebSocket(config.websocket_url, logsManager);
|
|
221
|
+
const SCROLL_HEIGHT = terminalRows - 6;
|
|
222
|
+
const LOGS_HEIGHT = SCROLL_HEIGHT;
|
|
223
|
+
const INPUT_BOX_HEIGHT = 5; // Border (2) + marginTop (1) + content (1) + padding (~1)
|
|
224
|
+
const CHAT_HISTORY_HEIGHT = SCROLL_HEIGHT - INPUT_BOX_HEIGHT;
|
|
225
|
+
useEffect(() => {
|
|
226
|
+
const handleResize = () => {
|
|
227
|
+
if (stdout?.rows) {
|
|
228
|
+
setTerminalRows(stdout.rows);
|
|
229
|
+
}
|
|
230
|
+
if (stdout?.columns) {
|
|
231
|
+
setTerminalCols(stdout.columns);
|
|
232
|
+
}
|
|
233
|
+
// DO NOT re-run or re-process logs here
|
|
234
|
+
};
|
|
235
|
+
process.stdout.on("resize", handleResize);
|
|
236
|
+
return () => {
|
|
237
|
+
process.stdout.off("resize", handleResize);
|
|
238
|
+
};
|
|
239
|
+
}, [stdout]);
|
|
240
|
+
//web socket connection
|
|
241
|
+
useEffect(() => {
|
|
242
|
+
connectWebSocket();
|
|
243
|
+
}, []);
|
|
244
|
+
//get the AIMessage content inside the progress event
|
|
245
|
+
let lastAIMessage = "";
|
|
246
|
+
function extractAIMessages(obj) {
|
|
247
|
+
if (obj?.type !== "progress")
|
|
248
|
+
return undefined;
|
|
249
|
+
const messages = (obj.data && obj.data?.messages) ?? [];
|
|
250
|
+
const latestAI = [...messages]
|
|
251
|
+
.reverse()
|
|
252
|
+
.find((m) => m.id?.includes("AIMessage"));
|
|
253
|
+
const content = latestAI?.kwargs?.content?.trim();
|
|
254
|
+
if (!content)
|
|
255
|
+
return undefined;
|
|
256
|
+
if (content === lastAIMessage) {
|
|
257
|
+
return undefined;
|
|
258
|
+
}
|
|
259
|
+
lastAIMessage = content;
|
|
260
|
+
if (content === undefined)
|
|
261
|
+
return undefined;
|
|
262
|
+
return content;
|
|
263
|
+
}
|
|
264
|
+
// Auto-switch to chat pane when server finishes responding
|
|
265
|
+
useEffect(() => {
|
|
266
|
+
if (chatResponseMessages.length === 0)
|
|
267
|
+
return;
|
|
268
|
+
const lastMessage = chatResponseMessages[chatResponseMessages.length - 1];
|
|
269
|
+
// Switch focus when we get a final message type (response, ask_user, or error)
|
|
270
|
+
// if (
|
|
271
|
+
// lastMessage?.type === "response" ||
|
|
272
|
+
// lastMessage?.type === "progress" ||
|
|
273
|
+
// lastMessage?.type === "ask_user" ||
|
|
274
|
+
// lastMessage?.type === "error"
|
|
275
|
+
// ) {
|
|
276
|
+
// setTimeout(() => setActivePane("chat"), 600);
|
|
277
|
+
// }
|
|
278
|
+
// if (lastMessage && lastMessage.type && lastMessage?.type === "response") {
|
|
279
|
+
// setShowControlR(true);
|
|
280
|
+
// } else {
|
|
281
|
+
// setShowControlR(false);
|
|
282
|
+
// }
|
|
283
|
+
// const d = fs.readFileSync('logs')
|
|
284
|
+
// fs.writeFileSync('logs', `${d} \n ${JSON.stringify(chatResponseMessages)}`)
|
|
285
|
+
}, [chatResponseMessages]);
|
|
286
|
+
const chatLinesChat = useMemo(() => {
|
|
287
|
+
const availableWidth = Math.floor(terminalColsRef.current / 2) - 6;
|
|
288
|
+
if (!chatResponseMessages || chatResponseMessages.length <= 0) {
|
|
289
|
+
return [];
|
|
290
|
+
}
|
|
291
|
+
return chatResponseMessages.flatMap((msg, index) => {
|
|
292
|
+
const prefix = `${msg.type === "human" ? "🧑" : "🤖"} `;
|
|
293
|
+
const prefixWidth = stringWidth(prefix);
|
|
294
|
+
// const extracted = extractAIMessages(msg);
|
|
295
|
+
// const content =
|
|
296
|
+
// msg.type === "progress"
|
|
297
|
+
// ? Array.isArray(extracted)
|
|
298
|
+
// ? extracted.join("\n")
|
|
299
|
+
// : extracted || ""
|
|
300
|
+
// : msg.message ||
|
|
301
|
+
// msg?.data?.finalMessage ||
|
|
302
|
+
// msg?.data?.message ||
|
|
303
|
+
// JSON.stringify(msg, null, 2);
|
|
304
|
+
const content = msg.content;
|
|
305
|
+
if (content === "")
|
|
306
|
+
return [];
|
|
307
|
+
let contentLines = content?.split("\n");
|
|
308
|
+
if (!contentLines || contentLines.length <= 0)
|
|
309
|
+
contentLines = [];
|
|
310
|
+
const lines = contentLines.flatMap((line, lineIndex) => {
|
|
311
|
+
const fullLine = (lineIndex === 0 ? prefix : " ".repeat(prefixWidth)) + line;
|
|
312
|
+
const wrappedLines = wrapText(fullLine, availableWidth);
|
|
313
|
+
return wrappedLines.map((wrappedLine, wrapIndex) => ({
|
|
314
|
+
key: `chat-${index}-line-${lineIndex}-wrap-${wrapIndex}`,
|
|
315
|
+
text: wrappedLine,
|
|
316
|
+
}));
|
|
317
|
+
});
|
|
318
|
+
if (msg.type === "user") {
|
|
319
|
+
lines.push({
|
|
320
|
+
key: `chat-${index}-spacer`,
|
|
321
|
+
text: " ",
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
return lines;
|
|
325
|
+
});
|
|
326
|
+
}, [chatResponseMessages]);
|
|
327
|
+
const currentLogDataString = useMemo(() => rawLogData.map((l) => l.text).join("\n"), [rawLogData]);
|
|
328
|
+
// Process truncation once when inserting logs (so resize won't re-process)
|
|
329
|
+
const getProcessedLine = (text) => {
|
|
330
|
+
const availableWidth = Math.floor(terminalColsRef.current / 2) - 6;
|
|
331
|
+
const expandedText = text.replace(/\t/g, " ".repeat(8));
|
|
332
|
+
const width = stringWidth(expandedText);
|
|
333
|
+
if (width > availableWidth && availableWidth > 3) {
|
|
334
|
+
const truncated = sliceAnsi(expandedText, 0, Math.max(0, availableWidth - 3));
|
|
335
|
+
return truncated + "...";
|
|
336
|
+
}
|
|
337
|
+
return expandedText;
|
|
338
|
+
};
|
|
339
|
+
// Keep logLines purely tied to stored processed lines
|
|
340
|
+
const logLines = useMemo(() => rawLogData, [rawLogData]);
|
|
341
|
+
// Stable function to run bash command: does NOT depend on terminalCols or LOGS_HEIGHT
|
|
342
|
+
const runBashCommandWithPipe = useCallback((command) => {
|
|
343
|
+
const shell = process.env.SHELL || "bash";
|
|
344
|
+
const cols = Math.max(10, Math.floor(terminalColsRef.current / 2) - 6);
|
|
345
|
+
const rows = Math.max(1, logsHeightRef.current - 2);
|
|
346
|
+
const ptyProcess = ptySpawn(shell, ["-c", command], {
|
|
347
|
+
cwd: process.cwd(),
|
|
348
|
+
env: process.env,
|
|
349
|
+
cols: cols,
|
|
350
|
+
rows: rows,
|
|
351
|
+
});
|
|
352
|
+
ptyProcess.onData((chunk) => {
|
|
353
|
+
setUnTamperedLogs(oldLines => oldLines + chunk);
|
|
354
|
+
logsManager.addChunk(chunk);
|
|
355
|
+
let data = partialLine.current + chunk;
|
|
356
|
+
const lines = data.split("\n");
|
|
357
|
+
partialLine.current = lines.pop() || "";
|
|
358
|
+
if (lines.length > 0) {
|
|
359
|
+
const newLines = lines.map((line) => ({
|
|
360
|
+
key: `log-${logKeyCounter.current++}`,
|
|
361
|
+
text: getProcessedLine(line), // process once here
|
|
362
|
+
}));
|
|
363
|
+
// Append in single update
|
|
364
|
+
setRawLogData((prevLines) => [...prevLines, ...newLines]);
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
ptyProcess.onExit(({ exitCode }) => {
|
|
368
|
+
if (partialLine.current.length > 0) {
|
|
369
|
+
const remainingLine = {
|
|
370
|
+
key: `log-${logKeyCounter.current++}`,
|
|
371
|
+
text: getProcessedLine(partialLine.current),
|
|
372
|
+
};
|
|
373
|
+
setRawLogData((prevLines) => [...prevLines, remainingLine]);
|
|
374
|
+
partialLine.current = "";
|
|
375
|
+
}
|
|
376
|
+
const exitLine = {
|
|
377
|
+
key: `log-${logKeyCounter.current++}`,
|
|
378
|
+
text: `\n[Process exited with code ${exitCode}]\n`,
|
|
379
|
+
};
|
|
380
|
+
setRawLogData((prevLines) => [...prevLines, exitLine]);
|
|
381
|
+
});
|
|
382
|
+
return () => {
|
|
383
|
+
try {
|
|
384
|
+
ptyProcess.kill();
|
|
385
|
+
}
|
|
386
|
+
catch (e) {
|
|
387
|
+
// ignore
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
}, []);
|
|
391
|
+
// Start the pty once on mount. Do NOT restart on resize.
|
|
392
|
+
useEffect(() => {
|
|
393
|
+
const cmd = process.argv.slice(2).join(" ") ||
|
|
394
|
+
'echo "Welcome to the Scrollable CLI Debugger." && echo "Run a command after the script: tsx cli-app.tsx ls -la" && sleep 0.5 && echo "Fetching logs..." && echo "---------------------------" && ls -la';
|
|
395
|
+
const unsubscribe = runBashCommandWithPipe(cmd);
|
|
396
|
+
return () => {
|
|
397
|
+
if (unsubscribe) {
|
|
398
|
+
unsubscribe();
|
|
399
|
+
}
|
|
400
|
+
if (partialLine.current.length > 0) {
|
|
401
|
+
const remainingLine = {
|
|
402
|
+
key: `log-${logKeyCounter.current++}`,
|
|
403
|
+
text: getProcessedLine(partialLine.current),
|
|
404
|
+
};
|
|
405
|
+
setRawLogData((prev) => [...prev, remainingLine]);
|
|
406
|
+
partialLine.current = "";
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
}, [runBashCommandWithPipe]);
|
|
410
|
+
function generateArchitecture(projects) {
|
|
411
|
+
let res = "";
|
|
412
|
+
function avaiableData(project) {
|
|
413
|
+
if (project.code_available && project.logs_available) {
|
|
414
|
+
return `- codebase
|
|
415
|
+
- logs`;
|
|
416
|
+
}
|
|
417
|
+
if (project.code_available)
|
|
418
|
+
return `- codebase`;
|
|
419
|
+
if (project.logs_available)
|
|
420
|
+
return `- logs`;
|
|
421
|
+
}
|
|
422
|
+
projects.forEach(project => {
|
|
423
|
+
res += `
|
|
424
|
+
id: ${project.window_id}
|
|
425
|
+
service_name: ${project.name}
|
|
426
|
+
service_description: ${project.description}
|
|
427
|
+
available_data:
|
|
428
|
+
${avaiableData(project)}
|
|
429
|
+
`;
|
|
430
|
+
});
|
|
431
|
+
return res;
|
|
432
|
+
}
|
|
433
|
+
async function userMessageSubmitted() {
|
|
434
|
+
if (!chatInput.trim())
|
|
435
|
+
return;
|
|
436
|
+
// const lastMessage = chatResponseMessages[chatResponseMessages.length - 1];
|
|
437
|
+
const logs = chatResponseMessages.length <= 0 ? getLast50Lines(unTamperedLogs) : "";
|
|
438
|
+
// if (lastMessage?.type === "response" && lastMessage?.data?.state) {
|
|
439
|
+
// sendQuery(chatInput, logs, lastMessage.data.state);
|
|
440
|
+
// } else if (lastMessage?.type === "ask_user" && lastMessage?.data?.state) {
|
|
441
|
+
// sendQuery(chatInput, logs, lastMessage?.data?.state);
|
|
442
|
+
// } else {
|
|
443
|
+
// sendQuery(chatInput, logs);
|
|
444
|
+
// }
|
|
445
|
+
const userMessage = new HumanMessage(chatInput);
|
|
446
|
+
const projects = await fetchProjectsFromCluster(loadProjectMetadata());
|
|
447
|
+
sendQuery([...chatResponseMessages, userMessage], generateArchitecture(projects.projects), logs, planningDoc);
|
|
448
|
+
setChatResponseMessages((prev) => [
|
|
449
|
+
...prev,
|
|
450
|
+
userMessage
|
|
451
|
+
]);
|
|
452
|
+
setTrimmedChats((prev) => [
|
|
453
|
+
...prev,
|
|
454
|
+
userMessage
|
|
455
|
+
]);
|
|
456
|
+
setChatInput("");
|
|
457
|
+
}
|
|
458
|
+
useInput((inputStr, key) => {
|
|
459
|
+
ctrlPressedRef.current = key.ctrl;
|
|
460
|
+
if (inputStr === "c" && key.ctrl) {
|
|
461
|
+
exit();
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
if (key.ctrl && inputStr === "l") {
|
|
465
|
+
setRawLogData([]);
|
|
466
|
+
logKeyCounter.current = 0;
|
|
467
|
+
partialLine.current = "";
|
|
468
|
+
setLogScroll(0);
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
if (key.ctrl && inputStr === "k") {
|
|
472
|
+
setChatResponseMessages([]);
|
|
473
|
+
setChatScroll(0);
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
if (key.ctrl && inputStr === "r") {
|
|
477
|
+
setShowControlR(false);
|
|
478
|
+
setChatResponseMessages(() => []);
|
|
479
|
+
setTrimmedChats(() => []);
|
|
480
|
+
setCompleteChatHistory(() => []);
|
|
481
|
+
setSocketId[""];
|
|
482
|
+
socket?.close();
|
|
483
|
+
setIsConnected(false);
|
|
484
|
+
setIsLoading(false);
|
|
485
|
+
connectWebSocket();
|
|
486
|
+
setActivePane("input");
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
if (key.ctrl && inputStr === "d") {
|
|
490
|
+
setMode((prev) => {
|
|
491
|
+
if (prev === "NORMAL")
|
|
492
|
+
return "COPY";
|
|
493
|
+
if (prev === "COPY")
|
|
494
|
+
return "LOGS";
|
|
495
|
+
return "NORMAL";
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
if (key.tab) {
|
|
499
|
+
if (activePane === "input")
|
|
500
|
+
setActivePane("logs");
|
|
501
|
+
else if (activePane === "logs")
|
|
502
|
+
setActivePane("chat");
|
|
503
|
+
else
|
|
504
|
+
setActivePane("input");
|
|
505
|
+
return true;
|
|
506
|
+
}
|
|
507
|
+
const isScrollPane = activePane === "logs" || activePane === "chat";
|
|
508
|
+
const scrollDelta = 1;
|
|
509
|
+
if (isScrollPane && (key.upArrow || key.downArrow)) {
|
|
510
|
+
const currentScroll = activePane === "logs" ? logScroll : chatScroll;
|
|
511
|
+
const setScroll = activePane === "logs" ? setLogScroll : setChatScroll;
|
|
512
|
+
const lines = activePane === "logs" ? logLines : chatLinesChat;
|
|
513
|
+
const maxHeight = activePane === "logs" ? LOGS_HEIGHT - 2 : CHAT_HISTORY_HEIGHT - 1;
|
|
514
|
+
const totalLines = lines.length;
|
|
515
|
+
const visibleLines = Math.max(0, Math.min(maxHeight, totalLines));
|
|
516
|
+
const maxOffset = Math.max(0, totalLines - visibleLines);
|
|
517
|
+
let newScroll = currentScroll;
|
|
518
|
+
if (key.upArrow) {
|
|
519
|
+
newScroll = Math.max(0, currentScroll - scrollDelta);
|
|
520
|
+
}
|
|
521
|
+
else if (key.downArrow) {
|
|
522
|
+
newScroll = Math.min(maxOffset, currentScroll + scrollDelta);
|
|
523
|
+
}
|
|
524
|
+
if (newScroll !== currentScroll) {
|
|
525
|
+
setScroll(newScroll);
|
|
526
|
+
}
|
|
527
|
+
return true;
|
|
528
|
+
}
|
|
529
|
+
});
|
|
530
|
+
const handleInputChange = useCallback((input) => {
|
|
531
|
+
if (!ctrlPressedRef.current)
|
|
532
|
+
setChatInput(input);
|
|
533
|
+
}, []);
|
|
534
|
+
if (mode === "COPY") {
|
|
535
|
+
return (_jsxs(Box, { flexDirection: "column", height: terminalRows, width: "100%", padding: 0, overflow: "hidden", children: [_jsx(Box, { flexDirection: "row", width: "100%", height: LOGS_HEIGHT, padding: 0, overflow: "hidden", children: _jsxs(BorderBoxNoBorder, { title: "AI Chat", isFocused: activePane === "chat", width: "100%", children: [!API_KEY ? (_jsxs(Text, { children: ["Auth key not found.", "\n", "Login using:", " ", _jsx(Text, { color: "blue", children: "oncall login <your-auth-key>" })] })) : isConnected ? (_jsx(ScrollableContentChat, { lines: chatLinesChat, maxHeight: CHAT_HISTORY_HEIGHT - 1, isFocused: activePane === "chat", scrollOffset: chatScroll, onScrollChange: setChatScroll, isLoading: isLoading, showControlR: showControlR })) : (_jsx(Text, { children: "AI Chat not connected" })), isConnected && (_jsxs(Box, { borderStyle: "round", borderColor: activePane === "input" ? "greenBright" : "white", paddingX: 1, width: "100%", overflow: "hidden", children: [_jsx(Text, { color: "white", bold: activePane === "input", children: "Input:" }), _jsx(TextInput, { placeholder: "What's bugging you today?", value: chatInput, onChange: handleInputChange, onSubmit: userMessageSubmitted, focus: activePane === "input" })] }))] }) }), _jsx(ShortcutsFooter, { shortcuts: shortcuts })] }));
|
|
536
|
+
}
|
|
537
|
+
else if (mode === "LOGS") {
|
|
538
|
+
return (_jsxs(Box, { flexDirection: "column", height: terminalRows, width: "100%", padding: 0, overflow: "hidden", children: [_jsx(Box, { flexDirection: "row", width: "100%", height: LOGS_HEIGHT, padding: 0, overflow: "hidden", children: _jsx(BorderBoxNoBorder, { title: "Command Logs", isFocused: activePane === "chat", width: "100%", children: _jsx(ScrollableContent, { lines: logLines, maxHeight: LOGS_HEIGHT - 2, isFocused: activePane === "logs", scrollOffset: logScroll, onScrollChange: setLogScroll }) }) }), _jsx(ShortcutsFooter, { shortcuts: shortcuts })] }));
|
|
539
|
+
}
|
|
540
|
+
else {
|
|
541
|
+
return (_jsxs(Box, { flexDirection: "column", height: terminalRows, width: "100%", padding: 0, overflow: "hidden", children: [_jsxs(Box, { flexDirection: "row", width: "100%", height: LOGS_HEIGHT, padding: 0, overflow: "hidden", children: [_jsx(BorderBox, { title: "Command Logs", isFocused: activePane === "logs", width: "50%", children: _jsx(ScrollableContent, { lines: logLines, maxHeight: LOGS_HEIGHT - 2, isFocused: activePane === "logs", scrollOffset: logScroll, onScrollChange: setLogScroll }) }), _jsxs(BorderBox, { title: "AI Chat", isFocused: activePane === "chat", width: "50%", children: [!API_KEY ? (_jsxs(Text, { children: ["Auth key not found.", "\n", "Login using:", " ", _jsx(Text, { color: "blue", children: "oncall login <your-auth-key>" })] })) : isConnected ? (_jsx(ScrollableContentChat, { lines: chatLinesChat, maxHeight: CHAT_HISTORY_HEIGHT - 1, isFocused: activePane === "chat", scrollOffset: chatScroll, onScrollChange: setChatScroll, isLoading: isLoading, showControlR: showControlR })) : (_jsx(Text, { children: "AI Chat not connected" })), isConnected && (_jsxs(Box, { borderStyle: "round", borderColor: activePane === "input" ? "greenBright" : "white", paddingX: 1, width: "100%", overflow: "hidden", children: [_jsx(Text, { color: "white", bold: activePane === "input", children: "Input:" }), _jsx(TextInput, { placeholder: "What's bugging you today?", value: chatInput, onChange: handleInputChange, onSubmit: userMessageSubmitted, focus: activePane === "input" })] }))] })] }), _jsx(ShortcutsFooter, { shortcuts: shortcuts })] }));
|
|
542
|
+
}
|
|
543
|
+
};
|
|
544
|
+
render(_jsx(App, {}));
|
|
545
|
+
//# sourceMappingURL=index.js.map
|