auto-coder 0.1.353__py3-none-any.whl → 0.1.355__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.

Potentially problematic release.


This version of auto-coder might be problematic. Click here for more details.

Files changed (60) hide show
  1. {auto_coder-0.1.353.dist-info → auto_coder-0.1.355.dist-info}/METADATA +1 -1
  2. {auto_coder-0.1.353.dist-info → auto_coder-0.1.355.dist-info}/RECORD +60 -45
  3. autocoder/agent/agentic_filter.py +1 -1
  4. autocoder/auto_coder.py +8 -0
  5. autocoder/auto_coder_rag.py +37 -1
  6. autocoder/auto_coder_runner.py +58 -77
  7. autocoder/chat/conf_command.py +270 -0
  8. autocoder/chat/models_command.py +485 -0
  9. autocoder/chat_auto_coder.py +29 -24
  10. autocoder/chat_auto_coder_lang.py +26 -2
  11. autocoder/commands/auto_command.py +60 -132
  12. autocoder/commands/auto_web.py +1 -1
  13. autocoder/commands/tools.py +1 -1
  14. autocoder/common/__init__.py +3 -1
  15. autocoder/common/command_completer.py +58 -12
  16. autocoder/common/command_completer_v2.py +576 -0
  17. autocoder/common/conversations/__init__.py +52 -0
  18. autocoder/common/conversations/compatibility.py +303 -0
  19. autocoder/common/conversations/conversation_manager.py +502 -0
  20. autocoder/common/conversations/example.py +152 -0
  21. autocoder/common/file_monitor/__init__.py +5 -0
  22. autocoder/common/file_monitor/monitor.py +383 -0
  23. autocoder/common/global_cancel.py +53 -16
  24. autocoder/common/ignorefiles/__init__.py +4 -0
  25. autocoder/common/ignorefiles/ignore_file_utils.py +103 -0
  26. autocoder/common/ignorefiles/test_ignore_file_utils.py +91 -0
  27. autocoder/common/rulefiles/__init__.py +15 -0
  28. autocoder/common/rulefiles/autocoderrules_utils.py +173 -0
  29. autocoder/common/save_formatted_log.py +54 -0
  30. autocoder/common/v2/agent/agentic_edit.py +10 -39
  31. autocoder/common/v2/agent/agentic_edit_tools/list_files_tool_resolver.py +1 -1
  32. autocoder/common/v2/agent/agentic_edit_tools/search_files_tool_resolver.py +73 -43
  33. autocoder/common/v2/code_agentic_editblock_manager.py +9 -9
  34. autocoder/common/v2/code_diff_manager.py +2 -2
  35. autocoder/common/v2/code_editblock_manager.py +31 -18
  36. autocoder/common/v2/code_strict_diff_manager.py +3 -2
  37. autocoder/dispacher/actions/action.py +6 -6
  38. autocoder/dispacher/actions/plugins/action_regex_project.py +2 -2
  39. autocoder/events/event_manager_singleton.py +1 -1
  40. autocoder/index/index.py +3 -3
  41. autocoder/models.py +22 -9
  42. autocoder/rag/api_server.py +14 -2
  43. autocoder/rag/cache/local_byzer_storage_cache.py +1 -1
  44. autocoder/rag/cache/local_duckdb_storage_cache.py +8 -0
  45. autocoder/rag/cache/simple_cache.py +63 -33
  46. autocoder/rag/loaders/docx_loader.py +1 -1
  47. autocoder/rag/loaders/filter_utils.py +133 -76
  48. autocoder/rag/loaders/image_loader.py +15 -3
  49. autocoder/rag/loaders/pdf_loader.py +2 -2
  50. autocoder/rag/long_context_rag.py +11 -0
  51. autocoder/rag/qa_conversation_strategy.py +5 -31
  52. autocoder/rag/utils.py +21 -2
  53. autocoder/utils/_markitdown.py +66 -25
  54. autocoder/utils/auto_coder_utils/chat_stream_out.py +4 -4
  55. autocoder/utils/thread_utils.py +9 -27
  56. autocoder/version.py +1 -1
  57. {auto_coder-0.1.353.dist-info → auto_coder-0.1.355.dist-info}/LICENSE +0 -0
  58. {auto_coder-0.1.353.dist-info → auto_coder-0.1.355.dist-info}/WHEEL +0 -0
  59. {auto_coder-0.1.353.dist-info → auto_coder-0.1.355.dist-info}/entry_points.txt +0 -0
  60. {auto_coder-0.1.353.dist-info → auto_coder-0.1.355.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,576 @@
1
+ import os
2
+ import shlex
3
+ from typing import Callable, Dict, Any, List, Iterable, Optional
4
+
5
+ import pydantic
6
+ from pydantic import BaseModel, SkipValidation
7
+ from prompt_toolkit.completion import Completer, Completion, CompleteEvent
8
+ from prompt_toolkit.document import Document
9
+
10
+ from autocoder.common import AutoCoderArgs
11
+ from autocoder.common.command_completer import FileSystemModel, MemoryConfig # Reuse models
12
+ from autocoder import models as models_module
13
+
14
+ # Define command structure in a more structured way if needed,
15
+ # but primarily rely on handlers for logic.
16
+ COMMAND_HIERARCHY = {
17
+ "/add_files": {"/group": {"/add", "/drop", "/reset", "/set"}, "/refresh": {}},
18
+ "/remove_files": {"/all"},
19
+ "/conf": {"/drop", "/export", "/import","/get"}, # Added list/get for clarity
20
+ "/coding": {"/apply", "/next"},
21
+ "/chat": {"/new", "/save", "/copy", "/mcp", "/rag", "/review", "/learn", "/no_context"},
22
+ "/mcp": {"/add", "/remove", "/list", "/list_running", "/refresh", "/info"},
23
+ "/lib": {"/add", "/remove", "/list", "/set-proxy", "/refresh", "/get"},
24
+ "/models": {"/chat", "/add", "/add_model", "/remove", "/list", "/speed", "/speed-test", "/input_price", "/output_price", "/activate"},
25
+ "/auto": {},
26
+ "/shell": {"/chat"},
27
+ "/active_context": {"/list", "/run"},
28
+ "/index": {"/query", "/build", "/export", "/import"},
29
+ "/exclude_files": {"/list", "/drop"},
30
+ "/exclude_dirs": {}, # No specific subcommands shown in V1, treat as simple list
31
+ "/commit": {}, # No specific subcommands shown in V1
32
+ "/revert": {},
33
+ "/ask": {},
34
+ "/design": {"/svg", "/sd", "/logo"},
35
+ "/summon": {},
36
+ "/mode": {}, # Simple value completion
37
+ "/voice_input": {},
38
+ "/exit": {},
39
+ "/help": {},
40
+ "/list_files": {},
41
+ "/clear": {},
42
+ "/cls": {},
43
+ "/debug": {},
44
+ }
45
+
46
+ class CommandCompleterV2(Completer):
47
+ """
48
+ A more extensible command completer using a handler-based approach.
49
+ """
50
+ def __init__(self, commands: List[str], file_system_model: FileSystemModel, memory_model: MemoryConfig):
51
+ self.base_commands = commands # Top-level commands starting with /
52
+ self.file_system_model = file_system_model
53
+ self.memory_model = memory_model
54
+
55
+ # Data stores, initialized and refreshable
56
+ self.all_file_names: List[str] = []
57
+ self.all_files: List[str] = []
58
+ self.all_dir_names: List[str] = []
59
+ self.all_files_with_dot: List[str] = []
60
+ self.symbol_list: List[Any] = [] # Use Any for SymbolItem structure from runner
61
+ self.current_file_names: List[str] = []
62
+ self.config_keys = list(AutoCoderArgs.model_fields.keys())
63
+ self.group_names: List[str] = []
64
+ self.lib_names: List[str] = []
65
+ self.model_names: List[str] = [] # Assuming models can be fetched
66
+
67
+ self.refresh_files() # Initial data load
68
+ self._update_dynamic_data() # Load groups, libs etc.
69
+
70
+ # Map command prefixes or patterns to handler methods
71
+ self.command_handlers: Dict[str, Callable] = {
72
+ "/": self._handle_base_command,
73
+ "/add_files": self._handle_add_files,
74
+ "/remove_files": self._handle_remove_files,
75
+ "/exclude_dirs": self._handle_exclude_dirs,
76
+ "/exclude_files": self._handle_exclude_files,
77
+ "/conf": self._handle_conf,
78
+ "/lib": self._handle_lib,
79
+ "/mcp": self._handle_mcp,
80
+ "/models": self._handle_models,
81
+ "/active_context": self._handle_active_context,
82
+ "/mode": self._handle_mode,
83
+ "/chat": self._handle_text_with_symbols,
84
+ "/coding": self._handle_text_with_symbols,
85
+ "/auto": self._handle_text_with_symbols,
86
+ "/ask": self._handle_text_with_symbols, # Treat like chat for @/@@
87
+ "/summon": self._handle_text_with_symbols,
88
+ "/design": self._handle_design,
89
+ # Add handlers for other commands if they need specific logic beyond @/@@
90
+ # Default handler for plain text or commands not explicitly handled
91
+ "default": self._handle_text_with_symbols,
92
+ }
93
+
94
+ def _update_dynamic_data(self):
95
+ """Load or update data that changes during runtime (groups, libs, current files)."""
96
+ self.current_file_names = self.memory_model.get_memory_func().get("current_files", {}).get("files", [])
97
+ self.group_names = list(self.memory_model.get_memory_func().get("current_files", {}).get("groups", {}).keys())
98
+ self.lib_names = list(self.memory_model.get_memory_func().get("libs", {}).keys())
99
+ # In a real scenario, might fetch model names from models_module
100
+ try:
101
+ self.model_names = [m.get("name","") for m in models_module.load_models()]
102
+ except ImportError:
103
+ self.model_names = [] # Fallback if models module not available
104
+
105
+ def refresh_files(self):
106
+ """Refresh file and symbol lists from the file system model."""
107
+ self.all_file_names = self.file_system_model.get_all_file_names_in_project()
108
+ self.all_files = self.file_system_model.get_all_file_in_project()
109
+ self.all_dir_names = self.file_system_model.get_all_dir_names_in_project()
110
+ self.all_files_with_dot = self.file_system_model.get_all_file_in_project_with_dot()
111
+ self.symbol_list = self.file_system_model.get_symbol_list()
112
+ self._update_dynamic_data() # Also refresh dynamic data
113
+
114
+
115
+ # --- Main Completion Logic ---
116
+
117
+ def get_completions(self, document: Document, complete_event: CompleteEvent) -> Iterable[Completion]:
118
+ text = document.text_before_cursor
119
+ word_before_cursor = document.get_word_before_cursor(WORD=True)
120
+
121
+ # Update dynamic data on each completion request
122
+ self._update_dynamic_data()
123
+
124
+ if not text.strip(): # Empty input
125
+ yield from self._handle_base_command(document, complete_event, word_before_cursor, text)
126
+ return
127
+
128
+ parts = text.split(maxsplit=1)
129
+ first_word = parts[0]
130
+
131
+ # 1. Handle Base Command Completion (e.g., typing "/")
132
+ if first_word.startswith("/") and len(parts) == 1 and not text.endswith(" "):
133
+ yield from self._handle_base_command(document, complete_event, word_before_cursor, text)
134
+
135
+ # 2. Dispatch to Specific Command Handlers
136
+ elif first_word in self.command_handlers:
137
+ handler = self.command_handlers[first_word]
138
+ yield from handler(document, complete_event, word_before_cursor, text)
139
+
140
+ # 3. Handle Special Prefixes within general text or unhandled commands
141
+ elif word_before_cursor.startswith("@") and not word_before_cursor.startswith("@@"):
142
+ yield from self._handle_at_completion(document, complete_event, word_before_cursor, text)
143
+ elif word_before_cursor.startswith("@@"):
144
+ yield from self._handle_double_at_completion(document, complete_event, word_before_cursor, text)
145
+ elif word_before_cursor.startswith("<"): # Potential tag completion
146
+ yield from self._handle_img_tag(document, complete_event, word_before_cursor, text)
147
+
148
+ # 4. Default Handler (for plain text or commands without specific handlers)
149
+ else:
150
+ handler = self.command_handlers.get("default")
151
+ if handler:
152
+ yield from handler(document, complete_event, word_before_cursor, text)
153
+
154
+
155
+ # --- Handler Methods ---
156
+
157
+ def _handle_base_command(self, document: Document, complete_event: CompleteEvent, word: str, text: str) -> Iterable[Completion]:
158
+ """Handles completion for top-level commands starting with '/'."""
159
+ command_prefix = text.lstrip() # The word being typed
160
+ for cmd in self.base_commands:
161
+ if cmd.startswith(command_prefix):
162
+ yield Completion(cmd, start_position=-len(command_prefix))
163
+
164
+ def _handle_add_files(self, document: Document, complete_event: CompleteEvent, word: str, text: str) -> Iterable[Completion]:
165
+ """Handles completions for /add_files command."""
166
+ args_text = text[len("/add_files"):].lstrip()
167
+ parts = args_text.split()
168
+ last_part = parts[-1] if parts and not text.endswith(" ") else ""
169
+
170
+ # Sub-command completion
171
+ if not args_text or (len(parts) == 1 and not text.endswith(" ")):
172
+ for sub_cmd in COMMAND_HIERARCHY["/add_files"]:
173
+ if sub_cmd.startswith(last_part):
174
+ yield Completion(sub_cmd, start_position=-len(last_part))
175
+
176
+ # File/Group completion based on context
177
+ if args_text.startswith("/group"):
178
+ group_args_text = args_text[len("/group"):].lstrip()
179
+ group_parts = group_args_text.split()
180
+ group_last_part = group_parts[-1] if group_parts and not text.endswith(" ") else ""
181
+
182
+ # Complete subcommands of /group
183
+ if not group_args_text or (len(group_parts) == 1 and not text.endswith(" ")):
184
+ for group_sub_cmd in COMMAND_HIERARCHY["/add_files"]["/group"]:
185
+ if group_sub_cmd.startswith(group_last_part):
186
+ yield Completion(group_sub_cmd, start_position=-len(group_last_part))
187
+
188
+ # Complete group names for /drop or direct use
189
+ elif group_parts and group_parts[0] in ["/drop", "/set"] or len(group_parts) >= 1 and not group_parts[0].startswith("/"):
190
+ current_word_for_group = group_last_part
191
+ # Handle comma-separated group names
192
+ if "," in current_word_for_group:
193
+ current_word_for_group = current_word_for_group.split(",")[-1]
194
+
195
+ yield from self._complete_items(current_word_for_group, self.group_names)
196
+
197
+ elif args_text.startswith("/refresh"):
198
+ pass # No further completion needed
199
+
200
+ # Default: File path completion
201
+ else:
202
+ yield from self._complete_file_paths(word, text)
203
+
204
+
205
+ def _handle_remove_files(self, document: Document, complete_event: CompleteEvent, word: str, text: str) -> Iterable[Completion]:
206
+ """Handles completions for /remove_files command."""
207
+ # 'word' is document.get_word_before_cursor(WORD=True)
208
+
209
+ # Complete /all subcommand
210
+ if "/all".startswith(word):
211
+ yield Completion("/all", start_position=-len(word))
212
+
213
+ # Complete from current file paths (relative paths)
214
+ relative_current_files = [os.path.relpath(f, self.file_system_model.project_root) for f in self.current_file_names]
215
+ yield from self._complete_items_with_in(word, relative_current_files)
216
+
217
+ # Also complete from just the base filenames
218
+ current_basenames = [os.path.basename(f) for f in self.current_file_names]
219
+ # Avoid duplicates if basename is same as relative path (e.g., top-level file)
220
+ unique_basenames = [b for b in current_basenames if b not in relative_current_files]
221
+ yield from self._complete_items_with_in(word, unique_basenames)
222
+
223
+
224
+ def _handle_exclude_dirs(self, document: Document, complete_event: CompleteEvent, word: str, text: str) -> Iterable[Completion]:
225
+ """Handles completions for /exclude_dirs command."""
226
+ args_text = text[len("/exclude_dirs"):].lstrip()
227
+ current_word = args_text.split(",")[-1].strip()
228
+ yield from self._complete_items(current_word, self.all_dir_names)
229
+
230
+ def _handle_exclude_files(self, document: Document, complete_event: CompleteEvent, word: str, text: str) -> Iterable[Completion]:
231
+ """Handles completions for /exclude_files command."""
232
+ args_text = text[len("/exclude_files"):].lstrip()
233
+ parts = args_text.split()
234
+ last_part = parts[-1] if parts and not text.endswith(" ") else ""
235
+
236
+ if not args_text or (len(parts) == 1 and not text.endswith(" ")):
237
+ for sub_cmd in COMMAND_HIERARCHY["/exclude_files"]:
238
+ if sub_cmd.startswith(last_part):
239
+ yield Completion(sub_cmd, start_position=-len(last_part))
240
+
241
+ elif parts and parts[0] == "/drop":
242
+ current_word = last_part
243
+ yield from self._complete_items(current_word, self.memory_model.get_memory_func().get("exclude_files", []))
244
+ else:
245
+ # Suggest prefix for regex
246
+ if not last_part:
247
+ yield Completion("regex://", start_position=0)
248
+ elif "regex://".startswith(last_part):
249
+ yield Completion("regex://", start_position=-len(last_part))
250
+
251
+
252
+ def _handle_conf(self, document: Document, complete_event: CompleteEvent, word: str, text: str) -> Iterable[Completion]:
253
+ """Handles completions for /conf command."""
254
+ args_text = text[len("/conf"):].lstrip()
255
+ parts = args_text.split()
256
+ last_part = parts[-1] if parts and not text.endswith(" ") else ""
257
+ # Complete subcommands like /drop, /export, /import, /list, /get
258
+ if not args_text or (len(parts) == 1 and not text.endswith(" ") and ":" not in text):
259
+ for sub_cmd in COMMAND_HIERARCHY["/conf"]:
260
+ if sub_cmd.startswith(last_part):
261
+ yield Completion(sub_cmd, start_position=-len(last_part))
262
+ # Also complete config keys directly
263
+ yield from self._complete_config_keys(last_part, add_colon=False)
264
+
265
+ # Complete config keys after /drop or /get
266
+ elif parts and parts[0] in ["/drop", "/get"]:
267
+ yield from self._complete_config_keys(last_part, add_colon=False)
268
+
269
+ # Complete file paths after /export or /import
270
+ elif parts and parts[0] in ["/export", "/import"]:
271
+ yield from self._complete_file_paths(word, text) # Use word here as it's likely the path
272
+
273
+ # Complete config keys for setting (key:value)
274
+ elif ":" not in last_part:
275
+ yield from self._complete_config_keys(last_part, add_colon=True)
276
+
277
+ # Complete values after colon
278
+ elif ":" in args_text:
279
+ key_part = args_text.split(":", 1)[0].strip()
280
+ value_part = args_text.split(":", 1)[1].strip() if ":" in args_text else ""
281
+ yield from self._complete_config_values(key_part, value_part)
282
+ # Example: Complete enum values or suggest file paths for path-like keys
283
+ pass # Placeholder for future value completions
284
+
285
+ def _complete_config_values(self, key: str, value: str) -> Iterable[Completion]:
286
+ """Helper to complete configuration values based on the key."""
287
+ start_pos = -len(value)
288
+
289
+ # Model name completion for keys containing "model"
290
+ if key.endswith("_model") or key == "model":
291
+ # Refresh model names if they can change dynamically
292
+ # self.refresh_model_names()
293
+ for model_name in self.model_names:
294
+ if model_name.startswith(value) or value==":":
295
+ yield Completion(model_name, start_position=start_pos)
296
+ # If a model name matched, we might prioritize these completions.
297
+ # Consider returning here if model names are the only relevant values.
298
+
299
+ # Boolean value completion
300
+ field_info = AutoCoderArgs.model_fields.get(key)
301
+ if field_info and field_info.annotation == bool:
302
+ if "true".startswith(value): yield Completion("true", start_position=start_pos)
303
+ if "false".startswith(value): yield Completion("false", start_position=start_pos)
304
+ # If boolean matched, we might prioritize these completions.
305
+ # Consider returning here if boolean is the only relevant value type.
306
+
307
+ # Add more value completions based on key type or name here
308
+ # e.g., enums, file paths, specific string formats
309
+
310
+
311
+ def _handle_lib(self, document: Document, complete_event: CompleteEvent, word: str, text: str) -> Iterable[Completion]:
312
+ """Handles completions for /lib command."""
313
+ args_text = text[len("/lib"):].lstrip()
314
+ parts = args_text.split()
315
+ last_part = parts[-1] if parts and not text.endswith(" ") else ""
316
+
317
+ # Complete subcommands
318
+ if not args_text or (len(parts) == 1 and not text.endswith(" ")):
319
+ for sub_cmd in COMMAND_HIERARCHY["/lib"]:
320
+ if sub_cmd.startswith(last_part):
321
+ yield Completion(sub_cmd, start_position=-len(last_part))
322
+
323
+ # Complete lib names for add/remove/get
324
+ elif parts and parts[0] in ["/add", "/remove", "/get"]:
325
+ yield from self._complete_items(last_part, self.lib_names)
326
+
327
+ # Complete proxy URL for set-proxy (less specific, maybe suggest http/https?)
328
+ elif parts and parts[0] == "/set-proxy":
329
+ if "http://".startswith(last_part): yield Completion("http://", start_position=-len(last_part))
330
+ if "https://".startswith(last_part): yield Completion("https://", start_position=-len(last_part))
331
+
332
+
333
+ def _handle_mcp(self, document: Document, complete_event: CompleteEvent, word: str, text: str) -> Iterable[Completion]:
334
+ """Handles completions for /mcp command."""
335
+ args_text = text[len("/mcp"):].lstrip()
336
+ parts = args_text.split()
337
+ last_part = parts[-1] if parts and not text.endswith(" ") else ""
338
+
339
+ # Complete subcommands
340
+ if not args_text or (len(parts) == 1 and not text.endswith(" ")):
341
+ for sub_cmd in COMMAND_HIERARCHY["/mcp"]:
342
+ if sub_cmd.startswith(last_part):
343
+ yield Completion(sub_cmd, start_position=-len(last_part))
344
+ # Potentially complete server names after /remove, /refresh, /add if available
345
+
346
+
347
+ def _handle_models(self, document: Document, complete_event: CompleteEvent, word: str, text: str) -> Iterable[Completion]:
348
+ """Handles completions for /models command."""
349
+ args_text = text[len("/models"):].lstrip()
350
+ parts = args_text.split()
351
+ last_part = parts[-1] if parts and not text.endswith(" ") else ""
352
+
353
+ # Complete subcommands
354
+ if not args_text or (len(parts) == 1 and not text.endswith(" ")):
355
+ for sub_cmd in COMMAND_HIERARCHY["/models"]:
356
+ if sub_cmd.startswith(last_part):
357
+ yield Completion(sub_cmd, start_position=-len(last_part))
358
+
359
+ # Complete model names for add/remove/speed/input_price/output_price/activate/chat
360
+ elif parts and parts[0] in ["/add", "/remove", "/speed", "/input_price", "/output_price", "/activate", "/chat"]:
361
+ yield from self._complete_items(last_part, self.model_names)
362
+
363
+ # Complete parameters for /add_model (e.g., name=, base_url=)
364
+ elif parts and parts[0] == "/add_model":
365
+ # Suggest common keys if the last part is empty or partially typed
366
+ common_keys = ["name=", "model_type=", "model_name=", "base_url=", "api_key_path=", "description=", "is_reasoning="]
367
+ yield from self._complete_items(last_part, common_keys)
368
+
369
+ elif parts and parts[0] == "/speed-test":
370
+ if "/long_context".startswith(last_part):
371
+ yield Completion("/long_context", start_position=-len(last_part))
372
+
373
+
374
+ def _handle_active_context(self, document: Document, complete_event: CompleteEvent, word: str, text: str) -> Iterable[Completion]:
375
+ """Handles completions for /active_context command."""
376
+ args_text = text[len("/active_context"):].lstrip()
377
+ parts = args_text.split()
378
+ last_part = parts[-1] if parts and not text.endswith(" ") else ""
379
+
380
+ # Complete subcommands
381
+ if not args_text or (len(parts) == 1 and not text.endswith(" ")):
382
+ for sub_cmd in COMMAND_HIERARCHY["/active_context"]:
383
+ if sub_cmd.startswith(last_part):
384
+ yield Completion(sub_cmd, start_position=-len(last_part))
385
+
386
+ # Complete action file names for /run
387
+ elif parts and parts[0] == "/run":
388
+ # Assuming action files are in 'actions' dir and end with .yml
389
+ action_dir = "actions"
390
+ if os.path.isdir(action_dir):
391
+ try:
392
+ action_files = [f for f in os.listdir(action_dir) if f.endswith(".yml")]
393
+ yield from self._complete_items(last_part, action_files)
394
+ except OSError:
395
+ pass # Ignore if cannot list dir
396
+
397
+
398
+ def _handle_mode(self, document: Document, complete_event: CompleteEvent, word: str, text: str) -> Iterable[Completion]:
399
+ """Handles completions for /mode command."""
400
+ args_text = text[len("/mode"):].lstrip()
401
+ modes = ["normal", "auto_detect", "voice_input"]
402
+ yield from self._complete_items(args_text, modes)
403
+
404
+ def _handle_design(self, document: Document, complete_event: CompleteEvent, word: str, text: str) -> Iterable[Completion]:
405
+ """Handles completions for /design command."""
406
+ args_text = text[len("/design"):].lstrip()
407
+ parts = args_text.split()
408
+ last_part = parts[-1] if parts and not text.endswith(" ") else ""
409
+
410
+ # Complete subcommands
411
+ if not args_text or (len(parts) == 1 and not text.endswith(" ")):
412
+ for sub_cmd in COMMAND_HIERARCHY["/design"]:
413
+ if sub_cmd.startswith(last_part):
414
+ yield Completion(sub_cmd, start_position=-len(last_part))
415
+
416
+ def _handle_text_with_symbols(self, document: Document, complete_event: CompleteEvent, word: str, text: str) -> Iterable[Completion]:
417
+ """Handles general text input, including @, @@, <img> tags and command-specific subcommands."""
418
+ # Check for command-specific subcommands first
419
+ parts = text.split(maxsplit=1)
420
+ command = parts[0]
421
+ if command in COMMAND_HIERARCHY:
422
+ args_text = parts[1] if len(parts) > 1 else ""
423
+ sub_parts = args_text.split()
424
+ last_part = sub_parts[-1] if sub_parts and not text.endswith(" ") else ""
425
+
426
+ # Complete subcommands if applicable
427
+ if not args_text or (len(sub_parts) == 1 and not text.endswith(" ")):
428
+ if isinstance(COMMAND_HIERARCHY[command], dict):
429
+ for sub_cmd in COMMAND_HIERARCHY[command]:
430
+ if sub_cmd.startswith(last_part):
431
+ yield Completion(sub_cmd, start_position=-len(last_part))
432
+
433
+ # Now handle @, @@, <img> regardless of command (or if no command)
434
+ if word.startswith("@") and not word.startswith("@@"):
435
+ yield from self._handle_at_completion(document, complete_event, word, text)
436
+ elif word.startswith("@@"):
437
+ yield from self._handle_double_at_completion(document, complete_event, word, text)
438
+ elif word.startswith("<"): # Potential tag completion
439
+ yield from self._handle_img_tag(document, complete_event, word, text)
440
+
441
+
442
+ # --- Symbol/Tag Handlers ---
443
+ def _handle_at_completion(self, document: Document, complete_event: CompleteEvent, word: str, text: str) -> Iterable[Completion]:
444
+ """Handles completion for single '@' (file paths)."""
445
+ name = word[1:]
446
+ yield from self._complete_file_paths(name, text, is_symbol=True)
447
+
448
+ def _handle_double_at_completion(self, document: Document, complete_event: CompleteEvent, word: str, text: str) -> Iterable[Completion]:
449
+ """Handles completion for double '@@' (symbols)."""
450
+ name = word[2:]
451
+ yield from self._complete_symbols(name)
452
+
453
+ def _handle_img_tag(self, document: Document, complete_event: CompleteEvent, word: str, text: str) -> Iterable[Completion]:
454
+ """Handles completion for <img> tags and paths within them."""
455
+ image_extensions = (
456
+ ".png", ".jpg", ".jpeg", ".gif", ".bmp", ".tiff", ".tif", ".webp",
457
+ ".svg", ".ico", ".heic", ".heif", ".raw", ".cr2", ".nef", ".arw",
458
+ ".dng", ".orf", ".rw2", ".pef", ".srw", ".eps", ".ai", ".psd", ".xcf",
459
+ )
460
+
461
+ # Basic tag completion
462
+ if "<img".startswith(word):
463
+ yield Completion("<img>", start_position=-len(word))
464
+ if "</img".startswith(word):
465
+ yield Completion("</img>", start_position=-len(word))
466
+
467
+ # Path completion inside <img> tag
468
+ # Find the last opening <img> tag that isn't closed yet
469
+ last_open_img = text.rfind("<img>")
470
+ last_close_img = text.rfind("</img>")
471
+
472
+ if last_open_img != -1 and (last_close_img == -1 or last_close_img < last_open_img):
473
+ path_prefix = text[last_open_img + len("<img>"):]
474
+ current_path_word = document.get_word_before_cursor(WORD=True) # Path part being typed
475
+
476
+ # Only complete if cursor is within the tag content
477
+ if document.cursor_position > last_open_img + len("<img>"):
478
+
479
+ search_dir = os.path.dirname(path_prefix) if os.path.dirname(path_prefix) else "."
480
+ file_basename = os.path.basename(current_path_word)
481
+
482
+ try:
483
+ if os.path.isdir(search_dir):
484
+ for item in os.listdir(search_dir):
485
+ full_path = os.path.join(search_dir, item)
486
+ # Suggest directories or image files matching the prefix
487
+ if item.startswith(file_basename):
488
+ if os.path.isdir(full_path):
489
+ relative_path = os.path.relpath(full_path, ".") # Use relative path
490
+ yield Completion(relative_path + os.sep, start_position=-len(current_path_word), display=item + "/")
491
+ elif item.lower().endswith(image_extensions):
492
+ relative_path = os.path.relpath(full_path, ".") # Use relative path
493
+ yield Completion(relative_path, start_position=-len(current_path_word), display=item)
494
+ except OSError:
495
+ pass # Ignore errors listing directories
496
+
497
+
498
+ # --- Helper Methods ---
499
+
500
+ def _complete_items_with_in(self, word: str, items: Iterable[str]) -> Iterable[Completion]:
501
+ """Generic helper to complete a word from a list of items."""
502
+ for item in items:
503
+ if item and word in item:
504
+ yield Completion(item, start_position=-len(word))
505
+
506
+ def _complete_items(self, word: str, items: Iterable[str]) -> Iterable[Completion]:
507
+ """Generic helper to complete a word from a list of items."""
508
+ if word is None:
509
+ word = ""
510
+ for item in items:
511
+ if item and item.startswith(word):
512
+ yield Completion(item, start_position=-len(word))
513
+
514
+ def _complete_config_keys(self, word: str, add_colon: bool = False) -> Iterable[Completion]:
515
+ """Helper to complete configuration keys."""
516
+ suffix = ":" if add_colon else ""
517
+ for key in self.config_keys:
518
+ if key.startswith(word):
519
+ yield Completion(key + suffix, start_position=-len(word))
520
+
521
+ def _complete_file_paths(self, name: str, text: str, is_symbol: bool = False) -> Iterable[Completion]:
522
+ """Helper to complete file paths (@ completion or general path)."""
523
+ if name is None: name = ""
524
+ start_pos = -len(name)
525
+
526
+ # Prioritize active files if triggered by @
527
+ if is_symbol:
528
+ for file_path in self.current_file_names:
529
+ rel_path = os.path.relpath(file_path, self.file_system_model.project_root)
530
+ display_name = self._get_display_path(file_path)
531
+ if name in rel_path or name in os.path.basename(file_path):
532
+ yield Completion(rel_path, start_position=start_pos, display=f"{display_name} (active)")
533
+
534
+ # General file path completion (relative paths with dot)
535
+ if name.startswith("."):
536
+ yield from self._complete_items(name, self.all_files_with_dot)
537
+ return # Don't mix with other completions if starting with .
538
+
539
+ # Complete base file names
540
+ yield from self._complete_items(name, self.all_file_names)
541
+
542
+ # Complete full paths (if name is part of the path)
543
+ for file_path in self.all_files:
544
+ rel_path = os.path.relpath(file_path, self.file_system_model.project_root)
545
+ if name and name in rel_path and file_path not in self.current_file_names: # Avoid duplicates if already shown as active
546
+ display_name = self._get_display_path(file_path)
547
+ yield Completion(rel_path, start_position=start_pos, display=display_name)
548
+
549
+
550
+ def _complete_symbols(self, name: str) -> Iterable[Completion]:
551
+ """Helper to complete symbols (@@ completion)."""
552
+ if name is None: name = ""
553
+ start_pos = -len(name)
554
+ for symbol in self.symbol_list:
555
+ # Assuming symbol has attributes symbol_name, file_name, symbol_type
556
+ if name in symbol.symbol_name:
557
+ file_name = symbol.file_name
558
+ display_name = self._get_display_path(file_name)
559
+ display_text = f"{symbol.symbol_name} ({display_name}/{symbol.symbol_type})"
560
+ completion_text = f"{symbol.symbol_name} ({file_name}/{symbol.symbol_type})"
561
+ yield Completion(completion_text, start_position=start_pos, display=display_text)
562
+
563
+ def _get_display_path(self, file_path: str, max_parts: int = 3) -> str:
564
+ """Helper to create a shorter display path."""
565
+ try:
566
+ # Use relative path for display consistency
567
+ rel_path = os.path.relpath(file_path, self.file_system_model.project_root)
568
+ parts = rel_path.split(os.sep)
569
+ if len(parts) > max_parts:
570
+ return os.path.join("...", *parts[-max_parts:])
571
+ return rel_path
572
+ except ValueError: # Handle cases where paths are not relative (e.g., different drives on Windows)
573
+ parts = file_path.split(os.sep)
574
+ if len(parts) > max_parts:
575
+ return os.path.join("...", *parts[-max_parts:])
576
+ return file_path
@@ -0,0 +1,52 @@
1
+ """
2
+ Conversation management package for AutoCoder.
3
+
4
+ This package provides a unified conversation management system that serves
5
+ different components of AutoCoder, replacing separate implementations in:
6
+ - agentic_edit.py
7
+ - auto_command.py
8
+ - agentic_edit_conversation.py
9
+ """
10
+
11
+ from autocoder.common.conversations.conversation_manager import (
12
+ ConversationManager,
13
+ ConversationType,
14
+ get_conversation_manager,
15
+ Message,
16
+ Conversation
17
+ )
18
+
19
+ from autocoder.common.conversations.compatibility import (
20
+ # Command conversation compatibility
21
+ CommandMessage,
22
+ ExtendedCommandMessage,
23
+ CommandConversation,
24
+ load_command_conversation,
25
+ save_command_conversation,
26
+ save_to_command_memory_file,
27
+
28
+ # Agentic edit conversation compatibility
29
+ get_agentic_conversation,
30
+ AgenticConversationWrapper
31
+ )
32
+
33
+ __all__ = [
34
+ # Main conversation manager
35
+ 'ConversationManager',
36
+ 'ConversationType',
37
+ 'get_conversation_manager',
38
+ 'Message',
39
+ 'Conversation',
40
+
41
+ # Command conversation compatibility
42
+ 'CommandMessage',
43
+ 'ExtendedCommandMessage',
44
+ 'CommandConversation',
45
+ 'load_command_conversation',
46
+ 'save_command_conversation',
47
+ 'save_to_command_memory_file',
48
+
49
+ # Agentic edit conversation compatibility
50
+ 'get_agentic_conversation',
51
+ 'AgenticConversationWrapper'
52
+ ]