Compare commits

..

3 Commits

Author SHA1 Message Date
Otto
42b7b6ee37 fix: Use strokeOpacity and stroke color for SVG hit detection
The invisible path needs an actual stroke color (just made invisible via
strokeOpacity=0) for SVG to register pointer events on the stroke area.
Also added react-flow__edge-interaction class for consistency with the
legacy builder pattern.
2026-02-12 09:43:16 +00:00
Krzysztof Czerwinski
a7f9bf3cb8 Merge branch 'dev' into fix/edge-hover-x-button-SECRT-1943 2026-02-12 18:07:43 +09:00
Otto
764070f6a7 fix(builder): Show X button on edge line hover, not just button hover
Add invisible interaction path along edge that triggers hover state,
making the remove button appear when hovering anywhere on the connection
line rather than requiring users to find the small button directly.

Fixes SECRT-1943
2026-02-12 08:25:56 +00:00
4 changed files with 39 additions and 137 deletions

View File

@@ -1,6 +1,4 @@
import base64
import json import json
import logging
import shlex import shlex
import uuid import uuid
from typing import Literal, Optional from typing import Literal, Optional
@@ -23,11 +21,6 @@ from backend.data.model import (
) )
from backend.integrations.providers import ProviderName from backend.integrations.providers import ProviderName
logger = logging.getLogger(__name__)
# Maximum size for binary files to extract (50MB)
MAX_BINARY_FILE_SIZE = 50 * 1024 * 1024
class ClaudeCodeExecutionError(Exception): class ClaudeCodeExecutionError(Exception):
"""Exception raised when Claude Code execution fails. """Exception raised when Claude Code execution fails.
@@ -187,9 +180,7 @@ class ClaudeCodeBlock(Block):
path: str path: str
relative_path: str # Path relative to working directory (for GitHub, etc.) relative_path: str # Path relative to working directory (for GitHub, etc.)
name: str name: str
content: str # Text content for text files, empty string for binary files content: str
is_binary: bool = False # True if this is a binary file
content_base64: Optional[str] = None # Base64-encoded content for binary files
class Output(BlockSchemaOutput): class Output(BlockSchemaOutput):
response: str = SchemaField( response: str = SchemaField(
@@ -197,11 +188,8 @@ class ClaudeCodeBlock(Block):
) )
files: list["ClaudeCodeBlock.FileOutput"] = SchemaField( files: list["ClaudeCodeBlock.FileOutput"] = SchemaField(
description=( description=(
"List of files created/modified by Claude Code during this execution. " "List of text files created/modified by Claude Code during this execution. "
"Each file has 'path', 'relative_path', 'name', 'content', 'is_binary', " "Each file has 'path', 'relative_path', 'name', and 'content' fields."
"and 'content_base64' fields. For text files, 'content' contains the text "
"and 'is_binary' is False. For binary files (PDFs, images, etc.), "
"'is_binary' is True and 'content_base64' contains the base64-encoded data."
) )
) )
conversation_history: str = SchemaField( conversation_history: str = SchemaField(
@@ -264,8 +252,6 @@ class ClaudeCodeBlock(Block):
"relative_path": "index.html", "relative_path": "index.html",
"name": "index.html", "name": "index.html",
"content": "<html>Hello World</html>", "content": "<html>Hello World</html>",
"is_binary": False,
"content_base64": None,
} }
], ],
), ),
@@ -286,8 +272,6 @@ class ClaudeCodeBlock(Block):
relative_path="index.html", relative_path="index.html",
name="index.html", name="index.html",
content="<html>Hello World</html>", content="<html>Hello World</html>",
is_binary=False,
content_base64=None,
) )
], # files ], # files
"User: Create a hello world HTML file\n" "User: Create a hello world HTML file\n"
@@ -547,6 +531,7 @@ class ClaudeCodeBlock(Block):
".env", ".env",
".gitignore", ".gitignore",
".dockerfile", ".dockerfile",
"Dockerfile",
".vue", ".vue",
".svelte", ".svelte",
".astro", ".astro",
@@ -555,44 +540,6 @@ class ClaudeCodeBlock(Block):
".tex", ".tex",
".csv", ".csv",
".log", ".log",
".svg", # SVG is XML-based text
}
# Binary file extensions we can read and base64-encode
binary_extensions = {
# Images
".png",
".jpg",
".jpeg",
".gif",
".webp",
".ico",
".bmp",
".tiff",
".tif",
# Documents
".pdf",
# Archives (useful for downloads)
".zip",
".tar",
".gz",
".7z",
# Audio/Video (if small enough)
".mp3",
".wav",
".mp4",
".webm",
# Other binary formats
".woff",
".woff2",
".ttf",
".otf",
".eot",
".bin",
".exe",
".dll",
".so",
".dylib",
} }
try: try:
@@ -617,26 +564,10 @@ class ClaudeCodeBlock(Block):
if not file_path: if not file_path:
continue continue
# Check if it's a text file we can read (case-insensitive) # Check if it's a text file we can read
file_path_lower = file_path.lower()
is_text = any( is_text = any(
file_path_lower.endswith(ext) for ext in text_extensions file_path.endswith(ext) for ext in text_extensions
) or file_path_lower.endswith("dockerfile") ) or file_path.endswith("Dockerfile")
# Check if it's a binary file we should extract
is_binary = any(
file_path_lower.endswith(ext) for ext in binary_extensions
)
# Helper to extract filename and relative path
def get_file_info(path: str, work_dir: str) -> tuple[str, str]:
name = path.split("/")[-1]
rel_path = path
if path.startswith(work_dir):
rel_path = path[len(work_dir) :]
if rel_path.startswith("/"):
rel_path = rel_path[1:]
return name, rel_path
if is_text: if is_text:
try: try:
@@ -645,72 +576,32 @@ class ClaudeCodeBlock(Block):
if isinstance(content, bytes): if isinstance(content, bytes):
content = content.decode("utf-8", errors="replace") content = content.decode("utf-8", errors="replace")
file_name, relative_path = get_file_info( # Extract filename from path
file_path, working_directory file_name = file_path.split("/")[-1]
)
# Calculate relative path by stripping working directory
relative_path = file_path
if file_path.startswith(working_directory):
relative_path = file_path[len(working_directory) :]
# Remove leading slash if present
if relative_path.startswith("/"):
relative_path = relative_path[1:]
files.append( files.append(
ClaudeCodeBlock.FileOutput( ClaudeCodeBlock.FileOutput(
path=file_path, path=file_path,
relative_path=relative_path, relative_path=relative_path,
name=file_name, name=file_name,
content=content, content=content,
is_binary=False,
content_base64=None,
) )
) )
except Exception as e: except Exception:
logger.warning(f"Failed to read text file {file_path}: {e}") # Skip files that can't be read
elif is_binary: pass
try:
# Check file size before reading to avoid OOM
stat_result = await sandbox.commands.run(
f"stat -c %s {shlex.quote(file_path)} 2>/dev/null"
)
if stat_result.exit_code != 0 or not stat_result.stdout:
logger.warning(
f"Skipping binary file {file_path}: "
f"could not determine file size"
)
continue
file_size = int(stat_result.stdout.strip())
if file_size > MAX_BINARY_FILE_SIZE:
logger.warning(
f"Skipping binary file {file_path}: "
f"size {file_size} exceeds limit "
f"{MAX_BINARY_FILE_SIZE}"
)
continue
# Read binary file as bytes using format="bytes" except Exception:
content_bytes = await sandbox.files.read( # If file extraction fails, return empty results
file_path, format="bytes" pass
)
# Base64 encode the binary content
content_b64 = base64.b64encode(content_bytes).decode(
"ascii"
)
file_name, relative_path = get_file_info(
file_path, working_directory
)
files.append(
ClaudeCodeBlock.FileOutput(
path=file_path,
relative_path=relative_path,
name=file_name,
content="", # Empty for binary files
is_binary=True,
content_base64=content_b64,
)
)
except Exception as e:
logger.warning(
f"Failed to read binary file {file_path}: {e}"
)
except Exception as e:
logger.warning(f"File extraction failed: {e}")
return files return files

