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
15 changes: 11 additions & 4 deletions src/toad/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,20 +56,27 @@ def check_directory(path: str) -> None:
async def get_agent_binding(launch_agent: str) -> AgentBinding:
if not launch_agent:
return NoAgent()
launch_agent = launch_agent.lower()
requested = launch_agent.lower()

from toad.agents import read_agents

agents = await read_agents()

for agent_data in agents.values():
if (
agent_data.short_name.lower() == launch_agent
or agent_data.identity.lower() == launch_agent
agent_data.short_name.lower() == requested
or agent_data.identity.lower() == requested
):
return AgentAttached(agent_data)

return NoAgent()
known = ", ".join(sorted(
f"{agent.short_name} ({agent.identity})" if agent.short_name else agent.identity
for agent in agents.values()
))
raise click.BadParameter(
f"Unknown agent {launch_agent!r}. Known agents: {known}",
param_hint="-a / --agent",
)


class DefaultCommandGroup(click.Group):
Expand Down
42 changes: 23 additions & 19 deletions src/toad/widgets/committed_presentation.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,31 @@

from abc import ABC, abstractmethod
from collections.abc import Callable, Iterable
from functools import partial
from dataclasses import dataclass
from contextlib import asynccontextmanager
from dataclasses import dataclass
from functools import partial
from typing import TYPE_CHECKING

from agent_comms.acp_extension import InputStartedUpdate
from agent_comms.sent_message import OutgoingMessageReference, PublishedSentMessage
from agent_comms.transcripts import TranscriptCursor, TranscriptPage
from textual.widget import Widget

from toad.core.source_events import TranscriptCoverage
from toad.transcript_state import CommitTranscript, PreparedReplacementViewportRequest
from toad.widgets.history_anchor import LatestPosition
from agent_comms.sent_message import PublishedSentMessage, OutgoingMessageReference
from toad.core.source_events import TranscriptCoverage
from agent_comms.acp_extension import InputStartedUpdate

from toad.widgets.presentation_window import protected_presentations

if TYPE_CHECKING:
from toad.widgets.transcript_history import CommittedHistory, TranscriptHistory
from toad.transcript_preparation import PreparedTranscriptPage
from toad.widgets.conversation import Conversation
from toad.widgets.history_anchor import HistoryWindow
from toad.transcript_preparation import PreparedTranscriptPage
from toad.widgets.transcript_fragments import BusMessageFragment, SentMessageFragment, TranscriptFragment
from toad.widgets.transcript_fragments import (
BusMessageFragment,
SentMessageFragment,
TranscriptFragment,
)
from toad.widgets.transcript_history import CommittedHistory, TranscriptHistory

@dataclass(frozen=True)
class CommitEvidence:
Expand Down Expand Up @@ -190,7 +194,7 @@ class PreparedSourceCommit(PreparedCommit, ABC):
page: PreparedTranscriptPage

@classmethod
async def capture(cls, view, history, page, sequences):
async def capture(cls, view: Conversation, history: CommittedHistory, page: TranscriptPage, sequences: frozenset[int]):
from toad.transcript_preparation import TranscriptPageRenderTask
from toad.work_preparation import RenderPreparation

Expand All @@ -201,7 +205,7 @@ async def capture(cls, view, history, page, sequences):
def outgoing(self) -> frozenset[OutgoingMessageReference]:
return self.history.outgoing_coverage | self.page.outgoing_coverage

