diff --git a/cecli/tools/context_manager.py b/cecli/tools/context_manager.py index c5883f97e16..447dc90a6c1 100644 --- a/cecli/tools/context_manager.py +++ b/cecli/tools/context_manager.py @@ -94,7 +94,7 @@ def execute( "You must specify at least one of: remove, editable, view, create, or stop" ) - coder.io.tool_output("⛭ Modifying Context", type="tool-result") + coder.io.tool_output("⚙️ Modifying Context.") messages = [] for f in create_files: @@ -104,7 +104,15 @@ def execute( for f in view_files: messages.append(cls._view(coder, f)) for f in editable_files: - messages.append(cls._editable(coder, f)) + try: + abs_path = coder.abs_root_path(f) + except Exception: + abs_path = None + if abs_path is not None and not os.path.isfile(abs_path): + coder.io.tool_output(f"ℹ️ `{f}` missing on disk — using **create** instead of add") + messages.append(cls._create(coder, f)) + else: + messages.append(cls._editable(coder, f)) for key in stop_keys: messages.append(cls._stop_command(coder, key)) @@ -169,7 +177,7 @@ def _remove(cls, coder, file_path): removed = True if not removed: - coder.io.tool_output(f"⚠ File '{file_path}' not in context", type="tool-result") + coder.io.tool_output(f"⚠️ File '{file_path}' not in context") return f"File not in context: {file_path}" coder.recently_removed[rel_path] = {"removed_at": time.time()} @@ -178,7 +186,7 @@ def _remove(cls, coder, file_path): ConversationService.get_chunks(coder).defer_removal(abs_path) ConversationService.get_chunks(coder).defer_removal(rel_path) - coder.io.tool_output(f"✗ Removed '{file_path}' from context", type="tool-result") + coder.io.tool_output(f"🗑️ Removed '{file_path}' from context") return ( f"Removed: {file_path}\n" "Old file contents may remain visible. This is an acceptable system behavior." @@ -195,9 +203,7 @@ def _stop_command(cls, coder, command_key): command_key ) if success: - coder.io.tool_output( - f"✗ Stopped background command '{command_key}'", type="tool-result" - ) + coder.io.tool_output(f"🛑 Stopped background command '{command_key}'") return ( f"Background command stopped: {command_key}\n" f"Exit code: {exit_code}\n" @@ -205,8 +211,7 @@ def _stop_command(cls, coder, command_key): ) else: coder.io.tool_output( - f"⚠ Background command '{command_key}' not found or not running", - type="tool-result", + f"⚠️ Background command '{command_key}' not found or not running" ) return f"Command not found or not running: {command_key}" except Exception as e: @@ -219,12 +224,10 @@ def _editable(cls, coder, file_path): try: abs_path = cls._resolve_file_path(coder, file_path) if abs_path in coder.abs_fnames: - coder.io.tool_output( - f"🗀 File '{file_path}' is already editable", type="tool-result" - ) + coder.io.tool_output(f"📝 File '{file_path}' is already editable") return f"Already editable: {file_path}" if not os.path.isfile(abs_path): - coder.io.tool_output(f"⚠ File '{file_path}' not found on disk", type="tool-result") + coder.io.tool_output(f"⚠️ File '{file_path}' not found on disk") return f"File not found: {file_path}" was_read_only = False if abs_path in coder.abs_read_only_fnames: @@ -232,14 +235,10 @@ def _editable(cls, coder, file_path): was_read_only = True coder.abs_fnames.add(abs_path) if was_read_only: - coder.io.tool_output( - f"🗀 Moved '{file_path}' from read-only to editable", type="tool-result" - ) + coder.io.tool_output(f"📝 Moved '{file_path}' from read-only to editable") return f"Made editable (moved): {file_path}" else: - coder.io.tool_output( - f"🗀 Added '{file_path}' directly to editable context", type="tool-result" - ) + coder.io.tool_output(f"📝 Added '{file_path}' directly to editable context") return f"Made editable (added): {file_path}" except Exception as e: coder.io.tool_error(f"Error making editable '{file_path}': {str(e)}") @@ -263,11 +262,13 @@ def _create(cls, coder, file_path): # Check if file already exists if os.path.exists(abs_path): - coder.io.tool_output(f"⚠ File '{file_path}' already exists", type="tool-result") + coder.io.tool_output(f"⚠️ File '{file_path}' already exists") return f"File already exists: {file_path}" # Create parent directories if they don't exist - os.makedirs(os.path.dirname(abs_path), exist_ok=True) + parent = os.path.dirname(abs_path) + if parent: + os.makedirs(parent, exist_ok=True) # Create an empty file with open(abs_path, "w", encoding="utf-8"): @@ -276,9 +277,7 @@ def _create(cls, coder, file_path): # Add the file to editable context coder.abs_fnames.add(abs_path) - coder.io.tool_output( - f"🗀 Created '{file_path}' and made it editable", type="tool-result" - ) + coder.io.tool_output(f"📝 Created '{file_path}' and made it editable") return f"Created and made editable: {file_path}" except Exception as e: diff --git a/tests/tools/test_context_manager_add_create.py b/tests/tools/test_context_manager_add_create.py new file mode 100644 index 00000000000..d2b3bb290a0 --- /dev/null +++ b/tests/tools/test_context_manager_add_create.py @@ -0,0 +1,63 @@ +"""ContextManager add on missing paths upgrades to create.""" + +from __future__ import annotations + +import tempfile +import unittest +from pathlib import Path +from types import SimpleNamespace +from unittest.mock import Mock + +from cecli.tools.context_manager import Tool + + +class _CoderStub: + def __init__(self, root: Path): + self.root = str(root) + self.repo = SimpleNamespace(root=str(root)) + self.io = SimpleNamespace( + tool_output=Mock(), + tool_error=Mock(), + tool_warning=Mock(), + ) + self.abs_fnames: set[str] = set() + self.abs_read_only_fnames: set[str] = set() + self.tui = lambda: False + + def abs_root_path(self, file_path: str) -> str: + path = Path(file_path) + if path.is_absolute(): + return str(path) + return str((Path(self.root) / path).resolve()) + + +class TestContextManagerAddCreate(unittest.TestCase): + def test_add_missing_file_creates_on_disk(self): + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + coder = _CoderStub(root) + rel = "src/new_module.py" + + result = Tool.execute(coder, add=[rel]) + + abs_path = coder.abs_root_path(rel) + self.assertTrue(Path(abs_path).is_file()) + self.assertIn(abs_path, coder.abs_fnames) + self.assertIn("create", result.lower()) + coder.io.tool_output.assert_any_call( + "ℹ️ `src/new_module.py` missing on disk — using **create** instead of add" + ) + + def test_create_root_level_file_without_makedirs_error(self): + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + coder = _CoderStub(root) + + result = Tool.execute(coder, create=["README.md"]) + + self.assertTrue((root / "README.md").is_file()) + self.assertIn("Created", result) + + +if __name__ == "__main__": + unittest.main()