mirror of https://github.com/kortix-ai/suna.git
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:
commit
e7ad67caef
|
@ -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>
|
||||
|
||||
|
|
|
@ -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**
|
||||
|
||||
"""
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'],
|
||||
|
|
|
@ -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)}")
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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[] = [];
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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',
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -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
|
||||
};
|
||||
};
|
|
@ -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} />;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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'],
|
||||
|
|
Loading…
Reference in New Issue