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
4 changes: 2 additions & 2 deletions src/toad/conversation_kind.py
Original file line number Diff line number Diff line change
Expand Up @@ -300,8 +300,8 @@ def prompt(self, project: Checkout):

async def status_text(self, comms: Comms, *, read_only: bool) -> str:
def read():
snapshot = comms.registry.snapshot()
return comms.configuration.answer(self.peer.require_current(snapshot).name)
references = CurrentDisplayReferences.from_registry(str(comms.root), comms.registry.snapshot())
return comms.configuration.answer(self.destination(references).name)

return (await asyncio.to_thread(read)).status_label

Expand Down
1 change: 1 addition & 0 deletions src/toad/widgets/comms_chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ async def initialize_view(self) -> None:
f"Conversation root {self._bound_root} differs from observed root {shared.root}"
)
comms = shared
await self.app.coordination_access.current_sidebar()
self.message_history.initialize_source(WireSource(
comms, self.conversation, self.project_path, self._me,
IrcMessageStyle(),
Expand Down
55 changes: 26 additions & 29 deletions src/toad/widgets/thread_comms.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
from toad.navigation_target import NavigationTarget, ThreadTarget

import asyncio
from collections.abc import Mapping
from dataclasses import dataclass, field

from agent_comms.display_order import ThreadSort
Expand Down Expand Up @@ -45,9 +44,10 @@


def entry_target(entry: RelationshipEntry) -> NavigationTarget:
for person in entry.person.present():
return ThreadTarget(person.thread.name)
return NavigationTarget.named(entry.target)
person = entry.person
if person is None:
return NavigationTarget.named(entry.target)
return ThreadTarget(person.thread.name)


@dataclass
Expand Down Expand Up @@ -111,9 +111,7 @@ class RelationshipRow(ThreadStatusRow, actions=(CopyIdentityAction,)):
available = True
entry: RelationshipEntry | None = None

def present(
self, entry: RelationshipEntry, rows: Mapping[str, ThreadRowModel]
) -> None:
def present(self, entry, model) -> None:
previous, self.entry = self.entry, entry
self.available, self.target = entry.available, entry_target(entry)
if not entry.available:
Expand All @@ -122,39 +120,36 @@ def present(
Content(f"? {entry.target}\n Unavailable · Ctrl+C copies name"),
layout=False,
)
self._show_entry_detail(entry)
return
for person in entry.person.present():
model = rows.get(person.thread.name)
if model is not None and model.incarnation == person.thread.incarnation:
elif entry.person is not None:
if (
model is not None
and model.incarnation == entry.person.thread.incarnation
):
self.workers.cancel_group(self, "relationship-row")
self._show_person(entry, model)
elif previous != entry:
self.run_worker(
self._present_person(entry, person),
self._present_person(entry),
group="relationship-row",
exclusive=True,
)
return
if previous != entry:
elif previous != entry:
self.set_label(entry.target)
self._show_entry_detail(entry)

def _show_entry_detail(self, entry):
self.tooltip = (
Content(f"{entry.target}\n{entry.detail}")
if entry.detail
else entry.target
)
if entry.person is None:
self.tooltip = (
Content(f"{entry.target}\n{entry.detail}")
if entry.detail
else entry.target
)

def _show_person(self, entry, model):
self.show(model)
if entry.detail:
self._show_entry_detail(entry)
self.tooltip = Content(f"{entry.target}\n{entry.detail}")

async def _present_person(self, entry, person):
async def _present_person(self, entry):
model = await self.app.preparation.run_thread(
ThreadRowModel.of, person, UnobservedUnread()
ThreadRowModel.of, entry.person, UnobservedUnread()
)
if self.entry == entry:
self._show_person(entry, model)
Expand Down Expand Up @@ -212,8 +207,7 @@ def keys_for_threads(self, names: frozenset[str]) -> frozenset[str]:
return frozenset(
entry.target
for entry in self.model.entries.values()
for person in entry.person.present()
if person.thread.name in names
if entry.person is not None and entry.person.thread.name in names
)

def new_row(self, key: str):
Expand All @@ -223,7 +217,10 @@ def new_row(self, key: str):
def show_member(self, key: str, row: RelationshipRow) -> None:
entry = self.model.entries[key]
threads = self.query_ancestor(ThreadCommsSidebar).model_state.shown.threads.rows
row.present(entry, threads)
row.present(
entry,
threads.get(entry.person.thread.name) if entry.person is not None else None,
)

def sync_members(self, changed: frozenset[str]) -> None:
"""Reconcile member rows from this group's entries and the tree's derived thread rows."""
Expand Down
5 changes: 5 additions & 0 deletions src/toad/wire_history.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

from agent_comms.comms import Comms
from agent_comms.repo import Checkout
from agent_comms.display_references import CurrentDisplayReferences
from agent_comms.historical_views import (
ConversationHistoryView,
ConversationObservation,
Expand Down Expand Up @@ -300,6 +301,10 @@ class WireSource:
def viewer(self) -> NamedViewer:
return self.comms.views.display_viewer(self.project)

def references(self) -> CurrentDisplayReferences:
"""Resolve stable destinations through this read's registry cut."""
return CurrentDisplayReferences.from_registry(str(self.comms.root), self.comms.registry.snapshot())

def observe(self, seen: ConversationObservation) -> SourceObservation:
"""The observed state owns how changed or unchanged history is drawn."""
revision = self.conversation.history_view.revision(
Expand Down
63 changes: 0 additions & 63 deletions tests/textual/test_channel_updates_and_returns.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
"""A real shared observer updates a shown channel and leaves its parked view alone."""

import asyncio

async def test_live_update_paints_parked_channel_reads_nothing_and_return_shows_the_update(workspace):
from toad.widgets.comms_chat import CommsChatView, session_thread_name

Expand Down Expand Up @@ -34,64 +32,3 @@ async def test_live_update_paints_parked_channel_reads_nothing_and_return_shows_
await ui.until_shows("PARKED UPDATE")
await ui.until(lambda: bool(access.notification_interest.reads), why="the returned channel to resubscribe")
assert app.selected_session.query_one(CommsChatView) is chat


async def test_first_wire_page_paints_before_pending_sidebar_and_keeps_source_fences(workspace):
from agent_comms.launch_root import LaunchRoot
from support.roots import installed_wire
from toad.preferences import SETTINGS
from toad.widgets.channel_prompt import ChannelPrompt
from toad.widgets.comms_chat import CommsChatView

sender = workspace.thread("sender", "first-page")
workspace.thread("reader", "first-page")
workspace.comms.messaging.send("sender", "#first-page", "FIRST_PAGE_WITHOUT_SIDEBAR")
other = installed_wire(workspace.roots.attempt / "other-route")
async with workspace.app(size=(113, 34)) as ui:
app = ui.app
access = app.coordination_access
await access.current_sidebar()
observed = access.observation.observed()
process = observed.process
# Join the actual receiver, not a replacement service or fake answer.
# Its child keeps running and the real sidebar reply remains in its pipe.
await process._reader.aclose()
async with asyncio.timeout(20), asyncio.TaskGroup() as work:
try:
app.saved_settings.toggle(SETTINGS.sidebar.show_archived)
sidebar = work.create_task(access.current_sidebar())
references = work.create_task(
observed.current_references(access, str(workspace.comms.root)),
)
await ui.until(lambda: bool(access.sidebar_waiters),
why="the genuine changed-interest sidebar answer to remain pending")
opening = work.create_task(ui.open_channel("#first-page", actor="reader"))
await ui.until_shows("FIRST_PAGE_WITHOUT_SIDEBAR")
chat = app.selected_session.query_one(CommsChatView)
history = chat.message_history
source = history.page_source
assert not history.undrawn
assert not sidebar.done() and not references.done()
assert not chat.query_one(ChannelPrompt)._candidates
finally:
# Reattach the same observer's one reader; deliver its original reply.
process.start_receiving(observed.receive, observed.receive_failed)
mode = await opening
answer = await sidebar
catalog = await references
assert catalog.thread(sender.incarnation).name == sender.name
assert answer.require_snapshot().references.thread(sender.incarnation).name == sender.name
await ui.until(
lambda: any(candidate.name == sender.name
for candidate in chat.query_one(ChannelPrompt)._candidates),
why="the genuine channel mention label to finish after its sidebar answer",
)
assert chat.agent_info.plain == chat.conversation.label(catalog)
snapshot = history.source_snapshot()
source = history.page_source
with LaunchRoot.from_path(other.root).selected():
assert not await history.source_is_current(snapshot)
assert await history.source_is_current(snapshot)
await app.session_navigation.close(mode)
assert source.closed
assert not await source.route_is_current()
Loading