ideacode 1.2.0 → 1.2.2
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 +82 -25
- package/dist/tools/file.js +9 -3
- package/dist/ui/format.js +5 -2
- package/package.json +1 -1
package/dist/repl.js
CHANGED
|
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
2
2
|
/**
|
|
3
3
|
* Main REPL UI: input, log viewport, slash/@ suggestions, modals (model picker, palette), API loop and tool dispatch.
|
|
4
4
|
*/
|
|
5
|
-
import { useState, useCallback, useRef, useMemo, useEffect } from "react";
|
|
5
|
+
import React, { useState, useCallback, useRef, useMemo, useEffect } from "react";
|
|
6
6
|
import { Box, Text, useInput, useStdout } from "ink";
|
|
7
7
|
import { globSync } from "glob";
|
|
8
8
|
import * as path from "node:path";
|
|
@@ -39,7 +39,7 @@ const MAX_AT_SUGGESTIONS = 12;
|
|
|
39
39
|
const INITIAL_BANNER_LINES = 12;
|
|
40
40
|
const ENABLE_PARALLEL_TOOL_CALLS = process.env.IDEACODE_PARALLEL_TOOL_CALLS !== "0";
|
|
41
41
|
const PARALLEL_SAFE_TOOLS = new Set(["read", "glob", "grep", "web_fetch", "web_search"]);
|
|
42
|
-
const LOADING_TICK_MS =
|
|
42
|
+
const LOADING_TICK_MS = 90;
|
|
43
43
|
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.)";
|
|
44
44
|
function truncateToolResult(content) {
|
|
45
45
|
if (content.length <= MAX_TOOL_RESULT_CHARS)
|
|
@@ -62,6 +62,42 @@ function listFilesWithFilter(cwd, filter) {
|
|
|
62
62
|
return [];
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
|
+
function summarizeBashCommand(cmdRaw) {
|
|
66
|
+
const fragments = cmdRaw
|
|
67
|
+
.split(/\n|&&|;/g)
|
|
68
|
+
.map((s) => s.trim())
|
|
69
|
+
.filter(Boolean);
|
|
70
|
+
const useful = fragments.filter((f) => !/^echo\s+["']?[=\-#]/i.test(f) &&
|
|
71
|
+
!/^cat\s+<<\s*['"]?EOF/i.test(f) &&
|
|
72
|
+
f.toUpperCase() !== "EOF");
|
|
73
|
+
const source = useful.length > 0 ? useful : fragments;
|
|
74
|
+
const shown = source.slice(0, 3);
|
|
75
|
+
const suffix = source.length > 3 ? ` ; … +${source.length - 3}` : "";
|
|
76
|
+
const joined = shown.join(" ; ") + suffix;
|
|
77
|
+
return joined.slice(0, 140);
|
|
78
|
+
}
|
|
79
|
+
function toolArgPreview(toolName, toolArgs) {
|
|
80
|
+
if (toolName === "bash") {
|
|
81
|
+
const cmd = String(toolArgs.cmd ?? "").trim();
|
|
82
|
+
return cmd ? summarizeBashCommand(cmd) : "—";
|
|
83
|
+
}
|
|
84
|
+
if ("path" in toolArgs && typeof toolArgs.path === "string") {
|
|
85
|
+
return toolArgs.path.slice(0, 140) || "—";
|
|
86
|
+
}
|
|
87
|
+
const firstVal = Object.values(toolArgs)[0];
|
|
88
|
+
return String(firstVal ?? "").slice(0, 140) || "—";
|
|
89
|
+
}
|
|
90
|
+
function parseEditDelta(result) {
|
|
91
|
+
const both = result.match(/ok\s*\(\+(\d+)\s*-(\d+)\)/i);
|
|
92
|
+
if (both) {
|
|
93
|
+
return { added: Number.parseInt(both[1] ?? "0", 10), removed: Number.parseInt(both[2] ?? "0", 10) };
|
|
94
|
+
}
|
|
95
|
+
const addOnly = result.match(/ok\s*\(\+(\d+)\)/i);
|
|
96
|
+
if (addOnly) {
|
|
97
|
+
return { added: Number.parseInt(addOnly[1] ?? "0", 10), removed: 0 };
|
|
98
|
+
}
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
65
101
|
function parseAtSegments(value) {
|
|
66
102
|
const segments = [];
|
|
67
103
|
let pos = 0;
|
|
@@ -115,11 +151,13 @@ function replayMessagesToLogLines(messages) {
|
|
|
115
151
|
const block = toolUses.find((b) => b.id === tr.tool_use_id);
|
|
116
152
|
if (block?.name) {
|
|
117
153
|
const name = block.name.trim().toLowerCase();
|
|
118
|
-
const
|
|
119
|
-
|
|
154
|
+
const args = block.input && typeof block.input === "object"
|
|
155
|
+
? block.input
|
|
156
|
+
: {};
|
|
157
|
+
const argPreview = toolArgPreview(name, args).slice(0, 60);
|
|
120
158
|
const content = tr.content ?? "";
|
|
121
159
|
const ok = !content.startsWith("error:");
|
|
122
|
-
lines.push(toolCallBox(name, argPreview, ok));
|
|
160
|
+
lines.push(toolCallBox(name, argPreview, ok, 0, name === "edit" || name === "write" ? parseEditDelta(content) : undefined));
|
|
123
161
|
const tokens = estimateTokensForString(content);
|
|
124
162
|
lines.push(toolResultTokenLine(tokens, ok));
|
|
125
163
|
}
|
|
@@ -158,6 +196,37 @@ function useTerminalSize() {
|
|
|
158
196
|
}, [stdout]);
|
|
159
197
|
return size;
|
|
160
198
|
}
|
|
199
|
+
function shimmerLabel(label, frame) {
|
|
200
|
+
if (!label)
|
|
201
|
+
return "";
|
|
202
|
+
const width = Math.max(4, Math.min(10, Math.floor(label.length / 3)));
|
|
203
|
+
const travel = Math.max(1, label.length - width);
|
|
204
|
+
const period = travel * 2;
|
|
205
|
+
const phase = frame % period;
|
|
206
|
+
const head = phase <= travel ? phase : period - phase;
|
|
207
|
+
let out = "";
|
|
208
|
+
for (let i = 0; i < label.length; i++) {
|
|
209
|
+
const inWindow = i >= head && i < head + width;
|
|
210
|
+
out += inWindow ? colors.gray(label[i] ?? "") : colors.mutedDark(label[i] ?? "");
|
|
211
|
+
}
|
|
212
|
+
return out;
|
|
213
|
+
}
|
|
214
|
+
const LoadingStatus = React.memo(function LoadingStatus({ active, label, }) {
|
|
215
|
+
const [frame, setFrame] = useState(0);
|
|
216
|
+
const [startedAt, setStartedAt] = useState(0);
|
|
217
|
+
useEffect(() => {
|
|
218
|
+
if (!active)
|
|
219
|
+
return;
|
|
220
|
+
setStartedAt(Date.now());
|
|
221
|
+
setFrame(0);
|
|
222
|
+
const t = setInterval(() => setFrame((n) => n + 1), LOADING_TICK_MS);
|
|
223
|
+
return () => clearInterval(t);
|
|
224
|
+
}, [active, label]);
|
|
225
|
+
if (!active)
|
|
226
|
+
return _jsx(Text, { color: inkColors.textSecondary, children: "\u00A0" });
|
|
227
|
+
const elapsedSec = Math.max(0, (Date.now() - startedAt) / 1000);
|
|
228
|
+
return (_jsxs(Text, { color: inkColors.textSecondary, children: [" ", shimmerLabel(label, frame), " ", colors.gray(`${elapsedSec.toFixed(1)}s`)] }));
|
|
229
|
+
});
|
|
161
230
|
export function Repl({ apiKey, cwd, onQuit }) {
|
|
162
231
|
const { rows: termRows, columns: termColumns } = useTerminalSize();
|
|
163
232
|
// Big ASCII art logo for ideacode
|
|
@@ -239,6 +308,7 @@ export function Repl({ apiKey, cwd, onQuit }) {
|
|
|
239
308
|
onQuit();
|
|
240
309
|
}, [cwd, onQuit]);
|
|
241
310
|
const [loading, setLoading] = useState(false);
|
|
311
|
+
const [loadingLabel, setLoadingLabel] = useState("Thinking…");
|
|
242
312
|
const [showPalette, setShowPalette] = useState(false);
|
|
243
313
|
const [paletteIndex, setPaletteIndex] = useState(0);
|
|
244
314
|
const [showModelSelector, setShowModelSelector] = useState(false);
|
|
@@ -262,9 +332,6 @@ export function Repl({ apiKey, cwd, onQuit }) {
|
|
|
262
332
|
process.stdout.write("\x1b[?1006l\x1b[?1000l");
|
|
263
333
|
};
|
|
264
334
|
}, []);
|
|
265
|
-
const [spinnerTick, setSpinnerTick] = useState(0);
|
|
266
|
-
const loadingStartedAtRef = useRef(0);
|
|
267
|
-
const SPINNER = ["●○○", "○●○", "○○●", "○●○"];
|
|
268
335
|
const estimatedTokens = useMemo(() => estimateTokens(messages, undefined), [messages]);
|
|
269
336
|
const contextWindowK = useMemo(() => {
|
|
270
337
|
const ctx = modelList.find((m) => m.id === currentModel)?.context_length;
|
|
@@ -295,14 +362,6 @@ export function Repl({ apiKey, cwd, onQuit }) {
|
|
|
295
362
|
if (showModelSelector && filteredModelList.length > 0)
|
|
296
363
|
setModelIndex((i) => Math.min(i, filteredModelList.length - 1));
|
|
297
364
|
}, [showModelSelector, filteredModelList.length]);
|
|
298
|
-
useEffect(() => {
|
|
299
|
-
if (!loading)
|
|
300
|
-
return;
|
|
301
|
-
loadingStartedAtRef.current = Date.now();
|
|
302
|
-
setSpinnerTick(0);
|
|
303
|
-
const t = setInterval(() => setSpinnerTick((n) => n + 1), LOADING_TICK_MS);
|
|
304
|
-
return () => clearInterval(t);
|
|
305
|
-
}, [loading]);
|
|
306
365
|
const showSlashSuggestions = inputValue.startsWith("/");
|
|
307
366
|
const filteredSlashCommands = useMemo(() => {
|
|
308
367
|
const filter = inputValue.slice(1).trim();
|
|
@@ -445,10 +504,12 @@ export function Repl({ apiKey, cwd, onQuit }) {
|
|
|
445
504
|
appendLog(userPromptBox(userInput));
|
|
446
505
|
appendLog("");
|
|
447
506
|
let state = [...messages, { role: "user", content: userInput }];
|
|
448
|
-
const systemPrompt = `Concise coding assistant. cwd: ${cwd}. PRIORITIZE grep to locate; then read with offset and limit to fetch only relevant sections. Do not read whole files unless the user explicitly asks. Use focused greps (specific patterns, narrow paths) and read in chunks when files are large; avoid one huge grep or read that floods context. When exploring a dependency, set path to that package (e.g. node_modules/<pkg>) and list/read only what you need. Prefer grep or keyword search for the most recent or specific occurrence; avoid tail/read of thousands of lines. If a tool result says it was truncated, call the tool again with offset, limit, or a narrower pattern to get what you need. Use as many parallel read/search/web tool calls as needed in one turn when they are independent (often more than 3 is appropriate for broad research), but keep each call high-signal, non-redundant, and minimal in output size.`;
|
|
507
|
+
const systemPrompt = `Concise coding assistant. cwd: ${cwd}. PRIORITIZE grep to locate; then read with offset and limit to fetch only relevant sections. Do not read whole files unless the user explicitly asks. Use focused greps (specific patterns, narrow paths) and read in chunks when files are large; avoid one huge grep or read that floods context. When exploring a dependency, set path to that package (e.g. node_modules/<pkg>) and list/read only what you need. Prefer grep or keyword search for the most recent or specific occurrence; avoid tail/read of thousands of lines. If a tool result says it was truncated, call the tool again with offset, limit, or a narrower pattern to get what you need. Use as many parallel read/search/web tool calls as needed in one turn when they are independent (often more than 3 is appropriate for broad research), but keep each call high-signal, non-redundant, and minimal in output size. For bash tool calls, avoid decorative echo headers; run direct commands and keep commands concise.`;
|
|
449
508
|
const modelContext = modelList.find((m) => m.id === currentModel)?.context_length;
|
|
450
509
|
const maxContextTokens = Math.floor((modelContext ?? CONTEXT_WINDOW_K * 1024) * 0.85);
|
|
451
510
|
const stateBeforeCompress = state;
|
|
511
|
+
setLoadingLabel("Compressing context…");
|
|
512
|
+
setLoading(true);
|
|
452
513
|
state = await ensureUnderBudget(apiKey, state, systemPrompt, currentModel, {
|
|
453
514
|
maxTokens: maxContextTokens,
|
|
454
515
|
keepLast: 6,
|
|
@@ -457,13 +518,13 @@ export function Repl({ apiKey, cwd, onQuit }) {
|
|
|
457
518
|
appendLog(colors.muted(" (context compressed to stay under limit)\n"));
|
|
458
519
|
}
|
|
459
520
|
for (;;) {
|
|
460
|
-
|
|
521
|
+
setLoadingLabel("Thinking…");
|
|
461
522
|
const response = await callApi(apiKey, state, systemPrompt, currentModel);
|
|
462
523
|
const contentBlocks = response.content ?? [];
|
|
463
524
|
const toolResults = [];
|
|
464
525
|
const renderToolOutcome = (planned, result, extraIndent = 0) => {
|
|
465
526
|
const ok = !result.startsWith("error:");
|
|
466
|
-
appendLog(toolCallBox(planned.toolName, planned.argPreview, ok, extraIndent));
|
|
527
|
+
appendLog(toolCallBox(planned.toolName, planned.argPreview, ok, extraIndent, planned.toolName === "edit" || planned.toolName === "write" ? parseEditDelta(result) : undefined));
|
|
467
528
|
const contentForApi = truncateToolResult(result);
|
|
468
529
|
const tokens = estimateTokensForString(contentForApi);
|
|
469
530
|
appendLog(toolResultTokenLine(tokens, ok, extraIndent));
|
|
@@ -501,12 +562,11 @@ export function Repl({ apiKey, cwd, onQuit }) {
|
|
|
501
562
|
continue;
|
|
502
563
|
const toolName = block.name.trim().toLowerCase();
|
|
503
564
|
const toolArgs = block.input;
|
|
504
|
-
const firstVal = Object.values(toolArgs)[0];
|
|
505
565
|
const planned = {
|
|
506
566
|
block,
|
|
507
567
|
toolName,
|
|
508
568
|
toolArgs,
|
|
509
|
-
argPreview:
|
|
569
|
+
argPreview: toolArgPreview(toolName, toolArgs),
|
|
510
570
|
};
|
|
511
571
|
if (ENABLE_PARALLEL_TOOL_CALLS && PARALLEL_SAFE_TOOLS.has(toolName)) {
|
|
512
572
|
parallelBatch.push(planned);
|
|
@@ -984,10 +1044,7 @@ export function Repl({ apiKey, cwd, onQuit }) {
|
|
|
984
1044
|
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 })] }));
|
|
985
1045
|
}
|
|
986
1046
|
const footerLines = suggestionBoxLines + 1 + inputLineCount;
|
|
987
|
-
return (_jsxs(Box, { flexDirection: "column", height: termRows, overflow: "hidden", children: [_jsxs(Box, { flexDirection: "column", flexGrow: 1, minHeight: 0, overflow: "hidden", children: [_jsx(Box, { flexDirection: "column", height: logViewportHeight, overflow: "hidden", children: visibleLogLines.map((line, i) => (_jsx(Text, { children: line === "" ? "\u00A0" : line }, effectiveLogLines.length - visibleLogLines.length + i))) }), _jsx(Box, { flexDirection: "row", marginTop: 1, marginBottom: 0, children: _jsxs(Text, { color: inkColors.textSecondary, children:
|
|
988
|
-
? `${SPINNER[spinnerTick % SPINNER.length]} Thinking… ${((Date.now() - loadingStartedAtRef.current) /
|
|
989
|
-
1000).toFixed(1)}s`
|
|
990
|
-
: "\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) => {
|
|
1047
|
+
return (_jsxs(Box, { flexDirection: "column", height: termRows, overflow: "hidden", children: [_jsxs(Box, { flexDirection: "column", flexGrow: 1, minHeight: 0, overflow: "hidden", children: [_jsx(Box, { flexDirection: "column", height: logViewportHeight, overflow: "hidden", children: visibleLogLines.map((line, i) => (_jsx(Text, { children: line === "" ? "\u00A0" : line }, effectiveLogLines.length - visibleLogLines.length + i))) }), _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) => {
|
|
991
1048
|
const i = filteredSlashCommands.length - 1 - rev;
|
|
992
1049
|
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));
|
|
993
1050
|
})), _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/dist/tools/file.js
CHANGED
|
@@ -19,8 +19,10 @@ export function readFile(args) {
|
|
|
19
19
|
suffix);
|
|
20
20
|
}
|
|
21
21
|
export function writeFile(args) {
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
const content = args.content;
|
|
23
|
+
fs.writeFileSync(args.path, content, "utf-8");
|
|
24
|
+
const added = content === "" ? 0 : content.split("\n").length;
|
|
25
|
+
return `ok (+${added})`;
|
|
24
26
|
}
|
|
25
27
|
export function editFile(args) {
|
|
26
28
|
const text = fs.readFileSync(args.path, "utf-8");
|
|
@@ -39,5 +41,9 @@ export function editFile(args) {
|
|
|
39
41
|
}
|
|
40
42
|
const replacement = args.all ? text.split(oldStr).join(newStr) : text.replace(oldStr, newStr);
|
|
41
43
|
fs.writeFileSync(args.path, replacement, "utf-8");
|
|
42
|
-
|
|
44
|
+
const oldLines = oldStr === "" ? 0 : oldStr.split("\n").length;
|
|
45
|
+
const newLines = newStr === "" ? 0 : newStr.split("\n").length;
|
|
46
|
+
const removed = oldLines * count;
|
|
47
|
+
const added = newLines * count;
|
|
48
|
+
return `ok (+${added} -${removed})`;
|
|
43
49
|
}
|
package/dist/ui/format.js
CHANGED
|
@@ -308,14 +308,17 @@ export function userPromptBox(prompt) {
|
|
|
308
308
|
}
|
|
309
309
|
const TOOL_INDENT = " ";
|
|
310
310
|
const toolSubdued = chalk.gray;
|
|
311
|
-
export function toolCallBox(toolName, argPreview, success = true, extraIndent = 0) {
|
|
311
|
+
export function toolCallBox(toolName, argPreview, success = true, extraIndent = 0, editDelta) {
|
|
312
312
|
const indent = TOOL_INDENT + " ".repeat(Math.max(0, extraIndent));
|
|
313
313
|
const diamondColor = success ? colors.toolSuccess : colors.toolFail;
|
|
314
314
|
const nameColor = diamondColor;
|
|
315
315
|
const argColor = success ? toolSubdued : colors.toolFail;
|
|
316
316
|
const parenColor = colors.mutedDark;
|
|
317
317
|
const name = " " + toolName.trim().toLowerCase();
|
|
318
|
-
|
|
318
|
+
const delta = editDelta != null
|
|
319
|
+
? ` ${colors.toolSuccess(`+${editDelta.added}`)} ${colors.toolFail(`-${editDelta.removed}`)}`
|
|
320
|
+
: "";
|
|
321
|
+
return `${indent}${diamondColor(icons.tool)}${nameColor(name)}${parenColor("(")}${argColor(argPreview)}${parenColor(")")}${delta}`;
|
|
319
322
|
}
|
|
320
323
|
export function toolResultLine(preview, success = true) {
|
|
321
324
|
const pipeColor = success ? toolSubdued : colors.toolFail;
|