-
-
Notifications
You must be signed in to change notification settings - Fork 154
Watch directory #166
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Watch directory #166
Changes from all commits
5c199b0
b0e5d78
ac29bfb
298489b
68ae732
33896c0
967b1ca
c48e134
7fa55a9
7dfa78c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,76 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import asyncio | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from pathlib import Path | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import rich.repr | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from textual.message import Message | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from textual.widget import Widget | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from watchdog.events import ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| FileSystemEvent, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| FileSystemEventHandler, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| FileCreatedEvent, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| FileDeletedEvent, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| FileMovedEvent, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| DirCreatedEvent, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| DirDeletedEvent, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| DirMovedEvent, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from watchdog.observers import Observer | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class DirectoryChanged(Message): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """The directory was changed.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @rich.repr.auto | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class DirectoryWatcher(FileSystemEventHandler): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Watch for changes to a directory, ignoring purely file data changes.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def __init__(self, path: Path, widget: Widget) -> None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Args: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| path: Root path to monitor. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| widget: Widget which will receive the `DirectoryChanged` event. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self._path = path | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+35
to
+37
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self._widget = widget | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self._observer = Observer() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| super().__init__() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def on_any_event(self, event: FileSystemEvent) -> None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Send DirectoryChanged event when the FS is updated.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self._widget.post_message(DirectoryChanged()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def __rich_repr__(self) -> rich.repr.Result: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| yield self._path | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| yield self._widget | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def start(self) -> None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Start the watcher.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self._observer.schedule( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| str(self._path), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| recursive=True, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| event_filter=[ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| FileCreatedEvent, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+46
to
+58
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| FileDeletedEvent, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| FileMovedEvent, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| DirCreatedEvent, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| DirDeletedEvent, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| DirMovedEvent, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self._observer.start() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+50
to
+67
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def start(self) -> None: | |
| """Start the watcher.""" | |
| self._observer.schedule( | |
| self, | |
| str(self._path), | |
| recursive=True, | |
| event_filter=[ | |
| FileCreatedEvent, | |
| FileDeletedEvent, | |
| FileMovedEvent, | |
| DirCreatedEvent, | |
| DirDeletedEvent, | |
| DirMovedEvent, | |
| ], | |
| ) | |
| self._observer.start() | |
| async def start(self) -> None: | |
| """Start the watcher.""" | |
| def run() -> None: | |
| """Start the observer in a thread.""" | |
| self._observer.schedule( | |
| self, | |
| str(self._path), | |
| recursive=True, | |
| event_filter=[ | |
| FileCreatedEvent, | |
| FileDeletedEvent, | |
| FileMovedEvent, | |
| DirCreatedEvent, | |
| DirDeletedEvent, | |
| DirMovedEvent, | |
| ], | |
| ) | |
| self._observer.start() | |
| await asyncio.to_thread(run) |
Copilot
AI
Jan 4, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The observer is stopped with a 1-second timeout in the join() call. If the observer thread doesn't stop within this timeout, it will be left running. Consider checking the return value or adding error handling to ensure the thread is properly cleaned up. Alternatively, document why a 1-second timeout is sufficient and what happens if it's exceeded.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -40,6 +40,7 @@ | |||||||||||||||||||||||||||||||||||||||
| from toad.acp.agent import Mode | ||||||||||||||||||||||||||||||||||||||||
| from toad.answer import Answer | ||||||||||||||||||||||||||||||||||||||||
| from toad.agent import AgentBase, AgentReady, AgentFail | ||||||||||||||||||||||||||||||||||||||||
| from toad.directory_watcher import DirectoryWatcher, DirectoryChanged | ||||||||||||||||||||||||||||||||||||||||
| from toad.history import History | ||||||||||||||||||||||||||||||||||||||||
| from toad.widgets.flash import Flash | ||||||||||||||||||||||||||||||||||||||||
| from toad.widgets.menu import Menu | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -321,6 +322,9 @@ def __init__(self, project_path: Path, agent: AgentData | None = None) -> None: | |||||||||||||||||||||||||||||||||||||||
| self._turn_count = 0 | ||||||||||||||||||||||||||||||||||||||||
| self._shell_count = 0 | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| self._directory_changed = False | ||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||
| self._directory_watcher: DirectoryWatcher | None = None | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| @property | ||||||||||||||||||||||||||||||||||||||||
| def agent_title(self) -> str | None: | ||||||||||||||||||||||||||||||||||||||||
| if self._agent_data is not None: | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -433,6 +437,11 @@ def add_focusable_terminal(self, terminal: Terminal) -> None: | |||||||||||||||||||||||||||||||||||||||
| if not terminal.is_finalized: | ||||||||||||||||||||||||||||||||||||||||
| self._focusable_terminals.append(terminal) | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| @on(DirectoryChanged) | ||||||||||||||||||||||||||||||||||||||||
| def on_directory_changed(self, event: DirectoryChanged) -> None: | ||||||||||||||||||||||||||||||||||||||||
| event.stop() | ||||||||||||||||||||||||||||||||||||||||
| self._directory_changed = True | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| @on(Terminal.Finalized) | ||||||||||||||||||||||||||||||||||||||||
| def on_terminal_finalized(self, event: Terminal.Finalized) -> None: | ||||||||||||||||||||||||||||||||||||||||
| """Terminal was finalized, so we can remove it from the list.""" | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -441,6 +450,9 @@ def on_terminal_finalized(self, event: Terminal.Finalized) -> None: | |||||||||||||||||||||||||||||||||||||||
| except ValueError: | ||||||||||||||||||||||||||||||||||||||||
| pass | ||||||||||||||||||||||||||||||||||||||||
| self.prompt.project_directory_updated() | ||||||||||||||||||||||||||||||||||||||||
| if self._directory_changed: | ||||||||||||||||||||||||||||||||||||||||
| self._directory_changed = False | ||||||||||||||||||||||||||||||||||||||||
| self.post_message(messages.ProjectDirectoryUpdated()) | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| @on(Terminal.AlternateScreenChanged) | ||||||||||||||||||||||||||||||||||||||||
| def on_terminal_alternate_screen_( | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -593,6 +605,8 @@ async def on_agent_ready(self) -> None: | |||||||||||||||||||||||||||||||||||||||
| async def on_unmount(self) -> None: | ||||||||||||||||||||||||||||||||||||||||
| if self.agent is not None: | ||||||||||||||||||||||||||||||||||||||||
| await self.agent.stop() | ||||||||||||||||||||||||||||||||||||||||
| if self._directory_watcher is not None: | ||||||||||||||||||||||||||||||||||||||||
| await self._directory_watcher.stop() | ||||||||||||||||||||||||||||||||||||||||
| if self._agent_data is not None and self.session_start_time is not None: | ||||||||||||||||||||||||||||||||||||||||
| session_time = monotonic() - self.session_start_time | ||||||||||||||||||||||||||||||||||||||||
| await self.app.capture_event( | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -712,8 +726,12 @@ async def agent_turn_over(self, stop_reason: str | None) -> None: | |||||||||||||||||||||||||||||||||||||||
| await self._loading.remove() | ||||||||||||||||||||||||||||||||||||||||
| self._agent_response = None | ||||||||||||||||||||||||||||||||||||||||
| self._agent_thought = None | ||||||||||||||||||||||||||||||||||||||||
| self.post_message(messages.ProjectDirectoryUpdated()) | ||||||||||||||||||||||||||||||||||||||||
| self.prompt.project_directory_updated() | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| if self._directory_changed: | ||||||||||||||||||||||||||||||||||||||||
| self._directory_changed = False | ||||||||||||||||||||||||||||||||||||||||
| self.post_message(messages.ProjectDirectoryUpdated()) | ||||||||||||||||||||||||||||||||||||||||
| self.prompt.project_directory_updated() | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| self._turn_count += 1 | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| if stop_reason != "end_turn": | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -1185,6 +1203,9 @@ async def watch_agent_ready(self, ready: bool) -> None: | |||||||||||||||||||||||||||||||||||||||
| with suppress(asyncio.TimeoutError): | ||||||||||||||||||||||||||||||||||||||||
| async with asyncio.timeout(2.0): | ||||||||||||||||||||||||||||||||||||||||
| await self.shell.wait_for_ready() | ||||||||||||||||||||||||||||||||||||||||
| if ready: | ||||||||||||||||||||||||||||||||||||||||
| self._directory_watcher = DirectoryWatcher(self.project_path, self) | ||||||||||||||||||||||||||||||||||||||||
| self._directory_watcher.start() | ||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+1206
to
+1208
|
||||||||||||||||||||||||||||||||||||||||
| if ready: | |
| self._directory_watcher = DirectoryWatcher(self.project_path, self) | |
| self._directory_watcher.start() | |
| existing_watcher = getattr(self, "_directory_watcher", None) | |
| if not ready: | |
| # Agent is no longer ready; stop any existing directory watcher. | |
| if existing_watcher is not None: | |
| existing_watcher.stop() | |
| self._directory_watcher = None | |
| return | |
| # Agent is ready; restart the directory watcher to reflect current state. | |
| if existing_watcher is not None: | |
| existing_watcher.stop() | |
| self._directory_watcher = DirectoryWatcher(self.project_path, self) | |
| self._directory_watcher.start() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is a spelling error in this line. The word "were" should be "where" to make the sentence grammatically correct: "Smarter filesystem monitoring to avoid refreshes where nothing has changed".