UNPKG

@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
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