code-puppy 0.0.196__py3-none-any.whl → 0.0.197__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.
@@ -720,10 +720,7 @@ class BaseAgent(ABC):
720
720
  emit_system_message(
721
721
  f"[green]Successfully loaded {len(servers)} MCP server(s)[/green]"
722
722
  )
723
- else:
724
- emit_system_message(
725
- "[yellow]No MCP servers available (check if servers are enabled)[/yellow]"
726
- )
723
+ # Stay silent when there are no servers configured/available
727
724
  return servers
728
725
 
729
726
  def reload_mcp_servers(self):
@@ -891,7 +888,8 @@ class BaseAgent(ABC):
891
888
  asyncio.CancelledError: When execution is cancelled by user
892
889
  """
893
890
  group_id = str(uuid.uuid4())
894
- pydantic_agent = self.reload_code_generation_agent()
891
+ # Avoid double-loading: reuse existing agent if already built
892
+ pydantic_agent = self._code_generation_agent or self.reload_code_generation_agent()
895
893
 
896
894
  async def run_agent_task():
897
895
  try:
@@ -11,125 +11,93 @@ from code_puppy.tools.tools_content import tools_content
11
11
 
12
12
 
13
13
  def get_commands_help():
14
- """Generate commands help using Rich Text objects to avoid markup conflicts."""
14
+ """Generate aligned commands help using Rich Text for safe markup."""
15
15
  from rich.text import Text
16
16
 
17
17
  # Ensure plugins are loaded so custom help can register
18
18
  _ensure_plugins_loaded()
19
19
 
20
- # Build help text programmatically
21
- help_lines = []
22
-
23
- # Title
24
- help_lines.append(Text("Commands Help", style="bold magenta"))
25
-
26
- # Commands - build each line programmatically
27
- help_lines.append(
28
- Text("/help, /h", style="cyan") + Text(" Show this help message")
29
- )
30
- help_lines.append(
31
- Text("/cd", style="cyan")
32
- + Text(" <dir> Change directory or show directories")
33
- )
34
- help_lines.append(
35
- Text("/agent", style="cyan")
36
- + Text(" <name> Switch to a different agent or show available agents")
37
- )
38
- help_lines.append(
39
- Text("/exit, /quit", style="cyan") + Text(" Exit interactive mode")
40
- )
41
- help_lines.append(
42
- Text("/generate-pr-description", style="cyan")
43
- + Text(" [@dir] Generate comprehensive PR description")
44
- )
45
- help_lines.append(
46
- Text("/model, /m", style="cyan") + Text(" <model> Set active model")
47
- )
48
- help_lines.append(
49
- Text("/reasoning", style="cyan")
50
- + Text(" <low|medium|high> Set OpenAI reasoning effort for GPT-5 models")
51
- )
52
- help_lines.append(
53
- Text("/pin_model", style="cyan")
54
- + Text(" <agent> <model> Pin a specific model to an agent")
55
- )
56
- help_lines.append(
57
- Text("/mcp", style="cyan")
58
- + Text(" Manage MCP servers (list, start, stop, status, etc.)")
59
- )
60
- help_lines.append(
61
- Text("/motd", style="cyan")
62
- + Text(" Show the latest message of the day (MOTD)")
63
- )
64
- help_lines.append(
65
- Text("/show", style="cyan")
66
- + Text(" Show puppy config key-values")
67
- )
68
- help_lines.append(
69
- Text("/compact", style="cyan")
70
- + Text(
71
- " Summarize and compact current chat history (uses compaction_strategy config)"
72
- )
73
- )
74
- help_lines.append(
75
- Text("/dump_context", style="cyan")
76
- + Text(" <name> Save current message history to file")
77
- )
78
- help_lines.append(
79
- Text("/load_context", style="cyan")
80
- + Text(" <name> Load message history from file")
81
- )
82
- help_lines.append(
83
- Text("/set", style="cyan")
84
- + Text(
85
- " Set puppy config key-values (e.g., /set yolo_mode true, /set auto_save_session true)"
86
- )
87
- )
88
- help_lines.append(
89
- Text("/tools", style="cyan")
90
- + Text(" Show available tools and capabilities")
91
- )
92
- help_lines.append(
93
- Text("/truncate", style="cyan")
94
- + Text(
95
- " <N> Truncate message history to N most recent messages (keeping system message)"
96
- )
97
- )
98
- help_lines.append(
99
- Text("/<unknown>", style="cyan")
100
- + Text(" Show unknown command warning")
101
- )
20
+ # Collect core commands with their syntax parts and descriptions
21
+ # (cmd_syntax, description)
22
+ core_cmds = [
23
+ ("/help, /h", "Show this help message"),
24
+ ("/cd <dir>", "Change directory or show directories"),
25
+ (
26
+ "/agent <name>",
27
+ "Switch to a different agent or show available agents",
28
+ ),
29
+ ("/exit, /quit", "Exit interactive mode"),
30
+ ("/generate-pr-description [@dir]", "Generate comprehensive PR description"),
31
+ ("/model, /m <model>", "Set active model"),
32
+ ("/reasoning <low|medium|high>", "Set OpenAI reasoning effort for GPT-5 models"),
33
+ ("/pin_model <agent> <model>", "Pin a specific model to an agent"),
34
+ ("/mcp", "Manage MCP servers (list, start, stop, status, etc.)"),
35
+ ("/motd", "Show the latest message of the day (MOTD)"),
36
+ ("/show", "Show puppy config key-values"),
37
+ (
38
+ "/compact",
39
+ "Summarize and compact current chat history (uses compaction_strategy config)",
40
+ ),
41
+ ("/dump_context <name>", "Save current message history to file"),
42
+ ("/load_context <name>", "Load message history from file"),
43
+ (
44
+ "/set",
45
+ "Set puppy config (e.g., /set yolo_mode true, /set auto_save_session true)",
46
+ ),
47
+ ("/tools", "Show available tools and capabilities"),
48
+ (
49
+ "/truncate <N>",
50
+ "Truncate history to N most recent messages (keeping system message)",
51
+ ),
52
+ ("/<unknown>", "Show unknown command warning"),
53
+ ]
54
+
55
+ # Determine padding width for the left column
56
+ left_width = max(len(cmd) for cmd, _ in core_cmds) + 2 # add spacing
57
+
58
+ lines: list[Text] = []
59
+ lines.append(Text("Commands Help", style="bold magenta"))
60
+
61
+ for cmd, desc in core_cmds:
62
+ left = Text(cmd.ljust(left_width), style="cyan")
63
+ right = Text(desc)
64
+ line = Text()
65
+ line.append_text(left)
66
+ line.append_text(right)
67
+ lines.append(line)
102
68
 
103
69
  # Add custom commands from plugins (if any)
104
70
  try:
105
71
  from code_puppy import callbacks
106
72
 
107
73
  custom_help_results = callbacks.on_custom_command_help()
108
- # Flatten various returns into a list of (name, description)
109
- custom_entries = []
74
+ custom_entries: list[tuple[str, str]] = []
110
75
  for res in custom_help_results:
111
76
  if not res:
112
77
  continue
113
78
  if isinstance(res, tuple) and len(res) == 2:
114
- custom_entries.append(res)
79
+ custom_entries.append((str(res[0]), str(res[1])))
115
80
  elif isinstance(res, list):
116
81
  for item in res:
117
82
  if isinstance(item, tuple) and len(item) == 2:
118
- custom_entries.append(item)
83
+ custom_entries.append((str(item[0]), str(item[1])))
119
84
  if custom_entries:
120
- help_lines.append(Text("\n", style="dim"))
121
- help_lines.append(Text("Custom Commands", style="bold magenta"))
85
+ lines.append(Text("", style="dim"))
86
+ lines.append(Text("Custom Commands", style="bold magenta"))
87
+ # Compute padding for custom commands as well
88
+ custom_left_width = max(len(name) for name, _ in custom_entries) + 3
122
89
  for name, desc in custom_entries:
123
- help_lines.append(
124
- Text(f"/{name}", style="cyan") + Text(f" {desc}")
125
- )
90
+ left = Text(f"/{name}".ljust(custom_left_width), style="cyan")
91
+ right = Text(desc)
92
+ line = Text()
93
+ line.append_text(left)
94
+ line.append_text(right)
95
+ lines.append(line)
126
96
  except Exception:
127
- # If callbacks fail, skip custom help silently
128
97
  pass
129
98
 
130
- # Combine all lines
131
99
  final_text = Text()
132
- for i, line in enumerate(help_lines):
100
+ for i, line in enumerate(lines):
133
101
  if i > 0:
134
102
  final_text.append("\n")
135
103
  final_text.append_text(line)
@@ -194,24 +194,48 @@ async def get_input_with_combined_completion(
194
194
  LoadContextCompleter(trigger="/load_context"),
195
195
  ]
196
196
  )
197
- # Add custom key bindings for multiline input
197
+ # Add custom key bindings and multiline toggle
198
198
  bindings = KeyBindings()
199
199
 
200
- @bindings.add(Keys.Escape, "m") # Alt+M (legacy support)
200
+ # Multiline mode state
201
+ multiline = {"enabled": False}
202
+
203
+ # Toggle multiline with Alt+M
204
+ @bindings.add(Keys.Escape, "m")
201
205
  def _(event):
202
- event.app.current_buffer.insert_text("\n")
206
+ multiline["enabled"] = not multiline["enabled"]
207
+ status = "ON" if multiline["enabled"] else "OFF"
208
+ # Print status for user feedback (version-agnostic)
209
+ print(f"[multiline] {status}", flush=True)
210
+
211
+ # Also toggle multiline with F2 (more reliable across platforms)
212
+ @bindings.add("f2")
213
+ def _(event):
214
+ multiline["enabled"] = not multiline["enabled"]
215
+ status = "ON" if multiline["enabled"] else "OFF"
216
+ print(f"[multiline] {status}", flush=True)
203
217
 
204
- # Create a special binding for shift+enter
205
- @bindings.add("escape", "enter")
218
+ # Newline insert bindings robust and explicit
219
+ # Ctrl+J (line feed) works in virtually all terminals; mark eager so it wins
220
+ @bindings.add("c-j", eager=True)
206
221
  def _(event):
207
- """Pressing alt+enter (meta+enter) inserts a newline."""
208
222
  event.app.current_buffer.insert_text("\n")
209
223
 
210
- # Override the default enter behavior to check for shift
211
- @bindings.add("enter", filter=~is_searching)
224
+ # Also allow Ctrl+Enter for newline (terminal-dependent)
225
+ try:
226
+ @bindings.add("c-enter", eager=True)
227
+ def _(event):
228
+ event.app.current_buffer.insert_text("\n")
229
+ except Exception:
230
+ pass
231
+
232
+ # Enter behavior depends on multiline mode
233
+ @bindings.add("enter", filter=~is_searching, eager=True)
212
234
  def _(event):
213
- """Accept input only when we're not in an interactive search buffer."""
214
- event.current_buffer.validate_and_handle()
235
+ if multiline["enabled"]:
236
+ event.app.current_buffer.insert_text("\n")
237
+ else:
238
+ event.current_buffer.validate_and_handle()
215
239
 
