codepp 0.0.437__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 (288) hide show
  1. code_puppy/__init__.py +10 -0
  2. code_puppy/__main__.py +10 -0
  3. code_puppy/agents/__init__.py +31 -0
  4. code_puppy/agents/agent_c_reviewer.py +155 -0
  5. code_puppy/agents/agent_code_puppy.py +117 -0
  6. code_puppy/agents/agent_code_reviewer.py +90 -0
  7. code_puppy/agents/agent_cpp_reviewer.py +132 -0
  8. code_puppy/agents/agent_creator_agent.py +638 -0
  9. code_puppy/agents/agent_golang_reviewer.py +151 -0
  10. code_puppy/agents/agent_helios.py +124 -0
  11. code_puppy/agents/agent_javascript_reviewer.py +160 -0
  12. code_puppy/agents/agent_manager.py +742 -0
  13. code_puppy/agents/agent_pack_leader.py +385 -0
  14. code_puppy/agents/agent_planning.py +165 -0
  15. code_puppy/agents/agent_python_programmer.py +169 -0
  16. code_puppy/agents/agent_python_reviewer.py +90 -0
  17. code_puppy/agents/agent_qa_expert.py +163 -0
  18. code_puppy/agents/agent_qa_kitten.py +208 -0
  19. code_puppy/agents/agent_scheduler.py +121 -0
  20. code_puppy/agents/agent_security_auditor.py +181 -0
  21. code_puppy/agents/agent_terminal_qa.py +323 -0
  22. code_puppy/agents/agent_typescript_reviewer.py +166 -0
  23. code_puppy/agents/base_agent.py +2156 -0
  24. code_puppy/agents/event_stream_handler.py +348 -0
  25. code_puppy/agents/json_agent.py +202 -0
  26. code_puppy/agents/pack/__init__.py +34 -0
  27. code_puppy/agents/pack/bloodhound.py +304 -0
  28. code_puppy/agents/pack/husky.py +327 -0
  29. code_puppy/agents/pack/retriever.py +393 -0
  30. code_puppy/agents/pack/shepherd.py +348 -0
  31. code_puppy/agents/pack/terrier.py +287 -0
  32. code_puppy/agents/pack/watchdog.py +367 -0
  33. code_puppy/agents/prompt_reviewer.py +145 -0
  34. code_puppy/agents/subagent_stream_handler.py +276 -0
  35. code_puppy/api/__init__.py +13 -0
  36. code_puppy/api/app.py +169 -0
  37. code_puppy/api/main.py +21 -0
  38. code_puppy/api/pty_manager.py +453 -0
  39. code_puppy/api/routers/__init__.py +12 -0
  40. code_puppy/api/routers/agents.py +36 -0
  41. code_puppy/api/routers/commands.py +217 -0
  42. code_puppy/api/routers/config.py +75 -0
  43. code_puppy/api/routers/sessions.py +234 -0
  44. code_puppy/api/templates/terminal.html +361 -0
  45. code_puppy/api/websocket.py +154 -0
  46. code_puppy/callbacks.py +692 -0
  47. code_puppy/chatgpt_codex_client.py +338 -0
  48. code_puppy/claude_cache_client.py +672 -0
  49. code_puppy/cli_runner.py +1073 -0
  50. code_puppy/command_line/__init__.py +1 -0
  51. code_puppy/command_line/add_model_menu.py +1092 -0
  52. code_puppy/command_line/agent_menu.py +662 -0
  53. code_puppy/command_line/attachments.py +395 -0
  54. code_puppy/command_line/autosave_menu.py +704 -0
  55. code_puppy/command_line/clipboard.py +527 -0
  56. code_puppy/command_line/colors_menu.py +532 -0
  57. code_puppy/command_line/command_handler.py +293 -0
  58. code_puppy/command_line/command_registry.py +150 -0
  59. code_puppy/command_line/config_commands.py +719 -0
  60. code_puppy/command_line/core_commands.py +867 -0
  61. code_puppy/command_line/diff_menu.py +865 -0
  62. code_puppy/command_line/file_path_completion.py +73 -0
  63. code_puppy/command_line/load_context_completion.py +52 -0
  64. code_puppy/command_line/mcp/__init__.py +10 -0
  65. code_puppy/command_line/mcp/base.py +32 -0
  66. code_puppy/command_line/mcp/catalog_server_installer.py +175 -0
  67. code_puppy/command_line/mcp/custom_server_form.py +688 -0
  68. code_puppy/command_line/mcp/custom_server_installer.py +195 -0
  69. code_puppy/command_line/mcp/edit_command.py +148 -0
  70. code_puppy/command_line/mcp/handler.py +138 -0
  71. code_puppy/command_line/mcp/help_command.py +147 -0
  72. code_puppy/command_line/mcp/install_command.py +214 -0
  73. code_puppy/command_line/mcp/install_menu.py +705 -0
  74. code_puppy/command_line/mcp/list_command.py +94 -0
  75. code_puppy/command_line/mcp/logs_command.py +235 -0
  76. code_puppy/command_line/mcp/remove_command.py +82 -0
  77. code_puppy/command_line/mcp/restart_command.py +100 -0
  78. code_puppy/command_line/mcp/search_command.py +123 -0
  79. code_puppy/command_line/mcp/start_all_command.py +135 -0
  80. code_puppy/command_line/mcp/start_command.py +117 -0
  81. code_puppy/command_line/mcp/status_command.py +184 -0
  82. code_puppy/command_line/mcp/stop_all_command.py +112 -0
  83. code_puppy/command_line/mcp/stop_command.py +80 -0
  84. code_puppy/command_line/mcp/test_command.py +107 -0
  85. code_puppy/command_line/mcp/utils.py +129 -0
  86. code_puppy/command_line/mcp/wizard_utils.py +334 -0
  87. code_puppy/command_line/mcp_completion.py +174 -0
  88. code_puppy/command_line/model_picker_completion.py +197 -0
  89. code_puppy/command_line/model_settings_menu.py +932 -0
  90. code_puppy/command_line/motd.py +96 -0
  91. code_puppy/command_line/onboarding_slides.py +179 -0
  92. code_puppy/command_line/onboarding_wizard.py +342 -0
  93. code_puppy/command_line/pin_command_completion.py +329 -0
  94. code_puppy/command_line/prompt_toolkit_completion.py +846 -0
  95. code_puppy/command_line/session_commands.py +302 -0
  96. code_puppy/command_line/shell_passthrough.py +145 -0
  97. code_puppy/command_line/skills_completion.py +160 -0
  98. code_puppy/command_line/uc_menu.py +893 -0
  99. code_puppy/command_line/utils.py +93 -0
  100. code_puppy/command_line/wiggum_state.py +78 -0
  101. code_puppy/config.py +1770 -0
  102. code_puppy/error_logging.py +134 -0
  103. code_puppy/gemini_code_assist.py +385 -0
  104. code_puppy/gemini_model.py +754 -0
  105. code_puppy/hook_engine/README.md +105 -0
  106. code_puppy/hook_engine/__init__.py +21 -0
  107. code_puppy/hook_engine/aliases.py +155 -0
  108. code_puppy/hook_engine/engine.py +221 -0
  109. code_puppy/hook_engine/executor.py +296 -0
  110. code_puppy/hook_engine/matcher.py +156 -0
  111. code_puppy/hook_engine/models.py +240 -0
  112. code_puppy/hook_engine/registry.py +106 -0
  113. code_puppy/hook_engine/validator.py +144 -0
  114. code_puppy/http_utils.py +361 -0
  115. code_puppy/keymap.py +128 -0
  116. code_puppy/main.py +10 -0
  117. code_puppy/mcp_/__init__.py +66 -0
  118. code_puppy/mcp_/async_lifecycle.py +286 -0
  119. code_puppy/mcp_/blocking_startup.py +469 -0
  120. code_puppy/mcp_/captured_stdio_server.py +275 -0
  121. code_puppy/mcp_/circuit_breaker.py +290 -0
  122. code_puppy/mcp_/config_wizard.py +507 -0
  123. code_puppy/mcp_/dashboard.py +308 -0
  124. code_puppy/mcp_/error_isolation.py +407 -0
  125. code_puppy/mcp_/examples/retry_example.py +226 -0
  126. code_puppy/mcp_/health_monitor.py +589 -0
  127. code_puppy/mcp_/managed_server.py +428 -0
  128. code_puppy/mcp_/manager.py +807 -0
  129. code_puppy/mcp_/mcp_logs.py +224 -0
  130. code_puppy/mcp_/registry.py +451 -0
  131. code_puppy/mcp_/retry_manager.py +337 -0
  132. code_puppy/mcp_/server_registry_catalog.py +1126 -0
  133. code_puppy/mcp_/status_tracker.py +355 -0
  134. code_puppy/mcp_/system_tools.py +209 -0
  135. code_puppy/mcp_prompts/__init__.py +1 -0
  136. code_puppy/mcp_prompts/hook_creator.py +103 -0
  137. code_puppy/messaging/__init__.py +255 -0
  138. code_puppy/messaging/bus.py +613 -0
  139. code_puppy/messaging/commands.py +167 -0
  140. code_puppy/messaging/markdown_patches.py +57 -0
  141. code_puppy/messaging/message_queue.py +361 -0
  142. code_puppy/messaging/messages.py +569 -0
  143. code_puppy/messaging/queue_console.py +271 -0
  144. code_puppy/messaging/renderers.py +311 -0
  145. code_puppy/messaging/rich_renderer.py +1158 -0
  146. code_puppy/messaging/spinner/__init__.py +83 -0
  147. code_puppy/messaging/spinner/console_spinner.py +240 -0
  148. code_puppy/messaging/spinner/spinner_base.py +95 -0
  149. code_puppy/messaging/subagent_console.py +460 -0
  150. code_puppy/model_factory.py +848 -0
  151. code_puppy/model_switching.py +63 -0
  152. code_puppy/model_utils.py +168 -0
  153. code_puppy/models.json +174 -0
  154. code_puppy/models_dev_api.json +1 -0
  155. code_puppy/models_dev_parser.py +592 -0
  156. code_puppy/plugins/__init__.py +186 -0
  157. code_puppy/plugins/agent_skills/__init__.py +22 -0
  158. code_puppy/plugins/agent_skills/config.py +175 -0
  159. code_puppy/plugins/agent_skills/discovery.py +136 -0
  160. code_puppy/plugins/agent_skills/downloader.py +392 -0
  161. code_puppy/plugins/agent_skills/installer.py +22 -0
  162. code_puppy/plugins/agent_skills/metadata.py +219 -0
  163. code_puppy/plugins/agent_skills/prompt_builder.py +60 -0
  164. code_puppy/plugins/agent_skills/register_callbacks.py +241 -0
  165. code_puppy/plugins/agent_skills/remote_catalog.py +322 -0
  166. code_puppy/plugins/agent_skills/skill_catalog.py +257 -0
  167. code_puppy/plugins/agent_skills/skills_install_menu.py +664 -0
  168. code_puppy/plugins/agent_skills/skills_menu.py +781 -0
  169. code_puppy/plugins/antigravity_oauth/__init__.py +10 -0
  170. code_puppy/plugins/antigravity_oauth/accounts.py +406 -0
  171. code_puppy/plugins/antigravity_oauth/antigravity_model.py +706 -0
  172. code_puppy/plugins/antigravity_oauth/config.py +42 -0
  173. code_puppy/plugins/antigravity_oauth/constants.py +133 -0
  174. code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
  175. code_puppy/plugins/antigravity_oauth/register_callbacks.py +518 -0
  176. code_puppy/plugins/antigravity_oauth/storage.py +288 -0
  177. code_puppy/plugins/antigravity_oauth/test_plugin.py +319 -0
  178. code_puppy/plugins/antigravity_oauth/token.py +167 -0
  179. code_puppy/plugins/antigravity_oauth/transport.py +863 -0
  180. code_puppy/plugins/antigravity_oauth/utils.py +168 -0
  181. code_puppy/plugins/chatgpt_oauth/__init__.py +8 -0
  182. code_puppy/plugins/chatgpt_oauth/config.py +52 -0
  183. code_puppy/plugins/chatgpt_oauth/oauth_flow.py +329 -0
  184. code_puppy/plugins/chatgpt_oauth/register_callbacks.py +176 -0
  185. code_puppy/plugins/chatgpt_oauth/test_plugin.py +301 -0
  186. code_puppy/plugins/chatgpt_oauth/utils.py +523 -0
  187. code_puppy/plugins/claude_code_hooks/__init__.py +1 -0
  188. code_puppy/plugins/claude_code_hooks/config.py +137 -0
  189. code_puppy/plugins/claude_code_hooks/register_callbacks.py +175 -0
  190. code_puppy/plugins/claude_code_oauth/README.md +167 -0
  191. code_puppy/plugins/claude_code_oauth/SETUP.md +93 -0
  192. code_puppy/plugins/claude_code_oauth/__init__.py +25 -0
  193. code_puppy/plugins/claude_code_oauth/config.py +52 -0
  194. code_puppy/plugins/claude_code_oauth/register_callbacks.py +453 -0
  195. code_puppy/plugins/claude_code_oauth/test_plugin.py +283 -0
  196. code_puppy/plugins/claude_code_oauth/token_refresh_heartbeat.py +241 -0
  197. code_puppy/plugins/claude_code_oauth/utils.py +640 -0
  198. code_puppy/plugins/customizable_commands/__init__.py +0 -0
  199. code_puppy/plugins/customizable_commands/register_callbacks.py +152 -0
  200. code_puppy/plugins/example_custom_command/README.md +280 -0
  201. code_puppy/plugins/example_custom_command/register_callbacks.py +51 -0
  202. code_puppy/plugins/file_permission_handler/__init__.py +4 -0
  203. code_puppy/plugins/file_permission_handler/register_callbacks.py +470 -0
  204. code_puppy/plugins/frontend_emitter/__init__.py +25 -0
  205. code_puppy/plugins/frontend_emitter/emitter.py +121 -0
  206. code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
  207. code_puppy/plugins/hook_creator/__init__.py +1 -0
  208. code_puppy/plugins/hook_creator/register_callbacks.py +33 -0
  209. code_puppy/plugins/hook_manager/__init__.py +1 -0
  210. code_puppy/plugins/hook_manager/config.py +290 -0
  211. code_puppy/plugins/hook_manager/hooks_menu.py +564 -0
  212. code_puppy/plugins/hook_manager/register_callbacks.py +227 -0
  213. code_puppy/plugins/oauth_puppy_html.py +228 -0
  214. code_puppy/plugins/scheduler/__init__.py +1 -0
  215. code_puppy/plugins/scheduler/register_callbacks.py +88 -0
  216. code_puppy/plugins/scheduler/scheduler_menu.py +522 -0
  217. code_puppy/plugins/scheduler/scheduler_wizard.py +341 -0
  218. code_puppy/plugins/shell_safety/__init__.py +6 -0
  219. code_puppy/plugins/shell_safety/agent_shell_safety.py +69 -0
  220. code_puppy/plugins/shell_safety/command_cache.py +156 -0
  221. code_puppy/plugins/shell_safety/register_callbacks.py +202 -0
  222. code_puppy/plugins/synthetic_status/__init__.py +1 -0
  223. code_puppy/plugins/synthetic_status/register_callbacks.py +132 -0
  224. code_puppy/plugins/synthetic_status/status_api.py +147 -0
  225. code_puppy/plugins/universal_constructor/__init__.py +13 -0
  226. code_puppy/plugins/universal_constructor/models.py +138 -0
  227. code_puppy/plugins/universal_constructor/register_callbacks.py +47 -0
  228. code_puppy/plugins/universal_constructor/registry.py +302 -0
  229. code_puppy/plugins/universal_constructor/sandbox.py +584 -0
  230. code_puppy/prompts/antigravity_system_prompt.md +1 -0
  231. code_puppy/pydantic_patches.py +356 -0
  232. code_puppy/reopenable_async_client.py +232 -0
  233. code_puppy/round_robin_model.py +150 -0
  234. code_puppy/scheduler/__init__.py +41 -0
  235. code_puppy/scheduler/__main__.py +9 -0
  236. code_puppy/scheduler/cli.py +118 -0
  237. code_puppy/scheduler/config.py +126 -0
  238. code_puppy/scheduler/daemon.py +280 -0
  239. code_puppy/scheduler/executor.py +155 -0
  240. code_puppy/scheduler/platform.py +19 -0
  241. code_puppy/scheduler/platform_unix.py +22 -0
  242. code_puppy/scheduler/platform_win.py +32 -0
  243. code_puppy/session_storage.py +338 -0
  244. code_puppy/status_display.py +257 -0
  245. code_puppy/summarization_agent.py +176 -0
  246. code_puppy/terminal_utils.py +418 -0
  247. code_puppy/tools/__init__.py +501 -0
  248. code_puppy/tools/agent_tools.py +603 -0
  249. code_puppy/tools/ask_user_question/__init__.py +26 -0
  250. code_puppy/tools/ask_user_question/constants.py +73 -0
  251. code_puppy/tools/ask_user_question/demo_tui.py +55 -0
  252. code_puppy/tools/ask_user_question/handler.py +232 -0
  253. code_puppy/tools/ask_user_question/models.py +304 -0
  254. code_puppy/tools/ask_user_question/registration.py +26 -0
  255. code_puppy/tools/ask_user_question/renderers.py +309 -0
  256. code_puppy/tools/ask_user_question/terminal_ui.py +329 -0
  257. code_puppy/tools/ask_user_question/theme.py +155 -0
  258. code_puppy/tools/ask_user_question/tui_loop.py +423 -0
  259. code_puppy/tools/browser/__init__.py +37 -0
  260. code_puppy/tools/browser/browser_control.py +289 -0
  261. code_puppy/tools/browser/browser_interactions.py +545 -0
  262. code_puppy/tools/browser/browser_locators.py +640 -0
  263. code_puppy/tools/browser/browser_manager.py +378 -0
  264. code_puppy/tools/browser/browser_navigation.py +251 -0
  265. code_puppy/tools/browser/browser_screenshot.py +179 -0
  266. code_puppy/tools/browser/browser_scripts.py +462 -0
  267. code_puppy/tools/browser/browser_workflows.py +221 -0
  268. code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
  269. code_puppy/tools/browser/terminal_command_tools.py +534 -0
  270. code_puppy/tools/browser/terminal_screenshot_tools.py +552 -0
  271. code_puppy/tools/browser/terminal_tools.py +525 -0
  272. code_puppy/tools/command_runner.py +1346 -0
  273. code_puppy/tools/common.py +1409 -0
  274. code_puppy/tools/display.py +84 -0
  275. code_puppy/tools/file_modifications.py +886 -0
  276. code_puppy/tools/file_operations.py +802 -0
  277. code_puppy/tools/scheduler_tools.py +412 -0
  278. code_puppy/tools/skills_tools.py +244 -0
  279. code_puppy/tools/subagent_context.py +158 -0
  280. code_puppy/tools/tools_content.py +51 -0
  281. code_puppy/tools/universal_constructor.py +889 -0
  282. code_puppy/uvx_detection.py +242 -0
  283. code_puppy/version_checker.py +82 -0
  284. codepp-0.0.437.dist-info/METADATA +766 -0
  285. codepp-0.0.437.dist-info/RECORD +288 -0
  286. codepp-0.0.437.dist-info/WHEEL +4 -0
  287. codepp-0.0.437.dist-info/entry_points.txt +3 -0
  288. codepp-0.0.437.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,704 @@