async def draw(self, cursor, is_current, live_blocks) -> bool:
async def draw(self, cursor, is_current: Callable[[], bool], live_blocks) -> bool:
history = self.history
async with history.window.history_lock:
if not history.source_ready or not is_current():
Expand All @@ -214,15 +218,15 @@ async def publish(self, cursor: TranscriptCursor, is_current: Callable[[], bool]


class PreparedAdvanceCommit(PreparedSourceCommit):
async def publish(self, cursor, is_current):
async def publish(self, cursor, is_current: Callable[[], bool]):
return await self.history.advance_committed(cursor, is_current)


@dataclass(frozen=True)
class PreparedPageCommit(PreparedSourceCommit):
history: TranscriptHistory

async def publish(self, cursor, is_current):
async def publish(self, cursor, is_current: Callable[[], bool]):
return await self.history.show_newest(PreparedReplacementViewportRequest(
LatestPosition.hold(self.history.window), self.page), publication_current=is_current)

Expand Down Expand Up @@ -271,15 +275,15 @@ def __init__(self, window: HistoryWindow) -> None:
def current(self, window: HistoryWindow) -> bool:
return window.follows_tail and window.scroll_revision == self.revision

async def extend(self, view, history, page, captured, is_current):
async def extend(self, view, history, page, captured, is_current: Callable[[], bool]):
return await PreparedAdvanceCommit.capture(view, history, page, TranscriptCoverage(page.events).sequences
| history.covered_sequences(required_sequences(captured)))

def finish(self, view, cursor):
def finish(self, view, cursor) -> None:
view.transcript.binding.painted(view.transcript, cursor)
view.call_after_refresh(self.restore_reader, view.window)

def restore_reader(self, window):
def restore_reader(self, window) -> None:
# The publication's captured intent owns its deferred layout effect as
# well. A later scroll or source return cannot inherit an older anchor.
if self.current(window):
Expand All @@ -291,7 +295,7 @@ def current(self, window: HistoryWindow) -> bool:
# Retirement checks the current viewport again when applying the change.
return not window.follows_tail

def permits(self, view, candidates):
def permits(self, view, candidates) -> bool:
candidates = tuple(candidates)
if not super().permits(view, candidates):
return False
Expand All @@ -306,14 +310,14 @@ def permits(self, view, candidates):
return not any(widget in visible and visible[widget][0].overlaps(viewport)
for widget in candidates)

async def extend(self, view, history, page, captured, is_current):
async def extend(self, view, history, page, captured, is_current: Callable[[], bool]):
required = required_sequences(captured)
known = TranscriptCoverage(page.events).sequences | history.covered_sequences(required)
covered = frozenset(sequence for sequence in required
if page.after.covers_incoming(sequence))
return PreparedCommit(history, known | covered)

def commit(self, prepared, cursor):
def commit(self, prepared, cursor) -> None:
prepared.history.retain_committed(cursor)

def commit_plan(window: HistoryWindow) -> CommitPlan:
Expand Down
2 changes: 1 addition & 1 deletion src/toad/widgets/conversation.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
from typing import TYPE_CHECKING, Callable

from acp import schema as acp_protocol
from agent_comms.goal_presentation import GoalExecution, IdleGoalExecution
from agent_comms.channel_targets import CommandTarget
from agent_comms.goal_presentation import GoalExecution, IdleGoalExecution
from agent_comms.goals import NoGoal
from agent_comms.mro_dispatch import MroDispatch, handles
from agent_comms.thread_presentation import NoThreadPresentation, ThreadPresentation
Expand Down
28 changes: 18 additions & 10 deletions src/toad/widgets/history_anchor.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,28 @@

from __future__ import annotations

import asyncio
from abc import ABC, abstractmethod
from collections.abc import Iterable
from contextlib import AsyncExitStack, asynccontextmanager, contextmanager
from dataclasses import dataclass, field, replace
from functools import cached_property
from contextlib import AsyncExitStack, asynccontextmanager, contextmanager
import asyncio
from typing import TYPE_CHECKING
from weakref import WeakSet

from textual.widget import Widget
from toad.widget_view import WorkspaceHosted
from textual import events
from textual.containers import VerticalScroll
from toad.widgets.presentation_window import DirectionalPreparation, PresentationBudget, OLDER_END, NEWER_END
from textual.geometry import Size
from textual.widget import Widget

from toad.transcript_state import LatestViewportRequest
from toad.widget_view import WorkspaceHosted
from toad.widgets.presentation_window import (
NEWER_END,
OLDER_END,
DirectionalPreparation,
PresentationBudget,
)

if TYPE_CHECKING:
from toad.transcript_preparation import PageFragmentRange
Expand Down Expand Up @@ -106,7 +114,7 @@ def preparation_start(self, history) -> tuple[int, int]:
"""Where preparation begins when no message supplies a more exact place."""
return 0, history.pages[0].prepared.start

async def prepare(self, history, current, target, selected, rows_needed, limit):
async def prepare(self, history, current, target, selected, rows_needed, limit: int):
"""Fill the reader's earlier rows, then the later rows in the remaining budget."""
start = self.preparation_start(history)
anchor = history.pages[start[0]].prepared
Expand Down Expand Up @@ -134,7 +142,7 @@ class TailPosition(ReaderPosition):
def preparation_start(self, history) -> tuple[int, int]:
return len(history.pages) - 1, len(history.pages[-1].prepared.fragments)

async def prepare(self, history, current, target, selected, rows_needed, limit):
async def prepare(self, history, current, target, selected, rows_needed, limit: int):
return await OLDER_END.walk(history, current, target, selected,
self.preparation_start(history), rows_needed, limit)

Expand Down Expand Up @@ -262,7 +270,7 @@ class UnreachedPosition(ReaderPosition):

position: ReaderPosition

async def prepare(self, history, current, target, selected, rows_needed, limit):
async def prepare(self, history, current, target, selected, rows_needed, limit: int):
return await self.position.prepare(history, current, target, selected, rows_needed, limit)

def bodies(self, window: "HistoryWindow") -> tuple[Widget, ...] | None:
Expand Down Expand Up @@ -765,7 +773,7 @@ def watch_scroll_y(self, old_value: float, new_value: float) -> None:
self.anchor(True)
self.tail_follow_changed()

def _size_updated(self, size, virtual_size, container_size, layout=True) -> bool:
def _size_updated(self, size: Size, virtual_size: Size, container_size: Size, layout: bool=True) -> bool:
# Native size commit owns scrollbar clamping. Compensate that movement
# here, without treating unrelated Screen layouts as reader restoration.
with self.layout_scroll():
Expand Down Expand Up @@ -794,7 +802,7 @@ def history_mutation_root(self) -> Widget | None:
return None
return self if self._history_mutation_root is None else self._history_mutation_root

def visible_history_items(self, items):
def visible_history_items(self, items: Iterable[Widget]):
"""Borrow this window's clipped cohort from the original native scene."""
compositor = self.screen._compositor
visible = compositor.published_widgets
Expand Down
Loading
Loading