code-puppy 0.0.287__py3-none-any.whl → 0.0.323__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 (110) hide show
  1. code_puppy/__init__.py +3 -1
  2. code_puppy/agents/agent_code_puppy.py +5 -4
  3. code_puppy/agents/agent_creator_agent.py +22 -18
  4. code_puppy/agents/agent_manager.py +2 -2
  5. code_puppy/agents/base_agent.py +496 -102
  6. code_puppy/callbacks.py +8 -0
  7. code_puppy/chatgpt_codex_client.py +283 -0
  8. code_puppy/cli_runner.py +795 -0
  9. code_puppy/command_line/add_model_menu.py +19 -16
  10. code_puppy/command_line/attachments.py +10 -5
  11. code_puppy/command_line/autosave_menu.py +269 -41
  12. code_puppy/command_line/colors_menu.py +515 -0
  13. code_puppy/command_line/command_handler.py +10 -24
  14. code_puppy/command_line/config_commands.py +106 -25
  15. code_puppy/command_line/core_commands.py +32 -20
  16. code_puppy/command_line/mcp/add_command.py +3 -16
  17. code_puppy/command_line/mcp/base.py +0 -3
  18. code_puppy/command_line/mcp/catalog_server_installer.py +15 -15
  19. code_puppy/command_line/mcp/custom_server_form.py +66 -5
  20. code_puppy/command_line/mcp/custom_server_installer.py +17 -17
  21. code_puppy/command_line/mcp/edit_command.py +15 -22
  22. code_puppy/command_line/mcp/handler.py +7 -2
  23. code_puppy/command_line/mcp/help_command.py +2 -2
  24. code_puppy/command_line/mcp/install_command.py +10 -14
  25. code_puppy/command_line/mcp/install_menu.py +2 -6
  26. code_puppy/command_line/mcp/list_command.py +2 -2
  27. code_puppy/command_line/mcp/logs_command.py +174 -65
  28. code_puppy/command_line/mcp/remove_command.py +2 -2
  29. code_puppy/command_line/mcp/restart_command.py +7 -2
  30. code_puppy/command_line/mcp/search_command.py +16 -10
  31. code_puppy/command_line/mcp/start_all_command.py +16 -6
  32. code_puppy/command_line/mcp/start_command.py +12 -10
  33. code_puppy/command_line/mcp/status_command.py +4 -5
  34. code_puppy/command_line/mcp/stop_all_command.py +5 -1
  35. code_puppy/command_line/mcp/stop_command.py +6 -4
  36. code_puppy/command_line/mcp/test_command.py +2 -2
  37. code_puppy/command_line/mcp/wizard_utils.py +20 -16
  38. code_puppy/command_line/model_settings_menu.py +53 -7
  39. code_puppy/command_line/motd.py +1 -1
  40. code_puppy/command_line/pin_command_completion.py +82 -7
  41. code_puppy/command_line/prompt_toolkit_completion.py +32 -9
  42. code_puppy/command_line/session_commands.py +11 -4
  43. code_puppy/config.py +217 -53
  44. code_puppy/error_logging.py +118 -0
  45. code_puppy/gemini_code_assist.py +385 -0
  46. code_puppy/keymap.py +126 -0
  47. code_puppy/main.py +5 -745
  48. code_puppy/mcp_/__init__.py +17 -0
  49. code_puppy/mcp_/blocking_startup.py +63 -36
  50. code_puppy/mcp_/captured_stdio_server.py +1 -1
  51. code_puppy/mcp_/config_wizard.py +4 -4
  52. code_puppy/mcp_/dashboard.py +15 -6
  53. code_puppy/mcp_/managed_server.py +25 -5
  54. code_puppy/mcp_/manager.py +65 -0
  55. code_puppy/mcp_/mcp_logs.py +224 -0
  56. code_puppy/mcp_/registry.py +6 -6
  57. code_puppy/messaging/__init__.py +184 -2
  58. code_puppy/messaging/bus.py +610 -0
  59. code_puppy/messaging/commands.py +167 -0
  60. code_puppy/messaging/markdown_patches.py +57 -0
  61. code_puppy/messaging/message_queue.py +3 -3
  62. code_puppy/messaging/messages.py +470 -0
  63. code_puppy/messaging/renderers.py +43 -141
  64. code_puppy/messaging/rich_renderer.py +900 -0
  65. code_puppy/messaging/spinner/console_spinner.py +39 -2
  66. code_puppy/model_factory.py +292 -53
  67. code_puppy/model_utils.py +57 -48
  68. code_puppy/models.json +19 -5
  69. code_puppy/plugins/__init__.py +152 -10
  70. code_puppy/plugins/chatgpt_oauth/config.py +20 -12
  71. code_puppy/plugins/chatgpt_oauth/oauth_flow.py +5 -6
  72. code_puppy/plugins/chatgpt_oauth/register_callbacks.py +3 -3
  73. code_puppy/plugins/chatgpt_oauth/test_plugin.py +30 -13
  74. code_puppy/plugins/chatgpt_oauth/utils.py +180 -65
  75. code_puppy/plugins/claude_code_oauth/config.py +15 -11
  76. code_puppy/plugins/claude_code_oauth/register_callbacks.py +28 -0
  77. code_puppy/plugins/claude_code_oauth/utils.py +6 -1
  78. code_puppy/plugins/example_custom_command/register_callbacks.py +2 -2
  79. code_puppy/plugins/oauth_puppy_html.py +3 -0
  80. code_puppy/plugins/shell_safety/agent_shell_safety.py +1 -134
  81. code_puppy/plugins/shell_safety/command_cache.py +156 -0
  82. code_puppy/plugins/shell_safety/register_callbacks.py +77 -3
  83. code_puppy/prompts/codex_system_prompt.md +310 -0
  84. code_puppy/pydantic_patches.py +131 -0
  85. code_puppy/session_storage.py +2 -1
  86. code_puppy/status_display.py +7 -5
  87. code_puppy/terminal_utils.py +126 -0
  88. code_puppy/tools/agent_tools.py +131 -70
  89. code_puppy/tools/browser/browser_control.py +10 -14
  90. code_puppy/tools/browser/browser_interactions.py +20 -28
  91. code_puppy/tools/browser/browser_locators.py +27 -29
  92. code_puppy/tools/browser/browser_navigation.py +9 -9
  93. code_puppy/tools/browser/browser_screenshot.py +12 -14
  94. code_puppy/tools/browser/browser_scripts.py +17 -29
  95. code_puppy/tools/browser/browser_workflows.py +24 -25
  96. code_puppy/tools/browser/camoufox_manager.py +22 -26
  97. code_puppy/tools/command_runner.py +410 -88
  98. code_puppy/tools/common.py +51 -38
  99. code_puppy/tools/file_modifications.py +98 -24
  100. code_puppy/tools/file_operations.py +113 -202
  101. code_puppy/version_checker.py +28 -13
  102. {code_puppy-0.0.287.data → code_puppy-0.0.323.data}/data/code_puppy/models.json +19 -5
  103. {code_puppy-0.0.287.dist-info → code_puppy-0.0.323.dist-info}/METADATA +3 -8
  104. code_puppy-0.0.323.dist-info/RECORD +168 -0
  105. code_puppy/tui_state.py +0 -55
  106. code_puppy-0.0.287.dist-info/RECORD +0 -153
  107. {code_puppy-0.0.287.data → code_puppy-0.0.323.data}/data/code_puppy/models_dev_api.json +0 -0
  108. {code_puppy-0.0.287.dist-info → code_puppy-0.0.323.dist-info}/WHEEL +0 -0
  109. {code_puppy-0.0.287.dist-info → code_puppy-0.0.323.dist-info}/entry_points.txt +0 -0
  110. {code_puppy-0.0.287.dist-info → code_puppy-0.0.323.dist-info}/licenses/LICENSE +0 -0
