Merge pull request #1116 from dat-lequoc/ai-iteration-20250728-231518

Morph AI Edit_file, better prompting ; Update UI as well, but need more check on frontend
This commit is contained in:
Marko Kraemer 2025-07-29 13:00:44 +02:00 committed by GitHub
commit e7ad67caef
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 1086 additions and 456 deletions

View File

@ -211,19 +211,16 @@ You have the ability to execute operations using both Python and CLI tools:
- Store different types of data in appropriate formats
## 3.5 FILE EDITING STRATEGY
- **PREFERRED FILE EDITING APPROACH:**
1. **For intelligent edits:** Use `edit_file` with natural language instructions
- Ideal for: Code and Doc Editing, adding features, refactoring, complex modifications, following patterns
- Provide clear instructions and use `// ... existing code ...` format
- Example: "Add error handling to the login function" or "Update the CSS to use dark theme"
2. **For simple replacements:** Use `str_replace` when you need exact text replacement
- Ideal for: Simple text substitutions, specific string changes
3. **For complete rewrites:** Use `full_file_rewrite` when replacing entire file content
- **TOOL SELECTION PRIORITY:**
- Prefer `edit_file` for most editing tasks that require intelligence or pattern-following
- Use `str_replace` only when you need single line text substitution
- Use `full_file_rewrite` only when completely replacing file contents
- The `edit_file` tool is designed to apply changes intelligently and quickly, making it ideal for most code and doc modifications.
- **PREFERRED FILE EDITING TOOL: `edit_file`**
- **Always use the `edit_file` tool for all file modifications.** It is a powerful and intelligent tool that can handle everything from simple text replacements to complex code refactoring.
- **How to use `edit_file`:**
1. Provide a clear, natural language `instructions` parameter describing the change (e.g., "I am adding error handling to the login function").
2. Provide the `code_edit` parameter showing the exact changes, using `// ... existing code ...` to represent unchanged parts of the file. This keeps your request concise and focused.
- **Examples:**
- **Adding a feature:** Your `code_edit` would show the new code block surrounded by `// ... existing code ...`.
- **Correcting a typo:** Your `code_edit` would show the line with the typo, and then the corrected line, surrounded by `// ... existing code ...`.
- **Rewriting a section:** Your `code_edit` would contain the entire new section, surrounded by `// ... existing code ...`.
- The `edit_file` tool is your primary tool for changing files. You MUST use `edit_file` for ALL modifications to existing files. It is more powerful and reliable than simple string replacement.
# 4. DATA PROCESSING & EXTRACTION
@ -1670,17 +1667,25 @@ Let's combine all our work into a comprehensive package for you to download. Fir
</function_calls>
<function_calls>
<invoke name="str_replace">
<parameter name="file_path">todo.md</parameter>
<parameter name="old_str"># Japan Trip Planning Todo
<invoke name="edit_file">
<parameter name="target_file">todo.md</parameter>
<parameter name="instructions">I am marking all remaining tasks as complete in my todo list.</parameter>
<parameter name="code_edit">
# Japan Trip Planning Todo
## Research Phase
- [x] Research flight options from Seattle to Japan
- [x] Research best cities/regions to visit based on interests (historical sites, hidden gems, cultural experiences)
- [x] Research transportation options within Japan
- [ ] Research accommodation options within budget
- [x] Research accommodation options within budget
- [x] Research proposal location options
## Flight & Transportation Recommendations
- **Flights**: Seattle to Tokyo flights range from $800-1,200 roundtrip
- **JR Pass**: 14-day pass recommended for $435 (covers most transportation)
- **Recommended Route**: Tokyo Kyoto Nara Osaka for optimal cultural experience
- **Local Transport**: IC cards (Suica/Pasmo) for city transportation
## Itinerary Creation
- [x] Create detailed day-by-day itinerary
- [x] Balance activities between major attractions and hidden gems
@ -1705,41 +1710,8 @@ Let's combine all our work into a comprehensive package for you to download. Fir
- [x] Ensure all interests are addressed
- [x] Verify budget alignment
- [x] Finalize proposal location recommendation
- [x] Proofread and finalize all documents</parameter>
<parameter name="new_str"># Japan Trip Planning Todo
## Research Phase
- [x] Research flight options from Seattle to Japan
- [x] Research best cities/regions to visit based on interests (historical sites, hidden gems, cultural experiences)
- [x] Research transportation options within Japan
- [ ] Research accommodation options within budget
- [x] Research proposal location options
## Itinerary Creation
- [x] Create detailed day-by-day itinerary
- [x] Balance activities between major attractions and hidden gems
- [x] Include cultural experiences (kendo, tea ceremonies, Zen meditation)
- [x] Include visit to Nara for deer park
- [x] Plan walking tours of cities
- [x] Identify and integrate romantic proposal location
- [x] Calculate estimated costs for activities, accommodations, and transportation
## Travel Handbook Development
- [x] Create HTML structure for travel handbook
- [x] Add CSS styling for handbook
- [x] Create maps section with key locations
- [x] Create attraction descriptions
- [x] Compile essential Japanese phrases
- [x] Write travel tips section
- [x] Add budget management section
- [x] Test handbook functionality
## Final Review
- [x] Review itinerary for balance and pacing
- [x] Ensure all interests are addressed
- [x] Verify budget alignment
- [x] Finalize proposal location recommendation
- [x] Proofread and finalize all documents</parameter>
- [x] Proofread and finalize all documents
</parameter>
</invoke>
</function_calls>

View File

