code-puppy 0.0.186__tar.gz → 0.0.188__tar.gz

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 (127) hide show
  1. {code_puppy-0.0.186 → code_puppy-0.0.188}/PKG-INFO +1 -3
  2. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/agents/base_agent.py +15 -14
  3. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/callbacks.py +32 -0
  4. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/command_line/command_handler.py +78 -0
  5. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/messaging/spinner/__init__.py +12 -0
  6. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/messaging/spinner/console_spinner.py +5 -0
  7. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/messaging/spinner/spinner_base.py +31 -0
  8. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/messaging/spinner/textual_spinner.py +6 -1
  9. code_puppy-0.0.188/code_puppy/plugins/example_custom_command/register_callbacks.py +51 -0
  10. {code_puppy-0.0.186 → code_puppy-0.0.188}/pyproject.toml +1 -3
  11. {code_puppy-0.0.186 → code_puppy-0.0.188}/.gitignore +0 -0
  12. {code_puppy-0.0.186 → code_puppy-0.0.188}/LICENSE +0 -0
  13. {code_puppy-0.0.186 → code_puppy-0.0.188}/README.md +0 -0
  14. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/__init__.py +0 -0
  15. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/__main__.py +0 -0
  16. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/agents/__init__.py +0 -0
  17. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/agents/agent_c_reviewer.py +0 -0
  18. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/agents/agent_code_puppy.py +0 -0
  19. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/agents/agent_code_reviewer.py +0 -0
  20. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/agents/agent_cpp_reviewer.py +0 -0
  21. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/agents/agent_creator_agent.py +0 -0
  22. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/agents/agent_golang_reviewer.py +0 -0
  23. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/agents/agent_javascript_reviewer.py +0 -0
  24. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/agents/agent_manager.py +0 -0
  25. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/agents/agent_python_reviewer.py +0 -0
  26. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/agents/agent_qa_expert.py +0 -0
  27. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/agents/agent_qa_kitten.py +0 -0
  28. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/agents/agent_security_auditor.py +0 -0
  29. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/agents/agent_typescript_reviewer.py +0 -0
  30. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/agents/json_agent.py +0 -0
  31. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/command_line/__init__.py +0 -0
  32. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/command_line/file_path_completion.py +0 -0
  33. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/command_line/load_context_completion.py +0 -0
  34. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/command_line/mcp/__init__.py +0 -0
  35. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/command_line/mcp/add_command.py +0 -0
  36. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/command_line/mcp/base.py +0 -0
  37. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/command_line/mcp/handler.py +0 -0
  38. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/command_line/mcp/help_command.py +0 -0
  39. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/command_line/mcp/install_command.py +0 -0
  40. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/command_line/mcp/list_command.py +0 -0
  41. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/command_line/mcp/logs_command.py +0 -0
  42. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/command_line/mcp/remove_command.py +0 -0
  43. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/command_line/mcp/restart_command.py +0 -0
  44. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/command_line/mcp/search_command.py +0 -0
  45. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/command_line/mcp/start_all_command.py +0 -0
  46. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/command_line/mcp/start_command.py +0 -0
  47. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/command_line/mcp/status_command.py +0 -0
  48. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/command_line/mcp/stop_all_command.py +0 -0
  49. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/command_line/mcp/stop_command.py +0 -0
  50. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/command_line/mcp/test_command.py +0 -0
  51. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/command_line/mcp/utils.py +0 -0
  52. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/command_line/mcp/wizard_utils.py +0 -0
  53. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/command_line/model_picker_completion.py +0 -0
  54. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/command_line/motd.py +0 -0
  55. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/command_line/prompt_toolkit_completion.py +0 -0
  56. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/command_line/utils.py +0 -0
  57. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/config.py +0 -0
  58. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/http_utils.py +0 -0
  59. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/main.py +0 -0
  60. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/mcp_/__init__.py +0 -0
  61. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/mcp_/async_lifecycle.py +0 -0
  62. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/mcp_/blocking_startup.py +0 -0
  63. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/mcp_/captured_stdio_server.py +0 -0
  64. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/mcp_/circuit_breaker.py +0 -0
  65. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/mcp_/config_wizard.py +0 -0
  66. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/mcp_/dashboard.py +0 -0
  67. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/mcp_/error_isolation.py +0 -0
  68. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/mcp_/examples/retry_example.py +0 -0
  69. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/mcp_/health_monitor.py +0 -0
  70. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/mcp_/managed_server.py +0 -0
  71. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/mcp_/manager.py +0 -0
  72. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/mcp_/registry.py +0 -0
  73. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/mcp_/retry_manager.py +0 -0
  74. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/mcp_/server_registry_catalog.py +0 -0
  75. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/mcp_/status_tracker.py +0 -0
  76. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/mcp_/system_tools.py +0 -0
  77. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/messaging/__init__.py +0 -0
  78. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/messaging/message_queue.py +0 -0
  79. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/messaging/queue_console.py +0 -0
  80. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/messaging/renderers.py +0 -0
  81. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/model_factory.py +0 -0
  82. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/models.json +0 -0
  83. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/plugins/__init__.py +0 -0
  84. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/reopenable_async_client.py +0 -0
  85. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/round_robin_model.py +0 -0
  86. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/status_display.py +0 -0
  87. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/summarization_agent.py +0 -0
  88. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/tools/__init__.py +0 -0
  89. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/tools/agent_tools.py +0 -0
  90. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/tools/browser/__init__.py +0 -0
  91. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/tools/browser/browser_control.py +0 -0
  92. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/tools/browser/browser_interactions.py +0 -0
  93. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/tools/browser/browser_locators.py +0 -0
  94. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/tools/browser/browser_navigation.py +0 -0
  95. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/tools/browser/browser_screenshot.py +0 -0
  96. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/tools/browser/browser_scripts.py +0 -0
  97. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/tools/browser/browser_workflows.py +0 -0
  98. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/tools/browser/camoufox_manager.py +0 -0
  99. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/tools/browser/vqa_agent.py +0 -0
  100. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/tools/command_runner.py +0 -0
  101. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/tools/common.py +0 -0
  102. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/tools/file_modifications.py +0 -0
  103. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/tools/file_operations.py +0 -0
  104. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/tools/tools_content.py +0 -0
  105. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/tui/__init__.py +0 -0
  106. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/tui/app.py +0 -0
  107. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/tui/components/__init__.py +0 -0
  108. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/tui/components/chat_view.py +0 -0
  109. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/tui/components/command_history_modal.py +0 -0
  110. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/tui/components/copy_button.py +0 -0
  111. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/tui/components/custom_widgets.py +0 -0
  112. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/tui/components/human_input_modal.py +0 -0
  113. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/tui/components/input_area.py +0 -0
  114. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/tui/components/sidebar.py +0 -0
  115. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/tui/components/status_bar.py +0 -0
  116. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/tui/messages.py +0 -0
  117. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/tui/models/__init__.py +0 -0
  118. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/tui/models/chat_message.py +0 -0
  119. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/tui/models/command_history.py +0 -0
  120. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/tui/models/enums.py +0 -0
  121. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/tui/screens/__init__.py +0 -0
  122. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/tui/screens/help.py +0 -0
  123. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/tui/screens/mcp_install_wizard.py +0 -0
  124. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/tui/screens/settings.py +0 -0
  125. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/tui/screens/tools.py +0 -0
  126. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/tui_state.py +0 -0
  127. {code_puppy-0.0.186 → code_puppy-0.0.188}/code_puppy/version_checker.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-puppy
