ideacode 1.2.1 → 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 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 = Math.min(300, Math.max(80, Number.parseInt(process.env.IDEACODE_SPINNER_MS ?? "110", 10) || 110));
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 firstVal = block.input && typeof block.input === "object" ? Object.values(block.input)[0] : undefined;
119
- const argPreview = String(firstVal ?? "").slice(0, 50) || "—";
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
@@ -263,9 +332,6 @@ export function Repl({ apiKey, cwd, onQuit }) {
263
332
  process.stdout.write("\x1b[?1006l\x1b[?1000l");
264
333
  };
265
334
  }, []);
266
- const [spinnerTick, setSpinnerTick] = useState(0);
267
- const loadingStartedAtRef = useRef(0);
268
- const SPINNER = ["●○○", "○●○", "○○●", "○●○"];
269
335
  const estimatedTokens = useMemo(() => estimateTokens(messages, undefined), [messages]);
270
336
  const contextWindowK = useMemo(() => {
271
337
  const ctx = modelList.find((m) => m.id === currentModel)?.context_length;
@@ -296,14 +362,6 @@ export function Repl({ apiKey, cwd, onQuit }) {
296
362
  if (showModelSelector && filteredModelList.length > 0)
297
363
  setModelIndex((i) => Math.min(i, filteredModelList.length - 1));
298
364
  }, [showModelSelector, filteredModelList.length]);
299
- useEffect(() => {
300
- if (!loading)
301
- return;
302
- loadingStartedAtRef.current = Date.now();
303
- setSpinnerTick(0);
304
- const t = setInterval(() => setSpinnerTick((n) => n + 1), LOADING_TICK_MS);
305
- return () => clearInterval(t);
306
- }, [loading]);
307
365
  const showSlashSuggestions = inputValue.startsWith("/");
308
366
  const filteredSlashCommands = useMemo(() => {
309
367
  const filter = inputValue.slice(1).trim();
@@ -446,7 +504,7 @@ export function Repl({ apiKey, cwd, onQuit }) {
446
504
  appendLog(userPromptBox(userInput));
447
505
  appendLog("");
448
506
  let state = [...messages, { role: "user", content: userInput }];
449
- 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.`;
450
508
  const modelContext = modelList.find((m) => m.id === currentModel)?.context_length;
451
509
  const maxContextTokens = Math.floor((modelContext ?? CONTEXT_WINDOW_K * 1024) * 0.85);
452
510
  const stateBeforeCompress = state;
@@ -466,7 +524,7 @@ export function Repl({ apiKey, cwd, onQuit }) {
466
524
  const toolResults = [];
467
525
  const renderToolOutcome = (planned, result, extraIndent = 0) => {
468
526
  const ok = !result.startsWith("error:");
469
- 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));
470
528
  const contentForApi = truncateToolResult(result);
471
529
  const tokens = estimateTokensForString(contentForApi);
472
530
  appendLog(toolResultTokenLine(tokens, ok, extraIndent));
@@ -504,12 +562,11 @@ export function Repl({ apiKey, cwd, onQuit }) {
504
562
  continue;
505
563
  const toolName = block.name.trim().toLowerCase();
506
564
  const toolArgs = block.input;
507
- const firstVal = Object.values(toolArgs)[0];
508
565
  const planned = {
509
566
  block,
510
567
  toolName,
511
568
  toolArgs,
512
- argPreview: String(firstVal ?? "").slice(0, 100) || "—",
569
+ argPreview: toolArgPreview(toolName, toolArgs),
513
570
  };
514
571
  if (ENABLE_PARALLEL_TOOL_CALLS && PARALLEL_SAFE_TOOLS.has(toolName)) {
515
572
  parallelBatch.push(planned);
@@ -987,10 +1044,7 @@ export function Repl({ apiKey, cwd, onQuit }) {
987
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 })] }));
988
1045
  }
989
1046
  const footerLines = suggestionBoxLines + 1 + inputLineCount;
990
- 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: [" ", loading
991
- ? `${SPINNER[spinnerTick % SPINNER.length]} ${loadingLabel} ${((Date.now() - loadingStartedAtRef.current) /
992
- 1000).toFixed(1)}s`
993
- : "\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) => {
994
1048
  const i = filteredSlashCommands.length - 1 - rev;
995
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));
996
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) => {
@@ -19,8 +19,10 @@ export function readFile(args) {
19
19
  suffix);
20
20
  }
21
21
  export function writeFile(args) {
22
- fs.writeFileSync(args.path, args.content, "utf-8");
23
- return "ok";
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
- return "ok";
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
- return `${indent}${diamondColor(icons.tool)}${nameColor(name)}${parenColor("(")}${argColor(argPreview)}${parenColor(")")}`;
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ideacode",
3
- "version": "1.2.1",
3
+ "version": "1.2.2",
4
4
  "description": "CLI TUI for AI agents via OpenRouter — agentic loop, tools, markdown",
5
5
  "type": "module",
6
6
  "repository": {