mirror of
https://github.com/langchain-ai/deepagents.git
synced 2026-07-21 17:25:26 -04:00
fix(code): support plain exit quit command (#4543)
Typing `exit` in dcode now quits the app without needing a slash command. Made by [Open SWE](https://openswe.vercel.app/agents/211b4c5d-e31a-1941-4047-9412e6dc5319) --------- Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com> Co-authored-by: Mason Daugherty <github@mdrxy.com> Co-authored-by: Mason Daugherty <mason@langchain.dev>
This commit is contained in:
committed by
GitHub
parent
8cf57aca9f
commit
e6f10a1490
@@ -7588,6 +7588,16 @@ class DeepAgentsApp(App):
|
||||
self.push_screen(dependency_screen)
|
||||
return await result_future
|
||||
|
||||
@staticmethod
|
||||
def _is_exit_keyword(value: str, mode: InputMode) -> bool:
|
||||
"""Return whether `value` is the bare `exit` keyword in normal mode.
|
||||
|
||||
Matches case-insensitively and ignores surrounding whitespace. Only
|
||||
`normal` mode qualifies, so `exit` typed in shell or command mode is
|
||||
routed normally rather than quitting the app.
|
||||
"""
|
||||
return mode == "normal" and value.lower().strip() == "exit"
|
||||
|
||||
def _can_bypass_queue(self, value: str) -> bool:
|
||||
"""Check if a slash command can skip the message queue.
|
||||
|
||||
@@ -7663,10 +7673,9 @@ class DeepAgentsApp(App):
|
||||
# COMMANDS and so carry no bypass tier. Both must run even when the
|
||||
# app is busy or wedged, so neither sits behind the queue.
|
||||
always_bypass = ALWAYS_IMMEDIATE | HIDDEN_COMMANDS
|
||||
normalized = value.lower().strip()
|
||||
|
||||
if force_bypass or (
|
||||
mode == "command" and value.lower().strip() in always_bypass
|
||||
):
|
||||
if force_bypass or (mode == "command" and normalized in always_bypass):
|
||||
await self._process_message(value, mode)
|
||||
return
|
||||
|
||||
@@ -7717,6 +7726,13 @@ class DeepAgentsApp(App):
|
||||
|
||||
await dispatch_hook("user.prompt", {})
|
||||
|
||||
# A bare `exit` quits the app (REPL convention), mirroring `/quit`.
|
||||
# Gated to this interactive path only, so external/scripted callers
|
||||
# (on_external_input) can still send the literal "exit" to the agent.
|
||||
if self._is_exit_keyword(value, mode):
|
||||
self.exit()
|
||||
return
|
||||
|
||||
await self._submit_input(value, mode)
|
||||
|
||||
async def _dismiss_startup_tip(self) -> None:
|
||||
|
||||
@@ -11884,6 +11884,76 @@ class TestSlashCommandBypass:
|
||||
exit_mock.assert_called_once()
|
||||
assert len(app._pending_messages) == 0
|
||||
|
||||
async def test_exit_keyword_exits_from_normal_mode(self) -> None:
|
||||
"""Plain exit quits from normal mode, case-insensitive and whitespace-stripped.
|
||||
|
||||
The ` EXIT ` literal is the only coverage of the `.lower().strip()`
|
||||
normalization; keep the padding and casing when editing this test.
|
||||
"""
|
||||
app = DeepAgentsApp()
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.pause()
|
||||
|
||||
with patch.object(app, "exit") as exit_mock:
|
||||
app.post_message(ChatInput.Submitted(" EXIT ", "normal"))
|
||||
await pilot.pause()
|
||||
|
||||
exit_mock.assert_called_once()
|
||||
assert len(app._pending_messages) == 0
|
||||
|
||||
async def test_exit_keyword_bypasses_queue_when_agent_running(self) -> None:
|
||||
"""Plain exit should quit immediately even when the agent is running."""
|
||||
app = DeepAgentsApp()
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.pause()
|
||||
app._agent_running = True
|
||||
|
||||
with patch.object(app, "exit") as exit_mock:
|
||||
app.post_message(ChatInput.Submitted("exit", "normal"))
|
||||
await pilot.pause()
|
||||
|
||||
exit_mock.assert_called_once()
|
||||
assert len(app._pending_messages) == 0
|
||||
|
||||
async def test_exit_keyword_bypasses_thread_switching(self) -> None:
|
||||
"""Plain exit should quit even during a thread switch."""
|
||||
app = DeepAgentsApp()
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.pause()
|
||||
app._thread_switching = True
|
||||
|
||||
with patch.object(app, "exit") as exit_mock:
|
||||
app.post_message(ChatInput.Submitted("exit", "normal"))
|
||||
await pilot.pause()
|
||||
|
||||
exit_mock.assert_called_once()
|
||||
assert len(app._pending_messages) == 0
|
||||
|
||||
async def test_exit_keyword_requires_exact_match(self) -> None:
|
||||
"""Other messages containing exit should still go to the agent."""
|
||||
app = DeepAgentsApp()
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.pause()
|
||||
|
||||
with (
|
||||
patch.object(app, "exit") as exit_mock,
|
||||
patch.object(
|
||||
app, "_handle_user_message", new_callable=AsyncMock
|
||||
) as handler,
|
||||
):
|
||||
app.post_message(ChatInput.Submitted("exit now", "normal"))
|
||||
await pilot.pause()
|
||||
|
||||
exit_mock.assert_not_called()
|
||||
handler.assert_awaited_once_with("exit now")
|
||||
|
||||
def test_exit_keyword_only_matches_normal_mode(self) -> None:
|
||||
"""`exit` quits only in normal mode; shell/command input is untouched."""
|
||||
assert DeepAgentsApp._is_exit_keyword("exit", "normal") is True
|
||||
assert DeepAgentsApp._is_exit_keyword("exit", "shell") is False
|
||||
assert DeepAgentsApp._is_exit_keyword("exit", "shell_incognito") is False
|
||||
assert DeepAgentsApp._is_exit_keyword("exit", "command") is False
|
||||
|
||||
async def test_force_clear_bypasses_queue_when_agent_running(self) -> None:
|
||||
"""/force-clear should process immediately when agent is running."""
|
||||
app = DeepAgentsApp()
|
||||
@@ -11957,6 +12027,28 @@ class TestSlashCommandBypass:
|
||||
QueuedMessage(text="next task", mode="normal")
|
||||
]
|
||||
|
||||
async def test_external_prompt_exit_is_forwarded(self) -> None:
|
||||
"""An external `exit` prompt should be sent to the agent, not quit."""
|
||||
app = DeepAgentsApp()
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.pause()
|
||||
|
||||
with (
|
||||
patch.object(app, "exit") as exit_mock,
|
||||
patch.object(
|
||||
app, "_handle_user_message", new_callable=AsyncMock
|
||||
) as handler,
|
||||
):
|
||||
app.post_message(
|
||||
ExternalInput(
|
||||
ExternalEvent(kind="prompt", payload="exit", source="test")
|
||||
)
|
||||
)
|
||||
await pilot.pause()
|
||||
|
||||
exit_mock.assert_not_called()
|
||||
handler.assert_awaited_once_with("exit")
|
||||
|
||||
async def test_version_executes_during_connecting(self) -> None:
|
||||
"""/version should process immediately when only connecting."""
|
||||
app = DeepAgentsApp()
|
||||
|
||||
Reference in New Issue
Block a user