216
240
  @bindings.add(Keys.Escape)
217
241
  def _(event):
code_puppy/main.py CHANGED
@@ -272,16 +272,13 @@ async def interactive_mode(message_renderer, initial_command: str = None) -> Non
272
272
  emit_info("[bold green]Code Puppy[/bold green] - Interactive Mode")
273
273
  emit_system_message("Type '/exit' or '/quit' to exit the interactive mode.")
274
274
  emit_system_message("Type 'clear' to reset the conversation history.")
275
+ emit_system_message("[dim]Type /help to view all commands[/dim]")
275
276
  emit_system_message(
276
- "Type [bold blue]@[/bold blue] for path completion, or [bold blue]/m[/bold blue] to pick a model. Use [bold blue]Esc+Enter[/bold blue] for multi-line input."
277
+ "Type [bold blue]@[/bold blue] for path completion, or [bold blue]/m[/bold blue] to pick a model. Toggle multiline with [bold blue]Alt+M[/bold blue] or [bold blue]F2[/bold blue]; newline: [bold blue]Ctrl+J[/bold blue]."
277
278
  )
278
279
  emit_system_message(
279
280
  "Press [bold red]Ctrl+C[/bold red] during processing to cancel the current task or inference."
280
281
  )
281
- from code_puppy.command_line.command_handler import get_commands_help
282
-
283
- help_text = get_commands_help()
284
- emit_system_message(help_text)
285
282
  try:
