tunacode-cli 0.0.5__py3-none-any.whl → 0.0.7__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 (36) hide show
  1. tunacode/cli/commands.py +91 -33
  2. tunacode/cli/main.py +6 -0
  3. tunacode/cli/model_selector.py +178 -0
  4. tunacode/cli/repl.py +11 -10
  5. tunacode/configuration/models.py +11 -1
  6. tunacode/constants.py +10 -10
  7. tunacode/context.py +1 -3
  8. tunacode/core/agents/main.py +52 -94
  9. tunacode/core/agents/tinyagent_main.py +173 -0
  10. tunacode/core/setup/git_safety_setup.py +39 -51
  11. tunacode/core/setup/optimized_coordinator.py +73 -0
  12. tunacode/exceptions.py +0 -2
  13. tunacode/services/enhanced_undo_service.py +322 -0
  14. tunacode/services/project_undo_service.py +311 -0
  15. tunacode/services/undo_service.py +13 -16
  16. tunacode/tools/base.py +11 -20
  17. tunacode/tools/tinyagent_tools.py +103 -0
  18. tunacode/tools/update_file.py +24 -14
  19. tunacode/tools/write_file.py +9 -7
  20. tunacode/ui/completers.py +98 -33
  21. tunacode/ui/input.py +8 -7
  22. tunacode/ui/keybindings.py +1 -3
  23. tunacode/ui/lexers.py +16 -17
  24. tunacode/ui/output.py +9 -3
  25. tunacode/ui/panels.py +4 -4
  26. tunacode/ui/prompt_manager.py +6 -4
  27. tunacode/utils/lazy_imports.py +59 -0
  28. tunacode/utils/regex_cache.py +33 -0
  29. tunacode/utils/system.py +40 -0
  30. tunacode_cli-0.0.7.dist-info/METADATA +262 -0
  31. {tunacode_cli-0.0.5.dist-info → tunacode_cli-0.0.7.dist-info}/RECORD +35 -27
  32. tunacode_cli-0.0.5.dist-info/METADATA +0 -247
  33. {tunacode_cli-0.0.5.dist-info → tunacode_cli-0.0.7.dist-info}/WHEEL +0 -0
  34. {tunacode_cli-0.0.5.dist-info → tunacode_cli-0.0.7.dist-info}/entry_points.txt +0 -0
  35. {tunacode_cli-0.0.5.dist-info → tunacode_cli-0.0.7.dist-info}/licenses/LICENSE +0 -0
  36. {tunacode_cli-0.0.5.dist-info → tunacode_cli-0.0.7.dist-info}/top_level.txt +0 -0
@@ -2,49 +2,28 @@
2
2
 
3
3
  Main agent functionality and coordination for the Sidekick CLI.
4
4
  Provides agent creation, message processing, and tool call management.
5
+ Now using tinyAgent instead of pydantic-ai.
5
6
  """
6
7
 
7
- from datetime import datetime, timezone
8
8
  from typing import Optional
9
9
 
10
- from pydantic_ai import Agent, Tool
11
- from pydantic_ai.messages import ModelRequest, ToolReturnPart
12
-
13
10
  from tunacode.core.state import StateManager
14
- from tunacode.services.mcp import get_mcp_servers
15
- from tunacode.tools.read_file import read_file
16
- from tunacode.tools.run_command import run_command
17
- from tunacode.tools.update_file import update_file
18
- from tunacode.tools.write_file import write_file
19
- from tunacode.types import (AgentRun, ErrorMessage, ModelName, PydanticAgent, ToolCallback,
20
- ToolCallId, ToolName)
21
-
22
-
23
- async def _process_node(node, tool_callback: Optional[ToolCallback], state_manager: StateManager):
24
- if hasattr(node, "request"):
25
- state_manager.session.messages.append(node.request)
26
-
27
- if hasattr(node, "model_response"):
28
- state_manager.session.messages.append(node.model_response)
29
- for part in node.model_response.parts:
30
- if part.part_kind == "tool-call" and tool_callback:
31
- await tool_callback(part, node)
32
-
33
-
34
- def get_or_create_agent(model: ModelName, state_manager: StateManager) -> PydanticAgent:
35
- if model not in state_manager.session.agents:
36
- max_retries = state_manager.session.user_config["settings"]["max_retries"]
37
- state_manager.session.agents[model] = Agent(
38
- model=model,
39
- tools=[
40
- Tool(read_file, max_retries=max_retries),
41
- Tool(run_command, max_retries=max_retries),
42
- Tool(update_file, max_retries=max_retries),
43
- Tool(write_file, max_retries=max_retries),
44
- ],
45
- mcp_servers=get_mcp_servers(state_manager),
46
- )
47
- return state_manager.session.agents[model]
11
+ from tunacode.types import AgentRun, ErrorMessage, ModelName, ToolCallback
12
+
13
+ # Import tinyAgent implementation
14
+ from .tinyagent_main import get_or_create_react_agent
15
+ from .tinyagent_main import patch_tool_messages as tinyagent_patch_tool_messages
16
+ from .tinyagent_main import process_request_with_tinyagent
17
+
18
+ # Wrapper functions for backward compatibility with pydantic-ai interface
19
+
20
+
21
+ def get_or_create_agent(model: ModelName, state_manager: StateManager):
22
+ """
23
+ Wrapper for backward compatibility.
24
+ Returns the ReactAgent instance from tinyAgent.
25
+ """
26
+ return get_or_create_react_agent(model, state_manager)
48
27
 
49
28
 
50
29
  def patch_tool_messages(
@@ -52,57 +31,10 @@ def patch_tool_messages(
52
31
  state_manager: StateManager = None,
53
32
  ):
54
33
  """
