agentcrew-ai 0.8.12__py3-none-any.whl → 0.8.13__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. AgentCrew/__init__.py +1 -1
  2. AgentCrew/main.py +55 -3
  3. AgentCrew/modules/agents/local_agent.py +25 -0
  4. AgentCrew/modules/code_analysis/__init__.py +8 -0
  5. AgentCrew/modules/code_analysis/parsers/__init__.py +67 -0
  6. AgentCrew/modules/code_analysis/parsers/base.py +93 -0
  7. AgentCrew/modules/code_analysis/parsers/cpp_parser.py +127 -0
  8. AgentCrew/modules/code_analysis/parsers/csharp_parser.py +162 -0
  9. AgentCrew/modules/code_analysis/parsers/generic_parser.py +63 -0
  10. AgentCrew/modules/code_analysis/parsers/go_parser.py +154 -0
  11. AgentCrew/modules/code_analysis/parsers/java_parser.py +103 -0
  12. AgentCrew/modules/code_analysis/parsers/javascript_parser.py +268 -0
  13. AgentCrew/modules/code_analysis/parsers/kotlin_parser.py +84 -0
  14. AgentCrew/modules/code_analysis/parsers/php_parser.py +107 -0
  15. AgentCrew/modules/code_analysis/parsers/python_parser.py +60 -0
  16. AgentCrew/modules/code_analysis/parsers/ruby_parser.py +46 -0
  17. AgentCrew/modules/code_analysis/parsers/rust_parser.py +72 -0
  18. AgentCrew/modules/code_analysis/service.py +231 -897
  19. AgentCrew/modules/command_execution/constants.py +2 -2
  20. AgentCrew/modules/console/confirmation_handler.py +4 -4
  21. AgentCrew/modules/console/console_ui.py +20 -1
  22. AgentCrew/modules/console/conversation_browser.py +557 -0
  23. AgentCrew/modules/console/diff_display.py +22 -51
  24. AgentCrew/modules/console/display_handlers.py +22 -22
  25. AgentCrew/modules/console/tool_display.py +4 -6
  26. AgentCrew/modules/file_editing/service.py +8 -8
  27. AgentCrew/modules/file_editing/tool.py +65 -67
  28. AgentCrew/modules/gui/components/tool_handlers.py +0 -2
  29. AgentCrew/modules/gui/widgets/diff_widget.py +30 -61
  30. AgentCrew/modules/llm/constants.py +5 -5
  31. AgentCrew/modules/memory/context_persistent.py +1 -0
  32. AgentCrew/modules/memory/tool.py +1 -1
  33. {agentcrew_ai-0.8.12.dist-info → agentcrew_ai-0.8.13.dist-info}/METADATA +1 -1
  34. {agentcrew_ai-0.8.12.dist-info → agentcrew_ai-0.8.13.dist-info}/RECORD +38 -24
  35. {agentcrew_ai-0.8.12.dist-info → agentcrew_ai-0.8.13.dist-info}/WHEEL +1 -1
  36. {agentcrew_ai-0.8.12.dist-info → agentcrew_ai-0.8.13.dist-info}/entry_points.txt +0 -0
  37. {agentcrew_ai-0.8.12.dist-info → agentcrew_ai-0.8.13.dist-info}/licenses/LICENSE +0 -0
  38. {agentcrew_ai-0.8.12.dist-info → agentcrew_ai-0.8.13.dist-info}/top_level.txt +0 -0
@@ -4,6 +4,7 @@ Provides split view diff display with syntax highlighting.
4
4
  """
5
5
 
6
6
  import difflib
7
+ from typing import List, Dict
7
8
  from rich.text import Text
8
9
  from rich.table import Table
9
10
  from rich.box import SIMPLE_HEAD
@@ -16,67 +17,37 @@ class DiffDisplay:
16
17
  """Helper class for creating split diff views."""
17
18
 
18
19
  @staticmethod
19
- def has_search_replace_blocks(text: str) -> bool:
20
- """Check if text contains search/replace blocks."""
21
- return (
22
- "<<<<<<< SEARCH" in text and "=======" in text and ">>>>>>> REPLACE" in text
20
+ def has_search_replace_blocks(blocks: List[Dict]) -> bool:
21
+ """Check if input is a valid list of search/replace blocks."""
22
+ if not isinstance(blocks, list):
23
+ return False
24
+ return len(blocks) > 0 and all(
25
+ isinstance(b, dict) and "search" in b and "replace" in b for b in blocks
23
26
  )
24
27
 
25
28
  @staticmethod
26
- def parse_search_replace_blocks(blocks_text: str) -> list:
29
+ def parse_search_replace_blocks(blocks: List[Dict]) -> List[Dict]:
27
30
  """
28
- Parse search/replace blocks from text.
31
+ Parse search/replace blocks from list format.
29
32
 
30
33
  Args:
31
- blocks_text: Text containing search/replace blocks
34
+ blocks: List of dicts with 'search' and 'replace' keys
32
35
 
33
36
  Returns:
34
37
  List of dicts with 'index', 'search', and 'replace' keys
35
38
  """
36
- blocks = []
37
- lines = blocks_text.split("\n")
38
- i = 0
39
- block_index = 0
40
-
41
- while i < len(lines):
42
- if lines[i].strip() == "<<<<<<< SEARCH":
43
- search_lines = []
44
- i += 1
45
-
46
- while i < len(lines) and lines[i].strip() != "=======":
47
- search_lines.append(lines[i])
48
- i += 1
49
-
50
- if i >= len(lines):
51
- break
52
-
53
- i += 1
54
- replace_lines = []
55
-
56
- while (
57
- i < len(lines)
58
- and lines[i].strip() != ">>>>>>> REPLACE"
59
- and lines[i].strip() != "======="
60
- ):
61
- replace_lines.append(lines[i])
62
- i += 1
63
-
64
- if i >= len(lines):
65
- break
66
-
67
- blocks.append(
68
- {
69
- "index": block_index,
70
- "search": "\n".join(search_lines),
71
- "replace": "\n".join(replace_lines),
72
- }
73
- )
74
- block_index += 1
75
- i += 1
76
- else:
77
- i += 1
78
-
79
- return blocks
39
+ if not isinstance(blocks, list):
40
+ return []
41
+
42
+ return [
43
+ {
44
+ "index": i,
45
+ "search": block.get("search", ""),
46
+ "replace": block.get("replace", ""),
47
+ }
48
+ for i, block in enumerate(blocks)
49
+ if isinstance(block, dict)
50
+ ]
80
51
 
81
52
  @staticmethod
82
53
  def create_split_diff_table(
@@ -157,34 +157,34 @@ class DisplayHandlers:
157
157
  f" - {agent_name}{current}: {agent_data['description']}"
158
158
  )
159
159
 
160
- def display_conversations(self, conversations: List[Dict[str, Any]]):
161
- """Display available conversations."""
160
+ def display_conversations(
161
+ self,
162
+ conversations: List[Dict[str, Any]],
163
+ get_history_callback=None,
164
+ ):
165
+ """Display available conversations using interactive browser.
166
+
167
+ Args:
168
+ conversations: List of conversation metadata
169
+ get_history_callback: Optional callback to fetch full conversation history
170
+
171
+ Returns:
172
+ Selected conversation ID or None if cancelled
173
+ """
162
174
  if not conversations:
163
175
  self.console.print(
164
176
  Text("No saved conversations found.", style=RICH_STYLE_YELLOW)
165
177
  )
166
- return
178
+ return None
167
179
 
168
- self.console.print(Text("Available conversations:", style=RICH_STYLE_YELLOW))
169
- for i, convo in enumerate(conversations[:30], 1):
170
- # Format timestamp for better readability
171
- timestamp = convo.get("timestamp", "Unknown")
172
- if isinstance(timestamp, (int, float)):
173
- timestamp = datetime.fromtimestamp(timestamp).strftime(
174
- "%Y-%m-%d %H:%M:%S"
175
- )
180
+ from .conversation_browser import ConversationBrowser
176
181
 
177
- title = convo.get("title", "Untitled")
178
- convo_id = convo.get("id", "unknown")
179
-
180
- # Display conversation with index for easy selection
181
- self.console.print(f" {i}. {title} [{convo_id}]")
182
- self.console.print(f" Created: {timestamp}")
183
-
184
- # Show a preview if available
185
- if "preview" in convo:
186
- self.console.print(f" Preview: {convo['preview']}")
187
- self.console.print("")
182
+ browser = ConversationBrowser(
183
+ console=self.console,
184
+ get_conversation_history=get_history_callback,
185
+ )
186
+ browser.set_conversations(conversations)
187
+ return browser.show()
188
188
 
189
189
  def display_consolidation_result(self, result: Dict[str, Any]):
190
190
  """Display information about a consolidation operation."""
@@ -109,9 +109,7 @@ class ToolDisplayHandlers:
109
109
  )
110
110
  )
111
111
 
112
- def _display_write_or_edit_file_use(
113
- self, tool_use: Dict, file_path: str, blocks_text: str
114
- ):
112
+ def _display_write_or_edit_file_use(self, tool_use: Dict, file_path: str, blocks):
115
113
  """Display write_or_edit_file tool with split diff view."""
116
114
  tool_icon = self.get_tool_icon(tool_use["name"])
117
115
 
@@ -121,10 +119,10 @@ class ToolDisplayHandlers:
121
119
 
122
120
  self.console.print(Panel(header, box=HORIZONTALS, title_align="left"))
123
121
 
124
- blocks = DiffDisplay.parse_search_replace_blocks(blocks_text)
122
+ parsed_blocks = DiffDisplay.parse_search_replace_blocks(blocks)
125
123
 
126
- if blocks:
127
- for block in blocks:
124
+ if parsed_blocks:
125
+ for block in parsed_blocks:
128
126
  diff_table = DiffDisplay.create_split_diff_table(
129
127
  block["search"], block["replace"], max_width=self.console.width - 4
130
128
  )
@@ -41,21 +41,21 @@ class FileEditingService:
41
41
  def write_or_edit_file(
42
42
  self,
43
43
  file_path: str,
44
- percentage_to_change: float,
45
44
  text_or_search_replace_blocks: str,
45
+ is_search_replace: bool = False,
46
46
  agent_name: Optional[str] = None,
47
47
  ) -> Dict[str, Any]:
48
48
  """
