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 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
@@ -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
- setLoading(true);
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: String(firstVal ?? "").slice(0, 100) || "—",
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: [" ", loading
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) => {
@@ -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.0",
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": {