code-puppy 0.0.287__py3-none-any.whl → 0.0.323__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 +3 -1
- code_puppy/agents/agent_code_puppy.py +5 -4
- code_puppy/agents/agent_creator_agent.py +22 -18
- code_puppy/agents/agent_manager.py +2 -2
- code_puppy/agents/base_agent.py +496 -102
- code_puppy/callbacks.py +8 -0
- code_puppy/chatgpt_codex_client.py +283 -0
- code_puppy/cli_runner.py +795 -0
- code_puppy/command_line/add_model_menu.py +19 -16
- code_puppy/command_line/attachments.py +10 -5
- code_puppy/command_line/autosave_menu.py +269 -41
- code_puppy/command_line/colors_menu.py +515 -0
- code_puppy/command_line/command_handler.py +10 -24
- code_puppy/command_line/config_commands.py +106 -25
- code_puppy/command_line/core_commands.py +32 -20
- code_puppy/command_line/mcp/add_command.py +3 -16
- code_puppy/command_line/mcp/base.py +0 -3
- code_puppy/command_line/mcp/catalog_server_installer.py +15 -15
- code_puppy/command_line/mcp/custom_server_form.py +66 -5
- code_puppy/command_line/mcp/custom_server_installer.py +17 -17
- code_puppy/command_line/mcp/edit_command.py +15 -22
- code_puppy/command_line/mcp/handler.py +7 -2
- code_puppy/command_line/mcp/help_command.py +2 -2
- code_puppy/command_line/mcp/install_command.py +10 -14
- code_puppy/command_line/mcp/install_menu.py +2 -6
- code_puppy/command_line/mcp/list_command.py +2 -2
- 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 +7 -2
- code_puppy/command_line/mcp/search_command.py +16 -10
- code_puppy/command_line/mcp/start_all_command.py +16 -6
- code_puppy/command_line/mcp/start_command.py +12 -10
- code_puppy/command_line/mcp/status_command.py +4 -5
- code_puppy/command_line/mcp/stop_all_command.py +5 -1
- code_puppy/command_line/mcp/stop_command.py +6 -4
- code_puppy/command_line/mcp/test_command.py +2 -2
- code_puppy/command_line/mcp/wizard_utils.py +20 -16
- code_puppy/command_line/model_settings_menu.py +53 -7
- code_puppy/command_line/motd.py +1 -1
- code_puppy/command_line/pin_command_completion.py +82 -7
- code_puppy/command_line/prompt_toolkit_completion.py +32 -9
- code_puppy/command_line/session_commands.py +11 -4
- code_puppy/config.py +217 -53
- code_puppy/error_logging.py +118 -0
- code_puppy/gemini_code_assist.py +385 -0
- code_puppy/keymap.py +126 -0
- code_puppy/main.py +5 -745
- code_puppy/mcp_/__init__.py +17 -0
- code_puppy/mcp_/blocking_startup.py +63 -36
- code_puppy/mcp_/captured_stdio_server.py +1 -1
- code_puppy/mcp_/config_wizard.py +4 -4
- code_puppy/mcp_/dashboard.py +15 -6
- code_puppy/mcp_/managed_server.py +25 -5
- code_puppy/mcp_/manager.py +65 -0
- code_puppy/mcp_/mcp_logs.py +224 -0
- code_puppy/mcp_/registry.py +6 -6
- code_puppy/messaging/__init__.py +184 -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 +3 -3
- code_puppy/messaging/messages.py +470 -0
- code_puppy/messaging/renderers.py +43 -141
- code_puppy/messaging/rich_renderer.py +900 -0
- code_puppy/messaging/spinner/console_spinner.py +39 -2
- code_puppy/model_factory.py +292 -53
- code_puppy/model_utils.py +57 -48
- code_puppy/models.json +19 -5
- code_puppy/plugins/__init__.py +152 -10
- code_puppy/plugins/chatgpt_oauth/config.py +20 -12
- code_puppy/plugins/chatgpt_oauth/oauth_flow.py +5 -6
- code_puppy/plugins/chatgpt_oauth/register_callbacks.py +3 -3
- code_puppy/plugins/chatgpt_oauth/test_plugin.py +30 -13
- code_puppy/plugins/chatgpt_oauth/utils.py +180 -65
- code_puppy/plugins/claude_code_oauth/config.py +15 -11
- code_puppy/plugins/claude_code_oauth/register_callbacks.py +28 -0
- code_puppy/plugins/claude_code_oauth/utils.py +6 -1
- code_puppy/plugins/example_custom_command/register_callbacks.py +2 -2
- code_puppy/plugins/oauth_puppy_html.py +3 -0
- code_puppy/plugins/shell_safety/agent_shell_safety.py +1 -134
- code_puppy/plugins/shell_safety/command_cache.py +156 -0
- code_puppy/plugins/shell_safety/register_callbacks.py +77 -3
- code_puppy/prompts/codex_system_prompt.md +310 -0
- code_puppy/pydantic_patches.py +131 -0
- code_puppy/session_storage.py +2 -1
- code_puppy/status_display.py +7 -5
- code_puppy/terminal_utils.py +126 -0
- code_puppy/tools/agent_tools.py +131 -70
- code_puppy/tools/browser/browser_control.py +10 -14
- code_puppy/tools/browser/browser_interactions.py +20 -28
- code_puppy/tools/browser/browser_locators.py +27 -29
- code_puppy/tools/browser/browser_navigation.py +9 -9
- code_puppy/tools/browser/browser_screenshot.py +12 -14
- code_puppy/tools/browser/browser_scripts.py +17 -29
- code_puppy/tools/browser/browser_workflows.py +24 -25
- code_puppy/tools/browser/camoufox_manager.py +22 -26
- code_puppy/tools/command_runner.py +410 -88
- code_puppy/tools/common.py +51 -38
- code_puppy/tools/file_modifications.py +98 -24
- code_puppy/tools/file_operations.py +113 -202
- code_puppy/version_checker.py +28 -13
- {code_puppy-0.0.287.data → code_puppy-0.0.323.data}/data/code_puppy/models.json +19 -5
- {code_puppy-0.0.287.dist-info → code_puppy-0.0.323.dist-info}/METADATA +3 -8
- code_puppy-0.0.323.dist-info/RECORD +168 -0
- code_puppy/tui_state.py +0 -55
- code_puppy-0.0.287.dist-info/RECORD +0 -153
- {code_puppy-0.0.287.data → code_puppy-0.0.323.data}/data/code_puppy/models_dev_api.json +0 -0
- {code_puppy-0.0.287.dist-info → code_puppy-0.0.323.dist-info}/WHEEL +0 -0
- {code_puppy-0.0.287.dist-info → code_puppy-0.0.323.dist-info}/entry_points.txt +0 -0
- {code_puppy-0.0.287.dist-info → code_puppy-0.0.323.dist-info}/licenses/LICENSE +0 -0
|
@@ -576,9 +576,16 @@ class AddModelMenu:
|
|
|
576
576
|
# Determine the model type
|
|
577
577
|
model_type = type_mapping.get(provider.id, "custom_openai")
|
|
578
578
|
|
|
579
|
+
# Special case: kimi-for-coding provider uses "kimi-for-coding" as the model name
|
|
580
|
+
# instead of the model_id from models.dev (which is "kimi-k2-thinking")
|
|
581
|
+
if provider.id == "kimi-for-coding":
|
|
582
|
+
model_name = "kimi-for-coding"
|
|
583
|
+
else:
|
|
584
|
+
model_name = model.model_id
|
|
585
|
+
|
|
579
586
|
config: dict = {
|
|
580
587
|
"type": model_type,
|
|
581
|
-
"name":
|
|
588
|
+
"name": model_name,
|
|
582
589
|
}
|
|
583
590
|
|
|
584
591
|
# Add custom endpoint for non-standard providers
|
|
@@ -697,13 +704,13 @@ class AddModelMenu:
|
|
|
697
704
|
)
|
|
698
705
|
return True
|
|
699
706
|
|
|
700
|
-
|
|
707
|
+
emit_info(f"\n🔑 {provider.name} requires the following credentials:\n")
|
|
701
708
|
|
|
702
709
|
for env_var in missing_vars:
|
|
703
710
|
# Show helpful hints based on common env var patterns
|
|
704
711
|
hint = self._get_env_var_hint(env_var)
|
|
705
712
|
if hint:
|
|
706
|
-
|
|
713
|
+
emit_info(f" {hint}")
|
|
707
714
|
|
|
708
715
|
try:
|
|
709
716
|
# Use regular input - simpler and works in threaded context
|
|
@@ -722,7 +729,7 @@ class AddModelMenu:
|
|
|
722
729
|
emit_info(f"✅ Saved {env_var} to config")
|
|
723
730
|
|
|
724
731
|
except (KeyboardInterrupt, EOFError):
|
|
725
|
-
|
|
732
|
+
emit_info("") # Clean newline
|
|
726
733
|
emit_warning("Credential input cancelled")
|
|
727
734
|
return False
|
|
728
735
|
|
|
@@ -760,9 +767,9 @@ class AddModelMenu:
|
|
|
760
767
|
if not provider:
|
|
761
768
|
return None
|
|
762
769
|
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
770
|
+
emit_info(f"\n✨ Adding custom model for {provider.name}\n")
|
|
771
|
+
emit_info(" Enter the model ID exactly as the provider expects it.")
|
|
772
|
+
emit_info(
|
|
766
773
|
" Examples: gpt-4-turbo-preview, claude-3-opus-20240229, gemini-1.5-pro-latest\n"
|
|
767
774
|
)
|
|
768
775
|
|
|
@@ -774,8 +781,8 @@ class AddModelMenu:
|
|
|
774
781
|
return None
|
|
775
782
|
|
|
776
783
|
# Ask for context size
|
|
777
|
-
|
|
778
|
-
|
|
784
|
+
emit_info("\n Enter the context window size (in tokens).")
|
|
785
|
+
emit_info(" Common sizes: 8192, 32768, 128000, 200000, 1000000\n")
|
|
779
786
|
|
|
780
787
|
context_input = input(" Context size [128000]: ").strip()
|
|
781
788
|
|
|
@@ -806,7 +813,7 @@ class AddModelMenu:
|
|
|
806
813
|
return (model_name, context_length)
|
|
807
814
|
|
|
808
815
|
except (KeyboardInterrupt, EOFError):
|
|
809
|
-
|
|
816
|
+
emit_info("") # Clean newline
|
|
810
817
|
emit_warning("Custom model input cancelled")
|
|
811
818
|
return None
|
|
812
819
|
|
|
@@ -840,11 +847,7 @@ class AddModelMenu:
|
|
|
840
847
|
True if a model was added, False otherwise
|
|
841
848
|
"""
|
|
842
849
|
if not self.registry or not self.providers:
|
|
843
|
-
|
|
844
|
-
try:
|
|
845
|
-
print("No models data available.")
|
|
846
|
-
finally:
|
|
847
|
-
set_awaiting_user_input(False)
|
|
850
|
+
emit_warning("No models data available.")
|
|
848
851
|
return False
|
|
849
852
|
|
|
850
853
|
# Build UI
|
|
@@ -1036,7 +1039,7 @@ class AddModelMenu:
|
|
|
1036
1039
|
emit_info("Model addition cancelled.")
|
|
1037
1040
|
return False
|
|
1038
1041
|
except (KeyboardInterrupt, EOFError):
|
|
1039
|
-
|
|
1042
|
+
emit_info("")
|
|
1040
1043
|
return False
|
|
1041
1044
|
|
|
1042
1045
|
# Prompt for any missing credentials
|
|
@@ -261,17 +261,22 @@ def _detect_path_tokens(prompt: str) -> tuple[list[_DetectedPath], list[str]]:
|
|
|
261
261
|
candidate_path_token = stripped_joined
|
|
262
262
|
candidate_placeholder = joined
|
|
263
263
|
consumed_until = end_index
|
|
264
|
+
if len(candidate_path_token) > MAX_PATH_LENGTH:
|
|
265
|
+
continue
|
|
264
266
|
try:
|
|
265
267
|
last_path = _normalise_path(candidate_path_token)
|
|
266
268
|
except AttachmentParsingError:
|
|
267
269
|
# Suppress warnings for non-file spans; just skip quietly
|
|
268
270
|
found_span = False
|
|
269
271
|
break
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
272
|
+
try:
|
|
273
|
+
if last_path.exists() and last_path.is_file():
|
|
274
|
+
path = last_path
|
|
275
|
+
found_span = True
|
|
276
|
+
# We'll rebuild escaped placeholder after this block
|
|
277
|
+
break
|
|
278
|
+
except OSError:
|
|
279
|
+
continue
|
|
275
280
|
if not found_span:
|
|
276
281
|
# Quietly skip tokens that are not files
|
|
277
282
|
index += 1
|
|
@@ -5,7 +5,6 @@ autosave sessions with live preview of message content.
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import json
|
|
8
|
-
import re
|
|
9
8
|
import sys
|
|
10
9
|
import time
|
|
11
10
|
from datetime import datetime
|
|
@@ -79,8 +78,77 @@ def _extract_last_user_message(history: list) -> str:
|
|
|
79
78
|
return "[No messages found]"
|
|
80
79
|
|
|
81
80
|
|
|
81
|
+
def _extract_message_content(msg) -> Tuple[str, str]:
|
|
82
|
+
"""Extract role and content from a message.
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
Tuple of (role, content) where role is 'user', 'assistant', or 'tool'
|
|
86
|
+
"""
|
|
87
|
+
# Determine role based on message kind AND part types
|
|
88
|
+
# tool-return comes in a 'request' message but it's not from the user
|
|
89
|
+
part_kinds = [getattr(p, "part_kind", "unknown") for p in msg.parts]
|
|
90
|
+
|
|
91
|
+
if msg.kind == "request":
|
|
92
|
+
# Check if this is a tool return (not actually user input)
|
|
93
|
+
if all(pk == "tool-return" for pk in part_kinds):
|
|
94
|
+
role = "tool"
|
|
95
|
+
else:
|
|
96
|
+
role = "user"
|
|
97
|
+
else:
|
|
98
|
+
# Response from assistant
|
|
99
|
+
if all(pk == "tool-call" for pk in part_kinds):
|
|
100
|
+
role = "tool" # Pure tool call, label as tool activity
|
|
101
|
+
else:
|
|
102
|
+
role = "assistant"
|
|
103
|
+
|
|
104
|
+
# Extract content from parts, handling different part types
|
|
105
|
+
content_parts = []
|
|
106
|
+
for part in msg.parts:
|
|
107
|
+
part_kind = getattr(part, "part_kind", "unknown")
|
|
108
|
+
|
|
109
|
+
if part_kind == "tool-call":
|
|
110
|
+
# Assistant is calling a tool - show tool name and args preview
|
|
111
|
+
tool_name = getattr(part, "tool_name", "unknown")
|
|
112
|
+
args = getattr(part, "args", {})
|
|
113
|
+
# Create a condensed args preview
|
|
114
|
+
if args:
|
|
115
|
+
args_preview = str(args)[:100]
|
|
116
|
+
if len(str(args)) > 100:
|
|
117
|
+
args_preview += "..."
|
|
118
|
+
content_parts.append(
|
|
119
|
+
f"🔧 Tool Call: {tool_name}\n Args: {args_preview}"
|
|
120
|
+
)
|
|
121
|
+
else:
|
|
122
|
+
content_parts.append(f"🔧 Tool Call: {tool_name}")
|
|
123
|
+
|
|
124
|
+
elif part_kind == "tool-return":
|
|
125
|
+
# Tool result being returned - show tool name and truncated result
|
|
126
|
+
tool_name = getattr(part, "tool_name", "unknown")
|
|
127
|
+
result = getattr(part, "content", "")
|
|
128
|
+
if isinstance(result, str) and result.strip():
|
|
129
|
+
# Truncate long results
|
|
130
|
+
preview = result[:200].replace("\n", " ")
|
|
131
|
+
if len(result) > 200:
|
|
132
|
+
preview += "..."
|
|
133
|
+
content_parts.append(f"📥 Tool Result: {tool_name}\n {preview}")
|
|
134
|
+
else:
|
|
135
|
+
content_parts.append(f"📥 Tool Result: {tool_name}")
|
|
136
|
+
|
|
137
|
+
elif hasattr(part, "content"):
|
|
138
|
+
# Regular text content (user-prompt, text, thinking, etc.)
|
|
139
|
+
content = part.content
|
|
140
|
+
if isinstance(content, str) and content.strip():
|
|
141
|
+
content_parts.append(content)
|
|
142
|
+
|
|
143
|
+
content = "\n\n".join(content_parts) if content_parts else "[No content]"
|
|
144
|
+
return role, content
|
|
145
|
+
|
|
146
|
+
|
|
82
147
|
def _render_menu_panel(
|
|
83
|
-
entries: List[Tuple[str, dict]],
|
|
148
|
+
entries: List[Tuple[str, dict]],
|
|
149
|
+
page: int,
|
|
150
|
+
selected_idx: int,
|
|
151
|
+
browse_mode: bool = False,
|
|
84
152
|
) -> List:
|
|
85
153
|
"""Render the left menu panel with pagination."""
|
|
86
154
|
lines = []
|
|
@@ -130,12 +198,20 @@ def _render_menu_panel(
|
|
|
130
198
|
|
|
131
199
|
lines.append(("", "\n"))
|
|
132
200
|
|
|
133
|
-
# Navigation hints
|
|
201
|
+
# Navigation hints - change based on browse mode
|
|
134
202
|
lines.append(("", "\n"))
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
203
|
+
if browse_mode:
|
|
204
|
+
lines.append(("fg:ansicyan", " ↑/↓ "))
|
|
205
|
+
lines.append(("", "Browse msgs\n"))
|
|
206
|
+
lines.append(("fg:ansiyellow", " Esc "))
|
|
207
|
+
lines.append(("", "Exit browser\n"))
|
|
208
|
+
else:
|
|
209
|
+
lines.append(("fg:ansibrightblack", " ↑/↓ "))
|
|
210
|
+
lines.append(("", "Navigate\n"))
|
|
211
|
+
lines.append(("fg:ansibrightblack", " ←/→ "))
|
|
212
|
+
lines.append(("", "Page\n"))
|
|
213
|
+
lines.append(("fg:ansicyan", " e "))
|
|
214
|
+
lines.append(("", "Browse msgs\n"))
|
|
139
215
|
lines.append(("fg:green", " Enter "))
|
|
140
216
|
lines.append(("", "Load\n"))
|
|
141
217
|
lines.append(("fg:ansibrightred", " Ctrl+C "))
|
|
@@ -144,6 +220,109 @@ def _render_menu_panel(
|
|
|
144
220
|
return lines
|
|
145
221
|
|
|
146
222
|
|
|
223
|
+
def _render_message_browser_panel(
|
|
224
|
+
history: list,
|
|
225
|
+
message_idx: int,
|
|
226
|
+
session_name: str,
|
|
227
|
+
) -> List:
|
|
228
|
+
"""Render the message browser panel showing a single message.
|
|
229
|
+
|
|
230
|
+
Args:
|
|
231
|
+
history: Full message history list
|
|
232
|
+
message_idx: Index into history (0 = most recent)
|
|
233
|
+
session_name: Name of the session being browsed
|
|
234
|
+
"""
|
|
235
|
+
lines = []
|
|
236
|
+
|
|
237
|
+
lines.append(("fg:ansicyan bold", " MESSAGE BROWSER"))
|
|
238
|
+
lines.append(("", "\n\n"))
|
|
239
|
+
|
|
240
|
+
total_messages = len(history)
|
|
241
|
+
if total_messages == 0:
|
|
242
|
+
lines.append(("fg:yellow", " No messages in this session."))
|
|
243
|
+
lines.append(("", "\n"))
|
|
244
|
+
return lines
|
|
245
|
+
|
|
246
|
+
# Clamp index to valid range
|
|
247
|
+
message_idx = max(0, min(message_idx, total_messages - 1))
|
|
248
|
+
|
|
249
|
+
# Get message (reverse index so 0 = most recent)
|
|
250
|
+
actual_idx = total_messages - 1 - message_idx
|
|
251
|
+
msg = history[actual_idx]
|
|
252
|
+
|
|
253
|
+
# Extract role and content
|
|
254
|
+
role, content = _extract_message_content(msg)
|
|
255
|
+
|
|
256
|
+
# Session info
|
|
257
|
+
lines.append(("fg:ansibrightblack", f" Session: {session_name}"))
|
|
258
|
+
lines.append(("", "\n"))
|
|
259
|
+
|
|
260
|
+
# Message position indicator
|
|
261
|
+
display_num = message_idx + 1 # 1-based for display
|
|
262
|
+
lines.append(("bold", f" Message {display_num} of {total_messages}"))
|
|
263
|
+
lines.append(("", "\n\n"))
|
|
264
|
+
|
|
265
|
+
# Role indicator with icon and color
|
|
266
|
+
if role == "user":
|
|
267
|
+
lines.append(("fg:ansicyan bold", " 🧑 USER"))
|
|
268
|
+
elif role == "tool":
|
|
269
|
+
lines.append(("fg:ansiyellow bold", " 🔧 TOOL"))
|
|
270
|
+
else:
|
|
271
|
+
lines.append(("fg:ansigreen bold", " 🤖 ASSISTANT"))
|
|
272
|
+
lines.append(("", "\n"))
|
|
273
|
+
|
|
274
|
+
# Separator line
|
|
275
|
+
lines.append(("fg:ansibrightblack", " " + "─" * 40))
|
|
276
|
+
lines.append(("", "\n"))
|
|
277
|
+
|
|
278
|
+
# Render content - use markdown for user/assistant, plain text for tool
|
|
279
|
+
try:
|
|
280
|
+
if role == "tool":
|
|
281
|
+
# Tool messages are already formatted, don't pass through markdown
|
|
282
|
+
# Use yellow color for tool output
|
|
283
|
+
rendered = content
|
|
284
|
+
text_color = "fg:ansiyellow"
|
|
285
|
+
else:
|
|
286
|
+
# User and assistant messages should be rendered as markdown
|
|
287
|
+
# Rich will handle the styling via ANSI codes
|
|
288
|
+
console = Console(
|
|
289
|
+
file=StringIO(),
|
|
290
|
+
legacy_windows=False,
|
|
291
|
+
no_color=False,
|
|
292
|
+
force_terminal=False,
|
|
293
|
+
width=72,
|
|
294
|
+
)
|
|
295
|
+
md = Markdown(content)
|
|
296
|
+
console.print(md)
|
|
297
|
+
rendered = console.file.getvalue()
|
|
298
|
+
# Don't override Rich's ANSI styling - use empty style
|
|
299
|
+
text_color = ""
|
|
300
|
+
|
|
301
|
+
# Truncate if too long (max 35 lines)
|
|
302
|
+
message_lines = rendered.split("\n")[:35]
|
|
303
|
+
is_truncated = len(rendered.split("\n")) > 35
|
|
304
|
+
|
|
305
|
+
for line in message_lines:
|
|
306
|
+
lines.append((text_color, f" {line}"))
|
|
307
|
+
lines.append(("", "\n"))
|
|
308
|
+
|
|
309
|
+
if is_truncated:
|
|
310
|
+
lines.append(("", "\n"))
|
|
311
|
+
lines.append(("fg:yellow", " ... truncated (message too long)"))
|
|
312
|
+
lines.append(("", "\n"))
|
|
313
|
+
|
|
314
|
+
except Exception as e:
|
|
315
|
+
lines.append(("fg:red", f" Error rendering message: {e}"))
|
|
316
|
+
lines.append(("", "\n"))
|
|
317
|
+
|
|
318
|
+
# Navigation hint at bottom
|
|
319
|
+
lines.append(("", "\n"))
|
|
320
|
+
lines.append(("fg:ansibrightblack", " ↑ older ↓ newer Esc exit"))
|
|
321
|
+
lines.append(("", "\n"))
|
|
322
|
+
|
|
323
|
+
return lines
|
|
324
|
+
|
|
325
|
+
|
|
147
326
|
def _render_preview_panel(base_dir: Path, entry: Optional[Tuple[str, dict]]) -> List:
|
|
148
327
|
"""Render the right preview panel with message content using rich markdown."""
|
|
149
328
|
lines = []
|
|
@@ -180,6 +359,7 @@ def _render_preview_panel(base_dir: Path, entry: Optional[Tuple[str, dict]]) ->
|
|
|
180
359
|
lines.append(("", "\n\n"))
|
|
181
360
|
|
|
182
361
|
lines.append(("bold", " Last Message:"))
|
|
362
|
+
lines.append(("fg:ansibrightblack", " (press 'e' to browse all)"))
|
|
183
363
|
lines.append(("", "\n"))
|
|
184
364
|
|
|
185
365
|
# Try to load and preview the last message
|
|
@@ -207,22 +387,8 @@ def _render_preview_panel(base_dir: Path, entry: Optional[Tuple[str, dict]]) ->
|
|
|
207
387
|
message_lines = rendered.split("\n")[:30]
|
|
208
388
|
|
|
209
389
|
for line in message_lines:
|
|
210
|
-
#
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
# Headers - make cyan and bold (dimmed)
|
|
214
|
-
if line.strip().startswith("#"):
|
|
215
|
-
lines.append(("fg:cyan", f" {styled_line}"))
|
|
216
|
-
# Code blocks - make them green (dimmed)
|
|
217
|
-
elif line.strip().startswith("│"):
|
|
218
|
-
lines.append(("fg:ansibrightblack", f" {styled_line}"))
|
|
219
|
-
# List items - make them dimmed
|
|
220
|
-
elif re.match(r"^\s*[•\-\*]", line):
|
|
221
|
-
lines.append(("fg:ansibrightblack", f" {styled_line}"))
|
|
222
|
-
# Regular text - dimmed
|
|
223
|
-
else:
|
|
224
|
-
lines.append(("fg:ansibrightblack", f" {styled_line}"))
|
|
225
|
-
|
|
390
|
+
# Rich already rendered the markdown, just display it dimmed
|
|
391
|
+
lines.append(("fg:ansibrightblack", f" {line}"))
|
|
226
392
|
lines.append(("", "\n"))
|
|
227
393
|
|
|
228
394
|
if is_long:
|
|
@@ -247,12 +413,9 @@ async def interactive_autosave_picker() -> Optional[str]:
|
|
|
247
413
|
entries = _get_session_entries(base_dir)
|
|
248
414
|
|
|
249
415
|
if not entries:
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
print("No autosave sessions found.")
|
|
254
|
-
finally:
|
|
255
|
-
set_awaiting_user_input(False)
|
|
416
|
+
from code_puppy.messaging import emit_info
|
|
417
|
+
|
|
418
|
+
emit_info("No autosave sessions found.")
|
|
256
419
|
return None
|
|
257
420
|
|
|
258
421
|
# State
|
|
@@ -260,6 +423,11 @@ async def interactive_autosave_picker() -> Optional[str]:
|
|
|
260
423
|
current_page = [0] # Current page
|
|
261
424
|
result = [None] # Selected session name
|
|
262
425
|
|
|
426
|
+
# Browse mode state
|
|
427
|
+
browse_mode = [False] # Are we browsing messages within a session?
|
|
428
|
+
message_idx = [0] # Current message index (0 = most recent)
|
|
429
|
+
cached_history = [None] # Cached history for current session in browse mode
|
|
430
|
+
|
|
263
431
|
total_pages = (len(entries) + PAGE_SIZE - 1) // PAGE_SIZE
|
|
264
432
|
|
|
265
433
|
def get_current_entry() -> Optional[Tuple[str, dict]]:
|
|
@@ -274,9 +442,17 @@ async def interactive_autosave_picker() -> Optional[str]:
|
|
|
274
442
|
def update_display():
|
|
275
443
|
"""Update both panels."""
|
|
276
444
|
menu_control.text = _render_menu_panel(
|
|
277
|
-
entries, current_page[0], selected_idx[0]
|
|
445
|
+
entries, current_page[0], selected_idx[0], browse_mode[0]
|
|
278
446
|
)
|
|
279
|
-
|
|
447
|
+
# Show message browser if in browse mode, otherwise show preview
|
|
448
|
+
if browse_mode[0] and cached_history[0] is not None:
|
|
449
|
+
entry = get_current_entry()
|
|
450
|
+
session_name = entry[0] if entry else "unknown"
|
|
451
|
+
preview_control.text = _render_message_browser_panel(
|
|
452
|
+
cached_history[0], message_idx[0], session_name
|
|
453
|
+
)
|
|
454
|
+
else:
|
|
455
|
+
preview_control.text = _render_preview_panel(base_dir, get_current_entry())
|
|
280
456
|
|
|
281
457
|
menu_window = Window(
|
|
282
458
|
content=menu_control, wrap_lines=True, width=Dimension(weight=30)
|
|
@@ -301,19 +477,33 @@ async def interactive_autosave_picker() -> Optional[str]:
|
|
|
301
477
|
|
|
302
478
|
@kb.add("up")
|
|
303
479
|
def _(event):
|
|
304
|
-
if
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
480
|
+
if browse_mode[0]:
|
|
481
|
+
# In browse mode: go to older message
|
|
482
|
+
if cached_history[0] and message_idx[0] < len(cached_history[0]) - 1:
|
|
483
|
+
message_idx[0] += 1
|
|
484
|
+
update_display()
|
|
485
|
+
else:
|
|
486
|
+
# Normal mode: navigate sessions
|
|
487
|
+
if selected_idx[0] > 0:
|
|
488
|
+
selected_idx[0] -= 1
|
|
489
|
+
# Update page if needed
|
|
490
|
+
current_page[0] = selected_idx[0] // PAGE_SIZE
|
|
491
|
+
update_display()
|
|
309
492
|
|
|
310
493
|
@kb.add("down")
|
|
311
494
|
def _(event):
|
|
312
|
-
if
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
495
|
+
if browse_mode[0]:
|
|
496
|
+
# In browse mode: go to newer message
|
|
497
|
+
if message_idx[0] > 0:
|
|
498
|
+
message_idx[0] -= 1
|
|
499
|
+
update_display()
|
|
500
|
+
else:
|
|
501
|
+
# Normal mode: navigate sessions
|
|
502
|
+
if selected_idx[0] < len(entries) - 1:
|
|
503
|
+
selected_idx[0] += 1
|
|
504
|
+
# Update page if needed
|
|
505
|
+
current_page[0] = selected_idx[0] // PAGE_SIZE
|
|
506
|
+
update_display()
|
|
317
507
|
|
|
318
508
|
@kb.add("left")
|
|
319
509
|
def _(event):
|
|
@@ -329,6 +519,44 @@ async def interactive_autosave_picker() -> Optional[str]:
|
|
|
329
519
|
selected_idx[0] = current_page[0] * PAGE_SIZE
|
|
330
520
|
update_display()
|
|
331
521
|
|
|
522
|
+
@kb.add("e")
|
|
523
|
+
def _(event):
|
|
524
|
+
"""Enter message browse mode."""
|
|
525
|
+
if browse_mode[0]:
|
|
526
|
+
return # Already in browse mode
|
|
527
|
+
entry = get_current_entry()
|
|
528
|
+
if entry:
|
|
529
|
+
session_name = entry[0]
|
|
530
|
+
try:
|
|
531
|
+
cached_history[0] = load_session(session_name, base_dir)
|
|
532
|
+
browse_mode[0] = True
|
|
533
|
+
message_idx[0] = 0 # Start at most recent
|
|
534
|
+
update_display()
|
|
535
|
+
except Exception:
|
|
536
|
+
pass # Silently fail if can't load
|
|
537
|
+
|
|
538
|
+
@kb.add("escape")
|
|
539
|
+
def _(event):
|
|
540
|
+
"""Exit browse mode or cancel."""
|
|
541
|
+
if browse_mode[0]:
|
|
542
|
+
browse_mode[0] = False
|
|
543
|
+
cached_history[0] = None
|
|
544
|
+
message_idx[0] = 0
|
|
545
|
+
update_display()
|
|
546
|
+
else:
|
|
547
|
+
# Not in browse mode - treat as cancel
|
|
548
|
+
result[0] = None
|
|
549
|
+
event.app.exit()
|
|
550
|
+
|
|
551
|
+
@kb.add("q")
|
|
552
|
+
def _(event):
|
|
553
|
+
"""Exit browse mode (only when in browse mode)."""
|
|
554
|
+
if browse_mode[0]:
|
|
555
|
+
browse_mode[0] = False
|
|
556
|
+
cached_history[0] = None
|
|
557
|
+
message_idx[0] = 0
|
|
558
|
+
update_display()
|
|
559
|
+
|
|
332
560
|
@kb.add("enter")
|
|
333
561
|
def _(event):
|
|
334
562
|
entry = get_current_entry()
|