tunacode-cli 0.0.75__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/ui/panels.py CHANGED
@@ -92,7 +92,7 @@ class StreamingAgentPanel:
92
92
  _dots_count: int
93
93
  _show_dots: bool
94
94
 
95
- def __init__(self, bottom: int = 1):
95
+ def __init__(self, bottom: int = 1, debug: bool = False):
96
96
  self.bottom = bottom
97
97
  self.title = f"[bold {colors.primary}]●[/bold {colors.primary}] {APP_NAME}"
98
98
  self.content = ""
@@ -101,6 +101,24 @@ class StreamingAgentPanel:
101
101
  self._dots_task = None
102
102
  self._dots_count = 0
103
103
  self._show_dots = True # Start with dots enabled for "Thinking..."
104
+ # Debug/diagnostic instrumentation (printed after stop to avoid Live interference)
105
+ self._debug_enabled = debug
106
+ self._debug_events: list[str] = []
107
+ self._update_count: int = 0
108
+ self._first_update_done: bool = False
109
+ self._dots_tick_count: int = 0
110
+ self._max_logged_dots: int = 10
111
+
112
+ def _log_debug(self, label: str, **data: Any) -> None:
113
+ if not self._debug_enabled:
114
+ return
115
+ try:
116
+ ts = time.perf_counter_ns()
117
+ except Exception:
118
+ ts = 0
119
+ payload = ", ".join(f"{k}={repr(v)}" for k, v in data.items()) if data else ""
120
+ line = f"[ui] {label} ts_ns={ts}{(' ' + payload) if payload else ''}"
121
+ self._debug_events.append(line)
104
122
 
105
123
  def _create_panel(self) -> Padding:
106
124
  """Create a Rich panel with current content."""
@@ -159,6 +177,14 @@ class StreamingAgentPanel:
159
177
  if current_time - self._last_update_time > delay_threshold:
160
178
  self._show_dots = True
161
179
  self._dots_count += 1
180
+ # Log only a few initial ticks to avoid noise
181
+ if (
182
+ self._debug_enabled
183
+ and not self.content
184
+ and self._dots_tick_count < self._max_logged_dots
185
+ ):
186
+ self._dots_tick_count += 1
187
+ self._log_debug("dots_tick", n=self._dots_count)
162
188
  if self.live:
163
189
  self.live.update(self._create_panel())
164
190
  else:
@@ -173,6 +199,7 @@ class StreamingAgentPanel:
173
199
 
174
200
  self.live = Live(self._create_panel(), console=console, refresh_per_second=4)
175
201
  self.live.start()
202
+ self._log_debug("start")
176
203
  # For "Thinking...", set time in past to trigger dots immediately
177
204
  if not self.content:
178
205
  self._last_update_time = time.time() - 0.4 # Triggers dots on first cycle
@@ -188,45 +215,83 @@ class StreamingAgentPanel:
188
215
  content_chunk = ""
189
216
 
190
217
  # Filter out plan mode system prompts and tool definitions from streaming
