Skip to content

feat(plugin): sub-agent (Invoke Agent) CLI panel#486

Merged
AndrewTilson merged 8 commits into
mainfrom
feat/subagent-panel-plugin
Jun 17, 2026
Merged

feat(plugin): sub-agent (Invoke Agent) CLI panel#486
AndrewTilson merged 8 commits into
mainfrom
feat/subagent-panel-plugin

Conversation

@WSxDemise

Copy link
Copy Markdown
Collaborator

feat(plugin): sub-agent (Invoke Agent) CLI panel

The whole PR

Consolidates the sub-agent status UX into a single built-in plugin
(code_puppy/plugins/subagent_panel/) — replacing the older split
cp_steer_overlay + cp_subagent_status experiment — and hardens the
interrupt/teardown path around it so a single Ctrl+C cleanly stops a whole
sub-agent swarm
instead of forcing you to mash it once per nested agent.

Along the way it fixes a spinner teardown race, makes the clipboard image path
Windows-aware, and gates the spinner's raw ANSI writes on isatty() so
redirected / non-VT consoles (including legacy Windows shells) don't get escape
garbage. macOS interactive behavior is byte-for-byte unchanged.

Why

  • Ctrl+C used to need N presses. The sub-agent status panel renders inside
    the spinner's Rich Live (repaints ~20×/sec). On Ctrl+C the cancel banner
    printed once and the next Live frame painted the panel right back over it, so
    a single press looked like it did nothing. Worse, the SIGINT handler killed
    shells before tearing down the panel — and _kill_process_group blocks up
    to 2s per nested shell — so in a deep swarm the UI froze for N×2s while
    the panel kept repainting. Fix: tear down the Live panel + emit the banner
    before the slow blocking kill
    , then force-cancel the whole agent tree.
  • The cancel didn't reach sub-agents. The shell SIGINT handler had no way to
    reach the active run's cancel callback, so it only ever killed the current
    batch of shells. Added a register_agent_cancel / clear_agent_cancel bridge
    (set at run start, cleared in finally) so one Ctrl+C collapses the entire
    agent/sub-agent swarm.
  • Spinner teardown race. pause()/stop() can null self._live from
    another thread between the refresh guard and the refresh call — and the new
    cancel path triggers exactly that. Snapshot _live locally + guard the frame.
  • Windows portability. The clipboard now handles ImageGrab.grabclipboard()
    returning a file-path list (the Windows / Finder behavior), and the
    spinner's raw \x1b[K writes are gated on sys.stdout.isatty().

Files changed (22)

