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,314 @@
|
|
|
1
|
+
"""Search Result Display for Textual TUI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, TypeVar
|
|
9
|
+
|
|
10
|
+
from rich.console import 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 TOOL_PANEL_WIDTH, UI_COLORS
|
|
16
|
+
from tunacode.ui.renderers.panels import RichPanelRenderer, SearchResultData
|
|
17
|
+
|
|
18
|
+
T = TypeVar("T")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _paginate(items: list[T], page: int, page_size: int) -> tuple[list[T], int, int]:
|
|
22
|
+
start_idx = (page - 1) * page_size
|
|
23
|
+
end_idx = start_idx + page_size
|
|
24
|
+
total_pages = (len(items) + page_size - 1) // page_size if items else 1
|
|
25
|
+
return items[start_idx:end_idx], start_idx, total_pages
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class FileSearchResult:
|
|
30
|
+
file_path: str
|
|
31
|
+
line_number: int | None = None
|
|
32
|
+
content: str = ""
|
|
33
|
+
match_start: int | None = None
|
|
34
|
+
match_end: int | None = None
|
|
35
|
+
relevance: float | None = None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class CodeSearchResult:
|
|
40
|
+
file_path: str
|
|
41
|
+
symbol_name: str
|
|
42
|
+
symbol_type: str
|
|
43
|
+
line_number: int
|
|
44
|
+
context: str = ""
|
|
45
|
+
relevance: float | None = None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class SearchDisplayRenderer:
|
|
49
|
+
@staticmethod
|
|
50
|
+
def render_file_results(
|
|
51
|
+
query: str,
|
|
52
|
+
results: list[FileSearchResult],
|
|
53
|
+
page: int = 1,
|
|
54
|
+
page_size: int = 10,
|
|
55
|
+
search_time_ms: float | None = None,
|
|
56
|
+
) -> RenderableType:
|
|
57
|
+
generic_results = []
|
|
58
|
+
for r in results:
|
|
59
|
+
result_dict: dict[str, Any] = {
|
|
60
|
+
"file": r.file_path,
|
|
61
|
+
"content": r.content,
|
|
62
|
+
}
|
|
63
|
+
if r.line_number is not None:
|
|
64
|
+
result_dict["title"] = f"{r.file_path}:{r.line_number}"
|
|
65
|
+
else:
|
|
66
|
+
result_dict["title"] = r.file_path
|
|
67
|
+
|
|
68
|
+
if r.relevance is not None:
|
|
69
|
+
result_dict["relevance"] = r.relevance
|
|
70
|
+
|
|
71
|
+
if r.match_start is not None and r.match_end is not None:
|
|
72
|
+
before = r.content[: r.match_start]
|
|
73
|
+
match = r.content[r.match_start : r.match_end]
|
|
74
|
+
after = r.content[r.match_end :]
|
|
75
|
+
result_dict["snippet"] = f"{before}[{match}]{after}"
|
|
76
|
+
else:
|
|
77
|
+
result_dict["snippet"] = r.content
|
|
78
|
+
|
|
79
|
+
generic_results.append(result_dict)
|
|
80
|
+
|
|
81
|
+
page_results, _, _ = _paginate(generic_results, page, page_size)
|
|
82
|
+
|
|
83
|
+
data = SearchResultData(
|
|
84
|
+
query=query,
|
|
85
|
+
results=page_results,
|
|
86
|
+
total_count=len(results),
|
|
87
|
+
current_page=page,
|
|
88
|
+
page_size=page_size,
|
|
89
|
+
search_time_ms=search_time_ms,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
return RichPanelRenderer.render_search_results(data)
|
|
93
|
+
|
|
94
|
+
@staticmethod
|
|
95
|
+
def render_code_results(
|
|
96
|
+
query: str,
|
|
97
|
+
results: list[CodeSearchResult],
|
|
98
|
+
page: int = 1,
|
|
99
|
+
page_size: int = 10,
|
|
100
|
+
search_time_ms: float | None = None,
|
|
101
|
+
) -> RenderableType:
|
|
102
|
+
generic_results = []
|
|
103
|
+
for r in results:
|
|
104
|
+
type_icons = {
|
|
105
|
+
"function": "fn",
|
|
106
|
+
"class": "cls",
|
|
107
|
+
"variable": "var",
|
|
108
|
+
"method": "mtd",
|
|
109
|
+
"constant": "const",
|
|
110
|
+
}
|
|
111
|
+
type_label = type_icons.get(r.symbol_type, r.symbol_type[:3])
|
|
112
|
+
|
|
113
|
+
result_dict: dict[str, Any] = {
|
|
114
|
+
"title": f"[{type_label}] {r.symbol_name}",
|
|
115
|
+
"file": f"{r.file_path}:{r.line_number}",
|
|
116
|
+
"snippet": r.context,
|
|
117
|
+
}
|
|
118
|
+
if r.relevance is not None:
|
|
119
|
+
result_dict["relevance"] = r.relevance
|
|
120
|
+
|
|
121
|
+
generic_results.append(result_dict)
|
|
122
|
+
|
|
123
|
+
page_results, _, _ = _paginate(generic_results, page, page_size)
|
|
124
|
+
|
|
125
|
+
data = SearchResultData(
|
|
126
|
+
query=query,
|
|
127
|
+
results=page_results,
|
|
128
|
+
total_count=len(results),
|
|
129
|
+
current_page=page,
|
|
130
|
+
page_size=page_size,
|
|
131
|
+
search_time_ms=search_time_ms,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
return RichPanelRenderer.render_search_results(data)
|
|
135
|
+
|
|
136
|
+
@staticmethod
|
|
137
|
+
def render_inline_results(
|
|
138
|
+
results: list[dict[str, Any]],
|
|
139
|
+
max_display: int = 5,
|
|
140
|
+
) -> RenderableType:
|
|
141
|
+
if not results:
|
|
142
|
+
return Text("No results found", style="dim")
|
|
143
|
+
|
|
144
|
+
content = Text()
|
|
145
|
+
display_results = results[:max_display]
|
|
146
|
+
|
|
147
|
+
for i, result in enumerate(display_results):
|
|
148
|
+
title = result.get("title", result.get("file", "..."))
|
|
149
|
+
content.append(f"{i + 1}. ", style="dim")
|
|
150
|
+
content.append(str(title), style=UI_COLORS["primary"])
|
|
151
|
+
content.append("\n")
|
|
152
|
+
|
|
153
|
+
if len(results) > max_display:
|
|
154
|
+
remaining = len(results) - max_display
|
|
155
|
+
content.append(f" ...and {remaining} more", style="dim")
|
|
156
|
+
|
|
157
|
+
return content
|
|
158
|
+
|
|
159
|
+
@staticmethod
|
|
160
|
+
def render_empty_results(query: str) -> RenderableType:
|
|
161
|
+
content = Text()
|
|
162
|
+
content.append("No results found for: ", style="dim")
|
|
163
|
+
content.append(query, style="bold")
|
|
164
|
+
content.append("\n\nSuggestions:\n", style="dim")
|
|
165
|
+
content.append(" - Try a different search term\n", style="dim")
|
|
166
|
+
content.append(" - Use broader patterns\n", style="dim")
|
|
167
|
+
content.append(" - Check file paths and extensions", style="dim")
|
|
168
|
+
|
|
169
|
+
return Panel(
|
|
170
|
+
content,
|
|
171
|
+
title=f"[{UI_COLORS['warning']}]No Results[/]",
|
|
172
|
+
border_style=Style(color=UI_COLORS["warning"]),
|
|
173
|
+
padding=(0, 1),
|
|
174
|
+
expand=True,
|
|
175
|
+
width=TOOL_PANEL_WIDTH,
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
@staticmethod
|
|
179
|
+
def parse_grep_output(text: str, query: str | None = None) -> SearchResultData | None:
|
|
180
|
+
if not text or "Found" not in text:
|
|
181
|
+
return None
|
|
182
|
+
|
|
183
|
+
header_match = re.match(r"Found (\d+) match(?:es)? for pattern: (.+)", text)
|
|
184
|
+
if not header_match:
|
|
185
|
+
return None
|
|
186
|
+
|
|
187
|
+
total_count = int(header_match.group(1))
|
|
188
|
+
detected_query = header_match.group(2).strip()
|
|
189
|
+
final_query = query or detected_query
|
|
190
|
+
|
|
191
|
+
results: list[dict[str, Any]] = []
|
|
192
|
+
file_pattern = re.compile(r"📁 (.+?):(\d+)")
|
|
193
|
+
match_pattern = re.compile(r"▶\s*(\d+)│\s*(.*?)⟨(.+?)⟩(.*)")
|
|
194
|
+
|
|
195
|
+
current_file: str | None = None
|
|
196
|
+
|
|
197
|
+
for line in text.split("\n"):
|
|
198
|
+
file_match = file_pattern.search(line)
|
|
199
|
+
if file_match:
|
|
200
|
+
current_file = file_match.group(1)
|
|
201
|
+
continue
|
|
202
|
+
|
|
203
|
+
match_line = match_pattern.search(line)
|
|
204
|
+
if match_line and current_file:
|
|
205
|
+
line_num = int(match_line.group(1))
|
|
206
|
+
before = match_line.group(2)
|
|
207
|
+
match_text = match_line.group(3)
|
|
208
|
+
after = match_line.group(4)
|
|
209
|
+
|
|
210
|
+
results.append(
|
|
211
|
+
{
|
|
212
|
+
"title": f"{current_file}:{line_num}",
|
|
213
|
+
"file": current_file,
|
|
214
|
+
"snippet": f"{before}[{match_text}]{after}",
|
|
215
|
+
"line_number": line_num,
|
|
216
|
+
}
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
if not results:
|
|
220
|
+
return None
|
|
221
|
+
|
|
222
|
+
return SearchResultData(
|
|
223
|
+
query=final_query,
|
|
224
|
+
results=results,
|
|
225
|
+
total_count=total_count,
|
|
226
|
+
current_page=1,
|
|
227
|
+
page_size=len(results),
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
@staticmethod
|
|
231
|
+
def parse_glob_output(text: str, pattern: str | None = None) -> SearchResultData | None:
|
|
232
|
+
if not text:
|
|
233
|
+
return None
|
|
234
|
+
|
|
235
|
+
# Extract source marker if present
|
|
236
|
+
source: str | None = None
|
|
237
|
+
lines = text.split("\n")
|
|
238
|
+
if lines and lines[0].startswith("[source:"):
|
|
239
|
+
marker = lines[0]
|
|
240
|
+
source = marker[8:-1] # Extract between "[source:" and "]"
|
|
241
|
+
text = "\n".join(lines[1:]) # Remove marker from text
|
|
242
|
+
|
|
243
|
+
if "Found" not in text:
|
|
244
|
+
return None
|
|
245
|
+
|
|
246
|
+
header_match = re.match(r"Found (\d+) files? matching pattern: (.+)", text)
|
|
247
|
+
if not header_match:
|
|
248
|
+
return None
|
|
249
|
+
|
|
250
|
+
total_count = int(header_match.group(1))
|
|
251
|
+
detected_pattern = header_match.group(2).strip()
|
|
252
|
+
final_pattern = pattern or detected_pattern
|
|
253
|
+
|
|
254
|
+
results: list[dict[str, Any]] = []
|
|
255
|
+
|
|
256
|
+
# Parse file paths - supports both plain paths and formatted output
|
|
257
|
+
for line in text.split("\n"):
|
|
258
|
+
line = line.strip()
|
|
259
|
+
# Skip header line, empty lines, and truncation notice
|
|
260
|
+
if not line or line.startswith("Found ") or line.startswith("(truncated"):
|
|
261
|
+
continue
|
|
262
|
+
|
|
263
|
+
# Handle plain file paths (new format)
|
|
264
|
+
if line.startswith("/") or line.startswith("./") or "/" in line:
|
|
265
|
+
path = Path(line)
|
|
266
|
+
results.append(
|
|
267
|
+
{
|
|
268
|
+
"title": line,
|
|
269
|
+
"file": line,
|
|
270
|
+
"snippet": path.name,
|
|
271
|
+
}
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
if not results:
|
|
275
|
+
return None
|
|
276
|
+
|
|
277
|
+
return SearchResultData(
|
|
278
|
+
query=final_pattern,
|
|
279
|
+
results=results,
|
|
280
|
+
total_count=total_count,
|
|
281
|
+
current_page=1,
|
|
282
|
+
page_size=len(results),
|
|
283
|
+
source=source,
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def file_search_panel(
|
|
288
|
+
query: str,
|
|
289
|
+
results: list[FileSearchResult],
|
|
290
|
+
page: int = 1,
|
|
291
|
+
search_time_ms: float | None = None,
|
|
292
|
+
) -> RenderableType:
|
|
293
|
+
if not results:
|
|
294
|
+
return SearchDisplayRenderer.render_empty_results(query)
|
|
295
|
+
return SearchDisplayRenderer.render_file_results(
|
|
296
|
+
query, results, page, search_time_ms=search_time_ms
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def code_search_panel(
|
|
301
|
+
query: str,
|
|
302
|
+
results: list[CodeSearchResult],
|
|
303
|
+
page: int = 1,
|
|
304
|
+
search_time_ms: float | None = None,
|
|
305
|
+
) -> RenderableType:
|
|
306
|
+
if not results:
|
|
307
|
+
return SearchDisplayRenderer.render_empty_results(query)
|
|
308
|
+
return SearchDisplayRenderer.render_code_results(
|
|
309
|
+
query, results, page, search_time_ms=search_time_ms
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def quick_results(results: list[dict[str, Any]], max_display: int = 5) -> RenderableType:
|
|
314
|
+
return SearchDisplayRenderer.render_inline_results(results, max_display)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Tool-specific panel renderers following NeXTSTEP UI principles."""
|
|
2
|
+
|
|
3
|
+
from tunacode.ui.renderers.tools.bash import render_bash
|
|
4
|
+
from tunacode.ui.renderers.tools.glob import render_glob
|
|
5
|
+
from tunacode.ui.renderers.tools.grep import render_grep
|
|
6
|
+
from tunacode.ui.renderers.tools.list_dir import render_list_dir
|
|
7
|
+
from tunacode.ui.renderers.tools.read_file import render_read_file
|
|
8
|
+
from tunacode.ui.renderers.tools.research import render_research_codebase
|
|
9
|
+
from tunacode.ui.renderers.tools.update_file import render_update_file
|
|
10
|
+
from tunacode.ui.renderers.tools.web_fetch import render_web_fetch
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"render_bash",
|
|
14
|
+
"render_glob",
|
|
15
|
+
"render_grep",
|
|
16
|
+
"render_list_dir",
|
|
17
|
+
"render_read_file",
|
|
18
|
+
"render_research_codebase",
|
|
19
|
+
"render_update_file",
|
|
20
|
+
"render_web_fetch",
|
|
21
|
+
]
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
"""NeXTSTEP-style panel renderer for bash 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
|
+
|
|
23
|
+
BOX_HORIZONTAL = "\u2500"
|
|
24
|
+
SEPARATOR_WIDTH = 52
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class BashData:
|
|
29
|
+
"""Parsed bash result for structured display."""
|
|
30
|
+
|
|
31
|
+
command: str
|
|
32
|
+
exit_code: int
|
|
33
|
+
working_dir: str
|
|
34
|
+
stdout: str
|
|
35
|
+
stderr: str
|
|
36
|
+
is_truncated: bool
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def parse_result(args: dict[str, Any] | None, result: str) -> BashData | None:
|
|
40
|
+
"""Extract structured data from bash output.
|
|
41
|
+
|
|
42
|
+
Expected format:
|
|
43
|
+
Command: <command>
|
|
44
|
+
Exit Code: <code>
|
|
45
|
+
Working Directory: <path>
|
|
46
|
+
|
|
47
|
+
STDOUT:
|
|
48
|
+
<output>
|
|
49
|
+
|
|
50
|
+
STDERR:
|
|
51
|
+
<errors>
|
|
52
|
+
"""
|
|
53
|
+
if not result:
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
# Parse structured output
|
|
57
|
+
command_match = re.search(r"Command: (.+)", result)
|
|
58
|
+
exit_match = re.search(r"Exit Code: (\d+)", result)
|
|
59
|
+
cwd_match = re.search(r"Working Directory: (.+)", result)
|
|
60
|
+
|
|
61
|
+
if not command_match or not exit_match:
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
command = command_match.group(1).strip()
|
|
65
|
+
exit_code = int(exit_match.group(1))
|
|
66
|
+
working_dir = cwd_match.group(1).strip() if cwd_match else "."
|
|
67
|
+
|
|
68
|
+
# Extract stdout and stderr
|
|
69
|
+
stdout = ""
|
|
70
|
+
stderr = ""
|
|
71
|
+
|
|
72
|
+
stdout_match = re.search(r"STDOUT:\n(.*?)(?=\n\nSTDERR:|\Z)", result, re.DOTALL)
|
|
73
|
+
if stdout_match:
|
|
74
|
+
stdout = stdout_match.group(1).strip()
|
|
75
|
+
if stdout == "(no output)":
|
|
76
|
+
stdout = ""
|
|
77
|
+
|
|
78
|
+
stderr_match = re.search(r"STDERR:\n(.*?)(?:\Z)", result, re.DOTALL)
|
|
79
|
+
if stderr_match:
|
|
80
|
+
stderr = stderr_match.group(1).strip()
|
|
81
|
+
if stderr == "(no errors)":
|
|
82
|
+
stderr = ""
|
|
83
|
+
|
|
84
|
+
# Check for truncation marker
|
|
85
|
+
is_truncated = "[truncated]" in result
|
|
86
|
+
|
|
87
|
+
return BashData(
|
|
88
|
+
command=command,
|
|
89
|
+
exit_code=exit_code,
|
|
90
|
+
working_dir=working_dir,
|
|
91
|
+
stdout=stdout,
|
|
92
|
+
stderr=stderr,
|
|
93
|
+
is_truncated=is_truncated,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _truncate_line(line: str) -> str:
|
|
98
|
+
"""Truncate a single line if too wide."""
|
|
99
|
+
if len(line) > MAX_PANEL_LINE_WIDTH:
|
|
100
|
+
return line[: MAX_PANEL_LINE_WIDTH - 3] + "..."
|
|
101
|
+
return line
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _truncate_output(output: str) -> tuple[str, int, int]:
|
|
105
|
+
"""Truncate output, return (truncated, shown, total)."""
|
|
106
|
+
lines = output.splitlines()
|
|
107
|
+
total = len(lines)
|
|
108
|
+
|
|
109
|
+
max_lines = TOOL_VIEWPORT_LINES
|
|
110
|
+
|
|
111
|
+
if total <= max_lines:
|
|
112
|
+
return "\n".join(_truncate_line(ln) for ln in lines), total, total
|
|
113
|
+
|
|
114
|
+
truncated = [_truncate_line(ln) for ln in lines[:max_lines]]
|
|
115
|
+
return "\n".join(truncated), max_lines, total
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def render_bash(
|
|
119
|
+
args: dict[str, Any] | None,
|
|
120
|
+
result: str,
|
|
121
|
+
duration_ms: float | None = None,
|
|
122
|
+
) -> RenderableType | None:
|
|
123
|
+
"""Render bash with NeXTSTEP zoned layout.
|
|
124
|
+
|
|
125
|
+
Zones:
|
|
126
|
+
- Header: command + exit code indicator
|
|
127
|
+
- Selection context: working directory, timeout
|
|
128
|
+
- Primary viewport: stdout/stderr output
|
|
129
|
+
- Status: truncation info, duration
|
|
130
|
+
"""
|
|
131
|
+
data = parse_result(args, result)
|
|
132
|
+
if not data:
|
|
133
|
+
return None
|
|
134
|
+
|
|
135
|
+
# Zone 1: Command + exit status
|
|
136
|
+
header = Text()
|
|
137
|
+
|
|
138
|
+
# Truncate command for header
|
|
139
|
+
cmd_display = data.command
|
|
140
|
+
if len(cmd_display) > 50:
|
|
141
|
+
cmd_display = cmd_display[:47] + "..."
|
|
142
|
+
header.append(f"$ {cmd_display}", style="bold")
|
|
143
|
+
|
|
144
|
+
# Exit code indicator
|
|
145
|
+
if data.exit_code == 0:
|
|
146
|
+
header.append(" ", style="")
|
|
147
|
+
header.append("ok", style="green bold")
|
|
148
|
+
else:
|
|
149
|
+
header.append(" ", style="")
|
|
150
|
+
header.append(f"exit {data.exit_code}", style="red bold")
|
|
151
|
+
|
|
152
|
+
# Zone 2: Parameters
|
|
153
|
+
args = args or {}
|
|
154
|
+
timeout = args.get("timeout", 30)
|
|
155
|
+
params = Text()
|
|
156
|
+
params.append("cwd:", style="dim")
|
|
157
|
+
params.append(f" {data.working_dir}", style="dim bold")
|
|
158
|
+
params.append(" timeout:", style="dim")
|
|
159
|
+
params.append(f" {timeout}s", style="dim bold")
|
|
160
|
+
|
|
161
|
+
separator = Text(BOX_HORIZONTAL * SEPARATOR_WIDTH, style="dim")
|
|
162
|
+
|
|
163
|
+
# Zone 3: Output viewport
|
|
164
|
+
viewport_parts: list[Text] = []
|
|
165
|
+
|
|
166
|
+
if data.stdout:
|
|
167
|
+
truncated_stdout, shown_stdout, total_stdout = _truncate_output(data.stdout)
|
|
168
|
+
stdout_text = Text()
|
|
169
|
+
stdout_text.append("stdout:\n", style="dim")
|
|
170
|
+
stdout_text.append(truncated_stdout)
|
|
171
|
+
viewport_parts.append(stdout_text)
|
|
172
|
+
|
|
173
|
+
if data.stderr:
|
|
174
|
+
if viewport_parts:
|
|
175
|
+
viewport_parts.append(Text("\n"))
|
|
176
|
+
truncated_stderr, shown_stderr, total_stderr = _truncate_output(data.stderr)
|
|
177
|
+
stderr_text = Text()
|
|
178
|
+
stderr_text.append("stderr:\n", style="dim")
|
|
179
|
+
stderr_text.append(truncated_stderr, style="red")
|
|
180
|
+
viewport_parts.append(stderr_text)
|
|
181
|
+
|
|
182
|
+
if not viewport_parts:
|
|
183
|
+
viewport_parts.append(Text("(no output)", style="dim"))
|
|
184
|
+
|
|
185
|
+
# Pad viewport to minimum height for visual consistency
|
|
186
|
+
viewport_line_count = sum(
|
|
187
|
+
1 + str(part).count("\n") for part in viewport_parts if isinstance(part, Text)
|
|
188
|
+
)
|
|
189
|
+
while viewport_line_count < MIN_VIEWPORT_LINES:
|
|
190
|
+
viewport_parts.append(Text(""))
|
|
191
|
+
viewport_line_count += 1
|
|
192
|
+
|
|
193
|
+
viewport = Group(*viewport_parts)
|
|
194
|
+
|
|
195
|
+
# Zone 4: Status
|
|
196
|
+
status_items: list[str] = []
|
|
197
|
+
|
|
198
|
+
if data.is_truncated:
|
|
199
|
+
status_items.append("(truncated)")
|
|
200
|
+
|
|
201
|
+
if data.stdout:
|
|
202
|
+
stdout_lines = len(data.stdout.splitlines())
|
|
203
|
+
status_items.append(f"stdout: {stdout_lines} lines")
|
|
204
|
+
|
|
205
|
+
if data.stderr:
|
|
206
|
+
stderr_lines = len(data.stderr.splitlines())
|
|
207
|
+
status_items.append(f"stderr: {stderr_lines} lines")
|
|
208
|
+
|
|
209
|
+
if duration_ms is not None:
|
|
210
|
+
status_items.append(f"{duration_ms:.0f}ms")
|
|
211
|
+
|
|
212
|
+
status = Text(" ".join(status_items), style="dim") if status_items else Text("")
|
|
213
|
+
|
|
214
|
+
# Compose
|
|
215
|
+
content = Group(
|
|
216
|
+
header,
|
|
217
|
+
Text("\n"),
|
|
218
|
+
params,
|
|
219
|
+
Text("\n"),
|
|
220
|
+
separator,
|
|
221
|
+
Text("\n"),
|
|
222
|
+
viewport,
|
|
223
|
+
Text("\n"),
|
|
224
|
+
separator,
|
|
225
|
+
Text("\n"),
|
|
226
|
+
status,
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
timestamp = datetime.now().strftime("%H:%M:%S")
|
|
230
|
+
|
|
231
|
+
# Use different colors based on exit code
|
|
232
|
+
if data.exit_code == 0:
|
|
233
|
+
border_color = UI_COLORS["success"]
|
|
234
|
+
status_text = "done"
|
|
235
|
+
else:
|
|
236
|
+
border_color = UI_COLORS["warning"]
|
|
237
|
+
status_text = f"exit {data.exit_code}"
|
|
238
|
+
|
|
239
|
+
return Panel(
|
|
240
|
+
content,
|
|
241
|
+
title=f"[{border_color}]bash[/] [{status_text}]",
|
|
242
|
+
subtitle=f"[{UI_COLORS['muted']}]{timestamp}[/]",
|
|
243
|
+
border_style=Style(color=border_color),
|
|
244
|
+
padding=(0, 1),
|
|
245
|
+
expand=True,
|
|
246
|
+
width=TOOL_PANEL_WIDTH,
|
|
247
|
+
)
|