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.
- package/dist/repl.js +121 -36
- 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
|
|
68
|
-
|
|
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]
|
|
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
|
|
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
|
-
|
|
384
|
-
|
|
412
|
+
loadingActiveRef.current = loading;
|
|
413
|
+
loadingLabelRef.current = loadingLabel;
|
|
414
|
+
if (!process.stdout.isTTY)
|
|
385
415
|
return;
|
|
386
|
-
const
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
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
|
-
|
|
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) => {
|