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.
Files changed (47) hide show
  1. devobin/__init__.py +4 -0
  2. devobin/app.py +42 -0
  3. devobin/cli/__init__.py +1 -0
  4. devobin/cli/chat.py +831 -0
  5. devobin/cli/commands.py +362 -0
  6. devobin/cli/slash_menu.py +74 -0
  7. devobin/cli/terminal.py +155 -0
  8. devobin/cli/ui.py +129 -0
  9. devobin/config/__init__.py +1 -0
  10. devobin/config/settings.py +124 -0
  11. devobin/core/__init__.py +1 -0
  12. devobin/core/analyzer.py +166 -0
  13. devobin/core/executor.py +244 -0
  14. devobin/core/memory.py +78 -0
  15. devobin/core/optimizer.py +106 -0
  16. devobin/core/prompt_builder.py +513 -0
  17. devobin/core/researcher.py +283 -0
  18. devobin/core/rtl.py +93 -0
  19. devobin/core/scanner.py +206 -0
  20. devobin/core/tools.py +283 -0
  21. devobin/core/validator.py +235 -0
  22. devobin/main.py +33 -0
  23. devobin/output/__init__.py +1 -0
  24. devobin/providers/__init__.py +85 -0
  25. devobin/providers/anthropic_provider.py +72 -0
  26. devobin/providers/google_provider.py +90 -0
  27. devobin/providers/ollama_provider.py +81 -0
  28. devobin/providers/openai_provider.py +102 -0
  29. devobin/storage/__init__.py +1 -0
  30. devobin/storage/database.py +207 -0
  31. devobin/ui/__init__.py +1 -0
  32. devobin/ui/ask_modal.py +96 -0
  33. devobin/ui/chat_screen.py +1029 -0
  34. devobin/ui/command_palette.py +131 -0
  35. devobin/ui/connect_modal.py +166 -0
  36. devobin/ui/export_modal.py +159 -0
  37. devobin/ui/history_modal.py +137 -0
  38. devobin/ui/memory_modal.py +88 -0
  39. devobin/ui/model_modal.py +143 -0
  40. devobin/ui/permission_modal.py +92 -0
  41. devobin/ui/project_modal.py +133 -0
  42. devobin/ui/settings_modal.py +89 -0
  43. devobin-1.0.0.dist-info/METADATA +364 -0
  44. devobin-1.0.0.dist-info/RECORD +47 -0
  45. devobin-1.0.0.dist-info/WHEEL +4 -0
  46. devobin-1.0.0.dist-info/entry_points.txt +2 -0
  47. 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()