fix(tui): autonomous background process completion notifications (#26071) (#26327)

* feat(process-registry): add format_process_notification shared helper

* feat(process-registry): add drain_notifications method

* refactor(cli): use shared drain_notifications and format_process_notification

* feat(tui): add background notification poller for completion_queue

* feat(tui): wire notification poller into session init/finalize

* refactor(tui): add post-turn drain using shared helper as safety net
This commit is contained in:
Siddharth Balyan 2026-05-15 19:31:00 +05:30 committed by GitHub
parent db84a78e61
commit d5416284f1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 486 additions and 55 deletions

View file

@ -826,6 +826,26 @@ class ProcessRegistry:
"""Check if a completion notification was already consumed via wait/poll/log."""
return session_id in self._completion_consumed
def drain_notifications(self) -> "list[tuple[dict, str]]":
"""Pop all pending notification events and return formatted pairs.
Returns a list of (raw_event, formatted_text) tuples.
Skips completion events that were already consumed via wait/poll/log.
"""
results = []
while not self.completion_queue.empty():
try:
evt = self.completion_queue.get_nowait()
except Exception:
break
_evt_sid = evt.get("session_id", "")
if evt.get("type") == "completion" and self.is_completion_consumed(_evt_sid):
continue
text = format_process_notification(evt)
if text:
results.append((evt, text))
return results
def get(self, session_id: str) -> Optional[ProcessSession]:
"""Get a session by ID (running or finished)."""
with self._lock:
@ -1388,6 +1408,44 @@ class ProcessRegistry:
process_registry = ProcessRegistry()
def format_process_notification(evt: dict) -> "str | None":
"""Format a process notification event into a [IMPORTANT: ...] message.
Handles completion events (notify_on_complete), watch pattern matches,
and watch disabled events from the unified completion_queue.
"""
evt_type = evt.get("type", "completion")
_sid = evt.get("session_id", "unknown")
_cmd = evt.get("command", "unknown")
if evt_type == "watch_disabled":
return f"[IMPORTANT: {evt.get('message', '')}]"
if evt_type == "watch_match":
_pat = evt.get("pattern", "?")
_out = evt.get("output", "")
_sup = evt.get("suppressed", 0)
text = (
f"[IMPORTANT: Background process {_sid} matched "
f"watch pattern \"{_pat}\".\n"
f"Command: {_cmd}\n"
f"Matched output:\n{_out}"
)
if _sup:
text += f"\n({_sup} earlier matches were suppressed by rate limit)"
text += "]"
return text
_exit = evt.get("exit_code", "?")
_out = evt.get("output", "")
return (
f"[IMPORTANT: Background process {_sid} completed "
f"(exit code {_exit}).\n"
f"Command: {_cmd}\n"
f"Output:\n{_out}]"
)
# ---------------------------------------------------------------------------
# Registry -- the "process" tool schema + handler
# ---------------------------------------------------------------------------