tunacode-cli 0.0.17__py3-none-any.whl → 0.0.19__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.py +73 -41
- tunacode/cli/main.py +29 -26
- tunacode/cli/repl.py +91 -37
- tunacode/cli/textual_app.py +69 -66
- tunacode/cli/textual_bridge.py +33 -32
- tunacode/configuration/settings.py +2 -9
- tunacode/constants.py +2 -4
- tunacode/context.py +1 -1
- tunacode/core/agents/__init__.py +12 -0
- tunacode/core/agents/main.py +89 -63
- tunacode/core/agents/orchestrator.py +99 -0
- tunacode/core/agents/planner_schema.py +9 -0
- tunacode/core/agents/readonly.py +51 -0
- tunacode/core/background/__init__.py +0 -0
- tunacode/core/background/manager.py +36 -0
- tunacode/core/llm/__init__.py +0 -0
- tunacode/core/llm/planner.py +63 -0
- tunacode/core/setup/config_setup.py +79 -44
- tunacode/core/setup/coordinator.py +20 -13
- tunacode/core/setup/git_safety_setup.py +35 -49
- tunacode/core/state.py +2 -9
- tunacode/exceptions.py +0 -2
- tunacode/prompts/system.txt +179 -69
- tunacode/tools/__init__.py +10 -1
- tunacode/tools/base.py +1 -1
- tunacode/tools/bash.py +5 -5
- tunacode/tools/grep.py +210 -250
- tunacode/tools/read_file.py +2 -8
- tunacode/tools/run_command.py +4 -11
- tunacode/tools/update_file.py +2 -6
- tunacode/ui/completers.py +32 -31
- tunacode/ui/console.py +3 -3
- tunacode/ui/input.py +8 -5
- tunacode/ui/keybindings.py +1 -3
- tunacode/ui/lexers.py +16 -16
- tunacode/ui/output.py +2 -2
- tunacode/ui/panels.py +8 -8
- tunacode/ui/prompt_manager.py +19 -7
- tunacode/utils/import_cache.py +11 -0
- tunacode/utils/user_configuration.py +24 -2
- {tunacode_cli-0.0.17.dist-info → tunacode_cli-0.0.19.dist-info}/METADATA +68 -11
- tunacode_cli-0.0.19.dist-info/RECORD +75 -0
- tunacode_cli-0.0.17.dist-info/RECORD +0 -67
- {tunacode_cli-0.0.17.dist-info → tunacode_cli-0.0.19.dist-info}/WHEEL +0 -0
- {tunacode_cli-0.0.17.dist-info → tunacode_cli-0.0.19.dist-info}/entry_points.txt +0 -0
- {tunacode_cli-0.0.17.dist-info → tunacode_cli-0.0.19.dist-info}/licenses/LICENSE +0 -0
- {tunacode_cli-0.0.17.dist-info → tunacode_cli-0.0.19.dist-info}/top_level.txt +0 -0
|
@@ -38,9 +38,30 @@ class ConfigSetup(BaseSetup):
|
|
|
38
38
|
return True
|
|
39
39
|
|
|
40
40
|
async def execute(self, force_setup: bool = False) -> None:
|
|
41
|
-
"""Setup configuration and run onboarding if needed."""
|
|
41
|
+
"""Setup configuration and run onboarding if needed, with config fingerprint fast path."""
|
|
42
|
+
import hashlib
|
|
43
|
+
|
|
42
44
|
self.state_manager.session.device_id = system.get_device_id()
|
|
43
45
|
loaded_config = user_configuration.load_config()
|
|
46
|
+
# Fast path: if config fingerprint matches last loaded and config is already present, skip reprocessing
|
|
47
|
+
new_fp = None
|
|
48
|
+
if loaded_config:
|
|
49
|
+
b = json.dumps(loaded_config, sort_keys=True).encode()
|
|
50
|
+
new_fp = hashlib.sha1(b).hexdigest()[:12]
|
|
51
|
+
last_fp = getattr(self.state_manager, "_config_fingerprint", None)
|
|
52
|
+
if (
|
|
53
|
+
loaded_config
|
|
54
|
+
and not force_setup
|
|
55
|
+
and new_fp
|
|
56
|
+
and last_fp == new_fp
|
|
57
|
+
and getattr(self.state_manager, "_config_valid", False)
|
|
58
|
+
):
|
|
59
|
+
# Fast path: config unchanged, already validated
|
|
60
|
+
self.state_manager.session.user_config = loaded_config
|
|
61
|
+
self.state_manager.session.current_model = loaded_config["default_model"]
|
|
62
|
+
return
|
|
63
|
+
# Save current config fingerprint for next run
|
|
64
|
+
self.state_manager._config_fingerprint = new_fp
|
|
44
65
|
|
|
45
66
|
# Handle CLI configuration if provided
|
|
46
67
|
if self.cli_config and any(self.cli_config.values()):
|
|
@@ -50,9 +71,7 @@ class ConfigSetup(BaseSetup):
|
|
|
50
71
|
if loaded_config and not force_setup:
|
|
51
72
|
# Silent loading
|
|
52
73
|
# Merge loaded config with defaults to ensure all required keys exist
|
|
53
|
-
self.state_manager.session.user_config = self._merge_with_defaults(
|
|
54
|
-
loaded_config
|
|
55
|
-
)
|
|
74
|
+
self.state_manager.session.user_config = self._merge_with_defaults(loaded_config)
|
|
56
75
|
else:
|
|
57
76
|
if force_setup:
|
|
58
77
|
await ui.muted("Running setup process, resetting config")
|
|
@@ -64,13 +83,18 @@ class ConfigSetup(BaseSetup):
|
|
|
64
83
|
else:
|
|
65
84
|
# No config found - show CLI usage instead of onboarding
|
|
66
85
|
from tunacode.ui.console import console
|
|
86
|
+
|
|
67
87
|
console.print("\n[bold red]No configuration found![/bold red]")
|
|
68
88
|
console.print("\n[bold]Quick Setup:[/bold]")
|
|
69
89
|
console.print("Configure TunaCode using CLI flags:")
|
|
70
90
|
console.print("\n[blue]Examples:[/blue]")
|
|
71
91
|
console.print(" [green]tunacode --model 'openai:gpt-4' --key 'your-key'[/green]")
|
|
72
|
-
console.print(
|
|
73
|
-
|
|
92
|
+
console.print(
|
|
93
|
+
" [green]tunacode --model 'anthropic:claude-3-opus' --key 'your-key'[/green]"
|
|
94
|
+
)
|
|
95
|
+
console.print(
|
|
96
|
+
" [green]tunacode --model 'openrouter:anthropic/claude-3.5-sonnet' --key 'your-key' --baseurl 'https://openrouter.ai/api/v1'[/green]"
|
|
97
|
+
)
|
|
74
98
|
console.print("\n[yellow]Run 'tunacode --help' for more options[/yellow]\n")
|
|
75
99
|
raise SystemExit(0)
|
|
76
100
|
|
|
@@ -84,23 +108,24 @@ class ConfigSetup(BaseSetup):
|
|
|
84
108
|
|
|
85
109
|
# No model validation - trust user's model choice
|
|
86
110
|
|
|
87
|
-
self.state_manager.session.current_model =
|
|
88
|
-
|
|
89
|
-
|
|
111
|
+
self.state_manager.session.current_model = self.state_manager.session.user_config[
|
|
112
|
+
"default_model"
|
|
113
|
+
]
|
|
90
114
|
|
|
91
115
|
async def validate(self) -> bool:
|
|
92
116
|
"""Validate that configuration is properly set up."""
|
|
93
117
|
# Check that we have a user config
|
|
118
|
+
valid = True
|
|
94
119
|
if not self.state_manager.session.user_config:
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
return
|
|
120
|
+
valid = False
|
|
121
|
+
elif not self.state_manager.session.user_config.get("default_model"):
|
|
122
|
+
valid = False
|
|
123
|
+
# Cache result for fastpath
|
|
124
|
+
if valid:
|
|
125
|
+
setattr(self.state_manager, "_config_valid", True)
|
|
126
|
+
else:
|
|
127
|
+
setattr(self.state_manager, "_config_valid", False)
|
|
128
|
+
return valid
|
|
104
129
|
|
|
105
130
|
def _merge_with_defaults(self, loaded_config: UserConfig) -> UserConfig:
|
|
106
131
|
"""Merge loaded config with defaults to ensure all required keys exist."""
|
|
@@ -119,9 +144,7 @@ class ConfigSetup(BaseSetup):
|
|
|
119
144
|
|
|
120
145
|
async def _onboarding(self):
|
|
121
146
|
"""Run the onboarding process for new users."""
|
|
122
|
-
initial_config = json.dumps(
|
|
123
|
-
self.state_manager.session.user_config, sort_keys=True
|
|
124
|
-
)
|
|
147
|
+
initial_config = json.dumps(self.state_manager.session.user_config, sort_keys=True)
|
|
125
148
|
|
|
126
149
|
await self._step1_api_keys()
|
|
127
150
|
|
|
@@ -134,15 +157,11 @@ class ConfigSetup(BaseSetup):
|
|
|
134
157
|
await self._step2_default_model()
|
|
135
158
|
|
|
136
159
|
# Compare configs to see if anything changed
|
|
137
|
-
current_config = json.dumps(
|
|
138
|
-
self.state_manager.session.user_config, sort_keys=True
|
|
139
|
-
)
|
|
160
|
+
current_config = json.dumps(self.state_manager.session.user_config, sort_keys=True)
|
|
140
161
|
if initial_config != current_config:
|
|
141
162
|
if user_configuration.save_config(self.state_manager):
|
|
142
163
|
message = f"Config saved to: [bold]{self.config_file}[/bold]"
|
|
143
|
-
await ui.panel(
|
|
144
|
-
"Finished", message, top=0, border_style=UI_COLORS["success"]
|
|
145
|
-
)
|
|
164
|
+
await ui.panel("Finished", message, top=0, border_style=UI_COLORS["success"])
|
|
146
165
|
else:
|
|
147
166
|
await ui.error("Failed to save configuration.")
|
|
148
167
|
else:
|
|
@@ -194,8 +213,10 @@ class ConfigSetup(BaseSetup):
|
|
|
194
213
|
async def _step2_default_model_simple(self):
|
|
195
214
|
"""Simple model selection - just enter model name."""
|
|
196
215
|
await ui.muted("Format: provider:model-name")
|
|
197
|
-
await ui.muted(
|
|
198
|
-
|
|
216
|
+
await ui.muted(
|
|
217
|
+
"Examples: openai:gpt-4.1, anthropic:claude-3-opus, google-gla:gemini-2.0-flash"
|
|
218
|
+
)
|
|
219
|
+
|
|
199
220
|
while True:
|
|
200
221
|
model_name = await ui.input(
|
|
201
222
|
"step2",
|
|
@@ -203,14 +224,14 @@ class ConfigSetup(BaseSetup):
|
|
|
203
224
|
state_manager=self.state_manager,
|
|
204
225
|
)
|
|
205
226
|
model_name = model_name.strip()
|
|
206
|
-
|
|
227
|
+
|
|
207
228
|
# Check if provider prefix is present
|
|
208
229
|
if ":" not in model_name:
|
|
209
230
|
await ui.error("Model name must include provider prefix")
|
|
210
231
|
await ui.muted("Format: provider:model-name")
|
|
211
232
|
await ui.muted("You can always change it later with /model")
|
|
212
233
|
continue
|
|
213
|
-
|
|
234
|
+
|
|
214
235
|
# No validation - user is responsible for correct model names
|
|
215
236
|
self.state_manager.session.user_config["default_model"] = model_name
|
|
216
237
|
await ui.warning("Model set without validation - verify the model name is correct")
|
|
@@ -224,26 +245,38 @@ class ConfigSetup(BaseSetup):
|
|
|
224
245
|
self.state_manager.session.user_config = self._merge_with_defaults(loaded_config)
|
|
225
246
|
else:
|
|
226
247
|
self.state_manager.session.user_config = DEFAULT_USER_CONFIG.copy()
|
|
227
|
-
|
|
248
|
+
|
|
228
249
|
# Apply CLI overrides
|
|
229
250
|
if self.cli_config.get("key"):
|
|
230
251
|
# Determine which API key to set based on the model or baseurl
|
|
231
252
|
if self.cli_config.get("baseurl") and "openrouter" in self.cli_config["baseurl"]:
|
|
232
|
-
self.state_manager.session.user_config["env"]["OPENROUTER_API_KEY"] =
|
|
253
|
+
self.state_manager.session.user_config["env"]["OPENROUTER_API_KEY"] = (
|
|
254
|
+
self.cli_config["key"]
|
|
255
|
+
)
|
|
233
256
|
elif self.cli_config.get("model"):
|
|
234
257
|
if "claude" in self.cli_config["model"] or "anthropic" in self.cli_config["model"]:
|
|
235
|
-
self.state_manager.session.user_config["env"]["ANTHROPIC_API_KEY"] =
|
|
258
|
+
self.state_manager.session.user_config["env"]["ANTHROPIC_API_KEY"] = (
|
|
259
|
+
self.cli_config["key"]
|
|
260
|
+
)
|
|
236
261
|
elif "gpt" in self.cli_config["model"] or "openai" in self.cli_config["model"]:
|
|
237
|
-
self.state_manager.session.user_config["env"]["OPENAI_API_KEY"] =
|
|
262
|
+
self.state_manager.session.user_config["env"]["OPENAI_API_KEY"] = (
|
|
263
|
+
self.cli_config["key"]
|
|
264
|
+
)
|
|
238
265
|
elif "gemini" in self.cli_config["model"]:
|
|
239
|
-
self.state_manager.session.user_config["env"]["GEMINI_API_KEY"] =
|
|
266
|
+
self.state_manager.session.user_config["env"]["GEMINI_API_KEY"] = (
|
|
267
|
+
self.cli_config["key"]
|
|
268
|
+
)
|
|
240
269
|
else:
|
|
241
270
|
# Default to OpenRouter for unknown models
|
|
242
|
-
self.state_manager.session.user_config["env"]["OPENROUTER_API_KEY"] =
|
|
243
|
-
|
|
271
|
+
self.state_manager.session.user_config["env"]["OPENROUTER_API_KEY"] = (
|
|
272
|
+
self.cli_config["key"]
|
|
273
|
+
)
|
|
274
|
+
|
|
244
275
|
if self.cli_config.get("baseurl"):
|
|
245
|
-
self.state_manager.session.user_config["env"]["OPENAI_BASE_URL"] = self.cli_config[
|
|
246
|
-
|
|
276
|
+
self.state_manager.session.user_config["env"]["OPENAI_BASE_URL"] = self.cli_config[
|
|
277
|
+
"baseurl"
|
|
278
|
+
]
|
|
279
|
+
|
|
247
280
|
if self.cli_config.get("model"):
|
|
248
281
|
model = self.cli_config["model"]
|
|
249
282
|
# Require provider prefix
|
|
@@ -253,12 +286,14 @@ class ConfigSetup(BaseSetup):
|
|
|
253
286
|
"Format: provider:model-name\n"
|
|
254
287
|
"Examples: openai:gpt-4.1, anthropic:claude-3-opus"
|
|
255
288
|
)
|
|
256
|
-
|
|
289
|
+
|
|
257
290
|
self.state_manager.session.user_config["default_model"] = model
|
|
258
|
-
|
|
291
|
+
|
|
259
292
|
# Set current model
|
|
260
|
-
self.state_manager.session.current_model = self.state_manager.session.user_config[
|
|
261
|
-
|
|
293
|
+
self.state_manager.session.current_model = self.state_manager.session.user_config[
|
|
294
|
+
"default_model"
|
|
295
|
+
]
|
|
296
|
+
|
|
262
297
|
# Save the configuration
|
|
263
298
|
if user_configuration.save_config(self.state_manager):
|
|
264
299
|
await ui.warning("Model set without validation - verify the model name is correct")
|
|
@@ -23,24 +23,31 @@ class SetupCoordinator:
|
|
|
23
23
|
self.setup_steps.append(step)
|
|
24
24
|
|
|
25
25
|
async def run_setup(self, force_setup: bool = False) -> None:
|
|
26
|
-
"""Run all registered setup steps
|
|
26
|
+
"""Run all registered setup steps concurrently if possible."""
|
|
27
|
+
# Run should_run checks sequentially (they may depend on order)
|
|
28
|
+
steps_to_run = []
|
|
27
29
|
for step in self.setup_steps:
|
|
28
30
|
try:
|
|
29
31
|
if await step.should_run(force_setup):
|
|
30
|
-
|
|
31
|
-
await step.execute(force_setup)
|
|
32
|
-
|
|
33
|
-
if not await step.validate():
|
|
34
|
-
await ui.error(f"Setup validation failed: {step.name}")
|
|
35
|
-
raise RuntimeError(
|
|
36
|
-
f"Setup step '{step.name}' failed validation"
|
|
37
|
-
)
|
|
38
|
-
else:
|
|
39
|
-
# Skip silently
|
|
40
|
-
pass
|
|
32
|
+
steps_to_run.append(step)
|
|
41
33
|
except Exception as e:
|
|
42
|
-
await ui.error(
|
|
34
|
+
await ui.error(
|
|
35
|
+
f"Setup failed at step '{getattr(step, 'name', repr(step))}': {str(e)}"
|
|
36
|
+
)
|
|
43
37
|
raise
|
|
38
|
+
# Run all .execute(force_setup) in parallel where possible (independent steps)
|
|
39
|
+
from asyncio import gather
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
await gather(*(step.execute(force_setup) for step in steps_to_run))
|
|
43
|
+
# Now validate all sequentially: if any fail, raise error
|
|
44
|
+
for step in steps_to_run:
|
|
45
|
+
if not await step.validate():
|
|
46
|
+
await ui.error(f"Setup validation failed: {step.name}")
|
|
47
|
+
raise RuntimeError(f"Setup step '{step.name}' failed validation")
|
|
48
|
+
except Exception as e:
|
|
49
|
+
await ui.error(f"Setup error: {str(e)}")
|
|
50
|
+
raise
|
|
44
51
|
|
|
45
52
|
def clear_steps(self) -> None:
|
|
46
53
|
"""Clear all registered setup steps."""
|
|
@@ -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
|
-
|
|
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 [
|
|
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,30 @@ 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
85
|
"You're in a detached HEAD state. TunaCode will continue without creating a branch.",
|
|
95
|
-
border_style="yellow"
|
|
86
|
+
border_style="yellow",
|
|
96
87
|
)
|
|
97
88
|
return
|
|
98
89
|
|
|
@@ -103,57 +94,52 @@ class GitSafetySetup(BaseSetup):
|
|
|
103
94
|
|
|
104
95
|
# Propose new branch name
|
|
105
96
|
new_branch = f"{current_branch}-tunacode"
|
|
106
|
-
|
|
97
|
+
|
|
107
98
|
# Check if there are uncommitted changes
|
|
108
99
|
result = subprocess.run(
|
|
109
|
-
["git", "status", "--porcelain"],
|
|
110
|
-
capture_output=True,
|
|
111
|
-
text=True,
|
|
112
|
-
check=True
|
|
100
|
+
["git", "status", "--porcelain"], capture_output=True, text=True, check=True
|
|
113
101
|
)
|
|
114
|
-
|
|
102
|
+
|
|
115
103
|
has_changes = bool(result.stdout.strip())
|
|
116
|
-
|
|
104
|
+
|
|
117
105
|
# Ask user if they want to create a safety branch
|
|
118
106
|
message = (
|
|
119
107
|
f"For safety, TunaCode can create a new branch '{new_branch}' based on '{current_branch}'.\n"
|
|
120
108
|
f"This helps protect your work from unintended changes.\n"
|
|
121
109
|
)
|
|
122
|
-
|
|
110
|
+
|
|
123
111
|
if has_changes:
|
|
124
|
-
message +=
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
112
|
+
message += (
|
|
113
|
+
"\n⚠️ You have uncommitted changes that will be brought to the new branch."
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
create_branch = await yes_no_prompt(f"{message}\n\nCreate safety branch?", default=True)
|
|
117
|
+
|
|
131
118
|
if not create_branch:
|
|
132
119
|
# User declined - show warning
|
|
133
120
|
await panel(
|
|
134
121
|
"⚠️ Working Without Safety Branch",
|
|
135
122
|
"You've chosen to work directly on your current branch.\n"
|
|
136
123
|
"TunaCode will modify files in place. Make sure you have backups!",
|
|
137
|
-
border_style="red"
|
|
124
|
+
border_style="red",
|
|
138
125
|
)
|
|
139
126
|
# Save preference
|
|
140
127
|
self.state_manager.session.user_config["skip_git_safety"] = True
|
|
141
128
|
return
|
|
142
|
-
|
|
129
|
+
|
|
143
130
|
# Create and checkout the new branch
|
|
144
131
|
try:
|
|
145
132
|
# Check if branch already exists
|
|
146
133
|
result = subprocess.run(
|
|
147
134
|
["git", "show-ref", "--verify", f"refs/heads/{new_branch}"],
|
|
148
135
|
capture_output=True,
|
|
149
|
-
check=False
|
|
136
|
+
check=False,
|
|
150
137
|
)
|
|
151
|
-
|
|
138
|
+
|
|
152
139
|
if result.returncode == 0:
|
|
153
140
|
# Branch exists, ask to use it
|
|
154
141
|
use_existing = await yes_no_prompt(
|
|
155
|
-
f"Branch '{new_branch}' already exists. Switch to it?",
|
|
156
|
-
default=True
|
|
142
|
+
f"Branch '{new_branch}' already exists. Switch to it?", default=True
|
|
157
143
|
)
|
|
158
144
|
if use_existing:
|
|
159
145
|
subprocess.run(["git", "checkout", new_branch], check=True)
|
|
@@ -164,24 +150,24 @@ class GitSafetySetup(BaseSetup):
|
|
|
164
150
|
# Create new branch
|
|
165
151
|
subprocess.run(["git", "checkout", "-b", new_branch], check=True)
|
|
166
152
|
await ui.success(f"Created and switched to new branch: {new_branch}")
|
|
167
|
-
|
|
153
|
+
|
|
168
154
|
except subprocess.CalledProcessError as e:
|
|
169
155
|
await panel(
|
|
170
156
|
"❌ Failed to Create Branch",
|
|
171
157
|
f"Could not create branch '{new_branch}': {str(e)}\n"
|
|
172
158
|
"Continuing on current branch.",
|
|
173
|
-
border_style="red"
|
|
159
|
+
border_style="red",
|
|
174
160
|
)
|
|
175
|
-
|
|
161
|
+
|
|
176
162
|
except Exception as e:
|
|
177
163
|
# Non-fatal error - just warn the user
|
|
178
164
|
await panel(
|
|
179
165
|
"⚠️ Git Safety Setup Failed",
|
|
180
166
|
f"Could not set up Git safety: {str(e)}\n"
|
|
181
167
|
"TunaCode will continue without branch protection.",
|
|
182
|
-
border_style="yellow"
|
|
168
|
+
border_style="yellow",
|
|
183
169
|
)
|
|
184
170
|
|
|
185
171
|
async def validate(self) -> bool:
|
|
186
172
|
"""Validate git safety setup - always returns True as this is optional."""
|
|
187
|
-
return True
|
|
173
|
+
return True
|
tunacode/core/state.py
CHANGED
|
@@ -8,15 +8,8 @@ import uuid
|
|
|
8
8
|
from dataclasses import dataclass, field
|
|
9
9
|
from typing import Any, Optional
|
|
10
10
|
|
|
11
|
-
from tunacode.types import (
|
|
12
|
-
|
|
13
|
-
InputSessions,
|
|
14
|
-
MessageHistory,
|
|
15
|
-
ModelName,
|
|
16
|
-
SessionId,
|
|
17
|
-
ToolName,
|
|
18
|
-
UserConfig,
|
|
19
|
-
)
|
|
11
|
+
from tunacode.types import (DeviceId, InputSessions, MessageHistory, ModelName, SessionId, ToolName,
|
|
12
|
+
UserConfig)
|
|
20
13
|
|
|
21
14
|
|
|
22
15
|
@dataclass
|