tzamuncode 0.1.0__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.
- tzamuncode/__init__.py +10 -0
- tzamuncode/agents/__init__.py +1 -0
- tzamuncode/agents/coder.py +144 -0
- tzamuncode/agents/tools.py +159 -0
- tzamuncode/auth/__init__.py +1 -0
- tzamuncode/auth/auth_manager.py +159 -0
- tzamuncode/cli/__init__.py +1 -0
- tzamuncode/cli/agentic_commands.py +131 -0
- tzamuncode/cli/auth_commands.py +125 -0
- tzamuncode/cli/commands.py +203 -0
- tzamuncode/cli/enhanced_chat.py +312 -0
- tzamuncode/cli/interactive_chat.py +323 -0
- tzamuncode/cli/main.py +444 -0
- tzamuncode/cli/realtime_chat.py +965 -0
- tzamuncode/cli/realtime_chat_methods.py +200 -0
- tzamuncode/cli/tui_chat.py +323 -0
- tzamuncode/config/__init__.py +1 -0
- tzamuncode/models/__init__.py +1 -0
- tzamuncode/models/ollama.py +124 -0
- tzamuncode/models/vllm_client.py +121 -0
- tzamuncode/utils/__init__.py +1 -0
- tzamuncode/utils/file_ops.py +59 -0
- tzamuncode/utils/project_scanner.py +193 -0
- tzamuncode-0.1.0.dist-info/METADATA +200 -0
- tzamuncode-0.1.0.dist-info/RECORD +29 -0
- tzamuncode-0.1.0.dist-info/WHEEL +5 -0
- tzamuncode-0.1.0.dist-info/entry_points.txt +3 -0
- tzamuncode-0.1.0.dist-info/licenses/LICENSE +21 -0
- tzamuncode-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,965 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Real-time chat with keystroke interception (Claude Code approach)
|
|
3
|
+
Uses raw terminal mode + ANSI cursor manipulation
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import sys
|
|
7
|
+
import tty
|
|
8
|
+
import termios
|
|
9
|
+
from typing import Optional, List
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
from rich.panel import Panel
|
|
12
|
+
from rich.table import Table
|
|
13
|
+
from rich.live import Live
|
|
14
|
+
from rich.text import Text
|
|
15
|
+
|
|
16
|
+
from ..models.ollama import OllamaClient
|
|
17
|
+
from ..models.vllm_client import VLLMClient
|
|
18
|
+
from ..utils.file_ops import FileManager
|
|
19
|
+
from ..utils.project_scanner import ProjectScanner
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
import subprocess
|
|
22
|
+
from rich.prompt import Confirm
|
|
23
|
+
|
|
24
|
+
console = Console()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class RealtimeChat:
|
|
28
|
+
"""Chat with real-time keystroke detection like Claude Code"""
|
|
29
|
+
|
|
30
|
+
COMMANDS = {
|
|
31
|
+
'/help': 'Show available commands',
|
|
32
|
+
'/models': 'List and switch models',
|
|
33
|
+
'/backend': 'Switch between vLLM/Ollama',
|
|
34
|
+
'/settings': 'Show settings',
|
|
35
|
+
'/files': 'List files in workspace',
|
|
36
|
+
'/read': 'Read a file (e.g., /read app.py)',
|
|
37
|
+
'/write': 'Write to a file (e.g., /write test.py)',
|
|
38
|
+
'/project': 'Show project structure',
|
|
39
|
+
'/history': 'Show conversation history',
|
|
40
|
+
'/save': 'Save conversation to file',
|
|
41
|
+
'/load': 'Load previous conversation',
|
|
42
|
+
'/clear': 'Clear conversation',
|
|
43
|
+
'/exit': 'Exit chat',
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
def __init__(self, model: str = "qwen2.5:32b", use_vllm: bool = False):
|
|
47
|
+
self.model = model
|
|
48
|
+
self.use_vllm = use_vllm
|
|
49
|
+
|
|
50
|
+
# Initialize appropriate client
|
|
51
|
+
if use_vllm:
|
|
52
|
+
self.client = VLLMClient(model=model)
|
|
53
|
+
self.backend = "vLLM"
|
|
54
|
+
else:
|
|
55
|
+
self.client = OllamaClient(model=model)
|
|
56
|
+
self.backend = "Ollama"
|
|
57
|
+
|
|
58
|
+
self.conversation_history = []
|
|
59
|
+
self.current_input = ""
|
|
60
|
+
self.show_menu = False
|
|
61
|
+
self.selected_index = 0
|
|
62
|
+
self.command_history = [] # Store previous commands
|
|
63
|
+
self.history_index = -1 # Current position in history
|
|
64
|
+
self.multi_line_mode = False # Multi-line input mode
|
|
65
|
+
self.multi_line_buffer = [] # Buffer for multi-line input
|
|
66
|
+
|
|
67
|
+
# Code skills - file operations and project scanning
|
|
68
|
+
self.file_manager = FileManager()
|
|
69
|
+
self.project_scanner = ProjectScanner(Path.cwd())
|
|
70
|
+
self.workspace = Path.cwd()
|
|
71
|
+
|
|
72
|
+
# Load models
|
|
73
|
+
try:
|
|
74
|
+
self.available_models = self.client.list_models()
|
|
75
|
+
except:
|
|
76
|
+
self.available_models = [model]
|
|
77
|
+
|
|
78
|
+
# Add system prompt for code skills
|
|
79
|
+
self.system_prompt = """You are TzamunCode, an AI coding assistant with autonomous capabilities:
|
|
80
|
+
|
|
81
|
+
**File Operations:**
|
|
82
|
+
- You can read files when asked
|
|
83
|
+
- You can write/create files when asked (show the code first, then confirm)
|
|
84
|
+
- You can list files in directories
|
|
85
|
+
- You can analyze project structure
|
|
86
|
+
|
|
87
|
+
**Autonomous Command Execution:**
|
|
88
|
+
- You can suggest and execute terminal commands when needed
|
|
89
|
+
- To execute a command, use this format: [EXECUTE: command here]
|
|
90
|
+
- Example: "Let me check the files. [EXECUTE: ls -la]"
|
|
91
|
+
- User will confirm before execution
|
|
92
|
+
- You can see command output and continue helping
|
|
93
|
+
|
|
94
|
+
**Code Skills:**
|
|
95
|
+
- Explain code and concepts
|
|
96
|
+
- Generate code snippets
|
|
97
|
+
- Debug and fix issues
|
|
98
|
+
- Suggest improvements
|
|
99
|
+
- Answer coding questions
|
|
100
|
+
|
|
101
|
+
**Important:**
|
|
102
|
+
- When you need to run a command, use [EXECUTE: command] format
|
|
103
|
+
- Always explain what the command does before suggesting it
|
|
104
|
+
- Be proactive - suggest commands when they would help
|
|
105
|
+
- You can chain multiple commands if needed
|
|
106
|
+
|
|
107
|
+
Current workspace: {workspace}
|
|
108
|
+
""".format(workspace=str(self.workspace))
|
|
109
|
+
|
|
110
|
+
def show_header(self):
|
|
111
|
+
"""Show professional header"""
|
|
112
|
+
from .. import __version__
|
|
113
|
+
from ..auth.auth_manager import AuthManager
|
|
114
|
+
|
|
115
|
+
# Get user info
|
|
116
|
+
auth = AuthManager()
|
|
117
|
+
user_info = auth.get_user_info()
|
|
118
|
+
|
|
119
|
+
console.print()
|
|
120
|
+
console.print("─" * console.width, style="dim")
|
|
121
|
+
console.print()
|
|
122
|
+
console.print("[bold bright_cyan]████████╗███████╗ █████╗ ███╗ ███╗██╗ ██╗███╗ ██╗ ██████╗ ██████╗ ██████╗ ███████╗[/bold bright_cyan]")
|
|
123
|
+
console.print("[bold bright_cyan]╚══██╔══╝╚══███╔╝██╔══██╗████╗ ████║██║ ██║████╗ ██║ ██╔════╝██╔═══██╗██╔══██╗██╔════╝[/bold bright_cyan]")
|
|
124
|
+
console.print("[bold cyan] ██║ ███╔╝ ███████║██╔████╔██║██║ ██║██╔██╗ ██║ ██║ ██║ ██║██║ ██║█████╗ [/bold cyan]")
|
|
125
|
+
console.print("[bold cyan] ██║ ███╔╝ ██╔══██║██║╚██╔╝██║██║ ██║██║╚██╗██║ ██║ ██║ ██║██║ ██║██╔══╝ [/bold cyan]")
|
|
126
|
+
console.print("[bold blue] ██║ ███████╗██║ ██║██║ ╚═╝ ██║╚██████╔╝██║ ╚████║ ╚██████╗╚██████╔╝██████╔╝███████╗[/bold blue]")
|
|
127
|
+
console.print("[bold blue] ╚═╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝[/bold blue]")
|
|
128
|
+
console.print()
|
|
129
|
+
console.print("[bold white] ╔════════════════════════════════════╗[/bold white]")
|
|
130
|
+
console.print("[bold white] ║ [bright_cyan]A I[/bright_cyan] [dim]•[/dim] [cyan]A S S I S T A N T[/cyan] ║[/bold white]")
|
|
131
|
+
console.print("[bold white] ╚════════════════════════════════════╝[/bold white]")
|
|
132
|
+
console.print()
|
|
133
|
+
|
|
134
|
+
# Build info panel with user details
|
|
135
|
+
info_text = f"""[bold]AI Coding Assistant[/bold]
|
|
136
|
+
|
|
137
|
+
[cyan]Version:[/cyan] {__version__}
|
|
138
|
+
[cyan]Company:[/cyan] Tzamun Arabia IT Co. 🇸🇦
|
|
139
|
+
[cyan]User:[/cyan] {user_info.get('username', 'Guest')} [green]✓[/green]
|
|
140
|
+
[cyan]Backend:[/cyan] {self.backend}
|
|
141
|
+
[cyan]Model:[/cyan] {self.model}
|
|
142
|
+
[cyan]Models Available:[/cyan] {len(self.available_models)}
|
|
143
|
+
[cyan]Workspace:[/cyan] [yellow]{self.workspace}[/yellow]
|
|
144
|
+
|
|
145
|
+
[dim]Type '/help' for commands • Type '?' for shortcuts[/dim]"""
|
|
146
|
+
|
|
147
|
+
info_panel = Panel.fit(
|
|
148
|
+
info_text,
|
|
149
|
+
border_style="blue",
|
|
150
|
+
padding=(0, 2)
|
|
151
|
+
)
|
|
152
|
+
console.print(info_panel)
|
|
153
|
+
console.print()
|
|
154
|
+
console.print("─" * console.width, style="dim")
|
|
155
|
+
console.print()
|
|
156
|
+
|
|
157
|
+
def render_menu(self, filter_text: str = ""):
|
|
158
|
+
"""Render command menu below cursor (Claude Code style)"""
|
|
159
|
+
# Filter commands based on input
|
|
160
|
+
filtered_commands = {}
|
|
161
|
+
if filter_text:
|
|
162
|
+
for cmd, desc in self.COMMANDS.items():
|
|
163
|
+
if filter_text.lower() in cmd.lower():
|
|
164
|
+
filtered_commands[cmd] = desc
|
|
165
|
+
else:
|
|
166
|
+
filtered_commands = self.COMMANDS
|
|
167
|
+
|
|
168
|
+
if not filtered_commands:
|
|
169
|
+
return
|
|
170
|
+
|
|
171
|
+
# Save cursor position
|
|
172
|
+
sys.stdout.write('\x1B[s')
|
|
173
|
+
sys.stdout.flush()
|
|
174
|
+
|
|
175
|
+
# Move to next line
|
|
176
|
+
console.print()
|
|
177
|
+
|
|
178
|
+
# Render menu
|
|
179
|
+
table = Table(
|
|
180
|
+
title="[bold cyan]Available Commands[/bold cyan]",
|
|
181
|
+
border_style="blue",
|
|
182
|
+
show_header=False,
|
|
183
|
+
expand=False,
|
|
184
|
+
width=60
|
|
185
|
+
)
|
|
186
|
+
table.add_column("Command", style="bold cyan", width=15)
|
|
187
|
+
table.add_column("Description", style="white", width=43)
|
|
188
|
+
|
|
189
|
+
for idx, (cmd, desc) in enumerate(filtered_commands.items()):
|
|
190
|
+
style = "bold green on blue" if idx == self.selected_index else ""
|
|
191
|
+
table.add_row(cmd, desc, style=style)
|
|
192
|
+
|
|
193
|
+
console.print(table)
|
|
194
|
+
|
|
195
|
+
# Restore cursor position
|
|
196
|
+
sys.stdout.write('\x1B[u')
|
|
197
|
+
sys.stdout.flush()
|
|
198
|
+
|
|
199
|
+
def clear_menu(self):
|
|
200
|
+
"""Clear the menu area"""
|
|
201
|
+
# Save current position
|
|
202
|
+
sys.stdout.write('\x1B[s')
|
|
203
|
+
|
|
204
|
+
# Move down and clear multiple lines (enough for menu)
|
|
205
|
+
for i in range(20):
|
|
206
|
+
sys.stdout.write('\x1B[1B') # Move down
|
|
207
|
+
sys.stdout.write('\x1B[2K') # Clear line
|
|
208
|
+
|
|
209
|
+
# Restore position
|
|
210
|
+
sys.stdout.write('\x1B[u')
|
|
211
|
+
sys.stdout.flush()
|
|
212
|
+
|
|
213
|
+
def get_char(self):
|
|
214
|
+
"""Get single character from terminal (raw mode)"""
|
|
215
|
+
fd = sys.stdin.fileno()
|
|
216
|
+
old_settings = termios.tcgetattr(fd)
|
|
217
|
+
try:
|
|
218
|
+
tty.setraw(fd)
|
|
219
|
+
char = sys.stdin.read(1)
|
|
220
|
+
finally:
|
|
221
|
+
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
|
|
222
|
+
return char
|
|
223
|
+
|
|
224
|
+
def read_input(self) -> Optional[str]:
|
|
225
|
+
"""Read input with real-time keystroke detection"""
|
|
226
|
+
self.current_input = ""
|
|
227
|
+
self.show_menu = False
|
|
228
|
+
self.history_index = -1
|
|
229
|
+
|
|
230
|
+
prompt = "\n[dim]›[/dim] " if not self.multi_line_mode else "[dim]…[/dim] "
|
|
231
|
+
console.print(prompt, end="")
|
|
232
|
+
|
|
233
|
+
while True:
|
|
234
|
+
char = self.get_char()
|
|
235
|
+
|
|
236
|
+
# Handle special keys
|
|
237
|
+
if char == '\x03': # Ctrl+C
|
|
238
|
+
raise KeyboardInterrupt
|
|
239
|
+
elif char == '\x04': # Ctrl+D - Quick exit
|
|
240
|
+
console.print("\n[dim]Goodbye! 👋[/dim]\n")
|
|
241
|
+
raise KeyboardInterrupt
|
|
242
|
+
elif char == '\x0c': # Ctrl+L - Clear screen
|
|
243
|
+
console.clear()
|
|
244
|
+
self.show_header()
|
|
245
|
+
console.print(prompt, end="")
|
|
246
|
+
sys.stdout.write(self.current_input)
|
|
247
|
+
sys.stdout.flush()
|
|
248
|
+
continue
|
|
249
|
+
elif char == '\x12': # Ctrl+R - Regenerate (placeholder for now)
|
|
250
|
+
continue
|
|
251
|
+
elif char == '\x1b': # Escape or arrow key
|
|
252
|
+
next1 = self.get_char()
|
|
253
|
+
if next1 == '[':
|
|
254
|
+
next2 = self.get_char()
|
|
255
|
+
if next2 == 'A': # Up arrow
|
|
256
|
+
if self.show_menu:
|
|
257
|
+
self.selected_index = max(0, self.selected_index - 1)
|
|
258
|
+
self.clear_menu()
|
|
259
|
+
self.render_menu()
|
|
260
|
+
elif self.command_history and not self.show_menu:
|
|
261
|
+
# Navigate command history
|
|
262
|
+
if self.history_index < len(self.command_history) - 1:
|
|
263
|
+
self.history_index += 1
|
|
264
|
+
# Clear current input
|
|
265
|
+
sys.stdout.write('\b \b' * len(self.current_input))
|
|
266
|
+
sys.stdout.flush()
|
|
267
|
+
# Show history item
|
|
268
|
+
self.current_input = self.command_history[-(self.history_index + 1)]
|
|
269
|
+
sys.stdout.write(self.current_input)
|
|
270
|
+
sys.stdout.flush()
|
|
271
|
+
elif next2 == 'B': # Down arrow
|
|
272
|
+
if self.show_menu:
|
|
273
|
+
self.selected_index = min(len(self.COMMANDS) - 1, self.selected_index + 1)
|
|
274
|
+
self.clear_menu()
|
|
275
|
+
self.render_menu()
|
|
276
|
+
elif self.history_index > -1:
|
|
277
|
+
# Navigate command history
|
|
278
|
+
self.history_index -= 1
|
|
279
|
+
# Clear current input
|
|
280
|
+
sys.stdout.write('\b \b' * len(self.current_input))
|
|
281
|
+
sys.stdout.flush()
|
|
282
|
+
if self.history_index >= 0:
|
|
283
|
+
self.current_input = self.command_history[-(self.history_index + 1)]
|
|
284
|
+
else:
|
|
285
|
+
self.current_input = ""
|
|
286
|
+
sys.stdout.write(self.current_input)
|
|
287
|
+
sys.stdout.flush()
|
|
288
|
+
else:
|
|
289
|
+
# Escape key - hide menu
|
|
290
|
+
if self.show_menu:
|
|
291
|
+
self.clear_menu()
|
|
292
|
+
self.show_menu = False
|
|
293
|
+
continue
|
|
294
|
+
elif char == '\r' or char == '\n': # Enter
|
|
295
|
+
if self.show_menu:
|
|
296
|
+
# Select command from menu
|
|
297
|
+
selected_cmd = list(self.COMMANDS.keys())[self.selected_index]
|
|
298
|
+
self.clear_menu()
|
|
299
|
+
self.show_menu = False
|
|
300
|
+
console.print()
|
|
301
|
+
return selected_cmd
|
|
302
|
+
else:
|
|
303
|
+
console.print()
|
|
304
|
+
# Add to command history if not empty
|
|
305
|
+
if self.current_input.strip() and not self.current_input.startswith('/'):
|
|
306
|
+
self.command_history.append(self.current_input)
|
|
307
|
+
# Keep history limited to 100 items
|
|
308
|
+
if len(self.command_history) > 100:
|
|
309
|
+
self.command_history.pop(0)
|
|
310
|
+
return self.current_input
|
|
311
|
+
elif char == '\x7f': # Backspace
|
|
312
|
+
if self.current_input:
|
|
313
|
+
self.current_input = self.current_input[:-1]
|
|
314
|
+
sys.stdout.write('\b \b')
|
|
315
|
+
sys.stdout.flush()
|
|
316
|
+
|
|
317
|
+
# Update menu filter or hide if input is cleared
|
|
318
|
+
if not self.current_input and self.show_menu:
|
|
319
|
+
self.clear_menu()
|
|
320
|
+
self.show_menu = False
|
|
321
|
+
elif self.show_menu and self.current_input.startswith('/'):
|
|
322
|
+
# Update filtered menu
|
|
323
|
+
filter_text = self.current_input[1:]
|
|
324
|
+
self.clear_menu()
|
|
325
|
+
self.render_menu(filter_text)
|
|
326
|
+
else:
|
|
327
|
+
# Regular character
|
|
328
|
+
self.current_input += char
|
|
329
|
+
sys.stdout.write(char)
|
|
330
|
+
sys.stdout.flush()
|
|
331
|
+
|
|
332
|
+
# Check for '?' shortcut trigger
|
|
333
|
+
if char == '?' and len(self.current_input) == 1:
|
|
334
|
+
console.print()
|
|
335
|
+
self.show_help()
|
|
336
|
+
self.current_input = ""
|
|
337
|
+
console.print("\n[dim]›[/dim] ", end="")
|
|
338
|
+
sys.stdout.flush()
|
|
339
|
+
continue
|
|
340
|
+
|
|
341
|
+
# Check for '/' trigger
|
|
342
|
+
if char == '/' and len(self.current_input) == 1:
|
|
343
|
+
self.show_menu = True
|
|
344
|
+
self.selected_index = 0
|
|
345
|
+
self.render_menu()
|
|
346
|
+
elif self.show_menu and self.current_input.startswith('/'):
|
|
347
|
+
# Update filtered menu as user types
|
|
348
|
+
filter_text = self.current_input[1:] # Remove '/' prefix
|
|
349
|
+
self.clear_menu()
|
|
350
|
+
self.render_menu(filter_text)
|
|
351
|
+
|
|
352
|
+
def run(self):
|
|
353
|
+
"""Run the chat"""
|
|
354
|
+
self.show_header()
|
|
355
|
+
|
|
356
|
+
# Add system prompt
|
|
357
|
+
self.conversation_history.append({
|
|
358
|
+
"role": "system",
|
|
359
|
+
"content": self.system_prompt
|
|
360
|
+
})
|
|
361
|
+
|
|
362
|
+
while True:
|
|
363
|
+
try:
|
|
364
|
+
user_input = self.read_input()
|
|
365
|
+
|
|
366
|
+
if not user_input or not user_input.strip():
|
|
367
|
+
continue
|
|
368
|
+
|
|
369
|
+
# Handle commands
|
|
370
|
+
if user_input.lower() in ['exit', 'quit', '/exit']:
|
|
371
|
+
console.print("\n[dim]Goodbye! 👋[/dim]\n")
|
|
372
|
+
break
|
|
373
|
+
|
|
374
|
+
if user_input == '/help':
|
|
375
|
+
self.show_help()
|
|
376
|
+
continue
|
|
377
|
+
|
|
378
|
+
if user_input == '/models':
|
|
379
|
+
self.show_models()
|
|
380
|
+
continue
|
|
381
|
+
|
|
382
|
+
if user_input == '/settings':
|
|
383
|
+
self.show_settings()
|
|
384
|
+
continue
|
|
385
|
+
|
|
386
|
+
if user_input == '/clear':
|
|
387
|
+
console.clear()
|
|
388
|
+
self.show_header()
|
|
389
|
+
self.conversation_history = [{"role": "system", "content": self.system_prompt}]
|
|
390
|
+
continue
|
|
391
|
+
|
|
392
|
+
if user_input == '/files':
|
|
393
|
+
self.show_files()
|
|
394
|
+
continue
|
|
395
|
+
|
|
396
|
+
if user_input == '/project':
|
|
397
|
+
self.show_project_info()
|
|
398
|
+
continue
|
|
399
|
+
|
|
400
|
+
if user_input.startswith('/read '):
|
|
401
|
+
file_path = user_input[6:].strip()
|
|
402
|
+
self.read_file(file_path)
|
|
403
|
+
continue
|
|
404
|
+
|
|
405
|
+
if user_input.startswith('/write '):
|
|
406
|
+
file_path = user_input[7:].strip()
|
|
407
|
+
self.write_file(file_path)
|
|
408
|
+
continue
|
|
409
|
+
|
|
410
|
+
if user_input == '/backend':
|
|
411
|
+
self.switch_backend()
|
|
412
|
+
continue
|
|
413
|
+
|
|
414
|
+
if user_input == '/history':
|
|
415
|
+
self.show_conversation_history()
|
|
416
|
+
continue
|
|
417
|
+
|
|
418
|
+
if user_input.startswith('/save'):
|
|
419
|
+
filename = user_input[5:].strip() or 'conversation.json'
|
|
420
|
+
self.save_conversation(filename)
|
|
421
|
+
continue
|
|
422
|
+
|
|
423
|
+
if user_input.startswith('/load '):
|
|
424
|
+
filename = user_input[6:].strip()
|
|
425
|
+
self.load_conversation(filename)
|
|
426
|
+
continue
|
|
427
|
+
|
|
428
|
+
if user_input == '?':
|
|
429
|
+
self.show_help()
|
|
430
|
+
continue
|
|
431
|
+
|
|
432
|
+
if user_input.startswith('/run '):
|
|
433
|
+
command = user_input[5:].strip()
|
|
434
|
+
self.execute_command(command)
|
|
435
|
+
continue
|
|
436
|
+
|
|
437
|
+
# Send to AI
|
|
438
|
+
self.send_message(user_input)
|
|
439
|
+
|
|
440
|
+
except KeyboardInterrupt:
|
|
441
|
+
console.print("\n[dim]Goodbye! 👋[/dim]\n")
|
|
442
|
+
break
|
|
443
|
+
except Exception as e:
|
|
444
|
+
console.print(f"\n[red]Error: {e}[/red]\n")
|
|
445
|
+
|
|
446
|
+
def show_help(self):
|
|
447
|
+
"""Show help with all commands and shortcuts"""
|
|
448
|
+
console.print()
|
|
449
|
+
|
|
450
|
+
# Commands table
|
|
451
|
+
table = Table(
|
|
452
|
+
title="[bold cyan]Available Commands[/bold cyan]",
|
|
453
|
+
border_style="blue",
|
|
454
|
+
show_header=True,
|
|
455
|
+
header_style="bold white on blue"
|
|
456
|
+
)
|
|
457
|
+
table.add_column("Command", style="bold cyan", width=20)
|
|
458
|
+
table.add_column("Description", style="white")
|
|
459
|
+
|
|
460
|
+
for cmd, desc in self.COMMANDS.items():
|
|
461
|
+
table.add_row(cmd, desc)
|
|
462
|
+
|
|
463
|
+
console.print(table)
|
|
464
|
+
|
|
465
|
+
# Shortcuts table
|
|
466
|
+
console.print()
|
|
467
|
+
shortcuts_table = Table(
|
|
468
|
+
title="[bold cyan]Keyboard Shortcuts[/bold cyan]",
|
|
469
|
+
border_style="blue",
|
|
470
|
+
show_header=True,
|
|
471
|
+
header_style="bold white on blue"
|
|
472
|
+
)
|
|
473
|
+
shortcuts_table.add_column("Shortcut", style="bold yellow", width=15)
|
|
474
|
+
shortcuts_table.add_column("Action", style="white")
|
|
475
|
+
|
|
476
|
+
shortcuts_table.add_row("?", "Show this help")
|
|
477
|
+
shortcuts_table.add_row("↑ / ↓", "Navigate command history")
|
|
478
|
+
shortcuts_table.add_row("Ctrl+L", "Clear screen")
|
|
479
|
+
shortcuts_table.add_row("Ctrl+D", "Quick exit")
|
|
480
|
+
shortcuts_table.add_row("Ctrl+C", "Cancel/Exit")
|
|
481
|
+
shortcuts_table.add_row("/", "Show command menu")
|
|
482
|
+
|
|
483
|
+
console.print(shortcuts_table)
|
|
484
|
+
console.print()
|
|
485
|
+
|
|
486
|
+
def show_models(self):
|
|
487
|
+
"""Show and select models interactively"""
|
|
488
|
+
selected_index = 0
|
|
489
|
+
|
|
490
|
+
while True:
|
|
491
|
+
# Clear screen and show header
|
|
492
|
+
console.clear()
|
|
493
|
+
self.show_header()
|
|
494
|
+
|
|
495
|
+
# Render interactive model table
|
|
496
|
+
console.print()
|
|
497
|
+
table = Table(
|
|
498
|
+
title="[bold cyan]Available Models[/bold cyan]",
|
|
499
|
+
border_style="blue",
|
|
500
|
+
show_header=True,
|
|
501
|
+
header_style="bold white on blue"
|
|
502
|
+
)
|
|
503
|
+
table.add_column("#", style="dim", width=4)
|
|
504
|
+
table.add_column("Model Name", style="cyan")
|
|
505
|
+
table.add_column("Status", style="green", width=12)
|
|
506
|
+
|
|
507
|
+
for idx, model_name in enumerate(self.available_models):
|
|
508
|
+
status = "✓ Active" if model_name == self.model else ""
|
|
509
|
+
style = "bold green on blue" if idx == selected_index else ""
|
|
510
|
+
table.add_row(str(idx + 1), model_name, status, style=style)
|
|
511
|
+
|
|
512
|
+
console.print(table)
|
|
513
|
+
console.print("\n[dim]Use ↑↓ arrows to navigate, Enter to select, Esc to cancel[/dim]")
|
|
514
|
+
|
|
515
|
+
# Get keystroke
|
|
516
|
+
char = self.get_char()
|
|
517
|
+
|
|
518
|
+
if char == '\x1b': # Escape or arrow key
|
|
519
|
+
next1 = self.get_char()
|
|
520
|
+
if next1 == '[':
|
|
521
|
+
next2 = self.get_char()
|
|
522
|
+
if next2 == 'A': # Up arrow
|
|
523
|
+
selected_index = max(0, selected_index - 1)
|
|
524
|
+
elif next2 == 'B': # Down arrow
|
|
525
|
+
selected_index = min(len(self.available_models) - 1, selected_index + 1)
|
|
526
|
+
else:
|
|
527
|
+
# Escape key - cancel
|
|
528
|
+
console.clear()
|
|
529
|
+
self.show_header()
|
|
530
|
+
return
|
|
531
|
+
elif char == '\r' or char == '\n': # Enter
|
|
532
|
+
# Select model
|
|
533
|
+
new_model = self.available_models[selected_index]
|
|
534
|
+
if new_model != self.model:
|
|
535
|
+
self.model = new_model
|
|
536
|
+
self.client = OllamaClient(model=new_model)
|
|
537
|
+
console.clear()
|
|
538
|
+
self.show_header()
|
|
539
|
+
console.print(f"\n[bold green]✓[/bold green] Switched to [cyan]{new_model}[/cyan]\n")
|
|
540
|
+
else:
|
|
541
|
+
console.clear()
|
|
542
|
+
self.show_header()
|
|
543
|
+
return
|
|
544
|
+
elif char == '\x03': # Ctrl+C
|
|
545
|
+
raise KeyboardInterrupt
|
|
546
|
+
|
|
547
|
+
def show_settings(self):
|
|
548
|
+
"""Show settings"""
|
|
549
|
+
from .. import __version__
|
|
550
|
+
from ..auth.auth_manager import AuthManager
|
|
551
|
+
|
|
552
|
+
auth = AuthManager()
|
|
553
|
+
user_info = auth.get_user_info()
|
|
554
|
+
|
|
555
|
+
console.print()
|
|
556
|
+
table = Table(
|
|
557
|
+
title="[bold cyan]Current Settings[/bold cyan]",
|
|
558
|
+
border_style="blue",
|
|
559
|
+
show_header=True,
|
|
560
|
+
header_style="bold white on blue"
|
|
561
|
+
)
|
|
562
|
+
table.add_column("Setting", style="bold cyan", width=20)
|
|
563
|
+
table.add_column("Value", style="yellow")
|
|
564
|
+
|
|
565
|
+
table.add_row("Version", __version__)
|
|
566
|
+
table.add_row("Company", "Tzamun Arabia IT Co. 🇸🇦")
|
|
567
|
+
table.add_row("User", f"{user_info.get('username', 'Guest')} ✓")
|
|
568
|
+
table.add_row("Auth Type", user_info.get('type', 'N/A').replace('_', ' ').title())
|
|
569
|
+
table.add_row("Backend", self.backend)
|
|
570
|
+
table.add_row("Model", self.model)
|
|
571
|
+
table.add_row("Available Models", str(len(self.available_models)))
|
|
572
|
+
table.add_row("Workspace", str(self.workspace))
|
|
573
|
+
table.add_row("Code Skills", "✓ Enabled")
|
|
574
|
+
|
|
575
|
+
console.print(table)
|
|
576
|
+
console.print()
|
|
577
|
+
|
|
578
|
+
def show_files(self):
|
|
579
|
+
"""Show files in workspace"""
|
|
580
|
+
console.print()
|
|
581
|
+
console.print("─" * 60, style="dim")
|
|
582
|
+
console.print()
|
|
583
|
+
|
|
584
|
+
files = self.file_manager.list_files(str(self.workspace))[:30]
|
|
585
|
+
|
|
586
|
+
console.print(f"[bold cyan]Files in {self.workspace.name}/[/bold cyan]\n")
|
|
587
|
+
for f in files:
|
|
588
|
+
console.print(f" [dim]•[/dim] {f}")
|
|
589
|
+
|
|
590
|
+
if len(files) >= 30:
|
|
591
|
+
console.print(f"\n[dim]... and more[/dim]")
|
|
592
|
+
|
|
593
|
+
console.print()
|
|
594
|
+
console.print("─" * 60, style="dim")
|
|
595
|
+
console.print()
|
|
596
|
+
|
|
597
|
+
def show_project_info(self):
|
|
598
|
+
"""Show project structure"""
|
|
599
|
+
console.print("\n")
|
|
600
|
+
console.print("─" * 60, style="dim")
|
|
601
|
+
console.print()
|
|
602
|
+
console.print("[dim]Scanning project...[/dim]")
|
|
603
|
+
console.print()
|
|
604
|
+
|
|
605
|
+
structure = self.project_scanner.scan()
|
|
606
|
+
|
|
607
|
+
table = Table(
|
|
608
|
+
title="[bold cyan]Project Structure[/bold cyan]",
|
|
609
|
+
border_style="blue",
|
|
610
|
+
show_header=True,
|
|
611
|
+
header_style="bold white on blue",
|
|
612
|
+
expand=False,
|
|
613
|
+
width=60
|
|
614
|
+
)
|
|
615
|
+
table.add_column("Property", style="bold cyan", width=20, no_wrap=True)
|
|
616
|
+
table.add_column("Value", style="yellow", overflow="fold", width=38)
|
|
617
|
+
|
|
618
|
+
table.add_row("Total Files", str(structure['total_files']))
|
|
619
|
+
table.add_row("Total Directories", str(structure['total_dirs']))
|
|
620
|
+
table.add_row("Languages", ", ".join(structure['languages']) if structure['languages'] else "None detected")
|
|
621
|
+
table.add_row("Project Type", self.project_scanner.get_project_type())
|
|
622
|
+
|
|
623
|
+
console.print(table)
|
|
624
|
+
|
|
625
|
+
if structure['key_files']:
|
|
626
|
+
console.print()
|
|
627
|
+
console.print("[bold cyan]Key Files:[/bold cyan]")
|
|
628
|
+
for f in structure['key_files'][:10]:
|
|
629
|
+
console.print(f" [dim]•[/dim] {f}")
|
|
630
|
+
|
|
631
|
+
console.print()
|
|
632
|
+
console.print("─" * 60, style="dim")
|
|
633
|
+
console.print()
|
|
634
|
+
|
|
635
|
+
def read_file(self, file_path: str):
|
|
636
|
+
"""Read and display a file with syntax highlighting"""
|
|
637
|
+
try:
|
|
638
|
+
from rich.syntax import Syntax
|
|
639
|
+
|
|
640
|
+
full_path = self.workspace / file_path
|
|
641
|
+
|
|
642
|
+
if not full_path.exists():
|
|
643
|
+
console.print(f"\n[bold red]✗[/bold red] File not found: {file_path}\n")
|
|
644
|
+
return
|
|
645
|
+
|
|
646
|
+
if full_path.is_dir():
|
|
647
|
+
console.print(f"\n[bold red]✗[/bold red] {file_path} is a directory, not a file\n")
|
|
648
|
+
return
|
|
649
|
+
|
|
650
|
+
# Read file content
|
|
651
|
+
content = full_path.read_text()
|
|
652
|
+
|
|
653
|
+
# Detect language for syntax highlighting
|
|
654
|
+
suffix = full_path.suffix.lstrip('.')
|
|
655
|
+
lang_map = {
|
|
656
|
+
'py': 'python', 'js': 'javascript', 'ts': 'typescript',
|
|
657
|
+
'jsx': 'jsx', 'tsx': 'tsx', 'java': 'java', 'cpp': 'cpp',
|
|
658
|
+
'c': 'c', 'go': 'go', 'rs': 'rust', 'rb': 'ruby',
|
|
659
|
+
'php': 'php', 'html': 'html', 'css': 'css', 'json': 'json',
|
|
660
|
+
'yaml': 'yaml', 'yml': 'yaml', 'xml': 'xml', 'md': 'markdown',
|
|
661
|
+
'sh': 'bash', 'bash': 'bash', 'sql': 'sql'
|
|
662
|
+
}
|
|
663
|
+
language = lang_map.get(suffix, 'text')
|
|
664
|
+
|
|
665
|
+
console.print()
|
|
666
|
+
console.print(f"[bold cyan]📄 {file_path}[/bold cyan] ({len(content)} chars, {len(content.splitlines())} lines)\n")
|
|
667
|
+
|
|
668
|
+
# Syntax highlighted display
|
|
669
|
+
syntax = Syntax(content, language, theme="monokai", line_numbers=True)
|
|
670
|
+
console.print(syntax)
|
|
671
|
+
console.print()
|
|
672
|
+
|
|
673
|
+
# Add to conversation context
|
|
674
|
+
self.conversation_history.append({
|
|
675
|
+
"role": "user",
|
|
676
|
+
"content": f"I read the file {file_path}:\n```{language}\n{content}\n```"
|
|
677
|
+
})
|
|
678
|
+
|
|
679
|
+
except Exception as e:
|
|
680
|
+
console.print(f"\n[bold red]✗ Error reading file:[/bold red] {e}\n")
|
|
681
|
+
|
|
682
|
+
def write_file(self, file_path: str):
|
|
683
|
+
"""Write content to a file interactively"""
|
|
684
|
+
try:
|
|
685
|
+
console.print()
|
|
686
|
+
console.print(f"[bold cyan]Writing to:[/bold cyan] {file_path}")
|
|
687
|
+
console.print("[dim]Enter content (type 'EOF' on a new line to finish):[/dim]\n")
|
|
688
|
+
|
|
689
|
+
lines = []
|
|
690
|
+
while True:
|
|
691
|
+
line = input()
|
|
692
|
+
if line == 'EOF':
|
|
693
|
+
break
|
|
694
|
+
lines.append(line)
|
|
695
|
+
|
|
696
|
+
content = '\n'.join(lines)
|
|
697
|
+
|
|
698
|
+
full_path = self.workspace / file_path
|
|
699
|
+
|
|
700
|
+
# Confirm if file exists
|
|
701
|
+
if full_path.exists():
|
|
702
|
+
if not Confirm.ask(f"\n[bold yellow]⚠[/bold yellow] File exists. Overwrite?"):
|
|
703
|
+
console.print("[dim]Write cancelled[/dim]\n")
|
|
704
|
+
return
|
|
705
|
+
|
|
706
|
+
# Create parent directories if needed
|
|
707
|
+
full_path.parent.mkdir(parents=True, exist_ok=True)
|
|
708
|
+
|
|
709
|
+
# Write file
|
|
710
|
+
full_path.write_text(content)
|
|
711
|
+
|
|
712
|
+
console.print(f"\n[bold green]✓[/bold green] Written {len(content)} chars to {file_path}\n")
|
|
713
|
+
|
|
714
|
+
# Add to conversation context
|
|
715
|
+
self.conversation_history.append({
|
|
716
|
+
"role": "user",
|
|
717
|
+
"content": f"I wrote to file {file_path}:\n```\n{content}\n```"
|
|
718
|
+
})
|
|
719
|
+
|
|
720
|
+
except KeyboardInterrupt:
|
|
721
|
+
console.print("\n[dim]Write cancelled[/dim]\n")
|
|
722
|
+
except Exception as e:
|
|
723
|
+
console.print(f"\n[bold red]✗ Error writing file:[/bold red] {e}\n")
|
|
724
|
+
|
|
725
|
+
def switch_backend(self):
|
|
726
|
+
"""Switch between vLLM and Ollama backends"""
|
|
727
|
+
from rich.prompt import Prompt
|
|
728
|
+
|
|
729
|
+
console.print()
|
|
730
|
+
console.print("[bold cyan]Switch AI Backend[/bold cyan]\n")
|
|
731
|
+
console.print(f"Current: [bold]{'vLLM' if self.use_vllm else 'Ollama'}[/bold]\n")
|
|
732
|
+
console.print(" [bold]1[/bold]. ⚡ [cyan]vLLM[/cyan] - Fast inference (Qwen 2.5 7B)")
|
|
733
|
+
console.print(" [bold]2[/bold]. 🦙 [green]Ollama[/green] - Powerful models (Qwen 2.5 32B)")
|
|
734
|
+
|
|
735
|
+
choice = Prompt.ask("\nSwitch to", choices=["1", "2"], default="2" if self.use_vllm else "1")
|
|
736
|
+
|
|
737
|
+
new_use_vllm = (choice == "1")
|
|
738
|
+
|
|
739
|
+
if new_use_vllm == self.use_vllm:
|
|
740
|
+
console.print("\n[dim]Already using this backend[/dim]\n")
|
|
741
|
+
return
|
|
742
|
+
|
|
743
|
+
# Switch backend
|
|
744
|
+
self.use_vllm = new_use_vllm
|
|
745
|
+
|
|
746
|
+
if self.use_vllm:
|
|
747
|
+
self.model = "qwen2.5-7b-instruct"
|
|
748
|
+
self.client = VLLMClient(model=self.model)
|
|
749
|
+
self.backend = "vLLM"
|
|
750
|
+
console.print("\n[bold green]✓[/bold green] Switched to [cyan]vLLM[/cyan] backend\n")
|
|
751
|
+
else:
|
|
752
|
+
self.model = "qwen2.5:32b"
|
|
753
|
+
self.client = OllamaClient(model=self.model)
|
|
754
|
+
self.backend = "Ollama"
|
|
755
|
+
console.print("\n[bold green]✓[/bold green] Switched to [green]Ollama[/green] backend\n")
|
|
756
|
+
|
|
757
|
+
def show_conversation_history(self):
|
|
758
|
+
"""Show conversation history"""
|
|
759
|
+
console.print()
|
|
760
|
+
|
|
761
|
+
if len(self.conversation_history) <= 1: # Only system prompt
|
|
762
|
+
console.print("[dim]No conversation history yet[/dim]\n")
|
|
763
|
+
return
|
|
764
|
+
|
|
765
|
+
console.print(f"[bold cyan]Conversation History[/bold cyan] ({len(self.conversation_history) - 1} messages)\n")
|
|
766
|
+
|
|
767
|
+
for idx, msg in enumerate(self.conversation_history[1:], 1): # Skip system prompt
|
|
768
|
+
role = msg['role']
|
|
769
|
+
content = msg['content']
|
|
770
|
+
|
|
771
|
+
if role == 'user':
|
|
772
|
+
console.print(f"[bold green]{idx}. You[/bold green] › {content[:100]}{'...' if len(content) > 100 else ''}")
|
|
773
|
+
else:
|
|
774
|
+
console.print(f"[bold blue]{idx}. AI[/bold blue] › {content[:100]}{'...' if len(content) > 100 else ''}")
|
|
775
|
+
|
|
776
|
+
console.print()
|
|
777
|
+
|
|
778
|
+
def save_conversation(self, filename: str):
|
|
779
|
+
"""Save conversation to JSON file"""
|
|
780
|
+
import json
|
|
781
|
+
|
|
782
|
+
try:
|
|
783
|
+
save_path = self.workspace / filename
|
|
784
|
+
|
|
785
|
+
# Prepare data
|
|
786
|
+
data = {
|
|
787
|
+
'model': self.model,
|
|
788
|
+
'backend': self.backend,
|
|
789
|
+
'workspace': str(self.workspace),
|
|
790
|
+
'conversation': self.conversation_history
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
# Save to file
|
|
794
|
+
with open(save_path, 'w') as f:
|
|
795
|
+
json.dump(data, f, indent=2)
|
|
796
|
+
|
|
797
|
+
console.print(f"\n[bold green]✓[/bold green] Conversation saved to {filename}\n")
|
|
798
|
+
|
|
799
|
+
except Exception as e:
|
|
800
|
+
console.print(f"\n[bold red]✗ Error saving:[/bold red] {e}\n")
|
|
801
|
+
|
|
802
|
+
def load_conversation(self, filename: str):
|
|
803
|
+
"""Load conversation from JSON file"""
|
|
804
|
+
import json
|
|
805
|
+
|
|
806
|
+
try:
|
|
807
|
+
load_path = self.workspace / filename
|
|
808
|
+
|
|
809
|
+
if not load_path.exists():
|
|
810
|
+
console.print(f"\n[bold red]✗[/bold red] File not found: {filename}\n")
|
|
811
|
+
return
|
|
812
|
+
|
|
813
|
+
# Load data
|
|
814
|
+
with open(load_path, 'r') as f:
|
|
815
|
+
data = json.load(f)
|
|
816
|
+
|
|
817
|
+
# Restore conversation
|
|
818
|
+
self.conversation_history = data['conversation']
|
|
819
|
+
|
|
820
|
+
console.print(f"\n[bold green]✓[/bold green] Loaded conversation from {filename}")
|
|
821
|
+
console.print(f"[dim]Model: {data.get('model', 'unknown')} | Messages: {len(self.conversation_history)}[/dim]\n")
|
|
822
|
+
|
|
823
|
+
except Exception as e:
|
|
824
|
+
console.print(f"\n[bold red]✗ Error loading:[/bold red] {e}\n")
|
|
825
|
+
|
|
826
|
+
def execute_command(self, command: str):
|
|
827
|
+
"""Execute a terminal command with user confirmation"""
|
|
828
|
+
console.print()
|
|
829
|
+
console.print(f"[bold yellow]Command to execute:[/bold yellow] [cyan]{command}[/cyan]")
|
|
830
|
+
console.print()
|
|
831
|
+
|
|
832
|
+
# Check for dangerous commands
|
|
833
|
+
dangerous_keywords = ['rm -rf', 'sudo rm', 'format', 'mkfs', 'dd if=']
|
|
834
|
+
if any(keyword in command.lower() for keyword in dangerous_keywords):
|
|
835
|
+
console.print("[bold red]⚠ Warning:[/bold red] This command appears dangerous!")
|
|
836
|
+
console.print("[dim]It may delete files or cause system damage.[/dim]\n")
|
|
837
|
+
|
|
838
|
+
# Ask for confirmation
|
|
839
|
+
if not Confirm.ask("[bold]Execute this command?[/bold]"):
|
|
840
|
+
console.print("[dim]Command cancelled[/dim]\n")
|
|
841
|
+
return
|
|
842
|
+
|
|
843
|
+
# Execute command
|
|
844
|
+
console.print("\n[dim]Executing...[/dim]\n")
|
|
845
|
+
|
|
846
|
+
try:
|
|
847
|
+
result = subprocess.run(
|
|
848
|
+
command,
|
|
849
|
+
shell=True,
|
|
850
|
+
cwd=str(self.workspace),
|
|
851
|
+
capture_output=True,
|
|
852
|
+
text=True,
|
|
853
|
+
timeout=30
|
|
854
|
+
)
|
|
855
|
+
|
|
856
|
+
# Show output
|
|
857
|
+
if result.stdout:
|
|
858
|
+
console.print("[bold green]Output:[/bold green]")
|
|
859
|
+
console.print(result.stdout)
|
|
860
|
+
|
|
861
|
+
if result.stderr:
|
|
862
|
+
console.print("[bold red]Errors:[/bold red]")
|
|
863
|
+
console.print(result.stderr)
|
|
864
|
+
|
|
865
|
+
if result.returncode == 0:
|
|
866
|
+
console.print(f"\n[bold green]✓[/bold green] Command completed successfully (exit code: {result.returncode})\n")
|
|
867
|
+
else:
|
|
868
|
+
console.print(f"\n[bold yellow]⚠[/bold yellow] Command exited with code: {result.returncode}\n")
|
|
869
|
+
if "No such file or directory" in result.stderr:
|
|
870
|
+
console.print("[dim]💡 Tip: Use /files to see available files or /project to see project structure[/dim]\n")
|
|
871
|
+
|
|
872
|
+
# Add to conversation history so AI can see the result
|
|
873
|
+
# Truncate output to prevent overwhelming context
|
|
874
|
+
max_output_length = 2000
|
|
875
|
+
stdout_truncated = result.stdout[:max_output_length] if result.stdout else ""
|
|
876
|
+
stderr_truncated = result.stderr[:max_output_length] if result.stderr else ""
|
|
877
|
+
|
|
878
|
+
if len(result.stdout) > max_output_length:
|
|
879
|
+
stdout_truncated += f"\n... (truncated {len(result.stdout) - max_output_length} characters)"
|
|
880
|
+
if len(result.stderr) > max_output_length:
|
|
881
|
+
stderr_truncated += f"\n... (truncated {len(result.stderr) - max_output_length} characters)"
|
|
882
|
+
|
|
883
|
+
self.conversation_history.append({
|
|
884
|
+
"role": "user",
|
|
885
|
+
"content": f"Executed command: {command}\nOutput: {stdout_truncated}\nErrors: {stderr_truncated}\nExit code: {result.returncode}"
|
|
886
|
+
})
|
|
887
|
+
|
|
888
|
+
except subprocess.TimeoutExpired:
|
|
889
|
+
console.print("[bold red]✗[/bold red] Command timed out (30s limit)\n")
|
|
890
|
+
except Exception as e:
|
|
891
|
+
console.print(f"[bold red]✗ Error:[/bold red] {e}\n")
|
|
892
|
+
|
|
893
|
+
def send_message(self, message: str):
|
|
894
|
+
"""Send message to AI"""
|
|
895
|
+
console.print(f"\n[bold green]You[/bold green] › {message}")
|
|
896
|
+
|
|
897
|
+
self.conversation_history.append({"role": "user", "content": message})
|
|
898
|
+
|
|
899
|
+
console.print("[bold blue]TzamunCode[/bold blue] › ", end="")
|
|
900
|
+
console.print("[dim](thinking...)[/dim] ", end="")
|
|
901
|
+
|
|
902
|
+
try:
|
|
903
|
+
response = ""
|
|
904
|
+
chunk_count = 0
|
|
905
|
+
buffer = ""
|
|
906
|
+
|
|
907
|
+
for chunk in self.client.chat_stream(self.conversation_history):
|
|
908
|
+
# Clear "thinking..." on first chunk
|
|
909
|
+
if chunk_count == 0:
|
|
910
|
+
sys.stdout.write('\b' * 15 + ' ' * 15 + '\b' * 15)
|
|
911
|
+
sys.stdout.flush()
|
|
912
|
+
|
|
913
|
+
response += chunk
|
|
914
|
+
buffer += chunk
|
|
915
|
+
|
|
916
|
+
# Flush buffer every 10 characters or on newline for instant display
|
|
917
|
+
if len(buffer) >= 10 or '\n' in chunk:
|
|
918
|
+
sys.stdout.write(buffer)
|
|
919
|
+
sys.stdout.flush()
|
|
920
|
+
buffer = ""
|
|
921
|
+
|
|
922
|
+
chunk_count += 1
|
|
923
|
+
|
|
924
|
+
# Flush any remaining buffer
|
|
925
|
+
if buffer:
|
|
926
|
+
sys.stdout.write(buffer)
|
|
927
|
+
sys.stdout.flush()
|
|
928
|
+
|
|
929
|
+
if chunk_count == 0:
|
|
930
|
+
# No response received
|
|
931
|
+
console.print("\n[bold yellow]⚠[/bold yellow] No response from model. Try a different model or check Ollama service.\n")
|
|
932
|
+
self.conversation_history.pop() # Remove the unanswered question
|
|
933
|
+
return
|
|
934
|
+
|
|
935
|
+
console.print("\n")
|
|
936
|
+
|
|
937
|
+
# Add response to history
|
|
938
|
+
self.conversation_history.append({"role": "assistant", "content": response})
|
|
939
|
+
|
|
940
|
+
# Auto-detect and execute commands from AI response
|
|
941
|
+
import re
|
|
942
|
+
execute_pattern = r'\[EXECUTE:\s*([^\]]+)\]'
|
|
943
|
+
matches = re.findall(execute_pattern, response)
|
|
944
|
+
|
|
945
|
+
if matches:
|
|
946
|
+
console.print(f"\n[bold yellow]🤖 AI suggested {len(matches)} command(s)[/bold yellow]\n")
|
|
947
|
+
for cmd in matches:
|
|
948
|
+
self.execute_command(cmd.strip())
|
|
949
|
+
|
|
950
|
+
# Show message to user - they can ask AI to analyze results
|
|
951
|
+
console.print("[dim]💡 Command executed. You can now ask me to analyze the results![/dim]\n")
|
|
952
|
+
|
|
953
|
+
except KeyboardInterrupt:
|
|
954
|
+
console.print("\n\n[dim]Response interrupted[/dim]\n")
|
|
955
|
+
self.conversation_history.pop() # Remove the unanswered question
|
|
956
|
+
except Exception as e:
|
|
957
|
+
console.print(f"\n[bold red]Error:[/bold red] {e}")
|
|
958
|
+
console.print("[dim]Try switching to a faster model with /models[/dim]\n")
|
|
959
|
+
self.conversation_history.pop() # Remove the unanswered question
|
|
960
|
+
|
|
961
|
+
|
|
962
|
+
def run_realtime_chat(model: str = "qwen2.5:32b", system: Optional[str] = None, use_vllm: bool = False):
|
|
963
|
+
"""Run real-time chat"""
|
|
964
|
+
chat = RealtimeChat(model=model, use_vllm=use_vllm)
|
|
965
|
+
chat.run()
|