tunacode-cli 0.0.67__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.
- tunacode/cli/commands/__init__.py +2 -0
- tunacode/cli/commands/implementations/__init__.py +2 -0
- tunacode/cli/commands/implementations/command_reload.py +48 -0
- tunacode/cli/commands/implementations/quickstart.py +43 -0
- tunacode/cli/commands/registry.py +131 -1
- tunacode/cli/commands/slash/__init__.py +32 -0
- tunacode/cli/commands/slash/command.py +157 -0
- tunacode/cli/commands/slash/loader.py +134 -0
- tunacode/cli/commands/slash/processor.py +294 -0
- tunacode/cli/commands/slash/types.py +93 -0
- tunacode/cli/commands/slash/validator.py +399 -0
- tunacode/cli/main.py +4 -1
- tunacode/cli/repl.py +25 -0
- tunacode/configuration/defaults.py +1 -0
- tunacode/constants.py +1 -1
- tunacode/core/agents/agent_components/agent_helpers.py +14 -13
- tunacode/core/agents/main.py +1 -1
- tunacode/core/agents/utils.py +4 -3
- tunacode/core/setup/config_setup.py +231 -6
- tunacode/core/setup/coordinator.py +13 -5
- tunacode/core/setup/git_safety_setup.py +5 -1
- tunacode/exceptions.py +119 -5
- tunacode/setup.py +5 -2
- tunacode/tools/glob.py +9 -46
- tunacode/tools/grep.py +9 -51
- tunacode/tools/xml_helper.py +83 -0
- tunacode/tutorial/__init__.py +9 -0
- tunacode/tutorial/content.py +98 -0
- tunacode/tutorial/manager.py +182 -0
- tunacode/tutorial/steps.py +124 -0
- tunacode/ui/output.py +1 -1
- tunacode/utils/user_configuration.py +45 -0
- tunacode_cli-0.0.68.dist-info/METADATA +192 -0
- {tunacode_cli-0.0.67.dist-info → tunacode_cli-0.0.68.dist-info}/RECORD +37 -24
- tunacode_cli-0.0.67.dist-info/METADATA +0 -327
- {tunacode_cli-0.0.67.dist-info → tunacode_cli-0.0.68.dist-info}/WHEEL +0 -0
- {tunacode_cli-0.0.67.dist-info → tunacode_cli-0.0.68.dist-info}/entry_points.txt +0 -0
- {tunacode_cli-0.0.67.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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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,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(
|
|
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
|