From 86c64cfb5bd5d1535c4f221132134af787157f2c Mon Sep 17 00:00:00 2001 From: kyssta-exe Date: Wed, 3 Jun 2026 10:47:46 +0000 Subject: [PATCH] fix(gateway): visually expire Discord interactive views on timeout All Discord interactive views (ExecApprovalView, SlashConfirmView, UpdatePromptView, ModelPickerView, ClarifyChoiceView) now edit their message when the view times out, disabling buttons and updating the embed to show a 'Prompt expired' footer. Previously, timed-out buttons remained visually clickable in the UI, causing Discord's generic 'Interaction failed' error when clicked. Fixes #38022 --- plugins/platforms/discord/adapter.py | 62 ++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/plugins/platforms/discord/adapter.py b/plugins/platforms/discord/adapter.py index 12cf05c38..117651765 100644 --- a/plugins/platforms/discord/adapter.py +++ b/plugins/platforms/discord/adapter.py @@ -4101,6 +4101,7 @@ class DiscordAdapter(BasePlatformAdapter): ) msg = await channel.send(embed=embed, view=view) + view._message = msg # store for on_timeout expiration editing return SendResult(success=True, message_id=str(msg.id)) except Exception as e: @@ -4140,6 +4141,7 @@ class DiscordAdapter(BasePlatformAdapter): ) msg = await channel.send(embed=embed, view=view) + view._message = msg # store for on_timeout expiration editing return SendResult(success=True, message_id=str(msg.id)) except Exception as e: return SendResult(success=False, error=str(e)) @@ -4217,6 +4219,8 @@ class DiscordAdapter(BasePlatformAdapter): view = None msg = await channel.send(embed=embed, view=view) if view else await channel.send(embed=embed) + if view: + view._message = msg # store for on_timeout expiration editing return SendResult(success=True, message_id=str(msg.id)) except Exception as e: logger.warning("[%s] send_clarify failed: %s", self.name, e) @@ -4252,6 +4256,7 @@ class DiscordAdapter(BasePlatformAdapter): allowed_role_ids=self._allowed_role_ids, ) msg = await channel.send(embed=embed, view=view) + view._message = msg # store for on_timeout expiration editing return SendResult(success=True, message_id=str(msg.id)) except Exception as e: return SendResult(success=False, error=str(e)) @@ -4311,6 +4316,7 @@ class DiscordAdapter(BasePlatformAdapter): ) msg = await channel.send(embed=embed, view=view) + view._message = msg # store for on_timeout expiration editing return SendResult(success=True, message_id=str(msg.id)) except Exception as e: @@ -5141,6 +5147,17 @@ def _define_discord_view_classes() -> None: self.resolved = True for child in self.children: child.disabled = True + # Visually update the Discord message so buttons appear disabled. + msg = getattr(self, '_message', None) + if msg: + try: + embed = msg.embeds[0] if msg.embeds else None + if embed: + embed.color = discord.Color.greyple() + embed.set_footer(text="⏱ Prompt expired — no action taken") + await msg.edit(embed=embed, view=self) + except Exception: + pass # message deleted or too old to edit class SlashConfirmView(discord.ui.View): """Three-button view for generic slash-command confirmations. @@ -5245,6 +5262,17 @@ def _define_discord_view_classes() -> None: self.resolved = True for child in self.children: child.disabled = True + # Visually update the Discord message so buttons appear disabled. + msg = getattr(self, '_message', None) + if msg: + try: + embed = msg.embeds[0] if msg.embeds else None + if embed: + embed.color = discord.Color.greyple() + embed.set_footer(text="⏱ Prompt expired — no action taken") + await msg.edit(embed=embed, view=self) + except Exception: + pass class UpdatePromptView(discord.ui.View): """Interactive Yes/No buttons for ``hermes update`` prompts. @@ -5330,6 +5358,17 @@ def _define_discord_view_classes() -> None: self.resolved = True for child in self.children: child.disabled = True + # Visually update the Discord message so buttons appear disabled. + msg = getattr(self, '_message', None) + if msg: + try: + embed = msg.embeds[0] if msg.embeds else None + if embed: + embed.color = discord.Color.greyple() + embed.set_footer(text="⏱ Prompt expired — no action taken") + await msg.edit(embed=embed, view=self) + except Exception: + pass class ModelPickerView(discord.ui.View): """Interactive select-menu view for model switching. @@ -5555,6 +5594,18 @@ def _define_discord_view_classes() -> None: async def on_timeout(self): self.resolved = True self.clear_items() + # Visually update the Discord message so it appears expired. + msg = getattr(self, '_message', None) + if msg: + try: + embed = discord.Embed( + title="⚙ Model Configuration", + description="⏱ Selection expired — no model change.", + color=discord.Color.greyple(), + ) + await msg.edit(embed=embed, view=self) + except Exception: + pass class ClarifyChoiceView(discord.ui.View): @@ -5740,6 +5791,17 @@ def _define_discord_view_classes() -> None: self.resolved = True for child in self.children: child.disabled = True + # Visually update the Discord message so buttons appear disabled. + msg = getattr(self, '_message', None) + if msg: + try: + embed = msg.embeds[0] if msg.embeds else None + if embed: + embed.color = discord.Color.greyple() + embed.set_footer(text="⏱ Prompt expired — no action taken") + await msg.edit(embed=embed, view=self) + except Exception: + pass if DISCORD_AVAILABLE: _define_discord_view_classes()