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,12 +1,11 @@
1
1
  """
2
- Shared spinner implementation for both TUI and CLI modes.
2
+ Shared spinner implementation for CLI mode.
3
3
 
4
4
  This module provides consistent spinner animations across different UI modes.
5
5
  """
6
6
 
7
7
  from .console_spinner import ConsoleSpinner
8
8
  from .spinner_base import SpinnerBase
9
- from .textual_spinner import TextualSpinner
10
9
 
11
10
  # Keep track of all active spinners to manage them globally
12
11
  _active_spinners = []
@@ -25,7 +24,16 @@ def unregister_spinner(spinner):
25
24
 
26
25
 
27
26
  def pause_all_spinners():
28
- """Pause all active spinners."""
27
+ """Pause all active spinners.
28
+
29
+ No-op when called from a sub-agent context to prevent
30
+ parallel sub-agents from interfering with the main spinner.
31
+ """
32
+ # Lazy import to avoid circular dependency
33
+ from code_puppy.tools.subagent_context import is_subagent
34
+
35
+ if is_subagent():
36
+ return # Sub-agents don't control the main spinner
29
37
  for spinner in _active_spinners:
30
38
  try:
31
39
  spinner.pause()
@@ -35,7 +43,16 @@ def pause_all_spinners():
35
43
 
36
44
 
37
45
  def resume_all_spinners():
38
- """Resume all active spinners."""
46
+ """Resume all active spinners.
47
+
48
+ No-op when called from a sub-agent context to prevent
49
+ parallel sub-agents from interfering with the main spinner.
50
+ """
51
+ # Lazy import to avoid circular dependency
52
+ from code_puppy.tools.subagent_context import is_subagent
53
+
54
+ if is_subagent():
55
+ return # Sub-agents don't control the main spinner
39
56
  for spinner in _active_spinners:
40
57
  try:
41
58
  spinner.resume()
@@ -44,12 +61,23 @@ def resume_all_spinners():
44
61
  pass
45
62
 
46
63
 
64
+ def update_spinner_context(info: str) -> None:
65
+ """Update the shared context information displayed beside active spinners."""
66
+ SpinnerBase.set_context_info(info)
67
+
68
+
69
+ def clear_spinner_context() -> None:
70
+ """Clear any context information displayed beside active spinners."""
71
+ SpinnerBase.clear_context_info()
72
+
73
+
47
74
  __all__ = [
48
75
  "SpinnerBase",
49
- "TextualSpinner",
50
76
  "ConsoleSpinner",
51
77
  "register_spinner",
52
78
  "unregister_spinner",
53
79
  "pause_all_spinners",
54
80
  "resume_all_spinners",
81
+ "update_spinner_context",
82
+ "clear_spinner_context",
55
83
  ]
@@ -2,6 +2,7 @@
2
2
  Console spinner implementation for CLI mode using Rich's Live Display.
