Skip to content
Open
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
18 changes: 11 additions & 7 deletions frontends/tuiapp_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -583,6 +583,16 @@ def _sidebar_last_user(sess: AgentSession) -> str:
return ""


# Textual 8.2.6 renamed _select_start to _select_state (SelectState.start.container).
def _select_origin_widget(screen: Any) -> Any:
state = getattr(screen, "_select_state", None)
if state is not None:
start = getattr(state, "start", None)
return start.container if start is not None else None
legacy = getattr(screen, "_select_start", None)
return legacy[0] if legacy is not None else None


def _sidebar_last_summary(sess: AgentSession) -> str:
try:
history = sess.agent.llmclient.backend.history
Expand Down Expand Up @@ -870,13 +880,7 @@ def patched(select_widget, mouse_coord, delta_y):
scroll_lines = app.SELECT_AUTO_SCROLL_LINES

candidates = [select_widget]
# Textual 8.2.6 renamed _select_start to _select_state (SelectState.start.container).
select_state = getattr(screen, "_select_state", None)
if select_state is not None:
sw = select_state.start.container
else:
ss = getattr(screen, "_select_start", None)
sw = ss[0] if ss is not None else None
sw = _select_origin_widget(screen)
if sw is not None and sw is not select_widget:
candidates.append(sw)

Expand Down
169 changes: 169 additions & 0 deletions tests/test_tuiapp_v2_select_origin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import importlib
import sys
import types
import unittest
from pathlib import Path


REPO_ROOT = Path(__file__).resolve().parents[1]
FRONTENDS = REPO_ROOT / "frontends"
for path in (str(REPO_ROOT), str(FRONTENDS)):
if path not in sys.path:
sys.path.insert(0, path)


def _install_import_stubs():
rich = types.ModuleType("rich")
rich_markdown = types.ModuleType("rich.markdown")

class Markdown:
def __init__(self, markup, **kwargs):
self.parsed = []

rich_markdown.Markdown = Markdown

rich_table = types.ModuleType("rich.table")
rich_table.Table = type("Table", (), {})

rich_text = types.ModuleType("rich.text")
rich_text.Text = type("Text", (), {})

textual = types.ModuleType("textual")
textual.events = types.ModuleType("textual.events")

class _Base:
BINDINGS: list = []
def __init__(self, *a, **kw):
pass
def __init_subclass__(cls, **kw):
super().__init_subclass__()
def __class_getitem__(cls, item):
return cls

textual_app = types.ModuleType("textual.app")
textual_app.App = type("App", (_Base,), {})
textual_app.ComposeResult = object

textual_binding = types.ModuleType("textual.binding")

class Binding:
def __init__(self, *a, **kw):
self.args = a
self.kwargs = kw

textual_binding.Binding = Binding

textual_containers = types.ModuleType("textual.containers")
textual_containers.Horizontal = type("Horizontal", (_Base,), {})
textual_containers.Vertical = type("Vertical", (_Base,), {})
textual_containers.VerticalScroll = type("VerticalScroll", (_Base,), {})

textual_message = types.ModuleType("textual.message")
textual_message.Message = type("Message", (_Base,), {})

textual_screen = types.ModuleType("textual.screen")
textual_screen.ModalScreen = type("ModalScreen", (_Base,), {})

textual_widgets = types.ModuleType("textual.widgets")
textual_widgets.OptionList = type("OptionList", (_Base,), {})
textual_widgets.Static = type("Static", (_Base,), {})
textual_widgets.TextArea = type("TextArea", (_Base,), {})

textual_widgets_option_list = types.ModuleType("textual.widgets.option_list")
textual_widgets_option_list.Option = type("Option", (_Base,), {})

chatapp_common = types.ModuleType("chatapp_common")
chatapp_common.format_restore = lambda: (([], "", 0), None)

btw_cmd = types.ModuleType("btw_cmd")
btw_cmd.handle_frontend_command = lambda *a, **k: ""

continue_cmd = types.ModuleType("continue_cmd")
continue_cmd.handle_frontend_command = lambda *a, **k: ""
continue_cmd.list_sessions = lambda *a, **k: []
continue_cmd.extract_ui_messages = lambda *a, **k: []

export_cmd = types.ModuleType("export_cmd")
export_cmd.last_assistant_text = lambda *a, **k: ""
export_cmd.export_to_temp = lambda *a, **k: ""
export_cmd.wrap_for_clipboard = lambda *a, **k: ""

agentmain = types.ModuleType("agentmain")

class GeneraticAgent:
def __init__(self):
self.verbose = False

agentmain.GeneraticAgent = GeneraticAgent

sys.modules.update({
"rich": rich,
"rich.markdown": rich_markdown,
"rich.table": rich_table,
"rich.text": rich_text,
"textual": textual,
"textual.events": textual.events,
"textual.app": textual_app,
"textual.binding": textual_binding,
"textual.containers": textual_containers,
"textual.message": textual_message,
"textual.screen": textual_screen,
"textual.widgets": textual_widgets,
"textual.widgets.option_list": textual_widgets_option_list,
"chatapp_common": chatapp_common,
"btw_cmd": btw_cmd,
"continue_cmd": continue_cmd,
"export_cmd": export_cmd,
"agentmain": agentmain,
})


_install_import_stubs()
tuiapp_v2 = importlib.import_module("tuiapp_v2")


class _Widget:
def __init__(self, name):
self.name = name


class _SelectStart:
def __init__(self, container=None):
self.container = container


class _SelectState:
def __init__(self, start):
self.start = start


class SelectOriginWidgetTests(unittest.TestCase):
def test_textual_8x_returns_select_state_start_container(self):
container = _Widget("messages")
screen = types.SimpleNamespace(_select_state=_SelectState(_SelectStart(container=container)))
self.assertIs(tuiapp_v2._select_origin_widget(screen), container)

def test_textual_8x_select_state_none_returns_none(self):
screen = types.SimpleNamespace(_select_state=None)
self.assertIsNone(tuiapp_v2._select_origin_widget(screen))

def test_textual_8x_start_none_returns_none(self):
screen = types.SimpleNamespace(_select_state=_SelectState(start=None))
self.assertIsNone(tuiapp_v2._select_origin_widget(screen))

def test_legacy_textual_7x_returns_first_element_of_select_start_tuple(self):
widget = _Widget("legacy")
screen = types.SimpleNamespace(_select_start=(widget, (0, 0)))
self.assertIs(tuiapp_v2._select_origin_widget(screen), widget)

def test_legacy_textual_7x_select_start_none_returns_none(self):
screen = types.SimpleNamespace(_select_start=None)
self.assertIsNone(tuiapp_v2._select_origin_widget(screen))

def test_neither_attribute_returns_none(self):
screen = types.SimpleNamespace()
self.assertIsNone(tuiapp_v2._select_origin_widget(screen))


if __name__ == "__main__":
unittest.main()