This commit is contained in:
marko-kraemer 2025-04-11 16:56:26 +01:00
parent 177ace2788
commit 6a835d7e60
8 changed files with 739 additions and 94 deletions

View File

@ -70,12 +70,175 @@ Your approach is deliberately methodical and persistent:
7. CRITICALLY IMPORTANT: You MUST ALWAYS explicitly use one of these two tools when you've completed your task or need user input
# TECHNICAL PROTOCOLS
- WORKSPACE DIRECTORY: You are operating in the "/workspace" directory by default. All file paths you provide must be relative to this directory. For example:
* If you want to access "/workspace/src/main.py", you should use "src/main.py"
* If you want to access "/workspace/README.md", you should use "README.md"
* Never use absolute paths or paths starting with "/workspace" - always use relative paths
* All file operations (create, read, write, delete) expect paths relative to "/workspace"
- CLI TOOLS PREFERENCE:
* Always prefer CLI tools over Python scripts when possible
* CLI tools are generally faster and more efficient for:
1. File operations and content extraction
2. Text processing and pattern matching
3. System operations and file management
4. Data transformation and filtering
* Use Python only when:
1. Complex logic is required
2. CLI tools are insufficient
3. Custom processing is needed
4. Integration with other Python code is necessary
- CONTENT EXTRACTION TOOLS:
* PDF Processing:
1. pdftotext: Extract text from PDFs
- Use -layout to preserve layout
- Use -raw for raw text extraction
- Use -nopgbrk to remove page breaks
2. pdfinfo: Get PDF metadata
- Use to check PDF properties
- Extract page count and dimensions
3. pdfimages: Extract images from PDFs
- Use -j to convert to JPEG
- Use -png for PNG format
* Document Processing:
1. antiword: Extract text from Word docs
2. unrtf: Convert RTF to text
3. catdoc: Extract text from Word docs
4. xls2csv: Convert Excel to CSV
* Text Processing:
1. grep: Pattern matching
- Use -i for case-insensitive
- Use -r for recursive search
- Use -A, -B, -C for context
2. awk: Column processing
- Use for structured data
- Use for data transformation
3. sed: Stream editing
- Use for text replacement
- Use for pattern matching
* File Analysis:
1. file: Determine file type
2. wc: Count words/lines
3. head/tail: View file parts
4. less: View large files
* Data Processing:
1. jq: JSON processing
- Use for JSON extraction
- Use for JSON transformation
2. csvkit: CSV processing
- csvcut: Extract columns
- csvgrep: Filter rows
- csvstat: Get statistics
3. xmlstarlet: XML processing
- Use for XML extraction
- Use for XML transformation
- DATA PROCESSING VERIFICATION:
* STRICT REQUIREMENT: Only use data that has been explicitly verified through actual extraction or processing
* NEVER use assumed, hallucinated, or inferred data
* For any data processing task:
1. First extract the data using appropriate tools
2. Save the extracted data to a file
3. Verify the extracted data matches the source
4. Only use the verified extracted data for further processing
5. If verification fails, debug and re-extract
* Data Processing Rules:
1. Every piece of data must come from actual extraction
2. No assumptions about data content or structure
3. No hallucination of missing data
4. No inference of data patterns
5. No use of data that hasn't been explicitly verified
* Verification Process:
1. Extract data using CLI tools or scripts
2. Save raw extracted data to files
3. Compare extracted data with source
4. Only proceed with verified data
5. Document verification steps
* Error Handling:
1. If data cannot be verified, stop processing
2. Report verification failures
3. Request clarification if needed
4. Never proceed with unverified data
5. Always maintain data integrity
- CLI TOOLS AND REGEX CAPABILITIES:
* Use CLI tools for efficient data processing and verification:
1. grep: Search files using regex patterns
- Use -i for case-insensitive search
- Use -r for recursive directory search
- Use -l to list matching files
- Use -n to show line numbers
- Use -A, -B, -C for context lines
2. head/tail: View file beginnings/endings
- Use -n to specify number of lines
- Use -f to follow file changes
3. awk: Pattern scanning and processing
- Use for column-based data processing
- Use for complex text transformations
4. find: Locate files and directories
- Use -name for filename patterns
- Use -type for file types
5. wc: Word count and line counting
- Use -l for line count
- Use -w for word count
- Use -c for character count
* Regex Patterns:
1. Use for precise text matching
2. Combine with CLI tools for powerful searches
3. Save complex patterns to files for reuse
4. Test patterns with small samples first
5. Use extended regex (-E) for complex patterns
* Data Processing Workflow:
1. Use grep to locate relevant files
2. Use head/tail to preview content
3. Use awk for data extraction
4. Use wc to verify results
5. Chain commands with pipes for efficiency
* Verification Steps:
1. Create regex patterns for expected content
2. Use grep to verify presence/absence
3. Use wc to count matches
4. Use awk to extract specific data
5. Save verification results to files
- VERIFICATION AND DATA INTEGRITY:
* NEVER assume or hallucinate contents from PDFs, documents, or script outputs
* ALWAYS verify data by running scripts and tools to extract information
* For any data extraction task:
1. First create and save the extraction script
2. Execute the script and capture its output
3. Use the actual output from the script, not assumptions
4. Verify the output matches expected format and content
5. If verification fails, debug and rerun the script
* For PDFs and documents:
1. Create a script to extract the content
2. Run the script and store its output
3. Use only the extracted content from the script output
4. Never make assumptions about document contents
* Tool Results Analysis:
1. Carefully examine all tool execution results
2. Verify script outputs match expected results
3. Check for errors or unexpected behavior
4. Use actual output data, never assume or hallucinate
5. If results are unclear, create additional verification steps
* Data Processing:
1. Save intermediate results to files
2. Verify each processing step's output
3. Use actual processed data, not assumptions
4. Create verification scripts for complex transformations
5. Run verification steps and use their results
- COMMUNICATION: Use message tools for updates and essential questions. Include the 'attachments' parameter with file paths or URLs when sharing resources with users.
- TOOL RESULTS: Carefully analyze all tool execution results to inform your next actions. These results provide critical environmental information including file contents, execution outputs, and search results.
- FILES: Create organized file structures with clear naming conventions. Store different types of data in appropriate formats.
- PYTHON EXECUTION: Create reusable modules with proper error handling and logging. Focus on maintainability and readability.
- CLI OPERATIONS:
* Use terminal commands for system operations, file manipulations, and quick tasks
* Leverage sessions for maintaining state between related commands
* Use the default session for one-off commands
* Create named sessions for complex operations requiring multiple steps
* Always clean up sessions after use
* Avoid commands requiring confirmation; actively use -y or -f flags for automatic confirmation
* Avoid commands with excessive output; save to files when necessary
* Chain multiple commands with && operator to minimize interruptions

