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

137 lines 5.94 kB
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; /** * Git Diff Tool * * View changes between states (staged, unstaged, or against a commit/branch). */ import { Box, Text } from 'ink'; import { useTheme } from '../../hooks/useTheme.js'; import { jsonSchema, tool } from '../../types/core.js'; import { execGit, truncateDiff } from './utils.js'; // ============================================================================ // Execution // ============================================================================ const executeGitDiff = async (args) => { try { const gitArgs = ['diff']; // Add --cached for staged changes if (args.staged) { gitArgs.push('--cached'); } // Add base reference (branch or commit) if (args.base) { gitArgs.push(args.base); } // Show stat only if (args.stat) { gitArgs.push('--stat'); } // Specific file if (args.file) { gitArgs.push('--', args.file); } const output = await execGit(gitArgs); if (!output.trim()) { if (args.staged) { return 'No staged changes.'; } if (args.base) { return `No differences with ${args.base}.`; } return 'No unstaged changes.'; } // Truncate if too long (unless stat mode which is already compact) if (!args.stat) { const { content, truncated, totalLines } = truncateDiff(output, 500); if (truncated) { return `${content}\n\n[Total: ${totalLines} lines]`; } } return output; } catch (error) { return `Error: ${error instanceof Error ? error.message : 'Unknown error'}`; } }; // ============================================================================ // Tool Definition // ============================================================================ const gitDiffCoreTool = tool({ description: 'View git diff of changes. Shows unstaged changes by default, use staged=true for staged changes, or base to compare against a branch/commit.', inputSchema: jsonSchema({ type: 'object', properties: { staged: { type: 'boolean', description: 'Show staged changes instead of unstaged (default: false)', }, file: { type: 'string', description: 'Show diff for a specific file only', }, base: { type: 'string', description: 'Compare against a branch or commit (e.g., "main", "HEAD~3")', }, stat: { type: 'boolean', description: 'Show only diffstat summary instead of full diff', }, }, required: [], }), // AUTO - read-only operation, never needs approval needsApproval: () => false, execute: async (args, _options) => { return await executeGitDiff(args); }, }); // ============================================================================ // Formatter // ============================================================================ function GitDiffFormatter({ args, result, }) { const { colors } = useTheme(); // Parse result for stats let filesChanged = 0; let insertions = 0; let deletions = 0; let isEmpty = false; if (result) { isEmpty = result.includes('No staged changes') || result.includes('No unstaged changes') || result.includes('No differences'); // Parse diffstat summary line const statMatch = result.match(/(\d+) files? changed(?:, (\d+) insertions?\(\+\))?(?:, (\d+) deletions?\(-\))?/); if (statMatch) { filesChanged = parseInt(statMatch[1], 10) || 0; insertions = parseInt(statMatch[2], 10) || 0; deletions = parseInt(statMatch[3], 10) || 0; } } // Determine what we're comparing let comparing = 'working tree vs HEAD'; if (args.staged) { comparing = 'staged vs HEAD'; } if (args.base) { comparing = `working tree vs ${args.base}`; if (args.staged) { comparing = `staged vs ${args.base}`; } } return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { color: colors.tool, children: "\u2692 git_diff" }), _jsxs(Box, { children: [_jsx(Text, { color: colors.secondary, children: "Comparing: " }), _jsx(Text, { color: colors.text, children: comparing })] }), args.file && (_jsxs(Box, { children: [_jsx(Text, { color: colors.secondary, children: "File: " }), _jsx(Text, { color: colors.primary, children: args.file })] })), args.stat && (_jsxs(Box, { children: [_jsx(Text, { color: colors.secondary, children: "Mode: " }), _jsx(Text, { color: colors.text, children: "stat only" })] })), isEmpty && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.success, children: "\u2713 No changes" }) })), !isEmpty && filesChanged > 0 && (_jsxs(Box, { children: [_jsx(Text, { color: colors.secondary, children: "Stats: " }), _jsxs(Text, { color: colors.text, children: [filesChanged, " files, "] }), _jsxs(Text, { color: colors.success, children: ["+", insertions] }), _jsx(Text, { color: colors.text, children: ", " }), _jsxs(Text, { color: colors.error, children: ["-", deletions] })] }))] })); } const formatter = (args, result) => { return _jsx(GitDiffFormatter, { args: args, result: result }); }; // ============================================================================ // Export // ============================================================================ export const gitDiffTool = { name: 'git_diff', tool: gitDiffCoreTool, formatter, readOnly: true, }; //# sourceMappingURL=git-diff.js.map