Skip to content

feat(slack): slash commands (Phase 3) + Block Kit interactions (Phase 2b)#60

Merged
jamiepine merged 1 commit intospacedriveapp:mainfrom
sookochoff:feat/slack-enhancements-pr2
Feb 19, 2026
Merged

feat(slack): slash commands (Phase 3) + Block Kit interactions (Phase 2b)#60
jamiepine merged 1 commit intospacedriveapp:mainfrom
sookochoff:feat/slack-enhancements-pr2

Conversation

@sookochoff
Copy link
Contributor

@sookochoff sookochoff commented Feb 19, 2026

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::Interaction variant (src/lib.rs)

Carries the result of a button click or select menu action:

  • action_id, block_id — identifies which block element was actioned
  • value, label — submitted value and human-readable label
  • message_ts — ts of the original message, for thread correlation

Slack-only inbound — Discord, Telegram, and Webhook never produce this variant. The Display impl renders it as [interaction: action_id → value] so every existing text-processing code path in channel.rs receives readable plain text with zero adaptation. Exhaustive match enforcement means no adapter was silently missed.

interaction_callback wired in Socket Mode

  • handle_interaction_event() registered via .with_interaction_events()
  • Only SlackInteractionEvent::BlockActions is converted to InboundMessage; all other types (view submission, shortcuts, etc.) are acknowledged and logged at debug — no silent failures
  • Each action in the payload becomes a separate InboundMessage so multi-action payloads are handled correctly
  • message_ts threaded through both slack_message_ts and slack_thread_ts metadata so the agent replies in the correct thread
  • trigger_id used as message ID (Slack guarantees uniqueness per interaction)

Phase 3 — Slash commands

SlackCommandConfig (src/config.rs)

New config type, added as Vec<SlackCommandConfig> on SlackConfig only. TOML syntax:

[[messaging.slack.commands]]
command = "/ask"
agent_id = "main"
description = "Ask the agent a question"

[[messaging.slack.commands]]
command = "/task"
agent_id = "main"
description = "Kick off a background task"

Fully backwards compatible — field defaults to empty vec. Existing configs need no changes. Discord, Telegram, and Webhook configs are untouched.

command_callback wired in Socket Mode

  • handle_command_event() registered via .with_command_events()
  • 3-second ack requirement met: returns immediately; the real reply arrives via the normal respond() path
  • Commands not in config get an ephemeral "not configured" reply — users get feedback instead of silence
  • Command forwarded as MessageContent::Text("/ask <args>") — familiar format for the agent
  • slack_command and slack_command_agent_id metadata keys allow per-command agent routing without requiring a separate binding entry per command

SlackAdapter::new() — new commands parameter

All four call sites updated (src/main.rs, src/api/messaging.rs, src/api/bindings.rs, src/config.rs). Commands converted to Arc<HashMap<String, String>> once at construction; read-only from callbacks.


What is not changed

  • Discord, Telegram, Webhook adapters — zero modifications
  • OutboundResponse enum — unchanged (interaction replies use existing Text/Ephemeral/RichMessage variants)
  • Binding resolution logic — unchanged; slack_command_agent_id metadata is advisory, existing bindings continue to work
  • All non-Slack MessageContent match sites — only two arms added, no existing arms modified

Test results

  • cargo check (lib + bin) — clean
  • cargo test --lib — 43/43 pass

@sookochoff sookochoff force-pushed the feat/slack-enhancements-pr2 branch 2 times, most recently from 4aab0e4 to e72fb01 Compare February 19, 2026 20:50
@sookochoff
Copy link
Contributor Author

Both issues fixed in the force-push. Walkthrough:

1. Channel filter bypass in app_mention — fixed

Added the missing channel filter block to handle_app_mention_event immediately after the workspace filter, matching the exact logic in handle_message_event:

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 SlackHyperClient and SlackApiToken on SlackAdapter itself, built once in new(). All six call sites (send_status, respond, broadcast, fetch_history, health_check, and start/auth_test) now call self.session() which does self.client.open_session(&self.token) — zero new allocations per call.

new() now returns anyhow::Result<Self> (the connector can fail to initialize) — all four call sites updated to handle the error with a tracing::error! log.

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
@sookochoff sookochoff force-pushed the feat/slack-enhancements-pr2 branch from e72fb01 to 0b829f8 Compare February 19, 2026 21:34
@jamiepine jamiepine merged commit 8e6b262 into spacedriveapp:main Feb 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

Comments