286
283
  from code_puppy.command_line.motd import print_motd
287
284
 
code_puppy/tui/app.py CHANGED
@@ -176,6 +176,13 @@ class CodePuppyTUI(App):
176
176
  # Start the message renderer EARLY to catch startup messages
177
177
  # Using call_after_refresh to start it as soon as possible after mount
178
178
  self.call_after_refresh(self.start_message_renderer_sync)
179
+
180
+ # Kick off a non-blocking preload of the agent/model so the
181
+ # status bar shows loading before first prompt
182
+ self.call_after_refresh(self.preload_agent_on_startup)
183
+
184
+ # After preload, offer to restore an autosave session (like interactive mode)
185
+ self.call_after_refresh(self.maybe_prompt_restore_autosave)
179
186
 
180
187
  # Apply responsive design adjustments
181
188
  self.apply_responsive_layout()
@@ -187,16 +194,40 @@ class CodePuppyTUI(App):
187
194
  if self.initial_command:
188
195
  self.call_after_refresh(self.process_initial_command)
189
196
 
197
+ def _tighten_text(self, text: str) -> str:
198
+ """Aggressively tighten whitespace: trim lines, collapse multiples, drop extra blanks."""
199
+ try:
200
+ import re
201
+
202
+ # Split into lines, strip each, drop empty runs
203
+ lines = [re.sub(r"\s+", " ", ln.strip()) for ln in text.splitlines()]
204
+ # Remove consecutive blank lines
205
+ tight_lines = []
206
+ last_blank = False
207
+ for ln in lines:
208
+ is_blank = (ln == "")
209
+ if is_blank and last_blank:
210
+ continue
211
+ tight_lines.append(ln)
212
+ last_blank = is_blank
213
+ return "\n".join(tight_lines).strip()
214
+ except Exception:
215
+ return text.strip()
216
+
190
217
  def add_system_message(
191
218
  self, content: str, message_group: str = None, group_id: str = None
192
219
  ) -> None:
193
220
  """Add a system message to the chat."""
194
221
  # Support both parameter names for backward compatibility
195
222
  final_group_id = message_group or group_id
223
+ # Tighten only plain strings
224
+ content_to_use = (
225
+ self._tighten_text(content) if isinstance(content, str) else content
226
+ )
196
227
  message = ChatMessage(
197
228
  id=f"sys_{datetime.now(timezone.utc).timestamp()}",
198
229
  type=MessageType.SYSTEM,
199
- content=content,
230
+ content=content_to_use,
200
231
  timestamp=datetime.now(timezone.utc),
201
232
  group_id=final_group_id,
202
233
  )
@@ -245,10 +276,13 @@ class CodePuppyTUI(App):
245
276
 
246
277
  def add_error_message(self, content: str, message_group: str = None) -> None:
247
278
  """Add an error message to the chat."""
279
+ content_to_use = (
280
+ self._tighten_text(content) if isinstance(content, str) else content
281
+ )
248
282
  message = ChatMessage(
249
283
  id=f"error_{datetime.now(timezone.utc).timestamp()}",
250
284
  type=MessageType.ERROR,
251
- content=content,
285
+ content=content_to_use,
252
286
  timestamp=datetime.now(timezone.utc),
253
287
  group_id=message_group,
254
288
  )
@@ -303,9 +337,9 @@ class CodePuppyTUI(App):
303
337
 
304
338
  # Only handle keys when input field is focused
305
339
  if input_field.has_focus:
306
- # Handle Ctrl+Enter for new lines (more reliable than Shift+Enter)
307
- if event.key == "ctrl+enter":
308
- input_field.insert("\\n")
340
+ # Handle Ctrl+Enter or Shift+Enter for a new line
341
+ if event.key in ("ctrl+enter", "shift+enter"):
342
+ input_field.insert("\n")
309
343
  event.prevent_default()
310
344
  return
311
345
 
@@ -484,6 +518,14 @@ class CodePuppyTUI(App):
484
518
  self.update_agent_progress("Processing", 75)
485
519
  agent_response = result.output
486
520
  self.add_agent_message(agent_response)
521
+
522
+ # Auto-save session if enabled (mirror --interactive)
523
+ try:
524
+ from code_puppy.config import auto_save_session_if_enabled
525
+ auto_save_session_if_enabled()
526
+ except Exception:
527
+ pass
528
+
487
529
  # Refresh history display to show new interaction
