Issue #6: File Explorer & File Operations
Epic: #1
Phase: 3 - Interactive Features
Estimated Time: 4-5 days
Priority: High
Depends On: #3, #5
Goal
Enable users to browse, view, edit, create, and delete project files directly from Mattermost using interactive dialogs and file operations.
Tasks
1. File Tree Dialog
Slash Command: /claude-files
Show file browser dialog:
func (p *Plugin) showFileBrowser(triggerID, sessionID string) error {
// Get file tree from bridge server
files, err := p.bridgeClient.ListFiles(sessionID)
if err != nil {
return err
}
// Build file tree options
options := make([]model.DialogElement, 0)
for _, file := range files {
options = append(options, model.DialogElement{
DisplayName: file.Path,
Name: "file",
Type: "select",
DataSource: "files",
})
}
dialog := model.OpenDialogRequest{
TriggerId: triggerID,
URL: p.getPluginURL() + "/api/dialog/file-action",
Dialog: model.Dialog{
Title: "Project Files",
Elements: options,
SubmitLabel: "Open",
},
}
return p.API.OpenInteractiveDialog(dialog)
}
2. File Actions Menu
After selecting a file, show actions:
type FileAction string
const (
FileActionView FileAction = "view"
FileActionEdit FileAction = "edit"
FileActionDelete FileAction = "delete"
FileActionDiff FileAction = "diff"
)
func (p *Plugin) showFileActions(triggerID, sessionID, filePath string) {
dialog := model.OpenDialogRequest{
TriggerId: triggerID,
URL: p.getPluginURL() + "/api/dialog/file-operation",
Dialog: model.Dialog{
Title: filepath.Base(filePath),
Elements: []model.DialogElement{
{
DisplayName: "Action",
Name: "action",
Type: "radio",
Options: []*model.PostActionOptions{
{Text: "👁️ View", Value: string(FileActionView)},
{Text: "✏️ Edit", Value: string(FileActionEdit)},
{Text: "🔍 Show Diff", Value: string(FileActionDiff)},
{Text: "🗑️ Delete", Value: string(FileActionDelete)},
},
},
},
SubmitLabel: "Execute",
},
}
p.API.OpenInteractiveDialog(dialog)
}
3. View File Content
Display file in code block:
func (p *Plugin) viewFile(channelID, sessionID, filePath string) error {
content, err := p.bridgeClient.GetFile(sessionID, filePath)
if err != nil {
return err
}
// Detect language for syntax highlighting
ext := filepath.Ext(filePath)
lang := getLanguageFromExtension(ext)
codeBlock := fmt.Sprintf("```%s\n%s\n```", lang, content)
attachment := &model.SlackAttachment{
Title: filePath,
Text: codeBlock,
Actions: []*model.PostAction{
{Name: "✏️ Edit", ...},
{Name: "📋 Copy", ...},
},
}
post := &model.Post{
ChannelId: channelID,
UserId: p.botUserID,
Message: "**File:** " + filePath,
Props: model.StringInterface{
"attachments": []*model.SlackAttachment{attachment},
},
}
_, err = p.API.CreatePost(post)
return err
}
4. Edit File Dialog
Open text editor dialog:
func (p *Plugin) showFileEditor(triggerID, sessionID, filePath string) error {
content, err := p.bridgeClient.GetFile(sessionID, filePath)
if err != nil {
return err
}
dialog := model.OpenDialogRequest{
TriggerId: triggerID,
URL: p.getPluginURL() + "/api/dialog/save-file",
Dialog: model.Dialog{
Title: "Edit: " + filepath.Base(filePath),
IntroductionText: "Editing " + filePath,
Elements: []model.DialogElement{
{
DisplayName: "Content",
Name: "content",
Type: "textarea",
Default: content,
MaxLength: 10000, // Mattermost limit
},
},
SubmitLabel: "Save",
},
}
return p.API.OpenInteractiveDialog(dialog)
}
Handle Save:
func (p *Plugin) handleSaveFile(w http.ResponseWriter, r *http.Request) {
submission := model.SubmitDialogRequestFromJson(r.Body)
filePath := submission.State // Stored from previous dialog
content := submission.Submission["content"].(string)
err := p.bridgeClient.UpdateFile(submission.ChannelId, filePath, content)
if err != nil {
response := &model.SubmitDialogResponse{
Error: "Failed to save file: " + err.Error(),
}
json.NewEncoder(w).Encode(response)
return
}
// Post confirmation
p.postBotMessage(submission.ChannelId, "✅ Saved changes to "+filePath)
response := &model.SubmitDialogResponse{}
json.NewEncoder(w).Encode(response)
}
5. Create New File
Slash Command: /claude-new-file
func (p *Plugin) showCreateFileDialog(triggerID, sessionID string) {
dialog := model.OpenDialogRequest{
TriggerId: triggerID,
URL: p.getPluginURL() + "/api/dialog/create-file",
Dialog: model.Dialog{
Title: "Create New File",
Elements: []model.DialogElement{
{
DisplayName: "File Path",
Name: "path",
Type: "text",
Placeholder: "src/components/MyComponent.tsx",
HelpText: "Relative to project root",
},
{
DisplayName: "Content",
Name: "content",
Type: "textarea",
Optional: true,
},
},
SubmitLabel: "Create",
},
}
p.API.OpenInteractiveDialog(dialog)
}
6. Show Diff
Display file changes:
func (p *Plugin) showFileDiff(channelID, sessionID, filePath string) error {
diff, err := p.bridgeClient.GetFileDiff(sessionID, filePath)
if err != nil {
return err
}
diffBlock := "```diff\n" + diff + "\n```"
attachment := &model.SlackAttachment{
Title: "Changes: " + filePath,
Text: diffBlock,
Color: "#FFAA00",
Actions: []*model.PostAction{
{Name: "✅ Apply", ...},
{Name: "❌ Discard", ...},
{Name: "💬 Discuss", ...},
},
}
post := &model.Post{
ChannelId: channelID,
UserId: p.botUserID,
Message: "**Uncommitted Changes**",
Props: model.StringInterface{
"attachments": []*model.SlackAttachment{attachment},
},
}
_, err = p.API.CreatePost(post)
return err
}
7. Delete File (with confirmation)
func (p *Plugin) confirmDeleteFile(triggerID, filePath string) {
dialog := model.OpenDialogRequest{
TriggerId: triggerID,
URL: p.getPluginURL() + "/api/dialog/confirm-delete",
Dialog: model.Dialog{
Title: "Delete File",
IntroductionText: fmt.Sprintf("⚠️ Are you sure you want to delete `%s`?\n\nThis cannot be undone.", filePath),
SubmitLabel: "Delete",
NotifyOnCancel: false,
},
}
p.API.OpenInteractiveDialog(dialog)
}
8. File Tree Component (React)
For future webapp enhancement:
// webapp/src/components/FileTree.tsx
interface FileNode {
name: string;
path: string;
type: 'file' | 'directory';
children?: FileNode[];
}
export const FileTree: React.FC<{ sessionId: string }> = ({ sessionId }) => {
const [files, setFiles] = useState<FileNode[]>([]);
useEffect(() => {
fetchFiles(sessionId).then(setFiles);
}, [sessionId]);
return (
<div className="file-tree">
{files.map(node => (
<FileTreeNode key={node.path} node={node} onSelect={handleFileSelect} />
))}
</div>
);
};
Bridge Server Endpoints
// GET /api/sessions/:id/files
router.get('/:id/files', async (req, res) => {
const files = await fileManager.listFiles(req.params.id);
res.json({ files });
});
// GET /api/sessions/:id/files/:path
router.get('/:id/files/:path', async (req, res) => {
const content = await fileManager.readFile(req.params.id, req.params.path);
res.json({ content });
});
// PUT /api/sessions/:id/files/:path
router.put('/:id/files/:path', async (req, res) => {
await fileManager.writeFile(req.params.id, req.params.path, req.body.content);
res.json({ success: true });
});
// POST /api/sessions/:id/files
router.post('/:id/files', async (req, res) => {
await fileManager.createFile(req.params.id, req.body.path, req.body.content);
res.json({ success: true });
});
// DELETE /api/sessions/:id/files/:path
router.delete('/:id/files/:path', async (req, res) => {
await fileManager.deleteFile(req.params.id, req.params.path);
res.json({ success: true });
});
File Structure
server/
├── file_operations.go # File operation handlers
├── file_browser.go # File browser dialog
└── types.go # File-related types
bridge-server/src/
├── file-manager.ts # File system operations
└── api/files.ts # File API endpoints
Security Considerations
func validateFilePath(projectPath, requestedPath string) error {
absPath := filepath.Join(projectPath, requestedPath)
// Prevent directory traversal
if !strings.HasPrefix(absPath, projectPath) {
return errors.New("invalid file path: outside project directory")
}
// Check file exists
if _, err := os.Stat(absPath); os.IsNotExist(err) {
return errors.New("file does not exist")
}
return nil
}
Testing
Unit Tests
Integration Tests
Acceptance Criteria
Related Issues
References
Issue #6: File Explorer & File Operations
Epic: #1
Phase: 3 - Interactive Features
Estimated Time: 4-5 days
Priority: High
Depends On: #3, #5
Goal
Enable users to browse, view, edit, create, and delete project files directly from Mattermost using interactive dialogs and file operations.
Tasks
1. File Tree Dialog
Slash Command:
/claude-filesShow file browser dialog:
2. File Actions Menu
After selecting a file, show actions:
3. View File Content
Display file in code block:
4. Edit File Dialog
Open text editor dialog:
Handle Save:
5. Create New File
Slash Command:
/claude-new-file6. Show Diff
Display file changes:
7. Delete File (with confirmation)
8. File Tree Component (React)
For future webapp enhancement:
Bridge Server Endpoints
File Structure
Security Considerations
Testing
Unit Tests
Integration Tests
Acceptance Criteria
/claude-filesRelated Issues
References