3
3
  """
4
4
 
5
+ import platform
5
6
  import threading
6
7
  import time
7
8
 
@@ -43,12 +44,15 @@ class ConsoleSpinner(SpinnerBase):
43
44
  if self._thread and self._thread.is_alive():
44
45
  return
45
46
 
47
+ # Print blank line before spinner for visual separation from content
48
+ self.console.print()
49
+
46
50
  # Create a Live display for the spinner
47
51
  self._live = Live(
48
52
  self._generate_spinner_panel(),
49
53
  console=self.console,
50
- refresh_per_second=10,
51
- transient=True,
54
+ refresh_per_second=20,
55
+ transient=True, # Clear the spinner line when stopped (no puppy litter!)
52
56
  auto_refresh=False, # Don't auto-refresh to avoid wiping out user input
53
57
  )
54
58
  self._live.start()
@@ -75,6 +79,33 @@ class ConsoleSpinner(SpinnerBase):
75
79
 
76
80
  self._thread = None
77
81
 
82
+ # Windows-specific cleanup: Rich's Live display can leave terminal in corrupted state
83
+ if platform.system() == "Windows":
84
+ import sys
85
+
86
+ try:
87
+ # Reset ANSI formatting for both stdout and stderr
88
+ sys.stdout.write("\x1b[0m") # Reset all attributes
89
+ sys.stdout.flush()
90
+ sys.stderr.write("\x1b[0m")
91
+ sys.stderr.flush()
92
+
93
+ # Clear the line and reposition cursor
94
+ sys.stdout.write("\r") # Return to start of line
95
+ sys.stdout.write("\x1b[K") # Clear to end of line
96
+ sys.stdout.flush()
97
+
98
+ # Flush keyboard input buffer to clear any stuck keys
99
+ try:
100
+ import msvcrt
101
+
102
+ while msvcrt.kbhit():
103
+ msvcrt.getch()
104
+ except ImportError:
105
+ pass # msvcrt not available (not Windows or different Python impl)
106
+ except Exception:
107
+ pass # Fail silently if cleanup doesn't work
108
+
78
109
  # Unregister this spinner from global management
79
110
  from . import unregister_spinner
80
111
 
@@ -86,23 +117,23 @@ class ConsoleSpinner(SpinnerBase):
86
117
 
87
118
  def _generate_spinner_panel(self):
88
119
  """Generate a Rich panel containing the spinner text."""
89
- if self._paused:
120
+ # Check if we're awaiting user input - show nothing during input prompts
121
+ from code_puppy.tools.command_runner import is_awaiting_user_input
122
+
123
+ if self._paused or is_awaiting_user_input():
90
124
  return Text("")
91
125
 
92
126
  text = Text()
93
127
 
94
- # Check if we're awaiting user input to determine which message to show
95
- from code_puppy.tools.command_runner import is_awaiting_user_input
96
-
97
- if is_awaiting_user_input():
98
- # Show waiting message when waiting for user input
99
- text.append(SpinnerBase.WAITING_MESSAGE, style="bold cyan")
100
- else:
101
- # Show thinking message during normal processing
102
- text.append(SpinnerBase.THINKING_MESSAGE, style="bold cyan")
103
-
128
+ # Show thinking message during normal processing
129
+ text.append(SpinnerBase.THINKING_MESSAGE, style="bold cyan")
104
130
  text.append(self.current_frame, style="bold cyan")
105
131
 
132
+ context_info = SpinnerBase.get_context_info()
133
+ if context_info:
134
+ text.append(" ")
135
+ text.append(context_info, style="bold white")
136
+
106
137
  # Return a simple Text object instead of a Panel for a cleaner look
107
138
  return text
108
139
 
@@ -127,39 +158,29 @@ class ConsoleSpinner(SpinnerBase):
127
158
  # Short sleep to control animation speed
128
159
  time.sleep(0.05)
129
160
  except Exception as e:
130
- print(f"\nSpinner error: {e}")
161
+ # Note: Using sys.stderr - can't use messaging during spinner
162
+ import sys
163
+
164
+ sys.stderr.write(f"\nSpinner error: {e}\n")
131
165
  self._is_spinning = False
132
166
 
133
167
  def pause(self):
134
168
  """Pause the spinner animation."""
135
169
  if self._is_spinning:
136
170
  self._paused = True
137
- # Update the live display to hide the spinner immediately
171
+ # Stop the live display completely to restore terminal echo during input
138
172
  if self._live:
139
173
  try:
140
- # When pausing, first update with the waiting message
141
- # so it's visible briefly before disappearing
142
- from code_puppy.tools.command_runner import is_awaiting_user_input
143
-
144
- if is_awaiting_user_input():
145
- text = Text()
146
- text.append(SpinnerBase.WAITING_MESSAGE, style="bold cyan")
147
- text.append(self.current_frame, style="bold cyan")
148
- self._live.update(text)
149
- self._live.refresh()
150
- # Allow a moment for the waiting message to be visible
151
- import time
152
-
153
- time.sleep(0.1)
154
-
155
- # Then clear the display
156
- self._live.update(Text(""))
174
+ self._live.stop()
175
+ self._live = None
176
+ # Clear the line to remove any artifacts
177
+ import sys
178
+
179
+ sys.stdout.write("\r") # Return to start of line
180
+ sys.stdout.write("\x1b[K") # Clear to end of line
181
+ sys.stdout.flush()
157
182
  except Exception:
158
- # If update fails, try stopping it completely
159
- try:
160
- self._live.stop()
161
- except Exception:
162
- pass
183
+ pass
163
184
 
164
185
  def resume(self):
165
186
  """Resume the spinner animation."""
@@ -171,24 +192,43 @@ class ConsoleSpinner(SpinnerBase):
171
192
 
172
193
  if self._is_spinning and self._paused:
173
194
  self._paused = False
174
- # Force an immediate update to show the spinner again
175
- if self._live:
195
+ # Restart the live display if it was stopped during pause
196
+ if not self._live:
197
+ try:
198
+ # Clear any leftover artifacts before starting
199
+ import sys
200
+
201
+ sys.stdout.write("\r") # Return to start of line
202
+ sys.stdout.write("\x1b[K") # Clear to end of line
203
+ sys.stdout.flush()
204
+
205
+ # Print blank line before spinner for visual separation
206
+ self.console.print()
207
+
208
+ self._live = Live(
209
+ self._generate_spinner_panel(),
210
+ console=self.console,
211
+ refresh_per_second=20,
212
+ transient=True, # Clear spinner line when stopped
213
+ auto_refresh=False,
214
+ )
215
+ self._live.start()
216
+ except Exception:
217
+ pass
218
+ else:
219
+ # If live display still exists, clear console state first
176
220
  try:
221
+ # Force Rich to reset any cached console state
222
+ if hasattr(self.console, "_buffer"):
223
+ # Clear Rich's internal buffer to prevent artifacts
224
+ self.console.file.write("\r") # Return to start
225
+ self.console.file.write("\x1b[K") # Clear line
226
+ self.console.file.flush()
227
+
177
228
  self._live.update(self._generate_spinner_panel())
229
+ self._live.refresh()
178
230
  except Exception:
179
- # If update fails, the live display might have been stopped
180
- # Try to restart it
181
- try:
182
- self._live = Live(
183
- self._generate_spinner_panel(),
184
- console=self.console,
185
- refresh_per_second=10,
186
- transient=True,
187
- auto_refresh=False, # Don't auto-refresh to avoid wiping out user input
188
- )
189
- self._live.start()
190
- except Exception:
191
- pass
231
+ pass
192
232
 
193
233
  def __enter__(self):
194
234
  """Support for context manager."""
@@ -3,6 +3,7 @@ Base spinner implementation to be extended for different UI modes.
3
3
  """
