Issue #5: Interactive Message Components
Epic: #1
Phase: 3 - Interactive Features
Estimated Time: 4-5 days
Priority: High
Depends On: #4
Goal
Add interactive buttons and actions to bot messages, enabling users to approve/reject changes, confirm commands, and perform actions without typing slash commands.
Tasks
1. Message Attachments with Actions
Approve/Reject Buttons for code changes:
func (p *Plugin) postChangeProposal(channelID, content string, changeID string) {
attachment := &model.SlackAttachment{
Text: content,
Actions: []*model.PostAction{
{
Id: "approve_" + changeID,
Name: "✅ Approve",
Type: model.POST_ACTION_TYPE_BUTTON,
Style: "primary",
Integration: &model.PostActionIntegration{
URL: p.getPluginURL() + "/api/action/approve",
Context: map[string]interface{}{
"change_id": changeID,
},
},
},
{
Id: "reject_" + changeID,
Name: "❌ Reject",
Type: model.POST_ACTION_TYPE_BUTTON,
Style: "danger",
Integration: &model.PostActionIntegration{
URL: p.getPluginURL() + "/api/action/reject",
Context: map[string]interface{}{
"change_id": changeID,
},
},
},
{
Id: "modify_" + changeID,
Name: "✏️ Modify",
Type: model.POST_ACTION_TYPE_BUTTON,
Integration: &model.PostActionIntegration{
URL: p.getPluginURL() + "/api/action/modify",
Context: map[string]interface{}{
"change_id": changeID,
},
},
},
},
}
post := &model.Post{
ChannelId: channelID,
UserId: p.botUserID,
Message: "Claude Code has proposed changes:",
Props: model.StringInterface{
"attachments": []*model.SlackAttachment{attachment},
},
}
p.API.CreatePost(post)
}
2. Action Handlers (Go Backend)
Approve Action:
func (p *Plugin) handleApprove(w http.ResponseWriter, r *http.Request) {
request := model.PostActionIntegrationRequestFromJson(r.Body)
changeID := request.Context["change_id"].(string)
// Send approval to bridge server
err := p.bridgeClient.ApproveChange(request.ChannelId, changeID)
if err != nil {
p.writeError(w, err)
return
}
// Update post to show it was approved
response := &model.PostActionIntegrationResponse{
Update: &model.Post{
Message: "✅ Changes approved by " + request.UserId,
Props: model.StringInterface{
"from_bot": "true",
},
},
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}
Reject Action:
func (p *Plugin) handleReject(w http.ResponseWriter, r *http.Request) {
request := model.PostActionIntegrationRequestFromJson(r.Body)
changeID := request.Context["change_id"].(string)
// Send rejection to bridge server
err := p.bridgeClient.RejectChange(request.ChannelId, changeID)
if err != nil {
p.writeError(w, err)
return
}
response := &model.PostActionIntegrationResponse{
Update: &model.Post{
Message: "❌ Changes rejected by " + request.UserId,
},
}
json.NewEncoder(w).Encode(response)
}
Modify Action (opens dialog):
func (p *Plugin) handleModify(w http.ResponseWriter, r *http.Request) {
request := model.PostActionIntegrationRequestFromJson(r.Body)
changeID := request.Context["change_id"].(string)
dialog := model.OpenDialogRequest{
TriggerId: request.TriggerId,
URL: p.getPluginURL() + "/api/dialog/modify-change",
Dialog: model.Dialog{
Title: "Modify Request",
Elements: []model.DialogElement{
{
DisplayName: "Modification Instructions",
Name: "instructions",
Type: "textarea",
Placeholder: "Tell Claude Code how to modify the changes...",
},
},
SubmitLabel: "Send",
},
}
p.API.OpenInteractiveDialog(dialog)
response := &model.PostActionIntegrationResponse{}
json.NewEncoder(w).Encode(response)
}
3. Quick Action Buttons
Add common action shortcuts:
func (p *Plugin) postWithQuickActions(channelID, content string) {
actions := []*model.PostAction{
{
Id: "continue",
Name: "▶️ Continue",
Type: model.POST_ACTION_TYPE_BUTTON,
Integration: &model.PostActionIntegration{
URL: p.getPluginURL() + "/api/action/continue",
},
},
{
Id: "explain",
Name: "💡 Explain",
Type: model.POST_ACTION_TYPE_BUTTON,
Integration: &model.PostActionIntegration{
URL: p.getPluginURL() + "/api/action/explain",
},
},
{
Id: "undo",
Name: "↩️ Undo",
Type: model.POST_ACTION_TYPE_BUTTON,
Style: "danger",
Integration: &model.PostActionIntegration{
URL: p.getPluginURL() + "/api/action/undo",
},
},
}
// ... create post with actions
}
4. Confirmation Dialogs
For destructive actions:
func (p *Plugin) confirmAction(triggerID, action, message string) {
dialog := model.OpenDialogRequest{
TriggerId: triggerID,
URL: p.getPluginURL() + "/api/dialog/confirm",
Dialog: model.Dialog{
Title: "Confirm Action",
IntroductionText: message,
SubmitLabel: "Confirm",
NotifyOnCancel: false,
},
}
p.API.OpenInteractiveDialog(dialog)
}
5. Code Preview with Actions
Show code diff with inline actions:
func (p *Plugin) postCodeChange(channelID, filename, diff string, changeID string) {
codeBlock := "```diff\n" + diff + "\n```"
post := &model.Post{
ChannelId: channelID,
UserId: p.botUserID,
Message: fmt.Sprintf("📝 **%s**\n\n%s", filename, codeBlock),
Props: model.StringInterface{
"attachments": []*model.SlackAttachment{
{
Actions: []*model.PostAction{
{Name: "✅ Apply", ...},
{Name: "❌ Discard", ...},
{Name: "👁️ View Full File", ...},
},
},
},
},
}
p.API.CreatePost(post)
}
6. Loading States
Show progress for long-running operations:
func (p *Plugin) updatePostWithProgress(postID, status string) {
post, err := p.API.GetPost(postID)
if err != nil {
return
}
post.Message = "⏳ " + status
p.API.UpdatePost(post)
}
// Example usage:
postID := p.postBotMessage(channelID, "⏳ Processing your request...")
p.bridgeClient.ProcessCommand(sessionID, command)
p.updatePostWithProgress(postID, "⏳ Generating code...")
// ... operation completes
p.updatePost(postID, "✅ Complete!")
7. Inline Actions Menu
Dropdown menu for multiple options:
func (p *Plugin) postWithMenu(channelID, content string, options []ActionOption) {
menuOptions := make([]*model.PostActionOptions, len(options))
for i, opt := range options {
menuOptions[i] = &model.PostActionOptions{
Text: opt.Label,
Value: opt.Value,
}
}
action := &model.PostAction{
Id: "action_menu",
Name: "Actions",
Type: model.POST_ACTION_TYPE_SELECT,
Integration: &model.PostActionIntegration{
URL: p.getPluginURL() + "/api/action/menu",
},
Options: menuOptions,
}
// ... create post with action
}
Bridge Server Updates
Add endpoints to support actions:
// POST /api/sessions/:id/approve
router.post('/:id/approve', async (req, res) => {
const { changeId } = req.body;
// Send approval signal to Claude Code CLI
await cliManager.approveChange(req.params.id, changeId);
res.json({ success: true });
});
// POST /api/sessions/:id/reject
router.post('/:id/reject', async (req, res) => {
const { changeId } = req.body;
await cliManager.rejectChange(req.params.id, changeId);
res.json({ success: true });
});
File Structure
server/
├── actions.go # Interactive action handlers
├── dialogs.go # Dialog handlers
├── post_utils.go # Helper functions for posts
└── types.go # Action types and structs
Testing
Unit Tests
Integration Tests
Acceptance Criteria
Related Issues
References
Issue #5: Interactive Message Components
Epic: #1
Phase: 3 - Interactive Features
Estimated Time: 4-5 days
Priority: High
Depends On: #4
Goal
Add interactive buttons and actions to bot messages, enabling users to approve/reject changes, confirm commands, and perform actions without typing slash commands.
Tasks
1. Message Attachments with Actions
Approve/Reject Buttons for code changes:
2. Action Handlers (Go Backend)
Approve Action:
Reject Action:
Modify Action (opens dialog):
3. Quick Action Buttons
Add common action shortcuts:
4. Confirmation Dialogs
For destructive actions:
5. Code Preview with Actions
Show code diff with inline actions:
6. Loading States
Show progress for long-running operations:
7. Inline Actions Menu
Dropdown menu for multiple options:
Bridge Server Updates
Add endpoints to support actions:
File Structure
Testing
Unit Tests
Integration Tests
Acceptance Criteria
Related Issues
References