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
tunacode/ui/main.py
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
"""CLI entry point for TunaCode."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
|
|
10
|
+
from tunacode.configuration.settings import ApplicationSettings
|
|
11
|
+
from tunacode.constants import SETTINGS_BASE_URL
|
|
12
|
+
from tunacode.core.state import StateManager
|
|
13
|
+
from tunacode.exceptions import UserAbortError
|
|
14
|
+
from tunacode.tools.authorization.handler import ToolHandler
|
|
15
|
+
from tunacode.ui.headless import resolve_output
|
|
16
|
+
from tunacode.ui.repl_support import run_textual_repl
|
|
17
|
+
from tunacode.utils.system import check_for_updates
|
|
18
|
+
|
|
19
|
+
DEFAULT_TIMEOUT_SECONDS = 600
|
|
20
|
+
BASE_URL_HELP_TEXT = "API base URL (e.g., https://openrouter.ai/api/v1)"
|
|
21
|
+
HEADLESS_NO_RESPONSE_ERROR = "Error: No response generated"
|
|
22
|
+
|
|
23
|
+
app_settings = ApplicationSettings()
|
|
24
|
+
app = typer.Typer(help="TunaCode - OS AI-powered development assistant")
|
|
25
|
+
state_manager = StateManager()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _handle_background_task_error(task: asyncio.Task) -> None:
|
|
29
|
+
try:
|
|
30
|
+
exception = task.exception()
|
|
31
|
+
if exception is not None:
|
|
32
|
+
# Background task failed - just pass without logging
|
|
33
|
+
pass
|
|
34
|
+
except asyncio.CancelledError:
|
|
35
|
+
pass
|
|
36
|
+
except Exception:
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _print_version() -> None:
|
|
41
|
+
from tunacode.constants import APP_VERSION
|
|
42
|
+
|
|
43
|
+
print(f"tunacode {APP_VERSION}")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _apply_base_url_override(state_manager: StateManager, base_url: str | None) -> None:
|
|
47
|
+
if not base_url:
|
|
48
|
+
return
|
|
49
|
+
|
|
50
|
+
user_config = state_manager.session.user_config
|
|
51
|
+
settings = user_config.get("settings")
|
|
52
|
+
if settings is None:
|
|
53
|
+
settings = {}
|
|
54
|
+
user_config["settings"] = settings
|
|
55
|
+
|
|
56
|
+
settings[SETTINGS_BASE_URL] = base_url
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
async def _run_textual_app(*, model: str | None, show_setup: bool) -> None:
|
|
60
|
+
update_task = asyncio.create_task(asyncio.to_thread(check_for_updates), name="update_check")
|
|
61
|
+
update_task.add_done_callback(_handle_background_task_error)
|
|
62
|
+
|
|
63
|
+
if model:
|
|
64
|
+
state_manager.session.current_model = model
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
tool_handler = ToolHandler(state_manager)
|
|
68
|
+
state_manager.set_tool_handler(tool_handler)
|
|
69
|
+
|
|
70
|
+
await run_textual_repl(state_manager, show_setup=show_setup)
|
|
71
|
+
except (KeyboardInterrupt, UserAbortError):
|
|
72
|
+
update_task.cancel()
|
|
73
|
+
return
|
|
74
|
+
except Exception as exc:
|
|
75
|
+
from tunacode.exceptions import ConfigurationError
|
|
76
|
+
|
|
77
|
+
if isinstance(exc, ConfigurationError):
|
|
78
|
+
print(f"Error: {exc}")
|
|
79
|
+
update_task.cancel()
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
import traceback
|
|
83
|
+
|
|
84
|
+
print(f"Error: {exc}\n\nTraceback:\n{traceback.format_exc()}")
|
|
85
|
+
update_task.cancel()
|
|
86
|
+
return
|
|
87
|
+
|
|
88
|
+
try:
|
|
89
|
+
has_update, latest_version = await update_task
|
|
90
|
+
if has_update:
|
|
91
|
+
print(f"Update available: {latest_version}")
|
|
92
|
+
except asyncio.CancelledError:
|
|
93
|
+
return
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _run_textual_cli(*, model: str | None, show_setup: bool) -> None:
|
|
97
|
+
asyncio.run(_run_textual_app(model=model, show_setup=show_setup))
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@app.callback(invoke_without_command=True)
|
|
101
|
+
def _default_command(
|
|
102
|
+
ctx: typer.Context,
|
|
103
|
+
version: bool = typer.Option(False, "--version", "-v", help="Show version and exit."),
|
|
104
|
+
setup: bool = typer.Option(False, "--setup", help="Run setup wizard"),
|
|
105
|
+
baseurl: str | None = typer.Option(None, "--baseurl", help=BASE_URL_HELP_TEXT),
|
|
106
|
+
model: str | None = typer.Option(
|
|
107
|
+
None, "--model", help="Default model to use (e.g., openai/gpt-4)"
|
|
108
|
+
),
|
|
109
|
+
_key: str = typer.Option(None, "--key", help="API key for the provider"), # noqa: ARG001
|
|
110
|
+
_context: int = typer.Option( # noqa: ARG001 - reserved for future use
|
|
111
|
+
None, "--context", help="Maximum context window size for custom models"
|
|
112
|
+
),
|
|
113
|
+
) -> None:
|
|
114
|
+
if version:
|
|
115
|
+
_print_version()
|
|
116
|
+
raise typer.Exit(code=0)
|
|
117
|
+
|
|
118
|
+
if ctx.invoked_subcommand is not None:
|
|
119
|
+
if setup:
|
|
120
|
+
raise typer.BadParameter("Use `tunacode --setup` without a subcommand.")
|
|
121
|
+
_apply_base_url_override(state_manager, baseurl)
|
|
122
|
+
if model:
|
|
123
|
+
state_manager.session.current_model = model
|
|
124
|
+
return
|
|
125
|
+
|
|
126
|
+
_apply_base_url_override(state_manager, baseurl)
|
|
127
|
+
_run_textual_cli(model=model, show_setup=setup)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@app.command(hidden=True)
|
|
131
|
+
def main(
|
|
132
|
+
version: bool = typer.Option(False, "--version", "-v", help="Show version and exit."),
|
|
133
|
+
baseurl: str | None = typer.Option(None, "--baseurl", help=BASE_URL_HELP_TEXT),
|
|
134
|
+
model: str | None = typer.Option(
|
|
135
|
+
None, "--model", help="Default model to use (e.g., openai/gpt-4)"
|
|
136
|
+
),
|
|
137
|
+
_key: str = typer.Option(None, "--key", help="API key for the provider"), # noqa: ARG001
|
|
138
|
+
_context: int = typer.Option( # noqa: ARG001 - reserved for future use
|
|
139
|
+
None, "--context", help="Maximum context window size for custom models"
|
|
140
|
+
),
|
|
141
|
+
setup: bool = typer.Option(False, "--setup", help="Run setup wizard"),
|
|
142
|
+
) -> None:
|
|
143
|
+
"""Deprecated alias for `tunacode`."""
|
|
144
|
+
if version:
|
|
145
|
+
_print_version()
|
|
146
|
+
raise typer.Exit(code=0)
|
|
147
|
+
|
|
148
|
+
_apply_base_url_override(state_manager, baseurl)
|
|
149
|
+
_run_textual_cli(model=model, show_setup=setup)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@app.command(name="run")
|
|
153
|
+
def run_headless(
|
|
154
|
+
prompt: str = typer.Argument(..., help="The prompt/instruction to execute"),
|
|
155
|
+
auto_approve: bool = typer.Option(
|
|
156
|
+
False, "--auto-approve", help="Skip tool authorization prompts"
|
|
157
|
+
),
|
|
158
|
+
output_json: bool = typer.Option(False, "--output-json", help="Output trajectory as JSON"),
|
|
159
|
+
timeout: int = typer.Option(
|
|
160
|
+
DEFAULT_TIMEOUT_SECONDS, "--timeout", help="Execution timeout in seconds"
|
|
161
|
+
),
|
|
162
|
+
cwd: str | None = typer.Option(None, "--cwd", help="Working directory for execution"),
|
|
163
|
+
baseurl: str | None = typer.Option(None, "--baseurl", help=BASE_URL_HELP_TEXT),
|
|
164
|
+
model: str | None = typer.Option(None, "--model", "-m", help="Model to use"),
|
|
165
|
+
) -> None:
|
|
166
|
+
"""Run TunaCode in non-interactive headless mode."""
|
|
167
|
+
from tunacode.core.agents.main import process_request
|
|
168
|
+
|
|
169
|
+
async def async_run() -> int:
|
|
170
|
+
# Change working directory if specified
|
|
171
|
+
if cwd:
|
|
172
|
+
if not os.path.isdir(cwd):
|
|
173
|
+
raise SystemExit(f"Invalid working directory: {cwd} (not a directory)")
|
|
174
|
+
if not os.access(cwd, os.R_OK | os.X_OK):
|
|
175
|
+
raise SystemExit(f"Inaccessible working directory: {cwd}")
|
|
176
|
+
os.chdir(cwd)
|
|
177
|
+
|
|
178
|
+
# Set model if provided
|
|
179
|
+
if model:
|
|
180
|
+
state_manager.session.current_model = model
|
|
181
|
+
|
|
182
|
+
_apply_base_url_override(state_manager, baseurl)
|
|
183
|
+
|
|
184
|
+
# Auto-approve mode (reuses existing yolo infrastructure)
|
|
185
|
+
if auto_approve:
|
|
186
|
+
state_manager.session.yolo = True
|
|
187
|
+
|
|
188
|
+
# Initialize tool handler
|
|
189
|
+
tool_handler = ToolHandler(state_manager)
|
|
190
|
+
state_manager.set_tool_handler(tool_handler)
|
|
191
|
+
|
|
192
|
+
try:
|
|
193
|
+
agent_run = await asyncio.wait_for(
|
|
194
|
+
process_request(
|
|
195
|
+
message=prompt,
|
|
196
|
+
model=state_manager.session.current_model,
|
|
197
|
+
state_manager=state_manager,
|
|
198
|
+
tool_callback=None,
|
|
199
|
+
streaming_callback=None,
|
|
200
|
+
tool_result_callback=None,
|
|
201
|
+
tool_start_callback=None,
|
|
202
|
+
),
|
|
203
|
+
timeout=timeout,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
if output_json:
|
|
207
|
+
trajectory = {
|
|
208
|
+
"messages": [_serialize_message(msg) for msg in state_manager.session.messages],
|
|
209
|
+
"tool_calls": state_manager.session.tool_calls,
|
|
210
|
+
"usage": state_manager.session.session_total_usage,
|
|
211
|
+
"success": True,
|
|
212
|
+
}
|
|
213
|
+
print(json.dumps(trajectory, indent=2))
|
|
214
|
+
return 0
|
|
215
|
+
|
|
216
|
+
headless_output = resolve_output(agent_run, state_manager.session.messages)
|
|
217
|
+
if headless_output is None:
|
|
218
|
+
print(HEADLESS_NO_RESPONSE_ERROR, file=sys.stderr)
|
|
219
|
+
return 1
|
|
220
|
+
|
|
221
|
+
print(headless_output)
|
|
222
|
+
|
|
223
|
+
return 0
|
|
224
|
+
|
|
225
|
+
except TimeoutError:
|
|
226
|
+
if output_json:
|
|
227
|
+
print(json.dumps({"success": False, "error": "timeout"}))
|
|
228
|
+
else:
|
|
229
|
+
print("Error: Execution timed out", file=sys.stderr)
|
|
230
|
+
return 1
|
|
231
|
+
except Exception as e:
|
|
232
|
+
if output_json:
|
|
233
|
+
print(json.dumps({"success": False, "error": str(e)}))
|
|
234
|
+
else:
|
|
235
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
236
|
+
return 1
|
|
237
|
+
|
|
238
|
+
exit_code = asyncio.run(async_run())
|
|
239
|
+
raise typer.Exit(code=exit_code)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _serialize_message(msg: object) -> dict:
|
|
243
|
+
"""Serialize a message object to a dictionary."""
|
|
244
|
+
if hasattr(msg, "model_dump"):
|
|
245
|
+
return msg.model_dump()
|
|
246
|
+
if hasattr(msg, "__dict__"):
|
|
247
|
+
return msg.__dict__
|
|
248
|
+
return {"content": str(msg)}
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
if __name__ == "__main__":
|
|
252
|
+
app()
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Rich content renderers for Textual TUI."""
|
|
2
|
+
|
|
3
|
+
from .errors import render_exception, render_tool_error, render_user_abort
|
|
4
|
+
from .panels import (
|
|
5
|
+
ErrorDisplayData,
|
|
6
|
+
RichPanelRenderer,
|
|
7
|
+
SearchResultData,
|
|
8
|
+
ToolDisplayData,
|
|
9
|
+
error_panel,
|
|
10
|
+
search_panel,
|
|
11
|
+
tool_panel,
|
|
12
|
+
tool_panel_smart,
|
|
13
|
+
)
|
|
14
|
+
from .search import (
|
|
15
|
+
CodeSearchResult,
|
|
16
|
+
FileSearchResult,
|
|
17
|
+
SearchDisplayRenderer,
|
|
18
|
+
code_search_panel,
|
|
19
|
+
file_search_panel,
|
|
20
|
+
quick_results,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"CodeSearchResult",
|
|
25
|
+
"ErrorDisplayData",
|
|
26
|
+
"FileSearchResult",
|
|
27
|
+
"RichPanelRenderer",
|
|
28
|
+
"SearchDisplayRenderer",
|
|
29
|
+
"SearchResultData",
|
|
30
|
+
"ToolDisplayData",
|
|
31
|
+
"code_search_panel",
|
|
32
|
+
"error_panel",
|
|
33
|
+
"file_search_panel",
|
|
34
|
+
"quick_results",
|
|
35
|
+
"render_exception",
|
|
36
|
+
"render_tool_error",
|
|
37
|
+
"render_user_abort",
|
|
38
|
+
"search_panel",
|
|
39
|
+
"tool_panel",
|
|
40
|
+
"tool_panel_smart",
|
|
41
|
+
]
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""Error Panel System for Textual TUI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from rich.console import RenderableType
|
|
6
|
+
|
|
7
|
+
from tunacode.ui.renderers.panels import ErrorDisplayData, RichPanelRenderer
|
|
8
|
+
|
|
9
|
+
ERROR_SEVERITY_MAP: dict[str, str] = {
|
|
10
|
+
"ToolExecutionError": "error",
|
|
11
|
+
"FileOperationError": "error",
|
|
12
|
+
"AgentError": "error",
|
|
13
|
+
"GitOperationError": "error",
|
|
14
|
+
"GlobalRequestTimeoutError": "error",
|
|
15
|
+
"ToolBatchingJSONError": "error",
|
|
16
|
+
"ConfigurationError": "warning",
|
|
17
|
+
"ModelConfigurationError": "warning",
|
|
18
|
+
"ValidationError": "warning",
|
|
19
|
+
"SetupValidationError": "warning",
|
|
20
|
+
"UserAbortError": "info",
|
|
21
|
+
"StateError": "info",
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
EXCEPTION_CONTEXT_ATTRS: dict[str, str] = {
|
|
25
|
+
"tool_name": "Tool",
|
|
26
|
+
"path": "Path",
|
|
27
|
+
"operation": "Operation",
|
|
28
|
+
"server_name": "Server",
|
|
29
|
+
"model": "Model",
|
|
30
|
+
"step": "Step",
|
|
31
|
+
"validation_type": "Validation",
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _extract_exception_context(exc: Exception) -> dict[str, str]:
|
|
36
|
+
context: dict[str, str] = {}
|
|
37
|
+
for attr, label in EXCEPTION_CONTEXT_ATTRS.items():
|
|
38
|
+
if hasattr(exc, attr):
|
|
39
|
+
value = getattr(exc, attr)
|
|
40
|
+
if value is not None:
|
|
41
|
+
context[label] = str(value)
|
|
42
|
+
return context
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
DEFAULT_RECOVERY_COMMANDS: dict[str, list[str]] = {
|
|
46
|
+
"ConfigurationError": [
|
|
47
|
+
"tunacode --setup # Run setup wizard",
|
|
48
|
+
"cat ~/.tunacode/tunacode.json # Check config",
|
|
49
|
+
],
|
|
50
|
+
"ModelConfigurationError": [
|
|
51
|
+
"/model # List available models",
|
|
52
|
+
"tunacode --setup # Reconfigure",
|
|
53
|
+
],
|
|
54
|
+
"FileOperationError": [
|
|
55
|
+
"ls -la <path> # Check permissions",
|
|
56
|
+
"pwd # Verify current directory",
|
|
57
|
+
],
|
|
58
|
+
"GitOperationError": [
|
|
59
|
+
"git status # Check repository state",
|
|
60
|
+
"git stash # Stash uncommitted changes",
|
|
61
|
+
],
|
|
62
|
+
"GlobalRequestTimeoutError": [
|
|
63
|
+
"Check network connectivity",
|
|
64
|
+
"Increase timeout in tunacode.json",
|
|
65
|
+
],
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def render_exception(exc: Exception) -> RenderableType:
|
|
70
|
+
error_type = type(exc).__name__
|
|
71
|
+
severity = ERROR_SEVERITY_MAP.get(error_type, "error")
|
|
72
|
+
|
|
73
|
+
suggested_fix = getattr(exc, "suggested_fix", None)
|
|
74
|
+
recovery_commands = getattr(exc, "recovery_commands", None)
|
|
75
|
+
|
|
76
|
+
context = _extract_exception_context(exc)
|
|
77
|
+
|
|
78
|
+
if not recovery_commands:
|
|
79
|
+
recovery_commands = DEFAULT_RECOVERY_COMMANDS.get(error_type)
|
|
80
|
+
|
|
81
|
+
message = str(exc)
|
|
82
|
+
for prefix in ("Fix: ", "Suggested fix: ", "Recovery commands:"):
|
|
83
|
+
if prefix in message:
|
|
84
|
+
message = message.split(prefix)[0].strip()
|
|
85
|
+
|
|
86
|
+
data = ErrorDisplayData(
|
|
87
|
+
error_type=error_type,
|
|
88
|
+
message=message,
|
|
89
|
+
suggested_fix=suggested_fix,
|
|
90
|
+
recovery_commands=recovery_commands,
|
|
91
|
+
context=context if context else None,
|
|
92
|
+
severity=severity,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
return RichPanelRenderer.render_error(data)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def render_tool_error(
|
|
99
|
+
tool_name: str,
|
|
100
|
+
message: str,
|
|
101
|
+
suggested_fix: str | None = None,
|
|
102
|
+
file_path: str | None = None,
|
|
103
|
+
) -> RenderableType:
|
|
104
|
+
context = {}
|
|
105
|
+
if file_path:
|
|
106
|
+
context["Path"] = file_path
|
|
107
|
+
recovery_commands = [
|
|
108
|
+
f"Check file exists: ls -la {file_path}",
|
|
109
|
+
"Try with different arguments",
|
|
110
|
+
]
|
|
111
|
+
else:
|
|
112
|
+
recovery_commands = ["Try with different arguments"]
|
|
113
|
+
|
|
114
|
+
data = ErrorDisplayData(
|
|
115
|
+
error_type=f"{tool_name} Error",
|
|
116
|
+
message=message,
|
|
117
|
+
suggested_fix=suggested_fix,
|
|
118
|
+
recovery_commands=recovery_commands,
|
|
119
|
+
context=context if context else None,
|
|
120
|
+
severity="error",
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
return RichPanelRenderer.render_error(data)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def render_validation_error(
|
|
127
|
+
field: str,
|
|
128
|
+
message: str,
|
|
129
|
+
valid_examples: list[str] | None = None,
|
|
130
|
+
) -> RenderableType:
|
|
131
|
+
suggested_fix = None
|
|
132
|
+
if valid_examples:
|
|
133
|
+
suggested_fix = f"Valid examples: {', '.join(valid_examples[:3])}"
|
|
134
|
+
|
|
135
|
+
data = ErrorDisplayData(
|
|
136
|
+
error_type="Validation Error",
|
|
137
|
+
message=f"{field}: {message}",
|
|
138
|
+
suggested_fix=suggested_fix,
|
|
139
|
+
context={"Field": field},
|
|
140
|
+
severity="warning",
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
return RichPanelRenderer.render_error(data)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def render_connection_error(
|
|
147
|
+
service: str,
|
|
148
|
+
message: str,
|
|
149
|
+
retry_available: bool = True,
|
|
150
|
+
) -> RenderableType:
|
|
151
|
+
recovery = []
|
|
152
|
+
if retry_available:
|
|
153
|
+
recovery.append("Retry the operation")
|
|
154
|
+
recovery.extend(
|
|
155
|
+
[
|
|
156
|
+
"Check network connectivity",
|
|
157
|
+
f"Verify {service} service status",
|
|
158
|
+
]
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
data = ErrorDisplayData(
|
|
162
|
+
error_type=f"{service} Connection Error",
|
|
163
|
+
message=message,
|
|
164
|
+
recovery_commands=recovery,
|
|
165
|
+
severity="error",
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
return RichPanelRenderer.render_error(data)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def render_user_abort() -> RenderableType:
|
|
172
|
+
data = ErrorDisplayData(
|
|
173
|
+
error_type="Operation Cancelled",
|
|
174
|
+
message="User cancelled the operation",
|
|
175
|
+
severity="info",
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
return RichPanelRenderer.render_error(data)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def render_catastrophic_error(exc: Exception, context: str | None = None) -> RenderableType:
|
|
182
|
+
"""Render a user-friendly error when something goes very wrong.
|
|
183
|
+
|
|
184
|
+
This is the catch-all error display for unexpected failures.
|
|
185
|
+
Shows a clear message asking the user to try again.
|
|
186
|
+
"""
|
|
187
|
+
error_details = str(exc)[:200] if str(exc) else type(exc).__name__
|
|
188
|
+
|
|
189
|
+
data = ErrorDisplayData(
|
|
190
|
+
error_type="Something Went Wrong",
|
|
191
|
+
message="An unexpected error occurred. Please try again.",
|
|
192
|
+
suggested_fix="If this persists, check the logs or report the issue.",
|
|
193
|
+
context={"Details": error_details} if error_details else None,
|
|
194
|
+
severity="error",
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
return RichPanelRenderer.render_error(data)
|