3
- Version: 0.0.186
3
+ Version: 0.0.188
4
4
  Summary: Code generation agent
5
5
  Project-URL: repository, https://github.com/mpfaffenberger/code_puppy
6
6
  Project-URL: HomePage, https://github.com/mpfaffenberger/code_puppy
@@ -39,8 +39,6 @@ Requires-Dist: tenacity>=8.2.0
39
39
  Requires-Dist: termcolor>=3.1.0
40
40
  Requires-Dist: textual-dev>=1.7.0
41
41
  Requires-Dist: textual>=5.0.0
42
- Requires-Dist: tree-sitter-language-pack>=0.8.0
43
- Requires-Dist: tree-sitter-typescript>=0.23.2
44
42
  Requires-Dist: uvicorn>=0.29.0
45
43
  Description-Content-Type: text/markdown
46
44
 
@@ -42,6 +42,10 @@ from code_puppy.messaging import (
42
42
  emit_system_message,
43
43
  emit_warning,
44
44
  )
45
+ from code_puppy.messaging.spinner import (
46
+ SpinnerBase,
47
+ update_spinner_context,
48
+ )
45
49
  from code_puppy.model_factory import ModelFactory
46
50
  from code_puppy.summarization_agent import run_summarization_sync
47
51
  from code_puppy.tools.common import console
@@ -527,6 +531,11 @@ class BaseAgent(ABC):
527
531
  # Check if we're in TUI mode and can update the status bar
528
532
  from code_puppy.tui_state import get_tui_app_instance, is_tui_mode
