Skip to content

Commit 681e184

Browse files
authored
fix(cli): Abort functionality, autocomplete, and fixed memory prompts (#246)
* fix(cli): Fix @ file autocomplete - directory navigation and backspace - Directories now auto-add / and continue showing contents - Backspace updates completions instead of closing menu - File parsing stops at punctuation (@README.md? → README.md) DirectoryAwarePathCompleter wraps PathCompleter to detect and add / to directories. Backspace key binding retriggers completions. Regex: [A-Za-z0-9._/~-]+ File: input.py * fix(cli): Fix bash command interrupt handling Use signal handlers to properly handle Ctrl+C during bash command execution. Previously, Ctrl+C would exit the entire CLI; now it only stops the command. - Install custom SIGINT handler during bash command execution - Kill subprocess immediately when Ctrl+C is pressed - Set stdin=DEVNULL to prevent interactive commands from hanging - Restore asyncio's signal handler after command completes * fix(cli): Improve HITL approval UI and fix double rendering - Add "User Approval Required:" header with cyan color - Add visual separation for better readability - Deduplicate action_requests to prevent double rendering - Keep minimal arrow-based navigation * fix(prompt): Tone down aggressive memory-first protocol - Remove "at session start" automatic memory check - Remove all-caps ALWAYS/FIRST emphasis - Make memory checks contextual rather than mandatory - Keep functionality but reduce eagerness * fix(cli): Fix double rendering in approval menu cursor positioning Remove conditional cursor movement logic that caused options to render twice. Now consistently returns cursor to top of options after each render. * fix(cli): Hide cursor during agent execution and approval menus Hides blinking cursor during: - Agent streaming and tool execution - Approval menu navigation Cursor is restored when returning to user input prompt. * fix(memory): Remove aggressive memory-checking prompts - Remove "CRITICAL - do this FIRST" and session-start triggers - Remove "Memory-first response pattern" that forced checking before every question - Remove all-caps "IMMEDIATELY" and "HIGH PRIORITY" language - Make memory checks contextual rather than automatic - Keep learning and update functionality but with gentler language * fix(cli): Fix default prompt path in reset command Updated path to correctly locate default_agent_prompt.md in src/deepagents/ instead of looking in libs/deepagents-cli/ * fix(cli): Move default_agent_prompt.md to correct location Moved default_agent_prompt.md from src/deepagents/ to libs/deepagents-cli/ where the config.py expects to find it. Reverted the path hack. * fix(cli): Remove extra blank line between messages Removed duplicate console.print() that was creating excessive spacing between agent response and next prompt. * fix(cli): Fix terminal resize bug causing repeated prompts Added prompt_continuation parameter to PromptSession to prevent the main prompt (>) from being repeated on continuation lines during terminal resize. Now continuation lines show two spaces instead of repeating >. * fix(cli): Remove bottom toolbar to fix terminal resize bug Removed bottom_toolbar from PromptSession which was causing prompts to render on every line during terminal resize. The toolbar + multiline combination was confusing prompt_toolkit's space calculation. Changes: - Removed bottom_toolbar and style parameters from PromptSession - Updated Ctrl+T to show status message when toggled - Auto-approve status still shown at startup in welcome message * fix(cli): Add terminal state reset and reserve_space_for_menu=0 Additional fixes for terminal resize bug: - Set prompt_continuation to empty string (no continuation prompts) - Add reserve_space_for_menu=0 to prevent extra vertical space allocation - Flush stdout/stderr and reset ANSI attributes before entering prompt loop - Ensures clean terminal state for prompt_toolkit * fix(cli): Restore bottom toolbar for auto-approve status Brought back the bottom toolbar with colored status indicator: - Green background when auto-approve is ON - Orange background when manual approve (OFF) - Updates automatically when toggling with Ctrl+T - Kept other resize fixes (reserve_space_for_menu=0, empty continuation, state reset) * add memory tests and fix unnecessary printing of warnings * fix(cli): Add terminal state flush to prevent prompt desync - Flush stdout/stderr after agent execution - Reset ANSI attributes to clean state - Prevents Rich console from corrupting terminal state * fix(cli): Remove approval requirement for web_search tool web_search is a read-only tool that fetches public information and should not require user approval before execution. This change makes it behave like http_request - executing automatically and showing results. Previously, web_search would pause for approval, and sending a new message would cancel the pending approval, creating a frustrating loop where users could never get search results. Changes: - Removed web_search from interrupt_on configuration - Deleted unused format_web_search_description function - Added comment explaining why web_search doesn't require approval * fix(cli): Show diffs in auto-approve mode for file operations When auto-approve is enabled, file edits and writes now display their diffs before executing, providing visibility into what changes are being made automatically. Previously, auto-approve mode would only show a brief message without any diff preview, making it hard to see what the agent was changing. Changes: - Build approval preview in auto-approve flow - Display diff using render_diff_block for file operations - Show preview title and details before auto-approving - Maintains same UX for non-file operations * fix(cli): Reserve space for autocomplete menu Calculate dynamic rows for menu visibility based on terminal height * fix(cli): Flush console output before resuming in auto-approve Prevents Rich console from interfering with agent resume
1 parent 2d83441 commit 681e184

File tree

10 files changed

+629
-212
lines changed

10 files changed

+629
-212
lines changed

libs/deepagents-cli/deepagents_cli/agent.py

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -199,14 +199,6 @@ def format_edit_file_description(tool_call: dict) -> str:
199199
f"Snippet delta: {delta:+} characters"
200200
)
201201