View File

@@ -63,6 +63,17 @@ const CustomEdge = ({
return ( return (
<> <>
{/* Invisible interaction path - wider hit area for hover detection */}
<path
d={edgePath}
fill="none"
stroke="black"
strokeOpacity={0}
strokeWidth={20}
className="react-flow__edge-interaction cursor-pointer"
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
/>
<BaseEdge <BaseEdge
path={edgePath} path={edgePath}
markerEnd={markerEnd} markerEnd={markerEnd}

View File

@@ -16,7 +16,7 @@ When activated, the block:
- Install dependencies (npm, pip, etc.) - Install dependencies (npm, pip, etc.)
- Run terminal commands - Run terminal commands
- Build and test applications - Build and test applications
5. Extracts all text and binary files created/modified during execution 5. Extracts all text files created/modified during execution
6. Returns the response and files, optionally keeping the sandbox alive for follow-up tasks 6. Returns the response and files, optionally keeping the sandbox alive for follow-up tasks
The block supports conversation continuation through three mechanisms: The block supports conversation continuation through three mechanisms:
@@ -42,7 +42,7 @@ The block supports conversation continuation through three mechanisms:
| Output | Description | | Output | Description |
|--------|-------------| |--------|-------------|
| Response | The output/response from Claude Code execution | | Response | The output/response from Claude Code execution |
| Files | List of files created/modified during execution. Each file includes path, relative_path, name, content, is_binary, and content_base64 fields. For text files, content contains the text and is_binary is False. For binary files (PDFs, images, etc.), is_binary is True and content_base64 contains the base64-encoded data | | Files | List of text files created/modified during execution. Each file includes path, relative_path, name, and content fields |
| Conversation History | Full conversation history including this turn. Use to restore context on a fresh sandbox | | Conversation History | Full conversation history including this turn. Use to restore context on a fresh sandbox |
| Session ID | Session ID for this conversation. Pass back with sandbox_id to continue the conversation | | Session ID | Session ID for this conversation. Pass back with sandbox_id to continue the conversation |
| Sandbox ID | ID of the sandbox instance (null if disposed). Pass back with session_id to continue the conversation | | Sandbox ID | ID of the sandbox instance (null if disposed). Pass back with session_id to continue the conversation |

View File

@@ -535,7 +535,7 @@ When activated, the block:
2. Installs the latest version of Claude Code in the sandbox 2. Installs the latest version of Claude Code in the sandbox
3. Optionally runs setup commands to prepare the environment 3. Optionally runs setup commands to prepare the environment
4. Executes your prompt using Claude Code, which can create/edit files, install dependencies, run terminal commands, and build applications 4. Executes your prompt using Claude Code, which can create/edit files, install dependencies, run terminal commands, and build applications
5. Extracts all text and binary files created/modified during execution 5. Extracts all text files created/modified during execution
6. Returns the response and files, optionally keeping the sandbox alive for follow-up tasks 6. Returns the response and files, optionally keeping the sandbox alive for follow-up tasks
The block supports conversation continuation through three mechanisms: The block supports conversation continuation through three mechanisms:
@@ -563,7 +563,7 @@ The block supports conversation continuation through three mechanisms:
|--------|-------------|------| |--------|-------------|------|
| error | Error message if execution failed | str | | error | Error message if execution failed | str |
| response | The output/response from Claude Code execution | str | | response | The output/response from Claude Code execution | str |
| files | List of files created/modified by Claude Code during this execution. Each file has 'path', 'relative_path', 'name', 'content', 'is_binary', and 'content_base64' fields. For text files, 'content' contains the text and 'is_binary' is False. For binary files (PDFs, images, etc.), 'is_binary' is True and 'content_base64' contains the base64-encoded data. | List[FileOutput] | | files | List of text files created/modified by Claude Code during this execution. Each file has 'path', 'relative_path', 'name', and 'content' fields. | List[FileOutput] |
| conversation_history | Full conversation history including this turn. Pass this to conversation_history input to continue on a fresh sandbox if the previous sandbox timed out. | str | | conversation_history | Full conversation history including this turn. Pass this to conversation_history input to continue on a fresh sandbox if the previous sandbox timed out. | str |
| session_id | Session ID for this conversation. Pass this back along with sandbox_id to continue the conversation. | str | | session_id | Session ID for this conversation. Pass this back along with sandbox_id to continue the conversation. | str |
| sandbox_id | ID of the sandbox instance. Pass this back along with session_id to continue the conversation. This is None if dispose_sandbox was True (sandbox was disposed). | str | | sandbox_id | ID of the sandbox instance. Pass this back along with session_id to continue the conversation. This is None if dispose_sandbox was True (sandbox was disposed). | str |