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/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: false,
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
- // borderStyle="round"
165
- 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] }));
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, setChatResponseMessages, isConnected, isLoading, setIsLoading, setTrimmedChats, API_KEY, connectionError, setSocketId, setIsConnected, socket, setIsControlRPressed, setShowControlR, showControlR, setCompleteChatHistory, } = useWebSocket(config.websocket_url);
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 "aaa";
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
- if (chatResponseMessages.length === 0)
278
+ // Use visibleChats for UI-related checks
279
+ const messagesToCheck = visibleChats || chatResponseMessages;
280
+ if (messagesToCheck.length === 0)
226
281
  return;
227
- const lastMessage = chatResponseMessages[chatResponseMessages.length - 1];
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 (lastMessage?.type === "response" ||
230
- lastMessage?.type === "progress" ||
231
- lastMessage?.type === "ask_user" ||
232
- lastMessage?.type === "error") {
233
- // Delay to ensure auto-scroll animation completes first (500ms scroll + 100ms buffer)
234
- setTimeout(() => setActivePane("chat"), 600);
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
- if (lastMessage?.type === "response") {
237
- setShowControlR(true);
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
- else {
240
- setShowControlR(false);
325
+ function extratMessageContent(messages) {
326
+ return messages.reduce((prev, message) => {
327
+ return prev + message.content;
328
+ }, "");
241
329
  }
242
- }, [chatResponseMessages]);
243
- const chatLinesChat = useMemo(() => {
244
- // Calculate available width for chat (half terminal width minus borders and padding)
245
- const availableWidth = Math.floor(terminalCols / 2) - 6; // Adjust as needed
246
- return chatResponseMessages.flatMap((msg, index) => {
247
- // Create a label depending on the message type
248
- const prefix = `${msg.type === "user" ? "🧑" : "🤖"} `;
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
- //progress event chat responses
251
- // Extract message content
252
- const extracted = extractAIMessages(msg);
253
- const content = msg.type === "progress"
254
- ? Array.isArray(extracted)
255
- ? extracted.join("\n") // nicely formatted output
256
- : extracted || ""
257
- : msg.message ||
258
- msg?.data?.finalMessage ||
259
- msg?.data?.message ||
260
- JSON.stringify(msg, null, 2);
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
- const contentLines = content?.split("\n");
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 user messages for readability
273
- if (msg.type === "user") {
274
- lines.push({
275
- key: `chat-${index}-spacer`,
276
- text: " ",
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
- }, [chatResponseMessages, terminalCols]);
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
- const logLines = useMemo(() => {
284
- const availableWidth = Math.floor(terminalCols / 2) - 6;
285
- return rawLogData.map((line) => {
286
- // Expand tabs to spaces (assuming 8-space tabs)
287
- const expandedText = line.text.replace(/\t/g, " ".repeat(8));
288
- // Truncate lines that are too wide, properly handling ANSI codes
289
- const width = stringWidth(expandedText);
290
- if (width > availableWidth) {
291
- const truncated = sliceAnsi(expandedText, 0, availableWidth - 3);
292
- return {
293
- key: line.key,
294
- text: truncated + "...",
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
- return {
298
- key: line.key,
299
- text: expandedText,
300
- };
301
- });
302
- }, [rawLogData, terminalCols]);
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 || "bash";
305
- const cols = Math.floor(terminalCols / 2) - 6; // Match the BorderBox width
306
- const rows = LOGS_HEIGHT - 2;
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 () => ptyProcess.kill();
341
- }, [terminalCols, LOGS_HEIGHT]);
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
- if (partialLine.current.length > 0) {
350
- const remainingLine = {
351
- key: `log-${logKeyCounter.current++}`,
352
- text: partialLine.current,
353
- };
354
- setRawLogData((prevLines) => [...prevLines, remainingLine]);
355
- partialLine.current = "";
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
- //send message to socket connection
364
- const userMessage = {
365
- type: "user",
366
- message: chatInput,
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
- JSON.parse(JSON.stringify(userMessage)),
570
+ userMessage
371
571
  ]);
372
572
  setTrimmedChats((prev) => [
373
573
  ...prev,
374
- JSON.parse(JSON.stringify(userMessage)),
574
+ userMessage
375
575
  ]);
376
- //last response message
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
- setIsControlRPressed(() => true);
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) => (prev === "COPY" ? "NORMAL" : "COPY"));
424
- setChatInput("");
425
- setActivePane("chat");
426
- return;
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
- // const text = dedent`
461
- // **Run your Ink application**:
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: "clippy 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" })
465
- // <Text>{connectionError}a</Text>
466
- ), 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"] }) })] }));
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: "clippy 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" })
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, {}));