devobin 1.0.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.
- devobin/__init__.py +4 -0
- devobin/app.py +42 -0
- devobin/cli/__init__.py +1 -0
- devobin/cli/chat.py +831 -0
- devobin/cli/commands.py +362 -0
- devobin/cli/slash_menu.py +74 -0
- devobin/cli/terminal.py +155 -0
- devobin/cli/ui.py +129 -0
- devobin/config/__init__.py +1 -0
- devobin/config/settings.py +124 -0
- devobin/core/__init__.py +1 -0
- devobin/core/analyzer.py +166 -0
- devobin/core/executor.py +244 -0
- devobin/core/memory.py +78 -0
- devobin/core/optimizer.py +106 -0
- devobin/core/prompt_builder.py +513 -0
- devobin/core/researcher.py +283 -0
- devobin/core/rtl.py +93 -0
- devobin/core/scanner.py +206 -0
- devobin/core/tools.py +283 -0
- devobin/core/validator.py +235 -0
- devobin/main.py +33 -0
- devobin/output/__init__.py +1 -0
- devobin/providers/__init__.py +85 -0
- devobin/providers/anthropic_provider.py +72 -0
- devobin/providers/google_provider.py +90 -0
- devobin/providers/ollama_provider.py +81 -0
- devobin/providers/openai_provider.py +102 -0
- devobin/storage/__init__.py +1 -0
- devobin/storage/database.py +207 -0
- devobin/ui/__init__.py +1 -0
- devobin/ui/ask_modal.py +96 -0
- devobin/ui/chat_screen.py +1029 -0
- devobin/ui/command_palette.py +131 -0
- devobin/ui/connect_modal.py +166 -0
- devobin/ui/export_modal.py +159 -0
- devobin/ui/history_modal.py +137 -0
- devobin/ui/memory_modal.py +88 -0
- devobin/ui/model_modal.py +143 -0
- devobin/ui/permission_modal.py +92 -0
- devobin/ui/project_modal.py +133 -0
- devobin/ui/settings_modal.py +89 -0
- devobin-1.0.0.dist-info/METADATA +364 -0
- devobin-1.0.0.dist-info/RECORD +47 -0
- devobin-1.0.0.dist-info/WHEEL +4 -0
- devobin-1.0.0.dist-info/entry_points.txt +2 -0
- devobin-1.0.0.dist-info/licenses/LICENSE +21 -0
devobin/cli/commands.py
ADDED
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
"""Slash command handler for DevObin AI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from devobin.cli.ui import (
|
|
8
|
+
console,
|
|
9
|
+
print_error,
|
|
10
|
+
print_help,
|
|
11
|
+
print_provider_table,
|
|
12
|
+
print_model_table,
|
|
13
|
+
print_success,
|
|
14
|
+
print_status,
|
|
15
|
+
print_warning,
|
|
16
|
+
)
|
|
17
|
+
from devobin.config.settings import get_config, CONFIG_DIR
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class CommandResult:
|
|
22
|
+
"""Result of processing a command."""
|
|
23
|
+
|
|
24
|
+
handled: bool = False
|
|
25
|
+
should_exit: bool = False
|
|
26
|
+
message: str = ""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
async def handle_command(command: str) -> CommandResult:
|
|
30
|
+
"""Process a slash command and return the result."""
|
|
31
|
+
parts = command.strip().split(maxsplit=1)
|
|
32
|
+
cmd = parts[0].lower() if parts else ""
|
|
33
|
+
args = parts[1] if len(parts) > 1 else ""
|
|
34
|
+
|
|
35
|
+
handlers = {
|
|
36
|
+
"/help": _cmd_help,
|
|
37
|
+
"/connect": _cmd_connect,
|
|
38
|
+
"/models": _cmd_models,
|
|
39
|
+
"/model": _cmd_model,
|
|
40
|
+
"/memory": _cmd_memory,
|
|
41
|
+
"/project": _cmd_project,
|
|
42
|
+
"/export": _cmd_export,
|
|
43
|
+
"/settings": _cmd_settings,
|
|
44
|
+
"/clear": _cmd_clear,
|
|
45
|
+
"/quit": _cmd_quit,
|
|
46
|
+
"/exit": _cmd_quit,
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
handler = handlers.get(cmd)
|
|
50
|
+
if handler:
|
|
51
|
+
return await handler(args)
|
|
52
|
+
|
|
53
|
+
print_error(f"Unknown command: {cmd}. Type /help to see available commands.")
|
|
54
|
+
return CommandResult(handled=True)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
async def _cmd_help(args: str) -> CommandResult:
|
|
58
|
+
print_help()
|
|
59
|
+
return CommandResult(handled=True)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
async def _cmd_connect(args: str) -> CommandResult:
|
|
63
|
+
"""Interactive provider connection wizard."""
|
|
64
|
+
from rich.prompt import Prompt, Confirm
|
|
65
|
+
|
|
66
|
+
config = get_config()
|
|
67
|
+
|
|
68
|
+
console.print()
|
|
69
|
+
console.print(" [bold cyan]AI Provider Setup[/]", style="bold")
|
|
70
|
+
console.print()
|
|
71
|
+
|
|
72
|
+
provider_types = ["openai", "anthropic", "google", "ollama", "openai-compatible"]
|
|
73
|
+
console.print(" Available provider types:")
|
|
74
|
+
for i, pt in enumerate(provider_types, 1):
|
|
75
|
+
console.print(f" {i}. {pt}")
|
|
76
|
+
console.print()
|
|
77
|
+
|
|
78
|
+
choice = Prompt.ask(" Select provider type", choices=[str(i) for i in range(1, 6)], default="1")
|
|
79
|
+
provider_type = provider_types[int(choice) - 1]
|
|
80
|
+
|
|
81
|
+
console.print()
|
|
82
|
+
name = Prompt.ask(" Provider name", default=provider_type)
|
|
83
|
+
api_key = Prompt.ask(" API key", password=True, default="")
|
|
84
|
+
|
|
85
|
+
base_url = ""
|
|
86
|
+
if provider_type == "openai-compatible":
|
|
87
|
+
base_url = Prompt.ask(" API base URL", default="https://api.openai.com/v1")
|
|
88
|
+
elif provider_type == "ollama":
|
|
89
|
+
base_url = Prompt.ask(" Ollama URL", default="http://localhost:11434")
|
|
90
|
+
|
|
91
|
+
# Get available models
|
|
92
|
+
from devobin.providers import AIProvider
|
|
93
|
+
|
|
94
|
+
temp_config = {
|
|
95
|
+
"api_key": api_key,
|
|
96
|
+
"base_url": base_url or None,
|
|
97
|
+
"model": "",
|
|
98
|
+
"type": provider_type,
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
# Try to get models from provider
|
|
102
|
+
model = ""
|
|
103
|
+
try:
|
|
104
|
+
if provider_type in ("openai", "openai-compatible"):
|
|
105
|
+
from devobin.providers.openai_provider import OpenAIProvider
|
|
106
|
+
temp_config["model"] = "gpt-4o"
|
|
107
|
+
p = OpenAIProvider(temp_config)
|
|
108
|
+
models = p.available_models()
|
|
109
|
+
elif provider_type == "anthropic":
|
|
110
|
+
from devobin.providers.anthropic_provider import AnthropicProvider
|
|
111
|
+
temp_config["model"] = "claude-sonnet-4-20250514"
|
|
112
|
+
p = AnthropicProvider(temp_config)
|
|
113
|
+
models = p.available_models()
|
|
114
|
+
elif provider_type == "google":
|
|
115
|
+
from devobin.providers.google_provider import GoogleProvider
|
|
116
|
+
temp_config["model"] = "gemini-2.0-flash"
|
|
117
|
+
p = GoogleProvider(temp_config)
|
|
118
|
+
models = p.available_models()
|
|
119
|
+
elif provider_type == "ollama":
|
|
120
|
+
from devobin.providers.ollama_provider import OllamaProvider
|
|
121
|
+
temp_config["model"] = "llama3"
|
|
122
|
+
p = OllamaProvider(temp_config)
|
|
123
|
+
models = p.available_models()
|
|
124
|
+
else:
|
|
125
|
+
models = []
|
|
126
|
+
except Exception:
|
|
127
|
+
models = []
|
|
128
|
+
|
|
129
|
+
if models:
|
|
130
|
+
console.print()
|
|
131
|
+
console.print(" Available models:")
|
|
132
|
+
for i, m in enumerate(models, 1):
|
|
133
|
+
console.print(f" {i}. {m}")
|
|
134
|
+
console.print()
|
|
135
|
+
model_choice = Prompt.ask(
|
|
136
|
+
" Select model (number or type name)",
|
|
137
|
+
default="1",
|
|
138
|
+
)
|
|
139
|
+
try:
|
|
140
|
+
idx = int(model_choice) - 1
|
|
141
|
+
model = models[idx] if 0 <= idx < len(models) else model_choice
|
|
142
|
+
except ValueError:
|
|
143
|
+
model = model_choice
|
|
144
|
+
else:
|
|
145
|
+
model = Prompt.ask(" Model name", default="gpt-4o")
|
|
146
|
+
|
|
147
|
+
# Save
|
|
148
|
+
provider_config = {
|
|
149
|
+
"type": provider_type,
|
|
150
|
+
"api_key": api_key,
|
|
151
|
+
"base_url": base_url or None,
|
|
152
|
+
"model": model,
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
config.add_provider(name, provider_config)
|
|
156
|
+
config.active_provider = name
|
|
157
|
+
config.active_model = model
|
|
158
|
+
|
|
159
|
+
console.print()
|
|
160
|
+
print_success(f"Connected to {name} ({provider_type}) — model: {model}")
|
|
161
|
+
console.print()
|
|
162
|
+
return CommandResult(handled=True)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
async def _cmd_models(args: str) -> CommandResult:
|
|
166
|
+
"""List available models from current provider."""
|
|
167
|
+
config = get_config()
|
|
168
|
+
providers = config.list_providers()
|
|
169
|
+
|
|
170
|
+
if not providers:
|
|
171
|
+
print_warning("No providers configured. Use /connect to set one up.")
|
|
172
|
+
return CommandResult(handled=True)
|
|
173
|
+
|
|
174
|
+
print_provider_table(providers)
|
|
175
|
+
return CommandResult(handled=True)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
async def _cmd_model(args: str) -> CommandResult:
|
|
179
|
+
"""Switch to a different model."""
|
|
180
|
+
config = get_config()
|
|
181
|
+
|
|
182
|
+
if not args:
|
|
183
|
+
current = config.active_model or "None"
|
|
184
|
+
print_status(f"Current model: [bold]{current}[/]")
|
|
185
|
+
return CommandResult(handled=True)
|
|
186
|
+
|
|
187
|
+
config.active_model = args.strip()
|
|
188
|
+
print_success(f"Switched to model: {args.strip()}")
|
|
189
|
+
return CommandResult(handled=True)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
async def _cmd_memory(args: str) -> CommandResult:
|
|
193
|
+
"""Show saved memories."""
|
|
194
|
+
from devobin.core.memory import MemoryManager
|
|
195
|
+
|
|
196
|
+
config = get_config()
|
|
197
|
+
manager = MemoryManager()
|
|
198
|
+
|
|
199
|
+
global_mems = manager.get_global_memories()
|
|
200
|
+
project_mems = manager.get_project_memories()
|
|
201
|
+
|
|
202
|
+
if not global_mems and not project_mems:
|
|
203
|
+
print_status("No memories saved yet. Chat with DevObin to build context.")
|
|
204
|
+
return CommandResult(handled=True)
|
|
205
|
+
|
|
206
|
+
if global_mems:
|
|
207
|
+
console.print()
|
|
208
|
+
console.print(" [bold cyan]Global Memories:[/]")
|
|
209
|
+
for m in global_mems[-15:]:
|
|
210
|
+
cat = m.get("category", "general")
|
|
211
|
+
console.print(f" [dim][{cat}][/dim] {m['content'][:100]}")
|
|
212
|
+
|
|
213
|
+
if project_mems:
|
|
214
|
+
console.print()
|
|
215
|
+
console.print(" [bold cyan]Project Memories:[/]")
|
|
216
|
+
for m in project_mems[-15:]:
|
|
217
|
+
cat = m.get("category", "general")
|
|
218
|
+
console.print(f" [dim][{cat}][/dim] {m['content'][:100]}")
|
|
219
|
+
|
|
220
|
+
console.print()
|
|
221
|
+
return CommandResult(handled=True)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
async def _cmd_project(args: str) -> CommandResult:
|
|
225
|
+
"""Manage projects."""
|
|
226
|
+
config = get_config()
|
|
227
|
+
parts = args.strip().split(maxsplit=1)
|
|
228
|
+
sub = parts[0].lower() if parts else ""
|
|
229
|
+
proj_name = parts[1].strip() if len(parts) > 1 else ""
|
|
230
|
+
|
|
231
|
+
if sub == "create":
|
|
232
|
+
if not proj_name:
|
|
233
|
+
print_error("Usage: /project create <name>")
|
|
234
|
+
return CommandResult(handled=True)
|
|
235
|
+
|
|
236
|
+
from devobin.storage.database import ProjectStorage
|
|
237
|
+
ProjectStorage(proj_name) # creates dirs
|
|
238
|
+
config.set("active_project", proj_name)
|
|
239
|
+
print_success(f"Created project: {proj_name}")
|
|
240
|
+
|
|
241
|
+
elif sub == "switch":
|
|
242
|
+
if not proj_name:
|
|
243
|
+
print_error("Usage: /project switch <name>")
|
|
244
|
+
return CommandResult(handled=True)
|
|
245
|
+
|
|
246
|
+
projects = [p.name for p in config.projects_dir().iterdir() if p.is_dir()]
|
|
247
|
+
if proj_name not in projects:
|
|
248
|
+
print_error(f"Project '{proj_name}' not found. Available: {', '.join(projects)}")
|
|
249
|
+
return CommandResult(handled=True)
|
|
250
|
+
|
|
251
|
+
config.set("active_project", proj_name)
|
|
252
|
+
print_success(f"Switched to project: {proj_name}")
|
|
253
|
+
|
|
254
|
+
elif sub == "list":
|
|
255
|
+
projects = [p.name for p in config.projects_dir().iterdir() if p.is_dir()]
|
|
256
|
+
if not projects:
|
|
257
|
+
print_status("No projects yet. Use /project create <name> to start.")
|
|
258
|
+
else:
|
|
259
|
+
console.print()
|
|
260
|
+
console.print(" [bold cyan]Projects:[/]")
|
|
261
|
+
active = config.get("active_project")
|
|
262
|
+
for p in sorted(projects):
|
|
263
|
+
marker = " [green]← active[/]" if p == active else ""
|
|
264
|
+
console.print(f" • {p}{marker}")
|
|
265
|
+
console.print()
|
|
266
|
+
|
|
267
|
+
else:
|
|
268
|
+
console.print(" [bold]Project Commands:[/]")
|
|
269
|
+
console.print(" /project create <name> — Create a new project")
|
|
270
|
+
console.print(" /project switch <name> — Switch to a project")
|
|
271
|
+
console.print(" /project list — List all projects")
|
|
272
|
+
|
|
273
|
+
return CommandResult(handled=True)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
async def _cmd_export(args: str) -> CommandResult:
|
|
277
|
+
"""Generate the project_prompt.md file."""
|
|
278
|
+
from devobin.cli.ui import render_markdown
|
|
279
|
+
|
|
280
|
+
config = get_config()
|
|
281
|
+
project = config.get("active_project")
|
|
282
|
+
|
|
283
|
+
if not project:
|
|
284
|
+
print_warning("No active project. Use /project create <name> first.")
|
|
285
|
+
return CommandResult(handled=True)
|
|
286
|
+
|
|
287
|
+
from devobin.storage.database import ProjectStorage
|
|
288
|
+
|
|
289
|
+
storage = ProjectStorage(project)
|
|
290
|
+
history = storage.get_history()
|
|
291
|
+
|
|
292
|
+
if not history:
|
|
293
|
+
print_warning("No conversation history to export.")
|
|
294
|
+
return CommandResult(handled=True)
|
|
295
|
+
|
|
296
|
+
# Re-analyze and build prompt from history
|
|
297
|
+
from devobin.core.analyzer import analyze_input
|
|
298
|
+
from devobin.core.researcher import research_analysis
|
|
299
|
+
from devobin.core.prompt_builder import build_prompt
|
|
300
|
+
|
|
301
|
+
# Combine all user messages for analysis
|
|
302
|
+
user_msgs = " ".join(m["content"] for m in history if m["role"] == "user")
|
|
303
|
+
analysis = analyze_input(user_msgs)
|
|
304
|
+
research = research_analysis(analysis)
|
|
305
|
+
|
|
306
|
+
from devobin.core.memory import MemoryManager
|
|
307
|
+
mm = MemoryManager(project)
|
|
308
|
+
memory_ctx = mm.get_memory_context()
|
|
309
|
+
|
|
310
|
+
from devobin.core.scanner import scan_workspace
|
|
311
|
+
pm = scan_workspace()
|
|
312
|
+
|
|
313
|
+
_, prompt_content = build_prompt(analysis, research, memory_context=memory_ctx, project_map=pm)
|
|
314
|
+
|
|
315
|
+
# Save to CWD with the canonical filename
|
|
316
|
+
import os
|
|
317
|
+
output_path = os.path.join(os.getcwd(), "devobin_prompt.md")
|
|
318
|
+
output_path.write_text(prompt_content, encoding="utf-8")
|
|
319
|
+
|
|
320
|
+
print_success(f"Generated: {output_path}")
|
|
321
|
+
console.print()
|
|
322
|
+
render_markdown(prompt_content[:2000])
|
|
323
|
+
if len(prompt_content) > 2000:
|
|
324
|
+
console.print("\n [dim]... (truncated preview)[/]")
|
|
325
|
+
console.print()
|
|
326
|
+
|
|
327
|
+
return CommandResult(handled=True)
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
async def _cmd_settings(args: str) -> CommandResult:
|
|
331
|
+
"""Show current settings."""
|
|
332
|
+
config = get_config()
|
|
333
|
+
|
|
334
|
+
console.print()
|
|
335
|
+
console.print(" [bold cyan]Current Settings:[/]")
|
|
336
|
+
console.print(f" Active provider: [bold]{config.active_provider or 'None'}[/]")
|
|
337
|
+
console.print(f" Active model: [bold]{config.active_model or 'None'}[/]")
|
|
338
|
+
console.print(f" Active project: [bold]{config.get('active_project') or 'None'}[/]")
|
|
339
|
+
console.print(f" Config dir: [dim]{CONFIG_DIR}[/]")
|
|
340
|
+
console.print()
|
|
341
|
+
return CommandResult(handled=True)
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
async def _cmd_clear(args: str) -> CommandResult:
|
|
345
|
+
"""Clear current conversation."""
|
|
346
|
+
from devobin.storage.database import ProjectStorage
|
|
347
|
+
config = get_config()
|
|
348
|
+
project = config.get("active_project")
|
|
349
|
+
if project:
|
|
350
|
+
storage = ProjectStorage(project)
|
|
351
|
+
storage.clear_history()
|
|
352
|
+
print_success("Conversation cleared.")
|
|
353
|
+
else:
|
|
354
|
+
print_warning("No active project to clear.")
|
|
355
|
+
return CommandResult(handled=True)
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
async def _cmd_quit(args: str) -> CommandResult:
|
|
359
|
+
console.print()
|
|
360
|
+
print_success("Goodbye! Happy coding!")
|
|
361
|
+
console.print()
|
|
362
|
+
return CommandResult(handled=True, should_exit=True)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Slash command menu system for DevObin AI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Callable
|
|
7
|
+
|
|
8
|
+
from devobin.cli.terminal import console, render_slash_menu
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class SlashCommand:
|
|
13
|
+
"""A slash command definition."""
|
|
14
|
+
|
|
15
|
+
name: str
|
|
16
|
+
description: str
|
|
17
|
+
handler: Callable[..., None]
|
|
18
|
+
requires_args: bool = False
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class SlashMenu:
|
|
22
|
+
"""Manages slash commands and their display."""
|
|
23
|
+
|
|
24
|
+
def __init__(self) -> None:
|
|
25
|
+
self._commands: dict[str, SlashCommand] = {}
|
|
26
|
+
|
|
27
|
+
def register(
|
|
28
|
+
self,
|
|
29
|
+
name: str,
|
|
30
|
+
description: str,
|
|
31
|
+
handler: Callable[..., None],
|
|
32
|
+
requires_args: bool = False,
|
|
33
|
+
) -> None:
|
|
34
|
+
"""Register a slash command."""
|
|
35
|
+
self._commands[name] = SlashCommand(
|
|
36
|
+
name=name,
|
|
37
|
+
description=description,
|
|
38
|
+
handler=handler,
|
|
39
|
+
requires_args=requires_args,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
def get_command(self, name: str) -> SlashCommand | None:
|
|
43
|
+
"""Get a command by name."""
|
|
44
|
+
return self._commands.get(name)
|
|
45
|
+
|
|
46
|
+
def show_menu(self) -> None:
|
|
47
|
+
"""Display the slash command menu."""
|
|
48
|
+
render_slash_menu()
|
|
49
|
+
|
|
50
|
+
def parse_command(self, input_text: str) -> tuple[str, str]:
|
|
51
|
+
"""Parse slash command input into (command, args)."""
|
|
52
|
+
parts = input_text.strip().split(maxsplit=1)
|
|
53
|
+
cmd = parts[0].lower() if parts else ""
|
|
54
|
+
args = parts[1] if len(parts) > 1 else ""
|
|
55
|
+
return cmd, args
|
|
56
|
+
|
|
57
|
+
def execute(self, input_text: str) -> bool:
|
|
58
|
+
"""Execute a slash command. Returns True if command was handled."""
|
|
59
|
+
cmd, args = self.parse_command(input_text)
|
|
60
|
+
|
|
61
|
+
if cmd == "/":
|
|
62
|
+
self.show_menu()
|
|
63
|
+
return True
|
|
64
|
+
|
|
65
|
+
command = self.get_command(cmd)
|
|
66
|
+
if command:
|
|
67
|
+
command.handler(args)
|
|
68
|
+
return True
|
|
69
|
+
|
|
70
|
+
return False
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def commands(self) -> dict[str, SlashCommand]:
|
|
74
|
+
return self._commands
|
devobin/cli/terminal.py
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""Terminal rendering for DevObin AI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from rich.console import Console
|
|
6
|
+
from rich.panel import Panel
|
|
7
|
+
from rich.table import Table
|
|
8
|
+
from rich.text import Text
|
|
9
|
+
from rich.columns import Columns
|
|
10
|
+
from rich import box
|
|
11
|
+
|
|
12
|
+
from devobin.config.settings import get_config
|
|
13
|
+
|
|
14
|
+
console = Console()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def render_banner() -> None:
|
|
18
|
+
"""Render the main DevObin banner."""
|
|
19
|
+
banner = Text()
|
|
20
|
+
banner.append(" ██████╗ ███████╗██╗ ██╗███████╗██╗ ██████╗ ██╗ ██╗\n", style="bold cyan")
|
|
21
|
+
banner.append(" ██╔══██╗██╔════╝██║ ██║██╔════╝██║ ██╔═══██╗██║ ██║\n", style="bold cyan")
|
|
22
|
+
banner.append(" ██║ ██║█████╗ ██║ ██║█████╗ ██║ ██║ ██║██║ █╗ ██║\n", style="bold cyan")
|
|
23
|
+
banner.append(" ██║ ██║██╔══╝ ╚██╗ ██╔╝██╔══╝ ██║ ██║ ██║██║███╗██║\n", style="bold cyan")
|
|
24
|
+
banner.append(" ██████╔╝███████╗ ╚████╔╝ ██║ ███████╗╚██████╔╝╚███╔███╔╝\n", style="bold cyan")
|
|
25
|
+
banner.append(" ╚═════╝ ╚══════╝ ╚═══╝ ╚═╝ ╚══════╝ ╚═════╝ ╚══╝╚══╝\n", style="bold cyan")
|
|
26
|
+
|
|
27
|
+
config = get_config()
|
|
28
|
+
model = config.active_model or "Not connected"
|
|
29
|
+
project = config.get("active_project") or "default"
|
|
30
|
+
|
|
31
|
+
status = Text()
|
|
32
|
+
status.append(f"\n Model: ", style="dim")
|
|
33
|
+
status.append(f"{model}\n", style="bold green")
|
|
34
|
+
status.append(f" Project: ", style="dim")
|
|
35
|
+
status.append(f"{project}\n", style="bold yellow")
|
|
36
|
+
status.append(f"\n Developer: mobin hasanghasemi", style="dim white")
|
|
37
|
+
status.append(f"\n GitHub: ", style="dim white")
|
|
38
|
+
status.append(f"https://github.com/mobinhasanghasemi", style="bold dim cyan")
|
|
39
|
+
|
|
40
|
+
content = Text()
|
|
41
|
+
content.append_text(banner)
|
|
42
|
+
content.append_text(status)
|
|
43
|
+
|
|
44
|
+
console.print(Panel(
|
|
45
|
+
content,
|
|
46
|
+
border_style="cyan",
|
|
47
|
+
padding=(0, 1),
|
|
48
|
+
box=box.DOUBLE,
|
|
49
|
+
))
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def render_slash_menu() -> None:
|
|
53
|
+
"""Render the beautiful slash command menu."""
|
|
54
|
+
table = Table(
|
|
55
|
+
show_header=False,
|
|
56
|
+
show_edge=False,
|
|
57
|
+
box=None,
|
|
58
|
+
padding=(0, 2),
|
|
59
|
+
)
|
|
60
|
+
table.add_column("Command", style="bold cyan", width=12)
|
|
61
|
+
table.add_column("Description", style="white")
|
|
62
|
+
|
|
63
|
+
commands = [
|
|
64
|
+
("/connect", "Connect AI provider"),
|
|
65
|
+
("/model", "Change active model"),
|
|
66
|
+
("/models", "List models"),
|
|
67
|
+
("/project", "Manage projects"),
|
|
68
|
+
("/memory", "View memory"),
|
|
69
|
+
("/export", "Generate prompt file"),
|
|
70
|
+
("/settings", "Settings"),
|
|
71
|
+
("/run", "Run shell command"),
|
|
72
|
+
("/read", "Read file contents"),
|
|
73
|
+
("/write", "Write / create file"),
|
|
74
|
+
("/ls", "List directory"),
|
|
75
|
+
("/tree", "Show file tree"),
|
|
76
|
+
("/git", "Git commands"),
|
|
77
|
+
("/pip", "Pip commands"),
|
|
78
|
+
("/python", "Run Python"),
|
|
79
|
+
("/clear", "Clear chat"),
|
|
80
|
+
("/exit", "Exit DevObin"),
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
for cmd, desc in commands:
|
|
84
|
+
table.add_row(cmd, desc)
|
|
85
|
+
|
|
86
|
+
console.print(Panel(
|
|
87
|
+
table,
|
|
88
|
+
title="[bold cyan]DevObin Commands[/]",
|
|
89
|
+
border_style="cyan",
|
|
90
|
+
box=box.ROUNDED,
|
|
91
|
+
padding=(1, 2),
|
|
92
|
+
))
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def render_action_menu(actions: list[tuple[str, str]]) -> None:
|
|
96
|
+
"""Render an action menu (like after generating context)."""
|
|
97
|
+
table = Table(
|
|
98
|
+
show_header=False,
|
|
99
|
+
show_edge=False,
|
|
100
|
+
box=None,
|
|
101
|
+
padding=(0, 1),
|
|
102
|
+
)
|
|
103
|
+
table.add_column("Choice", style="bold yellow", width=4)
|
|
104
|
+
table.add_column("Action", style="white")
|
|
105
|
+
|
|
106
|
+
for i, (action, desc) in enumerate(actions, 1):
|
|
107
|
+
table.add_row(f"[{i}]", f"{action} — {desc}")
|
|
108
|
+
|
|
109
|
+
console.print(Panel(
|
|
110
|
+
table,
|
|
111
|
+
title="[bold yellow]Actions[/]",
|
|
112
|
+
border_style="yellow",
|
|
113
|
+
box=box.ROUNDED,
|
|
114
|
+
padding=(0, 1),
|
|
115
|
+
))
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def render_status_bar() -> None:
|
|
119
|
+
"""Render the status bar with model and project info."""
|
|
120
|
+
config = get_config()
|
|
121
|
+
model = config.active_model or "No model"
|
|
122
|
+
project = config.get("active_project") or "No project"
|
|
123
|
+
|
|
124
|
+
status = Text()
|
|
125
|
+
status.append(" ╭────────────────────────────────────────────╮\n", style="dim")
|
|
126
|
+
status.append(f" │ Model: ", style="dim")
|
|
127
|
+
status.append(f"{model:<30}", style="bold green")
|
|
128
|
+
status.append("│\n", style="dim")
|
|
129
|
+
status.append(f" │ Project: ", style="dim")
|
|
130
|
+
status.append(f"{project:<28}", style="bold yellow")
|
|
131
|
+
status.append("│\n", style="dim")
|
|
132
|
+
status.append(" ╰────────────────────────────────────────────╯\n", style="dim")
|
|
133
|
+
|
|
134
|
+
console.print(status)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def render_divider() -> None:
|
|
138
|
+
"""Render a visual divider."""
|
|
139
|
+
console.print(" " + "─" * 44, style="dim")
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def render_input_prefix() -> Text:
|
|
143
|
+
"""Create the input prefix."""
|
|
144
|
+
prefix = Text()
|
|
145
|
+
prefix.append(" You ", style="bold cyan")
|
|
146
|
+
prefix.append("› ", style="bold white")
|
|
147
|
+
return prefix
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def render_ai_prefix() -> Text:
|
|
151
|
+
"""Create the AI response prefix."""
|
|
152
|
+
prefix = Text()
|
|
153
|
+
prefix.append(" DevObin ", style="bold green")
|
|
154
|
+
prefix.append("› ", style="bold white")
|
|
155
|
+
return prefix
|