529
533
 
534
+ context_summary = SpinnerBase.format_context_info(
535
+ total_current_tokens, model_max, proportion_used
536
+ )
537
+ update_spinner_context(context_summary)
538
+
530
539
  if is_tui_mode():
531
540
  tui_app = get_tui_app_instance()
532
541
  if tui_app:
@@ -538,22 +547,11 @@ class BaseAgent(ABC):
538
547
  )
539
548
  except Exception as e:
540
549
  emit_error(e)
541
- # Fallback to chat message if status bar update fails
542
- emit_info(
543
- f"\n[bold white on blue] Tokens in context: {total_current_tokens}, total model capacity: {model_max}, proportion used: {proportion_used:.2f} [/bold white on blue] \n",
544
- message_group="token_context_status",
545
- )
546
550
  else:
547
- # Fallback if no TUI app instance
548
551
  emit_info(
549
- f"\n[bold white on blue] Tokens in context: {total_current_tokens}, total model capacity: {model_max}, proportion used: {proportion_used:.2f} [/bold white on blue] \n",
552
+ f"Final token count after processing: {total_current_tokens}",
550
553
  message_group="token_context_status",
551
554
  )
552
- else:
553
- # Non-TUI mode - emit to console as before
554
- emit_info(
555
- f"\n[bold white on blue] Tokens in context: {total_current_tokens}, total model capacity: {model_max}, proportion used: {proportion_used:.2f} [/bold white on blue] \n"
556
- )
557
555
  # Get the configured compaction threshold
558
556
  compaction_threshold = get_compaction_threshold()
559
557
 
@@ -578,6 +576,11 @@ class BaseAgent(ABC):
578
576
  self.estimate_tokens_for_message(msg) for msg in result_messages
579
577
  )
580
578
  # Update status bar with final token count if in TUI mode
579
+ final_summary = SpinnerBase.format_context_info(
580
+ final_token_count, model_max, final_token_count / model_max
581
+ )
582
+ update_spinner_context(final_summary)
583
+
581
584
  if is_tui_mode():
582
585
  tui_app = get_tui_app_instance()
583
586
  if tui_app:
@@ -596,8 +599,6 @@ class BaseAgent(ABC):
596
599
  f"Final token count after processing: {final_token_count}",
597
600
  message_group="token_context_status",
598
601
  )
599
- else:
600
- emit_info(f"Final token count after processing: {final_token_count}")
601
602
  self.set_message_history(result_messages)
602
603
  for m in summarized_messages:
603
604
  self.add_compacted_message_hash(self.hash_message(m))
@@ -15,6 +15,8 @@ PhaseType = Literal[
15
15
  "load_model_config",
16
16
  "load_prompt",
17
17
  "agent_reload",
18
+ "custom_command",
19
+ "custom_command_help",
18
20
  ]
19
21
  CallbackFunc = Callable[..., Any]
20
22
 
@@ -30,6 +32,8 @@ _callbacks: Dict[PhaseType, List[CallbackFunc]] = {
30
32
  "load_model_config": [],
31
33
  "load_prompt": [],
32
34
  "agent_reload": [],
35
+ "custom_command": [],
36
+ "custom_command_help": [],
33
37
  }
34
38
 
35
39
  logger = logging.getLogger(__name__)
@@ -174,3 +178,31 @@ def on_agent_reload(*args, **kwargs) -> Any:
174
178
 
175
179
  def on_load_prompt():
176
180
  return _trigger_callbacks_sync("load_prompt")
181
+
182
+
183
+ def on_custom_command_help() -> List[Any]:
184
+ """Collect custom command help entries from plugins.
185
+
186
+ Each callback should return a list of tuples [(name, description), ...]
187
+ or a single tuple, or None. We'll flatten and sanitize results.
188
+ """
189
+ return _trigger_callbacks_sync("custom_command_help")
190
+
191
+
192
+ def on_custom_command(command: str, name: str) -> List[Any]:
193
+ """Trigger custom command callbacks.
194
+
195
+ This allows plugins to register handlers for slash commands
196
+ that are not built into the core command handler.
197
+
198
+ Args:
199
+ command: The full command string (e.g., "/foo bar baz").
200
+ name: The primary command name without the leading slash (e.g., "foo").
201
+
202
+ Returns:
203
+ Implementations may return:
204
+ - True if the command was handled (and no further action is needed)
205
+ - A string to be processed as user input by the caller
206
+ - None to indicate not handled
207
+ """
208
+ return _trigger_callbacks_sync("custom_command", command, name)
@@ -11,6 +11,9 @@ def get_commands_help():
11
11
  """Generate commands help using Rich Text objects to avoid markup conflicts."""
