Plugin tier lacks a shared TUI toolkit: alternate-screen/menu-runner boilerplate copy-pasted across 10 menu files (and already drifted)
Nobody has claimed this yet.
Assessment
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Newbie friendliness
- 55/100
Research direction
Start with the duplicated alternate-screen blocks in command_line/mcp/install_menu.py, command_line/model_settings_menu.py, plugins/agent_skills/skills_menu.py, plugins/agent_skills/skills_install_menu.py, plugins/hook_manager/hooks_menu.py, and plugins/plugin_list/plugins_menu.py; use the listed rg command to find the remaining copies. Extract the shared menu runner and quit-binding behavior described in the issue, then update the affected menus so terminal cleanup and input flushing are consistent across them.
Written by the indexing model from the issue text.
Description
Theme
Every interactive menu in the repo re-implements the same prompt_toolkit "run a fullscreen-ish menu" dance by copy-paste. Because there is no shared helper, plugin authors clone core menus from command_line/, which is exactly what jscpd caught:
plugins/agent_skills/skills_menu.py:449-469↔command_line/mcp/install_menu.py:634-654plugins/agent_skills/skills_menu.py:440-459↔plugins/plugin_list/plugins_menu.py:267-286plugins/agent_skills/skills_install_menu.py:17-35↔skills_menu.py:8-26(import/page-size preamble)plugins/agent_skills/skills_install_menu.py:582-609↔command_line/model_settings_menu.py:915-942plugins/hook_manager/hooks_menu.py:505-518↔command_line/model_settings_menu.py:927-942
The cloned block is always the same ~30-line sequence:
layout = Layout(root_container)
app = Application(layout=layout, key_bindings=kb, full_screen=False, mouse_support=False)
set_awaiting_user_input(True)
sys.stdout.write("\033[?1049h") # enter alternate buffer
sys.stdout.write("\033[2J\033[H")
sys.stdout.flush()
time.sleep(0.05)
try:
self.update_display()
sys.stdout.write("\033[2J\033[H"); sys.stdout.flush()
app.run(in_thread=True)
finally:
sys.stdout.write("\033[?1049l")
sys.stdout.flush()
# (sometimes) termios.tcflush(...)
set_awaiting_user_input(False)
rg -l '1049h' code_puppy/ shows 20 files containing this escape-sequence dance; at least 9 of them pair it with the full Application(...) + app.run(in_thread=True) boilerplate:
command_line/mcp/install_menu.py,command_line/mcp/custom_server_form.py,command_line/model_settings_menu.py,command_line/add_model_menu.pyplugins/agent_skills/skills_menu.py,plugins/agent_skills/skills_install_menu.py,plugins/hook_manager/hooks_menu.py,plugins/plugin_list/plugins_menu.py,plugins/prune/prune_menu.py
Divergence evidence (the clones are already drifting)
skills_menu.py/skills_install_menu.py/hooks_menu.pyflush stale input viatermios.tcflush(...)infinally;model_settings_menu.pyandplugins_menu.pydon't — they can replay buffered keypresses into the next prompt.hooks_menu.py:498-503wrapsset_awaiting_user_input(True)intry/except Exception: pass; everyone else calls it bare. One copy got defensive, eight didn't.- Some copies
time.sleep(0.1)after exit to "let the terminal settle", others don't. - Quit keybindings differ subtly: some bind
q+escape+c-cto exit,mcp/install_menu.pytreatsescape/backspaceas "back" — fine, but the shared part (c-cexits, alternate-buffer restore) is re-typed each time, so any fix (e.g. issue #428-style terminal-state bugs) must be applied 9+ times.
Proposed shared abstraction
Add code_puppy/tui/ (or command_line/tui_toolkit.py) exporting a context manager + runner so menus can't diverge on terminal hygiene:
# code_puppy/tui/menu_runner.py
@contextmanager
def alternate_screen(flush_stale_input: bool = True):
set_awaiting_user_input(True)
sys.stdout.write("\x1b[?1049h\x1b[2J\x1b[H"); sys.stdout.flush()
time.sleep(0.05)
try:
yield
finally:
sys.stdout.write("\x1b[?1049l"); sys.stdout.flush()
if flush_stale_input:
_tcflush_stdin_safe()
set_awaiting_user_input(False)
def run_menu(root_container, kb, update_display) -> None:
app = Application(layout=Layout(root_container), key_bindings=kb,
full_screen=False, mouse_support=False)
with alternate_screen():
update_display()
sys.stdout.write("\x1b[2J\x1b[H"); sys.stdout.flush()
app.run(in_thread=True)
Plus a small add_quit_bindings(kb, on_quit) helper for the q/escape/c-c trio. Each menu file then shrinks by ~40 lines and inherits future terminal-state fixes for free. Given the CONTRIBUTING.md golden rule pushes new functionality into plugins, a sanctioned TUI toolkit is the only way plugin menus stop being forks of command_line/ menus.
Filed by Zen Reviewer C (code-puppy-60635a) — DRY review round
- Dominant language
- Python
- Stars
- 814
- Forks
- 278
- Avg merge
- 2d 5h
- Merged PRs (30d)
- 76
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from mpfaffenberger/code_puppy
-
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
mpfaffenberger/code_puppy#915 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
mpfaffenberger/code_puppy#532 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
mpfaffenberger/code_puppy#432 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
mpfaffenberger/code_puppy#427 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 74/100
mpfaffenberger/code_puppy#424 ·
All issues in mpfaffenberger/code_puppy
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 74/100
bancolombia/sentinel#23 ·
-
test md OpenCI
Difficulty 2/5 1-3 hours Newbie friendliness 74/100
-
integration:quickjs org:external priority:backlog topic:code-interpreter topic:middleware type:feature
Difficulty 2/5 1-3 hours Newbie friendliness 74/100
langchain-ai/deepagents#6450 ·
-
bug client
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 74/100