tunacode-cli 0.0.76.9__py3-none-any.whl → 0.0.77.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.

@@ -49,25 +49,25 @@ class BranchCommand(SimpleCommand):
49
49
 
50
50
 
51
51
  class InitCommand(SimpleCommand):
52
- """Creates or updates TUNACODE.md with project-specific context."""
52
+ """Creates or updates AGENTS.md with project-specific context."""
53
53
 
54
54
  spec = CommandSpec(
55
55
  name="/init",
56
56
  aliases=[],
57
- description="Analyze codebase and create/update TUNACODE.md file",
57
+ description="Analyze codebase and create/update AGENTS.md file",
58
58
  category=CommandCategory.DEVELOPMENT,
59
59
  )
60
60
 
61
61
  async def execute(self, args, context: CommandContext) -> CommandResult:
62
62
  """Execute the init command."""
63
63
  # Minimal implementation to make test pass
64
- prompt = """Please analyze this codebase and create a TUNACODE.md file containing:
64
+ prompt = """Please analyze this codebase and create a AGENTS.md file containing:
65
65
  1. Build/lint/test commands - especially for running a single test
66
66
  2. Code style guidelines including imports, formatting, types, naming conventions, error handling, etc.
67
67
 
68
68
  The file you create will be given to agentic coding agents (such as yourself) that operate in this repository.
69
69
  Make it about 20 lines long.
70
- If there's already a TUNACODE.md, improve it.
70
+ If there's already a AGENTS.md, improve it.
71
71
  If there are Cursor rules (in .cursor/rules/ or .cursorrules) or Copilot rules (in .github/copilot-instructions.md),
