code-puppy 0.0.302__py3-none-any.whl → 0.0.335__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 (87) hide show
  1. code_puppy/agents/base_agent.py +343 -35
  2. code_puppy/chatgpt_codex_client.py +283 -0
  3. code_puppy/cli_runner.py +898 -0
  4. code_puppy/command_line/add_model_menu.py +23 -1
  5. code_puppy/command_line/autosave_menu.py +271 -35
  6. code_puppy/command_line/colors_menu.py +520 -0
  7. code_puppy/command_line/command_handler.py +8 -2
  8. code_puppy/command_line/config_commands.py +82 -10
  9. code_puppy/command_line/core_commands.py +70 -7
  10. code_puppy/command_line/diff_menu.py +5 -0
  11. code_puppy/command_line/mcp/custom_server_form.py +4 -0
  12. code_puppy/command_line/mcp/edit_command.py +3 -1
  13. code_puppy/command_line/mcp/handler.py +7 -2
  14. code_puppy/command_line/mcp/install_command.py +8 -3
  15. code_puppy/command_line/mcp/install_menu.py +5 -1
  16. code_puppy/command_line/mcp/logs_command.py +173 -64
  17. code_puppy/command_line/mcp/restart_command.py +7 -2
  18. code_puppy/command_line/mcp/search_command.py +10 -4
  19. code_puppy/command_line/mcp/start_all_command.py +16 -6
  20. code_puppy/command_line/mcp/start_command.py +3 -1
  21. code_puppy/command_line/mcp/status_command.py +2 -1
  22. code_puppy/command_line/mcp/stop_all_command.py +5 -1
  23. code_puppy/command_line/mcp/stop_command.py +3 -1
  24. code_puppy/command_line/mcp/wizard_utils.py +10 -4
  25. code_puppy/command_line/model_settings_menu.py +58 -7
  26. code_puppy/command_line/motd.py +13 -7
  27. code_puppy/command_line/onboarding_slides.py +180 -0
  28. code_puppy/command_line/onboarding_wizard.py +340 -0
  29. code_puppy/command_line/prompt_toolkit_completion.py +16 -2
  30. code_puppy/command_line/session_commands.py +11 -4
  31. code_puppy/config.py +106 -17
  32. code_puppy/http_utils.py +155 -196
  33. code_puppy/keymap.py +8 -0
  34. code_puppy/main.py +5 -828
  35. code_puppy/mcp_/__init__.py +17 -0
  36. code_puppy/mcp_/blocking_startup.py +61 -32
  37. code_puppy/mcp_/config_wizard.py +5 -1
  38. code_puppy/mcp_/managed_server.py +23 -3
  39. code_puppy/mcp_/manager.py +65 -0
  40. code_puppy/mcp_/mcp_logs.py +224 -0
  41. code_puppy/messaging/__init__.py +20 -4
  42. code_puppy/messaging/bus.py +64 -0
  43. code_puppy/messaging/markdown_patches.py +57 -0
  44. code_puppy/messaging/messages.py +16 -0
  45. code_puppy/messaging/renderers.py +21 -9
  46. code_puppy/messaging/rich_renderer.py +113 -67
  47. code_puppy/messaging/spinner/console_spinner.py +34 -0
  48. code_puppy/model_factory.py +271 -45
  49. code_puppy/model_utils.py +57 -48
  50. code_puppy/models.json +21 -7
  51. code_puppy/plugins/__init__.py +12 -0
  52. code_puppy/plugins/antigravity_oauth/__init__.py +10 -0
  53. code_puppy/plugins/antigravity_oauth/accounts.py +406 -0
  54. code_puppy/plugins/antigravity_oauth/antigravity_model.py +612 -0
  55. code_puppy/plugins/antigravity_oauth/config.py +42 -0
  56. code_puppy/plugins/antigravity_oauth/constants.py +136 -0
  57. code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
  58. code_puppy/plugins/antigravity_oauth/register_callbacks.py +406 -0
  59. code_puppy/plugins/antigravity_oauth/storage.py +271 -0
  60. code_puppy/plugins/antigravity_oauth/test_plugin.py +319 -0
  61. code_puppy/plugins/antigravity_oauth/token.py +167 -0
  62. code_puppy/plugins/antigravity_oauth/transport.py +595 -0
  63. code_puppy/plugins/antigravity_oauth/utils.py +169 -0
  64. code_puppy/plugins/chatgpt_oauth/config.py +5 -1
  65. code_puppy/plugins/chatgpt_oauth/oauth_flow.py +5 -6
  66. code_puppy/plugins/chatgpt_oauth/register_callbacks.py +5 -3
  67. code_puppy/plugins/chatgpt_oauth/test_plugin.py +26 -11
  68. code_puppy/plugins/chatgpt_oauth/utils.py +180 -65
  69. code_puppy/plugins/claude_code_oauth/register_callbacks.py +30 -0
  70. code_puppy/plugins/claude_code_oauth/utils.py +1 -0
  71. code_puppy/plugins/shell_safety/agent_shell_safety.py +1 -118
  72. code_puppy/plugins/shell_safety/register_callbacks.py +44 -3
  73. code_puppy/prompts/codex_system_prompt.md +310 -0
  74. code_puppy/pydantic_patches.py +131 -0
  75. code_puppy/reopenable_async_client.py +8 -8
  76. code_puppy/terminal_utils.py +291 -0
  77. code_puppy/tools/agent_tools.py +34 -9
  78. code_puppy/tools/command_runner.py +344 -27
  79. code_puppy/tools/file_operations.py +33 -45
  80. code_puppy/uvx_detection.py +242 -0
  81. {code_puppy-0.0.302.data → code_puppy-0.0.335.data}/data/code_puppy/models.json +21 -7
  82. {code_puppy-0.0.302.dist-info → code_puppy-0.0.335.dist-info}/METADATA +30 -1
  83. {code_puppy-0.0.302.dist-info → code_puppy-0.0.335.dist-info}/RECORD +87 -64
  84. {code_puppy-0.0.302.data → code_puppy-0.0.335.data}/data/code_puppy/models_dev_api.json +0 -0
  85. {code_puppy-0.0.302.dist-info → code_puppy-0.0.335.dist-info}/WHEEL +0 -0
  86. {code_puppy-0.0.302.dist-info → code_puppy-0.0.335.dist-info}/entry_points.txt +0 -0
  87. {code_puppy-0.0.302.dist-info → code_puppy-0.0.335.dist-info}/licenses/LICENSE +0 -0
