From e84ee52036f2144174da047fcd74dd3518456cfe Mon Sep 17 00:00:00 2001 From: Andres Torres Date: Wed, 25 Feb 2026 14:06:51 +0100 Subject: [PATCH 1/3] feat: add thinking indicator via emoji reaction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add an hourglass reaction (⏳) to the user's message while Claude Code is processing, then remove it when the response is posted. This gives users immediate visual feedback that the bot received their message. - Applies to both @mentions and thread replies - Uses `client` parameter from slack-bolt for reactions API - Errors in add/remove are silently logged (non-blocking) Co-Authored-By: Claude Opus 4.6 --- src/bender/slack_handler.py | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/src/bender/slack_handler.py b/src/bender/slack_handler.py index 71f8a36..34f3c44 100644 --- a/src/bender/slack_handler.py +++ b/src/bender/slack_handler.py @@ -12,6 +12,24 @@ logger = logging.getLogger(__name__) +THINKING_EMOJI = "hourglass_flowing_sand" + + +async def _add_reaction(client, channel: str, timestamp: str, emoji: str) -> None: + """Add a reaction to a message, silently ignoring errors.""" + try: + await client.reactions_add(channel=channel, name=emoji, timestamp=timestamp) + except Exception: + logger.debug("Could not add reaction %s", emoji, exc_info=True) + + +async def _remove_reaction(client, channel: str, timestamp: str, emoji: str) -> None: + """Remove a reaction from a message, silently ignoring errors.""" + try: + await client.reactions_remove(channel=channel, name=emoji, timestamp=timestamp) + except Exception: + logger.debug("Could not remove reaction %s", emoji, exc_info=True) + def register_handlers(app: AsyncApp, settings: Settings, sessions: SessionManager) -> None: """Register Slack event handlers on the bolt app.""" @@ -27,7 +45,7 @@ async def handle_reaction_removed(event: dict) -> None: pass @app.event("app_mention") - async def handle_mention(event: dict, say) -> None: + async def handle_mention(event: dict, say, client) -> None: """Handle new @Bender mentions — create session and invoke Claude Code.""" text = _strip_mention(event.get("text", "")) thread_ts = event.get("ts", "") @@ -41,6 +59,7 @@ async def handle_mention(event: dict, say) -> None: session_id = await sessions.create_session(thread_ts) + await _add_reaction(client, channel, thread_ts, THINKING_EMOJI) try: response = await invoke_claude( prompt=text, @@ -51,9 +70,11 @@ async def handle_mention(event: dict, say) -> None: except ClaudeCodeError as exc: logger.error("Claude Code invocation failed: %s", exc) await say(text=f"Sorry, something went wrong: {exc}", thread_ts=thread_ts) + finally: + await _remove_reaction(client, channel, thread_ts, THINKING_EMOJI) @app.event("message") - async def handle_message(event: dict, say) -> None: + async def handle_message(event: dict, say, client) -> None: """Handle thread replies — resume existing session if one exists.""" # Ignore bot messages to avoid loops if event.get("bot_id") or event.get("subtype"): @@ -74,8 +95,10 @@ async def handle_message(event: dict, say) -> None: return channel = event.get("channel", "") + message_ts = event.get("ts", "") logger.info("Thread reply in channel=%s thread=%s", channel, thread_ts) + await _add_reaction(client, channel, message_ts, THINKING_EMOJI) try: response = await invoke_claude( prompt=text, @@ -87,6 +110,8 @@ async def handle_message(event: dict, say) -> None: except ClaudeCodeError as exc: logger.error("Claude Code invocation failed: %s", exc) await say(text=f"Sorry, something went wrong: {exc}", thread_ts=thread_ts) + finally: + await _remove_reaction(client, channel, message_ts, THINKING_EMOJI) def _strip_mention(text: str) -> str: From c05b05c8cad5f6dae6ce3cdfd0bccbdb6af6f648 Mon Sep 17 00:00:00 2001 From: Andres Torres Date: Wed, 25 Feb 2026 14:15:25 +0100 Subject: [PATCH 2/3] feat: add configurable timeout via BENDER_TIMEOUT env var Default remains 300s but can now be overridden by setting BENDER_TIMEOUT in the environment. The setting is passed through to all invoke_claude() calls. Co-Authored-By: Claude Opus 4.6 --- src/bender/config.py | 1 + src/bender/slack_handler.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/src/bender/config.py b/src/bender/config.py index d6e32e5..ac8c5ea 100644 --- a/src/bender/config.py +++ b/src/bender/config.py @@ -20,6 +20,7 @@ class Settings(BaseSettings): # Optional bender_workspace: Path = Path.cwd() bender_api_port: int = 8080 + bender_timeout: int = 300 log_level: str = "info" # Optional: API key for authenticating external HTTP requests diff --git a/src/bender/slack_handler.py b/src/bender/slack_handler.py index 34f3c44..0e2ff58 100644 --- a/src/bender/slack_handler.py +++ b/src/bender/slack_handler.py @@ -65,6 +65,7 @@ async def handle_mention(event: dict, say, client) -> None: prompt=text, workspace=settings.bender_workspace, session_id=session_id, + timeout=settings.bender_timeout, ) await _post_response(say, response.result, thread_ts) except ClaudeCodeError as exc: @@ -105,6 +106,7 @@ async def handle_message(event: dict, say, client) -> None: workspace=settings.bender_workspace, session_id=session_id, resume=True, + timeout=settings.bender_timeout, ) await _post_response(say, response.result, thread_ts) except ClaudeCodeError as exc: From 87ca30c5f0c700a9e504e9d5854a1a6fe5b90793 Mon Sep 17 00:00:00 2001 From: Andres Torres Date: Wed, 25 Feb 2026 16:12:08 +0100 Subject: [PATCH 3/3] docs: add reactions:write to required bot scopes The thinking indicator feature requires the reactions:write scope to add/remove the hourglass emoji. Without it, the bot works fine but the thinking indicator silently fails. Co-Authored-By: Claude Opus 4.6 --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 0d17539..3f111f0 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,7 @@ LOG_LEVEL="info" # Logging level (default: info) - `chat:write` — Post messages and thread replies - `channels:history` — Read messages in public channels - `groups:history` — Read messages in private channels (if needed) + - `reactions:write` — Add/remove emoji reactions (thinking indicator) 4. Subscribe to these **Events**: - `app_mention` — Trigger on @mentions - `message.channels` — Listen for thread replies