55
- Find any tool calls without responses and add synthetic error responses for them.
56
- Takes an error message to use in the synthesized tool response.
57
-
58
- Ignores tools that have corresponding retry prompts as the model is already
59
- addressing them.
34
+ Wrapper for backward compatibility.
35
+ TinyAgent handles tool errors internally, so this is mostly a no-op.
60
36
  """
61
- if state_manager is None:
62
- raise ValueError("state_manager is required for patch_tool_messages")
63
-
64
- messages = state_manager.session.messages
65
-
66
- if not messages:
67
- return
68
-
69
- # Map tool calls to their tool returns
70
- tool_calls: dict[ToolCallId, ToolName] = {} # tool_call_id -> tool_name
71
- tool_returns: set[ToolCallId] = set() # set of tool_call_ids with returns
72
- retry_prompts: set[ToolCallId] = set() # set of tool_call_ids with retry prompts
73
-
74
- for message in messages:
75
- if hasattr(message, "parts"):
76
- for part in message.parts:
77
- if (
78
- hasattr(part, "part_kind")
79
- and hasattr(part, "tool_call_id")
80
- and part.tool_call_id
81
- ):
82
- if part.part_kind == "tool-call":
83
- tool_calls[part.tool_call_id] = part.tool_name
84
- elif part.part_kind == "tool-return":
85
- tool_returns.add(part.tool_call_id)
86
- elif part.part_kind == "retry-prompt":
87
- retry_prompts.add(part.tool_call_id)
88
-
89
- # Identify orphaned tools (those without responses and not being retried)
90
- for tool_call_id, tool_name in list(tool_calls.items()):
91
- if tool_call_id not in tool_returns and tool_call_id not in retry_prompts:
92
- messages.append(
93
- ModelRequest(
94
- parts=[
95
- ToolReturnPart(
96
- tool_name=tool_name,
97
- content=error_message,
98
- tool_call_id=tool_call_id,
99
- timestamp=datetime.now(timezone.utc),
100
- part_kind="tool-return",
101
- )
102
- ],
103
- kind="request",
104
- )
105
- )
37
+ tinyagent_patch_tool_messages(error_message, state_manager)
106
38
 
107
39
 
108
40
  async def process_request(
@@ -111,9 +43,35 @@ async def process_request(
111
43
  state_manager: StateManager,
112
44
  tool_callback: Optional[ToolCallback] = None,
113
45
  ) -> AgentRun:
114
- agent = get_or_create_agent(model, state_manager)
115
- mh = state_manager.session.messages.copy()
116
- async with agent.iter(message, message_history=mh) as agent_run:
117
- async for node in agent_run:
118
- await _process_node(node, tool_callback, state_manager)
119
- return agent_run
46
+ """
47
+ Process a request using tinyAgent.
48
+ Returns a result that mimics the pydantic-ai AgentRun structure.
49
+ """
50
+ result = await process_request_with_tinyagent(model, message, state_manager, tool_callback)
51
+
52
+ # Create a mock AgentRun object for compatibility
53
+ class MockAgentRun:
54
+ def __init__(self, result_dict):
55
+ self._result = result_dict
56
+
57
+ @property
58
+ def result(self):
59
+ class MockResult:
60
+ def __init__(self, content):
61
+ self._content = content
62
+
63
+ @property
64
+ def output(self):
65
+ return self._content
66
+
67
+ return MockResult(self._result.get("result", ""))
68
+
69
+ @property
70
+ def messages(self):
71
+ return state_manager.session.messages
72
+
73
+ @property
74
+ def model(self):
75
+ return self._result.get("model", model)
76
+
77
+ return MockAgentRun(result)
@@ -0,0 +1,173 @@
1
+ """TinyAgent-based agent implementation."""
2
+
3
+ from contextlib import asynccontextmanager
4
+ from datetime import datetime, timezone
5
+ from typing import Any, Dict, Optional
6
+
7
+ from tinyagent import ReactAgent
8
+
9
+ from tunacode.core.state import StateManager
10
+ from tunacode.tools.tinyagent_tools import read_file, run_command, update_file, write_file
11
+ from tunacode.types import ModelName, ToolCallback
12
+
13
+
14
+ def get_or_create_react_agent(model: ModelName, state_manager: StateManager) -> ReactAgent:
15
+ """
16
+ Get or create a ReactAgent for the specified model.
17
+
18
+ Args:
19
+ model: The model name (e.g., "openai:gpt-4o", "openrouter:openai/gpt-4.1")
20
+ state_manager: The state manager instance
21
+
22
+ Returns:
23
+ ReactAgent instance configured for the model
24
+ """
25
+ agents = state_manager.session.agents
26
+
27
+ if model not in agents:
28
+ # Parse model string to determine provider and actual model name
29
+ # Format: "provider:model" or "openrouter:provider/model"
30
+ if model.startswith("openrouter:"):
31
+ # OpenRouter model - extract the actual model name
32
+ actual_model = model.replace("openrouter:", "")
33
+ # Set environment to use OpenRouter base URL
34
+ import os
35
+
36
+ os.environ["OPENAI_BASE_URL"] = "https://openrouter.ai/api/v1"
37
+ # Use OpenRouter API key if available
38
+ if state_manager.session.user_config["env"].get("OPENROUTER_API_KEY"):
39
+ os.environ["OPENAI_API_KEY"] = state_manager.session.user_config["env"][
40
+ "OPENROUTER_API_KEY"
41
+ ]
42
+ else:
43
+ # Direct provider (openai, anthropic, google-gla)
44
+ provider, actual_model = model.split(":", 1)
45
+ # Reset to default base URL for direct providers
46
+ import os
47
+
48
+ if provider == "openai":
49
+ os.environ["OPENAI_BASE_URL"] = "https://api.openai.com/v1"
50
+ # Set appropriate API key
51
+ provider_key_map = {
52
+ "openai": "OPENAI_API_KEY",
53
+ "anthropic": "ANTHROPIC_API_KEY",
54
+ "google-gla": "GEMINI_API_KEY",
55
+ }
56
+ if provider in provider_key_map:
57
+ key_name = provider_key_map[provider]
58
+ if state_manager.session.user_config["env"].get(key_name):
59
+ os.environ[key_name] = state_manager.session.user_config["env"][key_name]
60
+
61
+ # Create new ReactAgent with tools
62
+ # Note: tinyAgent gets model from environment variables, not constructor
63
+ agent = ReactAgent(tools=[read_file, write_file, update_file, run_command])
64
+
65
+ # Add MCP compatibility method
66
+ @asynccontextmanager
67
+ async def run_mcp_servers():
68
+ # TinyAgent doesn't have built-in MCP support yet
69
+ # This is a placeholder for compatibility
70
+ yield
71
+
72
+ agent.run_mcp_servers = run_mcp_servers
73
+
74
+ # Cache the agent
75
+ agents[model] = agent
76
+
77
+ return agents[model]
78
+
79
+
80
+ async def process_request_with_tinyagent(
81
+ model: ModelName,
82
+ message: str,
83
+ state_manager: StateManager,
84
+ tool_callback: Optional[ToolCallback] = None,
85
+ ) -> Dict[str, Any]:
86
+ """
87
+ Process a request using TinyAgent's ReactAgent.
88
+
89
+ Args:
90
+ model: The model to use
91
+ message: The user message
92
+ state_manager: State manager instance
93
+ tool_callback: Optional callback for tool execution (for UI updates)
94
+
95
+ Returns:
96
+ Dict containing the result and any metadata
97
+ """
98
+ agent = get_or_create_react_agent(model, state_manager)
99
+
100
+ # Convert message history to format expected by tinyAgent
101
+ # Note: tinyAgent handles message history differently than pydantic-ai
102
+ # We'll need to adapt based on tinyAgent's actual API
103
+
104
+ try:
105
+ # Run the agent with the message
106
+ # The new API's run() method might be synchronous based on the examples
107
+ import asyncio
108
+ if asyncio.iscoroutinefunction(agent.run):
109
+ result = await agent.run(message)
110
+ else:
111
+ result = agent.run(message)
112
+
113
+ # Update message history in state_manager
114
+ # This will need to be adapted based on how tinyAgent returns messages
115
+ state_manager.session.messages.append(
116
+ {
117
+ "role": "user",
118
+ "content": message,
119
+ "timestamp": datetime.now(timezone.utc).isoformat(),
120
+ }
121
+ )
122
+
123
+ state_manager.session.messages.append(
124
+ {
125
+ "role": "assistant",
126
+ "content": result,
127
+ "timestamp": datetime.now(timezone.utc).isoformat(),
128
+ }
129
+ )
130
+
131
+ return {"result": result, "success": True, "model": model}
132
+
133
+ except Exception as e:
134
+ # Handle errors
135
+ error_result = {
136
+ "result": f"Error: {str(e)}",
137
+ "success": False,
138
+ "model": model,
139
+ "error": str(e),
140
+ }
141
+
142
+ # Still update message history with the error
143
+ state_manager.session.messages.append(
144
+ {
145
+ "role": "user",
146
+ "content": message,
147
+ "timestamp": datetime.now(timezone.utc).isoformat(),
148
+ }
149
+ )
150
+
151
+ state_manager.session.messages.append(
152
+ {
153
+ "role": "assistant",
154
+ "content": f"Error occurred: {str(e)}",
155
+ "timestamp": datetime.now(timezone.utc).isoformat(),
156
+ "error": True,
157
+ }
158
+ )
159
+
160
+ return error_result
161
+
162
+
163
+ def patch_tool_messages(
164
+ error_message: str = "Tool operation failed",
165
+ state_manager: StateManager = None,
166
+ ):
167
+ """
168
+ Compatibility function for patching tool messages.
169
+ With tinyAgent, this may not be needed as it handles tool errors differently.
170
+ """
171
+ # TinyAgent handles tool retries and errors internally
172
+ # This function is kept for compatibility but may be simplified
173
+ pass
@@ -13,15 +13,12 @@ from tunacode.ui.panels import panel
13
13
  async def yes_no_prompt(question: str, default: bool = True) -> bool:
14
14
  """Simple yes/no prompt."""
15
15
  default_text = "[Y/n]" if default else "[y/N]"
16
- response = await prompt_input(
17
- session_key="yes_no",
18
- pretext=f"{question} {default_text}: "
19
- )
20
-
16
+ response = await prompt_input(session_key="yes_no", pretext=f"{question} {default_text}: ")
17
+
21
18
  if not response.strip():
22
19
  return default
23
-
24
- return response.lower().strip() in ['y', 'yes']
20
+
21
+ return response.lower().strip() in ["y", "yes"]
25
22
 
26
23
 
27
24
  class GitSafetySetup(BaseSetup):
@@ -29,7 +26,7 @@ class GitSafetySetup(BaseSetup):
29
26
 
30
27
  def __init__(self, state_manager: StateManager):
31
28
  super().__init__(state_manager)
32
-
29
+
33
30
  @property
34
31
  def name(self) -> str:
35
32
  """Return the name of this setup step."""
@@ -45,18 +42,15 @@ class GitSafetySetup(BaseSetup):
45
42
  try:
46
43
  # Check if git is installed
47
44
  result = subprocess.run(
48
- ["git", "--version"],
49
- capture_output=True,
50
- text=True,
51
- check=False
45
+ ["git", "--version"], capture_output=True, text=True, check=False
52
46
  )
53
-
47
+
54
48
  if result.returncode != 0:
55
49
  await panel(
56
50
  "⚠️ Git Not Found",
57
51
  "Git is not installed or not in PATH. TunaCode will modify files directly.\n"
58
52
  "It's strongly recommended to install Git for safety.",
59
- border_style="yellow"
53
+ border_style="yellow",
60
54
  )
61
55
  return
62
56
 
@@ -66,33 +60,31 @@ class GitSafetySetup(BaseSetup):
66
60
  capture_output=True,
67
61
  text=True,
68
62
  check=False,
69
- cwd=Path.cwd()
63
+ cwd=Path.cwd(),
70
64
  )
71
-
65
+
72
66
  if result.returncode != 0:
73
67
  await panel(
74
68
  "⚠️ Not a Git Repository",
75
69
  "This directory is not a Git repository. TunaCode will modify files directly.\n"
76
70
  "Consider initializing a Git repository for safety: git init",
77
- border_style="yellow"
71
+ border_style="yellow",
78
72
  )
79
73
  return
80
74
 
81
75
  # Get current branch name
82
76
  result = subprocess.run(
83
- ["git", "branch", "--show-current"],
84
- capture_output=True,
85
- text=True,
86
- check=True
77
+ ["git", "branch", "--show-current"], capture_output=True, text=True, check=True
87
78
  )
88
79
  current_branch = result.stdout.strip()
89
-
80
+
90
81
  if not current_branch:
91
82
  # Detached HEAD state
92
83
  await panel(
93
84
  "⚠️ Detached HEAD State",
94
- "You're in a detached HEAD state. TunaCode will continue without creating a branch.",
95
- border_style="yellow"
85
+ "You're in a detached HEAD state. TunaCode will continue "
86
+ "without creating a branch.",
87
+ border_style="yellow",
96
88
  )
97
89
  return
98
90
 
@@ -103,31 +95,28 @@ class GitSafetySetup(BaseSetup):
103
95
 
104
96
  # Propose new branch name
105
97
  new_branch = f"{current_branch}-tunacode"
106
-
98
+
107
99
  # Check if there are uncommitted changes
108
100
  result = subprocess.run(
109
- ["git", "status", "--porcelain"],
110
- capture_output=True,
111
- text=True,
112
- check=True
101
+ ["git", "status", "--porcelain"], capture_output=True, text=True, check=True
113
102
  )
114
-
103
+
115
104
  has_changes = bool(result.stdout.strip())
116
-
105
+
117
106
  # Ask user if they want to create a safety branch
118
107
  message = (
119
- f"For safety, TunaCode can create a new branch '{new_branch}' based on '{current_branch}'.\n"
108
+ f"For safety, TunaCode can create a new branch '{new_branch}' "
109
+ f"based on '{current_branch}'.\n"
120
110
  f"This helps protect your work from unintended changes.\n"
121
111
  )
122
-
112
+
123
113
  if has_changes:
124
- message += "\n⚠️ You have uncommitted changes that will be brought to the new branch."
125
-
126
- create_branch = await yes_no_prompt(
127
- f"{message}\n\nCreate safety branch?",
128
- default=True
129
- )
130
-
114
+ message += (
115
+ "\n⚠️ You have uncommitted changes that will be brought to the new branch."
116
+ )
117
+
118
+ create_branch = await yes_no_prompt(f"{message}\n\nCreate safety branch?", default=True)
119
+
131
120
  if not create_branch:
132
121
  # User declined - show warning
133
122
  await panel(
@@ -135,26 +124,25 @@ class GitSafetySetup(BaseSetup):
135
124
  "You've chosen to work directly on your current branch.\n"
136
125
  "TunaCode will modify files in place. Make sure you have backups!\n"
137
126
  "You can always use /undo to revert changes.",
138
- border_style="red"
127
+ border_style="red",
139
128
  )
140
129
  # Save preference
141
130
  self.state_manager.session.user_config["skip_git_safety"] = True
142
131
  return
143
-
132
+
144
133
  # Create and checkout the new branch
145
134
  try:
146
135
  # Check if branch already exists
147
136
  result = subprocess.run(
148
137
  ["git", "show-ref", "--verify", f"refs/heads/{new_branch}"],
149
138
  capture_output=True,
150
- check=False
139
+ check=False,
151
140
  )
152
-
141
+
153
142
  if result.returncode == 0:
154
143
  # Branch exists, ask to use it
155
144
  use_existing = await yes_no_prompt(
156
- f"Branch '{new_branch}' already exists. Switch to it?",
157
- default=True
145
+ f"Branch '{new_branch}' already exists. Switch to it?", default=True
158
146
  )
159
147
  if use_existing:
160
148
  subprocess.run(["git", "checkout", new_branch], check=True)
@@ -165,24 +153,24 @@ class GitSafetySetup(BaseSetup):
165
153
  # Create new branch
166
154
  subprocess.run(["git", "checkout", "-b", new_branch], check=True)
167
155
  await ui.success(f"Created and switched to new branch: {new_branch}")
168
-
156
+
169
157
  except subprocess.CalledProcessError as e:
170
158
  await panel(
171
159
  "❌ Failed to Create Branch",
172
160
  f"Could not create branch '{new_branch}': {str(e)}\n"
173
161
  "Continuing on current branch.",
174
- border_style="red"
162
+ border_style="red",
175
163
  )
176
-
164
+
177
165
  except Exception as e:
178
166
  # Non-fatal error - just warn the user
179
167
  await panel(
180
168
  "⚠️ Git Safety Setup Failed",
181
169
  f"Could not set up Git safety: {str(e)}\n"
182
170
  "TunaCode will continue without branch protection.",
183
- border_style="yellow"
171
+ border_style="yellow",
184
172
  )
185
173
 
186
174
  async def validate(self) -> bool:
187
175
  """Validate git safety setup - always returns True as this is optional."""
188
- return True
176
+ return True
@@ -0,0 +1,73 @@
1
+ """Optimized setup coordinator with deferred loading."""
2
+
3
+ import asyncio
4
+ from typing import List, Set
5
+
6
+ from tunacode.core.setup.base import BaseSetup
7
+ from tunacode.core.state import StateManager
8
+ from tunacode.ui import console as ui
9
+
10
+
11
+ class OptimizedSetupCoordinator:
12
+ """Optimized coordinator that defers non-critical setup steps."""
13
+
14
+ def __init__(self, state_manager: StateManager):
15
+ self.state_manager = state_manager
16
+ self.critical_steps: List[BaseSetup] = []
17
+ self.deferred_steps: List[BaseSetup] = []
18
+ self._deferred_task = None
19
+
20
+ # Define critical steps that must run at startup
21
+ self.critical_step_names: Set[str] = {
22
+ "Configuration", # Need config to know which model to use
23
+ "Environment Variables", # Need API keys
24
+ }
25
+
26
+ def register_step(self, step: BaseSetup) -> None:
27
+ """Register a setup step, separating critical from deferred."""
28
+ if step.name in self.critical_step_names:
29
+ self.critical_steps.append(step)
30
+ else:
31
+ self.deferred_steps.append(step)
32
+
33
+ async def run_setup(self, force_setup: bool = False) -> None:
34
+ """Run critical setup immediately, defer the rest."""
35
+ # Run critical steps synchronously
36
+ for step in self.critical_steps:
37
+ try:
38
+ if await step.should_run(force_setup):
39
+ await step.execute(force_setup)
40
+ if not await step.validate():
41
+ await ui.error(f"Setup validation failed: {step.name}")
42
+ raise RuntimeError(f"Setup step '{step.name}' failed validation")
43
+ except Exception as e:
44
+ await ui.error(f"Setup failed at step '{step.name}': {str(e)}")
45
+ raise
46
+
47
+ # Schedule deferred steps to run in background
48
+ if self.deferred_steps and not self._deferred_task:
49
+ self._deferred_task = asyncio.create_task(self._run_deferred_steps(force_setup))
50
+
51
+ async def _run_deferred_steps(self, force_setup: bool) -> None:
52
+ """Run deferred steps in the background."""
53
+ # Wait a moment to let the main UI start
54
+ await asyncio.sleep(0.1)
55
+
56
+ for step in self.deferred_steps:
57
+ try:
58
+ if await step.should_run(force_setup):
59
+ await step.execute(force_setup)
60
+ # Don't validate deferred steps - they're non-critical
61
+ except Exception:
62
+ # Log but don't fail on deferred steps
63
+ pass
64
+
65
+ async def ensure_deferred_complete(self) -> None:
66
+ """Ensure deferred steps are complete before certain operations."""
67
+ if self._deferred_task and not self._deferred_task.done():
68
+ await self._deferred_task
69
+
70
+ def clear_steps(self) -> None:
71
+ """Clear all registered setup steps."""
72
+ self.critical_steps.clear()
73
+ self.deferred_steps.clear()
tunacode/exceptions.py CHANGED
@@ -77,8 +77,6 @@ class MCPError(ServiceError):
77
77
  super().__init__(f"MCP server '{server_name}' error: {message}")
78
78
 
79
79
 
80
-
81
-
82
80
  class GitOperationError(ServiceError):
83
81
  """Raised when Git operations fail."""
84
82