ideacode 1.2.3 → 1.2.5

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 (2) hide show
  1. package/dist/repl.js +121 -36
  2. package/package.json +1 -1
package/dist/repl.js CHANGED
@@ -41,6 +41,7 @@ const INITIAL_BANNER_LINES = 12;
41
41
  const ENABLE_PARALLEL_TOOL_CALLS = process.env.IDEACODE_PARALLEL_TOOL_CALLS !== "0";
42
42
  const PARALLEL_SAFE_TOOLS = new Set(["read", "glob", "grep", "web_fetch", "web_search"]);
43
43
  const LOADING_TICK_MS = 80;
44
+ const MAX_EMPTY_ASSISTANT_RETRIES = 3;
44
45
  const TRUNCATE_NOTE = "\n\n(Output truncated to save context. Use read with offset/limit, grep with a specific pattern, or tail with fewer lines to get more.)";
45
46
  function truncateToolResult(content) {
46
47
  if (content.length <= MAX_TOOL_RESULT_CHARS)
@@ -63,11 +64,57 @@ function listFilesWithFilter(cwd, filter) {
63
64
  return [];
64
65
  }
65
66
  }
67
+ function stripHeredocBodies(cmdRaw) {
68
+ const lines = cmdRaw.replace(/\r\n/g, "\n").split("\n");
69
+ const out = [];
70
+ let i = 0;
71
+ while (i < lines.length) {
72
+ const line = lines[i] ?? "";
73
+ out.push(line);
74
+ const markerMatch = line.match(/<<-?\s*(['"]?)([A-Za-z_][A-Za-z0-9_]*)\1/);
75
+ if (!markerMatch) {
76
+ i += 1;
77
+ continue;
78
+ }
79
+ const marker = markerMatch[2] ?? "";
80
+ i += 1;
81
+ while (i < lines.length) {
82
+ const bodyLine = lines[i] ?? "";
83
+ if (bodyLine.trim() === marker) {
84
+ out.push(bodyLine);
85
+ break;
86
+ }
87
+ i += 1;
88
+ }
89
+ i += 1;
90
+ }
91
+ return out.join("\n");
92
+ }
66
93
  function summarizeBashCommand(cmdRaw) {
67
- const parts = cmdRaw
68
- .split(/\n|&&|;|\|/g)
94
+ const sanitized = stripHeredocBodies(cmdRaw);
95
+ const parts = sanitized
96
+ .split(/\n|&&|\|\||;|\|/g)
69
97
  .map((s) => s.trim())
70
98
  .filter(Boolean);
99
+ const skipTokens = new Set([
100
+ "if",
101
+ "then",
102
+ "else",
103
+ "elif",
104
+ "fi",
105
+ "for",
106
+ "while",
107
+ "do",
108
+ "done",
109
+ "case",
110
+ "esac",
111
+ "in",
112
+ "function",
113
+ "{",
114
+ "}",
115
+ "(",
116
+ ")",
117
+ ]);
71
118
  const commands = [];
72
119
  for (const part of parts) {
73
120
  let s = part.replace(/^\(+/, "").trim();
@@ -84,9 +131,11 @@ function summarizeBashCommand(cmdRaw) {
84
131
  }
85
132
  if (!s)
86
133
  continue;
87
- const token = s.split(/\s+/)[0]?.replace(/^['"]|['"]$/g, "") ?? "";
134
+ const token = (s.split(/\s+/)[0] ?? "").replace(/^['"]|['"]$/g, "").toLowerCase();
88
135
  if (!/^[A-Za-z0-9_./-]+$/.test(token))
89
136
  continue;
137
+ if (skipTokens.has(token))
138
+ continue;
90
139
  if (token === "echo")
91
140
  continue;
92
141
  if (token === "cat" && /<<\s*['"]?EOF/i.test(s))
@@ -232,30 +281,6 @@ function orbitDots(frame) {
232
281
  .map((ch, i) => (i === activeIndex ? colors.gray(ch) : colors.mutedDark(ch)))
233
282
  .join("");
234
283
  }
235
- const LoadingStatus = React.memo(function LoadingStatus({ active, label, }) {
236
- const [frame, setFrame] = useState(0);
237
- const startedAtRef = useRef(null);
238
- useEffect(() => {
239
- if (!active) {
240
- startedAtRef.current = null;
241
- return;
242
- }
243
- if (startedAtRef.current == null) {
244
- startedAtRef.current = Date.now();
245
- setFrame(0);
246
- }
247
- const anim = setInterval(() => setFrame((n) => n + 1), LOADING_TICK_MS);
248
- return () => {
249
- clearInterval(anim);
250
- };
251
- }, [active]);
252
- if (!active)
253
- return _jsx(Text, { color: inkColors.textSecondary, children: "\u00A0" });
254
- const startedAt = startedAtRef.current ?? Date.now();
255
- const elapsedSeconds = Math.max(0, (Date.now() - startedAt) / 1000);
256
- const elapsedText = elapsedSeconds < 10 ? `${elapsedSeconds.toFixed(1)}s` : `${Math.floor(elapsedSeconds)}s`;
257
- return (_jsxs(Text, { color: inkColors.textSecondary, children: [" ", orbitDots(frame), " ", colors.gray(label), " ", colors.gray(elapsedText)] }));
258
- });
259
284
  export function Repl({ apiKey, cwd, onQuit }) {
260
285
  const { rows: termRows, columns: termColumns } = useTerminalSize();
261
286
  // Big ASCII art logo for ideacode
@@ -354,7 +379,11 @@ export function Repl({ apiKey, cwd, onQuit }) {
354
379
  }, [cwd, onQuit]);
355
380
  const [loading, setLoading] = useState(false);
356
381
  const [loadingLabel, setLoadingLabel] = useState("Thinking…");
357
- const [cursorBlinkOn, setCursorBlinkOn] = useState(true);
382
+ const loadingActiveRef = useRef(false);
383
+ const loadingLabelRef = useRef(loadingLabel);
384
+ const loadingFooterLinesRef = useRef(2);
385
+ const loadingRenderRef = useRef(null);
386
+ const cursorBlinkOn = true;
358
387
  const [showPalette, setShowPalette] = useState(false);
359
388
  const [paletteIndex, setPaletteIndex] = useState(0);
360
389
  const [showModelSelector, setShowModelSelector] = useState(false);
@@ -380,14 +409,54 @@ export function Repl({ apiKey, cwd, onQuit }) {
380
409
  };
381
410
  }, []);
382
411
  useEffect(() => {
383
- setCursorBlinkOn(true);
384
- if (loading)
412
+ loadingActiveRef.current = loading;
413
+ loadingLabelRef.current = loadingLabel;
414
+ if (!process.stdout.isTTY)
385
415
  return;
386
- const timer = setInterval(() => {
387
- setCursorBlinkOn((prev) => !prev);
388
- }, 520);
389
- return () => clearInterval(timer);
390
- }, [loading, inputValue, inputCursor]);
416
+ const clearLoadingLine = () => {
417
+ const up = Math.max(1, loadingFooterLinesRef.current);
418
+ try {
419
+ writeSync(process.stdout.fd, `\x1b7\x1b[${up}A\r\x1b[2K\x1b8`);
420
+ }
421
+ catch {
422
+ // Best effort only.
423
+ }
424
+ };
425
+ if (!loading) {
426
+ if (loadingRenderRef.current) {
427
+ clearInterval(loadingRenderRef.current);
428
+ loadingRenderRef.current = null;
429
+ }
430
+ clearLoadingLine();
431
+ return;
432
+ }
433
+ const startedAt = Date.now();
434
+ let frame = 0;
435
+ const renderTick = () => {
436
+ if (!loadingActiveRef.current || !process.stdout.isTTY)
437
+ return;
438
+ const elapsedSeconds = Math.max(0, (Date.now() - startedAt) / 1000);
439
+ const elapsedText = elapsedSeconds < 10 ? `${elapsedSeconds.toFixed(1)}s` : `${Math.floor(elapsedSeconds)}s`;
440
+ const line = ` ${orbitDots(frame)} ${colors.gray(loadingLabelRef.current)} ${colors.gray(elapsedText)}`;
441
+ const up = Math.max(1, loadingFooterLinesRef.current);
442
+ try {
443
+ writeSync(process.stdout.fd, `\x1b7\x1b[${up}A\r\x1b[2K${line}\x1b8`);
444
+ }
445
+ catch {
446
+ // Best effort only.
447
+ }
448
+ frame = (frame + 1) % 6;
449
+ };
450
+ renderTick();
451
+ loadingRenderRef.current = setInterval(renderTick, LOADING_TICK_MS);
452
+ return () => {
453
+ if (loadingRenderRef.current) {
454
+ clearInterval(loadingRenderRef.current);
455
+ loadingRenderRef.current = null;
456
+ }
457
+ clearLoadingLine();
458
+ };
459
+ }, [loading, loadingLabel]);
391
460
  const estimatedTokens = useMemo(() => estimateTokens(messages, undefined), [messages]);
392
461
  const contextWindowK = useMemo(() => {
393
462
  const ctx = modelList.find((m) => m.id === currentModel)?.context_length;
@@ -588,6 +657,7 @@ export function Repl({ apiKey, cwd, onQuit }) {
588
657
  appendLog(colors.muted(" (context compressed to stay under limit)\n"));
589
658
  }
590
659
  setLoadingLabel("Thinking…");
660
+ let emptyAssistantRetries = 0;
591
661
  for (;;) {
592
662
  setLoading(true);
593
663
  setLoadingLabel("Thinking…");
@@ -597,6 +667,20 @@ export function Repl({ apiKey, cwd, onQuit }) {
597
667
  },
598
668
  });
599
669
  const contentBlocks = response.content ?? [];
670
+ const hasMeaningfulAssistantOutput = contentBlocks.some((block) => block.type === "tool_use" || (block.type === "text" && !!block.text?.trim()));
671
+ if (!hasMeaningfulAssistantOutput) {
672
+ emptyAssistantRetries += 1;
673
+ if (emptyAssistantRetries <= MAX_EMPTY_ASSISTANT_RETRIES) {
674
+ setLoadingLabel(`No output yet, retrying ${emptyAssistantRetries}/${MAX_EMPTY_ASSISTANT_RETRIES}…`);
675
+ appendLog(colors.muted(` ${icons.tool} model returned an empty turn, retrying (${emptyAssistantRetries}/${MAX_EMPTY_ASSISTANT_RETRIES})…`));
676
+ continue;
677
+ }
678
+ appendLog(colors.error(`${icons.error} model returned empty output repeatedly. Stopping this turn; you can submit "continue" to resume.`));
679
+ appendLog("");
680
+ setMessages(state);
681
+ break;
682
+ }
683
+ emptyAssistantRetries = 0;
600
684
  const toolResults = [];
601
685
  const renderToolOutcome = (planned, result, extraIndent = 0) => {
602
686
  const ok = !result.startsWith("error:");
@@ -1118,7 +1202,8 @@ export function Repl({ apiKey, cwd, onQuit }) {
1118
1202
  return (_jsxs(Box, { flexDirection: "column", height: termRows, overflow: "hidden", children: [_jsx(Box, { height: topPad }), _jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { width: leftPad }), _jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: inkColors.primary, paddingX: 2, paddingY: 1, width: paletteModalWidth, minHeight: paletteModalHeight, children: [_jsx(Text, { bold: true, children: " Command palette " }), COMMANDS.map((c, i) => (_jsxs(Text, { color: i === paletteIndex ? inkColors.primary : undefined, children: [i === paletteIndex ? "› " : " ", c.cmd, _jsxs(Text, { color: inkColors.textSecondary, children: [" \u2014 ", c.desc] })] }, c.cmd))), _jsxs(Text, { color: paletteIndex === COMMANDS.length ? inkColors.primary : undefined, children: [paletteIndex === COMMANDS.length ? "› " : " ", "Cancel (Esc)"] }), _jsx(Text, { color: inkColors.textSecondary, children: " \u2191/\u2193 select, Enter confirm, Esc close " })] })] }), _jsx(Box, { flexGrow: 1 })] }));
1119
1203
  }
1120
1204
  const footerLines = suggestionBoxLines + 1 + stableInputLineCount;
1121
- return (_jsxs(Box, { flexDirection: "column", height: termRows, overflow: "hidden", children: [_jsxs(Box, { flexDirection: "column", flexGrow: 1, minHeight: 0, overflow: "hidden", children: [_jsx(LogViewport, { lines: visibleLogLines, startIndex: logStartIndex, height: logViewportHeight }), _jsx(Box, { flexDirection: "row", marginTop: 1, marginBottom: 0, children: _jsx(LoadingStatus, { active: loading, label: loadingLabel }) })] }), _jsxs(Box, { flexDirection: "column", flexShrink: 0, height: footerLines, children: [showSlashSuggestions && (_jsxs(Box, { flexDirection: "column", marginBottom: 0, paddingLeft: 2, borderStyle: "single", borderColor: inkColors.textDisabled, children: [filteredSlashCommands.length === 0 ? (_jsx(Text, { color: inkColors.textSecondary, children: " No match " })) : ([...filteredSlashCommands].reverse().map((c, rev) => {
1205
+ loadingFooterLinesRef.current = footerLines;
1206
+ return (_jsxs(Box, { flexDirection: "column", height: termRows, overflow: "hidden", children: [_jsxs(Box, { flexDirection: "column", flexGrow: 1, minHeight: 0, overflow: "hidden", children: [_jsx(LogViewport, { lines: visibleLogLines, startIndex: logStartIndex, height: logViewportHeight }), _jsx(Box, { flexDirection: "row", marginTop: 1, marginBottom: 0, children: _jsx(Text, { color: inkColors.textSecondary, children: "\u00A0" }) })] }), _jsxs(Box, { flexDirection: "column", flexShrink: 0, height: footerLines, children: [showSlashSuggestions && (_jsxs(Box, { flexDirection: "column", marginBottom: 0, paddingLeft: 2, borderStyle: "single", borderColor: inkColors.textDisabled, children: [filteredSlashCommands.length === 0 ? (_jsx(Text, { color: inkColors.textSecondary, children: " No match " })) : ([...filteredSlashCommands].reverse().map((c, rev) => {
1122
1207
  const i = filteredSlashCommands.length - 1 - rev;
1123
1208
  return (_jsxs(Text, { color: i === clampedSlashIndex ? inkColors.primary : undefined, children: [i === clampedSlashIndex ? "› " : " ", c.cmd, _jsxs(Text, { color: inkColors.textSecondary, children: [" \u2014 ", c.desc] })] }, c.cmd));
1124
1209
  })), _jsx(Text, { color: inkColors.textSecondary, children: " Commands (\u2191/\u2193 select, Enter run, Esc clear) " })] })), cursorInAtSegment && !showSlashSuggestions && (_jsxs(Box, { flexDirection: "column", marginBottom: 0, paddingLeft: 2, borderStyle: "single", borderColor: inkColors.textDisabled, children: [filteredFilePaths.length === 0 ? (_jsxs(Text, { color: inkColors.textSecondary, children: [" ", hasCharsAfterAt ? "No match" : "Type to search files", " "] })) : ([...filteredFilePaths].reverse().map((p, rev) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ideacode",
3
- "version": "1.2.3",
3
+ "version": "1.2.5",
4
4
  "description": "CLI TUI for AI agents via OpenRouter — agentic loop, tools, markdown",
5
5
  "type": "module",
6
6
  "repository": {