Skip to content

Interactive Message Components #5

@ada-evorada

Description

@ada-evorada

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

  • Action button creation
  • Action handler routing
  • Dialog submission
  • Post updates

Integration Tests

  • Click approve button → changes applied
  • Click reject button → changes discarded
  • Modify dialog → sends instructions to CLI
  • Quick actions work correctly

Acceptance Criteria

  • Bot messages include interactive buttons
  • Approve/Reject buttons work for code changes
  • Modify button opens dialog with text area
  • Quick action buttons trigger appropriate commands
  • Post updates show action results
  • Loading states display during operations
  • Confirmation dialogs work for destructive actions
  • All actions are mobile-friendly

Related Issues

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions