ideacode 1.2.1 → 1.2.3
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/api.js +27 -12
- package/dist/context.js +59 -9
- package/dist/index.js +40 -2
- package/dist/repl.js +220 -40
- package/dist/tools/file.js +9 -3
- package/dist/ui/format.js +5 -2
- package/package.json +1 -1
package/dist/api.js
CHANGED
|
@@ -9,12 +9,25 @@ export async function fetchModels(apiKey) {
|
|
|
9
9
|
const json = (await res.json());
|
|
10
10
|
return json.data ?? [];
|
|
11
11
|
}
|
|
12
|
-
const MAX_RETRIES =
|
|
13
|
-
const INITIAL_BACKOFF_MS =
|
|
12
|
+
const MAX_RETRIES = 8;
|
|
13
|
+
const INITIAL_BACKOFF_MS = 1200;
|
|
14
|
+
const MAX_BACKOFF_MS = 30_000;
|
|
15
|
+
const RETRYABLE_STATUS = new Set([408, 429, 500, 502, 503, 504]);
|
|
14
16
|
function sleep(ms) {
|
|
15
17
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
16
18
|
}
|
|
17
|
-
|
|
19
|
+
function computeRetryDelayMs(attempt, retryAfterHeader) {
|
|
20
|
+
if (retryAfterHeader) {
|
|
21
|
+
const retrySeconds = Number.parseFloat(retryAfterHeader);
|
|
22
|
+
if (Number.isFinite(retrySeconds) && retrySeconds > 0) {
|
|
23
|
+
return Math.max(1000, Math.min(MAX_BACKOFF_MS, Math.round(retrySeconds * 1000)));
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
const base = Math.min(MAX_BACKOFF_MS, INITIAL_BACKOFF_MS * Math.pow(2, attempt));
|
|
27
|
+
const jitter = 0.75 + Math.random() * 0.5; // 0.75x .. 1.25x
|
|
28
|
+
return Math.max(1000, Math.round(base * jitter));
|
|
29
|
+
}
|
|
30
|
+
export async function callApi(apiKey, messages, systemPrompt, model, callbacks) {
|
|
18
31
|
const body = {
|
|
19
32
|
model,
|
|
20
33
|
max_tokens: 8192,
|
|
@@ -36,11 +49,15 @@ export async function callApi(apiKey, messages, systemPrompt, model) {
|
|
|
36
49
|
return res.json();
|
|
37
50
|
const text = await res.text();
|
|
38
51
|
lastError = new Error(`API ${res.status}: ${text}`);
|
|
39
|
-
if ((res.status
|
|
52
|
+
if (RETRYABLE_STATUS.has(res.status) && attempt < MAX_RETRIES) {
|
|
40
53
|
const retryAfter = res.headers.get("retry-after");
|
|
41
|
-
const waitMs = retryAfter
|
|
42
|
-
|
|
43
|
-
:
|
|
54
|
+
const waitMs = computeRetryDelayMs(attempt, retryAfter);
|
|
55
|
+
callbacks?.onRetry?.({
|
|
56
|
+
attempt: attempt + 1,
|
|
57
|
+
maxAttempts: MAX_RETRIES + 1,
|
|
58
|
+
waitMs,
|
|
59
|
+
status: res.status,
|
|
60
|
+
});
|
|
44
61
|
await sleep(waitMs);
|
|
45
62
|
continue;
|
|
46
63
|
}
|
|
@@ -187,7 +204,7 @@ export async function callApiStream(apiKey, messages, systemPrompt, model, callb
|
|
|
187
204
|
await flushRemainingToolCalls();
|
|
188
205
|
return finish();
|
|
189
206
|
}
|
|
190
|
-
const SUMMARIZE_SYSTEM = "You are a summarizer.
|
|
207
|
+
const SUMMARIZE_SYSTEM = "You are a summarizer. Compress the conversation while preserving fidelity. Output plain text with these exact sections: Goal, Constraints, Decisions, Open Questions, Next Actions, Critical Facts. Keep literals (paths, model ids, command names, env vars, URLs, numbers, error messages) whenever available. Include key tool results in concise form. Do not add preamble or commentary.";
|
|
191
208
|
export async function callSummarize(apiKey, messages, model) {
|
|
192
209
|
const body = {
|
|
193
210
|
model,
|
|
@@ -213,11 +230,9 @@ export async function callSummarize(apiKey, messages, model) {
|
|
|
213
230
|
}
|
|
214
231
|
const text = await res.text();
|
|
215
232
|
lastError = new Error(`Summarize API ${res.status}: ${text}`);
|
|
216
|
-
if ((res.status
|
|
233
|
+
if (RETRYABLE_STATUS.has(res.status) && attempt < MAX_RETRIES) {
|
|
217
234
|
const retryAfter = res.headers.get("retry-after");
|
|
218
|
-
const waitMs = retryAfter
|
|
219
|
-
? Math.max(1000, parseInt(retryAfter, 10) * 1000)
|
|
220
|
-
: INITIAL_BACKOFF_MS * Math.pow(2, attempt);
|
|
235
|
+
const waitMs = computeRetryDelayMs(attempt, retryAfter);
|
|
221
236
|
await sleep(waitMs);
|
|
222
237
|
continue;
|
|
223
238
|
}
|
package/dist/context.js
CHANGED
|
@@ -1,4 +1,52 @@
|
|
|
1
1
|
import { callSummarize } from "./api.js";
|
|
2
|
+
const MAX_PINNED_FACTS = 28;
|
|
3
|
+
const MAX_PINNED_FACT_CHARS = 200;
|
|
4
|
+
function messageContentToText(content) {
|
|
5
|
+
if (typeof content === "string")
|
|
6
|
+
return content;
|
|
7
|
+
if (Array.isArray(content)) {
|
|
8
|
+
const parts = [];
|
|
9
|
+
for (const item of content) {
|
|
10
|
+
if (item && typeof item === "object") {
|
|
11
|
+
const maybe = item;
|
|
12
|
+
if (typeof maybe.content === "string")
|
|
13
|
+
parts.push(maybe.content);
|
|
14
|
+
if (maybe.type === "tool_use" && typeof maybe.name === "string") {
|
|
15
|
+
parts.push(`tool_use ${maybe.name} ${JSON.stringify(maybe.input ?? {})}`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return parts.join("\n");
|
|
20
|
+
}
|
|
21
|
+
return JSON.stringify(content);
|
|
22
|
+
}
|
|
23
|
+
function extractPinnedFacts(messages) {
|
|
24
|
+
const allLines = messages
|
|
25
|
+
.flatMap((m) => messageContentToText(m.content).split("\n"))
|
|
26
|
+
.map((l) => l.trim())
|
|
27
|
+
.filter(Boolean);
|
|
28
|
+
const facts = [];
|
|
29
|
+
const seen = new Set();
|
|
30
|
+
const importantPattern = /(\/[A-Za-z0-9._/-]{2,}|https?:\/\/\S+|\b[A-Z][A-Z0-9_]{2,}\b|\b(error|failed|must|never|always|todo|fixme|constraint)\b)/i;
|
|
31
|
+
for (const line of allLines) {
|
|
32
|
+
if (!importantPattern.test(line))
|
|
33
|
+
continue;
|
|
34
|
+
const cleaned = line.replace(/\s+/g, " ").slice(0, MAX_PINNED_FACT_CHARS);
|
|
35
|
+
if (!cleaned || seen.has(cleaned))
|
|
36
|
+
continue;
|
|
37
|
+
seen.add(cleaned);
|
|
38
|
+
facts.push(cleaned);
|
|
39
|
+
if (facts.length >= MAX_PINNED_FACTS)
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
return facts;
|
|
43
|
+
}
|
|
44
|
+
function buildSummaryEnvelope(summary, pinnedFacts) {
|
|
45
|
+
const factBlock = pinnedFacts.length > 0
|
|
46
|
+
? `Pinned facts (verbatim, highest priority):\n${pinnedFacts.map((f) => `- ${f}`).join("\n")}\n\n`
|
|
47
|
+
: "";
|
|
48
|
+
return `${factBlock}Conversation summary:\n${summary}`.trim();
|
|
49
|
+
}
|
|
2
50
|
export function estimateTokens(messages, systemPrompt) {
|
|
3
51
|
let chars = 0;
|
|
4
52
|
for (const m of messages) {
|
|
@@ -21,9 +69,10 @@ export async function compressState(apiKey, state, systemPrompt, model, options)
|
|
|
21
69
|
const toSummarize = state.slice(0, state.length - keepLast);
|
|
22
70
|
const recent = state.slice(-keepLast);
|
|
23
71
|
const summary = await callSummarize(apiKey, toSummarize, model);
|
|
72
|
+
const pinnedFacts = extractPinnedFacts(toSummarize);
|
|
24
73
|
const summaryMessage = {
|
|
25
|
-
role: "
|
|
26
|
-
content:
|
|
74
|
+
role: "assistant",
|
|
75
|
+
content: buildSummaryEnvelope(summary, pinnedFacts),
|
|
27
76
|
};
|
|
28
77
|
return [summaryMessage, ...recent];
|
|
29
78
|
}
|
|
@@ -31,12 +80,13 @@ export async function ensureUnderBudget(apiKey, state, systemPrompt, model, opti
|
|
|
31
80
|
const { maxTokens, keepLast } = options;
|
|
32
81
|
if (estimateTokens(state, systemPrompt) <= maxTokens)
|
|
33
82
|
return state;
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
83
|
+
let working = state;
|
|
84
|
+
if (working.length > keepLast) {
|
|
85
|
+
working = await compressState(apiKey, working, systemPrompt, model, { keepLast });
|
|
86
|
+
}
|
|
87
|
+
// If still over budget, trim oldest messages as a hard fallback.
|
|
88
|
+
while (working.length > 1 && estimateTokens(working, systemPrompt) > maxTokens) {
|
|
89
|
+
working = working.slice(1);
|
|
40
90
|
}
|
|
41
|
-
return
|
|
91
|
+
return working;
|
|
42
92
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,11 +1,37 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
3
|
import "dotenv/config";
|
|
4
|
+
import { writeSync } from "node:fs";
|
|
4
5
|
import { render } from "ink";
|
|
5
6
|
import { getApiKey } from "./config.js";
|
|
6
7
|
import { getVersion } from "./version.js";
|
|
7
8
|
import { runOnboarding } from "./onboarding.js";
|
|
8
9
|
import { Repl } from "./repl.js";
|
|
10
|
+
const PRINT_TRANSCRIPT_ON_EXIT = process.env.IDEACODE_TRANSCRIPT_ON_EXIT === "1";
|
|
11
|
+
const CLEAR_SCREEN_ON_EXIT = process.env.IDEACODE_CLEAR_ON_EXIT !== "0";
|
|
12
|
+
const TERMINAL_RESTORE_SEQ = "\x1b[?2004l\x1b[?1004l\x1b[?1007l\x1b[?1015l\x1b[?1006l\x1b[?1003l\x1b[?1002l\x1b[?1000l\x1b[?25h\x1b[0m";
|
|
13
|
+
function restoreTerminalState() {
|
|
14
|
+
try {
|
|
15
|
+
if (process.stdin.isTTY && typeof process.stdin.setRawMode === "function") {
|
|
16
|
+
process.stdin.setRawMode(false);
|
|
17
|
+
}
|
|
18
|
+
if (process.stdout.isTTY) {
|
|
19
|
+
writeSync(process.stdout.fd, TERMINAL_RESTORE_SEQ);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
// Ignore restore failures during process teardown.
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
process.on("exit", restoreTerminalState);
|
|
27
|
+
process.on("SIGINT", () => {
|
|
28
|
+
restoreTerminalState();
|
|
29
|
+
process.exit(130);
|
|
30
|
+
});
|
|
31
|
+
process.on("SIGTERM", () => {
|
|
32
|
+
restoreTerminalState();
|
|
33
|
+
process.exit(143);
|
|
34
|
+
});
|
|
9
35
|
async function main() {
|
|
10
36
|
const args = process.argv.slice(2);
|
|
11
37
|
if (args.includes("-v") || args.includes("--version")) {
|
|
@@ -20,7 +46,19 @@ async function main() {
|
|
|
20
46
|
if (!apiKey) {
|
|
21
47
|
process.exit(1);
|
|
22
48
|
}
|
|
23
|
-
|
|
24
|
-
|
|
49
|
+
let app;
|
|
50
|
+
let transcriptLines = [];
|
|
51
|
+
app = render(_jsx(Repl, { apiKey: apiKey, cwd: process.cwd(), onQuit: (lines) => {
|
|
52
|
+
transcriptLines = lines;
|
|
53
|
+
app.unmount();
|
|
54
|
+
} }));
|
|
55
|
+
await app.waitUntilExit();
|
|
56
|
+
if (!PRINT_TRANSCRIPT_ON_EXIT && CLEAR_SCREEN_ON_EXIT && process.stdout.isTTY) {
|
|
57
|
+
// Clear lingering in-place TUI frame from primary screen buffer.
|
|
58
|
+
writeSync(process.stdout.fd, "\x1b[2J\x1b[H");
|
|
59
|
+
}
|
|
60
|
+
if (PRINT_TRANSCRIPT_ON_EXIT && transcriptLines.length > 0) {
|
|
61
|
+
process.stdout.write("\n" + transcriptLines.join("\n") + "\n");
|
|
62
|
+
}
|
|
25
63
|
}
|
|
26
64
|
main();
|
package/dist/repl.js
CHANGED
|
@@ -2,10 +2,11 @@ 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";
|
|
9
|
+
import { writeSync } from "node:fs";
|
|
9
10
|
import gradient from "gradient-string";
|
|
10
11
|
// Custom matcha-themed gradient: matcha green → dark sepia
|
|
11
12
|
const matchaGradient = gradient(["#7F9A65", "#5C4033"]);
|
|
@@ -39,7 +40,7 @@ const MAX_AT_SUGGESTIONS = 12;
|
|
|
39
40
|
const INITIAL_BANNER_LINES = 12;
|
|
40
41
|
const ENABLE_PARALLEL_TOOL_CALLS = process.env.IDEACODE_PARALLEL_TOOL_CALLS !== "0";
|
|
41
42
|
const PARALLEL_SAFE_TOOLS = new Set(["read", "glob", "grep", "web_fetch", "web_search"]);
|
|
42
|
-
const LOADING_TICK_MS =
|
|
43
|
+
const LOADING_TICK_MS = 80;
|
|
43
44
|
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
45
|
function truncateToolResult(content) {
|
|
45
46
|
if (content.length <= MAX_TOOL_RESULT_CHARS)
|
|
@@ -62,6 +63,65 @@ function listFilesWithFilter(cwd, filter) {
|
|
|
62
63
|
return [];
|
|
63
64
|
}
|
|
64
65
|
}
|
|
66
|
+
function summarizeBashCommand(cmdRaw) {
|
|
67
|
+
const parts = cmdRaw
|
|
68
|
+
.split(/\n|&&|;|\|/g)
|
|
69
|
+
.map((s) => s.trim())
|
|
70
|
+
.filter(Boolean);
|
|
71
|
+
const commands = [];
|
|
72
|
+
for (const part of parts) {
|
|
73
|
+
let s = part.replace(/^\(+/, "").trim();
|
|
74
|
+
if (!s || s.toUpperCase() === "EOF")
|
|
75
|
+
continue;
|
|
76
|
+
// Strip simple environment assignments at the front: FOO=bar CMD
|
|
77
|
+
while (/^[A-Za-z_][A-Za-z0-9_]*=/.test(s)) {
|
|
78
|
+
const idx = s.indexOf(" ");
|
|
79
|
+
if (idx === -1) {
|
|
80
|
+
s = "";
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
s = s.slice(idx + 1).trim();
|
|
84
|
+
}
|
|
85
|
+
if (!s)
|
|
86
|
+
continue;
|
|
87
|
+
const token = s.split(/\s+/)[0]?.replace(/^['"]|['"]$/g, "") ?? "";
|
|
88
|
+
if (!/^[A-Za-z0-9_./-]+$/.test(token))
|
|
89
|
+
continue;
|
|
90
|
+
if (token === "echo")
|
|
91
|
+
continue;
|
|
92
|
+
if (token === "cat" && /<<\s*['"]?EOF/i.test(s))
|
|
93
|
+
continue;
|
|
94
|
+
if (!commands.includes(token))
|
|
95
|
+
commands.push(token);
|
|
96
|
+
}
|
|
97
|
+
if (commands.length === 0)
|
|
98
|
+
return "bash";
|
|
99
|
+
const shown = commands.slice(0, 5);
|
|
100
|
+
const suffix = commands.length > 5 ? `, +${commands.length - 5}` : "";
|
|
101
|
+
return (shown.join(", ") + suffix).slice(0, 140);
|
|
102
|
+
}
|
|
103
|
+
function toolArgPreview(toolName, toolArgs) {
|
|
104
|
+
if (toolName === "bash") {
|
|
105
|
+
const cmd = String(toolArgs.cmd ?? "").trim();
|
|
106
|
+
return cmd ? summarizeBashCommand(cmd) : "—";
|
|
107
|
+
}
|
|
108
|
+
if ("path" in toolArgs && typeof toolArgs.path === "string") {
|
|
109
|
+
return toolArgs.path.slice(0, 140) || "—";
|
|
110
|
+
}
|
|
111
|
+
const firstVal = Object.values(toolArgs)[0];
|
|
112
|
+
return String(firstVal ?? "").slice(0, 140) || "—";
|
|
113
|
+
}
|
|
114
|
+
function parseEditDelta(result) {
|
|
115
|
+
const both = result.match(/ok\s*\(\+(\d+)\s*-(\d+)\)/i);
|
|
116
|
+
if (both) {
|
|
117
|
+
return { added: Number.parseInt(both[1] ?? "0", 10), removed: Number.parseInt(both[2] ?? "0", 10) };
|
|
118
|
+
}
|
|
119
|
+
const addOnly = result.match(/ok\s*\(\+(\d+)\)/i);
|
|
120
|
+
if (addOnly) {
|
|
121
|
+
return { added: Number.parseInt(addOnly[1] ?? "0", 10), removed: 0 };
|
|
122
|
+
}
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
65
125
|
function parseAtSegments(value) {
|
|
66
126
|
const segments = [];
|
|
67
127
|
let pos = 0;
|
|
@@ -115,11 +175,13 @@ function replayMessagesToLogLines(messages) {
|
|
|
115
175
|
const block = toolUses.find((b) => b.id === tr.tool_use_id);
|
|
116
176
|
if (block?.name) {
|
|
117
177
|
const name = block.name.trim().toLowerCase();
|
|
118
|
-
const
|
|
119
|
-
|
|
178
|
+
const args = block.input && typeof block.input === "object"
|
|
179
|
+
? block.input
|
|
180
|
+
: {};
|
|
181
|
+
const argPreview = toolArgPreview(name, args).slice(0, 60);
|
|
120
182
|
const content = tr.content ?? "";
|
|
121
183
|
const ok = !content.startsWith("error:");
|
|
122
|
-
lines.push(toolCallBox(name, argPreview, ok));
|
|
184
|
+
lines.push(toolCallBox(name, argPreview, ok, 0, name === "edit" || name === "write" ? parseEditDelta(content) : undefined));
|
|
123
185
|
const tokens = estimateTokensForString(content);
|
|
124
186
|
lines.push(toolResultTokenLine(tokens, ok));
|
|
125
187
|
}
|
|
@@ -158,6 +220,42 @@ function useTerminalSize() {
|
|
|
158
220
|
}, [stdout]);
|
|
159
221
|
return size;
|
|
160
222
|
}
|
|
223
|
+
const LogViewport = React.memo(function LogViewport({ lines, startIndex, height, }) {
|
|
224
|
+
return (_jsx(Box, { flexDirection: "column", height: height, overflow: "hidden", children: lines.map((line, i) => (_jsx(Text, { children: line === "" ? "\u00A0" : line }, startIndex + i))) }));
|
|
225
|
+
});
|
|
226
|
+
function orbitDots(frame) {
|
|
227
|
+
const phase = frame % 6;
|
|
228
|
+
const activeIndex = phase <= 3 ? phase : 6 - phase;
|
|
229
|
+
const slots = ["·", "·", "·", "·"];
|
|
230
|
+
slots[activeIndex] = "●";
|
|
231
|
+
return slots
|
|
232
|
+
.map((ch, i) => (i === activeIndex ? colors.gray(ch) : colors.mutedDark(ch)))
|
|
233
|
+
.join("");
|
|
234
|
+
}
|
|
235
|
+
const LoadingStatus = React.memo(function LoadingStatus({ active, label, }) {
|
|
236
|
+
const [frame, setFrame] = useState(0);
|
|
237
|
+
const startedAtRef = useRef(null);
|
|
238
|
+
useEffect(() => {
|
|
239
|
+
if (!active) {
|
|
240
|
+
startedAtRef.current = null;
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
if (startedAtRef.current == null) {
|
|
244
|
+
startedAtRef.current = Date.now();
|
|
245
|
+
setFrame(0);
|
|
246
|
+
}
|
|
247
|
+
const anim = setInterval(() => setFrame((n) => n + 1), LOADING_TICK_MS);
|
|
248
|
+
return () => {
|
|
249
|
+
clearInterval(anim);
|
|
250
|
+
};
|
|
251
|
+
}, [active]);
|
|
252
|
+
if (!active)
|
|
253
|
+
return _jsx(Text, { color: inkColors.textSecondary, children: "\u00A0" });
|
|
254
|
+
const startedAt = startedAtRef.current ?? Date.now();
|
|
255
|
+
const elapsedSeconds = Math.max(0, (Date.now() - startedAt) / 1000);
|
|
256
|
+
const elapsedText = elapsedSeconds < 10 ? `${elapsedSeconds.toFixed(1)}s` : `${Math.floor(elapsedSeconds)}s`;
|
|
257
|
+
return (_jsxs(Text, { color: inkColors.textSecondary, children: [" ", orbitDots(frame), " ", colors.gray(label), " ", colors.gray(elapsedText)] }));
|
|
258
|
+
});
|
|
161
259
|
export function Repl({ apiKey, cwd, onQuit }) {
|
|
162
260
|
const { rows: termRows, columns: termColumns } = useTerminalSize();
|
|
163
261
|
// Big ASCII art logo for ideacode
|
|
@@ -189,6 +287,10 @@ export function Repl({ apiKey, cwd, onQuit }) {
|
|
|
189
287
|
}
|
|
190
288
|
return banner;
|
|
191
289
|
});
|
|
290
|
+
const logLinesRef = useRef(logLines);
|
|
291
|
+
useEffect(() => {
|
|
292
|
+
logLinesRef.current = logLines;
|
|
293
|
+
}, [logLines]);
|
|
192
294
|
const [inputValue, setInputValue] = useState("");
|
|
193
295
|
const [currentModel, setCurrentModel] = useState(getModel);
|
|
194
296
|
const [messages, setMessages] = useState(() => loadConversation(cwd));
|
|
@@ -236,10 +338,23 @@ export function Repl({ apiKey, cwd, onQuit }) {
|
|
|
236
338
|
if (saveDebounceRef.current)
|
|
237
339
|
clearTimeout(saveDebounceRef.current);
|
|
238
340
|
saveConversation(cwd, messagesRef.current);
|
|
239
|
-
|
|
341
|
+
// Best-effort terminal mode reset in case process exits before React cleanup runs.
|
|
342
|
+
try {
|
|
343
|
+
if (process.stdin.isTTY && typeof process.stdin.setRawMode === "function") {
|
|
344
|
+
process.stdin.setRawMode(false);
|
|
345
|
+
}
|
|
346
|
+
if (process.stdout.isTTY) {
|
|
347
|
+
writeSync(process.stdout.fd, "\x1b[?2004l\x1b[?1004l\x1b[?1007l\x1b[?1015l\x1b[?1006l\x1b[?1003l\x1b[?1002l\x1b[?1000l\x1b[?25h\x1b[0m");
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
catch {
|
|
351
|
+
// Ignore restore failures during teardown.
|
|
352
|
+
}
|
|
353
|
+
onQuit(logLinesRef.current);
|
|
240
354
|
}, [cwd, onQuit]);
|
|
241
355
|
const [loading, setLoading] = useState(false);
|
|
242
356
|
const [loadingLabel, setLoadingLabel] = useState("Thinking…");
|
|
357
|
+
const [cursorBlinkOn, setCursorBlinkOn] = useState(true);
|
|
243
358
|
const [showPalette, setShowPalette] = useState(false);
|
|
244
359
|
const [paletteIndex, setPaletteIndex] = useState(0);
|
|
245
360
|
const [showModelSelector, setShowModelSelector] = useState(false);
|
|
@@ -258,14 +373,21 @@ export function Repl({ apiKey, cwd, onQuit }) {
|
|
|
258
373
|
const scrollBoundsRef = useRef({ maxLogScrollOffset: 0, logViewportHeight: 1 });
|
|
259
374
|
const prevEscRef = useRef(false);
|
|
260
375
|
useEffect(() => {
|
|
376
|
+
// Enable SGR mouse + basic tracking so trackpad wheel scrolling works.
|
|
261
377
|
process.stdout.write("\x1b[?1006h\x1b[?1000h");
|
|
262
378
|
return () => {
|
|
263
379
|
process.stdout.write("\x1b[?1006l\x1b[?1000l");
|
|
264
380
|
};
|
|
265
381
|
}, []);
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
382
|
+
useEffect(() => {
|
|
383
|
+
setCursorBlinkOn(true);
|
|
384
|
+
if (loading)
|
|
385
|
+
return;
|
|
386
|
+
const timer = setInterval(() => {
|
|
387
|
+
setCursorBlinkOn((prev) => !prev);
|
|
388
|
+
}, 520);
|
|
389
|
+
return () => clearInterval(timer);
|
|
390
|
+
}, [loading, inputValue, inputCursor]);
|
|
269
391
|
const estimatedTokens = useMemo(() => estimateTokens(messages, undefined), [messages]);
|
|
270
392
|
const contextWindowK = useMemo(() => {
|
|
271
393
|
const ctx = modelList.find((m) => m.id === currentModel)?.context_length;
|
|
@@ -279,6 +401,20 @@ export function Repl({ apiKey, cwd, onQuit }) {
|
|
|
279
401
|
return modelList.filter((m) => m.id.toLowerCase().includes(q) ||
|
|
280
402
|
(m.name ?? "").toLowerCase().includes(q));
|
|
281
403
|
}, [modelList, modelSearchFilter]);
|
|
404
|
+
const wrapWidth = Math.max(10, termColumns - PROMPT_INDENT_LEN - 2);
|
|
405
|
+
const inputLineCount = useMemo(() => {
|
|
406
|
+
const lines = inputValue.split("\n");
|
|
407
|
+
return lines.reduce((sum, line) => sum + Math.max(1, Math.ceil(line.length / wrapWidth)), 0);
|
|
408
|
+
}, [inputValue, wrapWidth]);
|
|
409
|
+
const [stableInputLineCount, setStableInputLineCount] = useState(inputLineCount);
|
|
410
|
+
useEffect(() => {
|
|
411
|
+
if (inputLineCount <= 1) {
|
|
412
|
+
setStableInputLineCount(1);
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
const t = setTimeout(() => setStableInputLineCount(inputLineCount), 90);
|
|
416
|
+
return () => clearTimeout(t);
|
|
417
|
+
}, [inputLineCount]);
|
|
282
418
|
useEffect(() => {
|
|
283
419
|
setInputCursor((c) => Math.min(c, Math.max(0, inputValue.length)));
|
|
284
420
|
}, [inputValue.length]);
|
|
@@ -296,14 +432,6 @@ export function Repl({ apiKey, cwd, onQuit }) {
|
|
|
296
432
|
if (showModelSelector && filteredModelList.length > 0)
|
|
297
433
|
setModelIndex((i) => Math.min(i, filteredModelList.length - 1));
|
|
298
434
|
}, [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
435
|
const showSlashSuggestions = inputValue.startsWith("/");
|
|
308
436
|
const filteredSlashCommands = useMemo(() => {
|
|
309
437
|
const filter = inputValue.slice(1).trim();
|
|
@@ -446,7 +574,7 @@ export function Repl({ apiKey, cwd, onQuit }) {
|
|
|
446
574
|
appendLog(userPromptBox(userInput));
|
|
447
575
|
appendLog("");
|
|
448
576
|
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.`;
|
|
577
|
+
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
578
|
const modelContext = modelList.find((m) => m.id === currentModel)?.context_length;
|
|
451
579
|
const maxContextTokens = Math.floor((modelContext ?? CONTEXT_WINDOW_K * 1024) * 0.85);
|
|
452
580
|
const stateBeforeCompress = state;
|
|
@@ -454,19 +582,25 @@ export function Repl({ apiKey, cwd, onQuit }) {
|
|
|
454
582
|
setLoading(true);
|
|
455
583
|
state = await ensureUnderBudget(apiKey, state, systemPrompt, currentModel, {
|
|
456
584
|
maxTokens: maxContextTokens,
|
|
457
|
-
keepLast:
|
|
585
|
+
keepLast: 8,
|
|
458
586
|
});
|
|
459
587
|
if (state.length < stateBeforeCompress.length) {
|
|
460
588
|
appendLog(colors.muted(" (context compressed to stay under limit)\n"));
|
|
461
589
|
}
|
|
590
|
+
setLoadingLabel("Thinking…");
|
|
462
591
|
for (;;) {
|
|
592
|
+
setLoading(true);
|
|
463
593
|
setLoadingLabel("Thinking…");
|
|
464
|
-
const response = await callApi(apiKey, state, systemPrompt, currentModel
|
|
594
|
+
const response = await callApi(apiKey, state, systemPrompt, currentModel, {
|
|
595
|
+
onRetry: ({ attempt, maxAttempts, waitMs, status }) => {
|
|
596
|
+
setLoadingLabel(`Rate limited (${status}), retry ${attempt}/${maxAttempts} in ${(waitMs / 1000).toFixed(1)}s…`);
|
|
597
|
+
},
|
|
598
|
+
});
|
|
465
599
|
const contentBlocks = response.content ?? [];
|
|
466
600
|
const toolResults = [];
|
|
467
601
|
const renderToolOutcome = (planned, result, extraIndent = 0) => {
|
|
468
602
|
const ok = !result.startsWith("error:");
|
|
469
|
-
appendLog(toolCallBox(planned.toolName, planned.argPreview, ok, extraIndent));
|
|
603
|
+
appendLog(toolCallBox(planned.toolName, planned.argPreview, ok, extraIndent, planned.toolName === "edit" || planned.toolName === "write" ? parseEditDelta(result) : undefined));
|
|
470
604
|
const contentForApi = truncateToolResult(result);
|
|
471
605
|
const tokens = estimateTokensForString(contentForApi);
|
|
472
606
|
appendLog(toolResultTokenLine(tokens, ok, extraIndent));
|
|
@@ -477,6 +611,7 @@ export function Repl({ apiKey, cwd, onQuit }) {
|
|
|
477
611
|
const runParallelBatch = async (batch) => {
|
|
478
612
|
if (batch.length === 0)
|
|
479
613
|
return;
|
|
614
|
+
setLoadingLabel(`Running ${batch.length} tools in parallel…`);
|
|
480
615
|
const started = Date.now();
|
|
481
616
|
const groupedTools = Array.from(batch.reduce((acc, planned) => {
|
|
482
617
|
acc.set(planned.toolName, (acc.get(planned.toolName) ?? 0) + 1);
|
|
@@ -504,12 +639,11 @@ export function Repl({ apiKey, cwd, onQuit }) {
|
|
|
504
639
|
continue;
|
|
505
640
|
const toolName = block.name.trim().toLowerCase();
|
|
506
641
|
const toolArgs = block.input;
|
|
507
|
-
const firstVal = Object.values(toolArgs)[0];
|
|
508
642
|
const planned = {
|
|
509
643
|
block,
|
|
510
644
|
toolName,
|
|
511
645
|
toolArgs,
|
|
512
|
-
argPreview:
|
|
646
|
+
argPreview: toolArgPreview(toolName, toolArgs),
|
|
513
647
|
};
|
|
514
648
|
if (ENABLE_PARALLEL_TOOL_CALLS && PARALLEL_SAFE_TOOLS.has(toolName)) {
|
|
515
649
|
parallelBatch.push(planned);
|
|
@@ -517,11 +651,11 @@ export function Repl({ apiKey, cwd, onQuit }) {
|
|
|
517
651
|
}
|
|
518
652
|
await runParallelBatch(parallelBatch);
|
|
519
653
|
parallelBatch = [];
|
|
654
|
+
setLoadingLabel(`Running ${planned.toolName}…`);
|
|
520
655
|
const result = await runTool(planned.toolName, planned.toolArgs);
|
|
521
656
|
renderToolOutcome(planned, result);
|
|
522
657
|
}
|
|
523
658
|
await runParallelBatch(parallelBatch);
|
|
524
|
-
setLoading(false);
|
|
525
659
|
state = [...state, { role: "assistant", content: contentBlocks }];
|
|
526
660
|
if (toolResults.length === 0) {
|
|
527
661
|
setMessages(state);
|
|
@@ -530,6 +664,7 @@ export function Repl({ apiKey, cwd, onQuit }) {
|
|
|
530
664
|
state = [...state, { role: "user", content: toolResults }];
|
|
531
665
|
setMessages(state);
|
|
532
666
|
}
|
|
667
|
+
setLoading(false);
|
|
533
668
|
return true;
|
|
534
669
|
}, [apiKey, cwd, currentModel, messages, modelList, appendLog, openModelSelector, openBraveKeyModal, openHelpModal]);
|
|
535
670
|
const handleSubmit = useCallback(async (value) => {
|
|
@@ -947,13 +1082,8 @@ export function Repl({ apiKey, cwd, onQuit }) {
|
|
|
947
1082
|
? 4 + Math.max(1, filteredFilePaths.length)
|
|
948
1083
|
: 0;
|
|
949
1084
|
const suggestionBoxLines = slashSuggestionBoxLines || atSuggestionBoxLines;
|
|
950
|
-
const wrapWidth = Math.max(10, termColumns - PROMPT_INDENT_LEN - 2);
|
|
951
|
-
const inputLineCount = (() => {
|
|
952
|
-
const lines = inputValue.split("\n");
|
|
953
|
-
return lines.reduce((sum, line) => sum + Math.max(1, Math.ceil(line.length / wrapWidth)), 0);
|
|
954
|
-
})();
|
|
955
1085
|
// Keep a fixed loading row reserved to avoid viewport jumps/flicker when loading starts/stops.
|
|
956
|
-
const reservedLines = 1 +
|
|
1086
|
+
const reservedLines = 1 + stableInputLineCount + 2;
|
|
957
1087
|
const logViewportHeight = Math.max(1, termRows - reservedLines - suggestionBoxLines);
|
|
958
1088
|
const effectiveLogLines = logLines;
|
|
959
1089
|
const maxLogScrollOffset = Math.max(0, effectiveLogLines.length - logViewportHeight);
|
|
@@ -963,7 +1093,8 @@ export function Repl({ apiKey, cwd, onQuit }) {
|
|
|
963
1093
|
logStartIndex = 0;
|
|
964
1094
|
}
|
|
965
1095
|
const sliceEnd = logStartIndex + logViewportHeight;
|
|
966
|
-
const visibleLogLines = effectiveLogLines.slice(logStartIndex, sliceEnd);
|
|
1096
|
+
const visibleLogLines = useMemo(() => effectiveLogLines.slice(logStartIndex, sliceEnd), [effectiveLogLines, logStartIndex, sliceEnd]);
|
|
1097
|
+
const useSimpleInputRenderer = inputLineCount > 1;
|
|
967
1098
|
if (showHelpModal) {
|
|
968
1099
|
const helpModalWidth = Math.min(88, Math.max(80, termColumns - 4));
|
|
969
1100
|
const helpContentRows = 20;
|
|
@@ -986,17 +1117,14 @@ export function Repl({ apiKey, cwd, onQuit }) {
|
|
|
986
1117
|
const leftPad = Math.max(0, Math.floor((termColumns - paletteModalWidth) / 2));
|
|
987
1118
|
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
1119
|
}
|
|
989
|
-
const footerLines = suggestionBoxLines + 1 +
|
|
990
|
-
return (_jsxs(Box, { flexDirection: "column", height: termRows, overflow: "hidden", children: [_jsxs(Box, { flexDirection: "column", flexGrow: 1, minHeight: 0, overflow: "hidden", children: [_jsx(
|
|
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) => {
|
|
1120
|
+
const footerLines = suggestionBoxLines + 1 + stableInputLineCount;
|
|
1121
|
+
return (_jsxs(Box, { flexDirection: "column", height: termRows, overflow: "hidden", children: [_jsxs(Box, { flexDirection: "column", flexGrow: 1, minHeight: 0, overflow: "hidden", children: [_jsx(LogViewport, { lines: visibleLogLines, startIndex: logStartIndex, height: logViewportHeight }), _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
1122
|
const i = filteredSlashCommands.length - 1 - rev;
|
|
995
1123
|
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
1124
|
})), _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) => {
|
|
997
1125
|
const i = filteredFilePaths.length - 1 - rev;
|
|
998
1126
|
return (_jsxs(Text, { color: i === clampedAtFileIndex ? inkColors.primary : undefined, children: [i === clampedAtFileIndex ? "› " : " ", p] }, p));
|
|
999
|
-
})), _jsx(Box, { flexDirection: "row", marginTop: 1, children: _jsx(Text, { color: inkColors.textSecondary, children: " Files (\u2191/\u2193 select, Enter/Tab complete, Esc clear) " }) })] })), _jsxs(Box, { flexDirection: "row", marginTop: 0, children: [_jsxs(Text, { color: "gray", children: [" ", icons.tool, " ", tokenDisplay] }), _jsx(Text, { color: "gray", children: ` · / ! @ trackpad/↑/↓ scroll Opt/Fn+select Ctrl+J newline Tab queue Esc Esc edit ${pasteShortcut} paste Ctrl+C exit` })] }), _jsx(Box, { flexDirection: "column", marginTop: 0, children: inputValue.length === 0 ? (_jsxs(Box, { flexDirection: "row", children: [_jsxs(Text, { color: inkColors.primary, children: [icons.prompt, " "] }), _jsx(Text, { inverse: true, color: inkColors.primary, children: " " }), _jsx(Text, { color: inkColors.textSecondary, children: "Message or / for commands, @ for files, ! for shell, ? for help..." })] })) : ((() => {
|
|
1127
|
+
})), _jsx(Box, { flexDirection: "row", marginTop: 1, children: _jsx(Text, { color: inkColors.textSecondary, children: " Files (\u2191/\u2193 select, Enter/Tab complete, Esc clear) " }) })] })), _jsxs(Box, { flexDirection: "row", marginTop: 0, children: [_jsxs(Text, { color: "gray", children: [" ", icons.tool, " ", tokenDisplay] }), _jsx(Text, { color: "gray", children: ` · / ! @ trackpad/↑/↓ scroll Opt/Fn+select Ctrl+J newline Tab queue Esc Esc edit ${pasteShortcut} paste Ctrl+C exit` })] }), _jsx(Box, { flexDirection: "column", marginTop: 0, children: inputValue.length === 0 ? (_jsxs(Box, { flexDirection: "row", children: [_jsxs(Text, { color: inkColors.primary, children: [icons.prompt, " "] }), cursorBlinkOn ? (_jsx(Text, { inverse: true, color: inkColors.primary, children: " " })) : (_jsx(Text, { color: inkColors.primary, children: " " })), _jsx(Text, { color: inkColors.textSecondary, children: "Message or / for commands, @ for files, ! for shell, ? for help..." })] })) : ((() => {
|
|
1000
1128
|
const lines = inputValue.split("\n");
|
|
1001
1129
|
let lineStart = 0;
|
|
1002
1130
|
return (_jsx(_Fragment, { children: lines.flatMap((lineText, lineIdx) => {
|
|
@@ -1005,6 +1133,49 @@ export function Repl({ apiKey, cwd, onQuit }) {
|
|
|
1005
1133
|
const cursorOffsetInLine = cursorOnThisLine ? inputCursor - lineStart : -1;
|
|
1006
1134
|
const currentLineStart = lineStart;
|
|
1007
1135
|
lineStart = lineEnd + 1;
|
|
1136
|
+
if (useSimpleInputRenderer) {
|
|
1137
|
+
const visualLines = wrapLine(lineText, wrapWidth);
|
|
1138
|
+
return visualLines.map((visualChunk, v) => {
|
|
1139
|
+
const visualStart = v * wrapWidth;
|
|
1140
|
+
const visualEnd = Math.min((v + 1) * wrapWidth, lineText.length);
|
|
1141
|
+
const isLastVisualOfThisLine = v === visualLines.length - 1;
|
|
1142
|
+
const cursorAtEndOfVisual = isLastVisualOfThisLine && cursorOffsetInLine === visualEnd;
|
|
1143
|
+
const cursorPosInVisual = cursorOnThisLine &&
|
|
1144
|
+
cursorOffsetInLine >= visualStart &&
|
|
1145
|
+
(cursorOffsetInLine < visualEnd || cursorAtEndOfVisual)
|
|
1146
|
+
? cursorOffsetInLine < visualEnd
|
|
1147
|
+
? cursorOffsetInLine - visualStart
|
|
1148
|
+
: visualEnd - visualStart
|
|
1149
|
+
: -1;
|
|
1150
|
+
const isFirstRow = lineIdx === 0 && v === 0;
|
|
1151
|
+
const isLastLogicalLine = lineIdx === lines.length - 1;
|
|
1152
|
+
const isLastVisualOfLine = v === visualLines.length - 1;
|
|
1153
|
+
const rowNodes = [];
|
|
1154
|
+
if (lineText === "" && v === 0 && cursorOnThisLine) {
|
|
1155
|
+
rowNodes.push(cursorBlinkOn
|
|
1156
|
+
? (_jsx(Text, { inverse: true, color: inkColors.primary, children: "\u00A0" }, "cursor-empty-on"))
|
|
1157
|
+
: (_jsx(Text, { color: inkColors.primary, children: "\u00A0" }, "cursor-empty-off")));
|
|
1158
|
+
}
|
|
1159
|
+
else if (cursorPosInVisual >= 0) {
|
|
1160
|
+
const before = visualChunk.slice(0, cursorPosInVisual);
|
|
1161
|
+
const curChar = cursorPosInVisual < visualChunk.length
|
|
1162
|
+
? visualChunk[cursorPosInVisual]
|
|
1163
|
+
: "\u00A0";
|
|
1164
|
+
const after = cursorPosInVisual < visualChunk.length
|
|
1165
|
+
? visualChunk.slice(cursorPosInVisual + 1)
|
|
1166
|
+
: "";
|
|
1167
|
+
rowNodes.push(_jsx(Text, { children: before }, "plain-before"));
|
|
1168
|
+
rowNodes.push(cursorBlinkOn
|
|
1169
|
+
? (_jsx(Text, { inverse: true, color: inkColors.primary, children: curChar }, "plain-caret-on"))
|
|
1170
|
+
: (_jsx(Text, { children: curChar }, "plain-caret-off")));
|
|
1171
|
+
rowNodes.push(_jsx(Text, { children: after }, "plain-after"));
|
|
1172
|
+
}
|
|
1173
|
+
else {
|
|
1174
|
+
rowNodes.push(_jsx(Text, { children: visualChunk }, "plain"));
|
|
1175
|
+
}
|
|
1176
|
+
return (_jsxs(Box, { flexDirection: "row", children: [isFirstRow ? (_jsxs(Text, { color: inkColors.primary, children: [icons.prompt, " "] })) : (_jsx(Text, { children: " ".repeat(PROMPT_INDENT_LEN) })), rowNodes, isLastLogicalLine && isLastVisualOfLine && inputValue.startsWith("!") && (_jsxs(Text, { color: inkColors.textDisabled, children: [" — ", "type a shell command to run"] }))] }, `simple-${lineIdx}-${v}`));
|
|
1177
|
+
});
|
|
1178
|
+
}
|
|
1008
1179
|
const segments = parseAtSegments(lineText);
|
|
1009
1180
|
let runIdx = 0;
|
|
1010
1181
|
const segmentsWithStyle = [];
|
|
@@ -1068,7 +1239,9 @@ export function Repl({ apiKey, cwd, onQuit }) {
|
|
|
1068
1239
|
: -1;
|
|
1069
1240
|
const lineNodes = [];
|
|
1070
1241
|
if (lineText === "" && v === 0 && cursorOnThisLine) {
|
|
1071
|
-
lineNodes.push(
|
|
1242
|
+
lineNodes.push(cursorBlinkOn
|
|
1243
|
+
? (_jsx(Text, { inverse: true, color: inkColors.primary, children: "\u00A0" }, "cursor-on"))
|
|
1244
|
+
: (_jsx(Text, { color: inkColors.primary, children: "\u00A0" }, "cursor-off")));
|
|
1072
1245
|
}
|
|
1073
1246
|
else {
|
|
1074
1247
|
let cursorRendered = false;
|
|
@@ -1088,7 +1261,12 @@ export function Repl({ apiKey, cwd, onQuit }) {
|
|
|
1088
1261
|
const after = text.slice(segRel + 1);
|
|
1089
1262
|
const usePath = "color" in seg.style && !!seg.style.color;
|
|
1090
1263
|
lineNodes.push(_jsx(Text, { ...seg.style, children: before }, `${segIdx}-a`));
|
|
1091
|
-
|
|
1264
|
+
if (cursorBlinkOn) {
|
|
1265
|
+
lineNodes.push(_jsx(Text, { inverse: true, color: usePath ? inkColors.path : inkColors.primary, bold: "bold" in seg.style && !!seg.style.bold, children: curChar }, `${segIdx}-b-on`));
|
|
1266
|
+
}
|
|
1267
|
+
else {
|
|
1268
|
+
lineNodes.push(_jsx(Text, { ...seg.style, children: curChar }, `${segIdx}-b-off`));
|
|
1269
|
+
}
|
|
1092
1270
|
lineNodes.push(_jsx(Text, { ...seg.style, children: after }, `${segIdx}-c`));
|
|
1093
1271
|
cursorRendered = true;
|
|
1094
1272
|
}
|
|
@@ -1101,7 +1279,9 @@ export function Repl({ apiKey, cwd, onQuit }) {
|
|
|
1101
1279
|
}
|
|
1102
1280
|
});
|
|
1103
1281
|
if (cursorPosInVisual >= 0 && !cursorRendered) {
|
|
1104
|
-
lineNodes.push(
|
|
1282
|
+
lineNodes.push(cursorBlinkOn
|
|
1283
|
+
? (_jsx(Text, { inverse: true, color: inkColors.primary, children: "\u00A0" }, "cursor-end-on"))
|
|
1284
|
+
: (_jsx(Text, { color: inkColors.primary, children: "\u00A0" }, "cursor-end-off")));
|
|
1105
1285
|
}
|
|
1106
1286
|
}
|
|
1107
1287
|
const isFirstRow = lineIdx === 0 && v === 0;
|
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;
|