code-puppy 0.0.169__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 (243) hide show
  1. code_puppy/__init__.py +7 -1
  2. code_puppy/agents/__init__.py +8 -8
  3. code_puppy/agents/agent_c_reviewer.py +155 -0
  4. code_puppy/agents/agent_code_puppy.py +9 -2
  5. code_puppy/agents/agent_code_reviewer.py +90 -0
  6. code_puppy/agents/agent_cpp_reviewer.py +132 -0
  7. code_puppy/agents/agent_creator_agent.py +48 -9
  8. code_puppy/agents/agent_golang_reviewer.py +151 -0
  9. code_puppy/agents/agent_javascript_reviewer.py +160 -0
  10. code_puppy/agents/agent_manager.py +146 -199
  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 +90 -0
  15. code_puppy/agents/agent_qa_expert.py +163 -0
  16. code_puppy/agents/agent_qa_kitten.py +208 -0
  17. code_puppy/agents/agent_security_auditor.py +181 -0
  18. code_puppy/agents/agent_terminal_qa.py +323 -0
  19. code_puppy/agents/agent_typescript_reviewer.py +166 -0
  20. code_puppy/agents/base_agent.py +1713 -1
  21. code_puppy/agents/event_stream_handler.py +350 -0
  22. code_puppy/agents/json_agent.py +12 -1
  23. code_puppy/agents/pack/__init__.py +34 -0
  24. code_puppy/agents/pack/bloodhound.py +304 -0
  25. code_puppy/agents/pack/husky.py +321 -0
  26. code_puppy/agents/pack/retriever.py +393 -0
  27. code_puppy/agents/pack/shepherd.py +348 -0
  28. code_puppy/agents/pack/terrier.py +287 -0
  29. code_puppy/agents/pack/watchdog.py +367 -0
  30. code_puppy/agents/prompt_reviewer.py +145 -0
  31. code_puppy/agents/subagent_stream_handler.py +276 -0
  32. code_puppy/api/__init__.py +13 -0
  33. code_puppy/api/app.py +169 -0
  34. code_puppy/api/main.py +21 -0
  35. code_puppy/api/pty_manager.py +446 -0
  36. code_puppy/api/routers/__init__.py +12 -0
  37. code_puppy/api/routers/agents.py +36 -0
  38. code_puppy/api/routers/commands.py +217 -0
  39. code_puppy/api/routers/config.py +74 -0
  40. code_puppy/api/routers/sessions.py +232 -0
  41. code_puppy/api/templates/terminal.html +361 -0
  42. code_puppy/api/websocket.py +154 -0
  43. code_puppy/callbacks.py +174 -4
  44. code_puppy/chatgpt_codex_client.py +283 -0
  45. code_puppy/claude_cache_client.py +586 -0
  46. code_puppy/cli_runner.py +916 -0
  47. code_puppy/command_line/add_model_menu.py +1079 -0
  48. code_puppy/command_line/agent_menu.py +395 -0
  49. code_puppy/command_line/attachments.py +395 -0
  50. code_puppy/command_line/autosave_menu.py +605 -0
  51. code_puppy/command_line/clipboard.py +527 -0
  52. code_puppy/command_line/colors_menu.py +520 -0
  53. code_puppy/command_line/command_handler.py +233 -627
  54. code_puppy/command_line/command_registry.py +150 -0
  55. code_puppy/command_line/config_commands.py +715 -0
  56. code_puppy/command_line/core_commands.py +792 -0
  57. code_puppy/command_line/diff_menu.py +863 -0
  58. code_puppy/command_line/load_context_completion.py +15 -22
  59. code_puppy/command_line/mcp/base.py +1 -4
  60. code_puppy/command_line/mcp/catalog_server_installer.py +175 -0
  61. code_puppy/command_line/mcp/custom_server_form.py +688 -0
  62. code_puppy/command_line/mcp/custom_server_installer.py +195 -0
  63. code_puppy/command_line/mcp/edit_command.py +148 -0
  64. code_puppy/command_line/mcp/handler.py +9 -4
  65. code_puppy/command_line/mcp/help_command.py +6 -5
  66. code_puppy/command_line/mcp/install_command.py +16 -27
  67. code_puppy/command_line/mcp/install_menu.py +685 -0
  68. code_puppy/command_line/mcp/list_command.py +3 -3
  69. code_puppy/command_line/mcp/logs_command.py +174 -65
  70. code_puppy/command_line/mcp/remove_command.py +2 -2
  71. code_puppy/command_line/mcp/restart_command.py +12 -4
  72. code_puppy/command_line/mcp/search_command.py +17 -11
  73. code_puppy/command_line/mcp/start_all_command.py +22 -13
  74. code_puppy/command_line/mcp/start_command.py +50 -31
  75. code_puppy/command_line/mcp/status_command.py +6 -7
  76. code_puppy/command_line/mcp/stop_all_command.py +11 -8
  77. code_puppy/command_line/mcp/stop_command.py +11 -10
  78. code_puppy/command_line/mcp/test_command.py +2 -2
  79. code_puppy/command_line/mcp/utils.py +1 -1
  80. code_puppy/command_line/mcp/wizard_utils.py +22 -18
  81. code_puppy/command_line/mcp_completion.py +174 -0
  82. code_puppy/command_line/model_picker_completion.py +89 -30
  83. code_puppy/command_line/model_settings_menu.py +884 -0
  84. code_puppy/command_line/motd.py +14 -8
  85. code_puppy/command_line/onboarding_slides.py +179 -0
  86. code_puppy/command_line/onboarding_wizard.py +340 -0
  87. code_puppy/command_line/pin_command_completion.py +329 -0
  88. code_puppy/command_line/prompt_toolkit_completion.py +626 -75
  89. code_puppy/command_line/session_commands.py +296 -0
  90. code_puppy/command_line/utils.py +54 -0
  91. code_puppy/config.py +1181 -51
  92. code_puppy/error_logging.py +118 -0
  93. code_puppy/gemini_code_assist.py +385 -0
  94. code_puppy/gemini_model.py +602 -0
  95. code_puppy/http_utils.py +220 -104
  96. code_puppy/keymap.py +128 -0
  97. code_puppy/main.py +5 -594
  98. code_puppy/{mcp → mcp_}/__init__.py +17 -0
  99. code_puppy/{mcp → mcp_}/async_lifecycle.py +35 -4
  100. code_puppy/{mcp → mcp_}/blocking_startup.py +70 -43
  101. code_puppy/{mcp → mcp_}/captured_stdio_server.py +2 -2
  102. code_puppy/{mcp → mcp_}/config_wizard.py +5 -5
  103. code_puppy/{mcp → mcp_}/dashboard.py +15 -6
  104. code_puppy/{mcp → mcp_}/examples/retry_example.py +4 -1
  105. code_puppy/{mcp → mcp_}/managed_server.py +66 -39
  106. code_puppy/{mcp → mcp_}/manager.py +146 -52
  107. code_puppy/mcp_/mcp_logs.py +224 -0
  108. code_puppy/{mcp → mcp_}/registry.py +6 -6
  109. code_puppy/{mcp → mcp_}/server_registry_catalog.py +25 -8
  110. code_puppy/messaging/__init__.py +199 -2
  111. code_puppy/messaging/bus.py +610 -0
  112. code_puppy/messaging/commands.py +167 -0
  113. code_puppy/messaging/markdown_patches.py +57 -0
  114. code_puppy/messaging/message_queue.py +17 -48
  115. code_puppy/messaging/messages.py +500 -0
  116. code_puppy/messaging/queue_console.py +1 -24
  117. code_puppy/messaging/renderers.py +43 -146
  118. code_puppy/messaging/rich_renderer.py +1027 -0
  119. code_puppy/messaging/spinner/__init__.py +33 -5
  120. code_puppy/messaging/spinner/console_spinner.py +92 -52
  121. code_puppy/messaging/spinner/spinner_base.py +29 -0
  122. code_puppy/messaging/subagent_console.py +461 -0
  123. code_puppy/model_factory.py +686 -80
  124. code_puppy/model_utils.py +167 -0
  125. code_puppy/models.json +86 -104
  126. code_puppy/models_dev_api.json +1 -0
  127. code_puppy/models_dev_parser.py +592 -0
  128. code_puppy/plugins/__init__.py +164 -10
  129. code_puppy/plugins/antigravity_oauth/__init__.py +10 -0
  130. code_puppy/plugins/antigravity_oauth/accounts.py +406 -0
  131. code_puppy/plugins/antigravity_oauth/antigravity_model.py +704 -0
  132. code_puppy/plugins/antigravity_oauth/config.py +42 -0
  133. code_puppy/plugins/antigravity_oauth/constants.py +136 -0
  134. code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
  135. code_puppy/plugins/antigravity_oauth/register_callbacks.py +406 -0
  136. code_puppy/plugins/antigravity_oauth/storage.py +271 -0
  137. code_puppy/plugins/antigravity_oauth/test_plugin.py +319 -0
  138. code_puppy/plugins/antigravity_oauth/token.py +167 -0
  139. code_puppy/plugins/antigravity_oauth/transport.py +767 -0
  140. code_puppy/plugins/antigravity_oauth/utils.py +169 -0
  141. code_puppy/plugins/chatgpt_oauth/__init__.py +8 -0
  142. code_puppy/plugins/chatgpt_oauth/config.py +52 -0
  143. code_puppy/plugins/chatgpt_oauth/oauth_flow.py +328 -0
  144. code_puppy/plugins/chatgpt_oauth/register_callbacks.py +94 -0
  145. code_puppy/plugins/chatgpt_oauth/test_plugin.py +293 -0
  146. code_puppy/plugins/chatgpt_oauth/utils.py +489 -0
  147. code_puppy/plugins/claude_code_oauth/README.md +167 -0
  148. code_puppy/plugins/claude_code_oauth/SETUP.md +93 -0
  149. code_puppy/plugins/claude_code_oauth/__init__.py +6 -0
  150. code_puppy/plugins/claude_code_oauth/config.py +50 -0
  151. code_puppy/plugins/claude_code_oauth/register_callbacks.py +308 -0
  152. code_puppy/plugins/claude_code_oauth/test_plugin.py +283 -0
  153. code_puppy/plugins/claude_code_oauth/utils.py +518 -0
  154. code_puppy/plugins/customizable_commands/__init__.py +0 -0
  155. code_puppy/plugins/customizable_commands/register_callbacks.py +169 -0
  156. code_puppy/plugins/example_custom_command/README.md +280 -0
  157. code_puppy/plugins/example_custom_command/register_callbacks.py +51 -0
  158. code_puppy/plugins/file_permission_handler/__init__.py +4 -0
  159. code_puppy/plugins/file_permission_handler/register_callbacks.py +523 -0
  160. code_puppy/plugins/frontend_emitter/__init__.py +25 -0
  161. code_puppy/plugins/frontend_emitter/emitter.py +121 -0
  162. code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
  163. code_puppy/plugins/oauth_puppy_html.py +228 -0
  164. code_puppy/plugins/shell_safety/__init__.py +6 -0
  165. code_puppy/plugins/shell_safety/agent_shell_safety.py +69 -0
  166. code_puppy/plugins/shell_safety/command_cache.py +156 -0
  167. code_puppy/plugins/shell_safety/register_callbacks.py +202 -0
  168. code_puppy/prompts/antigravity_system_prompt.md +1 -0
  169. code_puppy/prompts/codex_system_prompt.md +310 -0
  170. code_puppy/pydantic_patches.py +131 -0
  171. code_puppy/reopenable_async_client.py +8 -8
  172. code_puppy/round_robin_model.py +10 -15
  173. code_puppy/session_storage.py +294 -0
  174. code_puppy/status_display.py +21 -4
  175. code_puppy/summarization_agent.py +52 -14
  176. code_puppy/terminal_utils.py +418 -0
  177. code_puppy/tools/__init__.py +139 -6
  178. code_puppy/tools/agent_tools.py +548 -49
  179. code_puppy/tools/browser/__init__.py +37 -0
  180. code_puppy/tools/browser/browser_control.py +289 -0
  181. code_puppy/tools/browser/browser_interactions.py +545 -0
  182. code_puppy/tools/browser/browser_locators.py +640 -0
  183. code_puppy/tools/browser/browser_manager.py +316 -0
  184. code_puppy/tools/browser/browser_navigation.py +251 -0
  185. code_puppy/tools/browser/browser_screenshot.py +179 -0
  186. code_puppy/tools/browser/browser_scripts.py +462 -0
  187. code_puppy/tools/browser/browser_workflows.py +221 -0
  188. code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
  189. code_puppy/tools/browser/terminal_command_tools.py +521 -0
  190. code_puppy/tools/browser/terminal_screenshot_tools.py +556 -0
  191. code_puppy/tools/browser/terminal_tools.py +525 -0
  192. code_puppy/tools/command_runner.py +941 -153
  193. code_puppy/tools/common.py +1146 -6
  194. code_puppy/tools/display.py +84 -0
  195. code_puppy/tools/file_modifications.py +288 -89
  196. code_puppy/tools/file_operations.py +352 -266
  197. code_puppy/tools/subagent_context.py +158 -0
  198. code_puppy/uvx_detection.py +242 -0
  199. code_puppy/version_checker.py +30 -11
  200. code_puppy-0.0.366.data/data/code_puppy/models.json +110 -0
  201. code_puppy-0.0.366.data/data/code_puppy/models_dev_api.json +1 -0
  202. {code_puppy-0.0.169.dist-info → code_puppy-0.0.366.dist-info}/METADATA +184 -67
  203. code_puppy-0.0.366.dist-info/RECORD +217 -0
  204. {code_puppy-0.0.169.dist-info → code_puppy-0.0.366.dist-info}/WHEEL +1 -1
  205. {code_puppy-0.0.169.dist-info → code_puppy-0.0.366.dist-info}/entry_points.txt +1 -0
  206. code_puppy/agent.py +0 -231
  207. code_puppy/agents/agent_orchestrator.json +0 -26
  208. code_puppy/agents/runtime_manager.py +0 -272
  209. code_puppy/command_line/mcp/add_command.py +0 -183
  210. code_puppy/command_line/meta_command_handler.py +0 -153
  211. code_puppy/message_history_processor.py +0 -490
  212. code_puppy/messaging/spinner/textual_spinner.py +0 -101
  213. code_puppy/state_management.py +0 -200
  214. code_puppy/tui/__init__.py +0 -10
  215. code_puppy/tui/app.py +0 -986
  216. code_puppy/tui/components/__init__.py +0 -21
  217. code_puppy/tui/components/chat_view.py +0 -550
  218. code_puppy/tui/components/command_history_modal.py +0 -218
  219. code_puppy/tui/components/copy_button.py +0 -139
  220. code_puppy/tui/components/custom_widgets.py +0 -63
  221. code_puppy/tui/components/human_input_modal.py +0 -175
  222. code_puppy/tui/components/input_area.py +0 -167
  223. code_puppy/tui/components/sidebar.py +0 -309
  224. code_puppy/tui/components/status_bar.py +0 -182
  225. code_puppy/tui/messages.py +0 -27
  226. code_puppy/tui/models/__init__.py +0 -8
  227. code_puppy/tui/models/chat_message.py +0 -25
  228. code_puppy/tui/models/command_history.py +0 -89
  229. code_puppy/tui/models/enums.py +0 -24
  230. code_puppy/tui/screens/__init__.py +0 -15
  231. code_puppy/tui/screens/help.py +0 -130
  232. code_puppy/tui/screens/mcp_install_wizard.py +0 -803
  233. code_puppy/tui/screens/settings.py +0 -290
  234. code_puppy/tui/screens/tools.py +0 -74
  235. code_puppy-0.0.169.data/data/code_puppy/models.json +0 -128
  236. code_puppy-0.0.169.dist-info/RECORD +0 -112
  237. /code_puppy/{mcp → mcp_}/circuit_breaker.py +0 -0
  238. /code_puppy/{mcp → mcp_}/error_isolation.py +0 -0
  239. /code_puppy/{mcp → mcp_}/health_monitor.py +0 -0
  240. /code_puppy/{mcp → mcp_}/retry_manager.py +0 -0
  241. /code_puppy/{mcp → mcp_}/status_tracker.py +0 -0
  242. /code_puppy/{mcp → mcp_}/system_tools.py +0 -0
  243. {code_puppy-0.0.169.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 • Alt+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,182 +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
- 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_compaction_threshold
105
-
106
- get_compaction_threshold = get_compaction_threshold()
107
-
108
- if self.token_proportion > get_compaction_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
- get_compaction_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
@@ -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"]