Skip to content

Commit 49a905b

Browse files
committed
add feature allow user to paste image
1 parent 854b9bd commit 49a905b

File tree

8 files changed

+1008
-682
lines changed

8 files changed

+1008
-682
lines changed

AgentCrew/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "0.6.5-4"
1+
__version__ = "0.6.6"

AgentCrew/modules/clipboard/service.py

Lines changed: 109 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,22 @@
11
import pyperclip
22
import base64
33
import io
4+
import os
5+
import tempfile
46
from PIL import ImageGrab, Image
5-
from typing import Dict, Any
7+
from typing import Dict, Any, Optional
8+
import logging
9+
10+
logger = logging.getLogger(__name__)
611

712

813
class ClipboardService:
914
"""Service for interacting with the system clipboard."""
1015

16+
def __init__(self):
17+
"""Initialize the clipboard service."""
18+
self.temp_files = [] # Keep track of temporary files for cleanup
19+
1120
def write_text(self, content: str) -> Dict[str, Any]:
1221
"""
1322
Write text content to the clipboard.
@@ -30,6 +39,36 @@ def write_text(self, content: str) -> Dict[str, Any]:
3039
"error": f"Failed to write to clipboard: {str(e)}",
3140
}
3241

42+
def _create_temp_file_from_image(self, image: Image.Image) -> Optional[str]:
43+
"""
44+
Create a temporary file from a PIL Image.
45+
46+
Args:
47+
image: PIL Image object
48+
49+
Returns:
50+
Path to the temporary file or None if failed
51+
"""
52+
try:
53+
# Create a temporary file
54+
temp_fd, temp_path = tempfile.mkstemp(
55+
suffix=".png", prefix="clipboard_image_"
56+
)
57+
os.close(temp_fd) # Close the file descriptor
58+
59+
# Save the image to the temporary file
60+
image.save(temp_path, format="PNG")
61+
62+
# Keep track of temp file for cleanup
63+
self.temp_files.append(temp_path)
64+
65+
logger.info(f"Created temporary image file: {temp_path}")
66+
return temp_path
67+
68+
except Exception as e:
69+
logger.error(f"Failed to create temporary file from image: {str(e)}")
70+
return None
71+
3372
def read(self) -> Dict[str, Any]:
3473
"""
3574
Read content from the clipboard and automatically determine the content type.
@@ -72,6 +111,71 @@ def read(self) -> Dict[str, Any]:
72111
"error": f"Failed to read from clipboard: {str(e)}",
73112
}
74113

114+
def read_and_process_paste(self) -> Dict[str, Any]:
115+
"""
116+
Read clipboard content and if it's an image or binary file, create a temporary file
117+
and return a file command that can be processed.
118+
119+
Returns:
120+
Dict containing either processed file command or regular text content
121+
"""
122+
123+
image = ImageGrab.grabclipboard()
124+
125+
clipboard_result = {
126+
"success": False,
127+
}
128+
129+
if image is not None and isinstance(image, Image.Image):
130+
# Handle image content - create temporary file
131+
temp_file_path = self._create_temp_file_from_image(image)
132+
133+
if temp_file_path:
134+
clipboard_result = {
135+
"success": True,
136+
"content": temp_file_path,
137+
"type": "image_file",
138+
"format": "file",
139+
"cleanup_required": True,
140+
}
141+
142+
if not clipboard_result["success"]:
143+
return clipboard_result
144+
145+
content_type = clipboard_result.get("type")
146+
147+
if content_type in ["image_file", "binary_file"]:
148+
# Return a file command for the temporary file
149+
temp_file_path = clipboard_result["content"]
150+
return {
151+
"success": True,
152+
"content": f"/file {temp_file_path}",
153+
"type": "file_command",
154+
"temp_file_path": temp_file_path,
155+
"original_type": content_type,
156+
"cleanup_required": clipboard_result.get("cleanup_required", False),
157+
}
158+
elif content_type == "image":
159+
# For base64 images, still return as is for backwards compatibility
160+
return clipboard_result
161+
else:
162+
# Regular text content
163+
return clipboard_result
164+
165+
def cleanup_temp_files(self):
166+
"""Clean up any temporary files created by this service."""
167+
for temp_file in self.temp_files:
168+
try:
169+
if os.path.exists(temp_file):
170+
os.unlink(temp_file)
171+
logger.info(f"Cleaned up temporary file: {temp_file}")
172+
except Exception as e:
173+
logger.warning(
174+
f"Failed to cleanup temporary file {temp_file}: {str(e)}"
175+
)
176+
177+
self.temp_files = []
178+
75179
def write(self, content: str) -> Dict[str, Any]:
76180
"""
77181
Write content to the clipboard.
@@ -83,3 +187,7 @@ def write(self, content: str) -> Dict[str, Any]:
83187
Dict containing success status and any error information
84188
"""
85189
return self.write_text(content)
190+
191+
def __del__(self):
192+
"""Cleanup temporary files when the service is destroyed."""
193+
self.cleanup_temp_files()