488
530
  self.refresh_history_display()
489
531
 
@@ -842,6 +884,36 @@ class CodePuppyTUI(App):
842
884
  """Synchronous wrapper to start message renderer via run_worker."""
843
885
  self.run_worker(self.start_message_renderer(), exclusive=False)
844
886
 
887
+ async def preload_agent_on_startup(self) -> None:
888
+ """Preload the agent/model at startup so loading status is visible."""
889
+ try:
890
+ # Show loading in status bar and spinner
891
+ self.start_agent_progress("Loading")
892
+
893
+ # Warm up agent/model without blocking UI
894
+ import asyncio
895
+
896
+ from code_puppy.agents.agent_manager import get_current_agent
897
+
898
+ agent = get_current_agent()
899
+
900
+ # Run the synchronous reload in a worker thread
901
+ await asyncio.to_thread(agent.reload_code_generation_agent)
902
+
903
+ # After load, refresh current model (in case of fallback or changes)
904
+ from code_puppy.config import get_global_model_name
905
+
906
+ self.current_model = get_global_model_name()
907
+
908
+ # Let the user know model/agent are ready
909
+ self.add_system_message("Model and agent preloaded. Ready to roll 🛼")
910
+ except Exception as e:
911
+ # Surface any preload issues but keep app usable
912
+ self.add_error_message(f"Startup preload failed: {e}")
913
+ finally:
914
+ # Always stop spinner and set ready state
915
+ self.stop_agent_progress()
916
+
845
917
  async def start_message_renderer(self):
846
918
  """Start the message renderer to consume messages from the queue."""
847
919
  if not self._renderer_started:
@@ -884,9 +956,9 @@ class CodePuppyTUI(App):
884
956
  f"Error processing startup message: {e}"
885
957
  )
886
958
 
887
- # Create a single grouped startup message
959
+ # Create a single grouped startup message (tightened)
888
960
  grouped_content = "\n".join(startup_content_lines)
889
- self.add_system_message(grouped_content)
961
+ self.add_system_message(self._tighten_text(grouped_content))
890
962
 
891
963
  # Clear the startup buffer after processing
892
964
  self.message_queue.clear_startup_buffer()
@@ -894,6 +966,80 @@ class CodePuppyTUI(App):
894
966
  # Now start the regular message renderer
895
967
  await self.message_renderer.start()
896
968
 
969
+ async def maybe_prompt_restore_autosave(self) -> None:
970
+ """Offer to restore an autosave session at startup (TUI version)."""
971
+ try:
972
+ import asyncio
973
+ from pathlib import Path
974
+
975
+ from code_puppy.config import AUTOSAVE_DIR, set_current_autosave_from_session_name
976
+ from code_puppy.session_storage import list_sessions, load_session
977
+
978
+ base_dir = Path(AUTOSAVE_DIR)
979
+ sessions = list_sessions(base_dir)
980
+ if not sessions:
981
+ return
982
+
983
+ # Show modal picker for selection
984
+ from .screens.autosave_picker import AutosavePicker
985
+
986
+ async def handle_result(result_name: str | None):
987
+ if not result_name:
988
+ return
989
+ try:
990
+ # Load history and set into agent
991
+ from code_puppy.agents.agent_manager import get_current_agent
992
+
993
+ history = load_session(result_name, base_dir)
994
+ agent = get_current_agent()
995
+ agent.set_message_history(history)
996
+
997
+ # Set current autosave session id so subsequent autosaves overwrite this session
998
+ try:
999
+ set_current_autosave_from_session_name(result_name)
1000
+ except Exception:
1001
+ pass
1002
+
1003
+ # Update token info/status bar
1004
+ total_tokens = sum(
1005
+ agent.estimate_tokens_for_message(msg) for msg in history
1006
+ )
1007
+ try:
1008
+ status_bar = self.query_one(StatusBar)
1009
+ status_bar.update_token_info(
1010
+ total_tokens,
1011
+ agent.get_model_context_length(),
1012
+ total_tokens / max(1, agent.get_model_context_length()),
1013
+ )
1014
+ except Exception:
1015
+ pass
1016
+
1017
+ # Notify
1018
+ session_path = base_dir / f"{result_name}.pkl"
1019
+ self.add_system_message(
1020
+ f"✅ Autosave loaded: {len(history)} messages ({total_tokens} tokens)\n"
1021
+ f"📁 From: {session_path}"
1022
+ )
1023
+
1024
+ # Refresh history sidebar
1025
+ self.refresh_history_display()
1026
+ except Exception as e:
1027
+ self.add_error_message(f"Failed to load autosave: {e}")
1028
+
1029
+ # Push modal and await result
1030
+ picker = AutosavePicker(base_dir)
1031
+
1032
+ # Use Textual's push_screen with a result callback
1033
+ def on_picker_result(result_name=None):
1034
+ # Schedule async handler to avoid blocking UI
1035
+ import asyncio
1036
+ self.run_worker(handle_result(result_name), exclusive=False)
1037
+
1038
+ self.push_screen(picker, on_picker_result)
1039
+ except Exception as e:
1040
+ # Fail silently but show debug in chat
1041
+ self.add_system_message(f"[dim]Autosave prompt error: {e}[/dim]")
1042
+
897
1043
  async def stop_message_renderer(self):
898
1044
  """Stop the message renderer."""
899
1045
  if self._renderer_started:
@@ -133,7 +133,7 @@ class InputArea(Container):
133
133
  yield CustomTextArea(id="input-field", show_line_numbers=False)
134
134
  yield SubmitCancelButton()
