mirror of
https://github.com/langchain-ai/deepagents.git
synced 2026-07-21 17:25:26 -04:00
fix(code): quit with Ctrl+D at end of prompt (#4678)
Follow-up to #4626. `Ctrl+D` now quits Deep Agents Code when the cursor is at the end of the prompt, including when the prompt contains text. It continues to delete selected text or content to the right of the cursor. --- After #4626, `Ctrl+D` preserves its delete-right behavior whenever the prompt contains a draft. At the end of a non-empty prompt, however, there is nothing to delete and the key does nothing. Treat the end of the prompt like the empty-prompt case: `Ctrl+D` quits when there is no selection and no content to the right. It continues to delete selected text, delete the next character, or join multiline input when the cursor is not at the true end of the prompt.
This commit is contained in:
@@ -12877,9 +12877,10 @@ class DeepAgentsApp(App):
|
||||
"""Handle the Ctrl+D binding.
|
||||
|
||||
Delete-confirm screens and the auth/thread selectors keep their own
|
||||
Ctrl+D behavior. Otherwise, when the chat input is focused and holds a
|
||||
draft, Ctrl+D deletes right of the cursor instead of quitting; the app
|
||||
only exits from an empty (or unfocused) prompt.
|
||||
Ctrl+D behavior. Otherwise, when the chat input is focused, Ctrl+D
|
||||
deletes a non-empty selection or the character right of the cursor.
|
||||
Only at the end of the prompt with no active selection does it exit
|
||||
the app.
|
||||
"""
|
||||
from deepagents_code.tui.widgets.auth import (
|
||||
AuthPromptScreen,
|
||||
@@ -12906,14 +12907,22 @@ class DeepAgentsApp(App):
|
||||
self._arm_quit_pending("Ctrl+D")
|
||||
return
|
||||
|
||||
# Delegate Ctrl+D to the chat input's delete-right when it holds a
|
||||
# draft. Check `self.focused` (the active screen's focused widget), not
|
||||
# `text_area.has_focus`: a draft hidden behind a modal keeps focus but
|
||||
# must not be edited from under it, so Ctrl+D quits in that case.
|
||||
# Delegate Ctrl+D when delete-right can remove selected text or content
|
||||
# after the cursor. Check `self.focused` (the active screen's focused
|
||||
# widget), not `text_area.has_focus`: a draft hidden behind a modal keeps
|
||||
# focus but must not be edited from under it, so Ctrl+D quits in that case.
|
||||
chat_input = self._chat_input
|
||||
if chat_input is not None:
|
||||
text_area = chat_input.input_widget
|
||||
if text_area is not None and self.focused is text_area and chat_input.value:
|
||||
if (
|
||||
text_area is not None
|
||||
and self.focused is text_area
|
||||
and chat_input.value
|
||||
and (
|
||||
not text_area.selection.is_empty
|
||||
or text_area.cursor_location != text_area.document.end
|
||||
)
|
||||
):
|
||||
text_area.action_delete_right()
|
||||
return
|
||||
|
||||
|
||||
@@ -1978,8 +1978,8 @@ class TestCtrlDChatInput:
|
||||
# A draft swallows the quit rather than half-arming the double-tap.
|
||||
assert app._quit_pending is False
|
||||
|
||||
async def test_ctrl_d_does_not_quit_at_end_of_non_empty_input(self) -> None:
|
||||
"""Ctrl+D should be a no-op at the end of a non-empty draft."""
|
||||
async def test_ctrl_d_quits_at_end_of_non_empty_input(self) -> None:
|
||||
"""Ctrl+D should quit at the end of a non-empty draft."""
|
||||
app = DeepAgentsApp()
|
||||
async with app.run_test() as pilot:
|
||||
chat_input = app.query_one(ChatInput)
|
||||
@@ -1988,14 +1988,84 @@ class TestCtrlDChatInput:
|
||||
text_area.focus()
|
||||
await pilot.press("h", "i")
|
||||
|
||||
# Precondition for the quit path: cursor at document end, no
|
||||
# selection. Asserted explicitly so the test documents why this
|
||||
# configuration quits and survives any future default that would
|
||||
# pre-select or reposition the input.
|
||||
assert text_area.selection.is_empty
|
||||
assert text_area.cursor_location == text_area.document.end
|
||||
|
||||
with patch.object(app, "exit") as exit_mock:
|
||||
await pilot.press("ctrl+d")
|
||||
await pilot.pause()
|
||||
|
||||
assert chat_input.value == "hi"
|
||||
exit_mock.assert_not_called()
|
||||
exit_mock.assert_called_once()
|
||||
assert app._quit_pending is False
|
||||
|
||||
async def test_ctrl_d_deletes_selection_at_end_of_input(self) -> None:
|
||||
"""Ctrl+D should delete a selection reaching document end, not quit."""
|
||||
app = DeepAgentsApp()
|
||||
async with app.run_test() as pilot:
|
||||
chat_input = app.query_one(ChatInput)
|
||||
text_area = chat_input.input_widget
|
||||
assert text_area is not None
|
||||
text_area.focus()
|
||||
await pilot.press("h", "i")
|
||||
# select_all leaves a non-empty selection with the cursor at
|
||||
# document end, so only the `selection.is_empty` branch — not the
|
||||
# end-of-document check — keeps Ctrl+D from quitting here.
|
||||
text_area.select_all()
|
||||
assert not text_area.selection.is_empty
|
||||
assert text_area.cursor_location == text_area.document.end
|
||||
|
||||
with patch.object(app, "exit") as exit_mock:
|
||||
await pilot.press("ctrl+d")
|
||||
await pilot.pause()
|
||||
|
||||
assert chat_input.value == ""
|
||||
exit_mock.assert_not_called()
|
||||
|
||||
async def test_ctrl_d_deletes_right_at_end_of_non_final_line(self) -> None:
|
||||
"""Ctrl+D mid-draft (end of a non-final line) should join lines, not quit."""
|
||||
app = DeepAgentsApp()
|
||||
async with app.run_test() as pilot:
|
||||
chat_input = app.query_one(ChatInput)
|
||||
text_area = chat_input.input_widget
|
||||
assert text_area is not None
|
||||
text_area.text = "ab\ncd"
|
||||
text_area.focus()
|
||||
# End of the first line: not document end, so delete-right removes
|
||||
# the newline and joins the two lines instead of quitting.
|
||||
text_area.move_cursor((0, 2))
|
||||
assert text_area.cursor_location != text_area.document.end
|
||||
|
||||
with patch.object(app, "exit") as exit_mock:
|
||||
await pilot.press("ctrl+d")
|
||||
await pilot.pause()
|
||||
|
||||
assert chat_input.value == "abcd"
|
||||
exit_mock.assert_not_called()
|
||||
|
||||
async def test_ctrl_d_quits_at_document_end_of_multiline_input(self) -> None:
|
||||
"""Ctrl+D at the true end of a multi-line draft should quit."""
|
||||
app = DeepAgentsApp()
|
||||
async with app.run_test() as pilot:
|
||||
chat_input = app.query_one(ChatInput)
|
||||
text_area = chat_input.input_widget
|
||||
assert text_area is not None
|
||||
text_area.text = "ab\ncd"
|
||||
text_area.focus()
|
||||
text_area.move_cursor(text_area.document.end)
|
||||
assert text_area.selection.is_empty
|
||||
|
||||
with patch.object(app, "exit") as exit_mock:
|
||||
await pilot.press("ctrl+d")
|
||||
await pilot.pause()
|
||||
|
||||
assert chat_input.value == "ab\ncd"
|
||||
exit_mock.assert_called_once()
|
||||
|
||||
@pytest.mark.parametrize("kind", ["image", "video"])
|
||||
async def test_ctrl_d_deletes_bound_media_placeholder_atomically(
|
||||
self, kind: str
|
||||
|
||||
Reference in New Issue
Block a user