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 +3 -0
- package/dist/conversation.js +30 -0
- package/dist/repl.js +90 -20
- package/dist/ui/format.js +35 -0
- package/dist/ui/index.js +1 -1
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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: [
|
|
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";
|