fix(gateway): Slack approval UX in threads — block-size overflow + typed-prefix instruction text (#43444)
Two fixes for the reported Slack thread approval UX: 1. Slack Block Kit approval/confirm sends silently overflowed the 3000-char section-block cap (flat 2900-char truncation + header + reason), so long execute_code approvals failed with invalid_blocks and fell back to the plain-text prompt with no buttons. Budget the command preview against the rendered fixed parts so blocks never exceed the cap (send_exec_approval + send_slash_confirm). 2. The text fallbacks told users to reply /approve — which Slack blocks inside threads and Matrix clients reserve client-side. Add a typed_command_prefix capability flag on BasePlatformAdapter (default "/"; Slack and Matrix set "!" to match their existing bang-prefix rewrite) and use it in the shared fallback prompt builders (exec approval, update prompt, destructive slash confirm, expensive-model confirm) plus Matrix's reaction-prompt text. The slash-confirm text-intercept now also accepts bang-prefixed replies (!always, !cancel) since those keywords aren't registered commands and the adapters' rewrite doesn't touch them.
This commit is contained in:
@@ -1804,6 +1804,18 @@ class BasePlatformAdapter(ABC):
|
||||
# preview (see gateway/run.py progress_callback).
|
||||
supports_code_blocks: bool = False
|
||||
|
||||
# The command prefix users can always TYPE on this platform to reach
|
||||
# Hermes commands. Default "/" (most platforms deliver "/approve" etc.
|
||||
# as plain message text). Platforms where typing a leading "/" is
|
||||
# intercepted or restricted by the client (Slack blocks native slash
|
||||
# commands inside threads; Matrix clients reserve "/" for client-local
|
||||
# commands) ship a "!" alias rewrite in their adapter and set this to
|
||||
# "!" so user-facing instruction text ("Reply `!approve` ...") tells
|
||||
# users the form that actually works everywhere. Capability flag —
|
||||
# shared prompt builders read it via getattr(adapter,
|
||||
# "typed_command_prefix", "/"); no per-platform branching at call sites.
|
||||
typed_command_prefix: str = "/"
|
||||
|
||||
def __init__(self, config: PlatformConfig, platform: Platform):
|
||||
self.config = config
|
||||
self.platform = platform
|
||||
|
||||
@@ -422,6 +422,11 @@ class MatrixAdapter(BasePlatformAdapter):
|
||||
|
||||
supports_code_blocks = True # Matrix renders fenced code blocks (HTML/markdown)
|
||||
|
||||
# Matrix clients commonly reserve typed "/" for client-local commands;
|
||||
# the adapter accepts "!command" as the alias that always reaches Hermes
|
||||
# (see _normalize_matrix_bang_command), so instruction text shows "!".
|
||||
typed_command_prefix = "!"
|
||||
|
||||
# Threshold for detecting Matrix client-side message splits.
|
||||
# When a chunk is near the ~4000-char practical limit, a continuation
|
||||
# is almost certain.
|
||||
@@ -1350,11 +1355,11 @@ class MatrixAdapter(BasePlatformAdapter):
|
||||
"⚠️ **Dangerous command requires approval**\n"
|
||||
f"```\n{cmd_preview}\n```\n"
|
||||
f"Reason: {description}\n\n"
|
||||
"Reply `/approve` to execute, `/approve session` to approve this pattern for the session, "
|
||||
"`/approve always` to approve permanently, or `/deny` to cancel.\n\n"
|
||||
"Reply `!approve` to execute, `!approve session` to approve this pattern for the session, "
|
||||
"`!approve always` to approve permanently, or `!deny` to cancel.\n\n"
|
||||
"You can also click the reaction to approve:\n"
|
||||
"✅ = /approve\n"
|
||||
"❎ = /deny"
|
||||
"✅ = approve\n"
|
||||
"❎ = deny"
|
||||
)
|
||||
|
||||
result = await self.send(chat_id, text, metadata=metadata)
|
||||
|
||||
@@ -318,6 +318,11 @@ class SlackAdapter(BasePlatformAdapter):
|
||||
|
||||
MAX_MESSAGE_LENGTH = 39000 # Slack API allows 40,000 chars; leave margin
|
||||
supports_code_blocks = True # Slack mrkdwn renders fenced code blocks
|
||||
# Slack blocks typed native slash commands inside threads ("/approve is
|
||||
# not supported in threads. Sorry!"). The adapter rewrites a leading
|
||||
# "!" to "/" for known commands (see _handle_slack_message), so "!" is
|
||||
# the prefix that works everywhere — instruction text must show it.
|
||||
typed_command_prefix = "!"
|
||||
|
||||
def __init__(self, config: PlatformConfig):
|
||||
super().__init__(config, Platform.SLACK)
|
||||
@@ -2692,19 +2697,26 @@ class SlackAdapter(BasePlatformAdapter):
|
||||
return SendResult(success=False, error="Not connected")
|
||||
|
||||
try:
|
||||
cmd_preview = command[:2900] + "..." if len(command) > 2900 else command
|
||||
thread_ts = self._resolve_thread_ts(None, metadata)
|
||||
|
||||
# Slack hard-caps a section block's text at 3000 chars; an
|
||||
# oversized block fails the whole send with ``invalid_blocks``
|
||||
# and the gateway falls back to the plain-text prompt (no
|
||||
# buttons). execute_code approvals embed the entire script in
|
||||
# ``command``, so budget the preview against the fixed parts
|
||||
# instead of a flat truncation that overflows once the header +
|
||||
# reason are added.
|
||||
header = ":warning: *Command Approval Required*\n"
|
||||
reason = f"Reason: {description[:500]}"
|
||||
budget = 3000 - len(header) - len(reason) - len("``````\n") - len("...")
|
||||
cmd_preview = command[:budget] + "..." if len(command) > budget else command
|
||||
|
||||
blocks = [
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": (
|
||||
f":warning: *Command Approval Required*\n"
|
||||
f"```{cmd_preview}```\n"
|
||||
f"Reason: {description}"
|
||||
),
|
||||
"text": f"{header}```{cmd_preview}```\n{reason}",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -2772,8 +2784,13 @@ class SlackAdapter(BasePlatformAdapter):
|
||||
return SendResult(success=False, error="Not connected")
|
||||
|
||||
try:
|
||||
body = message[:2900] + "..." if len(message) > 2900 else message
|
||||
thread_ts = self._resolve_thread_ts(None, metadata)
|
||||
# Same 3000-char section-block cap as send_exec_approval: budget
|
||||
# the body against the rendered title so the wrapper never pushes
|
||||
# the block over the limit (overflow → invalid_blocks → no buttons).
|
||||
_title = (title or "Confirm")[:150]
|
||||
budget = 3000 - len(f"*{_title}*\n\n") - len("...")
|
||||
body = message[:budget] + "..." if len(message) > budget else message
|
||||
# Encode session_key and confirm_id into the button value so the
|
||||
# callback handler can resolve without extra bookkeeping.
|
||||
value = f"{session_key}|{confirm_id}"
|
||||
@@ -2783,7 +2800,7 @@ class SlackAdapter(BasePlatformAdapter):
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": f"*{title or 'Confirm'}*\n\n{body}",
|
||||
"text": f"*{_title}*\n\n{body}",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user