191
- if any(
192
- phrase in str(content_chunk)
218
+ # Use more precise filtering to avoid false positives
219
+ content_str = str(content_chunk).strip()
220
+ if content_str and any(
221
+ content_str.startswith(phrase) or phrase in content_str
193
222
  for phrase in [
194
223
  "🔧 PLAN MODE",
195
224
  "TOOL EXECUTION ONLY",
196
225
  "planning assistant that ONLY communicates",
197
226
  "namespace functions {",
198
227
  "namespace multi_tool_use {",
199
- "You are trained on data up to",
200
228
  ]
201
229
  ):
202
230
  return
203
231
 
232
+ # Special handling for the training data phrase - only filter if it's a complete system message
233
+ if "You are trained on data up to" in content_str and len(content_str) > 50:
234
+ # Only filter if this looks like a complete system message, not user content
235
+ if (
236
+ content_str.startswith("You are trained on data up to")
237
+ or "The current date is" in content_str
238
+ ):
239
+ return
240
+
204
241
  # Ensure type safety for concatenation
205
- self.content = (self.content or "") + str(content_chunk)
242
+ incoming = str(content_chunk)
243
+ # First-chunk diagnostics
244
+ is_first_chunk = (not self.content) and bool(incoming)
245
+ if is_first_chunk:
246
+ self._log_debug(
247
+ "first_chunk_received",
248
+ chunk_repr=incoming[:5],
249
+ chunk_len=len(incoming),
250
+ )
251
+ self.content = (self.content or "") + incoming
206
252
 
207
253
  # Reset the update timer when we get new content
208
254
  self._last_update_time = time.time()
209
- self._show_dots = False # Hide dots immediately when new content arrives
255
+ # Hide dots immediately when new content arrives
256
+ if self._show_dots:
257
+ self._log_debug("disable_dots_called")
258
+ self._show_dots = False
210
259
 
211
260
  if self.live:
261
+ # Log timing around the first two live.update() calls
262
+ self._update_count += 1
263
+ if self._update_count <= 2:
264
+ self._log_debug("live_update.start", update_index=self._update_count)
212
265
  self.live.update(self._create_panel())
266
+ if self._update_count <= 2:
267
+ self._log_debug("live_update.end", update_index=self._update_count)
213
268
 
214
269
  async def set_content(self, content: str):
215
270
  """Set the complete content (overwrites previous)."""
216
271
  # Filter out plan mode system prompts and tool definitions
217
- if any(
218
- phrase in str(content)
272
+ # Use more precise filtering to avoid false positives
273
+ content_str = str(content).strip()
274
+ if content_str and any(
275
+ content_str.startswith(phrase) or phrase in content_str
219
276
  for phrase in [
220
277
  "🔧 PLAN MODE",
221
278
  "TOOL EXECUTION ONLY",
222
279
  "planning assistant that ONLY communicates",
223
280
  "namespace functions {",
224
281
  "namespace multi_tool_use {",
225
- "You are trained on data up to",
226
282
  ]
227
283
  ):
228
284
  return
229
285
 
286
+ # Special handling for the training data phrase - only filter if it's a complete system message
287
+ if "You are trained on data up to" in content_str and len(content_str) > 50:
288
+ # Only filter if this looks like a complete system message, not user content
289
+ if (
290
+ content_str.startswith("You are trained on data up to")
291
+ or "The current date is" in content_str
292
+ ):
293
+ return
294
+
230
295
  self.content = content
231
296
  if self.live:
232
297
  self.live.update(self._create_panel())
@@ -240,6 +305,8 @@ class StreamingAgentPanel:
240
305
  await self._dots_task
241
306
  except asyncio.CancelledError:
242
307
  pass
308
+ finally:
309
+ self._log_debug("dots_task_cancelled")
243
310
 
244
311
  if self.live:
245
312
  # Get the console before stopping the live display
@@ -263,6 +330,22 @@ class StreamingAgentPanel:
263
330
 
264
331
  self.live = None
265
332
 
333
+ # Emit debug diagnostics after Live has been stopped (to avoid interference)
334
+ if self._debug_enabled:
335
+ from .output import print as ui_print
336
+
337
+ # Summarize UI buffer state
338
+ ui_prefix = "[debug]"
339
+ ui_buffer_first5 = repr((self.content or "")[:5])
340
+ lines = [
341
+ f"{ui_prefix} ui_buffer_first5={ui_buffer_first5} total_len={len(self.content or '')}",
342
+ ]
343
+ # Include recorded event lines
344
+ lines.extend(self._debug_events)
345
+ # Flush lines to console
346
+ for line in lines:
347
+ await ui_print(line)
348
+
266
349
 
267
350
  async def agent_streaming(content_stream, bottom: int = 1):
268
351
  """Display an agent panel with streaming content updates.
@@ -0,0 +1,340 @@
1
+ """
2
+ Module: tunacode.utils.config_comparator
3
+
4
+ Configuration comparison utility for analyzing user configurations against defaults.
5
+ Provides detailed analysis of customizations, defaults, and configuration state.
6
+ """
7
+
8
+ import json
9
+ from dataclasses import dataclass
10
+ from pathlib import Path
11
+ from typing import Any, Dict, List, Optional, Set, Union
12
+
13
+ from tunacode.configuration.defaults import DEFAULT_USER_CONFIG
14
+ from tunacode.types import UserConfig
15
+
16
+
17
+ @dataclass
18
+ class ConfigDifference:
19
+ """Represents a difference between user config and defaults."""
20
+
21
+ key_path: str
22
+ user_value: Any
23
+ default_value: Any
24
+ difference_type: str # "custom", "missing", "extra", "type_mismatch"
25
+ section: str
26
+ description: str
27
+
28
+
29
+ @dataclass
30
+ class ConfigAnalysis:
31
+ """Complete analysis of configuration state."""
32
+
33
+ user_config: UserConfig
34
+ default_config: UserConfig
35
+ differences: List[ConfigDifference]
36
+ custom_keys: Set[str]
37
+ missing_keys: Set[str]
38
+ extra_keys: Set[str]
39
+ type_mismatches: Set[str]
40
+ sections_analyzed: Set[str]
41
+ total_keys: int
42
+ custom_percentage: float
43
+
44
+
45
+ class ConfigComparator:
46
+ """Compares user configuration against defaults to identify customizations."""
47
+
48
+ def __init__(self, default_config: Optional[UserConfig] = None):
49
+ """Initialize comparator with default configuration."""
50
+ self.default_config = default_config or DEFAULT_USER_CONFIG
51
+
52
+ def _get_key_description(self, key_path: str, difference_type: str) -> str:
53
+ """Get a descriptive explanation for a configuration key difference."""
54
+ try:
55
+ from tunacode.configuration.key_descriptions import get_key_description
56
+
57
+ desc = get_key_description(key_path)
58
+
59
+ if desc:
60
+ if difference_type == "custom":
61
+ return f"Custom: {desc.description}"
62
+ elif difference_type == "missing":
63
+ return f"Missing: {desc.description}"
64
+ elif difference_type == "extra":
65
+ return f"Extra: {desc.description}"
66
+ elif difference_type == "type_mismatch":
67
+ return f"Type mismatch: {desc.description}"
68
+
69
+ except ImportError:
70
+ pass # Fall back to basic descriptions
71
+
72
+ # Fallback descriptions
73
+ if difference_type == "custom":
74
+ return f"Custom value: {key_path}"
75
+ elif difference_type == "missing":
76
+ return f"Missing configuration key: {key_path}"
77
+ elif difference_type == "extra":
78
+ return f"Extra configuration key: {key_path}"
79
+ elif difference_type == "type_mismatch":
80
+ return f"Type mismatch for: {key_path}"
81
+
82
+ return f"Configuration difference: {key_path}"
83
+
84
+ def analyze_config(self, user_config: UserConfig) -> ConfigAnalysis:
85
+ """Perform complete analysis of user configuration."""
86
+ differences: list[ConfigDifference] = []
87
+ custom_keys: set[str] = set()
88
+ missing_keys: set[str] = set()
89
+ extra_keys: set[str] = set()
90
+ type_mismatches: set[str] = set()
91
+
92
+ # Analyze each section recursively
93
+ self._analyze_recursive(
94
+ user_config=user_config,
95
+ default_config=self.default_config,
96
+ current_path="",
97
+ differences=differences,
98
+ custom_keys=custom_keys,
99
+ missing_keys=missing_keys,
100
+ extra_keys=extra_keys,
101
+ type_mismatches=type_mismatches,
102
+ )
103
+
104
+ # Calculate statistics
105
+ sections_analyzed: set[str] = set()
106
+ self._collect_sections(user_config, sections_analyzed)
107
+ self._collect_sections(self.default_config, sections_analyzed)
108
+
109
+ total_keys = len(custom_keys) + len(missing_keys) + len(extra_keys) + len(type_mismatches)
110
+ custom_percentage = (len(custom_keys) / total_keys * 100) if total_keys > 0 else 0
111
+
112
+ return ConfigAnalysis(
113
+ user_config=user_config,
114
+ default_config=self.default_config,
115
+ differences=differences,
116
+ custom_keys=custom_keys,
117
+ missing_keys=missing_keys,
118
+ extra_keys=extra_keys,
119
+ type_mismatches=type_mismatches,
120
+ sections_analyzed=sections_analyzed,
121
+ total_keys=total_keys,
122
+ custom_percentage=custom_percentage,
123
+ )
124
+
125
+ def _analyze_recursive(
126
+ self,
127
+ user_config: Dict[str, Any],
128
+ default_config: Dict[str, Any],
129
+ current_path: str,
130
+ differences: List[ConfigDifference],
131
+ custom_keys: Set[str],
132
+ missing_keys: Set[str],
133
+ extra_keys: Set[str],
134
+ type_mismatches: Set[str],
135
+ ) -> None:
136
+ """Recursively analyze configuration differences."""
137
+
138
+ # Check for missing keys (present in default but not in user)
139
+ for key, default_value in default_config.items():
140
+ full_key = f"{current_path}.{key}" if current_path else key
141
+
142
+ if key not in user_config:
143
+ missing_keys.add(full_key)
144
+ differences.append(
145
+ ConfigDifference(
146
+ key_path=full_key,
147
+ user_value=None,
148
+ default_value=default_value,
149
+ difference_type="missing",
150
+ section=current_path or "root",
151
+ description=self._get_key_description(full_key, "missing"),
152
+ )
153
+ )
154
+ continue
155
+
156
+ user_value = user_config[key]
157
+
158
+ # Recursively analyze nested dictionaries
159
+ if isinstance(default_value, dict) and isinstance(user_value, dict):
160
+ self._analyze_recursive(
161
+ user_config=user_value,
162
+ default_config=default_value,
163
+ current_path=full_key,
164
+ differences=differences,
165
+ custom_keys=custom_keys,
166
+ missing_keys=missing_keys,
167
+ extra_keys=extra_keys,
168
+ type_mismatches=type_mismatches,
169
+ )
170
+ continue
171
+
172
+ # Check for type mismatches
173
+ if not isinstance(user_value, type(default_value)):
174
+ type_mismatches.add(full_key)
175
+ differences.append(
176
+ ConfigDifference(
177
+ key_path=full_key,
178
+ user_value=user_value,
179
+ default_value=default_value,
180
+ difference_type="type_mismatch",
181
+ section=current_path or "root",
182
+ description=self._get_key_description(full_key, "type_mismatch"),
183
+ )
184
+ )
185
+ continue
186
+
187
+ # Check for custom values
188
+ if user_value != default_value:
189
+ custom_keys.add(full_key)
190
+ differences.append(
191
+ ConfigDifference(
192
+ key_path=full_key,
193
+ user_value=user_value,
194
+ default_value=default_value,
195
+ difference_type="custom",
196
+ section=current_path or "root",
197
+ description=self._get_key_description(full_key, "custom"),
198
+ )
199
+ )
200
+
201
+ # Check for extra keys (present in user but not in default)
202
+ for key, user_value in user_config.items():
203
+ if key not in default_config:
204
+ full_key = f"{current_path}.{key}" if current_path else key
205
+ extra_keys.add(full_key)
206
+ differences.append(
207
+ ConfigDifference(
208
+ key_path=full_key,
209
+ user_value=user_value,
210
+ default_value=None,
211
+ difference_type="extra",
212
+ section=current_path or "root",
213
+ description=self._get_key_description(full_key, "extra"),
214
+ )
215
+ )
216
+
217
+ def _collect_sections(self, config: Dict[str, Any], sections: Set[str]) -> None:
218
+ """Collect all section names from configuration."""
219
+ for key, value in config.items():
220
+ if isinstance(value, dict):
221
+ sections.add(key)
222
+ self._collect_sections(value, sections)
223
+
224
+ def get_summary_stats(self, analysis: ConfigAnalysis) -> Dict[str, Any]:
225
+ """Get summary statistics for the configuration analysis."""
226
+ return {
227
+ "total_keys_analyzed": analysis.total_keys,
228
+ "custom_keys_count": len(analysis.custom_keys),
229
+ "missing_keys_count": len(analysis.missing_keys),
230
+ "extra_keys_count": len(analysis.extra_keys),
231
+ "type_mismatches_count": len(analysis.type_mismatches),
232
+ "custom_percentage": analysis.custom_percentage,
233
+ "sections_analyzed": len(analysis.sections_analyzed),
234
+ "has_issues": bool(analysis.missing_keys or analysis.type_mismatches),
235
+ }
236
+
237
+ def get_section_analysis(
238
+ self, analysis: ConfigAnalysis, section: str
239
+ ) -> List[ConfigDifference]:
240
+ """Get differences for a specific section."""
241
+ return [diff for diff in analysis.differences if diff.section == section]
242
+
243
+ def is_config_healthy(self, analysis: ConfigAnalysis) -> bool:
244
+ """Check if configuration is healthy (no critical issues)."""
245
+ # Type mismatches are considered critical
246
+ if analysis.type_mismatches:
247
+ return False
248
+
249
+ # Missing keys might be acceptable depending on the context
250
+ # For now, we'll consider missing keys as warnings, not errors
251
+ return True
252
+
253
+ def get_recommendations(self, analysis: ConfigAnalysis) -> List[str]:
254
+ """Get recommendations based on configuration analysis."""
255
+ recommendations = []
256
+
257
+ if analysis.type_mismatches:
258
+ recommendations.append(
259
+ f"Fix {len(analysis.type_mismatches)} type mismatch(es) in configuration"
260
+ )
261
+
262
+ if analysis.missing_keys:
263
+ recommendations.append(
264
+ f"Consider adding {len(analysis.missing_keys)} missing configuration key(s)"
265
+ )
266
+
267
+ if analysis.custom_percentage > 80:
268
+ recommendations.append(
269
+ "High customization detected - consider documenting your configuration"
270
+ )
271
+
272
+ if analysis.extra_keys:
273
+ recommendations.append(
274
+ f"Found {len(analysis.extra_keys)} unrecognized configuration key(s)"
275
+ )
276
+
277
+ return recommendations
278
+
279
+
280
+ def load_and_analyze_config(config_path: Optional[Union[str, Path]] = None) -> ConfigAnalysis:
281
+ """Load configuration from file and analyze it."""
282
+ from tunacode.utils.user_configuration import load_config
283
+
284
+ if config_path:
285
+ try:
286
+ with open(config_path, "r") as f:
287
+ user_config = json.load(f)
288
+ except (FileNotFoundError, json.JSONDecodeError) as e:
289
+ raise ValueError(f"Failed to load config from {config_path}: {e}")
290
+ else:
291
+ user_config = load_config()
292
+ if user_config is None:
293
+ raise ValueError("No user configuration found")
294
+
295
+ comparator = ConfigComparator()
296
+ return comparator.analyze_config(user_config)
297
+
298
+
299
+ def create_config_report(analysis: ConfigAnalysis) -> str:
300
+ """Create a human-readable report of configuration analysis."""
301
+ stats = ConfigComparator().get_summary_stats(analysis)
302
+
303
+ report = [
304
+ "Configuration Analysis Report",
305
+ "=" * 50,
306
+ f"Total keys analyzed: {stats['total_keys_analyzed']}",
307
+ f"Custom keys: {stats['custom_keys_count']} ({stats['custom_percentage']:.1f}%)",
308
+ f"Missing keys: {stats['missing_keys_count']}",
309
+ f"Extra keys: {stats['extra_keys_count']}",
310
+ f"Type mismatches: {stats['type_mismatches_count']}",
311
+ f"Sections analyzed: {stats['sections_analyzed']}",
312
+ f"Configuration healthy: {'Yes' if stats['has_issues'] else 'No'}",
313
+ "",
314
+ ]
315
+
316
+ if analysis.custom_keys:
317
+ report.append("Custom Values:")
318
+ for key in sorted(analysis.custom_keys):
319
+ report.append(f" ✓ {key}")
320
+ report.append("")
321
+
322
+ if analysis.missing_keys:
323
+ report.append("Missing Keys:")
324
+ for key in sorted(analysis.missing_keys):
325
+ report.append(f" ⚠ {key}")
326
+ report.append("")
327
+
328
+ if analysis.type_mismatches:
329
+ report.append("Type Mismatches:")
330
+ for key in sorted(analysis.type_mismatches):
331
+ report.append(f" ✗ {key}")
332
+ report.append("")
333
+
334
+ recommendations = ConfigComparator().get_recommendations(analysis)
335
+ if recommendations:
336
+ report.append("Recommendations:")
337
+ for rec in recommendations:
338
+ report.append(f" • {rec}")
339
+
340
+ return "\n".join(report)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tunacode-cli
3
- Version: 0.0.75
3
+ Version: 0.0.76.1
4
4
  Summary: Your agentic CLI developer.
5
5
  Project-URL: Homepage, https://tunacode.xyz/
6
6
  Project-URL: Repository, https://github.com/alchemiststudiosDOTai/tunacode
@@ -75,6 +75,27 @@ pip install tunacode-cli
75
75
 
76
76
  For detailed installation and configuration instructions, see the [**Getting Started Guide**](documentation/user/getting-started.md).
77
77
 
78
+ ## Quickstart
79
+
80
+ ```bash
81
+ # 1) Install
82
+ pip install tunacode-cli
83
+
84
+ # 2) Launch the CLI
85
+ tunacode --wizard # guided setup (enter an API key, pick a model)
86
+
87
+ # 3) Try common commands in the REPL
88
+ /help # see commands
89
+ /model # explore models and set a default
90
+ /plan # enter read-only Plan Mode
91
+ ```
92
+
93
+ Tip: You can also skip the wizard and set everything via flags:
94
+
95
+ ```bash
96
+ tunacode --model openai:gpt-4.1 --key sk-your-key
97
+ ```
98
+
78
99
  ## Development Installation
79
100
 
80
101
  For contributors and developers who want to work on TunaCode:
@@ -87,13 +108,13 @@ cd tunacode
87
108
  # Quick setup (recommended)
88
109
  ./scripts/setup_dev_env.sh
89
110
 
90
- # Or manual setup
91
- python3 -m venv venv
92
- source venv/bin/activate # On Windows: venv\Scripts\activate
93
- pip install -e ".[dev]"
111
+ # Or manual setup with UV (recommended)
112
+ uv venv
113
+ source .venv/bin/activate # On Windows: .venv\Scripts\activate
114
+ uv pip install -e ".[dev]"
94
115
 
95
116
  # Verify installation
96
- python -m tunacode --version
117
+ tunacode --version
97
118
  ```
98
119
 
99
120
  See the [Hatch Build System Guide](documentation/development/hatch-build-system.md) for detailed instructions on the development environment.
@@ -102,6 +123,42 @@ See the [Hatch Build System Guide](documentation/development/hatch-build-system.
102
123
 
103
124
  Choose your AI provider and set your API key. For more details, see the [Configuration Section](documentation/user/getting-started.md#2-configuration) in the Getting Started Guide. For local models (LM Studio, Ollama, etc.), see the [Local Models Setup Guide](documentation/configuration/local-models.md).
104
125
 
126
+ ### New: Enhanced Model Selection
127
+
128
+ TunaCode now automatically saves your model selection for future sessions. When you choose a model using `/model <provider:name>`, it will be remembered across restarts.
129
+
130
+ **If you encounter API key errors**, you can manually create a configuration file that matches the current schema:
131
+
132
+ ```bash
133
+ # Create the config file
134
+ cat > ~/.config/tunacode.json << 'EOF'
135
+ {
136
+ "default_model": "openai:gpt-4.1",
137
+ "env": {
138
+ "OPENAI_API_KEY": "your-openai-api-key-here",
139
+ "ANTHROPIC_API_KEY": "",
140
+ "GEMINI_API_KEY": "",
141
+ "OPENROUTER_API_KEY": ""
142
+ },
143
+ "settings": {
144
+ "enable_streaming": true,
145
+ "max_iterations": 40,
146
+ "context_window_size": 200000
147
+ },
148
+ "mcpServers": {}
149
+ }
150
+ EOF
151
+ ```
152
+
153
+ Replace the model and API key with your preferred provider and credentials. Examples:
154
+ - `openai:gpt-4.1` (requires OPENAI_API_KEY)
155
+ - `anthropic:claude-4-sonnet-20250522` (requires ANTHROPIC_API_KEY)
156
+ - `google:gemini-2.5-pro` (requires GEMINI_API_KEY)
157
+
158
+ ### ⚠️ Important Notice
159
+
160
+ I apologize for any recent issues with model selection and configuration. I'm actively working to fix these problems and improve the overall stability of TunaCode. Your patience and feedback are greatly appreciated as I work to make the tool more reliable.
161
+
105
162
  ### Recommended Models
106
163
 
107
164
  Based on extensive testing, these models provide the best performance: