diff --git a/src/toad/navigation_target.py b/src/toad/navigation_target.py index cd2d06d89..4abcce40d 100644 --- a/src/toad/navigation_target.py +++ b/src/toad/navigation_target.py @@ -10,7 +10,10 @@ from agent_comms.channel_targets import ChannelTarget as CoreChannelTarget from agent_comms.channel_targets import ThreadTarget as CoreThreadTarget from agent_comms.nominal.declared_family import DeclaredFamily +from agent_comms.thread_identity import ThreadIncarnation +from toad.comms_root import Observing +from toad.work_preparation import PreparationRuntime from toad.core.events import CoreEvent from toad.constants import ALL_COMMS_TARGET from toad.navigation_context import NavigationContext, NavigationOwner @@ -98,9 +101,26 @@ async def open(self, context: NavigationContext) -> str: class ThreadTarget(NavigationTarget): async def open(self, context: NavigationContext) -> str: return await context.app.thread_navigation.open( - owner_mode=context.owner_mode, project_path=context.project_path, target=self.name, + owner_mode=context.owner_mode, project_path=context.project_path, target=self, ) + async def incarnation_in(self, preparation: PreparationRuntime, observed: Observing) -> ThreadIncarnation: + """A name names a thread by its current birth, which Core answers off the loop.""" + name = self.name + return await preparation.run_thread( + lambda: observed.registry_for(name).require(name).incarnation) + + +@dataclass(frozen=True) +class KnownThreadTarget(ThreadTarget): + """A thread already drawn: its row holds the birth, so opening it asks Core nothing before + its tab shows; the tab's own read confirms the birth behind the loading view.""" + + incarnation: ThreadIncarnation + + async def incarnation_in(self, preparation: PreparationRuntime, observed: Observing) -> ThreadIncarnation: + return self.incarnation + class HistoryRoute: """A route declaration registers its conversation kind exactly once.""" diff --git a/src/toad/thread_actions.py b/src/toad/thread_actions.py index 38902e3a8..119b94849 100644 --- a/src/toad/thread_actions.py +++ b/src/toad/thread_actions.py @@ -108,8 +108,8 @@ async def completed(self, app, selected, result: CliResult): await app.session_navigation.retire_missing() await app.session_navigation.reconnect(selected, result.reconnect_targets()) for target in result.open_targets(): - await app.thread_navigation.open(owner_mode=self.context.mode, - project_path=self.context.project, target=target) + await app.thread_navigation.open_named(owner_mode=self.context.mode, + project_path=self.context.project, name=target) result.show(app, self.definition.label, self.context.title) @classmethod diff --git a/src/toad/thread_navigation.py b/src/toad/thread_navigation.py index f986acdbd..cf7e4d1f6 100644 --- a/src/toad/thread_navigation.py +++ b/src/toad/thread_navigation.py @@ -9,6 +9,7 @@ from agent_comms.errors import RelationViolationError from agent_comms.repo import Checkout +from toad.navigation_target import ThreadTarget from toad.tab_identity.kinds import MailboxTab from toad.navigation_preparation import ( OpenThread, @@ -52,7 +53,11 @@ def __init__(self, app: ToadApp) -> None: self.app = app self.pending: dict[tuple[str, str, str], ThreadOpening] = {} - async def open(self, *, owner_mode: str, project_path: Checkout, target: str) -> str: + async def open_named(self, *, owner_mode: str, project_path: Checkout, name: str) -> str: + """A thread known only by name (a command's result, a label): Core answers its birth.""" + return await self.open(owner_mode=owner_mode, project_path=project_path, target=ThreadTarget(name)) + + async def open(self, *, owner_mode: str, project_path: Checkout, target: ThreadTarget) -> str: app = self.app try: source = app.workspace_sessions.origin(owner_mode).require() @@ -64,11 +69,7 @@ async def open(self, *, owner_mode: str, project_path: Checkout, target: str) -> requested_root = str(observed.root) if not origin.current(self, owner_mode): return app.selected_mode - identity = await app.preparation.run_thread( - lambda: MailboxTab( - observed.registry_for(target).require(target).incarnation, requested_root - ) - ) + identity = MailboxTab(await target.incarnation_in(app.preparation, observed), requested_root) mounted_root = str(Path(requested_root).expanduser()) open_threads = [] for details in app.session_tracker.ordered_sessions: @@ -76,13 +77,13 @@ async def open(self, *, owner_mode: str, project_path: Checkout, target: str) -> if view is None: continue root, name = view.coordination_root, view.thread_identity.thread_name() - if (root, name) == (mounted_root, target): + if (root, name) == (mounted_root, target.name): await app.select_session(details.mode_name) return details.mode_name if root is not None: open_threads.append(OpenThread(details.mode_name, root, name)) request = ThreadNavigationRequest( - observed, target, project_path, tuple(open_threads) + observed, target.name, project_path, tuple(open_threads) ) key = owner_mode, request.root, request.target if pending := self.pending.get(key): diff --git a/src/toad/widgets/comms_sidebar.py b/src/toad/widgets/comms_sidebar.py index e0aaf0231..83aaeadac 100644 --- a/src/toad/widgets/comms_sidebar.py +++ b/src/toad/widgets/comms_sidebar.py @@ -38,7 +38,7 @@ from toad.thread_identity import SessionIdentity from toad.widgets.thread_identity import ShowsThreadIdentity from toad.core_event_carrier import CoreEventReceiver -from toad.navigation_target import channel_target, ThreadTarget +from toad.navigation_target import KnownThreadTarget, NavigationTarget, ThreadTarget, channel_target from toad.session_tracker import SidebarSelection from toad.widgets.selection import HoverSelection, ViewLabel from toad.application_actions import NewThreadAction @@ -48,7 +48,6 @@ from toad.tone_paint import AlternatingTone from toad.textual_colours import ChannelAlternateRowToneColour, ChannelRowToneColour from toad.widgets.side_bar import SideBar, SideBarCollapsible, SidebarPanelWidget, SidebarVisibilityObserver -from toad.navigation_target import NavigationTarget from toad.app_node import ToadNode from toad.native_style import ( AutoHeight, @@ -300,6 +299,8 @@ def show(self, row: ThreadRowModel, *, pinned: bool = False) -> None: return previous = self.thread_row self.thread_row, self.pinned, self.action_status = row, pinned, action_status + # The drawn row knows the thread's birth: opening it asks Core nothing first. + self.target = KnownThreadTarget(row.name, row.incarnation) summary = action_status if action_status is not None else row.summary self.tooltip = Content("\n".join(value for value in ( row.name, summary, row.unread.detail, "Pinned in this channel" if pinned else None, *row.model.texts(), diff --git a/src/toad/widgets/session_details.py b/src/toad/widgets/session_details.py index 128eb9aa2..ea4582224 100644 --- a/src/toad/widgets/session_details.py +++ b/src/toad/widgets/session_details.py @@ -217,6 +217,6 @@ async def _on_collapsible_title_toggle(self, event) -> None: event.prevent_default() event.stop() context = self.query_ancestor(SessionScope).navigation_owner.navigation_context - await self.app.thread_navigation.open( + await self.app.thread_navigation.open_named( owner_mode=context.owner_mode, project_path=context.project_path, - target=self.label) + name=self.label) diff --git a/tests/app/test_channel_mentions.py b/tests/app/test_channel_mentions.py index bc8f5a8b7..802cb85e5 100644 --- a/tests/app/test_channel_mentions.py +++ b/tests/app/test_channel_mentions.py @@ -1,10 +1,15 @@ """Completing a thread mention in a channel keeps the draft and delivers to the channel.""" -from toad.line_blocks import OpenTarget +from agent_comms.channel_targets import ChannelTarget +from agent_comms.display_references import ThreadDisplayReference +from agent_comms.repo import Checkout + +from toad.link_click import OpenLineLink +from toad.line_blocks import OpenReference async def test_mention_completion_delivery_and_link(workspace): - workspace.thread("alpha", "team") + alpha = workspace.thread("alpha", "team") workspace.thread("beta", "team") comms = workspace.comms async with workspace.app(size=(100, 35)) as ui: @@ -15,14 +20,22 @@ async def test_mention_completion_delivery_and_link(workspace): assert ui.draft() == "Can @alpha " await ui.type("review this?") await ui.press("enter") - await ui.until(lambda: len(comms.views.channel_history("#team")) == 1, why="the message on the wire") - sent = comms.views.channel_history("#team")[0] + team = ChannelTarget("#team").display_history(comms.views, Checkout(workspace.project)) + + def on_the_wire(): + return team.page(comms.views.presentation).messages + + await ui.until(lambda: len(on_the_wire()) == 1, why="the message on the wire") + sent = on_the_wire()[0] assert sent.target == "#team" and sent.body == "Can @alpha review this?" assert sent.mentions[0].thread == "alpha" - assert len(comms.bus.inbox("alpha")) == len(comms.bus.inbox("beta")) == 1 + assert len(comms.bus.inbox("alpha").page.messages) == len(comms.bus.inbox("beta").page.messages) == 1 await ui.until_shows("Can @alpha review this?") # The name becomes a link when the message's styled lines replace its plain ones. - await ui.until(lambda: ui.link_at("@alpha review") == OpenTarget("alpha").click, + # A mention names alpha's exact birth on this wire, not whichever thread holds the name later. + mention = OpenLineLink(OpenReference(ThreadDisplayReference( + root=str(workspace.roots.wire), incarnation=alpha.incarnation))) + await ui.until(lambda: ui.link_at("@alpha review") == mention, why="the mention drawn as a link") await ui.write_draft("@") diff --git a/tests/app/test_markdown_file_links.py b/tests/app/test_markdown_file_links.py index f424dafae..29fb88868 100644 --- a/tests/app/test_markdown_file_links.py +++ b/tests/app/test_markdown_file_links.py @@ -1,4 +1,5 @@ """A file named in an agent answer is a link to the project file; clicking it opens the file's preview and returning keeps the draft.""" +from toad.link_click import OpenLineLink from toad.session_scope import AgentAttached from support.agents import peer @@ -25,7 +26,7 @@ async def test_file_link_in_answer_opens_preview_and_return_keeps_draft(workspac await ui.until_shows("PEER_ANSWER") await ui.write_draft("KEEP_MARKDOWN_DRAFT") links = file_links(ui, "file.md") - opens_file = DirectProjectFileLink(target, original_root=workspace.project).click + opens_file = OpenLineLink(DirectProjectFileLink(target, original_root=workspace.project)) assert sum(link == opens_file for link in links) == 2, links assert any("https://example.com" in str(link) for link in file_links(ui, "website")), ui.visible_text() drawn = [i for i, line in enumerate(line for line in ui.visible_lines() if "file.md" in line)] diff --git a/tests/app/test_native_message_parts.py b/tests/app/test_native_message_parts.py index 09e508402..3711ff5b8 100644 --- a/tests/app/test_native_message_parts.py +++ b/tests/app/test_native_message_parts.py @@ -13,8 +13,8 @@ async def test_saved_message_in_parts_shows_as_one_message(workspace): session = write_journal(workspace.project / "native.jsonl", entry) workspace.comms.registry.declare(NativeThread("worker", frozenset(), Checkout(workspace.project), session_file=RegistrySessionLocation.from_path(session, workspace.comms.root))) async with workspace.app(size=(110, 38)) as ui: - await ui.app.thread_navigation.open( - owner_mode=ui.selected_tab, project_path=ui.app.project_dir, target="worker") + await ui.app.thread_navigation.open_named( + owner_mode=ui.selected_tab, project_path=ui.app.project_dir, name="worker") await ui.step() await ui.until(lambda: ui.shows("value = 123") and not ui.shows("## Plan"), why="the styled message") lines = [line.strip(" │▌").strip() for line in ui.visible_lines()] diff --git a/tests/app/test_transcript_recipient.py b/tests/app/test_transcript_recipient.py index 626d3d525..00a831566 100644 --- a/tests/app/test_transcript_recipient.py +++ b/tests/app/test_transcript_recipient.py @@ -35,12 +35,12 @@ async def test_unbound_tab_refuses_another_threads_snapshot(workspace): identity, thread=replace(identity.thread, created_at=identity.thread.created_at - 0.5)), owner) async with workspace.app(size=(110, 38)) as ui: - alpha_tab = await ui.app.thread_navigation.open( - owner_mode=ui.selected_tab, project_path=ui.app.project_dir, target="alpha") + alpha_tab = await ui.app.thread_navigation.open_named( + owner_mode=ui.selected_tab, project_path=ui.app.project_dir, name="alpha") await ui.until(lambda: ui.shows("Alpha-only history"), why="alpha's saved history") - await ui.app.thread_navigation.open( - owner_mode=alpha_tab, project_path=ui.app.project_dir, target="beta") + await ui.app.thread_navigation.open_named( + owner_mode=alpha_tab, project_path=ui.app.project_dir, name="beta") await ui.step() beta = ui.detached_agent(None) ui.attach_agent(beta) diff --git a/tests/native/test_native_journey.py b/tests/native/test_native_journey.py index c01c9d806..1f4472df3 100644 --- a/tests/native/test_native_journey.py +++ b/tests/native/test_native_journey.py @@ -232,8 +232,8 @@ def reply(request, number): model=parent.model, thinking_level=parent.thinking_level, ).apply, comms) assert result.created == "fork" - child_tab = await app.thread_navigation.open( - owner_mode=parent_tab, project_path=app.project_dir, target="fork") + child_tab = await app.thread_navigation.open_named( + owner_mode=parent_tab, project_path=app.project_dir, name="fork") assert child_tab != parent_tab and ui.selected_tab == child_tab await ui.until_agent_ready() view = ui.conversation @@ -340,7 +340,7 @@ async def declare_second(comms, project, _provider): app, _pilot = ui.textual owner_tab = ui.selected_tab await ui.until_agent_ready() - opened = await app.thread_navigation.open(owner_mode=owner_tab, project_path=app.project_dir, target="gamma") + opened = await app.thread_navigation.open_named(owner_mode=owner_tab, project_path=app.project_dir, name="gamma") assert opened != owner_tab and ui.selected_tab == opened assert [tab.identity for tab in ui.tabs()] == [owner_tab, opened] view = ui.conversation diff --git a/tests/native/test_thread_open_during_slow_load.py b/tests/native/test_thread_open_during_slow_load.py index 46fde0352..f8cdd63d2 100644 --- a/tests/native/test_thread_open_during_slow_load.py +++ b/tests/native/test_thread_open_during_slow_load.py @@ -72,7 +72,7 @@ def journey_group(roster): await ui.until(lambda: "fresh-peer" in group.members, why="the channel's members") tabs_before = tuple(app.tab_order.names) - open_peer = partial(app.thread_navigation.open, owner_mode=owner, project_path=project, target="fresh-peer") + open_peer = partial(app.thread_navigation.open_named, owner_mode=owner, project_path=project, name="fresh-peer") assert await pilot.click(group.members["fresh-peer"]) opening = asyncio.create_task(open_peer()) duplicate = asyncio.create_task(open_peer()) diff --git a/tests/support/ui.py b/tests/support/ui.py index 202093883..e3e3b1aab 100644 --- a/tests/support/ui.py +++ b/tests/support/ui.py @@ -18,6 +18,8 @@ from pathlib import Path from typing import TYPE_CHECKING +from textual.actions import StyleAction + from rich.cells import cell_len from support.agents import AppAgents, command from toad import paths @@ -102,8 +104,8 @@ async def click(self, text: str, *, occurrence: int = 0) -> None: """Click the first cell of `text` where it is drawn on screen.""" @abstractmethod - def link_at(self, text: str, *, occurrence: int = 0) -> tuple[str, tuple] | None: - """The action a click on `text` would trigger, as (name, arguments), if it is a link.""" + def link_at(self, text: str, *, occurrence: int = 0) -> StyleAction | None: + """The action a click on `text` would run, if it is a link.""" @abstractmethod def hyperlink_at(self, text: str, *, occurrence: int = 0) -> str | None: @@ -336,7 +338,7 @@ async def click(self, text: str, *, occurrence: int = 0) -> None: ), f"the click on {text!r} left the screen" await self.step() - def link_at(self, text: str, *, occurrence: int = 0) -> tuple[str, tuple] | None: + def link_at(self, text: str, *, occurrence: int = 0) -> StyleAction | None: return self.app.screen.get_style_at(*self._cell_of(text, occurrence)).meta.get( "@click" ) diff --git a/tests/textual/test_block_menu_and_copy.py b/tests/textual/test_block_menu_and_copy.py index 0b2a9b01a..66ee30225 100644 --- a/tests/textual/test_block_menu_and_copy.py +++ b/tests/textual/test_block_menu_and_copy.py @@ -9,6 +9,7 @@ import pytest from textual.widgets import Static +from toad.link_click import OpenLineLink from toad.block_content import BlockContent from toad.block_navigation import ConversationBlock from toad.widgets.conversation import FocusPromptAction @@ -113,7 +114,7 @@ async def send_and_preview(tab, target): ) await ui.until_shows("clipboard-") expected = DirectProjectFileLink(path, original_root=workspace.project) - await ui.until(lambda: ui.link_at("clipboard-") == expected.click, + await ui.until(lambda: ui.link_at("clipboard-") == OpenLineLink(expected), why="the complete quoted attachment rendered as a file link") await ui.click("clipboard-") await ui.until(lambda: ui.selected_tab != tab, why="the image preview tab") diff --git a/tests/textual/test_existing_thread_link.py b/tests/textual/test_existing_thread_link.py index bb9f78312..45e038d77 100644 --- a/tests/textual/test_existing_thread_link.py +++ b/tests/textual/test_existing_thread_link.py @@ -3,6 +3,7 @@ from unittest.mock import patch from toad.line_blocks import OpenTarget +from toad.link_click import OpenLineLink from toad.message_rows import MessageRow from toad.widgets.comms_chat import CommsChatView from toad.widgets.goal_bar import GoalBar @@ -31,7 +32,7 @@ async def test_name_of_an_open_thread_selects_its_tab_and_creates_nothing(worksp await ui.select_tab(owner) await ui.open_channel("#team", actor="owner", owner=owner) await ui.until_shows(header.label) - await ui.until(lambda: app.screen.get_style_at(*ui.link_cell(header, sender)).meta.get("@click") == sender.click, + await ui.until(lambda: app.screen.get_style_at(*ui.link_cell(header, sender)).meta.get("@click") == OpenLineLink(sender), why="the sender drawn as a link") # The channel tab's unread count clears as its message is read; the tabs themselves must not change. def order(): @@ -49,8 +50,8 @@ def order(): assert not app.thread_navigation.pending # Repeated and self links reuse the very same mounted tab too. for _ in range(2): - assert await app.thread_navigation.open( - owner_mode=existing, project_path=ui.app.project_dir, target="peer") == existing + assert await app.thread_navigation.open_named( + owner_mode=existing, project_path=ui.app.project_dir, name="peer") == existing assert reads.call_count == 0 assert order() == opened @@ -82,7 +83,7 @@ async def test_name_clicked_in_a_narrow_direct_chat_reuses_the_open_thread_tab(w clip = app.screen._compositor.find_widget(underline).clip assert underline.region.intersection(clip).height == 1 await ui.until_shows(header.label) - await ui.until(lambda: app.screen.get_style_at(*ui.link_cell(header, sender)).meta.get("@click") == sender.click, + await ui.until(lambda: app.screen.get_style_at(*ui.link_cell(header, sender)).meta.get("@click") == OpenLineLink(sender), why="the sender drawn as a link") # The chat tab's title drops its unread count once the message is marked read. order = [tab.identity for tab in ui.tabs()] diff --git a/tests/textual/test_hovered_name_link.py b/tests/textual/test_hovered_name_link.py index a5c13dc8c..8e25f3567 100644 --- a/tests/textual/test_hovered_name_link.py +++ b/tests/textual/test_hovered_name_link.py @@ -1,6 +1,7 @@ """Textual-facing: the pointer over one name in a chat highlights that name only, not every equal name on screen.""" +from toad.link_click import OpenLineLink from toad.line_blocks import OpenTarget from toad.message_rows import MessageRow @@ -17,7 +18,7 @@ async def test_the_pointer_over_one_sender_highlights_that_one_and_leaves_equal_ app, pilot = ui.textual await ui.open_channel("#team", actor="owner", owner=ui.selected_tab) await ui.until(lambda: ui.visible_text().count(header.label) == 3, why="three messages from one sender") - await ui.until(lambda: all(app.screen.get_style_at(*ui.link_cell(header, sender, occurrence=n)).meta.get("@click") == sender.click for n in range(3)), + await ui.until(lambda: all(app.screen.get_style_at(*ui.link_cell(header, sender, occurrence=n)).meta.get("@click") == OpenLineLink(sender) for n in range(3)), why="three messages whose sender is the same link") cells = [ui.link_cell(header, sender, occurrence=n) for n in range(3)] body, _ = app.screen.get_widget_at(*cells[1]) @@ -30,6 +31,6 @@ async def test_the_pointer_over_one_sender_highlights_that_one_and_leaves_equal_ assert [app.screen.get_style_at(*cells[n]).bgcolor for n in (0, 2)] == [resting[0], resting[2]] # The destination on the same row is another link: it stays as drawn too. target = ui.link_cell(header, OpenTarget("#team"), occurrence=1) - assert app.screen.get_style_at(*target).meta["@click"] == OpenTarget("#team").click + assert app.screen.get_style_at(*target).meta["@click"] == OpenLineLink(OpenTarget("#team")) assert app.screen.get_style_at(*target).bgcolor == resting[1] assert ui.failure() is None diff --git a/tests/textual/test_navigation_route_reads.py b/tests/textual/test_navigation_route_reads.py index f72a56c35..eb46a8307 100644 --- a/tests/textual/test_navigation_route_reads.py +++ b/tests/textual/test_navigation_route_reads.py @@ -22,7 +22,7 @@ async def test_a_renamed_thread_reuses_its_open_tab_before_the_rename_reaches_th existing = app.session_navigation.source(other) await workspace.bind(existing, "metadata-peer") workspace.comms.registry.rename("metadata-peer", "metadata-renamed") - reused = await app.thread_navigation.open( - owner_mode=owner, project_path=ui.app.project_dir, target="metadata-peer" + reused = await app.thread_navigation.open_named( + owner_mode=owner, project_path=ui.app.project_dir, name="metadata-peer" ) assert reused == other and app.session_tracker.session_count == 2 diff --git a/tests/textual/test_scroll_down_bounce.py b/tests/textual/test_scroll_down_bounce.py index 4a50df82a..ca3b2bb64 100644 --- a/tests/textual/test_scroll_down_bounce.py +++ b/tests/textual/test_scroll_down_bounce.py @@ -11,6 +11,7 @@ from agent_comms.repo import Checkout from agent_comms.session_file import RegistrySessionLocation from agent_comms.threads import NativeThread +from toad.link_click import OpenLineLink from toad.app import ToadApp from toad.preferences import SETTINGS from toad.screens.main import MainScreen @@ -153,7 +154,7 @@ async def test_scroll_down_through_evicted_pages_never_moves_up(workspace, recor for row in page.fragment_placement(fragment).rows: x = 0 for segment in page.render_line(row, page._paint_state): - if segment.style is not None and segment.style.meta.get("@click") == fragment.details_link.click: + if segment.style is not None and segment.style.meta.get("@click") == OpenLineLink(fragment.details_link): links.append((x, row)) x += segment.cell_length assert links, "The second occurrence has no rendered disclosure link" diff --git a/tests/textual/test_wire_message_lines.py b/tests/textual/test_wire_message_lines.py index 01d5d6e7b..401a6dca1 100644 --- a/tests/textual/test_wire_message_lines.py +++ b/tests/textual/test_wire_message_lines.py @@ -9,6 +9,7 @@ from agent_comms.messages import InfoKind, Message from textual.app import App +from toad.link_click import OpenLineLink from toad.line_blocks import Divider, OpenTarget, Span, UserRole from toad.markdown_links import LinkContext from toad.notification_status import CheckedNotifications @@ -60,8 +61,8 @@ def test_rows_fit_the_width_wrap_on_words_and_keep_drawn_names_clickable_at_ever drawn = "\n".join(text) if width >= 80: assert all(name in drawn for name in names), (width, text) - expected = {OpenTarget(name).click for name in names if name in drawn} - actual = clicks(rows) - {shown.details_link.click} + expected = {OpenLineLink(OpenTarget(name)) for name in names if name in drawn} + actual = clicks(rows) - {OpenLineLink(shown.details_link)} assert actual == expected, (width, text, actual, expected) @@ -72,7 +73,7 @@ def test_divider_acquisition_reuses_equal_fades_rewraps_and_repaints_changed_sty acquired = NoRows().plain(36, RichLineRenderer(current_theme, 36), (block,)) first = acquired.lines assert tuple(first) == RichLineRenderer(current_theme, 36).rows((block,)) - assert link.click in clicks(first) + assert OpenLineLink(link) in clicks(first) equal_fade = replace(current_theme, styles=dict(current_theme.styles)) assert acquired.plain(36, RichLineRenderer(equal_fade, 36), (block,)) is acquired @@ -86,7 +87,7 @@ def test_divider_acquisition_reuses_equal_fades_rewraps_and_repaints_changed_sty assert changed is not acquired assert tuple(changed.lines) == RichLineRenderer(changed_fade, 80).rows((block,)) assert tuple(changed.lines) != tuple(acquired.lines) - assert link.click in clicks(changed.lines) + assert OpenLineLink(link) in clicks(changed.lines) def test_opening_notification_details_draws_more_rows_than_the_collapsed_message():