clippy-test 1.0.8 → 2.0.0
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 +396 -0
- package/dist/api.js +10 -1
- package/dist/cli.js +20 -20
- package/dist/config.js +7 -7
- package/dist/helpers/cli-helpers.d.ts +25 -0
- package/dist/helpers/cli-helpers.js +329 -0
- package/dist/helpers/config-helpers.js +189 -0
- package/dist/helpers/ripgrep-tool.d.ts +15 -0
- package/dist/helpers/ripgrep-tool.js +126 -0
- package/dist/index.js +318 -122
- package/dist/logsManager.d.ts +31 -0
- package/dist/logsManager.js +90 -0
- package/dist/postinstall.js +20 -0
- package/dist/tools/ripgrep.d.ts +15 -0
- package/dist/tools/ripgrep.js +110 -0
- package/dist/useWebSocket.d.ts +14 -7
- package/dist/useWebSocket.js +291 -48
- package/dist/utils/version-check.d.ts +2 -0
- package/dist/utils/version-check.js +124 -0
- package/dist/utils.d.ts +16 -0
- package/dist/utils.js +125 -4
- package/dist/websocket-server.d.ts +24 -0
- package/dist/websocket-server.js +235 -0
- package/package.json +19 -6
- package/bin/clippy.js +0 -109
- package/dist/api.js.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/code_hierarchy.js.map +0 -1
- package/dist/config.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/ui-graph.js.map +0 -1
- package/dist/useWebSocket.js.map +0 -1
- package/dist/utils.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -12,6 +12,10 @@ import { markedTerminal } from "marked-terminal";
|
|
|
12
12
|
import { useWebSocket } from "./useWebSocket.js";
|
|
13
13
|
import { config } from "./config.js";
|
|
14
14
|
import Spinner from "ink-spinner";
|
|
15
|
+
import { HumanMessage } from "langchain";
|
|
16
|
+
import { loadProjectMetadata, fetchProjectsFromCluster, logd } from "./helpers/cli-helpers.js";
|
|
17
|
+
import logsManager from "./logsManager.js";
|
|
18
|
+
import { extractMessageContent } from "./utils.js";
|
|
15
19
|
dotenv.config();
|
|
16
20
|
if (typeof process !== "undefined" && process.on) {
|
|
17
21
|
process.on("SIGINT", () => {
|
|
@@ -22,15 +26,24 @@ if (typeof process !== "undefined" && process.on) {
|
|
|
22
26
|
marked.use(markedTerminal({
|
|
23
27
|
reflowText: false,
|
|
24
28
|
showSectionPrefix: false,
|
|
25
|
-
unescape:
|
|
29
|
+
unescape: true,
|
|
30
|
+
emoji: true
|
|
26
31
|
}));
|
|
32
|
+
const COLON_REPLACER = '*#COLON|*';
|
|
33
|
+
function escapeRegExp(str) {
|
|
34
|
+
return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&');
|
|
35
|
+
}
|
|
36
|
+
const COLON_REPLACER_REGEXP = new RegExp(escapeRegExp(COLON_REPLACER), 'g');
|
|
37
|
+
function undoColon(str) {
|
|
38
|
+
return str.replace(COLON_REPLACER_REGEXP, ':');
|
|
39
|
+
}
|
|
27
40
|
// Override just the 'text' renderer to handle inline tokens:
|
|
28
41
|
marked.use({
|
|
29
42
|
renderer: {
|
|
30
43
|
text(tokenOrString) {
|
|
31
44
|
if (typeof tokenOrString === "object" && tokenOrString?.tokens) {
|
|
32
45
|
// @ts-ignore - 'this' is the renderer context with a parser
|
|
33
|
-
return this.parser.parseInline(tokenOrString.tokens);
|
|
46
|
+
return undoColon(this.parser.parseInline(tokenOrString.tokens));
|
|
34
47
|
}
|
|
35
48
|
return typeof tokenOrString === "string"
|
|
36
49
|
? tokenOrString
|
|
@@ -94,12 +107,12 @@ const ScrollableContent = ({ lines, maxHeight, isFocused, onScrollChange, scroll
|
|
|
94
107
|
}
|
|
95
108
|
const scrollBarIndicator = totalLines > visibleLines ? `[${scrollPosition}%]` : "";
|
|
96
109
|
return (_jsxs(Box, { flexDirection: "column", height: maxHeight, overflow: "hidden", children: [displayedLines.map((line) => {
|
|
97
|
-
const rendered = marked.parseInline(line.text);
|
|
110
|
+
const rendered = undoColon(marked.parseInline(line.text));
|
|
98
111
|
return _jsx(Text, { children: rendered }, line.key);
|
|
99
112
|
}), _jsx(Box, { position: "absolute", children: _jsx(Text, { color: "yellowBright", bold: true, children: scrollBarIndicator }) })] }));
|
|
100
113
|
};
|
|
101
114
|
//AI chat
|
|
102
|
-
const ScrollableContentChat = ({ lines, maxHeight, isFocused, onScrollChange, scrollOffset, isLoading, showControlR, }) => {
|
|
115
|
+
const ScrollableContentChat = ({ lines, maxHeight, isFocused, onScrollChange, scrollOffset, isLoading, showControlR, customMessage, }) => {
|
|
103
116
|
const totalLines = lines.length;
|
|
104
117
|
const visibleLines = Math.max(0, Math.min(maxHeight, totalLines));
|
|
105
118
|
const maxOffset = Math.max(0, totalLines - visibleLines);
|
|
@@ -153,21 +166,44 @@ const ScrollableContentChat = ({ lines, maxHeight, isFocused, onScrollChange, sc
|
|
|
153
166
|
const scrollBarIndicator = totalLines > visibleLines ? `[${scrollPosition}%]` : "";
|
|
154
167
|
return (_jsxs(Box, { flexDirection: "column", height: maxHeight, overflow: "hidden", children: [displayedLines.map((line) => {
|
|
155
168
|
if (line?.text === "" || line?.text === undefined)
|
|
156
|
-
return;
|
|
157
|
-
const rendered = marked.parseInline(line?.text);
|
|
169
|
+
return null;
|
|
170
|
+
const rendered = undoColon(marked.parseInline(line?.text));
|
|
158
171
|
return _jsx(Text, { children: rendered }, line.key);
|
|
159
|
-
}), 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 }) })] }));
|
|
172
|
+
}), isLoading && (_jsx(_Fragment, { children: _jsxs(Text, { color: "grey", children: [_jsx(Spinner, { type: "dots" }), "\u00A0 ", customMessage || "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 }) })] }));
|
|
160
173
|
};
|
|
161
174
|
//border for the content
|
|
162
175
|
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] }));
|
|
163
|
-
const BorderBoxNoBorder = ({ title, children, isFocused, width, }) => (_jsxs(Box, { flexDirection: "column",
|
|
164
|
-
|
|
165
|
-
|
|
176
|
+
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] }));
|
|
177
|
+
const ShortcutBadge = ({ label }) => (_jsxs(Text, { backgroundColor: "#1f2937", color: "#f8fafc", bold: true, children: [" ", label, " "] }));
|
|
178
|
+
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}` })] }));
|
|
179
|
+
const ShortcutsFooter = ({ shortcuts, }) => {
|
|
180
|
+
const firstRow = shortcuts.slice(0, 3);
|
|
181
|
+
const secondRow = shortcuts.slice(3);
|
|
182
|
+
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]
|
|
183
|
+
.filter((row) => row.length > 0)
|
|
184
|
+
.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}`))) })] }));
|
|
185
|
+
};
|
|
186
|
+
const getShortcutsForMode = (mode) => {
|
|
187
|
+
const ctrlDAction = mode === "COPY"
|
|
188
|
+
? "Expand Logs"
|
|
189
|
+
: mode === "LOGS"
|
|
190
|
+
? "Collapse Logs"
|
|
191
|
+
: "Toggle chat";
|
|
192
|
+
return [
|
|
193
|
+
{ shortcut: "[Tab]", description: "Switch Focus" },
|
|
194
|
+
{ shortcut: "[ ⬆ / ⬇ ]", description: "Scroll (Keyboard Only)" },
|
|
195
|
+
{ shortcut: "[Enter]", description: "Send" },
|
|
196
|
+
{ shortcut: "[Ctrl+D]", description: ctrlDAction },
|
|
197
|
+
{ shortcut: "[Ctrl+C]", description: "Exit" },
|
|
198
|
+
{ shortcut: "[Ctrl+R]", description: "Reload AI chat" },
|
|
199
|
+
];
|
|
200
|
+
};
|
|
166
201
|
export const App = () => {
|
|
167
202
|
const { stdout } = useStdout();
|
|
168
203
|
const { exit } = useApp();
|
|
169
204
|
const [chatInput, setChatInput] = useState("");
|
|
170
205
|
const [rawLogData, setRawLogData] = useState([]);
|
|
206
|
+
const [planningDoc, setPlanningDoc] = useState("");
|
|
171
207
|
const partialLine = useRef("");
|
|
172
208
|
const logKeyCounter = useRef(0);
|
|
173
209
|
const [activePane, setActivePane] = useState("input");
|
|
@@ -176,8 +212,24 @@ export const App = () => {
|
|
|
176
212
|
const [terminalRows, setTerminalRows] = useState(stdout?.rows || 20);
|
|
177
213
|
const [terminalCols, setTerminalCols] = useState(stdout?.columns || 80);
|
|
178
214
|
const [mode, setMode] = useState("NORMAL");
|
|
215
|
+
const ctrlPressedRef = useRef(false);
|
|
216
|
+
const shortcuts = useMemo(() => getShortcutsForMode(mode), [mode]);
|
|
217
|
+
const [unTamperedLogs, setUnTamperedLogs] = useState("");
|
|
218
|
+
// refs for current dims (used by stable callbacks)
|
|
219
|
+
const terminalColsRef = useRef(terminalCols);
|
|
220
|
+
const terminalRowsRef = useRef(terminalRows);
|
|
221
|
+
const logsHeightRef = useRef(Math.max(5, terminalRows - 6));
|
|
222
|
+
useEffect(() => {
|
|
223
|
+
terminalColsRef.current = terminalCols;
|
|
224
|
+
}, [terminalCols]);
|
|
225
|
+
useEffect(() => {
|
|
226
|
+
terminalRowsRef.current = terminalRows;
|
|
227
|
+
logsHeightRef.current = Math.max(5, terminalRows - 6);
|
|
228
|
+
}, [terminalRows]);
|
|
179
229
|
//websocket hook
|
|
180
|
-
const { connectWebSocket, sendQuery, chatResponseMessages,
|
|
230
|
+
const { connectWebSocket, sendQuery, chatResponseMessages, // Full history including ToolMessage (for backend queries)
|
|
231
|
+
visibleChats, // Filtered for UI display (excludes ToolMessage except reflections)
|
|
232
|
+
setChatResponseMessages, isConnected, isLoading, setIsLoading, setTrimmedChats, API_KEY, connectionError, setSocketId, setIsConnected, socket, setShowControlR, showControlR, setCompleteChatHistory, customMessage, setVisibleChats, graphState, setGraphState } = useWebSocket(config.websocket_url, logsManager);
|
|
181
233
|
const SCROLL_HEIGHT = terminalRows - 6;
|
|
182
234
|
const LOGS_HEIGHT = SCROLL_HEIGHT;
|
|
183
235
|
const INPUT_BOX_HEIGHT = 5; // Border (2) + marginTop (1) + content (1) + padding (~1)
|
|
@@ -190,6 +242,7 @@ export const App = () => {
|
|
|
190
242
|
if (stdout?.columns) {
|
|
191
243
|
setTerminalCols(stdout.columns);
|
|
192
244
|
}
|
|
245
|
+
// DO NOT re-run or re-process logs here
|
|
193
246
|
};
|
|
194
247
|
process.stdout.on("resize", handleResize);
|
|
195
248
|
return () => {
|
|
@@ -204,7 +257,7 @@ export const App = () => {
|
|
|
204
257
|
let lastAIMessage = "";
|
|
205
258
|
function extractAIMessages(obj) {
|
|
206
259
|
if (obj?.type !== "progress")
|
|
207
|
-
return
|
|
260
|
+
return undefined;
|
|
208
261
|
const messages = (obj.data && obj.data?.messages) ?? [];
|
|
209
262
|
const latestAI = [...messages]
|
|
210
263
|
.reverse()
|
|
@@ -217,50 +270,101 @@ export const App = () => {
|
|
|
217
270
|
}
|
|
218
271
|
lastAIMessage = content;
|
|
219
272
|
if (content === undefined)
|
|
220
|
-
return;
|
|
273
|
+
return undefined;
|
|
221
274
|
return content;
|
|
222
275
|
}
|
|
223
276
|
// Auto-switch to chat pane when server finishes responding
|
|
224
277
|
useEffect(() => {
|
|
225
|
-
|
|
278
|
+
// Use visibleChats for UI-related checks
|
|
279
|
+
const messagesToCheck = visibleChats || chatResponseMessages;
|
|
280
|
+
if (messagesToCheck.length === 0)
|
|
226
281
|
return;
|
|
227
|
-
const lastMessage =
|
|
282
|
+
const lastMessage = messagesToCheck[messagesToCheck.length - 1];
|
|
228
283
|
// Switch focus when we get a final message type (response, ask_user, or error)
|
|
229
|
-
if (
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
284
|
+
// if (
|
|
285
|
+
// lastMessage?.type === "response" ||
|
|
286
|
+
// lastMessage?.type === "progress" ||
|
|
287
|
+
// lastMessage?.type === "ask_user" ||
|
|
288
|
+
// lastMessage?.type === "error"
|
|
289
|
+
// ) {
|
|
290
|
+
// setTimeout(() => setActivePane("chat"), 600);
|
|
291
|
+
// }
|
|
292
|
+
// if (lastMessage && lastMessage.type && lastMessage?.type === "response") {
|
|
293
|
+
// setShowControlR(true);
|
|
294
|
+
// } else {
|
|
295
|
+
// setShowControlR(false);
|
|
296
|
+
// }
|
|
297
|
+
// const d = fs.readFileSync('logs')
|
|
298
|
+
// fs.writeFileSync('logs', `${d} \n ${JSON.stringify(chatResponseMessages)}`)
|
|
299
|
+
}, [visibleChats, chatResponseMessages]);
|
|
300
|
+
const chatLinesChat = useMemo(() => {
|
|
301
|
+
const availableWidth = Math.floor(terminalColsRef.current / 2) - 10;
|
|
302
|
+
// Use visibleChats for UI rendering (filtered, excludes ToolMessage except reflections)
|
|
303
|
+
const messagesToRender = visibleChats || chatResponseMessages;
|
|
304
|
+
if (!messagesToRender || messagesToRender.length <= 0) {
|
|
305
|
+
return [];
|
|
235
306
|
}
|
|
236
|
-
|
|
237
|
-
|
|
307
|
+
function groupMessageChunks(messages) {
|
|
308
|
+
return messages.reduce((prev, message) => {
|
|
309
|
+
if (!prev)
|
|
310
|
+
prev = { lastId: "", messages: [] };
|
|
311
|
+
if (!message.id)
|
|
312
|
+
message.id = (new Date()).getTime() + "";
|
|
313
|
+
if (message.id !== prev.lastId) {
|
|
314
|
+
prev.messages.push([message]);
|
|
315
|
+
prev.lastId = message.id;
|
|
316
|
+
return prev;
|
|
317
|
+
}
|
|
318
|
+
else {
|
|
319
|
+
}
|
|
320
|
+
const lastMessageIndex = prev.messages.length - 1;
|
|
321
|
+
prev.messages[lastMessageIndex].push(message);
|
|
322
|
+
return prev;
|
|
323
|
+
}, { lastId: "", messages: [] });
|
|
238
324
|
}
|
|
239
|
-
|
|
240
|
-
|
|
325
|
+
function extratMessageContent(messages) {
|
|
326
|
+
return messages.reduce((prev, message) => {
|
|
327
|
+
return prev + message.content;
|
|
328
|
+
}, "");
|
|
241
329
|
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
330
|
+
function extractThinkMessage(messages) {
|
|
331
|
+
if (messages.length < 5)
|
|
332
|
+
return "";
|
|
333
|
+
return messages.slice(4, -3).reduce((prev, message) => {
|
|
334
|
+
if (message.tool_call_chunks && message.tool_call_chunks[0] && message.tool_call_chunks[0].args)
|
|
335
|
+
return prev + message.tool_call_chunks[0].args;
|
|
336
|
+
return prev;
|
|
337
|
+
}, "");
|
|
338
|
+
}
|
|
339
|
+
return groupMessageChunks(messagesToRender).messages.flatMap((msgs, index) => {
|
|
340
|
+
// Get message type from BaseMessage (LangChain messages use getType() or constructor name)
|
|
341
|
+
const msgAny = msgs;
|
|
342
|
+
const msgType = (typeof msgAny[0].getType === "function" && msgAny[0].getType()) ||
|
|
343
|
+
msgAny[0]._type ||
|
|
344
|
+
msgs.constructor.name ||
|
|
345
|
+
"";
|
|
346
|
+
const isHuman = msgType === "human" || msgs.constructor.name === "HumanMessage";
|
|
347
|
+
// const prefix = `${isHuman ? "🧑" : "🤖"} `;
|
|
348
|
+
const prefix = `${isHuman ? ">" : "⏺"} `;
|
|
249
349
|
const prefixWidth = stringWidth(prefix);
|
|
250
|
-
//
|
|
251
|
-
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
350
|
+
// let content = `${index}: ` + extractMessageContent(msgs)
|
|
351
|
+
let content = extractMessageContent(msgs);
|
|
352
|
+
const fMessage = msgs[0];
|
|
353
|
+
if (fMessage.tool_calls && fMessage.tool_calls.length > 0) {
|
|
354
|
+
logd("---------------- tool calls received --------------");
|
|
355
|
+
logd(JSON.stringify(msgs));
|
|
356
|
+
if (fMessage.tool_calls[0].name === "thinkTool") {
|
|
357
|
+
content = extractThinkMessage(msgs);
|
|
358
|
+
}
|
|
359
|
+
else {
|
|
360
|
+
content = `Calling: ${fMessage.tool_calls[0].name}`;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
261
363
|
if (content === "")
|
|
262
|
-
return;
|
|
263
|
-
|
|
364
|
+
return [];
|
|
365
|
+
let contentLines = content?.split("\n");
|
|
366
|
+
if (!contentLines || contentLines.length <= 0)
|
|
367
|
+
contentLines = [];
|
|
264
368
|
const lines = contentLines.flatMap((line, lineIndex) => {
|
|
265
369
|
const fullLine = (lineIndex === 0 ? prefix : " ".repeat(prefixWidth)) + line;
|
|
266
370
|
const wrappedLines = wrapText(fullLine, availableWidth);
|
|
@@ -269,41 +373,96 @@ export const App = () => {
|
|
|
269
373
|
text: wrappedLine,
|
|
270
374
|
}));
|
|
271
375
|
});
|
|
272
|
-
// Add spacing after
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
376
|
+
// Add spacing after each message for better readability
|
|
377
|
+
lines.push({
|
|
378
|
+
key: `chat-${index}-spacer`,
|
|
379
|
+
text: " ",
|
|
380
|
+
});
|
|
381
|
+
// Add an extra blank line after human messages for clearer separation
|
|
382
|
+
// if (isHuman || msgType === "user") {
|
|
383
|
+
// lines.push({
|
|
384
|
+
// key: `chat-${index}-spacer-extra`,
|
|
385
|
+
// text: " ",
|
|
386
|
+
// });
|
|
387
|
+
// }
|
|
279
388
|
return lines;
|
|
280
389
|
});
|
|
281
|
-
|
|
390
|
+
// return groupMessageChunks(messagesToRender).messages.flatMap((msgs, index) => {
|
|
391
|
+
// // Get message type from BaseMessage (LangChain messages use getType() or constructor name)
|
|
392
|
+
// const msgAny = msgs as any;
|
|
393
|
+
// const msgType =
|
|
394
|
+
// (typeof msgAny[0].getType === "function" && msgAny[0].getType()) ||
|
|
395
|
+
// msgAny[0]._type ||
|
|
396
|
+
// msgs.constructor.name ||
|
|
397
|
+
// "";
|
|
398
|
+
// const isHuman = msgType === "human" || msgs.constructor.name === "HumanMessage";
|
|
399
|
+
// // const prefix = `${isHuman ? "🧑" : "🤖"} `;
|
|
400
|
+
// const prefix = `${isHuman ? "> " : "⏺ "} `;
|
|
401
|
+
// const prefixWidth = stringWidth(prefix);
|
|
402
|
+
// let content = extractMessageContent(msgs)
|
|
403
|
+
// const fMessage = msgs[0] as AIMessage
|
|
404
|
+
// if (fMessage.tool_calls && fMessage.tool_calls.length > 0) {
|
|
405
|
+
// logd("---------------- tool calls received --------------");
|
|
406
|
+
// logd(JSON.stringify(msgs));
|
|
407
|
+
// if (fMessage.tool_calls[0].name === "thinkTool") {
|
|
408
|
+
// content = extractThinkMessage(msgs as AIMessageChunk[])
|
|
409
|
+
// } else {
|
|
410
|
+
// content = `Calling: ${fMessage.tool_calls[0].name}`
|
|
411
|
+
// }
|
|
412
|
+
// }
|
|
413
|
+
// return {
|
|
414
|
+
// key: `chat-${index}`,
|
|
415
|
+
// text: prefix + content,
|
|
416
|
+
// }
|
|
417
|
+
// });
|
|
418
|
+
}, [visibleChats, chatResponseMessages]);
|
|
282
419
|
const currentLogDataString = useMemo(() => rawLogData.map((l) => l.text).join("\n"), [rawLogData]);
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
const
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
420
|
+
// Process truncation once when inserting logs (so resize won't re-process)
|
|
421
|
+
const getProcessedLine = (text) => {
|
|
422
|
+
const availableWidth = Math.floor(terminalColsRef.current / 2) - 6;
|
|
423
|
+
const expandedText = text.replace(/\t/g, " ".repeat(8)).replace(/\x1B\[1G/g, '').replace(/\x1B\[0K/g, '');
|
|
424
|
+
const width = stringWidth(expandedText);
|
|
425
|
+
if (width > availableWidth && availableWidth > 3) {
|
|
426
|
+
const truncated = sliceAnsi(expandedText, 0, Math.max(0, availableWidth - 3));
|
|
427
|
+
return truncated + "...";
|
|
428
|
+
}
|
|
429
|
+
// fs.appendFileSync("./temp-logs", text)
|
|
430
|
+
// fs.appendFileSync("./temp-logs", "\n")
|
|
431
|
+
// fs.appendFileSync("./temp-logs", expandedText)
|
|
432
|
+
return expandedText;
|
|
433
|
+
};
|
|
434
|
+
// Keep logLines purely tied to stored processed lines
|
|
435
|
+
const logLines = useMemo(() => rawLogData, [rawLogData]);
|
|
436
|
+
// Prefer a known-good shell over a potentially broken $SHELL on some machines
|
|
437
|
+
function getSafeShell() {
|
|
438
|
+
const candidates = [
|
|
439
|
+
process.env.SHELL, // user-configured shell
|
|
440
|
+
"/bin/zsh",
|
|
441
|
+
"/bin/bash",
|
|
442
|
+
].filter(Boolean);
|
|
443
|
+
for (const shell of candidates) {
|
|
444
|
+
try {
|
|
445
|
+
// Synchronously test if the shell is executable by spawning a no-op command.
|
|
446
|
+
// Use child_process directly to avoid triggering node-pty in this probe.
|
|
447
|
+
const { spawnSync } = require("child_process");
|
|
448
|
+
const res = spawnSync(shell, ["-c", "echo"], { stdio: "ignore" });
|
|
449
|
+
if (res.status === 0) {
|
|
450
|
+
return shell;
|
|
451
|
+
}
|
|
296
452
|
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
453
|
+
catch {
|
|
454
|
+
// ignore and try next candidate
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
// Fallback to a generic bash lookup; node-pty will still error if it's truly missing,
|
|
458
|
+
// but this avoids depending on a bad $SHELL value.
|
|
459
|
+
return "bash";
|
|
460
|
+
}
|
|
461
|
+
// Stable function to run bash command: does NOT depend on terminalCols or LOGS_HEIGHT
|
|
303
462
|
const runBashCommandWithPipe = useCallback((command) => {
|
|
304
|
-
const shell = process.env.SHELL || "
|
|
305
|
-
const cols = Math.floor(
|
|
306
|
-
const rows =
|
|
463
|
+
const shell = process.env.SHELL || "/bin/zsh";
|
|
464
|
+
const cols = Math.max(10, Math.floor(terminalColsRef.current / 2) - 6);
|
|
465
|
+
const rows = Math.max(1, logsHeightRef.current - 2);
|
|
307
466
|
const ptyProcess = ptySpawn(shell, ["-c", command], {
|
|
308
467
|
cwd: process.cwd(),
|
|
309
468
|
env: process.env,
|
|
@@ -311,14 +470,17 @@ export const App = () => {
|
|
|
311
470
|
rows: rows,
|
|
312
471
|
});
|
|
313
472
|
ptyProcess.onData((chunk) => {
|
|
473
|
+
setUnTamperedLogs((oldLines) => oldLines + chunk);
|
|
474
|
+
logsManager.addChunk(chunk);
|
|
314
475
|
let data = partialLine.current + chunk;
|
|
315
476
|
const lines = data.split("\n");
|
|
316
477
|
partialLine.current = lines.pop() || "";
|
|
317
478
|
if (lines.length > 0) {
|
|
318
479
|
const newLines = lines.map((line) => ({
|
|
319
480
|
key: `log-${logKeyCounter.current++}`,
|
|
320
|
-
text: line,
|
|
481
|
+
text: getProcessedLine(line), // process once here
|
|
321
482
|
}));
|
|
483
|
+
// Append in single update
|
|
322
484
|
setRawLogData((prevLines) => [...prevLines, ...newLines]);
|
|
323
485
|
}
|
|
324
486
|
});
|
|
@@ -326,7 +488,7 @@ export const App = () => {
|
|
|
326
488
|
if (partialLine.current.length > 0) {
|
|
327
489
|
const remainingLine = {
|
|
328
490
|
key: `log-${logKeyCounter.current++}`,
|
|
329
|
-
text: partialLine.current,
|
|
491
|
+
text: getProcessedLine(partialLine.current),
|
|
330
492
|
};
|
|
331
493
|
setRawLogData((prevLines) => [...prevLines, remainingLine]);
|
|
332
494
|
partialLine.current = "";
|
|
@@ -337,8 +499,16 @@ export const App = () => {
|
|
|
337
499
|
};
|
|
338
500
|
setRawLogData((prevLines) => [...prevLines, exitLine]);
|
|
339
501
|
});
|
|
340
|
-
return () =>
|
|
341
|
-
|
|
502
|
+
return () => {
|
|
503
|
+
try {
|
|
504
|
+
ptyProcess.kill();
|
|
505
|
+
}
|
|
506
|
+
catch (e) {
|
|
507
|
+
// ignore
|
|
508
|
+
}
|
|
509
|
+
};
|
|
510
|
+
}, []);
|
|
511
|
+
// Start the pty once on mount. Do NOT restart on resize.
|
|
342
512
|
useEffect(() => {
|
|
343
513
|
const cmd = process.argv.slice(2).join(" ") ||
|
|
344
514
|
'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';
|
|
@@ -346,50 +516,68 @@ export const App = () => {
|
|
|
346
516
|
return () => {
|
|
347
517
|
if (unsubscribe) {
|
|
348
518
|
unsubscribe();
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
519
|
+
}
|
|
520
|
+
if (partialLine.current.length > 0) {
|
|
521
|
+
const remainingLine = {
|
|
522
|
+
key: `log-${logKeyCounter.current++}`,
|
|
523
|
+
text: getProcessedLine(partialLine.current),
|
|
524
|
+
};
|
|
525
|
+
setRawLogData((prev) => [...prev, remainingLine]);
|
|
526
|
+
partialLine.current = "";
|
|
357
527
|
}
|
|
358
528
|
};
|
|
359
529
|
}, [runBashCommandWithPipe]);
|
|
530
|
+
function generateArchitecture(projects) {
|
|
531
|
+
let res = "";
|
|
532
|
+
function avaiableData(project) {
|
|
533
|
+
if (project.code_available && project.logs_available) {
|
|
534
|
+
return `- codebase
|
|
535
|
+
- logs`;
|
|
536
|
+
}
|
|
537
|
+
if (project.code_available)
|
|
538
|
+
return `- codebase`;
|
|
539
|
+
if (project.logs_available)
|
|
540
|
+
return `- logs`;
|
|
541
|
+
}
|
|
542
|
+
projects.forEach((project) => {
|
|
543
|
+
res += `
|
|
544
|
+
id: ${project.window_id}
|
|
545
|
+
service_name: ${project.name}
|
|
546
|
+
service_description: ${project.description}
|
|
547
|
+
available_data:
|
|
548
|
+
${avaiableData(project)}
|
|
549
|
+
`;
|
|
550
|
+
});
|
|
551
|
+
return res;
|
|
552
|
+
}
|
|
360
553
|
async function userMessageSubmitted() {
|
|
361
554
|
if (!chatInput.trim())
|
|
362
555
|
return;
|
|
363
|
-
//
|
|
364
|
-
const
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
}
|
|
556
|
+
// const lastMessage = chatResponseMessages[chatResponseMessages.length - 1];
|
|
557
|
+
const logs = chatResponseMessages.length <= 0 ? getLast50Lines(unTamperedLogs) : "";
|
|
558
|
+
// if (lastMessage?.type === "response" && lastMessage?.data?.state) {
|
|
559
|
+
// sendQuery(chatInput, logs, lastMessage.data.state);
|
|
560
|
+
// } else if (lastMessage?.type === "ask_user" && lastMessage?.data?.state) {
|
|
561
|
+
// sendQuery(chatInput, logs, lastMessage?.data?.state);
|
|
562
|
+
// } else {
|
|
563
|
+
// sendQuery(chatInput, logs);
|
|
564
|
+
// }
|
|
565
|
+
const userMessage = new HumanMessage(chatInput);
|
|
566
|
+
const projects = await fetchProjectsFromCluster(loadProjectMetadata());
|
|
567
|
+
sendQuery([...chatResponseMessages, userMessage], generateArchitecture(projects.projects), logs, planningDoc);
|
|
368
568
|
setChatResponseMessages((prev) => [
|
|
369
569
|
...prev,
|
|
370
|
-
|
|
570
|
+
userMessage
|
|
371
571
|
]);
|
|
372
572
|
setTrimmedChats((prev) => [
|
|
373
573
|
...prev,
|
|
374
|
-
|
|
574
|
+
userMessage
|
|
375
575
|
]);
|
|
376
|
-
|
|
377
|
-
const lastMessage = chatResponseMessages[chatResponseMessages.length - 1];
|
|
378
|
-
//logs
|
|
379
|
-
const logs = getLast50Lines(currentLogDataString);
|
|
380
|
-
// Decide whether to send state or not
|
|
381
|
-
if (lastMessage?.type === "response" && lastMessage?.data?.state) {
|
|
382
|
-
sendQuery(chatInput, logs, lastMessage.data.state);
|
|
383
|
-
}
|
|
384
|
-
else if (lastMessage?.type === "ask_user" && lastMessage?.data?.state) {
|
|
385
|
-
sendQuery(chatInput, logs, lastMessage?.data?.state);
|
|
386
|
-
}
|
|
387
|
-
else {
|
|
388
|
-
sendQuery(chatInput, logs);
|
|
389
|
-
}
|
|
576
|
+
setVisibleChats(prev => [...prev, userMessage]);
|
|
390
577
|
setChatInput("");
|
|
391
578
|
}
|
|
392
579
|
useInput((inputStr, key) => {
|
|
580
|
+
ctrlPressedRef.current = key.ctrl;
|
|
393
581
|
if (inputStr === "c" && key.ctrl) {
|
|
394
582
|
exit();
|
|
395
583
|
return;
|
|
@@ -401,16 +589,21 @@ export const App = () => {
|
|
|
401
589
|
setLogScroll(0);
|
|
402
590
|
return;
|
|
403
591
|
}
|
|
592
|
+
if (key.ctrl && inputStr === "a") {
|
|
593
|
+
setVisibleChats(chatResponseMessages);
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
404
596
|
if (key.ctrl && inputStr === "k") {
|
|
405
597
|
setChatResponseMessages([]);
|
|
406
598
|
setChatScroll(0);
|
|
407
599
|
return;
|
|
408
600
|
}
|
|
409
601
|
if (key.ctrl && inputStr === "r") {
|
|
410
|
-
|
|
602
|
+
setShowControlR(false);
|
|
411
603
|
setChatResponseMessages(() => []);
|
|
412
604
|
setTrimmedChats(() => []);
|
|
413
605
|
setCompleteChatHistory(() => []);
|
|
606
|
+
setGraphState(null);
|
|
414
607
|
setSocketId[""];
|
|
415
608
|
socket?.close();
|
|
416
609
|
setIsConnected(false);
|
|
@@ -420,10 +613,13 @@ export const App = () => {
|
|
|
420
613
|
return;
|
|
421
614
|
}
|
|
422
615
|
if (key.ctrl && inputStr === "d") {
|
|
423
|
-
setMode((prev) =>
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
616
|
+
setMode((prev) => {
|
|
617
|
+
if (prev === "NORMAL")
|
|
618
|
+
return "COPY";
|
|
619
|
+
if (prev === "COPY")
|
|
620
|
+
return "LOGS";
|
|
621
|
+
return "NORMAL";
|
|
622
|
+
});
|
|
427
623
|
}
|
|
428
624
|
if (key.tab) {
|
|
429
625
|
if (activePane === "input")
|
|
@@ -457,18 +653,18 @@ export const App = () => {
|
|
|
457
653
|
return true;
|
|
458
654
|
}
|
|
459
655
|
});
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
656
|
+
const handleInputChange = useCallback((input) => {
|
|
657
|
+
if (!ctrlPressedRef.current)
|
|
658
|
+
setChatInput(input);
|
|
659
|
+
}, []);
|
|
463
660
|
if (mode === "COPY") {
|
|
464
|
-
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: "
|
|
465
|
-
|
|
466
|
-
|
|
661
|
+
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, customMessage: customMessage })) : (_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 })] }));
|
|
662
|
+
}
|
|
663
|
+
else if (mode === "LOGS") {
|
|
664
|
+
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 })] }));
|
|
467
665
|
}
|
|
468
666
|
else {
|
|
469
|
-
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: "
|
|
470
|
-
// <Text>{connectionError}a</Text>
|
|
471
|
-
), 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: setChatInput, onSubmit: userMessageSubmitted, focus: activePane === "input" })] }))] })] }), _jsx(Box, { marginTop: 1, justifyContent: "center", children: _jsxs(Text, { color: "#949494", children: ["[Tab] Switch Focus [ \u2B06 / \u2B07 ] Scroll (Keyboard Only) [Enter] Send [Ctrl+D] Open AI chat [Ctrl+C] Exit ", "\n", "[Ctrl+R] Reload AI chat"] }) })] }));
|
|
667
|
+
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, customMessage: customMessage })) : (_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 })] }));
|
|
472
668
|
}
|
|
473
669
|
};
|
|
474
670
|
render(_jsx(App, {}));
|