ideacode 1.2.3 → 1.2.4

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 +68 -13
  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))
@@ -354,7 +403,7 @@ export function Repl({ apiKey, cwd, onQuit }) {
354
403
  }, [cwd, onQuit]);
355
404
  const [loading, setLoading] = useState(false);
356
405
  const [loadingLabel, setLoadingLabel] = useState("Thinking…");
357
- const [cursorBlinkOn, setCursorBlinkOn] = useState(true);
406
+ const cursorBlinkOn = true;
358
407
  const [showPalette, setShowPalette] = useState(false);
359
408
  const [paletteIndex, setPaletteIndex] = useState(0);
360
409
  const [showModelSelector, setShowModelSelector] = useState(false);
@@ -379,15 +428,6 @@ export function Repl({ apiKey, cwd, onQuit }) {
379
428
  process.stdout.write("\x1b[?1006l\x1b[?1000l");
380
429
  };
381
430
  }, []);
382
- useEffect(() => {
383
- setCursorBlinkOn(true);
384
- if (loading)
385
- return;
386
- const timer = setInterval(() => {
387
- setCursorBlinkOn((prev) => !prev);
388
- }, 520);
389
- return () => clearInterval(timer);
390
- }, [loading, inputValue, inputCursor]);
391
431
  const estimatedTokens = useMemo(() => estimateTokens(messages, undefined), [messages]);
392
432
  const contextWindowK = useMemo(() => {
393
433
  const ctx = modelList.find((m) => m.id === currentModel)?.context_length;
@@ -588,6 +628,7 @@ export function Repl({ apiKey, cwd, onQuit }) {
588
628
  appendLog(colors.muted(" (context compressed to stay under limit)\n"));
589
629
  }
590
630
  setLoadingLabel("Thinking…");
631
+ let emptyAssistantRetries = 0;
591
632
  for (;;) {
592
633
  setLoading(true);
593
634
  setLoadingLabel("Thinking…");
@@ -597,6 +638,20 @@ export function Repl({ apiKey, cwd, onQuit }) {
597
638
  },
598
639
  });
599
640
  const contentBlocks = response.content ?? [];
641
+ const hasMeaningfulAssistantOutput = contentBlocks.some((block) => block.type === "tool_use" || (block.type === "text" && !!block.text?.trim()));
642
+ if (!hasMeaningfulAssistantOutput) {
643
+ emptyAssistantRetries += 1;
644
+ if (emptyAssistantRetries <= MAX_EMPTY_ASSISTANT_RETRIES) {
645
+ setLoadingLabel(`No output yet, retrying ${emptyAssistantRetries}/${MAX_EMPTY_ASSISTANT_RETRIES}…`);
646
+ appendLog(colors.muted(` ${icons.tool} model returned an empty turn, retrying (${emptyAssistantRetries}/${MAX_EMPTY_ASSISTANT_RETRIES})…`));
647
+ continue;
648
+ }
649
+ appendLog(colors.error(`${icons.error} model returned empty output repeatedly. Stopping this turn; you can submit "continue" to resume.`));
650
+ appendLog("");
651
+ setMessages(state);
652
+ break;
653
+ }
654
+ emptyAssistantRetries = 0;
600
655
  const toolResults = [];
601
656
  const renderToolOutcome = (planned, result, extraIndent = 0) => {
602
657
  const ok = !result.startsWith("error:");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ideacode",
3
- "version": "1.2.3",
3
+ "version": "1.2.4",
4
4
  "description": "CLI TUI for AI agents via OpenRouter — agentic loop, tools, markdown",
5
5
  "type": "module",
6
6
  "repository": {