135
135
  yield Static(
136
- "Enter to send • Alt+Enter for new line • Ctrl+1 for help",
136
+ "Enter to send • Shift+Enter for new line • Ctrl+1 for help",
137
137
  id="input-help",
138
138
  )
139
139
 
@@ -83,7 +83,10 @@ class StatusBar(Static):
83
83
  elif self.agent_status == "Busy":
84
84
  status_indicator = "🔄"
85
85
  status_color = "orange"
86
- else: # Ready
86
+ elif self.agent_status == "Loading":
87
+ status_indicator = "⏳"
88
+ status_color = "cyan"
89
+ else: # Ready or anything else
87
90
  status_indicator = "✅"
88
91
  status_color = "green"
89
92
 
@@ -6,10 +6,12 @@ from .help import HelpScreen
6
6
  from .mcp_install_wizard import MCPInstallWizardScreen
7
7
  from .settings import SettingsScreen
8
8
  from .tools import ToolsScreen
9
+ from .autosave_picker import AutosavePicker
9
10
 
10
11
  __all__ = [
11
12
  "HelpScreen",
12
13
  "SettingsScreen",
13
14
  "ToolsScreen",
14
15
  "MCPInstallWizardScreen",
16
+ "AutosavePicker",
15
17
  ]
@@ -0,0 +1,166 @@
1
+ """
2
+ Autosave Picker modal for TUI.
3
+ Lists recent autosave sessions and lets the user load one.
4
+ """
5
+ from __future__ import annotations
6
+
7
+ import json
8
+ from dataclasses import dataclass
9
+ from datetime import datetime
10
+ from pathlib import Path
11
+ from typing import List, Optional, Tuple
12
+
13
+ from textual import on
14
+ from textual.app import ComposeResult
15
+ from textual.containers import Container, Horizontal
16
+ from textual.screen import ModalScreen
17
+ from textual.widgets import Button, Label, ListItem, ListView, Static
18
+
19
+ from code_puppy.session_storage import list_sessions
20
+
21
+
22
+ @dataclass(slots=True)
23
+ class AutosaveEntry:
24
+ name: str
25
+ timestamp: Optional[str]
26
+ message_count: Optional[int]
27
+
28
+
29
+ def _load_metadata(base_dir: Path, name: str) -> Tuple[Optional[str], Optional[int]]:
30
+ meta_path = base_dir / f"{name}_meta.json"
31
+ try:
32
+ with meta_path.open("r", encoding="utf-8") as meta_file:
33
+ data = json.load(meta_file)
34
+ return data.get("timestamp"), data.get("message_count")
35
+ except Exception:
36
+ return None, None
37
+
38
+
39
+ class AutosavePicker(ModalScreen):
40
+ """Modal to present available autosave sessions for selection."""
41
+
42
+ DEFAULT_CSS = """
43
+ AutosavePicker {
44
+ align: center middle;
45
+ }
46
+
47
+ #modal-container {
48
+ width: 80%;
49
+ max-width: 100;
50
+ height: 24;
51
+ min-height: 18;
52
+ background: $surface;
53
+ border: solid $primary;
54
+ padding: 1 2;
55
+ layout: vertical;
56
+ }
57
+
58
+ #list-label {
59
+ width: 100%;
60
+ height: 1;
61
+ color: $text;
62
+ text-align: left;
63
+ }
64
+
65
+ #autosave-list {
66
+ height: 1fr;
67
+ overflow: auto;
68
+ border: solid $primary-darken-2;
69
+ background: $surface-darken-1;
70
+ margin: 1 0;
71
+ }
72
+
73
+ .button-row {
74
+ height: 3;
75
+ align-horizontal: right;
76
+ margin-top: 1;
77
+ }
78
+
79
+ #cancel-button { background: $primary-darken-1; }
80
+ #load-button { background: $success; }
81
+ """
82
+
83
+ def __init__(self, autosave_dir: Path, **kwargs):
84
+ super().__init__(**kwargs)
85
+ self.autosave_dir = autosave_dir
86
+ self.entries: List[AutosaveEntry] = []
87
+ self.list_view: Optional[ListView] = None
88
+
89
+ def on_mount(self) -> None:
90
+ names = list_sessions(self.autosave_dir)
91
+ raw_entries: List[Tuple[str, Optional[str], Optional[int]]] = []
92
+ for name in names:
93
+ ts, count = _load_metadata(self.autosave_dir, name)
94
+ raw_entries.append((name, ts, count))
95
+
96
+ def sort_key(entry):
97
+ _, ts, _ = entry
98
+ if ts:
99
+ try:
100
+ return datetime.fromisoformat(ts)
101
+ except ValueError:
102
+ return datetime.min
103
+ return datetime.min
104
+
105
+ raw_entries.sort(key=sort_key, reverse=True)
106
+ self.entries = [AutosaveEntry(*e) for e in raw_entries]
107
+
108
+ # Populate the ListView now that entries are ready
109
+ if self.list_view is None:
110
+ try:
111
+ self.list_view = self.query_one("#autosave-list", ListView)
112
+ except Exception:
113
+ self.list_view = None
114
+
115
+ if self.list_view is not None:
116
+ # Clear existing items if any
117
+ try:
118
+ self.list_view.clear()
119
+ except Exception:
120
+ # Fallback: remove children manually
121
+ self.list_view.children.clear() # type: ignore
122
+
123
+ for entry in self.entries[:50]:
124
+ ts = entry.timestamp or "unknown time"
125
+ count = f"{entry.message_count} msgs" if entry.message_count is not None else "unknown size"
126
+ label = f"{entry.name} — {count}, saved at {ts}"
127
+ self.list_view.append(ListItem(Static(label)))
128
+
129
+ # Focus and select first item for better UX
130
+ if len(self.entries) > 0:
131
+ self.list_view.index = 0
132
+ self.list_view.focus()
133
+
134
+ def compose(self) -> ComposeResult:
135
+ with Container(id="modal-container"):
136
+ yield Label("Select an autosave to load (Esc to cancel)", id="list-label")
137
+ self.list_view = ListView(id="autosave-list")
138
+ # populate items
139
+ for entry in self.entries[:50]: # cap to avoid long lists
140
+ ts = entry.timestamp or "unknown time"
141
+ count = f"{entry.message_count} msgs" if entry.message_count is not None else "unknown size"
142
+ label = f"{entry.name} — {count}, saved at {ts}"
143
+ self.list_view.append(ListItem(Static(label)))
144
+ yield self.list_view
145
+ with Horizontal(classes="button-row"):
146
+ yield Button("Cancel", id="cancel-button")
147
+ yield Button("Load", id="load-button", variant="primary")
148
+
149
+ @on(Button.Pressed, "#cancel-button")
150
+ def cancel(self) -> None:
151
+ self.dismiss(None)
152
+
153
+ @on(Button.Pressed, "#load-button")
154
+ def load_selected(self) -> None:
155
+ if not self.list_view or not self.entries:
156
+ self.dismiss(None)
157
+ return
158
+ idx = self.list_view.index if self.list_view.index is not None else 0
159
+ if 0 <= idx < len(self.entries):
160
+ self.dismiss(self.entries[idx].name)
161
+ else:
162
+ self.dismiss(None)
163
+
164
+ def on_list_view_selected(self, event: ListView.Selected) -> None: # type: ignore
165
+ # Double-enter may select; we just map to load button
166
+ self.load_selected()
@@ -4,7 +4,7 @@ Settings modal screen.
4
4
 