4
4
 
5
5
  from abc import ABC, abstractmethod
6
+ from threading import Lock
6
7
 
7
8
  from code_puppy.config import get_puppy_name
8
9
 
@@ -33,6 +34,9 @@ class SpinnerBase(ABC):
33
34
  # Current message - starts with thinking by default
34
35
  MESSAGE = THINKING_MESSAGE
35
36
 
37
+ _context_info: str = ""
38
+ _context_lock: Lock = Lock()
39
+
36
40
  def __init__(self):
37
41
  """Initialize the spinner."""
38
42
  self._is_spinning = False
@@ -64,3 +68,28 @@ class SpinnerBase(ABC):
64
68
  def is_spinning(self):
65
69
  """Check if the spinner is currently spinning."""
66
70
  return self._is_spinning
71
+
72
+ @classmethod
73
+ def set_context_info(cls, info: str) -> None:
74
+ """Set shared context information displayed beside the spinner."""
75
+ with cls._context_lock:
76
+ cls._context_info = info
77
+
78
+ @classmethod
79
+ def clear_context_info(cls) -> None:
80
+ """Clear any context information displayed beside the spinner."""
81
+ cls.set_context_info("")
82
+
83
+ @classmethod
84
+ def get_context_info(cls) -> str:
85
+ """Return the current spinner context information."""
86
+ with cls._context_lock:
87
+ return cls._context_info
88
+
89
+ @staticmethod
90
+ def format_context_info(total_tokens: int, capacity: int, proportion: float) -> str:
91
+ """Create a concise context summary for spinner display."""
92
+ if capacity <= 0:
93
+ return ""
94
+ proportion_pct = proportion * 100
95
+ return f"Tokens: {total_tokens:,}/{capacity:,} ({proportion_pct:.1f}% used)"