Adds a Stop button to the chat input actions bar (next to Pause Agent and Nudge) that cancels the currently running agent process in the active session and returns the agent to idle state.
Agent Zero has Pause and Nudge but no way to simply cancel a wrong command and go idle:
| Action | What it does | Agent state after |
|---|---|---|
| Pause Agent | Suspends the agent at the next checkpoint | Paused (resumes on Resume) |
| Nudge | Kills the current process and starts a new task with a nudge prompt | Running (new task) |
| Stop (this plugin) | Kills the current process and goes idle | Idle (waiting for input) |
The Stop button fills the gap: it fully terminates a wrong or unwanted command without pausing indefinitely or nudging (which re engages the agent).
- Stop button always visible in the bottom chat actions bar
- Gray/disabled when idle, red/active when a process is running
- Immediately terminates the running agent task
- Shows "⛔ Process stopped by user." warning message in the chat log
- Instant UI reset: input box returns to idle/waiting state immediately (no polling delay)
- Ghost process prevention: kills orphaned child OS processes (pager, less, etc.) spawned by terminal sessions
- Context isolation: only affects the active chat; other chats are never touched
- Toast notification confirming the action
- Copy the
stop_processfolder to/a0/usr/plugins/ - Enable the plugin in Settings → Plugins
- Refresh the page
Or install from the Plugin Hub in Agent Zero.
The plugin consists of two parts: a frontend UI component injected into the chat input actions bar and a backend API handler that performs the actual process termination.
User clicks Stop button
│
▼
┌─────────────────────────────────┐
│ Frontend (stop-button.html) │
│ Sends POST to backend API │
│ with current chat context ID │
└──────────────┬──────────────────┘
│
▼
┌─────────────────────────────────┐
│ Backend (stop_process.py) │
│ 1. Resolve context via │
│ self.use_context(ctxid) │
│ 2. Kill terminal child procs │
│ 3. context.kill_process() │
│ 4. context.paused = False │
│ 5. Log warning to chat │
└──────────────┬──────────────────┘
│
▼
┌─────────────────────────────────┐
│ Frontend receives response │
│ Sets running = false on store │
│ Input box resets to idle │
│ Toast notification shown │
└─────────────────────────────────┘
The HTML extension is injected into the chat-input-bottom-actions-end extension point, placing it alongside the existing Pause and Nudge buttons.
Visual states:
- When the agent is idle: button is grayed out (40% opacity,
not-allowedcursor, disabled) - When the agent is running: button turns red (#e74c3c), 80% opacity, clickable
Reactive bindings use Alpine.js:
:disabled="!$store.chats?.selectedContext?.running"prevents clicking when idle:class="{ 'stop-active': $store.chats?.selectedContext?.running }"toggles visual state
Immediate UI reset after a successful stop:
- Imports
chatsStorefrom the sidebar chats store - Sets
chatsStore.selectedContext.running = falsedirectly (reactive, propagates instantly) - Also triggers
globalThis.poll()after 300ms as a safety fallback
The API handler follows the same pattern as the built in Pause and Nudge handlers:
context = self.use_context(ctxid) # Resolve context with proper thread locking
await self._interrupt_terminal_sessions(context) # Kill ghost processes
context.kill_process() # Cancel the DeferredTask
context.paused = False # Ensure clean idle state
context.log.log(type="warning", content="⛔ Process stopped by user.")Key detail: uses self.use_context(ctxid) (NOT AgentContext.use()) to ensure proper thread locking and context resolution, consistent with the Pause and Nudge handlers.
When context.kill_process() is called, it cancels the Python DeferredTask (the asyncio Future). However, it does not terminate child OS processes spawned by code_execution_tool terminal sessions.
For example, if the agent runs git log in a terminal session, the shell spawns a pager process. When the DeferredTask is cancelled, the pager process becomes an orphan and keeps running at full CPU:
PID COMMAND CPU
18624 pager 90.9% ← orphaned, consuming CPU after Stop
18716 pager 90.9% ← orphaned, consuming CPU after Stop
Before calling context.kill_process(), the handler runs _interrupt_terminal_sessions() which:
- Accesses the terminal state via
agent.get_data("_cet_state")to get all active shell sessions - For each shell session, gets the
TTYSession._proc.pid(the shell's PID) - Scans
/procto find all child processes of that shell PID - Recursively kills the entire process tree (children of children first, then children) using
SIGKILL - Sends
Ctrl+C(\x03) to the shell itself to interrupt any running command
Shell (bash) PID 1234
├── git log PID 1235 ← killed by _kill_process_tree()
│ └── pager PID 1236 ← killed by _kill_process_tree()
└── (shell survives for future commands)
The Stop button only affects the active chat. This is guaranteed by the architecture:
| Layer | Scope |
|---|---|
| Frontend | Sends only the current chat's context ID via globalThis.getContext() |
| Backend | self.use_context(ctxid) retrieves only that specific AgentContext |
| Task kill | context.kill_process() kills only that context's DeferredTask |
| Terminal cleanup | agent.get_data("_cet_state") returns only that agent's terminal sessions |
| Process tree kill | Starts from shell PIDs of that agent only; other chats' PIDs are never reached |
Each chat has its own AgentContext (unique ID), its own Agent instance (agent0), its own terminal sessions, and its own DeferredTask. Clicking Stop in Chat A will never affect processes running in Chat B.
stop_process/
├── plugin.yaml # Plugin manifest
├── README.md # This file
├── LICENSE # MIT License
├── api/
│ ├── __init__.py # Python module marker
│ └── stop_process.py # Backend API handler
└── extensions/
└── webui/
└── chat-input-bottom-actions-end/
└── stop-button.html # Frontend UI component
Agent Zero v1.1+
MIT