improve: persistent typing indicator with poll-based refresh

This commit is contained in:
linuztx 2026-03-26 18:31:38 +08:00
parent 8d0ec86f15
commit 3dd01cd04c
2 changed files with 26 additions and 3 deletions

View file

@ -51,7 +51,7 @@ class WhatsAppResponseIntercept(Extension):
if attachments:
PrintStyle.info(f"WhatsApp: sending update with {len(attachments)} attachment(s)")
error = await send_wa_reply(context, text, attachments or None, reply_to=reply_to)
error = await send_wa_reply(context, text, attachments or None, reply_to=reply_to, keep_typing=True)
if error:
result = agent.read_prompt("fw.wa.update_error.md", error=error)

View file

@ -34,6 +34,7 @@ CTX_WA_LAST_MSG_ID = "wa_last_msg_id"
# Transient — consumed per-reply, not persisted
CTX_WA_ATTACHMENTS = "_wa_response_attachments"
CTX_WA_REPLY_TO = "_wa_reply_to"
CTX_WA_TYPING_ACTIVE = "_wa_typing_active"
# Poll task — lives here (not in extension module) because
# extension modules are re-executed on each job_loop tick,
@ -45,10 +46,25 @@ _poll_task: asyncio.Task | None = None # type: ignore[type-arg]
# Poll loop
# ------------------------------------------------------------------
async def _refresh_typing(base_url: str) -> None:
"""Re-send composing for all contexts with active typing flag."""
for ctx in AgentContext._contexts.values():
if not isinstance(ctx, AgentContext):
continue
if not ctx.data.get(CTX_WA_TYPING_ACTIVE):
continue
chat_id = ctx.data.get(CTX_WA_CHAT_ID, "")
if chat_id:
await wa_client.send_typing(base_url, chat_id)
async def poll_messages(config: dict) -> None:
port = int(config.get("bridge_port", 3100))
base_url = bridge_manager.get_bridge_url(port)
# Refresh typing indicator for active sessions (beats 25s WhatsApp timeout)
await _refresh_typing(base_url)
try:
messages = await wa_client.get_messages(base_url)
except Exception as e:
@ -117,6 +133,7 @@ async def _start_new_chat(config: dict, msg: dict) -> None:
context.data[CTX_WA_IS_GROUP] = is_group
context.data[CTX_WA_LAST_BODY] = msg.get("body", "")
context.data[CTX_WA_LAST_MSG_ID] = msg.get("messageId", "")
context.data[CTX_WA_TYPING_ACTIVE] = True
project = config.get("project", "")
if project:
@ -154,6 +171,7 @@ async def _route_to_chat(
context.data[CTX_WA_LAST_BODY] = msg.get("body", "")
context.data[CTX_WA_LAST_MSG_ID] = msg.get("messageId", "")
context.data[CTX_WA_TYPING_ACTIVE] = True
user_msg = _build_user_message(context.agent0, msg, config)
msg_id = str(uuid.uuid4())
@ -224,6 +242,7 @@ async def send_wa_reply(
response_text: str,
attachments: list[str] | None = None,
reply_to: str = "",
keep_typing: bool = False,
) -> str | None:
chat_id = context.data.get(CTX_WA_CHAT_ID)
if not chat_id:
@ -265,8 +284,12 @@ async def send_wa_reply(
except Exception as e:
PrintStyle.warning(f"WhatsApp: attachment error: {e}")
# Clear typing indicator
await wa_client.send_typing(base_url, chat_id, paused=True)
# Typing: restart if agent is still working, stop if final reply
if keep_typing:
await wa_client.send_typing(base_url, chat_id)
else:
await wa_client.send_typing(base_url, chat_id, paused=True)
context.data[CTX_WA_TYPING_ACTIVE] = False
return None