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