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.
@@ -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()