72
72
  make sure to include them."""
73
73
 
@@ -32,13 +32,6 @@ CONFIG_KEY_DESCRIPTIONS: Dict[str, KeyDescription] = {
32
32
  help_text="Format: provider:model-name. Examples: openai:gpt-4, anthropic:claude-3-sonnet, google:gemini-pro",
33
33
  category="AI Models",
34
34
  ),
35
- "skip_git_safety": KeyDescription(
36
- name="skip_git_safety",
37
- description="Skip Git safety checks when making changes",
38
- example=True,
39
- help_text="When true, TunaCode won't create safety branches before making changes. Use with caution!",
40
- category="Safety Settings",
41
- ),
42
35
  # Environment variables (API Keys)
43
36
  "env.OPENAI_API_KEY": KeyDescription(
44
37
  name="OPENAI_API_KEY",
@@ -108,8 +101,8 @@ CONFIG_KEY_DESCRIPTIONS: Dict[str, KeyDescription] = {
108
101
  "settings.guide_file": KeyDescription(
109
102
  name="guide_file",
110
103
  description="Name of your project guide file",
111
- example="TUNACODE.md",
112
- help_text="TunaCode looks for this file to understand your project. Usually TUNACODE.md or README.md.",
104
+ example="AGENTS.md",
105
+ help_text="TunaCode looks for this file to understand your project. Usually AGENTS.md or README.md.",
113
106
  category="Project Settings",
114
107
  ),
115
108
  "settings.fallback_response": KeyDescription(
tunacode/constants.py CHANGED
@@ -9,12 +9,12 @@ from enum import Enum
9
9
 
10
10
  # Application info
11
11
  APP_NAME = "TunaCode"
12
- APP_VERSION = "0.0.76.9"
12
+ APP_VERSION = "0.0.77.1"
13
13
 
14
14
 
15
15
  # File patterns
16
16
  GUIDE_FILE_PATTERN = "{name}.md"
17
- GUIDE_FILE_NAME = "TUNACODE.md"
17
+ GUIDE_FILE_NAME = "AGENTS.md"
18
18
  ENV_FILE = ".env"
19
19
  CONFIG_FILE_NAME = "tunacode.json"
20
20
 
tunacode/context.py CHANGED
@@ -50,16 +50,16 @@ async def get_directory_structure(max_depth: int = 3) -> str:
50
50
 
51
51
 
52
52
  async def get_code_style() -> str:
53
- """Concatenate contents of all TUNACODE.md files up the directory tree."""
53
+ """Concatenate contents of all AGENTS.md files up the directory tree."""
54
54
  parts: List[str] = []
55
55
  current = Path.cwd()
56
56
  while True:
57
- file = current / "TUNACODE.md"
57
+ file = current / "AGENTS.md"
58
58
  if file.exists():
59
59
  try:
60
60
  parts.append(file.read_text(encoding="utf-8"))
61
61
  except Exception as e:
62
- logger.debug(f"Failed to read TUNACODE.md at {file}: {e}")
62
+ logger.debug(f"Failed to read AGENTS.md at {file}: {e}")
63
63
  if current == current.parent:
64
64
  break
65
65
  current = current.parent
@@ -67,5 +67,5 @@ async def get_code_style() -> str:
67
67
 
68
68
 
69
69
  async def get_claude_files() -> List[str]:
70
- """Return a list of additional TUNACODE.md files in the repo."""
71
- return ripgrep("TUNACODE.md", ".")
70
+ """Return a list of additional AGENTS.md files in the repo."""
71
+ return ripgrep("AGENTS.md", ".")
@@ -13,6 +13,7 @@ from .agent_helpers import (
13
13
  get_tool_description,
14
14
  get_tool_summary,
15
15
  get_user_prompt_part_class,
16
+ handle_empty_response,
16
17
  )
17
18
  from .json_tool_parser import extract_and_execute_tool_calls, parse_json_tool_calls
18
19
  from .message_handler import get_model_messages, patch_tool_messages
@@ -47,6 +48,7 @@ __all__ = [
47
48
  "get_tool_description",
48
49
  "get_tool_summary",
49
50
  "get_user_prompt_part_class",
51
+ "handle_empty_response",
50
52
  "stream_model_request_node",
51
53
  "get_batch_description",
52
54
  ]
@@ -30,6 +30,9 @@ _TUNACODE_CACHE: Dict[str, Tuple[str, float]] = {}
30
30
  _AGENT_CACHE: Dict[ModelName, PydanticAgent] = {}
31
31
  _AGENT_CACHE_VERSION: Dict[ModelName, int] = {}
32
32
 
33
+ _PROMPT_FILENAMES: Tuple[str, ...] = ("system.xml", "system.md", "system.txt")
34
+ _DEFAULT_SYSTEM_PROMPT = "You are a helpful AI assistant."
35
+
33
36
 
34
37
  def clear_all_caches():
35
38
  """Clear all module-level caches. Useful for testing."""
@@ -46,55 +49,55 @@ def get_agent_tool():
46
49
  return Agent, Tool
47
50
 
48
51
 
49
- def load_system_prompt(base_path: Path) -> str:
50
- """Load the system prompt from file with caching."""
51
- prompt_path = base_path / "prompts" / "system.md"
52
+ def _read_prompt_from_path(prompt_path: Path) -> str:
53
+ """Return prompt content from disk, leveraging the cache when possible."""
52
54
  cache_key = str(prompt_path)
53
55
 
54
- # Check cache with file modification time
55
56
  try:
56
- if cache_key in _PROMPT_CACHE:
57
- cached_content, cached_mtime = _PROMPT_CACHE[cache_key]
58
- current_mtime = prompt_path.stat().st_mtime
59
- if current_mtime == cached_mtime:
60
- return cached_content
57
+ current_mtime = prompt_path.stat().st_mtime
58
+ except FileNotFoundError as error:
59
+ raise FileNotFoundError from error
61
60
 
62
- # Load from file and cache
63
- with open(prompt_path, "r", encoding="utf-8") as f:
64
- content = f.read().strip()
65
- _PROMPT_CACHE[cache_key] = (content, prompt_path.stat().st_mtime)
66
- return content
61
+ if cache_key in _PROMPT_CACHE:
62
+ cached_content, cached_mtime = _PROMPT_CACHE[cache_key]
63
+ if current_mtime == cached_mtime:
64
+ return cached_content
65
+
66
+ try:
67
+ content = prompt_path.read_text(encoding="utf-8").strip()
68
+ except FileNotFoundError as error:
69
+ raise FileNotFoundError from error
67
70
 
68
- except FileNotFoundError:
69
- # Fallback to system.txt if system.md not found
70
- prompt_path = base_path / "prompts" / "system.txt"
71
- cache_key = str(prompt_path)
71
+ _PROMPT_CACHE[cache_key] = (content, current_mtime)
72
+ return content
72
73
 
73
- try:
74
- if cache_key in _PROMPT_CACHE:
75
- cached_content, cached_mtime = _PROMPT_CACHE[cache_key]
76
- current_mtime = prompt_path.stat().st_mtime
77
- if current_mtime == cached_mtime:
78
- return cached_content
79
74
 
80
- with open(prompt_path, "r", encoding="utf-8") as f:
81
- content = f.read().strip()
82
- _PROMPT_CACHE[cache_key] = (content, prompt_path.stat().st_mtime)
83
- return content
75
+ def load_system_prompt(base_path: Path) -> str:
76
+ """Load the system prompt from file with caching."""
77
+ prompts_dir = base_path / "prompts"
84
78
 
79
+ for prompt_name in _PROMPT_FILENAMES:
80
+ prompt_path = prompts_dir / prompt_name
81
+ if not prompt_path.exists():
82
+ continue
83
+
84
+ try:
85
+ return _read_prompt_from_path(prompt_path)
85
86
  except FileNotFoundError:
86
- # Use a default system prompt if neither file exists
87
- return "You are a helpful AI assistant."
87
+ # File disappeared between exists() check and read. Try next candidate.
88
+ continue
89
+
90
+ return _DEFAULT_SYSTEM_PROMPT
88
91
 
89
92
 
90
93
  def load_tunacode_context() -> str:
91
- """Load TUNACODE.md context if it exists with caching."""
94
+ """Load AGENTS.md context if it exists with caching."""
92
95
  try:
93
- tunacode_path = Path.cwd() / "TUNACODE.md"
96
+ tunacode_path = Path.cwd() / "AGENTS.md"
94
97
  cache_key = str(tunacode_path)
95
98
 
96
99
  if not tunacode_path.exists():
97
- logger.info("📄 TUNACODE.md not found: Using default context")
100
+ logger.info("📄 AGENTS.md not found: Using default context")
98
101
  return ""
99
102
 
100
103
  # Check cache with file modification time
@@ -107,17 +110,17 @@ def load_tunacode_context() -> str:
107
110
  # Load from file and cache
108
111
  tunacode_content = tunacode_path.read_text(encoding="utf-8")
109
112
  if tunacode_content.strip():
110
- logger.info("📄 TUNACODE.md located: Loading context...")
111
- result = "\n\n# Project Context from TUNACODE.md\n" + tunacode_content
113
+ logger.info("📄 AGENTS.md located: Loading context...")
114
+ result = "\n\n# Project Context from AGENTS.md\n" + tunacode_content
112
115
  _TUNACODE_CACHE[cache_key] = (result, tunacode_path.stat().st_mtime)
113
116
  return result
114
117
  else:
115
- logger.info("📄 TUNACODE.md not found: Using default context")
118
+ logger.info("📄 AGENTS.md not found: Using default context")
116
119
  _TUNACODE_CACHE[cache_key] = ("", tunacode_path.stat().st_mtime)
117
120
  return ""
118
121
 
119
122
  except Exception as e:
120
- logger.debug(f"Error loading TUNACODE.md: {e}")
123
+ logger.debug(f"Error loading AGENTS.md: {e}")
121
124
  return ""
122
125
 
123
126
 
@@ -164,7 +167,7 @@ def get_or_create_agent(model: ModelName, state_manager: StateManager) -> Pydant
164
167
  base_path = Path(__file__).parent.parent.parent.parent
165
168
  system_prompt = load_system_prompt(base_path)
166
169
 
167
- # Load TUNACODE.md context
170
+ # Load AGENTS.md context
168
171
  system_prompt += load_tunacode_context()
169
172
 
170
173
  # Add plan mode context if in plan mode
@@ -201,6 +201,33 @@ def create_fallback_response(
201
201
  return fallback
202
202
 
203
203
 
204
+ async def handle_empty_response(
205
+ message: str,
206
+ reason: str,
207
+ iter_index: int,
208
+ state: Any,
209
+ ) -> None:
210
+ """Handle empty responses by creating a synthetic user message with retry guidance."""
211
+ from tunacode.ui import console as ui
212
+
213
+ force_action_content = create_empty_response_message(
214
+ message,
215
+ reason,
216
+ getattr(state.sm.session, "tool_calls", []),
217
+ iter_index,
218
+ state.sm,
219
+ )
220
+ create_user_message(force_action_content, state.sm)
221
+
222
+ if state.show_thoughts:
223
+ await ui.warning("\nEMPTY RESPONSE FAILURE - AGGRESSIVE RETRY TRIGGERED")
224
+ await ui.muted(f" Reason: {reason}")
225
+ await ui.muted(
226
+ f" Recent tools: {get_recent_tools_context(getattr(state.sm.session, 'tool_calls', []))}"
227
+ )
228
+ await ui.muted(" Injecting retry guidance prompt")
229
+
230
+
204
231
  def format_fallback_output(fallback: FallbackResponse) -> str:
205
232
  """Format a fallback response into a comprehensive output string."""
206
233
  output_parts = [fallback.summary, ""]
@@ -31,20 +31,10 @@ from tunacode.types import (
31
31
  ToolCallback,
32
32
  UsageTrackerProtocol,
33
33
  )
34
- from tunacode.ui.tool_descriptions import get_batch_description
35
-
36
- # Optional UI console (avoid nested imports in hot paths)
37
- try:
38
- from tunacode.ui import console as ui # rich-style helpers with async methods
39
- except Exception: # pragma: no cover - UI is optional
40
34
 
41
- class _NoopUI: # minimal no-op shim
42
- async def muted(self, *_: Any, **__: Any) -> None: ...
43
- async def warning(self, *_: Any, **__: Any) -> None: ...
44
- async def success(self, *_: Any, **__: Any) -> None: ...
45
- async def update_spinner_message(self, *_: Any, **__: Any) -> None: ...
46
-
47
- ui = _NoopUI() # type: ignore
35
+ # CLAUDE_ANCHOR[key=d595ceb5] Direct UI console import aligns with removal of defensive shim
36
+ from tunacode.ui import console as ui
37
+ from tunacode.ui.tool_descriptions import get_batch_description
48
38
 
49
39
  # Streaming parts (keep guarded import but avoid per-iteration imports)
50
40
  try:
@@ -56,16 +46,10 @@ except Exception: # pragma: no cover
56
46
  TextPartDelta = None # type: ignore
57
47
  STREAMING_AVAILABLE = False
58
48
 
59
- # Agent components (flattned to a single module import to reduce coupling)
60
49
  from . import agent_components as ac
61
50
 
62
- # Configure logging
63
51
  logger = get_logger(__name__)
64
52
 
65
-
66
- # -----------------------
67
- # Module exports
68
- # -----------------------
69
53
  __all__ = [
70
54
  "process_request",
71
55
  "get_mcp_servers",
@@ -73,9 +57,6 @@ __all__ = [
73
57
  "check_query_satisfaction",
74
58
  ]
75
59
 
76
- # -----------------------
77
- # Constants & Defaults
78
- # -----------------------
79
60
  DEFAULT_MAX_ITERATIONS = 15
80
61
  UNPRODUCTIVE_LIMIT = 3 # iterations without tool use before forcing action
81
62
  FALLBACK_VERBOSITY_DEFAULT = "normal"
@@ -84,9 +65,6 @@ FORCED_REACT_INTERVAL = 2
84
65
  FORCED_REACT_LIMIT = 5
85
66
 
86
67
 
87
- # -----------------------
88
- # Data structures
89
- # -----------------------
90
68
  @dataclass(slots=True)
91
69
  class RequestContext:
92
70
  request_id: str
@@ -96,12 +74,11 @@ class RequestContext:
96
74
 
97
75
 
98
76
  class StateFacade:
99
- """Thin wrapper to centralize session mutations and reads."""
77
+ """wrapper to centralize session mutations and reads."""
100
78
 
101
79
  def __init__(self, state_manager: StateManager) -> None:
102
80
  self.sm = state_manager
103
81
 
104
- # ---- safe getters ----
105
82
  def get_setting(self, dotted: str, default: Any) -> Any:
106
83
  cfg: Dict[str, Any] = getattr(self.sm.session, "user_config", {}) or {}
107
84
  node = cfg
@@ -119,7 +96,6 @@ class StateFacade:
119
96
  def messages(self) -> list:
120
97
  return list(getattr(self.sm.session, "messages", []))
121
98
 
122
- # ---- safe setters ----
123
99
  def set_request_id(self, req_id: str) -> None:
124
100
  try:
125
101
  self.sm.session.request_id = req_id
@@ -160,9 +136,6 @@ class StateFacade:
160
136
  setattr(self.sm.session, "consecutive_empty_responses", 0)
161
137
 
162
138
 
163
- # -----------------------
164
- # Helper functions
165
- # -----------------------
166
139
  def _init_context(state: StateFacade, fallback_enabled: bool) -> RequestContext:
167
140
  req_id = str(uuid.uuid4())[:8]
168
141
  state.set_request_id(req_id)
@@ -292,30 +265,6 @@ async def _maybe_force_react_snapshot(
292
265
  logger.debug("Forced react snapshot failed", exc_info=True)
293
266
 
294
267
 
295
- async def _handle_empty_response(
296
- message: str,
297
- reason: str,
298
- iter_index: int,
299
- state: StateFacade,
300
- ) -> None:
301
- force_action_content = ac.create_empty_response_message(
302
- message,
303
- reason,
304
- getattr(state.sm.session, "tool_calls", []),
305
- iter_index,
306
- state.sm,
307
- )
308
- ac.create_user_message(force_action_content, state.sm)
309
-
310
- if state.show_thoughts:
311
- await ui.warning("\nEMPTY RESPONSE FAILURE - AGGRESSIVE RETRY TRIGGERED")
312
- await ui.muted(f" Reason: {reason}")
313
- await ui.muted(
314
- f" Recent tools: {ac.get_recent_tools_context(getattr(state.sm.session, 'tool_calls', []))}"
315
- )
316
- await ui.muted(" Injecting retry guidance prompt")
317
-
318
-
319
268
  async def _force_action_if_unproductive(
320
269
  message: str,
321
270
  unproductive_count: int,
@@ -445,9 +394,6 @@ def _build_fallback_output(
445
394
  return ac.format_fallback_output(fallback)
446
395
 
447
396
 
448
- # -----------------------
449
- # Public API
450
- # -----------------------
451
397
  def get_agent_tool() -> tuple[type[Agent], type["Tool"]]:
452
398
  """Return Agent and Tool classes without importing at module load time."""
453
399
  from pydantic_ai import Agent as AgentCls
@@ -526,7 +472,7 @@ async def process_request(
526
472
  # Handle empty response (aggressive retry prompt)
527
473
  if empty_response:
528
474
  if state.increment_empty_response() >= 1:
529
- await _handle_empty_response(message, empty_reason, i, state)
475
+ await ac.handle_empty_response(message, empty_reason, i, state)
530
476
  state.clear_empty_response()
531
477
  else:
532
478
  state.clear_empty_response()
@@ -601,11 +547,7 @@ async def process_request(
601
547
  "Progress summary:\n"
602
548
  f"- Tools used: {tools_str}\n"
603
549
  f"- Iterations completed: {i}\n\n"
604
- "The task appears incomplete. Would you like me to:\n"
605
- "1. Continue working (extend limit)\n"
606
- "2. Summarize what I've done and stop\n"
607
- "3. Try a different approach\n\n"
608
- "Please let me know how to proceed."
550
+ "Plese add more context to the task."
609
551
  )
610
552
  ac.create_user_message(extend_content, state.sm)
611
553
  if state.show_thoughts:
@@ -617,7 +559,6 @@ async def process_request(
617
559
 
618
560
  i += 1
619
561
 
620
- # Final buffered read-only tasks (batch)
621
562
  await _finalize_buffered_tasks(tool_buffer, tool_callback, state)
622
563
 
623
564
  # Build fallback synthesis if needed
@@ -3,7 +3,6 @@ from .base import BaseSetup
3
3
  from .config_setup import ConfigSetup
4
4
  from .coordinator import SetupCoordinator
5
5
  from .environment_setup import EnvironmentSetup
6
- from .git_safety_setup import GitSafetySetup
7
6
  from .template_setup import TemplateSetup
8
7
 
9
8
  __all__ = [
@@ -11,7 +10,6 @@ __all__ = [
11
10
  "SetupCoordinator",
12
11
  "ConfigSetup",
13
12
  "EnvironmentSetup",
14
- "GitSafetySetup",
15
13
  "AgentSetup",
16
14
  "TemplateSetup",
17
15
  ]
@@ -1,5 +1,4 @@
1
- ###Instruction###
2
-
1
+ <instructions>
3
2
  You are "TunaCode", a senior software developer AI assistant operating inside the user's terminal.
4
3
 
5
4
  YOU ARE NOT A CHATBOT. YOU ARE AN OPERATIONAL EXPERIENCED DEVELOPER WITH AGENT WITH TOOLS.
@@ -18,9 +17,10 @@ CRITICAL BEHAVIOR RULES:
18
17
  9. Prefer sequential simplicity: break complex tasks into clear, interactive steps and confirm assumptions.
19
18
  10. Use affirmative directives and directive phrasing in your own planning: "Your task is...", "You MUST..." when restating goals.
20
19
  11. you MUST follow best practises, you will be punished for cheap bandaid fixes. ALWAYS aim to fix issues properly.
20
+ </instructions>
21
21
 
22
22
  ### Completion Signaling
23
-
23
+ <completion>
24
24
  When you have fully completed the user’s task:
25
25
 
26
26
  - Start your response with a single line: `TUNACODE DONE:` followed by a brief outcome summary.
@@ -28,9 +28,10 @@ When you have fully completed the user’s task:
28
28
  - Do NOT mark DONE if you have queued tools in the same response — execute tools first, then mark DONE.
29
29
  - Example:
30
30
  - `TUNACODE DONE: Implemented enum state machine and updated completion logic`
31
+ </completion>
31
32
 
32
33
  ###Tool Access Rules###
33
-
34
+ <tools>
34
35
  You have 9 powerful tools at your disposal. Understanding their categories is CRITICAL for performance:
35
36
 
36
37
  READONLY TOOLS (Safe, ParallelExecutable)
@@ -72,11 +73,11 @@ These tools modify state and MUST run one at a time with user confirmation:
72
73
  9. `bash(command: str)` — Advanced shell with environment control
73
74
  Safety: Enhanced security, output limits (5KB)
74
75
  Use for: Complex scripts, interactive commands
76
+ </tools>
75
77
 
76
78
 
77
79
 
78
- ###Tool Examples LEARN THESE PATTERNS###
79
-
80
+ <examples>
80
81
  CRITICAL: These examples show EXACTLY how to use each tool. Study them carefully.
81
82
 
82
83
  1. read_file Reading File Contents
@@ -284,6 +285,7 @@ bash("echo $PATH && which python && python --version")
284
285
  bash("python -m venv venv && source venv/bin/activate && pip list")
285
286
  → Returns: Installed packages in new venv
286
287
  ```
288
+ </examples>
287
289
 
288
290
  REMEMBER:
289
291
  Always use these exact patterns
@@ -337,7 +339,7 @@ Tool Selection Quick Guide:
337
339
  Need to run commands? → `run_command` (simple) or `bash` (complex)
338
340
 
339
341
  ### CRITICAL JSON FORMATTING RULES ###
340
-
342
+ <formatting>
341
343
  **TOOL ARGUMENT JSON RULES - MUST FOLLOW EXACTLY:**
342
344
 
343
345
  1. **ALWAYS emit exactly ONE JSON object per tool call**
@@ -363,6 +365,7 @@ read_file({"filepath": "main.py"}{"filepath": "config.py"})
363
365
  ```
364
366
 
365
367
  **VALIDATION:** Every tool argument must parse as a single, valid JSON object. Concatenated objects will cause tool execution failures.
368
+ </formatting>
366
369
 
367
370
  OUTPUT AND STYLE RULES:
368
371
  1. Directness: Keep responses short and to the point. Avoid polite filler.
tunacode/setup.py CHANGED
@@ -11,7 +11,6 @@ from tunacode.core.setup import (
11
11
  AgentSetup,
12
12
  ConfigSetup,
13
13
  EnvironmentSetup,
14
- GitSafetySetup,
15
14
  SetupCoordinator,
16
15
  TemplateSetup,
17
16
  )
@@ -39,7 +38,6 @@ async def setup(
39
38
  coordinator.register_step(config_setup)
40
39
  coordinator.register_step(EnvironmentSetup(state_manager))
41
40
  coordinator.register_step(TemplateSetup(state_manager))
42
- coordinator.register_step(GitSafetySetup(state_manager))
43
41
 
44
42
  # Run all setup steps
45
43
  await coordinator.run_setup(force_setup=run_setup, wizard_mode=wizard_mode)
tunacode/tools/grep.py CHANGED
@@ -281,7 +281,6 @@ Usage:
281
281
  def run_enhanced_ripgrep():
282
282
  """Execute ripgrep search using the new executor."""
283
283
  start_time = time.time()
284
- first_match_time = None
285
284
  results = []
286
285
 
287
286
  # Configure timeout from settings
@@ -306,17 +305,8 @@ Usage:
306
305
  context_after=config.context_lines,
307
306
  )
308
307
 
309
- # Track first match time for metrics
310
- if search_results and first_match_time is None:
311
- first_match_time = time.time() - start_time
312
-
313
- # Check if we exceeded the first match deadline
314
- if first_match_time > config.first_match_deadline:
315
- if self._config.get("debug", False):
316
- logger.debug(
317
- f"Search exceeded first match deadline: {first_match_time:.2f}s"
318
- )
319
- raise TooBroadPatternError(pattern, config.first_match_deadline)
308
+ # Ripgrep doesn't provide timing info for first match, so we rely on
309
+ # the overall timeout mechanism instead of first_match_deadline
320
310
 
321
311
  # Parse results
322
312
  for result_line in search_results:
@@ -363,10 +353,7 @@ Usage:
363
353
  )
364
354
 
365
355
  if self._config.get("debug", False):
366
- logger.debug(
367
- f"Ripgrep search completed in {total_time:.2f}s "
368
- f"(first match: {first_match_time:.2f}s if found)"
369
- )
356
+ logger.debug(f"Ripgrep search completed in {total_time:.2f}s")
370
357
 
371
358
  return results
372
359
 
tunacode/ui/output.py CHANGED
@@ -22,27 +22,16 @@ from .constants import SPINNER_TYPE
22
22
  from .decorators import create_sync_wrapper
23
23
  from .logging_compat import ui_logger
24
24
 
25
- # Create console with explicit settings to ensure ANSI codes work properly
26
25
  console = Console()
27
26
  colors = DotDict(UI_COLORS)
28
27
 
29
28
  BANNER = """[bold cyan]
