From d78ad3e6e578b0ae93d71cbf7b1008e0e4025948 Mon Sep 17 00:00:00 2001 From: Will Bradley <3030224+wbbradley@users.noreply.github.com> Date: Fri, 10 Jul 2026 05:05:25 +0000 Subject: [PATCH 1/4] fix(code): preserve Ctrl+D deletion in non-empty input Co-authored-by: open-swe[bot] --- libs/code/deepagents_code/app.py | 6 +++ libs/code/tests/unit_tests/test_app.py | 55 ++++++++++++++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/libs/code/deepagents_code/app.py b/libs/code/deepagents_code/app.py index 3ad2197059..7e58cd1d77 100644 --- a/libs/code/deepagents_code/app.py +++ b/libs/code/deepagents_code/app.py @@ -12587,6 +12587,12 @@ def action_quit_app(self) -> None: return self._arm_quit_pending("Ctrl+D") return + chat_input = self._chat_input + if chat_input is not None: + text_area = chat_input.input_widget + if text_area is not None and text_area.has_focus and chat_input.value: + text_area.action_delete_right() + return self.exit() def exit( diff --git a/libs/code/tests/unit_tests/test_app.py b/libs/code/tests/unit_tests/test_app.py index 101fe845c9..95c5eb4f1d 100644 --- a/libs/code/tests/unit_tests/test_app.py +++ b/libs/code/tests/unit_tests/test_app.py @@ -1953,6 +1953,61 @@ def test_ctrl_e_not_bound(self) -> None: assert "ctrl+e" not in bindings_by_key +class TestCtrlDChatInput: + """Test Ctrl+D deletion and quit behavior in the main chat input.""" + + async def test_ctrl_d_deletes_right_when_input_has_text(self) -> None: + """Ctrl+D should delete right of the cursor without quitting.""" + 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", "e", "l", "l", "o") + await pilot.press("ctrl+a") + + with patch.object(app, "exit") as exit_mock: + await pilot.press("ctrl+d") + await pilot.pause() + + assert chat_input.value == "ello" + exit_mock.assert_not_called() + + 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.""" + 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") + + 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() + + async def test_ctrl_d_quits_when_input_is_empty(self) -> None: + """Ctrl+D should still quit when the focused chat input is empty.""" + 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 + assert chat_input.value == "" + text_area.focus() + + with patch.object(app, "exit") as exit_mock: + await pilot.press("ctrl+d") + await pilot.pause() + + exit_mock.assert_called_once() + + class TestCtrlCCopySelection: """Test Ctrl+C copying a focused input's selection instead of quitting.""" From a000942dad114fc44bdfbd993934e8ebdead32de Mon Sep 17 00:00:00 2001 From: Will Bradley <3030224+wbbradley@users.noreply.github.com> Date: Fri, 10 Jul 2026 05:24:14 +0000 Subject: [PATCH 2/4] fix(code): avoid editing hidden Ctrl+D drafts Co-authored-by: open-swe[bot] --- libs/code/deepagents_code/app.py | 2 +- libs/code/tests/unit_tests/test_app.py | 24 ++++++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/libs/code/deepagents_code/app.py b/libs/code/deepagents_code/app.py index 7e58cd1d77..bb5757444e 100644 --- a/libs/code/deepagents_code/app.py +++ b/libs/code/deepagents_code/app.py @@ -12590,7 +12590,7 @@ def action_quit_app(self) -> None: chat_input = self._chat_input if chat_input is not None: text_area = chat_input.input_widget - if text_area is not None and text_area.has_focus and chat_input.value: + if text_area is not None and self.focused is text_area and chat_input.value: text_area.action_delete_right() return self.exit() diff --git a/libs/code/tests/unit_tests/test_app.py b/libs/code/tests/unit_tests/test_app.py index 95c5eb4f1d..3bb143290c 100644 --- a/libs/code/tests/unit_tests/test_app.py +++ b/libs/code/tests/unit_tests/test_app.py @@ -2007,6 +2007,30 @@ async def test_ctrl_d_quits_when_input_is_empty(self) -> None: exit_mock.assert_called_once() + async def test_ctrl_d_quits_with_non_empty_draft_hidden_by_modal(self) -> None: + """Ctrl+D should quit instead of editing a draft hidden behind a modal.""" + from deepagents_code.tui.widgets.update_available import UpdateAvailableScreen + + 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("d", "r", "a", "f", "t", "ctrl+a") + + app.push_screen(UpdateAvailableScreen(_update_entry())) + await pilot.pause() + + assert text_area.has_focus + assert app.focused is not text_area + with patch.object(app, "exit") as exit_mock: + await pilot.press("ctrl+d") + await pilot.pause() + + assert chat_input.value == "draft" + exit_mock.assert_called_once() + class TestCtrlCCopySelection: """Test Ctrl+C copying a focused input's selection instead of quitting.""" From b42fd2a83b37cca7c58ab8dc1ab97316947fc328 Mon Sep 17 00:00:00 2001 From: Will Bradley Date: Fri, 10 Jul 2026 12:45:37 -0600 Subject: [PATCH 3/4] address review feedback re whitespace --- libs/code/deepagents_code/app.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/libs/code/deepagents_code/app.py b/libs/code/deepagents_code/app.py index bb5757444e..3477fd0e41 100644 --- a/libs/code/deepagents_code/app.py +++ b/libs/code/deepagents_code/app.py @@ -12587,12 +12587,14 @@ def action_quit_app(self) -> None: return self._arm_quit_pending("Ctrl+D") return + 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: text_area.action_delete_right() return + self.exit() def exit( From e1f940b3620046e04232f932ec06c0cacbfc0450 Mon Sep 17 00:00:00 2001 From: Mason Daugherty Date: Mon, 13 Jul 2026 02:14:57 -0400 Subject: [PATCH 4/4] cr --- libs/code/deepagents_code/app.py | 12 +++- .../deepagents_code/tui/widgets/chat_input.py | 10 ++-- libs/code/tests/unit_tests/test_app.py | 57 ++++++++++++++++++- 3 files changed, 72 insertions(+), 7 deletions(-) diff --git a/libs/code/deepagents_code/app.py b/libs/code/deepagents_code/app.py index 7d607b8f50..a781a623ae 100644 --- a/libs/code/deepagents_code/app.py +++ b/libs/code/deepagents_code/app.py @@ -12654,7 +12654,13 @@ def _arm_clear_input_pending(self) -> None: self.set_timer(timeout, lambda: setattr(self, "_clear_input_pending", False)) def action_quit_app(self) -> None: - """Handle quit action (Ctrl+D).""" + """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. + """ from deepagents_code.tui.widgets.auth import ( AuthPromptScreen, DeleteCredentialConfirmScreen, @@ -12680,6 +12686,10 @@ def action_quit_app(self) -> None: 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. chat_input = self._chat_input if chat_input is not None: text_area = chat_input.input_widget diff --git a/libs/code/deepagents_code/tui/widgets/chat_input.py b/libs/code/deepagents_code/tui/widgets/chat_input.py index f72b74331e..75012f1edc 100644 --- a/libs/code/deepagents_code/tui/widgets/chat_input.py +++ b/libs/code/deepagents_code/tui/widgets/chat_input.py @@ -1254,11 +1254,6 @@ async def _on_key(self, event: events.Key) -> None: event.stop() return - if event.key == "delete" and self._delete_placeholder_token(backwards=False): - event.prevent_default() - event.stop() - return - # If completion is active, let parent handle navigation keys. # Space is included so that slash-command completion can accept the # selected suggestion via the same code path as Tab (avoiding a @@ -1304,6 +1299,11 @@ async def _on_key(self, event: events.Key) -> None: await super()._on_key(event) + def action_delete_right(self) -> None: + """Delete a bound placeholder atomically or the next character.""" + if not self._delete_placeholder_token(backwards=False): + super().action_delete_right() + def _delete_placeholder_token(self, *, backwards: bool) -> bool: """Delete a full placeholder token (image, video, or paste) in one keypress. diff --git a/libs/code/tests/unit_tests/test_app.py b/libs/code/tests/unit_tests/test_app.py index 2d3503f4d8..fe7262b7bf 100644 --- a/libs/code/tests/unit_tests/test_app.py +++ b/libs/code/tests/unit_tests/test_app.py @@ -58,7 +58,7 @@ _warn_discarded_goal_channels, ) from deepagents_code.event_bus import ExternalEvent -from deepagents_code.media_utils import ImageData +from deepagents_code.media_utils import ImageData, VideoData from deepagents_code.tui.widgets.ask_user import AskUserTextArea from deepagents_code.tui.widgets.chat_input import ChatInput from deepagents_code.tui.widgets.goal_review import GoalReviewMenu, GoalReviewResult @@ -1965,6 +1965,8 @@ async def test_ctrl_d_deletes_right_when_input_has_text(self) -> None: assert text_area is not None text_area.focus() await pilot.press("h", "e", "l", "l", "o") + # ctrl+a moves the cursor to the start of the line (not select-all), + # so delete-right removes the leading "h". await pilot.press("ctrl+a") with patch.object(app, "exit") as exit_mock: @@ -1973,6 +1975,8 @@ async def test_ctrl_d_deletes_right_when_input_has_text(self) -> None: assert chat_input.value == "ello" exit_mock.assert_not_called() + # 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.""" @@ -1990,6 +1994,57 @@ async def test_ctrl_d_does_not_quit_at_end_of_non_empty_input(self) -> None: assert chat_input.value == "hi" exit_mock.assert_not_called() + assert app._quit_pending is False + + @pytest.mark.parametrize("kind", ["image", "video"]) + async def test_ctrl_d_deletes_bound_media_placeholder_atomically( + self, kind: str + ) -> None: + """Ctrl+D should delete a bound media placeholder as one token.""" + 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 + if kind == "image": + placeholder = app._image_tracker.add_image( + ImageData(base64_data="abc", format="png", placeholder="") + ) + else: + placeholder = app._image_tracker.add_video( + VideoData(base64_data="abc", format="mp4", placeholder="") + ) + chat_input.value = placeholder + text_area.move_cursor((0, 0)) + text_area.focus() + + with patch.object(app, "exit") as exit_mock: + await pilot.press("ctrl+d") + await pilot.pause() + + assert chat_input.value == "" + assert app._image_tracker.get_images() == [] + assert app._image_tracker.get_videos() == [] + exit_mock.assert_not_called() + + async def test_ctrl_d_deletes_collapsed_paste_placeholder_atomically(self) -> None: + """Ctrl+D should preserve collapsed-paste integrity when deleting it.""" + 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 + chat_input.handle_external_paste("p" * 900) + text_area.move_cursor((0, 0)) + assert chat_input.value == "[Pasted text #1]" + + with patch.object(app, "exit") as exit_mock: + await pilot.press("ctrl+d") + await pilot.pause() + + assert chat_input.value == "" + assert 1 in chat_input._pasted_contents + exit_mock.assert_not_called() async def test_ctrl_d_quits_when_input_is_empty(self) -> None: """Ctrl+D should still quit when the focused chat input is empty."""