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
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""Model selection modal for DevObin AI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from textual.app import ComposeResult
|
|
6
|
+
from textual.screen import ModalScreen
|
|
7
|
+
from textual.widgets import Static, OptionList, Button, Input
|
|
8
|
+
from textual.containers import Vertical
|
|
9
|
+
from textual.binding import Binding
|
|
10
|
+
from textual import on
|
|
11
|
+
|
|
12
|
+
from devobin.config.settings import get_config
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ModelModal(ModalScreen[None]):
|
|
16
|
+
"""Modal for selecting AI model with search."""
|
|
17
|
+
|
|
18
|
+
CSS = """
|
|
19
|
+
ModelModal {
|
|
20
|
+
align: center middle;
|
|
21
|
+
}
|
|
22
|
+
#model-container {
|
|
23
|
+
width: 60;
|
|
24
|
+
max-height: 35;
|
|
25
|
+
background: $surface;
|
|
26
|
+
border: tall $primary;
|
|
27
|
+
padding: 2;
|
|
28
|
+
}
|
|
29
|
+
#model-title {
|
|
30
|
+
text-align: center;
|
|
31
|
+
text-style: bold;
|
|
32
|
+
color: $primary;
|
|
33
|
+
margin-bottom: 1;
|
|
34
|
+
}
|
|
35
|
+
#current-model {
|
|
36
|
+
text-align: center;
|
|
37
|
+
color: $text-muted;
|
|
38
|
+
margin-bottom: 1;
|
|
39
|
+
}
|
|
40
|
+
#search-input {
|
|
41
|
+
width: 100%;
|
|
42
|
+
margin-bottom: 1;
|
|
43
|
+
}
|
|
44
|
+
#model-list {
|
|
45
|
+
height: auto;
|
|
46
|
+
max-height: 18;
|
|
47
|
+
}
|
|
48
|
+
#close-btn {
|
|
49
|
+
width: 100%;
|
|
50
|
+
margin-top: 1;
|
|
51
|
+
}
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
BINDINGS = [
|
|
55
|
+
Binding("escape", "close", "Close", show=True),
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
def __init__(self) -> None:
|
|
59
|
+
super().__init__()
|
|
60
|
+
self.config = get_config()
|
|
61
|
+
self.all_models: list[str] = []
|
|
62
|
+
|
|
63
|
+
def compose(self) -> ComposeResult:
|
|
64
|
+
with Vertical(id="model-container"):
|
|
65
|
+
yield Static("Select Model", id="model-title")
|
|
66
|
+
|
|
67
|
+
current = self.config.active_model or "None"
|
|
68
|
+
yield Static(f"Current: [bold green]{current}[/]", id="current-model")
|
|
69
|
+
|
|
70
|
+
yield Input(placeholder="Search models...", id="search-input")
|
|
71
|
+
|
|
72
|
+
self.all_models = self.get_models()
|
|
73
|
+
|
|
74
|
+
if self.all_models:
|
|
75
|
+
yield OptionList(*self.all_models, id="model-list")
|
|
76
|
+
else:
|
|
77
|
+
yield Static("[dim]No models available. Connect a provider first.[/]")
|
|
78
|
+
|
|
79
|
+
yield Button("Close", id="close-btn", variant="default")
|
|
80
|
+
|
|
81
|
+
def get_models(self) -> list[str]:
|
|
82
|
+
"""Get available models from current provider."""
|
|
83
|
+
provider_name = self.config.active_provider
|
|
84
|
+
if not provider_name:
|
|
85
|
+
return []
|
|
86
|
+
|
|
87
|
+
provider_cfg = self.config.get_provider(provider_name)
|
|
88
|
+
if not provider_cfg:
|
|
89
|
+
return []
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
provider_type = provider_cfg.get("type", provider_name)
|
|
93
|
+
|
|
94
|
+
if provider_type in ("openai", "openai-compatible"):
|
|
95
|
+
from devobin.providers.openai_provider import OpenAIProvider
|
|
96
|
+
provider = OpenAIProvider(provider_cfg)
|
|
97
|
+
return provider.available_models()
|
|
98
|
+
elif provider_type == "anthropic":
|
|
99
|
+
from devobin.providers.anthropic_provider import AnthropicProvider
|
|
100
|
+
provider = AnthropicProvider(provider_cfg)
|
|
101
|
+
return provider.available_models()
|
|
102
|
+
elif provider_type == "google":
|
|
103
|
+
from devobin.providers.google_provider import GoogleProvider
|
|
104
|
+
provider = GoogleProvider(provider_cfg)
|
|
105
|
+
return provider.available_models()
|
|
106
|
+
elif provider_type == "ollama":
|
|
107
|
+
from devobin.providers.ollama_provider import OllamaProvider
|
|
108
|
+
provider = OllamaProvider(provider_cfg)
|
|
109
|
+
return provider.available_models()
|
|
110
|
+
except Exception:
|
|
111
|
+
pass
|
|
112
|
+
|
|
113
|
+
return []
|
|
114
|
+
|
|
115
|
+
@on(Input.Changed, "#search-input")
|
|
116
|
+
def on_search_changed(self, event: Input.Changed) -> None:
|
|
117
|
+
"""Filter models based on search."""
|
|
118
|
+
try:
|
|
119
|
+
table = self.query_one("#model-list", OptionList)
|
|
120
|
+
except Exception:
|
|
121
|
+
return
|
|
122
|
+
query = event.value.lower()
|
|
123
|
+
table.clear_options()
|
|
124
|
+
for model in (m for m in self.all_models if query in m.lower()):
|
|
125
|
+
table.add_option(model)
|
|
126
|
+
|
|
127
|
+
@on(OptionList.OptionSelected, "#model-list")
|
|
128
|
+
def on_option_selected(self, event: OptionList.OptionSelected) -> None:
|
|
129
|
+
"""Handle model selection (works with filtered lists too)."""
|
|
130
|
+
model = str(event.option.prompt)
|
|
131
|
+
self.config.active_model = model
|
|
132
|
+
|
|
133
|
+
current = self.query_one("#current-model", Static)
|
|
134
|
+
current.update(f"Current: [bold green]{model}[/]")
|
|
135
|
+
|
|
136
|
+
@on(Button.Pressed, "#close-btn")
|
|
137
|
+
def on_close_pressed(self) -> None:
|
|
138
|
+
"""Close the modal."""
|
|
139
|
+
self.dismiss()
|
|
140
|
+
|
|
141
|
+
def action_close(self) -> None:
|
|
142
|
+
"""Close the modal."""
|
|
143
|
+
self.dismiss()
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Permission modal — asks the user before DevObin reads project files."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from textual.app import ComposeResult
|
|
6
|
+
from textual.screen import ModalScreen
|
|
7
|
+
from textual.widgets import Static, Button
|
|
8
|
+
from textual.containers import Vertical
|
|
9
|
+
from textual.binding import Binding
|
|
10
|
+
from textual import on
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class PermissionModal(ModalScreen[str | None]):
|
|
14
|
+
"""Modal that asks the user whether DevObin may read their project files.
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
"all" — allow reading all files for this session
|
|
18
|
+
"one" — ask before each file read
|
|
19
|
+
None — deny all file reads
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
CSS = """
|
|
23
|
+
PermissionModal {
|
|
24
|
+
align: center middle;
|
|
25
|
+
}
|
|
26
|
+
#perm-container {
|
|
27
|
+
width: 55;
|
|
28
|
+
max-height: 25;
|
|
29
|
+
background: $surface;
|
|
30
|
+
border: tall $primary;
|
|
31
|
+
padding: 2;
|
|
32
|
+
}
|
|
33
|
+
#perm-title {
|
|
34
|
+
text-align: center;
|
|
35
|
+
text-style: bold;
|
|
36
|
+
color: $primary;
|
|
37
|
+
margin-bottom: 1;
|
|
38
|
+
}
|
|
39
|
+
#perm-body {
|
|
40
|
+
height: auto;
|
|
41
|
+
max-height: 12;
|
|
42
|
+
overflow-y: auto;
|
|
43
|
+
margin-bottom: 1;
|
|
44
|
+
color: $text;
|
|
45
|
+
}
|
|
46
|
+
#perm-buttons {
|
|
47
|
+
height: auto;
|
|
48
|
+
margin-top: 1;
|
|
49
|
+
}
|
|
50
|
+
#perm-buttons Button {
|
|
51
|
+
margin: 0 1;
|
|
52
|
+
width: 1fr;
|
|
53
|
+
}
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
BINDINGS = [
|
|
57
|
+
Binding("escape", "deny", "Deny", show=True),
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
def __init__(self, file_count: int = 0) -> None:
|
|
61
|
+
super().__init__()
|
|
62
|
+
self._file_count = file_count
|
|
63
|
+
|
|
64
|
+
def compose(self) -> ComposeResult:
|
|
65
|
+
with Vertical(id="perm-container"):
|
|
66
|
+
yield Static("File Access Permission", id="perm-title")
|
|
67
|
+
body = (
|
|
68
|
+
f"DevObin found **{self._file_count} files** in your project.\n\n"
|
|
69
|
+
"To build an accurate engineering prompt, it needs to read\n"
|
|
70
|
+
"the key files and understand the architecture.\n\n"
|
|
71
|
+
"Allow file access?"
|
|
72
|
+
)
|
|
73
|
+
yield Static(body, id="perm-body")
|
|
74
|
+
with Vertical(id="perm-buttons"):
|
|
75
|
+
yield Button("Yes — Read All Files", id="perm-all", variant="primary")
|
|
76
|
+
yield Button("Ask Before Each File", id="perm-one", variant="default")
|
|
77
|
+
yield Button("No — Skip File Reading", id="perm-deny", variant="default")
|
|
78
|
+
|
|
79
|
+
@on(Button.Pressed, "#perm-all")
|
|
80
|
+
def on_all_pressed(self) -> None:
|
|
81
|
+
self.dismiss("all")
|
|
82
|
+
|
|
83
|
+
@on(Button.Pressed, "#perm-one")
|
|
84
|
+
def on_one_pressed(self) -> None:
|
|
85
|
+
self.dismiss("one")
|
|
86
|
+
|
|
87
|
+
@on(Button.Pressed, "#perm-deny")
|
|
88
|
+
def on_deny_pressed(self) -> None:
|
|
89
|
+
self.dismiss(None)
|
|
90
|
+
|
|
91
|
+
def action_deny(self) -> None:
|
|
92
|
+
self.dismiss(None)
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""Project management modal for DevObin AI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from textual.app import ComposeResult
|
|
6
|
+
from textual.screen import ModalScreen
|
|
7
|
+
from textual.widgets import Static, Input, OptionList, Button
|
|
8
|
+
from textual.containers import Vertical
|
|
9
|
+
from textual.binding import Binding
|
|
10
|
+
from textual import on
|
|
11
|
+
|
|
12
|
+
from devobin.config.settings import get_config
|
|
13
|
+
from devobin.storage.database import ProjectStorage
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ProjectModal(ModalScreen[None]):
|
|
17
|
+
"""Modal for managing projects."""
|
|
18
|
+
|
|
19
|
+
CSS = """
|
|
20
|
+
ProjectModal {
|
|
21
|
+
align: center middle;
|
|
22
|
+
}
|
|
23
|
+
#project-container {
|
|
24
|
+
width: 50;
|
|
25
|
+
max-height: 30;
|
|
26
|
+
background: $surface;
|
|
27
|
+
border: tall $primary;
|
|
28
|
+
padding: 2;
|
|
29
|
+
}
|
|
30
|
+
#project-title {
|
|
31
|
+
text-align: center;
|
|
32
|
+
text-style: bold;
|
|
33
|
+
color: $primary;
|
|
34
|
+
margin-bottom: 1;
|
|
35
|
+
}
|
|
36
|
+
#project-list {
|
|
37
|
+
height: auto;
|
|
38
|
+
max-height: 12;
|
|
39
|
+
}
|
|
40
|
+
#new-project-input {
|
|
41
|
+
width: 100%;
|
|
42
|
+
margin-top: 1;
|
|
43
|
+
}
|
|
44
|
+
#create-btn {
|
|
45
|
+
width: 100%;
|
|
46
|
+
margin-top: 1;
|
|
47
|
+
}
|
|
48
|
+
#close-btn {
|
|
49
|
+
width: 100%;
|
|
50
|
+
margin-top: 1;
|
|
51
|
+
}
|
|
52
|
+
#status-message {
|
|
53
|
+
margin-top: 1;
|
|
54
|
+
text-align: center;
|
|
55
|
+
}
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
BINDINGS = [
|
|
59
|
+
Binding("escape", "close", "Close", show=True),
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
def __init__(self) -> None:
|
|
63
|
+
super().__init__()
|
|
64
|
+
self.config = get_config()
|
|
65
|
+
self._project_names: list[str] = []
|
|
66
|
+
|
|
67
|
+
def compose(self) -> ComposeResult:
|
|
68
|
+
with Vertical(id="project-container"):
|
|
69
|
+
yield Static("Manage Projects", id="project-title")
|
|
70
|
+
|
|
71
|
+
projects = self.get_projects()
|
|
72
|
+
active = self.config.get("active_project")
|
|
73
|
+
|
|
74
|
+
if projects:
|
|
75
|
+
self._project_names = sorted(projects)
|
|
76
|
+
labels = [
|
|
77
|
+
f"{'→ ' if p == active else ''}{p}"
|
|
78
|
+
for p in self._project_names
|
|
79
|
+
]
|
|
80
|
+
yield OptionList(*labels, id="project-list")
|
|
81
|
+
else:
|
|
82
|
+
yield Static("[dim]No projects yet.[/]")
|
|
83
|
+
|
|
84
|
+
yield Static("New Project:", classes="field-label")
|
|
85
|
+
yield Input(placeholder="project-name", id="new-project-input")
|
|
86
|
+
yield Button("Create & Switch", id="create-btn", variant="primary")
|
|
87
|
+
yield Button("Close", id="close-btn", variant="default")
|
|
88
|
+
yield Static("", id="status-message")
|
|
89
|
+
|
|
90
|
+
def get_projects(self) -> list[str]:
|
|
91
|
+
"""Get list of projects."""
|
|
92
|
+
projects_dir = self.config.projects_dir()
|
|
93
|
+
return [p.name for p in projects_dir.iterdir() if p.is_dir()]
|
|
94
|
+
|
|
95
|
+
@on(OptionList.OptionSelected, "#project-list")
|
|
96
|
+
def on_option_selected(self, event: OptionList.OptionSelected) -> None:
|
|
97
|
+
"""Switch to selected project."""
|
|
98
|
+
idx = event.option_index
|
|
99
|
+
if 0 <= idx < len(self._project_names):
|
|
100
|
+
project = self._project_names[idx]
|
|
101
|
+
self.config.set("active_project", project)
|
|
102
|
+
self.update_status(f"Switched to: {project}", "success")
|
|
103
|
+
|
|
104
|
+
@on(Button.Pressed, "#create-btn")
|
|
105
|
+
def on_create_pressed(self) -> None:
|
|
106
|
+
"""Create new project."""
|
|
107
|
+
name = self.query_one("#new-project-input", Input).value.strip()
|
|
108
|
+
if not name:
|
|
109
|
+
self.update_status("Enter a project name", "error")
|
|
110
|
+
return
|
|
111
|
+
|
|
112
|
+
ProjectStorage(name)
|
|
113
|
+
self.config.set("active_project", name)
|
|
114
|
+
self.update_status(f"Created: {name}", "success")
|
|
115
|
+
|
|
116
|
+
@on(Button.Pressed, "#close-btn")
|
|
117
|
+
def on_close_pressed(self) -> None:
|
|
118
|
+
"""Close the modal."""
|
|
119
|
+
self.dismiss()
|
|
120
|
+
|
|
121
|
+
def update_status(self, message: str, style: str) -> None:
|
|
122
|
+
"""Update status message."""
|
|
123
|
+
from rich.markup import escape as _escape
|
|
124
|
+
status = self.query_one("#status-message", Static)
|
|
125
|
+
safe = _escape(str(message))
|
|
126
|
+
if style == "success":
|
|
127
|
+
status.update(f"[green]✓[/] {safe}")
|
|
128
|
+
else:
|
|
129
|
+
status.update(f"[red]✗[/] {safe}")
|
|
130
|
+
|
|
131
|
+
def action_close(self) -> None:
|
|
132
|
+
"""Close the modal."""
|
|
133
|
+
self.dismiss()
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Settings modal for DevObin AI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from textual.app import ComposeResult
|
|
6
|
+
from textual.screen import ModalScreen
|
|
7
|
+
from textual.widgets import Static, Button
|
|
8
|
+
from textual.containers import Vertical
|
|
9
|
+
from textual.binding import Binding
|
|
10
|
+
from textual import on
|
|
11
|
+
|
|
12
|
+
from devobin.config.settings import get_config, CONFIG_DIR
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class SettingsModal(ModalScreen[None]):
|
|
16
|
+
"""Modal for viewing settings."""
|
|
17
|
+
|
|
18
|
+
CSS = """
|
|
19
|
+
SettingsModal {
|
|
20
|
+
align: center middle;
|
|
21
|
+
}
|
|
22
|
+
#settings-container {
|
|
23
|
+
width: 50;
|
|
24
|
+
max-height: 25;
|
|
25
|
+
background: $surface;
|
|
26
|
+
border: tall $primary;
|
|
27
|
+
padding: 2;
|
|
28
|
+
}
|
|
29
|
+
#settings-title {
|
|
30
|
+
text-align: center;
|
|
31
|
+
text-style: bold;
|
|
32
|
+
color: $primary;
|
|
33
|
+
margin-bottom: 1;
|
|
34
|
+
}
|
|
35
|
+
.setting-row {
|
|
36
|
+
margin: 0 0 1 0;
|
|
37
|
+
}
|
|
38
|
+
.setting-label {
|
|
39
|
+
color: $text-muted;
|
|
40
|
+
}
|
|
41
|
+
.setting-value {
|
|
42
|
+
color: $text;
|
|
43
|
+
text-style: bold;
|
|
44
|
+
}
|
|
45
|
+
#close-btn {
|
|
46
|
+
width: 100%;
|
|
47
|
+
margin-top: 2;
|
|
48
|
+
}
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
BINDINGS = [
|
|
52
|
+
Binding("escape", "close", "Close", show=True),
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
def __init__(self) -> None:
|
|
56
|
+
super().__init__()
|
|
57
|
+
self.config = get_config()
|
|
58
|
+
|
|
59
|
+
def compose(self) -> ComposeResult:
|
|
60
|
+
with Vertical(id="settings-container"):
|
|
61
|
+
yield Static("Settings", id="settings-title")
|
|
62
|
+
|
|
63
|
+
yield Static(
|
|
64
|
+
f"[dim]Provider:[/] [bold]{self.config.active_provider or 'None'}[/]",
|
|
65
|
+
classes="setting-row",
|
|
66
|
+
)
|
|
67
|
+
yield Static(
|
|
68
|
+
f"[dim]Model:[/] [bold]{self.config.active_model or 'None'}[/]",
|
|
69
|
+
classes="setting-row",
|
|
70
|
+
)
|
|
71
|
+
yield Static(
|
|
72
|
+
f"[dim]Project:[/] [bold]{self.config.get('active_project') or 'None'}[/]",
|
|
73
|
+
classes="setting-row",
|
|
74
|
+
)
|
|
75
|
+
yield Static(
|
|
76
|
+
f"[dim]Config:[/] [bold]{CONFIG_DIR}[/]",
|
|
77
|
+
classes="setting-row",
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
yield Button("Close", id="close-btn", variant="default")
|
|
81
|
+
|
|
82
|
+
@on(Button.Pressed, "#close-btn")
|
|
83
|
+
def on_close_pressed(self) -> None:
|
|
84
|
+
"""Close the modal."""
|
|
85
|
+
self.dismiss()
|
|
86
|
+
|
|
87
|
+
def action_close(self) -> None:
|
|
88
|
+
"""Close the modal."""
|
|
89
|
+
self.dismiss()
|