202-
def format_web_search_description(tool_call: dict) -> str:
203-
"""Format web_search tool call for approval prompt."""
204-
args = tool_call.get("args", {})
205-
query = args.get("query", "unknown")
206-
max_results = args.get("max_results", 5)
207-
208-
return f"Query: {query}\nMax results: {max_results}\n\n⚠️ This will use Tavily API credits"
209-
210202
def format_task_description(tool_call: dict) -> str:
211203
"""Format task (subagent) tool call for approval prompt."""
212204
args = tool_call.get("args", {})
@@ -248,11 +240,6 @@ def format_task_description(tool_call: dict) -> str:
248240
"description": lambda tool_call, state, runtime: format_edit_file_description(tool_call),
249241
}
250242

251-
web_search_interrupt_config: InterruptOnConfig = {
252-
"allowed_decisions": ["approve", "reject"],
253-
"description": lambda tool_call, state, runtime: format_web_search_description(tool_call),
254-
}
255-
256243
task_interrupt_config: InterruptOnConfig = {
257244
"allowed_decisions": ["approve", "reject"],
258245
"description": lambda tool_call, state, runtime: format_task_description(tool_call),
@@ -268,8 +255,9 @@ def format_task_description(tool_call: dict) -> str:
268255
"shell": shell_interrupt_config,
269256
"write_file": write_file_interrupt_config,
270257
"edit_file": edit_file_interrupt_config,
271-
"web_search": web_search_interrupt_config,
272258
"task": task_interrupt_config,
259+
# Note: web_search is NOT here - it's a read-only tool like http_request
260+
# and should execute automatically without requiring approval
273261
},
274262
).with_config(config)
275263

libs/deepagents-cli/deepagents_cli/commands.py

Lines changed: 77 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
"""Command handlers for slash commands and bash execution."""
22

3+
import os
4+
import signal
35
import subprocess
46
from pathlib import Path
57

@@ -57,33 +59,92 @@ def execute_bash_command(command: str) -> bool:
5759
if not cmd:
5860
return True
5961

62+
process = None
63+
interrupted = False
64+
original_handler = None
65+
66+
def sigint_handler(signum, frame):
67+
"""Custom SIGINT handler - kills subprocess immediately and sets flag."""
68+
nonlocal interrupted, process
69+
interrupted = True
70+
71+
# Kill subprocess immediately when Ctrl+C is pressed
72+
if process and process.poll() is None:
73+
try:
74+
if hasattr(os, "killpg"):
75+
os.killpg(os.getpgid(process.pid), signal.SIGINT)
76+
else:
77+
process.send_signal(signal.SIGINT)
78+
except (ProcessLookupError, OSError):
79+
pass
80+
6081
try:
82+
# Install our signal handler (temporarily overrides asyncio's handler)
83+
original_handler = signal.signal(signal.SIGINT, sigint_handler)
84+
6185
console.print()
6286
console.print(f"[dim]$ {cmd}[/dim]")
6387

