fix diff renderer component

This commit is contained in:
Ismail Pelaseyed committed 2025-07-20 22:45:19 +02:00
1 parent 0f5b101502
commit d41a31cf36
12 files changed
+285 -226

No files matched your search

+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) [year] [copyright holders]
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission statement shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+13 -40
View File
@@ -52,11 +52,9 @@ You have access to these tools:
- view_file: View file contents or directory listings
- create_file: Create new files with content (ONLY use this for files that don't exist yet)
- str_replace_editor: Replace text in existing files (ALWAYS use this to edit or update existing files)
- bash: Execute bash commands
- bash: Execute bash commands (use for searching, file discovery, navigation, and system operations)
- create_todo_list: Create a visual todo list for planning and tracking tasks
- update_todo_list: Update existing todos in your todo list
- request_confirmation: Request user confirmation before performing operations
- check_session_acceptance: Check if user has accepted operations for this session
IMPORTANT TOOL USAGE RULES:
- NEVER use create_file on files that already exist - this will overwrite them completely
@@ -64,6 +62,12 @@ IMPORTANT TOOL USAGE RULES:
- Before editing a file, use view_file to see its current contents
- Use create_file ONLY when creating entirely new files that don't exist
SEARCHING AND EXPLORATION:
- Use bash with commands like 'find', 'grep', 'rg' (ripgrep), 'ls', etc. for searching files and content
- Examples: 'find . -name "*.js"', 'grep -r "function" src/', 'rg "import.*react"'
- Use bash for directory navigation, file discovery, and content searching
- view_file is best for reading specific files you already know exist
When a user asks you to edit, update, modify, or change an existing file:
1. First use view_file to see the current contents
2. Then use str_replace_editor to make the specific changes
@@ -81,32 +85,10 @@ TASK PLANNING WITH TODO LISTS:
- Todo lists provide visual feedback with colors: ✅ Green (completed), 🔄 Cyan (in progress), ⏳ Yellow (pending)
- Always create todos with priorities: 'high' (🔴), 'medium' (🟡), 'low' (🟢)
MANDATORY USER CONFIRMATION SYSTEM:
CRITICAL: You MUST follow this confirmation protocol for ALL file operations and bash commands:
USER CONFIRMATION SYSTEM:
File operations (create_file, str_replace_editor) and bash commands will automatically request user confirmation before execution. The confirmation system will show users the actual content or command before they decide. Users can choose to approve individual operations or approve all operations of that type for the session.
1. BEFORE performing ANY file operation (create_file, str_replace_editor) or bash command:
- First use check_session_acceptance to see if the user has already accepted operations for this session
- If the response shows hasAnyAcceptance: true for the relevant operation type, you may proceed without additional confirmation
- If hasAnyAcceptance: false or the specific operation type is not accepted, you MUST use request_confirmation before proceeding
2. Session acceptance tracking:
- fileOperationsAccepted: true means user accepted "don't ask again" for file operations
- bashCommandsAccepted: true means user accepted "don't ask again" for bash commands
- allOperationsAccepted: true means user accepted "don't ask again" for all operations
- hasAnyAcceptance: true means user has accepted at least one type of operation
3. Confirmation workflow:
- Use request_confirmation with operation type, filename/command, and optional description
- If confirmation is rejected, DO NOT proceed with the operation
- If confirmation is accepted, proceed with the file operation or bash command
- The confirmation system will automatically track "don't ask again" preferences
4. Operation types for confirmation:
- "Create file" for create_file operations
- "Edit file" for str_replace_editor operations
- "Run bash command" for bash operations
NEVER bypass the confirmation system - it is a critical security feature that protects users from unintended operations.
If a user rejects an operation, the tool will return an error and you should not proceed with that specific operation.
Be helpful, direct, and efficient. Always explain what you're doing and show the results.
@@ -169,6 +151,7 @@ Current working directory: ${process.cwd()}`
// Execute tool calls
for (const toolCall of assistantMessage.tool_calls) {
const result = await this.executeTool(toolCall);
const toolResultEntry: ChatEntry = {
type: 'tool_result',
content: result.success ? result.output || 'Success' : result.error || 'Error occurred',
@@ -179,7 +162,7 @@ Current working directory: ${process.cwd()}`
this.chatHistory.push(toolResultEntry);
newEntries.push(toolResultEntry);
// Add tool result to messages with proper format
// Add tool result to messages with proper format (needed for AI context)
this.messages.push({
role: 'tool',
content: result.success ? (result.output || 'Success') : (result.error || 'Error'),
@@ -372,7 +355,7 @@ Current working directory: ${process.cwd()}`
toolResult: result
};
// Add tool result with proper format
// Add tool result with proper format (needed for AI context)
this.messages.push({
role: 'tool',
content: result.success ? (result.output || 'Success') : (result.error || 'Error'),
@@ -435,16 +418,6 @@ Current working directory: ${process.cwd()}`
case 'update_todo_list':
return await this.todoTool.updateTodoList(args.updates);
case 'request_confirmation':
return await this.confirmationTool.requestConfirmation({
operation: args.operation,
filename: args.filename,
description: args.description,
showVSCodeOpen: args.showVSCodeOpen
});
case 'check_session_acceptance':
return await this.confirmationTool.checkSessionAcceptance();
default:
return {
-41
View File
@@ -170,46 +170,5 @@ export const GROK_TOOLS: GrokTool[] = [
required: ['updates']
}
}
},
{
type: 'function',
function: {
name: 'request_confirmation',
description: 'Request user confirmation before performing operations. Use this tool before editing/creating/deleting files or running bash commands.',
parameters: {
type: 'object',
properties: {
operation: {
type: 'string',
description: 'Description of the operation to be performed (e.g., "Edit file", "Create file", "Run bash command")'
},
filename: {
type: 'string',
description: 'Name of the file or command to be operated on'
},
description: {
type: 'string',
description: 'Optional additional description of what will be done'
},
showVSCodeOpen: {
type: 'boolean',
description: 'Whether to offer opening the file in VS Code for review (default: false)'
}
},
required: ['operation', 'filename']
}
}
},
{
type: 'function',
function: {
name: 'check_session_acceptance',
description: 'Check if the user has already accepted operations for this session to avoid unnecessary confirmation prompts',
parameters: {
type: 'object',
properties: {},
required: []
}
}
}
];
+21
View File
@@ -1,15 +1,36 @@
import { exec } from 'child_process';
import { promisify } from 'util';
import { ToolResult } from '../types';
import { ConfirmationService } from '../utils/confirmation-service';
const execAsync = promisify(exec);
export class BashTool {
private currentDirectory: string = process.cwd();
private confirmationService = ConfirmationService.getInstance();
async execute(command: string, timeout: number = 30000): Promise<ToolResult> {
try {
// Check if user has already accepted bash commands for this session
const sessionFlags = this.confirmationService.getSessionFlags();
if (!sessionFlags.bashCommands && !sessionFlags.allOperations) {
// Request confirmation showing the command
const confirmationResult = await this.confirmationService.requestConfirmation({
operation: 'Run bash command',
filename: command,
showVSCodeOpen: false,
content: `Command: ${command}\nWorking directory: ${this.currentDirectory}`
}, 'bash');
if (!confirmationResult.confirmed) {
return {
success: false,
error: confirmationResult.feedback || 'Command execution cancelled by user'
};
}
}
if (command.startsWith('cd ')) {
const newDir = command.substring(3).trim();
try {
+3 -2
View File
@@ -58,14 +58,15 @@ export class ConfirmationTool {
async checkSessionAcceptance(): Promise<ToolResult> {
try {
const sessionFlags = this.confirmationService.getSessionFlags();
// Return structured data without JSON output to avoid displaying raw JSON
return {
success: true,
output: JSON.stringify({
data: {
fileOperationsAccepted: sessionFlags.fileOperations,
bashCommandsAccepted: sessionFlags.bashCommands,
allOperationsAccepted: sessionFlags.allOperations,
hasAnyAcceptance: sessionFlags.fileOperations || sessionFlags.bashCommands || sessionFlags.allOperations
})
}
};
} catch (error: any) {
return {
+189 -94
View File
@@ -1,109 +1,147 @@
import * as fs from 'fs-extra';
import * as path from 'path';
import { ToolResult, EditorCommand } from '../types';
import * as fs from "fs-extra";
import * as path from "path";
import { ToolResult, EditorCommand } from "../types";
import { ConfirmationService } from "../utils/confirmation-service";
export class TextEditorTool {
private editHistory: EditorCommand[] = [];
private confirmationService = ConfirmationService.getInstance();
async view(filePath: string, viewRange?: [number, number]): Promise<ToolResult> {
async view(
filePath: string,
viewRange?: [number, number]
): Promise<ToolResult> {
try {
const resolvedPath = path.resolve(filePath);
if (await fs.pathExists(resolvedPath)) {
const stats = await fs.stat(resolvedPath);
if (stats.isDirectory()) {
const files = await fs.readdir(resolvedPath);
return {
success: true,
output: `Directory contents of ${filePath}:\n${files.join('\n')}`
output: `Directory contents of ${filePath}:\n${files.join("\n")}`,
};
}
const content = await fs.readFile(resolvedPath, 'utf-8');
const lines = content.split('\n');
const content = await fs.readFile(resolvedPath, "utf-8");
const lines = content.split("\n");
if (viewRange) {
const [start, end] = viewRange;
const selectedLines = lines.slice(start - 1, end);
const numberedLines = selectedLines.map((line, idx) =>
`${start + idx}: ${line}`
).join('\n');
const numberedLines = selectedLines
.map((line, idx) => `${start + idx}: ${line}`)
.join("\n");
return {
success: true,
output: `Lines ${start}-${end} of ${filePath}:\n${numberedLines}`
output: `Lines ${start}-${end} of ${filePath}:\n${numberedLines}`,
};
}
const totalLines = lines.length;
const displayLines = totalLines > 10 ? lines.slice(0, 10) : lines;
const numberedLines = displayLines.map((line, idx) =>
`${idx + 1}: ${line}`
).join('\n');
const additionalLinesMessage = totalLines > 10 ? `\n... +${totalLines - 10} lines` : '';
const numberedLines = displayLines
.map((line, idx) => `${idx + 1}: ${line}`)
.join("\n");
const additionalLinesMessage =
totalLines > 10 ? `\n... +${totalLines - 10} lines` : "";
return {
success: true,
output: `Contents of ${filePath}:\n${numberedLines}${additionalLinesMessage}`
output: `Contents of ${filePath}:\n${numberedLines}${additionalLinesMessage}`,
};
} else {
return {
success: false,
error: `File or directory not found: ${filePath}`
error: `File or directory not found: ${filePath}`,
};
}
} catch (error: any) {
return {
success: false,
error: `Error viewing ${filePath}: ${error.message}`
error: `Error viewing ${filePath}: ${error.message}`,
};
}
}
async strReplace(filePath: string, oldStr: string, newStr: string): Promise<ToolResult> {
async strReplace(
filePath: string,
oldStr: string,
newStr: string
): Promise<ToolResult> {
try {
const resolvedPath = path.resolve(filePath);
if (!(await fs.pathExists(resolvedPath))) {
return {
success: false,
error: `File not found: ${filePath}`
error: `File not found: ${filePath}`,
};
}
const content = await fs.readFile(resolvedPath, 'utf-8');
const content = await fs.readFile(resolvedPath, "utf-8");
if (!content.includes(oldStr)) {
return {
success: false,
error: `String not found in file: "${oldStr}"`
error: `String not found in file: "${oldStr}"`,
};
}
// Check if user has already accepted file operations for this session
const sessionFlags = this.confirmationService.getSessionFlags();
if (!sessionFlags.fileOperations && !sessionFlags.allOperations) {
// Create a proper diff preview showing the change
const newContent = content.replace(oldStr, newStr);
const oldLines = content.split("\n");
const newLines = newContent.split("\n");
const diffContent = this.generateDiff(oldLines, newLines, filePath);
const confirmationResult =
await this.confirmationService.requestConfirmation(
{
operation: "Edit file",
filename: filePath,
showVSCodeOpen: false,
content: diffContent,
},
"file"
);
if (!confirmationResult.confirmed) {
return {
success: false,
error: confirmationResult.feedback || "File edit cancelled by user",
};
}
}
const newContent = content.replace(oldStr, newStr);
await fs.writeFile(resolvedPath, newContent, 'utf-8');
await fs.writeFile(resolvedPath, newContent, "utf-8");
this.editHistory.push({
command: 'str_replace',
command: "str_replace",
path: filePath,
old_str: oldStr,
new_str: newStr
new_str: newStr,
});
// Generate diff output
const oldLines = content.split('\n');
const newLines = newContent.split('\n');
const oldLines = content.split("\n");
const newLines = newContent.split("\n");
const diff = this.generateDiff(oldLines, newLines, filePath);
return {
success: true,
output: diff
output: diff,
};
} catch (error: any) {
return {
success: false,
error: `Error replacing text in ${filePath}: ${error.message}`
error: `Error replacing text in ${filePath}: ${error.message}`,
};
}
}
@@ -111,68 +149,105 @@ export class TextEditorTool {
async create(filePath: string, content: string): Promise<ToolResult> {
try {
const resolvedPath = path.resolve(filePath);
const dir = path.dirname(resolvedPath);
// Check if user has already accepted file operations for this session
const sessionFlags = this.confirmationService.getSessionFlags();
if (!sessionFlags.fileOperations && !sessionFlags.allOperations) {
// Create a diff-style preview for file creation
const contentLines = content.split("\n");
const diffContent = [
`Created ${filePath}`,
`--- /dev/null`,
`+++ b/${filePath}`,
`@@ -0,0 +1,${contentLines.length} @@`,
...contentLines.map((line) => `+${line}`),
].join("\n");
const confirmationResult =
await this.confirmationService.requestConfirmation(
{
operation: "Write",
filename: filePath,
showVSCodeOpen: false,
content: diffContent,
},
"file"
);
if (!confirmationResult.confirmed) {
return {
success: false,
error:
confirmationResult.feedback || "File creation cancelled by user",
};
}
}
const dir = path.dirname(resolvedPath);
await fs.ensureDir(dir);
await fs.writeFile(resolvedPath, content, 'utf-8');
await fs.writeFile(resolvedPath, content, "utf-8");
this.editHistory.push({
command: 'create',
command: "create",
path: filePath,
content
content,
});
// Generate diff output using the same method as str_replace
const oldLines: string[] = []; // Empty for new files
const newLines = content.split('\n');
const newLines = content.split("\n");
const diff = this.generateDiff(oldLines, newLines, filePath);
return {
success: true,
output: diff
output: diff,
};
} catch (error: any) {
return {
success: false,
error: `Error creating ${filePath}: ${error.message}`
error: `Error creating ${filePath}: ${error.message}`,
};
}
}
async insert(filePath: string, insertLine: number, content: string): Promise<ToolResult> {
async insert(
filePath: string,
insertLine: number,
content: string
): Promise<ToolResult> {
try {
const resolvedPath = path.resolve(filePath);
if (!(await fs.pathExists(resolvedPath))) {
return {
success: false,
error: `File not found: ${filePath}`
error: `File not found: ${filePath}`,
};
}
const fileContent = await fs.readFile(resolvedPath, 'utf-8');
const lines = fileContent.split('\n');
const fileContent = await fs.readFile(resolvedPath, "utf-8");
const lines = fileContent.split("\n");
lines.splice(insertLine - 1, 0, content);
const newContent = lines.join('\n');
await fs.writeFile(resolvedPath, newContent, 'utf-8');
const newContent = lines.join("\n");
await fs.writeFile(resolvedPath, newContent, "utf-8");
this.editHistory.push({
command: 'insert',
command: "insert",
path: filePath,
insert_line: insertLine,
content
content,
});
return {
success: true,
output: `Successfully inserted content at line ${insertLine} in ${filePath}`
output: `Successfully inserted content at line ${insertLine} in ${filePath}`,
};
} catch (error: any) {
return {
success: false,
error: `Error inserting content in ${filePath}: ${error.message}`
error: `Error inserting content in ${filePath}: ${error.message}`,
};
}
}
@@ -181,62 +256,77 @@ export class TextEditorTool {
if (this.editHistory.length === 0) {
return {
success: false,
error: 'No edits to undo'
error: "No edits to undo",
};
}
const lastEdit = this.editHistory.pop()!;
try {
switch (lastEdit.command) {
case 'str_replace':
case "str_replace":
if (lastEdit.path && lastEdit.old_str && lastEdit.new_str) {
const content = await fs.readFile(lastEdit.path, 'utf-8');
const revertedContent = content.replace(lastEdit.new_str, lastEdit.old_str);
await fs.writeFile(lastEdit.path, revertedContent, 'utf-8');
const content = await fs.readFile(lastEdit.path, "utf-8");
const revertedContent = content.replace(
lastEdit.new_str,
lastEdit.old_str
);
await fs.writeFile(lastEdit.path, revertedContent, "utf-8");
}
break;
case 'create':
case "create":
if (lastEdit.path) {
await fs.remove(lastEdit.path);
}
break;
case 'insert':
case "insert":
if (lastEdit.path && lastEdit.insert_line) {
const content = await fs.readFile(lastEdit.path, 'utf-8');
const lines = content.split('\n');
const content = await fs.readFile(lastEdit.path, "utf-8");
const lines = content.split("\n");
lines.splice(lastEdit.insert_line - 1, 1);
await fs.writeFile(lastEdit.path, lines.join('\n'), 'utf-8');
await fs.writeFile(lastEdit.path, lines.join("\n"), "utf-8");
}
break;
}
return {
success: true,
output: `Successfully undid ${lastEdit.command} operation`
output: `Successfully undid ${lastEdit.command} operation`,
};
} catch (error: any) {
return {
success: false,
error: `Error undoing edit: ${error.message}`
error: `Error undoing edit: ${error.message}`,
};
}
}
private generateDiff(oldLines: string[], newLines: string[], filePath: string): string {
private generateDiff(
oldLines: string[],
newLines: string[],
filePath: string
): string {
// Count actual changes
let addedLines = 0;
let removedLines = 0;
let i = 0, j = 0;
let i = 0,
j = 0;
// Simple algorithm to detect changes
while (i < oldLines.length || j < newLines.length) {
if (i < oldLines.length && j < newLines.length && oldLines[i] === newLines[j]) {
if (
i < oldLines.length &&
j < newLines.length &&
oldLines[i] === newLines[j]
) {
i++;
j++;
} else if (i < oldLines.length && (j >= newLines.length || oldLines[i] !== newLines[j])) {
} else if (
i < oldLines.length &&
(j >= newLines.length || oldLines[i] !== newLines[j])
) {
removedLines++;
i++;
} else if (j < newLines.length) {
@@ -244,32 +334,37 @@ export class TextEditorTool {
j++;
}
}
let summary = `Updated ${filePath}`;
if (addedLines > 0 && removedLines > 0) {
summary += ` with ${addedLines} addition${addedLines !== 1 ? 's' : ''} and ${removedLines} removal${removedLines !== 1 ? 's' : ''}`;
summary += ` with ${addedLines} addition${
addedLines !== 1 ? "s" : ""
} and ${removedLines} removal${removedLines !== 1 ? "s" : ""}`;
} else if (addedLines > 0) {
summary += ` with ${addedLines} addition${addedLines !== 1 ? 's' : ''}`;
summary += ` with ${addedLines} addition${addedLines !== 1 ? "s" : ""}`;
} else if (removedLines > 0) {
summary += ` with ${removedLines} removal${removedLines !== 1 ? 's' : ''}`;
summary += ` with ${removedLines} removal${
removedLines !== 1 ? "s" : ""
}`;
} else {
summary += ' with changes';
summary += " with changes";
}
// Generate proper git-style diff format
let diff = summary + '\n';
let diff = summary + "\n";
diff += `--- a/${filePath}\n`;
diff += `+++ b/${filePath}\n`;
diff += `@@ -1,${oldLines.length} +1,${newLines.length} @@\n`;
// Generate unified diff
i = 0; j = 0;
i = 0;
j = 0;
const CONTEXT_LINES = 3;
while (i < oldLines.length || j < newLines.length) {
const oldLine = i < oldLines.length ? oldLines[i] : null;
const newLine = j < newLines.length ? newLines[j] : null;
if (oldLine === newLine && oldLine !== null) {
// Context line
diff += ` ${oldLine}\n`;
@@ -288,11 +383,11 @@ export class TextEditorTool {
}
}
}
return diff.trim();
}
getEditHistory(): EditorCommand[] {
return [...this.editHistory];
}
}
}
+1
View File
@@ -2,6 +2,7 @@ export interface ToolResult {
success: boolean;
output?: string;
error?: string;
data?: any;
}
export interface Tool {
+5 -1
View File
@@ -109,12 +109,13 @@ export function ChatHistory({ entries }: ChatHistoryProps) {
const actionName = getToolActionName(toolName);
const filePath = getToolFilePath(entry.toolCall);
const shouldShowDiff =
toolName === "str_replace_editor" || toolName === "create_file";
const shouldShowFileContent = toolName === "view_file";
return (
<Box key={index} flexDirection="column">
<Box key={index} flexDirection="column" marginTop={1}>
<Box>
<Text color="magenta">⏺</Text>
<Text color="white">
@@ -130,6 +131,9 @@ export function ChatHistory({ entries }: ChatHistoryProps) {
{renderFileContent(entry.content)}
</Box>
</Box>
) : shouldShowDiff ? (
// For diff results, show only the summary line, not the raw content
<Text color="gray">⎿ {entry.content.split('\n')[0]}</Text>
) : (
<Text color="gray">⎿ {entry.content}</Text>
)}
+3
View File
@@ -87,6 +87,7 @@ export default function ChatInterface({ agent }: ChatInterfaceProps) {
};
}, [confirmationService]);
useEffect(() => {
if (!isProcessing && !isStreaming) {
setProcessingTime(0);
@@ -141,11 +142,13 @@ export default function ChatInterface({ agent }: ChatInterfaceProps) {
operation={confirmationOptions.operation}
filename={confirmationOptions.filename}
showVSCodeOpen={confirmationOptions.showVSCodeOpen}
content={confirmationOptions.content}
onConfirm={handleConfirmation}
onReject={handleRejection}
/>
)}
{!confirmationOptions && (
<>
<LoadingSpinner
+17
View File
@@ -1,5 +1,6 @@
import React, { useState } from "react";
import { Box, Text, useInput } from "ink";
import { DiffRenderer } from "./diff-renderer";
interface ConfirmationDialogProps {
operation: string;
@@ -7,6 +8,7 @@ interface ConfirmationDialogProps {
onConfirm: (dontAskAgain?: boolean) => void;
onReject: (feedback?: string) => void;
showVSCodeOpen?: boolean;
content?: string; // Optional content to show (file content or command)
}
export default function ConfirmationDialog({
@@ -15,6 +17,7 @@ export default function ConfirmationDialog({
onConfirm,
onReject,
showVSCodeOpen = false,
content,
}: ConfirmationDialogProps) {
const [selectedOption, setSelectedOption] = useState(0);
const [feedbackMode, setFeedbackMode] = useState(false);
@@ -124,6 +127,20 @@ export default function ConfirmationDialog({
<Text color="gray">⎿ Opened changes in Visual Studio Code ⧉</Text>
</Box>
)}
{/* Show content preview if provided */}
{content && (
<>
<Text color="gray">⎿ {content.split('\n')[0]}</Text>
<Box marginLeft={4} flexDirection="column">
<DiffRenderer
diffContent={content}
filename={filename}
terminalWidth={80}
/>
</Box>
</>
)}
</Box>
{/* Confirmation options */}
+11 -48
View File
@@ -118,52 +118,18 @@ export const DiffRenderer = ({
const parsedLines = parseDiffWithLineNumbers(actualDiffContent);
if (parsedLines.length === 0) {
return (
<Box borderStyle="round" borderColor={Colors.Gray} padding={1}>
<Text dimColor>No changes detected.</Text>
</Box>
);
return <Text dimColor>No changes detected.</Text>;
}
// Check if the diff represents a new file (only additions and header lines)
const isNewFile = parsedLines.every(
(line) =>
line.type === 'add' ||
line.type === 'hunk' ||
line.type === 'other' ||
line.content.startsWith('diff --git') ||
line.content.startsWith('new file mode'),
// Always render as diff format to show line numbers and + signs
const renderedOutput = renderDiffContent(
parsedLines,
filename,
tabWidth,
availableTerminalHeight,
terminalWidth,
);
let renderedOutput: React.ReactNode;
if (isNewFile) {
// Extract only the added lines' content
const addedContent = parsedLines
.filter((line) => line.type === 'add')
.map((line) => line.content)
.join('\n');
// Attempt to infer language from filename, default to plain text if no filename
const fileExtension = filename?.split('.').pop() || null;
const language = fileExtension
? getLanguageFromExtension(fileExtension)
: null;
renderedOutput = colorizeCode(
addedContent,
language,
availableTerminalHeight,
terminalWidth,
);
} else {
renderedOutput = renderDiffContent(
parsedLines,
filename,
tabWidth,
availableTerminalHeight,
terminalWidth,
);
}
return <>{renderedOutput}</>;
};
@@ -186,11 +152,7 @@ const renderDiffContent = (
);
if (displayableLines.length === 0) {
return (
<Box borderStyle="round" borderColor={Colors.Gray} padding={1}>
<Text dimColor>No changes detected.</Text>
</Box>
);
return <Text dimColor>No changes detected.</Text>;
}
// Calculate the minimum indentation across all displayable lines
@@ -282,7 +244,8 @@ const renderDiffContent = (
acc.push(
<Box key={lineKey} flexDirection="row">
<Text color={backgroundColor ? '#000000' : Colors.Gray} backgroundColor={backgroundColor} dimColor={!backgroundColor && dim}>{gutterNumStr.padEnd(3)}{prefixSymbol} </Text>
<Text color={Colors.Gray} dimColor={dim}>{gutterNumStr.padEnd(4)}</Text>
<Text color={backgroundColor ? '#000000' : undefined} backgroundColor={backgroundColor} dimColor={!backgroundColor && dim}>{prefixSymbol} </Text>
<Text color={backgroundColor ? '#000000' : undefined} backgroundColor={backgroundColor} dimColor={!backgroundColor && dim} wrap="wrap">
{displayContent}
</Text>
+1
View File
@@ -8,6 +8,7 @@ export interface ConfirmationOptions {
operation: string;
filename: string;
showVSCodeOpen?: boolean;
content?: string; // Content to show in confirmation dialog
}
export interface ConfirmationResult {