Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/bender/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
31 changes: 29 additions & 2 deletions src/bender/slack_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand All @@ -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", "")
Expand All @@ -41,19 +59,23 @@ 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,
workspace=settings.bender_workspace,
session_id=session_id,
timeout=settings.bender_timeout,
)
await _post_response(say, response.result, thread_ts)
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"):
Expand All @@ -74,19 +96,24 @@ 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,
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:
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:
Expand Down