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
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)