Skip to content

Commit 9ed6483

Browse files
authored
ctrl-c protection, add buffer to avoid accidentally exiting thread (#300)
- add time buffer on ctrl+c on prompt input box to avoid accidental exit, tells user can press twice to exit
1 parent ddf762b commit 9ed6483

File tree

3 files changed

+59
-1
lines changed

3 files changed

+59
-1
lines changed

libs/deepagents-cli/deepagents_cli/config.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ class SessionState:
6161

6262
def __init__(self, auto_approve: bool = False):
6363
self.auto_approve = auto_approve
64+
self.exit_hint_until: float | None = None
65+
self.exit_hint_handle = None
6466

6567
def toggle_auto_approve(self) -> bool:
6668
"""Toggle auto-approve and return new state."""

libs/deepagents-cli/deepagents_cli/input.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
"""Input handling, completers, and prompt session for the CLI."""
22

3+
import asyncio
34
import os
45
import re
6+
import time
57
from collections.abc import Callable
68
from pathlib import Path
79

@@ -23,6 +25,8 @@
2325
AT_MENTION_RE = re.compile(r"@(?P<path>(?:[^\s@]|(?<=\\)\s)*)$")
2426
SLASH_COMMAND_RE = re.compile(r"^/(?P<command>[a-z]*)$")
2527

28+
EXIT_CONFIRM_WINDOW = 3.0
29+
2630

2731
class FilePathCompleter(Completer):
2832
"""Activate filesystem completion only when cursor is after '@'."""
@@ -154,6 +158,16 @@ def toolbar() -> list[tuple[str, str]]:
154158

155159
parts.append((base_class, base_msg))
156160

161+
# Show exit confirmation hint if active
162+
hint_until = session_state.exit_hint_until
163+
if hint_until is not None:
164+
now = time.monotonic()
165+
if now < hint_until:
166+
parts.append(("", " | "))
167+
parts.append(("class:toolbar-exit", " Ctrl+C again to exit "))
168+
else:
169+
session_state.exit_hint_until = None
170+
157171
return parts
158172

159173
return toolbar
@@ -168,6 +182,44 @@ def create_prompt_session(assistant_id: str, session_state: SessionState) -> Pro
168182
# Create key bindings
169183
kb = KeyBindings()
170184

185+
@kb.add("c-c")
186+
def _(event):
187+
"""Require double Ctrl+C within a short window to exit."""
188+
app = event.app
189+
now = time.monotonic()
190+
191+
if session_state.exit_hint_until is not None and now < session_state.exit_hint_until:
192+
handle = session_state.exit_hint_handle
193+
if handle:
194+
handle.cancel()
195+
session_state.exit_hint_handle = None
196+
session_state.exit_hint_until = None
197+
app.invalidate()
198+
app.exit(exception=KeyboardInterrupt())
199+
return
200+
201+
session_state.exit_hint_until = now + EXIT_CONFIRM_WINDOW
202+
203+
handle = session_state.exit_hint_handle
204+
if handle:
205+
handle.cancel()
206+
207+
loop = asyncio.get_running_loop()
208+
app_ref = app
209+
210+
def clear_hint():
211+
if (
212+
session_state.exit_hint_until is not None
213+
and time.monotonic() >= session_state.exit_hint_until
214+
):
215+
session_state.exit_hint_until = None
216+
session_state.exit_hint_handle = None
217+
app_ref.invalidate()
218+
219+
session_state.exit_hint_handle = loop.call_later(EXIT_CONFIRM_WINDOW, clear_hint)
220+
221+
app.invalidate()
222+
171223
# Bind Ctrl+T to toggle auto-approve
172224
@kb.add("c-t")
173225
def _(event):
@@ -240,6 +292,7 @@ def _(event):
240292
"bottom-toolbar": "noreverse", # Disable default reverse video
241293
"toolbar-green": "bg:#10b981 #000000", # Green for auto-accept ON
242294
"toolbar-orange": "bg:#f59e0b #000000", # Orange for manual accept
295+
"toolbar-exit": "bg:#2563eb #ffffff", # Blue for exit hint
243296
}
244297
)
245298

libs/deepagents-cli/deepagents_cli/main.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,11 +136,14 @@ async def simple_cli(agent, assistant_id: str | None, session_state, baseline_to
136136
while True:
137137
try:
138138
user_input = await session.prompt_async()
139+
if session_state.exit_hint_handle:
140+
session_state.exit_hint_handle.cancel()
141+
session_state.exit_hint_handle = None
142+
session_state.exit_hint_until = None
139143
user_input = user_input.strip()
140144
except EOFError:
141145
break
142146
except KeyboardInterrupt:
143-
# Ctrl+C at prompt - exit the program
144147
console.print("\nGoodbye!", style=COLORS["primary"])
145148
break
146149

0 commit comments

Comments
 (0)