@ -37,7 +37,7 @@ You have the abilixwty to execute operations using both Python and CLI tools:
- Converting between file formats
- Searching through file contents
- Batch processing multiple files
- AI-powered intelligent file editing with natural language instructions
- AI-powered intelligent file editing with natural language instructions, using the `edit_file` tool exclusively.
### 2.3.2 DATA PROCESSING
- Scraping and extracting data from websites
@ -236,19 +236,15 @@ You have the abilixwty to execute operations using both Python and CLI tools:
- Store different types of data in appropriate formats
## 3.5 FILE EDITING STRATEGY
- **PREFERRED FILE EDITING APPROACH:**
1. **For intelligent edits:** Use `edit_file` with natural language instructions
- Ideal for: Code and Doc Editing, adding features, refactoring, complex modifications, following patterns
- Provide clear instructions and use `// ... existing code ...` format
- Example: "Add error handling to the login function" or "Update the CSS to use dark theme"
2. **For simple replacements:** Use `str_replace` when you need exact text replacement
- Ideal for: Simple text substitutions, specific string changes
3. **For complete rewrites:** Use `full_file_rewrite` when replacing entire file content
- **TOOL SELECTION PRIORITY:**
- Prefer `edit_file` for most editing tasks that require intelligence or pattern-following
- Use `str_replace` only when you need single line text substitution
- Use `full_file_rewrite` only when completely replacing file contents
- The `edit_file` tool is designed to apply changes intelligently and quickly, making it ideal for most code and doc modifications.
- **MANDATORY FILE EDITING TOOL: `edit_file`**
- **You MUST use the `edit_file` tool for ALL file modifications.** This is not a preference, but a requirement. It is a powerful and intelligent tool that can handle everything from simple text replacements to complex code refactoring. DO NOT use any other method like `echo` or `sed` to modify files.
- **How to use `edit_file`:**
1. Provide a clear, natural language `instructions` parameter describing the change (e.g., "I am adding error handling to the login function").
2. Provide the `code_edit` parameter showing the exact changes, using `// ... existing code ...` to represent unchanged parts of the file. This keeps your request concise and focused.
- **Examples:**
- **Update todo.md:** Write down only checked boxes, like simply "[x] Add some content and styling"
- **Improve a large file:** Your `code_edit` would show the changes efficiently while skipping unchanged parts.
- The `edit_file` tool is your ONLY tool for changing files. You MUST use `edit_file` for ALL modifications to existing files. It is more powerful and reliable than any other method. Using other tools for file modification is strictly forbidden.
# 4. DATA PROCESSING & EXTRACTION
@ -729,7 +725,11 @@ Before implementing any configuration changes, ALWAYS ask detailed questions to
You are Suna, and you can now evolve and adapt based on user needs. When someone asks you to gain new capabilities or connect to services, proactively use these tools to enhance yourself. You're not just following instructions - you're actively improving your own abilities to better serve your users.
Remember: You maintain all your core Suna capabilities while gaining the power to extend and customize yourself. This makes you more helpful and adaptable to each user's specific needs.
Remember: You maintain all your core Suna capabilities while gaining the power to extend and customize yourself. This makes you more helpful and adaptable to each user's specific needs. ALWAYS use the `edit_file` tool to make changes to files. The `edit_file` tool is smart enough to find and replace the specific parts you mention, so you should:
1. **Show only the exact lines that change**
2. **Use `// ... existing code ...` for context when needed**
3. **Never reproduce entire files or large unchanged sections**
"""

View File

@ -19,13 +19,12 @@ First, I'll create a structured plan in a todo.md file to organize my approach:
- [ ] Set up the HTML canvas element
- [ ] Create basic styling
## Game Implementation
- [ ] Implement game canvas and rendering context
- [ ] Create the paddles (player and opponent)
- [ ] Create the ball with movement physics
- [ ] Implement collision detection
- [ ] Add scoring system
- [ ] Implement basic AI for the opponent paddle
## Testing
- [ ] Test game mechanics
- [ ] Test collision detection
- [ ] Test scoring
- [ ] Test on different screen sizes
- [ ] Final review and adjustments
## Game Polish
- [ ] Add sound effects
@ -33,13 +32,6 @@ First, I'll create a structured plan in a todo.md file to organize my approach:
- [ ] Add visual effects (ball trails, paddle hits)
- [ ] Add game instructions
- [ ] Ensure responsive design
## Testing
- [ ] Test game mechanics
- [ ] Test collision detection
- [ ] Test scoring
- [ ] Test on different screen sizes
- [ ] Final review and adjustments
</parameter>
</invoke>
</function_calls>
@ -611,37 +603,21 @@ render();
Let me update our todo.md file to reflect our progress:
<function_calls>
<invoke name="full_file_rewrite">
<parameter name="file_path">todo.md</parameter>
<parameter name="file_contents">
# Pong Game Development Plan
## Project Setup
<invoke name="edit_file">
<parameter name="target_file">todo.md</parameter>
<parameter name="instructions">I am updating my todo list.</parameter>
<parameter name="code_edit">
// ... existing code ...
- [x] Create basic project structure (HTML, CSS, JS files)
- [x] Set up the HTML canvas element
- [x] Create basic styling
## Game Implementation
- [x] Implement game canvas and rendering context
- [x] Create the paddles (player and opponent)
- [x] Create the ball with movement physics
- [x] Implement collision detection
- [x] Add scoring system
- [x] Implement basic AI for the opponent paddle
## Game Polish
// ... existing code ...
- [x] Add sound effects
- [x] Implement start/restart game functionality
- [x] Add visual effects (ball trails, paddle hits)
- [x] Add game instructions
- [x] Ensure responsive design
## Testing
- [x] Test game mechanics
- [x] Test collision detection
- [x] Test scoring
- [x] Test on different screen sizes
- [x] Final review and adjustments
// ... existing code ...
</parameter>
</invoke>
</function_calls>
@ -686,7 +662,6 @@ Let's update our todo list to mark the testing as complete:
<parameter name="instructions">I am marking all tasks in the 'Testing' section of my todo list as complete.</parameter>
<parameter name="code_edit">
// ... existing code ...
## Testing
- [x] Test game mechanics
- [x] Test collision detection
- [x] Test scoring

View File

@ -999,9 +999,11 @@ Now let's check that our files are in place and update our todo list:
</function_calls>
<function_calls>
<invoke name="full_file_rewrite">
<parameter name="file_path">todo.md</parameter>
<parameter name="file_contents"># Japan Trip Planning Todo
<invoke name="edit_file">
<parameter name="target_file">todo.md</parameter>
<parameter name="instructions">I am marking all remaining tasks as complete in my todo list.</parameter>
<parameter name="code_edit">
# Japan Trip Planning Todo
## Research Phase
- [x] Research flight options from Seattle to Japan

View File

@ -40,7 +40,7 @@ class WorkflowTool(AgentBuilderBaseTool):
tool_mapping = {
'sb_shell_tool': ['execute_command'],
'sb_files_tool': ['create_file', 'str_replace', 'full_file_rewrite', 'delete_file', 'edit_file'],
'sb_files_tool': ['create_file', 'edit_file', 'str_replace', 'full_file_rewrite', 'delete_file'],
'sb_browser_tool': ['browser_navigate_to', 'browser_take_screenshot'],
'sb_vision_tool': ['see_image'],
'sb_deploy_tool': ['deploy'],

View File

@ -1,12 +1,13 @@
from agentpress.tool import ToolResult, openapi_schema, xml_schema
from sandbox.tool_base import SandboxToolsBase
from sandbox.tool_base import SandboxToolsBase
from utils.files_utils import should_exclude_file, clean_path
from agentpress.thread_manager import ThreadManager
from utils.logger import logger
from utils.config import config
import os
import json
import httpx
import litellm
import openai
import asyncio
from typing import Optional
@ -167,7 +168,7 @@ class SandboxFilesTool(SandboxToolsBase):
"type": "function",
"function": {
"name": "str_replace",
"description": "Replace specific text in a file. The file path must be relative to /workspace (e.g., 'src/main.py' for /workspace/src/main.py). Use this when you need to replace a unique string that appears exactly once in the file.",
"description": "Replace specific text in a file. The file path must be relative to /workspace (e.g., 'src/main.py' for /workspace/src/main.py). IMPORTANT: Prefer using edit_file for faster, shorter edits to avoid repetition. Only use this tool when you need to replace a unique string that appears exactly once in the file and edit_file is not suitable.",
"parameters": {
"type": "object",
"properties": {
@ -251,7 +252,7 @@ class SandboxFilesTool(SandboxToolsBase):
"type": "function",
"function": {
"name": "full_file_rewrite",
"description": "Completely rewrite an existing file with new content. The file path must be relative to /workspace (e.g., 'src/main.py' for /workspace/src/main.py). Use this when you need to replace the entire file content or make extensive changes throughout the file.",
"description": "Completely rewrite an existing file with new content. The file path must be relative to /workspace (e.g., 'src/main.py' for /workspace/src/main.py). IMPORTANT: Always prefer using edit_file for making changes to code. Only use this tool when edit_file fails or when you need to replace the entire file content.",
"parameters": {
"type": "object",
"properties": {
@ -367,78 +368,74 @@ class SandboxFilesTool(SandboxToolsBase):
except Exception as e:
return self.fail_response(f"Error deleting file: {str(e)}")
async def _call_morph_api(self, file_content: str, code_edit: str, instructions: str, file_path: str) -> Optional[str]:
"""Call Morph API to apply edits to file content"""
async def _call_morph_api(self, file_content: str, code_edit: str, instructions: str, file_path: str) -> tuple[Optional[str], Optional[str]]:
"""
Call Morph API to apply edits to file content.
Returns a tuple (new_content, error_message).
On success, error_message is None.
On failure, new_content is None.
"""
try:
morph_api_key = getattr(config, 'MORPH_API_KEY', None) or os.getenv('MORPH_API_KEY')
openrouter_key = getattr(config, 'OPENROUTER_API_KEY', None) or os.getenv('OPENROUTER_API_KEY')
api_key = None
base_url = None
messages = [{
"role": "user",
"content": f"<instruction>{instructions}</instruction>\n<code>{file_content}</code>\n<update>{code_edit}</update>"
}]
response = None
if morph_api_key:
api_key = morph_api_key
base_url = "https://api.morphllm.com/v1"
logger.debug("Using Morph API for file editing.")
logger.debug("Using direct Morph API for file editing.")
client = openai.AsyncOpenAI(
api_key=morph_api_key,
base_url="https://api.morphllm.com/v1"
)
response = await client.chat.completions.create(
model="morph-v3-large",
messages=messages,
temperature=0.0,
timeout=30.0
)
elif openrouter_key:
api_key = openrouter_key
base_url = "https://openrouter.ai/api/v1"
logger.debug("Morph API key not set, falling back to OpenRouter for file editing.")
logger.debug("Morph API key not set, falling back to OpenRouter for file editing via litellm.")
response = await litellm.acompletion(
model="openrouter/morph/morph-v3-large",
messages=messages,
api_key=openrouter_key,
api_base="https://openrouter.ai/api/v1",
temperature=0.0,
timeout=30.0
)
else:
error_msg = "No Morph or OpenRouter API key found, cannot perform AI edit."
logger.warning(error_msg)
return None, error_msg
if not api_key:
logger.warning("No Morph or OpenRouter API key found, falling back to traditional editing")
return None
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
"HTTP-Referer": "https://suna.ai",
"X-Title": "Suna AI Agent"
}
# Prepare the request for Morph's fast apply using the exact format from their docs
payload = {
"model": "morph/morph-code-edit",
"messages": [
{
"role": "user",
"content": f"<instructions>\n{instructions}\n</instructions>\n\n<code>\n{file_content}\n</code>\n\n<update>\n{code_edit}\n</update>"
}
],
"max_tokens": 16384,
"temperature": 0.0
}
async with httpx.AsyncClient(timeout=30.0) as client:
response = await client.post(f"{base_url}/chat/completions", json=payload, headers=headers)
response.raise_for_status()
if response and response.choices and len(response.choices) > 0:
content = response.choices[0].message.content.strip()
# Extract code block if wrapped in markdown
if content.startswith("```") and content.endswith("```"):
lines = content.split('\n')
if len(lines) > 2:
content = '\n'.join(lines[1:-1])
result = response.json()
return content, None
else:
error_msg = f"Invalid response from Morph/OpenRouter API: {response}"
logger.error(error_msg)
return None, error_msg
if result.get("choices") and len(result["choices"]) > 0:
content = result["choices"][0]["message"]["content"].strip()
# Extract code block if wrapped in markdown
if content.startswith("```") and content.endswith("```"):
lines = content.split('\n')
if len(lines) > 2:
# Remove first line (```language) and last line (```)
content = '\n'.join(lines[1:-1])
return content
else:
logger.error("Invalid response from Morph API")
return None
except httpx.TimeoutException:
logger.error("Morph API request timed out")
return None
except httpx.HTTPStatusError as e:
logger.error(f"Morph API returned error: {e.response.status_code}")
return None
except Exception as e:
logger.error(f"Error calling Morph API: {str(e)}")
return None
error_message = f"AI model call for file edit failed. Exception: {str(e)}"
# Try to get more details from the exception if it's an API error
if hasattr(e, 'response') and hasattr(e.response, 'text'):
error_message += f"\n\nAPI Response Body:\n{e.response.text}"
elif hasattr(e, 'body'): # litellm sometimes puts it in body
error_message += f"\n\nAPI Response Body:\n{e.body}"
logger.error(f"Error calling Morph/OpenRouter API: {error_message}", exc_info=True)
return None, error_message
@openapi_schema({
"type": "function",
@ -473,11 +470,33 @@ class SandboxFilesTool(SandboxToolsBase):
{"param_name": "code_edit", "node_type": "element", "path": "code_edit"}
],
example='''
<!-- Example: Mark multiple scattered tasks as complete in a todo list -->
<function_calls>
<invoke name="edit_file">
<parameter name="target_file">todo.md</parameter>
<parameter name="instructions">I am marking the research and setup tasks as complete in my todo list.</parameter>
<parameter name="code_edit">
// ... existing code ...
- [x] Research topic A
- [ ] Research topic B
- [x] Research topic C
// ... existing code ...
- [x] Setup database
- [x] Configure server
// ... existing code ...
</parameter>
</invoke>
</function_calls>
<!-- Example: Add error handling and logging to a function -->
<function_calls>
<invoke name="edit_file">
<parameter name="target_file">src/main.py</parameter>
<parameter name="instructions">I am adding error handling to the user authentication function</parameter>
<parameter name="instructions">I am adding error handling and logging to the user authentication function</parameter>
<parameter name="code_edit">
// ... existing imports ...
from my_app.logging import logger
from my_app.exceptions import DatabaseError
// ... existing code ...
def authenticate_user(username, password):
try:
@ -513,40 +532,60 @@ def authenticate_user(username, password):
# Try Morph AI editing first
logger.info(f"Attempting AI-powered edit for file '{target_file}' with instructions: {instructions[:100]}...")
new_content = await self._call_morph_api(original_content, code_edit, instructions, target_file)
new_content, error_message = await self._call_morph_api(original_content, code_edit, instructions, target_file)
if error_message:
return ToolResult(success=False, output=json.dumps({
"message": f"AI editing failed: {error_message}",
"file_path": target_file,
"original_content": original_content,
"updated_content": None
}))
if new_content is None:
return ToolResult(success=False, output=json.dumps({
"message": "AI editing failed for an unknown reason. The model returned no content.",
"file_path": target_file,
"original_content": original_content,
"updated_content": None
}))
if new_content == original_content:
return ToolResult(success=True, output=json.dumps({
"message": f"AI editing resulted in no changes to the file '{target_file}'.",
"file_path": target_file,
"original_content": original_content,
"updated_content": original_content
}))
# AI editing successful
await self.sandbox.fs.upload_file(new_content.encode(), full_path)
if new_content and new_content != original_content:
# AI editing successful
await self.sandbox.fs.upload_file(new_content.encode(), full_path)
# Show snippet of changes
original_lines = original_content.split('\n')
new_lines = new_content.split('\n')
# Find first differing line for context
diff_line = 0
for i, (old_line, new_line) in enumerate(zip(original_lines, new_lines)):
if old_line != new_line:
diff_line = i
break
# Show context around the change
start_line = max(0, diff_line - self.SNIPPET_LINES)
end_line = min(len(new_lines), diff_line + self.SNIPPET_LINES + 1)
snippet = '\n'.join(new_lines[start_line:end_line])
message = f"File '{target_file}' edited successfully using AI-powered editing."
if snippet:
message += f"\n\nPreview of changes (around line {diff_line + 1}):\n{snippet}"
return self.success_response(message)
else:
# No changes could be made
return self.fail_response(f"AI editing was unable to apply the requested changes. The edit may be unclear or the file content may not match the expected format.")
# Return rich data for frontend diff view
return ToolResult(success=True, output=json.dumps({
"message": f"File '{target_file}' edited successfully.",
"file_path": target_file,
"original_content": original_content,
"updated_content": new_content
}))
except Exception as e:
return self.fail_response(f"Error editing file: {str(e)}")
logger.error(f"Unhandled error in edit_file: {str(e)}", exc_info=True)
# Try to get original_content if possible
original_content_on_error = None
try:
full_path_on_error = f"{self.workspace_path}/{self.clean_path(target_file)}"
if await self._file_exists(full_path_on_error):
original_content_on_error = (await self.sandbox.fs.download_file(full_path_on_error)).decode()
except:
pass
return ToolResult(success=False, output=json.dumps({
"message": f"Error editing file: {str(e)}",
"file_path": target_file,
"original_content": original_content_on_error,
"updated_content": None
}))
# @openapi_schema({
# "type": "function",
@ -648,5 +687,4 @@ def authenticate_user(username, password):
# except UnicodeDecodeError:
# return self.fail_response(f"File '{file_path}' appears to be binary and cannot be read as text")
# except Exception as e:
# return self.fail_response(f"Error reading file: {str(e)}")
# return self.fail_response(f"Error reading file: {str(e)}")

View File

@ -57,7 +57,22 @@ class ContextManager:
return msg_content
elif isinstance(msg_content, dict):
if len(json.dumps(msg_content)) > max_length:
return json.dumps(msg_content)[:max_length] + "... (truncated)" + f"\n\nmessage_id \"{message_id}\"\nUse expand-message tool to see contents"
# Special handling for edit_file tool result to preserve JSON structure
tool_execution = msg_content.get("tool_execution", {})
if tool_execution.get("function_name") == "edit_file":
output = tool_execution.get("result", {}).get("output", {})
if isinstance(output, dict):
# Truncate file contents within the JSON
for key in ["original_content", "updated_content"]:
if isinstance(output.get(key), str) and len(output[key]) > max_length // 4:
output[key] = output[key][:max_length // 4] + "\n... (truncated)"
# After potential truncation, check size again
if len(json.dumps(msg_content)) > max_length:
# If still too large, fall back to string truncation
return json.dumps(msg_content)[:max_length] + "... (truncated)" + f"\n\nmessage_id \"{message_id}\"\nUse expand-message tool to see contents"
else:
return msg_content
else:
return msg_content

View File

@ -1637,23 +1637,42 @@ class ResponseProcessor:
# Determine message role based on strategy
result_role = "user" if strategy == "user_message" else "assistant"
# Create the new structured tool result format
structured_result = self._create_structured_tool_result(tool_call, result, parsing_details)
# Create two versions of the structured result
# 1. Rich version for the frontend
structured_result_for_frontend = self._create_structured_tool_result(tool_call, result, parsing_details, for_llm=False)
# 2. Concise version for the LLM
structured_result_for_llm = self._create_structured_tool_result(tool_call, result, parsing_details, for_llm=True)
# Add the message with the appropriate role to the conversation history
# This allows the LLM to see the tool result in subsequent interactions
result_message = {
result_message_for_llm = {
"role": result_role,
"content": json.dumps(structured_result)
"content": json.dumps(structured_result_for_llm)
}
message_obj = await self.add_message(
# Add rich content to metadata for frontend use
if metadata is None:
metadata = {}
metadata['frontend_content'] = structured_result_for_frontend
message_obj = await self._add_message_with_agent_info(
thread_id=thread_id,
type="tool",
content=result_message,
content=result_message_for_llm, # Save the LLM-friendly version
is_llm_message=True,
metadata=metadata
)
return message_obj # Return the full message object
# If the message was saved, modify it in-memory for the frontend before returning
if message_obj:
# The frontend expects the rich content in the 'content' field.
# The DB has the rich content in metadata.frontend_content.
# Let's reconstruct the message for yielding.
message_for_yield = message_obj.copy()
message_for_yield['content'] = structured_result_for_frontend
return message_for_yield
return message_obj # Return the modified message object
except Exception as e:
logger.error(f"Error adding tool result: {str(e)}", exc_info=True)
self.trace.event(name="error_adding_tool_result", level="ERROR", status_message=(f"Error adding tool result: {str(e)}"), metadata={"tool_call": tool_call, "result": result, "strategy": strategy, "assistant_message_id": assistant_message_id, "parsing_details": parsing_details})
@ -1676,13 +1695,14 @@ class ResponseProcessor:
self.trace.event(name="failed_even_with_fallback_message", level="ERROR", status_message=(f"Failed even with fallback message: {str(e2)}"), metadata={"tool_call": tool_call, "result": result, "strategy": strategy, "assistant_message_id": assistant_message_id, "parsing_details": parsing_details})
return None # Return None on error
def _create_structured_tool_result(self, tool_call: Dict[str, Any], result: ToolResult, parsing_details: Optional[Dict[str, Any]] = None):
def _create_structured_tool_result(self, tool_call: Dict[str, Any], result: ToolResult, parsing_details: Optional[Dict[str, Any]] = None, for_llm: bool = False):
"""Create a structured tool result format that's tool-agnostic and provides rich information.
Args:
tool_call: The original tool call that was executed
result: The result from the tool execution
parsing_details: Optional parsing details for XML calls
for_llm: If True, creates a concise version for the LLM context.
Returns:
Structured dictionary containing tool execution information
@ -1692,7 +1712,6 @@ class ResponseProcessor:
xml_tag_name = tool_call.get("xml_tag_name")
arguments = tool_call.get("arguments", {})
tool_call_id = tool_call.get("id")
logger.info(f"Creating structured tool result for tool_call: {tool_call}")
# Process the output - if it's a JSON string, parse it back to an object
output = result.output if hasattr(result, 'output') else str(result)
@ -1707,7 +1726,15 @@ class ResponseProcessor:
except Exception:
# If parsing fails, keep the original string
pass
output_to_use = output
# If this is for the LLM and it's an edit_file tool, create a concise output
if for_llm and function_name == 'edit_file' and isinstance(output, dict):
# The frontend needs original_content and updated_content to render diffs.
# The concise version for the LLM was causing issues.
# We will now pass the full output, and rely on the ContextManager to truncate if needed.
output_to_use = output
# Create the structured result
structured_result_v1 = {
"tool_execution": {
@ -1717,53 +1744,11 @@ class ResponseProcessor:
"arguments": arguments,
"result": {
"success": result.success if hasattr(result, 'success') else True,
"output": output, # Now properly structured for frontend
"output": output_to_use, # This will be either rich or concise based on `for_llm`
"error": getattr(result, 'error', None) if hasattr(result, 'error') else None
},
# "execution_details": {
# "timestamp": datetime.now(timezone.utc).isoformat(),
# "parsing_details": parsing_details
# }
}
}
# STRUCTURED_OUTPUT_TOOLS = {
# "str_replace",
# "get_data_provider_endpoints",
# }
# summary_output = result.output if hasattr(result, 'output') else str(result)
# if xml_tag_name:
# status = "completed successfully" if structured_result_v1["tool_execution"]["result"]["success"] else "failed"
# summary = f"Tool '{xml_tag_name}' {status}. Output: {summary_output}"
# else:
# status = "completed successfully" if structured_result_v1["tool_execution"]["result"]["success"] else "failed"
# summary = f"Function '{function_name}' {status}. Output: {summary_output}"
# if self.is_agent_builder:
# return summary
# if function_name in STRUCTURED_OUTPUT_TOOLS:
# return structured_result_v1
# else:
# return summary
summary_output = result.output if hasattr(result, 'output') else str(result)
success_status = structured_result_v1["tool_execution"]["result"]["success"]
# # Create a more comprehensive summary for the LLM
# if xml_tag_name:
# status = "completed successfully" if structured_result_v1["tool_execution"]["result"]["success"] else "failed"
# summary = f"Tool '{xml_tag_name}' {status}. Output: {summary_output}"
# else:
# status = "completed successfully" if structured_result_v1["tool_execution"]["result"]["success"] else "failed"
# summary = f"Function '{function_name}' {status}. Output: {summary_output}"
# if self.is_agent_builder:
# return summary
# elif function_name == "get_data_provider_endpoints":
# logger.info(f"Returning sumnary for data provider call: {summary}")
# return summary
return structured_result_v1