AgentCrew/modules/console/input_handler.py

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
from AgentCrew.modules import logger
2020
from AgentCrew.modules.chat import MessageHandler
21+
from AgentCrew.modules.clipboard.service import ClipboardService
2122
from .completers import ChatCompleter
2223
from .display_handlers import DisplayHandlers
2324
from .constants import (
@@ -41,6 +42,7 @@ def __init__(
4142
self.console = console
4243
self.message_handler = message_handler
4344
self.display_handlers = display_handlers
45+
self.clipboard_service = ClipboardService()
4446

4547
# Threading for user input
4648
self._input_queue = queue.Queue()
@@ -75,6 +77,40 @@ def _(event):
7577
# This will be handled by the main console UI
7678
pass
7779

80+
@kb.add(Keys.ControlV)
81+
def _(event):
82+
"""Handle Ctrl+V with image/binary detection."""
83+
try:
84+
# Check if clipboard contains image or binary content
85+
paste_result = self.clipboard_service.read_and_process_paste()
86+
87+
if paste_result["success"]:
88+
content_type = paste_result.get("type")
89+
90+
if content_type == "file_command":
91+
# Insert the file command
92+
file_command = paste_result["content"]
93+
94+
# Insert the file command into the current buffer
95+
event.current_buffer.insert_text(file_command)
96+
event.current_buffer.validate_and_handle()
97+
98+
return
99+
100+
# For regular text content, use default paste behavior
101+
event.current_buffer.paste_clipboard_data(
102+
event.app.clipboard.get_data()
103+
)
104+
105+
except Exception:
106+
# Fall back to default paste behavior if anything goes wrong
107+
try:
108+
event.current_buffer.paste_clipboard_data(
109+
event.app.clipboard.get_data()
110+
)
111+
except Exception:
112+
pass # Ignore if even default paste fails
113+
78114
@kb.add(Keys.ControlC)
79115
def _(event):
80116
"""Handle Ctrl+C with confirmation for exit."""
@@ -257,7 +293,7 @@ def get_user_input(self):
257293
style=RICH_STYLE_BLUE,
258294
)
259295
title.append(
260-
"\n(Press Enter for new line, Ctrl+S/Alt+Enter to submit, Up/Down for history)\n",
296+
"\n(Press Enter for new line, Ctrl+S/Alt+Enter to submit, Up/Down for history, Ctrl+V to paste)\n",
261297
style=RICH_STYLE_YELLOW,
262298
)
263299
self.console.print(title)

AgentCrew/modules/gui/components/input_components.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,16 @@
33
from PySide6.QtWidgets import (
44
QVBoxLayout,
55
QHBoxLayout,
6-
QTextEdit,
76
QPushButton,
87
QCompleter,
98
QFileDialog,
109
QSizePolicy,
1110
)
12-
from PySide6.QtCore import Qt, QStringListModel
11+
from PySide6.QtCore import Qt, QStringListModel, Slot
1312
from PySide6.QtGui import QTextCursor
1413
import qtawesome as qta
1514
from AgentCrew.modules.console.completers import DirectoryListingCompleter
15+
from AgentCrew.modules.gui.widgets.paste_aware_textedit import PasteAwareTextEdit
1616
from .completers import GuiChatCompleter
1717

1818

@@ -29,8 +29,9 @@ def __init__(self, chat_window):
2929

3030
def _setup_input_area(self):
3131
"""Set up the input area with text input and buttons."""
32-
# Input area
33-
self.chat_window.message_input = QTextEdit()
32+
# Input area - use our custom paste-aware text edit
33+
self.chat_window.message_input = PasteAwareTextEdit()
34+
3435
input_font = self.chat_window.message_input.font()
3536
input_font.setPixelSize(16)
3637
self.chat_window.message_input.setFont(input_font)
@@ -46,6 +47,7 @@ def _setup_input_area(self):
4647
self.chat_window.message_input.setSizePolicy(
4748
QSizePolicy.Policy.Minimum, QSizePolicy.Policy.MinimumExpanding
4849
)
50+
self.chat_window.message_input.image_inserted.connect(self.image_inserted)
4951

5052
# Create buttons layout
5153
buttons_layout = QVBoxLayout()
@@ -75,6 +77,10 @@ def _setup_input_area(self):
7577
# Store the buttons layout for use in main window
7678
self.buttons_layout = buttons_layout
7779

80+
@Slot(str)
81+
def image_inserted(self, file_command):
82+
self.chat_window.llm_worker.process_request.emit(file_command)
83+
7884
def _setup_file_completion(self):
7985
"""Set up file path completion for the input field."""
8086
# Set up file path completion
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
from PySide6.QtWidgets import (
2+
QTextEdit,
3+
)
4+
from PySide6.QtCore import Signal
5+
from AgentCrew.modules.clipboard.service import ClipboardService
6+
7+
8+
class PasteAwareTextEdit(QTextEdit):
9+
"""Custom QTextEdit that handles paste events to detect images and binary content."""
10+
11+
image_inserted = Signal(str)
12+
13+
def __init__(self, parent=None):
14+
super().__init__(parent)
15+
self.clipboard_service = ClipboardService()
16+
17+
def insertFromMimeData(self, source):
18+
"""Override paste behavior to detect and handle images/binary content."""
19+
try:
20+
# Check clipboard content using our service
21+
paste_result = self.clipboard_service.read_and_process_paste()
22+
23+
if paste_result["success"]:
24+
content_type = paste_result.get("type")
25+
26+
if content_type == "file_command":
27+
# It's an image or binary file - use the file command
28+
file_command = paste_result["content"]
29+
30+
self.image_inserted.emit(file_command)
31+
32+
# Show status message
33+
34+
return # Don't call parent method
35+
36+
elif content_type == "text":
37+
# Regular text content - let the parent handle it normally
38+
super().insertFromMimeData(source)
39+
return
40+
41+
else:
42+
# Other content types (like base64 image) - handle normally for now
43+
super().insertFromMimeData(source)
44+
return
45+
else:
46+
# Failed to read clipboard, fall back to default behavior
47+
super().insertFromMimeData(source)
48+
49+
except Exception as e:
50+
# If anything goes wrong, fall back to default paste behavior
51+
print(f"Error in paste handling: {e}")
52+
super().insertFromMimeData(source)

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ Here are some things AgentCrew can do:
116116
window (GUI).
117117
- **File Handling:** AI agents can work with text and image files in chat.
118118
AgentCrew also supports PDF, DOCX, XLSX, and PPTX files.
119+
- **📋 Smart Paste Detection:** Automatically detects images and binary content when pasted (Ctrl+V). Images from screenshots, copied files, or other sources are automatically converted to `/file` commands for seamless processing.
119120
- **Streaming Responses:** Get real-time replies from AI agents.
120121
- **"Thinking Mode":** Some AI models can show their reasoning process.
121122
- **Rollback Messages:** Easily go back to an earlier point in your

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "agentcrew-ai"
3-
version = "0.6.5-4"
3+
version = "0.6.6"
44
requires-python = ">=3.12"
55
classifiers = [
66
"Programming Language :: Python :: 3",

0 commit comments

Comments
 (0)