12
12
  from rich.text import Text
13
13
 
14
+ # Ensure plugins are loaded so custom help can register
15
+ _ensure_plugins_loaded()
16
+
14
17
  # Build help text programmatically
15
18
  help_lines = []
16
19
 
@@ -90,6 +93,33 @@ def get_commands_help():
90
93
  + Text(" Show unknown command warning")
91
94
  )
92
95
 
96
+ # Add custom commands from plugins (if any)
97
+ try:
98
+ from code_puppy import callbacks
99
+
100
+ custom_help_results = callbacks.on_custom_command_help()
101
+ # Flatten various returns into a list of (name, description)
102
+ custom_entries = []
103
+ for res in custom_help_results:
104
+ if not res:
105
+ continue
106
+ if isinstance(res, tuple) and len(res) == 2:
107
+ custom_entries.append(res)
108
+ elif isinstance(res, list):
109
+ for item in res:
110
+ if isinstance(item, tuple) and len(item) == 2:
111
+ custom_entries.append(item)
112
+ if custom_entries:
113
+ help_lines.append(Text("\n", style="dim"))
114
+ help_lines.append(Text("Custom Commands", style="bold magenta"))
115
+ for name, desc in custom_entries:
116
+ help_lines.append(
117
+ Text(f"/{name}", style="cyan") + Text(f" {desc}")
118
+ )
119
+ except Exception:
120
+ # If callbacks fail, skip custom help silently
121
+ pass
122
+
93
123
  # Combine all lines
94
124
  final_text = Text()
95
125
  for i, line in enumerate(help_lines):
@@ -100,8 +130,32 @@ def get_commands_help():
100
130
  return final_text
101
131
 
102
132
 
133
+ _PLUGINS_LOADED = False
134
+
135
+
136
+ def _ensure_plugins_loaded() -> None:
137
+ global _PLUGINS_LOADED
138
+ if _PLUGINS_LOADED:
139
+ return
140
+ try:
141
+ from code_puppy import plugins
142
+
143
+ plugins.load_plugin_callbacks()
144
+ _PLUGINS_LOADED = True
145
+ except Exception as e:
146
+ # If plugins fail to load, continue gracefully but note it
147
+ try:
148
+ from code_puppy.messaging import emit_warning
149
+
150
+ emit_warning(f"Plugin load error: {e}")
151
+ except Exception:
152
+ pass
153
+ _PLUGINS_LOADED = True
154
+
155
+
103
156
  def handle_command(command: str):
104
157
  from code_puppy.messaging import emit_error, emit_info, emit_success, emit_warning
158
+ _ensure_plugins_loaded()
105
159
 