@@ -27,6 +27,8 @@ def get_commands_help():
27
27
  )
28
28
  def handle_show_command(command: str) -> bool:
29
29
  """Show current puppy configuration."""
30
+ from rich.text import Text
31
+
30
32
  from code_puppy.agents import get_current_agent
31
33
  from code_puppy.command_line.model_picker_completion import get_active_model
32
34
  from code_puppy.config import (
@@ -44,6 +46,7 @@ def handle_show_command(command: str) -> bool:
44
46
  get_use_dbos,
45
47
  get_yolo_mode,
46
48
  )
49
+ from code_puppy.keymap import get_cancel_agent_display_name
47
50
  from code_puppy.messaging import emit_info
48
51
 
49
52
  puppy_name = get_puppy_name()
@@ -77,16 +80,17 @@ def handle_show_command(command: str) -> bool:
77
80
  [bold]reasoning_effort:[/bold] [cyan]{get_openai_reasoning_effort()}[/cyan]
78
81
  [bold]verbosity:[/bold] [cyan]{get_openai_verbosity()}[/cyan]
79
82
  [bold]temperature:[/bold] [cyan]{effective_temperature if effective_temperature is not None else "(model default)"}[/cyan]{" (per-model)" if effective_temperature != global_temperature and effective_temperature is not None else ""}
