feat(slack): slash commands (Phase 3) + Block Kit interactions (Phase 2b)#60
Conversation
4aab0e4 to
e72fb01
Compare
|
Both issues fixed in the force-push. Walkthrough: 1. Channel filter bypass in app_mention — fixed Added the missing channel filter block to if let Some(allowed) = perms.channel_filter.get(&team_id_str) {
if !allowed.is_empty() && !allowed.contains(&channel_id) {
return Ok(());
}
}2. Fresh client per send_status call — fixed Cached the
The socket mode listener gets its own separate client instance, which is correct — it manages a persistent WebSocket connection internally and needs to own that client for the lifetime of the connection. REST calls and the socket mode connection use separate clients but now there is only ever one of each. |
…se 2b)
Completes the Slack connector enhancement plan from docs/PRD-slack-enhancements.md.
Carries the result of a button click or select menu action from Slack:
- action_id, block_id — identifies which block element was actioned
- value, label — the submitted value and human-readable label
- message_ts — ts of the original message, for correlation
Slack-only inbound; Discord/Telegram/Webhook never emit this variant.
Display impl renders it as '[interaction: action_id → value]' so the
LLM receives readable plain text in all existing channel.rs code paths.
- handle_interaction_event() registered via .with_interaction_events()
- Only SlackInteractionEvent::BlockActions is converted to InboundMessage;
all other interaction types (view_submission, shortcut, etc.) are acked
and logged at debug level — no silent failures
- Each action in the payload becomes a separate InboundMessage so multi-
action payloads are handled correctly (Slack allows >1 per event)
- message_ts threaded through both slack_message_ts and slack_thread_ts
metadata so the agent can reply in the correct thread
- trigger_id used as the message ID (guaranteed unique per interaction)
Both MessageContent match sites get an Interaction arm that delegates to
Display, keeping the existing text-processing pipeline intact. No change
to Discord/Telegram/Webhook code paths.
New config struct: command string + agent_id + optional description hint.
Added as Vec<SlackCommandConfig> on SlackConfig and TomlSlackConfig.
Deserialized from TOML:
[[messaging.slack.commands]]
command = "/ask"
agent_id = "main"
description = "Ask the agent a question"
Fully backwards compatible — field defaults to empty vec, existing configs
need no changes.
- handle_command_event() registered via .with_command_events()
- Acks Slack within the 3-second window by returning immediately; real
reply arrives via the normal respond() path
- Unknown commands (not in config) get an ephemeral 'not configured'
reply so users get feedback instead of silence
- Command text forwarded as MessageContent::Text("/ask <args>") so the
agent receives the full invocation in a familiar format
- slack_command and slack_command_agent_id metadata keys included so the
router can honour per-command agent routing without a separate binding
entry per command
src/api/messaging.rs, src/api/bindings.rs, src/config.rs)
Added Vec<SlackCommandConfig> parameter. All four call sites updated to
pass slack_config.commands. commands Arc<HashMap> built once at
construction, read-only from callbacks.
cargo check (lib + bin) — clean, zero new errors
cargo test --lib — 43/43 pass
e72fb01 to
0b829f8
Compare
Completes the Slack enhancement plan from
docs/PRD-slack-enhancements.md(shipped with PR #58).Depends on #58 (or rebases cleanly if that merges first).
Phase 2b — Block Kit interactive components (inbound)
New
MessageContent::Interactionvariant (src/lib.rs)Carries the result of a button click or select menu action:
action_id,block_id— identifies which block element was actionedvalue,label— submitted value and human-readable labelmessage_ts— ts of the original message, for thread correlationSlack-only inbound — Discord, Telegram, and Webhook never produce this variant. The
Displayimpl renders it as[interaction: action_id → value]so every existing text-processing code path inchannel.rsreceives readable plain text with zero adaptation. Exhaustive match enforcement means no adapter was silently missed.interaction_callbackwired in Socket Modehandle_interaction_event()registered via.with_interaction_events()SlackInteractionEvent::BlockActionsis converted toInboundMessage; all other types (view submission, shortcuts, etc.) are acknowledged and logged atdebug— no silent failuresInboundMessageso multi-action payloads are handled correctlymessage_tsthreaded through bothslack_message_tsandslack_thread_tsmetadata so the agent replies in the correct threadtrigger_idused as message ID (Slack guarantees uniqueness per interaction)Phase 3 — Slash commands
SlackCommandConfig(src/config.rs)New config type, added as
Vec<SlackCommandConfig>onSlackConfigonly. TOML syntax:Fully backwards compatible — field defaults to empty vec. Existing configs need no changes. Discord, Telegram, and Webhook configs are untouched.
command_callbackwired in Socket Modehandle_command_event()registered via.with_command_events()respond()pathMessageContent::Text("/ask <args>")— familiar format for the agentslack_commandandslack_command_agent_idmetadata keys allow per-command agent routing without requiring a separate binding entry per commandSlackAdapter::new()— newcommandsparameterAll four call sites updated (
src/main.rs,src/api/messaging.rs,src/api/bindings.rs,src/config.rs). Commands converted toArc<HashMap<String, String>>once at construction; read-only from callbacks.What is not changed
OutboundResponseenum — unchanged (interaction replies use existingText/Ephemeral/RichMessagevariants)slack_command_agent_idmetadata is advisory, existing bindings continue to workMessageContentmatch sites — only two arms added, no existing arms modifiedTest results
cargo check(lib + bin) — cleancargo test --lib— 43/43 pass