30
- ████████╗██╗ ██╗███╗ ██╗ █████╗
31
- ╚══██╔══╝██║ ██║████╗ ██║██╔══██╗
32
- ██║ ██║ ██║██╔██╗ ██║███████║
33
- ██║ ██║ ██║██║╚██╗██║██╔══██║
34
- ██║ ╚██████╔╝██║ ╚████║██║ ██║
35
- ╚═╝ ╚═════╝ ╚═╝ ╚═══╝╚═╝ ╚═╝
36
-
37
- ██████╗ ██████╗ ██████╗ ███████╗ dev
38
- ██╔════╝██╔═══██╗██╔══██╗██╔════╝
39
- ██║ ██║ ██║██║ ██║█████╗
40
- ██║ ██║ ██║██║ ██║██╔══╝
41
- ╚██████╗╚██████╔╝██████╔╝███████╗
42
- ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝
29
+ ><>
30
+ ><(((°>
31
+ ><> TunaCode <>
43
32
  [/bold cyan]
44
33
 
45
- ● Caution: This tool can modify your codebase - always use git branches"""
34
+ """
46
35
 
47
36
 
48
37
  @create_sync_wrapper
@@ -62,9 +62,9 @@ class ModelLimits(BaseModel):
62
62
  if v is None:
63
63
  return v
64
64
  iv = int(v)
65
- if iv <= 0:
66
- raise ValueError("limits must be positive integers")
67
- return iv
65
+ if iv < 0:
66
+ raise ValueError("limits must be non-negative integers")
67
+ return iv if iv > 0 else None
68
68
 
69
69
  def format_limits(self) -> str:
70
70
  """Format limits as a readable string."""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tunacode-cli
3
- Version: 0.0.76.9
3
+ Version: 0.0.77.1
4
4
  Summary: Your agentic CLI developer.
5
5
  Project-URL: Homepage, https://tunacode.xyz/
6
6
  Project-URL: Repository, https://github.com/alchemiststudiosDOTai/tunacode
@@ -1,9 +1,9 @@
1
1
  tunacode/__init__.py,sha256=yUul8igNYMfUrHnYfioIGAqvrH8b5BKiO_pt1wVnmd0,119
2
- tunacode/constants.py,sha256=8Fd-GBPcVI2qDgNHlCI1klhROmwAg2dOSYhjaCcwSPQ,6170
3
- tunacode/context.py,sha256=YtfRjUiqsSkk2k9Nn_pjb_m-AXyh6XcOBOJWtFI0wVw,2405
2
+ tunacode/constants.py,sha256=my0w0CGjHBr32c9QDlNDMpTqiBJExfTg9qNi61w82M0,6168
3
+ tunacode/context.py,sha256=4xMzqhSBqtTLuXnPBkJzn0SA61G5kAQytFvVY8TDnYY,2395
4
4
  tunacode/exceptions.py,sha256=m80njR-LqBXhFAEOPqCE7N2QPU4Fkjlf_f6CWKO0_Is,8479
5
5
  tunacode/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
- tunacode/setup.py,sha256=F1E4zHVnbByu_Uo6AhCJ-W-lIGF_gV6kB84HLAGLmVY,2103
6
+ tunacode/setup.py,sha256=1GXwD1cNDkGcmylZbytdlGnnHzpsGIwnGpnN0OFp1F0,2022
7
7
  tunacode/types.py,sha256=xNpDRjIRYg4qGNbl3EG8B13CWAWBoob9ekVm8_6dvnc,10496
8
8
  tunacode/cli/__init__.py,sha256=zgs0UbAck8hfvhYsWhWOfBe5oK09ug2De1r4RuQZREA,55
9
9
  tunacode/cli/main.py,sha256=MAKPFA4kGSFciqMdxnyp2r9XzVp8TfxvK6ztt7dvjwM,3445
@@ -16,7 +16,7 @@ tunacode/cli/commands/implementations/__init__.py,sha256=dFczjIqCJTPrsSycD6PZYnp
16
16
  tunacode/cli/commands/implementations/command_reload.py,sha256=GyjeKvJbgE4VYkaasGajspdk9wffumZMNLzfCUeNazM,1555
17
17
  tunacode/cli/commands/implementations/conversation.py,sha256=ZijCNaRi1p5v1Q-IaVHtU2_BripSW3JCVKTtqFkOUjg,4676
18
18
  tunacode/cli/commands/implementations/debug.py,sha256=w2fUgqFB4ipBCmNotbvaOOVW4OiCwJM6MXNWlyKyoqs,6754
19
- tunacode/cli/commands/implementations/development.py,sha256=I8jHgYY3VgjTU8its0D0ysruuVqKbNTBur0JjPIUIZA,2844
19
+ tunacode/cli/commands/implementations/development.py,sha256=Imm8LiDtE69CtLwdtIdOzhCE4FAFEBZJqQimnNtlPY0,2836
20
20
  tunacode/cli/commands/implementations/model.py,sha256=dFRmMlcN78TdGMFX-B2OPyoWqOVQL72XC8ayPyUQmpA,16166
21
21
  tunacode/cli/commands/implementations/plan.py,sha256=iZtvdGPqvGqMr8_lYil8_8NOL1iyc54Bxtb0gb9VOnw,1825
22
22
  tunacode/cli/commands/implementations/quickstart.py,sha256=53H7ubYMGMgmCeYCs6o_F91Q4pd3Ky008lCU4GPuRP8,1363
@@ -36,7 +36,7 @@ tunacode/cli/repl_components/output_display.py,sha256=uzse2bhxSyCWnJD0Ni5lwnp0Bm
36
36
  tunacode/cli/repl_components/tool_executor.py,sha256=IBzlyyJrVJwlmmIetBRli9aIPIJqB4xKfAtGZlvdOgY,3762
37
37
  tunacode/configuration/__init__.py,sha256=MbVXy8bGu0yKehzgdgZ_mfWlYGvIdb1dY2Ly75nfuPE,17
38
38
  tunacode/configuration/defaults.py,sha256=eFUDD73tTWa3HM320BEn0VWM-XuDKW7d6m32qTK2eRI,1313
39
- tunacode/configuration/key_descriptions.py,sha256=tzJOeoIVQryS9HQFoGMyjUk9Wf-YgSMLNc7-mmle_Zk,11412
39
+ tunacode/configuration/key_descriptions.py,sha256=4sD2Up5URtq7ZIcCnt-tZmXrfinMET77MTKEAiEC7x4,11095
40
40
  tunacode/configuration/models.py,sha256=buH8ZquvcYI3OQBDIZeJ08cu00rSCeNABtUwl3VQS0E,4103
41
41
  tunacode/configuration/settings.py,sha256=9wtIWBlLhW_ZBlLx-GA4XDfVZyGj2Gs6Zk49vk-nHq0,1047
42
42
  tunacode/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -44,11 +44,11 @@ tunacode/core/code_index.py,sha256=2qxEn2eTIegV4F_gLeZO5lAOv8mkf4Y_t21whZ9F2Fk,1
44
44
  tunacode/core/state.py,sha256=4SzI_OPiL2VIDywtUavtE6jZnIJS7K2IVgj8AYcawW0,8706
45
45
  tunacode/core/tool_handler.py,sha256=42yUfnq5jgk-0LK93JoJgtsXfVDTf-7hNXyKEfH2FM0,3626
46
46
  tunacode/core/agents/__init__.py,sha256=ZOSvWhqBWX3ADzTUZ7ILY92xZ7VMXanjPQ_Sf37WYmU,1005
47
- tunacode/core/agents/main.py,sha256=wJx07AQrZJGYNtlLSAZKIdwzDug5TRcJchxA-7o1Vyc,25233
47
+ tunacode/core/agents/main.py,sha256=IFKuGy-3t9XJDAs4rO0oTPZ1xGkZAXxoK1efdgIN15Q,23251
48
48
  tunacode/core/agents/utils.py,sha256=cQJbpXzMcixJ1TKdLsEcpJHMMMXiH6_qcT3wbnSQ9gc,9411
49
- tunacode/core/agents/agent_components/__init__.py,sha256=ES2drzZ2dNlTcriM9LgqpSq3pbw97uJY4nqMuvkD5Rs,1650
50
- tunacode/core/agents/agent_components/agent_config.py,sha256=rVoFxmtu8Ly6-UAqTzvyv1NgPYTG5ZuYzJf5Qgo4384,13096
51
- tunacode/core/agents/agent_components/agent_helpers.py,sha256=FFX-zXDhNoXpzMVe2iznBpNzNtk7OJ2lHf44cfZhTk8,8268
49
+ tunacode/core/agents/agent_components/__init__.py,sha256=65V5ijSKen0F0zLvUO3AkZJmCrocSW3lEbqNPrHqxoc,1706
50
+ tunacode/core/agents/agent_components/agent_config.py,sha256=X4S5PCsndoHBd7EHHuNlwrXRN7Q2WuWtmEoC8w-2LKc,12921
51
+ tunacode/core/agents/agent_components/agent_helpers.py,sha256=m0sB0_ztHhRJYFnG2tmQyNmyZk_WnZExBE5jKIHN5lA,9121
52
52
  tunacode/core/agents/agent_components/json_tool_parser.py,sha256=HuyNT0rs-ppx_gLAI2e0XMVGbR_F0WXZfP3sx38VoMg,3447
53
53
  tunacode/core/agents/agent_components/message_handler.py,sha256=KJGOtb9VhumgZpxxwO45HrKLhU9_MwuoWRsSQwJviNU,3704
54
54
  tunacode/core/agents/agent_components/node_processor.py,sha256=oOi3Yuccpdmnlza6vZRr_wB2_OczV0OLcqvs0a3BJcA,24734
@@ -68,20 +68,19 @@ tunacode/core/logging/config.py,sha256=bhJ6KYrEliKC5BehXKXZHHPBJUBX0g5O3uxbr8qUK
68
68
  tunacode/core/logging/formatters.py,sha256=uWx-M0jSvsAVo5JVdCK1VIVawXNONjJ2CvMwPuuUTg8,1236
69
69
  tunacode/core/logging/handlers.py,sha256=lkLczpcI6kSammSdjrCccosGMrRdcAA_3UmuTOiPnxg,3788
70
70
  tunacode/core/logging/logger.py,sha256=9RjRuX0GoUojRJ8WnJGQPFdXiluiJMCoFmvc8xEioB8,142
71
- tunacode/core/setup/__init__.py,sha256=seoWYpRonptxNyApapS-yGz4o3jTj8vLsRPCTUO4siM,439
71
+ tunacode/core/setup/__init__.py,sha256=edzZ5tdWPdokPaOuFgYGEUGY_Fcn6bcWSiDOhGGZTBc,372
72
72
  tunacode/core/setup/agent_setup.py,sha256=tpOIW85C6o1m8pwAZQBIMKxKIyBUOpHHn4JJmDBFH3Q,1403
73
73
  tunacode/core/setup/base.py,sha256=FMjBQQS_q3KOxHqfg7NJGmKq-1nxC40htiPZprzTu7I,970
74
74
  tunacode/core/setup/config_setup.py,sha256=j04mf4DAi_WLJid3h-ylomqQIybWbCMoPd_70JOEfEs,19158
75
75
  tunacode/core/setup/config_wizard.py,sha256=nYlgq1Q645h8hsrD9VdhG933lPdxbDUNREAWYfozfaA,9361
76
76
  tunacode/core/setup/coordinator.py,sha256=5ZhD4rHUrW0RIdGnjmoK4wCvqlNGcXal4Qwev4s039U,2393
77
77
  tunacode/core/setup/environment_setup.py,sha256=n3IrObKEynHZSwtUJ1FddMg2C4sHz7ca42awemImV8s,2225
78
- tunacode/core/setup/git_safety_setup.py,sha256=Htt8A4BAn7F4DbjhNu_SO01zjwaRQ3wMv-vZujE1-JA,7328
79
78
  tunacode/core/setup/template_setup.py,sha256=0lDGhNVCvGN7ykqHnl3pj4CONH3I2PvMzkmIZibfSoc,2640
80
79
  tunacode/core/token_usage/api_response_parser.py,sha256=plLltHg4zGVzxjv3MFj45bbd-NOJeT_v3P0Ki4zlvn4,1831
81
80
  tunacode/core/token_usage/cost_calculator.py,sha256=RjO-O0JENBuGOrWP7QgBZlZxeXC-PAIr8tj_9p_BxOU,2058
82
81
  tunacode/core/token_usage/usage_tracker.py,sha256=YUCnF-712nLrbtEvFrsC-VZuYjKUCz3hf-_do6GKSDA,6016
83
- tunacode/prompts/system.md,sha256=Q9QhUzeHISdxHJNkufo5QNJH5P0b-b8qwhgrMuTc3Zk,13345
84
82
  tunacode/prompts/system.md.bak,sha256=q0gbk_-pvQlNtZBonRo4gNILkKStqNxgDN0ZEwzC3E4,17541
83
+ tunacode/prompts/system.xml,sha256=IKGelP8ukqP3x-bNPCH_MJ7VcdMjrQjo_Mik89ZBKYM,13405
85
84
  tunacode/services/__init__.py,sha256=w_E8QK6RnvKSvU866eDe8BCRV26rAm4d3R-Yg06OWCU,19
86
85
  tunacode/services/mcp.py,sha256=quO13skECUGt-4QE2NkWk6_8qhmZ5qjgibvw8tUOt-4,3761
87
86
  tunacode/templates/__init__.py,sha256=ssEOPrPjyCywtKI-QFcoqcWhMjlfI5TbI8Ip0_wyqGM,241
@@ -91,7 +90,7 @@ tunacode/tools/base.py,sha256=jQz_rz2rNZrKo2vZtyArwiHCMdAaqRYJGYtSZ27nxcU,10711
91
90
  tunacode/tools/bash.py,sha256=fEjI5Vm7yqQiOzc83kFzu1n4zAPiWLQNHZYY-ORNV4Q,12437
92
91
  tunacode/tools/exit_plan_mode.py,sha256=DOl_8CsY7h9N-SuCg2YgMjp8eEMuO5I8Tv8XjoJcTJ0,10597
93
92
  tunacode/tools/glob.py,sha256=_uAMV5cloRP0AQMbm7h_bKeqfhe7KFoBx9gfYls5ZzE,22956
94
- tunacode/tools/grep.py,sha256=nKKpJjr2uupErB2KAUgTog3ZqC8oKiho5qkKeFvpY70,22178
93
+ tunacode/tools/grep.py,sha256=5539a4VHNUhfyKBd51mRYIckOMpL1N82otHoTwu9w0Y,21545
95
94
  tunacode/tools/list_dir.py,sha256=aJ2FdAUU-HxOmAwBk188KYIYB94thESIrSBflzoUlYs,12402
96
95
  tunacode/tools/present_plan.py,sha256=PjpZ7Ll9T6Ij-oBNPK9iysvGJZpvKr1-lqBpURNXiLM,10856
97
96
  tunacode/tools/react.py,sha256=qEXhtxFM3skoz__L9R0Rabt1bmKdNkRyFMyAgNB_TFo,5602
@@ -134,7 +133,7 @@ tunacode/ui/keybindings.py,sha256=8j58NN432XyawffssFNe86leXaPur12qBX3O7hOOGsc,23
134
133
  tunacode/ui/lexers.py,sha256=tmg4ic1enyTRLzanN5QPP7D_0n12YjX_8ZhsffzhXA4,1340
135
134
  tunacode/ui/logging_compat.py,sha256=5v6lcjVaG1CxdY1Zm9FAGr9H7Sy-tP6ihGfhP-5YvAY,1406
136
135
  tunacode/ui/model_selector.py,sha256=07kV9v0VWFgDapiXTz1_BjzHF1AliRq24I9lDq-hmHc,13426
137
- tunacode/ui/output.py,sha256=ybVVutiilOQcULtA1zjjs_tTu5okwxHFp2MHtNz3s2E,6767
136
+ tunacode/ui/output.py,sha256=YW0ABzZQbIE_jkDdrWVIeinTPuQUTnl-W638RUAqeoA,5569
138
137
  tunacode/ui/panels.py,sha256=6XGOeax4m-yQvwP1iML67GK10AKlDLD46dB522gcNPU,17236
139
138
  tunacode/ui/path_heuristics.py,sha256=SkhGaM8WCRuK86vLwypbfhtI81PrXtOsWoz-P0CTsmQ,2221
140
139
  tunacode/ui/prompt_manager.py,sha256=HUL6443pFPb41uDAnAKD-sZsrWd_VhWYRGwvrFH_9SI,5618
@@ -151,7 +150,7 @@ tunacode/utils/file_utils.py,sha256=84g-MQRzmBI2aG_CuXsDl2OhvvWoSL7YdL5Kz_UKSwk,
151
150
  tunacode/utils/import_cache.py,sha256=q_xjJbtju05YbFopLDSkIo1hOtCx3DOTl3GQE5FFDgs,295
152
151
  tunacode/utils/json_utils.py,sha256=cMVctSwwV9Z1c-rZdj6UuOlZwsUPSTF5oUruP6uPix0,6470
153
152
  tunacode/utils/message_utils.py,sha256=V4MrZZPmwO22_MVGupMqtE5ltQEBwaSIqGD5LEb_bLw,1050
154
- tunacode/utils/models_registry.py,sha256=zNbujehYZ5FQLU8ywY9cSOcQDRh1ToLoGLGekEfOtbg,21254
153
+ tunacode/utils/models_registry.py,sha256=Tn2ByGFV1yJsWumFYy6JuT0eVpuPeZ1Zxj6JYsRRy1g,21277
155
154
  tunacode/utils/retry.py,sha256=AHdUzY6m-mwlT4OPXdtWWMAafL_NeS7JAMORGyM8c5k,4931
156
155
  tunacode/utils/ripgrep.py,sha256=VdGWYPQ1zCwUidw2QicuVmG5OiAgqI93jAsjS3y3ksE,11001
157
156
  tunacode/utils/security.py,sha256=i3eGKg4o-qY2S_ObTlEaHO93q14iBfiPXR5O7srHn58,6579
@@ -159,8 +158,8 @@ tunacode/utils/system.py,sha256=J8KqJ4ZqQrNSnM5rrJxPeMk9z2xQQp6dWtI1SKBY1-0,1112
159
158
  tunacode/utils/text_utils.py,sha256=HAwlT4QMy41hr53cDbbNeNo05MI461TpI9b_xdIv8EY,7288
160
159
  tunacode/utils/token_counter.py,sha256=dmFuqVz4ywGFdLfAi5Mg9bAGf8v87Ek-mHU-R3fsYjI,2711
161
160
  tunacode/utils/user_configuration.py,sha256=OA-L0BgWNbf9sWpc8lyivgLscwJdpdI8TAYbe0wRs1s,4836
162
- tunacode_cli-0.0.76.9.dist-info/METADATA,sha256=oZtCGyxk4S-zEvwH2U3F3BXwKMFl1ewpQxI2T8byzi0,8913
163
- tunacode_cli-0.0.76.9.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
164
- tunacode_cli-0.0.76.9.dist-info/entry_points.txt,sha256=hbkytikj4dGu6rizPuAd_DGUPBGF191RTnhr9wdhORY,51
165
- tunacode_cli-0.0.76.9.dist-info/licenses/LICENSE,sha256=Btzdu2kIoMbdSp6OyCLupB1aRgpTCJ_szMimgEnpkkE,1056
166
- tunacode_cli-0.0.76.9.dist-info/RECORD,,
161
+ tunacode_cli-0.0.77.1.dist-info/METADATA,sha256=64_1eQhSU7--GGUc5RVSArPEfWhD0sJKAYmwuDF7dQs,8913
162
+ tunacode_cli-0.0.77.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
163
+ tunacode_cli-0.0.77.1.dist-info/entry_points.txt,sha256=hbkytikj4dGu6rizPuAd_DGUPBGF191RTnhr9wdhORY,51
164
+ tunacode_cli-0.0.77.1.dist-info/licenses/LICENSE,sha256=Btzdu2kIoMbdSp6OyCLupB1aRgpTCJ_szMimgEnpkkE,1056
165
+ tunacode_cli-0.0.77.1.dist-info/RECORD,,
@@ -1,186 +0,0 @@
1
- """Git safety setup to create a working branch for TunaCode."""
2
-
3
- import subprocess
4
- from pathlib import Path
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
- from tunacode.ui.input import input as prompt_input
10
- from tunacode.ui.panels import panel
11
-
12
-
13
- async def yes_no_prompt(question: str, default: bool = True) -> bool:
14
- """Simple yes/no prompt."""
15
- default_text = "[Y/n]" if default else "[y/N]"
16
- response = await prompt_input(session_key="yes_no", pretext=f"{question} {default_text}: ")
17
-
18
- if not response.strip():
19
- return default
20
-
21
- return response.lower().strip() in ["y", "yes"]
22
-
23
-
24
- class GitSafetySetup(BaseSetup):
25
- """Setup step to create a safe working branch for TunaCode."""
26
-
27
- def __init__(self, state_manager: StateManager):
28
- super().__init__(state_manager)
29
-
30
- @property
31
- def name(self) -> str:
32
- """Return the name of this setup step."""
33
- return "Git Safety"
34
-
35
- async def should_run(self, _force: bool = False) -> bool:
36
- """Check if we should run git safety setup."""
37
- # Always run unless user has explicitly disabled it
38
- return not self.state_manager.session.user_config.get("skip_git_safety", False)
39
-
40
- async def execute(self, _force: bool = False, wizard_mode: bool = False) -> None:
41
- """Create a safety branch for TunaCode operations."""
42
- # Skip git safety during wizard mode to avoid UI interference
43
- if wizard_mode:
44
- return
45
-
46
- try:
47
- # Check if git is installed
48
- result = subprocess.run(
49
- ["git", "--version"], capture_output=True, text=True, check=False
50
- )
51
-
52
- if result.returncode != 0:
53
- await panel(
54
- " Git Not Found",
55
- "Git is not installed or not in PATH. TunaCode will modify files directly.\n"
56
- "It's strongly recommended to install Git for safety.",
57
- border_style="yellow",
58
- )
59
- return
60
-
61
- # Check if we're in a git repository
62
- result = subprocess.run(
63
- ["git", "rev-parse", "--git-dir"],
64
- capture_output=True,
65
- text=True,
66
- check=False,
67
- cwd=Path.cwd(),
68
- )
69
-
70
- if result.returncode != 0:
71
- await panel(
72
- " Not a Git Repository",
73
- "This directory is not a Git repository. TunaCode will modify files directly.\n"
74
- "Consider initializing a Git repository for safety: git init",
75
- border_style="yellow",
76
- )
77
- return
78
-
79
- # Get current branch name
80
- result = subprocess.run(
81
- ["git", "branch", "--show-current"], capture_output=True, text=True, check=True
82
- )
83
- current_branch = result.stdout.strip()
84
-
85
- if not current_branch:
86
- # Detached HEAD state
87
- await panel(
88
- " Detached HEAD State",
89
- "You're in a detached HEAD state. TunaCode will continue without creating a branch.",
90
- border_style="yellow",
91
- )
92
- return
93
-
94
- # Check if we're already on a -tunacode branch
95
- if current_branch.endswith("-tunacode"):
96
- await ui.info(f"Already on a TunaCode branch: {current_branch}")
97
- return
98
-
99
- # Propose new branch name
100
- new_branch = f"{current_branch}-tunacode"
101
-
102
- # Check if there are uncommitted changes
103
- result = subprocess.run(
104
- ["git", "status", "--porcelain"], capture_output=True, text=True, check=True
105
- )
106
-
107
- has_changes = bool(result.stdout.strip())
108
-
109
- # Ask user if they want to create a safety branch
110
- message = (
111
- f"For safety, TunaCode can create a new branch '{new_branch}' based on '{current_branch}'.\n"
112
- f"This helps protect your work from unintended changes.\n"
113
- )
114
-
115
- if has_changes:
116
- message += "\n You have uncommitted changes that will be brought to the new branch."
117
-
118
- create_branch = await yes_no_prompt(f"{message}\n\nCreate safety branch?", default=True)
119
-
120
- if not create_branch:
121
- # User declined - show warning
122
- await panel(
123
- " Working Without Safety Branch",
124
- "You've chosen to work directly on your current branch.\n"
125
- "TunaCode will modify files in place. Make sure you have backups!",
126
- border_style="red",
127
- )
128
- # Save preference
129
- self.state_manager.session.user_config["skip_git_safety"] = True
130
- # Save the updated configuration to disk
131
- try:
132
- from tunacode.utils.user_configuration import save_config
133
-
134
- save_config(self.state_manager)
135
- except Exception as e:
136
- # Log the error but don't fail the setup process
137
- import logging
138
-
139
- logging.warning(f"Failed to save skip_git_safety preference: {e}")
140
- return
141
-
142
- # Create and checkout the new branch
143
- try:
144
- # Check if branch already exists
145
- result = subprocess.run(
146
- ["git", "show-ref", "--verify", f"refs/heads/{new_branch}"],
147
- capture_output=True,
148
- check=False,
149
- text=True,
150
- )
151
-
152
- if result.returncode == 0:
153
- # Branch exists, ask to use it
154
- use_existing = await yes_no_prompt(
155
- f"Branch '{new_branch}' already exists. Switch to it?", default=True
156
- )
157
- if use_existing:
158
- subprocess.run(["git", "checkout", new_branch], check=True)
159
- await ui.success(f"Switched to existing branch: {new_branch}")
160
- else:
161
- await ui.warning("Continuing on current branch")
162
- else:
163
- # Create new branch
164
- subprocess.run(["git", "checkout", "-b", new_branch], check=True)
165
- await ui.success(f"Created and switched to new branch: {new_branch}")
166
-
167
- except subprocess.CalledProcessError as e:
168
- await panel(
169
- " Failed to Create Branch",
170
- f"Could not create branch '{new_branch}': {str(e)}\n"
171
- "Continuing on current branch.",
172
- border_style="red",
173
- )
174
-
175
- except Exception as e:
176
- # Non-fatal error - just warn the user
177
- await panel(
178
- " Git Safety Setup Failed",
179
- f"Could not set up Git safety: {str(e)}\n"
180
- "TunaCode will continue without branch protection.",
181
- border_style="yellow",
182
- )
183
-
184
- async def validate(self) -> bool:
185
- """Validate git safety setup - always returns True as this is optional."""
186
- return True