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.
Files changed (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +172 -0
  3. package/bin/oncall.js +303 -0
  4. package/dist/api.d.ts +1 -0
  5. package/dist/api.js +23 -0
  6. package/dist/api.js.map +1 -0
  7. package/dist/cli.d.ts +2 -0
  8. package/dist/cli.js +91 -0
  9. package/dist/cli.js.map +1 -0
  10. package/dist/code_hierarchy.d.ts +4 -0
  11. package/dist/code_hierarchy.js +92 -0
  12. package/dist/code_hierarchy.js.map +1 -0
  13. package/dist/config.d.ts +9 -0
  14. package/dist/config.js +12 -0
  15. package/dist/config.js.map +1 -0
  16. package/dist/helpers/cli-helpers.d.ts +22 -0
  17. package/dist/helpers/cli-helpers.js +261 -0
  18. package/dist/helpers/cli-helpers.js.map +1 -0
  19. package/dist/helpers/config-helpers.js +161 -0
  20. package/dist/helpers/ripgrep-tool.d.ts +15 -0
  21. package/dist/helpers/ripgrep-tool.js +110 -0
  22. package/dist/helpers/ripgrep-tool.js.map +1 -0
  23. package/dist/index.d.ts +3 -0
  24. package/dist/index.js +545 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/logsManager.d.ts +31 -0
  27. package/dist/logsManager.js +90 -0
  28. package/dist/logsManager.js.map +1 -0
  29. package/dist/tools/ripgrep.d.ts +15 -0
  30. package/dist/tools/ripgrep.js +110 -0
  31. package/dist/tools/ripgrep.js.map +1 -0
  32. package/dist/ui-graph.d.ts +1 -0
  33. package/dist/ui-graph.js +125 -0
  34. package/dist/ui-graph.js.map +1 -0
  35. package/dist/useWebSocket.d.ts +21 -0
  36. package/dist/useWebSocket.js +411 -0
  37. package/dist/useWebSocket.js.map +1 -0
  38. package/dist/utils/version-check.d.ts +2 -0
  39. package/dist/utils/version-check.js +124 -0
  40. package/dist/utils/version-check.js.map +1 -0
  41. package/dist/utils.d.ts +1 -0
  42. package/dist/utils.js +22 -0
  43. package/dist/utils.js.map +1 -0
  44. package/dist/websocket-server.d.ts +24 -0
  45. package/dist/websocket-server.js +221 -0
  46. package/dist/websocket-server.js.map +1 -0
  47. 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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"}
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env tsx
2
+ import React from "react";
3
+ export declare const App: React.FC;
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