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
@@ -0,0 +1,585 @@
1
+ """
2
+ Interactive configuration dashboard UI component.
3
+
4
+ Provides terminal-based visualization of configuration state with
5
+ navigation, filtering, and detailed inspection capabilities.
6
+ """
7
+
8
+ from dataclasses import dataclass
9
+ from typing import Any, Dict, List, Optional
10
+
11
+ from rich.box import ROUNDED
12
+ from rich.console import Console, Group
13
+ from rich.layout import Layout
14
+ from rich.live import Live
15
+ from rich.panel import Panel
16
+ from rich.table import Table
17
+ from rich.text import Text
18
+ from rich.tree import Tree
19
+ from textual.containers import Vertical
20
+
21
+ from tunacode.utils.config_comparator import (
22
+ ConfigAnalysis,
23
+ ConfigComparator,
24
+ ConfigDifference,
25
+ create_config_report,
26
+ )
27
+
28
+
29
+ @dataclass
30
+ class DashboardConfig:
31
+ """Configuration for dashboard behavior."""
32
+
33
+ show_defaults: bool = True
34
+ show_custom: bool = True
35
+ show_missing: bool = True
36
+ show_extra: bool = True
37
+ show_type_mismatches: bool = True
38
+ max_section_items: int = 20
39
+ sort_by: str = "section" # "section", "type", "key"
40
+ filter_section: Optional[str] = None
41
+ filter_type: Optional[str] = None
42
+
43
+
44
+ class ConfigDashboard:
45
+ """Interactive configuration dashboard with Rich UI."""
46
+
47
+ def __init__(self, user_config: Optional[Dict[str, Any]] = None):
48
+ """Initialize the configuration dashboard."""
49
+ self.console = Console()
50
+ self.analysis: Optional[ConfigAnalysis] = None
51
+ self.config = DashboardConfig()
52
+ self.selected_item: Optional[ConfigDifference] = None
53
+
54
+ # Load and analyze configuration
55
+ if user_config is None:
56
+ from tunacode.utils.user_configuration import load_config
57
+
58
+ user_config = load_config()
59
+ if user_config is None:
60
+ raise ValueError("No user configuration found")
61
+
62
+ self.load_analysis(user_config)
63
+
64
+ def load_analysis(self, user_config: Dict[str, Any]) -> None:
65
+ """Load and analyze the user configuration."""
66
+ comparator = ConfigComparator()
67
+ self.analysis = comparator.analyze_config(user_config)
68
+
69
+ def render_overview(self) -> Panel:
70
+ """Render the overview panel with key statistics."""
71
+ if not self.analysis:
72
+ return Panel("No configuration loaded", title="Overview", box=ROUNDED)
73
+
74
+ stats = ConfigComparator().get_summary_stats(self.analysis)
75
+
76
+ # Create overview table
77
+ table = Table.grid(padding=(0, 2))
78
+ table.add_column("Metric", style="cyan", no_wrap=True)
79
+ table.add_column("Value", style="white")
80
+
81
+ table.add_row("Total Keys", str(stats["total_keys_analyzed"]))
82
+ table.add_row(
83
+ "Custom Keys", f"{stats['custom_keys_count']} ({stats['custom_percentage']:.1f}%)"
84
+ )
85
+ table.add_row("Missing Keys", str(stats["missing_keys_count"]))
86
+ table.add_row("Extra Keys", str(stats["extra_keys_count"]))
87
+ table.add_row("Type Mismatches", str(stats["type_mismatches_count"]))
88
+ table.add_row("Sections", str(stats["sections_analyzed"]))
89
+
90
+ health_status = "✅ Healthy" if not stats["has_issues"] else "⚠️ Issues Found"
91
+ health_style = "green" if not stats["has_issues"] else "yellow"
92
+ table.add_row("Status", Text(health_status, style=health_style))
93
+
94
+ return Panel(table, title="Configuration Overview", box=ROUNDED, border_style="blue")
95
+
96
+ def render_section_tree(self) -> Panel:
97
+ """Render the configuration section tree."""
98
+ if not self.analysis:
99
+ return Panel("No configuration loaded", title="Sections", box=ROUNDED)
100
+
101
+ tree = Tree("Configuration Structure")
102
+
103
+ for section in sorted(self.analysis.sections_analyzed):
104
+ section_diffs = [diff for diff in self.analysis.differences if diff.section == section]
105
+
106
+ if section_diffs:
107
+ section_node = tree.add(f"[bold]{section}[/bold] ({len(section_diffs)} items)")
108
+
109
+ for diff in section_diffs[: self.config.max_section_items]:
110
+ self._add_diff_to_tree(section_node, diff)
111
+
112
+ if len(section_diffs) > self.config.max_section_items:
113
+ more_count = len(section_diffs) - self.config.max_section_items
114
+ section_node.add(Text(f"[dim]... and {more_count} more[/dim]"))
115
+ else:
116
+ tree.add(f"[dim]{section}[/dim]")
117
+
118
+ return Panel(tree, title="Configuration Sections", box=ROUNDED, border_style="green")
119
+
120
+ def _add_diff_to_tree(self, parent: Tree, diff: ConfigDifference) -> None:
121
+ """Add a configuration difference to the tree."""
122
+ icon_map = {"custom": "✏️", "missing": "❌", "extra": "➕", "type_mismatch": "⚠️"}
123
+
124
+ style_map = {
125
+ "custom": "yellow",
126
+ "missing": "red",
127
+ "extra": "blue",
128
+ "type_mismatch": "bold red",
129
+ }
130
+
131
+ icon = icon_map.get(diff.difference_type, "❓")
132
+ style = style_map.get(diff.difference_type, "white")
133
+
134
+ diff_text = f"{icon} [dim]{diff.key_path}[/dim]"
135
+
136
+ if diff.user_value is not None:
137
+ # Mask sensitive values
138
+ display_value = self._mask_sensitive_value(diff.user_value)
139
+ diff_text += f": [white]{display_value}[/white]"
140
+
141
+ parent.add(Text(diff_text, style=style))
142
+
143
+ def render_differences_table(self) -> Panel:
144
+ """Render the detailed differences table."""
145
+ if not self.analysis:
146
+ return Panel("No configuration loaded", title="Differences", box=ROUNDED)
147
+
148
+ # Filter differences based on config
149
+ filtered_diffs = self._filter_differences()
150
+
151
+ if not filtered_diffs:
152
+ return Panel("No differences to display", title="Differences", box=ROUNDED)
153
+
154
+ # Create differences table
155
+ table = Table(box=ROUNDED, show_header=True, header_style="bold magenta")
156
+
157
+ table.add_column("Key", style="cyan", no_wrap=True)
158
+ table.add_column("Type", style="yellow")
159
+ table.add_column("User Value", style="white")
160
+ table.add_column("Default Value", style="dim")
161
+ table.add_column("Section", style="green")
162
+
163
+ for diff in filtered_diffs[: self.config.max_section_items]:
164
+ user_value = self._mask_sensitive_value(diff.user_value, diff.key_path)
165
+ default_value = self._mask_sensitive_value(diff.default_value, diff.key_path)
166
+
167
+ # Format type with icon
168
+ type_map = {
169
+ "custom": "✏️ Custom",
170
+ "missing": "❌ Missing",
171
+ "extra": "➕ Extra",
172
+ "type_mismatch": "⚠️ Type Mismatch",
173
+ }
174
+
175
+ diff_type = type_map.get(diff.difference_type, diff.difference_type)
176
+
177
+ table.add_row(
178
+ diff.key_path,
179
+ diff_type,
180
+ str(user_value) if user_value is not None else "",
181
+ str(default_value) if default_value is not None else "",
182
+ diff.section,
183
+ )
184
+
185
+ if len(filtered_diffs) > self.config.max_section_items:
186
+ table.add_row(
187
+ "",
188
+ f"[dim]... and {len(filtered_diffs) - self.config.max_section_items} more[/dim]",
189
+ "",
190
+ "",
191
+ "",
192
+ )
193
+
194
+ return Panel(
195
+ table,
196
+ title=f"Configuration Differences ({len(filtered_diffs)} items)",
197
+ box=ROUNDED,
198
+ border_style="magenta",
199
+ )
200
+
201
+ def _filter_differences(self) -> List[ConfigDifference]:
202
+ """Filter differences based on dashboard configuration."""
203
+ if not self.analysis:
204
+ return []
205
+
206
+ filtered = []
207
+
208
+ for diff in self.analysis.differences:
209
+ # Apply type filter
210
+ if self.config.filter_type and diff.difference_type != self.config.filter_type:
211
+ continue
212
+
213
+ # Apply section filter
214
+ if self.config.filter_section and diff.section != self.config.filter_section:
215
+ continue
216
+
217
+ # Apply show/hide filters
218
+ if diff.difference_type == "custom" and not self.config.show_custom:
219
+ continue
220
+ elif diff.difference_type == "missing" and not self.config.show_missing:
221
+ continue
222
+ elif diff.difference_type == "extra" and not self.config.show_extra:
223
+ continue
224
+ elif diff.difference_type == "type_mismatch" and not self.config.show_type_mismatches:
225
+ continue
226
+
227
+ filtered.append(diff)
228
+
229
+ # Sort differences
230
+ if self.config.sort_by == "section":
231
+ filtered.sort(key=lambda d: (d.section, d.key_path))
232
+ elif self.config.sort_by == "type":
233
+ filtered.sort(key=lambda d: (d.difference_type, d.key_path))
234
+ else: # key
235
+ filtered.sort(key=lambda d: d.key_path)
236
+
237
+ return filtered
238
+
239
+ def _mask_sensitive_value(self, value: Any, key_path: str = "") -> str:
240
+ """Mask sensitive configuration values for display with service identification."""
241
+ if value is None:
242
+ return ""
243
+
244
+ value_str = str(value)
245
+
246
+ # Empty values should show as empty
247
+ if not value_str.strip():
248
+ return "[dim]<not configured>[/dim]"
249
+
250
+ # Check if this is an API key based on key path
251
+ service_type = self._get_service_type_from_key_path(key_path)
252
+ if service_type:
253
+ return self._format_api_key_with_service(value_str, service_type)
254
+
255
+ # Check for common API key patterns
256
+ if value_str.startswith("sk-"):
257
+ # OpenAI-style keys
258
+ service = "OpenAI" if "openai" in key_path.lower() else "Unknown"
259
+ return self._format_api_key_with_service(value_str, service.lower())
260
+ elif value_str.startswith("sk-ant-"):
261
+ return self._format_api_key_with_service(value_str, "anthropic")
262
+ elif value_str.startswith("sk-or-"):
263
+ return self._format_api_key_with_service(value_str, "openrouter")
264
+ elif value_str.startswith("AIza"):
265
+ return self._format_api_key_with_service(value_str, "google")
266
+
267
+ # Check for other sensitive patterns (non-API keys)
268
+ sensitive_patterns = [
269
+ "secret",
270
+ "token",
271
+ "password",
272
+ "credential",
273
+ ]
274
+
275
+ for pattern in sensitive_patterns:
276
+ if pattern in key_path.lower() or pattern in value_str.lower():
277
+ return "•" * 8 # Fully mask non-API key secrets
278
+
279
+ return value_str
280
+
281
+ def _get_service_type_from_key_path(self, key_path: str) -> str:
282
+ """Determine service type from configuration key path."""
283
+ key_lower = key_path.lower()
284
+
285
+ if "openai_api_key" in key_lower:
286
+ return "openai"
287
+ elif "anthropic_api_key" in key_lower:
288
+ return "anthropic"
289
+ elif "openrouter_api_key" in key_lower:
290
+ return "openrouter"
291
+ elif "gemini_api_key" in key_lower:
292
+ return "google"
293
+
294
+ return ""
295
+
296
+ def _format_api_key_with_service(self, api_key: str, service_type: str) -> str:
297
+ """Format API key with service identification and partial masking."""
298
+ service_names = {
299
+ "openai": "OpenAI",
300
+ "anthropic": "Anthropic",
301
+ "openrouter": "OpenRouter",
302
+ "google": "Google",
303
+ }
304
+
305
+ service_name = service_names.get(service_type, service_type.title())
306
+
307
+ if len(api_key) <= 8:
308
+ # Short keys - just show service and mask
309
+ return f"[cyan]{service_name}:[/cyan] •••••••"
310
+ else:
311
+ # Show first 4 and last 4 characters with service label
312
+ masked = f"{api_key[:4]}...{api_key[-4:]}"
313
+ return f"[cyan]{service_name}:[/cyan] {masked}"
314
+
315
+ def render_recommendations(self) -> Panel:
316
+ """Render configuration recommendations."""
317
+ if not self.analysis:
318
+ return Panel("No configuration loaded", title="Recommendations", box=ROUNDED)
319
+
320
+ recommendations = ConfigComparator().get_recommendations(self.analysis)
321
+
322
+ if not recommendations:
323
+ return Panel(
324
+ "✅ No recommendations - configuration looks good!",
325
+ title="Recommendations",
326
+ box=ROUNDED,
327
+ border_style="green",
328
+ )
329
+
330
+ rec_text = "\n".join(f"• {rec}" for rec in recommendations)
331
+
332
+ return Panel(rec_text, title="Recommendations", box=ROUNDED, border_style="yellow")
333
+
334
+ def render_custom_settings(self) -> Panel:
335
+ """Render panel showing only custom (user-modified) settings."""
336
+ if not self.analysis:
337
+ return Panel("No configuration loaded", title="Your Customizations", box=ROUNDED)
338
+
339
+ # Filter for only custom settings
340
+ custom_diffs = [
341
+ diff for diff in self.analysis.differences if diff.difference_type == "custom"
342
+ ]
343
+
344
+ if not custom_diffs:
345
+ message = (
346
+ "✨ You're using all default settings!\n\n"
347
+ "This means TunaCode is running with its built-in configuration. "
348
+ "You can customize settings by editing ~/.config/tunacode.json"
349
+ )
350
+ return Panel(
351
+ message,
352
+ title="🔧 Your Customizations (0)",
353
+ box=ROUNDED,
354
+ border_style="green",
355
+ )
356
+
357
+ # Create table for custom settings
358
+ table = Table(box=ROUNDED, show_header=True, header_style="bold cyan")
359
+ table.add_column("Setting", style="cyan", no_wrap=True)
360
+ table.add_column("Your Value", style="white")
361
+ table.add_column("Default Value", style="dim")
362
+ table.add_column("Category", style="green")
363
+
364
+ for diff in custom_diffs[:15]: # Limit to prevent overflow
365
+ user_value = self._mask_sensitive_value(diff.user_value, diff.key_path)
366
+ default_value = self._mask_sensitive_value(diff.default_value, diff.key_path)
367
+
368
+ # Get category from key descriptions
369
+ from tunacode.configuration.key_descriptions import get_key_description
370
+
371
+ desc = get_key_description(diff.key_path)
372
+ category = desc.category if desc else diff.section.title()
373
+
374
+ table.add_row(
375
+ diff.key_path,
376
+ str(user_value) if user_value is not None else "",
377
+ str(default_value) if default_value is not None else "",
378
+ category,
379
+ )
380
+
381
+ if len(custom_diffs) > 15:
382
+ table.add_row("", f"[dim]... and {len(custom_diffs) - 15} more[/dim]", "", "")
383
+
384
+ summary = (
385
+ f"You have customized {len(custom_diffs)} out of "
386
+ f"{self.analysis.total_keys} available settings"
387
+ )
388
+
389
+ content = Group(Text(summary, style="bold"), Text(""), table)
390
+
391
+ return Panel(
392
+ content,
393
+ title=f"🔧 Your Customizations ({len(custom_diffs)})",
394
+ box=ROUNDED,
395
+ border_style="cyan",
396
+ )
397
+
398
+ def render_default_settings_summary(self) -> Panel:
399
+ """Render panel showing summary of default settings by category."""
400
+ if not self.analysis:
401
+ return Panel("No configuration loaded", title="Default Settings", box=ROUNDED)
402
+
403
+ # Get all default settings (not customized)
404
+ custom_keys = {
405
+ diff.key_path for diff in self.analysis.differences if diff.difference_type == "custom"
406
+ }
407
+
408
+ # Import here to avoid circular imports
409
+ from tunacode.configuration.key_descriptions import get_categories
410
+
411
+ categories = get_categories()
412
+
413
+ # Count settings by category
414
+ category_counts = {}
415
+ category_examples = {}
416
+
417
+ for category, descriptions in categories.items():
418
+ default_count = 0
419
+ examples: List[str] = []
420
+
421
+ for desc in descriptions:
422
+ if desc.name not in custom_keys:
423
+ default_count += 1
424
+ if len(examples) < 3: # Show up to 3 examples
425
+ examples.append(f"• {desc.name}")
426
+
427
+ if default_count > 0:
428
+ category_counts[category] = default_count
429
+ category_examples[category] = examples
430
+
431
+ if not category_counts:
432
+ return Panel(
433
+ "All settings have been customized", title="📋 Default Settings", box=ROUNDED
434
+ )
435
+
436
+ # Create summary table
437
+ table = Table.grid(padding=(0, 2))
438
+ table.add_column("Category", style="yellow", no_wrap=True)
439
+ table.add_column("Count", style="white")
440
+ table.add_column("Examples", style="dim")
441
+
442
+ for category, count in sorted(category_counts.items()):
443
+ examples_text = "\n".join(category_examples[category])
444
+ table.add_row(category, f"{count} settings", examples_text)
445
+
446
+ total_defaults = sum(category_counts.values())
447
+ summary = f"Using TunaCode defaults for {total_defaults} settings"
448
+
449
+ content = Group(Text(summary, style="bold"), Text(""), table)
450
+
451
+ return Panel(
452
+ content,
453
+ title=f"📋 Default Settings ({total_defaults})",
454
+ box=ROUNDED,
455
+ border_style="blue",
456
+ )
457
+
458
+ def render_help(self) -> Panel:
459
+ """Render help information with configuration key glossary."""
460
+ # Import here to avoid circular imports
461
+ from tunacode.configuration.key_descriptions import get_configuration_glossary
462
+
463
+ help_text = """
464
+ [bold]Configuration Dashboard Guide[/bold]
465
+
466
+ [cyan]Dashboard Sections:[/cyan]
467
+ • [yellow]Your Customizations[/yellow]: Settings you've changed from defaults (🔧)
468
+ • [yellow]Default Settings[/yellow]: TunaCode's built-in settings you're using (📋)
469
+ • [yellow]All Differences[/yellow]: Complete comparison view
470
+
471
+ [cyan]API Key Display:[/cyan]
472
+ • [cyan]OpenAI:[/cyan] sk-abc...xyz - Shows service and partial key
473
+ • [cyan]Anthropic:[/cyan] sk-ant...xyz - Secure but identifiable
474
+ • [dim]<not configured>[/dim] - No API key set
475
+
476
+ [cyan]Visual Indicators:[/cyan]
477
+ • 🔧 Custom: Values you've changed from defaults
478
+ • 📋 Default: TunaCode's built-in settings
479
+ • ❌ Missing: Required configuration keys not found
480
+ • ➕ Extra: Keys not in default configuration
481
+ • ⚠️ Type Mismatch: Wrong data type for configuration
482
+
483
+ [cyan]Exit:[/cyan]
484
+ • Press Ctrl+C to return to TunaCode
485
+ """
486
+
487
+ glossary = get_configuration_glossary()
488
+
489
+ content = Vertical(Text(help_text.strip()), Text(""), Text(glossary))
490
+
491
+ return Panel(content, title="Help & Glossary", box=ROUNDED, border_style="blue")
492
+
493
+ def render_dashboard(self) -> Layout:
494
+ """Render the complete dashboard layout with improved organization."""
495
+ layout = Layout()
496
+
497
+ # Split into main areas - increase footer size for glossary
498
+ layout.split_column(
499
+ Layout(name="header", size=3), Layout(name="main"), Layout(name="footer", size=12)
500
+ )
501
+
502
+ # Split main area into three columns for better organization
503
+ layout["main"].split_row(
504
+ Layout(name="left", ratio=1),
505
+ Layout(name="center", ratio=1),
506
+ Layout(name="right", ratio=1),
507
+ )
508
+
509
+ # Left column: Overview and custom settings
510
+ layout["left"].split_column(
511
+ Layout(name="overview", size=8), Layout(name="custom_settings", ratio=1)
512
+ )
513
+
514
+ # Center column: Default settings and section tree
515
+ layout["center"].split_column(
516
+ Layout(name="default_settings", ratio=1), Layout(name="sections", ratio=1)
517
+ )
518
+
519
+ # Right column: All differences and recommendations
520
+ layout["right"].split_column(
521
+ Layout(name="differences", ratio=2), Layout(name="recommendations", size=6)
522
+ )
523
+
524
+ # Add content to each area
525
+ layout["header"].update(
526
+ Panel("🐟 TunaCode Configuration Dashboard", style="bold blue", box=ROUNDED)
527
+ )
528
+
529
+ layout["overview"].update(self.render_overview())
530
+ layout["custom_settings"].update(self.render_custom_settings())
531
+ layout["default_settings"].update(self.render_default_settings_summary())
532
+ layout["sections"].update(self.render_section_tree())
533
+ layout["differences"].update(self.render_differences_table())
534
+ layout["recommendations"].update(self.render_recommendations())
535
+ layout["footer"].update(self.render_help())
536
+
537
+ return layout
538
+
539
+ def show(self, wait_for_input: bool = True) -> None:
540
+ """Display the interactive dashboard.
541
+
542
+ Args:
543
+ wait_for_input: If True, wait for user to press Enter.
544
+ Set to False for non-interactive usage.
545
+ """
546
+ if not self.analysis:
547
+ self.console.print("[red]No configuration analysis available[/red]")
548
+ return
549
+
550
+ layout = self.render_dashboard()
551
+
552
+ try:
553
+ with Live(layout, console=self.console, refresh_per_second=4):
554
+ # In a real implementation, this would handle user input
555
+ # For now, we'll just display the dashboard
556
+ if wait_for_input:
557
+ input("Press Enter to continue...")
558
+ except KeyboardInterrupt:
559
+ self.console.print("\n[dim]Dashboard closed[/dim]")
560
+
561
+ def generate_report(self) -> str:
562
+ """Generate a text report of the configuration analysis."""
563
+ if not self.analysis:
564
+ return "No configuration analysis available"
565
+
566
+ return create_config_report(self.analysis)
567
+
568
+
569
+ def show_config_dashboard(
570
+ user_config: Optional[Dict[str, Any]] = None, wait_for_input: bool = False
571
+ ) -> None:
572
+ """Convenience function to show the configuration dashboard.
573
+
574
+ Args:
575
+ user_config: Optional user configuration dictionary.
576
+ wait_for_input: If True, wait for user to press Enter. Defaults to False for CLI usage.
577
+ """
578
+ dashboard = ConfigDashboard(user_config)
579
+ dashboard.show(wait_for_input=wait_for_input)
580
+
581
+
582
+ def generate_config_report(user_config: Optional[Dict[str, Any]] = None) -> str:
583
+ """Convenience function to generate a configuration report."""
584
+ dashboard = ConfigDashboard(user_config)
585
+ return dashboard.generate_report()
tunacode/ui/console.py CHANGED
@@ -3,14 +3,12 @@
3
3
  Provides high-level console functions and coordinates between different UI components.
4
4
  """
5
5
 
6
- from rich.console import Console as RichConsole
7
- from rich.markdown import Markdown
8
-
9
6
  # Import and re-export all functions from specialized modules
7
+ # Lazy loading of Rich components
8
+ from typing import TYPE_CHECKING, Any, Optional
9
+
10
10
  from .input import formatted_text, input, multiline_input
11
11
  from .keybindings import create_key_bindings
12
-
13
- # Unified UI logger compatibility layer
14
12
  from .logging_compat import ui_logger
15
13
  from .output import (
16
14
  banner,
@@ -29,6 +27,7 @@ from .panels import (
29
27
  StreamingAgentPanel,
30
28
  agent,
31
29
  agent_streaming,
30
+ batch,
32
31
  dump_messages,
33
32
  help,
34
33
  models,
@@ -40,6 +39,29 @@ from .panels import (
40
39
  from .prompt_manager import PromptConfig, PromptManager
41
40
  from .validators import ModelValidator
42
41
 
42
+ if TYPE_CHECKING:
43
+ from rich.console import Console as RichConsole
44
+
45
+ _console: Optional["RichConsole"] = None
46
+ _keybindings: Optional[Any] = None
47
+
48
+
49
+ def get_console() -> "RichConsole":
50
+ """Get or create the Rich console instance lazily."""
51
+ global _console
52
+ if _console is None:
53
+ from rich.console import Console as RichConsole
54
+
55
+ _console = RichConsole(force_terminal=True, legacy_windows=False)
56
+ return _console
57
+
58
+
59
+ def get_markdown() -> type:
60
+ """Get the Markdown class lazily."""
61
+ from rich.markdown import Markdown
62
+
63
+ return Markdown
64
+
43
65
 
44
66
  # Async wrappers for UI logging
45
67
  async def info(message: str) -> None:
@@ -62,16 +84,45 @@ async def success(message: str) -> None:
62
84
  await ui_logger.success(message)
63
85
 
64
86
 
65
- # Create console object for backward compatibility
66
- console = RichConsole(force_terminal=True, legacy_windows=False)
87
+ # Create lazy console object for backward compatibility
88
+ class _LazyConsole:
89
+ """Lazy console accessor."""
90
+
91
+ def __str__(self):
92
+ return str(get_console())
93
+
94
+ def __getattr__(self, name):
95
+ return getattr(get_console(), name)
96
+
97
+
98
+ # Create lazy key bindings object for backward compatibility
99
+ class _LazyKeyBindings:
100
+ """Lazy key bindings accessor."""
101
+
102
+ def __str__(self):
103
+ return str(get_keybindings())
104
+
105
+ def __getattr__(self, name):
106
+ return getattr(get_keybindings(), name)
107
+
108
+
109
+ def get_keybindings() -> Any:
110
+ """Get key bindings lazily."""
111
+ global _keybindings
112
+ if _keybindings is None:
113
+ _keybindings = create_key_bindings()
114
+ return _keybindings
115
+
67
116
 
68
- # Create key bindings object for backward compatibility
69
- kb = create_key_bindings()
117
+ # Module-level lazy instances
118
+ console = _LazyConsole()
119
+ kb = _LazyKeyBindings()
70
120
 
71
121
 
72
122
  # Re-export markdown utility for backward compatibility
73
- def markdown(text: str) -> Markdown:
74
- """Create a Markdown object."""
123
+ def markdown(text: str) -> Any:
124
+ """Create a Markdown object lazily."""
125
+ Markdown = get_markdown()
75
126
  return Markdown(text)
76
127
 
77
128
 
@@ -106,6 +157,7 @@ __all__ = [
106
157
  # From panels module
107
158
  "agent",
108
159
  "agent_streaming",
160
+ "batch",
109
161
  "dump_messages",
110
162
  "help",
111
163
  "models",