tunacode-cli 0.0.11__py3-none-any.whl → 0.0.13__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 +3 -4
- tunacode/cli/main.py +5 -4
- tunacode/cli/repl.py +2 -13
- tunacode/cli/textual_app.py +423 -0
- tunacode/cli/textual_bridge.py +158 -0
- tunacode/configuration/defaults.py +3 -4
- tunacode/configuration/models.py +3 -3
- tunacode/configuration/settings.py +2 -2
- tunacode/constants.py +2 -10
- tunacode/core/agents/main.py +5 -3
- tunacode/core/setup/__init__.py +0 -2
- tunacode/core/setup/agent_setup.py +3 -3
- tunacode/core/setup/base.py +3 -3
- tunacode/core/setup/coordinator.py +2 -2
- tunacode/core/setup/environment_setup.py +2 -2
- tunacode/core/setup/git_safety_setup.py +1 -2
- tunacode/core/state.py +3 -3
- tunacode/setup.py +2 -3
- tunacode/tools/__init__.py +1 -0
- tunacode/tools/base.py +4 -43
- tunacode/tools/bash.py +252 -0
- tunacode/tools/read_file.py +2 -2
- tunacode/tools/run_command.py +2 -2
- tunacode/tools/update_file.py +3 -3
- tunacode/tools/write_file.py +3 -3
- tunacode/ui/completers.py +1 -1
- tunacode/ui/console.py +2 -3
- tunacode/ui/constants.py +1 -1
- tunacode/ui/decorators.py +2 -2
- tunacode/ui/input.py +1 -1
- tunacode/ui/keybindings.py +1 -1
- tunacode/ui/output.py +11 -4
- tunacode/ui/panels.py +3 -4
- tunacode/ui/prompt_manager.py +1 -1
- tunacode/ui/validators.py +1 -1
- tunacode/utils/diff_utils.py +3 -3
- {tunacode_cli-0.0.11.dist-info → tunacode_cli-0.0.13.dist-info}/METADATA +94 -40
- tunacode_cli-0.0.13.dist-info/RECORD +66 -0
- tunacode/core/setup/undo_setup.py +0 -33
- tunacode/services/undo_service.py +0 -244
- tunacode_cli-0.0.11.dist-info/RECORD +0 -65
- {tunacode_cli-0.0.11.dist-info → tunacode_cli-0.0.13.dist-info}/WHEEL +0 -0
- {tunacode_cli-0.0.11.dist-info → tunacode_cli-0.0.13.dist-info}/entry_points.txt +0 -0
- {tunacode_cli-0.0.11.dist-info → tunacode_cli-0.0.13.dist-info}/licenses/LICENSE +0 -0
- {tunacode_cli-0.0.11.dist-info → tunacode_cli-0.0.13.dist-info}/top_level.txt +0 -0
tunacode/cli/commands.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Command system for
|
|
1
|
+
"""Command system for TunaCode CLI."""
|
|
2
2
|
|
|
3
3
|
from abc import ABC, abstractmethod
|
|
4
4
|
from dataclasses import dataclass
|
|
@@ -248,14 +248,13 @@ class BranchCommand(SimpleCommand):
|
|
|
248
248
|
|
|
249
249
|
async def execute(self, args: List[str], context: CommandContext) -> None:
|
|
250
250
|
import subprocess
|
|
251
|
-
|
|
252
|
-
from ..services.undo_service import is_in_git_project
|
|
251
|
+
import os
|
|
253
252
|
|
|
254
253
|
if not args:
|
|
255
254
|
await ui.error("Usage: /branch <branch-name>")
|
|
256
255
|
return
|
|
257
256
|
|
|
258
|
-
if not
|
|
257
|
+
if not os.path.exists(".git"):
|
|
259
258
|
await ui.error("Not a git repository")
|
|
260
259
|
return
|
|
261
260
|
|
tunacode/cli/main.py
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Module:
|
|
2
|
+
Module: tunacode.cli.main
|
|
3
3
|
|
|
4
|
-
CLI entry point
|
|
5
|
-
Manages application startup, version checking, and REPL initialization.
|
|
4
|
+
Enhanced CLI entry point with better styling while staying CLI-based.
|
|
6
5
|
"""
|
|
7
6
|
|
|
8
7
|
import asyncio
|
|
@@ -17,7 +16,7 @@ from tunacode.ui import console as ui
|
|
|
17
16
|
from tunacode.utils.system import check_for_updates
|
|
18
17
|
|
|
19
18
|
app_settings = ApplicationSettings()
|
|
20
|
-
app = typer.Typer(help=
|
|
19
|
+
app = typer.Typer(help="🐟 TunaCode - Your AI-powered development assistant")
|
|
21
20
|
state_manager = StateManager()
|
|
22
21
|
|
|
23
22
|
|
|
@@ -29,6 +28,8 @@ def main(
|
|
|
29
28
|
model: str = typer.Option(None, "--model", help="Default model to use (e.g., openai/gpt-4)"),
|
|
30
29
|
key: str = typer.Option(None, "--key", help="API key for the provider"),
|
|
31
30
|
):
|
|
31
|
+
"""🚀 Start TunaCode - Your AI-powered development assistant"""
|
|
32
|
+
|
|
32
33
|
if version:
|
|
33
34
|
asyncio.run(ui.version())
|
|
34
35
|
return
|
tunacode/cli/repl.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Module:
|
|
2
|
+
Module: tunacode.cli.repl
|
|
3
3
|
|
|
4
|
-
Interactive REPL (Read-Eval-Print Loop) implementation for
|
|
4
|
+
Interactive REPL (Read-Eval-Print Loop) implementation for TunaCode.
|
|
5
5
|
Handles user input, command processing, and agent interaction in an interactive shell.
|
|
6
6
|
"""
|
|
7
7
|
|
|
@@ -207,18 +207,7 @@ async def repl(state_manager: StateManager):
|
|
|
207
207
|
action = None
|
|
208
208
|
|
|
209
209
|
# Professional startup information
|
|
210
|
-
await ui.info("TunaCode v0.1 - Beta Release")
|
|
211
|
-
await ui.muted("• Caution: This tool can modify your codebase")
|
|
212
210
|
await ui.muted(f"• Model: {state_manager.session.current_model}")
|
|
213
|
-
await ui.line()
|
|
214
|
-
|
|
215
|
-
# Important safety warning
|
|
216
|
-
await ui.warning("⚠️ IMPORTANT: The /undo command has been removed for safety reasons")
|
|
217
|
-
await ui.muted("• Always use git branches before making major changes")
|
|
218
|
-
await ui.muted("• Use '/branch <name>' to create a new branch for experiments")
|
|
219
|
-
await ui.muted("• Commit your work frequently to preserve changes")
|
|
220
|
-
await ui.line()
|
|
221
|
-
|
|
222
211
|
await ui.success("Ready to assist with your development")
|
|
223
212
|
await ui.line()
|
|
224
213
|
|
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Modern Textual-based TUI for TunaCode.
|
|
3
|
+
|
|
4
|
+
Provides a rich, interactive terminal user interface with:
|
|
5
|
+
- Split-pane layout with chat history and input
|
|
6
|
+
- Sidebar with model info and commands
|
|
7
|
+
- Modern dialog boxes for tool confirmations
|
|
8
|
+
- Real-time status updates
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
from typing import Optional
|
|
13
|
+
|
|
14
|
+
from rich.markdown import Markdown
|
|
15
|
+
from textual import on, work
|
|
16
|
+
from textual.app import App, ComposeResult
|
|
17
|
+
from textual.binding import Binding
|
|
18
|
+
from textual.containers import Container, Horizontal, Vertical, VerticalScroll
|
|
19
|
+
from textual.message import Message
|
|
20
|
+
from textual.reactive import reactive
|
|
21
|
+
from textual.widgets import (
|
|
22
|
+
Button, Footer, Header, Input, Label, Markdown as MarkdownWidget,
|
|
23
|
+
Static, TextArea, TabbedContent, TabPane
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
from tunacode.configuration.settings import ApplicationSettings
|
|
27
|
+
from tunacode.core.state import StateManager
|
|
28
|
+
from tunacode.setup import setup
|
|
29
|
+
from tunacode.utils.system import check_for_updates
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ChatMessage(Static):
|
|
33
|
+
"""A single chat message widget."""
|
|
34
|
+
|
|
35
|
+
def __init__(self, sender: str, content: str, message_type: str = "user"):
|
|
36
|
+
super().__init__()
|
|
37
|
+
self.sender = sender
|
|
38
|
+
self.content = content
|
|
39
|
+
self.message_type = message_type
|
|
40
|
+
|
|
41
|
+
def compose(self) -> ComposeResult:
|
|
42
|
+
"""Compose the chat message."""
|
|
43
|
+
if self.message_type == "user":
|
|
44
|
+
yield Static(f"[bold cyan]❯ You[/bold cyan]\n{self.content}", classes="user-message")
|
|
45
|
+
elif self.message_type == "agent":
|
|
46
|
+
yield Static(f"[bold green]🤖 TunaCode[/bold green]\n{self.content}", classes="agent-message")
|
|
47
|
+
elif self.message_type == "system":
|
|
48
|
+
yield Static(f"[bold yellow]⚠️ System[/bold yellow]\n{self.content}", classes="system-message")
|
|
49
|
+
elif self.message_type == "tool":
|
|
50
|
+
yield Static(f"[bold magenta]🔧 Tool[/bold magenta]\n{self.content}", classes="tool-message")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class Sidebar(Container):
|
|
54
|
+
"""Sidebar with model info and commands."""
|
|
55
|
+
|
|
56
|
+
def __init__(self, state_manager: StateManager):
|
|
57
|
+
super().__init__()
|
|
58
|
+
self.state_manager = state_manager
|
|
59
|
+
|
|
60
|
+
def compose(self) -> ComposeResult:
|
|
61
|
+
"""Compose the sidebar."""
|
|
62
|
+
yield Static("[bold]TunaCode[/bold]", classes="sidebar-title")
|
|
63
|
+
yield Static(f"Model: {self.state_manager.session.current_model}", id="current-model")
|
|
64
|
+
yield Static("", classes="spacer")
|
|
65
|
+
|
|
66
|
+
yield Static("[bold]Commands[/bold]", classes="section-title")
|
|
67
|
+
yield Static("/help - Show help", classes="command-item")
|
|
68
|
+
yield Static("/clear - Clear chat", classes="command-item")
|
|
69
|
+
yield Static("/model - Switch model", classes="command-item")
|
|
70
|
+
yield Static("/yolo - Toggle confirmations", classes="command-item")
|
|
71
|
+
yield Static("", classes="spacer")
|
|
72
|
+
|
|
73
|
+
yield Static("[bold]Status[/bold]", classes="section-title")
|
|
74
|
+
yield Static("● Ready", id="status", classes="status-ready")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class ChatHistory(VerticalScroll):
|
|
78
|
+
"""Scrollable chat history container."""
|
|
79
|
+
|
|
80
|
+
def add_message(self, sender: str, content: str, message_type: str = "user") -> None:
|
|
81
|
+
"""Add a new message to the chat history."""
|
|
82
|
+
message = ChatMessage(sender, content, message_type)
|
|
83
|
+
self.mount(message)
|
|
84
|
+
self.scroll_end(animate=True)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class InputArea(Container):
|
|
88
|
+
"""Input area with text area and send button."""
|
|
89
|
+
|
|
90
|
+
class SendMessage(Message):
|
|
91
|
+
"""Message sent when user submits input."""
|
|
92
|
+
|
|
93
|
+
def __init__(self, content: str) -> None:
|
|
94
|
+
self.content = content
|
|
95
|
+
super().__init__()
|
|
96
|
+
|
|
97
|
+
def compose(self) -> ComposeResult:
|
|
98
|
+
"""Compose the input area."""
|
|
99
|
+
with Horizontal():
|
|
100
|
+
yield TextArea(id="message-input")
|
|
101
|
+
yield Button("Send", id="send-button", variant="primary")
|
|
102
|
+
|
|
103
|
+
@on(Button.Pressed, "#send-button")
|
|
104
|
+
def send_message(self) -> None:
|
|
105
|
+
"""Send the current message."""
|
|
106
|
+
text_area = self.query_one("#message-input", TextArea)
|
|
107
|
+
content = text_area.text.strip()
|
|
108
|
+
if content:
|
|
109
|
+
self.post_message(self.SendMessage(content))
|
|
110
|
+
text_area.clear()
|
|
111
|
+
|
|
112
|
+
def on_key(self, event) -> None:
|
|
113
|
+
"""Handle key events."""
|
|
114
|
+
if event.key == "ctrl+enter":
|
|
115
|
+
self.send_message()
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class TunaCodeApp(App):
|
|
119
|
+
"""Main TunaCode Textual application."""
|
|
120
|
+
|
|
121
|
+
CSS = """
|
|
122
|
+
Sidebar {
|
|
123
|
+
width: 30;
|
|
124
|
+
background: $surface;
|
|
125
|
+
border: thick $primary;
|
|
126
|
+
padding: 1;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.sidebar-title {
|
|
130
|
+
text-align: center;
|
|
131
|
+
color: $primary;
|
|
132
|
+
margin-bottom: 1;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.section-title {
|
|
136
|
+
color: $accent;
|
|
137
|
+
margin: 1 0;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.command-item {
|
|
141
|
+
color: $text-muted;
|
|
142
|
+
margin-left: 1;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.status-ready {
|
|
146
|
+
color: $success;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.status-busy {
|
|
150
|
+
color: $warning;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.status-error {
|
|
154
|
+
color: $error;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
ChatHistory {
|
|
158
|
+
border: thick $primary;
|
|
159
|
+
padding: 1;
|
|
160
|
+
height: 1fr;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.user-message {
|
|
164
|
+
background: $surface;
|
|
165
|
+
border-left: thick $primary;
|
|
166
|
+
padding: 1;
|
|
167
|
+
margin: 1 0;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.agent-message {
|
|
171
|
+
background: $surface;
|
|
172
|
+
border-left: thick $success;
|
|
173
|
+
padding: 1;
|
|
174
|
+
margin: 1 0;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.system-message {
|
|
178
|
+
background: $surface;
|
|
179
|
+
border-left: thick $warning;
|
|
180
|
+
padding: 1;
|
|
181
|
+
margin: 1 0;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.tool-message {
|
|
185
|
+
background: $surface;
|
|
186
|
+
border-left: thick $accent;
|
|
187
|
+
padding: 1;
|
|
188
|
+
margin: 1 0;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
InputArea {
|
|
192
|
+
height: 5;
|
|
193
|
+
padding: 1;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
#message-input {
|
|
197
|
+
height: 3;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
#send-button {
|
|
201
|
+
width: 10;
|
|
202
|
+
margin-left: 1;
|
|
203
|
+
}
|
|
204
|
+
"""
|
|
205
|
+
|
|
206
|
+
BINDINGS = [
|
|
207
|
+
Binding("ctrl+c", "quit", "Quit"),
|
|
208
|
+
Binding("ctrl+l", "clear_chat", "Clear"),
|
|
209
|
+
Binding("f1", "help", "Help"),
|
|
210
|
+
Binding("f2", "model_info", "Model"),
|
|
211
|
+
]
|
|
212
|
+
|
|
213
|
+
def __init__(self, state_manager: StateManager):
|
|
214
|
+
super().__init__()
|
|
215
|
+
self.state_manager = state_manager
|
|
216
|
+
self.chat_history: Optional[ChatHistory] = None
|
|
217
|
+
self.sidebar: Optional[Sidebar] = None
|
|
218
|
+
self.input_area: Optional[InputArea] = None
|
|
219
|
+
|
|
220
|
+
def compose(self) -> ComposeResult:
|
|
221
|
+
"""Compose the main application layout."""
|
|
222
|
+
yield Header()
|
|
223
|
+
|
|
224
|
+
with Horizontal():
|
|
225
|
+
self.sidebar = Sidebar(self.state_manager)
|
|
226
|
+
yield self.sidebar
|
|
227
|
+
|
|
228
|
+
with Vertical():
|
|
229
|
+
self.chat_history = ChatHistory()
|
|
230
|
+
yield self.chat_history
|
|
231
|
+
|
|
232
|
+
self.input_area = InputArea()
|
|
233
|
+
yield self.input_area
|
|
234
|
+
|
|
235
|
+
yield Footer()
|
|
236
|
+
|
|
237
|
+
def on_mount(self) -> None:
|
|
238
|
+
"""Called when the app is mounted."""
|
|
239
|
+
# Add welcome messages
|
|
240
|
+
self.chat_history.add_message(
|
|
241
|
+
"System",
|
|
242
|
+
"Welcome to TunaCode v0.11 - Your AI-powered development assistant",
|
|
243
|
+
"system"
|
|
244
|
+
)
|
|
245
|
+
self.chat_history.add_message(
|
|
246
|
+
"System",
|
|
247
|
+
f"Current model: {self.state_manager.session.current_model}",
|
|
248
|
+
"system"
|
|
249
|
+
)
|
|
250
|
+
self.chat_history.add_message(
|
|
251
|
+
"System",
|
|
252
|
+
"⚠️ IMPORTANT: Always use git branches before making major changes\nType '/help' for available commands",
|
|
253
|
+
"system"
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
@on(InputArea.SendMessage)
|
|
257
|
+
async def handle_message(self, message: InputArea.SendMessage) -> None:
|
|
258
|
+
"""Handle incoming messages from the input area."""
|
|
259
|
+
content = message.content
|
|
260
|
+
|
|
261
|
+
# Add user message to chat
|
|
262
|
+
self.chat_history.add_message("You", content, "user")
|
|
263
|
+
|
|
264
|
+
# Update status
|
|
265
|
+
status_widget = self.sidebar.query_one("#status", Static)
|
|
266
|
+
status_widget.update("● Processing...")
|
|
267
|
+
status_widget.classes = "status-busy"
|
|
268
|
+
|
|
269
|
+
if content.startswith("/"):
|
|
270
|
+
await self.handle_command(content)
|
|
271
|
+
else:
|
|
272
|
+
await self.handle_user_input(content)
|
|
273
|
+
|
|
274
|
+
# Reset status
|
|
275
|
+
status_widget.update("● Ready")
|
|
276
|
+
status_widget.classes = "status-ready"
|
|
277
|
+
|
|
278
|
+
async def handle_command(self, command: str) -> None:
|
|
279
|
+
"""Handle slash commands."""
|
|
280
|
+
if command == "/help":
|
|
281
|
+
help_text = """Available Commands:
|
|
282
|
+
|
|
283
|
+
/help - Show this help message
|
|
284
|
+
/clear - Clear chat history
|
|
285
|
+
/model - Show current model info
|
|
286
|
+
/yolo - Toggle confirmation prompts
|
|
287
|
+
/quit - Exit the application
|
|
288
|
+
|
|
289
|
+
Keyboard Shortcuts:
|
|
290
|
+
Ctrl+C - Quit
|
|
291
|
+
Ctrl+L - Clear chat
|
|
292
|
+
F1 - Help
|
|
293
|
+
F2 - Model info
|
|
294
|
+
Ctrl+Enter - Send message"""
|
|
295
|
+
self.chat_history.add_message("System", help_text, "system")
|
|
296
|
+
|
|
297
|
+
elif command == "/clear":
|
|
298
|
+
await self.action_clear_chat()
|
|
299
|
+
|
|
300
|
+
elif command == "/model":
|
|
301
|
+
model_info = f"Current model: {self.state_manager.session.current_model}"
|
|
302
|
+
self.chat_history.add_message("System", model_info, "system")
|
|
303
|
+
|
|
304
|
+
elif command == "/yolo":
|
|
305
|
+
# Toggle yolo mode
|
|
306
|
+
current_state = getattr(self.state_manager.session, 'yolo_mode', False)
|
|
307
|
+
self.state_manager.session.yolo_mode = not current_state
|
|
308
|
+
new_state = "enabled" if not current_state else "disabled"
|
|
309
|
+
self.chat_history.add_message("System", f"Confirmation prompts {new_state}", "system")
|
|
310
|
+
|
|
311
|
+
elif command == "/quit":
|
|
312
|
+
await self.action_quit()
|
|
313
|
+
|
|
314
|
+
else:
|
|
315
|
+
self.chat_history.add_message("System", f"Unknown command: {command}", "system")
|
|
316
|
+
|
|
317
|
+
async def handle_user_input(self, text: str) -> None:
|
|
318
|
+
"""Handle regular user input."""
|
|
319
|
+
try:
|
|
320
|
+
# Use the bridge to process the input
|
|
321
|
+
if not hasattr(self, '_bridge'):
|
|
322
|
+
from tunacode.cli.textual_bridge import TextualAgentBridge
|
|
323
|
+
self._bridge = TextualAgentBridge(self.state_manager, self._bridge_message_callback)
|
|
324
|
+
|
|
325
|
+
# Process the request
|
|
326
|
+
response = await self._bridge.process_user_input(text)
|
|
327
|
+
|
|
328
|
+
# Add the agent's response to chat
|
|
329
|
+
self.chat_history.add_message("TunaCode", response, "agent")
|
|
330
|
+
|
|
331
|
+
except Exception as e:
|
|
332
|
+
self.chat_history.add_message("System", f"Error: {str(e)}", "system")
|
|
333
|
+
|
|
334
|
+
async def _bridge_message_callback(self, message_type: str, content: str) -> None:
|
|
335
|
+
"""Callback for bridge to send messages to the UI."""
|
|
336
|
+
self.chat_history.add_message("System", content, message_type)
|
|
337
|
+
|
|
338
|
+
def action_clear_chat(self) -> None:
|
|
339
|
+
"""Clear the chat history."""
|
|
340
|
+
self.chat_history.remove_children()
|
|
341
|
+
self.chat_history.add_message("System", "Chat cleared", "system")
|
|
342
|
+
|
|
343
|
+
def action_help(self) -> None:
|
|
344
|
+
"""Show help information."""
|
|
345
|
+
self.handle_command("/help")
|
|
346
|
+
|
|
347
|
+
def action_model_info(self) -> None:
|
|
348
|
+
"""Show model information."""
|
|
349
|
+
self.handle_command("/model")
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
async def run_textual_app(state_manager: StateManager) -> None:
|
|
353
|
+
"""Run the Textual application."""
|
|
354
|
+
app = TunaCodeApp(state_manager)
|
|
355
|
+
await app.run_async()
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def main():
|
|
359
|
+
"""Main entry point for the Textual app."""
|
|
360
|
+
import sys
|
|
361
|
+
|
|
362
|
+
# Handle command line arguments
|
|
363
|
+
version_flag = "--version" in sys.argv or "-v" in sys.argv
|
|
364
|
+
if version_flag:
|
|
365
|
+
from tunacode.constants import APP_VERSION
|
|
366
|
+
print(f"TunaCode v{APP_VERSION}")
|
|
367
|
+
return
|
|
368
|
+
|
|
369
|
+
# Initialize state manager
|
|
370
|
+
state_manager = StateManager()
|
|
371
|
+
app_settings = ApplicationSettings()
|
|
372
|
+
|
|
373
|
+
# Show banner
|
|
374
|
+
print("🐟 TunaCode - Modern AI Development Assistant")
|
|
375
|
+
print("=" * 50)
|
|
376
|
+
|
|
377
|
+
# Check for updates
|
|
378
|
+
has_update, latest_version = check_for_updates()
|
|
379
|
+
if has_update:
|
|
380
|
+
print(f"📦 Update available: v{latest_version}")
|
|
381
|
+
print("Run: pip install --upgrade tunacode-cli")
|
|
382
|
+
print()
|
|
383
|
+
|
|
384
|
+
# Parse CLI arguments for configuration
|
|
385
|
+
cli_config = {}
|
|
386
|
+
args = sys.argv[1:]
|
|
387
|
+
i = 0
|
|
388
|
+
while i < len(args):
|
|
389
|
+
if args[i] == "--model" and i + 1 < len(args):
|
|
390
|
+
cli_config["model"] = args[i + 1]
|
|
391
|
+
i += 2
|
|
392
|
+
elif args[i] == "--key" and i + 1 < len(args):
|
|
393
|
+
cli_config["key"] = args[i + 1]
|
|
394
|
+
i += 2
|
|
395
|
+
elif args[i] == "--baseurl" and i + 1 < len(args):
|
|
396
|
+
cli_config["baseurl"] = args[i + 1]
|
|
397
|
+
i += 2
|
|
398
|
+
elif args[i] == "--setup":
|
|
399
|
+
cli_config["setup"] = True
|
|
400
|
+
i += 1
|
|
401
|
+
else:
|
|
402
|
+
i += 1
|
|
403
|
+
|
|
404
|
+
async def run_app():
|
|
405
|
+
try:
|
|
406
|
+
# Run setup
|
|
407
|
+
run_setup = cli_config.get("setup", False)
|
|
408
|
+
await setup(run_setup, state_manager, cli_config)
|
|
409
|
+
|
|
410
|
+
# Run the Textual app
|
|
411
|
+
await run_textual_app(state_manager)
|
|
412
|
+
|
|
413
|
+
except KeyboardInterrupt:
|
|
414
|
+
print("\n👋 Goodbye!")
|
|
415
|
+
except Exception as e:
|
|
416
|
+
print(f"❌ Error: {e}")
|
|
417
|
+
|
|
418
|
+
# Run the async app
|
|
419
|
+
asyncio.run(run_app())
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
if __name__ == "__main__":
|
|
423
|
+
main()
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Bridge module to integrate existing TunaCode agent logic with Textual UI.
|
|
3
|
+
|
|
4
|
+
This module adapts the existing REPL and agent processing logic to work
|
|
5
|
+
with the new Textual-based interface while maintaining compatibility.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
from typing import Callable, Optional
|
|
10
|
+
from asyncio.exceptions import CancelledError
|
|
11
|
+
|
|
12
|
+
from pydantic_ai.exceptions import UnexpectedModelBehavior
|
|
13
|
+
|
|
14
|
+
from tunacode.core.agents import main as agent
|
|
15
|
+
from tunacode.core.agents.main import patch_tool_messages
|
|
16
|
+
from tunacode.core.tool_handler import ToolHandler
|
|
17
|
+
from tunacode.exceptions import AgentError, UserAbortError, ValidationError
|
|
18
|
+
from tunacode.types import StateManager
|
|
19
|
+
from tunacode.cli.commands import CommandRegistry
|
|
20
|
+
from tunacode.cli.repl import _parse_args
|
|
21
|
+
from tunacode.ui.tool_ui import ToolUI
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class TextualAgentBridge:
|
|
25
|
+
"""Bridge between Textual UI and existing agent logic."""
|
|
26
|
+
|
|
27
|
+
def __init__(self, state_manager: StateManager, message_callback: Callable):
|
|
28
|
+
self.state_manager = state_manager
|
|
29
|
+
self.message_callback = message_callback
|
|
30
|
+
self.tool_ui = ToolUI()
|
|
31
|
+
self.command_registry = CommandRegistry()
|
|
32
|
+
self.command_registry.register_all_default_commands()
|
|
33
|
+
|
|
34
|
+
async def process_user_input(self, text: str) -> str:
|
|
35
|
+
"""Process user input and return the agent's response."""
|
|
36
|
+
if text.startswith("/"):
|
|
37
|
+
return await self._handle_command(text)
|
|
38
|
+
else:
|
|
39
|
+
return await self._process_agent_request(text)
|
|
40
|
+
|
|
41
|
+
async def _handle_command(self, command: str) -> str:
|
|
42
|
+
"""Handle slash commands."""
|
|
43
|
+
try:
|
|
44
|
+
from tunacode.types import CommandContext
|
|
45
|
+
|
|
46
|
+
# Create command context
|
|
47
|
+
context = CommandContext(
|
|
48
|
+
state_manager=self.state_manager,
|
|
49
|
+
process_request=self._process_agent_request
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
# Set the process_request callback for commands that need it
|
|
53
|
+
self.command_registry.set_process_request_callback(self._process_agent_request)
|
|
54
|
+
|
|
55
|
+
# Execute the command
|
|
56
|
+
result = await self.command_registry.execute(command, context)
|
|
57
|
+
|
|
58
|
+
if result == "restart":
|
|
59
|
+
return "Application restart requested."
|
|
60
|
+
elif result:
|
|
61
|
+
return str(result)
|
|
62
|
+
else:
|
|
63
|
+
return f"Command '{command}' executed successfully."
|
|
64
|
+
|
|
65
|
+
except ValidationError as e:
|
|
66
|
+
return f"Error: {str(e)}"
|
|
67
|
+
except Exception as e:
|
|
68
|
+
return f"Command error: {str(e)}"
|
|
69
|
+
|
|
70
|
+
async def _process_agent_request(self, text: str) -> str:
|
|
71
|
+
"""Process input using the agent."""
|
|
72
|
+
try:
|
|
73
|
+
# Expand @file references before sending to the agent
|
|
74
|
+
try:
|
|
75
|
+
from tunacode.utils.text_utils import expand_file_refs
|
|
76
|
+
text = expand_file_refs(text)
|
|
77
|
+
except ValueError as e:
|
|
78
|
+
return f"Error: {str(e)}"
|
|
79
|
+
|
|
80
|
+
# Notify UI about processing start
|
|
81
|
+
await self.message_callback("tool", "Processing request...")
|
|
82
|
+
|
|
83
|
+
# Create a tool callback that integrates with Textual
|
|
84
|
+
def tool_callback_with_state(part, node):
|
|
85
|
+
return self._tool_handler(part, node)
|
|
86
|
+
|
|
87
|
+
# Get or create agent instance
|
|
88
|
+
instance = agent.get_or_create_agent(
|
|
89
|
+
self.state_manager.session.current_model,
|
|
90
|
+
self.state_manager
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# Process the request
|
|
94
|
+
async with instance.run_mcp_servers():
|
|
95
|
+
res = await agent.process_request(
|
|
96
|
+
self.state_manager.session.current_model,
|
|
97
|
+
text,
|
|
98
|
+
self.state_manager,
|
|
99
|
+
tool_callback=tool_callback_with_state,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
if res and res.result:
|
|
103
|
+
return res.result.output
|
|
104
|
+
else:
|
|
105
|
+
return "Request processed (no output)."
|
|
106
|
+
|
|
107
|
+
except CancelledError:
|
|
108
|
+
return "Request cancelled."
|
|
109
|
+
except UserAbortError:
|
|
110
|
+
return "Operation aborted."
|
|
111
|
+
except UnexpectedModelBehavior as e:
|
|
112
|
+
error_message = str(e)
|
|
113
|
+
patch_tool_messages(error_message, self.state_manager)
|
|
114
|
+
return f"Model error: {error_message}"
|
|
115
|
+
except Exception as e:
|
|
116
|
+
# Wrap unexpected exceptions in AgentError for better tracking
|
|
117
|
+
agent_error = AgentError(f"Agent processing failed: {str(e)}")
|
|
118
|
+
agent_error.__cause__ = e
|
|
119
|
+
return f"Error: {str(e)}"
|
|
120
|
+
|
|
121
|
+
async def _tool_handler(self, part, node):
|
|
122
|
+
"""Handle tool execution with Textual UI integration."""
|
|
123
|
+
await self.message_callback("tool", f"Tool: {part.tool_name}")
|
|
124
|
+
|
|
125
|
+
try:
|
|
126
|
+
# Create tool handler with state
|
|
127
|
+
tool_handler = ToolHandler(self.state_manager)
|
|
128
|
+
args = _parse_args(part.args)
|
|
129
|
+
|
|
130
|
+
# Check if confirmation is needed
|
|
131
|
+
if tool_handler.should_confirm(part.tool_name):
|
|
132
|
+
# Create confirmation request
|
|
133
|
+
request = tool_handler.create_confirmation_request(part.tool_name, args)
|
|
134
|
+
|
|
135
|
+
# For now, show a simple confirmation in the UI
|
|
136
|
+
# In a full implementation, this would show a proper modal dialog
|
|
137
|
+
await self.message_callback("system", f"Tool confirmation: {part.tool_name} with args: {args}")
|
|
138
|
+
|
|
139
|
+
# For demo purposes, auto-approve (in production, this should be interactive)
|
|
140
|
+
if not tool_handler.process_confirmation(True, part.tool_name):
|
|
141
|
+
raise UserAbortError("User aborted tool execution.")
|
|
142
|
+
|
|
143
|
+
except UserAbortError:
|
|
144
|
+
patch_tool_messages("Operation aborted by user.", self.state_manager)
|
|
145
|
+
raise
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class TextualToolConfirmation:
|
|
149
|
+
"""Handle tool confirmations in Textual UI."""
|
|
150
|
+
|
|
151
|
+
def __init__(self, app_instance):
|
|
152
|
+
self.app = app_instance
|
|
153
|
+
|
|
154
|
+
async def show_confirmation(self, tool_name: str, args: dict) -> bool:
|
|
155
|
+
"""Show tool confirmation dialog and return user's choice."""
|
|
156
|
+
# This would show a modal dialog in the Textual app
|
|
157
|
+
# For now, return True (auto-approve)
|
|
158
|
+
return True
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Module:
|
|
2
|
+
Module: tunacode.configuration.defaults
|
|
3
3
|
|
|
4
|
-
Default configuration values for the
|
|
5
|
-
Provides
|
|
6
|
-
tool settings, and MCP servers.
|
|
4
|
+
Default configuration values for the TunaCode CLI.
|
|
5
|
+
Provides sensible defaults for user configuration and environment variables.
|
|
7
6
|
"""
|
|
8
7
|
|
|
9
8
|
from tunacode.constants import GUIDE_FILE_NAME, TOOL_READ_FILE
|
tunacode/configuration/models.py
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Module:
|
|
2
|
+
Module: tunacode.configuration.models
|
|
3
3
|
|
|
4
|
-
Configuration
|
|
5
|
-
|
|
4
|
+
Configuration data models and model registry for TunaCode CLI.
|
|
5
|
+
Defines available AI models and their configurations.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
from tunacode.types import ModelConfig, ModelName, ModelPricing
|