mpfaffenberger / mpfaffenberger/code_puppy
ThreadPoolExecutor + asyncio.run() boilerplate copy-pasted across 8 command handlers
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 814
- Forks
- 278
- Avg merge
- 2d 5h
- Merged PRs (30d)
- 76
Description
Problem
Eight command handlers in code_puppy/command_line/ spin up a brand-new ThreadPoolExecutor + asyncio.run(...) just to run one coroutine from a sync context, each with a copy-pasted comment block:
core_commands.py:201(/tutorial),:284(/agentpicker),:469(/modelpicker)config_commands.py:254(/setpicker),:578(diff picker),:608(colors picker)mcp_binding_menu.py:388uc_menu.py:900(bareasyncio.run, slightly different variant)
Representative copy (appears nearly verbatim three times in core_commands.py alone):
# Run the async picker using asyncio utilities
# Since we're called from an async context but this function is sync,
# we need to carefully schedule and wait for the coroutine
import concurrent.futures
with concurrent.futures.ThreadPoolExecutor() as executor:
future = executor.submit(lambda: asyncio.run(interactive_model_picker()))
selected_model = future.result(timeout=300) # 5 min timeout
Why it matters
- DRY / "one obvious way": the same 8-line dance, comments included, is pasted 8 times. Any fix (e.g., the timeout, error handling, terminal-state restoration) must be applied in 8 places.
- Subtle hazard: each call runs the picker on a different, transient event loop while the main loop is alive.
agent_menualready grew a_PENDING_PIN_RELOADSqueue workaround for exactly this (see comment atcore_commands.py:296-299) — proof the pattern is leaking complexity outward. future.result(timeout=300)raisesconcurrent.futures.TimeoutErrorwhile the thread keeps running the TUI — the prompt and the orphaned picker then fight over the terminal.
Suggested fix
Extract one helper, e.g. in command_line/utils.py:
def run_async_in_thread(coro_factory: Callable[[], Coroutine], timeout: float = 300):
"""Run an async TUI from a sync command handler on a fresh loop."""
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
return pool.submit(lambda: asyncio.run(coro_factory())).result(timeout=timeout)
and replace all 8 call sites. Longer term, consider making command handlers natively async so pickers run on the main loop and the _PENDING_PIN_RELOADS workaround can be deleted.
Filed by Zen Reviewer C (code-puppy-60635a)
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.
Research direction
Start with the repeated ThreadPoolExecutor and asyncio.run() blocks in command_line/core_commands.py, config_commands.py, mcp_binding_menu.py, and uc_menu.py, then inspect command_line/utils.py as the proposed helper location. Compare the eight listed call sites and the _PENDING_PIN_RELOADS workaround before replacing them with one helper. Done means all eight handlers use the shared path without changing picker behavior.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- cli
- Issue type
- Refactor
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 68/100