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