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,131 @@
1
+ """Command palette 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
8
+ from textual.containers import Vertical
9
+ from textual.binding import Binding
10
+ from textual import on
11
+
12
+
13
+ class CommandPalette(ModalScreen[None]):
14
+ """Command palette modal with keyboard navigation."""
15
+
16
+ CSS = """
17
+ CommandPalette {
18
+ align: center middle;
19
+ }
20
+ #palette-container {
21
+ width: 50;
22
+ max-height: 30;
23
+ background: $surface;
24
+ border: tall $primary;
25
+ padding: 1 2;
26
+ }
27
+ #palette-title {
28
+ text-align: center;
29
+ text-style: bold;
30
+ color: $primary;
31
+ margin-bottom: 1;
32
+ }
33
+ #command-list {
34
+ height: auto;
35
+ max-height: 20;
36
+ }
37
+ """
38
+
39
+ BINDINGS = [
40
+ Binding("escape", "close", "Close", show=True),
41
+ ]
42
+
43
+ COMMANDS = [
44
+ ("connect", "Connect AI Provider", "🔌"),
45
+ ("model", "Select Model", "🤖"),
46
+ ("project", "Manage Projects", "📂"),
47
+ ("memory", "View Memory", "🧠"),
48
+ ("history", "Chat History", "📜"),
49
+ ("export", "Export Prompt", "📤"),
50
+ ("scan", "Scan Workspace", "🔍"),
51
+ ("settings", "Settings", "⚙"),
52
+ ("run", "Run Shell Command", "▶"),
53
+ ("read", "Read File", "📄"),
54
+ ("write", "Write / Create File", "✏"),
55
+ ("ls", "List Directory", "📁"),
56
+ ("tree", "Show File Tree", "🌲"),
57
+ ("git", "Git Commands", "🔀"),
58
+ ("pip", "Pip Commands", "📦"),
59
+ ("python", "Run Python", "🐍"),
60
+ ("clear", "Clear Chat", "🗑"),
61
+ ("exit", "Exit DevObin", "🚪"),
62
+ ]
63
+
64
+ def compose(self) -> ComposeResult:
65
+ with Vertical(id="palette-container"):
66
+ yield Static("DevObin Commands", id="palette-title")
67
+ labels = [f"{icon} {label}" for _, label, icon in self.COMMANDS]
68
+ yield OptionList(*labels, id="command-list")
69
+
70
+ def on_mount(self) -> None:
71
+ self.query_one("#command-list", OptionList).focus()
72
+
73
+ @on(OptionList.OptionSelected, "#command-list")
74
+ def on_option_selected(self, event: OptionList.OptionSelected) -> None:
75
+ """Handle command selection."""
76
+ idx = event.option_index
77
+ if 0 <= idx < len(self.COMMANDS):
78
+ cmd = self.COMMANDS[idx][0]
79
+ self.dismiss()
80
+ self.execute_command(cmd)
81
+
82
+ def execute_command(self, cmd: str) -> None:
83
+ """Execute the selected command."""
84
+ from devobin.ui.connect_modal import ConnectModal
85
+ from devobin.ui.model_modal import ModelModal
86
+ from devobin.ui.project_modal import ProjectModal
87
+ from devobin.ui.memory_modal import MemoryModal
88
+ from devobin.ui.history_modal import HistoryModal
89
+ from devobin.ui.export_modal import ExportModal
90
+ from devobin.ui.settings_modal import SettingsModal
91
+
92
+ modals = {
93
+ "connect": ConnectModal,
94
+ "model": ModelModal,
95
+ "project": ProjectModal,
96
+ "memory": MemoryModal,
97
+ "history": HistoryModal,
98
+ "export": ExportModal,
99
+ "settings": SettingsModal,
100
+ }
101
+
102
+ # Commands that go to the chat input
103
+ input_cmds = {
104
+ "run": "/run ", "read": "/read ", "write": "/write ",
105
+ "ls": "/ls ", "tree": "/tree ", "git": "/git ",
106
+ "pip": "/pip ", "python": "/python ", "npm": "/npm ",
107
+ "scan": "/scan",
108
+ }
109
+
110
+ if cmd == "clear":
111
+ screen = self.app.screen
112
+ if hasattr(screen, "cmd_clear"):
113
+ screen.cmd_clear("")
114
+ elif cmd == "exit":
115
+ self.app.exit()
116
+ elif cmd in modals:
117
+ self.app.push_screen(modals[cmd]())
118
+ elif cmd in input_cmds:
119
+ # Focus the chat input and pre-fill the command
120
+ try:
121
+ chat_screen = self.app.screen
122
+ if hasattr(chat_screen, "query_one"):
123
+ inp = chat_screen.query_one("#chat-input")
124
+ inp.value = input_cmds[cmd]
125
+ inp.focus()
126
+ except Exception:
127
+ pass
128
+
129
+ def action_close(self) -> None:
130
+ """Close the palette."""
131
+ self.dismiss()
@@ -0,0 +1,166 @@
1
+ """Connect 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, Select, 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
+
14
+
15
+ class ConnectModal(ModalScreen[None]):
16
+ """Modal for connecting AI provider."""
17
+
18
+ CSS = """
19
+ ConnectModal {
20
+ align: center middle;
21
+ }
22
+ #connect-container {
23
+ width: 60;
24
+ max-height: 35;
25
+ background: $surface;
26
+ border: tall $primary;
27
+ padding: 2;
28
+ }
29
+ #connect-title {
30
+ text-align: center;
31
+ text-style: bold;
32
+ color: $primary;
33
+ margin-bottom: 1;
34
+ }
35
+ .field-label {
36
+ color: $text-muted;
37
+ margin-top: 1;
38
+ }
39
+ #provider-select {
40
+ width: 100%;
41
+ }
42
+ #base-url-input, #api-key-input, #model-input {
43
+ width: 100%;
44
+ }
45
+ #save-btn {
46
+ width: 100%;
47
+ margin-top: 2;
48
+ }
49
+ #status-message {
50
+ margin-top: 1;
51
+ text-align: center;
52
+ }
53
+ .success {
54
+ color: $success;
55
+ }
56
+ .error {
57
+ color: $error;
58
+ }
59
+ """
60
+
61
+ BINDINGS = [
62
+ Binding("escape", "close", "Close", show=True),
63
+ ]
64
+
65
+ PROVIDERS = [
66
+ ("openai", "OpenAI"),
67
+ ("anthropic", "Anthropic Claude"),
68
+ ("google", "Google Gemini"),
69
+ ("ollama", "Ollama (Local)"),
70
+ ("openai-compatible", "OpenAI Compatible"),
71
+ ]
72
+
73
+ def __init__(self) -> None:
74
+ super().__init__()
75
+ self.config = get_config()
76
+
77
+ def compose(self) -> ComposeResult:
78
+ with Vertical(id="connect-container"):
79
+ yield Static("Connect AI Provider", id="connect-title")
80
+
81
+ yield Static("Provider:", classes="field-label")
82
+ yield Select(
83
+ [(label, value) for value, label in self.PROVIDERS],
84
+ id="provider-select",
85
+ prompt="Select provider...",
86
+ )
87
+
88
+ yield Static("Base URL:", classes="field-label")
89
+ yield Input(placeholder="https://api.openai.com/v1", id="base-url-input")
90
+
91
+ yield Static("API Key:", classes="field-label")
92
+ yield Input(placeholder="sk-...", password=True, id="api-key-input")
93
+
94
+ yield Static("Model:", classes="field-label")
95
+ yield Input(placeholder="gpt-4o", id="model-input")
96
+
97
+ yield Button("Save", id="save-btn", variant="primary")
98
+ yield Static("", id="status-message")
99
+
100
+ @on(Select.Changed, "#provider-select")
101
+ def on_provider_changed(self, event: Select.Changed) -> None:
102
+ """Update placeholder based on provider."""
103
+ provider = event.value
104
+ base_url_input = self.query_one("#base-url-input", Input)
105
+ model_input = self.query_one("#model-input", Input)
106
+
107
+ defaults = {
108
+ "openai": ("https://api.openai.com/v1", "gpt-4o"),
109
+ "anthropic": ("", "claude-sonnet-4-20250514"),
110
+ "google": ("", "gemini-2.0-flash"),
111
+ "ollama": ("http://localhost:11434", "llama3"),
112
+ "openai-compatible": ("", ""),
113
+ }
114
+
115
+ if provider in defaults:
116
+ url, model = defaults[provider]
117
+ base_url_input.placeholder = url
118
+ model_input.placeholder = model
119
+
120
+ @on(Button.Pressed, "#save-btn")
121
+ def on_save_pressed(self, event: Button.Pressed) -> None:
122
+ """Save provider configuration."""
123
+ provider = self.query_one("#provider-select", Select).value
124
+ if provider is Select.BLANK:
125
+ self.update_status("Please select a provider", "error")
126
+ return
127
+
128
+ base_url = self.query_one("#base-url-input", Input).value.strip()
129
+ api_key = self.query_one("#api-key-input", Input).value.strip()
130
+ model = self.query_one("#model-input", Input).value.strip()
131
+
132
+ if not api_key and provider != "ollama":
133
+ self.update_status("API key is required", "error")
134
+ return
135
+
136
+ if not model:
137
+ self.update_status("Model is required", "error")
138
+ return
139
+
140
+ provider_config = {
141
+ "type": provider,
142
+ "api_key": api_key,
143
+ "base_url": base_url or None,
144
+ "model": model,
145
+ }
146
+
147
+ self.config.add_provider(provider, provider_config)
148
+ self.config.active_provider = provider
149
+ self.config.active_model = model
150
+
151
+ self.update_status(f"Connected to {provider} — {model}", "success")
152
+ self.set_timer(0.9, self.dismiss)
153
+
154
+ def update_status(self, message: str, style: str) -> None:
155
+ """Update status message."""
156
+ from rich.markup import escape as _escape
157
+ status = self.query_one("#status-message", Static)
158
+ safe = _escape(str(message))
159
+ if style == "success":
160
+ status.update(f"[green]✓[/] {safe}")
161
+ else:
162
+ status.update(f"[red]✗[/] {safe}")
163
+
164
+ def action_close(self) -> None:
165
+ """Close the modal."""
166
+ self.dismiss()
@@ -0,0 +1,159 @@
1
+ """Export modal for DevObin AI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from textual.app import ComposeResult
7
+ from textual.screen import ModalScreen
8
+ from textual.widgets import Static, Button
9
+ from textual.containers import Vertical
10
+ from textual.binding import Binding
11
+ from textual import on
12
+ from rich.markup import escape
13
+
14
+ from devobin.config.settings import get_config
15
+ from devobin.storage.database import ProjectStorage
16
+ from devobin.core.analyzer import analyze_input
17
+ from devobin.core.researcher import research_analysis
18
+ from devobin.core.prompt_builder import build_prompt
19
+ from devobin.core.memory import MemoryManager
20
+
21
+
22
+ class ExportModal(ModalScreen[None]):
23
+ """Modal for exporting project prompt."""
24
+
25
+ CSS = """
26
+ ExportModal {
27
+ align: center middle;
28
+ }
29
+ #export-container {
30
+ width: 70;
31
+ max-height: 40;
32
+ background: $surface;
33
+ border: tall $primary;
34
+ padding: 2;
35
+ }
36
+ #export-title {
37
+ text-align: center;
38
+ text-style: bold;
39
+ color: $primary;
40
+ margin-bottom: 1;
41
+ }
42
+ #export-content {
43
+ height: auto;
44
+ max-height: 25;
45
+ overflow-y: auto;
46
+ }
47
+ #status-message {
48
+ margin-top: 1;
49
+ text-align: center;
50
+ }
51
+ #close-btn {
52
+ width: 100%;
53
+ margin-top: 1;
54
+ }
55
+ """
56
+
57
+ BINDINGS = [
58
+ Binding("escape", "close", "Close", show=True),
59
+ ]
60
+
61
+ def __init__(self, custom_filename: str = "", ai_response: str = "") -> None:
62
+ super().__init__()
63
+ self.config = get_config()
64
+ self._custom_filename = custom_filename
65
+ self._ai_response = ai_response
66
+
67
+ def compose(self) -> ComposeResult:
68
+ with Vertical(id="export-container"):
69
+ yield Static("Export Prompt", id="export-title")
70
+ yield Static("[dim]Generating...[/]", id="export-content")
71
+ yield Static("", id="status-message")
72
+ yield Button("Close", id="close-btn", variant="default")
73
+
74
+ def on_mount(self) -> None:
75
+ """Generate prompt after the modal is shown."""
76
+ self.set_timer(0.05, self.generate_prompt)
77
+
78
+ def generate_prompt(self) -> None:
79
+ """Generate the project prompt and save to workspace."""
80
+ project = self.config.get("active_project")
81
+ filename = "devobin_prompt.md"
82
+
83
+ # If we have the actual AI response, use it directly
84
+ if self._ai_response and self._ai_response.strip():
85
+ prompt_content = self._ai_response.strip()
86
+ else:
87
+ # Fallback to prompt_builder template
88
+ if not project:
89
+ self.update_status("No active project. Create one with /project.", "error")
90
+ self.query_one("#export-content", Static).update("[dim]Nothing to export.[/]")
91
+ return
92
+
93
+ storage = ProjectStorage(project)
94
+ history = storage.get_history()
95
+
96
+ if not history:
97
+ self.update_status("No conversation history yet.", "error")
98
+ self.query_one("#export-content", Static).update("[dim]Chat first, then export.[/]")
99
+ return
100
+
101
+ try:
102
+ user_msgs = " ".join(m["content"] for m in history if m["role"] == "user")
103
+ analysis = analyze_input(user_msgs)
104
+ research = research_analysis(analysis)
105
+
106
+ memory_ctx = MemoryManager(project).get_memory_context()
107
+
108
+ # Scan the workspace so the exported document is grounded
109
+ from devobin.core.scanner import scan_workspace
110
+ pm = scan_workspace()
111
+
112
+ filename, prompt_content = build_prompt(
113
+ analysis, research,
114
+ memory_context=memory_ctx,
115
+ project_name=self._custom_filename or project,
116
+ project_map=pm,
117
+ )
118
+ except Exception as e:
119
+ self.update_status(f"Export failed: {e}", "error")
120
+ return
121
+
122
+ try:
123
+ # Save to current working directory (where CLI was opened)
124
+ cwd = os.getcwd()
125
+ output_path = os.path.join(cwd, filename)
126
+ with open(output_path, "w", encoding="utf-8") as f:
127
+ f.write(prompt_content)
128
+ except Exception as e:
129
+ self.update_status(f"Export failed: {e}", "error")
130
+ return
131
+
132
+ content = self.query_one("#export-content", Static)
133
+ preview = prompt_content[:2000]
134
+ if len(prompt_content) > 2000:
135
+ preview += "\n\n... (truncated)"
136
+ content.update(escape(preview))
137
+
138
+ self.update_status(
139
+ f"Created: {output_path}",
140
+ "success",
141
+ )
142
+
143
+ def update_status(self, message: str, style: str) -> None:
144
+ """Update status message."""
145
+ status = self.query_one("#status-message", Static)
146
+ safe = escape(str(message))
147
+ if style == "success":
148
+ status.update(f"[green]✓[/] {safe}")
149
+ else:
150
+ status.update(f"[red]✗[/] {safe}")
151
+
152
+ @on(Button.Pressed, "#close-btn")
153
+ def on_close_pressed(self) -> None:
154
+ """Close the modal."""
155
+ self.dismiss()
156
+
157
+ def action_close(self) -> None:
158
+ """Close the modal."""
159
+ self.dismiss()
@@ -0,0 +1,137 @@
1
+ """History modal — shows past chat sessions for the active project."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+ from textual.app import ComposeResult
8
+ from textual.screen import ModalScreen
9
+ from textual.widgets import Static, Button, ListItem, ListView
10
+ from textual.containers import Vertical, Horizontal
11
+ from textual.binding import Binding
12
+ from textual import on
13
+ from rich.markup import escape
14
+
15
+ from devobin.config.settings import get_config
16
+ from devobin.storage.database import ProjectStorage
17
+
18
+
19
+ class HistoryModal(ModalScreen[None]):
20
+ """Modal that lists past chat sessions from the active project."""
21
+
22
+ CSS = """
23
+ HistoryModal {
24
+ align: center middle;
25
+ }
26
+ #hist-container {
27
+ width: 70;
28
+ height: 35;
29
+ background: $surface;
30
+ border: tall $primary;
31
+ padding: 1;
32
+ }
33
+ #hist-title {
34
+ text-align: center;
35
+ text-style: bold;
36
+ color: $primary;
37
+ height: 1;
38
+ margin-bottom: 1;
39
+ }
40
+ #hist-list {
41
+ height: 1fr;
42
+ overflow-y: auto;
43
+ }
44
+ #hist-list ListItem {
45
+ padding: 0 1;
46
+ }
47
+ #hist-detail {
48
+ height: 12;
49
+ overflow-y: auto;
50
+ border: solid $primary;
51
+ margin-top: 1;
52
+ padding: 1;
53
+ }
54
+ #hist-buttons {
55
+ height: auto;
56
+ margin-top: 1;
57
+ }
58
+ #hist-buttons Button {
59
+ margin: 0 1;
60
+ }
61
+ """
62
+
63
+ BINDINGS = [
64
+ Binding("escape", "close", "Close", show=True),
65
+ ]
66
+
67
+ def __init__(self) -> None:
68
+ super().__init__()
69
+ self.config = get_config()
70
+ self._project = self.config.get("active_project")
71
+ self._sessions: list[dict] = []
72
+ self._selected_index: int = -1
73
+
74
+ def compose(self) -> ComposeResult:
75
+ with Vertical(id="hist-container"):
76
+ yield Static("Chat History", id="hist-title")
77
+ yield ListView(id="hist-list")
78
+ yield Static("[dim]Select a session to view details[/]", id="hist-detail")
79
+ with Horizontal(id="hist-buttons"):
80
+ yield Button("Close", id="hist-close", variant="default")
81
+
82
+ def on_mount(self) -> None:
83
+ if not self._project:
84
+ self.query_one("#hist-title", Static).update(
85
+ "[dim]No active project. Use /project create first.[/]"
86
+ )
87
+ return
88
+
89
+ storage = ProjectStorage(self._project)
90
+ self._sessions = storage.list_sessions()
91
+
92
+ if not self._sessions:
93
+ self.query_one("#hist-title", Static).update(
94
+ "[dim]No sessions yet. Start chatting![/]"
95
+ )
96
+ return
97
+
98
+ lv = self.query_one("#hist-list", ListView)
99
+ for i, s in enumerate(self._sessions):
100
+ label = f"Session {i+1} — {s['count']} messages — {s['preview']}"
101
+ lv.append(ListItem(Static(escape(label))))
102
+
103
+ self.query_one("#hist-title", Static).update(
104
+ f"Chat History — {self._project} ({len(self._sessions)} sessions)"
105
+ )
106
+
107
+ @on(ListView.Selected)
108
+ def on_selected(self, event: ListView.Selected) -> None:
109
+ # Find the index by matching the item
110
+ lv = self.query_one("#hist-list", ListView)
111
+ idx = event.item_index if hasattr(event, "item_index") else -1
112
+ if idx < 0:
113
+ # Fallback: count items
114
+ for i, item in enumerate(lv.children):
115
+ if item is event.item:
116
+ idx = i
117
+ break
118
+
119
+ if 0 <= idx < len(self._sessions):
120
+ self._selected_index = idx
121
+ session = self._sessions[idx]
122
+ lines = []
123
+ for msg in session["messages"][:20]:
124
+ role = msg.get("role", "?")
125
+ content = msg.get("content", "")[:100]
126
+ lines.append(f"[bold]{role}[/bold]: {content}")
127
+ detail = "\n".join(lines)
128
+ if len(session["messages"]) > 20:
129
+ detail += f"\n\n... ({len(session['messages']) - 20} more messages)"
130
+ self.query_one("#hist-detail", Static).update(detail)
131
+
132
+ @on(Button.Pressed, "#hist-close")
133
+ def on_close_pressed(self) -> None:
134
+ self.dismiss()
135
+
136
+ def action_close(self) -> None:
137
+ self.dismiss()
@@ -0,0 +1,88 @@
1
+ """Memory 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, DataTable
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.core.memory import MemoryManager
14
+
15
+
16
+ class MemoryModal(ModalScreen[None]):
17
+ """Modal for viewing memories."""
18
+
19
+ CSS = """
20
+ MemoryModal {
21
+ align: center middle;
22
+ }
23
+ #memory-container {
24
+ width: 60;
25
+ max-height: 30;
26
+ background: $surface;
27
+ border: tall $primary;
28
+ padding: 2;
29
+ }
30
+ #memory-title {
31
+ text-align: center;
32
+ text-style: bold;
33
+ color: $primary;
34
+ margin-bottom: 1;
35
+ }
36
+ #memory-table {
37
+ height: auto;
38
+ max-height: 18;
39
+ }
40
+ #close-btn {
41
+ width: 100%;
42
+ margin-top: 1;
43
+ }
44
+ """
45
+
46
+ BINDINGS = [
47
+ Binding("escape", "close", "Close", show=True),
48
+ ]
49
+
50
+ def __init__(self) -> None:
51
+ super().__init__()
52
+ self.config = get_config()
53
+
54
+ def compose(self) -> ComposeResult:
55
+ with Vertical(id="memory-container"):
56
+ yield Static("Memory", id="memory-title")
57
+
58
+ table = DataTable(id="memory-table")
59
+ table.add_columns("Type", "Content")
60
+ yield table
61
+
62
+ yield Button("Close", id="close-btn", variant="default")
63
+
64
+ def on_mount(self) -> None:
65
+ """Load memories on mount."""
66
+ table = self.query_one("#memory-table", DataTable)
67
+ manager = MemoryManager()
68
+
69
+ global_mems = manager.get_global_memories()
70
+ project_mems = manager.get_project_memories()
71
+
72
+ for mem in global_mems[-10:]:
73
+ table.add_row("Global", mem.get("content", "")[:60])
74
+
75
+ for mem in project_mems[-10:]:
76
+ table.add_row("Project", mem.get("content", "")[:60])
77
+
78
+ if not global_mems and not project_mems:
79
+ table.add_row("-", "No memories saved yet")
80
+
81
+ @on(Button.Pressed, "#close-btn")
82
+ def on_close_pressed(self) -> None:
83
+ """Close the modal."""
84
+ self.dismiss()
85
+
86
+ def action_close(self) -> None:
87
+ """Close the modal."""
88
+ self.dismiss()