code-puppy 0.0.196__py3-none-any.whl → 0.0.198__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:
@@ -101,6 +101,14 @@ class JSONAgent(BaseAgent):
101
101
  """Get tool configuration from JSON config."""
102
102
  return self._config.get("tools_config")
103
103
 
104
+ def refresh_config(self) -> None:
105
+ """Reload the agent configuration from disk.
106
+
107
+ This keeps long-lived agent instances in sync after external edits.
108
+ """
109
+ self._config = self._load_config()
110
+ self._validate_config()
111
+
104
112
  def get_model_name(self) -> Optional[str]:
105
113
  """Get pinned model name from JSON config, if specified.
106
114
 
@@ -5,131 +5,103 @@ from pathlib import Path
5
5
  from code_puppy.command_line.model_picker_completion import update_model_in_input
6
6
  from code_puppy.command_line.motd import print_motd
7
7
  from code_puppy.command_line.utils import make_directory_table
8
- from code_puppy.config import CONTEXTS_DIR, get_config_keys
8
+ from code_puppy.config import (
9
+ CONTEXTS_DIR,
10
+ finalize_autosave_session,
11
+ get_config_keys,
12
+ )
9
13
  from code_puppy.session_storage import list_sessions, load_session, save_session
10
14
  from code_puppy.tools.tools_content import tools_content
11
15
 
12
16
 
13
17
  def get_commands_help():
14
- """Generate commands help using Rich Text objects to avoid markup conflicts."""
18
+ """Generate aligned commands help using Rich Text for safe markup."""
15
19
  from rich.text import Text
16
20
 
17
21
  # Ensure plugins are loaded so custom help can register
18
22
  _ensure_plugins_loaded()
19
23
 
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
- )
24
+ # Collect core commands with their syntax parts and descriptions
25
+ # (cmd_syntax, description)
26
+ core_cmds = [
27
+ ("/help, /h", "Show this help message"),
28
+ ("/cd <dir>", "Change directory or show directories"),
29
+ (
30
+ "/agent <name>",
31
+ "Switch to a different agent or show available agents",
32
+ ),
33
+ ("/exit, /quit", "Exit interactive mode"),
34
+ ("/generate-pr-description [@dir]", "Generate comprehensive PR description"),
35
+ ("/model, /m <model>", "Set active model"),
36
+ ("/reasoning <low|medium|high>", "Set OpenAI reasoning effort for GPT-5 models"),
37
+ ("/pin_model <agent> <model>", "Pin a specific model to an agent"),
38
+ ("/mcp", "Manage MCP servers (list, start, stop, status, etc.)"),
39
+ ("/motd", "Show the latest message of the day (MOTD)"),
40
+ ("/show", "Show puppy config key-values"),
41
+ (
42
+ "/compact",
43
+ "Summarize and compact current chat history (uses compaction_strategy config)",
44
+ ),
45
+ ("/dump_context <name>", "Save current message history to file"),
46
+ ("/load_context <name>", "Load message history from file"),
47
+ (
48
+ "/set",
49
+ "Set puppy config (e.g., /set yolo_mode true, /set auto_save_session true)",
50
+ ),
51
+ ("/tools", "Show available tools and capabilities"),
52
+ (
53
+ "/truncate <N>",
54
+ "Truncate history to N most recent messages (keeping system message)",
55
+ ),
56
+ ("/<unknown>", "Show unknown command warning"),
57
+ ]
58
+
59
+ # Determine padding width for the left column
60
+ left_width = max(len(cmd) for cmd, _ in core_cmds) + 2 # add spacing
61
+
62
+ lines: list[Text] = []
63
+ lines.append(Text("Commands Help", style="bold magenta"))
64
+
65
+ for cmd, desc in core_cmds:
66
+ left = Text(cmd.ljust(left_width), style="cyan")
67
+ right = Text(desc)
68
+ line = Text()
69
+ line.append_text(left)
70
+ line.append_text(right)
71
+ lines.append(line)
102
72
 
103
73
  # Add custom commands from plugins (if any)
104
74
  try:
105
75
  from code_puppy import callbacks
106
76
 
107
77
  custom_help_results = callbacks.on_custom_command_help()
108
- # Flatten various returns into a list of (name, description)
109
- custom_entries = []
78
+ custom_entries: list[tuple[str, str]] = []
110
79
  for res in custom_help_results:
111
80
  if not res:
112
81
  continue
113
82
  if isinstance(res, tuple) and len(res) == 2:
114
- custom_entries.append(res)
83
+ custom_entries.append((str(res[0]), str(res[1])))
115
84
  elif isinstance(res, list):
116
85
  for item in res:
117
86
  if isinstance(item, tuple) and len(item) == 2:
118
- custom_entries.append(item)
87
+ custom_entries.append((str(item[0]), str(item[1])))
119
88
  if custom_entries:
120
- help_lines.append(Text("\n", style="dim"))
121
- help_lines.append(Text("Custom Commands", style="bold magenta"))
89
+ lines.append(Text("", style="dim"))
90
+ lines.append(Text("Custom Commands", style="bold magenta"))
91
+ # Compute padding for custom commands as well
92
+ custom_left_width = max(len(name) for name, _ in custom_entries) + 3
122
93
  for name, desc in custom_entries:
123
- help_lines.append(
124
- Text(f"/{name}", style="cyan") + Text(f" {desc}")
125
- )
94
+ left = Text(f"/{name}".ljust(custom_left_width), style="cyan")
95
+ right = Text(desc)
96
+ line = Text()
97
+ line.append_text(left)
98
+ line.append_text(right)
99
+ lines.append(line)
126
100
  except Exception:
127
- # If callbacks fail, skip custom help silently
128
101
  pass
129
102
 
130
- # Combine all lines
131
103
  final_text = Text()
132
- for i, line in enumerate(help_lines):
104
+ for i, line in enumerate(lines):
133
105
  if i > 0:
134
106
  final_text.append("\n")
135
107
  final_text.append_text(line)
@@ -461,31 +433,44 @@ def handle_command(command: str):
461
433
  import uuid
462
434
 
463
435
  group_id = str(uuid.uuid4())
436
+ available_agents = get_available_agents()
464
437
 
465
- if set_current_agent(agent_name):
466
- # Reload the agent with new configuration
467
- agent = get_current_agent()
468
- agent.reload_code_generation_agent()
469
- new_agent = get_current_agent()
470
- emit_success(
471
- f"Switched to agent: {new_agent.display_name}",
438
+ if agent_name not in available_agents:
439
+ emit_error(f"Agent '{agent_name}' not found", message_group=group_id)
440
+ emit_warning(
441
+ f"Available agents: {', '.join(available_agents.keys())}",
472
442
  message_group=group_id,
473
443
  )
474
- emit_info(f"[dim]{new_agent.description}[/dim]", message_group=group_id)
475
444
  return True
476
- else:
477
- # Generate a group ID for all messages in this command
478
- import uuid
479
445
 
480
- group_id = str(uuid.uuid4())
446
+ current_agent = get_current_agent()
447
+ if current_agent.name == agent_name:
448
+ emit_info(
449
+ f"Already using agent: {current_agent.display_name}",
450
+ message_group=group_id,
451
+ )
452
+ return True
481
453
 
482
- available_agents = get_available_agents()
483
- emit_error(f"Agent '{agent_name}' not found", message_group=group_id)
454
+ new_session_id = finalize_autosave_session()
455
+ if not set_current_agent(agent_name):
484
456
  emit_warning(
485
- f"Available agents: {', '.join(available_agents.keys())}",
457
+ "Agent switch failed after autosave rotation. Your context was preserved.",
486
458
  message_group=group_id,
487
459
  )
488
460
  return True
461
+
462
+ new_agent = get_current_agent()
463
+ new_agent.reload_code_generation_agent()
464
+ emit_success(
465
+ f"Switched to agent: {new_agent.display_name}",
466
+ message_group=group_id,
467
+ )
468
+ emit_info(f"[dim]{new_agent.description}[/dim]", message_group=group_id)
469
+ emit_info(
470
+ f"[dim]Auto-save session rotated to: {new_session_id}[/dim]",
471
+ message_group=group_id,
472
+ )
473
+ return True
489
474
  else:
490
475
  emit_warning("Usage: /agent [agent-name]")
491
476
  return True
@@ -625,12 +610,22 @@ def handle_command(command: str):
625
610
 
626
611
  emit_success(f"Model '{model_name}' pinned to agent '{agent_name}'")
627
612
 
628
- # If this is the current agent, reload it to use the new model
613
+ # If this is the current agent, refresh it so the prompt updates immediately
629
614
  from code_puppy.agents import get_current_agent
630
615
 
631
616
  current_agent = get_current_agent()
632
617
  if current_agent.name == agent_name:
633
- emit_info(f"Active agent reloaded with pinned model '{model_name}'")
618
+ try:
619
+ if is_json_agent and hasattr(current_agent, "refresh_config"):
620
+ current_agent.refresh_config()
621
+ current_agent.reload_code_generation_agent()
622
+ emit_info(
623
+ f"Active agent reloaded with pinned model '{model_name}'"
624
+ )
625
+ except Exception as reload_error:
626
+ emit_warning(
627
+ f"Pinned model applied but reload failed: {reload_error}"
628
+ )
634
629
 
635
630
  return True
636
631
 
@@ -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/config.py CHANGED
@@ -784,3 +784,9 @@ def auto_save_session_if_enabled() -> bool:
784
784
 
785
785
  Console().print(f"[dim]❌ Failed to auto-save session: {exc}[/dim]")
786
786
  return False
787
+
788
+
789
+ def finalize_autosave_session() -> str:
790
+ """Persist the current autosave snapshot and rotate to a fresh session."""
791
+ auto_save_session_if_enabled()
792
+ return rotate_autosave_id()
code_puppy/main.py CHANGED
@@ -24,6 +24,7 @@ from code_puppy.config import (
24
24
  AUTOSAVE_DIR,
25
25
  COMMAND_HISTORY_FILE,
26
26
  ensure_config_exists,
27
+ finalize_autosave_session,
27
28
  initialize_command_history_file,
28
29
  save_command_to_history,
29
30
  )
@@ -272,16 +273,13 @@ async def interactive_mode(message_renderer, initial_command: str = None) -> Non
272
273
  emit_info("[bold green]Code Puppy[/bold green] - Interactive Mode")
273
274
  emit_system_message("Type '/exit' or '/quit' to exit the interactive mode.")
274
275
  emit_system_message("Type 'clear' to reset the conversation history.")
276
+ emit_system_message("[dim]Type /help to view all commands[/dim]")
275
277
  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."
278
+ "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
279
  )
278
280
  emit_system_message(
279
281
  "Press [bold red]Ctrl+C[/bold red] during processing to cancel the current task or inference."
280
282
  )
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
283
  try:
286
284
  from code_puppy.command_line.motd import print_motd
287
285
 
@@ -417,12 +415,14 @@ async def interactive_mode(message_renderer, initial_command: str = None) -> Non
417
415
 
418
416
  # Check for clear command (supports both `clear` and `/clear`)
419
417
  if task.strip().lower() in ("clear", "/clear"):
420
- from code_puppy.messaging import emit_system_message, emit_warning
418
+ from code_puppy.messaging import emit_info, emit_system_message, emit_warning
421
419
 
422
420
  agent = get_current_agent()
421
+ new_session_id = finalize_autosave_session()
423
422
  agent.clear_message_history()
424
423
  emit_warning("Conversation history cleared!")
425
424
  emit_system_message("The agent will not remember previous interactions.\n")
425
+ emit_info(f"[dim]Auto-save session rotated to: {new_session_id}[/dim]")
426
426
  continue
427
427
 
428
428
  # Handle / commands before anything else
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.198
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.
@@ -1,9 +1,9 @@
1
1
  code_puppy/__init__.py,sha256=ehbM1-wMjNmOXk_DBhhJECFyBv2dRHwwo7ucjHeM68E,107
2
2
  code_puppy/__main__.py,sha256=pDVssJOWP8A83iFkxMLY9YteHYat0EyWDQqMkKHpWp4,203
3
3
  code_puppy/callbacks.py,sha256=ukSgVFaEO68o6J09qFwDrnmNanrVv3toTLQhS504Meo,6162
4
- code_puppy/config.py,sha256=S-VcC97Syz8j-bVNBr85DlhOr6exxAHQQR9JdHYOODs,25877
4
+ code_puppy/config.py,sha256=xT-nU1U4n7u8pyzJPG18-cJZBKv5OZI2CtHLt9DGRzU,26065
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=TIFaySHV3um9Q3BDUjCjh6s-WWqcNnTY3EsD2WdW6MQ,22245
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
31
- code_puppy/agents/json_agent.py,sha256=KPS1q-Rr3b5ekem4i3wtu8eLJRDd5nSPiZ8duJ_tn0U,4630
30
+ code_puppy/agents/base_agent.py,sha256=ikeV6Sui3HAagJBPTtI9T9pCDCFTCYDNEkFFw7XU21Y,39084
31
+ code_puppy/agents/json_agent.py,sha256=lhopDJDoiSGHvD8A6t50hi9ZBoNRKgUywfxd0Po_Dzc,4886
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=alxMe5v_4jq8Sm6HETsgfF-VoDtgExj9dVzxP77fwmY,31614
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.198.data/data/code_puppy/models.json,sha256=dClUciCo2RlVDs0ZAQCIur8MOavZUEAXHEecn0uPa-4,1629
126
+ code_puppy-0.0.198.dist-info/METADATA,sha256=p-54EnWxhZ2h9yAcVbjToU39Fqch7Si6kmSVHCtqAWA,20759
127
+ code_puppy-0.0.198.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
128
+ code_puppy-0.0.198.dist-info/entry_points.txt,sha256=Tp4eQC99WY3HOKd3sdvb22vZODRq0XkZVNpXOag_KdI,91
129
+ code_puppy-0.0.198.dist-info/licenses/LICENSE,sha256=31u8x0SPgdOq3izJX41kgFazWsM43zPEF9eskzqbJMY,1075
130
+ code_puppy-0.0.198.dist-info/RECORD,,