Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion libs/code/deepagents_code/tui/widgets/ask_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -264,12 +264,16 @@ def confirm_and_advance(self, index: int) -> None:

def _set_active_question(self, index: int) -> None:
"""Update the visual indicator and focus for the active question."""
self._highlight_question(index)
self._question_widgets[index].focus_input()

def _highlight_question(self, index: int) -> None:
"""Highlight `index` and dim the rest without changing focus."""
self._current_question = index
for i, qw in enumerate(self._question_widgets):
if i == index:
qw.add_class("ask-user-question-active")
qw.remove_class("ask-user-question-inactive")
qw.focus_input()
else:
qw.remove_class("ask-user-question-active")
qw.add_class("ask-user-question-inactive")
Expand Down Expand Up @@ -300,6 +304,22 @@ def action_cancel(self) -> None: # noqa: D102
self._future.set_result({"type": "cancelled"})
self.post_message(self.Cancelled())

def on_descendant_focus(self, event: events.DescendantFocus) -> None:
"""Keep the active-question highlight in sync with focus.

A mouse click moves focus into another question's input or choice list
without going through `_set_active_question`, which would otherwise
leave the highlight on the previously active question. Sync the
highlight to the focused question so exactly one question is ever
active. Focus is not moved here, so the widget the user clicked keeps
focus.
"""
node: Any = event.widget
while node is not None and not isinstance(node, _QuestionWidget):
node = node.parent
if node is not None and node._index != self._current_question:
self._highlight_question(node._index)

def on_blur(self, event: events.Blur) -> None: # noqa: PLR6301 # Textual event handler
"""Prevent blur from propagating and dismissing the menu."""
event.stop()
Expand Down
86 changes: 86 additions & 0 deletions libs/code/tests/unit_tests/tui/widgets/test_ask_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,92 @@ async def test_previous_question_navigates_backward(self) -> None:
assert qw0.has_class("ask-user-question-active")
assert qw1.has_class("ask-user-question-inactive")

async def test_clicking_text_question_moves_active_highlight(self) -> None:
"""Clicking a dimmed text question makes it the active/highlighted one."""
app = _AskUserTestApp(
[
{"question": "Q1?", "type": "text"},
{"question": "Q2?", "type": "text"},
]
)

async with app.run_test() as pilot:
await pilot.pause()
menu = app.query_one("#ask-user-menu", AskUserMenu)
qw0 = menu._question_widgets[0]
qw1 = menu._question_widgets[1]
assert qw0.has_class("ask-user-question-active")

second_input = qw1.query_one(".ask-user-text-input", AskUserTextArea)
await pilot.click(second_input)
await pilot.pause()

assert menu._current_question == 1
assert qw1.has_class("ask-user-question-active")
assert qw0.has_class("ask-user-question-inactive")
assert not qw0.has_class("ask-user-question-active")
assert second_input.has_focus

async def test_clicking_multiple_choice_question_moves_active_highlight(
self,
) -> None:
"""Clicking a dimmed multiple-choice question highlights it."""
app = _AskUserTestApp(
[
{
"question": "Color?",
"type": "multiple_choice",
"choices": [{"value": "red"}, {"value": "blue"}],
},
{
"question": "Size?",
"type": "multiple_choice",
"choices": [{"value": "S"}, {"value": "M"}],
},
]
)

async with app.run_test() as pilot:
await pilot.pause()
menu = app.query_one("#ask-user-menu", AskUserMenu)
qw0 = menu._question_widgets[0]
qw1 = menu._question_widgets[1]
assert qw0.has_class("ask-user-question-active")

await pilot.click(qw1)
await pilot.pause()

assert menu._current_question == 1
assert qw1.has_class("ask-user-question-active")
assert qw0.has_class("ask-user-question-inactive")

async def test_focus_sync_does_not_steal_focus_from_clicked_widget(self) -> None:
"""Syncing the highlight must not move focus off the clicked question."""
app = _AskUserTestApp(
[
{"question": "Q1?", "type": "text"},
{"question": "Q2?", "type": "text"},
]
)

async with app.run_test() as pilot:
await pilot.pause()
menu = app.query_one("#ask-user-menu", AskUserMenu)
qw1 = menu._question_widgets[1]
second_input = qw1.query_one(".ask-user-text-input", AskUserTextArea)

await pilot.click(second_input)
await pilot.pause()

# Typing lands in the clicked question's input, not the first one.
await pilot.press("h", "i")
await pilot.pause()
assert second_input.text == "hi"
first_input = menu._question_widgets[0].query_one(
".ask-user-text-input", AskUserTextArea
)
assert first_input.text == ""

async def test_previous_question_clamps_at_first(self) -> None:
"""At first question: previous is a no-op."""
app = _AskUserTestApp(
Expand Down
Loading