code-puppy 0.0.302__py3-none-any.whl → 0.0.335__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/agents/base_agent.py +343 -35
- code_puppy/chatgpt_codex_client.py +283 -0
- code_puppy/cli_runner.py +898 -0
- code_puppy/command_line/add_model_menu.py +23 -1
- code_puppy/command_line/autosave_menu.py +271 -35
- code_puppy/command_line/colors_menu.py +520 -0
- code_puppy/command_line/command_handler.py +8 -2
- code_puppy/command_line/config_commands.py +82 -10
- code_puppy/command_line/core_commands.py +70 -7
- code_puppy/command_line/diff_menu.py +5 -0
- code_puppy/command_line/mcp/custom_server_form.py +4 -0
- code_puppy/command_line/mcp/edit_command.py +3 -1
- code_puppy/command_line/mcp/handler.py +7 -2
- code_puppy/command_line/mcp/install_command.py +8 -3
- code_puppy/command_line/mcp/install_menu.py +5 -1
- code_puppy/command_line/mcp/logs_command.py +173 -64
- code_puppy/command_line/mcp/restart_command.py +7 -2
- code_puppy/command_line/mcp/search_command.py +10 -4
- code_puppy/command_line/mcp/start_all_command.py +16 -6
- code_puppy/command_line/mcp/start_command.py +3 -1
- code_puppy/command_line/mcp/status_command.py +2 -1
- code_puppy/command_line/mcp/stop_all_command.py +5 -1
- code_puppy/command_line/mcp/stop_command.py +3 -1
- code_puppy/command_line/mcp/wizard_utils.py +10 -4
- code_puppy/command_line/model_settings_menu.py +58 -7
- code_puppy/command_line/motd.py +13 -7
- code_puppy/command_line/onboarding_slides.py +180 -0
- code_puppy/command_line/onboarding_wizard.py +340 -0
- code_puppy/command_line/prompt_toolkit_completion.py +16 -2
- code_puppy/command_line/session_commands.py +11 -4
- code_puppy/config.py +106 -17
- code_puppy/http_utils.py +155 -196
- code_puppy/keymap.py +8 -0
- code_puppy/main.py +5 -828
- code_puppy/mcp_/__init__.py +17 -0
- code_puppy/mcp_/blocking_startup.py +61 -32
- code_puppy/mcp_/config_wizard.py +5 -1
- code_puppy/mcp_/managed_server.py +23 -3
- code_puppy/mcp_/manager.py +65 -0
- code_puppy/mcp_/mcp_logs.py +224 -0
- code_puppy/messaging/__init__.py +20 -4
- code_puppy/messaging/bus.py +64 -0
- code_puppy/messaging/markdown_patches.py +57 -0
- code_puppy/messaging/messages.py +16 -0
- code_puppy/messaging/renderers.py +21 -9
- code_puppy/messaging/rich_renderer.py +113 -67
- code_puppy/messaging/spinner/console_spinner.py +34 -0
- code_puppy/model_factory.py +271 -45
- code_puppy/model_utils.py +57 -48
- code_puppy/models.json +21 -7
- code_puppy/plugins/__init__.py +12 -0
- 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 +612 -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 +595 -0
- code_puppy/plugins/antigravity_oauth/utils.py +169 -0
- code_puppy/plugins/chatgpt_oauth/config.py +5 -1
- code_puppy/plugins/chatgpt_oauth/oauth_flow.py +5 -6
- code_puppy/plugins/chatgpt_oauth/register_callbacks.py +5 -3
- code_puppy/plugins/chatgpt_oauth/test_plugin.py +26 -11
- code_puppy/plugins/chatgpt_oauth/utils.py +180 -65
- code_puppy/plugins/claude_code_oauth/register_callbacks.py +30 -0
- code_puppy/plugins/claude_code_oauth/utils.py +1 -0
- code_puppy/plugins/shell_safety/agent_shell_safety.py +1 -118
- code_puppy/plugins/shell_safety/register_callbacks.py +44 -3
- 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/terminal_utils.py +291 -0
- code_puppy/tools/agent_tools.py +34 -9
- code_puppy/tools/command_runner.py +344 -27
- code_puppy/tools/file_operations.py +33 -45
- code_puppy/uvx_detection.py +242 -0
- {code_puppy-0.0.302.data → code_puppy-0.0.335.data}/data/code_puppy/models.json +21 -7
- {code_puppy-0.0.302.dist-info → code_puppy-0.0.335.dist-info}/METADATA +30 -1
- {code_puppy-0.0.302.dist-info → code_puppy-0.0.335.dist-info}/RECORD +87 -64
- {code_puppy-0.0.302.data → code_puppy-0.0.335.data}/data/code_puppy/models_dev_api.json +0 -0
- {code_puppy-0.0.302.dist-info → code_puppy-0.0.335.dist-info}/WHEEL +0 -0
- {code_puppy-0.0.302.dist-info → code_puppy-0.0.335.dist-info}/entry_points.txt +0 -0
- {code_puppy-0.0.302.dist-info → code_puppy-0.0.335.dist-info}/licenses/LICENSE +0 -0
|
@@ -571,14 +571,22 @@ class AddModelMenu:
|
|
|
571
571
|
"cerebras": "cerebras",
|
|
572
572
|
"cohere": "custom_openai",
|
|
573
573
|
"perplexity": "custom_openai",
|
|
574
|
+
"minimax": "custom_anthropic",
|
|
574
575
|
}
|
|
575
576
|
|
|
576
577
|
# Determine the model type
|
|
577
578
|
model_type = type_mapping.get(provider.id, "custom_openai")
|
|
578
579
|
|
|
580
|
+
# Special case: kimi-for-coding provider uses "kimi-for-coding" as the model name
|
|
581
|
+
# instead of the model_id from models.dev (which is "kimi-k2-thinking")
|
|
582
|
+
if provider.id == "kimi-for-coding":
|
|
583
|
+
model_name = "kimi-for-coding"
|
|
584
|
+
else:
|
|
585
|
+
model_name = model.model_id
|
|
586
|
+
|
|
579
587
|
config: dict = {
|
|
580
588
|
"type": model_type,
|
|
581
|
-
"name":
|
|
589
|
+
"name": model_name,
|
|
582
590
|
}
|
|
583
591
|
|
|
584
592
|
# Add custom endpoint for non-standard providers
|
|
@@ -593,6 +601,16 @@ class AddModelMenu:
|
|
|
593
601
|
api_key_env = f"${provider.env[0]}" if provider.env else "$API_KEY"
|
|
594
602
|
config["custom_endpoint"] = {"url": api_url, "api_key": api_key_env}
|
|
595
603
|
|
|
604
|
+
# Special handling for minimax: uses custom_anthropic but needs custom_endpoint
|
|
605
|
+
# and the URL needs /v1 stripped (comes as https://api.minimax.io/anthropic/v1)
|
|
606
|
+
if provider.id == "minimax" and provider.api:
|
|
607
|
+
api_url = provider.api
|
|
608
|
+
# Strip /v1 suffix if present
|
|
609
|
+
if api_url.endswith("/v1"):
|
|
610
|
+
api_url = api_url[:-3]
|
|
611
|
+
api_key_env = f"${provider.env[0]}" if provider.env else "$API_KEY"
|
|
612
|
+
config["custom_endpoint"] = {"url": api_url, "api_key": api_key_env}
|
|
613
|
+
|
|
596
614
|
# Add context length if available
|
|
597
615
|
if model.context_length and model.context_length > 0:
|
|
598
616
|
config["context_length"] = model.context_length
|
|
@@ -977,6 +995,10 @@ class AddModelMenu:
|
|
|
977
995
|
# Reset awaiting input flag
|
|
978
996
|
set_awaiting_user_input(False)
|
|
979
997
|
|
|
998
|
+
# Clear exit message (unless we're about to prompt for more input)
|
|
999
|
+
if self.result not in ("pending_credentials", "pending_custom_model"):
|
|
1000
|
+
emit_info("✓ Exited model browser")
|
|
1001
|
+
|
|
980
1002
|
# Handle unsupported provider
|
|
981
1003
|
if self.result == "unsupported" and self.current_provider:
|
|
982
1004
|
reason = UNSUPPORTED_PROVIDERS.get(
|
|
@@ -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:
|
|
@@ -257,6 +423,11 @@ async def interactive_autosave_picker() -> Optional[str]:
|
|
|
257
423
|
current_page = [0] # Current page
|
|
258
424
|
result = [None] # Selected session name
|
|
259
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
|
+
|
|
260
431
|
total_pages = (len(entries) + PAGE_SIZE - 1) // PAGE_SIZE
|
|
261
432
|
|
|
262
433
|
def get_current_entry() -> Optional[Tuple[str, dict]]:
|
|
@@ -271,9 +442,17 @@ async def interactive_autosave_picker() -> Optional[str]:
|
|
|
271
442
|
def update_display():
|
|
272
443
|
"""Update both panels."""
|
|
273
444
|
menu_control.text = _render_menu_panel(
|
|
274
|
-
entries, current_page[0], selected_idx[0]
|
|
445
|
+
entries, current_page[0], selected_idx[0], browse_mode[0]
|
|
275
446
|
)
|
|
276
|
-
|
|
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())
|
|
277
456
|
|
|
278
457
|
menu_window = Window(
|
|
279
458
|
content=menu_control, wrap_lines=True, width=Dimension(weight=30)
|
|
@@ -298,19 +477,33 @@ async def interactive_autosave_picker() -> Optional[str]:
|
|
|
298
477
|
|
|
299
478
|
@kb.add("up")
|
|
300
479
|
def _(event):
|
|
301
|
-
if
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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()
|
|
306
492
|
|
|
307
493
|
@kb.add("down")
|
|
308
494
|
def _(event):
|
|
309
|
-
if
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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()
|
|
314
507
|
|
|
315
508
|
@kb.add("left")
|
|
316
509
|
def _(event):
|
|
@@ -326,6 +519,44 @@ async def interactive_autosave_picker() -> Optional[str]:
|
|
|
326
519
|
selected_idx[0] = current_page[0] * PAGE_SIZE
|
|
327
520
|
update_display()
|
|
328
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
|
+
|
|
329
560
|
@kb.add("enter")
|
|
330
561
|
def _(event):
|
|
331
562
|
entry = get_current_entry()
|
|
@@ -372,4 +603,9 @@ async def interactive_autosave_picker() -> Optional[str]:
|
|
|
372
603
|
# Reset awaiting input flag
|
|
373
604
|
set_awaiting_user_input(False)
|
|
374
605
|
|
|
606
|
+
# Clear exit message
|
|
607
|
+
from code_puppy.messaging import emit_info
|
|
608
|
+
|
|
609
|
+
emit_info("✓ Exited session browser")
|
|
610
|
+
|
|
375
611
|
return result[0]
|