Skip to content

feat(slack): UI + API to change DM-default agent per workspace (#584) - #593

Merged
vybe merged 2 commits into
devfrom
feature/584-slack-dm-default-setter
Apr 30, 2026
Merged

feat(slack): UI + API to change DM-default agent per workspace (#584)#593
vybe merged 2 commits into
devfrom
feature/584-slack-dm-default-setter

Conversation

@pavshulin

Copy link
Copy Markdown
Contributor

Summary

Closes #584. Until now the Slack is_dm_default flag was auto-set on
the first agent connected to a workspace and unchangeable through
any UI or API — the only workaround was direct SQLite edit. Multi-agent
workspaces were stuck routing every DM to the first agent forever.

This PR adds the missing setter and tightens unbind semantics so the
workspace can never end up with no DM default.

Changes

Backend

  • db/slack_channels.py — new set_dm_default(team_id, agent_name) uses single-tx clear-then-set inside one BEGIN/COMMIT to avoid any window with two defaults (schema has no exclusivity constraint).
  • routers/slack.pyPUT /api/agents/{name}/slack/channel/dm-default: owner-gated via can_user_share_agent, audit-logged via AGENT_LIFECYCLE/slack_dm_default_changed (details: team_id, workspace_name, previous, new_default). Idempotent — returns status=unchanged if caller is already default.
  • routers/slack.pyDELETE /slack/channel now returns 409 if the target is the workspace's DM default and other agents are bound. Promoting a sibling first is the only path past the guard. When the target is the only agent, unbind is allowed (clean cascade).
  • routers/slack.py — GET response now includes workspace_agent_count so the UI can decide whether to disable Unbind.

Frontend

  • SlackChannelPanel.vue — replaces the read-only (DM default) label with a "Make default" button (when not default) or a "✓ DM default" badge (when default). Both carry a hover tooltip explaining DM-default semantics. Unbind button is disabled with an explanatory tooltip when this agent is the DM default and siblings remain.

Tests — 10 unit tests, all pass:

  • set_dm_default: returns False when agent unbound, sets exclusively, idempotent, isolated per workspace, doesn't touch siblings (5 tests)
  • unbind_agent: pure delete (does NOT auto-promote — router enforces guard), works on non-default + last-agent paths, unknown agent returns False (4 tests)
  • Plus migration/setup smoke (1 test)

Docs

  • feature-flows/slack-channel-routing.md — endpoint table, UI state, DM-default semantics
  • feature-flows.md index — Recent Updates row added
  • tests/registry.json — entry for the new test file

Live verification on local dev

  • ✅ DM routing flips immediately after PUT /dm-default (no restart, no cache — get_slack_dm_default_agent() queries DB on every call)
  • ✅ Channel @mention routing unaffected (channel binding wins over DM default)
  • ✅ 409 surfaces with the explanatory message when blocked
  • audit_log row written with correct actor_type=user, event_action=slack_dm_default_changed, full details
  • ✅ Verified end-to-end via direct call against SlackAdapter.get_agent_name() — flipping is_dm_default via API changes which agent is returned for an is_dm=True NormalizedMessage

Test plan

  • pytest tests/unit/test_slack_dm_default.py -v — 10/10 pass
  • Regression: pytest tests/unit/test_agent_shared_files_migration.py tests/unit/test_file_sharing_mixin.py tests/unit/test_file_download_no_session_gate.py -q — 39/39 pass
  • Manual: flip via API, verify routing changes, verify 409 guard, verify audit row written
  • Reviewer: /validate-pr 585 before merge
  • Reviewer: confirm UI behavior on the Sharing tab (button visible, tooltip readable, disabled state correct when this agent is DM default with siblings)

Out of scope (deferred)

  • MCP tool wrapping the same operation (issue marked it optional)
  • Per-channel routing customization beyond the DM default (existing channel bindings already cover that)
  • Migration away from the is_dm_default boolean schema

Related

🤖 Generated with Claude Code

Closes #584. Adds the missing setter for the Slack DM-default routing
flag — until now it was auto-set on the first agent ever connected and
unchangeable through any UI or API; the only workaround was direct DB
edit. Multi-agent workspaces were stuck routing every DM to the first
agent forever.

Backend:
- db/slack_channels.py — new `set_dm_default(team_id, agent_name)`
  uses single-tx clear-then-set inside one BEGIN/COMMIT to avoid
  any window with two defaults (schema has no exclusivity constraint).
- routers/slack.py — `PUT /api/agents/{name}/slack/channel/dm-default`,
  owner-gated via `can_user_share_agent`, audit-logged via
  `AGENT_LIFECYCLE/slack_dm_default_changed` (details: team_id,
  workspace_name, previous, new_default). Idempotent: returns
  status=unchanged when caller is already the default.
- routers/slack.py — DELETE /slack/channel now returns **409** if the
  target is the workspace's DM default and other agents are bound.
  When the target is the only agent, unbind is allowed (clean cascade).
  Promoting a sibling first is the only path past the guard — keeps the
  workspace from ending up with zero DM default.
- routers/slack.py — GET response includes `workspace_agent_count` so
  the UI can decide whether to disable Unbind.

Frontend:
- SlackChannelPanel.vue — replaces the read-only `(DM default)` label
  with a "Make default" button (when not default) or a "✓ DM default"
  badge (when default). Both carry a hover tooltip explaining what
  DM default means. Unbind button is disabled with an explanatory
  tooltip when this agent is the DM default and the workspace has
  other bound agents.

Tests (10 new, all pass):
- test_slack_dm_default.py — set_dm_default contract: returns False
  when agent unbound, sets exclusively, idempotent, isolated per
  workspace, doesn't touch siblings. unbind_agent contract: pure
  delete (does NOT auto-promote), works on non-default + last-agent
  paths, unknown agent returns False.

Live verification on dev:
- DM routing flips immediately after `PUT /dm-default` (no restart,
  no cache — `get_slack_dm_default_agent()` queries DB on every call)
- Channel @mention routing unaffected (channel binding wins over DM
  default)
- 409 surfaces with explanatory message when blocked
- audit_log row written with correct actor/details

Docs:
- feature-flows/slack-channel-routing.md — endpoint table, UI state
  description, and DM-default semantics updated
- feature-flows.md index — Recent Updates row added
- tests/registry.json — new entry

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Keep both the #584 slack-dm-default entry (HEAD) and the dev entries
(#598, #590, #364, #587, VOICE-001, file-upload) by including all rows.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@vybe
vybe merged commit c64dca2 into dev Apr 30, 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