Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
63 changes: 54 additions & 9 deletions libs/code/deepagents_code/tui/widgets/_inline_prompt.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from collections import Counter
from typing import TYPE_CHECKING, Any, Generic, TypeVar

from textual.containers import Horizontal
from textual.content import Content
from textual.message import Message
from textual.widgets import Static
Expand All @@ -16,6 +17,7 @@
from pathlib import Path

from textual import events
from textual.app import ComposeResult
from textual.widget import Widget

from deepagents_code import theme
Expand Down Expand Up @@ -294,16 +296,36 @@ async def _reject_dropped_media(self, text: str) -> bool:
return True


class InlinePromptOption(Static):
"""Render a selectable inline-prompt option with a cursor."""
class InlinePromptOption(Horizontal):
"""Render a selectable inline-prompt option with a cursor gutter."""

DEFAULT_CSS = """
InlinePromptOption {
height: auto;
}

InlinePromptOption > .inline-prompt-option-cursor {
width: 2;
height: 1;
}

InlinePromptOption > .inline-prompt-option-label {
width: 1fr;
height: auto;
}

InlinePromptOption.inline-prompt-option-selected > .inline-prompt-option-label {
color: $primary;
}
"""

def __init__(
self,
text: str,
index: int,
*,
selected: bool = False,
selected_class: str | None = None,
selected_class: str | None = "inline-prompt-option-selected",
**kwargs: Any,
) -> None:
"""Initialize an option.
Expand All @@ -313,16 +335,33 @@ def __init__(
index: Position in its owning prompt's option list.
selected: Whether to render the option selected initially.
selected_class: CSS class applied while the option is highlighted.
**kwargs: Additional `Static` arguments.
**kwargs: Additional `Horizontal` arguments.
"""
self.option_index = index
self._cursor_visible = selected
self._highlighted = selected
self._text = text
self._selected_class = selected_class
super().__init__(self._render(), **kwargs)
self._cursor_widget: Static | None = None
super().__init__(**kwargs)
self._sync_selected_class()

def compose(self) -> ComposeResult:
"""Compose the cursor gutter and independently wrapping label.

Yields:
The fixed cursor gutter followed by the wrapping label.
"""
self._cursor_widget = Static(
self._cursor_content(),
classes="inline-prompt-option-cursor",
)
yield self._cursor_widget
yield Static(
Content.from_markup("$text", text=self._text),
classes="inline-prompt-option-label",
)

@property
def selected(self) -> bool:
"""Whether the selection cursor is currently shown on this option."""
Expand All @@ -345,13 +384,19 @@ def set_state(self, *, cursor: bool, highlighted: bool) -> None:
"""
self._cursor_visible = cursor
self._highlighted = highlighted
self.update(self._render())
if self._cursor_widget is not None:
self._cursor_widget.update(self._cursor_content())
self._sync_selected_class()

def _render(self) -> Content:
def _cursor_content(self) -> Content:
glyphs = get_glyphs()
prefix = f"{glyphs.cursor} " if self._cursor_visible else " "
return Content.from_markup("$prefix$text", prefix=prefix, text=self._text)
marker = glyphs.cursor if self._cursor_visible else self._unselected_marker
return Content(f"{marker} ")

@property
def _unselected_marker(self) -> str:
"""Marker shown in the cursor gutter when this option is not selected."""
return " "

def _sync_selected_class(self) -> None:
if self._selected_class is None:
Expand Down
4 changes: 4 additions & 0 deletions libs/code/deepagents_code/tui/widgets/ask_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,10 @@ def on_blur(self, event: events.Blur) -> None: # noqa: PLR6301 # Textual event
class _ChoiceOption(InlinePromptOption):
"""A single selectable ask-user choice option."""

@property
def _unselected_marker(self) -> str:
return get_glyphs().bullet

def __init__(
self, text: str, index: int, *, selected: bool = False, **kwargs: Any
) -> None:
Expand Down
8 changes: 6 additions & 2 deletions libs/code/tests/unit_tests/tui/widgets/test_ask_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
MISSING_ANSWER_TOAST,
AskUserMenu,
AskUserTextArea,
_ChoiceOption,
_QuestionWidget,
)

Expand Down Expand Up @@ -210,8 +211,11 @@ async def test_multiple_choice_option_wraps_in_narrow_menu(self) -> None:

async with app.run_test(size=(36, 24)) as pilot:
await pilot.pause()
choice = app.query_one(".ask-user-choice", Static)
assert choice.size.height > 1
choice = app.query_one(".ask-user-choice", _ChoiceOption)
cursor = choice.query_one(".inline-prompt-option-cursor", Static)
label = choice.query_one(".inline-prompt-option-label", Static)
assert label.size.height > 1
assert label.region.x == cursor.region.x + cursor.region.width

async def test_text_question_submits_typed_answer(self) -> None:
app = _AskUserTestApp([{"question": "What is your name?", "type": "text"}])
Expand Down
Loading