code-puppy 0.0.214__py3-none-any.whl → 0.0.366__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 (231) hide show
  1. code_puppy/__init__.py +7 -1
  2. code_puppy/agents/__init__.py +2 -0
  3. code_puppy/agents/agent_c_reviewer.py +59 -6
  4. code_puppy/agents/agent_code_puppy.py +7 -1
  5. code_puppy/agents/agent_code_reviewer.py +12 -2
  6. code_puppy/agents/agent_cpp_reviewer.py +73 -6
  7. code_puppy/agents/agent_creator_agent.py +45 -4
  8. code_puppy/agents/agent_golang_reviewer.py +92 -3
  9. code_puppy/agents/agent_javascript_reviewer.py +101 -8
  10. code_puppy/agents/agent_manager.py +81 -4
  11. code_puppy/agents/agent_pack_leader.py +383 -0
  12. code_puppy/agents/agent_planning.py +163 -0
  13. code_puppy/agents/agent_python_programmer.py +165 -0
  14. code_puppy/agents/agent_python_reviewer.py +28 -6
  15. code_puppy/agents/agent_qa_expert.py +98 -6
  16. code_puppy/agents/agent_qa_kitten.py +12 -7
  17. code_puppy/agents/agent_security_auditor.py +113 -3
  18. code_puppy/agents/agent_terminal_qa.py +323 -0
  19. code_puppy/agents/agent_typescript_reviewer.py +106 -7
  20. code_puppy/agents/base_agent.py +802 -176
  21. code_puppy/agents/event_stream_handler.py +350 -0
  22. code_puppy/agents/pack/__init__.py +34 -0
  23. code_puppy/agents/pack/bloodhound.py +304 -0
  24. code_puppy/agents/pack/husky.py +321 -0
  25. code_puppy/agents/pack/retriever.py +393 -0
  26. code_puppy/agents/pack/shepherd.py +348 -0
  27. code_puppy/agents/pack/terrier.py +287 -0
  28. code_puppy/agents/pack/watchdog.py +367 -0
  29. code_puppy/agents/prompt_reviewer.py +145 -0
  30. code_puppy/agents/subagent_stream_handler.py +276 -0
  31. code_puppy/api/__init__.py +13 -0
  32. code_puppy/api/app.py +169 -0
  33. code_puppy/api/main.py +21 -0
  34. code_puppy/api/pty_manager.py +446 -0
  35. code_puppy/api/routers/__init__.py +12 -0
  36. code_puppy/api/routers/agents.py +36 -0
  37. code_puppy/api/routers/commands.py +217 -0
  38. code_puppy/api/routers/config.py +74 -0
  39. code_puppy/api/routers/sessions.py +232 -0
  40. code_puppy/api/templates/terminal.html +361 -0
  41. code_puppy/api/websocket.py +154 -0
  42. code_puppy/callbacks.py +142 -4
  43. code_puppy/chatgpt_codex_client.py +283 -0
  44. code_puppy/claude_cache_client.py +586 -0
  45. code_puppy/cli_runner.py +916 -0
  46. code_puppy/command_line/add_model_menu.py +1079 -0
  47. code_puppy/command_line/agent_menu.py +395 -0
  48. code_puppy/command_line/attachments.py +10 -5
  49. code_puppy/command_line/autosave_menu.py +605 -0
  50. code_puppy/command_line/clipboard.py +527 -0
  51. code_puppy/command_line/colors_menu.py +520 -0
  52. code_puppy/command_line/command_handler.py +176 -738
  53. code_puppy/command_line/command_registry.py +150 -0
  54. code_puppy/command_line/config_commands.py +715 -0
  55. code_puppy/command_line/core_commands.py +792 -0
  56. code_puppy/command_line/diff_menu.py +863 -0
  57. code_puppy/command_line/load_context_completion.py +15 -22
  58. code_puppy/command_line/mcp/base.py +0 -3
  59. code_puppy/command_line/mcp/catalog_server_installer.py +175 -0
  60. code_puppy/command_line/mcp/custom_server_form.py +688 -0
  61. code_puppy/command_line/mcp/custom_server_installer.py +195 -0
  62. code_puppy/command_line/mcp/edit_command.py +148 -0
  63. code_puppy/command_line/mcp/handler.py +9 -4
  64. code_puppy/command_line/mcp/help_command.py +6 -5
  65. code_puppy/command_line/mcp/install_command.py +15 -26
  66. code_puppy/command_line/mcp/install_menu.py +685 -0
  67. code_puppy/command_line/mcp/list_command.py +2 -2
  68. code_puppy/command_line/mcp/logs_command.py +174 -65
  69. code_puppy/command_line/mcp/remove_command.py +2 -2
  70. code_puppy/command_line/mcp/restart_command.py +12 -4
  71. code_puppy/command_line/mcp/search_command.py +16 -10
  72. code_puppy/command_line/mcp/start_all_command.py +18 -6
  73. code_puppy/command_line/mcp/start_command.py +47 -25
  74. code_puppy/command_line/mcp/status_command.py +4 -5
  75. code_puppy/command_line/mcp/stop_all_command.py +7 -1
  76. code_puppy/command_line/mcp/stop_command.py +8 -4
  77. code_puppy/command_line/mcp/test_command.py +2 -2
  78. code_puppy/command_line/mcp/wizard_utils.py +20 -16
  79. code_puppy/command_line/mcp_completion.py +174 -0
  80. code_puppy/command_line/model_picker_completion.py +75 -25
  81. code_puppy/command_line/model_settings_menu.py +884 -0
  82. code_puppy/command_line/motd.py +14 -8
  83. code_puppy/command_line/onboarding_slides.py +179 -0
  84. code_puppy/command_line/onboarding_wizard.py +340 -0
  85. code_puppy/command_line/pin_command_completion.py +329 -0
  86. code_puppy/command_line/prompt_toolkit_completion.py +463 -63
  87. code_puppy/command_line/session_commands.py +296 -0
  88. code_puppy/command_line/utils.py +54 -0
  89. code_puppy/config.py +898 -112
  90. code_puppy/error_logging.py +118 -0
  91. code_puppy/gemini_code_assist.py +385 -0
  92. code_puppy/gemini_model.py +602 -0
  93. code_puppy/http_utils.py +210 -148
  94. code_puppy/keymap.py +128 -0
  95. code_puppy/main.py +5 -698
  96. code_puppy/mcp_/__init__.py +17 -0
  97. code_puppy/mcp_/async_lifecycle.py +35 -4
  98. code_puppy/mcp_/blocking_startup.py +70 -43
  99. code_puppy/mcp_/captured_stdio_server.py +2 -2
  100. code_puppy/mcp_/config_wizard.py +4 -4
  101. code_puppy/mcp_/dashboard.py +15 -6
  102. code_puppy/mcp_/managed_server.py +65 -38
  103. code_puppy/mcp_/manager.py +146 -52
  104. code_puppy/mcp_/mcp_logs.py +224 -0
  105. code_puppy/mcp_/registry.py +6 -6
  106. code_puppy/mcp_/server_registry_catalog.py +24 -5
  107. code_puppy/messaging/__init__.py +199 -2
  108. code_puppy/messaging/bus.py +610 -0
  109. code_puppy/messaging/commands.py +167 -0
  110. code_puppy/messaging/markdown_patches.py +57 -0
  111. code_puppy/messaging/message_queue.py +17 -48
  112. code_puppy/messaging/messages.py +500 -0
  113. code_puppy/messaging/queue_console.py +1 -24
  114. code_puppy/messaging/renderers.py +43 -146
  115. code_puppy/messaging/rich_renderer.py +1027 -0
  116. code_puppy/messaging/spinner/__init__.py +21 -5
  117. code_puppy/messaging/spinner/console_spinner.py +86 -51
  118. code_puppy/messaging/subagent_console.py +461 -0
  119. code_puppy/model_factory.py +634 -83
  120. code_puppy/model_utils.py +167 -0
  121. code_puppy/models.json +66 -68
  122. code_puppy/models_dev_api.json +1 -0
  123. code_puppy/models_dev_parser.py +592 -0
  124. code_puppy/plugins/__init__.py +164 -10
  125. code_puppy/plugins/antigravity_oauth/__init__.py +10 -0
  126. code_puppy/plugins/antigravity_oauth/accounts.py +406 -0
  127. code_puppy/plugins/antigravity_oauth/antigravity_model.py +704 -0
  128. code_puppy/plugins/antigravity_oauth/config.py +42 -0
  129. code_puppy/plugins/antigravity_oauth/constants.py +136 -0
  130. code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
  131. code_puppy/plugins/antigravity_oauth/register_callbacks.py +406 -0
  132. code_puppy/plugins/antigravity_oauth/storage.py +271 -0
  133. code_puppy/plugins/antigravity_oauth/test_plugin.py +319 -0
  134. code_puppy/plugins/antigravity_oauth/token.py +167 -0
  135. code_puppy/plugins/antigravity_oauth/transport.py +767 -0
  136. code_puppy/plugins/antigravity_oauth/utils.py +169 -0
  137. code_puppy/plugins/chatgpt_oauth/__init__.py +8 -0
  138. code_puppy/plugins/chatgpt_oauth/config.py +52 -0
  139. code_puppy/plugins/chatgpt_oauth/oauth_flow.py +328 -0
  140. code_puppy/plugins/chatgpt_oauth/register_callbacks.py +94 -0
  141. code_puppy/plugins/chatgpt_oauth/test_plugin.py +293 -0
  142. code_puppy/plugins/chatgpt_oauth/utils.py +489 -0
  143. code_puppy/plugins/claude_code_oauth/README.md +167 -0
  144. code_puppy/plugins/claude_code_oauth/SETUP.md +93 -0
  145. code_puppy/plugins/claude_code_oauth/__init__.py +6 -0
  146. code_puppy/plugins/claude_code_oauth/config.py +50 -0
  147. code_puppy/plugins/claude_code_oauth/register_callbacks.py +308 -0
  148. code_puppy/plugins/claude_code_oauth/test_plugin.py +283 -0
  149. code_puppy/plugins/claude_code_oauth/utils.py +518 -0
  150. code_puppy/plugins/customizable_commands/__init__.py +0 -0
  151. code_puppy/plugins/customizable_commands/register_callbacks.py +169 -0
  152. code_puppy/plugins/example_custom_command/README.md +280 -0
  153. code_puppy/plugins/example_custom_command/register_callbacks.py +2 -2
  154. code_puppy/plugins/file_permission_handler/__init__.py +4 -0
  155. code_puppy/plugins/file_permission_handler/register_callbacks.py +523 -0
  156. code_puppy/plugins/frontend_emitter/__init__.py +25 -0
  157. code_puppy/plugins/frontend_emitter/emitter.py +121 -0
  158. code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
  159. code_puppy/plugins/oauth_puppy_html.py +228 -0
  160. code_puppy/plugins/shell_safety/__init__.py +6 -0
  161. code_puppy/plugins/shell_safety/agent_shell_safety.py +69 -0
  162. code_puppy/plugins/shell_safety/command_cache.py +156 -0
  163. code_puppy/plugins/shell_safety/register_callbacks.py +202 -0
  164. code_puppy/prompts/antigravity_system_prompt.md +1 -0
  165. code_puppy/prompts/codex_system_prompt.md +310 -0
  166. code_puppy/pydantic_patches.py +131 -0
  167. code_puppy/reopenable_async_client.py +8 -8
  168. code_puppy/round_robin_model.py +9 -12
  169. code_puppy/session_storage.py +2 -1
  170. code_puppy/status_display.py +21 -4
  171. code_puppy/summarization_agent.py +41 -13
  172. code_puppy/terminal_utils.py +418 -0
  173. code_puppy/tools/__init__.py +37 -1
  174. code_puppy/tools/agent_tools.py +536 -52
  175. code_puppy/tools/browser/__init__.py +37 -0
  176. code_puppy/tools/browser/browser_control.py +19 -23
  177. code_puppy/tools/browser/browser_interactions.py +41 -48
  178. code_puppy/tools/browser/browser_locators.py +36 -38
  179. code_puppy/tools/browser/browser_manager.py +316 -0
  180. code_puppy/tools/browser/browser_navigation.py +16 -16
  181. code_puppy/tools/browser/browser_screenshot.py +79 -143
  182. code_puppy/tools/browser/browser_scripts.py +32 -42
  183. code_puppy/tools/browser/browser_workflows.py +44 -27
  184. code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
  185. code_puppy/tools/browser/terminal_command_tools.py +521 -0
  186. code_puppy/tools/browser/terminal_screenshot_tools.py +556 -0
  187. code_puppy/tools/browser/terminal_tools.py +525 -0
  188. code_puppy/tools/command_runner.py +930 -147
  189. code_puppy/tools/common.py +1113 -5
  190. code_puppy/tools/display.py +84 -0
  191. code_puppy/tools/file_modifications.py +288 -89
  192. code_puppy/tools/file_operations.py +226 -154
  193. code_puppy/tools/subagent_context.py +158 -0
  194. code_puppy/uvx_detection.py +242 -0
  195. code_puppy/version_checker.py +30 -11
  196. code_puppy-0.0.366.data/data/code_puppy/models.json +110 -0
  197. code_puppy-0.0.366.data/data/code_puppy/models_dev_api.json +1 -0
  198. {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/METADATA +149 -75
  199. code_puppy-0.0.366.dist-info/RECORD +217 -0
  200. {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/WHEEL +1 -1
  201. code_puppy/command_line/mcp/add_command.py +0 -183
  202. code_puppy/messaging/spinner/textual_spinner.py +0 -106
  203. code_puppy/tools/browser/camoufox_manager.py +0 -216
  204. code_puppy/tools/browser/vqa_agent.py +0 -70
  205. code_puppy/tui/__init__.py +0 -10
  206. code_puppy/tui/app.py +0 -1105
  207. code_puppy/tui/components/__init__.py +0 -21
  208. code_puppy/tui/components/chat_view.py +0 -551
  209. code_puppy/tui/components/command_history_modal.py +0 -218
  210. code_puppy/tui/components/copy_button.py +0 -139
  211. code_puppy/tui/components/custom_widgets.py +0 -63
  212. code_puppy/tui/components/human_input_modal.py +0 -175
  213. code_puppy/tui/components/input_area.py +0 -167
  214. code_puppy/tui/components/sidebar.py +0 -309
  215. code_puppy/tui/components/status_bar.py +0 -185
  216. code_puppy/tui/messages.py +0 -27
  217. code_puppy/tui/models/__init__.py +0 -8
  218. code_puppy/tui/models/chat_message.py +0 -25
  219. code_puppy/tui/models/command_history.py +0 -89
  220. code_puppy/tui/models/enums.py +0 -24
  221. code_puppy/tui/screens/__init__.py +0 -17
  222. code_puppy/tui/screens/autosave_picker.py +0 -175
  223. code_puppy/tui/screens/help.py +0 -130
  224. code_puppy/tui/screens/mcp_install_wizard.py +0 -803
  225. code_puppy/tui/screens/settings.py +0 -306
  226. code_puppy/tui/screens/tools.py +0 -74
  227. code_puppy/tui_state.py +0 -55
  228. code_puppy-0.0.214.data/data/code_puppy/models.json +0 -112
  229. code_puppy-0.0.214.dist-info/RECORD +0 -131
  230. {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/entry_points.txt +0 -0
  231. {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/licenses/LICENSE +0 -0
@@ -1,167 +0,0 @@
1
- """
2
- Input area component for message input.
3
- """
4
-
5
- from textual.app import ComposeResult
6
- from textual.containers import Container, Horizontal
7
- from textual.message import Message
8
- from textual.reactive import reactive
9
- from textual.widgets import Button, Static
10
-
11
- from code_puppy.messaging.spinner import TextualSpinner
12
-
13
- from .custom_widgets import CustomTextArea
14
-
15
- # Alias SimpleSpinnerWidget to TextualSpinner for backward compatibility
16
- SimpleSpinnerWidget = TextualSpinner
17
-
18
-
19
- class SubmitCancelButton(Button):
20
- """A button that toggles between submit and cancel states."""
21
-
22
- is_cancel_mode = reactive(False)
23
-
24
- DEFAULT_CSS = """
25
- SubmitCancelButton {
26
- width: 3;
27
- min-width: 3;
28
- height: 3;
29
- content-align: center middle;
30
- border: none;
31
- background: $surface;
32
- }
33
-
34
- SubmitCancelButton:focus {
35
- border: none;
36
- color: $surface;
37
- background: $surface;
38
- }
39
-
40
- SubmitCancelButton:hover {
41
- border: none;
42
- background: $surface;
43
- }
44
- """
45
-
46
- def __init__(self, **kwargs):
47
- super().__init__("▶️", **kwargs)
48
- self.id = "submit-cancel-button"
49
-
50
- def watch_is_cancel_mode(self, is_cancel: bool) -> None:
51
- """Update the button label when cancel mode changes."""
52
- self.label = "⏹️" if is_cancel else "▶️"
53
-
54
- def on_click(self) -> None:
55
- """Handle click event and bubble it up to parent."""
56
- # When clicked, send a ButtonClicked message that will be handled by the parent
57
- self.post_message(self.Clicked(self))
58
-
59
- class Clicked(Message):
60
- """Button was clicked."""
61
-
62
- def __init__(self, button: "SubmitCancelButton") -> None:
63
- self.is_cancel_mode = button.is_cancel_mode
64
- super().__init__()
65
-
66
-
67
- class InputArea(Container):
68
- """Input area with text input, spinner, help text, and send button."""
69
-
70
- DEFAULT_CSS = """
71
- InputArea {
72
- dock: bottom;
73
- height: 9;
74
- margin: 1;
75
- }
76
-
77
- #spinner {
78
- height: 1;
79
- width: 1fr;
80
- margin: 0 3 0 1;
81
- content-align: left middle;
82
- text-align: left;
83
- display: none;
84
- }
85
-
86
- #spinner.visible {
87
- display: block;
88
- }
89
-
90
- #input-container {
91
- height: 5;
92
- width: 1fr;
93
- margin: 1 3 0 1;
94
- align: center middle;
95
- }
96
-
97
- #input-field {
98
- height: 5;
99
- width: 1fr;
100
- border: round $primary;
101
- background: $surface;
102
- }
103
-
104
- #submit-cancel-button {
105
- height: 3;
106
- width: 3;
107
- min-width: 3;
108
- margin: 1 0 1 1;
109
- content-align: center middle;
110
- border: none;
111
- background: $surface;
112
- }
113
-
114
- #input-help {
115
- height: 1;
116
- width: 1fr;
117
- margin: 0 3 1 1;
118
- color: $text-muted;
119
- text-align: center;
120
- }
121
- """
122
-
123
- def on_mount(self) -> None:
124
- """Initialize the button state based on the app's agent_busy state."""
125
- app = self.app
126
- if hasattr(app, "agent_busy"):
127
- button = self.query_one(SubmitCancelButton)
128
- button.is_cancel_mode = app.agent_busy
129
-
130
- def compose(self) -> ComposeResult:
131
- yield SimpleSpinnerWidget(id="spinner")
132
- with Horizontal(id="input-container"):
133
- yield CustomTextArea(id="input-field", show_line_numbers=False)
134
- yield SubmitCancelButton()
135
- yield Static(
136
- "Enter to send • Shift+Enter for new line • Ctrl+1 for help",
137
- id="input-help",
138
- )
139
-
140
- def on_submit_cancel_button_clicked(
141
- self, event: SubmitCancelButton.Clicked
142
- ) -> None:
143
- """Handle button clicks based on current mode."""
144
- if event.is_cancel_mode:
145
- # Cancel mode - stop the current process
146
- self.post_message(self.CancelRequested())
147
- else:
148
- # Submit mode - send the message
149
- self.post_message(self.SubmitRequested())
150
-
151
- # Return focus to the input field
152
- self.app.call_after_refresh(self.focus_input_field)
153
-
154
- def focus_input_field(self) -> None:
155
- """Focus the input field after button click."""
156
- input_field = self.query_one("#input-field")
157
- input_field.focus()
158
-
159
- class SubmitRequested(Message):
160
- """Request to submit the current input."""
161
-
162
- pass
163
-
164
- class CancelRequested(Message):
165
- """Request to cancel the current process."""
166
-
167
- pass
@@ -1,309 +0,0 @@
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": ""}
@@ -1,185 +0,0 @@
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
- elif self.agent_status == "Loading":
87
- status_indicator = "⏳"
88
- status_color = "cyan"
89
- else: # Ready or anything else
90
- status_indicator = "✅"
91
- status_color = "green"
92
-
93
- # Get terminal width for responsive content
94
- try:
95
- terminal_width = self.app.size.width if hasattr(self.app, "size") else 80
96
- except Exception:
97
- terminal_width = 80
98
-
99
- # Create responsive status text based on terminal width
100
- rich_text = Text()
101
-
102
- # Token status with color coding
103
- token_status = ""
104
- token_color = "green"
105
- if self.token_count > 0 and self.token_capacity > 0:
106
- # Import here to avoid circular import
107
- from code_puppy.config import get_compaction_threshold
108
-
109
- get_compaction_threshold = get_compaction_threshold()
110
-
111
- if self.token_proportion > get_compaction_threshold:
112
- token_color = "red"
113
- token_status = f"🔴 {self.token_count}/{self.token_capacity} ({self.token_proportion:.1%})"
114
- elif self.token_proportion > (
115
- get_compaction_threshold - 0.15
116
- ): # 15% before summarization threshold
117
- token_color = "yellow"
118
- token_status = f"🟡 {self.token_count}/{self.token_capacity} ({self.token_proportion:.1%})"
119
- else:
120
- token_color = "green"
121
- token_status = f"🟢 {self.token_count}/{self.token_capacity} ({self.token_proportion:.1%})"
122
-
123
- if terminal_width >= 140:
124
- # Extra wide - show full path and all info including tokens
125
- rich_text.append(
126
- f"📁 {cwd} | 🐶 {self.puppy_name} | Model: {self.current_model} | "
127
- )
128
- if token_status:
129
- rich_text.append(f"{token_status} | ", style=token_color)
130
- rich_text.append(
131
- f"{status_indicator} {self.agent_status}", style=status_color
132
- )
133
- elif terminal_width >= 100:
134
- # Full status display for wide terminals
135
- rich_text.append(
136
- f"📁 {cwd_short} | 🐶 {self.puppy_name} | Model: {self.current_model} | "
137
- )
138
- rich_text.append(
139
- f"{status_indicator} {self.agent_status}", style=status_color
140
- )
141
- elif terminal_width >= 120:
142
- # Medium display - shorten model name if needed
143
- model_display = (
144
- self.current_model[:15] + "..."
145
- if len(self.current_model) > 18
146
- else self.current_model
147
- )
148
- rich_text.append(
149
- f"📁 {cwd_short} | 🐶 {self.puppy_name} | {model_display} | "
150
- )
151
- if token_status:
152
- rich_text.append(f"{token_status} | ", style=token_color)
153
- rich_text.append(
154
- f"{status_indicator} {self.agent_status}", style=status_color
155
- )
156
- elif terminal_width >= 60:
157
- # Compact display - use abbreviations
158
- puppy_short = (
159
- self.puppy_name[:8] + "..."
160
- if len(self.puppy_name) > 10
161
- else self.puppy_name
162
- )
163
- model_short = (
164
- self.current_model[:12] + "..."
165
- if len(self.current_model) > 15
166
- else self.current_model
167
- )
168
- rich_text.append(f"📁 {cwd_short} | 🐶 {puppy_short} | {model_short} | ")
169
- rich_text.append(f"{status_indicator}", style=status_color)
170
- else:
171
- # Minimal display for very narrow terminals
172
- cwd_mini = cwd_short[:8] + "..." if len(cwd_short) > 10 else cwd_short
173
- rich_text.append(f"📁 {cwd_mini} | ")
174
- rich_text.append(f"{status_indicator}", style=status_color)
175
-
176
- rich_text.justify = "right"
177
- status_widget.update(rich_text)
178
-
179
- def update_token_info(
180
- self, current_tokens: int, max_tokens: int, proportion: float
181
- ) -> None:
182
- """Update token information in the status bar."""
183
- self.token_count = current_tokens
184
- self.token_capacity = max_tokens
185
- self.token_proportion = proportion
@@ -1,27 +0,0 @@
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__()
@@ -1,8 +0,0 @@
1
- """
2
- TUI models package.
3
- """
4
-
5
- from .chat_message import ChatMessage
6
- from .enums import MessageType
7
-
8
- __all__ = ["MessageType", "ChatMessage"]