mpfaffenberger / mpfaffenberger/code_puppy

ThreadPoolExecutor + asyncio.run() boilerplate copy-pasted across 8 command handlers

Open
#414 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

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 (/agent picker), :469 (/model picker)
  • config_commands.py:254 (/set picker), :578 (diff picker), :608 (colors picker)
  • mcp_binding_menu.py:388
  • uc_menu.py:900 (bare asyncio.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_menu already grew a _PENDING_PIN_RELOADS queue workaround for exactly this (see comment at core_commands.py:296-299) — proof the pattern is leaking complexity outward.
  • future.result(timeout=300) raises concurrent.futures.TimeoutError while 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

  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 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.