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/cli/commands/implementations/model.py +33 -5
- 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 +40 -4
- tunacode/core/agents/agent_components/streaming.py +268 -0
- tunacode/core/agents/main.py +30 -15
- tunacode/core/setup/config_setup.py +67 -224
- tunacode/core/setup/config_wizard.py +229 -0
- tunacode/core/state.py +3 -1
- tunacode/prompts/system.md +43 -29
- tunacode/ui/config_dashboard.py +567 -0
- tunacode/ui/panels.py +92 -9
- tunacode/utils/config_comparator.py +340 -0
- {tunacode_cli-0.0.75.dist-info → tunacode_cli-0.0.76.1.dist-info}/METADATA +63 -6
- {tunacode_cli-0.0.75.dist-info → tunacode_cli-0.0.76.1.dist-info}/RECORD +21 -16
- {tunacode_cli-0.0.75.dist-info → tunacode_cli-0.0.76.1.dist-info}/WHEEL +0 -0
- {tunacode_cli-0.0.75.dist-info → tunacode_cli-0.0.76.1.dist-info}/entry_points.txt +0 -0
- {tunacode_cli-0.0.75.dist-info → tunacode_cli-0.0.76.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -11,12 +11,16 @@ from tunacode.configuration.defaults import DEFAULT_USER_CONFIG
|
|
|
11
11
|
from tunacode.configuration.models import ModelRegistry
|
|
12
12
|
from tunacode.constants import APP_NAME, CONFIG_FILE_NAME, UI_COLORS
|
|
13
13
|
from tunacode.core.setup.base import BaseSetup
|
|
14
|
+
from tunacode.core.setup.config_wizard import ConfigWizard
|
|
14
15
|
from tunacode.core.state import StateManager
|
|
15
16
|
from tunacode.exceptions import ConfigurationError
|
|
16
17
|
from tunacode.types import ConfigFile, ConfigPath, UserConfig
|
|
17
18
|
from tunacode.ui import console as ui
|
|
18
19
|
from tunacode.utils import system, user_configuration
|
|
19
|
-
from tunacode.utils.api_key_validation import
|
|
20
|
+
from tunacode.utils.api_key_validation import (
|
|
21
|
+
get_required_api_key_for_model,
|
|
22
|
+
validate_api_key_for_model,
|
|
23
|
+
)
|
|
20
24
|
from tunacode.utils.text_utils import key_to_title
|
|
21
25
|
|
|
22
26
|
|
|
@@ -105,14 +109,17 @@ class ConfigSetup(BaseSetup):
|
|
|
105
109
|
raise
|
|
106
110
|
|
|
107
111
|
if wizard_mode:
|
|
108
|
-
|
|
112
|
+
wizard = ConfigWizard(self.state_manager, self.model_registry, self.config_file)
|
|
113
|
+
await wizard.run_onboarding()
|
|
109
114
|
else:
|
|
110
115
|
await self._onboarding()
|
|
111
116
|
else:
|
|
112
|
-
# No config found - show CLI usage
|
|
117
|
+
# No config found - show CLI usage and continue with safe defaults (no crash)
|
|
113
118
|
from tunacode.ui.console import console
|
|
114
119
|
|
|
115
|
-
console.print(
|
|
120
|
+
console.print(
|
|
121
|
+
"\n[bold yellow]No configuration found — using safe defaults.[/bold yellow]"
|
|
122
|
+
)
|
|
116
123
|
console.print("\n[bold]Quick Setup:[/bold]")
|
|
117
124
|
console.print("Configure TunaCode using CLI flags:")
|
|
118
125
|
console.print("\n[blue]Examples:[/blue]")
|
|
@@ -127,16 +134,19 @@ class ConfigSetup(BaseSetup):
|
|
|
127
134
|
console.print("\n[yellow]Run 'tunacode --help' for more options[/yellow]\n")
|
|
128
135
|
console.print("\n[cyan]Or use --wizard for guided setup[/cyan]\n")
|
|
129
136
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
137
|
+
# Initialize in-memory defaults so we don't crash
|
|
138
|
+
self.state_manager.session.user_config = DEFAULT_USER_CONFIG.copy()
|
|
139
|
+
# Mark config as not fully validated for the fast path
|
|
140
|
+
setattr(self.state_manager, "_config_valid", False)
|
|
133
141
|
|
|
134
142
|
if not self.state_manager.session.user_config.get("default_model"):
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
143
|
+
# Gracefully apply default model instead of crashing
|
|
144
|
+
self.state_manager.session.user_config["default_model"] = DEFAULT_USER_CONFIG[
|
|
145
|
+
"default_model"
|
|
146
|
+
]
|
|
147
|
+
await ui.warning(
|
|
148
|
+
"No default model set in config; applying safe default "
|
|
149
|
+
f"'{self.state_manager.session.user_config['default_model']}'."
|
|
140
150
|
)
|
|
141
151
|
|
|
142
152
|
# Validate API key exists for the selected model
|
|
@@ -145,10 +155,42 @@ class ConfigSetup(BaseSetup):
|
|
|
145
155
|
model, self.state_manager.session.user_config
|
|
146
156
|
)
|
|
147
157
|
if not is_valid:
|
|
148
|
-
|
|
158
|
+
# Try to pick a fallback model based on whichever provider has a key configured
|
|
159
|
+
fallback = self._pick_fallback_model(self.state_manager.session.user_config)
|
|
160
|
+
if fallback and fallback != model:
|
|
161
|
+
await ui.warning(
|
|
162
|
+
"API key missing for selected model; switching to configured provider: "
|
|
163
|
+
f"'{fallback}'."
|
|
164
|
+
)
|
|
165
|
+
self.state_manager.session.user_config["default_model"] = fallback
|
|
166
|
+
model = fallback
|
|
167
|
+
else:
|
|
168
|
+
# No suitable fallback; continue without crashing but mark invalid
|
|
169
|
+
await ui.warning(
|
|
170
|
+
(error_msg or "API key missing for model")
|
|
171
|
+
+ "\nContinuing without provider initialization; run 'tunacode --setup' later."
|
|
172
|
+
)
|
|
173
|
+
setattr(self.state_manager, "_config_valid", False)
|
|
149
174
|
|
|
150
175
|
self.state_manager.session.current_model = model
|
|
151
176
|
|
|
177
|
+
def _pick_fallback_model(self, user_config: UserConfig) -> str | None:
|
|
178
|
+
"""Select a reasonable fallback model based on configured API keys."""
|
|
179
|
+
env = (user_config or {}).get("env", {})
|
|
180
|
+
|
|
181
|
+
# Preference order: OpenAI → Anthropic → Google → OpenRouter
|
|
182
|
+
if env.get("OPENAI_API_KEY", "").strip():
|
|
183
|
+
return "openai:gpt-4o"
|
|
184
|
+
if env.get("ANTHROPIC_API_KEY", "").strip():
|
|
185
|
+
return "anthropic:claude-sonnet-4"
|
|
186
|
+
if env.get("GEMINI_API_KEY", "").strip():
|
|
187
|
+
return "google:gemini-2.5-flash"
|
|
188
|
+
if env.get("OPENROUTER_API_KEY", "").strip():
|
|
189
|
+
# Use the project default when OpenRouter is configured
|
|
190
|
+
return DEFAULT_USER_CONFIG.get("default_model", "openrouter:openai/gpt-4.1")
|
|
191
|
+
|
|
192
|
+
return None
|
|
193
|
+
|
|
152
194
|
async def validate(self) -> bool:
|
|
153
195
|
"""Validate that configuration is properly set up."""
|
|
154
196
|
# Check that we have a user config
|
|
@@ -168,6 +210,18 @@ class ConfigSetup(BaseSetup):
|
|
|
168
210
|
# Store error message for later use
|
|
169
211
|
setattr(self.state_manager, "_config_error", error_msg)
|
|
170
212
|
|
|
213
|
+
# Provide actionable guidance for manual setup
|
|
214
|
+
required_key, provider_name = get_required_api_key_for_model(model)
|
|
215
|
+
setup_hint = (
|
|
216
|
+
f"Missing API key for {provider_name}.\n"
|
|
217
|
+
f"Either run 'tunacode --wizard' (recommended) or add it manually to: {self.config_file}\n\n"
|
|
218
|
+
"Example snippet (add under 'env'):\n"
|
|
219
|
+
' "env": {\n'
|
|
220
|
+
f' "{required_key or "PROVIDER_API_KEY"}": "your-key-here"\n'
|
|
221
|
+
" }\n"
|
|
222
|
+
)
|
|
223
|
+
await ui.error(setup_hint)
|
|
224
|
+
|
|
171
225
|
# Cache result for fastpath
|
|
172
226
|
if valid:
|
|
173
227
|
setattr(self.state_manager, "_config_valid", True)
|
|
@@ -373,214 +427,3 @@ class ConfigSetup(BaseSetup):
|
|
|
373
427
|
await ui.success(f"Configuration saved to: {self.config_file}")
|
|
374
428
|
except ConfigurationError as e:
|
|
375
429
|
await ui.error(str(e))
|
|
376
|
-
|
|
377
|
-
async def _wizard_onboarding(self):
|
|
378
|
-
"""Run enhanced wizard-style onboarding process for new users."""
|
|
379
|
-
initial_config = json.dumps(self.state_manager.session.user_config, sort_keys=True)
|
|
380
|
-
|
|
381
|
-
# Welcome message with provider guidance
|
|
382
|
-
await ui.panel(
|
|
383
|
-
"Welcome to TunaCode Setup Wizard!",
|
|
384
|
-
"This guided setup will help you configure TunaCode in under 5 minutes.\n"
|
|
385
|
-
"We'll help you choose a provider, set up your API keys, and configure your preferred model.",
|
|
386
|
-
border_style=UI_COLORS["primary"],
|
|
387
|
-
)
|
|
388
|
-
|
|
389
|
-
# Step 1: Provider selection with detailed guidance
|
|
390
|
-
await self._wizard_step1_provider_selection()
|
|
391
|
-
|
|
392
|
-
# Step 2: API key setup with provider-specific guidance
|
|
393
|
-
await self._wizard_step2_api_key_setup()
|
|
394
|
-
|
|
395
|
-
# Step 3: Model selection with smart recommendations
|
|
396
|
-
await self._wizard_step3_model_selection()
|
|
397
|
-
|
|
398
|
-
# Step 4: Optional settings configuration
|
|
399
|
-
await self._wizard_step4_optional_settings()
|
|
400
|
-
|
|
401
|
-
# Save configuration and finish
|
|
402
|
-
current_config = json.dumps(self.state_manager.session.user_config, sort_keys=True)
|
|
403
|
-
if initial_config != current_config:
|
|
404
|
-
try:
|
|
405
|
-
user_configuration.save_config(self.state_manager)
|
|
406
|
-
await ui.panel(
|
|
407
|
-
"Setup Complete!",
|
|
408
|
-
f"Configuration saved to: [bold]{self.config_file}[/bold]\n\n"
|
|
409
|
-
"You're ready to start using TunaCode!\n"
|
|
410
|
-
"Use [green]/quickstart[/green] anytime for a tutorial.",
|
|
411
|
-
border_style=UI_COLORS["success"],
|
|
412
|
-
)
|
|
413
|
-
except ConfigurationError as e:
|
|
414
|
-
await ui.error(str(e))
|
|
415
|
-
|
|
416
|
-
async def _wizard_step1_provider_selection(self):
|
|
417
|
-
"""Wizard step 1: Provider selection with detailed explanations."""
|
|
418
|
-
provider_info = {
|
|
419
|
-
"1": {
|
|
420
|
-
"name": "OpenRouter",
|
|
421
|
-
"description": "Access to multiple models (GPT-4, Claude, Gemini, etc.)",
|
|
422
|
-
"signup": "https://openrouter.ai/",
|
|
423
|
-
"key_name": "OPENROUTER_API_KEY",
|
|
424
|
-
},
|
|
425
|
-
"2": {
|
|
426
|
-
"name": "OpenAI",
|
|
427
|
-
"description": "GPT-4 models",
|
|
428
|
-
"signup": "https://platform.openai.com/signup",
|
|
429
|
-
"key_name": "OPENAI_API_KEY",
|
|
430
|
-
},
|
|
431
|
-
"3": {
|
|
432
|
-
"name": "Anthropic",
|
|
433
|
-
"description": "Claude-3 models",
|
|
434
|
-
"signup": "https://console.anthropic.com/",
|
|
435
|
-
"key_name": "ANTHROPIC_API_KEY",
|
|
436
|
-
},
|
|
437
|
-
"4": {
|
|
438
|
-
"name": "Google",
|
|
439
|
-
"description": "Gemini models",
|
|
440
|
-
"signup": "https://ai.google.dev/",
|
|
441
|
-
"key_name": "GEMINI_API_KEY",
|
|
442
|
-
},
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
message = "Choose your AI provider:\n\n"
|
|
446
|
-
for key, info in provider_info.items():
|
|
447
|
-
message += f" {key} - {info['name']}: {info['description']}\n"
|
|
448
|
-
|
|
449
|
-
await ui.panel("Provider Selection", message, border_style=UI_COLORS["primary"])
|
|
450
|
-
|
|
451
|
-
while True:
|
|
452
|
-
choice = await ui.input(
|
|
453
|
-
"wizard_provider",
|
|
454
|
-
pretext=" Choose provider (1-4): ",
|
|
455
|
-
state_manager=self.state_manager,
|
|
456
|
-
)
|
|
457
|
-
|
|
458
|
-
if choice.strip() in provider_info:
|
|
459
|
-
selected = provider_info[choice.strip()]
|
|
460
|
-
self._wizard_selected_provider = selected
|
|
461
|
-
|
|
462
|
-
await ui.success(f"Selected: {selected['name']}")
|
|
463
|
-
await ui.info(f"Sign up at: {selected['signup']}")
|
|
464
|
-
break
|
|
465
|
-
else:
|
|
466
|
-
await ui.error("Please enter 1, 2, 3, or 4")
|
|
467
|
-
|
|
468
|
-
async def _wizard_step2_api_key_setup(self):
|
|
469
|
-
"""Wizard step 2: API key setup with provider-specific guidance."""
|
|
470
|
-
provider = self._wizard_selected_provider
|
|
471
|
-
|
|
472
|
-
message = f"Enter your {provider['name']} API key:\n\n"
|
|
473
|
-
message += f"Get your key from: {provider['signup']}\n"
|
|
474
|
-
message += "Your key will be stored securely in your local config"
|
|
475
|
-
|
|
476
|
-
await ui.panel(f"{provider['name']} API Key", message, border_style=UI_COLORS["primary"])
|
|
477
|
-
|
|
478
|
-
while True:
|
|
479
|
-
api_key = await ui.input(
|
|
480
|
-
"wizard_api_key",
|
|
481
|
-
pretext=f" {provider['name']} API Key: ",
|
|
482
|
-
is_password=True,
|
|
483
|
-
state_manager=self.state_manager,
|
|
484
|
-
)
|
|
485
|
-
|
|
486
|
-
if api_key.strip():
|
|
487
|
-
# Ensure env dict exists
|
|
488
|
-
if "env" not in self.state_manager.session.user_config:
|
|
489
|
-
self.state_manager.session.user_config["env"] = {}
|
|
490
|
-
|
|
491
|
-
self.state_manager.session.user_config["env"][provider["key_name"]] = (
|
|
492
|
-
api_key.strip()
|
|
493
|
-
)
|
|
494
|
-
await ui.success("API key saved successfully!")
|
|
495
|
-
break
|
|
496
|
-
else:
|
|
497
|
-
await ui.error("API key cannot be empty")
|
|
498
|
-
|
|
499
|
-
async def _wizard_step3_model_selection(self):
|
|
500
|
-
"""Wizard step 3: Model selection with smart recommendations."""
|
|
501
|
-
provider = self._wizard_selected_provider
|
|
502
|
-
|
|
503
|
-
# Provide smart recommendations based on provider
|
|
504
|
-
recommendations = {
|
|
505
|
-
"OpenAI": [
|
|
506
|
-
("openai:gpt-4o", "GPT-4o flagship multimodal model (recommended)"),
|
|
507
|
-
("openai:gpt-4.1", "Latest GPT-4.1 with enhanced coding"),
|
|
508
|
-
("openai:o3", "Advanced reasoning model for complex tasks"),
|
|
509
|
-
],
|
|
510
|
-
"Anthropic": [
|
|
511
|
-
("anthropic:claude-sonnet-4", "Claude Sonnet 4 latest generation (recommended)"),
|
|
512
|
-
("anthropic:claude-opus-4.1", "Most capable Claude with extended thinking"),
|
|
513
|
-
("anthropic:claude-3.5-sonnet", "Claude 3.5 Sonnet proven performance"),
|
|
514
|
-
],
|
|
515
|
-
"OpenRouter": [
|
|
516
|
-
(
|
|
517
|
-
"openrouter:anthropic/claude-sonnet-4",
|
|
518
|
-
"Claude Sonnet 4 via OpenRouter (recommended)",
|
|
519
|
-
),
|
|
520
|
-
("openrouter:openai/gpt-4.1", "GPT-4.1 via OpenRouter"),
|
|
521
|
-
("openrouter:google/gemini-2.5-flash", "Google Gemini 2.5 Flash latest"),
|
|
522
|
-
],
|
|
523
|
-
"Google": [
|
|
524
|
-
(
|
|
525
|
-
"google:gemini-2.5-pro",
|
|
526
|
-
"Gemini 2.5 Pro with thinking capabilities (recommended)",
|
|
527
|
-
),
|
|
528
|
-
("google:gemini-2.5-flash", "Gemini 2.5 Flash best price-performance"),
|
|
529
|
-
("google:gemini-2.0-flash", "Gemini 2.0 Flash with native tool use"),
|
|
530
|
-
],
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
models = recommendations.get(provider["name"], [])
|
|
534
|
-
message = f"Choose your default {provider['name']} model:\n\n"
|
|
535
|
-
|
|
536
|
-
for i, (model_id, description) in enumerate(models, 1):
|
|
537
|
-
message += f" {i} - {description}\n"
|
|
538
|
-
|
|
539
|
-
message += "\nYou can change this later with [green]/model[/green]"
|
|
540
|
-
|
|
541
|
-
await ui.panel("Model Selection", message, border_style=UI_COLORS["primary"])
|
|
542
|
-
|
|
543
|
-
while True:
|
|
544
|
-
choice = await ui.input(
|
|
545
|
-
"wizard_model",
|
|
546
|
-
pretext=f" Choose model (1-{len(models)}): ",
|
|
547
|
-
state_manager=self.state_manager,
|
|
548
|
-
)
|
|
549
|
-
|
|
550
|
-
try:
|
|
551
|
-
index = int(choice.strip()) - 1
|
|
552
|
-
if 0 <= index < len(models):
|
|
553
|
-
selected_model = models[index][0]
|
|
554
|
-
self.state_manager.session.user_config["default_model"] = selected_model
|
|
555
|
-
await ui.success(f"Selected: {selected_model}")
|
|
556
|
-
break
|
|
557
|
-
else:
|
|
558
|
-
await ui.error(f"Please enter a number between 1 and {len(models)}")
|
|
559
|
-
except ValueError:
|
|
560
|
-
await ui.error("Please enter a valid number")
|
|
561
|
-
|
|
562
|
-
async def _wizard_step4_optional_settings(self):
|
|
563
|
-
"""Wizard step 4: Optional settings configuration."""
|
|
564
|
-
message = "Configure optional settings:\n\n"
|
|
565
|
-
message += "• Tutorial: Enable interactive tutorial for new users\n"
|
|
566
|
-
message += "\nSkip this step to use recommended defaults"
|
|
567
|
-
|
|
568
|
-
await ui.panel("Optional Settings", message, border_style=UI_COLORS["primary"])
|
|
569
|
-
|
|
570
|
-
# Ask about tutorial
|
|
571
|
-
tutorial_choice = await ui.input(
|
|
572
|
-
"wizard_tutorial",
|
|
573
|
-
pretext=" Enable tutorial for new users? [Y/n]: ",
|
|
574
|
-
state_manager=self.state_manager,
|
|
575
|
-
)
|
|
576
|
-
|
|
577
|
-
enable_tutorial = tutorial_choice.strip().lower() not in ["n", "no", "false"]
|
|
578
|
-
|
|
579
|
-
if "settings" not in self.state_manager.session.user_config:
|
|
580
|
-
self.state_manager.session.user_config["settings"] = {}
|
|
581
|
-
|
|
582
|
-
self.state_manager.session.user_config["settings"]["enable_tutorial"] = enable_tutorial
|
|
583
|
-
|
|
584
|
-
# Streaming is always enabled - no user choice needed
|
|
585
|
-
|
|
586
|
-
await ui.info("Optional settings configured!")
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
"""Module: tunacode.core.setup.config_wizard
|
|
2
|
+
|
|
3
|
+
Wizard-style onboarding helpers extracted from ConfigSetup to reduce file size
|
|
4
|
+
and keep responsibilities focused.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Dict, Optional
|
|
10
|
+
|
|
11
|
+
from tunacode.constants import UI_COLORS
|
|
12
|
+
from tunacode.exceptions import ConfigurationError
|
|
13
|
+
from tunacode.ui import console as ui
|
|
14
|
+
from tunacode.utils import user_configuration
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ConfigWizard:
|
|
18
|
+
"""Encapsulates the interactive configuration wizard flow."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, state_manager, model_registry, config_file: Path):
|
|
21
|
+
self.state_manager = state_manager
|
|
22
|
+
self.model_registry = model_registry
|
|
23
|
+
self.config_file = config_file
|
|
24
|
+
self._wizard_selected_provider: Optional[Dict[str, str]] = None
|
|
25
|
+
|
|
26
|
+
async def run_onboarding(self) -> None:
|
|
27
|
+
"""Run enhanced wizard-style onboarding process for new users."""
|
|
28
|
+
initial_config = json.dumps(self.state_manager.session.user_config, sort_keys=True)
|
|
29
|
+
|
|
30
|
+
# Welcome message with provider guidance
|
|
31
|
+
await ui.panel(
|
|
32
|
+
"Welcome to TunaCode Setup Wizard!",
|
|
33
|
+
"This guided setup will help you configure TunaCode in under 5 minutes.\n"
|
|
34
|
+
"We'll help you choose a provider, set up your API keys, and configure your preferred model.",
|
|
35
|
+
border_style=UI_COLORS["primary"],
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
# Steps
|
|
39
|
+
await self._step1_provider_selection()
|
|
40
|
+
await self._step2_api_key_setup()
|
|
41
|
+
await self._step3_model_selection()
|
|
42
|
+
await self._step4_optional_settings()
|
|
43
|
+
|
|
44
|
+
# Save configuration and finish
|
|
45
|
+
current_config = json.dumps(self.state_manager.session.user_config, sort_keys=True)
|
|
46
|
+
if initial_config != current_config:
|
|
47
|
+
try:
|
|
48
|
+
user_configuration.save_config(self.state_manager)
|
|
49
|
+
await ui.panel(
|
|
50
|
+
"Setup Complete!",
|
|
51
|
+
f"Configuration saved to: [bold]{self.config_file}[/bold]\n\n"
|
|
52
|
+
"You're ready to start using TunaCode!\n"
|
|
53
|
+
"Use [green]/quickstart[/green] anytime for a tutorial.",
|
|
54
|
+
border_style=UI_COLORS["success"],
|
|
55
|
+
)
|
|
56
|
+
except ConfigurationError as e:
|
|
57
|
+
await ui.error(str(e))
|
|
58
|
+
|
|
59
|
+
async def _step1_provider_selection(self) -> None:
|
|
60
|
+
"""Wizard step 1: Provider selection with detailed explanations."""
|
|
61
|
+
provider_info = {
|
|
62
|
+
"1": {
|
|
63
|
+
"name": "OpenRouter",
|
|
64
|
+
"description": "Access to multiple models (GPT-4, Claude, Gemini, etc.)",
|
|
65
|
+
"signup": "https://openrouter.ai/",
|
|
66
|
+
"key_name": "OPENROUTER_API_KEY",
|
|
67
|
+
},
|
|
68
|
+
"2": {
|
|
69
|
+
"name": "OpenAI",
|
|
70
|
+
"description": "GPT-4 models",
|
|
71
|
+
"signup": "https://platform.openai.com/signup",
|
|
72
|
+
"key_name": "OPENAI_API_KEY",
|
|
73
|
+
},
|
|
74
|
+
"3": {
|
|
75
|
+
"name": "Anthropic",
|
|
76
|
+
"description": "Claude-3 models",
|
|
77
|
+
"signup": "https://console.anthropic.com/",
|
|
78
|
+
"key_name": "ANTHROPIC_API_KEY",
|
|
79
|
+
},
|
|
80
|
+
"4": {
|
|
81
|
+
"name": "Google",
|
|
82
|
+
"description": "Gemini models",
|
|
83
|
+
"signup": "https://ai.google.dev/",
|
|
84
|
+
"key_name": "GEMINI_API_KEY",
|
|
85
|
+
},
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
message = "Choose your AI provider:\n\n"
|
|
89
|
+
for key, info in provider_info.items():
|
|
90
|
+
message += f" {key} - {info['name']}: {info['description']}\n"
|
|
91
|
+
|
|
92
|
+
await ui.panel("Provider Selection", message, border_style=UI_COLORS["primary"])
|
|
93
|
+
|
|
94
|
+
while True:
|
|
95
|
+
choice = await ui.input(
|
|
96
|
+
"wizard_provider",
|
|
97
|
+
pretext=" Choose provider (1-4): ",
|
|
98
|
+
state_manager=self.state_manager,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
if choice.strip() in provider_info:
|
|
102
|
+
selected = provider_info[choice.strip()]
|
|
103
|
+
self._wizard_selected_provider = selected
|
|
104
|
+
|
|
105
|
+
await ui.success(f"Selected: {selected['name']}")
|
|
106
|
+
await ui.info(f"Sign up at: {selected['signup']}")
|
|
107
|
+
break
|
|
108
|
+
else:
|
|
109
|
+
await ui.error("Please enter 1, 2, 3, or 4")
|
|
110
|
+
|
|
111
|
+
async def _step2_api_key_setup(self) -> None:
|
|
112
|
+
"""Wizard step 2: API key setup with provider-specific guidance."""
|
|
113
|
+
provider = self._wizard_selected_provider
|
|
114
|
+
|
|
115
|
+
message = f"Enter your {provider['name']} API key:\n\n"
|
|
116
|
+
message += f"Get your key from: {provider['signup']}\n"
|
|
117
|
+
message += "Your key will be stored securely in your local config"
|
|
118
|
+
|
|
119
|
+
await ui.panel(f"{provider['name']} API Key", message, border_style=UI_COLORS["primary"])
|
|
120
|
+
|
|
121
|
+
while True:
|
|
122
|
+
api_key = await ui.input(
|
|
123
|
+
"wizard_api_key",
|
|
124
|
+
pretext=f" {provider['name']} API Key: ",
|
|
125
|
+
is_password=True,
|
|
126
|
+
state_manager=self.state_manager,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
if api_key.strip():
|
|
130
|
+
# Ensure env dict exists
|
|
131
|
+
if "env" not in self.state_manager.session.user_config:
|
|
132
|
+
self.state_manager.session.user_config["env"] = {}
|
|
133
|
+
|
|
134
|
+
self.state_manager.session.user_config["env"][provider["key_name"]] = (
|
|
135
|
+
api_key.strip()
|
|
136
|
+
)
|
|
137
|
+
await ui.success("API key saved successfully!")
|
|
138
|
+
break
|
|
139
|
+
else:
|
|
140
|
+
await ui.error("API key cannot be empty")
|
|
141
|
+
|
|
142
|
+
async def _step3_model_selection(self) -> None:
|
|
143
|
+
"""Wizard step 3: Model selection with smart recommendations."""
|
|
144
|
+
provider = self._wizard_selected_provider
|
|
145
|
+
|
|
146
|
+
# Provide smart recommendations based on provider
|
|
147
|
+
recommendations = {
|
|
148
|
+
"OpenAI": [
|
|
149
|
+
("openai:gpt-4o", "GPT-4o flagship multimodal model (recommended)"),
|
|
150
|
+
("openai:gpt-4.1", "Latest GPT-4.1 with enhanced coding"),
|
|
151
|
+
("openai:o3", "Advanced reasoning model for complex tasks"),
|
|
152
|
+
],
|
|
153
|
+
"Anthropic": [
|
|
154
|
+
("anthropic:claude-sonnet-4", "Claude Sonnet 4 latest generation (recommended)"),
|
|
155
|
+
("anthropic:claude-opus-4.1", "Most capable Claude with extended thinking"),
|
|
156
|
+
("anthropic:claude-3.5-sonnet", "Claude 3.5 Sonnet proven performance"),
|
|
157
|
+
],
|
|
158
|
+
"OpenRouter": [
|
|
159
|
+
(
|
|
160
|
+
"openrouter:anthropic/claude-sonnet-4",
|
|
161
|
+
"Claude Sonnet 4 via OpenRouter (recommended)",
|
|
162
|
+
),
|
|
163
|
+
("openrouter:openai/gpt-4.1", "GPT-4.1 via OpenRouter"),
|
|
164
|
+
("openrouter:google/gemini-2.5-flash", "Google Gemini 2.5 Flash latest"),
|
|
165
|
+
],
|
|
166
|
+
"Google": [
|
|
167
|
+
(
|
|
168
|
+
"google:gemini-2.5-pro",
|
|
169
|
+
"Gemini 2.5 Pro with thinking capabilities (recommended)",
|
|
170
|
+
),
|
|
171
|
+
("google:gemini-2.5-flash", "Gemini 2.5 Flash best price-performance"),
|
|
172
|
+
("google:gemini-2.0-flash", "Gemini 2.0 Flash with native tool use"),
|
|
173
|
+
],
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
models = recommendations.get(provider["name"], [])
|
|
177
|
+
message = f"Choose your default {provider['name']} model:\n\n"
|
|
178
|
+
|
|
179
|
+
for i, (model_id, description) in enumerate(models, 1):
|
|
180
|
+
message += f" {i} - {description}\n"
|
|
181
|
+
|
|
182
|
+
message += "\nYou can change this later with [green]/model[/green]"
|
|
183
|
+
|
|
184
|
+
await ui.panel("Model Selection", message, border_style=UI_COLORS["primary"])
|
|
185
|
+
|
|
186
|
+
while True:
|
|
187
|
+
choice = await ui.input(
|
|
188
|
+
"wizard_model",
|
|
189
|
+
pretext=f" Choose model (1-{len(models)}): ",
|
|
190
|
+
state_manager=self.state_manager,
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
try:
|
|
194
|
+
index = int(choice.strip()) - 1
|
|
195
|
+
if 0 <= index < len(models):
|
|
196
|
+
selected_model = models[index][0]
|
|
197
|
+
self.state_manager.session.user_config["default_model"] = selected_model
|
|
198
|
+
await ui.success(f"Selected: {selected_model}")
|
|
199
|
+
break
|
|
200
|
+
else:
|
|
201
|
+
await ui.error(f"Please enter a number between 1 and {len(models)}")
|
|
202
|
+
except ValueError:
|
|
203
|
+
await ui.error("Please enter a valid number")
|
|
204
|
+
|
|
205
|
+
async def _step4_optional_settings(self) -> None:
|
|
206
|
+
"""Wizard step 4: Optional settings configuration."""
|
|
207
|
+
message = "Configure optional settings:\n\n"
|
|
208
|
+
message += "• Tutorial: Enable interactive tutorial for new users\n"
|
|
209
|
+
message += "\nSkip this step to use recommended defaults"
|
|
210
|
+
|
|
211
|
+
await ui.panel("Optional Settings", message, border_style=UI_COLORS["primary"])
|
|
212
|
+
|
|
213
|
+
# Ask about tutorial
|
|
214
|
+
tutorial_choice = await ui.input(
|
|
215
|
+
"wizard_tutorial",
|
|
216
|
+
pretext=" Enable tutorial for new users? [Y/n]: ",
|
|
217
|
+
state_manager=self.state_manager,
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
enable_tutorial = tutorial_choice.strip().lower() not in ["n", "no", "false"]
|
|
221
|
+
|
|
222
|
+
if "settings" not in self.state_manager.session.user_config:
|
|
223
|
+
self.state_manager.session.user_config["settings"] = {}
|
|
224
|
+
|
|
225
|
+
self.state_manager.session.user_config["settings"]["enable_tutorial"] = enable_tutorial
|
|
226
|
+
|
|
227
|
+
# Streaming is always enabled - no user choice needed
|
|
228
|
+
|
|
229
|
+
await ui.info("Optional settings configured!")
|
tunacode/core/state.py
CHANGED
|
@@ -10,6 +10,7 @@ import uuid
|
|
|
10
10
|
from dataclasses import dataclass, field
|
|
11
11
|
from typing import TYPE_CHECKING, Any, Optional
|
|
12
12
|
|
|
13
|
+
from tunacode.configuration.defaults import DEFAULT_USER_CONFIG
|
|
13
14
|
from tunacode.types import (
|
|
14
15
|
DeviceId,
|
|
15
16
|
InputSessions,
|
|
@@ -39,7 +40,8 @@ class SessionState:
|
|
|
39
40
|
) # Keep as dict[str, Any] for agent instances
|
|
40
41
|
messages: MessageHistory = field(default_factory=list)
|
|
41
42
|
total_cost: float = 0.0
|
|
42
|
-
|
|
43
|
+
# Keep session default in sync with configuration default
|
|
44
|
+
current_model: ModelName = DEFAULT_USER_CONFIG["default_model"]
|
|
43
45
|
spinner: Optional[Any] = None
|
|
44
46
|
tool_ignore: list[ToolName] = field(default_factory=list)
|
|
45
47
|
yolo: bool = False
|