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.
- code_puppy/__init__.py +7 -1
- code_puppy/agents/__init__.py +8 -8
- code_puppy/agents/agent_c_reviewer.py +155 -0
- code_puppy/agents/agent_code_puppy.py +9 -2
- code_puppy/agents/agent_code_reviewer.py +90 -0
- code_puppy/agents/agent_cpp_reviewer.py +132 -0
- code_puppy/agents/agent_creator_agent.py +48 -9
- code_puppy/agents/agent_golang_reviewer.py +151 -0
- code_puppy/agents/agent_javascript_reviewer.py +160 -0
- code_puppy/agents/agent_manager.py +146 -199
- code_puppy/agents/agent_pack_leader.py +383 -0
- code_puppy/agents/agent_planning.py +163 -0
- code_puppy/agents/agent_python_programmer.py +165 -0
- code_puppy/agents/agent_python_reviewer.py +90 -0
- code_puppy/agents/agent_qa_expert.py +163 -0
- code_puppy/agents/agent_qa_kitten.py +208 -0
- code_puppy/agents/agent_security_auditor.py +181 -0
- code_puppy/agents/agent_terminal_qa.py +323 -0
- code_puppy/agents/agent_typescript_reviewer.py +166 -0
- code_puppy/agents/base_agent.py +1713 -1
- code_puppy/agents/event_stream_handler.py +350 -0
- code_puppy/agents/json_agent.py +12 -1
- code_puppy/agents/pack/__init__.py +34 -0
- code_puppy/agents/pack/bloodhound.py +304 -0
- code_puppy/agents/pack/husky.py +321 -0
- code_puppy/agents/pack/retriever.py +393 -0
- code_puppy/agents/pack/shepherd.py +348 -0
- code_puppy/agents/pack/terrier.py +287 -0
- code_puppy/agents/pack/watchdog.py +367 -0
- code_puppy/agents/prompt_reviewer.py +145 -0
- code_puppy/agents/subagent_stream_handler.py +276 -0
- code_puppy/api/__init__.py +13 -0
- code_puppy/api/app.py +169 -0
- code_puppy/api/main.py +21 -0
- code_puppy/api/pty_manager.py +446 -0
- code_puppy/api/routers/__init__.py +12 -0
- code_puppy/api/routers/agents.py +36 -0
- code_puppy/api/routers/commands.py +217 -0
- code_puppy/api/routers/config.py +74 -0
- code_puppy/api/routers/sessions.py +232 -0
- code_puppy/api/templates/terminal.html +361 -0
- code_puppy/api/websocket.py +154 -0
- code_puppy/callbacks.py +174 -4
- code_puppy/chatgpt_codex_client.py +283 -0
- code_puppy/claude_cache_client.py +586 -0
- code_puppy/cli_runner.py +916 -0
- code_puppy/command_line/add_model_menu.py +1079 -0
- code_puppy/command_line/agent_menu.py +395 -0
- code_puppy/command_line/attachments.py +395 -0
- code_puppy/command_line/autosave_menu.py +605 -0
- code_puppy/command_line/clipboard.py +527 -0
- code_puppy/command_line/colors_menu.py +520 -0
- code_puppy/command_line/command_handler.py +233 -627
- code_puppy/command_line/command_registry.py +150 -0
- code_puppy/command_line/config_commands.py +715 -0
- code_puppy/command_line/core_commands.py +792 -0
- code_puppy/command_line/diff_menu.py +863 -0
- code_puppy/command_line/load_context_completion.py +15 -22
- code_puppy/command_line/mcp/base.py +1 -4
- code_puppy/command_line/mcp/catalog_server_installer.py +175 -0
- code_puppy/command_line/mcp/custom_server_form.py +688 -0
- code_puppy/command_line/mcp/custom_server_installer.py +195 -0
- code_puppy/command_line/mcp/edit_command.py +148 -0
- code_puppy/command_line/mcp/handler.py +9 -4
- code_puppy/command_line/mcp/help_command.py +6 -5
- code_puppy/command_line/mcp/install_command.py +16 -27
- code_puppy/command_line/mcp/install_menu.py +685 -0
- code_puppy/command_line/mcp/list_command.py +3 -3
- code_puppy/command_line/mcp/logs_command.py +174 -65
- code_puppy/command_line/mcp/remove_command.py +2 -2
- code_puppy/command_line/mcp/restart_command.py +12 -4
- code_puppy/command_line/mcp/search_command.py +17 -11
- code_puppy/command_line/mcp/start_all_command.py +22 -13
- code_puppy/command_line/mcp/start_command.py +50 -31
- code_puppy/command_line/mcp/status_command.py +6 -7
- code_puppy/command_line/mcp/stop_all_command.py +11 -8
- code_puppy/command_line/mcp/stop_command.py +11 -10
- code_puppy/command_line/mcp/test_command.py +2 -2
- code_puppy/command_line/mcp/utils.py +1 -1
- code_puppy/command_line/mcp/wizard_utils.py +22 -18
- code_puppy/command_line/mcp_completion.py +174 -0
- code_puppy/command_line/model_picker_completion.py +89 -30
- code_puppy/command_line/model_settings_menu.py +884 -0
- code_puppy/command_line/motd.py +14 -8
- code_puppy/command_line/onboarding_slides.py +179 -0
- code_puppy/command_line/onboarding_wizard.py +340 -0
- code_puppy/command_line/pin_command_completion.py +329 -0
- code_puppy/command_line/prompt_toolkit_completion.py +626 -75
- code_puppy/command_line/session_commands.py +296 -0
- code_puppy/command_line/utils.py +54 -0
- code_puppy/config.py +1181 -51
- code_puppy/error_logging.py +118 -0
- code_puppy/gemini_code_assist.py +385 -0
- code_puppy/gemini_model.py +602 -0
- code_puppy/http_utils.py +220 -104
- code_puppy/keymap.py +128 -0
- code_puppy/main.py +5 -594
- code_puppy/{mcp → mcp_}/__init__.py +17 -0
- code_puppy/{mcp → mcp_}/async_lifecycle.py +35 -4
- code_puppy/{mcp → mcp_}/blocking_startup.py +70 -43
- code_puppy/{mcp → mcp_}/captured_stdio_server.py +2 -2
- code_puppy/{mcp → mcp_}/config_wizard.py +5 -5
- code_puppy/{mcp → mcp_}/dashboard.py +15 -6
- code_puppy/{mcp → mcp_}/examples/retry_example.py +4 -1
- code_puppy/{mcp → mcp_}/managed_server.py +66 -39
- code_puppy/{mcp → mcp_}/manager.py +146 -52
- code_puppy/mcp_/mcp_logs.py +224 -0
- code_puppy/{mcp → mcp_}/registry.py +6 -6
- code_puppy/{mcp → mcp_}/server_registry_catalog.py +25 -8
- code_puppy/messaging/__init__.py +199 -2
- code_puppy/messaging/bus.py +610 -0
- code_puppy/messaging/commands.py +167 -0
- code_puppy/messaging/markdown_patches.py +57 -0
- code_puppy/messaging/message_queue.py +17 -48
- code_puppy/messaging/messages.py +500 -0
- code_puppy/messaging/queue_console.py +1 -24
- code_puppy/messaging/renderers.py +43 -146
- code_puppy/messaging/rich_renderer.py +1027 -0
- code_puppy/messaging/spinner/__init__.py +33 -5
- code_puppy/messaging/spinner/console_spinner.py +92 -52
- code_puppy/messaging/spinner/spinner_base.py +29 -0
- code_puppy/messaging/subagent_console.py +461 -0
- code_puppy/model_factory.py +686 -80
- code_puppy/model_utils.py +167 -0
- code_puppy/models.json +86 -104
- code_puppy/models_dev_api.json +1 -0
- code_puppy/models_dev_parser.py +592 -0
- code_puppy/plugins/__init__.py +164 -10
- code_puppy/plugins/antigravity_oauth/__init__.py +10 -0
- code_puppy/plugins/antigravity_oauth/accounts.py +406 -0
- code_puppy/plugins/antigravity_oauth/antigravity_model.py +704 -0
- code_puppy/plugins/antigravity_oauth/config.py +42 -0
- code_puppy/plugins/antigravity_oauth/constants.py +136 -0
- code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
- code_puppy/plugins/antigravity_oauth/register_callbacks.py +406 -0
- code_puppy/plugins/antigravity_oauth/storage.py +271 -0
- code_puppy/plugins/antigravity_oauth/test_plugin.py +319 -0
- code_puppy/plugins/antigravity_oauth/token.py +167 -0
- code_puppy/plugins/antigravity_oauth/transport.py +767 -0
- code_puppy/plugins/antigravity_oauth/utils.py +169 -0
- code_puppy/plugins/chatgpt_oauth/__init__.py +8 -0
- code_puppy/plugins/chatgpt_oauth/config.py +52 -0
- code_puppy/plugins/chatgpt_oauth/oauth_flow.py +328 -0
- code_puppy/plugins/chatgpt_oauth/register_callbacks.py +94 -0
- code_puppy/plugins/chatgpt_oauth/test_plugin.py +293 -0
- code_puppy/plugins/chatgpt_oauth/utils.py +489 -0
- code_puppy/plugins/claude_code_oauth/README.md +167 -0
- code_puppy/plugins/claude_code_oauth/SETUP.md +93 -0
- code_puppy/plugins/claude_code_oauth/__init__.py +6 -0
- code_puppy/plugins/claude_code_oauth/config.py +50 -0
- code_puppy/plugins/claude_code_oauth/register_callbacks.py +308 -0
- code_puppy/plugins/claude_code_oauth/test_plugin.py +283 -0
- code_puppy/plugins/claude_code_oauth/utils.py +518 -0
- code_puppy/plugins/customizable_commands/__init__.py +0 -0
- code_puppy/plugins/customizable_commands/register_callbacks.py +169 -0
- code_puppy/plugins/example_custom_command/README.md +280 -0
- code_puppy/plugins/example_custom_command/register_callbacks.py +51 -0
- code_puppy/plugins/file_permission_handler/__init__.py +4 -0
- code_puppy/plugins/file_permission_handler/register_callbacks.py +523 -0
- code_puppy/plugins/frontend_emitter/__init__.py +25 -0
- code_puppy/plugins/frontend_emitter/emitter.py +121 -0
- code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
- code_puppy/plugins/oauth_puppy_html.py +228 -0
- code_puppy/plugins/shell_safety/__init__.py +6 -0
- code_puppy/plugins/shell_safety/agent_shell_safety.py +69 -0
- code_puppy/plugins/shell_safety/command_cache.py +156 -0
- code_puppy/plugins/shell_safety/register_callbacks.py +202 -0
- code_puppy/prompts/antigravity_system_prompt.md +1 -0
- code_puppy/prompts/codex_system_prompt.md +310 -0
- code_puppy/pydantic_patches.py +131 -0
- code_puppy/reopenable_async_client.py +8 -8
- code_puppy/round_robin_model.py +10 -15
- code_puppy/session_storage.py +294 -0
- code_puppy/status_display.py +21 -4
- code_puppy/summarization_agent.py +52 -14
- code_puppy/terminal_utils.py +418 -0
- code_puppy/tools/__init__.py +139 -6
- code_puppy/tools/agent_tools.py +548 -49
- code_puppy/tools/browser/__init__.py +37 -0
- code_puppy/tools/browser/browser_control.py +289 -0
- code_puppy/tools/browser/browser_interactions.py +545 -0
- code_puppy/tools/browser/browser_locators.py +640 -0
- code_puppy/tools/browser/browser_manager.py +316 -0
- code_puppy/tools/browser/browser_navigation.py +251 -0
- code_puppy/tools/browser/browser_screenshot.py +179 -0
- code_puppy/tools/browser/browser_scripts.py +462 -0
- code_puppy/tools/browser/browser_workflows.py +221 -0
- code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
- code_puppy/tools/browser/terminal_command_tools.py +521 -0
- code_puppy/tools/browser/terminal_screenshot_tools.py +556 -0
- code_puppy/tools/browser/terminal_tools.py +525 -0
- code_puppy/tools/command_runner.py +941 -153
- code_puppy/tools/common.py +1146 -6
- code_puppy/tools/display.py +84 -0
- code_puppy/tools/file_modifications.py +288 -89
- code_puppy/tools/file_operations.py +352 -266
- code_puppy/tools/subagent_context.py +158 -0
- code_puppy/uvx_detection.py +242 -0
- code_puppy/version_checker.py +30 -11
- code_puppy-0.0.366.data/data/code_puppy/models.json +110 -0
- code_puppy-0.0.366.data/data/code_puppy/models_dev_api.json +1 -0
- {code_puppy-0.0.169.dist-info → code_puppy-0.0.366.dist-info}/METADATA +184 -67
- code_puppy-0.0.366.dist-info/RECORD +217 -0
- {code_puppy-0.0.169.dist-info → code_puppy-0.0.366.dist-info}/WHEEL +1 -1
- {code_puppy-0.0.169.dist-info → code_puppy-0.0.366.dist-info}/entry_points.txt +1 -0
- code_puppy/agent.py +0 -231
- code_puppy/agents/agent_orchestrator.json +0 -26
- code_puppy/agents/runtime_manager.py +0 -272
- code_puppy/command_line/mcp/add_command.py +0 -183
- code_puppy/command_line/meta_command_handler.py +0 -153
- code_puppy/message_history_processor.py +0 -490
- code_puppy/messaging/spinner/textual_spinner.py +0 -101
- code_puppy/state_management.py +0 -200
- code_puppy/tui/__init__.py +0 -10
- code_puppy/tui/app.py +0 -986
- code_puppy/tui/components/__init__.py +0 -21
- code_puppy/tui/components/chat_view.py +0 -550
- code_puppy/tui/components/command_history_modal.py +0 -218
- code_puppy/tui/components/copy_button.py +0 -139
- code_puppy/tui/components/custom_widgets.py +0 -63
- code_puppy/tui/components/human_input_modal.py +0 -175
- code_puppy/tui/components/input_area.py +0 -167
- code_puppy/tui/components/sidebar.py +0 -309
- code_puppy/tui/components/status_bar.py +0 -182
- code_puppy/tui/messages.py +0 -27
- code_puppy/tui/models/__init__.py +0 -8
- code_puppy/tui/models/chat_message.py +0 -25
- code_puppy/tui/models/command_history.py +0 -89
- code_puppy/tui/models/enums.py +0 -24
- code_puppy/tui/screens/__init__.py +0 -15
- code_puppy/tui/screens/help.py +0 -130
- code_puppy/tui/screens/mcp_install_wizard.py +0 -803
- code_puppy/tui/screens/settings.py +0 -290
- code_puppy/tui/screens/tools.py +0 -74
- code_puppy-0.0.169.data/data/code_puppy/models.json +0 -128
- code_puppy-0.0.169.dist-info/RECORD +0 -112
- /code_puppy/{mcp → mcp_}/circuit_breaker.py +0 -0
- /code_puppy/{mcp → mcp_}/error_isolation.py +0 -0
- /code_puppy/{mcp → mcp_}/health_monitor.py +0 -0
- /code_puppy/{mcp → mcp_}/retry_manager.py +0 -0
- /code_puppy/{mcp → mcp_}/status_tracker.py +0 -0
- /code_puppy/{mcp → mcp_}/system_tools.py +0 -0
- {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
|
+
)
|
code_puppy/status_display.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
222
|
-
f"
|
|
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
|
|
8
|
-
|
|
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 =
|
|
90
|
+
model_name = get_global_model_name()
|
|
57
91
|
model = ModelFactory.get_model(model_name, models_config)
|
|
58
92
|
|
|
59
|
-
#
|
|
60
|
-
instructions =
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
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
|
|