64-
# Execute the command
65-
result = subprocess.run(
66-
cmd, check=False, shell=True, capture_output=True, text=True, timeout=30, cwd=Path.cwd()
88+
# Create subprocess in new session (process group) to isolate from parent's signals
89+
process = subprocess.Popen(
90+
cmd,
91+
shell=True,
92+
text=True,
93+
stdin=subprocess.DEVNULL, # Close stdin to prevent interactive commands from hanging
94+
stdout=subprocess.PIPE,
95+
stderr=subprocess.PIPE,
96+
start_new_session=True, # Isolate subprocess in its own process group
97+
cwd=Path.cwd(),
6798
)
6899

69-
# Display output
70-
if result.stdout:
71-
console.print(result.stdout, style=COLORS["dim"], markup=False)
72-
if result.stderr:
73-
console.print(result.stderr, style="red", markup=False)
74-
75-
# Show return code if non-zero
76-
if result.returncode != 0:
77-
console.print(f"[dim]Exit code: {result.returncode}[/dim]")
100+
try:
101+
# Wait for command to complete with timeout
102+
stdout, stderr = process.communicate(timeout=30)
103+
104+
# Display output
105+
if stdout:
106+
console.print(stdout, style=COLORS["dim"], markup=False)
107+
if stderr:
108+
console.print(stderr, style="red", markup=False)
109+
110+
# Check if interrupted via our flag
111+
if interrupted:
112+
console.print("\n[yellow]Command interrupted by user[/yellow]\n")
113+
elif process.returncode != 0:
114+
# Exit code 130 = 128 + SIGINT (2) - command was interrupted
115+
# Exit code -2 also indicates interrupt in some shells
116+
if process.returncode == 130 or process.returncode == -2:
117+
console.print("[yellow]Command interrupted[/yellow]")
118+
else:
119+
console.print(f"[dim]Exit code: {process.returncode}[/dim]")
120+
121+
except subprocess.TimeoutExpired:
122+
# Timeout - kill the process group
123+
if hasattr(os, "killpg"):
124+
try:
125+
os.killpg(os.getpgid(process.pid), signal.SIGKILL)
126+
except (ProcessLookupError, OSError):
127+
pass
128+
else:
129+
process.kill()
130+
131+
# Clean up zombie process
132+
try:
133+
process.wait(timeout=1)
134+
except subprocess.TimeoutExpired:
135+
pass
136+
137+
console.print("[red]Command timed out after 30 seconds[/red]")
78138

79139
console.print()
80140
return True
81141

82-
except subprocess.TimeoutExpired:
83-
console.print("[red]Command timed out after 30 seconds[/red]")
84-
console.print()
85-
return True
86142
except Exception as e:
87143
console.print(f"[red]Error executing command: {e}[/red]")
88144
console.print()
89145
return True
146+
147+
finally:
148+
# CRITICAL: Always restore original signal handler so asyncio can handle Ctrl+C at prompt
149+
if original_handler is not None:
150+
signal.signal(signal.SIGINT, original_handler)

libs/deepagents-cli/deepagents_cli/config.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@
4848
# Common bash commands for autocomplete
4949
COMMON_BASH_COMMANDS = {
5050
"ls": "List directory contents",
51-
"ls -la": "List all files with details",
5251
"cd": "Change directory",
5352
"pwd": "Print working directory",
5453
"cat": "Display file contents",

libs/deepagents-cli/deepagents_cli/default_agent_prompt.md

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,12 @@ You are an AI assistant that helps users with various tasks including coding, re
33
# Core Role
44
Your core role and behavior may be updated based on user feedback and instructions. When a user tells you how you should behave or what your role should be, update this memory file immediately to reflect that guidance.
55

6-
## Memory-First Protocol
7-
You have access to a persistent memory system. ALWAYS follow this protocol:
6+
## Memory System
7+
You have access to persistent memory in `/memories/`.
88

9-
**At session start:**
10-
- Check `ls /memories/` to see what knowledge you have stored
11-
- If your role description references specific topics, check /memories/ for relevant guides
12-
13-
**Before answering questions:**
14-
- If asked "what do you know about X?" or "how do I do Y?" → Check `ls /memories/` FIRST
15-
- If relevant memory files exist → Read them and base your answer on saved knowledge
9+
**When answering questions:**
10+
- If asked "what do you know about X?" or similar questions about specific topics → Check `/memories/` for relevant knowledge
11+
- If you recall working on or saving information about the current topic → Read those memory files
1612
- Prefer saved knowledge over general knowledge when available
1713

1814
**When learning new information:**

0 commit comments

Comments
 (0)