View File

@ -9,6 +9,7 @@
"version": "0.1.0",
"dependencies": {
"@calcom/embed-react": "^1.5.2",
"@codemirror/view": "^6.38.1",
"@hookform/resolvers": "^5.0.1",
"@next/third-parties": "^15.3.1",
"@number-flow/react": "^0.5.7",
@ -85,6 +86,7 @@
"postcss": "8.4.33",
"react": "^18",
"react-day-picker": "^8.10.1",
"react-diff-viewer-continued": "^3.4.0",
"react-dom": "^18",
"react-hook-form": "^7.55.0",
"react-icons": "^5.5.0",
@ -852,10 +854,9 @@
}
},
"node_modules/@codemirror/view": {
"version": "6.37.1",
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.37.1.tgz",
"integrity": "sha512-Qy4CAUwngy/VQkEz0XzMKVRcckQuqLYWKqVpDDDghBe5FSXSqfVrJn49nw3ePZHxRUz4nRmb05Lgi+9csWo4eg==",
"license": "MIT",
"version": "6.38.1",
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.1.tgz",
"integrity": "sha512-RmTOkE7hRU3OVREqFVITWHz6ocgBjv08GoePscAakgVQfciA3SGCEk7mb9IzwW61cKKmlTpHXG6DUE5Ubx+MGQ==",
"dependencies": {
"@codemirror/state": "^6.5.0",
"crelt": "^1.0.6",
@ -905,6 +906,95 @@
"tslib": "^2.4.0"
}
},
"node_modules/@emotion/babel-plugin": {
"version": "11.13.5",
"resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz",
"integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==",
"dependencies": {
"@babel/helper-module-imports": "^7.16.7",
"@babel/runtime": "^7.18.3",
"@emotion/hash": "^0.9.2",
"@emotion/memoize": "^0.9.0",
"@emotion/serialize": "^1.3.3",
"babel-plugin-macros": "^3.1.0",
"convert-source-map": "^1.5.0",
"escape-string-regexp": "^4.0.0",
"find-root": "^1.1.0",
"source-map": "^0.5.7",
"stylis": "4.2.0"
}
},
"node_modules/@emotion/babel-plugin/node_modules/convert-source-map": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
"integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="
},
"node_modules/@emotion/cache": {
"version": "11.14.0",
"resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz",
"integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==",
"dependencies": {
"@emotion/memoize": "^0.9.0",
"@emotion/sheet": "^1.4.0",
"@emotion/utils": "^1.4.2",
"@emotion/weak-memoize": "^0.4.0",
"stylis": "4.2.0"
}
},
"node_modules/@emotion/css": {
"version": "11.13.5",
"resolved": "https://registry.npmjs.org/@emotion/css/-/css-11.13.5.tgz",
"integrity": "sha512-wQdD0Xhkn3Qy2VNcIzbLP9MR8TafI0MJb7BEAXKp+w4+XqErksWR4OXomuDzPsN4InLdGhVe6EYcn2ZIUCpB8w==",
"dependencies": {
"@emotion/babel-plugin": "^11.13.5",
"@emotion/cache": "^11.13.5",
"@emotion/serialize": "^1.3.3",
"@emotion/sheet": "^1.4.0",
"@emotion/utils": "^1.4.2"
}
},
"node_modules/@emotion/hash": {
"version": "0.9.2",
"resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz",
"integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g=="
},
"node_modules/@emotion/memoize": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz",
"integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ=="
},
"node_modules/@emotion/serialize": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz",
"integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==",
"dependencies": {
"@emotion/hash": "^0.9.2",
"@emotion/memoize": "^0.9.0",
"@emotion/unitless": "^0.10.0",
"@emotion/utils": "^1.4.2",
"csstype": "^3.0.2"
}
},
"node_modules/@emotion/sheet": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz",
"integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg=="
},
"node_modules/@emotion/unitless": {
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz",
"integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg=="
},
"node_modules/@emotion/utils": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz",
"integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA=="
},
"node_modules/@emotion/weak-memoize": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz",
"integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg=="
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz",
@ -1433,19 +1523,30 @@
}
},
"node_modules/@eslint/plugin-kit": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.1.tgz",
"integrity": "sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w==",
"version": "0.3.4",
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.4.tgz",
"integrity": "sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@eslint/core": "^0.14.0",
"@eslint/core": "^0.15.1",
"levn": "^0.4.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/@eslint/plugin-kit/node_modules/@eslint/core": {
"version": "0.15.1",
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz",
"integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==",
"dev": true,
"dependencies": {
"@types/json-schema": "^7.0.15"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/@floating-ui/core": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.0.tgz",
@ -5312,6 +5413,11 @@
"@types/node": "*"
}
},
"node_modules/@types/parse-json": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz",
"integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw=="
},
"node_modules/@types/phoenix": {
"version": "1.6.6",
"resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.6.tgz",
@ -5566,11 +5672,10 @@
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
}
@ -6570,6 +6675,20 @@
"node": ">= 0.4"
}
},
"node_modules/babel-plugin-macros": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz",
"integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==",
"dependencies": {
"@babel/runtime": "^7.12.5",
"cosmiconfig": "^7.0.0",
"resolve": "^1.19.0"
},
"engines": {
"node": ">=10",
"npm": ">=6"
}
},
"node_modules/bail": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz",
@ -6657,11 +6776,10 @@
}
},
"node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@ -6831,7 +6949,6 @@
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
@ -7427,6 +7544,21 @@
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
"license": "MIT"
},
"node_modules/cosmiconfig": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz",
"integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==",
"dependencies": {
"@types/parse-json": "^4.0.0",
"import-fresh": "^3.2.1",
"parse-json": "^5.0.0",
"path-type": "^4.0.0",
"yaml": "^1.10.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/country-flag-icons": {
"version": "1.5.19",
"resolved": "https://registry.npmjs.org/country-flag-icons/-/country-flag-icons-1.5.19.tgz",
@ -7907,6 +8039,19 @@
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/error-ex": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
"integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
"dependencies": {
"is-arrayish": "^0.2.1"
}
},
"node_modules/error-ex/node_modules/is-arrayish": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
"integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="
},
"node_modules/es-abstract": {
"version": "1.24.0",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz",
@ -8143,7 +8288,6 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
@ -8774,6 +8918,11 @@
"node": ">=8"
}
},
"node_modules/find-root": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz",
"integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng=="
},
"node_modules/find-up": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
@ -8943,7 +9092,6 @@
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
@ -9252,7 +9400,6 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
@ -9577,7 +9724,6 @@
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
"integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"parent-module": "^1.0.0",
@ -9782,7 +9928,6 @@
"version": "2.16.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
"integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
"dev": true,
"license": "MIT",
"dependencies": {
"hasown": "^2.0.2"
@ -10226,6 +10371,11 @@
"dev": true,
"license": "MIT"
},
"node_modules/json-parse-even-better-errors": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="
},
"node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
@ -10633,6 +10783,11 @@
"node": ">= 0.4"
}
},
"node_modules/lines-and-columns": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="
},
"node_modules/locate-path": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
@ -11055,6 +11210,11 @@
"integrity": "sha512-aa5tG6sDoK+k70B9iEX1NeyfT8ObCKhNDs6lJVpwF6r8vhUfuKMslIcirq6HIUYuuUYLefcEQOn9bSBOvawtwg==",
"license": "MIT"
},
"node_modules/memoize-one": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz",
"integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw=="
},
"node_modules/merge-refs": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/merge-refs/-/merge-refs-1.3.0.tgz",
@ -12249,7 +12409,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
"integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
"dev": true,
"license": "MIT",
"dependencies": {
"callsites": "^3.0.0"
@ -12283,6 +12442,23 @@
"integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==",
"license": "MIT"
},
"node_modules/parse-json": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
"integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
"dependencies": {
"@babel/code-frame": "^7.0.0",
"error-ex": "^1.3.1",
"json-parse-even-better-errors": "^2.3.0",
"lines-and-columns": "^1.1.6"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/parse-svg-path": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/parse-svg-path/-/parse-svg-path-0.1.2.tgz",
@ -12325,9 +12501,16 @@
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"dev": true,
"license": "MIT"
},
"node_modules/path-type": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
"integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
"engines": {
"node": ">=8"
}
},
"node_modules/path2d": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/path2d/-/path2d-0.2.2.tgz",
@ -12681,6 +12864,33 @@
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/react-diff-viewer-continued": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/react-diff-viewer-continued/-/react-diff-viewer-continued-3.4.0.tgz",
"integrity": "sha512-kMZmUyb3Pv5L9vUtCfIGYsdOHs8mUojblGy1U1Sm0D7FhAOEsH9QhnngEIRo5hXWIPNGupNRJls1TJ6Eqx84eg==",
"dependencies": {
"@emotion/css": "^11.11.2",
"classnames": "^2.3.2",
"diff": "^5.1.0",
"memoize-one": "^6.0.0",
"prop-types": "^15.8.1"
},
"engines": {
"node": ">= 8"
},
"peerDependencies": {
"react": "^15.3.0 || ^16.0.0 || ^17.0.0 || ^18.0.0",
"react-dom": "^15.3.0 || ^16.0.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/react-diff-viewer-continued/node_modules/diff": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz",
"integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==",
"engines": {
"node": ">=0.3.1"
}
},
"node_modules/react-dom": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
@ -13194,7 +13404,6 @@
"version": "1.22.8",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
"integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-core-module": "^2.13.0",
@ -13212,7 +13421,6 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
"integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
@ -13735,6 +13943,14 @@
"react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc"
}
},
"node_modules/source-map": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
"integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@ -13999,6 +14215,11 @@
}
}
},
"node_modules/stylis": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz",
"integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw=="
},
"node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@ -14016,7 +14237,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@ -15030,6 +15250,14 @@
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
"license": "ISC"
},
"node_modules/yaml": {
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
"integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
"engines": {
"node": ">= 6"
}
},
"node_modules/yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",

