fix: pause typing indicator during approval waits (#5893)

When the agent waits for dangerous-command approval, the typing
indicator (_keep_typing loop) kept refreshing. On Slack's Assistant
API this is critical: assistant_threads_setStatus disables the
compose box, preventing users from typing /approve or /deny.

- Add _typing_paused set + pause/resume methods to BasePlatformAdapter
- _keep_typing skips send_typing when chat_id is paused
- _approval_notify_sync pauses typing before sending approval prompt
- _handle_approve_command / _handle_deny_command resume typing after

Benefits all platforms — no reason to show 'is thinking...' while
the agent is idle waiting for human input.
This commit is contained in:
Teknium 2026-04-07 11:04:50 -07:00 committed by GitHub
parent 1a2a03ca69
commit ab0c1e58f1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 43 additions and 2 deletions

View file

@ -484,6 +484,9 @@ class BasePlatformAdapter(ABC):
self._background_tasks: set[asyncio.Task] = set()
# Chats where auto-TTS on voice input is disabled (set by /voice off)
self._auto_tts_disabled_chats: set = set()
# Chats where typing indicator is paused (e.g. during approval waits).
# _keep_typing skips send_typing when the chat_id is in this set.
self._typing_paused: set = set()
@property
def has_fatal_error(self) -> bool:
@ -943,10 +946,16 @@ class BasePlatformAdapter(ABC):
Telegram/Discord typing status expires after ~5 seconds, so we refresh every 2
to recover quickly after progress messages interrupt it.
Skips send_typing when the chat is in ``_typing_paused`` (e.g. while
the agent is waiting for dangerous-command approval). This is critical
for Slack's Assistant API where ``assistant_threads_setStatus`` disables
the compose box pausing lets the user type ``/approve`` or ``/deny``.
"""
try:
while True:
await self.send_typing(chat_id, metadata=metadata)
if chat_id not in self._typing_paused:
await self.send_typing(chat_id, metadata=metadata)
await asyncio.sleep(interval)
except asyncio.CancelledError:
pass # Normal cancellation when handler completes
@ -960,7 +969,20 @@ class BasePlatformAdapter(ABC):
await self.stop_typing(chat_id)
except Exception:
pass
self._typing_paused.discard(chat_id)
def pause_typing_for_chat(self, chat_id: str) -> None:
"""Pause typing indicator for a chat (e.g. during approval waits).
Thread-safe (CPython GIL) can be called from the sync agent thread
while ``_keep_typing`` runs on the async event loop.
"""
self._typing_paused.add(chat_id)
def resume_typing_for_chat(self, chat_id: str) -> None:
"""Resume typing indicator for a chat after approval resolves."""
self._typing_paused.discard(chat_id)
# ── Processing lifecycle hooks ──────────────────────────────────────────
# Subclasses override these to react to message processing events
# (e.g. Discord adds 👀/✅/❌ reactions).

View file

@ -5434,6 +5434,11 @@ class GatewayRunner:
if not count:
return "No pending command to approve."
# Resume typing indicator — agent is about to continue processing.
_adapter = self.adapters.get(source.platform)
if _adapter:
_adapter.resume_typing_for_chat(source.chat_id)
count_msg = f" ({count} commands)" if count > 1 else ""
logger.info("User approved %d dangerous command(s) via /approve%s", count, scope_msg)
return f"✅ Command{'s' if count > 1 else ''} approved{scope_msg}{count_msg}. The agent is resuming..."
@ -5466,6 +5471,11 @@ class GatewayRunner:
if not count:
return "No pending command to deny."
# Resume typing indicator — agent continues (with BLOCKED result).
_adapter = self.adapters.get(source.platform)
if _adapter:
_adapter.resume_typing_for_chat(source.chat_id)
count_msg = f" ({count} commands)" if count > 1 else ""
logger.info("User denied %d dangerous command(s) via /deny", count)
return f"❌ Command{'s' if count > 1 else ''} denied{count_msg}."
@ -6759,6 +6769,15 @@ class GatewayRunner:
UX. Otherwise fall back to a plain text message with
``/approve`` instructions.
"""
# Pause the typing indicator while the agent waits for
# user approval. Critical for Slack's Assistant API where
# assistant_threads_setStatus disables the compose box — the
# user literally cannot type /approve while "is thinking..."
# is active. The approval message send auto-clears the Slack
# status; pausing prevents _keep_typing from re-setting it.
# Typing resumes in _handle_approve_command/_handle_deny_command.
_status_adapter.pause_typing_for_chat(_status_chat_id)
cmd = approval_data.get("command", "")
desc = approval_data.get("description", "dangerous command")