1
+ """Interactive terminal UI for loading autosave sessions.
2
+
3
+ Provides a beautiful split-panel interface for browsing and loading
4
+ autosave sessions with live preview of message content.
5
+ """
6
+
7
+ import asyncio
8
+ import json
9
+ import sys
10
+ from datetime import datetime
11
+ from io import StringIO
12
+ from pathlib import Path
13
+ from typing import List, Optional, Tuple
14
+
15
+ from prompt_toolkit.application import Application
16
+ from prompt_toolkit.key_binding import KeyBindings
17
+ from prompt_toolkit.layout import Dimension, Layout, VSplit, Window
18
+ from prompt_toolkit.layout.controls import FormattedTextControl
19
+ from prompt_toolkit.widgets import Frame
20
+ from rich.console import Console
21
+ from rich.markdown import Markdown
22
+
23
+ from code_puppy.config import AUTOSAVE_DIR
24
+ from code_puppy.session_storage import list_sessions, load_session
25
+ from code_puppy.tools.command_runner import set_awaiting_user_input
26
+
27
+ PAGE_SIZE = 15 # Sessions per page
28
+
29
+
30
+ def _get_session_metadata(base_dir: Path, session_name: str) -> dict:
31
+ """Load metadata for a session."""
32
+ meta_path = base_dir / f"{session_name}_meta.json"
33
+ try:
34
+ with meta_path.open("r", encoding="utf-8") as f:
35
+ return json.load(f)
36
+ except Exception:
37
+ return {}
38
+
39
+
40
+ def _get_session_entries(base_dir: Path) -> List[Tuple[str, dict]]:
41
+ """Get all sessions with their metadata, sorted by timestamp."""
42
+ try:
43
+ sessions = list_sessions(base_dir)
44
+ except (FileNotFoundError, PermissionError):
45
+ return []
46
+
47
+ entries = []
48
+
49
+ for name in sessions:
50
+ try:
51
+ metadata = _get_session_metadata(base_dir, name)
52
+ except (FileNotFoundError, PermissionError):
53
+ metadata = {}
54
+ entries.append((name, metadata))
55
+
56
+ # Sort by timestamp (most recent first)
57
+ def sort_key(entry):
58
+ _, metadata = entry
59
+ timestamp = metadata.get("timestamp")
60
+ if timestamp:
61
+ try:
62
+ return datetime.fromisoformat(timestamp)
63
+ except ValueError:
64
+ return datetime.min
65
+ return datetime.min
66
+
67
+ entries.sort(key=sort_key, reverse=True)
68
+ return entries
69
+
70
+
71
+ def _extract_last_user_message(history: list) -> str:
72
+ """Extract the most recent user message from history.
73
+
74
+ Joins all content parts from the message since messages can have
75
+ multiple parts (e.g., text + attachments, multi-part prompts).
76
+ """
77
+ # Walk backwards through history to find last user message
78
+ for msg in reversed(history):
79
+ content_parts = []
80
+ for part in msg.parts:
81
+ if hasattr(part, "content"):
82
+ content = part.content
83
+ if isinstance(content, str) and content.strip():
84
+ content_parts.append(content)
85
+ if content_parts:
86
+ return "\n\n".join(content_parts)
87
+ return "[No messages found]"
88
+
89
+
90
+ def _extract_message_content(msg) -> Tuple[str, str]:
91
+ """Extract role and content from a message.
92
+
93
+ Returns:
94
+ Tuple of (role, content) where role is 'user', 'assistant', or 'tool'
95
+ """
96
+ # Determine role based on message kind AND part types
97
+ # tool-return comes in a 'request' message but it's not from the user
98
+ part_kinds = [getattr(p, "part_kind", "unknown") for p in msg.parts]
99
+
100
+ if msg.kind == "request":
101
+ # Check if this is a tool return (not actually user input)
102
+ if all(pk == "tool-return" for pk in part_kinds):
103
+ role = "tool"
104
+ else:
105
+ role = "user"
106
+ else:
107
+ # Response from assistant
108
+ if all(pk == "tool-call" for pk in part_kinds):
109
+ role = "tool" # Pure tool call, label as tool activity
110
+ else:
111
+ role = "assistant"
112
+
113
+ # Extract content from parts, handling different part types
114
+ content_parts = []
115
+ for part in msg.parts:
116
+ part_kind = getattr(part, "part_kind", "unknown")
117
+
118
+ if part_kind == "tool-call":
119
+ # Assistant is calling a tool - show tool name and args preview
120
+ tool_name = getattr(part, "tool_name", "unknown")
121
+ args = getattr(part, "args", {})
122
+ # Create a condensed args preview
123
+ if args:
124
+ args_preview = str(args)[:100]
125
+ if len(str(args)) > 100:
126
+ args_preview += "..."
127
+ content_parts.append(
128
+ f"🔧 Tool Call: {tool_name}\n Args: {args_preview}"
129
+ )
130
+ else:
131
+ content_parts.append(f"🔧 Tool Call: {tool_name}")
132
+
133
+ elif part_kind == "tool-return":
134
+ # Tool result being returned - show tool name and truncated result
135
+ tool_name = getattr(part, "tool_name", "unknown")
136
+ result = getattr(part, "content", "")
137
+ if isinstance(result, str) and result.strip():
138
+ # Truncate long results
139
+ preview = result[:200].replace("\n", " ")
140
+ if len(result) > 200:
141
+ preview += "..."
142
+ content_parts.append(f"📥 Tool Result: {tool_name}\n {preview}")
143
+ else:
144
+ content_parts.append(f"📥 Tool Result: {tool_name}")
145
+
146
+ elif hasattr(part, "content"):
147
+ # Regular text content (user-prompt, text, thinking, etc.)
148
+ content = part.content
149
+ if isinstance(content, str) and content.strip():
150
+ content_parts.append(content)
151
+
152
+ content = "\n\n".join(content_parts) if content_parts else "[No content]"
153
+ return role, content
154
+
155
+
156
+ def _render_menu_panel(
157
+ entries: List[Tuple[str, dict]],
158
+ page: int,
159
+ selected_idx: int,
160
+ browse_mode: bool = False,
161
+ ) -> List:
162
+ """Render the left menu panel with pagination."""
163
+ lines = []
164
+ total_pages = (len(entries) + PAGE_SIZE - 1) // PAGE_SIZE if entries else 1
165
+ start_idx = page * PAGE_SIZE
166
+ end_idx = min(start_idx + PAGE_SIZE, len(entries))
167
+
168
+ lines.append(("", f" Session Page(s): ({page + 1}/{total_pages})"))
169
+ lines.append(("", "\n\n"))
170
+
171
+ if not entries:
172
+ lines.append(("fg:yellow", " No autosave sessions found."))
173
+ lines.append(("", "\n\n"))
174
+ # Navigation hints (always show)
175
+ lines.append(("", "\n"))
176
+ lines.append(("fg:ansibrightblack", " ↑/↓ "))
177
+ lines.append(("", "Navigate\n"))
178
+ lines.append(("fg:ansibrightblack", " ←/→ "))
179
+ lines.append(("", "Page\n"))
180
+ lines.append(("fg:green", " Enter "))
181
+ lines.append(("", "Load\n"))
182
+ lines.append(("fg:ansibrightred", " Ctrl+C "))
183
+ lines.append(("", "Cancel"))
184
+ return lines
185
+
186
+ # Show sessions for current page
187
+ for i in range(start_idx, end_idx):
188
+ session_name, metadata = entries[i]
189
+ is_selected = i == selected_idx
190
+
191
+ # Format timestamp
192
+ timestamp = metadata.get("timestamp", "unknown")
193
+ try:
194
+ dt = datetime.fromisoformat(timestamp)
195
+ time_str = dt.strftime("%Y-%m-%d %H:%M")
196
+ except Exception:
197
+ time_str = "unknown time"
198
+
199
+ # Format message count
200
+ msg_count = metadata.get("message_count", "?")
201
+
202
+ # Highlight selected item
203
+ if is_selected:
204
+ lines.append(("fg:ansibrightblack", f" > {time_str} • {msg_count} msgs"))
205
+ else:
206
+ lines.append(("fg:ansibrightblack", f" {time_str} • {msg_count} msgs"))
207
+
208
+ lines.append(("", "\n"))
209
+
210
+ # Navigation hints - change based on browse mode
211
+ lines.append(("", "\n"))
212
+ if browse_mode:
213
+ lines.append(("fg:ansicyan", " ↑/↓ "))
214
+ lines.append(("", "Browse msgs\n"))
215
+ lines.append(("fg:ansiyellow", " Esc "))
216
+ lines.append(("", "Exit browser\n"))
217
+ else:
218
+ lines.append(("fg:ansibrightblack", " ↑/↓ "))
219
+ lines.append(("", "Navigate\n"))
220
+ lines.append(("fg:ansibrightblack", " ←/→ "))
221
+ lines.append(("", "Page\n"))
222
+ lines.append(("fg:ansicyan", " e "))
223
+ lines.append(("", "Browse msgs\n"))
224
+ lines.append(("fg:green", " Enter "))
225
+ lines.append(("", "Load\n"))
226
+ lines.append(("fg:ansibrightred", " Ctrl+C "))
227
+ lines.append(("", "Cancel"))
228
+
229
+ return lines
230
+
231
+
232
+ def _render_message_browser_panel(
233
+ history: list,
234
+ message_idx: int,
235
+ session_name: str,
236
+ ) -> List:
237
+ """Render the message browser panel showing a single message.
238
+
239
+ Args:
240
+ history: Full message history list
241
+ message_idx: Index into history (0 = most recent)
242
+ session_name: Name of the session being browsed
243
+ """
244
+ lines = []
245
+
246
+ lines.append(("fg:ansicyan bold", " MESSAGE BROWSER"))
247
+ lines.append(("", "\n\n"))
248
+
249
+ total_messages = len(history)
250
+ if total_messages == 0:
251
+ lines.append(("fg:yellow", " No messages in this session."))
252
+ lines.append(("", "\n"))
253
+ return lines
254
+
255
+ # Clamp index to valid range
256
+ message_idx = max(0, min(message_idx, total_messages - 1))
257
+
258
+ # Get message (reverse index so 0 = most recent)
259
+ actual_idx = total_messages - 1 - message_idx
260
+ msg = history[actual_idx]
261
+
262
+ # Extract role and content
263
+ role, content = _extract_message_content(msg)
264
+
265
+ # Session info
266
+ lines.append(("fg:ansibrightblack", f" Session: {session_name}"))
267
+ lines.append(("", "\n"))
268
+
269
+ # Message position indicator
270
+ display_num = message_idx + 1 # 1-based for display
271
+ lines.append(("bold", f" Message {display_num} of {total_messages}"))
272
+ lines.append(("", "\n\n"))
273
+
274
+ # Role indicator with icon and color
275
+ if role == "user":
276
+ lines.append(("fg:ansicyan bold", " 🧑 USER"))
277
+ elif role == "tool":
278
+ lines.append(("fg:ansiyellow bold", " 🔧 TOOL"))
279
+ else:
280
+ lines.append(("fg:ansigreen bold", " 🤖 ASSISTANT"))
281
+ lines.append(("", "\n"))
282
+
283
+ # Separator line
284
+ lines.append(("fg:ansibrightblack", " " + "─" * 40))
285
+ lines.append(("", "\n"))
286
+
287
+ # Render content - use markdown for user/assistant, plain text for tool
288
+ try:
289
+ if role == "tool":
290
+ # Tool messages are already formatted, don't pass through markdown
291
+ # Use yellow color for tool output
292
+ rendered = content
293
+ text_color = "fg:ansiyellow"
294
+ else:
295
+ # User and assistant messages should be rendered as markdown
296
+ # Rich will handle the styling via ANSI codes
297
+ console = Console(
298
+ file=StringIO(),
299
+ legacy_windows=False,
300
+ no_color=False,
301
+ force_terminal=False,
302
+ width=72,
303
+ )
304
+ md = Markdown(content)
305
+ console.print(md)
306
+ rendered = console.file.getvalue()
307
+ # Don't override Rich's ANSI styling - use empty style
308
+ text_color = ""
309
+
310
+ # Show full message without truncation
311
+ message_lines = rendered.split("\n")
312
+
313
+ for line in message_lines:
314
+ lines.append((text_color, f" {line}"))
315
+ lines.append(("", "\n"))
316
+
317
+ except Exception as e:
318
+ lines.append(("fg:red", f" Error rendering message: {e}"))
319
+ lines.append(("", "\n"))
320
+
321
+ # Navigation hint at bottom
322
+ lines.append(("", "\n"))
323
+ lines.append(("fg:ansibrightblack", " ↑ older ↓ newer Esc exit"))
324
+ lines.append(("", "\n"))
325
+
326
+ return lines
327
+
328
+
329
+ def _render_preview_panel(base_dir: Path, entry: Optional[Tuple[str, dict]]) -> List:
330
+ """Render the right preview panel with message content using rich markdown."""
331
+ lines = []
332
+
333
+ lines.append(("dim cyan", " PREVIEW"))
334
+ lines.append(("", "\n\n"))
335
+
336
+ if not entry:
337
+ lines.append(("fg:yellow", " No session selected."))
338
+ lines.append(("", "\n"))
339
+ return lines
340
+
341
+ session_name, metadata = entry
342
+
343
+ # Show metadata
344
+ lines.append(("bold", " Session: "))
345
+ lines.append(("", session_name))
346
+ lines.append(("", "\n"))
347
+
348
+ timestamp = metadata.get("timestamp", "unknown")
349
+ try:
350
+ dt = datetime.fromisoformat(timestamp)
351
+ time_str = dt.strftime("%Y-%m-%d %H:%M:%S")
352
+ except Exception:
353
+ time_str = timestamp
354
+ lines.append(("fg:ansibrightblack", f" Saved: {time_str}"))
355
+ lines.append(("", "\n"))
356
+
357
+ msg_count = metadata.get("message_count", 0)
358
+ tokens = metadata.get("total_tokens", 0)
359
+ lines.append(
360
+ ("fg:ansibrightblack", f" Messages: {msg_count} • Tokens: {tokens:,}")
361
+ )
362
+ lines.append(("", "\n\n"))
363
+
364
+ lines.append(("bold", " Last Message:"))
365
+ lines.append(("fg:ansibrightblack", " (press 'e' to browse full history)"))
366
+ lines.append(("", "\n"))
367
+
368
+ # Try to load and preview the last message
369
+ try:
370
+ history = load_session(session_name, base_dir)
371
+ last_message = _extract_last_user_message(history)
372
+
373
+ # Render markdown with rich
374
+ console = Console(
375
+ file=StringIO(),
376
+ legacy_windows=False,
377
+ no_color=False,
378
+ force_terminal=False,
379
+ width=76,
380
+ )
381
+ md = Markdown(last_message)
382
+ console.print(md)
383
+ rendered = console.file.getvalue()
384
+
385
+ # Show full message without truncation
386
+ message_lines = rendered.split("\n")
387
+
388
+ for line in message_lines:
389
+ # Rich already rendered the markdown, just display it dimmed
390
+ lines.append(("fg:ansibrightblack", f" {line}"))
391
+ lines.append(("", "\n"))
392
+
393
+ except Exception as e:
394
+ lines.append(("fg:red", f" Error loading preview: {e}"))
395
+ lines.append(("", "\n"))
396
+
397
+ return lines
398
+
399
+
400
+ # Default number of messages to display when resuming a session
401
+ # This is overridden by the user config 'resume_message_count'
402
+ DEFAULT_RESUME_DISPLAY_COUNT = 50
403
+
404
+
405
+ def display_resumed_history(
406
+ history: list,
407
+ num_messages: int | None = None,
408
+ ) -> None:
409
+ """Display recent message history after resuming a session.
410
+
411
+ Shows the last N messages from the conversation so users have context
412
+ about where they left off. Uses the same rendering style as normal chat.
413
+
414
+ Args:
415
+ history: The full message history list
416
+ num_messages: Number of messages to display. If None, uses the
417
+ 'resume_message_count' config value (default 50).
418
+ Configurable via: /set resume_message_count=50
419
+ """
420
+ from rich.console import Console
421
+ from rich.markdown import Markdown
422
+ from rich.rule import Rule
423
+
424
+ from code_puppy.config import get_banner_color, get_resume_message_count
425
+
426
+ if not history:
427
+ return
428
+
429
+ # Use config value if num_messages not explicitly provided
430
+ if num_messages is None:
431
+ num_messages = get_resume_message_count()
432
+
433
+ console = Console()
434
+ total_messages = len(history)
435
+
436
+ # Skip if only system message exists
437
+ if total_messages <= 1:
438
+ return
439
+
440
+ # Determine which messages to show (skip first system message)
441
+ # We want to show the last N non-system messages
442
+ displayable_history = history[1:] # Skip system message
443
+ total_displayable = len(displayable_history)
444
+
445
+ if total_displayable == 0:
446
+ return
447
+
448
+ messages_to_show = (
449
+ displayable_history[-num_messages:]
450
+ if total_displayable > num_messages
451
+ else displayable_history
452
+ )
453
+ hidden_count = total_displayable - len(messages_to_show)
454
+
455
+ # Print header with hidden count if applicable
456
+ console.print()
457
+ if hidden_count > 0:
458
+ console.print(
459
+ Rule(
460
+ f"{hidden_count} earlier messages",
461
+ style="dim",
462
+ )
463
+ )
464
+ console.print()
465
+
466
+ # Get banner color for agent responses
467
+ response_color = get_banner_color("agent_response")
468
+
469
+ # Render each message in the same style as normal chat
470
+ for msg in messages_to_show:
471
+ role, content = _extract_message_content(msg)
472
+
473
+ # Print banner matching normal chat style
474
+ if role == "user":
475
+ # User messages don't have a banner in normal chat,
476
+ # but we add one for clarity in resumed history
477
+ console.print("[dim]> [/dim]", end="")
478
+ console.print(f"[bold]{content}[/bold]")
479
+ elif role == "tool":
480
+ # Tool output is typically dim/collapsed
481
+ console.print(f"[dim]{content}[/dim]")
482
+ else: # assistant
483
+ # Use the exact same banner format as normal AGENT RESPONSE
484
+ banner = f"[bold white on {response_color}] AGENT RESPONSE [/bold white on {response_color}]"
485
+ console.print(f"\n{banner}")
486
+ # Render content as markdown (same as normal chat)
487
+ md = Markdown(content)
488
+ console.print(md)
489
+
490
+ console.print() # Blank line between messages
491
+
492
+ # Print footer separator
493
+ console.print(Rule("Session Resumed", style="bold green"))
494
+ console.print()
495
+
496
+
497
+ async def interactive_autosave_picker() -> Optional[str]:
498
+ """Show interactive terminal UI to select an autosave session.
499
+
500
+ Returns:
501
+ Session name to load, or None if cancelled
502
+ """
503
+ base_dir = Path(AUTOSAVE_DIR)
504
+ entries = _get_session_entries(base_dir)
505
+
506
+ if not entries:
507
+ from code_puppy.messaging import emit_info
508
+
509
+ emit_info("No autosave sessions found.")
510
+ return None
511
+
512
+ # State
513
+ selected_idx = [0] # Current selection (global index)
514
+ current_page = [0] # Current page
515
+ result = [None] # Selected session name
516
+
517
+ # Browse mode state
518
+ browse_mode = [False] # Are we browsing messages within a session?
519
+ message_idx = [0] # Current message index (0 = most recent)
520
+ cached_history = [None] # Cached history for current session in browse mode
521
+
522
+ total_pages = (len(entries) + PAGE_SIZE - 1) // PAGE_SIZE
523
+
524
+ def get_current_entry() -> Optional[Tuple[str, dict]]:
525
+ if 0 <= selected_idx[0] < len(entries):
526
+ return entries[selected_idx[0]]
527
+ return None
528
+
529
+ # Build UI
530
+ menu_control = FormattedTextControl(text="")
531
+ preview_control = FormattedTextControl(text="")
532
+
533
+ def update_display():
534
+ """Update both panels."""
535
+ menu_control.text = _render_menu_panel(
536
+ entries, current_page[0], selected_idx[0], browse_mode[0]
537
+ )
538
+ # Show message browser if in browse mode, otherwise show preview
539
+ if browse_mode[0] and cached_history[0] is not None:
540
+ entry = get_current_entry()
541
+ session_name = entry[0] if entry else "unknown"
542
+ preview_control.text = _render_message_browser_panel(
543
+ cached_history[0], message_idx[0], session_name
544
+ )
545
+ else:
546
+ preview_control.text = _render_preview_panel(base_dir, get_current_entry())
547
+
548
+ menu_window = Window(
549
+ content=menu_control, wrap_lines=True, width=Dimension(weight=30)
550
+ )
551
+ preview_window = Window(
552
+ content=preview_control, wrap_lines=True, width=Dimension(weight=70)
553
+ )
554
+
555
+ menu_frame = Frame(menu_window, width=Dimension(weight=30), title="Sessions")
556
+ preview_frame = Frame(preview_window, width=Dimension(weight=70), title="Preview")
557
+
558
+ # Make left panel narrower (15% vs 85%)
559
+ root_container = VSplit(
560
+ [
561
+ menu_frame,
562
+ preview_frame,
563
+ ]
564
+ )
565
+
566
+ # Key bindings
567
+ kb = KeyBindings()
568
+
569
+ @kb.add("up")
570
+ @kb.add("c-p") # Ctrl+P = previous (Emacs-style)
571
+ def _(event):
572
+ if browse_mode[0]:
573
+ # In browse mode: go to older message
574
+ if cached_history[0] and message_idx[0] < len(cached_history[0]) - 1:
575
+ message_idx[0] += 1
576
+ update_display()
577
+ else:
578
+ # Normal mode: navigate sessions
579
+ if selected_idx[0] > 0:
580
+ selected_idx[0] -= 1
581
+ # Update page if needed
582
+ current_page[0] = selected_idx[0] // PAGE_SIZE
583
+ update_display()
584
+
585
+ @kb.add("down")
586
+ @kb.add("c-n") # Ctrl+N = next (Emacs-style)
587
+ def _(event):
588
+ if browse_mode[0]:
589
+ # In browse mode: go to newer message
590
+ if message_idx[0] > 0:
591
+ message_idx[0] -= 1
592
+ update_display()
593
+ else:
594
+ # Normal mode: navigate sessions
595
+ if selected_idx[0] < len(entries) - 1:
596
+ selected_idx[0] += 1
597
+ # Update page if needed
598
+ current_page[0] = selected_idx[0] // PAGE_SIZE
599
+ update_display()
600
+
601
+ @kb.add("left")
602
+ def _(event):
603
+ if current_page[0] > 0:
604
+ current_page[0] -= 1
605
+ selected_idx[0] = current_page[0] * PAGE_SIZE
606
+ update_display()
607
+
608
+ @kb.add("right")
609
+ def _(event):
610
+ if current_page[0] < total_pages - 1:
611
+ current_page[0] += 1
612
+ selected_idx[0] = current_page[0] * PAGE_SIZE
613
+ update_display()
614
+
615
+ @kb.add("e")
616
+ def _(event):
617
+ """Enter message browse mode."""
618
+ if browse_mode[0]:
619
+ return # Already in browse mode
620
+ entry = get_current_entry()
621
+ if entry:
622
+ session_name = entry[0]
623
+ try:
624
+ cached_history[0] = load_session(session_name, base_dir)
625
+ browse_mode[0] = True
626
+ message_idx[0] = 0 # Start at most recent
627
+ update_display()
628
+ except Exception:
629
+ pass # Silently fail if can't load
630
+
631
+ @kb.add("escape")
632
+ def _(event):
633
+ """Exit browse mode or cancel."""
634
+ if browse_mode[0]:
635
+ browse_mode[0] = False
636
+ cached_history[0] = None
637
+ message_idx[0] = 0
638
+ update_display()
639
+ else:
640
+ # Not in browse mode - treat as cancel
641
+ result[0] = None
642
+ event.app.exit()
643
+
644
+ @kb.add("q")
645
+ def _(event):
646
+ """Exit browse mode (only when in browse mode)."""
647
+ if browse_mode[0]:
648
+ browse_mode[0] = False
649
+ cached_history[0] = None
650
+ message_idx[0] = 0
651
+ update_display()
652
+
653
+ @kb.add("enter")
654
+ def _(event):
655
+ entry = get_current_entry()
656
+ if entry:
657
+ result[0] = entry[0] # Store session name
658
+ event.app.exit()
659
+
660
+ @kb.add("c-c")
661
+ def _(event):
662
+ result[0] = None
663
+ event.app.exit()
664
+
665
+ layout = Layout(root_container)
666
+ app = Application(
667
+ layout=layout,
668
+ key_bindings=kb,
669
+ full_screen=False,
670
+ mouse_support=False,
671
+ )
672
+
673
+ set_awaiting_user_input(True)
674
+
675
+ # Enter alternate screen buffer once for entire session
676
+ sys.stdout.write("\033[?1049h") # Enter alternate buffer
677
+ sys.stdout.write("\033[2J\033[H") # Clear and home
678
+ sys.stdout.flush()
679
+ await asyncio.sleep(0.05)
680
+
681
+ try:
682
+ # Initial display
683
+ update_display()
684
+
685
+ # Just clear the current buffer (don't switch buffers)
686
+ sys.stdout.write("\033[2J\033[H") # Clear screen within current buffer
687
+ sys.stdout.flush()
688
+
689
+ # Run application (stays in same alternate buffer)
690
+ await app.run_async()
691
+
692
+ finally:
693
+ # Exit alternate screen buffer once at end
694
+ sys.stdout.write("\033[?1049l") # Exit alternate buffer
695
+ sys.stdout.flush()
696
+ # Reset awaiting input flag
697
+ set_awaiting_user_input(False)
698
+
699
+ # Clear exit message
700
+ from code_puppy.messaging import emit_info
701
+
702
+ emit_info("✓ Exited session browser")
703
+
704
+ return result[0]