code-puppy 0.0.361__py3-none-any.whl → 0.0.362__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.
@@ -0,0 +1,325 @@
1
+ """Interactive terminal UI for selecting agents.
2
+
3
+ Provides a split-panel interface for browsing and selecting agents
4
+ with live preview of agent details.
5
+ """
6
+
7
+ import sys
8
+ import time
9
+ from typing import List, Optional, Tuple
10
+
11
+ from prompt_toolkit.application import Application
12
+ from prompt_toolkit.key_binding import KeyBindings
13
+ from prompt_toolkit.layout import Dimension, Layout, VSplit, Window
14
+ from prompt_toolkit.layout.controls import FormattedTextControl
15
+ from prompt_toolkit.widgets import Frame
16
+
17
+ from code_puppy.agents import (
18
+ get_agent_descriptions,
19
+ get_available_agents,
20
+ get_current_agent,
21
+ )
22
+ from code_puppy.tools.command_runner import set_awaiting_user_input
23
+
24
+ PAGE_SIZE = 10 # Agents per page
25
+
26
+
27
+ def _get_agent_entries() -> List[Tuple[str, str, str]]:
28
+ """Get all agents with their display names and descriptions.
29
+
30
+ Returns:
31
+ List of tuples (agent_name, display_name, description) sorted by name.
32
+ """
33
+ available = get_available_agents()
34
+ descriptions = get_agent_descriptions()
35
+
36
+ entries = []
37
+ for name, display_name in available.items():
38
+ description = descriptions.get(name, "No description available")
39
+ entries.append((name, display_name, description))
40
+
41
+ # Sort alphabetically by agent name
42
+ entries.sort(key=lambda x: x[0].lower())
43
+ return entries
44
+
45
+
46
+ def _render_menu_panel(
47
+ entries: List[Tuple[str, str, str]],
48
+ page: int,
49
+ selected_idx: int,
50
+ current_agent_name: str,
51
+ ) -> List:
52
+ """Render the left menu panel with pagination.
53
+
54
+ Args:
55
+ entries: List of (name, display_name, description) tuples
56
+ page: Current page number (0-indexed)
57
+ selected_idx: Currently selected index (global)
58
+ current_agent_name: Name of the current active agent
59
+
60
+ Returns:
61
+ List of (style, text) tuples for FormattedTextControl
62
+ """
63
+ lines = []
64
+ total_pages = (len(entries) + PAGE_SIZE - 1) // PAGE_SIZE if entries else 1
65
+ start_idx = page * PAGE_SIZE
66
+ end_idx = min(start_idx + PAGE_SIZE, len(entries))
67
+
68
+ lines.append(("bold", " Agents"))
69
+ lines.append(("fg:ansibrightblack", f" (Page {page + 1}/{total_pages})"))
70
+ lines.append(("", "\n\n"))
71
+
72
+ if not entries:
73
+ lines.append(("fg:yellow", " No agents found."))
74
+ lines.append(("", "\n\n"))
75
+ else:
76
+ # Show agents for current page
77
+ for i in range(start_idx, end_idx):
78
+ name, display_name, _ = entries[i]
79
+ is_selected = i == selected_idx
80
+ is_current = name == current_agent_name
81
+
82
+ # Build the line
83
+ if is_selected:
84
+ lines.append(("fg:ansigreen", " ▶ "))
85
+ lines.append(("fg:ansigreen bold", display_name))
86
+ else:
87
+ lines.append(("", " "))
88
+ lines.append(("", display_name))
89
+
90
+ # Add current marker
91
+ if is_current:
92
+ lines.append(("fg:ansicyan", " ← current"))
93
+
94
+ lines.append(("", "\n"))
95
+
96
+ # Navigation hints
97
+ lines.append(("", "\n"))
98
+ lines.append(("fg:ansibrightblack", " ↑↓ "))
99
+ lines.append(("", "Navigate\n"))
100
+ lines.append(("fg:ansibrightblack", " ←→ "))
101
+ lines.append(("", "Page\n"))
102
+ lines.append(("fg:green", " Enter "))
103
+ lines.append(("", "Select\n"))
104
+ lines.append(("fg:ansibrightred", " Ctrl+C "))
105
+ lines.append(("", "Cancel"))
106
+
107
+ return lines
108
+
109
+
110
+ def _render_preview_panel(
111
+ entry: Optional[Tuple[str, str, str]],
112
+ current_agent_name: str,
113
+ ) -> List:
114
+ """Render the right preview panel with agent details.
115
+
116
+ Args:
117
+ entry: Tuple of (name, display_name, description) or None
118
+ current_agent_name: Name of the current active agent
119
+
120
+ Returns:
121
+ List of (style, text) tuples for FormattedTextControl
122
+ """
123
+ lines = []
124
+
125
+ lines.append(("dim cyan", " AGENT DETAILS"))
126
+ lines.append(("", "\n\n"))
127
+
128
+ if not entry:
129
+ lines.append(("fg:yellow", " No agent selected."))
130
+ lines.append(("", "\n"))
131
+ return lines
132
+
133
+ name, display_name, description = entry
134
+ is_current = name == current_agent_name
135
+
136
+ # Agent name (identifier)
137
+ lines.append(("bold", " Name: "))
138
+ lines.append(("", name))
139
+ lines.append(("", "\n\n"))
140
+
141
+ # Display name
142
+ lines.append(("bold", " Display Name: "))
143
+ lines.append(("fg:ansicyan", display_name))
144
+ lines.append(("", "\n\n"))
145
+
146
+ # Description
147
+ lines.append(("bold", " Description:"))
148
+ lines.append(("", "\n"))
149
+
150
+ # Wrap description to fit panel
151
+ desc_lines = description.split("\n")
152
+ for desc_line in desc_lines:
153
+ # Word wrap long lines
154
+ words = desc_line.split()
155
+ current_line = " "
156
+ for word in words:
157
+ if len(current_line) + len(word) + 1 > 60:
158
+ lines.append(("fg:ansibrightblack", current_line))
159
+ lines.append(("", "\n"))
160
+ current_line = " " + word
161
+ else:
162
+ if current_line == " ":
163
+ current_line += word
164
+ else:
165
+ current_line += " " + word
166
+ if current_line.strip():
167
+ lines.append(("fg:ansibrightblack", current_line))
168
+ lines.append(("", "\n"))
169
+
170
+ lines.append(("", "\n"))
171
+
172
+ # Current status
173
+ lines.append(("bold", " Status: "))
174
+ if is_current:
175
+ lines.append(("fg:ansigreen bold", "✓ Currently Active"))
176
+ else:
177
+ lines.append(("fg:ansibrightblack", "Not active"))
178
+ lines.append(("", "\n"))
179
+
180
+ return lines
181
+
182
+
183
+ async def interactive_agent_picker() -> Optional[str]:
184
+ """Show interactive terminal UI to select an agent.
185
+
186
+ Returns:
187
+ Agent name to switch to, or None if cancelled.
188
+ """
189
+ entries = _get_agent_entries()
190
+ current_agent = get_current_agent()
191
+ current_agent_name = current_agent.name if current_agent else ""
192
+
193
+ if not entries:
194
+ from code_puppy.messaging import emit_info
195
+
196
+ emit_info("No agents found.")
197
+ return None
198
+
199
+ # State
200
+ selected_idx = [0] # Current selection (global index)
201
+ current_page = [0] # Current page
202
+ result = [None] # Selected agent name
203
+
204
+ total_pages = (len(entries) + PAGE_SIZE - 1) // PAGE_SIZE
205
+
206
+ def get_current_entry() -> Optional[Tuple[str, str, str]]:
207
+ if 0 <= selected_idx[0] < len(entries):
208
+ return entries[selected_idx[0]]
209
+ return None
210
+
211
+ # Build UI
212
+ menu_control = FormattedTextControl(text="")
213
+ preview_control = FormattedTextControl(text="")
214
+
215
+ def update_display():
216
+ """Update both panels."""
217
+ menu_control.text = _render_menu_panel(
218
+ entries, current_page[0], selected_idx[0], current_agent_name
219
+ )
220
+ preview_control.text = _render_preview_panel(
221
+ get_current_entry(), current_agent_name
222
+ )
223
+
224
+ menu_window = Window(
225
+ content=menu_control, wrap_lines=True, width=Dimension(weight=35)
226
+ )
227
+ preview_window = Window(
228
+ content=preview_control, wrap_lines=True, width=Dimension(weight=65)
229
+ )
230
+
231
+ menu_frame = Frame(menu_window, width=Dimension(weight=35), title="Agents")
232
+ preview_frame = Frame(preview_window, width=Dimension(weight=65), title="Preview")
233
+
234
+ root_container = VSplit(
235
+ [
236
+ menu_frame,
237
+ preview_frame,
238
+ ]
239
+ )
240
+
241
+ # Key bindings
242
+ kb = KeyBindings()
243
+
244
+ @kb.add("up")
245
+ def _(event):
246
+ if selected_idx[0] > 0:
247
+ selected_idx[0] -= 1
248
+ # Update page if needed
249
+ current_page[0] = selected_idx[0] // PAGE_SIZE
250
+ update_display()
251
+
252
+ @kb.add("down")
253
+ def _(event):
254
+ if selected_idx[0] < len(entries) - 1:
255
+ selected_idx[0] += 1
256
+ # Update page if needed
257
+ current_page[0] = selected_idx[0] // PAGE_SIZE
258
+ update_display()
259
+
260
+ @kb.add("left")
261
+ def _(event):
262
+ if current_page[0] > 0:
263
+ current_page[0] -= 1
264
+ selected_idx[0] = current_page[0] * PAGE_SIZE
265
+ update_display()
266
+
267
+ @kb.add("right")
268
+ def _(event):
269
+ if current_page[0] < total_pages - 1:
270
+ current_page[0] += 1
271
+ selected_idx[0] = current_page[0] * PAGE_SIZE
272
+ update_display()
273
+
274
+ @kb.add("enter")
275
+ def _(event):
276
+ entry = get_current_entry()
277
+ if entry:
278
+ result[0] = entry[0] # Store agent name
279
+ event.app.exit()
280
+
281
+ @kb.add("c-c")
282
+ def _(event):
283
+ result[0] = None
284
+ event.app.exit()
285
+
286
+ layout = Layout(root_container)
287
+ app = Application(
288
+ layout=layout,
289
+ key_bindings=kb,
290
+ full_screen=False,
291
+ mouse_support=False,
292
+ )
293
+
294
+ set_awaiting_user_input(True)
295
+
296
+ # Enter alternate screen buffer once for entire session
297
+ sys.stdout.write("\033[?1049h") # Enter alternate buffer
298
+ sys.stdout.write("\033[2J\033[H") # Clear and home
299
+ sys.stdout.flush()
300
+ time.sleep(0.05)
301
+
302
+ try:
303
+ # Initial display
304
+ update_display()
305
+
306
+ # Clear the current buffer
307
+ sys.stdout.write("\033[2J\033[H")
308
+ sys.stdout.flush()
309
+
310
+ # Run application
311
+ await app.run_async()
312
+
313
+ finally:
314
+ # Exit alternate screen buffer once at end
315
+ sys.stdout.write("\033[?1049l") # Exit alternate buffer
316
+ sys.stdout.flush()
317
+ # Reset awaiting input flag
318
+ set_awaiting_user_input(False)
319
+
320
+ # Clear exit message
321
+ from code_puppy.messaging import emit_info
322
+
323
+ emit_info("✓ Exited agent picker")
324
+
325
+ return result[0]
@@ -6,6 +6,7 @@ discovered by the command registry system.
6
6
 
