code-puppy 0.0.97__py3-none-any.whl → 0.0.119__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- code_puppy/__init__.py +2 -5
- code_puppy/__main__.py +10 -0
- code_puppy/agent.py +125 -40
- code_puppy/agent_prompts.py +30 -24
- code_puppy/callbacks.py +152 -0
- code_puppy/command_line/command_handler.py +359 -0
- code_puppy/command_line/load_context_completion.py +59 -0
- code_puppy/command_line/model_picker_completion.py +14 -21
- code_puppy/command_line/motd.py +44 -28
- code_puppy/command_line/prompt_toolkit_completion.py +42 -23
- code_puppy/config.py +266 -26
- code_puppy/http_utils.py +122 -0
- code_puppy/main.py +570 -383
- code_puppy/message_history_processor.py +195 -104
- code_puppy/messaging/__init__.py +46 -0
- code_puppy/messaging/message_queue.py +288 -0
- code_puppy/messaging/queue_console.py +293 -0
- code_puppy/messaging/renderers.py +305 -0
- code_puppy/messaging/spinner/__init__.py +55 -0
- code_puppy/messaging/spinner/console_spinner.py +200 -0
- code_puppy/messaging/spinner/spinner_base.py +66 -0
- code_puppy/messaging/spinner/textual_spinner.py +97 -0
- code_puppy/model_factory.py +73 -105
- code_puppy/plugins/__init__.py +32 -0
- code_puppy/reopenable_async_client.py +225 -0
- code_puppy/state_management.py +60 -21
- code_puppy/summarization_agent.py +56 -35
- code_puppy/token_utils.py +7 -9
- code_puppy/tools/__init__.py +1 -4
- code_puppy/tools/command_runner.py +187 -32
- code_puppy/tools/common.py +44 -35
- code_puppy/tools/file_modifications.py +335 -118
- code_puppy/tools/file_operations.py +368 -95
- code_puppy/tools/token_check.py +27 -11
- code_puppy/tools/tools_content.py +53 -0
- code_puppy/tui/__init__.py +10 -0
- code_puppy/tui/app.py +1050 -0
- code_puppy/tui/components/__init__.py +21 -0
- code_puppy/tui/components/chat_view.py +512 -0
- code_puppy/tui/components/command_history_modal.py +218 -0
- code_puppy/tui/components/copy_button.py +139 -0
- code_puppy/tui/components/custom_widgets.py +58 -0
- code_puppy/tui/components/input_area.py +167 -0
- code_puppy/tui/components/sidebar.py +309 -0
- code_puppy/tui/components/status_bar.py +182 -0
- code_puppy/tui/messages.py +27 -0
- code_puppy/tui/models/__init__.py +8 -0
- code_puppy/tui/models/chat_message.py +25 -0
- code_puppy/tui/models/command_history.py +89 -0
- code_puppy/tui/models/enums.py +24 -0
- code_puppy/tui/screens/__init__.py +13 -0
- code_puppy/tui/screens/help.py +130 -0
- code_puppy/tui/screens/settings.py +255 -0
- code_puppy/tui/screens/tools.py +74 -0
- code_puppy/tui/tests/__init__.py +1 -0
- code_puppy/tui/tests/test_chat_message.py +28 -0
- code_puppy/tui/tests/test_chat_view.py +88 -0
- code_puppy/tui/tests/test_command_history.py +89 -0
- code_puppy/tui/tests/test_copy_button.py +191 -0
- code_puppy/tui/tests/test_custom_widgets.py +27 -0
- code_puppy/tui/tests/test_disclaimer.py +27 -0
- code_puppy/tui/tests/test_enums.py +15 -0
- code_puppy/tui/tests/test_file_browser.py +60 -0
- code_puppy/tui/tests/test_help.py +38 -0
- code_puppy/tui/tests/test_history_file_reader.py +107 -0
- code_puppy/tui/tests/test_input_area.py +33 -0
- code_puppy/tui/tests/test_settings.py +44 -0
- code_puppy/tui/tests/test_sidebar.py +33 -0
- code_puppy/tui/tests/test_sidebar_history.py +153 -0
- code_puppy/tui/tests/test_sidebar_history_navigation.py +132 -0
- code_puppy/tui/tests/test_status_bar.py +54 -0
- code_puppy/tui/tests/test_timestamped_history.py +52 -0
- code_puppy/tui/tests/test_tools.py +82 -0
- code_puppy/version_checker.py +26 -3
- {code_puppy-0.0.97.dist-info → code_puppy-0.0.119.dist-info}/METADATA +9 -2
- code_puppy-0.0.119.dist-info/RECORD +86 -0
- code_puppy-0.0.97.dist-info/RECORD +0 -32
- {code_puppy-0.0.97.data → code_puppy-0.0.119.data}/data/code_puppy/models.json +0 -0
- {code_puppy-0.0.97.dist-info → code_puppy-0.0.119.dist-info}/WHEEL +0 -0
- {code_puppy-0.0.97.dist-info → code_puppy-0.0.119.dist-info}/entry_points.txt +0 -0
- {code_puppy-0.0.97.dist-info → code_puppy-0.0.119.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
from code_puppy.command_line.model_picker_completion import (
|
|
4
|
+
load_model_names,
|
|
5
|
+
update_model_in_input,
|
|
6
|
+
)
|
|
7
|
+
from code_puppy.command_line.motd import print_motd
|
|
8
|
+
from code_puppy.command_line.utils import make_directory_table
|
|
9
|
+
from code_puppy.config import get_config_keys
|
|
10
|
+
from code_puppy.tools.tools_content import tools_content
|
|
11
|
+
|
|
12
|
+
COMMANDS_HELP = """
|
|
13
|
+
[bold magenta]Commands Help[/bold magenta]
|
|
14
|
+
/help, /h Show this help message
|
|
15
|
+
/cd <dir> Change directory or show directories
|
|
16
|
+
|
|
17
|
+
/exit, /quit Exit interactive mode
|
|
18
|
+
/generate-pr-description [@dir] Generate comprehensive PR description
|
|
19
|
+
/m <model> Set active model
|
|
20
|
+
/motd Show the latest message of the day (MOTD)
|
|
21
|
+
/show Show puppy config key-values
|
|
22
|
+
/compact Summarize and compact current chat history
|
|
23
|
+
/dump_context <name> Save current message history to file
|
|
24
|
+
/load_context <name> Load message history from file
|
|
25
|
+
/set Set puppy config key-values (e.g., /set yolo_mode true)
|
|
26
|
+
/tools Show available tools and capabilities
|
|
27
|
+
/<unknown> Show unknown command warning
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def handle_command(command: str):
|
|
32
|
+
from code_puppy.messaging import emit_error, emit_info, emit_success, emit_warning
|
|
33
|
+
|
|
34
|
+
"""
|
|
35
|
+
Handle commands prefixed with '/'.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
command: The command string to handle
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
True if the command was handled, False if not, or a string to be processed as user input
|
|
42
|
+
"""
|
|
43
|
+
command = command.strip()
|
|
44
|
+
|
|
45
|
+
if command.strip().startswith("/motd"):
|
|
46
|
+
print_motd(force=True)
|
|
47
|
+
return True
|
|
48
|
+
|
|
49
|
+
if command.strip().startswith("/compact"):
|
|
50
|
+
from code_puppy.message_history_processor import (
|
|
51
|
+
estimate_tokens_for_message,
|
|
52
|
+
summarize_messages,
|
|
53
|
+
)
|
|
54
|
+
from code_puppy.messaging import (
|
|
55
|
+
emit_error,
|
|
56
|
+
emit_info,
|
|
57
|
+
emit_success,
|
|
58
|
+
emit_warning,
|
|
59
|
+
)
|
|
60
|
+
from code_puppy.state_management import get_message_history, set_message_history
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
history = get_message_history()
|
|
64
|
+
if not history:
|
|
65
|
+
emit_warning("No history to compact yet. Ask me something first!")
|
|
66
|
+
return True
|
|
67
|
+
|
|
68
|
+
before_tokens = sum(estimate_tokens_for_message(m) for m in history)
|
|
69
|
+
emit_info(
|
|
70
|
+
f"🤔 Compacting {len(history)} messages... (~{before_tokens} tokens)"
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
compacted, _ = summarize_messages(history, with_protection=False)
|
|
74
|
+
if not compacted:
|
|
75
|
+
emit_error("Summarization failed. History unchanged.")
|
|
76
|
+
return True
|
|
77
|
+
|
|
78
|
+
set_message_history(compacted)
|
|
79
|
+
|
|
80
|
+
after_tokens = sum(estimate_tokens_for_message(m) for m in compacted)
|
|
81
|
+
reduction_pct = (
|
|
82
|
+
((before_tokens - after_tokens) / before_tokens * 100)
|
|
83
|
+
if before_tokens > 0
|
|
84
|
+
else 0
|
|
85
|
+
)
|
|
86
|
+
emit_success(
|
|
87
|
+
f"✨ Done! History: {len(history)} → {len(compacted)} messages\n"
|
|
88
|
+
f"🏦 Tokens: {before_tokens:,} → {after_tokens:,} ({reduction_pct:.1f}% reduction)"
|
|
89
|
+
)
|
|
90
|
+
return True
|
|
91
|
+
except Exception as e:
|
|
92
|
+
emit_error(f"/compact error: {e}")
|
|
93
|
+
return True
|
|
94
|
+
|
|
95
|
+
if command.startswith("/cd"):
|
|
96
|
+
tokens = command.split()
|
|
97
|
+
if len(tokens) == 1:
|
|
98
|
+
try:
|
|
99
|
+
table = make_directory_table()
|
|
100
|
+
emit_info(table)
|
|
101
|
+
except Exception as e:
|
|
102
|
+
emit_error(f"Error listing directory: {e}")
|
|
103
|
+
return True
|
|
104
|
+
elif len(tokens) == 2:
|
|
105
|
+
dirname = tokens[1]
|
|
106
|
+
target = os.path.expanduser(dirname)
|
|
107
|
+
if not os.path.isabs(target):
|
|
108
|
+
target = os.path.join(os.getcwd(), target)
|
|
109
|
+
if os.path.isdir(target):
|
|
110
|
+
os.chdir(target)
|
|
111
|
+
emit_success(f"Changed directory to: {target}")
|
|
112
|
+
else:
|
|
113
|
+
emit_error(f"Not a directory: {dirname}")
|
|
114
|
+
return True
|
|
115
|
+
|
|
116
|
+
if command.strip().startswith("/show"):
|
|
117
|
+
from code_puppy.command_line.model_picker_completion import get_active_model
|
|
118
|
+
from code_puppy.config import (
|
|
119
|
+
get_owner_name,
|
|
120
|
+
get_protected_token_count,
|
|
121
|
+
get_puppy_name,
|
|
122
|
+
get_summarization_threshold,
|
|
123
|
+
get_yolo_mode,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
puppy_name = get_puppy_name()
|
|
127
|
+
owner_name = get_owner_name()
|
|
128
|
+
model = get_active_model()
|
|
129
|
+
yolo_mode = get_yolo_mode()
|
|
130
|
+
protected_tokens = get_protected_token_count()
|
|
131
|
+
summary_threshold = get_summarization_threshold()
|
|
132
|
+
|
|
133
|
+
status_msg = f"""[bold magenta]🐶 Puppy Status[/bold magenta]
|
|
134
|
+
|
|
135
|
+
[bold]puppy_name:[/bold] [cyan]{puppy_name}[/cyan]
|
|
136
|
+
[bold]owner_name:[/bold] [cyan]{owner_name}[/cyan]
|
|
137
|
+
[bold]model:[/bold] [green]{model}[/green]
|
|
138
|
+
[bold]YOLO_MODE:[/bold] {"[red]ON[/red]" if yolo_mode else "[yellow]off[/yellow]"}
|
|
139
|
+
[bold]protected_tokens:[/bold] [cyan]{protected_tokens:,}[/cyan] recent tokens preserved
|
|
140
|
+
[bold]summary_threshold:[/bold] [cyan]{summary_threshold:.1%}[/cyan] context usage triggers summarization
|
|
141
|
+
|
|
142
|
+
"""
|
|
143
|
+
emit_info(status_msg)
|
|
144
|
+
return True
|
|
145
|
+
|
|
146
|
+
if command.startswith("/set"):
|
|
147
|
+
# Syntax: /set KEY=VALUE or /set KEY VALUE
|
|
148
|
+
from code_puppy.config import set_config_value
|
|
149
|
+
|
|
150
|
+
tokens = command.split(None, 2)
|
|
151
|
+
argstr = command[len("/set") :].strip()
|
|
152
|
+
key = None
|
|
153
|
+
value = None
|
|
154
|
+
if "=" in argstr:
|
|
155
|
+
key, value = argstr.split("=", 1)
|
|
156
|
+
key = key.strip()
|
|
157
|
+
value = value.strip()
|
|
158
|
+
elif len(tokens) >= 3:
|
|
159
|
+
key = tokens[1]
|
|
160
|
+
value = tokens[2]
|
|
161
|
+
elif len(tokens) == 2:
|
|
162
|
+
key = tokens[1]
|
|
163
|
+
value = ""
|
|
164
|
+
else:
|
|
165
|
+
emit_warning(
|
|
166
|
+
f"Usage: /set KEY=VALUE or /set KEY VALUE\nConfig keys: {', '.join(get_config_keys())}"
|
|
167
|
+
)
|
|
168
|
+
return True
|
|
169
|
+
if key:
|
|
170
|
+
set_config_value(key, value)
|
|
171
|
+
emit_success(f'🌶 Set {key} = "{value}" in puppy.cfg!')
|
|
172
|
+
else:
|
|
173
|
+
emit_error("You must supply a key.")
|
|
174
|
+
return True
|
|
175
|
+
|
|
176
|
+
if command.startswith("/tools"):
|
|
177
|
+
# Display the tools_content.py file content with markdown formatting
|
|
178
|
+
from rich.markdown import Markdown
|
|
179
|
+
|
|
180
|
+
markdown_content = Markdown(tools_content)
|
|
181
|
+
emit_info(markdown_content)
|
|
182
|
+
return True
|
|
183
|
+
|
|
184
|
+
if command.startswith("/m"):
|
|
185
|
+
# Try setting model and show confirmation
|
|
186
|
+
new_input = update_model_in_input(command)
|
|
187
|
+
if new_input is not None:
|
|
188
|
+
from code_puppy.agent import get_code_generation_agent
|
|
189
|
+
from code_puppy.command_line.model_picker_completion import get_active_model
|
|
190
|
+
|
|
191
|
+
model = get_active_model()
|
|
192
|
+
# Make sure this is called for the test
|
|
193
|
+
get_code_generation_agent(force_reload=True)
|
|
194
|
+
emit_success(f"Active model set and loaded: {model}")
|
|
195
|
+
return True
|
|
196
|
+
# If no model matched, show available models
|
|
197
|
+
model_names = load_model_names()
|
|
198
|
+
emit_warning("Usage: /m <model-name>")
|
|
199
|
+
emit_warning(f"Available models: {', '.join(model_names)}")
|
|
200
|
+
return True
|
|
201
|
+
if command in ("/help", "/h"):
|
|
202
|
+
emit_info(COMMANDS_HELP)
|
|
203
|
+
return True
|
|
204
|
+
|
|
205
|
+
if command.startswith("/generate-pr-description"):
|
|
206
|
+
# Parse directory argument (e.g., /generate-pr-description @some/dir)
|
|
207
|
+
tokens = command.split()
|
|
208
|
+
directory_context = ""
|
|
209
|
+
for t in tokens:
|
|
210
|
+
if t.startswith("@"):
|
|
211
|
+
directory_context = f" Please work in the directory: {t[1:]}"
|
|
212
|
+
break
|
|
213
|
+
|
|
214
|
+
# Hard-coded prompt from user requirements
|
|
215
|
+
pr_prompt = f"""Generate a comprehensive PR description for my current branch changes. Follow these steps:
|
|
216
|
+
|
|
217
|
+
1 Discover the changes: Use git CLI to find the base branch (usually main/master/develop) and get the list of changed files, commits, and diffs.
|
|
218
|
+
2 Analyze the code: Read and analyze all modified files to understand:
|
|
219
|
+
• What functionality was added/changed/removed
|
|
220
|
+
• The technical approach and implementation details
|
|
221
|
+
• Any architectural or design pattern changes
|
|
222
|
+
• Dependencies added/removed/updated
|
|
223
|
+
3 Generate a structured PR description with these sections:
|
|
224
|
+
• Title: Concise, descriptive title (50 chars max)
|
|
225
|
+
• Summary: Brief overview of what this PR accomplishes
|
|
226
|
+
• Changes Made: Detailed bullet points of specific changes
|
|
227
|
+
• Technical Details: Implementation approach, design decisions, patterns used
|
|
228
|
+
• Files Modified: List of key files with brief description of changes
|
|
229
|
+
• Testing: What was tested and how (if applicable)
|
|
230
|
+
• Breaking Changes: Any breaking changes (if applicable)
|
|
231
|
+
• Additional Notes: Any other relevant information
|
|
232
|
+
4 Create a markdown file: Generate a PR_DESCRIPTION.md file with proper GitHub markdown formatting that I can directly copy-paste into GitHub's PR
|
|
233
|
+
description field. Use proper markdown syntax with headers, bullet points, code blocks, and formatting.
|
|
234
|
+
5 Make it review-ready: Ensure the description helps reviewers understand the context, approach, and impact of the changes.
|
|
235
|
+
6. If you have Github MCP, or gh cli is installed and authenticated then find the PR for the branch we analyzed and update the PR description there and then delete the PR_DESCRIPTION.md file. (If you have a better name (title) for the PR, go ahead and update the title too.{directory_context}"""
|
|
236
|
+
|
|
237
|
+
# Return the prompt to be processed by the main chat system
|
|
238
|
+
return pr_prompt
|
|
239
|
+
|
|
240
|
+
if command.startswith("/dump_context"):
|
|
241
|
+
import json
|
|
242
|
+
import pickle
|
|
243
|
+
from datetime import datetime
|
|
244
|
+
from pathlib import Path
|
|
245
|
+
|
|
246
|
+
from code_puppy.config import CONFIG_DIR
|
|
247
|
+
from code_puppy.message_history_processor import estimate_tokens_for_message
|
|
248
|
+
from code_puppy.state_management import get_message_history
|
|
249
|
+
|
|
250
|
+
tokens = command.split()
|
|
251
|
+
if len(tokens) != 2:
|
|
252
|
+
emit_warning("Usage: /dump_context <session_name>")
|
|
253
|
+
return True
|
|
254
|
+
|
|
255
|
+
session_name = tokens[1]
|
|
256
|
+
history = get_message_history()
|
|
257
|
+
|
|
258
|
+
if not history:
|
|
259
|
+
emit_warning("No message history to dump!")
|
|
260
|
+
return True
|
|
261
|
+
|
|
262
|
+
# Create contexts directory inside CONFIG_DIR if it doesn't exist
|
|
263
|
+
contexts_dir = Path(CONFIG_DIR) / "contexts"
|
|
264
|
+
contexts_dir.mkdir(parents=True, exist_ok=True)
|
|
265
|
+
|
|
266
|
+
try:
|
|
267
|
+
# Save as pickle for exact preservation
|
|
268
|
+
pickle_file = contexts_dir / f"{session_name}.pkl"
|
|
269
|
+
with open(pickle_file, "wb") as f:
|
|
270
|
+
pickle.dump(history, f)
|
|
271
|
+
|
|
272
|
+
# Also save metadata as JSON for readability
|
|
273
|
+
meta_file = contexts_dir / f"{session_name}_meta.json"
|
|
274
|
+
metadata = {
|
|
275
|
+
"session_name": session_name,
|
|
276
|
+
"timestamp": datetime.now().isoformat(),
|
|
277
|
+
"message_count": len(history),
|
|
278
|
+
"total_tokens": sum(estimate_tokens_for_message(m) for m in history),
|
|
279
|
+
"file_path": str(pickle_file),
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
with open(meta_file, "w") as f:
|
|
283
|
+
json.dump(metadata, f, indent=2)
|
|
284
|
+
|
|
285
|
+
emit_success(
|
|
286
|
+
f"✅ Context saved: {len(history)} messages ({metadata['total_tokens']} tokens)\n"
|
|
287
|
+
f"📁 Files: {pickle_file}, {meta_file}"
|
|
288
|
+
)
|
|
289
|
+
return True
|
|
290
|
+
|
|
291
|
+
except Exception as e:
|
|
292
|
+
emit_error(f"Failed to dump context: {e}")
|
|
293
|
+
return True
|
|
294
|
+
|
|
295
|
+
if command.startswith("/load_context"):
|
|
296
|
+
import pickle
|
|
297
|
+
from pathlib import Path
|
|
298
|
+
|
|
299
|
+
from code_puppy.config import CONFIG_DIR
|
|
300
|
+
from code_puppy.message_history_processor import estimate_tokens_for_message
|
|
301
|
+
from code_puppy.state_management import set_message_history
|
|
302
|
+
|
|
303
|
+
tokens = command.split()
|
|
304
|
+
if len(tokens) != 2:
|
|
305
|
+
emit_warning("Usage: /load_context <session_name>")
|
|
306
|
+
return True
|
|
307
|
+
|
|
308
|
+
session_name = tokens[1]
|
|
309
|
+
contexts_dir = Path(CONFIG_DIR) / "contexts"
|
|
310
|
+
pickle_file = contexts_dir / f"{session_name}.pkl"
|
|
311
|
+
|
|
312
|
+
if not pickle_file.exists():
|
|
313
|
+
emit_error(f"Context file not found: {pickle_file}")
|
|
314
|
+
# List available contexts
|
|
315
|
+
available = list(contexts_dir.glob("*.pkl"))
|
|
316
|
+
if available:
|
|
317
|
+
names = [f.stem for f in available]
|
|
318
|
+
emit_info(f"Available contexts: {', '.join(names)}")
|
|
319
|
+
return True
|
|
320
|
+
|
|
321
|
+
try:
|
|
322
|
+
with open(pickle_file, "rb") as f:
|
|
323
|
+
history = pickle.load(f)
|
|
324
|
+
|
|
325
|
+
set_message_history(history)
|
|
326
|
+
total_tokens = sum(estimate_tokens_for_message(m) for m in history)
|
|
327
|
+
|
|
328
|
+
emit_success(
|
|
329
|
+
f"✅ Context loaded: {len(history)} messages ({total_tokens} tokens)\n"
|
|
330
|
+
f"📁 From: {pickle_file}"
|
|
331
|
+
)
|
|
332
|
+
return True
|
|
333
|
+
|
|
334
|
+
except Exception as e:
|
|
335
|
+
emit_error(f"Failed to load context: {e}")
|
|
336
|
+
return True
|
|
337
|
+
|
|
338
|
+
if command in ("/exit", "/quit"):
|
|
339
|
+
emit_success("Goodbye!")
|
|
340
|
+
# Signal to the main app that we want to exit
|
|
341
|
+
# The actual exit handling is done in main.py
|
|
342
|
+
return True
|
|
343
|
+
if command.startswith("/"):
|
|
344
|
+
name = command[1:].split()[0] if len(command) > 1 else ""
|
|
345
|
+
if name:
|
|
346
|
+
emit_warning(
|
|
347
|
+
f"Unknown command: {command}\n[dim]Type /help for options.[/dim]"
|
|
348
|
+
)
|
|
349
|
+
else:
|
|
350
|
+
# Show current model ONLY here
|
|
351
|
+
from code_puppy.command_line.model_picker_completion import get_active_model
|
|
352
|
+
|
|
353
|
+
current_model = get_active_model()
|
|
354
|
+
emit_info(
|
|
355
|
+
f"[bold green]Current Model:[/bold green] [cyan]{current_model}[/cyan]"
|
|
356
|
+
)
|
|
357
|
+
return True
|
|
358
|
+
|
|
359
|
+
return False
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from prompt_toolkit.completion import Completer, Completion
|
|
4
|
+
|
|
5
|
+
from code_puppy.config import CONFIG_DIR
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class LoadContextCompleter(Completer):
|
|
9
|
+
def __init__(self, trigger: str = "/load_context"):
|
|
10
|
+
self.trigger = trigger
|
|
11
|
+
|
|
12
|
+
def get_completions(self, document, complete_event):
|
|
13
|
+
text_before_cursor = document.text_before_cursor
|
|
14
|
+
stripped_text_for_trigger_check = text_before_cursor.lstrip()
|
|
15
|
+
|
|
16
|
+
if not stripped_text_for_trigger_check.startswith(self.trigger):
|
|
17
|
+
return
|
|
18
|
+
|
|
19
|
+
# Determine the part of the text that is relevant for this completer
|
|
20
|
+
actual_trigger_pos = text_before_cursor.find(self.trigger)
|
|
21
|
+
effective_input = text_before_cursor[actual_trigger_pos:]
|
|
22
|
+
|
|
23
|
+
tokens = effective_input.split()
|
|
24
|
+
|
|
25
|
+
# Case 1: Input is exactly the trigger (e.g., "/load_context") and nothing more
|
|
26
|
+
if (
|
|
27
|
+
len(tokens) == 1
|
|
28
|
+
and tokens[0] == self.trigger
|
|
29
|
+
and not effective_input.endswith(" ")
|
|
30
|
+
):
|
|
31
|
+
yield Completion(
|
|
32
|
+
text=self.trigger + " ",
|
|
33
|
+
start_position=-len(tokens[0]),
|
|
34
|
+
display=self.trigger + " ",
|
|
35
|
+
display_meta="load saved context",
|
|
36
|
+
)
|
|
37
|
+
return
|
|
38
|
+
|
|
39
|
+
# Case 2: Input is trigger + space or trigger + partial session name
|
|
40
|
+
session_filter = ""
|
|
41
|
+
if len(tokens) > 1: # e.g., ["/load_context", "partial"]
|
|
42
|
+
session_filter = tokens[1]
|
|
43
|
+
|
|
44
|
+
# Get available context files
|
|
45
|
+
try:
|
|
46
|
+
contexts_dir = Path(CONFIG_DIR) / "contexts"
|
|
47
|
+
if contexts_dir.exists():
|
|
48
|
+
for pkl_file in contexts_dir.glob("*.pkl"):
|
|
49
|
+
session_name = pkl_file.stem # removes .pkl extension
|
|
50
|
+
if session_name.startswith(session_filter):
|
|
51
|
+
yield Completion(
|
|
52
|
+
session_name,
|
|
53
|
+
start_position=-len(session_filter),
|
|
54
|
+
display=session_name,
|
|
55
|
+
display_meta="saved context session",
|
|
56
|
+
)
|
|
57
|
+
except Exception:
|
|
58
|
+
# Silently ignore errors (e.g., permission issues, non-existent dir)
|
|
59
|
+
pass
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import json
|
|
2
1
|
import os
|
|
3
2
|
from typing import Iterable, Optional
|
|
4
3
|
|
|
@@ -8,17 +7,13 @@ from prompt_toolkit.document import Document
|
|
|
8
7
|
from prompt_toolkit.history import FileHistory
|
|
9
8
|
|
|
10
9
|
from code_puppy.config import get_model_name, set_model_name
|
|
11
|
-
|
|
12
|
-
MODELS_JSON_PATH = os.environ.get("MODELS_JSON_PATH")
|
|
13
|
-
if not MODELS_JSON_PATH:
|
|
14
|
-
MODELS_JSON_PATH = os.path.join(os.path.dirname(__file__), "..", "models.json")
|
|
15
|
-
MODELS_JSON_PATH = os.path.abspath(MODELS_JSON_PATH)
|
|
10
|
+
from code_puppy.model_factory import ModelFactory
|
|
16
11
|
|
|
17
12
|
|
|
18
13
|
def load_model_names():
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
return list(
|
|
14
|
+
"""Load model names from the config that's fetched from the endpoint."""
|
|
15
|
+
models_config = ModelFactory.load_config()
|
|
16
|
+
return list(models_config.keys())
|
|
22
17
|
|
|
23
18
|
|
|
24
19
|
def get_active_model():
|
|
@@ -31,11 +26,9 @@ def get_active_model():
|
|
|
31
26
|
|
|
32
27
|
def set_active_model(model_name: str):
|
|
33
28
|
"""
|
|
34
|
-
Sets the active model name by updating
|
|
35
|
-
and env (for process lifetime override).
|
|
29
|
+
Sets the active model name by updating the config (for persistence).
|
|
36
30
|
"""
|
|
37
31
|
set_model_name(model_name)
|
|
38
|
-
os.environ["MODEL_NAME"] = model_name.strip()
|
|
39
32
|
# Reload agent globally
|
|
40
33
|
try:
|
|
41
34
|
from code_puppy.agent import reload_code_generation_agent
|
|
@@ -47,11 +40,11 @@ def set_active_model(model_name: str):
|
|
|
47
40
|
|
|
48
41
|
class ModelNameCompleter(Completer):
|
|
49
42
|
"""
|
|
50
|
-
A completer that triggers on '
|
|
51
|
-
Only '
|
|
43
|
+
A completer that triggers on '/m' to show available models from models.json.
|
|
44
|
+
Only '/m' (not just '/') will trigger the dropdown.
|
|
52
45
|
"""
|
|
53
46
|
|
|
54
|
-
def __init__(self, trigger: str = "
|
|
47
|
+
def __init__(self, trigger: str = "/m"):
|
|
55
48
|
self.trigger = trigger
|
|
56
49
|
self.model_names = load_model_names()
|
|
57
50
|
|
|
@@ -77,23 +70,23 @@ class ModelNameCompleter(Completer):
|
|
|
77
70
|
|
|
78
71
|
|
|
79
72
|
def update_model_in_input(text: str) -> Optional[str]:
|
|
80
|
-
# If input starts with
|
|
73
|
+
# If input starts with /m and a model name, set model and strip it out
|
|
81
74
|
content = text.strip()
|
|
82
|
-
if content.startswith("
|
|
75
|
+
if content.startswith("/m"):
|
|
83
76
|
rest = content[2:].strip()
|
|
84
77
|
for model in load_model_names():
|
|
85
78
|
if rest == model:
|
|
86
79
|
set_active_model(model)
|
|
87
|
-
# Remove
|
|
88
|
-
idx = text.find("
|
|
80
|
+
# Remove /mmodel from the input
|
|
81
|
+
idx = text.find("/m" + model)
|
|
89
82
|
if idx != -1:
|
|
90
|
-
new_text = (text[:idx] + text[idx + len("
|
|
83
|
+
new_text = (text[:idx] + text[idx + len("/m" + model) :]).strip()
|
|
91
84
|
return new_text
|
|
92
85
|
return None
|
|
93
86
|
|
|
94
87
|
|
|
95
88
|
async def get_input_with_model_completion(
|
|
96
|
-
prompt_str: str = ">>> ", trigger: str = "
|
|
89
|
+
prompt_str: str = ">>> ", trigger: str = "/m", history_file: Optional[str] = None
|
|
97
90
|
) -> str:
|
|
98
91
|
history = FileHistory(os.path.expanduser(history_file)) if history_file else None
|
|
99
92
|
session = PromptSession(
|
code_puppy/command_line/motd.py
CHANGED
|
@@ -1,35 +1,25 @@
|
|
|
1
1
|
"""
|
|
2
|
-
MOTD (Message of the Day) feature for code-puppy
|
|
3
|
-
Stores seen versions in ~/.
|
|
2
|
+
🐶 MOTD (Message of the Day) feature for code-puppy! 🐕
|
|
3
|
+
Stores seen versions in ~/.code_puppy/motd.txt - woof woof! 🐾
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
6
|
import os
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
🐾 Happy Sunday, Aug 17, 2025!
|
|
12
|
-
|
|
13
|
-
Biscuit the code puppy learned two new tricks!
|
|
14
|
-
Major paws-ups:
|
|
15
|
-
1. On-the-fly summarization: when your model's context hits 90%,
|
|
16
|
-
Biscuit auto-summarizes older messages to keep you cruising. No sweat, no tokens spilled.
|
|
17
|
-
2. AGENT.md support: ship your project rules and style guide,
|
|
18
|
-
and Biscuit will obey them like the good pup he is.
|
|
19
|
-
|
|
20
|
-
• Use ~m to swap models mid-session.
|
|
21
|
-
• YOLO_MODE=true skips command confirmations (danger, zoomies!).
|
|
22
|
-
• Keep files under 600 lines; split big ones like a responsible hooman.
|
|
23
|
-
• DRY code, happy pup.
|
|
24
|
-
|
|
25
|
-
Today's vibe: sniff context, summarize smartly, obey AGENT.md, and ship.
|
|
26
|
-
Run ~motd anytime you need more puppy hype!
|
|
8
|
+
from code_puppy.config import CONFIG_DIR
|
|
9
|
+
from code_puppy.messaging import emit_info
|
|
27
10
|
|
|
11
|
+
MOTD_VERSION = "2025-08-24"
|
|
12
|
+
MOTD_MESSAGE = """🐕🦺
|
|
13
|
+
🐾```
|
|
14
|
+
# 🐶🎉🐕 WOOF WOOF! AUGUST 24th 🐕🎉🐶
|
|
15
|
+
40k Downloads! Woot!
|
|
16
|
+
Thanks for your support!
|
|
17
|
+
-Mike
|
|
28
18
|
"""
|
|
29
|
-
MOTD_TRACK_FILE = os.path.
|
|
19
|
+
MOTD_TRACK_FILE = os.path.join(CONFIG_DIR, "motd.txt")
|
|
30
20
|
|
|
31
21
|
|
|
32
|
-
def has_seen_motd(version: str) -> bool:
|
|
22
|
+
def has_seen_motd(version: str) -> bool: # 🐕 Check if puppy has seen this MOTD!
|
|
33
23
|
if not os.path.exists(MOTD_TRACK_FILE):
|
|
34
24
|
return False
|
|
35
25
|
with open(MOTD_TRACK_FILE, "r") as f:
|
|
@@ -37,15 +27,41 @@ def has_seen_motd(version: str) -> bool:
|
|
|
37
27
|
return version in seen_versions
|
|
38
28
|
|
|
39
29
|
|
|
40
|
-
def mark_motd_seen(version: str):
|
|
30
|
+
def mark_motd_seen(version: str): # 🐶 Mark MOTD as seen by this good puppy!
|
|
31
|
+
# Create directory if it doesn't exist 🏠🐕
|
|
41
32
|
os.makedirs(os.path.dirname(MOTD_TRACK_FILE), exist_ok=True)
|
|
42
|
-
with open(MOTD_TRACK_FILE, "a") as f:
|
|
43
|
-
f.write(f"{version}\n")
|
|
44
33
|
|
|
34
|
+
# Check if the version is already in the file 📋🐶
|
|
35
|
+
seen_versions = set()
|
|
36
|
+
if os.path.exists(MOTD_TRACK_FILE):
|
|
37
|
+
with open(MOTD_TRACK_FILE, "r") as f:
|
|
38
|
+
seen_versions = {line.strip() for line in f if line.strip()}
|
|
39
|
+
|
|
40
|
+
# Only add the version if it's not already there 📝🐕🦺
|
|
41
|
+
if version not in seen_versions:
|
|
42
|
+
with open(MOTD_TRACK_FILE, "a") as f:
|
|
43
|
+
f.write(f"{version}\n")
|
|
45
44
|
|
|
46
|
-
|
|
45
|
+
|
|
46
|
+
def print_motd(
|
|
47
|
+
console=None, force: bool = False
|
|
48
|
+
) -> bool: # 🐶 Print exciting puppy MOTD!
|
|
49
|
+
"""
|
|
50
|
+
🐕 Print the message of the day to the user - woof woof! 🐕
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
console: Optional console object (for backward compatibility) 🖥️🐶
|
|
54
|
+
force: Whether to force printing even if the MOTD has been seen 💪🐕🦺
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
True if the MOTD was printed, False otherwise 🐾
|
|
58
|
+
"""
|
|
47
59
|
if force or not has_seen_motd(MOTD_VERSION):
|
|
48
|
-
|
|
60
|
+
# Create a Rich Markdown object for proper rendering 🎨🐶
|
|
61
|
+
from rich.markdown import Markdown
|
|
62
|
+
|
|
63
|
+
markdown_content = Markdown(MOTD_MESSAGE)
|
|
64
|
+
emit_info(markdown_content)
|
|
49
65
|
mark_motd_seen(MOTD_VERSION)
|
|
50
66
|
return True
|
|
51
67
|
return False
|