Skip to content

Commit 1da6e4f

Browse files
committed
Enhanced code editing with LLM-driven changes and permission management
1 parent 1d5f522 commit 1da6e4f

File tree

2 files changed

+163
-16
lines changed

2 files changed

+163
-16
lines changed

core/repl.py

Lines changed: 104 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -616,42 +616,130 @@ def persist_mode_manager():
616616
persist_mode_manager()
617617
console.print(msg)
618618
continue
619-
elif cmd[0] == "/edit":
619+
elif cmd[0] in ("/fix", "/add", "/remove"):
620620
if not mode_manager.is_build_mode():
621621
console.print("[yellow]Not in build mode. Use /mode build first.[/yellow]")
622622
continue
623-
if len(cmd) < 2:
624-
console.print("Usage: /edit <file>")
623+
if len(cmd) < 3:
624+
console.print(f"Usage: {cmd[0]} <file> <your request>")
625625
continue
626626
file_path = cmd[1]
627+
user_request = " ".join(cmd[2:])
627628
try:
628629
orig = open(file_path, encoding="utf-8").read()
629630
except Exception:
630631
orig = ""
631-
console.print("Enter new content for the file. End input with a single line containing only 'END'.")
632-
lines = []
633-
while True:
634-
l = console.input()
635-
if l.strip() == "END":
636-
break
637-
lines.append(l)
638-
new_content = "\n".join(lines)
639-
change = FileChange(file_path, orig, new_content, "edit")
632+
# --- Real LLM-driven code change ---
633+
# LLM prompt: ask for only the changed code, not markdown or code blocks
634+
llm_prompt = (
635+
f"You are an expert code editor. User request: {user_request}\n"
636+
f"Current file content:\n{orig}\n"
637+
"Respond with the new file content only, as plain text. Do NOT use markdown or code block formatting."
638+
)
639+
try:
640+
from core import model as model_mod
641+
selected_model = load_model_choice() or "deepseek-r1:latest"
642+
new_content = model_mod.query_ollama(llm_prompt, selected_model)
643+
# Remove accidental code block markers if present
644+
if new_content.strip().startswith("```"):
645+
new_content = new_content.strip().lstrip("`python").strip("`").strip()
646+
except Exception as e:
647+
console.print(f"[red]LLM error: {e}. Using fallback stub.[/red]")
648+
if cmd[0] == "/fix":
649+
new_content = orig + f"\n# FIX REQUEST: {user_request}\n"
650+
elif cmd[0] == "/add":
651+
new_content = orig + f"\n# ADD REQUEST: {user_request}\n"
652+
elif cmd[0] == "/remove":
653+
new_content = orig + f"\n# REMOVE REQUEST: {user_request}\n"
654+
change = FileChange(file_path, orig, new_content, cmd[0][1:])
640655
mode_manager.add_pending_change(change)
656+
# Enhanced diff preview with rich
657+
from rich.syntax import Syntax
658+
from rich.panel import Panel
641659
diff = change.get_diff()
642-
console.print(mode_manager.request_permission_message(file_path, "edit", diff))
643-
resp = console.input("Your response: ")
644-
allowed, msg = mode_manager.handle_permission_response(resp, file_path)
660+
syntax = Syntax(diff, "diff", theme="monokai", line_numbers=False, word_wrap=True)
661+
console.print(Panel(syntax, title="Diff Preview", border_style="cyan"))
662+
# --- Inline terminal permission menu ---
663+
options = [
664+
("accept once", "Accept once"),
665+
("accept all", "Accept all for this file"),
666+
("accept global", "Accept all for all files"),
667+
("reject", "Reject this change"),
668+
("show full", "Show full diff")
669+
]
670+
selected = 0
671+
while True:
672+
console.print("\n[bold cyan]BUILD MODE: Permission Required[/bold cyan]")
673+
console.print(f"File: [bold]{file_path}[/bold] Operation: [bold]{cmd[0][1:]}[/bold]")
674+
for idx, (val, label) in enumerate(options):
675+
style = "bold cyan" if idx == selected else ""
676+
prefix = "→ " if idx == selected else " "
677+
console.print(f"{prefix}[{val}] ", style=style, end="")
678+
console.print(label, style=style)
679+
console.print("\nUse [bold]up/down[/bold] arrows then [bold]Enter[/bold] to select.")
680+
key = console.input("Select option (u/d/Enter): ").strip().lower()
681+
if key in ("u", "up") and selected > 0:
682+
selected -= 1
683+
elif key in ("d", "down") and selected < len(options) - 1:
684+
selected += 1
685+
elif key == "" or key == "enter":
686+
break
687+
# Hide permission block by clearing screen section
688+
console.clear() # Optionally, use console.clear() or print blank lines
689+
result = options[selected][0]
690+
allowed, msg = mode_manager.handle_permission_response(result, file_path)
691+
# --- Logging/audit trail ---
692+
import datetime
693+
with open(os.path.join(SESSION_DIR, "edit_audit.log"), "a") as logf:
694+
logf.write(f"[{datetime.datetime.now()}] {cmd[0][1:].upper()} {file_path} | {user_request} | {result} | {msg}\n")
645695
console.print(msg)
696+
# Autonomous file/folder creation for test tasks
697+
import re
698+
def is_test_task(request):
699+
return bool(re.search(r"unit test|test case|write test|add test", request, re.I))
700+
def get_test_file_path(src_path):
701+
p = Path(src_path)
702+
if p.name.startswith("test_"):
703+
return str(p)
704+
return str(p.parent / ("test_" + p.name))
646705
if allowed:
647-
if mode_manager.apply_change(change):
706+
# If the user request is for a test, create a test file/folder as needed
707+
if is_test_task(user_request):
708+
test_file = get_test_file_path(file_path)
709+
test_dir = os.path.dirname(test_file)
710+
if not os.path.exists(test_dir):
711+
os.makedirs(test_dir, exist_ok=True)
712+
with open(test_file, "w", encoding="utf-8") as f:
713+
f.write(new_content)
714+
undo_stack.append(change)
715+
persist_mode_manager()
716+
console.print(f"[green]New test file created: {test_file}.[/green]")
717+
with open(os.path.join(SESSION_DIR, "edit_audit.log"), "a") as logf:
718+
logf.write(f"[{datetime.datetime.now()}] CREATED {test_file}\n")
719+
elif not os.path.exists(file_path):
720+
with open(file_path, "w", encoding="utf-8") as f:
721+
f.write(new_content)
722+
undo_stack.append(change)
723+
persist_mode_manager()
724+
console.print(f"[green]New file created: {file_path}.[/green]")
725+
with open(os.path.join(SESSION_DIR, "edit_audit.log"), "a") as logf:
726+
logf.write(f"[{datetime.datetime.now()}] CREATED {file_path}\n")
727+
elif mode_manager.apply_change(change):
648728
undo_stack.append(change)
649729
persist_mode_manager()
650730
console.print(f"[green]Change applied to {file_path}.[/green]")
731+
with open(os.path.join(SESSION_DIR, "edit_audit.log"), "a") as logf:
732+
logf.write(f"[{datetime.datetime.now()}] APPLIED {file_path}\n")
651733
else:
652734
console.print("[red]Permission denied or error applying change.[/red]")
653735
else:
654736
console.print("[yellow]Change not applied.[/yellow]")
737+
# LLM clarification: if the LLM output contains a special marker, prompt user for more info
738+
if "[NEED_USER_INPUT]" in new_content:
739+
clarification = console.input("[bold yellow]LLM needs more info: Please clarify your request: [/bold yellow]")
740+
# Re-run the command with the clarification
741+
cmd.append(clarification)
742+
continue
655743
continue
656744
elif cmd[0] == "/review_changes":
657745
# List all pending changes

tests/test_mode_manager_llm.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import os
2+
import tempfile
3+
import shutil
4+
import pytest
5+
from core.mode_manager import ModeManager, FileChange, PermissionLevel
6+
from unittest.mock import patch
7+
8+
def test_llm_edit_and_logging(tmp_path):
9+
mm = ModeManager()
10+
mm.set_mode("build")
11+
file_path = tmp_path / "llm_test.py"
12+
file_path.write_text("print('hello')\n")
13+
orig = file_path.read_text()
14+
# Simulate LLM edit
15+
new_content = orig + "# LLM EDIT\n"
16+
change = FileChange(str(file_path), orig, new_content, "fix")
17+
mm.add_pending_change(change)
18+
# Patch can_edit_file to always allow
19+
mm.permissions[str(file_path)] = PermissionLevel.ALL
20+
mm.apply_change(change)
21+
# Check audit log
22+
log_path = os.path.join(os.path.dirname(str(file_path)), "../sessions/edit_audit.log")
23+
# Not guaranteed to exist in test, but check that no error is raised
24+
assert change.applied
25+
26+
def test_enhanced_diff():
27+
orig = "a = 1\nb = 2\n"
28+
new = "a = 1\nb = 3\n"
29+
change = FileChange("dummy.py", orig, new, "fix")
30+
diff = change.get_diff()
31+
assert "-b = 2" in diff and "+b = 3" in diff
32+
33+
def test_permission_policies(tmp_path):
34+
mm = ModeManager()
35+
mm.set_mode("build")
36+
file_path = tmp_path / "policy.py"
37+
file_path.write_text("x = 1\n")
38+
change = FileChange(str(file_path), "x = 1\n", "x = 2\n", "fix")
39+
mm.add_pending_change(change)
40+
# Simulate user policy: always allow for .py files
41+
mm.permissions[str(file_path)] = PermissionLevel.ALL
42+
assert mm.can_edit_file(str(file_path))[0]
43+
assert mm.apply_change(change)
44+
assert file_path.read_text() == "x = 2\n"
45+
46+
def test_directory_pattern_permissions(tmp_path):
47+
mm = ModeManager()
48+
mm.set_mode("build")
49+
subdir = tmp_path / "subdir"
50+
subdir.mkdir()
51+
file_path = subdir / "foo.py"
52+
file_path.write_text("y = 1\n")
53+
change = FileChange(str(file_path), "y = 1\n", "y = 2\n", "fix")
54+
mm.add_pending_change(change)
55+
# Simulate directory-level permission
56+
mm.permissions[str(subdir)] = PermissionLevel.ALL
57+
# Directory-level permission logic (to be implemented in ModeManager for real)
58+
allowed = any(str(file_path).startswith(str(p)) and perm == PermissionLevel.ALL for p, perm in mm.permissions.items())
59+
assert allowed

0 commit comments

Comments
 (0)