49
49
  Main entry point for file editing.
50
50
 
51
51
  Decision logic:
52
- - percentage > 50: Full file write
53
- - percentage <= 50: Search/replace blocks
52
+ - is_search_replace=False: Full file write
53
+ - is_search_replace=True: Search/replace blocks
54
54
 
55
55
  Args:
56
56
  file_path: Path to file (absolute or relative, ~ supported)
57
- percentage_to_change: Percentage of lines changing (0-100)
58
57
  text_or_search_replace_blocks: Full content or search/replace blocks
58
+ is_search_replace: True for search/replace mode, False for full content
59
59
  agent_name: Optional agent name for permission checks
60
60
 
61
61
  Returns:
@@ -94,12 +94,12 @@ class FileEditingService:
94
94
  }
95
95
 
96
96
  try:
97
- if percentage_to_change > 50:
98
- result = self._write_full_file(file_path, text_or_search_replace_blocks)
99
- else:
97
+ if is_search_replace:
100
98
  result = self._apply_search_replace(
101
99
  file_path, text_or_search_replace_blocks
102
100
  )
101
+ else:
102
+ result = self._write_full_file(file_path, text_or_search_replace_blocks)
103
103
 
104
104
  if result["status"] != "success":
105
105
  return result
@@ -166,7 +166,7 @@ class FileEditingService:
166
166
  return {
167
167
  "status": "error",
168
168
  "error": f"File not found: {file_path}",
169
- "suggestion": "Use percentage_to_change > 50 to create new files",
169
+ "suggestion": "Use full content mode (string) to create new files",
170
170
  }
171
171
 
172
172
  try:
@@ -4,39 +4,42 @@ File editing tool definitions and handlers for AgentCrew.
4
4
  Provides file_write_or_edit tool for intelligent file editing with search/replace blocks.
5
5
  """
6
6
 
7
- from typing import Dict, Any, Callable, Optional
7
+ from typing import Dict, Any, Callable, Optional, List
8
8
  from .service import FileEditingService
9
9
 
10
10
 
11
- def get_file_write_or_edit_tool_definition(provider="claude") -> Dict[str, Any]:
12
- """
13
- Get tool definition for file editing.
11
+ def convert_blocks_to_string(blocks: List[Dict[str, str]]) -> str:
12
+ result_parts = []
13
+ for block in blocks:
14
+ search_text = block.get("search", "")
15
+ replace_text = block.get("replace", "")
16
+ block_str = (
17
+ f"<<<<<<< SEARCH\n{search_text}\n=======\n{replace_text}\n>>>>>>> REPLACE"
18
+ )
19
+ result_parts.append(block_str)
20
+ return "\n".join(result_parts)
21
+
14
22
 
15
- Args:
16
- provider: LLM provider name ("claude", "openai", "groq", "google")
23
+ def is_full_content_mode(blocks: List[Dict[str, str]]) -> bool:
24
+ if len(blocks) == 1:
25
+ block = blocks[0]
26
+ search_text = block.get("search", "")
27
+ return search_text == ""
28
+ return False
17
29
 
18
- Returns:
19
- Provider-specific tool definition
20
- """
21
- tool_description = """Write/edit files via search/replace blocks or full content.
22
30
 
23
- LOGIC: percentage_to_change >50 = full content | ≤50 = search/replace
31
+ def get_file_write_or_edit_tool_definition(provider="claude") -> Dict[str, Any]:
32
+ tool_description = """Write/edit files via search/replace blocks.
24
33
 
25
- SEARCH/REPLACE BLOCK FORMAT:
26
- <<<<<<< SEARCH
27
- [exact content to find]
28
- =======
29
- [replacement content]
30
- >>>>>>> REPLACE
34
+ FORMAT: Array of {"search": "...", "replace": "..."} objects
35
+ - Empty search + replace with content = write full file content
36
+ - Non-empty search + replace = search/replace operation
37
+ - Non-empty search + empty replace = delete matched content
31
38
 
32
39
  RULES:
33
40
  1. SEARCH must match exactly (character-perfect)
34
41
  2. Include changing lines +0-3 context
35
- 3. Multiple blocks in one call OK
36
- 4. Preserve whitespace/indentation
37
- 5. Empty REPLACE = delete
38
-
39
- EXAMPLES: Add import (existing+new) | Delete (full→empty) | Modify (signature+changes)
42
+ 3. Preserve whitespace/indentation
40
43
 
41
44
  Auto syntax check (30+ langs) with rollback on error
42
45
  """
@@ -46,19 +49,28 @@ Auto syntax check (30+ langs) with rollback on error
46
49
  "type": "string",
47
50
  "description": "Path (absolute/relative). Use ~ for home. Ex: './src/main.py'",
48
51
  },
49
- "percentage_to_change": {
50
- "type": "number",
51
- "description": "% lines changing (0-100). >50=full, ≤50=blocks",
52
- },
53
52
  "text_or_search_replace_blocks": {
54
- "type": "string",
55
- "description": "Full content (>50%) OR search/replace blocks (≤50%)",
53
+ "type": "array",
54
+ "items": {
55
+ "type": "object",
56
+ "properties": {
57
+ "search": {
58
+ "type": "string",
59
+ "description": "Exact content to find. Empty string means full file write mode.",
60
+ },
61
+ "replace": {
62
+ "type": "string",
63
+ "description": "Replacement content (empty string to delete)",
64
+ },
65
+ },
66
+ "required": ["search", "replace"],
67
+ },
68
+ "description": 'Array of search/replace blocks. For full file write: [{"search": "", "replace": "full content"}]. For edits: [{"search": "exact match", "replace": "replacement"}]',
56
69
  },
57
70
  }
58
71
 