83
+ [bold]cancel_agent_key:[/bold] [cyan]{get_cancel_agent_display_name()}[/cyan] (options: ctrl+c, ctrl+k, ctrl+q)
80
84
 
81
85
  """
82
- emit_info(status_msg)
86
+ emit_info(Text.from_markup(status_msg))
83
87
  return True
84
88
 
85
89
 
86
90
  @register_command(
87
91
  name="reasoning",
88
92
  description="Set OpenAI reasoning effort for GPT-5 models (e.g., /reasoning high)",
89
- usage="/reasoning <low|medium|high>",
93
+ usage="/reasoning <minimal|low|medium|high|xhigh>",
90
94
  category="config",
91
95
  )
92
96
  def handle_reasoning_command(command: str) -> bool:
@@ -95,7 +99,7 @@ def handle_reasoning_command(command: str) -> bool:
95
99
 
96
100
  tokens = command.split()
97
101
  if len(tokens) != 2:
98
- emit_warning("Usage: /reasoning <low|medium|high>")
102
+ emit_warning("Usage: /reasoning <minimal|low|medium|high|xhigh>")
99
103
  return True
100
104
 
101
105
  effort = tokens[1]
@@ -171,6 +175,8 @@ def handle_verbosity_command(command: str) -> bool:
171
175
  )
172
176
  def handle_set_command(command: str) -> bool:
173
177
  """Set configuration values."""
178
+ from rich.text import Text
179
+
174
180
  from code_puppy.config import set_config_value
175
181
  from code_puppy.messaging import emit_error, emit_info, emit_success, emit_warning
176
182
 
@@ -196,15 +202,40 @@ def handle_set_command(command: str) -> bool:
196
202
  "\n[yellow]Session Management[/yellow]"
197
203
  "\n [cyan]auto_save_session[/cyan] Auto-save chat after every response (true/false)"
198
204
  )
205
+ keymap_help = (
206
+ "\n[yellow]Keyboard Shortcuts[/yellow]"
207
+ "\n [cyan]cancel_agent_key[/cyan] Key to cancel agent tasks (ctrl+c, ctrl+k, or ctrl+q)"
208
+ )
199
209
  emit_warning(
200
- f"Usage: /set KEY=VALUE or /set KEY VALUE\nConfig keys: {', '.join(config_keys)}\n[dim]Note: compaction_strategy can be 'summarization' or 'truncation'[/dim]{session_help}"
210
+ Text.from_markup(
211
+ f"Usage: /set KEY=VALUE or /set KEY VALUE\nConfig keys: {', '.join(config_keys)}\n[dim]Note: compaction_strategy can be 'summarization' or 'truncation'[/dim]{session_help}{keymap_help}"
212
+ )
201
213
  )
202
214
  return True
203
215
  if key:
204
216
  # Check if we're toggling DBOS enablement
205
217
  if key == "enable_dbos":
206
218
  emit_info(
207
- "[yellow]⚠️ DBOS configuration changed. Please restart Code Puppy for this change to take effect.[/yellow]"
219
+ Text.from_markup(
220
+ "[yellow]⚠️ DBOS configuration changed. Please restart Code Puppy for this change to take effect.[/yellow]"
221
+ )
222
+ )
223
+
224
+ # Validate cancel_agent_key before setting
225
+ if key == "cancel_agent_key":
226
+ from code_puppy.keymap import VALID_CANCEL_KEYS
227
+
228
+ normalized_value = value.strip().lower()
229
+ if normalized_value not in VALID_CANCEL_KEYS:
230
+ emit_error(
231
+ f"Invalid cancel_agent_key '{value}'. Valid options: {', '.join(sorted(VALID_CANCEL_KEYS))}"
232
+ )
233
+ return True
234
+ value = normalized_value # Use normalized value
235
+ emit_info(
236
+ Text.from_markup(
237
+ "[yellow]⚠️ cancel_agent_key changed. Please restart Code Puppy for this change to take effect.[/yellow]"
238
+ )
208
239
  )
209
240
 
210
241
  set_config_value(key, value)
@@ -539,6 +570,37 @@ def handle_diff_command(command: str) -> bool:
539
570
  return True
540
571
 
541
572
 
573
+ @register_command(
574
+ name="colors",
575
+ description="Configure banner colors for tool outputs (THINKING, SHELL COMMAND, etc.)",
576
+ usage="/colors",
577
+ category="config",
578
+ )
579
+ def handle_colors_command(command: str) -> bool:
580
+ """Configure banner colors via interactive TUI."""
581
+ import asyncio
582
+ import concurrent.futures
583
+
584
+ from code_puppy.command_line.colors_menu import interactive_colors_picker
585
+ from code_puppy.config import set_banner_color
586
+ from code_puppy.messaging import emit_error, emit_success
587
+
588
+ # Show interactive picker for banner color configuration
589
+ with concurrent.futures.ThreadPoolExecutor() as executor:
590
+ future = executor.submit(lambda: asyncio.run(interactive_colors_picker()))
591
+ result = future.result(timeout=300) # 5 min timeout
592
+
593
+ if result:
594
+ # Apply the changes
595
+ try:
596
+ for banner_name, color in result.items():
597
+ set_banner_color(banner_name, color)
598
+ emit_success("Banner colors saved! 🎨")
599
+ except Exception as e:
600
+ emit_error(f"Failed to apply banner color settings: {e}")
601
+ return True
602
+
603
+
542
604
  # ============================================================================
543
605
  # UTILITY FUNCTIONS
544
606
  # ============================================================================
@@ -550,6 +612,8 @@ def _show_color_options(color_type: str):
550
612
  # ============================================================================
551
613
 
552
614
  """Show available Rich color options organized by category."""
615
+ from rich.text import Text
616
+
553
617
  from code_puppy.messaging import emit_info
554
618
 
555
619
  # Standard Rich colors organized by category
@@ -607,11 +671,15 @@ def _show_color_options(color_type: str):
607
671
  ("sea_green1", "🟢"),
608
672
  ]
609
673
  emit_info(
610
- "[bold white on green]🎨 Recommended Colors for Additions:[/bold white on green]"
674
+ Text.from_markup(
675
+ "[bold white on green]🎨 Recommended Colors for Additions:[/bold white on green]"
676
+ )
611
677
  )
612
678
  for color, emoji in suggestions:
613
679
  emit_info(
614
- f" [cyan]{color:<16}[/cyan] [white on {color}]■■■■■■■■■■[/white on {color}] {emoji}"
680
+ Text.from_markup(
681
+ f" [cyan]{color:<16}[/cyan] [white on {color}]■■■■■■■■■■[/white on {color}] {emoji}"
682
+ )
615
683
  )
616
684
  elif color_type == "deletions":
617
685
  suggestions = [
@@ -622,11 +690,15 @@ def _show_color_options(color_type: str):
622
690
  ("dark_red", "🔴"),
623
691
  ]
624
692
  emit_info(
625
- "[bold white on orange1]🎨 Recommended Colors for Deletions:[/bold white on orange1]"
693
+ Text.from_markup(
694
+ "[bold white on orange1]🎨 Recommended Colors for Deletions:[/bold white on orange1]"
695
+ )
626
696
  )
627
697
  for color, emoji in suggestions:
628
698
  emit_info(
629
- f" [cyan]{color:<16}[/cyan] [white on {color}]■■■■■■■■■■[/white on {color}] {emoji}"
699
+ Text.from_markup(
700
+ f" [cyan]{color:<16}[/cyan] [white on {color}]■■■■■■■■■■[/white on {color}] {emoji}"
701
+ )
630
702
  )
631
703
 
632
704
  emit_info("\n🎨 All Available Rich Colors:")
@@ -636,7 +708,7 @@ def _show_color_options(color_type: str):
636
708
  for i in range(0, len(colors), 4):
637
709
  row = colors[i : i + 4]
638
710
  row_text = " ".join([f"[{color}]■[/{color}] {color}" for color, _ in row])
639
- emit_info(f" {row_text}")
711
+ emit_info(Text.from_markup(f" {row_text}"))
640
712
 
641
713
  emit_info("\nUsage: /diff {color_type} <color_name>")
642
714
  emit_info("All diffs use white text on your chosen background colors")
@@ -115,6 +115,57 @@ def handle_motd_command(command: str) -> bool:
115
115
  return True
116
116
 
117
117
 
118
+ @register_command(
119
+ name="tutorial",
120
+ description="Run the interactive tutorial wizard",
121
+ usage="/tutorial",
122
+ category="core",
123
+ )
124
+ def handle_tutorial_command(command: str) -> bool:
125
+ """Run the interactive tutorial wizard.
126
+
127
+ Usage:
128
+ /tutorial - Run the tutorial (can be run anytime)
129
+ """
130
+ import asyncio
131
+ import concurrent.futures
132
+
133
+ from code_puppy.command_line.onboarding_wizard import (
134
+ reset_onboarding,
135
+ run_onboarding_wizard,
136
+ )
137
+ from code_puppy.config import set_model_name
138
+
139
+ # Always reset so user can re-run the tutorial anytime
140
+ reset_onboarding()
141
+
142
+ # Run the async wizard in a thread pool (same pattern as agent picker)
143
+ with concurrent.futures.ThreadPoolExecutor() as executor:
144
+ future = executor.submit(lambda: asyncio.run(run_onboarding_wizard()))
145
+ result = future.result(timeout=300) # 5 min timeout
146
+
147
+ if result == "chatgpt":
148
+ emit_info("🔐 Starting ChatGPT OAuth flow...")
149
+ from code_puppy.plugins.chatgpt_oauth.oauth_flow import run_oauth_flow
150
+
151
+ run_oauth_flow()
152
+ set_model_name("chatgpt-gpt-5.2-codex")
153
+ elif result == "claude":
154
+ emit_info("🔐 Starting Claude Code OAuth flow...")
155
+ from code_puppy.plugins.claude_code_oauth.register_callbacks import (
156
+ _perform_authentication,
157
+ )
158
+
159
+ _perform_authentication()
160
+ set_model_name("claude-code-claude-opus-4-5-20251101")
161
+ elif result == "completed":
162
+ emit_info("🎉 Tutorial complete! Happy coding!")
163
+ elif result == "skipped":
164
+ emit_info("⏭️ Tutorial skipped. Run /tutorial anytime!")
165
+
166
+ return True
167
+
168
+
118
169
  @register_command(
119
170
  name="exit",
120
171
  description="Exit interactive mode",
@@ -145,6 +196,8 @@ def handle_exit_command(command: str) -> bool:
145
196
  )
146
197
  def handle_agent_command(command: str) -> bool:
147
198
  """Handle agent switching."""
199
+ from rich.text import Text
200
+
148
201
  from code_puppy.agents import (
149
202
  get_agent_descriptions,
150
203
  get_available_agents,
@@ -201,7 +254,9 @@ def handle_agent_command(command: str) -> bool:
201
254
  )
202
255
  emit_info(f"{new_agent.description}", message_group=group_id)
203
256
  emit_info(
204
- f"[dim]Auto-save session rotated to: {new_session_id}[/dim]",
257
+ Text.from_markup(
258
+ f"[dim]Auto-save session rotated to: {new_session_id}[/dim]"
259
+ ),
205
260
  message_group=group_id,
206
261
  )
207
262
  else:
@@ -224,15 +279,19 @@ def handle_agent_command(command: str) -> bool:
224
279
  group_id = str(uuid.uuid4())
225
280
 
226
281
  emit_info(
227
- f"[bold green]Current Agent:[/bold green] {current_agent.display_name}",
282
+ Text.from_markup(
283
+ f"[bold green]Current Agent:[/bold green] {current_agent.display_name}"
284
+ ),
228
285
  message_group=group_id,
229
286
  )
230
287
  emit_info(
231
- f"[dim]{current_agent.description}[/dim]\n", message_group=group_id
288
+ Text.from_markup(f"[dim]{current_agent.description}[/dim]\n"),
289
+ message_group=group_id,
232
290
  )
233
291
 
234
292
  emit_info(
235
- "[bold magenta]Available Agents:[/bold magenta]", message_group=group_id
293
+ Text.from_markup("[bold magenta]Available Agents:[/bold magenta]"),
294
+ message_group=group_id,
236
295
  )
237
296
  for name, display_name in available_agents.items():
238
297
  description = descriptions.get(name, "No description")
@@ -240,13 +299,15 @@ def handle_agent_command(command: str) -> bool:
240
299
  " [green]← current[/green]" if name == current_agent.name else ""
241
300
  )
242
301
  emit_info(
243
- f" [cyan]{name:<12}[/cyan] {display_name}{current_marker}",
302
+ Text.from_markup(
303
+ f" [cyan]{name:<12}[/cyan] {display_name}{current_marker}"
304
+ ),
244
305
  message_group=group_id,
245
306
  )
246
307
  emit_info(f" {description}", message_group=group_id)
247
308
 
248
309
  emit_info(
249
- "\n[yellow]Usage:[/yellow] /agent <agent-name>",
310
+ Text.from_markup("\n[yellow]Usage:[/yellow] /agent <agent-name>"),
250
311
  message_group=group_id,
251
312
  )
252
313
  return True
@@ -292,7 +353,9 @@ def handle_agent_command(command: str) -> bool:
292
353
  )
293
354
  emit_info(f"{new_agent.description}", message_group=group_id)
294
355
  emit_info(
295
- f"[dim]Auto-save session rotated to: {new_session_id}[/dim]",
356
+ Text.from_markup(
357
+ f"[dim]Auto-save session rotated to: {new_session_id}[/dim]"
358
+ ),
296
359
  message_group=group_id,
297
360
  )
298
361
  return True
@@ -456,6 +456,11 @@ async def interactive_diff_picker() -> Optional[dict]:
456
456
  sys.stdout.write("\033[?1049l") # Exit alternate buffer
457
457
  sys.stdout.flush()
458
458
 
459
+ # Clear exit message
460
+ from code_puppy.messaging import emit_info
461
+
462
+ emit_info("✓ Exited diff color configuration")
463
+
459
464
  # Return changes if any
460
465
  if config.has_changes():
461
466
  return {
@@ -604,6 +604,10 @@ class CustomServerForm:
604
604
  sys.stdout.flush()
605
605
  set_awaiting_user_input(False)
606
606
 
607
+ # Clear exit message if not installing
608
+ if self.result != "installed":
609
+ emit_info("✓ Exited custom server form")
610
+
607
611
  # Handle result
608
612
  if self.result == "installed":
609
613
  if self.edit_mode:
@@ -8,6 +8,8 @@ import logging
8
8
  import os
9
9
  from typing import List, Optional
10
10
 
11
+ from rich.text import Text
12
+
11
13
  from code_puppy.config import MCP_SERVERS_FILE
12
14
  from code_puppy.messaging import emit_error, emit_info, emit_warning
13
15
 
@@ -39,7 +41,7 @@ class EditCommand(MCPCommandBase):
39
41
  # Need a server name
40
42
  if not args:
41
43
  emit_info(
42
- "[yellow]Usage: /mcp edit <server_name>[/yellow]",
44
+ Text.from_markup("[yellow]Usage: /mcp edit <server_name>[/yellow]"),
43
45
  message_group=group_id,
44
46
  )
45
47
  emit_info(
@@ -8,6 +8,8 @@ to their respective command modules.
8
8
  import logging
9
9
  import shlex
10
10
 
11
+ from rich.text import Text
12
+
11
13
  from code_puppy.messaging import emit_info
12
14
 
13
15
  from .add_command import AddCommand
@@ -103,7 +105,8 @@ class MCPCommandHandler(MCPCommandBase):
103
105
  args = shlex.split(args_str)
104
106
  except ValueError as e:
105
107
  emit_info(
106
- f"[red]Invalid command syntax: {e}[/red]", message_group=group_id
108
+ Text.from_markup(f"[red]Invalid command syntax: {e}[/red]"),
109
+ message_group=group_id,
107
110
  )
108
111
  return True
109
112
 
@@ -121,7 +124,9 @@ class MCPCommandHandler(MCPCommandBase):
121
124
  return True
122
125
  else:
123
126
  emit_info(
124
- f"[yellow]Unknown MCP subcommand: {subcommand}[/yellow]",
127
+ Text.from_markup(
128
+ f"[yellow]Unknown MCP subcommand: {subcommand}[/yellow]"
129
+ ),
125
130
  message_group=group_id,
126
131
  )
127
132
  emit_info(
@@ -5,6 +5,8 @@ MCP Install Command - Installs pre-configured MCP servers from the registry.
5
5
  import logging
6
6
  from typing import List, Optional
7
7
 
8
+ from rich.text import Text
9
+
8
10
  from code_puppy.messaging import emit_error, emit_info
9
11
 
10
12
  from .base import MCPCommandBase
@@ -150,7 +152,9 @@ class InstallCommand(MCPCommandBase):
150
152
  required_env_vars = selected_server.get_environment_vars()
151
153
  if required_env_vars:
152
154
  emit_info(
153
- "\n[yellow]Required Environment Variables:[/yellow]",
155
+ Text.from_markup(
156
+ "\n[yellow]Required Environment Variables:[/yellow]"
157
+ ),
154
158
  message_group=group_id,
155
159
  )
156
160
  for var in required_env_vars:
@@ -160,7 +164,7 @@ class InstallCommand(MCPCommandBase):
160
164
  current_value = os.environ.get(var, "")
161
165
  if current_value:
162
166
  emit_info(
163
- f" {var}: [green]Already set[/green]",
167
+ Text.from_markup(f" {var}: [green]Already set[/green]"),
164
168
  message_group=group_id,
165
169
  )
166
170
  env_vars[var] = current_value
@@ -173,7 +177,8 @@ class InstallCommand(MCPCommandBase):
173
177
  required_cmd_args = selected_server.get_command_line_args()
174
178
  if required_cmd_args:
175
179
  emit_info(
176
- "\n[yellow]Command Line Arguments:[/yellow]", message_group=group_id
180
+ Text.from_markup("\n[yellow]Command Line Arguments:[/yellow]"),
181
+ message_group=group_id,
177
182
  )
178
183
  for arg_config in required_cmd_args:
179
184
  name = arg_config.get("name", "")
@@ -16,7 +16,7 @@ from prompt_toolkit.layout import Dimension, Layout, VSplit, Window
16
16
  from prompt_toolkit.layout.controls import FormattedTextControl
17
17
  from prompt_toolkit.widgets import Frame
18
18
 
19
- from code_puppy.messaging import emit_error, emit_warning
19
+ from code_puppy.messaging import emit_error, emit_info, emit_warning
20
20
  from code_puppy.tools.command_runner import set_awaiting_user_input
21
21
 
22
22
  from .catalog_server_installer import (
@@ -635,6 +635,10 @@ class MCPInstallMenu:
635
635
  sys.stdout.flush()
636
636
  set_awaiting_user_input(False)
637
637
 
638
+ # Clear exit message (unless we're about to prompt for more input)
639
+ if self.result not in ("pending_custom", "pending_install"):
640
+ emit_info("✓ Exited MCP server browser")
641
+
638
642
  # Handle custom server after TUI exits
639
643
  if self.result == "pending_custom":
640
644
  success = run_custom_server_form(self.manager)