File Net change
code_puppy/plugins/subagent_panel/ (new plugin: register_callbacks, state, coalesce_patch, resume_repaint, README, init) +1331
code_puppy/tools/command_runner.py +110 / − (SIGINT reorder, cancel bridge, _tear_down_live_panels)
code_puppy/messaging/spinner/console_spinner.py +58 / − (live-snapshot race fix, isatty() ANSI gate)
code_puppy/command_line/clipboard.py +78 / − (file-list path, reusable PNG helpers; dropped orphan)
code_puppy/agents/_run_signals.py +21 / − (force= cancel param, panel teardown)
code_puppy/agents/_runtime.py +14 (register/clear cancel cb around the run)
code_puppy/plugins/__init__.py +10 / − (skip user plugins colliding with builtin names)
code_puppy/model_factory.py +5 (dormant advisor_tool_enabled beta header gate)
code_puppy/tools/image_tools.py +5 (docstring guardrail vs guessed image paths)
tests/** (swarm-cancel, spinner races, clipboard file-list, plugin-skip, image-tool guardrail, pause-guard, conftest) +569 / −

~2,134 insertions across 22 files.

Tests

  • Added tests/tools/test_command_runner_swarm_cancel.py — kill-before-cancel
    ordering (teardown → banner → kill → cancel), one-shot dedupe on mashed
    presses, headless fallback, force-cancel, panel-teardown resilience.
  • Added tests/test_console_spinner_races.py — live-snapshot teardown race,
    pause() clears _live even when stop() raises, frame-skip on stopped Live.
  • Added tests/command_line/test_clipboard_image_files.pyImageGrab
    file-list path + non-image suffix rejection on the live helper.
  • Added tests/plugins/test_duplicate_user_plugin_skip.py — builtin name
    suppresses same-named user plugin.
  • Added tests/tools/test_image_tool_guardrails.py — load-image docstring
    discourages guessed paths.
  • Extended tests/agents/test_run_signals_pause_guard.py — cancel tears down
    the panel before the banner; no teardown when no sub-agents.
  • All touched suites green locally (110 passed across the affected modules).

Manual validation

  1. Kick off a deep sub-agent swarm (children → grandchildren → great-grandchildren,
    each holding a sleep). Press Ctrl+C once → banner shows instantly, the
    panel vanishes, and the whole tree cancels (no per-agent mashing).
  2. Repeat with output piped to a file → no literal ←[K escape garbage.
  3. Verified on Windows (no os.killpg/SIGKILL evaluated — taskkill /F /T
    early-returns; clipboard file-list path works for ScreenClip paste).
  4. macOS interactive run → spinner/Live behavior unchanged.

Platform status

  • macOS: complete, dogfooded.
  • Windows: verified (signal path already guarded; isatty() gate + clipboard
    file-list make the new code Windows-clean).
  • Linux: expected to behave like macOS (POSIX path); not separately exercised.

History

This is the consolidation of the earlier cp_steer_overlay + cp_subagent_status
plugins into one subagent_panel plugin. Key pivots:

  • Kill-first → teardown-first SIGINT ordering. The first cut killed shells
    before hiding the panel; in a deep swarm the blocking per-process kill froze the
    UI for seconds while the panel repainted. Reordered to hide + announce before
    the slow kill.
  • Shells-only cancel → full-tree cancel bridge. Added the
    register/clear_agent_cancel bridge so Ctrl+C reaches every sub-agent, not just
    the current shell batch.
  • Dropped orphaned capture_image_file_to_pending. Its only prod consumer was
    the removed cp_steer_overlay; the reusable get_image_file_as_png helper stays
    live via get_clipboard_image's file-list branch.
  • advisor_tool_enabled is intentionally dormant (no config sets it yet) —
    scaffolding for a future Anthropic beta opt-in, gated so it stays inert.

PR Type

Enhancement, Bug fix


Description

  • Consolidate sub-agent status UX into built-in plugin with live two-line panel

  • Fix Ctrl+C to cleanly stop entire sub-agent swarm instead of requiring N presses

  • Harden spinner teardown race and gate ANSI writes on isatty() for Windows

  • Support Windows clipboard image-file paths and add steer overlay image paste


Diagram Walkthrough

flowchart LR
  A["Ctrl+C Signal"] --> B["Tear Down Live Panel"]
  B --> C["Emit Cancel Banner"]
  C --> D["Kill Shell Processes"]
  D --> E["Cancel Agent Tree"]
  E --> F["Swarm Stops"]
  
  G["Sub-agent Invocation"] --> H["Register in State"]
  H --> I["Render Live Block"]
  I --> J["Stream Events Update Status"]
  J --> K["On Completion: Freeze Record"]
  
  L["Windows Clipboard"] --> M["Handle File Paths"]
  M --> N["Convert to PNG"]
  N --> O["Attach to Message"]
Loading

File Walkthrough

Relevant files
Enhancement
11 files
_run_signals.py
Add force-cancel parameter to bypass shell-running guard 
+18/-3   
_runtime.py
Register and clear agent cancel callback around run           
+14/-0   
clipboard.py
Support Windows image-file paths and extract PNG helpers 
+56/-22 
model_factory.py
Add dormant advisor-tool beta flag support                             
+5/-0     
__init__.py
Skip user plugins when built-in name already loaded           
+6/-4     
__init__.py
New plugin module docstring and exports                                   
+24/-0   
coalesce_patch.py
Batch sub-agent stream events to prevent steer lag             
+236/-0 
register_callbacks.py
Install monkeypatches for live panel rendering and state 
+581/-0 
resume_repaint.py
Repaint panel after Ctrl+T steer overlay resumes                 
+167/-0 
state.py
Thread-safe registry of active sub-agents and status         
+223/-0 
command_runner.py
Implement responsive Ctrl+C swarm-cancel with panel teardown
+107/-3 
Bug fix
1 files
console_spinner.py
Fix teardown race and gate ANSI writes on isatty                 
+41/-17 
Documentation
2 files
README.md
Documentation for sub-agent panel plugin                                 
+100/-0 
image_tools.py
Add guardrail to discourage guessed image paths                   
+5/-0     
Tests
7 files
test_run_signals_pause_guard.py
Add tests for cancel panel teardown and force parameter   
+89/-0   
test_clipboard_image_files.py
New tests for Windows image-file clipboard handling           
+48/-0   
conftest.py
Improve MCP mock setup to prefer real package                       
+25/-19 
test_duplicate_user_plugin_skip.py
New regression test for duplicate plugin skip behavior     
+25/-0   
test_console_spinner_races.py
New tests for spinner teardown race conditions                     
+109/-0 
test_command_runner_swarm_cancel.py
New comprehensive tests for Ctrl+C swarm-cancel behavior 
+230/-0 
test_image_tool_guardrails.py
New test for load-image tool docstring guardrails               
+24/-0   

Wes Blakemore added 7 commits June 17, 2026 10:48
… collection

Probe the real mcp package (including client.session/sse/streamable_http)
before falling back to MagicMock stubs, so plugin test collection works
in both full dev envs and slim CI images without breaking pydantic_ai.mcp
imports.
Harden the console spinner against concurrent teardown of its Rich Live
display so racing stop/refresh calls during streaming no longer raise.
Detect image file paths on the clipboard and surface them so they can be
attached to a message, including platform-specific handling.
Clarify that load_image should only be used for concrete filesystem paths
the user provided, not for guessed paths when an image is already attached
to the conversation.
Append the advisor-tool-2026-03-01 beta header when a model config sets
advisor_tool_enabled. No model enables it yet, so the flag stays inert
until explicitly turned on.
Bridge the agent cancel callback into the shell SIGINT handler so one
Ctrl+C while shells are running kills the shells and then cancels every
agent/sub-agent task, instead of only stopping the current batch of
shells. The shell handler requests a forced cancel (it has already killed
the shells, so the anti-orphan guard no longer applies) and tears down any
live status panels first so the cancel banner is not immediately repainted.
The cancel callback is registered for the duration of a run and cleared in
a finally block.
Ship a built-in plugin that renders a live INVOKE AGENT status tree for
sub-agent runs, with stream-event coalescing and resume repaint support.
The plugin loader now skips a user plugin when a built-in (or project)
plugin of the same name has already loaded, preventing duplicate callback
registration. Includes a regression test for the duplicate-skip behavior.
@AndrewTilson AndrewTilson merged commit 0f1c5c2 into main Jun 17, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants