@nanocollective/nanocoder
Version:
A local-first CLI coding agent that brings the power of agentic coding tools like Claude Code and Gemini CLI to local models or controlled APIs like OpenRouter
171 lines • 12.4 kB
JavaScript
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { resolve } from 'node:path';
import { highlight } from 'cli-highlight';
import { Box, Text } from 'ink';
import ToolMessage from '../../components/tool-message.js';
import { getColors } from '../../config/index.js';
import { DEFAULT_TERMINAL_COLUMNS } from '../../constants.js';
import { truncateAnsi } from '../../utils/ansi-truncate.js';
import { getCachedFileContent } from '../../utils/file-cache.js';
import { normalizeIndentation } from '../../utils/indentation-normalizer.js';
import { areLinesSimlar, computeInlineDiff } from '../../utils/inline-diff.js';
import { getLanguageFromExtension } from '../../utils/programming-language-helper.js';
/** Truncate a plain line to fit terminal width */
const truncateLine = (line, maxWidth) => {
if (line.length <= maxWidth)
return line;
return line.slice(0, maxWidth - 1) + '…';
};
export async function formatStringReplacePreview(args, result, colors) {
const themeColors = colors || getColors();
const { path, old_str, new_str } = args;
const terminalWidth = process.stdout.columns || DEFAULT_TERMINAL_COLUMNS;
const lineNumPrefixWidth = 8;
const availableWidth = Math.max(terminalWidth - lineNumPrefixWidth - 2, 20);
const isResult = result !== undefined;
try {
const absPath = resolve(path);
const cached = await getCachedFileContent(absPath);
const fileContent = cached.content;
const ext = path.split('.').pop()?.toLowerCase() ?? '';
const language = getLanguageFromExtension(ext);
// Preview mode - validate old_str exists and is unique
if (!isResult) {
const occurrences = fileContent.split(old_str).length - 1;
if (occurrences === 0) {
return (_jsx(ToolMessage, { message: _jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { color: themeColors.tool, children: "\u2692 string_replace" }), _jsxs(Box, { children: [_jsx(Text, { color: themeColors.secondary, children: "Path: " }), _jsx(Text, { color: themeColors.primary, children: path })] }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(Text, { color: themeColors.error, children: "\u2717 Error: Content not found in file. The file may have changed since you last read it." }) })] }), hideBox: true }));
}
if (occurrences > 1) {
return (_jsx(ToolMessage, { message: _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: themeColors.tool, children: "\u2692 string_replace" }), _jsxs(Box, { children: [_jsx(Text, { color: themeColors.secondary, children: "Path: " }), _jsx(Text, { color: themeColors.primary, children: path })] }), _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { color: themeColors.error, children: ["\u2717 Error: Found ", occurrences, " matches"] }), _jsx(Text, { color: themeColors.secondary, children: "Add more surrounding context to make the match unique." })] })] }), hideBox: true }));
}
}
// Find location of the match in the file
const searchStr = isResult ? new_str : old_str;
const matchIndex = fileContent.indexOf(searchStr);
const beforeContent = fileContent.substring(0, matchIndex);
const beforeLines = beforeContent.split('\n');
const startLine = beforeLines.length;
const oldStrLines = old_str.split('\n');
const newStrLines = new_str.split('\n');
const contentLines = isResult ? newStrLines : oldStrLines;
const endLine = startLine + contentLines.length - 1;
const allLines = fileContent.split('\n');
const contextLines = 3;
const showStart = Math.max(0, startLine - 1 - contextLines);
const showEnd = Math.min(allLines.length - 1, endLine - 1 + contextLines);
// Collect all lines for normalization
const linesToNormalize = [];
for (let i = showStart; i < startLine - 1; i++) {
linesToNormalize.push(allLines[i] || '');
}
for (let i = 0; i < oldStrLines.length; i++) {
linesToNormalize.push(oldStrLines[i] || '');
}
if (isResult) {
for (let i = 0; i < newStrLines.length; i++) {
linesToNormalize.push(allLines[startLine - 1 + i] || '');
}
}
else {
for (let i = 0; i < newStrLines.length; i++) {
linesToNormalize.push(newStrLines[i] || '');
}
}
const contextAfterStart = isResult
? startLine - 1 + newStrLines.length
: endLine;
for (let i = contextAfterStart; i <= showEnd; i++) {
linesToNormalize.push(allLines[i] || '');
}
const normalizedLines = normalizeIndentation(linesToNormalize);
// Split normalized lines back into sections
let lineIndex = 0;
const contextBeforeCount = startLine - 1 - showStart;
const normalizedContextBefore = normalizedLines.slice(lineIndex, lineIndex + contextBeforeCount);
lineIndex += contextBeforeCount;
const normalizedOldLines = normalizedLines.slice(lineIndex, lineIndex + oldStrLines.length);
lineIndex += oldStrLines.length;
const normalizedNewLines = normalizedLines.slice(lineIndex, lineIndex + newStrLines.length);
lineIndex += newStrLines.length;
const normalizedContextAfter = normalizedLines.slice(lineIndex);
// Render context before
const contextBefore = normalizedContextBefore.map((line, i) => {
const actualLineNum = showStart + i;
const lineNumStr = String(actualLineNum + 1).padStart(4, ' ');
let displayLine;
try {
displayLine = truncateAnsi(highlight(line, { language, theme: 'default' }), availableWidth);
}
catch {
displayLine = truncateLine(line, availableWidth);
}
return (_jsxs(Box, { children: [_jsxs(Text, { color: themeColors.secondary, children: [lineNumStr, " "] }), _jsx(Text, { wrap: "truncate-end", children: displayLine })] }, `before-${i}`));
});
// Build unified diff
const diffLines = [];
let oldIdx = 0;
let newIdx = 0;
let diffKey = 0;
while (oldIdx < normalizedOldLines.length ||
newIdx < normalizedNewLines.length) {
const oldLine = oldIdx < normalizedOldLines.length ? normalizedOldLines[oldIdx] : null;
const newLine = newIdx < normalizedNewLines.length ? normalizedNewLines[newIdx] : null;
if (oldLine !== null && newLine !== null && oldLine === newLine) {
const lineNumStr = String(startLine + oldIdx).padStart(4, ' ');
diffLines.push(_jsxs(Box, { children: [_jsxs(Text, { color: themeColors.secondary, children: [lineNumStr, " "] }), _jsx(Text, { wrap: "truncate-end", children: truncateLine(oldLine, availableWidth) })] }, `diff-${diffKey++}`));
oldIdx++;
newIdx++;
}
else if (oldLine !== null &&
newLine !== null &&
areLinesSimlar(oldLine, newLine)) {
const truncatedOldLine = truncateLine(oldLine, availableWidth);
const truncatedNewLine = truncateLine(newLine, availableWidth);
const segments = computeInlineDiff(truncatedOldLine, truncatedNewLine);
const lineNumStr = String(startLine + oldIdx).padStart(4, ' ');
const oldParts = segments
.filter(seg => seg.type === 'unchanged' || seg.type === 'removed')
.map((seg, s) => (_jsx(Text, { bold: seg.type === 'removed', underline: seg.type === 'removed', children: seg.text }, `old-seg-${s}`)));
diffLines.push(_jsxs(Box, { children: [_jsxs(Text, { backgroundColor: themeColors.diffRemoved, color: themeColors.diffRemovedText, children: [lineNumStr, " -"] }), _jsx(Text, { wrap: "truncate-end", backgroundColor: themeColors.diffRemoved, color: themeColors.diffRemovedText, children: oldParts })] }, `diff-${diffKey++}`));
const newParts = segments
.filter(seg => seg.type === 'unchanged' || seg.type === 'added')
.map((seg, s) => (_jsx(Text, { bold: seg.type === 'added', underline: seg.type === 'added', children: seg.text }, `new-seg-${s}`)));
diffLines.push(_jsxs(Box, { children: [_jsxs(Text, { backgroundColor: themeColors.diffAdded, color: themeColors.diffAddedText, children: [lineNumStr, " +"] }), _jsx(Text, { wrap: "truncate-end", backgroundColor: themeColors.diffAdded, color: themeColors.diffAddedText, children: newParts })] }, `diff-${diffKey++}`));
oldIdx++;
newIdx++;
}
else if (oldLine !== null) {
const lineNumStr = String(startLine + oldIdx).padStart(4, ' ');
diffLines.push(_jsxs(Box, { children: [_jsxs(Text, { backgroundColor: themeColors.diffRemoved, color: themeColors.diffRemovedText, children: [lineNumStr, " -"] }), _jsx(Text, { wrap: "truncate-end", backgroundColor: themeColors.diffRemoved, color: themeColors.diffRemovedText, children: truncateLine(oldLine, availableWidth) })] }, `diff-${diffKey++}`));
oldIdx++;
}
else if (newLine !== null) {
const lineNumStr = String(startLine + newIdx).padStart(4, ' ');
diffLines.push(_jsxs(Box, { children: [_jsxs(Text, { backgroundColor: themeColors.diffAdded, color: themeColors.diffAddedText, children: [lineNumStr, " +"] }), _jsx(Text, { wrap: "truncate-end", backgroundColor: themeColors.diffAdded, color: themeColors.diffAddedText, children: truncateLine(newLine, availableWidth) })] }, `diff-${diffKey++}`));
newIdx++;
}
}
// Render context after
const lineDelta = newStrLines.length - oldStrLines.length;
const contextAfter = normalizedContextAfter.map((line, i) => {
const actualLineNum = endLine + i;
const lineNumStr = String(actualLineNum + lineDelta + 1).padStart(4, ' ');
let displayLine;
try {
displayLine = truncateAnsi(highlight(line, { language, theme: 'default' }), availableWidth);
}
catch {
displayLine = truncateLine(line, availableWidth);
}
return (_jsxs(Box, { children: [_jsxs(Text, { color: themeColors.secondary, children: [lineNumStr, " "] }), _jsx(Text, { wrap: "truncate-end", children: displayLine })] }, `after-${i}`));
});
const rangeDesc = startLine === endLine
? `line ${startLine}`
: `lines ${startLine}-${endLine}`;
return (_jsx(ToolMessage, { message: _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: themeColors.tool, children: "\u2692 string_replace" }), _jsxs(Box, { children: [_jsx(Text, { color: themeColors.secondary, children: "Path: " }), _jsx(Text, { color: themeColors.primary, children: path })] }), _jsxs(Box, { children: [_jsx(Text, { color: themeColors.secondary, children: "Location: " }), _jsx(Text, { color: themeColors.text, children: rangeDesc })] }), _jsxs(Box, { flexDirection: "column", marginTop: 1, marginBottom: 1, children: [_jsxs(Text, { color: themeColors.success, children: [isResult ? '✓ Replace completed' : '✓ Replacing', ' ', oldStrLines.length, " line", oldStrLines.length > 1 ? 's' : '', ' ', "with ", newStrLines.length, " line", newStrLines.length > 1 ? 's' : ''] }), _jsxs(Box, { flexDirection: "column", children: [contextBefore, diffLines, contextAfter] })] })] }), hideBox: true }));
}
catch (error) {
return (_jsx(ToolMessage, { message: _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: themeColors.tool, children: "\u2692 string_replace" }), _jsxs(Box, { children: [_jsx(Text, { color: themeColors.secondary, children: "Path: " }), _jsx(Text, { color: themeColors.primary, children: path })] }), _jsxs(Box, { children: [_jsx(Text, { color: themeColors.error, children: "Error: " }), _jsx(Text, { color: themeColors.error, children: error instanceof Error ? error.message : String(error) })] })] }), hideBox: true }));
}
}
//# sourceMappingURL=string-replace-preview.js.map