ideacode 1.1.8 → 1.2.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/repl.js +82 -40
- package/dist/tools/index.js +1 -1
- package/dist/tools/web.js +174 -53
- package/dist/ui/format.js +262 -29
- package/dist/ui/theme.js +2 -2
- package/package.json +1 -1
package/dist/repl.js
CHANGED
|
@@ -37,6 +37,9 @@ const CONTEXT_WINDOW_K = 128;
|
|
|
37
37
|
const MAX_TOOL_RESULT_CHARS = 3500;
|
|
38
38
|
const MAX_AT_SUGGESTIONS = 12;
|
|
39
39
|
const INITIAL_BANNER_LINES = 12;
|
|
40
|
+
const ENABLE_PARALLEL_TOOL_CALLS = process.env.IDEACODE_PARALLEL_TOOL_CALLS !== "0";
|
|
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));
|
|
40
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.)";
|
|
41
44
|
function truncateToolResult(content) {
|
|
42
45
|
if (content.length <= MAX_TOOL_RESULT_CHARS)
|
|
@@ -88,12 +91,19 @@ function wrapLine(line, width) {
|
|
|
88
91
|
return out.length > 0 ? out : [""];
|
|
89
92
|
}
|
|
90
93
|
function replayMessagesToLogLines(messages) {
|
|
94
|
+
const sanitizePrompt = (value) => value
|
|
95
|
+
.replace(/\x1b\[[0-9;]*m/g, "")
|
|
96
|
+
.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, "")
|
|
97
|
+
.trim();
|
|
91
98
|
const lines = [];
|
|
92
99
|
for (let i = 0; i < messages.length; i++) {
|
|
93
100
|
const msg = messages[i];
|
|
94
101
|
if (msg.role === "user") {
|
|
95
102
|
if (typeof msg.content === "string") {
|
|
96
|
-
|
|
103
|
+
const prompt = sanitizePrompt(msg.content);
|
|
104
|
+
if (!prompt)
|
|
105
|
+
continue;
|
|
106
|
+
lines.push("", ...userPromptBox(prompt).split("\n"), "");
|
|
97
107
|
}
|
|
98
108
|
else if (Array.isArray(msg.content)) {
|
|
99
109
|
const prev = messages[i - 1];
|
|
@@ -110,8 +120,6 @@ function replayMessagesToLogLines(messages) {
|
|
|
110
120
|
const content = tr.content ?? "";
|
|
111
121
|
const ok = !content.startsWith("error:");
|
|
112
122
|
lines.push(toolCallBox(name, argPreview, ok));
|
|
113
|
-
const preview = content.split("\n")[0]?.slice(0, 60) ?? "";
|
|
114
|
-
lines.push(toolResultLine(preview, ok));
|
|
115
123
|
const tokens = estimateTokensForString(content);
|
|
116
124
|
lines.push(toolResultTokenLine(tokens, ok));
|
|
117
125
|
}
|
|
@@ -255,7 +263,8 @@ export function Repl({ apiKey, cwd, onQuit }) {
|
|
|
255
263
|
};
|
|
256
264
|
}, []);
|
|
257
265
|
const [spinnerTick, setSpinnerTick] = useState(0);
|
|
258
|
-
const
|
|
266
|
+
const loadingStartedAtRef = useRef(0);
|
|
267
|
+
const SPINNER = ["●○○", "○●○", "○○●", "○●○"];
|
|
259
268
|
const estimatedTokens = useMemo(() => estimateTokens(messages, undefined), [messages]);
|
|
260
269
|
const contextWindowK = useMemo(() => {
|
|
261
270
|
const ctx = modelList.find((m) => m.id === currentModel)?.context_length;
|
|
@@ -289,7 +298,9 @@ export function Repl({ apiKey, cwd, onQuit }) {
|
|
|
289
298
|
useEffect(() => {
|
|
290
299
|
if (!loading)
|
|
291
300
|
return;
|
|
292
|
-
|
|
301
|
+
loadingStartedAtRef.current = Date.now();
|
|
302
|
+
setSpinnerTick(0);
|
|
303
|
+
const t = setInterval(() => setSpinnerTick((n) => n + 1), LOADING_TICK_MS);
|
|
293
304
|
return () => clearInterval(t);
|
|
294
305
|
}, [loading]);
|
|
295
306
|
const showSlashSuggestions = inputValue.startsWith("/");
|
|
@@ -434,7 +445,7 @@ export function Repl({ apiKey, cwd, onQuit }) {
|
|
|
434
445
|
appendLog(userPromptBox(userInput));
|
|
435
446
|
appendLog("");
|
|
436
447
|
let state = [...messages, { role: "user", content: userInput }];
|
|
437
|
-
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.`;
|
|
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.`;
|
|
438
449
|
const modelContext = modelList.find((m) => m.id === currentModel)?.context_length;
|
|
439
450
|
const maxContextTokens = Math.floor((modelContext ?? CONTEXT_WINDOW_K * 1024) * 0.85);
|
|
440
451
|
const stateBeforeCompress = state;
|
|
@@ -450,34 +461,63 @@ export function Repl({ apiKey, cwd, onQuit }) {
|
|
|
450
461
|
const response = await callApi(apiKey, state, systemPrompt, currentModel);
|
|
451
462
|
const contentBlocks = response.content ?? [];
|
|
452
463
|
const toolResults = [];
|
|
464
|
+
const renderToolOutcome = (planned, result, extraIndent = 0) => {
|
|
465
|
+
const ok = !result.startsWith("error:");
|
|
466
|
+
appendLog(toolCallBox(planned.toolName, planned.argPreview, ok, extraIndent));
|
|
467
|
+
const contentForApi = truncateToolResult(result);
|
|
468
|
+
const tokens = estimateTokensForString(contentForApi);
|
|
469
|
+
appendLog(toolResultTokenLine(tokens, ok, extraIndent));
|
|
470
|
+
if (planned.block.id) {
|
|
471
|
+
toolResults.push({ type: "tool_result", tool_use_id: planned.block.id, content: contentForApi });
|
|
472
|
+
}
|
|
473
|
+
};
|
|
474
|
+
const runParallelBatch = async (batch) => {
|
|
475
|
+
if (batch.length === 0)
|
|
476
|
+
return;
|
|
477
|
+
const started = Date.now();
|
|
478
|
+
const groupedTools = Array.from(batch.reduce((acc, planned) => {
|
|
479
|
+
acc.set(planned.toolName, (acc.get(planned.toolName) ?? 0) + 1);
|
|
480
|
+
return acc;
|
|
481
|
+
}, new Map()))
|
|
482
|
+
.map(([name, count]) => (count > 1 ? `${name}×${count}` : name))
|
|
483
|
+
.join(", ");
|
|
484
|
+
appendLog(colors.gray(` ${icons.tool} parallel batch (${batch.length}): ${groupedTools}`));
|
|
485
|
+
const settled = await Promise.all(batch.map(async (planned) => ({ planned, result: await runTool(planned.toolName, planned.toolArgs) })));
|
|
486
|
+
const elapsed = Date.now() - started;
|
|
487
|
+
appendLog(colors.gray(` completed in ${elapsed}ms`));
|
|
488
|
+
for (const { planned, result } of settled) {
|
|
489
|
+
renderToolOutcome(planned, result, 1);
|
|
490
|
+
}
|
|
491
|
+
};
|
|
492
|
+
let parallelBatch = [];
|
|
453
493
|
for (const block of contentBlocks) {
|
|
454
494
|
if (block.type === "text" && block.text?.trim()) {
|
|
455
495
|
if (lastLogLineRef.current !== "")
|
|
456
496
|
appendLog("");
|
|
457
497
|
appendLog(agentMessage(block.text).trimEnd());
|
|
498
|
+
continue;
|
|
458
499
|
}
|
|
459
|
-
if (block.type
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
appendLog(toolResultLine(preview, ok));
|
|
474
|
-
const contentForApi = truncateToolResult(result);
|
|
475
|
-
const tokens = estimateTokensForString(contentForApi);
|
|
476
|
-
appendLog(toolResultTokenLine(tokens, ok));
|
|
477
|
-
if (block.id)
|
|
478
|
-
toolResults.push({ type: "tool_result", tool_use_id: block.id, content: contentForApi });
|
|
500
|
+
if (block.type !== "tool_use" || !block.name || !block.input)
|
|
501
|
+
continue;
|
|
502
|
+
const toolName = block.name.trim().toLowerCase();
|
|
503
|
+
const toolArgs = block.input;
|
|
504
|
+
const firstVal = Object.values(toolArgs)[0];
|
|
505
|
+
const planned = {
|
|
506
|
+
block,
|
|
507
|
+
toolName,
|
|
508
|
+
toolArgs,
|
|
509
|
+
argPreview: String(firstVal ?? "").slice(0, 100) || "—",
|
|
510
|
+
};
|
|
511
|
+
if (ENABLE_PARALLEL_TOOL_CALLS && PARALLEL_SAFE_TOOLS.has(toolName)) {
|
|
512
|
+
parallelBatch.push(planned);
|
|
513
|
+
continue;
|
|
479
514
|
}
|
|
515
|
+
await runParallelBatch(parallelBatch);
|
|
516
|
+
parallelBatch = [];
|
|
517
|
+
const result = await runTool(planned.toolName, planned.toolArgs);
|
|
518
|
+
renderToolOutcome(planned, result);
|
|
480
519
|
}
|
|
520
|
+
await runParallelBatch(parallelBatch);
|
|
481
521
|
setLoading(false);
|
|
482
522
|
state = [...state, { role: "assistant", content: contentBlocks }];
|
|
483
523
|
if (toolResults.length === 0) {
|
|
@@ -892,10 +932,10 @@ export function Repl({ apiKey, cwd, onQuit }) {
|
|
|
892
932
|
const visibleModelCount = Math.min(filteredModelList.length, modelModalHeight - 4);
|
|
893
933
|
const modelScrollOffset = Math.max(0, Math.min(modelIndex - Math.floor(visibleModelCount / 2), filteredModelList.length - visibleModelCount));
|
|
894
934
|
const visibleModels = filteredModelList.slice(modelScrollOffset, modelScrollOffset + visibleModelCount);
|
|
895
|
-
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: modelModalWidth, minHeight: modelModalHeight, children: [_jsx(Text, { bold: true, children: " Select model " }), _jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color:
|
|
935
|
+
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: modelModalWidth, minHeight: modelModalHeight, children: [_jsx(Text, { bold: true, children: " Select model " }), _jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: inkColors.textSecondary, children: " Filter: " }), _jsx(Text, { children: modelSearchFilter || " " }), modelSearchFilter.length > 0 && (_jsxs(Text, { color: inkColors.textDisabled, children: [" ", "(", filteredModelList.length, " match", filteredModelList.length !== 1 ? "es" : "", ")"] }))] }), visibleModels.length === 0 ? (_jsx(Text, { color: inkColors.textSecondary, children: " No match \u2014 type to search by id or name " })) : (visibleModels.map((m, i) => {
|
|
896
936
|
const actualIndex = modelScrollOffset + i;
|
|
897
937
|
return (_jsxs(Text, { color: actualIndex === modelIndex ? inkColors.primary : undefined, children: [actualIndex === modelIndex ? "› " : " ", m.name ? `${m.id} — ${m.name}` : m.id] }, m.id));
|
|
898
|
-
})), _jsx(Text, { color:
|
|
938
|
+
})), _jsx(Text, { color: inkColors.textSecondary, children: " \u2191/\u2193 select Enter confirm Esc cancel Type to filter " })] })] }), _jsx(Box, { flexGrow: 1 })] }));
|
|
899
939
|
}
|
|
900
940
|
const slashSuggestionBoxLines = showSlashSuggestions
|
|
901
941
|
? 3 + Math.max(1, filteredSlashCommands.length)
|
|
@@ -909,7 +949,8 @@ export function Repl({ apiKey, cwd, onQuit }) {
|
|
|
909
949
|
const lines = inputValue.split("\n");
|
|
910
950
|
return lines.reduce((sum, line) => sum + Math.max(1, Math.ceil(line.length / wrapWidth)), 0);
|
|
911
951
|
})();
|
|
912
|
-
|
|
952
|
+
// Keep a fixed loading row reserved to avoid viewport jumps/flicker when loading starts/stops.
|
|
953
|
+
const reservedLines = 1 + inputLineCount + 2;
|
|
913
954
|
const logViewportHeight = Math.max(1, termRows - reservedLines - suggestionBoxLines);
|
|
914
955
|
const effectiveLogLines = logLines;
|
|
915
956
|
const maxLogScrollOffset = Math.max(0, effectiveLogLines.length - logViewportHeight);
|
|
@@ -919,9 +960,7 @@ export function Repl({ apiKey, cwd, onQuit }) {
|
|
|
919
960
|
logStartIndex = 0;
|
|
920
961
|
}
|
|
921
962
|
const sliceEnd = logStartIndex + logViewportHeight;
|
|
922
|
-
const visibleLogLines =
|
|
923
|
-
? ["", ...effectiveLogLines.slice(0, logViewportHeight - 1)]
|
|
924
|
-
: effectiveLogLines.slice(logStartIndex, sliceEnd);
|
|
963
|
+
const visibleLogLines = effectiveLogLines.slice(logStartIndex, sliceEnd);
|
|
925
964
|
if (showHelpModal) {
|
|
926
965
|
const helpModalWidth = Math.min(88, Math.max(80, termColumns - 4));
|
|
927
966
|
const helpContentRows = 20;
|
|
@@ -929,29 +968,32 @@ export function Repl({ apiKey, cwd, onQuit }) {
|
|
|
929
968
|
const helpLeftPad = Math.max(0, Math.floor((termColumns - helpModalWidth) / 2));
|
|
930
969
|
const labelWidth = 20;
|
|
931
970
|
const descWidth = helpModalWidth - (2 * 2) - labelWidth - 2;
|
|
932
|
-
return (_jsxs(Box, { flexDirection: "column", height: termRows, overflow: "hidden", children: [_jsx(Box, { height: helpTopPad }), _jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { width: helpLeftPad }), _jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: inkColors.primary, paddingX: 2, paddingY: 1, width: helpModalWidth, children: [_jsx(Text, { bold: true, children: " Help " }), _jsx(Text, { color:
|
|
971
|
+
return (_jsxs(Box, { flexDirection: "column", height: termRows, overflow: "hidden", children: [_jsx(Box, { height: helpTopPad }), _jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { width: helpLeftPad }), _jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: inkColors.primary, paddingX: 2, paddingY: 1, width: helpModalWidth, children: [_jsx(Text, { bold: true, children: " Help " }), _jsx(Text, { color: inkColors.textSecondary, children: " What you can do " }), _jsxs(Box, { marginTop: 1, flexDirection: "row", alignItems: "flex-start", children: [_jsx(Box, { width: labelWidth, flexShrink: 0, children: _jsx(Text, { color: inkColors.primary, children: " Message " }) }), _jsx(Box, { width: descWidth, flexGrow: 0, children: _jsx(Text, { color: inkColors.textSecondary, children: " Type and Enter to send to the agent. " }) })] }), _jsxs(Box, { marginTop: 1, flexDirection: "row", alignItems: "flex-start", children: [_jsx(Box, { width: labelWidth, flexShrink: 0, children: _jsx(Text, { color: inkColors.primary, children: " / " }) }), _jsx(Box, { width: descWidth, flexGrow: 0, children: _jsx(Text, { color: inkColors.textSecondary, children: " Commands. Type / then pick: /models, /brave, /help, /clear, /status, /q. Ctrl+P palette. " }) })] }), _jsxs(Box, { marginTop: 1, flexDirection: "row", alignItems: "flex-start", children: [_jsx(Box, { width: labelWidth, flexShrink: 0, children: _jsx(Text, { color: inkColors.primary, children: " @ " }) }), _jsx(Box, { width: descWidth, flexGrow: 0, children: _jsx(Text, { color: inkColors.textSecondary, children: " Attach files. Type @ then path; Tab to complete. " }) })] }), _jsxs(Box, { marginTop: 1, flexDirection: "row", alignItems: "flex-start", children: [_jsx(Box, { width: labelWidth, flexShrink: 0, children: _jsx(Text, { color: inkColors.primary, children: " ! " }) }), _jsx(Box, { width: descWidth, flexGrow: 0, children: _jsx(Text, { color: inkColors.textSecondary, children: " Run a shell command. Type ! then the command. " }) })] }), _jsxs(Box, { marginTop: 1, flexDirection: "row", alignItems: "flex-start", children: [_jsx(Box, { width: labelWidth, flexShrink: 0, children: _jsx(Text, { color: inkColors.primary, children: " Word / char nav " }) }), _jsx(Box, { width: descWidth, flexGrow: 0, children: _jsx(Text, { color: inkColors.textSecondary, children: " Ctrl+\u2190/\u2192 or Meta+\u2190/\u2192 word; Ctrl+F/B char (Emacs). Opt+\u2190/\u2192 needs terminal to send Meta (e.g. iTerm2: Esc+). " }) })] }), _jsxs(Box, { marginTop: 1, flexDirection: "row", alignItems: "flex-start", children: [_jsx(Box, { width: labelWidth, flexShrink: 0, children: _jsx(Text, { color: inkColors.primary, children: " Scroll " }) }), _jsx(Box, { width: descWidth, flexGrow: 0, children: _jsx(Text, { color: inkColors.textSecondary, children: " Trackpad/\u2191/\u2193 scroll. To select text: hold Option (iTerm2) or Fn (Terminal.app) or Shift (Windows/Linux). " }) })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: inkColors.textSecondary, children: " Press any key to close " }) })] })] }), _jsx(Box, { flexGrow: 1 })] }));
|
|
933
972
|
}
|
|
934
973
|
if (showBraveKeyModal) {
|
|
935
974
|
const braveModalWidth = 52;
|
|
936
975
|
const topPad = Math.max(0, Math.floor((termRows - 6) / 2));
|
|
937
976
|
const leftPad = Math.max(0, Math.floor((termColumns - braveModalWidth) / 2));
|
|
938
|
-
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: braveModalWidth, children: [_jsx(Text, { bold: true, children: " Brave Search API key " }), _jsx(Text, { color:
|
|
977
|
+
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: braveModalWidth, children: [_jsx(Text, { bold: true, children: " Brave Search API key " }), _jsx(Text, { color: inkColors.textSecondary, children: " Get one at https://brave.com/search/api " }), braveKeyInput === BRAVE_KEY_PLACEHOLDER && (_jsx(Text, { color: inkColors.textSecondary, children: " Key already set. Type or paste to replace. " })), _jsxs(Box, { flexDirection: "row", marginTop: 1, children: [_jsx(Text, { color: inkColors.primary, children: " Key: " }), _jsx(Text, { children: braveKeyInput || "\u00A0" })] }), _jsx(Text, { color: inkColors.textSecondary, children: " Enter to save, Esc to cancel " })] })] }), _jsx(Box, { flexGrow: 1 })] }));
|
|
939
978
|
}
|
|
940
979
|
if (showPalette) {
|
|
941
980
|
const paletteModalHeight = COMMANDS.length + 4;
|
|
942
981
|
const paletteModalWidth = 52;
|
|
943
982
|
const topPad = Math.max(0, Math.floor((termRows - paletteModalHeight) / 2));
|
|
944
983
|
const leftPad = Math.max(0, Math.floor((termColumns - paletteModalWidth) / 2));
|
|
945
|
-
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:
|
|
984
|
+
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 })] }));
|
|
946
985
|
}
|
|
947
986
|
const footerLines = suggestionBoxLines + 1 + inputLineCount;
|
|
948
|
-
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))) }),
|
|
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) => {
|
|
949
991
|
const i = filteredSlashCommands.length - 1 - rev;
|
|
950
|
-
return (_jsxs(Text, { color: i === clampedSlashIndex ? inkColors.primary : undefined, children: [i === clampedSlashIndex ? "› " : " ", c.cmd, _jsxs(Text, { color:
|
|
951
|
-
})), _jsx(Text, { color:
|
|
992
|
+
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
|
+
})), _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) => {
|
|
952
994
|
const i = filteredFilePaths.length - 1 - rev;
|
|
953
995
|
return (_jsxs(Text, { color: i === clampedAtFileIndex ? inkColors.primary : undefined, children: [i === clampedAtFileIndex ? "› " : " ", p] }, p));
|
|
954
|
-
})), _jsx(Box, { flexDirection: "row", marginTop: 1, children: _jsx(Text, { color:
|
|
996
|
+
})), _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..." })] })) : ((() => {
|
|
955
997
|
const lines = inputValue.split("\n");
|
|
956
998
|
let lineStart = 0;
|
|
957
999
|
return (_jsx(_Fragment, { children: lines.flatMap((lineText, lineIdx) => {
|
|
@@ -1062,7 +1104,7 @@ export function Repl({ apiKey, cwd, onQuit }) {
|
|
|
1062
1104
|
const isFirstRow = lineIdx === 0 && v === 0;
|
|
1063
1105
|
const isLastLogicalLine = lineIdx === lines.length - 1;
|
|
1064
1106
|
const isLastVisualOfLine = v === visualLines.length - 1;
|
|
1065
|
-
return (_jsxs(Box, { flexDirection: "row", children: [isFirstRow ? (_jsxs(Text, { color: inkColors.primary, children: [icons.prompt, " "] })) : (_jsx(Text, { children: " ".repeat(PROMPT_INDENT_LEN) })), lineNodes, isLastLogicalLine && isLastVisualOfLine && inputValue.startsWith("!") && (_jsxs(Text, {
|
|
1107
|
+
return (_jsxs(Box, { flexDirection: "row", children: [isFirstRow ? (_jsxs(Text, { color: inkColors.primary, children: [icons.prompt, " "] })) : (_jsx(Text, { children: " ".repeat(PROMPT_INDENT_LEN) })), lineNodes, isLastLogicalLine && isLastVisualOfLine && inputValue.startsWith("!") && (_jsxs(Text, { color: inkColors.textDisabled, children: [" — ", "type a shell command to run"] }))] }, `${lineIdx}-${v}`));
|
|
1066
1108
|
});
|
|
1067
1109
|
}) }));
|
|
1068
1110
|
})()) })] })] }));
|
package/dist/tools/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import { runBash } from "./bash.js";
|
|
|
5
5
|
import { webFetch, webSearch } from "./web.js";
|
|
6
6
|
export const TOOLS = {
|
|
7
7
|
read: [
|
|
8
|
-
"Read file with line numbers (file path, not directory).
|
|
8
|
+
"Read a portion of a file with line numbers (file path, not directory). Prefer offset and limit for anything beyond a small file; do not read entire large files in one call. Long output is truncated; use offset/limit to get more.",
|
|
9
9
|
{ path: "string", offset: "number?", limit: "number?" },
|
|
10
10
|
readFile,
|
|
11
11
|
],
|
package/dist/tools/web.js
CHANGED
|
@@ -2,105 +2,226 @@ import { getBraveSearchApiKey } from "../config.js";
|
|
|
2
2
|
const MAX_FETCH_CHARS = 40_000;
|
|
3
3
|
const FETCH_TIMEOUT_MS = 20_000;
|
|
4
4
|
const MAX_SEARCH_RESULTS = 8;
|
|
5
|
+
const FETCH_CACHE_TTL_MS = 5 * 60_000;
|
|
6
|
+
const fetchCache = new Map();
|
|
5
7
|
function formatSearchResults(hits) {
|
|
6
8
|
const slice = hits.slice(0, MAX_SEARCH_RESULTS);
|
|
7
9
|
return slice
|
|
8
10
|
.map((r, i) => `${i + 1}. ${(r.title ?? "Untitled").replace(/<[^>]+>/g, "")}\n ${r.url ?? ""}${r.snippet ? "\n " + String(r.snippet).replace(/<[^>]+>/g, "").slice(0, 300) : ""}`)
|
|
9
11
|
.join("\n\n");
|
|
10
12
|
}
|
|
11
|
-
const PLAYWRIGHT_INSTALL_MSG = "Playwright Chromium not installed. Run: npx playwright install chromium (in the project directory). web_fetch uses it only when plain fetch
|
|
12
|
-
function
|
|
13
|
-
|
|
13
|
+
const PLAYWRIGHT_INSTALL_MSG = "Playwright Chromium not installed. Run: npx playwright install chromium (in the project directory). web_fetch uses it only when plain fetch cannot reliably extract content.";
|
|
14
|
+
function decodeHtmlEntities(text) {
|
|
15
|
+
return text
|
|
16
|
+
.replace(/ /gi, " ")
|
|
17
|
+
.replace(/&/gi, "&")
|
|
18
|
+
.replace(/</gi, "<")
|
|
19
|
+
.replace(/>/gi, ">")
|
|
20
|
+
.replace(/"/gi, '"')
|
|
21
|
+
.replace(/'/gi, "'")
|
|
22
|
+
.replace(/&#x([0-9a-f]+);/gi, (_m, hex) => {
|
|
23
|
+
const n = Number.parseInt(hex, 16);
|
|
24
|
+
return Number.isFinite(n) ? String.fromCodePoint(n) : _m;
|
|
25
|
+
})
|
|
26
|
+
.replace(/&#(\d+);/g, (_m, dec) => {
|
|
27
|
+
const n = Number.parseInt(dec, 10);
|
|
28
|
+
return Number.isFinite(n) ? String.fromCodePoint(n) : _m;
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
function pickMainHtml(html) {
|
|
32
|
+
const article = html.match(/<article\b[^>]*>[\s\S]*?<\/article>/i)?.[0];
|
|
33
|
+
if (article)
|
|
34
|
+
return article;
|
|
35
|
+
const main = html.match(/<main\b[^>]*>[\s\S]*?<\/main>/i)?.[0];
|
|
36
|
+
if (main)
|
|
37
|
+
return main;
|
|
38
|
+
return html;
|
|
39
|
+
}
|
|
40
|
+
function htmlToText(html) {
|
|
41
|
+
const focused = pickMainHtml(html);
|
|
42
|
+
let s = focused
|
|
14
43
|
.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, "")
|
|
15
44
|
.replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, "")
|
|
45
|
+
.replace(/<svg\b[^>]*>[\s\S]*?<\/svg>/gi, "")
|
|
46
|
+
.replace(/<noscript\b[^>]*>[\s\S]*?<\/noscript>/gi, "")
|
|
47
|
+
.replace(/<nav\b[^>]*>[\s\S]*?<\/nav>/gi, "")
|
|
48
|
+
.replace(/<header\b[^>]*>[\s\S]*?<\/header>/gi, "")
|
|
49
|
+
.replace(/<footer\b[^>]*>[\s\S]*?<\/footer>/gi, "")
|
|
50
|
+
.replace(/<form\b[^>]*>[\s\S]*?<\/form>/gi, "")
|
|
51
|
+
.replace(/<aside\b[^>]*>[\s\S]*?<\/aside>/gi, "")
|
|
52
|
+
.replace(/<br\s*\/?>/gi, "\n")
|
|
53
|
+
.replace(/<\/(p|div|section|article|h[1-6]|li|tr|ul|ol|table)>/gi, "\n")
|
|
54
|
+
.replace(/<li\b[^>]*>/gi, "- ")
|
|
16
55
|
.replace(/<[^>]+>/g, " ");
|
|
17
|
-
|
|
56
|
+
s = decodeHtmlEntities(s);
|
|
57
|
+
s = s
|
|
58
|
+
.replace(/[ \t]+/g, " ")
|
|
59
|
+
.replace(/ *\n */g, "\n")
|
|
60
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
61
|
+
.trim();
|
|
62
|
+
return s;
|
|
63
|
+
}
|
|
64
|
+
function isLikelyBlockedOrThin(text) {
|
|
65
|
+
const lowered = text.toLowerCase();
|
|
66
|
+
const blockedSignals = [
|
|
67
|
+
"enable javascript",
|
|
68
|
+
"verify you are human",
|
|
69
|
+
"captcha",
|
|
70
|
+
"cloudflare",
|
|
71
|
+
"access denied",
|
|
72
|
+
"unusual traffic",
|
|
73
|
+
"robot",
|
|
74
|
+
"bot detection",
|
|
75
|
+
"request blocked",
|
|
76
|
+
"forbidden",
|
|
77
|
+
];
|
|
78
|
+
if (blockedSignals.some((s) => lowered.includes(s)))
|
|
79
|
+
return true;
|
|
80
|
+
return text.length < 280;
|
|
81
|
+
}
|
|
82
|
+
function clipOutput(text) {
|
|
83
|
+
if (text.length <= MAX_FETCH_CHARS)
|
|
84
|
+
return text;
|
|
85
|
+
return text.slice(0, MAX_FETCH_CHARS) + `\n\n... (truncated, total ${text.length} chars)`;
|
|
86
|
+
}
|
|
87
|
+
function getCachedFetch(url) {
|
|
88
|
+
const hit = fetchCache.get(url);
|
|
89
|
+
if (!hit)
|
|
90
|
+
return null;
|
|
91
|
+
if (Date.now() - hit.cachedAt > FETCH_CACHE_TTL_MS) {
|
|
92
|
+
fetchCache.delete(url);
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
return hit.value;
|
|
96
|
+
}
|
|
97
|
+
function setCachedFetch(url, value) {
|
|
98
|
+
fetchCache.set(url, { cachedAt: Date.now(), value });
|
|
18
99
|
}
|
|
19
100
|
async function fetchWithTimeout(url) {
|
|
20
101
|
const ac = new AbortController();
|
|
21
102
|
const t = setTimeout(() => ac.abort(), FETCH_TIMEOUT_MS);
|
|
22
103
|
try {
|
|
23
|
-
|
|
104
|
+
return await fetch(url, {
|
|
24
105
|
signal: ac.signal,
|
|
25
|
-
headers: {
|
|
106
|
+
headers: {
|
|
107
|
+
"User-Agent": "ideacode-web-fetch/2",
|
|
108
|
+
Accept: "text/html,application/json,text/plain,*/*",
|
|
109
|
+
},
|
|
110
|
+
redirect: "follow",
|
|
26
111
|
});
|
|
27
|
-
return res;
|
|
28
112
|
}
|
|
29
113
|
finally {
|
|
30
114
|
clearTimeout(t);
|
|
31
115
|
}
|
|
32
116
|
}
|
|
117
|
+
async function fetchWithPlaywright(url) {
|
|
118
|
+
const { chromium } = await import("playwright");
|
|
119
|
+
const browser = await chromium.launch({ headless: true });
|
|
120
|
+
try {
|
|
121
|
+
const page = await browser.newPage({
|
|
122
|
+
userAgent: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
|
|
123
|
+
});
|
|
124
|
+
await page.goto(url, { waitUntil: "domcontentloaded", timeout: FETCH_TIMEOUT_MS });
|
|
125
|
+
const text = await page.evaluate(() => {
|
|
126
|
+
const root = document.querySelector("article") ??
|
|
127
|
+
document.querySelector("main") ??
|
|
128
|
+
document.querySelector("[role='main']") ??
|
|
129
|
+
document.body;
|
|
130
|
+
if (!root)
|
|
131
|
+
return "";
|
|
132
|
+
const clone = root.cloneNode(true);
|
|
133
|
+
for (const el of clone.querySelectorAll("script, style, nav, header, footer, aside, form, noscript, svg")) {
|
|
134
|
+
el.remove();
|
|
135
|
+
}
|
|
136
|
+
return (clone.innerText ?? clone.textContent ?? "")
|
|
137
|
+
.replace(/[ \t]+/g, " ")
|
|
138
|
+
.replace(/ *\n */g, "\n")
|
|
139
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
140
|
+
.trim();
|
|
141
|
+
});
|
|
142
|
+
return text || "(no text content)";
|
|
143
|
+
}
|
|
144
|
+
finally {
|
|
145
|
+
await browser.close().catch(() => { });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
33
148
|
export async function webFetch(args) {
|
|
34
149
|
const url = args.url?.trim();
|
|
35
150
|
if (!url)
|
|
36
151
|
return "error: url is required";
|
|
37
|
-
if (!url.startsWith("http://") && !url.startsWith("https://"))
|
|
152
|
+
if (!url.startsWith("http://") && !url.startsWith("https://")) {
|
|
38
153
|
return "error: url must be http or https";
|
|
154
|
+
}
|
|
155
|
+
const cached = getCachedFetch(url);
|
|
156
|
+
if (cached)
|
|
157
|
+
return cached;
|
|
158
|
+
let needsBrowserFallback = false;
|
|
159
|
+
let fallbackReason = "";
|
|
39
160
|
try {
|
|
40
161
|
const res = await fetchWithTimeout(url);
|
|
162
|
+
const ct = (res.headers.get("content-type") ?? "").toLowerCase();
|
|
41
163
|
if (res.ok) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
164
|
+
if (ct.includes("application/json")) {
|
|
165
|
+
const raw = await res.text();
|
|
166
|
+
const clipped = clipOutput(raw.trim() || "(no text content)");
|
|
167
|
+
setCachedFetch(url, clipped);
|
|
168
|
+
return clipped;
|
|
169
|
+
}
|
|
170
|
+
if (ct.includes("text/plain") || ct.includes("text/") || ct.includes("application/xml") || ct.includes("application/xhtml")) {
|
|
47
171
|
const raw = await res.text();
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
172
|
+
const normalized = ct.includes("html") ? htmlToText(raw) : raw.replace(/\s+/g, " ").trim();
|
|
173
|
+
if (!normalized) {
|
|
174
|
+
needsBrowserFallback = true;
|
|
175
|
+
fallbackReason = "empty fetch result";
|
|
176
|
+
}
|
|
177
|
+
else if (ct.includes("html") && isLikelyBlockedOrThin(normalized)) {
|
|
178
|
+
needsBrowserFallback = true;
|
|
179
|
+
fallbackReason = "thin or blocked html content";
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
const clipped = clipOutput(normalized);
|
|
183
|
+
setCachedFetch(url, clipped);
|
|
184
|
+
return clipped;
|
|
56
185
|
}
|
|
57
|
-
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
return `error: unsupported content-type: ${ct || "unknown"}`;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
const bodyPreview = (await res.text()).slice(0, 220);
|
|
193
|
+
if (res.status === 403 || res.status === 429 || res.status === 503) {
|
|
194
|
+
needsBrowserFallback = true;
|
|
195
|
+
fallbackReason = `http ${res.status}`;
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
return `error: HTTP ${res.status}${bodyPreview ? `: ${bodyPreview}` : ""}`;
|
|
58
199
|
}
|
|
59
200
|
}
|
|
60
201
|
}
|
|
61
202
|
catch {
|
|
62
|
-
|
|
203
|
+
needsBrowserFallback = true;
|
|
204
|
+
fallbackReason = "fetch failed";
|
|
63
205
|
}
|
|
206
|
+
if (!needsBrowserFallback)
|
|
207
|
+
return "error: unable to fetch content";
|
|
64
208
|
try {
|
|
65
|
-
const
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
await page.goto(url, { waitUntil: "domcontentloaded", timeout: FETCH_TIMEOUT_MS });
|
|
70
|
-
const text = await page.evaluate(() => {
|
|
71
|
-
const body = document.body;
|
|
72
|
-
if (!body)
|
|
73
|
-
return "";
|
|
74
|
-
const clone = body.cloneNode(true);
|
|
75
|
-
for (const el of clone.querySelectorAll("script, style, nav, header, footer, [role='navigation']")) {
|
|
76
|
-
el.remove();
|
|
77
|
-
}
|
|
78
|
-
return (clone.innerText ?? clone.textContent ?? "").replace(/\s+/g, " ").trim();
|
|
79
|
-
});
|
|
80
|
-
if (text.length > MAX_FETCH_CHARS) {
|
|
81
|
-
return (text.slice(0, MAX_FETCH_CHARS) +
|
|
82
|
-
"\n\n... (truncated, total " +
|
|
83
|
-
text.length +
|
|
84
|
-
" chars)");
|
|
85
|
-
}
|
|
86
|
-
return text || "(no text content)";
|
|
87
|
-
}
|
|
88
|
-
finally {
|
|
89
|
-
await browser.close().catch(() => { });
|
|
90
|
-
}
|
|
209
|
+
const browserText = await fetchWithPlaywright(url);
|
|
210
|
+
const clipped = clipOutput(browserText);
|
|
211
|
+
setCachedFetch(url, clipped);
|
|
212
|
+
return clipped;
|
|
91
213
|
}
|
|
92
214
|
catch (err) {
|
|
93
215
|
const msg = err instanceof Error ? err.message : String(err);
|
|
94
216
|
const causeMsg = err instanceof Error && err.cause instanceof Error ? String(err.cause.message) : "";
|
|
95
217
|
const combined = (msg + " " + causeMsg).toLowerCase();
|
|
96
|
-
const
|
|
218
|
+
const needsPlaywrightInstall = combined.includes("executable doesn't exist") ||
|
|
97
219
|
combined.includes("browser was not found") ||
|
|
98
220
|
combined.includes("chromium revision is not downloaded") ||
|
|
99
221
|
(combined.includes("playwright") && (combined.includes("install") || combined.includes("not found")));
|
|
100
|
-
if (
|
|
222
|
+
if (needsPlaywrightInstall)
|
|
101
223
|
return `error: ${PLAYWRIGHT_INSTALL_MSG}`;
|
|
102
|
-
}
|
|
103
|
-
return `error: ${msg}`;
|
|
224
|
+
return `error: web_fetch failed after ${fallbackReason || "fallback"}. ${msg}`;
|
|
104
225
|
}
|
|
105
226
|
}
|
|
106
227
|
async function braveSearchApi(query, apiKey) {
|
package/dist/ui/format.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { marked } from "marked";
|
|
2
|
-
import
|
|
2
|
+
import Table from "cli-table3";
|
|
3
3
|
import chalk from "chalk";
|
|
4
4
|
import boxen from "boxen";
|
|
5
5
|
import { colors, icons, theme, inkColors } from "./theme.js";
|
|
@@ -7,27 +7,258 @@ const hrLine = () => {
|
|
|
7
7
|
const cols = process.stdout.columns ?? 80;
|
|
8
8
|
return colors.muted("─".repeat(Math.min(cols, 100)));
|
|
9
9
|
};
|
|
10
|
-
marked.use(markedTerminal({
|
|
11
|
-
code: chalk.hex(theme.syntax.code),
|
|
12
|
-
codespan: chalk.hex(theme.syntax.code),
|
|
13
|
-
blockquote: chalk.hex(theme.colors.muted.main).italic,
|
|
14
|
-
strong: chalk.bold,
|
|
15
|
-
em: chalk.italic,
|
|
16
|
-
heading: chalk.hex(theme.syntax.heading).bold,
|
|
17
|
-
firstHeading: chalk.hex(theme.colors.primary.dim).underline.bold,
|
|
18
|
-
link: chalk.hex(theme.syntax.link),
|
|
19
|
-
href: chalk.hex(theme.syntax.href).underline,
|
|
20
|
-
hr: () => hrLine(),
|
|
21
|
-
}, { theme: { keyword: chalk.hex(theme.syntax.keyword), string: chalk.hex(theme.syntax.string) } }));
|
|
22
10
|
const codeStyle = (s) => chalk.hex(theme.syntax.code)(s);
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
11
|
+
function stripAnsi(input) {
|
|
12
|
+
return input.replace(/\x1b\[[0-9;]*m/g, "");
|
|
13
|
+
}
|
|
14
|
+
function visibleLen(input) {
|
|
15
|
+
return stripAnsi(input).length;
|
|
16
|
+
}
|
|
17
|
+
function preprocessMarkdown(input) {
|
|
18
|
+
return input
|
|
19
|
+
.replace(/\r\n/g, "\n")
|
|
20
|
+
.replace(/<br\s*\/?>/gi, " / ")
|
|
21
|
+
.replace(/\t/g, " ");
|
|
22
|
+
}
|
|
23
|
+
function normalizeTableCellText(input) {
|
|
24
|
+
let s = input.replace(/\s+/g, " ").trim();
|
|
25
|
+
if (!s)
|
|
26
|
+
return s;
|
|
27
|
+
// Many models emit " / " as pseudo-newlines in tables; promote to bullet lines.
|
|
28
|
+
// Keep URL-like tokens intact by only rewriting slash tokens surrounded by spaces.
|
|
29
|
+
s = s.replace(/\s+\/\s+(?=[^\s])/g, "\n• ");
|
|
30
|
+
s = s.replace(/(^|\n)\s*•\s*•\s*/g, "$1• ");
|
|
31
|
+
if (!s.startsWith("• ") && s.includes("\n• "))
|
|
32
|
+
s = "• " + s;
|
|
33
|
+
return s;
|
|
34
|
+
}
|
|
35
|
+
function splitRenderedLines(rendered) {
|
|
36
|
+
return rendered.split("\n").map((l) => l.replace(/[ \t]+$/g, ""));
|
|
37
|
+
}
|
|
38
|
+
function renderInlineTokens(tokens) {
|
|
39
|
+
if (!tokens || tokens.length === 0)
|
|
40
|
+
return "";
|
|
41
|
+
return tokens
|
|
42
|
+
.map((token) => {
|
|
43
|
+
const t = token.type ?? "";
|
|
44
|
+
if (t === "text")
|
|
45
|
+
return token.text ?? "";
|
|
46
|
+
if (t === "escape")
|
|
47
|
+
return token.text ?? "";
|
|
48
|
+
if (t === "strong")
|
|
49
|
+
return chalk.bold(renderInlineTokens(token.tokens));
|
|
50
|
+
if (t === "em")
|
|
51
|
+
return chalk.italic(renderInlineTokens(token.tokens));
|
|
52
|
+
if (t === "codespan")
|
|
53
|
+
return codeStyle(token.text ?? "");
|
|
54
|
+
if (t === "del")
|
|
55
|
+
return chalk.strikethrough(renderInlineTokens(token.tokens));
|
|
56
|
+
if (t === "link") {
|
|
57
|
+
const label = renderInlineTokens(token.tokens) || token.text || token.href || "";
|
|
58
|
+
const labelStyled = chalk.hex(theme.syntax.link).underline(label);
|
|
59
|
+
if (!token.href || label === token.href)
|
|
60
|
+
return labelStyled;
|
|
61
|
+
return `${labelStyled}${colors.mutedDark(` (${token.href})`)}`;
|
|
62
|
+
}
|
|
63
|
+
if (t === "br")
|
|
64
|
+
return "\n";
|
|
65
|
+
if (t === "html") {
|
|
66
|
+
const raw = token.raw ?? token.text ?? "";
|
|
67
|
+
if (/^<br\s*\/?\s*>$/i.test(raw.trim()))
|
|
68
|
+
return "\n";
|
|
69
|
+
return raw.replace(/<[^>]+>/g, "");
|
|
70
|
+
}
|
|
71
|
+
return token.text ?? token.raw ?? "";
|
|
72
|
+
})
|
|
73
|
+
.join("");
|
|
74
|
+
}
|
|
75
|
+
function headingRule(text) {
|
|
76
|
+
const width = Math.min(96, Math.max(24, visibleLen(text) + 8));
|
|
77
|
+
return colors.muted("─".repeat(width));
|
|
78
|
+
}
|
|
79
|
+
function renderHeading(token) {
|
|
80
|
+
const depth = token.depth ?? 2;
|
|
81
|
+
const headingText = renderInlineTokens(token.tokens) || token.text || "";
|
|
82
|
+
const clean = headingText.trim();
|
|
83
|
+
if (!clean)
|
|
84
|
+
return [];
|
|
85
|
+
if (depth <= 1) {
|
|
86
|
+
return [chalk.hex(theme.syntax.heading).bold.underline(clean), headingRule(clean), ""];
|
|
87
|
+
}
|
|
88
|
+
if (depth === 2) {
|
|
89
|
+
return [chalk.hex(theme.syntax.heading).bold(clean), headingRule(clean), ""];
|
|
90
|
+
}
|
|
91
|
+
if (depth === 3) {
|
|
92
|
+
return [chalk.hex(theme.syntax.heading).bold(clean), ""];
|
|
93
|
+
}
|
|
94
|
+
return [colors.accentDim(clean), ""];
|
|
95
|
+
}
|
|
96
|
+
function renderCodeBlock(token) {
|
|
97
|
+
const raw = (token.text ?? "").replace(/\n$/, "");
|
|
98
|
+
const lang = (token.lang ?? "").trim();
|
|
99
|
+
const body = raw || "(empty)";
|
|
100
|
+
const title = lang ? ` ${lang} ` : " code ";
|
|
101
|
+
const boxed = boxen(codeStyle(body), {
|
|
102
|
+
padding: { top: 0, bottom: 0, left: 1, right: 1 },
|
|
103
|
+
margin: { top: 0, bottom: 0, left: 0, right: 0 },
|
|
104
|
+
borderColor: inkColors.mutedDim,
|
|
105
|
+
borderStyle: "round",
|
|
106
|
+
title,
|
|
107
|
+
titleAlignment: "left",
|
|
108
|
+
});
|
|
109
|
+
return splitRenderedLines(boxed).concat("");
|
|
110
|
+
}
|
|
111
|
+
function renderTable(token) {
|
|
112
|
+
const headerCells = (token.header ?? []).map((c) => renderInlineTokens(c.tokens) || c.text || "");
|
|
113
|
+
const rows = (token.rows ?? []).map((row) => row.map((c) => normalizeTableCellText(renderInlineTokens(c.tokens) || c.text || "")));
|
|
114
|
+
const colCount = Math.max(headerCells.length, ...rows.map((r) => r.length), 0);
|
|
115
|
+
if (colCount === 0)
|
|
116
|
+
return [];
|
|
117
|
+
const paddedHeader = [...headerCells];
|
|
118
|
+
while (paddedHeader.length < colCount)
|
|
119
|
+
paddedHeader.push("");
|
|
120
|
+
const paddedRows = rows.map((r) => {
|
|
121
|
+
const copy = [...r];
|
|
122
|
+
while (copy.length < colCount)
|
|
123
|
+
copy.push("");
|
|
124
|
+
return copy;
|
|
125
|
+
});
|
|
126
|
+
const maxColsWidth = Math.max(48, (process.stdout.columns ?? 100) - 8);
|
|
127
|
+
const minColWidth = 10;
|
|
128
|
+
const maxColWidth = 40;
|
|
129
|
+
const widths = Array.from({ length: colCount }, (_, i) => {
|
|
130
|
+
const maxLen = Math.max(visibleLen(paddedHeader[i] ?? ""), ...paddedRows.map((r) => visibleLen(r[i] ?? "")), minColWidth);
|
|
131
|
+
return Math.min(maxColWidth, maxLen + 2);
|
|
132
|
+
});
|
|
133
|
+
const currentWidth = widths.reduce((a, b) => a + b, 0) + colCount + 1;
|
|
134
|
+
if (currentWidth > maxColsWidth) {
|
|
135
|
+
let excess = currentWidth - maxColsWidth;
|
|
136
|
+
while (excess > 0) {
|
|
137
|
+
let shrunk = false;
|
|
138
|
+
for (let i = 0; i < widths.length && excess > 0; i++) {
|
|
139
|
+
if ((widths[i] ?? minColWidth) > minColWidth) {
|
|
140
|
+
widths[i] = (widths[i] ?? minColWidth) - 1;
|
|
141
|
+
excess--;
|
|
142
|
+
shrunk = true;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (!shrunk)
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
const table = new Table({
|
|
150
|
+
head: paddedHeader.map((h) => chalk.bold(colors.accentPale(h))),
|
|
151
|
+
colWidths: widths,
|
|
152
|
+
wordWrap: true,
|
|
153
|
+
style: { border: [], head: [] },
|
|
154
|
+
chars: {
|
|
155
|
+
top: colors.muted("─"),
|
|
156
|
+
"top-mid": colors.muted("┬"),
|
|
157
|
+
"top-left": colors.muted("┌"),
|
|
158
|
+
"top-right": colors.muted("┐"),
|
|
159
|
+
bottom: colors.muted("─"),
|
|
160
|
+
"bottom-mid": colors.muted("┴"),
|
|
161
|
+
"bottom-left": colors.muted("└"),
|
|
162
|
+
"bottom-right": colors.muted("┘"),
|
|
163
|
+
left: colors.muted("│"),
|
|
164
|
+
"left-mid": colors.muted("├"),
|
|
165
|
+
mid: colors.muted("─"),
|
|
166
|
+
"mid-mid": colors.muted("┼"),
|
|
167
|
+
right: colors.muted("│"),
|
|
168
|
+
"right-mid": colors.muted("┤"),
|
|
169
|
+
middle: colors.muted("│"),
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
for (const row of paddedRows)
|
|
173
|
+
table.push(row);
|
|
174
|
+
return splitRenderedLines(table.toString()).concat("");
|
|
175
|
+
}
|
|
176
|
+
function renderList(token, indent) {
|
|
177
|
+
const out = [];
|
|
178
|
+
const ordered = !!token.ordered;
|
|
179
|
+
const start = Number(token.start ?? 1) || 1;
|
|
180
|
+
const items = token.items ?? [];
|
|
181
|
+
for (let i = 0; i < items.length; i++) {
|
|
182
|
+
const item = items[i] ?? {};
|
|
183
|
+
const marker = ordered ? `${start + i}.` : "•";
|
|
184
|
+
const itemLines = renderBlockTokens(item.tokens ?? [], indent + 2, true);
|
|
185
|
+
const first = (itemLines[0] ?? "").trimStart();
|
|
186
|
+
out.push(`${" ".repeat(indent)}${marker} ${first}`.trimEnd());
|
|
187
|
+
const continuationPrefix = " ".repeat(indent + marker.length + 1);
|
|
188
|
+
for (let j = 1; j < itemLines.length; j++) {
|
|
189
|
+
out.push(`${continuationPrefix}${itemLines[j] ?? ""}`.trimEnd());
|
|
190
|
+
}
|
|
191
|
+
}
|
|
29
192
|
return out;
|
|
30
193
|
}
|
|
194
|
+
function renderBlockTokens(tokens, indent = 0, compact = false) {
|
|
195
|
+
const out = [];
|
|
196
|
+
for (const token of tokens) {
|
|
197
|
+
const t = token.type ?? "";
|
|
198
|
+
if (t === "space") {
|
|
199
|
+
if (!compact)
|
|
200
|
+
out.push("");
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
if (t === "heading") {
|
|
204
|
+
out.push(...renderHeading(token));
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
if (t === "paragraph" || t === "text") {
|
|
208
|
+
const line = renderInlineTokens(token.tokens ?? [{ type: "text", text: token.text ?? "" }]);
|
|
209
|
+
if (line.trim())
|
|
210
|
+
out.push(`${" ".repeat(indent)}${line}`.trimEnd());
|
|
211
|
+
if (!compact)
|
|
212
|
+
out.push("");
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
if (t === "blockquote") {
|
|
216
|
+
const inner = renderBlockTokens(token.tokens ?? [], indent + 2, true);
|
|
217
|
+
for (const line of inner)
|
|
218
|
+
out.push(`${colors.muted("│")} ${colors.muted(line)}`.trimEnd());
|
|
219
|
+
if (!compact)
|
|
220
|
+
out.push("");
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
if (t === "list") {
|
|
224
|
+
out.push(...renderList(token, indent));
|
|
225
|
+
if (!compact)
|
|
226
|
+
out.push("");
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
if (t === "code") {
|
|
230
|
+
out.push(...renderCodeBlock(token));
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
if (t === "table") {
|
|
234
|
+
out.push(...renderTable(token));
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
if (t === "hr") {
|
|
238
|
+
out.push(hrLine(), "");
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
const fallback = (token.text ?? token.raw ?? "").trim();
|
|
242
|
+
if (fallback) {
|
|
243
|
+
out.push(`${" ".repeat(indent)}${fallback}`);
|
|
244
|
+
if (!compact)
|
|
245
|
+
out.push("");
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
while (out.length > 0 && out[out.length - 1] === "")
|
|
249
|
+
out.pop();
|
|
250
|
+
return out;
|
|
251
|
+
}
|
|
252
|
+
export function renderMarkdown(text) {
|
|
253
|
+
const normalized = preprocessMarkdown(text);
|
|
254
|
+
const tokens = marked.lexer(normalized, { gfm: true, breaks: true });
|
|
255
|
+
const lines = renderBlockTokens(tokens, 0, false)
|
|
256
|
+
.join("\n")
|
|
257
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
258
|
+
.replace(/[ \t]+\n/g, "\n")
|
|
259
|
+
.trimEnd();
|
|
260
|
+
return lines;
|
|
261
|
+
}
|
|
31
262
|
export function separator() {
|
|
32
263
|
const cols = process.stdout.columns ?? 80;
|
|
33
264
|
return colors.muted("─".repeat(Math.min(cols, 100)));
|
|
@@ -66,7 +297,7 @@ export function userPromptBox(prompt) {
|
|
|
66
297
|
const cols = process.stdout.columns ?? 80;
|
|
67
298
|
const boxWidth = Math.max(20, cols - 4);
|
|
68
299
|
const innerWidth = boxWidth - 2 - 2;
|
|
69
|
-
const text = wrapToWidth(
|
|
300
|
+
const text = wrapToWidth(prompt.trim() || "\u00A0", innerWidth);
|
|
70
301
|
return boxen(text, {
|
|
71
302
|
width: boxWidth,
|
|
72
303
|
padding: { top: 0, bottom: 0, left: 1, right: 1 },
|
|
@@ -76,14 +307,15 @@ export function userPromptBox(prompt) {
|
|
|
76
307
|
});
|
|
77
308
|
}
|
|
78
309
|
const TOOL_INDENT = " ";
|
|
79
|
-
const toolSubdued = chalk.
|
|
80
|
-
export function toolCallBox(toolName, argPreview, success = true) {
|
|
310
|
+
const toolSubdued = chalk.gray;
|
|
311
|
+
export function toolCallBox(toolName, argPreview, success = true, extraIndent = 0) {
|
|
312
|
+
const indent = TOOL_INDENT + " ".repeat(Math.max(0, extraIndent));
|
|
81
313
|
const diamondColor = success ? colors.toolSuccess : colors.toolFail;
|
|
82
|
-
const nameColor =
|
|
314
|
+
const nameColor = diamondColor;
|
|
83
315
|
const argColor = success ? toolSubdued : colors.toolFail;
|
|
84
|
-
const parenColor =
|
|
85
|
-
const name = " " + toolName.
|
|
86
|
-
return `${
|
|
316
|
+
const parenColor = colors.mutedDark;
|
|
317
|
+
const name = " " + toolName.trim().toLowerCase();
|
|
318
|
+
return `${indent}${diamondColor(icons.tool)}${nameColor(name)}${parenColor("(")}${argColor(argPreview)}${parenColor(")")}`;
|
|
87
319
|
}
|
|
88
320
|
export function toolResultLine(preview, success = true) {
|
|
89
321
|
const pipeColor = success ? toolSubdued : colors.toolFail;
|
|
@@ -95,11 +327,12 @@ function formatTokenCount(n) {
|
|
|
95
327
|
return (n / 1000).toFixed(1).replace(/\.0$/, "") + "K";
|
|
96
328
|
return String(n);
|
|
97
329
|
}
|
|
98
|
-
export function toolResultTokenLine(tokens, success = true) {
|
|
330
|
+
export function toolResultTokenLine(tokens, success = true, extraIndent = 0) {
|
|
331
|
+
const indent = TOOL_INDENT + " ".repeat(Math.max(0, extraIndent));
|
|
99
332
|
const pipeColor = success ? toolSubdued : colors.toolFail;
|
|
100
|
-
const textColor =
|
|
333
|
+
const textColor = toolSubdued;
|
|
101
334
|
const tokenStr = `+${formatTokenCount(tokens)} tokens`;
|
|
102
|
-
return `${
|
|
335
|
+
return `${indent}${TOOL_INDENT}${pipeColor(icons.pipe + " ")}${textColor(tokenStr)}`;
|
|
103
336
|
}
|
|
104
337
|
export function agentMessage(text) {
|
|
105
338
|
const rendered = renderMarkdown(text.trim());
|
package/dist/ui/theme.js
CHANGED
|
@@ -22,7 +22,7 @@ const THEME_DARK = {
|
|
|
22
22
|
error: { main: "#f87171", dim: "#dc2626" },
|
|
23
23
|
muted: { main: "#8a9a7a", dim: "#6a7a5a", dark: "#4a5a3a" },
|
|
24
24
|
background: { dark: "#1a1a1a", darker: "#0f0f0f" },
|
|
25
|
-
text: { primary: "#e2e8f0", secondary: "#
|
|
25
|
+
text: { primary: "#e2e8f0", secondary: "#98a08f", disabled: "#6f7867" },
|
|
26
26
|
},
|
|
27
27
|
ui: {
|
|
28
28
|
borderColor: "#7F9A65",
|
|
@@ -51,7 +51,7 @@ const THEME_LIGHT = {
|
|
|
51
51
|
error: { main: "#dc2626", dim: "#b91c1c" },
|
|
52
52
|
muted: { main: "#6b7a5a", dim: "#5a6a4a", dark: "#4a5a3a" },
|
|
53
53
|
background: { dark: "#f1f5f9", darker: "#e2e8f0" },
|
|
54
|
-
text: { primary: "#1e293b", secondary: "#
|
|
54
|
+
text: { primary: "#1e293b", secondary: "#5f6758", disabled: "#778070" },
|
|
55
55
|
},
|
|
56
56
|
ui: {
|
|
57
57
|
borderColor: "#5a7247",
|