tunacode-cli 0.0.55__py3-none-any.whl → 0.0.78.6__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 (114) hide show
  1. tunacode/cli/commands/__init__.py +2 -2
  2. tunacode/cli/commands/implementations/__init__.py +2 -3
  3. tunacode/cli/commands/implementations/command_reload.py +48 -0
  4. tunacode/cli/commands/implementations/debug.py +2 -2
  5. tunacode/cli/commands/implementations/development.py +10 -8
  6. tunacode/cli/commands/implementations/model.py +357 -29
  7. tunacode/cli/commands/implementations/quickstart.py +43 -0
  8. tunacode/cli/commands/implementations/system.py +96 -3
  9. tunacode/cli/commands/implementations/template.py +0 -2
  10. tunacode/cli/commands/registry.py +139 -5
  11. tunacode/cli/commands/slash/__init__.py +32 -0
  12. tunacode/cli/commands/slash/command.py +157 -0
  13. tunacode/cli/commands/slash/loader.py +135 -0
  14. tunacode/cli/commands/slash/processor.py +294 -0
  15. tunacode/cli/commands/slash/types.py +93 -0
  16. tunacode/cli/commands/slash/validator.py +400 -0
  17. tunacode/cli/main.py +23 -2
  18. tunacode/cli/repl.py +217 -190
  19. tunacode/cli/repl_components/command_parser.py +38 -4
  20. tunacode/cli/repl_components/error_recovery.py +85 -4
  21. tunacode/cli/repl_components/output_display.py +12 -1
  22. tunacode/cli/repl_components/tool_executor.py +1 -1
  23. tunacode/configuration/defaults.py +12 -3
  24. tunacode/configuration/key_descriptions.py +284 -0
  25. tunacode/configuration/settings.py +0 -1
  26. tunacode/constants.py +12 -40
  27. tunacode/core/agents/__init__.py +43 -2
  28. tunacode/core/agents/agent_components/__init__.py +7 -0
  29. tunacode/core/agents/agent_components/agent_config.py +249 -55
  30. tunacode/core/agents/agent_components/agent_helpers.py +43 -13
  31. tunacode/core/agents/agent_components/node_processor.py +179 -139
  32. tunacode/core/agents/agent_components/response_state.py +123 -6
  33. tunacode/core/agents/agent_components/state_transition.py +116 -0
  34. tunacode/core/agents/agent_components/streaming.py +296 -0
  35. tunacode/core/agents/agent_components/task_completion.py +19 -6
  36. tunacode/core/agents/agent_components/tool_buffer.py +21 -1
  37. tunacode/core/agents/agent_components/tool_executor.py +10 -0
  38. tunacode/core/agents/main.py +522 -370
  39. tunacode/core/agents/main_legact.py +538 -0
  40. tunacode/core/agents/prompts.py +66 -0
  41. tunacode/core/agents/utils.py +29 -121
  42. tunacode/core/code_index.py +83 -29
  43. tunacode/core/setup/__init__.py +0 -2
  44. tunacode/core/setup/config_setup.py +110 -20
  45. tunacode/core/setup/config_wizard.py +230 -0
  46. tunacode/core/setup/coordinator.py +14 -5
  47. tunacode/core/state.py +16 -20
  48. tunacode/core/token_usage/usage_tracker.py +5 -3
  49. tunacode/core/tool_authorization.py +352 -0
  50. tunacode/core/tool_handler.py +67 -40
  51. tunacode/exceptions.py +119 -5
  52. tunacode/prompts/system.xml +751 -0
  53. tunacode/services/mcp.py +125 -7
  54. tunacode/setup.py +5 -25
  55. tunacode/tools/base.py +163 -0
  56. tunacode/tools/bash.py +110 -1
  57. tunacode/tools/glob.py +332 -34
  58. tunacode/tools/grep.py +179 -82
  59. tunacode/tools/grep_components/result_formatter.py +98 -4
  60. tunacode/tools/list_dir.py +132 -2
  61. tunacode/tools/prompts/bash_prompt.xml +72 -0
  62. tunacode/tools/prompts/glob_prompt.xml +45 -0
  63. tunacode/tools/prompts/grep_prompt.xml +98 -0
  64. tunacode/tools/prompts/list_dir_prompt.xml +31 -0
  65. tunacode/tools/prompts/react_prompt.xml +23 -0
  66. tunacode/tools/prompts/read_file_prompt.xml +54 -0
  67. tunacode/tools/prompts/run_command_prompt.xml +64 -0
  68. tunacode/tools/prompts/update_file_prompt.xml +53 -0
  69. tunacode/tools/prompts/write_file_prompt.xml +37 -0
  70. tunacode/tools/react.py +153 -0
  71. tunacode/tools/read_file.py +91 -0
  72. tunacode/tools/run_command.py +114 -0
  73. tunacode/tools/schema_assembler.py +167 -0
  74. tunacode/tools/update_file.py +94 -0
  75. tunacode/tools/write_file.py +86 -0
  76. tunacode/tools/xml_helper.py +83 -0
  77. tunacode/tutorial/__init__.py +9 -0
  78. tunacode/tutorial/content.py +98 -0
  79. tunacode/tutorial/manager.py +182 -0
  80. tunacode/tutorial/steps.py +124 -0
  81. tunacode/types.py +20 -27
  82. tunacode/ui/completers.py +434 -50
  83. tunacode/ui/config_dashboard.py +585 -0
  84. tunacode/ui/console.py +63 -11
  85. tunacode/ui/input.py +20 -3
  86. tunacode/ui/keybindings.py +7 -4
  87. tunacode/ui/model_selector.py +395 -0
  88. tunacode/ui/output.py +40 -19
  89. tunacode/ui/panels.py +212 -43
  90. tunacode/ui/path_heuristics.py +91 -0
  91. tunacode/ui/prompt_manager.py +5 -1
  92. tunacode/ui/tool_ui.py +33 -10
  93. tunacode/utils/api_key_validation.py +93 -0
  94. tunacode/utils/config_comparator.py +340 -0
  95. tunacode/utils/json_utils.py +206 -0
  96. tunacode/utils/message_utils.py +14 -4
  97. tunacode/utils/models_registry.py +593 -0
  98. tunacode/utils/ripgrep.py +332 -9
  99. tunacode/utils/text_utils.py +18 -1
  100. tunacode/utils/user_configuration.py +45 -0
  101. tunacode_cli-0.0.78.6.dist-info/METADATA +260 -0
  102. tunacode_cli-0.0.78.6.dist-info/RECORD +158 -0
  103. {tunacode_cli-0.0.55.dist-info → tunacode_cli-0.0.78.6.dist-info}/WHEEL +1 -2
  104. tunacode/cli/commands/implementations/todo.py +0 -217
  105. tunacode/context.py +0 -71
  106. tunacode/core/setup/git_safety_setup.py +0 -182
  107. tunacode/prompts/system.md +0 -731
  108. tunacode/tools/read_file_async_poc.py +0 -196
  109. tunacode/tools/todo.py +0 -349
  110. tunacode_cli-0.0.55.dist-info/METADATA +0 -322
  111. tunacode_cli-0.0.55.dist-info/RECORD +0 -126
  112. tunacode_cli-0.0.55.dist-info/top_level.txt +0 -1
  113. {tunacode_cli-0.0.55.dist-info → tunacode_cli-0.0.78.6.dist-info}/entry_points.txt +0 -0
  114. {tunacode_cli-0.0.55.dist-info → tunacode_cli-0.0.78.6.dist-info}/licenses/LICENSE +0 -0