View File

@ -89,6 +89,7 @@
"postcss": "8.4.33",
"react": "^18",
"react-day-picker": "^8.10.1",
"react-diff-viewer-continued": "^3.4.0",
"react-dom": "^18",
"react-hook-form": "^7.55.0",
"react-icons": "^5.5.0",

View File

@ -395,18 +395,31 @@ export default function ThreadPage({
const unifiedMessages = (messagesData || [])
.filter((msg) => msg.type !== 'status')
.map((msg: ApiMessageType) => ({
message_id: msg.message_id || null,
thread_id: msg.thread_id || threadId,
type: (msg.type || 'system') as UnifiedMessage['type'],
is_llm_message: Boolean(msg.is_llm_message),
content: msg.content || '',
metadata: msg.metadata || '{}',
created_at: msg.created_at || new Date().toISOString(),
updated_at: msg.updated_at || new Date().toISOString(),
agent_id: (msg as any).agent_id,
agents: (msg as any).agents,
}));
.map((msg: ApiMessageType) => {
let finalContent: string | object = msg.content || '';
if (msg.metadata) {
try {
const metadata = JSON.parse(msg.metadata);
if (metadata.frontend_content) {
finalContent = metadata.frontend_content;
}
} catch (e) {
// ignore
}
}
return {
message_id: msg.message_id || null,
thread_id: msg.thread_id || threadId,
type: (msg.type || 'system') as UnifiedMessage['type'],
is_llm_message: Boolean(msg.is_llm_message),
content: typeof finalContent === 'string' ? finalContent : JSON.stringify(finalContent),
metadata: msg.metadata || '{}',
created_at: msg.created_at || new Date().toISOString(),
updated_at: msg.updated_at || new Date().toISOString(),
agent_id: (msg as any).agent_id,
agents: (msg as any).agents,
};
});
setMessages(unifiedMessages);
const historicalToolPairs: ToolCallInput[] = [];

View File

@ -11,6 +11,7 @@ const HIDE_STREAMING_XML_TAGS = new Set([
'create-file',
'delete-file',
'full-file-rewrite',
'edit-file',
'str-replace',
'browser-click-element',
'browser-close-tab',

View File

@ -2,13 +2,14 @@ import React, { useEffect, useRef, useState } from 'react';
import { CircleDashed } from 'lucide-react';
import { extractToolNameFromStream } from '@/components/thread/tool-views/xml-parser';
import { getToolIcon, getUserFriendlyToolName, extractPrimaryParam } from '@/components/thread/utils';
import { CodeBlockCode } from '@/components/ui/code-block';
import { getLanguageFromFileName } from '../tool-views/file-operation/_utils';
// Only show streaming for file operation tools
const FILE_OPERATION_TOOLS = new Set([
'Create File',
'Delete File',
'Full File Rewrite',
'Read File',
'Creating File',
'Rewriting File',
'AI File Edit',
]);
interface ShowToolStreamProps {
@ -37,30 +38,40 @@ export const ShowToolStream: React.FC<ShowToolStreamProps> = ({
stableStartTimeRef.current = Date.now();
}
const toolName = extractToolNameFromStream(content);
const rawToolName = extractToolNameFromStream(content);
const toolName = getUserFriendlyToolName(rawToolName || '');
const isEditFile = toolName === 'AI File Edit';
const isCreateFile = toolName === 'Creating File';
const isFullFileRewrite = toolName === 'Rewriting File';
// Time-based logic - show streaming content after 1500ms
useEffect(() => {
const effectiveStartTime = stableStartTimeRef.current;
const streamingFileContent = React.useMemo(() => {
if (!content) return '';
let paramName: string | null = null;
if (isEditFile) paramName = 'code_edit';
else if (isCreateFile || isFullFileRewrite) paramName = 'file_contents';
// Only show expanded content for file operation tools
if (!effectiveStartTime || !showExpanded || !FILE_OPERATION_TOOLS.has(toolName || '')) {
setShouldShowContent(false);
return;
if (paramName) {
const newMatch = content.match(new RegExp(`<parameter\\s+name=["']${paramName}["']>([\\s\\S]*)`, 'i'));
if (newMatch && newMatch[1]) {
return newMatch[1].replace(/<\/parameter>[\s\S]*$/, '');
}
// Fallback for old formats
if (isEditFile) {
const oldMatch = content.match(/<code_edit>([\s\S]*)/i);
if (oldMatch && oldMatch[1]) {
return oldMatch[1].replace(/<\/code_edit>[\s\S]*$/, '');
}
}
}
return content; // fallback to full content
}, [content, isEditFile, isCreateFile, isFullFileRewrite]);
const elapsed = Date.now() - effectiveStartTime;
if (elapsed >= 2000) {
// Show streaming content immediately for file operations
useEffect(() => {
if (showExpanded && FILE_OPERATION_TOOLS.has(toolName || '')) {
setShouldShowContent(true);
} else {
const delay = 2000 - elapsed;
const timer = setTimeout(() => {
setShouldShowContent(true);
}, delay);
return () => {
clearTimeout(timer);
};
setShouldShowContent(false);
}
}, [showExpanded, toolName]);
@ -92,12 +103,12 @@ export const ShowToolStream: React.FC<ShowToolStreamProps> = ({
// Check if this is a file operation tool
const isFileOperationTool = FILE_OPERATION_TOOLS.has(toolName);
const IconComponent = getToolIcon(toolName);
const displayName = getUserFriendlyToolName(toolName);
const paramDisplay = extractPrimaryParam(toolName, content);
const IconComponent = getToolIcon(rawToolName || '');
const displayName = toolName;
const paramDisplay = extractPrimaryParam(rawToolName || '', content);
// Always show tool button, conditionally show content below for file operations only
if (showExpanded && isFileOperationTool) {
if (showExpanded && (isFileOperationTool || isEditFile)) {
return (
<div className="my-1">
{shouldShowContent ? (
@ -126,7 +137,7 @@ export const ShowToolStream: React.FC<ShowToolStreamProps> = ({
WebkitMaskImage: 'linear-gradient(to bottom, transparent 0%, black 8%, black 92%, transparent 100%)'
}}
>
{content}
{isEditFile || isCreateFile || isFullFileRewrite ? streamingFileContent : content}
</div>
{/* Top gradient */}
<div className={`absolute top-0 left-0 right-0 h-8 pointer-events-none transition-all duration-500 ease-in-out ${shouldShowContent

View File

@ -0,0 +1,238 @@
import React, { useState } from 'react';
import {
FileDiff,
CheckCircle,
AlertTriangle,
Loader2,
File,
ChevronDown,
ChevronUp,
Minus,
Plus,
} from 'lucide-react';
import { cn } from '@/lib/utils';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { ScrollArea } from "@/components/ui/scroll-area";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { Button } from '@/components/ui/button';
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import {
extractFileEditData,
generateLineDiff,
calculateDiffStats,
LineDiff,
DiffStats
} from './_utils';
import { formatTimestamp, getToolTitle } from '../utils';
import { ToolViewProps } from '../types';
import { LoadingState } from '../shared/LoadingState';
import ReactDiffViewer from 'react-diff-viewer-continued';
const UnifiedDiffView: React.FC<{ oldCode: string; newCode: string }> = ({ oldCode, newCode }) => (
<ReactDiffViewer
oldValue={oldCode}
newValue={newCode}
splitView={false}
hideLineNumbers={true}
useDarkTheme={document.documentElement.classList.contains('dark')}
styles={{
variables: {
dark: {
diffViewerColor: '#e2e8f0',
diffViewerBackground: '#09090b',
addedBackground: '#104a32',
addedColor: '#6ee7b7',
removedBackground: '#5c1a2e',
removedColor: '#fca5a5',
},
},
diffContainer: {
backgroundColor: 'var(--card)',
border: 'none',
},
diffRemoved: {
display: 'none',
},
line: {
fontFamily: 'monospace',
},
}}
/>
);
const SplitDiffView: React.FC<{ oldCode: string; newCode: string }> = ({ oldCode, newCode }) => (
<ReactDiffViewer
oldValue={oldCode}
newValue={newCode}
splitView={true}
useDarkTheme={document.documentElement.classList.contains('dark')}
styles={{
variables: {
dark: {
diffViewerColor: '#e2e8f0',
diffViewerBackground: '#09090b',
addedBackground: '#104a32',
addedColor: '#6ee7b7',
removedBackground: '#5c1a2e',
removedColor: '#fca5a5',
},
},
diffContainer: {
backgroundColor: 'var(--card)',
border: 'none',
},
gutter: {
backgroundColor: 'var(--muted)',
'&:hover': {
backgroundColor: 'var(--accent)',
},
},
line: {
fontFamily: 'monospace',
},
}}
/>
);
const ErrorState: React.FC<{ message?: string }> = ({ message }) => (
<div className="flex flex-col items-center justify-center h-full py-12 px-6 bg-gradient-to-b from-white to-zinc-50 dark:from-zinc-950 dark:to-zinc-900">
<div className="text-center w-full max-w-xs">
<AlertTriangle className="h-16 w-16 mx-auto mb-6 text-amber-500" />
<h3 className="text-lg font-medium text-zinc-900 dark:text-zinc-100 mb-2">
Invalid File Edit
</h3>
<p className="text-sm text-zinc-500 dark:text-zinc-400">
{message || "Could not extract the file changes from the tool result."}
</p>
</div>
</div>
);
export function FileEditToolView({
name = 'edit-file',
assistantContent,
toolContent,
assistantTimestamp,
toolTimestamp,
isSuccess = true,
isStreaming = false,
}: ToolViewProps): JSX.Element {
const [viewMode, setViewMode] = useState<'unified' | 'split'>('unified');
const {
filePath,
originalContent,
updatedContent,
actualIsSuccess,
actualToolTimestamp,
errorMessage,
} = extractFileEditData(
assistantContent,
toolContent,
isSuccess,
toolTimestamp,
assistantTimestamp
);
const toolTitle = getToolTitle(name);
const lineDiff = originalContent && updatedContent ? generateLineDiff(originalContent, updatedContent) : [];
const stats: DiffStats = calculateDiffStats(lineDiff);
const shouldShowError = !isStreaming && (!actualIsSuccess || (actualIsSuccess && (originalContent === null || updatedContent === null)));
return (
<Card className="gap-0 flex border shadow-none border-t border-b-0 border-x-0 p-0 rounded-none flex-col h-full overflow-hidden bg-card">
<CardHeader className="h-14 bg-zinc-50/80 dark:bg-zinc-900/80 backdrop-blur-sm border-b p-2 px-4 space-y-2">
<div className="flex flex-row items-center justify-between">
<div className="flex items-center gap-2">
<div className="relative p-2 rounded-lg bg-gradient-to-br from-blue-500/20 to-blue-600/10 border border-blue-500/20">
<FileDiff className="w-5 h-5 text-blue-500 dark:text-blue-400" />
</div>
<CardTitle className="text-base font-medium text-zinc-900 dark:text-zinc-100">
{toolTitle}
</CardTitle>
</div>
{!isStreaming && (
<Badge
variant="secondary"
className={
actualIsSuccess
? "bg-gradient-to-b from-emerald-200 to-emerald-100 text-emerald-700 dark:from-emerald-800/50 dark:to-emerald-900/60 dark:text-emerald-300"
: "bg-gradient-to-b from-rose-200 to-rose-100 text-rose-700 dark:from-rose-800/50 dark:to-rose-900/60 dark:text-rose-300"
}
>
{actualIsSuccess ? (
<CheckCircle className="h-3.5 w-3.5 mr-1" />
) : (
<AlertTriangle className="h-3.5 w-3.5 mr-1" />
)}
{actualIsSuccess ? 'Edit applied' : 'Edit failed'}
</Badge>
)}
</div>
</CardHeader>
<CardContent className="p-0 h-full flex-1 overflow-hidden relative">
{isStreaming ? (
<LoadingState
icon={FileDiff}
iconColor="text-blue-500 dark:text-blue-400"
bgColor="bg-gradient-to-b from-blue-100 to-blue-50 shadow-inner dark:from-blue-800/40 dark:to-blue-900/60 dark:shadow-blue-950/20"
title="Applying File Edit"
filePath={filePath || 'Processing file...'}
progressText="Analyzing changes"
subtitle="Please wait while the file is being modified"
/>
) : shouldShowError ? (
<ErrorState message={errorMessage} />
) : (
<div className="h-full flex flex-col">
<div className="p-3 border-b border-zinc-200 dark:border-zinc-800 bg-accent flex items-center justify-between">
<div className="flex items-center">
<File className="h-4 w-4 mr-2 text-zinc-500 dark:text-zinc-400" />
<code className="text-sm font-mono text-zinc-700 dark:text-zinc-300">
{filePath || 'Unknown file'}
</code>
</div>
<div className="flex items-center gap-2">
<div className="flex items-center text-xs text-zinc-500 dark:text-zinc-400 gap-3">
{stats.additions === 0 && stats.deletions === 0 ? (
<Badge variant="outline" className="text-xs font-normal">No changes</Badge>
) : (
<>
<div className="flex items-center">
<Plus className="h-3.5 w-3.5 text-emerald-500 mr-1" />
<span>{stats.additions}</span>
</div>
<div className="flex items-center">
<Minus className="h-3.5 w-3.5 text-red-500 mr-1" />
<span>{stats.deletions}</span>
</div>
</>
)}
</div>
<Tabs value={viewMode} onValueChange={(v) => setViewMode(v as 'unified' | 'split')} className="w-auto">
<TabsList className="h-7 p-0.5">
<TabsTrigger value="unified" className="text-xs h-6 px-2">Unified</TabsTrigger>
<TabsTrigger value="split" className="text-xs h-6 px-2">Split</TabsTrigger>
</TabsList>
</Tabs>
</div>
</div>
<ScrollArea className="flex-1">
{viewMode === 'unified' ? (
<UnifiedDiffView oldCode={originalContent!} newCode={updatedContent!} />
) : (
<SplitDiffView oldCode={originalContent!} newCode={updatedContent!} />
)}
</ScrollArea>
</div>
)}
</CardContent>
</Card>
);
}

View File

@ -1,6 +1,121 @@
import { LucideIcon, FilePen, Replace, Trash2, FileCode, FileSpreadsheet, File } from 'lucide-react';
export type FileOperation = 'create' | 'rewrite' | 'delete' | 'edit';
export type DiffType = 'unchanged' | 'added' | 'removed';
export interface LineDiff {
type: DiffType;
oldLine: string | null;
newLine: string | null;
lineNumber: number;
}
export interface CharDiffPart {
text: string;
type: DiffType;
}
export interface DiffStats {
additions: number;
deletions: number;
}
export const parseNewlines = (text: string): string => {
return text.replace(/\\n/g, '\n');
};
export const generateLineDiff = (oldText: string, newText: string): LineDiff[] => {
const parsedOldText = parseNewlines(oldText);
const parsedNewText = parseNewlines(newText);
const oldLines = parsedOldText.split('\n');
const newLines = parsedNewText.split('\n');
const diffLines: LineDiff[] = [];
const maxLines = Math.max(oldLines.length, newLines.length);
for (let i = 0; i < maxLines; i++) {
const oldLine = i < oldLines.length ? oldLines[i] : null;
const newLine = i < newLines.length ? newLines[i] : null;
if (oldLine === newLine) {
diffLines.push({ type: 'unchanged', oldLine, newLine, lineNumber: i + 1 });
} else {
if (oldLine !== null) {
diffLines.push({ type: 'removed', oldLine, newLine: null, lineNumber: i + 1 });
}
if (newLine !== null) {
diffLines.push({ type: 'added', oldLine: null, newLine, lineNumber: i + 1 });
}
}
}
return diffLines;
};
export const generateCharDiff = (oldText: string, newText: string): CharDiffPart[] => {
const parsedOldText = parseNewlines(oldText);
const parsedNewText = parseNewlines(newText);
let prefixLength = 0;
while (
prefixLength < parsedOldText.length &&
prefixLength < parsedNewText.length &&
parsedOldText[prefixLength] === parsedNewText[prefixLength]
) {
prefixLength++;
}
let oldSuffixStart = parsedOldText.length;
let newSuffixStart = parsedNewText.length;
while (
oldSuffixStart > prefixLength &&
newSuffixStart > prefixLength &&
parsedOldText[oldSuffixStart - 1] === parsedNewText[newSuffixStart - 1]
) {
oldSuffixStart--;
newSuffixStart--;
}
const parts: CharDiffPart[] = [];
if (prefixLength > 0) {
parts.push({
text: parsedOldText.substring(0, prefixLength),
type: 'unchanged',
});
}
if (oldSuffixStart > prefixLength) {
parts.push({
text: parsedOldText.substring(prefixLength, oldSuffixStart),
type: 'removed',
});
}
if (newSuffixStart > prefixLength) {
parts.push({
text: parsedNewText.substring(prefixLength, newSuffixStart),
type: 'added',
});
}
if (oldSuffixStart < parsedOldText.length) {
parts.push({
text: parsedOldText.substring(oldSuffixStart),
type: 'unchanged',
});
}
return parts;
};
export const calculateDiffStats = (lineDiff: LineDiff[]): DiffStats => {
return {
additions: lineDiff.filter(line => line.type === 'added').length,
deletions: lineDiff.filter(line => line.type === 'removed').length
};
};
export type FileOperation = 'create' | 'rewrite' | 'delete' | 'edit' | 'str-replace';
export interface OperationConfig {
icon: LucideIcon;
@ -77,12 +192,141 @@ export const getLanguageFromFileName = (fileName: string): string => {
return extensionMap[extension] || 'text';
};
export interface ExtractedEditData {
filePath: string | null;
originalContent: string | null;
updatedContent: string | null;
success?: boolean;
timestamp?: string;
errorMessage?: string;
}
const parseContent = (content: any): any => {
if (typeof content === 'string') {
try {
return JSON.parse(content);
} catch (e) {
return content;
}
}
return content;
};
const parseOutput = (output: any) => {
if (typeof output === 'string') {
try {
return JSON.parse(output);
} catch {
return output; // Return as string if not JSON
}
}
return output;
};
export const extractFileEditData = (
assistantContent: any,
toolContent: any,
isSuccess: boolean,
toolTimestamp?: string,
assistantTimestamp?: string
): {
filePath: string | null;
originalContent: string | null;
updatedContent: string | null;
actualIsSuccess: boolean;
actualToolTimestamp?: string;
actualAssistantTimestamp?: string;
errorMessage?: string;
} => {
const parseOutput = (output: any) => {
if (typeof output === 'string') {
try {
return JSON.parse(output);
} catch {
return output; // Return as string if not JSON
}
}
return output;
};
const extractData = (content: any) => {
let parsed = typeof content === 'string' ? parseContent(content) : content;
// Handle nested content structures like { role: '...', content: '...' }
if (parsed?.role && parsed?.content) {
parsed = typeof parsed.content === 'string' ? parseContent(parsed.content) : parsed.content;
}
if (parsed?.tool_execution) {
const args = parsed.tool_execution.arguments || {};
const output = parseOutput(parsed.tool_execution.result?.output);
const success = parsed.tool_execution.result?.success;
let errorMessage: string | undefined;
if (success === false) {
if (typeof output === 'object' && output !== null && output.message) {
errorMessage = output.message;
} else if (typeof output === 'string') {
errorMessage = output;
} else {
errorMessage = JSON.stringify(output);
}
}
return {
filePath: args.target_file || (typeof output === 'object' && output?.file_path) || null,
originalContent: (typeof output === 'object' && output?.original_content) ?? null,
updatedContent: (typeof output === 'object' && output?.updated_content) ?? null,
success: success,
timestamp: parsed.tool_execution.execution_details?.timestamp,
errorMessage: errorMessage,
};
}
// Fallback for when toolContent is just the output object from the tool result
if (typeof parsed === 'object' && parsed !== null && (parsed.original_content !== undefined || parsed.updated_content !== undefined)) {
return {
filePath: parsed.file_path || null,
originalContent: parsed.original_content ?? null,
updatedContent: parsed.updated_content ?? null,
success: parsed.updated_content !== null, // Success is false if updated_content is null
timestamp: null,
errorMessage: parsed.message,
};
}
return {};
};
const toolData = extractData(toolContent);
const assistantData = extractData(assistantContent);
const filePath = toolData.filePath || assistantData.filePath;
const originalContent = toolData.originalContent || assistantData.originalContent;
const updatedContent = toolData.updatedContent || assistantData.updatedContent;
const errorMessage = toolData.errorMessage || assistantData.errorMessage;
let actualIsSuccess = isSuccess;
let actualToolTimestamp = toolTimestamp;
let actualAssistantTimestamp = assistantTimestamp;
if (toolData.success !== undefined) {
actualIsSuccess = toolData.success;
actualToolTimestamp = toolData.timestamp || toolTimestamp;
} else if (assistantData.success !== undefined) {
actualIsSuccess = assistantData.success;
actualAssistantTimestamp = assistantData.timestamp || assistantTimestamp;
}
return { filePath, originalContent, updatedContent, actualIsSuccess, actualToolTimestamp, actualAssistantTimestamp, errorMessage };
};
export const getOperationType = (name?: string, assistantContent?: any): FileOperation => {
if (name) {
if (name.includes('create')) return 'create';
if (name.includes('rewrite')) return 'rewrite';
if (name.includes('delete')) return 'delete';
if (name.includes('edit')) return 'edit';
if (name.includes('edit-file')) return 'edit'; // Specific for edit_file
if (name.includes('str-replace')) return 'str-replace';
}
if (!assistantContent) return 'create';
@ -155,6 +399,17 @@ export const getOperationConfigs = (): Record<FileOperation, OperationConfig> =>
badgeColor: 'bg-red-100 text-red-700 border-red-200',
hoverColor: 'hover:bg-red-100',
},
'str-replace': {
icon: Replace,
color: 'text-blue-600',
successMessage: 'String replaced successfully',
progressMessage: 'Replacing string...',
bgColor: 'bg-blue-50',
gradientBg: 'from-blue-50 to-blue-100',
borderColor: 'border-blue-200',
badgeColor: 'bg-blue-100 text-blue-700 border-blue-200',
hoverColor: 'hover:bg-blue-100',
},
};
};

View File

@ -1,21 +1,15 @@
export type DiffType = 'unchanged' | 'added' | 'removed';
import {
DiffType,
LineDiff,
CharDiffPart,
DiffStats,
generateLineDiff,
generateCharDiff,
calculateDiffStats,
} from '../file-operation/_utils';
export interface LineDiff {
type: DiffType;
oldLine: string | null;
newLine: string | null;
lineNumber: number;
}
export interface CharDiffPart {
text: string;
type: DiffType;
}
export interface DiffStats {
additions: number;
deletions: number;
}
export type { DiffType, LineDiff, CharDiffPart, DiffStats };
export { generateLineDiff, generateCharDiff, calculateDiffStats };
export interface ExtractedData {
filePath: string | null;
@ -121,102 +115,4 @@ export const extractFromLegacyFormat = (content: any, extractToolData: any, extr
oldStr: strReplaceContent.oldStr,
newStr: strReplaceContent.newStr
};
};
export const parseNewlines = (text: string): string => {
return text.replace(/\\n/g, '\n');
};
export const generateLineDiff = (oldText: string, newText: string): LineDiff[] => {
const parsedOldText = parseNewlines(oldText);
const parsedNewText = parseNewlines(newText);
const oldLines = parsedOldText.split('\n');
const newLines = parsedNewText.split('\n');
const diffLines: LineDiff[] = [];
const maxLines = Math.max(oldLines.length, newLines.length);
for (let i = 0; i < maxLines; i++) {
const oldLine = i < oldLines.length ? oldLines[i] : null;
const newLine = i < newLines.length ? newLines[i] : null;
if (oldLine === newLine) {
diffLines.push({ type: 'unchanged', oldLine, newLine, lineNumber: i + 1 });
} else {
if (oldLine !== null) {
diffLines.push({ type: 'removed', oldLine, newLine: null, lineNumber: i + 1 });
}
if (newLine !== null) {
diffLines.push({ type: 'added', oldLine: null, newLine, lineNumber: i + 1 });
}
}
}
return diffLines;
};
export const generateCharDiff = (oldText: string, newText: string): CharDiffPart[] => {
const parsedOldText = parseNewlines(oldText);
const parsedNewText = parseNewlines(newText);
let prefixLength = 0;
while (
prefixLength < parsedOldText.length &&
prefixLength < parsedNewText.length &&
parsedOldText[prefixLength] === parsedNewText[prefixLength]
) {
prefixLength++;
}
let oldSuffixStart = parsedOldText.length;
let newSuffixStart = parsedNewText.length;
while (
oldSuffixStart > prefixLength &&
newSuffixStart > prefixLength &&
parsedOldText[oldSuffixStart - 1] === parsedNewText[newSuffixStart - 1]
) {
oldSuffixStart--;
newSuffixStart--;
}
const parts: CharDiffPart[] = [];
if (prefixLength > 0) {
parts.push({
text: parsedOldText.substring(0, prefixLength),
type: 'unchanged',
});
}
if (oldSuffixStart > prefixLength) {
parts.push({
text: parsedOldText.substring(prefixLength, oldSuffixStart),
type: 'removed',
});
}
if (newSuffixStart > prefixLength) {
parts.push({
text: parsedNewText.substring(prefixLength, newSuffixStart),
type: 'added',
});
}
if (oldSuffixStart < parsedOldText.length) {
parts.push({
text: parsedOldText.substring(oldSuffixStart),
type: 'unchanged',
});
}
return parts;
};
export const calculateDiffStats = (lineDiff: LineDiff[]): DiffStats => {
return {
additions: lineDiff.filter(line => line.type === 'added').length,
deletions: lineDiff.filter(line => line.type === 'removed').length
};
};

View File

@ -5,6 +5,7 @@ import { BrowserToolView } from '../BrowserToolView';
import { CommandToolView } from '../command-tool/CommandToolView';
import { ExposePortToolView } from '../expose-port-tool/ExposePortToolView';
import { FileOperationToolView } from '../file-operation/FileOperationToolView';
import { FileEditToolView } from '../file-operation/FileEditToolView';
import { StrReplaceToolView } from '../str-replace/StrReplaceToolView';
import { WebCrawlToolView } from '../WebCrawlToolView';
import { WebScrapeToolView } from '../web-scrape-tool/WebScrapeToolView';
@ -56,7 +57,7 @@ const defaultRegistry: ToolViewRegistryType = {
'delete-file': FileOperationToolView,
'full-file-rewrite': FileOperationToolView,
'read-file': FileOperationToolView,
'edit-file': FileOperationToolView,
'edit-file': FileEditToolView,
'str-replace': StrReplaceToolView,
@ -138,4 +139,4 @@ export function useToolView(toolName: string): ToolViewComponent {
export function ToolView({ name = 'default', ...props }: ToolViewProps) {
const ToolViewComponent = useToolView(name);
return <ToolViewComponent name={name} {...props} />;
}
}

View File

@ -100,14 +100,12 @@ export function isNewXmlFormat(content: string): boolean {
export function extractToolNameFromStream(content: string): string | null {
const invokeMatch = content.match(/<invoke\s+name=["']([^"']+)["']/i);
if (invokeMatch) {
const toolName = invokeMatch[1].replace(/_/g, '-');
return formatToolNameForDisplay(toolName);
return invokeMatch[1].replace(/_/g, '-');
}
const oldFormatMatch = content.match(/<([a-zA-Z\-_]+)(?:\s+[^>]*)?>(?!\/)/);
if (oldFormatMatch) {
const toolName = oldFormatMatch[1].replace(/_/g, '-');
return formatToolNameForDisplay(toolName);
return oldFormatMatch[1].replace(/_/g, '-');
}
return null;

View File

@ -246,9 +246,9 @@ export const extractPrimaryParam = (
return match ? match[1].split('/').pop() || match[1] : null;
case 'edit-file':
// Try to match target_file attribute for edit-file
match = content.match(/target_file=(?:"|')([^"|']+)(?:"|')/);
match = content.match(/target_file=(?:"|')([^"|']+)(?:"|')/) || content.match(/<parameter\s+name=["']target_file["']>([^<]+)/i);
// Return just the filename part
return match ? match[1].split('/').pop() || match[1] : null;
return match ? (match[1].split('/').pop() || match[1]).trim() : null;
// Shell commands
case 'execute-command':
@ -304,6 +304,7 @@ const TOOL_DISPLAY_NAMES = new Map([
['str-replace', 'Editing Text'],
['str_replace', 'Editing Text'],
['edit_file', 'AI File Edit'],
['edit-file', 'AI File Edit'],
['browser-click-element', 'Clicking Element'],
['browser-close-tab', 'Closing Tab'],