@@ -7,7 +7,9 @@ Provides interactive functionality for installing and configuring MCP servers.
7
7
  import logging
8
8
  from typing import Any, Dict, Optional
9
9
 
10
- from code_puppy.messaging import emit_info, emit_prompt
10
+ from rich.text import Text
11
+
12
+ from code_puppy.messaging import emit_error, emit_info, emit_prompt
11
13
 
12
14
  # Configure logging
13
15
  logger = logging.getLogger(__name__)
@@ -51,7 +53,7 @@ def run_interactive_install_wizard(manager, group_id: str) -> bool:
51
53
  required_env_vars = selected_server.get_environment_vars()
52
54
  if required_env_vars:
53
55
  emit_info(
54
- "\n[yellow]Required Environment Variables:[/yellow]",
56
+ Text.from_markup("\n[yellow]Required Environment Variables:[/yellow]"),
55
57
  message_group=group_id,
56
58
  )
57
59
  for var in required_env_vars:
@@ -61,7 +63,8 @@ def run_interactive_install_wizard(manager, group_id: str) -> bool:
61
63
  current_value = os.environ.get(var, "")
62
64
  if current_value:
63
65
  emit_info(
64
- f" {var}: [green]Already set[/green]", message_group=group_id
66
+ Text.from_markup(f" {var}: [green]Already set[/green]"),
67
+ message_group=group_id,
65
68
  )
