code-puppy 0.0.169__py3-none-any.whl → 0.0.366__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (243) hide show
  1. code_puppy/__init__.py +7 -1
  2. code_puppy/agents/__init__.py +8 -8
  3. code_puppy/agents/agent_c_reviewer.py +155 -0
  4. code_puppy/agents/agent_code_puppy.py +9 -2
  5. code_puppy/agents/agent_code_reviewer.py +90 -0
  6. code_puppy/agents/agent_cpp_reviewer.py +132 -0
  7. code_puppy/agents/agent_creator_agent.py +48 -9
  8. code_puppy/agents/agent_golang_reviewer.py +151 -0
  9. code_puppy/agents/agent_javascript_reviewer.py +160 -0
  10. code_puppy/agents/agent_manager.py +146 -199
  11. code_puppy/agents/agent_pack_leader.py +383 -0
  12. code_puppy/agents/agent_planning.py +163 -0
  13. code_puppy/agents/agent_python_programmer.py +165 -0
  14. code_puppy/agents/agent_python_reviewer.py +90 -0
  15. code_puppy/agents/agent_qa_expert.py +163 -0
  16. code_puppy/agents/agent_qa_kitten.py +208 -0
  17. code_puppy/agents/agent_security_auditor.py +181 -0
  18. code_puppy/agents/agent_terminal_qa.py +323 -0
  19. code_puppy/agents/agent_typescript_reviewer.py +166 -0
  20. code_puppy/agents/base_agent.py +1713 -1
  21. code_puppy/agents/event_stream_handler.py +350 -0
  22. code_puppy/agents/json_agent.py +12 -1
  23. code_puppy/agents/pack/__init__.py +34 -0
  24. code_puppy/agents/pack/bloodhound.py +304 -0
  25. code_puppy/agents/pack/husky.py +321 -0
  26. code_puppy/agents/pack/retriever.py +393 -0
  27. code_puppy/agents/pack/shepherd.py +348 -0
  28. code_puppy/agents/pack/terrier.py +287 -0
  29. code_puppy/agents/pack/watchdog.py +367 -0
  30. code_puppy/agents/prompt_reviewer.py +145 -0
  31. code_puppy/agents/subagent_stream_handler.py +276 -0
  32. code_puppy/api/__init__.py +13 -0
  33. code_puppy/api/app.py +169 -0
  34. code_puppy/api/main.py +21 -0
  35. code_puppy/api/pty_manager.py +446 -0
  36. code_puppy/api/routers/__init__.py +12 -0
  37. code_puppy/api/routers/agents.py +36 -0
  38. code_puppy/api/routers/commands.py +217 -0
  39. code_puppy/api/routers/config.py +74 -0
  40. code_puppy/api/routers/sessions.py +232 -0
  41. code_puppy/api/templates/terminal.html +361 -0
  42. code_puppy/api/websocket.py +154 -0
  43. code_puppy/callbacks.py +174 -4
  44. code_puppy/chatgpt_codex_client.py +283 -0
  45. code_puppy/claude_cache_client.py +586 -0
  46. code_puppy/cli_runner.py +916 -0
  47. code_puppy/command_line/add_model_menu.py +1079 -0
  48. code_puppy/command_line/agent_menu.py +395 -0
  49. code_puppy/command_line/attachments.py +395 -0
  50. code_puppy/command_line/autosave_menu.py +605 -0
  51. code_puppy/command_line/clipboard.py +527 -0
  52. code_puppy/command_line/colors_menu.py +520 -0
  53. code_puppy/command_line/command_handler.py +233 -627
  54. code_puppy/command_line/command_registry.py +150 -0
  55. code_puppy/command_line/config_commands.py +715 -0
  56. code_puppy/command_line/core_commands.py +792 -0
  57. code_puppy/command_line/diff_menu.py +863 -0
  58. code_puppy/command_line/load_context_completion.py +15 -22
  59. code_puppy/command_line/mcp/base.py +1 -4
  60. code_puppy/command_line/mcp/catalog_server_installer.py +175 -0
  61. code_puppy/command_line/mcp/custom_server_form.py +688 -0
  62. code_puppy/command_line/mcp/custom_server_installer.py +195 -0
  63. code_puppy/command_line/mcp/edit_command.py +148 -0
  64. code_puppy/command_line/mcp/handler.py +9 -4
  65. code_puppy/command_line/mcp/help_command.py +6 -5
  66. code_puppy/command_line/mcp/install_command.py +16 -27
  67. code_puppy/command_line/mcp/install_menu.py +685 -0
  68. code_puppy/command_line/mcp/list_command.py +3 -3
  69. code_puppy/command_line/mcp/logs_command.py +174 -65
  70. code_puppy/command_line/mcp/remove_command.py +2 -2
  71. code_puppy/command_line/mcp/restart_command.py +12 -4
  72. code_puppy/command_line/mcp/search_command.py +17 -11
  73. code_puppy/command_line/mcp/start_all_command.py +22 -13
  74. code_puppy/command_line/mcp/start_command.py +50 -31
  75. code_puppy/command_line/mcp/status_command.py +6 -7
  76. code_puppy/command_line/mcp/stop_all_command.py +11 -8
  77. code_puppy/command_line/mcp/stop_command.py +11 -10
  78. code_puppy/command_line/mcp/test_command.py +2 -2
  79. code_puppy/command_line/mcp/utils.py +1 -1
  80. code_puppy/command_line/mcp/wizard_utils.py +22 -18
  81. code_puppy/command_line/mcp_completion.py +174 -0
  82. code_puppy/command_line/model_picker_completion.py +89 -30
  83. code_puppy/command_line/model_settings_menu.py +884 -0
  84. code_puppy/command_line/motd.py +14 -8
  85. code_puppy/command_line/onboarding_slides.py +179 -0
  86. code_puppy/command_line/onboarding_wizard.py +340 -0
  87. code_puppy/command_line/pin_command_completion.py +329 -0
  88. code_puppy/command_line/prompt_toolkit_completion.py +626 -75
  89. code_puppy/command_line/session_commands.py +296 -0
  90. code_puppy/command_line/utils.py +54 -0
  91. code_puppy/config.py +1181 -51
  92. code_puppy/error_logging.py +118 -0
  93. code_puppy/gemini_code_assist.py +385 -0
  94. code_puppy/gemini_model.py +602 -0
  95. code_puppy/http_utils.py +220 -104
  96. code_puppy/keymap.py +128 -0
  97. code_puppy/main.py +5 -594
  98. code_puppy/{mcp → mcp_}/__init__.py +17 -0
  99. code_puppy/{mcp → mcp_}/async_lifecycle.py +35 -4
  100. code_puppy/{mcp → mcp_}/blocking_startup.py +70 -43
  101. code_puppy/{mcp → mcp_}/captured_stdio_server.py +2 -2
  102. code_puppy/{mcp → mcp_}/config_wizard.py +5 -5
  103. code_puppy/{mcp → mcp_}/dashboard.py +15 -6
  104. code_puppy/{mcp → mcp_}/examples/retry_example.py +4 -1
  105. code_puppy/{mcp → mcp_}/managed_server.py +66 -39
  106. code_puppy/{mcp → mcp_}/manager.py +146 -52
  107. code_puppy/mcp_/mcp_logs.py +224 -0
  108. code_puppy/{mcp → mcp_}/registry.py +6 -6
  109. code_puppy/{mcp → mcp_}/server_registry_catalog.py +25 -8
  110. code_puppy/messaging/__init__.py +199 -2
  111. code_puppy/messaging/bus.py +610 -0
  112. code_puppy/messaging/commands.py +167 -0
  113. code_puppy/messaging/markdown_patches.py +57 -0
  114. code_puppy/messaging/message_queue.py +17 -48
  115. code_puppy/messaging/messages.py +500 -0
  116. code_puppy/messaging/queue_console.py +1 -24
  117. code_puppy/messaging/renderers.py +43 -146
  118. code_puppy/messaging/rich_renderer.py +1027 -0
  119. code_puppy/messaging/spinner/__init__.py +33 -5
  120. code_puppy/messaging/spinner/console_spinner.py +92 -52
  121. code_puppy/messaging/spinner/spinner_base.py +29 -0
  122. code_puppy/messaging/subagent_console.py +461 -0
  123. code_puppy/model_factory.py +686 -80
  124. code_puppy/model_utils.py +167 -0
  125. code_puppy/models.json +86 -104
  126. code_puppy/models_dev_api.json +1 -0
  127. code_puppy/models_dev_parser.py +592 -0
  128. code_puppy/plugins/__init__.py +164 -10
  129. code_puppy/plugins/antigravity_oauth/__init__.py +10 -0
  130. code_puppy/plugins/antigravity_oauth/accounts.py +406 -0
  131. code_puppy/plugins/antigravity_oauth/antigravity_model.py +704 -0
  132. code_puppy/plugins/antigravity_oauth/config.py +42 -0
  133. code_puppy/plugins/antigravity_oauth/constants.py +136 -0
  134. code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
  135. code_puppy/plugins/antigravity_oauth/register_callbacks.py +406 -0
  136. code_puppy/plugins/antigravity_oauth/storage.py +271 -0
  137. code_puppy/plugins/antigravity_oauth/test_plugin.py +319 -0
  138. code_puppy/plugins/antigravity_oauth/token.py +167 -0
  139. code_puppy/plugins/antigravity_oauth/transport.py +767 -0
  140. code_puppy/plugins/antigravity_oauth/utils.py +169 -0
  141. code_puppy/plugins/chatgpt_oauth/__init__.py +8 -0
  142. code_puppy/plugins/chatgpt_oauth/config.py +52 -0
  143. code_puppy/plugins/chatgpt_oauth/oauth_flow.py +328 -0
  144. code_puppy/plugins/chatgpt_oauth/register_callbacks.py +94 -0
  145. code_puppy/plugins/chatgpt_oauth/test_plugin.py +293 -0
  146. code_puppy/plugins/chatgpt_oauth/utils.py +489 -0
  147. code_puppy/plugins/claude_code_oauth/README.md +167 -0
  148. code_puppy/plugins/claude_code_oauth/SETUP.md +93 -0
  149. code_puppy/plugins/claude_code_oauth/__init__.py +6 -0
  150. code_puppy/plugins/claude_code_oauth/config.py +50 -0
  151. code_puppy/plugins/claude_code_oauth/register_callbacks.py +308 -0
  152. code_puppy/plugins/claude_code_oauth/test_plugin.py +283 -0
  153. code_puppy/plugins/claude_code_oauth/utils.py +518 -0
  154. code_puppy/plugins/customizable_commands/__init__.py +0 -0
  155. code_puppy/plugins/customizable_commands/register_callbacks.py +169 -0
  156. code_puppy/plugins/example_custom_command/README.md +280 -0
  157. code_puppy/plugins/example_custom_command/register_callbacks.py +51 -0
  158. code_puppy/plugins/file_permission_handler/__init__.py +4 -0
  159. code_puppy/plugins/file_permission_handler/register_callbacks.py +523 -0
  160. code_puppy/plugins/frontend_emitter/__init__.py +25 -0
  161. code_puppy/plugins/frontend_emitter/emitter.py +121 -0
  162. code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
  163. code_puppy/plugins/oauth_puppy_html.py +228 -0
  164. code_puppy/plugins/shell_safety/__init__.py +6 -0
  165. code_puppy/plugins/shell_safety/agent_shell_safety.py +69 -0
  166. code_puppy/plugins/shell_safety/command_cache.py +156 -0
  167. code_puppy/plugins/shell_safety/register_callbacks.py +202 -0
  168. code_puppy/prompts/antigravity_system_prompt.md +1 -0
  169. code_puppy/prompts/codex_system_prompt.md +310 -0
  170. code_puppy/pydantic_patches.py +131 -0
  171. code_puppy/reopenable_async_client.py +8 -8
  172. code_puppy/round_robin_model.py +10 -15
  173. code_puppy/session_storage.py +294 -0
  174. code_puppy/status_display.py +21 -4
  175. code_puppy/summarization_agent.py +52 -14
  176. code_puppy/terminal_utils.py +418 -0
  177. code_puppy/tools/__init__.py +139 -6
  178. code_puppy/tools/agent_tools.py +548 -49
  179. code_puppy/tools/browser/__init__.py +37 -0
  180. code_puppy/tools/browser/browser_control.py +289 -0
  181. code_puppy/tools/browser/browser_interactions.py +545 -0
  182. code_puppy/tools/browser/browser_locators.py +640 -0
  183. code_puppy/tools/browser/browser_manager.py +316 -0
  184. code_puppy/tools/browser/browser_navigation.py +251 -0
  185. code_puppy/tools/browser/browser_screenshot.py +179 -0
  186. code_puppy/tools/browser/browser_scripts.py +462 -0
  187. code_puppy/tools/browser/browser_workflows.py +221 -0
  188. code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
  189. code_puppy/tools/browser/terminal_command_tools.py +521 -0
  190. code_puppy/tools/browser/terminal_screenshot_tools.py +556 -0
  191. code_puppy/tools/browser/terminal_tools.py +525 -0
  192. code_puppy/tools/command_runner.py +941 -153
  193. code_puppy/tools/common.py +1146 -6
  194. code_puppy/tools/display.py +84 -0
  195. code_puppy/tools/file_modifications.py +288 -89
  196. code_puppy/tools/file_operations.py +352 -266
  197. code_puppy/tools/subagent_context.py +158 -0
  198. code_puppy/uvx_detection.py +242 -0
  199. code_puppy/version_checker.py +30 -11
  200. code_puppy-0.0.366.data/data/code_puppy/models.json +110 -0
  201. code_puppy-0.0.366.data/data/code_puppy/models_dev_api.json +1 -0
  202. {code_puppy-0.0.169.dist-info → code_puppy-0.0.366.dist-info}/METADATA +184 -67
  203. code_puppy-0.0.366.dist-info/RECORD +217 -0
  204. {code_puppy-0.0.169.dist-info → code_puppy-0.0.366.dist-info}/WHEEL +1 -1
  205. {code_puppy-0.0.169.dist-info → code_puppy-0.0.366.dist-info}/entry_points.txt +1 -0
  206. code_puppy/agent.py +0 -231
  207. code_puppy/agents/agent_orchestrator.json +0 -26
  208. code_puppy/agents/runtime_manager.py +0 -272
  209. code_puppy/command_line/mcp/add_command.py +0 -183
  210. code_puppy/command_line/meta_command_handler.py +0 -153
  211. code_puppy/message_history_processor.py +0 -490
  212. code_puppy/messaging/spinner/textual_spinner.py +0 -101
  213. code_puppy/state_management.py +0 -200
  214. code_puppy/tui/__init__.py +0 -10
  215. code_puppy/tui/app.py +0 -986
  216. code_puppy/tui/components/__init__.py +0 -21
  217. code_puppy/tui/components/chat_view.py +0 -550
  218. code_puppy/tui/components/command_history_modal.py +0 -218
  219. code_puppy/tui/components/copy_button.py +0 -139
  220. code_puppy/tui/components/custom_widgets.py +0 -63
  221. code_puppy/tui/components/human_input_modal.py +0 -175
  222. code_puppy/tui/components/input_area.py +0 -167
  223. code_puppy/tui/components/sidebar.py +0 -309
  224. code_puppy/tui/components/status_bar.py +0 -182
  225. code_puppy/tui/messages.py +0 -27
  226. code_puppy/tui/models/__init__.py +0 -8
  227. code_puppy/tui/models/chat_message.py +0 -25
  228. code_puppy/tui/models/command_history.py +0 -89
  229. code_puppy/tui/models/enums.py +0 -24
  230. code_puppy/tui/screens/__init__.py +0 -15
  231. code_puppy/tui/screens/help.py +0 -130
  232. code_puppy/tui/screens/mcp_install_wizard.py +0 -803
  233. code_puppy/tui/screens/settings.py +0 -290
  234. code_puppy/tui/screens/tools.py +0 -74
  235. code_puppy-0.0.169.data/data/code_puppy/models.json +0 -128
  236. code_puppy-0.0.169.dist-info/RECORD +0 -112
  237. /code_puppy/{mcp → mcp_}/circuit_breaker.py +0 -0
  238. /code_puppy/{mcp → mcp_}/error_isolation.py +0 -0
  239. /code_puppy/{mcp → mcp_}/health_monitor.py +0 -0
  240. /code_puppy/{mcp → mcp_}/retry_manager.py +0 -0
  241. /code_puppy/{mcp → mcp_}/status_tracker.py +0 -0
  242. /code_puppy/{mcp → mcp_}/system_tools.py +0 -0
  243. {code_puppy-0.0.169.dist-info → code_puppy-0.0.366.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,294 @@
1
+ """Shared helpers for persisting and restoring chat sessions.
2
+
3
+ This module centralises the pickle + metadata handling that used to live in
4
+ both the CLI command handler and the auto-save feature. Keeping it here helps
5
+ us avoid duplication while staying inside the Zen-of-Python sweet spot: simple
6
+ is better than complex, nested side effects are worse than deliberate helpers.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import pickle
13
+ from dataclasses import dataclass
14
+ from pathlib import Path
15
+ from typing import Any, Callable, List
16
+
17
+ SessionHistory = List[Any]
18
+ TokenEstimator = Callable[[Any], int]
19
+
20
+
21
+ @dataclass(slots=True)
22
+ class SessionPaths:
23
+ pickle_path: Path
24
+ metadata_path: Path
25
+
26
+
27
+ @dataclass(slots=True)
28
+ class SessionMetadata:
29
+ session_name: str
30
+ timestamp: str
31
+ message_count: int
32
+ total_tokens: int
33
+ pickle_path: Path
34
+ metadata_path: Path
35
+ auto_saved: bool = False
36
+
37
+ def as_serialisable(self) -> dict[str, Any]:
38
+ return {
39
+ "session_name": self.session_name,
40
+ "timestamp": self.timestamp,
41
+ "message_count": self.message_count,
42
+ "total_tokens": self.total_tokens,
43
+ "file_path": str(self.pickle_path),
44
+ "auto_saved": self.auto_saved,
45
+ }
46
+
47
+
48
+ def ensure_directory(path: Path) -> Path:
49
+ path.mkdir(parents=True, exist_ok=True)
50
+ return path
51
+
52
+
53
+ def build_session_paths(base_dir: Path, session_name: str) -> SessionPaths:
54
+ pickle_path = base_dir / f"{session_name}.pkl"
55
+ metadata_path = base_dir / f"{session_name}_meta.json"
56
+ return SessionPaths(pickle_path=pickle_path, metadata_path=metadata_path)
57
+
58
+
59
+ def save_session(
60
+ *,
61
+ history: SessionHistory,
62
+ session_name: str,
63
+ base_dir: Path,
64
+ timestamp: str,
65
+ token_estimator: TokenEstimator,
66
+ auto_saved: bool = False,
67
+ ) -> SessionMetadata:
68
+ ensure_directory(base_dir)
69
+ paths = build_session_paths(base_dir, session_name)
70
+
71
+ with paths.pickle_path.open("wb") as pickle_file:
72
+ pickle.dump(history, pickle_file)
73
+
74
+ total_tokens = sum(token_estimator(message) for message in history)
75
+ metadata = SessionMetadata(
76
+ session_name=session_name,
77
+ timestamp=timestamp,
78
+ message_count=len(history),
79
+ total_tokens=total_tokens,
80
+ pickle_path=paths.pickle_path,
81
+ metadata_path=paths.metadata_path,
82
+ auto_saved=auto_saved,
83
+ )
84
+
85
+ with paths.metadata_path.open("w", encoding="utf-8") as metadata_file:
86
+ json.dump(metadata.as_serialisable(), metadata_file, indent=2)
87
+
88
+ return metadata
89
+
90
+
91
+ def load_session(session_name: str, base_dir: Path) -> SessionHistory:
92
+ paths = build_session_paths(base_dir, session_name)
93
+ if not paths.pickle_path.exists():
94
+ raise FileNotFoundError(paths.pickle_path)
95
+ with paths.pickle_path.open("rb") as pickle_file:
96
+ return pickle.load(pickle_file)
97
+
98
+
99
+ def list_sessions(base_dir: Path) -> List[str]:
100
+ if not base_dir.exists():
101
+ return []
102
+ return sorted(path.stem for path in base_dir.glob("*.pkl"))
103
+
104
+
105
+ def cleanup_sessions(base_dir: Path, max_sessions: int) -> List[str]:
106
+ if max_sessions <= 0:
107
+ return []
108
+
109
+ if not base_dir.exists():
110
+ return []
111
+
112
+ candidate_paths = list(base_dir.glob("*.pkl"))
113
+ if len(candidate_paths) <= max_sessions:
114
+ return []
115
+
116
+ sorted_candidates = sorted(
117
+ ((path.stat().st_mtime, path) for path in candidate_paths),
118
+ key=lambda item: item[0],
119
+ )
120
+
121
+ stale_entries = sorted_candidates[:-max_sessions]
122
+ removed_sessions: List[str] = []
123
+ for _, pickle_path in stale_entries:
124
+ metadata_path = base_dir / f"{pickle_path.stem}_meta.json"
125
+ try:
126
+ pickle_path.unlink(missing_ok=True)
127
+ metadata_path.unlink(missing_ok=True)
128
+ removed_sessions.append(pickle_path.stem)
129
+ except OSError:
130
+ continue
131
+
132
+ return removed_sessions
133
+
134
+
135
+ async def restore_autosave_interactively(base_dir: Path) -> None:
136
+ """Prompt the user to load an autosave session from base_dir, if any exist.
137
+
138
+ This helper is deliberately placed in session_storage to keep autosave
139
+ restoration close to the persistence layer. It uses the same public APIs
140
+ (list_sessions, load_session) and mirrors the interactive behaviours from
141
+ the command handler.
142
+ """
143
+ sessions = list_sessions(base_dir)
144
+ if not sessions:
145
+ return
146
+
147
+ # Import locally to avoid pulling the messaging layer into storage modules
148
+ from datetime import datetime
149
+
150
+ from prompt_toolkit.formatted_text import FormattedText
151
+
152
+ from code_puppy.agents.agent_manager import get_current_agent
153
+ from code_puppy.command_line.prompt_toolkit_completion import (
154
+ get_input_with_combined_completion,
155
+ )
156
+ from code_puppy.messaging import emit_success, emit_system_message, emit_warning
157
+
158
+ entries = []
159
+ for name in sessions:
160
+ meta_path = base_dir / f"{name}_meta.json"
161
+ try:
162
+ with meta_path.open("r", encoding="utf-8") as meta_file:
163
+ data = json.load(meta_file)
164
+ timestamp = data.get("timestamp")
165
+ message_count = data.get("message_count")
166
+ except Exception:
167
+ timestamp = None
168
+ message_count = None
169
+ entries.append((name, timestamp, message_count))
170
+
171
+ def sort_key(entry):
172
+ _, timestamp, _ = entry
173
+ if timestamp:
174
+ try:
175
+ return datetime.fromisoformat(timestamp)
176
+ except ValueError:
177
+ return datetime.min
178
+ return datetime.min
179
+
180
+ entries.sort(key=sort_key, reverse=True)
181
+
182
+ PAGE_SIZE = 5
183
+ total = len(entries)
184
+ page = 0
185
+
186
+ def render_page() -> None:
187
+ start = page * PAGE_SIZE
188
+ end = min(start + PAGE_SIZE, total)
189
+ page_entries = entries[start:end]
190
+ emit_system_message("Autosave Sessions Available:")
191
+ for idx, (name, timestamp, message_count) in enumerate(page_entries, start=1):
192
+ timestamp_display = timestamp or "unknown time"
193
+ message_display = (
194
+ f"{message_count} messages"
195
+ if message_count is not None
196
+ else "unknown size"
197
+ )
198
+ emit_system_message(
199
+ f" [{idx}] {name} ({message_display}, saved at {timestamp_display})"
200
+ )
201
+ # If there are more pages, offer next-page; show 'Return to first page' on last page
202
+ if total > PAGE_SIZE:
203
+ page_count = (total + PAGE_SIZE - 1) // PAGE_SIZE
204
+ is_last_page = (page + 1) >= page_count
205
+ remaining = total - (page * PAGE_SIZE + len(page_entries))
206
+ summary = (
207
+ f" and {remaining} more" if (remaining > 0 and not is_last_page) else ""
208
+ )
209
+ label = "Return to first page" if is_last_page else f"Next page{summary}"
210
+ emit_system_message(f" [6] {label}")
211
+ emit_system_message(" [Enter] Skip loading autosave")
212
+
213
+ chosen_name: str | None = None
214
+
215
+ while True:
216
+ render_page()
217
+ try:
218
+ selection = await get_input_with_combined_completion(
219
+ FormattedText(
220
+ [
221
+ (
222
+ "class:prompt",
223
+ "Pick 1-5 to load, 6 for next, or name/Enter: ",
224
+ )
225
+ ]
226
+ )
227
+ )
228
+ except (KeyboardInterrupt, EOFError):
229
+ emit_warning("Autosave selection cancelled")
230
+ return
231
+
232
+ selection = (selection or "").strip()
233
+ if not selection:
234
+ return
235
+
236
+ # Numeric choice: 1-5 select within current page; 6 advances page
237
+ if selection.isdigit():
238
+ num = int(selection)
239
+ if num == 6 and total > PAGE_SIZE:
240
+ page = (page + 1) % ((total + PAGE_SIZE - 1) // PAGE_SIZE)
241
+ # loop and re-render next page
242
+ continue
243
+ if 1 <= num <= 5:
244
+ start = page * PAGE_SIZE
245
+ idx = start + (num - 1)
246
+ if 0 <= idx < total:
247
+ chosen_name = entries[idx][0]
248
+ break
249
+ else:
250
+ emit_warning("Invalid selection for this page")
251
+ continue
252
+ emit_warning("Invalid selection; choose 1-5 or 6 for next")
253
+ continue
254
+
255
+ # Allow direct typing by exact session name
256
+ for name, _ts, _mc in entries:
257
+ if name == selection:
258
+ chosen_name = name
259
+ break
260
+ if chosen_name:
261
+ break
262
+ emit_warning("No autosave loaded (invalid selection)")
263
+ # keep looping and allow another try
264
+
265
+ if not chosen_name:
266
+ return
267
+
268
+ try:
269
+ history = load_session(chosen_name, base_dir)
270
+ except FileNotFoundError:
271
+ emit_warning(f"Autosave '{chosen_name}' could not be found")
272
+ return
273
+ except Exception as exc:
274
+ emit_warning(f"Failed to load autosave '{chosen_name}': {exc}")
275
+ return
276
+
277
+ agent = get_current_agent()
278
+ agent.set_message_history(history)
279
+
280
+ # Set current autosave session id so subsequent autosaves overwrite this session
281
+ try:
282
+ from code_puppy.config import set_current_autosave_from_session_name
283
+
284
+ set_current_autosave_from_session_name(chosen_name)
285
+ except Exception:
286
+ pass
287
+
288
+ total_tokens = sum(agent.estimate_tokens_for_message(msg) for msg in history)
289
+
290
+ session_path = base_dir / f"{chosen_name}.pkl"
291
+ emit_success(
292
+ f"✅ Autosave loaded: {len(history)} messages ({total_tokens} tokens)\n"
293
+ f"📁 From: {session_path}"
294
+ )
@@ -112,6 +112,9 @@ class StatusDisplay:
112
112
  # Reset token counters for new task
113
113
  self.last_token_count = 0
114
114
  self.current_rate = 0.0
115
+ # Set initial token count
116
+ self.token_count = tokens if tokens >= 0 else 0
117
+ return # Don't calculate rate on first initialization
115
118
 
116
119
  # Allow for incremental updates (common for streaming) or absolute updates
117
120
  if tokens > self.token_count or tokens < 0:
@@ -142,7 +145,7 @@ class StatusDisplay:
142
145
  # Create a highly visible status message
143
146
  status_text = Text.assemble(
144
147
  Text(f"⏳ {rate_text} ", style="bold cyan"),
145
- self.spinner,
148
+ str(self.spinner),
146
149
  Text(
147
150
  f" {self.loading_messages[self.current_message_index]} ⏳",
148
151
  style="bold yellow",
@@ -181,8 +184,11 @@ class StatusDisplay:
181
184
 
182
185
  async def _update_display(self) -> None:
183
186
  """Update the display continuously while active using Rich Live display"""
187
+ # Lazy import to avoid circular dependency during module initialization
188
+ from code_puppy.messaging import emit_info
189
+
184
190
  # Add a newline to ensure we're below the blue bar
185
- self.console.print("\n")
191
+ emit_info("")
186
192
 
187
193
  # Create a Live display that will update in-place
188
194
  with Live(
@@ -209,6 +215,9 @@ class StatusDisplay:
209
215
 
210
216
  def stop(self) -> None:
211
217
  """Stop the status display"""
218
+ # Lazy import to avoid circular dependency during module initialization
219
+ from code_puppy.messaging import emit_info
220
+
212
221
  if self.is_active:
213
222
  self.is_active = False
214
223
  if self.task:
@@ -218,8 +227,8 @@ class StatusDisplay:
218
227
  # Print final stats
219
228
  elapsed = time.time() - self.start_time if self.start_time else 0
220
229
  avg_rate = self.token_count / elapsed if elapsed > 0 else 0
221
- self.console.print(
222
- f"[dim]Completed: {self.token_count} tokens in {elapsed:.1f}s ({avg_rate:.1f} t/s avg)[/dim]"
230
+ emit_info(
231
+ f"Completed: {self.token_count} tokens in {elapsed:.1f}s ({avg_rate:.1f} t/s avg)"
223
232
  )
224
233
 
225
234
  # Reset state
@@ -232,3 +241,11 @@ class StatusDisplay:
232
241
  # Reset global rate to 0 to avoid affecting subsequent tasks
233
242
  global CURRENT_TOKEN_RATE
234
243
  CURRENT_TOKEN_RATE = 0.0
244
+ else:
245
+ # Even if not active, ensure we print stats when stop is called
246
+ # This is for testing purposes
247
+ elapsed = time.time() - self.start_time if self.start_time else 0
248
+ avg_rate = self.token_count / elapsed if elapsed > 0 else 0
249
+ emit_info(
250
+ f"Completed: {self.token_count} tokens in {elapsed:.1f}s ({avg_rate:.1f} t/s avg)"
251
+ )
@@ -4,8 +4,11 @@ from typing import List
4
4
 
5
5
  from pydantic_ai import Agent
6
6
 
7
- from code_puppy.config import get_model_name
8
- from code_puppy.model_factory import ModelFactory
7
+ from code_puppy.config import (
8
+ get_global_model_name,
9
+ get_use_dbos,
10
+ )
11
+ from code_puppy.model_factory import ModelFactory, make_model_settings
9
12
 
10
13
  # Keep a module-level agent reference to avoid rebuilding per call
11
14
  _summarization_agent = None
@@ -14,6 +17,9 @@ _summarization_agent = None
14
17
  # Avoids "event loop is already running" by offloading to a separate thread loop when needed
15
18
  _thread_pool: ThreadPoolExecutor | None = None
16
19
 
20
+ # Reload counter
21
+ _reload_count = 0
22
+
17
23
 
18
24
  def _ensure_thread_pool():
19
25
  global _thread_pool
@@ -30,6 +36,16 @@ async def _run_agent_async(agent: Agent, prompt: str, message_history: List):
30
36
 
31
37
  def run_summarization_sync(prompt: str, message_history: List) -> List:
32
38
  agent = get_summarization_agent()
39
+
40
+ # Handle claude-code models: prepend system prompt to user prompt
41
+ from code_puppy.model_utils import prepare_prompt_for_model
42
+
43
+ model_name = get_global_model_name()
44
+ prepared = prepare_prompt_for_model(
45
+ model_name, _get_summarization_instructions(), prompt
46
+ )
47
+ prompt = prepared.user_prompt
48
+
33
49
  try:
34
50
  # Try to detect if we're already in an event loop
35
51
  asyncio.get_running_loop()
@@ -50,31 +66,53 @@ def run_summarization_sync(prompt: str, message_history: List) -> List:
50
66
  return result.new_messages()
51
67
 
52
68
 
69
+ def _get_summarization_instructions() -> str:
70
+ """Get the system instructions for the summarization agent."""
71
+ return """You are a message summarization expert. Your task is to summarize conversation messages
72
+ while preserving important context and information. The summaries should be concise but capture the essential content
73
+ and intent of the original messages. This is to help manage token usage in a conversation history
74
+ while maintaining context for the AI to continue the conversation effectively.
75
+
76
+ When summarizing:
77
+ 1. Keep summary concise but informative
78
+ 2. Preserve important context and key information and decisions
79
+ 3. Keep any important technical details
80
+ 4. Don't summarize the system message
81
+ 5. Make sure all tool calls and responses are summarized, as they are vital
82
+ 6. Focus on token usage efficiency and system message preservation"""
83
+
84
+
53
85
  def reload_summarization_agent():
54
86
  """Create a specialized agent for summarizing messages when context limit is reached."""
87
+ from code_puppy.model_utils import prepare_prompt_for_model
88
+
55
89
  models_config = ModelFactory.load_config()
56
- model_name = get_model_name()
90
+ model_name = get_global_model_name()
57
91
  model = ModelFactory.get_model(model_name, models_config)
58
92
 
59
- # Specialized instructions for summarization
60
- instructions = """You are a message summarization expert. Your task is to summarize conversation messages
61
- while preserving important context and information. The summaries should be concise but capture the essential
62
- content and intent of the original messages. This is to help manage token usage in a conversation history
63
- while maintaining context for the AI to continue the conversation effectively.
93
+ # Handle claude-code models: swap instructions (prompt prepending happens in run_summarization_sync)
94
+ instructions = _get_summarization_instructions()
95
+ prepared = prepare_prompt_for_model(
96
+ model_name, instructions, "", prepend_system_to_user=False
97
+ )
98
+ instructions = prepared.instructions
64
99
 
65
- When summarizing:
66
- 1. Keep summary brief but informative
67
- 2. Preserve key information and decisions
68
- 3. Keep any important technical details
69
- 4. Don't summarize the system message
70
- 5. Make sure all tool calls and responses are summarized, as they are vital"""
100
+ model_settings = make_model_settings(model_name)
71
101
 
72
102
  agent = Agent(
73
103
  model=model,
74
104
  instructions=instructions,
75
105
  output_type=str,
76
106
  retries=1, # Fewer retries for summarization
107
+ model_settings=model_settings,
77
108
  )
109
+ if get_use_dbos():
110
+ from pydantic_ai.durable_exec.dbos import DBOSAgent
111
+
112
+ global _reload_count
113
+ _reload_count += 1
114
+ dbos_agent = DBOSAgent(agent, name=f"summarization-agent-{_reload_count}")
115
+ return dbos_agent
78
116
  return agent
79
117
 
80
118