feat: add /stop command to cancel running Claude calls#116
feat: add /stop command to cancel running Claude calls#116F1orian wants to merge 16 commits intoRichardAtCT:mainfrom
Conversation
Allows users to immediately cancel an in-progress Claude call. The command cancels the asyncio task wrapping run_command(), cleans up the progress message, and logs the cancellation. Registered in both agentic and classic modes. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
task.cancel() only cancels the asyncio wrapper but leaves the Claude CLI subprocess running. Add abort() to ClaudeSDKManager and ClaudeIntegration facade that calls client.disconnect(), which terminates the underlying subprocess via SIGTERM. The /stop handler now calls abort() before task.cancel() to ensure the CLI process is killed immediately. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The original /stop implementation could never actually stop a running Claude call because PTB's concurrent_updates defaults to False, meaning the /stop handler was queued behind the still-running message handler. This commit fixes three issues: 1. Enable concurrent update processing (concurrent_updates=True) so /stop can run while a Claude call is in progress. 2. Track active calls per-thread instead of a single _claude_task on context.user_data. Each project thread gets its own entry in an _active_calls dict, so /stop in one channel doesn't affect another. 3. Replace the single _active_pid with a per-call PID dict and use process-tree killing (via /proc/*/children) instead of killpg to safely terminate the CLI and all its subagents without killing the bot's own process group. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
PR Review Summary
What looks good
Issues / questions
Suggested tests
Verdict — Friday, AI assistant to @RichardAtCT |
- Document concurrent_updates safety: all user_data reads in stop_command happen synchronously before the first await - Add platform guard in _get_descendants(): return [] on non-Linux so _kill_pid gracefully falls back to killing only the root process - Replace id(object()) with itertools.count() monotonic counter for call_id (avoids CPython address reuse) - Add unit tests: /stop with nothing running, thread isolation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Thanks for the thorough review! Pushed fixes for all three points: 1. 2. Linux-only 3. Tests added:
|
|
PR Review Summary
What looks good
Issues / questions
Suggested tests (if needed)
Verdict — Friday, AI assistant to @RichardAtCT |
Introduce CommandPaletteScanner that reads ~/.claude/ to discover installed plugins, their skills/commands, and custom user skills. Includes data models (ActionType, PaletteItem, PluginInfo), a YAML frontmatter parser, plugin toggle support, and 43 comprehensive tests. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Implement MenuBuilder (builds top-level and category sub-menus with short
IDs for Telegram's 64-byte callback_data limit), menu_command and
menu_callback handlers, and wire them into the agentic orchestrator.
- MenuBuilder: top-level menu (Bot, plugins 2-per-row, custom skills,
Plugin Store), category sub-menus with toggle and back buttons,
single-item plugins execute directly, multi-item open categories
- menu_command: scans filesystem via CommandPaletteScanner, sends
inline keyboard, stores builder in user_data for callback resolution
- menu_callback: handles back/cat/run/tog/store actions, re-scans
on navigation, sets pending skill/command for future Claude injection
- Orchestrator: registers /menu CommandHandler + menu: CallbackQueryHandler,
adds BotCommand("menu") to Telegram command menu
- 39 menu tests + 3 orchestrator integration tests (78 total, all pass)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When users tap skill buttons (/commit, /review-pr, custom skills), the menu now actually calls Claude via ClaudeIntegration.run_command() instead of just storing a pending value. Direct commands (/new, /status, /stop, /verbose, /repo) are executed inline with appropriate feedback. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Just catching up with my clanker! |
- Add get_enabled_plugin_paths() to command_palette.py to resolve installed plugin paths from ~/.claude/ config files - Pass enabled plugins to ClaudeAgentOptions via SdkPluginConfig so skills/commands from plugins are available in CLI sessions - Change setting_sources from ["project"] to ["project", "user"] so custom user skills (~/.claude/skills/) are also discovered - Fix menu skill invocation to send slash commands directly instead of hacky description-based prompts - Add message_thread_id to all send_message calls in menu handler to fix wrong-channel responses in supergroup forum topics - Add fallback confirmation message when skill produces no visible text output - Include design docs for the dynamic command menu feature Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
PR Review Summary
What looks good
Issues / questions
Suggested tests (if needed)
Verdict — Friday, AI assistant to @RichardAtCT |
Design for intercepting AskUserQuestion via PreToolUse SDK hooks and routing questions through Telegram inline keyboards. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
8-task plan with TDD steps for intercepting AskUserQuestion via PreToolUse hooks and routing through Telegram inline keyboards. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…oard builders Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…interactive questions Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…strator, and menu Thread telegram_context from Telegram handlers through facade.py and into sdk_integration.py so the PreToolUse hook for AskUserQuestion can send interactive inline keyboards to the user's chat. - sdk_integration.py: accept telegram_context param, register HookMatcher for AskUserQuestion, cancel pending questions in finally block - facade.py: thread telegram_context through run_command and _execute - orchestrator.py: build TelegramContext in agentic_text, register askq callback and other-text handlers in _register_agentic_handlers - menu.py: build TelegramContext in INJECT_SKILL section, move thread_id extraction before run_command - interactive_questions.py: use ApplicationHandlerStop instead of return True to prevent double-handling of consumed messages - Lazy imports in sdk_integration.py to avoid circular import with bot.features Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Update test_setting_sources_includes_project to match the setting_sources=["project", "user"] change. Apply black and isort formatting to all interactive questions files. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Review — new commits since last review (interactive AskUserQuestion feature) The interactive question routing via Race condition in multi-question flow
If a user taps "Other..." and then abandons the session,
The Minor: lazy imports inside hot paths
Overall the design is sound and the test coverage looks good. The race condition is the only one I'd call a real bug worth fixing before merge. — Friday, AI assistant to @RichardAtCT |
|
Closing — superseded by #122 which implements the same functionality (cancel running requests) via an inline Stop button rather than a /stop command. The inline approach provides better UX and a smaller diff. Thanks for the contribution! |
Summary
/stopcommand that immediately cancels a running Claude callconcurrent_updates(True)on the PTB Application so/stopcan run while a long-running Claude call is in progress (PTB defaults to sequential update processing, which blocked/stopbehind the active handler)_active_callsdict keyed by thread state_key, so/stopin one project thread doesn't affect another_active_pidwith per-call PID tracking (_active_pidsdict) and process-tree killing via/proc/<pid>/task/<pid>/childrento safely terminate the CLI and all its subagents without killing the bot's process groupChanges
src/bot/core.py— enableconcurrent_updates(True)src/bot/orchestrator.py— per-thread task tracking via_active_calls,_thread_key()helpersrc/bot/handlers/message.py— same per-thread tracking for classic modesrc/bot/handlers/command.py—/stopresolves correct thread, usesabort_call(call_id)src/claude/facade.py— threadcall_idthroughrun_command, addabort_call()src/claude/sdk_integration.py— per-call PID dict, process-tree kill (_get_descendants,_kill_pid),abort()for shutdown,abort_call()for targeted stopTest plan
/stopcancels running call immediately (confirmed via logs:success: true)/stopwith nothing running → "Nothing running." response/stopin one does not affect the other/stop(no process group kill)🤖 Generated with Claude Code