fix: update file operations for inclusive line range handling

linuztx committed Feb 27, 2026 at 10:06 UTC d45552572c2a10b83e8a06722ebe925bff356494
2 files changed +21 -13
plugins/text_editor/helpers/file_ops.py
+18 -11
@@ -7,7 +7,7 @@ No agent/tool dependencies — only stdlib + tokens helper.
7 import os
8 import shutil
9 import tempfile
10 -from dataclasses import dataclass, field
10 +from dataclasses import dataclass
11 from typing import TYPE_CHECKING
12
13 from python.helpers import plugins, tokens
@@ -60,12 +60,17 @@ class ReadResult:
60 def read_file(
61 path: str,
62 line_from: int = 0,
63 - line_to: int = 0,
63 + line_to: int | None = None,
64 max_line_tokens: int = 500,
65 default_line_count: int = 100,
66 max_total_read_tokens: int = 4000,
67 ) -> ReadResult:
68 - """Read a text file and return numbered lines with token budgeting."""
68 + """
69 + Read a text file and return numbered lines with token budgeting.
70 +
71 + line_from and line_to are both inclusive. None line_to defaults to
72 + line_from + default_line_count - 1.
73 + """
74 path = os.path.expanduser(path)
75
76 if not os.path.isfile(path):
@@ -82,11 +87,12 @@ def read_file(
87
88 total_lines = len(all_lines)
89 line_from = max(line_from, 0)
85 - if not line_to:
86 - line_to = min(line_from + default_line_count, total_lines)
87 - line_to = min(line_to, total_lines)
90 + if line_to is None:
91 + line_to = line_from + default_line_count - 1
92 + line_to = min(line_to, total_lines - 1)
93
89 - selected = all_lines[line_from:line_to]
94 + # Slice is exclusive on the right, so +1
95 + selected = all_lines[line_from:line_to + 1]
96
97 warn_parts: list[str] = []
98 cropped_lines: list[int] = []
@@ -146,8 +152,10 @@ class WriteResult:
152 error: str = ""
153
154
149 -def write_file(path: str, content: str) -> WriteResult:
155 +def write_file(path: str, content: str | None) -> WriteResult:
156 """Create or overwrite a file."""
157 + if content is None:
158 + content = ""
159 path = os.path.expanduser(path)
160 try:
161 os.makedirs(os.path.dirname(path) or ".", exist_ok=True)
@@ -173,7 +181,7 @@ class PatchResult:
181 error: str = ""
182
183
176 -def validate_edits(edits: list) -> tuple[list[dict], str]:
184 +def validate_edits(edits: list | None) -> tuple[list[dict], str]:
185 """
186 Normalise and validate an edits array.
187
@@ -292,7 +300,7 @@ def apply_patch(path: str, edits: list[dict]) -> int:
300 raise
301
302
295 -def patch_file(path: str, edits: list) -> PatchResult:
303 +def patch_file(path: str, edits: list | None) -> PatchResult:
304 """Validate and apply edits to a file."""
305 path = os.path.expanduser(path)
306 if not os.path.isfile(path):
@@ -318,4 +326,3 @@ def _count_content_lines(content: str) -> int:
326 return content.count("\n") + (
327 1 if content and not content.endswith("\n") else 0
328 )
321 -
\ No newline at end of file
plugins/text_editor/tools/text_editor.py
+3 -2
@@ -34,7 +34,8 @@ class TextEditor(Tool):
34
35 cfg = get_config(self.agent)
36 line_from = int(kwargs.get("line_from", 0))
37 - line_to = int(kwargs.get("line_to", 0))
37 + raw_to = kwargs.get("line_to")
38 + line_to = int(raw_to) if raw_to is not None else None
39
40 result = read_file(
41 path,
@@ -66,7 +67,7 @@ class TextEditor(Tool):
67 # ------------------------------------------------------------------
68 # WRITE
69 # ------------------------------------------------------------------
69 - async def _write(self, path: str = "", content: str = "", **kwargs) -> Response:
70 + async def _write(self, path: str = "", content: str | None = "", **kwargs) -> Response:
71 if not path:
72 return self._error("write", path, "path is required")
73