tunacode-cli 0.0.66__py3-none-any.whl → 0.0.68__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 (39) hide show
  1. tunacode/cli/commands/__init__.py +2 -0
  2. tunacode/cli/commands/implementations/__init__.py +2 -0
  3. tunacode/cli/commands/implementations/command_reload.py +48 -0
  4. tunacode/cli/commands/implementations/quickstart.py +43 -0
  5. tunacode/cli/commands/implementations/system.py +27 -3
  6. tunacode/cli/commands/registry.py +131 -1
  7. tunacode/cli/commands/slash/__init__.py +32 -0
  8. tunacode/cli/commands/slash/command.py +157 -0
  9. tunacode/cli/commands/slash/loader.py +134 -0
  10. tunacode/cli/commands/slash/processor.py +294 -0
  11. tunacode/cli/commands/slash/types.py +93 -0
  12. tunacode/cli/commands/slash/validator.py +399 -0
  13. tunacode/cli/main.py +4 -1
  14. tunacode/cli/repl.py +25 -0
  15. tunacode/configuration/defaults.py +1 -0
  16. tunacode/constants.py +1 -1
  17. tunacode/core/agents/agent_components/agent_helpers.py +14 -13
  18. tunacode/core/agents/main.py +1 -1
  19. tunacode/core/agents/utils.py +4 -3
  20. tunacode/core/setup/config_setup.py +231 -6
  21. tunacode/core/setup/coordinator.py +13 -5
  22. tunacode/core/setup/git_safety_setup.py +5 -1
  23. tunacode/exceptions.py +119 -5
  24. tunacode/setup.py +5 -2
  25. tunacode/tools/glob.py +9 -46
  26. tunacode/tools/grep.py +9 -51
  27. tunacode/tools/xml_helper.py +83 -0
  28. tunacode/tutorial/__init__.py +9 -0
  29. tunacode/tutorial/content.py +98 -0
  30. tunacode/tutorial/manager.py +182 -0
  31. tunacode/tutorial/steps.py +124 -0
  32. tunacode/ui/output.py +1 -1
  33. tunacode/utils/user_configuration.py +45 -0
  34. tunacode_cli-0.0.68.dist-info/METADATA +192 -0
  35. {tunacode_cli-0.0.66.dist-info → tunacode_cli-0.0.68.dist-info}/RECORD +38 -25
  36. tunacode_cli-0.0.66.dist-info/METADATA +0 -327
  37. {tunacode_cli-0.0.66.dist-info → tunacode_cli-0.0.68.dist-info}/WHEEL +0 -0
  38. {tunacode_cli-0.0.66.dist-info → tunacode_cli-0.0.68.dist-info}/entry_points.txt +0 -0
  39. {tunacode_cli-0.0.66.dist-info → tunacode_cli-0.0.68.dist-info}/licenses/LICENSE +0 -0
tunacode/tools/grep.py CHANGED
@@ -20,8 +20,6 @@ from functools import lru_cache
20
20
  from pathlib import Path
21
21
  from typing import Any, Dict, List, Optional, Union
22
22
 
23
- import defusedxml.ElementTree as ET
24
-
25
23
  from tunacode.configuration.defaults import DEFAULT_USER_CONFIG
26
24
  from tunacode.exceptions import TooBroadPatternError, ToolExecutionError
27
25
  from tunacode.tools.base import BaseTool
@@ -32,6 +30,7 @@ from tunacode.tools.grep_components import (
32
30
  SearchResult,
33
31
  )
34
32
  from tunacode.tools.grep_components.result_formatter import ResultFormatter
33
+ from tunacode.tools.xml_helper import load_parameters_schema_from_xml, load_prompt_from_xml
35
34
  from tunacode.utils.ripgrep import RipgrepExecutor
36
35
  from tunacode.utils.ripgrep import metrics as ripgrep_metrics
37
36
 
@@ -66,17 +65,10 @@ class ParallelGrep(BaseTool):
66
65
  Returns:
