code-puppy 0.0.97__py3-none-any.whl → 0.0.118__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 (81) hide show
  1. code_puppy/__init__.py +2 -5
  2. code_puppy/__main__.py +10 -0
  3. code_puppy/agent.py +125 -40
  4. code_puppy/agent_prompts.py +30 -24
  5. code_puppy/callbacks.py +152 -0
  6. code_puppy/command_line/command_handler.py +359 -0
  7. code_puppy/command_line/load_context_completion.py +59 -0
  8. code_puppy/command_line/model_picker_completion.py +14 -21
  9. code_puppy/command_line/motd.py +44 -28
  10. code_puppy/command_line/prompt_toolkit_completion.py +42 -23
  11. code_puppy/config.py +266 -26
  12. code_puppy/http_utils.py +122 -0
  13. code_puppy/main.py +570 -383
  14. code_puppy/message_history_processor.py +195 -104
  15. code_puppy/messaging/__init__.py +46 -0
  16. code_puppy/messaging/message_queue.py +288 -0
  17. code_puppy/messaging/queue_console.py +293 -0
  18. code_puppy/messaging/renderers.py +305 -0
  19. code_puppy/messaging/spinner/__init__.py +55 -0
  20. code_puppy/messaging/spinner/console_spinner.py +200 -0
  21. code_puppy/messaging/spinner/spinner_base.py +66 -0
  22. code_puppy/messaging/spinner/textual_spinner.py +97 -0
  23. code_puppy/model_factory.py +73 -105
  24. code_puppy/plugins/__init__.py +32 -0
  25. code_puppy/reopenable_async_client.py +225 -0
  26. code_puppy/state_management.py +60 -21
  27. code_puppy/summarization_agent.py +56 -35
  28. code_puppy/token_utils.py +7 -9
  29. code_puppy/tools/__init__.py +1 -4
  30. code_puppy/tools/command_runner.py +187 -32
  31. code_puppy/tools/common.py +44 -35
  32. code_puppy/tools/file_modifications.py +335 -118
  33. code_puppy/tools/file_operations.py +368 -95
  34. code_puppy/tools/token_check.py +27 -11
  35. code_puppy/tools/tools_content.py +53 -0
  36. code_puppy/tui/__init__.py +10 -0
  37. code_puppy/tui/app.py +1050 -0
  38. code_puppy/tui/components/__init__.py +21 -0
  39. code_puppy/tui/components/chat_view.py +512 -0
  40. code_puppy/tui/components/command_history_modal.py +218 -0
  41. code_puppy/tui/components/copy_button.py +139 -0
  42. code_puppy/tui/components/custom_widgets.py +58 -0
  43. code_puppy/tui/components/input_area.py +167 -0
  44. code_puppy/tui/components/sidebar.py +309 -0
  45. code_puppy/tui/components/status_bar.py +182 -0
  46. code_puppy/tui/messages.py +27 -0
  47. code_puppy/tui/models/__init__.py +8 -0
  48. code_puppy/tui/models/chat_message.py +25 -0
  49. code_puppy/tui/models/command_history.py +89 -0
  50. code_puppy/tui/models/enums.py +24 -0
  51. code_puppy/tui/screens/__init__.py +13 -0
  52. code_puppy/tui/screens/help.py +130 -0
  53. code_puppy/tui/screens/settings.py +256 -0
  54. code_puppy/tui/screens/tools.py +74 -0
  55. code_puppy/tui/tests/__init__.py +1 -0
  56. code_puppy/tui/tests/test_chat_message.py +28 -0
  57. code_puppy/tui/tests/test_chat_view.py +88 -0
  58. code_puppy/tui/tests/test_command_history.py +89 -0
  59. code_puppy/tui/tests/test_copy_button.py +191 -0
  60. code_puppy/tui/tests/test_custom_widgets.py +27 -0
  61. code_puppy/tui/tests/test_disclaimer.py +27 -0
  62. code_puppy/tui/tests/test_enums.py +15 -0
  63. code_puppy/tui/tests/test_file_browser.py +60 -0
  64. code_puppy/tui/tests/test_help.py +38 -0
  65. code_puppy/tui/tests/test_history_file_reader.py +107 -0
  66. code_puppy/tui/tests/test_input_area.py +33 -0
  67. code_puppy/tui/tests/test_settings.py +44 -0
  68. code_puppy/tui/tests/test_sidebar.py +33 -0
  69. code_puppy/tui/tests/test_sidebar_history.py +153 -0
  70. code_puppy/tui/tests/test_sidebar_history_navigation.py +132 -0
  71. code_puppy/tui/tests/test_status_bar.py +54 -0
  72. code_puppy/tui/tests/test_timestamped_history.py +52 -0
  73. code_puppy/tui/tests/test_tools.py +82 -0
  74. code_puppy/version_checker.py +26 -3
  75. {code_puppy-0.0.97.dist-info → code_puppy-0.0.118.dist-info}/METADATA +9 -2
  76. code_puppy-0.0.118.dist-info/RECORD +86 -0
  77. code_puppy-0.0.97.dist-info/RECORD +0 -32
  78. {code_puppy-0.0.97.data → code_puppy-0.0.118.data}/data/code_puppy/models.json +0 -0
  79. {code_puppy-0.0.97.dist-info → code_puppy-0.0.118.dist-info}/WHEEL +0 -0
  80. {code_puppy-0.0.97.dist-info → code_puppy-0.0.118.dist-info}/entry_points.txt +0 -0
  81. {code_puppy-0.0.97.dist-info → code_puppy-0.0.118.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,309 @@
1
+ """
2
+ Sidebar component with history tab.
3
+ """
4
+
5
+ import time
6
+
7
+ from textual import on
8
+ from textual.app import ComposeResult
9
+ from textual.containers import Container
10
+ from textual.events import Key
11
+ from textual.widgets import Label, ListItem, ListView, TabbedContent, TabPane
12
+
13
+ from ..components.command_history_modal import CommandHistoryModal
14
+
15
+ # Import the shared message class and history reader
16
+ from ..models.command_history import HistoryFileReader
17
+
18
+
19
+ class Sidebar(Container):
20
+ """Sidebar with session history."""
21
+
22
+ def __init__(self, **kwargs):
23
+ super().__init__(**kwargs)
24
+ # Double-click detection variables
25
+ self._last_click_time = 0
26
+ self._last_clicked_item = None
27
+ self._double_click_threshold = 0.5 # 500ms for double-click
28
+
29
+ # Initialize history reader
30
+ self.history_reader = HistoryFileReader()
31
+
32
+ # Current index for history navigation - centralized reference
33
+ self.current_history_index = 0
34
+ self.history_entries = []
35
+
36
+ DEFAULT_CSS = """
37
+ Sidebar {
38
+ dock: left;
39
+ width: 30;
40
+ min-width: 20;
41
+ max-width: 50;
42
+ background: $surface;
43
+ border-right: solid $primary;
44
+ display: none;
45
+ }
46
+
47
+ #sidebar-tabs {
48
+ height: 1fr;
49
+ }
50
+
51
+ #history-list {
52
+ height: 1fr;
53
+ }
54
+
55
+ .history-interactive {
56
+ color: #34d399;
57
+ }
58
+
59
+ .history-tui {
60
+ color: #60a5fa;
61
+ }
62
+
63
+ .history-system {
64
+ color: #fbbf24;
65
+ text-style: italic;
66
+ }
67
+
68
+ .history-command {
69
+ /* Use default text color from theme */
70
+ }
71
+
72
+ .history-generic {
73
+ color: #d1d5db;
74
+ }
75
+
76
+ .history-empty {
77
+ color: #6b7280;
78
+ text-style: italic;
79
+ }
80
+
81
+ .history-error {
82
+ color: #ef4444;
83
+ }
84
+
85
+ .file-item {
86
+ color: #d1d5db;
87
+ }
88
+ """
89
+
90
+ def compose(self) -> ComposeResult:
91
+ """Create the sidebar layout with tabs."""
92
+ with TabbedContent(id="sidebar-tabs"):
93
+ with TabPane("📜 History", id="history-tab"):
94
+ yield ListView(id="history-list")
95
+
96
+ def on_mount(self) -> None:
97
+ """Initialize the sidebar when mounted."""
98
+ # Set up event handlers for keyboard interaction
99
+ history_list = self.query_one("#history-list", ListView)
100
+
101
+ # Add a class to make it focusable
102
+ history_list.can_focus = True
103
+
104
+ # Load command history
105
+ self.load_command_history()
106
+
107
+ @on(ListView.Highlighted)
108
+ def on_list_highlighted(self, event: ListView.Highlighted) -> None:
109
+ """Handle highlighting of list items to ensure they can be selected."""
110
+ # This ensures the item gets focus when highlighted by arrow keys
111
+ if event.list_view.id == "history-list":
112
+ event.list_view.focus()
113
+ # Sync the current_history_index with the ListView index to fix modal sync issue
114
+ self.current_history_index = event.list_view.index
115
+
116
+ @on(ListView.Selected)
117
+ def on_list_selected(self, event: ListView.Selected) -> None:
118
+ """Handle selection of list items (including mouse clicks).
119
+
120
+ Implements double-click detection to allow users to retrieve history items
121
+ by either pressing ENTER or double-clicking with the mouse.
122
+ """
123
+ if event.list_view.id == "history-list":
124
+ current_time = time.time()
125
+ selected_item = event.item
126
+
127
+ # Check if this is a double-click
128
+ if (
129
+ selected_item == self._last_clicked_item
130
+ and current_time - self._last_click_time <= self._double_click_threshold
131
+ and hasattr(selected_item, "command_entry")
132
+ ):
133
+ # Double-click detected! Show command in modal
134
+ # Find the index of this item
135
+ history_list = self.query_one("#history-list", ListView)
136
+ self.current_history_index = history_list.index
137
+
138
+ # Push the modal screen - it will get data from the sidebar
139
+ self.app.push_screen(CommandHistoryModal())
140
+
141
+ # Reset click tracking to prevent triple-click issues
142
+ self._last_click_time = 0
143
+ self._last_clicked_item = None
144
+ else:
145
+ # Single click - just update tracking
146
+ self._last_click_time = current_time
147
+ self._last_clicked_item = selected_item
148
+
149
+ @on(Key)
150
+ def on_key(self, event: Key) -> None:
151
+ """Handle key events for the sidebar."""
152
+ # Handle Enter key on the history list
153
+ if event.key == "enter":
154
+ history_list = self.query_one("#history-list", ListView)
155
+ if (
156
+ history_list.has_focus
157
+ and history_list.highlighted_child
158
+ and hasattr(history_list.highlighted_child, "command_entry")
159
+ ):
160
+ # Show command details in modal
161
+ # Update the current history index to match this item
162
+ self.current_history_index = history_list.index
163
+
164
+ # Push the modal screen - it will get data from the sidebar
165
+ self.app.push_screen(CommandHistoryModal())
166
+
167
+ # Stop propagation
168
+ event.stop()
169
+ event.prevent_default()
170
+
171
+ def load_command_history(self) -> None:
172
+ """Load command history from file into the history list."""
173
+ try:
174
+ # Clear existing items
175
+ history_list = self.query_one("#history-list", ListView)
176
+ history_list.clear()
177
+
178
+ # Get command history entries (limit to last 50)
179
+ entries = self.history_reader.read_history(max_entries=50)
180
+
181
+ # Filter out CLI-specific commands that aren't relevant for TUI
182
+ cli_commands = {
183
+ "/help",
184
+ "/exit",
185
+ "/m",
186
+ "/motd",
187
+ "/show",
188
+ "/set",
189
+ "/tools",
190
+ }
191
+ filtered_entries = []
192
+ for entry in entries:
193
+ command = entry.get("command", "").strip()
194
+ # Skip CLI commands but keep everything else
195
+ if not any(command.startswith(cli_cmd) for cli_cmd in cli_commands):
196
+ filtered_entries.append(entry)
197
+
198
+ # Store filtered entries centrally
199
+ self.history_entries = filtered_entries
200
+
201
+ # Reset history index
202
+ self.current_history_index = 0
203
+
204
+ if not filtered_entries:
205
+ # No history available (after filtering)
206
+ history_list.append(
207
+ ListItem(Label("No command history", classes="history-empty"))
208
+ )
209
+ return
210
+
211
+ # Add filtered entries to the list (most recent first)
212
+ for entry in filtered_entries:
213
+ timestamp = entry["timestamp"]
214
+ command = entry["command"]
215
+
216
+ # Format timestamp for display
217
+ time_display = self.history_reader.format_timestamp(timestamp)
218
+
219
+ # Truncate command for display if needed
220
+ display_text = command
221
+ if len(display_text) > 60:
222
+ display_text = display_text[:57] + "..."
223
+
224
+ # Create list item
225
+ label = Label(
226
+ f"[{time_display}] {display_text}", classes="history-command"
227
+ )
228
+ list_item = ListItem(label)
229
+ list_item.command_entry = entry
230
+ history_list.append(list_item)
231
+
232
+ # Focus on the most recent command (first in the list)
233
+ if len(history_list.children) > 0:
234
+ history_list.index = 0
235
+ # Sync the current_history_index to match the ListView index
236
+ self.current_history_index = 0
237
+
238
+ # Note: We don't automatically show the modal here when just loading the history
239
+ # That will be handled by the app's action_toggle_sidebar method
240
+ # This ensures the modal only appears when explicitly opening the sidebar, not during refresh
241
+
242
+ except Exception as e:
243
+ # Add error item
244
+ history_list = self.query_one("#history-list", ListView)
245
+ history_list.clear()
246
+ history_list.append(
247
+ ListItem(
248
+ Label(f"Error loading history: {str(e)}", classes="history-error")
249
+ )
250
+ )
251
+
252
+ def navigate_to_next_command(self) -> bool:
253
+ """Navigate to the next command in history.
254
+
255
+ Returns:
256
+ bool: True if navigation succeeded, False otherwise
257
+ """
258
+ if (
259
+ not self.history_entries
260
+ or self.current_history_index >= len(self.history_entries) - 1
261
+ ):
262
+ return False
263
+
264
+ # Increment the index
265
+ self.current_history_index += 1
266
+
267
+ # Update the listview selection
268
+ try:
269
+ history_list = self.query_one("#history-list", ListView)
270
+ if history_list and self.current_history_index < len(history_list.children):
271
+ history_list.index = self.current_history_index
272
+ except Exception:
273
+ pass
274
+
275
+ return True
276
+
277
+ def navigate_to_previous_command(self) -> bool:
278
+ """Navigate to the previous command in history.
279
+
280
+ Returns:
281
+ bool: True if navigation succeeded, False otherwise
282
+ """
283
+ if not self.history_entries or self.current_history_index <= 0:
284
+ return False
285
+
286
+ # Decrement the index
287
+ self.current_history_index -= 1
288
+
289
+ # Update the listview selection
290
+ try:
291
+ history_list = self.query_one("#history-list", ListView)
292
+ if history_list and self.current_history_index >= 0:
293
+ history_list.index = self.current_history_index
294
+ except Exception:
295
+ pass
296
+
297
+ return True
298
+
299
+ def get_current_command_entry(self) -> dict:
300
+ """Get the current command entry based on the current index.
301
+
302
+ Returns:
303
+ dict: The current command entry or empty dict if not available
304
+ """
305
+ if self.history_entries and 0 <= self.current_history_index < len(
306
+ self.history_entries
307
+ ):
308
+ return self.history_entries[self.current_history_index]
309
+ return {"command": "", "timestamp": ""}
@@ -0,0 +1,182 @@
1
+ """
2
+ Status bar component for the TUI.
3
+ """
4
+
5
+ import os
6
+
7
+ from rich.text import Text
8
+ from textual.app import ComposeResult
9
+ from textual.reactive import reactive
10
+ from textual.widgets import Static
11
+
12
+
13
+ class StatusBar(Static):
14
+ """Status bar showing current model, puppy name, and connection status."""
15
+
16
+ DEFAULT_CSS = """
17
+ StatusBar {
18
+ dock: top;
19
+ height: 1;
20
+ background: $primary;
21
+ color: $text;
22
+ text-align: right;
23
+ padding: 0 1;
24
+ }
25
+
26
+ #status-content {
27
+ text-align: right;
28
+ width: 100%;
29
+ }
30
+ """
31
+
32
+ current_model = reactive("")
33
+ puppy_name = reactive("")
34
+ connection_status = reactive("Connected")
35
+ agent_status = reactive("Ready")
36
+ progress_visible = reactive(False)
37
+ token_count = reactive(0)
38
+ token_capacity = reactive(0)
39
+ token_proportion = reactive(0.0)
40
+
41
+ def compose(self) -> ComposeResult:
42
+ yield Static(id="status-content")
43
+
44
+ def watch_current_model(self) -> None:
45
+ self.update_status()
46
+
47
+ def watch_puppy_name(self) -> None:
48
+ self.update_status()
49
+
50
+ def watch_connection_status(self) -> None:
51
+ self.update_status()
52
+
53
+ def watch_agent_status(self) -> None:
54
+ self.update_status()
55
+
56
+ def watch_token_count(self) -> None:
57
+ self.update_status()
58
+
59
+ def watch_token_capacity(self) -> None:
60
+ self.update_status()
61
+
62
+ def watch_token_proportion(self) -> None:
63
+ self.update_status()
64
+
65
+ def watch_progress_visible(self) -> None:
66
+ self.update_status()
67
+
68
+ def update_status(self) -> None:
69
+ """Update the status bar content with responsive design."""
70
+ status_widget = self.query_one("#status-content", Static)
71
+
72
+ # Get current working directory
73
+ cwd = os.getcwd()
74
+ cwd_short = os.path.basename(cwd) if cwd != "/" else "/"
75
+
76
+ # Add agent status indicator with different colors
77
+ if self.agent_status == "Thinking":
78
+ status_indicator = "🤔"
79
+ status_color = "yellow"
80
+ elif self.agent_status == "Processing":
81
+ status_indicator = "⚡"
82
+ status_color = "blue"
83
+ elif self.agent_status == "Busy":
84
+ status_indicator = "🔄"
85
+ status_color = "orange"
86
+ else: # Ready
87
+ status_indicator = "✅"
88
+ status_color = "green"
89
+
90
+ # Get terminal width for responsive content
91
+ try:
92
+ terminal_width = self.app.size.width if hasattr(self.app, "size") else 80
93
+ except Exception:
94
+ terminal_width = 80
95
+
96
+ # Create responsive status text based on terminal width
97
+ rich_text = Text()
98
+
99
+ # Token status with color coding
100
+ token_status = ""
101
+ token_color = "green"
102
+ if self.token_count > 0 and self.token_capacity > 0:
103
+ # Import here to avoid circular import
104
+ from code_puppy.config import get_summarization_threshold
105
+
106
+ summarization_threshold = get_summarization_threshold()
107
+
108
+ if self.token_proportion > summarization_threshold:
109
+ token_color = "red"
110
+ token_status = f"🔴 {self.token_count}/{self.token_capacity} ({self.token_proportion:.1%})"
111
+ elif self.token_proportion > (
112
+ summarization_threshold - 0.15
113
+ ): # 15% before summarization threshold
114
+ token_color = "yellow"
115
+ token_status = f"🟡 {self.token_count}/{self.token_capacity} ({self.token_proportion:.1%})"
116
+ else:
117
+ token_color = "green"
118
+ token_status = f"🟢 {self.token_count}/{self.token_capacity} ({self.token_proportion:.1%})"
119
+
120
+ if terminal_width >= 140:
121
+ # Extra wide - show full path and all info including tokens
122
+ rich_text.append(
123
+ f"📁 {cwd} | 🐶 {self.puppy_name} | Model: {self.current_model} | "
124
+ )
125
+ if token_status:
126
+ rich_text.append(f"{token_status} | ", style=token_color)
127
+ rich_text.append(
128
+ f"{status_indicator} {self.agent_status}", style=status_color
129
+ )
130
+ elif terminal_width >= 100:
131
+ # Full status display for wide terminals
132
+ rich_text.append(
133
+ f"📁 {cwd_short} | 🐶 {self.puppy_name} | Model: {self.current_model} | "
134
+ )
135
+ rich_text.append(
136
+ f"{status_indicator} {self.agent_status}", style=status_color
137
+ )
138
+ elif terminal_width >= 120:
139
+ # Medium display - shorten model name if needed
140
+ model_display = (
141
+ self.current_model[:15] + "..."
142
+ if len(self.current_model) > 18
143
+ else self.current_model
144
+ )
145
+ rich_text.append(
146
+ f"📁 {cwd_short} | 🐶 {self.puppy_name} | {model_display} | "
147
+ )
148
+ if token_status:
149
+ rich_text.append(f"{token_status} | ", style=token_color)
150
+ rich_text.append(
151
+ f"{status_indicator} {self.agent_status}", style=status_color
152
+ )
153
+ elif terminal_width >= 60:
154
+ # Compact display - use abbreviations
155
+ puppy_short = (
156
+ self.puppy_name[:8] + "..."
157
+ if len(self.puppy_name) > 10
158
+ else self.puppy_name
159
+ )
160
+ model_short = (
161
+ self.current_model[:12] + "..."
162
+ if len(self.current_model) > 15
163
+ else self.current_model
164
+ )
165
+ rich_text.append(f"📁 {cwd_short} | 🐶 {puppy_short} | {model_short} | ")
166
+ rich_text.append(f"{status_indicator}", style=status_color)
167
+ else:
168
+ # Minimal display for very narrow terminals
169
+ cwd_mini = cwd_short[:8] + "..." if len(cwd_short) > 10 else cwd_short
170
+ rich_text.append(f"📁 {cwd_mini} | ")
171
+ rich_text.append(f"{status_indicator}", style=status_color)
172
+
173
+ rich_text.justify = "right"
174
+ status_widget.update(rich_text)
175
+
176
+ def update_token_info(
177
+ self, current_tokens: int, max_tokens: int, proportion: float
178
+ ) -> None:
179
+ """Update token information in the status bar."""
180
+ self.token_count = current_tokens
181
+ self.token_capacity = max_tokens
182
+ self.token_proportion = proportion
@@ -0,0 +1,27 @@
1
+ """
2
+ Custom message classes for TUI components.
3
+ """
4
+
5
+ from textual.message import Message
6
+
7
+
8
+ class HistoryEntrySelected(Message):
9
+ """Message sent when a history entry is selected from the sidebar."""
10
+
11
+ def __init__(self, history_entry: dict) -> None:
12
+ """Initialize with the history entry data."""
13
+ self.history_entry = history_entry
14
+ super().__init__()
15
+
16
+
17
+ class CommandSelected(Message):
18
+ """Message sent when a command is selected from the history modal."""
19
+
20
+ def __init__(self, command: str) -> None:
21
+ """Initialize with the command text.
22
+
23
+ Args:
24
+ command: The command text that was selected
25
+ """
26
+ self.command = command
27
+ super().__init__()
@@ -0,0 +1,8 @@
1
+ """
2
+ TUI models package.
3
+ """
4
+
5
+ from .chat_message import ChatMessage
6
+ from .enums import MessageType
7
+
8
+ __all__ = ["MessageType", "ChatMessage"]
@@ -0,0 +1,25 @@
1
+ """
2
+ Chat message data model.
3
+ """
4
+
5
+ from dataclasses import dataclass
6
+ from datetime import datetime
7
+ from typing import Any, Dict
8
+
9
+ from .enums import MessageType
10
+
11
+
12
+ @dataclass
13
+ class ChatMessage:
14
+ """Represents a message in the chat interface."""
15
+
16
+ id: str
17
+ type: MessageType
18
+ content: str
19
+ timestamp: datetime
20
+ metadata: Dict[str, Any] = None
21
+ group_id: str = None
22
+
23
+ def __post_init__(self):
24
+ if self.metadata is None:
25
+ self.metadata = {}
@@ -0,0 +1,89 @@
1
+ """
2
+ Command history reader for TUI history tab.
3
+ """
4
+
5
+ import os
6
+ import re
7
+ from datetime import datetime
8
+ from typing import Dict, List
9
+
10
+ from code_puppy.config import COMMAND_HISTORY_FILE
11
+
12
+
13
+ class HistoryFileReader:
14
+ """Reads and parses the command history file for display in the TUI history tab."""
15
+
16
+ def __init__(self, history_file_path: str = COMMAND_HISTORY_FILE):
17
+ """Initialize the history file reader.
18
+
19
+ Args:
20
+ history_file_path: Path to the command history file. Defaults to the standard location.
21
+ """
22
+ self.history_file_path = history_file_path
23
+ self._timestamp_pattern = re.compile(
24
+ r"^# (\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})"
25
+ )
26
+
27
+ def read_history(self, max_entries: int = 100) -> List[Dict[str, str]]:
28
+ """Read command history from the history file.
29
+
30
+ Args:
31
+ max_entries: Maximum number of entries to read. Defaults to 100.
32
+
33
+ Returns:
34
+ List of history entries with timestamp and command, most recent first.
35
+ """
36
+ if not os.path.exists(self.history_file_path):
37
+ return []
38
+
39
+ try:
40
+ with open(self.history_file_path, "r") as f:
41
+ content = f.read()
42
+
43
+ # Split content by timestamp marker
44
+ raw_chunks = re.split(r"(# \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})", content)
45
+
46
+ # Filter out empty chunks
47
+ chunks = [chunk for chunk in raw_chunks if chunk.strip()]
48
+
49
+ entries = []
50
+
51
+ # Process chunks in pairs (timestamp and command)
52
+ i = 0
53
+ while i < len(chunks) - 1:
54
+ if self._timestamp_pattern.match(chunks[i]):
55
+ timestamp = self._timestamp_pattern.match(chunks[i]).group(1)
56
+ command_text = chunks[i + 1].strip()
57
+
58
+ if command_text: # Skip empty commands
59
+ entries.append(
60
+ {"timestamp": timestamp, "command": command_text}
61
+ )
62
+
63
+ i += 2
64
+ else:
65
+ # Skip invalid chunks
66
+ i += 1
67
+
68
+ # Limit the number of entries and reverse to get most recent first
69
+ return entries[-max_entries:][::-1]
70
+
71
+ except Exception:
72
+ # Return empty list on any error
73
+ return []
74
+
75
+ def format_timestamp(self, timestamp: str, format_str: str = "%H:%M:%S") -> str:
76
+ """Format a timestamp string for display.
77
+
78
+ Args:
79
+ timestamp: ISO format timestamp string (YYYY-MM-DDThh:mm:ss)
80
+ format_str: Format string for datetime.strftime
81
+
82
+ Returns:
83
+ Formatted timestamp string
84
+ """
85
+ try:
86
+ dt = datetime.fromisoformat(timestamp)
87
+ return dt.strftime(format_str)
88
+ except (ValueError, TypeError):
89
+ return timestamp
@@ -0,0 +1,24 @@
1
+ """
2
+ Enums for the TUI module.
3
+ """
4
+
5
+ from enum import Enum
6
+
7
+
8
+ class MessageType(Enum):
9
+ """Types of messages in the chat interface."""
10
+
11
+ USER = "user"
12
+ AGENT = "agent"
13
+ SYSTEM = "system"
14
+ ERROR = "error"
15
+ DIVIDER = "divider"
16
+ INFO = "info"
17
+ SUCCESS = "success"
18
+ WARNING = "warning"
19
+ TOOL_OUTPUT = "tool_output"
20
+ COMMAND_OUTPUT = "command_output"
21
+
22
+ AGENT_REASONING = "agent_reasoning"
23
+ PLANNED_NEXT_STEPS = "planned_next_steps"
24
+ AGENT_RESPONSE = "agent_response"
@@ -0,0 +1,13 @@
1
+ """
2
+ TUI screens package.
3
+ """
4
+
5
+ from .help import HelpScreen
6
+ from .settings import SettingsScreen
7
+ from .tools import ToolsScreen
8
+
9
+ __all__ = [
10
+ "HelpScreen",
11
+ "SettingsScreen",
12
+ "ToolsScreen",
13
+ ]