ideacode 1.0.4 → 1.1.0

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/config.js CHANGED
@@ -7,6 +7,9 @@ const CONFIG_DIR = process.env.XDG_CONFIG_HOME
7
7
  ? path.join(process.env.LOCALAPPDATA ?? os.homedir(), "ideacode")
8
8
  : path.join(os.homedir(), ".config", "ideacode");
9
9
  const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
10
+ export function getConfigDir() {
11
+ return CONFIG_DIR;
12
+ }
10
13
  function loadConfigFile() {
11
14
  try {
12
15
  const raw = fs.readFileSync(CONFIG_FILE, "utf-8");
@@ -0,0 +1,30 @@
1
+ import * as crypto from "node:crypto";
2
+ import * as fs from "node:fs";
3
+ import * as path from "node:path";
4
+ import { getConfigDir } from "./config.js";
5
+ function hashCwd(cwd) {
6
+ return crypto.createHash("sha256").update(cwd, "utf8").digest("hex").slice(0, 16);
7
+ }
8
+ export function getConversationPath(cwd) {
9
+ const dir = path.join(getConfigDir(), "conversations");
10
+ return path.join(dir, `${hashCwd(cwd)}.json`);
11
+ }
12
+ export function loadConversation(cwd) {
13
+ const filePath = getConversationPath(cwd);
14
+ try {
15
+ const raw = fs.readFileSync(filePath, "utf-8");
16
+ const data = JSON.parse(raw);
17
+ if (!Array.isArray(data))
18
+ return [];
19
+ return data;
20
+ }
21
+ catch {
22
+ return [];
23
+ }
24
+ }
25
+ export function saveConversation(cwd, messages) {
26
+ const filePath = getConversationPath(cwd);
27
+ const dir = path.dirname(filePath);
28
+ fs.mkdirSync(dir, { recursive: true });
29
+ fs.writeFileSync(filePath, JSON.stringify(messages), "utf-8");
30
+ }
package/dist/repl.js CHANGED
@@ -10,11 +10,12 @@ import gradient from "gradient-string";
10
10
  // Custom matcha-themed gradient: matcha green → dark sepia
11
11
  const matchaGradient = gradient(["#7F9A65", "#5C4033"]);
12
12
  import { getModel, saveModel, saveBraveSearchApiKey, getBraveSearchApiKey } from "./config.js";
13
+ import { loadConversation, saveConversation } from "./conversation.js";
13
14
  import { callApi, fetchModels } from "./api.js";
14
15
  import { estimateTokens, ensureUnderBudget } from "./context.js";
15
16
  import { runTool } from "./tools/index.js";
16
17
  import { COMMANDS, matchCommand, resolveCommand } from "./commands.js";
17
- import { colors, icons, separator, agentMessage, toolCallBox, toolResultLine, inkColors, } from "./ui/index.js";
18
+ import { colors, icons, separator, agentMessage, toolCallBox, toolResultLine, userPromptBox, inkColors, } from "./ui/index.js";
18
19
  function wordStartBackward(value, cursor) {
19
20
  let i = cursor - 1;
20
21
  while (i >= 0 && /\s/.test(value[i]))
@@ -83,6 +84,46 @@ function wrapLine(line, width) {
83
84
  }
84
85
  return out.length > 0 ? out : [""];
85
86
  }
87
+ function replayMessagesToLogLines(messages) {
88
+ const lines = [];
89
+ for (let i = 0; i < messages.length; i++) {
90
+ const msg = messages[i];
91
+ if (msg.role === "user") {
92
+ if (typeof msg.content === "string") {
93
+ lines.push(...userPromptBox(msg.content).split("\n"), "");
94
+ }
95
+ else if (Array.isArray(msg.content)) {
96
+ const prev = messages[i - 1];
97
+ const toolResults = msg.content;
98
+ if (prev?.role === "assistant" && Array.isArray(prev.content)) {
99
+ const blocks = prev.content;
100
+ const toolUses = blocks.filter((b) => b.type === "tool_use");
101
+ for (const tr of toolResults) {
102
+ const block = toolUses.find((b) => b.id === tr.tool_use_id);
103
+ if (block?.name) {
104
+ const firstVal = block.input && typeof block.input === "object" ? Object.values(block.input)[0] : undefined;
105
+ const argPreview = String(firstVal ?? "").slice(0, 50);
106
+ const ok = !(tr.content ?? "").startsWith("error:");
107
+ lines.push(toolCallBox(block.name, argPreview, ok));
108
+ const preview = (tr.content ?? "").split("\n")[0]?.slice(0, 60) ?? "";
109
+ lines.push(toolResultLine(preview, ok));
110
+ }
111
+ }
112
+ }
113
+ }
114
+ }
115
+ else if (msg.role === "assistant" && Array.isArray(msg.content)) {
116
+ const blocks = msg.content;
117
+ for (const block of blocks) {
118
+ if (block.type === "text" && block.text) {
119
+ lines.push("");
120
+ lines.push(...agentMessage(block.text).trimEnd().split("\n"));
121
+ }
122
+ }
123
+ }
124
+ }
125
+ return lines;
126
+ }
86
127
  function useTerminalSize() {
87
128
  const { stdout } = useStdout();
88
129
  const [size, setSize] = useState(() => ({
@@ -124,7 +165,45 @@ export function Repl({ apiKey, cwd, onQuit }) {
124
165
  });
125
166
  const [inputValue, setInputValue] = useState("");
126
167
  const [currentModel, setCurrentModel] = useState(getModel);
127
- const [messages, setMessages] = useState([]);
168
+ const [messages, setMessages] = useState(() => loadConversation(cwd));
169
+ const messagesRef = useRef(messages);
170
+ const hasRestoredLogRef = useRef(false);
171
+ useEffect(() => {
172
+ messagesRef.current = messages;
173
+ }, [messages]);
174
+ useEffect(() => {
175
+ if (messages.length > 0 && !hasRestoredLogRef.current) {
176
+ hasRestoredLogRef.current = true;
177
+ const model = getModel();
178
+ const banner = [
179
+ "",
180
+ matchaGradient(bigLogo),
181
+ colors.accent(` ${model}`) + colors.dim(" · ") + colors.accentPale("OpenRouter") + colors.dim(` · ${cwd}`),
182
+ colors.mutedDark(" / commands ! shell @ files · Ctrl+P palette · Ctrl+C or /q to quit"),
183
+ "",
184
+ ];
185
+ setLogLines([...banner, ...replayMessagesToLogLines(messages)]);
186
+ }
187
+ }, [messages, cwd]);
188
+ const saveDebounceRef = useRef(null);
189
+ useEffect(() => {
190
+ if (saveDebounceRef.current)
191
+ clearTimeout(saveDebounceRef.current);
192
+ saveDebounceRef.current = setTimeout(() => {
193
+ saveDebounceRef.current = null;
194
+ saveConversation(cwd, messages);
195
+ }, 500);
196
+ return () => {
197
+ if (saveDebounceRef.current)
198
+ clearTimeout(saveDebounceRef.current);
199
+ };
200
+ }, [cwd, messages]);
201
+ const handleQuit = useCallback(() => {
202
+ if (saveDebounceRef.current)
203
+ clearTimeout(saveDebounceRef.current);
204
+ saveConversation(cwd, messagesRef.current);
205
+ onQuit();
206
+ }, [cwd, onQuit]);
128
207
  const [loading, setLoading] = useState(false);
129
208
  const [showPalette, setShowPalette] = useState(false);
130
209
  const [paletteIndex, setPaletteIndex] = useState(0);
@@ -140,7 +219,6 @@ export function Repl({ apiKey, cwd, onQuit }) {
140
219
  const skipNextSubmitRef = useRef(false);
141
220
  const queuedMessageRef = useRef(null);
142
221
  const lastUserMessageRef = useRef("");
143
- const [lastUserPrompt, setLastUserPrompt] = useState("");
144
222
  const [logScrollOffset, setLogScrollOffset] = useState(0);
145
223
  const prevEscRef = useRef(false);
146
224
  const [spinnerTick, setSpinnerTick] = useState(0);
@@ -303,7 +381,8 @@ export function Repl({ apiKey, cwd, onQuit }) {
303
381
  setLogScrollOffset(0);
304
382
  }
305
383
  lastUserMessageRef.current = userInput;
306
- setLastUserPrompt(userInput);
384
+ appendLog(userPromptBox(userInput));
385
+ appendLog("");
307
386
  let state = [...messages, { role: "user", content: userInput }];
308
387
  const systemPrompt = `Concise coding assistant. cwd: ${cwd}. 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.`;
309
388
  const modelContext = modelList.find((m) => m.id === currentModel)?.context_length;
@@ -388,7 +467,7 @@ export function Repl({ apiKey, cwd, onQuit }) {
388
467
  try {
389
468
  const cont = await processInput(value);
390
469
  if (!cont) {
391
- onQuit();
470
+ handleQuit();
392
471
  return;
393
472
  }
394
473
  const queued = queuedMessageRef.current;
@@ -401,7 +480,7 @@ export function Repl({ apiKey, cwd, onQuit }) {
401
480
  appendLog(colors.error(`${icons.error} ${err instanceof Error ? err.message : String(err)}`));
402
481
  appendLog("");
403
482
  }
404
- }, [processInput, onQuit, appendLog, openModelSelector, openBraveKeyModal, openHelpModal]);
483
+ }, [processInput, handleQuit, appendLog, openModelSelector, openBraveKeyModal, openHelpModal]);
405
484
  useInput((input, key) => {
406
485
  if (showHelpModal) {
407
486
  setShowHelpModal(false);
@@ -471,7 +550,7 @@ export function Repl({ apiKey, cwd, onQuit }) {
471
550
  setShowPalette(false);
472
551
  processInput(selected.cmd).then((cont) => {
473
552
  if (!cont)
474
- onQuit();
553
+ handleQuit();
475
554
  }).catch((err) => {
476
555
  appendLog(colors.error(`${icons.error} ${err instanceof Error ? err.message : String(err)}`));
477
556
  appendLog("");
@@ -522,7 +601,7 @@ export function Repl({ apiKey, cwd, onQuit }) {
522
601
  }
523
602
  processInput(selected.cmd).then((cont) => {
524
603
  if (!cont)
525
- onQuit();
604
+ handleQuit();
526
605
  }).catch((err) => {
527
606
  appendLog(colors.error(`${icons.error} ${err instanceof Error ? err.message : String(err)}`));
528
607
  appendLog("");
@@ -718,7 +797,7 @@ export function Repl({ apiKey, cwd, onQuit }) {
718
797
  setShowPalette(true);
719
798
  }
720
799
  if (key.ctrl && input === "c") {
721
- onQuit();
800
+ handleQuit();
722
801
  }
723
802
  });
724
803
  if (showModelSelector) {
@@ -747,11 +826,7 @@ export function Repl({ apiKey, cwd, onQuit }) {
747
826
  const lines = inputValue.split("\n");
748
827
  return lines.reduce((sum, line) => sum + Math.max(1, Math.ceil(line.length / wrapWidth)), 0);
749
828
  })();
750
- const lastPromptLineCount = lastUserPrompt ? lastUserPrompt.split("\n").length : 0;
751
- const lastPromptLines = lastUserPrompt
752
- ? (lastPromptLineCount > 3 ? 4 : lastPromptLineCount)
753
- : 0;
754
- const reservedLines = 1 + lastPromptLines + inputLineCount + (loading ? 2 : 1);
829
+ const reservedLines = 1 + inputLineCount + (loading ? 2 : 1);
755
830
  const logViewportHeight = Math.max(1, termRows - reservedLines - suggestionBoxLines);
756
831
  const maxLogScrollOffset = Math.max(0, logLines.length - logViewportHeight);
757
832
  const logStartIndex = Math.max(0, logLines.length - logViewportHeight - Math.min(logScrollOffset, maxLogScrollOffset));
@@ -776,12 +851,7 @@ export function Repl({ apiKey, cwd, onQuit }) {
776
851
  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: "gray", children: [" \u2014 ", c.desc] })] }, c.cmd))), _jsxs(Text, { color: paletteIndex === COMMANDS.length ? inkColors.primary : undefined, children: [paletteIndex === COMMANDS.length ? "› " : " ", "Cancel (Esc)"] }), _jsx(Text, { color: "gray", children: " \u2191/\u2193 select, Enter confirm, Esc close " })] })] }), _jsx(Box, { flexGrow: 1 })] }));
777
852
  }
778
853
  const footerLines = suggestionBoxLines + 1 + inputLineCount;
779
- return (_jsxs(Box, { flexDirection: "column", height: termRows, overflow: "hidden", children: [_jsxs(Box, { flexDirection: "column", flexGrow: 1, minHeight: 0, overflow: "hidden", children: [lastUserPrompt ? (_jsx(Box, { flexDirection: "column", children: (() => {
780
- const lines = lastUserPrompt.split("\n");
781
- const showLines = lines.slice(0, 3);
782
- const hasMore = lines.length > 3;
783
- return (_jsxs(_Fragment, { children: [_jsxs(Box, { flexDirection: "row", children: [_jsxs(Text, { color: inkColors.primary, dimColor: true, children: [icons.prompt, " Last:", " "] }), _jsx(Text, { dimColor: true, color: "gray", children: showLines[0] ?? "" })] }), showLines.slice(1).map((ln, i) => (_jsx(Box, { flexDirection: "row", paddingLeft: 8, children: _jsx(Text, { dimColor: true, color: "gray", children: ln }) }, i))), hasMore && (_jsx(Box, { flexDirection: "row", paddingLeft: 8, children: _jsx(Text, { dimColor: true, color: "gray", children: "\u2026" }) }))] }));
784
- })() })) : null, _jsx(Box, { flexDirection: "column", height: logViewportHeight, overflow: "hidden", children: visibleLogLines.map((line, i) => (_jsx(Text, { children: line === "" ? "\u00A0" : line }, logLines.length - visibleLogLines.length + i))) }), loading && (_jsx(Box, { flexDirection: "row", marginTop: 1, marginBottom: 0, children: _jsxs(Text, { color: "gray", children: [" ", SPINNER[spinnerTick % SPINNER.length], " Thinking\u2026"] }) }))] }), _jsxs(Box, { flexDirection: "column", flexShrink: 0, height: footerLines, children: [showSlashSuggestions && (_jsxs(Box, { flexDirection: "column", marginBottom: 0, paddingLeft: 2, borderStyle: "single", borderColor: "gray", children: [filteredSlashCommands.length === 0 ? (_jsx(Text, { color: "gray", children: " No match " })) : ([...filteredSlashCommands].reverse().map((c, rev) => {
854
+ 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 }, logLines.length - visibleLogLines.length + i))) }), loading && (_jsx(Box, { flexDirection: "row", marginTop: 1, marginBottom: 0, children: _jsxs(Text, { color: "gray", children: [" ", SPINNER[spinnerTick % SPINNER.length], " Thinking\u2026"] }) }))] }), _jsxs(Box, { flexDirection: "column", flexShrink: 0, height: footerLines, children: [showSlashSuggestions && (_jsxs(Box, { flexDirection: "column", marginBottom: 0, paddingLeft: 2, borderStyle: "single", borderColor: "gray", children: [filteredSlashCommands.length === 0 ? (_jsx(Text, { color: "gray", children: " No match " })) : ([...filteredSlashCommands].reverse().map((c, rev) => {
785
855
  const i = filteredSlashCommands.length - 1 - rev;
786
856
  return (_jsxs(Text, { color: i === clampedSlashIndex ? inkColors.primary : undefined, children: [i === clampedSlashIndex ? "› " : " ", c.cmd, _jsxs(Text, { color: "gray", children: [" \u2014 ", c.desc] })] }, c.cmd));
787
857
  })), _jsx(Text, { color: "gray", children: " Commands (\u2191/\u2193 select, Enter run, Esc clear) " })] })), cursorInAtSegment && !showSlashSuggestions && (_jsxs(Box, { flexDirection: "column", marginBottom: 0, paddingLeft: 2, borderStyle: "single", borderColor: "gray", children: [filteredFilePaths.length === 0 ? (_jsxs(Text, { color: "gray", children: [" ", hasCharsAfterAt ? "No match" : "Type to search files", " "] })) : ([...filteredFilePaths].reverse().map((p, rev) => {
package/dist/ui/format.js CHANGED
@@ -40,6 +40,41 @@ export function header(title, subtitle) {
40
40
  borderStyle: "round",
41
41
  });
42
42
  }
43
+ function wrapToWidth(text, width) {
44
+ const lines = [];
45
+ for (const line of text.split("\n")) {
46
+ if (line.length <= width) {
47
+ lines.push(line);
48
+ continue;
49
+ }
50
+ let rest = line;
51
+ while (rest.length > 0) {
52
+ if (rest.length <= width) {
53
+ lines.push(rest);
54
+ break;
55
+ }
56
+ const chunk = rest.slice(0, width);
57
+ const lastSpace = chunk.lastIndexOf(" ");
58
+ const breakAt = lastSpace > width >> 1 ? lastSpace : width;
59
+ lines.push(rest.slice(0, breakAt).trimEnd());
60
+ rest = rest.slice(breakAt).trimStart();
61
+ }
62
+ }
63
+ return lines.join("\n");
64
+ }
65
+ export function userPromptBox(prompt) {
66
+ const cols = process.stdout.columns ?? 80;
67
+ const boxWidth = Math.max(20, cols - 4);
68
+ const innerWidth = boxWidth - 2 - 2;
69
+ const text = wrapToWidth((prompt.trim() || "\u00A0"), innerWidth);
70
+ return boxen(text, {
71
+ width: boxWidth,
72
+ padding: { top: 0, bottom: 0, left: 1, right: 1 },
73
+ margin: { bottom: 0 },
74
+ borderColor: inkColors.primary,
75
+ borderStyle: "round",
76
+ });
77
+ }
43
78
  const TOOL_INDENT = " ";
44
79
  const toolSubdued = chalk.hex("#3d3d3d");
45
80
  export function toolCallBox(toolName, argPreview, success = true) {
package/dist/ui/index.js CHANGED
@@ -1 +1 @@
1
- export { colors, icons, theme, inkColors, separator, agentMessage, toolCallBox, toolResultLine, bashOutputLine, renderMarkdown, header, } from "./format.js";
1
+ export { colors, icons, theme, inkColors, separator, agentMessage, toolCallBox, toolResultLine, userPromptBox, bashOutputLine, renderMarkdown, header, } from "./format.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ideacode",
3
- "version": "1.0.4",
3
+ "version": "1.1.0",
4
4
  "description": "CLI TUI for AI agents via OpenRouter — agentic loop, tools, markdown",
5
5
  "type": "module",
6
6
  "repository": {