67
66
  str: The loaded prompt from XML or a default prompt
68
67
  """
69
- try:
70
- # Load prompt from XML file
71
- prompt_file = Path(__file__).parent / "prompts" / "grep_prompt.xml"
72
- if prompt_file.exists():
73
- tree = ET.parse(prompt_file)
74
- root = tree.getroot()
75
- description = root.find("description")
76
- if description is not None:
77
- return description.text.strip()
78
- except Exception as e:
79
- logger.warning(f"Failed to load XML prompt for grep: {e}")
68
+ # Try to load from XML helper
69
+ prompt = load_prompt_from_xml("grep")
70
+ if prompt:
71
+ return prompt
80
72
 
81
73
  # Fallback to default prompt
82
74
  return """A powerful search tool built on ripgrep
@@ -94,44 +86,10 @@ Usage:
94
86
  Returns:
95
87
  Dict containing the JSON schema for tool parameters
96
88
  """
97
- # Try to load from XML first
98
- try:
99
- prompt_file = Path(__file__).parent / "prompts" / "grep_prompt.xml"
100
- if prompt_file.exists():
101
- tree = ET.parse(prompt_file)
102
- root = tree.getroot()
103
- parameters = root.find("parameters")
104
- if parameters is not None:
105
- schema: Dict[str, Any] = {"type": "object", "properties": {}, "required": []}
106
- required_fields: List[str] = []
107
-
108
- for param in parameters.findall("parameter"):
109
- name = param.get("name")
110
- required = param.get("required", "false").lower() == "true"
111
- param_type = param.find("type")
112
- description = param.find("description")
113
-
114
- if name and param_type is not None:
115
- prop = {
116
- "type": param_type.text.strip(),
117
- "description": description.text.strip()
118
- if description is not None
119
- else "",
120
- }
121
-
122
- # Add enum values if present
123
- enums = param.findall("enum")
124
- if enums:
125
- prop["enum"] = [e.text.strip() for e in enums]
126
-
127
- schema["properties"][name] = prop
128
- if required:
129
- required_fields.append(name)
130
-
131
- schema["required"] = required_fields
132
- return schema
133
- except Exception as e:
134
- logger.warning(f"Failed to load parameters from XML for grep: {e}")
89
+ # Try to load from XML helper
90
+ schema = load_parameters_schema_from_xml("grep")
91
+ if schema:
92
+ return schema
135
93
 
136
94
  # Fallback to hardcoded schema
137
95
  return {
@@ -0,0 +1,83 @@
1
+ """Helper module for loading prompts and schemas from XML files."""
2
+
3
+ import logging
4
+ from functools import lru_cache
5
+ from pathlib import Path
6
+ from typing import Any, Dict, List, Optional
7
+
8
+ import defusedxml.ElementTree as ET
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ @lru_cache(maxsize=32)
14
+ def load_prompt_from_xml(tool_name: str) -> Optional[str]:
15
+ """Load and return the base prompt from XML file.
16
+
17
+ Args:
18
+ tool_name: Name of the tool (e.g., 'grep', 'glob')
19
+
20
+ Returns:
21
+ str: The loaded prompt from XML or None if not found
22
+ """
23
+ try:
24
+ prompt_file = Path(__file__).parent / "prompts" / f"{tool_name}_prompt.xml"
25
+ if prompt_file.exists():
26
+ tree = ET.parse(prompt_file)
27
+ root = tree.getroot()
28
+ description = root.find("description")
29
+ if description is not None:
30
+ return description.text.strip()
31
+ except Exception as e:
32
+ logger.warning(f"Failed to load XML prompt for {tool_name}: {e}")
33
+ return None
34
+
35
+
36
+ @lru_cache(maxsize=32)
37
+ def load_parameters_schema_from_xml(tool_name: str) -> Optional[Dict[str, Any]]:
38
+ """Load and return the parameters schema from XML file.
39
+
40
+ Args:
41
+ tool_name: Name of the tool (e.g., 'grep', 'glob')
42
+
43
+ Returns:
44
+ Dict containing the JSON schema for tool parameters or None if not found
45
+ """
46
+ try:
47
+ prompt_file = Path(__file__).parent / "prompts" / f"{tool_name}_prompt.xml"
48
+ if prompt_file.exists():
49
+ tree = ET.parse(prompt_file)
50
+ root = tree.getroot()
51
+ parameters = root.find("parameters")
52
+ if parameters is not None:
53
+ schema: Dict[str, Any] = {"type": "object", "properties": {}, "required": []}
54
+ required_fields: List[str] = []
55
+
56
+ for param in parameters.findall("parameter"):
57
+ name = param.get("name")
58
+ required = param.get("required", "false").lower() == "true"
59
+ param_type = param.find("type")
60
+ description = param.find("description")
61
+
62
+ if name and param_type is not None:
63
+ prop = {
64
+ "type": param_type.text.strip(),
65
+ "description": description.text.strip()
66
+ if description is not None
67
+ else "",
68
+ }
69
+
70
+ # Add enum values if present
71
+ enums = param.findall("enum")
72
+ if enums:
73
+ prop["enum"] = [e.text.strip() for e in enums]
74
+
75
+ schema["properties"][name] = prop
76
+ if required:
77
+ required_fields.append(name)
78
+
79
+ schema["required"] = required_fields
80
+ return schema
81
+ except Exception as e:
82
+ logger.warning(f"Failed to load parameters from XML for {tool_name}: {e}")
83
+ return None
@@ -0,0 +1,9 @@
1
+ """
2
+ Module: tunacode.tutorial
3
+
4
+ Tutorial system for TunaCode onboarding and user guidance.
5
+ """
6
+
7
+ from .manager import TutorialManager
8
+
9
+ __all__ = ["TutorialManager"]
@@ -0,0 +1,98 @@
1
+ """
2
+ Module: tunacode.tutorial.content
3
+
4
+ Tutorial content definitions and step configurations.
5
+ """
6
+
7
+ from typing import Dict, List
8
+
9
+ # Tutorial step content library
10
+ TUTORIAL_CONTENT: Dict[str, Dict[str, str]] = {
11
+ "welcome": {
12
+ "title": "🎯 Welcome to TunaCode!",
13
+ "content": """TunaCode is your AI-powered development assistant.
14
+
15
+ In this quick tutorial, you'll learn how to:
16
+ • Chat with AI about your code
17
+ • Use commands to control TunaCode
18
+ • Work with files and projects
19
+ • Get help when you need it
20
+
21
+ This tutorial takes about 2-3 minutes. Ready to start?""",
22
+ "action": "Press Enter to continue...",
23
+ },
24
+ "basic_chat": {
25
+ "title": "💬 Basic AI Chat",
26
+ "content": """The core of TunaCode is natural conversation with AI.
27
+
28
+ You can ask questions like:
29
+ • "How do I implement a binary search in Python?"
30
+ • "Review this function and suggest improvements"
31
+ • "Help me debug this error message"
32
+ • "Explain what this code does"
33
+
34
+ Just type your question naturally - no special syntax needed!""",
35
+ "action": "Try asking: 'What can you help me with?'",
36
+ },
37
+ "file_operations": {
38
+ "title": "📁 Working with Files",
39
+ "content": """TunaCode can read, create, and modify files in your project.
40
+
41
+ Useful commands:
42
+ • Reference files with @filename.py
43
+ • Use /read to explicitly read files
44
+ • Ask to create or modify files
45
+ • Get help with /help
46
+
47
+ TunaCode understands your project structure and can work across multiple files.""",
48
+ "action": "Try: 'Read the current directory structure'",
49
+ },
50
+ "commands": {
51
+ "title": "⚙️ TunaCode Commands",
52
+ "content": """Commands start with / and give you control over TunaCode:
53
+
54
+ Essential commands:
55
+ • /help - Show all available commands
56
+ • /model - Switch AI models
57
+ • /clear - Clear conversation history
58
+ • /exit - Exit TunaCode
59
+
60
+ System commands:
61
+ • !command - Run shell commands
62
+ • /streaming - Toggle streaming responses""",
63
+ "action": "Try typing: /help",
64
+ },
65
+ "best_practices": {
66
+ "title": "✨ Best Practices",
67
+ "content": """To get the most out of TunaCode:
68
+
69
+ 🎯 Be specific: "Fix the bug in login.py line 42" vs "fix my code"
70
+ 📁 Use file references: "@app.py" to include files in context
71
+ 🔄 Break down large tasks: Ask for step-by-step guidance
72
+ 💬 Ask follow-up questions: TunaCode remembers your conversation
73
+ 🚀 Experiment: Try different prompts to see what works best
74
+
75
+ Remember: TunaCode is here to help you code faster and better!""",
76
+ "action": "Press Enter to complete the tutorial...",
77
+ },
78
+ "completion": {
79
+ "title": "🎉 Tutorial Complete!",
80
+ "content": """Congratulations! You're ready to use TunaCode.
81
+
82
+ Quick recap:
83
+ ✅ Chat naturally with AI about code
84
+ ✅ Use @ to reference files
85
+ ✅ Try /help for commands
86
+ ✅ Ask specific questions for better results
87
+
88
+ 🚀 Ready to start coding? Just ask TunaCode anything!
89
+
90
+ Need help later? Use /quickstart to review this tutorial anytime.""",
91
+ "action": "Press Enter to start using TunaCode...",
92
+ },
93
+ }
94
+
95
+
96
+ def get_tutorial_steps() -> List[str]:
97
+ """Get the ordered list of tutorial step IDs."""
98
+ return ["welcome", "basic_chat", "file_operations", "commands", "best_practices", "completion"]
@@ -0,0 +1,182 @@
1
+ """
2
+ Module: tunacode.tutorial.manager
3
+
4
+ Tutorial manager for orchestrating the TunaCode onboarding experience.
5
+ """
6
+
7
+ import logging
8
+
9
+ from ..types import StateManager
10
+ from ..ui import console as ui
11
+ from .steps import (
12
+ create_tutorial_steps,
13
+ is_first_time_user,
14
+ is_tutorial_completed,
15
+ is_tutorial_declined,
16
+ load_tutorial_progress,
17
+ mark_tutorial_completed,
18
+ mark_tutorial_declined,
19
+ save_tutorial_progress,
20
+ )
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+
25
+ class TutorialManager:
26
+ """Manages the tutorial experience for TunaCode users."""
27
+
28
+ def __init__(self, state_manager: StateManager):
29
+ self.state_manager = state_manager
30
+ self.steps = [
31
+ "welcome",
32
+ "basic_chat",
33
+ "file_operations",
34
+ "commands",
35
+ "best_practices",
36
+ "completion",
37
+ ]
38
+
39
+ async def should_offer_tutorial(self) -> bool:
40
+ """Determine if we should offer the tutorial to the user."""
41
+ # Don't offer if already completed or declined
42
+ if is_tutorial_completed(self.state_manager):
43
+ return False
44
+
45
+ if is_tutorial_declined(self.state_manager):
46
+ return False
47
+
48
+ # Check if tutorial is enabled in settings
49
+ settings = self.state_manager.session.user_config.get("settings", {})
50
+ tutorial_enabled = settings.get("enable_tutorial", True)
51
+
52
+ if not tutorial_enabled:
53
+ return False
54
+
55
+ # Only offer to first-time users (installed within last 7 days)
56
+ if not is_first_time_user(self.state_manager):
57
+ return False
58
+
59
+ # Check if this is a fresh session (no significant interaction yet)
60
+ message_count = len(self.state_manager.session.messages)
61
+
62
+ # Offer tutorial if user has minimal interaction history
63
+ return message_count < 3
64
+
65
+ async def offer_tutorial(self) -> bool:
66
+ """Offer the tutorial to the user and return whether they accepted."""
67
+ await ui.panel(
68
+ "🎯 Welcome to TunaCode!",
69
+ "Would you like a quick 2-minute tutorial to get started?\n"
70
+ "This will help you learn the basics and start coding faster.",
71
+ border_style="cyan",
72
+ )
73
+
74
+ choice = await ui.input(
75
+ "tutorial_offer",
76
+ pretext=" → Start tutorial? [Y/n]: ",
77
+ state_manager=self.state_manager,
78
+ )
79
+
80
+ choice = choice.strip().lower()
81
+
82
+ if choice in ["n", "no", "false"]:
83
+ await ui.muted("Tutorial skipped. Use [green]/quickstart[/green] anytime to start it!")
84
+ # Mark as declined so we don't ask again
85
+ mark_tutorial_declined(self.state_manager)
86
+
87
+ # Save the configuration to persist the declined status
88
+ try:
89
+ from ..utils import user_configuration
90
+
91
+ user_configuration.save_config(self.state_manager)
92
+ except Exception as e:
93
+ logger.warning(f"Failed to save tutorial declined status: {e}")
94
+
95
+ return False
96
+
97
+ return True
98
+
99
+ async def run_tutorial(self, resume: bool = False) -> bool:
100
+ """
101
+ Run the complete tutorial experience.
102
+
103
+ Args:
104
+ resume: If True, resume from saved progress
105
+
106
+ Returns:
107
+ True if tutorial completed successfully, False if cancelled
108
+ """
109
+ try:
110
+ from .content import TUTORIAL_CONTENT
111
+
112
+ # Load progress or start from beginning
113
+ current_step = load_tutorial_progress(self.state_manager) if resume else 0
114
+ steps = create_tutorial_steps()
115
+
116
+ await ui.line()
117
+ await ui.info(f"🎯 Starting TunaCode Tutorial ({len(steps)} steps)")
118
+ await ui.line()
119
+
120
+ while current_step < len(steps):
121
+ step_id = steps[current_step]
122
+ step_content = TUTORIAL_CONTENT.get(step_id, {})
123
+
124
+ if not step_content:
125
+ logger.warning(f"Missing content for tutorial step: {step_id}")
126
+ current_step += 1
127
+ continue
128
+
129
+ # Display step content
130
+ await ui.panel(
131
+ f"Step {current_step + 1}/{len(steps)}: {step_content.get('title', step_id)}",
132
+ step_content.get("content", ""),
133
+ border_style="cyan",
134
+ )
135
+
136
+ # Get user input for progression
137
+ action_text = step_content.get("action", "Press Enter to continue...")
138
+ try:
139
+ user_input = await ui.input(
140
+ f"tutorial_step_{current_step}",
141
+ pretext=f" → {action_text} ",
142
+ state_manager=self.state_manager,
143
+ )
144
+
145
+ # Allow users to exit tutorial early
146
+ if user_input.lower() in ["quit", "exit", "skip"]:
147
+ await ui.info(
148
+ "Tutorial cancelled. Use [green]/quickstart[/green] to restart anytime!"
149
+ )
150
+ return False
151
+
152
+ except Exception as e:
153
+ logger.warning(f"Tutorial interrupted: {e}")
154
+ # Save progress before exiting
155
+ save_tutorial_progress(self.state_manager, current_step)
156
+ await ui.info("Tutorial paused. Use [green]/quickstart[/green] to resume!")
157
+ return False
158
+
159
+ current_step += 1
160
+ save_tutorial_progress(self.state_manager, current_step)
161
+
162
+ # Tutorial completed successfully
163
+ mark_tutorial_completed(self.state_manager)
164
+
165
+ # Save the completion status
166
+ try:
167
+ from ..utils import user_configuration
168
+
169
+ user_configuration.save_config(self.state_manager)
170
+ except Exception as e:
171
+ logger.warning(f"Failed to save tutorial completion status: {e}")
172
+
173
+ await ui.line()
174
+ await ui.success("🎉 Tutorial completed! You're ready to use TunaCode.")
175
+ await ui.line()
176
+
177
+ return True
178
+
179
+ except Exception as e:
180
+ logger.error(f"Tutorial failed: {e}")
181
+ await ui.error(f"Tutorial encountered an error: {e}")
182
+ return False
@@ -0,0 +1,124 @@
1
+ """
2
+ Module: tunacode.tutorial.steps
3
+
4
+ Tutorial step management and progress tracking functionality.
5
+ """
6
+
7
+ import logging
8
+ from dataclasses import dataclass
9
+ from typing import List, Optional
10
+
11
+ from ..types import StateManager
12
+ from .content import get_tutorial_steps
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ @dataclass
18
+ class TutorialStepResult:
19
+ """Result of executing a tutorial step."""
20
+
21
+ completed: bool
22
+ should_continue: bool
23
+ user_input: Optional[str] = None
24
+
25
+
26
+ def create_tutorial_steps() -> List[str]:
27
+ """Create and return the list of tutorial steps."""
28
+ return get_tutorial_steps()
29
+
30
+
31
+ def get_tutorial_completion_key() -> str:
32
+ """Get the config key for storing tutorial completion status."""
33
+ return "tutorial_completed"
34
+
35
+
36
+ def get_tutorial_progress_key() -> str:
37
+ """Get the config key for storing tutorial progress."""
38
+ return "tutorial_progress"
39
+
40
+
41
+ def mark_tutorial_completed(state_manager: StateManager) -> None:
42
+ """Mark the tutorial as completed in user config."""
43
+ if "settings" not in state_manager.session.user_config:
44
+ state_manager.session.user_config["settings"] = {}
45
+
46
+ state_manager.session.user_config["settings"][get_tutorial_completion_key()] = True
47
+
48
+ # Clear any existing progress since it's now completed
49
+ if get_tutorial_progress_key() in state_manager.session.user_config["settings"]:
50
+ del state_manager.session.user_config["settings"][get_tutorial_progress_key()]
51
+
52
+
53
+ def save_tutorial_progress(state_manager: StateManager, current_step: int) -> None:
54
+ """Save tutorial progress to user config."""
55
+ if "settings" not in state_manager.session.user_config:
56
+ state_manager.session.user_config["settings"] = {}
57
+
58
+ state_manager.session.user_config["settings"][get_tutorial_progress_key()] = current_step
59
+
60
+
61
+ def load_tutorial_progress(state_manager: StateManager) -> int:
62
+ """Load tutorial progress from user config."""
63
+ settings = state_manager.session.user_config.get("settings", {})
64
+ return settings.get(get_tutorial_progress_key(), 0)
65
+
66
+
67
+ def clear_tutorial_progress(state_manager: StateManager) -> None:
68
+ """Clear tutorial progress from user config."""
69
+ if "settings" not in state_manager.session.user_config:
70
+ return
71
+
72
+ if get_tutorial_progress_key() in state_manager.session.user_config["settings"]:
73
+ del state_manager.session.user_config["settings"][get_tutorial_progress_key()]
74
+
75
+
76
+ def is_tutorial_completed(state_manager: StateManager) -> bool:
77
+ """Check if the tutorial has been completed."""
78
+ settings = state_manager.session.user_config.get("settings", {})
79
+ return settings.get(get_tutorial_completion_key(), False)
80
+
81
+
82
+ def get_tutorial_declined_key() -> str:
83
+ """Get the config key for storing tutorial declined status."""
84
+ return "tutorial_declined"
85
+
86
+
87
+ def mark_tutorial_declined(state_manager: StateManager) -> None:
88
+ """Mark the tutorial as declined in user config."""
89
+ if "settings" not in state_manager.session.user_config:
90
+ state_manager.session.user_config["settings"] = {}
91
+
92
+ state_manager.session.user_config["settings"][get_tutorial_declined_key()] = True
93
+
94
+ # Clear any existing progress since it was declined
95
+ if get_tutorial_progress_key() in state_manager.session.user_config["settings"]:
96
+ del state_manager.session.user_config["settings"][get_tutorial_progress_key()]
97
+
98
+
99
+ def is_tutorial_declined(state_manager: StateManager) -> bool:
100
+ """Check if the tutorial has been declined."""
101
+ settings = state_manager.session.user_config.get("settings", {})
102
+ return settings.get(get_tutorial_declined_key(), False)
103
+
104
+
105
+ def is_first_time_user(state_manager: StateManager) -> bool:
106
+ """Check if this is a first-time user based on installation date."""
107
+ from datetime import datetime, timedelta
108
+
109
+ settings = state_manager.session.user_config.get("settings", {})
110
+ installation_date_str = settings.get("first_installation_date")
111
+
112
+ if not installation_date_str:
113
+ # No installation date means legacy user, treat as experienced
114
+ return False
115
+
116
+ try:
117
+ installation_date = datetime.fromisoformat(installation_date_str)
118
+ now = datetime.now()
119
+
120
+ # Consider first-time if installed within last 7 days
121
+ return (now - installation_date) <= timedelta(days=7)
122
+ except (ValueError, TypeError):
123
+ # Invalid date format, treat as experienced user
124
+ return False
tunacode/ui/output.py CHANGED
@@ -23,7 +23,7 @@ from .decorators import create_sync_wrapper
23
23
  from .logging_compat import ui_logger
24
24
 
25
25
  # Create console with explicit settings to ensure ANSI codes work properly
26
- console = Console(force_terminal=True, legacy_windows=False)
26
+ console = Console()
27
27
  colors = DotDict(UI_COLORS)
28
28
 
29
29
  BANNER = """[bold cyan]
@@ -45,6 +45,10 @@ def load_config() -> Optional[UserConfig]:
45
45
  # else, update fast path
46
46
  _config_fingerprint = new_fp
47
47
  _config_cache = loaded
48
+
49
+ # Initialize onboarding defaults for new configurations
50
+ _ensure_onboarding_defaults(loaded)
51
+
48
52
  return loaded
49
53
  except FileNotFoundError:
50
54
  return None
@@ -91,3 +95,44 @@ def set_default_model(model_name: ModelName, state_manager: "StateManager") -> b
91
95
  except ConfigurationError:
92
96
  # Re-raise ConfigurationError to be handled by caller
93
97
  raise
98
+
99
+
100
+ def _ensure_onboarding_defaults(config: UserConfig) -> None:
101
+ """Ensure onboarding-related default settings are present in config."""
102
+ from datetime import datetime
103
+
104
+ if "settings" not in config:
105
+ config["settings"] = {}
106
+
107
+ settings = config["settings"]
108
+
109
+ # Set tutorial enabled by default for new users
110
+ if "enable_tutorial" not in settings:
111
+ settings["enable_tutorial"] = True
112
+
113
+ # Set first installation date if not present (for new installs)
114
+ if "first_installation_date" not in settings:
115
+ settings["first_installation_date"] = datetime.now().isoformat()
116
+
117
+
118
+ def initialize_first_time_user(state_manager: "StateManager") -> None:
119
+ """Initialize first-time user settings and save configuration."""
120
+ from datetime import datetime
121
+
122
+ # Ensure settings section exists
123
+ if "settings" not in state_manager.session.user_config:
124
+ state_manager.session.user_config["settings"] = {}
125
+
126
+ settings = state_manager.session.user_config["settings"]
127
+
128
+ # Only set installation date if it doesn't exist (true first-time)
129
+ if "first_installation_date" not in settings:
130
+ settings["first_installation_date"] = datetime.now().isoformat()
131
+ settings["enable_tutorial"] = True
132
+
133
+ # Save the updated configuration
134
+ try:
135
+ save_config(state_manager)
136
+ except ConfigurationError:
137
+ # Non-critical error, continue without failing
138
+ pass