ideacode 1.1.9 → 1.2.1
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 +86 -41
- 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
|
}
|
|
@@ -231,6 +239,7 @@ export function Repl({ apiKey, cwd, onQuit }) {
|
|
|
231
239
|
onQuit();
|
|
232
240
|
}, [cwd, onQuit]);
|
|
233
241
|
const [loading, setLoading] = useState(false);
|
|
242
|
+
const [loadingLabel, setLoadingLabel] = useState("Thinking…");
|
|
234
243
|
const [showPalette, setShowPalette] = useState(false);
|
|
235
244
|
const [paletteIndex, setPaletteIndex] = useState(0);
|
|
236
245
|
const [showModelSelector, setShowModelSelector] = useState(false);
|
|
@@ -255,7 +264,8 @@ export function Repl({ apiKey, cwd, onQuit }) {
|
|
|
255
264
|
};
|
|
256
265
|
}, []);
|
|
257
266
|
const [spinnerTick, setSpinnerTick] = useState(0);
|
|
258
|
-
const
|
|
267
|
+
const loadingStartedAtRef = useRef(0);
|
|
268
|
+
const SPINNER = ["●○○", "○●○", "○○●", "○●○"];
|
|
259
269
|
const estimatedTokens = useMemo(() => estimateTokens(messages, undefined), [messages]);
|
|
260
270
|
const contextWindowK = useMemo(() => {
|
|
261
271
|
const ctx = modelList.find((m) => m.id === currentModel)?.context_length;
|
|
@@ -289,7 +299,9 @@ export function Repl({ apiKey, cwd, onQuit }) {
|
|
|
289
299
|
useEffect(() => {
|
|
290
300
|
if (!loading)
|
|
291
301
|
return;
|
|
292
|
-
|
|
302
|
+
loadingStartedAtRef.current = Date.now();
|
|
303
|
+
setSpinnerTick(0);
|
|
304
|
+
const t = setInterval(() => setSpinnerTick((n) => n + 1), LOADING_TICK_MS);
|
|
293
305
|
return () => clearInterval(t);
|
|
294
306
|
}, [loading]);
|
|
295
307
|
const showSlashSuggestions = inputValue.startsWith("/");
|
|
@@ -434,10 +446,12 @@ export function Repl({ apiKey, cwd, onQuit }) {
|
|
|
434
446
|
appendLog(userPromptBox(userInput));
|
|
435
447
|
appendLog("");
|
|
436
448
|
let state = [...messages, { role: "user", content: userInput }];
|
|
437
|
-
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.`;
|
|
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.`;
|
|
438
450
|
const modelContext = modelList.find((m) => m.id === currentModel)?.context_length;
|
|
439
451
|
const maxContextTokens = Math.floor((modelContext ?? CONTEXT_WINDOW_K * 1024) * 0.85);
|
|
440
452
|
const stateBeforeCompress = state;
|
|
453
|
+
setLoadingLabel("Compressing context…");
|
|
454
|
+
setLoading(true);
|
|
441
455
|
state = await ensureUnderBudget(apiKey, state, systemPrompt, currentModel, {
|
|
442
456
|
maxTokens: maxContextTokens,
|
|
443
457
|
keepLast: 6,
|
|
@@ -446,38 +460,67 @@ export function Repl({ apiKey, cwd, onQuit }) {
|
|
|
446
460
|
appendLog(colors.muted(" (context compressed to stay under limit)\n"));
|
|
447
461
|
}
|
|
448
462
|
for (;;) {
|
|
449
|
-
|
|
463
|
+
setLoadingLabel("Thinking…");
|
|
450
464
|
const response = await callApi(apiKey, state, systemPrompt, currentModel);
|
|
451
465
|
const contentBlocks = response.content ?? [];
|
|
452
466
|
const toolResults = [];
|
|
467
|
+
const renderToolOutcome = (planned, result, extraIndent = 0) => {
|
|
468
|
+
const ok = !result.startsWith("error:");
|
|
469
|
+
appendLog(toolCallBox(planned.toolName, planned.argPreview, ok, extraIndent));
|
|
470
|
+
const contentForApi = truncateToolResult(result);
|
|
471
|
+
const tokens = estimateTokensForString(contentForApi);
|
|
472
|
+
appendLog(toolResultTokenLine(tokens, ok, extraIndent));
|
|
473
|
+
if (planned.block.id) {
|
|
474
|
+
toolResults.push({ type: "tool_result", tool_use_id: planned.block.id, content: contentForApi });
|
|
475
|
+
}
|
|
476
|
+
};
|
|
477
|
+
const runParallelBatch = async (batch) => {
|
|
478
|
+
if (batch.length === 0)
|
|
479
|
+
return;
|
|
480
|
+
const started = Date.now();
|
|
481
|
+
const groupedTools = Array.from(batch.reduce((acc, planned) => {
|
|
482
|
+
acc.set(planned.toolName, (acc.get(planned.toolName) ?? 0) + 1);
|
|
483
|
+
return acc;
|
|
484
|
+
}, new Map()))
|
|
485
|
+
.map(([name, count]) => (count > 1 ? `${name}×${count}` : name))
|
|
486
|
+
.join(", ");
|
|
487
|
+
appendLog(colors.gray(` ${icons.tool} parallel batch (${batch.length}): ${groupedTools}`));
|
|
488
|
+
const settled = await Promise.all(batch.map(async (planned) => ({ planned, result: await runTool(planned.toolName, planned.toolArgs) })));
|
|
489
|
+
const elapsed = Date.now() - started;
|
|
490
|
+
appendLog(colors.gray(` completed in ${elapsed}ms`));
|
|
491
|
+
for (const { planned, result } of settled) {
|
|
492
|
+
renderToolOutcome(planned, result, 1);
|
|
493
|
+
}
|
|
494
|
+
};
|
|
495
|
+
let parallelBatch = [];
|
|
453
496
|
for (const block of contentBlocks) {
|
|
454
497
|
if (block.type === "text" && block.text?.trim()) {
|
|
455
498
|
if (lastLogLineRef.current !== "")
|
|
456
499
|
appendLog("");
|
|
457
500
|
appendLog(agentMessage(block.text).trimEnd());
|
|
501
|
+
continue;
|
|
458
502
|
}
|
|
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 });
|
|
503
|
+
if (block.type !== "tool_use" || !block.name || !block.input)
|
|
504
|
+
continue;
|
|
505
|
+
const toolName = block.name.trim().toLowerCase();
|
|
506
|
+
const toolArgs = block.input;
|
|
507
|
+
const firstVal = Object.values(toolArgs)[0];
|
|
508
|
+
const planned = {
|
|
509
|
+
block,
|
|
510
|
+
toolName,
|
|
511
|
+
toolArgs,
|
|
512
|
+
argPreview: String(firstVal ?? "").slice(0, 100) || "—",
|
|
513
|
+
};
|
|
514
|
+
if (ENABLE_PARALLEL_TOOL_CALLS && PARALLEL_SAFE_TOOLS.has(toolName)) {
|
|
515
|
+
parallelBatch.push(planned);
|
|
516
|
+
continue;
|
|
479
517
|
}
|
|
518
|
+
await runParallelBatch(parallelBatch);
|
|
519
|
+
parallelBatch = [];
|
|
520
|
+
const result = await runTool(planned.toolName, planned.toolArgs);
|
|
521
|
+
renderToolOutcome(planned, result);
|
|
480
522
|
}
|
|
523
|
+
await runParallelBatch(parallelBatch);
|
|
481
524
|
setLoading(false);
|
|
482
525
|
state = [...state, { role: "assistant", content: contentBlocks }];
|
|
483
526
|
if (toolResults.length === 0) {
|
|
@@ -892,10 +935,10 @@ export function Repl({ apiKey, cwd, onQuit }) {
|
|
|
892
935
|
const visibleModelCount = Math.min(filteredModelList.length, modelModalHeight - 4);
|
|
893
936
|
const modelScrollOffset = Math.max(0, Math.min(modelIndex - Math.floor(visibleModelCount / 2), filteredModelList.length - visibleModelCount));
|
|
894
937
|
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:
|
|
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: 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
939
|
const actualIndex = modelScrollOffset + i;
|
|
897
940
|
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:
|
|
941
|
+
})), _jsx(Text, { color: inkColors.textSecondary, children: " \u2191/\u2193 select Enter confirm Esc cancel Type to filter " })] })] }), _jsx(Box, { flexGrow: 1 })] }));
|
|
899
942
|
}
|
|
900
943
|
const slashSuggestionBoxLines = showSlashSuggestions
|
|
901
944
|
? 3 + Math.max(1, filteredSlashCommands.length)
|
|
@@ -909,7 +952,8 @@ export function Repl({ apiKey, cwd, onQuit }) {
|
|
|
909
952
|
const lines = inputValue.split("\n");
|
|
910
953
|
return lines.reduce((sum, line) => sum + Math.max(1, Math.ceil(line.length / wrapWidth)), 0);
|
|
911
954
|
})();
|
|
912
|
-
|
|
955
|
+
// Keep a fixed loading row reserved to avoid viewport jumps/flicker when loading starts/stops.
|
|
956
|
+
const reservedLines = 1 + inputLineCount + 2;
|
|
913
957
|
const logViewportHeight = Math.max(1, termRows - reservedLines - suggestionBoxLines);
|
|
914
958
|
const effectiveLogLines = logLines;
|
|
915
959
|
const maxLogScrollOffset = Math.max(0, effectiveLogLines.length - logViewportHeight);
|
|
@@ -919,9 +963,7 @@ export function Repl({ apiKey, cwd, onQuit }) {
|
|
|
919
963
|
logStartIndex = 0;
|
|
920
964
|
}
|
|
921
965
|
const sliceEnd = logStartIndex + logViewportHeight;
|
|
922
|
-
const visibleLogLines =
|
|
923
|
-
? ["", ...effectiveLogLines.slice(0, logViewportHeight - 1)]
|
|
924
|
-
: effectiveLogLines.slice(logStartIndex, sliceEnd);
|
|
966
|
+
const visibleLogLines = effectiveLogLines.slice(logStartIndex, sliceEnd);
|
|
925
967
|
if (showHelpModal) {
|
|
926
968
|
const helpModalWidth = Math.min(88, Math.max(80, termColumns - 4));
|
|
927
969
|
const helpContentRows = 20;
|
|
@@ -929,29 +971,32 @@ export function Repl({ apiKey, cwd, onQuit }) {
|
|
|
929
971
|
const helpLeftPad = Math.max(0, Math.floor((termColumns - helpModalWidth) / 2));
|
|
930
972
|
const labelWidth = 20;
|
|
931
973
|
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:
|
|
974
|
+
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
975
|
}
|
|
934
976
|
if (showBraveKeyModal) {
|
|
935
977
|
const braveModalWidth = 52;
|
|
936
978
|
const topPad = Math.max(0, Math.floor((termRows - 6) / 2));
|
|
937
979
|
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:
|
|
980
|
+
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
981
|
}
|
|
940
982
|
if (showPalette) {
|
|
941
983
|
const paletteModalHeight = COMMANDS.length + 4;
|
|
942
984
|
const paletteModalWidth = 52;
|
|
943
985
|
const topPad = Math.max(0, Math.floor((termRows - paletteModalHeight) / 2));
|
|
944
986
|
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:
|
|
987
|
+
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
988
|
}
|
|
947
989
|
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))) }),
|
|
990
|
+
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
|
|
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) => {
|
|
949
994
|
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:
|
|
995
|
+
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
|
+
})), _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
997
|
const i = filteredFilePaths.length - 1 - rev;
|
|
953
998
|
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:
|
|
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..." })] })) : ((() => {
|
|
955
1000
|
const lines = inputValue.split("\n");
|
|
956
1001
|
let lineStart = 0;
|
|
957
1002
|
return (_jsx(_Fragment, { children: lines.flatMap((lineText, lineIdx) => {
|
|
@@ -1062,7 +1107,7 @@ export function Repl({ apiKey, cwd, onQuit }) {
|
|
|
1062
1107
|
const isFirstRow = lineIdx === 0 && v === 0;
|
|
1063
1108
|
const isLastLogicalLine = lineIdx === lines.length - 1;
|
|
1064
1109
|
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, {
|
|
1110
|
+
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
1111
|
});
|
|
1067
1112
|
}) }));
|
|
1068
1113
|
})()) })] })] }));
|
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",
|