7
7
  import os
8
8
 
9
+ from code_puppy.command_line.agent_menu import interactive_agent_picker
9
10
  from code_puppy.command_line.command_registry import register_command
10
11
  from code_puppy.command_line.model_picker_completion import update_model_in_input
11
12
  from code_puppy.command_line.motd import print_motd
@@ -398,115 +399,6 @@ def handle_agent_command(command: str) -> bool:
398
399
  return True
399
400
 
400
401
 
401
- async def interactive_agent_picker() -> str | None:
402
- """Show an interactive arrow-key selector to pick an agent (async version).
403
-
404
- Returns:
405
- The selected agent name, or None if cancelled
406
- """
407
- import sys
408
- import time
409
-
410
- from rich.console import Console
411
- from rich.panel import Panel
412
- from rich.text import Text
413
-
414
- from code_puppy.agents import (
415
- get_agent_descriptions,
416
- get_available_agents,
417
- get_current_agent,
418
- )
419
- from code_puppy.tools.command_runner import set_awaiting_user_input
420
- from code_puppy.tools.common import arrow_select_async
421
-
422
- # Load available agents
423
- available_agents = get_available_agents()
424
- descriptions = get_agent_descriptions()
425
- current_agent = get_current_agent()
426
-
427
- # Build choices with current agent indicator and keep track of agent names
428
- choices = []
429
- agent_names = list(available_agents.keys())
430
- for agent_name in agent_names:
431
- display_name = available_agents[agent_name]
432
- if agent_name == current_agent.name:
433
- choices.append(f"✓ {agent_name} - {display_name} (current)")
434
- else:
435
- choices.append(f" {agent_name} - {display_name}")
436
-
437
- # Create preview callback to show agent description
438
- def get_preview(index: int) -> str:
439
- """Get the description for the agent at the given index."""
440
- agent_name = agent_names[index]
441
- description = descriptions.get(agent_name, "No description available")
442
- return description
443
-
444
- # Create panel content
445
- panel_content = Text()
446
- panel_content.append("🐶 Select an agent to use\n", style="bold cyan")
447
- panel_content.append("Current agent: ", style="dim")
448
- panel_content.append(f"{current_agent.name}", style="bold green")
449
- panel_content.append(" - ", style="dim")
450
- panel_content.append(current_agent.display_name, style="bold green")
451
- panel_content.append("\n", style="dim")
452
- panel_content.append(current_agent.description, style="dim italic")
453
-
454
- # Display panel
455
- panel = Panel(
456
- panel_content,
457
- title="[bold white]Agent Selection[/bold white]",
458
- border_style="cyan",
459
- padding=(1, 2),
460
- )
461
-
462
- # Pause spinners BEFORE showing panel
463
- set_awaiting_user_input(True)
464
- time.sleep(0.3) # Let spinners fully stop
465
-
466
- local_console = Console()
467
- emit_info("")
468
- local_console.print(panel)
469
- emit_info("")
470
-
471
- # Flush output before prompt_toolkit takes control
472
- sys.stdout.flush()
473
- sys.stderr.flush()
474
- time.sleep(0.1)
475
-
476
- selected_agent = None
477
-
478
- try:
479
- # Final flush
480
- sys.stdout.flush()
481
-
482
- # Show arrow-key selector with preview (async version)
483
- choice = await arrow_select_async(
484
- "💭 Which agent would you like to use?",
485
- choices,
486
- preview_callback=get_preview,
487
- )
488
-
489
- # Extract agent name from choice (remove prefix and suffix)
490
- if choice:
491
- # Remove the "✓ " or " " prefix and extract agent name (before " - ")
492
- choice_stripped = choice.strip().lstrip("✓").strip()
493
- # Split on " - " and take the first part (agent name)
494
- agent_name = choice_stripped.split(" - ")[0].strip()
495
- # Remove " (current)" suffix if present
496
- if agent_name.endswith(" (current)"):
497
- agent_name = agent_name[:-10].strip()
498
- selected_agent = agent_name
499
-
500
- except (KeyboardInterrupt, EOFError):
501
- emit_error("Cancelled by user")
502
- selected_agent = None
503
-
504
- finally:
505
- set_awaiting_user_input(False)
506
-
507
- return selected_agent
508
-
509
-
510
402
  async def interactive_model_picker() -> str | None:
511
403
  """Show an interactive arrow-key selector to pick a model (async version).
512
404
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-puppy
3
- Version: 0.0.361
3
+ Version: 0.0.362
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
@@ -67,6 +67,7 @@ code_puppy/api/routers/sessions.py,sha256=GqYRT7IJYPpEdTseLF3FIpbvvD86lIqwwPswL3
67
67
  code_puppy/api/templates/terminal.html,sha256=9alh6tTbLyXPDjBvkXw8nEWPXB-m_LIceGGRYpSLuyo,13125
68
68
  code_puppy/command_line/__init__.py,sha256=y7WeRemfYppk8KVbCGeAIiTuiOszIURCDjOMZv_YRmU,45
69
69
  code_puppy/command_line/add_model_menu.py,sha256=CpURhxPvUhLHLBV_uwH1ODfJ-WAcGklvlsjEf5Vfvg4,43255
70
+ code_puppy/command_line/agent_menu.py,sha256=A63k3jkr9htUmzR4v2ORWcyO-COzvbTwaIgDz_AnVK8,9468
70
71
  code_puppy/command_line/attachments.py,sha256=4Q5I2Es4j0ltnz5wjw2z0QXMsiMJvEfWRkPf_lJeITM,13093
71
72
  code_puppy/command_line/autosave_menu.py,sha256=de7nOmFmEH6x5T7C95U8N8xgxxeF-l5lgaJzGJsF3ZY,19824
72
73
  code_puppy/command_line/clipboard.py,sha256=oe9bfAX5RnT81FiYrDmhvHaePS1tAT-NFG1fSXubSD4,16869
@@ -74,7 +75,7 @@ code_puppy/command_line/colors_menu.py,sha256=LoFVfJ-Mo-Eq9hnb2Rj5mn7oBCnadAGr-8
74
75
  code_puppy/command_line/command_handler.py,sha256=CY9F27eovZJK_kpU1YmbroYLWGTCuouCOQ-TXfDp-nw,10916
75
76
  code_puppy/command_line/command_registry.py,sha256=qFySsw1g8dol3kgi0p6cXrIDlP11_OhOoaQ5nAadWXg,4416
76
77
  code_puppy/command_line/config_commands.py,sha256=qS9Cm758DPz2QGvHLhAV4Tp_Xfgo3PyoCoLDusbnmCw,25742
77
- code_puppy/command_line/core_commands.py,sha256=q5yi_xjPSoCjX8ET4PnMVsZlGwJ2PlEXVnao_7_1rJk,30619
78
+ code_puppy/command_line/core_commands.py,sha256=OF3u5MKmj78zEvX15k5BdcU9CbVTEJJYKz24HX_Dmtk,27088
78
79
  code_puppy/command_line/diff_menu.py,sha256=_Gr9SP9fbItk-08dya9WTAR53s_PlyAvEnbt-8VWKPk,24141
79
80
  code_puppy/command_line/file_path_completion.py,sha256=gw8NpIxa6GOpczUJRyh7VNZwoXKKn-yvCqit7h2y6Gg,2931
80
81
  code_puppy/command_line/load_context_completion.py,sha256=a3JvLDeLLSYxVgTjAdqWzS4spjv6ccCrK2LKZgVJ1IM,2202
@@ -206,10 +207,10 @@ code_puppy/tools/browser/chromium_terminal_manager.py,sha256=w1thQ_ACb6oV45L93TS
206
207
  code_puppy/tools/browser/terminal_command_tools.py,sha256=9byOZku-dwvTtCl532xt7Lumed_jTn0sLvUe_X75XCQ,19068
207
208
  code_puppy/tools/browser/terminal_screenshot_tools.py,sha256=J_21YO_495NvYgNFu9KQP6VYg2K_f8CtSdZuF94Yhnw,18448
208
209
  code_puppy/tools/browser/terminal_tools.py,sha256=F5LjVH3udSCFHmqC3O1UJLoLozZFZsEdX42jOmkqkW0,17853
209
- code_puppy-0.0.361.data/data/code_puppy/models.json,sha256=FMQdE_yvP_8y0xxt3K918UkFL9cZMYAqW1SfXcQkU_k,3105
210
- code_puppy-0.0.361.data/data/code_puppy/models_dev_api.json,sha256=wHjkj-IM_fx1oHki6-GqtOoCrRMR0ScK0f-Iz0UEcy8,548187
211
- code_puppy-0.0.361.dist-info/METADATA,sha256=AkNpl1g19rE0ZEPI-0MWepw8wFq_zLHhe8o_dVlYaRQ,27614
212
- code_puppy-0.0.361.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
213
- code_puppy-0.0.361.dist-info/entry_points.txt,sha256=Tp4eQC99WY3HOKd3sdvb22vZODRq0XkZVNpXOag_KdI,91
214
- code_puppy-0.0.361.dist-info/licenses/LICENSE,sha256=31u8x0SPgdOq3izJX41kgFazWsM43zPEF9eskzqbJMY,1075
215
- code_puppy-0.0.361.dist-info/RECORD,,
210
+ code_puppy-0.0.362.data/data/code_puppy/models.json,sha256=FMQdE_yvP_8y0xxt3K918UkFL9cZMYAqW1SfXcQkU_k,3105
211
+ code_puppy-0.0.362.data/data/code_puppy/models_dev_api.json,sha256=wHjkj-IM_fx1oHki6-GqtOoCrRMR0ScK0f-Iz0UEcy8,548187
212
+ code_puppy-0.0.362.dist-info/METADATA,sha256=3o2P9erqXHD7H0pAfDy_DTolwLVEkDatTTPDzHS2hY8,27614
213
+ code_puppy-0.0.362.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
214
+ code_puppy-0.0.362.dist-info/entry_points.txt,sha256=Tp4eQC99WY3HOKd3sdvb22vZODRq0XkZVNpXOag_KdI,91
215
+ code_puppy-0.0.362.dist-info/licenses/LICENSE,sha256=31u8x0SPgdOq3izJX41kgFazWsM43zPEF9eskzqbJMY,1075
216
+ code_puppy-0.0.362.dist-info/RECORD,,