Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 23 additions & 24 deletions cecli/tools/context_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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))

Expand Down Expand Up @@ -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()}
Expand All @@ -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."
Expand All @@ -195,18 +203,15 @@ 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"
f"Final output:\n{output}"
)
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:
Expand All @@ -219,27 +224,21 @@ 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:
coder.abs_read_only_fnames.remove(abs_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)}")
Expand All @@ -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"):
Expand All @@ -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:
Expand Down
63 changes: 63 additions & 0 deletions tests/tools/test_context_manager_add_create.py
Original file line number Diff line number Diff line change
@@ -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()
Loading