5
5
  from textual import on
6
6
  from textual.app import ComposeResult
7
- from textual.containers import Container
7
+ from textual.containers import Container, VerticalScroll
8
8
  from textual.screen import ModalScreen
9
9
  from textual.widgets import Button, Input, Select, Static
10
10
 
@@ -27,6 +27,7 @@ class SettingsScreen(ModalScreen):
27
27
 
28
28
  #settings-form {
29
29
  height: 1fr;
30
+ overflow: auto;
30
31
  }
31
32
 
32
33
  .setting-row {
@@ -70,7 +71,8 @@ class SettingsScreen(ModalScreen):
70
71
  def compose(self) -> ComposeResult:
71
72
  with Container(id="settings-dialog"):
72
73
  yield Static("⚙️ Settings Configuration", id="settings-title")
73
- with Container(id="settings-form"):
74
+ # Make the form scrollable so long content fits
75
+ with VerticalScroll(id="settings-form"):
74
76
  with Container(classes="setting-row"):
75
77
  yield Static("Puppy Name:", classes="setting-label")
76
78
  yield Input(id="puppy-name-input", classes="setting-input")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-puppy
3
- Version: 0.0.196
3
+ Version: 0.0.197
4
4
  Summary: Code generation agent
5
5
  Project-URL: repository, https://github.com/mpfaffenberger/code_puppy
6
6
  Project-URL: HomePage, https://github.com/mpfaffenberger/code_puppy
@@ -25,7 +25,7 @@ Requires-Dist: logfire>=0.7.1
25
25
  Requires-Dist: openai>=1.99.1
26
26
  Requires-Dist: pathspec>=0.11.0
27
27
  Requires-Dist: playwright>=1.40.0
28
- Requires-Dist: prompt-toolkit>=3.0.38
28
+ Requires-Dist: prompt-toolkit>=3.0.52
29
29
  Requires-Dist: pydantic-ai==1.0.6
30
30
  Requires-Dist: pydantic>=2.4.0
31
31
  Requires-Dist: pyjwt>=2.8.0
@@ -69,6 +69,17 @@ Code Puppy is an AI-powered code generation agent, designed to understand progra
69
69
 
70
70
  ## Features
71
71
 
72
+ ### Session Autosave & Contexts
73
+ - Autosaves live in `~/.code_puppy/autosaves` and include a `.pkl` and `_meta.json` per session.
74
+ - On startup, you’ll be prompted to optionally load a recent autosave (with message counts and timestamps).
75
+ - Autosaves use a stable session ID per interactive run so subsequent prompts overwrite the same session (not N new files). Rotate via `/session new` when you want a fresh session.
76
+ - Loading an autosave makes it the active autosave target (future autosaves overwrite that loaded session).
77
+ - Loading a manual context with `/load_context <name>` automatically rotates the autosave ID to avoid overwriting anything.
78
+ - Helpers:
79
+ - `/session id` shows the current autosave ID and file prefix
80
+ - `/session new` rotates the autosave ID
81
+
82
+
72
83
  - **Multi-language support**: Capable of generating code in various programming languages.
73
84
  - **Interactive CLI**: A command-line interface for interactive use.
74
85
  - **Detailed explanations**: Provides insights into generated code to understand its logic and structure.
@@ -3,7 +3,7 @@ code_puppy/__main__.py,sha256=pDVssJOWP8A83iFkxMLY9YteHYat0EyWDQqMkKHpWp4,203
3
3
  code_puppy/callbacks.py,sha256=ukSgVFaEO68o6J09qFwDrnmNanrVv3toTLQhS504Meo,6162
4
4
  code_puppy/config.py,sha256=S-VcC97Syz8j-bVNBr85DlhOr6exxAHQQR9JdHYOODs,25877
5
5
  code_puppy/http_utils.py,sha256=YLd8Y16idbI32JGeBXG8n5rT4o4X_zxk9FgUvK9XFo8,8248
6
- code_puppy/main.py,sha256=Csdkoufbgt3aTQBopQqq29XDyHIy84Ink6M7H5NBrXs,22076
6
+ code_puppy/main.py,sha256=bljWR8TQRy5GgQf3I_xCgnA5WaBXRS3g_0IcWmiCY3A,22062
7
7
  code_puppy/model_factory.py,sha256=ZbIAJWMNKNdTCEMQK8Ig6TDDZlVNyGO9hOLHoLLPMYw,15397
8
8
  code_puppy/models.json,sha256=dClUciCo2RlVDs0ZAQCIur8MOavZUEAXHEecn0uPa-4,1629
9
9
  code_puppy/reopenable_async_client.py,sha256=4UJRaMp5np8cbef9F0zKQ7TPKOfyf5U-Kv-0zYUWDho,8274
@@ -27,15 +27,15 @@ code_puppy/agents/agent_qa_expert.py,sha256=wCGXzuAVElT5c-QigQVb8JX9Gw0JmViCUQQn
27
27
  code_puppy/agents/agent_qa_kitten.py,sha256=5PeFFSwCFlTUvP6h5bGntx0xv5NmRwBiw0HnMqY8nLI,9107
28
28
  code_puppy/agents/agent_security_auditor.py,sha256=ADafi2x4gqXw6m-Nch5vjiKjO0Urcbj0x4zxHti3gDw,3712
29
29
  code_puppy/agents/agent_typescript_reviewer.py,sha256=EDY1mFkVpuJ1BPXsJFu2wQ2pfAV-90ipc_8w9ymrKPg,4054
30
- code_puppy/agents/base_agent.py,sha256=rJm0xA9kLT_NU9MSZIrN-Z_T5O4Q-QuUmQM8paGZKHQ,39066
30
+ code_puppy/agents/base_agent.py,sha256=ikeV6Sui3HAagJBPTtI9T9pCDCFTCYDNEkFFw7XU21Y,39084
31
31
  code_puppy/agents/json_agent.py,sha256=KPS1q-Rr3b5ekem4i3wtu8eLJRDd5nSPiZ8duJ_tn0U,4630
32
32
  code_puppy/command_line/__init__.py,sha256=y7WeRemfYppk8KVbCGeAIiTuiOszIURCDjOMZv_YRmU,45
33
- code_puppy/command_line/command_handler.py,sha256=1oR2pvTX7-3ciHqb3kVTfpVfsZdExmN8CfbyWiATkOQ,31274
33
+ code_puppy/command_line/command_handler.py,sha256=pLPnaASIezgODHXm0_pr5W2rB-ZoPyyRLhE2jLPncTE,30618
34
34
  code_puppy/command_line/file_path_completion.py,sha256=gw8NpIxa6GOpczUJRyh7VNZwoXKKn-yvCqit7h2y6Gg,2931
35
35
  code_puppy/command_line/load_context_completion.py,sha256=6eZxV6Bs-EFwZjN93V8ZDZUC-6RaWxvtZk-04Wtikyw,2240
36
36
  code_puppy/command_line/model_picker_completion.py,sha256=vYNCZS1QWu6fxF__hTwpc7jwH7h_48wUxrnITawc83E,4140
37
37
  code_puppy/command_line/motd.py,sha256=PEdkp3ZnydVfvd7mNJylm8YyFNUKg9jmY6uwkA1em8c,2152
38
- code_puppy/command_line/prompt_toolkit_completion.py,sha256=NbagxHj950ADRncqXuVM8gAwqdrfKeGfC0x6-IhrsXM,9860
38
+ code_puppy/command_line/prompt_toolkit_completion.py,sha256=_8SUUCKfjOxgNMkcHTeB08NIjCeqL3utHljROXLTEZE,10656
39
39
  code_puppy/command_line/utils.py,sha256=7eyxDHjPjPB9wGDJQQcXV_zOsGdYsFgI0SGCetVmTqE,1251
40
40
  code_puppy/command_line/mcp/__init__.py,sha256=0-OQuwjq_pLiTVJ1_NrirVwdRerghyKs_MTZkwPC7YY,315
41
41
  code_puppy/command_line/mcp/add_command.py,sha256=lZ09RpFDIeghX1zhc2YIAqBASs5Ra52x5YAasUKvqJg,6409
@@ -101,7 +101,7 @@ code_puppy/tools/browser/browser_workflows.py,sha256=jplJ1T60W3G4-dhVJX-CXkm9ssk
101
101
  code_puppy/tools/browser/camoufox_manager.py,sha256=RYvLcs0iAoVNtpLjrrA1uu6a5k9tAdBbmhWFGSWjX_A,6106
102
102
  code_puppy/tools/browser/vqa_agent.py,sha256=0GMDgJAK728rIuSQxAVytFSNagjo0LCjCUxBTm3w9Po,1952
103
103
  code_puppy/tui/__init__.py,sha256=XesAxIn32zLPOmvpR2wIDxDAnnJr81a5pBJB4cZp1Xs,321
104
- code_puppy/tui/app.py,sha256=t9HAmf58_0Qo-MMosvMWBfRurbhJIcEpgMBsUc52v38,38246
104
+ code_puppy/tui/app.py,sha256=D-8qHzxYbe-bVgrkBLl2lLBw7HRbUoVqDTRKy1gaE-E,44279
105
105
  code_puppy/tui/messages.py,sha256=zQoToWI0eWdT36NEsY6RdCFzcDfAmfvoPlHv8jiCbgo,720
106
106
  code_puppy/tui/components/__init__.py,sha256=uj5pnk3s6SEN3SbFI0ZnzaA2KK1NNg8TfUj6U-Z732U,455
107
107
  code_puppy/tui/components/chat_view.py,sha256=Ff6uM6J0yENISNAOYroX7F-JL73_ajUUcP5IZSf2mng,19914
@@ -109,21 +109,22 @@ code_puppy/tui/components/command_history_modal.py,sha256=pUPEQvoCWa2iUnuMgNwO22
109
109
  code_puppy/tui/components/copy_button.py,sha256=E4-OJYk5YNzDf-E81NyiVGKsTRPrUX-RnQ8qFuVnabw,4375
110
110
  code_puppy/tui/components/custom_widgets.py,sha256=qsVsPLh_oUjMWBznewH8Ya1BdGSiIwNiad2qkdfvCJk,2114
111
111
  code_puppy/tui/components/human_input_modal.py,sha256=isj-zrSIcK5iy3L7HJNgDFWN1zhxY4f3zvp4krbs07E,5424
112
- code_puppy/tui/components/input_area.py,sha256=R4R32eXPZ2R8KFisIbldNGq60KMk7kCxWrdbeTgJUr8,4395
112
+ code_puppy/tui/components/input_area.py,sha256=RRnprt1_mIelXla_tmv1PFS8oTwdDAuB054S5Pnbea8,4397
113
113
  code_puppy/tui/components/sidebar.py,sha256=nGtCiYzZalPmiFaJ4dwj2S4EJBu5wQZVzhoigYYY7U4,10369
114
- code_puppy/tui/components/status_bar.py,sha256=GgznJqF8Wk6XkurBuKohLyu75eT_ucBTvl9oPcySmnM,6338
114
+ code_puppy/tui/components/status_bar.py,sha256=TCtfQ0w6NXjaN-0eRsbhKrufDQNEKJ5UiBNj_r70664,6471
115
115
  code_puppy/tui/models/__init__.py,sha256=5Eq7BMibz-z_t_v7B4H4tCdKRG41i2CaCuNQf_lteAE,140
116
116
  code_puppy/tui/models/chat_message.py,sha256=2fSqsl4EHKgGsi_cVKWBbFq1NQwZyledGuJ9djovtLY,477
117
117
  code_puppy/tui/models/command_history.py,sha256=bPWr_xnyQvjG5tPg_5pwqlEzn2fR170HlvBJwAXRpAE,2895
118
118
  code_puppy/tui/models/enums.py,sha256=1ulsei95Gxy4r1sk-m-Sm5rdmejYCGRI-YtUwJmKFfM,501
119
- code_puppy/tui/screens/__init__.py,sha256=tJ00d0aYQ9kzOGHRChqy6cCQ6JUKKXBzLUTEbk_eA2Y,286
119
+ code_puppy/tui/screens/__init__.py,sha256=qxiJKyO3MKCNdPjUuHA2-Pnpda0JN20n7e9sU25eC9M,352
120
+ code_puppy/tui/screens/autosave_picker.py,sha256=9bazha2C5N3Xg_VcmpcTv-CYOBq_mwcECBfrQM9tHxA,5416
120
121
  code_puppy/tui/screens/help.py,sha256=eJuPaOOCp7ZSUlecearqsuX6caxWv7NQszUh0tZJjBM,3232
121
122
  code_puppy/tui/screens/mcp_install_wizard.py,sha256=vObpQwLbXjQsxmSg-WCasoev1usEi0pollKnL0SHu9U,27693
122
- code_puppy/tui/screens/settings.py,sha256=-WLldnKyWVKUYVPJcfOn1UU6eP9t8lLPUAVI317SOOM,10685
123
+ code_puppy/tui/screens/settings.py,sha256=EsoL_gbN5FpEXGuDqhtdDznNZy_eGNMMuZnWuARSWi8,10790
123
124
  code_puppy/tui/screens/tools.py,sha256=3pr2Xkpa9Js6Yhf1A3_wQVRzFOui-KDB82LwrsdBtyk,1715
124
- code_puppy-0.0.196.data/data/code_puppy/models.json,sha256=dClUciCo2RlVDs0ZAQCIur8MOavZUEAXHEecn0uPa-4,1629
125
- code_puppy-0.0.196.dist-info/METADATA,sha256=o-3kdJ2IzyMxkaEa4_XOH7StuqfcZX3YwjZF3Sy4Yv8,19987
126
- code_puppy-0.0.196.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
127
- code_puppy-0.0.196.dist-info/entry_points.txt,sha256=Tp4eQC99WY3HOKd3sdvb22vZODRq0XkZVNpXOag_KdI,91
128
- code_puppy-0.0.196.dist-info/licenses/LICENSE,sha256=31u8x0SPgdOq3izJX41kgFazWsM43zPEF9eskzqbJMY,1075
129
- code_puppy-0.0.196.dist-info/RECORD,,
125
+ code_puppy-0.0.197.data/data/code_puppy/models.json,sha256=dClUciCo2RlVDs0ZAQCIur8MOavZUEAXHEecn0uPa-4,1629
126
+ code_puppy-0.0.197.dist-info/METADATA,sha256=8oE9DiR1mb1WzqTdKNjfWDkZrSB_87mr7_HMnm9VJkY,20759
127
+ code_puppy-0.0.197.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
128
+ code_puppy-0.0.197.dist-info/entry_points.txt,sha256=Tp4eQC99WY3HOKd3sdvb22vZODRq0XkZVNpXOag_KdI,91
129
+ code_puppy-0.0.197.dist-info/licenses/LICENSE,sha256=31u8x0SPgdOq3izJX41kgFazWsM43zPEF9eskzqbJMY,1075
130
+ code_puppy-0.0.197.dist-info/RECORD,,