tunacode-cli 0.1.21__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of tunacode-cli might be problematic. Click here for more details.
- tunacode/__init__.py +0 -0
- tunacode/cli/textual_repl.tcss +283 -0
- tunacode/configuration/__init__.py +1 -0
- tunacode/configuration/defaults.py +45 -0
- tunacode/configuration/models.py +147 -0
- tunacode/configuration/models_registry.json +1 -0
- tunacode/configuration/pricing.py +74 -0
- tunacode/configuration/settings.py +35 -0
- tunacode/constants.py +227 -0
- tunacode/core/__init__.py +6 -0
- tunacode/core/agents/__init__.py +39 -0
- tunacode/core/agents/agent_components/__init__.py +48 -0
- tunacode/core/agents/agent_components/agent_config.py +441 -0
- tunacode/core/agents/agent_components/agent_helpers.py +290 -0
- tunacode/core/agents/agent_components/message_handler.py +99 -0
- tunacode/core/agents/agent_components/node_processor.py +477 -0
- tunacode/core/agents/agent_components/response_state.py +129 -0
- tunacode/core/agents/agent_components/result_wrapper.py +51 -0
- tunacode/core/agents/agent_components/state_transition.py +112 -0
- tunacode/core/agents/agent_components/streaming.py +271 -0
- tunacode/core/agents/agent_components/task_completion.py +40 -0
- tunacode/core/agents/agent_components/tool_buffer.py +44 -0
- tunacode/core/agents/agent_components/tool_executor.py +101 -0
- tunacode/core/agents/agent_components/truncation_checker.py +37 -0
- tunacode/core/agents/delegation_tools.py +109 -0
- tunacode/core/agents/main.py +545 -0
- tunacode/core/agents/prompts.py +66 -0
- tunacode/core/agents/research_agent.py +231 -0
- tunacode/core/compaction.py +218 -0
- tunacode/core/prompting/__init__.py +27 -0
- tunacode/core/prompting/loader.py +66 -0
- tunacode/core/prompting/prompting_engine.py +98 -0
- tunacode/core/prompting/sections.py +50 -0
- tunacode/core/prompting/templates.py +69 -0
- tunacode/core/state.py +409 -0
- tunacode/exceptions.py +313 -0
- tunacode/indexing/__init__.py +5 -0
- tunacode/indexing/code_index.py +432 -0
- tunacode/indexing/constants.py +86 -0
- tunacode/lsp/__init__.py +112 -0
- tunacode/lsp/client.py +351 -0
- tunacode/lsp/diagnostics.py +19 -0
- tunacode/lsp/servers.py +101 -0
- tunacode/prompts/default_prompt.md +952 -0
- tunacode/prompts/research/sections/agent_role.xml +5 -0
- tunacode/prompts/research/sections/constraints.xml +14 -0
- tunacode/prompts/research/sections/output_format.xml +57 -0
- tunacode/prompts/research/sections/tool_use.xml +23 -0
- tunacode/prompts/sections/advanced_patterns.xml +255 -0
- tunacode/prompts/sections/agent_role.xml +8 -0
- tunacode/prompts/sections/completion.xml +10 -0
- tunacode/prompts/sections/critical_rules.xml +37 -0
- tunacode/prompts/sections/examples.xml +220 -0
- tunacode/prompts/sections/output_style.xml +94 -0
- tunacode/prompts/sections/parallel_exec.xml +105 -0
- tunacode/prompts/sections/search_pattern.xml +100 -0
- tunacode/prompts/sections/system_info.xml +6 -0
- tunacode/prompts/sections/tool_use.xml +84 -0
- tunacode/prompts/sections/user_instructions.xml +3 -0
- tunacode/py.typed +0 -0
- tunacode/templates/__init__.py +5 -0
- tunacode/templates/loader.py +15 -0
- tunacode/tools/__init__.py +10 -0
- tunacode/tools/authorization/__init__.py +29 -0
- tunacode/tools/authorization/context.py +32 -0
- tunacode/tools/authorization/factory.py +20 -0
- tunacode/tools/authorization/handler.py +58 -0
- tunacode/tools/authorization/notifier.py +35 -0
- tunacode/tools/authorization/policy.py +19 -0
- tunacode/tools/authorization/requests.py +119 -0
- tunacode/tools/authorization/rules.py +72 -0
- tunacode/tools/bash.py +222 -0
- tunacode/tools/decorators.py +213 -0
- tunacode/tools/glob.py +353 -0
- tunacode/tools/grep.py +468 -0
- tunacode/tools/grep_components/__init__.py +9 -0
- tunacode/tools/grep_components/file_filter.py +93 -0
- tunacode/tools/grep_components/pattern_matcher.py +158 -0
- tunacode/tools/grep_components/result_formatter.py +87 -0
- tunacode/tools/grep_components/search_result.py +34 -0
- tunacode/tools/list_dir.py +205 -0
- tunacode/tools/prompts/bash_prompt.xml +10 -0
- tunacode/tools/prompts/glob_prompt.xml +7 -0
- tunacode/tools/prompts/grep_prompt.xml +10 -0
- tunacode/tools/prompts/list_dir_prompt.xml +7 -0
- tunacode/tools/prompts/read_file_prompt.xml +9 -0
- tunacode/tools/prompts/todoclear_prompt.xml +12 -0
- tunacode/tools/prompts/todoread_prompt.xml +16 -0
- tunacode/tools/prompts/todowrite_prompt.xml +28 -0
- tunacode/tools/prompts/update_file_prompt.xml +9 -0
- tunacode/tools/prompts/web_fetch_prompt.xml +11 -0
- tunacode/tools/prompts/write_file_prompt.xml +7 -0
- tunacode/tools/react.py +111 -0
- tunacode/tools/read_file.py +68 -0
- tunacode/tools/todo.py +222 -0
- tunacode/tools/update_file.py +62 -0
- tunacode/tools/utils/__init__.py +1 -0
- tunacode/tools/utils/ripgrep.py +311 -0
- tunacode/tools/utils/text_match.py +352 -0
- tunacode/tools/web_fetch.py +245 -0
- tunacode/tools/write_file.py +34 -0
- tunacode/tools/xml_helper.py +34 -0
- tunacode/types/__init__.py +166 -0
- tunacode/types/base.py +94 -0
- tunacode/types/callbacks.py +53 -0
- tunacode/types/dataclasses.py +121 -0
- tunacode/types/pydantic_ai.py +31 -0
- tunacode/types/state.py +122 -0
- tunacode/ui/__init__.py +6 -0
- tunacode/ui/app.py +542 -0
- tunacode/ui/commands/__init__.py +430 -0
- tunacode/ui/components/__init__.py +1 -0
- tunacode/ui/headless/__init__.py +5 -0
- tunacode/ui/headless/output.py +72 -0
- tunacode/ui/main.py +252 -0
- tunacode/ui/renderers/__init__.py +41 -0
- tunacode/ui/renderers/errors.py +197 -0
- tunacode/ui/renderers/panels.py +550 -0
- tunacode/ui/renderers/search.py +314 -0
- tunacode/ui/renderers/tools/__init__.py +21 -0
- tunacode/ui/renderers/tools/bash.py +247 -0
- tunacode/ui/renderers/tools/diagnostics.py +186 -0
- tunacode/ui/renderers/tools/glob.py +226 -0
- tunacode/ui/renderers/tools/grep.py +228 -0
- tunacode/ui/renderers/tools/list_dir.py +198 -0
- tunacode/ui/renderers/tools/read_file.py +226 -0
- tunacode/ui/renderers/tools/research.py +294 -0
- tunacode/ui/renderers/tools/update_file.py +237 -0
- tunacode/ui/renderers/tools/web_fetch.py +182 -0
- tunacode/ui/repl_support.py +226 -0
- tunacode/ui/screens/__init__.py +16 -0
- tunacode/ui/screens/model_picker.py +303 -0
- tunacode/ui/screens/session_picker.py +181 -0
- tunacode/ui/screens/setup.py +218 -0
- tunacode/ui/screens/theme_picker.py +90 -0
- tunacode/ui/screens/update_confirm.py +69 -0
- tunacode/ui/shell_runner.py +129 -0
- tunacode/ui/styles/layout.tcss +98 -0
- tunacode/ui/styles/modals.tcss +38 -0
- tunacode/ui/styles/panels.tcss +81 -0
- tunacode/ui/styles/theme-nextstep.tcss +303 -0
- tunacode/ui/styles/widgets.tcss +33 -0
- tunacode/ui/styles.py +18 -0
- tunacode/ui/widgets/__init__.py +23 -0
- tunacode/ui/widgets/command_autocomplete.py +62 -0
- tunacode/ui/widgets/editor.py +402 -0
- tunacode/ui/widgets/file_autocomplete.py +47 -0
- tunacode/ui/widgets/messages.py +46 -0
- tunacode/ui/widgets/resource_bar.py +182 -0
- tunacode/ui/widgets/status_bar.py +98 -0
- tunacode/utils/__init__.py +0 -0
- tunacode/utils/config/__init__.py +13 -0
- tunacode/utils/config/user_configuration.py +91 -0
- tunacode/utils/messaging/__init__.py +10 -0
- tunacode/utils/messaging/message_utils.py +34 -0
- tunacode/utils/messaging/token_counter.py +77 -0
- tunacode/utils/parsing/__init__.py +13 -0
- tunacode/utils/parsing/command_parser.py +55 -0
- tunacode/utils/parsing/json_utils.py +188 -0
- tunacode/utils/parsing/retry.py +146 -0
- tunacode/utils/parsing/tool_parser.py +267 -0
- tunacode/utils/security/__init__.py +15 -0
- tunacode/utils/security/command.py +106 -0
- tunacode/utils/system/__init__.py +25 -0
- tunacode/utils/system/gitignore.py +155 -0
- tunacode/utils/system/paths.py +190 -0
- tunacode/utils/ui/__init__.py +9 -0
- tunacode/utils/ui/file_filter.py +135 -0
- tunacode/utils/ui/helpers.py +24 -0
- tunacode_cli-0.1.21.dist-info/METADATA +170 -0
- tunacode_cli-0.1.21.dist-info/RECORD +174 -0
- tunacode_cli-0.1.21.dist-info/WHEEL +4 -0
- tunacode_cli-0.1.21.dist-info/entry_points.txt +2 -0
- tunacode_cli-0.1.21.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"""NeXTSTEP-style panel renderer for list_dir tool output."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from rich.console import Group, RenderableType
|
|
11
|
+
from rich.panel import Panel
|
|
12
|
+
from rich.style import Style
|
|
13
|
+
from rich.text import Text
|
|
14
|
+
|
|
15
|
+
from tunacode.constants import (
|
|
16
|
+
MAX_PANEL_LINE_WIDTH,
|
|
17
|
+
MIN_VIEWPORT_LINES,
|
|
18
|
+
TOOL_PANEL_WIDTH,
|
|
19
|
+
TOOL_VIEWPORT_LINES,
|
|
20
|
+
UI_COLORS,
|
|
21
|
+
)
|
|
22
|
+
from tunacode.tools.list_dir import IGNORE_PATTERNS_COUNT
|
|
23
|
+
|
|
24
|
+
BOX_HORIZONTAL = "─"
|
|
25
|
+
SEPARATOR_WIDTH = 52
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class ListDirData:
|
|
30
|
+
"""Parsed list_dir result for structured display."""
|
|
31
|
+
|
|
32
|
+
directory: str
|
|
33
|
+
tree_content: str
|
|
34
|
+
file_count: int
|
|
35
|
+
dir_count: int
|
|
36
|
+
is_truncated: bool
|
|
37
|
+
max_files: int
|
|
38
|
+
show_hidden: bool
|
|
39
|
+
ignore_count: int
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def parse_result(args: dict[str, Any] | None, result: str) -> ListDirData | None:
|
|
43
|
+
"""Extract structured data from list_dir output.
|
|
44
|
+
|
|
45
|
+
New format:
|
|
46
|
+
45 files 12 dirs
|
|
47
|
+
dirname/
|
|
48
|
+
├── subdir/
|
|
49
|
+
│ └── file.txt
|
|
50
|
+
└── other.txt
|
|
51
|
+
"""
|
|
52
|
+
if not result:
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
lines = result.strip().splitlines()
|
|
56
|
+
if len(lines) < 2:
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
# First line is summary: "45 files 12 dirs" or "0 files 0 dirs"
|
|
60
|
+
summary_line = lines[0]
|
|
61
|
+
summary_match = re.match(r"(\d+)\s+files\s+(\d+)\s+dirs(?:\s+\(truncated\))?", summary_line)
|
|
62
|
+
|
|
63
|
+
if not summary_match:
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
file_count = int(summary_match.group(1))
|
|
67
|
+
dir_count = int(summary_match.group(2))
|
|
68
|
+
is_truncated = "(truncated)" in summary_line
|
|
69
|
+
|
|
70
|
+
# Second line is directory name
|
|
71
|
+
directory = lines[1].rstrip("/")
|
|
72
|
+
|
|
73
|
+
# Rest is tree content
|
|
74
|
+
tree_content = "\n".join(lines[1:])
|
|
75
|
+
|
|
76
|
+
args = args or {}
|
|
77
|
+
max_files = args.get("max_files", 100)
|
|
78
|
+
show_hidden = args.get("show_hidden", False)
|
|
79
|
+
ignore_list = args.get("ignore", [])
|
|
80
|
+
ignore_count = (
|
|
81
|
+
IGNORE_PATTERNS_COUNT + len(ignore_list) if ignore_list else IGNORE_PATTERNS_COUNT
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
return ListDirData(
|
|
85
|
+
directory=directory,
|
|
86
|
+
tree_content=tree_content,
|
|
87
|
+
file_count=file_count,
|
|
88
|
+
dir_count=dir_count,
|
|
89
|
+
is_truncated=is_truncated,
|
|
90
|
+
max_files=max_files,
|
|
91
|
+
show_hidden=show_hidden,
|
|
92
|
+
ignore_count=ignore_count,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _truncate_line(line: str) -> str:
|
|
97
|
+
"""Truncate a single line if too wide."""
|
|
98
|
+
if len(line) > MAX_PANEL_LINE_WIDTH:
|
|
99
|
+
return line[: MAX_PANEL_LINE_WIDTH - 3] + "..."
|
|
100
|
+
return line
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _truncate_tree(content: str) -> tuple[str, int, int]:
|
|
104
|
+
"""Truncate tree content, return (truncated, shown, total)."""
|
|
105
|
+
lines = content.splitlines()
|
|
106
|
+
total = len(lines)
|
|
107
|
+
|
|
108
|
+
if total <= TOOL_VIEWPORT_LINES:
|
|
109
|
+
return "\n".join(_truncate_line(ln) for ln in lines), total, total
|
|
110
|
+
|
|
111
|
+
truncated = [_truncate_line(ln) for ln in lines[:TOOL_VIEWPORT_LINES]]
|
|
112
|
+
return "\n".join(truncated), TOOL_VIEWPORT_LINES, total
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def render_list_dir(
|
|
116
|
+
args: dict[str, Any] | None,
|
|
117
|
+
result: str,
|
|
118
|
+
duration_ms: float | None = None,
|
|
119
|
+
) -> RenderableType | None:
|
|
120
|
+
"""Render list_dir with NeXTSTEP zoned layout.
|
|
121
|
+
|
|
122
|
+
Zones:
|
|
123
|
+
- Header: directory path + summary counts
|
|
124
|
+
- Selection context: hidden/max/ignore parameters
|
|
125
|
+
- Primary viewport: tree content with connectors
|
|
126
|
+
- Status: truncation info, duration
|
|
127
|
+
"""
|
|
128
|
+
data = parse_result(args, result)
|
|
129
|
+
if not data:
|
|
130
|
+
return None
|
|
131
|
+
|
|
132
|
+
# Zone 1: Directory + counts
|
|
133
|
+
header = Text()
|
|
134
|
+
header.append(data.directory, style="bold")
|
|
135
|
+
header.append(f" {data.file_count} files {data.dir_count} dirs", style="dim")
|
|
136
|
+
|
|
137
|
+
# Zone 2: Parameters
|
|
138
|
+
hidden_val = "on" if data.show_hidden else "off"
|
|
139
|
+
params = Text()
|
|
140
|
+
params.append("hidden:", style="dim")
|
|
141
|
+
params.append(f" {hidden_val}", style="dim bold")
|
|
142
|
+
params.append(" max:", style="dim")
|
|
143
|
+
params.append(f" {data.max_files}", style="dim bold")
|
|
144
|
+
params.append(" ignore:", style="dim")
|
|
145
|
+
params.append(f" {data.ignore_count}", style="dim bold")
|
|
146
|
+
|
|
147
|
+
separator = Text(BOX_HORIZONTAL * SEPARATOR_WIDTH, style="dim")
|
|
148
|
+
|
|
149
|
+
# Zone 3: Tree viewport (skip first line which is dirname)
|
|
150
|
+
tree_lines = data.tree_content.splitlines()[1:] # skip dirname, already in header
|
|
151
|
+
tree_only = "\n".join(tree_lines) if tree_lines else "(empty)"
|
|
152
|
+
truncated_tree, shown, total = _truncate_tree(tree_only)
|
|
153
|
+
|
|
154
|
+
# Pad viewport to minimum height for visual consistency
|
|
155
|
+
tree_line_list = truncated_tree.split("\n")
|
|
156
|
+
while len(tree_line_list) < MIN_VIEWPORT_LINES:
|
|
157
|
+
tree_line_list.append("")
|
|
158
|
+
truncated_tree = "\n".join(tree_line_list)
|
|
159
|
+
|
|
160
|
+
viewport = Text(truncated_tree)
|
|
161
|
+
|
|
162
|
+
# Zone 4: Status
|
|
163
|
+
status_items: list[str] = []
|
|
164
|
+
if data.is_truncated:
|
|
165
|
+
status_items.append("(truncated)")
|
|
166
|
+
if shown < total:
|
|
167
|
+
status_items.append(f"[{shown}/{total} lines]")
|
|
168
|
+
if duration_ms is not None:
|
|
169
|
+
status_items.append(f"{duration_ms:.0f}ms")
|
|
170
|
+
|
|
171
|
+
status = Text(" ".join(status_items), style="dim") if status_items else Text("")
|
|
172
|
+
|
|
173
|
+
# Compose
|
|
174
|
+
content = Group(
|
|
175
|
+
header,
|
|
176
|
+
Text("\n"),
|
|
177
|
+
params,
|
|
178
|
+
Text("\n"),
|
|
179
|
+
separator,
|
|
180
|
+
Text("\n"),
|
|
181
|
+
viewport,
|
|
182
|
+
Text("\n"),
|
|
183
|
+
separator,
|
|
184
|
+
Text("\n"),
|
|
185
|
+
status,
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
timestamp = datetime.now().strftime("%H:%M:%S")
|
|
189
|
+
|
|
190
|
+
return Panel(
|
|
191
|
+
content,
|
|
192
|
+
title=f"[{UI_COLORS['success']}]list_dir[/] [done]",
|
|
193
|
+
subtitle=f"[{UI_COLORS['muted']}]{timestamp}[/]",
|
|
194
|
+
border_style=Style(color=UI_COLORS["success"]),
|
|
195
|
+
padding=(0, 1),
|
|
196
|
+
expand=True,
|
|
197
|
+
width=TOOL_PANEL_WIDTH,
|
|
198
|
+
)
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"""NeXTSTEP-style panel renderer for read_file tool output."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from rich.console import Group, RenderableType
|
|
12
|
+
from rich.panel import Panel
|
|
13
|
+
from rich.style import Style
|
|
14
|
+
from rich.text import Text
|
|
15
|
+
|
|
16
|
+
from tunacode.constants import (
|
|
17
|
+
MAX_PANEL_LINE_WIDTH,
|
|
18
|
+
MIN_VIEWPORT_LINES,
|
|
19
|
+
TOOL_PANEL_WIDTH,
|
|
20
|
+
TOOL_VIEWPORT_LINES,
|
|
21
|
+
UI_COLORS,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
BOX_HORIZONTAL = "\u2500"
|
|
25
|
+
SEPARATOR_WIDTH = 52
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class ReadFileData:
|
|
30
|
+
"""Parsed read_file result for structured display."""
|
|
31
|
+
|
|
32
|
+
filepath: str
|
|
33
|
+
filename: str
|
|
34
|
+
content_lines: list[tuple[int, str]] # (line_number, content)
|
|
35
|
+
total_lines: int
|
|
36
|
+
offset: int
|
|
37
|
+
has_more: bool
|
|
38
|
+
end_message: str
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def parse_result(args: dict[str, Any] | None, result: str) -> ReadFileData | None:
|
|
42
|
+
"""Extract structured data from read_file output.
|
|
43
|
+
|
|
44
|
+
Expected format:
|
|
45
|
+
<file>
|
|
46
|
+
00001| line content
|
|
47
|
+
00002| line content
|
|
48
|
+
...
|
|
49
|
+
|
|
50
|
+
(File has more lines. Use 'offset' to read beyond line N)
|
|
51
|
+
</file>
|
|
52
|
+
|
|
53
|
+
Or:
|
|
54
|
+
<file>
|
|
55
|
+
...
|
|
56
|
+
(End of file - total N lines)
|
|
57
|
+
</file>
|
|
58
|
+
"""
|
|
59
|
+
if not result or "<file>" not in result:
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
# Extract content between <file> tags
|
|
63
|
+
file_match = re.search(r"<file>\n(.*?)\n</file>", result, re.DOTALL)
|
|
64
|
+
if not file_match:
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
content = file_match.group(1)
|
|
68
|
+
lines = content.strip().splitlines()
|
|
69
|
+
|
|
70
|
+
if not lines:
|
|
71
|
+
return None
|
|
72
|
+
|
|
73
|
+
# Parse line-numbered content: "00001| content"
|
|
74
|
+
content_lines: list[tuple[int, str]] = []
|
|
75
|
+
end_message = ""
|
|
76
|
+
total_lines = 0
|
|
77
|
+
has_more = False
|
|
78
|
+
|
|
79
|
+
line_pattern = re.compile(r"^(\d+)\|\s?(.*)")
|
|
80
|
+
|
|
81
|
+
for line in lines:
|
|
82
|
+
# Check for end message
|
|
83
|
+
if line.startswith("(File has more"):
|
|
84
|
+
has_more = True
|
|
85
|
+
end_message = line
|
|
86
|
+
# Extract total from "Use 'offset' to read beyond line N"
|
|
87
|
+
match = re.search(r"beyond line (\d+)", line)
|
|
88
|
+
if match:
|
|
89
|
+
total_lines = int(match.group(1))
|
|
90
|
+
continue
|
|
91
|
+
if line.startswith("(End of file"):
|
|
92
|
+
end_message = line
|
|
93
|
+
match = re.search(r"total (\d+) lines", line)
|
|
94
|
+
if match:
|
|
95
|
+
total_lines = int(match.group(1))
|
|
96
|
+
continue
|
|
97
|
+
|
|
98
|
+
# Parse line content
|
|
99
|
+
match = line_pattern.match(line)
|
|
100
|
+
if match:
|
|
101
|
+
line_num = int(match.group(1))
|
|
102
|
+
line_content = match.group(2)
|
|
103
|
+
content_lines.append((line_num, line_content))
|
|
104
|
+
|
|
105
|
+
if not content_lines:
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
args = args or {}
|
|
109
|
+
filepath = args.get("filepath", "unknown")
|
|
110
|
+
offset = args.get("offset", 0)
|
|
111
|
+
|
|
112
|
+
# If total_lines not found in message, estimate from content
|
|
113
|
+
if total_lines == 0:
|
|
114
|
+
total_lines = content_lines[-1][0] if content_lines else 0
|
|
115
|
+
|
|
116
|
+
return ReadFileData(
|
|
117
|
+
filepath=filepath,
|
|
118
|
+
filename=Path(filepath).name,
|
|
119
|
+
content_lines=content_lines,
|
|
120
|
+
total_lines=total_lines,
|
|
121
|
+
offset=offset,
|
|
122
|
+
has_more=has_more,
|
|
123
|
+
end_message=end_message,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _truncate_line(line: str) -> str:
|
|
128
|
+
"""Truncate a single line if too wide."""
|
|
129
|
+
if len(line) > MAX_PANEL_LINE_WIDTH:
|
|
130
|
+
return line[: MAX_PANEL_LINE_WIDTH - 3] + "..."
|
|
131
|
+
return line
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def render_read_file(
|
|
135
|
+
args: dict[str, Any] | None,
|
|
136
|
+
result: str,
|
|
137
|
+
duration_ms: float | None = None,
|
|
138
|
+
) -> RenderableType | None:
|
|
139
|
+
"""Render read_file with NeXTSTEP zoned layout.
|
|
140
|
+
|
|
141
|
+
Zones:
|
|
142
|
+
- Header: filename + line range
|
|
143
|
+
- Selection context: filepath
|
|
144
|
+
- Primary viewport: line-numbered content
|
|
145
|
+
- Status: total lines, continuation info, duration
|
|
146
|
+
"""
|
|
147
|
+
data = parse_result(args, result)
|
|
148
|
+
if not data:
|
|
149
|
+
return None
|
|
150
|
+
|
|
151
|
+
# Zone 1: Filename + line range
|
|
152
|
+
header = Text()
|
|
153
|
+
header.append(data.filename, style="bold")
|
|
154
|
+
|
|
155
|
+
if data.content_lines:
|
|
156
|
+
start_line = data.content_lines[0][0]
|
|
157
|
+
end_line = data.content_lines[-1][0]
|
|
158
|
+
header.append(f" lines {start_line}-{end_line}", style="dim")
|
|
159
|
+
|
|
160
|
+
# Zone 2: Full filepath
|
|
161
|
+
params = Text()
|
|
162
|
+
params.append("path:", style="dim")
|
|
163
|
+
params.append(f" {data.filepath}", style="dim bold")
|
|
164
|
+
|
|
165
|
+
separator = Text(BOX_HORIZONTAL * SEPARATOR_WIDTH, style="dim")
|
|
166
|
+
|
|
167
|
+
# Zone 3: Content viewport
|
|
168
|
+
viewport_lines: list[str] = []
|
|
169
|
+
max_display = TOOL_VIEWPORT_LINES
|
|
170
|
+
|
|
171
|
+
for i, (line_num, line_content) in enumerate(data.content_lines):
|
|
172
|
+
if i >= max_display:
|
|
173
|
+
break
|
|
174
|
+
formatted = f"{line_num:>5}| {line_content}"
|
|
175
|
+
viewport_lines.append(_truncate_line(formatted))
|
|
176
|
+
|
|
177
|
+
# Pad viewport to minimum height for visual consistency
|
|
178
|
+
while len(viewport_lines) < MIN_VIEWPORT_LINES:
|
|
179
|
+
viewport_lines.append("")
|
|
180
|
+
|
|
181
|
+
viewport = Text("\n".join(viewport_lines)) if viewport_lines else Text("(empty file)")
|
|
182
|
+
|
|
183
|
+
# Zone 4: Status
|
|
184
|
+
status_items: list[str] = []
|
|
185
|
+
|
|
186
|
+
shown = min(len(data.content_lines), max_display)
|
|
187
|
+
if shown < len(data.content_lines):
|
|
188
|
+
status_items.append(f"[{shown}/{len(data.content_lines)} displayed]")
|
|
189
|
+
|
|
190
|
+
if data.has_more:
|
|
191
|
+
status_items.append(f"total: {data.total_lines} lines")
|
|
192
|
+
status_items.append("(more available)")
|
|
193
|
+
elif data.total_lines > 0:
|
|
194
|
+
status_items.append(f"total: {data.total_lines} lines")
|
|
195
|
+
|
|
196
|
+
if duration_ms is not None:
|
|
197
|
+
status_items.append(f"{duration_ms:.0f}ms")
|
|
198
|
+
|
|
199
|
+
status = Text(" ".join(status_items), style="dim") if status_items else Text("")
|
|
200
|
+
|
|
201
|
+
# Compose
|
|
202
|
+
content = Group(
|
|
203
|
+
header,
|
|
204
|
+
Text("\n"),
|
|
205
|
+
params,
|
|
206
|
+
Text("\n"),
|
|
207
|
+
separator,
|
|
208
|
+
Text("\n"),
|
|
209
|
+
viewport,
|
|
210
|
+
Text("\n"),
|
|
211
|
+
separator,
|
|
212
|
+
Text("\n"),
|
|
213
|
+
status,
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
timestamp = datetime.now().strftime("%H:%M:%S")
|
|
217
|
+
|
|
218
|
+
return Panel(
|
|
219
|
+
content,
|
|
220
|
+
title=f"[{UI_COLORS['success']}]read_file[/] [done]",
|
|
221
|
+
subtitle=f"[{UI_COLORS['muted']}]{timestamp}[/]",
|
|
222
|
+
border_style=Style(color=UI_COLORS["success"]),
|
|
223
|
+
padding=(0, 1),
|
|
224
|
+
expand=True,
|
|
225
|
+
width=TOOL_PANEL_WIDTH,
|
|
226
|
+
)
|