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,605 @@
|
|
|
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 json
|
|
8
|
+
import sys
|
|
9
|
+
import time
|
|
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
|
+
async def interactive_autosave_picker() -> Optional[str]:
|
|
401
|
+
"""Show interactive terminal UI to select an autosave session.
|
|
402
|
+
|
|
403
|
+
Returns:
|
|
404
|
+
Session name to load, or None if cancelled
|
|
405
|
+
"""
|
|
406
|
+
base_dir = Path(AUTOSAVE_DIR)
|
|
407
|
+
entries = _get_session_entries(base_dir)
|
|
408
|
+
|
|
409
|
+
if not entries:
|
|
410
|
+
from code_puppy.messaging import emit_info
|
|
411
|
+
|
|
412
|
+
emit_info("No autosave sessions found.")
|
|
413
|
+
return None
|
|
414
|
+
|
|
415
|
+
# State
|
|
416
|
+
selected_idx = [0] # Current selection (global index)
|
|
417
|
+
current_page = [0] # Current page
|
|
418
|
+
result = [None] # Selected session name
|
|
419
|
+
|
|
420
|
+
# Browse mode state
|
|
421
|
+
browse_mode = [False] # Are we browsing messages within a session?
|
|
422
|
+
message_idx = [0] # Current message index (0 = most recent)
|
|
423
|
+
cached_history = [None] # Cached history for current session in browse mode
|
|
424
|
+
|
|
425
|
+
total_pages = (len(entries) + PAGE_SIZE - 1) // PAGE_SIZE
|
|
426
|
+
|
|
427
|
+
def get_current_entry() -> Optional[Tuple[str, dict]]:
|
|
428
|
+
if 0 <= selected_idx[0] < len(entries):
|
|
429
|
+
return entries[selected_idx[0]]
|
|
430
|
+
return None
|
|
431
|
+
|
|
432
|
+
# Build UI
|
|
433
|
+
menu_control = FormattedTextControl(text="")
|
|
434
|
+
preview_control = FormattedTextControl(text="")
|
|
435
|
+
|
|
436
|
+
def update_display():
|
|
437
|
+
"""Update both panels."""
|
|
438
|
+
menu_control.text = _render_menu_panel(
|
|
439
|
+
entries, current_page[0], selected_idx[0], browse_mode[0]
|
|
440
|
+
)
|
|
441
|
+
# Show message browser if in browse mode, otherwise show preview
|
|
442
|
+
if browse_mode[0] and cached_history[0] is not None:
|
|
443
|
+
entry = get_current_entry()
|
|
444
|
+
session_name = entry[0] if entry else "unknown"
|
|
445
|
+
preview_control.text = _render_message_browser_panel(
|
|
446
|
+
cached_history[0], message_idx[0], session_name
|
|
447
|
+
)
|
|
448
|
+
else:
|
|
449
|
+
preview_control.text = _render_preview_panel(base_dir, get_current_entry())
|
|
450
|
+
|
|
451
|
+
menu_window = Window(
|
|
452
|
+
content=menu_control, wrap_lines=True, width=Dimension(weight=30)
|
|
453
|
+
)
|
|
454
|
+
preview_window = Window(
|
|
455
|
+
content=preview_control, wrap_lines=True, width=Dimension(weight=70)
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
menu_frame = Frame(menu_window, width=Dimension(weight=30), title="Sessions")
|
|
459
|
+
preview_frame = Frame(preview_window, width=Dimension(weight=70), title="Preview")
|
|
460
|
+
|
|
461
|
+
# Make left panel narrower (15% vs 85%)
|
|
462
|
+
root_container = VSplit(
|
|
463
|
+
[
|
|
464
|
+
menu_frame,
|
|
465
|
+
preview_frame,
|
|
466
|
+
]
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
# Key bindings
|
|
470
|
+
kb = KeyBindings()
|
|
471
|
+
|
|
472
|
+
@kb.add("up")
|
|
473
|
+
def _(event):
|
|
474
|
+
if browse_mode[0]:
|
|
475
|
+
# In browse mode: go to older message
|
|
476
|
+
if cached_history[0] and message_idx[0] < len(cached_history[0]) - 1:
|
|
477
|
+
message_idx[0] += 1
|
|
478
|
+
update_display()
|
|
479
|
+
else:
|
|
480
|
+
# Normal mode: navigate sessions
|
|
481
|
+
if selected_idx[0] > 0:
|
|
482
|
+
selected_idx[0] -= 1
|
|
483
|
+
# Update page if needed
|
|
484
|
+
current_page[0] = selected_idx[0] // PAGE_SIZE
|
|
485
|
+
update_display()
|
|
486
|
+
|
|
487
|
+
@kb.add("down")
|
|
488
|
+
def _(event):
|
|
489
|
+
if browse_mode[0]:
|
|
490
|
+
# In browse mode: go to newer message
|
|
491
|
+
if message_idx[0] > 0:
|
|
492
|
+
message_idx[0] -= 1
|
|
493
|
+
update_display()
|
|
494
|
+
else:
|
|
495
|
+
# Normal mode: navigate sessions
|
|
496
|
+
if selected_idx[0] < len(entries) - 1:
|
|
497
|
+
selected_idx[0] += 1
|
|
498
|
+
# Update page if needed
|
|
499
|
+
current_page[0] = selected_idx[0] // PAGE_SIZE
|
|
500
|
+
update_display()
|
|
501
|
+
|
|
502
|
+
@kb.add("left")
|
|
503
|
+
def _(event):
|
|
504
|
+
if current_page[0] > 0:
|
|
505
|
+
current_page[0] -= 1
|
|
506
|
+
selected_idx[0] = current_page[0] * PAGE_SIZE
|
|
507
|
+
update_display()
|
|
508
|
+
|
|
509
|
+
@kb.add("right")
|
|
510
|
+
def _(event):
|
|
511
|
+
if current_page[0] < total_pages - 1:
|
|
512
|
+
current_page[0] += 1
|
|
513
|
+
selected_idx[0] = current_page[0] * PAGE_SIZE
|
|
514
|
+
update_display()
|
|
515
|
+
|
|
516
|
+
@kb.add("e")
|
|
517
|
+
def _(event):
|
|
518
|
+
"""Enter message browse mode."""
|
|
519
|
+
if browse_mode[0]:
|
|
520
|
+
return # Already in browse mode
|
|
521
|
+
entry = get_current_entry()
|
|
522
|
+
if entry:
|
|
523
|
+
session_name = entry[0]
|
|
524
|
+
try:
|
|
525
|
+
cached_history[0] = load_session(session_name, base_dir)
|
|
526
|
+
browse_mode[0] = True
|
|
527
|
+
message_idx[0] = 0 # Start at most recent
|
|
528
|
+
update_display()
|
|
529
|
+
except Exception:
|
|
530
|
+
pass # Silently fail if can't load
|
|
531
|
+
|
|
532
|
+
@kb.add("escape")
|
|
533
|
+
def _(event):
|
|
534
|
+
"""Exit browse mode or cancel."""
|
|
535
|
+
if browse_mode[0]:
|
|
536
|
+
browse_mode[0] = False
|
|
537
|
+
cached_history[0] = None
|
|
538
|
+
message_idx[0] = 0
|
|
539
|
+
update_display()
|
|
540
|
+
else:
|
|
541
|
+
# Not in browse mode - treat as cancel
|
|
542
|
+
result[0] = None
|
|
543
|
+
event.app.exit()
|
|
544
|
+
|
|
545
|
+
@kb.add("q")
|
|
546
|
+
def _(event):
|
|
547
|
+
"""Exit browse mode (only when in browse mode)."""
|
|
548
|
+
if browse_mode[0]:
|
|
549
|
+
browse_mode[0] = False
|
|
550
|
+
cached_history[0] = None
|
|
551
|
+
message_idx[0] = 0
|
|
552
|
+
update_display()
|
|
553
|
+
|
|
554
|
+
@kb.add("enter")
|
|
555
|
+
def _(event):
|
|
556
|
+
entry = get_current_entry()
|
|
557
|
+
if entry:
|
|
558
|
+
result[0] = entry[0] # Store session name
|
|
559
|
+
event.app.exit()
|
|
560
|
+
|
|
561
|
+
@kb.add("c-c")
|
|
562
|
+
def _(event):
|
|
563
|
+
result[0] = None
|
|
564
|
+
event.app.exit()
|
|
565
|
+
|
|
566
|
+
layout = Layout(root_container)
|
|
567
|
+
app = Application(
|
|
568
|
+
layout=layout,
|
|
569
|
+
key_bindings=kb,
|
|
570
|
+
full_screen=False,
|
|
571
|
+
mouse_support=False,
|
|
572
|
+
)
|
|
573
|
+
|
|
574
|
+
set_awaiting_user_input(True)
|
|
575
|
+
|
|
576
|
+
# Enter alternate screen buffer once for entire session
|
|
577
|
+
sys.stdout.write("\033[?1049h") # Enter alternate buffer
|
|
578
|
+
sys.stdout.write("\033[2J\033[H") # Clear and home
|
|
579
|
+
sys.stdout.flush()
|
|
580
|
+
time.sleep(0.05)
|
|
581
|
+
|
|
582
|
+
try:
|
|
583
|
+
# Initial display
|
|
584
|
+
update_display()
|
|
585
|
+
|
|
586
|
+
# Just clear the current buffer (don't switch buffers)
|
|
587
|
+
sys.stdout.write("\033[2J\033[H") # Clear screen within current buffer
|
|
588
|
+
sys.stdout.flush()
|
|
589
|
+
|
|
590
|
+
# Run application (stays in same alternate buffer)
|
|
591
|
+
await app.run_async()
|
|
592
|
+
|
|
593
|
+
finally:
|
|
594
|
+
# Exit alternate screen buffer once at end
|
|
595
|
+
sys.stdout.write("\033[?1049l") # Exit alternate buffer
|
|
596
|
+
sys.stdout.flush()
|
|
597
|
+
# Reset awaiting input flag
|
|
598
|
+
set_awaiting_user_input(False)
|
|
599
|
+
|
|
600
|
+
# Clear exit message
|
|
601
|
+
from code_puppy.messaging import emit_info
|
|
602
|
+
|
|
603
|
+
emit_info("✓ Exited session browser")
|
|
604
|
+
|
|
605
|
+
return result[0]
|