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.

Files changed (174) hide show
  1. tunacode/__init__.py +0 -0
  2. tunacode/cli/textual_repl.tcss +283 -0
  3. tunacode/configuration/__init__.py +1 -0
  4. tunacode/configuration/defaults.py +45 -0
  5. tunacode/configuration/models.py +147 -0
  6. tunacode/configuration/models_registry.json +1 -0
  7. tunacode/configuration/pricing.py +74 -0
  8. tunacode/configuration/settings.py +35 -0
  9. tunacode/constants.py +227 -0
  10. tunacode/core/__init__.py +6 -0
  11. tunacode/core/agents/__init__.py +39 -0
  12. tunacode/core/agents/agent_components/__init__.py +48 -0
  13. tunacode/core/agents/agent_components/agent_config.py +441 -0
  14. tunacode/core/agents/agent_components/agent_helpers.py +290 -0
  15. tunacode/core/agents/agent_components/message_handler.py +99 -0
  16. tunacode/core/agents/agent_components/node_processor.py +477 -0
  17. tunacode/core/agents/agent_components/response_state.py +129 -0
  18. tunacode/core/agents/agent_components/result_wrapper.py +51 -0
  19. tunacode/core/agents/agent_components/state_transition.py +112 -0
  20. tunacode/core/agents/agent_components/streaming.py +271 -0
  21. tunacode/core/agents/agent_components/task_completion.py +40 -0
  22. tunacode/core/agents/agent_components/tool_buffer.py +44 -0
  23. tunacode/core/agents/agent_components/tool_executor.py +101 -0
  24. tunacode/core/agents/agent_components/truncation_checker.py +37 -0
  25. tunacode/core/agents/delegation_tools.py +109 -0
  26. tunacode/core/agents/main.py +545 -0
  27. tunacode/core/agents/prompts.py +66 -0
  28. tunacode/core/agents/research_agent.py +231 -0
  29. tunacode/core/compaction.py +218 -0
  30. tunacode/core/prompting/__init__.py +27 -0
  31. tunacode/core/prompting/loader.py +66 -0
  32. tunacode/core/prompting/prompting_engine.py +98 -0
  33. tunacode/core/prompting/sections.py +50 -0
  34. tunacode/core/prompting/templates.py +69 -0
  35. tunacode/core/state.py +409 -0
  36. tunacode/exceptions.py +313 -0
  37. tunacode/indexing/__init__.py +5 -0
  38. tunacode/indexing/code_index.py +432 -0
  39. tunacode/indexing/constants.py +86 -0
  40. tunacode/lsp/__init__.py +112 -0
  41. tunacode/lsp/client.py +351 -0
  42. tunacode/lsp/diagnostics.py +19 -0
  43. tunacode/lsp/servers.py +101 -0
  44. tunacode/prompts/default_prompt.md +952 -0
  45. tunacode/prompts/research/sections/agent_role.xml +5 -0
  46. tunacode/prompts/research/sections/constraints.xml +14 -0
  47. tunacode/prompts/research/sections/output_format.xml +57 -0
  48. tunacode/prompts/research/sections/tool_use.xml +23 -0
  49. tunacode/prompts/sections/advanced_patterns.xml +255 -0
  50. tunacode/prompts/sections/agent_role.xml +8 -0
  51. tunacode/prompts/sections/completion.xml +10 -0
  52. tunacode/prompts/sections/critical_rules.xml +37 -0
  53. tunacode/prompts/sections/examples.xml +220 -0
  54. tunacode/prompts/sections/output_style.xml +94 -0
  55. tunacode/prompts/sections/parallel_exec.xml +105 -0
  56. tunacode/prompts/sections/search_pattern.xml +100 -0
  57. tunacode/prompts/sections/system_info.xml +6 -0
  58. tunacode/prompts/sections/tool_use.xml +84 -0
  59. tunacode/prompts/sections/user_instructions.xml +3 -0
  60. tunacode/py.typed +0 -0
  61. tunacode/templates/__init__.py +5 -0
  62. tunacode/templates/loader.py +15 -0
  63. tunacode/tools/__init__.py +10 -0
  64. tunacode/tools/authorization/__init__.py +29 -0
  65. tunacode/tools/authorization/context.py +32 -0
  66. tunacode/tools/authorization/factory.py +20 -0
  67. tunacode/tools/authorization/handler.py +58 -0
  68. tunacode/tools/authorization/notifier.py +35 -0
  69. tunacode/tools/authorization/policy.py +19 -0
  70. tunacode/tools/authorization/requests.py +119 -0
  71. tunacode/tools/authorization/rules.py +72 -0
  72. tunacode/tools/bash.py +222 -0
  73. tunacode/tools/decorators.py +213 -0
  74. tunacode/tools/glob.py +353 -0
  75. tunacode/tools/grep.py +468 -0
  76. tunacode/tools/grep_components/__init__.py +9 -0
  77. tunacode/tools/grep_components/file_filter.py +93 -0
  78. tunacode/tools/grep_components/pattern_matcher.py +158 -0
  79. tunacode/tools/grep_components/result_formatter.py +87 -0
  80. tunacode/tools/grep_components/search_result.py +34 -0
  81. tunacode/tools/list_dir.py +205 -0
  82. tunacode/tools/prompts/bash_prompt.xml +10 -0
  83. tunacode/tools/prompts/glob_prompt.xml +7 -0
  84. tunacode/tools/prompts/grep_prompt.xml +10 -0
  85. tunacode/tools/prompts/list_dir_prompt.xml +7 -0
  86. tunacode/tools/prompts/read_file_prompt.xml +9 -0
  87. tunacode/tools/prompts/todoclear_prompt.xml +12 -0
  88. tunacode/tools/prompts/todoread_prompt.xml +16 -0
  89. tunacode/tools/prompts/todowrite_prompt.xml +28 -0
  90. tunacode/tools/prompts/update_file_prompt.xml +9 -0
  91. tunacode/tools/prompts/web_fetch_prompt.xml +11 -0
  92. tunacode/tools/prompts/write_file_prompt.xml +7 -0
  93. tunacode/tools/react.py +111 -0
  94. tunacode/tools/read_file.py +68 -0
  95. tunacode/tools/todo.py +222 -0
  96. tunacode/tools/update_file.py +62 -0
  97. tunacode/tools/utils/__init__.py +1 -0
  98. tunacode/tools/utils/ripgrep.py +311 -0
  99. tunacode/tools/utils/text_match.py +352 -0
  100. tunacode/tools/web_fetch.py +245 -0
  101. tunacode/tools/write_file.py +34 -0
  102. tunacode/tools/xml_helper.py +34 -0
  103. tunacode/types/__init__.py +166 -0
  104. tunacode/types/base.py +94 -0
  105. tunacode/types/callbacks.py +53 -0
  106. tunacode/types/dataclasses.py +121 -0
  107. tunacode/types/pydantic_ai.py +31 -0
  108. tunacode/types/state.py +122 -0
  109. tunacode/ui/__init__.py +6 -0
  110. tunacode/ui/app.py +542 -0
  111. tunacode/ui/commands/__init__.py +430 -0
  112. tunacode/ui/components/__init__.py +1 -0
  113. tunacode/ui/headless/__init__.py +5 -0
  114. tunacode/ui/headless/output.py +72 -0
  115. tunacode/ui/main.py +252 -0
  116. tunacode/ui/renderers/__init__.py +41 -0
  117. tunacode/ui/renderers/errors.py +197 -0
  118. tunacode/ui/renderers/panels.py +550 -0
  119. tunacode/ui/renderers/search.py +314 -0
  120. tunacode/ui/renderers/tools/__init__.py +21 -0
  121. tunacode/ui/renderers/tools/bash.py +247 -0
  122. tunacode/ui/renderers/tools/diagnostics.py +186 -0
  123. tunacode/ui/renderers/tools/glob.py +226 -0
  124. tunacode/ui/renderers/tools/grep.py +228 -0
  125. tunacode/ui/renderers/tools/list_dir.py +198 -0
  126. tunacode/ui/renderers/tools/read_file.py +226 -0
  127. tunacode/ui/renderers/tools/research.py +294 -0
  128. tunacode/ui/renderers/tools/update_file.py +237 -0
  129. tunacode/ui/renderers/tools/web_fetch.py +182 -0
  130. tunacode/ui/repl_support.py +226 -0
  131. tunacode/ui/screens/__init__.py +16 -0
  132. tunacode/ui/screens/model_picker.py +303 -0
  133. tunacode/ui/screens/session_picker.py +181 -0
  134. tunacode/ui/screens/setup.py +218 -0
  135. tunacode/ui/screens/theme_picker.py +90 -0
  136. tunacode/ui/screens/update_confirm.py +69 -0
  137. tunacode/ui/shell_runner.py +129 -0
  138. tunacode/ui/styles/layout.tcss +98 -0
  139. tunacode/ui/styles/modals.tcss +38 -0
  140. tunacode/ui/styles/panels.tcss +81 -0
  141. tunacode/ui/styles/theme-nextstep.tcss +303 -0
  142. tunacode/ui/styles/widgets.tcss +33 -0
  143. tunacode/ui/styles.py +18 -0
  144. tunacode/ui/widgets/__init__.py +23 -0
  145. tunacode/ui/widgets/command_autocomplete.py +62 -0
  146. tunacode/ui/widgets/editor.py +402 -0
  147. tunacode/ui/widgets/file_autocomplete.py +47 -0
  148. tunacode/ui/widgets/messages.py +46 -0
  149. tunacode/ui/widgets/resource_bar.py +182 -0
  150. tunacode/ui/widgets/status_bar.py +98 -0
  151. tunacode/utils/__init__.py +0 -0
  152. tunacode/utils/config/__init__.py +13 -0
  153. tunacode/utils/config/user_configuration.py +91 -0
  154. tunacode/utils/messaging/__init__.py +10 -0
  155. tunacode/utils/messaging/message_utils.py +34 -0
  156. tunacode/utils/messaging/token_counter.py +77 -0
  157. tunacode/utils/parsing/__init__.py +13 -0
  158. tunacode/utils/parsing/command_parser.py +55 -0
  159. tunacode/utils/parsing/json_utils.py +188 -0
  160. tunacode/utils/parsing/retry.py +146 -0
  161. tunacode/utils/parsing/tool_parser.py +267 -0
  162. tunacode/utils/security/__init__.py +15 -0
  163. tunacode/utils/security/command.py +106 -0
  164. tunacode/utils/system/__init__.py +25 -0
  165. tunacode/utils/system/gitignore.py +155 -0
  166. tunacode/utils/system/paths.py +190 -0
  167. tunacode/utils/ui/__init__.py +9 -0
  168. tunacode/utils/ui/file_filter.py +135 -0
  169. tunacode/utils/ui/helpers.py +24 -0
  170. tunacode_cli-0.1.21.dist-info/METADATA +170 -0
  171. tunacode_cli-0.1.21.dist-info/RECORD +174 -0
  172. tunacode_cli-0.1.21.dist-info/WHEEL +4 -0
  173. tunacode_cli-0.1.21.dist-info/entry_points.txt +2 -0
  174. 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
+ )