codrninja 0.3.0__tar.gz → 0.3.2__tar.gz
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.
- {codrninja-0.3.0/src/codrninja.egg-info → codrninja-0.3.2}/PKG-INFO +1 -1
- {codrninja-0.3.0 → codrninja-0.3.2}/pyproject.toml +1 -1
- {codrninja-0.3.0 → codrninja-0.3.2}/src/codrninja/__init__.py +0 -1
- codrninja-0.3.2/src/codrninja/tui.py +335 -0
- {codrninja-0.3.0 → codrninja-0.3.2/src/codrninja.egg-info}/PKG-INFO +1 -1
- {codrninja-0.3.0 → codrninja-0.3.2}/src/codrninja.egg-info/SOURCES.txt +0 -2
- codrninja-0.3.0/src/codrninja/__version__.py +0 -1
- codrninja-0.3.0/src/codrninja/_version.py +0 -24
- codrninja-0.3.0/src/codrninja/tui.py +0 -322
- {codrninja-0.3.0 → codrninja-0.3.2}/.gitignore +0 -0
- {codrninja-0.3.0 → codrninja-0.3.2}/LICENSE +0 -0
- {codrninja-0.3.0 → codrninja-0.3.2}/README.md +0 -0
- {codrninja-0.3.0 → codrninja-0.3.2}/install.sh +0 -0
- {codrninja-0.3.0 → codrninja-0.3.2}/setup.cfg +0 -0
- {codrninja-0.3.0 → codrninja-0.3.2}/src/codrninja/agent.py +0 -0
- {codrninja-0.3.0 → codrninja-0.3.2}/src/codrninja/cli.py +0 -0
- {codrninja-0.3.0 → codrninja-0.3.2}/src/codrninja/config.py +0 -0
- {codrninja-0.3.0 → codrninja-0.3.2}/src/codrninja/core.py +0 -0
- {codrninja-0.3.0 → codrninja-0.3.2}/src/codrninja/interactive.py +0 -0
- {codrninja-0.3.0 → codrninja-0.3.2}/src/codrninja/providers.py +0 -0
- {codrninja-0.3.0 → codrninja-0.3.2}/src/codrninja/tools.py +0 -0
- {codrninja-0.3.0 → codrninja-0.3.2}/src/codrninja.egg-info/dependency_links.txt +0 -0
- {codrninja-0.3.0 → codrninja-0.3.2}/src/codrninja.egg-info/entry_points.txt +0 -0
- {codrninja-0.3.0 → codrninja-0.3.2}/src/codrninja.egg-info/requires.txt +0 -0
- {codrninja-0.3.0 → codrninja-0.3.2}/src/codrninja.egg-info/top_level.txt +0 -0
- {codrninja-0.3.0 → codrninja-0.3.2}/tests/test_core.py +0 -0
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
codrninja TUI — Interactive coding assistant using prompt_toolkit.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
from prompt_toolkit import PromptSession
|
|
13
|
+
from prompt_toolkit.completion import Completer, Completion
|
|
14
|
+
from prompt_toolkit.styles import Style
|
|
15
|
+
from prompt_toolkit.key_binding import KeyBindings
|
|
16
|
+
HAS_PROMPT = True
|
|
17
|
+
except ImportError:
|
|
18
|
+
HAS_PROMPT = False
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
from rich.console import Console
|
|
22
|
+
from rich.panel import Panel
|
|
23
|
+
from rich.syntax import Syntax
|
|
24
|
+
from rich.markdown import Markdown
|
|
25
|
+
from rich.status import Status
|
|
26
|
+
from rich.table import Table
|
|
27
|
+
from rich import box
|
|
28
|
+
HAS_RICH = True
|
|
29
|
+
except ImportError:
|
|
30
|
+
HAS_RICH = False
|
|
31
|
+
|
|
32
|
+
from codrninja.core import AICode
|
|
33
|
+
from codrninja.agent import Agent
|
|
34
|
+
from codrninja.tools import ToolRegistry
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
CODRNINJA_LOGO = """
|
|
38
|
+
╔══════════════════════════════════════════════════════════════════════════════════════╗
|
|
39
|
+
║ ║
|
|
40
|
+
║ ██████╗ ██████╗ ██████╗ ██████╗ ███╗ ██╗██╗███╗ ██╗ ██╗ █████╗ ║
|
|
41
|
+
║ ██╔════╝██╔═══██╗██╔══██╗██╔══██╗████╗ ██║██║████╗ ██║ ██║██╔══██╗ ║
|
|
42
|
+
║ ██║ ██║ ██║██║ ██║██████╔╝██╔██╗ ██║██║██╔██╗ ██║ ██║███████║ ║
|
|
43
|
+
║ ██║ ██║ ██║██║ ██║██╔══██╗██║╚██╗██║██║██║╚██╗██║██ ██║██╔══██║ ║
|
|
44
|
+
║ ╚██████╗╚██████╔╝██████╔╝██║ ██║██║ ╚████║██║██║ ╚████║╚█████╔╝██║ ██║ ║
|
|
45
|
+
║ ╚═════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═══╝╚═╝╚═╝ ╚═══╝ ╚════╝ ╚═╝ ╚═╝ ║
|
|
46
|
+
║ ║
|
|
47
|
+
║ AI-first coding assistant for automation ║
|
|
48
|
+
║ ║
|
|
49
|
+
╚══════════════════════════════════════════════════════════════════════════════════════╝
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
SLASH_COMMANDS = {
|
|
53
|
+
"/clear": "Clear screen",
|
|
54
|
+
"/commit": "Git commit changes",
|
|
55
|
+
"/context": "Show project context",
|
|
56
|
+
"/edit": "Edit file by replacement",
|
|
57
|
+
"/exec": "Execute shell command",
|
|
58
|
+
"/exit": "Exit codrninja",
|
|
59
|
+
"/explain": "Explain code or concept",
|
|
60
|
+
"/files": "List files in directory",
|
|
61
|
+
"/help": "Show detailed help",
|
|
62
|
+
"/model": "Show AI configuration",
|
|
63
|
+
"/plan": "Plan a feature or task",
|
|
64
|
+
"/read": "Read file",
|
|
65
|
+
"/review": "Review code for issues",
|
|
66
|
+
"/session": "Show session info",
|
|
67
|
+
"/test": "Run tests",
|
|
68
|
+
"/write": "Write to file",
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class SlashCompleter(Completer):
|
|
73
|
+
"""Custom completer for slash commands."""
|
|
74
|
+
|
|
75
|
+
def get_completions(self, document, complete_event):
|
|
76
|
+
text = document.text
|
|
77
|
+
if not text.startswith('/'):
|
|
78
|
+
return
|
|
79
|
+
|
|
80
|
+
partial = text[1:]
|
|
81
|
+
for cmd, desc in SLASH_COMMANDS.items():
|
|
82
|
+
if cmd[1:].startswith(partial):
|
|
83
|
+
yield Completion(
|
|
84
|
+
cmd,
|
|
85
|
+
start_position=-len(text),
|
|
86
|
+
display=f"{cmd:18} {desc}",
|
|
87
|
+
display_meta=desc,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class TUI:
|
|
92
|
+
"""Terminal User Interface using prompt_toolkit."""
|
|
93
|
+
|
|
94
|
+
def __init__(self, ai: AICode, session_name: str):
|
|
95
|
+
self.ai = ai
|
|
96
|
+
self.session_name = session_name
|
|
97
|
+
self.agent = Agent(ai, session_name)
|
|
98
|
+
self.console = Console() if HAS_RICH else None
|
|
99
|
+
self.tools = ToolRegistry()
|
|
100
|
+
|
|
101
|
+
def start(self):
|
|
102
|
+
"""Start the TUI."""
|
|
103
|
+
if not HAS_PROMPT:
|
|
104
|
+
print("Error: prompt_toolkit not installed")
|
|
105
|
+
print("Run: pip3 install prompt_toolkit")
|
|
106
|
+
return
|
|
107
|
+
|
|
108
|
+
self._show_welcome()
|
|
109
|
+
|
|
110
|
+
# Create prompt session with completion
|
|
111
|
+
style = Style.from_dict({
|
|
112
|
+
'prompt': '#00aa00 bold',
|
|
113
|
+
'completion-menu': 'bg:#333333 #ffffff',
|
|
114
|
+
'completion-menu.completion.current': 'bg:#ffffff #000000',
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
session = PromptSession(
|
|
118
|
+
completer=SlashCompleter(),
|
|
119
|
+
style=style,
|
|
120
|
+
complete_while_typing=True,
|
|
121
|
+
multiline=False,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
while True:
|
|
125
|
+
try:
|
|
126
|
+
message = session.prompt('You: ')
|
|
127
|
+
|
|
128
|
+
if not message.strip():
|
|
129
|
+
continue
|
|
130
|
+
|
|
131
|
+
if message.lower() in ['exit', 'quit', 'q']:
|
|
132
|
+
print("\nGoodbye!\n")
|
|
133
|
+
break
|
|
134
|
+
|
|
135
|
+
if message.startswith('/'):
|
|
136
|
+
if not self._handle_command(message):
|
|
137
|
+
break
|
|
138
|
+
continue
|
|
139
|
+
|
|
140
|
+
self._process_message(message)
|
|
141
|
+
|
|
142
|
+
except KeyboardInterrupt:
|
|
143
|
+
print("\nInterrupted. Type 'exit' to quit.")
|
|
144
|
+
except EOFError:
|
|
145
|
+
break
|
|
146
|
+
|
|
147
|
+
def _show_welcome(self):
|
|
148
|
+
"""Show welcome screen."""
|
|
149
|
+
if self.console:
|
|
150
|
+
for line in CODRNINJA_LOGO.strip().split('\n'):
|
|
151
|
+
self.console.print(f"[bold cyan]{line}[/bold cyan]")
|
|
152
|
+
self.console.print(f"\n Session: [bold yellow]{self.session_name}[/bold yellow]")
|
|
153
|
+
self.console.print(" Type / for commands, or just start typing\n")
|
|
154
|
+
else:
|
|
155
|
+
print(CODRNINJA_LOGO)
|
|
156
|
+
print(f"\n Session: {self.session_name}")
|
|
157
|
+
print(" Type / for commands\n")
|
|
158
|
+
|
|
159
|
+
def _process_message(self, message: str):
|
|
160
|
+
"""Process user message."""
|
|
161
|
+
if self.console:
|
|
162
|
+
with Status("[bold yellow]Thinking...[/bold yellow]", spinner="dots", console=self.console):
|
|
163
|
+
result = self.agent.run(message, auto_approve=False)
|
|
164
|
+
else:
|
|
165
|
+
print("Thinking...")
|
|
166
|
+
result = self.agent.run(message, auto_approve=False)
|
|
167
|
+
|
|
168
|
+
if not result['success']:
|
|
169
|
+
if self.console:
|
|
170
|
+
self.console.print(f"\n[bold red]Error:[/bold red] {result.get('error', 'Unknown error')}")
|
|
171
|
+
else:
|
|
172
|
+
print(f"\nError: {result.get('error', 'Unknown error')}")
|
|
173
|
+
return
|
|
174
|
+
|
|
175
|
+
if self.console:
|
|
176
|
+
self._display_response(result['response'])
|
|
177
|
+
if result.get('tool_calls', 0) > 0:
|
|
178
|
+
self._show_tool_usage(result['tools_used'], result['iterations'])
|
|
179
|
+
else:
|
|
180
|
+
print(f"\nAI: {result['response']}")
|
|
181
|
+
|
|
182
|
+
def _handle_command(self, command: str) -> bool:
|
|
183
|
+
"""Handle slash commands."""
|
|
184
|
+
parts = command.split()
|
|
185
|
+
cmd = parts[0].lower()
|
|
186
|
+
|
|
187
|
+
if cmd == '/exit':
|
|
188
|
+
print("\nGoodbye!\n")
|
|
189
|
+
return False
|
|
190
|
+
elif cmd == '/help':
|
|
191
|
+
print("\nCommands:")
|
|
192
|
+
for c, d in SLASH_COMMANDS.items():
|
|
193
|
+
print(f" {c:16} {d}")
|
|
194
|
+
print()
|
|
195
|
+
elif cmd == '/clear':
|
|
196
|
+
os.system('clear' if os.name != 'nt' else 'cls')
|
|
197
|
+
self._show_welcome()
|
|
198
|
+
elif cmd == '/files':
|
|
199
|
+
result = self.tools.list_files(path=".", depth=2)
|
|
200
|
+
if result.success:
|
|
201
|
+
if self.console:
|
|
202
|
+
self.console.print(Panel(result.output, title="📁 Files", border_style="blue"))
|
|
203
|
+
else:
|
|
204
|
+
print(f"\n{result.output}")
|
|
205
|
+
else:
|
|
206
|
+
print(f"Error: {result.error}")
|
|
207
|
+
elif cmd == '/model':
|
|
208
|
+
if self.console:
|
|
209
|
+
table = Table(title="AI Configuration", box=box.ROUNDED)
|
|
210
|
+
table.add_column("Setting", style="cyan")
|
|
211
|
+
table.add_column("Value", style="white")
|
|
212
|
+
table.add_row("Provider", self.ai.config.default_provider)
|
|
213
|
+
table.add_row("Model", self.ai.config.default_model)
|
|
214
|
+
table.add_row("Ollama URL", self.ai.config.ollama_url)
|
|
215
|
+
self.console.print(table)
|
|
216
|
+
else:
|
|
217
|
+
print(f"\nProvider: {self.ai.config.default_provider}")
|
|
218
|
+
print(f"Model: {self.ai.config.default_model}")
|
|
219
|
+
elif cmd == '/session':
|
|
220
|
+
history = self.ai.get_history(self.session_name)
|
|
221
|
+
if history:
|
|
222
|
+
print(f"\nSession: {self.session_name}")
|
|
223
|
+
print(f"Messages: {len(history['messages'])}")
|
|
224
|
+
else:
|
|
225
|
+
print(f"\nSession: {self.session_name} (new)")
|
|
226
|
+
elif cmd == '/exec':
|
|
227
|
+
if len(parts) > 1:
|
|
228
|
+
cmd_str = ' '.join(parts[1:])
|
|
229
|
+
if self.console:
|
|
230
|
+
with Status(f"[bold yellow]{cmd_str}[/bold yellow]", console=self.console):
|
|
231
|
+
result = self.tools.execute_command(cmd_str)
|
|
232
|
+
else:
|
|
233
|
+
result = self.tools.execute_command(cmd_str)
|
|
234
|
+
if result.success:
|
|
235
|
+
if self.console:
|
|
236
|
+
self.console.print(Panel(result.output, title=f"⚡ {cmd_str}", border_style="green"))
|
|
237
|
+
else:
|
|
238
|
+
print(f"\n{result.output}")
|
|
239
|
+
else:
|
|
240
|
+
print(f"Error: {result.error}")
|
|
241
|
+
elif cmd == '/read':
|
|
242
|
+
if len(parts) > 1:
|
|
243
|
+
result = self.tools.read_file(parts[1])
|
|
244
|
+
if result.success:
|
|
245
|
+
if self.console:
|
|
246
|
+
ext = os.path.splitext(parts[1])[1]
|
|
247
|
+
lang_map = {'.py': 'python', '.js': 'javascript', '.ts': 'typescript', '.jsx': 'javascript', '.tsx': 'typescript', '.json': 'json', '.css': 'css', '.html': 'html', '.md': 'markdown'}
|
|
248
|
+
lang = lang_map.get(ext, "text")
|
|
249
|
+
syntax = Syntax(result.output, lang, theme="monokai", line_numbers=True)
|
|
250
|
+
self.console.print(Panel(syntax, title=f"📄 {parts[1]}", border_style="blue"))
|
|
251
|
+
else:
|
|
252
|
+
print(f"\n{result.output}")
|
|
253
|
+
else:
|
|
254
|
+
print(f"Error: {result.error}")
|
|
255
|
+
elif cmd == '/plan':
|
|
256
|
+
topic = ' '.join(parts[1:]) if len(parts) > 1 else input("What to plan? ")
|
|
257
|
+
self._process_message(f"Create a detailed plan for: {topic}")
|
|
258
|
+
elif cmd == '/build':
|
|
259
|
+
topic = ' '.join(parts[1:]) if len(parts) > 1 else input("What to build? ")
|
|
260
|
+
self._process_message(f"Implement: {topic}")
|
|
261
|
+
elif cmd == '/review':
|
|
262
|
+
target = parts[1] if len(parts) > 1 else input("File to review? ")
|
|
263
|
+
result = self.tools.read_file(target)
|
|
264
|
+
if result.success:
|
|
265
|
+
self._process_message(f"Review this code:\n\n{result.output}")
|
|
266
|
+
else:
|
|
267
|
+
print(f"Error: {result.error}")
|
|
268
|
+
elif cmd == '/explain':
|
|
269
|
+
target = ' '.join(parts[1:]) if len(parts) > 1 else input("What to explain? ")
|
|
270
|
+
self._process_message(f"Explain: {target}")
|
|
271
|
+
elif cmd == '/test':
|
|
272
|
+
target = ' '.join(parts[1:]) if len(parts) > 1 else "npm test"
|
|
273
|
+
result = self.tools.execute_command(target)
|
|
274
|
+
if result.success:
|
|
275
|
+
if self.console:
|
|
276
|
+
self.console.print(Panel(result.output, title=f"🧪 {target}", border_style="green"))
|
|
277
|
+
else:
|
|
278
|
+
print(f"\n{result.output}")
|
|
279
|
+
else:
|
|
280
|
+
print(f"Error: {result.error}")
|
|
281
|
+
elif cmd == '/commit':
|
|
282
|
+
msg = ' '.join(parts[1:]) if len(parts) > 1 else input("Commit message? ")
|
|
283
|
+
self.tools.execute_command('git add -A')
|
|
284
|
+
result = self.tools.execute_command(f'git commit -m "{msg}"')
|
|
285
|
+
if result.success:
|
|
286
|
+
print(f"✅ Committed: {msg}")
|
|
287
|
+
else:
|
|
288
|
+
print(f"Error: {result.error}")
|
|
289
|
+
else:
|
|
290
|
+
print(f"Unknown command: {cmd}. Type /help for list.")
|
|
291
|
+
|
|
292
|
+
return True
|
|
293
|
+
|
|
294
|
+
def _display_response(self, response: str):
|
|
295
|
+
"""Display AI response with Rich."""
|
|
296
|
+
parts = response.split('```')
|
|
297
|
+
for i, part in enumerate(parts):
|
|
298
|
+
if i % 2 == 0:
|
|
299
|
+
if part.strip():
|
|
300
|
+
self.console.print(f"\n[bold blue]🤖 AI[/bold blue]:")
|
|
301
|
+
self.console.print(Markdown(part.strip()))
|
|
302
|
+
else:
|
|
303
|
+
lines = part.split('\n')
|
|
304
|
+
lang = lines[0].strip() if lines else ""
|
|
305
|
+
code = '\n'.join(lines[1:]) if lines else part
|
|
306
|
+
if code.strip():
|
|
307
|
+
syntax = Syntax(code, lang or "text", theme="monokai", line_numbers=True)
|
|
308
|
+
self.console.print(syntax)
|
|
309
|
+
|
|
310
|
+
def _show_tool_usage(self, tools_used: list, iterations: int):
|
|
311
|
+
"""Show tool usage summary."""
|
|
312
|
+
table = Table(title=f"Tools ({len(tools_used)} calls, {iterations} iterations)", box=box.ROUNDED)
|
|
313
|
+
table.add_column("Tool", style="cyan")
|
|
314
|
+
table.add_column("Status", style="green")
|
|
315
|
+
table.add_column("Result", style="white")
|
|
316
|
+
for tool in tools_used:
|
|
317
|
+
status = "✅" if tool['success'] else "❌"
|
|
318
|
+
result = tool['output'][:60] + "..." if len(tool['output']) > 60 else tool['output']
|
|
319
|
+
table.add_row(tool['tool'], status, result)
|
|
320
|
+
self.console.print(table)
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def main():
|
|
324
|
+
if len(sys.argv) < 2:
|
|
325
|
+
print("Usage: codrninja-tui <session-name>")
|
|
326
|
+
sys.exit(1)
|
|
327
|
+
|
|
328
|
+
session_name = sys.argv[1]
|
|
329
|
+
ai = AICode()
|
|
330
|
+
tui = TUI(ai, session_name)
|
|
331
|
+
tui.start()
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
if __name__ == "__main__":
|
|
335
|
+
main()
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.3.0"
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
# file generated by vcs-versioning
|
|
2
|
-
# don't change, don't track in version control
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
__all__ = [
|
|
6
|
-
"__version__",
|
|
7
|
-
"__version_tuple__",
|
|
8
|
-
"version",
|
|
9
|
-
"version_tuple",
|
|
10
|
-
"__commit_id__",
|
|
11
|
-
"commit_id",
|
|
12
|
-
]
|
|
13
|
-
|
|
14
|
-
version: str
|
|
15
|
-
__version__: str
|
|
16
|
-
__version_tuple__: tuple[int | str, ...]
|
|
17
|
-
version_tuple: tuple[int | str, ...]
|
|
18
|
-
commit_id: str | None
|
|
19
|
-
__commit_id__: str | None
|
|
20
|
-
|
|
21
|
-
__version__ = version = '0.1.dev21+g30f955b0f.d20260506'
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 1, 'dev21', 'g30f955b0f.d20260506')
|
|
23
|
-
|
|
24
|
-
__commit_id__ = commit_id = 'g30f955b0f'
|
|
@@ -1,322 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
codrninja TUI — Interactive coding assistant using prompt_toolkit.
|
|
4
|
-
Features: Arrow key navigation, tab completion, beautiful UI.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
import json
|
|
8
|
-
import os
|
|
9
|
-
import sys
|
|
10
|
-
from typing import Optional, List
|
|
11
|
-
|
|
12
|
-
try:
|
|
13
|
-
from prompt_toolkit import Application
|
|
14
|
-
from prompt_toolkit.buffer import Buffer
|
|
15
|
-
from prompt_toolkit.layout.containers import HSplit, Window, WindowAlign
|
|
16
|
-
from prompt_toolkit.layout.controls import BufferControl, FormattedTextControl
|
|
17
|
-
from prompt_toolkit.layout.layout import Layout
|
|
18
|
-
from prompt_toolkit.key_binding import KeyBindings
|
|
19
|
-
from prompt_toolkit.formatted_text import HTML
|
|
20
|
-
from prompt_toolkit.styles import Style
|
|
21
|
-
HAS_PROMPT = True
|
|
22
|
-
except ImportError:
|
|
23
|
-
HAS_PROMPT = False
|
|
24
|
-
|
|
25
|
-
try:
|
|
26
|
-
from rich.console import Console
|
|
27
|
-
HAS_RICH = True
|
|
28
|
-
except ImportError:
|
|
29
|
-
HAS_RICH = False
|
|
30
|
-
|
|
31
|
-
from codrninja.core import AICode
|
|
32
|
-
from codrninja.agent import Agent
|
|
33
|
-
from codrninja.tools import ToolRegistry
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
# Milan's exact ASCII art for codrninja
|
|
37
|
-
CODRNINJA_LOGO = """
|
|
38
|
-
╔══════════════════════════════════════════════════════════════════════════════════════╗
|
|
39
|
-
║ ║
|
|
40
|
-
║ ██████╗ ██████╗ ██████╗ ██████╗ ███╗ ██╗██╗███╗ ██╗ ██╗ █████╗ ║
|
|
41
|
-
║ ██╔════╝██╔═══██╗██╔══██╗██╔══██╗████╗ ██║██║████╗ ██║ ██║██╔══██╗ ║
|
|
42
|
-
║ ██║ ██║ ██║██║ ██║██████╔╝██╔██╗ ██║██║██╔██╗ ██║ ██║███████║ ║
|
|
43
|
-
║ ██║ ██║ ██║██║ ██║██╔══██╗██║╚██╗██║██║██║╚██╗██║██ ██║██╔══██║ ║
|
|
44
|
-
║ ╚██████╗╚██████╔╝██████╔╝██║ ██║██║ ╚████║██║██║ ╚████║╚█████╔╝██║ ██║ ║
|
|
45
|
-
║ ╚═════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═══╝╚═╝╚═╝ ╚═══╝ ╚════╝ ╚═╝ ╚═╝ ║
|
|
46
|
-
║ ║
|
|
47
|
-
║ AI-first coding assistant for automation ║
|
|
48
|
-
║ ║
|
|
49
|
-
╚══════════════════════════════════════════════════════════════════════════════════════╝
|
|
50
|
-
"""
|
|
51
|
-
|
|
52
|
-
# Slash commands
|
|
53
|
-
SLASH_COMMANDS = [
|
|
54
|
-
"/clear", "/commit", "/context", "/edit", "/exec", "/exit",
|
|
55
|
-
"/explain", "/files", "/help", "/model", "/plan", "/read",
|
|
56
|
-
"/review", "/session", "/test", "/write",
|
|
57
|
-
]
|
|
58
|
-
|
|
59
|
-
COMMAND_DESCRIPTIONS = {
|
|
60
|
-
"/clear": "Clear screen",
|
|
61
|
-
"/commit": "Git commit changes",
|
|
62
|
-
"/context": "Show project context",
|
|
63
|
-
"/edit": "Edit file by replacement",
|
|
64
|
-
"/exec": "Execute shell command",
|
|
65
|
-
"/exit": "Exit codrninja",
|
|
66
|
-
"/explain": "Explain code or concept",
|
|
67
|
-
"/files": "List files in directory",
|
|
68
|
-
"/help": "Show detailed help",
|
|
69
|
-
"/model": "Show AI configuration",
|
|
70
|
-
"/plan": "Plan a feature or task",
|
|
71
|
-
"/read": "Read file",
|
|
72
|
-
"/review": "Review code for issues",
|
|
73
|
-
"/session": "Show session info",
|
|
74
|
-
"/test": "Run tests",
|
|
75
|
-
"/write": "Write to file",
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
class TUIPromptToolkit:
|
|
80
|
-
"""Terminal UI using prompt_toolkit for proper terminal handling."""
|
|
81
|
-
|
|
82
|
-
def __init__(self, ai: AICode, session_name: str):
|
|
83
|
-
self.ai = ai
|
|
84
|
-
self.session_name = session_name
|
|
85
|
-
self.agent = Agent(ai, session_name)
|
|
86
|
-
self.console = Console() if HAS_RICH else None
|
|
87
|
-
self.tools = ToolRegistry()
|
|
88
|
-
|
|
89
|
-
def start(self):
|
|
90
|
-
"""Start the TUI."""
|
|
91
|
-
if not HAS_PROMPT:
|
|
92
|
-
print("Error: prompt_toolkit not installed")
|
|
93
|
-
print("Run: pip3 install prompt_toolkit")
|
|
94
|
-
return
|
|
95
|
-
|
|
96
|
-
self._show_welcome()
|
|
97
|
-
self._run_app()
|
|
98
|
-
|
|
99
|
-
def _show_welcome(self):
|
|
100
|
-
"""Show welcome screen."""
|
|
101
|
-
if self.console:
|
|
102
|
-
for line in CODRNINJA_LOGO.strip().split('\n'):
|
|
103
|
-
self.console.print(f"[bold cyan]{line}[/bold cyan]")
|
|
104
|
-
self.console.print(f"\n Session: [bold yellow]{self.session_name}[/bold yellow]")
|
|
105
|
-
self.console.print(" Type / for commands\n")
|
|
106
|
-
else:
|
|
107
|
-
print(CODRNINJA_LOGO)
|
|
108
|
-
print(f"\n Session: {self.session_name}")
|
|
109
|
-
print(" Type / for commands\n")
|
|
110
|
-
|
|
111
|
-
def _run_app(self):
|
|
112
|
-
"""Run the prompt_toolkit application."""
|
|
113
|
-
kb = KeyBindings()
|
|
114
|
-
|
|
115
|
-
# State
|
|
116
|
-
suggestions = []
|
|
117
|
-
selected = 0
|
|
118
|
-
showing_suggestions = False
|
|
119
|
-
|
|
120
|
-
# Layout components
|
|
121
|
-
input_buffer = Buffer(multiline=False)
|
|
122
|
-
suggestion_text = FormattedTextControl("")
|
|
123
|
-
|
|
124
|
-
def update_suggestions():
|
|
125
|
-
nonlocal suggestions, selected, showing_suggestions
|
|
126
|
-
text = input_buffer.text
|
|
127
|
-
|
|
128
|
-
if text.startswith('/'):
|
|
129
|
-
partial = text[1:]
|
|
130
|
-
suggestions = [(cmd, COMMAND_DESCRIPTIONS.get(cmd, ""))
|
|
131
|
-
for cmd in SLASH_COMMANDS
|
|
132
|
-
if cmd[1:].startswith(partial)]
|
|
133
|
-
if suggestions:
|
|
134
|
-
selected = 0
|
|
135
|
-
showing_suggestions = True
|
|
136
|
-
self._render_suggestions(suggestion_text, suggestions, selected)
|
|
137
|
-
else:
|
|
138
|
-
showing_suggestions = False
|
|
139
|
-
suggestion_text.text = ""
|
|
140
|
-
else:
|
|
141
|
-
showing_suggestions = False
|
|
142
|
-
suggestion_text.text = ""
|
|
143
|
-
|
|
144
|
-
@kb.add('c-c')
|
|
145
|
-
@kb.add('c-d')
|
|
146
|
-
def _(event):
|
|
147
|
-
event.app.exit()
|
|
148
|
-
|
|
149
|
-
@kb.add('up')
|
|
150
|
-
def _(event):
|
|
151
|
-
nonlocal selected
|
|
152
|
-
if showing_suggestions and suggestions:
|
|
153
|
-
selected = max(0, selected - 1)
|
|
154
|
-
self._render_suggestions(suggestion_text, suggestions, selected)
|
|
155
|
-
|
|
156
|
-
@kb.add('down')
|
|
157
|
-
def _(event):
|
|
158
|
-
nonlocal selected
|
|
159
|
-
if showing_suggestions and suggestions:
|
|
160
|
-
selected = min(len(suggestions) - 1, selected + 1)
|
|
161
|
-
self._render_suggestions(suggestion_text, suggestions, selected)
|
|
162
|
-
|
|
163
|
-
@kb.add('tab')
|
|
164
|
-
def _(event):
|
|
165
|
-
nonlocal showing_suggestions
|
|
166
|
-
if showing_suggestions and suggestions:
|
|
167
|
-
cmd = suggestions[selected][0]
|
|
168
|
-
input_buffer.text = cmd + " "
|
|
169
|
-
input_buffer.cursor_position = len(input_buffer.text)
|
|
170
|
-
showing_suggestions = False
|
|
171
|
-
suggestion_text.text = ""
|
|
172
|
-
|
|
173
|
-
@kb.add('enter')
|
|
174
|
-
def _(event):
|
|
175
|
-
text = input_buffer.text.strip()
|
|
176
|
-
if not text:
|
|
177
|
-
return
|
|
178
|
-
|
|
179
|
-
showing_suggestions = False
|
|
180
|
-
suggestion_text.text = ""
|
|
181
|
-
|
|
182
|
-
# Print the input
|
|
183
|
-
if self.console:
|
|
184
|
-
self.console.print(f"\n[bold green]You[/bold green] {text}")
|
|
185
|
-
else:
|
|
186
|
-
print(f"\nYou: {text}")
|
|
187
|
-
|
|
188
|
-
# Process
|
|
189
|
-
if text.startswith('/'):
|
|
190
|
-
if not self._handle_command(text):
|
|
191
|
-
event.app.exit()
|
|
192
|
-
return
|
|
193
|
-
else:
|
|
194
|
-
self._process_message(text)
|
|
195
|
-
|
|
196
|
-
# Clear input
|
|
197
|
-
input_buffer.text = ""
|
|
198
|
-
input_buffer.cursor_position = 0
|
|
199
|
-
|
|
200
|
-
@kb.add('c-l')
|
|
201
|
-
def _(event):
|
|
202
|
-
os.system('clear' if os.name != 'nt' else 'cls')
|
|
203
|
-
self._show_welcome()
|
|
204
|
-
|
|
205
|
-
# Watch for text changes
|
|
206
|
-
def on_text_changed(_):
|
|
207
|
-
update_suggestions()
|
|
208
|
-
|
|
209
|
-
input_buffer.on_text_changed += on_text_changed
|
|
210
|
-
|
|
211
|
-
# Layout
|
|
212
|
-
input_window = Window(
|
|
213
|
-
content=BufferControl(buffer=input_buffer, focusable=True),
|
|
214
|
-
height=1,
|
|
215
|
-
dont_extend_height=True,
|
|
216
|
-
)
|
|
217
|
-
|
|
218
|
-
suggestion_window = Window(
|
|
219
|
-
content=suggestion_text,
|
|
220
|
-
height=10,
|
|
221
|
-
dont_extend_height=True,
|
|
222
|
-
)
|
|
223
|
-
|
|
224
|
-
root_container = HSplit([
|
|
225
|
-
input_window,
|
|
226
|
-
suggestion_window,
|
|
227
|
-
])
|
|
228
|
-
|
|
229
|
-
layout = Layout(root_container, focused_element=input_window)
|
|
230
|
-
|
|
231
|
-
style = Style.from_dict({
|
|
232
|
-
'suggestion': '#666666',
|
|
233
|
-
'selected': 'bg:#ffffff #000000',
|
|
234
|
-
})
|
|
235
|
-
|
|
236
|
-
app = Application(
|
|
237
|
-
layout=layout,
|
|
238
|
-
key_bindings=kb,
|
|
239
|
-
style=style,
|
|
240
|
-
full_screen=False,
|
|
241
|
-
mouse_support=False,
|
|
242
|
-
)
|
|
243
|
-
|
|
244
|
-
app.run()
|
|
245
|
-
|
|
246
|
-
print("\nGoodbye!\n")
|
|
247
|
-
|
|
248
|
-
def _render_suggestions(self, control, suggestions, selected):
|
|
249
|
-
"""Render the suggestion list."""
|
|
250
|
-
lines = []
|
|
251
|
-
for i, (cmd, desc) in enumerate(suggestions[:10]):
|
|
252
|
-
if i == selected:
|
|
253
|
-
lines.append(f" > {cmd:18} {desc}")
|
|
254
|
-
else:
|
|
255
|
-
lines.append(f" {cmd:18} {desc}")
|
|
256
|
-
control.text = "\n".join(lines)
|
|
257
|
-
|
|
258
|
-
def _process_message(self, message: str):
|
|
259
|
-
"""Process user message."""
|
|
260
|
-
print("Thinking...")
|
|
261
|
-
result = self.agent.run(message, auto_approve=False)
|
|
262
|
-
|
|
263
|
-
if not result['success']:
|
|
264
|
-
print(f"Error: {result.get('error', 'Unknown error')}")
|
|
265
|
-
return
|
|
266
|
-
|
|
267
|
-
print(f"\nAI: {result['response']}")
|
|
268
|
-
|
|
269
|
-
def _handle_command(self, command: str) -> bool:
|
|
270
|
-
"""Handle slash commands."""
|
|
271
|
-
parts = command.split()
|
|
272
|
-
cmd = parts[0].lower()
|
|
273
|
-
|
|
274
|
-
if cmd == '/exit':
|
|
275
|
-
return False
|
|
276
|
-
elif cmd == '/help':
|
|
277
|
-
print("\nCommands:")
|
|
278
|
-
for c, d in COMMAND_DESCRIPTIONS.items():
|
|
279
|
-
print(f" {c:16} {d}")
|
|
280
|
-
print()
|
|
281
|
-
elif cmd == '/clear':
|
|
282
|
-
os.system('clear' if os.name != 'nt' else 'cls')
|
|
283
|
-
self._show_welcome()
|
|
284
|
-
elif cmd == '/files':
|
|
285
|
-
result = self.tools.list_files(path=".", depth=2)
|
|
286
|
-
if result.success:
|
|
287
|
-
print(f"\n{result.output}")
|
|
288
|
-
else:
|
|
289
|
-
print(f"Error: {result.error}")
|
|
290
|
-
elif cmd == '/model':
|
|
291
|
-
print(f"\nProvider: {self.ai.config.default_provider}")
|
|
292
|
-
print(f"Model: {self.ai.config.default_model}")
|
|
293
|
-
print(f"Ollama URL: {self.ai.config.ollama_url}")
|
|
294
|
-
elif cmd == '/session':
|
|
295
|
-
print(f"\nSession: {self.session_name}")
|
|
296
|
-
elif cmd == '/exec':
|
|
297
|
-
if len(parts) > 1:
|
|
298
|
-
cmd_str = ' '.join(parts[1:])
|
|
299
|
-
result = self.tools.execute_command(cmd_str)
|
|
300
|
-
if result.success:
|
|
301
|
-
print(f"\n{result.output}")
|
|
302
|
-
else:
|
|
303
|
-
print(f"Error: {result.error}")
|
|
304
|
-
else:
|
|
305
|
-
print(f"Unknown command: {cmd}. Type /help for list.")
|
|
306
|
-
|
|
307
|
-
return True
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
def main():
|
|
311
|
-
if len(sys.argv) < 2:
|
|
312
|
-
print("Usage: codrninja-tui <session-name>")
|
|
313
|
-
sys.exit(1)
|
|
314
|
-
|
|
315
|
-
session_name = sys.argv[1]
|
|
316
|
-
ai = AICode()
|
|
317
|
-
tui = TUIPromptToolkit(ai, session_name)
|
|
318
|
-
tui.start()
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
if __name__ == "__main__":
|
|
322
|
-
main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|