From d41a31cf362fa0f2b8a847de0a62feb7e573a8ac Mon Sep 17 00:00:00 2001 From: Ismail Pelaseyed Date: Sun, 20 Jul 2025 22:45:13 +0200 Subject: [PATCH] fix diff renderer component --- LICENSE | 21 ++ src/agent/grok-agent.ts | 53 +--- src/grok/tools.ts | 41 ---- src/tools/bash.ts | 21 ++ src/tools/confirmation-tool.ts | 5 +- src/tools/text-editor.ts | 283 +++++++++++++++------- src/types/index.ts | 1 + src/ui/components/chat-history.tsx | 6 +- src/ui/components/chat-interface.tsx | 3 + src/ui/components/confirmation-dialog.tsx | 17 ++ src/ui/components/diff-renderer.tsx | 59 +---- src/utils/confirmation-service.ts | 1 + 12 files changed, 285 insertions(+), 226 deletions(-) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f242f29 --- /dev/null +++ b/LICENSE @@ -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. \ No newline at end of file diff --git a/src/agent/grok-agent.ts b/src/agent/grok-agent.ts index edd8d08..d48b9ae 100644 --- a/src/agent/grok-agent.ts +++ b/src/agent/grok-agent.ts @@ -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 { diff --git a/src/grok/tools.ts b/src/grok/tools.ts index 40e6ee2..684d798 100644 --- a/src/grok/tools.ts +++ b/src/grok/tools.ts @@ -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: [] - } - } } ]; \ No newline at end of file diff --git a/src/tools/bash.ts b/src/tools/bash.ts index 8d675eb..a780a72 100644 --- a/src/tools/bash.ts +++ b/src/tools/bash.ts @@ -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 { 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 { diff --git a/src/tools/confirmation-tool.ts b/src/tools/confirmation-tool.ts index 55a2761..1aea91c 100644 --- a/src/tools/confirmation-tool.ts +++ b/src/tools/confirmation-tool.ts @@ -58,14 +58,15 @@ export class ConfirmationTool { async checkSessionAcceptance(): Promise { 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 { diff --git a/src/tools/text-editor.ts b/src/tools/text-editor.ts index 9bb218e..f0ce303 100644 --- a/src/tools/text-editor.ts +++ b/src/tools/text-editor.ts @@ -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 { + async view( + filePath: string, + viewRange?: [number, number] + ): Promise { 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 { + async strReplace( + filePath: string, + oldStr: string, + newStr: string + ): Promise { 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 { 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 { + async insert( + filePath: string, + insertLine: number, + content: string + ): Promise { 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]; } -} \ No newline at end of file +} diff --git a/src/types/index.ts b/src/types/index.ts index f455594..46dfdb1 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -2,6 +2,7 @@ export interface ToolResult { success: boolean; output?: string; error?: string; + data?: any; } export interface Tool { diff --git a/src/ui/components/chat-history.tsx b/src/ui/components/chat-history.tsx index cde79b7..1b09c6e 100644 --- a/src/ui/components/chat-history.tsx +++ b/src/ui/components/chat-history.tsx @@ -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 ( - + ⏺ @@ -130,6 +131,9 @@ export function ChatHistory({ entries }: ChatHistoryProps) { {renderFileContent(entry.content)} + ) : shouldShowDiff ? ( + // For diff results, show only the summary line, not the raw content + ⎿ {entry.content.split('\n')[0]} ) : ( ⎿ {entry.content} )} diff --git a/src/ui/components/chat-interface.tsx b/src/ui/components/chat-interface.tsx index fdfc48f..4505ada 100644 --- a/src/ui/components/chat-interface.tsx +++ b/src/ui/components/chat-interface.tsx @@ -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 && ( <> 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({ ⎿ Opened changes in Visual Studio Code ⧉ )} + + {/* Show content preview if provided */} + {content && ( + <> + ⎿ {content.split('\n')[0]} + + + + + )} {/* Confirmation options */} diff --git a/src/ui/components/diff-renderer.tsx b/src/ui/components/diff-renderer.tsx index 184dcee..7eb4ccc 100644 --- a/src/ui/components/diff-renderer.tsx +++ b/src/ui/components/diff-renderer.tsx @@ -118,52 +118,18 @@ export const DiffRenderer = ({ const parsedLines = parseDiffWithLineNumbers(actualDiffContent); if (parsedLines.length === 0) { - return ( - - No changes detected. - - ); + return No changes detected.; } - // 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 ( - - No changes detected. - - ); + return No changes detected.; } // Calculate the minimum indentation across all displayable lines @@ -282,7 +244,8 @@ const renderDiffContent = ( acc.push( - {gutterNumStr.padEnd(3)}{prefixSymbol} + {gutterNumStr.padEnd(4)} + {prefixSymbol} {displayContent} diff --git a/src/utils/confirmation-service.ts b/src/utils/confirmation-service.ts index 5a69467..923e488 100644 --- a/src/utils/confirmation-service.ts +++ b/src/utils/confirmation-service.ts @@ -8,6 +8,7 @@ export interface ConfirmationOptions { operation: string; filename: string; showVSCodeOpen?: boolean; + content?: string; // Content to show in confirmation dialog } export interface ConfirmationResult {