diff --git a/pyproject.toml b/pyproject.toml index f0e2ba5df..8f7868347 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,8 +38,8 @@ dependencies = [ members = ["toad"] [tool.uv.sources] -ui-theme = { git = "https://github.com/OpenHCSDev/ui-theme.git", rev = "5e58d8b5907cec4dbc8810a9329aaf902f69f675" } -agent-comms = { git = "https://github.com/OpenHCSDev/agent-comms.git", rev = "00a3d74d4b1ab8962941d84a764b3e8c2e57013d" } +ui-theme = { git = "https://github.com/OpenHCSDev/ui-theme.git", rev = "aaa35af5948c6025d33fc178f6bd687d9dc5c322" } +agent-comms = { git = "https://github.com/OpenHCSDev/agent-comms.git", rev = "2fb445b94fc22ed58a365709f4a873ca53e4a303" } textual = { git = "https://github.com/OpenHCSDev/textual.git", rev = "18f4e7a67403d56846ef5abf55c93e3e0639be33" } textual-image = { git = "https://github.com/OpenHCSDev/textual-image.git", rev = "1f1d8b8ea3c30b39da3b14ea0ac0786ce9c49a80" } textual-diff-view = { git = "https://github.com/OpenHCSDev/textual-diff-view.git", rev = "8fa7d4d0db993ea3b761c2760ca9b6a56a6251e9" } diff --git a/src/toad/acp/agent.py b/src/toad/acp/agent.py index f88cae87d..e3c1b908d 100644 --- a/src/toad/acp/agent.py +++ b/src/toad/acp/agent.py @@ -4,7 +4,7 @@ from toad.acp.client_files import FileClientRequestOwner from toad.acp.tool_calls import SessionToolCalls from toad.acp.agent_configuration import ModelConfigurationSetting, ThinkingConfigurationSetting -from toad.acp.context_measurement import ContextMeasurement, ContextUnavailable +from toad.acp.context_measurement import ContextMeasurement from toad.acp.agent_log import AgentLog from toad.acp.agent_process import AgentProcess from toad.acp.agent_controller import AgentController @@ -24,6 +24,7 @@ ) from agent_comms.acp_failure import ACPFailure from agent_comms.field_codec import FieldCodec +from agent_comms.context_reading import UnmeasuredContext from agent_comms.goal_actions import GoalAction from agent_comms.goal_presentation import GoalSnapshot from agent_comms.goals import Goal @@ -111,7 +112,7 @@ def __init__( self.presentation.log_path.unlink(missing_ok=True) else: self.presentation.log_path = paths.get_log() / log_filename - self.context_measurement = ContextUnavailable("Native owner has not reported context usage") + self.context_measurement = ContextMeasurement.saved(UnmeasuredContext()) @property def command(self) -> str | None: diff --git a/src/toad/acp/comms_updates.py b/src/toad/acp/comms_updates.py index 58744101d..a1d895c7b 100644 --- a/src/toad/acp/comms_updates.py +++ b/src/toad/acp/comms_updates.py @@ -25,9 +25,11 @@ TranscriptSnapshotUpdate, TurnChangedUpdate, ) +from agent_comms.context_reading import UnmeasuredContext from agent_comms.mro_dispatch import MroDispatch, handles from agent_comms.thread_presentation import ThreadPresentation from toad.acp.client_session import ClientSessionRequest +from toad.acp.context_measurement import ContextMeasurement from toad.core import events as core_events @@ -122,7 +124,6 @@ def coordination_changed(self, update: CoordinationChangedUpdate) -> None: from dataclasses import replace from pathlib import Path - from .context_measurement import ContextMeasurement from agent_comms.route_selection import child_root agent = self.agent @@ -144,8 +145,7 @@ def goal_changed(self, update: GoalChangedUpdate) -> None: @handles(CompactionChangedUpdate) def compaction_changed(self, update: CompactionChangedUpdate) -> None: - from .context_measurement import ContextUnavailable - self.agent.context_measurement = ContextUnavailable("Native context measurement is pending") + self.agent.context_measurement = ContextMeasurement.saved(UnmeasuredContext()) self.agent.update_status_line() self.agent.events.publish(core_events.CommsUpdated(update, self.session_id)) diff --git a/src/toad/acp/context_measurement.py b/src/toad/acp/context_measurement.py index 1bad8c97f..7f6ad9225 100644 --- a/src/toad/acp/context_measurement.py +++ b/src/toad/acp/context_measurement.py @@ -5,6 +5,7 @@ from math import floor from typing import NamedTuple from rich.text import Text as RichText +from agent_comms.context_reading import ContextReading, UnmeasuredContext class Cost(NamedTuple): """A cost with associated currency.""" @@ -24,41 +25,27 @@ def __format__(self, _specifier: str) -> str: class ContextMeasurement(ABC): - available = False - @abstractmethod def status(self) -> RichText: ... @classmethod - def saved(cls, usage): - if usage is None: - return ContextUnavailable("Native owner has not reported context usage") - return SavedContextMeasurement(usage.used, usage.size) + def saved(cls, reading: ContextReading) -> ContextMeasurement: + return SavedContextMeasurement(reading) @classmethod def live(cls, used, size, cost): # The ACP adapter uses zero usage after compaction as unmeasured. # Decode that boundary sentinel once; never paint it as known 0%. if used <= 0 or size <= 0: - return ContextUnavailable("Native owner has not supplied a usable context measurement") + return SavedContextMeasurement(UnmeasuredContext()) return LiveContextMeasurement(used, size, Cost(cost.amount, cost.currency) if cost else None) @dataclass(frozen=True) -class ContextUnavailable(ContextMeasurement): - reason: str - - def status(self) -> RichText: - return RichText(f"Context unavailable · {self.reason}") - - -@dataclass(frozen=True) -class MeasuredContext(ContextMeasurement): +class LiveContextMeasurement(ContextMeasurement): used: int size: int cost: Cost | None = None - available = True - source_label = "" @property def percentage_used(self): @@ -71,16 +58,14 @@ def percentage_display(self): def status(self) -> RichText: parts = [RichText.assemble(f"{self.used / 1000:.1f}K (", (self.percentage_display, "bold"), ")")] - if self.source_label: - parts.append(RichText(self.source_label)) if self.cost is not None: parts.append(RichText.assemble((f"{self.cost}", "bold"))) return RichText(" • ").join(parts) -class LiveContextMeasurement(MeasuredContext): - pass - +@dataclass(frozen=True) +class SavedContextMeasurement(ContextMeasurement): + reading: ContextReading -class SavedContextMeasurement(MeasuredContext): - source_label = "last response" + def status(self) -> RichText: + return RichText(self.reading.status_text()) diff --git a/src/toad/conversation_kind.py b/src/toad/conversation_kind.py index b8ef0e093..4a93c7cc3 100644 --- a/src/toad/conversation_kind.py +++ b/src/toad/conversation_kind.py @@ -8,6 +8,9 @@ from agent_comms.declared_family import DeclaredFamily from agent_comms.comms import Comms from agent_comms.message_page import MessagePage +from textual.app import ComposeResult +from toad.widgets.native_history import NativeHistory +from toad.widgets.session_details import SessionDetails class ConversationKind(DeclaredFamily, affix="Conversation"): @@ -84,16 +87,18 @@ def read_only(cls, catalog, target): return False @classmethod - async def update_roster(cls, view, comms): - pass + @abstractmethod + async def update_mentions(cls, view, comms) -> None: ... @classmethod - async def agent_info(cls, comms, target): - return None + @abstractmethod + async def status_text(cls, comms, target, *, read_only: bool) -> str: ... @classmethod @abstractmethod - def activity_widget(cls, view): ... + def compose_activity(cls, view) -> ComposeResult: + """Compose this kind's activity using the existing view state.""" + ... @classmethod @abstractmethod @@ -150,10 +155,12 @@ def current_identity(cls, page: MessagePage) -> tuple | None: ) @classmethod - def activity_widget(cls, view): - from toad.widgets.channel_participants import ChannelParticipants + def compose_activity(cls, view) -> ComposeResult: + return () - return ChannelParticipants() + @classmethod + async def status_text(cls, comms, target, *, read_only: bool) -> str: + return "Read-only saved view" if read_only else "" @classmethod def prompt(cls, target): @@ -166,14 +173,11 @@ def read_only(cls, catalog, target): return catalog.is_view_target(target) @classmethod - async def update_roster(cls, view, comms): - from toad.widgets.channel_participants import ChannelParticipants + async def update_mentions(cls, view, comms): from toad.widgets.channel_prompt import ChannelPrompt - # The composer consumes membership and thread presentation, not the - # actor's inbox counts or channel activity clocks. Borrow the observed - # service's sidebar model rather than reading the same registry, - # activity and catalog again after every channel page. + # Mention completion consumes the observer's membership snapshot, + # not another registry/catalog read after every channel page. history = view.message_history source = history.source_snapshot() target = view.target @@ -182,19 +186,10 @@ async def update_roster(cls, view, comms): snapshot = answer.require_snapshot() observed = access.observation if observed is None or observed.service is not comms: - raise ValueError("Channel roster belongs to another Comms service") - rows = observed.sidebar.threads.rows - - def prepare(): - people = tuple(rows[person.thread.name] for person in snapshot.participants(target) - if person.thread.name in rows) - return (ChannelParticipants.prepare_participants(people), - snapshot.mention_candidates(target)) - - participants, mentions = await view.app.preparation.run_thread(prepare) + raise ValueError("Channel mentions belong to another Comms service") + mentions = await view.app.preparation.run_thread(snapshot.mention_candidates, target) if not await history.source_is_current(source) or view.target != target: return - view.query_one(ChannelParticipants).update_participants(*participants) view.query_one(ChannelPrompt).set_mention_candidates(mentions) @classmethod @@ -278,11 +273,16 @@ def label(cls, target: str) -> str: return f"@{target}" @classmethod - def activity_widget(cls, view): - from toad.widgets.session_details import SessionDetails + def compose_activity(cls, view) -> ComposeResult: + view.thread_activity = SessionDetails( + history=NativeHistory(), + status_source=lambda: view.thread_activity.row, + ) + yield view.thread_activity - view.thread_activity = SessionDetails() - return view.thread_activity + @classmethod + async def update_mentions(cls, view, comms) -> None: + """Direct-message composers have no channel mention roster.""" @classmethod def prompt(cls, target): @@ -291,8 +291,8 @@ def prompt(cls, target): return Prompt(simple_input=True, placeholder=cls.placeholder(target)) @classmethod - async def agent_info(cls, comms, target): - return await asyncio.to_thread(comms.configuration.answer, target) + async def status_text(cls, comms, target, *, read_only: bool) -> str: + return (await asyncio.to_thread(comms.configuration.answer, target)).status_label @classmethod def seen_page(cls, history, seen): diff --git a/src/toad/core/events.py b/src/toad/core/events.py index bff695b2f..f601a594b 100644 --- a/src/toad/core/events.py +++ b/src/toad/core/events.py @@ -363,6 +363,7 @@ class Subscription: """A subscriber owns this resource for as long as it wants publications.""" stream: CoreEventStream + event_type: type[CoreEvent] listener: InitVar[Callable[[CoreEvent, Subscription], object]] _listener: Callable[[], Callable[[CoreEvent, Subscription], object] | None] = field(init=False, repr=False) @@ -412,11 +413,13 @@ def publisher(self) -> object: raise RuntimeError("The publication resource outlived its original owner") return publisher - def subscribe(self, listener: Callable[[CoreEvent, Subscription], object]) -> Subscription: - subscription = Subscription(self, listener) + def subscribe(self, event_type: type[CoreEvent], + listener: Callable[[CoreEvent, Subscription], object]) -> Subscription: + subscription = Subscription(self, event_type, listener) self.subscriptions.add(subscription) return subscription def publish(self, event: CoreEvent) -> None: for subscription in tuple(self.subscriptions): - subscription.publish(event) + if isinstance(event, subscription.event_type): + subscription.publish(event) diff --git a/src/toad/core_event_carrier.py b/src/toad/core_event_carrier.py index 5832d0fa8..fb474b412 100644 --- a/src/toad/core_event_carrier.py +++ b/src/toad/core_event_carrier.py @@ -43,7 +43,7 @@ def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) def subscribe_core(self, stream: CoreEventStream) -> Subscription: - subscription = stream.subscribe(self.post_core_event) + subscription = stream.subscribe(CoreEvent, self.post_core_event) self._core_subscriptions.add(subscription) return subscription @@ -67,9 +67,10 @@ async def consume_handlers(self, message, handlers, *args, **kwargs): def observe_core(self, stream: CoreEventStream) -> Subscription: """Broadcast observations terminate at their original native recipient.""" for subscription in self._core_subscriptions: - if subscription.stream is stream and subscription.active: + if (subscription.stream is stream and subscription.event_type is CoreEvent + and subscription.active): return subscription - subscription = stream.subscribe(self.post_core_observation) + subscription = stream.subscribe(CoreEvent, self.post_core_observation) self._core_subscriptions.add(subscription) return subscription @@ -82,7 +83,8 @@ def retire_core_observations(self, stream: CoreEventStream) -> None: if subscription.stream is stream: self.retire_core(subscription) - def observe_core_callback(self, stream: CoreEventStream, callback) -> Subscription: + def observe_core_callback(self, stream: CoreEventStream, + event_type: type[CoreEvent], callback) -> Subscription: """An original deferred operation receives relief inside its native pump.""" recipient = ref(self) try: @@ -104,7 +106,7 @@ def publish(event, subscription) -> None: message.bubble = False receiver.post_message(message) - subscription = stream.subscribe(publish) + subscription = stream.subscribe(event_type, publish) self._core_subscriptions.add(subscription) return subscription diff --git a/src/toad/mcp_inventory.py b/src/toad/mcp_inventory.py index 0e5d37ade..3618a1e9f 100644 --- a/src/toad/mcp_inventory.py +++ b/src/toad/mcp_inventory.py @@ -13,6 +13,7 @@ from pathlib import Path from agent_comms.field_codec import FieldCodec +from agent_comms.launch_runtime import LaunchRuntime from toad import mcp_declarations @@ -53,7 +54,7 @@ def installed_mcp_command() -> tuple[str, str]: """Use MCP shipped inside the same verified native package as Comms.""" from agent_comms.native_pi import NativePiRpcLaunch - package = NativePiRpcLaunch.package_for_command("pi") + package = NativePiRpcLaunch.package_for_command("pi", LaunchRuntime.running()) node = shutil.which("node") if node is None: raise FileNotFoundError("Node is unavailable") diff --git a/src/toad/session_navigation.py b/src/toad/session_navigation.py index 627fe7e5a..c5f08c742 100644 --- a/src/toad/session_navigation.py +++ b/src/toad/session_navigation.py @@ -47,7 +47,8 @@ def direct_messages(self, target: str | None = None): def bind_events(self) -> None: self.app.subscribe_core(self.events) - self.app.observe_core_callback(self.app.coordination_access.events, self.observed) + self.app.observe_core_callback(self.app.coordination_access.events, + core_events.CoordinationObserved, self.observed) async def reveal(self) -> None: from toad.widgets.comms_sidebar import CommsSidebar diff --git a/src/toad/transcript_publication.py b/src/toad/transcript_publication.py index e240aff72..1209f111b 100644 --- a/src/toad/transcript_publication.py +++ b/src/toad/transcript_publication.py @@ -15,6 +15,7 @@ from textual.worker import Worker, WorkerCancelled from textual.await_complete import AwaitComplete from toad.widgets.history_anchor import HeldReaderDestination +from toad.core.events import CoordinationObserved if TYPE_CHECKING: from toad.screens.session_view import SessionOrigin @@ -380,7 +381,8 @@ def defer(self, publication: TranscriptPublication) -> None: return self.observation.close() self.observation.callback(view.retire_core, - view.observe_core_callback(view.app.coordination_access.events, self.resume)) + view.observe_core_callback(view.app.coordination_access.events, + CoordinationObserved, self.resume)) def resume(self, _event=None) -> bool: """Resume retained work only at an existing source/preparation signal.""" diff --git a/src/toad/transcript_source_preparation.py b/src/toad/transcript_source_preparation.py index bc5af25e9..7fae86bb9 100644 --- a/src/toad/transcript_source_preparation.py +++ b/src/toad/transcript_source_preparation.py @@ -8,6 +8,7 @@ from textual.worker import Worker, WorkerCancelled from textual.await_complete import AwaitComplete from toad.core_event_carrier import CoreEventReceiver +from toad.core.events import CoordinationObserved from toad.transcript_state import TranscriptState, RetiredSourceTranscript, ParkedSourceTranscript, WorkingTranscript, LatestViewportRequest from toad.transcript_preparation import PreparedPageSource from toad.core.source_events import TranscriptSourceWorkFinished @@ -183,7 +184,8 @@ def resume(_event) -> None: self.finish_source_work(operation) self.retire_core_observations(self.app.coordination_access.events) - self.observe_core_callback(self.app.coordination_access.events, resume) + self.observe_core_callback(self.app.coordination_access.events, + CoordinationObserved, resume) def supersede_source_work( self, operation: WorkingTranscript, request: LatestViewportRequest, diff --git a/src/toad/widgets/channel_participants.py b/src/toad/widgets/channel_participants.py deleted file mode 100644 index 4c10f42de..000000000 --- a/src/toad/widgets/channel_participants.py +++ /dev/null @@ -1,51 +0,0 @@ -"""The active members of a channel, shown above the composer.""" -from agent_comms.ui_model.sidebar import ThreadRowModel -from textual.app import ComposeResult -from textual.containers import VerticalScroll -from textual.content import Content -from textual.style import Style -from textual.widgets import Static - -from toad.line_blocks import OpenTarget -from toad.widgets.transcript_lines import OpensLinks - - -class ParticipantNames(OpensLinks, Static): - """The names of a channel's active threads; each opens its thread.""" - - -class ChannelParticipants(VerticalScroll): - DEFAULT_CSS = """ - ChannelParticipants { - height: auto; max-height: 4; overflow-y: auto; - padding: 0 1; color: $text-muted; - } - """ - - def __init__(self): - super().__init__() - self.names = ParticipantNames(markup=False) - - def compose(self) -> ComposeResult: - yield self.names - - @staticmethod - def prepare_participants(people: tuple[ThreadRowModel, ...]) -> tuple[Content, Content]: - """What to draw for these members, built off the UI thread.""" - names = [] - summaries = [] - for presentation in people: - summaries.append(f"{presentation.name}: {presentation.summary}") - names.append(Content.styled( - presentation.label, "$warning" if presentation.status.busy else "$text-muted", - ).stylize(Style.from_meta({"@click": OpenTarget(presentation.name).click}))) - content = Content.assemble("Active: ", Content(" · ").join(names)) if names else Content("No active turns") - return content, Content("\n".join(summaries)) - - def update_participants(self, content: Content, tooltip: Content) -> None: - """Draw prepared names; unchanged names are not redrawn.""" - previous = self.names.content - if not isinstance(previous, Content) or not content.is_same(previous): - self.names.update(content) - if tooltip != self.tooltip: - self.tooltip = tooltip diff --git a/src/toad/widgets/comms_chat.py b/src/toad/widgets/comms_chat.py index 8445e9bf2..942073851 100644 --- a/src/toad/widgets/comms_chat.py +++ b/src/toad/widgets/comms_chat.py @@ -126,7 +126,7 @@ def compose(self) -> ComposeResult: yield ChannelActivityTray(id="comms-activity") yield Flash() with containers.Vertical(id="prompt-stack"): - yield self.conversation_kind.activity_widget(self) + yield from self.conversation_kind.compose_activity(self) yield self.throbber yield self.conversation_kind.prompt(self.target).data_bind( project_path=Conversation.project_path, @@ -196,8 +196,8 @@ async def on_transcript_source_work_finished(self, event) -> None: return if self._refreshed_from != access.revision: # The observer announced a change while this work ran. The history - # may have read it from the stores already; the participants line - # reads the observer's sidebar, which only now shows it. + # may have read it from the stores already; mention completion + # reads the observer's snapshot, which only now shows it. await self._refresh() return if history.revision == access.revision: @@ -302,7 +302,7 @@ async def _refresh_source(self) -> None: finally: if undrawn: self.on_work_finished() - # A sent message may now be drawn: composing resumes before the roster is read. + # A sent message may now be drawn: composing resumes before mentions are read. self.settle_user_message() if changed and undrawn and (placeholder := self.query_one_optional("#history-loading")): placeholder.remove() @@ -311,7 +311,7 @@ async def _refresh_source(self) -> None: except (OSError, ValueError, RuntimeError) as error: self._show_wire_error(error) return - # The participants line and the status line read the observer's sidebar + # Composer mentions and DM status read the observer's snapshot # and the agent records, which can take seconds on a busy bus. They are # not this conversation's messages, so they run after this work ends # and the history can load pages while they wait. @@ -327,7 +327,7 @@ def _show_wire_error(self, error: Exception) -> None: self.status = message async def _refresh_labels(self, comms, changed: bool, newly_announced: bool, read_only: bool) -> None: - """Show who is in the conversation and its status line, for the history as it is drawn now.""" + """Refresh composer mentions and status for the history as it is drawn now.""" from agent_comms.coordination_errors import CoordinationReadUnavailable, StaleRevision history = self.message_history @@ -335,9 +335,9 @@ async def _refresh_labels(self, comms, changed: bool, newly_announced: bool, rea source, target = history.source, self.target try: # An unchanged history may have read the stores before the observer - # announced the change; the participants line reads the observer's sidebar. + # announced the change; mention completion reads the observer's snapshot. if changed or newly_announced: - await self.conversation_kind.update_roster(self, comms) + await self.conversation_kind.update_mentions(self, comms) if changed: if not await history.source_is_current(snapshot): if snapshot.current(history): @@ -349,7 +349,7 @@ async def _refresh_labels(self, comms, changed: bool, newly_announced: bool, rea self.call_after_refresh(history.mark_visible) if not changed: return - info = await self.conversation_kind.agent_info(comms, target) + status = await self.conversation_kind.status_text(comms, target, read_only=read_only) except (CoordinationReadUnavailable, StaleRevision): # The coordination store is busy or moved on: the observer's next # announcement refreshes this view and reads the labels again. @@ -359,19 +359,7 @@ async def _refresh_labels(self, comms, changed: bool, newly_announced: bool, rea return if not await history.source_is_current(snapshot) or self.target != target: return - if self.send_block_reason: - self.status = self.send_block_reason - elif info is not None: - self.status = " · ".join( - value - for value in ( - info.model or "", - info.context_usage.label if info.context_usage is not None else "", - ) - if value - ) - else: - self.status = "Read-only saved view" if read_only else "" + self.status = self.send_block_reason or status if self.window.follows_tail: self.window.anchor() if history.has_newer or (history.has_older and self.window.max_scroll_y == 0): diff --git a/src/toad/widgets/conversation.py b/src/toad/widgets/conversation.py index c61e8a884..23a77fff6 100644 --- a/src/toad/widgets/conversation.py +++ b/src/toad/widgets/conversation.py @@ -502,10 +502,8 @@ def __init__( ) -> None: super().__init__() self.turns = ConversationTurn(self._turn_changed, lambda: self.agent.presentation.turns if self.agent else None) - self.throbber = ObservedThrobber( - lambda: self.busy_count > 0 or self.turns.owner.busy or self._observed_working, - id="throbber", - ) + self.throbber = ObservedThrobber(self.working, id="throbber") + self.throbber_above = ObservedThrobber(self.working) self._initialize_session(project_path, agent, agent_session_id, session_pk, session_title, initial_prompt, transcript) @@ -880,8 +878,15 @@ async def sync_project_path(self, path: Path) -> None: await DB().session_update_project(session_pk, path) + def working(self) -> bool: + return self.busy_count > 0 or self.turns.owner.busy or self._observed_working + + def _sync_working(self) -> None: + for throbber in (self.throbber, self.throbber_above): + throbber.sync() + def _turn_changed(self, owner: TurnOwner) -> None: - self.throbber.sync() + self._sync_working() if (prompt := self.query_one_optional(Prompt)) is not None: prompt.sync_turn() if (details := self.query_one_optional(SessionDetails)) is not None: @@ -891,7 +896,7 @@ def _turn_changed(self, owner: TurnOwner) -> None: @on(ObservedThreadActivity.Changed) def _observed_activity_changed(self, event: ObservedThreadActivity.Changed) -> None: event.stop() - self.throbber.sync() + self._sync_working() @on(events.Key) async def on_key(self, event: events.Key): @@ -927,6 +932,7 @@ def compose(self) -> ComposeResult: ), ) self.refresh_thread_status() + yield self.throbber_above yield self.thread_activity prompt = self.operational_sources.prompt(self).data_bind( project_path=Conversation.project_path, @@ -1348,7 +1354,7 @@ def on_current_working_directory_changed( self.working_directory = str(Path(event.event.path).resolve().absolute()) async def watch_busy_count(self, _busy: int) -> None: - self.throbber.sync() + self._sync_working() @on(TerminalTool.Projection) async def on_terminal_projection(self, message: TerminalTool.Projection): diff --git a/uv.lock b/uv.lock index 600f3ee18..3b893e324 100644 --- a/uv.lock +++ b/uv.lock @@ -21,7 +21,7 @@ wheels = [ [[package]] name = "agent-comms" version = "0.1.0" -source = { git = "https://github.com/OpenHCSDev/agent-comms.git?rev=00a3d74d4b1ab8962941d84a764b3e8c2e57013d#00a3d74d4b1ab8962941d84a764b3e8c2e57013d" } +source = { git = "https://github.com/OpenHCSDev/agent-comms.git?rev=2fb445b94fc22ed58a365709f4a873ca53e4a303#2fb445b94fc22ed58a365709f4a873ca53e4a303" } dependencies = [ { name = "aiohttp" }, { name = "metaclass-registry" }, @@ -306,7 +306,7 @@ dev = [ [package.metadata] requires-dist = [ { name = "agent-client-protocol", specifier = "==0.12.1" }, - { name = "agent-comms", extras = ["acp"], git = "https://github.com/OpenHCSDev/agent-comms.git?rev=00a3d74d4b1ab8962941d84a764b3e8c2e57013d" }, + { name = "agent-comms", extras = ["acp"], git = "https://github.com/OpenHCSDev/agent-comms.git?rev=2fb445b94fc22ed58a365709f4a873ca53e4a303" }, { name = "aiosqlite", specifier = "==0.22.1" }, { name = "bashlex", specifier = "==0.18" }, { name = "click", specifier = "==8.4.0" }, @@ -329,7 +329,7 @@ requires-dist = [ { name = "textual-speedups", specifier = "==0.2.1" }, { name = "typeguard", specifier = "==4.5.2" }, { name = "typing-extensions", specifier = "==4.15.0" }, - { name = "ui-theme", git = "https://github.com/OpenHCSDev/ui-theme.git?rev=5e58d8b5907cec4dbc8810a9329aaf902f69f675" }, + { name = "ui-theme", git = "https://github.com/OpenHCSDev/ui-theme.git?rev=aaa35af5948c6025d33fc178f6bd687d9dc5c322" }, { name = "watchdog", specifier = "==6.0.0" }, { name = "xdg-base-dirs", specifier = ">=6.0.2" }, ] @@ -2021,7 +2021,7 @@ wheels = [ [[package]] name = "ui-theme" version = "0.1.0" -source = { git = "https://github.com/OpenHCSDev/ui-theme.git?rev=5e58d8b5907cec4dbc8810a9329aaf902f69f675#5e58d8b5907cec4dbc8810a9329aaf902f69f675" } +source = { git = "https://github.com/OpenHCSDev/ui-theme.git?rev=aaa35af5948c6025d33fc178f6bd687d9dc5c322#aaa35af5948c6025d33fc178f6bd687d9dc5c322" } dependencies = [ { name = "metaclass-registry" }, { name = "terminaltexteffects" },