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