106
160
  """
107
161
  Handle commands prefixed with '/'.
@@ -400,6 +454,8 @@ def handle_command(command: str):
400
454
 
401
455
  handler = MCPCommandHandler()
402
456
  return handler.handle_mcp_command(command)
457
+
458
+ # Built-in help
403
459
  if command in ("/help", "/h"):
404
460
  import uuid
405
461
 
@@ -705,8 +761,30 @@ def handle_command(command: str):
705
761
  # Signal to the main app that we want to exit
706
762
  # The actual exit handling is done in main.py
707
763
  return True
764
+
765
+ # Try plugin-provided custom commands before unknown warning
708
766
  if command.startswith("/"):
767
+ # Extract command name without leading slash and arguments intact
709
768
  name = command[1:].split()[0] if len(command) > 1 else ""
769
+ try:
770
+ from code_puppy import callbacks
771
+
772
+ results = callbacks.on_custom_command(command=command, name=name)
773
+ # Iterate through callback results; treat str as handled (no model run)
774
+ for res in results:
775
+ if res is True:
776
+ return True
777
+ if isinstance(res, str):
778
+ # Display returned text to the user and treat as handled
779
+ try:
780
+ emit_info(res)
781
+ except Exception:
782
+ pass
783
+ return True
784
+ except Exception as e:
785
+ # Log via emit_error but do not block default handling
786
+ emit_warning(f"Custom command hook error: {e}")
787
+
710
788
  if name:
711
789
  emit_warning(
712
790
  f"Unknown command: {command}\n[dim]Type /help for options.[/dim]"
@@ -44,6 +44,16 @@ def resume_all_spinners():
44
44
  pass
45
45
 
46
46
 
47
+ def update_spinner_context(info: str) -> None:
48
+ """Update the shared context information displayed beside active spinners."""
49
+ SpinnerBase.set_context_info(info)
50
+
51
+
52
+ def clear_spinner_context() -> None:
53
+ """Clear any context information displayed beside active spinners."""
54
+ SpinnerBase.clear_context_info()
55
+
56
+
47
57
  __all__ = [
48
58
  "SpinnerBase",
49
59
  "TextualSpinner",
@@ -52,4 +62,6 @@ __all__ = [
52
62
  "unregister_spinner",
53
63
  "pause_all_spinners",
54
64
  "resume_all_spinners",
65
+ "update_spinner_context",
66
+ "clear_spinner_context",
55
67
  ]
@@ -103,6 +103,11 @@ class ConsoleSpinner(SpinnerBase):
103
103
 
104
104
  text.append(self.current_frame, style="bold cyan")
105
105
 
106
+ context_info = SpinnerBase.get_context_info()
107
+ if context_info:
108
+ text.append(" ")
109
+ text.append(context_info, style="bold white")
110
+
106
111
  # Return a simple Text object instead of a Panel for a cleaner look
107
112
  return text
108
113
 
@@ -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,30 @@ 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 (
96
+ f"Tokens: {total_tokens:,}/{capacity:,} ({proportion_pct:.1f}% used)"
97
+ )
@@ -70,8 +70,13 @@ class TextualSpinner(Static):
70
70
  # Show thinking message during normal processing
71
71
  message = SpinnerBase.THINKING_MESSAGE
72
72
 
73
+ context_info = SpinnerBase.get_context_info()
74
+ context_segment = (
75
+ f" [bold white]{context_info}[/bold white]" if context_info else ""
76
+ )
77
+
73
78
  self.update(
74
- f"[bold cyan]{message}[/bold cyan][bold cyan]{current_frame}[/bold cyan]"
79
+ f"[bold cyan]{message}[/bold cyan][bold cyan]{current_frame}[/bold cyan]{context_segment}"
75
80
  )
76
81
 
77
82
  def pause(self):
@@ -0,0 +1,51 @@
1
+ from code_puppy.callbacks import register_callback
2
+ from code_puppy.messaging import emit_info
3
+
4
+
5
+ def _custom_help():
6
+ return [
7
+ ("woof", "Emit a playful woof message (no model)"),
8
+ ("echo", "Echo back your text (display only)"),
9
+ ]
10
+
11
+
12
+ def _handle_custom_command(command: str, name: str):
13
+ """Handle a demo custom command.
14
+
15
+ Policy: custom commands must NOT invoke the model. They should emit
16
+ messages or return True to indicate handling. Returning a string is
17
+ treated as a display-only message by the command handler.
18
+
19
+ Supports:
20
+ - /woof → emits a fun message and returns True
21
+ - /echo <text> → emits the text (display-only)
22
+ """
23
+ if not name:
24
+ return None
25
+
26
+ if name == "woof":
27
+ # If extra text is provided, pass it as a prompt; otherwise, send a fun default
28
+ parts = command.split(maxsplit=1)
29
+ if len(parts) == 2:
30
+ text = parts[1]
31
+ emit_info(f"🐶 Woof! sending prompt: {text}")
32
+ return text
33
+ emit_info("🐶 Woof! sending prompt: Tell me a dog fact")
34
+ return "Tell me a dog fact"
35
+
36
+ if name == "echo":
37
+ # Return the rest of the command (after the name) to be treated as input
38
+ # Example: "/echo Hello" → returns "Hello"
39
+ rest = command.split(maxsplit=1)
40
+ if len(rest) == 2:
41
+ text = rest[1]
42
+ emit_info(f"[dim]example plugin echo ->[/dim] {text}")
43
+ return text
44
+ emit_info("[dim]example plugin echo (empty)[/dim]")
45
+ return ""
46
+
47
+ return None
48
+
49
+
50
+ register_callback("custom_command_help", _custom_help)
51
+ register_callback("custom_command", _handle_custom_command)
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "code-puppy"
7
- version = "0.0.186"
7
+ version = "0.0.188"
8
8
  description = "Code generation agent"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -23,8 +23,6 @@ dependencies = [
23
23
  "pathspec>=0.11.0",
24
24
  "rapidfuzz>=3.13.0",
25
25
  "json-repair>=0.46.2",
26
- "tree-sitter-language-pack>=0.8.0",
27
- "tree-sitter-typescript>=0.23.2",
28
26
  "fastapi>=0.110.0",
29
27
  "uvicorn>=0.29.0",
30
28
  "PyJWT>=2.8.0",
File without changes
File without changes
File without changes