View File

@ -1,4 +1,5 @@
from daytona_sdk.process import SessionExecuteRequest
from typing import Optional
from agentpress.tool import ToolResult, openapi_schema, xml_schema
from sandbox.sandbox import SandboxToolsBase
@ -14,11 +15,20 @@ import os
class SandboxFilesTool(SandboxToolsBase):
"""Tool for executing file system operations in a Daytona sandbox."""
"""Tool for executing file system operations in a Daytona sandbox. All operations are performed relative to the /workspace directory."""
def __init__(self, sandbox_id: str, password: str):
super().__init__(sandbox_id, password)
self.SNIPPET_LINES = 4 # Number of context lines to show around edits
self.workspace_path = "/workspace" # Ensure we're always operating in /workspace
def clean_path(self, path: str) -> str:
"""Clean and normalize a path to be relative to /workspace"""
# Remove any leading /workspace/ or / prefix
path = path.lstrip('/')
if path.startswith('workspace/'):
path = path[9:]
return path
def _should_exclude_file(self, rel_path: str) -> bool:
"""Check if a file should be excluded based on path, name, or extension"""
@ -68,13 +78,13 @@ class SandboxFilesTool(SandboxToolsBase):
"type": "function",
"function": {
"name": "create_file",
"description": "Create a new file with the provided contents at a given path in the workspace",
"description": "Create a new file with the provided contents at a given path in the workspace. The path must be relative to /workspace (e.g., 'src/main.py' for /workspace/src/main.py)",
"parameters": {
"type": "object",
"properties": {
"file_path": {
"type": "string",
"description": "Path to the file to be created"
"description": "Path to the file to be created, relative to /workspace (e.g., 'src/main.py')"
},
"file_contents": {
"type": "string",
@ -97,7 +107,7 @@ class SandboxFilesTool(SandboxToolsBase):
{"param_name": "file_contents", "node_type": "content", "path": "."}
],
example='''
<create-file file_path="path/to/file">
<create-file file_path="src/main.py">
File contents go here
</create-file>
'''
@ -126,13 +136,13 @@ class SandboxFilesTool(SandboxToolsBase):
"type": "function",
"function": {
"name": "str_replace",
"description": "Replace specific text in a file. Use this when you need to replace a unique string that appears exactly once in the file. Best for targeted changes where you know the exact text to 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.",
"parameters": {
"type": "object",
"properties": {
"file_path": {
"type": "string",
"description": "Path to the target file"
"description": "Path to the target file, relative to /workspace (e.g., 'src/main.py')"
},
"old_str": {
"type": "string",
@ -155,7 +165,7 @@ class SandboxFilesTool(SandboxToolsBase):
{"param_name": "new_str", "node_type": "element", "path": "new_str"}
],
example='''
<str-replace file_path="path/to/file">
<str-replace file_path="src/main.py">
<old_str>text to replace (must appear exactly once in the file)</old_str>
<new_str>replacement text that will be inserted instead</new_str>
</str-replace>
@ -198,13 +208,13 @@ class SandboxFilesTool(SandboxToolsBase):
"type": "function",
"function": {
"name": "full_file_rewrite",
"description": "Completely rewrite an existing file with new content. Use this when you need to replace the entire file content or make extensive changes throughout the file. Safer than multiple str_replace operations for major rewrites.",
"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.",
"parameters": {
"type": "object",
"properties": {
"file_path": {
"type": "string",
"description": "Path to the file to be rewritten"
"description": "Path to the file to be rewritten, relative to /workspace (e.g., 'src/main.py')"
},
"file_contents": {
"type": "string",
@ -227,7 +237,7 @@ class SandboxFilesTool(SandboxToolsBase):
{"param_name": "file_contents", "node_type": "content", "path": "."}
],
example='''
<full-file-rewrite file_path="path/to/file">
<full-file-rewrite file_path="src/main.py">
This completely replaces the entire file content.
Use when making major changes to a file or when the changes
are too extensive for str-replace.
@ -253,13 +263,13 @@ class SandboxFilesTool(SandboxToolsBase):
"type": "function",
"function": {
"name": "delete_file",
"description": "Delete a file at the given path",
"description": "Delete a file at the given path. The path must be relative to /workspace (e.g., 'src/main.py' for /workspace/src/main.py)",
"parameters": {
"type": "object",
"properties": {
"file_path": {
"type": "string",
"description": "Path to the file to be deleted"
"description": "Path to the file to be deleted, relative to /workspace (e.g., 'src/main.py')"
}
},
"required": ["file_path"]
@ -272,7 +282,7 @@ class SandboxFilesTool(SandboxToolsBase):
{"param_name": "file_path", "node_type": "attribute", "path": "."}
],
example='''
<delete-file file_path="path/to/file">
<delete-file file_path="src/main.py">
</delete-file>
'''
)
@ -288,6 +298,101 @@ class SandboxFilesTool(SandboxToolsBase):
except Exception as e:
return self.fail_response(f"Error deleting file: {str(e)}")
@openapi_schema({
"type": "function",
"function": {
"name": "read_file",
"description": "Read and return the contents of a file. This tool is essential for verifying data, checking file contents, and analyzing information. Always use this tool to read file contents before processing or analyzing data. The file path must be relative to /workspace.",
"parameters": {
"type": "object",
"properties": {
"file_path": {
"type": "string",
"description": "Path to the file to read, relative to /workspace (e.g., 'src/main.py' for /workspace/src/main.py). Must be a valid file path within the workspace."
},
"start_line": {
"type": "integer",
"description": "Optional starting line number (1-based). Use this to read specific sections of large files. If not specified, reads from the beginning of the file.",
"default": 1
},
"end_line": {
"type": "integer",
"description": "Optional ending line number (inclusive). Use this to read specific sections of large files. If not specified, reads to the end of the file.",
"default": None
}
},
"required": ["file_path"]
}
}
})
@xml_schema(
tag_name="read-file",
mappings=[
{"param_name": "file_path", "node_type": "attribute", "path": "."},
{"param_name": "start_line", "node_type": "attribute", "path": ".", "required": False},
{"param_name": "end_line", "node_type": "attribute", "path": ".", "required": False}
],
example='''
<!-- Example 1: Read entire file -->
<read-file file_path="src/main.py">
</read-file>
<!-- Example 2: Read specific lines (lines 10-20) -->
<read-file file_path="src/main.py" start_line="10" end_line="20">
</read-file>
<!-- Example 3: Read from line 5 to end -->
<read-file file_path="config.json" start_line="5">
</read-file>
<!-- Example 4: Read last 10 lines -->
<read-file file_path="logs/app.log" start_line="-10">
</read-file>
'''
)
async def read_file(self, file_path: str, start_line: int = 1, end_line: Optional[int] = None) -> ToolResult:
"""Read file content with optional line range specification.
Args:
file_path: Path to the file relative to /workspace
start_line: Starting line number (1-based), defaults to 1
end_line: Ending line number (inclusive), defaults to None (end of file)
Returns:
ToolResult containing:
- Success: File content and metadata
- Failure: Error message if file doesn't exist or is binary
"""
try:
file_path = self.clean_path(file_path)
full_path = f"{self.workspace_path}/{file_path}"
if not self._file_exists(full_path):
return self.fail_response(f"File '{file_path}' does not exist")
# Download and decode file content
content = self.sandbox.fs.download_file(full_path).decode()
# Handle line range if specified
if start_line > 1 or end_line is not None:
lines = content.split('\n')
start_idx = max(0, start_line - 1) # Convert to 0-based index
end_idx = end_line if end_line is not None else len(lines)
content = '\n'.join(lines[start_idx:end_idx])
return self.success_response({
"content": content,
"file_path": file_path,
"start_line": start_line,
"end_line": end_line if end_line is not None else len(content.split('\n')),
"total_lines": len(content.split('\n'))
})
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)}")
async def test_files_tool():
files_tool = SandboxFilesTool(

View File

@ -1,3 +1,5 @@
from typing import Optional, Dict, List
from uuid import uuid4
from agentpress.tool import ToolResult, openapi_schema, xml_schema
from sandbox.sandbox import SandboxToolsBase
@ -11,23 +13,58 @@ from sandbox.sandbox import SandboxToolsBase
class SandboxShellTool(SandboxToolsBase):
"""Tool for executing tasks in a Daytona sandbox with browser-use capabilities."""
"""Tool for executing tasks in a Daytona sandbox with browser-use capabilities.
Uses sessions for maintaining state between commands and provides comprehensive process management."""
def __init__(self, sandbox_id: str, password: str):
super().__init__(sandbox_id, password)
self._sessions: Dict[str, str] = {} # Maps session names to session IDs
async def _ensure_session(self, session_name: str = "default") -> str:
"""Ensure a session exists and return its ID."""
if session_name not in self._sessions:
session_id = str(uuid4())
try:
self.sandbox.process.create_session(session_id)
self._sessions[session_name] = session_id
except Exception as e:
raise RuntimeError(f"Failed to create session: {str(e)}")
return self._sessions[session_name]
async def _cleanup_session(self, session_name: str):
"""Clean up a session if it exists."""
if session_name in self._sessions:
try:
self.sandbox.process.delete_session(self._sessions[session_name])
del self._sessions[session_name]
except Exception as e:
print(f"Warning: Failed to cleanup session {session_name}: {str(e)}")
@openapi_schema({
"type": "function",
"function": {
"name": "execute_command",
"description": "Execute a shell command in the workspace directory. Working directory is the workspace directory.",
"description": "Execute a shell command in the workspace directory. Uses sessions to maintain state between commands. This tool is essential for running CLI tools, installing packages, and managing system operations. Always verify command outputs before using the data.",
"parameters": {
"type": "object",
"properties": {
"command": {
"type": "string",
"description": "The shell command to execute"
"description": "The shell command to execute. Use this for running CLI tools, installing packages, or system operations. Example: 'pdftotext input.pdf -layout'"
},
"folder": {
"type": "string",
"description": "Optional relative path to a subdirectory of /workspace where the command should be executed. Example: 'data/pdfs'"
},
"session_name": {
"type": "string",
"description": "Optional name of the session to use. Use named sessions for related commands that need to maintain state. Defaults to 'default'.",
"default": "default"
},
"timeout": {
"type": "integer",
"description": "Optional timeout in seconds. Increase for long-running commands. Defaults to 60.",
"default": 60
}
},
"required": ["command"]
@ -38,31 +75,95 @@ class SandboxShellTool(SandboxToolsBase):
tag_name="execute-command",
mappings=[
{"param_name": "command", "node_type": "content", "path": "."},
{"param_name": "folder", "node_type": "attribute", "path": ".", "required": False},
{"param_name": "session_name", "node_type": "attribute", "path": ".", "required": False},
{"param_name": "timeout", "node_type": "attribute", "path": ".", "required": False}
],
example='''
<!-- Example 1: Basic command execution -->
<execute-command>
npm install package-name
ls -l
</execute-command>
<!-- Example 2: Command in specific directory -->
<execute-command folder="data/pdfs">
pdftotext document.pdf -layout
</execute-command>
<!-- Example 3: Using named session for related commands -->
<execute-command session_name="pdf_processing">
pdftotext input.pdf -layout > output.txt
</execute-command>
<!-- Example 4: Complex command with pipes -->
<execute-command>
grep -r "pattern" . | awk '{print $1}' | sort | uniq -c
</execute-command>
<!-- Example 5: Command with error handling -->
<execute-command>
pdftotext input.pdf -layout 2>&1 || echo "Error processing PDF"
</execute-command>
'''
)
async def execute_command(self, command: str, folder: str = None) -> ToolResult:
async def execute_command(
self,
command: str,
folder: Optional[str] = None,
session_name: str = "default",
timeout: int = 60
) -> ToolResult:
try:
folder = folder or self.workspace_path
response = self.sandbox.process.exec(command, cwd=folder, timeout=60)
# Ensure session exists
session_id = await self._ensure_session(session_name)
# Set up working directory
cwd = self.workspace_path
if folder:
folder = folder.strip('/')
cwd = f"{self.workspace_path}/{folder}"
# Execute command in session
from sandbox.sandbox import SessionExecuteRequest
req = SessionExecuteRequest(
command=command,
var_async=False
)
response = self.sandbox.process.execute_session_command(
session_id=session_id,
req=req,
timeout=timeout
)
# Get detailed logs
logs = self.sandbox.process.get_session_command_logs(
session_id=session_id,
command_id=response.cmd_id
)
if response.exit_code == 0:
return self.success_response({
"output": response.result,
"error": "",
"output": logs,
"exit_code": response.exit_code,
"cwd": folder
"cwd": cwd,
"session_id": session_id,
"command_id": response.cmd_id
})
else:
return self.fail_response(f"Command failed with exit code {response.exit_code}: {response.result}")
error_msg = f"Command failed with exit code {response.exit_code}"
if logs:
error_msg += f": {logs}"
return self.fail_response(error_msg)
except Exception as e:
return self.fail_response(f"Error executing command: {str(e)}")
async def cleanup(self):
"""Clean up all sessions."""
for session_name in list(self._sessions.keys()):
await self._cleanup_session(session_name)
async def test_shell_tool():

View File

@ -40,6 +40,26 @@ RUN apt-get update && apt-get install -y \
fonts-dejavu \
fonts-dejavu-core \
fonts-dejavu-extra \
# PDF Processing Tools
poppler-utils \
# Document Processing Tools
antiword \
unrtf \
catdoc \
xls2csv \
# Text Processing Tools
grep \
gawk \
sed \
# File Analysis Tools
file \
# Data Processing Tools
jq \
csvkit \
xmlstarlet \
# Additional Utilities
less \
vim \
&& rm -rf /var/lib/apt/lists/*
# Install noVNC

View File

@ -20,6 +20,7 @@
"@radix-ui/react-dialog": "^1.1.7",
"@radix-ui/react-dropdown-menu": "^2.1.6",
"@radix-ui/react-label": "^2.1.2",
"@radix-ui/react-scroll-area": "^1.2.4",
"@radix-ui/react-select": "^2.1.6",
"@radix-ui/react-separator": "^1.1.2",
"@radix-ui/react-slot": "^1.1.2",
@ -1861,6 +1862,188 @@
}
}
},
"node_modules/@radix-ui/react-scroll-area": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.4.tgz",
"integrity": "sha512-G9rdWTQjOR4sk76HwSdROhPU0jZWpfozn9skU1v4N0/g9k7TmswrJn8W8WMU+aYktnLLpk5LX6fofj2bGe5NFQ==",
"dependencies": {
"@radix-ui/number": "1.1.1",
"@radix-ui/primitive": "1.1.2",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-direction": "1.1.1",
"@radix-ui/react-presence": "1.1.3",
"@radix-ui/react-primitive": "2.0.3",
"@radix-ui/react-use-callback-ref": "1.1.1",
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/number": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz",
"integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==",
"license": "MIT"
},
"node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/primitive": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz",
"integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==",
"license": "MIT"
},
"node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-compose-refs": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
"integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-context": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
"integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-direction": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz",
"integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-presence": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.3.tgz",
"integrity": "sha512-IrVLIhskYhH3nLvtcBLQFZr61tBG7wx7O3kEmdzcYwRGAEBmBicGGL7ATzNgruYJ3xBTbuzEEq9OXJM3PAX3tA==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-primitive": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.3.tgz",
"integrity": "sha512-Pf/t/GkndH7CQ8wE2hbkXA+WyZ83fhQQn5DDmwDiDo6AwN/fhaH8oqZ0jRjMrO2iaMhDi6P1HRx6AZwyMinY1g==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-slot": "1.2.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-slot": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.0.tgz",
"integrity": "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-use-callback-ref": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
"integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-use-layout-effect": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
"integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-select": {
"version": "2.1.6",
"resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.1.6.tgz",

View File

@ -21,6 +21,7 @@
"@radix-ui/react-dialog": "^1.1.7",
"@radix-ui/react-dropdown-menu": "^2.1.6",
"@radix-ui/react-label": "^2.1.2",
"@radix-ui/react-scroll-area": "^1.2.4",
"@radix-ui/react-select": "^2.1.6",
"@radix-ui/react-separator": "^1.1.2",
"@radix-ui/react-slot": "^1.1.2",

View File

@ -46,7 +46,7 @@ export function ChatInput({
const [inputValue, setInputValue] = useState(value || "");
const textareaRef = useRef<HTMLTextAreaElement | null>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
const [uploadedFile, setUploadedFile] = useState<UploadedFile | null>(null);
const [uploadedFiles, setUploadedFiles] = useState<UploadedFile[]>([]);
const [isUploading, setIsUploading] = useState(false);
// Allow controlled or uncontrolled usage
@ -86,7 +86,7 @@ export function ChatInput({
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if ((!inputValue.trim() && !uploadedFile) || loading || (disabled && !isAgentRunning)) return;
if ((!inputValue.trim() && uploadedFiles.length === 0) || loading || (disabled && !isAgentRunning)) return;
if (isAgentRunning && onStopAgent) {
onStopAgent();
@ -95,9 +95,11 @@ export function ChatInput({
let message = inputValue;
// Add file information to the message if a file was uploaded
if (uploadedFile) {
const fileInfo = `[Uploaded file: ${uploadedFile.name} (${formatFileSize(uploadedFile.size)}) at ${uploadedFile.path}]`;
// Add file information to the message if files were uploaded
if (uploadedFiles.length > 0) {
const fileInfo = uploadedFiles.map(file =>
`[Uploaded file: ${file.name} (${formatFileSize(file.size)}) at ${file.path}]`
).join('\n');
message = message ? `${message}\n\n${fileInfo}` : fileInfo;
}
@ -107,8 +109,8 @@ export function ChatInput({
setInputValue("");
}
// Reset the uploaded file after sending
setUploadedFile(null);
// Reset the uploaded files after sending
setUploadedFiles([]);
};
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
@ -123,7 +125,7 @@ export function ChatInput({
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
if ((inputValue.trim() || uploadedFile) && !loading && (!disabled || isAgentRunning)) {
if ((inputValue.trim() || uploadedFiles.length > 0) && !loading && (!disabled || isAgentRunning)) {
handleSubmit(e as React.FormEvent);
}
}
@ -141,49 +143,56 @@ export function ChatInput({
try {
setIsUploading(true);
const file = event.target.files[0];
const files = Array.from(event.target.files);
const newUploadedFiles: UploadedFile[] = [];
if (file.size > 50 * 1024 * 1024) { // 50MB limit
toast.error("File size exceeds 50MB limit");
return;
for (const file of files) {
if (file.size > 50 * 1024 * 1024) { // 50MB limit
toast.error(`File size exceeds 50MB limit: ${file.name}`);
continue;
}
// Create a FormData object
const formData = new FormData();
formData.append('file', file);
// Upload to workspace root by default
const uploadPath = `/workspace/${file.name}`;
formData.append('path', uploadPath);
const supabase = createClient();
const { data: { session } } = await supabase.auth.getSession();
if (!session?.access_token) {
throw new Error('No access token available');
}
// Upload using FormData
const response = await fetch(`${API_URL}/sandboxes/${sandboxId}/files`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${session.access_token}`,
},
body: formData
});
if (!response.ok) {
throw new Error(`Upload failed: ${response.statusText}`);
}
// Add to uploaded files
newUploadedFiles.push({
name: file.name,
path: uploadPath,
size: file.size
});
toast.success(`File uploaded: ${file.name}`);
}
// Create a FormData object
const formData = new FormData();
formData.append('file', file);
// Update the uploaded files state
setUploadedFiles(prev => [...prev, ...newUploadedFiles]);
// Upload to workspace root by default
const uploadPath = `/workspace/${file.name}`;
formData.append('path', uploadPath);
const supabase = createClient();
const { data: { session } } = await supabase.auth.getSession();
if (!session?.access_token) {
throw new Error('No access token available');
}
// Upload using FormData
const response = await fetch(`${API_URL}/sandboxes/${sandboxId}/files`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${session.access_token}`,
},
body: formData
});
if (!response.ok) {
throw new Error(`Upload failed: ${response.statusText}`);
}
// Set the uploaded file info
setUploadedFile({
name: file.name,
path: uploadPath,
size: file.size
});
toast.success(`File uploaded: ${file.name}`);
} catch (error) {
console.error("File upload failed:", error);
toast.error(typeof error === 'string' ? error : (error instanceof Error ? error.message : "Failed to upload file"));
@ -200,31 +209,35 @@ export function ChatInput({
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
};
const removeUploadedFile = () => {
setUploadedFile(null);
const removeUploadedFile = (index: number) => {
setUploadedFiles(prev => prev.filter((_, i) => i !== index));
};
return (
<div className="w-full">
<form onSubmit={handleSubmit} className="relative">
{uploadedFile && (
<div className="mb-2 p-2 bg-secondary/20 rounded-md flex items-center justify-between">
<div className="flex items-center text-sm">
<File className="h-4 w-4 mr-2 text-primary" />
<span className="font-medium">{uploadedFile.name}</span>
<span className="text-xs text-muted-foreground ml-2">
({formatFileSize(uploadedFile.size)})
</span>
</div>
<Button
type="button"
variant="ghost"
size="icon"
className="h-6 w-6"
onClick={removeUploadedFile}
>
<X className="h-3 w-3" />
</Button>
{uploadedFiles.length > 0 && (
<div className="mb-2 space-y-1">
{uploadedFiles.map((file, index) => (
<div key={index} className="p-2 bg-secondary/20 rounded-md flex items-center justify-between">
<div className="flex items-center text-sm">
<File className="h-4 w-4 mr-2 text-primary" />
<span className="font-medium">{file.name}</span>
<span className="text-xs text-muted-foreground ml-2">
({formatFileSize(file.size)})
</span>
</div>
<Button
type="button"
variant="ghost"
size="icon"
className="h-6 w-6"
onClick={() => removeUploadedFile(index)}
>
<X className="h-3 w-3" />
</Button>
</div>
))}
</div>
)}
@ -252,7 +265,7 @@ export function ChatInput({
size="icon"
className="h-8 w-8 rounded-full"
disabled={loading || (disabled && !isAgentRunning) || isUploading}
aria-label="Upload file"
aria-label="Upload files"
>
{isUploading ? (
<Loader2 className="h-4 w-4 animate-spin" />
@ -267,6 +280,7 @@ export function ChatInput({
ref={fileInputRef}
className="hidden"
onChange={processFileUpload}
multiple
/>
{/* File browser button */}
@ -290,7 +304,7 @@ export function ChatInput({
variant="ghost"
size="icon"
className="h-8 w-8 rounded-full"
disabled={((!inputValue.trim() && !uploadedFile) && !isAgentRunning) || loading || (disabled && !isAgentRunning)}
disabled={((!inputValue.trim() && uploadedFiles.length === 0) && !isAgentRunning) || loading || (disabled && !isAgentRunning)}
aria-label={isAgentRunning ? 'Stop agent' : 'Send message'}
>
{loading ? (

View File

@ -0,0 +1,58 @@
"use client"
import * as React from "react"
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
import { cn } from "@/lib/utils"
function ScrollArea({
className,
children,
...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
return (
<ScrollAreaPrimitive.Root
data-slot="scroll-area"
className={cn("relative", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport
data-slot="scroll-area-viewport"
className="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1"
>
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
)
}
function ScrollBar({
className,
orientation = "vertical",
...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
return (
<ScrollAreaPrimitive.ScrollAreaScrollbar
data-slot="scroll-area-scrollbar"
orientation={orientation}
className={cn(
"flex touch-none p-px transition-colors select-none",
orientation === "vertical" &&
"h-full w-2.5 border-l border-l-transparent",
orientation === "horizontal" &&
"h-2.5 flex-col border-t border-t-transparent",
className
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb
data-slot="scroll-area-thumb"
className="bg-border relative flex-1 rounded-full"
/>
</ScrollAreaPrimitive.ScrollAreaScrollbar>
)
}
export { ScrollArea, ScrollBar }