tunacode/ui/input.py CHANGED
@@ -76,6 +76,13 @@ async def multiline_input(
76
76
  ) -> str:
77
77
  """Get multiline input from the user with @file completion and highlighting."""
78
78
  kb = create_key_bindings(state_manager)
79
+
80
+ # Clear any residual terminal output
81
+ import sys
82
+
83
+ sys.stdout.flush()
84
+
85
+ # Full placeholder with all keyboard shortcuts
79
86
  placeholder = formatted_text(
80
87
  (
81
88
  "<darkgrey>"
@@ -86,13 +93,23 @@ async def multiline_input(
86
93
  "</darkgrey>"
87
94
  )
88
95
  )
89
- return await input(
96
+
97
+ # Create models registry for auto-completion (lazy loaded)
98
+ from ..utils.models_registry import ModelsRegistry
99
+
100
+ models_registry = ModelsRegistry()
101
+ # Note: Registry will be loaded lazily by the completer when needed
102
+
103
+ # Display input area
104
+ result = await input(
90
105
  "multiline",
91
- pretext="> ", # Default prompt
106
+ pretext="> ",
92
107
  key_bindings=kb,
93
108
  multiline=True,
94
109
  placeholder=placeholder,
95
- completer=create_completer(command_registry),
110
+ completer=create_completer(command_registry, models_registry),
96
111
  lexer=FileReferenceLexer(),
97
112
  state_manager=state_manager,
98
113
  )
114
+
115
+ return result
@@ -30,8 +30,8 @@ def create_key_bindings(state_manager: StateManager = None) -> KeyBindings:
30
30
 
31
31
  @kb.add("escape")
32
32
  def _escape(event):
33
- """Handle ESC key - raises KeyboardInterrupt for unified abort handling."""
34
- logger.debug("ESC key pressed - raising KeyboardInterrupt")
33
+ """Handle ESC key - trigger Ctrl+C behavior."""
34
+ logger.debug("ESC key pressed - simulating Ctrl+C")
35
35
 
36
36
  # Cancel any active task if present
37
37
  if state_manager and hasattr(state_manager.session, "current_task"):
@@ -44,7 +44,10 @@ def create_key_bindings(state_manager: StateManager = None) -> KeyBindings:
44
44
  except Exception as e:
45
45
  logger.debug(f"Failed to cancel task: {e}")
46
46
 
47
- # Raise KeyboardInterrupt to trigger unified abort handling in REPL
48
- raise KeyboardInterrupt()
47
+ # Trigger the same behavior as Ctrl+C by sending the signal
48
+ import os
49
+ import signal
50
+
51
+ os.kill(os.getpid(), signal.SIGINT)
49
52
 
50
53
  return kb
@@ -0,0 +1,395 @@
1
+ """Interactive model selector UI component."""
2
+
3
+ from typing import List, Optional
4
+
5
+ from prompt_toolkit.application import Application
6
+ from prompt_toolkit.buffer import Buffer
7
+ from prompt_toolkit.formatted_text import HTML, StyleAndTextTuples
8
+ from prompt_toolkit.key_binding import KeyBindings
9
+ from prompt_toolkit.layout import (
10
+ FormattedTextControl,
11
+ HSplit,
12
+ Layout,
13
+ VSplit,
14
+ Window,
15
+ WindowAlign,
16
+ )
17
+ from prompt_toolkit.layout.controls import BufferControl
18
+ from prompt_toolkit.layout.dimension import Dimension
19
+ from prompt_toolkit.search import SearchState
20
+ from prompt_toolkit.styles import Style
21
+ from prompt_toolkit.widgets import Frame
22
+
23
+ from ..utils.models_registry import ModelInfo, ModelsRegistry
24
+
25
+
26
+ class ModelSelector:
27
+ """Interactive model selector with search and navigation."""
28
+
29
+ def __init__(self, registry: ModelsRegistry):
30
+ """Initialize the model selector."""
31
+ self.registry = registry
32
+ self.models: List[ModelInfo] = []
33
+ self.filtered_models: List[ModelInfo] = []
34
+ self.selected_index = 0
35
+ self.search_text = ""
36
+ self.selected_model: Optional[ModelInfo] = None
37
+
38
+ # Create key bindings
39
+ self.kb = self._create_key_bindings()
40
+
41
+ # Create search buffer
42
+ self.search_buffer = Buffer(on_text_changed=self._on_search_changed)
43
+
44
+ # Search state
45
+ self.search_state = SearchState()
46
+
47
+ def _create_key_bindings(self) -> KeyBindings:
48
+ """Create key bindings for the selector."""
49
+ kb = KeyBindings()
50
+
51
+ @kb.add("up", "k")
52
+ def move_up(event):
53
+ """Move selection up."""
54
+ if self.selected_index > 0:
55
+ self.selected_index -= 1
56
+ self._update_display()
57
+
58
+ @kb.add("down", "j")
59
+ def move_down(event):
60
+ """Move selection down."""
61
+ if self.selected_index < len(self.filtered_models) - 1:
62
+ self.selected_index += 1
63
+ self._update_display()
64
+
65
+ @kb.add("pageup")
66
+ def page_up(event):
67
+ """Move selection up by page."""
68
+ self.selected_index = max(0, self.selected_index - 10)
69
+ self._update_display()
70
+
71
+ @kb.add("pagedown")
72
+ def page_down(event):
73
+ """Move selection down by page."""
74
+ self.selected_index = min(len(self.filtered_models) - 1, self.selected_index + 10)
75
+ self._update_display()
76
+
77
+ @kb.add("enter")
78
+ def select_model(event):
79
+ """Select the current model."""
80
+ if 0 <= self.selected_index < len(self.filtered_models):
81
+ self.selected_model = self.filtered_models[self.selected_index]
82
+ event.app.exit(result=self.selected_model)
83
+
84
+ @kb.add("c-c", "escape", "q")
85
+ def cancel(event):
86
+ """Cancel selection."""
87
+ event.app.exit(result=None)
88
+
89
+ @kb.add("/")
90
+ def focus_search(event):
91
+ """Focus the search input."""
92
+ event.app.layout.focus(self.search_buffer)
93
+
94
+ @kb.add("tab")
95
+ def next_provider(event):
96
+ """Jump to next provider."""
97
+ if not self.filtered_models:
98
+ return
99
+
100
+ current_provider = self.filtered_models[self.selected_index].provider
101
+ for i in range(self.selected_index + 1, len(self.filtered_models)):
102
+ if self.filtered_models[i].provider != current_provider:
103
+ self.selected_index = i
104
+ self._update_display()
105
+ break
106
+
107
+ @kb.add("s-tab")
108
+ def prev_provider(event):
109
+ """Jump to previous provider."""
110
+ if not self.filtered_models:
111
+ return
112
+
113
+ current_provider = self.filtered_models[self.selected_index].provider
114
+ for i in range(self.selected_index - 1, -1, -1):
115
+ if self.filtered_models[i].provider != current_provider:
116
+ self.selected_index = i
117
+ self._update_display()
118
+ break
119
+
120
+ return kb
121
+
122
+ def _on_search_changed(self, buffer: Buffer) -> None:
123
+ """Handle search text changes."""
124
+ self.search_text = buffer.text
125
+ self._filter_models()
126
+ self._update_display()
127
+
128
+ def _filter_models(self) -> None:
129
+ """Filter models based on search text."""
130
+ if not self.search_text:
131
+ self.filtered_models = self.models.copy()
132
+ else:
133
+ # Search and sort by relevance
134
+ self.filtered_models = self.registry.search_models(self.search_text)
135
+
136
+ # Reset selection
137
+ self.selected_index = 0 if self.filtered_models else -1
138
+
139
+ def _get_model_lines(self) -> List[StyleAndTextTuples]:
140
+ """Get formatted lines for model display."""
141
+ lines = []
142
+
143
+ if not self.filtered_models:
144
+ lines.append([("class:muted", "No models found")])
145
+ return lines
146
+
147
+ # Group models by provider
148
+ current_provider = None
149
+ for i, model in enumerate(self.filtered_models):
150
+ # Add provider header if changed
151
+ if model.provider != current_provider:
152
+ if current_provider is not None:
153
+ lines.append([]) # Empty line between providers
154
+
155
+ provider_info = self.registry.providers.get(model.provider)
156
+ provider_name = provider_info.name if provider_info else model.provider
157
+ lines.append([("class:provider", f"▼ {provider_name}")])
158
+ current_provider = model.provider
159
+
160
+ # Model line
161
+ is_selected = i == self.selected_index
162
+
163
+ # Build model display
164
+ parts = []
165
+
166
+ # Selection indicator
167
+ if is_selected:
168
+ parts.append(("class:selected", "→ "))
169
+ else:
170
+ parts.append(("", " "))
171
+
172
+ # Model ID and name
173
+ parts.append(
174
+ ("class:model-id" if not is_selected else "class:selected-id", f"{model.id}")
175
+ )
176
+ parts.append(("class:muted", " - "))
177
+ parts.append(
178
+ ("class:model-name" if not is_selected else "class:selected-name", model.name)
179
+ )
180
+
181
+ # Cost and limits
182
+ details = []
183
+ if model.cost.input is not None:
184
+ details.append(f"${model.cost.input}/{model.cost.output}")
185
+ if model.limits.context:
186
+ details.append(f"{model.limits.context // 1000}k")
187
+
188
+ if details:
189
+ parts.append(("class:muted", f" ({', '.join(details)})"))
190
+
191
+ # Capabilities badges
192
+ badges = []
193
+ if model.capabilities.attachment:
194
+ badges.append("📎")
195
+ if model.capabilities.reasoning:
196
+ badges.append("🧠")
197
+ if model.capabilities.tool_call:
198
+ badges.append("🔧")
199
+
200
+ if badges:
201
+ parts.append(("class:badges", " " + "".join(badges)))
202
+
203
+ lines.append(parts)
204
+
205
+ return lines
206
+
207
+ def _get_details_panel(self) -> StyleAndTextTuples:
208
+ """Get the details panel content for selected model."""
209
+ if not self.filtered_models or self.selected_index < 0:
210
+ return [("", "Select a model to see details")]
211
+
212
+ model = self.filtered_models[self.selected_index]
213
+ lines = []
214
+
215
+ # Model name and ID
216
+ lines.append([("class:title", model.name)])
217
+ lines.append([("class:muted", f"{model.full_id}")])
218
+ lines.append([])
219
+
220
+ # Pricing
221
+ lines.append([("class:section", "Pricing:")])
222
+ if model.cost.input is not None:
223
+ lines.append([("", f" Input: ${model.cost.input} per 1M tokens")])
224
+ lines.append([("", f" Output: ${model.cost.output} per 1M tokens")])
225
+ else:
226
+ lines.append([("class:muted", " Not available")])
227
+ lines.append([])
228
+
229
+ # Limits
230
+ lines.append([("class:section", "Limits:")])
231
+ if model.limits.context:
232
+ lines.append([("", f" Context: {model.limits.context:,} tokens")])
233
+ if model.limits.output:
234
+ lines.append([("", f" Output: {model.limits.output:,} tokens")])
235
+ if not model.limits.context and not model.limits.output:
236
+ lines.append([("class:muted", " Not specified")])
237
+ lines.append([])
238
+
239
+ # Capabilities
240
+ lines.append([("class:section", "Capabilities:")])
241
+ caps = []
242
+ if model.capabilities.attachment:
243
+ caps.append("Attachments")
244
+ if model.capabilities.reasoning:
245
+ caps.append("Reasoning")
246
+ if model.capabilities.tool_call:
247
+ caps.append("Tool calling")
248
+ if model.capabilities.temperature:
249
+ caps.append("Temperature control")
250
+
251
+ if caps:
252
+ for cap in caps:
253
+ lines.append([("", f" ✓ {cap}")])
254
+ else:
255
+ lines.append([("class:muted", " Basic text generation")])
256
+
257
+ if model.capabilities.knowledge:
258
+ lines.append([])
259
+ lines.append([("class:section", "Knowledge cutoff:")])
260
+ lines.append([("", f" {model.capabilities.knowledge}")])
261
+
262
+ # Modalities
263
+ if model.modalities:
264
+ lines.append([])
265
+ lines.append([("class:section", "Modalities:")])
266
+ if "input" in model.modalities:
267
+ lines.append([("", f" Input: {', '.join(model.modalities['input'])}")])
268
+ if "output" in model.modalities:
269
+ lines.append([("", f" Output: {', '.join(model.modalities['output'])}")])
270
+
271
+ return lines
272
+
273
+ def _update_display(self) -> None:
274
+ """Update the display (called on changes)."""
275
+ # This will trigger a redraw through prompt_toolkit's event system
276
+ if hasattr(self, "app"):
277
+ self.app.invalidate()
278
+
279
+ def _create_layout(self) -> Layout:
280
+ """Create the application layout."""
281
+ # Model list
282
+ model_list = FormattedTextControl(self._get_model_lines, focusable=False, show_cursor=False)
283
+
284
+ model_window = Window(
285
+ content=model_list,
286
+ width=Dimension(min=40, preferred=60),
287
+ height=Dimension(min=10, preferred=20),
288
+ scroll_offsets=True,
289
+ wrap_lines=False,
290
+ )
291
+
292
+ # Details panel
293
+ details_control = FormattedTextControl(
294
+ self._get_details_panel, focusable=False, show_cursor=False
295
+ )
296
+
297
+ details_window = Window(
298
+ content=details_control, width=Dimension(min=30, preferred=40), wrap_lines=True
299
+ )
300
+
301
+ # Search bar
302
+ search_field = Window(
303
+ BufferControl(buffer=self.search_buffer, focus_on_click=True), height=1
304
+ )
305
+
306
+ search_label = Window(
307
+ FormattedTextControl(HTML("<b>Search:</b> ")), width=8, height=1, dont_extend_width=True
308
+ )
309
+
310
+ search_bar = VSplit([search_label, search_field])
311
+
312
+ # Help text
313
+ help_text = Window(
314
+ FormattedTextControl(
315
+ HTML(
316
+ "<muted>↑↓: Navigate | Enter: Select | /: Search | Tab: Next provider | "
317
+ "Esc: Cancel</muted>"
318
+ )
319
+ ),
320
+ height=1,
321
+ align=WindowAlign.CENTER,
322
+ )
323
+
324
+ # Main content
325
+ content = VSplit(
326
+ [Frame(model_window, title="Select Model"), Frame(details_window, title="Details")]
327
+ )
328
+
329
+ # Root layout
330
+ root = HSplit(
331
+ [
332
+ search_bar,
333
+ Window(height=1), # Spacer
334
+ content,
335
+ Window(height=1), # Spacer
336
+ help_text,
337
+ ]
338
+ )
339
+
340
+ return Layout(root)
341
+
342
+ async def select_model(self, initial_query: str = "") -> Optional[ModelInfo]:
343
+ """Show the model selector and return selected model."""
344
+ # Load all models
345
+ self.models = list(self.registry.models.values())
346
+ self.search_buffer.text = initial_query
347
+
348
+ # Filter initially
349
+ self._filter_models()
350
+
351
+ # Create application
352
+ self.app = Application(
353
+ layout=self._create_layout(),
354
+ key_bindings=self.kb,
355
+ mouse_support=True,
356
+ full_screen=False,
357
+ style=self._get_style(),
358
+ )
359
+
360
+ # Run the selector
361
+ result = await self.app.run_async()
362
+ return result
363
+
364
+ def _get_style(self) -> Style:
365
+ """Get the style for the selector."""
366
+ return Style.from_dict(
367
+ {
368
+ "provider": "bold cyan",
369
+ "model-id": "white",
370
+ "model-name": "ansiwhite",
371
+ "selected": "reverse bold",
372
+ "selected-id": "reverse bold white",
373
+ "selected-name": "reverse bold ansiwhite",
374
+ "muted": "gray",
375
+ "badges": "yellow",
376
+ "title": "bold ansiwhite",
377
+ "section": "bold cyan",
378
+ }
379
+ )
380
+
381
+
382
+ async def select_model_interactive(
383
+ registry: Optional[ModelsRegistry] = None, initial_query: str = ""
384
+ ) -> Optional[str]:
385
+ """Show interactive model selector and return selected model ID."""
386
+ if registry is None:
387
+ registry = ModelsRegistry()
388
+ await registry.load()
389
+
390
+ selector = ModelSelector(registry)
391
+ model = await selector.select_model(initial_query)
392
+
393
+ if model:
394
+ return model.full_id
395
+ return None
tunacode/ui/output.py CHANGED
@@ -1,12 +1,10 @@
1
1
  """Output and display functions for TunaCode UI."""
2
2
 
3
- from typing import Optional
3
+ from typing import TYPE_CHECKING, Any, Optional
4
4
 
5
5
  from prompt_toolkit.application import run_in_terminal
6
- from rich.console import Console
7
6
  from rich.padding import Padding
8
7
 
9
- from tunacode.configuration.settings import ApplicationSettings
10
8
  from tunacode.constants import (
11
9
  MSG_UPDATE_AVAILABLE,
12
10
  MSG_UPDATE_INSTRUCTION,
@@ -22,27 +20,48 @@ from .constants import SPINNER_TYPE
22
20
  from .decorators import create_sync_wrapper
23
21
  from .logging_compat import ui_logger
24
22
 
25
- # Create console with explicit settings to ensure ANSI codes work properly
26
- console = Console(force_terminal=True, legacy_windows=False)
23
+ # Lazy console initialization
24
+ if TYPE_CHECKING:
25
+ from rich.console import Console
26
+
27
+ _console: Optional["Console"] = None
28
+
29
+
30
+ def get_console() -> "Console":
31
+ """Get console instance lazily."""
32
+ from rich.console import Console
33
+
34
+ global _console
35
+ if _console is None:
36
+ _console = Console()
37
+ return _console
38
+
39
+
40
+ class _LazyConsole:
41
+ """Lightweight proxy that defers Console creation."""
42
+
43
+ def __getattr__(self, name: str) -> Any:
44
+ return getattr(get_console(), name)
45
+
46
+ def __str__(self) -> str:
47
+ return str(get_console())
48
+
49
+
50
+ console = _LazyConsole()
27
51
  colors = DotDict(UI_COLORS)
28
52
 
29
53
  BANNER = """[bold cyan]
30
- ████████╗██╗ ██╗███╗ ██╗ █████╗
31
- ╚══██╔══╝██║ ██║████╗ ██║██╔══██╗
32
- ██║ ██║ ██║██╔██╗ ██║███████║
33
- ██║ ██║ ██║██║╚██╗██║██╔══██║
34
- ██║ ╚██████╔╝██║ ╚████║██║ ██║
35
- ╚═╝ ╚═════╝ ╚═╝ ╚═══╝╚═╝ ╚═╝
36
-
37
- ██████╗ ██████╗ ██████╗ ███████╗ dev
38
- ██╔════╝██╔═══██╗██╔══██╗██╔════╝
39
- ██║ ██║ ██║██║ ██║█████╗
40
- ██║ ██║ ██║██║ ██║██╔══╝
41
- ╚██████╗╚██████╔╝██████╔╝███████╗
42
- ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝
54
+ ▐█████▌
55
+ ▐█▛█▛█▛█▛█▛█▌
56
+ ▐█████████████▌
57
+ ▐██ ██▌
58
+ ▐█████████████▌
59
+ ▐█▛█▛█▛█▛█▛█▌
60
+ ▐█████▌
61
+ TunaCode
43
62
  [/bold cyan]
44
63
 
45
- ● Caution: This tool can modify your codebase - always use git branches"""
64
+ """
46
65
 
47
66
 
48
67
  @create_sync_wrapper
@@ -83,6 +102,8 @@ async def usage(usage: str) -> None:
83
102
 
84
103
  async def version() -> None:
85
104
  """Print version information."""
105
+ from tunacode.configuration.settings import ApplicationSettings
106
+
86
107
  app_settings = ApplicationSettings()
87
108
  await info(MSG_VERSION_DISPLAY.format(version=app_settings.version))
88
109