66
69
  env_vars[var] = current_value
67
70
  else:
@@ -73,7 +76,8 @@ def run_interactive_install_wizard(manager, group_id: str) -> bool:
73
76
  required_cmd_args = selected_server.get_command_line_args()
74
77
  if required_cmd_args:
75
78
  emit_info(
76
- "\n[yellow]Command Line Arguments:[/yellow]", message_group=group_id
79
+ Text.from_markup("\n[yellow]Command Line Arguments:[/yellow]"),
80
+ message_group=group_id,
77
81
  )
78
82
  for arg_config in required_cmd_args:
79
83
  name = arg_config.get("name", "")
@@ -101,11 +105,11 @@ def run_interactive_install_wizard(manager, group_id: str) -> bool:
101
105
  )
102
106
 
103
107
  except ImportError:
104
- emit_info("[red]Server catalog not available[/red]", message_group=group_id)
108
+ emit_error("Server catalog not available", message_group=group_id)
105
109
  return False
106
110
  except Exception as e:
107
111
  logger.error(f"Error in interactive wizard: {e}")
108
- emit_info(f"[red]Wizard error: {e}[/red]", message_group=group_id)
112
+ emit_error(f"Wizard error: {e}", message_group=group_id)
109
113
  return False
110
114
 
111
115
 
@@ -122,9 +126,7 @@ def interactive_server_selection(group_id: str):
122
126
 
123
127
  servers = catalog.get_popular(10)
124
128
  if not servers:
125
- emit_info(
126
- "[red]No servers available in catalog[/red]", message_group=group_id
127
- )
129
+ emit_info("No servers available in catalog", message_group=group_id)
128
130
  return None
129
131
 
130
132
  emit_info("Popular MCP Servers:", message_group=group_id)
@@ -156,10 +158,10 @@ def interactive_server_selection(group_id: str):
156
158
  if 0 <= index < len(servers):
157
159
  return servers[index]
158
160
  else:
159
- emit_info("[red]Invalid selection[/red]", message_group=group_id)
161
+ emit_error("Invalid selection", message_group=group_id)
160
162
  return None
161
163
  except ValueError:
162
- emit_info("[red]Invalid input[/red]", message_group=group_id)
164
+ emit_error("Invalid input", message_group=group_id)
163
165
  return None
164
166
 
165
167
  except Exception as e:
@@ -215,7 +217,7 @@ def interactive_configure_server(
215
217
  if env_vars:
216
218
  emit_info("Environment Variables:", message_group=group_id)
217
219
  for var, value in env_vars.items():
218
- emit_info(f" {var}: [hidden]{value}[/hidden]", message_group=group_id)
220
+ emit_info(f" {var}: ***", message_group=group_id)
219
221
 
220
222
  if cmd_args:
221
223
  emit_info("Command Line Arguments:", message_group=group_id)
@@ -234,7 +236,7 @@ def interactive_configure_server(
234
236
 
235
237
  except Exception as e:
236
238
  logger.error(f"Error configuring server: {e}")
237
- emit_info(f"[red]Configuration error: {e}[/red]", message_group=group_id)
239
+ emit_error(f"Configuration error: {e}", message_group=group_id)
238
240
  return False
239
241
 
240
242
 
@@ -288,7 +290,7 @@ def install_server_from_catalog(
288
290
 
289
291
  if not server_id:
290
292
  emit_info(
291
- "[red]Failed to register server with manager[/red]",
293
+ "Failed to register server with manager",
292
294
  message_group=group_id,
293
295
  )
294
296
  return False
@@ -314,7 +316,9 @@ def install_server_from_catalog(
314
316
  json.dump(data, f, indent=2)
315
317
 
316
318
  emit_info(
317
- f"[green]✓ Successfully installed server: {server_name}[/green]",
319
+ Text.from_markup(
320
+ f"[green]✓ Successfully installed server: {server_name}[/green]"
321
+ ),
318
322
  message_group=group_id,
319
323
  )
320
324
  emit_info(
@@ -326,5 +330,5 @@ def install_server_from_catalog(
326
330
 
327
331
  except Exception as e:
328
332
  logger.error(f"Error installing server: {e}")
329
- emit_info(f"[red]Installation failed: {e}[/red]", message_group=group_id)
333
+ emit_error(f"Installation failed: {e}", message_group=group_id)
330
334
  return False
@@ -58,7 +58,7 @@ SETTING_DEFINITIONS: Dict[str, Dict] = {
58
58
  "name": "Reasoning Effort",
59
59
  "description": "Controls how much effort GPT-5 models spend on reasoning. Higher = more thorough but slower.",
60
60
  "type": "choice",
61
- "choices": ["low", "medium", "high"],
61
+ "choices": ["minimal", "low", "medium", "high", "xhigh"],
62
62
  "default": "medium",
63
63
  },
64
64
  "verbosity": {
@@ -72,7 +72,7 @@ SETTING_DEFINITIONS: Dict[str, Dict] = {
72
72
  "name": "Extended Thinking",
73
73
  "description": "Enable Claude's extended thinking mode for complex reasoning tasks.",
74
74
  "type": "boolean",
75
- "default": False,
75
+ "default": True,
76
76
  },
77
77
  "budget_tokens": {
78
78
  "name": "Thinking Budget (tokens)",
@@ -84,6 +84,12 @@ SETTING_DEFINITIONS: Dict[str, Dict] = {
84
84
  "default": 10000,
85
85
  "format": "{:.0f}",
86
86
  },
87
+ "interleaved_thinking": {
88
+ "name": "Interleaved Thinking",
89
+ "description": "Enable thinking between tool calls (Claude 4 only: Opus 4.5, Opus 4.1, Opus 4, Sonnet 4). Adds beta header. WARNING: On Vertex/Bedrock, this FAILS for non-Claude 4 models!",
90
+ "type": "boolean",
91
+ "default": False,
92
+ },
87
93
  }
88
94
 
89
95
 
@@ -93,6 +99,42 @@ def _load_all_model_names() -> List[str]:
93
99
  return list(models_config.keys())
94
100
 
95
101
 
102
+ def _get_setting_choices(
103
+ setting_key: str, model_name: Optional[str] = None
104
+ ) -> List[str]:
105
+ """Get the available choices for a setting, filtered by model capabilities.
106
+
107
+ For reasoning_effort, only codex models support 'xhigh' - regular GPT-5.2
108
+ models are capped at 'high'.
109
+
110
+ Args:
111
+ setting_key: The setting name (e.g., 'reasoning_effort', 'verbosity')
112
+ model_name: Optional model name to filter choices for
113
+
114
+ Returns:
115
+ List of valid choices for this setting and model combination.
116
+ """
117
+ setting_def = SETTING_DEFINITIONS.get(setting_key, {})
118
+ if setting_def.get("type") != "choice":
119
+ return []
120
+
121
+ base_choices = setting_def.get("choices", [])
122
+
123
+ # For reasoning_effort, filter 'xhigh' based on model support
124
+ if setting_key == "reasoning_effort" and model_name:
125
+ models_config = ModelFactory.load_config()
126
+ model_config = models_config.get(model_name, {})
127
+
128
+ # Check if model supports xhigh reasoning
129
+ supports_xhigh = model_config.get("supports_xhigh_reasoning", False)
130
+
131
+ if not supports_xhigh:
132
+ # Remove xhigh from choices for non-codex models
133
+ return [c for c in base_choices if c != "xhigh"]
134
+
135
+ return base_choices
136
+
137
+
96
138
  class ModelSettingsMenu:
97
139
  """Interactive TUI for model settings configuration.
98
140
 
@@ -427,7 +469,8 @@ class ModelSettingsMenu:
427
469
  if setting_def.get("type") == "choice":
428
470
  lines.append(("bold", " Options:"))
429
471
  lines.append(("", "\n"))
430
- choices = setting_def.get("choices", [])
472
+ # Get filtered choices based on model capabilities
473
+ choices = _get_setting_choices(setting_key, self.selected_model)
431
474
  lines.append(
432
475
  (
433
476
  "fg:ansibrightblack",
@@ -514,8 +557,11 @@ class ModelSettingsMenu:
514
557
  if current is not None:
515
558
  self.edit_value = current
516
559
  elif setting_def.get("type") == "choice":
517
- # For choice settings, start with the default
518
- self.edit_value = setting_def.get("default", setting_def["choices"][0])
560
+ # For choice settings, start with the default (using filtered choices)
561
+ choices = _get_setting_choices(setting_key, self.selected_model)
562
+ self.edit_value = setting_def.get(
563
+ "default", choices[0] if choices else None
564
+ )
519
565
  elif setting_def.get("type") == "boolean":
520
566
  # For boolean settings, start with the default
521
567
  self.edit_value = setting_def.get("default", False)
@@ -541,8 +587,8 @@ class ModelSettingsMenu:
541
587
  setting_def = SETTING_DEFINITIONS[setting_key]
542
588
 
543
589
  if setting_def.get("type") == "choice":
544
- # Cycle through choices
545
- choices = setting_def["choices"]
590
+ # Cycle through filtered choices based on model capabilities
591
+ choices = _get_setting_choices(setting_key, self.selected_model)
546
592
  current_idx = (
547
593
  choices.index(self.edit_value) if self.edit_value in choices else 0
548
594
  )
@@ -1,6 +1,6 @@
1
1
  """
2
2
  🐶 MOTD (Message of the Day) feature for code-puppy! 🐕
3
- Stores seen versions in ~/.code_puppy/motd.txt - woof woof! 🐾
3
+ Stores seen versions in XDG_CONFIG_HOME/code_puppy/motd.txt - woof woof! 🐾
4
4
  """
5
5
 
6
6
  import os
@@ -1,9 +1,84 @@
1
+ import json
1
2
  from typing import Iterable
2
3
 
3
4
  from prompt_toolkit.completion import Completer, Completion
4
5
  from prompt_toolkit.document import Document
5
6
 
6
7
 
8
+ def _get_json_agents_for_model(model_name: str) -> list:
9
+ """Get JSON agents that have this model pinned in their JSON file."""
10
+ try:
11
+ from code_puppy.agents.json_agent import discover_json_agents
12
+
13
+ pinned = []
14
+ json_agents = discover_json_agents()
15
+ for agent_name, agent_path in json_agents.items():
16
+ try:
17
+ with open(agent_path, "r") as f:
18
+ agent_data = json.load(f)
19
+ if agent_data.get("model") == model_name:
20
+ pinned.append(agent_name)
21
+ except Exception:
22
+ continue
23
+ return pinned
24
+ except Exception:
25
+ return []
26
+
27
+
28
+ def _get_pinned_model_for_agent(agent_name: str) -> str | None:
29
+ """Get the pinned model for an agent (config or JSON)."""
30
+ # Check config first (for built-in agents)
31
+ try:
32
+ from code_puppy.config import get_agent_pinned_model
33
+
34
+ pinned = get_agent_pinned_model(agent_name)
35
+ if pinned:
36
+ return pinned
37
+ except Exception:
38
+ pass
39
+
40
+ # Check if it's a JSON agent with a model key
41
+ try:
42
+ from code_puppy.agents.json_agent import discover_json_agents
43
+
44
+ json_agents = discover_json_agents()
45
+ if agent_name in json_agents:
46
+ with open(json_agents[agent_name], "r") as f:
47
+ agent_data = json.load(f)
48
+ return agent_data.get("model")
49
+ except Exception:
50
+ pass
51
+
52
+ return None
53
+
54
+
55
+ def _get_model_display_meta(model_name: str) -> str:
56
+ """Get display meta for a model showing pinned agents."""
57
+ try:
58
+ from code_puppy.config import get_agents_pinned_to_model
59
+
60
+ pinned_agents = get_agents_pinned_to_model(model_name)
61
+ pinned_agents.extend(_get_json_agents_for_model(model_name))
62
+ pinned_agents = list(set(pinned_agents)) # Deduplicate
63
+
64
+ if pinned_agents:
65
+ agents_str = ", ".join(pinned_agents[:2])
66
+ if len(pinned_agents) > 2:
67
+ agents_str += "..."
68
+ return f"Pinned: [{agents_str}]"
69
+ except Exception:
70
+ pass
71
+ return "Model"
72
+
73
+
74
+ def _get_agent_display_meta(agent_name: str) -> str:
75
+ """Get display meta for an agent showing pinned model."""
76
+ pinned_model = _get_pinned_model_for_agent(agent_name)
77
+ if pinned_model:
78
+ return f"→ {pinned_model}"
79
+ return "default"
80
+
81
+
7
82
  def load_agent_names():
8
83
  """Load all available agent names (both built-in and JSON agents)."""
9
84
  agents = set()
@@ -86,7 +161,7 @@ class PinCompleter(Completer):
86
161
  agent_name,
87
162
  start_position=-len(command_part),
88
163
  display=agent_name,
89
- display_meta="Agent",
164
+ display_meta=_get_agent_display_meta(agent_name),
90
165
  )
91
166
 
92
167
  # Case 2: Completing first argument (agent name)
@@ -115,7 +190,7 @@ class PinCompleter(Completer):
115
190
  model_name,
116
191
  start_position=0, # Insert at cursor position
117
192
  display=model_name,
118
- display_meta="Model",
193
+ display_meta=_get_model_display_meta(model_name),
119
194
  )
120
195
  else:
121
196
  # Still typing agent name, show agent completions
@@ -128,7 +203,7 @@ class PinCompleter(Completer):
128
203
  agent_name,
129
204
  start_position=start_pos,
130
205
  display=agent_name,
131
- display_meta="Agent",
206
+ display_meta=_get_agent_display_meta(agent_name),
132
207
  )
133
208
 
134
209
  # Case 3: Completing second argument (model name)
@@ -152,7 +227,7 @@ class PinCompleter(Completer):
152
227
  model_name,
153
228
  start_position=0,
154
229
  display=model_name,
155
- display_meta="Model",
230
+ display_meta=_get_model_display_meta(model_name),
156
231
  )
157
232
  else:
158
233
  # Filter based on what the user has typed
@@ -174,7 +249,7 @@ class PinCompleter(Completer):
174
249
  model_name,
175
250
  start_position=start_pos,
176
251
  display=model_name,
177
- display_meta="Model",
252
+ display_meta=_get_model_display_meta(model_name),
178
253
  )
179
254
 
180
255
  # Case 4: Handle special case when user selected (unpin)
@@ -233,7 +308,7 @@ class UnpinCompleter(Completer):
233
308
  agent_name,
234
309
  start_position=-len(command_part),
235
310
  display=agent_name,
236
- display_meta="Agent",
311
+ display_meta=_get_agent_display_meta(agent_name),
237
312
  )
238
313
  elif len(tokens) == 1:
239
314
  # Filter agent names based on partial input
@@ -247,7 +322,7 @@ class UnpinCompleter(Completer):
247
322
  agent_name,
248
323
  start_position=start_pos,
249
324
  display=agent_name,
250
- display_meta="Agent",
325
+ display_meta=_get_agent_display_meta(agent_name),
251
326
  )
252
327
  else:
253
328
  # No completion for additional arguments
@@ -98,10 +98,9 @@ class SafeFileHistory(FileHistory):
98
98
  except (UnicodeEncodeError, UnicodeDecodeError, OSError) as e:
99
99
  # If we still can't write, log the error but don't crash
100
100
  # This can happen with particularly malformed input
101
- print(
102
- f"Warning: Could not save to command history: {e}",
103
- file=sys.stderr,
104
- )
101
+ # Note: Using sys.stderr here intentionally - this is a low-level
102
+ # warning that shouldn't use the messaging system
103
+ sys.stderr.write(f"Warning: Could not save to command history: {e}\n")
105
104
 
106
105
 
107
106
  class SetCompleter(Completer):
@@ -375,13 +374,20 @@ class AgentCompleter(Completer):
375
374
  return
376
375
 
377
376
  # Filter and yield agent completions
377
+ try:
378
+ from code_puppy.command_line.pin_command_completion import (
379
+ _get_agent_display_meta,
380
+ )
381
+ except ImportError:
382
+ _get_agent_display_meta = lambda x: "default" # noqa: E731
383
+
378
384
  for agent_name in agent_names:
379
385
  if agent_name.lower().startswith(text_after_trigger.lower()):
380
386
  yield Completion(
381
387
  agent_name,
382
388
  start_position=start_position,
383
389
  display=agent_name,
384
- display_meta="Agent",
390
+ display_meta=_get_agent_display_meta(agent_name),
385
391
  )
386
392
 
387
393
 
@@ -576,12 +582,26 @@ async def get_input_with_combined_completion(
576
582
  # Ctrl+X keybinding - exit with KeyboardInterrupt for shell command cancellation
577
583
  @bindings.add(Keys.ControlX)
578
584
  def _(event):
579
- event.app.exit(exception=KeyboardInterrupt)
585
+ try:
586
+ event.app.exit(exception=KeyboardInterrupt)
587
+ except Exception:
588
+ # Ignore "Return value already set" errors when exit was already called
589
+ # This happens when user presses multiple exit keys in quick succession
590
+ pass
580
591
 
581
592
  # Escape keybinding - exit with KeyboardInterrupt
582
593
  @bindings.add(Keys.Escape)
583
594
  def _(event):
584
- event.app.exit(exception=KeyboardInterrupt)
595
+ try:
596
+ event.app.exit(exception=KeyboardInterrupt)
597
+ except Exception:
598
+ # Ignore "Return value already set" errors when exit was already called
599
+ pass
600
+
601
+ # NOTE: We intentionally do NOT override Ctrl+C here.
602
+ # prompt_toolkit's default Ctrl+C handler properly resets the terminal state on Windows.
603
+ # Overriding it with event.app.exit(exception=KeyboardInterrupt) can leave the terminal
604
+ # in a bad state where characters cannot be typed. Let prompt_toolkit handle Ctrl+C natively.
585
605
 
586
606
  # Toggle multiline with Alt+M
587
607
  @bindings.add(Keys.Escape, "m")
@@ -589,14 +609,17 @@ async def get_input_with_combined_completion(
589
609
  multiline["enabled"] = not multiline["enabled"]
590
610
  status = "ON" if multiline["enabled"] else "OFF"
591
611
  # Print status for user feedback (version-agnostic)
592
- print(f"[multiline] {status}", flush=True)
612
+ # Note: Using sys.stdout here for immediate feedback during input
613
+ sys.stdout.write(f"[multiline] {status}\n")
614
+ sys.stdout.flush()
593
615
 
594
616
  # Also toggle multiline with F2 (more reliable across platforms)
595
617
  @bindings.add("f2")
596
618
  def _(event):
597
619
  multiline["enabled"] = not multiline["enabled"]
598
620
  status = "ON" if multiline["enabled"] else "OFF"
599
- print(f"[multiline] {status}", flush=True)
621
+ sys.stdout.write(f"[multiline] {status}\n")
622
+ sys.stdout.flush()
600
623
 
601
624
  # Newline insert bindings — robust and explicit
602
625
  # Ctrl+J (line feed) works in virtually all terminals; mark eager so it wins
@@ -246,6 +246,8 @@ def handle_dump_context_command(command: str) -> bool:
246
246
  )
247
247
  def handle_load_context_command(command: str) -> bool:
248
248
  """Load message history from a file."""
249
+ from rich.text import Text
250
+
249
251
  from code_puppy.agents.agent_manager import get_current_agent
250
252
  from code_puppy.config import rotate_autosave_id
251
253
  from code_puppy.messaging import emit_error, emit_info, emit_success, emit_warning
@@ -278,12 +280,17 @@ def handle_load_context_command(command: str) -> bool:
278
280
  # Rotate autosave id to avoid overwriting any existing autosave
279
281
  try:
280
282
  new_id = rotate_autosave_id()
281
- autosave_info = f"\n[dim]Autosave session rotated to: {new_id}[/dim]"
283
+ autosave_info = Text.from_markup(
284
+ f"\n[dim]Autosave session rotated to: {new_id}[/dim]"
285
+ )
282
286
  except Exception:
283
- autosave_info = ""
287
+ autosave_info = Text("")
284
288
 
285
- emit_success(
289
+ # Build the success message with proper Text concatenation
290
+ success_msg = Text(
286
291
  f"✅ Context loaded: {len(history)} messages ({total_tokens} tokens)\n"
287
- f"📁 From: {session_path}{autosave_info}"
292
+ f"📁 From: {session_path}"
288
293
  )
294
+ success_msg.append_text(autosave_info)
295
+ emit_success(success_msg)
289
296
  return True