mpfaffenberger / mpfaffenberger/code_puppy

command_line/: 14 prompt_toolkit menus each reimplement alt-screen lifecycle, pagination keybindings, and split-panel scaffolding — extract a shared menu framework

Open
#449 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Python
Stars
814
Forks
278
Avg merge
2d 5h
Merged PRs (30d)
76

Description

Summary

The interactive menu family in command_line/ has no shared framework. Each menu independently reimplements the same five pieces of plumbing, and jscpd + manual review confirm the copies have already drifted (inconsistent keybindings, inconsistent pagination math, two different run-loop styles). This is the root cause behind several of the exact clones below and a major contributor to the 600-line-cap violations already filed as #409.

The menu family (all hand-rolling the same scaffolding)

File Lines Boilerplate (approx)
agent_menu.py 716 ~175 lines (kb setup 560-645, layout 540-560, alt-screen+loop 645-716)
autosave_menu.py 717 ~175 lines (kb setup 572-680, layout, alt-screen+loop 685-717)
judges_menu.py 809 ~140 lines (kb 665-745, alt-screen+loop 745-809)
uc_menu.py 908 ~200 lines (two KeyBindings sets 675-795, alt-screen+loop 798+)
add_model_menu.py 1412 ~150 lines (manual pagination, hints, alt-screen 1293+)
model_settings_menu.py 994 ~120 lines
mcp/install_menu.py 705 ~160 lines (manual page math 142-145 & 204-207, alt-screen 647+)
mcp/custom_server_form.py 701 ~100 lines (alt-screen 620+)
mcp_binding_menu.py 394 ~80 lines (2x Application, alt-screen 217, 332)
colors_menu.py / diff_menu.py 534 / 865 see companion issue on _split_panel_selector
model_picker_completion.py 609 alt-screen 523+
onboarding_wizard.py 375 alt-screen 225+
set_menu.py 532 Application + kb

Conservatively ~1,500-2,000 lines of repeated scaffolding across the family.

What every menu duplicates

  1. Alt-screen lifecycle — 13 files contain the identical sequence:

    set_awaiting_user_input(True)
    sys.stdout.write("\033[?1049h")
    sys.stdout.write("\033[2J\033[H")
    sys.stdout.flush()
    await asyncio.sleep(0.05)
    try: ...
    finally:
        sys.stdout.write("\033[?1049l")
        sys.stdout.flush()
        set_awaiting_user_input(False)
    

    (rg -c "1049h" command_line/ → 14 hits across 13 files.)

  2. Pagination keybindings on mutable-cell stateselected_idx[0] / current_page[0] up/down/left/right handlers calling ensure_visible_page(...). Exact clones found by jscpd:

    • agent_menu.py:576-595autosave_menu.py:604-623 (up/down handlers)
    • agent_menu.py:629-652autosave_menu.py:671-694 (enter/c-c + Application + alt-screen entry)
  3. Application(layout=..., key_bindings=kb, full_screen=False, mouse_support=False) — 14 occurrences, plus the pending_action[0] = ...; event.app.exit() → outer while True dispatch loop pattern (agent_menu 629-700, judges_menu 749-790, uc_menu 798+).

  4. Paginated list rendering with manual page math that bypasses the existing pagination.py:

    • mcp/install_menu.py:142-145 and :204-207: total_pages = (len(items) + PAGE_SIZE - 1) // PAGE_SIZEpagination.get_total_pages() exists for exactly this.
    • jscpd internal clone mcp/install_menu.py:166-183:224-241 — the category-list and server-list render loops are the same "prefix + icon + selected-style + page footer" pipeline with renamed variables.
  5. Navigation-hint footers_render_navigation_hints exists twice as near-identical methods: add_model_menu.py:474-499mcp/install_menu.py:243-258 (jscpd: add_model_menu.py:466-480install_menu.py:233-247); agent_menu/autosave_menu/judges_menu inline the same hint-tuple sequences.

Divergence evidence (the copies are already inconsistent)

  • autosave_menu.py:575-576 binds c-p/c-n as Emacs-style aliases for up/down; agent_menu.py:562/574 does not — same UI, different keys.
  • judges_menu.py:739-742 binds both escape and c-c to close; agent_menu.py only binds c-c (Esc does nothing in the agent picker).
  • mcp/install_menu.py:650 uses time.sleep(0.05) + app.run(in_thread=True) (sync); the rest use await asyncio.sleep(0.05) + await app.run_async().
  • install_menu/add_model_menu use class attributes for state; the others use 1-element list cells — two idioms for the same job ("one obvious way" violation).

Proposed abstraction: command_line/menu_framework.py

@asynccontextmanager
async def alt_screen_session():
    """Owns set_awaiting_user_input + alternate-buffer enter/clear/exit."""
    ...

@dataclass
class PaginatedListState:
    items: Sequence
    page_size: int
    selected: int = 0
    page: int = 0
    # move_up/move_down/page_prev/page_next built on pagination.py
    # current_item property; refresh(items, keep_name=...) for post-action reloads

def bind_list_navigation(kb: KeyBindings, state: PaginatedListState,
                         on_change: Callable, *, emacs_aliases=True): ...

def render_hints(lines: list, hints: list[tuple[str, str]]): ...

class SplitPanelMenu:
    """menu Frame + preview Frame in VSplit; subclasses provide
    render_menu(state), render_preview(item), and an actions dict
    {key: callable} replacing the pending_action[0] dance. run() owns
    the Application, alt_screen_session, and the action-dispatch loop."""

Migrating agent_menu + autosave_menu alone would delete ~300 duplicated lines and make Esc/Emacs-alias behavior consistent for free; the full family is a ~1.5k-line reduction and directly helps the #409 size-cap work.

Related (do not duplicate): #409 (file size cap), #429 (restore_autosave_interactively reimplements this yet again), #433 (keymap cancel/pause duplication).

Filed by Zen Reviewer B (code-puppy-60635a) — DRY review round

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by reading the duplicated scaffolding in command_line/agent_menu.py and command_line/autosave_menu.py, then inspect command_line/pagination.py and the other listed menus. Define the shared pieces in the proposed command_line/menu_framework.py and verify that the migrated menu family uses common lifecycle, navigation, pagination, and dispatch behavior without the repeated scaffolding.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
cli, tooling
Issue type
Refactor
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.