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,323 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Interactive TUI Chat with dropdown menus like Claude Code
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from textual.app import App, ComposeResult
|
|
6
|
+
from textual.containers import Container, Vertical
|
|
7
|
+
from textual.widgets import Header, Footer, Static, Input, RichLog, OptionList
|
|
8
|
+
from textual.widgets.option_list import Option
|
|
9
|
+
from textual.binding import Binding
|
|
10
|
+
from rich.text import Text
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
from ..models.ollama import OllamaClient
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class InteractiveTUIChat(App):
|
|
17
|
+
"""Interactive TUI Chat with dropdown menus"""
|
|
18
|
+
|
|
19
|
+
CSS = """
|
|
20
|
+
Screen {
|
|
21
|
+
background: #1e1e1e;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
#messages {
|
|
25
|
+
height: 1fr;
|
|
26
|
+
background: #1e1e1e;
|
|
27
|
+
overflow-y: scroll;
|
|
28
|
+
border: none;
|
|
29
|
+
padding: 0 1;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
#command-menu {
|
|
33
|
+
display: none;
|
|
34
|
+
layer: overlay;
|
|
35
|
+
width: 50;
|
|
36
|
+
height: auto;
|
|
37
|
+
max-height: 15;
|
|
38
|
+
background: #2d2d2d;
|
|
39
|
+
border: solid #3a3a3a;
|
|
40
|
+
offset: 2 2;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
#shortcuts-menu {
|
|
44
|
+
display: none;
|
|
45
|
+
layer: overlay;
|
|
46
|
+
width: 40;
|
|
47
|
+
height: auto;
|
|
48
|
+
max-height: 10;
|
|
49
|
+
background: #2d2d2d;
|
|
50
|
+
border: solid #3a3a3a;
|
|
51
|
+
offset: 2 2;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
#separator {
|
|
55
|
+
dock: bottom;
|
|
56
|
+
height: 1;
|
|
57
|
+
background: #1e1e1e;
|
|
58
|
+
color: #3a3a3a;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
#input-container {
|
|
62
|
+
dock: bottom;
|
|
63
|
+
height: 1;
|
|
64
|
+
background: #1e1e1e;
|
|
65
|
+
padding: 0 1;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
#input {
|
|
69
|
+
width: 100%;
|
|
70
|
+
background: #1e1e1e;
|
|
71
|
+
border: none;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
#shortcuts-hint {
|
|
75
|
+
dock: bottom;
|
|
76
|
+
height: 1;
|
|
77
|
+
background: #1e1e1e;
|
|
78
|
+
color: #666;
|
|
79
|
+
text-align: right;
|
|
80
|
+
padding: 0 1;
|
|
81
|
+
}
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
BINDINGS = [
|
|
85
|
+
Binding("ctrl+c", "quit", "Quit"),
|
|
86
|
+
Binding("ctrl+l", "clear", "Clear"),
|
|
87
|
+
Binding("escape", "hide_menus", "Close menu", show=False),
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
def __init__(self, model: str = "qwen2.5:32b", **kwargs):
|
|
91
|
+
super().__init__(**kwargs)
|
|
92
|
+
self.model = model
|
|
93
|
+
self.client = OllamaClient(model=model)
|
|
94
|
+
self.conversation_history = []
|
|
95
|
+
self.menu_visible = False
|
|
96
|
+
|
|
97
|
+
def compose(self) -> ComposeResult:
|
|
98
|
+
"""Create child widgets"""
|
|
99
|
+
# Message history (no header, clean terminal look)
|
|
100
|
+
yield RichLog(id="messages", highlight=True, markup=True)
|
|
101
|
+
|
|
102
|
+
# Command menu (hidden by default)
|
|
103
|
+
command_options = [
|
|
104
|
+
Option("/help - Show available commands", id="help"),
|
|
105
|
+
Option("/models - List and switch models", id="models"),
|
|
106
|
+
Option("/settings - Show settings", id="settings"),
|
|
107
|
+
Option("/clear - Clear conversation", id="clear"),
|
|
108
|
+
Option("/exit - Exit chat", id="exit"),
|
|
109
|
+
]
|
|
110
|
+
yield OptionList(*command_options, id="command-menu")
|
|
111
|
+
|
|
112
|
+
# Shortcuts menu (hidden by default)
|
|
113
|
+
shortcut_options = [
|
|
114
|
+
Option("Ctrl+C - Exit chat", id="ctrl_c"),
|
|
115
|
+
Option("Ctrl+L - Clear screen", id="ctrl_l"),
|
|
116
|
+
Option("/ - Show commands", id="slash"),
|
|
117
|
+
Option("? - Show shortcuts", id="question"),
|
|
118
|
+
]
|
|
119
|
+
yield OptionList(*shortcut_options, id="shortcuts-menu")
|
|
120
|
+
|
|
121
|
+
# Separator line (like Claude Code)
|
|
122
|
+
yield Static("─" * 200, id="separator")
|
|
123
|
+
|
|
124
|
+
# Input container
|
|
125
|
+
with Container(id="input-container"):
|
|
126
|
+
yield Input(
|
|
127
|
+
placeholder="",
|
|
128
|
+
id="input"
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
# Shortcuts hint (like Claude Code)
|
|
132
|
+
yield Static("? for shortcuts", id="shortcuts-hint")
|
|
133
|
+
|
|
134
|
+
def on_mount(self) -> None:
|
|
135
|
+
"""Called when app starts"""
|
|
136
|
+
messages = self.query_one("#messages", RichLog)
|
|
137
|
+
|
|
138
|
+
# Minimal welcome like Claude Code
|
|
139
|
+
messages.write("╔╦╗╔═╗╔═╗╔╦╗╦ ╦╔╗╔╔═╗╔═╗╔╦╗╔═╗")
|
|
140
|
+
messages.write(" ║ ╔═╝╠═╣║║║║ ║║║║║ ║ ║ ║║╣ ")
|
|
141
|
+
messages.write(" ╩ ╚═╝╩ ╩╩ ╩╚═╝╝╚╝╚═╝╚═╝═╩╝╚═╝")
|
|
142
|
+
messages.write("")
|
|
143
|
+
messages.write(f"[dim]AI Coding Assistant • Built in Saudi Arabia 🇸🇦[/dim]")
|
|
144
|
+
messages.write(f"[dim]Model: {self.model} • Type '/' for commands[/dim]")
|
|
145
|
+
messages.write("")
|
|
146
|
+
messages.write("[green]Welcome![/green]")
|
|
147
|
+
messages.write("")
|
|
148
|
+
|
|
149
|
+
# Focus input
|
|
150
|
+
self.query_one("#input", Input).focus()
|
|
151
|
+
|
|
152
|
+
def on_input_changed(self, event: Input.Changed) -> None:
|
|
153
|
+
"""Handle input changes in real-time"""
|
|
154
|
+
current_value = event.value
|
|
155
|
+
|
|
156
|
+
# Show command menu when user types '/'
|
|
157
|
+
if current_value == '/':
|
|
158
|
+
self.show_command_menu()
|
|
159
|
+
return
|
|
160
|
+
|
|
161
|
+
# Show shortcuts menu when user types '?'
|
|
162
|
+
if current_value == '?':
|
|
163
|
+
self.show_shortcuts_menu()
|
|
164
|
+
return
|
|
165
|
+
|
|
166
|
+
# Hide menus if user continues typing
|
|
167
|
+
if len(current_value) > 1 and self.menu_visible:
|
|
168
|
+
self.hide_menus()
|
|
169
|
+
|
|
170
|
+
def show_command_menu(self) -> None:
|
|
171
|
+
"""Show interactive command menu"""
|
|
172
|
+
menu = self.query_one("#command-menu", OptionList)
|
|
173
|
+
menu.styles.display = "block"
|
|
174
|
+
menu.focus()
|
|
175
|
+
self.menu_visible = True
|
|
176
|
+
|
|
177
|
+
def show_shortcuts_menu(self) -> None:
|
|
178
|
+
"""Show interactive shortcuts menu"""
|
|
179
|
+
menu = self.query_one("#shortcuts-menu", OptionList)
|
|
180
|
+
menu.styles.display = "block"
|
|
181
|
+
menu.focus()
|
|
182
|
+
self.menu_visible = True
|
|
183
|
+
|
|
184
|
+
def hide_menus(self) -> None:
|
|
185
|
+
"""Hide all menus"""
|
|
186
|
+
self.query_one("#command-menu").styles.display = "none"
|
|
187
|
+
self.query_one("#shortcuts-menu").styles.display = "none"
|
|
188
|
+
self.menu_visible = False
|
|
189
|
+
self.query_one("#input", Input).focus()
|
|
190
|
+
|
|
191
|
+
def action_hide_menus(self) -> None:
|
|
192
|
+
"""Action to hide menus"""
|
|
193
|
+
self.hide_menus()
|
|
194
|
+
# Clear input
|
|
195
|
+
self.query_one("#input", Input).value = ""
|
|
196
|
+
|
|
197
|
+
def on_option_list_option_selected(self, event: OptionList.OptionSelected) -> None:
|
|
198
|
+
"""Handle menu option selection"""
|
|
199
|
+
option_id = event.option_id
|
|
200
|
+
|
|
201
|
+
# Hide menus
|
|
202
|
+
self.hide_menus()
|
|
203
|
+
|
|
204
|
+
# Clear input
|
|
205
|
+
input_widget = self.query_one("#input", Input)
|
|
206
|
+
input_widget.value = ""
|
|
207
|
+
|
|
208
|
+
# Execute command
|
|
209
|
+
if option_id == "help":
|
|
210
|
+
self.show_help_in_messages()
|
|
211
|
+
elif option_id == "models":
|
|
212
|
+
self.show_models_in_messages()
|
|
213
|
+
elif option_id == "settings":
|
|
214
|
+
self.show_settings_in_messages()
|
|
215
|
+
elif option_id == "clear":
|
|
216
|
+
self.action_clear()
|
|
217
|
+
elif option_id == "exit":
|
|
218
|
+
self.exit()
|
|
219
|
+
|
|
220
|
+
def show_help_in_messages(self) -> None:
|
|
221
|
+
"""Show help in message area"""
|
|
222
|
+
messages = self.query_one("#messages", RichLog)
|
|
223
|
+
messages.write("[bold cyan]Available Commands:[/bold cyan]")
|
|
224
|
+
messages.write(" /help - Show available commands")
|
|
225
|
+
messages.write(" /models - List and switch models")
|
|
226
|
+
messages.write(" /settings - Show settings")
|
|
227
|
+
messages.write(" /clear - Clear conversation")
|
|
228
|
+
messages.write(" /exit - Exit chat")
|
|
229
|
+
messages.write("")
|
|
230
|
+
|
|
231
|
+
def show_models_in_messages(self) -> None:
|
|
232
|
+
"""Show models in message area"""
|
|
233
|
+
messages = self.query_one("#messages", RichLog)
|
|
234
|
+
messages.write("[bold cyan]Available Models:[/bold cyan]")
|
|
235
|
+
try:
|
|
236
|
+
models = self.client.list_models()
|
|
237
|
+
for idx, model in enumerate(models, 1):
|
|
238
|
+
status = "✓ Active" if model == self.model else ""
|
|
239
|
+
messages.write(f" {idx}. {model} {status}")
|
|
240
|
+
except Exception as e:
|
|
241
|
+
messages.write(f"[bold red]Error:[/bold red] {e}")
|
|
242
|
+
messages.write("")
|
|
243
|
+
|
|
244
|
+
def show_settings_in_messages(self) -> None:
|
|
245
|
+
"""Show settings in message area"""
|
|
246
|
+
messages = self.query_one("#messages", RichLog)
|
|
247
|
+
messages.write("[bold cyan]Current Settings:[/bold cyan]")
|
|
248
|
+
messages.write(f" Model: {self.model}")
|
|
249
|
+
messages.write(f" Streaming: Enabled")
|
|
250
|
+
messages.write("")
|
|
251
|
+
|
|
252
|
+
def on_input_submitted(self, event: Input.Submitted) -> None:
|
|
253
|
+
"""Handle input submission"""
|
|
254
|
+
user_input = event.value.strip()
|
|
255
|
+
|
|
256
|
+
if not user_input:
|
|
257
|
+
return
|
|
258
|
+
|
|
259
|
+
# Clear input
|
|
260
|
+
event.input.value = ""
|
|
261
|
+
|
|
262
|
+
# Handle exit
|
|
263
|
+
if user_input.lower() in ['exit', 'quit']:
|
|
264
|
+
self.exit()
|
|
265
|
+
return
|
|
266
|
+
|
|
267
|
+
# Send to AI
|
|
268
|
+
self.send_message(user_input)
|
|
269
|
+
|
|
270
|
+
def send_message(self, message: str) -> None:
|
|
271
|
+
"""Send message to AI"""
|
|
272
|
+
messages = self.query_one("#messages", RichLog)
|
|
273
|
+
|
|
274
|
+
# Show user message
|
|
275
|
+
messages.write(f"[bold green]You[/bold green] › {message}")
|
|
276
|
+
|
|
277
|
+
# Add to history
|
|
278
|
+
self.conversation_history.append({"role": "user", "content": message})
|
|
279
|
+
|
|
280
|
+
# Show thinking
|
|
281
|
+
messages.write("[dim]TzamunCode is thinking...[/dim]")
|
|
282
|
+
|
|
283
|
+
try:
|
|
284
|
+
# Get response
|
|
285
|
+
response = ""
|
|
286
|
+
for chunk in self.client.chat_stream(self.conversation_history):
|
|
287
|
+
response += chunk
|
|
288
|
+
|
|
289
|
+
# Clear and re-show conversation
|
|
290
|
+
messages.clear()
|
|
291
|
+
for msg in self.conversation_history:
|
|
292
|
+
if msg["role"] == "user":
|
|
293
|
+
messages.write(f"[bold green]You[/bold green] › {msg['content']}")
|
|
294
|
+
elif msg["role"] == "assistant":
|
|
295
|
+
messages.write(f"[bold blue]TzamunCode[/bold blue] › {msg['content']}")
|
|
296
|
+
|
|
297
|
+
# Show new response
|
|
298
|
+
messages.write(f"[bold blue]TzamunCode[/bold blue] › {response}")
|
|
299
|
+
messages.write("")
|
|
300
|
+
|
|
301
|
+
# Add to history
|
|
302
|
+
self.conversation_history.append({"role": "assistant", "content": response})
|
|
303
|
+
|
|
304
|
+
except Exception as e:
|
|
305
|
+
messages.write(f"[bold red]Error:[/bold red] {e}")
|
|
306
|
+
|
|
307
|
+
def action_clear(self) -> None:
|
|
308
|
+
"""Clear conversation"""
|
|
309
|
+
self.conversation_history = []
|
|
310
|
+
messages = self.query_one("#messages", RichLog)
|
|
311
|
+
messages.clear()
|
|
312
|
+
messages.write("[bold green]Conversation cleared[/bold green]")
|
|
313
|
+
messages.write("")
|
|
314
|
+
|
|
315
|
+
def action_quit(self) -> None:
|
|
316
|
+
"""Quit app"""
|
|
317
|
+
self.exit()
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def run_interactive_chat(model: str = "qwen2.5:32b", system: Optional[str] = None):
|
|
321
|
+
"""Run interactive chat"""
|
|
322
|
+
app = InteractiveTUIChat(model=model)
|
|
323
|
+
app.run()
|