59
72
  tool_required = [
60
73
  "file_path",
61
- "percentage_to_change",
62
74
  "text_or_search_replace_blocks",
63
75
  ]
64
76
 
@@ -72,7 +84,7 @@ Auto syntax check (30+ langs) with rollback on error
72
84
  "required": tool_required,
73
85
  },
74
86
  }
75
- else: # provider in ["openai", "google", "groq"] or other OpenAI-compatible
87
+ else:
76
88
  return {
77
89
  "type": "function",
78
90
  "function": {
@@ -90,44 +102,37 @@ Auto syntax check (30+ langs) with rollback on error
90
102
  def get_file_write_or_edit_tool_handler(
91
103
  file_editing_service: FileEditingService,
92
104
  ) -> Callable:
93
- """
94
- Get the handler function for the file editing tool.
95
-
96
- Args:
97
- file_editing_service: FileEditingService instance
98
-
99
- Returns:
100
- Handler function
101
- """
102
-
103
105
  def handle_file_write_or_edit(**params) -> str:
104
- """
105
- Tool execution handler.
106
-
107
- Args:
108
- **params: Tool parameters (file_path, percentage_to_change, text_or_search_replace_blocks)
109
-
110
- Returns:
111
- Success or error message
112
- """
113
106
  file_path = params.get("file_path")
114
- percentage_to_change = params.get("percentage_to_change")
115
- text_or_search_replace_blocks = params.get("text_or_search_replace_blocks")
107
+ blocks = params.get("text_or_search_replace_blocks")
116
108
 
117
109
  if not file_path:
118
110
  raise ValueError("Error: No file path provided.")
119
111
 
120
- if percentage_to_change is None:
121
- raise ValueError("Error: No percentage_to_change provided.")
112
+ if blocks is None:
113
+ raise ValueError("Error: No search/replace blocks provided.")
122
114
 
123
- if not text_or_search_replace_blocks:
124
- raise ValueError("Error: No content or search/replace blocks provided.")
115
+ if not isinstance(blocks, list):
116
+ raise ValueError(
117
+ "Error: text_or_search_replace_blocks must be an array of search/replace objects."
118
+ )
125
119
 
126
- result = file_editing_service.write_or_edit_file(
127
- file_path=file_path,
128
- percentage_to_change=float(percentage_to_change),
129
- text_or_search_replace_blocks=text_or_search_replace_blocks,
130
- )
120
+ full_content_mode = is_full_content_mode(blocks)
121
+
122
+ if full_content_mode:
123
+ content = blocks[0].get("replace", "")
124
+ result = file_editing_service.write_or_edit_file(
125
+ file_path=file_path,
126
+ is_search_replace=False,
127
+ text_or_search_replace_blocks=content,
128
+ )
129
+ else:
130
+ blocks_string = convert_blocks_to_string(blocks)
131
+ result = file_editing_service.write_or_edit_file(
132
+ file_path=file_path,
133
+ is_search_replace=True,
134
+ text_or_search_replace_blocks=blocks_string,
135
+ )
131
136
 
132
137
  if result["status"] == "success":
133
138
  parts = [f"{result['file_path']}"]
@@ -171,13 +176,6 @@ def get_file_write_or_edit_tool_handler(
171
176
 
172
177
 
173
178
  def register(service_instance: Optional[FileEditingService] = None, agent=None):
174
- """
175
- Register file editing tools with AgentCrew tool registry.
176
-
177
- Args:
178
- service_instance: Optional FileEditingService instance
179
- agent: Optional agent to register with directly
180
- """
181
179
  from AgentCrew.modules.tools.registration import register_tool
182
180
 
183
181
  if service_instance is None:
@@ -302,7 +302,6 @@ class ToolEventHandler:
302
302
  tool_input = tool_use.get("input", {})
303
303
  file_path = tool_input.get("file_path", "")
304
304
  text_or_blocks = tool_input.get("text_or_search_replace_blocks", "")
305
- percentage = tool_input.get("percentage_to_change", 0)
306
305
 
307
306
  has_diff = DiffWidget.has_search_replace_blocks(text_or_blocks)
308
307
 
@@ -321,7 +320,6 @@ class ToolEventHandler:
321
320
  layout.addWidget(header_label)
322
321
 
323
322
  info_label = QLabel(
324
- f"Change percentage: {percentage}% | "
325
323
  f"Mode: {'Search/Replace Blocks' if has_diff else 'Full Content'}"
326
324
  )
327
325
  info_label.setStyleSheet(
@@ -17,10 +17,6 @@ from PySide6.QtCore import Qt
17
17
  class DiffWidget(QWidget):
18
18
  """Widget to display split diff view for file changes."""
19
19
 
20
- SEARCH_DELIMITER = "<<<<<<< SEARCH"
21
- MIDDLE_DELIMITER = "======="
22
- REPLACE_DELIMITER = ">>>>>>> REPLACE"
23
-
24
20
  def __init__(self, parent=None, style_provider=None):
25
21
  super().__init__(parent)
26
22
  self._style_provider = style_provider
@@ -62,71 +58,44 @@ class DiffWidget(QWidget):
62
58
  self.main_layout.setSpacing(8)
63
59
 
64
60
  @staticmethod
65
- def has_search_replace_blocks(text: str) -> bool:
66
- """Check if text contains search/replace blocks."""
67
- return (
68
- "<<<<<<< SEARCH" in text and "=======" in text and ">>>>>>> REPLACE" in text
61
+ def has_search_replace_blocks(blocks: List[Dict]) -> bool:
62
+ """Check if input is a valid list of search/replace blocks."""
63
+ if not isinstance(blocks, list):
64
+ return False
65
+ return len(blocks) > 0 and all(
66
+ isinstance(b, dict) and "search" in b and "replace" in b for b in blocks
69
67
  )
70
68
 
71
69
  @staticmethod
72
- def parse_search_replace_blocks(blocks_text: str) -> List[Dict]:
70
+ def parse_search_replace_blocks(blocks: List[Dict]) -> List[Dict]:
73
71
  """
74
- Parse search/replace blocks from text.
72
+ Parse search/replace blocks from list format.
73
+
74
+ Args:
75
+ blocks: List of dicts with 'search' and 'replace' keys
75
76
 
76
77
  Returns:
77
78
  List of dicts with 'index', 'search', and 'replace' keys
78
79
  """
79
- blocks = []
80
- lines = blocks_text.split("\n")
81
- i = 0
82
- block_index = 0
83
-
84
- while i < len(lines):
85
- if lines[i].strip() == "<<<<<<< SEARCH":
86
- search_lines = []
87
- i += 1
88
-
89
- while i < len(lines) and lines[i].strip() != "=======":
90
- search_lines.append(lines[i])
91
- i += 1
92
-
93
- if i >= len(lines):
94
- break
95
-
96
- i += 1
97
- replace_lines = []
98
-
99
- while (
100
- i < len(lines)
101
- and lines[i].strip() != ">>>>>>> REPLACE"
102
- and lines[i].strip() != "======="
103
- ):
104
- replace_lines.append(lines[i])
105
- i += 1
106
-
107
- if i >= len(lines):
108
- break
109
-
110
- blocks.append(
111
- {
112
- "index": block_index,
113
- "search": "\n".join(search_lines),
114
- "replace": "\n".join(replace_lines),
115
- }
116
- )
117
- block_index += 1
118
- i += 1
119
- else:
120
- i += 1
121
-
122
- return blocks
123
-
124
- def set_diff_content(self, blocks_text: str, file_path: str = ""):
80
+ if not isinstance(blocks, list):
81
+ return []
82
+
83
+ return [
84
+ {
85
+ "index": i,
86
+ "search": block.get("search", ""),
87
+ "replace": block.get("replace", ""),
88
+ }
89
+ for i, block in enumerate(blocks)
90
+ if isinstance(block, dict)
91
+ ]
92
+
93
+ def set_diff_content(self, blocks: List[Dict], file_path: str = ""):
125
94
  """
126
95
  Set the diff content to display.
127
96
 
128
97
  Args:
129
- blocks_text: Text containing search/replace blocks
98
+ blocks: List of search/replace block dicts
130
99
  file_path: Optional file path to display in header
131
100
  """
132
101
  self._clear_layout()
@@ -139,7 +108,7 @@ class DiffWidget(QWidget):
139
108
  )
140
109
  self.main_layout.addWidget(header)
141
110
 
142
- blocks = self.parse_search_replace_blocks(blocks_text)
111
+ blocks = self.parse_search_replace_blocks(blocks)
143
112
 
144
113
  if not blocks:
145
114
  no_blocks_label = QLabel("No valid search/replace blocks found")
@@ -378,12 +347,12 @@ class CompactDiffWidget(QWidget):
378
347
  self.main_layout.setContentsMargins(4, 4, 4, 4)
379
348
  self.main_layout.setSpacing(4)
380
349
 
381
- def set_diff_content(self, blocks_text: str, file_path: str = ""):
350
+ def set_diff_content(self, blocks: List[Dict], file_path: str = ""):
382
351
  """Set the diff content to display in compact form."""
383
352
  self._clear_layout()
384
353
  colors = self._colors
385
354
 
386
- blocks = DiffWidget.parse_search_replace_blocks(blocks_text)
355
+ blocks = DiffWidget.parse_search_replace_blocks(blocks)
387
356
 
388
357
  if not blocks:
389
358
  return
@@ -428,7 +397,7 @@ class CompactDiffWidget(QWidget):
428
397
  layout.setContentsMargins(6, 6, 6, 6)
429
398
  layout.setSpacing(1)
430
399
 
431
- if len(DiffWidget.parse_search_replace_blocks(original + modified)) > 1:
400
+ if block_num > 1:
432
401
  block_header = QLabel(f"Block {block_num}")
433
402
  block_header.setStyleSheet(
434
403
  f"font-size: 10px; color: {colors['line_number_text']}; padding: 0 0 2px 0;"
@@ -325,14 +325,14 @@ _DEEPINFRA_MODELS = [
325
325
  output_token_price_1m=0.6,
326
326
  ),
327
327
  Model(
328
- id="zai-org/GLM-4.6",
328
+ id="zai-org/GLM-4.7",
329
329
  provider="deepinfra",
330
- name="Zai GLM-4.6",
331
- description="The GLM-4.6 series models are foundation models designed for intelligent agents",
330
+ name="Zai GLM-4.7",
331
+ description="GLM-4.7 is a state-of-the-art, multilingual Mixture-of-Experts (MoE) language model designed for complex reasoning, agentic coding, and tool use",
332
332
  force_sample_params=SampleParam(temperature=1, top_p=0.95, top_k=40),
333
333
  capabilities=["tool_use", "stream", "structured_output"],
334
- input_token_price_1m=0.6,
335
- output_token_price_1m=2.0,
334
+ input_token_price_1m=0.43,
335
+ output_token_price_1m=1.75,
336
336
  ),
337
337
  Model(
338
338
  id="Qwen/Qwen3-32B",
@@ -393,6 +393,7 @@ class ContextPersistenceService:
393
393
  {
394
394
  "id": conversation_id,
395
395
  "timestamp": timestamp,
396
+ "title": preview,
396
397
  "preview": preview,
397
398
  }
398
399
  )
@@ -53,7 +53,7 @@ def get_memory_forget_tool_handler(memory_service: BaseMemoryService) -> Callabl
53
53
  """Optimized memory forgetting handler with concise feedback."""
54
54
 
55
55
  def handle_memory_forget(**params) -> str:
56
- ids = params.get("ids", "").strip()
56
+ ids = params.get("ids", [])
57
57
 
58
58
  # Use provided agent_name or fallback to current agent
59
59
  current_agent = AgentManager.get_instance().get_current_agent()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentcrew-ai
3
- Version: 0.8.12
3
+ Version: 0.8.13
4
4
  Summary: Multi-Agents Interactive Chat Tool
5
5
  Author-email: Quy Truong <quy.truong@saigontechnology.com>
6
6
  License-Expression: Apache-2.0