From add233c5043264d47ecc6d3339a383f41a241ae8 Mon Sep 17 00:00:00 2001
From: Taylor Mullen <ntaylormullen@google.com>
Date: Tue, 15 Apr 2025 21:41:08 -0700
Subject: Initial commit of Gemini Code CLI

This commit introduces the initial codebase for the Gemini Code CLI, a command-line interface designed to facilitate interaction with the Gemini API for software engineering tasks.

The code was migrated from a previous git repository as a single squashed commit.

Core Features & Components:

*   **Gemini Integration:** Leverages the `@google/genai` SDK to interact with the Gemini models, supporting chat history, streaming responses, and function calling (tools).
*   **Terminal UI:** Built with Ink (React for CLIs) providing an interactive chat interface within the terminal, including input prompts, message display, loading indicators, and tool interaction elements.
*   **Tooling Framework:** Implements a robust tool system allowing Gemini to interact with the local environment. Includes tools for:
    *   File system listing (`ls`)
    *   File reading (`read-file`)
    *   Content searching (`grep`)
    *   File globbing (`glob`)
    *   File editing (`edit`)
    *   File writing (`write-file`)
    *   Executing bash commands (`terminal`)
*   **State Management:** Handles the streaming state of Gemini responses and manages the conversation history.
*   **Configuration:** Parses command-line arguments (`yargs`) and loads environment variables (`dotenv`) for setup.
*   **Project Structure:** Organized into `core`, `ui`, `tools`, `config`, and `utils` directories using TypeScript. Includes basic build (`tsc`) and start scripts.

This initial version establishes the foundation for a powerful CLI tool enabling developers to use Gemini for coding assistance directly in their terminal environment.

---
Created by yours truly: __Gemini Code__
---
 .../src/ui/components/messages/DiffRenderer.tsx    | 152 +++++++++++++++++++++
 1 file changed, 152 insertions(+)
 create mode 100644 packages/cli/src/ui/components/messages/DiffRenderer.tsx

(limited to 'packages/cli/src/ui/components/messages/DiffRenderer.tsx')

diff --git a/packages/cli/src/ui/components/messages/DiffRenderer.tsx b/packages/cli/src/ui/components/messages/DiffRenderer.tsx
new file mode 100644
index 00000000..5cae9004
--- /dev/null
+++ b/packages/cli/src/ui/components/messages/DiffRenderer.tsx
@@ -0,0 +1,152 @@
+import React from 'react';
+import { Box, Text } from 'ink'
+
+interface DiffLine {
+  type: 'add' | 'del' | 'context' | 'hunk' | 'other';
+  oldLine?: number;
+  newLine?: number;
+  content: string;
+}
+
+function parseDiffWithLineNumbers(diffContent: string): DiffLine[] {
+  const lines = diffContent.split('\n');
+  const result: DiffLine[] = [];
+  let currentOldLine = 0;
+  let currentNewLine = 0;
+  let inHunk = false;
+  const hunkHeaderRegex = /^@@ -(\d+),?\d* \+(\d+),?\d* @@/;
+
+  for (const line of lines) {
+    const hunkMatch = line.match(hunkHeaderRegex);
+    if (hunkMatch) {
+      currentOldLine = parseInt(hunkMatch[1], 10);
+      currentNewLine = parseInt(hunkMatch[2], 10);
+      inHunk = true;
+      result.push({ type: 'hunk', content: line });
+      // We need to adjust the starting point because the first line number applies to the *first* actual line change/context,
+      // but we increment *before* pushing that line. So decrement here.
+      currentOldLine--;
+      currentNewLine--;
+      continue;
+    }
+    if (!inHunk) {
+       // Skip standard Git header lines more robustly
+      if (line.startsWith('--- ') || line.startsWith('+++ ') || line.startsWith('diff --git') || line.startsWith('index ') || line.startsWith('similarity index') || line.startsWith('rename from') || line.startsWith('rename to') || line.startsWith('new file mode') || line.startsWith('deleted file mode')) continue;
+      // If it's not a hunk or header, skip (or handle as 'other' if needed)
+      continue;
+    }
+    if (line.startsWith('+')) {
+      currentNewLine++; // Increment before pushing
+      result.push({ type: 'add', newLine: currentNewLine, content: line.substring(1) });
+    } else if (line.startsWith('-')) {
+      currentOldLine++; // Increment before pushing
+      result.push({ type: 'del', oldLine: currentOldLine, content: line.substring(1) });
+    } else if (line.startsWith(' ')) {
+      currentOldLine++; // Increment before pushing
+      currentNewLine++;
+      result.push({ type: 'context', oldLine: currentOldLine, newLine: currentNewLine, content: line.substring(1) });
+    } else if (line.startsWith('\\')) { // Handle "\ No newline at end of file"
+      result.push({ type: 'other', content: line });
+    }
+  }
+  return result;
+}
+
+
+interface DiffRendererProps {
+  diffContent: string;
+  filename?: string;
+  tabWidth?: number;
+}
+
+const DEFAULT_TAB_WIDTH = 4; // Spaces per tab for normalization
+
+const DiffRenderer: React.FC<DiffRendererProps> = ({ diffContent, tabWidth = DEFAULT_TAB_WIDTH }) => {
+  if (!diffContent || typeof diffContent !== 'string') {
+    return <Text color="yellow">No diff content.</Text>;
+  }
+
+  const parsedLines = parseDiffWithLineNumbers(diffContent);
+
+  // 1. Normalize whitespace (replace tabs with spaces) *before* further processing
+  const normalizedLines = parsedLines.map(line => ({
+    ...line,
+    content: line.content.replace(/\t/g, ' '.repeat(tabWidth))
+  }));
+
+  // Filter out non-displayable lines (hunks, potentially 'other') using the normalized list
+  const displayableLines = normalizedLines.filter(l => l.type !== 'hunk' && l.type !== 'other');
+
+
+  if (displayableLines.length === 0) {
+    return (
+      <Box borderStyle="round" borderColor="gray" padding={1}>
+        <Text dimColor>No changes detected.</Text>
+      </Box>
+    );
+  }
+
+  // Calculate the minimum indentation across all displayable lines
+  let baseIndentation = Infinity; // Start high to find the minimum
+  for (const line of displayableLines) {
+    // Only consider lines with actual content for indentation calculation
+    if (line.content.trim() === '') continue;
+
+    const firstCharIndex = line.content.search(/\S/); // Find index of first non-whitespace char
+    const currentIndent = (firstCharIndex === -1) ? 0 : firstCharIndex; // Indent is 0 if no non-whitespace found
+    baseIndentation = Math.min(baseIndentation, currentIndent);
+  }
+  // If baseIndentation remained Infinity (e.g., no displayable lines with content), default to 0
+  if (!isFinite(baseIndentation)) {
+    baseIndentation = 0;
+  }
+  // --- End Modification ---
+
+
+  return (
+    <Box borderStyle="round" borderColor="gray" flexDirection="column">
+      {/* Iterate over the lines that should be displayed (already normalized) */}
+      {displayableLines.map((line, index) => {
+        const key = `diff-line-${index}`;
+        let gutterNumStr = '';
+        let color: string | undefined = undefined;
+        let prefixSymbol = ' ';
+        let dim = false;
+
+        switch (line.type) {
+          case 'add':
+            gutterNumStr = (line.newLine ?? '').toString();
+            color = 'green';
+            prefixSymbol = '+';
+            break;
+          case 'del':
+            gutterNumStr = (line.oldLine ?? '').toString();
+            color = 'red';
+            prefixSymbol = '-';
+            break;
+          case 'context':
+            // Show new line number for context lines in gutter
+            gutterNumStr = (line.newLine ?? '').toString();
+            dim = true;
+            prefixSymbol = ' ';
+            break;
+        }
+
+        // Render the line content *after* stripping the calculated *minimum* baseIndentation.
+        // The line.content here is already the tab-normalized version.
+        const displayContent = line.content.substring(baseIndentation);
+
+        return (
+          // Using your original rendering structure
+          <Box key={key} flexDirection="row">
+            <Text color="gray">{gutterNumStr}  </Text>
+            <Text color={color} dimColor={dim}>{prefixSymbol} </Text>
+            <Text color={color} dimColor={dim} wrap="wrap">{displayContent}</Text>
+          </Box>
+        );
+      })}
+    </Box>
+  );
+};
+
+export default DiffRenderer;
\ No newline at end of file
-- 
cgit v1.2.3

